```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-amber-bg: #fef9f0;\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 --accent-slate: #64748b;\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: 6px;\n background: var(--bg-panel);\n border-radius: var(--radius-xl);\n padding: 12px 16px;\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\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: 10px;\n flex-shrink: 0;\n padding-bottom: 5px;\n border-bottom: 1px solid var(--border-light);\n }\n\n .header-left { display: flex; align-items: center; gap: 9px; }\n\n .logo-mark {\n width: 32px;\n height: 32px;\n background: linear-gradient(135deg, #d4891a, #b07214);\n border-radius: var(--radius);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 15px;\n color: #ffffff;\n font-weight: 700;\n flex-shrink: 0;\n box-shadow: 0 2px 8px rgba(212,137,26,0.22);\n }\n\n .header-title h1 {\n font-family: var(--font-display);\n font-size: clamp(13px, 1.1vw, 18px);\n font-weight: 700;\n letter-spacing: -0.01em;\n color: var(--text-primary);\n line-height: 1.1;\n }\n\n .header-title span {\n font-size: 9px;\n color: var(--text-muted);\n font-weight: 500;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n }\n\n .header-right {\n display: flex;\n align-items: center;\n gap: 5px;\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 box-shadow: var(--shadow-xs);\n }\n\n .btn-group button {\n background: transparent;\n border: none;\n color: var(--text-secondary);\n padding: 4px 10px;\n cursor: pointer;\n font-family: var(--font-display);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.01em;\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: 16%;\n height: 68%;\n width: 1px;\n background: var(--border);\n }\n\n .btn-group button:hover { color: var(--text-primary); background: var(--bg-elevated); }\n\n .btn-group button.active {\n color: #ffffff;\n background: #d4891a;\n box-shadow: 0 0 10px rgba(212,137,26,0.2);\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\n select, .date-input {\n background: var(--bg-card);\n border: 1px solid var(--border);\n color: var(--text-primary);\n padding: 4px 7px;\n border-radius: var(--radius);\n font-family: var(--font-display);\n font-size: 11px;\n outline: none;\n transition: all var(--transition-base);\n cursor: pointer;\n box-shadow: var(--shadow-xs);\n }\n select:focus, .date-input:focus { border-color: #d4891a; box-shadow: 0 0 0 3px rgba(212,137,26,0.1); }\n .date-input { width: 108px; }\n select { min-width: 72px; }\n\n .date-separator { color: var(--text-muted); font-size: 11px; font-weight: 500; }\n\n .filter-label {\n font-size: 8px;\n color: var(--text-muted);\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n white-space: nowrap;\n }\n\n .btn-action {\n background: var(--bg-card);\n border: 1px solid var(--border);\n color: var(--text-secondary);\n width: 28px;\n height: 28px;\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: 11px;\n box-shadow: var(--shadow-xs);\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, #d4891a, #b07214);\n border: none;\n color: #ffffff;\n padding: 4px 13px;\n border-radius: var(--radius);\n cursor: pointer;\n font-family: var(--font-display);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.01em;\n transition: all var(--transition-base);\n white-space: nowrap;\n box-shadow: 0 2px 6px rgba(212,137,26,0.2);\n }\n\n .btn-primary:hover { filter: brightness(1.07); transform: translateY(-1px); box-shadow: 0 4px 14px rgba(212,137,26,0.26); }\n .btn-primary:active { transform: scale(0.97); }\n\n /* ============ FILTER BAR ============ */\n .filter-bar {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n flex-wrap: wrap;\n padding: 4px 0;\n }\n\n .filter-bar .sep {\n width: 1px;\n height: 20px;\n background: var(--border);\n margin: 0 2px;\n }\n\n /* ============ METRICS ROW (8 cards) ============ */\n .metrics-row {\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 5px;\n flex-shrink: 0;\n }\n\n .metric-card {\n background: var(--bg-card);\n border-radius: var(--radius-md);\n padding: 7px 9px;\n border: 1px solid var(--border-light);\n position: relative;\n overflow: hidden;\n transition: all var(--transition-base);\n cursor: default;\n display: flex;\n flex-direction: column;\n gap: 1px;\n box-shadow: var(--shadow-sm);\n }\n\n .metric-card:hover {\n border-color: var(--border-active);\n box-shadow: var(--shadow-elevated);\n transform: translateY(-2px);\n }\n\n .metric-card .card-accent {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 2px;\n }\n\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\n .metric-card .card-icon { font-size: 8px; color: var(--text-muted); }\n\n .metric-card .card-label {\n font-size: 7px;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n font-weight: 600;\n }\n\n .metric-card .card-value {\n font-family: var(--font-display);\n font-size: clamp(12px, 1.3vw, 18px);\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 { font-size: 7px; color: var(--text-muted); font-weight: 500; }\n\n /* ============ CHARTS AREA (3 columns) ============ */\n .charts-area {\n display: grid;\n grid-template-columns: 1.25fr 0.85fr 0.9fr;\n gap: 6px;\n flex: 1;\n min-height: 0;\n }\n\n .chart-box {\n background: var(--bg-card);\n border-radius: var(--radius-md);\n border: 1px solid var(--border-light);\n padding: 9px 11px;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: var(--shadow-sm);\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: 4px;\n gap: 5px;\n }\n\n .chart-box-title {\n font-family: var(--font-display);\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--text-secondary);\n text-transform: uppercase;\n white-space: nowrap;\n }\n\n .chart-legend {\n display: flex;\n gap: 5px;\n font-size: 7px;\n color: var(--text-muted);\n flex-wrap: wrap;\n justify-content: flex-end;\n }\n\n .chart-legend span { display: flex; align-items: center; gap: 2px; white-space: nowrap; }\n .chart-legend .dot { width: 5px; height: 5px; border-radius: 50%; display: inline-block; flex-shrink: 0; }\n .chart-legend .dash { width: 7px; height: 0; border-top: 1.5px dashed; display: inline-block; flex-shrink: 0; }\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 { width: 100%; height: 100%; display: block; }\n\n /* ============ RATIO PANEL ============ */\n .ratio-panel {\n background: var(--bg-card);\n border-radius: var(--radius-md);\n border: 1px solid var(--border-light);\n padding: 9px 11px;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: var(--shadow-sm);\n }\n\n .ratio-header { flex-shrink: 0; margin-bottom: 2px; }\n\n .ratio-body {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n min-height: 0;\n }\n\n .ratio-donut-wrap { flex: 1; min-width: 0; height: 100%; position: relative; }\n .ratio-donut-wrap canvas { width: 100%; height: 100%; display: block; }\n\n .ratio-legend-vert {\n display: flex;\n flex-direction: column;\n gap: 5px;\n flex-shrink: 0;\n font-size: 8px;\n font-family: var(--font-display);\n }\n\n .ratio-legend-item { display: flex; align-items: center; gap: 4px; }\n .ratio-legend-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }\n .ratio-legend-val { font-weight: 700; font-size: 11px; color: var(--text-primary); line-height: 1; }\n .ratio-legend-label { color: var(--text-muted); font-size: 7px; }\n\n /* ============ TABLE ============ */\n .table-section {\n flex-shrink: 0;\n background: var(--bg-card);\n border-radius: var(--radius-md);\n border: 1px solid var(--border-light);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n max-height: 105px;\n box-shadow: var(--shadow-sm);\n }\n\n .table-section-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 4px 10px;\n border-bottom: 1px solid var(--border-light);\n flex-shrink: 0;\n }\n\n .table-section-title {\n font-family: var(--font-display);\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.03em;\n color: var(--text-secondary);\n text-transform: uppercase;\n }\n\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\n table { width: 100%; border-collapse: collapse; font-size: 9px; font-family: var(--font-display); }\n\n thead th {\n text-align: left;\n padding: 4px 6px;\n color: var(--text-muted);\n font-weight: 500;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n font-size: 7px;\n position: sticky;\n top: 0;\n background: var(--bg-card);\n z-index: 2;\n border-bottom: 1px solid var(--border-light);\n }\n\n tbody td {\n padding: 3px 6px;\n border-bottom: 1px solid rgba(0,0,0,0.03);\n color: var(--text-primary);\n font-weight: 500;\n letter-spacing: 0.01em;\n }\n\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\n .cell-highlight { color: #d4891a; font-weight: 700; }\n .cell-cycles { color: #0f766e; font-weight: 700; }\n\n /* ============ TOAST ============ */\n .toast-container {\n position: fixed;\n top: 16px;\n right: 16px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 6px;\n pointer-events: none;\n }\n\n .toast {\n background: #ffffff;\n border: 1px solid var(--border);\n border-left: 3px solid #d4891a;\n color: var(--text-primary);\n padding: 8px 14px;\n border-radius: var(--radius);\n font-size: 11px;\n font-family: var(--font-body);\n box-shadow: 0 4px 20px rgba(0,0,0,0.1);\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: 6px;\n }\n\n .toast.show { opacity: 1; transform: translateX(0); }\n .toast i { color: #d4891a; font-size: 13px; }\n\n /* ============ RESPONSIVE ============ */\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; flex-direction: row; }\n .dashboard { aspect-ratio: auto; max-height: none; }\n .chart-box { min-height: 180px; }\n .ratio-panel { min-height: 160px; }\n }\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; flex-direction: column; }\n .header { flex-direction: column; align-items: stretch; }\n .header-right { justify-content: flex-start; }\n select, .date-input { font-size: 10px; padding: 3px 5px; }\n .date-input { width: 85px; }\n .dashboard { padding: 8px; }\n .chart-box { min-height: 160px; }\n }\n\n @media (max-width: 600px) {\n .metrics-row { grid-template-columns: repeat(2, 1fr); }\n .btn-group button { padding: 3px 7px; font-size: 10px; }\n .header-right { gap: 2px; }\n table { font-size: 7px; }\n thead th, tbody td { padding: 2px 3px; }\n .filter-bar { gap: 3px; }\n .filter-bar .sep { display: none; }\n .ratio-body { flex-direction: column; }\n }\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, .filter-bar .btn-group { 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\">\n <h1>矿车运营数据统计</h1>\n <span>Mining Truck Operations</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> 刷新</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: 8 cards -->\n <div class=\"metrics-row\" id=\"metricsRow\"></div>\n\n <!-- CHARTS AREA: 3 columns -->\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 <!-- STACKED BAR: 时长构成 -->\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 <!-- DONUT: 装载/行驶/卸载占比 -->\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-body\">\n <div class=\"ratio-donut-wrap\"><canvas id=\"donutChart\"></canvas></div>\n <div class=\"ratio-legend-vert\" id=\"ratioLegend\"></div>\n </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:8px;color:var(--text-muted)\" id=\"tableInfo\">共 0 条记录</span>\n </div>\n <div class=\"table-wrap\">\n <table>\n <thead id=\"tableHead\"></thead>\n <tbody id=\"tableBody\"></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 // ==================== VEHICLE DATA ====================\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\n // ==================== STATE ====================\n const state = {\n granularity: 'day',\n startDate: '2026-04-01',\n endDate: '2026-04-27',\n groupBy: 'mine',\n filterMine: '',\n filterType: '',\n filterVehicle: '',\n };\n\n const $ = (sel) => document.querySelector(sel);\n\n const dom = {\n granularityGroup: $('#granularityGroup'),\n groupByGroup: $('#groupByGroup'),\n startDate: $('#startDate'),\n endDate: $('#endDate'),\n filterMine: $('#filterMine'),\n filterType: $('#filterType'),\n filterVehicle: $('#filterVehicle'),\n metricsRow: $('#metricsRow'),\n lineChart: $('#lineChart'),\n barChart: $('#barChart'),\n donutChart: $('#donutChart'),\n lineLegend: $('#lineLegend'),\n ratioLegend: $('#ratioLegend'),\n tableHead: $('#tableHead'),\n tableBody: $('#tableBody'),\n tableInfo: $('#tableInfo'),\n toastContainer: $('#toastContainer'),\n };\n\n // ==================== GROUP COLORS ====================\n const GROUP_COLORS = ['#d4891a', '#0d82b8', '#0f766e', '#ea570c', '#7c3aed', '#2563eb', '#16a34a', '#dc2626'];\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(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 350); }, 2200);\n }\n\n // ==================== FILTER HELPERS ====================\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 prevMine = dom.filterMine.value;\n dom.filterMine.innerHTML = '<option value=\"\">全部矿区</option>' + MINES.map(m => `<option value=\"${m}\" ${prevMine === m ? 'selected' : ''}>${m}</option>`).join('');\n if (prevMine && MINES.includes(prevMine)) dom.filterMine.value = prevMine;\n\n const prevType = dom.filterType.value;\n const mineFilter = dom.filterMine.value;\n const availableTypes = mineFilter\n ? [...new Set(VEHICLES.filter(v => v.mine === mineFilter).map(v => v.type))]\n : TYPES;\n dom.filterType.innerHTML = '<option value=\"\">全部类型</option>' + availableTypes.map(t => `<option value=\"${t}\" ${prevType === t ? 'selected' : ''}>${t}</option>`).join('');\n if (prevType && availableTypes.includes(prevType)) dom.filterType.value = prevType;\n else dom.filterType.value = '';\n\n const prevVehicle = dom.filterVehicle.value;\n const typeFilter = dom.filterType.value;\n let availableVehicles = VEHICLES;\n if (mineFilter) availableVehicles = availableVehicles.filter(v => v.mine === mineFilter);\n if (typeFilter) availableVehicles = availableVehicles.filter(v => v.type === typeFilter);\n dom.filterVehicle.innerHTML = '<option value=\"\">全部车辆</option>' + availableVehicles.map(v => `<option value=\"${v.id}\" ${prevVehicle === v.id ? 'selected' : ''}>${v.id} (${v.capacity}t)</option>`).join('');\n if (prevVehicle && availableVehicles.some(v => v.id === prevVehicle)) dom.filterVehicle.value = prevVehicle;\n else dom.filterVehicle.value = '';\n }\n\n function getGroupKey(vehicle, groupBy) {\n if (groupBy === 'mine') return vehicle.mine;\n if (groupBy === 'type') return vehicle.type;\n return vehicle.id;\n }\n\n function getGroupDisplayLabel(key, groupBy) {\n if (groupBy === 'vehicle') {\n const v = VEHICLES.find(x => x.id === key);\n return v ? `${v.id}(${v.capacity}t)` : key;\n }\n return key;\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 generateTimeLabels(granularity, start, end) {\n const labels = [];\n const startD = new Date(start);\n const endD = new Date(end);\n if (isNaN(startD.getTime()) || isNaN(endD.getTime()) || startD > endD) return labels;\n const current = new Date(startD);\n if (granularity === 'day') {\n while (current <= endD) { labels.push(new Date(current)); current.setDate(current.getDate() + 1); }\n } else if (granularity === 'month') {\n current.setDate(1);\n const endM = new Date(endD.getFullYear(), endD.getMonth(), 1);\n while (current <= endM) { labels.push(new Date(current)); current.setMonth(current.getMonth() + 1); }\n } else {\n current.setMonth(0, 1);\n const endY = new Date(endD.getFullYear(), 0, 1);\n while (current <= endY) { labels.push(new Date(current)); current.setFullYear(current.getFullYear() + 1); }\n }\n if (labels.length > 120) {\n const step = Math.ceil(labels.length / 100);\n return labels.filter((_, i) => i % step === 0);\n }\n return labels;\n }\n\n function generateRecords() {\n const vehicles = getFilteredVehicles();\n const timeLabels = generateTimeLabels(state.granularity, state.startDate, state.endDate);\n if (vehicles.length === 0 || timeLabels.length === 0) return { records: [], groupKeys: [], timeLabels: [] };\n\n const groupKeysSet = new Set();\n vehicles.forEach(v => groupKeysSet.add(getGroupKey(v, state.groupBy)));\n const groupKeys = [...groupKeysSet].sort();\n\n const dataMap = {};\n groupKeys.forEach(gk => {\n dataMap[gk] = timeLabels.map(() => ({\n totalTime: 0, workTime: 0, cycles: 0, idleTime: 0,\n loadTime: 0, unloadTime: 0, emptyReturnTime: 0, loadedRunTime: 0,\n }));\n });\n\n vehicles.forEach(v => {\n const gk = getGroupKey(v, state.groupBy);\n timeLabels.forEach((d, ti) => {\n const rng = seedRandom(d.getTime() % 99997 + v.id.charCodeAt(0) * 73 + ti * 41 + v.capacity * 7);\n\n const cyclesBase = state.granularity === 'year' ? 4200 + rng() * 2800 : state.granularity === 'month' ? 350 + rng() * 240 : 13 + rng() * 11;\n const cycles = Math.round(cyclesBase);\n\n const loadPerCycle = 0.09 + rng() * 0.07;\n const loadedRunPerCycle = 0.16 + rng() * 0.12;\n const unloadPerCycle = 0.03 + rng() * 0.03;\n const emptyReturnPerCycle = 0.10 + rng() * 0.08;\n const idlePerCycle = 0.06 + rng() * 0.10;\n\n const loadTime = Math.round(cycles * loadPerCycle * 10) / 10;\n const loadedRunTime = Math.round(cycles * loadedRunPerCycle * 10) / 10;\n const unloadTime = Math.round(cycles * unloadPerCycle * 10) / 10;\n const emptyReturnTime = Math.round(cycles * emptyReturnPerCycle * 10) / 10;\n const idleTime = Math.round(cycles * idlePerCycle * 10) / 10;\n const workTime = Math.round((loadTime + loadedRunTime + unloadTime + emptyReturnTime) * 10) / 10;\n const totalTime = Math.round((workTime + idleTime) * 10) / 10;\n\n const cell = dataMap[gk][ti];\n cell.cycles += cycles;\n cell.loadTime += loadTime;\n cell.loadedRunTime += loadedRunTime;\n cell.unloadTime += unloadTime;\n cell.emptyReturnTime += emptyReturnTime;\n cell.idleTime += idleTime;\n cell.workTime += workTime;\n cell.totalTime += totalTime;\n });\n });\n\n const records = [];\n groupKeys.forEach(gk => {\n timeLabels.forEach((d, ti) => {\n const cell = dataMap[gk][ti];\n records.push({\n timeLabel: d,\n groupKey: gk,\n groupLabel: getGroupDisplayLabel(gk, state.groupBy),\n totalTime: Math.round(cell.totalTime * 10) / 10,\n workTime: Math.round(cell.workTime * 10) / 10,\n cycles: cell.cycles,\n idleTime: Math.round(cell.idleTime * 10) / 10,\n loadTime: Math.round(cell.loadTime * 10) / 10,\n unloadTime: Math.round(cell.unloadTime * 10) / 10,\n emptyReturnTime: Math.round(cell.emptyReturnTime * 10) / 10,\n loadedRunTime: Math.round(cell.loadedRunTime * 10) / 10,\n });\n });\n });\n\n return { records, groupKeys, timeLabels };\n }\n\n function fmtTimeLabel(d, granularity) {\n if (granularity === 'day') return `${d.getMonth() + 1}/${d.getDate()}`;\n if (granularity === 'month') return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}`;\n return `${d.getFullYear()}`;\n }\n\n function fmtTimeFull(d, granularity) {\n if (granularity === 'day') return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;\n if (granularity === 'month') return `${d.getFullYear()}年${d.getMonth() + 1}月`;\n return `${d.getFullYear()}年`;\n }\n\n // ==================== METRICS ====================\n function renderMetrics(records) {\n if (records.length === 0) {\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 sums = records.reduce((acc, r) => {\n Object.keys(acc).forEach(k => { acc[k] += r[k] || 0; });\n return acc;\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: '#d4891a' },\n { icon: 'fa-gears', label: '作业时长', value: sums.workTime.toFixed(1), unit: 'h', color: '#0d82b8' },\n { icon: 'fa-rotate', label: '循环次数', value: sums.cycles.toString(), unit: '次', color: '#0f766e' },\n { icon: 'fa-pause', label: '怠速时长', value: sums.idleTime.toFixed(1), unit: 'h', color: '#ea570c' },\n { icon: 'fa-arrow-down', label: '装载时长', value: sums.loadTime.toFixed(1), unit: 'h', color: '#7c3aed' },\n { icon: 'fa-arrow-up', label: '卸载时长', value: sums.unloadTime.toFixed(1), unit: 'h', color: '#2563eb' },\n { icon: 'fa-truck-fast', label: '空车返回', value: sums.emptyReturnTime.toFixed(1), unit: 'h', color: '#16a34a' },\n { icon: 'fa-weight-hanging', label: '重车运行', value: sums.loadedRunTime.toFixed(1), unit: 'h', color: '#dc2626' },\n ];\n\n dom.metricsRow.innerHTML = cfg.map(c => `\n <div class=\"metric-card\">\n <div class=\"card-accent\"></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 // ==================== LINE CHART ====================\n function renderLineChart(canvas, records, groupKeys, timeLabels, 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 ctx.clearRect(0, 0, w, h);\n\n const pad = { top: 8, right: 14, bottom: 26, left: 40 };\n const cw = w - pad.left - pad.right;\n const ch = h - pad.top - pad.bottom;\n\n const legendItems = groupKeys.map((gk, i) => ({ key: gk, color: GROUP_COLORS[i % GROUP_COLORS.length] }));\n dom.lineLegend.innerHTML = legendItems.map(l => `<span><span class=\"dot\" style=\"background:${l.color}\"></span>${l.key}</span>`).join('');\n\n let maxVal = 0;\n records.forEach(r => { if (r.totalTime > maxVal) maxVal = r.totalTime; });\n maxVal = Math.ceil(maxVal * 1.2) || 10;\n\n const gridLines = 5;\n ctx.strokeStyle = '#eef0f4';\n ctx.lineWidth = 1;\n for (let i = 0; i <= gridLines; i++) {\n const y = pad.top + (ch / gridLines) * i;\n ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();\n ctx.fillStyle = '#94a3b8';\n ctx.font = '8px DM Sans, sans-serif';\n ctx.textAlign = 'right';\n ctx.fillText(Math.round(maxVal - (maxVal / gridLines) * i), pad.left - 5, y + 3);\n }\n\n const xStep = cw / Math.max(timeLabels.length - 1, 1);\n const maxLabels = Math.min(timeLabels.length, 14);\n const labelStep = Math.max(1, Math.floor(timeLabels.length / maxLabels));\n ctx.fillStyle = '#94a3b8';\n ctx.font = '8px DM Sans, sans-serif';\n ctx.textAlign = 'center';\n timeLabels.forEach((d, i) => {\n if (i % labelStep === 0 || i === timeLabels.length - 1) {\n ctx.fillText(fmtTimeLabel(d, granularity), pad.left + xStep * i, pad.top + ch + 14);\n }\n });\n\n groupKeys.forEach((gk, gi) => {\n const color = GROUP_COLORS[gi % GROUP_COLORS.length];\n const grpRecords = records.filter(r => r.groupKey === gk);\n ctx.strokeStyle = color;\n ctx.lineWidth = 2;\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n ctx.beginPath();\n grpRecords.forEach((r, i) => {\n const x = pad.left + xStep * i;\n const y = pad.top + ch - (r.totalTime / maxVal) * ch;\n if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);\n });\n ctx.stroke();\n\n grpRecords.forEach((r, i) => {\n const x = pad.left + xStep * i;\n const y = pad.top + ch - (r.totalTime / maxVal) * ch;\n ctx.fillStyle = '#ffffff';\n ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fill();\n ctx.fillStyle = color;\n ctx.beginPath(); ctx.arc(x, y, 1.8, 0, Math.PI * 2); ctx.fill();\n });\n });\n }\n\n // ==================== STACKED BAR CHART ====================\n function renderBarChart(canvas, records, timeLabels, 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 ctx.clearRect(0, 0, w, h);\n\n const pad = { top: 8, right: 14, bottom: 26, left: 42 };\n const cw = w - pad.left - pad.right;\n const ch = h - pad.top - pad.bottom;\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 => {\n datasets.forEach(ds => { cell[ds.key] += r[ds.key]; });\n });\n return cell;\n });\n\n let maxStack = 0;\n aggregated.forEach(c => {\n const s = datasets.reduce((a, ds) => a + c[ds.key], 0);\n if (s > maxStack) maxStack = s;\n });\n maxStack = Math.ceil(maxStack * 1.18) || 10;\n\n const gridLines = 4;\n ctx.strokeStyle = '#eef0f4';\n ctx.lineWidth = 1;\n for (let i = 0; i <= gridLines; i++) {\n const y = pad.top + (ch / gridLines) * i;\n ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(w - pad.right, y); ctx.stroke();\n ctx.fillStyle = '#94a3b8';\n ctx.font = '8px DM Sans, sans-serif';\n ctx.textAlign = 'right';\n ctx.fillText(Math.round(maxStack - (maxStack / gridLines) * i), pad.left - 5, y + 3);\n }\n\n const maxBars = Math.min(aggregated.length, 24);\n const step = Math.max(1, Math.floor(aggregated.length / maxBars));\n const displayIndices = aggregated.map((_, i) => i).filter((_, i) => i % step === 0 || i === aggregated.length - 1);\n const barGap = 3;\n const barW = (cw / displayIndices.length) - barGap;\n if (barW <= 0) return;\n\n displayIndices.forEach((ti, di) => {\n const x = pad.left + (cw / displayIndices.length) * di + barGap / 2;\n let stackY = pad.top + ch;\n datasets.forEach((ds, dsi) => {\n const val = aggregated[ti][ds.key];\n const barH = (val / maxStack) * ch;\n const y = stackY - barH;\n ctx.fillStyle = ds.color;\n ctx.fillRect(x, y, barW, barH);\n if (dsi > 0 && barH > 1.5) {\n ctx.fillStyle = 'rgba(255,255,255,0.25)';\n ctx.fillRect(x, y, barW, 0.5);\n }\n stackY = y;\n });\n\n const labelStep2 = Math.max(1, Math.floor(displayIndices.length / 8));\n if (di % labelStep2 === 0 || di === displayIndices.length - 1) {\n ctx.fillStyle = '#94a3b8';\n ctx.font = '8px DM Sans, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(fmtTimeLabel(timeLabels[ti], granularity), x + barW / 2, pad.top + ch + 14);\n }\n });\n }\n\n // ==================== DONUT CHART ====================\n function renderDonutChart(canvas, records) {\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 ctx.clearRect(0, 0, w, h);\n\n const sums = { loadTime: 0, haulTime: 0, unloadTime: 0 };\n records.forEach(r => {\n sums.loadTime += r.loadTime;\n sums.haulTime += (r.loadedRunTime + r.emptyReturnTime);\n sums.unloadTime += r.unloadTime;\n });\n const total = sums.loadTime + sums.haulTime + sums.unloadTime;\n\n const segments = [\n { label: '装载', value: sums.loadTime, pct: total > 0 ? (sums.loadTime / total * 100) : 0, color: '#ea570c' },\n { label: '行驶', value: sums.haulTime, pct: total > 0 ? (sums.haulTime / total * 100) : 0, color: '#2563eb' },\n { label: '卸载', value: sums.unloadTime, pct: total > 0 ? (sums.unloadTime / total * 100) : 0, color: '#7c3aed' },\n ];\n\n dom.ratioLegend.innerHTML = segments.map(s => `\n <div class=\"ratio-legend-item\">\n <div class=\"ratio-legend-dot\" style=\"background:${s.color}\"></div>\n <div>\n <div class=\"ratio-legend-val\">${s.pct.toFixed(1)}%</div>\n <div class=\"ratio-legend-label\">${s.label} ${s.value.toFixed(1)}h</div>\n </div>\n </div>\n `).join('');\n\n const cx = w / 2;\n const cy = h / 2;\n const outerR = Math.min(cx, cy) - 8;\n const innerR = outerR * 0.52;\n\n if (total === 0) {\n ctx.fillStyle = '#94a3b8';\n ctx.font = '10px DM Sans, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText('暂无数据', cx, cy + 4);\n return;\n }\n\n let startAngle = -Math.PI / 2;\n segments.forEach(seg => {\n const sliceAngle = (seg.pct / 100) * Math.PI * 2;\n if (sliceAngle < 0.001) return;\n ctx.beginPath();\n ctx.arc(cx, cy, outerR, startAngle, startAngle + sliceAngle);\n ctx.arc(cx, cy, innerR, startAngle + sliceAngle, startAngle, true);\n ctx.closePath();\n ctx.fillStyle = seg.color;\n ctx.fill();\n startAngle += sliceAngle;\n });\n\n ctx.fillStyle = '#0f172a';\n ctx.font = 'bold 11px DM Sans, sans-serif';\n ctx.textAlign = 'center';\n ctx.fillText(total.toFixed(1) + 'h', cx, cy - 2);\n ctx.fillStyle = '#94a3b8';\n ctx.font = '7px DM Sans, sans-serif';\n ctx.fillText('作业总时长', cx, cy + 11);\n }\n\n // ==================== TABLE ====================\n function renderTable(records, granularity) {\n dom.tableInfo.textContent = `共 ${records.length} 条记录`;\n if (records.length === 0) {\n dom.tableHead.innerHTML = `<tr><th>周期</th><th>${state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆'}</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:12px;\">暂无数据</td></tr>';\n return;\n }\n\n const groupColLabel = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n dom.tableHead.innerHTML = `\n <tr>\n <th>周期</th>\n <th>${groupColLabel}</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\n const display = records.slice(-30);\n dom.tableBody.innerHTML = display.map(r => `\n <tr>\n <td>${fmtTimeFull(r.timeLabel, granularity)}</td>\n <td>${r.groupLabel}</td>\n <td class=\"cell-highlight\">${r.totalTime.toFixed(1)}</td>\n <td>${r.workTime.toFixed(1)}</td>\n <td class=\"cell-cycles\">${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 ====================\n let currentData = { records: [], groupKeys: [], timeLabels: [] };\n\n function updateDashboard(silent) {\n currentData = generateRecords();\n const { records, groupKeys, timeLabels } = currentData;\n\n renderMetrics(records);\n renderLineChart(dom.lineChart, records, groupKeys, timeLabels, state.granularity);\n renderBarChart(dom.barChart, records, timeLabels, state.granularity);\n renderDonutChart(dom.donutChart, records);\n renderTable(records, state.granularity);\n\n if (!silent) {\n const granLabel = state.granularity === 'day' ? '日' : state.granularity === 'month' ? '月' : '年';\n const groupLabel = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n showToast(`已按${groupLabel}加载 ${records.length} 条${granLabel}度运营数据`, 'fa-circle-check');\n }\n }\n\n // ==================== EVENTS ====================\n dom.granularityGroup.addEventListener('click', (e) => {\n const btn = e.target.closest('button');\n if (!btn || btn.classList.contains('active')) return;\n dom.granularityGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n state.granularity = btn.dataset.gran;\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.groupByGroup.addEventListener('click', (e) => {\n const btn = e.target.closest('button');\n if (!btn || btn.classList.contains('active')) return;\n dom.groupByGroup.querySelectorAll('button').forEach(b => b.classList.remove('active'));\n btn.classList.add('active');\n state.groupBy = btn.dataset.group;\n populateFilterOptions();\n updateDashboard();\n });\n\n dom.filterMine.addEventListener('change', () => {\n state.filterMine = dom.filterMine.value;\n populateFilterOptions();\n updateDashboard();\n });\n\n dom.filterType.addEventListener('change', () => {\n state.filterType = dom.filterType.value;\n populateFilterOptions();\n updateDashboard();\n });\n\n dom.filterVehicle.addEventListener('change', () => {\n state.filterVehicle = dom.filterVehicle.value;\n updateDashboard();\n });\n\n dom.startDate.addEventListener('change', () => { state.startDate = dom.startDate.value; updateDashboard(); });\n dom.endDate.addEventListener('change', () => { state.endDate = dom.endDate.value; updateDashboard(); });\n\n // ==================== GLOBALS ====================\n window.refreshData = function() {\n state.startDate = dom.startDate.value;\n state.endDate = dom.endDate.value;\n state.filterMine = dom.filterMine.value;\n state.filterType = dom.filterType.value;\n state.filterVehicle = dom.filterVehicle.value;\n updateDashboard();\n };\n\n window.exportCSV = function() {\n if (currentData.records.length === 0) { showToast('无数据可导出', 'fa-triangle-exclamation'); return; }\n const groupCol = state.groupBy === 'mine' ? '矿区' : state.groupBy === 'type' ? '车辆类型' : '车辆';\n let csv = `周期,${groupCol},运行时长(h),作业时长(h),循环次数,怠速时长(h),装载时长(h),卸载时长(h),空车返回(h),重车运行(h)\\n`;\n currentData.records.forEach(r => {\n csv += [\n fmtTimeFull(r.timeLabel, state.granularity), r.groupLabel,\n r.totalTime.toFixed(1), r.workTime.toFixed(1), r.cycles,\n r.idleTime.toFixed(1), r.loadTime.toFixed(1), r.unloadTime.toFixed(1),\n r.emptyReturnTime.toFixed(1), 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.groupBy}_${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 const { records, groupKeys, timeLabels } = currentData;\n if (records.length > 0) {\n renderLineChart(dom.lineChart, records, groupKeys, timeLabels, state.granularity);\n renderBarChart(dom.barChart, records, timeLabels, state.granularity);\n renderDonutChart(dom.donutChart, records);\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 populateFilterOptions();\n updateDashboard(true);\n }\n\n init();\n})();\n</script>\n\n</body>\n</html>"
},
"next": ["按班次拆分统计", "添加设备稼动率", "对比多矿区运营", "设置异常预警线", "导出PDF运营报告"],
"breakpoint": "none"
}
```
推理
用户希望我将矿车生产运营数据统计界面(最早的那个矿车运营数据仪表盘)也增加矿区/车辆类型/车辆的筛选与分组统计功能,就像刚才的载荷与生产效率统计界面那样。
回顾最早的矿车运营数据统计指标:
- 运行时长
- 作业时长(= 卸载+装载+空车返回+重车运行)
- 作业循环次数(空车+装载+重车运行+卸载 = 1次循环)
- 怠速时长
- 装载时长
- 卸载时长
- 空车返回时间
- 重车运行时间
还要保留:
- 时间区间选取
- 按年/月/天统计分析
- 显示每天/月/年的分布情况
- 装载/行驶/卸载时间占比分析
新增功能:
- 按矿区筛选
- 按车辆类型筛选
- 按车辆筛选
- 分组统计(按矿区/类型/车辆)
设计风格:白底企业级设计,与之前的界面风格保持一致。
我需要重新生成一个完整的HTML页面,整合矿车运营数据的所有功能加上新的筛选和分组功能。
让我参考载荷页面中的车辆数据结构和筛选逻辑,类似地构建矿车运营数据的版本。
关键设计:
- 5项指标卡片或8项指标卡片(根据之前的矿车界面有8项核心指标)
- 折线图:运行趋势(运行时长、作业时长、循环次数)
- 堆叠柱状图:时长构成(装载、重车、卸载、空返、怠速)
- 环形图:装载/行驶/卸载占比
- 数据明细表
- 筛选栏:分组方式(按矿区/车辆类型/车辆)+ 矿区下拉 + 车辆类型下拉 + 车辆下拉
- 时间粒度切换 + 日期区间
这是一个比较大的界面,需要仔细设计。考虑到是16:9的比例,需要合理利用空间。
让我开始构建这个界面。
我会复用载荷页面中的车辆数据(VEHICLES数组),但针对矿车运营数据生成不同的指标数据。
8项度量指标卡片可能在一行内排列会比较紧凑,考虑到16:9宽屏,可以放8列。但如果加上筛选栏可能会占空间。让我将指标卡片做成8列网格。
图表面板可以用3列(折线+堆叠柱状+环形占比),这与之前矿车运营数据的设计一致。
数据表放在底部。
筛选栏放在header下方,与载荷页面类似。
现在让我构建完整的HTML。
回顾最早的矿车运营数据统计指标:
- 运行时长
- 作业时长(= 卸载+装载+空车返回+重车运行)
- 作业循环次数(空车+装载+重车运行+卸载 = 1次循环)
- 怠速时长
- 装载时长
- 卸载时长
- 空车返回时间
- 重车运行时间
还要保留:
- 时间区间选取
- 按年/月/天统计分析
- 显示每天/月/年的分布情况
- 装载/行驶/卸载时间占比分析
新增功能:
- 按矿区筛选
- 按车辆类型筛选
- 按车辆筛选
- 分组统计(按矿区/类型/车辆)
设计风格:白底企业级设计,与之前的界面风格保持一致。
我需要重新生成一个完整的HTML页面,整合矿车运营数据的所有功能加上新的筛选和分组功能。
让我参考载荷页面中的车辆数据结构和筛选逻辑,类似地构建矿车运营数据的版本。
关键设计:
- 5项指标卡片或8项指标卡片(根据之前的矿车界面有8项核心指标)
- 折线图:运行趋势(运行时长、作业时长、循环次数)
- 堆叠柱状图:时长构成(装载、重车、卸载、空返、怠速)
- 环形图:装载/行驶/卸载占比
- 数据明细表
- 筛选栏:分组方式(按矿区/车辆类型/车辆)+ 矿区下拉 + 车辆类型下拉 + 车辆下拉
- 时间粒度切换 + 日期区间
这是一个比较大的界面,需要仔细设计。考虑到是16:9的比例,需要合理利用空间。
让我开始构建这个界面。
我会复用载荷页面中的车辆数据(VEHICLES数组),但针对矿车运营数据生成不同的指标数据。
8项度量指标卡片可能在一行内排列会比较紧凑,考虑到16:9宽屏,可以放8列。但如果加上筛选栏可能会占空间。让我将指标卡片做成8列网格。
图表面板可以用3列(折线+堆叠柱状+环形占比),这与之前矿车运营数据的设计一致。
数据表放在底部。
筛选栏放在header下方,与载荷页面类似。
现在让我构建完整的HTML。
角色
assistant
content
total_tokens
135435
summary
矿车生产运营数据统计仪表盘(增强版),白底企业级设计,8项核心指标+三图联动(折线趋势/堆叠柱状/环形占比),新增矿区→车辆类型→车辆三级联动筛选与分组统计,支持年/月/日粒度、日期区间选取、CSV导出与打印。
uiParsing
关闭