v0.3.7: 全面代码审计修复 - 20项Bug/安全/稳定性改进
This commit is contained in:
@@ -10,8 +10,9 @@ export function useAutoExpandDir(activeFilePath: string | null) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeFilePath || !rootPath) return
|
||||
const norm = (p: string) => p.replace(/\\/g, '/')
|
||||
if (!norm(activeFilePath).startsWith(norm(rootPath))) return
|
||||
const norm = (p: string) => p.replace(/[/\\]+$/, '').replace(/\\/g, '/')
|
||||
const normalizedRoot = norm(rootPath) + '/'
|
||||
if (!norm(activeFilePath).startsWith(normalizedRoot)) return
|
||||
|
||||
const dirsToExpand: string[] = []
|
||||
let dir = activeFilePath.replace(/[/\\][^/\\]+$/, '')
|
||||
|
||||
@@ -12,6 +12,9 @@ const AUTO_SAVE_DELAY = 2000
|
||||
* Uses `useTabStore.getState()` and `.subscribe()` so it doesn't need to
|
||||
* re-render on every keystroke — the effect is triggered once and runs
|
||||
* reactively via the store subscription.
|
||||
*
|
||||
* Captures the tabId at debounce start so the timeout always saves the
|
||||
* correct tab even if the user switches tabs during the debounce window.
|
||||
*/
|
||||
export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean; toggleAutoSave: () => void } {
|
||||
const [isAutoSaving, setIsAutoSaving] = useState(false)
|
||||
@@ -19,6 +22,7 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const isSavingRef = useRef(false)
|
||||
const enabledRef = useRef(true)
|
||||
const mountedRef = useRef(true)
|
||||
|
||||
const toggleAutoSave = useCallback(() => {
|
||||
setAutoSaveEnabled(prev => {
|
||||
@@ -33,6 +37,8 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true
|
||||
|
||||
// Subscribe to Zustand store — fires on every state change
|
||||
const unsub = useTabStore.subscribe((state) => {
|
||||
if (!enabledRef.current) return
|
||||
@@ -40,6 +46,9 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
const tab = state.getActiveTab()
|
||||
if (!tab || !tab.filePath || !tab.isModified) return
|
||||
|
||||
// Capture the tabId so the timeout saves the correct tab
|
||||
const tabIdToSave = tab.id
|
||||
|
||||
// Debounce: clear previous timer, start new one
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
@@ -48,33 +57,34 @@ export function useAutoSave(): { isAutoSaving: boolean; autoSaveEnabled: boolean
|
||||
timerRef.current = setTimeout(async () => {
|
||||
if (isSavingRef.current) return
|
||||
|
||||
// Re-read latest state inside the timeout callback
|
||||
// Re-read latest state; verify the captured tab still exists and is modified
|
||||
const currentState = useTabStore.getState()
|
||||
const currentTab = currentState.getActiveTab()
|
||||
if (!currentTab || !currentTab.filePath || !currentTab.isModified) return
|
||||
const tabToSave = currentState.tabs.find(t => t.id === tabIdToSave)
|
||||
if (!tabToSave || !tabToSave.filePath || !tabToSave.isModified) return
|
||||
|
||||
isSavingRef.current = true
|
||||
setIsAutoSaving(true)
|
||||
if (mountedRef.current) setIsAutoSaving(true)
|
||||
|
||||
try {
|
||||
if (!window.electronAPI) return
|
||||
const result = await window.electronAPI.saveFile({
|
||||
filePath: currentTab.filePath,
|
||||
content: currentTab.content
|
||||
filePath: tabToSave.filePath,
|
||||
content: tabToSave.content
|
||||
})
|
||||
if (result.success) {
|
||||
currentState.setModified(currentTab.id, false)
|
||||
if (result.success && mountedRef.current) {
|
||||
currentState.setModified(tabToSave.id, false)
|
||||
}
|
||||
} catch (error) {
|
||||
logError('自动保存失败', error)
|
||||
} finally {
|
||||
setIsAutoSaving(false)
|
||||
if (mountedRef.current) setIsAutoSaving(false)
|
||||
isSavingRef.current = false
|
||||
}
|
||||
}, AUTO_SAVE_DELAY)
|
||||
})
|
||||
|
||||
return () => {
|
||||
mountedRef.current = false
|
||||
unsub()
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
|
||||
@@ -16,7 +16,9 @@ export function useDragDrop(showToast: (msg: string) => void) {
|
||||
|
||||
let rejected = 0
|
||||
for (const file of Array.from(files)) {
|
||||
const filePath: string = (file as File & { path?: string }).path || file.name
|
||||
// Electron adds a `path` property to File objects
|
||||
const electronFile = file as File & { path?: string }
|
||||
const filePath: string = electronFile.path || file.name
|
||||
if (!isAllowedFile(filePath)) {
|
||||
rejected++
|
||||
continue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import type { ToastItem, ToastType } from '../components/Toast/Toast'
|
||||
|
||||
/** 默认自动消失时间 (ms) */
|
||||
@@ -7,16 +7,18 @@ const DEFAULT_DURATION = 3000
|
||||
/** 最大同时显示数量 */
|
||||
const MAX_TOASTS = 5
|
||||
|
||||
// Module-level counter survives component remounts
|
||||
let toastCounter = 0
|
||||
|
||||
/**
|
||||
* 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 id = `toast-${++toastCounter}`
|
||||
const newToast: ToastItem = { id, message: msg, type, duration }
|
||||
|
||||
setToasts(prev => {
|
||||
|
||||
Reference in New Issue
Block a user