feat(mixTrace): 添加轮胎追溯功能和图表优化

- 添加返回列表按钮和路由导航功能
- 集成轮胎全程追溯页面,支持胎号查询和流程图点击跳转
- 重构图表数据处理逻辑,支持混炼步骤和时间轴两种显示模式
- 优化图表配置,添加坐标轴范围计算和空数据提示
- 更新系列数据显示样式,设置折线不平滑和连接空值
- 添加上顶栓位置数据解析和显示功能
- 配置图表动画关闭和符号显示选项
master
zangch@mesnac.com 1 week ago
parent f7de7c01fe
commit 846142e739

@ -267,6 +267,45 @@ export const constantRoutes: RouteRecordRaw[] = [
meta: { title: '反向追溯详情', activeMenu: '/mes/mixTrace/show/backward1', noCache: true }
}
]
},
{
path: '/mes/mixTrace/show/tire1',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/mes/mixTrace/show/tire1.vue'),
name: 'MixTraceTire1',
meta: { title: '轮胎全程追溯', activeMenu: '/mes/mixTrace/show', noCache: true }
}
]
},
{
path: '/mes/mixTrace/show/tire2',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/mes/mixTrace/show/tire2.vue'),
name: 'MixTraceTire2',
meta: { title: '轮胎详情', activeMenu: '/mes/mixTrace/show/tire1', noCache: true }
}
]
},
{
path: '/mes/mixTrace/show/forward1',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/mes/mixTrace/show/forward1.vue'),
name: 'MixTraceForward1',
meta: { title: '正向追溯', activeMenu: '/mes/mixTrace/show', noCache: true }
}
]
}
];

