feat: 根据监测类型动态显示主指标名称和等级

- 室外/室内: 下拉框显示空气质量指数, 等级标签空气质量级别
- 水质监测: 下拉框显示水质等级, 等级标签水质等级, 等级名称(达标/未达标)
- 花粉监测: 下拉框显示过敏等级, 等级标签过敏等级, 等级名称(很低/低/中等/高/很高)
- 水质/花粉非24h: 柱状堆积图使用对应等级配置(颜色+名称)
- 删除硬编码aqiLevels/getAqiColor,统一使用config-based配置
This commit is contained in:
liuyunqin
2026-06-04 14:48:23 +08:00
parent 7931a0d863
commit 05482bca61
@@ -241,24 +241,6 @@ const ALL_DATA = {
}
};
/* AQI 等级配置 */
var aqiLevels = [
{name:'清洁', color:'#13C2C2', range:[0,20]},
{name:'未污染', color:'#52C41A', range:[21,50]},
{name:'轻污染', color:'#FAAD14', range:[51,100]},
{name:'中污染', color:'#FA541C', range:[101,150]},
{name:'重污染', color:'#FF4D4F', range:[151,200]}
];
/* 获取 AQI 颜色 */
function getAqiColor(val) {
if (val >= 151) return '#FF4D4F';
if (val >= 101) return '#FA541C';
if (val >= 51) return '#FAAD14';
if (val >= 21) return '#52C41A';
return '#13C2C2';
}
/* 根据数值映射到 SVG Y 坐标 */
function valToY(val, yMax) {
return chartBottom - (val / yMax) * chartHeight;
@@ -283,6 +265,8 @@ function genTrend(metricName, period) {
{yMin:101,yMax:150,label:'中污染',color:'#FA541C',opacity:.1},
{yMin:151,yMax:200,label:'重污染',color:'#FF4D4F',opacity:.1}
],
levelColors: ['#13C2C2','#52C41A','#FAAD14','#FA541C','#FF4D4F'],
levelNames: ['清洁','未污染','轻污染','中污染','重污染'],
'24h': {points:[55,42,35,48,65,78,82,75,62,58,52,48]},
'7d': {barData:[
{clean:6,good:8,light:6,moderate:4,heavy:0},
@@ -294,6 +278,41 @@ function genTrend(metricName, period) {
{clean:6,good:9,light:5,moderate:4,heavy:0}
]},
'30d': {barData:(function(){var r=[];for(var i=0;i<30;i++){r.push({clean:Math.floor(Math.random()*8)+2,good:Math.floor(Math.random()*8)+2,light:Math.floor(Math.random()*6)+2,moderate:Math.floor(Math.random()*6)+2,heavy:Math.floor(Math.random()*3)})}return r})()}
},
'水质等级': {
isGrade: true, yMax: 100, yTicks: [100,75,50,25,0],
limitUpper: null, limitLower: null,
levelBands: [
{yMin:0,yMax:20,label:'达标',color:'#52C41A',opacity:.1},
{yMin:21,yMax:100,label:'未达标',color:'#FF4D4F',opacity:.1}
],
levelColors: ['#52C41A','#FF4D4F'],
levelNames: ['达标','未达标'],
'24h': {points:[25,30,22,35,42,28,38,45,32,20,35,28]},
'7d': {barData:[
{pass:18,fail:6},{pass:20,fail:4},{pass:16,fail:8},
{pass:22,fail:2},{pass:19,fail:5},{pass:17,fail:7},{pass:20,fail:4}
]},
'30d': {barData:(function(){var r=[];for(var i=0;i<30;i++){r.push({pass:Math.floor(Math.random()*14)+8,fail:Math.floor(Math.random()*8)+2})}return r})()}
},
'过敏等级': {
isGrade: true, yMax: 200, yTicks: [200,150,100,50,0],
limitUpper: null, limitLower: null,
levelBands: [
{yMin:0,yMax:40,label:'很低',color:'#52C41A',opacity:.1},
{yMin:40,yMax:80,label:'低',color:'#73D13D',opacity:.1},
{yMin:80,yMax:120,label:'中等',color:'#FAAD14',opacity:.1},
{yMin:120,yMax:160,label:'高',color:'#FF4D4F',opacity:.1},
{yMin:160,yMax:200,label:'很高',color:'#722ED1',opacity:.1}
],
levelColors: ['#52C41A','#73D13D','#FAAD14','#FF4D4F','#722ED1'],
levelNames: ['很低','低','中等','高','很高'],
'24h': {points:[45,38,55,72,85,120,145,135,98,75,62,50]},
'7d': {barData:[
{vl:6,lo:8,md:6,hi:4,vh:0},{vl:8,lo:7,md:5,hi:4,vh:0},{vl:4,lo:6,md:8,hi:5,vh:1},
{vl:2,lo:5,md:7,hi:8,vh:2},{vl:7,lo:8,md:5,hi:4,vh:0},{vl:9,lo:7,md:4,hi:4,vh:0},{vl:6,lo:9,md:5,hi:4,vh:0}
]},
'30d': {barData:(function(){var r=[];for(var i=0;i<30;i++){r.push({vl:Math.floor(Math.random()*8)+2,lo:Math.floor(Math.random()*8)+2,md:Math.floor(Math.random()*6)+2,hi:Math.floor(Math.random()*6)+2,vh:Math.floor(Math.random()*3)})}return r})()}
}
};
@@ -314,7 +333,7 @@ function genTrend(metricName, period) {
'TDS':{range:[100,850],unit:'mg/L'}
};
if (configs[metricName]) return { metricName: metricName, cfg: configs[metricName], period: period, unit: '' };
if (configs[metricName]) return { metricName: metricName, cfg: configs[metricName], period: period, unit: '', gradeInfo: configs[metricName] };
var r = ranges[metricName];
if (!r) r = { range: [10, 100], unit: '' };
@@ -384,8 +403,8 @@ function calcXPositions(count) {
return positions;
}
/* 渲染 AQI 柱状堆积图 */
function renderAQIBarChart(timeData, labels) {
/* 渲染等级柱状堆积图 */
function renderGradeBarChart(cfg, timeData, labels) {
var barData = timeData.barData;
var barCount = barData.length;
var barGap = Math.max(2, Math.floor(chartWidth / (barCount * 6)));
@@ -413,18 +432,27 @@ function renderAQIBarChart(timeData, labels) {
}
}
// 根据 levelNames 动态构建分段
var levelNames = cfg.levelNames;
// barData 的 key 映射:空气质量指数→clean/good/light/moderate/heavy, 过敏等级→vl/lo/md/hi/vh, 水质等级→pass/fail
var levelKeys;
if (levelNames.length === 5 && levelNames[0] === '很低') {
levelKeys = [{k:'vh'},{k:'hi'},{k:'md'},{k:'lo'},{k:'vl'}];
} else if (levelNames.length === 2) {
levelKeys = [{k:'fail'},{k:'pass'}];
} else {
levelKeys = [{k:'heavy'},{k:'moderate'},{k:'light'},{k:'good'},{k:'clean'}];
}
// 柱状堆积
var barsHtml = '';
for (var i = 0; i < barCount; i++) {
var bd = barData[i];
var x = xPositions[i] - Math.floor(barWidth / 2);
var segments = [
{val: bd.heavy, color:'#FF4D4F', name:'重污染'},
{val: bd.moderate, color:'#FA541C', name:'中污染'},
{val: bd.light, color:'#FAAD14', name:'轻污染'},
{val: bd.good, color:'#52C41A', name:'未污染'},
{val: bd.clean, color:'#13C2C2', name:'清洁'}
];
var segments = [];
for (var li = 0; li < levelNames.length; li++) {
segments.push({ val: bd[levelKeys[li].k], color: cfg.levelColors[li], name: levelNames[li] });
}
var yBase = chartBottom;
for (var j = 0; j < segments.length; j++) {
var seg = segments[j];
@@ -443,10 +471,9 @@ function renderAQIBarChart(timeData, labels) {
var legendY = chartBottom + 22;
var legendHtml = '<g transform="translate(' + chartLeft + ',' + legendY + ')">';
var lx = 0;
for (var i = 0; i < aqiLevels.length; i++) {
var lv = aqiLevels[i];
legendHtml += '<rect x="' + lx + '" y="0" width="10" height="10" fill="' + lv.color + '" rx="1.5"/>';
legendHtml += '<text x="' + (lx + 14) + '" y="9" fill="#999" font-size="9">' + lv.name + '</text>';
for (var i = 0; i < levelNames.length; i++) {
legendHtml += '<rect x="' + lx + '" y="0" width="10" height="10" fill="' + cfg.levelColors[i] + '" rx="1.5"/>';
legendHtml += '<text x="' + (lx + 14) + '" y="9" fill="#999" font-size="9">' + levelNames[i] + '</text>';
lx += 52;
}
legendHtml += '</g>';
@@ -456,7 +483,7 @@ function renderAQIBarChart(timeData, labels) {
'<line x1="' + chartLeft + '" y1="' + chartBottom + '" x2="' + chartRight + '" y2="' + chartBottom + '" class="chart-x-axis"/>' +
barsHtml + legendHtml + '</svg>';
window._trendTooltipData = { key: 'aqiBar', barData: barData, labels: labels, xPositions: xPositions };
window._trendTooltipData = { key: 'gradeBar', barData: barData, labels: labels, xPositions: xPositions, levelNames: levelNames, levelColors: cfg.levelColors };
return svg;
}
@@ -467,10 +494,10 @@ function renderTrend(metricName, period) {
var timeData = cfg[period];
if (!timeData) return '';
// AQI 非 24h → 柱状堆积图
if (cfg.isAQI && period !== '24h') {
// 等级类指标(非24h) → 柱状堆积图
if ((cfg.isAQI || cfg.isGrade) && period !== '24h') {
var labels = period === '7d' ? getXLabels7() : getXLabels30();
return renderAQIBarChart(timeData, labels);
return renderGradeBarChart(cfg, timeData, labels);
}
var points = timeData.points;
@@ -481,9 +508,9 @@ function renderTrend(metricName, period) {
var yMax = cfg.yMax;
var yTicks = cfg.yTicks;
// AQI 等级背景(先渲染,作为底层)
// 等级背景(先渲染,作为底层)
var levelBandsHtml = '';
if (cfg.isAQI && cfg.levelBands) {
if ((cfg.isAQI || cfg.isGrade) && cfg.levelBands) {
for (var i = 0; i < cfg.levelBands.length; i++) {
var b = cfg.levelBands[i];
var bandY1 = valToY(b.yMax, yMax); // yMin value in data → SVG y
@@ -532,14 +559,14 @@ function renderTrend(metricName, period) {
var polylinePoints = '';
var dotsHtml = '';
var dotR = n > 20 ? 2.5 : 3.5;
var unit = cfg.isAQI ? '' : (gen.unit || '');
window._trendTooltipData = { key: 'line', points: points, labels: labels, unit: unit, isAQI: cfg.isAQI, xPositions: xPositions };
var unit = (cfg.isAQI || cfg.isGrade) ? '' : (gen.unit || '');
window._trendTooltipData = { key: 'line', points: points, labels: labels, unit: unit, isGrade: !!(cfg.isAQI || cfg.isGrade), gradeInfo: cfg.isAQI || cfg.isGrade ? { levelNames: cfg.levelNames, levelColors: cfg.levelColors, yMax: cfg.yMax, yTicks: cfg.yTicks } : null, xPositions: xPositions };
for (var i = 0; i < n; i++) {
var y = valToY(points[i], yMax);
var x = xPositions[i];
pathD += (i === 0 ? 'M' : 'L') + x + ',' + y + ' ';
polylinePoints += x + ',' + y + ' ';
var dotColor = cfg.isAQI ? getAqiColor(points[i]) : chartLine;
var dotColor = (cfg.isAQI || cfg.isGrade) ? getGradeColor(points[i], cfg) : chartLine;
dotsHtml += '<circle cx="' + x + '" cy="' + y + '" r="' + dotR + '" class="chart-dot" style="fill:' + dotColor + ';cursor:pointer;" ' +
'data-idx="' + i + '" onmouseenter="showTrendTip(event,this)" onmouseleave="hideTrendTip()"/>';
}
@@ -561,13 +588,25 @@ function renderTrend(metricName, period) {
return svg;
}
/* AQI 等级名称映射 */
function getAqiLevel(val) {
if (val >= 151) return '重污染';
if (val >= 101) return '中污染';
if (val >= 51) return '轻污染';
if (val >= 21) return '未污染';
return '清洁';
/* 通用等级名称映射(根据gradeInfo */
function getGradeLevel(val, cfg) {
var names = cfg.levelNames;
var bands = cfg.levelBands;
if (!bands) return names[0] || '';
for (var i = 0; i < bands.length; i++) {
if (val >= bands[i].yMin && val <= bands[i].yMax) return names[i];
}
return names[names.length - 1];
}
/* 通用等级颜色映射 */
function getGradeColor(val, cfg) {
var bands = cfg.levelBands;
if (!bands) return cfg.levelColors[0] || chartLine;
for (var i = 0; i < bands.length; i++) {
if (val >= bands[i].yMin && val <= bands[i].yMax) return cfg.levelColors[i];
}
return cfg.levelColors[cfg.levelColors.length - 1] || chartLine;
}
/* 显示折线图数据点 Tip */
@@ -578,10 +617,10 @@ function showTrendTip(e, el) {
var dateStr = td.labels[idx] || '';
var val = td.points[idx];
var unit = td.unit || '';
var color = td.isAQI ? getAqiColor(val) : chartLine;
var color = td.isGrade ? getGradeColor(val, td.gradeInfo) : chartLine;
document.getElementById('tooltipDate').textContent = dateStr;
if (td.isAQI) {
var level = getAqiLevel(val);
if (td.isGrade) {
var level = getGradeLevel(val, td.gradeInfo);
document.getElementById('tooltipBody').innerHTML =
'<div class="chart-tooltip__row">' +
'<span class="chart-tooltip__dot" style="background:' + color + '"></span>' +
@@ -605,19 +644,24 @@ function showBarTip(e, el) {
var dateStr = td.labels[idx] || '';
document.getElementById('tooltipDate').textContent = dateStr;
var rows = '';
var levels = [
{name:'清洁', color:'#13C2C2', val:bd.clean},
{name:'未污染', color:'#52C41A', val:bd.good},
{name:'轻污染', color:'#FAAD14', val:bd.light},
{name:'中污染', color:'#FA541C', val:bd.moderate},
{name:'重污染', color:'#FF4D4F', val:bd.heavy}
];
for (var i = 0; i < levels.length; i++) {
var lv = levels[i];
var pct = Math.round(lv.val / 24 * 100);
var levelNames = td.levelNames;
var levelColors = td.levelColors;
// 根据 levelKeys 读取 barData 值
var levelKeysArr;
if (levelNames[0] === '很低') {
levelKeysArr = ['vl','lo','md','hi','vh'];
} else if (levelNames.length === 2) {
levelKeysArr = ['pass','fail'];
} else {
levelKeysArr = ['clean','good','light','moderate','heavy'];
}
var total24 = 24;
for (var i = 0; i < levelNames.length; i++) {
var v = bd[levelKeysArr[i]] || 0;
var pct = Math.round(v / total24 * 100);
rows += '<div class="chart-tooltip__row">' +
'<span class="chart-tooltip__dot" style="background:' + lv.color + '"></span>' +
'<span class="chart-tooltip__label">' + lv.name + '</span>' +
'<span class="chart-tooltip__dot" style="background:' + levelColors[i] + '"></span>' +
'<span class="chart-tooltip__label">' + levelNames[i] + '</span>' +
'<span class="chart-tooltip__val">' + pct + '%</span></div>';
}
document.getElementById('tooltipBody').innerHTML = rows;
@@ -684,16 +728,20 @@ function buildGauge(levelIdx, total, color) {
function renderChart(metricIdx, period) {
if (metricIdx === -1) {
return renderTrend('空气质量指数', period);
return renderTrend(primaryMetricName, period);
}
return renderTrend(dev.metrics[metricIdx].k, period);
}
// 根据 tab 类型确定主指标名称和等级标签
var primaryMetricName = (tab === 'outdoor' || tab === 'indoor') ? '空气质量指数' : tab === 'water' ? '水质等级' : tab === 'pollen' ? '过敏等级' : '空气质量指数';
var gradeLabelText = (tab === 'outdoor' || tab === 'indoor') ? '空气质量级别:' : tab === 'water' ? '水质等级:' : tab === 'pollen' ? '过敏等级:' : '空气质量级别:';
function render() {
document.getElementById('levelName').textContent = tabData.levelLabels[li];
document.getElementById('levelSub').textContent = tabData.sub;
document.getElementById('indexVal').textContent = '综合指数:' + tabData.indexes[li];
document.getElementById('gradeVal').textContent = '空气质量级别:' + tabData.grades[li];
document.getElementById('gradeVal').textContent = gradeLabelText + tabData.grades[li];
document.getElementById('cardLoc').textContent = dev.unit + ' · ' + dev.loc;
document.getElementById('cardTime').textContent = '更新于今天 ' + dev.time;
document.getElementById('cardDesc').textContent = tabData.descs[li];
@@ -701,9 +749,9 @@ function render() {
document.getElementById('metricsGrid').innerHTML = dev.metrics.map(m =>
`<div class="metric-item"><div class="metric-item__val">${m.v}<span class="metric-item__unit">${m.u}</span></div><div class="metric-item__label">${m.k}</div></div>`
).join('');
// 趋势下拉框:增加"空气质量指数"并默认选中
// 趋势下拉框:根据 tab 显示对应的主指标名称
const sel = document.getElementById('trendMetricSelect');
let opts = '<option value="-1">空气质量指数</option>';
let opts = '<option value="-1">' + primaryMetricName + '</option>';
opts += dev.metrics.map((m, i) => `<option value="${i}">${m.k}</option>`).join('');
sel.innerHTML = opts;
sel.value = trendMetricIdx;