visualization_center/index_company.html

1427 lines
56 KiB
HTML
Raw Permalink 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 name="viewport" content="width=device-width, initial-scale=1">
<!-- 依赖 -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.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,.ctrl button{padding:6px 10px;border:1px solid #ddd;border-radius:4px;background:#fff;cursor:pointer;}
.ctrl button:hover{background:#f0f0f0;}
.container{padding:12px;}
.table-wrapper{background:#fff;border-radius:6px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:auto;margin-bottom:12px;}
table{width:100%;border-collapse:collapse;min-width:2000px;}
th,td{padding:10px;text-align:center;border:1px solid #e8e8e8;font-size:13px;}
th{background:#f5f7fa;font-weight:600;position:sticky;top:0;z-index:1;}
tr:hover{background:#f9f9f9;}
.highlight{color:#ff4d4f;font-weight:600;}
.export-btn{padding:4px 12px;background:#3388ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;}
.export-btn:hover{background:#2970d9;}
.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;}
}
</style>
</head>
<body>
<div class="header">
<h2>公司数据可视化中心</h2>
<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="companyFilter" multiple style="height:70px;width:200px;"></select>
</label>
<button id="exportAll">导出全部数据</button>
</div>
<div class="ctrl" style="margin-top:10px;padding:10px;background:#f5f5f5;border-radius:4px;">
<div style="font-weight:bold;margin-bottom:8px;">表头显示控制:</div>
<div id="columnToggles" style="display:flex;flex-wrap:wrap;gap:10px;">
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="订单数" checked> 订单数
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="拒单数" checked> 拒单数
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="催单" checked> 催单
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="派工" checked> 派工
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="到勘" checked> 到勘
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="好评" checked> 好评(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="投诉" checked> 投诉(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="聚合" checked> 聚合(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="抛单" checked> 抛单
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="派单" checked> 派单
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="求助" checked> 求助(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="过审" checked> 过审(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="超时" checked> 超时(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="预约" checked> 预约(率)
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="聚合到勘" checked> 聚合到勘
</label>
<label style="display:flex;align-items:center;cursor:pointer;">
<input type="checkbox" data-column="首联时效" checked> 首联时效
</label>
</div>
</div>
</div>
<div class="container">
<div class="table-wrapper">
<table id="dataTable">
<thead id="tableHead">
<tr id="tableHeadRow">
<!-- 表头将动态生成 -->
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</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="trendIndex" 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="chart7" 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="pieIndex" 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="chart8" class="chart"></div></div>
</div>
</div>
<script>
// 基础数据模板2025年10月的数据
const baseData = [
{
公司名: '啾啾会会员中心',
订单数: {完成: 8572, 总数: 9388},
拒单数: 11,
催单: 60,
派工: 1.22,
到勘: 16.62,
好评: {数量: 3233, : 37.72},
投诉: {数量: 17, : 0.2},
聚合: {数量: 7886, : 84},
抛单: 772,
派单: 730,
求助: {数量: 3086, : 32.87},
过审: {数量: 9176, : 97.74},
超时: {数量: 2141, : 22.81},
预约: {数量: 809, : 8.62},
聚合到勘: 15.67,
首联时效: 0.98,
highlight: [] // 需要标红的数据字段
},
{
公司名: '啾啾应急外协',
订单数: {完成: 477, 总数: 584},
拒单数: 0,
催单: 32,
派工: 20.97,
到勘: 54.77,
好评: {数量: 266, : 55.77},
投诉: {数量: 0, : 0},
聚合: {数量: 47, : 8.05},
抛单: 467,
派单: 70,
求助: {数量: 7, : 1.2},
过审: {数量: 583, : 99.83},
超时: {数量: 7, : 1.2},
预约: {数量: 107, : 18.32},
聚合到勘: 55.65,
首联时效: 5.03,
highlight: ['派工', '到勘', '聚合到勘', '首联时效'] // 标红的字段
},
{
公司名: '宁波远达道路救援有限公司',
订单数: {完成: 204, 总数: 229},
拒单数: 1,
催单: 1,
派工: 1.18,
到勘: 21.65,
好评: {数量: 66, : 32.35},
投诉: {数量: 2, : 0.98},
聚合: {数量: 129, : 56.33},
抛单: 23,
派单: 77,
求助: {数量: 53, : 23.14},
过审: {数量: 225, : 98.25},
超时: {数量: 73, : 31.88},
预约: {数量: 25, : 10.92},
聚合到勘: 19.58,
首联时效: 0.7,
highlight: []
},
{
公司名: '宁波行遇救援联盟',
订单数: {完成: 797, 总数: 858},
拒单数: 0,
催单: 0,
派工: 2.08,
到勘: 20.68,
好评: {数量: 210, : 26.35},
投诉: {数量: 1, : 0.13},
聚合: {数量: 329, : 38.34},
抛单: 213,
派单: 316,
求助: {数量: 152, : 17.72},
过审: {数量: 855, : 99.65},
超时: {数量: 99, : 11.54},
预约: {数量: 60, : 6.99},
聚合到勘: 15.98,
首联时效: 1.23,
highlight: []
},
{
公司名: '慈溪晓兆道路救援有限公司',
订单数: {完成: 322, 总数: 387},
拒单数: 0,
催单: 0,
派工: 2,
到勘: 18.87,
好评: {数量: 108, : 33.54},
投诉: {数量: 1, : 0.31},
聚合: {数量: 153, : 39.53},
抛单: 112,
派单: 122,
求助: {数量: 83, : 21.45},
过审: {数量: 382, : 98.71},
超时: {数量: 43, : 11.11},
预约: {数量: 65, : 16.8},
聚合到勘: 15.43,
首联时效: 0.7,
highlight: []
},
{
公司名: '杭州快易救援服务有限公司',
订单数: {完成: 1250, 总数: 1450},
拒单数: 5,
催单: 25,
派工: 1.85,
到勘: 19.25,
好评: {数量: 485, : 38.8},
投诉: {数量: 8, : 0.64},
聚合: {数量: 580, : 40},
抛单: 195,
派单: 675,
求助: {数量: 290, : 20},
过审: {数量: 1410, : 97.24},
超时: {数量: 320, : 22.07},
预约: {数量: 145, : 10},
聚合到勘: 16.5,
首联时效: 1.1,
highlight: []
},
{
公司名: '上海迅捷道路救援',
订单数: {完成: 2100, 总数: 2350},
拒单数: 8,
催单: 45,
派工: 1.5,
到勘: 17.8,
好评: {数量: 798, : 38},
投诉: {数量: 12, : 0.57},
聚合: {数量: 940, : 40},
抛单: 302,
派单: 1108,
求助: {数量: 470, : 20},
过审: {数量: 2295, : 97.66},
超时: {数量: 535, : 22.77},
预约: {数量: 235, : 10},
聚合到勘: 15.2,
首联时效: 0.95,
highlight: []
},
{
公司名: '深圳通达救援服务',
订单数: {完成: 980, 总数: 1120},
拒单数: 3,
催单: 18,
派工: 1.92,
到勘: 20.15,
好评: {数量: 372, : 37.96},
投诉: {数量: 6, : 0.61},
聚合: {数量: 448, : 40},
抛单: 134,
派单: 538,
求助: {数量: 224, : 20},
过审: {数量: 1092, : 97.5},
超时: {数量: 246, : 21.96},
预约: {数量: 112, : 10},
聚合到勘: 17.8,
首联时效: 1.05,
highlight: []
},
{
公司名: '广州顺达道路救援',
订单数: {完成: 1650, 总数: 1880},
拒单数: 6,
催单: 35,
派工: 1.75,
到勘: 18.5,
好评: {数量: 627, : 38},
投诉: {数量: 10, : 0.61},
聚合: {数量: 752, : 40},
抛单: 228,
派单: 900,
求助: {数量: 376, : 20},
过审: {数量: 1833, : 97.5},
超时: {数量: 413, : 21.97},
预约: {数量: 188, : 10},
聚合到勘: 16.8,
首联时效: 1.0,
highlight: []
},
{
公司名: '成都快捷救援服务',
订单数: {完成: 890, 总数: 1020},
拒单数: 2,
催单: 15,
派工: 2.15,
到勘: 21.2,
好评: {数量: 338, : 37.98},
投诉: {数量: 5, : 0.56},
聚合: {数量: 408, : 40},
抛单: 122,
派单: 490,
求助: {数量: 204, : 20},
过审: {数量: 995, : 97.55},
超时: {数量: 224, : 21.96},
预约: {数量: 102, : 10},
聚合到勘: 18.5,
首联时效: 1.15,
highlight: []
},
{
公司名: '武汉畅通救援联盟',
订单数: {完成: 1120, 总数: 1280},
拒单数: 4,
催单: 22,
派工: 1.88,
到勘: 19.8,
好评: {数量: 426, : 38.04},
投诉: {数量: 7, : 0.63},
聚合: {数量: 512, : 40},
抛单: 150,
派单: 618,
求助: {数量: 256, : 20},
过审: {数量: 1248, : 97.5},
超时: {数量: 281, : 21.95},
预约: {数量: 128, : 10},
聚合到勘: 17.5,
首联时效: 1.08,
highlight: []
},
{
公司名: '南京速达救援服务',
订单数: {完成: 750, 总数: 860},
拒单数: 2,
催单: 12,
派工: 2.05,
到勘: 20.5,
好评: {数量: 285, : 38},
投诉: {数量: 4, : 0.53},
聚合: {数量: 344, : 40},
抛单: 106,
派单: 410,
求助: {数量: 172, : 20},
过审: {数量: 839, : 97.56},
超时: {数量: 189, : 21.98},
预约: {数量: 86, : 10},
聚合到勘: 18.2,
首联时效: 1.12,
highlight: []
},
{
公司名: '天津环城救援服务',
订单数: {完成: 680, 总数: 780},
拒单数: 2,
催单: 10,
派工: 2.1,
到勘: 20.8,
好评: {数量: 258, : 37.94},
投诉: {数量: 4, : 0.59},
聚合: {数量: 312, : 40},
抛单: 98,
派单: 370,
求助: {数量: 156, : 20},
过审: {数量: 761, : 97.56},
超时: {数量: 171, : 21.92},
预约: {数量: 78, : 10},
聚合到勘: 18.5,
首联时效: 1.18,
highlight: []
},
{
公司名: '重庆山城救援服务',
订单数: {完成: 920, 总数: 1050},
拒单数: 3,
催单: 20,
派工: 1.95,
到勘: 19.5,
好评: {数量: 350, : 38.04},
投诉: {数量: 6, : 0.65},
聚合: {数量: 420, : 40},
抛单: 130,
派单: 500,
求助: {数量: 210, : 20},
过审: {数量: 1024, : 97.52},
超时: {数量: 230, : 21.9},
预约: {数量: 105, : 10},
聚合到勘: 17.8,
首联时效: 1.05,
highlight: []
},
{
公司名: '苏州江南救援联盟',
订单数: {完成: 1050, 总数: 1200},
拒单数: 4,
催单: 28,
派工: 1.82,
到勘: 19.2,
好评: {数量: 399, : 38},
投诉: {数量: 7, : 0.67},
聚合: {数量: 480, : 40},
抛单: 150,
派单: 570,
求助: {数量: 240, : 20},
过审: {数量: 1170, : 97.5},
超时: {数量: 263, : 21.92},
预约: {数量: 120, : 10},
聚合到勘: 17.2,
首联时效: 1.02,
highlight: []
},
{
公司名: '西安古城救援服务',
订单数: {完成: 580, 总数: 665},
拒单数: 1,
催单: 8,
派工: 2.2,
到勘: 21.5,
好评: {数量: 220, : 37.93},
投诉: {数量: 3, : 0.52},
聚合: {数量: 266, : 40},
抛单: 85,
派单: 319,
求助: {数量: 133, : 20},
过审: {数量: 649, : 97.59},
超时: {数量: 146, : 21.95},
预约: {数量: 67, : 10.08},
聚合到勘: 19.0,
首联时效: 1.2,
highlight: []
},
{
公司名: '长沙星城救援服务',
订单数: {完成: 720, 总数: 825},
拒单数: 2,
催单: 14,
派工: 2.0,
到勘: 20.0,
好评: {数量: 274, : 38.06},
投诉: {数量: 4, : 0.56},
聚合: {数量: 330, : 40},
抛单: 105,
派单: 390,
求助: {数量: 165, : 20},
过审: {数量: 805, : 97.58},
超时: {数量: 181, : 21.94},
预约: {数量: 83, : 10.06},
聚合到勘: 18.0,
首联时效: 1.1,
highlight: []
},
{
公司名: '郑州中原救援服务',
订单数: {完成: 640, 总数: 735},
拒单数: 2,
催单: 11,
派工: 2.12,
到勘: 20.8,
好评: {数量: 243, : 37.97},
投诉: {数量: 4, : 0.63},
聚合: {数量: 294, : 40},
抛单: 94,
派单: 347,
求助: {数量: 147, : 20},
过审: {数量: 717, : 97.55},
超时: {数量: 161, : 21.9},
预约: {数量: 74, : 10.07},
聚合到勘: 18.8,
首联时效: 1.15,
highlight: []
}
];
// 生成月份范围内的数据
function generateMonthlyData(startMonth, endMonth) {
const allData = [];
// 解析开始和结束月份
const start = new Date(startMonth + '-01');
const end = new Date(endMonth + '-01');
// 生成每个月份的数据
const current = new Date(start);
while (current <= end) {
const year = current.getFullYear();
const month = String(current.getMonth() + 1).padStart(2, '0');
const timeStr = `${year}${month}`; // 格式202510
baseData.forEach(company => {
const variation = 0.8 + Math.random() * 0.4; // 0.8-1.2 的随机变化
const rateVariation = 0.95 + Math.random() * 0.1; // 0.95-1.05 的随机变化
const newData = {
时间: timeStr,
公司名: company.公司名,
订单数: {
完成: Math.round(company.订单数.完成 * variation),
总数: Math.round(company.订单数.总数 * variation)
},
拒单数: Math.round(company.拒单数 * variation),
催单: Math.round(company.催单 * variation),
派工: Math.max(0, company.派工 * (0.9 + Math.random() * 0.2)),
到勘: Math.max(0, company.到勘 * (0.9 + Math.random() * 0.2)),
好评: {
数量: Math.round(company.好评.数量 * variation),
: Math.max(0, Math.min(100, company.好评. * rateVariation))
},
投诉: {
数量: Math.round(company.投诉.数量 * variation),
: Math.max(0, Math.min(100, company.投诉. * rateVariation))
},
聚合: {
数量: Math.round(company.聚合.数量 * variation),
: Math.max(0, Math.min(100, company.聚合. * rateVariation))
},
抛单: Math.round(company.抛单 * variation),
派单: Math.round(company.派单 * variation),
求助: {
数量: Math.round(company.求助.数量 * variation),
: Math.max(0, Math.min(100, company.求助. * rateVariation))
},
过审: {
数量: Math.round(company.过审.数量 * variation),
: Math.max(0, Math.min(100, company.过审. * rateVariation))
},
超时: {
数量: Math.round(company.超时.数量 * variation),
: Math.max(0, Math.min(100, company.超时. * rateVariation))
},
预约: {
数量: Math.round(company.预约.数量 * variation),
: Math.max(0, Math.min(100, company.预约. * rateVariation))
},
聚合到勘: Math.max(0, company.聚合到勘 * (0.9 + Math.random() * 0.2)),
首联时效: Math.max(0, company.首联时效 * (0.9 + Math.random() * 0.2)),
highlight: company.highlight ? [...company.highlight] : []
};
allData.push(newData);
});
// 移动到下一个月
current.setMonth(current.getMonth() + 1);
}
return allData;
}
// 初始化数据
let rawData = generateMonthlyData('2025-01', '2025-11');
// 列配置:定义所有列的显示名称和顺序
const columnConfig = [
{ key: '时间', label: '时间', alwaysShow: true },
{ key: '公司名', label: '公司名', alwaysShow: true },
{ key: '订单数', label: '订单数', alwaysShow: false },
{ key: '拒单数', label: '拒单数', alwaysShow: false },
{ key: '催单', label: '催单', alwaysShow: false },
{ key: '派工', label: '派工', alwaysShow: false },
{ key: '到勘', label: '到勘', alwaysShow: false },
{ key: '好评', label: '好评(率)', alwaysShow: false },
{ key: '投诉', label: '投诉(率)', alwaysShow: false },
{ key: '聚合', label: '聚合(率)', alwaysShow: false },
{ key: '抛单', label: '抛单', alwaysShow: false },
{ key: '派单', label: '派单', alwaysShow: false },
{ key: '求助', label: '求助(率)', alwaysShow: false },
{ key: '过审', label: '过审(率)', alwaysShow: false },
{ key: '超时', label: '超时(率)', alwaysShow: false },
{ key: '预约', label: '预约(率)', alwaysShow: false },
{ key: '聚合到勘', label: '聚合到勘', alwaysShow: false },
{ key: '首联时效', label: '首联时效', alwaysShow: false },
{ key: '导出', label: '导出', alwaysShow: true }
];
// 全局状态
const state = {
filteredData: rawData,
selectedCompanies: [],
startMonth: '2025-01',
endMonth: '2025-11',
trendIndex: '订单数',
pieIndex: '订单数'
};
// ECharts 实例
let chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8;
// 通用配色
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');
if (chart1Dom) chart1 = echarts.init(chart1Dom);
if (chart2Dom) chart2 = echarts.init(chart2Dom);
if (chart3Dom) chart3 = echarts.init(chart3Dom);
if (chart4Dom) chart4 = echarts.init(chart4Dom);
if (chart5Dom) chart5 = echarts.init(chart5Dom);
if (chart6Dom) chart6 = echarts.init(chart6Dom);
const chart7Dom = document.getElementById('chart7');
const chart8Dom = document.getElementById('chart8');
if (chart7Dom) chart7 = echarts.init(chart7Dom);
if (chart8Dom) chart8 = echarts.init(chart8Dom);
console.log('图表初始化成功');
} catch (error) {
console.error('图表初始化失败:', error);
}
}
// 渲染表格
function renderTable() {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
const columnVisibility = getColumnVisibility();
// 判断字段是否需要标红
const isHighlight = (field, row) => row.highlight && row.highlight.includes(field);
// 获取单元格内容
const getCellContent = (column, row) => {
switch(column) {
case '时间': return row.时间;
case '公司名': return row.公司名;
case '订单数': return `${row.订单数.完成}/${row.订单数.总数}`;
case '拒单数': return `<span class="${isHighlight('拒单数', row) ? 'highlight' : ''}">${row.拒单数}</span>`;
case '催单': return `<span class="${isHighlight('催单', row) ? 'highlight' : ''}">${row.催单}</span>`;
case '派工': return `<span class="${isHighlight('派工', row) ? 'highlight' : ''}">${row.派工}</span>`;
case '到勘': return `<span class="${isHighlight('到勘', row) ? 'highlight' : ''}">${row.到勘}</span>`;
case '好评': return `${row.好评.数量}(${row.好评.}%)`;
case '投诉': return `${row.投诉.数量}(${row.投诉.}%)`;
case '聚合': return `${row.聚合.数量}(${row.聚合.}%)`;
case '抛单': return row.抛单;
case '派单': return row.派单;
case '求助': return `${row.求助.数量}(${row.求助.}%)`;
case '过审': return `${row.过审.数量}(${row.过审.}%)`;
case '超时': return `${row.超时.数量}(${row.超时.}%)`;
case '预约': return `${row.预约.数量}(${row.预约.}%)`;
case '聚合到勘': return `<span class="${isHighlight('聚合到勘', row) ? 'highlight' : ''}">${row.聚合到勘}</span>`;
case '首联时效': return `<span class="${isHighlight('首联时效', row) ? 'highlight' : ''}">${row.首联时效}</span>`;
case '导出': return `<button class="export-btn" onclick="exportCompany('${row.公司名}', '${row.时间}')">导出</button>`;
default: return '';
}
};
state.filteredData.forEach((row, idx) => {
const tr = document.createElement('tr');
// 根据列配置生成单元格
const cells = columnConfig.map(col => {
// 根据列显示状态决定是否显示
const isVisible = columnVisibility[col.key] === true;
const content = getCellContent(col.key, row);
return `<td data-column="${col.key}" style="display:${isVisible ? '' : 'none'}">${content}</td>`;
}).join('');
tr.innerHTML = cells;
tbody.appendChild(tr);
});
}
// 获取列显示状态
function getColumnVisibility() {
const checkboxes = document.querySelectorAll('#columnToggles input[type="checkbox"]');
const columnVisibility = {};
// 先初始化所有列的显示状态(默认都显示)
columnConfig.forEach(col => {
if (col.alwaysShow) {
// 始终显示的列
columnVisibility[col.key] = true;
} else {
// 可控制的列,默认显示
columnVisibility[col.key] = true;
}
});
// 根据复选框状态更新可控制的列
checkboxes.forEach(cb => {
const column = cb.getAttribute('data-column');
if (column && !columnConfig.find(col => col.key === column)?.alwaysShow) {
columnVisibility[column] = cb.checked;
}
});
return columnVisibility;
}
// 渲染表头
function renderTableHeader() {
const theadRow = document.getElementById('tableHeadRow');
if (!theadRow) return;
const columnVisibility = getColumnVisibility();
// 根据列配置和显示状态生成表头
theadRow.innerHTML = columnConfig.map(col => {
// 根据列显示状态决定是否显示
const isVisible = columnVisibility[col.key] === true;
return `<th data-column="${col.key}" style="display:${isVisible ? '' : 'none'}">${col.label}</th>`;
}).join('');
}
// 切换列显示/隐藏
function toggleColumns() {
const columnVisibility = getColumnVisibility();
// 更新表头
const ths = document.querySelectorAll('#dataTable thead th');
ths.forEach(th => {
const column = th.getAttribute('data-column');
if (column && columnVisibility.hasOwnProperty(column)) {
th.style.display = columnVisibility[column] === true ? '' : 'none';
}
});
// 更新表格内容
const tds = document.querySelectorAll('#dataTable tbody td');
tds.forEach(td => {
const column = td.getAttribute('data-column');
if (column && columnVisibility.hasOwnProperty(column)) {
td.style.display = columnVisibility[column] === true ? '' : 'none';
}
});
}
// 更新图表
function updateCharts() {
updateChart1(); // 订单数对比
updateChart2(); // 好评率对比
updateChart3(); // 投诉率对比
updateChart4(); // 聚合率对比
updateChart5(); // 过审率对比
updateChart6(); // 超时率对比
updateChart7(); // 月度趋势
updateChart8(); // 饼图
}
function updateChart1() {
if (!chart1) return;
// 按公司和时间聚合数据
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {完成: 0, 总数: 0};
}
companyData[d.公司名].完成 += d.订单数.完成;
companyData[d.公司名].总数 += d.订单数.总数;
});
const companies = Object.keys(companyData);
const option = {
color: colors[0],
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: companies, axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '订单数' },
series: [{
name: '完成订单',
type: 'bar',
data: companies.map(c => companyData[c].完成)
}, {
name: '总订单',
type: 'bar',
data: companies.map(c => companyData[c].总数)
}]
};
chart1.setOption(option, true);
}
function updateChart2() {
if (!chart2) return;
// 按公司聚合数据,计算平均好评率
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {总数: 0, 好评数: 0};
}
companyData[d.公司名].总数 += d.订单数.总数;
companyData[d.公司名].好评数 += d.好评.数量;
});
const companies = Object.keys(companyData);
const option = {
color: colors[1],
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
xAxis: { type: 'category', data: companies, axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '好评率(%)' },
series: [{
name: '好评率',
type: 'bar',
data: companies.map(c => {
const rate = companyData[c].总数 > 0 ? (companyData[c].好评数 / companyData[c].总数 * 100) : 0;
return parseFloat(rate.toFixed(2));
})
}]
};
chart2.setOption(option, true);
}
function updateChart3() {
if (!chart3) return;
// 按公司聚合数据,计算平均投诉率
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {总数: 0, 投诉数: 0};
}
companyData[d.公司名].总数 += d.订单数.总数;
companyData[d.公司名].投诉数 += d.投诉.数量;
});
const companies = Object.keys(companyData);
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: companies, axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '投诉率(%)' },
series: [{
name: '投诉率',
type: 'bar',
data: companies.map(c => {
const rate = companyData[c].总数 > 0 ? (companyData[c].投诉数 / companyData[c].总数 * 100) : 0;
return parseFloat(rate.toFixed(2));
})
}]
};
chart3.setOption(option, true);
}
function updateChart4() {
if (!chart4) return;
const data = state.filteredData;
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: data.map(d => d.公司名), axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '聚合率(%)' },
series: [{
name: '聚合率',
type: 'bar',
data: data.map(d => d.聚合.)
}]
};
chart4.setOption(option, true);
}
function updateChart5() {
if (!chart5) return;
// 按公司聚合数据,计算平均过审率
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {总数: 0, 过审数: 0};
}
companyData[d.公司名].总数 += d.订单数.总数;
companyData[d.公司名].过审数 += d.过审.数量;
});
const companies = Object.keys(companyData);
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: companies, axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '过审率(%)' },
series: [{
name: '过审率',
type: 'bar',
data: companies.map(c => {
const rate = companyData[c].总数 > 0 ? (companyData[c].过审数 / companyData[c].总数 * 100) : 0;
return parseFloat(rate.toFixed(2));
})
}]
};
chart5.setOption(option, true);
}
function updateChart6() {
if (!chart6) return;
// 按公司聚合数据,计算平均超时率
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {总数: 0, 超时数: 0};
}
companyData[d.公司名].总数 += d.订单数.总数;
companyData[d.公司名].超时数 += d.超时.数量;
});
const companies = Object.keys(companyData);
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: companies, axisLabel: { rotate: 45 } },
yAxis: { type: 'value', name: '超时率(%)' },
series: [{
name: '超时率',
type: 'bar',
data: companies.map(c => {
const rate = companyData[c].总数 > 0 ? (companyData[c].超时数 / companyData[c].总数 * 100) : 0;
return parseFloat(rate.toFixed(2));
})
}]
};
chart6.setOption(option, true);
}
// 图表7月度趋势折线图
function updateChart7() {
if (!chart7) return;
const trendIndex = state.trendIndex || '订单数';
const selectedCompanies = state.selectedCompanies.length > 0 ? state.selectedCompanies : [...new Set(state.filteredData.map(d => d.公司名))];
// 按月份聚合数据
const monthData = {};
state.filteredData.forEach(d => {
if (!selectedCompanies.includes(d.公司名)) return;
if (!monthData[d.时间]) {
monthData[d.时间] = {};
}
if (!monthData[d.时间][d.公司名]) {
monthData[d.时间][d.公司名] = {
订单数: 0,
好评率: 0,
投诉率: 0,
聚合率: 0,
过审率: 0,
超时率: 0,
count: 0
};
}
const companyMonth = monthData[d.时间][d.公司名];
companyMonth.订单数 += d.订单数.完成;
companyMonth.好评率 += d.好评.;
companyMonth.投诉率 += d.投诉.;
companyMonth.聚合率 += d.聚合.;
companyMonth.过审率 += d.过审.;
companyMonth.超时率 += d.超时.;
companyMonth.count += 1;
});
// 计算平均值
Object.keys(monthData).forEach(time => {
Object.keys(monthData[time]).forEach(company => {
const data = monthData[time][company];
if (data.count > 0) {
data.好评率 = data.好评率 / data.count;
data.投诉率 = data.投诉率 / data.count;
data.聚合率 = data.聚合率 / data.count;
data.过审率 = data.过审率 / data.count;
data.超时率 = data.超时率 / data.count;
}
});
});
const months = Object.keys(monthData).sort();
// 为每个公司创建一条折线
const series = selectedCompanies.map((company, idx) => {
const data = months.map(month => {
const companyData = monthData[month] && monthData[month][company];
if (!companyData) return null;
if (trendIndex === '订单数') {
return companyData.订单数;
} else if (trendIndex === '好评率') {
return parseFloat(companyData.好评率.toFixed(2));
} else if (trendIndex === '投诉率') {
return parseFloat(companyData.投诉率.toFixed(2));
} else if (trendIndex === '聚合率') {
return parseFloat(companyData.聚合率.toFixed(2));
} else if (trendIndex === '过审率') {
return parseFloat(companyData.过审率.toFixed(2));
} else if (trendIndex === '超时率') {
return parseFloat(companyData.超时率.toFixed(2));
}
return null;
});
return {
name: company,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { width: 3 },
data: data
};
});
const option = {
color: colors,
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
legend: {
type: 'scroll',
top: 10,
data: selectedCompanies,
height: 100
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
data: months.map(m => {
// 格式化月份显示202510 -> 2025-10
if (m.length === 6) {
return m.substring(0, 4) + '-' + m.substring(4);
}
return m;
}),
axisLabel: { rotate: 45 }
},
yAxis: {
type: 'value',
name: trendIndex + (trendIndex.includes('率') ? '(%)' : '')
},
series: series
};
chart7.setOption(option, true);
}
// 图表8饼图
function updateChart8() {
if (!chart8) return;
const pieIndex = state.pieIndex || '订单数';
// 按公司聚合数据
const companyData = {};
state.filteredData.forEach(d => {
if (!companyData[d.公司名]) {
companyData[d.公司名] = {
订单数: 0,
好评率: 0,
投诉率: 0,
聚合率: 0,
过审率: 0,
超时率: 0,
count: 0
};
}
companyData[d.公司名].订单数 += d.订单数.完成;
companyData[d.公司名].好评率 += d.好评.;
companyData[d.公司名].投诉率 += d.投诉.;
companyData[d.公司名].聚合率 += d.聚合.;
companyData[d.公司名].过审率 += d.过审.;
companyData[d.公司名].超时率 += d.超时.;
companyData[d.公司名].count += 1;
});
// 计算平均值
Object.keys(companyData).forEach(company => {
const data = companyData[company];
if (data.count > 0) {
data.好评率 = data.好评率 / data.count;
data.投诉率 = data.投诉率 / data.count;
data.聚合率 = data.聚合率 / data.count;
data.过审率 = data.过审率 / data.count;
data.超时率 = data.超时率 / data.count;
}
});
// 准备饼图数据
const pieData = Object.keys(companyData).map(company => {
let value = 0;
if (pieIndex === '订单数') {
value = companyData[company].订单数;
} else if (pieIndex === '好评率') {
value = parseFloat(companyData[company].好评率.toFixed(2));
} else if (pieIndex === '投诉率') {
value = parseFloat(companyData[company].投诉率.toFixed(2));
} else if (pieIndex === '聚合率') {
value = parseFloat(companyData[company].聚合率.toFixed(2));
} else if (pieIndex === '过审率') {
value = parseFloat(companyData[company].过审率.toFixed(2));
} else if (pieIndex === '超时率') {
value = parseFloat(companyData[company].超时率.toFixed(2));
}
return {
name: company,
value: value
};
}).filter(d => d.value > 0).sort((a, b) => b.value - a.value);
const option = {
color: colors,
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}' + (pieIndex.includes('率') ? '%' : '') + ' ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
left: 'left',
top: 20,
height: 300
},
series: [{
name: pieIndex,
type: 'pie',
radius: ['40%', '70%'],
center: ['55%', '50%'],
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0,0,0,.5)'
}
},
label: {
show: true,
formatter: '{b}\n{c}' + (pieIndex.includes('率') ? '%' : '')
}
}]
};
chart8.setOption(option, true);
}
// 导出单个公司数据
function exportCompany(companyName, time) {
const company = state.filteredData.find(d => d.公司名 === companyName && d.时间 === time);
if (!company) return;
const csv = [
['字段', '值'],
['时间', company.时间],
['公司名', company.公司名],
['订单数(完成/总数)', `${company.订单数.完成}/${company.订单数.总数}`],
['拒单数', company.拒单数],
['催单', company.催单],
['派工', company.派工],
['到勘', company.到勘],
['好评(数量/率)', `${company.好评.数量}(${company.好评.}%)`],
['投诉(数量/率)', `${company.投诉.数量}(${company.投诉.}%)`],
['聚合(数量/率)', `${company.聚合.数量}(${company.聚合.}%)`],
['抛单', company.抛单],
['派单', company.派单],
['求助(数量/率)', `${company.求助.数量}(${company.求助.}%)`],
['过审(数量/率)', `${company.过审.数量}(${company.过审.}%)`],
['超时(数量/率)', `${company.超时.数量}(${company.超时.}%)`],
['预约(数量/率)', `${company.预约.数量}(${company.预约.}%)`],
['聚合到勘', company.聚合到勘],
['首联时效', company.首联时效]
].map(row => row.join(',')).join('\n');
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${companyName}_${time}.csv`;
link.click();
}
// 导出全部数据
function exportAllData() {
const headers = ['时间', '公司名', '订单数(完成/总数)', '拒单数', '催单', '派工', '到勘', '好评(数量/率)', '投诉(数量/率)', '聚合(数量/率)', '抛单', '派单', '求助(数量/率)', '过审(数量/率)', '超时(数量/率)', '预约(数量/率)', '聚合到勘', '首联时效'];
const rows = state.filteredData.map(d => [
d.时间,
d.公司名,
`${d.订单数.完成}/${d.订单数.总数}`,
d.拒单数,
d.催单,
d.派工,
d.到勘,
`${d.好评.数量}(${d.好评.}%)`,
`${d.投诉.数量}(${d.投诉.}%)`,
`${d.聚合.数量}(${d.聚合.}%)`,
d.抛单,
d.派单,
`${d.求助.数量}(${d.求助.}%)`,
`${d.过审.数量}(${d.过审.}%)`,
`${d.超时.数量}(${d.超时.}%)`,
`${d.预约.数量}(${d.预约.}%)`,
d.聚合到勘,
d.首联时效
]);
const csv = [headers.join(','), ...rows.map(row => row.join(','))].join('\n');
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `公司数据_${new Date().toISOString().split('T')[0]}.csv`;
link.click();
}
// 过滤数据
function filterData() {
const startMonth = document.getElementById('startMonth').value;
const endMonth = document.getElementById('endMonth').value;
const companyFilter = document.getElementById('companyFilter');
const selectedCompanies = [...companyFilter.selectedOptions].map(o => o.value);
// 确保结束月份不早于开始月份
if (startMonth && endMonth && startMonth > endMonth) {
document.getElementById('endMonth').value = startMonth;
return filterData();
}
// 重新生成数据(如果时间范围改变)
const currentStart = state.startMonth || '2025-01';
const currentEnd = state.endMonth || '2025-11';
if (startMonth !== currentStart || endMonth !== currentEnd) {
rawData = generateMonthlyData(startMonth || '2025-01', endMonth || '2025-11');
state.startMonth = startMonth;
state.endMonth = endMonth;
}
// 解析月份范围
const startTime = startMonth ? startMonth.replace('-', '') : '';
const endTime = endMonth ? endMonth.replace('-', '') : '';
state.filteredData = rawData.filter(d => {
const timeMatch = (!startTime && !endTime) ||
(d.时间 >= startTime && d.时间 <= endTime);
const companyMatch = selectedCompanies.length === 0 || selectedCompanies.includes(d.公司名);
return timeMatch && companyMatch;
});
renderTable();
updateCharts();
}
// 初始化控件
function initCtrl() {
const companyFilter = document.getElementById('companyFilter');
const startMonthInp = document.getElementById('startMonth');
const endMonthInp = document.getElementById('endMonth');
// 初始化公司列表
const companies = [...new Set(baseData.map(d => d.公司名))];
companies.forEach(company => {
const opt = document.createElement('option');
opt.value = company;
opt.textContent = company;
opt.selected = true;
companyFilter.appendChild(opt);
});
state.selectedCompanies = companies;
// 初始化月份
state.startMonth = startMonthInp.value;
state.endMonth = endMonthInp.value;
// 确保所有复选框默认都是选中状态
const columnToggles = document.querySelectorAll('#columnToggles input[type="checkbox"]');
columnToggles.forEach(cb => {
cb.checked = true; // 默认全部选中
cb.addEventListener('change', () => {
// 取消勾选时隐藏,勾选时显示
toggleColumns(); // 更新显示/隐藏
});
});
// 初始化表头(所有列都显示)
renderTableHeader();
// 绑定事件
startMonthInp.addEventListener('change', filterData);
endMonthInp.addEventListener('change', filterData);
companyFilter.addEventListener('change', () => {
state.selectedCompanies = [...companyFilter.selectedOptions].map(o => o.value);
filterData();
});
document.getElementById('exportAll').addEventListener('click', exportAllData);
// 趋势图指标切换
const trendIndexSel = document.getElementById('trendIndex');
if (trendIndexSel) {
trendIndexSel.value = state.trendIndex || '订单数';
trendIndexSel.addEventListener('change', e => {
state.trendIndex = e.target.value;
updateChart7();
});
}
// 饼图指标切换
const pieIndexSel = document.getElementById('pieIndex');
if (pieIndexSel) {
pieIndexSel.value = state.pieIndex || '订单数';
pieIndexSel.addEventListener('change', e => {
state.pieIndex = e.target.value;
updateChart8();
});
}
}
// 响应式
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 (chart7) chart7.resize();
if (chart8) chart8.resize();
}
window.addEventListener('resize', handleResize);
// 启动
function init() {
try {
console.log('开始初始化页面...');
initCtrl();
initCharts();
renderTable();
updateCharts();
console.log('页面初始化完成');
} catch (error) {
console.error('页面初始化失败:', error);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 100);
}
</script>
</body>
</html>