《vue3-实现蛇形单向流程图》
最终展示效果:

点击node节点,显示其详情:

话不多说,上代码
父组件引用及其传参:
<flowchart ref="flowchartRef" :chartData="hierarchicalArray" :Index="5" />
proxy.$refs.flowchartRef.initWidth(hierarchicalArray.value)
流程图组件:
<template>
<!-- 蛇形流程图-->
<div class="snakeFlowchart" >
<div style="width: 100%; display: flex; position: relative; margin-top: 20px" v-if="experienceData && experienceData.length > 0">
<!--左侧曲线-->
<div :style="`width:${sideWidth}vw ; margin-left: 30px`">
<div>
<span :style="`margin-top:${ItemHeight/2}px ; opacity: 0`" class="headerRadio"></span>
<div v-if="experienceData.length > Index">
<span
v-for="(num, index) in leftRows"
:key="index"
class="hingelisHeard"
:style="`height:${ItemHeight}px ; margin-top:${ItemHeight}px; ;border: ${LineWidth}px ${LineType} ${LineColor};`"
></span>
</div>
</div>
</div>
<!--中间流程项-->
<div style="width: 96%">
<div
style="display: flex"
v-for="(item, index) in experienceData"
:key="index"
:style="{ 'justify-content':(index + 1) % 2 !== 0?'flex-start':'flex-end'}"
>
<!-- 单数行 -->
<div style="display: flex ;justify-content: flex-start;" v-if="(index + 1) % 2 !== 0">
<div
class="timeline"
v-for="(item, i) in DisplayProcessing(experienceData, index + 1)"
:key="i"
:style="(i + 1) % Index !== 0 ? `width:${ItemWidth}vw ; height:${ItemHeight}px` : `width: ${IconWidth}vw;height:${ItemHeight}px`"
>
<el-popover placement="bottom" :width="600" trigger="click">
<template #reference>
<div class="Nodes" @click="onNodeClick(item)" :style="{ backgroundColor: item === selectedNode ? '#cce3dd' : '#FFF' }">
<span v-if="item.keyFlag == 'Y'" class="star-box">
<el-icon class="star-icon"><StarFilled /></el-icon>
</span>
<div class="NodesItem">
<div class="labelContent">
<el-icon v-if="item.checkPreProcess && item.checkPreProcess.length>0" style="color:green"><Grid /></el-icon>
{{ item.processName }}
</div>
</div>
</div>
</template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">{{ item.processName }} </span>
<el-tag
:type="item.enableFlag == 'Y' ? 'success' : 'danger'"
size="small"
style="margin-left:8px;"
>{{ item.enableFlag == 'Y' ? '启用' : '停用' }}</el-tag
>
</div>
</template>
<el-descriptions title="" :column="2" size="small">
<el-descriptions-item label="工序编码 :">
{{ item.processCode }}
</el-descriptions-item>
<el-descriptions-item label="是否关键工序 :">{{
item.keyFlag == 'Y'?'是':'否'
}}</el-descriptions-item>
<el-descriptions-item label="工序类型 :">{{ item.processType == '1'?'报工':'报工&测试' }}</el-descriptions-item>
<el-descriptions-item label="能否外协 :">{{ item.outSourcing == true?'能':'否' }}</el-descriptions-item>
<el-descriptions-item label="查前工序 :">{{ item.checkPreProcessNames }} </el-descriptions-item>
<el-descriptions-item label="备注 :">{{ item.remark }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-popover>
<div
class="border"
v-if="
(i + 1) % Index != 0 &&
i != DisplayProcessing(experienceData, index + 1).length - 1
"
>
<div class="borderTime " :style="`border-bottom:${LineWidth}px ${LineType} ${LineColor}`"></div>
</div>
</div>
</div>
<!-- 双数行 -->
<div style="display: flex ;justify-content: flex-end;" v-else>
<div
class="timeline2"
v-for="(itemDouble, i) in DisplayProcessing(experienceData, index + 1)"
:key="i"
:style="
i + 1 === 1 &&
DisplayProcessing(experienceData, index + 1).length !== 1
? `width: ${IconWidth}vw;height:${ItemHeight}px`
: `width:${ItemWidth}vw;height:${ItemHeight}px`
"
>
<el-popover placement="bottom" :width="600" trigger="click">
<template #reference>
<div class="Nodes" @click="onNodeClick(itemDouble)" :style="{ backgroundColor: itemDouble === selectedNode ? '#cce3dd' : '#FFF' }">
<span v-if="itemDouble.keyFlag == 'Y'" class="star-box">
<el-icon class="star-icon"><StarFilled /></el-icon>
</span>
<div class="NodesItem">
<div class="labelContent">
<el-icon v-if="itemDouble.checkPreProcess && itemDouble.checkPreProcess.length>0" style="color:green"><Grid /></el-icon>
{{ itemDouble.processName }}
</div>
</div>
</div>
</template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span class="title">{{ itemDouble.processName }} </span>
<el-tag
:type="itemDouble.enableFlag == 'Y' ? 'success' : 'danger'"
size="small"
>{{ itemDouble.enableFlag == 'Y' ? '启用' : '停用' }}</el-tag
>
</div>
</template>
<el-descriptions title="" :column="2" size="small">
<el-descriptions-item label="工序编码 :">
{{ itemDouble.processCode }}
</el-descriptions-item>
<el-descriptions-item label="是否关键工序 :">{{
itemDouble.keyFlag == 'Y'?'是':'否'
}}</el-descriptions-item>
<el-descriptions-item label="工序类型 :">{{ itemDouble.processType == '1'?'报工':'报工&测试' }}</el-descriptions-item>
<el-descriptions-item label="能否外协 :">{{ itemDouble.outSourcing == true?'能':'否' }}</el-descriptions-item>
<el-descriptions-item label="查前工序 :">{{ itemDouble.checkPreProcessNames }} </el-descriptions-item>
<el-descriptions-item label="备注 :">{{ itemDouble.remark }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-popover>
<div class="border" v-if="i !== 0">
<div class="borderTime" :style="`border-bottom:${LineWidth}px ${LineType} ${LineColor}`"></div>
</div>
</div>
</div>
</div>
</div>
<!--右侧曲线-->
<div :style="`width:${sideWidth}vw ; margin-right: 30px`">
<div>
<span class="hingelis" :style="`margin-top: ${ItemHeight/2}px;height:${ItemHeight}px; border: ${LineWidth}px ${LineType} ${LineColor};`" v-if="experienceData.length > Index"></span>
<div v-if="experienceData.length > Index * 2">
<span
class="hingelis"
v-for="(num, index) in rightRows"
:key="index"
:style="`height:${ItemHeight}px ; margin-top:${ItemHeight}px; border: ${LineWidth}px ${LineType} ${LineColor};`"
></span>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无数据" />
</div>
</template>
<script setup name="Flowchart">
import { Plus } from '@element-plus/icons-vue'
import { onMounted, ref, watch } from 'vue'
import { nextTick } from 'vue'
const { proxy } = getCurrentInstance()
const props = defineProps({
// 流程数据
chartData: {
type: Array,
default: () => {
return [];
},
},
//每行几条数据
Index: {
type: Number,
default: 5,
},
// 行高
ItemHeight: {
type: String,
default: '80',
},
// 线宽度
LineWidth:{
type: Number,
default: 1
},
// 线样式(实线/虚线)
LineType:{
type: String,
default: 'solid',
},
// 线颜色
LineColor:{
type: String,
default: '#ddd',
},
//已完成流程颜色
DoneItemColor: {
type: String,
default: '#a0a0a5',
},
// 当前流程颜色
CurrentItemColor:{
type: String,
default: '#10bc28',
},
//小球大小
IconWidth:{
type: String,
default: '0.85',
}
})
// 流程数据
const experienceData =ref([])
const leftRows=ref(0)
const rightRows = ref(0)
const leftShow = ref(false)
const rightShow = ref(false)
// 宽度
const ItemWidth =ref(19.4)
// 拐弯位置宽度
const sideWidth =ref('')
/** 初始化宽度 */
function initWidth(data){
experienceData.value = data;
let p = document.querySelector(".snakeFlowchart").parentNode;
// 1VW宽度
let VW = window.innerWidth/100
// 中间流程部分占8成,算出每一段长度
ItemWidth.value= ((Number(p.clientWidth))*0.70/VW)/(props.Index-1)
// console.log(this.ItemWidth)
// 两边拐弯部分共占2成
sideWidth.value = ((Number(p.clientWidth))*0.30/VW)/2
}
/** 计算数据排列 */
function DisplayProcessing(Arg, Num) {
//数据循环处理
let arr = Arg.slice(props.Index * (Num - 1), props.Index * Num);
arr = Num % 2 === 0 ? arr.reverse() : arr;
return arr;
}
const selectedNode = ref(null)
function onNodeClick(itemData) {
selectedNode.value = itemData
}
// 监听 props.chartData 的变化
watch(
() => props.chartData, (newVal) => {
experienceData.value = newVal
// console.log("experienceData===",experienceData.value)
let rows = Math.ceil(newVal.length / props.Index);
leftRows.value = rows === 2 ? 0 : rows % 2 === 0 ? parseInt(rows / 2) - 1 : parseInt(rows / 2);
rightRows.value = rows === 4 ? 1 : rows % 2 === 0 ? parseInt(rows / 2) % 2 === 0 ? parseInt(rows / 2) >= 4
? parseInt(rows / 2) - 1
: parseInt(rows / 2)
: parseInt(rows / 2) - 1
: parseInt(rows / 2) - 1;
leftShow.value = rows % 2 === 0;
rightShow.value = rows === 1 ? false : rows % 2 === 1;
},
{ deep: true }
)
defineExpose({
initWidth
})
</script>
<style scoped lang="scss"> .timeline { width: 21.4vw; display: flex; align-items: center; } .timeline2 { width: 21.4vw;; display: flex; align-items: center; justify-content: flex-end; } .border { width: 100%; justify-content: center; align-items: center; display: flex; .borderTime { width: 100%; } } .Nodes { position: absolute; min-width:90px; padding: 8px; height:auto; background-color: #fff; border-radius: 4px; box-shadow: 0 1px 5px rgba(30, 145, 121, 0.5); border: 1px solid #1e9179; cursor: pointer; .star-box{ z-index:98; position: absolute; top: 0; left: 0; width: 0; height: 0; border-top: 20px solid #027357; border-right: 20px solid transparent; .star-icon{ z-index:99; color: white; position: absolute; top: -20px; left: 0px; font-size: 12px; } } .NodesItem{ justify-content: center; align-items: center; display: flex; width: 100%; .labelContent{ max-width: 140px; position: relative; font-size: 14px; text-align: left; /* 内容从左向右展示 */ word-wrap: break-word; /* 允许长单词或 URL 地址换行 */ overflow-wrap: break-word; /* 同 word-wrap,增加兼容性 */ white-space: normal; /* 允许文本换行 */ } } } .hingelis { content: ""; display: block; width: 100%; border-radius: 0 15px 15px 0px; border-left: 0px !important; } .hingelisHeard { content: ""; display: block; width: 100%; border-radius: 15px 0px 0px 15px; border-right: 0px !important; } .headerRadio { display: block; width: 100%; border-bottom: 1px solid #cccccc; position: relative; } </style>

浙公网安备 33010602011771号