<template>
|
<div class="app-container process-route-demo">
|
<PageHeader :content="`工艺路线演示 - ${routeInfo.productName || ''}`">
|
<template #right-button>
|
<el-button @click="goBack">返回</el-button>
|
</template>
|
</PageHeader>
|
|
<DemoControls
|
ref="controlsRef"
|
:steps="demoSteps"
|
:explanations="demoExplanations"
|
@play="handlePlay"
|
@pause="handlePause"
|
@reset="handleReset"
|
@stepChange="handleStepChange"
|
@speedChange="handleSpeedChange"
|
/>
|
|
<div class="demo-container" v-loading="loading">
|
<div class="route-visualization">
|
<div class="process-flow-container" v-if="processList.length > 0">
|
<div class="flow-wrapper">
|
<ProcessNode
|
v-for="(process, index) in processList"
|
:key="process.id || index"
|
:process="process"
|
:index="index"
|
:delay="index * 200"
|
:animation-speed="animationSpeed"
|
:active="activeProcessId === process.id"
|
:completed="completedProcesses.includes(process.id)"
|
:current="currentProcessId === process.id"
|
:animating="isAnimating && currentProcessId === process.id"
|
:show-arrow="index < processList.length - 1"
|
:show-progress="showProgress"
|
:progress="getProcessProgress(process)"
|
@click="handleProcessClick"
|
/>
|
</div>
|
|
<div class="flow-start-end">
|
<div class="flow-node start">
|
<el-icon><Goods /></el-icon>
|
<span>原材料</span>
|
</div>
|
<div class="flow-connector">
|
<svg width="60" height="24">
|
<line x1="0" y1="12" x2="50" y2="12" stroke="#67c23a" stroke-width="2" stroke-dasharray="5,5" />
|
<polygon points="50,8 60,12 50,16" fill="#67c23a" />
|
</svg>
|
</div>
|
<div class="flow-node end">
|
<el-icon><Box /></el-icon>
|
<span>成品</span>
|
</div>
|
</div>
|
</div>
|
|
<el-empty v-else description="暂无工艺路线数据" />
|
|
<div class="visualization-sidebar">
|
<el-card class="info-card" shadow="hover">
|
<template #header>
|
<span>当前工序信息</span>
|
</template>
|
<el-descriptions :column="1" border size="small" v-if="activeProcess">
|
<el-descriptions-item label="工序名称">{{ activeProcess.technologyOperationName || activeProcess.operationName }}</el-descriptions-item>
|
<el-descriptions-item label="产品名称">{{ activeProcess.productName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="规格型号">{{ activeProcess.model || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="单位">{{ activeProcess.unit || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="计费类型">{{ activeProcess.type === 1 ? '计件' : '计时' }}</el-descriptions-item>
|
<el-descriptions-item label="是否质检">{{ activeProcess.isQuality ? '是' : '否' }}</el-descriptions-item>
|
<el-descriptions-item label="是否生产">{{ activeProcess.isProduction ? '是' : '否' }}</el-descriptions-item>
|
<el-descriptions-item label="工序序号">{{ activeProcess.dragSort || getProcessIndex(activeProcess) + 1 }}</el-descriptions-item>
|
</el-descriptions>
|
<div v-else class="empty-info">点击工序节点查看详细信息</div>
|
</el-card>
|
|
<el-card class="progress-card" shadow="hover">
|
<template #header>
|
<span>生产进度模拟</span>
|
</template>
|
<div class="progress-info">
|
<div class="progress-stat">
|
<span class="stat-label">已完成工序</span>
|
<span class="stat-value">{{ completedProcesses.length }} / {{ processList.length }}</span>
|
</div>
|
<el-progress
|
:percentage="processList.length > 0 ? (completedProcesses.length / processList.length) * 100 : 0"
|
:stroke-width="10"
|
status="success"
|
/>
|
</div>
|
<div class="progress-actions">
|
<el-button type="primary" size="small" @click="simulateProgress">
|
模拟进度
|
</el-button>
|
<el-button size="small" @click="resetProgress">
|
重置
|
</el-button>
|
</div>
|
</el-card>
|
|
<el-card class="legend-card" shadow="hover">
|
<template #header>
|
<span>状态说明</span>
|
</template>
|
<div class="legend-item">
|
<span class="legend-icon default"></span>
|
<span class="legend-text">待加工</span>
|
</div>
|
<div class="legend-item">
|
<span class="legend-icon current"></span>
|
<span class="legend-text">当前工序</span>
|
</div>
|
<div class="legend-item">
|
<span class="legend-icon completed"></span>
|
<span class="legend-text">已完成</span>
|
</div>
|
<div class="legend-item">
|
<span class="legend-icon active"></span>
|
<span class="legend-text">选中状态</span>
|
</div>
|
</el-card>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { useRouter, useRoute } from 'vue-router'
|
import { Goods, Box } from '@element-plus/icons-vue'
|
import ProcessNode from './components/ProcessNode.vue'
|
import DemoControls from './components/DemoControls.vue'
|
import { findProcessRouteItemList } from '@/api/productionManagement/processRouteItem.js'
|
|
const router = useRouter()
|
const route = useRoute()
|
|
const loading = ref(false)
|
const processList = ref([])
|
const controlsRef = ref(null)
|
const animationSpeed = ref(1)
|
const isAnimating = ref(false)
|
const activeProcessId = ref(null)
|
const activeProcess = ref(null)
|
const currentProcessId = ref(null)
|
const completedProcesses = ref([])
|
const showProgress = ref(false)
|
const playTimer = ref(null)
|
const progressTimer = ref(null)
|
|
const routeId = computed(() => route.query.id)
|
const routeInfo = computed(() => ({
|
processRouteCode: route.query.processRouteCode || '',
|
productName: route.query.productName || '',
|
model: route.query.model || '',
|
bomId: route.query.bomId || ''
|
}))
|
|
const demoSteps = [
|
{ title: '工序概览', description: '查看全部工序' },
|
{ title: '工序顺序', description: '了解流程' },
|
{ title: '工序详情', description: '查看参数' },
|
{ title: '进度模拟', description: '生产流程' },
|
{ title: '完成状态', description: '生产完成' }
|
]
|
|
const demoExplanations = [
|
{
|
title: '第1步:工序概览',
|
content: '工艺路线展示了产品从原材料到成品需要经过的所有工序。每个节点代表一道独立的加工步骤。'
|
},
|
{
|
title: '第2步:工序顺序',
|
content: '箭头表示工序的执行顺序。产品按照箭头方向依次经过每道工序,直到最终完成。'
|
},
|
{
|
title: '第3步:工序详情',
|
content: '点击工序节点可以查看详细信息,包括产品名称、规格、计费类型、是否需要质检等。'
|
},
|
{
|
title: '第4步:进度模拟',
|
content: '点击"模拟进度"可以观看产品在各工序间流转的过程。绿色表示已完成,黄色表示当前工序。'
|
},
|
{
|
title: '第5步:生产完成',
|
content: '当所有工序都完成后,产品就生产出来了。实际生产中,每道工序可能会有更详细的参数和操作要求。'
|
}
|
]
|
|
const fetchProcessList = async () => {
|
if (!routeId.value) return
|
loading.value = true
|
try {
|
const res = await findProcessRouteItemList({ routeId: routeId.value })
|
processList.value = res?.data || []
|
// 按序号排序
|
processList.value.sort((a, b) => (a.dragSort || 0) - (b.dragSort || 0))
|
} catch (error) {
|
console.error('获取工艺路线数据失败:', error)
|
} finally {
|
loading.value = false
|
}
|
}
|
|
const goBack = () => {
|
router.push('/productionManagement/teachingDemo')
|
}
|
|
const handleProcessClick = (process) => {
|
activeProcess.value = process
|
activeProcessId.value = process.id
|
}
|
|
const getProcessIndex = (process) => {
|
return processList.value.findIndex(p => p.id === process.id)
|
}
|
|
const getProcessProgress = (process) => {
|
if (completedProcesses.value.includes(process.id)) {
|
return 100
|
}
|
if (currentProcessId.value === process.id) {
|
return 50
|
}
|
return 0
|
}
|
|
const handlePlay = () => {
|
isAnimating.value = true
|
runAnimation()
|
}
|
|
const handlePause = () => {
|
isAnimating.value = false
|
if (playTimer.value) {
|
clearTimeout(playTimer.value)
|
}
|
}
|
|
const handleReset = () => {
|
isAnimating.value = false
|
activeProcessId.value = null
|
activeProcess.value = null
|
currentProcessId.value = null
|
completedProcesses.value = []
|
showProgress.value = false
|
if (playTimer.value) {
|
clearTimeout(playTimer.value)
|
}
|
if (progressTimer.value) {
|
clearInterval(progressTimer.value)
|
}
|
}
|
|
const handleStepChange = (step) => {
|
switch (step) {
|
case 0:
|
// 工序概览
|
break
|
case 1:
|
// 工序顺序
|
if (processList.value.length > 0) {
|
activeProcess.value = processList.value[0]
|
activeProcessId.value = processList.value[0].id
|
}
|
break
|
case 2:
|
// 工序详情
|
break
|
case 3:
|
// 进度模拟
|
simulateProgress()
|
break
|
case 4:
|
// 完成状态
|
completedProcesses.value = processList.value.map(p => p.id)
|
currentProcessId.value = null
|
break
|
}
|
}
|
|
const handleSpeedChange = (speed) => {
|
animationSpeed.value = speed
|
}
|
|
const simulateProgress = () => {
|
showProgress.value = true
|
completedProcesses.value = []
|
currentProcessId.value = null
|
|
let index = 0
|
const interval = 1500 / animationSpeed.value
|
|
progressTimer.value = setInterval(() => {
|
if (index >= processList.value.length) {
|
clearInterval(progressTimer.value)
|
currentProcessId.value = null
|
isAnimating.value = false
|
controlsRef.value?.setIsPlaying(false)
|
return
|
}
|
|
if (index > 0) {
|
completedProcesses.value.push(processList.value[index - 1].id)
|
}
|
|
currentProcessId.value = processList.value[index].id
|
activeProcess.value = processList.value[index]
|
activeProcessId.value = processList.value[index].id
|
|
index++
|
}, interval)
|
}
|
|
const resetProgress = () => {
|
if (progressTimer.value) {
|
clearInterval(progressTimer.value)
|
}
|
completedProcesses.value = []
|
currentProcessId.value = null
|
showProgress.value = false
|
}
|
|
const runAnimation = () => {
|
if (!isAnimating.value) return
|
|
const totalSteps = demoSteps.length
|
let currentStep = controlsRef.value?.currentStep || 0
|
|
const animate = () => {
|
if (!isAnimating.value) return
|
if (currentStep < totalSteps) {
|
controlsRef.value?.setStep(currentStep)
|
handleStepChange(currentStep)
|
currentStep++
|
playTimer.value = setTimeout(animate, 4000 / animationSpeed.value)
|
} else {
|
isAnimating.value = false
|
controlsRef.value?.setIsPlaying(false)
|
}
|
}
|
|
animate()
|
}
|
|
onMounted(() => {
|
fetchProcessList()
|
})
|
|
onUnmounted(() => {
|
if (playTimer.value) {
|
clearTimeout(playTimer.value)
|
}
|
if (progressTimer.value) {
|
clearInterval(progressTimer.value)
|
}
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.process-route-demo {
|
.demo-container {
|
background: #f5f7fa;
|
padding: 20px;
|
border-radius: 12px;
|
min-height: 400px;
|
|
.route-visualization {
|
display: flex;
|
gap: 20px;
|
|
.process-flow-container {
|
flex: 1;
|
background: #fff;
|
padding: 30px;
|
border-radius: 12px;
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
.flow-wrapper {
|
display: flex;
|
align-items: center;
|
gap: 0;
|
padding: 20px 0;
|
overflow-x: auto;
|
min-height: 200px;
|
}
|
|
.flow-start-end {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-top: 30px;
|
padding-top: 20px;
|
border-top: 1px dashed #e4e7ed;
|
|
.flow-node {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 8px;
|
padding: 16px 24px;
|
border-radius: 12px;
|
|
&.start {
|
background: linear-gradient(135deg, #f0f9eb 0%, #e1f3d8 100%);
|
border: 2px solid #67c23a;
|
|
.el-icon {
|
color: #67c23a;
|
font-size: 24px;
|
}
|
|
span {
|
color: #67c23a;
|
font-weight: 600;
|
}
|
}
|
|
&.end {
|
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
border: 2px solid #409eff;
|
|
.el-icon {
|
color: #409eff;
|
font-size: 24px;
|
}
|
|
span {
|
color: #409eff;
|
font-weight: 600;
|
}
|
}
|
}
|
|
.flow-connector {
|
margin: 0 20px;
|
}
|
}
|
}
|
|
.visualization-sidebar {
|
width: 300px;
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
|
.info-card,
|
.progress-card,
|
.legend-card {
|
.empty-info {
|
color: #909399;
|
text-align: center;
|
padding: 20px;
|
}
|
}
|
|
.progress-card {
|
.progress-info {
|
margin-bottom: 16px;
|
|
.progress-stat {
|
display: flex;
|
justify-content: space-between;
|
margin-bottom: 8px;
|
|
.stat-label {
|
color: #666;
|
font-size: 14px;
|
}
|
|
.stat-value {
|
color: #409eff;
|
font-weight: 600;
|
}
|
}
|
}
|
|
.progress-actions {
|
display: flex;
|
gap: 8px;
|
}
|
}
|
|
.legend-card {
|
.legend-item {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 8px;
|
|
.legend-icon {
|
width: 20px;
|
height: 20px;
|
border-radius: 4px;
|
border: 2px solid;
|
|
&.default {
|
background: #fff;
|
border-color: #e4e7ed;
|
}
|
|
&.current {
|
background: linear-gradient(135deg, #fdf6ec 0%, #fff 100%);
|
border-color: #e6a23c;
|
box-shadow: 0 0 8px rgba(230, 162, 60, 0.4);
|
}
|
|
&.completed {
|
background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%);
|
border-color: #67c23a;
|
}
|
|
&.active {
|
background: #fff;
|
border-color: #409eff;
|
box-shadow: 0 0 12px rgba(64, 158, 255, 0.5);
|
}
|
}
|
|
.legend-text {
|
font-size: 12px;
|
color: #666;
|
}
|
}
|
}
|
}
|
}
|
}
|
}
|
</style>
|