浏览代码

docs: add comprehensive test guide for todo issue persistence

- Created detailed testing documentation for team leader todo feature with 6 test cases covering creation, database verification, refresh persistence, error handling, concurrency, and data association
- Fixed todo issue persistence by implementing async database save in createTodoFromEvent method with proper error handling and optimistic UI updates
- Enhanced project status tracking by reading stagnation/modification state from data field to
徐福静0235668 15 小时之前
父节点
当前提交
4121344a94

+ 312 - 0
docs/team-leader-todo-test-guide.md

@@ -0,0 +1,312 @@
+# 待办问题功能测试指南
+
+## 🎯 测试目标
+验证紧急事件转为待办问题后,数据能够正确保存到数据库,页面刷新后不会消失。
+
+---
+
+## 📝 测试前准备
+
+### 1. 确认修复已应用
+检查 `dashboard.ts` 中的 `createTodoFromEvent` 方法是否包含数据库保存逻辑:
+
+```typescript
+// 应该是 async 方法
+async createTodoFromEvent(event: UrgentEvent): Promise<void> {
+  // ...
+  const saved = await issueObj.save(); // 应该有这行
+  // ...
+}
+```
+
+### 2. 打开浏览器开发者工具
+- 按 F12 打开控制台
+- 切换到 Console 标签
+- 清空现有日志(可选)
+
+---
+
+## 🧪 测试步骤
+
+### 测试用例 1:创建待办问题
+
+#### 步骤:
+1. 登录设计师组长端
+2. 打开工作台(Dashboard)
+3. 查看左侧"紧急事件"列表
+4. 找到任意一个紧急事件
+5. 点击事件卡片上的"转为待办问题"按钮
+
+#### 预期结果:
+- ✅ 右侧"待办任务"列表立即出现新任务
+- ✅ 任务标题以【紧急】开头
+- ✅ 任务显示正确的项目名称
+- ✅ 任务优先级显示为红色或橙色
+- ✅ 控制台显示成功日志:
+  ```
+  💾 [待办问题] 开始保存到数据库...
+  ✅ [待办问题] 保存成功: abc123xyz
+  ✅ [待办问题] 紧急事件已转为待办问题
+  ```
+- ✅ 弹窗提示"已成功转为待办问题"
+
+---
+
+### 测试用例 2:数据库验证
+
+#### 步骤:
+1. 从控制台复制任务ID(例如:`abc123xyz`)
+2. 打开 Parse Dashboard(数据库管理后台)
+3. 进入 ProjectIssue 表
+4. 搜索刚才的任务ID
+
+#### 预期结果:
+- ✅ 找到对应的记录
+- ✅ `title` 字段以【紧急】开头
+- ✅ `status` 字段为"待处理"
+- ✅ `priority` 字段为 'urgent' 或 'high'
+- ✅ `issueType` 字段为 'feedback'
+- ✅ `project` 字段正确关联项目
+- ✅ `creator` 字段正确关联当前用户
+- ✅ `isDeleted` 字段为 false
+- ✅ `data.sourceEvent` 包含来源事件信息
+
+**数据示例**:
+```json
+{
+  "objectId": "abc123xyz",
+  "title": "【紧急】客户催交付图纸",
+  "status": "待处理",
+  "priority": "urgent",
+  "issueType": "feedback",
+  "project": { "__type": "Pointer", "className": "Project", "objectId": "..." },
+  "creator": { "__type": "Pointer", "className": "Profile", "objectId": "..." },
+  "isDeleted": false,
+  "data": {
+    "tags": ["交付", "来自紧急事件"],
+    "comments": [],
+    "sourceEvent": {
+      "eventId": "...",
+      "eventType": "urgent",
+      "convertedAt": "2024-12-09T08:00:00.000Z",
+      "convertedBy": "..."
+    }
+  }
+}
+```
+
+---
+
+### 测试用例 3:刷新持久化测试(关键)
+
+#### 步骤:
+1. 创建待办问题后(按测试用例1)
+2. 记录任务标题(例如:【紧急】客户催交付图纸)
+3. 按 F5 刷新页面
+4. 等待数据加载完成(约2-3秒)
+5. 查看右侧"待办任务"列表
+
+#### 预期结果:
+- ✅ **刚才创建的待办问题仍然存在**
+- ✅ 任务标题、优先级、项目名称等信息完整
+- ✅ 任务排序正确(按更新时间或优先级)
+- ✅ 控制台显示加载日志:
+  ```
+  🔍 [TodoTaskService] 开始加载待办任务...
+  📥 [TodoTaskService] 查询到 X 条问题记录
+  ✅ 加载待办任务成功,共 X 条
+  ```
+
+#### ❌ 如果失败:
+- 任务消失 → 说明数据库保存失败
+- 检查控制台是否有错误日志
+- 检查 Parse Dashboard 中是否有记录
+
+---
+
+### 测试用例 4:错误处理测试
+
+#### 步骤:
+1. 断开网络连接(模拟网络故障)
+2. 点击"转为待办问题"按钮
+3. 观察界面反馈
+
+#### 预期结果:
+- ✅ 任务先出现在列表中(乐观更新)
+- ✅ 1-2秒后任务自动消失(保存失败回滚)
+- ✅ 弹窗提示错误信息:
+  ```
+  保存失败:Failed to fetch
+  请重试
+  ```
+- ✅ 控制台显示错误日志:
+  ```
+  ❌ [待办问题] 保存失败: TypeError: Failed to fetch
+  ```
+
+---
+
+### 测试用例 5:并发创建测试
+
+#### 步骤:
+1. 快速连续点击多个紧急事件的"转为待办问题"按钮
+2. 观察待办任务列表和控制台日志
+
+#### 预期结果:
+- ✅ 所有任务都成功创建
+- ✅ 每个任务都有唯一的ID
+- ✅ 数据库中有对应数量的记录
+- ✅ 刷新后所有任务都存在
+
+---
+
+### 测试用例 6:关联数据验证
+
+#### 步骤:
+1. 创建待办问题
+2. 点击任务卡片的"查看详情"按钮
+3. 验证跳转到正确的项目详情页
+
+#### 预期结果:
+- ✅ 跳转到正确的项目
+- ✅ 自动打开问题板块
+- ✅ 高亮显示该问题
+- ✅ 问题信息与待办任务一致
+
+---
+
+## 📊 测试结果记录表
+
+| 测试用例 | 测试时间 | 测试人 | 结果 | 备注 |
+|---------|---------|--------|------|------|
+| 用例1:创建待办问题 | | | ☐ 通过 / ☐ 失败 | |
+| 用例2:数据库验证 | | | ☐ 通过 / ☐ 失败 | |
+| 用例3:刷新持久化 | | | ☐ 通过 / ☐ 失败 | |
+| 用例4:错误处理 | | | ☐ 通过 / ☐ 失败 | |
+| 用例5:并发创建 | | | ☐ 通过 / ☐ 失败 | |
+| 用例6:关联数据 | | | ☐ 通过 / ☐ 失败 | |
+
+---
+
+## 🐛 常见问题排查
+
+### 问题1:保存失败(401 Unauthorized)
+
+**现象**:
+```
+❌ [待办问题] 保存失败: Error: Unauthorized
+```
+
+**原因**:当前用户未登录或 Session 过期
+
+**解决方法**:
+1. 退出登录
+2. 重新登录
+3. 再次测试
+
+---
+
+### 问题2:保存失败(找不到 project)
+
+**现象**:
+```
+❌ [待办问题] 保存失败: Error: Object not found
+```
+
+**原因**:紧急事件的 projectId 无效
+
+**解决方法**:
+1. 检查紧急事件的数据来源
+2. 验证 event.projectId 是否正确
+3. 在数据库中查询该项目是否存在
+
+---
+
+### 问题3:刷新后仍然消失
+
+**现象**:数据库有记录,但刷新后不显示
+
+**原因**:查询条件不匹配
+
+**检查项**:
+1. 检查 `status` 字段是否为"待处理"或"open"
+2. 检查 `isDeleted` 字段是否为 false
+3. 检查查询日志:
+   ```
+   📥 [TodoTaskService] 查询到 X 条问题记录
+   ```
+   如果 X=0,说明查询条件有问题
+
+---
+
+### 问题4:任务重复显示
+
+**现象**:同一个任务显示2次
+
+**原因**:内存添加 + 数据库加载重复
+
+**检查项**:
+1. 查看控制台日志,确认是否正确使用真实ID覆盖临时ID
+2. 检查代码中是否有这段逻辑:
+   ```typescript
+   this.todoTasksFromIssues = this.todoTasksFromIssues.map(task => {
+     if (task.id === tempId) {
+       return { ...task, id: saved.id };
+     }
+     return task;
+   });
+   ```
+
+---
+
+## ✅ 测试通过标准
+
+所有6个测试用例全部通过,具体标准:
+
+1. ✅ 创建待办问题成功,立即显示
+2. ✅ 数据库中存在对应记录,字段正确
+3. ✅ **页面刷新后待办问题仍然存在**(最关键)
+4. ✅ 错误处理正确,有友好提示
+5. ✅ 支持并发创建,无重复或丢失
+6. ✅ 关联数据正确,跳转功能正常
+
+---
+
+## 📸 测试截图要求
+
+请在测试过程中截图保存以下内容:
+
+1. **创建成功截图**:
+   - 待办任务列表显示新任务
+   - 控制台成功日志
+
+2. **数据库记录截图**:
+   - Parse Dashboard 中的记录详情
+
+3. **刷新后截图**:
+   - 刷新后待办任务仍然存在
+
+4. **错误处理截图**:
+   - 断网情况下的错误提示
+
+---
+
+## 🚀 测试完成后
+
+### 如果测试通过:
+1. ✅ 将测试结果记录到项目文档
+2. ✅ 关闭相关 Bug 单
+3. ✅ 通知相关人员功能已修复
+
+### 如果测试失败:
+1. ❌ 记录失败的具体现象
+2. ❌ 收集控制台错误日志
+3. ❌ 截图保存现场
+4. ❌ 提交详细的 Bug 报告
+5. ❌ 通知开发人员排查
+
+---
+
+**文档版本**:v1.0  
+**更新时间**:2024-12-09  
+**维护人**:QA团队

