矿车运营数据仪表盘:深色工业风,8项核心指标,折线柱状图展示时间分布

By 18561366332, 27 April, 2026
推理
The user wants a statistical dashboard for mining truck production operations data. Let me think about the requirements:

1. 16:9 ratio layout
2. Data metrics: 运行时长 (runtime), 作业时长 (work time), 作业循环次数 (cycle count), 怠速时长 (idle time), 装载时长 (loading time), 卸载时长 (unloading time), 空车返回时间 (empty return time), 重车运行时间 (loaded running time)
3. Time range selection
4. Analysis by year/month/day
5. Distribution display by day/month/year

Let me create a comprehensive single-page application. Before coding, let me fetch design system recommendations and assets.

Let me call the design system tool for mining/industrial dashboard.Let me now design a comprehensive mining truck operations statistics dashboard. I need to create:

1. A 16:9 aspect ratio container
2. 8 key metrics cards
3. Time range selector with year/month/day toggle
4. Distribution charts (line chart and bar chart)
5. A data table for detailed view

Let me build this with:
- Dark industrial theme with amber/orange accents (mining theme)
- Custom CSS for the industrial feel
- Canvas-based charts (no external chart library)
- Full responsive design
- Font Awesome for icons
- Google Fonts for better typography

Let me use a more interesting font than Inter - maybe something like "Rajdhani" or "Titillium Web" which have a more industrial/technical feel.

I'll use:
- Rajdhani for headings (industrial, technical feel)
- Noto Sans SC for Chinese characters
- A dark color scheme with amber/gold accents for the mining theme

Let me create the full HTML with:
- 16:9 locked aspect ratio container
- Top control bar
- 8 metric cards in a 4x2 grid
- A line/bar chart showing distribution
- A detailed data table

I need to generate realistic mining truck data. Let me think about typical values:
- 运行时长: total running hours
- 作业时长: working hours
- 作业循环次数: number of cycles
- 怠速时长: idle time
- 装载时长: loading time
- 卸载时长: unloading time
- 空车返回时间: empty return time
- 重车运行时间: loaded running time

For a mining truck, typical cycle:
- Loading: 3-5 minutes
- Loaded haul: 10-20 minutes
- Unloading: 1-2 minutes
- Empty return: 8-15 minutes
- Total cycle: ~25-40 minutes

Let me create the full dashboard now. I'll use canvas for the charts and make everything responsive.

For the 16:9 aspect ratio, I'll use a container with aspect-ratio: 16/9 or a max-width approach.

Let me build this comprehensively with:
- Canvas-drawn bar chart for distribution
- Canvas-drawn line chart for trends
- Interactive toggle between year/month/day
- Custom date range picker
- Toast notifications
- Export/print functionality

I'll use Google Fonts: Rajdhani for headings and Noto Sans SC for body text.

Let me code this up now.Current date is 2026-04-27. Let me now build the complete dashboard. I'll create a mining-themed industrial dashboard with all the required features.

