年度总结页面

This commit is contained in:
2026-02-06 16:51:36 +08:00
parent 83f36b12a8
commit 8e55d61a9e
10 changed files with 165600 additions and 79611 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
{
"name": "data",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "data",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"xlsx": "^0.18.5"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "data",
"version": "1.0.0",
"description": "",
"main": "read-excel.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"xlsx": "^0.18.5"
}
}

View File

@@ -0,0 +1,30 @@
const XLSX = require('xlsx');
const fs = require('fs');
const path = require('path');
// Excel文件路径
const excelPath = 'C:\\Users\\chenhaid\\Documents\\WXWork\\1688858118476511\\Cache\\File\\2026-02\\供应商年度KPIv5.xlsx';
// 输出JSON文件路径
const outputPath = path.join(__dirname, 'kpi.json');
// 读取Excel文件
const workbook = XLSX.readFile(excelPath);
// 获取第一个工作表名称
const sheetName = workbook.SheetNames[0];
console.log('工作表列表:', workbook.SheetNames);
// 获取工作表
const worksheet = workbook.Sheets[sheetName];
// 转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet);
console.log('读取到', jsonData.length, '条数据');
console.log('第一条数据示例:', JSON.stringify(jsonData[0], null, 2));
// 写入JSON文件
fs.writeFileSync(outputPath, JSON.stringify(jsonData, null, 2), 'utf8');
console.log('已保存到:', outputPath);

File diff suppressed because it is too large Load Diff

View File

