1179 lines
42 KiB
HTML
1179 lines
42 KiB
HTML
<!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> |