feat(generate): 图片持久化到本地 generation/ 目录,前端对接真实 API

后端:
- POST /api/v1/generate 接收 projectId,生成 taskId,将图片保存至 generation/{projectId}/{taskId}/
- 新增 GET /api/v1/tasks/:taskId 和 GET /api/v1/tasks/:taskId/assets 查询端点
- 添加 /generation 静态文件服务,前端可通过 URL 直接访问生成的图片
- PipelineInput 增加 ProjectID/TaskID 字段

前端:
- generate.ts 对接真实后端 API,移除 mock 模式
- 401 时不再强制登出跳转,改为抛错由调用方处理
- GeneratePage 加载工程风格,传递 projectId 和完整参数
- generation store 简化为同步 API 模式,移除 WebSocket mock
- ResultPage 使用 getTask/getAssets 按 taskId 查询结果
This commit is contained in:
2026-05-25 12:27:07 +08:00
parent 6bda5a1f35
commit 74c1725473
13 changed files with 297 additions and 114 deletions
+37 -45
View File
@@ -1,82 +1,74 @@
import { create } from 'zustand'
import type { Asset, PipelineProgress, PipelineStage } from '../api/types'
import type { Asset, GenerateRequest, GenerateResponse } from '../api/types'
import { submitGenerate } from '../api/generate'
import { createMockWebSocket } from '../api/mock'
type Status = 'idle' | 'submitting' | 'running' | 'completed' | 'failed'
interface GenerationState {
taskId: string | null
stage: PipelineStage | null
projectId: string | null
progress: number
status: Status
retryCount: number
rejectReason: string | null
assets: Asset[]
error: string | null
submit: (projectId: string, prompt: string, assetType: string) => Promise<void>
handleProgress: (msg: PipelineProgress) => void
submit: (req: GenerateRequest) => Promise<void>
reset: () => void
}
let cleanupWs: (() => void) | null = null
export const useGenerationStore = create<GenerationState>((set, get) => ({
export const useGenerationStore = create<GenerationState>((set) => ({
taskId: null,
stage: null,
projectId: null,
progress: 0,
status: 'idle',
retryCount: 0,
rejectReason: null,
assets: [],
error: null,
submit: async (projectId, prompt, assetType) => {
submit: async (req) => {
set({ status: 'submitting', error: null })
try {
const taskId = await submitGenerate(projectId, prompt, assetType)
set({ taskId, status: 'running', progress: 0 })
set({ status: 'running', progress: 30 })
const result = await submitGenerate(req)
// 启动 mock WebSocket
cleanupWs = createMockWebSocket(
taskId,
(msg) => get().handleProgress(msg),
(assets) => {
set({ status: 'completed', assets, progress: 100 })
},
(error) => {
set({ status: 'failed', error })
}
)
set({ progress: 80 })
const assets = mapAssets(result)
set({
taskId: result.taskId,
projectId: req.projectId,
status: 'completed',
progress: 100,
assets,
})
} catch (err) {
set({ status: 'failed', error: (err as Error).message })
}
},
handleProgress: (msg) => {
set({
stage: msg.stage,
progress: msg.progress,
retryCount: msg.retryCount ?? get().retryCount,
rejectReason: msg.rejectReason ?? null,
})
if (msg.result?.assets) {
set({ assets: msg.result.assets })
const errMsg = (err as Error).message
set({ status: 'failed', error: errMsg })
}
},
reset: () => {
cleanupWs?.()
cleanupWs = null
set({
taskId: null,
stage: null,
projectId: null,
progress: 0,
status: 'idle',
retryCount: 0,
rejectReason: null,
assets: [],
error: null,
})
},
}))
function mapAssets(resp: GenerateResponse): Asset[] {
return resp.assets.map((a, i) => ({
id: `asset-${i}`,
url: a.url,
format: a.format,
width: resp.metadata.frameWidth,
height: resp.metadata.frameHeight,
metadata: {
frameWidth: resp.metadata.frameWidth,
frameHeight: resp.metadata.frameHeight,
frameCount: resp.metadata.frameCount,
directions: resp.metadata.directions,
},
}))
}