初步完成可视化页面

master
songxiangjie 2025-12-12 16:17:42 +08:00
parent 5ea806182c
commit 919dcabafa
2 changed files with 843 additions and 1026 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,31 +3,109 @@
<head>
<meta charset="UTF-8">
<title>客服数据可视化中心</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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="./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;}
: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;
}
.loading{display:flex;justify-content:center;align-items:center;height:100%;color:#999;}
</style>
</head>
<body>
@ -36,10 +114,10 @@
<a href="index_company.html" target="_blank">公司数据可视化中心</a>
<div class="ctrl">
<label>开始月份
<input type="month" id="startMonth" value="2025-01">
<input id="startMonth" type="month" value="2025-01">
</label>
<label>结束月份
<input type="month" id="endMonth" value="2025-11">
<input id="endMonth" type="month" value="2025-11">
</label>
<button id="searchBtn">搜索</button>
<label>客服
@ -62,35 +140,51 @@
<div class="grid">
<div class="card">
<div class="card-title">订单数量(多指标对比)</div>
<div class="chart-container"><div id="chart1" class="chart"></div></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 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 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 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 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 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 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 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;">
@ -104,11 +198,15 @@
<option value="关闭率">关闭率</option>
</select>
</div>
<div class="chart-container"><div id="bar" class="chart"></div></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 class="chart-container">
<div id="hist" class="chart"></div>
</div>
</div>
</div>
<script>
@ -153,11 +251,11 @@
}
const processedData = [];
// 遍历每个客服
Object.keys(apiData.data).forEach(staffName => {
const staffMonthData = apiData.data[staffName];
// 遍历该客服的每个月份数据
staffMonthData.forEach(monthData => {
const row = {
@ -206,42 +304,42 @@
/*****************************************************************
* 1. 初始化空数据
*****************************************************************/
// 初始化为空数组,等待接口数据
// 初始化为空数组,等待接口数据
let novData = [];
/* 生成趋势数据(基于真实月度数据模拟日度数据) */
function genTrend(base){
const arr=[];
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=>{
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
日期: 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 = [];
@ -263,7 +361,7 @@
let chart1, chart2, chart3, chart4, chart5, chart6; // 时间序列图表
/* 通用配色 */
const colors = ['#3388ff','#00c362','#ff9f00','#ff4d4f','#9a6bfd','#36cfc9','#ff7ec2','#ffda5d'];
const colors = ['#3388ff', '#00c362', '#ff9f00', '#ff4d4f', '#9a6bfd', '#36cfc9', '#ff7ec2', '#ffda5d'];
/* 初始化图表 */
function initCharts() {
@ -331,7 +429,7 @@
monthData[month].关闭数 += d.关闭数 || 0;
monthData[month].投诉数 += d.投诉数 || 0;
monthData[month].溢价金额 += d.溢价金额 || 0;
// 如果是单个客服,直接使用百分比;如果是多个客服,稍后计算
if (staffs.length === 1) {
monthData[month].完成率 = d.完成率 || 0;
@ -339,7 +437,7 @@
monthData[month].空驶率 = d.空驶率 || 0;
monthData[month].关闭率 = d.关闭率 || 0;
}
monthData[month].count += 1;
});
@ -383,7 +481,7 @@
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
legend: {
data: ['订单总数', '完成率', '溢价金额', '投诉率', '空驶率', '关闭率'],
@ -399,7 +497,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: [
{
@ -467,7 +565,7 @@
color: colors[0],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
grid: {
left: '3%',
@ -479,7 +577,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: {
type: 'value',
@ -508,7 +606,7 @@
color: colors[2],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
grid: {
left: '3%',
@ -520,7 +618,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: {
type: 'value',
@ -549,7 +647,7 @@
color: colors[3],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
grid: {
left: '3%',
@ -561,7 +659,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: {
type: 'value',
@ -590,7 +688,7 @@
color: colors[4],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
grid: {
left: '3%',
@ -602,7 +700,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: {
type: 'value',
@ -631,7 +729,7 @@
color: colors[5],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' }
axisPointer: {type: 'shadow'}
},
grid: {
left: '3%',
@ -643,7 +741,7 @@
xAxis: {
type: 'category',
data: months,
axisLabel: { rotate: 45 }
axisLabel: {rotate: 45}
},
yAxis: {
type: 'value',
@ -660,7 +758,7 @@
}
/* 1. 饼图 - 客服完成率分布(支持切换指标) */
function updatePie(){
function updatePie() {
if (!pie) {
console.warn('饼图实例未初始化');
return;
@ -763,7 +861,7 @@
color: colors[0],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
axisPointer: {type: 'shadow'},
formatter: '{b}: {c}' + (isRate ? '%' : '')
},
grid: {
@ -801,14 +899,14 @@
}
/* 2. 折线 - 月度趋势(使用真实月度数据) */
function updateLine(){
function updateLine() {
if (!line) {
console.warn('折线图实例未初始化');
return;
}
const {novData, staff, index} = state;
// 过滤掉 '全部' 和 '测试' 账号
const validData = novData.filter(d => !d.调度.includes('全部') && !d.调度.includes('测试'));
@ -828,7 +926,7 @@
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
lineStyle: {width: 3},
data: months.map(month => {
const item = validData.find(d => d.月份 === month && d.调度 === s);
return item ? Math.max(0, item[index]) : 0;
@ -871,7 +969,7 @@
}
/* 3. 柱状 - 客服多指标对比 */
function updateBar(){
function updateBar() {
if (!bar) {
console.warn('柱状图实例未初始化');
return;
@ -965,7 +1063,7 @@
}
/* 4. 历史 - 单个或多个客服趋势(使用真实月度数据) */
function updateHist(){
function updateHist() {
if (!hist) {
console.warn('历史图表实例未初始化');
return;
@ -1012,7 +1110,7 @@
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
lineStyle: {width: 3},
data: months.map(month => {
const item = staffData.find(d => d.月份 === month);
return item ? Math.max(0, item[state.index]) : null;
@ -1032,7 +1130,7 @@
title: {
text: titleText,
left: 'center',
textStyle: { fontSize: 14 }
textStyle: {fontSize: 14}
},
color: colors,
tooltip: {
@ -1073,7 +1171,7 @@
}
/* 5. 控件事件 */
function bindCtrl(){
function bindCtrl() {
const startMonthInp = document.getElementById('startMonth');
const endMonthInp = document.getElementById('endMonth');
const staffSel = document.getElementById('staff');
@ -1090,24 +1188,24 @@
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();
@ -1190,7 +1288,7 @@
}
/* 6. 渲染全部 */
function renderAll(){
function renderAll() {
updateChart1();
updateChart2();
updateChart3();
@ -1204,12 +1302,12 @@
}
/* 7. 初始化控件选项 */
function initCtrl(){
function initCtrl() {
const staffSel = document.getElementById('staff');
// 清空现有选项
staffSel.innerHTML = '';
const staffs = [...new Set(novData.map(d => d.调度))].filter(s => s !== '全部' && s !== '技术部测试');
// 添加"全部"选项
@ -1249,29 +1347,29 @@
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();