gaoluyang
2026-06-24 712aa51536236d43e87273e4ce45ac5691dffad8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<script setup lang="ts">
import type { MenuSwiperItemProperty, MenuSwiperProperty } from './config';
 
import { ref, watch } from 'vue';
 
import { Carousel, Image } from 'ant-design-vue';
 
/** 菜单导航 */
defineOptions({ name: 'MenuSwiper' });
 
const props = defineProps<{ property: MenuSwiperProperty }>();
 
const TITLE_HEIGHT = 20; // 标题的高度
const ICON_SIZE = 32; // 图标的高度
const SPACE_Y = 16; // 垂直间距:一行上下的间距
 
const pages = ref<MenuSwiperItemProperty[][]>([]); // 分页
const carouselHeight = ref(0); // 轮播图高度
 
const rowHeight = ref(0); // 行高
const columnWidth = ref(''); // 列宽
 
watch(
  () => props.property,
  () => {
    // 计算列宽:每一列的百分比
    columnWidth.value = `${100 * (1 / props.property.column)}%`;
    // 计算行高:图标 + 文字(仅显示图片时为0) + 垂直间距 * 2
    rowHeight.value =
      (props.property.layout === 'iconText'
        ? ICON_SIZE + TITLE_HEIGHT
        : ICON_SIZE) +
      SPACE_Y * 2;
    // 计算轮播的高度:行数 * 行高
    carouselHeight.value = props.property.row * rowHeight.value;
 
    // 每页数量:行数 * 列数
    const pageSize = props.property.row * props.property.column;
    // 清空分页
    pages.value = [];
    // 每一页的菜单
    let pageItems: MenuSwiperItemProperty[] = [];
    for (const item of props.property.list) {
      // 本页满员,新建下一页
      if (pageItems.length === pageSize) {
        pageItems = [];
      }
      // 增加一页
      if (pageItems.length === 0) {
        pages.value.push(pageItems);
      }
      // 本页增加一个
      pageItems.push(item);
    }
  },
  { immediate: true, deep: true },
);
</script>
 
<template>
  <Carousel
    :autoplay="false"
    arrows
    dots
    :style="{ height: `${carouselHeight}px` }"
  >
    <div v-for="(page, pageIndex) in pages" :key="pageIndex">
      <div class="flex flex-row flex-wrap">
        <div
          v-for="(item, index) in page"
          :key="index"
          class="relative flex flex-col items-center justify-center"
          :style="{ width: columnWidth, height: `${rowHeight}px` }"
        >
          <div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
            <span
              v-if="item.badge?.show"
              class="absolute -right-2.5 -top-2.5 z-10 h-5 rounded-[10px] px-1.5 text-center text-xs leading-5"
              :style="{
                color: item.badge.textColor,
                backgroundColor: item.badge.bgColor,
              }"
            >
              {{ item.badge.text }}
            </span>
            <Image
              v-if="item.iconUrl"
              :src="item.iconUrl"
              class="h-full w-full"
              :preview="false"
            />
          </div>
          <span
            v-if="property.layout === 'iconText'"
            class="text-xs"
            :style="{
              color: item.titleColor,
              height: `${TITLE_HEIGHT}px`,
              lineHeight: `${TITLE_HEIGHT}px`,
            }"
          >
            {{ item.title }}
          </span>
        </div>
      </div>
    </div>
  </Carousel>
</template>
<style lang="scss">
// Ant Design Vue Carousel 样式调整
:deep(.ant-carousel .ant-carousel-dots) {
  .ant-carousel-dot {
    width: 6px;
    height: 6px;
    border-radius: 6px;
 
    button {
      width: 6px;
      height: 6px;
      background: hsl(var(--red));
      border-radius: 6px;
    }
  }
 
  .ant-carousel-dot-active button {
    width: 12px;
    background: hsl(var(--red));
  }
}
</style>