| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- /**
- * ProjectDetailView Component
- *
- * Displays project details and associated tasks.
- * Requirements: 1.5, 1.6
- */
- import React, { useEffect, useState } from 'react';
- import { useParams, useNavigate } from 'react-router-dom';
- import { useAtom } from 'jotai';
- import {
- Button,
- IconArrowLeft,
- IconEdit,
- IconPlus,
- DataTable,
- type ExtendedDataTableColumnDef,
- Badge,
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- } from '@humansignal/ui';
- import { getProject, getProjectTasks, createTask } from '../services/api';
- import { currentProjectAtom, projectLoadingAtom, projectErrorAtom } from '../atoms/project-atoms';
- import { tasksAtom, type Task } from '../atoms/task-atoms';
- import { TaskForm, type TaskFormData } from '../components';
- export const ProjectDetailView: React.FC = () => {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
-
- const [currentProject, setCurrentProject] = useAtom(currentProjectAtom);
- const [loading, setLoading] = useAtom(projectLoadingAtom);
- const [error, setError] = useAtom(projectErrorAtom);
- const [tasks, setTasks] = useAtom(tasksAtom);
- const [tasksLoading, setTasksLoading] = useState(false);
- const [isCreateTaskDialogOpen, setIsCreateTaskDialogOpen] = useState(false);
- const [isCreatingTask, setIsCreatingTask] = useState(false);
- // Load project details and tasks
- useEffect(() => {
- if (!id) return;
- const loadProjectData = async () => {
- try {
- setLoading(true);
- setError(null);
-
- // Load project details
- const projectData = await getProject(id);
- setCurrentProject(projectData);
-
- // Load project tasks
- setTasksLoading(true);
- const tasksData = await getProjectTasks(id);
- setTasks(tasksData);
- } catch (err: any) {
- setError(err.message || '加载项目详情失败');
- } finally {
- setLoading(false);
- setTasksLoading(false);
- }
- };
- loadProjectData();
- }, [id]);
- const handleEditProject = () => {
- navigate(`/projects/${id}/edit`);
- };
- const handleCreateTask = () => {
- setIsCreateTaskDialogOpen(true);
- };
- const handleTaskFormSubmit = async (data: TaskFormData) => {
- try {
- setIsCreatingTask(true);
-
- // Create task via API
- const newTask = await createTask({
- project_id: data.project_id,
- name: data.name,
- data: data.data,
- assigned_to: data.assigned_to || null,
- });
-
- // Add new task to tasks list
- setTasks((prevTasks) => [...prevTasks, newTask]);
-
- // Update project task count
- if (currentProject) {
- setCurrentProject({
- ...currentProject,
- task_count: currentProject.task_count + 1,
- });
- }
-
- // Close dialog
- setIsCreateTaskDialogOpen(false);
- } catch (err: any) {
- throw new Error(err.message || '创建任务失败');
- } finally {
- setIsCreatingTask(false);
- }
- };
- const handleTaskFormCancel = () => {
- setIsCreateTaskDialogOpen(false);
- };
- // Define task table columns
- const taskColumns: ExtendedDataTableColumnDef<Task>[] = [
- {
- accessorKey: 'name',
- header: '任务名称',
- enableSorting: true,
- cell: ({ row }) => (
- <span className="text-body-medium text-primary-foreground font-semibold">
- {row.original.name}
- </span>
- ),
- size: 300,
- minSize: 200,
- },
- {
- accessorKey: 'status',
- header: '状态',
- enableSorting: true,
- cell: ({ row }) => {
- const statusMap = {
- pending: { label: '待处理', variant: 'secondary' as const },
- in_progress: { label: '进行中', variant: 'info' as const },
- completed: { label: '已完成', variant: 'success' as const },
- };
- const status = statusMap[row.original.status];
- return <Badge variant={status.variant}>{status.label}</Badge>;
- },
- size: 120,
- minSize: 100,
- maxSize: 150,
- },
- {
- accessorKey: 'progress',
- header: '进度',
- enableSorting: true,
- cell: ({ row }) => (
- <span className="text-body-medium text-primary-foreground">
- {row.original.progress}%
- </span>
- ),
- size: 100,
- minSize: 80,
- maxSize: 120,
- },
- {
- accessorKey: 'assigned_to',
- header: '分配给',
- enableSorting: true,
- cell: ({ row }) => (
- <span className="text-body-medium text-secondary-foreground">
- {row.original.assigned_to || '未分配'}
- </span>
- ),
- size: 150,
- minSize: 120,
- maxSize: 180,
- },
- {
- accessorKey: 'created_at',
- header: '创建时间',
- enableSorting: true,
- cell: ({ row }) => {
- const date = new Date(row.original.created_at);
- return (
- <span className="text-body-medium text-secondary-foreground">
- {date.toLocaleDateString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- })}
- </span>
- );
- },
- size: 150,
- minSize: 120,
- maxSize: 180,
- },
- ];
- if (loading) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-center">
- <p className="text-body-medium text-secondary-foreground">加载中...</p>
- </div>
- </div>
- );
- }
- if (error) {
- return (
- <div className="flex flex-col gap-comfortable h-full">
- <div className="flex items-center gap-comfortable pb-comfortable border-b border-neutral-border">
- <Button
- variant="neutral"
- look="string"
- size="small"
- onClick={() => navigate('/projects')}
- leading={<IconArrowLeft className="size-4" />}
- >
- 返回
- </Button>
- </div>
- <div className="flex-1 flex items-center justify-center">
- <div className="bg-error-background text-error-foreground p-comfortable rounded-lg border border-error-border max-w-md">
- <div className="flex flex-col gap-tight">
- <span className="text-body-medium font-semibold">加载失败</span>
- <span className="text-body-medium">{error}</span>
- </div>
- </div>
- </div>
- </div>
- );
- }
- if (!currentProject) {
- return (
- <div className="flex items-center justify-center h-full">
- <div className="text-center">
- <p className="text-body-medium text-secondary-foreground">项目不存在</p>
- </div>
- </div>
- );
- }
- return (
- <div className="flex flex-col gap-comfortable h-full">
- {/* Header */}
- <div className="flex items-center justify-between pb-comfortable border-b border-neutral-border">
- <div className="flex items-center gap-comfortable">
- <Button
- variant="neutral"
- look="string"
- size="small"
- onClick={() => navigate('/projects')}
- leading={<IconArrowLeft className="size-4" />}
- >
- 返回
- </Button>
- <div>
- <h1 className="text-heading-large font-bold text-primary-foreground">
- {currentProject.name}
- </h1>
- <p className="text-body-medium text-secondary-foreground mt-tighter">
- {currentProject.description}
- </p>
- </div>
- </div>
- <Button
- variant="neutral"
- size="medium"
- onClick={handleEditProject}
- leading={<IconEdit className="size-4" />}
- >
- 编辑项目
- </Button>
- </div>
- {/* Project Info */}
- <div className="bg-primary-background border border-neutral-border rounded-lg p-comfortable">
- <h2 className="text-heading-small font-semibold text-primary-foreground mb-comfortable">
- 项目信息
- </h2>
- <div className="grid grid-cols-2 gap-comfortable">
- <div>
- <span className="text-body-small text-secondary-foreground">创建时间</span>
- <p className="text-body-medium text-primary-foreground mt-tighter">
- {new Date(currentProject.created_at).toLocaleString('zh-CN', {
- year: 'numeric',
- month: '2-digit',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- })}
- </p>
- </div>
- <div>
- <span className="text-body-small text-secondary-foreground">任务数量</span>
- <p className="text-body-medium text-primary-foreground mt-tighter">
- {currentProject.task_count} 个任务
- </p>
- </div>
- <div className="col-span-2">
- <span className="text-body-small text-secondary-foreground">标注配置</span>
- <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">
- {currentProject.config}
- </pre>
- </div>
- </div>
- </div>
- {/* Tasks Section */}
- <div className="flex-1 flex flex-col gap-comfortable overflow-hidden">
- <div className="flex items-center justify-between">
- <h2 className="text-heading-small font-semibold text-primary-foreground">
- 关联任务
- </h2>
- <Button
- variant="primary"
- size="medium"
- onClick={handleCreateTask}
- leading={<IconPlus className="size-4" />}
- >
- 创建任务
- </Button>
- </div>
- <div className="flex-1 overflow-hidden">
- <DataTable
- data={tasks}
- columns={taskColumns}
- isLoading={tasksLoading}
- loadingRows={5}
- enableSorting={true}
- emptyState={{
- title: '暂无任务',
- description: '点击"创建任务"按钮为此项目创建第一个任务',
- actions: (
- <Button
- variant="primary"
- size="medium"
- onClick={handleCreateTask}
- leading={<IconPlus className="size-4" />}
- >
- 创建任务
- </Button>
- ),
- }}
- />
- </div>
- </div>
- {/* Create Task Dialog */}
- <Dialog open={isCreateTaskDialogOpen} onOpenChange={setIsCreateTaskDialogOpen}>
- <DialogContent className="max-w-2xl">
- <DialogHeader>
- <DialogTitle>创建任务</DialogTitle>
- </DialogHeader>
- <TaskForm
- projectId={id}
- onSubmit={handleTaskFormSubmit}
- onCancel={handleTaskFormCancel}
- submitLabel="创建任务"
- isSubmitting={isCreatingTask}
- />
- </DialogContent>
- </Dialog>
- </div>
- );
- };
|