Files
klp-oa/klp-ui/src/views/wms/coil/info.vue
砂糖 b44d9d9daf refactor(wms/coil): 重构生产工艺数据展示模块
1.  将接口从get请求改为post请求,适配后端参数变更
2.  重新设计生产工艺数据页面,拆分为趋势参数、厚度曲线、带钢板形、板形曲线四个标签页
3.  新增趋势参数树形侧边栏,支持分类展开选择查看不同参数曲线
4.  新增厚度、板形3D热力图、板形曲线图表展示
5.  优化图表渲染逻辑和样式,统一数据处理与加载状态
2026-05-12 14:18:01 +08:00

2659 lines
91 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="coil-info-page" v-loading="loading">
<div class="content-container">
<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">
<CoilInfoRender :coilInfo="coilInfo" :column="5" />
</div>
</div>
<div class="section trace-section">
<div class="section-header">
<span class="section-icon">🔄</span>
<span class="section-title">生命周期追踪</span>
</div>
<div class="section-body">
<div v-if="standardSteps && standardSteps.length > 0" class="trace-content">
<div v-for="(step, index) in standardSteps" :key="index" class="step-item">
<!-- 第一个步骤是入库创建 -->
<div v-if="index === 0 && step.action === '创建'" 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">📦 入库</span>
</div>
<div v-for="(coil, idx) in getInboundCoil(step, index)" :key="idx" class="coil-card-futuristic"
v-if="coil && coil.currentCoilNo !== '-'">
<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="coilGradInbound1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.5" />
<stop offset="50%" style="stop-color:#2563eb;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#3b82f6;stop-opacity:0.5" />
</linearGradient>
<linearGradient id="coilGradInbound2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#93c5fd;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#bfdbfe;stop-opacity:0.3" />
</linearGradient>
</defs>
<!-- 外圈 -->
<circle cx="100" cy="100" r="70" fill="none" stroke="url(#coilGradInbound1)"
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(#coilGradInbound2)"
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(#coilGradInbound1)" 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(#coilGradInbound2)" opacity="0.2" />
<circle cx="100" cy="100" r="25" fill="url(#coilGradInbound1)" 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(#coilGradInbound1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="170" y2="100" stroke="url(#coilGradInbound1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="170" stroke="url(#coilGradInbound1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="30" y2="100" stroke="url(#coilGradInbound1)" stroke-width="1"
opacity="0.4" />
<!-- 中心点 -->
<circle cx="100" cy="100" r="4" fill="#3b82f6">
<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-line attr-line-top"></div>
<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-line attr-line-right"></div>
<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-line attr-line-left"></div>
<div class="attr-content">
<span class="attr-label">质量状态</span>
<span class="attr-value" :class="getFuturisticStatusClass(coil.qualityStatus)">{{
coil.qualityStatus }}</span>
</div>
</div>
<!-- 左上角 - 入场卷号 -->
<div class="attr-tag attr-top-left" v-if="coil.enterCoilNo && coil.enterCoilNo !== '-'">
<div class="attr-content attr-content-small">
<span class="attr-label">入场卷号</span>
<span class="attr-value">{{ coil.enterCoilNo }}</span>
</div>
</div>
<!-- 右上角 - 厂家原料号 -->
<div class="attr-tag attr-top-right" v-if="coil.supplierCoilNo && coil.supplierCoilNo !== '-'">
<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>
<div
v-if="getInboundCoil(step, index).length === 0 || getInboundCoil(step, index).every(c => !c || c.currentCoilNo === '-')"
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>
<!-- 其他步骤是变更 -->
<div v-if="index !== 0 && step.action !== '创建'" class="step-content">
<!-- 左侧旧钢卷卡片 -->
<div class="coil-card-wrapper old-coil-wrapper">
<div class="coil-card-header">
<span class="card-label">变更前</span>
</div>
<div v-for="(coil, idx) in step.oldCoilInfoList" :key="idx" class="coil-card-futuristic"
v-if="coil && coil.currentCoilNo !== '-'">
<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="coilGrad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:0.5" />
<stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#6366f1;stop-opacity:0.5" />
</linearGradient>
<linearGradient id="coilGrad2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#a78bfa;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#c084fc;stop-opacity:0.3" />
</linearGradient>
</defs>
<!-- 外圈 -->
<circle cx="100" cy="100" r="70" fill="none" stroke="url(#coilGrad1)" 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(#coilGrad2)" 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(#coilGrad1)" 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(#coilGrad2)" opacity="0.2" />
<circle cx="100" cy="100" r="25" fill="url(#coilGrad1)" 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(#coilGrad1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="170" y2="100" stroke="url(#coilGrad1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="170" stroke="url(#coilGrad1)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="30" y2="100" stroke="url(#coilGrad1)" stroke-width="1"
opacity="0.4" />
<!-- 中心点 -->
<circle cx="100" cy="100" r="4" fill="#6366f1">
<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-line attr-line-top"></div>
<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-line attr-line-right"></div>
<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-line attr-line-left"></div>
<div class="attr-content">
<span class="attr-label">状态</span>
<span class="attr-value" :class="getFuturisticStatusClass(coil.qualityStatus)">{{
coil.qualityStatus || '-' }}</span>
</div>
</div>
<!-- 左上角 - 入场卷号 -->
<div 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 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>
<div
v-if="step.oldCoilInfoList.length === 0 || step.oldCoilInfoList.every(c => !c || c.currentCoilNo === '-')"
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>
<div v-for="(coil, idx) in step.newCoilInfoList" :key="idx" class="coil-card-futuristic"
v-if="coil && coil.currentCoilNo !== '-'">
<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="coilGrad3" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#10b981;stop-opacity:0.5" />
<stop offset="50%" style="stop-color:#34d399;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#10b981;stop-opacity:0.5" />
</linearGradient>
<linearGradient id="coilGrad4" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#a7f3d0;stop-opacity:0.4" />
<stop offset="100%" style="stop-color:#fbcfe8;stop-opacity:0.3" />
</linearGradient>
</defs>
<!-- 外圈 -->
<circle cx="100" cy="100" r="70" fill="none" stroke="url(#coilGrad3)" 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(#coilGrad4)" stroke-width="1.8"
opacity="0.6" stroke-dasharray="50 50">
<animate attributeName="stroke-dashoffset" values="0;100" dur="3s"
repeatCount="indefinite" />
</circle>
<!-- 内层钢卷 -->
<circle cx="100" cy="100" r="45" fill="url(#coilGrad3)" 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(#coilGrad4)" opacity="0.2" />
<circle cx="100" cy="100" r="25" fill="url(#coilGrad3)" 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(#coilGrad3)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="170" y2="100" stroke="url(#coilGrad3)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="100" y2="170" stroke="url(#coilGrad3)" stroke-width="1"
opacity="0.4" />
<line x1="100" y1="100" x2="30" y2="100" stroke="url(#coilGrad3)" stroke-width="1"
opacity="0.4" />
<!-- 中心点 -->
<circle cx="100" cy="100" r="4" fill="#10b981">
<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-line attr-line-top"></div>
<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-line attr-line-right"></div>
<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-line attr-line-left"></div>
<div class="attr-content">
<span class="attr-label">状态</span>
<span class="attr-value" :class="getFuturisticStatusClass(coil.qualityStatus || '-')">{{
coil.qualityStatus || '-' }}</span>
</div>
</div>
<!-- 左上角 - 入场卷号 -->
<div 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 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>
<div
v-if="step.newCoilInfoList.length === 0 || step.newCoilInfoList.every(c => !c || c.currentCoilNo === '-')"
class="empty-coil">
<span>无钢卷信息</span>
</div>
</div>
</div>
</div>
<!-- 发货记录 -->
<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">🚚 发货</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-line attr-line-top"></div>
<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>
</div>
<div v-else class="empty-state">
<i class="el-icon-document"></i>
<span>未找到相关钢卷记录</span>
</div>
</div>
</div>
<div class="section sales-section">
<div class="section-header">
<span class="section-icon">💰</span>
<span class="section-title">销售信息</span>
</div>
<div class="section-body" v-if="salesInfo.orderId">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="合同号">{{ salesInfo.contractCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ salesInfo.customer || '-' }}</el-descriptions-item>
<el-descriptions-item label="销售人员">{{ salesInfo.salesman || '-' }}</el-descriptions-item>
<el-descriptions-item label="签订时间">{{ salesInfo.signTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="交货时间">{{ salesInfo.deliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单总额">{{ salesInfo.orderAmount || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 技术附件 -->
<h4>技术附件</h4>
<FileList :oss-ids="salesInfo.techAnnex" />
<!-- 排产函附件 -->
<h4>排产函</h4>
<FileList :oss-ids="salesInfo.productionSchedule" />
</div>
<div v-else class="empty-state">
<i class="el-icon-document"></i>
<span>未找到相关销售记录</span>
</div>
</div>
<div class="section combined-section">
<div class="section-header">
<span class="section-icon">📋</span>
<span class="section-title">移库记录 & 调拨记录</span>
</div>
<div class="section-body tables-row">
<!-- 移库记录 - 占3份 -->
<div class="table-wrapper warehouse-table">
<div class="table-title">移库记录</div>
<div class="table-container">
<el-table :data="warehouseTranferList" 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>
<!-- 调拨记录 - 占2份 -->
<div class="table-wrapper transfer-table">
<div class="table-title">调拨记录</div>
<div class="table-container">
<el-table :data="tranferList" 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>
<div class="section abnormal-section">
<div class="section-header">
<span class="section-icon"></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 v-if="isColdHardCoil" class="section production-section">
<div class="section-header">
<span class="section-icon">📈</span>
<span class="section-title">生产工艺数据</span>
<el-tag v-if="perfLoading || 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="perfActiveTab" size="small" class="perf-tabs" @tab-click="handlePerfTabSwitch">
<!-- 趋势参数左树形目录 + 右单图 -->
<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="toggleTrendGroup(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: selectedTrendParam && selectedTrendParam.col === item.col }"
@click="selectTrendParam(item)"
>{{ item.label }}</div>
</div>
</div>
</div>
<div class="trend-chart-area">
<div v-if="!selectedTrendParam" class="no-data-hint"> 点击左侧参数查看曲线</div>
<div ref="trendSingleChart" :style="{ display: selectedTrendParam ? '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>
<!-- 带钢板形 3D 热力图 -->
<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>
</div>
</div>
</template>
<script>
import { getMaterialCoil, listMaterialCoil, getMaterialCoilTrace } from '@/api/wms/coil';
import { getCoilWarehouseOperationLogByCoilId } from '@/api/wms/coilWarehouseOperationLog';
import { listCoilAbnormal } from '@/api/wms/coilAbnormal'
// 查询批量调拨的记录
import { listTransferOrderItem } from '@/api/wms/transferOrderItem'
// 查询技术部改判调拨的记录
import { listCoilQualityRejudge } from '@/api/wms/coilQualityRejudge'
// 引入 ECharts 和 L2 时序数据 API
import * as echarts from 'echarts'
import 'echarts-gl'
import { getTimingSegByEncoilId, getTimingPlanDetailByHotcoilId, getTimingRealtimeData } from '@/api/l2/timing'
import AbnormalTable from '@/views/wms/coil/components/AbnormalTable.vue';
import FileList from "@/components/FileList";
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: 'CoilInfo',
components: {
AbnormalTable,
FileList
},
data() {
return {
loading: false,
coilInfoLoading: false,
traceLoading: false,
traceResult: null,
coilDetails: {},
loadingCoilDetails: false,
coilInfo: {},
coilList: [],
wipList: [],
annealList: [],
standardSteps: [],
coilId: '',
warehouseTranferList: [],
abmornalList: [],
transferOrderItemList: [], // 批量调拨记录
coilQualityRejudgeList: [], // 技术部改判记录
tranferList: [], // 合并后的调拨记录
// 生产数据相关
perfLoading: false,
perfSeries: null,
perfSegCount: 0,
// 新增图表数据状态
segLoading: false,
realtimeLoading: false,
segData: null,
gaugeRows: null,
shapeRows: null,
perfActiveTab: 'trend',
// 趋势参数树状态
trendGroups: TREND_GROUPS,
expandedGroups: { '张力': true, '速度': true, '拉矫机': true, '酸洗段': true },
selectedTrendParam: null,
trendChartInst: null,
_trendResizeFn: null,
chartInstances: [],
resizeHandler: null
}
},
computed: {
salesInfo() {
return this.coilInfo.orderList?.[0] || {};
},
// 判断是否为冷硬卷
isColdHardCoil() {
return this.coilInfo.itemName && this.coilInfo.itemName.includes('冷硬卷');
},
hasPerfData() {
return this.perfSeries && this.perfSegCount > 0;
}
},
async created() {
this.chartInstances = [];
this.resizeHandler = null;
this.coilId = this.$route.params.coilId;
this.loading = true;
await this.getCoilInfo();
await this.getTraceInfo();
await this.getAbnormalList();
await this.getWarehouseTransferList();
await this.getTransferOrderItemList();
await this.getCoilQualityRejudgeList();
this.mergeTransferList();
// 如果是冷硬卷,加载生产数据
if (this.isColdHardCoil) {
await this.loadProductionData();
}
this.loading = false;
},
beforeDestroy() {
this.disposeCharts();
},
methods: {
async getCoilInfo() {
const res = await getMaterialCoil(this.coilId);
this.coilInfo = res.data;
},
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 getCoilList() {
const res = await listMaterialCoil({ enterCoilNo: this.coilInfo.enterCoilNo, pageNum: 1, pageSize: 100 });
this.coilList = res.rows;
},
async getAbnormalList() {
const res = await listCoilAbnormal({
coilId: this.coilId
});
this.abmornalList = 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;
},
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}`;
},
parseChangedFields(changedFieldsStr) {
if (!changedFieldsStr) return [];
const items = changedFieldsStr.split(';').filter(item => item.trim());
return items.map(item => {
const [fieldPart, valuePart] = item.split(':').map(part => part.trim());
if (!valuePart) return { field: fieldPart, oldVal: '-', newVal: '-' };
const [oldVal, newVal] = valuePart.split('→').map(val => {
const trimmedVal = val.trim();
return trimmedVal === 'null' ? '-' : trimmedVal;
});
return {
field: fieldPart,
oldVal: oldVal || '-',
newVal: newVal || '-'
};
});
},
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] || {};
// 确保 coil 不为 null 或 undefined
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); // 过滤掉 null/undefined 的项
},
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 coilList = res.rows;
const coilIdMap = {};
coilList.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;
}
},
getMiniStatusClass(status) {
if (!status) return '';
const statusLower = status.toLowerCase();
if (statusLower.includes('合格')) return 'mini-status-success';
if (statusLower.includes('不合格')) return 'mini-status-danger';
if (statusLower.includes('待检')) return 'mini-status-warning';
return 'mini-status-default';
},
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';
},
getInboundCoil(step, index) {
// 如果只有一个步骤用当前步骤的newCoilInfoList
if (this.standardSteps.length === 1) {
return step.newCoilInfoList;
}
// 如果有多个步骤用第二个步骤的oldCoilInfoList
if (index === 0 && this.standardSteps[1]) {
return this.standardSteps[1].oldCoilInfoList;
}
return step.newCoilInfoList;
},
// 加载生产数据
async loadProductionData() {
const hotCoilId = this.coilInfo.enterCoilNo;
if (!hotCoilId) return;
this.perfLoading = true;
try {
const detail = await getTimingPlanDetailByHotcoilId(hotCoilId);
const encoilId = detail?.data?.firstRow?.coilid || '';
// V_VBDA_GAUGE / V_VBDA_SHAPE 使用 EXCOILID 作为 MATID对应入场卷号
const excoilId = this.coilInfo.enterCoilNo;
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;
if (series && rows.length) {
await this.$nextTick();
this.selectTrendParam(TREND_GROUPS[0].children[0]);
// 数据加载完成后,根据当前选中的标签页渲染对应图表
this.renderCurrentTab();
} else if (this.gaugeRows?.length || this.shapeRows?.length) {
await this.$nextTick();
this.renderCurrentTab();
}
} catch (error) {
console.error('获取生产数据异常:', error);
this.perfSeries = null;
this.perfSegCount = 0;
this.segData = null;
this.gaugeRows = null;
this.shapeRows = null;
} finally {
this.perfLoading = false;
}
},
// 销毁图表
disposeCharts() {
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;
}
},
disposeSideCharts() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
this.resizeHandler = null;
}
this.chartInstances.forEach(c => { if (c && !c.isDisposed()) c.dispose(); });
this.chartInstances = [];
},
// 趋势参数树操作
toggleTrendGroup(label) {
this.$set(this.expandedGroups, label, !this.expandedGroups[label]);
},
selectTrendParam(item) {
this.selectedTrendParam = item;
this.$nextTick(() => this.renderTrendSingleChart());
},
renderTrendSingleChart() {
if (!this.selectedTrendParam || !this.segData) return;
const el = this.$refs.trendSingleChart;
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.selectedTrendParam.col);
this.trendChartInst.setOption(makeLine(this.selectedTrendParam.label, x, yData), true);
},
// 标签页切换
handlePerfTabSwitch() {
this.$nextTick(() => {
if (this.perfActiveTab === 'trend') {
if (this.selectedTrendParam && this.segData) {
this.renderTrendSingleChart();
}
} else {
this.renderCurrentTab();
}
});
},
renderCurrentTab() {
this.disposeSideCharts();
if (this.perfActiveTab === 'thickness' && this.gaugeRows?.length) this.renderGaugeCharts();
if (this.perfActiveTab === 'flatness3d' && this.shapeRows?.length) this.renderFlatness3d();
if (this.perfActiveTab === 'flatness' && this.shapeRows?.length) this.renderFlatnessCharts();
},
// SEG 数据辅助
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);
},
// 厚度曲线
renderGaugeCharts() {
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();
},
// 带钢板形 3D 热力图
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();
},
// 板形曲线
renderFlatnessCharts() {
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();
},
},
}
</script>
<style scoped>
.coil-info-page {
background: linear-gradient(180deg, #f0f4f8 0%, #e2e8f0 50%, #f7fafc 100%);
min-height: 100vh;
padding: 8px 12px;
}
.page-header {
margin-bottom: 16px;
}
.header-title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
text-shadow: 0 0 10px rgba(59, 130, 246, 0.1);
}
.header-divider {
height: 2px;
width: 60px;
background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 50%, #3b82f6 100%);
margin-top: 8px;
box-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
}
.content-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.section {
border: 1px solid rgba(59, 130, 246, 0.2);
background: rgba(255, 255, 255, 0.9);
overflow: hidden;
position: relative;
box-shadow:
0 0 0 1px rgba(59, 130, 246, 0.1) inset,
0 4px 16px rgba(59, 130, 246, 0.08);
}
.section-header {
display: flex;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid rgba(59, 130, 246, 0.15);
background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, rgba(59, 130, 246, 0.02) 100%);
}
.section-icon {
font-size: 16px;
margin-right: 8px;
filter: drop-shadow(0 0 6px rgba(59, 130, 246, 0.3));
}
.section-title {
font-size: 15px;
font-weight: 700;
color: #1e293b;
letter-spacing: 1px;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-body {
padding: 12px 16px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8) 0%, rgba(248, 250, 252, 0.6) 100%);
}
.trace-content {
display: flex;
flex-direction: column;
gap: 10px;
}
.step-item {
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
padding: 12px 14px;
border: 1px solid rgba(59, 130, 246, 0.2);
box-shadow:
0 2px 8px rgba(59, 130, 246, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
position: relative;
overflow: hidden;
}
.step-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.4), transparent);
}
.step-content {
display: flex;
align-items: stretch;
gap: 10px;
}
.coil-card-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.coil-card-header {
padding: 6px 12px;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-bottom: none;
}
.card-label {
font-size: 13px;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.5px;
}
.coil-card {
padding: 10px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
border: 1px solid rgba(59, 130, 246, 0.15);
box-shadow:
0 2px 6px rgba(59, 130, 246, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.old-coil-wrapper .coil-card-header {
background: linear-gradient(145deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.04) 100%);
border-color: rgba(245, 158, 11, 0.25);
}
.new-coil-wrapper .coil-card-header {
background: linear-gradient(145deg, rgba(16, 185, 129, 0.08) 0%, rgba(16, 185, 129, 0.04) 100%);
border-color: rgba(16, 185, 129, 0.25);
}
.inbound-header {
background: linear-gradient(145deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%) !important;
border-color: rgba(59, 130, 246, 0.3) !important;
}
.step-content-inbound {
display: flex;
align-items: stretch;
gap: 10px;
}
.inbound-coil-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.action-arrow-inbound {
color: #3b82f6 !important;
font-size: 28px;
margin-bottom: 10px;
text-shadow: 0 0 12px rgba(59, 130, 246, 0.6);
animation: bounce-in 2s ease-in-out infinite !important;
}
@keyframes bounce-in {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-4px);
}
}
.empty-coil {
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
border: 1px solid rgba(59, 130, 246, 0.15);
color: #64748b;
font-size: 13px;
}
.action-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 100px;
padding: 12px 8px;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.06) 0%, rgba(59, 130, 246, 0.03) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
box-shadow:
0 2px 6px rgba(59, 130, 246, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.action-arrow {
color: #3b82f6;
font-size: 28px;
margin-bottom: 10px;
text-shadow: 0 0 12px rgba(59, 130, 246, 0.6);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.action-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.action-tag {
font-size: 12px;
font-weight: 700;
}
.action-operator {
font-size: 12px;
font-weight: 600;
color: #1e293b;
}
.action-time {
font-size: 11px;
color: #64748b;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px 0;
color: #64748b;
font-size: 14px;
}
.empty-state i {
font-size: 48px;
margin-bottom: 12px;
color: #94a3b8;
text-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
}
::v-deep .el-descriptions--small .el-descriptions__cell {
padding: 8px 12px;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.7) 0%, rgba(248, 250, 252, 0.85) 100%);
border-color: rgba(59, 130, 246, 0.2) !important;
}
::v-deep .el-descriptions__label {
background: linear-gradient(145deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%) !important;
color: #1e293b !important;
font-weight: 700;
letter-spacing: 0.5px;
}
::v-deep .el-descriptions__content {
color: #1e293b !important;
}
::v-deep .el-table {
background: transparent !important;
}
::v-deep .el-table th,
::v-deep .el-table td {
padding: 8px 0;
background: transparent !important;
border-color: rgba(59, 130, 246, 0.2) !important;
color: #1e293b !important;
}
::v-deep .el-table th {
background: linear-gradient(180deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%) !important;
color: #1e293b !important;
font-weight: 700;
letter-spacing: 0.5px;
}
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
background: rgba(59, 130, 246, 0.03) !important;
}
::v-deep .el-table--border,
::v-deep .el-table--group {
border-color: rgba(59, 130, 246, 0.25) !important;
}
::v-deep .el-tag {
background: linear-gradient(145deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%) !important;
border: 1px solid rgba(59, 130, 246, 0.3) !important;
color: #1e293b !important;
box-shadow: 0 0 8px rgba(59, 130, 246, 0.2);
font-weight: 600;
}
/* CoilInfo组件样式优化 */
.coil-card ::v-deep .el-descriptions--small .el-descriptions__cell {
padding: 8px 10px;
font-size: 12px;
}
.coil-card ::v-deep .el-descriptions__label {
background: linear-gradient(145deg, rgba(241, 245, 249, 0.9) 0%, rgba(226, 232, 240, 1) 100%) !important;
color: #475569 !important;
font-weight: 600;
font-size: 12px;
}
.coil-card ::v-deep .el-descriptions__content {
color: #1e293b !important;
font-size: 12px;
}
/* 科技感钢卷卡片样式 */
.coil-card-futuristic {
padding: 8px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f0f4ff 100%);
border: 1px solid rgba(59, 130, 246, 0.25);
box-shadow:
0 2px 12px rgba(59, 130, 246, 0.1),
0 0 20px rgba(59, 130, 246, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
position: relative;
overflow: hidden;
}
/* 发光边框效果 */
.coil-card-futuristic::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%);
animation: rotateGlow 10s linear infinite;
pointer-events: none;
}
@keyframes rotateGlow {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.coil-futuristic-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
position: relative;
z-index: 1;
}
.coil-futuristic-no {
font-size: 16px;
font-weight: 700;
color: #1e293b;
text-shadow: 0 0 8px rgba(59, 130, 246, 0.3);
letter-spacing: 1px;
font-family: 'Courier New', monospace;
}
.coil-futuristic-weight {
font-size: 14px;
font-weight: 700;
color: #d97706;
padding: 4px 10px;
background: linear-gradient(145deg, rgba(245, 158, 11, 0.12) 0%, rgba(245, 158, 11, 0.06) 100%);
border: 1px solid rgba(245, 158, 11, 0.3);
text-shadow: 0 0 4px rgba(245, 158, 11, 0.3);
box-shadow: 0 0 8px rgba(245, 158, 11, 0.15);
}
.coil-futuristic-body {
position: relative;
height: 180px;
margin-bottom: 8px;
z-index: 1;
}
/* SVG钢卷图标容器 */
.coil-icon-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
}
.coil-svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.3));
}
/* 属性标签容器 */
.coil-attr-tags {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* 通用属性标签样式 */
.attr-tag {
position: absolute;
display: flex;
align-items: center;
pointer-events: auto;
}
.attr-content {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 249, 255, 0.95) 100%);
border: 1px solid rgba(59, 130, 246, 0.3);
padding: 5px 8px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.12);
min-width: 70px;
}
.attr-label {
display: block;
font-size: 10px;
color: #3b82f6;
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 2px;
font-weight: 700;
}
.attr-value {
display: block;
font-size: 12px;
color: #1e293b;
font-weight: 600;
word-break: break-all;
}
/* 属性连接线 */
.attr-line {
position: absolute;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.9));
animation: linePulse 2s ease-in-out infinite;
}
@keyframes linePulse {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}
/* 顶部属性标签 */
.attr-top {
top: 0;
left: 50%;
transform: translateX(-50%);
flex-direction: column;
}
.attr-top .attr-line-top {
width: 2px;
height: 16px;
background: linear-gradient(180deg, transparent, rgba(59, 130, 246, 0.8));
margin-bottom: 5px;
}
.attr-top .attr-content {
text-align: center;
}
/* 右侧属性标签 */
.attr-right {
right: 0;
top: 50%;
transform: translateY(-50%);
flex-direction: row-reverse;
}
.attr-right .attr-line-right {
width: 16px;
height: 2px;
background: linear-gradient(90deg, rgba(59, 130, 246, 0.8), transparent);
margin-left: 5px;
}
/* 底部属性标签 */
.attr-bottom {
bottom: 0;
left: 50%;
transform: translateX(-50%);
flex-direction: column-reverse;
}
.attr-bottom .attr-line-bottom {
width: 2px;
height: 16px;
background: linear-gradient(0deg, transparent, rgba(59, 130, 246, 0.8));
margin-top: 5px;
}
.attr-bottom .attr-content {
text-align: center;
}
/* 左侧属性标签 */
.attr-left {
left: 0;
top: 50%;
transform: translateY(-50%);
flex-direction: row;
}
.attr-left .attr-line-left {
width: 16px;
height: 2px;
background: linear-gradient(270deg, rgba(59, 130, 246, 0.8), transparent);
margin-right: 5px;
}
/* 左上角标签 */
.attr-top-left {
position: absolute;
top: 6px;
left: 6px;
}
/* 右上角标签 */
.attr-top-right {
position: absolute;
top: 6px;
right: 6px;
}
/* 左下角标签 */
.attr-bottom-left {
position: absolute;
bottom: 6px;
left: 6px;
}
/* 右下角标签 */
.attr-bottom-right {
position: absolute;
bottom: 6px;
right: 6px;
}
/* 小尺寸标签样式 */
.attr-content-small {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 249, 255, 0.95) 100%);
border: 1px solid rgba(59, 130, 246, 0.3);
padding: 4px 6px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 6px rgba(59, 130, 246, 0.1);
min-width: 60px;
}
.attr-content-small .attr-label {
font-size: 9px;
letter-spacing: 0.6px;
}
.attr-content-small .attr-value {
font-size: 11px;
}
/* 备注标签 */
.attr-remark {
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
}
.attr-content-remark {
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 249, 255, 1) 100%);
border: 1px solid rgba(59, 130, 246, 0.35);
padding: 5px 8px;
backdrop-filter: blur(4px);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.12);
max-width: 160px;
text-align: center;
}
.attr-content-remark .attr-value {
word-break: break-word;
font-size: 11px;
line-height: 1.3;
}
/* 状态标签颜色 */
.futuristic-status-success {
color: #22c55e !important;
text-shadow: 0 0 6px rgba(34, 197, 94, 0.5) !important;
}
.futuristic-status-danger {
color: #ef4444 !important;
text-shadow: 0 0 6px rgba(239, 68, 68, 0.5) !important;
}
.futuristic-status-warning {
color: #f59e0b !important;
text-shadow: 0 0 6px rgba(245, 158, 11, 0.5) !important;
}
.futuristic-status-default {
color: #64748b !important;
text-shadow: 0 0 6px rgba(100, 116, 139, 0.4) !important;
}
/* 底部信息 */
.coil-futuristic-footer {
display: flex;
justify-content: space-around;
padding-top: 8px;
border-top: 1px solid rgba(59, 130, 246, 0.2);
position: relative;
z-index: 1;
}
.footer-item {
text-align: center;
}
.footer-label {
display: block;
font-size: 10px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.6px;
margin-bottom: 2px;
font-weight: 700;
}
.footer-value {
display: block;
font-size: 12px;
color: #475569;
font-weight: 600;
}
/* 简化版钢卷卡片样式(保留用于其他地方使用)
.coil-card-mini {
padding: 16px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
border: 1px solid rgba(180, 180, 180, 0.4);
border-radius: 0 0 8px 8px;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.05),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.coil-mini-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.coil-mini-no {
font-size: 18px;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.5px;
}
.coil-mini-weight {
font-size: 16px;
font-weight: 600;
color: #f59e0b;
padding: 4px 12px;
background: linear-gradient(145deg, #fef3c7 0%, #fde68a 100%);
border-radius: 6px;
}
.coil-mini-sub {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.coil-mini-item {
font-size: 13px;
color: #64748b;
line-height: 1.4;
}
.coil-mini-status {
padding-top: 12px;
border-top: 1px dashed rgba(180, 180, 180, 0.3);
}
.mini-status-success {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 600;
color: #22c55e;
background: linear-gradient(145deg, #dcfce7 0%, #bbf7d0 100%);
border-radius: 6px;
}
.mini-status-danger {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 600;
color: #ef4444;
background: linear-gradient(145deg, #fee2e2 0%, #fecaca 100%);
border-radius: 6px;
}
.mini-status-warning {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 600;
color: #f59e0b;
background: linear-gradient(145deg, #fef3c7 0%, #fde68a 100%);
border-radius: 6px;
}
.mini-status-default {
display: inline-block;
padding: 4px 12px;
font-size: 13px;
font-weight: 600;
color: #64748b;
background: linear-gradient(145deg, #f1f5f9 0%, #e2e8f0 100%);
border-radius: 6px;
}
/* 表格行布局 */
.tables-row {
display: flex;
gap: 12px;
min-height: 320px;
}
.table-wrapper {
display: flex;
flex-direction: column;
}
.table-wrapper.warehouse-table {
flex: 2;
}
.table-wrapper.transfer-table {
flex: 2;
}
.table-title {
padding: 6px 12px;
margin-bottom: 10px;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
font-size: 13px;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.6px;
}
.table-container {
flex: 1;
overflow: hidden;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
box-shadow:
0 2px 8px rgba(59, 130, 246, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
/* 确保表格高度一致 */
.table-container ::v-deep .el-table {
height: 100%;
}
.table-container ::v-deep .el-table__body-wrapper {
flex: 1;
overflow-y: auto;
}
/* 附件区域样式 */
.annex-section {
margin-top: 12px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 50%, #f1f5f9 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
box-shadow:
0 2px 8px rgba(59, 130, 246, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
}
.annex-title {
display: flex;
align-items: center;
padding: 8px 14px;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border-bottom: 1px solid rgba(59, 130, 246, 0.2);
}
.annex-icon {
font-size: 14px;
margin-right: 8px;
filter: drop-shadow(0 0 6px rgba(59, 130, 246, 0.4));
}
.annex-title span:last-child {
font-size: 13px;
font-weight: 700;
color: #1e293b;
letter-spacing: 0.6px;
}
.annex-content {
padding: 12px 14px;
}
/* 附件区域的h4标题样式 */
.sales-section h4 {
margin: 12px 0 8px 0;
padding: 6px 10px;
font-size: 13px;
font-weight: 700;
color: #1e293b;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
letter-spacing: 0.6px;
}
/* 生产数据样式 */
.perf-count {
font-weight: normal;
color: #909399;
font-size: 12px;
margin-left: 4px;
}
.production-section {
min-height: 600px;
flex: 1;
}
.production-section .section-body {
padding: 0;
height: calc(100% - 48px);
min-height: 550px;
}
.perf-tabs {
height: 100%;
min-height: 550px;
display: flex;
flex-direction: column;
::v-deep .el-tabs__header {
flex-shrink: 0;
}
::v-deep .el-tabs__content {
flex: 1;
overflow: hidden;
min-height: 0;
padding: 0;
}
::v-deep .el-tab-pane {
height: 100%;
min-height: 500px;
}
}
.trend-layout {
display: flex;
height: 100%;
gap: 0;
}
.trend-tree {
width: 140px;
flex-shrink: 0;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #ebeef5;
padding: 4px 0;
max-height: calc(100vh - 400px);
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 2px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.tree-group {
user-select: none;
}
.tree-group-label {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
font-size: 12px;
font-weight: 600;
color: #303133;
cursor: pointer;
&:hover { background: #f5f7fa; }
i { font-size: 10px; color: #909399; }
}
.tree-children { padding-left: 4px; }
.tree-item {
padding: 4px 8px 4px 18px;
font-size: 12px;
color: #606266;
cursor: pointer;
border-radius: 3px;
margin: 1px 4px;
&:hover { background: #ecf5ff; color: #409eff; }
&.active {
background: #ecf5ff;
color: #409eff;
font-weight: 500;
}
}
.trend-chart-area {
flex: 1;
min-width: 0;
height: 500px;
padding: 4px 4px 4px 8px;
display: flex;
align-items: stretch;
}
.charts-scroll {
height: 100%;
overflow-y: auto;
padding: 8px;
box-sizing: border-box;
&::-webkit-scrollbar { width: 4px; }
&::-webkit-scrollbar-thumb { background: #dcdfe6; border-radius: 2px; }
}
.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-content: start;
min-height: 100%;
padding: 4px;
.chart-box { margin-bottom: 0; }
}
.chart-box {
width: 100%;
height: 260px;
margin-bottom: 12px;
min-height: 200px;
}
.chart-box-tall {
height: 480px;
min-height: 400px;
}
.no-data-hint {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 120px;
color: #c0c4cc;
font-size: 13px;
}
</style>