坐席业绩数据分析

豆包提示词:

使用papaparse.js,chart.js,tailwindcss和font-awesome,生成一个可以交互的简洁且可以运行的HTML代码,不要输出无关内容。

具体要求如下:

1、按坐席姓名输出业绩折线图。

2、系统导航区域:放置上传csv文件的按钮,需要正确解析日期、坐席姓名、一级机构至五级机构的机构名称、各业绩的中位值,(数据格式:日期,坐席姓名,一级机构,二级机构,三级机构,四级机构,五级机构,业务等级,在线时长,外呼时长,接通时长,外呼次数,接通次数,有效通次,接通率,违规次数,推荐次数),时长的单位均为分钟。

3、顶部区域:筛选X轴日期(日/周/月,使用按钮组件选择),Y轴业绩指标(通话时长/外呼次数/接通次数/..,使用按钮组件选择),可以根据业绩指标设置目标值,默认设置为对应业绩指标的中位值,允许修改。左侧区域选择坐席名称和机构名称,右侧区域显示图表。

4、坐席选择:勾选合法的坐席名称(坐席姓名的取值)

5、机构选择:先使用按钮组件的形式显示一级机构至五级机构,点击某级机构时,使用checkbox显示某级机构下唯一的合法的机构名称,默认选中五级机构并勾选中五级机构下的机构名称,被勾选的机构名称需要计算机构均值,目标值和机构均值都使用不同颜色虚线显示在折线图上。

6、坐席名称/每级机构的名称都可以全选/取消全选。

7、所有操作都会直接更新图表。

通过调整日/周/月的焦点、过滤掉undefined项、按所有坐席计算机构均值得到的最终代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>坐席业绩数据分析</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/papaparse.min.js"></script>
  <script>
    tailwind.config = {
      theme: {
        extend: {
          colors: {
            primary: '#3B82F6',
            secondary: '#10B981',
            accent: '#6366F1',
            neutral: '#6B7280',
            success: '#10B981',
            warning: '#F59E0B',
            danger: '#EF4444',
            info: '#06B6D4',
          },
          fontFamily: {
            sans: ['Inter', 'system-ui', 'sans-serif'],
          },
          boxShadow: {
            'card': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
            'card-hover': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
          }
        },
      }
    }
  </script>
  <style type="text/tailwindcss">
    @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .scrollbar-hide {
                -ms-overflow-style: none;
                scrollbar-width: none;
            }
            .scrollbar-hide::-webkit-scrollbar {
                display: none;
            }
            .chart-container {
                position: relative;
                height: 100%;
                width: 100%;
            }
            .btn-toggle.active {
                @apply bg-primary text-white;
            }
            .btn-toggle:not(.active) {
                @apply bg-gray-100 text-gray-700 hover:bg-gray-200;
            }
            .checkbox-container {
                max-height: 150px;
                overflow-y: auto;
                scrollbar-width: thin;
            }
            .checkbox-container::-webkit-scrollbar {
                width: 4px;
            }
            .checkbox-container::-webkit-scrollbar-track {
                background: #f1f1f1;
            }
            .checkbox-container::-webkit-scrollbar-thumb {
                background: #c1c1c1;
                border-radius: 4px;
            }
            .checkbox-container::-webkit-scrollbar-thumb:hover {
                background: #a1a1a1;
            }
            .loading-spinner {
                border-top-color: theme('colors.primary');
                animation: spinner 0.6s linear infinite;
            }
            @keyframes spinner {
                to {
                    transform: rotate(360deg);
                }
            }
        }
    </style>
</head>

