v0.2.0: 全面代码质量优化

- ESLint flat config + Prettier + EditorConfig
- Markdown处理器LRU缓存
- Zustand选择器优化减少重渲染
- CodeMirror Compartment主题热切换
- Preview防抖(150ms) + IndexedDB去抖(500ms)
- 组件拆分: App.tsx 310→104行, Sidebar.tsx 244→90行
- 统一错误处理 errorHandler.ts
- ConfirmDialog替代原生confirm
- LoadingSpinner加载状态
- Toast多条堆叠+类型区分
- 可访问性增强(ARIA属性、键盘导航)
- Vitest测试框架(78个测试用例)
- Git hooks(husky + lint-staged)
- 项目文档(README.md, CONTRIBUTING.md)
This commit is contained in:
thzxx
2026-06-03 22:13:32 +08:00
parent 3cecb0f9eb
commit 7a4e2b0a67
72 changed files with 5103 additions and 894 deletions
@@ -0,0 +1,112 @@
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 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) return
previousFocusRef.current = document.activeElement as HTMLElement
const timer = setTimeout(() => confirmRef.current?.focus(), 50)
return () => clearTimeout(timer)
}, [open])
// 关闭时恢复焦点
useEffect(() => {
if (open) return
return () => {
previousFocusRef.current?.focus()
}
}, [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'