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:
2026-06-29 18:41:07 +08:00
parent e0d91335c7
commit 337d6c1205
187 changed files with 5252 additions and 1702 deletions

View 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 };
}