feat(cost+wms): 新增生产指标标签功能并优化钢卷成本展示

1. 为生产指标实体、BO、VO新增tags标签字段并完善MyBatis映射
2. 在生产指标查询中添加标签模糊筛选条件
3. 新增生产指标计算结果API接口
4. 优化成本综合页面:支持标签字段的增改查,新增指标结果批量保存逻辑
5. 移除废弃的costDataService文件,重构钢卷详情页成本展示模块,新增加工路径可视化和吨钢成本计算展示
6. 注释并禁用了原有的检验任务相关代码逻辑
This commit is contained in:
2026-06-17 15:09:08 +08:00
parent 3719416cbf
commit 768b18c22a
11 changed files with 748 additions and 471 deletions

View File

@@ -180,6 +180,7 @@
<el-table :data="mgrList" border stripe size="mini">
<el-table-column label="名称" prop="metricName" width="150" />
<el-table-column label="公式" prop="metricFormula" min-width="200" />
<el-table-column label="标签" prop="tags" width="100" />
<el-table-column label="单位" prop="remark" width="70" />
<el-table-column label="操作" width="100">
<template slot-scope="s">
@@ -206,6 +207,7 @@
</template>
</div>
<el-form-item label="单位"><el-input v-model="defForm.unit" placeholder="如 %" /></el-form-item>
<el-form-item label="标签"><el-input v-model="defForm.tags" placeholder="请输入标签" /></el-form-item>
<el-form-item label="使用单价"><el-switch v-model="defForm.usePrice" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="单价"><el-input v-model="defForm.metricValue" type="number" placeholder="请输入单价" /></el-form-item>
</el-form>
@@ -324,6 +326,7 @@
import { listProdReport, getProdReport, addProdReport, updateProdReport, delProdReport, copyProdReport } from "@/api/cost/prodReport"
import { listProdDetail, batchSaveProdDetail } from "@/api/cost/prodDetail"
import { listProdMetric, addProdMetric, updateProdMetric, delProdMetric, getProdMetric } from "@/api/cost/prodMetric"
import { listProdMetricResult, batchSaveProdMetricResult } from "@/api/cost/prodMetricResult"
import { listItem } from "@/api/cost/item"
import { listLightPendingAction } from "@/api/wms/pendingAction"
import { getCoilStatisticsList } from "@/api/wms/coil"
@@ -608,7 +611,7 @@ export default {
},
doPickMetric() {
this.selMp.forEach(m => {
this.allCols.push({ $type: 'metric', metricId: m.metricId, metricName: m.metricName, metricFormula: m.metricFormula, unit: m.remark||'', isShift: false, color: null, metricValue: m.metricValue, usePrice: m.usePrice })
this.allCols.push({ $type: 'metric', metricId: m.metricId, metricName: m.metricName, metricFormula: m.metricFormula, unit: m.remark||'', tags: m.tags || '', isShift: false, color: null, metricValue: m.metricValue, usePrice: m.usePrice })
})
this.metricPickOpen = false
},
@@ -618,15 +621,15 @@ export default {
this.mgrList = this._allMetricDefs || []
this.mgrOpen = true
},
addMetricDef() { this.defForm = { metricId: null, metricName: '', metricFormula: '', unit: '', usePrice: 0, metricValue: '' }; this.defTitle = '新增指标'; this.defOpen = true },
editMetricDef(row) { this.defForm = { metricId: row.metricId, metricName: row.metricName, metricFormula: row.metricFormula, unit: row.remark||'', usePrice: row.usePrice || 0, metricValue: row.metricValue || '' }; this.defTitle = '编辑指标'; this.defOpen = true },
addMetricDef() { this.defForm = { metricId: null, metricName: '', metricFormula: '', unit: '', tags: '', usePrice: 0, metricValue: '' }; this.defTitle = '新增指标'; this.defOpen = true },
editMetricDef(row) { this.defForm = { metricId: row.metricId, metricName: row.metricName, metricFormula: row.metricFormula, unit: row.remark||'', tags: row.tags || '', usePrice: row.usePrice || 0, metricValue: row.metricValue || '' }; this.defTitle = '编辑指标'; this.defOpen = true },
async submitMetricDef() {
const f = this.defForm
if (!f.metricName) { this.$modal.msgWarning('请输入指标名称'); return }
if (f.metricId) {
await updateProdMetric({ metricId: f.metricId, metricName: f.metricName, metricFormula: f.metricFormula, remark: f.unit, usePrice: f.usePrice, metricValue: f.metricValue || 0 })
await updateProdMetric({ metricId: f.metricId, metricName: f.metricName, metricFormula: f.metricFormula, remark: f.unit, tags: f.tags || '', usePrice: f.usePrice, metricValue: f.metricValue || 0 })
} else {
await addProdMetric({ reportId: this.activeReport.reportId, metricCode: f.metricName, metricName: f.metricName, metricFormula: f.metricFormula, metricValue: f.metricValue || 0, remark: f.unit || '', usePrice: f.usePrice })
await addProdMetric({ reportId: this.activeReport.reportId, metricCode: f.metricName, metricName: f.metricName, metricFormula: f.metricFormula, metricValue: f.metricValue || 0, remark: f.unit || '', tags: f.tags || '', usePrice: f.usePrice })
}
this.defOpen = false; this.$modal.msgSuccess('保存成功')
await this.openMetricMgr()
@@ -676,7 +679,7 @@ export default {
// ensure metric definitions exist in DB for all metric columns before saving config
for (const mc of metricCols) {
if (!mc.metricId) {
const r = await addProdMetric({ reportId: rid, metricCode: mc.metricName, metricName: mc.metricName, metricFormula: mc.metricFormula || '', metricValue: 0, remark: mc.unit || '' })
const r = await addProdMetric({ reportId: rid, metricCode: mc.metricName, metricName: mc.metricName, metricFormula: mc.metricFormula || '', metricValue: 0, remark: mc.unit || '', tags: mc.tags || '' })
mc.metricId = r.data && r.data.metricId || r.metricId
}
}
@@ -721,7 +724,7 @@ export default {
if (!def && c.id) {
try { const r = await getProdMetric(c.id); if (r.data) { def = r.data; this._allMetricDefs.push(def) } } catch(e) {}
}
if (def) cols.push({ $type: 'metric', metricId: def.metricId, metricName: def.metricName, metricFormula: def.metricFormula, unit: def.remark||'', isShift: !!c.s, color: c.c || null, metricValue: def.metricValue, usePrice: def.usePrice })
if (def) cols.push({ $type: 'metric', metricId: def.metricId, metricName: def.metricName, metricFormula: def.metricFormula, unit: def.remark||'', tags: def.tags || '', isShift: !!c.s, color: c.c || null, metricValue: def.metricValue, usePrice: def.usePrice })
}
}
this.allCols = cols
@@ -782,6 +785,7 @@ export default {
async saveGrid() {
const rid = this.activeReport.reportId; if (!rid) return; this.saving = true
try {
this.recalcAll()
const exist = await listProdDetail({ reportId: rid, pageNum: 1, pageSize: 9999 })
const ids = (exist.rows||[]).map(d => d.detailId)
const detailCols = this.allCols.filter(c => c.$type === 'detail'); const detailList = []
@@ -793,6 +797,31 @@ export default {
})
})
await batchSaveProdDetail({ detailIds: ids, prodDetailList: detailList })
const metricCols = this.allCols.filter(c => c.$type === 'metric' && c.metricId)
if (metricCols.length) {
const existMR = await listProdMetricResult({ reportId: rid, pageNum: 1, pageSize: 99999 })
const resultIds = (existMR.rows || []).map(r => r.resultId)
const metricResultList = []
this.gridRows.forEach(row => {
if (!row.detailDate) return
metricCols.forEach(m => {
const push = (teamGroup, value) => {
if (value != null && value !== '') {
metricResultList.push({ reportId: rid, metricId: m.metricId, metricDate: row.detailDate, teamGroup, calcValue: value, tags: m.tags || '' })
}
}
if (m.isShift) {
push('1', row['mv' + m.mIdx + '_1'])
push('2', row['mv' + m.mIdx + '_2'])
} else {
push('0', row['mv' + m.mIdx])
}
})
})
await batchSaveProdMetricResult({ resultIds, prodMetricResultList: metricResultList })
}
this.$modal.msgSuccess("保存成功"); await this.loadGrid()
} catch(e) { this.$modal.msgError("保存失败") } finally { this.saving = false }
},
@@ -815,7 +844,7 @@ export default {
else if (sc.t === 'm') {
const sid = String(sc.id)
let def = this._allMetricDefs.find(m=>String(m.metricId)===sid)
if (def && !usedMids.has(sid)) { usedMids.add(sid); this.allCols.push({ $type:'metric', metricId:String(def.metricId), metricName:def.metricName, metricFormula:def.metricFormula, unit:def.remark||'', isShift:!!sc.s, color:sc.c||null, metricValue: def.metricValue, usePrice: def.usePrice }) }
if (def && !usedMids.has(sid)) { usedMids.add(sid); this.allCols.push({ $type:'metric', metricId:String(def.metricId), metricName:def.metricName, metricFormula:def.metricFormula, unit:def.remark||'', tags: def.tags || '', isShift:!!sc.s, color:sc.c||null, metricValue: def.metricValue, usePrice: def.usePrice }) }
}
})
this.copyCfgOpen = false; let mi = 0; this.allCols.forEach(c => { if (c.$type === 'metric') c.mIdx = mi++ })

