文件

153 行
14 KiB
HTML

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<link rel="stylesheet" href="__CDN__/assets/element-ui-2.15.14/css/index.css">
<link rel="stylesheet" href="__CDN__/assets/css/vvv.css">
<style>
.forecast-page { background: #fff; padding: 20px; }
.forecast-toolbar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 18px; }
.forecast-toolbar h3 { margin: 0 auto 0 0; font-size: 18px; }
.forecast-intro { color: #606266; margin: 0 0 20px; line-height: 1.8; }
.forecast-env { display: flex; flex-wrap: wrap; gap: 12px 28px; margin: 16px 0; color: #606266; }
.forecast-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
.forecast-fields .el-input, .forecast-fields .el-date-editor { width: 100%; }
.forecast-page .el-pagination { margin-top: 18px; text-align: right; }
.forecast-foot { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.forecast-error { margin-bottom: 16px; }
.forecast-actual-date { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 16px 0; }
.forecast-hint { color: #909399; font-size: 13px; line-height: 1.8; }
.forecast-page .el-dialog { max-width: calc(100vw - 24px); }
@media (max-width: 700px) { .forecast-fields { grid-template-columns: 1fr; } .forecast-page { padding: 12px; } }
</style>
<div id="forecast-app" class="forecast-page" v-cloak>
<div class="forecast-toolbar">
<h3>温湿度预测与实测</h3>
<el-select v-model="site" clearable placeholder="全部库区" @change="siteChanged" style="width: 150px" aria-label="筛选库区"><el-option v-for="s in siteOptions" :key="s.id" :value="s.id" :label="s.name"></el-option></el-select>
<el-select v-model="gid" clearable filterable placeholder="全部仓库" @change="filterChanged" style="width: 280px" aria-label="筛选仓库">
<el-option v-for="g in granaries" :key="g.id" :value="g.id" :label="g.label"></el-option>
</el-select>
<el-button icon="el-icon-refresh" size="small" @click="load" :loading="loading">刷新</el-button>
<el-button type="primary" icon="el-icon-plus" size="small" @click="newRecord">录入温湿度并预测</el-button>
</div>
<p class="forecast-intro">选择仓库,录入采集当天的仓内外温湿度,生成未来品质指标预测;取得检测结果后,在对应周期补录实测值并查看偏差。</p>
<forecast-list :rows="rows" :loading="loading" @open="openRecord" @edit="editRecord" @delete="deleteRecord"></forecast-list>
<el-pagination background layout="total, prev, pager, next" :total="total" :page-size="15" :current-page="page" @current-change="changePage"></el-pagination>
<el-dialog title="录入温湿度并预测" :visible.sync="creating" width="760px" :close-on-click-modal="false" :before-close="closeCreate" :show-close="!saving">
<forecast-create v-if="creating" ref="create" :granaries="granaries" :busy="saving" @submit="createRecord" @cancel="closeCreate"></forecast-create>
</el-dialog>
<el-dialog title="编辑温湿度并更新预测" :visible.sync="editing" width="760px" :close-on-click-modal="false" :before-close="closeEdit" :show-close="!saving">
<forecast-create v-if="editing && editData" ref="edit" :granaries="granaries" :busy="saving" :initial-data="editData" :is-edit="true" @submit="updateRecord" @cancel="closeEdit"></forecast-create>
</el-dialog>
<el-dialog title="预测与真实监测值对比" :visible.sync="viewing" width="1180px" :close-on-click-modal="false" :before-close="closeDetail" :show-close="!saving">
<forecast-detail v-if="viewing && selected" ref="detail" :record="selected" :busy="saving" @save="saveActual" @retry="retry"></forecast-detail>
<div class="forecast-foot" style="display:flex; justify-content:space-between; align-items:center;">
<div>
<el-button type="danger" plain size="small" @click="deleteFromDetail" :disabled="saving">删除此记录</el-button>
</div>
<div>
<el-button size="small" type="primary" plain @click="editFromDetail" :disabled="saving">编辑温湿度</el-button>
<el-button size="small" @click="closeDetail" :disabled="saving">关闭</el-button>
</div>
</div>
</el-dialog>
</div>
<script type="text/x-template" id="forecast-list-template">
<el-table :data="rows" border stripe v-loading="loading" empty-text="暂无预测记录,请先录入温湿度">
<el-table-column prop="id" label="记录编号" width="90"></el-table-column>
<el-table-column prop="granary_name" label="仓库" min-width="120"></el-table-column>
<el-table-column prop="storage" label="库区" min-width="180" show-overflow-tooltip></el-table-column>
<el-table-column prop="observed_date" label="采集日期" width="115"></el-table-column>
<el-table-column label="仓内温湿度" min-width="175">
<template slot-scope="s">
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
<span>{{number(s.row.tm_i)}}℃ / {{number(s.row.hm_i)}}%</span>
<el-tag :type="evalTemp(s.row.tm_i, s.row.hm_i).tagType" size="mini" effect="plain">{{evalTemp(s.row.tm_i, s.row.hm_i).label}}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="仓外温湿度" min-width="145"><template slot-scope="s">{{number(s.row.tm_o)}}℃ / {{number(s.row.hm_o)}}%</template></el-table-column>
<el-table-column label="预测状态" width="130"><template slot-scope="s"><el-tag :type="s.row.status === 'ready' ? 'success' : s.row.status === 'failed' ? 'danger' : 'warning'" size="small">{{status(s.row.status)}}</el-tag></template></el-table-column>
<el-table-column label="实测完成周期" width="120"><template slot-scope="s">{{s.row.measured}} / {{s.row.point_count}}</template></el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template slot-scope="s">
<el-button type="text" size="small" @click="$emit('open', s.row.id)">查看 / 补录实测</el-button>
<el-button type="text" size="small" @click="$emit('edit', s.row)">编辑</el-button>
<el-button type="text" size="small" style="color: #F56C6C;" @click="$emit('delete', s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</script>
<script type="text/x-template" id="forecast-create-template">
<el-form label-position="top" @submit.native.prevent>
<el-form-item label="所属仓库(小麦 / 稻谷)" required>
<el-select v-model="form.gid" filterable placeholder="请选择仓库" style="width: 100%" :disabled="busy">
<el-option v-for="g in granaries" :key="g.id" :value="g.id" :label="g.label"></el-option>
</el-select>
</el-form-item>
<!-- 中储粮温控规范快捷工况 -->
<div class="forecast-preset-box" style="margin-bottom: 16px; padding: 10px 14px; background: #f4f8fa; border: 1px solid #dbe6ee; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 13px; font-weight: bold; color: #304157;">🎯 快捷规范工况模板:</span>
<span style="font-size: 12px; color: #909399;">符合中储粮《粮油储藏技术规范》GB/T 29890</span>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<el-button v-for="(p, k) in presets" :key="k" size="mini" :type="selectedPresetKey === k ? p.type : ''" plain @click="applyPreset(k)">
{{p.name}} ({{p.tm_i}}℃ / {{p.hm_i}}%)
</el-button>
</div>
</div>
<!-- 实时温控规范研判指示卡片 -->
<div class="forecast-standard-banner" style="margin-bottom: 16px; padding: 8px 12px; border-radius: 6px; font-size: 13px; display: flex; align-items: center; justify-content: space-between;"
:style="{ background: tempStatus.isHigh ? '#fef0f0' : (tempStatus.grade === 'low' ? '#f0f9eb' : '#edf6fc'), border: '1px solid ' + (tempStatus.isHigh ? '#fde2e2' : (tempStatus.grade === 'low' ? '#e1f3d8' : '#d9ecff')) }">
<div style="display: flex; align-items: center; gap: 8px;">
<el-tag :type="tempStatus.tagType" size="small">{{tempStatus.label}}</el-tag>
<span :style="{ color: tempStatus.isHigh ? '#f56c6c' : (tempStatus.grade === 'low' ? '#67c23a' : '#409eff') }">{{tempStatus.hint}}</span>
</div>
<span v-if="tempStatus.isHigh" style="font-size: 12px; color: #f56c6c; font-weight: bold;">[注意通风降温]</span>
</div>
<div class="forecast-fields">
<el-form-item label="温湿度采集日期" required><el-date-picker v-model="form.observed_date" type="date" value-format="yyyy-MM-dd" :picker-options="dateOptions" :disabled="busy" :editable="false" placeholder="选择采集日期"></el-date-picker></el-form-item>
<el-form-item label="预测周期" required><el-checkbox-group v-model="form.horizons" :disabled="busy"><el-checkbox v-for="day in [7,15,30,60]" :key="day" :label="day">{{day}}天后</el-checkbox></el-checkbox-group></el-form-item>
<el-form-item v-for="field in fields" :key="field.key" :label="field.label" required>
<el-input v-model="form[field.key]" :label="field.label" type="number" :min="field.min" :max="field.max" step="0.1" :disabled="busy" placeholder="请输入实测环境值"><template slot="append">{{field.unit}}</template></el-input>
</el-form-item>
</div>
<p class="forecast-hint"><strong>中储粮控温技术规范:</strong>低温储粮 ≤15℃,准低温储粮 ≤20℃,常温储粮 ≤25℃(>25℃需通风或机械降温)。仓内相对湿度建议 ≤70%~75%。预测目标日期按采集日期加周期计算。</p>
<div class="forecast-foot">
<el-button :disabled="busy" @click="$emit('cancel')">取消</el-button>
<el-button type="primary" :loading="busy" @click="submit">{{ isEdit ? '保存修改并重新生成预测' : '保存并生成预测' }}</el-button>
</div>
</el-form>
</script>
<script type="text/x-template" id="forecast-detail-template">
<div>
<strong>{{record.storage}} · {{record.granary_name}} · {{record.grain_type === 'wheat' ? '小麦' : '稻谷'}}</strong>
<div class="forecast-env"><span>采集日期:{{record.observed_date}}</span><span>仓内:{{number(record.tm_i)}}℃ / {{number(record.hm_i)}}%</span><span>仓外:{{number(record.tm_o)}}℃ / {{number(record.hm_o)}}%</span></div>
<div v-if="record.status !== 'ready'" class="forecast-error">
<el-alert :title="record.status === 'processing' ? '预测正在生成,稍后刷新查看结果;若请求中断,两分钟后可重试。' : (record.error || '温湿度已保存,等待生成预测。')" :type="record.status === 'failed' ? 'error' : 'warning'" :closable="false" show-icon></el-alert>
<el-button style="margin-top: 10px" size="small" type="primary" :loading="busy" @click="$emit('retry', record.id)">重新生成预测 / 刷新状态</el-button>
</div>
<el-tabs v-model="active" :before-leave="beforeLeave">
<el-tab-pane v-for="p in record.points" :key="p.id" :name="String(p.id)" :label="'+' + p.horizon + '天 · ' + p.target_date"></el-tab-pane>
</el-tabs>
<div v-if="point">
<div class="forecast-actual-date"><strong>预测目标:{{point.target_date}}</strong><label>实际检测日期:</label><el-date-picker v-model="actualDate" type="date" value-format="yyyy-MM-dd" :picker-options="dateOptions" :editable="false" :disabled="busy" placeholder="取得结果后选择实际检测日期"></el-date-picker></div>
<el-alert v-if="actualDate && actualDate !== point.target_date" title="实际检测日期与预测目标日期不同,以下偏差仅供参考,请核对是否选对周期。" type="warning" :closable="false" show-icon style="margin-bottom: 12px"></el-alert>
<el-table :data="comparison" border>
<el-table-column prop="name" label="监测指标" min-width="200"></el-table-column>
<el-table-column prop="unit" label="单位" width="100"></el-table-column>
<el-table-column label="原始预测值" width="125"><template slot-scope="s">{{number(s.row.predicted)}}</template></el-table-column>
<el-table-column label="人工实测值" min-width="155"><template slot-scope="s"><el-input v-model="actual[s.row.key]" :label="s.row.name + '实测值'" type="number" min="0" :max="s.row.max" step="0.0001" :disabled="busy" placeholder="待检测,可留空"></el-input></template></el-table-column>
<el-table-column label="偏差(实测−预测)" width="150"><template slot-scope="s">{{signed(s.row.delta)}}</template></el-table-column>
<el-table-column label="相对偏差" width="115"><template slot-scope="s">{{s.row.percent === null ? '—' : signed(s.row.percent) + '%'}}</template></el-table-column>
</el-table>
<p class="forecast-hint">实测值可以分次补录;空白表示未检测,0 表示真实零值。偏差 = 实测值 − 预测值;相对偏差 = 偏差 ÷ |预测值|,预测值为0时不计算相对偏差。</p>
<div class="forecast-foot"><el-button type="primary" :loading="busy" @click="save">保存本周期实测值</el-button></div>
</div>
</div>
</script>
<script src="__CDN__/assets/element-ui-2.15.14/js/vue.js"></script>
<script src="__CDN__/assets/element-ui-2.15.14/js/index.js"></script>