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:
@@ -1,132 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useTaskStore } from '../stores/task'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { useGenerationStore } from '../stores/generation'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { extractTags } from '../api/prompt'
|
||||
import { mergeStyles } from '../utils/style'
|
||||
import type { GenerateRequest } from '../api/types'
|
||||
import GenerateForm from '../components/GenerateForm'
|
||||
import ProgressBar from '../components/ProgressBar'
|
||||
|
||||
export default function GeneratePage() {
|
||||
const { projectId = 'proj-default' } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const addToast = useToastStore(s => s.addToast)
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const { style: projectStyle, loadProject } = useProjectStore()
|
||||
const {
|
||||
status,
|
||||
stage,
|
||||
progress,
|
||||
taskId,
|
||||
statusText,
|
||||
retryCount,
|
||||
rejectReason,
|
||||
submit,
|
||||
reset: resetGeneration,
|
||||
} = useGenerationStore()
|
||||
|
||||
// 加载工程风格
|
||||
useEffect(() => {
|
||||
loadProject(projectId)
|
||||
}, [projectId, loadProject])
|
||||
|
||||
// 组件卸载时重置生成状态
|
||||
useEffect(() => {
|
||||
return () => resetGeneration()
|
||||
}, [resetGeneration])
|
||||
|
||||
// 失败时显示 toast
|
||||
useEffect(() => {
|
||||
if (status === 'failed') {
|
||||
const errText = useGenerationStore.getState().error || '未知错误'
|
||||
addToast({ type: 'error', message: `素材生成失败:${errText}` })
|
||||
}
|
||||
}, [status, addToast])
|
||||
|
||||
const handleSubmit = async (finalPrompt: string) => {
|
||||
const { taskStyle, params, enableAI, optimizedPrompt } = taskStore
|
||||
const mergedStyle = mergeStyles(projectStyle, taskStyle)
|
||||
const tags = extractTags(mergedStyle)
|
||||
|
||||
const req: GenerateRequest = {
|
||||
projectId,
|
||||
prompt: enableAI && optimizedPrompt ? optimizedPrompt : finalPrompt,
|
||||
assetType: taskStore.assetType,
|
||||
tags,
|
||||
projectStyle,
|
||||
taskStyle,
|
||||
resolution: params.resolution,
|
||||
directions: params.frames?.directions,
|
||||
framesPerDir: params.frames?.framesPerDirection,
|
||||
format: params.format,
|
||||
}
|
||||
|
||||
await submit(req)
|
||||
}
|
||||
|
||||
const handleViewResult = () => {
|
||||
if (taskId) navigate(`/projects/${projectId}/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
resetGeneration()
|
||||
taskStore.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>新建生成</h1>
|
||||
|
||||
{status === 'idle' || status === 'submitting' ? (
|
||||
<GenerateForm onSubmit={handleSubmit} submitting={status === 'submitting'} />
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
|
||||
<ProgressBar
|
||||
stage={stage}
|
||||
progress={progress}
|
||||
status={status}
|
||||
retryCount={retryCount}
|
||||
rejectReason={rejectReason}
|
||||
/>
|
||||
|
||||
{status === 'running' && (
|
||||
<p style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>
|
||||
{statusText || '管线执行中,请稍候...'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status === 'completed' && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p style={{ color: 'var(--success)', marginBottom: 16 }}>
|
||||
生成完成
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
|
||||
<button className="btn-primary" onClick={handleViewResult}>
|
||||
查看结果
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={handleReset}>
|
||||
继续生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'failed' && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p style={{ color: 'var(--error)', marginBottom: 16 }}>
|
||||
{useGenerationStore.getState().error || '生成失败'}
|
||||
</p>
|
||||
<button className="btn-primary" onClick={handleReset}>
|
||||
重新开始
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useProjectListStore } from '../stores/projectList'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import ProjectCard from '../components/ProjectCard'
|
||||
import CreateProjectModal from '../components/CreateProjectModal'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
import Skeleton from '../components/Skeleton'
|
||||
|
||||
export default function HomePage() {
|
||||
const { projects, loading, loadProjects, createProject, deleteProject } =
|
||||
useProjectListStore()
|
||||
const addToast = useToastStore(s => s.addToast)
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadProjects()
|
||||
}, [loadProjects])
|
||||
|
||||
const handleCreate = async (name: string, style: Record<string, string>) => {
|
||||
await createProject(name, style)
|
||||
addToast({ type: 'success', message: '工程创建成功' })
|
||||
}
|
||||
|
||||
const handleDelete = async (projectId: string) => {
|
||||
if (!confirm('确定删除该工程?所有任务和素材将被永久删除。')) return
|
||||
await deleteProject(projectId)
|
||||
addToast({ type: 'success', message: '工程已删除' })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 32 }}>
|
||||
<Skeleton variant="card" />
|
||||
<div style={{ height: 16 }} />
|
||||
<Skeleton variant="card" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 32, paddingBottom: 32 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<h1 style={{ fontSize: 22 }}>我的工程</h1>
|
||||
<button className="btn-primary" onClick={() => setModalOpen(true)}>
|
||||
新建工程
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<EmptyState
|
||||
title="还没有工程"
|
||||
description="创建你的第一个工程,开始生成游戏素材"
|
||||
action={{ label: '新建工程', onClick: () => setModalOpen(true) }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
{projects.map(project => (
|
||||
<ProjectCard
|
||||
key={project.id}
|
||||
project={project}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateProjectModal
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onSubmit={handleCreate}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { useTaskStore } from '../stores/task'
|
||||
import { useGenerationStore } from '../stores/generation'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { extractTags } from '../api/prompt'
|
||||
import { mergeStyles } from '../utils/style'
|
||||
import type { GenerateRequest } from '../api/types'
|
||||
import ProjectConfigPanel from '../components/ProjectConfigPanel'
|
||||
import GenerateForm from '../components/GenerateForm'
|
||||
import ProgressBar from '../components/ProgressBar'
|
||||
import TaskListPanel from '../components/TaskListPanel'
|
||||
|
||||
export default function ProjectDetailPage() {
|
||||
const { projectId = '' } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const addToast = useToastStore(s => s.addToast)
|
||||
|
||||
const { style: projectStyle } = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const {
|
||||
status,
|
||||
stage,
|
||||
progress,
|
||||
taskId,
|
||||
statusText,
|
||||
retryCount,
|
||||
rejectReason,
|
||||
submit,
|
||||
reset: resetGeneration,
|
||||
} = useGenerationStore()
|
||||
|
||||
// 重置生成状态(切换工程时)
|
||||
useEffect(() => {
|
||||
resetGeneration()
|
||||
taskStore.reset()
|
||||
}, [projectId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 失败时 toast
|
||||
useEffect(() => {
|
||||
if (status === 'failed') {
|
||||
const errText = useGenerationStore.getState().error || '未知错误'
|
||||
addToast({ type: 'error', message: `素材生成失败:${errText}` })
|
||||
}
|
||||
}, [status, addToast])
|
||||
|
||||
const handleSubmit = async (finalPrompt: string) => {
|
||||
const { taskStyle, params, enableAI, optimizedPrompt } = taskStore
|
||||
const mergedStyle = mergeStyles(projectStyle, taskStyle)
|
||||
const tags = extractTags(mergedStyle)
|
||||
|
||||
const req: GenerateRequest = {
|
||||
projectId,
|
||||
prompt: enableAI && optimizedPrompt ? optimizedPrompt : finalPrompt,
|
||||
assetType: taskStore.assetType,
|
||||
tags,
|
||||
projectStyle,
|
||||
taskStyle,
|
||||
resolution: params.resolution,
|
||||
directions: params.frames?.directions,
|
||||
framesPerDir: params.frames?.framesPerDirection,
|
||||
format: params.format,
|
||||
}
|
||||
|
||||
await submit(req)
|
||||
}
|
||||
|
||||
const handleViewResult = () => {
|
||||
if (taskId) navigate(`/projects/${projectId}/tasks/${taskId}`)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
resetGeneration()
|
||||
taskStore.reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '260px 1fr 320px',
|
||||
height: 'calc(100vh - 56px)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* 左栏:工程配置 */}
|
||||
<aside
|
||||
style={{
|
||||
borderRight: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<ProjectConfigPanel />
|
||||
</aside>
|
||||
|
||||
{/* 中间:生成表单 + 进度 */}
|
||||
<main
|
||||
className="page-enter"
|
||||
style={{ overflow: 'auto', padding: '24px 32px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 32, maxWidth: 720 }}>
|
||||
{/* 上半:生成表单 */}
|
||||
<section>
|
||||
<h2 style={{ fontSize: 18, marginBottom: 20 }}>新建生成</h2>
|
||||
<GenerateForm onSubmit={handleSubmit} submitting={status === 'submitting'} />
|
||||
</section>
|
||||
|
||||
{/* 下半:当前任务进度 */}
|
||||
{status !== 'idle' && (
|
||||
<section className="card">
|
||||
<h2 style={{ fontSize: 16, marginBottom: 16 }}>生成进度</h2>
|
||||
<ProgressBar
|
||||
stage={stage}
|
||||
progress={progress}
|
||||
status={status}
|
||||
retryCount={retryCount}
|
||||
rejectReason={rejectReason}
|
||||
/>
|
||||
|
||||
{status === 'running' && (
|
||||
<p style={{ textAlign: 'center', color: 'var(--text-secondary)', marginTop: 12 }}>
|
||||
{statusText || '管线执行中,请稍候...'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{status === 'completed' && (
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<p style={{ color: 'var(--success)', marginBottom: 12 }}>
|
||||
生成完成
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, justifyContent: 'center' }}>
|
||||
<button className="btn-primary" onClick={handleViewResult}>
|
||||
查看结果
|
||||
</button>
|
||||
<button className="btn-secondary" onClick={handleReset}>
|
||||
继续生成
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'failed' && (
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<p style={{ color: 'var(--error)', marginBottom: 12 }}>
|
||||
{useGenerationStore.getState().error || '生成失败'}
|
||||
</p>
|
||||
<button className="btn-primary" onClick={handleReset}>
|
||||
重新开始
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 右栏:任务列表 */}
|
||||
<aside
|
||||
style={{
|
||||
borderLeft: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<TaskListPanel />
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
import { getTasks } from '../api/project'
|
||||
import type { Task } from '../api/types'
|
||||
import StyleSelector from '../components/StyleSelector'
|
||||
import Skeleton from '../components/Skeleton'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
|
||||
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '等待中', color: 'var(--text-muted)' },
|
||||
running: { label: '运行中', color: 'var(--warning)' },
|
||||
completed: { label: '已完成', color: 'var(--success)' },
|
||||
failed: { label: '失败', color: 'var(--error)' },
|
||||
}
|
||||
|
||||
export default function ProjectPage() {
|
||||
const { projectId = 'proj-default' } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
name,
|
||||
style,
|
||||
loading,
|
||||
loadProject,
|
||||
loadStyle,
|
||||
updateStyle,
|
||||
saveStyle,
|
||||
} = useProjectStore()
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadProject(projectId)
|
||||
loadStyle(projectId)
|
||||
getTasks(projectId).then(setTasks)
|
||||
}, [projectId, loadProject, loadStyle])
|
||||
|
||||
const addToast = useToastStore(s => s.addToast)
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
await saveStyle()
|
||||
addToast({ type: 'success', message: '风格已保存' })
|
||||
} catch {
|
||||
addToast({ type: 'error', message: '保存失败,请重试' })
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>{loading ? <Skeleton variant="text" lines={1} /> : name}</h1>
|
||||
|
||||
{/* 工程风格 */}
|
||||
<section className="card" style={{ marginBottom: 24 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: 18 }}>工程风格</h2>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
style={{ padding: '8px 20px', fontSize: 13 }}
|
||||
>
|
||||
{saving ? '保存中...' : '保存风格'}
|
||||
</button>
|
||||
</div>
|
||||
<StyleSelector value={style} onChange={updateStyle} />
|
||||
</section>
|
||||
|
||||
{/* 任务列表 */}
|
||||
<section className="card">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: 18 }}>历史任务</h2>
|
||||
<Link
|
||||
to={`/projects/${projectId}/generate`}
|
||||
className="btn-primary"
|
||||
style={{
|
||||
padding: '8px 20px',
|
||||
fontSize: 13,
|
||||
borderRadius: 'var(--radius)',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
新建生成
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无任务"
|
||||
description="点击「新建生成」开始创建你的第一个游戏素材"
|
||||
action={{ label: '新建生成', onClick: () => navigate(`/projects/${projectId}/generate`) }}
|
||||
/>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<th style={thStyle}>提示词</th>
|
||||
<th style={thStyle}>类型</th>
|
||||
<th style={thStyle}>状态</th>
|
||||
<th style={thStyle}>创建时间</th>
|
||||
<th style={thStyle}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasks.map(task => {
|
||||
const statusInfo = STATUS_LABELS[task.status] ?? STATUS_LABELS.pending
|
||||
return (
|
||||
<tr key={task.id} className="table-row-hover" style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={tdStyle}>{task.prompt}</td>
|
||||
<td style={tdStyle}>{task.assetType}</td>
|
||||
<td style={{ ...tdStyle, color: statusInfo.color }}>{statusInfo.label}</td>
|
||||
<td style={tdStyle}>
|
||||
{new Date(task.createdAt).toLocaleString('zh-CN')}
|
||||
</td>
|
||||
<td style={tdStyle}>
|
||||
{task.status === 'completed' && (
|
||||
<Link
|
||||
to={`/projects/${projectId}/tasks/${task.id}`}
|
||||
style={{ fontSize: 13 }}
|
||||
>
|
||||
查看结果
|
||||
</Link>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const thStyle: React.CSSProperties = {
|
||||
textAlign: 'left',
|
||||
padding: '10px 12px',
|
||||
fontSize: 13,
|
||||
color: 'var(--text-secondary)',
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
const tdStyle: React.CSSProperties = {
|
||||
padding: '10px 12px',
|
||||
fontSize: 13,
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import AssetPreview from '../components/AssetPreview'
|
||||
import Skeleton from '../components/Skeleton'
|
||||
|
||||
export default function ResultPage() {
|
||||
const { projectId = 'proj-default', taskId } = useParams()
|
||||
const { projectId = '', taskId } = useParams()
|
||||
const [task, setTask] = useState<Task | null>(null)
|
||||
const [assets, setAssets] = useState<Asset[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -33,12 +33,10 @@ export default function ResultPage() {
|
||||
setPolling(false)
|
||||
setLoading(false)
|
||||
} else if (!pollRef.current) {
|
||||
// 开始轮询
|
||||
setPolling(true)
|
||||
pollRef.current = setInterval(fetchTask, 2000)
|
||||
}
|
||||
} catch {
|
||||
// 出错也停止加载态
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -53,10 +51,10 @@ export default function ResultPage() {
|
||||
if (loading || polling) {
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 40 }}>
|
||||
<div className="card" style={{ marginBottom: 24 }}>
|
||||
<div className="card">
|
||||
<Skeleton variant="text" lines={1} />
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Skeleton variant="text" lines={4} />
|
||||
<Skeleton variant="card" />
|
||||
</div>
|
||||
{task && (
|
||||
<p style={{ textAlign: 'center', color: 'var(--text-secondary)', marginTop: 16 }}>
|
||||
@@ -66,9 +64,6 @@ export default function ResultPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<Skeleton variant="card" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -86,58 +81,28 @@ export default function ResultPage() {
|
||||
|
||||
return (
|
||||
<div className="container page-enter" style={{ paddingTop: 24, paddingBottom: 40 }}>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 32 }}>生成结果</h1>
|
||||
|
||||
<section className="card" style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 0 }}>任务信息</h2>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
{assets.length > 0 && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => downloadAssets(assets)}
|
||||
style={{ padding: '8px 20px', fontSize: 13 }}
|
||||
>
|
||||
下载全部素材
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/projects/${projectId}/generate`}
|
||||
className="btn-secondary"
|
||||
style={{ padding: '8px 20px', fontSize: 13, borderRadius: 'var(--radius)', display: 'inline-block' }}
|
||||
>
|
||||
继续生成
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Link
|
||||
to={`/projects/${projectId}`}
|
||||
style={{ fontSize: 13, color: 'var(--text-secondary)' }}
|
||||
>
|
||||
← 返回工程
|
||||
</Link>
|
||||
<h1 style={{ fontSize: 22 }}>生成结果</h1>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '8px 16px', fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>提示词</span>
|
||||
<span>{task.prompt}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>素材类型</span>
|
||||
<span>{task.assetType}</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>状态</span>
|
||||
<span style={{ color: task.status === 'completed' ? 'var(--success)' : 'var(--error)' }}>
|
||||
{task.status === 'completed' ? '已完成' : '失败'}
|
||||
</span>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>创建时间</span>
|
||||
<span>{new Date(task.createdAt).toLocaleString('zh-CN')}</span>
|
||||
{task.retryCount != null && task.retryCount > 0 && (
|
||||
<>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>重试次数</span>
|
||||
<span>{task.retryCount}</span>
|
||||
</>
|
||||
)}
|
||||
{task.error && (
|
||||
<>
|
||||
<span style={{ color: 'var(--text-secondary)' }}>错误</span>
|
||||
<span style={{ color: 'var(--error)' }}>{task.error}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
{assets.length > 0 && (
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={() => downloadAssets(assets)}
|
||||
style={{ padding: '8px 20px', fontSize: 13 }}
|
||||
>
|
||||
下载全部素材
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<section className="card" style={{ marginBottom: 24 }}>
|
||||
<h2 style={{ fontSize: 16, marginBottom: 16 }}>素材预览</h2>
|
||||
<section className="card">
|
||||
<AssetPreview assets={assets} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user