This commit is contained in:
oneao committed 2026-10-11 22:14:13 +08:00
1 parent 32d423b759
commit 78eb811ff7
102 files changed
+12067 -209

No files matched your search

@@ -3,31 +3,59 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { House } from '@lucide/vue'
import { usePreferencesStore } from '@/preferences'
import { findMenuItemByPath, findParentKeyByPath } from '@/router'
import { menuItems } from '@/constants/menu'
import { findMenuItemByKey, findMenuItemByPath, findParentKeysByPath } from '@/router'
/** 面包屑单项:有 path 的是可点的父级,没有的是当前页。 */
interface Crumb {
title: string
path?: string
}
/**
* 面包屑。
* 数据来自菜单常量:父级分组 + 当前页,与侧栏同源,不额外维护。
* 数据来自菜单常量:祖先分组链 + 当前页,与侧栏同源,不额外维护。
*/
const route = useRoute()
const prefStore = usePreferencesStore()
const pref = computed(() => prefStore.preferences)
/** 当前路径对应的祖先链:[分组, 当前页] */
const crumbs = computed(() => {
const list: Array<{ title: string; path?: string }> = []
/**
* 当前路径对应的祖先链:[...分组, 当前页]。
*
* 父级用 findParentKeysByPath(返回【完整】祖先 key 链)而不是
* findParentKeyByPath(只取 [0] = 顶级)—— 后者在多级菜单下会漏掉中间层:
* 「海运 → 订舱业务 → 订舱单」只会显示成「海运 / 订舱单」。
*/
const crumbs = computed<Crumb[]>(() => {
const list: Crumb[] = []
if (pref.value.breadcrumb.showHome) {
list.push({ title: '首页', path: '/' })
}
const current = findMenuItemByPath(route.path)
const parentKey = findParentKeyByPath(route.path)
const parent = parentKey ? menuItems.find((item) => item.key === parentKey) : undefined
// 1) 菜单里的完整祖先链(顶级 → 最近一级分组)
for (const key of findParentKeysByPath(route.path)) {
const node = findMenuItemByKey(key)
if (node) list.push({ title: node.title, path: node.path })
}
if (parent) list.push({ title: parent.title, path: parent.path })
if (current && current.key !== parent?.key) list.push({ title: current.title })
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
})
@@ -50,11 +78,16 @@ const visible = computed(() => {
<span v-if="index > 0" class="layout-breadcrumb__sep">/</span>
<RouterLink v-if="crumb.path" class="layout-breadcrumb__item" :to="crumb.path">
<House v-if="index === 0 && pref.breadcrumb.showHome" :size="14" />
<span>{{ crumb.title }}</span>
<!-- __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">{{ crumb.title }}</span>
<span v-else class="layout-breadcrumb__item is-current">
<span class="layout-breadcrumb__content">{{ crumb.title }}</span>
</span>
</template>
</nav>
</template>
@@ -65,13 +98,8 @@ const visible = computed(() => {
align-items: center;
gap: 6px;
min-width: 0;
font-size: 13px;
&.is-background {
padding: 4px 10px;
background: var(--g3-color-fill-hover, #f2f3f5);
border-radius: 6px;
}
/* 字号:15px —— 比顶栏 18px 的图标按钮小一档,层次清楚又不至于太弱 */
font-size: 15px;
&__sep {
color: var(--g3-color-text-tertiary, #86909c);
@@ -80,7 +108,6 @@ const visible = computed(() => {
&__item {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--g3-color-text-secondary, #4e5969);
text-decoration: none;
@@ -92,5 +119,113 @@ const visible = computed(() => {
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>