View File

@@ -1,344 +0,0 @@
import { listProdReport } from '@/api/cost/prodReport'
import { listProdDetail } from '@/api/cost/prodDetail'
import { listProdMetric } from '@/api/cost/prodMetric'
import { listItem } from '@/api/cost/item'
import { listPrice } from '@/api/cost/price'
/**
* 安全计算公式值
* @param {string} f - 公式表达式
* @returns {number|null}
*/
function evalFormula(f) {
const s = f.replace(/[^0-9+\-*/.()\s]/g, '')
if (!s) return null
try {
const r = new Function('return (' + s + ')')()
return isFinite(r) ? Math.round(r * 10000) / 10000 : null
} catch (e) {
return null
}
}
/**
* 将明细数据按日期分组,并计算指标公式,生成完整行数据
*
* @param {Object} report - 生产月报
* @param {Array} details - 生产成本明细列表
* @param {Array} metrics - 生产指标列表
* @param {Array} items - 成本项目配置列表
* @returns {Array<Object>} data - 完整行数据数组
*/
function buildComputedRows(report, details, metrics, items) {
const itemCodeMap = {} // itemId -> itemCode
items.forEach(item => {
itemCodeMap[item.itemId] = item.itemCode
})
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
// 1. 按 detailDate 分组明细
const dateMap = {}
details.forEach(d => {
if (!d.detailDate) return
if (!dateMap[d.detailDate]) {
dateMap[d.detailDate] = { detailDate: d.detailDate }
}
const code = itemCodeMap[d.itemId]
const sfx = d.shift && d.shift !== '0' ? '_' + d.shift : ''
if (code) {
dateMap[d.detailDate]['q_' + code + sfx] = parseFloat(d.quantity) || 0
}
})
const rows = Object.values(dateMap).sort((a, b) => a.detailDate.localeCompare(b.detailDate))
// 2. 补齐每个 itemCode 的总量(跨班次汇总)
items.forEach(item => {
const code = item.itemCode
if (!code) return
rows.forEach(row => {
const v1 = row['q_' + code + '_1'] || 0
const v2 = row['q_' + code + '_2'] || 0
if (v1 || v2) {
row['q_' + code] = v1 + v2
}
})
})
// 3. 逐行计算指标公式
const rp = report || {}
rows.forEach(row => {
metrics.forEach((m, mi) => {
if (!m.metricFormula) return
let f = m.metricFormula
// 替换其他指标的引用(按定义顺序,前面的指标可被后面的引用)
for (let pmi = 0; pmi < mi; pmi++) {
const pm = metrics[pmi]
if (!pm.metricName) continue
const pn = esc(pm.metricName)
const pv = row['mv_' + pm.metricName]
if (pv != null) {
f = f.replace(new RegExp('@\\{' + pn + '\\}', 'g'), pv)
}
}
// 替换明细项引用 @{itemCode}
items.forEach(item => {
if (!item.itemCode) return
const code = esc(item.itemCode)
const totalVal = parseFloat(row['q_' + item.itemCode]) || 0
const val1 = parseFloat(row['q_' + item.itemCode + '_1']) || 0
const val2 = parseFloat(row['q_' + item.itemCode + '_2']) || 0
f = f.replace(new RegExp('@\\{' + code + '\\}\\.甲班', 'g'), val1)
f = f.replace(new RegExp('@\\{' + code + '\\}\\.乙班', 'g'), val2)
f = f.replace(new RegExp('@\\{' + code + '\\}', 'g'), totalVal)
})
// 替换系统变量
f = f.replace(/input_weight/g, rp.inputWeight || 0)
.replace(/output_weight/g, rp.outputWeight || 0)
.replace(/price/g, (m.usePrice === 1) ? (m.metricValue || 0) : 0)
const result = evalFormula(f)
if (result != null) {
row['mv_' + m.metricName] = result
}
})
})
return rows
}
/**
* 获取指定日期和产线类型的全部成本数据
*
* @param {string} date - 日期,格式 yyyy-MM-dd如 "2024-01-15"
* @param {string} lineType - 产线类型,"1"=镀锌,"2"=酸轧
* @returns {Promise<{
* report: Object|null, // 生产月报
* metrics: Array, // 生产指标明细列表(公式列)
* details: Array, // 生产成本明细列表(原始数据)
* items: Array, // 成本项目配置列表(原始数据列定义)
* data: Array, // 完整行数据:按日期分组,包含原始值(q_xxx)和计算值(mv_xxx)
* prices: Array, // 成本单价历史列表
* detailMap: Object, // 按 itemId 分组的明细
* itemMap: Object, // 按 itemId 索引的项目配置
* metricMap: Object // 按 metricId 索引的指标
* }>}
*/
export async function getCostDataByDate(date, lineType) {
// 1. 查找指定日期和产线类型的生产月报
// 假设每个日期只有一条月报,所以取第一个, 需要将日期转为第一天的日期
const firstDay = new Date(date)
firstDay.setDate(1)
console.log(firstDay)
// 使用北京时间,转换为东八区时间
const year = firstDay.getFullYear()
const month = (firstDay.getMonth() + 1).toString().padStart(2, '0')
const firstDayStr = year + '-' + month + '-01 00:00:00'
const reportRes = await listProdReport({
reportDate: firstDayStr,
lineType: lineType,
pageNum: 1,
pageSize: 1
})
console.log(reportRes)
const reports = reportRes.rows || []
const report = reports.length > 0 ? reports[0] : null
// 2. 获取所有成本项目配置(用于映射 itemId
const itemRes = await listItem({ pageNum: 1, pageSize: 9999 })
const items = itemRes.rows || []
const itemMap = {}
items.forEach(item => {
itemMap[item.itemId] = item
})
// 3. 获取该日期下生效的单价历史
const priceRes = await listPrice({
effectiveDate: date,
pageNum: 1,
pageSize: 9999
})
const prices = priceRes.rows || []
// 如果没有找到月报,返回基础数据
if (!report) {
return {
report: null,
metrics: [],
details: [],
items,
data: [],
prices,
detailMap: {},
itemMap,
metricMap: {}
}
}
const reportId = report.reportId
// 4. 获取该月报的所有生产成本明细
const detailRes = await listProdDetail({
reportId,
pageNum: 1,
pageSize: 9999
})
const details = detailRes.rows || []
// 5. 构建按 itemId 分组的明细
const detailMap = {}
details.forEach(detail => {
const key = detail.itemId
if (!detailMap[key]) {
detailMap[key] = []
}
detailMap[key].push(detail)
})
// 6. 获取该月报的所有生产指标
const metricRes = await listProdMetric({
reportId,
pageNum: 1,
pageSize: 9999
})
const metrics = metricRes.rows || []
const metricMap = {}
metrics.forEach(metric => {
metricMap[metric.metricId] = metric
})
// 7. 构建完整行数据(原始值 + 公式计算值)
const data = buildComputedRows(report, details, metrics, items)
return {
report,
metrics,
details,
items,
data,
prices,
detailMap,
itemMap,
metricMap
}
}
/**
* 获取指定日期范围、产线类型的成本数据,支持多天批量查询
*
* @param {string} startDate - 开始日期 yyyy-MM-dd
* @param {string} endDate - 结束日期 yyyy-MM-dd
* @param {string} lineType - 产线类型
* @returns {Promise<Array<{ date: string, report, metrics, details, items, data, prices }>>}
*/
export async function getCostDataByDateRange(startDate, endDate, lineType) {
// 查询日期范围内的所有月报
const reportRes = await listProdReport({
lineType,
pageNum: 1,
pageSize: 9999
})
const allReports = (reportRes.rows || []).filter(r => {
return r.reportDate >= startDate && r.reportDate <= endDate
})
// 获取所有项目配置和单价(共享数据,只查一次)
const itemRes = await listItem({ pageNum: 1, pageSize: 9999 })
const items = itemRes.rows || []
const itemMap = {}
items.forEach(item => {
itemMap[item.itemId] = item
})
const priceRes = await listPrice({
pageNum: 1,
pageSize: 9999
})
const prices = priceRes.rows || []
// 逐日报表获取完整数据
const results = []
for (const report of allReports) {
const reportId = report.reportId
const [detailRes, metricRes] = await Promise.all([
listProdDetail({ reportId, pageNum: 1, pageSize: 9999 }),
listProdMetric({ reportId, pageNum: 1, pageSize: 9999 })
])
const details = detailRes.rows || []
const metrics = metricRes.rows || []
const detailMap = {}
details.forEach(detail => {
const key = detail.itemId
if (!detailMap[key]) detailMap[key] = []
detailMap[key].push(detail)
})
const metricMap = {}
metrics.forEach(metric => {
metricMap[metric.metricId] = metric
})
const data = buildComputedRows(report, details, metrics, items)
results.push({
date: report.reportDate,
report,
metrics,
details,
items,
data,
prices,
detailMap,
itemMap,
metricMap
})
}
return results
}
/**
* 根据 itemId 获取指定日期下该成本项目的明细数据
*
* @param {string} date - 日期 yyyy-MM-dd
* @param {string} lineType - 产线类型
* @param {string|number} itemId - 成本项目ID
* @returns {Promise<{ quantity: number|null, unitPrice: number|null, amount: number|null }>}
*/
export async function getCostItemData(date, lineType, itemId) {
const { details, items } = await getCostDataByDate(date, lineType)
const itemDetails = details.filter(d => String(d.itemId) === String(itemId))
const item = items.find(i => String(i.itemId) === String(itemId))
let totalQuantity = 0
itemDetails.forEach(d => {
totalQuantity += parseFloat(d.quantity) || 0
})
return {
itemId,
itemName: item ? item.itemName : '',
itemCode: item ? item.itemCode : '',
category: item ? item.category : '',
unit: item ? item.unit : '',
quantity: totalQuantity || null,
unitPrice: itemDetails.length > 0 ? itemDetails[0].unitPrice : null,
amount: itemDetails.length > 0 ? itemDetails[0].amount : null,
details: itemDetails
}
}
export default {
getCostDataByDate,
getCostDataByDateRange,
getCostItemData
}