فهرست منبع

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

Future 1 روز پیش
والد
کامیت
c3b6dcb779

+ 12 - 5
src/app/pages/team-leader/dashboard/dashboard.html

@@ -379,15 +379,22 @@
                 {{ selectedEmployeeDetail.currentProjects }} 个
               </span>
             </div>
-            @if (selectedEmployeeDetail.projectNames.length > 0) {
+            @if (selectedEmployeeDetail.projectData.length > 0) {
               <div class="project-list">
                 <span class="project-label">核心项目:</span>
                 <div class="project-tags">
-                  @for (projectName of selectedEmployeeDetail.projectNames; track $index) {
-                    <span class="project-tag">{{ projectName }}</span>
+                  @for (project of selectedEmployeeDetail.projectData; track project.id) {
+                    <span class="project-tag clickable" 
+                          (click)="navigateToProjectFromPanel(project.id)"
+                          title="点击查看项目详情">
+                      {{ project.name }}
+                      <svg class="icon-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                        <path d="M7 17L17 7M17 7H7M17 7V17"/>
+                      </svg>
+                    </span>
                   }
-                  @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectNames.length) {
-                    <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectNames.length }}</span>
+                  @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectData.length) {
+                    <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectData.length }}</span>
                   }
                 </div>
               </div>

+ 36 - 0
src/app/pages/team-leader/dashboard/dashboard.scss

@@ -1231,9 +1231,45 @@
               border-radius: 16px;
               font-size: 12px;
               font-weight: 500;
+              transition: all 0.2s ease;
+              
+              &.clickable {
+                cursor: pointer;
+                display: inline-flex;
+                align-items: center;
+                gap: 4px;
+                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+                box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
+                
+                .icon-arrow {
+                  width: 14px;
+                  height: 14px;
+                  stroke-width: 2.5;
+                  opacity: 0;
+                  transform: translateX(-4px);
+                  transition: all 0.2s ease;
+                }
+                
+                &:hover {
+                  background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
+                  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+                  transform: translateY(-2px);
+                  
+                  .icon-arrow {
+                    opacity: 1;
+                    transform: translateX(0);
+                  }
+                }
+                
+                &:active {
+                  transform: translateY(0);
+                  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
+                }
+              }
               
               &.more {
                 background: #94a3b8;
+                cursor: default;
               }
             }
           }

+ 302 - 56
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -68,6 +68,7 @@ interface EmployeeDetail {
   name: string;
   currentProjects: number; // 当前负责项目数
   projectNames: string[]; // 项目名称列表(用于显示)
+  projectData: Array<{ id: string; name: string }>; // 项目数据(包含ID和名称,用于跳转)
   leaveRecords: LeaveRecord[]; // 未来7天请假记录
   redMarkExplanation: string; // 红色标记说明
 }
@@ -85,7 +86,6 @@ export class Dashboard implements OnInit, OnDestroy {
   projects: Project[] = [];
   filteredProjects: Project[] = [];
   todoTasks: TodoTask[] = [];
-  overdueProjects: Project[] = [];
   urgentPinnedProjects: Project[] = [];
   showAlert: boolean = false;
   selectedProjectId: string = '';
@@ -93,6 +93,9 @@ export class Dashboard implements OnInit, OnDestroy {
   // 真实设计师数据(从fmode-ng获取)
   realDesigners: any[] = [];
   
+  // 设计师工作量映射(从 ProjectTeam 表)
+  designerWorkloadMap: Map<string, any[]> = new Map(); // designerId/name -> projects[]
+  
   // 智能推荐相关
   showSmartMatch: boolean = false;
   selectedProject: any = null;
@@ -110,7 +113,6 @@ export class Dashboard implements OnInit, OnDestroy {
   private readonly MAX_SUGGESTIONS = 8; // 建议最大条数
   private isSearchFocused: boolean = false; // 是否处于输入聚焦态
   // 新增:临期项目与筛选状态
-  dueSoonProjects: Project[] = [];
   selectedType: 'all' | 'soft' | 'hard' = 'all';
   selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
   selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
@@ -228,9 +230,142 @@ export class Dashboard implements OnInit, OnDestroy {
         avgRating: d.tags.history.avgRating || 0,
         experience: 0 // 暂无此字段
       }));
-      console.log('✅ 加载设计师数据成功:', this.realDesigners.length, '人');
+      
+      // 加载设计师的实际工作量
+      await this.loadDesignerWorkload();
+    } catch (error) {
+      console.error('加载设计师数据失败:', error);
+    }
+  }
+  
+  /**
+   * 🔧 从 ProjectTeam 表加载每个设计师的实际工作量
+   */
+  async loadDesignerWorkload(): Promise<void> {
+    try {
+      const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
+      
+      // 查询所有 ProjectTeam 记录
+      const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
+      
+      // 先查询当前公司的所有项目
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', cid);
+      projectQuery.notEqualTo('isDeleted', true);
+      
+      // 查询当前公司项目的 ProjectTeam
+      const teamQuery = new Parse.Query('ProjectTeam');
+      teamQuery.matchesQuery('project', projectQuery);
+      teamQuery.notEqualTo('isDeleted', true);
+      teamQuery.include('project');
+      teamQuery.include('profile');
+      teamQuery.limit(1000);
+      
+      const teamRecords = await teamQuery.find();
+      
+      // 如果 ProjectTeam 表为空,使用降级方案
+      if (teamRecords.length === 0) {
+        await this.loadDesignerWorkloadFromProjects();
+        return;
+      }
+      
+      // 构建设计师工作量映射
+      this.designerWorkloadMap.clear();
+      
+      teamRecords.forEach((record: any) => {
+        const profile = record.get('profile');
+        const project = record.get('project');
+        
+        if (!profile || !project) {
+          return;
+        }
+        
+        const profileId = profile.id;
+        const profileName = profile.get('name') || profile.get('user')?.get?.('name') || `设计师-${profileId.slice(-4)}`;
+        
+        // 提取项目信息
+        const projectData = {
+          id: project.id,
+          name: project.get('title') || '未命名项目',
+          status: project.get('status') || '进行中',
+          currentStage: project.get('currentStage') || '未知阶段',
+          deadline: project.get('deadline'),
+          createdAt: project.get('createdAt'),
+          designerName: profileName // 设置为组员的名字
+        };
+        
+        // 添加到映射 (by ID)
+        if (!this.designerWorkloadMap.has(profileId)) {
+          this.designerWorkloadMap.set(profileId, []);
+        }
+        this.designerWorkloadMap.get(profileId)!.push(projectData);
+        
+        // 同时建立 name -> projects 的映射(用于甘特图)
+        if (!this.designerWorkloadMap.has(profileName)) {
+          this.designerWorkloadMap.set(profileName, []);
+        }
+        this.designerWorkloadMap.get(profileName)!.push(projectData);
+      });
+      
+    } catch (error) {
+      console.error('加载设计师工作量失败:', error);
+    }
+  }
+  
+  /**
+   * 🔧 降级方案:从 Project.assignee 统计工作量
+   * 当 ProjectTeam 表为空时使用
+   */
+  async loadDesignerWorkloadFromProjects(): Promise<void> {
+    try {
+      const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
+      const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
+      
+      // 查询所有项目
+      const projectQuery = new Parse.Query('Project');
+      projectQuery.equalTo('company', cid);
+      projectQuery.equalTo('isDeleted', false);
+      projectQuery.include('assignee');
+      projectQuery.include('department');
+      projectQuery.limit(1000);
+      
+      const projects = await projectQuery.find();
+      
+      // 构建设计师工作量映射
+      this.designerWorkloadMap.clear();
+      
+      projects.forEach((project: any) => {
+        const assignee = project.get('assignee');
+        if (!assignee) return;
+        
+        // 只统计组员角色的项目
+        const assigneeRole = assignee.get('roleName');
+        if (assigneeRole !== '组员') {
+          return;
+        }
+        
+        const assigneeName = assignee.get('name') || assignee.get('user')?.get?.('name') || `设计师-${assignee.id.slice(-4)}`;
+        
+        // 提取项目信息
+        const projectData = {
+          id: project.id,
+          name: project.get('title') || '未命名项目',
+          status: project.get('status') || '进行中',
+          currentStage: project.get('currentStage') || '未知阶段',
+          deadline: project.get('deadline'),
+          createdAt: project.get('createdAt'),
+          designerName: assigneeName
+        };
+        
+        // 添加到映射
+        if (!this.designerWorkloadMap.has(assigneeName)) {
+          this.designerWorkloadMap.set(assigneeName, []);
+        }
+        this.designerWorkloadMap.get(assigneeName)!.push(projectData);
+      });
+      
     } catch (error) {
-      console.error('❌ 加载设计师数据失败:', error);
+      console.error('[降级方案] 加载工作量失败:', error);
     }
   }
 
@@ -244,14 +379,12 @@ export class Dashboard implements OnInit, OnDestroy {
       // 如果有真实数据,使用真实数据
       if (realProjects && realProjects.length > 0) {
         this.projects = realProjects;
-        console.log('✅ 加载真实项目数据成功:', this.projects.length, '个项目');
       } else {
-        // 如果没有真实数据,使用模拟数据(便于开发测试)
-        console.warn('⚠️ 未找到真实项目数据,使用模拟数据');
+        // 如果没有真实数据,使用模拟数据
         this.projects = this.getMockProjects();
       }
     } catch (error) {
-      console.error('加载项目数据失败,使用模拟数据:', error);
+      console.error('加载项目数据失败:', error);
       this.projects = this.getMockProjects();
     }
     
