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 10 mēneši 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('📌 路由参数:', {
 
 
 
+
+
+
+
+
+