feat(cost): 新增成本综合页面的辅料能耗反填功能

本次提交新增了全量反填、单行反填、单元格反填的批量/单独反填能力,支持辅料和能耗类型的数据反填入库,同时新增了反填进度弹窗、优化了表格列头布局和输入框后缀操作按钮样式,补充了对应的API引入和反填处理器注册逻辑。
This commit is contained in:
2026-06-16 11:52:34 +08:00
parent a3a4986cb8
commit 2874f1727a

View File

@@ -22,28 +22,38 @@
<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-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>
<el-switch v-model="inputMode" size="small" />
</span>
</div>
<el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" />
<el-table v-loading="gridLoading" height="calc(100vh - 140px)" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<!-- <el-alert :title="'已配置'+allCols.length+'个列'" type="info" :closable="false" show-icon style="margin-bottom:8px" /> -->
<el-table v-loading="gridLoading" height="calc(100vh - 260px)" :data="gridRows" border stripe size="mini" style="width:100%" :header-cell-style="headerStyle" :key="'tbl-'+inputMode">
<el-table-column label="日期" width="135" fixed>
<template slot-scope="s"><el-date-picker v-model="s.row.detailDate" type="date" value-format="yyyy-MM-dd" size="mini" style="width:124px" @change="sortGrid" /></template>
</el-table-column>
<template v-for="col in displayCols">
<el-table-column v-if="col.$type==='detail' && !col.isShift" :key="'d'+col.itemId" :label="col.itemName+(col.unit?'('+col.unit+')':'')" width="105" align="center">
<el-table-column v-if="col.$type==='detail' && !col.isShift" :key="'d'+col.itemId" align="center" width="130">
<template slot="header">
<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">
<i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:13px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row)" />
<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)" />
</span>
</el-input>
</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='detail' && col.isShift" :key="'ds'+col.itemId" :label="col.itemName+(col.unit?'('+col.unit+')':'')" width="120" align="center">
<el-table-column v-else-if="col.$type==='detail' && col.isShift" :key="'ds'+col.itemId" align="center" width="135">
<template slot="header">
<div class="col-hd">{{ col.itemName }}{{ col.unit ? '('+col.unit+')' : '' }}</div>
</template>
<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"><i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:12px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row, '1')" /></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"><i slot="suffix" v-if="col.queryCondition" :class="autoLoading[col.itemId]?'el-icon-loading':'el-icon-refresh'" style="cursor:pointer;font-size:12px;line-height:24px;color:#409eff" @click.stop="fetchAutoData(col, s.row, '2')" /></el-input></div>
<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"><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"><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>
</template>
</el-table-column>
<el-table-column v-else-if="col.$type==='metric' && !col.isShift" :key="'m'+col.mIdx" :label="col.metricName+(col.unit?'('+col.unit+')':'')" width="85" align="center">
@@ -56,8 +66,13 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" width="55" fixed="right" align="center">
<template slot-scope="s"><el-button size="mini" type="text" style="padding:0" @click="gridRows.splice(s.$index,1)">删除</el-button></template>
<el-table-column label="操作" width="60" fixed="right" align="center">
<template slot-scope="s">
<!-- <el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="saveRow(s.row)">保存</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px" @click="fetchRow(s.row)">抓取</el-button>
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#67c23a" @click="backfillRow(s.row)">反填</el-button> -->
<el-button size="mini" type="text" style="padding:0 2px;font-size:11px;color:#f56c6c" @click="gridRows.splice(s.$index,1)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top:8px;text-align:center"><el-button type="default" icon="el-icon-plus" size="mini" @click="gridRows.push({detailDate:''})">添加日期行</el-button></div>
@@ -273,6 +288,23 @@
</div>
</el-dialog>
</el-dialog>
<!-- Progress dialog -->
<el-dialog :title="progressTitle" :visible.sync="progressOpen" width="580px" append-to-body top="5vh" :close-on-click-modal="false">
<el-table :data="progressTasks" border stripe size="mini" max-height="420">
<el-table-column label="任务" prop="label" min-width="220" />
<el-table-column label="状态" width="110" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.status==='pending'" type="info" size="mini">等待中</el-tag>
<el-tag v-else-if="s.row.status==='running'" type="warning" size="mini"><i class="el-icon-loading" /> 执行中</el-tag>
<el-tag v-else-if="s.row.status==='success'" type="success" size="mini">成功</el-tag>
<el-tag v-else-if="s.row.status==='fail'" type="danger" size="mini">失败</el-tag>
</template>
</el-table-column>
<el-table-column label="信息" prop="error" min-width="160" show-overflow-tooltip />
</el-table>
<div slot="footer"><el-button @click="progressOpen=false"> </el-button></div>
</el-dialog>
</template>
</div>
</div>
@@ -285,9 +317,10 @@ import { listProdMetric, addProdMetric, updateProdMetric, delProdMetric, getProd
import { listItem } from "@/api/cost/item"
import { listLightPendingAction } from "@/api/wms/pendingAction"
import { getCoilStatisticsList } from "@/api/wms/coil"
import { listAuxiliaryConsume } from "@/api/eqp/auxiliaryConsume"
import { listAuxiliaryConsume, addAuxiliaryConsume, updateAuxiliaryConsume } from "@/api/eqp/auxiliaryConsume"
import { listProductionLine } from "@/api/wms/productionLine"
import { listRollGrindAll } from "@/api/mes/roll/rollGrind"
import { listEnergyRecord, addEnergyRecord, updateEnergyRecord } from "@/api/ems/energyRecord"
function parseDateRange(detailDate) {
const d = (detailDate || '').slice(0, 10)
@@ -306,6 +339,11 @@ export function registerQueryHandler(category, handler) {
queryHandlers[category] = handler
}
const backfillHandlers = {}
export function registerBackfillHandler(category, handler) {
backfillHandlers[category] = handler
}
const teamMap = { '1': '甲', '2': '乙' }
registerQueryHandler('原料', async (queryCondition, row, col, report, shift) => {
@@ -367,6 +405,30 @@ registerQueryHandler('轧辊', async (queryCondition, row, col, report, shift) =
return total || null
})
registerBackfillHandler('辅料', async (queryCondition, row, col, report, shift, value) => {
if (!row.detailDate) return
const d = row.detailDate.slice(0, 10)
const res = await listAuxiliaryConsume({ recordDate: d, typeId: queryCondition, pageSize: 9999 })
const items = res.rows || []
if (items.length > 0) {
await updateAuxiliaryConsume({ consumeId: items[0].consumeId, consume: value })
} else {
await addAuxiliaryConsume({ recordDate: d, typeId: queryCondition, consume: value })
}
})
registerBackfillHandler('能耗', async (queryCondition, row, col, report, shift, value) => {
if (!row.detailDate) return
const d = row.detailDate.slice(0, 10)
const res = await listEnergyRecord({ recordDate: d, meterId: queryCondition, pageSize: 9999 })
const items = res.rows || []
if (items.length > 0) {
await updateEnergyRecord({ energyRecordId: items[0].energyRecordId, consumption: value })
} else {
await addEnergyRecord({ recordDate: d, meterId: queryCondition, consumption: value })
}
})
export default {
name: "CostComprehensive",
data() {
@@ -383,12 +445,14 @@ export default {
mgrOpen: false, mgrList: [], defOpen: false, defTitle: '', defForm: {},
copyCfgOpen: false, copyReports: [], copySrc: null,
configOpen: false,
autoLoading: {},
autoLoading: {}, backfillLoading: {},
lineOptions: [],
lineType: null,
noLineType: false,
inputMode: false,
priceOpen: false, priceList: [], priceSaving: false
priceOpen: false, priceList: [], priceSaving: false,
backfilling: false,
progressOpen: false, progressTitle: '', progressTasks: []
}
},
computed: {
@@ -774,6 +838,180 @@ export default {
}
},
async backfillCost() {
const detailCols = this.allCols.filter(c => c.$type === 'detail' && (c.category === '辅料' || c.category === '能耗') && c.queryCondition)
if (!detailCols.length) { this.$modal.msgWarning('无可用反填列(仅辅料/能耗)'); return }
this.backfilling = true
try {
let count = 0
for (const row of this.gridRows) {
if (!row.detailDate) continue
for (const col of detailCols) {
const handler = backfillHandlers[col.category]
if (!handler) continue
if (col.isShift) {
const v1 = row['q' + col.itemId + '_1']
if (v1 != null && v1 !== '') { await handler(col.queryCondition, row, col, this.activeReport, '1', v1); count++ }
const v2 = row['q' + col.itemId + '_2']
if (v2 != null && v2 !== '') { await handler(col.queryCondition, row, col, this.activeReport, '2', v2); count++ }
} else {
const v = row['q' + col.itemId]
if (v != null && v !== '') { await handler(col.queryCondition, row, col, this.activeReport, null, v); count++ }
}
}
}
this.$modal.msgSuccess(`反填完成,共处理${count}`)
} catch (e) {
this.$modal.msgError('反填失败')
} finally {
this.backfilling = false
}
},
async backfillCell(col, row, shift) {
if (!col.queryCondition || this.backfillLoading[col.itemId]) return
const handler = backfillHandlers[col.category]
if (!handler) { this.$modal.msgWarning(`类别 "${col.category}" 未注册反填处理器`); return }
const val = shift ? row['q' + col.itemId + '_' + shift] : row['q' + col.itemId]
if (val == null || val === '') { this.$modal.msgWarning('当前单元格无数据'); return }
this.$set(this.backfillLoading, col.itemId, true)
try {
await handler(col.queryCondition, row, col, this.activeReport, shift || null, val)
this.$modal.msgSuccess('反填成功')
} catch (e) {
this.$modal.msgError('反填失败')
} finally {
this.$set(this.backfillLoading, col.itemId, false)
}
},
async runBatchTasks(title, tasks) {
this.progressTitle = title
this.progressTasks = tasks.map(t => ({ label: t.label, status: 'pending', error: '', run: t.run }))
this.progressOpen = true
await this.$nextTick()
for (const task of this.progressTasks) {
if (!this.progressOpen) break
task.status = 'running'
try {
await task.run()
task.status = 'success'
} catch (e) {
task.status = 'fail'
task.error = e.message || '执行失败'
}
}
},
async batchFetchCol(col) {
if (!col.queryCondition) return
const handler = queryHandlers[col.category] || queryHandlers['default']
if (!handler) { this.$modal.msgWarning(`类别 "${col.category}" 未注册查询处理器`); return }
const round3 = n => Math.round(n * 1000) / 1000
const tasks = []
for (const row of this.gridRows) {
if (!row.detailDate) continue
tasks.push({
label: `${row.detailDate} ${col.itemName}`,
run: async () => {
const val = await handler(col.queryCondition, row, col, this.activeReport)
if (val != null) {
if (col.isShift && Array.isArray(val)) {
this.$set(row, 'q' + col.itemId + '_1', round3(val[0]))
this.$set(row, 'q' + col.itemId + '_2', round3(val[1]))
} else {
this.$set(row, 'q' + col.itemId, round3(val))
}
}
}
})
}
if (!tasks.length) { this.$modal.msgWarning('无可用行'); return }
this.$set(this.autoLoading, col.itemId, true)
await this.runBatchTasks(`批量抓取 - ${col.itemName}`, tasks)
this.$set(this.autoLoading, col.itemId, false)
this.recalcAll()
},
async batchBackfillCol(col) {
if (!col.queryCondition) return
const handler = backfillHandlers[col.category]
if (!handler) { this.$modal.msgWarning(`类别 "${col.category}" 未注册反填处理器`); return }
const tasks = []
for (const row of this.gridRows) {
if (!row.detailDate) continue
if (col.isShift) {
const v1 = row['q' + col.itemId + '_1']
if (v1 != null && v1 !== '') {
tasks.push({ label: `${row.detailDate} ${col.itemName}(甲)`, run: () => handler(col.queryCondition, row, col, this.activeReport, '1', v1) })
}
const v2 = row['q' + col.itemId + '_2']
if (v2 != null && v2 !== '') {
tasks.push({ label: `${row.detailDate} ${col.itemName}(乙)`, run: () => handler(col.queryCondition, row, col, this.activeReport, '2', v2) })
}
} else {
const v = row['q' + col.itemId]
if (v != null && v !== '') {
tasks.push({ label: `${row.detailDate} ${col.itemName}`, run: () => handler(col.queryCondition, row, col, this.activeReport, null, v) })
}
}
}
if (!tasks.length) { this.$modal.msgWarning('无可用数据'); return }
this.$set(this.backfillLoading, col.itemId, true)
await this.runBatchTasks(`批量反填 - ${col.itemName}`, tasks)
this.$set(this.backfillLoading, col.itemId, false)
},
async saveRow(row) {
if (!row.detailDate) return
const rid = this.activeReport.reportId; if (!rid) return
try {
const exist = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
const dateStr = row.detailDate
const removeIds = (exist.rows || []).filter(d => d.detailDate === dateStr).map(d => d.detailId)
const detailCols = this.allCols.filter(c => c.$type === 'detail')
const detailList = []
detailCols.forEach(col => {
const push = (shift, sfx) => { const qty = row['q' + col.itemId + sfx]; if (qty != null && qty !== '') detailList.push({ reportId: rid, detailDate: dateStr, shift: shift || '0', itemId: col.itemId, quantity: qty }) }
if (col.isShift) { push('1', '_1'); push('2', '_2') } else push(null, '')
})
await batchSaveProdDetail({ detailIds: removeIds, prodDetailList: detailList })
this.$modal.msgSuccess('行保存成功')
} catch (e) {
this.$modal.msgError('行保存失败')
}
},
async fetchRow(row) {
if (!row.detailDate) return
const cols = this.allCols.filter(c => c.$type === 'detail' && c.queryCondition)
for (const col of cols) {
if (col.isShift) { await this.fetchAutoData(col, row, '1'); await this.fetchAutoData(col, row, '2') }
else { await this.fetchAutoData(col, row) }
}
this.$modal.msgSuccess('行抓取完成')
},
async backfillRow(row) {
if (!row.detailDate) return
const cols = this.allCols.filter(c => c.$type === 'detail' && (c.category === '辅料' || c.category === '能耗') && c.queryCondition)
if (!cols.length) { this.$modal.msgWarning('无可用反填列'); return }
for (const col of cols) {
const handler = backfillHandlers[col.category]
if (!handler) continue
if (col.isShift) {
const v1 = row['q' + col.itemId + '_1']
if (v1 != null && v1 !== '') { await handler(col.queryCondition, row, col, this.activeReport, '1', v1) }
const v2 = row['q' + col.itemId + '_2']
if (v2 != null && v2 !== '') { await handler(col.queryCondition, row, col, this.activeReport, '2', v2) }
} else {
const v = row['q' + col.itemId]
if (v != null && v !== '') { await handler(col.queryCondition, row, col, this.activeReport, null, v) }
}
}
this.$modal.msgSuccess('行反填完成')
},
/* helpers */
async loadLines() { const r = await listProductionLine({ pageSize: 999 }); this.lineOptions = r.rows || [] },
lineName(row) {
@@ -828,4 +1066,12 @@ export default {
.drag-handle { cursor: grab; font-size: 14px; color: #909399; padding: 2px; display: inline-flex; align-items: center; }
.drag-handle:active { cursor: grabbing; }
.drag-handle:hover { color: #409eff; }
.col-hd { font-size: 12px; line-height: 1.3; }
.col-hd-actions { display: flex; align-items: center; gap: 4px; margin-top: 1px; font-size: 10px; }
.col-hd-actions .el-link { font-size: 10px; }
.input-suffix-actions { display: inline-flex; align-items: center; gap: 1px; }
.ica { cursor: pointer; font-size: 13px; line-height: 24px; }
.ica-fetch { color: #409eff; }
.ica-backfill { color: #67c23a; margin-right: 1px; }
.ica:hover { opacity: 0.7; }
</style>