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,69 @@
|
||||
import { create } from 'zustand'
|
||||
import * as authApi from '../api/auth'
|
||||
import { clearToken, getToken } from '../api/client'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: User | null
|
||||
isAuthenticated: boolean
|
||||
loading: boolean
|
||||
error: string | null
|
||||
login: (username: string, password: string) => Promise<void>
|
||||
register: (username: string, password: string, email?: string) => Promise<void>
|
||||
logout: () => void
|
||||
loadFromStorage: () => void
|
||||
clearError: () => void
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
login: async (username, password) => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const data = await authApi.login(username, password)
|
||||
set({
|
||||
user: { id: data.user.id, username: data.user.username, email: data.user.email },
|
||||
isAuthenticated: true,
|
||||
loading: false,
|
||||
})
|
||||
} catch (err) {
|
||||
set({ loading: false, error: (err as Error).message })
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
register: async (username, password, email) => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
await authApi.register(username, password, email)
|
||||
set({ loading: false })
|
||||
} catch (err) {
|
||||
set({ loading: false, error: (err as Error).message })
|
||||
throw err
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
clearToken()
|
||||
set({ user: null, isAuthenticated: false })
|
||||
},
|
||||
|
||||
loadFromStorage: () => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
// token 存在视为已登录,用户信息从 token 解析或后续 /auth/me 获取
|
||||
set({ isAuthenticated: true })
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}))
|
||||
@@ -0,0 +1,82 @@
|
||||
import { create } from 'zustand'
|
||||
import type { Asset, PipelineProgress, PipelineStage } 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
|
||||
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
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
let cleanupWs: (() => void) | null = null
|
||||
|
||||
export const useGenerationStore = create<GenerationState>((set, get) => ({
|
||||
taskId: null,
|
||||
stage: null,
|
||||
progress: 0,
|
||||
status: 'idle',
|
||||
retryCount: 0,
|
||||
rejectReason: null,
|
||||
assets: [],
|
||||
error: null,
|
||||
|
||||
submit: async (projectId, prompt, assetType) => {
|
||||
set({ status: 'submitting', error: null })
|
||||
try {
|
||||
const taskId = await submitGenerate(projectId, prompt, assetType)
|
||||
set({ taskId, status: 'running', progress: 0 })
|
||||
|
||||
// 启动 mock WebSocket
|
||||
cleanupWs = createMockWebSocket(
|
||||
taskId,
|
||||
(msg) => get().handleProgress(msg),
|
||||
(assets) => {
|
||||
set({ status: 'completed', assets, progress: 100 })
|
||||
},
|
||||
(error) => {
|
||||
set({ status: 'failed', error })
|
||||
}
|
||||
)
|
||||
} 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 })
|
||||
}
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
cleanupWs?.()
|
||||
cleanupWs = null
|
||||
set({
|
||||
taskId: null,
|
||||
stage: null,
|
||||
progress: 0,
|
||||
status: 'idle',
|
||||
retryCount: 0,
|
||||
rejectReason: null,
|
||||
assets: [],
|
||||
error: null,
|
||||
})
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,49 @@
|
||||
import { create } from 'zustand'
|
||||
import * as projectApi from '../api/project'
|
||||
|
||||
interface ProjectState {
|
||||
projectId: string
|
||||
name: string
|
||||
style: Record<string, string>
|
||||
loading: boolean
|
||||
loadProject: (projectId: string) => Promise<void>
|
||||
loadStyle: (projectId: string) => Promise<void>
|
||||
updateStyle: (key: string, value: string) => void
|
||||
saveStyle: () => Promise<void>
|
||||
}
|
||||
|
||||
export const useProjectStore = create<ProjectState>((set, get) => ({
|
||||
projectId: '',
|
||||
name: '',
|
||||
style: {},
|
||||
loading: false,
|
||||
|
||||
loadProject: async (projectId) => {
|
||||
set({ loading: true })
|
||||
try {
|
||||
const project = await projectApi.getProject(projectId)
|
||||
set({
|
||||
projectId: project.id,
|
||||
name: project.name,
|
||||
style: project.style.kvPairs,
|
||||
loading: false,
|
||||
})
|
||||
} catch {
|
||||
set({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
loadStyle: async (projectId) => {
|
||||
const kvPairs = await projectApi.getStyle(projectId)
|
||||
set({ style: kvPairs })
|
||||
},
|
||||
|
||||
updateStyle: (key, value) => {
|
||||
set(state => ({ style: { ...state.style, [key]: value } }))
|
||||
},
|
||||
|
||||
saveStyle: async () => {
|
||||
const { projectId, style } = get()
|
||||
await projectApi.saveStyle(projectId, style)
|
||||
},
|
||||
}))
|
||||
@@ -0,0 +1,58 @@
|
||||
import { create } from 'zustand'
|
||||
import type { AssetType } from '../api/types'
|
||||
|
||||
interface TaskParams {
|
||||
resolution: number
|
||||
frames?: { directions: number; framesPerDirection: number }
|
||||
format: 'spritesheet' | 'individual'
|
||||
}
|
||||
|
||||
interface TaskState {
|
||||
prompt: string
|
||||
assetType: AssetType
|
||||
taskStyle: Record<string, string>
|
||||
params: TaskParams
|
||||
setPrompt: (text: string) => void
|
||||
setAssetType: (type: AssetType) => void
|
||||
toggleTaskStyle: (key: string, value: string) => void
|
||||
setParams: (params: Partial<TaskParams>) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_PARAMS: TaskParams = {
|
||||
resolution: 256,
|
||||
format: 'spritesheet',
|
||||
}
|
||||
|
||||
export const useTaskStore = create<TaskState>((set) => ({
|
||||
prompt: '',
|
||||
assetType: 'sprite',
|
||||
taskStyle: {},
|
||||
params: { ...DEFAULT_PARAMS },
|
||||
|
||||
setPrompt: (text) => set({ prompt: text }),
|
||||
|
||||
setAssetType: (type) => set({ assetType: type }),
|
||||
|
||||
toggleTaskStyle: (key, value) =>
|
||||
set(state => {
|
||||
const next = { ...state.taskStyle }
|
||||
if (next[key] === value) {
|
||||
delete next[key]
|
||||
} else {
|
||||
next[key] = value
|
||||
}
|
||||
return { taskStyle: next }
|
||||
}),
|
||||
|
||||
setParams: (params) =>
|
||||
set(state => ({ params: { ...state.params, ...params } })),
|
||||
|
||||
reset: () =>
|
||||
set({
|
||||
prompt: '',
|
||||
assetType: 'sprite',
|
||||
taskStyle: {},
|
||||
params: { ...DEFAULT_PARAMS },
|
||||
}),
|
||||
}))
|
||||
Reference in New Issue
Block a user