《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>

 

posted @ 2025-07-17 10:22  爱听书的程序猿  阅读(108)  评论(0)    收藏  举报