Files
workspace/code/g3soft-erp/web/src/layouts/LayoutBreadcrumb.vue
T
2026-10-11 22:14:13 +08:00

232 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>