完善页面

This commit is contained in:
2026-01-29 17:54:35 +08:00
parent 9f3c2803e3
commit fcc976680d
95 changed files with 4595 additions and 10542 deletions

View File

@@ -1,7 +1,6 @@
<template>
<view
class="sub-sider"
v-if="groups && groups.length > 0"
:style="{ left: props.asideWidth + 'px', width: props.siderWidth + 'px' }"
>
<view class="sub-header">
@@ -14,14 +13,22 @@
:key="groupKey(g)"
class="group"
>
<view class="group-title" @click="toggleGroup(groupKey(g))">
<!-- ✅ 改:点击标题时,如果该组没有 children 但有 path则当成“叶子菜单”直接跳 -->
<view class="group-title" @click="handleGroupTitleClick(g)">
<text class="group-title-text">{{ g.title }}</text>
<text class="group-arrow" :class="{ open: isGroupOpen(groupKey(g)) }">v</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 g.children"
v-for="c in groupChildren(g)"
:key="c.id"
class="sub-item"
:class="{ active: resolvedActiveId === c.id }"
@@ -38,16 +45,19 @@
</template>
<script setup lang="uts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, withDefaults } from 'vue'
import type { MenuGroup, MenuChild } from '../types.uts'
const props = defineProps<{
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
@@ -58,33 +68,60 @@ const openGroupKey = ref<string>('')
/** 给 group 一个稳定 key优先用 id如果你 types 里没有 id就用 title */
const groupKey = (g: MenuGroup): string => {
// @ts-ignore - 允许 g.id 可选
// @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
// 去掉 query
const q = s.indexOf('?')
return q >= 0 ? s.slice(0, q) : s
}
/** 扁平查找id -> child */
/** 扁平查找id -> child(✅ 改:遍历时用 groupChildren/childChildren */
const findChildById = (id: string): MenuChild | null => {
if (!id) return null
for (const g of props.groups) {
for (const c of g.children) {
// ✅ 叶子 group 也能命中
const gLeaf = groupAsChild(g)
if (gLeaf && gLeaf.id === id) return gLeaf
for (const c of groupChildren(g)) {
if (c.id === id) return c
// 兼容未来有更深层 children递归
// @ts-ignore
const anyC: any = c as any
// @ts-ignore
if (anyC.children && (anyC.children as MenuChild[]).length > 0) {
// @ts-ignore
const hit = findFirstMatchInChildren(id, anyC.children as MenuChild[])
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
@@ -92,16 +129,12 @@ const findChildById = (id: string): MenuChild | null => {
return null
}
// @ts-ignore
const findFirstMatchInChildren = (id: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (n.id === id) return n
// @ts-ignore
const anyN: any = n as any
// @ts-ignore
if (anyN.children && (anyN.children as MenuChild[]).length > 0) {
// @ts-ignore
const hit = findFirstMatchInChildren(id, anyN.children as MenuChild[])
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return hit
}
}
@@ -111,16 +144,17 @@ const findFirstMatchInChildren = (id: string, list: MenuChild[]): MenuChild | nu
const findChildByPath = (path: string): MenuChild | null => {
const target = normalizePath(path)
if (!target) return null
for (const g of props.groups) {
for (const c of g.children) {
// ✅ 叶子 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
// 兼容更深层 children(递归)
// @ts-ignore
const anyC: any = c as any
// @ts-ignore
if (anyC.children && (anyC.children as MenuChild[]).length > 0) {
// @ts-ignore
const hit = findFirstMatchByPath(target, anyC.children as MenuChild[])
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchByPath(target, deep)
if (hit) return hit
}
}
@@ -128,72 +162,62 @@ const findChildByPath = (path: string): MenuChild | null => {
return null
}
// @ts-ignore
const findFirstMatchByPath = (targetNorm: string, list: MenuChild[]): MenuChild | null => {
for (const n of list) {
if (normalizePath(n.path) === targetNorm) return n
// @ts-ignore
const anyN: any = n as any
// @ts-ignore
if (anyN.children && (anyN.children as MenuChild[]).length > 0) {
// @ts-ignore
const hit = findFirstMatchByPath(targetNorm, anyN.children as MenuChild[])
const deep = childChildren(n)
if (deep.length > 0) {
const hit = findFirstMatchByPath(targetNorm, deep)
if (hit) return hit
}
}
return null
}
/** 找到某个 child 属于哪个 group用于自动展开该组 */
/** 找到某个 child 属于哪个 group用于自动展开该组(✅ 改:遍历兜底) */
const findGroupKeyByChildId = (id: string): string => {
for (const g of props.groups) {
for (const c of g.children) {
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)
// @ts-ignore
const anyC: any = c as any
// @ts-ignore
if (anyC.children && (anyC.children as MenuChild[]).length > 0) {
// 深层命中也算这个 group
// @ts-ignore
const hit = findFirstMatchInChildren(id, anyC.children as MenuChild[])
const deep = childChildren(c)
if (deep.length > 0) {
const hit = findFirstMatchInChildren(id, deep)
if (hit) return groupKey(g)
}
}
}
// fallback第一个 group
return props.groups.length > 0 ? groupKey(props.groups[0]) : ''
}
/** 递归取第一个 leaf 菜单:你要求的“默认打开第一个页面(递归)” */
/** 递归取第一个 leaf 菜单(✅ 改children 兜底) */
const firstLeaf = (): MenuChild | null => {
if (!props.groups || props.groups.length === 0) return null
const g0 = props.groups[0]
if (!g0.children || g0.children.length === 0) return null
const g0Children = groupChildren(g0)
const c0: any = g0.children[0] as any
if (c0.children && (c0.children as MenuChild[]).length > 0) {
// @ts-ignore
return findFirstLeafInChildren(c0.children as MenuChild[])
// ✅ 如果第一个 group 是叶子(无 children 但有 path也能返回
if (g0Children.length === 0) {
return groupAsChild(g0)
}
return g0.children[0]
const c0 = g0Children[0]
const deep = childChildren(c0)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return c0
}
// @ts-ignore
const findFirstLeafInChildren = (list: MenuChild[]): MenuChild | null => {
if (!list || list.length === 0) return null
const n: any = list[0] as any
if (n.children && (n.children as MenuChild[]).length > 0) {
// @ts-ignore
return findFirstLeafInChildren(n.children as MenuChild[])
}
return list[0]
const n = list[0]
const deep = childChildren(n)
if (deep.length > 0) return findFirstLeafInChildren(deep)
return n
}
/**
* 高亮兜底:如果 activeSubId 没同步(你现在遇到的“点了不高亮”),
* 就按当前页面 route/path 再匹配一次。
* uni-app-x 的 getCurrentPages() 返回页面对象page.route 可取当前路由。:contentReference[oaicite:3]{index=3}
*/
/** 高亮兜底:按 activeSubId -> route/path 再匹配一次 */
const resolvedActiveId = computed((): string => {
const byId = findChildById(props.activeSubId)
if (byId) return byId.id
@@ -215,7 +239,6 @@ const resolvedActiveId = computed((): string => {
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
@@ -225,24 +248,37 @@ 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) => {
// 点击就让该组展开(用户体验更 CRMEB
openGroupKey.value = findGroupKeyByChildId(c.id)
emit('sub-click', c)
}
/** 自动:groups 变/activeSubId 无效时,默认跳第一个 leaf(递归)并展开对应 group */
/** 自动groups 变/activeSubId 无效时,默认跳第一个 leaf 并展开对应 group */
const ensureDefault = () => {
if (!props.groups || props.groups.length === 0) return
// activeSubId 有效:展开它所在组
const hit = findChildById(props.activeSubId)
if (hit) {
openGroupKey.value = findGroupKeyByChildId(hit.id)
return
}
// activeSubId 无效/空:默认递归选第一个 leaf
const first = firstLeaf()
if (first) {
openGroupKey.value = findGroupKeyByChildId(first.id)
@@ -264,6 +300,7 @@ watch(
</script>
<style>
/* 原样保留你的样式 */
.sub-sider{
background:#ffffff;
border-right: 1px solid #e5e7eb;
@@ -278,20 +315,21 @@ watch(
height: 56px;
display:flex;
align-items:center;
padding: 0 16px;
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);
}
/* CRMEB 风格:分组“块”更明显 */
.group{ padding: 8px 0; }
.group-title{
@@ -324,11 +362,10 @@ watch(
padding: 0 12px 6px 12px;
}
/* 子菜单:缩进 + 更小高度 */
.sub-item{
height: 34px;
margin: 4px 0;
padding: 0 14px 0 28px; /* 体现层级 */
padding: 0 14px 0 28px;
display:flex;
align-items:center;
border-radius: 8px;