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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>