Files
supplier-dispatch-h5/public/res/year-statistic/index.html
2026-02-06 16:51:51 +08:00

1566 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2025年度总结报告</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
}
body {
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
background: #020818;
color: #fff;
overflow-x: hidden;
}
/* 加载状态 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #020818;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10000;
}
.loading-spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(0, 212, 255, 0.2);
border-top-color: #00d4ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20px;
color: #00d4ff;
font-size: 1.2rem;
}
.error-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #020818;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10000;
}
.error-overlay h2 {
color: #ff6b6b;
margin-bottom: 15px;
}
.error-overlay p {
color: #a0a0a0;
}
/* 粒子背景 */
#particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
/* 全屏滚动区块 */
.section {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px 20px;
position: relative;
scroll-snap-align: start;
overflow: hidden;
}
/* 封面 */
.cover {
background: radial-gradient(ellipse at center, #0a1628 0%, #020818 100%);
}
.cover::before {
content: '';
position: absolute;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(0, 150, 255, 0.15) 0%, transparent 70%);
border-radius: 50%;
animation: pulse 4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.2); opacity: 0.8; }
}
.cover-year {
font-family: 'Orbitron', sans-serif;
font-size: 8em;
font-weight: 900;
background: linear-gradient(180deg, #00d4ff 0%, #0066ff 50%, #003380 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 60px rgba(0, 150, 255, 0.5);
position: relative;
z-index: 1;
}
.cover-title {
font-size: 2.5em;
margin-top: 20px;
letter-spacing: 15px;
color: rgba(255, 255, 255, 0.9);
position: relative;
z-index: 1;
}
.cover-subtitle {
margin-top: 30px;
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 {
position: absolute;
bottom: 40px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
color: rgba(255, 255, 255, 0.5);
animation: bounce 2s infinite;
}
.scroll-hint .arrow {
width: 30px;
height: 30px;
border-right: 2px solid rgba(0, 200, 255, 0.6);
border-bottom: 2px solid rgba(0, 200, 255, 0.6);
transform: rotate(45deg);
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(10px); }
}
/* 通用标题样式 */
.section-label {
font-size: 0.9em;
color: #00d4ff;
letter-spacing: 5px;
text-transform: uppercase;
margin-bottom: 15px;
}
.section-title {
font-size: 2em;
margin-bottom: 40px;
text-align: center;
}
.highlight {
color: #00d4ff;
font-weight: bold;
}
/* 大数字展示 */
.big-stat {
text-align: center;
margin-bottom: 30px;
}
.big-stat-number {
font-family: 'Orbitron', sans-serif;
font-size: 6em;
font-weight: 700;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
}
.big-stat-unit {
font-size: 1.5em;
color: rgba(255, 255, 255, 0.6);
margin-top: 10px;
}
.big-stat-desc {
font-size: 1.1em;
color: rgba(255, 255, 255, 0.5);
margin-top: 15px;
}
/* 环形进度图 */
.donut-container {
display: flex;
justify-content: center;
gap: 40px;
flex-wrap: nowrap;
margin-top: 30px;
}
.donut-item {
text-align: center;
position: relative;
flex-shrink: 1;
}
.donut-ring {
width: 140px;
height: 140px;
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.donut-ring svg {
position: absolute;
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.donut-ring circle {
fill: none;
stroke-width: 8;
}
.donut-ring .bg {
stroke: rgba(255, 255, 255, 0.1);
}
.donut-ring .progress {
stroke-linecap: round;
transition: stroke-dashoffset 1.5s ease;
}
.donut-value {
font-family: 'Orbitron', sans-serif;
font-size: 1.8em;
font-weight: 700;
z-index: 1;
}
.donut-label {
margin-top: 15px;
font-size: 1em;
color: rgba(255, 255, 255, 0.8);
}
.donut-count {
font-size: 0.9em;
color: rgba(255, 255, 255, 0.5);
margin-top: 5px;
}
/* 排行榜卡片 */
.rank-section {
background: linear-gradient(180deg, #020818 0%, #0a1628 50%, #020818 100%);
}
.podium {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 20px;
margin-top: 40px;
}
.podium-item {
display: flex;
flex-direction: column;
align-items: center;
transition: transform 0.3s;
}
.podium-item:hover {
transform: translateY(-10px);
}
.podium-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2em;
margin-bottom: 15px;
position: relative;
}
.podium-item:nth-child(2) .podium-avatar {
width: 100px;
height: 100px;
font-size: 2.5em;
}
.podium-avatar::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
padding: 4px;
background: linear-gradient(135deg, #00d4ff, #0066ff);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.podium-item:nth-child(2) .podium-avatar::after {
background: linear-gradient(135deg, #ffd700, #ff8c00);
}
.podium-name {
font-size: 1.2em;
margin-bottom: 8px;
}
.podium-value {
font-family: 'Orbitron', sans-serif;
font-size: 1.3em;
color: #00d4ff;
}
.podium-item:nth-child(2) .podium-value {
color: #ffd700;
}
.podium-bar {
width: 100px;
margin-top: 15px;
border-radius: 8px 8px 0 0;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 15px;
font-family: 'Orbitron', sans-serif;
font-size: 1.5em;
font-weight: bold;
}
.podium-item:nth-child(1) .podium-bar {
height: 100px;
background: linear-gradient(180deg, #c0c0c0, #808080);
}
.podium-item:nth-child(2) .podium-bar {
height: 140px;
background: linear-gradient(180deg, #ffd700, #ff8c00);
width: 120px;
}
.podium-item:nth-child(3) .podium-bar {
height: 80px;
background: linear-gradient(180deg, #cd7f32, #8b4513);
}
/* 时间轴卡片 */
.time-card {
background: linear-gradient(135deg, rgba(0, 100, 200, 0.1), rgba(0, 200, 255, 0.05));
border: 1px solid rgba(0, 200, 255, 0.2);
border-radius: 20px;
padding: 40px;
max-width: 500px;
text-align: center;
position: relative;
overflow: hidden;
}
.time-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, transparent, #00d4ff, transparent);
}
.time-icon {
font-size: 4em;
margin-bottom: 20px;
}
.time-value {
font-family: 'Orbitron', sans-serif;
font-size: 4em;
color: #00d4ff;
margin: 20px 0;
}
.time-desc {
font-size: 1.1em;
color: rgba(255, 255, 255, 0.7);
line-height: 1.8;
}
/* 对比卡片 */
.compare-section {
background: linear-gradient(180deg, #020818 0%, #071230 50%, #020818 100%);
}
.compare-container {
display: flex;
gap: 40px;
flex-wrap: wrap;
justify-content: center;
max-width: 900px;
}
.compare-card {
flex: 1;
min-width: 280px;
max-width: 400px;
padding: 35px;
border-radius: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.compare-card.high {
background: linear-gradient(135deg, rgba(255, 80, 80, 0.15), rgba(255, 150, 100, 0.1));
border: 1px solid rgba(255, 100, 100, 0.3);
}
.compare-card.low {
background: linear-gradient(135deg, rgba(0, 200, 150, 0.15), rgba(0, 255, 200, 0.1));
border: 1px solid rgba(0, 200, 150, 0.3);
}
.compare-label {
font-size: 0.9em;
letter-spacing: 3px;
margin-bottom: 15px;
}
.compare-card.high .compare-label {
color: #ff6b6b;
}
.compare-card.low .compare-label {
color: #00ff88;
}
.compare-region {
font-size: 2.5em;
font-weight: bold;
margin-bottom: 15px;
}
.compare-value {
font-family: 'Orbitron', sans-serif;
font-size: 3.5em;
font-weight: 700;
}
.compare-card.high .compare-value {
color: #ff6b6b;
}
.compare-card.low .compare-value {
color: #00ff88;
}
.compare-unit {
font-size: 1em;
color: rgba(255, 255, 255, 0.6);
margin-top: 10px;
}
/* 警示卡片 */
.alert-section {
background: radial-gradient(ellipse at center, #1a0a0a 0%, #020818 70%);
}
.alert-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
max-width: 800px;
width: 100%;
}
.alert-card {
background: linear-gradient(135deg, rgba(255, 50, 50, 0.1), rgba(255, 100, 50, 0.05));
border: 1px solid rgba(255, 80, 80, 0.3);
border-radius: 20px;
padding: 30px;
text-align: center;
position: relative;
}
.alert-card::before {
content: '';
position: absolute;
top: -1px;
left: 50%;
transform: translateX(-50%);
width: 60%;
height: 3px;
background: linear-gradient(90deg, transparent, #ff6b6b, transparent);
}
.alert-icon {
font-size: 2.5em;
margin-bottom: 15px;
}
.alert-title {
font-size: 1em;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 15px;
}
.alert-value {
font-family: 'Orbitron', sans-serif;
font-size: 2.5em;
color: #ff6b6b;
font-weight: 700;
}
.alert-desc {
margin-top: 15px;
font-size: 1.1em;
color: rgba(255, 255, 255, 0.8);
}
/* 水波进度 */
.wave-section {
background: linear-gradient(180deg, #020818 0%, #051525 50%, #020818 100%);
}
.wave-container {
display: flex;
gap: 60px;
flex-wrap: wrap;
justify-content: center;
}
.wave-item {
text-align: center;
}
.wave-circle {
width: 180px;
height: 180px;
border-radius: 50%;
position: relative;
overflow: hidden;
background: rgba(0, 100, 150, 0.2);
border: 3px solid rgba(0, 200, 255, 0.3);
}
.wave {
position: absolute;
bottom: 0;
left: 0;
width: 200%;
height: 100%;
background: linear-gradient(180deg, rgba(0, 200, 255, 0.6) 0%, rgba(0, 100, 200, 0.8) 100%);
border-radius: 40%;
animation: wave 3s linear infinite;
}
.wave:nth-child(2) {
animation-delay: -0.5s;
opacity: 0.5;
}
@keyframes wave {
0% { transform: translateX(0) translateY(30%) rotate(0deg); }
100% { transform: translateX(-50%) translateY(30%) rotate(360deg); }
}
.wave-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-family: 'Orbitron', sans-serif;
font-size: 2.2em;
font-weight: 700;
z-index: 2;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}
.wave-label {
margin-top: 20px;
font-size: 1.1em;
color: rgba(255, 255, 255, 0.8);
}
/* 结尾 */
.ending {
background: radial-gradient(ellipse at center, #0a1628 0%, #020818 100%);
}
.ending-text {
font-size: 2em;
text-align: center;
line-height: 1.8;
max-width: 600px;
}
.ending-year {
font-family: 'Orbitron', sans-serif;
font-size: 1.5em;
color: #00d4ff;
}
.ending-wish {
margin-top: 40px;
font-family: 'Orbitron', sans-serif;
font-size: 1.8em;
color: rgba(255, 255, 255, 0.8);
}
/* 导航点 */
.nav-dots {
position: fixed;
right: 30px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 15px;
z-index: 100;
}
.nav-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
cursor: pointer;
transition: all 0.3s;
}
.nav-dot:hover, .nav-dot.active {
background: #00d4ff;
box-shadow: 0 0 15px rgba(0, 200, 255, 0.6);
}
/* 动画类 */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: all 0.8s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* 返回按钮 */
.back-btn {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 100, 150, 0.3);
border: 1px solid rgba(0, 200, 255, 0.3);
color: #00d4ff;
font-size: 1.2em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.back-btn:hover {
background: rgba(0, 150, 200, 0.4);
transform: scale(1.1);
box-shadow: 0 0 20px rgba(0, 200, 255, 0.4);
}
.back-btn:active {
transform: scale(0.95);
}
/* 响应式 */
@media (max-width: 768px) {
.cover-year { font-size: 4em; }
.cover-title { font-size: 1.5em; letter-spacing: 8px; }
.cover-subtitle { font-size: 1em; }
.big-stat-number { font-size: 3em; }
.big-stat-unit { font-size: 1.2em; }
.big-stat-desc { font-size: 0.95em; }
.section-title { font-size: 1.5em; }
.section-label { font-size: 0.8em; }
/* 领奖台小屏适配 */
.podium-avatar { width: 60px !important; height: 60px !important; }
.podium-item:nth-child(2) .podium-avatar { width: 80px !important; height: 80px !important; }
.podium-bar { height: 40px !important; font-size: 1em !important; }
.podium-item:nth-child(2) .podium-bar { height: 60px !important; }
/* 环形图适配 - 保持一排显示 */
.donut-container { gap: 15px; flex-wrap: nowrap; justify-content: center; }
.donut-ring { width: 90px; height: 90px; }
.donut-value { font-size: 1.2em; }
.donut-label { font-size: 0.85em; }
.donut-count { font-size: 0.75em; }
.donut-item { min-width: 0; }
/* 时间卡片适配 */
.time-card { padding: 25px; max-width: 90%; }
.time-value { font-size: 2.5em; }
.time-icon { font-size: 3em; }
.time-desc { font-size: 1em; }
/* 柱状图适配 */
.bar-chart { max-width: 90%; }
.bar-label { width: 50px; font-size: 0.85em; }
.bar-fill { font-size: 0.75em; padding-right: 10px; }
/* 警示卡片适配 */
.alert-grid { gap: 20px; }
.alert-card { padding: 20px; min-width: auto; }
.alert-icon { font-size: 2em; }
.alert-value { font-size: 1.8em; }
.alert-title { font-size: 0.9em; }
/* APP使用率适配 */
.app-usage-card { padding: 30px 40px; }
.app-usage-value { font-size: 3.5em; }
.app-usage-icon { font-size: 3em; }
.app-usage-desc { font-size: 1em; }
/* 对比卡片适配 */
.compare-container { gap: 20px; }
.compare-card { padding: 25px; min-width: 250px; }
.compare-region { font-size: 2em; }
.compare-value { font-size: 2.5em; }
/* 结尾适配 */
.ending-text { font-size: 1.5em; }
.ending-year { font-size: 1.3em; }
.ending-wish { font-size: 1em; }
.nav-dots { display: none; }
.section { padding: 50px 15px; }
.back-btn { top: 15px; left: 15px; width: 40px; height: 40px; }
}
/* 更小屏幕适配 */
@media (max-width: 375px) {
.cover-year { font-size: 3em; }
.cover-title { font-size: 1.2em; letter-spacing: 5px; }
.big-stat-number { font-size: 2.5em; }
.section-title { font-size: 1.3em; }
/* 环形图 - 更小屏幕 */
.donut-container { gap: 10px; }
.donut-ring { width: 75px; height: 75px; }
.donut-value { font-size: 1em; }
.donut-label { font-size: 0.75em; }
.donut-count { font-size: 0.7em; }
.time-value { font-size: 2em; }
.app-usage-value { font-size: 2.8em; }
.app-usage-card { padding: 25px 30px; }
.alert-value { font-size: 1.5em; }
}
/* 横向柱状图 */
.bar-chart {
width: 100%;
max-width: 500px;
margin-top: 30px;
}
.bar-item {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.bar-label {
width: 60px;
font-size: 1em;
color: rgba(255, 255, 255, 0.8);
}
.bar-track {
flex: 1;
height: 30px;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
overflow: hidden;
position: relative;
}
.bar-fill {
height: 100%;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 15px;
font-family: 'Orbitron', sans-serif;
font-size: 0.9em;
font-weight: bold;
transition: width 1.5s ease;
}
.bar-fill.blue { background: linear-gradient(90deg, #0066ff, #00d4ff); }
.bar-fill.purple { background: linear-gradient(90deg, #6600ff, #aa00ff); }
.bar-fill.orange { background: linear-gradient(90deg, #ff6600, #ffaa00); }
/* APP使用率样式 */
.app-usage-container {
display: flex;
justify-content: center;
}
.app-usage-card {
background: linear-gradient(135deg, rgba(0, 200, 150, 0.15), rgba(0, 255, 200, 0.1));
border: 1px solid rgba(0, 200, 150, 0.3);
border-radius: 20px;
padding: 50px 80px;
text-align: center;
position: relative;
overflow: hidden;
}
.app-usage-icon {
font-size: 4em;
margin-bottom: 20px;
}
.app-usage-value {
font-family: 'Orbitron', sans-serif;
font-size: 5em;
font-weight: 700;
color: #00ff88;
line-height: 1;
}
.app-usage-desc {
font-size: 1.2em;
color: rgba(255, 255, 255, 0.7);
margin-top: 15px;
margin-bottom: 30px;
}
.app-usage-bar {
width: 100%;
height: 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
overflow: hidden;
}
.app-usage-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88, #00d4ff);
border-radius: 6px;
transition: width 1.5s ease;
width: 0%;
}
.imgTopLogo {
width: 50%;
position: fixed;
z-index: 100000;
right: 20px;
top: 20px;
opacity: 0.8;
}
</style>
</head>
<body>
<!-- 加载中 -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
<div class="loading-text">加载数据中...</div>
</div>
<!-- 错误提示 -->
<div class="error-overlay" id="errorOverlay">
<h2>数据加载失败</h2>
<p id="errorMessage"></p>
</div>
<!-- 返回按钮 -->
<button class="back-btn" id="backBtn" onclick="goBack()" title="返回">
</button>
<!-- 主内容 -->
<div id="mainContent" style="display: none;">
<img src="./static/logo_top.png" class="imgTopLogo" alt="">
<!-- 粒子背景 -->
<canvas id="particles"></canvas>
<!-- 导航点 -->
<div class="nav-dots">
<div class="nav-dot active" data-section="0"></div>
<div class="nav-dot" data-section="1"></div>
<div class="nav-dot" data-section="2"></div>
<div class="nav-dot" data-section="3"></div>
<div class="nav-dot" data-section="4"></div>
<div class="nav-dot" data-section="5"></div>
<div class="nav-dot" data-section="6"></div>
<div class="nav-dot" data-section="7"></div>
</div>
<!-- 封面 -->
<section class="section cover">
<div class="cover-year" id="coverYear">2025</div>
<div class="cover-title">年 度 总 结</div>
<div class="cover-subtitle" id="providerName">服务商名称</div>
<div class="scroll-hint">
<span>向下滑动</span>
<div class="arrow"></div>
</div>
</section>
<!-- 总案件量 -->
<section class="section">
<!-- <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>-->
</section>
<!-- 聚合案件 -->
<section class="section">
<!-- <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-value" id="aggregatedCases">0</div>
<div class="big-stat-unit">个聚合量</div>
<div class="time-desc" id="aggregatedDesc">
占总案件量的 <span class="highlight">0%</span><br>
<!-- 平均每月聚合 <span class="highlight">0</span> 个案件-->
</div>
</div>
</section>
<!-- 案件量TOP3 -->
<section class="section rank-section">
<!-- <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>-->
</section>
<!-- 在线时长 -->
<section class="section wave-section">
<div class="section-label fade-in">ONLINE DURATION</div>
<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>
</section>
<!-- 拒单率警示 -->
<section class="section alert-section">
<div class="section-label fade-in" style="color: #ff6b6b;">ATTENTION</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>
<div class="app-usage-container fade-in">
<div class="app-usage-card">
<div class="app-usage-icon">📱</div>
<div class="app-usage-value" id="appUsageRate">0%</div>
<div class="app-usage-desc">年度APP使用率</div>
<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>-->
</section>
<!-- 结尾 -->
<section class="section ending">
<div class="ending-text fade-in">
<span class="ending-year" id="endYear">2025</span><br>
感谢每一位师傅的辛勤付出<br>
让我们一起迎接更好的<br>
<span class="ending-year">2026</span>
</div>
<div class="ending-wish fade-in">优质服务,共享成果</div>
</section>
</div>
<script>
// 全局数据
let excelData = {
kpi: [],
casesTop3: [],
rejectionRegion: []
};
let currentProvider = null;
// 获取URL参数
function getProviderIdFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
let providerId = urlParams.get('providerId');
// 处理URL格式错误的情况如 ?providerId=33041?token=xxx第二个?应该是&
if (providerId && providerId.includes('?')) {
providerId = providerId.split('?')[0];
}
return providerId || null;
}
// 读取JSON文件
async function loadJsonFile(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to load ' + url);
}
return response.json();
}
// 加载所有JSON数据
async function loadAllExcelData() {
const loading = document.getElementById('loadingOverlay');
const error = document.getElementById('errorOverlay');
const mainContent = document.getElementById('mainContent');
loading.style.display = 'flex';
error.style.display = 'none';
mainContent.style.display = 'none';
try {
// 并行加载所有JSON文件
const [kpi, casesTop3, rejectionRegion] = await Promise.all([
loadJsonFile('data/kpi.json'),
loadJsonFile('data/cases_top3.json'),
loadJsonFile('data/rejection_region.json')
]);
excelData.kpi = kpi;
excelData.casesTop3 = casesTop3;
excelData.rejectionRegion = rejectionRegion;
// 获取URL指定的服务商ID
const urlProviderId = getProviderIdFromUrl();
if (urlProviderId) {
const hasData = renderProviderData(urlProviderId);
if (hasData) {
initParticles();
initAnimations();
initNavigation();
mainContent.style.display = 'block';
}
} else {
// 如果没有指定服务商ID显示错误
throw new Error('请在URL中指定服务商ID例如: ?providerId=123');
}
loading.style.display = 'none';
} catch (err) {
console.error('加载数据失败:', err);
loading.style.display = 'none';
error.style.display = 'flex';
document.getElementById('errorMessage').textContent = '加载Excel数据失败: ' + err.message;
}
}
// 渲染欢迎页面(新服务商)
function renderWelcomePage() {
const mainContent = document.getElementById('mainContent');
mainContent.innerHTML = `
<canvas id="particles"></canvas>
<section class="section cover" style="min-height: 100vh;">
<div class="cover-year">2025</div>
<div class="cover-title">年 度 总 结</div>
<div style="margin-top: 60px; text-align: center;">
<div style="font-size: 3em; margin-bottom: 30px;">👋</div>
<div style="font-size: 1.8em; color: #00d4ff; margin-bottom: 20px;">中道救援的新朋友</div>
<div style="font-size: 1.5em; color: rgba(255,255,255,0.8);">欢迎您的加入</div>
<div style="margin-top: 40px; font-size: 1.1em; color: rgba(255,255,255,0.5);">期待与您共创辉煌的2026</div>
</div>
</section>
`;
mainContent.style.display = 'block';
initParticles();
}
// 根据服务商ID获取并渲染数据
function renderProviderData(providerId) {
currentProvider = providerId;
// 从KPI表获取基础数据
const kpiRow = excelData.kpi.find(row => String(row['服务商id']) === providerId);
if (!kpiRow) {
console.error('未找到服务商数据:', providerId);
renderWelcomePage();
return false;
}
const providerName = kpiRow['服务商'];
// 获取案件TOP3师傅
const casesTop3 = excelData.casesTop3
.filter(row => String(row['服务商id']) === providerId)
.slice(0, 3)
.map((row, i) => ({
rank: i + 1,
name: row['服务人员工号'],
cases: row['完成案件量'] || 0,
aggregatedCases: row['聚合案件量'] || 0
}));
// 获取该服务商所有拒单地区数据
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,
region: row['地区'] || '-',
count: row['拒单量'] || 0,
rate: row['拒单率'] || 0
}));
// 构建数据对象
const data = {
year: 2025,
serviceProviderName: providerName,
summary: {
totalCases: kpiRow['总案件量'] || 0,
caseBreakdown: {
towing: kpiRow['拖车案件量'] || 0,
minorRepair: kpiRow['小修案件量'] || 0,
predicament: kpiRow['困境案件量'] || 0
},
aggregatedCases: kpiRow['聚合案件量'] || 0
},
topMastersByCases: casesTop3,
onlineHours: {
averageTotal: kpiRow['平均每车每人在线时长(小时)'] || 0
},
rejectionTop3Regions: rejectionRegionTop3,
totalLoss: totalLoss,
appUsageRate: kpiRow['APP使用率.'] || 0,
appRankOverCustomers: kpiRow['超过客户'] || 0
};
renderDashboard(data);
return true;
}
// 渲染看板
function renderDashboard(data) {
// 封面
document.getElementById('coverYear').textContent = data.year;
document.getElementById('providerName').textContent = data.serviceProviderName;
document.getElementById('endYear').textContent = data.year;
// 总案件量
const totalCases = data.summary.totalCases;
document.getElementById('totalCases').textContent = totalCases.toLocaleString();
document.getElementById('totalCases').dataset.target = totalCases;
// 案件类型饼图(已注释)
// renderDonutChart(data.summary.caseBreakdown, totalCases);
// 聚合案件
const aggregatedCases = data.summary.aggregatedCases;
const aggregatedPercent = totalCases > 0 ? ((aggregatedCases / totalCases) * 100).toFixed(1) : 0;
const avgPerMonth = (aggregatedCases / 12).toFixed(0);
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>'
// TOP3 师傅
renderPodium(data.topMastersByCases, totalCases);
// 在线时长(日均)- 截断保留两位小数(不四舍五入)
const avgHours = data.onlineHours.averageTotal;
const truncatedHours = (avgHours * 100) / 100;
document.getElementById('avgOnlineHours').textContent = truncatedHours.toFixed(2);
// 拒单TOP3地区
renderRejectionTop3(data.rejectionTop3Regions, data.totalLoss);
// APP使用率
const appUsagePercent = (data.appUsageRate * 100).toFixed(2);
document.getElementById('appUsageRate').textContent = appUsagePercent + '%';
setTimeout(() => {
document.getElementById('appUsageFill').style.width = appUsagePercent + '%';
}, 100);
// APP排名超过客户百分比
const appRankPercent = (data.appRankOverCustomers * 100).toFixed(2);
document.getElementById('appRankPercent').textContent = appRankPercent + '%';
}
// 渲染环形图
function renderDonutChart(breakdown, total) {
const container = document.getElementById('donutContainer');
const circumference = 2 * Math.PI * 40;
const types = [
{ key: 'towing', label: '拖车', color: '#ff6b6b' },
{ key: 'minorRepair', label: '小修', color: '#00d4ff' },
{ key: 'predicament', label: '困境', color: '#aa00ff' }
];
container.innerHTML = types.map(type => {
const value = breakdown[type.key];
const percent = ((value / total) * 100).toFixed(0);
const offset = circumference - (circumference * percent / 100);
return `
<div class="donut-item">
<div class="donut-ring">
<svg viewBox="0 0 100 100">
<circle class="bg" cx="50" cy="50" r="40"/>
<circle class="progress" cx="50" cy="50" r="40"
stroke="${type.color}"
stroke-dasharray="${circumference}"
stroke-dashoffset="${offset}"
style="stroke-dashoffset: ${circumference};"/>
</svg>
<div class="donut-value" style="color: ${type.color};">${percent}%</div>
</div>
<div class="donut-label">${type.label}</div>
<div class="donut-count">${value} 个</div>
</div>
`;
}).join('');
// 动画进度条
setTimeout(() => {
const progressCircles = container.querySelectorAll('.donut-ring .progress');
types.forEach((type, i) => {
const value = breakdown[type.key];
const percent = ((value / total) * 100).toFixed(0);
const offset = circumference - (circumference * percent / 100);
progressCircles[i].style.strokeDashoffset = offset;
});
}, 100);
}
// 渲染领奖台
function renderPodium(topMasters, total) {
const container = document.getElementById('podium');
// 根据排名获取对应图标
const avatarMap = { 1: '👑', 2: '🥈', 3: '🥉' };
// 按 rank 排序领奖台布局2, 1, 3 从左到右第1名在中间
let sorted;
if (topMasters.length === 1) {
// 只有1个人直接显示
sorted = [...topMasters];
} else if (topMasters.length === 2) {
// 只有2个人按 2, 1 顺序第1名在右边/中间位置)
sorted = [...topMasters].sort((a, b) => b.rank - a.rank);
} else {
// 3个人按 2, 1, 3 顺序第1名在中间
sorted = [...topMasters].sort((a, b) => {
const order = { 2: 0, 1: 1, 3: 2 };
return order[a.rank] - order[b.rank];
});
}
container.innerHTML = sorted.map((master) => {
const avatar = avatarMap[master.rank] || '🏅';
return `
<div class="podium-item">
<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('');
}
// 渲染拒单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> 个案件';
}
// 渲染柱状图
function renderBarChart(topMasters) {
const container = document.getElementById('barChart');
const maxHours = Math.max(...topMasters.map(m => m.hours));
const colors = ['blue', 'purple', 'orange'];
container.innerHTML = topMasters.map((master, i) => {
const width = ((master.hours / maxHours) * 100).toFixed(1);
return `
<div class="bar-item">
<div class="bar-label">${master.name}</div>
<div class="bar-track">
<div class="bar-fill ${colors[i]}" style="width: 0%;">${master.hours}h</div>
</div>
</div>
`;
}).join('');
// 动画
setTimeout(() => {
container.querySelectorAll('.bar-fill').forEach((bar, i) => {
const width = ((topMasters[i].hours / maxHours) * 100).toFixed(1);
bar.style.width = width + '%';
});
}, 100);
}
// 数字滚动动画
function animateNumber(element, target, duration = 2000) {
let start = 0;
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeProgress = 1 - Math.pow(1 - progress, 3);
const current = Math.floor(easeProgress * target);
element.textContent = current.toLocaleString();
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// 粒子背景
function initParticles() {
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
let particles = [];
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function createParticles() {
particles = [];
const count = Math.floor((canvas.width * canvas.height) / 15000);
for (let i = 0; i < count; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 2 + 0.5,
speedX: (Math.random() - 0.5) * 0.5,
speedY: (Math.random() - 0.5) * 0.5,
opacity: Math.random() * 0.5 + 0.2
});
}
}
function drawParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 200, 255, ${p.opacity})`;
ctx.fill();
p.x += p.speedX;
p.y += p.speedY;
if (p.x < 0 || p.x > canvas.width) p.speedX *= -1;
if (p.y < 0 || p.y > canvas.height) p.speedY *= -1;
});
requestAnimationFrame(drawParticles);
}
resizeCanvas();
createParticles();
drawParticles();
window.addEventListener('resize', () => {
resizeCanvas();
createParticles();
});
}
// 滚动动画
function initAnimations() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
const numbers = entry.target.querySelectorAll('[data-target]');
numbers.forEach(num => {
if (!num.classList.contains('animated')) {
num.classList.add('animated');
animateNumber(num, parseInt(num.dataset.target));
}
});
}
});
}, { threshold: 0.3 });
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
}
// 导航
function initNavigation() {
const sections = document.querySelectorAll('.section');
const navDots = document.querySelectorAll('.nav-dot');
window.addEventListener('scroll', () => {
let current = 0;
sections.forEach((section, index) => {
const rect = section.getBoundingClientRect();
if (rect.top <= window.innerHeight / 2) {
current = index;
}
});
navDots.forEach((dot, index) => {
dot.classList.toggle('active', index === current);
});
});
navDots.forEach(dot => {
dot.addEventListener('click', () => {
const index = parseInt(dot.dataset.section);
sections[index].scrollIntoView({ behavior: 'smooth' });
});
});
}
// 返回按钮功能
function goBack() {
let data = {"action":"goBack","params":""}
var u = navigator.userAgent;
var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
// var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
if(isiOS){
window.webkit.messageHandlers.nativeObject.postMessage(data);
}else {
window.android.sendMessage("goBack");
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadAllExcelData();
});
</script>
</body>
</html>