232 lines
6.8 KiB
Vue
232 lines
6.8 KiB
Vue
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import { House } from '@lucide/vue'
|
||
import { usePreferencesStore } from '@/preferences'
|
||
import { findMenuItemByKey, findMenuItemByPath, findParentKeysByPath } from '@/router'
|
||
|
||
/** 面包屑单项:有 path 的是可点的父级,没有的是当前页。 */
|
||
interface Crumb {
|
||
title: string
|
||
path?: string
|
||
}
|
||
|
||
/**
|
||
* 面包屑。
|
||
* 数据来自菜单常量:祖先分组链 + 当前页,与侧栏同源,不额外维护。
|
||
*/
|
||
const route = useRoute()
|
||
const prefStore = usePreferencesStore()
|
||
const pref = computed(() => prefStore.preferences)
|
||
|
||
/**
|
||
* 当前路径对应的祖先链:[...分组, 当前页]。
|
||
*
|
||
* 父级用 findParentKeysByPath(返回【完整】祖先 key 链)而不是
|
||
* findParentKeyByPath(只取 [0] = 顶级)—— 后者在多级菜单下会漏掉中间层:
|
||
* 「海运 → 订舱业务 → 订舱单」只会显示成「海运 / 订舱单」。
|
||
*/
|
||
const crumbs = computed<Crumb[]>(() => {
|
||
const list: Crumb[] = []
|
||
|
||
if (pref.value.breadcrumb.showHome) {
|
||
list.push({ title: '首页', path: '/' })
|
||
}
|
||
|
||
// 1) 菜单里的完整祖先链(顶级 → 最近一级分组)
|
||
for (const key of findParentKeysByPath(route.path)) {
|
||
const node = findMenuItemByKey(key)
|
||
if (node) list.push({ title: node.title, path: node.path })
|
||
}
|
||
|
||
const current = findMenuItemByPath(route.path)
|
||
|
||
// 2) 菜单里查不到祖先(页面不在菜单树 / 菜单与路由不同步)时,
|
||
// 用路由层级兜底:route.matched 上带 title 的祖先记录即父级。
|
||
// 当前页不在这里补(由下面统一追加),避免自链接。
|
||
if (!current && list.length === 0) {
|
||
for (const record of route.matched) {
|
||
if (record.path === route.path) break
|
||
const title = record.meta.title as string | undefined
|
||
if (title) list.push({ title, path: record.path })
|
||
}
|
||
}
|
||
|
||
// 3) 当前页:菜单里查得到就用菜单名(业务页);
|
||
// 查不到(403/404/500 等缺省页、详情页)回退到 route.meta.title。
|
||
const currentTitle = current?.title ?? (route.meta.title as string | undefined)
|
||
if (currentTitle) list.push({ title: currentTitle })
|
||
|
||
return list
|
||
})
|
||
|
||
/** 只有一个时按偏好隐藏 */
|
||
const visible = computed(() => {
|
||
if (!pref.value.breadcrumb.enable) return false
|
||
if (pref.value.breadcrumb.hideOnlyOne && crumbs.value.length <= 1) return false
|
||
return crumbs.value.length > 0
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<nav
|
||
v-if="visible"
|
||
class="layout-breadcrumb"
|
||
:class="{ 'is-background': pref.breadcrumb.styleType === 'background' }"
|
||
>
|
||
<template v-for="(crumb, index) in crumbs" :key="crumb.title">
|
||
<span v-if="index > 0" class="layout-breadcrumb__sep">/</span>
|
||
|
||
<RouterLink v-if="crumb.path" class="layout-breadcrumb__item" :to="crumb.path">
|
||
<!-- __content 是 background 风格里做「内容整体左移」的包裹层(照 vben 的 li a > span) -->
|
||
<span class="layout-breadcrumb__content">
|
||
<House v-if="index === 0 && pref.breadcrumb.showHome" :size="14" />
|
||
{{ crumb.title }}
|
||
</span>
|
||
</RouterLink>
|
||
|
||
<span v-else class="layout-breadcrumb__item is-current">
|
||
<span class="layout-breadcrumb__content">{{ crumb.title }}</span>
|
||
</span>
|
||
</template>
|
||
</nav>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.layout-breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
/* 字号:15px —— 比顶栏 18px 的图标按钮小一档,层次清楚又不至于太弱 */
|
||
font-size: 15px;
|
||
|
||
&__sep {
|
||
color: var(--g3-color-text-tertiary, #86909c);
|
||
}
|
||
|
||
&__item {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
color: var(--g3-color-text-secondary, #4e5969);
|
||
text-decoration: none;
|
||
|
||
&:hover {
|
||
color: var(--g3-color-primary, #165dff);
|
||
}
|
||
|
||
&.is-current {
|
||
color: var(--g3-color-text, #1f2329);
|
||
}
|
||
}
|
||
|
||
&__content {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
/* ---- background 风格 ----
|
||
* 照搬 vben 的 breadcrumb-background.vue:整条不是「灰底圆角块」,而是
|
||
* 一段段 accent 底色的「>」形(面包屑步骤条)。段与段之间不再用 / 分隔符,
|
||
* 靠两块 14px 边框三角拼接:
|
||
* ::before 放在段左侧 28px 处,左三角透明 → 形成向左的凹口;
|
||
* ::after 放在段右侧,只留左三角着色 → 形成指向右的尖角。
|
||
* 14px 边框 × 上下两半 = 28px,正好等于段高 h-7(28px)。 */
|
||
&.is-background {
|
||
gap: 0;
|
||
font-size: 13px;
|
||
|
||
/* 有箭头就没有分隔符了 */
|
||
.layout-breadcrumb__sep {
|
||
display: none;
|
||
}
|
||
|
||
.layout-breadcrumb__item {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
height: 28px;
|
||
/* mr-9(36px):给右箭头占的 28px 留位,另留 8px 视觉间隙 */
|
||
margin-right: 36px;
|
||
padding: 0 8px 0 5px;
|
||
color: var(--g3-color-text-secondary, #4e5969);
|
||
background: var(--g3-color-accent, #f2f3f5);
|
||
transition: background-color 0.2s ease;
|
||
|
||
&::before,
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
width: 0;
|
||
height: 0;
|
||
border: 14px solid var(--g3-color-accent, #f2f3f5);
|
||
}
|
||
|
||
&::before {
|
||
left: -28px;
|
||
z-index: 10;
|
||
border-left-color: transparent;
|
||
}
|
||
|
||
&::after {
|
||
left: 100%;
|
||
border-color: transparent;
|
||
border-left-color: var(--g3-color-accent, #f2f3f5);
|
||
}
|
||
|
||
/* 首段:左端不开口,左侧留白加大并做左圆角 */
|
||
&:first-child {
|
||
padding-left: 15px;
|
||
border-radius: 2px 0 0 2px;
|
||
|
||
&::before {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
/* 末段(当前页):不做右箭头,右侧留白加大并做右圆角 */
|
||
&:last-child {
|
||
padding-right: 15px;
|
||
border-radius: 0 2px 2px 0;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
&.is-current {
|
||
color: var(--g3-color-foreground, #373c43);
|
||
}
|
||
|
||
/* 悬停只换底色,不改文字色(对齐 vben:当前页不可点,不参与 hover) */
|
||
&:not(:last-child):hover {
|
||
color: var(--g3-color-text-secondary, #4e5969);
|
||
background: var(--g3-color-accent-hover, #e3e5e8);
|
||
|
||
&::before {
|
||
border-color: var(--g3-color-accent-hover, #e3e5e8);
|
||
border-left-color: transparent;
|
||
}
|
||
|
||
&::after {
|
||
border-left-color: var(--g3-color-accent-hover, #e3e5e8);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 内容整体左移,视觉上落进「>」的口子里(照 vben 的 -ml-3 / 首段 -ml-1) */
|
||
.layout-breadcrumb__content {
|
||
position: relative;
|
||
z-index: 10;
|
||
margin-left: -12px;
|
||
}
|
||
|
||
.layout-breadcrumb__item:first-child .layout-breadcrumb__content {
|
||
margin-left: -4px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|