完善页面布局

This commit is contained in:
2026-02-06 10:14:46 +08:00
parent 7a75ab7df4
commit 9eaf5c1a64
28 changed files with 1166 additions and 177 deletions

View File

@@ -1,57 +1,160 @@
<template>
<view
class="admin-subsider"
:class="{ 'is-hidden': !visible }"
:style="{ left: asideWidth + 'px', width: siderWidth + 'px' }"
:class="{ 'is-hidden': !visible, 'sub-sider-overlay': isOverlay || layoutMode === 'mobile' }"
:style="{ left: asideWidth + 'px', width: width + 'px' }"
>
<view class="subsider-header">
<text class="header-title">{{ topMenuTitle }}</text>
<!-- 头部模块标题 (1:1 复刻 CRMEB) -->
<view v-if="menuTitle" class="subsider-cat-name">
<text class="cat-title">{{ menuTitle }}</text>
</view>
<view class="subsider-menu">
<view v-for="group in groups" :key="group.id" class="menu-group">
<view class="group-title">
<text>{{ group.title }}</text>
</view>
<!-- 菜单列表滚动 -->
<scroll-view class="subsider-content" scroll-y="true" show-scrollbar="false">
<view class="menu-list">
<!-- 分级手动多层渲染 (UTS 下直接循环比递归更稳定) -->
<template v-for="level1 in menuTree" :key="level1.id">
<!-- 一级层级 (通常是分组或顶级页面) -->
<view
class="menu-row level-1"
:class="{ 'is-active': isActive(level1), 'is-open': isOpen(level1.id, 1) }"
@click="handleNodeClick(level1, 1)"
>
<text class="menu-text">{{ level1.title }}</text>
<text v-if="level1.type === 'group'" class="chevron">
{{ isOpen(level1.id, 1) ? '▼' : '▶' }}
</text>
</view>
<view
v-for="route in getGroupRoutes(group.id)"
:key="route.id"
class="menu-item"
:class="{ active: route.id === activeRouteId }"
@click="onRouteClick(route.id)"
>
<text class="item-title">{{ route.title }}</text>
</view>
<!-- 二级子菜单容器 -->
<view v-if="level1.type === 'group' && isOpen(level1.id, 1)" class="sub-menu-container">
<template v-for="level2 in level1.children" :key="level2.id">
<view
class="menu-row level-2"
:class="{ 'is-active': isActive(level2), 'is-open': isOpen(level2.id, 2) }"
@click="handleNodeClick(level2, 2, level1.id)"
>
<text class="menu-text">{{ level2.title }}</text>
<text v-if="level2.type === 'group'" class="chevron">
{{ isOpen(level2.id, 2) ? '▼' : '▶' }}
</text>
</view>
<!-- 三级子菜单容器 -->
<view v-if="level2.type === 'group' && isOpen(level2.id, 2)" class="sub-menu-container">
<template v-for="level3 in level2.children" :key="level3.id">
<view
class="menu-row level-3"
:class="{ 'is-active': isActive(level3) }"
@click="handleNodeClick(level3, 3, level2.id)"
>
<text class="menu-text">{{ level3.title }}</text>
</view>
</template>
</view>
</template>
</view>
</template>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import type { MenuGroup, RouteRecord } from '@/layouts/admin/router/adminRoutes.uts'
import { ref, reactive, watch, onMounted } from 'vue'
import type { MenuNode } from '../router/settingSubSiderMenu.uts'
const props = defineProps<{
visible: boolean // ✅ 新增:由父层控制显示/隐藏(不要用 v-if 卸载)
topMenuTitle: string
groups: MenuGroup[]
routes: Map<string, RouteRecord[]>
activeRouteId: string
visible: boolean
menuTitle: string
menuTree: MenuNode[]
activeId: string
currentPath: string
asideWidth: number
siderWidth: number
width: number
isOverlay?: boolean
layoutMode?: string
}>()
const emit = defineEmits<{
(e: 'route-click', routeId: string): void
(e: 'sub-click', payload: { id: string, path: string }): void
}>()
function getGroupRoutes(groupId: string): RouteRecord[] {
return props.routes.get(groupId) || []
// --- 展开状态管理 ---
const openIdsByLevel = reactive<Record<number, string>>({
1: '',
2: '',
3: ''
})
/** 判断项是否激活 (页面级) */
function isActive(node: MenuNode): boolean {
// 只有非分组项才能被视为“激活”变色 (1:1 复刻 screenshot 效果)
if (node.type === 'group') return false
if (props.activeId != '' && node.id === props.activeId) return true
if (props.currentPath != '' && node.path === props.currentPath) return true
return false
}
function onRouteClick(routeId: string): void {
emit('route-click', routeId)
/** 判断分组是否展开 */
function isOpen(id: string, level: number): boolean {
return openIdsByLevel[level] === id
}
/** 统一点击处理 */
function handleNodeClick(node: MenuNode, level: number, parentId: string = ''): void {
if (node.type === 'group') {
// 手风琴逻辑
if (openIdsByLevel[level] === node.id) {
openIdsByLevel[level] = ''
} else {
openIdsByLevel[level] = node.id
}
} else {
// 页面跳转
emit('sub-click', { id: node.id, path: node.path || '' })
}
}
/** 自动展开逻辑:根据当前激活项回溯父级展开状态 */
function autoExpand() {
const targetId = props.activeId || ''
const targetPath = props.currentPath || ''
if (targetId === '' && targetPath === '') return
const findPath = (nodes: MenuNode[]): string[] | null => {
for (const node of nodes) {
if (node.type === 'page' && ((targetId != '' && node.id === targetId) || (targetPath != '' && node.path === targetPath))) {
return [node.id]
}
if (node.children != null) {
const childPath = findPath(node.children!)
if (childPath != null) {
return [node.id, ...childPath]
}
}
}
return null
}
const path = findPath(props.menuTree)
if (path != null) {
// path 包含了从祖先到叶子的所有 ID
for (let i = 0; i < path.length - 1; i++) {
openIdsByLevel[i + 1] = path[i]
}
}
}
watch(() => props.activeId, () => autoExpand())
watch(() => props.currentPath, () => autoExpand())
watch(() => props.menuTree, () => autoExpand(), { immediate: true })
onMounted(() => {
autoExpand()
})
</script>
<style scoped lang="scss">
@@ -59,118 +162,102 @@ function onRouteClick(routeId: string): void {
position: fixed;
top: 0;
bottom: 0;
background: #fff;
border-right: 1px solid #e8e8e8;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
background: #ffffff;
border-right: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
/* ✅ 动画只用 GPU 友好属性,避免 width/left 导致卡顿 */
opacity: 1;
transform: translate3d(0, 0, 0);
will-change: transform, opacity;
/* ✅ 可点击时才需要高 z-index */
z-index: 999;
/* ✅ 注意visibility 默认可见 */
visibility: visible;
/* 显示态visibility 不延迟 */
transition: transform 300ms ease, opacity 250ms ease, visibility 0s linear 0s;
}
/* 悬浮/抽屉模式:复刻 CRMEB 核心逻辑 */
.sub-sider-overlay {
z-index: 1001;
box-shadow: 6px 0 16px rgba(0, 0, 0, 0.08);
z-index: 1000;
transition: transform 300ms ease, opacity 250ms ease;
}
.admin-subsider.is-hidden {
/* ✅ 立即禁止拦截点击,彻底解决“遮挡可操作区” */
pointer-events: none;
/* ✅ 先淡出 + 滑出(推荐滑出 100%:完全离开可视区) */
opacity: 0;
transform: translate3d(-100%, 0, 0);
/* ✅ 动画结束后再隐藏(避免那一帧还挡住) */
visibility: hidden;
transition: transform 300ms ease, opacity 250ms ease, visibility 0s linear 300ms;
/* 可选:进一步避免层叠影响 */
z-index: 0;
opacity: 0;
pointer-events: none;
}
.subsider-header {
height: 64px;
.sub-sider-overlay {
z-index: 1010;
box-shadow: 6px 0 16px rgba(0, 0, 0, 0.08);
}
.subsider-cat-name {
height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #e8e8e8;
.header-title {
font-size: 16px;
padding: 0 20px;
border-bottom: 1px solid #f0f0f0;
background-color: #fff;
flex-shrink: 0;
.cat-title {
font-size: 15px;
font-weight: 600;
color: #333;
}
}
.subsider-menu {
.subsider-content {
flex: 1;
padding: 8px 0;
/* ✅ 用 auto避免一直显示滚动条影响布局 */
overflow-y: auto;
}
.menu-group {
margin-bottom: 16px;
.menu-list {
padding: 0; /* 移除 8px 边距,确保菜单项紧贴顶部/标题栏 */
}
.group-title {
padding: 8px 16px;
font-size: 12px;
color: #999;
font-weight: 500;
text {
display: block;
}
}
.menu-item {
height: 36px;
padding: 0 16px 0 24px;
.menu-row {
height: 50px;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
width: 100%;
border-bottom: 1px solid transparent; /* 保持布局稳定 */
/* ✅ 不要 transition: all会引发布局属性动画/回流) */
transition: background-color 0.2s ease, color 0.2s ease;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
color: #1890ff;
&::after {
content: '';
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 3px;
background: #1890ff;
&.is-active {
background-color: #e7f0ff;
.menu-text {
color: #2f65ff;
font-weight: 500;
}
}
.item-title {
.menu-text {
font-size: 14px;
color: #333;
flex: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 1:1 复刻 CRMEB 缩进逻辑 */
&.level-1 {
padding-left: 20px;
padding-right: 16px;
}
&.level-2 {
padding-left: 44px; /* 二级缩进 */
padding-right: 16px;
}
&.level-3 {
padding-left: 64px; /* 三级进一步缩进 */
padding-right: 16px;
}
}
.chevron {
font-size: 10px;
color: #c0c4cc;
margin-left: 4px;
}
.sub-menu-container {
overflow: hidden;
}
</style>