feat(quality): 支持质检参数大屏显示开启与关闭,并在数据大屏对齐联动过滤隐藏
这个提交包含在:
@@ -353,7 +353,7 @@
|
||||
<el-form-item label="检测参数:" prop="content">
|
||||
<div class="quality-editor-card">
|
||||
<div class="quality-editor-toolbar">
|
||||
<div><strong>录入提示:</strong>先填实测值、预警值和警告值,再选择指标偏好。等于阈值时归入较低一级。</div>
|
||||
<div><strong>录入提示:</strong>先填实测值、预警值和警告值,再选择指标偏好。大屏显示列可开启或关闭该参数在大屏上的展示。等于阈值时归入较低一级。</div>
|
||||
<div class="quality-legend" aria-label="颜色规则">
|
||||
<span class="good">绿 · 达标</span>
|
||||
<span class="warn">黄 · 预警</span>
|
||||
@@ -393,11 +393,11 @@
|
||||
v-model="typeList[formData.type]['content'][scope.$index].alert"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_number" width="100" label="图表" show-overflow-tooltip
|
||||
<el-table-column prop="is_number" width="110" label="大屏显示" show-overflow-tooltip
|
||||
align="center">
|
||||
<template scope="scope">
|
||||
<el-button @click="handelIsNumber(scope.$index, 2)" size="mini" v-if="scope.row.is_number == 1" type="success" icon="el-icon-check">是</el-button>
|
||||
<el-button @click="handelIsNumber(scope.$index, 1)" size="mini" v-else type="warning" icon="el-icon-close">否</el-button>
|
||||
<el-button @click="handelIsNumber(scope.$index, 2)" size="mini" v-if="scope.row.is_number == 1" type="success" icon="el-icon-check">显示</el-button>
|
||||
<el-button @click="handelIsNumber(scope.$index, 1)" size="mini" v-else type="warning" icon="el-icon-close">隐藏</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="270" label="指标偏好与颜色规则" align="left">
|
||||
@@ -488,6 +488,13 @@
|
||||
<el-table-column prop="alert" width="230" label="警告值" show-overflow-tooltip
|
||||
align="center">
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_number" width="100" label="大屏显示" show-overflow-tooltip
|
||||
align="center">
|
||||
<template scope="scope">
|
||||
<el-tag size="mini" v-if="scope.row.is_number == 1" type="success">显示</el-tag>
|
||||
<el-tag size="mini" v-else type="info">隐藏</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="250" label="指标偏好与颜色规则" align="left">
|
||||
<template scope="scope">
|
||||
<template v-if="isNumericRule(scope.row)">
|
||||
|
||||
@@ -45,6 +45,14 @@ export const DESIGN_THEME = {
|
||||
};
|
||||
|
||||
|
||||
// 全部质检状态(卡片、图表、提示)共用同一组语义色。
|
||||
export const QUALITY_STATUS_COLORS = {
|
||||
normal: DESIGN_THEME.colors.success,
|
||||
warn: DESIGN_THEME.colors.warning,
|
||||
danger: DESIGN_THEME.colors.danger,
|
||||
unknown: '#94a3b8'
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算大屏视口等比缩放与居中偏移(防止电视机溢出或出现滚动条)
|
||||
* @param {number} winW 浏览器/视口宽度
|
||||
@@ -241,17 +249,25 @@ export function normalizeGranaryList(apiData) {
|
||||
const rawList = apiData?.data?.list || apiData?.list || (Array.isArray(apiData) ? apiData : []);
|
||||
|
||||
return rawList.map(item => {
|
||||
const content = decodeLooseUnicodeDeep(item.content || {});
|
||||
const isOilTank = item.granary_type === 1 || item.name.includes('油罐');
|
||||
const isSilo = item.granary_type === 3 ||
|
||||
const content = { ...decodeLooseUnicodeDeep(item.content || {}) };
|
||||
const mappings = {capacity:'capacity', tankCapacity:'capacity', storageNum:'storage_num', nature:'nature', addr:'addr', harvestYear:'harvest_year', enterTime:'enter_time', level:'level', weight:'weight', impurity:'impurity', water:'water', volume:'volume', decVolume:'dec_volume', imperfect:'imperfect', managementForm:'admin_form', length:'length', width:'width', diam:'diam', height:'height', storageMethod:'save_form', custodian:'custodian'};
|
||||
for (const [key, column] of Object.entries(mappings)) {
|
||||
if ((content[key] === undefined || content[key] === null || String(content[key]).trim() === '' || content[key] === '-') && item[column] !== undefined && item[column] !== null) {
|
||||
if (['length','width','diam','height'].includes(key) && !Number(item[column])) continue;
|
||||
if (['harvestYear','enterTime'].includes(key) && (!item[column] || String(item[column]).startsWith('1970'))) continue;
|
||||
content[key] = item[column];
|
||||
}
|
||||
}
|
||||
const isOilTank = Number(item.granary_type) === 1 || item.name.includes('油罐');
|
||||
const isSilo = Number(item.granary_type) === 3 ||
|
||||
item.name.includes('浅圆') ||
|
||||
item.name.includes('筒仓') ||
|
||||
content.warehouseType?.includes('浅圆') ||
|
||||
content.warehouseType?.includes('筒仓');
|
||||
const grainTypeCategory = isOilTank ? 'oilTank' : (isSilo ? 'silo' : 'warehouse');
|
||||
|
||||
const capacityNum = Number(content.capacity || content.tankCapacity || 0);
|
||||
const storageNum = Number(content.storageNum || 0);
|
||||
const capacityNum = Number(content.capacity ?? content.tankCapacity ?? item.capacity ?? 0) || 0;
|
||||
const storageNum = Number(content.storageNum ?? item.storage_num ?? 0) || 0;
|
||||
const grainName = item.varieties?.name || content.varieties || (isOilTank ? '菜籽油' : '小麦');
|
||||
|
||||
return {
|
||||
@@ -261,7 +277,7 @@ export function normalizeGranaryList(apiData) {
|
||||
screenPosition: content.screenPosition || '',
|
||||
screenCoord: item.screen_coord || null,
|
||||
storage: item.storage || '遵义直属库有限公司中心库区',
|
||||
granaryType: item.granary_type,
|
||||
granaryType: Number(item.granary_type),
|
||||
isOilTank,
|
||||
grainTypeCategory,
|
||||
grainName,
|
||||
@@ -270,6 +286,9 @@ export function normalizeGranaryList(apiData) {
|
||||
utilization: capacityNum > 0 ? Number(((storageNum / capacityNum) * 100).toFixed(1)) : 0,
|
||||
rawContent: content,
|
||||
qualityTest: decodeLooseUnicodeDeep(item.quality_test || null),
|
||||
dataSource: apiData?.data?.source || 'reference',
|
||||
isDemo: Number(item.quality_test?.is_demo) === 1,
|
||||
site: item.site || apiData?.data?.site || apiData?.site || (String(item.storage || '').includes('习水') ? 'xishui' : 'center'),
|
||||
createTime: item.create_time
|
||||
};
|
||||
});
|
||||
@@ -355,45 +374,85 @@ export const QUALITY_BENCHMARKS = {
|
||||
* @param {Object} item 指标对象
|
||||
* @returns {Object} 状态结果 { status: 'normal'|'warn'|'danger', statusText, badgeText, color }
|
||||
*/
|
||||
function qualityNumber(value) {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') return NaN;
|
||||
const text = String(value).trim();
|
||||
if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(text)) return NaN;
|
||||
const number = Number(text);
|
||||
return Number.isFinite(number) ? number : NaN;
|
||||
}
|
||||
|
||||
const unknownQualityStatus = (text = '检测值缺失或无效') => ({
|
||||
status: 'unknown', statusText: text, badgeText: '待核实', color: QUALITY_STATUS_COLORS.unknown
|
||||
});
|
||||
|
||||
export function isQualityParamVisible(param) {
|
||||
if (!param || typeof param !== 'object') return false;
|
||||
if (param.is_display !== undefined && param.is_display !== null) {
|
||||
return Number(param.is_display) === 1 || param.is_display === true;
|
||||
}
|
||||
if (param.is_number !== undefined && param.is_number !== null) {
|
||||
return Number(param.is_number) === 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function evaluateQualityStatus(item) {
|
||||
if (!item) return { status: 'normal', statusText: '健康达标', badgeText: '达标', color: '#10b981' };
|
||||
if (!item) return { status: 'normal', statusText: '健康达标', badgeText: '达标', color: QUALITY_STATUS_COLORS.normal };
|
||||
|
||||
if (item.type === 'qualitative') {
|
||||
const val = String(item.value || '');
|
||||
const val = String(item.value ?? '').trim();
|
||||
if (!val || /^(?:\/|--?|未检测|未提供|暂无)$/.test(val)) return unknownQualityStatus();
|
||||
// Historical grade thresholds are descriptions, e.g. 一等/大于等于790.
|
||||
const firstGrade = String(item.warn ?? '').match(/(?:大于等于|≥)\s*(\d+(?:\.\d+)?)/);
|
||||
const gradeRanges = [...String(item.alert ?? '').matchAll(/(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)/g)];
|
||||
const gradeValue = Number(val);
|
||||
if (firstGrade && gradeRanges.length && val.trim() !== '' && Number.isFinite(gradeValue)) {
|
||||
const lowestGrade = Math.min(...gradeRanges.map(range => Number(range[1])));
|
||||
if (gradeValue >= Number(firstGrade[1])) {
|
||||
return { status: 'normal', statusText: '等级达标', badgeText: '达标', color: QUALITY_STATUS_COLORS.normal };
|
||||
}
|
||||
return gradeValue >= lowestGrade
|
||||
? { status: 'warn', statusText: '等级预警', badgeText: '预警', color: QUALITY_STATUS_COLORS.warn }
|
||||
: { status: 'danger', statusText: '低于等级标准', badgeText: '超标', color: QUALITY_STATUS_COLORS.danger };
|
||||
}
|
||||
const negative = /不合格|未达标|不达标|不宜存|不符合|异常|不具有|未有|有异味/.test(val.replace(/无异味/g, ''));
|
||||
const isOk = !negative && (val.includes('正常') || val.includes('达标') || val.includes('宜存') ||
|
||||
val.includes('符合') || val.includes('合格') || val.includes('透明') ||
|
||||
val.includes('变浅') || val.includes('棕') || val.includes('固有') ||
|
||||
val.includes('无析出物') || val.includes('无异味') || val.includes('未检出'));
|
||||
return isOk
|
||||
? { status: 'normal', statusText: '健康达标', badgeText: '达标', color: '#10b981' }
|
||||
: { status: 'danger', statusText: '超标异常', badgeText: '异常', color: '#ef4444' };
|
||||
? { status: 'normal', statusText: '健康达标', badgeText: '达标', color: QUALITY_STATUS_COLORS.normal }
|
||||
: { status: 'danger', statusText: '超标异常', badgeText: '异常', color: QUALITY_STATUS_COLORS.danger };
|
||||
}
|
||||
|
||||
const num = parseFloat(item.value);
|
||||
if (isNaN(num)) {
|
||||
return { status: 'normal', statusText: '健康达标', badgeText: '合格', color: '#10b981' };
|
||||
}
|
||||
const nonDetect = /^(?:未检出|未检出\s*ND|ND)$/i.test(String(item.value ?? '').trim());
|
||||
const num = nonDetect ? 0 : qualityNumber(item.value);
|
||||
if (!Number.isFinite(num)) return unknownQualityStatus();
|
||||
|
||||
const warn = parseFloat(item.warn);
|
||||
const alert = parseFloat(item.alert);
|
||||
const warn = qualityNumber(item.warn);
|
||||
const alert = qualityNumber(item.alert);
|
||||
if (!Number.isFinite(warn) || !Number.isFinite(alert)
|
||||
|| (item.direction === 'lower' ? warn < alert : warn > alert)) {
|
||||
return unknownQualityStatus('预警阈值无效,请核实配置');
|
||||
}
|
||||
|
||||
if (item.direction === 'lower') {
|
||||
if (!isNaN(alert) && num < alert) {
|
||||
return { status: 'danger', statusText: '超标警戒', badgeText: '超标', color: '#ef4444' };
|
||||
return { status: 'danger', statusText: '超标警戒', badgeText: '超标', color: QUALITY_STATUS_COLORS.danger };
|
||||
}
|
||||
if (!isNaN(warn) && num < warn) {
|
||||
return { status: 'warn', statusText: '轻度预警', badgeText: '预警', color: '#f59e0b' };
|
||||
return { status: 'warn', statusText: '轻度预警', badgeText: '预警', color: QUALITY_STATUS_COLORS.warn };
|
||||
}
|
||||
return { status: 'normal', statusText: '健康达标', badgeText: '达标', color: '#10b981' };
|
||||
return { status: 'normal', statusText: '健康达标', badgeText: '达标', color: QUALITY_STATUS_COLORS.normal };
|
||||
} else {
|
||||
if (!isNaN(alert) && num > alert) {
|
||||
return { status: 'danger', statusText: '超标警戒', badgeText: '超标', color: '#ef4444' };
|
||||
return { status: 'danger', statusText: '超标警戒', badgeText: '超标', color: QUALITY_STATUS_COLORS.danger };
|
||||
}
|
||||
if (!isNaN(warn) && num > warn) {
|
||||
return { status: 'warn', statusText: '轻度预警', badgeText: '预警', color: '#f59e0b' };
|
||||
return { status: 'warn', statusText: '轻度预警', badgeText: '预警', color: QUALITY_STATUS_COLORS.warn };
|
||||
}
|
||||
return { status: 'normal', statusText: '健康达标', badgeText: (item.warn !== undefined && num <= warn) ? '健康' : '达标', color: '#10b981' };
|
||||
return { status: 'normal', statusText: '健康达标', badgeText: (item.warn !== undefined && num <= warn) ? '健康' : '达标', color: QUALITY_STATUS_COLORS.normal };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +461,7 @@ export function evaluateQualityStatus(item) {
|
||||
* @param {string} grainTypeOrName 品种名称或类型
|
||||
* @param {Object} overrides 仓房特有指标覆盖 (如 { water: '12.8%' })
|
||||
*/
|
||||
export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides = {}, qualityTest = null) {
|
||||
export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides = {}, qualityTest = null, options = {}) {
|
||||
const str = String(grainTypeOrName || '').toLowerCase();
|
||||
let key = 'wheat';
|
||||
if (str.includes('油') || str.includes('oil') || str.includes('rapeseed')) {
|
||||
@@ -413,13 +472,23 @@ export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides =
|
||||
|
||||
const benchmark = QUALITY_BENCHMARKS[key];
|
||||
if (!benchmark) return null;
|
||||
const recordedParams = Array.isArray(qualityTest?.testing_params) ? qualityTest.testing_params : [];
|
||||
if (options.requireRecordedData && recordedParams.length === 0) {
|
||||
return {
|
||||
variety: benchmark.variety, varietyKey: key,
|
||||
inspectDate: '', inspectStage: '未录入化验记录', dataSource: 'missing', empty: true,
|
||||
items: [],
|
||||
summary: { totalCount: 0, dangerCount: 0, warnCount: 0, normalCount: 0, unknownCount: 0, overallStatus: 'unknown' }
|
||||
};
|
||||
}
|
||||
|
||||
let dangerCount = 0;
|
||||
let warnCount = 0;
|
||||
let normalCount = 0;
|
||||
let unknownCount = 0;
|
||||
|
||||
// A saved test is the source of truth for values, thresholds and direction.
|
||||
const savedParams = qualityTest?.testing_params || [];
|
||||
const savedParams = recordedParams.filter(isQualityParamVisible);
|
||||
const cleanName = name => String(name).replace(/[((].*?[))]/g, '').replace(/[、,,\s]/g, '');
|
||||
const aliases = {
|
||||
'杂质': 'impurity', '杂质总量': 'impurity', '谷外糙米': 'milledGrainOutside',
|
||||
@@ -434,15 +503,14 @@ export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides =
|
||||
}
|
||||
const base = benchmark.items.find(it => it.key === itemKey
|
||||
|| cleanName(it.name) === cleanName(param.name) || cleanName(it.shortName) === cleanName(param.name));
|
||||
const numeric = [param.warning, param.alert].every(v => v !== null && v !== undefined
|
||||
&& String(v).trim() !== '' && Number.isFinite(Number(v)));
|
||||
const numeric = [param.warning, param.alert].every(v => Number.isFinite(qualityNumber(v)));
|
||||
return {
|
||||
...(base || { key: `saved-${param.num || index}`, category: 'physical' }),
|
||||
name: param.name, shortName: base?.shortName || param.name,
|
||||
value: param.val, unit: param.unit || '', warn: param.warning, alert: param.alert,
|
||||
direction: param.direction === 'upper' || param.direction === 'lower' ? param.direction
|
||||
: (numeric && Number(param.warning) > Number(param.alert) ? 'lower' : 'upper'),
|
||||
type: numeric ? undefined : 'qualitative', savedRule: numeric
|
||||
type: numeric || Number(param.is_number) === 1 ? undefined : 'qualitative', savedRule: numeric
|
||||
};
|
||||
}) : benchmark.items;
|
||||
const items = sourceItems.map(baseItem => {
|
||||
@@ -459,16 +527,20 @@ export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides =
|
||||
|
||||
if (item.status === 'danger') dangerCount++;
|
||||
else if (item.status === 'warn') warnCount++;
|
||||
else if (item.status === 'unknown') unknownCount++;
|
||||
else normalCount++;
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
const overallStatus = dangerCount > 0 ? 'danger' : (warnCount > 0 ? 'warn' : 'normal');
|
||||
const overallStatus = dangerCount > 0 ? 'danger' : (warnCount > 0 ? 'warn' : (unknownCount > 0 ? 'unknown' : 'normal'));
|
||||
|
||||
return {
|
||||
variety: benchmark.variety,
|
||||
varietyKey: key,
|
||||
dataSource: savedParams.length ? (Number(qualityTest?.is_demo) === 1 ? 'demo' : 'recorded') : 'reference',
|
||||
isDemo: Number(qualityTest?.is_demo) === 1,
|
||||
empty: false,
|
||||
inspectDate: qualityTest?.test_date ? String(qualityTest.test_date).replace(/^(\d{4})(\d{2})$/, '$1-$2') : benchmark.inspectDate,
|
||||
inspectStage: qualityTest ? ({ 1: '春季检测', 2: '秋季检测', 3: '满仓验收' }[qualityTest.season] || '最新检测') : benchmark.inspectStage,
|
||||
items,
|
||||
@@ -477,6 +549,7 @@ export function getGranaryQualityReport(grainTypeOrName = '小麦', overrides =
|
||||
dangerCount,
|
||||
warnCount,
|
||||
normalCount,
|
||||
unknownCount,
|
||||
overallStatus
|
||||
}
|
||||
};
|
||||
@@ -501,12 +574,14 @@ export function buildQualityHtml(qualityReport, filterCategory = 'all') {
|
||||
: qualityReport.items.filter(it => it.category === filterCategory);
|
||||
|
||||
return items.map(it => {
|
||||
const valClass = it.status === 'danger' ? 'val-danger' : (it.status === 'warn' ? 'val-warn' : 'val-normal');
|
||||
const cardClass = it.status === 'danger' ? 'card-danger' : (it.status === 'warn' ? 'card-warn' : '');
|
||||
const valClass = `val-${it.status}`;
|
||||
const cardClass = it.status === 'danger' ? 'card-danger' : (it.status === 'warn' ? 'card-warn' : (it.status === 'unknown' ? 'card-unknown' : ''));
|
||||
|
||||
// 阈值说明文字 (保证极简防折行)
|
||||
let threshDesc = '';
|
||||
if (it.type === 'qualitative') {
|
||||
if (it.status === 'unknown') {
|
||||
threshDesc = it.statusText;
|
||||
} else if (it.type === 'qualitative') {
|
||||
threshDesc = it.status === 'normal' ? '标准达标' : '未达标';
|
||||
} else if (it.alert !== undefined) {
|
||||
const sym = it.direction === 'lower' ? '≥' : '≤';
|
||||
@@ -537,7 +612,7 @@ export function buildQualityHtml(qualityReport, filterCategory = 'all') {
|
||||
* @param {Object} qualityReport
|
||||
*/
|
||||
export function getCoreHudMetrics(qualityReport) {
|
||||
if (!qualityReport || !qualityReport.items) return [];
|
||||
if (!qualityReport || !qualityReport.items?.length || qualityReport.empty) return [];
|
||||
const key = qualityReport.varietyKey;
|
||||
const items = qualityReport.items;
|
||||
|
||||
@@ -609,8 +684,8 @@ export function getCoreHudMetrics(qualityReport) {
|
||||
unit: it.unit || '',
|
||||
status: it.status,
|
||||
badgeText: it.badgeText,
|
||||
gaugePercent,
|
||||
limitDesc
|
||||
gaugePercent: computeStripMetric(it, key).gaugePercent,
|
||||
limitDesc: computeStripMetric(it, key).limitDesc
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -683,11 +758,21 @@ export function computeStripMetric(it, varietyKey = 'wheat') {
|
||||
limitDesc = it.value === '未检出' ? `限量达标 · 未检出` : `监测达标`;
|
||||
}
|
||||
|
||||
if (it.type === 'qualitative' && /大于等于|≥/.test(String(it.warn))) {
|
||||
limitDesc = `等级标准 | ${it.warn}`;
|
||||
}
|
||||
|
||||
if (it.savedRule) {
|
||||
const lower = it.direction === 'lower';
|
||||
limitDesc = `${it.status === 'danger' ? '警告' : it.status === 'warn' ? '预警' : '正常'} | ${lower ? '越大越好(低值预警)' : '越小越好(高值预警)'} · 预警${lower ? '<' : '>'}${it.warn} · 警告${lower ? '<' : '>'}${it.alert}${it.unit || ''}`;
|
||||
}
|
||||
|
||||
if (it.status === 'unknown') {
|
||||
gaugePercent = 0;
|
||||
limitDesc = it.statusText || '检测值缺失或无效,请核实';
|
||||
}
|
||||
gaugePercent = Number.isFinite(gaugePercent) ? Math.max(0, Math.min(100, gaugePercent)) : 0;
|
||||
|
||||
return {
|
||||
...it,
|
||||
gaugePercent,
|
||||
@@ -703,6 +788,13 @@ export function computeStripMetric(it, varietyKey = 'wheat') {
|
||||
*/
|
||||
export function buildQualityHudHtml(qualityReport, filterCategory = 'all') {
|
||||
if (!qualityReport || !qualityReport.items) return '';
|
||||
if (qualityReport.empty) {
|
||||
return `<div class="quality-empty-state" role="status">
|
||||
<span class="quality-badge status-unknown">暂无化验数据</span>
|
||||
<h3>本仓尚未录入化验记录</h3>
|
||||
<p>请在后台添加本仓化验数据。保存后,检测值、预警方向及颜色状态将自动同步。</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const varietyKey = qualityReport.varietyKey;
|
||||
const allItems = qualityReport.items;
|
||||
@@ -732,9 +824,10 @@ export function buildQualityHudHtml(qualityReport, filterCategory = 'all') {
|
||||
// 生成全量水平条形卡片 (Horizontal Strip Cards)
|
||||
const stripCardsHtml = displayItems.map(rawItem => {
|
||||
const item = computeStripMetric(rawItem, varietyKey);
|
||||
const cardClass = item.status === 'danger' ? 'strip-danger' : (item.status === 'warn' ? 'strip-warn' : 'strip-normal');
|
||||
const valClass = item.status === 'danger' ? 'val-danger' : (item.status === 'warn' ? 'val-warn' : 'val-normal');
|
||||
const gaugeClass = item.status === 'danger' ? 'gauge-danger' : (item.status === 'warn' ? 'gauge-warn' : 'gauge-normal');
|
||||
if (qualityReport.isDemo) item.limitDesc = item.limitDesc.replace('国家标准合规', '演示规则').replace('检验通过', '示例达标');
|
||||
const cardClass = `strip-${item.status}`;
|
||||
const valClass = `val-${item.status}`;
|
||||
const gaugeClass = `gauge-${item.status}`;
|
||||
const alertPrefix = item.status === 'danger' ? '⚠️ ' : '';
|
||||
const displayName = item.shortName || item.name;
|
||||
const unitStr = item.unit ? `${item.unit}` : '';
|
||||
@@ -787,12 +880,38 @@ export function buildQualityHudHtml(qualityReport, filterCategory = 'all') {
|
||||
* 格式化仓房/油罐详细展示参数 (分类提取基础、尺寸、理化指标)
|
||||
* @param {Object} normalizedItem
|
||||
*/
|
||||
function formatRecordedGranaryParams(item) {
|
||||
const c = item.rawContent || {};
|
||||
const text = (key, fallback = '—') => c[key] === undefined || c[key] === null || String(c[key]).trim() === '' || c[key] === '-' ? fallback : String(c[key]);
|
||||
const unit = (key, suffix) => text(key) === '—' ? '—' : `${text(key)}${suffix}`;
|
||||
const percentage = key => text(key) === '—' ? '—' : (text(key).includes('%') ? text(key) : `${text(key)}%`);
|
||||
const oil = item.isOilTank;
|
||||
const silo = item.grainTypeCategory === 'silo';
|
||||
const natureName = {ZC:'中央储备粮 (ZC)',SP:'商品粮 (SP)',DF:'地方储备粮 (DF)'}[c.nature] || text('nature');
|
||||
const basic = {
|
||||
warehouseType: oil ? text('tankType','立式油罐') : (/^\d+$/.test(text('warehouseType')) ? (silo ? '立式浅圆仓' : '高大平房仓') : text('warehouseType',silo ? '立式浅圆仓' : '高大平房仓')),
|
||||
capacity: `${item.capacityNum} 吨`, storageNum: `${item.storageNum} 吨`, varieties: item.grainName, natureName,
|
||||
addr:text('addr'),productArea:text('productArea'),harvestYear:text('harvestYear'),machiningYear:text('machiningYear'),
|
||||
enterTime:text('enterTime'),managementForm:text('managementForm'),custodian:text('custodian'),storageMethod:text('storageMethod')
|
||||
};
|
||||
const size = oil ? {diameter:text('TankDiameter'),oilHeight:text('oilLoadingHeight'),oilVolume:unit('oilVolume',' m³')}
|
||||
: {length:text('length'),width:text('width'),diameter:text('diam'),height:text('height'),volume:unit('volume',' m³'),decVolume:unit('decVolume',' m³')};
|
||||
return {type:oil ? 'oilTank' : silo ? 'grainSilo' : 'grainWarehouse', basic, size,
|
||||
qualityReport:getGranaryQualityReport(item.grainName,c,item.qualityTest,{requireRecordedData:true}),
|
||||
quality:{level:text('level'),water:percentage('water'),impurity:percentage('impurity'),weight:text('weight'),imperfect:percentage('imperfect'),roughnessRate:text('roughnessRate'),riceGrains:text('riceGrains'),
|
||||
color:text('color'),odorAndTaste:text('odorAndTaste'),acidValue:text('acidValue'),peroxideValue:text('peroxideValue'),waterVolatileCompounds:text('waterVolatileCompounds'),insolubleImpurities:text('insolubleImpurities'),residualSolventContent:text('residualSolventContent'),heating:text('heating')}
|
||||
};
|
||||
}
|
||||
|
||||
export function formatGranaryParams(normalizedItem) {
|
||||
if (!normalizedItem) return null;
|
||||
if (normalizedItem.dataSource === 'database') return formatRecordedGranaryParams(normalizedItem);
|
||||
const c = normalizedItem.rawContent || {};
|
||||
const isOil = normalizedItem.isOilTank;
|
||||
const grainVariety = normalizedItem.grainName || c.varieties || (isOil ? '菜籽油' : '小麦');
|
||||
const qualityReport = getGranaryQualityReport(grainVariety, c, normalizedItem.qualityTest);
|
||||
const qualityReport = getGranaryQualityReport(grainVariety, c, normalizedItem.qualityTest, {
|
||||
requireRecordedData: normalizedItem.dataSource === 'database' || normalizedItem.site === 'xishui' || String(normalizedItem.storage || '').includes('习水')
|
||||
});
|
||||
|
||||
const natureMap = {
|
||||
'ZC': '中央储备粮 (ZC)',
|
||||
@@ -1216,7 +1335,8 @@ export function parseForecastResult(apiJson, grainType) {
|
||||
*/
|
||||
export function generateForecastSeries(grainType, currentReport, forecastList = [], env = null) {
|
||||
const type = grainType || currentReport?.grainType || 'wheat';
|
||||
const timeline = ['现状实测', ...forecastList.map(f => `+${f.day}天预测`)];
|
||||
if (currentReport?.empty) return { grainType: type, timeline: [], metrics: [], env: env || null };
|
||||
const timeline = [currentReport?.isDemo ? 'DEMO 演示现状' : '现状实测', ...forecastList.map(f => `+${f.day}天预测`)];
|
||||
|
||||
const currentItems = currentReport?.items || [];
|
||||
const findCurrentVal = (key, fallback) => {
|
||||
@@ -1238,18 +1358,26 @@ export function generateForecastSeries(grainType, currentReport, forecastList =
|
||||
];
|
||||
|
||||
const metrics = metricConfigs.map(cfg => {
|
||||
const curVal = findCurrentVal(cfg.key, cfg.fallback);
|
||||
const saved = currentItems.find(item => item.key === cfg.key);
|
||||
const unit = saved?.unit || cfg.unit;
|
||||
const canonicalUnit = value => String(value).replace(/[μµ]/g, 'u').replace(/\s/g, '').toLowerCase();
|
||||
const concentrations = {'ug/kg': 1, 'ng/g': 1, 'ug/g': 1000, 'mg/kg': 1000, 'mg/g': 1000000};
|
||||
const from = canonicalUnit(cfg.unit), to = canonicalUnit(unit);
|
||||
const factor = from === to ? 1 : concentrations[from] && concentrations[to] ? concentrations[from] / concentrations[to] : null;
|
||||
const curVal = findCurrentVal(cfg.key, factor === null ? null : cfg.fallback * factor);
|
||||
const rules = saved && Number.isFinite(qualityNumber(saved.warn)) && Number.isFinite(qualityNumber(saved.alert))
|
||||
? saved : cfg;
|
||||
const futureVals = forecastList.map(f => {
|
||||
const tVal = f.targets ? f.targets[cfg.targetKey] : undefined;
|
||||
return tVal !== undefined && tVal !== null && tVal !== '' && Number.isFinite(Number(tVal)) ? Number(Number(tVal).toFixed(2)) : null;
|
||||
return factor !== null && tVal !== undefined && tVal !== null && tVal !== '' && Number.isFinite(Number(tVal)) ? Number((Number(tVal) * factor).toFixed(4)) : null;
|
||||
});
|
||||
return {
|
||||
key: cfg.key,
|
||||
name: cfg.name,
|
||||
unit: cfg.unit,
|
||||
alert: cfg.alert,
|
||||
warn: cfg.warn,
|
||||
direction: cfg.key === 'glutenWater' ? 'lower' : 'upper',
|
||||
unit,
|
||||
alert: rules === cfg ? (factor === null ? undefined : cfg.alert * factor) : rules.alert,
|
||||
warn: rules === cfg ? (factor === null ? undefined : cfg.warn * factor) : rules.warn,
|
||||
direction: rules.direction || (cfg.key === 'glutenWater' ? 'lower' : 'upper'),
|
||||
values: [curVal, ...futureVals]
|
||||
};
|
||||
});
|
||||
@@ -1324,10 +1452,12 @@ export function buildForecastTrendChartOption(forecastSeries) {
|
||||
const c = forecastMetricColors.get(key);
|
||||
const current = forecastText(m.values[0]);
|
||||
const last = forecastText(m.values[m.values.length - 1]);
|
||||
const threshold = m.alert === undefined ? '' : `\n警戒 ${m.direction === 'lower' ? '≥' : '≤'} ${m.alert} ${m.unit || ''}`;
|
||||
titles.push({ text: m.name, subtext: `${current} → ${last} ${m.unit || ''}${threshold}`, left: 8, top: `${idx * row + 1}%`,
|
||||
const currentStatus = evaluateQualityStatus({...m, value: m.values[0]}).status;
|
||||
const lastStatus = evaluateQualityStatus({...m, value: m.values[m.values.length - 1]}).status;
|
||||
const threshold = `${m.warn === undefined ? '' : `\n{warn|预警 ${m.warn} ${m.unit || ''}}`}${m.alert === undefined ? '' : `\n{danger|警戒 ${m.alert} ${m.unit || ''}}`}`;
|
||||
titles.push({ text: m.name, subtext: `{${currentStatus}|${current} ${m.unit || ''}} → {${lastStatus}|${last === '暂无数据' ? '暂无预测' : last + ' ' + (m.unit || '')}}${threshold}`, left: 8, top: `${idx * row + 1}%`,
|
||||
textStyle: {color: '#e2eaf4', fontSize: 12, fontWeight: 600},
|
||||
subtextStyle: {color: c, fontSize: 12, lineHeight: 16}, itemGap: 5 });
|
||||
subtextStyle: {color: c, fontSize: 12, lineHeight: 16, rich: Object.fromEntries(Object.entries(QUALITY_STATUS_COLORS).map(([status, color]) => [status, {color, fontSize: 10}]))}, itemGap: 5 });
|
||||
grids.push({ left: 154, right: 36, top: `${idx * row + 3}%`, height: `${row - 9}%`, containLabel: true });
|
||||
xAxes.push({ type: 'category', gridIndex: idx, data: timeline, boundaryGap: false,
|
||||
axisTick: {show: false}, axisLine: {show: false},
|
||||
@@ -1343,17 +1473,25 @@ export function buildForecastTrendChartOption(forecastSeries) {
|
||||
emphasis: {focus: 'series', scale: 1.5},
|
||||
areaStyle: {color: {type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [{offset: 0, color: c + '16'}, {offset: 1, color: c + '00'}]}},
|
||||
data: m.values };
|
||||
if (m.alert !== undefined) result.markLine = {symbol: ['none', 'none'], silent: true, data: [{
|
||||
yAxis: m.alert, name: `${m.name}警戒线`,
|
||||
lineStyle: {color: '#c89e68', type: 'dashed', width: 1, opacity: 0.65},
|
||||
label: {formatter: `警戒 {c}${m.unit || ''}`, position: 'insideEndTop', color: '#dfb883', fontSize: 9,
|
||||
data: m.values.map(value => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const status = evaluateQualityStatus({...m, value});
|
||||
return {value, itemStyle: {color: status.status === 'normal' ? c : status.color}};
|
||||
}) };
|
||||
const references = [
|
||||
{value: m.warn, name: '预警', color: QUALITY_STATUS_COLORS.warn},
|
||||
{value: m.alert, name: '警戒', color: QUALITY_STATUS_COLORS.danger}
|
||||
].filter(ref => Number.isFinite(qualityNumber(ref.value)));
|
||||
result.markLine = {symbol: ['none', 'none'], silent: true, data: references.map(ref => ({
|
||||
yAxis: Number(ref.value), name: `${m.name}${ref.name}线`,
|
||||
lineStyle: {color: ref.color, type: 'dashed', width: 1, opacity: 0.8},
|
||||
label: {formatter: `${ref.name} {c}${m.unit || ''}`, position: ref.name === '预警' ? 'insideStartTop' : 'insideEndTop', color: ref.color, fontSize: 9,
|
||||
backgroundColor: '#122031', padding: [2, 4], borderRadius: 3}
|
||||
}]};
|
||||
}))};
|
||||
return result;
|
||||
});
|
||||
return { animationDuration: 350, title: titles, grid: grids, xAxis: xAxes, yAxis: yAxes,
|
||||
aria: {enabled: true, description: '各指标使用独立刻度。圆点为实测与预测值,点状虚线为预测趋势,金色虚线为警戒参考。'},
|
||||
aria: {enabled: true, description: '各指标使用独立刻度。圆点为实测与预测值,点状虚线为预测趋势,黄色虚线为预警参考,红色虚线为警戒参考。'},
|
||||
axisPointer: {link: [{xAxisIndex: 'all'}]},
|
||||
tooltip: {...forecastTooltip, formatter: params => {
|
||||
if (!Array.isArray(params) || !params.length) return '';
|
||||
@@ -1361,7 +1499,7 @@ export function buildForecastTrendChartOption(forecastSeries) {
|
||||
let html = `<div style="font-weight:600;margin-bottom:8px">${forecastEscape(heading)}</div>`;
|
||||
params.forEach(p => {
|
||||
const m = metrics[p.seriesIndex] || metrics.find(m => m.name === p.seriesName);
|
||||
html += `<div style="display:flex;justify-content:space-between;gap:24px"><span>${p.marker || ''} ${forecastEscape(p.seriesName)}</span><b>${forecastText(p.value)} <small>${forecastEscape(m?.unit)}</small></b></div>`;
|
||||
html += `<div style="display:flex;justify-content:space-between;gap:24px"><span>${p.marker || ''} ${forecastEscape(p.seriesName)}</span><b style="color:${evaluateQualityStatus({...m, value: p.value}).color}">${forecastText(p.value)} <small>${forecastEscape(m?.unit)}</small></b></div>`;
|
||||
});
|
||||
html += `<div style="margin-top:10px;padding-top:8px;border-top:1px solid #304157;color:#8ea3ba;font-size:11px">仓内 ${forecastEscape(envInfo.tm_i)}℃ / ${forecastEscape(envInfo.hm_i)}% · 仓外 ${forecastEscape(envInfo.tm_o)}℃ / ${forecastEscape(envInfo.hm_o)}%</div>`;
|
||||
return html;
|
||||
@@ -1377,9 +1515,15 @@ export function buildForecastCompareChartOption(grainType, currentItems = [], fu
|
||||
const unit = f.unit || cur?.unit || '';
|
||||
const scale = Math.max(current ?? 0, future ?? 0, 0.01);
|
||||
const delta = current !== null && future !== null ? Number((future - current).toFixed(2)) : null;
|
||||
return {...f, current, future, unit, scale, delta};
|
||||
return {...f, current, future, unit, scale, delta, rule: {...cur, ...f}};
|
||||
});
|
||||
const datum = (r, value) => ({value: value === null ? 0 : value / r.scale * 100, rawValue: value, unit: r.unit});
|
||||
const datum = (r, value) => {
|
||||
const status = evaluateQualityStatus({...r.rule, value});
|
||||
const color = status.status === 'normal' ? QUALITY_STATUS_COLORS.normal : status.color;
|
||||
return {value: value === null ? 0 : value / r.scale * 100, rawValue: value, unit: r.unit,
|
||||
// 未配置规则时维持实测/预测系列色;缺失值始终显示灰色。
|
||||
...(value === null || r.rule.alert !== undefined ? {label: {color}} : {})};
|
||||
};
|
||||
return { animationDuration: 350,
|
||||
aria: {enabled: true, description: '每个指标独立对比当前实测与未来预测,柱长仅可在同一指标内比较。'},
|
||||
tooltip: {...forecastTooltip, axisPointer: {type: 'shadow', shadowStyle: {color: '#94adc808'}}, formatter: params => {
|
||||
@@ -1387,7 +1531,7 @@ export function buildForecastCompareChartOption(grainType, currentItems = [], fu
|
||||
const r = rows[params[0].dataIndex];
|
||||
if (!r) return '';
|
||||
const change = r.delta === null ? '暂无预测' : `${r.delta > 0 ? '+' : ''}${r.delta} ${r.unit}`;
|
||||
return `<b>${forecastEscape(r.name)}</b><div>现状实测:${forecastText(r.current)} ${forecastEscape(r.unit)}</div><div>未来预测:${forecastText(r.future)} ${forecastEscape(r.unit)}</div><div style="border-top:1px solid #304157;margin-top:8px;padding-top:8px;color:#b9c9da">变化量:${forecastEscape(change)}</div>`;
|
||||
return `<b>${forecastEscape(r.name)}</b><div>现状实测:<span style="color:${evaluateQualityStatus({...r.rule, value: r.current}).color}">${forecastText(r.current)} ${forecastEscape(r.unit)}</span></div><div>未来预测:<span style="color:${evaluateQualityStatus({...r.rule, value: r.future}).color}">${forecastText(r.future)} ${forecastEscape(r.unit)}</span></div><div style="border-top:1px solid #304157;margin-top:8px;padding-top:8px;color:#b9c9da">变化量:${forecastEscape(change)}</div>`;
|
||||
}},
|
||||
legend: {top: 10, left: 12, itemWidth: 12, itemHeight: 7, itemGap: 22,
|
||||
textStyle: {color: '#a6bbcf', fontSize: 11}, data: ['现状实测', '未来预测']},
|
||||
@@ -1395,17 +1539,17 @@ export function buildForecastCompareChartOption(grainType, currentItems = [], fu
|
||||
yAxis: {type: 'category', inverse: true, data: rows.map(r => r.name),
|
||||
axisTick: {show: false}, axisLine: {show: false},
|
||||
axisLabel: {color: '#d2dfed', fontSize: 11, lineHeight: 17, width: 112, overflow: 'break',
|
||||
formatter: (v, i) => `${v.match(/.{1,8}/g)?.join('\n') || v}\n{unit|${rows[i].unit}}`, rich: {unit: {color: '#7089a2', fontSize: 10}}}},
|
||||
formatter: v => v.match(/.{1,8}/g)?.join('\n') || v}},
|
||||
xAxis: {type: 'value', max: 100, show: false},
|
||||
series: [{name: '现状实测', type: 'bar', barWidth: 9, barGap: '65%',
|
||||
itemStyle: {color: '#547797', borderRadius: 3},
|
||||
label: {show: true, position: 'right', distance: 8, color: '#afc3d8', fontSize: 11,
|
||||
formatter: p => `${forecastText(p.data.rawValue)} ${p.data.unit}`},
|
||||
formatter: p => p.data.rawValue === null ? '暂无数据' : `${forecastText(p.data.rawValue)} ${p.data.unit}`},
|
||||
data: rows.map(r => datum(r, r.current))},
|
||||
{name: '未来预测', type: 'bar', barWidth: 9,
|
||||
itemStyle: {color: '#59d9ce', borderRadius: 3},
|
||||
label: {show: true, position: 'right', distance: 8, color: '#83e9df', fontSize: 11, fontWeight: 600,
|
||||
formatter: p => `${forecastText(p.data.rawValue)} ${p.data.unit}`},
|
||||
formatter: p => p.data.rawValue === null ? '暂无数据' : `${forecastText(p.data.rawValue)} ${p.data.unit}`},
|
||||
data: rows.map(r => datum(r, r.future))}]
|
||||
};
|
||||
}
|
||||
@@ -2031,6 +2175,7 @@ export function getNextAerialMode(currentMode) {
|
||||
*/
|
||||
export function generateDepotAlerts(normalizedList = [], envData = null) {
|
||||
const alerts = [];
|
||||
let pendingQualityCount = 0;
|
||||
|
||||
// 1. 检查微气候环境工况
|
||||
if (envData) {
|
||||
@@ -2051,8 +2196,15 @@ export function generateDepotAlerts(normalizedList = [], envData = null) {
|
||||
// 2. 遍历各仓房质检报告
|
||||
(normalizedList || []).forEach(item => {
|
||||
const vName = item.grainName || (item.rawContent && item.rawContent.varieties) || (item.isOilTank ? '菜籽油' : '小麦');
|
||||
const report = getGranaryQualityReport(vName, item.rawContent || {}, item.qualityTest || null);
|
||||
const report = getGranaryQualityReport(vName, item.rawContent || {}, item.qualityTest || null, {
|
||||
requireRecordedData: item.dataSource === 'database' || item.site === 'xishui' || String(item.storage || '').includes('习水')
|
||||
});
|
||||
if (!report || !report.items) return;
|
||||
if (report.empty) {
|
||||
pendingQualityCount++;
|
||||
return;
|
||||
}
|
||||
if (report.summary.unknownCount > 0) pendingQualityCount++;
|
||||
|
||||
report.items.forEach(it => {
|
||||
if (it.status === 'danger') {
|
||||
@@ -2065,7 +2217,8 @@ export function generateDepotAlerts(normalizedList = [], envData = null) {
|
||||
itemKey: it.key,
|
||||
itemName: it.name,
|
||||
value: it.value,
|
||||
text: `⚠️ [超标警戒] ${item.name} (${vName}): ${it.shortName || it.name} 实测 ${it.value}${it.unit || ''} (警戒限值 ${it.direction === 'lower' ? '≥' : '≤'}${it.alert}${it.unit || ''})`
|
||||
isDemo: report.isDemo,
|
||||
text: `⚠️ [${report.isDemo ? 'DEMO 演示·' : ''}超标警戒] ${item.name} (${vName}): ${it.shortName || it.name} ${report.isDemo ? '演示值' : '实测'} ${it.value}${it.unit || ''} (警戒限值 ${it.direction === 'lower' ? '≥' : '≤'}${it.alert}${it.unit || ''})`
|
||||
});
|
||||
} else if (it.status === 'warn') {
|
||||
alerts.push({
|
||||
@@ -2077,12 +2230,20 @@ export function generateDepotAlerts(normalizedList = [], envData = null) {
|
||||
itemKey: it.key,
|
||||
itemName: it.name,
|
||||
value: it.value,
|
||||
text: `⚠️ [指标预警] ${item.name} (${vName}): ${it.shortName || it.name} 检测 ${it.value}${it.unit || ''} 接近阈值 (标准基线 ${it.direction === 'lower' ? '≥' : '≤'}${it.warn}${it.unit || ''})`
|
||||
isDemo: report.isDemo,
|
||||
text: `⚠️ [${report.isDemo ? 'DEMO 演示·' : ''}指标预警] ${item.name} (${vName}): ${it.shortName || it.name} ${report.isDemo ? '演示值' : '检测'} ${it.value}${it.unit || ''} 接近阈值 (标准基线 ${it.direction === 'lower' ? '≥' : '≤'}${it.warn}${it.unit || ''})`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (pendingQualityCount > 0) {
|
||||
alerts.push({
|
||||
id: 'quality-pending', type: 'quality-pending', level: 'unknown',
|
||||
text: `ℹ️ [化验数据待完善] ${pendingQualityCount}座仓房的化验记录缺失或检测值待核实,请在后台补充。`
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 若全库各项指标无异常
|
||||
if (alerts.length === 0) {
|
||||
alerts.push({
|
||||
@@ -2169,3 +2330,122 @@ export function parseQualityCsv(csvText) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 中储粮《粮油储藏技术规范》GB/T 29890 标准工况模板
|
||||
*/
|
||||
export const GRAIN_TEMPERATURE_PRESETS = {
|
||||
quasi_low: {
|
||||
key: 'quasi_low',
|
||||
name: '准低温储粮推荐工况',
|
||||
tm_i: 18.5,
|
||||
hm_i: 70.0,
|
||||
tm_o: 32.0,
|
||||
hm_o: 55.0,
|
||||
desc: '推荐工况:仓温≤20℃,控温防虫防霉,延缓陈化'
|
||||
},
|
||||
low: {
|
||||
key: 'low',
|
||||
name: '低温储粮标准工况',
|
||||
tm_i: 14.0,
|
||||
hm_i: 65.0,
|
||||
tm_o: 28.0,
|
||||
hm_o: 60.0,
|
||||
desc: '优质工况:仓温≤15℃,绿色抑虫控酶,保持新鲜品质'
|
||||
},
|
||||
normal: {
|
||||
key: 'normal',
|
||||
name: '常温储粮常规工况',
|
||||
tm_i: 22.0,
|
||||
hm_i: 72.0,
|
||||
tm_o: 32.0,
|
||||
hm_o: 55.0,
|
||||
desc: '常规工况:仓温≤25℃,处于安全储粮控温线内'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 依据中储粮储粮控温技术规范研判仓内温湿度状态
|
||||
* @param {number|string} tm_i 仓内温度 (℃)
|
||||
* @param {number|string} hm_i 仓内相对湿度 (%)
|
||||
* @returns {Object} 规范研判结果
|
||||
*/
|
||||
export function evaluateTemperatureStandard(tm_i, hm_i) {
|
||||
const temp = (tm_i !== '' && tm_i !== null && tm_i !== undefined && Number.isFinite(Number(tm_i))) ? Number(tm_i) : null;
|
||||
const hum = (hm_i !== '' && hm_i !== null && hm_i !== undefined && Number.isFinite(Number(hm_i))) ? Number(hm_i) : null;
|
||||
|
||||
if (temp === null) {
|
||||
return {
|
||||
tempGrade: 'unknown',
|
||||
tempLabel: '待设置',
|
||||
tagType: 'info',
|
||||
isHighTemp: false,
|
||||
hint: '请设置仓内温度',
|
||||
humidityGrade: 'unknown',
|
||||
isHighHumidity: false,
|
||||
humidityHint: ''
|
||||
};
|
||||
}
|
||||
|
||||
// 1. 仓温规范研判: 低温(<=15) -> 准低温(<=20) -> 常温(<=25) -> 高温预警(>25)
|
||||
let tempGrade = 'normal';
|
||||
let tempLabel = '常温储粮';
|
||||
let tagType = 'info';
|
||||
let isHighTemp = false;
|
||||
let hint = '符合常规储粮规范(安全受控)';
|
||||
|
||||
if (temp <= 15.0) {
|
||||
tempGrade = 'low';
|
||||
tempLabel = '低温储粮';
|
||||
tagType = 'success';
|
||||
hint = '达到低温储粮标准 (≤15℃,防虫抑霉最优,保鲜保质)';
|
||||
} else if (temp <= 20.0) {
|
||||
tempGrade = 'quasi_low';
|
||||
tempLabel = '准低温储粮';
|
||||
tagType = 'primary';
|
||||
hint = '达到中储粮准低温推荐标准 (≤20℃,绿色控温优选温区)';
|
||||
} else if (temp <= 25.0) {
|
||||
tempGrade = 'normal';
|
||||
tempLabel = '常温储粮';
|
||||
tagType = 'info';
|
||||
hint = '处于常温储粮安全控温范围 (≤25℃)';
|
||||
} else {
|
||||
tempGrade = 'high_warn';
|
||||
tempLabel = '高温预警';
|
||||
tagType = 'danger';
|
||||
isHighTemp = true;
|
||||
hint = '⚠️ 超过25℃中储粮控温安全线(当前' + temp + '℃),高温易引发粮食发热劣变与虫害,请及时开启空调或通风降温!';
|
||||
}
|
||||
|
||||
// 2. 湿度规范研判: 理想(<=70) -> 适中(70~75) -> 高湿预警(>75)
|
||||
let humidityGrade = 'safe';
|
||||
let isHighHumidity = false;
|
||||
let humidityHint = '相对湿度适宜';
|
||||
|
||||
if (hum !== null) {
|
||||
if (hum > 75.0) {
|
||||
humidityGrade = 'high_warn';
|
||||
isHighHumidity = true;
|
||||
humidityHint = '⚠️ 仓内相对湿度达 ' + hum + '%,超过75%高湿警戒线,易引发粮食吸湿结露霉变,请注意排湿!';
|
||||
} else if (hum > 70.0) {
|
||||
humidityGrade = 'moderate';
|
||||
humidityHint = '相对湿度在 70%~75% 适中安全区间';
|
||||
} else {
|
||||
humidityGrade = 'safe';
|
||||
humidityHint = '相对湿度 ≤70% 干燥理想';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tempGrade,
|
||||
tempLabel,
|
||||
tagType,
|
||||
isHighTemp,
|
||||
hint,
|
||||
humidityGrade,
|
||||
isHighHumidity,
|
||||
humidityHint
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户