Bootstrap

弧形菜单效果制作(vue3) 2021-12-24

目的

绘制弧线,菜单沿弧线分布,根据公式计算位置
公式:二次贝塞尔曲线

效果图

在这里插入图片描述

源码

弧线组件

<template>
  <div class="svg">
    <svg :width="s.width.value" :height="s.height.value">
      <defs>
        <linearGradient id="color">
          <stop
            offset="50%"
            :style="`stop-color: ${s.color.value}; stop-opacity: 0.1`"
          />
          <stop
            offset="100%"
            :style="`stop-color: ${s.color.value}; stop-opacity: 0.8`"
          />
        </linearGradient>
      </defs>
      <path
        :d="`
        M ${f(s.p1)} 
        Q ${f(s.cp1)} ${f(s.p2)}
        T ${f(s.p2)} 
        Q ${f(s.cp2)} ${f(s.p1)} Z`"
        fill="url(#color)"
      ></path>
    </svg>
  </div>
</template>

<script>
import _ from "lodash";
import { computed, reactive, ref } from "vue";

/**
 * 二次贝塞尔曲线
 * ————————————————
 * 版权声明:本文为CSDN博主「愚舜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
 * 原文链接:https://blog.csdn.net/first_shun/article/details/107346329
 */
function twoOrderBezier(t, p1, cp, p2) {
  //参数分别是t,起始点,控制点和终点
  var [x1, y1] = p1,
    [cx, cy] = cp,
    [x2, y2] = p2;
  var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * cx + t * t * x2,
    y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * cy + t * t * y2;
  return [x, y];
}

export default {
  props: ["width", "height", "color", "borderWidth"],
  setup(props) {
    let s = {
      width: computed(() => {
        if (props.width) return props.width;
        if (props.height) return props.height / 10;
        return 20;
      }),
      height: computed(() => {
        if (props.height) return props.height;
        if (props.width) return props.width * 10;
        return 200;
      }),
      color: computed(() => props.color || "blue"),
      borderWidth: computed(() => props.borderWidth || (s.width.value / 10)),
      p1: ref([0, 0]),
      cp1: computed(() => [s.width.value * 2, s.height.value / 2]),
      cp2: computed(() => [s.width.value * 2 - s.borderWidth.value, s.height.value / 2]),
      cp3: computed(() => [s.width.value * 2 - s.borderWidth.value / 2, s.height.value / 2]),
      p2: computed(() => [0, s.height.value]),
    };
    return {
      s,
      getCoordinate: (t) => {
        return twoOrderBezier(t, s.p1.value, s.cp3.value, s.p2.value);
      },
      f: (arr) => {
        return _(arr.value).join(",");
      },
    };
  },
};
</script>

<style>
.svg {
  display: inline-block;
}
</style>

菜单打印组件

<template>
  <div class="menu-box" :class="{ 'is-left': isLeft }" ref="box">
    <my-svg class="menu-bg" ref="bg" v-bind="bgStyle"></my-svg>
    <nav class="menu">
      <li
        class="menu-item"
        v-for="item in menuList"
        :key="item.id"
        :style="{
          top: `${item.y}px`,
          [isLeft ? 'left' : 'right']: `${item.x}px`,
        }"
      >
        <div class="menu-item-title">
          {{ item.name }}
          <div
            class="menu-item-title-dot"
            :style="{ background: fontColor }"
          ></div>
        </div>
      </li>
    </nav>
  </div>
</template>

<script setup>
import { computed, defineProps, ref, onMounted } from "vue";
// import mySvg from "/@/components/svg.vue";
import mySvg from "./svg.vue";
import _ from "lodash";

const box = ref(null);
const bg = ref(null);
const isNotInited = ref(true);

const props = defineProps({
  list: {
    type: Array,
    default() {
      return [];
    },
  },
  isLeft: {
    type: Boolean,
    default: false,
  },
});

const fontColor = computed(() => {
  if (isNotInited.value) return "blue";
  let color = box.value.computedStyleMap().get("color").toString();
  return color;
});

const menuList = computed(() => {
  if (isNotInited.value) return [];
  const base = 1 / (props.list.length + 1);
  return props.list.map((x, i) => {
    let bgW = bg.value.s.width.value;
    let coordinate = bg.value.getCoordinate(base * (i + 1));
    return { ...x, x: bgW - coordinate[0], y: coordinate[1] };
  });
});

const bgStyle = computed(() => {
  let s = { color: fontColor.value };
  if (!isNotInited.value) {
    const boxH = box.value.offsetHeight;
    _.assign(s, { height: boxH });
  }
  return s;
});

onMounted(() => {
  isNotInited.value = false;
});
</script>

<style scoped>
.menu-box {
  display: flex;
  position: relative;
  height: 500px;
  width: 500px;
}
.menu {
  flex: 1;
}
.menu-bg {
  position: absolute;
  top: 0;
  right: 0px;
  height: 100%;
}
.menu-box.is-left .menu-bg {
  left: 0px;
  right: initial;
  transform: scale(-1, 1);
}
.menu-item {
  position: absolute;
  list-style: none;
  transform: translateY(-50%);
  cursor: pointer;
}
.menu-item-title-dot {
  display: inline-block;
  width: 1em;
  height: 1em;
  padding: 0.2em;
  border-radius: 50%;
  margin-left: 1em;
  margin-right: -0.5em;
  vertical-align: middle;
  float: right;
}
.menu-box.is-left .menu-item-title-dot{
  float: left;
  margin-left: -0.5em;
  margin-right: 1em;
}
.menu-item-title:hover {
  color: orange;
}
.menu-item-title:hover .menu-item-title-dot {
  background: radial-gradient(
    orange 40%,
    transparent 45%,
    transparent 50%,
    orange 60%,
    orange 100%
  ) !important;
}
</style>

结语

不知道怎么封装更好,谁要是能给封装好,记得给贴个链接,如果有现成的组件,也请推荐一下,没有头绪,心不够静,😔
用的是vue3,如果不能用,可以参照下思路,能帮到谁那我就太高兴了。

更新2022-10-25

menu组件添加isLeft属性
在这里插入图片描述

;