Bladeren bron

feat:new workload-gantt

0235711 17 uur geleden
bovenliggende
commit
505bc0acdc

+ 121 - 118
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.ts

@@ -94,38 +94,33 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
     const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
     const todayTs = today.getTime();
 
-    // Time range
-    let xMin: number;
-    let xMax: number;
-    let xSplitNumber: number;
-    let xLabelFormatter: (value: number) => string;
-
+    // Time range & Category Data
+    let xCategories: string[] = [];
+    
     if (this.workloadGanttScale === 'week') {
       // Week view: next 7 days
-      xMin = todayTs;
-      xMax = todayTs + 7 * DAY;
-      xSplitNumber = 7;
-      xLabelFormatter = (val: any) => {
-        const date = new Date(val);
+      const days = 7;
+      for (let i = 0; i < days; i++) {
+        const d = new Date(todayTs + i * DAY);
         const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
-        return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
-      };
+        // 格式化为 "11/27\n周三" 或简单日期
+        xCategories.push(`${d.getMonth() + 1}/${d.getDate()}\n${weekDays[d.getDay()]}`);
+      }
     } else {
       // Month view: next 30 days
-      xMin = todayTs;
-      xMax = todayTs + 30 * DAY;
-      xSplitNumber = 30;
-      xLabelFormatter = (val: any) => {
-        const date = new Date(val);
-        return `${date.getMonth() + 1}/${date.getDate()}`;
-      };
+      const days = 30;
+      for (let i = 0; i < days; i++) {
+        const d = new Date(todayTs + i * DAY);
+        xCategories.push(`${d.getMonth() + 1}/${d.getDate()}`);
+      }
     }
 
     // Get all real designers
     let designers: string[] = [];
     
     if (this.realDesigners && this.realDesigners.length > 0) {
-      designers = this.realDesigners.map(d => d.name);
+      // 使用 Set 去重,防止同名设计师导致行重复
+      designers = Array.from(new Set(this.realDesigners.map(d => d.name)));
     } else {
       // Fallback: extract from filtered projects
       const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
@@ -240,7 +235,6 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
 
         let status: 'idle' | 'busy' | 'overload' | 'leave' = 'idle';
         let color = 'rgba(243, 244, 246, 0.4)'; // Idle - Very faint gray
-        let borderColor = 'transparent';
         
         const projectCount = dayProjects.length;
         
@@ -257,8 +251,7 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
 
         workloadByDesigner[designerName].push({
           name: `${designerName}-${i}`,
-          // value: [index, start, end, name, status, count, projects]
-          value: [yIndex, dayStart, dayEnd, designerName, status, projectCount, dayProjects],
+          value: [yIndex, i, i + 1, designerName, status, projectCount, dayProjects, dayStart],
           itemStyle: { 
             color: color,
             borderRadius: 4
@@ -275,112 +268,80 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
       tooltip: {
         trigger: 'item',
         padding: 0,
-        backgroundColor: 'rgba(255, 255, 255, 0.95)',
-        borderColor: '#e5e7eb',
+        backgroundColor: '#fff',
+        borderColor: '#e2e8f0',
         borderWidth: 1,
-        textStyle: { color: '#374151' },
+        textStyle: { color: '#334155' },
         extraCssText: 'box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-radius: 8px;',
         formatter: (params: any) => {
           const [yIndex, start, end, name, status, projectCount, projects = []] = params.value;
-          const startDate = new Date(start);
-          const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+          const date = new Date(start);
+          const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
           
-          // Status Header
-          let headerColor = '#f3f4f6';
-          let headerTextColor = '#374151';
-          let statusLabel = '空闲';
+          // 精简的 Tooltip
+          let listHtml = '';
           
-          if (status === 'overload') {
-            headerColor = '#fee2e2';
-            headerTextColor = '#991b1b';
-            statusLabel = '超负荷';
-          } else if (status === 'busy') {
-            headerColor = '#dbeafe';
-            headerTextColor = '#1e40af';
-            statusLabel = '忙碌';
-          }
-
-          // Project List
-          let projectListHtml = '';
           if (projects && projects.length > 0) {
-            const listItems = projects.slice(0, 6).map((p: any, idx: number) => {
-              const isUrgent = p.status === 'urgent' || p.status === 'overdue';
+            listHtml = projects.slice(0, 6).map((p: any) => {
               const stage = p.currentStage || '进行中';
               return `
-                <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; font-size: 12px; border-bottom: 1px dashed #f3f4f6;">
-                  <div style="display: flex; align-items: center; gap: 6px; max-width: 70%;">
-                    <span style="color: #9ca3af; font-size: 10px; width: 15px;">${idx + 1}.</span>
-                    <span style="color: #374151; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">${p.name}</span>
-                  </div>
-                  <div style="display: flex; align-items: center; gap: 4px;">
-                    <span style="background: #f3f4f6; color: #6b7280; padding: 1px 4px; border-radius: 3px; font-size: 10px;">${stage}</span>
-                    ${isUrgent ? '<span style="background: #fee2e2; color: #ef4444; width: 6px; height: 6px; border-radius: 50%; display: inline-block;"></span>' : ''}
-                  </div>
-                </div>
-              `;
+               <div style="display:flex;justify-content:space-between;font-size:12px;margin-top:4px;color:#64748b;">
+                 <span style="max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${p.name}</span>
+                 <span style="font-size:10px;background:#f1f5f9;padding:1px 4px;border-radius:2px;flex-shrink:0;">${stage}</span>
+               </div>`;
             }).join('');
             
-            projectListHtml = `
-              <div style="padding: 8px 12px;">
-                <div style="font-size: 11px; color: #9ca3af; margin-bottom: 4px; display: flex; justify-content: space-between;">
-                  <span>项目列表</span>
-                  <span>阶段</span>
-                </div>
-                ${listItems}
-                ${projects.length > 6 ? `<div style="text-align: center; font-size: 11px; color: #9ca3af; margin-top: 4px;">...共 ${projects.length} 个项目</div>` : ''}
-              </div>
-            `;
-          } else {
-            projectListHtml = `<div style="padding: 12px; text-align: center; color: #9ca3af; font-size: 12px;">无项目安排</div>`;
+            if (projects.length > 6) {
+              listHtml += `<div style="font-size:11px;color:#94a3b8;text-align:center;margin-top:4px;">...共 ${projects.length} 个项目</div>`;
+            }
+          }
+          
+          let statusColor = '#94a3b8'; // idle
+          let statusText = '空闲';
+          
+          if (status === 'overload') {
+            statusColor = '#ef4444';
+            statusText = '超负荷';
+          } else if (status === 'busy') {
+            statusColor = '#3b82f6';
+            statusText = '忙碌';
           }
           
           return `
-            <div style="width: 260px; overflow: hidden; border-radius: 8px;">
-              <div style="background: ${headerColor}; padding: 8px 12px; border-bottom: 1px solid rgba(0,0,0,0.05);">
-                <div style="display: flex; justify-content: space-between; align-items: center;">
-                  <span style="font-weight: 600; color: ${headerTextColor}; font-size: 14px;">${name}</span>
-                  <span style="font-size: 12px; color: ${headerTextColor}; opacity: 0.8;">${statusLabel} (${projectCount})</span>
-                </div>
-                <div style="font-size: 11px; color: ${headerTextColor}; opacity: 0.7; margin-top: 2px;">
-                  ${startDate.getMonth() + 1}月${startDate.getDate()}日 ${weekDays[startDate.getDay()]}
-                </div>
-              </div>
-              ${projectListHtml}
-              <div style="padding: 6px 12px; background: #f9fafb; border-top: 1px solid #f3f4f6; text-align: center;">
-                <span style="font-size: 10px; color: #9ca3af;">点击查看详情</span>
+            <div style="width:200px; padding:10px;">
+              <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f1f5f9; padding-bottom:6px; margin-bottom:6px;">
+                <span style="font-weight:600;">${name}</span>
+                <span style="font-size:12px; color:${statusColor}; font-weight:500;">${statusText} (${projectCount})</span>
               </div>
+              ${projectCount > 0 ? listHtml : '<div style="font-size:12px;color:#cbd5e1;text-align:center;">无安排</div>'}
             </div>
           `;
         }
       },
       grid: {
         left: 100,
-        right: 30,
+        right: 40, // 增加右侧留白,防止截断
         top: 70,
         bottom: 30,
         containLabel: true
       },
       xAxis: {
-        type: 'time',
-        min: xMin,
-        max: xMax,
+        type: 'category',
+        data: xCategories,
         position: 'top',
-        boundaryGap: false,
+        boundaryGap: true,
         axisLine: { show: false },
         axisTick: { show: false },
         axisLabel: {
-          color: '#6b7280',
-          formatter: xLabelFormatter,
+          color: '#9ca3af',
           interval: 0,
           margin: 12,
-          fontSize: 11
+          fontSize: 11,
+          align: 'center'
         },
         splitLine: { 
-          show: true,
-          lineStyle: { color: '#f3f4f6', type: 'dashed' }
-        },
-        splitNumber: xSplitNumber,
-        minInterval: DAY
+          show: false
+        }
       },
       yAxis: {
         type: 'category',
@@ -389,13 +350,11 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
         triggerEvent: true,
         axisLabel: { 
           color: '#374151', 
-          margin: 8,
+          margin: 12,
           fontSize: 13,
           fontWeight: 500,
           formatter: (value: string) => {
-            const totalProjects = designerTodayLoad[value] || 0;
-            const icon = totalProjects >= 3 ? '🔥' : totalProjects >= 2 ? '⚡' : totalProjects >= 1 ? '✓' : '○';
-            return `${icon} ${value} (${totalProjects})`;
+            return value;
           }
         },
         axisTick: { show: false },
@@ -406,19 +365,43 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
           type: 'custom',
           name: '工作负载',
           renderItem: (params: any, api: any) => {
-            const categoryIndex = api.value(0);
-            // Calculate coordinates
-            const start = api.coord([api.value(1), categoryIndex]);
-            const end = api.coord([api.value(2), categoryIndex]);
+            const categoryIndex = api.value(0); // Y轴索引
+            const xIndex = api.value(1); // X轴索引 (0, 1, 2...)
             
-            // Adjust height and position for a "pill" look
-            const height = api.size([0, 1])[1] * 0.7; // 70% of row height
+            // 1. 获取当前单元格中心点坐标
+            const point = api.coord([xIndex, categoryIndex]);
             
+            // 2. 计算列宽 (通过计算下一个点的距离,或者直接使用 size)
+            // api.size([1, 0]) 在 category 轴下可能不准,最稳妥是计算相邻点距离
+            // 模拟下一个点来计算步长
+            const nextPoint = api.coord([xIndex + 1, categoryIndex]);
+            const columnWidth = nextPoint[0] - point[0];
+            
+            // 3. 计算行高
+            const rowHeight = api.size([0, 1])[1];
+            
+            // 4. 确定矩形尺寸
+            const width = columnWidth * 0.85; // 宽度占 85%,留间隙
+            const height = rowHeight * 0.7;   // 高度占 70%
+            
+            // 5. 计算矩形左上角坐标 (基于中心点偏移)
+            const rectX = point[0] - width / 2;
+            const rectY = point[1] - height / 2;
+
+            // 获取项目数量和状态
+            const count = api.value(5);
+            const status = api.value(4); // idle, busy, overload
+
+            // 确定文字颜色
+            let textColor = '#9ca3af'; // 默认灰色 (idle)
+            if (status === 'busy') textColor = '#ffffff';
+            if (status === 'overload') textColor = '#ffffff';
+
             const rectShape = echarts.graphic.clipRectByRect({
-              x: start[0],
-              y: start[1] - height / 2,
-              width: Math.max(end[0] - start[0], 2), // Minimum width 2px
-              height
+              x: rectX,
+              y: rectY,
+              width: width,
+              height: height
             }, {
               x: params.coordSys.x,
               y: params.coordSys.y,
@@ -426,14 +409,34 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
               height: params.coordSys.height
             });
 
-            return rectShape ? { 
-              type: 'rect', 
-              shape: {
-                ...rectShape,
-                r: [4, 4, 4, 4] // Rounded corners
-              }, 
-              style: api.style() 
-            } : undefined;
+            if (!rectShape) return undefined;
+
+            return {
+              type: 'group',
+              children: [
+                {
+                  type: 'rect',
+                  shape: {
+                    ...rectShape,
+                    r: [4, 4, 4, 4] // 圆角
+                  },
+                  style: api.style()
+                },
+                {
+                  type: 'text',
+                  style: {
+                    text: count > 0 ? count : '',
+                    x: rectX + width / 2,
+                    y: rectY + height / 2,
+                    textAlign: 'center',
+                    textVerticalAlign: 'middle',
+                    fill: textColor,
+                    fontSize: 12,
+                    fontWeight: 'bold'
+                  }
+                }
+              ]
+            };
           },
           encode: { x: [1, 2], y: 0 },
           data,

+ 462 - 0
src/test/workload-gantt-preview.html

@@ -0,0 +1,462 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>工作量负载概览 - 方案预览</title>
+  <!-- 替换为国内稳定的 BootCDN 源 -->
+  <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
+  <style>
+    :root {
+      --primary-color: #3b82f6;
+      --bg-color: #f8fafc;
+      --card-bg: #ffffff;
+      --text-main: #1e293b;
+      --text-sub: #64748b;
+    }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+      background-color: var(--bg-color);
+      margin: 0;
+      padding: 40px;
+      color: var(--text-main);
+    }
+
+    .container {
+      max-width: 1000px;
+      margin: 0 auto;
+    }
+
+    .card {
+      background: var(--card-bg);
+      border-radius: 16px;
+      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+      padding: 24px;
+      margin-bottom: 30px;
+    }
+
+    .header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 24px;
+    }
+
+    .title-group h3 {
+      margin: 0 0 4px 0;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    .title-group p {
+      margin: 0;
+      font-size: 13px;
+      color: var(--text-sub);
+    }
+
+    .controls {
+      display: flex;
+      gap: 16px;
+      align-items: center;
+    }
+
+    .switch-group {
+      background: #f1f5f9;
+      padding: 4px;
+      border-radius: 8px;
+      display: flex;
+    }
+
+    .switch-btn {
+      border: none;
+      background: transparent;
+      padding: 6px 12px;
+      border-radius: 6px;
+      font-size: 13px;
+      cursor: pointer;
+      color: var(--text-sub);
+      transition: all 0.2s;
+    }
+
+    .switch-btn.active {
+      background: white;
+      color: var(--primary-color);
+      box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+      font-weight: 500;
+    }
+
+    .legend {
+      display: flex;
+      gap: 12px;
+      font-size: 12px;
+      color: var(--text-sub);
+    }
+
+    .legend-item {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    .dot {
+      width: 8px;
+      height: 8px;
+      border-radius: 2px;
+    }
+
+    .chart-container {
+      width: 100%;
+      height: 400px;
+    }
+
+    /* 方案说明样式 */
+    .scheme-tag {
+      display: inline-block;
+      padding: 4px 8px;
+      border-radius: 4px;
+      font-size: 12px;
+      font-weight: 600;
+      margin-bottom: 8px;
+    }
+    .tag-1 { background: #e0f2fe; color: #0369a1; }
+    .tag-2 { background: #f3e8ff; color: #7e22ce; }
+
+  </style>
+</head>
+<body>
+
+<div class="container">
+  
+  <!-- 方案一:极简热力方块 -->
+  <div class="scheme-tag tag-1">方案一:极简热力方块 (推荐)</div>
+  <div class="card">
+    <div class="header">
+      <div class="title-group">
+        <h3>工作量负载概览</h3>
+        <p>极简风格,通过色块直观展示忙闲分布</p>
+      </div>
+      <div class="controls">
+        <div class="legend">
+          <span class="legend-item"><span class="dot" style="background:#f1f5f9"></span>空闲</span>
+          <span class="legend-item"><span class="dot" style="background:#eff6ff; border:1px solid #bfdbfe"></span>适中</span>
+          <span class="legend-item"><span class="dot" style="background:#fee2e2; border:1px solid #fecaca"></span>超负荷</span>
+        </div>
+        <div class="switch-group">
+          <button class="switch-btn active" onclick="updateChart1('week')">周视图</button>
+          <button class="switch-btn" onclick="updateChart1('month')">月视图</button>
+        </div>
+      </div>
+    </div>
+    <div id="chart1" class="chart-container"></div>
+  </div>
+
+  <!-- 方案二:数字化日历 -->
+  <div class="scheme-tag tag-2">方案二:数字化日历</div>
+  <div class="card">
+    <div class="header">
+      <div class="title-group">
+        <h3>工作量负载概览</h3>
+        <p>数据风格,直接显示每日项目数量</p>
+      </div>
+      <div class="controls">
+        <div class="legend">
+          <span class="legend-item">数字代表项目数</span>
+        </div>
+      </div>
+    </div>
+    <div id="chart2" class="chart-container"></div>
+  </div>
+
+</div>
+
+<script>
+  // ================== 模拟数据生成 ==================
+  const designers = ['王刚', '李婷', '张伟', '陈思', '赵敏', '刘强'];
+  const today = new Date();
+  
+  // 生成未来30天的数据
+  function generateData(days) {
+    const data = [];
+    designers.forEach((designer, yIndex) => {
+      for (let i = 0; i < days; i++) {
+        const date = new Date(today);
+        date.setDate(today.getDate() + i);
+        const ts = date.getTime();
+        
+        // 随机生成负载 (0-4)
+        // 让某些人更忙,模拟真实情况
+        const baseLoad = (yIndex % 2 === 0) ? 2 : 0; 
+        const randomLoad = Math.floor(Math.random() * 3);
+        const count = Math.max(0, Math.min(4, baseLoad + randomLoad - 1));
+        
+        let status = 'idle';
+        if (count === 0) status = 'idle';
+        else if (count >= 3) status = 'overload';
+        else status = 'busy';
+
+        // 模拟项目详情
+        const projects = [];
+        for(let p=0; p<count; p++) {
+          projects.push({
+            name: ['锦江上院', '天鹅湖', '龙湖天街', '万科城市', '保利国际'][Math.floor(Math.random()*5)] + '-' + (p+1) + '期',
+            stage: ['建模', '渲染', '后期', '修改'][Math.floor(Math.random()*4)]
+          });
+        }
+
+        data.push({
+          value: [yIndex, ts, ts + 24*3600*1000, designer, status, count, projects],
+          itemStyle: getItemStyle(status)
+        });
+      }
+    });
+    return data;
+  }
+
+  function getItemStyle(status) {
+    if (status === 'overload') return { color: '#fee2e2', borderColor: '#fca5a5', borderWidth: 1 };
+    if (status === 'busy') return { color: '#eff6ff', borderColor: '#bfdbfe', borderWidth: 1 };
+    return { color: '#f8fafc', borderColor: '#f1f5f9', borderWidth: 1 }; // idle
+  }
+
+  // 声明图表实例变量
+  let chart1, chart2;
+
+  // ================== 方案一:极简热力方块 ==================
+  
+  function renderChart1(scale) {
+    // 确保 chart1 已初始化
+    if (!chart1) {
+        const dom = document.getElementById('chart1');
+        if (dom) {
+            chart1 = echarts.init(dom);
+        } else {
+            console.error('Chart1 DOM not found');
+            return;
+        }
+    }
+
+    const days = scale === 'week' ? 7 : 30;
+    const data = generateData(days);
+    const xMin = today.getTime();
+    const xMax = xMin + days * 24 * 3600 * 1000;
+
+    const option = {
+      tooltip: {
+        padding: 0,
+        backgroundColor: '#fff',
+        borderColor: '#e2e8f0',
+        textStyle: { color: '#334155' },
+        formatter: function (params) {
+          const val = params.value;
+          const [yIndex, start, end, name, status, count, projects] = val;
+          const date = new Date(start);
+          const dateStr = `${date.getMonth()+1}/${date.getDate()}`;
+          
+          // 精简的 Tooltip
+          let listHtml = projects.map(p => 
+            `<div style="display:flex;justify-content:space-between;font-size:12px;margin-top:4px;color:#64748b;">
+               <span>${p.name}</span><span style="font-size:10px;background:#f1f5f9;padding:1px 4px;border-radius:2px;">${p.stage}</span>
+             </div>`
+          ).join('');
+          
+          let statusColor = count >= 3 ? '#ef4444' : (count > 0 ? '#3b82f6' : '#94a3b8');
+          
+          return `
+            <div style="width:200px; padding:10px;">
+              <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f1f5f9; padding-bottom:6px; margin-bottom:6px;">
+                <span style="font-weight:600;">${name}</span>
+                <span style="font-size:12px; color:${statusColor}; font-weight:500;">${count}个项目</span>
+              </div>
+              ${count > 0 ? listHtml : '<div style="font-size:12px;color:#cbd5e1;text-align:center;">无安排</div>'}
+            </div>
+          `;
+        }
+      },
+      grid: { top: 30, left: 80, right: 20, bottom: 20 },
+      xAxis: {
+        type: 'time',
+        min: xMin,
+        max: xMax,
+        position: 'top',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { show: false }, // 去掉竖线,更干净
+        axisLabel: {
+          formatter: (val) => {
+            const d = new Date(val);
+            return `${d.getMonth()+1}/${d.getDate()}`;
+          },
+          color: '#94a3b8',
+          fontSize: 11,
+          margin: 10
+        }
+      },
+      yAxis: {
+        type: 'category',
+        data: designers,
+        inverse: true,
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: {
+          color: '#475569',
+          fontSize: 13,
+          fontWeight: 500,
+          margin: 20
+        }
+      },
+      series: [{
+        type: 'custom',
+        renderItem: function (params, api) {
+          const y = api.value(0);
+          const start = api.coord([api.value(1), y]);
+          const end = api.coord([api.value(2), y]);
+          const height = api.size([0, 1])[1] * 0.7; // 高度占比
+          const width = (end[0] - start[0]) * 0.85; // 宽度占比,留出间隙
+
+          // 绘制圆角矩形
+          return {
+            type: 'rect',
+            shape: {
+              x: start[0] + (end[0] - start[0] - width) / 2, // 居中
+              y: start[1] - height / 2,
+              width: width,
+              height: height,
+              r: 4 // 圆角
+            },
+            style: api.style()
+          };
+        },
+        data: data
+      }]
+    };
+    chart1.setOption(option);
+  }
+
+  function updateChart1(scale) {
+    // 更新按钮状态
+    const btns = document.querySelectorAll('.switch-btn');
+    btns.forEach(b => b.classList.remove('active'));
+    if(event && event.target) {
+        event.target.classList.add('active');
+    }
+    renderChart1(scale);
+  }
+
+  // ================== 方案二:数字化日历 ==================
+  
+  function renderChart2() {
+    // 确保 chart2 已初始化
+    if (!chart2) {
+        const dom = document.getElementById('chart2');
+        if (dom) {
+            chart2 = echarts.init(dom);
+        } else {
+            console.error('Chart2 DOM not found');
+            return;
+        }
+    }
+
+    const days = 14; // 展示两周
+    const data = generateData(days);
+    const xMin = today.getTime();
+    const xMax = xMin + days * 24 * 3600 * 1000;
+
+    const option = {
+      tooltip: { show: false }, // 直接显示数字,不需要Tooltip
+      grid: { top: 30, left: 80, right: 20, bottom: 20 },
+      xAxis: {
+        type: 'time',
+        min: xMin,
+        max: xMax,
+        position: 'top',
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { show: true, lineStyle: { color: '#f1f5f9' } },
+        axisLabel: {
+          formatter: (val) => {
+            const d = new Date(val);
+            return `${d.getMonth()+1}/${d.getDate()}`;
+          },
+          color: '#94a3b8'
+        }
+      },
+      yAxis: {
+        type: 'category',
+        data: designers,
+        inverse: true,
+        axisLine: { show: false },
+        axisTick: { show: false },
+        axisLabel: {
+          color: '#475569',
+          fontSize: 13,
+          margin: 20
+        }
+      },
+      series: [{
+        type: 'custom',
+        renderItem: function (params, api) {
+          const y = api.value(0);
+          const start = api.coord([api.value(1), y]);
+          const end = api.coord([api.value(2), y]);
+          const height = api.size([0, 1])[1];
+          const width = end[0] - start[0];
+          const count = api.value(5);
+
+          // 根据数量决定背景色深浅
+          let bgColor = '#fff';
+          let textColor = '#cbd5e1'; // 0 - 浅灰
+          if (count === 1) { bgColor = '#eff6ff'; textColor = '#3b82f6'; }
+          if (count === 2) { bgColor = '#dbeafe'; textColor = '#2563eb'; }
+          if (count >= 3) { bgColor = '#fee2e2'; textColor = '#dc2626'; }
+
+          return {
+            type: 'group',
+            children: [
+              {
+                type: 'rect',
+                shape: {
+                  x: start[0],
+                  y: start[1] - height / 2,
+                  width: width - 2, // 留微小缝隙
+                  height: height - 2
+                },
+                style: { fill: bgColor }
+              },
+              {
+                type: 'text',
+                style: {
+                  text: count > 0 ? count : '-',
+                  x: start[0] + width / 2,
+                  y: start[1],
+                  textAlign: 'center',
+                  textVerticalAlign: 'middle',
+                  fill: textColor,
+                  fontSize: 14,
+                  fontWeight: 'bold'
+                }
+              }
+            ]
+          };
+        },
+        data: data
+      }]
+    };
+    chart2.setOption(option);
+  }
+
+  // 页面加载完成后初始化图表
+  window.addEventListener('load', () => {
+      renderChart1('week');
+      renderChart2();
+  });
+
+  // 响应窗口大小
+  window.addEventListener('resize', () => {
+    if(chart1) chart1.resize();
+    if(chart2) chart2.resize();
+  });
+
+</script>
+</body>
+</html>