<template>
|
<div
|
class="process-node"
|
:class="{
|
'is-active': active,
|
'is-completed': completed,
|
'is-current': current,
|
'is-animating': animating
|
}"
|
:style="{ '--delay': delay + 'ms', '--speed': animationSpeed }"
|
>
|
<div class="node-badge">
|
<span class="badge-number">{{ index + 1 }}</span>
|
</div>
|
|
<div class="node-card" @click="handleClick">
|
<div class="node-header">
|
<div class="node-icon">
|
<el-icon><SetUp /></el-icon>
|
</div>
|
<div class="node-title">{{ process.technologyOperationName || process.operationName || '未命名工序' }}</div>
|
</div>
|
|
<div class="node-body">
|
<div class="node-info-item" v-if="process.productName">
|
<span class="info-label">产品:</span>
|
<span class="info-value">{{ process.productName }}</span>
|
</div>
|
<div class="node-info-item" v-if="process.model">
|
<span class="info-label">规格:</span>
|
<span class="info-value">{{ process.model }}</span>
|
</div>
|
<div class="node-tags">
|
<el-tag :type="process.type === 1 ? 'primary' : 'success'" size="small">
|
{{ process.type === 1 ? '计件' : '计时' }}
|
</el-tag>
|
<el-tag v-if="process.isQuality" type="warning" size="small">质检</el-tag>
|
<el-tag v-if="process.isProduction" type="info" size="small">生产</el-tag>
|
</div>
|
</div>
|
|
<div class="node-progress" v-if="showProgress">
|
<el-progress
|
:percentage="progress"
|
:status="completed ? 'success' : current ? '' : ''"
|
:stroke-width="6"
|
/>
|
</div>
|
</div>
|
|
<div class="node-arrow" v-if="showArrow">
|
<svg width="40" height="24" viewBox="0 0 40 24">
|
<defs>
|
<marker
|
id="arrowhead"
|
markerWidth="10"
|
markerHeight="7"
|
refX="9"
|
refY="3.5"
|
orient="auto"
|
>
|
<polygon points="0 0, 10 3.5, 0 7" fill="#409eff" />
|
</marker>
|
</defs>
|
<line
|
x1="0"
|
y1="12"
|
x2="30"
|
y2="12"
|
stroke="#409eff"
|
stroke-width="2"
|
marker-end="url(#arrowhead)"
|
:class="{ 'is-animating': animating }"
|
/>
|
<circle
|
v-if="animating"
|
r="4"
|
fill="#67c23a"
|
class="flow-dot"
|
>
|
<animate
|
attributeName="cx"
|
values="0;30"
|
:dur="`${1 / animationSpeed}s`"
|
repeatCount="indefinite"
|
/>
|
<animate
|
attributeName="cy"
|
values="12;12"
|
dur="1s"
|
repeatCount="indefinite"
|
/>
|
</circle>
|
</svg>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed } from 'vue'
|
import { SetUp } from '@element-plus/icons-vue'
|
|
const props = defineProps({
|
process: {
|
type: Object,
|
required: true
|
},
|
index: {
|
type: Number,
|
default: 0
|
},
|
delay: {
|
type: Number,
|
default: 0
|
},
|
animationSpeed: {
|
type: Number,
|
default: 1
|
},
|
active: {
|
type: Boolean,
|
default: false
|
},
|
completed: {
|
type: Boolean,
|
default: false
|
},
|
current: {
|
type: Boolean,
|
default: false
|
},
|
animating: {
|
type: Boolean,
|
default: false
|
},
|
showArrow: {
|
type: Boolean,
|
default: true
|
},
|
showProgress: {
|
type: Boolean,
|
default: false
|
},
|
progress: {
|
type: Number,
|
default: 0
|
}
|
})
|
|
const emit = defineEmits(['click'])
|
|
const handleClick = () => {
|
emit('click', props.process)
|
}
|
</script>
|
|
<style scoped lang="scss">
|
.process-node {
|
display: flex;
|
align-items: center;
|
animation: nodeSlideIn calc(var(--delay) * var(--speed)) ease-out both;
|
|
.node-badge {
|
position: absolute;
|
top: -12px;
|
left: 50%;
|
transform: translateX(-50%);
|
z-index: 1;
|
|
.badge-number {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 28px;
|
height: 28px;
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
color: #fff;
|
font-weight: bold;
|
font-size: 14px;
|
border-radius: 50%;
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
|
}
|
}
|
|
.node-card {
|
position: relative;
|
width: 180px;
|
background: #fff;
|
border-radius: 12px;
|
border: 2px solid #e4e7ed;
|
padding: 20px 16px 16px;
|
cursor: pointer;
|
transition: all 0.3s ease;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
&:hover {
|
transform: translateY(-4px);
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
border-color: #409eff;
|
}
|
|
.node-header {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 12px;
|
|
.node-icon {
|
width: 28px;
|
height: 28px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
border-radius: 6px;
|
|
.el-icon {
|
color: #fff;
|
font-size: 16px;
|
}
|
}
|
|
.node-title {
|
font-weight: 600;
|
color: #303133;
|
font-size: 13px;
|
flex: 1;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.node-body {
|
.node-info-item {
|
display: flex;
|
font-size: 12px;
|
margin-bottom: 4px;
|
|
.info-label {
|
color: #909399;
|
margin-right: 4px;
|
}
|
|
.info-value {
|
color: #606266;
|
flex: 1;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
}
|
|
.node-tags {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 4px;
|
margin-top: 8px;
|
}
|
}
|
|
.node-progress {
|
margin-top: 12px;
|
}
|
}
|
|
.node-arrow {
|
margin: 0 8px;
|
|
line {
|
transition: stroke 0.3s;
|
|
&.is-animating {
|
animation: arrowGlow 0.5s ease-in-out infinite alternate;
|
}
|
}
|
|
.flow-dot {
|
filter: drop-shadow(0 0 4px rgba(103, 194, 58, 0.6));
|
}
|
}
|
|
&.is-active {
|
.node-card {
|
border-color: #409eff;
|
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.3);
|
}
|
}
|
|
&.is-completed {
|
.node-card {
|
border-color: #67c23a;
|
background: linear-gradient(135deg, #f0f9eb 0%, #fff 100%);
|
|
.node-badge .badge-number {
|
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
}
|
}
|
}
|
|
&.is-current {
|
.node-card {
|
border-color: #e6a23c;
|
animation: currentPulse 1.5s ease-in-out infinite;
|
}
|
}
|
|
&.is-animating {
|
.node-card {
|
animation: cardPulse calc(1s * var(--speed)) ease-in-out infinite;
|
}
|
}
|
}
|
|
@keyframes nodeSlideIn {
|
from {
|
opacity: 0;
|
transform: translateX(-30px);
|
}
|
to {
|
opacity: 1;
|
transform: translateX(0);
|
}
|
}
|
|
@keyframes currentPulse {
|
0%, 100% {
|
box-shadow: 0 4px 16px rgba(230, 162, 60, 0.3);
|
}
|
50% {
|
box-shadow: 0 4px 24px rgba(230, 162, 60, 0.5);
|
}
|
}
|
|
@keyframes cardPulse {
|
0%, 100% {
|
transform: scale(1);
|
}
|
50% {
|
transform: scale(1.02);
|
}
|
}
|
|
@keyframes arrowGlow {
|
from {
|
stroke: #409eff;
|
filter: drop-shadow(0 0 2px rgba(64, 158, 255, 0.4));
|
}
|
to {
|
stroke: #66b1ff;
|
filter: drop-shadow(0 0 6px rgba(64, 158, 255, 0.6));
|
}
|
}
|
</style>
|