Bläddra i källkod

feat(甘特图): 增加日视图并优化设计师工作负荷可视化

refactor(上传成功弹窗): 实现完整HTML报告生成与下载功能
style(颜色描述区域): 改进UI视觉效果和交互体验
0235711 1 dag sedan
förälder
incheckning
70633a612c

+ 1 - 0
src/app/pages/team-leader/dashboard/dashboard.html

@@ -67,6 +67,7 @@
             @if (ganttMode === 'project') { 颜色标识紧急程度:红=高,橙=中,绿=低 } @else { 设计师排班:按项目数量由排满到空闲排列 }
           </div>
           <div class="scale-switch">
+            <button [class.active]="ganttScale === 'day'" (click)="setGanttScale('day')">日</button>
             <button [class.active]="ganttScale === 'week'" (click)="setGanttScale('week')">周</button>
             <button [class.active]="ganttScale === 'month'" (click)="setGanttScale('month')">月</button>
           </div>

+ 277 - 43
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -114,10 +114,14 @@ export class Dashboard implements OnInit, OnDestroy {
   
   // 设计师画像(用于智能推荐)
   designerProfiles: any[] = [
-    { id: 'zhang', name: '张三', skills: ['现代风格', '北欧风格'], workload: 70, avgRating: 4.5, experience: 3 },
-    { id: 'li', name: '李四', skills: ['新中式', '宋式风格'], workload: 45, avgRating: 4.8, experience: 5 },
-    { id: 'wang', name: '王五', skills: ['北欧风格', '日式风格'], workload: 85, avgRating: 4.2, experience: 2 },
-    { id: 'zhao', name: '赵六', skills: ['现代风格', '轻奢风格'], workload: 30, avgRating: 4.6, experience: 4 }
+    { id: 'zhang', name: '张三', skills: ['现代风格', '北欧风格'], workload: 95, avgRating: 4.5, experience: 3 },
+    { id: 'li', name: '李四', skills: ['新中式', '宋式风格'], workload: 25, avgRating: 4.8, experience: 5 },
+    { id: 'wang', name: '王五', skills: ['北欧风格', '日式风格'], workload: 75, avgRating: 4.2, experience: 2 },
+    { id: 'zhao', name: '赵六', skills: ['现代风格', '轻奢风格'], workload: 15, avgRating: 4.6, experience: 4 },
+    { id: 'sun', name: '孙七', skills: ['简约风格', '工业风格'], workload: 35, avgRating: 4.3, experience: 3 },
+    { id: 'zhou', name: '周八', skills: ['欧式风格', '美式风格'], workload: 5, avgRating: 4.7, experience: 6 },
+    { id: 'wu', name: '吴九', skills: ['地中海风格', '田园风格'], workload: 60, avgRating: 4.4, experience: 4 },
+    { id: 'chen', name: '陈十', skills: ['现代简约', '新古典'], workload: 0, avgRating: 4.9, experience: 7 }
   ];
 
   // 10个项目阶段
@@ -150,7 +154,7 @@ export class Dashboard implements OnInit, OnDestroy {
   private workloadChart: any | null = null;
   workloadDimension: 'designer' | 'member' = 'designer';
   // 甘特时间尺度:仅周/月
-  ganttScale: 'week' | 'month' = 'week';
+  ganttScale: 'day' | 'week' | 'month' = 'week';
   // 新增:甘特模式(项目 / 设计师排班)
   ganttMode: 'project' | 'designer' = 'project';
 
@@ -341,7 +345,7 @@ export class Dashboard implements OnInit, OnDestroy {
 
     // ===== 追加生成示例数据:保证总量达到100条 =====
     const stageIds = this.projectStages.map(s => s.id);
-    const designers = ['张三','李四','王五','赵六','钱七','孙八','周九','吴十','郑一','冯二','陈三','褚四'];
+    const designers = ['张三','李四','王五','赵六','孙七','周八','吴九','陈十'];
     const statusMap: Record<string, string> = {
       pendingApproval: '待确认',
       pendingAssignment: '待分配',
@@ -355,26 +359,32 @@ export class Dashboard implements OnInit, OnDestroy {
       delivery: '已完成'
     };
 
-    for (let i = 8; i <= 100; i++) {
-      const stageIndex = (i - 1) % stageIds.length;
+    // 为有项目的设计师分配项目
+    const busyDesigners = ['张三', '王五', '吴九']; // 高负荷设计师
+    const moderateDesigners = ['孙七']; // 中等负荷设计师
+    const idleDesigners = ['李四', '赵六', '周八', '陈十']; // 空闲设计师
+
+    // 为忙碌的设计师分配更多项目
+    for (let i = 8; i <= 30; i++) {
+      const designerIndex = (i - 8) % busyDesigners.length;
+      const designerName = busyDesigners[designerIndex];
+      const stageIndex = (i - 1) % 7 + 3; // 主要在进行中的阶段
       const currentStage = stageIds[stageIndex];
       const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
-      const urgency: 'high' | 'medium' | 'low' = i % 5 === 0 ? 'high' : (i % 3 === 0 ? 'medium' : 'low');
-      const isOverdue = ['planning','modeling','rendering','postProduction','review','revision','delivery'].includes(currentStage) ? i % 7 === 0 : false;
-      const overdueDays = isOverdue ? (i % 10) + 1 : 0;
-      const hasDesigner = !['pendingApproval', 'pendingAssignment'].includes(currentStage);
-      const designerName = hasDesigner ? designers[i % designers.length] : '';
+      const urgency: 'high' | 'medium' | 'low' = i % 4 === 0 ? 'high' : (i % 3 === 0 ? 'medium' : 'low');
+      const isOverdue = i % 8 === 0;
+      const overdueDays = isOverdue ? (i % 5) + 1 : 0;
       const status = statusMap[currentStage] || '进行中';
       const expectedEndDate = new Date();
-      const daysOffset = isOverdue ? - (overdueDays + (i % 5)) : ((i % 20) + 3);
+      const daysOffset = isOverdue ? -(overdueDays + (i % 3)) : ((i % 15) + 5);
       expectedEndDate.setDate(expectedEndDate.getDate() + daysOffset);
       const daysToDeadline = Math.ceil((expectedEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
       const dueSoon = !isOverdue && daysToDeadline >= 0 && daysToDeadline <= 3;
-      const memberType: 'vip' | 'normal' = i % 4 === 0 ? 'vip' : 'normal';
+      const memberType: 'vip' | 'normal' = i % 5 === 0 ? 'vip' : 'normal';
 
       this.projects.push({
         id: `proj-${String(i).padStart(3, '0')}`,
-        name: `${type === 'soft' ? '软装' : '硬装'}示例项目 ${i}`,
+        name: `${designerName}负责的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
         type,
         memberType,
         designerName,
@@ -389,6 +399,66 @@ export class Dashboard implements OnInit, OnDestroy {
         phases: []
       });
     }
+
+    // 为中等负荷设计师分配少量项目
+    for (let i = 31; i <= 35; i++) {
+      const designerName = moderateDesigners[0];
+      const stageIndex = (i - 1) % 5 + 4; // 中间阶段
+      const currentStage = stageIds[stageIndex];
+      const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
+      const urgency: 'high' | 'medium' | 'low' = 'medium';
+      const status = statusMap[currentStage] || '进行中';
+      const expectedEndDate = new Date();
+      expectedEndDate.setDate(expectedEndDate.getDate() + (i % 10) + 7);
+      const memberType: 'vip' | 'normal' = 'normal';
+
+      this.projects.push({
+        id: `proj-${String(i).padStart(3, '0')}`,
+        name: `${designerName}负责的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
+        type,
+        memberType,
+        designerName,
+        status,
+        expectedEndDate,
+        deadline: expectedEndDate,
+        isOverdue: false,
+        overdueDays: 0,
+        dueSoon: false,
+        urgency,
+        currentStage,
+        phases: []
+      });
+    }
+
+    // 空闲设计师不分配项目,或只分配很少的已完成项目
+    for (let i = 36; i <= 40; i++) {
+      const designerIndex = (i - 36) % idleDesigners.length;
+      const designerName = idleDesigners[designerIndex];
+      const currentStage = 'delivery'; // 已完成的项目
+      const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
+      const urgency: 'high' | 'medium' | 'low' = 'low';
+      const status = '已完成';
+      const expectedEndDate = new Date();
+      expectedEndDate.setDate(expectedEndDate.getDate() - (i % 10) - 5); // 过去的日期
+      const memberType: 'vip' | 'normal' = 'normal';
+
+      this.projects.push({
+        id: `proj-${String(i).padStart(3, '0')}`,
+        name: `${designerName}已完成的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
+        type,
+        memberType,
+        designerName,
+        status,
+        expectedEndDate,
+        deadline: expectedEndDate,
+        isOverdue: false,
+        overdueDays: 0,
+        dueSoon: false,
+        urgency,
+        currentStage,
+        phases: []
+      });
+    }
     // ===== 示例数据生成结束 =====
 
     // 统一补齐真实时间字段(deadline/createdAt),以真实字段贯通筛选与甘特
@@ -696,7 +766,7 @@ export class Dashboard implements OnInit, OnDestroy {
   }
 
   // 设置甘特时间尺度
-  setGanttScale(scale: 'week' | 'month'): void {
+  setGanttScale(scale: 'day' | 'week' | 'month'): void {
     if (this.ganttScale !== scale) {
       this.ganttScale = scale;
       this.updateGantt();
@@ -960,28 +1030,45 @@ export class Dashboard implements OnInit, OnDestroy {
     const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
     const todayTs = today.getTime();
 
-    // 时间轴按当前周/月
+    // 时间轴按当前周/月/日
     let xMin: number;
     let xMax: number;
     let xSplitNumber: number;
     let xLabelFormatter: (value: number) => string;
-    if (this.ganttScale === 'week') {
-      const day = today.getDay();
-      const diffToMonday = (day === 0 ? 6 : day - 1);
-      const startOfWeek = new Date(today.getTime() - diffToMonday * DAY);
-      const endOfWeek = new Date(startOfWeek.getTime() + 7 * DAY - 1);
+    if (this.ganttScale === 'day') {
+      // 日视图:显示今日24小时
+      const startOfDay = new Date(today.getTime());
+      const endOfDay = new Date(today.getTime() + DAY - 1);
+      xMin = startOfDay.getTime();
+      xMax = endOfDay.getTime();
+      xSplitNumber = 24;
+      xLabelFormatter = (val) => `${new Date(val).getHours()}:00`;
+    } else if (this.ganttScale === 'week') {
+      // 周视图:从今天开始显示未来7天的具体日期
+      const startOfWeek = new Date(today.getTime());
+      const endOfWeek = new Date(today.getTime() + 7 * DAY - 1);
       xMin = startOfWeek.getTime();
       xMax = endOfWeek.getTime();
       xSplitNumber = 7;
-      const WEEK_LABELS = ['周日','周一','周二','周三','周四','周五','周六'];
-      xLabelFormatter = (val) => WEEK_LABELS[new Date(val).getDay()];
+      xLabelFormatter = (val) => {
+        const date = new Date(val);
+        const month = date.getMonth() + 1;
+        const day = date.getDate();
+        return `${month}月${day}日`;
+      };
     } else {
+      // 月视图:从当前月份开始显示未来几个月
       const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
-      const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0, 23, 59, 59, 999);
+      const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 3, 0, 23, 59, 59, 999); // 显示未来3个月
       xMin = startOfMonth.getTime();
       xMax = endOfMonth.getTime();
-      xSplitNumber = 4;
-      xLabelFormatter = (val) => `第${Math.ceil(new Date(val).getDate() / 7)}周`;
+      xSplitNumber = 3;
+      xLabelFormatter = (val) => {
+        const date = new Date(val);
+        const year = date.getFullYear();
+        const month = date.getMonth() + 1;
+        return `${year}年${month}月`;
+      };
     }
 
     // 仅统计已分配项目
@@ -1001,18 +1088,28 @@ export class Dashboard implements OnInit, OnDestroy {
 
     const categories = sortedDesigners;
 
-    // 工作量等级(用于左侧小圆点颜色)
+    // 工作量等级(用于左侧小圆点颜色和负荷状态判断
     const workloadLevelMap: Record<string, 'high'|'medium'|'low'> = {} as any;
+    const workloadStatusMap: Record<string, 'overloaded'|'busy'|'available'> = {} as any;
     categories.forEach(name => {
       const cnt = busyCountMap[name] || 0;
-      workloadLevelMap[name] = cnt >= 5 ? 'high' : (cnt >= 3 ? 'medium' : 'low');
+      if (cnt >= 5) {
+        workloadLevelMap[name] = 'high';
+        workloadStatusMap[name] = 'overloaded'; // 不宜派单
+      } else if (cnt >= 3) {
+        workloadLevelMap[name] = 'medium';
+        workloadStatusMap[name] = 'busy'; // 适度忙碌
+      } else {
+        workloadLevelMap[name] = 'low';
+        workloadStatusMap[name] = 'available'; // 可接单
+      }
     });
 
-    // 条形颜色仍按项目紧急度
+    // 条形颜色按项目紧急度,增强高负荷时段的视觉效果
     const colorByUrgency: Record<'high'|'medium'|'low', string> = {
-      high: '#ef4444',
-      medium: '#f59e0b',
-      low: '#22c55e'
+      high: '#dc2626', // 更深的红色,突出高紧急度
+      medium: '#ea580c', // 更深的橙色
+      low: '#16a34a' // 更深的绿色
     } as const;
 
     const data = assigned.flatMap(p => {
@@ -1021,14 +1118,104 @@ export class Dashboard implements OnInit, OnDestroy {
       const start = p.createdAt ? new Date(p.createdAt).getTime() : end - baseDays * DAY;
       const yIndex = categories.indexOf(p.designerName);
       if (yIndex === -1) return [] as any[];
-      const color = colorByUrgency[p.urgency] || '#60a5fa';
+      
+      // 根据设计师工作负荷状态调整项目条的视觉效果
+      const workloadStatus = workloadStatusMap[p.designerName];
+      let color = colorByUrgency[p.urgency] || '#60a5fa';
+      let borderWidth = 1;
+      let borderColor = 'transparent';
+      
+      // 高负荷时段增强视觉效果
+      if (workloadStatus === 'overloaded') {
+        borderWidth = 3;
+        borderColor = '#991b1b'; // 深红色边框
+        // 对于超负荷状态,使用更深的红色调
+        if (p.urgency === 'high') {
+          color = '#7f1d1d'; // 深红色
+        } else if (p.urgency === 'medium') {
+          color = '#c2410c'; // 深橙色
+        } else {
+          color = '#dc2626'; // 红色(即使是低紧急度也用红色表示超负荷)
+        }
+      }
+      
       return [{
         name: p.name,
-        value: [yIndex, start, end, p.designerName, p.urgency, p.memberType, p.currentStage],
-        itemStyle: { color }
+        value: [yIndex, start, end, p.designerName, p.urgency, p.memberType, p.currentStage, workloadStatus],
+        itemStyle: { 
+          color,
+          borderWidth,
+          borderColor,
+          opacity: workloadStatus === 'overloaded' ? 0.9 : 1.0
+        }
       }];
     });
 
+    // 生成空闲时段背景数据 - 只在真正空闲的时间段显示
+    const idleBackgroundData: any[] = [];
+    
+    categories.forEach((designerName, yIndex) => {
+      const designerProjects = byDesigner[designerName] || [];
+      const workloadStatus = workloadStatusMap[designerName];
+      
+      // 获取该设计师的所有项目时间段
+      const projectTimeRanges = designerProjects.map(p => {
+        const end = new Date(p.deadline).getTime();
+        const baseDays = p.type === 'hard' ? 30 : 14;
+        const start = p.createdAt ? new Date(p.createdAt).getTime() : end - baseDays * DAY;
+        return { start, end };
+      }).sort((a, b) => a.start - b.start);
+      
+      // 找出空闲时间段
+      const idleTimeRanges: { start: number; end: number }[] = [];
+      
+      if (projectTimeRanges.length === 0) {
+        // 完全没有项目,整个时间轴都是空闲
+        idleTimeRanges.push({ start: xMin, end: xMax });
+      } else {
+        // 检查项目之间的空隙
+        let currentTime = xMin;
+        
+        for (const range of projectTimeRanges) {
+          if (currentTime < range.start) {
+            // 在项目开始前有空闲时间
+            idleTimeRanges.push({ start: currentTime, end: range.start });
+          }
+          currentTime = Math.max(currentTime, range.end);
+        }
+        
+        // 检查最后一个项目后是否还有空闲时间
+        if (currentTime < xMax) {
+          idleTimeRanges.push({ start: currentTime, end: xMax });
+        }
+      }
+      
+      // 为每个空闲时间段创建背景数据
+      idleTimeRanges.forEach((idleRange, index) => {
+        // 只有当空闲时间段足够长时才显示(至少1天)
+        if (idleRange.end - idleRange.start >= DAY) {
+          let backgroundColor = 'transparent';
+          
+          if (workloadStatus === 'available') {
+            backgroundColor = 'rgba(34, 197, 94, 0.15)'; // 淡绿色背景表示空闲可接单
+          } else if (workloadStatus === 'overloaded') {
+            backgroundColor = 'rgba(239, 68, 68, 0.1)'; // 淡红色背景表示超负荷
+          }
+          
+          if (backgroundColor !== 'transparent') {
+            idleBackgroundData.push({
+              name: `${designerName}-空闲${index + 1}`,
+              value: [yIndex, idleRange.start, idleRange.end, designerName, 'background', workloadStatus],
+              itemStyle: { 
+                color: backgroundColor,
+                borderWidth: 0
+              }
+            });
+          }
+        }
+      });
+    });
+
     const prevOpt: any = (this.ganttChart as any).getOption ? (this.ganttChart as any).getOption() : null;
     const total = categories.length || 1;
     const visible = Math.min(total, 30);
@@ -1042,12 +1229,27 @@ export class Dashboard implements OnInit, OnDestroy {
         trigger: 'item',
         formatter: (params: any) => {
           const v = params.value;
+          if (v[4] === 'background') {
+            const workloadStatus = v[5];
+            const statusText = workloadStatus === 'available' ? '空闲可接单' : 
+                              workloadStatus === 'overloaded' ? '超负荷不宜派单' : '适度忙碌';
+            return `设计师:${v[3]}<br/>状态:${statusText}`;
+          }
           const start = new Date(v[1]);
           const end = new Date(v[2]);
-          return `项目:${params.name}<br/>设计师:${v[3]}<br/>阶段:${v[6]}<br/>起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`;
+          const workloadStatus = v[7];
+          const statusText = workloadStatus === 'available' ? '可接单' : 
+                            workloadStatus === 'overloaded' ? '超负荷' : '适度忙碌';
+          return `项目:${params.name}<br/>设计师:${v[3]}(${statusText})<br/>阶段:${v[6]}<br/>起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`;
         }
       },
-      grid: { left: 110, right: 64, top: 30, bottom: 30 },
+      legend: {
+        data: ['空闲可接单', '适度忙碌', '超负荷不宜派单', '高紧急', '中紧急', '低紧急'],
+        bottom: 0,
+        itemGap: 20,
+        textStyle: { fontSize: 12 }
+      },
+      grid: { left: 110, right: 64, top: 30, bottom: 60 },
       xAxis: {
         type: 'time',
         min: xMin,
@@ -1067,13 +1269,16 @@ export class Dashboard implements OnInit, OnDestroy {
           formatter: (val: string) => {
             const lvl = workloadLevelMap[val] || 'low';
             const count = busyCountMap[val] || 0;
+            const status = workloadStatusMap[val] || 'available';
             const text = val.length > 8 ? val.slice(0, 8) + '…' : val;
-            return `{${lvl}Dot|●} ${text}(${count}项)`;
+            const statusIcon = status === 'available' ? '🟢' : 
+                              status === 'overloaded' ? '🔴' : '🟡';
+            return `{${lvl}Dot|●} ${statusIcon} ${text}(${count}项)`;
           },
           rich: {
-            highDot: { color: '#ef4444' },
-            mediumDot: { color: '#f59e0b' },
-            lowDot: { color: '#22c55e' }
+            highDot: { color: '#dc2626' },
+            mediumDot: { color: '#ea580c' },
+            lowDot: { color: '#16a34a' }
           }
         },
         axisTick: { show: false },
@@ -1084,8 +1289,36 @@ export class Dashboard implements OnInit, OnDestroy {
         { type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true }
       ],
       series: [
+        // 背景层 - 显示空闲时段
         {
           type: 'custom',
+          name: '工作负荷背景',
+          renderItem: (params: any, api: any) => {
+            const categoryIndex = api.value(0);
+            const start = api.coord([api.value(1), categoryIndex]);
+            const end = api.coord([api.value(2), categoryIndex]);
+            const height = api.size([0, 1])[1] * 0.8;
+            const rectShape = echarts.graphic.clipRectByRect({
+              x: start[0],
+              y: start[1] - height / 2,
+              width: Math.max(end[0] - start[0], 2),
+              height
+            }, {
+              x: params.coordSys.x,
+              y: params.coordSys.y,
+              width: params.coordSys.width,
+              height: params.coordSys.height
+            });
+            return rectShape ? { type: 'rect', shape: rectShape, style: api.style() } : undefined;
+          },
+          encode: { x: [1, 2], y: 0 },
+          data: idleBackgroundData,
+          z: 1
+        },
+        // 项目条层
+        {
+          type: 'custom',
+          name: '项目进度',
           renderItem: (params: any, api: any) => {
             const categoryIndex = api.value(0);
             const start = api.coord([api.value(1), categoryIndex]);
@@ -1108,6 +1341,7 @@ export class Dashboard implements OnInit, OnDestroy {
           data,
           itemStyle: { borderRadius: 4 },
           emphasis: { focus: 'self' },
+          z: 2,
           markLine: {
             silent: true,
             symbol: 'none',

+ 124 - 45
src/app/shared/components/upload-success-modal/upload-success-modal.component.scss

@@ -669,40 +669,84 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
     
     // 颜色描述区域
     .color-description {
-      margin-top: 20px;
-      padding-top: 20px;
-      border-top: 1px solid #e5e5ea;
+      margin-top: 24px;
+      padding: 24px;
+      background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+      border: 1px solid #e5e5ea;
+      border-radius: 16px;
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
       
       .description-header {
-        margin-bottom: 12px;
+        margin-bottom: 16px;
         
         h6 {
-          margin: 0;
-          font-size: 16px;
-          font-weight: 600;
+          margin: 0 0 4px 0;
+          font-size: 18px;
+          font-weight: 700;
           color: #1d1d1f;
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          
+          &::before {
+            content: '🎨';
+            font-size: 20px;
+          }
+        }
+        
+        p {
+          margin: 0;
+          font-size: 14px;
+          color: #8e8e93;
+          font-weight: 400;
         }
       }
       
       .description-content {
-        background: #f8f9fa;
-        border: 1px solid #e5e5ea;
-        border-radius: 8px;
-        padding: 16px;
-        margin-bottom: 12px;
-        font-size: 14px;
-        line-height: 1.6;
-        color: #1d1d1f;
+        background: #ffffff;
+        border: 2px solid #f0f0f0;
+        border-radius: 12px;
+        padding: 20px;
+        margin-bottom: 16px;
+        font-size: 15px;
+        line-height: 1.7;
+        color: #333333;
         white-space: pre-line;
         word-break: break-word;
-        max-height: 200px;
+        max-height: 180px;
         overflow-y: auto;
+        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+        transition: all 0.2s ease;
+        
+        &:hover {
+          border-color: #007aff;
+          box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1);
+        }
+        
+        &::-webkit-scrollbar {
+          width: 4px;
+        }
+        
+        &::-webkit-scrollbar-track {
+          background: transparent;
+        }
+        
+        &::-webkit-scrollbar-thumb {
+          background: rgba(0, 122, 255, 0.3);
+          border-radius: 2px;
+          
+          &:hover {
+            background: rgba(0, 122, 255, 0.5);
+          }
+        }
         
         &.empty {
           color: #8e8e93;
           font-style: italic;
           text-align: center;
-          padding: 24px 16px;
+          padding: 32px 20px;
+          background: #fafafa;
+          border-style: dashed;
         }
       }
       
@@ -710,7 +754,7 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
         display: flex;
         align-items: center;
         justify-content: space-between;
-        gap: 12px;
+        gap: 16px;
         
         @media (max-width: $mobile-breakpoint) {
           flex-direction: column;
@@ -721,30 +765,38 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
           display: inline-flex;
           align-items: center;
           justify-content: center;
-          gap: 6px;
-          padding: 8px 16px;
-          background: #ffffff;
-          color: #007aff;
-          border: 1px solid #007aff;
-          border-radius: 6px;
-          font-size: 13px;
-          font-weight: 500;
+          gap: 8px;
+          padding: 12px 20px;
+          background: linear-gradient(135deg, #007aff 0%, #0056cc 100%);
+          color: #ffffff;
+          border: none;
+          border-radius: 10px;
+          font-size: 14px;
+          font-weight: 600;
           cursor: pointer;
-          transition: all 0.2s ease;
-          min-width: 80px;
+          transition: all 0.3s ease;
+          min-width: 100px;
+          box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
           
           &:hover {
-            background: #f0f8ff;
+            transform: translateY(-2px);
+            box-shadow: 0 6px 20px rgba(0, 122, 255, 0.4);
+            background: linear-gradient(135deg, #0056cc 0%, #003d99 100%);
           }
           
           &:active {
-            transform: scale(0.98);
+            transform: translateY(0);
+            box-shadow: 0 2px 8px rgba(0, 122, 255, 0.3);
           }
           
           &.copied {
-            background: #34c759;
-            color: #ffffff;
-            border-color: #34c759;
+            background: linear-gradient(135deg, #34c759 0%, #28a745 100%);
+            box-shadow: 0 4px 12px rgba(52, 199, 89, 0.3);
+            
+            &:hover {
+              background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
+              box-shadow: 0 6px 20px rgba(52, 199, 89, 0.4);
+            }
           }
           
           svg {
@@ -1113,39 +1165,66 @@ $animation-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
         }
         
         .color-description {
+          background: linear-gradient(135deg, #2c2c2e 0%, #1c1c1e 100%);
+          border-color: #48484a;
+          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+          
           .description-header {
             h6 {
               color: #f2f2f7;
             }
+            
+            p {
+              color: #8e8e93;
+            }
           }
           
           .description-content {
-            background: #2c2c2e;
+            background: #1c1c1e;
             border-color: #48484a;
             color: #f2f2f7;
+            
+            &:hover {
+              border-color: #0a84ff;
+              box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.2);
+            }
+            
+            &::-webkit-scrollbar-thumb {
+              background: rgba(10, 132, 255, 0.4);
+              
+              &:hover {
+                background: rgba(10, 132, 255, 0.6);
+              }
+            }
+            
+            &.empty {
+              background: #2c2c2e;
+              color: #8e8e93;
+              border-color: #48484a;
+            }
           }
           
           .description-actions {
             .copy-btn {
-              background: #2c2c2e;
-              color: #f2f2f7;
-              border-color: #48484a;
+              background: linear-gradient(135deg, #0a84ff 0%, #0056cc 100%);
+              box-shadow: 0 4px 12px rgba(10, 132, 255, 0.3);
               
               &:hover {
-                background: #3a3a3c;
+                background: linear-gradient(135deg, #0056cc 0%, #003d99 100%);
+                box-shadow: 0 6px 20px rgba(10, 132, 255, 0.4);
               }
               
               &.copied {
-                background: #30d158;
-                color: #ffffff;
-                border-color: #30d158;
+                background: linear-gradient(135deg, #30d158 0%, #28a745 100%);
+                box-shadow: 0 4px 12px rgba(48, 209, 88, 0.3);
+                
+                &:hover {
+                  background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
+                  box-shadow: 0 6px 20px rgba(48, 209, 88, 0.4);
+                }
               }
             }
           }
-          
-          .description-tip {
-            color: #8e8e93;
-          }
         }
       }
     }

+ 230 - 2
src/app/shared/components/upload-success-modal/upload-success-modal.component.ts

@@ -153,6 +153,21 @@ export class UploadSuccessModalComponent implements OnInit, OnDestroy {
 
   onViewReportClick() {
     if (this.analysisResult) {
+      // 创建一个新的窗口来显示完整报告
+      const reportWindow = window.open('', '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes');
+      
+      if (reportWindow) {
+        // 生成完整的HTML报告内容
+        const reportHtml = this.generateFullReport();
+        reportWindow.document.write(reportHtml);
+        reportWindow.document.close();
+        reportWindow.focus();
+      } else {
+        // 如果弹窗被阻止,则下载报告文件
+        this.downloadReport();
+      }
+      
+      // 触发事件给父组件
       this.viewReport.emit(this.analysisResult);
     }
   }
@@ -462,13 +477,226 @@ export class UploadSuccessModalComponent implements OnInit, OnDestroy {
     document.body.removeChild(textArea);
   }
 
-  private checkScreenSize() {
+  // 生成完整的HTML报告
+  private generateFullReport(): string {
+    if (!this.analysisResult) return '';
+    
+    const colors = this.analysisResult.colors;
+    const colorDescription = this.generateColorDescription();
+    
+    return `
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>图片颜色分析完整报告</title>
+  <style>
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      line-height: 1.6;
+      color: #333;
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 40px 20px;
+      background: #f8f9fa;
+    }
+    .report-container {
+      background: white;
+      border-radius: 16px;
+      padding: 40px;
+      box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
+    }
+    h1 {
+      color: #1d1d1f;
+      font-size: 32px;
+      font-weight: 700;
+      margin-bottom: 8px;
+      text-align: center;
+    }
+    .subtitle {
+      color: #666;
+      font-size: 16px;
+      text-align: center;
+      margin-bottom: 40px;
+    }
+    .section {
+      margin-bottom: 40px;
+    }
+    .section-title {
+      color: #1d1d1f;
+      font-size: 24px;
+      font-weight: 600;
+      margin-bottom: 20px;
+      border-bottom: 2px solid #007AFF;
+      padding-bottom: 8px;
+    }
+    .color-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+      gap: 20px;
+      margin-bottom: 30px;
+    }
+    .color-item {
+      background: #f9f9f9;
+      border-radius: 12px;
+      padding: 20px;
+      text-align: center;
+      border: 1px solid #e5e5ea;
+      transition: transform 0.2s ease;
+    }
+    .color-item:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
+    }
+    .color-swatch {
+      width: 80px;
+      height: 80px;
+      border-radius: 50%;
+      margin: 0 auto 16px;
+      border: 3px solid white;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+    .color-name {
+      font-weight: 600;
+      font-size: 16px;
+      color: #1d1d1f;
+      margin-bottom: 4px;
+    }
+    .color-hex {
+      font-family: 'Monaco', 'Menlo', monospace;
+      font-size: 14px;
+      color: #666;
+      margin-bottom: 4px;
+    }
+    .color-percentage {
+      font-size: 18px;
+      font-weight: 700;
+      color: #007AFF;
+    }
+    .description-section {
+      background: #f9f9f9;
+      border-radius: 12px;
+      padding: 24px;
+      border-left: 4px solid #007AFF;
+    }
+    .description-text {
+      font-size: 16px;
+      line-height: 1.8;
+      color: #333;
+      white-space: pre-wrap;
+    }
+    .stats-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+      gap: 16px;
+      margin-top: 30px;
+    }
+    .stat-item {
+      text-align: center;
+      padding: 16px;
+      background: #f0f8ff;
+      border-radius: 8px;
+    }
+    .stat-value {
+      font-size: 24px;
+      font-weight: 700;
+      color: #007AFF;
+    }
+    .stat-label {
+      font-size: 14px;
+      color: #666;
+      margin-top: 4px;
+    }
+    .footer {
+      text-align: center;
+      margin-top: 40px;
+      padding-top: 20px;
+      border-top: 1px solid #e5e5ea;
+      color: #666;
+      font-size: 14px;
+    }
+    @media print {
+      body { background: white; }
+      .report-container { box-shadow: none; }
+    }
+  </style>
+</head>
+<body>
+  <div class="report-container">
+    <h1>图片颜色分析报告</h1>
+    <p class="subtitle">基于AI智能分析生成的详细颜色报告</p>
+    
+    <div class="section">
+      <h2 class="section-title">颜色分析结果</h2>
+      <div class="color-grid">
+        ${colors.map(color => `
+          <div class="color-item">
+            <div class="color-swatch" style="background-color: ${color.hex}"></div>
+            <div class="color-name">${this.getColorName(color.hex)}</div>
+            <div class="color-hex">${color.hex}</div>
+            <div class="color-percentage">${color.percentage}%</div>
+          </div>
+        `).join('')}
+      </div>
+      
+      <div class="stats-grid">
+        <div class="stat-item">
+          <div class="stat-value">${colors.length}</div>
+          <div class="stat-label">主要颜色数量</div>
+        </div>
+        <div class="stat-item">
+          <div class="stat-value">${colors[0]?.percentage || 0}%</div>
+          <div class="stat-label">主色占比</div>
+        </div>
+        <div class="stat-item">
+          <div class="stat-value">${colors.reduce((sum, c) => sum + c.percentage, 0).toFixed(1)}%</div>
+          <div class="stat-label">总覆盖率</div>
+        </div>
+      </div>
+    </div>
+    
+    <div class="section">
+      <h2 class="section-title">颜色描述文字</h2>
+      <div class="description-section">
+        <div class="description-text">${colorDescription}</div>
+      </div>
+    </div>
+    
+    <div class="footer">
+      <p>报告生成时间:${new Date().toLocaleString('zh-CN')}</p>
+      <p>由颜色分析系统自动生成</p>
+    </div>
+  </div>
+</body>
+</html>`;
+  }
+
+  // 下载报告文件
+  private downloadReport(): void {
+    if (!this.analysisResult) return;
+    
+    const reportHtml = this.generateFullReport();
+    const blob = new Blob([reportHtml], { type: 'text/html;charset=utf-8' });
+    const url = URL.createObjectURL(blob);
+    
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = `颜色分析报告_${new Date().toISOString().slice(0, 10)}.html`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    
+    URL.revokeObjectURL(url);
+  }
+
+  private checkScreenSize(): void {
     const width = window.innerWidth;
     this.isMobile = width < 768;
     this.isTablet = width >= 768 && width < 1024;
   }
 
-  private setupResizeListener() {
+  private setupResizeListener(): void {
     // 可以添加更复杂的响应式逻辑
   }
 }