# 前端工程 ## 技术栈 Vite 6 + React 18 + TypeScript + zustand + react-router-dom | 类别 | 选型 | 说明 | |------|------|------| | 构建 | Vite 6 | 开发服务器端口 3000,`/api` 代理到 `localhost:8080` | | UI 框架 | React 18 | 函数组件 + Hooks | | 状态管理 | zustand | 轻量,支持 devtools 中间件 | | 路由 | react-router-dom v6 | SPA 模式 | | 实时通信 | 原生 WebSocket | 连接后端 `/api/v1/tasks/:taskId/ws` | | HTTP 请求 | fetch + 封装层 | 统一错误处理、响应解包 | ## 目录结构 ``` frontend/src/ ├── main.tsx # 入口 ├── App.tsx # 根组件 + 路由配置 ├── api/ # API 封装层 │ ├── client.ts # fetch 封装:baseURL、统一错误处理、响应解包 │ ├── project.ts # 工程 CRUD + 风格 GET/PUT │ ├── generate.ts # POST /generate、GET /tasks/:taskId、GET /tasks/:taskId/assets │ └── types.ts # API 请求/响应类型定义(Task、Asset、Style、PipelineProgress 等) ├── stores/ # zustand stores │ ├── project.ts # 当前工程(id、风格、任务列表) │ ├── task.ts # 当前任务草稿(用户文本、素材类型、任务风格覆盖、技术参数) │ └── generation.ts # 生成状态(taskId、进度、阶段、结果、错误) ├── pages/ # 页面级组件 │ ├── ProjectPage.tsx # 工程首页:工程风格配置 + 任务列表 │ ├── GeneratePage.tsx # 生成页:提示词构建 + 提交 │ └── ResultPage.tsx # 结果页:素材预览 + 下载 + 元数据 ├── components/ # 可复用组件 │ ├── StyleSelector.tsx # 风格选择器 │ ├── PromptEditor.tsx # 三段式提示词编辑器 │ ├── GenerateForm.tsx # 生成表单 │ ├── ProgressBar.tsx # 管线进度条(显示当前阶段 + 重试状态) │ └── AssetPreview.tsx # 素材预览(spritesheet 预览、单帧预览) ├── hooks/ # 自定义 Hooks │ ├── useGenerate.ts # 提交生成任务 + WebSocket 订阅进度 │ └── useProjectStyle.ts # 工程风格加载/保存 ├── router/ # 路由定义 │ └── index.tsx └── utils/ # 工具函数 └── style.ts # 风格合并逻辑(与后端保持一致的 merge 算法) ``` ## 路由规划 | 路径 | 页面 | 说明 | |------|------|------| | `/` | — | 重定向到默认工程 | | `/projects/:projectId` | ProjectPage | 工程首页,配置工程风格,查看历史任务 | | `/projects/:projectId/generate` | GeneratePage | 提示词构建 + 提交生成 | | `/projects/:projectId/tasks/:taskId` | ResultPage | 任务结果页,预览素材、下载 | ## 组件树 ```mermaid graph TD App --> Router Router --> ProjectPage Router --> GeneratePage Router --> ResultPage ProjectPage --> StyleSelector["StyleSelector(工程风格编辑)"] ProjectPage --> TaskList["TaskList(历史任务列表)"] GeneratePage --> GenerateForm GenerateForm --> AssetTypePicker["AssetTypePicker"] GenerateForm --> StyleSelector2["StyleSelector(任务风格覆盖)"] GenerateForm --> PromptEditor["PromptEditor(三段式预览)"] GenerateForm --> ParamsForm["ParamsForm(分辨率、帧数)"] GeneratePage --> ProgressBar["ProgressBar(管线进度)"] ResultPage --> AssetPreview["AssetPreview(素材预览)"] ResultPage --> MetadataPanel["MetadataPanel(元数据)"] ResultPage --> DownloadButton["DownloadButton"] ``` ## 核心交互流程 ### 生成流程 ```mermaid sequenceDiagram actor User participant GP as GeneratePage participant API as 后端 API participant WS as WebSocket User->>GP: 填写 prompt、选择素材类型 User->>GP: StyleSelector 点选任务风格覆盖 GP->>GP: PromptEditor 实时预览三段式提示词 User->>GP: 点击提交 GP->>API: POST /api/v1/generate API-->>GP: 返回 taskId GP->>WS: ws://host/api/v1/tasks/:taskId/ws loop 管线执行中 WS-->>GP: PipelineProgress(stage + progress) GP->>GP: ProgressBar 更新阶段和进度 end WS-->>GP: format_adapter completed + assets GP->>GP: 跳转 ResultPage ``` 管线阶段对应 Eino Graph 节点,ProgressBar 展示: | 阶段 | 说明 | ProgressBar 展示 | |------|------|-----------------| | `prompt_builder` | 合并风格,生成三段式提示词 | 第 1 步 | | `asset_generator` | 调用 AI 推理 API 出图 | 第 2 步 | | `quality_supervisor` | 视觉模型质检 | 第 3 步(可能回退到第 1 步) | | `format_adapter` | 格式转换、spritesheet 打包 | 第 4 步 | 质检重试时,ProgressBar 显示回退动画和重试次数。 ### 风格编辑流程 ```mermaid sequenceDiagram actor User participant SP as StyleSelector participant API as 后端 API User->>SP: 进入 ProjectPage SP->>API: GET /api/v1/projects/:id/style API-->>SP: 返回 kvPairs SP->>SP: 按分类展示风格键值对 User->>SP: 点选/修改风格 SP->>SP: 实时展示当前配置 User->>SP: 点击保存 SP->>API: PUT /api/v1/projects/:id/style API-->>SP: 保存成功 ``` ## 错误处理 ### API 请求错误 - `api/client.ts` 统一拦截 `code !== 0` 的响应,抛出业务异常 - 401:自动清除本地 Token,跳转到登录页(或提示重新登录) - 403:提示无权访问,不自动跳转 - 429:提示请求过于频繁,稍后重试 - 500:展示通用错误提示 ### WebSocket 断连 - 连接断开后自动重连(指数退避,最大间隔 10 秒) - 重连失败超过 3 次后,降级为轮询模式(`GET /api/v1/tasks/:taskId`,间隔 2-3 秒) - 连接恢复后自动切回 WebSocket ### 加载状态 - 页面级加载(如 ProjectPage 进入时):展示骨架屏或 Loading 指示器 - 操作级提交(如保存风格、提交生成):按钮置为 loading 态,防止重复提交 ## 状态管理 ### zustand stores **project store** — 当前工程 ```typescript interface ProjectStore { projectId: string; name: string; style: Record; // kvPairs loading: boolean; loadProject: (projectId: string) => Promise; loadStyle: (projectId: string) => Promise; updateStyle: (kvPairs: Record) => void; saveStyle: () => Promise; } ``` **task store** — 任务草稿(对应后端 `PipelineInput`) ```typescript interface TaskStore { prompt: string; assetType: 'sprite' | 'background' | 'ui' | 'animation'; taskStyle: Record; // 仅覆盖的键 params: { resolution: number; frames?: { directions: number; framesPerDirection: number }; format: 'spritesheet' | 'individual'; }; setPrompt: (text: string) => void; setAssetType: (type: TaskStore['assetType']) => void; toggleTaskStyle: (key: string, value: string) => void; setParams: (params: Partial) => void; reset: () => void; } ``` **generation store** — 生成状态 ```typescript interface GenerationStore { taskId: string | null; stage: PipelineStage | null; progress: number; status: 'idle' | 'submitting' | 'running' | 'completed' | 'failed'; retryCount: number; rejectReason: string | null; assets: Asset[]; error: string | null; submit: (projectId: string, task: TaskStore) => Promise; reset: () => void; } type PipelineStage = | 'prompt_builder' | 'asset_generator' | 'quality_supervisor' | 'format_adapter'; ``` ## API 封装 `api/client.ts` 统一封装: - baseURL 从环境变量读取,开发模式默认 `/api/v1` - 所有响应按 `{ code, message, data }` 解包,`code !== 0` 时抛错 - 统一 401/403/500 错误处理 - WebSocket 连接封装为 `createTaskSocket(taskId)` 返回可订阅对象 ### API 类型定义(api/types.ts) ```typescript // 请求 interface CreateProjectRequest { name: string; } interface GenerateRequest { projectId: string; prompt: string; assetType: 'sprite' | 'background' | 'ui' | 'animation'; taskStyle?: Record; params?: { resolution?: number; frames?: { directions?: number; framesPerDirection?: number }; format?: 'spritesheet' | 'individual'; }; } interface UpdateStyleRequest { kvPairs: Record; } // 响应 interface Project { id: string; name: string; style: { kvPairs: Record }; createdAt: string; } interface Task { id: string; projectId: string; prompt: string; assetType: string; status: 'pending' | 'running' | 'completed' | 'failed'; stage?: PipelineStage; progress?: number; retryCount?: number; error?: string | null; createdAt: string; updatedAt: string; } interface Asset { id: string; url: string; format: string; width: number; height: number; metadata: { frameWidth?: number; frameHeight?: number; frameCount?: number; directions?: number; }; } // WebSocket 消息 interface PipelineProgress { stage: PipelineStage; status: 'running' | 'completed' | 'failed'; progress: number; message?: string; retryCount?: number; rejectReason?: string; result?: { assets: Asset[] }; error?: string; } ``` ## 预设风格键分类 前端 StyleSelector 以分类标签组织,完整键值表见 [预设风格键](style-keys.md)。 StyleSelector 两种使用场景: 1. **工程风格**(ProjectPage):全量编辑,保存到后端 2. **任务覆盖**(GeneratePage):基于工程风格展示,高亮已覆盖的键,仅记录差异