```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 48%; 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 .charts-row-bottom { flex: 1; display: grid; grid-template-columns: 0.9fr 1.1fr; gap: 4px; min-height: 0; }\n .chart-box { 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 .pie-body { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 0; }\n .pie-canvas-wrap { flex: 1; min-width: 0; height: 100%; position: relative; }\n .pie-canvas-wrap canvas { width: 100%; height: 100%; display: block; }\n .pie-legend-vert { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; font-size: 7px; font-family: var(--font-display); }\n .pie-legend-item { display: flex; align-items: center; gap: 4px; }\n .pie-legend-dot { width: 7px; height: 7px; border-radius: 2px; flex-shrink: 0; }\n .pie-legend-val { font-weight: 700; font-size: 10px; color: var(--text-primary); line-height: 1; }\n .pie-legend-label { color: var(--text-muted); font-size: 6px; }\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: 78px; 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 .charts-row-bottom { grid-template-columns: 1fr 1fr; }\n .dashboard { aspect-ratio: auto; max-height: none; }\n .chart-box-top { flex: 0 0 190px; }\n .chart-box { min-height: 180px; }\n }\n @media (max-width: 900px) {\n .metrics-row { grid-template-columns: repeat(2, 1fr); }\n .charts-row-bottom { grid-template-columns: 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 .pie-body { flex-direction: column; }\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\n <div class=\"charts-row-bottom\">\n <div class=\"chart-box\" id=\"pieBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-pie\"></i> 油耗分布</span>\n </div>\n <div class=\"pie-body\">\n <div class=\"pie-canvas-wrap\"><canvas id=\"pieChart\"></canvas></div>\n <div class=\"pie-legend-vert\" id=\"pieLegend\"></div>\n </div>\n </div>\n <div class=\"chart-box\" id=\"barChartBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-bar\"></i> 吨公里油耗分布</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 </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 pieChart: $('#pieChart'), pieLegend: $('#pieLegend'),\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 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 renderPieChart(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 sm = records.reduce((a, r) => { a.idleFuel += r.idleFuel; a.driveFuel += r.driveFuel; return a; }, { idleFuel: 0, driveFuel: 0 });\n const total = sm.idleFuel + sm.driveFuel;\n\n const segments = [\n { label: '驱动油耗', value: sm.driveFuel, pct: total > 0 ? (sm.driveFuel / total * 100) : 0, color: '#7c3aed' },\n { label: '怠速油耗', value: sm.idleFuel, pct: total > 0 ? (sm.idleFuel / total * 100) : 0, color: '#e8590c' },\n ];\n\n dom.pieLegend.innerHTML = segments.map(s => `\n <div class=\"pie-legend-item\">\n <div class=\"pie-legend-dot\" style=\"background:${s.color}\"></div>\n <div>\n <div class=\"pie-legend-val\">${s.pct.toFixed(1)}%</div>\n <div class=\"pie-legend-label\">${s.label} ${s.value.toFixed(1)}L</div>\n </div>\n </div>\n `).join('');\n\n const cx = w / 2, cy = h / 2;\n const outerR = Math.min(cx, cy) - 6;\n const innerR = outerR * 0.48;\n\n if (total === 0) {\n ctx.fillStyle = '#94a3b8'; ctx.font = '9px DM Sans, sans-serif'; 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; ctx.fill();\n ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1;\n ctx.stroke();\n startAngle += sliceAngle;\n });\n\n ctx.fillStyle = '#0f172a';\n ctx.font = 'bold 11px DM Sans, sans-serif'; ctx.textAlign = 'center';\n ctx.fillText(total.toFixed(1) + 'L', cx, cy - 2);\n ctx.fillStyle = '#94a3b8';\n ctx.font = '7px DM Sans, sans-serif'; ctx.textAlign = 'center';\n ctx.fillText('总燃油消耗', cx, cy + 11);\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 renderPieChart(dom.pieChart, 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) {\n renderLineChart(dom.lineChart, currentRecords);\n renderPieChart(dom.pieChart, currentRecords);\n renderBarChart(dom.barChart, currentRecords);\n }\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 me to modify the fuel/energy management dashboard. Let me analyze the current layout and the requested changes:
Current layout (from the latest version):
- Top: Header + Filter bar
- Metrics row (5 cards)
- Charts area:
- Top (52%): Line chart - 消耗趋势 (fuel consumption, idle fuel, electric energy, drive energy)
- Bottom (remaining): Bar chart - 吨公里油耗分布
- Table at bottom
Requested changes:
1. Add a pie chart showing "总燃油消耗与怠速油耗对比" (total fuel consumption vs idle fuel comparison)
2. Place this pie chart to the LEFT of the "吨公里油耗分布" bar chart
3. So the bottom area should become a two-column layout: pie chart on the left, bar chart on the right
Let me redesign the charts area:
- Top row: 消耗趋势 line chart (spans full width)
- Bottom row: Two columns - left: 油耗分布饼图 (fuel distribution pie), right: 吨公里油耗分布 (ton-km fuel bar chart)
I need to modify the CSS and HTML structure. The `.charts-area` currently has:
- `.chart-box-top` (52%)
- `.chart-box-bottom` (remaining)
I'll change the bottom to be a grid with two columns.
Let me also update the metrics to reflect the "驱动能耗" was already added in the last version. The 5 metrics are: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗.
Now let me build the complete updated HTML.
Current layout (from the latest version):
- Top: Header + Filter bar
- Metrics row (5 cards)
- Charts area:
- Top (52%): Line chart - 消耗趋势 (fuel consumption, idle fuel, electric energy, drive energy)
- Bottom (remaining): Bar chart - 吨公里油耗分布
- Table at bottom
Requested changes:
1. Add a pie chart showing "总燃油消耗与怠速油耗对比" (total fuel consumption vs idle fuel comparison)
2. Place this pie chart to the LEFT of the "吨公里油耗分布" bar chart
3. So the bottom area should become a two-column layout: pie chart on the left, bar chart on the right
Let me redesign the charts area:
- Top row: 消耗趋势 line chart (spans full width)
- Bottom row: Two columns - left: 油耗分布饼图 (fuel distribution pie), right: 吨公里油耗分布 (ton-km fuel bar chart)
I need to modify the CSS and HTML structure. The `.charts-area` currently has:
- `.chart-box-top` (52%)
- `.chart-box-bottom` (remaining)
I'll change the bottom to be a grid with two columns.
Let me also update the metrics to reflect the "驱动能耗" was already added in the last version. The 5 metrics are: 燃油消耗, 吨公里油耗, 怠速油耗, 驱动能耗, 电动设备能耗.
Now let me build the complete updated HTML.
角色
assistant
content
total_tokens
285128
summary
燃油/能耗管理仪表盘(饼图+柱状图版),白底企业级设计,5项核心指标,消耗趋势折线图(燃油/怠速/电动/驱动四线),下行双列(左:总燃油消耗与怠速油耗对比饼图,右:吨公里油耗柱状分布),矿区→车型→车辆三级筛选,年/月/日横轴切换,CSV导出与打印。
uiParsing
关闭