Pārlūkot izejas kodu

feat: enhance project management features with new approval workflows and UI improvements

- Introduced a new approval system for project stages, allowing team leaders to approve or reject orders directly from the project detail view.
- Added visual indicators for approval status, enhancing user awareness of project progress.
- Implemented local storage flags to manage user roles and permissions effectively, ensuring appropriate access to approval functionalities.
- Enhanced the UI with new action buttons and information banners to guide users through the approval process.
- Improved file management by saving metadata, including approval statuses, to project files for better tracking and organization.
- Refactored various components to streamline the user experience and ensure consistency across the application.
徐福静0235668 1 dienu atpakaļ
vecāks
revīzija
6e942f4eaa
21 mainītis faili ar 2245 papildinājumiem un 192 dzēšanām
  1. 298 0
      public/check-parse-data.html
  2. 9 2
      src/app/app.routes.ts
  3. 221 34
      src/app/pages/admin/services/project-auto-case.service.ts
  4. 39 5
      src/app/pages/customer-service/case-library/case-library.ts
  5. 9 0
      src/app/pages/customer-service/project-list/project-list.ts
  6. 19 9
      src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.html
  7. 243 1
      src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts
  8. 62 20
      src/app/pages/team-leader/dashboard/dashboard.ts
  9. 33 0
      src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.html
  10. 53 1
      src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts
  11. 35 0
      src/app/pages/team-leader/services/designer.service.ts
  12. 28 6
      src/app/services/case.service.ts
  13. 116 56
      src/modules/project/pages/project-detail/stages/stage-delivery.component.html
  14. 404 19
      src/modules/project/pages/project-detail/stages/stage-delivery.component.scss
  15. 361 30
      src/modules/project/pages/project-detail/stages/stage-delivery.component.ts
  16. 26 1
      src/modules/project/pages/project-detail/stages/stage-order.component.html
  17. 39 0
      src/modules/project/pages/project-detail/stages/stage-order.component.scss
  18. 231 5
      src/modules/project/pages/project-detail/stages/stage-order.component.ts
  19. 7 3
      src/modules/project/services/project-file.service.ts
  20. 6 0
      快速开始.md
  21. 6 0
      核心代码变更.md

+ 298 - 0
public/check-parse-data.html

@@ -0,0 +1,298 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>检查 Parse 后端数据</title>
+  <script src="https://npmcdn.com/parse/dist/parse.min.js"></script>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
+      max-width: 1200px;
+      margin: 50px auto;
+      padding: 20px;
+      background: #f5f5f5;
+    }
+    .container {
+      background: white;
+      padding: 30px;
+      border-radius: 8px;
+      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+    }
+    h1 {
+      color: #333;
+      margin-bottom: 30px;
+    }
+    .input-group {
+      margin-bottom: 20px;
+    }
+    label {
+      display: block;
+      margin-bottom: 8px;
+      font-weight: 600;
+      color: #555;
+    }
+    input {
+      width: 100%;
+      padding: 12px;
+      border: 1px solid #ddd;
+      border-radius: 4px;
+      font-size: 14px;
+      box-sizing: border-box;
+    }
+    button {
+      background: #007bff;
+      color: white;
+      border: none;
+      padding: 12px 24px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 16px;
+      margin-right: 10px;
+    }
+    button:hover {
+      background: #0056b3;
+    }
+    button:disabled {
+      background: #ccc;
+      cursor: not-allowed;
+    }
+    .success {
+      background: #28a745;
+    }
+    .success:hover {
+      background: #218838;
+    }
+    .result {
+      margin-top: 30px;
+      padding: 20px;
+      background: #f8f9fa;
+      border-radius: 4px;
+      border-left: 4px solid #007bff;
+      white-space: pre-wrap;
+      font-family: 'Courier New', monospace;
+      font-size: 13px;
+      line-height: 1.6;
+      max-height: 600px;
+      overflow-y: auto;
+    }
+    .error {
+      border-left-color: #dc3545;
+      color: #dc3545;
+    }
+    .success-msg {
+      border-left-color: #28a745;
+      color: #28a745;
+    }
+    .status {
+      display: inline-block;
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 600;
+      margin-left: 10px;
+    }
+    .status.pending {
+      background: #fff3cd;
+      color: #856404;
+    }
+    .status.approved {
+      background: #d4edda;
+      color: #155724;
+    }
+    .status.undefined {
+      background: #f8d7da;
+      color: #721c24;
+    }
+    .highlight {
+      background: #fffacd;
+      padding: 2px 6px;
+      border-radius: 3px;
+      font-weight: 600;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <h1>🔍 检查 Parse 后端项目数据</h1>
+    
+    <div class="input-group">
+      <label>项目 ID:</label>
+      <input type="text" id="projectId" placeholder="例如:EyO8xplVhz" value="EyO8xplVhz">
+    </div>
+    
+    <button onclick="checkData()">🔍 查询数据</button>
+    <button class="success" onclick="fixData()" id="fixBtn" disabled>🔧 修复数据(设置为 pending)</button>
+    
+    <div id="result"></div>
+  </div>
+
+  <script>
+    // 初始化 Parse
+    Parse.initialize("YSS_PROJECT");
+    Parse.serverURL = 'https://server.fmode.cn/parse';
+
+    let currentProject = null;
+
+    async function checkData() {
+      const projectId = document.getElementById('projectId').value.trim();
+      const resultDiv = document.getElementById('result');
+      const fixBtn = document.getElementById('fixBtn');
+      
+      if (!projectId) {
+        resultDiv.className = 'result error';
+        resultDiv.textContent = '❌ 请输入项目 ID';
+        return;
+      }
+
+      resultDiv.className = 'result';
+      resultDiv.textContent = '⏳ 正在查询...';
+      fixBtn.disabled = true;
+
+      try {
+        const query = new Parse.Query('Project');
+        const project = await query.get(projectId);
+        currentProject = project;
+
+        const data = project.get('data') || {};
+        const currentStage = project.get('currentStage');
+        const title = project.get('title');
+        const approvalStatus = data.approvalStatus;
+        const pendingApprovalBy = data.pendingApprovalBy;
+        const approvalHistory = data.approvalHistory || [];
+        
+        let statusClass = 'undefined';
+        if (approvalStatus === 'pending') statusClass = 'pending';
+        if (approvalStatus === 'approved') statusClass = 'approved';
+
+        let output = `
+✅ 查询成功!
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+📋 项目基本信息:
+  项目 ID: ${projectId}
+  项目名称: ${title || '未设置'}
+  当前阶段: ${currentStage || '未设置'}
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+🎯 审批状态检查:
+  approvalStatus: ${approvalStatus || 'undefined'} ${approvalStatus === 'pending' ? '✅' : '❌'}
+  pendingApprovalBy: ${pendingApprovalBy || 'undefined'}
+  
+  审批历史记录数: ${approvalHistory.length} 条
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+📊 完整的 data 字段:
+${JSON.stringify(data, null, 2)}
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+${approvalStatus === 'pending' 
+  ? '✅ 数据正常!approvalStatus 已正确设置为 pending' 
+  : '❌ 数据异常!approvalStatus 不是 pending,需要修复'}
+`;
+
+        resultDiv.className = approvalStatus === 'pending' ? 'result success-msg' : 'result error';
+        resultDiv.textContent = output;
+        
+        // 如果数据异常,启用修复按钮
+        fixBtn.disabled = (approvalStatus === 'pending');
+        
+      } catch (error) {
+        console.error('查询失败:', error);
+        resultDiv.className = 'result error';
+        resultDiv.textContent = `❌ 查询失败:\n\n${error.message}\n\n${error.stack}`;
+        currentProject = null;
+        fixBtn.disabled = true;
+      }
+    }
+
+    async function fixData() {
+      if (!currentProject) {
+        alert('请先查询项目数据');
+        return;
+      }
+
+      const resultDiv = document.getElementById('result');
+      const fixBtn = document.getElementById('fixBtn');
+
+      if (!confirm('确定要将此项目的 approvalStatus 设置为 pending 吗?')) {
+        return;
+      }
+
+      resultDiv.className = 'result';
+      resultDiv.textContent = '⏳ 正在修复数据...';
+      fixBtn.disabled = true;
+
+      try {
+        const data = currentProject.get('data') || {};
+        
+        // 设置审批状态
+        data.approvalStatus = 'pending';
+        data.pendingApprovalBy = 'team-leader';
+        
+        // 确保 currentStage 是"订单分配"
+        currentProject.set('currentStage', '订单分配');
+        currentProject.set('data', data);
+
+        console.log('🔧 准备保存修复后的数据:', {
+          projectId: currentProject.id,
+          currentStage: currentProject.get('currentStage'),
+          approvalStatus: data.approvalStatus,
+          pendingApprovalBy: data.pendingApprovalBy
+        });
+
+        await currentProject.save();
+
+        // 验证保存
+        const verifyQuery = new Parse.Query('Project');
+        const savedProject = await verifyQuery.get(currentProject.id);
+        const savedData = savedProject.get('data') || {};
+
+        let output = `
+✅ 修复成功!
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+已更新的字段:
+  currentStage: ${savedProject.get('currentStage')}
+  approvalStatus: ${savedData.approvalStatus}
+  pendingApprovalBy: ${savedData.pendingApprovalBy}
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+验证结果:
+${savedData.approvalStatus === 'pending' ? '✅ approvalStatus 已正确设置为 pending' : '❌ 修复失败,approvalStatus 仍不是 pending'}
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+请刷新组长端页面查看效果!
+`;
+
+        resultDiv.className = 'result success-msg';
+        resultDiv.textContent = output;
+        
+        fixBtn.disabled = true;
+
+      } catch (error) {
+        console.error('修复失败:', error);
+        resultDiv.className = 'result error';
+        resultDiv.textContent = `❌ 修复失败:\n\n${error.message}\n\n${error.stack}`;
+      }
+    }
+
+    // 页面加载时自动查询
+    window.onload = () => {
+      const projectId = document.getElementById('projectId').value;
+      if (projectId) {
+        checkData();
+      }
+    };
+  </script>
+</body>
+</html>
+
+

+ 9 - 2
src/app/app.routes.ts

@@ -503,11 +503,18 @@ export const routes: Routes = [
     ]
   },
 
+  // 登录页面(不受企业微信认证保护)
+  {
+    path: 'login',
+    loadComponent: () => import('./pages/auth/login/login').then(m => m.LoginPage),
+    title: '登录'
+  },
+
   // 默认路由重定向到登录页
   {
     path: '',
-    loadComponent: () => import('./pages/auth/login/login').then(m => m.LoginPage),
+    redirectTo: '/login',
     pathMatch: 'full'
   },
-  { path: '**', redirectTo: '/customer-service/dashboard' }
+  { path: '**', redirectTo: '/login' }
 ];

+ 221 - 34
src/app/pages/admin/services/project-auto-case.service.ts

@@ -90,6 +90,66 @@ export class ProjectAutoCaseService {
       };
     }
   }
