feat(cost): 新增成本综合页的异常分析功能

1. 新增异常分析弹窗组件AnomalyAnalysis,支持基于3σ法则检测异常数据
2. 在综合成本页面右上角添加异常分析入口按钮
3. 为异常单元格添加红色高亮样式,标记异常数据
4. 实现异常数据的接收、存储和表格标记逻辑
This commit is contained in:
2026-06-27 17:58:19 +08:00
parent dbea29eb23
commit 59dad19296
2 changed files with 673 additions and 4 deletions

View File

@@ -22,6 +22,7 @@
<el-button type="primary" size="mini" style="float:right;margin-left:8px" @click="saveGrid" :loading="saving">保存</el-button>
<el-button size="mini" style="float:right;margin-left:8px" @click="openColCfg">列配置</el-button>
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-money" @click="openPriceMgr">价格管理</el-button>
<el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-warning-outline" @click="anomalyOpen = true">异常分析</el-button>
<!-- <el-button size="mini" style="float:right;margin-left:8px" icon="el-icon-upload2" @click="backfillCost" :loading="backfilling">反填</el-button> -->
<span style="float:right;margin-right:12px;font-size:12px;color:#606266;display:flex;align-items:center">
<span style="margin-right:4px">{{ inputMode ? '录入' : '查看' }}</span>
@@ -39,7 +40,7 @@
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<template slot-scope="s">
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll">
<el-input v-model="s.row['q'+col.itemId]" size="mini" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions">
<i title="反填" v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row)" />
<i title="自动获取" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row)" />
@@ -54,13 +55,13 @@
<template slot-scope="s">
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll">
<el-input v-model="s.row['q'+col.itemId+'_1']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '1')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '1')" /></span> -->
</el-input>
</div>
<div class="shift-cell">
<span class="shift-tag"></span>
<el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll">
<el-input v-model="s.row['q'+col.itemId+'_2']" size="mini" class="shift-input" @input="recalcAll" :class="{'anomaly-input': isAnomalyCell(s.row.detailDate, col.itemId)}">
<!-- <span slot="suffix" v-if="col.queryCondition" class="input-suffix-actions"><i v-if="col.category==='辅料'||col.category==='能耗'" :class="backfillLoading[col.itemId]?'el-icon-loading':'el-icon-upload2'" class="ica ica-backfill" @click.stop="backfillCell(col, s.row, '2')" /><i :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" class="ica ica-fetch" @click.stop="fetchAutoData(col, s.row, '2')" /></span> -->
</el-input>
</div>
@@ -317,6 +318,9 @@
</el-table>
<div slot="footer"><el-button @click="progressOpen=false"> </el-button></div>
</el-dialog>
<!-- Anomaly analysis -->
<AnomalyAnalysis :visible.sync="anomalyOpen" :reportId="activeReport ? activeReport.reportId : null" :allItems="allItems" :gridRows="gridRows" :allCols="allCols" @anomaly-found="onAnomalyFound" />
</template>
</div>
</div>
@@ -334,6 +338,7 @@ import { listAuxiliaryConsume, addAuxiliaryConsume, updateAuxiliaryConsume } fro
import { listProductionLine } from "@/api/wms/productionLine"
import { listRollGrindAll } from "@/api/mes/roll/rollGrind"
import { listEnergyRecord, addEnergyRecord, updateEnergyRecord } from "@/api/ems/energyRecord"
import AnomalyAnalysis from "./components/AnomalyAnalysis"
function parseDateRange(detailDate) {
const d = (detailDate || '').slice(0, 10)
@@ -444,6 +449,7 @@ registerBackfillHandler('能耗', async (queryCondition, row, col, report, shift
export default {
name: "CostComprehensive",
components: { AnomalyAnalysis },
data() {
return {
loading: false, list: [], tabs: [], total: 0, sel: 0, selIds: [], showSearch: true,
@@ -465,7 +471,8 @@ export default {
inputMode: false,
priceOpen: false, priceList: [], priceSaving: false,
backfilling: false,
progressOpen: false, progressTitle: '', progressTasks: []
progressOpen: false, progressTitle: '', progressTasks: [],
anomalyOpen: false, anomalyMap: {}
}
},
computed: {
@@ -1060,6 +1067,27 @@ export default {
else return row.lineType || '-'
}
},
onAnomalyFound(list) {
const map = {}
list.forEach(a => {
// Normalize date to yyyy-MM-dd string
const dt = (a.detailDate || '').slice(0, 10)
if (!dt) return
if (!map[dt]) map[dt] = new Set()
map[dt].add(String(a.itemId))
})
this.anomalyMap = map
this.anomalyOpen = false
this.$forceUpdate()
this.$modal.msgSuccess(`已将 ${list.length} 条异常数据标记到表格`)
},
isAnomalyCell(detailDate, itemId) {
if (!detailDate) return false
const dt = (detailDate || '').slice ? detailDate.slice(0, 10) : String(detailDate).slice(0, 10)
const set = this.anomalyMap[dt]
if (!set) return false
return set.has(String(itemId))
},
async loadItems() { if (!this.allItems.length) { const r = await listItem({ pageNum:1, pageSize:999 }); this.allItems = r.rows || [] } },
async loadAllMetrics(rid) {
const q = { pageNum:1, pageSize:99999 }; if (rid) q.reportId = rid
@@ -1113,4 +1141,5 @@ export default {
.ica-fetch { color: #409eff; }
.ica-backfill { color: #67c23a; margin-right: 1px; }
.ica:hover { opacity: 0.7; }
/deep/ .anomaly-input .el-input__inner { background: #fef0f0 !important; border-color: #f56c6c !important; }
</style>