project-detail-view.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /**
  2. * ProjectDetailView Component
  3. *
  4. * Displays project details and associated tasks.
  5. * Requirements: 1.5, 1.6
  6. */
  7. import React, { useEffect, useState } from 'react';
  8. import { useParams, useNavigate } from 'react-router-dom';
  9. import { useAtom } from 'jotai';
  10. import {
  11. Button,
  12. IconArrowLeft,
  13. IconEdit,
  14. IconPlus,
  15. DataTable,
  16. type ExtendedDataTableColumnDef,
  17. Badge,
  18. Dialog,
  19. DialogContent,
  20. DialogHeader,
  21. DialogTitle,
  22. } from '@humansignal/ui';
  23. import { getProject, getProjectTasks, createTask } from '../services/api';
  24. import { currentProjectAtom, projectLoadingAtom, projectErrorAtom } from '../atoms/project-atoms';
  25. import { tasksAtom, type Task } from '../atoms/task-atoms';
  26. import { TaskForm, type TaskFormData } from '../components';
  27. export const ProjectDetailView: React.FC = () => {
  28. const { id } = useParams<{ id: string }>();
  29. const navigate = useNavigate();
  30. const [currentProject, setCurrentProject] = useAtom(currentProjectAtom);
  31. const [loading, setLoading] = useAtom(projectLoadingAtom);
  32. const [error, setError] = useAtom(projectErrorAtom);
  33. const [tasks, setTasks] = useAtom(tasksAtom);
  34. const [tasksLoading, setTasksLoading] = useState(false);
  35. const [isCreateTaskDialogOpen, setIsCreateTaskDialogOpen] = useState(false);
  36. const [isCreatingTask, setIsCreatingTask] = useState(false);
  37. // Load project details and tasks
  38. useEffect(() => {
  39. if (!id) return;
  40. const loadProjectData = async () => {
  41. try {
  42. setLoading(true);
  43. setError(null);
  44. // Load project details
  45. const projectData = await getProject(id);
  46. setCurrentProject(projectData);
  47. // Load project tasks
  48. setTasksLoading(true);
  49. const tasksData = await getProjectTasks(id);
  50. setTasks(tasksData);
  51. } catch (err: any) {
  52. setError(err.message || '加载项目详情失败');
  53. } finally {
  54. setLoading(false);
  55. setTasksLoading(false);
  56. }
  57. };
  58. loadProjectData();
  59. }, [id]);
  60. const handleEditProject = () => {
  61. navigate(`/projects/${id}/edit`);
  62. };
  63. const handleCreateTask = () => {
  64. setIsCreateTaskDialogOpen(true);
  65. };
  66. const handleTaskFormSubmit = async (data: TaskFormData) => {
  67. try {
  68. setIsCreatingTask(true);
  69. // Create task via API
  70. const newTask = await createTask({
  71. project_id: data.project_id,
  72. name: data.name,
  73. data: data.data,
  74. assigned_to: data.assigned_to || null,
  75. });
  76. // Add new task to tasks list
  77. setTasks((prevTasks) => [...prevTasks, newTask]);
  78. // Update project task count
  79. if (currentProject) {
  80. setCurrentProject({
  81. ...currentProject,
  82. task_count: currentProject.task_count + 1,
  83. });
  84. }
  85. // Close dialog
  86. setIsCreateTaskDialogOpen(false);
  87. } catch (err: any) {
  88. throw new Error(err.message || '创建任务失败');
  89. } finally {
  90. setIsCreatingTask(false);
  91. }
  92. };
  93. const handleTaskFormCancel = () => {
  94. setIsCreateTaskDialogOpen(false);
  95. };
  96. // Define task table columns
  97. const taskColumns: ExtendedDataTableColumnDef<Task>[] = [
  98. {
  99. accessorKey: 'name',
  100. header: '任务名称',
  101. enableSorting: true,
  102. cell: ({ row }) => (
  103. <span className="text-body-medium text-primary-foreground font-semibold">
  104. {row.original.name}
  105. </span>
  106. ),
  107. size: 300,
  108. minSize: 200,
  109. },
  110. {
  111. accessorKey: 'status',
  112. header: '状态',
  113. enableSorting: true,
  114. cell: ({ row }) => {
  115. const statusMap = {
  116. pending: { label: '待处理', variant: 'secondary' as const },
  117. in_progress: { label: '进行中', variant: 'info' as const },
  118. completed: { label: '已完成', variant: 'success' as const },
  119. };
  120. const status = statusMap[row.original.status];
  121. return <Badge variant={status.variant}>{status.label}</Badge>;
  122. },
  123. size: 120,
  124. minSize: 100,
  125. maxSize: 150,
  126. },
  127. {
  128. accessorKey: 'progress',
  129. header: '进度',
  130. enableSorting: true,
  131. cell: ({ row }) => (
  132. <span className="text-body-medium text-primary-foreground">
  133. {row.original.progress}%
  134. </span>
  135. ),
  136. size: 100,
  137. minSize: 80,
  138. maxSize: 120,
  139. },
  140. {
  141. accessorKey: 'assigned_to',
  142. header: '分配给',
  143. enableSorting: true,
  144. cell: ({ row }) => (
  145. <span className="text-body-medium text-secondary-foreground">
  146. {row.original.assigned_to || '未分配'}
  147. </span>
  148. ),
  149. size: 150,
  150. minSize: 120,
  151. maxSize: 180,
  152. },
  153. {
  154. accessorKey: 'created_at',
  155. header: '创建时间',
  156. enableSorting: true,
  157. cell: ({ row }) => {
  158. const date = new Date(row.original.created_at);
  159. return (
  160. <span className="text-body-medium text-secondary-foreground">
  161. {date.toLocaleDateString('zh-CN', {
  162. year: 'numeric',
  163. month: '2-digit',
  164. day: '2-digit',
  165. })}
  166. </span>
  167. );
  168. },
  169. size: 150,
  170. minSize: 120,
  171. maxSize: 180,
  172. },
  173. ];
  174. if (loading) {
  175. return (
  176. <div className="flex items-center justify-center h-full">
  177. <div className="text-center">
  178. <p className="text-body-medium text-secondary-foreground">加载中...</p>
  179. </div>
  180. </div>
  181. );
  182. }
  183. if (error) {
  184. return (
  185. <div className="flex flex-col gap-comfortable h-full">
  186. <div className="flex items-center gap-comfortable pb-comfortable border-b border-neutral-border">
  187. <Button
  188. variant="neutral"
  189. look="string"
  190. size="small"
  191. onClick={() => navigate('/projects')}
  192. leading={<IconArrowLeft className="size-4" />}
  193. >
  194. 返回
  195. </Button>
  196. </div>
  197. <div className="flex-1 flex items-center justify-center">
  198. <div className="bg-error-background text-error-foreground p-comfortable rounded-lg border border-error-border max-w-md">
  199. <div className="flex flex-col gap-tight">
  200. <span className="text-body-medium font-semibold">加载失败</span>
  201. <span className="text-body-medium">{error}</span>
  202. </div>
  203. </div>
  204. </div>
  205. </div>
  206. );
  207. }
  208. if (!currentProject) {
  209. return (
  210. <div className="flex items-center justify-center h-full">
  211. <div className="text-center">
  212. <p className="text-body-medium text-secondary-foreground">项目不存在</p>
  213. </div>
  214. </div>
  215. );
  216. }
  217. return (
  218. <div className="flex flex-col gap-comfortable h-full">
  219. {/* Header */}
  220. <div className="flex items-center justify-between pb-comfortable border-b border-neutral-border">
  221. <div className="flex items-center gap-comfortable">
  222. <Button
  223. variant="neutral"
  224. look="string"
  225. size="small"
  226. onClick={() => navigate('/projects')}
  227. leading={<IconArrowLeft className="size-4" />}
  228. >
  229. 返回
  230. </Button>
  231. <div>
  232. <h1 className="text-heading-large font-bold text-primary-foreground">
  233. {currentProject.name}
  234. </h1>
  235. <p className="text-body-medium text-secondary-foreground mt-tighter">
  236. {currentProject.description}
  237. </p>
  238. </div>
  239. </div>
  240. <Button
  241. variant="neutral"
  242. size="medium"
  243. onClick={handleEditProject}
  244. leading={<IconEdit className="size-4" />}
  245. >
  246. 编辑项目
  247. </Button>
  248. </div>
  249. {/* Project Info */}
  250. <div className="bg-primary-background border border-neutral-border rounded-lg p-comfortable">
  251. <h2 className="text-heading-small font-semibold text-primary-foreground mb-comfortable">
  252. 项目信息
  253. </h2>
  254. <div className="grid grid-cols-2 gap-comfortable">
  255. <div>
  256. <span className="text-body-small text-secondary-foreground">创建时间</span>
  257. <p className="text-body-medium text-primary-foreground mt-tighter">
  258. {new Date(currentProject.created_at).toLocaleString('zh-CN', {
  259. year: 'numeric',
  260. month: '2-digit',
  261. day: '2-digit',
  262. hour: '2-digit',
  263. minute: '2-digit',
  264. })}
  265. </p>
  266. </div>
  267. <div>
  268. <span className="text-body-small text-secondary-foreground">任务数量</span>
  269. <p className="text-body-medium text-primary-foreground mt-tighter">
  270. {currentProject.task_count} 个任务
  271. </p>
  272. </div>
  273. <div className="col-span-2">
  274. <span className="text-body-small text-secondary-foreground">标注配置</span>
  275. <pre className="text-body-small font-mono text-primary-foreground mt-tighter bg-secondary-background p-tight rounded border border-neutral-border overflow-x-auto">
  276. {currentProject.config}
  277. </pre>
  278. </div>
  279. </div>
  280. </div>
  281. {/* Tasks Section */}
  282. <div className="flex-1 flex flex-col gap-comfortable overflow-hidden">
  283. <div className="flex items-center justify-between">
  284. <h2 className="text-heading-small font-semibold text-primary-foreground">
  285. 关联任务
  286. </h2>
  287. <Button
  288. variant="primary"
  289. size="medium"
  290. onClick={handleCreateTask}
  291. leading={<IconPlus className="size-4" />}
  292. >
  293. 创建任务
  294. </Button>
  295. </div>
  296. <div className="flex-1 overflow-hidden">
  297. <DataTable
  298. data={tasks}
  299. columns={taskColumns}
  300. isLoading={tasksLoading}
  301. loadingRows={5}
  302. enableSorting={true}
  303. emptyState={{
  304. title: '暂无任务',
  305. description: '点击"创建任务"按钮为此项目创建第一个任务',
  306. actions: (
  307. <Button
  308. variant="primary"
  309. size="medium"
  310. onClick={handleCreateTask}
  311. leading={<IconPlus className="size-4" />}
  312. >
  313. 创建任务
  314. </Button>
  315. ),
  316. }}
  317. />
  318. </div>
  319. </div>
  320. {/* Create Task Dialog */}
  321. <Dialog open={isCreateTaskDialogOpen} onOpenChange={setIsCreateTaskDialogOpen}>
  322. <DialogContent className="max-w-2xl">
  323. <DialogHeader>
  324. <DialogTitle>创建任务</DialogTitle>
  325. </DialogHeader>
  326. <TaskForm
  327. projectId={id}
  328. onSubmit={handleTaskFormSubmit}
  329. onCancel={handleTaskFormCancel}
  330. submitLabel="创建任务"
  331. isSubmitting={isCreatingTask}
  332. />
  333. </DialogContent>
  334. </Dialog>
  335. </div>
  336. );
  337. };