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:
@@ -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'
|
||||
Reference in New Issue
Block a user