Selaa lähdekoodia

-dev:完善前端页面

LuoChinWen 1 kuukausi sitten
vanhempi
sitoutus
86def6043b

+ 14 - 12
.kiro/specs/annotation-platform/tasks.md

@@ -222,8 +222,8 @@
   - 测试任务列表显示
   - _Requirements: 1.5_
 
-- [ ] 13. 实现 TaskListView
-  - [-] 13.1 创建 TaskListView 组件
+- [x] 13. 实现 TaskListView
+  - [x] 13.1 创建 TaskListView 组件
     - 使用 @humansignal/ui 的 DataTable 显示任务列表
     - 实现状态筛选功能
     - 实现任务操作 (开始标注、查看详情、删除)
@@ -244,12 +244,14 @@
     - 验证用户只看到分配给自己的任务
     - _Requirements: 3.1_
 
-- [ ] 14. 实现 TaskForm 组件
-  - 创建 TaskForm 组件
-  - 使用 @humansignal/ui 的表单组件
-  - 实现表单验证
-  - 实现提交和取消功能
-  - _Requirements: 2.1, 2.2_
+- [x] 14. 实现 TaskForm 组件
+  - [x] 14.1 创建 TaskForm 组件
+    - 使用 @humansignal/ui 的表单组件
+    - 实现表单验证 (name, project_id, data 必填)
+    - 实现 JSON 格式验证
+    - 实现提交和取消功能
+    - 在 ProjectDetailView 中集成 TaskForm
+    - _Requirements: 2.1, 2.2_
 
 - [ ]* 14.1 编写单元测试
   - 在 web/apps/lq_label/test/ 目录创建测试文件
@@ -257,19 +259,19 @@
   - 测试提交功能
   - _Requirements: 2.1, 2.2_
 
-- [ ] 15. Checkpoint - 项目和任务管理完成
+- [x] 15. Checkpoint - 项目和任务管理完成
   - 确保项目和任务的 CRUD 功能正常
   - 确保所有测试通过
   - 询问用户是否有问题
 
-- [ ] 16. 实现 AnnotationView (LabelStudio 集成)
-  - [ ] 16.1 创建 AnnotationView 组件基础结构
+- [-] 16. 实现 AnnotationView (LabelStudio 集成)
+  - [x] 16.1 创建 AnnotationView 组件基础结构
     - 创建组件框架
     - 实现加载状态和错误状态显示
     - 添加保存和跳过按钮
     - _Requirements: 3.2, 10.4_
 
-  - [ ] 16.2 集成 LabelStudio 编辑器
+  - [-] 16.2 集成 LabelStudio 编辑器
     - 动态导入 @humansignal/editor
     - 获取项目配置和任务数据
     - 初始化 LabelStudio 实例

BIN
backend/annotation_platform.db


+ 2 - 2
web/apps/lq_label/src/app/app.tsx

@@ -8,6 +8,7 @@ import {
   ProjectEditView,
   TasksView,
   AnnotationsView,
+  AnnotationView,
 } from '../views';
 
 /**
@@ -33,8 +34,7 @@ export function App() {
 
         {/* Tasks Routes */}
         <Route path="/tasks" element={<TasksView />} />
-        {/* TODO: Add task annotation route */}
-        {/* <Route path="/tasks/:id/annotate" element={<AnnotationView />} /> */}
+        <Route path="/tasks/:id/annotate" element={<AnnotationView />} />
 
         {/* Annotations Routes */}
         <Route path="/annotations" element={<AnnotationsView />} />

+ 1 - 0
web/apps/lq_label/src/components/index.ts

@@ -10,4 +10,5 @@ export * from './layout';
 
 // Form components
 export * from './project-form';
+export * from './task-form';
 

+ 4 - 0
web/apps/lq_label/src/components/task-form/index.ts

@@ -0,0 +1,4 @@
+/**
+ * TaskForm Component Export
+ */
+export * from './task-form';

+ 10 - 0
web/apps/lq_label/src/components/task-form/task-form.module.scss