+ 124 - 6
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -272,8 +272,17 @@ export class Dashboard implements OnInit, OnDestroy {
       phases: [],
       expectedEndDate: deadline,
       
-      isStalled: false, // 暂无数据
-      isModification: false, // 暂无数据
+      // 🔥 修复:从 data 字段读取停滞期/改图期状态和原因信息(防止云函数覆盖)
+      isStalled: p.data?.isStalled === true,
+      isModification: p.data?.isModification === true,
+      stagnationReasonType: p.data?.stagnationReasonType,
+      stagnationCustomReason: p.data?.stagnationCustomReason,
+      modificationReasonType: p.data?.modificationReasonType,
+      modificationCustomReason: p.data?.modificationCustomReason,
+      estimatedResumeDate: p.data?.estimatedResumeDate ? new Date(p.data.estimatedResumeDate) : undefined,
+      reasonNotes: p.data?.reasonNotes,
+      markedAt: p.data?.markedAt ? new Date(p.data.markedAt) : undefined,
+      markedBy: p.data?.markedBy,
       
       isOverdue: isOverdue,
       overdueDays: isOverdue ? Math.abs(daysLeft) : 0,
@@ -1510,13 +1519,20 @@ export class Dashboard implements OnInit, OnDestroy {
     });
   }
 
