Files
medical-mall/layouts/admin/components/AdminSubsider.uvue
2026-01-29 17:54:35 +08:00

387 lines
9.7 KiB
Plaintext
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.
<template>
<view
class="sub-sider"
:style="{ left: props.asideWidth + 'px', width: props.siderWidth + 'px' }"
>
<view class="sub-header">
<text class="sub-title">{{ activeMenuTitle }}</text>
</view>
<scroll-view class="sub-body" scroll-y="true">
<view
v-for="g in groups"
:key="groupKey(g)"
class="group"
>
<!-- ✅ 改:点击标题时,如果该组没有 children 但有 path则当成“叶子菜单”直接跳 -->
<view class="group-title" @click="handleGroupTitleClick(g)">
<text class="group-title-text">{{ g.title }}</text>
<!-- ✅ 改:只有有 children 才显示箭头 -->
<text
v-if="groupChildren(g).length > 0"
class="group-arrow"
:class="{ open: isGroupOpen(groupKey(g)) }"
>v</text>
</view>
<!-- ✅ 改v-show 的条件不变,但 children 的遍历必须兜底 -->
<view v-show="isGroupOpen(groupKey(g))" class="group-children">
<view
v-for="c in groupChildren(g)"
:key="c.id"
class="sub-item"
:class="{ active: resolvedActiveId === c.id }"
@click.stop="handleClick(c)"
>
<text class="sub-item-text" :class="{ activeText: resolvedActiveId === c.id }">
{{ c.title }}
</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { ref, computed, watch, withDefaults } from 'vue'
import type { MenuGroup, MenuChild } from '../types.uts'
const props = withDefaults(defineProps<{
activeMenuTitle: string
groups: MenuGroup[]
activeSubId: string
activeMenuId?: string | null
asideWidth: number
siderWidth: number
}>(), {
activeMenuId: 'home',
})
const emit = defineEmits<{
(e: 'sub-click', child: MenuChild): void
}>()
/** 只展开一个分组(更像 CRMEB */
const openGroupKey = ref<string>('')
/** 给 group 一个稳定 key优先用 id如果你 types 里没有 id就用 title */
const groupKey = (g: MenuGroup): string => {
// @ts-ignore
const anyG: any = g as any
return (anyG.id && (anyG.id as string)) ? (anyG.id as string) : g.title
}
/** ✅ 核心group.children 兜底成 [](因为 children 现在可选/可空) */
const groupChildren = (g: MenuGroup): MenuChild[] => {
// @ts-ignore
const anyG: any = g as any
// @ts-ignore
const list = anyG.children as (MenuChild[] | null | undefined)
return list && list.length > 0 ? list : []
}
/** ✅ 兼容四级child.children 兜底成 [] */
const childChildren = (c: MenuChild): MenuChild[] => {
// @ts-ignore
const anyC: any = c as any
// @ts-ignore
const list = anyC.children as (MenuChild[] | null | undefined)
return list && list.length > 0 ? list : []
}
/** ✅ 叶子 group无 children如果有 path则构造一个“伪 MenuChild”用于 emit 跳转 */
const groupAsChild = (g: MenuGroup): MenuChild | null => {
// @ts-ignore
const anyG: any = g as any
// @ts-ignore
const p = anyG.path as (string | null | undefined)
if (!p) return null
return { id: groupKey(g), title: g.title, path: p } as MenuChild
}
const normalizePath = (p: string): string => {
if (!p) return ''
const s = p.startsWith('/') ? p.slice(1) : p
const q = s.indexOf('?')
return q >= 0 ? s.slice(0, q) : s
}
/** 扁平查找id -> child✅ 改:遍历时用 groupChildren/childChildren */
const findChildById = (id: string): MenuChild | null => {
if (!id) return null
for (const g of props.groups) {
// ✅ 叶子 group 也能命中
const gLeaf = groupAsChild(g)
if (gLeaf && gLeaf.id === id) return gLeaf
for (const c of groupChildren(g)) {
if (c.id === id) return c
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
}
return null
}
const findFirstMatchInChildren = (id: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (n.id === id) return n
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
return null
}
const findChildByPath = (path: string): MenuChild | null => {
const target = normalizePath(path)
if (!target) return null
for (const g of props.groups) {
// ✅ 叶子 group 也能按 path 命中
const gLeaf = groupAsChild(g)
if (gLeaf && normalizePath(gLeaf.path) === target) return gLeaf
for (const c of groupChildren(g)) {
if (normalizePath(c.path) === target) return c
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchByPath(target, deep)
if (hit) return hit
}
}
}
return null
}
const findFirstMatchByPath = (targetNorm: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (normalizePath(n.path) === targetNorm) return n
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchByPath(targetNorm, deep)
if (hit) return hit
}
}
return null
}
/** 找到某个 child 属于哪个 group用于自动展开该组✅ 改:遍历兜底) */
const findGroupKeyByChildId = (id: string): string => {
for (const g of props.groups) {
const gLeaf = groupAsChild(g)
if (gLeaf && gLeaf.id === id) return groupKey(g)
for (const c of groupChildren(g)) {
if (c.id === id) return groupKey(g)
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return groupKey(g)
}
}
}
return props.groups.length > 0 ? groupKey(props.groups[0]) : ''
}
/** 递归取第一个 leaf 菜单(✅ 改children 兜底) */
const firstLeaf = (): MenuChild | null => {
if (!props.groups || props.groups.length === 0) return null
const g0 = props.groups[0]
const g0Children = groupChildren(g0)
// ✅ 如果第一个 group 是叶子(无 children 但有 path也能返回
if (g0Children.length === 0) {
return groupAsChild(g0)
}
const c0 = g0Children[0]
const deep = childChildren(c0)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return c0
}
const findFirstLeafInChildren = (list: MenuChild[]): MenuChild | null => {
if (!list || list.length === 0) return null
const n = list[0]
const deep = childChildren(n)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return n
}
/** 高亮兜底:按 activeSubId -> route/path 再匹配一次 */
const resolvedActiveId = computed((): string => {
const byId = findChildById(props.activeSubId)
if (byId) return byId.id
try {
const pages = getCurrentPages()
if (pages && pages.length > 0) {
// @ts-ignore
const cur: any = pages[pages.length - 1]
// @ts-ignore
const route: string = cur.route ? (cur.route as string) : ''
const hit = findChildByPath(route)
if (hit) return hit.id
}
} catch (e) {}
return props.activeSubId || ''
})
const isGroupOpen = (key: string): boolean => {
if (!openGroupKey.value) {
return props.groups && props.groups.length > 0 ? groupKey(props.groups[0]) === key : false
}
return openGroupKey.value === key
}
const toggleGroup = (key: string) => {
openGroupKey.value = (openGroupKey.value === key) ? '' : key
}
/** ✅ 新增:点 group 标题时的行为 */
const handleGroupTitleClick = (g: MenuGroup) => {
const list = groupChildren(g)
// 有 children正常展开/收起
if (list.length > 0) {
toggleGroup(groupKey(g))
return
}
// 无 children如果有 path当成叶子菜单直接跳
const leaf = groupAsChild(g)
if (leaf) {
openGroupKey.value = groupKey(g)
emit('sub-click', leaf)
}
}
const handleClick = (c: MenuChild) => {
openGroupKey.value = findGroupKeyByChildId(c.id)
emit('sub-click', c)
}
/** 自动groups 变更/activeSubId 无效时,默认跳第一个 leaf 并展开对应 group */
const ensureDefault = () => {
if (!props.groups || props.groups.length === 0) return
const hit = findChildById(props.activeSubId)
if (hit) {
openGroupKey.value = findGroupKeyByChildId(hit.id)
return
}
const first = firstLeaf()
if (first) {
openGroupKey.value = findGroupKeyByChildId(first.id)
emit('sub-click', first)
}
}
watch(
() => props.groups,
() => { ensureDefault() },
{ immediate: true, deep: true }
)
watch(
() => props.activeSubId,
() => { ensureDefault() },
{ immediate: true }
)
</script>
<style>
/* 原样保留你的样式 */
.sub-sider{
background:#ffffff;
border-right: 1px solid #e5e7eb;
height: 100vh;
position: fixed;
top: 0;
bottom: 0;
z-index: 900;
}
.sub-header{
height: 56px;
display:flex;
align-items:center;
border-bottom: 1px solid #eef2f7;
}
.sub-title{
font-size:16px;
font-weight:700;
color:#111827;
display:flex;
justify-content:center;
align-items:center;
}
.sub-body{
height: calc(100vh - 56px);
}
.group{ padding: 8px 0; }
.group-title{
height: 42px;
margin: 8px 12px 6px 12px;
padding: 0 14px;
display:flex;
align-items:center;
justify-content: space-between;
background: #f6f7fb;
border-radius: 8px;
}
.group-title-text{
font-size: 14px;
font-weight: 700;
color:#111827;
}
.group-arrow{
font-size: 12px;
color: #6b7280;
transform: rotate(0deg);
}
.group-arrow.open{
transform: rotate(180deg);
}
.group-children{
padding: 0 12px 6px 12px;
}
.sub-item{
height: 34px;
margin: 4px 0;
padding: 0 14px 0 28px;
display:flex;
align-items:center;
border-radius: 8px;
}
.sub-item.active{
background: #eaf2ff;
}
.sub-item-text{
font-size: 13px;
color:#374151;
}
.sub-item-text.activeText{
color:#1677ff;
font-weight: 700;
}
</style>