feat(testing): 增加质检报告独立编辑与详情页面,不再使用弹窗

这个提交包含在:
wikiboss
2026-11-02 14:19:14 +08:00
父节点 6f877be41a
当前提交 43da3cc0ee
共有 38 个文件被更改,包括 2371 次插入 和 301 次删除

查看文件

@@ -1,9 +1,10 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/granary-site'], function ($, undefined, Backend, Table, Form, SiteMixin) {
var Controller = {
index: function () {
var vue = new Vue({
el: '#vue',
mixins: [SiteMixin],
data: {
granaryList: [],
// 表格当前页数据
@@ -22,6 +23,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
listLoading: false,
// 搜索字段
search: {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -61,6 +63,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
// 刷新页面
refreshPage() {
this.search = {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -103,6 +106,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.listLoading = true;
$.getJSON('content/index', {
keyword: key,
site: this.search.site,
type: this.search.type,
start_time: this.search.startTime,
end_time: this.search.endTime,
@@ -263,6 +267,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
add: function () {
let vue = new Vue({
el: '#vue',
mixins: [SiteMixin],
data: {
granaryList: [],
// 字段信息
@@ -341,6 +346,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
this.granaryList = data.granaryList;
const selected = this.granaryList.find(g => g.id === this.formData.gid);
this.formSite = selected ? selected.site : ((this.siteOptions[0] || {}).id || '');
} else {
this.$message.warning(res.msg);
}

查看文件

@@ -0,0 +1,68 @@
define(['jquery', 'bootstrap', 'backend', 'backend/forecast/components'], function ($, undefined, Backend, components) {
'use strict';
return { index: function () {
new Vue({
el: '#forecast-app',
components: {'forecast-list': components.list, 'forecast-create': components.create, 'forecast-detail': components.detail},
data: { rows: [], granaries: [], total: 0, page: 1, gid: '', site: '', siteOptions: Config.granarySites || [], loading: false, saving: false, creating: false, viewing: false, selected: null, loadSequence: 0 },
created: function () { this.load(); },
methods: {
api: function (action, data, method) {
var self = this;
return new Promise(function (resolve, reject) {
$.ajax({url: 'forecast/' + action, type: method || 'GET', data: data || {}, dataType: 'json', timeout: 45000,
success: function (res) {
if (res.code !== 200) { self.$message.error(res.msg || '操作失败,请刷新后重试'); reject(new Error(res.msg)); }
else resolve(res);
},
error: function () { self.$message.error('请求失败或登录已失效,请刷新查看记录后重试'); reject(new Error('request failed')); }
});
});
},
load: function () {
var self = this, sequence = ++this.loadSequence;
this.loading = true;
return this.api('index', {gid: this.gid, site: this.site, page: this.page}).then(function (res) {
if (sequence !== self.loadSequence) return;
self.rows = res.list; self.total = res.total; self.granaries = res.granaries;
}).catch(function () {}).then(function () { if (sequence === self.loadSequence) self.loading = false; });
},
siteChanged: function () { this.gid = ''; this.page = 1; this.load(); },
filterChanged: function () { this.page = 1; this.load(); },
changePage: function (page) { this.page = page; this.load(); },
newRecord: function () { this.creating = true; },
closeCreate: function (done) { if (!this.saving) { this.creating = false; if (typeof done === 'function') done(); } },
closeDetail: function (done) {
var self = this;
if (this.saving) return;
this.$refs.detail.confirmDiscard().then(function () { self.viewing = false; if (typeof done === 'function') done(); }).catch(function () {});
},
openRecord: function (id) {
var self = this;
this.api('detail', {id: id}).then(function (res) { self.selected = res.data; self.viewing = true; }).catch(function () {});
},
write: function (action, data, callback) {
if (this.saving) return;
var self = this;
this.saving = true;
this.api(action, data, 'POST').then(function (res) {
self.selected = res.data;
if (action !== 'actual' && res.data.status === 'failed') self.$message.warning('温湿度已保存,算法暂不可用,可稍后重新生成');
else self.$message.success(action === 'actual' ? '实测值已保存' : res.data.status === 'ready' ? '预测已生成并保存' : '记录已保存');
if (callback) callback();
self.load();
}).catch(function () {}).then(function () { self.saving = false; });
},
createRecord: function (form) {
var self = this;
this.write('add', form, function () { self.creating = false; self.viewing = true; });
},
saveActual: function (form) { this.write('actual', form); },
retry: function (id) {
var self = this;
this.$refs.detail.confirmDiscard().then(function () { self.write('retry', {id: id}); }).catch(function () {});
}
}
});
} };
});

查看文件

@@ -0,0 +1,204 @@
define([], function () {
'use strict';
function today() {
var d = new Date();
return [d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0')].join('-');
}
function number(v) { return v === null || v === undefined || v === '' ? '—' : String(Number(v)); }
function signed(v) { return v === null ? '—' : (v > 0 ? '+' : '') + Number(v); }
function requestKey() {
var bytes = new Uint8Array(16);
window.crypto.getRandomValues(bytes);
return Array.from(bytes, function (n) { return n.toString(16).padStart(2, '0'); }).join('');
}
var dateOptions = { disabledDate: function (date) { return date.getTime() > Date.now() || date.getFullYear() < 2000; } };
/**
* 中储粮《粮油储藏技术规范》GB/T 29890 标准工况模板
*/
var PRESETS = {
quasi_low: {
key: 'quasi_low',
name: '准低温储粮推荐工况',
tag: '准低温 (≤20℃)',
type: 'primary',
desc: '仓温≤20℃,绿色控温优选温区,延缓陈化',
tm_i: 18.5,
hm_i: 70.0,
tm_o: 32.0,
hm_o: 55.0
},
low: {
key: 'low',
name: '低温储粮标准工况',
tag: '低温 (≤15℃)',
type: 'success',
desc: '仓温≤15℃,抑虫抑菌保鲜,高标准储粮',
tm_i: 14.0,
hm_i: 65.0,
tm_o: 28.0,
hm_o: 60.0
},
normal: {
key: 'normal',
name: '常温储粮常规工况',
tag: '常温 (≤25℃)',
type: 'info',
desc: '仓温≤25℃,常规安全储粮受控温区',
tm_i: 22.0,
hm_i: 72.0,
tm_o: 32.0,
hm_o: 55.0
}
};
/**
* 依据中储粮温控标准研判仓内温湿度合规状态
*/
function evaluateTemp(tm_i, hm_i) {
if (tm_i === '' || tm_i === null || tm_i === undefined || !Number.isFinite(Number(tm_i))) {
return { grade: 'unknown', label: '待设置', tagType: 'info', isHigh: false, hint: '请设置仓内温度' };
}
var t = Number(tm_i);
if (t <= 15.0) {
return { grade: 'low', label: '低温储粮', tagType: 'success', isHigh: false, hint: '达到低温储粮标准 (≤15℃,抑虫控酶,保质保鲜最优)' };
}
if (t <= 20.0) {
return { grade: 'quasi_low', label: '准低温储粮', tagType: 'primary', isHigh: false, hint: '达到中储粮准低温推荐标准 (≤20℃,绿色控温优选温区)' };
}
if (t <= 25.0) {
return { grade: 'normal', label: '常温储粮', tagType: 'info', isHigh: false, hint: '处于常温储粮安全控温范围 (≤25℃)' };
}
return { grade: 'high_warn', label: '高温超标预警', tagType: 'danger', isHigh: true, hint: '⚠️ 超过25℃中储粮控温安全线(当前' + t + '℃),高温易致发热霉变与虫害,请及时开启空调或通风降温!' };
}
return {
list: {
template: '#forecast-list-template', props: ['rows', 'loading'],
methods: {
number: number,
status: function (s) { return {pending: '待生成', processing: '生成中', ready: '已生成', failed: '生成失败,可重试'}[s] || s; },
evalTemp: evaluateTemp
}
},
create: {
template: '#forecast-create-template', props: ['granaries', 'busy', 'siteDefaultEnv'],
data: function () { return {
form: {gid: '', observed_date: today(), horizons: [7, 15, 30, 60], tm_i: 18.5, hm_i: 70.0, tm_o: 32.0, hm_o: 55.0, request_key: requestKey()},
dateOptions: dateOptions,
presets: PRESETS,
selectedPresetKey: 'quasi_low',
fields: [
{key: 'tm_i', label: '仓内温度', unit: '℃', min: -50, max: 80, step: 0.1, placeholder: '例:18.5'},
{key: 'hm_i', label: '仓内相对湿度', unit: '%', min: 0, max: 100, step: 0.1, placeholder: '例:70.0'},
{key: 'tm_o', label: '仓外温度', unit: '℃', min: -50, max: 80, step: 0.1, placeholder: '例:32.0'},
{key: 'hm_o', label: '仓外相对湿度', unit: '%', min: 0, max: 100, step: 0.1, placeholder: '例:55.0'}
]
}; },
computed: {
tempStatus: function () {
return evaluateTemp(this.form.tm_i, this.form.hm_i);
},
selectedGranary: function () {
var gid = this.form.gid;
return this.granaries.find(function (g) { return String(g.id) === String(gid); }) || null;
}
},
watch: {
'form.gid': function (newGid) {
if (!newGid) return;
this.onGranaryChange(newGid);
}
},
methods: {
applyPreset: function (key) {
var p = this.presets[key];
if (!p) return;
this.selectedPresetKey = key;
this.form.tm_i = p.tm_i;
this.form.hm_i = p.hm_i;
this.form.tm_o = p.tm_o;
this.form.hm_o = p.hm_o;
this..success('已套用【' + p.name + '】规范温湿度');
},
onGranaryChange: function (gid) {
var granary = this.granaries.find(function (g) { return String(g.id) === String(gid); });
if (!granary) return;
// 若仓库自带最近一次历史环境记录 last_env,则智能推荐带出
if (granary.last_env && granary.last_env.tm_i !== undefined && granary.last_env.tm_i !== '') {
this.form.tm_i = Number(granary.last_env.tm_i);
this.form.hm_i = Number(granary.last_env.hm_i);
this.form.tm_o = Number(granary.last_env.tm_o);
this.form.hm_o = Number(granary.last_env.hm_o);
this.selectedPresetKey = '';
this..info('已自动读取【' + granary.name + '】最近一次温湿度记录');
} else if (this.form.tm_i === '' || this.form.tm_i === undefined) {
this.applyPreset('quasi_low');
}
},
submit: function () {
if (!this.form.gid || !this.form.observed_date || !this.form.horizons.length) return this..warning('请选择仓库、采集日期和预测周期');
for (var i = 0; i < this.fields.length; i++) {
var f = this.fields[i], v = this.form[f.key];
if (v === '' || !Number.isFinite(Number(v)) || Number(v) < f.min || Number(v) > f.max) return this..warning('请填写有效的' + f.label + '(' + f.min + '~' + f.max + ')');
}
this.('submit', JSON.parse(JSON.stringify(this.form)));
}
}
},
detail: {
template: '#forecast-detail-template', props: ['record', 'busy'],
data: function () { return { active: '', actualDate: '', actual: {}, baseline: '', dateOptions: dateOptions }; },
computed: {
point: function () { var id = this.active; return this.record.points.find(function (p) { return String(p.id) === id; }); },
dirty: function () { return this.baseline !== this.snapshot(); },
comparison: function () {
if (!this.point) return [];
var actual = this.actual;
return this.point.rows.map(function (row) {
var value = actual[row.key], val = value === '' || value === null || value === undefined || !Number.isFinite(Number(value)) ? null : Number(value);
var delta = val !== null && row.predicted !== null ? Number((val - row.predicted).toFixed(4)) : null;
return Object.assign({}, row, {delta: delta, percent: delta !== null && row.predicted !== 0 ? Number((delta / Math.abs(row.predicted) * 100).toFixed(2)) : null});
});
}
},
watch: {
active: function () { this.reset(); },
record: { immediate: true, handler: function () {
if (!this.record.points.some(function (p) { return String(p.id) === this.active; }, this)) this.active = String(this.record.points[0].id);
this.reset();
} }
},
methods: {
number: number, signed: signed,
snapshot: function () { return JSON.stringify([this.actualDate, this.actual]); },
reset: function () {
var point = this.point;
this.actualDate = point.actual_date || '';
var values = {};
this.record.metrics.forEach(function (m) { values[m.key] = point.actual[m.key] == null ? '' : String(point.actual[m.key]); });
this.actual = values;
this.baseline = this.snapshot();
},
confirmDiscard: function () {
if (this.busy) return Promise.reject();
return this.dirty ? this.('当前周期有未保存的实测数据,确定放弃修改?', '未保存的数据', {type: 'warning'}) : Promise.resolve();
},
beforeLeave: function () {
return this.confirmDiscard().then(function () { return true; });
},
save: function () {
var any = false;
for (var i = 0; i < this.record.metrics.length; i++) {
var m = this.record.metrics[i], v = this.actual[m.key];
if (v === '') continue;
any = true;
if (!Number.isFinite(Number(v)) || Number(v) < 0 || Number(v) > m.max) return this..warning('请填写有效的' + m.name + '实测值');
}
if (any && !this.actualDate) return this..warning('请选择实际检测日期');
this.('save', {point_id: this.point.id, version: this.point.version, actual_date: this.actualDate, actual: Object.assign({}, this.actual)});
}
}
}
};
});

查看文件

@@ -0,0 +1,23 @@
define([], function () {
'use strict';
return {
data: function () { return { siteOptions: Config.granarySites || [], formSite: '' }; },
computed: {
siteGranaries: function () {
var site = this.formSite;
return (this.granaryList || []).filter(function (g) { return site && g.site === site; });
}
},
methods: {
siteChanged: function () {
this.currentPage = 1;
this.search.gid = '';
this.search.rid = '';
this.multipleSelection = [];
this.loadData(this.search.keyword, 1, this.pageSize, this.search.sort);
},
formSiteChanged: function () { this.formData.gid = ''; },
granaryLabel: function (g) { return g.label || ((g.site === 'xishui' ? '习水库' : '遵义中心库') + ' · ' + g.name); }
}
};
});

查看文件

@@ -1,9 +1,10 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/granary-site'], function ($, undefined, Backend, Table, Form, SiteMixin) {
var Controller = {
index: function () {
var vue = new Vue({
el: '#vue',
mixins: [SiteMixin],
data: {
contentType: 'one',
varietiesType: 'one',
@@ -212,6 +213,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}],
// 搜索字段
search: {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -239,6 +241,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
message: '请您输入粮仓名称',
trigger: 'blur'
}],
site: [{
required: true, message: '请选择所属库区', trigger: 'change'
}],
storage: [{
required: true,
message: '请您输入仓库储存点',
@@ -263,6 +268,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.loadData(this.search.keyword, this.currentPage, this.pageSize, this.search.sort);
},
methods: {
changeSite(site) {
const selected = this.siteOptions.find(s => s.id === site);
this.formData.storage = selected ? selected.storage : '';
this.formData.screenPosition = '';
this.changeGranaryType(site === 'xishui' ? 3 : 2);
},
// 直达中心库大屏
openCenterScreen() {
window.open('/screen/index.html', '_blank');
@@ -274,6 +285,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
// 刷新页面
refreshPage() {
this.search = {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -291,7 +303,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
if (isNaN(num)) return;
this.formData.num = num;
if (this.title === '添加' && this.formData.granary_type !== 3) {
this.formData.granary_type = num > 18 ? 2 : 1;
this.formData.granary_type = this.formData.site === 'xishui' ? (num <= 8 ? 3 : 2) : (num > 18 ? 2 : 1);
this.changeGranaryType(this.formData.granary_type);
}
},
@@ -397,15 +409,15 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
id: 0,
name: '',
image: '',
storage: '成都直属库有限公司本部',
site: this.search.site || (this.siteOptions[0] || {}).id || '',
storage: '',
num: '',
screenPosition: '',
granary_type: 1,
varieties: '菜籽油',
content: []
};
this.contentType = 'one';
this.varietiesType = 'one';
this.changeSite(this.formData.site);
} else {
this.title = '编辑';
let isPreset = ['菜籽油', '大豆油', '稻谷', '小麦', '玉米'].indexOf(item.varieties) !== -1;
@@ -416,6 +428,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
id: item.id,
name: item.name,
image: item.image,
site: item.site,
storage: item.storage,
num: item.num,
screenPosition: (item.content || {}).screenPosition || '',
@@ -433,7 +446,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.contentType = 'two';
}
if (this.formData.granary_type == 1) {
if (Number(this.formData.granary_type) === 1) {
this.varietiesType = 'one';
} else {
this.varietiesType = 'two';
@@ -463,6 +476,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.listLoading = true;
$.getJSON('granary/index', {
keyword: key,
site: this.search.site,
type: this.search.type,
start_time: this.search.startTime,
end_time: this.search.endTime,

查看文件

@@ -1,9 +1,10 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/granary-site'], function ($, undefined, Backend, Table, Form, SiteMixin) {
var Controller = {
index: function () {
let vue = new Vue({
el: '#vue',
mixins: [SiteMixin],
data: {
typeName: '',
detail: {
@@ -62,6 +63,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
listLoading: false,
// 搜索字段
search: {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -115,9 +117,13 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.loadData(this.search.keyword, this.currentPage, this.pageSize, this.search.sort);
},
methods: {
isRuleNumber(value) {
return (typeof value === 'string' || typeof value === 'number')
&& /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(String(value).trim())
&& Number.isFinite(Number(value));
},
isNumericRule(item) {
return [item.warning, item.alert].every(value =>
value !== null && value !== undefined && String(value).trim() !== '' && Number.isFinite(Number(value)));
return [item.warning, item.alert].every(value => this.isRuleNumber(value));
},
ruleDirection(item) {
return item.direction === 'upper' || item.direction === 'lower' ? item.direction
@@ -140,7 +146,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
: [`当前值 ≥ ${w}`, `${a} ≤ 当前值 < ${w}`, `当前值 < ${a}`];
},
ruleError(item) {
const expectsNumeric = item.rule_numeric === true || Number(item.is_number) === 1
|| this.isRuleNumber(item.warning) || this.isRuleNumber(item.alert);
if (expectsNumeric && !this.isNumericRule(item)) return '预警值和警告值必须都是有限数字';
if (!this.isNumericRule(item)) return '';
if (!this.isRuleNumber(item.val)
&& !/^(?:\/|--?|未检测|未提供|暂无|未检出|未检出\s*ND|ND)$/i.test(String(item.val ?? '').trim())) {
return '检测值必须是有限数字,缺测可填 /';
}
const upper = this.ruleDirection(item) === 'upper';
return (upper && Number(item.warning) > Number(item.alert)) || (!upper && Number(item.warning) < Number(item.alert))
? (upper ? '越小越好指标要求:预警值 ≤ 警告值(高值预警)' : '越大越好指标要求:预警值 ≥ 警告值(低值预警)') : '';
@@ -148,6 +161,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
// 刷新页面
refreshPage() {
this.search = {
site: '',
startTime: '',
endTime: '',
keyword: '',
@@ -164,15 +178,37 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
let list = this.granaryList;
let item = list.find(v => v.id === e);
if (!item) return;
this.typeName = item.varieties;
this.formData.type = item.type;
},
handelIsNumber(index, val) {
this.typeList[this.formData.type]['content'][index].is_number = val;
},
openPage(url, title) {
if (typeof top !== 'undefined' && top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title);
} else if (typeof Backend !== 'undefined' && Backend && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title);
} else if (typeof window !== 'undefined' && window.location) {
window.location.href = url;
}
},
openAddPage() {
this.openPage('testing/add', '添加检测报告');
},
openEditPage(item) {
var id = typeof item === 'object' ? item.id : item;
var name = (item && item.granary && item.granary.name) ? ' - ' + item.granary.name : '';
this.openPage('testing/edit?ids=' + id, '编辑检测报告' + name);
},
openDetailPage(item) {
var id = typeof item === 'object' ? item.id : item;
var name = (item && item.granary && item.granary.name) ? ' - ' + item.granary.name : '';
this.openPage('testing/detail?ids=' + id, '检测报告详情' + name);
},
openDialogDetail(detail) {
this.detail = detail;
this.detailDialog = true;
this.openDetailPage(detail);
},
handleClearSelect(field) {
// 重新加载
@@ -261,13 +297,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
})
});
},
// 添加/编辑
// 添加/编辑(兼容原有状态准备)
openAddDialog(type, item) {
this.addDialog = true;
this.typeList = JSON.parse(JSON.stringify(this.newTypeList));
if (type === 'add') {
this.title = '添加';
this.formSite = this.search.site || (this.siteOptions[0] || {}).id || '';
this.formData = {
id: 0,
gid: '',
@@ -279,6 +316,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
};
} else {
this.title = '编辑';
this.formSite = item.granary.site;
this.formData = {
id: item.id,
gid: item.gid,
@@ -292,6 +330,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.typeName = item.granary.varieties;
this.typeList[item.type]['content'] = JSON.parse(JSON.stringify(item.testing_params));
}
for (const group of Object.values(this.typeList)) {
for (const row of group.content || []) {
this.$set(row, 'rule_numeric', this.isNumericRule(row));
}
}
},
handleSort(item) {
// 提交
@@ -325,6 +368,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
this.listLoading = true;
$.getJSON('testing/index', {
keyword: key,
site: this.search.site,
type: this.search.type,
gid: this.search.gid,
date: this.search.date,

查看文件

@@ -0,0 +1,44 @@
.quality-badge.status-unknown {
background: rgba(148, 163, 184, .16);
color: #cbd5e1;
border: 1px solid rgba(148, 163, 184, .4);
}
.val-unknown { color: #94a3b8; text-shadow: none; }
.quality-strip-card.strip-unknown, .quality-card.card-unknown {
border-color: rgba(148, 163, 184, .35);
box-shadow: none;
}
.gauge-unknown { background: #64748b; box-shadow: none; }
.quality-empty-state {
min-height: 240px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
padding: 24px 18px;
text-align: center;
color: #94a3b8;
box-sizing: border-box;
}
.quality-empty-state h3 { margin: 0; color: #cbd5e1; font-size: 14px; font-weight: 500; }
.quality-empty-state p { margin: 0; max-width: 280px; font-size: 12px; line-height: 1.9; }
.alert-item-pill.unknown { color: #cbd5e1; border-color: rgba(148, 163, 184, .4); background: rgba(148, 163, 184, .1); }
.alert-item-pill.unknown .pill-dot { background: #94a3b8; box-shadow: none; }
/* 状态颜色统一:数据状态与品种/系列配色分别表达。 */
.quality-badge.status-normal, .quality-card-value.val-normal,
.strip-main-val.val-normal, .alert-item-pill.normal { color: var(--status-normal); }
.quality-badge.status-warn, .quality-card-value.val-warn,
.strip-main-val.val-warn, .alert-item-pill.warn, .strip-warn .strip-limit-desc { color: var(--status-warn); }
.quality-badge.status-danger, .quality-card-value.val-danger,
.strip-main-val.val-danger, .alert-item-pill.danger, .strip-danger .strip-limit-desc { color: var(--status-danger); }
.quality-badge.status-unknown, .quality-card-value.val-unknown,
.strip-main-val.val-unknown, .alert-item-pill.unknown { color: var(--status-unknown); }
.strip-gauge-fill.gauge-normal { background: var(--status-normal); }
.strip-gauge-fill.gauge-warn { background: var(--status-warn); }
.strip-gauge-fill.gauge-danger { background: var(--status-danger); }
.strip-gauge-fill.gauge-unknown { background: var(--status-unknown); }
.alert-item-pill .pill-dot { background: currentColor; }

查看文件

@@ -26,6 +26,7 @@
--grain-oil: #f59e0b; /* 菜籽油脂琥珀金 */
--status-normal: #10b981; /* 运行正常/达标 */
--status-warn: #f59e0b; /* 关注/轻度预警 */
--status-unknown: #94a3b8; /* 缺失/待核实 */
--status-danger: #ef4444; /* 报警/异常 */
/* 文本阶梯色彩 Tokens */
@@ -373,7 +374,7 @@ html, body {
.alert-item-pill.danger {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.45);
color: #fca5a5;
color: var(--status-danger);
}
.alert-item-pill.danger:hover {
@@ -384,7 +385,7 @@ html, body {
.alert-item-pill.warn {
background: rgba(245, 158, 11, 0.2);
border: 1px solid rgba(245, 158, 11, 0.45);
color: #fde68a;
color: var(--status-warn);
}
.alert-item-pill.warn:hover {
@@ -395,7 +396,7 @@ html, body {
.alert-item-pill.normal {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.35);
color: #6ee7b7;
color: var(--status-normal);
}
/* ================= 顶部 KPI 栏 (高精密态势数据舱) ================= */
@@ -737,19 +738,19 @@ html, body {
.quality-badge.status-normal {
background: rgba(16, 185, 129, 0.18);
color: #34d399;
color: var(--status-normal);
border: 1px solid rgba(16, 185, 129, 0.35);
}
.quality-badge.status-warn {
background: rgba(245, 158, 11, 0.2);
color: #fbbf24;
color: var(--status-warn);
border: 1px solid rgba(245, 158, 11, 0.45);
}
.quality-badge.status-danger {
background: rgba(239, 68, 68, 0.25);
color: #f87171;
color: var(--status-danger);
border: 1px solid rgba(239, 68, 68, 0.6);
font-weight: 700;
animation: badge-blink 1.8s infinite ease-in-out;
@@ -779,15 +780,15 @@ html, body {
}
.quality-card-value.val-normal {
color: #34d399;
color: var(--status-normal);
}
.quality-card-value.val-warn {
color: #fbbf24;
color: var(--status-warn);
}
.quality-card-value.val-danger {
color: #f87171;
color: var(--status-danger);
text-shadow: 0 0 6px rgba(239, 68, 68, 0.5);
}
@@ -919,7 +920,7 @@ html, body {
}
50% {
box-shadow: 0 0 18px rgba(239, 68, 68, 0.7), inset 0 0 16px rgba(239, 68, 68, 0.25);
border-color: #f87171;
border-color: var(--status-danger);
}
}
@@ -970,17 +971,17 @@ html, body {
}
.strip-main-val.val-normal {
color: #34d399;
color: var(--status-normal);
text-shadow: 0 0 6px rgba(52, 211, 153, 0.3);
}
.strip-main-val.val-warn {
color: #fbbf24;
color: var(--status-warn);
text-shadow: 0 0 8px rgba(251, 191, 36, 0.4);
}
.strip-main-val.val-danger {
color: #f87171;
color: var(--status-danger);
text-shadow: 0 0 10px rgba(248, 113, 113, 0.6);
}
@@ -1013,15 +1014,15 @@ html, body {
}
.strip-gauge-fill.gauge-normal {
background: linear-gradient(90deg, #10b981, #34d399);
background: linear-gradient(90deg, #10b981, var(--status-normal));
}
.strip-gauge-fill.gauge-warn {
background: linear-gradient(90deg, #f59e0b, #fbbf24);
background: linear-gradient(90deg, #f59e0b, var(--status-warn));
}
.strip-gauge-fill.gauge-danger {
background: linear-gradient(90deg, #ef4444, #f87171);
background: linear-gradient(90deg, #ef4444, var(--status-danger));
}
/* 底部:限值说明文字 (保证完整露出,杜绝遮挡) */
@@ -1045,12 +1046,12 @@ html, body {
}
.strip-danger .strip-limit-desc {
color: #fca5a5;
color: var(--status-danger);
font-weight: 600;
}
.strip-warn .strip-limit-desc {
color: #fde68a;
color: var(--status-warn);
}
/* ================== 食品卫生安全绿盾微矩阵 (4 列 x 2 行) ================== */
@@ -1078,7 +1079,7 @@ html, body {
.svg-shield-icon {
width: 13px;
height: 13px;
color: #34d399;
color: var(--status-normal);
flex-shrink: 0;
margin-right: 4px;
}
@@ -1088,7 +1089,7 @@ html, body {
align-items: center;
font-size: 10.5px;
font-weight: 600;
color: #34d399;
color: var(--status-normal);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -1098,7 +1099,7 @@ html, body {
.shield-badge {
font-size: 9px;
background: rgba(16, 185, 129, 0.2);
color: #34d399;
color: var(--status-normal);
border: 1px solid rgba(16, 185, 129, 0.4);
border-radius: 3px;
padding: 0 4px;
@@ -1152,7 +1153,7 @@ html, body {
.shield-item-val {
font-size: 10px;
font-weight: 700;
color: #34d399;
color: var(--status-normal);
font-family: var(--font-number);
white-space: nowrap;
overflow: hidden;
@@ -1164,12 +1165,12 @@ html, body {
}
.shield-alert .shield-item-val {
color: #f87171;
color: var(--status-danger);
}
.shield-item-status {
font-size: 8px;
color: #34d399;
color: var(--status-normal);
white-space: nowrap;
line-height: 1;
margin-top: 1px;
@@ -1178,7 +1179,7 @@ html, body {
}
.shield-alert .shield-item-status {
color: #f87171;
color: var(--status-danger);
}
/* ==========================================================================
@@ -1258,7 +1259,7 @@ html, body {
.center-chart-close-btn:hover {
background: rgba(239, 68, 68, 0.2);
border-color: rgba(239, 68, 68, 0.5);
color: #f87171;
color: var(--status-danger);
}
/* 方案 A:大盘模式切换胶囊 */
@@ -1561,7 +1562,7 @@ html, body {
#btn-chart-forecast:hover {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.45) 0%, rgba(6, 182, 212, 0.42) 100%) !important;
border-color: #34d399 !important;
border-color: var(--status-normal) !important;
box-shadow: 0 0 16px rgba(52, 211, 153, 0.55);
}
@@ -1812,7 +1813,7 @@ html, body {
font-size: 8.5px;
padding: 1px 2.5px;
background: rgba(245, 158, 11, 0.25);
color: #fbbf24;
color: var(--status-warn);
}
.hotspot-pin.oil .bubble-arrow {
@@ -1840,19 +1841,19 @@ html, body {
width: 5px;
height: 5px;
border-radius: 50%;
background: #fbbf24;
box-shadow: 0 0 6px #fbbf24, 0 0 10px rgba(245, 158, 11, 0.8);
background: var(--status-warn);
box-shadow: 0 0 6px var(--status-warn), 0 0 10px rgba(245, 158, 11, 0.8);
}
.hotspot-pin.active .bubble-stem {
width: 2px;
background: linear-gradient(180deg, #fbbf24 0%, #ffffff 100%);
box-shadow: 0 0 10px #fbbf24;
background: linear-gradient(180deg, var(--status-warn) 0%, #ffffff 100%);
box-shadow: 0 0 10px var(--status-warn);
}
.hotspot-pin.active .stem-dot {
background: #ffffff;
box-shadow: 0 0 10px #ffffff, 0 0 16px #fbbf24;
box-shadow: 0 0 10px #ffffff, 0 0 16px var(--status-warn);
}
/* 激活状态高光与聚焦光晕 */
@@ -1877,13 +1878,13 @@ html, body {
}
.hotspot-pin.oil.active .bubble-inner {
background: linear-gradient(135deg, #d97706 0%, #fbbf24 100%);
background: linear-gradient(135deg, #d97706 0%, var(--status-warn) 100%);
border-color: #ffffff;
box-shadow: 0 0 20px rgba(245, 158, 11, 0.95), 0 4px 16px rgba(0, 0, 0, 0.8);
}
.hotspot-pin.oil.active .bubble-arrow {
border-top-color: #fbbf24;
border-top-color: var(--status-warn);
}
.hotspot-pin.paddy.active .bubble-inner {
@@ -2157,7 +2158,8 @@ html, body {
.forecast-charts-row .center-chart-card-title { color: #e3edf7; font-size: 13px; margin-bottom: 0; letter-spacing: .2px; }
.forecast-charts-row .center-chart-card:last-child { padding-left: 18px; border-left-color: rgba(148, 173, 200, .13); }
.forecast-chart-note { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px; color: #8ba4bd; font-size: 10px; line-height: 1.7; padding: 0 0 12px; }
.threshold-key { color: #dfb883; }
.threshold-key { color: var(--status-danger); }
.warning-key { color: var(--status-warn); }
.forecast-env-pill { color: #9eb5cb; border-color: #314459; background: #132438; font-size: 9px; }
.forecast-env-bar { border-color: #2a4058; box-shadow: none; padding: 8px 10px; flex-wrap: wrap; }
.forecast-env-items { flex-wrap: wrap; gap: 7px; }

查看文件

@@ -4,7 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>中储粮(遵义)智慧粮仓综合监测中心</title>
<link rel="stylesheet" href="./css/style.css?v=20261028_chart3">
<link rel="stylesheet" href="./css/style.css?v=20261028_status2">
<link rel="stylesheet" href="./css/quality-status.css?v=20261028_xishui1">
<!-- ECharts 可视化库 (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
</head>
@@ -343,7 +344,7 @@
<div class="center-chart-card-title" id="forecast-trend-title">多周期趋势</div>
<div class="forecast-env-pill" id="forecast-trend-env-pill">工况: 仓内 18.5℃ / 72.4% · 仓外 32.5℃ / 54.2%</div>
</div>
<div class="forecast-chart-note"><span>各指标独立刻度 · 保留原始单位</span><span>● 实测起点 ┈ 预测趋势 <span class="threshold-key">┄ 警戒参考</span></span></div>
<div class="forecast-chart-note"><span>各指标独立刻度 · 数值为实测 → 末期预测</span><span>● 实测起点 ┈ 预测趋势 <span class="warning-key">┄ 预警</span> <span class="threshold-key">┄ 警戒</span></span></div>
<div id="chart-forecast-trend" class="center-chart-canvas"></div>
</div>
<!-- 右侧:现状实测 vs 预测推演偏离对比柱状图 -->
@@ -417,6 +418,6 @@
</div>
<script type="module" src="./js/app.js?v=20261028_chart3"></script>
<script type="module" src="./js/app.js?v=20261028_demo3"></script>
</body>
</html>

查看文件

@@ -1,3 +1,4 @@
import { fetchSiteData, showSiteDataState } from './site_data.js?v=20261028_demo3';
import {
calcScreenScale,
normalizeGranaryList,
@@ -21,7 +22,7 @@ import {
resolveHotspotCoord,
generateDepotAlerts,
syncAlertMarqueeSpeed
} from './grain_monitor_core.js?v=20261028_chart3';
} from './grain_monitor_core.js?v=20261028_demo3';
// 全局状态
const STATE = {
@@ -92,28 +93,18 @@ function updateClock() {
// 3. 加载数据
async function fetchGranaryData() {
const endpoints = ['/api/index/granaryList', 'https://grain.gynhc.com/api/index/granaryList'];
for (const apiUrl of endpoints) {
try {
const res = await fetch(apiUrl, { cache: 'no-cache', signal: AbortSignal.timeout(2000) });
if (!res.ok) continue;
const json = await res.json();
if (json && json.data && Array.isArray(json.data.list) && json.data.list.length > 0) {
const list = json.data.list;
STATE.list = normalizeGranaryList({ ...json, data: { ...json.data, list } });
if (json.data && json.data.coords) {
STATE.customCoords = json.data.coords;
}
console.log(`✓ 成功从 [${apiUrl}] 在线加载`, STATE.list.length, '个仓房/油罐数据');
return;
}
} catch (err) {
// 当前端点连接失败,自动尝试下一端点
}
try {
const data = await fetchSiteData('center');
STATE.list = normalizeGranaryList({ data });
STATE.customCoords = data.coords || {};
STATE.activeId = STATE.list.some(item => item.id === STATE.activeId) ? STATE.activeId : (STATE.list[0]?.id || null);
showSiteDataState('center', STATE.list.length ? 'live' : 'empty', STATE.list.length, STATE.list.some(item => item.isDemo));
} catch (_) {
STATE.list = [];
STATE.customCoords = {};
STATE.activeId = null;
showSiteDataState('center', 'unavailable', 0);
}
console.warn('在线与本地 API 均不可用,使用内置官方完整数据集');
STATE.list = normalizeGranaryList(DEFAULT_GRANARY_DATA);
}
// 3.1 加载微气候环境工况
@@ -192,7 +183,7 @@ function renderKPIs() {
document.getElementById('kpi-oil-count').textContent = `${s.oilTankCount} 座`;
const filterLabels = {
all: `全部 (${STATE.list.length})`,
warehouse: `平房仓群 (${s.warehouseCount})`,
warehouse: `粮仓群 (${s.warehouseCount + s.siloCount})`,
oilTank: `储油罐区 (${s.oilTankCount})`
};
document.querySelectorAll('.filter-btn[data-filter]').forEach(button => {
@@ -444,8 +435,8 @@ function renderLeftPanel(item, params) {
document.getElementById('param-granary-type').textContent = b.warehouseType;
document.getElementById('param-granary-variety').textContent = b.varieties;
document.getElementById('param-granary-nature').textContent = b.natureName;
document.getElementById('param-granary-addr').textContent = b.addr || b.productArea || '安徽/河南';
document.getElementById('param-granary-year').textContent = b.harvestYear || b.machiningYear || '2024';
document.getElementById('param-granary-addr').textContent = item.isOilTank ? b.productArea : b.addr;
document.getElementById('param-granary-year').textContent = item.isOilTank ? b.machiningYear : b.harvestYear;
document.getElementById('param-granary-enter').textContent = b.enterTime || '2024-08';
document.getElementById('param-granary-custodian').textContent = b.custodian || '肖胜利';
document.getElementById('param-granary-mgmt').textContent = b.managementForm || '直接管理';
@@ -482,6 +473,12 @@ function renderLeftPanel(item, params) {
<span class="param-value">立式储油罐密闭</span>
</div>
`;
} else if (params.type === 'grainSilo') {
sizeGrid.innerHTML = `<div class="param-item"><span class="param-label">筒仓直径</span><span class="param-value highlight">${s.diameter}</span></div>
<div class="param-item"><span class="param-label">装粮线高</span><span class="param-value highlight">${s.height}</span></div>
<div class="param-item"><span class="param-label">粮堆体积</span><span class="param-value">${s.volume}</span></div>
<div class="param-item"><span class="param-label">扣除体积</span><span class="param-value">${s.decVolume}</span></div>
<div class="param-item"><span class="param-label">储粮方式</span><span class="param-value">${b.storageMethod}</span></div>`;
} else {
sizeGrid.innerHTML = `
<div class="param-item">
@@ -537,6 +534,12 @@ function renderRightPanel(item, params) {
const report = params.qualityReport;
if (!report) return;
if (report.empty) {
if (qualityTitle) qualityTitle.textContent = `${report.variety}品质全参数舱 · 未录入化验记录`;
if (qualityExtra) qualityExtra.textContent = '暂无化验数据';
qualityGrid.innerHTML = buildQualityHudHtml(report);
return;
}
// 标题与总体状态指示徽章 (短小精悍,杜绝截断)
if (qualityTitle) {
@@ -545,9 +548,12 @@ function renderRightPanel(item, params) {
statusBadge = `<span class="quality-badge status-danger" style="margin-left: 5px;">⚠️ ${report.summary.dangerCount}项超标</span>`;
} else if (report.summary.warnCount > 0) {
statusBadge = `<span class="quality-badge status-warn" style="margin-left: 5px;">⚠️ ${report.summary.warnCount}项预警</span>`;
} else {
} else if (!report.summary.unknownCount) {
statusBadge = `<span class="quality-badge status-normal" style="margin-left: 5px;">✓ 健康达标</span>`;
}
if (report.summary.unknownCount > 0) {
statusBadge += `<span class="quality-badge status-unknown" style="margin-left: 5px;">${report.summary.unknownCount}项待核实</span>`;
}
qualityTitle.innerHTML = `<span>${report.variety}品质全参数舱</span>${statusBadge}`;
}
@@ -646,7 +652,7 @@ async function initOrUpdateForecastCharts(granaryItem) {
setupForecastEnvControls();
const currentReport = getGranaryQualityReport(grainName, currentItem.rawContent || currentItem.content || {}, currentItem.qualityTest);
const currentReport = getGranaryQualityReport(grainName, currentItem.rawContent || currentItem.content || {}, currentItem.qualityTest, { requireRecordedData: currentItem.dataSource === 'database' });
// 1. 0 毫秒即时秒出基础图表 (杜绝任何白屏与残亮点)
const baselineList = [
@@ -687,6 +693,9 @@ async function initOrUpdateForecastCharts(granaryItem) {
key: m.key,
name: m.name,
unit: m.unit,
warn: m.warn,
alert: m.alert,
direction: m.direction,
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
}));
const initialCompareOption = buildForecastCompareChartOption(grainType, currentReport?.items || [], initialFuture30);
@@ -723,6 +732,9 @@ async function initOrUpdateForecastCharts(granaryItem) {
key: m.key,
name: m.name,
unit: m.unit,
warn: m.warn,
alert: m.alert,
direction: m.direction,
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
}));
@@ -886,7 +898,7 @@ function initOrUpdateGranaryBar() {
const names = STATE.list.map(i => i.name.replace('号仓房', '#').replace('号油罐', 'T#'));
const storageData = STATE.list.map(i => i.storageNum);
const capacityData = STATE.list.map(i => i.capacity);
const capacityData = STATE.list.map(i => i.capacityNum);
const option = {
tooltip: {
@@ -1139,6 +1151,7 @@ window.addEventListener('DOMContentLoaded', async () => {
}, 60000);
setupAutoPlay();
if (urlParams.get('view') === 'forecast') selectGranary(STATE.activeId, true);
const fsBtn = document.getElementById('btn-fullscreen');
if (fsBtn) fsBtn.addEventListener('click', toggleFullscreen);
@@ -1154,6 +1167,11 @@ window.addEventListener('DOMContentLoaded', async () => {
});
window.addEventListener('keydown', (e) => {
if (e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
if (e.key === 'Escape' && STATE.isCenterChartOpen) {
toggleCenterChartBoard(STATE.centerChartTab, false);
return;
}
const curIdx = STATE.list.findIndex(w => w.id === STATE.activeId);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
const nextIdx = (curIdx - 1 + STATE.list.length) % STATE.list.length;

查看文件

@@ -0,0 +1,46 @@
export function validateSitePayload(payload, site) {
const data = payload?.data;
if (!data || data.site !== site || !Array.isArray(data.list)) throw new Error('库区数据不匹配');
if (data.list.some(row => row.site !== site)) throw new Error('仓库归属不匹配');
return data;
}
export async function fetchSiteData(site, fetcher = fetch) {
const path = `/api/index/granaryList?site=${encodeURIComponent(site)}`;
const endpoints = [path, `https://grain.gynhc.com${path}`];
for (const url of endpoints) {
try {
const response = await fetcher(url, { cache: 'no-store', signal: AbortSignal.timeout(8000) });
if (!response.ok) continue;
return validateSitePayload(await response.json(), site);
} catch (_) { /* Try the configured service if this origin is unavailable. */ }
}
throw new Error('暂时无法读取库区数据');
}
export function showSiteDataState(site, state, count, hasDemo = false) {
const wrapper = document.getElementById('screen-wrapper');
if (!wrapper) return;
let badge = document.getElementById('site-data-state');
if (!badge) {
badge = document.createElement('div');
badge.id = 'site-data-state';
badge.setAttribute('role', 'status');
badge.style.cssText = 'position:absolute;top:83px;left:50%;transform:translateX(-50%);z-index:100;padding:5px 14px;border:1px solid #35536a;border-radius:4px;background:#0b1828;color:#bcd4e8;font-size:13px;pointer-events:none;white-space:nowrap';
wrapper.appendChild(badge);
}
const name = site === 'xishui' ? '习水库' : '遵义中心库';
badge.dataset.state = state;
badge.hidden = state === 'live';
badge.textContent = state === 'live' ? `${name} · 后台数据 · ${count} 座仓储设施${hasDemo ? ' · 含 DEMO 演示化验,非实测' : ''}`
: state === 'empty' ? `${name} · 暂无仓库资料` : `${name} · 数据连接失败,请稍后重试`;
let empty = document.getElementById('site-empty-state');
if (!empty) {
empty = document.createElement('div');
empty.id = 'site-empty-state';
empty.style.cssText = 'position:absolute;inset:120px 0 50px;z-index:99;display:none;align-items:center;justify-content:center;background:rgba(6,16,29,.97);color:#bcd4e8;font-size:24px';
wrapper.appendChild(empty);
}
empty.style.display = state === 'live' ? 'none' : 'flex';
empty.textContent = state === 'empty' ? '当前库区暂无仓库资料,请在后台录入' : '暂时无法读取库区数据,请检查网络后刷新';
}

查看文件

@@ -1,3 +1,4 @@
import { fetchSiteData, showSiteDataState } from './site_data.js?v=20261028_demo3';
import {
calcScreenScale,
normalizeGranaryList,
@@ -17,7 +18,7 @@ import {
getGranaryQualityReport,
generateDepotAlerts,
syncAlertMarqueeSpeed
} from './grain_monitor_core.js?v=20261028_chart3';
} from './grain_monitor_core.js?v=20261028_demo3';
// 全局状态
const STATE = {
@@ -86,37 +87,18 @@ function updateClock() {
}
// 3. 加载习水子库标准数据集
function loadGranaryData() {
STATE.list = normalizeGranaryList(XISHUI_GRANARY_DATA);
// 尝试连接后台接口同步最新配置
if (typeof fetch !== 'undefined') {
const urls = [
'/api/index/granaryList?site=xishui',
'https://grain.gynhc.com/api/index/granaryList?site=xishui',
'/api/granary/list?site=xishui'
];
(async () => {
for (const u of urls) {
try {
const res = await fetch(u, { cache: 'no-cache', signal: AbortSignal.timeout(2000) });
if (!res.ok) continue;
const json = await res.json();
if (json && json.data && Array.isArray(json.data.list) && json.data.list.length > 0) {
STATE.list = normalizeGranaryList(json);
if (json.data.coords) {
STATE.customCoords = json.data.coords;
}
renderKPIs();
renderHotspots();
renderMiniGrid();
renderDetailPanels();
renderDepotAlerts();
return;
}
} catch (e) {}
}
})();
async function loadGranaryData() {
try {
const data = await fetchSiteData('xishui');
STATE.list = normalizeGranaryList({ data });
STATE.customCoords = data.coords || {};
STATE.activeId = STATE.list.some(item => item.id === STATE.activeId) ? STATE.activeId : (STATE.list[0]?.id || null);
showSiteDataState('xishui', STATE.list.length ? 'live' : 'empty', STATE.list.length, STATE.list.some(item => item.isDemo));
} catch (_) {
STATE.list = [];
STATE.customCoords = {};
STATE.activeId = null;
showSiteDataState('xishui', 'unavailable', 0);
}
}
@@ -395,8 +377,8 @@ function renderLeftPanel(item, params) {
document.getElementById('param-granary-type').textContent = b.warehouseType;
document.getElementById('param-granary-variety').textContent = b.varieties;
document.getElementById('param-granary-nature').textContent = b.natureName;
document.getElementById('param-granary-addr').textContent = b.addr || '河南/湖北';
document.getElementById('param-granary-year').textContent = b.harvestYear || '2023';
document.getElementById('param-granary-addr').textContent = item.isOilTank ? b.productArea : b.addr;
document.getElementById('param-granary-year').textContent = item.isOilTank ? b.machiningYear : b.harvestYear;
document.getElementById('param-granary-enter').textContent = b.enterTime || '2023-08';
document.getElementById('param-granary-custodian').textContent = b.custodian || '张卫国';
document.getElementById('param-granary-mgmt').textContent = b.managementForm || '直接管理';
@@ -500,20 +482,26 @@ function renderRightPanel(item, params) {
// 标题与总体状态指示徽章 (短小精悍,杜绝截断)
if (qualityTitle) {
let statusBadge = '';
if (report.summary.dangerCount > 0) {
if (report.empty) {
statusBadge = '<span class="quality-badge status-unknown" style="margin-left: 5px;">暂无数据</span>';
} else if (report.summary.dangerCount > 0) {
statusBadge = `<span class="quality-badge status-danger" style="margin-left: 5px;">⚠️ ${report.summary.dangerCount}项超标</span>`;
} else if (report.summary.warnCount > 0) {
statusBadge = `<span class="quality-badge status-warn" style="margin-left: 5px;">⚠️ ${report.summary.warnCount}项预警</span>`;
} else {
} else if (!report.summary.unknownCount) {
statusBadge = `<span class="quality-badge status-normal" style="margin-left: 5px;">✓ 健康达标</span>`;
}
if (report.summary.unknownCount > 0) {
statusBadge += `<span class="quality-badge status-unknown" style="margin-left: 5px;">${report.summary.unknownCount}项待核实</span>`;
}
qualityTitle.innerHTML = `<span>${report.variety}品质全参数舱</span>${statusBadge}`;
}
// 标题栏右侧显示批次阶段与检验日期 (精炼格式)
if (qualityExtra) {
const stageShort = (report.inspectStage || '').split('/')[0].trim();
qualityExtra.innerHTML = `<span style="font-size: 9px; color: var(--text-sub); white-space: nowrap;">${stageShort} · ${report.inspectDate}</span>`;
if (report.empty) qualityExtra.textContent = stageShort;
if (!report.empty) qualityExtra.innerHTML = `<span style="font-size: 9px; color: var(--text-sub); white-space: nowrap;">${stageShort} · ${report.inspectDate}</span>`;
}
// 渲染方案A专属品质全参数舱 (全量水平条形卡片 + 分类Tab)
@@ -560,6 +548,27 @@ async function initOrUpdateForecastCharts(granaryItem) {
STATE.charts.forecastCompare = echarts.init(compareDom);
}
const currentReport = getGranaryQualityReport(grainName, currentItem.rawContent || currentItem.content || {}, currentItem.qualityTest, { requireRecordedData: true });
for (const id of ['btn-forecast-recalc', 'btn-forecast-reset']) {
const button = document.getElementById(id);
if (button) {
button.disabled = currentReport.empty;
button.title = currentReport.empty ? '请先录入本仓化验数据' : '';
}
}
if (currentReport.empty) {
const emptyOption = {
title: { text: '暂无化验数据\n录入本仓实测数据后可查看质量预测', left: 'center', top: '40%',
textStyle: { color: '#94a3b8', fontSize: 13, lineHeight: 24 } }
};
STATE.charts.forecastTrend.setOption(emptyOption, true);
STATE.charts.forecastCompare.setOption(emptyOption, true);
if (trendTitleEl) trendTitleEl.textContent = `${currentItem.name} · 暂无实测数据`;
if (compareTitleEl) compareTitleEl.textContent = '待录入化验记录';
delete STATE.forecastCache[currentItem.id];
return;
}
if (!grainType) {
const emptyOption = {
title: {
@@ -607,7 +616,7 @@ async function initOrUpdateForecastCharts(granaryItem) {
setupForecastEnvControls();
const currentReport = getGranaryQualityReport(grainName, currentItem.rawContent || currentItem.content || {}, currentItem.qualityTest);
// 1. 0 毫秒即时秒出基础图表 (杜绝任何白屏与残亮点)
const baselineList = [
@@ -648,6 +657,9 @@ async function initOrUpdateForecastCharts(granaryItem) {
key: m.key,
name: m.name,
unit: m.unit,
warn: m.warn,
alert: m.alert,
direction: m.direction,
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
}));
const initialCompareOption = buildForecastCompareChartOption(grainType, currentReport?.items || [], initialFuture30);
@@ -684,6 +696,9 @@ async function initOrUpdateForecastCharts(granaryItem) {
key: m.key,
name: m.name,
unit: m.unit,
warn: m.warn,
alert: m.alert,
direction: m.direction,
predictedValue: m.values[3] !== undefined ? m.values[3] : m.values[m.values.length - 1]
}));
@@ -847,7 +862,7 @@ function initOrUpdateGranaryBar() {
const names = STATE.list.map(i => i.name.replace('号浅圆仓', '#圆').replace('号仓房', '#平'));
const storageData = STATE.list.map(i => i.storageNum);
const capacityData = STATE.list.map(i => i.capacity);
const capacityData = STATE.list.map(i => i.capacityNum);
const option = {
tooltip: {
@@ -992,14 +1007,14 @@ function toggleFullscreen() {
}
// 初始化入口
window.addEventListener('DOMContentLoaded', () => {
window.addEventListener('DOMContentLoaded', async () => {
resizeScreen();
window.addEventListener('resize', resizeScreen);
updateClock();
setInterval(updateClock, 1000);
loadGranaryData();
await loadGranaryData();
loadEnvData();
const urlParams = new URLSearchParams(window.location.search);
@@ -1020,15 +1035,20 @@ window.addEventListener('DOMContentLoaded', () => {
renderDepotAlerts();
// 电视大屏 60 秒自动静默轮询同步最新台账与微气候
setInterval(() => {
setInterval(async () => {
try {
loadGranaryData();
await loadGranaryData();
loadEnvData();
renderKPIs();
renderHotspots();
renderMiniGrid();
renderDetailPanels();
renderDepotAlerts();
} catch (_) {}
}, 60000);
setupAutoPlay();
if (urlParams.get('view') === 'forecast') selectGranary(STATE.activeId, true);
const fsBtn = document.getElementById('btn-fullscreen');
if (fsBtn) fsBtn.addEventListener('click', toggleFullscreen);
@@ -1044,6 +1064,11 @@ window.addEventListener('DOMContentLoaded', () => {
});
window.addEventListener('keydown', (e) => {
if (e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
if (e.key === 'Escape' && STATE.isCenterChartOpen) {
toggleCenterChartBoard(STATE.centerChartTab, false);
return;
}
const curIdx = STATE.list.findIndex(w => w.id === STATE.activeId);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
const nextIdx = (curIdx - 1 + STATE.list.length) % STATE.list.length;

查看文件

@@ -4,7 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>中储粮(习水)智慧粮仓综合监测中心</title>
<link rel="stylesheet" href="./css/style.css?v=20261028_chart3">
<link rel="stylesheet" href="./css/style.css?v=20261028_status2">
<link rel="stylesheet" href="./css/quality-status.css?v=20261028_xishui1">
<!-- ECharts 可视化库 (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
</head>
@@ -328,7 +329,7 @@
<div class="center-chart-card-title" id="forecast-trend-title">多周期趋势</div>
<div class="forecast-env-pill" id="forecast-trend-env-pill">工况: 仓内 18.5℃ / 72.4% · 仓外 32.5℃ / 54.2%</div>
</div>
<div class="forecast-chart-note"><span>各指标独立刻度 · 保留原始单位</span><span>● 实测起点 ┈ 预测趋势 <span class="threshold-key">┄ 警戒参考</span></span></div>
<div class="forecast-chart-note"><span>各指标独立刻度 · 数值为实测 → 末期预测</span><span>● 实测起点 ┈ 预测趋势 <span class="warning-key">┄ 预警</span> <span class="threshold-key">┄ 警戒</span></span></div>
<div id="chart-forecast-trend" class="center-chart-canvas"></div>
</div>
<!-- 右侧:现状实测 vs 预测推演偏离对比柱状图 -->
@@ -402,6 +403,6 @@
</div>
<script type="module" src="./js/xishui_app.js?v=20261028_chart3"></script>
<script type="module" src="./js/xishui_app.js?v=20261028_demo3"></script>
</body>
</html>