@@ -0,0 +1,10 @@
+/**
+ * TaskForm Component Styles
+ * 
+ * Minimal styles for TaskForm component.
+ * Most styling is handled by Tailwind utility classes.
+ */
+
+.task-form {
+  // Additional custom styles if needed
+}

+ 282 - 0
web/apps/lq_label/src/components/task-form/task-form.tsx

@@ -0,0 +1,282 @@
+/**
+ * TaskForm Component
+ * 
+ * Reusable form component for creating and editing tasks.
+ * Requirements: 2.1, 2.2
+ */
+import React, { useState, useEffect } from 'react';
+import { Button } from '@humansignal/ui';
+
+export interface TaskFormData {
+  name: string;
+  project_id: string;
+  data: Record<string, any>;
+  assigned_to?: string;
+}
+
+export interface TaskFormProps {
+  initialData?: Partial<TaskFormData>;
+  projectId?: string; // Pre-fill project_id when creating from project detail page
+  onSubmit: (data: TaskFormData) => Promise<void>;
+  onCancel: () => void;
+  submitLabel?: string;
+  isSubmitting?: boolean;
+}
+
+export const TaskForm: React.FC<TaskFormProps> = ({
+  initialData,
+  projectId,
+  onSubmit,
+  onCancel,
+  submitLabel = '提交',
+  isSubmitting = false,
+}) => {
+  const [formData, setFormData] = useState<TaskFormData>({
+    name: initialData?.name || '',
+    project_id: projectId || initialData?.project_id || '',
+    data: initialData?.data || {},
+    assigned_to: initialData?.assigned_to,
+  });
+
+  const [dataJson, setDataJson] = useState<string>(
+    JSON.stringify(initialData?.data || {}, null, 2)
+  );
+
+  const [formErrors, setFormErrors] = useState<Record<string, string>>({});
+
+  // Update form data when initialData or projectId changes
+  useEffect(() => {
+    if (initialData || projectId) {
+      setFormData({
+        name: initialData?.name || '',
+        project_id: projectId || initialData?.project_id || '',
+        data: initialData?.data || {},
+        assigned_to: initialData?.assigned_to,
+      });
+      setDataJson(JSON.stringify(initialData?.data || {}, null, 2));
+    }
+  }, [initialData, projectId]);
+
+  const validateForm = (): boolean => {
+    const errors: Record<string, string> = {};
+
+    // Validate name (required, non-empty)
+    if (!formData.name.trim()) {
+      errors.name = '任务名称不能为空';
+    }
+
+    // Validate project_id (required, non-empty)
+    if (!formData.project_id.trim()) {
+      errors.project_id = '项目 ID 不能为空';
+    }
+
+    // Validate data JSON
+    try {
+      const parsedData = JSON.parse(dataJson);
+      if (typeof parsedData !== 'object' || parsedData === null) {
+        errors.data = '任务数据必须是有效的 JSON 对象';
+      }
+    } catch (e) {
+      errors.data = '任务数据必须是有效的 JSON 格式';
+    }
+
+    setFormErrors(errors);
+    return Object.keys(errors).length === 0;
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+
+    if (!validateForm()) {
+      return;
+    }
+
+    try {
+      // Parse JSON data
+      const parsedData = JSON.parse(dataJson);
+      
+      const submitData: TaskFormData = {
+        ...formData,
+        data: parsedData,
+        assigned_to: formData.assigned_to?.trim() || undefined,
+      };
+
+      await onSubmit(submitData);
+      
+      // Reset form on successful submission
+      setFormData({
+        name: '',
+        project_id: projectId || '',
+        data: {},
+        assigned_to: undefined,
+      });
+      setDataJson('{}');
+      setFormErrors({});
+    } catch (error: any) {
+      setFormErrors({ submit: error.message || '提交失败' });
+    }
+  };
+
+  const handleFieldChange = (field: keyof TaskFormData, value: string) => {
+    setFormData((prev) => ({ ...prev, [field]: value }));
+    // Clear field error when user starts typing
+    if (formErrors[field]) {
+      setFormErrors((prev) => {
+        const newErrors = { ...prev };
+        delete newErrors[field];
+        return newErrors;
+      });
+    }
+  };
+
+  const handleDataJsonChange = (value: string) => {
+    setDataJson(value);
+    // Clear data error when user starts typing
+    if (formErrors.data) {
+      setFormErrors((prev) => {
+        const newErrors = { ...prev };
+        delete newErrors.data;
+        return newErrors;
+      });
+    }
+  };
+
+  return (
+    <form onSubmit={handleSubmit} className="flex flex-col gap-comfortable">
+      {/* Name field */}
+      <div className="flex flex-col gap-tight">
+        <label
+          htmlFor="task-name"
+          className="text-body-medium font-semibold text-primary-foreground"
+        >
+          任务名称 <span className="text-error-foreground">*</span>
+        </label>
+        <input
+          id="task-name"
+          type="text"
+          value={formData.name}
+          onChange={(e) => handleFieldChange('name', e.target.value)}
+          className="px-comfortable py-tight border border-neutral-border rounded-lg text-body-medium bg-primary-background text-primary-foreground focus:outline-none focus:ring-2 focus:ring-primary-border"
+          placeholder="输入任务名称"
+          disabled={isSubmitting}
+          aria-invalid={!!formErrors.name}
+          aria-describedby={formErrors.name ? 'name-error' : undefined}
+        />
+        {formErrors.name && (
+          <span id="name-error" className="text-body-small text-error-foreground">
+            {formErrors.name}
+          </span>
+        )}
+      </div>
+
+      {/* Project ID field */}
+      <div className="flex flex-col gap-tight">
+        <label
+          htmlFor="task-project-id"
+          className="text-body-medium font-semibold text-primary-foreground"
+        >
+          项目 ID <span className="text-error-foreground">*</span>
+        </label>
+        <input
+          id="task-project-id"
+          type="text"
+          value={formData.project_id}
+          onChange={(e) => handleFieldChange('project_id', e.target.value)}
+          className="px-comfortable py-tight border border-neutral-border rounded-lg text-body-small font-mono bg-primary-background text-primary-foreground focus:outline-none focus:ring-2 focus:ring-primary-border"
+          placeholder="输入项目 ID"
+          disabled={isSubmitting || !!projectId}
+          aria-invalid={!!formErrors.project_id}
+          aria-describedby={formErrors.project_id ? 'project-id-error' : undefined}
+        />
+        {formErrors.project_id && (
+          <span id="project-id-error" className="text-body-small text-error-foreground">
+            {formErrors.project_id}
+          </span>
+        )}
+        {projectId && (
+          <span className="text-body-small text-secondary-foreground">
+            此任务将创建在当前项目下
+          </span>
+        )}
+      </div>
+
+      {/* Assigned to field (optional) */}
+      <div className="flex flex-col gap-tight">
+        <label
+          htmlFor="task-assigned-to"
+          className="text-body-medium font-semibold text-primary-foreground"
+        >
+          分配给
+        </label>
+        <input
+          id="task-assigned-to"
+          type="text"
+          value={formData.assigned_to}
+          onChange={(e) => handleFieldChange('assigned_to', e.target.value)}
+          className="px-comfortable py-tight border border-neutral-border rounded-lg text-body-medium bg-primary-background text-primary-foreground focus:outline-none focus:ring-2 focus:ring-primary-border"
+          placeholder="输入用户名(可选)"
+          disabled={isSubmitting}
+        />
+        <span className="text-body-small text-secondary-foreground">
+          可选字段,留空表示未分配
+        </span>
+      </div>
+
+      {/* Data field (JSON) */}
+      <div className="flex flex-col gap-tight">
+        <label
+          htmlFor="task-data"
+          className="text-body-medium font-semibold text-primary-foreground"
+        >
+          任务数据 (JSON) <span className="text-error-foreground">*</span>
+        </label>
+        <textarea
+          id="task-data"
+          value={dataJson}
+          onChange={(e) => handleDataJsonChange(e.target.value)}
+          className="px-comfortable py-tight border border-neutral-border rounded-lg text-body-small font-mono bg-primary-background text-primary-foreground focus:outline-none focus:ring-2 focus:ring-primary-border resize-none"
+          placeholder='输入任务数据 JSON,例如:{"text": "待标注的文本"}'
+          rows={8}
+          disabled={isSubmitting}
+          aria-invalid={!!formErrors.data}
+          aria-describedby={formErrors.data ? 'data-error' : undefined}
+        />
+        {formErrors.data && (
+          <span id="data-error" className="text-body-small text-error-foreground">
+            {formErrors.data}
+          </span>
+        )}
+        <span className="text-body-small text-secondary-foreground">
+          请输入有效的 JSON 对象,包含待标注的数据
+        </span>
+      </div>
+
+      {/* Submit error */}
+      {formErrors.submit && (
+        <div className="bg-error-background text-error-foreground p-comfortable rounded-lg border border-error-border">
+          {formErrors.submit}
+        </div>
+      )}
+
+      {/* Form actions */}
+      <div className="flex items-center justify-end gap-tight pt-comfortable">
+        <Button
+          type="button"
+          variant="neutral"
+          look="outlined"
+          onClick={onCancel}
+          disabled={isSubmitting}
+        >
+          取消
+        </Button>
+        <Button
+          type="submit"
+          variant="primary"
+          disabled={isSubmitting}
+        >
+          {isSubmitting ? '提交中...' : submitLabel}
+        </Button>
+      </div>
+    </form>
+  );
+};

