Compare commits
3 Commits
25beef5517
...
2874f1727a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2874f1727a | |||
| a3a4986cb8 | |||
| f525770094 |
@@ -35,6 +35,15 @@ export function updateMaterialWarning(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 批量处理钢卷通用维度告警(长度/厚度/宽度)
|
||||
export function batchHandleMaterial(data) {
|
||||
return request({
|
||||
url: '/wms/materialWarning/batchHandle',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除钢卷通用维度告警(长度/厚度/宽度)
|
||||
export function delMaterialWarning(warningId) {
|
||||
return request({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -665,7 +665,7 @@ export default {
|
||||
lockValue = 3
|
||||
} else if (this.acidRollingActionType == 120) {
|
||||
lockValue = 4
|
||||
} else if (this.acidRollingActionType >= 200 && this.acidRollingActionType <= 299) {
|
||||
} else if (this.acidRollingActionType >= 200 && this.acidRollingActionType <= 210) {
|
||||
lockValue = 5
|
||||
}
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
<script>
|
||||
import { getMaterialCoil, listMaterialCoil, createSpecialChild, completeSpecialSplit, updateMaterialCoilSimple, delMaterialCoil, getFirstHeatCoilMaterial } from '@/api/wms/coil'
|
||||
import { listCoilAbnormal } from '@/api/wms/coilAbnormal'
|
||||
import { completeAction, getPendingAction, updatePendingAction } from '@/api/wms/pendingAction'
|
||||
import { getPendingAction, updatePendingAction } from '@/api/wms/pendingAction'
|
||||
import { saveCoilCache, getCoilCacheByCoilId, delCoilCache } from '@/api/wms/coilCache'
|
||||
import { getGalvanize1TypingPrefill } from '@/api/pocket/acidTyping'
|
||||
import ProductSelect from '@/components/KLPService/ProductSelect'
|
||||
@@ -939,11 +939,11 @@ export default {
|
||||
}
|
||||
|
||||
// 2. 完成待办动作(根据业务逻辑调整)
|
||||
const actionRes = await completeAction(this.actionId, splitRes.data.childCoilIds.join(','))
|
||||
if (actionRes.code !== 200) {
|
||||
this.$message.error('完成待办动作失败:' + actionRes.msg)
|
||||
return
|
||||
}
|
||||
// const actionRes = await completeAction(this.actionId, splitRes.data.childCoilIds.join(','))
|
||||
// if (actionRes.code !== 200) {
|
||||
// this.$message.error('完成待办动作失败:' + actionRes.msg)
|
||||
// return
|
||||
// }
|
||||
this.buttonLoading = false
|
||||
|
||||
this.$message.success('分条操作已完成')
|
||||
|
||||
740
klp-ui/src/views/wms/report/line.vue
Normal file
740
klp-ui/src/views/wms/report/line.vue
Normal file
@@ -0,0 +1,740 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container line-report">
|
||||
<!-- 查询条件 -->
|
||||
<div class="filter-area">
|
||||
<el-form label-width="70px" inline class="filter-form">
|
||||
<el-form-item label="产线">
|
||||
<el-select v-model="actionTypes" style="width: 150px;" placeholder="产线" clearable size="small" @change="handleQuery">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option v-for="line in lineOptions" :key="line.value" :label="line.label" :value="line.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="品质">
|
||||
<muti-select
|
||||
v-model="queryParams.qualityStatusCsv"
|
||||
:options="dict.type.coil_quality_status"
|
||||
placeholder="品质"
|
||||
clearable
|
||||
style="width: 150px;"
|
||||
size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="规格">
|
||||
<memo-input
|
||||
v-model="queryParams.itemSpecification"
|
||||
style="width: 130px;"
|
||||
storage-key="coilSpec"
|
||||
placeholder="规格"
|
||||
clearable
|
||||
size="small"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="材质">
|
||||
<muti-select
|
||||
v-model="queryParams.itemMaterial"
|
||||
style="width: 150px;"
|
||||
:options="dict.type.coil_material"
|
||||
placeholder="材质"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="厂家">
|
||||
<muti-select
|
||||
v-model="queryParams.itemManufacturer"
|
||||
style="width: 150px;"
|
||||
:options="dict.type.coil_manufacturer"
|
||||
placeholder="厂家"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="入场号">
|
||||
<el-input
|
||||
v-model="queryParams.enterCoilNo"
|
||||
style="width: 150px;"
|
||||
placeholder="入场钢卷号"
|
||||
clearable
|
||||
size="small"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="time-row">
|
||||
<span class="time-label">时间范围</span>
|
||||
<el-date-picker
|
||||
v-model="startTime"
|
||||
style="width: 170px;"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="开始"
|
||||
size="small"
|
||||
/>
|
||||
<span class="time-sep">—</span>
|
||||
<el-date-picker
|
||||
v-model="endTime"
|
||||
style="width: 170px;"
|
||||
type="datetime"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
placeholder="结束"
|
||||
size="small"
|
||||
/>
|
||||
<span class="time-label period-label">周期</span>
|
||||
<el-select v-model="periodType" style="width: 90px;" size="small" @change="recalcPeriod">
|
||||
<el-option label="按日" value="day" />
|
||||
<el-option label="按周" value="week" />
|
||||
<el-option label="按月" value="month" />
|
||||
</el-select>
|
||||
<el-button-group size="small" class="quick-btns">
|
||||
<el-button @click="setQuickPeriod('thisMonth')">本月</el-button>
|
||||
<el-button @click="setQuickPeriod('lastMonth')">上月</el-button>
|
||||
<el-button @click="setQuickPeriod('thisQuarter')">本季</el-button>
|
||||
<el-button @click="setQuickPeriod('thisYear')">今年</el-button>
|
||||
<el-button @click="setQuickPeriod('lastYear')">去年</el-button>
|
||||
</el-button-group>
|
||||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 口径说明 -->
|
||||
<div class="explain-bar">
|
||||
<span class="explain-icon">i</span>
|
||||
<span class="explain-text">
|
||||
<strong>统计口径:</strong>按{{ periodLabel }}周期拆分,统计各期产出量与消耗量。
|
||||
<span class="explain-divider" />
|
||||
<strong>M卷定义:</strong>钢卷经部分加工后剩余部分返回原料库并重新编号,新钢卷号中包含字母"M"(且"M"不在前5位)的钢卷定义为M卷,不计入产出统计。
|
||||
<span class="explain-divider" />
|
||||
<strong>成品率</strong> = 产出总重 / 消耗总重 × 100%
|
||||
<strong>正品率</strong> = (1 - 异常率) × 100%
|
||||
<strong>损耗率</strong> = 1 - 成品率
|
||||
<strong>异常率</strong> = (异常库位钢卷数 + 特殊品质状态钢卷数) / 产出总数 × 100%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 周期数据摘要 -->
|
||||
<div v-if="periodData.length > 0" class="summary-bar">
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">周期数</span>
|
||||
<span class="summary-value">{{ periodData.length }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">产出合计</span>
|
||||
<span class="summary-value">{{ totalOutCount }}卷 / {{ totalOutWeight }}t</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">消耗合计</span>
|
||||
<span class="summary-value">{{ totalLossCount }}卷 / {{ totalLossWeight }}t</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 折线图区域 -->
|
||||
<div class="chart-grid">
|
||||
<div v-for="(cfg, idx) in chartConfigs" :key="idx" class="chart-group">
|
||||
<div class="group-title">{{ cfg.title }}</div>
|
||||
<div :ref="'chart_' + idx" class="chart-container" :style="{ height: cfg.height || '280px' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 明细表格 -->
|
||||
<div v-if="periodData.length > 0" class="detail-section">
|
||||
<div class="group-title">各期明细</div>
|
||||
<div class="table-wrapper">
|
||||
<el-table :data="periodData" border size="small" stripe height="400px" style="width: 100%">
|
||||
<el-table-column prop="periodLabel" label="周期" min-width="100" fixed />
|
||||
<el-table-column label="产出统计" align="center">
|
||||
<el-table-column prop="outCount" label="数量" min-width="70" />
|
||||
<el-table-column prop="outTotalWeight" label="总重(t)" min-width="90" />
|
||||
<el-table-column prop="outAvgWeight" label="均重(t)" min-width="90" />
|
||||
</el-table-column>
|
||||
<el-table-column label="消耗统计" align="center">
|
||||
<el-table-column prop="lossCount" label="数量" min-width="70" />
|
||||
<el-table-column prop="lossTotalWeight" label="总重(t)" min-width="90" />
|
||||
<el-table-column prop="lossAvgWeight" label="均重(t)" min-width="90" />
|
||||
</el-table-column>
|
||||
<el-table-column label="品质比率" align="center">
|
||||
<el-table-column prop="passRate" label="成品率" min-width="75" />
|
||||
<el-table-column prop="passRate2" label="正品率" min-width="75" />
|
||||
<el-table-column prop="lossRate" label="损耗率" min-width="75" />
|
||||
<el-table-column prop="abRate" label="异常率" min-width="75" />
|
||||
</el-table-column>
|
||||
<el-table-column label="M卷统计" align="center">
|
||||
<el-table-column prop="mOutCount" label="产出数量" min-width="70" />
|
||||
<el-table-column prop="mOutTotalWeight" label="产出总重(t)" min-width="90" />
|
||||
<el-table-column prop="mOutAvgWeight" label="产出均重(t)" min-width="90" />
|
||||
<el-table-column prop="mLossCount" label="消耗数量" min-width="70" />
|
||||
<el-table-column prop="mLossTotalWeight" label="消耗总重(t)" min-width="90" />
|
||||
<el-table-column prop="mLossAvgWeight" label="消耗均重(t)" min-width="90" />
|
||||
<el-table-column prop="mPassRate" label="成品率" min-width="75" />
|
||||
<el-table-column prop="mLossRate" label="损耗率" min-width="75" />
|
||||
<el-table-column prop="mPassRate2" label="正品率" min-width="75" />
|
||||
<el-table-column prop="mAbRate" label="异常率" min-width="75" />
|
||||
</el-table-column>
|
||||
<el-table-column label="异常库位" align="center">
|
||||
<el-table-column prop="abTechCount" label="技术部" min-width="70" />
|
||||
<el-table-column prop="abMiniCount" label="小钢卷库" min-width="75" />
|
||||
<el-table-column prop="abRubbishCount" label="废品库" min-width="70" />
|
||||
<el-table-column prop="abReturnCount" label="退货库" min-width="70" />
|
||||
<el-table-column prop="abTechRate" label="技术部占比" min-width="85" />
|
||||
<el-table-column prop="abMiniRate" label="小钢卷占比" min-width="85" />
|
||||
<el-table-column prop="abRubbishRate" label="废品库占比" min-width="85" />
|
||||
<el-table-column prop="abReturnRate" label="退货库占比" min-width="85" />
|
||||
</el-table-column>
|
||||
<el-table-column label="M异常库位" align="center">
|
||||
<el-table-column prop="mAbTechCount" label="技术部" min-width="70" />
|
||||
<el-table-column prop="mAbMiniCount" label="小钢卷库" min-width="75" />
|
||||
<el-table-column prop="mAbRubbishCount" label="废品库" min-width="70" />
|
||||
<el-table-column prop="mAbReturnCount" label="退货库" min-width="70" />
|
||||
<el-table-column prop="mAbTechRate" label="技术部占比" min-width="85" />
|
||||
<el-table-column prop="mAbMiniRate" label="小钢卷占比" min-width="85" />
|
||||
<el-table-column prop="mAbRubbishRate" label="废品库占比" min-width="85" />
|
||||
<el-table-column prop="mAbReturnRate" label="退货库占比" min-width="85" />
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { listLightCoil } from '@/api/wms/coil'
|
||||
import { listLightPendingAction } from '@/api/wms/pendingAction'
|
||||
import MemoInput from '@/components/MemoInput'
|
||||
import MutiSelect from '@/components/MutiSelect'
|
||||
import { calcSummary, calcMSummary, calcAbSummary } from '@/views/wms/report/js/calc'
|
||||
|
||||
export default {
|
||||
name: 'LineReport',
|
||||
components: { MemoInput, MutiSelect },
|
||||
dicts: ['coil_material', 'coil_manufacturer', 'coil_quality_status'],
|
||||
data() {
|
||||
const addZero = (num) => num.toString().padStart(2, '0')
|
||||
const now = new Date()
|
||||
const monthStart = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-01 00:00:00`
|
||||
const monthEnd = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-${addZero(now.getDate())} 23:59:59`
|
||||
|
||||
return {
|
||||
chartInstances: [],
|
||||
loading: false,
|
||||
startTime: monthStart,
|
||||
endTime: monthEnd,
|
||||
periodType: 'day',
|
||||
queryParams: {
|
||||
enterCoilNo: '', currentCoilNo: '', itemName: '', itemSpecification: '',
|
||||
itemMaterial: '', itemManufacturer: '', qualityStatusCsv: ''
|
||||
},
|
||||
actionTypes: '',
|
||||
lineOptions: [
|
||||
{ label: '酸轧线', value: '11,120,201,520' }, { label: '镀锌线', value: '202,501,521' },
|
||||
{ label: '双机架', value: '205,504,524' }, { label: '镀铬线', value: '206,505,525' },
|
||||
{ label: '拉矫线', value: '204,503,523' }, { label: '脱脂线', value: '203,502,522' }
|
||||
],
|
||||
allOutList: [],
|
||||
allLossList: [],
|
||||
periodData: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
periodLabel() {
|
||||
const map = { day: '日', week: '周', month: '月' }
|
||||
return map[this.periodType] || '日'
|
||||
},
|
||||
totalOutCount() {
|
||||
return this.periodData.reduce((s, i) => s + (i.outCount || 0), 0)
|
||||
},
|
||||
totalOutWeight() {
|
||||
return this.periodData.reduce((s, i) => s + (parseFloat(i.outTotalWeight) || 0), 0).toFixed(2)
|
||||
},
|
||||
totalLossCount() {
|
||||
return this.periodData.reduce((s, i) => s + (i.lossCount || 0), 0)
|
||||
},
|
||||
totalLossWeight() {
|
||||
return this.periodData.reduce((s, i) => s + (parseFloat(i.lossTotalWeight) || 0), 0).toFixed(2)
|
||||
},
|
||||
chartConfigs() {
|
||||
return [
|
||||
// ====== Row 1: 数量/总重 ======
|
||||
{
|
||||
title: '产出与消耗数量/总重',
|
||||
series: [
|
||||
{ key: 'outCount', label: '产出数量(卷)', color: '#409eff', yAxisIndex: 0 },
|
||||
{ key: 'lossCount', label: '消耗数量(卷)', color: '#f56c6c', yAxisIndex: 0 },
|
||||
{ key: 'outTotalWeight', label: '产出总重(t)', color: '#67c23a', yAxisIndex: 1 },
|
||||
{ key: 'lossTotalWeight', label: '消耗总重(t)', color: '#909399', yAxisIndex: 1 }
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', name: '数量(卷)' },
|
||||
{ type: 'value', name: '重量(t)' }
|
||||
],
|
||||
height: '280px'
|
||||
},
|
||||
{
|
||||
title: 'M-卷产出与消耗数量/总重',
|
||||
series: [
|
||||
{ key: 'mOutCount', label: 'M-产出数量(卷)', color: '#409eff', yAxisIndex: 0 },
|
||||
{ key: 'mLossCount', label: 'M-消耗数量(卷)', color: '#f56c6c', yAxisIndex: 0 },
|
||||
{ key: 'mOutTotalWeight', label: 'M-产出总重(t)', color: '#67c23a', yAxisIndex: 1 },
|
||||
{ key: 'mLossTotalWeight', label: 'M-消耗总重(t)', color: '#909399', yAxisIndex: 1 }
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', name: '数量(卷)' },
|
||||
{ type: 'value', name: '重量(t)' }
|
||||
],
|
||||
height: '280px'
|
||||
},
|
||||
// ====== Row 2: 均重 ======
|
||||
{
|
||||
title: '产出与消耗均重',
|
||||
series: [
|
||||
{ key: 'outAvgWeight', label: '产出均重(t)', color: '#e6a23c' },
|
||||
{ key: 'lossAvgWeight', label: '消耗均重(t)', color: '#b37feb' }
|
||||
],
|
||||
yAxis: [{ type: 'value', name: '均重(t)' }],
|
||||
height: '280px'
|
||||
},
|
||||
{
|
||||
title: 'M-卷均重',
|
||||
series: [
|
||||
{ key: 'mOutAvgWeight', label: 'M-产出均重(t)', color: '#e6a23c' },
|
||||
{ key: 'mLossAvgWeight', label: 'M-消耗均重(t)', color: '#b37feb' }
|
||||
],
|
||||
yAxis: [{ type: 'value', name: '均重(t)' }],
|
||||
height: '280px'
|
||||
},
|
||||
// ====== Row 3: 品质比率 ======
|
||||
{
|
||||
title: '品质比率',
|
||||
series: [
|
||||
{ key: 'passRate', label: '成品率', color: '#67c23a', percent: true },
|
||||
{ key: 'passRate2', label: '正品率', color: '#409eff', percent: true },
|
||||
{ key: 'lossRate', label: '损耗率', color: '#f56c6c', percent: true },
|
||||
{ key: 'abRate', label: '异常率', color: '#e6a23c', percent: true }
|
||||
],
|
||||
yAxis: [{ type: 'value', name: '%', axisLabel: { formatter: '{value}%' }}],
|
||||
height: '280px'
|
||||
},
|
||||
{
|
||||
title: 'M-卷品质比率',
|
||||
series: [
|
||||
{ key: 'mPassRate', label: 'M-成品率', color: '#67c23a', percent: true },
|
||||
{ key: 'mPassRate2', label: 'M-正品率', color: '#409eff', percent: true },
|
||||
{ key: 'mLossRate', label: 'M-损耗率', color: '#f56c6c', percent: true },
|
||||
{ key: 'mAbRate', label: 'M-异常率', color: '#e6a23c', percent: true }
|
||||
],
|
||||
yAxis: [{ type: 'value', name: '%', axisLabel: { formatter: '{value}%' }}],
|
||||
height: '280px'
|
||||
},
|
||||
// ====== Row 4: 异常库位 ======
|
||||
{
|
||||
title: '异常库位分布(钢卷数与占比)',
|
||||
series: [
|
||||
{ key: 'abTechCount', label: '技术部', color: '#409eff', yAxisIndex: 0 },
|
||||
{ key: 'abMiniCount', label: '小钢卷库', color: '#67c23a', yAxisIndex: 0 },
|
||||
{ key: 'abRubbishCount', label: '废品库', color: '#e6a23c', yAxisIndex: 0 },
|
||||
{ key: 'abReturnCount', label: '退货库', color: '#f56c6c', yAxisIndex: 0 },
|
||||
{ key: 'abTechRate', label: '技术部占比', color: '#409eff', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'abMiniRate', label: '小钢卷占比', color: '#67c23a', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'abRubbishRate', label: '废品库占比', color: '#e6a23c', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'abReturnRate', label: '退货库占比', color: '#f56c6c', yAxisIndex: 1, percent: true, dash: true }
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', name: '钢卷数' },
|
||||
{ type: 'value', name: '占比(%)', axisLabel: { formatter: '{value}%' }}
|
||||
],
|
||||
height: '280px'
|
||||
},
|
||||
{
|
||||
title: 'M-异常库位分布(钢卷数与占比)',
|
||||
series: [
|
||||
{ key: 'mAbTechCount', label: '技术部', color: '#409eff', yAxisIndex: 0 },
|
||||
{ key: 'mAbMiniCount', label: '小钢卷库', color: '#67c23a', yAxisIndex: 0 },
|
||||
{ key: 'mAbRubbishCount', label: '废品库', color: '#e6a23c', yAxisIndex: 0 },
|
||||
{ key: 'mAbReturnCount', label: '退货库', color: '#f56c6c', yAxisIndex: 0 },
|
||||
{ key: 'mAbTechRate', label: '技术部占比', color: '#409eff', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'mAbMiniRate', label: '小钢卷占比', color: '#67c23a', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'mAbRubbishRate', label: '废品库占比', color: '#e6a23c', yAxisIndex: 1, percent: true, dash: true },
|
||||
{ key: 'mAbReturnRate', label: '退货库占比', color: '#f56c6c', yAxisIndex: 1, percent: true, dash: true }
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', name: '钢卷数' },
|
||||
{ type: 'value', name: '占比(%)', axisLabel: { formatter: '{value}%' }}
|
||||
],
|
||||
height: '280px'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
periodData: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.$nextTick(() => this.renderCharts())
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.handleQuery()
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => this.renderCharts())
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disposeCharts()
|
||||
},
|
||||
methods: {
|
||||
// ====== 时间快捷选择 ======
|
||||
setQuickPeriod(type) {
|
||||
const now = new Date(); const addZero = n => n.toString().padStart(2, '0')
|
||||
const fd = d => `${d.getFullYear()}-${addZero(d.getMonth() + 1)}-${addZero(d.getDate())}`
|
||||
switch (type) {
|
||||
case 'thisMonth':
|
||||
this.startTime = `${fd(new Date(now.getFullYear(), now.getMonth(), 1))} 00:00:00`
|
||||
this.endTime = `${fd(now)} 23:59:59`
|
||||
this.periodType = 'day'
|
||||
break
|
||||
case 'lastMonth': {
|
||||
const lm = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||
const lme = new Date(now.getFullYear(), now.getMonth(), 0)
|
||||
this.startTime = `${fd(lm)} 00:00:00`
|
||||
this.endTime = `${fd(lme)} 23:59:59`
|
||||
this.periodType = 'day'
|
||||
break
|
||||
}
|
||||
case 'thisQuarter': {
|
||||
const qs = Math.floor(now.getMonth() / 3) * 3
|
||||
this.startTime = `${fd(new Date(now.getFullYear(), qs, 1))} 00:00:00`
|
||||
this.endTime = `${fd(now)} 23:59:59`
|
||||
this.periodType = 'month'
|
||||
break
|
||||
}
|
||||
case 'thisYear':
|
||||
this.startTime = `${now.getFullYear()}-01-01 00:00:00`
|
||||
this.endTime = `${fd(now)} 23:59:59`
|
||||
this.periodType = 'month'
|
||||
break
|
||||
case 'lastYear':
|
||||
this.startTime = `${now.getFullYear() - 1}-01-01 00:00:00`
|
||||
this.endTime = `${now.getFullYear() - 1}-12-31 23:59:59`
|
||||
this.periodType = 'month'
|
||||
break
|
||||
}
|
||||
this.handleQuery()
|
||||
},
|
||||
|
||||
// ====== 数据获取 ======
|
||||
handleQuery() {
|
||||
this.fetchData()
|
||||
},
|
||||
// 切换周期类型时仅前端重新汇总,无需重新请求接口
|
||||
recalcPeriod() {
|
||||
if (this.allOutList.length === 0 && this.allLossList.length === 0) return
|
||||
this.disposeCharts()
|
||||
this.chartInstances = []
|
||||
this.splitByPeriod()
|
||||
this.$nextTick(() => this.renderCharts())
|
||||
},
|
||||
async fetchData() {
|
||||
this.loading = true
|
||||
this.disposeCharts()
|
||||
this.chartInstances = []
|
||||
this.periodData = []
|
||||
|
||||
try {
|
||||
const baseQuery = {
|
||||
enterCoilNo: this.queryParams.enterCoilNo,
|
||||
currentCoilNo: this.queryParams.currentCoilNo,
|
||||
itemName: this.queryParams.itemName,
|
||||
itemSpecification: this.queryParams.itemSpecification,
|
||||
itemMaterial: this.queryParams.itemMaterial,
|
||||
itemManufacturer: this.queryParams.itemManufacturer,
|
||||
qualityStatusCsv: this.queryParams.qualityStatusCsv
|
||||
}
|
||||
|
||||
// 1. 获取整个时间范围内的所有待处理操作
|
||||
const actionRes = await listLightPendingAction({
|
||||
...baseQuery,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
actionTypes: this.actionTypes,
|
||||
actionStatus: 2
|
||||
})
|
||||
|
||||
const actions = actionRes.data || []
|
||||
if (actions.length === 0) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 提取产出和消耗的钢卷ID
|
||||
const outIds = actions.map(i => i.processedCoilIds).filter(Boolean).join(',')
|
||||
const lossIds = actions.filter(i => i.coilId).map(i => i.coilId).join(',')
|
||||
|
||||
if (!outIds) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 批量获取产出和消耗钢卷数据
|
||||
const mapItems = list => (list || []).map(item => {
|
||||
const [th, w] = item.specification?.split('*') || []
|
||||
return { ...item, computedThickness: parseFloat(th), computedWidth: parseFloat(w) }
|
||||
})
|
||||
|
||||
const [outRes, lossRes] = await Promise.all([
|
||||
listLightCoil({
|
||||
...baseQuery, coilIds: outIds, startTime: '', endTime: '',
|
||||
selectType: 'product', pageSize: 99999, pageNum: 1
|
||||
}),
|
||||
lossIds ? listLightCoil({
|
||||
...baseQuery, coilIds: lossIds, startTime: '', endTime: '',
|
||||
selectType: 'raw_material', pageSize: 99999, pageNum: 1
|
||||
}) : Promise.resolve({ data: [] })
|
||||
])
|
||||
|
||||
this.allOutList = mapItems(outRes)
|
||||
this.allLossList = mapItems(lossRes)
|
||||
|
||||
// 4. 按周期拆分数据
|
||||
this.splitByPeriod()
|
||||
|
||||
// 5. 渲染图表
|
||||
this.$nextTick(() => this.renderCharts())
|
||||
} catch (e) {
|
||||
console.error('数据获取失败:', e)
|
||||
this.$message.error('数据获取失败')
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
// ====== 周期拆分 ======
|
||||
splitByPeriod() {
|
||||
const periods = this.generatePeriods()
|
||||
this.periodData = periods.map(p => {
|
||||
const st = p.start.getTime(); const et = p.end.getTime()
|
||||
|
||||
const outList = this.allOutList.filter(c => {
|
||||
const t = new Date(c.createTime || c.createDate).getTime()
|
||||
return t >= st && t <= et
|
||||
})
|
||||
const lossList = this.allLossList.filter(c => {
|
||||
const t = new Date(c.createTime || c.createDate).getTime()
|
||||
return t >= st && t <= et
|
||||
})
|
||||
|
||||
const summary = calcSummary(outList, lossList)
|
||||
const mSummary = calcMSummary(outList, lossList)
|
||||
const abSummary = calcAbSummary(outList)
|
||||
const abMap = {}
|
||||
abSummary.forEach(i => { abMap[i.label] = i.value })
|
||||
// M-异常库位(处理M后的非M卷产出)
|
||||
const isMcoil = c => c.currentCoilNo && c.currentCoilNo.includes('M') && c.currentCoilNo.indexOf('M') > 4
|
||||
const nonMOut = outList.filter(c => !isMcoil(c))
|
||||
const mAbSummary = calcAbSummary(nonMOut)
|
||||
const mAbMap = {}
|
||||
mAbSummary.forEach(i => { mAbMap[i.label] = i.value })
|
||||
|
||||
return {
|
||||
periodLabel: p.label,
|
||||
// 基础统计
|
||||
...summary,
|
||||
// M卷统计
|
||||
mOutCount: mSummary.outCount,
|
||||
mOutTotalWeight: mSummary.outTotalWeight,
|
||||
mLossCount: mSummary.lossCount,
|
||||
mLossTotalWeight: mSummary.lossTotalWeight,
|
||||
mOutAvgWeight: mSummary.outAvgWeight,
|
||||
mLossAvgWeight: mSummary.lossAvgWeight,
|
||||
mPassRate: mSummary.passRate,
|
||||
mLossRate: mSummary.lossRate,
|
||||
mPassRate2: mSummary.passRate2,
|
||||
mAbRate: mSummary.abRate,
|
||||
// 异常库位统计
|
||||
abTechCount: abMap['技术部钢卷数'] || 0,
|
||||
abMiniCount: abMap['小钢卷库钢卷数'] || 0,
|
||||
abRubbishCount: abMap['废品库钢卷数'] || 0,
|
||||
abReturnCount: abMap['退货库钢卷数'] || 0,
|
||||
abTechRate: abMap['技术部占比'] || '0.00%',
|
||||
abMiniRate: abMap['小钢卷库占比'] || '0.00%',
|
||||
abRubbishRate: abMap['废品库占比'] || '0.00%',
|
||||
abReturnRate: abMap['退货库占比'] || '0.00%',
|
||||
// M异常库位统计
|
||||
mAbTechCount: mAbMap['技术部钢卷数'] || 0,
|
||||
mAbMiniCount: mAbMap['小钢卷库钢卷数'] || 0,
|
||||
mAbRubbishCount: mAbMap['废品库钢卷数'] || 0,
|
||||
mAbReturnCount: mAbMap['退货库钢卷数'] || 0,
|
||||
mAbTechRate: mAbMap['技术部占比'] || '0.00%',
|
||||
mAbMiniRate: mAbMap['小钢卷库占比'] || '0.00%',
|
||||
mAbRubbishRate: mAbMap['废品库占比'] || '0.00%',
|
||||
mAbReturnRate: mAbMap['退货库占比'] || '0.00%'
|
||||
}
|
||||
})
|
||||
},
|
||||
generatePeriods() {
|
||||
const periods = []
|
||||
const start = new Date(this.startTime)
|
||||
const end = new Date(this.endTime)
|
||||
|
||||
if (this.periodType === 'day') {
|
||||
const endMs = end.getTime()
|
||||
const cur = new Date(start)
|
||||
while (cur.getTime() <= endMs) {
|
||||
const dayEnd = new Date(cur)
|
||||
dayEnd.setHours(23, 59, 59, 999)
|
||||
periods.push({
|
||||
label: `${cur.getFullYear()}-${String(cur.getMonth() + 1).padStart(2, '0')}-${String(cur.getDate()).padStart(2, '0')}`,
|
||||
start: new Date(cur),
|
||||
end: new Date(Math.min(dayEnd.getTime(), endMs))
|
||||
})
|
||||
cur.setDate(cur.getDate() + 1)
|
||||
cur.setHours(0, 0, 0, 0)
|
||||
}
|
||||
} else if (this.periodType === 'week') {
|
||||
const endMs = end.getTime()
|
||||
const cur = new Date(start)
|
||||
const day = cur.getDay()
|
||||
const diff = day === 0 ? -6 : 1 - day
|
||||
cur.setDate(cur.getDate() + diff)
|
||||
cur.setHours(0, 0, 0, 0)
|
||||
|
||||
while (cur.getTime() <= endMs) {
|
||||
const weekEnd = new Date(cur)
|
||||
weekEnd.setDate(weekEnd.getDate() + 6)
|
||||
weekEnd.setHours(23, 59, 59, 999)
|
||||
periods.push({
|
||||
label: `${cur.getFullYear()}-W${this.getWeekNumber(cur)}`,
|
||||
start: new Date(Math.max(cur.getTime(), start.getTime())),
|
||||
end: new Date(Math.min(weekEnd.getTime(), endMs))
|
||||
})
|
||||
cur.setDate(cur.getDate() + 7)
|
||||
}
|
||||
} else if (this.periodType === 'month') {
|
||||
const endMs = end.getTime()
|
||||
const cur = new Date(start.getFullYear(), start.getMonth(), 1)
|
||||
while (cur.getTime() <= endMs) {
|
||||
const monthEnd = new Date(cur.getFullYear(), cur.getMonth() + 1, 0, 23, 59, 59, 999)
|
||||
periods.push({
|
||||
label: `${cur.getFullYear()}-${String(cur.getMonth() + 1).padStart(2, '0')}`,
|
||||
start: new Date(Math.max(cur.getTime(), start.getTime())),
|
||||
end: new Date(Math.min(monthEnd.getTime(), endMs))
|
||||
})
|
||||
cur.setMonth(cur.getMonth() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return periods
|
||||
},
|
||||
getWeekNumber(d) {
|
||||
const temp = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||||
temp.setDate(temp.getDate() + 3 - (temp.getDay() + 6) % 7)
|
||||
const week1 = new Date(temp.getFullYear(), 0, 4)
|
||||
return Math.round(((temp - week1) / 86400000 + week1.getDay() + 1) / 7)
|
||||
},
|
||||
|
||||
// ====== 图表渲染 ======
|
||||
renderCharts() {
|
||||
this.disposeCharts()
|
||||
if (this.periodData.length === 0) return
|
||||
|
||||
const xLabels = this.periodData.map(p => p.periodLabel)
|
||||
|
||||
this.chartConfigs.forEach((cfg, idx) => {
|
||||
const dom = this.$refs['chart_' + idx]
|
||||
if (!dom || !dom[0]) return
|
||||
|
||||
const chart = echarts.init(dom[0])
|
||||
this.chartInstances.push(chart)
|
||||
|
||||
const yAxis = cfg.yAxis || [{ type: 'value' }]
|
||||
const series = cfg.series.map(s => ({
|
||||
name: s.label,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: s.dash ? 'diamond' : 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { width: 2, color: s.color, type: s.dash ? 'dashed' : 'solid' },
|
||||
itemStyle: { color: s.color },
|
||||
yAxisIndex: s.yAxisIndex || 0,
|
||||
data: this.periodData.map(p => {
|
||||
const raw = p[s.key]
|
||||
if (s.percent) {
|
||||
return parseFloat(raw || '0') || 0
|
||||
}
|
||||
return raw !== undefined && raw !== null ? parseFloat(raw) || raw : 0
|
||||
})
|
||||
}))
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function(params) {
|
||||
let html = params[0].axisValue + '<br/>'
|
||||
params.forEach(p => {
|
||||
const val = typeof p.value === 'number' ? p.value.toFixed(2) : p.value
|
||||
html += p.marker + ' ' + p.seriesName + ':' + val + '<br/>'
|
||||
})
|
||||
return html
|
||||
}
|
||||
},
|
||||
legend: { data: cfg.series.map(s => s.label), top: 5 },
|
||||
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
|
||||
xAxis: { type: 'category', data: xLabels, axisLabel: { rotate: xLabels.length > 15 ? 45 : 0, fontSize: 11 }},
|
||||
yAxis: yAxis,
|
||||
dataZoom: [
|
||||
{ type: 'inside', start: 0, end: 100 },
|
||||
{ type: 'slider', start: 0, end: 100, height: 20, bottom: 5 }
|
||||
],
|
||||
series: series
|
||||
}
|
||||
|
||||
chart.setOption(option)
|
||||
|
||||
const handler = () => chart.resize()
|
||||
window.addEventListener('resize', handler)
|
||||
chart._resizeHandler = handler
|
||||
})
|
||||
},
|
||||
disposeCharts() {
|
||||
this.chartInstances.forEach(chart => {
|
||||
if (chart._resizeHandler) {
|
||||
window.removeEventListener('resize', chart._resizeHandler)
|
||||
}
|
||||
chart.dispose()
|
||||
})
|
||||
this.chartInstances = []
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-report { min-width: 900px; }
|
||||
.filter-area { background: #fff; padding: 8px 12px; margin-bottom: 8px; border-bottom: 1px solid #e4e7ed; }
|
||||
.filter-form { margin-bottom: 0; }
|
||||
.filter-form .el-form-item { margin-bottom: 6px; margin-right: 4px; }
|
||||
.filter-form >>> .el-form-item__label { font-size: 12px; }
|
||||
.time-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding-top: 6px; border-top: 1px solid #ebeef5; }
|
||||
.time-label { font-size: 12px; color: #909399; font-weight: 600; white-space: nowrap; }
|
||||
.period-label { color: #409eff; margin-left: 4px; }
|
||||
.time-sep { color: #c0c4cc; font-size: 12px; }
|
||||
.quick-btns { margin-left: auto; }
|
||||
.explain-bar { display: flex; align-items: flex-start; gap: 6px; padding: 6px 12px; margin-bottom: 8px; background: #f0f9ff; border-radius: 4px; font-size: 12px; color: #606266; line-height: 1.6; }
|
||||
.explain-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; min-width: 16px; border-radius: 50%; background: #909399; color: #fff; font-size: 11px; font-weight: 700; font-style: normal; margin-top: 2px; }
|
||||
.explain-text { flex: 1; }
|
||||
.explain-divider { display: inline-block; width: 0; height: 0; margin: 0 2px; border-left: 1px solid #c0c4cc; }
|
||||
|
||||
.summary-bar { display: flex; gap: 16px; padding: 10px 12px; background: #f5f7fa; border-radius: 4px; margin-bottom: 10px; flex-wrap: wrap; }
|
||||
.summary-item { display: flex; flex-direction: column; align-items: center; min-width: 100px; }
|
||||
.summary-label { font-size: 12px; color: #909399; white-space: nowrap; }
|
||||
.summary-value { font-size: 16px; font-weight: 600; color: #303133; margin-top: 2px; white-space: nowrap; }
|
||||
|
||||
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.group-title { font-size: 13px; font-weight: 600; color: #303133; padding: 5px 0; margin-bottom: 6px; border-bottom: 2px solid #409eff; }
|
||||
.chart-container { width: 100%; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||||
|
||||
.detail-section { margin-top: 8px; }
|
||||
.table-wrapper { overflow-x: auto; }
|
||||
.detail-section >>> .el-table th { background-color: #f5f7fa; font-size: 12px; }
|
||||
.detail-section >>> .el-table td { font-size: 12px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user