2026-07-20 18:25:04 +08:00
|
|
|
|
import { Button, Card, Descriptions, Progress, Space, Tag, Typography } from '@arco-design/web-react'
|
|
|
|
|
|
import { IconArrowLeft, IconCalendar, IconCheckCircle, IconPlus, IconUser } from '@arco-design/web-react/icon'
|
2026-07-20 13:02:26 +08:00
|
|
|
|
import type { ProjectWorkspace, TaskItem } from './project-types'
|
2026-07-20 12:26:21 +08:00
|
|
|
|
|
|
|
|
|
|
const { Title, Text } = Typography
|
|
|
|
|
|
|
2026-07-20 18:25:04 +08:00
|
|
|
|
export function ProjectTasks({
|
|
|
|
|
|
activeWorkspace,
|
|
|
|
|
|
activeTaskID,
|
|
|
|
|
|
onOpenTask,
|
|
|
|
|
|
onCloseTask,
|
|
|
|
|
|
onSelectItem,
|
2026-07-20 21:45:36 +08:00
|
|
|
|
onCreateTask,
|
2026-07-20 18:25:04 +08:00
|
|
|
|
}: {
|
|
|
|
|
|
activeWorkspace: ProjectWorkspace
|
|
|
|
|
|
activeTaskID: string | null
|
|
|
|
|
|
onOpenTask: (taskID: string) => void
|
|
|
|
|
|
onCloseTask: () => void
|
|
|
|
|
|
onSelectItem: (title: string) => void
|
2026-07-20 21:45:36 +08:00
|
|
|
|
onCreateTask: () => void
|
2026-07-20 18:25:04 +08:00
|
|
|
|
}) {
|
2026-07-20 13:02:26 +08:00
|
|
|
|
const { tasks, project } = activeWorkspace
|
2026-07-20 18:25:04 +08:00
|
|
|
|
const activeTask = tasks.find((task) => task.id === activeTaskID)
|
2026-07-20 13:02:26 +08:00
|
|
|
|
const lanes = makeLanes(tasks)
|
2026-07-20 18:25:04 +08:00
|
|
|
|
|
|
|
|
|
|
if (activeTask) {
|
|
|
|
|
|
return <TaskDetail activeWorkspace={activeWorkspace} task={activeTask} onBack={onCloseTask} />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 12:26:21 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="project-channel-page project-tasks-page overview-page">
|
|
|
|
|
|
<div className="overview-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Title heading={4}>工作计划</Title>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<Text type="secondary">{project.name} 的 TODO 计划、负责人和创建时间。</Text>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
</div>
|
2026-07-20 21:45:36 +08:00
|
|
|
|
<Button type="primary" icon={<IconPlus />} onClick={onCreateTask}>新建任务</Button>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="queue-section" bordered>
|
|
|
|
|
|
<div className="section-header">
|
|
|
|
|
|
<Title heading={6}>任务清单</Title>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<Tag color="arcoblue">{tasks.length} 个任务</Tag>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
{tasks.map((task) => (
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<button className="data-row task-row" key={task.id} onClick={() => onOpenTask(task.id)}>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
<span className="row-title"><IconCheckCircle /> {task.title}</span>
|
|
|
|
|
|
<Tag color="arcoblue">{task.tag}</Tag>
|
|
|
|
|
|
<span><IconUser /> {task.owner}</span>
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<span><IconCalendar /> {task.createdAt}</span>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
<Progress percent={Number.parseInt(task.progress, 10)} size="small" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="task-board">
|
|
|
|
|
|
{lanes.map((lane) => (
|
|
|
|
|
|
<Card className="task-lane" bordered key={lane.title}>
|
|
|
|
|
|
<div className="section-header">
|
|
|
|
|
|
<Title heading={6}>{lane.title}</Title>
|
|
|
|
|
|
<Tag color={lane.color}>{lane.items.length}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Space direction="vertical" size={8}>
|
|
|
|
|
|
{lane.items.map((title) => (
|
2026-07-20 18:25:04 +08:00
|
|
|
|
<button className="task-card-button" key={title} onClick={() => openTaskByTitle(tasks, title, onOpenTask, onSelectItem)}>
|
2026-07-20 12:26:21 +08:00
|
|
|
|
<Text>{title}</Text>
|
|
|
|
|
|
<Text type="secondary">优先级 · 本周</Text>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-20 13:02:26 +08:00
|
|
|
|
|
2026-07-20 18:25:04 +08:00
|
|
|
|
function TaskDetail({ activeWorkspace, task, onBack }: { activeWorkspace: ProjectWorkspace; task: TaskItem; onBack: () => void }) {
|
|
|
|
|
|
const percent = Number.parseInt(task.progress, 10)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="project-channel-page project-task-detail-page overview-page">
|
|
|
|
|
|
<div className="overview-head">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Button type="text" icon={<IconArrowLeft />} onClick={onBack}>返回任务列表</Button>
|
|
|
|
|
|
<Title heading={4}>{task.title}</Title>
|
|
|
|
|
|
<Text type="secondary">{activeWorkspace.project.name} 的任务详情</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Tag color={task.completed ? 'green' : 'arcoblue'}>{task.tag}</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Card className="queue-section task-detail-card" bordered>
|
|
|
|
|
|
<Descriptions
|
|
|
|
|
|
colon=":"
|
|
|
|
|
|
column={2}
|
|
|
|
|
|
data={[
|
|
|
|
|
|
{ label: '所属项目', value: activeWorkspace.project.name },
|
|
|
|
|
|
{ label: '负责人', value: task.owner },
|
|
|
|
|
|
{ label: '创建时间', value: task.createdAt },
|
|
|
|
|
|
{ label: '完成状态', value: task.completed ? '已完成' : '未完成' },
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="task-detail-progress">
|
|
|
|
|
|
<Text type="secondary">任务进度</Text>
|
|
|
|
|
|
<Progress percent={Number.isNaN(percent) ? 0 : percent} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="task-detail-summary">
|
|
|
|
|
|
<Title heading={6}>任务说明</Title>
|
|
|
|
|
|
<Text>{task.summary || '暂无任务说明'}</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openTaskByTitle(tasks: TaskItem[], title: string, onOpenTask: (taskID: string) => void, onSelectItem: (title: string) => void) {
|
|
|
|
|
|
const task = tasks.find((item) => item.title === title)
|
|
|
|
|
|
if (task) {
|
|
|
|
|
|
onOpenTask(task.id)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
onSelectItem(title)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-20 13:02:26 +08:00
|
|
|
|
function makeLanes(tasks: TaskItem[]) {
|
|
|
|
|
|
const done = tasks.filter((task) => task.completed)
|
|
|
|
|
|
const active = tasks.filter((task) => !task.completed)
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ title: '待开始', color: 'gray', items: active.slice(0, 1).map((task) => task.title) },
|
|
|
|
|
|
{ title: '进行中', color: 'arcoblue', items: active.map((task) => task.title) },
|
|
|
|
|
|
{ title: '已完成', color: 'green', items: done.length ? done.map((task) => task.title) : ['暂无已完成任务'] },
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|