面包屑导航的二次封装ing

21 阅读1分钟

(一)JS部分

import { computed } from "vue";
interface BreadcrumbItem {
  label: string;
  path?: string;
  disabled?: boolean;

  [key: string]: any;
}

interface EllipsisItem {
  type: "ellipsis";
  children: BreadcrumbItem[];
}

type DisplayItem = BreadcrumbItem | EllipsisItem;

const props = withDefaults(
  defineProps<{
    items: BreadcrumbItem[];
    ellipsisIndex?: number;
    afterCount?: number;
    itemMaxWidth?: number | string;
    separator?: string;
  }>(),
  {
    items: () => [],
    ellipsisIndex: 1,
    afterCount: 2,
    itemMaxWidth: 160,
    separator: "/",
  }
);

const emit = defineEmits<{
  (e: "click", item: BreadcrumbItem): void;
}>();

const itemStyle = computed(() => {
  const maxWidth = typeof props.itemMaxWidth === "number" ? `${props.itemMaxWidth}px` : props.itemMaxWidth;

  return {
    maxWidth,
  };
});

function isEllipsisItem(item: DisplayItem): item is EllipsisItem {
  return (item as EllipsisItem).type === "ellipsis";
}

function isCurrent(item: BreadcrumbItem) {
  return props.items[props.items.length - 1] === item;
}

function handleClick(item: BreadcrumbItem) {
  if (item.disabled || isCurrent(item)) return;
  emit("click", item);
}

function handleCommand(item: BreadcrumbItem) {
  handleClick(item);
}

function getItemKey(item: BreadcrumbItem, index: number) {
  return item.path || item.label || index;
}

function getRenderKey(item: DisplayItem, index: number) {
  if (isEllipsisItem(item)) return `ellipsis-${index}`;
  return getItemKey(item, index);
}

const displayItems = computed<DisplayItem[]>(() => {
  const list = props.items || [];
  const total = list.length;

  if (!total) return [];

  const safeEllipsisIndex = Math.max(0, props.ellipsisIndex);
  const safeAfterCount = Math.max(1, props.afterCount);

  const frontCount = Math.min(safeEllipsisIndex, total);
  const tailCount = Math.min(safeAfterCount, total);

  const hiddenStart = frontCount;
  const hiddenEnd = total - tailCount;

  if (hiddenStart >= hiddenEnd) {
    return list;
  }

  const hiddenList = list.slice(hiddenStart, hiddenEnd);

  // 折叠区只有一个,直接全展示
  if (hiddenList.length <= 1) {
    return list;
  }

  const frontList = list.slice(0, hiddenStart);
  const tailList = list.slice(hiddenEnd);

  return [
    ...frontList,
    {
      type: "ellipsis",
      children: hiddenList,
    },
    ...tailList,
  ];
});

(2) html 和 css 部分

<template>
  <el-breadcrumb class="smart-breadcrumb" :separator="separator">
    <template v-for="(item, index) in displayItems" :key="getRenderKey(item, index)">
      <el-breadcrumb-item v-if="isEllipsisItem(item)">
        <el-dropdown trigger="hover" placement="bottom-start" @command="handleCommand">
          <span class="smart-breadcrumb__link smart-breadcrumb__ellipsis">...</span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item
                v-for="(hiddenItem, hiddenIndex) in item.children"
                :key="getItemKey(hiddenItem, hiddenIndex)"
                :command="hiddenItem"
                :disabled="isCurrent(hiddenItem) || hiddenItem.disabled"
              >
                {{ hiddenItem.label }}
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-breadcrumb-item>
      <!-- 普通项 -->
      <el-breadcrumb-item v-else>
        <span
          class="smart-breadcrumb__label"
          :class="{
            'smart-breadcrumb__link': !isCurrent(item) && !item.disabled,
            'smart-breadcrumb__current': isCurrent(item),
            'smart-breadcrumb__disabled': item.disabled,
          }"
          :style="itemStyle"
          :title="item.label"
          @click="handleClick(item)"
        >
          {{ item.label }}
        </span>
      </el-breadcrumb-item>
    </template>
  </el-breadcrumb>
</template>
<style scoped lang="scss">
.smart-breadcrumb {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  :deep(.el-breadcrumb__inner) {
    display: inline-flex;
    align-items: center;
    min-width: 0;
    max-width: 100%;
    font-weight: 400;
  }

  :deep(.el-breadcrumb__separator) {
    margin: 0 8px;
    color: #c0c4cc;
  }
}

.smart-breadcrumb__label,
.smart-breadcrumb__ellipsis {
  display: inline-block;
  min-width: 0;
  vertical-align: middle;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.smart-breadcrumb__label {
  color: #606266;
  transition: color 0.2s;
}

.smart-breadcrumb__link {
  cursor: pointer;
  color: #606266;
}

.smart-breadcrumb__link:hover {
  color: #409eff;
}

.smart-breadcrumb__current {
  color: #303133;
  font-weight: 600;
  cursor: default;
}

.smart-breadcrumb__disabled {
  color: #c0c4cc;
  cursor: not-allowed;
}

.smart-breadcrumb__ellipsis {
  max-width: none !important;
}
</style>