-  createTodoFromEvent(event: UrgentEvent): void {
+  /**
+   * 🔥 将紧急事件转为待办问题(持久化到数据库)
+   */
+  async createTodoFromEvent(event: UrgentEvent): Promise<void> {
     const now = new Date();
+    
+    // 1. 创建本地任务对象(用于立即显示)
+    const tempId = `urgent-todo-${event.id}-${now.getTime()}`;
     const newTask: TodoTaskFromIssue = {
-      id: `urgent-todo-${event.id}-${now.getTime()}`,
+      id: tempId,
       title: `【紧急】${event.title}`,
       description: event.description,
-      priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium',
+      priority: event.urgencyLevel === 'critical' ? 'urgent' : 
+                event.urgencyLevel === 'high' ? 'high' : 'medium',
       type: 'feedback',
       status: 'open',
       projectId: event.projectId,
@@ -1529,8 +1545,110 @@ export class Dashboard implements OnInit, OnDestroy {
       dueDate: event.deadline,
       tags: [...(event.labels || []), '来自紧急事件']
     };
+    
+    // 2. 立即添加到内存(优先显示给用户)
     this.todoTasksFromIssues = [newTask, ...this.todoTasksFromIssues];
-    this.resolveUrgentEvent(event);
+    this.cdr.markForCheck();
+    
+    // 3. 🔥 保存到数据库(关键修复)
+    try {
+      console.log('💾 [待办问题] 开始保存到数据库...', {
+        projectId: event.projectId,
+        title: newTask.title
+      });
+      
+      // 获取当前用户的 Profile ID
+      const cid = localStorage.getItem("company");
+      if (!cid) {
+        throw new Error('无法获取公司ID');
+      }
+      
+      const wwAuth = new WxworkAuth({ cid });
+      const profile = await wwAuth.currentProfile();
+      const creatorId = profile?.id;
+      
+      if (!creatorId) {
+        throw new Error('无法获取当前用户ID');
+      }
+      
+      // 创建 ProjectIssue 对象
+      const ProjectIssue = Parse.Object.extend('ProjectIssue');
+      const issueObj = new ProjectIssue();
+      
+      issueObj.set('project', {
+        __type: 'Pointer',
+        className: 'Project',
+        objectId: event.projectId
+      });
+      issueObj.set('title', newTask.title);
+      issueObj.set('description', newTask.description || '');
+      issueObj.set('priority', newTask.priority);
+      issueObj.set('issueType', newTask.type);
+      issueObj.set('status', '待处理'); // 使用中文状态
+      issueObj.set('creator', {
+        __type: 'Pointer',
+        className: 'Profile',
+        objectId: creatorId
+      });
+      
+      // 设置关联信息
+      if (newTask.relatedStage) {
+        issueObj.set('relatedStage', newTask.relatedStage);
+      }
+      if (newTask.dueDate) {
+        issueObj.set('dueDate', newTask.dueDate);
+      }
+      
+      // 设置 data 字段
+      issueObj.set('data', {
+        tags: newTask.tags || [],
+        comments: [],
+        relatedStage: newTask.relatedStage,
+        sourceEvent: {
+          eventId: event.id,
+          eventType: 'urgent',
+          convertedAt: new Date(),
+          convertedBy: creatorId
+        }
+      });
+      
+      issueObj.set('isDeleted', false);
+      
+      // 保存到数据库
+      const saved = await issueObj.save();
+      
+      console.log('✅ [待办问题] 保存成功:', saved.id);
+      
+      // 4. 更新内存中的任务ID(从临时ID改为真实ID)
+      this.todoTasksFromIssues = this.todoTasksFromIssues.map(task => {
+        if (task.id === tempId) {
+          return {
+            ...task,
+            id: saved.id // 使用数据库返回的真实ID
+          };
+        }
+        return task;
+      });
+      
+      // 5. 标记紧急事件为已处理
+      this.resolveUrgentEvent(event);
+      
+      console.log('✅ [待办问题] 紧急事件已转为待办问题');
+      window?.fmode?.alert('已成功转为待办问题');
+      
+    } catch (error) {
+      console.error('❌ [待办问题] 保存失败:', error);
+      
+      // 保存失败,从内存中移除(避免误导用户)
+      this.todoTasksFromIssues = this.todoTasksFromIssues.filter(
+        task => task.id !== tempId
+      );
+      
+      const errorMsg = error instanceof Error ? error.message : '未知错误';
+      window?.fmode?.alert(`保存失败:${errorMsg}\n请重试`);
+    } finally {
+      this.cdr.markForCheck();
+    }
   }
   
   // 待办任务操作(由子组件触发)

+ 1 - 14
src/modules/project/pages/project-detail/stages/stage-order.component.html

@@ -201,7 +201,7 @@
     </app-team-assign>
 
     <!-- 4. 操作按钮 -->
-    @if (canEdit && isFromCustomerService) {
+    @if (canEdit) {
       <div class="action-buttons">
         <button
           class="btn btn-outline"
@@ -224,19 +224,6 @@
         </button>
       </div>
     }
-    
-    <!-- 非客服板块进入时的提示 -->
-    @if (canEdit && !isFromCustomerService && !isTeamLeader) {
-      <div class="action-buttons">
-        <div class="info-banner">
-          <div class="info-icon">ℹ️</div>
-          <div class="info-content">
-            <p>订单确认操作仅限客服人员在项目列表中进行</p>
-            <p class="info-hint">请返回客服板块的项目列表查看此项目</p>
-          </div>
-        </div>
-      </div>
-    }
   </div>
 
 }

+ 99 - 126
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -2762,6 +2762,68 @@
 // 移动端优化 (≤480px)
 @media (max-width: 480px) {
   .stage-order-container {
+    padding: 8px;
+
+    // 审批状态横幅优化
+    .approval-status-banner {
+      padding: 12px;
+      margin-bottom: 16px;
+      flex-direction: column;
+      gap: 12px;
+
+      .status-icon {
+        font-size: 28px;
+        align-self: center;
+      }
+
+      .status-content {
+        text-align: center;
+
+        h4 {
+          font-size: 16px;
+          margin-bottom: 6px;
+        }
+
+        p {
+          font-size: 13px;
+          line-height: 1.4;
+        }
+
+        .btn-resubmit {
+          margin-top: 10px;
+          width: 100%;
+          padding: 10px 16px;
+          font-size: 13px;
+        }
+      }
+    }
+
+    // 组长审批栏优化
+    .leader-approval-bar {
+      padding: 16px 12px;
+      margin: 16px 0;
+
+      .approval-buttons-container {
+        flex-direction: column;
+        gap: 12px;
+
+        button {
+          width: 100%;
+          min-width: auto;
+          padding: 12px 20px;
+          font-size: 14px;
+
+          .btn-icon {
+            font-size: 18px;
+          }
+
+          .btn-text {
+            font-size: 14px;
+          }
+        }
+      }
+    }
+
     .card {
       border-radius: 10px;
       margin-bottom: 12px;
@@ -2882,21 +2944,20 @@
 
     .action-buttons,
     .action-buttons-horizontal {
-      // 移动端也保持横排显示
       flex-direction: row;
       gap: 8px;
       padding: 16px 12px;
       margin-top: 16px;
-      overflow-x: auto; // 如果屏幕太小,允许横向滚动
+      overflow-x: auto;
 
       .btn {
         max-width: none;
         flex: 1;
-        min-width: 90px; // 设置最小宽度,保证按钮不会太窄
+        min-width: 90px;
         padding: 12px 16px;
         font-size: 13px;
         min-height: 48px;
-        white-space: nowrap; // 防止文字换行
+        white-space: nowrap;
 
         .icon {
           width: 16px;
@@ -2906,11 +2967,21 @@
     }
 
     .form-group {
+      margin-bottom: 14px;
+
+      .form-label {
+        font-size: 13px;
+        margin-bottom: 6px;
+      }
+
       .form-input,
       .form-select,
       .form-textarea {
-        padding: 10px 12px;
-        font-size: 13px;
+        padding: 12px;
+        font-size: 14px;
+        min-height: 44px;
+        -webkit-appearance: none;
+        appearance: none; // 标准属性
       }
 
       .form-select {
@@ -2918,139 +2989,41 @@
         background-size: 18px;
         padding-right: 36px;
       }
-    }
-  }
-}
-
-    .quotation-card {
-      .space-section {
-        margin-bottom: 20px;
-        padding-bottom: 20px;
-
-        .space-header {
-          padding: 8px 12px;
-
-          .icon {
-            width: 18px;
-            height: 18px;
-          }
-
-          h3 {
-            font-size: 14px;
-          }
-        }
-
-        .process-grid {
-          gap: 10px;
-
-          .process-item {
-            padding: 10px;
-
-            .process-header {
-              .checkbox-wrapper {
-                .checkbox-custom {
-                  height: 18px;
-                  width: 18px;
-
-                  &::after {
-                    width: 4px;
-                    height: 8px;
-                  }
-                }
-              }
 
-              .badge {
-                font-size: 11px;
-                padding: 3px 8px;
-              }
-            }
-          }
-        }
-      }
-
-      .total-section {
-        padding: 12px 16px;
-
-        .total-label {
-          font-size: 14px;
-        }
-
-        .total-amount {
-          font-size: 20px;
-        }
+      .form-textarea {
+        min-height: 100px;
+        resize: vertical;
       }
     }
 
-    .designer-card {
-      .designer-grid {
-        .designer-item {
-          padding: 10px;
+    // 项目基本信息卡片优化
+    .project-info-card {
+      .card-header {
+        &.collapsible {
+          padding: 14px 12px;
+          -webkit-tap-highlight-color: transparent;
 
-          .designer-avatar {
-            width: 40px;
-            height: 40px;
+          .card-title {
+            font-size: 15px;
 
-            .avatar-icon {
-              width: 40px;
-              height: 40px;
+            .icon {
+              width: 18px;
+              height: 18px;
             }
           }
 
-          .designer-info {
-            h4 {
-              font-size: 14px;
+          .collapse-toggle {
+            .toggle-text {
+              font-size: 12px;
             }
 
-            p {
-              font-size: 11px;
+            .icon.arrow {
+              width: 18px;
+              height: 18px;
             }
           }
-
-          .selected-icon {
-            width: 24px;
-            height: 24px;
-          }
-        }
-      }
-    }
-
-    .action-buttons,
-    .action-buttons-horizontal {
-      // 移动端也保持横排显示
-      flex-direction: row;
-      gap: 8px;
-      padding: 16px 12px;
-      margin-top: 16px;
-      overflow-x: auto; // 如果屏幕太小,允许横向滚动
-
-      .btn {
-        max-width: none;
-        flex: 1;
-        min-width: 90px; // 设置最小宽度,保证按钮不会太窄
-        padding: 12px 16px;
-        font-size: 13px;
-        min-height: 48px;
-        white-space: nowrap; // 防止文字换行
-
-        .icon {
-          width: 16px;
-          height: 16px;
         }
       }
     }
-
-    .form-group {
-      .form-input,
-      .form-select,
-      .form-textarea {
-        padding: 10px 12px;
-        font-size: 13px;
-      }
-
-      .form-select {
-        background-position: right 10px center;
-        background-size: 18px;
-        padding-right: 36px;
-      }
-    }
-  
+  }
+}