1299 lines
44 KiB
HTML
1299 lines
44 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="./js/echarts.min.js"></script>
|
||
<script src="./js/dayjs.min.js"></script>
|
||
<script src="./js/isoWeek.js"></script>
|
||
<script src="./node_modules/axios/dist/axios.min.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>
|
||
<button id="searchBtn">搜索</button>
|
||
<label>客服
|
||
<select id="staff"></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">导出excel</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">客服完成率分布</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" style="display:flex;justify-content:space-between;align-items:center;">
|
||
<span>客服多指标对比</span>
|
||
<select id="barChartIndex" 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>
|
||
<option value="关闭率">关闭率</option>
|
||
</select>
|
||
</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. API 请求配置
|
||
*****************************************************************/
|
||
const apiUrl = [
|
||
'https://backend.jjsos.cn/third-api/', //正式
|
||
'https://backendt.jjsos.cn/third-api/',//测试
|
||
'http://backend.jjdev.cn/third-api/' //本地
|
||
];
|
||
let url = apiUrl[2];
|
||
|
||
// 调用 getInfo 接口
|
||
async function fetchData(startMonth, endMonth) {
|
||
try {
|
||
console.log('开始请求数据...', {
|
||
time_start: startMonth,
|
||
time_end: endMonth
|
||
});
|
||
const response = await axios({
|
||
url: url + 'get-data',
|
||
method: 'get',
|
||
params: {
|
||
time_start: startMonth,
|
||
time_end: endMonth
|
||
}
|
||
});
|
||
console.log('接口返回数据:', response.data);
|
||
return response.data;
|
||
} catch (error) {
|
||
console.error('接口请求失败:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 处理接口返回的数据,转换为内部数据格式
|
||
function processApiData(apiData) {
|
||
if (!apiData || !apiData.data) {
|
||
console.warn('接口数据为空');
|
||
return [];
|
||
}
|
||
|
||
const processedData = [];
|
||
|
||
// 遍历每个客服
|
||
Object.keys(apiData.data).forEach(staffName => {
|
||
const staffMonthData = apiData.data[staffName];
|
||
|
||
// 遍历该客服的每个月份数据
|
||
staffMonthData.forEach(monthData => {
|
||
const row = {
|
||
月份: monthData.month,
|
||
调度: monthData.name,
|
||
订单总数: parseInt(monthData.total) || 0,
|
||
考核数: 0, // 接口未提供
|
||
完成数: parseInt(monthData.finishNum) || 0,
|
||
完成率: parseFloat(monthData.finishPer) || 0,
|
||
空驶数: parseInt(monthData.emptyNum) || 0,
|
||
空驶率: parseFloat(monthData.emptyPer) || 0,
|
||
线下数: 0,
|
||
线下率: 0,
|
||
关闭数: parseInt(monthData.closeNum) || 0,
|
||
关闭率: parseFloat(monthData.closePer) || 0,
|
||
拒单数: 0,
|
||
拒单率: 0,
|
||
溢价金额: parseFloat(monthData.premium) || 0,
|
||
溢价率: 0,
|
||
不满数: 0,
|
||
不满率: 0,
|
||
投诉数: parseInt(monthData.complainNum) || 0,
|
||
投诉率: parseFloat(monthData.complainPer) || 0,
|
||
审单数: 0,
|
||
审单率: 0,
|
||
预约数: 0,
|
||
预约率: 0,
|
||
未接50数: 0,
|
||
未接50率: 0,
|
||
超时数: 0,
|
||
超时率: 0,
|
||
不符数: 0,
|
||
不符率: 0,
|
||
未联系数: 0,
|
||
未联系率: 0
|
||
};
|
||
|
||
processedData.push(row);
|
||
});
|
||
});
|
||
|
||
console.log('处理后的数据:', processedData);
|
||
return processedData;
|
||
}
|
||
|
||
/*****************************************************************
|
||
* 1. 初始化空数据
|
||
*****************************************************************/
|
||
// 初始化为空数组,等待接口数据
|
||
let novData = [];
|
||
|
||
/* 生成趋势数据(基于真实月度数据模拟日度数据) */
|
||
function genTrend(base){
|
||
const arr=[];
|
||
// 如果没有数据,返回空数组
|
||
if (!base || base.length === 0) return arr;
|
||
|
||
// 获取最新月份
|
||
const months = [...new Set(base.map(b => b.月份))].sort();
|
||
const latestMonth = months[months.length - 1];
|
||
|
||
if (!latestMonth) return arr;
|
||
|
||
// 使用最新月份的数据生成30天趋势
|
||
const latestData = base.filter(b => b.月份 === latestMonth);
|
||
|
||
for(let d=1;d<=30;d++){
|
||
const date=dayjs(latestMonth + '-01').add(d-1,'day');
|
||
latestData.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;
|
||
}
|
||
|
||
// 初始化为空数组
|
||
let trendData = [];
|
||
|
||
/* 全局状态 */
|
||
const state = {
|
||
month: '',
|
||
staff: [],
|
||
index: '完成率',
|
||
trendData: [],
|
||
novData: [],
|
||
drillName: null,
|
||
selectedStaffs: [], // 从柱状图点击选中的客服列表
|
||
pieChartIndex: '完成率', // 饼图/柱状图切换的指标
|
||
barChartIndex: '完成率' // 柱状图切换的指标
|
||
};
|
||
|
||
/* 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,
|
||
空驶数: 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;
|
||
|
||
// 如果是单个客服,直接使用百分比;如果是多个客服,稍后计算
|
||
if (staffs.length === 1) {
|
||
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 (staffs.length === 0 || staffs.length > 1) {
|
||
// 全部客服:动态计算百分比,保留2位小数
|
||
if (m.订单总数 > 0) {
|
||
// 完成率 = (完成数 + 空驶数) / 订单总数 * 100
|
||
m.完成率 = parseFloat(((m.完成数 + m.空驶数) / m.订单总数 * 100).toFixed(2));
|
||
m.投诉率 = parseFloat((m.投诉数 / m.订单总数 * 100).toFixed(2));
|
||
m.空驶率 = parseFloat((m.空驶数 / m.订单总数 * 100).toFixed(2));
|
||
m.关闭率 = parseFloat((m.关闭数 / m.订单总数 * 100).toFixed(2));
|
||
} else {
|
||
m.完成率 = 0;
|
||
m.投诉率 = 0;
|
||
m.空驶率 = 0;
|
||
m.关闭率 = 0;
|
||
}
|
||
}
|
||
// 溢价金额也保留2位小数
|
||
m.溢价金额 = parseFloat(m.溢价金额.toFixed(2));
|
||
});
|
||
|
||
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];
|
||
|
||
console.log('饼图数据 - 最新月份:', latestMonth, '指标:', pieIndex);
|
||
console.log('当前novData:', state.novData);
|
||
|
||
// 无论是否选择客服,都显示所有客服的数据(排除"全部"和"技术部测试")
|
||
const data = state.novData
|
||
.filter(d => {
|
||
const isValid = d.调度 !== '全部' && d.调度 !== '技术部测试' && d.月份 === latestMonth;
|
||
if (isValid) {
|
||
console.log('饼图客服:', d.调度, pieIndex + ':', d[pieIndex]);
|
||
}
|
||
return isValid;
|
||
})
|
||
.map(d => {
|
||
const value = d[pieIndex];
|
||
// 确保值是有效的数字
|
||
const parsedValue = parseFloat(value);
|
||
return {
|
||
name: d.调度,
|
||
value: (!isNaN(parsedValue) && parsedValue > 0) ? parsedValue : 0
|
||
};
|
||
})
|
||
.filter(d => d.value > 0); // 过滤掉值为0的数据
|
||
|
||
console.log('饼图最终数据:', data);
|
||
|
||
// 如果没有有效数据,显示提示
|
||
if (data.length === 0) {
|
||
console.warn('饼图没有有效数据');
|
||
pie.setOption({
|
||
title: {
|
||
text: '暂无数据',
|
||
left: 'center',
|
||
top: 'center',
|
||
textStyle: {
|
||
color: '#999',
|
||
fontSize: 16
|
||
}
|
||
}
|
||
}, true);
|
||
return;
|
||
}
|
||
|
||
// 如果是完成率,显示饼图;其他指标显示柱状图
|
||
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 {novData, staff, index} = state;
|
||
|
||
// 过滤掉 '全部' 和 '测试' 账号
|
||
const validData = novData.filter(d => !d.调度.includes('全部') && !d.调度.includes('测试'));
|
||
|
||
if (!validData || validData.length === 0) {
|
||
console.warn('趋势数据为空');
|
||
return;
|
||
}
|
||
|
||
const months = [...new Set(validData.map(d => d.月份))].sort();
|
||
|
||
// 如果没有选择客服,显示所有客服
|
||
const staffsToShow = staff.length > 0 ? staff : [...new Set(validData.map(d => d.调度))];
|
||
|
||
const series = staffsToShow.map((s, idx) => ({
|
||
name: s,
|
||
type: 'line',
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: { width: 3 },
|
||
data: months.map(month => {
|
||
const item = validData.find(d => d.月份 === month && 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: months,
|
||
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} = state;
|
||
const index = state.barChartIndex || '完成率';
|
||
|
||
if (!novData || novData.length === 0) {
|
||
console.warn('没有数据可显示');
|
||
return;
|
||
}
|
||
|
||
// 获取最新月份
|
||
const months = [...new Set(novData.map(d => d.月份))].sort();
|
||
const latestMonth = months[months.length - 1];
|
||
|
||
const filterData = novData.filter(d =>
|
||
d.月份 === latestMonth &&
|
||
d.调度 !== '全部' && !d.调度.includes('测试')
|
||
);
|
||
|
||
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 + (index.includes('率') ? '(%)' : '')
|
||
},
|
||
series: [{
|
||
name: 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 idx = state.selectedStaffs.indexOf(name);
|
||
if (idx > -1) {
|
||
// 已选中,取消选择
|
||
state.selectedStaffs.splice(idx, 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.调度.includes('全部') && !d.调度.includes('测试'))?.调度 || '陈航明';
|
||
staffsToShow = [defaultStaff];
|
||
}
|
||
|
||
// 获取所有选中客服的数据(使用 novData)
|
||
const allData = state.novData.filter(d => staffsToShow.includes(d.调度));
|
||
|
||
if (!allData || allData.length === 0) {
|
||
console.warn('没有找到客服数据:', staffsToShow);
|
||
return;
|
||
}
|
||
|
||
// 获取所有月份
|
||
const months = [...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: months.map(month => {
|
||
const item = staffData.find(d => d.月份 === month);
|
||
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: months,
|
||
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');
|
||
const searchBtn = document.getElementById('searchBtn');
|
||
|
||
/* 初始化月份范围状态 */
|
||
state.startMonth = startMonthInp.value;
|
||
state.endMonth = endMonthInp.value;
|
||
state.staff = [];
|
||
state.index = indexSel.value;
|
||
|
||
// 搜索按钮点击事件
|
||
searchBtn.addEventListener('click', async () => {
|
||
state.startMonth = startMonthInp.value;
|
||
state.endMonth = endMonthInp.value;
|
||
|
||
// 确保月份范围有效
|
||
if (dayjs(state.endMonth).isBefore(dayjs(state.startMonth))) {
|
||
alert('结束月份不能早于开始月份');
|
||
return;
|
||
}
|
||
|
||
// 请求数据
|
||
const apiData = await fetchData(state.startMonth, state.endMonth);
|
||
if (apiData && apiData.code === 200 && apiData.data) {
|
||
novData = processApiData(apiData);
|
||
state.novData = novData;
|
||
|
||
// 重新生成趋势数据
|
||
const trendBase = novData.filter(b => b.调度 !== '全部' && b.调度 !== '技术部测试');
|
||
trendData = genTrend(trendBase);
|
||
state.trendData = trendData;
|
||
|
||
// 重新初始化客服列表
|
||
initCtrl();
|
||
renderAll();
|
||
} else {
|
||
alert('获取数据失败,请检查网络连接或联系管理员');
|
||
}
|
||
});
|
||
|
||
// 开始月份变化事件
|
||
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;
|
||
}
|
||
});
|
||
|
||
// 结束月份变化事件
|
||
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;
|
||
}
|
||
});
|
||
|
||
staffSel.addEventListener('change', e => {
|
||
const selectedValue = e.target.value;
|
||
if (selectedValue === '') {
|
||
// 选择全部
|
||
state.staff = [];
|
||
} else {
|
||
// 选择单个客服
|
||
state.staff = [selectedValue];
|
||
}
|
||
// 当从下拉框选择客服时,清空柱状图点击选择的客服
|
||
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();
|
||
});
|
||
}
|
||
*/
|
||
|
||
// 柱状图指标切换
|
||
const barChartIndexSel = document.getElementById('barChartIndex');
|
||
if (barChartIndexSel) {
|
||
barChartIndexSel.value = state.barChartIndex || '完成率';
|
||
barChartIndexSel.addEventListener('change', e => {
|
||
state.barChartIndex = e.target.value;
|
||
updateBar();
|
||
});
|
||
}
|
||
|
||
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');
|
||
|
||
// 清空现有选项
|
||
staffSel.innerHTML = '';
|
||
|
||
const staffs = [...new Set(novData.map(d => d.调度))].filter(s => s !== '全部' && s !== '技术部测试');
|
||
|
||
// 添加"全部"选项
|
||
const allOpt = document.createElement('option');
|
||
allOpt.value = '';
|
||
allOpt.textContent = '全部客服';
|
||
staffSel.appendChild(allOpt);
|
||
|
||
staffs.forEach(s => {
|
||
const opt = document.createElement('option');
|
||
opt.value = s;
|
||
opt.textContent = s;
|
||
staffSel.appendChild(opt);
|
||
});
|
||
|
||
// 初始化选中全部
|
||
state.staff = [];
|
||
}
|
||
|
||
/* 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. 启动 */
|
||
async function init() {
|
||
try {
|
||
console.log('开始初始化页面...');
|
||
|
||
// 先请求接口数据
|
||
const startMonthInp = document.getElementById('startMonth');
|
||
const endMonthInp = document.getElementById('endMonth');
|
||
const apiData = await fetchData(startMonthInp.value, endMonthInp.value);
|
||
|
||
// 如果接口返回数据,替换 novData
|
||
if (apiData && apiData.code === 200 && apiData.data) {
|
||
novData = processApiData(apiData);
|
||
state.novData = novData;
|
||
|
||
// 重新生成趋势数据
|
||
const trendBase = novData.filter(b => b.调度 !== '全部' && b.调度 !== '技术部测试');
|
||
trendData = genTrend(trendBase);
|
||
state.trendData = trendData;
|
||
|
||
console.log('使用接口数据,共', novData.length, '条记录');
|
||
} else {
|
||
console.warn('接口数据获取失败,请检查接口配置');
|
||
// 显示友好提示
|
||
alert('数据加载失败,请检查网络连接或联系管理员');
|
||
}
|
||
|
||
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> |