报警数据统计仪表盘重构版:白底红强调,8项核心指标与四级联动筛选

By 18561366332, 27 April, 2026
推理
用户想要对报警数据统计界面进行以下修改:

1. **报警数量趋势**:去掉高频故障线,增加车辆分组(即按所选车辆分别展示折线)
2. **高频故障统计**:从列表改为柱状图
3. **去掉确认率与恢复率的图形化**(即去掉那两个迷你环形图),但保留数据指标
4. **筛选条件**:增加故障等级筛选

让我重新设计这个界面。需要保留的核心功能:
- 8项核心指标卡片
- 报警数量趋势图(按车辆分组多线)
- 矿区报警对比柱状图
- 高频故障改为柱状图
- 子系统饼图保留
- 确认率和恢复率保留在指标卡片中,但不需要图形化
- 筛选:矿区、车型、车辆、故障等级
- 横轴按日/月/年切换
- CSV导出与打印

让我重新设计布局:
- 上行全宽:报警数量趋势(按车辆分组多线)
- 下行三列:矿区报警对比柱状图 | 高频故障柱状图 | 子系统分布饼图
- 确认率和恢复率只在指标卡片中展示数字

关于"报警数量趋势增加车辆分组"——这意味着折线图上有多条线,每条线代表一个车辆(或按筛选条件聚合后的车辆)。如果筛选了具体车辆就显示该车辆,如果选了矿区/车型但没有选具体车辆,可能需要按车辆分组展示。考虑到可能有15辆车,全部展示会很多线,我可以按筛选后的车辆来展示,如果车辆数超过一定数量(比如8个),就只展示前几个。

实际上用户说"增加车辆分组",意思应该是在趋势图中按车辆维度展示不同颜色的折线。当用户筛选了矿区+车型后,如果没有选具体车辆,会展示该矿区+车型下的所有车辆。如果选了具体车辆,就只展示该车辆。

但这个可能会有很多线。让我限制最多展示的车辆数,或者用不同方式进行聚合。考虑到16:9的比例和上行全宽的布局,我最多可以展示约8条线。如果筛选后的车辆数超过8,我可以取前8个。

不对,让我重新理解:用户在筛选条件中选择了矿区、车型、车辆,这些是级联的。如果选了具体车辆,那趋势图就是该车的数据;如果只选了矿区+车型,那就是该矿区该车型所有车辆的聚合数据。用户说"增加车辆分组",可能指的是在趋势图中展示不同车辆的对比——类似于之前矿车运营数据中按分组展示多线。

我觉得用户想要的是:在报警数量趋势折线图中,如果用户在选择矿区+车型后没有指定具体车辆,则折线图按车辆拆分显示多条线(每个车辆一条)。这样用户可以看到不同车辆的报警趋势对比。

但15辆车全部展示太多了,我限制最多8条线。

另外用户说"去掉高频故障线",意思是从趋势图中移除高频故障那条线。

关于"高频故障统计换成柱状图"——现在高频故障是用一个列表展示故障类型和数量,用户想换成柱状图。这很好,柱状图更适合直观对比。

关于"去掉确认率与恢复率图形化"——意思是去掉那两个迷你环形图,确认率和恢复率只在指标卡片中保留数字,不占图表空间。这样右侧面板可以更简洁,只保留子系统分布饼图。

关于"筛选条件增加故障等级"——在筛选栏中增加一个故障等级的下拉选择(严重/重要/一般)。

让我重新设计布局:
- 上行全宽(约40%高度):报警数量趋势(按车辆分组多线,如果筛选了具体车辆则单线,否则按车辆分多线)
- 下行三列(约60%高度):
- 左:各矿区报警数量对比(堆叠柱状,严重/重要/一般)
- 中:高频故障统计(柱状图)
- 右:子系统分布饼图

确认率和恢复率仅作为指标卡片展示,不需要图形化。这样右侧面板就更简洁了。

让我开始构建完整的HTML。
角色
assistant
content

