2026-06-11 14:02:49 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="section basic-info-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<span class="section-icon">💰</span>
|
|
|
|
|
|
<span class="section-title">成本信息</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="section-body">
|
|
|
|
|
|
<el-descriptions :column="3" border size="small">
|
|
|
|
|
|
<el-descriptions-item label="囤积成本">
|
|
|
|
|
|
<span class="cost-value">{{ hoardingCost }}</span>
|
|
|
|
|
|
<span class="cost-unit">t·天</span>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="囤积天数">
|
|
|
|
|
|
<span>{{ hoardingDays }} 天</span>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="钢卷净重">
|
|
|
|
|
|
<span>{{ coilInfo.netWeight || 0 }} t</span>
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
2026-06-16 16:33:27 +08:00
|
|
|
|
|
2026-06-17 15:09:08 +08:00
|
|
|
|
<div v-if="!pathChecked || pathGraphData.nodes.length > 1" class="cost-chart-wrap">
|
|
|
|
|
|
<div class="cost-table-title">加工路径</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="processedCostTable.length" class="ton-cost-table-wrap">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="processedCostTable"
|
|
|
|
|
|
border
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:header-cell-style="{ background: '#f5f7fa', color: '#303133', fontWeight: 600 }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-table-column prop="actionName" label="工序" min-width="120" />
|
|
|
|
|
|
<el-table-column prop="lineName" label="产线" width="100" />
|
|
|
|
|
|
<el-table-column prop="coilNo" label="钢卷号" min-width="140" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column prop="costPerTon" label="吨钢成本(元/吨)" width="130" align="right">
|
2026-06-16 16:33:27 +08:00
|
|
|
|
<template slot-scope="s">
|
2026-06-17 15:09:08 +08:00
|
|
|
|
<span :class="s.row.costPerTon ? 'cost-ton-val' : 'cost-ton-zero'">{{ s.row.costPerTon || '-' }}</span>
|
2026-06-16 16:33:27 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-17 15:09:08 +08:00
|
|
|
|
<el-table-column prop="netWeight" label="净重(t)" width="100" align="right">
|
|
|
|
|
|
<template slot-scope="s">{{ s.row.netWeight }}</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="totalCost" label="总成本(元)" width="120" align="right">
|
2026-06-16 16:33:27 +08:00
|
|
|
|
<template slot-scope="s">
|
2026-06-17 15:09:08 +08:00
|
|
|
|
<span class="cost-total-val">{{ s.row.costPerTon ? s.row.totalCost : '-' }}</span>
|
2026-06-16 16:33:27 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-06-17 15:09:08 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div ref="pathChart" v-loading="pathLoading || costLoading" class="path-chart" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="!pathLoading && pathChecked" class="cost-chart-wrap">
|
|
|
|
|
|
<el-empty description="暂无加工路径数据" :image-size="60" />
|
2026-06-16 16:33:27 +08:00
|
|
|
|
</div>
|
2026-06-11 14:02:49 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-06-17 15:09:08 +08:00
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
|
import { listPendingAction } from '@/api/wms/pendingAction'
|
|
|
|
|
|
import { getMaterialCoil } from '@/api/wms/coil'
|
|
|
|
|
|
import { listProdReport } from '@/api/cost/prodReport'
|
|
|
|
|
|
import { listProdMetric } from '@/api/cost/prodMetric'
|
|
|
|
|
|
import { listProdMetricResult } from '@/api/cost/prodMetricResult'
|
|
|
|
|
|
|
|
|
|
|
|
const ACTION_TO_PROD_LINE = {
|
|
|
|
|
|
11: 2, 120: 2, 201: 2, 520: 2,
|
|
|
|
|
|
501: 1, 202: 1, 521: 1
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-17 15:09:08 +08:00
|
|
|
|
const PROD_LINE_NAME = { 1: '镀锌产线', 2: '酸轧产线' }
|
2026-06-16 16:33:27 +08:00
|
|
|
|
|
2026-06-17 15:09:08 +08:00
|
|
|
|
const ACTION_TYPE_MAP = {
|
|
|
|
|
|
11: '酸连轧工序',
|
|
|
|
|
|
120: '酸轧分条',
|
|
|
|
|
|
201: '酸轧合卷',
|
|
|
|
|
|
202: '镀锌合卷',
|
|
|
|
|
|
203: '脱脂合卷',
|
|
|
|
|
|
204: '拉矫平整合卷',
|
|
|
|
|
|
205: '双机架合卷',
|
|
|
|
|
|
206: '镀铬合卷',
|
|
|
|
|
|
501: '镀锌工序',
|
|
|
|
|
|
502: '脱脂工序',
|
|
|
|
|
|
504: '双机架工序',
|
|
|
|
|
|
506: '纵剪分条工序'
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-11 14:02:49 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'CostInfoSection',
|
|
|
|
|
|
props: {
|
|
|
|
|
|
coilInfo: { type: Object, default: () => ({}) },
|
|
|
|
|
|
traceResult: { type: Object, default: null },
|
|
|
|
|
|
hoardingDays: { type: Number, default: 0 },
|
|
|
|
|
|
hoardingCost: { type: [String, Number], default: '0.00' }
|
2026-06-16 16:33:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2026-06-17 15:09:08 +08:00
|
|
|
|
pathLoading: false,
|
|
|
|
|
|
costLoading: false,
|
|
|
|
|
|
pathChecked: false,
|
|
|
|
|
|
chart: null,
|
|
|
|
|
|
pathGraphData: {
|
|
|
|
|
|
nodes: [],
|
|
|
|
|
|
links: []
|
|
|
|
|
|
},
|
|
|
|
|
|
processedCostTable: []
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-17 15:09:08 +08:00
|
|
|
|
watch: {
|
|
|
|
|
|
'coilInfo.coilId': {
|
|
|
|
|
|
handler(val) {
|
|
|
|
|
|
if (val) {
|
|
|
|
|
|
this.loadProcessingPath()
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-17 15:09:08 +08:00
|
|
|
|
},
|
|
|
|
|
|
immediate: true
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-17 15:09:08 +08:00
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
if (this.chart) {
|
|
|
|
|
|
this.chart.dispose()
|
|
|
|
|
|
this.chart = null
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-17 15:09:08 +08:00
|
|
|
|
window.removeEventListener('resize', this.handleResize)
|
2026-06-16 16:33:27 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
formatVal(val) {
|
|
|
|
|
|
return val != null ? val : '-'
|
|
|
|
|
|
},
|
2026-06-17 15:09:08 +08:00
|
|
|
|
async loadProcessingPath() {
|
|
|
|
|
|
const coilId = this.coilInfo.coilId
|
|
|
|
|
|
if (!coilId) return
|
|
|
|
|
|
|
|
|
|
|
|
this.pathLoading = true
|
|
|
|
|
|
this.pathChecked = false
|
2026-06-16 16:33:27 +08:00
|
|
|
|
try {
|
2026-06-17 15:09:08 +08:00
|
|
|
|
const lineage = await this.buildCoilLineage(coilId, new Set(), 0)
|
|
|
|
|
|
if (lineage.length <= 1) {
|
|
|
|
|
|
this.pathGraphData = { nodes: [], links: [] }
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const coilMap = {}
|
|
|
|
|
|
lineage.forEach(c => { coilMap[String(c.coilId)] = c })
|
|
|
|
|
|
const allCoilIds = lineage.map(c => c.coilId).filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
// 用每个卷的 coilId 查询完整的 pendingAction,批量并发
|
|
|
|
|
|
const actionResults = await Promise.allSettled(
|
|
|
|
|
|
allCoilIds.map(id => listPendingAction({
|
|
|
|
|
|
coilId: id,
|
|
|
|
|
|
actionStatus: 2,
|
|
|
|
|
|
pageSize: 99999
|
|
|
|
|
|
}))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const actionMap = {}
|
|
|
|
|
|
actionResults.forEach(r => {
|
|
|
|
|
|
if (r.status === 'fulfilled' && r.value?.rows) {
|
|
|
|
|
|
r.value.rows.forEach(a => {
|
|
|
|
|
|
if (a.actionId) {
|
|
|
|
|
|
actionMap[a.actionId] = a
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(actionMap).length) {
|
|
|
|
|
|
const sampleAction = Object.values(actionMap)[0]
|
|
|
|
|
|
console.log('pendingAction 字段列表:', Object.keys(sampleAction))
|
|
|
|
|
|
console.log('pendingAction 样例:', sampleAction)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.pathGraphData = this.buildGraphData(lineage, coilId, Object.values(actionMap), coilMap)
|
|
|
|
|
|
await this.loadCostData()
|
2026-06-16 16:33:27 +08:00
|
|
|
|
} catch (e) {
|
2026-06-17 15:09:08 +08:00
|
|
|
|
console.error('获取加工路径失败:', e)
|
|
|
|
|
|
this.pathGraphData = { nodes: [], links: [] }
|
2026-06-16 16:33:27 +08:00
|
|
|
|
} finally {
|
2026-06-17 15:09:08 +08:00
|
|
|
|
this.pathLoading = false
|
|
|
|
|
|
this.pathChecked = true
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.initChart()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
async buildCoilLineage(coilId, visited, depth) {
|
|
|
|
|
|
if (!coilId || visited.has(String(coilId)) || depth > 20) return []
|
|
|
|
|
|
visited.add(String(coilId))
|
|
|
|
|
|
|
|
|
|
|
|
const res = await getMaterialCoil(coilId)
|
|
|
|
|
|
const coil = res.data || {}
|
|
|
|
|
|
if (!coil.coilId) return []
|
|
|
|
|
|
|
|
|
|
|
|
const parents = []
|
|
|
|
|
|
if (coil.parentCoilId) {
|
|
|
|
|
|
const parentIds = String(coil.parentCoilId).split(',').map(id => id.trim()).filter(Boolean)
|
|
|
|
|
|
for (const pid of parentIds) {
|
|
|
|
|
|
if (!visited.has(pid)) {
|
|
|
|
|
|
const pLineage = await this.buildCoilLineage(pid, visited, depth + 1)
|
|
|
|
|
|
parents.push(...pLineage)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-17 15:09:08 +08:00
|
|
|
|
|
|
|
|
|
|
return [...parents, coil]
|
|
|
|
|
|
},
|
|
|
|
|
|
buildGraphData(lineage, currentCoilId, actions, coilMap) {
|
|
|
|
|
|
const nodes = []
|
|
|
|
|
|
const links = []
|
|
|
|
|
|
const nodeIdxMap = {}
|
|
|
|
|
|
|
|
|
|
|
|
// 建立 outputCoilIds → action 的索引,兼容多种后端字段名
|
|
|
|
|
|
const actionByOutput = {}
|
|
|
|
|
|
const actionByInput = {}
|
|
|
|
|
|
function getOutputIds(a) {
|
|
|
|
|
|
return a.processedCoilIds || a.newCoilIds || a.outputCoilIds ||
|
|
|
|
|
|
a.completedCoilIds || a.coilIds || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
actions.forEach(a => {
|
|
|
|
|
|
const outIds = getOutputIds(a)
|
|
|
|
|
|
if (outIds) {
|
|
|
|
|
|
String(outIds).split(',').forEach(id => {
|
|
|
|
|
|
const tid = id.trim()
|
|
|
|
|
|
if (tid) actionByOutput[tid] = a
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
// 同时建立输入侧索引作为兜底
|
|
|
|
|
|
if (a.coilId) {
|
|
|
|
|
|
const key = String(a.coilId)
|
|
|
|
|
|
if (!actionByInput[key]) actionByInput[key] = a
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 计算层级
|
|
|
|
|
|
const levels = {}
|
|
|
|
|
|
lineage.forEach(coil => {
|
|
|
|
|
|
if (!coil.parentCoilId) {
|
|
|
|
|
|
levels[String(coil.coilId)] = 0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const parentIds = String(coil.parentCoilId).split(',').map(id => id.trim()).filter(Boolean)
|
|
|
|
|
|
const maxParentLevel = Math.max(...parentIds.map(pid => levels[pid] || 0))
|
|
|
|
|
|
levels[String(coil.coilId)] = maxParentLevel + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const levelGroups = {}
|
|
|
|
|
|
lineage.forEach(coil => {
|
|
|
|
|
|
const lv = levels[String(coil.coilId)]
|
|
|
|
|
|
if (!levelGroups[lv]) levelGroups[lv] = []
|
|
|
|
|
|
levelGroups[lv].push(coil)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 构建节点
|
|
|
|
|
|
lineage.forEach(coil => {
|
|
|
|
|
|
const isRoot = !coil.parentCoilId
|
|
|
|
|
|
const isCurrent = String(coil.coilId) === String(currentCoilId)
|
|
|
|
|
|
const category = isCurrent ? 'current' : (isRoot ? 'root' : 'intermediate')
|
|
|
|
|
|
|
|
|
|
|
|
const lv = levels[String(coil.coilId)]
|
|
|
|
|
|
const siblings = levelGroups[lv] || []
|
|
|
|
|
|
const sibIdx = siblings.findIndex(c => String(c.coilId) === String(coil.coilId))
|
|
|
|
|
|
const totalInLevel = siblings.length
|
|
|
|
|
|
|
|
|
|
|
|
nodeIdxMap[String(coil.coilId)] = nodes.length
|
|
|
|
|
|
|
|
|
|
|
|
const nodeName = String(coil.currentCoilNo || coil.coilId || '')
|
|
|
|
|
|
nodes.push({
|
|
|
|
|
|
name: nodeName,
|
|
|
|
|
|
id: String(coil.coilId),
|
|
|
|
|
|
category,
|
|
|
|
|
|
level: lv,
|
|
|
|
|
|
levelIndex: sibIdx,
|
|
|
|
|
|
levelTotal: totalInLevel,
|
|
|
|
|
|
symbolSize: 65,
|
|
|
|
|
|
coilNo: coil.currentCoilNo,
|
|
|
|
|
|
coilInfo: {
|
|
|
|
|
|
enterCoilNo: coil.enterCoilNo,
|
|
|
|
|
|
currentCoilNo: coil.currentCoilNo,
|
|
|
|
|
|
supplierCoilNo: coil.supplierCoilNo,
|
|
|
|
|
|
itemName: coil.itemName,
|
|
|
|
|
|
specification: coil.specification,
|
|
|
|
|
|
material: coil.material,
|
|
|
|
|
|
netWeight: coil.netWeight,
|
|
|
|
|
|
grossWeight: coil.grossWeight,
|
|
|
|
|
|
warehouseName: coil.warehouseName,
|
|
|
|
|
|
actualWarehouseName: coil.actualWarehouseName,
|
|
|
|
|
|
manufacturer: coil.manufacturer,
|
|
|
|
|
|
zincLayer: coil.zincLayer,
|
|
|
|
|
|
qualityStatus: coil.qualityStatus,
|
|
|
|
|
|
surfaceTreatmentDesc: coil.surfaceTreatmentDesc,
|
|
|
|
|
|
createTime: coil.createTime
|
|
|
|
|
|
},
|
|
|
|
|
|
isCurrent
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 确保节点 name 唯一(ECharts 以 name 标识节点,重名会合并,导致 dataIndex 错误)
|
|
|
|
|
|
const nameCount = {}
|
|
|
|
|
|
nodes.forEach(n => {
|
|
|
|
|
|
nameCount[n.name] = (nameCount[n.name] || 0) + 1
|
|
|
|
|
|
})
|
|
|
|
|
|
const nameUsed = {}
|
|
|
|
|
|
nodes.forEach(n => {
|
|
|
|
|
|
if (nameCount[n.name] > 1) {
|
|
|
|
|
|
nameUsed[n.name] = (nameUsed[n.name] || 0) + 1
|
|
|
|
|
|
n.name = `${n.name}(${nameUsed[n.name]})`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 构建边
|
|
|
|
|
|
lineage.forEach(coil => {
|
|
|
|
|
|
if (!coil.parentCoilId) return
|
|
|
|
|
|
const childIdx = nodeIdxMap[String(coil.coilId)]
|
|
|
|
|
|
const parentIds = String(coil.parentCoilId).split(',').map(id => id.trim()).filter(Boolean)
|
|
|
|
|
|
|
|
|
|
|
|
const action = actionByOutput[String(coil.coilId)] || {}
|
|
|
|
|
|
|
|
|
|
|
|
parentIds.forEach(pid => {
|
|
|
|
|
|
const parentIdx = nodeIdxMap[pid]
|
|
|
|
|
|
if (parentIdx != null && childIdx != null) {
|
|
|
|
|
|
const edgeAction = Object.keys(action).length ? action
|
|
|
|
|
|
: (actionByInput[pid] || {})
|
|
|
|
|
|
const actionTypeVal = edgeAction.actionType != null ? edgeAction.actionType : null
|
|
|
|
|
|
const actionLabel = ACTION_TYPE_MAP[actionTypeVal] ||
|
|
|
|
|
|
(actionTypeVal != null ? `工序${actionTypeVal}` : '')
|
|
|
|
|
|
|
|
|
|
|
|
links.push({
|
|
|
|
|
|
source: parentIdx,
|
|
|
|
|
|
target: childIdx,
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: !!actionLabel,
|
|
|
|
|
|
formatter: actionLabel,
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
color: '#606266'
|
|
|
|
|
|
},
|
|
|
|
|
|
actionInfo: {
|
|
|
|
|
|
actionId: edgeAction.actionId,
|
|
|
|
|
|
actionType: edgeAction.actionType,
|
|
|
|
|
|
actionTypeName: actionLabel,
|
|
|
|
|
|
processTime: edgeAction.processTime,
|
|
|
|
|
|
completeTime: edgeAction.completeTime,
|
|
|
|
|
|
createTime: edgeAction.createTime,
|
|
|
|
|
|
createBy: edgeAction.createBy,
|
|
|
|
|
|
priority: edgeAction.priority,
|
|
|
|
|
|
remark: edgeAction.remark
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return { nodes, links }
|
|
|
|
|
|
},
|
|
|
|
|
|
async loadCostData() {
|
|
|
|
|
|
const { nodes, links } = this.pathGraphData
|
|
|
|
|
|
if (!links.length) return
|
|
|
|
|
|
|
|
|
|
|
|
this.costLoading = true
|
|
|
|
|
|
this.processedCostTable = []
|
|
|
|
|
|
|
|
|
|
|
|
const buildFirstDay = (dateStr) => {
|
|
|
|
|
|
if (!dateStr) return null
|
|
|
|
|
|
const d = new Date(dateStr)
|
|
|
|
|
|
if (isNaN(d.getTime())) return null
|
|
|
|
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01`
|
|
|
|
|
|
}
|
|
|
|
|
|
const buildDate = (dateStr) => {
|
|
|
|
|
|
if (!dateStr) return null
|
|
|
|
|
|
const d = new Date(dateStr)
|
|
|
|
|
|
if (isNaN(d.getTime())) return null
|
|
|
|
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cache = {}
|
|
|
|
|
|
const costQueries = links.map(async(link, idx) => {
|
|
|
|
|
|
const actionType = link.actionInfo?.actionType
|
|
|
|
|
|
if (actionType == null) return
|
|
|
|
|
|
const prodLineId = ACTION_TO_PROD_LINE[actionType]
|
|
|
|
|
|
if (!prodLineId) return
|
|
|
|
|
|
|
|
|
|
|
|
const targetNode = nodes[link.target]
|
|
|
|
|
|
const createTime = targetNode?.coilInfo?.createTime
|
|
|
|
|
|
if (!createTime) return
|
|
|
|
|
|
|
|
|
|
|
|
const firstDay = buildFirstDay(createTime)
|
|
|
|
|
|
const prodDate = buildDate(createTime)
|
|
|
|
|
|
if (!firstDay || !prodDate) return
|
|
|
|
|
|
|
|
|
|
|
|
const cacheKey = `${prodLineId}_${firstDay}`
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let rpMetric = cache[cacheKey]
|
|
|
|
|
|
if (rpMetric === undefined) {
|
|
|
|
|
|
const rpRes = await listProdReport({ lineType: prodLineId, reportDate: firstDay, pageNum: 1, pageSize: 10 })
|
|
|
|
|
|
const reports = rpRes?.rows || []
|
|
|
|
|
|
if (reports.length !== 1) { cache[cacheKey] = null; return }
|
|
|
|
|
|
const reportId = reports[0].reportId
|
|
|
|
|
|
|
|
|
|
|
|
const mRes = await listProdMetric({ reportId, pageNum: 1, pageSize: 9999, tags: '吨钢成本' })
|
|
|
|
|
|
const metrics = (mRes?.rows || []).filter(m => (m.tags || '').includes('吨钢成本'))
|
|
|
|
|
|
if (metrics.length !== 1) { cache[cacheKey] = null; return }
|
|
|
|
|
|
|
|
|
|
|
|
rpMetric = { reportId, metricId: metrics[0].metricId }
|
|
|
|
|
|
cache[cacheKey] = rpMetric
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rpMetric) return
|
|
|
|
|
|
|
|
|
|
|
|
const rRes = await listProdMetricResult({
|
|
|
|
|
|
reportId: rpMetric.reportId,
|
|
|
|
|
|
metricId: rpMetric.metricId,
|
|
|
|
|
|
metricDate: prodDate,
|
|
|
|
|
|
pageNum: 1, pageSize: 10
|
|
|
|
|
|
})
|
|
|
|
|
|
const results = rRes?.rows || []
|
|
|
|
|
|
const costPerTon = results.length > 0 ? parseFloat(results[0].calcValue) || 0 : 0
|
|
|
|
|
|
|
|
|
|
|
|
link.costPerTon = costPerTon
|
|
|
|
|
|
link.lineName = PROD_LINE_NAME[prodLineId] || ''
|
|
|
|
|
|
|
|
|
|
|
|
const netWeight = parseFloat(targetNode.coilInfo.netWeight) || 0
|
|
|
|
|
|
this.processedCostTable.push({
|
|
|
|
|
|
idx,
|
|
|
|
|
|
actionName: link.actionInfo?.actionTypeName || link.label?.formatter || '',
|
|
|
|
|
|
lineName: link.lineName,
|
|
|
|
|
|
coilNo: targetNode.coilNo || targetNode.name || '',
|
|
|
|
|
|
costPerTon,
|
|
|
|
|
|
netWeight,
|
|
|
|
|
|
totalCost: Math.round(costPerTon * netWeight * 100) / 100
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('查询吨钢成本失败(actionType=' + actionType + '):', e)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
await Promise.allSettled(costQueries)
|
|
|
|
|
|
this.costLoading = false
|
|
|
|
|
|
},
|
|
|
|
|
|
initChart() {
|
|
|
|
|
|
if (this.chart) {
|
|
|
|
|
|
this.chart.dispose()
|
|
|
|
|
|
this.chart = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dom = this.$refs.pathChart
|
|
|
|
|
|
if (!dom) return
|
|
|
|
|
|
const { nodes, links } = this.pathGraphData
|
|
|
|
|
|
if (!nodes.length) return
|
|
|
|
|
|
|
|
|
|
|
|
const containerWidth = dom.clientWidth || 800
|
|
|
|
|
|
|
|
|
|
|
|
const maxLevel = Math.max(...nodes.map(n => n.level || 0), 0)
|
|
|
|
|
|
const totalLevels = maxLevel + 1
|
|
|
|
|
|
|
|
|
|
|
|
const levelGroups = {}
|
|
|
|
|
|
nodes.forEach(n => {
|
|
|
|
|
|
const lv = n.level || 0
|
|
|
|
|
|
if (!levelGroups[lv]) levelGroups[lv] = []
|
|
|
|
|
|
levelGroups[lv].push(n)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const maxNodesInLevel = Math.max(...Object.values(levelGroups).map(g => g.length), 1)
|
|
|
|
|
|
const paddingX = 100
|
|
|
|
|
|
const paddingY = 70
|
|
|
|
|
|
|
|
|
|
|
|
const LEVEL_GAP_X = totalLevels > 1
|
|
|
|
|
|
? Math.max(240, (containerWidth - paddingX * 2) / (totalLevels - 1))
|
|
|
|
|
|
: 240
|
|
|
|
|
|
const LEVEL_GAP_Y = 110
|
|
|
|
|
|
|
|
|
|
|
|
const START_X = paddingX
|
|
|
|
|
|
const START_Y = paddingY
|
|
|
|
|
|
|
|
|
|
|
|
nodes.forEach(n => {
|
|
|
|
|
|
const lv = n.level || 0
|
|
|
|
|
|
const sibIdx = n.levelIndex != null ? n.levelIndex : 0
|
|
|
|
|
|
const totalInLevel = n.levelTotal || 1
|
|
|
|
|
|
n.x = START_X + lv * LEVEL_GAP_X
|
|
|
|
|
|
n.y = START_Y + (sibIdx - (totalInLevel - 1) / 2) * LEVEL_GAP_Y
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const chartHeight = Math.max(350, maxNodesInLevel * LEVEL_GAP_Y + paddingY * 2)
|
|
|
|
|
|
|
|
|
|
|
|
dom.style.width = containerWidth + 'px'
|
|
|
|
|
|
dom.style.height = chartHeight + 'px'
|
|
|
|
|
|
|
|
|
|
|
|
this.chart = echarts.init(dom)
|
|
|
|
|
|
|
|
|
|
|
|
const rootGradient = { type: 'radial', x: 0.5, y: 0.5, r: 0.5, colorStops: [{ offset: 0, color: '#93c5fd' }, { offset: 1, color: '#3b82f6' }] }
|
|
|
|
|
|
const intermediateGradient = { type: 'radial', x: 0.5, y: 0.5, r: 0.5, colorStops: [{ offset: 0, color: '#6ee7b7' }, { offset: 1, color: '#10b981' }] }
|
|
|
|
|
|
const currentGradient = { type: 'radial', x: 0.5, y: 0.5, r: 0.5, colorStops: [{ offset: 0, color: '#fde68a' }, { offset: 1, color: '#f59e0b' }] }
|
|
|
|
|
|
|
|
|
|
|
|
const gradientMap = {
|
|
|
|
|
|
root: rootGradient,
|
|
|
|
|
|
intermediate: intermediateGradient,
|
|
|
|
|
|
current: currentGradient
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
|
confine: true,
|
|
|
|
|
|
backgroundColor: 'rgba(20,20,30,0.92)',
|
|
|
|
|
|
borderColor: '#3b82f6',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
padding: [10, 14],
|
|
|
|
|
|
textStyle: { color: '#e5e7eb', fontSize: 12 },
|
|
|
|
|
|
formatter: (params) => {
|
|
|
|
|
|
if (params.dataType === 'edge') {
|
|
|
|
|
|
const ai = params.data.actionInfo || {}
|
|
|
|
|
|
const cp = params.data.costPerTon
|
|
|
|
|
|
const rows = [
|
|
|
|
|
|
`<div style="font-weight:bold;color:#fbbf24;margin-bottom:4px;font-size:13px">${ai.actionTypeName || '工序'}</div>`
|
|
|
|
|
|
]
|
|
|
|
|
|
if (cp) rows.push(`<span style="color:#34d399">吨钢成本</span>:${cp} 元/吨`)
|
|
|
|
|
|
let hasInfo = false
|
|
|
|
|
|
if (ai.processTime) { rows.push(`处理时间:${ai.processTime}`); hasInfo = true }
|
|
|
|
|
|
if (ai.completeTime) { rows.push(`完成时间:${ai.completeTime}`); hasInfo = true }
|
|
|
|
|
|
if (ai.createTime) { rows.push(`创建时间:${ai.createTime}`); hasInfo = true }
|
|
|
|
|
|
if (ai.createBy) { rows.push(`创建人:${ai.createBy}`); hasInfo = true }
|
|
|
|
|
|
if (!cp && !hasInfo) return ''
|
|
|
|
|
|
return rows.join('<br/>')
|
|
|
|
|
|
}
|
|
|
|
|
|
const d = params.data
|
|
|
|
|
|
const ci = d.coilInfo || {}
|
|
|
|
|
|
const badge = d.isCurrent ? '<span style="display:inline-block;background:#f59e0b;color:#fff;padding:1px 6px;border-radius:4px;font-size:11px;margin-left:6px">当前卷</span>' : ''
|
|
|
|
|
|
const rows = [
|
|
|
|
|
|
`<div style="font-weight:bold;color:#60a5fa;margin-bottom:4px;font-size:13px">${ci.currentCoilNo || '-'}${badge}</div>`
|
|
|
|
|
|
]
|
|
|
|
|
|
if (ci.enterCoilNo) rows.push(`<span style="color:#9ca3af">入场钢卷号</span>:${ci.enterCoilNo}`)
|
|
|
|
|
|
if (ci.supplierCoilNo) rows.push(`<span style="color:#9ca3af">厂家原料号</span>:${ci.supplierCoilNo}`)
|
|
|
|
|
|
if (ci.itemName) rows.push(`<span style="color:#9ca3af">物料名</span>:${ci.itemName}`)
|
|
|
|
|
|
if (ci.specification) rows.push(`<span style="color:#9ca3af">规格</span>:${ci.specification}`)
|
|
|
|
|
|
if (ci.material) rows.push(`<span style="color:#9ca3af">材质</span>:${ci.material}`)
|
|
|
|
|
|
if (ci.netWeight != null) rows.push(`<span style="color:#9ca3af">净重</span>:${ci.netWeight} t`)
|
|
|
|
|
|
if (ci.manufacturer) rows.push(`<span style="color:#9ca3af">厂家</span>:${ci.manufacturer}`)
|
|
|
|
|
|
if (ci.warehouseName) rows.push(`<span style="color:#9ca3af">逻辑库位</span>:${ci.warehouseName}`)
|
|
|
|
|
|
if (ci.actualWarehouseName) rows.push(`<span style="color:#9ca3af">实际库区</span>:${ci.actualWarehouseName}`)
|
|
|
|
|
|
if (ci.zincLayer) rows.push(`<span style="color:#9ca3af">镀层质量</span>:${ci.zincLayer}`)
|
|
|
|
|
|
if (ci.qualityStatus) rows.push(`<span style="color:#9ca3af">质量状态</span>:${ci.qualityStatus}`)
|
|
|
|
|
|
if (ci.surfaceTreatmentDesc) rows.push(`<span style="color:#9ca3af">表面处理</span>:${ci.surfaceTreatmentDesc}`)
|
|
|
|
|
|
if (ci.createTime) rows.push(`<span style="color:#9ca3af">创建时间</span>:${ci.createTime}`)
|
|
|
|
|
|
if (ci.createBy) rows.push(`<span style="color:#9ca3af">创建人</span>:${ci.createBy}`)
|
|
|
|
|
|
return rows.join('<br/>')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
animationDuration: 1000,
|
|
|
|
|
|
animationEasing: 'cubicOut',
|
|
|
|
|
|
animationEasingUpdate: 'quinticInOut',
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'graph',
|
|
|
|
|
|
layout: 'none',
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
draggable: false,
|
|
|
|
|
|
data: nodes.map(n => ({
|
|
|
|
|
|
name: n.name,
|
|
|
|
|
|
x: n.x,
|
|
|
|
|
|
y: n.y,
|
|
|
|
|
|
symbol: 'roundRect',
|
|
|
|
|
|
symbolSize: n.symbolSize,
|
|
|
|
|
|
category: undefined,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: gradientMap[n.category] || '#909399',
|
|
|
|
|
|
borderColor: n.isCurrent ? '#f59e0b' : 'rgba(255,255,255,0.6)',
|
|
|
|
|
|
borderWidth: n.isCurrent ? 4 : 2,
|
|
|
|
|
|
shadowBlur: n.isCurrent ? 20 : 8,
|
|
|
|
|
|
shadowColor: n.isCurrent ? 'rgba(245,158,11,0.55)' : 'rgba(0,0,0,0.2)',
|
|
|
|
|
|
shadowOffsetY: 2
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
formatter: (p) => {
|
|
|
|
|
|
const cn = p.data.coilNo || p.data.name
|
|
|
|
|
|
return cn && cn.length > 12 ? cn.slice(0, 11) + '...' : (cn || '-')
|
|
|
|
|
|
},
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
fontWeight: n.isCurrent ? 'bold' : 'normal',
|
|
|
|
|
|
color: n.isCurrent ? '#f59e0b' : '#374151',
|
|
|
|
|
|
position: 'bottom',
|
|
|
|
|
|
distance: 8
|
|
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
scale: 1.2,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
shadowBlur: n.isCurrent ? 30 : 20,
|
|
|
|
|
|
shadowColor: n.isCurrent ? 'rgba(245,158,11,0.7)' : 'rgba(59,130,246,0.5)',
|
|
|
|
|
|
borderWidth: n.isCurrent ? 5 : 3
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
coilNo: n.coilNo,
|
|
|
|
|
|
coilInfo: n.coilInfo,
|
|
|
|
|
|
isCurrent: n.isCurrent
|
|
|
|
|
|
})),
|
|
|
|
|
|
links: links.map(l => ({
|
|
|
|
|
|
source: l.source,
|
|
|
|
|
|
target: l.target,
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: !!(l.label && l.label.formatter) || !!l.costPerTon,
|
|
|
|
|
|
formatter: (l.costPerTon != null)
|
|
|
|
|
|
? `${l.label && l.label.formatter ? l.label.formatter + ' ' : ''}${l.costPerTon}元/吨`
|
|
|
|
|
|
: (l.label && l.label.formatter ? l.label.formatter : ''),
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
color: l.costPerTon != null ? '#059669' : '#606266'
|
|
|
|
|
|
},
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: l.costPerTon != null ? 'rgba(5,150,105,0.6)' : 'rgba(107,114,128,0.55)',
|
|
|
|
|
|
width: l.costPerTon != null ? 3 : 2.5,
|
|
|
|
|
|
curveness: 0.15,
|
|
|
|
|
|
cap: 'round',
|
|
|
|
|
|
opacity: 0.9
|
|
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: '#3b82f6',
|
|
|
|
|
|
width: 4,
|
|
|
|
|
|
opacity: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
fontWeight: 'bold',
|
|
|
|
|
|
color: '#2563eb'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
actionInfo: l.actionInfo,
|
|
|
|
|
|
costPerTon: l.costPerTon
|
|
|
|
|
|
})),
|
|
|
|
|
|
edgeSymbol: ['none', 'arrow'],
|
|
|
|
|
|
edgeSymbolSize: [0, 12],
|
|
|
|
|
|
edgeLabel: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
color: '#4b5563',
|
|
|
|
|
|
position: 'middle',
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.85)',
|
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
|
padding: [2, 6],
|
|
|
|
|
|
distance: 6
|
|
|
|
|
|
},
|
|
|
|
|
|
categories: [
|
|
|
|
|
|
{ name: 'root', itemStyle: { color: '#3b82f6' }},
|
|
|
|
|
|
{ name: 'intermediate', itemStyle: { color: '#10b981' }},
|
|
|
|
|
|
{ name: 'current', itemStyle: { color: '#f59e0b' }}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.chart.setOption(option)
|
|
|
|
|
|
this.handleResize = () => {
|
|
|
|
|
|
if (this.chart && !this.chart.isDisposed()) {
|
|
|
|
|
|
this.chart.resize()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
window.addEventListener('resize', this.handleResize)
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
2026-06-11 14:02:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
2026-06-16 16:33:27 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.cost-value {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-unit {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 15:09:08 +08:00
|
|
|
|
.cost-chart-wrap {
|
|
|
|
|
|
margin-top: 12px;
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-table-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
padding: 6px 0;
|
|
|
|
|
|
border-bottom: 1px solid #ebeef5;
|
2026-06-17 15:09:08 +08:00
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.path-chart {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ton-cost-table-wrap {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-ton-val {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #059669;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-ton-zero {
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cost-total-val {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #f56c6c;
|
2026-06-16 16:33:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|