@@ -537,15 +670,13 @@ export class Dashboard implements OnInit, OnDestroy {
       return { ...p, deadline, createdAt } as Project;
     });
 
-    // 筛选超期与临期项目
-    this.overdueProjects = this.projects.filter(project => project.isOverdue);
-    this.dueSoonProjects = this.projects.filter(project => project.dueSoon && !project.isOverdue);
+    // 筛选结果初始化为全部项目
     this.filteredProjects = [...this.projects];
 
     // 供筛选用的设计师列表
     this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n)));
 
-    // 显示超期提醒
+    // 显示超期提醒(使用 getter)
     if (this.overdueProjects.length > 0) {
       this.showAlert = true;
     }
@@ -946,9 +1077,8 @@ export class Dashboard implements OnInit, OnDestroy {
     
     try {
       this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners);
-      console.log('✅ 智能推荐结果:', this.recommendations);
     } catch (error) {
-      console.error('智能推荐失败:', error);
+      console.error('智能推荐失败:', error);
       this.recommendations = [];
     }
   }
@@ -1848,9 +1978,9 @@ export class Dashboard implements OnInit, OnDestroy {
     if (this.workloadGanttScale === 'week') {
       // 周视图:显示未来7天
       xMin = todayTs;
-      xMax = todayTs + 7 * DAY - 1;
+      xMax = todayTs + 7 * DAY;
       xSplitNumber = 7;
-      xLabelFormatter = (val) => {
+      xLabelFormatter = (val: any) => {
         const date = new Date(val);
         const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
         return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
@@ -1858,26 +1988,23 @@ export class Dashboard implements OnInit, OnDestroy {
     } else {
       // 月视图:显示未来30天
       xMin = todayTs;
-      xMax = todayTs + 30 * DAY - 1;
+      xMax = todayTs + 30 * DAY;
       xSplitNumber = 30;
-      xLabelFormatter = (val) => {
+      xLabelFormatter = (val: any) => {
         const date = new Date(val);
         return `${date.getMonth() + 1}/${date.getDate()}`;
       };
     }
 
-    // 获取所有真实设计师(优先使用realDesigners)
+    // 获取所有真实设计师
     let designers: string[] = [];
     
     if (this.realDesigners && this.realDesigners.length > 0) {
-      // 使用真实的设计师列表
       designers = this.realDesigners.map(d => d.name);
-      console.log('✅ 使用真实设计师列表:', designers.length, '人');
     } else {
-      // 降级:从已分配的项目中提取设计师(过滤掉"未分配")
+      // 降级:从已分配的项目中提取设计师
       const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
       designers = Array.from(new Set(assigned.map(p => p.designerName)));
-      console.warn('⚠️ 使用项目中提取的设计师列表:', designers.length, '人');
     }
     
     if (designers.length === 0) {
@@ -1896,10 +2023,7 @@ export class Dashboard implements OnInit, OnDestroy {
       return;
     }
     
-    // 获取所有已分配的项目(过滤掉"未分配")
-    const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
-    
-    // 计算每个设计师的每日工作状态
+    // 🔧 使用 ProjectTeam 表的数据(实际执行人)
     const workloadByDesigner: Record<string, any[]> = {};
     designers.forEach(name => {
       workloadByDesigner[name] = [];
@@ -1908,7 +2032,7 @@ export class Dashboard implements OnInit, OnDestroy {
     // 计算每个设计师的总负载(用于排序)
     const designerTotalLoad: Record<string, number> = {};
     designers.forEach(name => {
-      const projects = assigned.filter(p => p.designerName === name);
+      const projects = this.designerWorkloadMap.get(name) || [];
       designerTotalLoad[name] = projects.length;
     });
     
@@ -1919,7 +2043,7 @@ export class Dashboard implements OnInit, OnDestroy {
     
     // 为每个设计师生成时间段数据
     sortedDesigners.forEach((designerName, yIndex) => {
-      const designerProjects = assigned.filter(p => p.designerName === designerName);
+      const designerProjects = this.designerWorkloadMap.get(designerName) || [];
       
       // 计算每一天的状态
       const days = this.workloadGanttScale === 'week' ? 7 : 30;
@@ -1929,8 +2053,31 @@ export class Dashboard implements OnInit, OnDestroy {
         
         // 查找该天有哪些项目
         const dayProjects = designerProjects.filter(p => {
-          const pStart = p.createdAt ? new Date(p.createdAt).getTime() : dayStart;
+          // 如果项目没有 deadline,则认为项目一直在进行中
+          if (!p.deadline) {
+            return true; // 没有截止日期的项目始终显示
+          }
+          
           const pEnd = new Date(p.deadline).getTime();
+          
+          // 检查时间是否有效
+          if (isNaN(pEnd)) {
+            return true; // 如果截止日期无效,认为项目在进行中
+          }
+          
+          // 🔧 修复:对于进行中的项目(状态不是"已完成"),即使过期也显示
+          // 这样可以在甘特图中看到超期的项目
+          const isCompleted = p.status === '已完成' || p.status === '已交付';
+          if (!isCompleted) {
+            // 进行中的项目:只要截止日期还没到很久之前(比如30天前),就显示
+            const thirtyDaysAgo = todayTs - 30 * DAY;
+            if (pEnd >= thirtyDaysAgo) {
+              return true; // 30天内的项目都显示
+            }
+          }
+          
+          // 已完成的项目:正常时间范围判断
+          const pStart = p.createdAt ? new Date(p.createdAt).getTime() : dayStart;
           return !(pEnd < dayStart || pStart > dayEnd);
         });
 
@@ -2043,17 +2190,29 @@ export class Dashboard implements OnInit, OnDestroy {
         bottom: 60
       },
       xAxis: {
-        type: 'value',
+        type: 'time',
         min: xMin,
         max: xMax,
-        splitNumber: xSplitNumber,
+        boundaryGap: false,
         axisLine: { lineStyle: { color: '#e5e7eb' } },
         axisLabel: {
           color: '#6b7280',
           formatter: xLabelFormatter,
-          interval: this.workloadGanttScale === 'week' ? 0 : 4
+          interval: 0,
+          rotate: this.workloadGanttScale === 'week' ? 0 : 45,
+          showMinLabel: true,
+          showMaxLabel: true
         },
-        splitLine: { lineStyle: { color: '#f1f5f9' } }
+        axisTick: {
+          alignWithLabel: true,
+          interval: 0
+        },
+        splitLine: { 
+          show: true,
+          lineStyle: { color: '#f1f5f9' }
+        },
+        splitNumber: xSplitNumber,
+        minInterval: DAY
       },
       yAxis: {
         type: 'category',
@@ -2143,14 +2302,65 @@ export class Dashboard implements OnInit, OnDestroy {
 
   // 新增:阶段到核心阶段的映射
   private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' {
-    // 订单分配:立项初期
-    if (stageId === 'pendingApproval' || stageId === 'pendingAssignment') return 'order';
-    // 确认需求:需求沟通 + 方案规划
-    if (stageId === 'requirement' || stageId === 'planning') return 'requirements';
-    // 交付执行:制作与评审修订过程
-    if (stageId === 'modeling' || stageId === 'rendering' || stageId === 'postProduction' || stageId === 'review' || stageId === 'revision') return 'delivery';
-    // 售后:交付完成后的跟进(当前数据以交付完成代表进入售后)
-    return 'aftercare';
+    if (!stageId) return 'order'; // 空值默认为订单分配
+    
+    // 标准化阶段名称(去除空格,转小写)
+    const normalizedStage = stageId.trim().toLowerCase();
+    
+    // 1. 订单分配阶段(英文ID + 中文名称)
+    if (normalizedStage === 'order' || 
+        normalizedStage === 'pendingapproval' || 
+        normalizedStage === 'pendingassignment' ||
+        normalizedStage === '订单分配' ||
+        normalizedStage === '待审批' ||
+        normalizedStage === '待分配') {
+      return 'order';
+    }
+    
+    // 2. 确认需求阶段(英文ID + 中文名称)
+    if (normalizedStage === 'requirements' ||
+        normalizedStage === 'requirement' || 
+        normalizedStage === 'planning' ||
+        normalizedStage === '确认需求' ||
+        normalizedStage === '需求沟通' ||
+        normalizedStage === '方案规划') {
+      return 'requirements';
+    }
+    
+    // 3. 交付执行阶段(英文ID + 中文名称)
+    if (normalizedStage === 'delivery' ||
+        normalizedStage === 'modeling' || 
+        normalizedStage === 'rendering' || 
+        normalizedStage === 'postproduction' || 
+        normalizedStage === 'review' || 
+        normalizedStage === 'revision' ||
+        normalizedStage === '交付执行' ||
+        normalizedStage === '建模' ||
+        normalizedStage === '建模阶段' ||
+        normalizedStage === '渲染' ||
+        normalizedStage === '渲染阶段' ||
+        normalizedStage === '后期制作' ||
+        normalizedStage === '评审' ||
+        normalizedStage === '修改' ||
+        normalizedStage === '修订') {
+      return 'delivery';
+    }
+    
+    // 4. 售后归档阶段(英文ID + 中文名称)
+    if (normalizedStage === 'aftercare' ||
+        normalizedStage === 'completed' ||
+        normalizedStage === 'archived' ||
+        normalizedStage === '售后归档' ||
+        normalizedStage === '售后' ||
+        normalizedStage === '归档' ||
+        normalizedStage === '已完成' ||
+        normalizedStage === '已交付') {
+      return 'aftercare';
+    }
+    
+    // 未匹配的阶段:默认为交付执行(因为大部分时间项目都在执行中)
+    console.warn(`⚠️ 未识别的阶段: "${stageId}" → 默认归类为交付执行`);
+    return 'delivery';
   }
 
   // 新增:获取核心阶段的项目
@@ -2168,16 +2378,34 @@ export class Dashboard implements OnInit, OnDestroy {
     return this.getProjectsByStage(stageId).length;
   }
 
-  // 待审批项目:currentStage === 'pendingApproval'
+  // 🔥 已延期项目
+  get overdueProjects(): Project[] {
+    return this.projects.filter(p => p.isOverdue);
+  }
+
+  // ⏳ 临期项目(3天内)
+  get dueSoonProjects(): Project[] {
+    return this.projects.filter(p => p.dueSoon && !p.isOverdue);
+  }
+
+  // 📋 待审批项目(支持中文和英文阶段名称)
   get pendingApprovalProjects(): Project[] {
-    const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects;
-    return src.filter(p => p.currentStage === 'pendingApproval');
+    return this.projects.filter(p => {
+      const stage = (p.currentStage || '').trim().toLowerCase();
+      return stage === 'pendingapproval' || 
+             stage === '待审批' || 
+             stage === '待确认';
+    });
   }
 
-  // 待指派项目:currentStage === 'pendingAssignment'
+  // 🎯 待分配项目(支持中文和英文阶段名称)
   get pendingAssignmentProjects(): Project[] {
-    const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects;
-    return src.filter(p => p.currentStage === 'pendingAssignment');
+    return this.projects.filter(p => {
+      const stage = (p.currentStage || '').trim().toLowerCase();
+      return stage === 'pendingassignment' || 
+             stage === '待分配' ||
+             stage === '订单分配';
+    });
   }
 
   // 智能推荐设计师
@@ -2219,7 +2447,6 @@ export class Dashboard implements OnInit, OnDestroy {
   // 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI)
   viewProjectDetails(projectId: string): void {
     if (!projectId) {
-      console.warn('⚠️ 项目ID为空,无法跳转');
       return;
     }
     
@@ -2227,10 +2454,7 @@ export class Dashboard implements OnInit, OnDestroy {
     const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
     
     // 跳转到wxwork路由的项目详情页(纯净页面,无管理端侧边栏)
-    // 路由格式:/wxwork/:cid/project/:projectId/order
     this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);
-    
-    console.log('✅ 组长端跳转到纯净项目详情页:', projectId);
   }
 
   // 快速分配项目(增强:加入智能推荐)
@@ -2346,10 +2570,17 @@ export class Dashboard implements OnInit, OnDestroy {
 
   // 生成员工详情数据
   private generateEmployeeDetail(employeeName: string): EmployeeDetail {
-    // 获取该员工负责的项目
-    const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
+    // 🔧 从 ProjectTeam 表获取该员工负责的项目
+    const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
     const currentProjects = employeeProjects.length;
-    const projectNames = employeeProjects.slice(0, 3).map(p => p.name); // 最多显示3个项目名称
+    
+    // 保存完整的项目数据(最多显示3个)
+    const projectData = employeeProjects.slice(0, 3).map(p => ({
+      id: p.id,
+      name: p.name
+    }));
+    
+    const projectNames = projectData.map(p => p.name); // 项目名称列表
     
     // 获取该员工的请假记录(未来7天)
     const today = new Date();
@@ -2370,6 +2601,7 @@ export class Dashboard implements OnInit, OnDestroy {
       name: employeeName,
       currentProjects,
       projectNames,
+      projectData,
       leaveRecords: employeeLeaveRecords,
       redMarkExplanation
     };
@@ -2409,6 +2641,20 @@ export class Dashboard implements OnInit, OnDestroy {
     this.selectedEmployeeDetail = null;
   }
 
+  // 从员工详情面板跳转到项目详情
+  navigateToProjectFromPanel(projectId: string): void {
+    if (!projectId) {
+      return;
+    }
+    
+    // 关闭员工详情面板
+    this.closeEmployeeDetailPanel();
+    
+    // 跳转到项目详情页(使用纯净的wxwork路由)
+    const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
+    this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);
+  }
+
   // 获取请假类型显示文本
   getLeaveTypeText(leaveType?: string): string {
     const typeMap: Record<string, string> = {
@@ -2454,7 +2700,7 @@ export class Dashboard implements OnInit, OnDestroy {
       });
 
       // 检查项目繁忙情况,如果项目数>=3,也添加红色标记
-      const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
+      const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
       if (employeeProjects.length >= 3) {
         // 在当前日期添加繁忙标记
         const today = new Date();

+ 3 - 21
src/app/pages/team-leader/services/designer.service.ts

@@ -35,7 +35,6 @@ export class DesignerService {
   
   constructor() {
     this.cid = localStorage.getItem('company') || '';
-    console.log('🏢 DesignerService初始化,当前公司ID:', this.cid || '(未设置)');
     this.initParse();
   }
   
@@ -46,9 +45,8 @@ export class DesignerService {
     try {
       const { FmodeParse } = await import('fmode-ng/parse');
       this.Parse = FmodeParse.with("nova");
-      console.log('✅ DesignerService: FmodeParse 初始化成功');
     } catch (error) {
-      console.error('DesignerService: FmodeParse 初始化失败:', error);
+      console.error('DesignerService: FmodeParse 初始化失败:', error);
     }
   }
   
@@ -81,8 +79,6 @@ export class DesignerService {
       
       const profiles = await query.find();
       
-      console.log(`✅ 获取到 ${profiles.length} 个设计师(组员)`);
-      
       return profiles.map((p: any, index: number) => {
         const data = p.get('data') || {};
         const tags = data.tags || this.getDefaultTags();
@@ -114,11 +110,6 @@ export class DesignerService {
           name = `设计师-${p.id.slice(-4)}`;
         }
         
-        // 如果使用了备用名称,发出警告
-        if (!p.get('name')) {
-          console.warn(`⚠️ Profile ${p.id} 缺少 name 字段,使用备用显示: ${name}`);
-        }
-        
         return {
           id: p.id,
           name: name,
@@ -170,10 +161,9 @@ export class DesignerService {
       
       await profile.save();
       
-      console.log('✅ 设计师tags更新成功:', designerId);
       return true;
     } catch (error) {
-      console.error('更新设计师tags失败:', error);
+      console.error('更新设计师tags失败:', error);
       return false;
     }
   }
@@ -285,7 +275,6 @@ export class DesignerService {
     }
     
     try {
-      console.log('🔍 开始查询项目,company:', this.cid);
       const query = new Parse.Query('Project');
       query.equalTo('company', this.cid);
       query.notEqualTo('isDeleted', true);
@@ -294,12 +283,6 @@ export class DesignerService {
       query.limit(1000);
       
       const projects = await query.find();
-      console.log(`✅ Parse查询成功,找到 ${projects.length} 个项目`);
-      
-      if (projects.length === 0) {
-        console.warn('⚠️ 数据库中没有符合条件的项目数据');
-        console.warn('💡 提示:请确保Project表中有数据,且company字段=', this.cid);
-      }
       
       return projects.map((p: any) => this.transformProject(p));
     } catch (error) {
@@ -510,10 +493,9 @@ export class DesignerService {
       project.set('status', '进行中');
       
       await project.save();
-      console.log('✅ 项目分配成功');
       return true;
     } catch (error) {
-      console.error('项目分配失败:', error);
+      console.error('项目分配失败:', error);
       return false;
     }
   }

+ 302 - 245
src/modules/project/pages/project-survey/project-survey.component.html

@@ -1,243 +1,292 @@
-<ion-header>
-  <ion-toolbar>
-    <ion-buttons slot="start">
-      <ion-button (click)="goBack()">
-        <ion-icon name="arrow-back"></ion-icon>
-      </ion-button>
-    </ion-buttons>
-    <ion-title>项目需求调查</ion-title>
-  </ion-toolbar>
-</ion-header>
-
-<ion-content class="survey-content">
+<!-- 顶部导航栏 -->
+<div class="survey-header">
+  <div class="header-content">
+    <button class="back-button" (click)="goBack()">
+      <svg class="icon" viewBox="0 0 512 512">
+        <path fill="currentColor" d="M244 400L100 256l144-144M120 256h292"/>
+      </svg>
+    </button>
+    <h1 class="header-title">项目需求调查</h1>
+    <div class="header-spacer"></div>
+  </div>
+</div>
+
+<!-- 主内容区 -->
+<div class="survey-container">
   <!-- 加载状态 -->
   @if (loading) {
-    <div class="loading-container">
-      <ion-spinner name="crescent"></ion-spinner>
-      <p>加载中...</p>
+    <div class="status-view loading-view">
+      <div class="spinner">
+        <div class="spinner-circle"></div>
+      </div>
+      <p class="status-text">加载中...</p>
     </div>
   }
 
   <!-- 错误状态 -->
   @if (error && !loading) {
-    <div class="error-container">
-      <ion-icon name="alert-circle-outline"></ion-icon>
-      <p>{{ error }}</p>
-      <ion-button (click)="goBack()">返回</ion-button>
+    <div class="status-view error-view">
+      <div class="error-icon-wrapper">
+        <svg class="icon error-icon" viewBox="0 0 512 512">
+          @if (isCustomerOnly) {
+            <!-- 仅限客户图标 -->
+            <path fill="currentColor" d="M336 256c-20.56 0-40.44-9.18-56-25.84-15.13-16.25-24.37-37.92-26-61-1.74-24.62 5.77-47.26 21.14-63.76S312 80 336 80c23.83 0 45.38 9.06 60.7 25.52 15.47 16.62 23 39.22 21.26 63.63-1.67 23.11-10.9 44.77-26 61C376.44 246.82 356.57 256 336 256zm66-88c0-51.18-42.82-92-94-92s-94 40.82-94 92 42.82 92 94 92 94-40.82 94-92z" opacity=".3"/>
+            <path fill="currentColor" d="M336 256c-20.56 0-40.44-9.18-56-25.84-15.13-16.25-24.37-37.92-26-61-1.74-24.62 5.77-47.26 21.14-63.76S312 80 336 80c23.83 0 45.38 9.06 60.7 25.52 15.47 16.62 23 39.22 21.26 63.63-1.67 23.11-10.9 44.77-26 61C376.44 246.82 356.57 256 336 256zM467.83 432H204.18a27.71 27.71 0 01-22-10.67 30.22 30.22 0 01-5.26-25.79c8.42-33.81 29.28-61.85 60.32-81.08C264.79 297.4 299.86 288 336 288c36.85 0 71 9.23 98.83 26.73 31.45 19.86 52.3 48 60.38 81.55a30.27 30.27 0 01-5.32 25.78A27.68 27.68 0 01467.83 432z"/>
+          } @else {
+            <!-- 常规错误图标 -->
+            <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 48zm0 319.91a20 20 0 1120-20 20 20 0 01-20 20zm21.72-201.15l-5.74 122a16 16 0 01-32 0l-5.74-121.94v-.05a21.74 21.74 0 1143.44 0z"/>
+          }
+        </svg>
+      </div>
+      <h2 class="error-title">{{ isCustomerOnly ? '仅限客户填写' : '加载失败' }}</h2>
+      <p class="error-message">{{ error }}</p>
+      @if (isCustomerOnly) {
+        <div class="info-box">
+          <svg class="icon" viewBox="0 0 512 512">
+            <path fill="currentColor" d="M256 56C145.72 56 56 145.72 56 256s89.72 200 200 200 200-89.72 200-200S366.28 56 256 56zm0 82a26 26 0 11-26 26 26 26 0 0126-26zm48 226h-88a16 16 0 010-32h28v-88h-16a16 16 0 010-32h32a16 16 0 0116 16v104h28a16 16 0 010 32z"/>
+          </svg>
+          <div class="info-text">
+            <p class="info-title">客户填写入口</p>
+            <p class="info-desc">请通过企微群聊中收到的问卷链接进入</p>
+          </div>
+        </div>
+      }
+      <button class="btn-primary" (click)="goBack()">返回</button>
     </div>
   }
 
   <!-- 欢迎页 -->
   @if (currentState === 'welcome' && !loading && !error) {
-    <div class="welcome-page">
-      <div class="welcome-header">
-        @if (currentContact) {
-          <div class="user-avatar">
-            <img [src]="currentContact.get('data')?.avatar || 'assets/default-avatar.png'" alt="头像" />
-          </div>
-          <h2>您好,{{ currentContact.get('realname') || currentContact.get('name') }}</h2>
-        }
+    <div class="welcome-view">
+      <!-- 用户信息卡片 -->
+      <div class="user-card">
+        <div class="user-avatar">
+          <img [src]="currentContact?.get('data')?.avatar || 'assets/default-avatar.png'" alt="头像" />
+        </div>
+        <h2 class="user-greeting">您好, {{ currentContact?.get('realname') || currentContact?.get('name') }}</h2>
+        <p class="user-subtitle">欢迎参与需求调查</p>
       </div>
 
-      <div class="welcome-content">
-        <h1>《家装效果图服务初次合作需求调查表》</h1>
+      <!-- 问卷介绍 -->
+      <div class="intro-card">
+        <div class="intro-header">
+          <svg class="icon" viewBox="0 0 512 512">
+            <path fill="currentColor" d="M336 64h32a48 48 0 0148 48v320a48 48 0 01-48 48H144a48 48 0 01-48-48V112a48 48 0 0148-48h32" opacity=".3"/>
+            <path fill="currentColor" d="M336 64h-80a48 48 0 00-96 0h-80a48 48 0 00-48 48v320a48 48 0 0048 48h224a48 48 0 0048-48V112a48 48 0 00-48-48zM256 32a16 16 0 11-16 16 16 16 0 0116-16zm112 400H144V112h224z"/>
+          </svg>
+          <h3>家装效果图服务需求调查</h3>
+        </div>
 
-        <div class="welcome-intro">
-          <p>尊敬的伙伴:</p>
-          <p>为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷,您的偏好将直接帮我们校准服务方向,感谢支持!</p>
+        <div class="intro-body">
+          <p class="intro-text">
+            尊敬的伙伴,为让本次效果图服务更贴合您的工作节奏与核心需求,我们准备了简短选择式问卷。
+          </p>
+          <p class="intro-text">
+            您的偏好将直接帮我们校准服务方向,感谢支持!
+          </p>
         </div>
 
-        <div class="survey-info">
-          <div class="info-item">
-            <ion-icon name="time-outline"></ion-icon>
-            <span>预计用时: 3-5分钟</span>
+        <!-- 问卷信息标签 -->
+        <div class="info-tags">
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200zm61.8-104.4l-84.9-61.7c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h32c6.6 0 12 5.4 12 12v141.7l66.8 48.6c5.4 3.9 6.5 11.4 2.6 16.8L334.6 349c-3.9 5.3-11.4 6.5-16.8 2.6z"/>
+            </svg>
+            <span>3-5分钟</span>
           </div>
-          <div class="info-item">
-            <ion-icon name="list-outline"></ion-icon>
-            <span>题目数量: {{ effectiveQuestions.length }}题</span>
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M144 144v296a8 8 0 008 8h56V144zm144 0v304h56a8 8 0 008-8V144zm144 0v272a24 24 0 01-24 24h-40V144zM64 144v328a24 24 0 0024 24h40V144z" opacity=".3"/>
+            </svg>
+            <span>{{ effectiveQuestions.length }}道题</span>
           </div>
-          <div class="info-item">
-            <ion-icon name="checkmark-circle-outline"></ion-icon>
-            <span>题型: 选择题为主</span>
+          <div class="info-tag">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+            </svg>
+            <span>选择为主</span>
           </div>
         </div>
-
-        <ion-button expand="block" size="large" (click)="startSurvey()" class="start-button">
-          开始填写
-        </ion-button>
       </div>
+
+      <!-- 开始按钮 -->
+      <button class="btn-start" (click)="startSurvey()">
+        <span>开始填写</span>
+        <svg class="icon" viewBox="0 0 512 512">
+          <path fill="currentColor" d="M294.1 256L167 129c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.3 34 0L345 239c9.1 9.1 9.3 23.7.7 33.1L201.1 417c-4.7 4.7-10.9 7-17 7s-12.3-2.3-17-7c-9.4-9.4-9.4-24.6 0-33.9l127-127.1z"/>
+        </svg>
+      </button>
     </div>
   }
 
   <!-- 答题页 -->
   @if (currentState === 'questionnaire' && !loading && !error) {
-    <div class="questionnaire-page">
-      <!-- 进度条 -->
-      <div class="progress-bar">
-        <div class="progress-fill" [style.width.%]="getProgress()"></div>
-      </div>
-      <div class="progress-text">
-        {{ currentQuestionIndex + 1 }} / {{ effectiveQuestions.length }}
+    <div class="questionnaire-view">
+      <!-- 进度指示器 -->
+      <div class="progress-section">
+        <div class="progress-bar-wrapper">
+          <div class="progress-bar">
+            <div class="progress-fill" [style.width.%]="getProgress()"></div>
+          </div>
+        </div>
+        <div class="progress-info">
+          <span class="progress-current">{{ currentQuestionIndex + 1 }}</span>
+          <span class="progress-separator">/</span>
+          <span class="progress-total">{{ effectiveQuestions.length }}</span>
+        </div>
       </div>
 
       @if (getCurrentQuestion(); as question) {
-        <div class="question-container">
-          <!-- 章节标题 -->
-          <div class="section-title">{{ question.section }}</div>
-
-          <!-- 题目 -->
-          <div class="question-title">
-            <span class="question-number">{{ currentQuestionIndex + 1 }}.</span>
-            {{ question.title }}
-            @if (question.required) {
-              <span class="required-mark">*</span>
-            }
-          </div>
-
-          <!-- 单选题 -->
-          @if (question.type === 'single') {
-            <div class="options-container">
-              @for (option of question.options; track option) {
-                <div
-                  class="option-item"
-                  [class.selected]="answers[question.id] === option"
-                  (click)="selectSingleOption(option)"
-                >
-                  <div class="option-radio">
-                    @if (answers[question.id] === option) {
-                      <ion-icon name="radio-button-on"></ion-icon>
-                    } @else {
-                      <ion-icon name="radio-button-off"></ion-icon>
-                    }
-                  </div>
-                  <div class="option-text">{{ option }}</div>
-                </div>
+        <!-- 题目卡片 -->
+        <div class="question-card">
+          <!-- 章节标签 -->
+          <div class="section-badge">{{ question.section }}</div>
+
+          <!-- 题目内容 -->
+          <div class="question-content">
+            <h3 class="question-title">
+              <span class="question-number">{{ currentQuestionIndex + 1 }}.</span>
+              <span class="question-text">{{ question.title }}</span>
+              @if (question.required) {
+                <span class="required-star">*</span>
               }
-
-              @if (question.hasOther) {
-                <div
-                  class="option-item"
-                  [class.selected]="answers[question.id]?.startsWith('其他')"
-                  (click)="selectSingleOption('其他')"
-                >
-                  <div class="option-radio">
-                    @if (answers[question.id]?.startsWith('其他')) {
-                      <ion-icon name="radio-button-on"></ion-icon>
-                    } @else {
-                      <ion-icon name="radio-button-off"></ion-icon>
-                    }
+            </h3>
+
+            <!-- 单选题 -->
+            @if (question.type === 'single') {
+              <div class="options-list">
+                @for (option of question.options; track option) {
+                  <div
+                    class="option-item"
+                    [class.selected]="answers[question.id] === option"
+                    (click)="selectSingleOption(option)">
+                    <div class="option-radio">
+                      <div class="radio-outer">
+                        <div class="radio-inner"></div>
+                      </div>
+                    </div>
+                    <span class="option-label">{{ option }}</span>
                   </div>
-                  <div class="option-text">其他</div>
-                </div>
-              }
-            </div>
-
-            @if (showOtherInput) {
-              <div class="other-input-container">
-                <ion-input
-                  [(ngModel)]="otherInput"
-                  placeholder="请输入其他内容..."
-                  class="other-input"
-                ></ion-input>
+                }
+
+                @if (question.hasOther) {
+                  <div
+                    class="option-item"
+                    [class.selected]="answers[question.id]?.startsWith('其他')"
+                    (click)="selectSingleOption('其他')">
+                    <div class="option-radio">
+                      <div class="radio-outer">
+                        <div class="radio-inner"></div>
+                      </div>
+                    </div>
+                    <span class="option-label">其他</span>
+                  </div>
+                }
               </div>
-            }
-          }
 
-          <!-- 多选题 -->
-          @if (question.type === 'multiple') {
-            <div class="options-container">
-              @for (option of question.options; track option) {
-                <div
-                  class="option-item"
-                  [class.selected]="hasMultipleOption(question.id, option)"
-                  (click)="toggleMultipleOption(option)"
-                >
-                  <div class="option-checkbox">
-                    @if (hasMultipleOption(question.id, option)) {
-                      <ion-icon name="checkbox"></ion-icon>
-                    } @else {
-                      <ion-icon name="square-outline"></ion-icon>
-                    }
-                  </div>
-                  <div class="option-text">{{ option }}</div>
+              @if (showOtherInput) {
+                <div class="input-wrapper">
+                  <input
+                    type="text"
+                    class="text-input"
+                    [(ngModel)]="otherInput"
+                    placeholder="请输入其他内容..."
+                    autofocus />
                 </div>
               }
+            }
 
-              @if (question.hasOther) {
-                <div
-                  class="option-item"
-                  [class.selected]="hasMultipleOptionStartsWith(question.id, '其他')"
-                  (click)="toggleMultipleOption('其他')"
-                >
-                  <div class="option-checkbox">
-                    @if (hasMultipleOptionStartsWith(question.id, '其他')) {
-                      <ion-icon name="checkbox"></ion-icon>
-                    } @else {
-                      <ion-icon name="square-outline"></ion-icon>
-                    }
+            <!-- 多选题 -->
+            @if (question.type === 'multiple') {
+              <div class="options-list">
+                @for (option of question.options; track option) {
+                  <div
+                    class="option-item"
+                    [class.selected]="hasMultipleOption(question.id, option)"
+                    (click)="toggleMultipleOption(option)">
+                    <div class="option-checkbox">
+                      <div class="checkbox-box">
+                        <svg class="icon checkmark" viewBox="0 0 512 512">
+                          <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+                        </svg>
+                      </div>
+                    </div>
+                    <span class="option-label">{{ option }}</span>
+                  </div>
+                }
+
+                @if (question.hasOther) {
+                  <div
+                    class="option-item"
+                    [class.selected]="hasMultipleOptionStartsWith(question.id, '其他')"
+                    (click)="toggleMultipleOption('其他')">
+                    <div class="option-checkbox">
+                      <div class="checkbox-box">
+                        <svg class="icon checkmark" viewBox="0 0 512 512">
+                          <path fill="currentColor" d="M173.898 439.404l-166.4-166.4c-9.997-9.997-9.997-26.206 0-36.204l36.203-36.204c9.997-9.998 26.207-9.998 36.204 0L192 312.69 432.095 72.596c9.997-9.997 26.207-9.997 36.204 0l36.203 36.204c9.997 9.997 9.997 26.206 0 36.204l-294.4 294.401c-9.998 9.997-26.207 9.997-36.204-.001z"/>
+                        </svg>
+                      </div>
+                    </div>
+                    <span class="option-label">其他</span>
                   </div>
-                  <div class="option-text">其他</div>
+                }
+              </div>
+
+              @if (showOtherInput) {
+                <div class="input-wrapper">
+                  <input
+                    type="text"
+                    class="text-input"
+                    [(ngModel)]="otherInput"
+                    placeholder="请输入其他内容..." />
                 </div>
               }
-            </div>
+            }
 
-            @if (showOtherInput) {
-              <div class="other-input-container">
-                <ion-input
-                  [(ngModel)]="otherInput"
-                  placeholder="请输入其他内容..."
-                  class="other-input"
-                ></ion-input>
+            <!-- 文本题 -->
+            @if (question.type === 'text') {
+              <div class="input-wrapper">
+                <textarea
+                  class="textarea-input"
+                  [(ngModel)]="answers[question.id]"
+                  [placeholder]="question.placeholder || '请输入...'"
+                  rows="4"></textarea>
               </div>
             }
-          }
 
-          <!-- 文本题 -->
-          @if (question.type === 'text') {
-            <div class="text-input-container">
-              <ion-textarea
-                [(ngModel)]="answers[question.id]"
-                [placeholder]="question.placeholder || '请输入...'"
-                rows="4"
-                class="text-input"
-              ></ion-textarea>
-            </div>
-          }
-
-          <!-- 数字题 -->
-          @if (question.type === 'number') {
-            <div class="number-input-container">
-              <ion-input
-                type="number"
-                [(ngModel)]="answers[question.id]"
-                [placeholder]="question.placeholder || '请输入数字...'"
-                class="number-input"
-              ></ion-input>
-            </div>
-          }
+            <!-- 数字题 -->
+            @if (question.type === 'number') {
+              <div class="input-wrapper">
+                <input
+                  type="number"
+                  class="text-input"
+                  [(ngModel)]="answers[question.id]"
+                  [placeholder]="question.placeholder || '请输入数字...'" />
+              </div>
+            }
+          </div>
         </div>
 
         <!-- 导航按钮 -->
         <div class="nav-buttons">
-          <ion-button
-            fill="outline"
-            (click)="previousQuestion()"
+          <button
+            class="btn-nav btn-prev"
             [disabled]="currentQuestionIndex === 0"
-          >
-            <ion-icon name="chevron-back" slot="start"></ion-icon>
-            上一题
-          </ion-button>
-
-          <ion-button
-            (click)="nextQuestion()"
-          >
-            @if (currentQuestionIndex >= effectiveQuestions.length - 1) {
-              提交
-            } @else {
-              下一题
-            }
-            <ion-icon name="chevron-forward" slot="end"></ion-icon>
-          </ion-button>
+            (click)="previousQuestion()">
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M217.9 256L345 129c9.4-9.4 9.4-24.6 0-33.9-9.4-9.4-24.6-9.3-34 0L167 239c-9.1 9.1-9.3 23.7-.7 33.1L310.9 417c4.7 4.7 10.9 7 17 7s12.3-2.3 17-7c9.4-9.4 9.4-24.6 0-33.9L217.9 256z"/>
+            </svg>
+            <span>上一题</span>
+          </button>
+
+          <button
+            class="btn-nav btn-next"
+            (click)="nextQuestion()">
+            <span>{{ currentQuestionIndex >= effectiveQuestions.length - 1 ? '提交' : '下一题' }}</span>
+            <svg class="icon" viewBox="0 0 512 512">
+              <path fill="currentColor" d="M294.1 256L167 129c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.3 34 0L345 239c9.1 9.1 9.3 23.7.7 33.1L201.1 417c-4.7 4.7-10.9 7-17 7s-12.3-2.3-17-7c-9.4-9.4-9.4-24.6 0-33.9l127-127.1z"/>
+            </svg>
+          </button>
         </div>
       }
     </div>
@@ -245,81 +294,89 @@
 
   <!-- 结果页 -->
   @if (currentState === 'result' && !loading && !error) {
-    <div class="result-page">
-      <div class="result-header">
-        <ion-icon name="checkmark-circle" color="success"></ion-icon>
-        <h2>问卷提交成功</h2>
-        <p>感谢您的反馈!</p>
-        <p>我们将根据您的选择制定服务方案</p>
+    <div class="result-view">
+      <!-- 成功图标 -->
+      <div class="success-icon-wrapper">
+        <svg class="icon success-icon" 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 48zm108.25 138.29l-134.4 160a16 16 0 01-12 5.71h-.27a16 16 0 01-11.89-5.3l-57.6-64a16 16 0 1123.78-21.4l45.29 50.32 122.59-145.91a16 16 0 0124.5 20.58z"/>
+        </svg>
       </div>
 
-      <div class="result-content">
-        <h3>【您的答卷】</h3>
+      <h2 class="result-title">问卷提交成功</h2>
+      <p class="result-subtitle">感谢您的反馈!</p>
+      <p class="result-desc">我们将根据您的选择制定服务方案</p>
 
-        <div class="result-item">
-          <div class="result-label">核心服务:</div>
-          <div class="result-value">{{ getFormattedAnswer('q1') }}</div>
-        </div>
+      <!-- 答卷内容 -->
+      <div class="result-card">
+        <h3 class="result-card-title">您的答卷</h3>
 
-        <div class="result-item">
-          <div class="result-label">空间数量:</div>
-          <div class="result-value">{{ getFormattedAnswer('q2') }}</div>
-        </div>
-
-        <div class="result-item">
-          <div class="result-label">价值侧重:</div>
-          <div class="result-value">{{ getFormattedAnswer('q3') }}</div>
-        </div>
-
-        <div class="result-item">
-          <div class="result-label">技术配合:</div>
-          <div class="result-value">{{ getFormattedAnswer('q4') }}</div>
-        </div>
+        <div class="result-list">
+          <div class="result-item">
+            <div class="result-label">核心服务</div>
+            <div class="result-value">{{ getFormattedAnswer('q1') }}</div>
+          </div>
 
-        <div class="result-item">
-          <div class="result-label">协作方式:</div>
-          <div class="result-value">{{ getFormattedAnswer('q5') }}</div>
-        </div>
+          <div class="result-item">
+            <div class="result-label">空间数量</div>
+            <div class="result-value">{{ getFormattedAnswer('q2') }}</div>
+          </div>
 
-        @if (answers['q6']) {
           <div class="result-item">
-            <div class="result-label">注意事项:</div>
-            <div class="result-value">{{ getFormattedAnswer('q6') }}</div>
+            <div class="result-label">价值侧重</div>
+            <div class="result-value">{{ getFormattedAnswer('q3') }}</div>
           </div>
-        }
 
-        @if (answers['q7']) {
           <div class="result-item">
-            <div class="result-label">特殊要求:</div>
-            <div class="result-value">{{ getFormattedAnswer('q7') }}</div>
+            <div class="result-label">技术配合</div>
+            <div class="result-value">{{ getFormattedAnswer('q4') }}</div>
           </div>
-        }
 
-        @if (answers['q8']) {
           <div class="result-item">
-            <div class="result-label">参考素材:</div>
-            <div class="result-value">{{ getFormattedAnswer('q8') }}</div>
+            <div class="result-label">协作方式</div>
+            <div class="result-value">{{ getFormattedAnswer('q5') }}</div>
           </div>
-        }
 
-        <div class="result-divider"></div>
+          @if (answers['q6']) {
+            <div class="result-item">
+              <div class="result-label">注意事项</div>
+              <div class="result-value">{{ getFormattedAnswer('q6') }}</div>
+            </div>
+          }
+
+          @if (answers['q7']) {
+            <div class="result-item">
+              <div class="result-label">特殊要求</div>
+              <div class="result-value">{{ getFormattedAnswer('q7') }}</div>
+            </div>
+          }
 
-        <div class="result-item">
-          <div class="result-label">对接人:</div>
-          <div class="result-value">{{ answers['contact_name'] || currentContact?.get('realname') || '-' }}</div>
+          @if (answers['q8']) {
+            <div class="result-item">
+              <div class="result-label">参考素材</div>
+              <div class="result-value">{{ getFormattedAnswer('q8') }}</div>
+            </div>
+          }
         </div>
 
-        <div class="result-item">
-          <div class="result-label">电话:</div>
-          <div class="result-value">
-            {{ maskPhone(answers['contact_phone'] || currentContact?.get('mobile') || '') }}
+        <div class="result-divider"></div>
+
+        <div class="result-list">
+          <div class="result-item">
+            <div class="result-label">对接人</div>
+            <div class="result-value">{{ answers['contact_name'] || currentContact?.get('realname') || '-' }}</div>
+          </div>
+
+          <div class="result-item">
+            <div class="result-label">电话</div>
+            <div class="result-value">{{ maskPhone(answers['contact_phone'] || currentContact?.get('mobile') || '') }}</div>
           </div>
         </div>
       </div>
 
-      <ion-button expand="block" size="large" (click)="goBack()" class="back-button">
-        返回项目
-      </ion-button>
+      <!-- 返回按钮 -->
+      <button class="btn-primary" (click)="goBack()">
+        <span>返回项目</span>
+      </button>
     </div>
   }
-</ion-content>
+</div>

+ 749 - 231
src/modules/project/pages/project-survey/project-survey.component.scss

@@ -1,48 +1,222 @@
-.survey-content {
-  --background: #f5f5f5;
+// 项目问卷组件 - 移动端优先设计
+
+// CSS 变量
+:host {
+  --primary-color: #3880ff;
+  --success-color: #2dd36f;
+  --warning-color: #ffc409;
+  --danger-color: #eb445a;
+  --dark-color: #222428;
+  --medium-color: #92949c;
+  --light-color: #f4f5f8;
+  --white: #ffffff;
+  --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08);
+  --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.12);
+  --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.16);
+  --radius-sm: 8px;
+  --radius-md: 12px;
+  --radius-lg: 16px;
+  --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 }
 
-// 加载和错误状态
-.loading-container,
-.error-container {
+// 重置样式
+* {
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: transparent;
+}
+
+// 顶部导航栏
+.survey-header {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1000;
+  background: var(--white);
+  box-shadow: var(--shadow-sm);
+
+  .header-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 56px;
+    padding: 0 16px;
+    max-width: 640px;
+    margin: 0 auto;
+  }
+
+  .back-button {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    border: none;
+    background: transparent;
+    color: var(--dark-color);
+    cursor: pointer;
+    border-radius: var(--radius-sm);
+    transition: var(--transition);
+
+    &:active {
+      background: var(--light-color);
+      transform: scale(0.95);
+    }
+
+    .icon {
+      width: 24px;
+      height: 24px;
+    }
+  }
+
+  .header-title {
+    flex: 1;
+    margin: 0 16px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--dark-color);
+    text-align: center;
+  }
+
+  .header-spacer {
+    width: 40px;
+  }
+}
+
+// 主容器
+.survey-container {
+  min-height: 100vh;
+  padding-top: 56px;
+  background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
+}
+
+// 状态视图(加载/错误)
+.status-view {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: center;
-  min-height: 60vh;
-  padding: 2rem;
+  min-height: calc(100vh - 56px);
+  padding: 32px 24px;
   text-align: center;
 
-  ion-icon {
-    font-size: 4rem;
-    color: var(--ion-color-medium);
-    margin-bottom: 1rem;
+  .status-text {
+    margin: 16px 0 0;
+    font-size: 16px;
+    color: var(--medium-color);
+  }
+}
+
+// 加载动画
+.spinner {
+  width: 48px;
+  height: 48px;
+  position: relative;
+
+  .spinner-circle {
+    width: 100%;
+    height: 100%;
+    border: 4px solid var(--light-color);
+    border-top-color: var(--primary-color);
+    border-radius: 50%;
+    animation: spin 0.8s linear infinite;
+  }
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+// 错误视图
+.error-view {
+  .error-icon-wrapper {
+    width: 80px;
+    height: 80px;
+    margin-bottom: 24px;
+    border-radius: 50%;
+    background: rgba(235, 68, 90, 0.1);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .error-icon {
+      width: 48px;
+      height: 48px;
+      color: var(--danger-color);
+    }
   }
 
-  p {
-    font-size: 1rem;
-    color: var(--ion-color-medium);
-    margin: 1rem 0;
+  .error-title {
+    margin: 0 0 12px;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--dark-color);
+  }
+
+  .error-message {
+    margin: 0 0 24px;
+    font-size: 15px;
+    color: var(--medium-color);
+    line-height: 1.5;
+  }
+
+  .info-box {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+    padding: 16px;
+    margin-bottom: 24px;
+    background: rgba(255, 196, 9, 0.1);
+    border-radius: var(--radius-md);
+    border-left: 4px solid var(--warning-color);
+    text-align: left;
+
+    .icon {
+      width: 24px;
+      height: 24px;
+      color: var(--warning-color);
+      flex-shrink: 0;
+      margin-top: 2px;
+    }
+
+    .info-text {
+      flex: 1;
+
+      .info-title {
+        margin: 0 0 4px;
+        font-size: 14px;
+        font-weight: 600;
+        color: var(--dark-color);
+      }
+
+      .info-desc {
+        margin: 0;
+        font-size: 13px;
+        color: var(--medium-color);
+        line-height: 1.4;
+      }
+    }
   }
 }
 
 // 欢迎页
-.welcome-page {
-  padding: 2rem 1.5rem;
-  max-width: 600px;
+.welcome-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 24px 16px 32px;
 
-  .welcome-header {
+  .user-card {
     text-align: center;
-    margin-bottom: 2rem;
+    margin-bottom: 24px;
+    animation: fadeInUp 0.6s ease;
 
     .user-avatar {
       width: 80px;
       height: 80px;
-      margin: 0 auto 1rem;
+      margin: 0 auto 16px;
       border-radius: 50%;
       overflow: hidden;
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+      box-shadow: var(--shadow-md);
 
       img {
         width: 100%;
@@ -51,37 +225,58 @@
       }
     }
 
-    h2 {
-      font-size: 1.5rem;
+    .user-greeting {
+      margin: 0 0 8px;
+      font-size: 24px;
       font-weight: 600;
-      color: var(--ion-color-dark);
+      color: var(--dark-color);
+    }
+
+    .user-subtitle {
       margin: 0;
+      font-size: 15px;
+      color: var(--medium-color);
     }
   }
 
-  .welcome-content {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+  .intro-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 24px;
+    box-shadow: var(--shadow-sm);
+    animation: fadeInUp 0.6s ease 0.1s both;
 
-    h1 {
-      font-size: 1.25rem;
-      font-weight: 600;
-      color: var(--ion-color-primary);
-      margin: 0 0 1.5rem;
-      text-align: center;
-      line-height: 1.6;
+    .intro-header {
+      display: flex;
+      align-items: flex-start;
+      gap: 12px;
+      margin-bottom: 20px;
+
+      .icon {
+        width: 28px;
+        height: 28px;
+        color: var(--primary-color);
+        flex-shrink: 0;
+      }
+
+      h3 {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--dark-color);
+        line-height: 1.4;
+      }
     }
 
-    .welcome-intro {
-      margin-bottom: 2rem;
-      line-height: 1.8;
+    .intro-body {
+      margin-bottom: 20px;
 
-      p {
-        margin: 0 0 1rem;
-        color: var(--ion-color-dark);
-        font-size: 0.95rem;
+      .intro-text {
+        margin: 0 0 12px;
+        font-size: 15px;
+        color: var(--dark-color);
+        line-height: 1.6;
 
         &:last-child {
           margin-bottom: 0;
@@ -89,279 +284,602 @@
       }
     }
 
-    .survey-info {
-      background: #f9f9f9;
-      border-radius: 8px;
-      padding: 1.5rem;
-      margin-bottom: 2rem;
+    .info-tags {
+      display: flex;
+      justify-content: space-around;
+      gap: 12px;
+      padding-top: 20px;
+      border-top: 1px solid var(--light-color);
 
-      .info-item {
+      .info-tag {
         display: flex;
+        flex-direction: column;
         align-items: center;
-        margin-bottom: 0.75rem;
-
-        &:last-child {
-          margin-bottom: 0;
-        }
+        gap: 8px;
+        flex: 1;
 
-        ion-icon {
-          font-size: 1.25rem;
-          color: var(--ion-color-primary);
-          margin-right: 0.75rem;
+        .icon {
+          width: 24px;
+          height: 24px;
+          color: var(--primary-color);
+          opacity: 0.8;
         }
 
         span {
-          font-size: 0.95rem;
-          color: var(--ion-color-dark);
+          font-size: 13px;
+          font-weight: 500;
+          color: var(--medium-color);
         }
       }
     }
+  }
+
+  .btn-start {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    height: 56px;
+    background: var(--primary-color);
+    color: var(--white);
+    border: none;
+    border-radius: var(--radius-md);
+    font-size: 16px;
+    font-weight: 600;
+    box-shadow: var(--shadow-md);
+    cursor: pointer;
+    transition: var(--transition);
+    animation: fadeInUp 0.6s ease 0.2s both;
+
+    &:active {
+      transform: translateY(2px);
+      box-shadow: var(--shadow-sm);
+    }
 
-    .start-button {
-      margin-top: 1.5rem;
-      --border-radius: 8px;
-      font-weight: 600;
+    .icon {
+      width: 20px;
+      height: 20px;
     }
   }
 }
 
 // 答题页
-.questionnaire-page {
-  padding: 1.5rem;
-  max-width: 600px;
+.questionnaire-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 24px 16px 32px;
 
-  .progress-bar {
-    height: 4px;
-    background: #e0e0e0;
-    border-radius: 2px;
-    overflow: hidden;
-    margin-bottom: 0.5rem;
-
-    .progress-fill {
-      height: 100%;
-      background: var(--ion-color-primary);
-      transition: width 0.3s ease;
-    }
-  }
+  .progress-section {
+    margin-bottom: 24px;
 
-  .progress-text {
-    text-align: center;
-    font-size: 0.875rem;
-    color: var(--ion-color-medium);
-    margin-bottom: 1.5rem;
-  }
+    .progress-bar-wrapper {
+      margin-bottom: 12px;
 
-  .question-container {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
-    margin-bottom: 1.5rem;
+      .progress-bar {
+        height: 6px;
+        background: var(--light-color);
+        border-radius: 3px;
+        overflow: hidden;
 
-    .section-title {
-      font-size: 0.875rem;
-      color: var(--ion-color-primary);
-      font-weight: 600;
-      margin-bottom: 1rem;
-      text-transform: uppercase;
-      letter-spacing: 0.5px;
+        .progress-fill {
+          height: 100%;
+          background: linear-gradient(90deg, var(--primary-color) 0%, #5a9cff 100%);
+          border-radius: 3px;
+          transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+        }
+      }
     }
 
-    .question-title {
-      font-size: 1.125rem;
-      font-weight: 600;
-      color: var(--ion-color-dark);
-      margin-bottom: 1.5rem;
-      line-height: 1.6;
+    .progress-info {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 4px;
+      font-size: 14px;
 
-      .question-number {
-        color: var(--ion-color-primary);
-        margin-right: 0.5rem;
+      .progress-current {
+        font-size: 18px;
+        font-weight: 600;
+        color: var(--primary-color);
       }
 
-      .required-mark {
-        color: var(--ion-color-danger);
-        margin-left: 0.25rem;
+      .progress-separator {
+        color: var(--medium-color);
+      }
+
+      .progress-total {
+        color: var(--medium-color);
       }
     }
+  }
 
-    .options-container {
-      .option-item {
-        display: flex;
-        align-items: center;
-        padding: 1rem;
-        margin-bottom: 0.75rem;
-        border: 2px solid #e0e0e0;
-        border-radius: 8px;
-        cursor: pointer;
-        transition: all 0.2s ease;
+  .question-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 20px;
+    box-shadow: var(--shadow-sm);
+    animation: slideInRight 0.4s ease;
+
+    .section-badge {
+      display: inline-block;
+      padding: 4px 12px;
+      margin-bottom: 16px;
+      background: rgba(56, 128, 255, 0.1);
+      color: var(--primary-color);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 600;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
 
-        &:last-child {
-          margin-bottom: 0;
-        }
+    .question-content {
+      .question-title {
+        margin: 0 0 20px;
+        font-size: 17px;
+        font-weight: 600;
+        color: var(--dark-color);
+        line-height: 1.5;
 
-        &:hover {
-          border-color: var(--ion-color-primary-tint);
-          background: #f9f9f9;
+        .question-number {
+          color: var(--primary-color);
+          margin-right: 8px;
         }
 
-        &.selected {
-          border-color: var(--ion-color-primary);
-          background: var(--ion-color-primary-tint);
+        .question-text {
+          display: inline;
+        }
 
-          .option-text {
-            color: var(--ion-color-primary);
-            font-weight: 600;
-          }
+        .required-star {
+          color: var(--danger-color);
+          margin-left: 4px;
         }
+      }
+
+      .options-list {
+        display: flex;
+        flex-direction: column;
+        gap: 12px;
 
-        .option-radio,
-        .option-checkbox {
-          font-size: 1.5rem;
-          margin-right: 0.75rem;
+        .option-item {
           display: flex;
           align-items: center;
+          gap: 12px;
+          padding: 16px;
+          background: var(--white);
+          border: 2px solid var(--light-color);
+          border-radius: var(--radius-md);
+          cursor: pointer;
+          transition: var(--transition);
+
+          &:active {
+            transform: scale(0.98);
+          }
 
-          ion-icon {
-            color: var(--ion-color-primary);
+          &.selected {
+            border-color: var(--primary-color);
+            background: rgba(56, 128, 255, 0.05);
+
+            .option-radio .radio-outer {
+              border-color: var(--primary-color);
+
+              .radio-inner {
+                transform: scale(1);
+                background: var(--primary-color);
+              }
+            }
+
+            .option-checkbox .checkbox-box {
+              border-color: var(--primary-color);
+              background: var(--primary-color);
+
+              .checkmark {
+                opacity: 1;
+                transform: scale(1);
+              }
+            }
+
+            .option-label {
+              color: var(--primary-color);
+              font-weight: 600;
+            }
           }
-        }
 
-        .option-text {
-          flex: 1;
-          font-size: 1rem;
-          color: var(--ion-color-dark);
+          .option-radio {
+            flex-shrink: 0;
+
+            .radio-outer {
+              width: 24px;
+              height: 24px;
+              border: 2px solid var(--medium-color);
+              border-radius: 50%;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              transition: var(--transition);
+
+              .radio-inner {
+                width: 12px;
+                height: 12px;
+                border-radius: 50%;
+                transform: scale(0);
+                transition: var(--transition);
+              }
+            }
+          }
+
+          .option-checkbox {
+            flex-shrink: 0;
+
+            .checkbox-box {
+              width: 24px;
+              height: 24px;
+              border: 2px solid var(--medium-color);
+              border-radius: 6px;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              transition: var(--transition);
+
+              .checkmark {
+                width: 14px;
+                height: 14px;
+                color: var(--white);
+                opacity: 0;
+                transform: scale(0);
+                transition: var(--transition);
+              }
+            }
+          }
+
+          .option-label {
+            flex: 1;
+            font-size: 15px;
+            color: var(--dark-color);
+            line-height: 1.4;
+            transition: var(--transition);
+          }
         }
       }
-    }
 
-    .other-input-container,
-    .text-input-container,
-    .number-input-container {
-      margin-top: 1rem;
-
-      .other-input,
-      .text-input,
-      .number-input {
-        --background: #f9f9f9;
-        --padding-start: 1rem;
-        --padding-end: 1rem;
-        border-radius: 8px;
-        border: 2px solid #e0e0e0;
+      .input-wrapper {
+        margin-top: 12px;
+
+        .text-input,
+        .textarea-input {
+          width: 100%;
+          padding: 14px 16px;
+          border: 2px solid var(--light-color);
+          border-radius: var(--radius-md);
+          font-size: 15px;
+          color: var(--dark-color);
+          background: var(--white);
+          font-family: inherit;
+          transition: var(--transition);
+
+          &:focus {
+            outline: none;
+            border-color: var(--primary-color);
+            box-shadow: 0 0 0 4px rgba(56, 128, 255, 0.1);
+          }
+
+          &::placeholder {
+            color: var(--medium-color);
+          }
+        }
+
+        .textarea-input {
+          resize: vertical;
+          min-height: 100px;
+          line-height: 1.5;
+        }
       }
     }
   }
 
   .nav-buttons {
     display: flex;
-    gap: 1rem;
+    gap: 12px;
 
-    ion-button {
+    .btn-nav {
       flex: 1;
-      --border-radius: 8px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 8px;
+      height: 50px;
+      border: none;
+      border-radius: var(--radius-md);
+      font-size: 15px;
       font-weight: 600;
+      cursor: pointer;
+      transition: var(--transition);
+
+      .icon {
+        width: 18px;
+        height: 18px;
+      }
+
+      &.btn-prev {
+        background: var(--white);
+        color: var(--dark-color);
+        border: 2px solid var(--light-color);
+
+        &:active:not(:disabled) {
+          transform: translateX(-2px);
+          background: var(--light-color);
+        }
+
+        &:disabled {
+          opacity: 0.4;
+          cursor: not-allowed;
+        }
+      }
+
+      &.btn-next {
+        background: var(--primary-color);
+        color: var(--white);
+        box-shadow: var(--shadow-sm);
+
+        &:active {
+          transform: translateX(2px);
+          box-shadow: none;
+        }
+      }
     }
   }
 }
 
 // 结果页
-.result-page {
-  padding: 2rem 1.5rem;
-  max-width: 600px;
+.result-view {
+  max-width: 640px;
   margin: 0 auto;
+  padding: 32px 16px;
+  text-align: center;
 
-  .result-header {
-    text-align: center;
-    margin-bottom: 2rem;
-
-    ion-icon {
-      font-size: 4rem;
-      margin-bottom: 1rem;
+  .success-icon-wrapper {
+    width: 80px;
+    height: 80px;
+    margin: 0 auto 24px;
+    border-radius: 50%;
+    background: rgba(45, 211, 111, 0.1);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    animation: scaleIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+    .success-icon {
+      width: 48px;
+      height: 48px;
+      color: var(--success-color);
     }
+  }
 
-    h2 {
-      font-size: 1.5rem;
-      font-weight: 600;
-      color: var(--ion-color-dark);
-      margin: 0 0 0.5rem;
-    }
+  .result-title {
+    margin: 0 0 8px;
+    font-size: 24px;
+    font-weight: 600;
+    color: var(--dark-color);
+    animation: fadeInUp 0.6s ease 0.1s both;
+  }
 
-    p {
-      font-size: 1rem;
-      color: var(--ion-color-medium);
-      margin: 0.25rem 0;
-    }
+  .result-subtitle {
+    margin: 0 0 8px;
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--medium-color);
+    animation: fadeInUp 0.6s ease 0.2s both;
   }
 
-  .result-content {
-    background: white;
-    border-radius: 12px;
-    padding: 2rem;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
-    margin-bottom: 1.5rem;
+  .result-desc {
+    margin: 0 0 32px;
+    font-size: 14px;
+    color: var(--medium-color);
+    animation: fadeInUp 0.6s ease 0.3s both;
+  }
 
-    h3 {
-      font-size: 1.125rem;
+  .result-card {
+    background: var(--white);
+    border-radius: var(--radius-lg);
+    padding: 24px;
+    margin-bottom: 24px;
+    box-shadow: var(--shadow-sm);
+    text-align: left;
+    animation: fadeInUp 0.6s ease 0.4s both;
+
+    .result-card-title {
+      margin: 0 0 20px;
+      font-size: 16px;
       font-weight: 600;
-      color: var(--ion-color-dark);
-      margin: 0 0 1.5rem;
+      color: var(--dark-color);
       text-align: center;
     }
 
-    .result-item {
-      display: flex;
-      margin-bottom: 1rem;
-      padding-bottom: 1rem;
-      border-bottom: 1px solid #f0f0f0;
-
-      &:last-child {
-        margin-bottom: 0;
-        padding-bottom: 0;
-        border-bottom: none;
-      }
+    .result-list {
+      .result-item {
+        display: flex;
+        gap: 12px;
+        padding: 12px 0;
+        border-bottom: 1px solid var(--light-color);
 
-      .result-label {
-        width: 100px;
-        font-weight: 600;
-        color: var(--ion-color-medium);
-        font-size: 0.95rem;
-        flex-shrink: 0;
-      }
+        &:first-child {
+          padding-top: 0;
+        }
 
-      .result-value {
-        flex: 1;
-        color: var(--ion-color-dark);
-        font-size: 0.95rem;
-        line-height: 1.6;
+        &:last-child {
+          padding-bottom: 0;
+          border-bottom: none;
+        }
+
+        .result-label {
+          min-width: 80px;
+          font-size: 14px;
+          font-weight: 600;
+          color: var(--medium-color);
+          flex-shrink: 0;
+        }
+
+        .result-value {
+          flex: 1;
+          font-size: 14px;
+          color: var(--dark-color);
+          line-height: 1.5;
+          word-break: break-word;
+        }
       }
     }
 
     .result-divider {
       height: 1px;
-      background: #e0e0e0;
-      margin: 1.5rem 0;
+      background: var(--light-color);
+      margin: 20px 0;
     }
   }
 
-  .back-button {
-    --border-radius: 8px;
+  .btn-primary {
+    width: 100%;
+    height: 50px;
+    background: var(--primary-color);
+    color: var(--white);
+    border: none;
+    border-radius: var(--radius-md);
+    font-size: 16px;
     font-weight: 600;
+    box-shadow: var(--shadow-sm);
+    cursor: pointer;
+    transition: var(--transition);
+    animation: fadeInUp 0.6s ease 0.5s both;
+
+    &:active {
+      transform: translateY(2px);
+      box-shadow: none;
+    }
+  }
+}
+
+// 动画
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes slideInRight {
+  from {
+    opacity: 0;
+    transform: translateX(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0);
+  }
+}
+
+@keyframes scaleIn {
+  from {
+    opacity: 0;
+    transform: scale(0);
+  }
+  to {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+// 平板适配 (>= 768px)
+@media (min-width: 768px) {
+  .survey-header .header-content {
+    max-width: 768px;
+  }
+
+  .welcome-view,
+  .questionnaire-view,
+  .result-view {
+    max-width: 768px;
+    padding-left: 32px;
+    padding-right: 32px;
+  }
+
+  .intro-card,
+  .question-card,
+  .result-card {
+    padding: 32px;
+  }
+
+  .btn-start,
+  .btn-primary {
+    height: 56px;
+    font-size: 17px;
+  }
+
+  .nav-buttons .btn-nav {
+    height: 52px;
   }
 }
 
-// 响应式适配
+// 小屏适配 (<= 375px)
 @media (max-width: 375px) {
-  .welcome-page,
-  .questionnaire-page,
-  .result-page {
-    padding-left: 1rem;
-    padding-right: 1rem;
+  .survey-header {
+    .header-title {
+      font-size: 16px;
+    }
   }
 
-  .welcome-page .welcome-content,
-  .questionnaire-page .question-container,
-  .result-page .result-content {
-    padding: 1.5rem;
+  .welcome-view {
+    .user-greeting {
+      font-size: 20px;
+    }
+
+    .intro-card {
+      padding: 20px;
+
+      .intro-header h3 {
+        font-size: 16px;
+      }
+
+      .intro-text {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .questionnaire-view {
+    .question-card {
+      padding: 20px;
+
+      .question-title {
+        font-size: 16px;
+      }
+
+      .option-item {
+        padding: 14px;
+
+        .option-label {
+          font-size: 14px;
+        }
+      }
+    }
+
+    .nav-buttons .btn-nav {
+      height: 48px;
+      font-size: 14px;
+    }
+  }
+
+  .result-view {
+    .result-title {
+      font-size: 20px;
+    }
+
+    .result-card {
+      padding: 20px;
+    }
   }
 }

+ 21 - 7
src/modules/project/pages/project-survey/project-survey.component.ts

@@ -54,6 +54,7 @@ export class ProjectSurveyComponent implements OnInit {
   // 加载状态
   loading: boolean = true;
   error: string | null = null;
+  isCustomerOnly: boolean = false; // 是否仅限客户填写
 
   // 数据对象
   project: FmodeObject | null = null;
@@ -210,15 +211,28 @@ export class ProjectSurveyComponent implements OnInit {
       this.loading = true;
 
       // 1. 获取当前外部联系人
-      if (this.wxAuth) {
-        try {
-          this.currentContact = await this.wxAuth.currentContact();
-          console.log('✅ 当前联系人:', this.currentContact?.get('name'));
-        } catch (error) {
-          console.error('❌ 获取联系人失败:', error);
-          this.error = '无法识别您的身份,请通过企微群聊进入';
+      if (!this.wxAuth) {
+        this.isCustomerOnly = true;
+        this.error = '该问卷仅供客户填写,请通过企微群聊进入';
+        return;
+      }
+
+      try {
+        this.currentContact = await this.wxAuth.currentContact();
+        console.log('✅ 当前联系人:', this.currentContact?.get('name'));
+
+        // 验证是否为外部联系人
+        if (!this.currentContact || !this.currentContact.id) {
+          console.warn('⚠️ 未找到外部联系人信息');
+          this.isCustomerOnly = true;
+          this.error = '该问卷仅供客户填写';
           return;
         }
+      } catch (error) {
+        console.error('❌ 获取联系人失败:', error);
+        this.isCustomerOnly = true;
+        this.error = '该问卷仅供客户填写,请通过企微群聊进入';
+        return;
       }
 
       // 2. 加载项目