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:
2026-05-24 14:18:39 +08:00
parent 302b9ba88e
commit a2736f722c
33 changed files with 2272 additions and 20 deletions
+124
View File
@@ -0,0 +1,124 @@
import type { AssetType } from '../api/types'
import { useProjectStore } from '../stores/project'
import { useTaskStore } from '../stores/task'
import { mergeStyles } from '../utils/style'
import StyleSelector from './StyleSelector'
import PromptEditor from './PromptEditor'
const ASSET_TYPES: { value: AssetType; label: string }[] = [
{ value: 'sprite', label: '精灵' },
{ value: 'background', label: '背景' },
{ value: 'ui', label: 'UI' },
{ value: 'animation', label: '动画' },
]
interface GenerateFormProps {
onSubmit: () => void
submitting: boolean
}
export default function GenerateForm({ onSubmit, submitting }: GenerateFormProps) {
const { style: projectStyle } = useProjectStore()
const {
prompt,
assetType,
taskStyle,
params,
setPrompt,
setAssetType,
toggleTaskStyle,
setParams,
} = useTaskStore()
const mergedStyle = mergeStyles(projectStyle, taskStyle)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{/* 素材类型 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
素材类型
</h3>
<div style={{ display: 'flex', gap: 8 }}>
{ASSET_TYPES.map(t => (
<button
key={t.value}
type="button"
className={assetType === t.value ? 'btn-primary' : 'btn-secondary'}
onClick={() => setAssetType(t.value)}
style={{ padding: '8px 20px' }}
>
{t.label}
</button>
))}
</div>
</div>
{/* 任务风格覆盖 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
风格覆盖(可选)
</h3>
<StyleSelector value={taskStyle} onChange={toggleTaskStyle} compact />
</div>
{/* 提示词 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
提示词
</h3>
<PromptEditor
prompt={prompt}
onPromptChange={setPrompt}
style={mergedStyle}
params={{ resolution: params.resolution, format: params.format }}
/>
</div>
{/* 参数 */}
<div>
<h3 style={{ marginBottom: 12, fontSize: 14, color: 'var(--text-secondary)' }}>
参数
</h3>
<div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>分辨率</span>
<input
type="number"
value={params.resolution}
onChange={e => setParams({ resolution: Number(e.target.value) })}
min={32}
max={1024}
step={32}
style={{ width: 100 }}
/>
<span style={{ fontSize: 13, color: 'var(--text-muted)' }}>px</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 13, color: 'var(--text-secondary)' }}>格式</span>
<select
value={params.format}
onChange={e =>
setParams({ format: e.target.value as 'spritesheet' | 'individual' })
}
>
<option value="spritesheet">Spritesheet</option>
<option value="individual">单帧</option>
</select>
</label>
</div>
</div>
{/* 提交 */}
<button
type="button"
className="btn-primary"
onClick={onSubmit}
disabled={submitting || !prompt.trim()}
style={{ alignSelf: 'flex-start', padding: '12px 32px', fontSize: 16 }}
>
{submitting ? '提交中...' : '开始生成'}
</button>
</div>
)
}