```json
{
"summary": "矿车运营数据统计仪表盘,全分组对比+时间横轴版,8项核心指标,折线趋势(分组多线+时间横轴)、堆叠柱状(时间横轴各周期构成)、横向占比条(分组对比),支持矿区/车辆类型/车辆三级联动筛选,年/月/日粒度切换,CSV导出与打印。",
"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=DM+Sans:opsz,wght@9..40,400;9…\" 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-page: #f5f6f8;\n --bg-panel: #fdfdfd;\n --bg-card: #ffffff;\n --bg-elevated: #fafbfc;\n --bg-stripe: #f8f9fb;\n --border: #e5e8ed;\n --border-light: #eff1f5;\n --border-active: #c9ced7;\n --text-primary: #0f172a;\n --text-secondary: #475569;\n --text-muted: #94a3b8;\n --accent-amber: #d4891a;\n --accent-cyan: #0d82b8;\n --accent-teal: #0f766e;\n --accent-orange: #ea570c;\n --accent-purple: #7c3aed;\n --accent-blue: #2563eb;\n --accent-green: #16a34a;\n --accent-red: #dc2626;\n --radius-sm: 4px;\n --radius: 8px;\n --radius-md: 10px;\n --radius-lg: 14px;\n --radius-xl: 18px;\n --shadow-xs: 0 1px 2px rgba(0,0,0,0.03);\n --shadow-sm: 0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03);\n --shadow-card: 0 2px 8px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.02);\n --shadow-elevated: 0 8px 28px rgba(0,0,0,0.07), 0 2px 6px rgba(0,0,0,0.03);\n --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);\n --font-display: 'DM Sans', 'Noto Sans SC', sans-serif;\n --font-body: 'Noto Sans SC', 'DM Sans', sans-serif;\n }\n\n * { box-sizing: border-box; margin: 0; padding: 0; }\n\n body {\n background: var(--bg-page);\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: 10px;\n background-image:\n radial-gradient(ellipse at 15% 10%, rgba(212,137,26,0.03) 0%, transparent 55%),\n radial-gradient(ellipse at 85% 90%, rgba(13,130,184,0.02) 0%, transparent 55%);\n }\n\n .dashboard {\n width: 100%;\n max-width: 1600px;\n aspect-ratio: 16 / 9;\n max-height: calc(100vh - 20px);\n display: flex;\n flex-direction: column;\n gap: 5px;\n background: var(--bg-panel);\n border-radius: var(--radius-xl);\n padding: 10px 14px;\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(212,137,26,0.012) 0%, transparent 25%),\n linear-gradient(0deg, rgba(13,130,184,0.008) 0%, transparent 35%);\n border-radius: var(--radius-xl);\n z-index: 0;\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: 8px;\n flex-shrink: 0;\n padding-bottom: 4px;\n border-bottom: 1px solid var(--border-light);\n }\n .header-left { display: flex; align-items: center; gap: 8px; }\n .logo-mark {\n width: 30px; height: 30px;\n background: linear-gradient(135deg, #d4891a, #b07214);\n border-radius: var(--radius);\n display: flex; align-items: center; justify-content: center;\n font-size: 14px; color: #ffffff; font-weight: 700; flex-shrink: 0;\n box-shadow: 0 2px 8px rgba(212,137,26,0.22);\n }\n .header-title h1 {\n font-family: var(--font-display);\n font-size: clamp(12px, 1vw, 17px);\n font-weight: 700; letter-spacing: -0.01em;\n color: var(--text-primary); line-height: 1.1;\n }\n .header-title span {\n font-size: 8px; color: var(--text-muted); font-weight: 500;\n letter-spacing: 0.06em; text-transform: uppercase;\n }\n .header-right {\n display: flex; align-items: center; gap: 4px;\n flex-wrap: wrap; justify-content: flex-end;\n }\n\n .btn-group {\n display: flex; background: var(--bg-card);\n border-radius: var(--radius); border: 1px solid var(--border);\n overflow: hidden; box-shadow: var(--shadow-xs);\n }\n .btn-group button {\n background: transparent; border: none; color: var(--text-secondary);\n padding: 4px 9px; cursor: pointer;\n font-family: var(--font-display); font-size: 10px; font-weight: 600;\n letter-spacing: 0.01em; transition: all var(--transition-base);\n white-space: nowrap; position: relative;\n }\n .btn-group button:not(:last-child)::after {\n content: ''; position: absolute; right: 0; top: 14%;\n height: 72%; width: 1px; background: var(--border);\n }\n .btn-group button:hover { color: var(--text-primary); background: var(--bg-elevated); }\n .btn-group button.active {\n color: #ffffff; background: #d4891a;\n box-shadow: 0 0 8px rgba(212,137,26,0.2); 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\n select, .date-input {\n background: var(--bg-card); border: 1px solid var(--border);\n color: var(--text-primary); padding: 4px 6px;\n border-radius: var(--radius); font-family: var(--font-display);\n font-size: 10px; outline: none; transition: all var(--transition-base);\n cursor: pointer; box-shadow: var(--shadow-xs);\n }\n select:focus, .date-input:focus { border-color: #d4891a; box-shadow: 0 0 0 2px rgba(212,137,26,0.1); }\n .date-input { width: 100px; }\n select { min-width: 68px; }\n .date-separator { color: var(--text-muted); font-size: 10px; font-weight: 500; }\n .filter-label {\n font-size: 7px; color: var(--text-muted); font-weight: 600;\n text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap;\n }\n .btn-action {\n background: var(--bg-card); border: 1px solid var(--border);\n color: var(--text-secondary); width: 26px; height: 26px;\n border-radius: var(--radius); cursor: pointer;\n display: flex; align-items: center; justify-content: center;\n transition: all var(--transition-base); font-size: 10px; box-shadow: var(--shadow-xs);\n }\n .btn-action:hover { color: var(--text-primary); border-color: var(--border-active); background: var(--bg-elevated); }\n .btn-primary {\n background: linear-gradient(135deg, #d4891a, #b07214); border: none;\n color: #ffffff; padding: 4px 12px; border-radius: var(--radius);\n cursor: pointer; font-family: var(--font-display); font-size: 10px;\n font-weight: 600; letter-spacing: 0.01em; transition: all var(--transition-base);\n white-space: nowrap; box-shadow: 0 2px 6px rgba(212,137,26,0.2);\n }\n .btn-primary:hover { filter: brightness(1.07); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(212,137,26,0.26); }\n .btn-primary:active { transform: scale(0.97); }\n\n /* ============ FILTER BAR ============ */\n .filter-bar {\n display: flex; align-items: center; gap: 5px;\n flex-shrink: 0; flex-wrap: wrap; padding: 3px 0;\n }\n .filter-bar .sep { width: 1px; height: 18px; background: var(--border); margin: 0 1px; }\n\n /* ============ METRICS ROW ============ */\n .metrics-row {\n display: grid; grid-template-columns: repeat(8, 1fr);\n gap: 4px; flex-shrink: 0;\n }\n .metric-card {\n background: var(--bg-card); border-radius: var(--radius-md);\n padding: 6px 8px; border: 1px solid var(--border-light);\n position: relative; overflow: hidden; transition: all var(--transition-base);\n cursor: default; display: flex; flex-direction: column; gap: 1px; box-shadow: var(--shadow-sm);\n }\n .metric-card:hover { border-color: var(--border-active); box-shadow: var(--shadow-elevated); transform: translateY(-1px); }\n .metric-card .card-accent { position: absolute; top: 0; left: 0; width: 100%; height: 2px; }\n .metric-card:nth-child(1) .card-accent { background: #d4891a; }\n .metric-card:nth-child(2) .card-accent { background: #0d82b8; }\n .metric-card:nth-child(3) .card-accent { background: #0f766e; }\n .metric-card:nth-child(4) .card-accent { background: #ea570c; }\n .metric-card:nth-child(5) .card-accent { background: #7c3aed; }\n .metric-card:nth-child(6) .card-accent { background: #2563eb; }\n .metric-card:nth-child(7) .card-accent { background: #16a34a; }\n .metric-card:nth-child(8) .card-accent { background: #dc2626; }\n .metric-card .card-icon { font-size: 7px; color: var(--text-muted); }\n .metric-card .card-label { font-size: 7px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }\n .metric-card .card-value { font-family: var(--font-display); font-size: clamp(11px, 1.2vw, 17px); font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; line-height: 1; }\n .metric-card .card-sub { font-size: 6px; color: var(--text-muted); font-weight: 500; }\n\n /* ============ CHARTS AREA ============ */\n .charts-area {\n display: grid; grid-template-columns: 1.25fr 0.85fr 0.9fr;\n gap: 5px; flex: 1; min-height: 0;\n }\n .chart-box {\n background: var(--bg-card); border-radius: var(--radius-md);\n border: 1px solid var(--border-light); padding: 8px 10px;\n display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-sm);\n }\n .chart-box-header {\n display: flex; align-items: center; justify-content: space-between;\n flex-shrink: 0; margin-bottom: 3px; gap: 4px;\n }\n .chart-box-title {\n font-family: var(--font-display); font-size: 9px; font-weight: 600;\n letter-spacing: 0.02em; color: var(--text-secondary);\n text-transform: uppercase; white-space: nowrap;\n }\n .chart-legend {\n display: flex; gap: 4px; font-size: 7px; color: var(--text-muted);\n flex-wrap: wrap; justify-content: flex-end;\n }\n .chart-legend span { display: flex; align-items: center; gap: 2px; white-space: nowrap; }\n .chart-legend .dot { width: 4px; height: 4px; border-radius: 50%; display: inline-block; flex-shrink: 0; }\n .chart-canvas-wrap { flex: 1; min-height: 0; position: relative; width: 100%; }\n .chart-canvas-wrap canvas { width: 100%; height: 100%; display: block; }\n\n /* ============ RATIO PANEL ============ */\n .ratio-panel {\n background: var(--bg-card); border-radius: var(--radius-md);\n border: 1px solid var(--border-light); padding: 8px 10px;\n display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-sm);\n }\n .ratio-header { flex-shrink: 0; margin-bottom: 2px; }\n .ratio-body {\n flex: 1; display: flex; flex-direction: column; gap: 3px;\n min-height: 0; overflow-y: auto;\n }\n .ratio-body::-webkit-scrollbar { width: 3px; }\n .ratio-body::-webkit-scrollbar-track { background: transparent; }\n .ratio-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\n .ratio-group-row { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }\n .ratio-group-name {\n font-family: var(--font-display); font-size: 8px; font-weight: 600;\n color: var(--text-secondary); width: 52px; text-align: right;\n flex-shrink: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n }\n .ratio-hbar-wrap { flex: 1; height: 10px; background: #eef0f4; border-radius: 3px; overflow: hidden; display: flex; }\n .ratio-hbar-wrap .seg-load { background: #ea570c; height: 100%; }\n .ratio-hbar-wrap .seg-haul { background: #2563eb; height: 100%; }\n .ratio-hbar-wrap .seg-unload { background: #7c3aed; height: 100%; }\n .ratio-group-pct { font-family: var(--font-display); font-size: 8px; font-weight: 700; color: var(--text-primary); width: 30px; text-align: right; flex-shrink: 0; }\n .ratio-legend-row {\n display: flex; gap: 8px; font-size: 7px; color: var(--text-muted);\n flex-shrink: 0; padding-bottom: 2px; border-bottom: 1px solid var(--border-light);\n }\n .ratio-legend-row span { display: flex; align-items: center; gap: 2px; }\n .ratio-legend-row .dot { width: 5px; height: 5px; border-radius: 2px; display: inline-block; flex-shrink: 0; }\n\n /* ============ TABLE ============ */\n .table-section {\n flex-shrink: 0; background: var(--bg-card); border-radius: var(--radius-md);\n border: 1px solid var(--border-light); overflow: hidden;\n display: flex; flex-direction: column; max-height: 95px; box-shadow: var(--shadow-sm);\n }\n .table-section-header {\n display: flex; align-items: center; justify-content: space-between;\n padding: 3px 10px; border-bottom: 1px solid var(--border-light); flex-shrink: 0;\n }\n .table-section-title { font-family: var(--font-display); font-size: 9px; font-weight: 600; letter-spacing: 0.03em; color: var(--text-secondary); text-transform: uppercase; }\n .table-wrap { overflow-y: auto; flex: 1; }\n .table-wrap::-webkit-scrollbar { width: 3px; }\n .table-wrap::-webkit-scrollbar-track { background: transparent; }\n .table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }\n table { width: 100%; border-collapse: collapse; font-size: 8px; font-family: var(--font-display); }\n thead th { text-align: left; padding: 3px 5px; color: var(--text-muted); font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; font-size: 7px; position: sticky; top: 0; background: var(--bg-card); z-index: 2; border-bottom: 1px solid var(--border-light); }\n tbody td { padding: 3px 5px; border-bottom: 1px solid rgba(0,0,0,0.03); color: var(--text-primary); font-weight: 500; letter-spacing: 0.01em; }\n tbody tr:nth-child(even) { background: var(--bg-stripe); }\n tbody tr:hover { background: rgba(212,137,26,0.03); }\n tbody tr { transition: background var(--transition-base); }\n .cell-highlight { color: #d4891a; font-weight: 700; }\n .cell-cycles { color: #0f766e; font-weight: 700; }\n\n /* ============ TOAST ============ */\n .toast-container { position: fixed; top: 14px; right: 14px; z-index: 9999; display: flex; flex-direction: column; gap: 5px; pointer-events: none; }\n .toast {\n background: #ffffff; border: 1px solid var(--border); border-left: 3px solid #d4891a;\n color: var(--text-primary); padding: 7px 12px; border-radius: var(--radius);\n font-size: 11px; font-family: var(--font-body); box-shadow: 0 4px 20px rgba(0,0,0,0.1);\n opacity: 0; transform: translateX(30px); transition: all 280ms cubic-bezier(0.4, 0, 0.2, 1);\n pointer-events: auto; display: flex; align-items: center; gap: 5px;\n }\n .toast.show { opacity: 1; transform: translateX(0); }\n .toast i { color: #d4891a; font-size: 12px; }\n\n @media (max-width: 1400px) {\n .metrics-row { grid-template-columns: repeat(4, 1fr); }\n .charts-area { grid-template-columns: 1fr 1fr; }\n .ratio-panel { grid-column: span 2; }\n .dashboard { aspect-ratio: auto; max-height: none; }\n .chart-box { min-height: 170px; }\n .ratio-panel { min-height: 140px; }\n }\n @media (max-width: 900px) {\n .metrics-row { grid-template-columns: repeat(3, 1fr); }\n .charts-area { grid-template-columns: 1fr; }\n .ratio-panel { grid-column: span 1; }\n .header { flex-direction: column; align-items: stretch; }\n .header-right { justify-content: flex-start; }\n select, .date-input { font-size: 9px; padding: 3px 5px; }\n .date-input { width: 80px; }\n .dashboard { padding: 6px; }\n .chart-box { min-height: 150px; }\n }\n @media (max-width: 600px) {\n .metrics-row { grid-template-columns: repeat(2, 1fr); }\n .btn-group button { padding: 3px 6px; font-size: 9px; }\n .header-right { gap: 2px; }\n table { font-size: 7px; }\n thead th, tbody td { padding: 2px 3px; }\n .filter-bar { gap: 2px; }\n .filter-bar .sep { display: none; }\n }\n @media print {\n body { background: #fff; padding: 0; }\n .dashboard { box-shadow: none; border: none; aspect-ratio: auto; max-height: none; }\n .btn-primary, .btn-action, .btn-group, .filter-bar select, .filter-bar .date-input { display: none; }\n .toast-container { display: none; }\n }\n </style>\n</head>\n<body>\n\n<div class=\"dashboard\" id=\"dashboard\">\n\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\"><h1>矿车运营数据统计</h1><span>Mining Truck Operations</span></div>\n </div>\n <div class=\"header-right\">\n <span class=\"filter-label\">横轴</span>\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> 刷新</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 <!-- FILTER BAR -->\n <div class=\"filter-bar\">\n <span class=\"filter-label\">分组</span>\n <div class=\"btn-group\" id=\"groupByGroup\">\n <button data-group=\"mine\" class=\"active\">按矿区</button>\n <button data-group=\"type\">按车辆类型</button>\n <button data-group=\"vehicle\">按车辆</button>\n </div>\n <span class=\"sep\"></span>\n <span class=\"filter-label\">矿区</span>\n <select id=\"filterMine\"><option value=\"\">全部矿区</option></select>\n <span class=\"filter-label\">车辆类型</span>\n <select id=\"filterType\"><option value=\"\">全部类型</option></select>\n <span class=\"filter-label\">车辆</span>\n <select id=\"filterVehicle\"><option value=\"\">全部车辆</option></select>\n </div>\n\n <!-- METRICS ROW -->\n <div class=\"metrics-row\" id=\"metricsRow\"></div>\n\n <!-- CHARTS AREA -->\n <div class=\"charts-area\">\n <!-- LINE CHART: 运行趋势(分组多线 + 时间横轴) -->\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> 运行时长趋势</span>\n <div class=\"chart-legend\" id=\"lineLegend\"></div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"lineChart\"></canvas></div>\n </div>\n\n <!-- BAR CHART: 时长构成分布(时间横轴 + 堆叠柱状) -->\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> 时长构成分布</span>\n <div class=\"chart-legend\">\n <span><span class=\"dot\" style=\"background:#ea570c\"></span>装载</span>\n <span><span class=\"dot\" style=\"background:#2563eb\"></span>重车</span>\n <span><span class=\"dot\" style=\"background:#7c3aed\"></span>卸载</span>\n <span><span class=\"dot\" style=\"background:#16a34a\"></span>空返</span>\n <span><span class=\"dot\" style=\"background:#dc2626\"></span>怠速</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"barChart\"></canvas></div>\n </div>\n\n <!-- RATIO PANEL: 装载/行驶/卸载占比(分组横向堆叠条) -->\n <div class=\"ratio-panel\" id=\"ratioPanel\">\n <div class=\"ratio-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-pie\"></i> 各分组 装载 / 行驶 / 卸载占比</span>\n </div>\n <div class=\"ratio-legend-row\">\n <span><span class=\"dot\" style=\"background:#ea570c\"></span>装载</span>\n <span><span class=\"dot\" style=\"background:#2563eb\"></span>行驶</span>\n <span><span class=\"dot\" style=\"background:#7c3aed\"></span>卸载</span>\n </div>\n <div class=\"ratio-body\" id=\"ratioBody\"></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> 数据明细</span>\n <span style=\"font-size:7px;color:var(--text-muted)\" id=\"tableInfo\">共 0 条记录</span>\n </div>\n <div class=\"table-wrap\">\n <table><thead id=\"tableHead\"></thead><tbody id=\"tableBody\"></tbody></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 const VEHICLES = [\n { id: 'A-E-001', mine: '矿区A', type: '电动矿卡', capacity: 60 },\n { id: 'A-E-002', mine: '矿区A', type: '电动矿卡', capacity: 60 },\n { id: 'A-E-003', mine: '矿区A', type: '电动矿卡', capacity: 55 },\n { id: 'A-F-001', mine: '矿区A', type: '燃油矿卡', capacity: 70 },\n { id: 'A-F-002', mine: '矿区A', type: '燃油矿卡', capacity: 70 },\n { id: 'B-E-001', mine: '矿区B', type: '电动矿卡', capacity: 60 },\n { id: 'B-E-002', mine: '矿区B', type: '电动矿卡', capacity: 55 },\n { id: 'B-H-001', mine: '矿区B', type: '混合动力矿卡', capacity: 65 },\n { id: 'B-H-002', mine: '矿区B', type: '混合动力矿卡', capacity: 65 },\n { id: 'B-H-003', mine: '矿区B', type: '混合动力矿卡', capacity: 60 },\n { id: 'C-F-001', mine: '矿区C', type: '燃油矿卡', capacity: 70 },\n { id: 'C-F-002', mine: '矿区C', type: '燃油矿卡', capacity: 75 },\n { id: 'C-F-003', mine: '矿区C', type: '燃油矿卡', capacity: 70 },\n { id: 'C-H-001', mine: '矿区C', type: '混合动力矿卡', capacity: 65 },\n { id: 'C-H-002', mine: '矿区C', type: '混合动力矿卡', capacity: 60 },\n ];\n\n const MINES = [...new Set(VEHICLES.map(v => v.mine))];\n const TYPES = [...new Set(VEHICLES.map(v => v.type))];\n const GC = ['#d4891a','#0d82b8','#0f766e','#ea570c','#7c3aed','#2563eb','#16a34a','#dc2626','#059669','#db2777','#6366f1','#0891b2','#ca8a04','#9333ea','#e11d48'];\n\n const state = {\n granularity: 'day', startDate: '2026-04-01', endDate: '2026-04-27',\n groupBy: 'mine', filterMine: '', filterType: '', filterVehicle: '',\n };\n\n const $ = s => document.querySelector(s);\n const dom = {\n granularityGroup: $('#granularityGroup'), groupByGroup: $('#groupByGroup'),\n startDate: $('#startDate'), endDate: $('#endDate'),\n filterMine: $('#filterMine'), filterType: $('#filterType'), filterVehicle: $('#filterVehicle'),\n metricsRow: $('#metricsRow'), lineChart: $('#lineChart'), barChart: $('#barChart'),\n lineLegend: $('#lineLegend'), ratioBody: $('#ratioBody'),\n tableHead: $('#tableHead'), tableBody: $('#tableBody'), tableInfo: $('#tableInfo'),\n toastContainer: $('#toastContainer'),\n };\n\n function showToast(msg, icon) {\n const el = document.createElement('div'); 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(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 350); }, 2000);\n }\n\n function getFilteredVehicles() {\n return VEHICLES.filter(v => {\n if (state.filterMine && v.mine !== state.filterMine) return false;\n if (state.filterType && v.type !== state.filterType) return false;\n if (state.filterVehicle && v.id !== state.filterVehicle) return false;\n return true;\n });\n }\n\n function populateFilterOptions() {\n const pm = dom.filterMine.value;\n dom.filterMine.innerHTML = '<option value=\"\">全部矿区</option>' + MINES.map(m => `<option value=\"${m}\" ${pm===m?'selected':''}>${m}</option>`).join('');\n if (pm && MINES.includes(pm)) dom.filterMine.value = pm;\n const pt = dom.filterType.value;\n const mf = dom.filterMine.value;\n const at = mf ? [...new Set(VEHICLES.filter(v=>v.mine===mf).map(v=>v.type))] : TYPES;\n dom.filterType.innerHTML = '<option value=\"\">全部类型</option>' + at.map(t => `<option value=\"${t}\" ${pt===t?'selected':''}>${t}</option>`).join('');\n if (pt && at.includes(pt)) dom.filterType.value = pt; else dom.filterType.value = '';\n const pv = dom.filterVehicle.value;\n const tf = dom.filterType.value;\n let av = VEHICLES; if (mf) av = av.filter(v=>v.mine===mf); if (tf) av = av.filter(v=>v.type===tf);\n dom.filterVehicle.innerHTML = '<option value=\"\">全部车辆</option>' + av.map(v => `<option value=\"${v.id}\" ${pv===v.id?'selected':''}>${v.id}(${v.capacity}t)</option>`).join('');\n if (pv && av.some(v=>v.id===pv)) dom.filterVehicle.value = pv; else dom.filterVehicle.value = '';\n }\n\n function getGroupKey(v) {\n if (state.groupBy === 'mine') return v.mine;\n if (state.groupBy === 'type') return v.type;\n return v.id;\n }\n function getGroupLabel(k) {\n if (state.groupBy === 'vehicle') { const v = VEHICLES.find(x=>x.id===k); return v ? `${v.id}(${v.capacity}t)` : k; }\n return k;\n }\n\n function seedRandom(seed) { let s = seed; return () => { s = (s * 16807 + 0) % 2147483647; return (s - 1) / 2147483646; }; }\n\n function generateTimeLabels() {\n const labels = [];\n const sd = new Date(state.startDate), ed = new Date(state.endDate);\n if (isNaN(sd.getTime()) || isNaN(ed.getTime()) || sd > ed) return labels;\n const cur = new Date(sd);\n if (state.granularity === 'day') {\n while (cur <= ed) { labels.push(new Date(cur)); cur.setDate(cur.getDate() + 1); }\n } else if (state.granularity === 'month') {\n cur.setDate(1); const em = new Date(ed.getFullYear(), ed.getMonth(), 1);\n while (cur <= em) { labels.push(new Date(cur)); cur.setMonth(cur.getMonth() + 1); }\n } else {\n cur.setMonth(0, 1); const ey = new Date(ed.getFullYear(), 0, 1);\n while (cur <= ey) { labels.push(new Date(cur)); cur.setFullYear(cur.getFullYear() + 1); }\n }\n if (labels.length > 120) { const step = Math.ceil(labels.length / 100); return labels.filter((_, i) => i % step === 0); }\n return labels;\n }\n\n function generateRecords() {\n const vehicles = getFilteredVehicles();\n const timeLabels = generateTimeLabels();\n if (vehicles.length === 0 || timeLabels.length === 0) return { records: [], groupKeys: [], timeLabels: [] };\n\n const gkSet = new Set(); vehicles.forEach(v => gkSet.add(getGroupKey(v)));\n const groupKeys = [...gkSet].sort();\n\n const dm = {};\n groupKeys.forEach(gk => { dm[gk] = timeLabels.map(() => ({ totalTime:0, workTime:0, cycles:0, idleTime:0, loadTime:0, unloadTime:0, emptyReturnTime:0, loadedRunTime:0 })); });\n\n vehicles.forEach(v => {\n const gk = getGroupKey(v);\n timeLabels.forEach((d, ti) => {\n const rng = seedRandom(d.getTime() % 99997 + v.id.charCodeAt(0) * 73 + ti * 41 + v.capacity * 7);\n const cm = state.granularity === 'year' ? 4200 + rng() * 2800 : state.granularity === 'month' ? 350 + rng() * 240 : 13 + rng() * 11;\n const cyc = Math.round(cm);\n const lpc=0.09+rng()*0.07, lrpc=0.16+rng()*0.12, upc=0.03+rng()*0.03, epc=0.10+rng()*0.08, ipc=0.06+rng()*0.10;\n const lt=Math.round(cyc*lpc*10)/10, lrt=Math.round(cyc*lrpc*10)/10, ut=Math.round(cyc*upc*10)/10, ert=Math.round(cyc*epc*10)/10, it=Math.round(cyc*ipc*10)/10;\n const wt=Math.round((lt+lrt+ut+ert)*10)/10, tt=Math.round((wt+it)*10)/10;\n const c=dm[gk][ti];\n c.cycles+=cyc; c.loadTime+=lt; c.loadedRunTime+=lrt; c.unloadTime+=ut; c.emptyReturnTime+=ert; c.idleTime+=it; c.workTime+=wt; c.totalTime+=tt;\n });\n });\n\n const records = [];\n groupKeys.forEach(gk => {\n timeLabels.forEach((d, ti) => {\n const c = dm[gk][ti];\n records.push({\n timeLabel: d, groupKey: gk, groupLabel: getGroupLabel(gk),\n totalTime: Math.round(c.totalTime*10)/10, workTime: Math.round(c.workTime*10)/10,\n cycles: c.cycles, idleTime: Math.round(c.idleTime*10)/10,\n loadTime: Math.round(c.loadTime*10)/10, unloadTime: Math.round(c.unloadTime*10)/10,\n emptyReturnTime: Math.round(c.emptyReturnTime*10)/10, loadedRunTime: Math.round(c.loadedRunTime*10)/10,\n });\n });\n });\n\n return { records, groupKeys, timeLabels };\n }\n\n function fmtTL(d) {\n if (state.granularity === 'day') return `${d.getMonth()+1}/${d.getDate()}`;\n if (state.granularity === 'month') return `${d.getFullYear()}/${String(d.getMonth()+1).padStart(2,'0')}`;\n return `${d.getFullYear()}`;\n }\n function fmtTF(d) {\n if (state.granularity === 'day') return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;\n if (state.granularity === 'month') return `${d.getFullYear()}年${d.getMonth()+1}月`;\n return `${d.getFullYear()}年`;\n }\n\n // ==================== METRICS ====================\n function renderMetrics(records) {\n if (!records.length) {\n dom.metricsRow.innerHTML = Array.from({length:8}, () => `<div class=\"metric-card\"><div class=\"card-accent\"></div><div class=\"card-label\">--</div><div class=\"card-value\">0</div></div>`).join('');\n return;\n }\n const sm = records.reduce((a,r) => { Object.keys(a).forEach(k=>{a[k]+=r[k]||0;}); return a; }, {totalTime:0,workTime:0,cycles:0,idleTime:0,loadTime:0,unloadTime:0,emptyReturnTime:0,loadedRunTime:0});\n const cf = [\n {icon:'fa-clock',label:'运行时长',value:sm.totalTime.toFixed(1),unit:'h',color:'#d4891a'},\n {icon:'fa-gears',label:'作业时长',value:sm.workTime.toFixed(1),unit:'h',color:'#0d82b8'},\n {icon:'fa-rotate',label:'循环次数',value:sm.cycles.toString(),unit:'次',color:'#0f766e'},\n {icon:'fa-pause',label:'怠速时长',value:sm.idleTime.toFixed(1),unit:'h',color:'#ea570c'},\n {icon:'fa-arrow-down',label:'装载时长',value:sm.loadTime.toFixed(1),unit:'h',color:'#7c3aed'},\n {icon:'fa-arrow-up',label:'卸载时长',value:sm.unloadTime.toFixed(1),unit:'h',color:'#2563eb'},\n {icon:'fa-truck-fast',label:'空车返回',value:sm.emptyReturnTime.toFixed(1),unit:'h',color:'#16a34a'},\n {icon:'fa-weight-hanging',label:'重车运行',value:sm.loadedRunTime.toFixed(1),unit:'h',color:'#dc2626'},\n ];\n dom.metricsRow.innerHTML = cf.map(c => `<div class=\"metric-card\"><div class=\"card-accent\"></div><div class=\"card-icon\"><i class=\"fa-solid ${c.icon}\"></i></div><div class=\"card-label\">${c.label}</div><div class=\"card-value\" style=\"color:${c.color}\">${c.value}</div><div class=\"card-sub\">${c.unit}</div></div>`).join('');\n }\n\n // ==================== LINE CHART ====================\n function renderLineChart(canvas, records, groupKeys, timeLabels) {\n if (!canvas || !records.length) return;\n const dpr = window.devicePixelRatio || 1;\n const rect = canvas.parentElement.getBoundingClientRect();\n const w = rect.width, h = rect.height;\n canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px';\n const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h);\n\n const pad = { top: 8, right: 10, bottom: 24, left: 38 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n dom.lineLegend.innerHTML = groupKeys.map((gk,i) => `<span><span class=\"dot\" style=\"background:${GC[i%GC.length]}\"></span>${gk}</span>`).join('');\n\n let mx = 0; records.forEach(r => { if (r.totalTime > mx) mx = r.totalTime; });\n mx = Math.ceil(mx * 1.2) || 10;\n\n const gl = 5;\n ctx.strokeStyle = '#eef0f4'; ctx.lineWidth = 1;\n for (let i = 0; i <= gl; i++) {\n const y = pad.top + (ch / gl) * i;\n ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();\n ctx.fillStyle = '#94a3b8'; ctx.font = '7px DM Sans, sans-serif'; ctx.textAlign = 'right';\n ctx.fillText(Math.round(mx - (mx / gl) * i), pad.left - 4, y + 3);\n }\n\n const xs = cw / Math.max(timeLabels.length - 1, 1);\n const ml = Math.min(timeLabels.length, 14);\n const ls = Math.max(1, Math.floor(timeLabels.length / ml));\n ctx.fillStyle = '#94a3b8'; ctx.font = '7px DM Sans, sans-serif'; ctx.textAlign = 'center';\n timeLabels.forEach((d, i) => { if (i % ls === 0 || i === timeLabels.length - 1) ctx.fillText(fmtTL(d), pad.left + xs * i, pad.top + ch + 13); });\n\n groupKeys.forEach((gk, gi) => {\n const color = GC[gi % GC.length];\n const gr = records.filter(r => r.groupKey === gk);\n ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.lineJoin = 'round'; ctx.lineCap = 'round';\n ctx.beginPath();\n gr.forEach((r, i) => { const x = pad.left + xs * i, y = pad.top + ch - (r.totalTime / mx) * ch; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); });\n ctx.stroke();\n gr.forEach((r, i) => {\n const x = pad.left + xs * i, y = pad.top + ch - (r.totalTime / mx) * ch;\n ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI*2); ctx.fill();\n ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI*2); ctx.fill();\n });\n });\n }\n\n // ==================== BAR CHART(时间横轴 + 堆叠柱状) ====================\n function renderBarChart(canvas, records, timeLabels) {\n if (!canvas || !records.length || !timeLabels.length) return;\n const dpr = window.devicePixelRatio || 1;\n const rect = canvas.parentElement.getBoundingClientRect();\n const w = rect.width, h = rect.height;\n canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px';\n const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h);\n\n const datasets = [\n { key: 'loadTime', color: '#ea570c' },\n { key: 'loadedRunTime', color: '#2563eb' },\n { key: 'unloadTime', color: '#7c3aed' },\n { key: 'emptyReturnTime', color: '#16a34a' },\n { key: 'idleTime', color: '#dc2626' },\n ];\n\n // Aggregate across groups for each time point\n const aggregated = timeLabels.map((d, ti) => {\n const cell = { loadTime:0, loadedRunTime:0, unloadTime:0, emptyReturnTime:0, idleTime:0 };\n records.filter(r => r.timeLabel.getTime() === d.getTime()).forEach(r => { datasets.forEach(ds => { cell[ds.key] += r[ds.key]; }); });\n return cell;\n });\n\n let mx = 0;\n aggregated.forEach(c => { const s = datasets.reduce((a,d)=>a+c[d.key],0); if (s > mx) mx = s; });\n mx = Math.ceil(mx * 1.18) || 10;\n\n const pad = { top: 8, right: 10, bottom: 24, left: 40 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n const gl = 5;\n ctx.strokeStyle = '#eef0f4'; ctx.lineWidth = 1;\n for (let i = 0; i <= gl; i++) {\n const y = pad.top + (ch / gl) * i;\n ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();\n ctx.fillStyle = '#94a3b8'; ctx.font = '7px DM Sans, sans-serif'; ctx.textAlign = 'right';\n ctx.fillText(Math.round(mx - (mx / gl) * i), pad.left - 4, y + 3);\n }\n\n const maxBars = Math.min(aggregated.length, 28);\n const step = Math.max(1, Math.floor(aggregated.length / maxBars));\n const di = aggregated.map((_, i) => i).filter((_, i) => i % step === 0 || i === aggregated.length - 1);\n const barGap = 3;\n const barW = (cw / di.length) - barGap;\n if (barW <= 0) return;\n\n di.forEach((ti, bi) => {\n const x = pad.left + (cw / di.length) * bi + barGap / 2;\n let sy = pad.top + ch;\n datasets.forEach((ds, dsi) => {\n const val = aggregated[ti][ds.key];\n const bh = (val / mx) * ch;\n const y = sy - bh;\n ctx.fillStyle = ds.color; ctx.fillRect(x, y, barW, bh);\n if (dsi > 0 && bh > 1.5) { ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(x, y, barW, 0.4); }\n sy = y;\n });\n const ls2 = Math.max(1, Math.floor(di.length / 10));\n if (bi % ls2 === 0 || bi === di.length - 1) {\n ctx.fillStyle = '#94a3b8'; ctx.font = '7px DM Sans, sans-serif'; ctx.textAlign = 'center';\n ctx.fillText(fmtTL(timeLabels[ti]), x + barW / 2, pad.top + ch + 13);\n }\n });\n }\n\n // ==================== RATIO PANEL ====================\n function renderRatioPanel(records, groupKeys) {\n if (!records.length || !groupKeys.length) { dom.ratioBody.innerHTML = '<div style=\"text-align:center;color:var(--text-muted);padding:8px;font-size:9px;\">暂无数据</div>'; return; }\n\n const ga = groupKeys.map(gk => {\n const gr = records.filter(r => r.groupKey === gk);\n const load = gr.reduce((s,r)=>s+r.loadTime,0);\n const haul = gr.reduce((s,r)=>s+r.loadedRunTime+r.emptyReturnTime,0);\n const unload = gr.reduce((s,r)=>s+r.unloadTime,0);\n const total = load + haul + unload;\n return {\n groupKey: gk, total,\n loadPct: total > 0 ? load / total * 100 : 0,\n haulPct: total > 0 ? haul / total * 100 : 0,\n unloadPct: total > 0 ? unload / total * 100 : 0,\n };\n });\n\n dom.ratioBody.innerHTML = ga.map(g => `\n <div class=\"ratio-group-row\">\n <div class=\"ratio-group-name\" title=\"${g.groupKey}\">${g.groupKey.length > 6 ? g.groupKey.slice(0,5)+'…' : g.groupKey}</div>\n <div class=\"ratio-hbar-wrap\">\n <div class=\"seg-load\" style=\"width:${g.loadPct.toFixed(1)}%\"></div>\n <div class=\"seg-haul\" style=\"width:${g.haulPct.toFixed(1)}%\"></div>\n <div class=\"seg-unload\" style=\"width:${g.unloadPct.toFixed(1)}%\"></div>\n </div>\n <div class=\"ratio-group-pct\">${g.total.toFixed(1)}h</div>\n </div>\n `).join('');\n }\n\n // ==================== TABLE ====================\n function renderTable(records) {\n dom.tableInfo.textContent = `共 ${records.length} 条记录`;\n if (!records.length) {\n dom.tableHead.innerHTML = '<tr><th>周期</th><th>分组</th><th>运行(h)</th><th>作业(h)</th><th>循环</th><th>怠速(h)</th><th>装载(h)</th><th>卸载(h)</th><th>空返(h)</th><th>重车(h)</th></tr>';\n dom.tableBody.innerHTML = '<tr><td colspan=\"10\" style=\"text-align:center;color:var(--text-muted);padding:10px;\">暂无数据</td></tr>';\n return;\n }\n const gl = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n dom.tableHead.innerHTML = `<tr><th>周期</th><th>${gl}</th><th>运行(h)</th><th>作业(h)</th><th>循环</th><th>怠速(h)</th><th>装载(h)</th><th>卸载(h)</th><th>空返(h)</th><th>重车(h)</th></tr>`;\n const disp = records.slice(-28);\n dom.tableBody.innerHTML = disp.map(r => `<tr><td>${fmtTF(r.timeLabel)}</td><td>${r.groupLabel}</td><td class=\"cell-highlight\">${r.totalTime.toFixed(1)}</td><td>${r.workTime.toFixed(1)}</td><td class=\"cell-cycles\">${r.cycles}</td><td>${r.idleTime.toFixed(1)}</td><td>${r.loadTime.toFixed(1)}</td><td>${r.unloadTime.toFixed(1)}</td><td>${r.emptyReturnTime.toFixed(1)}</td><td>${r.loadedRunTime.toFixed(1)}</td></tr>`).join('');\n }\n\n // ==================== MAIN ====================\n let cd = { records: [], groupKeys: [], timeLabels: [] };\n\n function updateDashboard(silent) {\n cd = generateRecords();\n const { records, groupKeys, timeLabels } = cd;\n renderMetrics(records);\n renderLineChart(dom.lineChart, records, groupKeys, timeLabels);\n renderBarChart(dom.barChart, records, timeLabels);\n renderRatioPanel(records, groupKeys);\n renderTable(records);\n if (!silent) {\n const gl = state.granularity === 'day' ? '日' : state.granularity === 'month' ? '月' : '年';\n const gbl = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n showToast(`横轴按${gl} · 分组按${gbl} · 共 ${records.length} 条`, 'fa-circle-check');\n }\n }\n\n // ==================== EVENTS ====================\n dom.granularityGroup.addEventListener('click', e => {\n const btn = e.target.closest('button'); if (!btn || btn.classList.contains('active')) return;\n dom.granularityGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n btn.classList.add('active'); state.granularity = btn.dataset.gran;\n const now = new Date();\n if (state.granularity === 'day') { state.startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0,10); state.endDate = now.toISOString().slice(0,10); }\n else if (state.granularity === 'month') { state.startDate = new Date(now.getFullYear()-1, now.getMonth(), 1).toISOString().slice(0,10); state.endDate = now.toISOString().slice(0,10); }\n else { state.startDate = new Date(now.getFullYear()-4, 0, 1).toISOString().slice(0,10); state.endDate = now.toISOString().slice(0,10); }\n dom.startDate.value = state.startDate; dom.endDate.value = state.endDate;\n updateDashboard();\n });\n dom.groupByGroup.addEventListener('click', e => {\n const btn = e.target.closest('button'); if (!btn || btn.classList.contains('active')) return;\n dom.groupByGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n btn.classList.add('active'); state.groupBy = btn.dataset.group;\n populateFilterOptions(); updateDashboard();\n });\n dom.filterMine.addEventListener('change', () => { state.filterMine = dom.filterMine.value; populateFilterOptions(); updateDashboard(); });\n dom.filterType.addEventListener('change', () => { state.filterType = dom.filterType.value; populateFilterOptions(); updateDashboard(); });\n dom.filterVehicle.addEventListener('change', () => { state.filterVehicle = dom.filterVehicle.value; updateDashboard(); });\n dom.startDate.addEventListener('change', () => { state.startDate = dom.startDate.value; updateDashboard(); });\n dom.endDate.addEventListener('change', () => { state.endDate = dom.endDate.value; updateDashboard(); });\n\n window.refreshData = function() { state.startDate = dom.startDate.value; state.endDate = dom.endDate.value; state.filterMine = dom.filterMine.value; state.filterType = dom.filterType.value; state.filterVehicle = dom.filterVehicle.value; updateDashboard(); };\n window.exportCSV = function() {\n if (!cd.records.length) { showToast('无数据可导出', 'fa-triangle-exclamation'); return; }\n const gc = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n let csv = `周期,${gc},运行时长(h),作业时长(h),循环次数,怠速时长(h),装载时长(h),卸载时长(h),空车返回(h),重车运行(h)\\n`;\n cd.records.forEach(r => { csv += [fmtTF(r.timeLabel), r.groupLabel, r.totalTime.toFixed(1), r.workTime.toFixed(1), r.cycles, r.idleTime.toFixed(1), r.loadTime.toFixed(1), r.unloadTime.toFixed(1), r.emptyReturnTime.toFixed(1), r.loadedRunTime.toFixed(1)].join(',') + '\\n'; });\n const blob = new Blob(['\\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `矿车运营数据_${state.granularity}_${state.groupBy}_${state.startDate}_${state.endDate}.csv`; a.click(); URL.revokeObjectURL(url);\n showToast('CSV 已下载', 'fa-file-csv');\n };\n window.printDashboard = function() { showToast('正在准备打印...', 'fa-print'); setTimeout(() => window.print(), 400); };\n\n let rt;\n window.addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(() => { if (cd.records.length > 0) { renderLineChart(dom.lineChart, cd.records, cd.groupKeys, cd.timeLabels); renderBarChart(dom.barChart, cd.records, cd.timeLabels); renderRatioPanel(cd.records, cd.groupKeys); } }, 250); });\n\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; dom.endDate.value = state.endDate;\n populateFilterOptions(); updateDashboard(true);\n }\n init();\n})();\n</script>\n\n</body>\n</html>"
},
"next": ["按班次拆分统计", "添加设备稼动率", "设置异常预警线", "导出PDF运营报告", "接入实时运营数据"],
"breakpoint": "none"
}
```
推理
The user is asking for the dashboard to support "按日、月、年作为横轴数据" (support day, month, year as horizontal axis data). Looking at the current code, I already have a granularity group with buttons for 按日 (by day), 按月 (by month), 按年 (by year) which changes the `state.granularity`. The line chart and bar chart already use `timeLabels` which are generated based on the granularity, and the X-axis labels are formatted accordingly using `fmtTL()`.
But wait - the current bar chart (时长构成对比) aggregates across ALL time periods within the group and shows one stacked bar per group, not per time period. The user might want the bar chart to also show time-based distribution on the X-axis (day/month/year).
Let me re-read the request: "支持按日、月、年作为横轴数据" - this means the X-axis should be day/month/year. The line chart already does this. But the bar chart currently shows one bar per group (aggregated across all time), not one bar per time period.
So I need to modify the bar chart to show time periods on the X-axis (days/months/years) with stacked bars per time period. And the ratio panel could also show per-time-period or aggregated per group.
Actually, looking more carefully at the current implementation:
- Line chart: X-axis = time (day/month/year), multiple lines for each group - this is correct
- Bar chart (时长构成对比): X-axis = groups (矿区A, 矿区B, etc.), each group has one stacked bar - this aggregates time
- Ratio panel: horizontal bars per group showing load/haul/unload percentage
The user wants ALL charts to support day/month/year on the X-axis. So the bar chart should also show time periods on X-axis, and the ratio panel should follow the same pattern.
Let me redesign:
1. Line chart - already has time on X-axis, groups as lines ✓
2. Bar chart - change to show time on X-axis (not groups), with stacked bars showing the breakdown per time period
3. Ratio panel - show per time period ratio breakdown, or at least show the overall per-group breakdown
Actually, for the bar chart and ratio to work with time on X-axis, I should aggregate across groups for each time point. This makes more sense for the bar chart showing 时长构成 per day/month/year.
For the ratio panel, I can show a summary per group (since showing every single time period would be too many rows).
Let me modify:
- Bar chart: X-axis = time labels, each bar = stacked 装载/重车/卸载/空返/怠速 (aggregated across all groups)
- Ratio panel: keep the per-group horizontal stacked bars (since this is a summary across time)
This way all three charts properly support the day/month/year granularity on the X-axis where applicable.
Let me update the code.
But wait - the current bar chart (时长构成对比) aggregates across ALL time periods within the group and shows one stacked bar per group, not per time period. The user might want the bar chart to also show time-based distribution on the X-axis (day/month/year).
Let me re-read the request: "支持按日、月、年作为横轴数据" - this means the X-axis should be day/month/year. The line chart already does this. But the bar chart currently shows one bar per group (aggregated across all time), not one bar per time period.
So I need to modify the bar chart to show time periods on the X-axis (days/months/years) with stacked bars per time period. And the ratio panel could also show per-time-period or aggregated per group.
Actually, looking more carefully at the current implementation:
- Line chart: X-axis = time (day/month/year), multiple lines for each group - this is correct
- Bar chart (时长构成对比): X-axis = groups (矿区A, 矿区B, etc.), each group has one stacked bar - this aggregates time
- Ratio panel: horizontal bars per group showing load/haul/unload percentage
The user wants ALL charts to support day/month/year on the X-axis. So the bar chart should also show time periods on X-axis, and the ratio panel should follow the same pattern.
Let me redesign:
1. Line chart - already has time on X-axis, groups as lines ✓
2. Bar chart - change to show time on X-axis (not groups), with stacked bars showing the breakdown per time period
3. Ratio panel - show per time period ratio breakdown, or at least show the overall per-group breakdown
Actually, for the bar chart and ratio to work with time on X-axis, I should aggregate across groups for each time point. This makes more sense for the bar chart showing 时长构成 per day/month/year.
For the ratio panel, I can show a summary per group (since showing every single time period would be too many rows).
Let me modify:
- Bar chart: X-axis = time labels, each bar = stacked 装载/重车/卸载/空返/怠速 (aggregated across all groups)
- Ratio panel: keep the per-group horizontal stacked bars (since this is a summary across time)
This way all three charts properly support the day/month/year granularity on the X-axis where applicable.
Let me update the code.
角色
assistant
content
total_tokens
167186
summary
矿车运营数据统计仪表盘,全分组对比+时间横轴版,8项核心指标,折线趋势(分组多线+时间横轴)、堆叠柱状(时间横轴各周期构成)、横向占比条(分组对比),支持矿区/车辆类型/车辆三级联动筛选,年/月/日粒度切换,CSV导出与打印。
uiParsing
关闭