CRITICAL 修复: - 路径遍历防护绕过:validatePath在normalize()之后检查..,可被绕过读取任意文件 - 编辑器异步创建竞态条件:editor.create()完成后组件已卸载导致内存泄漏 - editor.create() Promise缺少.catch() → 未捕获的Promise拒绝 - handleOpenRecent空文件被跳过:result.content truthy检查阻塞空文件打开 MAJOR 修复: - DIR_WATCH/TAB_SWITCHED IPC缺少路径校验,可被用于监视任意文件 - AboutDialog内联箭头函数使React.memo完全失效 - ConfirmDialog关闭时焦点恢复逻辑反转 - markdown.ts一刀切屏蔽..导致合法相对路径图片不显示 安全加固: - 移除未被使用的path.normalize导入 - 所有文件操作IPC使用validatePath统一校验 - 编辑器初始化添加cancelled标志防止卸载后设置实例 验证: TypeScript零错误, ESLint零错误零警告, 78/78测试通过
109 lines
2.7 KiB
TypeScript
109 lines
2.7 KiB
TypeScript
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'
|