1. 最简单的手写方式(使用 html + css)
// parent component
<template>
<div class="wrapper">
<span v-for="(item, index) in innerAndOuterflow" :key="item.id + index">
<ProcessNode
:title="item.statusName"
:bgc="'#67c23a'"
:arrow-flag="index !== innerAndOuterflow.length - 1 ? true : false"
:arrow-direction="getArrowDirection(index, innerAndOuterflow)"
/>
</span>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch } from 'vue'
import { IFlowChart, IInnerAndOuterFlowChart } from '@/axios/api/types'
import ProcessNode from './ProcessNode.vue'
interface IProps {
flowData: IInnerAndOuterFlowChart
}
const props = defineProps<IProps>()
const innerAndOuterflow = ref<IFlowChart[]>([])
const getArrowDirection = (index: number, totalArr: IFlowChart[]) => {
let division
division = Math.floor(index / 6)
if (
// such as 5, 6
((index + 1) % 6 === 0 && Math.floor((index + 1) / 6) % 2 === 1) ||
(index % 6 === 0 && division % 2 === 1 && totalArr.length >= (division + 1) * 6)
) {
return 'down'
}
return division % 2 === 0 ? 'right' : 'left'
}
// the flow chart is sorted as 'greedy-snake', we need to change our array
const transformToGreedySnakeShape = (array: IFlowChart[]) => {
const result: IFlowChart[] = []
const groupMap = new Map()
array.map((item, index) => {
const mod = Math.floor(index / 6)
if (!groupMap.get(mod)) {
groupMap.set(mod, [item])
} else {
const previousSameModIndex = groupMap.get(mod)
groupMap.set(mod, [...previousSameModIndex, item])
}
})
for (const entry of groupMap.entries()) {
const [key, value] = entry
if (key % 2 === 0) {
result.push(...value)
} else {
const reversedArray = value.toReversed()
result.push(...reversedArray)
}
}
return result
}
watch(
() => props.flowData,
(newValue) => {
if (newValue.auditFlows.length) {
innerAndOuterflow.value = []
const reversedAudits = newValue.auditFlows.toReversed()
const reversedAgents = newValue.agentFlows.toReversed()
const waitingToTransformedArray = [...reversedAudits, ...reversedAgents]
innerAndOuterflow.value = transformToGreedySnakeShape(waitingToTransformedArray)
}
},
{ deep: true }
)
</script>
<style lang="scss" scoped>
.wrapper {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-row-gap: 120px;
padding-top: 20px;
padding-left: 20px;
}
</style>
// child component -> ProcessNode.vue
<template>
<div class="process-node" :style="{ width: boxWidth, height: boxWidth, backgroundColor: bgc }">
<div class="inner-content">
<span class="title">{{ title }}</span>
</div>
<div
v-if="arrowFlag"
:class="{
'line-left': arrowDirection === 'left',
'line-right': arrowDirection === 'right',
'line-down': arrowDirection === 'down'
}"
></div>
</div>
</template>
<script setup lang="ts">
import { defineProps, withDefaults } from 'vue'
interface IProps {
title: string
bgc: string
boxWidth?: string
arrowFlag: boolean
arrowDirection?: string
}
withDefaults(defineProps<IProps>(), {
boxWidth: '80px',
arrowDirection: 'right'
})
</script>
<style lang="scss" scoped>
.process-node {
display: flex;
justify-content: center;
align-items: center;
transform: rotate(45deg);
position: relative;
}
.inner-content {
text-align: center;
transform: rotate(-45deg);
}
.line-left {
position: absolute;
right: 56px;
bottom: -55px;
width: 159px;
border-top: 1px solid #8c8c8c;
transform: rotate(-45deg);
}
.line-left::after {
content: '<';
position: absolute;
top: -11px;
left: 0;
color: #8c8c8c;
}
.line-right {
position: absolute;
left: 56px;
top: -55px;
width: 159px;
border-top: 1px solid #8c8c8c;
transform: rotate(-45deg);
}
.line-right::after {
content: '>';
position: absolute;
top: -11px;
right: 0;
color: #8c8c8c;
}
.line-down {
position: absolute;
bottom: -30px;
left: 66px;
width: 88px;
border-top: 1px solid #8c8c8c;
transform: rotate(-135deg);
}
.line-down::after {
content: '<';
position: absolute;
top: -11px;
left: -4px;
color: #8c8c8c;
}
</style>
PS: 这种写法有个弊端,就是当宽度改变的时候需要重新修改线条的位置。