豆包提示词:
使用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>
效果: