You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

612 lines
22 KiB
Vue

<template>
<div class="backward2-container">
<el-card class="tree-card" shadow="never" v-loading="loading">
<template #header>
<div class="card-header">
<span class="header-title">追溯结构</span>
<el-button type="primary" link icon="ArrowLeft" @click="goBack"></el-button>
</div>
</template>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<el-icon v-if="!data.children"><Document /></el-icon>
<el-icon v-else><Folder /></el-icon>
<span class="label-text">{{ node.label }}</span>
</span>
</template>
</el-tree>
</el-card>
<div class="content-area">
<div class="main-area">
<div class="top-section">
<el-card class="production-card" shadow="never" v-loading="loading">
<template #header>
<div class="blue-header">
<span class="header-title">生产信息</span>
</div>
</template>
<el-descriptions v-if="barcodeType === '1'" :column="1" border size="small" class="production-desc">
<el-descriptions-item label="条码">{{ productionInfo.barcode }}</el-descriptions-item>
<el-descriptions-item label="生产日期">{{ productionInfo.productionDate }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ productionInfo.machine }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ productionInfo.shift }}</el-descriptions-item>
<el-descriptions-item label="班组">{{ productionInfo.team }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ productionInfo.materialName }}</el-descriptions-item>
<el-descriptions-item label="车次">{{ productionInfo.trainNo }}</el-descriptions-item>
<el-descriptions-item label="重量">{{ productionInfo.weight }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ productionInfo.operator }}</el-descriptions-item>
</el-descriptions>
<el-descriptions v-else-if="barcodeType === '2'" :column="1" border size="small" class="production-desc">
<el-descriptions-item label="流转卡号">{{ productionInfo.cardNo }}</el-descriptions-item>
<el-descriptions-item label="接班编号">{{ productionInfo.shiftNo }}</el-descriptions-item>
<el-descriptions-item label="架子号">{{ productionInfo.shelfNo }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ productionInfo.materialName }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ productionInfo.qty }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ productionInfo.unitName }}</el-descriptions-item>
<el-descriptions-item label="宽度">{{ productionInfo.width }}</el-descriptions-item>
<el-descriptions-item label="计划号">{{ productionInfo.orderNumber }}</el-descriptions-item>
<el-descriptions-item label="重量">{{ productionInfo.weight }}</el-descriptions-item>
</el-descriptions>
<el-descriptions v-else-if="barcodeType === '3'" :column="1" border size="small" class="production-desc">
<el-descriptions-item label="成型号">{{ productionInfo.greenTyreNo }}</el-descriptions-item>
<el-descriptions-item label="成型时间">{{ productionInfo.beginTime }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ productionInfo.equipId }}</el-descriptions-item>
<el-descriptions-item label="操作人">{{ productionInfo.operator }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ productionInfo.shift }}</el-descriptions-item>
<el-descriptions-item label="班组">{{ productionInfo.team }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ productionInfo.materialName }}</el-descriptions-item>
<el-descriptions-item label="重量">{{ productionInfo.weight }}</el-descriptions-item>
</el-descriptions>
<el-descriptions v-else :column="1" border size="small" class="production-desc">
<el-descriptions-item label="条码">{{ productionInfo.barcode }}</el-descriptions-item>
<el-descriptions-item label="生产日期">{{ productionInfo.productionDate }}</el-descriptions-item>
<el-descriptions-item label="物料名称">{{ productionInfo.materialName }}</el-descriptions-item>
<el-descriptions-item label="机台">{{ productionInfo.machine }}</el-descriptions-item>
<el-descriptions-item label="班次">{{ productionInfo.shift }}</el-descriptions-item>
<el-descriptions-item label="班组">{{ productionInfo.team }}</el-descriptions-item>
<el-descriptions-item label="车次">{{ productionInfo.trainNo }}</el-descriptions-item>
<el-descriptions-item label="设定重量">{{ productionInfo.setWeight }}</el-descriptions-item>
<el-descriptions-item label="实际重量">{{ productionInfo.actualWeight }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="quality-card" shadow="never" v-loading="loading">
<template #header>
<div class="blue-header">
<span class="header-title">质检信息</span>
</div>
</template>
<div class="table-scroll">
<el-table :data="qualityData" border stripe size="small" :fit="false" :scrollbar-always-on="true" height="240">
<el-table-column prop="reportNo" label="报告单号" min-width="130" align="center" show-overflow-tooltip />
<el-table-column prop="times" label="检验次数" min-width="90" align="center" />
<el-table-column prop="item" label="检验项目" min-width="140" align="center" show-overflow-tooltip />
<el-table-column prop="standard" label="标准" min-width="130" align="center" show-overflow-tooltip />
<el-table-column prop="value" label="检验值" min-width="100" align="center" show-overflow-tooltip />
<el-table-column prop="deviation" label="偏差" min-width="90" align="center" />
<el-table-column prop="result" label="结果" min-width="90" align="center" />
</el-table>
</div>
</el-card>
</div>
<el-card v-if="selectedNodeLevel <= 2" class="weighing-card" shadow="never" v-loading="loading">
<template #header>
<div class="blue-header">
<span class="header-title">称量信息</span>
</div>
</template>
<div class="table-scroll">
<el-table :data="weighingData" border stripe size="small" :fit="false" :scrollbar-always-on="true" height="240">
<el-table-column prop="carBarcode" label="车条码" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="order" label="称量次序" min-width="90" align="center" />
<el-table-column label="物料编码" min-width="170" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.materialName || row.materialCode || '-' }}</template>
</el-table-column>
<el-table-column prop="machine" label="机台" min-width="100" align="center" />
<el-table-column prop="setWeight" label="设定重量" min-width="100" align="right" />
<el-table-column prop="actualWeight" label="实际重量" min-width="100" align="right" />
<el-table-column prop="tolerance" label="允许误差" min-width="100" align="center" />
<el-table-column prop="actualDeviation" label="实际误差" min-width="100" align="center" />
<el-table-column prop="weighTime" label="称量时间" min-width="170" align="center" show-overflow-tooltip />
<el-table-column prop="trainNo" label="车次" min-width="90" align="center" />
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center" />
</el-table>
</div>
</el-card>
<el-card v-if="selectedNodeLevel <= 2" class="mixing-card" shadow="never" v-loading="loading">
<template #header>
<div class="blue-header">
<span class="header-title">混炼信息</span>
</div>
</template>
<div class="table-scroll">
<el-table :data="mixingProcessData" border stripe size="small" :fit="false" :scrollbar-always-on="true" height="240">
<el-table-column prop="carBarcode" label="车条码" min-width="180" align="center" show-overflow-tooltip />
<el-table-column prop="step" label="混炼步骤" min-width="90" align="center" />
<el-table-column prop="condition" label="条件" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="duration" label="时间" min-width="90" align="center" />
<el-table-column prop="temperature" label="温度(℃)" min-width="100" align="right" />
<el-table-column prop="energy" label="能量" min-width="90" align="right" />
<el-table-column prop="power" label="功率" min-width="90" align="right" />
<el-table-column prop="pressure" label="压力" min-width="90" align="right" />
<el-table-column prop="rpm" label="转速" min-width="90" align="right" />
<el-table-column prop="action" label="动作" min-width="120" align="center" show-overflow-tooltip />
<el-table-column prop="recordTime" label="保存时间" min-width="180" align="center" show-overflow-tooltip />
</el-table>
</div>
</el-card>
</div>
<div class="curve-area" :class="{ collapsed: curveCollapsed }">
<div class="curve-toggle" @click="toggleCurve">
<span class="toggle-text">曲线信息</span>
<el-icon class="toggle-icon" :class="{ rotated: !curveCollapsed }"><ArrowRight /></el-icon>
</div>
<el-card class="curve-card" shadow="never" v-show="!curveCollapsed">
<template #header>
<div class="blue-header">
<span class="header-title">曲线信息</span>
<div class="legend-group">
<span class="legend-item"><i class="legend-dot" style="background: #ff4d4f"></i>温度</span>
<span class="legend-item"><i class="legend-dot" style="background: #40a9ff"></i>功率</span>
<span class="legend-item"><i class="legend-dot" style="background: #36cfc9"></i>能量</span>
<span class="legend-item"><i class="legend-dot" style="background: #95de64"></i>压力</span>
<span class="legend-item"><i class="legend-dot" style="background: #f7d13d"></i>转速</span>
</div>
<el-button size="small" text type="primary" style="color: #fff" @click="toggleCurve"></el-button>
</div>
</template>
<div ref="chartRef" class="chart-container"></div>
</el-card>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="MixTraceBackward2">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ArrowRight, Document, Folder } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import * as echarts from 'echarts';
import backwardData from './data/backward2.json';
import curveDataJson from './data/混炼曲线数据.json';
type BarcodeType = '1' | '2' | '3' | '4';
const route = useRoute();
const router = useRouter();
const detailMap = (backwardData as any).detailMap || {};
const detailKeys = Object.keys(detailMap);
const loading = ref(false);
const detailId = ref<string>('');
const barcodeType = ref<BarcodeType>('4');
const treeData = ref<any[]>([]);
const qualityData = ref<any[]>([]);
const weighingData = ref<any[]>([]);
const mixingProcessData = ref<any[]>([]);
const curveCollapsed = ref(true);
/** 当前选中节点的树层级1-2级显示全部3级+只显示生产和质检 */
const selectedNodeLevel = ref<number>(1);
const treeProps = { children: 'children', label: 'label' };
const treeRef = ref();
const chartRef = ref<HTMLDivElement>();
let chartInstance: echarts.ECharts | null = null;
const goBack = () => {
if (window.history.length > 1) {
router.back();
return;
}
router.push('/mes/mixTrace/show/backward1');
};
const productionInfo = computed(() => {
return detailMap[detailId.value]?.production || {};
});
// 演示模式固定使用假数据条码 QUXIAN_DEMO_001
const curveDataSource = computed(() => (curveDataJson as any)['QUXIAN_DEMO_001'] || {});
const resolveDetailId = (routeId: string, routeType: string): string => {
if (routeId && detailMap[routeId]) return routeId;
if (routeType) {
const match = detailKeys.find((key) => String(detailMap[key]?.barcodeType) === routeType);
if (match) return match;
}
return detailKeys[0] || '';
};
const syncByRoute = () => {
const routeId = String(route.query.id || '');
const routeType = String(route.query.barcodeType || '');
const resolved = resolveDetailId(routeId, routeType);
if (!resolved) return;
detailId.value = resolved;
barcodeType.value = String(detailMap[resolved]?.barcodeType || routeType || '4') as BarcodeType;
};
const loadData = () => {
loading.value = true;
syncByRoute();
const detail = detailMap[detailId.value] || {};
treeData.value = detail.treeData || [];
qualityData.value = detail.qualityData || [];
weighingData.value = detail.weighingData || [];
mixingProcessData.value = detail.mixingProcessData || [];
nextTick(() => {
const defaultKey = treeData.value?.[0]?.id;
if (defaultKey && treeRef.value) {
treeRef.value.setCurrentKey(defaultKey);
}
});
loading.value = false;
};
const initChart = () => {
if (!chartRef.value) return;
const cd = curveDataSource.value;
const toNumberOrNull = (value: any): number | null => {
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
let xAxisData: Array<string | number> = [];
let temperatureData: Array<number | null> = [];
let powerData: Array<number | null> = [];
let energyData: Array<number | null> = [];
let pressureData: Array<number | null> = [];
let speedData: Array<number | null> = [];
if (Array.isArray(cd.time) && cd.time.length > 0) {
xAxisData = cd.time.map((v: any) => {
const num = Number(v);
return Number.isFinite(num) ? num : 0;
});
const buildSeriesData = (source: any): Array<number | null> => {
const arr = Array.isArray(source) ? source : [];
return arr.map((v: any) => toNumberOrNull(v));
};
temperatureData = buildSeriesData(cd.MixingTemp);
powerData = buildSeriesData(cd.MixingPower);
energyData = buildSeriesData(cd.MixingEnergy);
pressureData = buildSeriesData(cd.MixingPress);
speedData = buildSeriesData(cd.MixingSpeed);
}
const calcRange = (arr: Array<number | null>) => {
const nums = arr.filter((n): n is number => typeof n === 'number');
if (!nums.length) return {};
const min = Math.min(...nums);
const max = Math.max(...nums);
const pad = min === max ? Math.max(Math.abs(min) * 0.1, 1) : (max - min) * 0.1;
return {
min: Number((min - pad).toFixed(2)),
max: Number((max + pad).toFixed(2))
};
};
const leftAxisRange = calcRange([...temperatureData, ...energyData, ...pressureData, ...speedData]);
const rightAxisRange = calcRange(powerData);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
chartInstance = echarts.init(chartRef.value);
if (xAxisData.length === 0) {
chartInstance.setOption({
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: { text: '暂无曲线数据', fill: '#909399', fontSize: 14 }
}
});
return;
}
chartInstance.setOption({
animation: false,
grid: { left: '5%', right: '5%', top: '10%', bottom: '10%', containLabel: true },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: xAxisData,
boundaryGap: false,
name: '时间(s)'
},
yAxis: [
{ type: 'value', name: '温度/能量/压力/转速', position: 'left', axisLine: { show: true }, ...leftAxisRange },
{ type: 'value', name: '功率(kW)', position: 'right', axisLine: { show: true }, ...rightAxisRange }
],
series: [
{ name: '温度', type: 'line', yAxisIndex: 0, data: temperatureData, smooth: true, connectNulls: false, showSymbol: false, itemStyle: { color: '#ff4d4f' } },
{ name: '功率', type: 'line', yAxisIndex: 1, data: powerData, smooth: true, connectNulls: false, showSymbol: false, itemStyle: { color: '#40a9ff' } },
{ name: '能量', type: 'line', yAxisIndex: 0, data: energyData, smooth: true, connectNulls: false, showSymbol: false, itemStyle: { color: '#36cfc9' } },
{ name: '压力', type: 'line', yAxisIndex: 0, data: pressureData, smooth: true, connectNulls: false, showSymbol: false, itemStyle: { color: '#95de64' } },
{ name: '转速', type: 'line', yAxisIndex: 0, data: speedData, smooth: true, connectNulls: false, showSymbol: false, itemStyle: { color: '#f7d13d' } }
]
});
};
const resizeChart = () => {
chartInstance?.resize();
};
const renderChartWithDelay = () => {
nextTick(() => {
setTimeout(() => {
initChart();
resizeChart();
}, 320);
});
};
const toggleCurve = () => {
curveCollapsed.value = !curveCollapsed.value;
if (!curveCollapsed.value) {
renderChartWithDelay();
}
};
const handleNodeClick = (nodeData: any, node: any) => {
selectedNodeLevel.value = node?.level ?? 1;
if (nodeData?.detailId && detailMap[nodeData.detailId]) {
detailId.value = nodeData.detailId;
barcodeType.value = String(detailMap[detailId.value]?.barcodeType || '4') as BarcodeType;
loadData();
if (!curveCollapsed.value) {
renderChartWithDelay();
}
return;
}
ElMessage.info(`当前节点: ${nodeData?.label || '-'}`);
};
watch(
() => [route.query.id, route.query.barcodeType],
() => {
loadData();
if (!curveCollapsed.value) {
renderChartWithDelay();
}
}
);
watch(detailId, () => {
if (!curveCollapsed.value) {
renderChartWithDelay();
}
});
onMounted(() => {
loadData();
window.addEventListener('resize', resizeChart);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeChart);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped lang="scss">
.backward2-container {
display: flex;
gap: 16px;
padding: 16px;
height: calc(100vh - 90px);
.tree-card {
width: 310px;
flex-shrink: 0;
overflow-y: auto;
:deep(.el-card__header) {
padding: 0;
}
.card-header {
background-color: #2f6ea5;
color: #fff;
padding: 12px 14px;
.header-title {
font-size: 14px;
font-weight: 600;
}
}
:deep(.el-tree-node__content) {
height: 32px;
}
.custom-tree-node {
display: flex;
align-items: center;
font-size: 13px;
.label-text {
margin-left: 6px;
}
}
}
.content-area {
flex: 1;
min-width: 0;
display: flex;
overflow: hidden;
.main-area {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 16px;
overflow-y: auto;
> * {
flex-shrink: 0;
}
}
.top-section {
display: flex;
gap: 16px;
flex-shrink: 0;
.production-card {
flex: 0 0 420px;
}
.quality-card {
flex: 1;
min-width: 0;
}
}
.table-scroll {
width: 100%;
min-width: 0;
}
.blue-header {
background-color: #2f6ea5;
color: #fff;
padding: 10px 14px;
margin: -20px -20px 16px -20px;
display: flex;
align-items: center;
justify-content: space-between;
.header-title {
font-size: 14px;
font-weight: 600;
}
}
.production-desc {
:deep(.el-descriptions__label) {
width: 90px;
font-weight: 600;
background: #fafafa;
}
}
:deep(.weighing-card .el-card__body),
:deep(.mixing-card .el-card__body),
:deep(.quality-card .el-card__body) {
overflow: visible;
}
.curve-area {
flex: 0 0 50%;
display: flex;
height: 100%;
flex-shrink: 0;
transition: flex-basis 0.3s ease;
&.collapsed {
flex-basis: 30px;
width: 30px;
}
.curve-toggle {
width: 30px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #fff;
background-color: #2f6ea5;
cursor: pointer;
user-select: none;
.toggle-text {
writing-mode: vertical-rl;
letter-spacing: 3px;
font-size: 13px;
font-weight: 600;
}
.toggle-icon {
margin-top: 8px;
transition: transform 0.3s ease;
&.rotated {
transform: rotate(180deg);
}
}
}
.curve-card {
flex: 1;
min-width: 0;
overflow: hidden;
:deep(.el-card__header) {
padding: 0;
}
:deep(.el-card__body) {
height: calc(100% - 48px);
padding: 10px;
}
.legend-group {
display: flex;
gap: 10px;
flex-wrap: nowrap;
align-items: center;
.legend-item {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
white-space: nowrap;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
}
.chart-container {
width: 100%;
height: 100%;
min-height: 300px;
}
}
}
}
}
</style>