v0.3.4: Bug修复+性能优化+文档大纲功能

🔴 Bug修复 (6):
- Sidebar反斜杠正则修复 (Windows路径规范化)
- FileWatcher文件删除后自动恢复监听
- SearchReplace replaceAll防过期匹配位置
- openFolderDialog null结果守卫
- 保存异常时空路径保护
- Markdown绝对路径图片拼接修复

🟡 性能优化:
- Editor切换标签时内容比较,相同跳过replaceAll

🔵 代码质量:
- 创建共享常量模块 src/shared/constants.ts,消除双副本

🎯 新增功能:
- 文档大纲 (Table of Contents) — 侧边栏标题导航

🔧 换行符统一: CRLF → LF
This commit is contained in:
thzxx
2026-06-04 13:24:23 +08:00
parent efa7f61809
commit 899d2b7914
28 changed files with 2657 additions and 2374 deletions
@@ -1,108 +1,108 @@
import React, { useEffect, useRef, useCallback } from 'react'
interface ConfirmDialogProps {
open: boolean
title: string
message: string
confirmLabel?: string
cancelLabel?: string
variant?: 'danger' | 'warning' | 'info'
onConfirm: () => void
onCancel: () => void
}
export const ConfirmDialog = React.memo(function ConfirmDialog({
open,
title,
message,
confirmLabel = '确定',
cancelLabel = '取消',
variant = 'warning',
onConfirm,
onCancel
}: ConfirmDialogProps) {
const confirmRef = useRef<HTMLButtonElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
// 打开时保存焦点并聚焦确认按钮;关闭时恢复焦点
useEffect(() => {
if (!open) {
previousFocusRef.current?.focus()
return
}
previousFocusRef.current = document.activeElement as HTMLElement
const timer = setTimeout(() => confirmRef.current?.focus(), 50)
return () => {
clearTimeout(timer)
}
}, [open])
// ESC 键关闭
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault()
onCancel()
}
}, [onCancel])
useEffect(() => {
if (!open) return
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [open, handleKeyDown])
// 防止背景滚动
useEffect(() => {
if (!open) return
const original = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = original }
}, [open])
if (!open) return null
return (
<div
className="confirm-overlay"
onClick={onCancel}
role="presentation"
>
<div
className="confirm-dialog"
role="alertdialog"
aria-modal="true"
aria-labelledby="confirm-title"
aria-describedby="confirm-message"
onClick={e => e.stopPropagation()}
>
<div className={`confirm-header confirm-${variant}`}>
<h3 id="confirm-title">{title}</h3>
</div>
<div className="confirm-body">
<p id="confirm-message">{message}</p>
</div>
<div className="confirm-actions">
<button
className="confirm-btn confirm-btn-cancel"
onClick={onCancel}
type="button"
>
{cancelLabel}
</button>
<button
ref={confirmRef}
className={`confirm-btn confirm-btn-${variant}`}
onClick={onConfirm}
type="button"
>
{confirmLabel}
</button>
</div>
</div>
</div>
)
})
ConfirmDialog.displayName = 'ConfirmDialog'
import React, { useEffect, useRef, useCallback } from 'react'
interface ConfirmDialogProps {
open: boolean
title: string
message: string
confirmLabel?: string
cancelLabel?: string
variant?: 'danger' | 'warning' | 'info'
onConfirm: () => void
onCancel: () => void
}
export const ConfirmDialog = React.memo(function ConfirmDialog({
open,
title,
message,
confirmLabel = '确定',
cancelLabel = '取消',
variant = 'warning',
onConfirm,
onCancel
}: ConfirmDialogProps) {
const confirmRef = useRef<HTMLButtonElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
// 打开时保存焦点并聚焦确认按钮;关闭时恢复焦点
useEffect(() => {
if (!open) {
previousFocusRef.current?.focus()
return
}
previousFocusRef.current = document.activeElement as HTMLElement
const timer = setTimeout(() => confirmRef.current?.focus(), 50)
return () => {
clearTimeout(timer)
}
}, [open])
// ESC 键关闭
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault()
onCancel()
}
}, [onCancel])
useEffect(() => {
if (!open) return
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [open, handleKeyDown])
// 防止背景滚动
useEffect(() => {
if (!open) return
const original = document.body.style.overflow
document.body.style.overflow = 'hidden'
return () => { document.body.style.overflow = original }
}, [open])
if (!open) return null
return (
<div
className="confirm-overlay"
onClick={onCancel}
role="presentation"
>
<div
className="confirm-dialog"
role="alertdialog"
aria-modal="true"
aria-labelledby="confirm-title"
aria-describedby="confirm-message"
onClick={e => e.stopPropagation()}
>
<div className={`confirm-header confirm-${variant}`}>
<h3 id="confirm-title">{title}</h3>
</div>
<div className="confirm-body">
<p id="confirm-message">{message}</p>
</div>
<div className="confirm-actions">
<button
className="confirm-btn confirm-btn-cancel"
onClick={onCancel}
type="button"
>
{cancelLabel}
</button>
<button
ref={confirmRef}
className={`confirm-btn confirm-btn-${variant}`}
onClick={onConfirm}
type="button"
>
{confirmLabel}
</button>
</div>
</div>
</div>
)
})
ConfirmDialog.displayName = 'ConfirmDialog'