Files
gen2d/frontend/src/components/TaskListPanel.tsx
T
wonder e0a70106c1 feat: 添加管线阶段可视化展示并改进任务日志
- 添加 PipelineStages 组件,用圆角矩形和箭头展示管线阶段
- 三色方案:已完成为浅绿、进行中为浅橙、未完成为灰色
- 任务列表中显示进度百分比和管线阶段
- 增强后端日志输出,添加任务启动、进度更新和管线完成日志
- 用于诊断任务卡在 running 状态的问题
2026-05-25 18:34:46 +08:00

125 lines
4.2 KiB
TypeScript

import { useEffect, useState, useCallback } from 'react'
import { useParams } from 'react-router-dom'
import { getTasks } from '../api/project'
import type { Task } from '../api/types'
import StatusBadge from './StatusBadge'
import PipelineStages from './PipelineStages'
import Skeleton from './Skeleton'
const ASSET_TYPE_LABELS: Record<string, string> = {
sprite: '精灵',
background: '背景',
ui: 'UI',
animation: '动画',
}
function formatRelativeTime(dateStr: string): string {
const now = Date.now()
const then = new Date(dateStr).getTime()
const diff = now - then
const minutes = Math.floor(diff / 60000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}小时前`
const days = Math.floor(hours / 24)
return `${days}天前`
}
export default function TaskListPanel() {
const { projectId = '' } = useParams()
const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(true)
const fetchTasks = useCallback(async () => {
if (!projectId) return
try {
const data = await getTasks(projectId)
setTasks(data)
} finally {
setLoading(false)
}
}, [projectId])
useEffect(() => {
fetchTasks()
const timer = setInterval(fetchTasks, 15000)
return () => clearInterval(timer)
}, [fetchTasks])
if (loading) {
return (
<div style={{ padding: 16 }}>
<Skeleton variant="text" lines={4} />
</div>
)
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<h3 style={{ fontSize: 14, fontWeight: 600, padding: '16px 16px 12px', borderBottom: '1px solid var(--border)' }}>
任务列表 ({tasks.length})
</h3>
<div style={{ flex: 1, overflow: 'auto' }}>
{tasks.length === 0 ? (
<p style={{ padding: 16, fontSize: 13, color: 'var(--text-muted)', textAlign: 'center' }}>
暂无任务
</p>
) : (
tasks.map(task => (
<a
key={task.id}
href={task.status === 'completed' ? `/projects/${projectId}/tasks/${task.id}` : undefined}
style={{
display: 'block',
padding: '12px 16px',
borderBottom: '1px solid var(--border)',
textDecoration: 'none',
color: 'inherit',
cursor: task.status === 'completed' ? 'pointer' : 'default',
transition: 'background 0.15s',
}}
onMouseEnter={e => {
if (task.status === 'completed')
(e.currentTarget as HTMLElement).style.background = 'var(--accent-dim)'
}}
onMouseLeave={e => {
(e.currentTarget as HTMLElement).style.background = ''
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
{ASSET_TYPE_LABELS[task.assetType] || task.assetType}
</span>
<StatusBadge status={task.status} />
</div>
<p
style={{
fontSize: 13,
lineHeight: 1.4,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{task.prompt}
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 4 }}>
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
{formatRelativeTime(task.createdAt)}
</span>
{task.progress !== undefined && task.status === 'running' && (
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
{task.progress}%
</span>
)}
</div>
<PipelineStages currentStage={task.stage ?? null} status={task.status} />
</a>
))
)}
</div>
</div>
)
}