```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-red: #dc2626;\n --accent-orange: #e8590c;\n --accent-blue: #0e7bb5;\n --accent-green: #16a34a;\n --accent-teal: #0d9488;\n --accent-purple: #7c3aed;\n --accent-amber: #d4891a;\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 * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n background: var(--bg-page); color: var(--text-primary); font-family: var(--font-body);\n min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 8px;\n background-image: radial-gradient(ellipse at 15% 10%, rgba(220,38,38,0.03) 0%, transparent 55%), radial-gradient(ellipse at 85% 90%, rgba(14,123,181,0.02) 0%, transparent 55%);\n }\n .dashboard {\n width: 100%; max-width: 1600px; aspect-ratio: 16 / 9; max-height: calc(100vh - 16px);\n display: flex; flex-direction: column; gap: 4px; background: var(--bg-panel);\n border-radius: var(--radius-xl); padding: 9px 13px;\n box-shadow: var(--shadow-elevated); border: 1px solid var(--border); position: relative; overflow: hidden;\n }\n .dashboard::before {\n content: ''; position: absolute; inset: 0; pointer-events: none;\n background: linear-gradient(180deg, rgba(220,38,38,0.012) 0%, transparent 25%), linear-gradient(0deg, rgba(14,123,181,0.008) 0%, transparent 35%);\n border-radius: var(--radius-xl); 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, #dc2626, #b91c1c); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; font-size: 13px; color: #fff; font-weight: 700; flex-shrink: 0; box-shadow: 0 2px 8px rgba(220,38,38,0.22); }\n .header-title h1 { font-family: var(--font-display); font-size: clamp(12px,1vw,15px); 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: #dc2626; box-shadow: 0 0 8px rgba(220,38,38,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: #dc2626; box-shadow: 0 0 0 2px rgba(220,38,38,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, #dc2626, #b91c1c); 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(220,38,38,0.2); }\n .btn-primary:hover { filter: brightness(1.07); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(220,38,38,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(8,1fr); gap: 3px; flex-shrink: 0; }\n .metric-card { background: var(--bg-card); border-radius: var(--radius-md); padding: 4px 6px; 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: #dc2626; }\n .metric-card:nth-child(2) .card-accent { background: #e8590c; }\n .metric-card:nth-child(3) .card-accent { background: #0e7bb5; }\n .metric-card:nth-child(4) .card-accent { background: #16a34a; }\n .metric-card:nth-child(5) .card-accent { background: #0d9488; }\n .metric-card:nth-child(6) .card-accent { background: #7c3aed; }\n .metric-card:nth-child(7) .card-accent { background: #d4891a; }\n .metric-card:nth-child(8) .card-accent { background: #64748b; }\n .metric-card .card-icon { font-size: 6px; 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(9px,1vw,14px); 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-top { flex: 0 0 36%; min-height: 0; display: flex; }\n .chart-box-row-top { flex: 1; background: var(--bg-card); border-radius: var(--radius-md); border: 1px solid var(--border-light); padding: 6px 9px; display: flex; flex-direction: column; overflow: hidden; box-shadow: var(--shadow-sm); }\n .charts-area-bottom { flex: 1; min-height: 0; display: grid; grid-template-columns: 0.9fr 0.7fr 0.9fr; gap: 4px; }\n .chart-box { background: var(--bg-card); border-radius: var(--radius-md); border: 1px solid var(--border-light); padding: 6px 8px; 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: 2px; 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: 3px; font-size: 6px; color: var(--text-muted); flex-wrap: wrap; justify-content: flex-end; }\n .chart-legend span { display: flex; align-items: center; gap: 1px; white-space: nowrap; }\n .chart-legend .dot { width: 3px; height: 3px; border-radius: 50%; display: inline-block; flex-shrink: 0; }\n .chart-canvas-wrap { flex: 1; min-height: 0; position: relative; width: 100%; }\n .chart-canvas-wrap canvas { width: 100%; height: 100%; display: block; }\n\n .pie-legend-wrap { display: flex; gap: 3px; flex-wrap: wrap; font-size: 6px; font-family: var(--font-display); flex-shrink: 0; padding: 2px 0; }\n .pie-legend-item { display: flex; align-items: center; gap: 1px; }\n .pie-legend-dot { width: 4px; height: 4px; border-radius: 1px; flex-shrink: 0; }\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: 70px; box-shadow: var(--shadow-sm); }\n .table-section-header { display: flex; align-items: center; justify-content: space-between; padding: 2px 8px; 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(220,38,38,0.03); }\n tbody tr { transition: background var(--transition-base); }\n .cell-alert { color: #dc2626; font-weight: 700; }\n .cell-warn { color: #e8590c; font-weight: 600; }\n .cell-good { color: #16a34a; 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 #dc2626; 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: #dc2626; font-size: 11px; }\n\n @media(max-width:1400px) {\n .metrics-row { grid-template-columns: repeat(4,1fr); }\n .charts-area-bottom { grid-template-columns: 1fr 1fr; }\n .dashboard { aspect-ratio: auto; max-height: none; }\n }\n @media(max-width:900px) {\n .metrics-row { grid-template-columns: repeat(3,1fr); }\n .charts-area-bottom { grid-template-columns: 1fr; }\n .charts-area-top { flex: 0 0 180px; }\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: repeat(2,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-bell\"></i></div>\n <div class=\"header-title\"><h1>报警数据统计</h1><span>Alarm Statistics & 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>&nbsp;刷新</button>\n <button class=\"btn-action\" onclick=\"exportCSV()\" title=\"导出CSV\"><i class=\"fa-solid fa-file-csv\"></i></button>\n <button class=\"btn-action\" onclick=\"printDashboard()\" title=\"打印\"><i class=\"fa-solid fa-print\"></i></button>\n </div>\n </div>\n\n <div class=\"filter-bar\">\n <span class=\"filter-label\"><i class=\"fa-solid fa-filter\"></i>&nbsp;筛选</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 class=\"sep\"></span>\n <span class=\"filter-label\">故障等级</span>\n <select id=\"filterLevel\"><option value=\"\">全部等级</option><option value=\"严重\">严重</option><option value=\"重要\">重要</option><option value=\"一般\">一般</option></select>\n <span id=\"filterSummary\" style=\"font-size:6px;color:var(--text-muted);margin-left:3px;\"></span>\n </div>\n\n <div class=\"metrics-row\" id=\"metricsRow\"></div>\n\n <div class=\"charts-area-top\">\n <div class=\"chart-box-row-top\" id=\"lineChartBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-line\"></i>&nbsp;报警数量趋势(按车辆分组)</span>\n <div class=\"chart-legend\" id=\"lineLegend\"></div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"lineChart\"></canvas></div>\n </div>\n </div>\n\n <div class=\"charts-area-bottom\">\n <div class=\"chart-box\" id=\"barMineBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-chart-bar\"></i>&nbsp;各矿区报警数量对比</span>\n <div class=\"chart-legend\">\n <span><span class=\"dot\" style=\"background:#dc2626\"></span>严重</span>\n <span><span class=\"dot\" style=\"background:#e8590c\"></span>重要</span>\n <span><span class=\"dot\" style=\"background:#d4891a\"></span>一般</span>\n </div>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"barMineChart\"></canvas></div>\n </div>\n\n <div class=\"chart-box\" id=\"barFaultBox\">\n <div class=\"chart-box-header\">\n <span class=\"chart-box-title\"><i class=\"fa-solid fa-fire\"></i>&nbsp;高频故障统计</span>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"barFaultChart\"></canvas></div>\n </div>\n\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>&nbsp;子系统报警分布</span>\n </div>\n <div class=\"chart-canvas-wrap\"><canvas id=\"pieChart\"></canvas></div>\n <div class=\"pie-legend-wrap\" id=\"pieLegend\"></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>&nbsp;报警数据明细</span>\n <span style=\"font-size:6px;color:var(--text-muted)\" id=\"tableInfo\">共 0 条记录</span>\n </div>\n <div class=\"table-wrap\"><table><thead><tr><th>周期</th><th>报警总数</th><th>高频故障</th><th>确认数</th><th>恢复数</th><th>确认率</th><th>恢复率</th><th>MTBF(h)</th><th>MTTR(h)</th></tr></thead><tbody id=\"tableBody\"></tbody></table></div>\n </div>\n</div>\n\n<div class=\"toast-container\" id=\"toastContainer\"></div>\n\n<script>\n(function() {\n 'use strict';\n\n const VEHICLES = [\n { id: 'A-E-001', mine: '矿区A', type: '电动矿卡', capacity: 60 },\n { id: 'A-E-002', mine: '矿区A', type: '电动矿卡', capacity: 60 },\n { id: 'A-E-003', mine: '矿区A', type: '电动矿卡', capacity: 55 },\n { id: 'A-F-001', mine: '矿区A', type: '燃油矿卡', capacity: 70 },\n { id: 'A-F-002', mine: '矿区A', type: '燃油矿卡', capacity: 70 },\n { id: 'B-E-001', mine: '矿区B', type: '电动矿卡', capacity: 60 },\n { id: 'B-E-002', mine: '矿区B', type: '电动矿卡', capacity: 55 },\n { id: 'B-H-001', mine: '矿区B', type: '混合动力矿卡', capacity: 65 },\n { id: 'B-H-002', mine: '矿区B', type: '混合动力矿卡', capacity: 65 },\n { id: 'B-H-003', mine: '矿区B', type: '混合动力矿卡', capacity: 60 },\n { id: 'C-F-001', mine: '矿区C', type: '燃油矿卡', capacity: 70 },\n { id: 'C-F-002', mine: '矿区C', type: '燃油矿卡', capacity: 75 },\n { id: 'C-F-003', mine: '矿区C', type: '燃油矿卡', capacity: 70 },\n { id: 'C-H-001', mine: '矿区C', type: '混合动力矿卡', capacity: 65 },\n { id: 'C-H-002', mine: '矿区C', type: '混合动力矿卡', capacity: 60 },\n ];\n\n const MINES = [...new Set(VEHICLES.map(v => v.mine))];\n const TYPES = [...new Set(VEHICLES.map(v => v.type))];\n const FAULT_TYPES = ['发动机过热', '液压系统压力异常', '传动轴异响', '制动片磨损', '电气短路', '冷却液泄漏', '轮胎气压不足', '传感器信号丢失', '燃油系统堵塞', '电池电压异常'];\n const SUBSYSTEMS = ['发动机系统', '液压系统', '传动系统', '制动系统', '电气系统', '冷却系统', '轮胎系统'];\n const SUBSYS_COLORS = ['#dc2626', '#e8590c', '#d4891a', '#0e7bb5', '#7c3aed', '#0d9488', '#16a34a'];\n const FAULT_COLORS = ['#dc2626', '#e8590c', '#d4891a', '#0e7bb5', '#7c3aed', '#0d9488', '#16a34a', '#db2777', '#6366f1', '#0891b2'];\n const LEVELS = ['严重', '重要', '一般'];\n const LEVEL_COLORS = ['#dc2626', '#e8590c', '#d4891a'];\n const GROUP_COLORS = ['#dc2626', '#0e7bb5', '#0d9488', '#e8590c', '#7c3aed', '#16a34a', '#d4891a', '#db2777', '#6366f1', '#0891b2', '#ca8a04', '#9333ea', '#e11d48', '#0f766e', '#2563eb'];\n\n const state = {\n granularity: 'day', startDate: '2026-04-01', endDate: '2026-04-27',\n filterMine: '', filterType: '', filterVehicle: '', filterLevel: '',\n };\n\n const $ = s => document.querySelector(s);\n const dom = {\n granularityGroup: $('#granularityGroup'), startDate: $('#startDate'), endDate: $('#endDate'),\n filterMine: $('#filterMine'), filterType: $('#filterType'), filterVehicle: $('#filterVehicle'), filterLevel: $('#filterLevel'),\n filterSummary: $('#filterSummary'),\n metricsRow: $('#metricsRow'),\n lineChart: $('#lineChart'), lineLegend: $('#lineLegend'),\n barMineChart: $('#barMineChart'), barFaultChart: $('#barFaultChart'),\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 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 rng = seedRandom(d.getTime() % 99997 + i * 211);\n const bm = state.granularity === 'year' ? 350 : state.granularity === 'month' ? 30 : 1;\n const vCount = vehicles.length;\n let totalAlarms = 0, highFreqFaults = 0, acknowledged = 0, recovered = 0;\n const vehicleRecords = {};\n const faultTotals = {};\n FAULT_TYPES.forEach(ft => { faultTotals[ft] = 0; });\n const subsysTotals = {};\n SUBSYSTEMS.forEach(s => { subsysTotals[s] = 0; });\n const mineTotals = {};\n MINES.forEach(m => { mineTotals[m] = 0; });\n const levelTotals = {};\n LEVELS.forEach(l => { levelTotals[l] = 0; });\n\n vehicles.forEach(v => {\n const vrng = seedRandom(d.getTime() % 99997 + v.id.charCodeAt(0) * 73 + i * 37 + v.capacity * 7);\n const vAlarms = Math.round(bm * (1.2 + vrng() * 3.0));\n const vHighFreq = Math.round(vAlarms * (0.08 + vrng() * 0.15));\n const vAck = Math.round(vAlarms * (0.65 + vrng() * 0.3));\n const vRec = Math.round(vAck * (0.6 + vrng() * 0.35));\n vehicleRecords[v.id] = { totalAlarms: vAlarms, highFreqFaults: vHighFreq, acknowledged: vAck, recovered: vRec };\n totalAlarms += vAlarms;\n highFreqFaults += vHighFreq;\n acknowledged += vAck;\n recovered += vRec;\n\n let remaining = vHighFreq;\n FAULT_TYPES.forEach((ft, fi) => {\n const shares = [0.20, 0.15, 0.12, 0.10, 0.09, 0.07, 0.07, 0.06, 0.06, 0.06];\n const val = Math.round(vHighFreq * (shares[fi] + (vrng() - 0.5) * 0.04));\n const add = Math.min(val, remaining);\n faultTotals[ft] += add;\n remaining = Math.max(0, remaining - add);\n });\n if (remaining > 0) faultTotals[FAULT_TYPES[0]] += remaining;\n\n SUBSYSTEMS.forEach((s, si) => {\n const shares = [0.22, 0.17, 0.15, 0.13, 0.12, 0.10, 0.11];\n subsysTotals[s] += Math.round(vAlarms * (shares[si] + (vrng() - 0.5) * 0.06));\n });\n\n if (mineTotals[v.mine] !== undefined) {\n mineTotals[v.mine] += vAlarms;\n }\n\n LEVELS.forEach((l, li) => {\n const shares = [0.18, 0.32, 0.50];\n levelTotals[l] += Math.round(vAlarms * (shares[li] + (vrng() - 0.5) * 0.05));\n });\n });\n\n // Apply level filter\n let filterMult = 1;\n if (state.filterLevel) {\n const li = LEVELS.indexOf(state.filterLevel);\n const shares = [0.18, 0.32, 0.50];\n const origShare = shares[li];\n filterMult = origShare > 0 ? (levelTotals[state.filterLevel] / Math.max(totalAlarms * origShare, 1)) : 1;\n const lv = levelTotals[state.filterLevel] || 0;\n totalAlarms = Math.round(lv);\n highFreqFaults = Math.round(highFreqFaults * filterMult);\n acknowledged = Math.round(acknowledged * filterMult);\n recovered = Math.round(recovered * filterMult);\n }\n\n const ackRate = totalAlarms > 0 ? Math.round(acknowledged / totalAlarms * 1000) / 10 : 0;\n const recRate = totalAlarms > 0 ? Math.round(recovered / totalAlarms * 1000) / 10 : 0;\n const totalRunHours = bm * vCount * (state.granularity === 'year' ? 480 : state.granularity === 'month' ? 40 : 1.3);\n const faultsForMT = Math.max(highFreqFaults, 1);\n const mtbf = Math.round(totalRunHours / faultsForMT * 10) / 10;\n const mttr = Math.round((0.3 + rng() * 3.5) * 10) / 10;\n\n return { label: d, totalAlarms, highFreqFaults, acknowledged, recovered, ackRate, recRate, mtbf, mttr, faultTotals, subsysTotals, mineTotals, levelTotals, vehicleRecords };\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: 8 }, () => `<div class=\"metric-card\"><div class=\"card-accent\"></div><div class=\"card-label\">--</div><div class=\"card-value\">0</div></div>`).join('');\n return;\n }\n const sm = records.reduce((a, r) => {\n a.totalAlarms += r.totalAlarms; a.highFreqFaults += r.highFreqFaults;\n a.acknowledged += r.acknowledged; a.recovered += r.recovered;\n a.mtbfSum += r.mtbf; a.mttrSum += r.mttr;\n return a;\n }, { totalAlarms: 0, highFreqFaults: 0, acknowledged: 0, recovered: 0, mtbfSum: 0, mttrSum: 0 });\n const n = records.length;\n const avgAckRate = sm.totalAlarms > 0 ? Math.round(sm.acknowledged / sm.totalAlarms * 1000) / 10 : 0;\n const avgRecRate = sm.totalAlarms > 0 ? Math.round(sm.recovered / sm.totalAlarms * 1000) / 10 : 0;\n const avgMtbf = n > 0 ? Math.round(sm.mtbfSum / n * 10) / 10 : 0;\n const avgMttr = n > 0 ? Math.round(sm.mttrSum / n * 10) / 10 : 0;\n const numVehicles = getFilteredVehicles().length;\n\n const cfg = [\n { icon: 'fa-fire', label: '高频故障', value: sm.highFreqFaults.toString(), unit: '次', color: '#dc2626' },\n { icon: 'fa-bell', label: '报警总数', value: sm.totalAlarms.toString(), unit: '次', color: '#e8590c' },\n { icon: 'fa-check-circle', label: '确认率', value: avgAckRate.toFixed(1), unit: '%', color: '#0e7bb5' },\n { icon: 'fa-rotate-back', label: '恢复率', value: avgRecRate.toFixed(1), unit: '%', color: '#16a34a' },\n { icon: 'fa-clock', label: 'MTBF', value: avgMtbf.toFixed(1), unit: 'h', color: '#0d9488' },\n { icon: 'fa-wrench', label: 'MTTR', value: avgMttr.toFixed(1), unit: 'h', color: '#7c3aed' },\n { icon: 'fa-microchip', label: '子系统', value: SUBSYSTEMS.length.toString(), unit: '个', color: '#d4891a' },\n { icon: 'fa-truck', label: '车辆数', value: numVehicles.toString(), unit: '辆', color: '#64748b' },\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: 34 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n const vehicles = getFilteredVehicles();\n const maxVehicles = Math.min(vehicles.length, 10);\n const displayVehicles = vehicles.slice(0, maxVehicles);\n\n dom.lineLegend.innerHTML = displayVehicles.map((v, i) => `<span><span class=\"dot\" style=\"background:${GROUP_COLORS[i % GROUP_COLORS.length]}\"></span>${v.id}</span>`).join('');\n\n let mx = 0;\n records.forEach(r => {\n if (r.vehicleRecords) {\n displayVehicles.forEach(v => {\n const vr = r.vehicleRecords[v.id];\n if (vr && vr.totalAlarms > mx) mx = vr.totalAlarms;\n });\n }\n });\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(Math.round(mx - (mx / gl) * i), 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 displayVehicles.forEach((v, vi) => {\n const color = GROUP_COLORS[vi % GROUP_COLORS.length];\n ctx.strokeStyle = color; ctx.lineWidth = 1.6; ctx.lineJoin = 'round'; ctx.lineCap = 'round';\n ctx.beginPath();\n let hasData = false;\n records.forEach((r, ii) => {\n const vr = r.vehicleRecords ? r.vehicleRecords[v.id] : null;\n const val = vr ? vr.totalAlarms : 0;\n const x = pad.left + xs * ii, y = pad.top + ch - (val / mx) * ch;\n if (ii === 0) { ctx.moveTo(x, y); hasData = true; }\n else ctx.lineTo(x, y);\n });\n if (hasData) ctx.stroke();\n });\n }\n\n function renderMineBarChart(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 mineAgg = {};\n MINES.forEach(m => { mineAgg[m] = {}; LEVELS.forEach(l => { mineAgg[m][l] = 0; }); });\n records.forEach(r => {\n const alarmTotal = r.totalAlarms || 1;\n Object.entries(r.mineTotals || {}).forEach(([mine, count]) => {\n Object.entries(r.levelTotals || {}).forEach(([level, lcount]) => {\n if (mineAgg[mine] && mineAgg[mine][level] !== undefined) {\n mineAgg[mine][level] += Math.round(count * (lcount / alarmTotal));\n }\n });\n });\n });\n\n let mx = 0;\n MINES.forEach(m => { const s = LEVELS.reduce((a, l) => a + (mineAgg[m][l] || 0), 0); if (s > mx) mx = s; });\n mx = Math.ceil(mx * 1.2) || 10;\n\n const pad = { top: 6, right: 8, bottom: 22, left: 34 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n const gl = 4;\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(Math.round(mx - (mx / gl) * i), pad.left - 3, y + 3);\n }\n\n const barGap = 2;\n const barW = (cw / MINES.length) - barGap;\n if (barW <= 0) return;\n\n MINES.forEach((m, mi) => {\n const x = pad.left + (cw / MINES.length) * mi + barGap / 2;\n let sy = pad.top + ch;\n LEVELS.forEach((l, li) => {\n const val = mineAgg[m][l] || 0;\n const bh = (val / mx) * ch;\n const y = sy - bh;\n ctx.fillStyle = LEVEL_COLORS[li];\n ctx.fillRect(x, y, barW, bh);\n if (li > 0 && bh > 0.8) { ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillRect(x, y, barW, 0.3); }\n sy = y;\n });\n ctx.fillStyle = '#475569'; ctx.font = '6px DM Sans, sans-serif'; ctx.textAlign = 'center';\n ctx.fillText(m, x + barW / 2, pad.top + ch + 11);\n const total = LEVELS.reduce((a, l) => a + (mineAgg[m][l] || 0), 0);\n ctx.fillStyle = '#0f172a'; ctx.font = 'bold 6px DM Sans, sans-serif';\n ctx.fillText(total, x + barW / 2, pad.top + ch - (total / mx) * ch - 3);\n });\n }\n\n function renderFaultBarChart(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 totals = {};\n FAULT_TYPES.forEach(ft => { totals[ft] = 0; });\n records.forEach(r => {\n Object.entries(r.faultTotals || {}).forEach(([ft, c]) => { if (totals[ft] !== undefined) totals[ft] += c; });\n });\n const entries = Object.entries(totals).sort((a, b) => b[1] - a[1]);\n const maxVal = entries[0]?.[1] || 1;\n\n const pad = { top: 6, right: 6, bottom: 30, left: 32 };\n const cw = w - pad.left - pad.right, ch = h - pad.top - pad.bottom;\n\n const gl = 4;\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(Math.round(maxVal - (maxVal / gl) * i), pad.left - 3, y + 3);\n }\n\n const barGap = 2;\n const barW = (cw / entries.length) - barGap;\n if (barW <= 0) return;\n\n entries.forEach(([name, count], ei) => {\n const x = pad.left + (cw / entries.length) * ei + barGap / 2;\n const bh = (count / maxVal) * ch;\n const y = pad.top + ch - bh;\n ctx.fillStyle = FAULT_COLORS[ei % FAULT_COLORS.length];\n ctx.fillRect(x, y, barW, bh);\n ctx.fillStyle = '#475569'; ctx.font = '5px DM Sans, sans-serif'; ctx.textAlign = 'center';\n const shortName = name.length > 5 ? name.slice(0, 3) + '…' : name;\n ctx.fillText(shortName, x + barW / 2, pad.top + ch + 9);\n ctx.fillStyle = '#0f172a'; ctx.font = 'bold 5px DM Sans, sans-serif';\n ctx.fillText(count, x + barW / 2, y - 2);\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 totals = {};\n SUBSYSTEMS.forEach(s => { totals[s] = 0; });\n records.forEach(r => { Object.entries(r.subsysTotals || {}).forEach(([s, c]) => { if (totals[s] !== undefined) totals[s] += c; }); });\n const entries = Object.entries(totals).filter(([, v]) => v > 0);\n const grandTotal = entries.reduce((a, [, v]) => a + v, 0);\n if (grandTotal === 0) return;\n\n const cx = w / 2, cy = h / 2;\n const outerR = Math.min(cx, cy) - 4;\n let startAngle = -Math.PI / 2;\n const legendParts = [];\n\n entries.forEach(([name, val], i) => {\n const sliceAngle = (val / grandTotal) * Math.PI * 2;\n if (sliceAngle < 0.01) return;\n ctx.beginPath();\n ctx.moveTo(cx, cy);\n ctx.arc(cx, cy, outerR, startAngle, startAngle + sliceAngle);\n ctx.closePath();\n ctx.fillStyle = SUBSYS_COLORS[i % SUBSYS_COLORS.length];\n ctx.fill();\n ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 0.8;\n ctx.stroke();\n const pct = (val / grandTotal * 100).toFixed(1);\n legendParts.push(`<span class=\"pie-legend-item\"><span class=\"pie-legend-dot\" style=\"background:${SUBSYS_COLORS[i % SUBSYS_COLORS.length]}\"></span>${name} ${pct}%</span>`);\n startAngle += sliceAngle;\n });\n dom.pieLegend.innerHTML = legendParts.join('');\n }\n\n function renderTable(records) {\n dom.tableInfo.textContent = `共 ${records.length} 条记录`;\n if (!records.length) { dom.tableBody.innerHTML = '<tr><td colspan=\"9\" style=\"text-align:center;color:var(--text-muted);padding:6px;\">暂无数据</td></tr>'; return; }\n const disp = records.slice(-18);\n dom.tableBody.innerHTML = disp.map(r => `\n <tr>\n <td>${fmtFull(r.label)}</td>\n <td class=\"cell-alert\">${r.totalAlarms}</td>\n <td class=\"cell-warn\">${r.highFreqFaults}</td>\n <td class=\"cell-info\">${r.acknowledged}</td>\n <td class=\"cell-good\">${r.recovered}</td>\n <td>${r.ackRate.toFixed(1)}%</td>\n <td>${r.recRate.toFixed(1)}%</td>\n <td>${r.mtbf.toFixed(1)}</td>\n <td>${r.mttr.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 renderMineBarChart(dom.barMineChart, currentRecords);\n renderFaultBarChart(dom.barFaultChart, currentRecords);\n renderPieChart(dom.pieChart, 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');\n 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.filterLevel.addEventListener('change', () => { state.filterLevel = dom.filterLevel.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; state.filterLevel = dom.filterLevel.value;\n updateDashboard();\n };\n\n window.exportCSV = function() {\n if (!currentRecords.length) { showToast('无数据可导出', 'fa-triangle-exclamation'); return; }\n let csv = '周期,报警总数,高频故障,确认数,恢复数,确认率(%),恢复率(%),MTBF(h),MTTR(h)\\n';\n currentRecords.forEach(r => {\n csv += [fmtFull(r.label), r.totalAlarms, r.highFreqFaults, r.acknowledged, r.recovered, r.ackRate.toFixed(1), r.recRate.toFixed(1), r.mtbf.toFixed(1), r.mttr.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 renderMineBarChart(dom.barMineChart, currentRecords);\n renderFaultBarChart(dom.barFaultChart, currentRecords);\n renderPieChart(dom.pieChart, 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报警报告", "设置MTBF/MTTR预警", "添加实时报警推送", "对比不同时段趋势"],
"breakpoint": "none"
}
```

total_tokens
259124
summary
报警数据统计仪表盘(重构版),白底红色强调设计,8项核心指标,上行全宽报警趋势折线图(按车辆分组多线),下行三列(矿区报警对比堆叠柱状/高频故障柱状/子系统饼图),确认恢复率仅指标展示,矿区→车型→车辆→故障等级四级联动筛选,年/月/日横轴切换,CSV导出与打印。
uiParsing
关闭