feat(frontend): 构建前端 MVP,实现完整 UI 框架与 mock 数据层
- 安装 zustand + react-router-dom 依赖 - 修复 Vite 代理配置,添加 /auth 路由转发 - 实现 API 封装层(client.ts 统一 fetch、auth.ts 真实调用) - 实现 mock 数据层(mock.ts 模拟 WebSocket 管线进度) - 实现 4 个 zustand stores(auth/project/task/generation) - 实现 5 个组件(StyleSelector/PromptEditor/GenerateForm/ProgressBar/AssetPreview) - 实现 5 个页面(Login/Register/Project/Generate/Result) - 实现路由含 ProtectedRoute 守卫 - 添加暗色主题全局样式 - 更新前端文档与 API 文档
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import type { Asset, PipelineProgress, PipelineStage, Project, Task } from './types'
|
||||
|
||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const randomDelay = () => delay(300 + Math.random() * 500)
|
||||
|
||||
// Mock 数据
|
||||
const MOCK_PROJECT: Project = {
|
||||
id: 'proj-default',
|
||||
name: '我的工程',
|
||||
style: {
|
||||
kvPairs: {
|
||||
artStyle: 'pixel',
|
||||
palette: 'warm',
|
||||
lineWeight: 'thin',
|
||||
scene: 'forest',
|
||||
lighting: 'bright',
|
||||
mood: 'cheerful',
|
||||
},
|
||||
},
|
||||
createdAt: '2026-05-20T10:00:00Z',
|
||||
}
|
||||
|
||||
const MOCK_TASKS: Task[] = [
|
||||
{
|
||||
id: 'task-001',
|
||||
projectId: 'proj-default',
|
||||
prompt: '一个拿剑的小人',
|
||||
assetType: 'sprite',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
createdAt: '2026-05-24T09:00:00Z',
|
||||
updatedAt: '2026-05-24T09:02:00Z',
|
||||
},
|
||||
{
|
||||
id: 'task-002',
|
||||
projectId: 'proj-default',
|
||||
prompt: '森林背景',
|
||||
assetType: 'background',
|
||||
status: 'failed',
|
||||
error: '生成超时',
|
||||
createdAt: '2026-05-24T08:00:00Z',
|
||||
updatedAt: '2026-05-24T08:10:00Z',
|
||||
},
|
||||
{
|
||||
id: 'task-003',
|
||||
projectId: 'proj-default',
|
||||
prompt: '魔法药水瓶',
|
||||
assetType: 'ui',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
createdAt: '2026-05-23T15:00:00Z',
|
||||
updatedAt: '2026-05-23T15:03:00Z',
|
||||
},
|
||||
]
|
||||
|
||||
const MOCK_ASSETS: Asset[] = [
|
||||
{
|
||||
id: 'asset-001',
|
||||
url: 'https://placehold.co/256x256/e94560/1a1a2e?text=Sprite',
|
||||
format: 'png',
|
||||
width: 256,
|
||||
height: 256,
|
||||
metadata: {
|
||||
frameWidth: 64,
|
||||
frameHeight: 64,
|
||||
frameCount: 16,
|
||||
directions: 4,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Mock API 函数
|
||||
export async function mockGetProject(_id: string): Promise<Project> {
|
||||
await randomDelay()
|
||||
return { ...MOCK_PROJECT }
|
||||
}
|
||||
|
||||
export async function mockGetStyle(
|
||||
_projectId: string
|
||||
): Promise<Record<string, string>> {
|
||||
await randomDelay()
|
||||
return { ...MOCK_PROJECT.style.kvPairs }
|
||||
}
|
||||
|
||||
export async function mockSaveStyle(
|
||||
_projectId: string,
|
||||
_kvPairs: Record<string, string>
|
||||
): Promise<void> {
|
||||
await randomDelay()
|
||||
}
|
||||
|
||||
export async function mockGetTasks(_projectId: string): Promise<Task[]> {
|
||||
await randomDelay()
|
||||
return MOCK_TASKS.map(t => ({ ...t }))
|
||||
}
|
||||
|
||||
export async function mockGetTask(taskId: string): Promise<Task> {
|
||||
await randomDelay()
|
||||
const task = MOCK_TASKS.find(t => t.id === taskId)
|
||||
if (!task) throw new Error('任务不存在')
|
||||
return { ...task }
|
||||
}
|
||||
|
||||
export async function mockGetAssets(_taskId: string): Promise<Asset[]> {
|
||||
await randomDelay()
|
||||
return MOCK_ASSETS.map(a => ({ ...a }))
|
||||
}
|
||||
|
||||
export async function mockSubmitGenerate(
|
||||
_projectId: string,
|
||||
prompt: string,
|
||||
assetType: string
|
||||
): Promise<string> {
|
||||
await delay(500)
|
||||
const taskId = `task-${Date.now()}`
|
||||
MOCK_TASKS.unshift({
|
||||
id: taskId,
|
||||
projectId: _projectId,
|
||||
prompt,
|
||||
assetType,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
return taskId
|
||||
}
|
||||
|
||||
// 模拟 WebSocket 管线进度
|
||||
const STAGES: PipelineStage[] = [
|
||||
'prompt_builder',
|
||||
'asset_generator',
|
||||
'quality_supervisor',
|
||||
'format_adapter',
|
||||
]
|
||||
|
||||
const STAGE_LABELS: Record<PipelineStage, string> = {
|
||||
prompt_builder: '提示词构建',
|
||||
asset_generator: '素材生成',
|
||||
quality_supervisor: '质量检查',
|
||||
format_adapter: '格式转换',
|
||||
}
|
||||
|
||||
export { STAGE_LABELS }
|
||||
|
||||
export function createMockWebSocket(
|
||||
_taskId: string,
|
||||
onProgress: (msg: PipelineProgress) => void,
|
||||
onComplete: (assets: Asset[]) => void,
|
||||
_onError?: (error: string) => void
|
||||
): () => void {
|
||||
let stageIndex = 0
|
||||
let tick = 0
|
||||
let retryCount = 0
|
||||
const maxRetries = 1
|
||||
let cancelled = false
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (cancelled) return
|
||||
|
||||
const stage = STAGES[stageIndex]
|
||||
const stageProgress = (tick % 3) * 50 // 0, 50, 100
|
||||
|
||||
// 质检阶段有概率重试
|
||||
if (
|
||||
stage === 'quality_supervisor' &&
|
||||
stageProgress === 100 &&
|
||||
retryCount < maxRetries &&
|
||||
Math.random() < 0.3
|
||||
) {
|
||||
onProgress({
|
||||
stage: 'quality_supervisor',
|
||||
status: 'failed',
|
||||
progress: 100,
|
||||
retryCount: retryCount + 1,
|
||||
rejectReason: '风格一致性不足',
|
||||
})
|
||||
retryCount++
|
||||
stageIndex = 0
|
||||
tick = 0
|
||||
return
|
||||
}
|
||||
|
||||
const isLastTickOfStage = stageProgress === 100
|
||||
const isLastStage = stageIndex === STAGES.length - 1
|
||||
|
||||
if (isLastTickOfStage && isLastStage) {
|
||||
// 管线完成
|
||||
onProgress({
|
||||
stage: 'format_adapter',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
result: { assets: MOCK_ASSETS },
|
||||
})
|
||||
onComplete(MOCK_ASSETS)
|
||||
clearInterval(interval)
|
||||
return
|
||||
}
|
||||
|
||||
onProgress({
|
||||
stage,
|
||||
status: isLastTickOfStage ? 'completed' : 'running',
|
||||
progress: stageProgress,
|
||||
retryCount: retryCount > 0 ? retryCount : undefined,
|
||||
})
|
||||
|
||||
tick++
|
||||
if (isLastTickOfStage) {
|
||||
stageIndex++
|
||||
tick = 0
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user