Files
ele-HeiXiu/src/modules/canvas/hooks/useDirectoryScan.ts
YoungestSongMo 337d6c1205 feat: 构建系统重构 + VCHSM 架构迁移 + 右键菜单修复 + 尺寸参数统一
## 构建、打包与分发系统重构 (build/)

- 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本

- electron-builder 配置从 package.json 提取为 build/electron-builder.js

- 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数

- OSS 路径按渠道隔离

- 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验

- 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成

- updater.ts 发送 channel/edition/deviceFingerprint

- 新增 src/shared/utils/rollout.ts 灰度测试工具

- 新增 10 条 NPM 脚本

## VCHSM 五层架构迁移

- 7 个业务模块 + ~500 处导入路径同步

## 修复

- MediaCardContextMenu 改用共享 ContextMenu 组件

- ComboBox 尺寸参数显示统一 (size-format.ts)

## 文档

- UpdateA.md / build/README.md / README.md / .env.example 更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 18:41:07 +08:00

165 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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<AbortController | null>(null);
// 防抖 timer
const debounceRef = useRef<ReturnType<typeof setTimeout> | 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 };
}