|
1 | | -import { IconCheck, IconLoading, IconClockCircle } from "@arco-design/web-react/icon"; |
| 1 | +import { useState } from "react"; |
| 2 | +import { IconCheck, IconLoading, IconDown } from "@arco-design/web-react/icon"; |
2 | 3 | import type { Task } from "@App/app/service/agent/tools/task_tools"; |
3 | 4 |
|
4 | 5 | function TaskStatusIcon({ status }: { status: Task["status"] }) { |
5 | 6 | switch (status) { |
6 | 7 | case "completed": |
7 | 8 | return ( |
8 | | - <span className="tw-w-4 tw-h-4 tw-rounded-full tw-bg-[rgb(var(--green-1))] tw-flex tw-items-center tw-justify-center tw-shrink-0"> |
9 | | - <IconCheck style={{ fontSize: 10, color: "rgb(var(--green-6))" }} /> |
| 9 | + <span className="tw-w-[18px] tw-h-[18px] tw-rounded-full tw-bg-[rgb(var(--green-6))] tw-flex tw-items-center tw-justify-center tw-shrink-0"> |
| 10 | + <IconCheck style={{ fontSize: 10, color: "#fff" }} /> |
10 | 11 | </span> |
11 | 12 | ); |
12 | 13 | case "in_progress": |
13 | 14 | return ( |
14 | | - <span className="tw-w-4 tw-h-4 tw-flex tw-items-center tw-justify-center tw-shrink-0"> |
15 | | - <IconLoading style={{ fontSize: 12, color: "rgb(var(--arcoblue-6))" }} /> |
| 15 | + <span |
| 16 | + className="tw-w-[18px] tw-h-[18px] tw-rounded-full tw-border-2 tw-border-solid tw-flex tw-items-center tw-justify-center tw-shrink-0" |
| 17 | + style={{ borderColor: "rgb(var(--arcoblue-6))" }} |
| 18 | + > |
| 19 | + <IconLoading style={{ fontSize: 10, color: "rgb(var(--arcoblue-6))" }} /> |
16 | 20 | </span> |
17 | 21 | ); |
18 | 22 | default: |
19 | 23 | return ( |
20 | | - <span className="tw-w-4 tw-h-4 tw-rounded-full tw-border tw-border-solid tw-border-[var(--color-border-2)] tw-bg-transparent tw-shrink-0" /> |
| 24 | + <span className="tw-w-[18px] tw-h-[18px] tw-rounded-full tw-border-2 tw-border-solid tw-border-[var(--color-border-2)] tw-bg-transparent tw-shrink-0" /> |
21 | 25 | ); |
22 | 26 | } |
23 | 27 | } |
24 | 28 |
|
25 | 29 | export default function TaskListBlock({ tasks }: { tasks: Task[] }) { |
| 30 | + const [collapsed, setCollapsed] = useState(false); |
| 31 | + |
26 | 32 | if (tasks.length === 0) return null; |
27 | 33 |
|
28 | 34 | const completed = tasks.filter((t) => t.status === "completed").length; |
| 35 | + const inProgress = tasks.filter((t) => t.status === "in_progress").length; |
29 | 36 | const total = tasks.length; |
| 37 | + const progress = total > 0 ? (completed / total) * 100 : 0; |
| 38 | + const allDone = completed === total; |
30 | 39 |
|
31 | 40 | return ( |
32 | | - <div className="tw-my-3 tw-rounded-lg tw-border tw-border-solid tw-border-[var(--color-border-1)] tw-bg-[var(--color-fill-1)] tw-overflow-hidden"> |
33 | | - {/* 标题栏 */} |
34 | | - <div className="tw-flex tw-items-center tw-justify-between tw-px-3 tw-py-2 tw-border-b tw-border-solid tw-border-[var(--color-border-1)]"> |
35 | | - <div className="tw-flex tw-items-center tw-gap-1.5"> |
36 | | - <IconClockCircle style={{ fontSize: 12 }} className="tw-text-[var(--color-text-3)]" /> |
37 | | - <span className="tw-text-xs tw-font-medium tw-text-[var(--color-text-2)]">Tasks</span> |
| 41 | + <div className="tw-my-3 tw-rounded-xl tw-border tw-border-solid tw-border-[var(--color-border-1)] tw-bg-[var(--color-bg-2)] tw-overflow-hidden tw-shadow-sm"> |
| 42 | + {/* 可点击的标题栏 */} |
| 43 | + <div |
| 44 | + className="tw-flex tw-items-center tw-gap-3 tw-px-4 tw-py-3 tw-cursor-pointer tw-select-none hover:tw-bg-[var(--color-fill-2)] tw-transition-colors tw-duration-150" |
| 45 | + onClick={() => setCollapsed((c) => !c)} |
| 46 | + > |
| 47 | + {/* 左侧:环形进度指示 + 文字 */} |
| 48 | + <div className="tw-flex tw-items-center tw-gap-2.5 tw-flex-1 tw-min-w-0"> |
| 49 | + <ProgressRing progress={progress} allDone={allDone} /> |
| 50 | + <div className="tw-flex tw-flex-col tw-min-w-0"> |
| 51 | + <span className="tw-text-sm tw-font-medium tw-text-[var(--color-text-1)] tw-leading-tight"> |
| 52 | + Tasks |
| 53 | + </span> |
| 54 | + <span className="tw-text-xs tw-text-[var(--color-text-3)] tw-leading-tight tw-mt-0.5"> |
| 55 | + {allDone |
| 56 | + ? `${total} 项任务已完成` |
| 57 | + : inProgress > 0 |
| 58 | + ? `正在执行 ${inProgress} 项,${completed}/${total} 已完成` |
| 59 | + : `${completed}/${total} 已完成`} |
| 60 | + </span> |
| 61 | + </div> |
38 | 62 | </div> |
39 | | - <span className="tw-text-xs tw-text-[var(--color-text-3)]"> |
40 | | - {completed}/{total} |
41 | | - </span> |
42 | | - </div> |
43 | 63 |
|
44 | | - {/* 进度条 */} |
45 | | - <div className="tw-h-0.5 tw-bg-[var(--color-fill-3)]"> |
46 | | - <div |
47 | | - className="tw-h-full tw-bg-[rgb(var(--green-6))] tw-transition-all tw-duration-300" |
48 | | - style={{ width: `${total > 0 ? (completed / total) * 100 : 0}%` }} |
| 64 | + {/* 右侧:展开/收起箭头 */} |
| 65 | + <IconDown |
| 66 | + className="tw-text-[var(--color-text-3)] tw-transition-transform tw-duration-200 tw-shrink-0" |
| 67 | + style={{ |
| 68 | + fontSize: 12, |
| 69 | + transform: collapsed ? "rotate(-90deg)" : "rotate(0deg)", |
| 70 | + }} |
49 | 71 | /> |
50 | 72 | </div> |
51 | 73 |
|
52 | | - {/* 任务列表 */} |
53 | | - <div className="tw-px-3 tw-py-1.5"> |
54 | | - {tasks.map((task) => ( |
55 | | - <div key={task.id} className="tw-flex tw-items-start tw-gap-2 tw-py-1.5"> |
56 | | - <div className="tw-mt-0.5"> |
| 74 | + {/* 可收缩的任务列表 */} |
| 75 | + <div |
| 76 | + className="tw-transition-all tw-duration-200 tw-ease-in-out tw-overflow-hidden" |
| 77 | + style={{ |
| 78 | + maxHeight: collapsed ? 0 : `${tasks.length * 40 + 16}px`, |
| 79 | + opacity: collapsed ? 0 : 1, |
| 80 | + }} |
| 81 | + > |
| 82 | + <div className="tw-border-t tw-border-solid tw-border-[var(--color-border-1)]" /> |
| 83 | + <div className="tw-px-4 tw-py-2"> |
| 84 | + {tasks.map((task, index) => ( |
| 85 | + <div |
| 86 | + key={task.id} |
| 87 | + className="tw-flex tw-items-center tw-gap-2.5 tw-py-[7px]" |
| 88 | + style={{ |
| 89 | + // 逐项淡入动画 |
| 90 | + animation: "taskFadeIn 0.15s ease-out both", |
| 91 | + animationDelay: `${index * 30}ms`, |
| 92 | + }} |
| 93 | + > |
57 | 94 | <TaskStatusIcon status={task.status} /> |
| 95 | + <span |
| 96 | + className={`tw-text-[13px] tw-leading-normal tw-transition-colors tw-duration-200 ${ |
| 97 | + task.status === "completed" |
| 98 | + ? "tw-text-[var(--color-text-4)] tw-line-through" |
| 99 | + : task.status === "in_progress" |
| 100 | + ? "tw-text-[var(--color-text-1)] tw-font-medium" |
| 101 | + : "tw-text-[var(--color-text-2)]" |
| 102 | + }`} |
| 103 | + > |
| 104 | + {task.subject} |
| 105 | + </span> |
58 | 106 | </div> |
59 | | - <span |
60 | | - className={`tw-text-xs tw-leading-relaxed ${ |
61 | | - task.status === "completed" |
62 | | - ? "tw-text-[var(--color-text-4)] tw-line-through" |
63 | | - : "tw-text-[var(--color-text-1)]" |
64 | | - }`} |
65 | | - > |
66 | | - {task.subject} |
67 | | - </span> |
68 | | - </div> |
69 | | - ))} |
| 107 | + ))} |
| 108 | + </div> |
| 109 | + </div> |
| 110 | + |
| 111 | + {/* 底部进度条 */} |
| 112 | + <div className="tw-h-[3px] tw-bg-[var(--color-fill-2)]"> |
| 113 | + <div |
| 114 | + className="tw-h-full tw-transition-all tw-duration-500 tw-ease-out tw-rounded-r-full" |
| 115 | + style={{ |
| 116 | + width: `${progress}%`, |
| 117 | + background: allDone |
| 118 | + ? "rgb(var(--green-6))" |
| 119 | + : "linear-gradient(90deg, rgb(var(--arcoblue-5)), rgb(var(--arcoblue-6)))", |
| 120 | + }} |
| 121 | + /> |
70 | 122 | </div> |
71 | 123 | </div> |
72 | 124 | ); |
73 | 125 | } |
| 126 | + |
| 127 | +/** 环形进度指示器 */ |
| 128 | +function ProgressRing({ progress, allDone }: { progress: number; allDone: boolean }) { |
| 129 | + const size = 28; |
| 130 | + const strokeWidth = 2.5; |
| 131 | + const radius = (size - strokeWidth) / 2; |
| 132 | + const circumference = 2 * Math.PI * radius; |
| 133 | + const offset = circumference - (progress / 100) * circumference; |
| 134 | + |
| 135 | + return ( |
| 136 | + <svg |
| 137 | + width={size} |
| 138 | + height={size} |
| 139 | + className="tw-shrink-0" |
| 140 | + style={{ transform: "rotate(-90deg)" }} |
| 141 | + > |
| 142 | + {/* 背景圆环 */} |
| 143 | + <circle |
| 144 | + cx={size / 2} |
| 145 | + cy={size / 2} |
| 146 | + r={radius} |
| 147 | + fill="none" |
| 148 | + stroke="var(--color-fill-3)" |
| 149 | + strokeWidth={strokeWidth} |
| 150 | + /> |
| 151 | + {/* 进度圆环 */} |
| 152 | + <circle |
| 153 | + cx={size / 2} |
| 154 | + cy={size / 2} |
| 155 | + r={radius} |
| 156 | + fill="none" |
| 157 | + stroke={allDone ? "rgb(var(--green-6))" : "rgb(var(--arcoblue-6))"} |
| 158 | + strokeWidth={strokeWidth} |
| 159 | + strokeLinecap="round" |
| 160 | + strokeDasharray={circumference} |
| 161 | + strokeDashoffset={offset} |
| 162 | + className="tw-transition-all tw-duration-500 tw-ease-out" |
| 163 | + /> |
| 164 | + </svg> |
| 165 | + ); |
| 166 | +} |
0 commit comments