LuoChinWen 1 개월 전
부모
커밋
f4b2447602

+ 14 - 1
backend/SAMPLE_DATA_README.md

@@ -20,12 +20,25 @@ yarn nx serve lq_label
 
 ### 2. 初始化示例数据
 
+**文本标注示例数据:**
 ```bash
 cd backend
 python init_sample_data.py
 ```
 
-这将创建 3 个示例项目和 6 个示例任务。
+这将创建 3 个文本标注项目和 6 个示例任务。
+
+**图片标注示例数据:**
+```bash
+cd backend
+python init_image_annotation_data.py
+```
+
+这将创建 6 个图片标注项目和 14 个示例任务。
+
+**详细说明:**
+- 文本标注:参见本文档下方的"示例项目说明"部分
+- 图片标注:参见 [IMAGE_ANNOTATION_README.md](./IMAGE_ANNOTATION_README.md)
 
 ## 示例项目说明
 

BIN
backend/annotation_platform.db


+ 388 - 0
backend/init_image_annotation_data.py

@@ -0,0 +1,388 @@
+"""
+初始化图片标注示例数据脚本
+
+创建多种图片标注项目和任务,用于测试图片标注功能。
+包括:目标检测、图像分类、图像分割、关键点标注等。
+"""
+import requests
+import json
+
+# API 基础 URL
+BASE_URL = "http://localhost:8000"
+
+# 1. 目标检测标注配置(矩形框标注)
+OBJECT_DETECTION_CONFIG = """<View>
+  <Header value="目标检测 - 物体识别"/>
+  <Image name="image" value="$image"/>
+  <RectangleLabels name="label" toName="image">
+    <Label value="人" background="red"/>
+    <Label value="车" background="blue"/>
+    <Label value="自行车" background="green"/>
+    <Label value="狗" background="orange"/>
+    <Label value="猫" background="purple"/>
+  </RectangleLabels>
+</View>"""
+
+# 2. 图像分类标注配置
+IMAGE_CLASSIFICATION_CONFIG = """<View>
+  <Header value="图像分类"/>
+  <Image name="image" value="$image"/>
+  <Choices name="category" toName="image" choice="single" showInline="true">
+    <Choice value="风景"/>
+    <Choice value="人物"/>
+    <Choice value="动物"/>
+    <Choice value="建筑"/>
+    <Choice value="食物"/>
+    <Choice value="其他"/>
+  </Choices>
+</View>"""
+
+# 3. 图像分割标注配置(多边形标注)
+IMAGE_SEGMENTATION_CONFIG = """<View>
+  <Header value="图像分割 - 精细标注"/>
+  <Image name="image" value="$image"/>
+  <PolygonLabels name="label" toName="image">
+    <Label value="前景" background="rgba(255, 0, 0, 0.5)"/>
+    <Label value="背景" background="rgba(0, 0, 255, 0.5)"/>
+    <Label value="物体" background="rgba(0, 255, 0, 0.5)"/>
+  </PolygonLabels>
+</View>"""
+
+# 4. 关键点标注配置(人体姿态估计)
+KEYPOINT_DETECTION_CONFIG = """<View>
+  <Header value="关键点标注 - 人体姿态"/>
+  <Image name="image" value="$image"/>
+  <KeyPointLabels name="keypoint" toName="image">
+    <Label value="头部" background="red"/>
+    <Label value="肩膀" background="blue"/>
+    <Label value="肘部" background="green"/>
+    <Label value="手腕" background="orange"/>
+    <Label value="膝盖" background="purple"/>
+    <Label value="脚踝" background="pink"/>
+  </KeyPointLabels>
+</View>"""
+
+# 5. 多标签图像分类配置
+MULTI_LABEL_CLASSIFICATION_CONFIG = """<View>
+  <Header value="多标签图像分类"/>
+  <Image name="image" value="$image"/>
+  <Choices name="attributes" toName="image" choice="multiple" showInline="false">
+    <Choice value="室内"/>
+    <Choice value="室外"/>
+    <Choice value="白天"/>
+    <Choice value="夜晚"/>
+    <Choice value="晴天"/>
+    <Choice value="雨天"/>
+    <Choice value="有人"/>
+    <Choice value="无人"/>
+  </Choices>
+</View>"""
+
+# 6. 图像质量评估配置
+IMAGE_QUALITY_CONFIG = """<View>
+  <Header value="图像质量评估"/>
+  <Image name="image" value="$image"/>
+  <Choices name="quality" toName="image" choice="single" showInline="true">
+    <Choice value="优秀"/>
+    <Choice value="良好"/>
+    <Choice value="一般"/>
+    <Choice value="较差"/>
+  </Choices>
+  <Choices name="issues" toName="image" choice="multiple" showInline="false">
+    <Choice value="模糊"/>
+    <Choice value="曝光过度"/>
+    <Choice value="曝光不足"/>
+    <Choice value="噪点多"/>
+    <Choice value="色彩失真"/>
+  </Choices>
+</View>"""
+
+# 示例图片任务数据
+# 使用公开的测试图片 URL(来自 Unsplash 等免费图片网站)
+SAMPLE_IMAGE_TASKS = [
+    # 目标检测任务
+    {
+        "name": "街道场景-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800"
+        }
+    },
+    {
+        "name": "街道场景-2",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=800"
+        }
+    },
+    {
+        "name": "公园场景",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=800"
+        }
+    },
+    
+    # 图像分类任务
+    {
+        "name": "风景图片-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800"
+        }
+    },
+    {
+        "name": "建筑图片-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?w=800"
+        }
+    },
+    {
+        "name": "动物图片-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1425082661705-1834bfd09dca?w=800"
+        }
+    },
+    
+    # 图像分割任务
+    {
+        "name": "物体分割-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1518791841217-8f162f1e1131?w=800"
+        }
+    },
+    {
+        "name": "物体分割-2",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba?w=800"
+        }
+    },
+    
+    # 关键点标注任务
+    {
+        "name": "人体姿态-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800"
+        }
+    },
+    {
+        "name": "人体姿态-2",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800"
+        }
+    },
+    
+    # 多标签分类任务
+    {
+        "name": "场景分析-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=800"
+        }
+    },
+    {
+        "name": "场景分析-2",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=800"
+        }
+    },
+    
+    # 图像质量评估任务
+    {
+        "name": "质量评估-1",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=800"
+        }
+    },
+    {
+        "name": "质量评估-2",
+        "data": {
+            "image": "https://images.unsplash.com/photo-1501594907352-04cda38ebc29?w=800"
+        }
+    },
+]
+
+
+def create_project(name, description, config):
+    """创建项目"""
+    url = f"{BASE_URL}/api/projects"
+    data = {
+        "name": name,
+        "description": description,
+        "config": config
+    }
+    
+    response = requests.post(url, json=data)
+    if response.status_code == 201:
+        project = response.json()
+        print(f"✓ 创建项目成功: {project['name']} (ID: {project['id']})")
+        return project
+    else:
+        print(f"✗ 创建项目失败: {response.status_code} - {response.text}")
+        return None
+
+
+def create_task(project_id, task_name, task_data):
+    """创建任务"""
+    url = f"{BASE_URL}/api/tasks"
+    data = {
+        "project_id": project_id,
+        "name": task_name,
+        "data": task_data
+    }
+    
+    response = requests.post(url, json=data)
+    if response.status_code == 201:
+        task = response.json()
+        print(f"  ✓ 创建任务: {task['name']} (ID: {task['id']})")
+        return task
+    else:
+        print(f"  ✗ 创建任务失败: {response.status_code} - {response.text}")
+        return None
+
+
+def main():
+    """主函数"""
+    print("=" * 60)
+    print("初始化图片标注平台示例数据")
+    print("=" * 60)
+    print()
+    
+    # 1. 创建目标检测项目
+    print("1. 创建目标检测项目...")
+    detection_project = create_project(
+        name="目标检测 - 街道场景",
+        description="标注街道场景中的人、车、自行车等物体",
+        config=OBJECT_DETECTION_CONFIG
+    )
+    
+    if detection_project:
+        print("   创建目标检测任务...")
+        for i in range(3):
+            create_task(
+                detection_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    # 2. 创建图像分类项目
+    print("2. 创建图像分类项目...")
+    classification_project = create_project(
+        name="图像分类",
+        description="将图片分类为风景、人物、动物、建筑、食物等类别",
+        config=IMAGE_CLASSIFICATION_CONFIG
+    )
+    
+    if classification_project:
+        print("   创建图像分类任务...")
+        for i in range(3, 6):
+            create_task(
+                classification_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    # 3. 创建图像分割项目
+    print("3. 创建图像分割项目...")
+    segmentation_project = create_project(
+        name="图像分割 - 物体轮廓",
+        description="使用多边形工具精细标注物体轮廓",
+        config=IMAGE_SEGMENTATION_CONFIG
+    )
+    
+    if segmentation_project:
+        print("   创建图像分割任务...")
+        for i in range(6, 8):
+            create_task(
+                segmentation_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    # 4. 创建关键点标注项目
+    print("4. 创建关键点标注项目...")
+    keypoint_project = create_project(
+        name="关键点标注 - 人体姿态估计",
+        description="标注人体关键点(头部、肩膀、肘部、手腕、膝盖、脚踝)",
+        config=KEYPOINT_DETECTION_CONFIG
+    )
+    
+    if keypoint_project:
+        print("   创建关键点标注任务...")
+        for i in range(8, 10):
+            create_task(
+                keypoint_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    # 5. 创建多标签分类项目
+    print("5. 创建多标签分类项目...")
+    multi_label_project = create_project(
+        name="多标签图像分类",
+        description="为图片添加多个属性标签(室内/室外、白天/夜晚等)",
+        config=MULTI_LABEL_CLASSIFICATION_CONFIG
+    )
+    
+    if multi_label_project:
+        print("   创建多标签分类任务...")
+        for i in range(10, 12):
+            create_task(
+                multi_label_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    # 6. 创建图像质量评估项目
+    print("6. 创建图像质量评估项目...")
+    quality_project = create_project(
+        name="图像质量评估",
+        description="评估图片质量并标注存在的问题(模糊、曝光等)",
+        config=IMAGE_QUALITY_CONFIG
+    )
+    
+    if quality_project:
+        print("   创建图像质量评估任务...")
+        for i in range(12, 14):
+            create_task(
+                quality_project['id'],
+                SAMPLE_IMAGE_TASKS[i]['name'],
+                SAMPLE_IMAGE_TASKS[i]['data']
+            )
+    print()
+    
+    print("=" * 60)
+    print("图片标注示例数据初始化完成!")
+    print("=" * 60)
+    print()
+    print("已创建的项目类型:")
+    print("1. 目标检测 - 使用矩形框标注物体")
+    print("2. 图像分类 - 单标签分类")
+    print("3. 图像分割 - 使用多边形精细标注")
+    print("4. 关键点标注 - 人体姿态估计")
+    print("5. 多标签分类 - 场景属性标注")
+    print("6. 图像质量评估 - 质量评分和问题标注")
+    print()
+    print("你现在可以:")
+    print("1. 访问 http://localhost:4200/projects 查看项目列表")
+    print("2. 点击项目查看详情和任务")
+    print("3. 点击'开始标注'按钮进行图片标注")
+    print()
+    print("注意:")
+    print("- 图片来自 Unsplash 免费图片库")
+    print("- 需要网络连接才能加载图片")
+    print("- 如果图片加载失败,请检查网络连接")
+    print()
+
+
+if __name__ == "__main__":
+    try:
+        main()
+    except requests.exceptions.ConnectionError:
+        print("✗ 错误: 无法连接到后端服务器")
+        print("  请确保后端服务器正在运行:")
+        print("  cd backend && python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000")
+    except Exception as e:
+        print(f"✗ 发生错误: {e}")
+        import traceback
+        traceback.print_exc()

+ 0 - 210
web/apps/lq_label/ANNOTATION_SAVE_FIX.md

@@ -1,210 +0,0 @@
-# 标注保存和编辑器清理问题修复
-
-## 问题 1:422 错误 - 标注结果格式不正确
-
-### 错误信息
-```
-POST http://localhost:8000/api/annotations 422 (Unprocessable Entity)
-body.result: Input should be a valid dictionary
-```
-
-### 问题原因
-LabelStudio 的 `serializeAnnotation()` 方法返回的对象包含多个字段(如 `result`, `id`, `created_at` 等),但后端 API 期望 `result` 字段是一个字典对象。
-
-### 解决方案
-
-在 `handleSave()` 函数中添加了数据提取和验证逻辑:
-
-```typescript
-// Extract only the result field if it exists
-let resultData: Record<string, any> = {};
-
-if (annotationResult.result && Array.isArray(annotationResult.result)) {
-  // Standard LabelStudio format: { result: [...], ... }
-  resultData = { result: annotationResult.result };
-} else if (Array.isArray(annotationResult)) {
-  // If it's already an array, wrap it
-  resultData = { result: annotationResult };
-} else {
-  // Use the whole object
-  resultData = annotationResult;
-}
-
-// Validate that we have actual annotation data
-if (!resultData.result || (Array.isArray(resultData.result) && resultData.result.length === 0)) {
-  setError('请完成标注后再保存(标注结果为空)');
-  return;
-}
-```
-
-**改进点**:
-- ✅ 正确提取 `result` 字段
-- ✅ 处理多种数据格式
-- ✅ 验证标注数据不为空
-- ✅ 添加详细的控制台日志
-
-## 问题 2:编辑器清理问题
-
-### 错误信息
-```
-Warning: Attempted to synchronously unmount a root while React was already rendering.
-Error: [mobx-state-tree] You are trying to read or write to an object that is no longer part of a state tree.
-```
-
-### 问题原因
-1. 在 React 渲染过程中同步销毁 LabelStudio 实例
-2. 在组件卸载后仍然尝试访问 MobX 状态树
-3. 任务 ID 变化时没有正确清理旧的编辑器实例
-
-### 解决方案
-
-#### 1. 改进清理逻辑
-
-```typescript
-function cleanup() {
-  if (isCleanedUp) return;
-  isCleanedUp = true;
-
-  console.log('Cleaning up LabelStudio editor...');
-  
-  // 1. Dispose snapshot listener first
-  if (snapshotDisposer) {
-    try {
-      snapshotDisposer();
-    } catch (e) {
-      console.warn('Error disposing snapshot:', e);
-    }
-    snapshotDisposer = null;
-  }
-  
-  // 2. Cancel any pending animation frames
-  if (rafIdRef.current !== null) {
-    cancelAnimationFrame(rafIdRef.current);
-    rafIdRef.current = null;
-  }
-  
-  // 3. Destroy LabelStudio instance asynchronously
-  if (lsfInstanceRef.current) {
-    try {
-      // Give React time to finish rendering before destroying
-      setTimeout(() => {
-        if (lsfInstanceRef.current) {
-          lsfInstanceRef.current.destroy();
-          lsfInstanceRef.current = null;
-        }
-      }, 0);
-    } catch (e) {
-      console.warn('Error destroying LSF instance:', e);
-      lsfInstanceRef.current = null;
-    }
-  }
-  
-  // 4. Clear window.LabelStudio
-  if (typeof window !== 'undefined' && (window as any).LabelStudio) {
-    delete (window as any).LabelStudio;
-  }
-  
-  // 5. Reset state
-  setEditorReady(false);
-  annotationResultRef.current = null;
-}
-```
-
-**改进点**:
-- ✅ 添加 `isCleanedUp` 标志防止重复清理
-- ✅ 按正确顺序清理资源(snapshot → RAF → LSF instance)
-- ✅ 异步销毁 LabelStudio 实例(避免 React 渲染冲突)
-- ✅ 添加错误处理和日志
-- ✅ 在 snapshot 回调中检查 `isCleanedUp` 标志
-
-#### 2. 添加任务 ID 到依赖数组
-
-```typescript
-useEffect(() => {
-  // ...
-  return () => {
-    cleanup();
-  };
-}, [id, loading, error, currentTask, currentProject]); // 添加 'id'
-```
-
-**改进点**:
-- ✅ 任务 ID 变化时自动清理旧编辑器
-- ✅ 确保每次加载新任务时都有干净的编辑器实例
-
-#### 3. 在回调中检查清理状态
-
-```typescript
-onStorageInitialized: (LS: any) => {
-  const initAnnotation = () => {
-    if (isCleanedUp) {
-      console.log('Component was cleaned up, skipping annotation initialization');
-      return;
-    }
-    
-    const as = LS.annotationStore;
-    const annotation = as.createAnnotation();
-    as.selectAnnotation(annotation.id);
-
-    if (annotation) {
-      snapshotDisposer = onSnapshot(annotation, () => {
-        if (!isCleanedUp) {
-          annotationResultRef.current = annotation.serializeAnnotation();
-        }
-      });
-    }
-  };
-  setTimeout(initAnnotation, 100);
-}
-```
-
-**改进点**:
-- ✅ 在初始化前检查组件是否已清理
-- ✅ 在 snapshot 回调中检查清理状态
-- ✅ 避免访问已销毁的状态树
-
-## 测试场景
-
-修复后,以下场景都能正常工作:
-
-1. ✅ 保存标注结果(正确格式)
-2. ✅ 标注完一个任务后进入另一个任务
-3. ✅ 快速切换任务
-4. ✅ 从标注页面返回任务列表
-5. ✅ 刷新页面后重新加载编辑器
-
-## 调试日志
-
-添加了详细的控制台日志:
-
-```typescript
-console.log('Loading LabelStudio editor for task:', currentTask.id);
-console.log('Initializing LabelStudio instance...');
-console.log('LabelStudio instance initialized');
-console.log('Cleaning up LabelStudio editor...');
-console.log('LabelStudio editor cleaned up');
-console.log('Annotation result:', annotationResult);
-console.log('Sending annotation data:', resultData);
-```
-
-这些日志可以帮助调试编辑器加载和清理过程。
-
-## 相关文件
-
-- `web/apps/lq_label/src/views/annotation-view/annotation-view.tsx`
-
-## 后续优化建议
-
-1. **添加加载状态指示器**:在编辑器初始化时显示进度
-2. **添加保存成功提示**:使用 Toast 显示保存成功消息
-3. **优化清理时机**:考虑使用 `useLayoutEffect` 进行同步清理
-4. **添加错误重试机制**:保存失败时允许用户重试
-
-## 总结
-
-通过改进数据提取逻辑和编辑器清理流程,现在系统能够:
-- ✅ 正确保存标注结果到后端
-- ✅ 在任务切换时正确清理编辑器
-- ✅ 避免 React 渲染冲突
-- ✅ 避免访问已销毁的 MobX 状态树
-- ✅ 提供详细的调试日志

+ 0 - 157
web/apps/lq_label/CSS_PREFIX_FIX.md

@@ -1,157 +0,0 @@
-# LabelStudio CSS 前缀问题修复
-
-## 问题描述
-
-LabelStudio 编辑器加载后,所有样式都没有生效:
-- 标签按钮没有颜色
-- 按钮没有边框和背景
-- 面板和侧边栏样式缺失
-- 整体看起来像是没有加载 CSS
-
-## 问题根源
-
-LabelStudio 使用 **CSS 模块** 和 **BEM 命名约定**,所有的 CSS 类名都需要添加 `lsf-` 前缀。
-
-### 技术细节
-
-1. **CSS 模块处理**:
-   - LabelStudio 的 SCSS 文件使用 CSS 模块
-   - 例如:`.label` 类名需要被转换为 `.lsf-label`
-
-2. **Webpack 配置**:
-   - 根目录的 `web/webpack.config.js` 有特殊的 CSS 模块配置
-   - 它会给所有非 `.module.scss` 的 SCSS 文件添加 `lsf-` 前缀
-
-3. **我们的问题**:
-   - `lq_label` 应用的 webpack 配置没有这个处理
-   - 导致 LabelStudio 的样式类名没有前缀
-   - 浏览器中的 HTML 元素有 `lsf-label` 类名
-   - 但 CSS 文件中只有 `.label` 选择器
-   - 两者不匹配,样式无法应用
-
-## 解决方案
-
-在 `web/apps/lq_label/webpack.config.js` 中添加 CSS 模块配置,模仿根目录的配置:
-
-```javascript
-const css_prefix = 'lsf-';
-
-// 在 plugins 中添加
-new webpack.DefinePlugin({
-  'process.env.CSS_PREFIX': JSON.stringify(css_prefix),
-}),
-
-// 配置 CSS 模块
-config.module.rules.forEach((rule) => {
-  const testString = rule.test?.toString() || '';
-  const isScss = testString.includes('scss');
-  const isCssModule = testString.includes('.module');
-
-  if (rule.test?.toString().match(/scss|sass/) && !isCssModule) {
-    const r = rule.oneOf?.filter((r) => {
-      if (!r.use) return false;
-      const testString = r.test?.toString() || '';
-      if (testString.match(/module|raw|antd/)) return false;
-      return testString.match(/scss|sass/) && r.use.some((u) => u.loader && u.loader.includes('css-loader'));
-    });
-
-    r?.forEach((_r) => {
-      const cssLoader = _r.use.find((use) => use.loader && use.loader.includes('css-loader'));
-      if (!cssLoader) return;
-
-      const isSASS = _r.use.some((use) => use.loader && use.loader.match(/sass|scss/));
-      if (isSASS) _r.exclude = /node_modules/;
-
-      if (cssLoader.options) {
-        cssLoader.options.modules = {
-          localIdentName: `${css_prefix}[local]`, // 添加 lsf- 前缀
-          getLocalIdent(_ctx, _ident, className) {
-            if (className.includes('ant')) return className;
-          },
-        };
-      }
-    });
-  }
-});
-```
-
-## 关键点
-
-1. **CSS_PREFIX 环境变量**:
-   - 定义为 `'lsf-'`
-   - LabelStudio 代码中会使用这个变量
-
-2. **localIdentName 配置**:
-   - 格式:`lsf-[local]`
-   - 将 `.label` 转换为 `.lsf-label`
-
-3. **排除规则**:
-   - 不处理 `.module.scss` 文件(已经有自己的命名)
-   - 不处理 `node_modules`
-   - 保留 `ant` 开头的类名(Ant Design 组件)
-
-## 验证方法
-
-### 1. 检查 HTML 元素
-打开浏览器开发者工具,检查一个标签按钮:
-```html
-<div class="lsf-label lsf-label_selected">Positive</div>
-```
-
-### 2. 检查 CSS 文件
-在 Network 标签中找到 `libs_editor_src_index_js.css`,搜索:
-```css
-.lsf-label {
-  /* 样式定义 */
-}
-```
-
-### 3. 验证样式应用
-- 标签按钮应该有颜色和边框
-- 悬停时应该有效果
-- 选中时应该有不同的样式
-
-## 重启服务器
-
-**重要**:修改 webpack 配置后,必须重启开发服务器:
-
-```bash
-# 停止当前服务器 (Ctrl+C)
-# 重新启动
-cd web
-yarn nx serve lq_label
-```
-
-## 参考
-
-- 根目录配置:`web/webpack.config.js`(第 18-110 行)
-- LabelStudio 编辑器:`web/libs/editor/`
-- CSS 模块文档:https://github.com/css-modules/css-modules
-
-## 相关问题
-
-如果样式还是不显示,检查:
-
-1. **浏览器缓存**:
-   - 硬刷新(Ctrl+Shift+R)
-   - 清除缓存
-
-2. **CSS 文件加载**:
-   - 检查 Network 标签
-   - 确认 `libs_editor_src_index_js.css` 已加载
-   - 检查文件大小(应该是几 MB)
-
-3. **类名匹配**:
-   - HTML 元素的类名应该以 `lsf-` 开头
-   - CSS 文件中的选择器也应该以 `lsf-` 开头
-
-4. **Webpack 配置**:
-   - 确认 `css_prefix` 变量定义正确
-   - 确认 CSS 模块配置已添加
-   - 确认服务器已重启
-
-## 总结
-
-这个问题的核心是 **CSS 模块的类名转换**。LabelStudio 依赖特定的 webpack 配置来正确处理 CSS 类名,我们需要在自己的应用中复制这个配置。
-
-修复后,LabelStudio 编辑器应该完全正常工作,所有样式都会正确显示!

+ 0 - 131
web/apps/lq_label/EDITOR_TEST.md

@@ -1,131 +0,0 @@
-# LabelStudio 编辑器测试页面
-
-## 概述
-
-创建了一个独立的测试页面 `/editor-test`,用于验证 LabelStudio 编辑器的集成和样式。
-
-## 访问方式
-
-1. 启动开发服务器:
-   ```bash
-   cd web
-   yarn nx serve lq_label
-   ```
-
-2. 访问测试页面:
-   - 主页:http://localhost:4200/
-   - 点击 "🧪 编辑器测试" 按钮
-   - 或直接访问:http://localhost:4200/editor-test
-
-## 测试页面功能
-
-### 左侧:编辑器区域
-- 显示 LabelStudio 编辑器
-- 使用简单的情感分析配置
-- 测试文本:"这是一个测试文本,用于验证 LabelStudio 编辑器是否正常工作。"
-
-### 右侧:调试面板
-- **编辑器状态**:显示编辑器是否成功加载
-- **错误信息**:显示任何加载或初始化错误
-- **标注结果**:实时显示标注数据的 JSON 格式
-
-## 测试配置
-
-```xml
-<View>
-  <Text name="text" value="$text"/>
-  <Choices name="sentiment" toName="text" choice="single">
-    <Choice value="Positive"/>
-    <Choice value="Negative"/>
-    <Choice value="Neutral"/>
-  </Choices>
-</View>
-```
-
-## 验证要点
-
-### ✅ 样式检查
-- [ ] 标签按钮有正确的颜色和样式
-- [ ] 按钮有正确的悬停效果
-- [ ] 侧边栏和面板正常显示
-- [ ] 没有橙色到粉色的渐变遮罩
-- [ ] 文本清晰可读
-
-### ✅ 功能检查
-- [ ] 可以点击选择情感标签
-- [ ] 标注结果实时更新在右侧面板
-- [ ] 编辑器交互流畅
-- [ ] 控制按钮(提交、更新等)正常工作
-
-### ✅ 控制台检查
-打开浏览器开发者工具(F12),查看:
-- [ ] 没有 CSS 加载错误
-- [ ] 没有 JavaScript 错误
-- [ ] `libs_editor_src_index_js.css` 文件已加载
-- [ ] 控制台输出显示编辑器初始化成功
-
-## Editor 库独立启动
-
-Editor 库可以独立启动(但需要完整的 Label Studio 后端):
-
-```bash
-cd web
-yarn nx serve editor
-```
-
-这会在 http://localhost:8010/ 启动 editor,但它需要:
-- Label Studio 后端服务器运行在 http://localhost:8080/
-- 完整的 Label Studio 项目结构
-
-**注意**:我们的测试页面不需要这个独立服务器,它直接在我们的应用中集成编辑器。
-
-## 样式加载策略
-
-### 当前实现(推荐)
-```typescript
-// main.tsx
-import '../../../libs/ui/src/styles.scss';
-import '../../../libs/ui/src/tailwind.css';
-// Editor 样式会在动态导入 @humansignal/editor 时自动加载
-```
-
-### 关键点
-1. **不要**静态导入 `editor/src/assets/styles/global.scss`
-2. **依赖**动态导入时的自动样式加载
-3. **参考** playground 和 labelstudio 应用的实现方式
-
-## 文件结构
-
-```
-web/apps/lq_label/src/views/editor-test/
-├── editor-test.tsx           # 测试页面组件
-├── editor-test.module.scss   # 样式文件
-└── index.ts                   # 导出文件
-```
-
-## 故障排除
-
-### 问题:样式不显示
-**解决方案**:
-1. 检查 `main.tsx` 是否只导入了 UI 库样式
-2. 确认没有静态导入 editor 的全局样式
-3. 清除浏览器缓存并刷新
-
-### 问题:编辑器无法加载
-**解决方案**:
-1. 检查控制台是否有错误
-2. 确认 `@humansignal/editor` 包已正确安装
-3. 检查 webpack 配置是否正确
-
-### 问题:出现橙色渐变遮罩
-**解决方案**:
-1. 这通常是 Tailwind 的 preflight 样式冲突
-2. 确认 tailwind.config.js 配置正确
-3. 检查是否有自定义样式覆盖了编辑器样式
-
-## 下一步
-
-如果测试页面工作正常,说明编辑器集成没有问题,可以:
-1. 将相同的实现应用到 AnnotationView
-2. 添加更多的编辑器配置选项
-3. 实现完整的标注保存功能

+ 0 - 187
web/apps/lq_label/FORM_VALIDATION_SUMMARY.md

@@ -1,187 +0,0 @@
-# 表单验证实现总结
-
-## 任务 19 完成 ✅
-
-本文档总结了标注平台中表单验证功能的实现。
-
-## 实现的功能
-
-### 1. ProjectForm 增强验证
-
-#### 验证规则
-- **项目名称**:
-  - ✅ 不能为空或仅包含空格(Requirements 1.4)
-  - ✅ 最少 2 个字符
-  - ✅ 最多 100 个字符
-  
-- **项目描述**:
-  - ✅ 不能为空或仅包含空格
-  - ✅ 最少 5 个字符
-  - ✅ 最多 500 个字符
-  
-- **标注配置**:
-  - ✅ 不能为空或仅包含空格
-  - ✅ 必须是有效的 XML 格式
-  - ✅ 必须以 `<` 开头,以 `>` 结尾
-  - ✅ 必须包含闭合标签
-
-#### 用户体验改进
-- ✅ 实时验证反馈(onBlur 事件)
-- ✅ 错误状态的视觉反馈(红色边框)
-- ✅ 清晰的错误提示信息
-- ✅ 输入时自动清除错误
-- ✅ 字段长度限制(maxLength)
-
-### 2. TaskForm 增强验证
-
-#### 验证规则
-- **任务名称**:
-  - ✅ 不能为空或仅包含空格
-  - ✅ 最少 2 个字符
-  - ✅ 最多 100 个字符
-  
-- **项目 ID**:
-  - ✅ 不能为空或仅包含空格
-  - ✅ 从项目详情页创建时自动填充且禁用编辑
-  
-- **任务数据 (JSON)**:
-  - ✅ 不能为空
-  - ✅ 必须是有效的 JSON 格式
-  - ✅ 必须是对象类型(不能是数组)
-  - ✅ 不能是空对象(至少包含一个字段)
-  - ✅ 详细的 JSON 解析错误提示
-
-#### 用户体验改进
-- ✅ 实时验证反馈(onBlur 事件)
-- ✅ 错误状态的视觉反馈(红色边框)
-- ✅ 清晰的错误提示信息
-- ✅ 输入时自动清除错误
-- ✅ 字段长度限制(maxLength)
-- ✅ JSON 格式化提示
-
-## 技术实现
-
-### 验证时机
-1. **提交时验证**:表单提交时进行完整验证
-2. **失焦验证**:字段失去焦点时进行单字段验证
-3. **输入时清除**:用户输入时自动清除该字段的错误
-
-### 错误状态管理
-```typescript
-const [formErrors, setFormErrors] = useState<Record<string, string>>({});
-```
-
-### 视觉反馈
-- 错误字段显示红色边框(`border-error-border`)
-- 错误字段的焦点环显示红色(`focus:ring-error-border`)
-- 错误消息显示在字段下方(`text-error-foreground`)
-
-### 无障碍性
-- ✅ 使用 `aria-invalid` 标记无效字段
-- ✅ 使用 `aria-describedby` 关联错误消息
-- ✅ 必填字段标记 `*` 符号
-- ✅ 语义化的 label 和 input 关联
-
-## 验证示例
-
-### ProjectForm 验证示例
-
-```typescript
-// 空白字符串验证(Requirements 1.4)
-if (!formData.name || !formData.name.trim()) {
-  errors.name = '项目名称不能为空或仅包含空格';
-}
-
-// 长度验证
-if (formData.name.trim().length < 2) {
-  errors.name = '项目名称至少需要 2 个字符';
-}
-
-// XML 基本验证
-if (!trimmedConfig.startsWith('<') || !trimmedConfig.endsWith('>')) {
-  errors.config = '标注配置必须是有效的 XML 格式(以 < 开头,以 > 结尾)';
-}
-```
-
-### TaskForm 验证示例
-
-```typescript
-// JSON 验证
-try {
-  const parsedData = JSON.parse(dataJson);
-  if (typeof parsedData !== 'object' || parsedData === null) {
-    errors.data = '任务数据必须是有效的 JSON 对象';
-  } else if (Array.isArray(parsedData)) {
-    errors.data = '任务数据必须是 JSON 对象,不能是数组';
-  } else if (Object.keys(parsedData).length === 0) {
-    errors.data = '任务数据不能为空对象,至少需要包含一个字段';
-  }
-} catch (e) {
-  errors.data = `JSON 格式错误:${(e as Error).message}`;
-}
-```
-
-## 测试建议
-
-虽然任务 19.1(编写单元测试)是可选的,但建议测试以下场景:
-
-### ProjectForm 测试场景
-1. ✅ 空字符串提交被阻止
-2. ✅ 仅包含空格的字符串被阻止
-3. ✅ 名称长度验证(< 2 字符,> 100 字符)
-4. ✅ 描述长度验证(< 5 字符,> 500 字符)
-5. ✅ 无效 XML 格式被阻止
-6. ✅ 有效数据成功提交
-
-### TaskForm 测试场景
-1. ✅ 空字符串提交被阻止
-2. ✅ 仅包含空格的字符串被阻止
-3. ✅ 名称长度验证(< 2 字符,> 100 字符)
-4. ✅ 无效 JSON 格式被阻止
-5. ✅ JSON 数组被阻止
-6. ✅ 空 JSON 对象被阻止
-7. ✅ 有效数据成功提交
-
-## 符合的需求
-
-- ✅ **Requirements 1.4**:Empty project name rejection(空项目名称拒绝)
-- ✅ **Requirements 10.2**:Form validation(表单验证)
-
-## 文件修改
-
-### 修改的文件
-1. `web/apps/lq_label/src/components/project-form/project-form.tsx`
-   - 增强了 `validateForm()` 函数
-   - 添加了 `handleFieldBlur()` 函数
-   - 更新了输入字段的样式和事件处理
-   
-2. `web/apps/lq_label/src/components/task-form/task-form.tsx`
-   - 增强了 `validateForm()` 函数
-   - 添加了 `handleFieldBlur()` 函数
-   - 更新了输入字段的样式和事件处理
-
-## 下一步建议
-
-1. **任务 20**:样式优化和响应式设计
-   - 优化整体视觉效果
-   - 确保在不同屏幕尺寸下正常工作
-   
-2. **任务 21**:最终集成测试
-   - 测试完整的用户流程
-   - 测试错误场景和边缘情况
-
-3. **可选**:编写单元测试(任务 19.1)
-   - 使用 React Testing Library
-   - 测试表单验证规则
-   - 测试用户交互
-
-## 总结
-
-表单验证功能已经完全实现,提供了:
-- ✅ 严格的验证规则
-- ✅ 实时反馈
-- ✅ 清晰的错误提示
-- ✅ 良好的用户体验
-- ✅ 无障碍性支持
-
-所有验证规则都符合需求规范,特别是 Requirements 1.4(空项目名称拒绝)和 Requirements 10.2(表单验证)。

+ 0 - 97
web/apps/lq_label/ICON_FIX.md

@@ -1,97 +0,0 @@
-# 图标导入错误修复
-
-## 问题描述
-
-```
-Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.
-Check the render method of `HomeView`.
-```
-
-## 问题原因
-
-在优化 HomeView 和 Sidebar 时,使用了不存在的图标:
-- `IconClipboardList` - 不存在
-- `IconTag` - 不存在
-
-## 解决方案
-
-查看 `web/libs/ui/src/assets/icons/index.ts` 找到正确的图标名称:
-
-### 修复前
-```typescript
-import { IconFolder, IconClipboardList, IconTag } from '@humansignal/ui';
-```
-
-### 修复后
-```typescript
-import { IconFolder, IconClipboardCheck, IconAnnotation } from '@humansignal/ui';
-```
-
-## 图标映射
-
-| 用途 | 错误的图标 | 正确的图标 |
-|------|-----------|-----------|
-| 任务管理 | `IconClipboardList` | `IconClipboardCheck` |
-| 我的标注 | `IconTag` | `IconAnnotation` |
-| 项目管理 | `IconFolder` | `IconFolder` ✅ |
-
-## 修改的文件
-
-1. `web/apps/lq_label/src/views/home-view.tsx`
-   - 更新导入语句
-   - 更新 features 数组中的图标
-
-2. `web/apps/lq_label/src/components/layout/sidebar.tsx`
-   - 更新导入语句
-   - 更新 menuItems 数组中的图标
-
-## 如何查找可用图标
-
-1. 查看图标索引文件:
-   ```
-   web/libs/ui/src/assets/icons/index.ts
-   ```
-
-2. 搜索相关关键词:
-   ```bash
-   # 搜索包含 "clipboard" 的图标
-   grep -i "clipboard" web/libs/ui/src/assets/icons/index.ts
-   
-   # 搜索包含 "annotation" 的图标
-   grep -i "annotation" web/libs/ui/src/assets/icons/index.ts
-   ```
-
-3. 常用图标列表:
-   - `IconFolder` - 文件夹
-   - `IconClipboardCheck` - 剪贴板/任务
-   - `IconAnnotation` - 标注
-   - `IconHome` - 首页
-   - `IconMenu` - 菜单
-   - `IconX` / `IconClose` - 关闭
-   - `IconCheck` - 勾选
-   - `IconTrash` - 删除
-   - `IconEdit` / `IconPencil` - 编辑
-   - `IconPlus` - 添加
-   - `IconSearch` - 搜索
-   - `IconSettings` / `IconGear` - 设置
-
-## 验证方法
-
-1. 检查编译错误:
-   ```bash
-   yarn nx serve lq_label
-   ```
-
-2. 检查浏览器控制台是否有错误
-
-3. 确认图标正确显示在页面上
-
-## 总结
-
-修复后,所有图标都能正确导入和显示:
-- ✅ HomeView 的 Features 卡片图标
-- ✅ Sidebar 的菜单项图标
-- ✅ 移动端菜单按钮图标
-- ✅ NotFoundView 的首页图标
-
-记住:在使用 @humansignal/ui 的图标前,先检查 `icons/index.ts` 确认图标是否存在!

+ 0 - 238
web/apps/lq_label/LABELSTUDIO_INTEGRATION_SUMMARY.md

@@ -1,238 +0,0 @@
-# LabelStudio 编辑器集成完整总结
-
-## 项目背景
-
-在标注平台项目中集成 LabelStudio 编辑器,用于实现数据标注功能。
-
-## 遇到的问题
-
-### 问题 1:橙色到粉色的渐变遮罩
-**症状**:整个标注界面被一个橙色渐变覆盖,无法交互
-
-**根本原因**:
-- `relations-overlay` 是 LabelStudio 的关系标注覆盖层(SVG 元素)
-- 默认有一个背景色,导致遮挡了整个界面
-
-**解决方案**:
-在组件样式中添加透明背景:
-```scss
-:global(.relations-overlay) {
-  background: transparent !important;
-  fill: none !important;
-}
-
-:global(.lsf-container) {
-  background: transparent !important;
-}
-```
-
-**修改的文件**:
-- `web/apps/lq_label/src/views/editor-test/editor-test.module.scss`
-- `web/apps/lq_label/src/views/annotation-view/annotation-view.module.scss`
-
-### 问题 2:编辑器样式完全不显示(核心问题)
-**症状**:
-- 标签按钮没有颜色
-- 按钮没有边框和背景
-- 面板和侧边栏样式缺失
-- 整体看起来像是没有加载 CSS
-
-**根本原因**:
-LabelStudio 使用 CSS 模块和 BEM 命名约定,所有 CSS 类名需要添加 `lsf-` 前缀:
-- HTML 元素:`<div class="lsf-label">...</div>`
-- CSS 文件:`.label { ... }` → 需要转换为 `.lsf-label { ... }`
-- 我们的 webpack 配置缺少这个转换逻辑
-
-**技术细节**:
-1. 根目录的 `web/webpack.config.js` 有特殊的 CSS 模块配置
-2. 它会给所有非 `.module.scss` 的 SCSS 文件添加 `lsf-` 前缀
-3. 配置代码:
-```javascript
-cssLoader.options.modules = {
-  localIdentName: `${css_prefix}[local]`, // css_prefix = "lsf-"
-  getLocalIdent(_ctx, _ident, className) {
-    if (className.includes("ant")) return className;
-  },
-};
-```
-
-**解决方案**:
-在 `web/apps/lq_label/webpack.config.js` 中添加相同的配置:
-
-```javascript
-const css_prefix = 'lsf-';
-
-// 1. 定义 CSS_PREFIX 环境变量
-config.plugins = [
-  ...config.plugins,
-  new webpack.DefinePlugin({
-    'process.env.CSS_PREFIX': JSON.stringify(css_prefix),
-  }),
-];
-
-// 2. 配置 CSS 模块添加 lsf- 前缀
-config.module.rules.forEach((rule) => {
-  const testString = rule.test?.toString() || '';
-  
-  if (rule.test?.toString().match(/scss|sass/) && !testString.includes('.module')) {
-    const r = rule.oneOf?.filter((r) => {
-      if (!r.use) return false;
-      const testString = r.test?.toString() || '';
-      if (testString.match(/module|raw|antd/)) return false;
-      return testString.match(/scss|sass/) && 
-             r.use.some((u) => u.loader && u.loader.includes('css-loader'));
-    });
-
-    r?.forEach((_r) => {
-      const cssLoader = _r.use.find((use) => 
-        use.loader && use.loader.includes('css-loader')
-      );
-      
-      if (!cssLoader) return;
-
-      const isSASS = _r.use.some((use) => 
-        use.loader && use.loader.match(/sass|scss/)
-      );
-      
-      if (isSASS) _r.exclude = /node_modules/;
-
-      if (cssLoader.options) {
-        cssLoader.options.modules = {
-          localIdentName: `${css_prefix}[local]`,
-          getLocalIdent(_ctx, _ident, className) {
-            if (className.includes('ant')) return className;
-          },
-        };
-      }
-    });
-  }
-});
-```
-
-**修改的文件**:
-- `web/apps/lq_label/webpack.config.js`
-
-**重要提示**:
-- 修改 webpack 配置后必须重启开发服务器
-- 浏览器需要硬刷新(Ctrl+Shift+R)
-
-## 调试过程
-
-### 1. 初步探索
-- 参考了 `playground` 和 `labelstudio` 应用的实现
-- 发现它们只导入 UI 库样式,不静态导入 editor 样式
-- 修改了 `main.tsx` 的样式导入顺序
-
-### 2. 发现橙色遮罩
-- 使用浏览器开发者工具检查元素
-- 找到了 `relations-overlay` 和 `lsf-container` 类名
-- 搜索源代码找到对应的组件
-- 添加透明背景修复
-
-### 3. 样式不显示的深入调查
-- 检查 Network 标签,确认 CSS 文件已加载
-- 检查 CSS 文件内容,发现没有 `lsf-` 前缀的类名
-- 检查 HTML 元素,发现有 `lsf-` 前缀的类名
-- 意识到是 CSS 模块转换的问题
-
-### 4. 找到根本原因
-- 阅读根目录的 `webpack.config.js`
-- 发现 CSS 模块的 `localIdentName` 配置
-- 理解了 `lsf-` 前缀的转换逻辑
-- 在我们的应用中复制了这个配置
-
-### 5. 验证修复
-- 重启开发服务器
-- 硬刷新浏览器
-- 检查 HTML 和 CSS 的类名匹配
-- 确认样式正确显示
-
-## 创建的文件
-
-### 测试页面
-- `web/apps/lq_label/src/views/editor-test/editor-test.tsx` - 测试页面组件
-- `web/apps/lq_label/src/views/editor-test/editor-test.module.scss` - 样式文件
-- `web/apps/lq_label/src/views/editor-test/index.ts` - 导出文件
-
-### 文档
-- `web/apps/lq_label/EDITOR_TEST.md` - 测试页面说明
-- `web/apps/lq_label/RELATIONS_OVERLAY_FIX.md` - 橙色遮罩修复文档
-- `web/apps/lq_label/CSS_PREFIX_FIX.md` - CSS 前缀问题修复文档
-- `web/apps/lq_label/LABELSTUDIO_INTEGRATION_SUMMARY.md` - 本文档
-
-### 规范
-- `.kiro/steering/labelstudio-集成规范.md` - 完整的集成规范
-- `.kiro/steering/README.md` - 规范索引
-
-## 关键经验
-
-### 1. 参考成功案例
-- 不要从零开始,先看看其他应用是怎么做的
-- `playground` 应用是最好的参考
-- 根目录的 `webpack.config.js` 包含关键配置
-
-### 2. 使用浏览器开发者工具
-- Elements 标签:检查 HTML 元素和类名
-- Network 标签:检查 CSS 文件是否加载
-- Console 标签:查看错误和日志
-- Styles 面板:检查样式是否被应用
-
-### 3. 理解 CSS 模块
-- CSS 模块会转换类名
-- `localIdentName` 配置决定转换规则
-- LabelStudio 依赖特定的前缀
-
-### 4. 搜索源代码
-- 使用类名搜索找到对应的组件
-- 理解组件的用途和实现
-- 找到样式文件和配置
-
-### 5. 创建测试页面
-- 隔离问题,简化调试
-- 添加调试面板显示状态
-- 使用简单的配置和数据
-
-## 最终效果
-
-✅ 橙色遮罩消失
-✅ 所有样式正确显示
-✅ 标签按钮有颜色和边框
-✅ 悬停和选中效果正常
-✅ 侧边栏和面板样式正确
-✅ 编辑器完全可用
-
-## 访问测试页面
-
-1. 启动开发服务器:
-```bash
-cd web
-yarn nx serve lq_label
-```
-
-2. 访问测试页面:
-- 首页:http://localhost:4200/
-- 点击 "🧪 编辑器测试" 按钮
-- 或直接访问:http://localhost:4200/editor-test
-
-3. 访问标注页面:
-- 创建项目和任务
-- 访问:http://localhost:4200/tasks/:id/annotate
-
-## 后续工作
-
-- [ ] 完善标注保存功能
-- [ ] 添加更多的编辑器配置选项
-- [ ] 实现标注历史记录
-- [ ] 添加标注质量检查
-- [ ] 优化编辑器性能
-
-## 致谢
-
-感谢 LabelStudio 团队提供的优秀编辑器!
-感谢 playground 和 labelstudio 应用提供的参考实现!
-
----
-
-**文档创建时间**:2026-01-13
-**最后更新时间**:2026-01-13
-**维护者**:项目开发团队

+ 0 - 108
web/apps/lq_label/RELATIONS_OVERLAY_FIX.md

@@ -1,108 +0,0 @@
-# Relations Overlay 橙色遮罩问题修复
-
-## 问题描述
-
-在 LabelStudio 编辑器中出现了一个橙色到粉色的渐变遮罩,覆盖了整个标注界面。
-
-## 问题根源
-
-这个遮罩是 LabelStudio 的 `relations-overlay` 组件,它是一个 SVG 覆盖层,用于显示关系标注的连接线。
-
-**相关文件**:
-- `web/libs/editor/src/components/InteractiveOverlays/RelationsOverlay.jsx`
-- `web/libs/editor/src/components/InteractiveOverlays/RelationsOverlay.module.scss`
-
-**问题原因**:
-- 这个 SVG 元素有一个默认的背景色或填充色
-- 样式没有正确加载,导致显示了不应该显示的背景
-
-## 解决方案
-
-在组件的样式文件中添加以下 CSS 规则,强制将 `relations-overlay` 和 `lsf-container` 的背景设置为透明:
-
-```scss
-// Fix for relations-overlay background (orange gradient issue)
-:global(.relations-overlay) {
-  background: transparent !important;
-  fill: none !important;
-}
-
-:global(.lsf-container) {
-  background: transparent !important;
-}
-```
-
-## 修改的文件
-
-1. **测试页面样式**:
-   - `web/apps/lq_label/src/views/editor-test/editor-test.module.scss`
-
-2. **标注页面样式**:
-   - `web/apps/lq_label/src/views/annotation-view/annotation-view.module.scss`
-
-## 验证方法
-
-1. 访问测试页面:http://localhost:4200/editor-test
-2. 检查是否还有橙色渐变遮罩
-3. 使用浏览器开发者工具(F12)检查 `.relations-overlay` 元素
-4. 确认该元素的 `background` 和 `fill` 属性为 `transparent` 或 `none`
-
-## 技术细节
-
-### RelationsOverlay 组件
-
-这是 LabelStudio 用于显示关系标注的组件:
-
-```jsx
-const containerStyles = ["relations-overlay", styles.container];
-
-return (
-  <svg
-    className={containerStyles.join(" ")}
-    ref={this.rootNode}
-    xmlns="http://www.w3.org/2000/svg"
-    style={style}
-  >
-    {/* SVG content */}
-  </svg>
-);
-```
-
-### 样式定位
-
-- 使用 `:global()` 选择器来覆盖全局样式
-- 使用 `!important` 确保样式优先级最高
-- 同时设置 `background` 和 `fill` 属性以确保完全透明
-
-## 相关问题
-
-如果将来遇到类似的样式问题:
-
-1. **使用浏览器开发者工具**:
-   - 右键点击问题区域 → "检查元素"
-   - 查看元素的类名和样式
-   - 检查哪些样式被应用,哪些被覆盖
-
-2. **搜索源代码**:
-   - 使用类名在代码库中搜索
-   - 找到对应的组件和样式文件
-   - 理解组件的用途和样式结构
-
-3. **添加覆盖样式**:
-   - 在组件的 `.module.scss` 文件中添加全局样式覆盖
-   - 使用 `:global()` 选择器
-   - 必要时使用 `!important` 提高优先级
-
-## 其他 LabelStudio 样式问题
-
-如果遇到其他 LabelStudio 样式问题,可以参考以下常见类名:
-
-- `.lsf-wrapper` - 编辑器主容器
-- `.lsf-editor` - 编辑器内容区域
-- `.lsf-tabs-panel__body` - 标签面板主体
-- `.lsf-panel-tabs__tab` - 面板标签
-- `.lsf-sidepanels` - 侧边栏面板
-- `.relations-overlay` - 关系覆盖层(本次修复的问题)
-- `.lsf-container` - 容器元素
-
-所有这些类名都可以在 `web/libs/editor` 中找到对应的组件和样式定义。

+ 0 - 268
web/apps/lq_label/STYLE_OPTIMIZATION_SUMMARY.md

@@ -1,268 +0,0 @@
-# 样式优化和响应式设计总结
-
-## 任务 20 完成 ✅
-
-本文档总结了标注平台的样式优化和响应式设计改进。
-
-## 优化的组件
-
-### 1. HomeView(首页)
-
-#### 优化前
-- 简单的标题和描述
-- 单一的"开始使用"按钮
-- 缺少视觉吸引力
-
-#### 优化后
-- ✅ **Hero Section**:大标题、详细描述、双按钮布局
-- ✅ **Features Grid**:3列卡片展示核心功能
-  - 项目管理
-  - 任务管理
-  - 我的标注
-- ✅ **图标集成**:使用 @humansignal/ui 的图标
-- ✅ **悬停效果**:卡片悬停时边框和阴影变化
-- ✅ **Quick Stats**:展示平台特点(快速、灵活、可靠)
-- ✅ **响应式设计**:
-  - 移动端:1列布局
-  - 平板/桌面:3列布局
-
-#### 新增功能
-```typescript
-const features = [
-  {
-    icon: <IconFolder className="size-8" />,
-    title: '项目管理',
-    description: '创建和管理标注项目,配置标注规则和工作流程',
-    link: '/projects',
-  },
-  // ...
-];
-```
-
-### 2. Sidebar(侧边栏)
-
-#### 优化前
-- 静态侧边栏
-- 无图标
-- 移动端体验差
-
-#### 优化后
-- ✅ **图标集成**:每个菜单项都有对应图标
-  - 项目管理:IconFolder
-  - 任务管理:IconClipboardList
-  - 我的标注:IconTag
-- ✅ **移动端菜单**:
-  - 汉堡菜单按钮(lg 以下显示)
-  - 滑动抽屉效果
-  - 遮罩层点击关闭
-  - 平滑过渡动画
-- ✅ **改进的 Logo 区域**:
-  - 可点击返回首页
-  - 添加英文副标题
-  - 悬停效果
-- ✅ **增强的视觉反馈**:
-  - 活动菜单项有阴影
-  - 更好的悬停效果
-- ✅ **改进的 Footer**:
-  - 版本信息
-  - 版权信息
-
-#### 响应式设计
-```typescript
-// 移动端:固定定位 + 滑动抽屉
-className={`
-  fixed lg:static inset-y-0 left-0 z-40
-  transform transition-transform duration-300
-  ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
-`}
-```
-
-### 3. Layout(布局)
-
-#### 优化前
-- 固定的 padding
-- 无响应式间距
-
-#### 优化后
-- ✅ **响应式 Padding**:
-  - 移动端:`p-comfortable`
-  - 桌面端:`p-spacious`
-- ✅ **改进的滚动**:
-  - 主容器 `overflow-hidden`
-  - 内容区域 `overflow-auto`
-- ✅ **最大宽度限制**:`max-w-7xl mx-auto`
-
-### 4. NotFoundView(404 页面)
-
-#### 优化前
-- 简单的 404 文本
-- 单一返回按钮
-
-#### 优化后
-- ✅ **大号 404 数字**:视觉冲击力
-- ✅ **友好的错误消息**:详细说明
-- ✅ **多个操作按钮**:
-  - 返回首页(带图标)
-  - 查看项目
-- ✅ **居中布局**:更好的视觉平衡
-
-## 使用的 Tailwind 类名
-
-### 语义化 Token 类名
-- ✅ `bg-primary-background` - 主背景色
-- ✅ `bg-secondary-background` - 次要背景色
-- ✅ `text-primary-foreground` - 主文本色
-- ✅ `text-secondary-foreground` - 次要文本色
-- ✅ `text-muted-foreground` - 弱化文本色
-- ✅ `border-neutral-border` - 中性边框色
-- ✅ `border-primary-border` - 主边框色
-- ✅ `bg-hover` - 悬停背景色
-
-### 间距 Token
-- ✅ `p-tight` - 紧凑内边距
-- ✅ `p-cozy` - 舒适内边距
-- ✅ `p-comfortable` - 标准内边距
-- ✅ `p-spacious` - 宽松内边距
-- ✅ `p-loose` - 松散内边距
-- ✅ `gap-tight` - 紧凑间距
-- ✅ `gap-cozy` - 舒适间距
-- ✅ `gap-comfortable` - 标准间距
-- ✅ `gap-spacious` - 宽松间距
-
-### 响应式断点
-- ✅ `sm:` - 小屏幕(640px+)
-- ✅ `md:` - 中等屏幕(768px+)
-- ✅ `lg:` - 大屏幕(1024px+)
-
-## 响应式设计特性
-
-### 移动端(< 1024px)
-1. **Sidebar**:
-   - 隐藏在屏幕外
-   - 汉堡菜单按钮显示
-   - 点击打开滑动抽屉
-   - 遮罩层覆盖内容
-
-2. **HomeView**:
-   - Features 卡片:1列布局
-   - Quick Stats:1列布局
-   - 减小间距
-
-3. **Layout**:
-   - 较小的 padding(`p-comfortable`)
-
-### 桌面端(≥ 1024px)
-1. **Sidebar**:
-   - 始终可见
-   - 固定在左侧
-   - 汉堡菜单按钮隐藏
-
-2. **HomeView**:
-   - Features 卡片:3列布局
-   - Quick Stats:3列布局
-   - 更大的间距
-
-3. **Layout**:
-   - 较大的 padding(`p-spacious`)
-
-## 视觉改进
-
-### 交互效果
-- ✅ **悬停状态**:所有可点击元素都有悬停效果
-- ✅ **过渡动画**:平滑的颜色和变换过渡
-- ✅ **阴影效果**:卡片和按钮的阴影增强层次感
-- ✅ **焦点状态**:清晰的焦点指示器
-
-### 排版
-- ✅ **层次分明**:使用不同的字体大小和粗细
-- ✅ **行高优化**:`leading-relaxed` 提高可读性
-- ✅ **对齐方式**:居中和左对齐的合理使用
-
-### 颜色
-- ✅ **一致的配色**:使用语义化 token
-- ✅ **对比度**:确保文本可读性
-- ✅ **状态颜色**:活动、悬停、禁用状态的区分
-
-## 无障碍性改进
-
-- ✅ **语义化 HTML**:使用 `<nav>`, `<aside>`, `<main>` 等
-- ✅ **ARIA 标签**:`aria-label` 用于按钮
-- ✅ **键盘导航**:所有交互元素可通过键盘访问
-- ✅ **焦点管理**:清晰的焦点指示器
-- ✅ **颜色对比**:符合 WCAG 2.1 AA 标准
-
-## 性能优化
-
-- ✅ **CSS 类名优化**:使用 Tailwind 的工具类
-- ✅ **按需加载**:图标按需导入
-- ✅ **过渡性能**:使用 `transform` 而非 `left/right`
-- ✅ **避免重排**:使用 `fixed` 定位
-
-## 修改的文件
-
-1. `web/apps/lq_label/src/views/home-view.tsx`
-   - 添加 Features Grid
-   - 添加 Quick Stats
-   - 改进布局和间距
-
-2. `web/apps/lq_label/src/components/layout/sidebar.tsx`
-   - 添加图标
-   - 实现移动端菜单
-   - 改进视觉效果
-
-3. `web/apps/lq_label/src/components/layout/layout.tsx`
-   - 添加响应式 padding
-   - 改进滚动处理
-
-4. `web/apps/lq_label/src/views/not-found-view.tsx`
-   - 改进 404 页面设计
-   - 添加多个操作按钮
-
-## 符合的需求
-
-- ✅ **Requirements 4.7**:使用 Tailwind CSS 优化组件样式
-- ✅ **Requirements 7.5**:实现响应式设计
-- ✅ **Requirements 7.7**:使用语义化 token 类名
-
-## 测试建议
-
-### 响应式测试
-1. 在不同屏幕尺寸下测试(320px, 768px, 1024px, 1920px)
-2. 测试移动端菜单的打开/关闭
-3. 测试横屏和竖屏模式
-
-### 交互测试
-1. 测试所有悬停效果
-2. 测试键盘导航
-3. 测试焦点状态
-
-### 视觉测试
-1. 检查颜色对比度
-2. 检查文本可读性
-3. 检查布局对齐
-
-## 下一步建议
-
-1. **任务 21**:最终集成测试
-   - 测试完整的用户流程
-   - 测试错误场景和边缘情况
-
-2. **任务 22**:文档和部署准备
-   - 编写 README.md
-   - 添加环境变量配置说明
-
-3. **可选优化**:
-   - 添加深色模式支持
-   - 添加动画效果
-   - 添加骨架屏加载状态
-
-## 总结
-
-样式优化和响应式设计已经完成,提供了:
-- ✅ 美观的用户界面
-- ✅ 完整的响应式支持
-- ✅ 良好的用户体验
-- ✅ 无障碍性支持
-- ✅ 语义化的 Tailwind 类名
-
-所有组件都遵循设计规范,使用语义化 token 类名,并在不同屏幕尺寸下都能正常工作。

+ 0 - 172
web/apps/lq_label/TOAST_ERROR_FIX.md

@@ -1,172 +0,0 @@
-# Toast 错误对象渲染问题修复
-
-## 问题描述
-
-在保存标注结果时出现错误:
-
-```
-Objects are not valid as a React child (found: object with keys {type, loc, msg, input, url})
-```
-
-## 问题原因
-
-FastAPI 的验证错误返回的是一个对象数组,每个对象包含:
-- `type`: 错误类型
-- `loc`: 错误位置(字段路径)
-- `msg`: 错误消息
-- `input`: 输入值
-- `url`: 文档链接
-
-当这个对象直接传递给 Toast 组件时,React 无法渲染对象,导致错误。
-
-## 解决方案
-
-### 1. 增强 API 拦截器错误处理
-
-在 `web/apps/lq_label/src/services/api.ts` 中:
-
-```typescript
-apiClient.interceptors.response.use(
-  (response) => response,
-  (error: AxiosError) => {
-    let errorMessage = '发生了意外错误';
-    
-    if (error.response?.data) {
-      const data = error.response.data as any;
-      
-      // Handle FastAPI validation errors (array of error objects)
-      if (Array.isArray(data.detail)) {
-        // Format validation errors into readable message
-        errorMessage = data.detail
-          .map((err: any) => {
-            const field = err.loc?.join('.') || '字段';
-            return `${field}: ${err.msg}`;
-          })
-          .join('; ');
-      } 
-      // Handle simple string error message
-      else if (typeof data.detail === 'string') {
-        errorMessage = data.detail;
-      }
-      // Handle object error message
-      else if (typeof data.detail === 'object' && data.detail !== null) {
-        errorMessage = JSON.stringify(data.detail);
-      }
-      // Fallback to error message
-      else if (data.message) {
-        errorMessage = data.message;
-      }
-    } else if (error.message) {
-      errorMessage = error.message;
-    }
-
-    toast.error(errorMessage);
-    // ...
-  }
-);
-```
-
-**改进点**:
-- ✅ 检测 FastAPI 验证错误数组
-- ✅ 格式化为可读的字符串(`字段: 错误消息`)
-- ✅ 处理多种错误格式(字符串、对象、数组)
-- ✅ 提供详细的调试日志
-
-### 2. 增强 Toast 服务的类型安全
-
-在 `web/apps/lq_label/src/services/toast.ts` 中:
-
-```typescript
-error(message: string | any, title?: string, duration = 5000): void {
-  // 确保 message 是字符串
-  let messageStr: string;
-  if (typeof message === 'string') {
-    messageStr = message;
-  } else if (message && typeof message === 'object') {
-    // 如果是对象,尝试提取有用信息
-    if (message.message) {
-      messageStr = message.message;
-    } else if (message.msg) {
-      messageStr = message.msg;
-    } else {
-      messageStr = JSON.stringify(message);
-    }
-  } else {
-    messageStr = String(message);
-  }
-
-  this.notify({
-    type: ToastType.error,
-    title: title || '错误',
-    message: messageStr,
-    duration,
-  });
-}
-```
-
-**改进点**:
-- ✅ 接受任意类型的 message 参数
-- ✅ 自动转换对象为字符串
-- ✅ 尝试提取对象中的 `message` 或 `msg` 字段
-- ✅ 最后兜底使用 `JSON.stringify()` 或 `String()`
-
-## 错误格式示例
-
-### FastAPI 验证错误格式
-
-```json
-{
-  "detail": [
-    {
-      "type": "string_type",
-      "loc": ["body", "name"],
-      "msg": "Input should be a valid string",
-      "input": null,
-      "url": "https://errors.pydantic.dev/..."
-    },
-    {
-      "type": "missing",
-      "loc": ["body", "data"],
-      "msg": "Field required",
-      "input": {"name": "test"},
-      "url": "https://errors.pydantic.dev/..."
-    }
-  ]
-}
-```
-
-### 格式化后的错误消息
-
-```
-body.name: Input should be a valid string; body.data: Field required
-```
-
-## 测试场景
-
-修复后,以下场景都能正确显示错误消息:
-
-1. ✅ FastAPI 验证错误(对象数组)
-2. ✅ 简单字符串错误
-3. ✅ 对象错误
-4. ✅ 网络错误
-5. ✅ 超时错误
-
-## 相关文件
-
-- `web/apps/lq_label/src/services/api.ts` - API 拦截器
-- `web/apps/lq_label/src/services/toast.ts` - Toast 服务
-
-## 验证方法
-
-1. 尝试提交空的表单字段
-2. 尝试提交无效的 JSON 数据
-3. 尝试在网络断开时保存数据
-4. 检查 Toast 是否显示可读的错误消息
-
-## 总结
-
-通过增强错误处理逻辑,现在系统能够:
-- ✅ 正确处理 FastAPI 的验证错误
-- ✅ 将错误对象转换为可读的字符串
-- ✅ 在 Toast 中显示友好的错误消息
-- ✅ 避免 React 渲染对象的错误