初步完成可视化页面

master
songxiangjie 2025-12-11 16:45:47 +08:00
parent fc56f98c70
commit 5ea806182c
2 changed files with 137 additions and 195 deletions

View File

@ -85,16 +85,7 @@
<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="card-title">客服完成率分布</div>
<div class="chart-container"><div id="pie" class="chart"></div></div>
</div>
<div class="card">
@ -102,7 +93,17 @@
<div class="chart-container"><div id="line" class="chart"></div></div>
</div>
<div class="card">
<div class="card-title">客服多指标对比</div>
<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">
@ -203,152 +204,29 @@
}
/*****************************************************************
* 1. 原始数据明细 + 2025年1月-10月随机数据
* 1. 初始化空数据
*****************************************************************/
// 基础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=[];
// 如果没有数据,返回空数组
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('2025-11-01').add(d-1,'day');
base.forEach(b=>{
const date=dayjs(latestMonth + '-01').add(d-1,'day');
latestData.forEach(b=>{
arr.push({
日期:date.format('YYYY-MM-DD'),
客服:b.调度,
@ -363,18 +241,21 @@
}
return arr;
}
const trendData = genTrend(novData.filter(b=>b.调度!=='全部' && b.调度!=='技术部测试'));
// 初始化为空数组
let trendData = [];
/* 全局状态 */
const state = {
month:'2025-11',
month: '',
staff: [],
index: '完成率',
trendData,
novData,
trendData: [],
novData: [],
drillName: null,
selectedStaffs: [], // 从柱状图点击选中的客服列表
pieChartIndex: '完成率' // 饼图/柱状图切换的指标
pieChartIndex: '完成率', // 饼图/柱状图切换的指标
barChartIndex: '完成率' // 柱状图切换的指标
};
/* ECharts 实例 */
@ -466,13 +347,13 @@
Object.keys(monthData).forEach(month => {
const m = monthData[month];
if (staffs.length === 0 || staffs.length > 1) {
// 全部客服:动态计算百分比
// 全部客服:动态计算百分比保留2位小数
if (m.订单总数 > 0) {
// 完成率 = (完成数 + 空驶数) / 订单总数 * 100
m.完成率 = ((m.完成数 + m.空驶数) / m.订单总数 * 100);
m.投诉率 = (m.投诉数 / m.订单总数 * 100);
m.空驶率 = (m.空驶数 / m.订单总数 * 100);
m.关闭率 = (m.关闭数 / m.订单总数 * 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;
@ -480,6 +361,8 @@
m.关闭率 = 0;
}
}
// 溢价金额也保留2位小数
m.溢价金额 = parseFloat(m.溢价金额.toFixed(2));
});
return Object.values(monthData).sort((a, b) => a.月份.localeCompare(b.月份));
@ -795,18 +678,48 @@
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 => d.调度 !== '全部' && d.调度 !== '技术部测试' && d.月份 === latestMonth)
.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: (value !== undefined && value !== null && !isNaN(value)) ? value : 0
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 = {
@ -887,23 +800,27 @@
}
}
/* 2. 折线 - 月度趋势(按日 */
/* 2. 折线 - 月度趋势(使用真实月度数据 */
function updateLine(){
if (!line) {
console.warn('折线图实例未初始化');
return;
}
const {trendData, staff, index} = state;
if (!trendData || trendData.length === 0) {
const {novData, staff, index} = state;
// 过滤掉 '全部' 和 '测试' 账号
const validData = novData.filter(d => !d.调度.includes('全部') && !d.调度.includes('测试'));
if (!validData || validData.length === 0) {
console.warn('趋势数据为空');
return;
}
const days = [...new Set(trendData.map(d => d.日期))].sort();
const months = [...new Set(validData.map(d => d.月份))].sort();
// 如果没有选择客服,显示所有客服
const staffsToShow = staff.length > 0 ? staff : [...new Set(trendData.map(d => d.客服))];
const staffsToShow = staff.length > 0 ? staff : [...new Set(validData.map(d => d.调度))];
const series = staffsToShow.map((s, idx) => ({
name: s,
@ -912,8 +829,8 @@
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
data: days.map(day => {
const item = trendData.find(d => d.日期 === day && d.客服 === s);
data: months.map(month => {
const item = validData.find(d => d.月份 === month && d.调度 === s);
return item ? Math.max(0, item[index]) : 0;
})
}));
@ -937,7 +854,7 @@
},
xAxis: {
type: 'category',
data: days,
data: months,
boundaryGap: false,
axisLabel: {
rotate: 45
@ -960,14 +877,21 @@
return;
}
const {novData, staff, index} = state;
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 =>
staff.length ? staff.includes(d.调度) : (d.调度 !== '全部' && d.调度 !== '技术部测试')
d.月份 === latestMonth &&
d.调度 !== '全部' && !d.调度.includes('测试')
);
const barData = filterData.map(d => {
@ -1001,10 +925,10 @@
},
yAxis: {
type: 'value',
name: index + (state.index.includes('率') ? '(%)' : '')
name: index + (index.includes('率') ? '(%)' : '')
},
series: [{
name: state.index,
name: index,
type: 'bar',
data: barData.map(d => d.value),
barMaxWidth: 40
@ -1022,10 +946,10 @@
if (!state.selectedStaffs) {
state.selectedStaffs = [];
}
const index = state.selectedStaffs.indexOf(name);
if (index > -1) {
const idx = state.selectedStaffs.indexOf(name);
if (idx > -1) {
// 已选中,取消选择
state.selectedStaffs.splice(index, 1);
state.selectedStaffs.splice(idx, 1);
} else {
// 未选中,添加选择
state.selectedStaffs.push(name);
@ -1040,7 +964,7 @@
});
}
/* 4. 历史 - 单个或多个客服趋势 */
/* 4. 历史 - 单个或多个客服趋势(使用真实月度数据) */
function updateHist(){
if (!hist) {
console.warn('历史图表实例未初始化');
@ -1064,24 +988,24 @@
}
// 最后使用默认客服
else {
const defaultStaff = state.novData.find(d => d.调度 !== '全部' && d.调度 !== '技术部测试')?.调度 || '陈航明';
const defaultStaff = state.novData.find(d => !d.调度.includes('全部') && !d.调度.includes('测试'))?.调度 || '陈航明';
staffsToShow = [defaultStaff];
}
// 获取所有选中客服的数据
const allData = state.trendData.filter(d => staffsToShow.includes(d.客服));
// 获取所有选中客服的数据(使用 novData
const allData = state.novData.filter(d => staffsToShow.includes(d.调度));
if (!allData || allData.length === 0) {
console.warn('没有找到客服数据:', staffsToShow);
return;
}
// 获取所有日期
const days = [...new Set(allData.map(d => d.日期))].sort();
// 获取所有月份
const months = [...new Set(allData.map(d => d.月份))].sort();
// 为每个客服创建一条折线
const series = staffsToShow.map((staffName, idx) => {
const staffData = allData.filter(d => d.客服 === staffName);
const staffData = allData.filter(d => d.调度 === staffName);
return {
name: staffName,
type: 'line',
@ -1089,8 +1013,8 @@
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
data: days.map(day => {
const item = staffData.find(d => d.日期 === day);
data: months.map(month => {
const item = staffData.find(d => d.月份 === month);
return item ? Math.max(0, item[state.index]) : null;
})
};
@ -1132,7 +1056,7 @@
},
xAxis: {
type: 'category',
data: days,
data: months,
boundaryGap: false,
axisLabel: {
rotate: 45
@ -1181,11 +1105,14 @@
// 重新生成趋势数据
const trendBase = novData.filter(b => b.调度 !== '全部' && b.调度 !== '技术部测试');
state.trendData = genTrend(trendBase);
trendData = genTrend(trendBase);
state.trendData = trendData;
// 重新初始化客服列表
initCtrl();
renderAll();
} else {
alert('获取数据失败,请检查网络连接或联系管理员');
}
});
@ -1229,7 +1156,8 @@
renderAll();
});
// 饼图指标切换
// 饼图指标切换(已移除,改为柱状图指标切换)
/*
const pieChartIndexSel = document.getElementById('pieChartIndex');
if (pieChartIndexSel) {
pieChartIndexSel.value = state.pieChartIndex || '完成率';
@ -1238,6 +1166,17 @@
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) {
@ -1323,11 +1262,14 @@
// 重新生成趋势数据
const trendBase = novData.filter(b => b.调度 !== '全部' && b.调度 !== '技术部测试');
state.trendData = genTrend(trendBase);
trendData = genTrend(trendBase);
state.trendData = trendData;
console.log('使用接口数据,共', novData.length, '条记录');
} else {
console.warn('接口数据异常,使用本地默认数据');
console.warn('接口数据获取失败,请检查接口配置');
// 显示友好提示
alert('数据加载失败,请检查网络连接或联系管理员');
}
initCtrl();