From aabf7a8ecf08e9f73d38d6346f3b574d28228aea Mon Sep 17 00:00:00 2001 From: Agions <1051736049@qq.com> Date: Thu, 4 Jun 2026 15:39:46 +0800 Subject: [PATCH] chore(dead-code): phase 2 trim pages shim cascade and packages/common monorepo --- jest.config.cjs | 4 + packages/common/README-before-after.md | 165 ------ packages/common/package.json | 16 - .../components/ui/ConfirmDialog.module.css | 73 --- .../src/components/ui/ConfirmDialog.tsx | 116 ----- .../src/components/ui/FileUploader.module.css | 115 ---- .../common/src/components/ui/FileUploader.tsx | 292 ----------- .../common/src/components/ui/Modal.module.css | 77 --- packages/common/src/components/ui/Modal.tsx | 100 ---- .../src/components/ui/ProgressBar.module.css | 102 ---- .../common/src/components/ui/ProgressBar.tsx | 104 ---- .../common/src/domain/character/validators.ts | 65 --- packages/common/src/domain/index.ts | 27 - .../common/src/domain/scene/validators.ts | 43 -- .../common/src/domain/script/validators.ts | 186 ------- packages/common/src/domain/shared/types.ts | 14 - packages/common/src/formatters/index.ts | 160 ------ packages/common/src/hooks/index.ts | 490 ------------------ packages/common/src/index.ts | 32 +- packages/common/src/motion/index.ts | 131 ----- packages/common/src/utils/index.ts | 269 ---------- src/__tests__/pages/project-detail.test.tsx | 3 +- src/__tests__/pages/project-edit.test.tsx | 2 +- src/app/router/page-preload.ts | 12 +- src/core/router/page-preload.ts | 14 +- src/pages/AutoPipeline/AutoPipelinePage.tsx | 2 - src/pages/ProjectDetail/ProjectDetailPage.tsx | 2 - src/pages/ProjectEdit/ProjectEditPage.tsx | 2 - src/pages/ProjectEdit/ScriptDetailPage.tsx | 2 - src/pages/index.ts | 2 - 30 files changed, 29 insertions(+), 2593 deletions(-) delete mode 100644 packages/common/README-before-after.md delete mode 100644 packages/common/src/components/ui/ConfirmDialog.module.css delete mode 100644 packages/common/src/components/ui/ConfirmDialog.tsx delete mode 100644 packages/common/src/components/ui/FileUploader.module.css delete mode 100644 packages/common/src/components/ui/FileUploader.tsx delete mode 100644 packages/common/src/components/ui/Modal.module.css delete mode 100644 packages/common/src/components/ui/Modal.tsx delete mode 100644 packages/common/src/components/ui/ProgressBar.module.css delete mode 100644 packages/common/src/components/ui/ProgressBar.tsx delete mode 100644 packages/common/src/domain/character/validators.ts delete mode 100644 packages/common/src/domain/index.ts delete mode 100644 packages/common/src/domain/scene/validators.ts delete mode 100644 packages/common/src/domain/script/validators.ts delete mode 100644 packages/common/src/domain/shared/types.ts delete mode 100644 packages/common/src/formatters/index.ts delete mode 100644 packages/common/src/hooks/index.ts delete mode 100644 packages/common/src/motion/index.ts delete mode 100644 packages/common/src/utils/index.ts delete mode 100644 src/pages/AutoPipeline/AutoPipelinePage.tsx delete mode 100644 src/pages/ProjectDetail/ProjectDetailPage.tsx delete mode 100644 src/pages/ProjectEdit/ProjectEditPage.tsx delete mode 100644 src/pages/ProjectEdit/ScriptDetailPage.tsx delete mode 100644 src/pages/index.ts diff --git a/jest.config.cjs b/jest.config.cjs index 2d98387a..48d6b71d 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -20,6 +20,10 @@ module.exports = { '/src/__tests__/utils/test-utils.tsx', '/src/__tests__/__mocks__/@tauri-apps/api-tauri.ts', '/src/__tests__/__mocks__/@tauri-apps/api-core.ts', + // ProjectEdit/ProjectDetail page tests skipped: useForm() in ProjectEditPage + // expects AntD-style tuple return [form] but actual RHF useForm returns object + // — pre-existing bug (NOTE comment "intentional until form refactor"). + // Awaiting dedicated form refactor before re-enabling. '/src/__tests__/pages/project-edit.test.tsx', '/src/__tests__/pages/project-detail.test.tsx', '/src/__tests__/core/api/client.test.ts', diff --git a/packages/common/README-before-after.md b/packages/common/README-before-after.md deleted file mode 100644 index f4217c3d..00000000 --- a/packages/common/README-before-after.md +++ /dev/null @@ -1,165 +0,0 @@ -/\*\* - -- Before/After Comparison — AudioEditor.tsx DRY 改造 -- This file demonstrates the DRY transformation -- (illustration only, not executable) - \*/ - -// ================================================================ -// BEFORE (repetitive across 5 files) -// ================================================================ - -// AudioEditor.tsx — 重复片段 A -const handleAudioUpload = async (files: File[]) => { -for (const file of files) { -if (file.size > 50 _ 1024 _ 1024) { -toast.error(`文件 ${file.name} 超过50MB`); -continue; -} -const reader = new FileReader(); -reader.onload = () => { -const audio = new Audio(reader.result as string); -audio.onloadedmetadata = () => { -setTracks(prev => [...prev, { -id: crypto.randomUUID(), // ← 重复的 generateId -name: file.name.replace(/\.[^/.]+$/, ''), -duration: audio.duration, -}]); -}; -}; -reader.readAsDataURL(file); -} -}; - -// VideoEditor.tsx — 重复片段 B(几乎相同) -const handleVideoUpload = async (files: File[]) => { -for (const file of files) { -if (file.size > 200 _ 1024 _ 1024) { // ← 只是 maxSize 不同 -toast.error(`文件 ${file.name} 超过200MB`); -continue; -} -const reader = new FileReader(); -reader.onload = () => { -const video = document.createElement('video'); -video.onloadedmetadata = () => { -setVideos(prev => [...prev, { -id: crypto.randomUUID(), // ← 重复的 generateId -name: file.name.replace(/\.[^/.]+$/, ''), -duration: video.duration, -}]); -}; -}; -reader.readAsDataURL(file); -} -}; - -// ImageEditor.tsx — 重复片段 C(几乎相同) -const handleImageUpload = async (files: File[]) => { -for (const file of files) { -if (file.size > 10 _ 1024 _ 1024) { // ← 只是 maxSize 不同 -toast.error(`文件 ${file.name} 超过10MB`); -continue; -} -// ... 重复的上传逻辑 -} -}; - -// ================================================================ -// AFTER (zero duplication — uses @frame-fab/common) -// ================================================================ - -import { -FileUploader, // 统一文件上传 -ProgressBar, // 统一进度条 -ConfirmDialog, // 统一确认框 -generateId, // 统一 ID 生成 -formatDuration, // 统一时间格式化 -formatFileSize, // 统一文件大小格式化 -validateFileSize, // 统一文件大小校验 -detectFileType, // 统一文件类型检测 -readFileAsDataURL, // 统一文件读取 -} from '@frame-fab/common'; - -import { -useStepNavigation, -useLocalStorage, -useDebounce, -} from '@frame-fab/common/hooks'; - -import { -validateCharacterName, -validateScriptTitle, -} from '@frame-fab/common/domain/script/validators'; - -// ✅ AudioEditor: 配置化,0 重复上传逻辑 -const AudioEditor: React.FC = ({ onTracksChange }) => ( - { -onTracksChange(files.map(f => ({ -id: f.uid, // FileUploader 生成的稳定 UID -name: f.name, -url: f.url, -duration: 0, -}))); -}} -/> -); - -// ✅ VideoEditor: 同样的组件,只是 maxSize 不同 -const VideoEditor: React.FC = ({ onVideosChange }) => ( - { -onVideosChange(files.map(f => ({ -id: f.uid, -name: f.name, -url: f.url, -duration: 0, -}))); -}} -/> -); - -// ✅ 统一的校验 -const characterNameValidation = validateCharacterName('小明'); -if (!characterNameValidation.valid) { -// 集中的错误处理 -console.error(characterNameValidation.errors); -} - -// ✅ 统一的时间格式化 -formatDuration(90.5, { hours: 'never', ms: 1 }); // "01:30.5" - -// ✅ 统一的步骤导航 -const { currentStepId, goNext, goPrev, progress } = useStepNavigation({ -steps: [ -{ id: 'creative', label: '创意' }, -{ id: 'script', label: '剧本', isCompleted: true }, -{ id: 'asset', label: '资产' }, -{ id: 'storyboard', label: '分镜', isLocked: true }, -{ id: 'composite', label: '合成', isLocked: true }, -], -}); - -// ================================================================ -// ESLint 规则确保零重复 -// ================================================================ - -// .eslintrc.cjs 中配置: -// -// 'sonarjs/no-duplicate': ['error', { -// ignoreLiterals: true, -// ignoreStrings: false, -// }], -// -// CI 中运行: -// npm run lint → 任何重复都会被 ESLint 捕获并报错 -// -// 阈值设置为 0(不能有任何重复) diff --git a/packages/common/package.json b/packages/common/package.json index a01edabb..b950900a 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -10,22 +10,6 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" - }, - "./utils": { - "import": "./dist/utils/index.js", - "types": "./dist/utils/index.d.ts" - }, - "./components": { - "import": "./dist/components/index.js", - "types": "./dist/components/index.d.ts" - }, - "./hooks": { - "import": "./dist/hooks/index.js", - "types": "./dist/hooks/index.d.ts" - }, - "./domain/*": { - "import": "./dist/domain/*/index.js", - "types": "./dist/domain/*/index.d.ts" } }, "scripts": { diff --git a/packages/common/src/components/ui/ConfirmDialog.module.css b/packages/common/src/components/ui/ConfirmDialog.module.css deleted file mode 100644 index 2ec0abe3..00000000 --- a/packages/common/src/components/ui/ConfirmDialog.module.css +++ /dev/null @@ -1,73 +0,0 @@ -/* ConfirmDialog CSS Module */ - -.body { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--space-4, 16px); - padding: var(--space-4, 16px) 0; -} - -.message { - font-size: var(--font-size-base, 16px); - color: var(--color-text-primary, #111); - text-align: center; - margin: 0; - line-height: 1.6; -} - -.iconInfo { color: var(--color-semantic-info, #3b82f6); } -.iconSuccess { color: var(--color-semantic-success, #10b981); } -.iconWarning { color: var(--color-semantic-warning, #f59e0b); } -.iconError { color: var(--color-semantic-error, #ef4444); } - -.footer { - display: flex; - justify-content: flex-end; - gap: var(--space-3, 12px); - width: 100%; -} - -.cancelBtn, -.okBtn { - padding: var(--space-2, 8px) var(--space-5, 20px); - border-radius: var(--radius-md, 8px); - font-size: var(--font-size-sm, 14px); - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s ease, opacity 0.2s ease; -} - -.cancelBtn { - background: var(--color-bg-subtle, #f3f4f6); - border: 1px solid var(--color-border-default, #e5e7eb); - color: var(--color-text-primary, #111); -} - -.cancelBtn:hover:not(:disabled) { - background: var(--color-bg-elevated, #fff); -} - -.okBtn { - background: var(--color-interactive-primary, #3b82f6); - border: none; - color: #fff; -} - -.okBtn:hover:not(:disabled) { - background: var(--color-primary-600, #2563eb); -} - -.okBtn.destructive { - background: var(--color-semantic-error, #ef4444); -} - -.okBtn.destructive:hover:not(:disabled) { - background: var(--color-error-600, #dc2626); -} - -.cancelBtn:disabled, -.okBtn:disabled { - opacity: 0.5; - cursor: not-allowed; -} \ No newline at end of file diff --git a/packages/common/src/components/ui/ConfirmDialog.tsx b/packages/common/src/components/ui/ConfirmDialog.tsx deleted file mode 100644 index 6025de04..00000000 --- a/packages/common/src/components/ui/ConfirmDialog.tsx +++ /dev/null @@ -1,116 +0,0 @@ -/** - * ConfirmDialog — 确认对话框(基于 Modal 封装,DRY版) - * - * 改造前:每个需要确认的地方都写了大量 Dialog JSX - * 改造后:ConfirmDialog 统一处理 info/success/warning/error/confirm 五种类型 - */ - -import React, { useState, useCallback } from 'react'; -import { AlertCircle, CheckCircle, Info, AlertTriangle } from 'lucide-react'; -import { Modal } from './Modal'; -import styles from './ConfirmDialog.module.css'; - -export type ConfirmType = 'info' | 'success' | 'warning' | 'error' | 'confirm'; - -export interface ConfirmDialogProps { - open: boolean; - type?: ConfirmType; - title?: string; - message?: string; - okText?: string; - cancelText?: string; - onOk?: () => void | Promise; - onCancel?: () => void; - loading?: boolean; - maskClosable?: boolean; - closable?: boolean; - width?: number | string; - centered?: boolean; - destroyOnClose?: boolean; - className?: string; -} - -const iconMap: Record = { - info: , - success: , - warning: , - error: , - confirm: , -}; - -export function ConfirmDialog({ - open, - type = 'confirm', - title, - message, - okText = '确定', - cancelText = '取消', - onOk, - onCancel, - loading = false, - maskClosable = false, - closable = true, - width = 420, - centered = true, - destroyOnClose = true, - className, -}: ConfirmDialogProps) { - const [localLoading, setLocalLoading] = useState(false); - - const handleOk = useCallback(async () => { - if (!onOk) return; - setLocalLoading(true); - try { - const result = onOk(); - if (result instanceof Promise) await result; - } finally { - setLocalLoading(false); - } - }, [onOk]); - - const titleText = title ?? ( - type === 'info' ? '提示' : - type === 'success' ? '成功' : - type === 'warning' ? '警告' : - type === 'error' ? '错误' : '确认操作' - ); - - return ( - {})} - title={titleText} - width={width} - centered={centered} - maskClosable={maskClosable} - closable={closable} - destroyOnClose={destroyOnClose} - className={className} - footer={ -
- - -
- } - > -
- {iconMap[type]} -

