refactor(wms/coil): 重构钢卷详情页面,拆分组件并优化页面结构

1.  将原钢卷详情页面拆分为多个独立组件,按功能模块划分
2.  调整路由指向,将页面入口指向新的页面文件
3.  新增状态工具类,封装通用格式化和样式
This commit is contained in:
2026-06-11 14:02:49 +08:00
parent ea71a6dd93
commit b9fb4b4611
16 changed files with 3003 additions and 3400 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
<template>
<div class="section basic-info-section">
<div class="section-header">
<span class="section-icon">&#128230;</span>
<span class="section-title">基本信息</span>
</div>
<div class="section-body">
<CoilInfoRender :coilInfo="coilInfo" :column="5" />
</div>
</div>
</template>
<script>
export default {
name: 'BasicInfoSection',
props: {
coilInfo: { type: Object, default: () => ({}) }
}
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div :class="['coil-card-hover', variant]" v-if="coil && coil.currentCoilNo !== '-'">
<div class="coil-mini-header">
<span class="coil-mini-no">{{ coil.currentCoilNo }}</span>
<span class="coil-mini-weight">{{ coil.netWeight ? coil.netWeight + 't' : '-' }}</span>
</div>
<div class="coil-mini-body">
<span class="coil-mini-spec">{{ coil.specification || '-' }}</span>
<span class="coil-mini-material">{{ coil.material || '-' }}</span>
<span :class="['coil-mini-status', statusClass(coil.qualityStatus)]">{{ coil.qualityStatus || '-' }}</span>
</div>
<div class="coil-detail-popup">
<div class="popup-header">
<span class="popup-title">钢卷详情</span>
<span class="popup-close">&#10005;</span>
</div>
<div class="popup-content">
<div class="popup-row">
<span class="popup-label">入场卷号</span>
<span class="popup-value">{{ coil.enterCoilNo || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">当前卷号</span>
<span class="popup-value">{{ coil.currentCoilNo || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">物料名称</span>
<span class="popup-value">{{ coil.itemName || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">规格</span>
<span class="popup-value">{{ coil.specification || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">材质</span>
<span class="popup-value">{{ coil.material || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">净重</span>
<span class="popup-value">{{ coil.netWeight || 0 }} t</span>
</div>
<div class="popup-row">
<span class="popup-label">生产厂家</span>
<span class="popup-value">{{ coil.manufacturer || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">镀层</span>
<span class="popup-value">{{ coil.zincLayer || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">质量状态</span>
<span class="popup-value">{{ coil.qualityStatus || '-' }}</span>
</div>
<div class="popup-row">
<span class="popup-label">逻辑库位</span>
<span class="popup-value">{{ coil.warehouseName || '-' }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { getFuturisticStatusClass } from '../statusUtils'
export default {
name: 'CoilCardCompact',
props: {
coil: { type: Object, default: () => ({}) },
variant: { type: String, default: '' }
},
methods: {
statusClass(status) {
const cls = getFuturisticStatusClass(status)
const map = {
'futuristic-status-success': 'status-success',
'futuristic-status-danger': 'status-danger',
'futuristic-status-warning': 'status-warning',
'futuristic-status-default': 'status-default'
}
return map[cls] || 'status-default'
}
}
}
</script>

View File

@@ -0,0 +1,153 @@
<template>
<div class="coil-card-futuristic">
<div class="coil-futuristic-header">
<span class="coil-futuristic-no">{{ coil.currentCoilNo || '-' }}</span>
<span class="coil-futuristic-weight">{{ coil.netWeight ? coil.netWeight + ' t' : '-' }}</span>
</div>
<div class="coil-futuristic-body">
<div class="coil-icon-container">
<svg class="coil-svg" viewBox="0 0 200 200">
<defs>
<linearGradient :id="gradId1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" :style="'stop-color:' + gradColor1 + ';stop-opacity:0.5'" />
<stop offset="50%" :style="'stop-color:' + gradColor2 + ';stop-opacity:0.4'" />
<stop offset="100%" :style="'stop-color:' + gradColor1 + ';stop-opacity:0.5'" />
</linearGradient>
<linearGradient :id="gradId2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" :style="'stop-color:' + gradColor3 + ';stop-opacity:0.4'" />
<stop offset="100%" :style="'stop-color:' + gradColor4 + ';stop-opacity:0.3'" />
</linearGradient>
</defs>
<circle cx="100" cy="100" r="70" fill="none" :stroke="'url(#' + gradId1 + ')'" stroke-width="2.5">
<animate attributeName="r" values="70;72;70" dur="2s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="60" fill="none" :stroke="'url(#' + gradId2 + ')'" stroke-width="1.8" opacity="0.6" :stroke-dasharray="dashArray">
<animate attributeName="stroke-dashoffset" values="0;377" dur="4s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="45" :fill="'url(#' + gradId1 + ')'" opacity="0.15">
<animate attributeName="opacity" values="0.15;0.3;0.15" dur="3s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="35" :fill="'url(#' + gradId2 + ')'" opacity="0.2" />
<circle cx="100" cy="100" r="25" :fill="'url(#' + gradId1 + ')'" opacity="0.25" />
<circle cx="100" cy="100" r="15" fill="white" opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="30" :stroke="'url(#' + gradId1 + ')'" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="170" y2="100" :stroke="'url(#' + gradId1 + ')'" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="170" :stroke="'url(#' + gradId1 + ')'" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="30" y2="100" :stroke="'url(#' + gradId1 + ')'" stroke-width="1" opacity="0.4" />
<circle cx="100" cy="100" r="4" :fill="gradColor1">
<animate attributeName="r" values="4;6;4" dur="1.5s" repeatCount="indefinite" />
</circle>
</svg>
</div>
<div class="coil-attr-tags">
<div class="attr-tag attr-top">
<div class="attr-content">
<span class="attr-label">材质</span>
<span class="attr-value">{{ coil.material || '-' }}</span>
</div>
</div>
<div class="attr-tag attr-right">
<div class="attr-content">
<span class="attr-label">规格</span>
<span class="attr-value">{{ coil.specification || '-' }}</span>
</div>
</div>
<div class="attr-tag attr-left">
<div class="attr-content">
<span class="attr-label">{{ leftLabel }}</span>
<span class="attr-value" :class="statusClass(coil.qualityStatus)">{{ coil.qualityStatus || '-' }}</span>
</div>
</div>
<div v-if="coil.enterCoilNo && coil.enterCoilNo !== '-'" class="attr-tag attr-top-left">
<div class="attr-content attr-content-small">
<span class="attr-label">入场卷号</span>
<span class="attr-value">{{ coil.enterCoilNo }}</span>
</div>
</div>
<div v-if="coil.supplierCoilNo && coil.supplierCoilNo !== '-'" class="attr-tag attr-top-right">
<div class="attr-content attr-content-small">
<span class="attr-label">厂家原料号</span>
<span class="attr-value">{{ coil.supplierCoilNo }}</span>
</div>
</div>
<div class="attr-tag attr-bottom-left">
<div class="attr-content attr-content-small">
<span class="attr-label">班组</span>
<span class="attr-value">{{ coil.team || '-' }}</span>
</div>
</div>
<div class="attr-tag attr-bottom-right">
<div class="attr-content attr-content-small">
<span class="attr-label">厂家</span>
<span class="attr-value">{{ coil.manufacturer || '-' }}</span>
</div>
</div>
<div class="attr-tag attr-remark">
<div class="attr-content attr-content-remark">
<span class="attr-label">备注</span>
<span class="attr-value">{{ coil.remark || '-' }}</span>
</div>
</div>
</div>
</div>
<div class="coil-futuristic-footer">
<div class="footer-item">
<span class="footer-label">镀层</span>
<span class="footer-value">{{ coil.zincLayer || '-' }}</span>
</div>
<div class="footer-item">
<span class="footer-label">表面处理</span>
<span class="footer-value">{{ coil.surfaceTreatmentDesc || '-' }}</span>
</div>
<div class="footer-item">
<span class="footer-label">逻辑库位</span>
<span class="footer-value">{{ coil.warehouseName || '-' }}</span>
</div>
</div>
</div>
</template>
<script>
import { getFuturisticStatusClass } from '../statusUtils'
let uid = 0
export default {
name: 'CoilCardFuturistic',
props: {
coil: { type: Object, default: () => ({}) },
type: { type: String, default: 'inbound' }
},
data() {
uid++
return { _uid: uid }
},
computed: {
gradId1() { return 'coilGradA_' + this._uid },
gradId2() { return 'coilGradB_' + this._uid },
gradConfig() {
const configs = {
inbound: { c1: '#3b82f6', c2: '#2563eb', c3: '#93c5fd', c4: '#bfdbfe' },
old: { c1: '#6366f1', c2: '#8b5cf6', c3: '#a78bfa', c4: '#c084fc' },
new: { c1: '#10b981', c2: '#34d399', c3: '#a7f3d0', c4: '#fbcfe8' },
}
return configs[this.type] || configs.inbound
},
gradColor1() { return this.gradConfig.c1 },
gradColor2() { return this.gradConfig.c2 },
gradColor3() { return this.gradConfig.c3 },
gradColor4() { return this.gradConfig.c4 },
dashArray() {
return this.type === 'new' ? '50 50' : undefined
},
leftLabel() {
return this.type === 'inbound' ? '质量状态' : '状态'
}
},
methods: {
statusClass(status) {
return getFuturisticStatusClass(status)
}
}
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<div class="section sales-section">
<div class="section-header">
<span class="section-icon">&#128176;</span>
<span class="section-title">{{ title }}</span>
</div>
<div class="section-body" v-if="info.orderId">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="合同号">{{ info.contractCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ info.customer || '-' }}</el-descriptions-item>
<el-descriptions-item label="销售人员">{{ info.salesman || '-' }}</el-descriptions-item>
<el-descriptions-item label="签订时间">{{ info.signTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="交货时间">{{ info.deliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单总额">{{ info.orderAmount || '-' }}</el-descriptions-item>
</el-descriptions>
<h4>技术附件</h4>
<FileList :oss-ids="info.techAnnex" />
<h4>排产函</h4>
<FileList :oss-ids="info.productionSchedule" />
</div>
<div v-else class="empty-state">
<i class="el-icon-document"></i>
<span>{{ emptyText }}</span>
</div>
</div>
</template>
<script>
import FileList from '@/components/FileList'
export default {
name: 'ContractInfo',
components: { FileList },
props: {
title: { type: String, default: '合同信息' },
info: { type: Object, default: () => ({}) },
emptyText: { type: String, default: '未找到相关合同信息' }
}
}
</script>

View File

@@ -0,0 +1,36 @@
<template>
<div class="section basic-info-section">
<div class="section-header">
<span class="section-icon">&#128176;</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&#183;</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>
</div>
</div>
</template>
<script>
import { formatTime } from '../statusUtils'
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' }
}
}
</script>

View File

@@ -0,0 +1,110 @@
<template>
<div class="section inspection-section">
<div class="section-header">
<span class="section-icon">&#128300;</span>
<span class="section-title">检验信息</span>
</div>
<div class="section-body">
<el-table :data="taskList" v-loading="loading" size="small" border stripe style="width: 100%"
@expand-change="onExpand" :row-key="row => row.taskId">
<el-table-column type="expand">
<template slot-scope="props">
<div v-loading="itemLoadingMap[props.row.taskId]" style="padding: 8px;">
<el-table :data="itemMap[props.row.taskId] || []" size="mini" border stripe style="width: 100%">
<el-table-column label="检验项目名称" align="center" prop="itemName" min-width="120" />
<el-table-column label="标准值" align="center" prop="standardValue" width="80" />
<el-table-column label="上限" align="center" prop="upperLimit" width="80" />
<el-table-column label="下限" align="center" prop="lowerLimit" width="80" />
<el-table-column label="单位" align="center" prop="unit" width="60" />
<el-table-column label="定性/定量" align="center" prop="itemType" width="80" />
<el-table-column label="检验值" align="center" prop="inspectValue" width="100" />
<el-table-column label="是否合格" align="center" prop="isQualified" width="80" />
<el-table-column label="判定结果" align="center" prop="judgeResult" width="100" />
<el-table-column label="检验人" align="center" prop="inspectUser" width="80" />
<el-table-column label="检验时间" align="center" prop="inspectTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.inspectTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="100" />
</el-table>
<div v-if="!itemMap[props.row.taskId] || itemMap[props.row.taskId].length === 0"
style="text-align:center;padding:16px;color:#999;font-size:13px;">
暂无检验明细
</div>
</div>
</template>
</el-table-column>
<el-table-column label="任务编号" align="center" prop="taskCode" width="160" />
<el-table-column label="任务类型" align="center" prop="taskType" width="100" />
<el-table-column label="所属单位" align="center" prop="belongCompany" width="120" />
<el-table-column label="方案名称" align="center" prop="schemeName" min-width="120" />
<el-table-column label="状态" align="center" prop="status" width="90">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="info" size="small">待检验</el-tag>
<el-tag v-else-if="scope.row.status === 1 || scope.row.status === '1'" type="warning" size="small">已检验</el-tag>
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="success" size="small">已审核</el-tag>
<span v-else>{{ scope.row.status }}</span>
</template>
</el-table-column>
<el-table-column label="检验人" align="center" prop="inspectUser" width="80" />
<el-table-column label="审核人" align="center" prop="auditUser" width="80" />
<el-table-column label="最终结果" align="center" prop="result" min-width="100" />
<el-table-column label="关联钢卷" align="center" width="140">
<template slot-scope="scope">
<div v-if="scope.row.coilList && scope.row.coilList.length > 0"
style="display: flex; flex-wrap: wrap; gap: 4px;">
<div v-for="(coil, index) in scope.row.coilList" :key="coil.coilId || index">
<CurrentCoilNo :currentCoilNo="coil.currentCoilNo || coil.coilNo || ''" />
</div>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="100" show-overflow-tooltip />
</el-table>
<div v-if="taskList.length === 0 && !loading"
style="text-align:center;padding:24px 0;color:#999;font-size:14px;">
<i class="el-icon-document" style="font-size:48px;display:block;margin-bottom:8px;color:#ddd;"></i>
未找到相关检验信息
</div>
</div>
</div>
</template>
<script>
import { listInspectionItem } from '@/api/mes/qc/inspectionItem'
import CurrentCoilNo from '@/components/KLPService/Renderer/CurrentCoilNo.vue'
export default {
name: 'InspectionInfo',
components: { CurrentCoilNo },
props: {
taskList: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
},
data() {
return {
itemMap: {},
itemLoadingMap: {}
}
},
methods: {
async onExpand(row, expandedRows) {
if (!expandedRows || !expandedRows.includes(row)) return
const taskId = row.taskId
if (this.itemMap[taskId]) return
this.$set(this.itemLoadingMap, taskId, true)
try {
const res = await listInspectionItem({ taskId, pageNum: 1, pageSize: 100 })
this.$set(this.itemMap, taskId, res.rows || [])
} catch (e) {
console.error('获取检验明细异常:', e)
this.$set(this.itemMap, taskId, [])
} finally {
this.$set(this.itemLoadingMap, taskId, false)
}
}
}
}
</script>

View File

@@ -0,0 +1,158 @@
<template>
<div class="section trace-section">
<div class="section-header">
<span class="section-icon">&#128259;</span>
<span class="section-title">生命周期追踪</span>
</div>
<div class="section-body">
<coil-trace-result :trace-result="traceResult">
<template #stepBody="{ step, compact }">
<div class="step-item">
<template v-if="step.action === '创建'">
<div v-if="compact" class="step-content step-content-compact step-content-inbound">
<div class="coil-card-wrapper inbound-coil-wrapper">
<div class="coil-card-header inbound-header">
<span class="card-label">&#128230; 入库</span>
</div>
<CoilCardCompact v-for="(coil, idx) in step.newCoilInfoList" :key="idx" :coil="coil"
v-if="coil && coil.currentCoilNo !== '-'" variant="inbound" />
<div v-if="isEmpty(step.newCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
<div class="action-center-compact">
<div class="action-arrow action-arrow-inbound">
<i class="el-icon-download"></i>
</div>
<div class="action-info">
<el-tag size="mini" type="primary" class="action-tag">入库</el-tag>
<span class="action-operator">{{ step.operation }}</span>
<span class="action-time">{{ step.time }}</span>
</div>
</div>
</div>
<div v-else class="step-content step-content-inbound">
<div class="coil-card-wrapper inbound-coil-wrapper">
<div class="coil-card-header inbound-header">
<span class="card-label">&#128230; 入库</span>
</div>
<CoilCardFuturistic v-for="(coil, idx) in step.newCoilInfoList" :key="idx" :coil="coil" type="inbound"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(step.newCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
<div class="action-center">
<div class="action-arrow action-arrow-inbound">
<i class="el-icon-download"></i>
</div>
<div class="action-info">
<el-tag size="mini" type="primary" class="action-tag">入库</el-tag>
<span class="action-operator">{{ step.operation }}</span>
<span class="action-time">{{ step.time }}</span>
</div>
</div>
</div>
</template>
<template v-else>
<div v-if="compact" class="step-content-compact-change">
<div class="coil-change-wrapper">
<div class="coil-change-header">
<span class="change-label">变更前</span>
</div>
<CoilCardCompact v-for="(coil, idx) in step.oldCoilInfoList" :key="idx" :coil="coil" variant="old"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(step.oldCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
<div class="action-center-compact">
<div class="action-arrow">
<i class="el-icon-arrow-down"></i>
</div>
<div class="action-info">
<el-tag size="mini" type="info" class="action-tag">{{ step.action }}</el-tag>
<span class="action-operator">{{ step.operation }}</span>
<span class="action-time">{{ step.time }}</span>
</div>
</div>
<div class="coil-change-wrapper new">
<div class="coil-change-header">
<span class="change-label">变更后</span>
</div>
<CoilCardCompact v-for="(coil, idx) in step.newCoilInfoList" :key="idx" :coil="coil" variant="new"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(step.newCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
</div>
<div v-else class="step-content">
<div class="coil-card-wrapper old-coil-wrapper">
<div class="coil-card-header">
<span class="card-label">变更前</span>
</div>
<CoilCardFuturistic v-for="(coil, idx) in step.oldCoilInfoList" :key="idx" :coil="coil" type="old"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(step.oldCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
<div class="action-center">
<div class="action-arrow">
<i class="el-icon-right"></i>
</div>
<div class="action-info">
<el-tag size="mini" type="info" class="action-tag">{{ step.action }}</el-tag>
<span class="action-operator">{{ step.operation }}</span>
<span class="action-time">{{ step.time }}</span>
</div>
</div>
<div class="coil-card-wrapper new-coil-wrapper">
<div class="coil-card-header">
<span class="card-label">变更后</span>
</div>
<CoilCardFuturistic v-for="(coil, idx) in step.newCoilInfoList" :key="idx" :coil="coil" type="new"
v-if="coil && coil.currentCoilNo !== '-'" />
<div v-if="isEmpty(step.newCoilInfoList)" class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
</div>
</template>
</div>
</template>
</coil-trace-result>
<ShipmentCard :coilInfo="coilInfo" />
</div>
</div>
</template>
<script>
import CoilTraceResult from '@/views/wms/coil/panels/CoilTraceResult.vue'
import CoilCardFuturistic from './CoilCardFuturistic.vue'
import CoilCardCompact from './CoilCardCompact.vue'
import ShipmentCard from './ShipmentCard.vue'
export default {
name: 'LifecycleTrace',
components: { CoilTraceResult, CoilCardFuturistic, CoilCardCompact, ShipmentCard },
props: {
traceResult: { type: Object, default: null },
coilInfo: { type: Object, default: () => ({}) }
},
methods: {
isEmpty(list) {
return !list || list.length === 0 || list.every(c => !c || c.currentCoilNo === '-')
}
}
}
</script>

View File

@@ -0,0 +1,434 @@
<template>
<div class="section production-section">
<div class="section-header">
<span class="section-icon">&#128200;</span>
<span class="section-title">生产工艺数据</span>
<el-tag v-if="segLoading || realtimeLoading" size="mini" type="info" style="margin-left:8px">加载中</el-tag>
<span v-if="hasPerfData" class="perf-count">({{ perfSegCount }} )</span>
</div>
<div class="section-body">
<el-tabs v-model="activeTab" size="small" class="perf-tabs" @tab-click="handleTabSwitch">
<el-tab-pane label="趋势参数" name="trend">
<div v-if="!hasPerfData && !segLoading" class="no-data-hint">暂无生产数据</div>
<div v-else-if="segLoading" class="no-data-hint">加载中</div>
<div v-else class="trend-layout">
<div class="trend-tree">
<div v-for="group in trendGroups" :key="group.label" class="tree-group">
<div class="tree-group-label" @click="toggleGroup(group.label)">
<i :class="expandedGroups[group.label] ? 'el-icon-caret-bottom' : 'el-icon-caret-right'" />
{{ group.label }}
</div>
<div v-show="expandedGroups[group.label]" class="tree-children">
<div v-for="item in group.children" :key="item.col" class="tree-item"
:class="{ active: selectedParam && selectedParam.col === item.col }"
@click="selectParam(item)">{{ item.label }}</div>
</div>
</div>
</div>
<div class="trend-chart-area">
<div v-if="!selectedParam" class="no-data-hint">&#8592; 点击左侧参数查看曲线</div>
<div ref="trendChart"
:style="{ display: selectedParam ? 'block' : 'none', height: '100%', width: '100%' }" />
</div>
</div>
</el-tab-pane>
<el-tab-pane label="厚度曲线" name="thickness">
<div v-if="!gaugeRows || !gaugeRows.length" class="no-data-hint">暂无厚度数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartGauge1" class="chart-box" />
<div ref="chartGauge2" class="chart-box" />
<div ref="chartGauge3" class="chart-box" />
<div ref="chartGauge4" class="chart-box" />
</div>
</el-tab-pane>
<el-tab-pane label="带钢板形" name="flatness3d">
<div v-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll">
<div ref="chartFlatness3d" class="chart-box chart-box-tall" />
</div>
</el-tab-pane>
<el-tab-pane label="板形曲线" name="flatness">
<div v-if="!shapeRows || !shapeRows.length" class="no-data-hint">暂无板形数据</div>
<div v-else class="charts-scroll charts-grid">
<div ref="chartFlatDev" class="chart-box" />
<div ref="chartTilt" class="chart-box" />
<div ref="chartWrBend" class="chart-box" />
<div ref="chartIrBend" class="chart-box" />
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import 'echarts-gl'
const TREND_GROUPS = [
{
label: '张力',
children: [
{ label: '开卷张力', col: 'PORTENS' },
{ label: '入口活套张力', col: 'ENLTENS' },
{ label: '拉矫张力', col: 'TLTENS' },
{ label: '酸洗张力', col: 'PLTENS' },
{ label: '出口活套张力', col: 'CXLTENS' },
{ label: '圆盘剪张力', col: 'TRIMTENS' }
]
},
{
label: '速度',
children: [
{ label: '开卷速度', col: 'PORSPEED' },
{ label: '酸洗速度', col: 'PLSPEED' },
{ label: '圆盘剪速度', col: 'TRIMSPEED' },
{ label: '轧机入口速度', col: 'MILLENTRYSPEED' },
{ label: '轧机出口速度', col: 'MILLEXITSPEED' }
]
},
{
label: '拉矫机',
children: [
{ label: '1#插入量', col: 'TLMESH1' },
{ label: '2#插入量', col: 'TLMESH2' },
{ label: '3#插入量', col: 'TLMESH3' },
{ label: '延伸率', col: 'TLELONG' }
]
},
{
label: '酸洗段',
children: [
{ label: '1#温度', col: 'TK1TEMP' },
{ label: '2#温度', col: 'TK2TEMP' },
{ label: '3#温度', col: 'TK3TEMP' },
{ label: '漂洗温度', col: 'RINSETEMP' }
]
}
]
const GAUGE_COLS = [
{ col: 'THICK0', title: '入口测厚仪 [mm]' },
{ col: 'THICK1', title: '1架出口厚度 [mm]' },
{ col: 'THICK4', title: '末架出口厚度 [mm]' },
{ col: 'EXIT_SPEED', title: '轧制速度 [m/min]' }
]
const SHAPE_SCALAR_COLS = [
{ col: 'ABSDEVIATION', title: '总板形偏差 [IU]' },
{ col: 'TILT', title: '末架倾斜量 [mm]' },
{ col: 'WRBEND', title: '工作辊弯辊力 [kN]' },
{ col: 'IRBEND', title: '中间辊弯辊力 [kN]' }
]
function calcYRange(vals) {
const nums = vals.filter(v => v != null && isFinite(Number(v))).map(Number)
if (!nums.length) return {}
const min = Math.min(...nums)
const max = Math.max(...nums)
if (min === max) {
const base = Math.abs(min) || 1
return { min: parseFloat((min - base * 0.2).toFixed(4)), max: parseFloat((max + base * 0.2).toFixed(4)) }
}
const pad = (max - min) * 0.15
return { min: parseFloat((min - pad).toFixed(4)), max: parseFloat((max + pad).toFixed(4)) }
}
function makeLine(title, xData, yData) {
const range = calcYRange(yData)
return {
title: { text: title, textStyle: { fontSize: 12, fontWeight: 'normal' }, top: 4, left: 8 },
tooltip: { trigger: 'axis' },
grid: { top: 36, bottom: 28, left: 8, right: 16, containLabel: true },
xAxis: { type: 'category', data: xData, name: 'pos(m)', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 } },
yAxis: { type: 'value', min: range.min, max: range.max, nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 10 } },
dataZoom: [
{ type: 'inside', xAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true },
{ type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: false, moveOnMouseMove: true }
],
series: [{ name: title, type: 'line', smooth: false, symbol: 'none', lineStyle: { width: 1 }, data: yData }]
}
}
function getRowVal(row, col) {
const v = row[col] !== undefined ? row[col] : row[col.toLowerCase()]
return v == null ? null : Number(v)
}
function xLocData(rows) {
return rows.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(1)
})
}
export default {
name: 'ProductionCharts',
props: {
segData: { type: Object, default: null },
gaugeRows: { type: Array, default: null },
shapeRows: { type: Array, default: null },
perfSegCount: { type: Number, default: 0 },
segLoading: { type: Boolean, default: false },
realtimeLoading: { type: Boolean, default: false }
},
data() {
return {
activeTab: 'trend',
trendGroups: TREND_GROUPS,
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedParam: null,
trendChartInst: null,
_trendResizeFn: null,
chartInstances: [],
resizeHandler: null
}
},
computed: {
hasPerfData() {
return this.segData && this.perfSegCount > 0
}
},
mounted() {
if (this.hasPerfData) {
this.selectParam(TREND_GROUPS[0].children[0])
this.renderCurrentTab()
} else if (this.gaugeRows?.length || this.shapeRows?.length) {
this.$nextTick(() => this.renderCurrentTab())
}
},
beforeDestroy() {
this.disposeAll()
},
watch: {
segData(val) {
if (val && this.perfSegCount > 0) {
this.$nextTick(() => {
this.selectParam(this.trendGroups[0].children[0])
})
}
},
gaugeRows(val) {
if (val && val.length && this.activeTab === 'thickness') {
this.$nextTick(() => this.renderCurrentTab())
}
},
shapeRows(val) {
if (val && val.length && (this.activeTab === 'flatness3d' || this.activeTab === 'flatness')) {
this.$nextTick(() => this.renderCurrentTab())
}
}
},
methods: {
toggleGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label])
},
selectParam(item) {
this.selectedParam = item
this.$nextTick(() => this.renderTrendChart())
},
handleTabSwitch() {
this.$nextTick(() => {
if (this.activeTab === 'trend') {
if (this.selectedParam && this.segData) this.renderTrendChart()
} else {
this.renderCurrentTab()
}
})
},
renderCurrentTab() {
this.disposeSideCharts()
if (this.activeTab === 'thickness' && this.gaugeRows?.length) this.renderGauge()
if (this.activeTab === 'flatness3d' && this.shapeRows?.length) this.renderFlatness3d()
if (this.activeTab === 'flatness' && this.shapeRows?.length) this.renderFlatnessScalar()
},
renderTrendChart() {
if (!this.selectedParam || !this.segData) return
const el = this.$refs.trendChart
if (!el) return
if (!this.trendChartInst || this.trendChartInst.isDisposed()) {
this.trendChartInst = echarts.init(el)
const resizeFn = () => this.trendChartInst && !this.trendChartInst.isDisposed() && this.trendChartInst.resize()
window.addEventListener('resize', resizeFn)
this._trendResizeFn = resizeFn
}
const x = this.segX()
const yData = this.seg(this.selectedParam.col)
this.trendChartInst.setOption(makeLine(this.selectedParam.label, x, yData), true)
},
seg(col) {
const s = this.segData
const arr = s[col] !== undefined ? s[col] : (s[col.toLowerCase()] || [])
return arr.map(v => v == null ? null : Number(Number(v).toFixed(3)))
},
segX() {
const s = this.segData
const arr = s['STARTPOS'] !== undefined ? s['STARTPOS'] : (s['startpos'] || [])
return arr.map(v => v == null ? '' : Number(v).toFixed(1))
},
makeChart(ref, option) {
const el = this.$refs[ref]
if (!el) return null
const chart = echarts.init(el)
chart.setOption(option)
return chart
},
setupResize() {
this.resizeHandler = () => this.chartInstances.forEach(c => {
if (c && !c.isDisposed()) c.resize()
})
window.addEventListener('resize', this.resizeHandler)
},
renderGauge() {
const rows = this.gaugeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartGauge1', 'chartGauge2', 'chartGauge3', 'chartGauge4']
const charts = refs.map((ref, i) => {
const { col, title } = GAUGE_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(4))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
renderFlatness3d() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const firstRow = rows[0]
const high = parseInt(getRowVal(firstRow, 'HIGHZONEID')) || 26
const low = parseInt(getRowVal(firstRow, 'LOWZONEID')) || 1
const numZones = Math.min(Math.max(high - low + 1, 1), 26)
const zoneCols = Array.from({ length: numZones }, (_, i) =>
`VALUES${String(low + i).padStart(2, '0')}`
)
const step = Math.max(1, Math.floor(rows.length / 200))
const sampled = rows.filter((_, i) => i % step === 0)
const numX = sampled.length
const xLabels = sampled.map(r => {
const v = r.XLOCATION !== undefined ? r.XLOCATION : r.xlocation
return v == null ? '' : Number(v).toFixed(0)
})
let minV = Infinity, maxV = -Infinity
sampled.forEach(row => {
zoneCols.forEach(col => {
const v = getRowVal(row, col)
if (v != null) {
if (v < minV) minV = v
if (v > maxV) maxV = v
}
})
})
if (!isFinite(minV)) { minV = -30; maxV = 30 }
const absMax = Math.max(Math.abs(minV), Math.abs(maxV))
const channelLines = zoneCols.map((col, yi) => ({
type: 'line3D',
coordinateSystem: 'cartesian3D',
data: sampled.map((row, xi) => {
const v = getRowVal(row, col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean),
lineStyle: { width: 2, opacity: 1 }
}))
const xStride = Math.max(1, Math.floor(numX / 60))
const crossLines = []
for (let xi = 0; xi < numX; xi += xStride) {
const pts = zoneCols.map((col, yi) => {
const v = getRowVal(sampled[xi], col)
return v == null ? null : [xi, yi, parseFloat(v.toFixed(2))]
}).filter(Boolean)
if (pts.length > 1) {
crossLines.push({ type: 'line3D', coordinateSystem: 'cartesian3D', data: pts, lineStyle: { width: 1.5, opacity: 1 } })
}
}
const series = [...channelLines, ...crossLines]
const option = {
title: { text: '实测平直度 [IU]', textStyle: { fontSize: 13, fontWeight: 'normal' }, top: 6, left: 10 },
tooltip: {},
visualMap: {
show: true, dimension: 2, min: -absMax, max: absMax, calculable: true,
orient: 'vertical', right: 10, top: 'center', textStyle: { fontSize: 10 },
inRange: {
color: ['#8B0000', '#CC2200', '#E84C00', '#F46D43',
'#FDAE61', '#FEE08B', '#66BD63', '#1A9850', '#006837',
'#3288BD', '#5E4FA2', '#762A83']
}
},
grid3D: {
boxWidth: 200, boxHeight: 60, boxDepth: 80,
viewControl: { projection: 'orthographic', autoRotate: false, rotateSensitivity: 1, zoomSensitivity: 1 },
light: { main: { intensity: 1.2, shadow: false }, ambient: { intensity: 0.3 } }
},
xAxis3D: {
type: 'value', name: '位置', min: 0, max: numX - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 9, formatter: v => xLabels[Math.round(v)] || '' }
},
yAxis3D: {
type: 'value', name: '通道', min: 0, max: numZones - 1,
nameTextStyle: { fontSize: 10 },
axisLabel: { fontSize: 9, formatter: v => String(low + Math.round(v)) }
},
zAxis3D: { type: 'value', name: 'IU', nameTextStyle: { fontSize: 10 }, axisLabel: { fontSize: 9 } },
series
}
const el = this.$refs.chartFlatness3d
if (!el) return
const chart = echarts.init(el)
chart.setOption(option)
this.chartInstances = [chart]
this.setupResize()
},
renderFlatnessScalar() {
const rows = this.shapeRows
if (!rows || !rows.length) return
const xData = xLocData(rows)
const refs = ['chartFlatDev', 'chartTilt', 'chartWrBend', 'chartIrBend']
const charts = refs.map((ref, i) => {
const { col, title } = SHAPE_SCALAR_COLS[i]
const yData = rows.map(r => {
const v = getRowVal(r, col)
return v == null ? null : parseFloat(v.toFixed(3))
})
return this.makeChart(ref, makeLine(title, xData, yData))
})
this.chartInstances = charts.filter(Boolean)
this.setupResize()
},
disposeSideCharts() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler)
this.resizeHandler = null
}
this.chartInstances.forEach(c => { if (c && !c.isDisposed()) c.dispose() })
this.chartInstances = []
},
disposeAll() {
this.disposeSideCharts()
if (this._trendResizeFn) {
window.removeEventListener('resize', this._trendResizeFn)
this._trendResizeFn = null
}
if (this.trendChartInst && !this.trendChartInst.isDisposed()) {
this.trendChartInst.dispose()
this.trendChartInst = null
}
}
}
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div class="section sales-section">
<div class="section-header">
<span class="section-icon">&#128176;</span>
<span class="section-title">订单异议</span>
</div>
<div class="section-body" v-if="list.length > 0">
<el-table :data="list" size="small" border stripe style="width: 100%">
<el-table-column label="产品类别" align="center" prop="productCategory" />
<el-table-column label="反馈日期" align="center" prop="returnDate" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.returnDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="投诉情况" align="center" prop="complaintContent" show-overflow-tooltip />
<el-table-column label="客户诉求" align="center" prop="customerDemand" show-overflow-tooltip />
<el-table-column label="状态" align="center" prop="objectionStatus">
<template slot-scope="scope">
<el-tag v-if="scope.row.objectionStatus === 0" type="danger">待处理</el-tag>
<el-tag v-else-if="scope.row.objectionStatus === 1" type="success">已处理</el-tag>
<el-tag v-else-if="scope.row.objectionStatus === 2" type="info">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="处理人" align="center" prop="handleUser" />
<el-table-column label="处理时间" align="center" prop="handleTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.handleTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
</div>
<div v-else class="empty-state">
<i class="el-icon-document"></i>
<span>未找到相关订单异议记录</span>
</div>
</div>
</template>
<script>
export default {
name: 'SalesObjectionTable',
props: {
list: { type: Array, default: () => [] }
}
}
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="step-item">
<div class="step-content">
<div class="coil-card-wrapper inbound-coil-wrapper">
<div class="coil-card-header inbound-header">
<span class="card-label">&#128666; 发货</span>
</div>
<div class="coil-card-futuristic">
<div class="coil-futuristic-body">
<div class="coil-icon-container">
<svg class="coil-svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="coilGradShip1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:0.5" />
<stop offset="50%" style="stop-color:#ef4444;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#f59e0b;stop-opacity:0.5" />
</linearGradient>
<linearGradient id="coilGradShip2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#fcd34d;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#fca5a5;stop-opacity:0.3" />
</linearGradient>
</defs>
<circle cx="100" cy="100" r="70" fill="none" stroke="url(#coilGradShip1)" stroke-width="2.5">
<animate attributeName="r" values="70;72;70" dur="2s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="60" fill="none" stroke="url(#coilGradShip2)" stroke-width="1.8" opacity="0.6">
<animate attributeName="stroke-dashoffset" values="0;377" dur="4s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="45" fill="url(#coilGradShip1)" opacity="0.15">
<animate attributeName="opacity" values="0.15;0.3;0.15" dur="3s" repeatCount="indefinite" />
</circle>
<circle cx="100" cy="100" r="35" fill="url(#coilGradShip2)" opacity="0.2" />
<circle cx="100" cy="100" r="25" fill="url(#coilGradShip1)" opacity="0.25" />
<circle cx="100" cy="100" r="15" fill="white" opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="30" stroke="url(#coilGradShip1)" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="170" y2="100" stroke="url(#coilGradShip1)" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="170" stroke="url(#coilGradShip1)" stroke-width="1" opacity="0.4" />
<line x1="100" y1="100" x2="30" y2="100" stroke="url(#coilGradShip1)" stroke-width="1" opacity="0.4" />
<circle cx="100" cy="100" r="4" fill="#f59e0b">
<animate attributeName="r" values="4;6;4" dur="1.5s" repeatCount="indefinite" />
</circle>
</svg>
</div>
<div class="coil-attr-tags">
<div class="attr-tag attr-top">
<div class="attr-content">
<span class="attr-label">发货状态</span>
<span class="attr-value" :class="coilInfo.status === 1 ? 'futuristic-status-success' : 'futuristic-status-warning'">
{{ coilInfo.status === 1 ? '已发货' : '未发货' }}
</span>
</div>
</div>
</div>
</div>
<div class="coil-futuristic-footer" v-if="coilInfo.status === 1">
<div class="footer-item">
<span class="footer-label">发货人</span>
<span class="footer-value">{{ coilInfo.exportByName || '-' }}</span>
</div>
<div class="footer-item">
<span class="footer-label">发货时间</span>
<span class="footer-value">{{ formatTime(coilInfo.exportTime) }}</span>
</div>
</div>
<div class="coil-futuristic-footer" v-else>
<div class="footer-item" style="flex: 1;">
<span class="footer-label">状态</span>
<span class="footer-value">等待发货</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { formatTime } from '../statusUtils'
export default {
name: 'ShipmentCard',
props: {
coilInfo: { type: Object, default: () => ({}) }
},
methods: {
formatTime(time) {
return formatTime(time)
}
}
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<div class="section combined-section">
<div class="section-header">
<span class="section-icon">&#128203;</span>
<span class="section-title">移库记录 & 调拨记录</span>
</div>
<div class="section-body tables-row">
<div class="table-wrapper warehouse-table">
<div class="table-title">移库记录</div>
<div class="table-container">
<el-table :data="warehouseList" size="small" border stripe style="width: 100%">
<el-table-column prop="createTime" label="操作时间"></el-table-column>
<el-table-column prop="operationType" label="操作类型">
<template slot-scope="scope">
<span v-if="scope.row.operationType === 1">收货</span>
<span v-else-if="scope.row.operationType === 2">加工</span>
<span v-else-if="scope.row.operationType === 3">调拨</span>
<span v-else-if="scope.row.operationType === 4">发货</span>
</template>
</el-table-column>
<el-table-column prop="inOutType" label="出入库">
<template slot-scope="scope">
{{ scope.row.inOutType === 1 ? '入库' : '出库' }}
</template>
</el-table-column>
<el-table-column prop="warehouse.actualWarehouseName" label="库位">
<template slot-scope="scope">
{{ scope.row.warehouse.actualWarehouseName || '-' }}
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip></el-table-column>
</el-table>
</div>
</div>
<div class="table-wrapper transfer-table">
<div class="table-title">调拨记录</div>
<div class="table-container">
<el-table :data="transferList" size="small" border stripe style="width: 100%">
<el-table-column prop="type" label="类型" min-width="100"></el-table-column>
<el-table-column label="变更前">
<template slot-scope="scope">
{{ scope.row.before }}
</template>
</el-table-column>
<el-table-column label="变更后">
<template slot-scope="scope">
{{ scope.row.after }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间"></el-table-column>
<el-table-column prop="remark" label="备注" show-overflow-tooltip></el-table-column>
</el-table>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'TransferRecords',
props: {
warehouseList: { type: Array, default: () => [] },
transferList: { type: Array, default: () => [] }
}
}
</script>

View File

@@ -0,0 +1,451 @@
<template>
<div class="coil-info-page" v-loading="loading">
<div class="content-container">
<BasicInfoSection :coilInfo="coilInfo" />
<CostInfoSection :coilInfo="coilInfo" :traceResult="traceResult"
:hoardingDays="hoardingDays" :hoardingCost="hoardingCost" />
<LifecycleTrace :traceResult="traceResult" :coilInfo="coilInfo" />
<ContractInfo title="生产合同信息" :info="salesInfo" emptyText="未找到相关生产合同信息" />
<ContractInfo title="发货合同信息" :info="deliveryOrderInfo" emptyText="未找到相关发货合同信息" />
<SalesObjectionTable :list="salesObjectionInfo" />
<TransferRecords :warehouseList="warehouseTranferList" :transferList="tranferList" />
<div class="section abnormal-section">
<div class="section-header">
<span class="section-icon">&#9888;&#65039;</span>
<span class="section-title">异常信息</span>
</div>
<div class="section-body">
<abnormal-table ref="abnormalTable" :list="abmornalList" :editable="false" :show-coil="false"
:coil-info="coilInfo" />
</div>
</div>
<div class="section abnormal-section">
<div class="section-header">
<span class="section-icon">&#128218;</span>
<span class="section-title">异常历史</span>
</div>
<div class="section-body">
<abnormal-table ref="historyAbnormalTable" :list="allAbmornalList" :editable="false" :show-coil="true" />
</div>
</div>
<InspectionInfo :taskList="inspectionTaskList" :loading="inspectionLoading" />
<ProductionCharts v-if="isColdHardCoil"
:segData="segData" :gaugeRows="gaugeRows" :shapeRows="shapeRows"
:perfSegCount="perfSegCount" :segLoading="segLoading" :realtimeLoading="realtimeLoading" />
</div>
</div>
</template>
<script>
import { getMaterialCoil, listMaterialCoil, getMaterialCoilTrace, getDeliveryOrderInfo } from '@/api/wms/coil'
import { getCoilWarehouseOperationLogByCoilId } from '@/api/wms/coilWarehouseOperationLog'
import { listSalesObjection } from "@/api/crm/salesObjection"
import { listCoilAbnormal } from '@/api/wms/coilAbnormal'
import { listTransferOrderItem } from '@/api/wms/transferOrderItem'
import { listCoilQualityRejudge } from '@/api/wms/coilQualityRejudge'
import { getTimingSegByEncoilId, getTimingPlanDetailByHotcoilId, getTimingRealtimeData } from '@/api/l2/timing'
import { listInspectionTask } from "@/api/mes/qc/inspectionTask"
import AbnormalTable from '@/views/wms/coil/components/AbnormalTable.vue'
import { formatTime } from './statusUtils'
import BasicInfoSection from './components/BasicInfoSection.vue'
import CostInfoSection from './components/CostInfoSection.vue'
import LifecycleTrace from './components/LifecycleTrace.vue'
import ContractInfo from './components/ContractInfo.vue'
import SalesObjectionTable from './components/SalesObjectionTable.vue'
import TransferRecords from './components/TransferRecords.vue'
import InspectionInfo from './components/InspectionInfo.vue'
import ProductionCharts from './components/ProductionCharts.vue'
export default {
name: 'CoilInfo',
components: {
AbnormalTable,
BasicInfoSection,
CostInfoSection,
LifecycleTrace,
ContractInfo,
SalesObjectionTable,
TransferRecords,
InspectionInfo,
ProductionCharts
},
data() {
return {
loading: false,
coilInfoLoading: false,
traceLoading: false,
traceResult: null,
coilDetails: {},
loadingCoilDetails: false,
coilInfo: {},
coilList: [],
coilId: '',
warehouseTranferList: [],
abmornalList: [],
allAbmornalList: [],
transferOrderItemList: [],
coilQualityRejudgeList: [],
tranferList: [],
perfLoading: false,
perfSeries: null,
perfSegCount: 0,
segLoading: false,
realtimeLoading: false,
segData: null,
gaugeRows: null,
shapeRows: null,
deliveryOrderInfo: {},
salesObjectionInfo: [],
inspectionTaskList: [],
inspectionLoading: false,
standardSteps: [],
}
},
computed: {
salesInfo() {
return this.coilInfo.orderList?.[0] || {}
},
isColdHardCoil() {
return this.coilInfo.itemName && this.coilInfo.itemName.includes('冷硬卷')
},
hasPerfData() {
return this.perfSeries && this.perfSegCount > 0
},
hoardingDays() {
const inboundTime = this.getInboundTime()
if (!inboundTime) return 0
const inboundDate = new Date(inboundTime)
const endDate = this.coilInfo.status == 1
? new Date(this.coilInfo.exportTime)
: new Date()
const diffTime = endDate.getTime() - inboundDate.getTime()
return Math.max(0, Math.floor(diffTime / (1000 * 60 * 60 * 24)))
},
hoardingCost() {
const netWeight = parseFloat(this.coilInfo.netWeight) || 0
return (this.hoardingDays * netWeight).toFixed(2)
}
},
async created() {
this.coilId = this.$route.params.coilId
this.loading = true
await this.getCoilInfo()
await this.getTraceInfo()
await this.getHistoryAbnormalList()
await this.getAbnormalList()
await this.getWarehouseTransferList()
await this.getTransferOrderItemList()
await this.getCoilQualityRejudgeList()
await this.fetchDeliveryOrderInfo()
this.mergeTransferList()
await this.getSalesObjectionList()
await this.getInspectionTasks()
if (this.isColdHardCoil) {
await this.loadProductionData()
}
this.loading = false
},
methods: {
async getCoilInfo() {
const res = await getMaterialCoil(this.coilId)
this.coilInfo = res.data || {}
},
async fetchDeliveryOrderInfo() {
const res = await getDeliveryOrderInfo(this.coilId)
this.deliveryOrderInfo = res.data || {}
},
async getSalesObjectionList() {
const res = await listSalesObjection({ coilIds: this.coilId })
this.salesObjectionInfo = res.rows || []
},
async getTraceInfo() {
const res = await getMaterialCoilTrace({
coilId: this.coilId,
currentCoilNo: this.coilInfo.currentCoilNo
})
this.traceResult = res.data
this.standardSteps = this.traceResult.steps?.map(step => this.formatStep(step)) || []
this.fetchCoilDetails()
},
async getAbnormalList() {
const res = await listCoilAbnormal({ coilId: this.coilId })
this.abmornalList = res.rows || []
},
async getHistoryAbnormalList() {
const coilIds = this.traceResult.steps?.map(step => step.old_coil_id).filter(Boolean) || []
if (coilIds.length === 0) return
const res = await listCoilAbnormal({ coilIds: coilIds.join(',') })
this.allAbmornalList = res.rows || []
},
async getWarehouseTransferList() {
const res = await getCoilWarehouseOperationLogByCoilId({ coilId: this.coilId })
this.warehouseTranferList = res.data || []
},
async getTransferOrderItemList() {
const res = await listTransferOrderItem({ coilId: this.coilId, pageNum: 1, pageSize: 100 })
this.transferOrderItemList = res.rows || []
},
async getCoilQualityRejudgeList() {
const res = await listCoilQualityRejudge({ coilId: this.coilId, pageNum: 1, pageSize: 100 })
this.coilQualityRejudgeList = res.rows || []
},
mergeTransferList() {
const list = []
this.transferOrderItemList.forEach(item => {
list.push({
type: '批量调拨',
before: '逻辑库:' + (item.warehouseNameBefore || '-'),
after: '逻辑库:' + (item.warehouseNameAfter || '-'),
createTime: item.createTime || '-',
remark: item.remark || '-',
...item
})
})
this.coilQualityRejudgeList.forEach(item => {
list.push({
...item,
type: '技术部改判',
before: '质量状态:' + (item.beforeQuality || '-'),
after: '质量状态:' + (item.afterQuality || '-'),
createTime: item.createTime || '-',
remark: item.rejudgeReason,
})
})
list.sort((a, b) => {
const timeA = new Date(a.createTime || 0).getTime()
const timeB = new Date(b.createTime || 0).getTime()
return timeB - timeA
})
this.tranferList = list
},
async getInspectionTasks() {
this.inspectionLoading = true
try {
const res = await listInspectionTask({ enterCoilNos: this.coilInfo.enterCoilNo, pageNum: 1, pageSize: 100 })
this.inspectionTaskList = res.rows || []
} catch (e) {
console.error('获取检验任务异常:', e)
this.inspectionTaskList = []
} finally {
this.inspectionLoading = false
}
},
getInboundTime() {
if (!this.traceResult || !this.traceResult.steps) {
return this.coilInfo.createTime || null
}
const createStep = this.traceResult.steps.find(step =>
step.action === '新增' || step.action === '创建'
)
if (createStep) {
return createStep.create_time || createStep.update_time || createStep.time
}
return this.coilInfo.createTime || null
},
formatTime(timeStamp) {
return formatTime(timeStamp)
},
formatStep(originalStep) {
const standardStep = {
action: '', time: '-', operation: '-',
oldCoilIds: [], newCoilIds: [],
changedFields: '', oldCoilInfoList: [], newCoilInfoList: [],
deletedCoilInfo: null, restoredCoilInfo: null,
original: originalStep
}
if (originalStep.action === '新增') {
standardStep.action = '创建'
} else if (originalStep.action === '更新') {
if (originalStep.operation === '信息更新') standardStep.action = '更新'
else if (originalStep.operation === '分卷') standardStep.action = '分卷'
else if (originalStep.operation === '合卷') standardStep.action = '合卷'
else standardStep.action = '更新'
} else if (originalStep.action === '回滚') {
standardStep.action = '回滚'
} else {
standardStep.action = originalStep.action || '-'
}
if (originalStep.update_time || originalStep.create_time) {
standardStep.time = this.formatTime(originalStep.update_time || originalStep.create_time)
} else if (originalStep.rollback_time) {
standardStep.time = this.formatTime(originalStep.rollback_time)
}
standardStep.operation = originalStep.operator_nickname || originalStep.operator || originalStep.create_by || '-'
switch (standardStep.action) {
case '创建': standardStep.oldCoilIds = []; break
case '更新': standardStep.oldCoilIds = originalStep.old_coil_id ? [originalStep.old_coil_id.trim()] : []; break
case '退火': standardStep.oldCoilIds = originalStep.old_coil_id ? [originalStep.old_coil_id.trim()] : []; break
case '分卷': standardStep.oldCoilIds = originalStep.old_coil_id ? [originalStep.old_coil_id.trim()] : []; break
case '合卷':
standardStep.oldCoilIds = originalStep.parent_coil_ids
? originalStep.parent_coil_ids.split(',').map(id => id.trim()).filter(Boolean) : []
break
case '回滚':
standardStep.oldCoilIds = originalStep.deleted_coil_id ? [originalStep.deleted_coil_id.trim()] : []; break
default: standardStep.oldCoilIds = []
}
switch (standardStep.action) {
case '创建': standardStep.newCoilIds = originalStep.current_coil_id ? [originalStep.current_coil_id.trim()] : []; break
case '更新': standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : []; break
case '退火': standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : []; break
case '分卷':
standardStep.newCoilIds = originalStep.child_coil_ids
? originalStep.child_coil_ids.split(',').map(id => id.trim()).filter(Boolean) : []
break
case '合卷': standardStep.newCoilIds = originalStep.new_coil_id ? [originalStep.new_coil_id.trim()] : []; break
case '回滚':
standardStep.newCoilIds = originalStep.restored_coil_id ? [originalStep.restored_coil_id.trim()] : []; break
default: standardStep.newCoilIds = []
}
if (standardStep.action === '更新') {
standardStep.changedFields = originalStep.changed_fields || ''
}
if (standardStep.action === '创建') {
listMaterialCoil({ currentCoilNo: originalStep.current_coil_no, enterCoilNo: originalStep.current_coil_no }).then(res => {
standardStep.newCoilInfoList = res.rows || []
})
}
standardStep.newCoilInfoList = this.mapCoilInfoList(standardStep.newCoilIds)
if (standardStep.action === '回滚') {
standardStep.deletedCoilInfo = standardStep.oldCoilInfoList[0] || null
standardStep.restoredCoilInfo = standardStep.newCoilInfoList[0] || null
}
return standardStep
},
mapCoilInfoList(coilIds) {
if (!coilIds || !coilIds.length) return []
return coilIds.map(coilId => {
const coil = this.coilDetails[coilId] || {}
const safeCoil = coil || {}
return {
...safeCoil,
enterCoilNo: safeCoil.enterCoilNo || '-',
currentCoilNo: safeCoil.currentCoilNo || '-',
materialType: safeCoil.materialType || '-',
itemName: safeCoil.itemName || '-',
specification: safeCoil.specification || '-',
material: safeCoil.material || '-',
netWeight: safeCoil.netWeight || 0,
warehouseName: safeCoil.warehouseName || '-',
actualWarehouseName: safeCoil.actualWarehouseName || '-',
manufacturer: safeCoil.manufacturer || '-',
zincLayer: safeCoil.zincLayer || '-',
qualityStatus: safeCoil.qualityStatus || '-',
createTime: safeCoil.createTime || '-',
status: safeCoil.status || 0,
exportByName: safeCoil.exportByName || '-',
exportTime: safeCoil.exportTime || '-',
}
}).filter(coil => coil)
},
collectCoilIds() {
if (!this.standardSteps.length) return []
const coilIds = new Set()
this.standardSteps.forEach(step => {
step.oldCoilIds.forEach(id => id && coilIds.add(id))
step.newCoilIds.forEach(id => id && coilIds.add(id))
})
return [...coilIds]
},
async fetchCoilDetails() {
const coilIds = this.collectCoilIds().filter(id => id)
if (coilIds.length === 0) { this.coilDetails = {}; return }
if (this.loadingCoilDetails) return
this.loadingCoilDetails = true
try {
const res = await listMaterialCoil({ coilIds: coilIds.join(',') })
if (res && res.code === 200 && res.rows) {
const coilIdMap = {}
res.rows.forEach(coil => { if (coil.coilId) coilIdMap[coil.coilId] = coil })
this.coilDetails = coilIdMap
this.standardSteps.forEach(step => {
step.oldCoilInfoList = this.mapCoilInfoList(step.oldCoilIds)
step.newCoilInfoList = this.mapCoilInfoList(step.newCoilIds)
if (step.action === '回滚') {
step.deletedCoilInfo = step.oldCoilInfoList[0] || null
step.restoredCoilInfo = step.newCoilInfoList[0] || null
}
})
} else {
this.$message.warning('获取钢卷详情失败')
}
} catch (error) {
console.error('获取钢卷详情接口异常:', error)
this.$message.error('获取钢卷详情异常,请刷新重试')
} finally {
this.loadingCoilDetails = false
}
},
async loadProductionData() {
const hotCoilId = this.coilInfo.enterCoilNo
if (!hotCoilId) return
this.segLoading = true
this.realtimeLoading = true
try {
const detail = await getTimingPlanDetailByHotcoilId(hotCoilId)
const encoilId = detail?.data?.firstRow?.coilid || ''
const [segRes, realtimeRes] = await Promise.all([
encoilId ? getTimingSegByEncoilId(encoilId) : Promise.resolve({ data: null }),
encoilId ? getTimingRealtimeData(encoilId) : Promise.resolve({ data: null })
])
const series = segRes?.data?.series || null
const rows = segRes?.data?.rows || []
this.perfSegCount = rows.length
this.perfSeries = series
this.segData = series
const g = realtimeRes?.data?.gauge?.result
const s = realtimeRes?.data?.shape?.result
this.gaugeRows = Array.isArray(g) ? g : null
this.shapeRows = Array.isArray(s) ? s : null
} catch (error) {
console.error('获取生产数据异常:', error)
this.perfSeries = null
this.perfSegCount = 0
this.segData = null
this.gaugeRows = null
this.shapeRows = null
} finally {
this.segLoading = false
this.realtimeLoading = false
}
},
},
}
</script>
<style lang="scss">
@import './coil-info.scss';
</style>
<style scoped>
.coil-info-page {
background: linear-gradient(180deg, #f0f4f8 0%, #e2e8f0 50%, #f7fafc 100%);
min-height: 100vh;
padding: 8px 12px;
}
.content-container {
display: flex;
flex-direction: column;
gap: 12px;
}
</style>

View File

@@ -0,0 +1,20 @@
export function getFuturisticStatusClass(status) {
if (!status) return ''
const statusLower = status.toLowerCase()
if (statusLower.includes('合格')) return 'futuristic-status-success'
if (statusLower.includes('不合格')) return 'futuristic-status-danger'
if (statusLower.includes('待检')) return 'futuristic-status-warning'
return 'futuristic-status-default'
}
export function formatTime(timeStamp) {
if (!timeStamp) return '-'
const date = new Date(timeStamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}