visualization_center/index.html

1179 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>客服数据可视化中心</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 依赖 -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/isoweek.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/isoWeek.js"></script>
<style>
:root{--primary:#3388ff;}
* {box-sizing:border-box;}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Arial;font-size:14px;background:#f5f7fa;color:#333;}
.header{position:sticky;top:0;z-index:9;background:#fff;padding:12px 16px;box-shadow:0 2px 4px rgba(0,0,0,.08);}
.header h2{margin:0 0 8px;font-size:18px;}
.ctrl{display:flex;gap:12px;flex-wrap:wrap;align-items:center;}
.ctrl select,.ctrl input{padding:6px 10px;border:1px solid #ddd;border-radius:4px;background:#fff;}
.grid{display:grid;grid-template-columns:1fr;gap:12px;padding:12px;}
.card{background:#fff;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden;}
.card-title{font-weight:700;padding:12px 16px 0;font-size:15px;margin:0;}
.chart{height:400px;width:100%;min-height:350px;}
.chart-container{position:relative;width:100%;height:100%;}
/* 移动端 */
@media(max-width:600px){
.grid{grid-template-columns:1fr;}
.ctrl{flex-direction:column;align-items:stretch;}
}
.loading{display:flex;justify-content:center;align-items:center;height:100%;color:#999;}
</style>
</head>
<body>
<div class="header">
<h2>客服数据可视化中心</h2>
<a href="index_company.html" target="_blank">公司数据可视化中心</a>
<div class="ctrl">
<label>开始月份
<input type="month" id="startMonth" value="2025-01">
</label>
<label>结束月份
<input type="month" id="endMonth" value="2025-11">
</label>
<label>客服(多选)
<select id="staff" multiple style="height:70px;width:150px;"></select>
</label>
<label>指标
<select id="index">
<option value="完成率">完成率</option>
<option value="订单总数">订单总数</option>
<option value="溢价金额">溢价金额</option>
<option value="投诉率">投诉率</option>
<option value="空驶率">空驶率</option>
<option value="关闭率">关闭率</option>
</select>
</label>
<button id="export">导出 PNG</button>
</div>
</div>
<div class="grid">
<div class="card">
<div class="card-title">订单数量(多指标对比)</div>
<div class="chart-container"><div id="chart1" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">完成率</div>
<div class="chart-container"><div id="chart2" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">溢价金额</div>
<div class="chart-container"><div id="chart3" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">投诉率</div>
<div class="chart-container"><div id="chart4" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">空驶率</div>
<div class="chart-container"><div id="chart5" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">关闭率</div>
<div class="chart-container"><div id="chart6" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;">
<span>客服完成率分布</span>
<select id="pieChartIndex" style="padding:4px 8px;border:1px solid #ddd;border-radius:4px;font-size:12px;">
<option value="完成率">完成率</option>
<option value="溢价金额">溢价金额</option>
<option value="投诉率">投诉率</option>
<option value="空驶率">空驶率</option>
<option value="关闭率">关闭率</option>
</select>
</div>
<div class="chart-container"><div id="pie" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">月度趋势</div>
<div class="chart-container"><div id="line" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">客服多指标对比</div>
<div class="chart-container"><div id="bar" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">单个客服历史表现</div>
<div class="chart-container"><div id="hist" class="chart"></div></div>
</div>
</div>
<script>
/*****************************************************************
* 0. 原始数据明细 + 2025年1月-10月随机数据
*****************************************************************/
// 基础11月数据
const baseNovData = [
["2025-11","陈航明",891,415,750,"84.18%",50,"5.61%",68,"7.63%",91,"10.21%",2,"0.22%",3906.60,"4.28%",4,"0.45%",0,"0%",793,"99.13%",101,"11.34%",274,"30.75%",36,"4.04%",24,"2.69%",35,"3.93%"],
["2025-11","周汝琪",305,149,249,"81.64%",18,"5.9%",8,"2.62%",38,"12.46%",2,"0.66%",859.00,"2.91%",2,"0.66%",0,"0%",255,"95.51%",15,"4.92%",68,"22.3%",7,"2.3%",2,"0.66%",9,"2.95%"],
["2025-11","卢紫嫣",705,353,579,"82.13%",37,"5.25%",31,"4.4%",89,"12.62%",0,"0%",3382.00,"5.3%",3,"0.43%",2,"0.28%",616,"100%",79,"11.21%",194,"27.52%",37,"5.25%",24,"3.4%",66,"9.36%"],
["2025-11","徐婉茹",1010,466,840,"83.17%",52,"5.15%",62,"6.14%",118,"11.68%",1,"0.1%",2728.00,"2.66%",1,"0.1%",2,"0.2%",881,"98.77%",105,"10.4%",287,"28.42%",33,"3.27%",30,"2.97%",60,"5.94%"],
["2025-11","刘春霞",986,493,813,"82.45%",53,"5.38%",63,"6.39%",120,"12.17%",0,"0%",5342.00,"5.34%",4,"0.41%",0,"0%",861,"99.42%",85,"8.62%",295,"29.92%",29,"2.94%",38,"3.85%",59,"5.98%"],
["2025-11","王燕燕",336,160,275,"81.85%",21,"6.25%",15,"4.46%",40,"11.9%",1,"0.3%",1765.00,"4.97%",1,"0.3%",0,"0%",287,"96.96%",14,"4.17%",70,"20.83%",15,"4.46%",8,"2.38%",11,"3.27%"],
["2025-11","何丹妮",39,21,32,"82.05%",5,"12.82%",0,"0%",2,"5.13%",0,"0%",0.00,"0%",0,"0%",0,"0%",35,"94.59%",4,"10.26%",6,"15.38%",2,"5.13%",1,"2.56%",3,"7.69%"],
["2025-11","技术部测试",4,0,0,"0%",0,"0%",0,"0%",4,"100%",0,"0%",0.00,"0%",0,"0%",0,"0%",0,"0%",0,"0%",0,"0%",0,"0%",0,"0%",0,"0%",0,"0%"],
["2025-11","蒋卡泽",174,63,128,"73.56%",9,"5.17%",17,"9.77%",37,"21.26%",8,"4.6%",1355.00,"8.05%",0,"0%",0,"0%",137,"100%",13,"7.47%",56,"32.18%",4,"2.3%",2,"1.15%",6,"3.45%"],
["2025-11","冯雪",841,393,708,"84.19%",46,"5.47%",50,"5.95%",87,"10.34%",0,"0%",3697.00,"4.14%",2,"0.24%",1,"0.12%",690,"91.51%",68,"8.09%",233,"27.71%",26,"3.09%",18,"2.14%",53,"6.3%"],
["2025-11","啾啾AI",5070,6,5056,"99.72%",0,"0%",1,"0.02%",14,"0.28%",0,"0%",0.00,"0%",8,"0.16%",1,"0.02%",0,"0%",1,"0.02%",454,"8.95%",0,"0%",111,"2.19%",5050,"99.61%"],
["2025-11","王淑静",903,425,756,"83.72%",65,"7.2%",71,"7.86%",82,"9.08%",0,"0%",4089.00,"4.04%",6,"0.66%",1,"0.11%",780,"95.01%",94,"10.41%",269,"29.79%",22,"2.44%",14,"1.55%",60,"6.64%"],
["2025-11","方文明",521,292,433,"83.11%",38,"7.29%",22,"4.22%",50,"9.6%",0,"0%",2158.00,"3.83%",5,"0.96%",0,"0%",442,"93.84%",39,"7.49%",123,"23.61%",17,"3.26%",9,"1.73%",27,"5.18%"],
["2025-11","陈家南",11,3,9,"81.82%",0,"0%",2,"18.18%",2,"18.18%",0,"0%",850.00,"16.66%",0,"0%",0,"0%",2,"22.22%",1,"9.09%",2,"18.18%",0,"0%",0,"0%",8,"72.73%"],
["2025-11","全部",11796,3239,10628,"90.1%",394,"3.34%",410,"3.48%",774,"6.56%",14,"0.12%",30131.60,"2.53%",36,"0.31%",7,"0.06%",5779,"52.43%",619,"5.25%",2331,"19.76%",228,"1.93%",281,"2.38%",5447,"46.18%"]
];
// 生成2025年1月-10月随机数据
function generateMonthlyData(baseData, months) {
const allData = [...baseData];
for (let month = 1; month <= 10; month++) {
const monthStr = month.toString().padStart(2, '0');
const yearMonth = `2025-${monthStr}`;
baseData.forEach(row => {
if (row[1] !== '全部') { // 跳过"全部"汇总行
const newRow = [...row];
newRow[0] = yearMonth; // 更新月份
// 为数值字段添加随机变化 (±20%)
for (let i = 2; i <= 30; i += 2) {
if (typeof newRow[i] === 'number') {
const variation = 0.8 + Math.random() * 0.4; // 0.8-1.2
newRow[i] = Math.round(newRow[i] * variation);
}
}
// 为百分比字段添加随机变化 (±5%)
for (let i = 3; i <= 31; i += 2) {
if (typeof newRow[i] === 'string' && newRow[i].includes('%')) {
const baseValue = parseFloat(newRow[i]);
const variation = 0.95 + Math.random() * 0.1; // 0.95-1.05
const newValue = Math.max(0, Math.min(100, baseValue * variation));
newRow[i] = newValue.toFixed(2) + '%';
}
}
allData.push(newRow);
}
});
// 添加月度汇总行
const summaryRow = [...baseData.find(row => row[1] === '全部')];
summaryRow[0] = yearMonth;
allData.push(summaryRow);
}
return allData;
}
const rawNov = generateMonthlyData(baseNovData, 10);
/* 解析为统一对象 - 修复变量名问题,添加错误处理 */
function parseRow(r){
// 辅助函数:安全解析百分比或数值
function safeParse(value, defaultValue = 0) {
if (value === null || value === undefined) return defaultValue;
if (typeof value === 'number') return value;
if (typeof value === 'string') {
// 移除 % 符号并解析
const cleaned = value.replace('%', '').trim();
// 检查是否包含无效字符
if (cleaned === '' || cleaned === '极' || isNaN(cleaned)) {
return defaultValue;
}
const parsed = parseFloat(cleaned);
return isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
}
const [
月份,调度,订单总数,考核数,完成数,完成率,
空驶数,空驶率,线下数,线下率,关闭数,关闭率,
拒单数,拒单率,溢价金额,溢价率,不满数,不满率,
投诉数,投诉率,审单数,审单率,预约数,预约率,
未接50数,未接50率,超时数,超时率,不符数,不符率,
未联系数,未联系率
] = r;
return {
月份,调度,
订单总数: safeParse(订单总数, 0),
考核数: safeParse(考核数, 0),
完成数: safeParse(完成数, 0),
完成率: safeParse(完成率, 0),
空驶数: safeParse(空驶数, 0),
空驶率: safeParse(空驶率, 0),
线下数: safeParse(线下数, 0),
线下率: safeParse(线下率, 0),
关闭数: safeParse(关闭数, 0),
关闭率: safeParse(关闭率, 0),
拒单数: safeParse(拒单数, 0),
拒单率: safeParse(拒单率, 0),
溢价金额: safeParse(溢价金额, 0),
溢价率: safeParse(溢价率, 0),
不满数: safeParse(不满数, 0),
不满率: safeParse(不满率, 0),
投诉数: safeParse(投诉数, 0),
投诉率: safeParse(投诉率, 0),
审单数: safeParse(审单数, 0),
审单率: safeParse(审单率, 0),
预约数: safeParse(预约数, 0),
预约率: safeParse(预约率, 0),
未接50数: safeParse(未接50数, 0),
未接50率: safeParse(未接50率, 0),
超时数: safeParse(超时数, 0),
超时率: safeParse(超时率, 0),
不符数: safeParse(不符数, 0),
不符率: safeParse(不符率, 0),
未联系数: safeParse(未联系数, 0),
未联系率: safeParse(未联系率, 0)
};
}
// 解析数据,添加错误处理
let novData = [];
try {
novData = rawNov.map(parseRow);
console.log('数据解析成功,共', novData.length, '条记录');
} catch (error) {
console.error('数据解析失败:', error);
// 如果解析失败,至少显示空数据,避免页面完全无法显示
novData = [];
}
/* 生成 30 天趋势Demo */
function genTrend(base){
const arr=[];
for(let d=1;d<=30;d++){
const date=dayjs('2025-11-01').add(d-1,'day');
base.forEach(b=>{
arr.push({
日期:date.format('YYYY-MM-DD'),
客服:b.调度,
完成率:b.完成率+(Math.random()-0.5)*4,
订单总数:b.订单总数+Math.floor((Math.random()-0.5)*100),
溢价金额:b.溢价金额+(Math.random()-0.5)*500,
投诉率:b.投诉率+(Math.random()-0.5)*0.5,
空驶率:b.空驶率+(Math.random()-0.5)*1,
关闭率:b.关闭率+(Math.random()-0.5)*1
});
});
}
return arr;
}
const trendData = genTrend(novData.filter(b=>b.调度!=='全部' && b.调度!=='技术部测试'));
/* 全局状态 */
const state = {
month:'2025-11',
staff:[],
index:'完成率',
trendData,
novData,
drillName: null,
selectedStaffs: [], // 从柱状图点击选中的客服列表
pieChartIndex: '完成率' // 饼图/柱状图切换的指标
};
/* ECharts 实例 */
let pie, line, bar, hist;
let chart1, chart2, chart3, chart4, chart5, chart6; // 时间序列图表
/* 通用配色 */
const colors = ['#3388ff','#00c362','#ff9f00','#ff4d4f','#9a6bfd','#36cfc9','#ff7ec2','#ffda5d'];
/* 初始化图表 */
function initCharts() {
try {
const chart1Dom = document.getElementById('chart1');
const chart2Dom = document.getElementById('chart2');
const chart3Dom = document.getElementById('chart3');
const chart4Dom = document.getElementById('chart4');
const chart5Dom = document.getElementById('chart5');
const chart6Dom = document.getElementById('chart6');
const pieDom = document.getElementById('pie');
const lineDom = document.getElementById('line');
const barDom = document.getElementById('bar');
const histDom = document.getElementById('hist');
if (chart1Dom && !chart1) chart1 = echarts.init(chart1Dom);
if (chart2Dom && !chart2) chart2 = echarts.init(chart2Dom);
if (chart3Dom && !chart3) chart3 = echarts.init(chart3Dom);
if (chart4Dom && !chart4) chart4 = echarts.init(chart4Dom);
if (chart5Dom && !chart5) chart5 = echarts.init(chart5Dom);
if (chart6Dom && !chart6) chart6 = echarts.init(chart6Dom);
if (pieDom && !pie) pie = echarts.init(pieDom);
if (lineDom && !line) line = echarts.init(lineDom);
if (barDom && !bar) bar = echarts.init(barDom);
if (histDom && !hist) hist = echarts.init(histDom);
console.log('图表初始化成功');
} catch (error) {
console.error('图表初始化失败:', error);
}
}
/* 按月份聚合数据 */
function aggregateByMonth(data, staffs) {
const monthData = {};
// 过滤选中的客服
const filteredData = data.filter(d =>
staffs.length === 0 || staffs.includes(d.调度)
);
filteredData.forEach(d => {
if (d.调度 === '全部' || d.调度 === '技术部测试') return;
const month = d.月份;
if (!monthData[month]) {
monthData[month] = {
月份: month,
订单总数: 0,
完成率: 0,
溢价金额: 0,
投诉率: 0,
空驶率: 0,
关闭率: 0,
count: 0
};
}
monthData[month].订单总数 += d.订单总数 || 0;
monthData[month].完成率 += d.完成率 || 0;
monthData[month].溢价金额 += d.溢价金额 || 0;
monthData[month].投诉率 += d.投诉率 || 0;
monthData[month].空驶率 += d.空驶率 || 0;
monthData[month].关闭率 += d.关闭率 || 0;
monthData[month].count += 1;
});
// 计算平均值(对于百分比指标)
Object.keys(monthData).forEach(month => {
const m = monthData[month];
if (m.count > 0) {
m.完成率 = m.完成率 / m.count;
m.投诉率 = m.投诉率 / m.count;
m.空驶率 = m.空驶率 / m.count;
m.关闭率 = m.关闭率 / m.count;
}
});
return Object.values(monthData).sort((a, b) => a.月份.localeCompare(b.月份));
}
/* 时间序列图表更新函数 */
/* 图表1订单数量多指标对比 */
function updateChart1() {
if (!chart1) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
legend: {
data: ['订单总数', '完成率', '溢价金额', '投诉率', '空驶率', '关闭率'],
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: [
{
type: 'value',
name: '订单总数/金额',
position: 'left'
},
{
type: 'value',
name: '百分比(%)',
position: 'right'
}
],
series: [
{
name: '订单总数',
type: 'bar',
data: monthData.map(d => d.订单总数)
},
{
name: '溢价金额',
type: 'bar',
data: monthData.map(d => d.溢价金额)
},
{
name: '完成率',
type: 'line',
yAxisIndex: 1,
data: monthData.map(d => d.完成率)
},
{
name: '投诉率',
type: 'line',
yAxisIndex: 1,
data: monthData.map(d => d.投诉率)
},
{
name: '空驶率',
type: 'line',
yAxisIndex: 1,
data: monthData.map(d => d.空驶率)
},
{
name: '关闭率',
type: 'line',
yAxisIndex: 1,
data: monthData.map(d => d.关闭率)
}
]
};
chart1.setOption(option, true);
}
/* 图表2完成率 */
function updateChart2() {
if (!chart2) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors[0],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: '完成率(%)'
},
series: [{
name: '完成率',
type: 'bar',
data: monthData.map(d => d.完成率)
}]
};
chart2.setOption(option, true);
}
/* 图表3溢价金额 */
function updateChart3() {
if (!chart3) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors[2],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: '溢价金额(元)'
},
series: [{
name: '溢价金额',
type: 'bar',
data: monthData.map(d => d.溢价金额)
}]
};
chart3.setOption(option, true);
}
/* 图表4投诉率 */
function updateChart4() {
if (!chart4) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors[3],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: '投诉率(%)'
},
series: [{
name: '投诉率',
type: 'bar',
data: monthData.map(d => d.投诉率)
}]
};
chart4.setOption(option, true);
}
/* 图表5空驶率 */
function updateChart5() {
if (!chart5) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors[4],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: '空驶率(%)'
},
series: [{
name: '空驶率',
type: 'bar',
data: monthData.map(d => d.空驶率)
}]
};
chart5.setOption(option, true);
}
/* 图表6关闭率 */
function updateChart6() {
if (!chart6) return;
const {novData, staff} = state;
const monthData = aggregateByMonth(novData, staff);
const months = monthData.map(d => d.月份);
const option = {
color: colors[5],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: '关闭率(%)'
},
series: [{
name: '关闭率',
type: 'bar',
data: monthData.map(d => d.关闭率)
}]
};
chart6.setOption(option, true);
}
/* 1. 饼图 - 客服完成率分布(支持切换指标) */
function updatePie(){
if (!pie) {
console.warn('饼图实例未初始化');
return;
}
if (!state.novData || state.novData.length === 0) {
console.warn('没有数据可显示');
return;
}
const pieIndex = state.pieChartIndex || '完成率';
const isRate = pieIndex.includes('率');
// 获取最新月份的数据(假设数据按月份排序,取最后一个月)
const months = [...new Set(state.novData.map(d => d.月份))].sort();
const latestMonth = months[months.length - 1] || months[0];
const data = state.novData
.filter(d => d.调度 !== '全部' && d.调度 !== '技术部测试' && d.月份 === latestMonth)
.map(d => {
const value = d[pieIndex];
return {
name: d.调度,
value: (value !== undefined && value !== null && !isNaN(value)) ? value : 0
};
})
.filter(d => d.value > 0); // 过滤掉值为0的数据
// 如果是完成率,显示饼图;其他指标显示柱状图
if (pieIndex === '完成率') {
const option = {
color: colors,
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}' + (isRate ? '%' : '')
},
legend: {
type: 'scroll',
orient: 'vertical',
left: 'left',
top: 20,
height: 200
},
series: [{
name: pieIndex,
type: 'pie',
radius: ['40%', '70%'],
center: ['55%', '50%'],
data: data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0,0,0,.5)'
}
},
label: {
show: true,
formatter: '{b}\n{c}' + (isRate ? '%' : '')
}
}]
};
pie.setOption(option, true);
} else {
// 其他指标显示柱状图
const barData = data.sort((a, b) => b.value - a.value); // 按值降序排序
const option = {
color: colors[0],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: '{b}: {c}' + (isRate ? '%' : '')
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: barData.map(d => d.name),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: pieIndex + (isRate ? '(%)' : '')
},
series: [{
name: pieIndex,
type: 'bar',
data: barData.map(d => d.value),
barMaxWidth: 40,
label: {
show: true,
position: 'top',
formatter: '{c}' + (isRate ? '%' : '')
}
}]
};
pie.setOption(option, true);
}
}
/* 2. 折线 - 月度趋势(按日) */
function updateLine(){
if (!line) {
console.warn('折线图实例未初始化');
return;
}
const {trendData, staff, index} = state;
if (!trendData || trendData.length === 0) {
console.warn('趋势数据为空');
return;
}
const days = [...new Set(trendData.map(d => d.日期))].sort();
const series = staff.map((s, idx) => ({
name: s,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
data: days.map(day => {
const item = trendData.find(d => d.日期 === day && d.客服 === s);
return item ? Math.max(0, item[index]) : 0;
})
}));
const option = {
color: colors,
tooltip: {
trigger: 'axis'
},
legend: {
type: 'scroll',
top: 5,
height: 60
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: days,
boundaryGap: false,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: index + (state.index.includes('率') ? '(%)' : '')
},
series: series
};
line.setOption(option, true);
}
/* 3. 柱状 - 客服多指标对比 */
function updateBar(){
if (!bar) {
console.warn('柱状图实例未初始化');
return;
}
const {novData, staff, index} = state;
if (!novData || novData.length === 0) {
console.warn('没有数据可显示');
return;
}
const filterData = novData.filter(d =>
staff.length ? staff.includes(d.调度) : (d.调度 !== '全部' && d.调度 !== '技术部测试')
);
const barData = filterData.map(d => {
const value = d[index];
return {
name: d.调度,
value: (value !== undefined && value !== null && !isNaN(value)) ? value : 0
};
});
const option = {
color: colors[0],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: barData.map(d => d.name),
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: index + (state.index.includes('率') ? '(%)' : '')
},
series: [{
name: state.index,
type: 'bar',
data: barData.map(d => d.value),
barMaxWidth: 40
}],
animationDuration: 600
};
bar.setOption(option, true);
// 柱图点击钻取 → 历史表现(支持多选)
bar.off('click');
bar.on('click', params => {
const name = params.name;
// 如果已选择该客服,则取消选择;否则添加选择
if (!state.selectedStaffs) {
state.selectedStaffs = [];
}
const index = state.selectedStaffs.indexOf(name);
if (index > -1) {
// 已选中,取消选择
state.selectedStaffs.splice(index, 1);
} else {
// 未选中,添加选择
state.selectedStaffs.push(name);
}
// 如果只有一个,设置 drillName否则清空
if (state.selectedStaffs.length === 1) {
state.drillName = state.selectedStaffs[0];
} else {
state.drillName = null;
}
updateHist();
});
}
/* 4. 历史 - 单个或多个客服趋势 */
function updateHist(){
if (!hist) {
console.warn('历史图表实例未初始化');
return;
}
// 确定要显示的客服列表
let staffsToShow = [];
// 优先使用选中的客服(从柱状图点击选择)
if (state.selectedStaffs && state.selectedStaffs.length > 0) {
staffsToShow = state.selectedStaffs;
}
// 其次使用 drillName单个客服
else if (state.drillName) {
staffsToShow = [state.drillName];
}
// 再次使用选中的客服(从下拉框选择)
else if (state.staff && state.staff.length > 0) {
staffsToShow = state.staff;
}
// 最后使用默认客服
else {
const defaultStaff = state.novData.find(d => d.调度 !== '全部' && d.调度 !== '技术部测试')?.调度 || '陈航明';
staffsToShow = [defaultStaff];
}
// 获取所有选中客服的数据
const allData = state.trendData.filter(d => staffsToShow.includes(d.客服));
if (!allData || allData.length === 0) {
console.warn('没有找到客服数据:', staffsToShow);
return;
}
// 获取所有日期
const days = [...new Set(allData.map(d => d.日期))].sort();
// 为每个客服创建一条折线
const series = staffsToShow.map((staffName, idx) => {
const staffData = allData.filter(d => d.客服 === staffName);
return {
name: staffName,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
data: days.map(day => {
const item = staffData.find(d => d.日期 === day);
return item ? Math.max(0, item[state.index]) : null;
})
};
});
// 设置标题
let titleText = '';
if (staffsToShow.length === 1) {
titleText = staffsToShow[0] + ' 历史表现';
} else {
titleText = '多个客服历史表现对比';
}
const option = {
title: {
text: titleText,
left: 'center',
textStyle: { fontSize: 14 }
},
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
type: 'scroll',
top: 30,
data: staffsToShow,
height: 40
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
data: days,
boundaryGap: false,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: state.index + (state.index.includes('率') ? '(%)' : '')
},
series: series
};
hist.setOption(option, true);
}
/* 5. 控件事件 */
function bindCtrl(){
const startMonthInp = document.getElementById('startMonth');
const endMonthInp = document.getElementById('endMonth');
const staffSel = document.getElementById('staff');
const indexSel = document.getElementById('index');
/* 初始化月份范围状态 */
state.startMonth = startMonthInp.value;
state.endMonth = endMonthInp.value;
state.staff = [...staffSel.selectedOptions].map(o => o.value);
state.index = indexSel.value;
// 开始月份变化事件
startMonthInp.addEventListener('change', e => {
state.startMonth = e.target.value;
// 确保结束月份不早于开始月份
if (dayjs(state.endMonth).isBefore(dayjs(state.startMonth))) {
endMonthInp.value = state.startMonth;
state.endMonth = state.startMonth;
}
renderAll();
});
// 结束月份变化事件
endMonthInp.addEventListener('change', e => {
state.endMonth = e.target.value;
// 确保开始月份不晚于结束月份
if (dayjs(state.startMonth).isAfter(dayjs(state.endMonth))) {
startMonthInp.value = state.endMonth;
state.startMonth = state.endMonth;
}
renderAll();
});
staffSel.addEventListener('change', e => {
state.staff = [...e.target.selectedOptions].map(o => o.value);
// 当从下拉框选择客服时,清空柱状图点击选择的客服
if (state.staff.length > 0) {
state.selectedStaffs = [];
state.drillName = null;
}
renderAll();
});
indexSel.addEventListener('change', e => {
state.index = e.target.value;
renderAll();
});
// 饼图指标切换
const pieChartIndexSel = document.getElementById('pieChartIndex');
if (pieChartIndexSel) {
pieChartIndexSel.value = state.pieChartIndex || '完成率';
pieChartIndexSel.addEventListener('change', e => {
state.pieChartIndex = e.target.value;
updatePie();
});
}
document.getElementById('export').addEventListener('click', () => {
if (bar) {
const url = bar.getDataURL({type: 'png', pixelRatio: 2});
const a = document.createElement('a');
a.download = '客服对比_' + state.index + '.png';
a.href = url;
a.click();
}
});
}
/* 6. 渲染全部 */
function renderAll(){
updateChart1();
updateChart2();
updateChart3();
updateChart4();
updateChart5();
updateChart6();
updatePie();
updateLine();
updateBar();
updateHist();
}
/* 7. 初始化控件选项 */
function initCtrl(){
const staffSel = document.getElementById('staff');
const staffs = [...new Set(novData.map(d => d.调度))].filter(s => s !== '全部' && s !== '技术部测试');
staffs.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.textContent = s;
opt.selected = true;
staffSel.appendChild(opt);
});
// 初始化选中客服
state.staff = staffs;
}
/* 8. 响应式 */
function handleResize() {
if (chart1) chart1.resize();
if (chart2) chart2.resize();
if (chart3) chart3.resize();
if (chart4) chart4.resize();
if (chart5) chart5.resize();
if (chart6) chart6.resize();
if (pie) pie.resize();
if (line) line.resize();
if (bar) bar.resize();
if (hist) hist.resize();
}
window.addEventListener('resize', handleResize);
/* 9. 启动 */
function init() {
try {
console.log('开始初始化页面...');
initCtrl();
initCharts();
bindCtrl();
renderAll();
console.log('页面初始化完成');
} catch (error) {
console.error('页面初始化失败:', error);
// 显示错误信息给用户
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:20px;background:#fff;border:2px solid #f00;border-radius:8px;z-index:9999;';
errorMsg.innerHTML = '<h3>页面加载错误</h3><p>' + error.message + '</p><p>请检查浏览器控制台获取详细信息</p>';
document.body.appendChild(errorMsg);
}
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
// 延迟一点执行,确保所有资源都已加载
setTimeout(init, 100);
}
</script>
</body>
</html>