1427 lines
56 KiB
HTML
1427 lines
56 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="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>
|