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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
// 风格
|
||||
|
||||
Reference in New Issue
Block a user