// ============================================================ // useDirectoryScan — 目录扫描 Hook // // 根据 selectedProject / selectedEpisode / selectedShot / currentStage // 智能选择扫描策略: // - Assets 阶段 → scanAssetFiles(深度递归扫描 assets/) // - 非 Assets 阶段 → scanStageFiles(扫描 shots/{ep}/{shot}/{stage}/) // // 防抖 300ms,窗口聚焦时自动重扫。 // ============================================================ import { useEffect, useRef, useCallback } from 'react'; import { logger } from '@/shared/utils/bus'; import { useCanvasStore } from '../stores/useCanvasStore'; export function useDirectoryScan() { // 选择性订阅 const workspaceRoot = useCanvasStore((s) => s.workspaceRoot); const selectedProject = useCanvasStore((s) => s.selectedProject); const selectedEpisode = useCanvasStore((s) => s.selectedEpisode); const selectedShot = useCanvasStore((s) => s.selectedShot); const currentStage = useCanvasStore((s) => s.currentStage); const assetTypeFilter = useCanvasStore((s) => s.assetTypeFilter); const keywordQuery = useCanvasStore((s) => s.keywordQuery); const tagQuery = useCanvasStore((s) => s.tagQuery); const clearItems = useCanvasStore((s) => s.clearItems); const appendItems = useCanvasStore((s) => s.appendItems); const setIsScanning = useCanvasStore((s) => s.setIsScanning); const setScanProgress = useCanvasStore((s) => s.setScanProgress); const getDataSource = useCanvasStore((s) => s.getDataSource); const getProjectPath = useCanvasStore((s) => s.getProjectPath); // 扫描 abort 控制器 ref const abortRef = useRef(null); // 防抖 timer const debounceRef = useRef | null>(null); // 当前 epoch const epochRef = useRef(0); const doScan = useCallback(async () => { const projectPath = getProjectPath(); if (!projectPath) { console.log('[useDirectoryScan] ⏸️ 跳过扫描:projectPath 为空(未选择项目)'); clearItems(); return; } // 取消之前的扫描 abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; const epoch = ++epochRef.current; const isAssets = currentStage === 'Assets'; console.log(`[useDirectoryScan] 🔍 开始扫描 (epoch=${epoch})`, { projectPath, stage: currentStage, isAssets, selectedEpisode: selectedEpisode || '(未选)', selectedShot: selectedShot || '(未选)', }); if (!isAssets && (!selectedEpisode || !selectedShot)) { console.log('[useDirectoryScan] ⏸️ 跳过扫描:非 Assets 阶段需先选择集数和镜头'); clearItems(); return; } const t0 = performance.now(); setIsScanning(true); setScanProgress({ found: 0, currentPath: '' }); try { const dataSource = getDataSource(); let entries: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>; if (isAssets) { // Assets 阶段:深度递归扫描 entries = await dataSource.scanAssetFiles({ projectPath, assetTypeFilter, }); } else { // 非 Assets 阶段:扫描指定镜头的阶段目录 entries = await dataSource.scanStageFiles({ projectPath, episode: selectedEpisode!, shot: selectedShot!, stage: currentStage, }); } // Epoch 检查 if (epoch !== epochRef.current) { console.log(`[useDirectoryScan] ⚠️ epoch 过期 (${epoch} → ${epochRef.current}),丢弃结果`); return; } if (controller.signal.aborted) { console.log('[useDirectoryScan] ⚠️ 扫描已被 abort,丢弃结果'); return; } const elapsed = (performance.now() - t0).toFixed(0); console.log(`[useDirectoryScan] ✅ 扫描完成 (epoch=${epoch}, 耗时=${elapsed}ms)`, { totalEntries: entries.length, }); clearItems(); appendItems(entries); setScanProgress(null); } catch (err) { if (controller.signal.aborted || epoch !== epochRef.current) return; logger.warn('canvas', '目录扫描失败', err instanceof Error ? err : new Error(String(err))); } finally { if (epoch === epochRef.current) { setIsScanning(false); } } }, [ selectedProject, selectedEpisode, selectedShot, currentStage, assetTypeFilter, clearItems, appendItems, setIsScanning, setScanProgress, getDataSource, getProjectPath, ]); // 防抖触发扫描 useEffect(() => { if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { void doScan(); }, 100); return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, [doScan, workspaceRoot, keywordQuery, tagQuery]); // 窗口聚焦时重新扫描 useEffect(() => { const handleFocus = () => { setTimeout(() => { void doScan(); }, 500); }; window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [doScan]); return { rescan: doScan }; }