@ -4,6 +4,7 @@
<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
@ -182,7 +183,7 @@
<script setup lang="ts" name="MixTraceBackward2">
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
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';
@ -191,6 +192,7 @@ import backwardData from './data/backward2.json';
type BarcodeType = '1' | '2' | '3' | '4';
const route = useRoute();
const router = useRouter();
const detailMap = (backwardData as any).detailMap || {};
const detailKeys = Object.keys(detailMap);
@ -208,6 +210,14 @@ 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 || {};
});
@ -258,7 +268,86 @@ const loadData = () => {
const initChart = () => {
if (!chartRef.value) return;
const cd = curveDataSource.value;
const xAxisData = cd.mixingTime || [];
const mixingRows = Array.isArray(mixingProcessData.value) ? mixingProcessData.value : [];
const toNumberOrNull = (value: any): number | null => {
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
const listLength = (value: any): number => {
return Array.isArray(value) ? value.length : 0;
};
const buildPositionFromAction = (row: any): number | null => {
const actionText = String(row?.action || row?.condition || '');
if (actionText.includes('压上顶栓')) return 1;
if (actionText.includes('升上顶栓')) return 0;
return 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> = [];
let positionData: Array<number | null> = [];
if (mixingRows.length > 0) {
xAxisData = mixingRows.map((row: any, idx: number) => row.step ?? idx + 1);
temperatureData = mixingRows.map((row: any) => toNumberOrNull(row.temperature));
powerData = mixingRows.map((row: any) => toNumberOrNull(row.power));
energyData = mixingRows.map((row: any) => toNumberOrNull(row.energy));
pressureData = mixingRows.map((row: any) => toNumberOrNull(row.pressure));
speedData = mixingRows.map((row: any) => toNumberOrNull(row.rpm));
positionData = mixingRows.map((row: any) => buildPositionFromAction(row));
} else {
const maxLen = Math.max(
listLength(cd.mixingTime),
listLength(cd.temperature),
listLength(cd.power),
listLength(cd.energy),
listLength(cd.pressure),
listLength(cd.speed),
listLength(cd.position)
);
xAxisData =
Array.isArray(cd.mixingTime) && cd.mixingTime.length > 0
? cd.mixingTime.map((v: any, i: number) => {
const num = Number(v);
return Number.isFinite(num) ? num : i + 1;
})
: Array.from({ length: maxLen }, (_, i) => i + 1);
const buildSeriesData = (source: any): Array<number | null> => {
const arr = Array.isArray(source) ? source : [];
return xAxisData.map((_, idx) => toNumberOrNull(arr[idx]));
};
temperatureData = buildSeriesData(cd.temperature);
powerData = buildSeriesData(cd.power);
energyData = buildSeriesData(cd.energy);
pressureData = buildSeriesData(cd.pressure);
speedData = buildSeriesData(cd.speed);
positionData = buildSeriesData(cd.position);
}
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);
const rightAxisRange = calcRange([...powerData, ...energyData, ...pressureData, ...speedData, ...positionData]);
if (chartInstance) {
chartInstance.dispose();
@ -266,26 +355,47 @@ const initChart = () => {
}
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)' },
xAxis: {
type: 'category',
data: xAxisData,
boundaryGap: false,
name: mixingRows.length > 0 ? '混炼步骤' : '时间(s)'
},
yAxis: [
{ type: 'value', name: '温度(℃)', position: 'left', axisLine: { show: true } },
{ type: 'value', name: '其他参数', position: 'right', axisLine: { show: true } }
{ type: 'value', name: '温度(℃)', position: 'left', axisLine: { show: true }, ...leftAxisRange },
{ type: 'value', name: '其他参数', position: 'right', axisLine: { show: true }, ...rightAxisRange }
],
series: [
{ name: '温度', type: 'line', yAxisIndex: 0, data: cd.temperature || [], smooth: true, itemStyle: { color: '#ff4d4f' } },
{ name: '功率', type: 'line', yAxisIndex: 1, data: cd.power || [], smooth: true, itemStyle: { color: '#40a9ff' } },
{ name: '能量', type: 'line', yAxisIndex: 1, data: cd.energy || [], smooth: true, itemStyle: { color: '#36cfc9' } },
{ name: '压力', type: 'line', yAxisIndex: 1, data: cd.pressure || [], smooth: true, itemStyle: { color: '#95de64' } },
{ name: '转速', type: 'line', yAxisIndex: 1, data: cd.speed || [], smooth: true, itemStyle: { color: '#f7d13d' } },
{ name: '温度', type: 'line', yAxisIndex: 0, data: temperatureData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#ff4d4f' } },
{ name: '功率', type: 'line', yAxisIndex: 1, data: powerData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#40a9ff' } },
{ name: '能量', type: 'line', yAxisIndex: 1, data: energyData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#36cfc9' } },
{ name: '压力', type: 'line', yAxisIndex: 1, data: pressureData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#95de64' } },
{ name: '转速', type: 'line', yAxisIndex: 1, data: speedData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#f7d13d' } },
{
name: '上顶栓',
type: 'line',
yAxisIndex: 1,
data: cd.position || [],
data: positionData,
smooth: false,
connectNulls: false,
showSymbol: true,
lineStyle: { type: 'dashed' },
itemStyle: { color: '#ffa07a' }
}

@ -13,5 +13,28 @@
{ "label": "成型机台", "value": "CX402" },
{ "label": "硫化操作人", "value": "马小平" },
{ "label": "成型操作人", "value": "克一赛尔·克然木" }
],
"treeData": [
{
"id": "T1",
"label": "轮胎条码: 0G0AB38390",
"children": [
{
"id": "T1-1",
"label": "胎胚条码: 226402001270",
"children": [
{ "id": "T1-1-1", "label": "KB110-F 胎面 01130052601040025" },
{ "id": "T1-1-2", "label": "N465A1005A 内衬层 260101N4600125" },
{ "id": "T1-1-3", "label": "RW032 胎侧 260101RW00010360" },
{ "id": "T1-1-4", "label": "K2A182K26 2#带束层 260101K2A00010195" },
{ "id": "T1-1-5", "label": "K1A190A26 1#带束层 260101K1A00010224" },
{ "id": "T1-1-6", "label": "E1D62090 1#胎体帘布 260101E1D00010087" },
{ "id": "T1-1-7", "label": "E2D50090 2#胎体帘布 260101E2D00010062" },
{ "id": "T1-1-8", "label": "RS001 冠带条 260101RS000010068" },
{ "id": "T1-1-9", "label": "GA4106D25AA 胎圈 260101GA400070003" }
]
}
]
}
]
}

@ -234,6 +234,86 @@ const handleSearch = () => {
const initChart = () => {
if (!chartRef.value) return;
const cd = curveDataSource.value;
const mixingRows = Array.isArray(mixingProcessData.value) ? mixingProcessData.value : [];
const toNumberOrNull = (value: any): number | null => {
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
const listLength = (value: any): number => {
return Array.isArray(value) ? value.length : 0;
};
const buildPositionFromAction = (row: any): number | null => {
const actionText = String(row?.action || row?.condition || '');
if (actionText.includes('压上顶栓')) return 1;
if (actionText.includes('升上顶栓')) return 0;
return 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> = [];
let positionData: Array<number | null> = [];
if (mixingRows.length > 0) {
xAxisData = mixingRows.map((row: any, idx: number) => row.step ?? idx + 1);
temperatureData = mixingRows.map((row: any) => toNumberOrNull(row.temperature));
powerData = mixingRows.map((row: any) => toNumberOrNull(row.power));
energyData = mixingRows.map((row: any) => toNumberOrNull(row.energy));
pressureData = mixingRows.map((row: any) => toNumberOrNull(row.pressure));
speedData = mixingRows.map((row: any) => toNumberOrNull(row.rpm));
positionData = mixingRows.map((row: any) => buildPositionFromAction(row));
} else {
const maxLen = Math.max(
listLength(cd.mixingTime),
listLength(cd.temperature),
listLength(cd.power),
listLength(cd.energy),
listLength(cd.pressure),
listLength(cd.speed),
listLength(cd.position)
);
xAxisData =
Array.isArray(cd.mixingTime) && cd.mixingTime.length > 0
? cd.mixingTime.map((v: any, i: number) => {
const num = Number(v);
return Number.isFinite(num) ? num : i + 1;
})
: Array.from({ length: maxLen }, (_, i) => i + 1);
const buildSeriesData = (source: any): Array<number | null> => {
const arr = Array.isArray(source) ? source : [];
return xAxisData.map((_, idx) => toNumberOrNull(arr[idx]));
};
temperatureData = buildSeriesData(cd.temperature);
powerData = buildSeriesData(cd.power);
energyData = buildSeriesData(cd.energy);
pressureData = buildSeriesData(cd.pressure);
speedData = buildSeriesData(cd.speed);
positionData = buildSeriesData(cd.position);
}
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);
const rightAxisRange = calcRange([...powerData, ...energyData, ...pressureData, ...speedData, ...positionData]);
if (chartInstance) {
chartInstance.dispose();
@ -241,26 +321,47 @@ const initChart = () => {
}
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: cd.mixingTime || [], boundaryGap: false, name: '时间(s)' },
xAxis: {
type: 'category',
data: xAxisData,
boundaryGap: false,
name: mixingRows.length > 0 ? '混炼步骤' : '时间(s)'
},
yAxis: [
{ type: 'value', name: '温度(℃)', position: 'left', axisLine: { show: true } },
{ type: 'value', name: '其他参数', position: 'right', axisLine: { show: true } }
{ type: 'value', name: '温度(℃)', position: 'left', axisLine: { show: true }, ...leftAxisRange },
{ type: 'value', name: '其他参数', position: 'right', axisLine: { show: true }, ...rightAxisRange }
],
series: [
{ name: '温度', type: 'line', yAxisIndex: 0, data: cd.temperature || [], smooth: true, itemStyle: { color: '#ff4d4f' } },
{ name: '功率', type: 'line', yAxisIndex: 1, data: cd.power || [], smooth: true, itemStyle: { color: '#40a9ff' } },
{ name: '能量', type: 'line', yAxisIndex: 1, data: cd.energy || [], smooth: true, itemStyle: { color: '#36cfc9' } },
{ name: '压力', type: 'line', yAxisIndex: 1, data: cd.pressure || [], smooth: true, itemStyle: { color: '#95de64' } },
{ name: '转速', type: 'line', yAxisIndex: 1, data: cd.speed || [], smooth: true, itemStyle: { color: '#f7d13d' } },
{ name: '温度', type: 'line', yAxisIndex: 0, data: temperatureData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#ff4d4f' } },
{ name: '功率', type: 'line', yAxisIndex: 1, data: powerData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#40a9ff' } },
{ name: '能量', type: 'line', yAxisIndex: 1, data: energyData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#36cfc9' } },
{ name: '压力', type: 'line', yAxisIndex: 1, data: pressureData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#95de64' } },
{ name: '转速', type: 'line', yAxisIndex: 1, data: speedData, smooth: false, connectNulls: false, showSymbol: true, itemStyle: { color: '#f7d13d' } },
{
name: '上顶栓',
type: 'line',
yAxisIndex: 1,
data: cd.position || [],
data: positionData,
smooth: false,
connectNulls: false,
showSymbol: true,
lineStyle: { type: 'dashed' },
itemStyle: { color: '#ffa07a' }
}

@ -3,71 +3,75 @@
<el-card class="header-card" shadow="never">
<div class="header-row">
<span class="label">胎号:</span>
<el-input v-model="queryTireNo" style="width: 220px" clearable />
<el-input v-model="queryTireNo" style="width: 220px" clearable @keyup.enter="handleQuery" />
<el-button type="primary" @click="handleQuery"></el-button>
<el-button @click="handleClear"></el-button>
</div>
</el-card>
<el-card class="process-card" shadow="never">
<div class="process-flow">
<template v-for="(step, idx) in processSteps" :key="step.name">
<div class="process-step">
<div class="step-icon" :class="{ active: step.active }">
<el-icon :size="28"><component :is="step.icon" /></el-icon>
</div>
<div class="step-label">{{ step.name }}</div>
</div>
<div v-if="idx < processSteps.length - 1" class="arrow"></div>
</template>
<div class="process-wrapper" @click="handleProcessClick">
<img class="process-img" :src="mixTireFull" alt="轮胎工艺流程" />
</div>
</el-card>
<el-card class="info-card" shadow="never">
<el-card class="tree-card" shadow="never">
<template #header>
<div class="blue-header">
<span class="header-title">轮胎产品: {{ tireNo }}</span>
<span class="header-title">批次列表</span>
</div>
</template>
<div class="info-grid">
<div class="info-item" v-for="item in tireInfoList" :key="`${item.label}-${item.value}`">
<span class="item-label">{{ item.label }}:</span>
<span class="item-value">{{ item.value }}</span>
</div>
</div>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
highlight-current
>
<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>
</template>
<script setup lang="ts" name="MixTraceTire1">
import { computed, ref } from 'vue';
import {
Box,
MagicStick,
Odometer,
Operation,
Promotion,
Select,
View
} from '@element-plus/icons-vue';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Document, Folder } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import tireData from './data/tire1.json';
import mixTireFull from './mixTire.png';
const tireNo = ref((tireData as any).tireNo || '');
const router = useRouter();
const source = tireData as any;
const tireNo = ref(source.tireNo || '');
const queryTireNo = ref(tireNo.value);
const tireInfoList = ref<any[]>((tireData as any).tireInfoList || []);
const treeData = ref<any[]>(source.treeData || []);
const treeProps = { children: 'children', label: 'label' };
const treeRef = ref();
const processSteps = computed(() => [
{ name: '半制品', icon: Operation, active: false },
{ name: '半制品质检', icon: Select, active: false },
{ name: '成型', icon: Operation, active: true },
{ name: '成型质检', icon: Select, active: true },
{ name: '硫化', icon: MagicStick, active: true },
{ name: '动平衡质检', icon: Odometer, active: false },
{ name: '外观质检', icon: View, active: false },
{ name: '成品入库', icon: Box, active: false },
{ name: '成品出库', icon: Promotion, active: false }
]);
/**
* 流程图各节点在图片中的水平位置比例基于 mixTire.png 实际布局
* 半制品(~8%) 半制品质检(~20%) 成型(~31%) 成型质检(~39%) 硫化(~47%)
* 质检区[外观/X光/动平衡/均匀性](~62%) 成品入库(~80%) 成品出库(~92%)
*
* 硫化左侧 = 反向追溯上游工序硫化右侧 = 正向追溯下游质检/入库/出库
*/
const REGIONS = {
// barcodeType=3
molding: { left: 0.25, right: 0.37 },
// /X//barcodeType=5
qualityCheck: { left: 0.52, right: 0.73 }
};
const handleQuery = () => {
if (!queryTireNo.value.trim()) {
@ -81,6 +85,37 @@ const handleQuery = () => {
const handleClear = () => {
queryTireNo.value = '';
};
/**
* 点击流程图节点根据坐标判断所属区域并跳转
* 反向追溯条码类型1-架子号 2-半制品卡号 3-成型号 4-硫化号
* 正向追溯条码类型1-原材料 2-胶料 3-半制品 4-胎胚 5-成品胎 6-小料
*/
const handleProcessClick = (event: MouseEvent) => {
if (!tireNo.value) {
ElMessage.warning('请先输入胎号并查询');
return;
}
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
const x = event.clientX - rect.left;
const ratio = x / rect.width;
if (ratio >= REGIONS.molding.left && ratio <= REGIONS.molding.right) {
// "" =3()
router.push({
path: '/mes/mixTrace/show/backward1',
query: { barcodeType: '3', barcodeNo: tireNo.value }
});
} else if (ratio >= REGIONS.qualityCheck.left && ratio <= REGIONS.qualityCheck.right) {
// ""/X// =5()
router.push({
path: '/mes/mixTrace/show/forward1',
query: { barcodeType: '5', barcodeNo: tireNo.value }
});
}
};
</script>
<style scoped lang="scss">
@ -104,62 +139,32 @@ const handleClear = () => {
.process-card {
margin-bottom: 16px;
background: linear-gradient(135deg, #2f6ea5 0%, #3f88c5 100%);
background: white;
:deep(.el-card__body) {
padding: 26px 16px;
padding: 24px 16px;
}
}
.process-flow {
.process-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
overflow-x: auto;
cursor: pointer;
}
.process-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
min-width: 78px;
.step-icon {
width: 70px;
height: 70px;
border-radius: 12px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
color: #2f6ea5;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
&.active {
background: linear-gradient(135deg, #f97316 0%, #ef4444 100%);
color: #fff;
}
.process-img {
display: block;
width: 100%;
height: 50vh;
user-select: none;
object-fit: contain;
}
.step-label {
color: #fff;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
text-align: center;
}
}
.arrow {
color: #fff;
font-size: 22px;
font-weight: 700;
}
.info-card {
.tree-card {
:deep(.el-card__header) {
padding: 0;
}
}
.blue-header {
background-color: #2f6ea5;
@ -170,27 +175,18 @@ const handleClear = () => {
font-weight: 600;
}
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.info-item {
background: #f7f9fc;
border-left: 3px solid #2f6ea5;
border-radius: 4px;
padding: 10px;
.custom-tree-node {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
.item-label {
font-weight: 600;
color: #4b5563;
.el-icon {
color: #2f6ea5;
}
.item-value {
margin-left: 4px;
color: #1f2937;
.label-text {
color: #303133;
}
}
}

Loading…
Cancel
Save