项目背景:需要实现道路情况鱼骨图,根据上下行道路分别显示对应的道路情况和沿路设施状况,箭头根据所示方向平滑移动

1.封装组件,创建FishboneDiagram.vue文件

<template>
  <div class="fishedOneBox flex items-center">
    <div @click="scrollContent('left')" class="left cursor-pointer leftButtonBox pl-20px box-border w-60px p-15px box-border h-165px flex justify-center items-center">
      <button class="text-(16px #7BA9FA) font-bold leading-20px">哈密方向</button>
    </div>
    <div class="content" ref="scrollContainers">
      <div class="upList mb-6px">
        <!-- 上行公路 -->
        <div class="road">
          <div class="upRoadItem relative" v-for="(item, index) in upList" :key="index"
               :style="{ width: sectionWidth, background: item.status === 1 ? '#4ACF50' : '#D6D6D6', borderLeft: getLeftBorder(index, upList), borderRight: getRightBorder(index, upList) }">
            <img class="arrows arrow-lefts" src="/@/assets/images/iconLook/left_arrow.png" alt="">
            <img class="arrow arrow-left" src="/@/assets/images/iconLook/left_arrow.png" alt="">
            <!-- 桩号 -->
            <div class="text-(12px #999999) absolute right--30px top--18px">{{ item.id }}</div>
            <!-- 路侧设备(服务区,互通) -->
            <div class="absolute top--51px" v-if="item.staketype !== 4 && item.staketype !== 5">
              <div class="flex rounded-16px pl-7px pr-7px pt-3px pb-3px" :class="item.staketype === 2 ? 'bg-#E4FEE0' : 'bg-#E4EEFF'">
                <img class="w-20px h-20px" :src="getImageUrl(item.staketype === 1 ? 'tollStation':item.staketype === 2 ? 'service':item.staketype === 3 ? 'interFlow' : '' , 'fishBoneIcon')" alt="">
                <span class="text-(14px #333333) ml-2px">{{ item.stakename }}</span>
              </div>

              <div class="line w-full flex justify-center">
                <img class="w-5px h-24px" :src="getImageUrl(`${item.staketype === 2 ? 'line_up_green' : 'line_up_blue'}`, 'fishBoneIcon')" alt="">
              </div>
            </div>
          </div>
        </div>



      </div>
      <div class="downList">
        <!-- 下行公路 -->
        <div class="road">
          <div class="upRoadItem relative" v-for="(item, index) in downList" :key="index"
            :style="{ width: downSectionWidth, background: item.status === 1 ? '#4ACF50' : '#D6D6D6', borderLeft: getLeftBorder(index, downList), borderRight: getRightBorder(index, downList) }">
            <img class="downArrow arrow-right" src="/@/assets/images/iconLook/right_arrow.png" alt="">
            <img class="downArrows arrow-rights" src="/@/assets/images/iconLook/right_arrow.png" alt="">
            <!-- 桩号 -->
            <div class="text-(12px #999999) absolute right--30px bottom--20px">{{ item.id }}</div>
            <!-- 下行路侧设备(服务区,互通) -->
            <div class="absolute bottom--51px" v-if="item.staketype !== 4 && item.staketype !== 5">
              <div class="line w-full flex justify-center">
                <img class="w-5px h-24px" :src="getImageUrl(`${item.staketype === 2 ? 'line_down_green' : 'line_down_blue'}`, 'fishBoneIcon')" alt="">
              </div>
              <div class="flex rounded-16px pl-7px pr-7px pt-3px pb-3px" :class="item.staketype === 2 ? 'bg-#E4FEE0' : 'bg-#E4EEFF'">
                <img class="w-20px h-20px" :src="getImageUrl(item.staketype === 1 ? 'tollStation':item.staketype === 2 ? 'service':item.staketype === 3 ? 'interFlow' : '' , 'fishBoneIcon')" alt="">
                <span class="text-(14px #333333) ml-2px">{{ item.stakename }}</span>
              </div>
            </div>
          </div>
        </div>

      </div>
    </div>
    <div @click="scrollContent('right')" class="right cursor-pointer rightButtonBox pr-20px box-border w-60px h-165px flex justify-center p-15px box-border items-center">
      <button class="text-(16px #7BA9FA) font-bold leading-20px" >星星峡方向</button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref, computed } from "vue"
import { getImageUrl } from '/@/utils'
const myTimeout = ref()
const scrollTimeout = ref()
const scrollContainers = ref()
const maxScroll = ref()

const props = defineProps({
  value: {
    type: Object,
    default: {}
  }
});

// 上行
const upList = computed(() => {
  return props.value.upList
})

// 下行
const downList = computed(() => {
  return props.value.downList
})
// 根据路段设施数量确定区间宽度
const sectionWidth = computed(() => {
  const widthShow = scrollContainers.value?.offsetWidth >= upList.value.length * 120
  let sectionWidth: any = "120px"
  if(upList.value.length >= downList.value.length) {
    if (widthShow) {
      sectionWidth = (scrollContainers.value?.offsetWidth / upList.value.length) + 'px'
    } else {
      sectionWidth = "120px"
    }
  } else {
    const width = downList.value.length * 120
    sectionWidth = (width / upList.value.length) + 'px'
  }
 return sectionWidth
})
const downSectionWidth = computed(() => {
  const downWidthShow = scrollContainers.value?.offsetWidth >= downList.value.length * 120
  let downSectionWidth: any = "120px"
  if(downList.value.length >= upList.value.length) {
    if (downWidthShow) {
      downSectionWidth = (scrollContainers.value?.offsetWidth / downList.value.length) + 'px'
    } else {
      downSectionWidth = "120px"
    }
  } else {
    const width = upList.value.length * 120
    downSectionWidth = (width / downList.value.length) + 'px'
  }
  return downSectionWidth
})
// 边框逻辑
const getLeftBorder = (index: number, list: any[]) => {
  if (index === 0) return ''; // 第一个元素始终显示左边框
  const prevItem = list[index - 1];
  const current = list[index];
  if (prevItem.end === 1 && current.start === 1) {
    return ''; // 当前元素的左边框不显示
  } else if (current.start === 1) {
    return '2px solid #ffffff';
  }
  return '';
};

