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,26 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSidebarStore } from '../stores/sidebarStore'
|
||||
|
||||
/**
|
||||
* AR-02: 自动展开到活动文件所在的目录
|
||||
*/
|
||||
export function useAutoExpandDir(activeFilePath: string | null) {
|
||||
const rootPath = useSidebarStore(s => s.rootPath)
|
||||
const expandDirs = useSidebarStore(s => s.expandDirs)
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeFilePath || !rootPath) return
|
||||
const norm = (p: string) => p.replace(/\\/g, '/')
|
||||
if (!norm(activeFilePath).startsWith(norm(rootPath))) return
|
||||
|
||||
const dirsToExpand: string[] = []
|
||||
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '')
|
||||
let prev = ''
|
||||
while (dir && dir.length >= rootPath.length && dir !== rootPath && dir !== prev) {
|
||||
prev = dir
|
||||
dirsToExpand.push(dir)
|
||||
dir = dir.replace(/[/\\][^/\\]+$/, '')
|
||||
}
|
||||
if (dirsToExpand.length > 0) expandDirs(dirsToExpand)
|
||||
}, [activeFilePath, rootPath, expandDirs])
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
|
||||
interface ConfirmOptions {
|
||||
title: string
|
||||
message: string
|
||||
confirmLabel?: string
|
||||
cancelLabel?: string
|
||||
variant?: 'danger' | 'warning' | 'info'
|
||||
}
|
||||
|
||||
interface ConfirmState extends ConfirmOptions {
|
||||
open: boolean
|
||||
resolve: ((value: boolean) => void) | null
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-01: Promise-based 确认对话框 hook
|
||||
* 替代原生 confirm(),返回 Promise<boolean>
|
||||
*/
|
||||
export function useConfirm() {
|
||||
const [state, setState] = useState<ConfirmState>({
|
||||
open: false,
|
||||
title: '',
|
||||
message: '',
|
||||
resolve: null
|
||||
})
|
||||
|
||||
// 使用 ref 确保回调中能拿到最新的 resolve
|
||||
const resolveRef = useRef<((value: boolean) => void) | null>(null)
|
||||
|
||||
const confirm = useCallback((options: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveRef.current = resolve
|
||||
setState({
|
||||
open: true,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
confirmLabel: options.confirmLabel,
|
||||
cancelLabel: options.cancelLabel,
|
||||
variant: options.variant,
|
||||
resolve
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
resolveRef.current?.(true)
|
||||
setState(prev => ({ ...prev, open: false, resolve: null }))
|
||||
resolveRef.current = null
|
||||
}, [])
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
resolveRef.current?.(false)
|
||||
setState(prev => ({ ...prev, open: false, resolve: null }))
|
||||
resolveRef.current = null
|
||||
}, [])
|
||||
|
||||
return {
|
||||
confirm,
|
||||
confirmDialogProps: {
|
||||
open: state.open,
|
||||
title: state.title,
|
||||
message: state.message,
|
||||
confirmLabel: state.confirmLabel,
|
||||
cancelLabel: state.cancelLabel,
|
||||
variant: state.variant,
|
||||
onConfirm: handleConfirm,
|
||||
onCancel: handleCancel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useCallback } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { isAllowedFile } from '../lib/fileUtils'
|
||||
import { MAX_FILE_SIZE } from '../lib/constants'
|
||||
import { logError } from '../lib/errorHandler'
|
||||
|
||||
export function useDragDrop(showToast: (msg: string) => void) {
|
||||
const createTab = useTabStore(s => s.createTab)
|
||||
@@ -15,7 +16,7 @@ export function useDragDrop(showToast: (msg: string) => void) {
|
||||
|
||||
let rejected = 0
|
||||
for (const file of Array.from(files)) {
|
||||
const filePath = (file as File & { path?: string }).path || file.name
|
||||
const filePath: string = (file as File & { path?: string }).path || file.name
|
||||
if (!isAllowedFile(filePath)) {
|
||||
rejected++
|
||||
continue
|
||||
@@ -31,12 +32,12 @@ export function useDragDrop(showToast: (msg: string) => void) {
|
||||
if (result.success && result.content !== undefined) {
|
||||
createTab(filePath, result.content)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to read file via electronAPI:', err)
|
||||
} catch (error) {
|
||||
logError('拖拽读取文件失败', error)
|
||||
}
|
||||
} else {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (ev) => {
|
||||
reader.onload = (ev: ProgressEvent<FileReader>) => {
|
||||
const content = ev.target?.result as string
|
||||
createTab(file.name, content)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { recentFilesRepository } from '../db/recentFilesRepository'
|
||||
import { logError } from '../lib/errorHandler'
|
||||
import type { ToastType } from '../components/Toast/Toast'
|
||||
|
||||
/**
|
||||
* AR-01: 从 App.tsx 提取的文件操作逻辑
|
||||
* UX-02: 添加 loading 状态指示
|
||||
*/
|
||||
export function useFileOperations(showToast: (msg: string, type?: ToastType) => void) {
|
||||
const createTab = useTabStore(s => s.createTab)
|
||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||
const setLoading = useEditorStore(s => s.setLoading)
|
||||
|
||||
const handleOpenFile = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
if (!window.electronAPI) return
|
||||
setLoading('file-open', true)
|
||||
const result = await window.electronAPI.openFile()
|
||||
if (result && 'filePath' in result) {
|
||||
createTab(result.filePath, result.content)
|
||||
if (result.filePath) recentFilesRepository.add(result.filePath)
|
||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||
}
|
||||
} catch (error) {
|
||||
logError('打开文件失败', error)
|
||||
showToast('打开文件失败', 'error')
|
||||
} finally {
|
||||
setLoading('file-open', false)
|
||||
}
|
||||
}, [createTab, showToast, setLoading])
|
||||
|
||||
const handleSave = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const tab = getActiveTab()
|
||||
if (!tab || !window.electronAPI) return
|
||||
const result = await window.electronAPI.saveFile({
|
||||
filePath: tab.filePath,
|
||||
content: tab.content
|
||||
})
|
||||
if (result.success) {
|
||||
useTabStore.getState().setModified(tab.id, false)
|
||||
showToast('已保存', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
logError('保存文件失败', error)
|
||||
showToast('保存失败', 'error')
|
||||
}
|
||||
}, [getActiveTab, showToast])
|
||||
|
||||
const handleSaveAs = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const tab = getActiveTab()
|
||||
if (!tab || !window.electronAPI) return
|
||||
const result = await window.electronAPI.saveFileAs({ content: tab.content })
|
||||
if (result.success) {
|
||||
useTabStore.getState().setModified(tab.id, false)
|
||||
showToast('已保存', 'success')
|
||||
}
|
||||
} catch (error) {
|
||||
logError('另存为失败', error)
|
||||
showToast('另存为失败', 'error')
|
||||
}
|
||||
}, [getActiveTab, showToast])
|
||||
|
||||
const handleOpenRecent = useCallback(async (filePath: string): Promise<void> => {
|
||||
if (!window.electronAPI) return
|
||||
setLoading('file-open', true)
|
||||
try {
|
||||
const result = await window.electronAPI.readFile(filePath)
|
||||
if (result.success && result.content) {
|
||||
createTab(filePath, result.content)
|
||||
recentFilesRepository.add(filePath)
|
||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||
}
|
||||
} finally {
|
||||
setLoading('file-open', false)
|
||||
}
|
||||
}, [createTab, setLoading])
|
||||
|
||||
return { handleOpenFile, handleSave, handleSaveAs, handleOpenRecent }
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
|
||||
/**
|
||||
* AR-03: 文件外部修改检测
|
||||
* 使用 Zustand store 状态替代 DOM CustomEvent
|
||||
*/
|
||||
export function useFileWatch() {
|
||||
const getActiveTab = useTabStore(s => s.getActiveTab)
|
||||
const setExternallyModified = useEditorStore(s => s.setExternallyModified)
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) return
|
||||
@@ -10,10 +16,11 @@ export function useFileWatch() {
|
||||
const unsubscribe = window.electronAPI.onExternalModification((filePath: string) => {
|
||||
const tab = getActiveTab()
|
||||
if (tab && tab.filePath === filePath) {
|
||||
window.dispatchEvent(new CustomEvent('file-externally-modified', { detail: filePath }))
|
||||
// AR-03: 直接更新 store 状态,不再派发 CustomEvent
|
||||
setExternallyModified({ filePath })
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [getActiveTab])
|
||||
}, [getActiveTab, setExternallyModified])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useSidebarStore } from '../stores/sidebarStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
|
||||
/**
|
||||
* AR-02: 从 Sidebar.tsx 提取的文件夹操作逻辑
|
||||
* UX-02: 添加目录加载 loading 状态
|
||||
*/
|
||||
export function useFolderOperations() {
|
||||
const rootPath = useSidebarStore(s => s.rootPath)
|
||||
const setRootPath = useSidebarStore(s => s.setRootPath)
|
||||
const setTree = useSidebarStore(s => s.setTree)
|
||||
const expandDirs = useSidebarStore(s => s.expandDirs)
|
||||
const setLoading = useEditorStore(s => s.setLoading)
|
||||
|
||||
const handleOpenFolder = useCallback(async () => {
|
||||
if (!window.electronAPI) return
|
||||
const result = await window.electronAPI.openFolderDialog()
|
||||
if (!result.canceled && result.filePaths[0]) {
|
||||
const dirPath = result.filePaths[0]
|
||||
setRootPath(dirPath)
|
||||
expandDirs([dirPath])
|
||||
setLoading('dir-load', true)
|
||||
try {
|
||||
const dirTree = await window.electronAPI.readDirTree(dirPath)
|
||||
if (dirTree.success && dirTree.tree) {
|
||||
setTree(dirTree.tree)
|
||||
window.electronAPI.watchDir(dirPath)
|
||||
}
|
||||
} finally {
|
||||
setLoading('dir-load', false)
|
||||
}
|
||||
}
|
||||
}, [setRootPath, setTree, expandDirs, setLoading])
|
||||
|
||||
const refreshTree = useCallback(async () => {
|
||||
if (!rootPath || !window.electronAPI) return
|
||||
setLoading('dir-load', true)
|
||||
try {
|
||||
const result = await window.electronAPI.readDirTree(rootPath)
|
||||
if (result.success && result.tree) setTree(result.tree)
|
||||
} finally {
|
||||
setLoading('dir-load', false)
|
||||
}
|
||||
}, [rootPath, setTree, setLoading])
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) return
|
||||
const unsubscribe = window.electronAPI.onDirChanged(() => refreshTree())
|
||||
return unsubscribe
|
||||
}, [refreshTree])
|
||||
|
||||
return { handleOpenFolder }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { recentFilesRepository } from '../db/recentFilesRepository'
|
||||
|
||||
/**
|
||||
* 主进程事件注册 hook
|
||||
* 处理菜单触发的文件打开、保存、另存为事件
|
||||
*/
|
||||
export function useIpcListeners(handleSave: () => void, handleSaveAs: () => void) {
|
||||
const createTab = useTabStore(s => s.createTab)
|
||||
const handleSaveRef = useRef(handleSave)
|
||||
const handleSaveAsRef = useRef(handleSaveAs)
|
||||
handleSaveRef.current = handleSave
|
||||
handleSaveAsRef.current = handleSaveAs
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) return
|
||||
const api = window.electronAPI
|
||||
const onOpen = (data: { filePath: string; content: string }) => {
|
||||
createTab(data.filePath, data.content)
|
||||
if (data.filePath) recentFilesRepository.add(data.filePath)
|
||||
setTimeout(() => useTabStore.getState().saveToDB(), 100)
|
||||
}
|
||||
const unsub1 = api.onFileOpenInTab(onOpen)
|
||||
const unsub2 = api.onMenuSave(() => handleSaveRef.current())
|
||||
const unsub3 = api.onMenuSaveAs(() => handleSaveAsRef.current())
|
||||
return () => { unsub1(); unsub2(); unsub3() }
|
||||
}, [createTab])
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import type { ViewMode } from '../types/settings'
|
||||
|
||||
export function useKeyboard(handleOpenFile: () => void, handleSave: () => void, handleSaveAs: () => void) {
|
||||
const setViewMode = useEditorStore(s => s.setViewMode)
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { useEffect, useCallback } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { settingsRepository } from '../db/settingsRepository'
|
||||
import type { ViewMode } from '../types/settings'
|
||||
|
||||
/**
|
||||
* AR-04: 设置 hook
|
||||
* 不再独立加载设置(由 useSettingsInit 统一加载)
|
||||
* 仅负责视图模式的读取和保存
|
||||
*/
|
||||
export function useSettings() {
|
||||
const viewMode = useEditorStore(s => s.viewMode)
|
||||
const setViewMode = useEditorStore(s => s.setViewMode)
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
settingsRepository.load().then(settings => {
|
||||
setViewMode(settings.viewMode ?? 'editor')
|
||||
}).catch(err => console.error('Failed to load settings:', err))
|
||||
}, [setViewMode])
|
||||
|
||||
// M-10: 使用 useCallback 稳定函数引用
|
||||
const saveViewMode = useCallback((mode: ViewMode) => {
|
||||
setViewMode(mode)
|
||||
settingsRepository.save({ viewMode: mode })
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { useSidebarStore } from '../stores/sidebarStore'
|
||||
import { settingsRepository } from '../db/settingsRepository'
|
||||
import { logError } from '../lib/errorHandler'
|
||||
|
||||
/**
|
||||
* AR-04: 统一设置加载 hook
|
||||
* 一次性从 IndexedDB 加载所有设置,分发到各 store
|
||||
* 替代 useTheme、useSettings、sidebarStore.loadFromDB 各自独立加载的模式
|
||||
*/
|
||||
export function useSettingsInit() {
|
||||
const setDarkMode = useEditorStore(s => s.setDarkMode)
|
||||
const setViewMode = useEditorStore(s => s.setViewMode)
|
||||
const isInitialized = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) return
|
||||
isInitialized.current = true
|
||||
|
||||
settingsRepository.load()
|
||||
.then((settings) => {
|
||||
// 主题
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const darkMode = settings.darkMode !== undefined ? settings.darkMode : prefersDark
|
||||
setDarkMode(darkMode)
|
||||
document.documentElement.classList.toggle('dark', darkMode)
|
||||
|
||||
// 视图模式
|
||||
setViewMode(settings.viewMode ?? 'editor')
|
||||
|
||||
// Sidebar 设置(直接分发,避免 sidebarStore 再次读取 IndexedDB)
|
||||
const sidebarStore = useSidebarStore.getState()
|
||||
if (!sidebarStore._loaded) {
|
||||
useSidebarStore.setState({
|
||||
isVisible: !settings.sidebarCollapsed,
|
||||
sidebarWidth: settings.sidebarWidth,
|
||||
_loaded: true
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
logError('加载设置失败', error)
|
||||
})
|
||||
}, [setDarkMode, setViewMode])
|
||||
|
||||
return { isInitialized }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
/**
|
||||
* AR-02: 从 Sidebar.tsx 提取的 resize 逻辑
|
||||
*/
|
||||
export function useSidebarResize() {
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const sidebarRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResizing) return
|
||||
|
||||
const handleMouseMove = (e: MouseEvent): void => {
|
||||
if (sidebarRef.current) {
|
||||
const newWidth = Math.max(180, Math.min(500, e.clientX))
|
||||
sidebarRef.current.style.width = newWidth + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = (): void => setIsResizing(false)
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}, [isResizing])
|
||||
|
||||
const startResize = (): void => setIsResizing(true)
|
||||
|
||||
return { sidebarRef, isResizing, startResize }
|
||||
}
|
||||
@@ -1,29 +1,21 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEditorStore } from '../stores/editorStore'
|
||||
import { settingsRepository } from '../db/settingsRepository'
|
||||
|
||||
/**
|
||||
* AR-04: 主题 hook
|
||||
* 不再独立加载设置(由 useSettingsInit 统一加载)
|
||||
* 仅负责主题切换时的同步和保存
|
||||
*/
|
||||
export function useTheme() {
|
||||
const darkMode = useEditorStore(s => s.darkMode)
|
||||
const setDarkMode = useEditorStore(s => s.setDarkMode)
|
||||
const isInitialized = useRef(false)
|
||||
const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
|
||||
|
||||
// B-05: 从 IndexedDB 加载主题设置
|
||||
// darkMode 变化时同步 class 并保存
|
||||
useEffect(() => {
|
||||
settingsRepository.load().then(settings => {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
setDarkMode(settings.darkMode !== undefined ? settings.darkMode : prefersDark)
|
||||
isInitialized.current = true
|
||||
}).catch(err => console.error('Failed to load theme settings:', err))
|
||||
}, [setDarkMode])
|
||||
|
||||
// B-05: 首次加载完成后才保存主题设置
|
||||
useEffect(() => {
|
||||
if (!isInitialized.current) return
|
||||
document.documentElement.classList.toggle('dark', darkMode)
|
||||
settingsRepository.save({ darkMode })
|
||||
}, [darkMode])
|
||||
|
||||
const toggleDarkMode = useEditorStore(s => s.toggleDarkMode)
|
||||
|
||||
return { darkMode, toggleDarkMode }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import type { ToastItem, ToastType } from '../components/Toast/Toast'
|
||||
|
||||
/** 默认自动消失时间 (ms) */
|
||||
const DEFAULT_DURATION = 3000
|
||||
|
||||
/** 最大同时显示数量 */
|
||||
const MAX_TOASTS = 5
|
||||
|
||||
/**
|
||||
* UX-05: 升级版 Toast 状态管理 hook
|
||||
* 支持多条堆叠、类型区分、自动消失
|
||||
*/
|
||||
export function useToast() {
|
||||
const [toasts, setToasts] = useState<ToastItem[]>([])
|
||||
const counterRef = useRef(0)
|
||||
|
||||
const showToast = useCallback((msg: string, type: ToastType = 'info', duration = DEFAULT_DURATION) => {
|
||||
const id = `toast-${++counterRef.current}`
|
||||
const newToast: ToastItem = { id, message: msg, type, duration }
|
||||
|
||||
setToasts(prev => {
|
||||
const next = [...prev, newToast]
|
||||
// 超过最大数量时移除最旧的
|
||||
if (next.length > MAX_TOASTS) {
|
||||
return next.slice(next.length - MAX_TOASTS)
|
||||
}
|
||||
return next
|
||||
})
|
||||
|
||||
return id
|
||||
}, [])
|
||||
|
||||
const dismissToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id))
|
||||
}, [])
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
setToasts([])
|
||||
}, [])
|
||||
|
||||
return { toasts, showToast, dismissToast, cleanup }
|
||||
}
|
||||
@@ -1,6 +1,24 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useCallback, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 未保存提醒 hook
|
||||
* UX-01: 接受外部 confirm 函数,替代原生 confirm()
|
||||
*/
|
||||
export function useUnsavedWarning(
|
||||
hasUnsaved: () => boolean,
|
||||
confirmFn?: (message: string) => Promise<boolean>
|
||||
) {
|
||||
const confirmFnRef = useRef(confirmFn)
|
||||
confirmFnRef.current = confirmFn
|
||||
|
||||
const doConfirm = useCallback(async (message: string): Promise<boolean> => {
|
||||
if (confirmFnRef.current) {
|
||||
return confirmFnRef.current(message)
|
||||
}
|
||||
// 后备方案:如果没有提供 confirmFn,使用 beforeunload 行为
|
||||
return true
|
||||
}, [])
|
||||
|
||||
export function useUnsavedWarning(hasUnsaved: () => boolean) {
|
||||
useEffect(() => {
|
||||
if (!window.electronAPI) {
|
||||
const handler = (e: BeforeUnloadEvent) => {
|
||||
@@ -15,12 +33,12 @@ export function useUnsavedWarning(hasUnsaved: () => boolean) {
|
||||
|
||||
const api = window.electronAPI
|
||||
|
||||
const unsubscribe = api.onConfirmClose(() => {
|
||||
const unsubscribe = api.onConfirmClose(async () => {
|
||||
if (!hasUnsaved()) {
|
||||
api.forceClose()
|
||||
return
|
||||
}
|
||||
const shouldClose = confirm('有文件尚未保存,确定要关闭吗?')
|
||||
const shouldClose = await doConfirm('有文件尚未保存,确定要关闭吗?')
|
||||
if (shouldClose) {
|
||||
api.forceClose()
|
||||
} else {
|
||||
@@ -29,5 +47,5 @@ export function useUnsavedWarning(hasUnsaved: () => boolean) {
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
}, [hasUnsaved])
|
||||
}, [hasUnsaved, doConfirm])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user