visualization_center/index.html

1397 lines
45 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 content="width=device-width, initial-scale=1" name="viewport">
<!-- 依赖 -->
<script src="./js/echarts.min.js"></script>
<script src="./js/dayjs.min.js"></script>
<script src="./js/isoWeek.js"></script>
<script src="./js/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 id="startMonth" type="month" value="2025-01">
</label>
<label>结束月份
<input id="endMonth" type="month" 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>