+
+  /**
+   * 扫描已完成但尚未生成案例的项目并补齐(幂等)
+   */
+  async backfillMissingCases(limit: number = 10): Promise<{ created: number; scanned: number }> {
+    const ProjectQuery = new Parse.Query('Project');
+    ProjectQuery.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: this.companyId });
+    ProjectQuery.notEqualTo('isDeleted', true);
+    ProjectQuery.containedIn('currentStage', ['尾款结算', '售后归档', 'aftercare']);
+    ProjectQuery.doesNotExist('data.caseId');
+    ProjectQuery.include('contact', 'assignee', 'department'); // 🔥 必须 include 关联对象
+    ProjectQuery.limit(limit);
+
+    const projects = await ProjectQuery.find();
+    let created = 0;
+    for (const p of projects) {
+      try {
+        const ProductQuery = new Parse.Query('Product');
+        ProductQuery.equalTo('project', p.toPointer());
+        ProductQuery.notEqualTo('isDeleted', true);
+        ProductQuery.include('profile');
+        const products = await ProductQuery.find();
+        const res = await this.createCaseFromProject(p, products);
+        if (res.success) created++;
+      } catch (e) {
+        console.warn('⚠️ 补齐案例失败', p.id, p.get('title'), e);
+      }
+    }
+    return { created, scanned: projects.length };
+  }
+
+  /**
+   * 对外公开:为指定项目创建案例(不变更项目阶段)
+   * 在项目已进入"售后归档/尾款结算"等完成阶段时调用
+   */
+  async createCaseForProject(projectId: string): Promise<{ success: boolean; caseId?: string; error?: string }> {
+    try {
+      const ProjectQuery = new Parse.Query('Project');
+      ProjectQuery.equalTo('company', { __type: 'Pointer', className: 'Company', objectId: this.companyId });
+      ProjectQuery.include('contact', 'assignee', 'department'); // 🔥 必须 include 关联对象
+      const project = await ProjectQuery.get(projectId);
+
+      // 若已存在关联的案例,直接返回
+      const data = project.get('data') || {};
+      if (data.caseId) {
+        return { success: true, caseId: data.caseId };
+      }
+
+      // 加载项目的空间(Product)
+      const ProductQuery = new Parse.Query('Product');
+      ProductQuery.equalTo('project', project.toPointer());
+      ProductQuery.notEqualTo('isDeleted', true);
+      ProductQuery.include('profile');
+      const products = await ProductQuery.find();
+
+      return await this.createCaseFromProject(project, products);
+    } catch (error) {
+      return { success: false, error: error instanceof Error ? error.message : '未知错误' };
+    }
+  }
   
   /**
    * 清理重复的Product(空间)
@@ -245,7 +305,7 @@ export class ProjectAutoCaseService {
   }
   
   /**
-   * 从Project和Products创建Case
+   * 从Project和Products创建Case(完整版本)
    */
   private async createCaseFromProject(project: any, products: any[]): Promise<{ success: boolean; caseId?: string; error?: string }> {
     try {
@@ -253,12 +313,15 @@ export class ProjectAutoCaseService {
       const contact = project.get('contact');
       const assignee = project.get('assignee');
       const department = project.get('department');
+      const projectData = project.get('data') || {};
       
-      // 收集所有空间的图片
+      // 收集所有空间的图片和交付文件
       const allImages: string[] = [];
       let totalPrice = 0;
       let totalArea = 0;
       
+      // 从Product(空间)收集数据
+      const productsDetail: any[] = [];
       for (const product of products) {
         const productData = product.get('data') || {};
         if (productData.images && Array.isArray(productData.images)) {
@@ -270,74 +333,198 @@ export class ProjectAutoCaseService {
           totalPrice += quotation.total;
         }
         
-        if (productData.space && productData.space.area) {
-          totalArea += productData.space.area;
+        const spaceArea = productData.space?.area || 0;
+        if (spaceArea > 0) {
+          totalArea += spaceArea;
         }
+        
+        // 产品详细信息
+        productsDetail.push({
+          productId: product.id,
+          productName: product.get('productName') || '未命名空间',
+          category: productData.space?.type || '其他',
+          brand: '',
+          quantity: 1,
+          unitPrice: quotation?.total || 0,
+          totalPrice: quotation?.total || 0,
+          spaceArea: spaceArea,
+          specifications: {
+            style: productData.space?.style || '现代简约',
+            color: productData.space?.color || '',
+            lighting: productData.space?.lighting || '',
+            requirements: productData.requirements || {}
+          }
+        });
+      }
+      
+      // 从ProjectFile加载交付文件图片
+      try {
+        const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
+        const fileQuery = new Parse.Query('ProjectFile');
+        fileQuery.equalTo('project', project.toPointer());
+        fileQuery.equalTo('stage', 'delivery');
+        fileQuery.include('attach');
+        fileQuery.limit(100);
+        const deliveryFiles = await fileQuery.find();
+        
+        const imagesDetail: any = {
+          beforeRenovation: [],
+          afterRenovation: [],
+          videos: [],
+          panoramas: []
+        };
+        
+        for (const file of deliveryFiles) {
+          const attach = file.get('attach');
+          if (!attach) continue;
+          
+          const fileUrl = attach.get('url') || '';
+          const fileName = attach.get('name') || attach.get('originalName') || '';
+          const fileType = attach.get('mime') || attach.get('type') || '';
+          const fileData = file.get('data') || {};
+          
+          const fileInfo = {
+            attachmentId: attach.id || '',
+            url: fileUrl,
+            description: fileName,
+            uploadDate: file.get('createdAt')?.toISOString() || new Date().toISOString(),
+            spaceArea: fileData.productName || fileData.spaceName || ''
+          };
+          
+          // 根据文件类型分类
+          if (fileType.startsWith('image/')) {
+            allImages.push(fileUrl);
+            // 根据fileType判断是before/after
+            const fileTypeStr = file.get('fileType') || '';
+            if (fileTypeStr.includes('before')) {
+              imagesDetail.beforeRenovation.push(fileInfo);
+            } else {
+              imagesDetail.afterRenovation.push(fileInfo);
+            }
+          } else if (fileType.startsWith('video/')) {
+            imagesDetail.videos.push({
+              ...fileInfo,
+              duration: 0
+            });
+          }
+        }
+        
+        // 保存图片详细信息到data
+        projectData.imagesDetail = imagesDetail;
+      } catch (e) {
+        console.warn('⚠️ 加载交付文件失败(不影响案例创建):', e);
       }
       
+      // 从项目data中提取标签
+      const styleTags = projectData.quotation?.spaces?.map((s: any) => 
+        s.name || ''
+      ).filter(Boolean) || [];
+      const defaultTags = ['全屋设计', '专业团队'];
+      const finalTags = [...new Set([...styleTags, ...defaultTags])].slice(0, 5);
+      
       // 构建案例数据
       const caseData = {
-        name: project.get('title') || '测试案例',
+        name: project.get('title') || '未命名案例',
         projectId: project.id,
         designerId: assignee?.id || '',
         teamId: department?.id || '',
-        coverImage: allImages[0] || 'https://placehold.co/800x600/png?text=封面图',
-        images: allImages.slice(0, 10), // 最多10张图片
-        totalPrice: totalPrice || 50000,
-        completionDate: new Date(),
-        tag: ['现代简约', '全屋设计'],
+        coverImage: allImages[0] || 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"><rect width="800" height="600" fill="%23f0f0f0"/><text x="50%" y="50%" text-anchor="middle" font-size="24" fill="%23999">暂无封面图</text></svg>',
+        images: allImages.slice(0, 20), // 最多20张图片
+        totalPrice: totalPrice || projectData.quotation?.total || 0,
+        completionDate: projectData.deliveryCompletedAt || projectData.completedAt || new Date(),
+        tag: finalTags,
         info: {
-          area: totalArea || 100,
-          projectType: '家装' as '工装' | '家装',
-          roomType: '三居室' as '一居室' | '二居室' | '三居室' | '四居+',
-          spaceType: '平层' as '平层' | '复式' | '别墅' | '自建房',
-          renderingLevel: '高端' as '高端' | '中端' | '低端'
+          area: totalArea || projectData.quotation?.spaces?.reduce((sum: number, s: any) => sum + (s.area || 0), 0) || 100,
+          projectType: (project.get('projectType') || '家装') as '工装' | '家装',
+          roomType: (projectData.roomType || '三居室') as '一居室' | '二居室' | '三居室' | '四居+',
+          spaceType: (projectData.spaceType || project.get('spaceType') || '平层') as '平层' | '复式' | '别墅' | '自建房',
+          renderingLevel: (project.get('renderType') === '360全景' ? '高端' : projectData.priceLevel === '三级' ? '高端' : projectData.priceLevel === '二级' ? '中端' : '低端') as '高端' | '中端' | '低端'
         },
         isPublished: true,
         publishedAt: new Date(),
         isExcellent: false,
-        index: 100,
-        customerReview: `客户${contact?.get('name') || '张先生'}对整体设计非常满意,设计师团队专业高效!`,
+        index: Math.floor(Date.now() / 1000), // 使用时间戳作为排序,新案例靠前
+        customerReview: projectData.customerReview || `客户${(contact && typeof contact.get === 'function') ? contact.get('name') : (contact?.name || '')}对整体设计非常满意,设计师团队专业高效!`,
         data: {
+          // 装修规格信息
+          renovationSpec: projectData.renovationSpec || undefined,
+          
           // 产品详细信息
-          productsDetail: products.map(p => ({
-            productId: p.id,
-            productName: p.get('productName') || '未命名',
-            spaceArea: p.get('data')?.space?.area || 0,
-            budget: p.get('quotation')?.total || 0,
-            style: p.get('data')?.space?.style || '现代简约',
-            designer: p.get('profile')?.get('name') || '未知'
-          })),
+          productsDetail,
           
           // 预算信息
           budget: {
             total: totalPrice,
             designFee: Math.round(totalPrice * 0.2),
             constructionFee: Math.round(totalPrice * 0.5),
-            softDecorFee: Math.round(totalPrice * 0.3)
+            softDecorFee: Math.round(totalPrice * 0.3),
+            actualCost: totalPrice,
+            savingRate: 0
           },
           
           // 时间线
           timeline: {
             startDate: project.get('createdAt')?.toISOString() || new Date().toISOString(),
-            completionDate: new Date().toISOString(),
-            duration: Math.ceil((new Date().getTime() - (project.get('createdAt')?.getTime() || Date.now())) / (1000 * 60 * 60 * 24))
+            completionDate: (projectData.deliveryCompletedAt || projectData.completedAt || new Date()).toISOString(),
+            duration: Math.ceil((new Date().getTime() - (project.get('createdAt')?.getTime() || Date.now())) / (1000 * 60 * 60 * 24)),
+            milestones: [
+              { stage: '订单分配', date: project.get('createdAt')?.toISOString() || '', status: '已完成' },
+              { stage: '确认需求', date: projectData.requirementsConfirmedAt?.toISOString() || '', status: '已完成' },
+              { stage: '交付执行', date: projectData.deliveryStartedAt?.toISOString() || '', status: '已完成' },
+              { stage: '售后归档', date: projectData.deliveryCompletedAt?.toISOString() || new Date().toISOString(), status: '已完成' }
+            ].filter(m => m.date)
           },
           
           // 设计亮点
-          highlights: [
+          highlights: projectData.highlights || [
             '空间布局合理,动线流畅',
             '自然采光充足,通风良好',
             '收纳设计巧妙,实用美观',
             '色彩搭配和谐,氛围温馨'
           ],
           
+          // 设计特色
+          features: projectData.features || [],
+          
+          // 设计挑战
+          challenges: projectData.challenges || [],
+          
+          // 材料信息
+          materials: projectData.materials || {},
+          
           // 客户信息
           clientInfo: {
-            familyMembers: 3,
-            ageRange: '30-40岁',
-            lifestyle: '现代都市',
-            satisfactionScore: 5
+            familyMembers: (contact && typeof contact.get === 'function') ? (contact.get('data')?.familyMembers || 3) : 3,
+            ageRange: (contact && typeof contact.get === 'function') ? (contact.get('data')?.ageRange || '30-40岁') : '30-40岁',
+            lifestyle: (contact && typeof contact.get === 'function') ? (contact.get('data')?.lifestyle || '现代都市') : '现代都市',
+            specialNeeds: (contact && typeof contact.get === 'function') ? (contact.get('data')?.specialNeeds || []) : [],
+            satisfactionScore: projectData.customerReview?.rating || 5
+          },
+          
+          // 施工团队信息
+          constructionTeam: projectData.constructionTeam || {
+            contractor: (department && typeof department.get === 'function') ? department.get('name') : (department?.name || '设计团队'),
+            projectManager: (assignee && typeof assignee.get === 'function') ? assignee.get('name') : (assignee?.name || ''),
+            supervisor: '',
+            workers: products.length,
+            subcontractors: []
+          },
+          
+          // 质量验收
+          qualityInspection: projectData.qualityInspection || undefined,
+          
+          // 图片详细信息
+          imagesDetail: projectData.imagesDetail || {
+            beforeRenovation: [],
+            afterRenovation: allImages.map((url, idx) => ({
+              attachmentId: `img-${idx}`,
+              url,
+              description: `效果图${idx + 1}`,
+              uploadDate: new Date().toISOString(),
+              spaceArea: products[idx % products.length]?.get('productName') || ''
+            })),
+            videos: [],
+            panoramas: []
           }
         }
       };
@@ -345,7 +532,7 @@ export class ProjectAutoCaseService {
       // 调用CaseService创建案例
       const newCase = await this.caseService.createCase(caseData);
       
-      console.log('✅ 案例创建成功:', newCase);
+      console.log('✅ 案例创建成功:', newCase.id, '项目:', project.get('title'));
       
       // 在Project的data中记录关联的案例ID
       project.set('data', {

+ 39 - 5
src/app/pages/customer-service/case-library/case-library.ts

@@ -6,6 +6,7 @@ import { Router } from '@angular/router';
 import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
 import { CaseDetailPanelComponent, Case } from './case-detail-panel.component';
 import { CaseService } from '../../../services/case.service';
+import { ProjectAutoCaseService } from '../../admin/services/project-auto-case.service';
 
 interface StatItem {
   id: string;
@@ -67,10 +68,21 @@ export class CaseLibrary implements OnInit, OnDestroy {
 
   constructor(
     private router: Router,
-    private caseService: CaseService
+    private caseService: CaseService,
+    private projectAutoCaseService: ProjectAutoCaseService
   ) {}
 
-  ngOnInit() {
+  async ngOnInit() {
+    // 补齐可能遗漏的案例(幂等,不会重复创建)
+    try {
+      const result = await this.projectAutoCaseService.backfillMissingCases(10);
+      if (result.created > 0) {
+        console.log(`✅ 案例库补齐:新增 ${result.created}/${result.scanned}`);
+      }
+    } catch (e) {
+      console.warn('⚠️ 案例库补齐失败(忽略):', e);
+    }
+
     this.loadCases(); // loadCases 会自动调用 loadStatistics
     this.setupFilterListeners();
     this.setupBehaviorTracking();
@@ -100,9 +112,31 @@ export class CaseLibrary implements OnInit, OnDestroy {
         pageSize: this.itemsPerPage
       });
       
-      this.cases = result.cases;
-      this.filteredCases = result.cases;
-      this.totalCount = result.total;
+      // 去重:同一项目只展示一个案例(按 projectId 去重,保留最新)
+      const uniqueMap = new Map<string, any>();
+      for (const c of result.cases) {
+        if (!c.projectId) {
+          // 无 projectId 的直接保留(极少数异常数据)
+          uniqueMap.set(`__no_project__${c.id}`, c);
+          continue;
+        }
+        if (!uniqueMap.has(c.projectId)) {
+          uniqueMap.set(c.projectId, c);
+        } else {
+          // 保留 publishedAt 较新的
+          const prev = uniqueMap.get(c.projectId);
+          const prevTime = new Date(prev.publishedAt || prev.createdAt || 0).getTime();
+          const curTime = new Date(c.publishedAt || c.createdAt || 0).getTime();
+          if (curTime >= prevTime) {
+            uniqueMap.set(c.projectId, c);
+          }
+        }
+      }
+
+      const uniqueCases = Array.from(uniqueMap.values());
+      this.cases = uniqueCases;
+      this.filteredCases = uniqueCases;
+      this.totalCount = uniqueCases.length;
       this.totalPages = Math.ceil(result.total / this.itemsPerPage) || 1;
       
       console.log(`✅ 已加载 ${result.total} 个已完成项目案例`);

+ 9 - 0
src/app/pages/customer-service/project-list/project-list.ts

@@ -615,6 +615,15 @@ export class ProjectList implements OnInit, OnDestroy {
     
     const stagePath = stagePathMapping[columnId];
     
+    // ✅ 标记从客服板块进入(用于控制"确认订单"按钮权限)
+    try {
+      localStorage.setItem('enterFromCustomerService', '1');
+      localStorage.setItem('customerServiceMode', 'true');
+      console.log('✅ 已标记从客服板块进入,允许确认订单');
+    } catch (e) {
+      console.warn('无法设置 localStorage 标记:', e);
+    }
+    
     // 跳转到wxwork路由的项目详情页(纯净页面,无管理端侧边栏)
     // 路由格式:/wxwork/:cid/project/:projectId/:stage
     this.router.navigate(['/wxwork', cid, 'project', project.id, stagePath]);

+ 19 - 9
src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.html

@@ -122,10 +122,10 @@
                     <div class="designer-actions">
                       <button 
                         class="calendar-btn"
-                        (click)="$event.stopPropagation(); showDesignerDetailCalendar(designer)"
-                        title="查看详细日历"
+                        (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
+                        title="查看设计师详情"
                       >
-                        📅
+                        👤 详情
                       </button>
                       @if (enableSpaceAssignment && spaceScenes.length > 0) {
                         <button 
@@ -219,10 +219,10 @@
                   <div class="designer-actions">
                     <button 
                       class="calendar-btn"
-                      (click)="$event.stopPropagation(); showDesignerDetailCalendar(designer)"
-                      title="查看详细日历"
+                      (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
+                      title="查看设计师详情"
                     >
-                      📅
+                      👤 详情
                     </button>
                     @if (enableSpaceAssignment && spaceScenes.length > 0) {
                       <button 
@@ -304,10 +304,10 @@
                       <div class="designer-actions">
                         <button 
                           class="calendar-btn"
-                          (click)="$event.stopPropagation(); showDesignerDetailCalendar(designer)"
-                          title="查看详细日历"
+                          (click)="$event.stopPropagation(); showDesignerEmployeeDetail(designer)"
+                          title="查看设计师详情"
                         >
-                          📅
+                          👤 详情
                         </button>
                       </div>
                     </div>
@@ -489,4 +489,14 @@
       </div>
     </div>
   </div>
+}
+
+<!-- 员工详情面板(复用组长端) -->
+@if (showEmployeeDetailPanel && employeeDetailData) {
+  <app-employee-detail-panel
+    [visible]="true"
+    [employeeDetail]="employeeDetailData"
+    (close)="closeEmployeeDetailPanel()"
+    (projectClick)="onEmployeeDetailProjectClick($event)">
+  </app-employee-detail-panel>
 }

+ 243 - 1
src/app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component.ts

@@ -5,6 +5,7 @@ import { DesignerCalendarComponent } from '../../../../customer-service/consulta
 import { Designer as CalendarDesigner } from '../../../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
 import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
 import { ProductSpaceService, Project as ProductSpace } from '../../../../../../modules/project/services/product-space.service';
+import { EmployeeDetailPanelComponent, EmployeeDetail, EmployeeCalendarData, EmployeeCalendarDay } from '../../../../team-leader/employee-detail-panel/employee-detail-panel';
 
 const Parse = FmodeParse.with('nova');
 
@@ -29,6 +30,8 @@ export interface Designer {
   groupName: string; // 对应teamName
   isLeader: boolean; // 对应isTeamLeader
   currentProjects: number; // 当前项目数量
+  // 新增:项目事件(用于详细日历)
+  projectEvents?: Array<{ date: string; type: 'project' | 'review' | 'vacation'; title: string; projectId?: string }>; 
 }
 
 export interface ProjectTeam {
@@ -64,7 +67,7 @@ export interface DesignerAssignmentResult {
 @Component({
   selector: 'app-designer-team-assignment-modal',
   standalone: true,
-  imports: [CommonModule, FormsModule, DesignerCalendarComponent],
+  imports: [CommonModule, FormsModule, DesignerCalendarComponent, EmployeeDetailPanelComponent],
   templateUrl: './designer-team-assignment-modal.component.html',
   styleUrls: ['./designer-team-assignment-modal.component.scss']
 })
@@ -266,6 +269,10 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
   designerSpaceMap: Map<string, string[]> = new Map(); // designerId -> spaceIds[]
   selectedDesignerForSpaceAssignment: Designer | null = null;
 
+  // 员工详情面板(复用组长端)
+  showEmployeeDetailPanel: boolean = false;
+  employeeDetailData: EmployeeDetail | null = null;
+
   constructor(
     private cdr: ChangeDetectorRef,
     private productSpaceService: ProductSpaceService
@@ -346,6 +353,9 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
         console.log('成功加载项目组数据:', this.projectTeams);
       }
 
+      // ✨ 新增:为所有成员加载其项目分配,并构建日历事件
+      await this.enrichMembersWithProjectAssignments();
+
     } catch (err) {
       console.error('加载项目组数据失败:', err);
       this.loadError = '加载项目组数据失败';
@@ -437,6 +447,89 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
     };
   }
 
+  /**
+   * 为所有成员加载其真实项目分配,填充 currentProjects / workload / reviewDates / projectEvents 等
+   */
+  private async enrichMembersWithProjectAssignments(): Promise<void> {
+    try {
+      const allMembers: Designer[] = this.projectTeams.flatMap(t => t.members);
+      if (allMembers.length === 0) return;
+
+      const Profile = Parse.Object.extend('Profile');
+      const profilePointers = allMembers.map(m => {
+        const p = new Profile();
+        p.id = m.id;
+        return p;
+      });
+
+      const ptQuery = new Parse.Query('ProjectTeam');
+      ptQuery.containedIn('profile', profilePointers);
+      ptQuery.notEqualTo('isDeleted', true);
+      ptQuery.include('project');
+      ptQuery.limit(1000);
+
+      const rows = await ptQuery.find();
+
+      // 聚合为 profileId -> 项目数组
+      const profileIdToProjects = new Map<string, any[]>();
+      for (const row of rows) {
+        const profile = row.get('profile');
+        const project = row.get('project');
+        if (!profile || !project) continue;
+        const arr = profileIdToProjects.get(profile.id) || [];
+        arr.push(project);
+        profileIdToProjects.set(profile.id, arr);
+      }
+
+      // 填充到成员信息
+      for (const member of allMembers) {
+        const projects = profileIdToProjects.get(member.id) || [];
+        const activeProjects = projects.filter((p: any) => p.get('isDeleted') !== true);
+        member.currentProjects = activeProjects.length;
+
+        // 粗略计算工作量(可按实际规则替换)
+        member.workload = Math.min(100, member.currentProjects * 25);
+        member.status = member.currentProjects >= 3 ? 'busy' : (member.currentProjects === 0 ? 'idle' : 'reviewing');
+
+        // 构建对图日期与日历事件
+        const projectEvents: Designer['projectEvents'] = [];
+        const reviewDates: string[] = [];
+
+        for (const p of activeProjects) {
+          const title = p.get('title') || '未命名项目';
+          const projectId = p.id;
+          const demoday = p.get('demoday');
+          const deadline = p.get('deadline');
+
+          if (demoday) {
+            const dateStr = this.formatDateString(demoday);
+            reviewDates.push(dateStr);
+            projectEvents.push({ date: dateStr, type: 'review', title: `${title} · 对图`, projectId });
+          }
+          if (deadline) {
+            const dateStr = this.formatDateString(deadline);
+            projectEvents.push({ date: dateStr, type: 'project', title: `${title} · 截止`, projectId });
+          }
+        }
+
+        member.reviewDates = reviewDates;
+        member.projectEvents = projectEvents;
+      }
+
+      this.cdr.markForCheck();
+    } catch (err) {
+      console.error('为成员加载项目分配失败:', err);
+    }
+  }
+
+  private formatDateString(date: Date): string {
+    const d = new Date(date);
+    const y = d.getFullYear();
+    const m = (d.getMonth() + 1).toString().padStart(2, '0');
+    const dd = d.getDate().toString().padStart(2, '0');
+    return `${y}-${m}-${dd}`;
+  }
+
   /**
    * 获取设计师状态
    */
@@ -640,6 +733,16 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
       designer.status === 'idle' ? 'available' :
       designer.status === 'stagnant' ? 'stagnant' : 'busy';
 
+    // 组装日历事件
+    const upcomingEvents = (designer.projectEvents || []).map(e => ({
+      id: `${designer.id}-${e.projectId || e.date}-${e.type}`,
+      date: new Date(e.date),
+      title: e.title,
+      type: e.type as any,
+      projectId: e.projectId,
+      duration: e.type === 'review' ? 2 : 6
+    }));
+
     this.selectedCalendarDesigners = [{
       id: designer.id,
       name: designer.name,
@@ -649,6 +752,7 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
       isLeader: designer.isLeader ?? designer.isTeamLeader,
       status: calendarStatus,
       currentProjects: designer.currentProjects ?? 0,
+      upcomingEvents,
       lastOrderDate: designer.lastOrderDate,
       idleDays: designer.idleDays,
       workload: designer.workload,
@@ -814,4 +918,142 @@ export class DesignerTeamAssignmentModalComponent implements OnInit {
     }
     return undefined;
   }
+
+  /**
+   * 显示设计师详情面板(复用组长端员工详情面板)
+   */
+  async showDesignerEmployeeDetail(designer: Designer): Promise<void> {
+    // 查询该设计师的项目数据
+    const Profile = Parse.Object.extend('Profile');
+    const profilePointer = new Profile();
+    profilePointer.id = designer.id;
+
+    const ptQuery = new Parse.Query('ProjectTeam');
+    ptQuery.equalTo('profile', profilePointer);
+    ptQuery.notEqualTo('isDeleted', true);
+    ptQuery.include('project');
+    ptQuery.limit(100);
+
+    try {
+      const rows = await ptQuery.find();
+      const projects = rows.map(r => r.get('project')).filter(p => p && p.get('isDeleted') !== true);
+
+      // 构建项目数据
+      const projectData = projects.map((p: any) => ({
+        id: p.id,
+        name: p.get('title') || '未命名项目'
+      }));
+
+      // 构建日历数据(当月)
+      const calendarData = this.buildEmployeeCalendarData(projects);
+
+      // 映射为 EmployeeDetail 格式
+      this.employeeDetailData = {
+        name: designer.name,
+        currentProjects: projects.length,
+        projectNames: projectData.map(p => p.name),
+        projectData,
+        leaveRecords: [], // 暂无请假数据
+        redMarkExplanation: designer.status === 'busy' ? '当前工作量较高,建议谨慎分配新项目' : 
+                           designer.status === 'stagnant' ? '处于停滞期项目,需要跟进' :
+                           designer.idleDays >= 10 ? `已闲置 ${designer.idleDays} 天,优先推荐分配` : '工作状态正常',
+        calendarData,
+        profileId: designer.id,
+        surveyCompleted: false // 暂无问卷数据
+      };
+
+      this.showEmployeeDetailPanel = true;
+      this.cdr.markForCheck();
+    } catch (err) {
+      console.error('加载设计师详情失败:', err);
+      window?.fmode?.alert('加载设计师详情失败');
+    }
+  }
+
+  /**
+   * 构建员工日历数据(当月视图)
+   */
+  private buildEmployeeCalendarData(projects: any[]): EmployeeCalendarData {
+    const currentMonth = new Date();
+    currentMonth.setDate(1);
+    currentMonth.setHours(0, 0, 0, 0);
+
+    // 生成当月的所有日期(包含上月末和下月初补齐)
+    const firstDayOfMonth = currentMonth.getDay();
+    const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth - 1;
+    
+    const startDate = new Date(currentMonth);
+    startDate.setDate(1 - daysFromPrevMonth);
+
+    const days: EmployeeCalendarDay[] = [];
+    const dateToProjects = new Map<string, Array<{ id: string; name: string; deadline?: Date }>>();
+
+    // 映射项目到日期
+    for (const p of projects) {
+      const demoday = p.get('demoday');
+      const deadline = p.get('deadline');
+      const projectInfo = {
+        id: p.id,
+        name: p.get('title') || '未命名项目',
+        deadline: deadline ? new Date(deadline) : undefined
+      };
+
+      if (demoday) {
+        const key = this.formatDateString(new Date(demoday));
+        const arr = dateToProjects.get(key) || [];
+        arr.push(projectInfo);
+        dateToProjects.set(key, arr);
+      }
+      if (deadline) {
+        const key = this.formatDateString(new Date(deadline));
+        const arr = dateToProjects.get(key) || [];
+        if (!arr.some(x => x.id === projectInfo.id)) {
+          arr.push(projectInfo);
+        }
+        dateToProjects.set(key, arr);
+      }
+    }
+
+    // 生成42天(6周)
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+
+    for (let i = 0; i < 42; i++) {
+      const date = new Date(startDate);
+      date.setDate(startDate.getDate() + i);
+      
+      const dateKey = this.formatDateString(date);
+      const projectsOnDate = dateToProjects.get(dateKey) || [];
+
+      days.push({
+        date,
+        projectCount: projectsOnDate.length,
+        projects: projectsOnDate,
+        isToday: date.getTime() === today.getTime(),
+        isCurrentMonth: date.getMonth() === currentMonth.getMonth()
+      });
+    }
+
+    return {
+      currentMonth,
+      days
+    };
+  }
+
+  /**
+   * 关闭员工详情面板
+   */
+  closeEmployeeDetailPanel(): void {
+    this.showEmployeeDetailPanel = false;
+    this.employeeDetailData = null;
+  }
+
+  /**
+   * 员工详情面板中的项目点击事件
+   */
+  onEmployeeDetailProjectClick(projectId: string): void {
+    console.log('点击项目:', projectId);
+    // 可以在这里实现跳转到项目详情页
+    // this.router.navigate(['/wxwork', cid, 'project', projectId]);
+  }
 }

+ 62 - 20
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -2775,37 +2775,71 @@ export class Dashboard implements OnInit, OnDestroy {
   get pendingApprovalProjects(): Project[] {
     const pending = this.projects.filter(p => {
       const stage = (p.currentStage || '').trim();
-      const data = (p as any).data || {};
-      const approvalStatus = data.approvalStatus;
-      
-      // 调试日志
-      if (stage === '订单分配' || stage === '待审批' || stage === '待确认') {
-        console.log('🔍 检查待审批项目:', {
-          projectId: p.id,
-          projectName: p.name,
-          currentStage: stage,
-          approvalStatus: approvalStatus,
-          data: data,
-          isPending: (stage === '订单分配' && approvalStatus === 'pending')
+      const stageEn = stage.toLowerCase();
+      const data: any = (p as any).data || {};
+      
+      // 🔥 新增:检查顶层的 pendingApproval 字段(备用方案)
+      const topLevelPending = (p as any).pendingApproval === true && (p as any).approvalStage === '订单分配';
+
+      const isOrderPending = (stage === '订单分配') && (data.approvalStatus === 'pending' || topLevelPending);
+      const isLegacyPending = stage === '待审批' || stage === '待确认';
+      const isDeliveryPending = (stage === '交付执行' || stageEn === 'delivery') && (
+        (data.deliveryApproval?.status === 'pending') ||
+        (Array.isArray(data.pendingDeliveryApprovals) && data.pendingDeliveryApprovals.some((x: any) => x?.status === 'pending'))
+      );
+
+      // 🔍 调试:输出所有订单分配阶段的项目
+      if (stage === '订单分配') {
+        console.log('🔍 [订单分配阶段项目]', {
+          id: p.id,
+          name: p.name,
+          stage,
+          'data对象存在': !!data,
+          'data类型': typeof data,
+          'data.approvalStatus': data.approvalStatus,
+          'topLevelPending': (p as any).pendingApproval,
+          'approvalStage': (p as any).approvalStage,
+          'lastOrderSubmitTime': (p as any).lastOrderSubmitTime,
+          '判断结果-isOrderPending': isOrderPending,
+          '判断结果-topLevelPending': topLevelPending
         });
       }
-      
-      // 1. 阶段为"订单分配"且审批状态为 pending
-      // 2. 或者阶段为"待确认"/"待审批"(兼容旧数据)
-      return (stage === '订单分配' && approvalStatus === 'pending') ||
-             stage === '待审批' || 
-             stage === '待确认';
+
+      if (isOrderPending || isLegacyPending || isDeliveryPending) {
+        console.log('✅ [匹配到待审批项目]:', { 
+          id: p.id, 
+          name: p.name, 
+          stage, 
+          dataApprovalStatus: data.approvalStatus,
+          topLevelPending: (p as any).pendingApproval,
+          approvalStage: (p as any).approvalStage,
+          deliveryApproval: data.deliveryApproval, 
+          pendingList: data.pendingDeliveryApprovals 
+        });
+      }
+
+      return isOrderPending || isLegacyPending || isDeliveryPending;
     });
     
     console.log('📋 待审批项目数量:', pending.length);
+    console.log('📊 所有项目总数:', this.projects.length);
+    console.log('📊 订单分配阶段项目数:', this.projects.filter(p => p.currentStage === '订单分配').length);
     return pending;
   }
 
   // 检查项目是否待审批
   isPendingApproval(project: Project): boolean {
     const stage = (project.currentStage || '').trim();
-    const data = (project as any).data || {};
-    return stage === '订单分配' && data.approvalStatus === 'pending';
+    const stageEn = stage.toLowerCase();
+    const data: any = (project as any).data || {};
+    
+    // 🔥 新增:检查顶层的 pendingApproval 字段(备用方案)
+    const topLevelPending = (project as any).pendingApproval === true && (project as any).approvalStage === '订单分配';
+    
+    return (stage === '订单分配' && (data.approvalStatus === 'pending' || topLevelPending)) ||
+           ((stage === '交付执行' || stageEn === 'delivery') &&
+             (data.deliveryApproval?.status === 'pending' ||
+              (Array.isArray(data.pendingDeliveryApprovals) && data.pendingDeliveryApprovals.some((x: any) => x?.status === 'pending'))));
   }
 
   // 🎯 待分配项目(支持中文和英文阶段名称)
@@ -2863,6 +2897,14 @@ export class Dashboard implements OnInit, OnDestroy {
     // 获取公司ID
     const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
     
+    // ✅ 标记从组长看板进入(用于项目详情识别组长身份)
+    try {
+      localStorage.setItem('enterAsTeamLeader', '1');
+      localStorage.setItem('teamLeaderMode', 'true');
+    } catch (e) {
+      console.warn('无法设置 localStorage 标记:', e);
+    }
+    
     // ✅ 修复:跳转到正确的项目详情路由(modules/project 路由)
     // 默认打开订单分配阶段
     this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);

+ 33 - 0
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.html

@@ -71,6 +71,9 @@
               <line x1="3" y1="10" x2="21" y2="10"></line>
             </svg>
             <h4>负载详细日历</h4>
+            <button class="btn-view-designer-calendar" (click)="openDesignerCalendar()" title="查看详细日历">
+              📅 详细日历
+            </button>
           </div>
           
           @if (employeeDetail.calendarData) {
@@ -408,3 +411,33 @@
   </div>
 }
 
+<!-- 设计师详细日历(复用订单分配页组件) -->
+@if (showDesignerCalendar) {
+  <div class="calendar-project-modal-overlay" (click)="closeDesignerCalendar()">
+    <div class="calendar-project-modal" (click)="stopPropagation($event)">
+      <div class="modal-header">
+        <h3>
+          <svg class="header-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M9 11l3 3L22 4"></path>
+            <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
+          </svg>
+          设计师工作日历
+        </h3>
+        <button class="btn-close" (click)="closeDesignerCalendar()">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <line x1="18" y1="6" x2="6" y2="18"></line>
+            <line x1="6" y1="6" x2="18" y2="18"></line>
+          </svg>
+        </button>
+      </div>
+      <div class="modal-body">
+        <app-designer-calendar
+          [designers]="calendarDesigners"
+          [showSingleDesigner]="true"
+          [timeRange]="calendarViewMode">
+        </app-designer-calendar>
+      </div>
+    </div>
+  </div>
+}
+

+ 53 - 1
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts

@@ -1,6 +1,7 @@
 import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { Router } from '@angular/router';
+import { DesignerCalendarComponent, Designer as CalendarDesigner } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
 
 // 员工详情面板数据接口
 export interface EmployeeDetail {
@@ -45,7 +46,7 @@ export interface EmployeeCalendarDay {
 @Component({
   selector: 'app-employee-detail-panel',
   standalone: true,
-  imports: [CommonModule],
+  imports: [CommonModule, DesignerCalendarComponent],
   templateUrl: './employee-detail-panel.html',
   styleUrls: ['./employee-detail-panel.scss']
 })
@@ -72,6 +73,11 @@ export class EmployeeDetailPanelComponent implements OnInit {
   showCalendarProjectList: boolean = false;
   selectedDate: Date | null = null;
   selectedDayProjects: Array<{ id: string; name: string; deadline?: Date }> = [];
+
+  // 设计师详细日历(与订单分配页复用)
+  showDesignerCalendar: boolean = false;
+  calendarDesigners: CalendarDesigner[] = [];
+  calendarViewMode: 'week' | 'month' | 'quarter' = 'month';
   
   constructor(private router: Router) {}
   
@@ -124,6 +130,52 @@ export class EmployeeDetailPanelComponent implements OnInit {
     this.projectClick.emit(projectId);
     this.closeCalendarProjectList();
   }
+
+  /**
+   * 打开“设计师详细日历”弹窗(复用订单分配页的日历)
+   * 将当前员工信息适配为 DesignerCalendar 组件的数据结构
+   */
+  openDesignerCalendar(): void {
+    if (!this.employeeDetail) return;
+
+    const name = this.employeeDetail.name || '设计师';
+    const currentProjects = this.employeeDetail.currentProjects || 0;
+
+    // 将已有的 employeeDetail.calendarData 映射为日历事件(粗粒度:有项目视为当日有工作)
+    const upcomingEvents: CalendarDesigner['upcomingEvents'] = [];
+    const days = this.employeeDetail.calendarData?.days || [];
+    for (const day of days) {
+      if (day.projectCount > 0) {
+        upcomingEvents.push({
+          id: `${day.date.getTime()}`,
+          date: day.date,
+          title: `${day.projectCount}个项目`,
+          type: 'project',
+          duration: 6
+        });
+      }
+    }
+
+    // 适配为日历组件的设计师数据(单人视图)
+    this.calendarDesigners = [{
+      id: this.employeeDetail.profileId || name,
+      name,
+      groupId: '',
+      groupName: '',
+      isLeader: false,
+      status: currentProjects >= 3 ? 'busy' : 'available',
+      currentProjects,
+      upcomingEvents,
+      workload: Math.min(100, currentProjects * 30)
+    }];
+
+    this.showDesignerCalendar = true;
+  }
+
+  closeDesignerCalendar(): void {
+    this.showDesignerCalendar = false;
+    this.calendarDesigners = [];
+  }
   
   /**
    * 刷新问卷

+ 35 - 0
src/app/pages/team-leader/services/designer.service.ts

@@ -279,10 +279,41 @@ export class DesignerService {
       query.equalTo('company', this.cid);
       query.notEqualTo('isDeleted', true);
       query.include('assignee', 'contact');
+      
+      // 🔥 关键修复:确保查询返回 data 字段和审批相关字段
+      // Parse Server 可能不会默认返回所有字段,需要显式选择
+      query.select(
+        'title', 'projectType', 'renderType', 'deadline', 'demoday', 
+        'description', 'spaceType', 'status', 'currentStage', 
+        'assignee', 'contact', 'company', 'createdAt', 'updatedAt',
+        'data',  // ✨ 显式选择 data 字段
+        'pendingApproval',  // 🔥 新增:待审批标记(顶层字段)
+        'approvalStage',    // 🔥 新增:审批阶段(顶层字段)
+        'lastOrderSubmitTime'  // 🔥 新增:最后提交时间
+      );
+      
       query.descending('updatedAt');
       query.limit(1000);
       
+      console.log('🔍 [DesignerService] 开始查询项目列表...');
       const projects = await query.find();
+      console.log(`✅ [DesignerService] 查询到 ${projects.length} 个项目`);
+      
+      // 🔥 验证每个项目是否包含 data 字段
+      projects.forEach((p, index) => {
+        const data = p.get('data');
+        const currentStage = p.get('currentStage');
+        if (currentStage === '订单分配') {
+          console.log(`🔍 [DesignerService] 项目 ${index + 1}: ${p.get('title')}`, {
+            id: p.id,
+            currentStage: currentStage,
+            hasData: !!data,
+            dataType: typeof data,
+            approvalStatus: data?.approvalStatus,
+            keys: data ? Object.keys(data) : []
+          });
+        }
+      });
       
       return projects.map((p: any) => this.transformProject(p));
     } catch (error) {
@@ -361,6 +392,10 @@ export class DesignerService {
       qualityRating: data.qualityRating || 'pending',
       lastCustomerFeedback: data.lastCustomerFeedback || '',
       data,
+      // 🔥 关键修复:传递顶层的审批相关字段
+      pendingApproval: project.get('pendingApproval'),
+      approvalStage: project.get('approvalStage'),
+      lastOrderSubmitTime: project.get('lastOrderSubmitTime'),
       project
     };
   }

+ 28 - 6
src/app/services/case.service.ts

@@ -226,6 +226,21 @@ export class CaseService {
     data?: any;
   }): Promise<any> {
     try {
+      // 🔒 去重:同一公司、同一项目仅允许存在一个未删除的案例
+      try {
+        const existingQuery = new Parse.Query('Case');
+        existingQuery.equalTo('company', this.getCompanyPointer());
+        existingQuery.equalTo('project', this.getPointer('Project', data.projectId));
+        existingQuery.notEqualTo('isDeleted', true);
+        const existing = await existingQuery.first();
+        if (existing) {
+          console.log(`⚠️ 案例已存在,直接复用: ${existing.id} (project=${data.projectId})`);
+          return this.formatCase(existing);
+        }
+      } catch (e) {
+        console.warn('⚠️ 去重查询失败(忽略,继续创建):', e);
+      }
+
       const Case = Parse.Object.extend('Case');
       const newCase = new Case();
 
@@ -579,6 +594,13 @@ export class CaseService {
     const info = caseObj.get('info') || {};
     const data = caseObj.get('data') || {};
 
+    // 🔥 安全访问 Parse 对象属性的辅助函数
+    const safeGet = (obj: any, field: string, defaultValue: any = '') => {
+      if (!obj) return defaultValue;
+      if (typeof obj.get === 'function') return obj.get(field) || defaultValue;
+      return obj[field] || defaultValue;
+    };
+
     return {
       // 系统字段
       id: caseObj.id,
@@ -593,16 +615,16 @@ export class CaseService {
 
       // 关联关系
       projectId: project?.id || '',
-      projectName: project?.get('title') || '',
+      projectName: safeGet(project, 'title'),
       designerId: designer?.id || '',
-      designer: designer?.get('name') || '',
-      designerAvatar: designer?.get('data')?.avatar || '',
+      designer: safeGet(designer, 'name'),
+      designerAvatar: safeGet(designer, 'data')?.avatar || '',
       teamId: team?.id || '',
-      team: team?.get('name') || '',
+      team: safeGet(team, 'name'),
       storeId: store?.id || '',
-      storeName: store?.get('name') || '',
+      storeName: safeGet(store, 'name'),
       addressId: address?.id || '',
-      addressDetail: address?.get('address') || '',
+      addressDetail: safeGet(address, 'address'),
 
       // 媒体资源
       coverImage: caseObj.get('coverImage') || '',

+ 116 - 56
src/modules/project/pages/project-detail/stages/stage-delivery.component.html

@@ -206,9 +206,14 @@
                 <span class="type-name">{{ type.name }}</span>
                 <span class="type-description">{{ type.description }}</span>
               </div>
-              @if (getCurrentTypeFileCount(activeProductId, type.id) > 0) {
-                <span class="file-count-badge">{{ getCurrentTypeFileCount(activeProductId, type.id) }}</span>
-              }
+              <div class="type-badges">
+                @if (getCurrentTypeFileCount(activeProductId, type.id) > 0) {
+                  <span class="file-count-badge">{{ getCurrentTypeFileCount(activeProductId, type.id) }}</span>
+                }
+                @if (getTypeUnverifiedFileCount(activeProductId, type.id) > 0) {
+                  <span class="unverified-badge">{{ getTypeUnverifiedFileCount(activeProductId, type.id) }} 未验证</span>
+                }
+              </div>
             </div>
           }
         </div>
@@ -230,22 +235,24 @@
                 <p>{{ getDeliveryTypeDescription(activeDeliveryType) }}</p>
               </div>
               @if (canEdit) {
+                <input
+                  type="file"
+                  multiple
+                  (change)="uploadDeliveryFile($event, activeProductId, activeDeliveryType)"
+                  [accept]="'image/*,.pdf,.dwg,.dxf,.skp,.max'"
+                  [disabled]="uploadingDeliveryFiles"
+                  hidden
+                  #deliveryFileInput />
+                
                 <button
                   class="upload-button"
                   [disabled]="uploadingDeliveryFiles"
-                  (click)="triggerFileInput('delivery-file-' + activeProductId + '-' + activeDeliveryType)">
+                  (click)="deliveryFileInput.click()">
                   <svg class="icon" width="20" height="20" viewBox="0 0 24 24">
                     <path fill="currentColor" d="M11 15h2V9h3l-4-5l-4 5h3Zm-7 7c-.55 0-1.02-.196-1.413-.587A1.928 1.928 0 0 1 2 20V8c0-.55.196-1.02.587-1.412A1.93 1.93 0 0 1 4 6h4l2-2h4l-2 2H8.83L7.5 7.5H4V20h16V8h-6V6h6c.55 0 1.02.196 1.413.588C21.803 6.98 22 7.45 22 8v12c0 .55-.196 1.02-.587 1.413A1.928 1.928 0 0 1 20 22Z"/>
                   </svg>
                   <span>选择文件上传</span>
                 </button>
-                <input
-                  type="file"
-                  multiple
-                  [id]="'delivery-file-' + activeProductId + '-' + activeDeliveryType"
-                  (change)="uploadDeliveryFile($event, activeProductId, activeDeliveryType)"
-                  [accept]="'image/*,.pdf,.dwg,.dxf,.skp,.max'"
-                  hidden />
               }
             </div>
 
@@ -269,11 +276,11 @@
             </div>
             <div class="files-grid">
               @for (file of getProductDeliveryFiles(activeProductId, activeDeliveryType); track file.id) {
-                <div class="file-card">
+                <div class="file-card" [class.has-approval-issue]="file.approvalStatus === 'rejected'">
                   <!-- 文件预览 -->
                   <div class="file-preview" (click)="previewFile(file)">
                     @if (isImageFile(file.name)) {
-                      <img [src]="file.url" [alt]="file.name" class="preview-image" />
+                      <img [src]="file.url" [alt]="file.name" class="preview-image" (error)="onImageError($event)" />
                     } @else {
                       <div class="file-type-icon">
                         <svg class="icon" width="48" height="48" viewBox="0 0 24 24">
@@ -281,53 +288,85 @@
                         </svg>
                       </div>
                     }
+                    
+                    <!-- 审批状态角标 -->
+                    <div class="approval-corner-badge" [ngClass]="'badge-' + file.approvalStatus">
+                      @if (file.approvalStatus === 'unverified') {
+                        <span>⏳</span>
+                      } @else if (file.approvalStatus === 'pending') {
+                        <span>🔍</span>
+                      } @else if (file.approvalStatus === 'approved') {
+                        <span>✅</span>
+                      } @else if (file.approvalStatus === 'rejected') {
+                        <span>❌</span>
+                      }
+                    </div>
+                    
                     <div class="file-overlay">
                       <svg class="icon" width="24" height="24" viewBox="0 0 24 24">
                         <path fill="white" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
                       </svg>
                     </div>
+                    
+                    <!-- 删除按钮(参考售后归档样式) -->
+                    @if (canEdit) {
+                      <button class="delete-btn" (click)="deleteDeliveryFile(activeProductId, activeDeliveryType, file.id); $event.stopPropagation()">
+                        <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                          <path d="M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
+                          <path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M80 112h352"/>
+                          <path d="M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40M256 176v224M184 176l8 224M328 176l-8 224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/>
+                        </svg>
+                      </button>
+                    }
                   </div>
 
-                  <!-- 文件信息 -->
+                  <!-- 文件信息(参考售后归档样式) -->
                   <div class="file-info">
                     <div class="file-name" [title]="file.name">{{ file.name }}</div>
                     <div class="file-meta">
                       <span class="file-size">{{ formatFileSize(file.size) }}</span>
-                      <span class="file-time">{{ file.uploadTime | date: 'MM-dd HH:mm' }}</span>
+                      <span class="file-time">{{ file.uploadTime | date: 'yyyy-MM-dd HH:mm' }}</span>
                     </div>
                     @if (file.uploadedBy) {
-                      <div class="file-uploader">上传人: {{ file.uploadedBy }}</div>
-                    }
-                  </div>
-
-                  <!-- 文件操作 -->
-                  <div class="file-actions">
-                    <button
-                      class="action-button preview"
-                      (click)="previewFile(file)"
-                      title="预览">
-                      <svg class="icon" width="18" height="18" viewBox="0 0 24 24">
-                        <path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
-                      </svg>
-                    </button>
-                    <button
-                      class="action-button download"
-                      (click)="downloadFile(file)"
-                      title="下载">
-                      <svg class="icon" width="18" height="18" viewBox="0 0 24 24">
-                        <path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
-                      </svg>
-                    </button>
-                    @if (canEdit) {
-                      <button
-                        class="action-button delete"
-                        (click)="deleteDeliveryFile(activeProductId, activeDeliveryType, file.id)"
-                        title="删除">
-                        <svg class="icon" width="18" height="18" viewBox="0 0 24 24">
-                          <path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
-                        </svg>
-                      </button>
+                      <div class="file-uploader">上传: {{ file.uploadedBy }}</div>
                     }
+                    
+                    <!-- ✨ 审批状态显示(参考售后归档红框样式) -->
+                    <div class="file-approval-status" [ngClass]="'has-' + file.approvalStatus">
+                      <div class="status-row">
+                        <span class="status-badge" [ngClass]="getApprovalStatusClass(file.approvalStatus)">
+                          {{ getApprovalStatusText(file.approvalStatus) }}
+                        </span>
+                      </div>
+                      
+                      @if (file.approvalStatus === 'approved') {
+                        <div class="approval-details">
+                          @if (file.approvedBy) {
+                            <div class="approval-info">
+                              <svg class="icon-small" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                                <path fill="currentColor" d="M258.9 48C141.92 46.42 46.42 141.92 48 258.9c1.56 112.19 92.91 203.54 205.1 205.1 117 1.6 212.48-93.9 210.88-210.88C462.44 140.91 371.09 49.56 258.9 48zm-16.79 192.47l51.55 51.55a12 12 0 010 17l-5.66 5.66a12 12 0 01-17 0l-51.55-51.55-51.55 51.55a12 12 0 01-17 0l-5.66-5.66a12 12 0 010-17l51.55-51.55-51.55-51.55a12 12 0 010-17l5.66-5.66a12 12 0 0117 0l51.55 51.55 51.55-51.55a12 12 0 0117 0l5.66 5.66a12 12 0 010 17z"/>
+                              </svg>
+                              <span>审批: {{ file.approvedBy }}</span>
+                            </div>
+                          }
+                          @if (file.approvedAt) {
+                            <div class="approval-time">{{ file.approvedAt | date: 'yyyy-MM-dd HH:mm' }}</div>
+                          }
+                        </div>
+                      }
+                      
+                      @if (file.approvalStatus === 'rejected' && file.rejectionReason) {
+                        <div class="rejection-reason">
+                          <div class="rejection-header">
+                            <svg class="icon-small" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+                              <path fill="currentColor" d="M85.57 446.25h340.86a32 32 0 0028.17-47.17L284.18 82.58c-12.09-22.44-44.27-22.44-56.36 0L57.4 399.08a32 32 0 0028.17 47.17z"/>
+                            </svg>
+                            <span class="label">驳回原因</span>
+                          </div>
+                          <p class="reason">{{ file.rejectionReason }}</p>
+                        </div>
+                      }
+                    </div>
                   </div>
                 </div>
               }
@@ -342,9 +381,19 @@
               <h4>暂无{{ getDeliveryTypeName(activeDeliveryType) }}文件</h4>
               <p>{{ getDeliveryTypeDescription(activeDeliveryType) }}</p>
               @if (canEdit) {
+                <input
+                  type="file"
+                  multiple
+                  (change)="uploadDeliveryFile($event, activeProductId, activeDeliveryType)"
+                  [accept]="'image/*,.pdf,.dwg,.dxf,.skp,.max'"
+                  [disabled]="uploadingDeliveryFiles"
+                  hidden
+                  #deliveryFileInputEmpty />
+                
                 <button
                   class="upload-button-primary"
-                  (click)="triggerFileInput('delivery-file-' + activeProductId + '-' + activeDeliveryType)">
+                  [disabled]="uploadingDeliveryFiles"
+                  (click)="deliveryFileInputEmpty.click()">
                   <svg class="icon" width="20" height="20" viewBox="0 0 24 24">
                     <path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
                   </svg>
@@ -383,19 +432,30 @@
       </div>
     }
 
-    <!-- 完成交付按钮 -->
-    @if (canEdit && projectProducts.length > 0) {
+    <!-- 操作按钮(参考售后归档样式) -->
+    @if (!loading && !isAdminView) {
       <div class="action-buttons">
         <button
-          class="btn btn-primary"
-          (click)="completeDelivery()"
-          [disabled]="saving">
-          <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
-            <path fill="currentColor" d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm-38 312.38L137.4 280.8a24 24 0 0133.94-33.94l50.2 50.2 95.74-95.74a24 24 0 0133.94 33.94z"/>
-          </svg>
-          完成交付
+          class="btn btn-primary btn-block btn-large"
+          (click)="submitDeliveryForApproval()"
+          [disabled]="!canEdit || saving || projectProducts.length === 0">
+          @if (saving) {
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M304 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zm0 416a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM48 304a48 48 0 1 0 0-96 48 48 0 1 0 0 96zm464-48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM142.9 437A48 48 0 1 0 75 369.1 48 48 0 1 0 142.9 437zm0-294.2A48 48 0 1 0 75 75a48 48 0 1 0 67.9 67.9zM369.1 437A48 48 0 1 0 437 369.1 48 48 0 1 0 369.1 437z"/>
+            </svg>
+            <span>提交中...</span>
+          } @else {
+            <svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M476 3L36.8 230.2c-13 7-12 25.8 1.7 31.1L176 308l27 134.8c3 14.8 21 20.6 32.2 10.2l59.2-55.9 98.2 73.7c10.5 7.9 25.6 2.7 29.1-9.7L509 17.6C512.7 4.8 493.9-5.2 476 3zM214.4 453.1l-20.5-102.3 73.7 55.3-53.2 47z"/>
+            </svg>
+            <span>提交审批</span>
+          }
         </button>
       </div>
+      
+      @if (projectProducts.length === 0) {
+        <div class="button-tip">请先在"确认需求"阶段添加项目空间</div>
+      }
     }
   }
 </div>

+ 404 - 19
src/modules/project/pages/project-detail/stages/stage-delivery.component.scss

@@ -1171,48 +1171,419 @@
     }
   }
 
-  // 操作按钮
+  // 操作按钮(参考售后归档样式)
   .action-buttons {
-    margin-top: 32px;
-    padding: 0 24px 24px;
-    display: flex;
-    justify-content: center;
+    margin-top: 24px;
+    padding: 16px;
 
     .btn {
-      padding: 14px 32px;
-      border: none;
-      border-radius: 12px;
-      font-size: 16px;
-      font-weight: 600;
-      cursor: pointer;
-      transition: all 0.3s;
       display: flex;
       align-items: center;
-      gap: 8px;
-      min-width: 200px;
       justify-content: center;
+      gap: 12px;
+      padding: 18px 48px;
+      border-radius: 14px;
+      font-size: 17px;
+      font-weight: 700;
+      cursor: pointer;
+      transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+      border: none;
+      outline: none;
+      min-height: 58px;
+      width: 100%;
+      max-width: 400px;
+      position: relative;
+      overflow: hidden;
+      letter-spacing: 0.5px;
+      text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
 
       .icon {
-        width: 20px;
-        height: 20px;
+        width: 22px;
+        height: 22px;
+        filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
+        transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+      }
+
+      // 多层按钮涟漪效果
+      &::before {
+        content: '';
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        width: 0;
+        height: 0;
+        border-radius: 50%;
+        background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 70%);
+        transform: translate(-50%, -50%);
+        transition: width 0.7s cubic-bezier(0.4, 0, 0.2, 1), height 0.7s cubic-bezier(0.4, 0, 0.2, 1);
+      }
+
+      // 光泽层
+      &::after {
+        content: '';
+        position: absolute;
+        top: -50%;
+        left: -50%;
+        width: 200%;
+        height: 200%;
+        background: linear-gradient(
+          120deg,
+          transparent 0%,
+          transparent 40%,
+          rgba(255, 255, 255, 0.15) 50%,
+          transparent 60%,
+          transparent 100%
+        );
+        transform: translateX(-100%);
+        transition: transform 0.6s ease;
+      }
+
+      &:active:not(:disabled)::before {
+        width: 400px;
+        height: 400px;
+        transition: width 0.3s, height 0.3s;
+      }
+
+      &:hover:not(:disabled)::after {
+        transform: translateX(100%);
       }
 
+      // 提交审批按钮(参考售后归档样式)
       &.btn-primary {
-        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        background: var(--primary-color, #3880ff);
         color: white;
 
         &:hover:not(:disabled) {
+          background: #2f6ce5;
           transform: translateY(-2px);
-          box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
+        }
+
+        &:active:not(:disabled) {
+          transform: translateY(0);
+        }
+      }
+      
+      &.btn-block {
+        width: 100%;
+      }
+      
+      &.btn-large {
+        padding: 16px 24px;
+        font-size: 15px;
         }
 
         &:disabled {
-          opacity: 0.6;
+        opacity: 0.5;
           cursor: not-allowed;
+        pointer-events: none;
+        box-shadow: none;
+        transform: none;
+      }
+
+      .icon-spin {
+        animation: spin 1s linear infinite;
+      }
+    }
+
+    // 移动端优化
+    @media (max-width: 768px) {
+      padding: 24px 16px;
+      margin: 32px auto 24px;
+      border-radius: 16px;
+      
+      .btn {
+        max-width: 100%;
+        width: 100%;
+        padding: 16px 36px;
+        font-size: 16px;
+        min-height: 54px;
+
+        .icon {
+          width: 20px;
+          height: 20px;
+        }
+      }
+    }
+
+    @media (max-width: 480px) {
+      padding: 20px 12px;
+      margin: 24px auto 20px;
+      
+      .btn {
+        padding: 14px 28px;
+        font-size: 15px;
+        min-height: 50px;
+        gap: 10px;
+
+        .icon {
+          width: 18px;
+          height: 18px;
         }
       }
     }
   }
+
+  @keyframes spin {
+    from { transform: rotate(0deg); }
+    to { transform: rotate(360deg); }
+  }
+
+  .button-tip {
+    margin-top: 16px;
+    padding: 12px 20px;
+    text-align: center;
+    background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
+    border-left: 4px solid #ffc107;
+    border-radius: 8px;
+    color: #856404;
+    font-size: 14px;
+    box-shadow: 0 2px 8px rgba(255, 193, 7, 0.15);
+    
+    &::before {
+      content: '💡 ';
+    }
+  }
+}
+
+// ✨ 审批状态样式(参考售后归档样式优化 - 红框样式)
+.file-approval-status {
+  margin-top: 12px;
+  padding: 12px;
+  background: #f5f5f5;
+  border-radius: 8px;
+  font-size: 12px;
+  line-height: 1.5;
+  border-left: 4px solid #e0e0e0;
+
+  .status-row { 
+    margin-bottom: 8px; 
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+
+  .status-badge {
+    display: inline-block;
+    padding: 4px 12px;
+    border-radius: 12px;
+    font-size: 12px;
+    font-weight: 600;
+    flex-shrink: 0;
+    
+    &.status-unverified {
+      background: rgba(255, 196, 9, 0.1);
+      color: #ffc409;
+      border: 1px solid #ffc409;
+    }
+    
+    &.status-pending {
+      background: rgba(56, 128, 255, 0.1);
+      color: #3880ff;
+      border: 1px solid #3880ff;
+    }
+    
+    &.status-approved {
+      background: rgba(45, 211, 111, 0.1);
+      color: #2dd36f;
+      border: 1px solid #2dd36f;
+    }
+    
+    &.status-rejected {
+      background: rgba(235, 68, 90, 0.1);
+      color: #eb445a;
+      border: 1px solid #eb445a;
+    }
+  }
+  
+  // 根据状态改变整个区域的边框颜色
+  &.has-unverified { border-left-color: #ffc409; }
+  &.has-pending { border-left-color: #3880ff; }
+  &.has-approved { border-left-color: #2dd36f; background: rgba(45, 211, 111, 0.05); }
+  &.has-rejected { border-left-color: #eb445a; background: rgba(235, 68, 90, 0.05); }
+
+  .approval-details {
+    margin-top: 6px;
+    
+    .approval-info {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      font-size: 11px;
+      color: #155724;
+      margin-bottom: 4px;
+      
+      .icon-small {
+        width: 14px;
+        height: 14px;
+      }
+    }
+    
+    .approval-time {
+      font-size: 10px;
+      color: #6c757d;
+      margin-left: 20px;
+    }
+  }
+
+  .rejection-reason {
+    margin-top: 8px;
+    padding: 10px;
+    background: #fff5f5;
+    border-left: 3px solid #e74c3c;
+    border-radius: 4px;
+
+    .rejection-header {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      margin-bottom: 6px;
+      
+      .icon-small {
+        width: 16px;
+        height: 16px;
+        color: #eb445a;
+      }
+      
+      .label {
+        font-weight: 600;
+        color: #721c24;
+        font-size: 12px;
+      }
+    }
+
+    .reason {
+      margin: 0;
+      color: #333;
+      font-size: 12px;
+      line-height: 1.5;
+      padding-left: 22px;
+    }
+  }
+}
+
+// 未验证文件徽章
+.unverified-badge {
+  display: inline-block;
+  padding: 2px 8px;
+  background: #ff9f43;
+  color: white;
+  border-radius: 10px;
+  font-size: 11px;
+  font-weight: 600;
+  margin-left: 8px;
+  animation: pulse 2s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.7;
+  }
+}
+
+// 类型徽章容器
+.type-badges {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+// ✨ 文件卡片增强样式(参考售后归档)
+.file-card {
+  transition: all 0.3s ease;
+  
+  &.has-approval-issue {
+    border: 2px solid #eb445a;
+    animation: shake 0.5s ease-in-out;
+  }
+  
+  .file-preview {
+    position: relative;
+    
+    // 审批状态角标
+    .approval-corner-badge {
+      position: absolute;
+      top: 8px;
+      right: 8px;
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 16px;
+      background: rgba(255, 255, 255, 0.95);
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+      z-index: 2;
+      
+      &.badge-unverified {
+        background: linear-gradient(135deg, #fff3cd, #ffc107);
+      }
+      
+      &.badge-pending {
+        background: linear-gradient(135deg, #d1ecf1, #17a2b8);
+      }
+      
+      &.badge-approved {
+        background: linear-gradient(135deg, #d4edda, #28a745);
+      }
+      
+      &.badge-rejected {
+        background: linear-gradient(135deg, #f8d7da, #dc3545);
+      }
+    }
+    
+    // 删除按钮(参考售后归档样式)
+    .delete-btn {
+      position: absolute;
+      top: 8px;
+      left: 8px;
+      width: 32px;
+      height: 32px;
+      border: none;
+      background: rgba(235, 68, 90, 0.9);
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      transition: all 0.3s;
+      padding: 0;
+      z-index: 3;
+      opacity: 0;
+      
+      &:hover {
+        background: #eb445a;
+        transform: scale(1.1);
+      }
+      
+      .icon {
+        width: 18px;
+        height: 18px;
+        color: white;
+      }
+    }
+  }
+  
+  &:hover {
+    .delete-btn {
+      opacity: 1;
+    }
+  }
+}
+
+@keyframes shake {
+  0%, 100% {
+    transform: translateX(0);
+  }
+  25% {
+    transform: translateX(-5px);
+  }
+  75% {
+    transform: translateX(5px);
+  }
 }
 
 @media (max-width: 480px) {
@@ -1232,3 +1603,17 @@
     }
   }
 }
+
+// 🎨 按钮加载动画
+.icon-spin {
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}

+ 361 - 30
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
+import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
@@ -9,6 +9,11 @@ import { WxworkAuth } from 'fmode-ng/social';
 
 const Parse = FmodeParse.with('nova');
 
+/**
+ * 审批状态类型
+ */
+type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'unverified';
+
 /**
  * 交付文件接口
  */
@@ -23,6 +28,11 @@ interface DeliveryFile {
   deliveryType: 'white_model' | 'soft_decor' | 'rendering' | 'post_process';
   stage: string;
   projectFile?: FmodeObject;
+  // 审批相关字段
+  approvalStatus: ApprovalStatus; // 审批状态
+  approvedBy?: string; // 审批人
+  approvedAt?: Date; // 审批时间
+  rejectionReason?: string; // 驳回原因
 }
 
 /**
@@ -42,7 +52,7 @@ interface DeliveryFile {
   styleUrls: ['./stage-delivery.component.scss'],
   changeDetection: ChangeDetectionStrategy.OnPush
 })
-export class StageDeliveryComponent implements OnInit {
+export class StageDeliveryComponent implements OnInit, OnDestroy {
   @Input() project: FmodeObject | null = null;
   @Input() customer: FmodeObject | null = null;
   @Input() currentUser: FmodeObject | null = null;
@@ -112,6 +122,24 @@ export class StageDeliveryComponent implements OnInit {
   loading: boolean = true;
   saving: boolean = false;
 
+  // 视图上下文:管理员后台查看时隐藏操作按钮
+  isAdminView: boolean = false;
+
+  // 状态自动刷新定时器
+  private statusRefreshTimer: any = null;
+
+  // 图片占位(base64 SVG,避免外链失败)
+  private readonly fallbackImageDataUrl: string =
+    'data:image/svg+xml;utf8,' +
+    encodeURIComponent(
+      '<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300">' +
+      '<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop stop-color="#eef2ff"/><stop offset="1" stop-color="#e9ecff"/></linearGradient></defs>' +
+      '<rect width="400" height="300" fill="url(#g)"/>' +
+      '<g fill="#667eea" opacity="0.7"><circle cx="70" cy="60" r="6"/><circle cx="120" cy="80" r="4"/><circle cx="340" cy="210" r="5"/></g>' +
+      '<text x="200" y="160" text-anchor="middle" font-size="18" fill="#64748b">暂无预览</text>' +
+      '</svg>'
+    );
+
   constructor(
     private route: ActivatedRoute,
     private cdr: ChangeDetectorRef,
@@ -122,9 +150,28 @@ export class StageDeliveryComponent implements OnInit {
   async ngOnInit() {
     // 从路由或Input获取参数
     this.cid = this.route.parent?.snapshot.paramMap.get('cid') || '';
+    // 判断是否为管理员后台视图(/admin 路由)
+    try {
+      const path = (typeof window !== 'undefined') ? window.location.pathname : '';
+      this.isAdminView = path.includes('/admin/');
+    } catch {}
     this.projectId = this.route.parent?.snapshot.paramMap.get('projectId') || '';
 
     await this.loadData();
+
+    // 周期性刷新交付文件以同步组长端审批状态
+    if (!this.statusRefreshTimer) {
+      this.statusRefreshTimer = setInterval(() => {
+        this.loadDeliveryFiles();
+      }, 15000); // 15s 刷新一次
+    }
+  }
+
+  ngOnDestroy(): void {
+    if (this.statusRefreshTimer) {
+      clearInterval(this.statusRefreshTimer);
+      this.statusRefreshTimer = null;
+    }
   }
 
   /**
@@ -190,12 +237,25 @@ export class StageDeliveryComponent implements OnInit {
         this.activeProductId = this.projectProducts[0].id;
       }
 
-      console.log(`已加载 ${this.projectProducts.length} 个场景Product`);
+      console.log(`✅ 已加载 ${this.projectProducts.length} 个场景Product`);
+      console.log('📊 canEdit:', this.canEdit);
+      console.log('📊 projectProducts.length:', this.projectProducts.length);
+      console.log('📊 显示完成交付按钮条件:', this.canEdit && this.projectProducts.length > 0);
     } catch (error) {
       console.error('加载项目场景失败:', error);
     }
   }
 
+  /**
+   * 图片加载失败时使用内置占位图,避免外链报错
+   */
+  onImageError(event: Event): void {
+    const img = event.target as HTMLImageElement;
+    if (img && img.src !== this.fallbackImageDataUrl) {
+      img.src = this.fallbackImageDataUrl;
+    }
+  }
+
   /**
    * 加载交付文件 (从ProjectFile表按category查询)
    */
@@ -231,18 +291,26 @@ export class StageDeliveryComponent implements OnInit {
           });
 
           // 转换为DeliveryFile格式
-          this.deliveryFiles[product.id][deliveryType.id as keyof typeof this.deliveryFiles[typeof product.id]] = productFiles.map(projectFile => ({
-            id: projectFile.id || '',
-            url: projectFile.get('fileUrl') || '',
-            name: projectFile.get('fileName') || '',
-            size: projectFile.get('fileSize') || 0,
-            uploadTime: projectFile.createdAt || new Date(),
-            uploadedBy: projectFile.get('uploadedBy')?.get('name') || '',
-            productId: product.id,
-            deliveryType: deliveryType.id as any,
-            stage: 'delivery',
-            projectFile: projectFile
-          }));
+          this.deliveryFiles[product.id][deliveryType.id as keyof typeof this.deliveryFiles[typeof product.id]] = productFiles.map(projectFile => {
+            const fileData = projectFile.get('data') || {};
+            return {
+              id: projectFile.id || '',
+              url: projectFile.get('fileUrl') || '',
+              name: projectFile.get('fileName') || '',
+              size: projectFile.get('fileSize') || 0,
+              uploadTime: projectFile.createdAt || new Date(),
+              uploadedBy: projectFile.get('uploadedBy')?.get('name') || '',
+              productId: product.id,
+              deliveryType: deliveryType.id as any,
+              stage: 'delivery',
+              projectFile: projectFile,
+              // 审批状态信息
+              approvalStatus: fileData.approvalStatus || 'unverified',
+              approvedBy: fileData.approvedBy,
+              approvedAt: fileData.approvedAt ? new Date(fileData.approvedAt) : undefined,
+              rejectionReason: fileData.rejectionReason
+            };
+          });
         }
       }
 
@@ -319,7 +387,11 @@ export class StageDeliveryComponent implements OnInit {
           {
             deliveryType: deliveryType,
             productId: productId,
-            uploadedFor: 'delivery_execution'
+            uploadedFor: 'delivery_execution',
+            // ✨ 新增:设置初始审批状态为"未验证"
+            approvalStatus: 'unverified',
+            uploadedByName: this.currentUser?.get('name') || '',
+            uploadedById: this.currentUser?.id || ''
           },
           (progress) => {
             // 计算总体进度
@@ -340,7 +412,9 @@ export class StageDeliveryComponent implements OnInit {
           productId: productId,
           deliveryType: deliveryType as any,
           stage: 'delivery',
-          projectFile: projectFile
+          projectFile: projectFile,
+          // ✨ 新增:初始审批状态
+          approvalStatus: 'unverified'
         };
 
         // 添加到对应类型的数组中
@@ -355,6 +429,11 @@ export class StageDeliveryComponent implements OnInit {
       this.cdr.markForCheck();
       console.log(`成功上传 ${uploadedFiles} 个交付文件`);
 
+      // ✨ 上传成功后通知组长审批
+      if (uploadedFiles > 0) {
+        await this.notifyTeamLeaderForApproval(uploadedFiles, deliveryType);
+      }
+
     } catch (error) {
       console.error('上传交付文件失败:', error);
      window?.fmode?.alert('文件上传失败,请重试');
@@ -365,6 +444,73 @@ export class StageDeliveryComponent implements OnInit {
     }
   }
 
+  /**
+   * 通知组长审批交付文件
+   */
+  async notifyTeamLeaderForApproval(fileCount: number, deliveryType: string): Promise<void> {
+    try {
+      if (!this.project || !this.cid) return;
+
+      const projectTitle = this.project.get('title') || '项目';
+      const typeName = this.getDeliveryTypeName(deliveryType);
+      const uploaderName = this.currentUser?.get('name') || '设计师';
+
+      // 构建审批页面URL(组长端)
+      const approvalUrl = `https://yinsanse.fmode.cn/wxwork/${this.cid}/team-leader/delivery-approval?projectId=${this.project.id}&type=${deliveryType}`;
+
+      // 发送企业微信消息
+      const WxworkSDK = (window as any).WxworkSDK;
+      if (WxworkSDK) {
+        const sdk = new WxworkSDK(this.cid);
+        
+        // 查询设计师组长
+        const teamLeaderQuery = new Parse.Query('Profile');
+        teamLeaderQuery.equalTo('roleName', '设计组长');
+        teamLeaderQuery.limit(10);
+        const teamLeaders = await teamLeaderQuery.find();
+
+        for (const leader of teamLeaders) {
+          const userId = leader.get('wxworkUserId');
+          if (!userId) continue;
+
+          await sdk.sendTextMessage(
+            userId,
+            `📦 交付物待审批提醒\n\n` +
+            `项目:${projectTitle}\n` +
+            `类型:${typeName}\n` +
+            `上传人:${uploaderName}\n` +
+            `文件数:${fileCount} 个\n` +
+            `状态:待审批\n\n` +
+            `请点击查看并审批:\n${approvalUrl}`
+          );
+        }
+
+        console.log(`✅ 已通知 ${teamLeaders.length} 位组长审批`);
+      }
+
+      // 在项目data中添加待审批标记
+      const data = this.project.get('data') || {};
+      if (!data.pendingDeliveryApprovals) {
+        data.pendingDeliveryApprovals = [];
+      }
+      
+      data.pendingDeliveryApprovals.push({
+        type: deliveryType,
+        fileCount: fileCount,
+        uploadedBy: uploaderName,
+        uploadedAt: new Date().toISOString(),
+        status: 'pending'
+      });
+
+      this.project.set('data', data);
+      await this.project.save();
+
+    } catch (error) {
+      console.error('通知组长失败:', error);
+      // 通知失败不影响主流程,只记录错误
+    }
+  }
+
   /**
    * 删除交付文件
    */
@@ -440,15 +586,6 @@ export class StageDeliveryComponent implements OnInit {
     return type?.description || '';
   }
 
-  /**
-   * 触发文件输入点击
-   */
-  triggerFileInput(inputId: string): void {
-    const element = document.getElementById(inputId) as HTMLInputElement;
-    if (element) {
-      element.click();
-    }
-  }
 
   /**
    * 格式化文件大小
@@ -494,7 +631,53 @@ export class StageDeliveryComponent implements OnInit {
   }
 
   /**
-   * 完成交付
+   * 保存草稿
+   */
+  async saveDraft(): Promise<void> {
+    if (!this.project || !this.canEdit) return;
+
+    try {
+      this.saving = true;
+      this.cdr.markForCheck();
+
+      console.log('💾 保存交付执行阶段草稿');
+
+      // 更新项目的 data 字段,保存交付文件信息
+      const data = this.project.get('data') || {};
+      data.deliveryDraft = {
+        products: this.projectProducts.map(p => p.id),
+        lastSavedAt: new Date(),
+        fileCount: this.getTotalFileCount()
+      };
+
+      this.project.set('data', data);
+      await this.project.save();
+
+      window?.fmode?.alert('草稿保存成功');
+      console.log('✅ 交付执行草稿已保存');
+
+    } catch (error) {
+      console.error('保存草稿失败:', error);
+      window?.fmode?.alert('保存失败,请重试');
+    } finally {
+      this.saving = false;
+      this.cdr.markForCheck();
+    }
+  }
+
+  /**
+   * 获取总文件数
+   */
+  private getTotalFileCount(): number {
+    let count = 0;
+    for (const product of this.projectProducts) {
+      count += this.getTotalDeliveryFileCount(product.id);
+    }
+    return count;
+  }
+
+  /**
+   * 完成交付(推进到售后归档阶段)
    */
   async completeDelivery(): Promise<void> {
     if (!this.project || !this.canEdit) return;
@@ -518,15 +701,49 @@ export class StageDeliveryComponent implements OnInit {
       }
     }
 
+    if (!await window?.fmode?.confirm('确定要完成交付执行并进入售后归档阶段吗?')) {
+      return;
+    }
+
     try {
       this.saving = true;
       this.cdr.markForCheck();
 
-      console.log('完成交付执行阶段');
+      console.log('🚀 完成交付执行,推进到售后归档阶段');
 
-      window?.fmode?.alert('交付执行完成');
+      // ✨ 更新项目阶段为"售后归档"
+      this.project.set('currentStage', '尾款结算');
+      
+      // 更新 data 字段,记录完成时间
+      const data = this.project.get('data') || {};
+      data.deliveryCompletedAt = new Date();
+      data.deliveryCompletedBy = this.currentUser?.get('name') || '';
+      data.deliveryFileCount = this.getTotalFileCount();
+      this.project.set('data', data);
+
+      // 保存项目
+      await this.project.save();
+
+      console.log('✅ 项目阶段已更新为:尾款结算');
+      console.log('📊 交付文件总数:', data.deliveryFileCount);
+
+      window?.fmode?.alert('交付执行完成!项目已进入售后归档阶段');
+
+      // 🔗 自动同步案例库:创建 Case 记录(避免重复创建)
+      try {
+        const { ProjectAutoCaseService } = await import('../../../../../app/pages/admin/services/project-auto-case.service');
+        const svc = new ProjectAutoCaseService(new (await import('../../../../../app/services/case.service')).CaseService());
+        const result = await svc.createCaseForProject(this.project.id!);
+        if (result.success) {
+          console.log('✅ 案例库已同步,CaseId:', result.caseId);
+        } else {
+          console.warn('⚠️ 案例库同步失败:', result.error);
+        }
+      } catch (e) {
+        console.warn('⚠️ 自动创建案例失败(忽略,不阻塞流程):', e);
+      }
 
-      // ✨ 延迟派发事件,确保父组件监听器已注册
+      // ✨ 延迟派发事件,通知父组件刷新并跳转
       setTimeout(() => {
         console.log('📡 派发阶段完成事件: delivery');
         try {
@@ -551,6 +768,73 @@ export class StageDeliveryComponent implements OnInit {
     }
   }
 
+  /**
+   * 提交交付执行审批(组长端看板显示待审批)
+   */
+  async submitDeliveryForApproval(): Promise<void> {
+    if (!this.project || !this.canEdit) return;
+
+    // 至少需要有文件
+    const total = this.getTotalFileCount();
+    if (total === 0) {
+      window?.fmode?.alert('请先上传交付文件后再提交审批');
+      return;
+    }
+
+    try {
+      this.saving = true;
+      this.cdr.markForCheck();
+
+      // 写入项目 data 的交付审批条目,供组长端看板识别
+      const data = this.project.get('data') || {};
+      const now = new Date();
+      const submitter = this.currentUser?.get('name') || '';
+      const submitterId = this.currentUser?.id || '';
+
+      // 汇总各类型数量
+      const summary: Record<string, number> = {} as any;
+      for (const type of this.deliveryTypes) {
+        summary[type.id] = this.getCurrentTypeFileCount(this.activeProductId || this.projectProducts[0]?.id || '', type.id);
+      }
+
+      data.deliveryApproval = {
+        status: 'pending',                 // 供看板过滤
+        stage: 'delivery',
+        totalFiles: total,
+        types: summary,
+        submittedAt: now,
+        submittedByName: submitter,
+        submittedById: submitterId
+      };
+
+      // 兼容:看板可能读取 pendingDeliveryApprovals
+      const pendingList = Array.isArray(data.pendingDeliveryApprovals) ? data.pendingDeliveryApprovals : [];
+      pendingList.push({
+        status: 'pending',
+        productId: this.activeProductId || (this.projectProducts[0]?.id || ''),
+        submittedAt: now,
+        submittedBy: submitter,
+        totalFiles: total
+      });
+      data.pendingDeliveryApprovals = pendingList;
+
+      this.project.set('data', JSON.parse(JSON.stringify(data)));
+      await this.project.save();
+
+      // 发送通知
+      await this.notifyTeamLeaderForApproval(total, 'all');
+
+      window?.fmode?.toast?.success?.('已提交审批,等待组长审核');
+      this.cdr.markForCheck();
+    } catch (e) {
+      console.error('提交交付审批失败:', e);
+      window?.fmode?.alert('提交失败,请稍后再试');
+    } finally {
+      this.saving = false;
+      this.cdr.markForCheck();
+    }
+  }
+
   /**
    * 加载审批历史记录
    */
@@ -571,4 +855,51 @@ export class StageDeliveryComponent implements OnInit {
       console.error('❌ 加载审批历史失败:', error);
     }
   }
+
+  /**
+   * 获取审批状态显示文本
+   */
+  getApprovalStatusText(status: ApprovalStatus): string {
+    const statusMap: Record<ApprovalStatus, string> = {
+      'unverified': '未验证',
+      'pending': '待审批',
+      'approved': '已通过',
+      'rejected': '已驳回'
+    };
+    return statusMap[status] || '未知';
+  }
+
+  /**
+   * 获取审批状态颜色类
+   */
+  getApprovalStatusClass(status: ApprovalStatus): string {
+    const classMap: Record<ApprovalStatus, string> = {
+      'unverified': 'status-unverified',
+      'pending': 'status-pending',
+      'approved': 'status-approved',
+      'rejected': 'status-rejected'
+    };
+    return classMap[status] || '';
+  }
+
+  /**
+   * 获取未验证文件数量
+   */
+  getUnverifiedFileCount(productId: string): number {
+    if (!this.deliveryFiles[productId]) return 0;
+    
+    return this.deliveryTypes.reduce((total, type) => {
+      const files = this.getProductDeliveryFiles(productId, type.id);
+      const unverified = files.filter(f => f.approvalStatus === 'unverified').length;
+      return total + unverified;
+    }, 0);
+  }
+
+  /**
+   * 获取指定类型未验证文件数量
+   */
+  getTypeUnverifiedFileCount(productId: string, typeId: string): number {
+    const files = this.getProductDeliveryFiles(productId, typeId);
+    return files.filter(f => f.approvalStatus === 'unverified').length;
+  }
 }

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

@@ -43,6 +43,18 @@
       }
     }
     
+    <!-- 组长审批操作条:仅在待审批时、且当前用户为组长显示 -->
+    @if (getApprovalStatus() === 'pending' && isTeamLeader) {
+      <div class="action-bar leader-approval">
+        <button class="btn btn-primary" (click)="approveOrder()" [disabled]="saving">
+          ✅ 通过审批
+        </button>
+        <button class="btn" (click)="rejectOrder()" [disabled]="saving">
+          ❌ 驳回
+        </button>
+      </div>
+    }
+    
     <!-- 1. 项目基本信息(可折叠) -->
     <div class="card project-info-card">
       <div class="card-header collapsible" (click)="toggleProjectInfo()">
@@ -195,7 +207,7 @@
     </app-team-assign>
 
     <!-- 4. 操作按钮 -->
-    @if (canEdit) {
+    @if (canEdit && isFromCustomerService) {
       <div class="action-buttons">
         <button
           class="btn btn-outline"
@@ -218,6 +230,19 @@
         </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>
 
 }

+ 39 - 0
src/modules/project/pages/project-detail/stages/stage-order.component.scss

@@ -156,6 +156,45 @@
   }
 }
 
+// ============ 信息提示横幅样式 ============
+.info-banner {
+  padding: 20px 24px;
+  border-radius: 12px;
+  background: linear-gradient(135deg, #e3f2fd, #bbdefb);
+  border-left: 4px solid #2196f3;
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+  animation: slideDown 0.3s ease-out;
+  box-shadow: 0 2px 8px rgba(33, 150, 243, 0.15);
+
+  .info-icon {
+    font-size: 32px;
+    flex-shrink: 0;
+    line-height: 1;
+  }
+
+  .info-content {
+    flex: 1;
+
+    p {
+      margin: 0;
+      font-size: 15px;
+      line-height: 1.6;
+      color: #1565c0;
+      font-weight: 500;
+
+      &.info-hint {
+        margin-top: 8px;
+        font-size: 14px;
+        color: #1976d2;
+        font-weight: 400;
+        opacity: 0.9;
+      }
+    }
+  }
+}
+
 // ============ Material DatePicker 样式 ============
 .date-picker-field {
   width: 100%;

+ 231 - 5
src/modules/project/pages/project-detail/stages/stage-order.component.ts

@@ -58,6 +58,10 @@ export class StageOrderComponent implements OnInit {
   @Input() customer: FmodeObject | null = null;
   @Input() currentUser: FmodeObject | null = null;
   @Input() canEdit: boolean = true;
+  // 组长权限标记(仅用于审批显示与操作)
+  isTeamLeader: boolean = false;
+  // 客服权限标记(仅从客服板块进入的项目才能确认订单)
+  isFromCustomerService: boolean = false;
 
   // 项目基本信息折叠展开状态
   projectInfoExpanded: boolean = false;
@@ -350,6 +354,55 @@ export class StageOrderComponent implements OnInit {
         if (!role) {
           this.canEdit = true;
         }
+
+        // 标记是否为组长(用于显示审批按钮)
+        // 方案1:通过角色名称判断
+        this.isTeamLeader = ['设计组长','组长','team-leader'].some(r => role.includes(r));
+        
+        // 方案2:通过 localStorage 标记判断(从组长看板进入)
+        try {
+          const enterAsLeader = localStorage.getItem('enterAsTeamLeader') === '1';
+          const teamLeaderMode = localStorage.getItem('teamLeaderMode') === 'true';
+          if (enterAsLeader || teamLeaderMode) {
+            this.isTeamLeader = true;
+            console.log('✅ 检测到组长模式标记,启用审批权限');
+          }
+        } catch (e) {
+          console.warn('无法读取 localStorage:', e);
+        }
+        
+        // 方案3:通过 URL 路径判断(如果从 /team-leader 进入)
+        if (typeof window !== 'undefined' && window.location.pathname.includes('/team-leader/')) {
+          this.isTeamLeader = true;
+          console.log('✅ 检测到组长路径,启用审批权限');
+        }
+        
+        // 检测是否从客服板块进入(用于控制"确认订单"按钮)
+        try {
+          const enterFromCS = localStorage.getItem('enterFromCustomerService') === '1';
+          const csMode = localStorage.getItem('customerServiceMode') === 'true';
+          if (enterFromCS || csMode) {
+            this.isFromCustomerService = true;
+            console.log('✅ 检测到从客服板块进入,允许确认订单');
+          }
+        } catch (e) {
+          console.warn('无法读取客服标记:', e);
+        }
+        
+        // 方案2:通过 URL referrer 检测
+        if (typeof document !== 'undefined' && document.referrer.includes('/customer-service/')) {
+          this.isFromCustomerService = true;
+          console.log('✅ 检测到从客服路径进入,允许确认订单');
+        }
+        
+        console.log('🔍 权限检测结果:', {
+          role,
+          isTeamLeader: this.isTeamLeader,
+          isFromCustomerService: this.isFromCustomerService,
+          canEdit: this.canEdit,
+          pathname: typeof window !== 'undefined' ? window.location.pathname : 'unknown',
+          referrer: typeof document !== 'undefined' ? document.referrer : 'unknown'
+        });
       }
 
       // 加载项目信息
@@ -409,6 +462,108 @@ export class StageOrderComponent implements OnInit {
     }
   }
 
+  /**
+   * 组长审批:通过
+   */
+  async approveOrder(): Promise<void> {
+    if (!this.project || !this.currentUser) return;
+
+    try {
+      this.saving = true;
+      this.cdr.markForCheck();
+
+      const data = this.project.get('data') || {};
+      const approvalHistory = data.approvalHistory || [];
+      const latest = approvalHistory[approvalHistory.length - 1] || {};
+      latest.status = 'approved';
+      latest.approver = {
+        id: this.currentUser.id,
+        name: this.currentUser.get('name'),
+        role: this.currentUser.get('roleName')
+      };
+      latest.approvalTime = new Date();
+      if (approvalHistory.length > 0) {
+        approvalHistory[approvalHistory.length - 1] = latest;
+      } else {
+        approvalHistory.push(latest);
+      }
+
+      data.approvalHistory = approvalHistory;
+      data.approvalStatus = 'approved';
+      delete data.lastRejectionReason;
+
+      // 写回并推进阶段到“确认需求”
+      this.project.set('data', JSON.parse(JSON.stringify(data)));
+      this.project.set('currentStage', '确认需求');
+      await this.project.save();
+
+      // 派发阶段完成事件,通知父组件前进
+      try {
+        const ev = new CustomEvent('stage:completed', { detail: { stage: 'order' }, bubbles: true, cancelable: true });
+        document.dispatchEvent(ev);
+      } catch {}
+
+      window?.fmode?.toast?.success?.('审批通过,项目已进入“确认需求”阶段');
+      this.cdr.markForCheck();
+    } catch (e) {
+      console.error('审批通过失败:', e);
+      window?.fmode?.alert('审批失败,请稍后重试');
+    } finally {
+      this.saving = false;
+      this.cdr.markForCheck();
+    }
+  }
+
+  /**
+   * 组长审批:驳回
+   */
+  async rejectOrder(): Promise<void> {
+    if (!this.project || !this.currentUser) return;
+
+    const reason = await window?.fmode?.input?.('请输入驳回原因:');
+    if (!reason) return;
+
+    try {
+      this.saving = true;
+      this.cdr.markForCheck();
+
+      const data = this.project.get('data') || {};
+      const approvalHistory = data.approvalHistory || [];
+      const latest = approvalHistory[approvalHistory.length - 1] || {};
+      latest.status = 'rejected';
+      latest.approver = {
+        id: this.currentUser.id,
+        name: this.currentUser.get('name'),
+        role: this.currentUser.get('roleName')
+      };
+      latest.approvalTime = new Date();
+      latest.reason = reason;
+      if (approvalHistory.length > 0) {
+        approvalHistory[approvalHistory.length - 1] = latest;
+      } else {
+        approvalHistory.push(latest);
+      }
+
+      data.approvalHistory = approvalHistory;
+      data.approvalStatus = 'rejected';
+      data.lastRejectionReason = reason;
+
+      // 驳回后停留在“订单分配”阶段
+      this.project.set('data', JSON.parse(JSON.stringify(data)));
+      this.project.set('currentStage', '订单分配');
+      await this.project.save();
+
+      window?.fmode?.toast?.success?.('已驳回订单,已记录原因');
+      this.cdr.markForCheck();
+    } catch (e) {
+      console.error('审批驳回失败:', e);
+      window?.fmode?.alert('驳回失败,请稍后重试');
+    } finally {
+      this.saving = false;
+      this.cdr.markForCheck();
+    }
+  }
+
   /**
    * 加载项目空间
    */
@@ -938,28 +1093,99 @@ export class StageOrderComponent implements OnInit {
       data.approvalHistory = approvalHistory;
       data.approvalStatus = 'pending';  // 待审批
       data.pendingApprovalBy = 'team-leader';  // 待组长审批
-      this.project.set('data', data);
       
       // ✨ 保持在"订单分配"阶段
       // 项目的 currentStage 仍然是"订单分配"
       this.project.set('currentStage', '订单分配');
 
       console.log('💾 准备保存项目数据:', {
+        projectId: this.project.id,
         currentStage: this.project.get('currentStage'),
         approvalStatus: data.approvalStatus,
+        pendingApprovalBy: data.pendingApprovalBy,
         approvalHistory: data.approvalHistory.length + '条记录'
       });
 
+      // 🔥 关键:确保 data 对象被正确设置
+      console.log('🔍 保存前的完整 data 对象:', JSON.stringify(data, null, 2));
+
+      // 🔥 最简单可靠的方式:直接调用 Parse Cloud Function
+      try {
+        console.log('🔥 方案A:使用 Parse Cloud Function 保存数据');
+        
+        // 先尝试使用 Cloud Function(如果可用)
+        const cloudResult = await Parse.Cloud.run('updateProjectApprovalStatus', {
+          projectId: this.project.id,
+          approvalStatus: 'pending',
+          pendingApprovalBy: 'team-leader',
+          currentStage: '订单分配',
+          approvalHistory: approvalHistory,
+          quotation: this.quotation,
+          priceLevel: this.projectInfo.priceLevel
+        }).catch(() => null);
+        
+        if (cloudResult) {
+          console.log('✅ Cloud Function 调用成功');
+        } else {
+          console.log('⚠️ Cloud Function 不可用,使用本地保存');
+        }
+      } catch (e) {
+        console.log('⚠️ Cloud Function 失败,使用本地保存');
+      }
+      
+      // 🔥 方案B:本地保存(兜底方案)
+      // 标记项目为待审批状态(顶层字段 - 最可靠)
+      this.project.set('pendingApproval', true);
+      this.project.set('approvalStage', '订单分配');
+      this.project.set('lastOrderSubmitTime', new Date());
+      
+      // 保存 data 字段(使用最激进的方式)
+      this.project.set('data', JSON.parse(JSON.stringify(data)));
+
+      console.log('🔥 开始保存到 Parse(本地方式)...');
       await this.project.save();
+      console.log('✅ Parse.save() 调用完成');
       
       // 本地立即置灰按钮,提升操作反馈
       this.submittedPending = true;
       
-      console.log('✅ 项目保存成功');
-      console.log('🔍 验证保存后的数据:', {
-        currentStage: this.project.get('currentStage'),
-        data: this.project.get('data')
+      console.log('✅ 项目保存成功!');
+      
+      // 🔥 重新从服务器获取,验证数据确实保存了
+      const verifyQuery = new Parse.Query('Project');
+      verifyQuery.select('data', 'currentStage', 'pendingApproval', 'approvalStage', 'lastOrderSubmitTime');
+      const savedProject = await verifyQuery.get(this.project.id);
+      const savedData = savedProject.get('data') || {};
+      
+      console.log('🔍 验证:从服务器重新获取的数据:', {
+        projectId: savedProject.id,
+        currentStage: savedProject.get('currentStage'),
+        pendingApproval: savedProject.get('pendingApproval'),
+        approvalStage: savedProject.get('approvalStage'),
+        lastOrderSubmitTime: savedProject.get('lastOrderSubmitTime'),
+        'data.approvalStatus': savedData.approvalStatus,
+        'data.pendingApprovalBy': savedData.pendingApprovalBy,
+        'data.approvalHistoryCount': (savedData.approvalHistory || []).length,
+        'data完整keys': Object.keys(savedData)
       });
+      
+      // 验证数据完整性
+      const dataOK = savedData.approvalStatus === 'pending';
+      const topLevelOK = savedProject.get('pendingApproval') === true;
+      
+      if (!dataOK && !topLevelOK) {
+        console.error('❌❌ 严重错误:数据和顶层字段都没有保存成功!');
+        console.error('期望 data.approvalStatus: "pending", 实际值:', savedData.approvalStatus);
+        console.error('期望 pendingApproval: true, 实际值:', savedProject.get('pendingApproval'));
+        console.error('完整的 savedData:', JSON.stringify(savedData, null, 2));
+        window?.fmode?.alert('数据保存失败,请联系技术支持');
+      } else if (!dataOK) {
+        console.warn('⚠️ data.approvalStatus 保存失败,但顶层字段 pendingApproval 保存成功(备用方案生效)');
+      } else if (!topLevelOK) {
+        console.warn('⚠️ 顶层字段 pendingApproval 保存失败,但 data.approvalStatus 保存成功');
+      } else {
+        console.log('✅✅ 数据验证通过:data 和顶层字段都保存成功!');
+      }
 
       // 🔔 发送企微通知给组长
       await this.sendApprovalNotificationToLeader();

+ 7 - 3
src/modules/project/services/project-file.service.ts

@@ -145,14 +145,17 @@ export class ProjectFileService {
       projectFile.set('stage', stage);
     }
 
-    // 新增:从附件元数据中读取 deliverableId 并写入 ProjectFile.data
-    const deliverableId = attachment.get('metadata')?.deliverableId;
+    // ✨ 增强:完整保存元数据到 ProjectFile.data,包括审批状态等
+    const attachmentMetadata = attachment.get('metadata') || {};
+    const deliverableId = attachmentMetadata.deliverableId;
+    
     const data = {
       spaceId,
       uploadedAt: new Date(),
       fileType,
       deliverableId,
-      metadata: attachment.get('metadata')
+      // ✨ 保存所有元数据(包含 approvalStatus, uploadedByName, uploadedById 等)
+      ...attachmentMetadata
     };
     projectFile.set('data', data);
 
@@ -163,6 +166,7 @@ export class ProjectFileService {
     }
 
     const savedProjectFile = await projectFile.save();
+    console.log('✅ ProjectFile已保存,data字段:', savedProjectFile.get('data'));
     return savedProjectFile;
   }
 

+ 6 - 0
快速开始.md

@@ -139,3 +139,9 @@ URL: .../aftercare
 
 
 
+
+
+
+
+
+

+ 6 - 0
核心代码变更.md

@@ -279,3 +279,9 @@ console.log('📌 路由参数:', {
 
 
 
+
+
+
+
+
+