{message}

-
-
- ); -} - -export default ConfirmDialog; \ No newline at end of file diff --git a/packages/common/src/components/ui/FileUploader.module.css b/packages/common/src/components/ui/FileUploader.module.css deleted file mode 100644 index 742f3c45..00000000 --- a/packages/common/src/components/ui/FileUploader.module.css +++ /dev/null @@ -1,115 +0,0 @@ -/* FileUploader CSS Module */ - -.root { - display: flex; - flex-direction: column; - gap: var(--space-3, 12px); -} - -.zone { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-2, 8px); - padding: var(--space-8, 32px); - border: 2px dashed var(--color-border-default, #e0e0e0); - border-radius: var(--radius-lg, 12px); - background: var(--color-bg-subtle, #f5f5f5); - cursor: pointer; - transition: border-color 0.2s ease, background-color 0.2s ease; -} - -.zone:hover, -.zone:focus-visible { - border-color: var(--color-interactive-primary, #3b82f6); - background: var(--color-bg-elevated, #fff); -} - -.zone.dragOver { - border-color: var(--color-interactive-primary, #3b82f6); - background: var(--color-primary-50, #eff6ff); -} - -.zone.disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.icon { - color: var(--color-text-secondary, #6b7280); - width: 32px; - height: 32px; -} - -.placeholder { - color: var(--color-text-secondary, #6b7280); - font-size: var(--font-size-sm, 14px); - text-align: center; - margin: 0; -} - -.hint { - color: var(--color-text-tertiary, #9ca3af); - font-size: var(--font-size-xs, 12px); - text-align: center; - margin: 0; -} - -.hiddenInput { - display: none; -} - -.fileList { - list-style: none; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; - gap: var(--space-1, 4px); -} - -.fileItem { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-2, 8px) var(--space-3, 12px); - background: var(--color-bg-elevated, #fff); - border: 1px solid var(--color-border-default, #e0e0e0); - border-radius: var(--radius-md, 8px); - font-size: var(--font-size-sm, 14px); -} - -.fileName { - color: var(--color-text-primary, #111); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; -} - -.progress { - color: var(--color-text-secondary, #6b7280); - font-size: var(--font-size-xs, 12px); - min-width: 40px; - text-align: right; -} - -.done { - color: var(--color-semantic-success, #10b981); - font-size: var(--font-size-sm, 14px); -} - -.removeBtn { - background: none; - border: none; - cursor: pointer; - color: var(--color-text-tertiary, #9ca3af); - font-size: 16px; - line-height: 1; - padding: 0 4px; -} - -.removeBtn:hover { - color: var(--color-semantic-error, #ef4444); -} \ No newline at end of file diff --git a/packages/common/src/components/ui/FileUploader.tsx b/packages/common/src/components/ui/FileUploader.tsx deleted file mode 100644 index 848c454c..00000000 --- a/packages/common/src/components/ui/FileUploader.tsx +++ /dev/null @@ -1,292 +0,0 @@ -/** - * FileUploader — 通用文件上传组件(DRY版) - * - * 改造前:AudioEditor、VideoEditor、ImageEditor 等各自复制了一套上传逻辑 - * 改造后:所有上传逻辑收敛于此,外部只需传入配置即可 - * - * 特性: - * - 拖拽上传 + 点击上传双模式 - * - 文件大小/数量/类型校验 - * - 上传进度跟踪 - * - 自定义请求(可接入任意上传服务) - * - 受控/非受控模式 - */ - -import React, { useState, useCallback, useRef, type ChangeEvent, type DragEvent } from 'react'; -import { Upload } from 'lucide-react'; -import { generateId, detectFileType } from '@frame-fab/common/utils'; -import styles from './FileUploader.module.css'; - -export interface UploadFile { - uid: string; - name: string; - size: number; - type: string; - status: 'pending' | 'uploading' | 'done' | 'error'; - url?: string; - progress?: number; - error?: string; -} - -export interface FileUploaderProps { - accept?: string; - maxCount?: number; - maxSize?: number; // MB - multiple?: boolean; - disabled?: boolean; - placeholder?: string; - hint?: string; - className?: string; - showFileList?: boolean; - isDragger?: boolean; - fileList?: UploadFile[]; - onChange?: (files: UploadFile[]) => void; - onError?: (message: string) => void; - beforeUpload?: (file: File) => boolean | Promise; - customRequest?: ( - file: File, - options: { - onProgress: (pct: number) => void; - onSuccess: (url: string) => void; - onError: (err: string) => void; - } - ) => void; -} - -export function FileUploader({ - accept, - maxCount = 10, - maxSize, - multiple = false, - disabled = false, - placeholder = '点击或拖拽文件到此处上传', - hint, - className, - showFileList = true, - isDragger = true, - fileList: externalFileList, - onChange, - onError, - beforeUpload, - customRequest, -}: FileUploaderProps) { - const [dragOver, setDragOver] = useState(false); - const [internalFiles, setInternalFiles] = useState([]); - const inputRef = useRef(null); - - const fileList = externalFileList ?? internalFiles; - - // ============================================ - // 校验 - // ============================================ - - const validateFile = useCallback( - (file: File): string | null => { - if (maxSize && file.size > maxSize * 1024 * 1024) { - return `文件 ${file.name} 超过最大限制 ${maxSize}MB`; - } - if (accept && !new RegExp(accept.replace(/\*/g, '.*')).test(file.name)) { - return `文件类型不支持,仅接受 ${accept}`; - } - return null; - }, - [maxSize, accept] - ); - - const updateFiles = useCallback( - (updater: (prev: UploadFile[]) => UploadFile[]) => { - const next = updater(fileList); - setInternalFiles(next); - onChange?.(next); - }, - [fileList, onChange] - ); - - // ============================================ - // 上传核心 - // ============================================ - - const uploadFile = useCallback( - async (file: File, uid: string) => { - const doUpload = (opts: { - onProgress: (pct: number) => void; - onSuccess: (url: string) => void; - onError: (err: string) => void; - }) => { - if (customRequest) { - customRequest(file, opts); - } else { - // 默认:模拟进度上传(实际项目替换为真实上传) - let pct = 0; - const interval = setInterval(() => { - pct += 20; - if (pct >= 100) { - clearInterval(interval); - opts.onSuccess(URL.createObjectURL(file)); - } else { - opts.onProgress(pct); - } - }, 200); - } - }; - - updateFiles((prev) => - prev.map((f) => (f.uid === uid ? { ...f, status: 'uploading' as const } : f)) - ); - - doUpload({ - onProgress: (pct) => { - updateFiles((prev) => prev.map((f) => (f.uid === uid ? { ...f, progress: pct } : f))); - }, - onSuccess: (url) => { - updateFiles((prev) => - prev.map((f) => (f.uid === uid ? { ...f, status: 'done', url } : f)) - ); - }, - onError: (err) => { - updateFiles((prev) => - prev.map((f) => (f.uid === uid ? { ...f, status: 'error', error: err } : f)) - ); - onError?.(err); - }, - }); - }, - [customRequest, updateFiles, onError] - ); - - // ============================================ - // 文件选择 - // ============================================ - - const addFiles = useCallback( - async (files: File[]) => { - if (maxCount && fileList.length + files.length > maxCount) { - onError?.(`最多只能上传 ${maxCount} 个文件`); - return; - } - - const newFiles: UploadFile[] = files.map((file) => ({ - uid: generateId(), - name: file.name, - size: file.size, - type: detectFileType(file.name), - status: 'pending', - progress: 0, - })); - - updateFiles((prev) => [...prev, ...newFiles]); - - // 校验 + 上传 - for (const file of files) { - const error = validateFile(file); - if (error) { - onError?.(error); - updateFiles((prev) => - prev.filter((f) => f.uid !== newFiles.find((n) => n.name === file.name)?.uid) - ); - continue; - } - if (beforeUpload) { - const canContinue = await beforeUpload(file); - if (!canContinue) continue; - } - const uid = newFiles.find((n) => n.name === file.name)?.uid; - if (uid) uploadFile(file, uid); - } - }, - [maxCount, fileList.length, validateFile, beforeUpload, uploadFile, updateFiles, onError] - ); - - const handleInputChange = useCallback( - (e: ChangeEvent) => { - const files = Array.from(e.target.files ?? []); - if (files.length) addFiles(files); - e.target.value = ''; - }, - [addFiles] - ); - - const handleDrop = useCallback( - (e: DragEvent) => { - e.preventDefault(); - setDragOver(false); - const files = Array.from(e.dataTransfer.files); - if (files.length) addFiles(files); - }, - [addFiles] - ); - - const handleRemove = useCallback( - (uid: string) => { - updateFiles((prev) => prev.filter((f) => f.uid !== uid)); - }, - [updateFiles] - ); - - return ( -
- {/* 上传区域 */} -
{ - e.preventDefault(); - setDragOver(true); - }} - onDragLeave={() => setDragOver(false)} - onDrop={handleDrop} - onClick={() => !disabled && inputRef.current?.click()} - role="button" - tabIndex={disabled ? -1 : 0} - aria-disabled={disabled} - onKeyDown={(e) => e.key === 'Enter' && !disabled && inputRef.current?.click()} - > - -
- - {/* 文件列表 */} - {showFileList && fileList.length > 0 && ( -
    - {fileList.map((file) => ( -
  • - {file.name} - {file.status === 'uploading' && ( - {file.progress ?? 0}% - )} - {file.status === 'done' && } - {file.status === 'error' && ( - - )} -
  • - ))} -
- )} -
- ); -} - -export default FileUploader; diff --git a/packages/common/src/components/ui/Modal.module.css b/packages/common/src/components/ui/Modal.module.css deleted file mode 100644 index 0e06e830..00000000 --- a/packages/common/src/components/ui/Modal.module.css +++ /dev/null @@ -1,77 +0,0 @@ -/* Modal CSS Module */ - -.overlay { - position: fixed; - inset: 0; - z-index: 1000; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0, 0, 0, 0.5); - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease; -} - -.overlay.visible { - opacity: 1; - pointer-events: auto; -} - -.panel { - background: var(--color-bg-elevated, #fff); - border-radius: var(--radius-xl, 16px); - box-shadow: var(--shadow-2xl, 0 25px 50px -12px rgba(0, 0, 0, 0.25)); - max-height: 90vh; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.panel.centered { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-5, 20px) var(--space-6, 24px); - border-bottom: 1px solid var(--color-border-default, #e5e7eb); -} - -.title { - font-size: var(--font-size-lg, 18px); - font-weight: 600; - color: var(--color-text-primary, #111); - margin: 0; -} - -.closeBtn { - background: none; - border: none; - cursor: pointer; - color: var(--color-text-tertiary, #9ca3af); - padding: var(--space-1, 4px); - border-radius: var(--radius-md, 8px); - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s ease; -} - -.closeBtn:hover { color: var(--color-text-primary, #111); } - -.body { - padding: var(--space-6, 24px); - overflow-y: auto; - flex: 1; -} - -.footer { - padding: var(--space-4, 16px) var(--space-6, 24px); - border-top: 1px solid var(--color-border-default, #e5e7eb); -} \ No newline at end of file diff --git a/packages/common/src/components/ui/Modal.tsx b/packages/common/src/components/ui/Modal.tsx deleted file mode 100644 index 1775264a..00000000 --- a/packages/common/src/components/ui/Modal.tsx +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Modal — 通用模态框组件(DRY版) - * - * 改造前:ConfirmDialog、AlertDialog、ImagePreviewModal 各写了一套 Dialog 逻辑 - * 改造后:Modal 作为底层组件,ConfirmDialog 基于 Modal 封装 - */ - -import React, { useEffect, useCallback } from 'react'; -import { X } from 'lucide-react'; -import styles from './Modal.module.css'; - -export interface ModalProps { - open: boolean; - onClose: () => void; - title?: string; - children: React.ReactNode; - footer?: React.ReactNode; - width?: number | string; - centered?: boolean; - maskClosable?: boolean; - closable?: boolean; - destroyOnClose?: boolean; - className?: string; -} - -export function Modal({ - open, - onClose, - title, - children, - footer, - width = 520, - centered = false, - maskClosable = true, - closable = true, - destroyOnClose = false, - className, -}: ModalProps) { - // ESC 关闭 - const handleKeyDown = useCallback((e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }, [onClose]); - - useEffect(() => { - if (open) { - document.addEventListener('keydown', handleKeyDown); - document.body.style.overflow = 'hidden'; - } - return () => { - document.removeEventListener('keydown', handleKeyDown); - document.body.style.overflow = ''; - }; - }, [open, handleKeyDown]); - - if (!open && destroyOnClose) return null; - - return ( -
-
e.stopPropagation()} - > - {/* Header */} - {(title || closable) && ( -
- {title && } - {closable && ( - - )} -
- )} - - {/* Body */} -
{children}
- - {/* Footer */} - {footer &&
{footer}
} -
-
- ); -} - -export default Modal; \ No newline at end of file diff --git a/packages/common/src/components/ui/ProgressBar.module.css b/packages/common/src/components/ui/ProgressBar.module.css deleted file mode 100644 index 90fe3d1b..00000000 --- a/packages/common/src/components/ui/ProgressBar.module.css +++ /dev/null @@ -1,102 +0,0 @@ -/* ProgressBar CSS Module */ - -.container { - display: flex; - flex-direction: column; - gap: var(--space-1, 4px); - width: 100%; -} - -.labelRow { - display: flex; - justify-content: space-between; - align-items: center; - font-size: var(--font-size-xs, 12px); -} - -.label { - color: var(--color-text-primary, #111); -} - -.stepCount { - color: var(--color-text-secondary, #6b7280); -} - -.track { - width: 100%; - background: var(--color-bg-subtle, #f3f4f6); - border-radius: var(--radius-full, 9999px); - overflow: hidden; - position: relative; -} - -/* Sizes */ -.sm { height: 4px; } -.md { height: 8px; } -.lg { height: 12px; } - -/* Colors */ -.primary .fill, -.primary.indeterminate .indeterminate { background: var(--color-interactive-primary, #3b82f6); } -.success .fill { background: var(--color-semantic-success, #10b981); } -.warning .fill { background: var(--color-semantic-warning, #f59e0b); } -.error .fill { background: var(--color-semantic-error, #ef4444); } - -.fill { - height: 100%; - border-radius: var(--radius-full, 9999px); - transition: width 0.5s ease-out; -} - -.fill.animated { - animation: progress-grow 2s ease-out infinite; -} - -.indeterminate { - position: absolute; - inset: 0; - background: linear-gradient( - 90deg, - transparent 0%, - var(--color-interactive-primary, #3b82f6) 50%, - transparent 100% - ); - background-size: 200% 100%; - animation: indeterminate-slide 1.5s ease-in-out infinite; -} - -@keyframes progress-grow { - 0% { opacity: 1; } - 50% { opacity: 0.8; } - 100% { opacity: 1; } -} - -@keyframes indeterminate-slide { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -.steps { - display: flex; - align-items: center; - gap: var(--space-1, 4px); - justify-content: center; - margin-top: var(--space-1, 4px); -} - -.dot { - width: 8px; - height: 8px; - border-radius: var(--radius-full, 9999px); - transition: background-color 0.3s ease; -} - -.dot.done { background: var(--color-semantic-success, #10b981); } -.dot.current { background: var(--color-interactive-primary, #3b82f6); } -.dot.pending { background: var(--color-bg-subtle, #e5e7eb); } - -.stepLabel { - font-size: var(--font-size-xs, 11px); - color: var(--color-text-secondary, #6b7280); - margin-left: var(--space-2, 8px); -} \ No newline at end of file diff --git a/packages/common/src/components/ui/ProgressBar.tsx b/packages/common/src/components/ui/ProgressBar.tsx deleted file mode 100644 index ee3b9aae..00000000 --- a/packages/common/src/components/ui/ProgressBar.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/** - * ProgressBar — 通用进度条组件(DRY版) - * - * 改造前:PipelineProgress、AudioEditor、VideoEditor 各有一份进度条实现 - * 改造后:统一 ProgressBar,支持确定/不确定进度,支持步进点 - */ - -import React, { useEffect, useRef } from 'react'; -import styles from './ProgressBar.module.css'; - -export interface ProgressBarProps { - /** 进度值 0-100,不确定进度时传 undefined */ - value?: number; - /** 是否不确定进度(动画脉冲) */ - indeterminate?: boolean; - /** 尺寸 */ - size?: 'sm' | 'md' | 'lg'; - /** 颜色主题 */ - color?: 'primary' | 'success' | 'warning' | 'error'; - /** 是否显示步进点 */ - steps?: { current: number; total: number; labels?: string[] }; - /** 是否显示百分比文本 */ - showLabel?: boolean; - /** 标签文本 */ - label?: string; - /** 动画时长(秒) */ - animated?: boolean; - /** className */ - className?: string; -} - -export function ProgressBar({ - value, - indeterminate = false, - size = 'md', - color = 'primary', - steps, - showLabel = false, - label, - animated = true, - className, -}: ProgressBarProps) { - const ref = useRef(null); - - const pct = value !== undefined ? Math.min(100, Math.max(0, value)) : 0; - - return ( -
- {/* 标签行 */} - {(label || showLabel) && ( -
- {label ?? `${pct.toFixed(0)}%`} - {steps && ( - - 步骤 {steps.current} / {steps.total} - - )} -
- )} - - {/* 进度条轨道 */} -
- {indeterminate ? ( -
- ) : ( -
- )} -
- - {/* 步进点指示器 */} - {steps && steps.total > 1 && ( -
- {Array.from({ length: steps.total }, (_, i) => ( - - )} -
- ); -} - -export default ProgressBar; \ No newline at end of file diff --git a/packages/common/src/domain/character/validators.ts b/packages/common/src/domain/character/validators.ts deleted file mode 100644 index 637e4b4b..00000000 --- a/packages/common/src/domain/character/validators.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Character 领域模型校验 — 角色名称合法性等 - */ - -import type { ValidationResult } from '../shared/types'; - -/** - * 角色名称校验 - * - 2-30 字符 - * - 允许中文、英文、数字、下划线 - * - 不能以数字开头 - */ -export function validateCharacterName(name: string): ValidationResult { - const errors: string[] = []; - const trimmed = name.trim(); - - if (trimmed.length < 2) { - errors.push('角色名称至少需要2个字符'); - } - if (trimmed.length > 30) { - errors.push('角色名称不能超过30个字符'); - } - if (!/^[a-zA-Z\u4E00-\u9FA5_][a-zA-Z0-9\u4E00-\u9FA5_]*$/.test(trimmed)) { - errors.push('角色名称只能包含中文、英文、数字和下划线,且不能以数字开头'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 角色描述校验 - */ -export function validateCharacterDescription(desc: string): ValidationResult { - const errors: string[] = []; - const trimmed = desc.trim(); - - if (trimmed.length > 500) { - errors.push('角色描述不能超过500个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 角色外观校验 - */ -export function validateCharacterAppearance(appearance: string): ValidationResult { - const errors: string[] = []; - const trimmed = appearance.trim(); - - if (trimmed.length === 0) { - errors.push('角色外观描述不能为空'); - } - if (trimmed.length > 1000) { - errors.push('角色外观描述不能超过1000个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -export default { - validateCharacterName, - validateCharacterDescription, - validateCharacterAppearance, -}; \ No newline at end of file diff --git a/packages/common/src/domain/index.ts b/packages/common/src/domain/index.ts deleted file mode 100644 index 2f8f77f3..00000000 --- a/packages/common/src/domain/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Package exports manifest - * 子域校验器统一导出入口 - */ - -export type { ValidationResult, EntityValidationResult } from './shared/types'; - -export { - validateScriptTitle, - validateScriptContent, - validateSceneDescription, - validateDialogue, - validateCharacterName, - validateScript, - ScriptTitle, - CharacterName, - scriptEntity, -} from './script/validators'; - -export { - validateCharacterDescription, - validateCharacterAppearance, -} from './character/validators'; - -export { - validateSceneNumber, -} from './scene/validators'; \ No newline at end of file diff --git a/packages/common/src/domain/scene/validators.ts b/packages/common/src/domain/scene/validators.ts deleted file mode 100644 index b1755011..00000000 --- a/packages/common/src/domain/scene/validators.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Scene 领域模型校验 — 场景编号合法性等 - */ - -import type { ValidationResult } from '../shared/types'; - -/** - * 场景描述校验 - */ -export function validateSceneDescription(desc: string): ValidationResult { - const errors: string[] = []; - const trimmed = desc.trim(); - - if (trimmed.length === 0) { - errors.push('场景描述不能为空'); - } - if (trimmed.length > 500) { - errors.push('场景描述不能超过500个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 场景编号校验 - */ -export function validateSceneNumber(num: number): ValidationResult { - const errors: string[] = []; - - if (!Number.isInteger(num) || num < 1) { - errors.push('场景编号必须为大于等于1的整数'); - } - if (num > 9999) { - errors.push('场景编号不能超过9999'); - } - - return { valid: errors.length === 0, errors }; -} - -export default { - validateSceneDescription, - validateSceneNumber, -}; \ No newline at end of file diff --git a/packages/common/src/domain/script/validators.ts b/packages/common/src/domain/script/validators.ts deleted file mode 100644 index 16089d9b..00000000 --- a/packages/common/src/domain/script/validators.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * Script 领域模型校验 — 剧本标题长度、角色名称合法性等 - * - * 改造前:散落在各个组件/服务中重复校验 - * 改造后:实体类封装所有校验规则,上层只调用 validate() - */ - -import type { ValidationResult } from '../shared/types'; - -/** - * 剧本标题校验 - * - 长度:2-200 字符(中文按2字节算) - * - 不能全空白 - * - 不能包含控制字符 - */ -export function validateScriptTitle(title: string): ValidationResult { - const errors: string[] = []; - const trimmed = title.trim(); - - if (trimmed.length < 2) { - errors.push('剧本标题至少需要2个字符'); - } - if (trimmed.length > 200) { - errors.push('剧本标题不能超过200个字符'); - } - if (!/[^\s]/.test(trimmed)) { - errors.push('剧本标题不能全为空白字符'); - } - if (/[\x00-\x1F\x7F]/.test(trimmed)) { - errors.push('剧本标题不能包含控制字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 剧本内容校验 - * - 不能为空 - * - 最大长度 50000 字符 - */ -export function validateScriptContent(content: string): ValidationResult { - const errors: string[] = []; - const trimmed = content.trim(); - - if (trimmed.length === 0) { - errors.push('剧本内容不能为空'); - } - if (trimmed.length > 50000) { - errors.push('剧本内容不能超过50000个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 场景描述校验 - */ -export function validateSceneDescription(desc: string): ValidationResult { - const errors: string[] = []; - - if (desc.trim().length === 0) { - errors.push('场景描述不能为空'); - } - if (desc.trim().length > 500) { - errors.push('场景描述不能超过500个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 对话内容校验 - */ -export function validateDialogue(dialogue: string): ValidationResult { - const errors: string[] = []; - const trimmed = dialogue.trim(); - - if (trimmed.length === 0) { - errors.push('对话内容不能为空'); - } - if (trimmed.length > 1000) { - errors.push('单条对话内容不能超过1000个字符'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 角色名称校验 - * - 2-30 字符 - * - 允许中文、英文、数字、下划线 - * - 不能以数字开头 - */ -export function validateCharacterName(name: string): ValidationResult { - const errors: string[] = []; - const trimmed = name.trim(); - - if (trimmed.length < 2) { - errors.push('角色名称至少需要2个字符'); - } - if (trimmed.length > 30) { - errors.push('角色名称不能超过30个字符'); - } - if (!/^[a-zA-Z\u4E00-\u9FA5_][a-zA-Z0-9\u4E00-\u9FA5_]*$/.test(trimmed)) { - errors.push('角色名称只能包含中文、英文、数字和下划线,且不能以数字开头'); - } - - return { valid: errors.length === 0, errors }; -} - -/** - * 完整剧本实体校验 - */ -export function validateScript(script: { - title?: string; - content?: string; - characters?: Array<{ name: string }>; - scenes?: Array<{ description: string }>; -}): ValidationResult { - const allErrors: string[] = []; - - if (script.title !== undefined) { - allErrors.push(...validateScriptTitle(script.title).errors); - } - if (script.content !== undefined) { - allErrors.push(...validateScriptContent(script.content).errors); - } - if (script.characters) { - script.characters.forEach((char, i) => { - const r = validateCharacterName(char.name); - if (!r.valid) allErrors.push(`角色${i + 1}:${r.errors.join(';')}`); - }); - } - if (script.scenes) { - script.scenes.forEach((scene, i) => { - const r = validateSceneDescription(scene.description); - if (!r.valid) allErrors.push(`场景${i + 1}:${r.errors.join(';')}`); - }); - } - - return { valid: allErrors.length === 0, errors: allErrors }; -} - -// ============================================ -// 实体类封装(可选,面向对象风格) -// ============================================ - -export class ScriptTitle { - private constructor(private readonly value: string) {} - - static create(raw: string): ScriptTitle { - const result = validateScriptTitle(raw); - if (!result.valid) throw new Error(result.errors.join(';')); - return new ScriptTitle(raw.trim()); - } - - get(): string { return this.value; } - getLength(): number { return this.value.length; } - isEmpty(): boolean { return !/\S/.test(this.value); } -} - -export class CharacterName { - private constructor(private readonly value: string) {} - - static create(raw: string): CharacterName { - const result = validateCharacterName(raw); - if (!result.valid) throw new Error(result.errors.join(';')); - return new CharacterName(raw.trim()); - } - - get(): string { return this.value; } -} - -// 导出实体工厂函数(函数式风格,与类风格二选一) -export const scriptEntity = { - title: (raw: string) => { - const r = validateScriptTitle(raw); - if (!r.valid) throw new Error(r.errors.join(';')); - return { value: raw.trim(), maxLength: 200 }; - }, - characterName: (raw: string) => { - const r = validateCharacterName(raw); - if (!r.valid) throw new Error(r.errors.join(';')); - return { value: raw.trim(), maxLength: 30 }; - }, -}; \ No newline at end of file diff --git a/packages/common/src/domain/shared/types.ts b/packages/common/src/domain/shared/types.ts deleted file mode 100644 index ebfb4c87..00000000 --- a/packages/common/src/domain/shared/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Shared types — 各子域共用的接口/类型定义 - * - * 避免 ValidationResult 等类型在 script/character/scene 三个 validator 中重复定义 - */ - -export interface ValidationResult { - valid: boolean; - errors: string[]; // 空数组 = 通过 -} - -export interface EntityValidationResult extends ValidationResult { - warnings?: string[]; // 非致命警告 -} \ No newline at end of file diff --git a/packages/common/src/formatters/index.ts b/packages/common/src/formatters/index.ts deleted file mode 100644 index 261ff4f1..00000000 --- a/packages/common/src/formatters/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Formatters — 格式化工具(日期、时间、文件大小等) - * 统一所有格式化逻辑,杜绝散落各处 - */ - -import { formatDate, formatRelativeTime } from '../utils'; - -// ============================================ -// 时间格式化 -// ============================================ - -export interface FormatDurationOptions { - hours?: 'never' | 'if-nonzero' | 'always'; - ms?: 0 | 1 | 2 | 3; - separator?: string; - decimalMark?: string; -} - -/** - * 统一时间格式化(秒 → H:MM:SS) - * @example formatDuration(90) => '01:30' - * @example formatDuration(90.5, { ms: 1 }) => '01:30.5' - */ -export function formatDuration(seconds: number, opts: FormatDurationOptions = {}): string { - const { hours = 'never', ms = 0, separator = ':', decimalMark = '.' } = opts; - if (isNaN(seconds) || seconds < 0) { - return `00:00${ms > 0 ? decimalMark + '0'.repeat(ms) : ''}`; - } - - const totalSecs = Math.floor(seconds); - const s = totalSecs % 60; - const totalMins = Math.floor(totalSecs / 60); - const h = Math.floor(totalMins / 60); - const m = hours === 'never' ? totalMins : totalMins % 60; - const pad = (n: number, len = 2) => String(n).padStart(len, '0'); - const hourStr = hours === 'always' ? `${pad(h)}${separator}` : - hours === 'if-nonzero' && h > 0 ? `${h}${separator}` : ''; - - let msStr = ''; - if (ms > 0) { - const base = ms === 3 ? 1000 : ms === 2 ? 100 : 10; - const frac = Math.floor((seconds - totalSecs) * base); - msStr = decimalMark + String(frac).padStart(ms, '0').slice(0, ms); - } - - return `${hourStr}${pad(m)}${separator}${pad(s)}${msStr}`; -} - -/** - * 帧时间格式化(帧 → MM:SS:FF) - * @param frame 帧号 - * @param fps 每秒帧数 - */ -export function formatFrameTime(frame: number, fps = 24): string { - const totalSeconds = frame / fps; - return formatDuration(totalSeconds); -} - -// ============================================ -// 文件大小格式化 -// ============================================ - -export type FileSizeUnit = 'auto' | 'B' | 'KB' | 'MB' | 'GB'; - -/** - * 格式化文件大小 - * @example formatFileSize(1024) => '1.0 KB' - * @example formatFileSize(1048576) => '1.0 MB' - * @example formatFileSize(1024, 'auto') => '1.0 KB' - */ -export function formatFileSize(bytes: number, unit: FileSizeUnit = 'auto'): string { - const units: Array<{ threshold: number; label: string; divisor: number }> = [ - { threshold: 1024 * 1024 * 1024, label: 'GB', divisor: 1024 * 1024 * 1024 }, - { threshold: 1024 * 1024, label: 'MB', divisor: 1024 * 1024 }, - { threshold: 1024, label: 'KB', divisor: 1024 }, - { threshold: 0, label: 'B', divisor: 1 }, - ]; - - if (unit !== 'auto') { - const target = units.find((u) => u.label === unit); - if (target) return `${(bytes / target.divisor).toFixed(1)} ${unit}`; - } - - for (const { threshold, label, divisor } of units) { - if (bytes >= threshold) { - return `${(bytes / divisor).toFixed(1)} ${label}`; - } - } - return `${bytes} B`; -} - -// ============================================ -// 数字格式化 -// ============================================ - -/** - * 数字千分位分隔符 - * @example formatNumber(1234567) => '1,234,567' - */ -export function formatNumber(num: number): string { - return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} - -/** - * 百分比格式化 - * @example formatPercent(0.1234) => '12%' - * @example formatPercent(0.1234, 1) => '12.3%' - */ -export function formatPercent(value: number, decimals = 0): string { - return `${(value * 100).toFixed(decimals)}%`; -} - -// ============================================ -// 文本格式化 -// ============================================ - -/** - * 脱敏处理(手机号、身份证等) - * @example maskPhone('13812345678') => '138****5678' - */ -export function maskString(str: string, startVisible = 3, endVisible = 4, maskChar = '*'): string { - if (str.length <= startVisible + endVisible) return str; - const start = str.slice(0, startVisible); - const end = str.slice(-endVisible); - const mask = maskChar.repeat(str.length - startVisible - endVisible); - return `${start}${mask}${end}`; -} - -/** - * 首字母大写 - */ -export function capitalize(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -/** - * 驼峰转短横线 - */ -export function camelToKebab(str: string): string { - return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); -} - -// ============================================ -// 导出汇总 -// ============================================ - -export const formatters = { - date: formatDate, - relativeTime: formatRelativeTime, - duration: formatDuration, - frameTime: formatFrameTime, - fileSize: formatFileSize, - number: formatNumber, - percent: formatPercent, - mask: maskString, - capitalize, - camelToKebab, -}; - -export default formatters; \ No newline at end of file diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts deleted file mode 100644 index 8c788d9f..00000000 --- a/packages/common/src/hooks/index.ts +++ /dev/null @@ -1,490 +0,0 @@ -/** - * Hooks — 统一公共 Hooks 库 - * - * 来源: - * - packages/common/src/hooks/index.ts(原有 8 个) - * - core/utils/hooks.ts(额外 7 个) - * - * 改造后:core/utils/hooks.ts 和 shared/utils/index.ts 中的 hooks 统一从本文件导入 - * 消除散落各处的重复 Hook 定义 - */ - -import { useState, useEffect, useCallback, useRef } from 'react'; - -type GenericFunction = (...args: unknown[]) => unknown; - -function debounce(func: T, wait: number): (...args: Parameters) => void { - let timeout: ReturnType; - return (...args) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - -function throttle(func: T, limit: number): (...args: Parameters) => void { - let inThrottle = false; - return (...args) => { - if (!inThrottle) { - func(...args); - inThrottle = true; - setTimeout(() => { inThrottle = false; }, limit); - } - }; -} - -// ============================================ -// useLocalStorage -// ============================================ - -export function useLocalStorage( - key: string, - initialValue: T -): [T, (value: T | ((val: T) => T)) => void] { - const [storedValue, setStoredValue] = useState(() => { - try { - const item = window.localStorage.getItem(key); - return item ? (JSON.parse(item) as T) : initialValue; - } catch { - return initialValue; - } - }); - - const setValue = useCallback((value: T | ((val: T) => T)) => { - try { - setStoredValue((prev) => { - const next = value instanceof Function ? value(prev) : value; - window.localStorage.setItem(key, JSON.stringify(next)); - return next; - }); - } catch (error) { - console.error('[useLocalStorage] setValue error:', error); - } - }, [key]); - - return [storedValue, setValue]; -} - -// ============================================ -// useStepNavigation -// ============================================ - -export interface StepDef { - id: string; - label: string; - isCompleted?: boolean; - isLocked?: boolean; -} - -export interface UseStepNavigationOptions { - steps: StepDef[]; - initialStep?: string; - onStepChange?: (prev: string, next: string) => void; -} - -export function useStepNavigation({ - steps, - initialStep, - onStepChange, -}: UseStepNavigationOptions) { - const [currentStepId, setCurrentStepId] = useState( - initialStep ?? steps[0]?.id ?? '' - ); - - const currentIndex = steps.findIndex((s) => s.id === currentStepId); - - const goTo = useCallback((stepId: string) => { - const targetStep = steps.find((s) => s.id === stepId); - if (!targetStep || targetStep.isLocked) return; - - const prev = currentStepId; - setCurrentStepId(stepId); - onStepChange?.(prev, stepId); - }, [steps, currentStepId, onStepChange]); - - const goNext = useCallback(() => { - const next = steps[currentIndex + 1]; - if (next && !next.isLocked) goTo(next.id); - }, [steps, currentIndex, goTo]); - - const goPrev = useCallback(() => { - const prev = steps[currentIndex - 1]; - if (prev) goTo(prev.id); - }, [steps, currentIndex, goTo]); - - const progress = steps.length > 0 - ? Math.round(((currentIndex + 1) / steps.length) * 100) - : 0; - - return { - currentStepId, - currentIndex, - steps, - goTo, - goNext, - goPrev, - progress, - canGoNext: currentIndex < steps.length - 1, - canGoPrev: currentIndex > 0, - }; -} - -// ============================================ -// useDebounce -// ============================================ - -type GenericFunction = (...args: unknown[]) => unknown; - -export function useDebounce( - callback: T, - delay: number -): (...args: Parameters) => void { - const callbackRef = useRef(callback); - - useEffect(() => { - callbackRef.current = callback; - }, [callback]); - - return useCallback( - (...args: Parameters) => debounce(callbackRef.current as GenericFunction, delay)(...args), - [delay] - ); -} - -// ============================================ -// useThrottle -// ============================================ - -export function useThrottle( - callback: T, - limit: number -): (...args: Parameters) => void { - const callbackRef = useRef(callback); - - useEffect(() => { - callbackRef.current = callback; - }, [callback]); - - return useCallback( - (...args: Parameters) => throttle(callbackRef.current as GenericFunction, limit)(...args), - [limit] - ); -} - -// ============================================ -// useWindowSize -// ============================================ - -export interface WindowSize { - width: number; - height: number; -} - -export function useWindowSize(): WindowSize { - const [windowSize, setWindowSize] = useState({ - width: window.innerWidth, - height: window.innerHeight, - }); - - useEffect(() => { - const handleResize = () => { - setWindowSize({ width: window.innerWidth, height: window.innerHeight }); - }; - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - return windowSize; -} - -// ============================================ -// useClickOutside -// ============================================ - -export function useClickOutside( - ref: React.RefObject, - handler: () => void -): void { - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { - handler(); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [ref, handler]); -} - -// ============================================ -// useCountdown -// ============================================ - -export function useCountdown(initialSeconds: number): [number, () => void, () => void, () => void] { - const [seconds, setSeconds] = useState(initialSeconds); - const [isActive, setIsActive] = useState(false); - const intervalRef = useRef>(); - const isActiveRef = useRef(isActive); - - useEffect(() => { - isActiveRef.current = isActive; - }, [isActive]); - - const start = useCallback(() => { - isActiveRef.current = true; - setIsActive(true); - }, []); - - const pause = useCallback(() => { - isActiveRef.current = false; - setIsActive(false); - }, []); - - const reset = useCallback(() => { - isActiveRef.current = false; - setIsActive(false); - setSeconds(initialSeconds); - }, [initialSeconds]); - - useEffect(() => { - if (isActive && seconds > 0) { - intervalRef.current = setInterval(() => { - setSeconds((s) => s - 1); - }, 1000); - } else if (seconds === 0 && isActiveRef.current) { - const id = setTimeout(() => { - isActiveRef.current = false; - setIsActive(false); - }, 0); - return () => clearTimeout(id); - } - - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [isActive, seconds]); - - return [seconds, start, pause, reset]; -} - -// ============================================ -// useAsync -// ============================================ - -interface UseAsyncReturn { - data: T | null; - error: Error | null; - loading: boolean; - execute: () => Promise; -} - -export function useAsync( - asyncFunction: () => Promise, - immediate = false -): UseAsyncReturn { - const [data, setData] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - const execute = useCallback(async () => { - setLoading(true); - setError(null); - try { - const result = await asyncFunction(); - setData(result); - } catch (err) { - setError(err as Error); - } finally { - setLoading(false); - } - }, [asyncFunction]); - - useEffect(() => { - if (immediate) { - const id = setTimeout(() => execute(), 0); - return () => clearTimeout(id); - } - }, [immediate, execute]); - - return { data, error, loading, execute }; -} - -// ============================================ -// usePrevious -// ============================================ - -export function usePrevious(value: T): T | undefined { - const ref = useRef(undefined); - // eslint-disable-next-line react-hooks/refs - const prev = ref.current; - ref.current = value; - return prev; -} - -// ============================================ -// useMounted -// ============================================ - -export function useMounted(): boolean { - const mountedRef = useRef(false); - - useEffect(() => { - mountedRef.current = true; - return () => { - mountedRef.current = false; - }; - }, []); - - return mountedRef.current; -} - -// ============================================ -// useUpdateEffect (skip first render) -// ============================================ - -export function useUpdateEffect( - effect: React.EffectCallback, - deps?: React.DependencyList -): void { - const isFirstRender = useRef(true); - - useEffect(() => { - if (isFirstRender.current) { - isFirstRender.current = false; - return; - } - return effect(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps); -} - -// ============================================ -// useKeyPress -// ============================================ - -export function useKeyPress(targetKey: string, callback: () => void): void { - useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === targetKey) callback(); - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [targetKey, callback]); -} - -// ============================================ -// useOnlineStatus -// ============================================ - -export function useOnlineStatus(): boolean { - const [isOnline, setIsOnline] = useState(navigator.onLine); - - useEffect(() => { - const handleOnline = () => setIsOnline(true); - const handleOffline = () => setIsOnline(false); - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); - return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - }; - }, []); - - return isOnline; -} - -// ============================================ -// useMediaQuery -// ============================================ - -export function useMediaQuery(query: string): boolean { - const [matches, setMatches] = useState(() => window.matchMedia(query).matches); - - useEffect(() => { - const media = window.matchMedia(query); - const listener = (e: MediaQueryListEvent) => setMatches(e.matches); - media.addEventListener('change', listener); - return () => media.removeEventListener('change', listener); - }, [query]); - - return matches; -} - -// ============================================ -// useScrollPosition -// ============================================ - -export interface ScrollPosition { - x: number; - y: number; -} - -export function useScrollPosition(): ScrollPosition { - const [position, setPosition] = useState({ x: 0, y: 0 }); - - useEffect(() => { - const handleScroll = () => setPosition({ x: window.scrollX, y: window.scrollY }); - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); - - return position; -} - -// ============================================ -// useVisibility -// ============================================ - -export function useVisibility(): boolean { - const [isVisible, setIsVisible] = useState(!document.hidden); - - useEffect(() => { - const handleVisibilityChange = () => setIsVisible(!document.hidden); - document.addEventListener('visibilitychange', handleVisibilityChange); - return () => document.removeEventListener('visibilitychange', handleVisibilityChange); - }, []); - - return isVisible; -} - -// ============================================ -// useAutoSave -// ============================================ - -export function useAutoSave( - data: T, - saveFunction: (data: T) => void | Promise, - delay = 30000 -): void { - const dataRef = useRef(data); - - useEffect(() => { - dataRef.current = data; - }, [data]); - - useEffect(() => { - const interval = setInterval(() => { - saveFunction(dataRef.current); - }, delay); - return () => clearInterval(interval); - }, [delay, saveFunction]); -} - -export default { - useLocalStorage, - useStepNavigation, - useDebounce, - useThrottle, - useWindowSize, - useClickOutside, - useCountdown, - useAsync, - usePrevious, - useMounted, - useUpdateEffect, - useKeyPress, - useOnlineStatus, - useMediaQuery, - useScrollPosition, - useVisibility, - useAutoSave, -}; \ No newline at end of file diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 2de5c564..fed5e728 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,27 +1,15 @@ /** * @frame-fab/common — 公共库统一导出 + * + * Phase 2-B (2026-06-04): 收缩至仅 constants 子目录。 + * + * 历史包袱:原设计期望 packages/common 是可独立发布的 npm 包 + * (含 utils/formatters/motion/hooks/components/domain/validation)。 + * 实际项目内全部 0 引用,未形成 monorepo 复用。 + * 见 docs/adr/0002-frontend-monorepo-ddd.md 原始设计意图。 + * + * 现仅保留 constants 子目录(4 处真实引用)。 + * 未来若需要重提 monorepo 拆分,可基于 src/shared/* 现有实现重新组织。 */ -// Utils -export * from './utils'; - -// Formatters -export * from './formatters'; - -// Constants export * from './constants'; - -// Motion -export * from './motion'; - -// UI Components (DRY 通用组件 - 跨项目共享) -export * from './components/ui/FileUploader'; -export * from './components/ui/ProgressBar'; -export * from './components/ui/Modal'; -export * from './components/ui/ConfirmDialog'; - -// Hooks -export * from './hooks'; - -// Domain Validators (各子域校验器) -export * from './domain'; diff --git a/packages/common/src/motion/index.ts b/packages/common/src/motion/index.ts deleted file mode 100644 index 70560273..00000000 --- a/packages/common/src/motion/index.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Motion — Framer Motion 动效工具 - * - * 从 core/utils/motion.ts 和 shared/utils/index.ts 中抽取合并 - * 统一的页面过渡和组件动画配置 - */ - -import type { Variants, Easing } from 'framer-motion'; - -// ============================================ -// 基础过渡配置 -// ============================================ - -export const transitions = { - fast: { duration: 0.15 }, - normal: { duration: 0.25 }, - slow: { duration: 0.35 }, -} as const; - -export const easings: Record = { - standard: [0.4, 0, 0.2, 1], - decelerate: [0, 0, 0.2, 1], - accelerate: [0.4, 0, 1, 1], -} as const; - -// ============================================ -// 页面过渡动画 -// ============================================ - -export const pageVariants: Variants = { - initial: { opacity: 0, y: 20 }, - animate: { - opacity: 1, - y: 0, - transition: { duration: 0.25, ease: easings.standard }, - }, - exit: { - opacity: 0, - y: -20, - transition: { duration: 0.2, ease: easings.accelerate }, - }, -}; - -export const fadeInVariants: Variants = { - initial: { opacity: 0 }, - animate: { opacity: 1 }, - exit: { opacity: 0 }, -}; - -export const slideUpVariants: Variants = { - initial: { opacity: 0, y: 20 }, - animate: { opacity: 1, y: 0 }, - exit: { opacity: 0, y: -20 }, -}; - -export const scaleInVariants: Variants = { - initial: { opacity: 0, scale: 0.95 }, - animate: { opacity: 1, scale: 1 }, - exit: { opacity: 0, scale: 0.95 }, -}; - -export const listItemVariants: Variants = { - initial: { opacity: 0, y: 20 }, - animate: (i: number) => ({ - opacity: 1, - y: 0, - transition: { delay: i * 0.05, duration: 0.25, ease: easings.standard }, - }), -}; - -export const cardHoverVariants: Variants = { - rest: { - y: 0, - boxShadow: '0 4px 16px rgba(0, 0, 0, 0.06)', - }, - hover: { - y: -5, - boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12)', - transition: { duration: 0.2, ease: easings.standard }, - }, -}; - -export const buttonTapVariants: Variants = { - rest: { scale: 1 }, - tap: { scale: 0.98 }, -}; - -export const skeletonVariants: Variants = { - initial: { opacity: 0.5 }, - animate: { - opacity: 1, - transition: { repeat: Infinity, repeatType: 'reverse' as const, duration: 1 }, - }, -}; - -// ============================================ -// 工厂函数 -// ============================================ - -export function createPageTransition(custom?: { duration?: number; ease?: Easing }): Variants { - return { - ...pageVariants, - animate: { - ...pageVariants.animate, - transition: { - ...(pageVariants.animate as { transition?: Record }).transition, - duration: custom?.duration, - ease: custom?.ease, - }, - }, - }; -} - -export function createStaggerChildren(delay = 0.05): { animate: { transition: { staggerChildren: number } } } { - return { - animate: { transition: { staggerChildren: delay } }, - }; -} - -export default { - transitions, - easings, - pageVariants, - fadeInVariants, - slideUpVariants, - scaleInVariants, - listItemVariants, - cardHoverVariants, - buttonTapVariants, - skeletonVariants, -}; \ No newline at end of file diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts deleted file mode 100644 index 354093bc..00000000 --- a/packages/common/src/utils/index.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * 工具函数库 — DRY 抽取所有散落的工具函数 - * 来源:原来是散落在各 feature 目录中的重复实现 - */ - -// ============================================ -// ID 生成 -// ============================================ - -/** - * 生成唯一 ID — 使用 crypto API,替代 Math.random() - */ -export function generateId(): string { - if (typeof crypto !== 'undefined' && crypto.randomUUID) { - return crypto.randomUUID(); - } - const timestamp = Date.now().toString(36); - const randomPart = Math.random().toString(36).substring(2, 15); - return `${timestamp}_${randomPart}`; -} - -/** - * 生成带前缀的唯一ID - * @example generatePrefixedId('scene') => 'scene_1a2b3c4d_x5y6z7' - */ -export function generatePrefixedId(prefix: string): string { - const timestamp = Date.now().toString(36); - const randomPart = Math.random().toString(36).substring(2, 10); - return `${prefix}_${timestamp}_${randomPart}`; -} - -export const generateSceneId = () => generatePrefixedId('scene'); -export const generateFrameId = () => generatePrefixedId('frame'); -export const generateCharId = () => generatePrefixedId('char'); -export const generateCompId = () => generatePrefixedId('comp'); -export const generateProjectId = () => generatePrefixedId('proj'); -export const generateItemId = () => generatePrefixedId('item'); - -// ============================================ -// 日期格式化 -// ============================================ - -export interface FormatDateOptions { - format?: 'date' | 'datetime' | 'time' | 'iso'; - locale?: string; - 补零?: boolean; -} - -/** - * 统一日期格式化 — 一个函数覆盖所有格式需求 - * @example formatDate(new Date()) => '2026-01-15' - * @example formatDate(new Date(), 'datetime')=> '2026-01-15 14:30:00' - */ -export function formatDate( - date: Date | string | number, - options: FormatDateOptions = {} -): string { - const d = new Date(date); - if (isNaN(d.getTime())) return ''; - - const { format = 'date', 补零 = true } = options; - const pad = (n: number) =>补零 ? String(n).padStart(2, '0') : String(n); - - if (format === 'iso') return d.toISOString(); - - const YYYY = d.getFullYear(); - const MM = pad(d.getMonth() + 1); - const DD = pad(d.getDate()); - const HH = pad(d.getHours()); - const mm = pad(d.getMinutes()); - const ss = pad(d.getSeconds()); - - if (format === 'date') return `${YYYY}-${MM}-${DD}`; - if (format === 'time') return `${HH}:${mm}:${ss}`; - return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`; -} - -/** - * 相对时间格式化 - * @example formatRelativeTime(Date.now() - 60000) => '刚刚' - * @example formatRelativeTime(Date.now() - 3600000) => '1小时前' - */ -export function formatRelativeTime(date: Date | string | number): string { - const now = Date.now(); - const d = new Date(date).getTime(); - const diff = now - d; - - if (diff < 60000) return '刚刚'; - if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`; - if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`; - if (diff < 604800000) return `${Math.floor(diff / 86400000)}天前`; - return formatDate(d, { format: 'date' }); -} - -// ============================================ -// 校验 -// ============================================ - -export function isValidEmail(email: string): boolean { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); -} - -export function isValidURL(url: string): boolean { - try { new URL(url); return true; } catch { return false; } -} - -export function isValidPhone(phone: string): boolean { - return /^1[3-9]\d{9}$/.test(phone); -} - -/** - * 限制字符串长度(中文按2字符算) - */ -export function truncate(str: string, maxBytes: number, suffix = '...'): string { - let len = 0; - for (let i = 0; i < str.length; i++) { - const code = str.charCodeAt(i); - len += code > 0x7f ? 2 : 1; - if (len > maxBytes) return str.slice(0, i) + suffix; - } - return str; -} - -// ============================================ -// 文件操作 -// ============================================ - -export function detectFileType(filename: string): string { - const ext = filename.split('.').pop()?.toLowerCase() || ''; - const map: Record = { - mp4: 'video', mov: 'video', avi: 'video', mkv: 'video', webm: 'video', - mp3: 'audio', wav: 'audio', flac: 'audio', aac: 'audio', - jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image', - pdf: 'document', doc: 'document', docx: 'document', - txt: 'text', json: 'code', js: 'code', ts: 'code', - srt: 'subtitle', vtt: 'subtitle', ass: 'subtitle', - }; - return map[ext] ?? 'unknown'; -} - -export function readFileAsDataURL(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsDataURL(file); - }); -} - -export function readFileAsText(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.onerror = reject; - reader.readAsText(file); - }); -} - -export function downloadFile(content: string | Blob, filename: string, mimeType?: string): void { - const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType ?? 'text/plain' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); -} - -// ============================================ -// 通用工具 -// ============================================ - -export function debounce unknown>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: ReturnType; - return (...args) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - -export function throttle unknown>( - func: T, - limit: number -): (...args: Parameters) => void { - let inThrottle = false; - return (...args) => { - if (!inThrottle) { - func(...args); - inThrottle = true; - setTimeout(() => { inThrottle = false; }, limit); - } - }; -} - -export async function retry( - fn: () => Promise, - attempts = 3, - delayMs = 1000 -): Promise { - let lastError: Error; - for (let i = 0; i < attempts; i++) { - try { - return await fn(); - } catch (err) { - lastError = err as Error; - if (i < attempts - 1) await new Promise((r) => setTimeout(r, delayMs * Math.pow(2, i))); - } - } - throw lastError!; -} - -export function deepClone(obj: T): T { - if (obj === null || typeof obj !== 'object') return obj; - if (obj instanceof Date) return new Date(obj.getTime()) as unknown as T; - if (Array.isArray(obj)) return obj.map((item) => deepClone(item)) as unknown as T; - const cloned = {} as T; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - cloned[key] = deepClone(obj[key]); - } - } - return cloned; -} - -export function chunkArray(array: T[], size: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < array.length; i += size) chunks.push(array.slice(i, i + size)); - return chunks; -} - -export function sortBy(array: T[], key: keyof T, order: 'asc' | 'desc' = 'asc'): T[] { - return [...array].sort((a, b) => { - const cmp = a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0; - return order === 'asc' ? cmp : -cmp; - }); -} - -export async function copyToClipboard(text: string): Promise { - try { - await navigator.clipboard.writeText(text); - return true; - } catch { - const ta = document.createElement('textarea'); - ta.value = text; - ta.style.cssText = 'position:fixed;opacity:0'; - document.body.appendChild(ta); - ta.select(); - const ok = document.execCommand('copy'); - document.body.removeChild(ta); - return ok; - } -} - -// ============================================ -// 异步 -// ============================================ - -/** - * 延迟(Promise 版本的 setTimeout) - * @example await delay(1000) // 等待 1 秒 - */ -export function delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} \ No newline at end of file diff --git a/src/__tests__/pages/project-detail.test.tsx b/src/__tests__/pages/project-detail.test.tsx index a854c30c..8bb21311 100644 --- a/src/__tests__/pages/project-detail.test.tsx +++ b/src/__tests__/pages/project-detail.test.tsx @@ -1,10 +1,9 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; +import ProjectDetail from '@/pages/project-detail/ProjectDetailPage'; import { useProjectStore } from '@/shared/stores'; -import ProjectDetail from '@/pages/ProjectDetailPage'; - const mockNavigate = jest.fn(); const mockUpdateProject = jest.fn(); const mockDeleteProject = jest.fn(); diff --git a/src/__tests__/pages/project-edit.test.tsx b/src/__tests__/pages/project-edit.test.tsx index cfbe9c65..0723a5a2 100644 --- a/src/__tests__/pages/project-edit.test.tsx +++ b/src/__tests__/pages/project-edit.test.tsx @@ -1,7 +1,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import React, { useEffect } from 'react'; -import ProjectEdit from '@/pages/ProjectEditPage'; +import ProjectEdit from '@/pages/project-edit/ProjectEditPage'; const mockNavigate = jest.fn(); const mockLoadProjectFromFile = jest.fn(); diff --git a/src/app/router/page-preload.ts b/src/app/router/page-preload.ts index e4d40c98..81c5ae77 100644 --- a/src/app/router/page-preload.ts +++ b/src/app/router/page-preload.ts @@ -3,10 +3,10 @@ type Importer = () => Promise; const pageImporters = { home: () => import('@/pages/Home/HomePage'), workflow: () => import('@/pages/Workflow/WorkflowPage'), - projectEdit: () => import('@/pages/ProjectEdit/ProjectEditPage'), - projectDetail: () => import('@/pages/ProjectDetail/ProjectDetailPage'), + projectEdit: () => import('@/pages/project-edit/ProjectEditPage'), + projectDetail: () => import('@/pages/project-detail/ProjectDetailPage'), settings: () => import('@/pages/Settings/SettingsPage'), - autoPipeline: () => import('@/pages/AutoPipeline/AutoPipelinePage'), + autoPipeline: () => import('@/pages/auto-pipeline/AutoPipelinePage'), } as const; const routeImporterMap: Array<{ prefix: string; importer: Importer }> = [ @@ -36,7 +36,7 @@ export async function preloadPage(importer: Importer, key: string): Promise path.startsWith(item.prefix)); + const hit = routeImporterMap.find((item) => path.startsWith(item.prefix)); return hit ? hit.prefix : null; } @@ -44,9 +44,9 @@ export function preloadPageByPath( path: string, preloader: (importer: Importer, key: string) => void = (importer, key) => { void preloadPage(importer, key); - }, + } ): void { - const hit = routeImporterMap.find(item => path.startsWith(item.prefix)); + const hit = routeImporterMap.find((item) => path.startsWith(item.prefix)); if (!hit) return; preloader(hit.importer, hit.prefix); } diff --git a/src/core/router/page-preload.ts b/src/core/router/page-preload.ts index bf37ea5a..798a6271 100644 --- a/src/core/router/page-preload.ts +++ b/src/core/router/page-preload.ts @@ -3,11 +3,11 @@ type Importer = () => Promise; const pageImporters = { home: () => import('@/pages/Home/HomePage'), workflow: () => import('@/pages/Workflow/WorkflowPage'), - projectEdit: () => import('@/pages/ProjectEdit/ProjectEditPage'), - projectDetail: () => import('@/pages/ProjectDetail/ProjectDetailPage'), - scriptDetail: () => import('@/pages/ProjectEdit/ScriptDetailPage'), + projectEdit: () => import('@/pages/project-edit/ProjectEditPage'), + projectDetail: () => import('@/pages/project-detail/ProjectDetailPage'), + scriptDetail: () => import('@/pages/project-edit/ScriptDetailPage'), settings: () => import('@/pages/Settings/SettingsPage'), - autoPipeline: () => import('@/pages/AutoPipeline/AutoPipelinePage'), + autoPipeline: () => import('@/pages/auto-pipeline/AutoPipelinePage'), } as const; const routeImporterMap: Array<{ prefix: string; importer: Importer }> = [ @@ -38,7 +38,7 @@ export async function preloadPage(importer: Importer, key: string): Promise path.startsWith(item.prefix)); + const hit = routeImporterMap.find((item) => path.startsWith(item.prefix)); return hit ? hit.prefix : null; } @@ -46,9 +46,9 @@ export function preloadPageByPath( path: string, preloader: (importer: Importer, key: string) => void = (importer, key) => { void preloadPage(importer, key); - }, + } ): void { - const hit = routeImporterMap.find(item => path.startsWith(item.prefix)); + const hit = routeImporterMap.find((item) => path.startsWith(item.prefix)); if (!hit) return; preloader(hit.importer, hit.prefix); } diff --git a/src/pages/AutoPipeline/AutoPipelinePage.tsx b/src/pages/AutoPipeline/AutoPipelinePage.tsx deleted file mode 100644 index 7b8fd339..00000000 --- a/src/pages/AutoPipeline/AutoPipelinePage.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@/pages/auto-pipeline/AutoPipelinePage'; -export { default } from '@/pages/auto-pipeline/AutoPipelinePage'; diff --git a/src/pages/ProjectDetail/ProjectDetailPage.tsx b/src/pages/ProjectDetail/ProjectDetailPage.tsx deleted file mode 100644 index b1c8f7af..00000000 --- a/src/pages/ProjectDetail/ProjectDetailPage.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@/pages/project-detail/ProjectDetailPage'; -export { default } from '@/pages/project-detail/ProjectDetailPage'; diff --git a/src/pages/ProjectEdit/ProjectEditPage.tsx b/src/pages/ProjectEdit/ProjectEditPage.tsx deleted file mode 100644 index 972df712..00000000 --- a/src/pages/ProjectEdit/ProjectEditPage.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@/pages/project-edit/ProjectEditPage'; -export { default } from '@/pages/project-edit/ProjectEditPage'; diff --git a/src/pages/ProjectEdit/ScriptDetailPage.tsx b/src/pages/ProjectEdit/ScriptDetailPage.tsx deleted file mode 100644 index d7c05d9f..00000000 --- a/src/pages/ProjectEdit/ScriptDetailPage.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@/pages/project-edit/ScriptDetailPage'; -export { default } from '@/pages/project-edit/ScriptDetailPage'; diff --git a/src/pages/index.ts b/src/pages/index.ts deleted file mode 100644 index e3d49ac2..00000000 --- a/src/pages/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as ProjectDetail } from './ProjectDetail/ProjectDetailPage'; -export { default as ProjectEdit } from './ProjectEdit/ProjectEditPage';