@@ -151,10 +151,12 @@
.cover-subtitle {
margin-top: 30px;
color: rgba(255, 255, 255, 0.5);
font-size: 1.1em;
color: rgba(255, 255, 255, 0.9);
font-size: 1.8em;
font-weight: bold;
position: relative;
z-index: 1;
text-shadow: 0 0 20px rgba(0, 150, 255, 0.5);
}
.scroll-hint {
@@ -653,8 +655,9 @@
.ending-wish {
margin-top: 40px;
font-size: 1.2em;
color: rgba(255, 255, 255, 0.6);
font-family: 'Orbitron', sans-serif;
font-size: 1.8em;
color: rgba(255, 255, 255, 0.8);
}
/* 导航点 */
@@ -905,6 +908,14 @@
transition: width 1.5s ease;
width: 0%;
}
.imgTopLogo {
width: 50%;
position: fixed;
z-index: 100000;
right: 20px;
top: 20px;
opacity: 0.8;
}
</style>
</head>
@@ -928,6 +939,7 @@
<!-- 主内容 -->
<div id="mainContent" style="display: none;">
<img src="./static/logo_top.png" class="imgTopLogo" alt="">
<!-- 粒子背景 -->
<canvas id="particles"></canvas>
@@ -956,74 +968,62 @@
<!-- 总案件量 -->
<section class="section">
<div class="section-label fade-in">TOTAL CASES</div>
<h2 class="section-title fade-in">这一年,我们共处理了</h2>
<!-- <div class="section-label fade-in">TOTAL CASES</div>-->
<h2 class="section-title fade-in">2025年共处理了</h2>
<div class="big-stat fade-in">
<div class="big-stat-number" id="totalCases">0</div>
<div class="big-stat-unit">个案件</div>
</div>
<div class="donut-container fade-in" id="donutContainer"></div>
<!-- <div class="donut-container fade-in" id="donutContainer"></div>-->
</section>
<!-- 聚合案件 -->
<section class="section">
<div class="section-label fade-in">AGGREGATED CASES</div>
<h2 class="section-title fade-in">年度聚合案件量</h2>
<!-- <div class="section-label fade-in">AGGREGATED CASES</div>-->
<h2 class="section-title fade-in">在线聚合为您带来了</h2>
<div class="time-card fade-in">
<div class="time-icon">📦</div>
<!-- <div class="time-icon">📦</div>-->
<div class="time-value" id="aggregatedCases">0</div>
<div class="big-stat-unit"></div>
<div class="big-stat-unit">聚合量</div>
<div class="time-desc" id="aggregatedDesc">
占总案件量的 <span class="highlight">0%</span><br>
平均每月聚合 <span class="highlight">0</span> 个案件
<!-- 平均每月聚合 <span class="highlight">0</span> 个案件-->
</div>
</div>
</section>
<!-- 案件量TOP3 -->
<section class="section rank-section">
<div class="section-label fade-in">TOP PERFORMERS</div>
<!-- <div class="section-label fade-in">TOP PERFORMERS</div>-->
<h2 class="section-title fade-in">年度案件量 TOP 3 师傅</h2>
<div class="podium fade-in" id="podium"></div>
<p class="fade-in" id="top3Summary" style="margin-top: 40px; color: rgba(255,255,255,0.5);"></p>
<!-- <p class="fade-in" id="top3Summary" style="margin-top: 40px; color: rgba(255,255,255,0.5);"></p>-->
</section>
<!-- 在线时长 -->
<section class="section wave-section">
<div class="section-label fade-in">ONLINE DURATION</div>
<h2 class="section-title fade-in">车辆平均总在线时长</h2>
<h2 class="section-title fade-in">车辆日均在线时长</h2>
<div class="big-stat fade-in">
<div class="big-stat-number" id="avgOnlineHours">0</div>
<div class="big-stat-unit">小时</div>
<div class="big-stat-desc" id="onlineDesc">相当于 <span class="highlight">0</span> 天 · 日均在线 <span class="highlight">0</span> 小时</div>
</div>
<div class="bar-chart fade-in" id="barChart"></div>
</section>
<!-- 拒单率警示 -->
<section class="section alert-section">
<div class="section-label fade-in" style="color: #ff6b6b;">ATTENTION</div>
<h2 class="section-title fade-in">需要关注的数据</h2>
<div class="alert-grid fade-in">
<div class="alert-card">
<div class="alert-icon">📍</div>
<div class="alert-title">拒单率最高地区</div>
<div class="alert-value" id="rejectionRegion">-</div>
<div class="alert-desc">拒单率达 <span style="color: #ff6b6b; font-weight: bold;" id="rejectionRate">0%</span></div>
</div>
<div class="alert-card">
<div class="alert-icon">🕐</div>
<div class="alert-title">拒单率最高时段</div>
<div class="alert-value" id="rejectionTimeSlot">-</div>
<div class="alert-desc" id="rejectionTimeDesc">时段描述</div>
</div>
<h2 class="section-title fade-in">未接单损失TOP3地区</h2>
<div class="bar-chart fade-in" id="rejectionBarChart">
<!-- 动态渲染拒单TOP3地区柱状图 -->
</div>
<p class="fade-in" id="totalLossSummary" style="margin-top: 40px; color: rgba(255,255,255,0.8); font-size: 0.9em;"></p>
</section>
<!-- APP使用率 -->
<section class="section compare-section">
<div class="section-label fade-in">APP USAGE</div>
<h2 class="section-title fade-in">APP 使用率</h2>
<h2 class="section-title fade-in">使用APP,操作更方便,结算更快捷!</h2>
<div class="app-usage-container fade-in">
<div class="app-usage-card">
<div class="app-usage-icon">📱</div>
@@ -1032,9 +1032,12 @@
<div class="app-usage-bar">
<div class="app-usage-fill" id="appUsageFill"></div>
</div>
<div class="app-usage-rank" id="appUsageRank" style="margin-top: 20px; font-size: 1.1em; color: rgba(255,255,255,0.8);">
APP排名超过了 <span class="highlight" id="appRankPercent">0%</span> 的客户
</div>
</div>
</div>
<p class="fade-in" id="appUsageDesc" style="margin-top: 40px; color: rgba(255,255,255,0.5);"></p>
<!-- <p class="fade-in" id="appUsageDesc" style="margin-top: 40px; color: rgba(255,255,255,0.5);"></p>-->
</section>
<!-- 结尾 -->
@@ -1045,7 +1048,7 @@
让我们一起迎接更好的<br>
<span class="ending-year">2026</span>
</div>
<div class="ending-wish fade-in">砥砺前行 · 再创辉煌</div>
<div class="ending-wish fade-in">优质服务,共享成果</div>
</section>
</div>
@@ -1054,9 +1057,7 @@
let excelData = {
kpi: [],
casesTop3: [],
onlineTop3: [],
rejectionRegion: [],
rejectionTime: []
rejectionRegion: []
};
let currentProvider = null;
@@ -1092,19 +1093,15 @@
try {
// 并行加载所有JSON文件
const [kpi, casesTop3, onlineTop3, rejectionRegion, rejectionTime] = await Promise.all([
const [kpi, casesTop3, rejectionRegion] = await Promise.all([
loadJsonFile('data/kpi.json'),
loadJsonFile('data/cases_top3.json'),
loadJsonFile('data/online_top3.json'),
loadJsonFile('data/rejection_region.json'),
loadJsonFile('data/rejection_time.json')
loadJsonFile('data/rejection_region.json')
]);
excelData.kpi = kpi;
excelData.casesTop3 = casesTop3;
excelData.onlineTop3 = onlineTop3;
excelData.rejectionRegion = rejectionRegion;
excelData.rejectionTime = rejectionTime;
// 获取URL指定的服务商ID
const urlProviderId = getProviderIdFromUrl();
@@ -1172,54 +1169,50 @@
.map((row, i) => ({
rank: i + 1,
name: row['服务人员工号'],
cases: row['完成案件量'] || 0
cases: row['完成案件量'] || 0,
aggregatedCases: row['聚合案件量'] || 0
}));
// 获取在线时长TOP3师傅使用服务商id匹配
const onlineTop3 = excelData.onlineTop3
.filter(row => String(row['服务商id']) === providerId)
// 获取该服务商所有拒单地区数据
const allRejectionRegions = excelData.rejectionRegion
.filter(row => String(row['服务商id']) === providerId);
// 总损失案件量使用kpi.json的拒单量
const totalLoss = kpiRow['拒单量'] || 0;
// 获取拒单率TOP3地区
const rejectionRegionTop3 = allRejectionRegions
.slice(0, 3)
.map((row, i) => ({
rank: i + 1,
name: row['司机姓名'],
hours: Math.round(row['年度总在线时长(小时)'] || 0)
region: row['地区'] || '-',
count: row['拒单量'] || 0,
rate: row['拒单率'] || 0
}));
// 获取拒单率最高地区使用服务商id匹配
const rejectionRegionData = excelData.rejectionRegion.find(row => String(row['服务商id']) === providerId);
// 获取拒单率最高时段使用服务商id匹配
const rejectionTimeData = excelData.rejectionTime.find(row => String(row['服务商id']) === providerId);
// 构建数据对象
const data = {
year: 2025,
serviceProviderName: providerName,
summary: {
totalCases: kpiRow['完成案件量'] || 0,
totalCases: kpiRow['案件量'] || 0,
caseBreakdown: {
towing: kpiRow['拖车完成量'] || 0,
minorRepair: kpiRow['小修完成量'] || 0,
predicament: kpiRow['困境完成量'] || 0
towing: kpiRow['拖车案件量'] || 0,
minorRepair: kpiRow['小修案件量'] || 0,
predicament: kpiRow['困境案件量'] || 0
},
aggregatedCases: kpiRow['聚合案件量'] || 0
},
topMastersByCases: casesTop3,
onlineHours: {
averageTotal: Math.round(kpiRow['年度车辆平均总在线时长(小时)'] || 0),
topMasters: onlineTop3
averageTotal: kpiRow['平均每车每人在线时长(小时)'] || 0
},
rejectionRate: {
highestRegion: {
name: rejectionRegionData ? rejectionRegionData['地区'] : '-',
rate: rejectionRegionData ? ((rejectionRegionData['拒单率'] || 0) * 100).toFixed(2) : 0
},
highestTimeSlot: {
period: rejectionTimeData ? rejectionTimeData['时段'] : '-',
rate: rejectionTimeData ? ((rejectionTimeData['拒单率'] || 0) * 100).toFixed(2) : 0
}
},
appUsageRate: kpiRow['APP使用率.'] || 0
rejectionTop3Regions: rejectionRegionTop3,
totalLoss: totalLoss,
appUsageRate: kpiRow['APP使用率.'] || 0,
appRankOverCustomers: kpiRow['超过客户'] || 0
};
renderDashboard(data);
@@ -1238,8 +1231,8 @@
document.getElementById('totalCases').textContent = totalCases.toLocaleString();
document.getElementById('totalCases').dataset.target = totalCases;
// 案件类型饼图
renderDonutChart(data.summary.caseBreakdown, totalCases);
// 案件类型饼图(已注释)
// renderDonutChart(data.summary.caseBreakdown, totalCases);
// 聚合案件
const aggregatedCases = data.summary.aggregatedCases;
@@ -1248,43 +1241,19 @@
document.getElementById('aggregatedCases').textContent = Math.round(aggregatedCases).toLocaleString();
document.getElementById('aggregatedCases').dataset.target = Math.round(aggregatedCases);
document.getElementById('aggregatedDesc').innerHTML =
'占总案件量的 <span class="highlight">' + aggregatedPercent + '%</span><br>' +
'平均每月聚合 <span class="highlight">' + avgPerMonth + '</span> 个案件';
'占总案件量的 <span class="highlight">' + aggregatedPercent + '%</span><br>'
// TOP3 师傅
renderPodium(data.topMastersByCases, totalCases);
// 在线时长
// 在线时长(日均)- 截断保留两位小数(不四舍五入)
const avgHours = data.onlineHours.averageTotal;
const days = (avgHours / 24).toFixed(1);
const dailyHours = (avgHours / 365).toFixed(1);
document.getElementById('avgOnlineHours').textContent = avgHours.toLocaleString();
document.getElementById('avgOnlineHours').dataset.target = avgHours;
document.getElementById('onlineDesc').innerHTML =
'相当于 <span class="highlight">' + days + '</span> 天 · 日均在线 <span class="highlight">' + dailyHours + '</span> 小时';
const truncatedHours = (avgHours * 100) / 100;
document.getElementById('avgOnlineHours').textContent = truncatedHours.toFixed(2);
renderBarChart(data.onlineHours.topMasters);
// 拒单率
const regionRate = parseFloat(data.rejectionRate.highestRegion.rate) || 0;
const timeRate = parseFloat(data.rejectionRate.highestTimeSlot.rate) || 0;
if (regionRate === 0 || data.rejectionRate.highestRegion.name === '-') {
document.getElementById('rejectionRegion').textContent = '无';
document.getElementById('rejectionRate').textContent = '';
document.querySelector('.alert-card:first-child .alert-desc').textContent = '您没有拒单高发地区';
} else {
document.getElementById('rejectionRegion').textContent = data.rejectionRate.highestRegion.name;
document.getElementById('rejectionRate').textContent = data.rejectionRate.highestRegion.rate + '%';
}
if (timeRate === 0 || data.rejectionRate.highestTimeSlot.period === '-') {
document.getElementById('rejectionTimeSlot').textContent = '无';
document.getElementById('rejectionTimeDesc').textContent = '您没有拒单高发时段';
} else {
document.getElementById('rejectionTimeSlot').textContent = data.rejectionRate.highestTimeSlot.period;
document.getElementById('rejectionTimeDesc').textContent = '拒单率 ' + data.rejectionRate.highestTimeSlot.rate + '%';
}
// 拒单TOP3地区
renderRejectionTop3(data.rejectionTop3Regions, data.totalLoss);
// APP使用率
const appUsagePercent = (data.appUsageRate * 100).toFixed(2);
@@ -1293,12 +1262,10 @@
document.getElementById('appUsageFill').style.width = appUsagePercent + '%';
}, 100);
let usageLevel = '优秀';
if (appUsagePercent < 90) usageLevel = '良好';
if (appUsagePercent < 80) usageLevel = '一般';
if (appUsagePercent < 70) usageLevel = '需改进';
document.getElementById('appUsageDesc').innerHTML =
'APP使用率表现<span class="highlight">' + usageLevel + '</span>,继续保持高效的数字化运营';
// APP排名超过客户百分比
const appRankPercent = (data.appRankOverCustomers * 100).toFixed(2);
document.getElementById('appRankPercent').textContent = appRankPercent + '%';
}
// 渲染环形图
@@ -1375,15 +1342,61 @@
<div class="podium-avatar">${avatar}</div>
<div class="podium-name">${master.name}</div>
<div class="podium-value">${master.cases} 个</div>
<div class="podium-agg" style="font-size: 0.9em; color: #00ff88; margin-top: 5px;">聚合 ${master.aggregatedCases} 个</div>
<div class="podium-bar">${master.rank}</div>
</div>
`;
}).join('');
const totalTop3 = topMasters.reduce((sum, m) => sum + m.cases, 0);
const percent = ((totalTop3 / total) * 100).toFixed(0);
document.getElementById('top3Summary').innerHTML =
'TOP3 合计完成 <span class="highlight">' + totalTop3 + '</span> 个案件,占总量 <span class="highlight">' + percent + '%</span>';
}
// 渲染拒单TOP3地区纵向柱状图
function renderRejectionTop3(regions, totalLoss) {
const container = document.getElementById('rejectionBarChart');
const summaryEl = document.getElementById('totalLossSummary');
if (!regions || regions.length === 0) {
// 没有拒单地区,显示奖状
container.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div style="font-size: 4em; margin-bottom: 20px;">🏆</div>
<div style="color: #00ff88; font-size: 1.5em; margin-bottom: 10px;">优秀服务商</div>
<div style="color: rgba(255,255,255,0.7);">恭喜您在2025年度没有拒单记录</div>
</div>
`;
summaryEl.textContent = '';
return;
}
const maxCount = Math.max(...regions.map(r => r.count));
const colors = ['#ff6b6b', '#ff8c42', '#ffb347'];
container.innerHTML = `
<div style="display: flex; justify-content: center; align-items: flex-end; gap: 30px; height: 250px; padding: 20px;">
${regions.map((item, i) => {
const height = maxCount > 0 ? ((item.count / maxCount) * 100).toFixed(1) : 0;
return `
<div style="display: flex; flex-direction: column; align-items: center; flex: 1; max-width: 120px;">
<div style="font-family: 'Orbitron', sans-serif; font-size: 1.1em; color: ${colors[i]}; margin-bottom: 10px;">${item.count} 单</div>
<div style="width: 60px; height: 180px; background: rgba(255,255,255,0.1); border-radius: 8px 8px 0 0; display: flex; align-items: flex-end; overflow: hidden;">
<div class="vertical-bar" data-height="${height}" style="width: 100%; height: 0%; background: linear-gradient(180deg, ${colors[i]}, ${colors[i]}88); border-radius: 8px 8px 0 0; transition: height 1.5s ease;"></div>
</div>
<div style="margin-top: 15px; font-size: 0.9em; color: rgba(255,255,255,0.8); text-align: center; word-break: break-all; max-width: 100px;">${item.region}</div>
</div>
`;
}).join('')}
</div>
`;
// 动画
setTimeout(() => {
container.querySelectorAll('.vertical-bar').forEach((bar) => {
bar.style.height = bar.dataset.height + '%';
});
}, 100);
// 显示总损失
summaryEl.innerHTML = '由于您未及时接单2025年共损失了 <span style="color: #ff6b6b; font-weight: bold; font-size: 1.3em;">' + totalLoss.toLocaleString() + '</span> 个案件';
}
// 渲染柱状图

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB