v0.3.10: 全面优化增强 — 17项Bug修复/稳定性/功能改进

P0 致命Bug修复:
- A1: openFolderDialog 类型/运行时崩溃(文件夹打开功能完全失效)
- A2: SourceEditor 受控textarea手动改DOM反模式
- A3: tabStore.updateTabContent 内容相同时误标isModified

死代码清理:
- B1: 删除menu:*/save/as/viewMode 三个永不触发的IPC通道

健壮性修复:
- E1: rehypeFixImages 路径规范化+防越界加固
- E2: getFileName 尾斜杠返回正确文件名
- E3: EditorToolbar 行内代码按钮改用toggleInlineCodeCommand
- E4: ErrorBoundary 内联样式抽为CSS类

功能增强:
- D1: 标签页拖拽排序(tabStore.moveTab + TabBar DnD + CSS)
- D2: Milkdown自动配对括号/引号(ProseMirror插件)
- D3: 状态栏自动保存开关可点击
- D4: 文档大纲活跃标题高亮(useActiveHeading hook)
- D5: 关闭窗口前flush防抖数据(flushSaveToDB)
- D6: 搜索替换支持正则表达式

类型/架构:
- C2: preload类型集中定义(ElectronAPI契约)
- __pycache__ typo修复

文档/版本:
- README/DESIGN同步为Milkdown + v0.3.10
- 项目结构树/技术栈/快捷键表更新

验证: typecheck(仅7预存), lint, test 96/96, vite build
This commit is contained in:
2026-06-23 10:47:45 +08:00
parent 7f070eb11d
commit b65e34e288
27 changed files with 639 additions and 252 deletions
+82
View File
@@ -0,0 +1,82 @@
import { useEffect, useRef, useState, useCallback } from 'react'
/**
* D4: 基于视口的活跃标题追踪 hook
* 在 preview 模式下监听目标容器的滚动事件,
* 根据文档中标题元素的 offsetTop 判断当前可见的标题索引。
*
* 仅在目标容器 ref 存在时生效(preview 面板挂载后)。
*
* @param containerRef - 包含 Markdown 渲染结果的 DOM 元素 ref
* @param headings - 解析出的标题列表
* @returns - 当前活跃标题的索引(null 表示无法判定或不在范围内)
*/
export function useActiveHeading(
containerRef: React.RefObject<HTMLElement | null>,
headings: { level: number; text: string }[]
): number | null {
const [activeIndex, setActiveIndex] = useState<number | null>(null)
const observerRef = useRef<IntersectionObserver | null>(null)
const handleScroll = useCallback(() => {
const container = containerRef.current
if (!container || headings.length === 0) {
setActiveIndex(null)
return
}
// 收集容器内所有 h1-h6 元素的 offsetTop
const headingElements = Array.from(
container.querySelectorAll('h1, h2, h3, h4, h5, h6')
) as HTMLElement[]
if (headingElements.length === 0) {
setActiveIndex(null)
return
}
const scrollTop = container.scrollTop
const containerHeight = container.clientHeight
const threshold = scrollTop + containerHeight * 0.3 // 上方 30% 位置视为"到达"
let bestIndex: number | null = null
for (let i = 0; i < headingElements.length; i++) {
const el = headingElements[i]
// 使用容器顶部的相对偏移而非 getBoundingClientRect(滚动容器不是 window
const top = el.offsetTop - (container.offsetTop || 0)
if (top <= threshold) {
// 找到 headings 中匹配的索引
const text = el.textContent?.trim() ?? ''
const matchIdx = headings.findIndex(
h => h.text.trim() === text && el.tagName.slice(-1) === String(h.level)
)
if (matchIdx >= 0) bestIndex = matchIdx
}
}
setActiveIndex(bestIndex)
}, [containerRef, headings])
useEffect(() => {
const container = containerRef.current
if (!container || headings.length === 0) {
setActiveIndex(null)
return
}
// 监听滚动事件
container.addEventListener('scroll', handleScroll, { passive: true })
// 初始计算
handleScroll()
const observer = observerRef.current
return () => {
container.removeEventListener('scroll', handleScroll)
observer?.disconnect()
}
}, [containerRef, headings, handleScroll])
return activeIndex
}