Files
klp-oa/klp-ui/src/views/wms/coil/info.vue
砂糖 80fbe70ab7 fix(wms/coil/info): 调整入库天数计算的截止日期逻辑
根据卷料状态切换使用当前时间或出库时间作为计算截止日期
2026-05-22 13:07:53 +08:00

3400 lines
122 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 basic-info-section">
<div class="section-header">
<span class="section-icon">💰</span>
<span class="section-title">成本信息</span>
</div>
<div class="section-body">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="囤积成本">
<span class="cost-value">{{ hoardingCost }}</span>
<span class="cost-unit">t·</span>
</el-descriptions-item>
<el-descriptions-item label="囤积天数">
<span>{{ hoardingDays }} </span>
</el-descriptions-item>
<el-descriptions-item label="钢卷净重">
<span>{{ coilInfo.netWeight || 0 }} t</span>
</el-descriptions-item>
</el-descriptions>
</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">
<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">📦 入库</span>
</div>
<div v-for="(coil, idx) in step.newCoilInfoList" :key="idx" class="coil-card-hover"
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', getFuturisticStatusClass(coil.qualityStatus)]">{{ coil.qualityStatus || '-' }}</span>
</div>
<div class="coil-detail-popup">
<div class="popup-header">
<span class="popup-title">钢卷详情</span>
<span class="popup-close"></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>
<div v-if="step.newCoilInfoList.length === 0 || step.newCoilInfoList.every(c => !c || c.currentCoilNo === '-')" 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">📦 入库</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="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="step.newCoilInfoList.length === 0 || step.newCoilInfoList.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>
</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>
<div v-for="(coil, idx) in step.oldCoilInfoList" :key="idx" class="coil-card-hover old" 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', getFuturisticStatusClass(coil.qualityStatus)]">{{ coil.qualityStatus || '-' }}</span>
</div>
<div class="coil-detail-popup">
<div class="popup-header">
<span class="popup-title">钢卷详情</span>
<span class="popup-close"></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>
<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-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>
<div v-for="(coil, idx) in step.newCoilInfoList" :key="idx" class="coil-card-hover new" 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', getFuturisticStatusClass(coil.qualityStatus)]">{{ coil.qualityStatus || '-' }}</span>
</div>
<div class="coil-detail-popup">
<div class="popup-header">
<span class="popup-title">钢卷详情</span>
<span class="popup-close"></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>
<div v-if="step.newCoilInfoList.length === 0 || step.newCoilInfoList.every(c => !c || c.currentCoilNo === '-')" 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>
<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-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">状态</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-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">状态</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>
</template>
</div>
</template>
</coil-trace-result>
<!-- 发货记录 -->
<div class="step-item" v-if="coilInfo">
<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-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>
<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 sales-section">
<!-- 发货单信息 -->
<div class="section-header">
<span class="section-icon">💰</span>
<span class="section-title">发货合同信息</span>
</div>
<div class="section-body" v-if="deliveryOrderInfo.orderId">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="合同号">{{ deliveryOrderInfo.contractCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ deliveryOrderInfo.customer || '-' }}</el-descriptions-item>
<el-descriptions-item label="销售人员">{{ deliveryOrderInfo.salesman || '-' }}</el-descriptions-item>
<el-descriptions-item label="签订时间">{{ deliveryOrderInfo.signTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="交货时间">{{ deliveryOrderInfo.deliveryTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="订单总额">{{ deliveryOrderInfo.orderAmount || '-' }}</el-descriptions-item>
</el-descriptions>
<!-- 技术附件 -->
<h4>技术附件</h4>
<FileList :oss-ids="deliveryOrderInfo.techAnnex" />
<!-- 排产函附件 -->
<h4>排产函</h4>
<FileList :oss-ids="deliveryOrderInfo.productionSchedule" />
</div>
<div v-else class="empty-state">
<i class="el-icon-document"></i>
<span>未找到相关发货合同信息</span>
</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="salesObjectionInfo.length > 0">
<el-table :data="salesObjectionInfo" 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>
<el-table-column label="客户诉求" align="center" prop="customerDemand" show-overflow-tooltip></el-table-column>
<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>
<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 class="section inspection-section">
<div class="section-header">
<span class="section-icon">🔬</span>
<span class="section-title">检验信息</span>
</div>
<div class="section-body">
<el-table :data="inspectionTaskList" v-loading="inspectionLoading" size="small" border stripe style="width: 100%"
@expand-change="handleInspectionExpand" :row-key="row => row.taskId">
<el-table-column type="expand">
<template slot-scope="props">
<div v-loading="inspectionItemLoadingMap[props.row.taskId]" style="padding: 8px;">
<el-table :data="inspectionItemMap[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="!inspectionItemMap[props.row.taskId] || inspectionItemMap[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="inspectionTaskList.length === 0 && !inspectionLoading"
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>
<!-- 生产工艺数据图表 -->
<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, 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'
// 引入 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";
import CoilTraceResult from '@/views/wms/coil/panels/CoilTraceResult.vue';
import { listInspectionTask } from "@/api/mes/qc/inspectionTask";
import { listInspectionItem } from "@/api/mes/qc/inspectionItem";
import CurrentCoilNo from "@/components/KLPService/Renderer/CurrentCoilNo.vue";
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,
CoilTraceResult,
CurrentCoilNo
},
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,
// 发货单绑定的销售信息
deliveryOrderInfo: {},
// 销售异议信息
salesObjectionInfo: [],
// 检验信息
inspectionTaskList: [],
inspectionLoading: false,
inspectionItemMap: {},
inspectionItemLoadingMap: {},
}
},
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);
let endDate;
if (this.coilInfo.status == 1) {
endDate = new Date(this.coilInfo.exportTime);
} else {
endDate = new Date();
}
// const today = new Date();
const diffTime = endDate.getTime() - inboundDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return Math.max(0, diffDays);
},
hoardingCost() {
const netWeight = parseFloat(this.coilInfo.netWeight) || 0;
return (this.hoardingDays * netWeight).toFixed(2);
}
},
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();
await this.fetchDeliveryOrderInfo();
this.mergeTransferList();
await this.getSalesObjectionList();
await this.getInspectionTasks();
// 如果是冷硬卷,加载生产数据
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 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 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;
},
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;
}
},
async handleInspectionExpand(row, expandedRows) {
if (!expandedRows || !expandedRows.includes(row)) return;
const taskId = row.taskId;
if (this.inspectionItemMap[taskId]) return;
this.$set(this.inspectionItemLoadingMap, taskId, true);
try {
const res = await listInspectionItem({ taskId, pageNum: 1, pageSize: 100 });
this.$set(this.inspectionItemMap, taskId, res.rows || []);
} catch (e) {
console.error('获取检验明细异常:', e);
this.$set(this.inspectionItemMap, taskId, []);
} finally {
this.$set(this.inspectionItemLoadingMap, taskId, 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) {
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;
}
/* 紧凑模式下的悬停卡片样式 */
.coil-card-hover {
position: relative;
padding: 8px;
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.coil-card-hover:hover {
border-color: rgba(59, 130, 246, 0.5);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15);
transform: translateY(-2px);
}
.coil-card-hover.old {
border-color: rgba(245, 158, 11, 0.3);
background: linear-gradient(145deg, #fffbeb 0%, #fef3c7 100%);
}
.coil-card-hover.old:hover {
border-color: rgba(245, 158, 11, 0.5);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.15);
}
.coil-card-hover.new {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
}
.coil-card-hover.new:hover {
border-color: rgba(16, 185, 129, 0.5);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.15);
}
.coil-mini-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.coil-mini-no {
font-size: 13px;
font-weight: 700;
color: #1e293b;
font-family: 'Courier New', monospace;
}
.coil-mini-weight {
font-size: 12px;
font-weight: 600;
color: #d97706;
padding: 2px 6px;
background: rgba(245, 158, 11, 0.15);
border-radius: 4px;
}
.coil-mini-body {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.coil-mini-spec {
font-size: 11px;
color: #475569;
padding: 2px 5px;
background: rgba(255, 255, 255, 0.8);
border-radius: 3px;
}
.coil-mini-material {
font-size: 11px;
color: #475569;
padding: 2px 5px;
background: rgba(255, 255, 255, 0.8);
border-radius: 3px;
}
.coil-mini-status {
font-size: 11px;
font-weight: 600;
padding: 2px 5px;
border-radius: 3px;
}
.coil-mini-status.status-success {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.coil-mini-status.status-danger {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.coil-mini-status.status-warning {
background: rgba(245, 158, 11, 0.15);
color: #f59e0b;
}
.coil-mini-status.status-default {
background: rgba(100, 116, 139, 0.15);
color: #64748b;
}
/* 悬停弹窗样式 */
.coil-detail-popup {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 8px;
background: #ffffff;
border: 1px solid rgba(59, 130, 246, 0.3);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.2);
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.coil-card-hover:hover .coil-detail-popup {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
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);
border-radius: 8px 8px 0 0;
}
.popup-title {
font-size: 13px;
font-weight: 700;
color: #1e293b;
}
.popup-close {
font-size: 14px;
color: #94a3b8;
cursor: pointer;
padding: 2px 4px;
border-radius: 4px;
transition: all 0.2s;
}
.popup-close:hover {
background: rgba(59, 130, 246, 0.1);
color: #64748b;
}
.popup-content {
padding: 10px 12px;
max-height: 300px;
overflow-y: auto;
}
.popup-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px dashed rgba(59, 130, 246, 0.1);
}
.popup-row:last-child {
border-bottom: none;
}
.popup-label {
font-size: 11px;
color: #64748b;
min-width: 70px;
}
.popup-value {
font-size: 11px;
color: #1e293b;
font-weight: 500;
text-align: right;
flex: 1;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 紧凑模式变更结构(上下布局) */
.step-content-compact-change {
display: flex;
flex-direction: column;
gap: 10px;
}
.coil-change-wrapper {
background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 8px;
padding: 8px;
}
.coil-change-wrapper.new {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 50%, #ffffff 100%);
}
.coil-change-header {
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px dashed rgba(59, 130, 246, 0.2);
}
.change-label {
font-size: 12px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.coil-change-wrapper.new .change-label {
color: #10b981;
}
/* 紧凑模式操作中心 */
.action-center-compact {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
background: linear-gradient(145deg, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.04) 100%);
border-radius: 8px;
}
.action-center-compact .action-arrow {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #3b82f6 0%, #2563eb 100%);
color: white;
border-radius: 50%;
margin-right: 10px;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.4);
}
.action-center-compact .action-info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.action-center-compact .action-tag {
font-size: 11px;
margin-bottom: 2px;
}
.action-center-compact .action-operator,
.action-center-compact .action-time {
font-size: 11px;
color: #64748b;
}
/* 紧凑模式 - 用于合卷段并排展示 */
.step-content-compact {
flex-direction: column;
gap: 8px;
}
.step-content-compact .coil-card-wrapper {
width: 100%;
min-width: 240px;
}
.step-content-compact .coil-card-futuristic {
padding: 6px;
}
.step-content-compact .coil-futuristic-header {
padding: 4px 6px;
margin-bottom: 6px;
}
.step-content-compact .coil-futuristic-no {
font-size: 13px;
font-weight: 700;
}
.step-content-compact .coil-futuristic-weight {
font-size: 12px;
padding: 2px 6px;
}
.step-content-compact .coil-icon-container {
width: 50px;
height: 50px;
}
.step-content-compact .coil-svg {
width: 50px;
height: 50px;
}
.step-content-compact .coil-attr-tags {
padding-left: 55px;
}
.step-content-compact .attr-tag.attr-top-left,
.step-content-compact .attr-tag.attr-top-right,
.step-content-compact .attr-tag.attr-bottom-left,
.step-content-compact .attr-tag.attr-bottom-right {
display: none;
}
.step-content-compact .attr-tag.attr-remark {
display: none;
}
.step-content-compact .coil-futuristic-footer {
padding: 4px 6px;
}
.step-content-compact .footer-item {
padding: 2px 4px;
margin-right: 6px;
}
.step-content-compact .footer-label {
font-size: 9px;
}
.step-content-compact .footer-value {
font-size: 10px;
}
.step-content-compact .action-center {
flex-direction: row;
padding: 6px 8px;
background: rgba(59, 130, 246, 0.08);
border-radius: 6px;
}
.step-content-compact .action-arrow {
width: 28px;
height: 28px;
margin-right: 8px;
}
.step-content-compact .action-info {
flex: 1;
justify-content: flex-start;
}
.step-content-compact .action-tag,
.step-content-compact .action-operator,
.step-content-compact .action-time {
font-size: 11px;
}
.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;
}
.cost-value {
font-size: 20px;
font-weight: 700;
color: #d97706;
text-shadow: 0 0 8px rgba(217, 119, 6, 0.4);
}
.cost-unit {
font-size: 12px;
color: #64748b;
margin-left: 4px;
}
</style>