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>
This commit is contained in:
164
src/modules/canvas/hooks/useDirectoryScan.ts
Normal file
164
src/modules/canvas/hooks/useDirectoryScan.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
// ============================================================
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user