```json
{
"summary": "燃油/能耗管理仪表盘(改版),白底企业级设计,5项核心指标(燃油消耗/吨公里油耗/怠速油耗/驱动能耗/电动能耗),消耗趋势折线图新增驱动能耗曲线并下置吨公里油耗柱状分布图,矿区→车型→车辆三级筛选,横轴按日/月/年切换,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-green: #0d9488;\n --accent-amber: #d4891a;\n --accent-orange: #e8590c;\n --accent-teal: #0e7bb5;\n --accent-purple: #7c3aed;\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 * { box-sizing: border-box; margin: 0; padding: 0; }\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(13,148,136,0.035) 0%, transparent 55%),\n radial-gradient(ellipse at 85% 90%, rgba(14,123,181,0.025) 0%, transparent 55%);\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 .dashboard::before {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background:\n linear-gradient(180deg, rgba(13,148,136,0.012) 0%, transparent 25%),\n linear-gradient(0deg, rgba(14,123,181,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 { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-shrink: 0; padding-bottom: 3px; border-bottom: 1px solid var(--border-light); }\n .header-left { display: flex; align-items: center; gap: 7px; }\n .logo-mark { width: 28px; height: 28px; background: linear-gradient(135deg, #0d9488, #0f766e); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; font-size: 13px; color: #ffffff; font-weight: 700; flex-shrink: 0; box-shadow: 0 2px 8px rgba(13,148,136,0.22); }\n .header-title h1 { font-family: var(--font-display); font-size: clamp(12px, 1vw, 16px); font-weight: 700; letter-spacing: -0.01em; color: var(--text-primary); line-height: 1.1; }\n .header-title span { font-size: 7px; color: var(--text-muted); font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; }\n .header-right { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; justify-content: flex-end; }\n\n .btn-group { display: flex; background: var(--bg-card); border-radius: var(--radius); border: 1px solid var(--border); overflow: hidden; box-shadow: var(--shadow-xs); }\n .btn-group button { background: transparent; border: none; color: var(--text-secondary); padding: 3px 7px; cursor: pointer; font-family: var(--font-display); font-size: 9px; font-weight: 600; letter-spacing: 0.01em; transition: all var(--transition-base); white-space: nowrap; position: relative; }\n .btn-group button:not(:last-child)::after { content: ''; position: absolute; right: 0; top: 14%; height: 72%; width: 1px; background: var(--border); }\n .btn-group button:hover { color: var(--text-primary); background: var(--bg-elevated); }\n .btn-group button.active { color: #fff; background: #0d9488; box-shadow: 0 0 8px rgba(13,148,136,0.2); border-radius: 0; }\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 { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-primary); padding: 3px 5px; border-radius: var(--radius); font-family: var(--font-display); font-size: 9px; outline: none; transition: all var(--transition-base); cursor: pointer; box-shadow: var(--shadow-xs); }\n select:focus, .date-input:focus { border-color: #0d9488; box-shadow: 0 0 0 2px rgba(13,148,136,0.1); }\n .date-input { width: 90px; } select { min-width: 58px; max-width: 100px; }\n .date-separator { color: var(--text-muted); font-size: 9px; font-weight: 500; }\n .filter-label { font-size: 6px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }\n\n .btn-action { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary); width: 22px; height: 22px; border-radius: var(--radius); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--transition-base); font-size: 9px; box-shadow: var(--shadow-xs); }\n .btn-action:hover { color: var(--text-primary); border-color: var(--border-active); background: var(--bg-elevated); }\n .btn-primary { background: linear-gradient(135deg, #0d9488, #0f766e); border: none; color: #fff; padding: 3px 10px; border-radius: var(--radius); cursor: pointer; font-family: var(--font-display); font-size: 9px; font-weight: 600; letter-spacing: 0.01em; transition: all var(--transition-base); white-space: nowrap; box-shadow: 0 2px 6px rgba(13,148,136,0.2); }\n .btn-primary:hover { filter: brightness(1.07); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(13,148,136,0.26); }\n .btn-primary:active { transform: scale(0.97); }\n\n .filter-bar { display: flex; align-items: center; gap: 4px; flex-shrink: 0; flex-wrap: wrap; padding: 2px 0; border-bottom: 1px solid var(--border-light); }\n .filter-bar .sep { width: 1px; height: 14px; background: var(--border); margin: 0 1px; }\n\n .metrics-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; flex-shrink: 0; }\n .metric-card { background: var(--bg-card); border-radius: var(--radius-md); padding: 5px 7px; border: 1px solid var(--border-light); position: relative; overflow: hidden; transition: all var(--transition-base); cursor: default; display: flex; flex-direction: column; gap: 1px; box-shadow: var(--shadow-sm); }\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: #0d9488; }\n .metric-card:nth-child(2) .card-accent { background: #d4891a; }\n .metric-card:nth-child(3) .card-accent { background: #e8590c; }\n .metric-card:nth-child(4) .card-accent { background: #7c3aed; }\n .metric-card:nth-child(5) .card-accent { background: #0e7bb5; }\n .metric-card .card-icon { font-size: 7px; color: var(--text-muted); }\n .metric-card .card-label { font-size: 6px; 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(10px, 1.1vw, 15px); font-weight: 700; color: var(--text-primary); letter-spacing: -0.02em; line-height: 1; }\n .metric-card .card-sub { font-size: 5px; color: var(--text-muted); font-weight: 500; }\n\n .charts-area { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 4px; }\n .chart-box-top { flex: 0 0 52%; background: var(--bg-card); border-radius: var(--radius-md); border: 1px solid var(--border-light); padding: 7px 10px; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-sm); }\n .chart-box-bottom { flex: 1; background: var(--bg-card); border-radius: var(--radius-md); border: 1px solid var(--border-light); padding: 7px 10px; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-sm); }\n .chart-box-header { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; margin-bottom: 3px; gap: 4px; }\n .chart-box-title { font-family: var(--font-display); font-size: 8px; font-weight: 600; letter-spacing: 0.02em; color: var(--text-secondary); text-transform: uppercase; white-space: nowrap; }\n .chart-legend { display: flex; gap: 4px; font-size: 6px; color: var(--text-muted); flex-wrap: wrap; justify-content: flex-end; }\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-legend .dash { width: 6px; height: 0; border-top: 1.5px dashed; 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 .table-section { flex-shrink: 0; background: var(--bg-card); border-radius: var(--radius-md); border: 1px solid var(--border-light); overflow: hidden; display: flex; flex-direction: column; max-height: 80px; box-shadow: var(--shadow-sm); }\n .table-section-header { display: flex; align-items: center; justify-content: space-between; padding: 2px 9px; border-bottom: 1px solid var(--border-light); flex-shrink: 0; }\n .table-section-title { font-family: var(--font-display); font-size: 8px; 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: 2px; }\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: 6px; font-family: var(--font-display); }\n thead th { text-align: left; padding: 2px 4px; color: var(--text-muted); font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; font-size: 5px; position: sticky; top: 0; background: var(--bg-card); z-index: 2; border-bottom: 1px solid var(--border-light); }\n tbody td { padding: 2px 4px; 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(13,148,136,0.03); }\n tbody tr { transition: background var(--transition-base); }\n .cell-highlight { color: #0d9488; font-weight: 700; }\n .cell-warn { color: #e8590c; font-weight: 600; }\n .cell-info { color: #0e7bb5; font-weight: 600; }\n\n .toast-container { position: fixed; top: 10px; right: 10px; z-index: 9999; display: flex; flex-direction: column; gap: 4px; pointer-events: none; }\n .toast { background: #fff; border: 1px solid var(--border); border-left: 3px solid #0d9488; color: var(--text-primary); padding: 5px 10px; border-radius: var(--radius); font-size: 10px; font-family: var(--font-body); box-shadow: 0 4px 18px rgba(0,0,0,0.1); opacity: 0; transform: translateX(30px); transition: all 280ms cubic-bezier(0.4,0,0.2,1); pointer-events: auto; display: flex; align-items: center; gap: 4px; }\n .toast.show { opacity: 1; transform: translateX(0); }\n .toast i { color: #0d9488; font-size: 11px; }\n\n @media (max-width: 1400px) {\n .metrics-row { grid-template-columns: repeat(3, 1fr); }\n .dashboard { aspect-ratio: auto; max-height: none; }\n .chart-box-top { flex: 0 0 200px; }\n .chart-box-bottom { min-height: 180px; }\n }\n @media (max-width: 900px) {\n .metrics-row { grid-template-columns: repeat(2, 1fr); }\n .header { flex-direction: column; align-items: stretch; }\n .header-right { justify-content: flex-start; }\n select, .date-input { font-size: 8px; padding: 2px 4px; }\n .date-input { width: 72px; }\n .dashboard { padding: 5px; }\n }\n @media (max-width: 600px) {\n .metrics-row { grid-template-columns: 1fr 1fr; }\n .btn-group button { padding: 2px 5px; font-size: 8px; }\n .header-right { gap: 2px; }\n table { font-size: 5px; }\n thead th, tbody td { padding: 1px 2px; }\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, select, .date-input, .filter-bar { display: none; }\n .toast-container { display: none; }\n }\n </style>\n</head>\n<body>\n\n<div class=\"dashboard\" id=\"dashboard\">\n\n <div class=\"header\">\n <div class=\"header-left\">\n <div class=\"logo-mark\"><i class=\"fa-solid fa-gas-pump\"></i></div>\n <div class=\"header-title\"><h1>燃油 / 能耗管理统计</h1><span>Fuel & Energy Consumption Analytics</span></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 <div class=\"filter-bar\">\n <span class=\"filter-label\"><i class=\"fa-solid fa-filter\"></i> 筛选</span>\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 <span id=\"filterSummary\" style=\"font-size:6px;color:var(--text-muted);margin-left:2px;\"></span>\n </div>\n\n <div class=\"metrics-row\" id=\"metricsRow\"></div>\n\n <div class=\"charts-area\">\n <div class=\"chart-box-top\" 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\">\n <span><span class=\"dot\" style=\"background:#0d9488\"></span>燃油消耗</span>\n <span><span class=\"dot\" style=\"background:#e8590c\"></span>怠速油耗</span>\n <span><span class=\"dash\" style=\"border-color:#0e7bb5\"></span>电动能耗</span>\n <span><span class=\"dot\" style=\"background:#7c3aed\"></span>驱动能耗</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"lineChart\"></canvas></div>\n </div>\n <div class=\"chart-box-bottom\" 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:#d4891a\"></span>吨公里油耗</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"barChart\"></canvas></div>\n </div>\n </div>\n\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:6px;color:var(--text-muted)\" id=\"tableInfo\">共 0 条记录</span>\n </div>\n <div class=\"table-wrap\">\n <table>\n <thead>\n <tr>\n <th>周期</th>\n <th>燃油消耗(L)</th>\n <th>吨公里油耗</th>\n <th>怠速油耗(L)</th>\n <th>驱动能耗(L)</th>\n <th>电动能耗(kWh)</th>\n <th>运量(t·km)</th>\n </tr>\n </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 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 const state = {\n granularity: 'day',\n startDate: '2026-04-01',\n endDate: '2026-04-27',\n filterMine: '',\n filterType: '',\n filterVehicle: '',\n };\n\n const $ = s => document.querySelector(s);\n const dom = {\n granularityGroup: $('#granularityGroup'),\n startDate: $('#startDate'), endDate: $('#endDate'),\n filterMine: $('#filterMine'), filterType: $('#filterType'), filterVehicle: $('#filterVehicle'),\n filterSummary: $('#filterSummary'),\n metricsRow: $('#metricsRow'),\n lineChart: $('#lineChart'), barChart: $('#barChart'),\n 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 const fv = getFilteredVehicles();\n dom.filterSummary.textContent = fv.length < VEHICLES.length ? `已筛选 ${fv.length}/${VEHICLES.length} 辆车` : '';\n }\n\n function seedRandom(seed) { let s = seed; return () => { s = (s * 16807 + 0) % 2147483647; return (s - 1) / 2147483646; }; }\n\n function generateRecords() {\n const vehicles = getFilteredVehicles();\n const records = [];\n const sd = new Date(state.startDate), ed = new Date(state.endDate);\n if (isNaN(sd.getTime()) || isNaN(ed.getTime()) || sd > ed || vehicles.length === 0) return records;\n\n const 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).map((d, i) => genOne(d, i, vehicles)); }\n return labels.map((d, i) => genOne(d, i, vehicles));\n }\n\n function genOne(d, i, vehicles) {\n const vCount = vehicles.length;\n let fuelConsumption = 0, idleFuel = 0, driveFuel = 0, electricEnergy = 0, tonKm = 0;\n\n vehicles.forEach(v => {\n const rng = seedRandom(d.getTime() % 99997 + v.id.charCodeAt(0) * 73 + (i||0) * 41 + v.capacity * 7);\n const bm = state.granularity === 'year' ? 300 : state.granularity === 'month' ? 26 : 1;\n\n const vTonKm = Math.round(bm * (180 + rng() * 420) * 10) / 10;\n const vFuel = Math.round(bm * (45 + rng() * 120) * 10) / 10;\n const idleRate = 0.15 + rng() * 0.20;\n const vIdleFuel = Math.round(vFuel * idleRate * 10) / 10;\n const vDriveFuel = Math.round((vFuel - vIdleFuel) * 10) / 10;\n const vElectric = Math.round(bm * (15 + rng() * 65) * 10) / 10;\n\n fuelConsumption += vFuel;\n idleFuel += vIdleFuel;\n driveFuel += vDriveFuel;\n electricEnergy += vElectric;\n tonKm += vTonKm;\n });\n\n const tonKmFuel = tonKm > 0 ? Math.round((fuelConsumption / tonKm) * 1000) / 1000 : 0;\n\n return {\n label: d,\n fuelConsumption: Math.round(fuelConsumption * 10) / 10,\n tonKmFuel,\n idleFuel: Math.round(idleFuel * 10) / 10,\n driveFuel: Math.round(driveFuel * 10) / 10,\n electricEnergy: Math.round(electricEnergy * 10) / 10,\n tonKm: Math.round(tonKm * 10) / 10,\n };\n }\n\n function fmtLabel(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 fmtFull(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 function renderMetrics(records) {\n if (!records.length) {\n dom.metricsRow.innerHTML = Array.from({ length: 5 }, () => `<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) => {\n a.fuelConsumption += r.fuelConsumption;\n a.idleFuel += r.idleFuel;\n a.driveFuel += r.driveFuel;\n a.electricEnergy += r.electricEnergy;\n a.tonKm += r.tonKm;\n return a;\n }, { fuelConsumption: 0, idleFuel: 0, driveFuel: 0, electricEnergy: 0, tonKm: 0 });\n\n const avgTonKmFuel = sm.tonKm > 0 ? Math.round((sm.fuelConsumption / sm.tonKm) * 1000) / 1000 : 0;\n\n const cfg = [\n { icon: 'fa-gas-pump', label: '燃油消耗', value: sm.fuelConsumption.toFixed(1), unit: 'L', color: '#0d9488' },\n { icon: 'fa-weight-hanging', label: '吨公里油耗', value: avgTonKmFuel.toFixed(3), unit: 'L/t·km', color: '#d4891a' },\n { icon: 'fa-pause', label: '怠速油耗', value: sm.idleFuel.toFixed(1), unit: 'L', color: '#e8590c' },\n { icon: 'fa-gauge-high', label: '驱动能耗', value: sm.driveFuel.toFixed(1), unit: 'L', color: '#7c3aed' },\n { icon: 'fa-bolt', label: '电动设备能耗', value: sm.electricEnergy.toFixed(1), unit: 'kWh', color: '#0e7bb5' },\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>`).join('');\n }\n\n function renderLineChart(canvas, records) {\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;\n 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: 6, right: 10, bottom: 22, left: 36 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n const lineDefs = [\n { key: 'fuelConsumption', color: '#0d9488', dash: false },\n { key: 'idleFuel', color: '#e8590c', dash: false },\n { key: 'electricEnergy', color: '#0e7bb5', dash: true },\n { key: 'driveFuel', color: '#7c3aed', dash: false },\n ];\n\n let mx = 0;\n lineDefs.forEach(def => { records.forEach(r => { if (r[def.key] > mx) mx = r[def.key]; }); });\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 = '6px DM Sans, sans-serif'; ctx.textAlign = 'right';\n ctx.fillText((mx - (mx / gl) * i).toFixed(0), pad.left - 3, y + 3);\n }\n\n const xs = cw / Math.max(records.length - 1, 1);\n const ml = Math.min(records.length, 14);\n const ls = Math.max(1, Math.floor(records.length / ml));\n ctx.fillStyle = '#94a3b8'; ctx.font = '6px DM Sans, sans-serif'; ctx.textAlign = 'center';\n records.forEach((r, i) => { if (i % ls === 0 || i === records.length - 1) ctx.fillText(fmtLabel(r.label), pad.left + xs * i, pad.top + ch + 12); });\n\n lineDefs.forEach(def => {\n ctx.strokeStyle = def.color; ctx.lineWidth = 1.8; ctx.lineJoin = 'round'; ctx.lineCap = 'round';\n ctx.setLineDash(def.dash ? [4, 3] : []);\n ctx.beginPath();\n records.forEach((r, ii) => {\n const x = pad.left + xs * ii, y = pad.top + ch - (r[def.key] / mx) * ch;\n if (ii === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);\n });\n ctx.stroke();\n ctx.setLineDash([]);\n if (!def.dash) {\n records.forEach((r, ii) => {\n const x = pad.left + xs * ii, y = pad.top + ch - (r[def.key] / mx) * ch;\n ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI * 2); ctx.fill();\n ctx.fillStyle = def.color; ctx.beginPath(); ctx.arc(x, y, 1.5, 0, Math.PI * 2); ctx.fill();\n });\n }\n });\n }\n\n function renderBarChart(canvas, records) {\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;\n 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: 6, right: 10, bottom: 22, left: 40 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n let mx = 0;\n records.forEach(r => { if (r.tonKmFuel > mx) mx = r.tonKmFuel; });\n mx = Math.ceil(mx * 1.3 * 1000) / 1000;\n if (mx === 0) mx = 0.1;\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 = '6px DM Sans, sans-serif'; ctx.textAlign = 'right';\n ctx.fillText((mx - (mx / gl) * i).toFixed(3), pad.left - 3, y + 3);\n }\n\n const maxBars = Math.min(records.length, 28);\n const step = Math.max(1, Math.floor(records.length / maxBars));\n const di = records.map((_, i) => i).filter((_, i) => i % step === 0 || i === records.length - 1);\n const barGap = 3;\n const barW = (cw / di.length) - barGap;\n if (barW <= 0) return;\n\n di.forEach((ri, bi) => {\n const r = records[ri];\n const x = pad.left + (cw / di.length) * bi + barGap / 2;\n const bh = (r.tonKmFuel / mx) * ch;\n const y = pad.top + ch - bh;\n const grad = ctx.createLinearGradient(x, y, x, pad.top + ch);\n grad.addColorStop(0, '#e8b84b');\n grad.addColorStop(1, '#d4891a');\n ctx.fillStyle = grad;\n ctx.fillRect(x, y, barW, bh);\n const ls2 = Math.max(1, Math.floor(di.length / 8));\n if (bi % ls2 === 0 || bi === di.length - 1) {\n ctx.fillStyle = '#94a3b8'; ctx.font = '6px DM Sans, sans-serif'; ctx.textAlign = 'center';\n ctx.fillText(fmtLabel(r.label), x + barW / 2, pad.top + ch + 12);\n }\n });\n }\n\n function renderTable(records) {\n dom.tableInfo.textContent = `共 ${records.length} 条记录`;\n if (!records.length) { dom.tableBody.innerHTML = '<tr><td colspan=\"7\" style=\"text-align:center;color:var(--text-muted);padding:6px;\">暂无数据</td></tr>'; return; }\n const disp = records.slice(-16);\n dom.tableBody.innerHTML = disp.map(r => `\n <tr>\n <td>${fmtFull(r.label)}</td>\n <td class=\"cell-highlight\">${r.fuelConsumption.toFixed(1)}</td>\n <td>${r.tonKmFuel.toFixed(3)}</td>\n <td class=\"cell-warn\">${r.idleFuel.toFixed(1)}</td>\n <td>${r.driveFuel.toFixed(1)}</td>\n <td class=\"cell-info\">${r.electricEnergy.toFixed(1)}</td>\n <td>${r.tonKm.toFixed(1)}</td>\n </tr>`).join('');\n }\n\n let currentRecords = [];\n\n function updateDashboard(silent) {\n currentRecords = generateRecords();\n renderMetrics(currentRecords);\n renderLineChart(dom.lineChart, currentRecords);\n renderBarChart(dom.barChart, currentRecords);\n renderTable(currentRecords);\n populateFilterOptions();\n if (!silent) {\n const gl = state.granularity === 'day' ? '日' : state.granularity === 'month' ? '月' : '年';\n const vc = getFilteredVehicles().length;\n showToast(`横轴按${gl} · ${vc}辆车 · 已加载 ${currentRecords.length} 条能耗数据`, 'fa-circle-check');\n }\n }\n\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\n dom.filterMine.addEventListener('change', () => { state.filterMine = dom.filterMine.value; state.filterType = ''; state.filterVehicle = ''; updateDashboard(); });\n dom.filterType.addEventListener('change', () => { state.filterType = dom.filterType.value; state.filterVehicle = ''; 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() {\n state.startDate = dom.startDate.value; state.endDate = dom.endDate.value;\n state.filterMine = dom.filterMine.value; state.filterType = dom.filterType.value; state.filterVehicle = dom.filterVehicle.value;\n updateDashboard();\n };\n\n window.exportCSV = function() {\n if (!currentRecords.length) { showToast('无数据可导出', 'fa-triangle-exclamation'); return; }\n let csv = '周期,燃油消耗(L),吨公里油耗(L/t·km),怠速油耗(L),驱动能耗(L),电动能耗(kWh),运量(t·km)\\n';\n currentRecords.forEach(r => {\n csv += [fmtFull(r.label), r.fuelConsumption.toFixed(1), r.tonKmFuel.toFixed(3), r.idleFuel.toFixed(1), r.driveFuel.toFixed(1), r.electricEnergy.toFixed(1), r.tonKm.toFixed(1)].join(',') + '\\n';\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');\n a.href = url; a.download = `能耗管理统计_${state.granularity}_${state.startDate}_${state.endDate}.csv`; a.click();\n URL.revokeObjectURL(url);\n showToast('CSV 已下载', 'fa-file-csv');\n };\n\n window.printDashboard = function() { showToast('正在准备打印...', 'fa-print'); setTimeout(() => window.print(), 400); };\n\n let rt;\n window.addEventListener('resize', () => {\n clearTimeout(rt); rt = setTimeout(() => {\n if (currentRecords.length > 0) { renderLineChart(dom.lineChart, currentRecords); renderBarChart(dom.barChart, currentRecords); }\n }, 250);\n });\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 updateDashboard(true);\n }\n init();\n})();\n</script>\n\n</body>\n</html>"
},
"next": ["驱动能耗按车型拆分", "添加能耗异常预警线", "导出PDF能耗报告", "对比月度能耗趋势", "接入实时油耗数据"],
"breakpoint": "none"
}
```
推理
The user wants to modify the fuel/energy consumption management dashboard. Key changes:
1. In the "消耗趋势" (consumption trend) line chart, add "驱动能耗" (drive energy consumption)
2. Move "吨公里油耗分布" (ton-km fuel consumption distribution) below the "消耗趋势" chart
Let me recall the most recent fuel/energy dashboard. I need to:
- Add "驱动能耗" as a new metric/data point alongside 燃油消耗, 怠速油耗, 电动能耗
- Rearrange the layout so the ton-km fuel distribution bar chart is below the consumption trend line chart
The driving energy consumption would be the energy used for actual driving (loaded + empty return), which is different from idle consumption.
Let me redesign this. The layout should be:
- Top row: 5 metric cards
- Charts area: line chart (消耗趋势 with 4 lines: 燃油消耗, 怠速油耗, 电动能耗, 驱动能耗) on top, bar chart (吨公里油耗分布) below
- Table at bottom
Actually, looking at the most recent fuel dashboard design, it had:
- Header
- Filter bar
- 4 metric cards
- Charts area with 2 columns: line chart (消耗趋势) + bar chart (吨公里油耗分布)
- Table
Now the user wants:
- Add 驱动能耗 to the consumption trend
- Move 吨公里油耗分布 below 消耗趋势
So the charts area becomes a single column with line chart on top and bar chart below, or we keep it 2-column but put the bar chart below in a full-width row.
Let me think about this more carefully. The user says "吨公里油耗分布放到消耗趋势下面" - so the bar chart goes below the line chart. This suggests a top-bottom layout rather than side-by-side.
I'll restructure the charts area:
- Top: full-width 消耗趋势 line chart (with 驱动能耗 added)
- Bottom: full-width 吨公里油耗分布 bar chart
And add 驱动能耗 as a new data point. Drive energy = fuel used for actual driving (not idle). It's essentially 燃油消耗 - 怠速油耗, converted to appropriate units. Or it could be a separate calculated quantity representing the energy consumed during loaded+empty running.
Let me also add 驱动能耗 as a metric card (making it 5 cards total), or keep 4 cards and just add it to the chart.
I think 5 cards makes sense: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗.
Actually wait - let me re-read. The user says to add 驱动能耗 to the consumption trend chart. This could mean adding it as a new line in the line chart. And also possibly as a new metric card.
Let me keep it clean - add 驱动能耗 as a new metric card and add it as a line in the trend chart. Rearrange layout so bar chart is below line chart.
Let me design this now. I'll go with:
- 5 metric cards: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗
- Charts: line chart full width (4 lines: 燃油消耗, 怠速油耗, 电动能耗, 驱动能耗), then bar chart full width below (吨公里油耗分布)
- Table
Actually, thinking about it more - if both charts are full width, it might be too tall. Let me use a reasonable proportion.
Let me go with the design. I'll keep the white background enterprise style consistent.
1. In the "消耗趋势" (consumption trend) line chart, add "驱动能耗" (drive energy consumption)
2. Move "吨公里油耗分布" (ton-km fuel consumption distribution) below the "消耗趋势" chart
Let me recall the most recent fuel/energy dashboard. I need to:
- Add "驱动能耗" as a new metric/data point alongside 燃油消耗, 怠速油耗, 电动能耗
- Rearrange the layout so the ton-km fuel distribution bar chart is below the consumption trend line chart
The driving energy consumption would be the energy used for actual driving (loaded + empty return), which is different from idle consumption.
Let me redesign this. The layout should be:
- Top row: 5 metric cards
- Charts area: line chart (消耗趋势 with 4 lines: 燃油消耗, 怠速油耗, 电动能耗, 驱动能耗) on top, bar chart (吨公里油耗分布) below
- Table at bottom
Actually, looking at the most recent fuel dashboard design, it had:
- Header
- Filter bar
- 4 metric cards
- Charts area with 2 columns: line chart (消耗趋势) + bar chart (吨公里油耗分布)
- Table
Now the user wants:
- Add 驱动能耗 to the consumption trend
- Move 吨公里油耗分布 below 消耗趋势
So the charts area becomes a single column with line chart on top and bar chart below, or we keep it 2-column but put the bar chart below in a full-width row.
Let me think about this more carefully. The user says "吨公里油耗分布放到消耗趋势下面" - so the bar chart goes below the line chart. This suggests a top-bottom layout rather than side-by-side.
I'll restructure the charts area:
- Top: full-width 消耗趋势 line chart (with 驱动能耗 added)
- Bottom: full-width 吨公里油耗分布 bar chart
And add 驱动能耗 as a new data point. Drive energy = fuel used for actual driving (not idle). It's essentially 燃油消耗 - 怠速油耗, converted to appropriate units. Or it could be a separate calculated quantity representing the energy consumed during loaded+empty running.
Let me also add 驱动能耗 as a metric card (making it 5 cards total), or keep 4 cards and just add it to the chart.
I think 5 cards makes sense: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗.
Actually wait - let me re-read. The user says to add 驱动能耗 to the consumption trend chart. This could mean adding it as a new line in the line chart. And also possibly as a new metric card.
Let me keep it clean - add 驱动能耗 as a new metric card and add it as a line in the trend chart. Rearrange layout so bar chart is below line chart.
Let me design this now. I'll go with:
- 5 metric cards: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗
- Charts: line chart full width (4 lines: 燃油消耗, 怠速油耗, 电动能耗, 驱动能耗), then bar chart full width below (吨公里油耗分布)
- Table
Actually, thinking about it more - if both charts are full width, it might be too tall. Let me use a reasonable proportion.
Let me go with the design. I'll keep the white background enterprise style consistent.
角色
assistant
content
total_tokens
271572
summary
燃油/能耗管理仪表盘(改版),白底企业级设计,5项核心指标(燃油消耗/吨公里油耗/怠速油耗/驱动能耗/电动能耗),消耗趋势折线图新增驱动能耗曲线并下置吨公里油耗柱状分布图,矿区→车型→车辆三级筛选,横轴按日/月/年切换,CSV导出与打印。
uiParsing
关闭