Key design decisions:
1. Dark industrial theme (bg: #0a0a0a with amber/gold accents #d4a537)
2. Font: Rajdhani for numbers/headings, Noto Sans SC for Chinese
3. 16:9 container with aspect-ratio
4. Canvas-based charts
5. All 8 metrics displayed
6. Year/month/day toggle
7. Date range picker
8. Distribution charts
9. Data table

Let me create a truly comprehensive and visually stunning dashboard.The assets don't have mining-specific imagery but that's fine. I'll use a dark industrial theme with texture overlay and strong typography. I don't need images for this dashboard - it's primarily data-driven.

Now let me build the complete HTML. This will be a comprehensive single-page application.

Let me structure the code:

1. CSS variables for the dark industrial theme
2. 16:9 aspect ratio container
3. Header with controls
4. 8 metric cards (2 rows of 4)
5. Chart area (line chart + bar chart)
6. Distribution table
7. Toast notifications
8. All interactions with JavaScript

I'll use canvas API for charts. Let me create detailed, beautiful charts.

For the 16:9 aspect ratio, I'll set a container with `aspect-ratio: 16/9` and max-width.

Let me code this up now. This will be substantial.
角色
assistant
content

```json{
"summary": "矿车运营数据统计仪表盘,深色工业风格+琥珀色强调,16:9宽屏布局,8项核心指标卡片,Canvas原生折线图与柱状图展示年/月/日分布,可切换时间粒度、选取日期区间、导出打印,底部滚动数据明细表。",
"widget": {
"type": "full-html",
"html": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>矿车生产运营数据统计</title>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link href=\"https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&…\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.c…\">\n <style>\n :root {\n --bg-deep: #080b0f;\n --bg-panel: #11161d;\n --bg-card: #161c26;\n --bg-elevated: #1c2330;\n --border: #232a35;\n --border-active: #3a4455;\n --text-primary: #e8ecf1;\n --text-secondary: #8895a7;\n --text-muted: #5a6577;\n --accent-amber: #e8a820;\n --accent-amber-glow: rgba(232, 168, 32, 0.25);\n --accent-orange: #f47b3b;\n --accent-teal: #2dd4bf;\n --accent-cyan: #22a6e0;\n --accent-red: #f05252;\n --accent-purple: #a78bfa;\n --accent-green: #4ade80;\n --accent-blue: #60a5fa;\n --radius-sm: 4px;\n --radius: 8px;\n --radius-lg: 14px;\n --radius-xl: 20px;\n --shadow-card: 0 2px 12px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.03);\n --shadow-elevated: 0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.06);\n --transition-base: 220ms cubic-bezier(0.4, 0, 0.2, 1);\n --font-display: 'Rajdhani', 'Noto Sans SC', sans-serif;\n --font-body: 'Noto Sans SC', 'Rajdhani', sans-serif;\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n background: var(--bg-deep);\n color: var(--text-primary);\n font-family: var(--font-body);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background-image:\n radial-gradient(ellipse at 30% 20%, rgba(232,168,32,0.04) 0%, transparent 60%),\n radial-gradient(ellipse at 70% 80%, rgba(34,166,224,0.03) 0%, transparent 60%),\n radial-gradient(ellipse at 50% 50%, rgba(255,255,255,0.01) 0%, transparent 100%);\n }\n\n .dashboard {\n width: 100%;\n max-width: 1600px;\n aspect-ratio: 16 / 9;\n max-height: calc(100vh - 32px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n background: var(--bg-panel);\n border-radius: var(--radius-xl);\n padding: 18px 22px;\n box-shadow: var(--shadow-elevated);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n\n .dashboard::before {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background:\n linear-gradient(180deg, rgba(232,168,32,0.03) 0%, transparent 30%),\n linear-gradient(0deg, rgba(34,166,224,0.02) 0%, transparent 40%);\n border-radius: var(--radius-xl);\n z-index: 0;\n }\n\n .dashboard > * { position: relative; z-index: 1; }\n\n /* ============ HEADER ============ */\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-shrink: 0;\n padding-bottom: 10px;\n border-bottom: 1px solid var(--border);\n }\n\n .header-left {\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .logo-mark {\n width: 36px;\n height: 36px;\n background: linear-gradient(135deg, var(--accent-amber), #c47f17);\n border-radius: var(--radius);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n color: #0a0a0a;\n font-weight: 700;\n flex-shrink: 0;\n box-shadow: 0 0 20px var(--accent-amber-glow);\n }\n\n .header-title h1 {\n font-family: var(--font-display);\n font-size: clamp(16px, 1.4vw, 22px);\n font-weight: 700;\n letter-spacing: 0.04em;\n color: var(--text-primary);\n line-height: 1.1;\n }\n\n .header-title span {\n font-size: 11px;\n color: var(--text-muted);\n font-weight: 400;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n }\n\n .header-right {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n justify-content: flex-end;\n }\n\n .btn-group {\n display: flex;\n background: var(--bg-card);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n overflow: hidden;\n }\n\n .btn-group button {\n background: transparent;\n border: none;\n color: var(--text-secondary);\n padding: 7px 14px;\n cursor: pointer;\n font-family: var(--font-display);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.03em;\n transition: all var(--transition-base);\n white-space: nowrap;\n position: relative;\n }\n\n .btn-group button:not(:last-child)::after {\n content: '';\n position: absolute;\n right: 0;\n top: 20%;\n height: 60%;\n width: 1px;\n background: var(--border);\n }\n\n .btn-group button:hover { color: var(--text-primary); background: rgba(255,255,255,0.03); }\n .btn-group button.active {\n color: #0a0a0a;\n background: var(--accent-amber);\n box-shadow: 0 0 14px var(--accent-amber-glow);\n border-radius: 0;\n }\n .btn-group button.active::after { display: none; }\n .btn-group button:first-child.active { border-radius: var(--radius) 0 0 var(--radius); }\n .btn-group button:last-child.active { border-radius: 0 var(--radius) var(--radius) 0; }\n .btn-group button:only-child.active { border-radius: var(--radius); }\n\n .date-input {\n background: var(--bg-card);\n border: 1px solid var(--border);\n color: var(--text-primary);\n padding: 7px 10px;\n border-radius: var(--radius);\n font-family: var(--font-display);\n font-size: 13px;\n outline: none;\n transition: all var(--transition-base);\n width: 130px;\n cursor: pointer;\n }\n\n .date-input:focus { border-color: var(--accent-amber); box-shadow: 0 0 0 3px var(--accent-amber-glow); }\n\n .date-separator {\n color: var(--text-muted);\n font-size: 12px;\n font-weight: 500;\n }\n\n .btn-action {\n background: var(--bg-card);\n border: 1px solid var(--border);\n color: var(--text-secondary);\n width: 34px;\n height: 34px;\n border-radius: var(--radius);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all var(--transition-base);\n font-size: 14px;\n }\n\n .btn-action:hover { color: var(--text-primary); border-color: var(--border-active); background: var(--bg-elevated); }\n\n .btn-primary {\n background: linear-gradient(135deg, var(--accent-amber), #c47f17);\n border: none;\n color: #0a0a0a;\n padding: 7px 18px;\n border-radius: var(--radius);\n cursor: pointer;\n font-family: var(--font-display);\n font-size: 13px;\n font-weight: 700;\n letter-spacing: 0.03em;\n transition: all var(--transition-base);\n white-space: nowrap;\n box-shadow: 0 2px 10px var(--accent-amber-glow);\n }\n\n .btn-primary:hover { filter: brightness(1.1); transform: translateY(-1px); box-shadow: 0 4px 18px var(--accent-amber-glow); }\n .btn-primary:active { transform: scale(0.97); }\n\n /* ============ METRICS ROW ============ */\n .metrics-row {\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 8px;\n flex-shrink: 0;\n }\n\n .metric-card {\n background: var(--bg-card);\n border-radius: var(--radius-lg);\n padding: 12px 14px;\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n transition: all var(--transition-base);\n cursor: default;\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .metric-card:hover {\n border-color: var(--border-active);\n background: var(--bg-elevated);\n transform: translateY(-2px);\n box-shadow: var(--shadow-elevated);\n }\n\n .metric-card .card-indicator {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 2px;\n }\n\n .metric-card:nth-child(1) .card-indicator { background: var(--accent-amber); }\n .metric-card:nth-child(2) .card-indicator { background: var(--accent-cyan); }\n .metric-card:nth-child(3) .card-indicator { background: var(--accent-teal); }\n .metric-card:nth-child(4) .card-indicator { background: var(--accent-orange); }\n .metric-card:nth-child(5) .card-indicator { background: var(--accent-purple); }\n .metric-card:nth-child(6) .card-indicator { background: var(--accent-blue); }\n .metric-card:nth-child(7) .card-indicator { background: var(--accent-green); }\n .metric-card:nth-child(8) .card-indicator { background: var(--accent-red); }\n\n .metric-card .card-icon {\n font-size: 12px;\n color: var(--text-muted);\n margin-bottom: 2px;\n }\n\n .metric-card .card-label {\n font-size: 10px;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n font-weight: 500;\n }\n\n .metric-card .card-value {\n font-family: var(--font-display);\n font-size: clamp(18px, 2vw, 26px);\n font-weight: 700;\n color: var(--text-primary);\n letter-spacing: -0.02em;\n line-height: 1;\n }\n\n .metric-card .card-sub {\n font-size: 10px;\n color: var(--text-muted);\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .metric-card .card-sub .trend-up { color: var(--accent-green); }\n .metric-card .card-sub .trend-down { color: var(--accent-red); }\n\n /* ============ CHARTS AREA ============ */\n .charts-area {\n display: grid;\n grid-template-columns: 1.4fr 1fr;\n gap: 10px;\n flex: 1;\n min-height: 0;\n }\n\n .chart-box {\n background: var(--bg-card);\n border-radius: var(--radius-lg);\n border: 1px solid var(--border);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n .chart-box-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-shrink: 0;\n margin-bottom: 8px;\n }\n\n .chart-box-title {\n font-family: var(--font-display);\n font-size: 13px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--text-secondary);\n text-transform: uppercase;\n }\n\n .chart-legend {\n display: flex;\n gap: 10px;\n font-size: 10px;\n color: var(--text-muted);\n }\n\n .chart-legend span {\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .chart-legend .dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n display: inline-block;\n }\n\n .chart-canvas-wrap {\n flex: 1;\n min-height: 0;\n position: relative;\n width: 100%;\n }\n\n .chart-canvas-wrap canvas {\n width: 100%;\n height: 100%;\n display: block;\n }\n\n /* ============ DATA TABLE ============ */\n .table-section {\n flex-shrink: 0;\n background: var(--bg-card);\n border-radius: var(--radius-lg);\n border: 1px solid var(--border);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n max-height: 160px;\n }\n\n .table-section-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n border-bottom: 1px solid var(--border);\n flex-shrink: 0;\n }\n\n .table-section-title {\n font-family: var(--font-display);\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--text-secondary);\n text-transform: uppercase;\n }\n\n .table-wrap {\n overflow-y: auto;\n flex: 1;\n }\n\n .table-wrap::-webkit-scrollbar { width: 4px; }\n .table-wrap::-webkit-scrollbar-track { background: transparent; }\n .table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\n\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n font-family: var(--font-display);\n }\n\n thead th {\n text-align: left;\n padding: 7px 10px;\n color: var(--text-muted);\n font-weight: 500;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n font-size: 10px;\n position: sticky;\n top: 0;\n background: var(--bg-card);\n z-index: 2;\n border-bottom: 1px solid var(--border);\n }\n\n tbody td {\n padding: 6px 10px;\n border-bottom: 1px solid rgba(255,255,255,0.03);\n color: var(--text-primary);\n font-weight: 500;\n letter-spacing: 0.02em;\n }\n\n tbody tr { transition: background var(--transition-base); }\n tbody tr:hover { background: rgba(255,255,255,0.02); }\n\n .cell-highlight {\n color: var(--accent-amber);\n font-weight: 700;\n }\n\n /* ============ TOAST ============ */\n .toast-container {\n position: fixed;\n top: 20px;\n right: 20px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n pointer-events: none;\n }\n\n .toast {\n background: var(--bg-elevated);\n border: 1px solid var(--border);\n border-left: 3px solid var(--accent-amber);\n color: var(--text-primary);\n padding: 10px 16px;\n border-radius: var(--radius);\n font-size: 12px;\n font-family: var(--font-body);\n box-shadow: var(--shadow-elevated);\n opacity: 0;\n transform: translateX(30px);\n transition: all 280ms cubic-bezier(0.4, 0, 0.2, 1);\n pointer-events: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .toast.show {\n opacity: 1;\n transform: translateX(0);\n }\n\n .toast i { color: var(--accent-amber); font-size: 14px; }\n\n /* ============ RESPONSIVE ============ */\n @media (max-width: 1400px) {\n .metrics-row { grid-template-columns: repeat(4, 1fr); }\n .charts-area { grid-template-columns: 1fr; }\n .dashboard { aspect-ratio: auto; max-height: none; }\n .chart-box { min-height: 220px; }\n }\n\n @media (max-width: 900px) {\n .metrics-row { grid-template-columns: repeat(3, 1fr); }\n .header { flex-direction: column; align-items: stretch; }\n .header-right { justify-content: flex-start; }\n .date-input { width: 100px; }\n .dashboard { padding: 12px; }\n }\n\n @media (max-width: 600px) {\n .metrics-row { grid-template-columns: repeat(2, 1fr); }\n .btn-group button { padding: 6px 10px; font-size: 11px; }\n .date-input { width: 90px; font-size: 11px; }\n .header-right { gap: 4px; }\n table { font-size: 10px; }\n thead th, tbody td { padding: 5px 6px; }\n }\n </style>\n</head>\n<body>\n\n<div class=\"dashboard\" id=\"dashboard\">\n <!-- HEADER -->\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"logo-mark\"><i class=\"fa-solid fa-truck\"></i></div>\n <div class=\"header-title\">\n <h1>矿车运营数据统计</h1>\n <span>Mining Truck Operations Analytics</span>\n </div>\n </div>\n <div class=\"header-right\">\n <div class=\"btn-group\" id=\"granularityGroup\">\n <button data-gran=\"day\" class=\"active\">按日</button>\n <button data-gran=\"month\">按月</button>\n <button data-gran=\"year\">按年</button>\n </div>\n <input type=\"date\" class=\"date-input\" id=\"startDate\" value=\"2026-04-01\">\n <span class=\"date-separator\">至</span>\n <input type=\"date\" class=\"date-input\" id=\"endDate\" value=\"2026-04-27\">\n <button class=\"btn-primary\" onclick=\"refreshData()\"><i class=\"fa-solid fa-arrows-rotate\"></i>&nbsp;刷新</button>\n <button class=\"btn-action\" onclick=\"exportCSV()\" title=\"导出CSV\"><i class=\"fa-solid fa-file-csv\"></i></button>\n <button class=\"btn-action\" onclick=\"printDashboard()\" title=\"打印\"><i class=\"fa-solid fa-print\"></i></button>\n </div>\n </div>\n\n <!-- METRICS -->\n <div class=\"metrics-row\" id=\"metricsRow\">\n <!-- Injected by JS -->\n </div>\n\n <!-- CHARTS -->\n <div class=\"charts-area\">\n <div class=\"chart-box\" id=\"lineChartBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-line\"></i>&nbsp;运行趋势分布</span>\n <div class=\"chart-legend\">\n <span><span class=\"dot\" style=\"background:var(--accent-amber)\"></span>运行时长</span>\n <span><span class=\"dot\" style=\"background:var(--accent-cyan)\"></span>重车运行</span>\n <span><span class=\"dot\" style=\"background:var(--accent-teal)\"></span>作业时长</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"lineChart\"></canvas></div>\n </div>\n <div class=\"chart-box\" id=\"barChartBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-bar\"></i>&nbsp;时长构成分布</span>\n <div class=\"chart-legend\">\n <span><span class=\"dot\" style=\"background:var(--accent-orange)\"></span>装载</span>\n <span><span class=\"dot\" style=\"background:var(--accent-purple)\"></span>卸载</span>\n <span><span class=\"dot\" style=\"background:var(--accent-blue)\"></span>空返</span>\n <span><span class=\"dot\" style=\"background:var(--accent-red)\"></span>怠速</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"barChart\"></canvas></div>\n </div>\n </div>\n\n <!-- TABLE -->\n <div class=\"table-section\">\n <div class=\"table-section-header\">\n <span class=\"table-section-title\"><i class=\"fa-solid fa-table-list\"></i>&nbsp;数据明细</span>\n <span style=\"font-size:10px;color:var(--text-muted)\" id=\"tableInfo\">共 0 条记录</span>\n </div>\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>周期</th>\n <th>运行时长(h)</th>\n <th>作业时长(h)</th>\n <th>循环次数</th>\n <th>怠速(h)</th>\n <th>装载(h)</th>\n <th>卸载(h)</th>\n <th>空返(h)</th>\n <th>重车(h)</th>\n </tr>\n </thead>\n <tbody id=\"tableBody\">\n <!-- Injected by JS -->\n </tbody>\n </table>\n </div>\n </div>\n</div>\n\n<div class=\"toast-container\" id=\"toastContainer\"></div>\n\n<script>\n(function() {\n 'use strict';\n\n // ============ STATE ============\n const state = {\n granularity: 'day',\n startDate: '2026-04-01',\n endDate: '2026-04-27',\n };\n\n // ============ DOM REFS ============\n const $ = (sel) => document.querySelector(sel);\n const $$ = (sel) => document.querySelectorAll(sel);\n\n const dom = {\n granularityGroup: $('#granularityGroup'),\n startDate: $('#startDate'),\n endDate: $('#endDate'),\n metricsRow: $('#metricsRow'),\n lineChart: $('#lineChart'),\n barChart: $('#barChart'),\n tableBody: $('#tableBody'),\n tableInfo: $('#tableInfo'),\n toastContainer: $('#toastContainer'),\n lineChartBox: $('#lineChartBox'),\n barChartBox: $('#barChartBox'),\n };\n\n // ============ TOAST ============\n function showToast(msg, icon) {\n const el = document.createElement('div');\n el.className = 'toast';\n el.innerHTML = `<i class=\"fa-solid ${icon || 'fa-circle-check'}\"></i>${msg}`;\n dom.toastContainer.appendChild(el);\n requestAnimationFrame(() => el.classList.add('show'));\n setTimeout(() => {\n el.classList.remove('show');\n setTimeout(() => el.remove(), 350);\n }, 2200);\n }\n\n // ============ DATA GENERATION ============\n function seedRandom(seed) {\n let s = seed;\n return function() {\n s = (s * 16807 + 0) % 2147483647;\n return (s - 1) / 2147483646;\n };\n }\n\n function generateRecords(granularity, start, end) {\n const records = [];\n const startD = new Date(start);\n const endD = new Date(end);\n\n if (isNaN(startD.getTime()) || isNaN(endD.getTime()) || startD > endD) {\n return records;\n }\n\n let labels = [];\n let current = new Date(startD);\n\n if (granularity === 'day') {\n while (current <= endD) {\n labels.push(new Date(current));\n current.setDate(current.getDate() + 1);\n }\n } else if (granularity === 'month') {\n current = new Date(startD.getFullYear(), startD.getMonth(), 1);\n const endMonth = new Date(endD.getFullYear(), endD.getMonth(), 1);\n while (current <= endMonth) {\n labels.push(new Date(current));\n current.setMonth(current.getMonth() + 1);\n }\n } else {\n current = new Date(startD.getFullYear(), 0, 1);\n const endYear = new Date(endD.getFullYear(), 0, 1);\n while (current <= endYear) {\n labels.push(new Date(current));\n current.setFullYear(current.getFullYear() + 1);\n }\n }\n\n // Cap records for performance\n if (labels.length > 120) {\n const step = Math.ceil(labels.length / 100);\n labels = labels.filter((_, i) => i % step === 0);\n }\n\n labels.forEach((d, i) => {\n const rng = seedRandom(d.getTime() % 100000 + i * 73);\n // Base values for a mining truck fleet\n const cyclesBase = granularity === 'year' ? 4500 + rng() * 3000 : granularity === 'month' ? 380 + rng() * 250 : 15 + rng() * 12;\n const cycles = Math.round(cyclesBase);\n\n const idlePerCycle = 0.08 + rng() * 0.12;\n const loadPerCycle = 0.10 + rng() * 0.08;\n const unloadPerCycle = 0.03 + rng() * 0.04;\n const emptyReturnPerCycle = 0.12 + rng() * 0.10;\n const loadedRunPerCycle = 0.18 + rng() * 0.14;\n\n const idleTime = Math.round(cycles * idlePerCycle * 10) / 10;\n const loadTime = Math.round(cycles * loadPerCycle * 10) / 10;\n const unloadTime = Math.round(cycles * unloadPerCycle * 10) / 10;\n const emptyReturnTime = Math.round(cycles * emptyReturnPerCycle * 10) / 10;\n const loadedRunTime = Math.round(cycles * loadedRunPerCycle * 10) / 10;\n const workTime = Math.round((loadTime + unloadTime + loadedRunTime) * 10) / 10;\n const totalTime = Math.round((workTime + idleTime + emptyReturnTime) * 10) / 10;\n\n records.push({\n label: d,\n totalTime,\n workTime,\n cycles,\n idleTime,\n loadTime,\n unloadTime,\n emptyReturnTime,\n loadedRunTime,\n });\n });\n\n return records;\n }\n\n function formatLabel(d, granularity) {\n if (granularity === 'day') {\n return `${d.getMonth() + 1}/${d.getDate()}`;\n } else if (granularity === 'month') {\n return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}`;\n } else {\n return `${d.getFullYear()}`;\n }\n }\n\n function formatFullLabel(d, granularity) {\n if (granularity === 'day') {\n return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n } else if (granularity === 'month') {\n return `${d.getFullYear()}年${d.getMonth() + 1}月`;\n } else {\n return `${d.getFullYear()}年`;\n }\n }\n\n // ============ RENDER METRICS ============\n function renderMetrics(records) {\n if (records.length === 0) {\n const html = Array.from({ length: 8 }, () => `\n <div class=\"metric-card\">\n <div class=\"card-indicator\"></div>\n <div class=\"card-label\">--</div>\n <div class=\"card-value\">0</div>\n </div>`).join('');\n dom.metricsRow.innerHTML = html;\n return;\n }\n\n const sums = records.reduce((acc, r) => ({\n totalTime: acc.totalTime + r.totalTime,\n workTime: acc.workTime + r.workTime,\n cycles: acc.cycles + r.cycles,\n idleTime: acc.idleTime + r.idleTime,\n loadTime: acc.loadTime + r.loadTime,\n unloadTime: acc.unloadTime + r.unloadTime,\n emptyReturnTime: acc.emptyReturnTime + r.emptyReturnTime,\n loadedRunTime: acc.loadedRunTime + r.loadedRunTime,\n }), { totalTime: 0, workTime: 0, cycles: 0, idleTime: 0, loadTime: 0, unloadTime: 0, emptyReturnTime: 0, loadedRunTime: 0 });\n\n const cfg = [\n { icon: 'fa-clock', label: '运行时长', value: sums.totalTime.toFixed(1), unit: 'h', color: 'var(--accent-amber)' },\n { icon: 'fa-gears', label: '作业时长', value: sums.workTime.toFixed(1), unit: 'h', color: 'var(--accent-cyan)' },\n { icon: 'fa-rotate', label: '循环次数', value: sums.cycles.toString(), unit: '次', color: 'var(--accent-teal)' },\n { icon: 'fa-pause', label: '怠速时长', value: sums.idleTime.toFixed(1), unit: 'h', color: 'var(--accent-orange)' },\n { icon: 'fa-arrow-down', label: '装载时长', value: sums.loadTime.toFixed(1), unit: 'h', color: 'var(--accent-purple)' },\n { icon: 'fa-arrow-up', label: '卸载时长', value: sums.unloadTime.toFixed(1), unit: 'h', color: 'var(--accent-blue)' },\n { icon: 'fa-truck-fast', label: '空车返回', value: sums.emptyReturnTime.toFixed(1), unit: 'h', color: 'var(--accent-green)' },\n { icon: 'fa-weight-hanging', label: '重车运行', value: sums.loadedRunTime.toFixed(1), unit: 'h', color: 'var(--accent-red)' },\n ];\n\n dom.metricsRow.innerHTML = cfg.map(c => `\n <div class=\"metric-card\">\n <div class=\"card-indicator\"></div>\n <div class=\"card-icon\"><i class=\"fa-solid ${c.icon}\"></i></div>\n <div class=\"card-label\">${c.label}</div>\n <div class=\"card-value\" style=\"color:${c.color}\">${c.value}</div>\n <div class=\"card-sub\">${c.unit}</div>\n </div>\n `).join('');\n }\n\n // ============ CANVAS CHARTS ============\n function renderLineChart(canvas, records, granularity) {\n if (!canvas || records.length === 0) return;\n const dpr = window.devicePixelRatio || 1;\n const rect = canvas.parentElement.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n canvas.width = w * dpr;\n canvas.height = h * dpr;\n canvas.style.width = w + 'px';\n canvas.style.height = h + 'px';\n const ctx = canvas.getContext('2d');\n ctx.scale(dpr, dpr);\n\n const pad = { top: 16, right: 20, bottom: 30, left: 50 };\n const cw = w - pad.left - pad.right;\n const ch = h - pad.top - pad.bottom;\n\n // Background\n ctx.fillStyle = 'transparent';\n ctx.fillRect(0, 0, w, h);\n\n // Data\n const datasets = [\n { key: 'totalTime', color: '#e8a820', label: '运行时长' },\n { key: 'loadedRunTime', color: '#22a6e0', label: '重车运行' },\n { key: 'workTime', color: '#2dd4bf', label: '作业时长' },\n ];\n\n let maxVal = 0;\n datasets.forEach(ds => {\n records.forEach(r => { if (r[ds.key] > maxVal) maxVal = r[ds.key]; });\n });\n maxVal = Math.ceil(maxVal * 1.15);\n if (maxVal === 0) maxVal = 10;\n\n // Grid\n const gridLines = 5;\n ctx.strokeStyle = 'rgba(255,255,255,0.05)';\n ctx.lineWidth = 1;\n for (let i = 0; i <= gridLines; i++) {\n const y = pad.top + (ch / gridLines) * i;\n ctx.beginPath();\n ctx.moveTo(pad.left, y);\n ctx.lineTo(w - pad.right, y);\n ctx.stroke();\n\n ctx.fillStyle = '#5a6577';\n ctx.font = '10px Rajdhani, sans-serif';\n ctx.textAlign = 'right';\n ctx.fillText(Math.round(maxVal - (maxVal / gridLines) * i), pad.left - 6, y + 4);\n }\n\n // X labels\n const xStep = cw / Math.max(records.length - 1, 1);\n const maxLabels = Math.min(records.length, 12);\n const labelStep = Math.max(1, Math.floor(records.length / maxLabels));\n ctx.fillStyle = '#5a6577';\n ctx.font = '10px Rajdhani, sans-serif';\n ctx.textAlign = 'center';\n records.forEach((r, i) => {\n if (i % labelStep === 0 || i === records.length - 1) {\n const x = pad.left + xStep * i;\n ctx.fillText(formatLabel(r.label, granularity), x, pad.top + ch + 16);\n }\n });\n\n // Draw lines\n datasets.forEach(ds => {\n ctx.strokeStyle = ds.color;\n ctx.lineWidth = 2;\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n ctx.beginPath();\n records.forEach((r, i) => {\n const x = pad.left + xStep * i;\n const y = pad.top + ch - (r[ds.key] / maxVal) * ch;\n if (i === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n });\n ctx.stroke();\n\n // Dots\n records.forEach((r, i) => {\n const x = pad.left + xStep * i;\n const y = pad.top + ch - (r[ds.key] / maxVal) * ch;\n ctx.fillStyle = ds.color;\n ctx.beginPath();\n ctx.arc(x, y, 3, 0, Math.PI * 2);\n ctx.fill();\n });\n });\n }\n\n function renderBarChart(canvas, records, granularity) {\n if (!canvas || records.length === 0) return;\n const dpr = window.devicePixelRatio || 1;\n const rect = canvas.parentElement.getBoundingClientRect();\n const w = rect.width;\n const h = rect.height;\n canvas.width = w * dpr;\n canvas.height = h * dpr;\n canvas.style.width = w + 'px';\n canvas.style.height = h + 'px';\n const ctx = canvas.getContext('2d');\n ctx.scale(dpr, dpr);\n\n const pad = { top: 16, right: 20, bottom: 30, left: 48 };\n const cw = w - pad.left - pad.right;\n const ch = h - pad.top - pad.bottom;\n\n const datasets = [\n { key: 'loadTime', color: '#f47b3b', label: '装载' },\n { key: 'unloadTime', color: '#a78bfa', label: '卸载' },\n { key: 'emptyReturnTime', color: '#4ade80', label: '空返' },\n { key: 'idleTime', color: '#f05252', label: '怠速' },\n ];\n\n let maxStack = 0;\n records.forEach(r => {\n const stack = datasets.reduce((s, ds) => s + r[ds.key], 0);\n if (stack > maxStack) maxStack = stack;\n });\n maxStack = Math.ceil(maxStack * 1.2);\n if (maxStack === 0) maxStack = 10;\n\n // Grid\n const gridLines = 4;\n ctx.strokeStyle = 'rgba(255,255,255,0.05)';\n ctx.lineWidth = 1;\n for (let i = 0; i <= gridLines; i++) {\n const y = pad.top + (ch / gridLines) * i;\n ctx.beginPath();\n ctx.moveTo(pad.left, y);\n ctx.lineTo(w - pad.right, y);\n ctx.stroke();\n ctx.fillStyle = '#5a6577';\n ctx.font = '10px Rajdhani, sans-serif';\n ctx.textAlign = 'right';\n ctx.fillText(Math.round(maxStack - (maxStack / gridLines) * i), pad.left - 6, y + 4);\n }\n\n // Max visible bars\n const maxBars = Math.min(records.length, 24);\n const step = Math.max(1, Math.floor(records.length / maxBars));\n const displayRecords = records.filter((_, i) => i % step === 0 || i === records.length - 1);\n const barGap = 2;\n const barTotalWidth = (cw / displayRecords.length) - barGap;\n if (barTotalWidth <= 0) return;\n\n displayRecords.forEach((r, di) => {\n const x = pad.left + (cw / displayRecords.length) * di + barGap / 2;\n let stackY = pad.top + ch;\n datasets.forEach(ds => {\n const val = r[ds.key];\n const barH = (val / maxStack) * ch;\n const y = stackY - barH;\n ctx.fillStyle = ds.color;\n ctx.fillRect(x, y, barTotalWidth, barH);\n stackY = y;\n });\n\n // X label\n const labelStep2 = Math.max(1, Math.floor(displayRecords.length / 8));\n if (di % labelStep2 === 0 || di === displayRecords.length - 1) {\n ctx.fillStyle = '#5a6577';\n ctx.font = '10px Rajdhani, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(formatLabel(r.label, granularity), x + barTotalWidth / 2, pad.top + ch + 16);\n }\n });\n }\n\n // ============ RENDER TABLE ============\n function renderTable(records, granularity) {\n dom.tableInfo.textContent = `共 ${records.length} 条记录`;\n if (records.length === 0) {\n dom.tableBody.innerHTML = '<tr><td colspan=\"9\" style=\"text-align:center;color:var(--text-muted);padding:20px;\">暂无数据</td></tr>';\n return;\n }\n\n const display = records.slice(-30);\n dom.tableBody.innerHTML = display.map(r => `\n <tr>\n <td>${formatFullLabel(r.label, granularity)}</td>\n <td class=\"cell-highlight\">${r.totalTime.toFixed(1)}</td>\n <td>${r.workTime.toFixed(1)}</td>\n <td>${r.cycles}</td>\n <td>${r.idleTime.toFixed(1)}</td>\n <td>${r.loadTime.toFixed(1)}</td>\n <td>${r.unloadTime.toFixed(1)}</td>\n <td>${r.emptyReturnTime.toFixed(1)}</td>\n <td>${r.loadedRunTime.toFixed(1)}</td>\n </tr>\n `).join('');\n }\n\n // ============ MAIN UPDATE ============\n let currentRecords = [];\n\n function updateDashboard(silent) {\n const records = generateRecords(state.granularity, state.startDate, state.endDate);\n currentRecords = records;\n\n renderMetrics(records);\n renderLineChart(dom.lineChart, records, state.granularity);\n renderBarChart(dom.barChart, records, state.granularity);\n renderTable(records, state.granularity);\n\n if (!silent) {\n showToast(`已加载 ${records.length} 条${state.granularity === 'day' ? '日' : state.granularity === 'month' ? '月' : '年'}度统计数据`, 'fa-circle-check');\n }\n }\n\n // ============ EVENT HANDLERS ============\n dom.granularityGroup.addEventListener('click', (e) => {\n const btn = e.target.closest('button');\n if (!btn) return;\n dom.granularityGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n state.granularity = btn.dataset.gran;\n\n // Adjust date range based on granularity\n const now = new Date();\n if (state.granularity === 'day') {\n state.startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n state.endDate = now.toISOString().slice(0, 10);\n } else if (state.granularity === 'month') {\n state.startDate = new Date(now.getFullYear() - 1, now.getMonth(), 1).toISOString().slice(0, 10);\n state.endDate = now.toISOString().slice(0, 10);\n } else {\n state.startDate = new Date(now.getFullYear() - 4, 0, 1).toISOString().slice(0, 10);\n state.endDate = now.toISOString().slice(0, 10);\n }\n dom.startDate.value = state.startDate;\n dom.endDate.value = state.endDate;\n updateDashboard();\n });\n\n dom.startDate.addEventListener('change', () => {\n state.startDate = dom.startDate.value;\n updateDashboard();\n });\n\n dom.endDate.addEventListener('change', () => {\n state.endDate = dom.endDate.value;\n updateDashboard();\n });\n\n // ============ GLOBAL FUNCTIONS ============\n window.refreshData = function() {\n state.startDate = dom.startDate.value;\n state.endDate = dom.endDate.value;\n updateDashboard();\n };\n\n window.exportCSV = function() {\n if (currentRecords.length === 0) {\n showToast('无数据可导出', 'fa-triangle-exclamation');\n return;\n }\n let csv = '周期,运行时长(h),作业时长(h),循环次数,怠速时长(h),装载时长(h),卸载时长(h),空车返回(h),重车运行(h)\\n';\n currentRecords.forEach(r => {\n csv += [\n formatFullLabel(r.label, state.granularity),\n r.totalTime.toFixed(1),\n r.workTime.toFixed(1),\n r.cycles,\n r.idleTime.toFixed(1),\n r.loadTime.toFixed(1),\n r.unloadTime.toFixed(1),\n r.emptyReturnTime.toFixed(1),\n r.loadedRunTime.toFixed(1),\n ].join(',') + '\\n';\n });\n const blob = new Blob(['\\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `矿车运营数据_${state.granularity}_${state.startDate}_${state.endDate}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n showToast('CSV 文件已下载', 'fa-file-csv');\n };\n\n window.printDashboard = function() {\n showToast('正在准备打印...', 'fa-print');\n setTimeout(() => window.print(), 400);\n };\n\n // ============ RESIZE ============\n let resizeTid;\n window.addEventListener('resize', () => {\n clearTimeout(resizeTid);\n resizeTid = setTimeout(() => {\n if (currentRecords.length > 0) {\n renderLineChart(dom.lineChart, currentRecords, state.granularity);\n renderBarChart(dom.barChart, currentRecords, state.granularity);\n }\n }, 250);\n });\n\n // ============ INIT ============\n function init() {\n const now = new Date();\n state.startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);\n state.endDate = now.toISOString().slice(0, 10);\n dom.startDate.value = state.startDate;\n dom.endDate.value = state.endDate;\n updateDashboard(true);\n }\n\n init();\n})();\n</script>\n\n</body>\n</html>"
},
"next": ["按矿车编号筛选数据", "添加实时数据看板", "生成PDF运营报告", "对比不同矿区数据", "设置异常阈值告警"],
"breakpoint": "none"
}
```

total_tokens
22027
summary
矿车运营数据统计仪表盘,深色工业风格+琥珀色强调,16:9宽屏布局,8项核心指标卡片,Canvas原生折线图与柱状图展示年/月/日分布,可切换时间粒度、选取日期区间、导出打印,底部滚动数据明细表。
uiParsing
关闭