refactor: 前端重构为项目+任务双核心模型

- 新增 HomePage 项目画廊页,支持新建/删除工程
- 新增 ProjectDetailPage 三栏工作台(配置+生成+任务列表)
- 新增 CustomTagsEditor 支持自定义风格标签(custom: 前缀存入 kvPairs)
- 重构 project store 为 draft/commit 模式,支持手动保存/取消
- 新增 projectList store 管理工程列表
- 扩展 mock.ts 为多工程数据结构
- 扩展 api/project.ts 新增项目 CRUD API(mock-gated)
- 简化 ResultPage,仅展示素材预览
- 删除 GeneratePage、ProjectPage 及未使用的 hooks
- 更新 docs/ 文档(frontend.md、api.md、style-keys.md、_index.md)
This commit is contained in:
2026-05-25 16:15:31 +08:00
parent 2bc0ec2eaa
commit a603f5548c
26 changed files with 1397 additions and 644 deletions
+4
View File
@@ -64,3 +64,7 @@ export function put<T>(url: string, body?: unknown): Promise<T> {
body: body ? JSON.stringify(body) : undefined,
})
}
export function del<T>(url: string): Promise<T> {
return request<T>(url, { method: 'DELETE' })
}
+172 -58
View File
@@ -3,60 +3,114 @@ import type { Asset, PipelineProgress, PipelineStage, Project, Task } from './ty
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',
// Mock 工程数据
const MOCK_PROJECTS: Project[] = [
{
id: 'proj-pixel-forest',
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',
createdAt: '2026-05-20T10:00:00Z',
taskCount: 3,
lastActivityAt: '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: 'proj-dark-dungeon',
name: '暗黑地牢',
style: {
kvPairs: {
artStyle: 'hand-drawn',
palette: 'muted',
lineWeight: 'thick',
scene: 'dungeon',
lighting: 'dim',
mood: 'dark',
},
},
createdAt: '2026-05-18T14:00:00Z',
taskCount: 1,
lastActivityAt: '2026-05-22T16:30: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',
id: 'proj-neon-city',
name: '霓虹都市',
style: {
kvPairs: {
artStyle: 'vector',
palette: 'vibrant',
lineWeight: 'medium',
scene: 'city',
lighting: 'neon',
mood: 'epic',
},
},
createdAt: '2026-05-15T08:00:00Z',
taskCount: 0,
lastActivityAt: '2026-05-15T08:00:00Z',
},
]
// Mock 任务数据(按工程分组)
const MOCK_TASKS: Record<string, Task[]> = {
'proj-pixel-forest': [
{
id: 'task-001',
projectId: 'proj-pixel-forest',
prompt: '一个拿剑的小人',
assetType: 'sprite',
status: 'completed',
progress: 100,
createdAt: '2026-05-24T09:00:00Z',
updatedAt: '2026-05-24T09:02:00Z',
},
{
id: 'task-002',
projectId: 'proj-pixel-forest',
prompt: '森林背景',
assetType: 'background',
status: 'failed',
error: '生成超时',
createdAt: '2026-05-24T08:00:00Z',
updatedAt: '2026-05-24T08:10:00Z',
},
{
id: 'task-003',
projectId: 'proj-pixel-forest',
prompt: '魔法药水瓶',
assetType: 'ui',
status: 'completed',
progress: 100,
createdAt: '2026-05-23T15:00:00Z',
updatedAt: '2026-05-23T15:03:00Z',
},
],
'proj-dark-dungeon': [
{
id: 'task-004',
projectId: 'proj-dark-dungeon',
prompt: '骷髅战士',
assetType: 'sprite',
status: 'completed',
progress: 100,
createdAt: '2026-05-22T16:28:00Z',
updatedAt: '2026-05-22T16:30:00Z',
},
],
'proj-neon-city': [],
}
const MOCK_ASSETS: Asset[] = [
{
id: 'asset-001',
key: 'generation/proj-001/task-001/0.png',
key: 'generation/proj-pixel-forest/task-001/0.png',
url: 'https://placehold.co/256x256/e94560/1a1a2e?text=Sprite',
format: 'png',
width: 256,
@@ -70,36 +124,89 @@ const MOCK_ASSETS: Asset[] = [
},
]
// Mock API 函数
export async function mockGetProject(_id: string): Promise<Project> {
// Mock API 函数 — 工程
export async function mockListProjects(): Promise<Project[]> {
await randomDelay()
return { ...MOCK_PROJECT }
return MOCK_PROJECTS.map(p => ({ ...p }))
}
export async function mockGetProject(projectId: string): Promise<Project> {
await randomDelay()
const project = MOCK_PROJECTS.find(p => p.id === projectId)
if (!project) throw new Error('工程不存在')
return { ...project }
}
export async function mockCreateProject(
name: string,
style?: Record<string, string>
): Promise<Project> {
await delay(500)
const id = `proj-${Date.now()}`
const now = new Date().toISOString()
const project: Project = {
id,
name,
style: { kvPairs: style || {} },
createdAt: now,
taskCount: 0,
lastActivityAt: now,
}
MOCK_PROJECTS.unshift(project)
MOCK_TASKS[id] = []
return { ...project }
}
export async function mockDeleteProject(projectId: string): Promise<void> {
await randomDelay()
const index = MOCK_PROJECTS.findIndex(p => p.id === projectId)
if (index !== -1) MOCK_PROJECTS.splice(index, 1)
delete MOCK_TASKS[projectId]
}
export async function mockUpdateProject(
projectId: string,
data: { name?: string }
): Promise<Project> {
await randomDelay()
const project = MOCK_PROJECTS.find(p => p.id === projectId)
if (!project) throw new Error('工程不存在')
if (data.name !== undefined) project.name = data.name
return { ...project }
}
// Mock API 函数 — 风格
export async function mockGetStyle(
_projectId: string
projectId: string
): Promise<Record<string, string>> {
await randomDelay()
return { ...MOCK_PROJECT.style.kvPairs }
const project = MOCK_PROJECTS.find(p => p.id === projectId)
if (!project) throw new Error('工程不存在')
return { ...project.style.kvPairs }
}
export async function mockSaveStyle(
_projectId: string,
_kvPairs: Record<string, string>
projectId: string,
kvPairs: Record<string, string>
): Promise<void> {
await randomDelay()
const project = MOCK_PROJECTS.find(p => p.id === projectId)
if (project) project.style.kvPairs = { ...kvPairs }
}
export async function mockGetTasks(_projectId: string): Promise<Task[]> {
// Mock API 函数 — 任务
export async function mockGetTasks(projectId: string): Promise<Task[]> {
await randomDelay()
return MOCK_TASKS.map(t => ({ ...t }))
return (MOCK_TASKS[projectId] || []).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 }
for (const tasks of Object.values(MOCK_TASKS)) {
const task = tasks.find(t => t.id === taskId)
if (task) return { ...task }
}
throw new Error('任务不存在')
}
export async function mockGetAssets(_taskId: string): Promise<Asset[]> {
@@ -108,21 +215,28 @@ export async function mockGetAssets(_taskId: string): Promise<Asset[]> {
}
export async function mockSubmitGenerate(
_projectId: string,
projectId: string,
prompt: string,
assetType: string
): Promise<string> {
await delay(500)
const taskId = `task-${Date.now()}`
MOCK_TASKS.unshift({
if (!MOCK_TASKS[projectId]) MOCK_TASKS[projectId] = []
MOCK_TASKS[projectId].unshift({
id: taskId,
projectId: _projectId,
projectId,
prompt,
assetType,
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
// 更新工程的 lastActivityAt 和 taskCount
const project = MOCK_PROJECTS.find(p => p.id === projectId)
if (project) {
project.lastActivityAt = new Date().toISOString()
project.taskCount = (project.taskCount || 0) + 1
}
return taskId
}
+31 -2
View File
@@ -1,16 +1,45 @@
import type { Project, Task } from './types'
import {
mockListProjects,
mockGetProject,
mockCreateProject,
mockDeleteProject,
mockUpdateProject,
mockGetStyle,
mockGetTasks,
mockSaveStyle,
mockGetTasks,
} from './mock'
const USE_MOCK = true
export async function listProjects(): Promise<Project[]> {
if (USE_MOCK) return mockListProjects()
throw new Error('Not implemented')
}
export async function getProject(projectId: string): Promise<Project> {
if (USE_MOCK) return mockGetProject(projectId)
// TODO: 真实 API 调用
throw new Error('Not implemented')
}
export async function createProject(
name: string,
style?: Record<string, string>
): Promise<Project> {
if (USE_MOCK) return mockCreateProject(name, style)
throw new Error('Not implemented')
}
export async function deleteProject(projectId: string): Promise<void> {
if (USE_MOCK) return mockDeleteProject(projectId)
throw new Error('Not implemented')
}
export async function updateProject(
projectId: string,
data: { name?: string }
): Promise<Project> {
if (USE_MOCK) return mockUpdateProject(projectId, data)
throw new Error('Not implemented')
}
+5 -3
View File
@@ -1,5 +1,5 @@
import { post } from './client'
import { STYLE_CATEGORIES } from '../utils/style'
import { STYLE_CATEGORIES, getCustomTags } from '../utils/style'
interface OptimizePromptParams {
tags: string[]
@@ -14,15 +14,17 @@ interface OptimizePromptResponse {
}
/**
* 从风格键值对中提取标签的中文名称作为 tags
* 从风格键值对中提取标签的中文名称作为 tags,包含自定义标签
*/
export function extractTags(style: Record<string, string>): string[] {
return STYLE_CATEGORIES.flatMap(cat => {
const presetTags = STYLE_CATEGORIES.flatMap(cat => {
const value = style[cat.key]
if (!value) return []
const option = cat.options.find(o => o.value === value)
return option ? [option.label] : []
})
const customTags = getCustomTags(style)
return [...presetTags, ...customTags]
}
/**
+3
View File
@@ -50,10 +50,13 @@ export interface Project {
name: string
style: { kvPairs: Record<string, string> }
createdAt: string
taskCount?: number
lastActivityAt?: string
}
export interface CreateProjectRequest {
name: string
style?: Record<string, string>
}
// 风格