+ 10 - 0
web/apps/lq_label/src/views/annotation-view/annotation-view.module.scss

@@ -0,0 +1,10 @@
+/**
+ * AnnotationView Component Styles
+ * 
+ * Minimal styles for AnnotationView component.
+ * Most styling is handled by Tailwind utility classes.
+ */
+
+.annotation-view {
+  // Additional custom styles if needed
+}

+ 214 - 0
web/apps/lq_label/src/views/annotation-view/annotation-view.tsx

@@ -0,0 +1,214 @@
+/**
+ * AnnotationView Component
+ * 
+ * Annotation interface with LabelStudio editor integration.
+ * Requirements: 3.2, 8.1, 8.2, 8.3, 10.4
+ */
+import React, { useEffect, useState, useRef } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useAtom } from 'jotai';
+import {
+  Button,
+  IconArrowLeft,
+  IconCheck,
+  IconForward,
+} from '@humansignal/ui';
+import { getTask, getProject, createAnnotation, updateTask } from '../../services/api';
+import { currentTaskAtom } from '../../atoms/task-atoms';
+import { currentProjectAtom } from '../../atoms/project-atoms';
+
+export const AnnotationView: React.FC = () => {
+  const { id } = useParams<{ id: string }>();
+  const navigate = useNavigate();
+  
+  const [currentTask, setCurrentTask] = useAtom(currentTaskAtom);
+  const [currentProject, setCurrentProject] = useAtom(currentProjectAtom);
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState<string | null>(null);
+  const [isSaving, setIsSaving] = useState(false);
+  
+  const editorContainerRef = useRef<HTMLDivElement>(null);
+
+  // Load task and project data
+  useEffect(() => {
+    if (!id) return;
+
+    const loadData = async () => {
+      try {
+        setLoading(true);
+        setError(null);
+        
+        // Load task details
+        const taskData = await getTask(id);
+        setCurrentTask(taskData);
+        
+        // Load project details to get annotation config
+        const projectData = await getProject(taskData.project_id);
+        setCurrentProject(projectData);
+      } catch (err: any) {
+        setError(err.message || '加载任务失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    loadData();
+  }, [id]);
+
+  const handleSave = async () => {
+    if (!currentTask || !id) return;
+
+    try {
+      setIsSaving(true);
+      
+      // TODO: Get annotation result from LabelStudio editor
+      const annotationResult = {};
+      
+      // Create annotation
+      await createAnnotation({
+        task_id: id,
+        user_id: 'current_user', // TODO: Get from auth context
+        result: annotationResult,
+      });
+      
+      // Update task status to in_progress or completed
+      await updateTask(id, {
+        status: 'in_progress',
+      });
+      
+      // Navigate back to tasks list
+      navigate('/tasks');
+    } catch (err: any) {
+      setError(err.message || '保存标注失败');
+    } finally {
+      setIsSaving(false);
+    }
+  };
+
+  const handleSkip = () => {
+    // Navigate back to tasks list without saving
+    navigate('/tasks');
+  };
+
+  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('/tasks')}
+            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 (!currentTask || !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 h-full">
+      {/* Header */}
+      <div className="flex items-center justify-between p-comfortable border-b border-neutral-border bg-primary-background">
+        <div className="flex items-center gap-comfortable">
+          <Button
+            variant="neutral"
+            look="string"
+            size="small"
+            onClick={() => navigate('/tasks')}
+            leading={<IconArrowLeft className="size-4" />}
+          >
+            返回
+          </Button>
+          <div>
+            <h1 className="text-heading-medium font-bold text-primary-foreground">
+              {currentTask.name}
+            </h1>
+            <p className="text-body-small text-secondary-foreground">
+              项目: {currentProject.name} | 进度: {currentTask.progress}%
+            </p>
+          </div>
+        </div>
+        <div className="flex items-center gap-tight">
+          <Button
+            variant="neutral"
+            size="medium"
+            onClick={handleSkip}
+            disabled={isSaving}
+            leading={<IconForward className="size-4" />}
+          >
+            跳过
+          </Button>
+          <Button
+            variant="primary"
+            size="medium"
+            onClick={handleSave}
+            disabled={isSaving}
+            leading={<IconCheck className="size-4" />}
+          >
+            {isSaving ? '保存中...' : '保存'}
+          </Button>
+        </div>
+      </div>
+
+      {/* Editor Container */}
+      <div className="flex-1 overflow-hidden bg-secondary-background">
+        <div
+          ref={editorContainerRef}
+          className="w-full h-full"
+          id="label-studio-editor"
+        >
+          {/* LabelStudio editor will be mounted here */}
+          <div className="flex items-center justify-center h-full">
+            <div className="text-center">
+              <h2 className="text-heading-medium font-semibold text-primary-foreground mb-tight">
+                标注编辑器
+              </h2>
+              <p className="text-body-medium text-secondary-foreground mb-comfortable">
+                LabelStudio 编辑器将在此处加载
+              </p>
+              <div className="bg-primary-background border border-neutral-border rounded-lg p-comfortable max-w-2xl mx-auto">
+                <h3 className="text-body-medium font-semibold text-primary-foreground mb-tight">
+                  任务数据:
+                </h3>
+                <pre className="text-body-small font-mono text-primary-foreground text-left bg-secondary-background p-tight rounded border border-neutral-border overflow-auto">
+                  {JSON.stringify(currentTask.data, null, 2)}
+                </pre>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 4 - 0
web/apps/lq_label/src/views/annotation-view/index.ts

@@ -0,0 +1,4 @@
+/**
+ * AnnotationView Component Export
+ */
+export * from './annotation-view';

+ 1 - 0
web/apps/lq_label/src/views/index.ts

@@ -15,4 +15,5 @@ export { ProjectDetailView } from './project-detail-view';
 export { ProjectEditView } from './project-edit-view';
 export { TasksView } from './tasks-view';
 export { AnnotationsView } from './annotations-view';
+export { AnnotationView } from './annotation-view';
 

+ 61 - 3
web/apps/lq_label/src/views/project-detail-view.tsx

@@ -15,10 +15,15 @@ import {
   DataTable,
   type ExtendedDataTableColumnDef,
   Badge,
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
 } from '@humansignal/ui';
-import { getProject, getProjectTasks } from '../services/api';
+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 }>();
@@ -29,6 +34,8 @@ export const ProjectDetailView: React.FC = () => {
   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(() => {
@@ -63,8 +70,43 @@ export const ProjectDetailView: React.FC = () => {
   };
 
   const handleCreateTask = () => {
-    // TODO: Implement task creation
-    alert('创建任务功能即将推出');
+    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
@@ -298,6 +340,22 @@ export const ProjectDetailView: React.FC = () => {
           />
         </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>
   );
 };

+ 127 - 20
web/apps/lq_label/src/views/project-edit-view.tsx

@@ -4,19 +4,86 @@
  * Allows editing of existing projects.
  * Requirements: 1.6
  */
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import { useParams, useNavigate } from 'react-router-dom';
+import { useAtom } from 'jotai';
 import { Button, IconArrowLeft } from '@humansignal/ui';
+import { getProject, updateProject } from '../services/api';
+import { currentProjectAtom, projectLoadingAtom, projectErrorAtom } from '../atoms/project-atoms';
+import { ProjectForm, type ProjectFormData } from '../components';
 
 export const ProjectEditView: 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 [isSubmitting, setIsSubmitting] = useState(false);
 
-  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">
+  // Load project details
+  useEffect(() => {
+    if (!id) return;
+
+    const loadProject = async () => {
+      try {
+        setLoading(true);
+        setError(null);
+        const projectData = await getProject(id);
+        setCurrentProject(projectData);
+      } catch (err: any) {
+        setError(err.message || '加载项目失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    loadProject();
+  }, [id]);
+
+  const handleSubmit = async (data: ProjectFormData) => {
+    if (!id) return;
+
+    try {
+      setIsSubmitting(true);
+      
+      // Update project via API
+      const updatedProject = await updateProject(id, {
+        name: data.name,
+        description: data.description,
+        config: data.config,
+      });
+      
+      // Update current project atom
+      setCurrentProject(updatedProject);
+      
+      // Navigate back to project detail
+      navigate(`/projects/${id}`);
+    } catch (err: any) {
+      throw new Error(err.message || '更新项目失败');
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  const handleCancel = () => {
+    navigate(`/projects/${id}`);
+  };
+
+  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"
@@ -26,28 +93,68 @@ export const ProjectEditView: React.FC = () => {
           >
             返回
           </Button>
-          <div>
-            <h1 className="text-heading-large font-bold text-primary-foreground">
-              编辑项目
-            </h1>
-            <p className="text-body-medium text-secondary-foreground mt-tighter">
-              项目 ID: {id}
-            </p>
+        </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>
+    );
+  }
 
-      {/* Content */}
-      <div className="flex-1 flex items-center justify-center">
+  if (!currentProject) {
+    return (
+      <div className="flex items-center justify-center h-full">
         <div className="text-center">
-          <h2 className="text-heading-medium font-semibold text-primary-foreground mb-tight">
-            项目编辑页面
-          </h2>
-          <p className="text-body-medium text-secondary-foreground">
-            此页面正在开发中...
+          <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 gap-comfortable pb-comfortable border-b border-neutral-border">
+        <Button
+          variant="neutral"
+          look="string"
+          size="small"
+          onClick={() => navigate(`/projects/${id}`)}
+          leading={<IconArrowLeft className="size-4" />}
+        >
+          返回
+        </Button>
+        <div>
+          <h1 className="text-heading-large font-bold text-primary-foreground">
+            编辑项目
+          </h1>
+          <p className="text-body-medium text-secondary-foreground mt-tighter">
+            {currentProject.name}
           </p>
         </div>
       </div>
+
+      {/* Edit Form */}
+      <div className="flex-1 overflow-auto">
+        <div className="max-w-3xl">
+          <ProjectForm
+            initialData={{
+              name: currentProject.name,
+              description: currentProject.description,
+              config: currentProject.config,
+            }}
+            onSubmit={handleSubmit}
+            onCancel={handleCancel}
+            submitLabel="保存更改"
+            isSubmitting={isSubmitting}
+          />
+        </div>
+      </div>
     </div>
   );
 };

+ 5 - 13
web/apps/lq_label/src/views/tasks-view.tsx

@@ -1,20 +1,12 @@
 /**
- * Tasks View
+ * TasksView
  * 
- * Displays list of tasks (placeholder).
- * Will be replaced with TaskListView component.
+ * Main view for task management page.
+ * Requirements: 2.3, 2.4
  */
 import React from 'react';
+import { TaskListView } from './task-list-view';
 
 export const TasksView: React.FC = () => {
-  return (
-    <div>
-      <h1 className="text-heading-large font-bold text-primary-foreground mb-comfortable">
-        任务管理
-      </h1>
-      <p className="text-body-medium text-secondary-foreground">
-        任务列表视图(待实现)
-      </p>
-    </div>
-  );
+  return <TaskListView />;
 };