const getRightBorder = (index: number, list: any[]) => {
  if (index === list.length - 1) return ''; // 最后一个元素始终显示右边框
  const current = list[index];
  const nextItem = list[index + 1];
  if (current.end === 1 && nextItem.start === 1) {
    return ''; // 当前元素的右边框不显示
  } else if (current.end === 1) {
    return '2px solid #ffffff';
  }
  return '';
};
const scrollContent = (direction: any) => {
  // 清除之前的防抖计时器(如果存在)
  clearTimeout(scrollTimeout.value);
  scrollTimeout.value = setTimeout(() => {
    clearTimeout(myTimeout.value)
    const scrollContainer = scrollContainers.value;
    const scrollStep = 40; // 每次滚动的步长
    const scrollInterval = setInterval(() => {
      if (direction === 'left') {
        if (scrollContainer?.scrollLeft > 0) {
          scrollContainer.scrollLeft -= scrollStep;
        } else {
          clearInterval(scrollInterval);
        }
      } else {
        if (scrollContainer?.scrollLeft < maxScroll.value) {
          scrollContainer.scrollLeft += scrollStep;
        } else {
          clearInterval(scrollInterval);
        }
      }
    }, 20); // 滚动间隔时间,数值越小滚动越快
    myTimeout.value = setTimeout(() => {
      clearInterval(scrollInterval)
    }, 200)
  }, 200)
}
const updateScrollRange = () => {
  const scrollContainer = scrollContainers.value;
  // maxScroll.value = scrollContainer?.scrollWidth - scrollContainer?.clientWidth;
  maxScroll.value = scrollContainer?.scrollWidth
}
onMounted(() => {
  updateScrollRange()
})
</script>

<style lang="scss" scoped>
.fishedOneBox {
  width: 100%;
  height: 200px;
}
.leftButtonBox{
  background: url('/@/assets/images/leftButtonBox.png') no-repeat left center;
  background-size: 60% 100%;
}
.rightButtonBox{
  background: url('/@/assets/images/rightButtonBox.png') no-repeat right center;
  background-size: 60% 100%;
}
.content {
  display: flex;
  height: 186px;
  flex-direction: column;
  justify-content: center;
  width: calc(100% - 120px);
  overflow-x: scroll;
  overflow-y: hidden;
  .upList,
  .downList {
    display: flex;
    align-items: center;
    justify-content: flex-start; /* 确保子项目从左到右排列 */
    position: relative;
    .road {
      display: flex;
      background: #D6D6D6;
      .upRoadItem {
        background: #4ACF50;
        height: 30px;
        display: flex;
        align-items: center;
        //border-left: 1px solid #ffffff;
        //border-right: 1px solid #ffffff;

        .arrow {
          display: inline-block;
          position: relative;
          pointer-events: none;
        }
        .arrows {
           display: inline-block;
           position: relative;
           pointer-events: none;
         }

        .downArrow {
          display: inline-block;
          position: relative;
          pointer-events: none;
        }
        .downArrows {
          display: inline-block;
          position: relative;
          pointer-events: none;
        }
      }
    }

  }
}
.arrow-right {
  animation: moveRight 2.5s infinite linear forwards;
}
.arrow-rights {
  animation: moveRights 2.5s infinite linear forwards;
}
.arrow-left {
  animation: moveLeft 2.5s infinite linear forwards;
}
.arrow-lefts {
  animation: moveLefts 2.5s infinite linear forwards;
}
@keyframes moveRight {
  0% {
    left: -55%;
  }
  100% {
    left: 45%;
  }
}
@keyframes moveRights {
  0% {
    left: -5%;
  }
  100% {
    left: 95%;
  }
}
@keyframes moveLeft {
  0% {
    left: 40%;
  }
  100% {
    left: -60%;
  }
}
@keyframes moveLefts {
  0% {
    left: 95%;
  }
  100% {
    left: -5%;
  }
}
</style>

2. 引用组件

<template>
<Fishbone :value="dataInfoList" />
</template>

<script setup lang="ts">
import Fishbone from "./Fishbone.vue"

// 鱼骨图上行下行数据
const dataInfoList: any = ref({
  upList: [],
  downList: []
})
</script>

小结:

1. 根据上行下行数据画出上行下行路段

2. 再根据每段路中是否存在一些设备设施,通过v-if渲染出来

3. 道路状况也可以根据当前此段道路的拥堵情况渲染不同的颜色

4. 路段动画根据图标方向对图标做left或者right的平移动画

根据自身业务情况适当修改,鱼骨图可以根据业务方向继续延伸,希望大家能有一点思路,我也是从毫无头绪慢慢画出来,又get到一个新的知识点,希望大家多多指正,一起加油!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