<body class="bg-gray-50 font-sans text-gray-800 min-h-screen flex flex-col">
  <!-- 导航栏 -->
  <header class="bg-white shadow-sm sticky top-0 z-50">
    <div class="container mx-auto px-4 py-3 flex flex-col md:flex-row md:items-center justify-between">
      <div class="flex items-center mb-3 md:mb-0">
        <h1 class="text-xl md:text-2xl font-bold text-primary flex items-center">
          <i class="fa fa-bar-chart mr-2"></i>
          <span>坐席业绩数据分析</span>
        </h1>
      </div>
      <div class="flex items-center">
        <label for="file-upload"
          class="cursor-pointer bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-md transition-all duration-200 flex items-center">
          <i class="fa fa-upload mr-2"></i>
          <span>上传CSV文件</span>
        </label>
        <input id="file-upload" type="file" accept=".csv" class="hidden" />
        <span id="file-name" class="ml-3 text-sm text-gray-500"></span>
      </div>
    </div>
  </header>

  <!-- 主要内容区 -->
  <main class="flex-grow container mx-auto px-4 py-6">
    <!-- 顶部筛选区 -->
    <div class="bg-white rounded-lg shadow-card p-4 mb-6 transform transition-all duration-300 hover:shadow-card-hover">
      <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
        <!-- X轴日期筛选 -->
        <div class="space-y-2">
          <h3 class="font-semibold text-gray-700 flex items-center">
            <i class="fa fa-calendar-alt mr-2 text-primary"></i>
            <span>X轴日期筛选</span>
          </h3>
          <div class="flex flex-wrap gap-2">
            <button id="date-day"
              class="btn-toggle active px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              日
            </button>
            <button id="date-week"
              class="btn-toggle px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              周
            </button>
            <button id="date-month"
              class="btn-toggle px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              月
            </button>
          </div>
        </div>

        <!-- Y轴业绩指标筛选 -->
        <div class="space-y-2">
          <h3 class="font-semibold text-gray-700 flex items-center">
            <i class="fa fa-line-chart mr-2 text-primary"></i>
            <span>Y轴业绩指标</span>
          </h3>
          <div class="flex flex-wrap gap-2">
            <button id="metric-duration"
              class="btn-toggle active px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              通话时长
            </button>
            <button id="metric-call"
              class="btn-toggle px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              外呼次数
            </button>
            <button id="metric-connect"
              class="btn-toggle px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              接通次数
            </button>
            <button id="metric-effective"
              class="btn-toggle px-3 py-1 rounded-md text-sm font-medium transition-all duration-200">
              有效通次
            </button>
          </div>
        </div>

        <!-- 目标值设置 -->
        <div class="space-y-2">
          <h3 class="font-semibold text-gray-700 flex items-center">
            <i class="fa fa-bullseye mr-2 text-primary"></i>
            <span>目标值设置</span>
          </h3>
          <div class="flex items-center">
            <input type="number" id="target-value"
              class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all duration-200"
              placeholder="输入目标值">
            <button id="set-target"
              class="ml-2 bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md transition-all duration-200">
              设置
            </button>
          </div>
          <p id="current-median" class="text-sm text-gray-500 mt-1">当前中位值: <span class="font-medium">-</span></p>
        </div>
      </div>
    </div>

    <!-- 筛选和图表区域 -->
    <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
      <!-- 左侧筛选区 -->
      <div class="lg:col-span-4 space-y-6">
        <!-- 坐席选择 -->
        <div class="bg-white rounded-lg shadow-card p-4 transform transition-all duration-300 hover:shadow-card-hover">
          <div class="flex justify-between items-center mb-3">
            <h3 class="font-semibold text-gray-700 flex items-center">
              <i class="fa fa-users mr-2 text-primary"></i>
              <span>坐席选择</span>
            </h3>
            <div class="flex space-x-2">
              <button id="select-all-agents"
                class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200">
                全选
              </button>
              <button id="deselect-all-agents"
                class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200">
                取消全选
              </button>
            </div>
          </div>
          <div id="agent-container" class="checkbox-container p-2 border border-gray-200 rounded-md">
            <!-- 坐席选项将通过JS动态生成 -->
            <div class="flex items-center justify-center h-20 text-gray-400">
              <i class="fa fa-file-csv text-2xl mr-2"></i>
              <span>请先上传CSV文件</span>
            </div>
          </div>
        </div>

        <!-- 机构选择 -->
        <div class="bg-white rounded-lg shadow-card p-4 transform transition-all duration-300 hover:shadow-card-hover">
          <h3 class="font-semibold text-gray-700 flex items-center mb-3">
            <i class="fa fa-sitemap mr-2 text-primary"></i>
            <span>机构选择</span>
          </h3>

          <!-- 机构级别选择 -->
          <div class="flex flex-wrap gap-2 mb-3">
            <button data-level="1"
              class="org-level-btn px-3 py-1 rounded-md text-sm font-medium bg-gray-100 hover:bg-gray-200 transition-all duration-200">
              一级机构
            </button>
            <button data-level="2"
              class="org-level-btn px-3 py-1 rounded-md text-sm font-medium bg-gray-100 hover:bg-gray-200 transition-all duration-200">
              二级机构
            </button>
            <button data-level="3"
              class="org-level-btn px-3 py-1 rounded-md text-sm font-medium bg-gray-100 hover:bg-gray-200 transition-all duration-200">
              三级机构
            </button>
            <button data-level="4"
              class="org-level-btn px-3 py-1 rounded-md text-sm font-medium bg-gray-100 hover:bg-gray-200 transition-all duration-200">
              四级机构
            </button>
            <button data-level="5"
              class="org-level-btn active px-3 py-1 rounded-md text-sm font-medium bg-primary text-white transition-all duration-200">
              五级机构
            </button>
          </div>

          <!-- 机构列表 -->
          <div class="mb-3">
            <div id="current-org-level" class="text-sm text-gray-500 mb-2">当前显示: 五级机构</div>
            <div class="flex space-x-2 mb-2">
              <button id="select-all-orgs"
                class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200">
                全选
              </button>
              <button id="deselect-all-orgs"
                class="text-xs px-2 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200">
                取消全选
              </button>
            </div>
            <div id="org-container" class="checkbox-container p-2 border border-gray-200 rounded-md">
              <!-- 机构选项将通过JS动态生成 -->
              <div class="flex items-center justify-center h-20 text-gray-400">
                <i class="fa fa-file-csv text-2xl mr-2"></i>
                <span>请先上传CSV文件</span>
              </div>
            </div>
          </div>

          <!-- 机构均值显示 -->
          <div id="org-average-container" class="p-3 bg-gray-50 rounded-md border border-gray-200">
            <h4 class="font-medium text-sm mb-1">机构均值: <span id="current-org-average"
                class="text-primary font-semibold">-</span></h4>
            <div class="w-full bg-gray-200 rounded-full h-2">
              <div id="org-average-bar" class="bg-primary h-2 rounded-full" style="width: 0%"></div>
            </div>
          </div>
        </div>
      </div>

      <!-- 右侧图表区 -->
      <div
        class="lg:col-span-8 bg-white rounded-lg shadow-card p-4 transform transition-all duration-300 hover:shadow-card-hover">
        <div class="flex justify-between items-center mb-4">
          <h3 class="font-semibold text-gray-700 flex items-center">
            <i class="fa fa-chart-line mr-2 text-primary"></i>
            <span id="chart-title">坐席业绩趋势分析</span>
          </h3>
          <div class="flex space-x-2">
            <button id="download-png"
              class="text-sm px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200 flex items-center">
              <i class="fa fa-download mr-1"></i> PNG
            </button>
            <button id="download-svg"
              class="text-sm px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded transition-all duration-200 flex items-center">
              <i class="fa fa-download mr-1"></i> SVG
            </button>
          </div>
        </div>
        <!-- 修改图表容器 -->
        <div class="h-[400px] w-full chart-container overflow-hidden relative">
          <canvas id="performance-chart" class="absolute top-0 left-0 w-full h-full"></canvas>
        </div>
        <div id="chart-loading" class="hidden absolute inset-0 flex items-center justify-center bg-white/80">
          <div class="flex flex-col items-center">
            <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary"></div>
            <p class="mt-2 text-gray-500">正在加载图表...</p>
          </div>
        </div>
      </div>
    </div>
  </main>

  <!-- 页脚 -->
  <footer class="bg-white border-t border-gray-200 py-4 mt-8">
    <div class="container mx-auto px-4 text-center text-gray-500 text-sm">
      <p>© 2025 坐席业绩数据分析系统 | 设计与开发</p>
    </div>
  </footer>

  <!-- 通知组件 -->
  <div id="notification"
    class="fixed bottom-4 right-4 bg-white shadow-lg rounded-lg p-4 transform transition-all duration-300 translate-y-20 opacity-0 z-50 flex items-center max-w-xs">
    <div id="notification-icon" class="mr-3 text-primary">
      <i class="fa fa-info-circle"></i>
    </div>
    <div>
      <h4 id="notification-title" class="font-medium text-gray-800">通知标题</h4>
      <p id="notification-message" class="text-sm text-gray-600">通知内容将显示在这里...</p>
    </div>
    <button id="close-notification" class="ml-4 text-gray-400 hover:text-gray-600">
      <i class="fa fa-times"></i>
    </button>
  </div>

  <script>
    // 全局变量
    let csvData = null;
    let chart = null;
    let selectedDateType = 'day';
    let selectedMetric = 'duration';
    let targetValue = null;
    let medians = {};
    let currentOrgLevel = 5;
    let chartData = {};
    let isChartUpdating = false;
    let loadingStartTime = 0;
    const MIN_LOADING_TIME = 500; // 最小加载时间(毫秒)


    // DOM 元素
    const fileUpload = document.getElementById('file-upload');
    const fileName = document.getElementById('file-name');
    const agentContainer = document.getElementById('agent-container');
    const orgContainer = document.getElementById('org-container');
    const currentOrgLevelEl = document.getElementById('current-org-level');
    const currentMedianEl = document.getElementById('current-median').querySelector('span');
    const currentOrgAverageEl = document.getElementById('current-org-average');
    const orgAverageBar = document.getElementById('org-average-bar');
    const chartTitle = document.getElementById('chart-title');
    const chartLoading = document.getElementById('chart-loading');
    const notification = document.getElementById('notification');
    const notificationTitle = document.getElementById('notification-title');
    const notificationMessage = document.getElementById('notification-message');
    const notificationIcon = document.getElementById('notification-icon');
    const closeNotification = document.getElementById('close-notification');
    const targetValueInput = document.getElementById('target-value');
    const setTargetBtn = document.getElementById('set-target');
    const downloadPngBtn = document.getElementById('download-png');
    const downloadSvgBtn = document.getElementById('download-svg');

    // 日期类型按钮
    const dateDayBtn = document.getElementById('date-day');
    const dateWeekBtn = document.getElementById('date-week');
    const dateMonthBtn = document.getElementById('date-month');

    // 指标类型按钮
    const metricDurationBtn = document.getElementById('metric-duration');
    const metricCallBtn = document.getElementById('metric-call');
    const metricConnectBtn = document.getElementById('metric-connect');
    const metricEffectiveBtn = document.getElementById('metric-effective');

    // 机构级别按钮
    const orgLevelBtns = document.querySelectorAll('.org-level-btn');

    // 全选/取消全选按钮
    const selectAllAgentsBtn = document.getElementById('select-all-agents');
    const deselectAllAgentsBtn = document.getElementById('deselect-all-agents');
    const selectAllOrgsBtn = document.getElementById('select-all-orgs');
    const deselectAllOrgsBtn = document.getElementById('deselect-all-orgs');

    // 初始化图表
    function initChart() {
      const ctx = document.getElementById('performance-chart').getContext('2d');

      // 销毁已存在的图表
      if (chart) {
        chart.destroy();
      }

      // 创建新图表
      chart = new Chart(ctx, {
        type: 'line',
        data: {
          labels: [],
          datasets: []
        },
        options: {
          responsive: true,
          maintainAspectRatio: false,
          interaction: {
            mode: 'index',
            intersect: false,
          },
          plugins: {
            legend: {
              position: 'top',
              labels: {
                usePointStyle: true,
                boxWidth: 6
              }
            },
            tooltip: {
              backgroundColor: 'rgba(255, 255, 255, 0.9)',
              titleColor: '#333',
              bodyColor: '#666',
              borderColor: '#ddd',
              borderWidth: 1,
              padding: 12,
              boxPadding: 6,
              usePointStyle: true,
              callbacks: {
                label: function (context) {
                  let label = context.dataset.label || '';
                  if (label) {
                    label += ': ';
                  }
                  if (context.parsed.y !== null) {
                    const value = context.parsed.y;
                    label += selectedMetric === 'duration' ?
                      value.toFixed(1) + ' 分钟' :
                      value.toFixed(0);
                  }
                  return label;
                }
              }
            }
          },
          scales: {
            x: {
              grid: {
                display: false
              }
            },
            y: {
              beginAtZero: true,
              grid: {
                color: 'rgba(0, 0, 0, 0.05)'
              }
            }
          },
          animations: {
            tension: {
              duration: 1000,
              easing: 'linear'
            }
          }
        }
      });
    }

    // 解析CSV文件
    function parseCSV(file) {
      showLoading(true);

      Papa.parse(file, {
        header: true,
        dynamicTyping: true,
        complete: function (results) {
          csvData = results.data;
          fileName.textContent = file.name;

          // 处理数据
          processData();

          // 初始化图表
          initChart();

          // 更新图表
          updateChart();

          showLoading(false);
          showNotification('成功', 'CSV文件已成功导入', 'success');
        },
        error: function (error) {
          showLoading(false);
          showNotification('错误', 'CSV文件解析失败: ' + error.message, 'error');
        }
      });
    }

    // 处理数据
    function processData() {
      if (!csvData || csvData.length === 0) return;

      // 过滤掉包含undefined或无效值的行
      const validData = csvData.filter(row => {
        return (
          row['坐席姓名'] !== undefined &&
          row['一级机构'] !== undefined &&
          row['二级机构'] !== undefined &&
          row['三级机构'] !== undefined &&
          row['四级机构'] !== undefined &&
          row['五级机构'] !== undefined
        );
      });

      // 提取坐席名称,过滤掉空值和undefined
      const agents = [...new Set(validData.map(row => row['坐席姓名']))]
        .filter(agent => agent !== undefined && agent !== '')
        .sort();

      // 提取各级机构,过滤掉空值和undefined
      const organizations = {
        1: [...new Set(validData.map(row => row['一级机构']))].filter(org => org !== undefined && org !== '').sort(),
        2: [...new Set(validData.map(row => row['二级机构']))].filter(org => org !== undefined && org !== '').sort(),
        3: [...new Set(validData.map(row => row['三级机构']))].filter(org => org !== undefined && org !== '').sort(),
        4: [...new Set(validData.map(row => row['四级机构']))].filter(org => org !== undefined && org !== '').sort(),
        5: [...new Set(validData.map(row => row['五级机构']))].filter(org => org !== undefined && org !== '').sort()
      };

      // 计算各业绩的中位值
      calculateMedians();

      // 更新坐席选择
      updateAgentSelection(agents);

      // 更新机构选择
      updateOrgSelection(organizations[currentOrgLevel], currentOrgLevel);

      // 设置默认目标值
      targetValue = medians[selectedMetric];
      targetValueInput.value = targetValue;
      currentMedianEl.textContent = targetValue;
    }

    // 计算各业绩的中位值
    function calculateMedians() {
      if (!csvData || csvData.length === 0) return;

      // 提取需要计算中位值的字段
      const fields = ['在线时长', '外呼时长', '接通时长', '外呼次数', '接通次数', '有效通次', '接通率', '违规次数', '推荐次数'];

      fields.forEach(field => {
        // 过滤掉无效值并排序
        const values = csvData
          .map(row => row[field])
          .filter(value => typeof value === 'number' && !isNaN(value))
          .sort((a, b) => a - b);

        if (values.length > 0) {
          // 计算中位值
          const middle = Math.floor(values.length / 2);
          medians[field] = values.length % 2 === 0 ?
            (values[middle - 1] + values[middle]) / 2 :
            values[middle];
        } else {
          medians[field] = 0;
        }
      });

      // 映射指标到中文名称
      medians['duration'] = medians['接通时长'];
      medians['call'] = medians['外呼次数'];
      medians['connect'] = medians['接通次数'];
      medians['effective'] = medians['有效通次'];
    }

    // 更新坐席选择
    function updateAgentSelection(agents) {
      agentContainer.innerHTML = '';

      if (!agents || agents.length === 0) {
        agentContainer.innerHTML = `
                    <div class="flex items-center justify-center h-20 text-gray-400">
                        <i class="fa fa-exclamation-circle text-2xl mr-2"></i>
                        <span>未找到坐席数据</span>
                    </div>
                `;
        return;
      }

      agents.forEach(agent => {
        const checkbox = document.createElement('div');
        checkbox.className = 'flex items-center mb-2';
        checkbox.innerHTML = `
                    <input type="checkbox" id="agent-${agent}" name="agent" value="${agent}" class="agent-checkbox rounded text-primary focus:ring-primary h-4 w-4">
                    <label for="agent-${agent}" class="ml-2 text-sm text-gray-700">${agent}</label>
                `;
        agentContainer.appendChild(checkbox);

        // 添加事件监听器
        const input = checkbox.querySelector('input');
        input.addEventListener('change', updateChartDebounced);
      });

      // 默认全选
      document.querySelectorAll('.agent-checkbox').forEach(cb => {
        cb.checked = true;
      });
    }

    // 更新机构选择
    function updateOrgSelection(orgs, level) {
      orgContainer.innerHTML = '';

      // 过滤掉undefined和空字符串
      const validOrgs = orgs.filter(org => org !== undefined && org !== '');

      if (validOrgs.length === 0) {
        orgContainer.innerHTML = `
                  <div class="flex items-center justify-center h-20 text-gray-400">
                      <i class="fa fa-exclamation-circle text-2xl mr-2"></i>
                      <span>未找到机构数据</span>
                  </div>
              `;
        return;
      }

      // 创建机构复选框
      validOrgs.forEach(org => {
        const checkbox = document.createElement('div');
        checkbox.className = 'flex items-center mb-2';
        checkbox.innerHTML = `
                  <input type="checkbox" id="org-${level}-${org}" name="org" value="${org}" class="org-checkbox rounded text-primary focus:ring-primary h-4 w-4">
                  <label for="org-${level}-${org}" class="ml-2 text-sm text-gray-700">${org}</label>
              `;
        orgContainer.appendChild(checkbox);

        // 添加事件监听器
        const input = checkbox.querySelector('input');
        input.addEventListener('change', updateChartDebounced);
      });

      // 更新当前机构级别显示
      currentOrgLevelEl.textContent = `当前显示: ${['一级', '二级', '三级', '四级', '五级'][level - 1]}机构`;

      // 默认全选
      document.querySelectorAll('.org-checkbox').forEach(cb => {
        cb.checked = true;
      });
    }

    // 准备图表数据
    function prepareChartData() {
      if (!csvData || csvData.length === 0) return;

      // 过滤掉包含undefined或无效值的行
      const validData = csvData.filter(row => {
        return (
          row['坐席姓名'] !== undefined &&
          row['一级机构'] !== undefined &&
          row['二级机构'] !== undefined &&
          row['三级机构'] !== undefined &&
          row['四级机构'] !== undefined &&
          row['五级机构'] !== undefined &&
          row['日期'] !== undefined
        );
      });

      // 获取选中的坐席和机构
      const selectedAgents = Array.from(document.querySelectorAll('.agent-checkbox:checked'))
        .map(cb => cb.value);

      const selectedOrgs = Array.from(document.querySelectorAll('.org-checkbox:checked'))
        .map(cb => cb.value);

      // 检查是否有选中的坐席和机构
      if (selectedAgents.length === 0) {
        showNotification('提示', '请至少选择一个坐席', 'warning');
        return null;
      }

      if (selectedOrgs.length === 0) {
        showNotification('提示', '请至少选择一个机构', 'warning');
        return null;
      }

      // 确定使用的日期字段
      let dateField = '日期';

      // 根据日期类型分组
      const groupedData = {};
      const allAgentsGroupedData = {}; // 存储所有坐席的数据,用于计算机构均值

      // 确定要显示的指标
      let metricField = '';
      let metricName = '';

      switch (selectedMetric) {
        case 'duration':
          metricField = '接通时长';
          metricName = '通话时长(分钟)';
          break;
        case 'call':
          metricField = '外呼次数';
          metricName = '外呼次数';
          break;
        case 'connect':
          metricField = '接通次数';
          metricName = '接通次数';
          break;
        case 'effective':
          metricField = '有效通次';
          metricName = '有效通次';
          break;
      }

      // 更新图表标题
      chartTitle.textContent = `坐席${metricName}趋势分析`;

      // 处理数据 - 计算所有坐席的机构数据
      validData.forEach(row => {
        // 只过滤未选中的机构(保留所有坐席)
        if (!selectedOrgs.includes(row[`${['一', '二', '三', '四', '五'][currentOrgLevel - 1]}级机构`])) return;

        // 格式化日期
        let dateKey = row[dateField];

        // 根据日期类型调整
        if (selectedDateType === 'week') {
          const date = new Date(dateKey);
          const weekNum = Math.ceil((date.getDate() + 6 - date.getDay()) / 7);
          dateKey = `${date.getFullYear()}-W${weekNum}`;
        } else if (selectedDateType === 'month') {
          const date = new Date(dateKey);
          dateKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
        }

        // 初始化日期组
        if (!allAgentsGroupedData[dateKey]) {
          allAgentsGroupedData[dateKey] = {
            orgTotal: 0,
            orgCount: 0
          };
        }

        // 机构数据 (包含所有坐席)
        allAgentsGroupedData[dateKey].orgTotal += row[metricField];
        allAgentsGroupedData[dateKey].orgCount += 1;
      });

      // 处理数据 - 计算选中坐席的数据
      validData.forEach(row => {
        // 过滤未选中的坐席和机构
        if (!selectedAgents.includes(row['坐席姓名'])) return;
        if (!selectedOrgs.includes(row[`${['一', '二', '三', '四', '五'][currentOrgLevel - 1]}级机构`])) return;

        // 格式化日期 (与上面相同)
        let dateKey = row[dateField];

        if (selectedDateType === 'week') {
          const date = new Date(dateKey);
          const weekNum = Math.ceil((date.getDate() + 6 - date.getDay()) / 7);
          dateKey = `${date.getFullYear()}-W${weekNum}`;
        } else if (selectedDateType === 'month') {
          const date = new Date(dateKey);
          dateKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
        }

        // 初始化日期组
        if (!groupedData[dateKey]) {
          groupedData[dateKey] = {
            agents: {},
            orgTotal: 0,
            orgCount: 0
          };
        }

        // 坐席数据
        if (!groupedData[dateKey].agents[row['坐席姓名']]) {
          groupedData[dateKey].agents[row['坐席姓名']] = {
            value: 0,
            count: 0
          };
        }

        groupedData[dateKey].agents[row['坐席姓名']].value += row[metricField];
        groupedData[dateKey].agents[row['坐席姓名']].count += 1;

        // 机构数据 (仅选中坐席)
        groupedData[dateKey].orgTotal += row[metricField];
        groupedData[dateKey].orgCount += 1;
      });

      // 转换为图表可用的数据格式
      const sortedDates = Object.keys(groupedData).sort();
      const agentData = {};
      const orgAverageData = [];

      // 初始化坐席数据
      selectedAgents.forEach(agent => {
        agentData[agent] = [];
      });

      // 填充数据
      sortedDates.forEach(date => {
        const group = groupedData[date];
        const allAgentsGroup = allAgentsGroupedData[date] || { orgTotal: 0, orgCount: 0 };

        // 坐席数据
        selectedAgents.forEach(agent => {
          if (group.agents[agent]) {
            agentData[agent].push(group.agents[agent].value / group.agents[agent].count);
          } else {
            agentData[agent].push(null);
          }
        });

        // 机构平均数据 - 使用所有坐席的数据
        orgAverageData.push(allAgentsGroup.orgCount > 0 ?
          allAgentsGroup.orgTotal / allAgentsGroup.orgCount : null);
      });

      // 计算总体机构平均值 - 使用所有坐席的数据
      const allValidOrgValues = Object.values(allAgentsGroupedData)
        .map(g => g.orgCount > 0 ? g.orgTotal / g.orgCount : null)
        .filter(value => value !== null);

      const overallOrgAverage = allValidOrgValues.length > 0 ?
        allValidOrgValues.reduce((sum, value) => sum + value, 0) / allValidOrgValues.length :
        0;

      // 更新机构平均值显示
      currentOrgAverageEl.textContent = selectedMetric === 'duration' ?
        overallOrgAverage.toFixed(1) + ' 分钟' :
        overallOrgAverage.toFixed(0);

      // 更新进度条
      const maxValue = Math.max(
        overallOrgAverage,
        targetValue,
        ...Object.values(agentData).flat().filter(value => value !== null)
      );

      orgAverageBar.style.width = `${(overallOrgAverage / maxValue) * 100}%`;

      // 构建图表数据
      chartData = {
        labels: sortedDates,
        datasets: [],
        orgAverage: orgAverageData,
        overallOrgAverage: overallOrgAverage,
        maxValue: maxValue
      };

      // 为每个坐席创建数据集
      const colors = [
        '#3B82F6', '#10B981', '#6366F1', '#F59E0B', '#EF4444',
        '#06B6D4', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'
      ];

      selectedAgents.forEach((agent, index) => {
        chartData.datasets.push({
          label: agent,
          data: agentData[agent],
          borderColor: colors[index % colors.length],
          backgroundColor: `${colors[index % colors.length]}20`,
          borderWidth: 2,
          pointRadius: 3,
          pointHoverRadius: 5,
          tension: 0.1,
          fill: false
        });
      });

      // 添加机构平均线
      chartData.datasets.push({
        label: '机构平均',
        data: orgAverageData,
        borderColor: '#6B7280',
        borderWidth: 2,
        borderDash: [5, 5],
        pointRadius: 0,
        fill: false,
        order: 2
      });

      // 添加目标线
      if (targetValue !== null) {
        chartData.datasets.push({
          label: '目标值',
          data: sortedDates.map(() => targetValue),
          borderColor: '#F59E0B',
          borderWidth: 2,
          borderDash: [10, 5],
          pointRadius: 0,
          fill: false,
          order: 1
        });
      }

      return chartData;
    }


    // 更新图表
    function updateChart() {
      if (isChartUpdating) return;

      const chartData = prepareChartData();
      if (!chartData) {
        // 数据准备失败,不更新图表
        return;
      }

      isChartUpdating = true;
      showLoading(true);

      // 延迟更新图表,确保加载动画至少显示 MIN_LOADING_TIME 毫秒
      const loadingDuration = Date.now() - loadingStartTime;
      const delay = Math.max(0, MIN_LOADING_TIME - loadingDuration);

      setTimeout(() => {
        // 更新图表
        if (chart) {
          chart.data.labels = chartData.labels;
          chart.data.datasets = chartData.datasets;

          // 更新Y轴最大值,留出一些空间
          chart.options.scales.y.suggestedMax = chartData.maxValue * 1.1;

          // 更新标题
          chart.options.plugins.title = {
            display: true,
            text: chartTitle.textContent,
            font: {
              size: 16,
              weight: 'bold'
            }
          };

          chart.update();
        }

        isChartUpdating = false;
        showLoading(false);
      }, delay);
    }

    // 防抖处理更新图表
    let updateChartTimeout;
    function updateChartDebounced() {
      clearTimeout(updateChartTimeout);
      updateChartTimeout = setTimeout(updateChart, 300);
    }

    // 显示/隐藏加载状态
    function showLoading(show) {
      if (show) {
        loadingStartTime = Date.now();
        chartLoading.classList.remove('hidden');
      } else {
        chartLoading.classList.add('hidden');
      }
    }

    // 显示通知
    function showNotification(title, message, type = 'info') {
      notificationTitle.textContent = title;
      notificationMessage.textContent = message;

      // 设置图标
      notificationIcon.innerHTML = '';
      let iconClass = 'fa-info-circle';

      switch (type) {
        case 'success':
          iconClass = 'fa-check-circle';
          notificationIcon.className = 'mr-3 text-success';
          break;
        case 'error':
          iconClass = 'fa-exclamation-circle';
          notificationIcon.className = 'mr-3 text-danger';
          break;
        case 'warning':
          iconClass = 'fa-exclamation-triangle';
          notificationIcon.className = 'mr-3 text-warning';
          break;
        default:
          iconClass = 'fa-info-circle';
          notificationIcon.className = 'mr-3 text-primary';
      }

      notificationIcon.innerHTML = `<i class="fa ${iconClass}"></i>`;

      // 显示通知
      notification.classList.remove('translate-y-20', 'opacity-0');

      // 自动关闭
      setTimeout(() => {
        closeNotificationHandler();
      }, 5000);
    }

    // 关闭通知
    function closeNotificationHandler() {
      notification.classList.add('translate-y-20', 'opacity-0');
    }

    // 初始化
    function init() {
      // 初始化图表
      initChart();

      // 文件上传事件
      fileUpload.addEventListener('change', function (e) {
        const file = e.target.files[0];
        if (file) {
          parseCSV(file);
        }
      });

      // 修改日期筛选按钮事件处理函数
      dateDayBtn.addEventListener('click', function () {
        // 只清除日期组按钮的active状态
        document.querySelectorAll('#date-day, #date-week, #date-month').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedDateType = 'day';
        updateChartDebounced();
      });

      dateWeekBtn.addEventListener('click', function () {
        // 只清除日期组按钮的active状态
        document.querySelectorAll('#date-day, #date-week, #date-month').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedDateType = 'week';
        updateChartDebounced();
      });

      dateMonthBtn.addEventListener('click', function () {
        // 只清除日期组按钮的active状态
        document.querySelectorAll('#date-day, #date-week, #date-month').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedDateType = 'month';
        updateChartDebounced();
      });

      // 修改业绩指标按钮事件处理函数
      metricDurationBtn.addEventListener('click', function () {
        // 只清除指标组按钮的active状态
        document.querySelectorAll('#metric-duration, #metric-call, #metric-connect, #metric-effective').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedMetric = 'duration';
        targetValue = medians[selectedMetric];
        targetValueInput.value = targetValue;
        currentMedianEl.textContent = targetValue;
        updateChartDebounced();
      });

      metricCallBtn.addEventListener('click', function () {
        // 只清除指标组按钮的active状态
        document.querySelectorAll('#metric-duration, #metric-call, #metric-connect, #metric-effective').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedMetric = 'call';
        targetValue = medians[selectedMetric];
        targetValueInput.value = targetValue;
        currentMedianEl.textContent = targetValue;
        updateChartDebounced();
      });

      metricConnectBtn.addEventListener('click', function () {
        // 只清除指标组按钮的active状态
        document.querySelectorAll('#metric-duration, #metric-call, #metric-connect, #metric-effective').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedMetric = 'connect';
        targetValue = medians[selectedMetric];
        targetValueInput.value = targetValue;
        currentMedianEl.textContent = targetValue;
        updateChartDebounced();
      });

      metricEffectiveBtn.addEventListener('click', function () {
        // 只清除指标组按钮的active状态
        document.querySelectorAll('#metric-duration, #metric-call, #metric-connect, #metric-effective').forEach(btn => {
          btn.classList.remove('active');
        });
        this.classList.add('active');

        selectedMetric = 'effective';
        targetValue = medians[selectedMetric];
        targetValueInput.value = targetValue;
        currentMedianEl.textContent = targetValue;
        updateChartDebounced();
      });

      // 机构级别选择事件
      orgLevelBtns.forEach(btn => {
        btn.addEventListener('click', function () {
          currentOrgLevel = parseInt(this.dataset.level);
          orgLevelBtns.forEach(b => b.classList.remove('active', 'bg-primary', 'text-white'));
          orgLevelBtns.forEach(b => b.classList.add('bg-gray-100', 'hover:bg-gray-200'));
          this.classList.add('active', 'bg-primary', 'text-white');
          this.classList.remove('bg-gray-100', 'hover:bg-gray-200');

          // 更新机构选择
          if (csvData && csvData.length > 0) {
            const orgs = [...new Set(csvData.map(row => row[`${['一', '二', '三', '四', '五'][currentOrgLevel - 1]}级机构`]))].sort();
            updateOrgSelection(orgs, currentOrgLevel);
            updateChartDebounced();
          }
        });
      });

      // 全选/取消全选坐席
      selectAllAgentsBtn.addEventListener('click', function () {
        document.querySelectorAll('.agent-checkbox').forEach(cb => {
          cb.checked = true;
        });
        updateChartDebounced();
      });

      deselectAllAgentsBtn.addEventListener('click', function () {
        document.querySelectorAll('.agent-checkbox').forEach(cb => {
          cb.checked = false;
        });
        updateChartDebounced();
      });

      // 全选/取消全选机构
      selectAllOrgsBtn.addEventListener('click', function () {
        document.querySelectorAll('.org-checkbox').forEach(cb => {
          cb.checked = true;
        });
        updateChartDebounced();
      });

      deselectAllOrgsBtn.addEventListener('click', function () {
        document.querySelectorAll('.org-checkbox').forEach(cb => {
          cb.checked = false;
        });
        updateChartDebounced();
      });

      // 设置目标值
      setTargetBtn.addEventListener('click', function () {
        const value = parseFloat(targetValueInput.value);
        if (!isNaN(value)) {
          targetValue = value;
          updateChartDebounced();
          showNotification('成功', '目标值已更新', 'success');
        } else {
          showNotification('错误', '请输入有效的数值', 'error');
        }
      });

      // 按Enter键设置目标值
      targetValueInput.addEventListener('keypress', function (e) {
        if (e.key === 'Enter') {
          setTargetBtn.click();
        }
      });

      // 下载图表
      downloadPngBtn.addEventListener('click', function () {
        if (chart) {
          const link = document.createElement('a');
          link.download = '坐席业绩分析.png';
          link.href = chart.toBase64Image('image/png', 1.0);
          link.click();
        }
      });

      downloadSvgBtn.addEventListener('click', function () {
        if (chart) {
          // 注意:Chart.js默认不支持直接导出SVG,但可以通过一些库实现
          showNotification('提示', 'SVG导出功能需要额外的库支持', 'info');
        }
      });

      // 关闭通知
      closeNotification.addEventListener('click', closeNotificationHandler);

      // 初始提示
      showNotification('提示', '请上传CSV格式的业绩数据文件', 'info');
    }

    // 页面加载完成后初始化
    document.addEventListener('DOMContentLoaded', init);
  </script>
</body>

</html>

效果:

​​​​​​​

相关推荐
A_aspectJ10 小时前
【Bootstrap V4系列】学习入门教程之 组件-输入组(Input group)
前端·css·学习·bootstrap·html
想睡好16 小时前
圆角边框 盒子阴影 文字阴影
前端·css·html
zfyljx16 小时前
2048 html
前端·css·html
神仙别闹17 小时前
基于HTML+JavaScript+CSS实现教学网站
javascript·css·html
逊嘘1 天前
【Web前端开发】HTML基础
前端·html
Leo.yuan1 天前
热力图是什么?三分钟学会热力图数据分析怎么做!
大数据·数据库·数据挖掘·数据分析·html
GanGuaGua1 天前
CSS:盒子模型
开发语言·前端·css·html
2401_837088501 天前
CSS vertical-align
前端·html
Navicat中国2 天前
Navicat BI 数据分析功能上线 | 数据洞察新方法
数据库·人工智能·信息可视化·数据挖掘·数据分析·navicat·bi