423 lines
11 KiB
Markdown
423 lines
11 KiB
Markdown
# 确保 Admin 页面侧边栏一直显示的完整步骤
|
||
|
||
## 概述
|
||
|
||
确保 uni-app-x 的 Admin 页面在切换过程中保持侧边栏显示需要从多个维度进行配置。以下是完整的步骤检查清单。
|
||
|
||
---
|
||
|
||
## 第一部分:文件和路由配置
|
||
|
||
### 1.1 创建新的 Admin 页面文件
|
||
|
||
**文件路径**: `pages/mall/admin/maintain/<category>/<page-name>.uvue`
|
||
|
||
**重点**:
|
||
|
||
- ✅ 使用 UTF-8 编码(**不要 BOM**)
|
||
- ✅ 严格的 SFC 结构: `<template>` → `<script>` → `<style>`
|
||
- ✅ 没有额外内容在closing tags之后
|
||
|
||
**模板**:
|
||
|
||
```uvue
|
||
<template>
|
||
<AdminLayout currentPage="<page-id>">
|
||
<view class="page">
|
||
<!-- 页面内容 -->
|
||
</view>
|
||
</AdminLayout>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import AdminLayout from '@/layouts/admin/AdminLayout.uvue'
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
padding: 20px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### 1.2 在 pages.json 中注册路由
|
||
|
||
**位置**: `pages.json` 中的合适分类下
|
||
|
||
**格式**:
|
||
|
||
```json
|
||
{
|
||
"path": "maintain/<category>/<page-name>",
|
||
"style": {
|
||
"navigationBarTitleText": "页面标题",
|
||
"navigationStyle": "custom"
|
||
}
|
||
}
|
||
```
|
||
|
||
**重点**:
|
||
|
||
- ✅ `navigationStyle: "custom"` 必须设置,允许自定义 AdminLayout
|
||
- ✅ `path` 必须与文件结构匹配
|
||
|
||
---
|
||
|
||
## 第二部分:菜单配置
|
||
|
||
### 2.1 在 menu.uts 中定义菜单项
|
||
|
||
**文件**: `layouts/admin/utils/menu.uts`
|
||
|
||
**两种菜单结构**:
|
||
|
||
#### 选项 A:有子菜单的菜单组(推荐)
|
||
|
||
```typescript
|
||
{
|
||
id: 'maintain',
|
||
title: '维护',
|
||
icon: '/static/maintain.svg',
|
||
path: '/pages/mall/admin/maintain/...',
|
||
groups: [
|
||
{
|
||
id: 'dev-config',
|
||
title: '开发配置',
|
||
children: [
|
||
{
|
||
id: 'module-config',
|
||
title: '模块配置',
|
||
path: '/pages/mall/admin/maintain/dev-config/module-config'
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 选项 B:没有子菜单的菜单组(叶子节点)
|
||
|
||
```typescript
|
||
{
|
||
id: 'maintain',
|
||
title: '维护',
|
||
groups: [
|
||
{
|
||
id: 'system-info',
|
||
title: '系统信息',
|
||
path: '/pages/mall/admin/maintain/system-info',
|
||
children: [] // ⚠️ 必须显式设置为空数组
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**重点**:
|
||
|
||
- ✅ 每个 menu item 必须有唯一的 `id`
|
||
- ✅ 如果是叶子节点,必须显式设置 `children: []`
|
||
- ✅ `path` 必须与 pages.json 路由匹配
|
||
|
||
### 2.2 菜单 ID 命名规则
|
||
|
||
**建议**:
|
||
|
||
```
|
||
一级菜单: maintain / user / order / product
|
||
二级组: dev-config / security / data / external
|
||
子项: module-config / permission / cron-job
|
||
|
||
currentPage 值应该与 menu.id 对应:
|
||
- 对应一级: currentPage="maintain"
|
||
- 对应二级: currentPage="system-info"
|
||
- 对应三级: currentPage="module-config"
|
||
```
|
||
|
||
---
|
||
|
||
## 第三部分:导航逻辑
|
||
|
||
### 3.1 nav.uts 匹配规则
|
||
|
||
**文件**: `layouts/admin/utils/nav.uts`
|
||
|
||
**关键函数**: `findActiveByCurrentPage(menuList, currentPage)`
|
||
|
||
**匹配顺序** (必须按此顺序):
|
||
|
||
1. 一级菜单 ID 匹配: `m.id === currentPage`
|
||
2. 二级菜单组 ID 匹配: `g.id === currentPage` ⚠️ **包括叶子节点**
|
||
3. 二级菜单组 path 匹配: `normalize(g.path) === normalize(currentPage)`
|
||
4. 三级菜单子项 ID 匹配: `c.id === currentPage`
|
||
5. 三级菜单子项 path 匹配: `normalize(c.path) === normalize(currentPage)`
|
||
6. 四级及以上: 递归查找
|
||
7. 默认兜底: 返回 `{ activeMenuId: 'home', activeSubId: '' }`
|
||
|
||
**核心代码**:
|
||
|
||
```typescript
|
||
// 关键:在检查 children 前,先检查 group 本身是否是叶子节点
|
||
for (const g of groups) {
|
||
if (g.id === page) {
|
||
return { activeMenuId: m.id, activeSubId: g.id }; // ✅ 叶子节点匹配
|
||
}
|
||
if (g.path && normalize(g.path) === pageNorm) {
|
||
return { activeMenuId: m.id, activeSubId: g.id }; // ✅ 叶子节点路径匹配
|
||
}
|
||
// 然后才检查 children
|
||
const cs = g.children ?? [];
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 3.2 页面中使用 currentPage
|
||
|
||
**在页面文件中**:
|
||
|
||
```uvue
|
||
<AdminLayout currentPage="system-info">
|
||
<!-- 页面内容 -->
|
||
</AdminLayout>
|
||
```
|
||
|
||
**currentPage 值确定规则**:
|
||
|
||
- 如果页面是二级菜单组的叶子: 使用 group id (`system-info`)
|
||
- 如果页面是三级菜单子项: 使用 child id (`module-config`)
|
||
- 也可以使用路径形式 (`/pages/mall/admin/maintain/system-info`)
|
||
|
||
---
|
||
|
||
## 第四部分:AdminLayout 组件
|
||
|
||
### 4.1 AdminLayout.uvue 的关键逻辑
|
||
|
||
**文件**: `layouts/admin/AdminLayout.uvue`
|
||
|
||
**核心职责**:
|
||
|
||
```typescript
|
||
// 1. 导入必要的生命周期和工具
|
||
import { onShow } from "@dcloudio/uni-app";
|
||
import { findActiveByCurrentPage } from "./utils/nav.uts";
|
||
|
||
// 2. 接收 currentPage prop
|
||
const props = defineProps<{ currentPage: string }>();
|
||
|
||
// 3. 同步导航状态的关键函数
|
||
const syncActiveByCurrentPage = () => {
|
||
let current = props.currentPage;
|
||
if (!current) {
|
||
// 如果没有 currentPage,从路由获取
|
||
const pages = getCurrentPages();
|
||
const last = pages[pages.length - 1];
|
||
current = last?.route ? `/${last.route}` : "";
|
||
}
|
||
const r = findActiveByCurrentPage(menuList.value, current);
|
||
activeMenuId.value = r.activeMenuId; // ✅ 更新一级菜单
|
||
activeSubId.value = r.activeSubId; // ✅ 更新二级菜单
|
||
};
|
||
|
||
// 4. 在多个生命周期调用同步函数
|
||
watch(
|
||
() => props.currentPage,
|
||
() => syncActiveByCurrentPage(),
|
||
{ immediate: true },
|
||
);
|
||
onMounted(() => syncActiveByCurrentPage());
|
||
onShow(() => syncActiveByCurrentPage());
|
||
|
||
// 5. 计算二级侧边栏的内容
|
||
const activeGroups = computed(() => {
|
||
const m = menuList.value.find((it) => it.id === activeMenuId.value);
|
||
return m?.groups ?? [];
|
||
});
|
||
|
||
// 6. 根据 activeSubId 计算面包屑标题
|
||
const breadcrumb = computed(() => {
|
||
let subTitle = "";
|
||
const groups = activeGroups.value;
|
||
for (const g of groups) {
|
||
// ✅ 检查 group 本身(支持叶子节点)
|
||
if (g.id === activeSubId.value) {
|
||
subTitle = g.title;
|
||
break;
|
||
}
|
||
// ✅ 检查 group 的 children
|
||
const cs = g.children ?? [];
|
||
const hit = cs.find((c) => c.id === activeSubId.value);
|
||
if (hit) {
|
||
subTitle = hit.title;
|
||
break;
|
||
}
|
||
}
|
||
return subTitle
|
||
? `${activeMenuTitle.value} / ${subTitle}`
|
||
: activeMenuTitle.value;
|
||
});
|
||
```
|
||
|
||
**渲染 AdminSubSider 时**:
|
||
|
||
```uvue
|
||
<AdminSubSider
|
||
v-if="activeGroups.length > 0"
|
||
:activeMenuTitle="activeMenuTitle"
|
||
:groups="activeGroups"
|
||
:activeSubId="activeSubId"
|
||
:activeMenuId="activeMenuId || 'home'"
|
||
@sub-click="onSubClick"
|
||
/>
|
||
```
|
||
|
||
---
|
||
|
||
## 第五部分:AdminSubSider 二级侧边栏
|
||
|
||
### 5.1 二级侧边栏的 groupAsChild 逻辑
|
||
|
||
**文件**: `layouts/admin/components/AdminSubSider.uvue`
|
||
|
||
**关键逻辑**:
|
||
|
||
```typescript
|
||
// 支持 group 作为菜单项(叶子节点)的点击处理
|
||
const handleGroupTitleClick = (group: MenuGroup) => {
|
||
// 如果 group 有 path,直接导航
|
||
if (group.path) {
|
||
go(group.path)
|
||
}
|
||
// 否则选中这个 group
|
||
else {
|
||
activeSubId.value = group.id
|
||
}
|
||
}
|
||
|
||
// 模板中
|
||
<template v-for="group in groups">
|
||
<view
|
||
v-if="!group.children || group.children.length === 0"
|
||
class="group-as-child"
|
||
@click="handleGroupTitleClick(group)"
|
||
:class="{ active: activeSubId === group.id }"
|
||
>
|
||
{{ group.title }}
|
||
</view>
|
||
<view v-else class="group-normal">
|
||
<!-- 正常的组处理 -->
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## 第六部分:状态管理(可选但推荐)
|
||
|
||
### 6.1 使用 state.uts 管理全局状态
|
||
|
||
**文件**: `layouts/admin/state.uts`
|
||
|
||
```typescript
|
||
import { ref } from "vue";
|
||
|
||
// 跨页面持久化的状态
|
||
export const tabs = ref<TabItem[]>([]);
|
||
export const activeTabId = ref("");
|
||
export const isCollapsed = ref(false);
|
||
export const hasNotification = ref(false);
|
||
```
|
||
|
||
**优点**:
|
||
|
||
- ✅ 页面切换时保持侧边栏收起/展开状态
|
||
- ✅ 标签页状态持久化
|
||
- ✅ 通知状态保持
|
||
|
||
---
|
||
|
||
## 完整检查清单
|
||
|
||
### 📋 新增页面时必须检查:
|
||
|
||
- [ ] **文件**
|
||
- [ ] 文件位置正确: `pages/mall/admin/maintain/<category>/<page-name>.uvue`
|
||
- [ ] 编码是 UTF-8(无 BOM)
|
||
- [ ] 正确的 SFC 结构
|
||
- [ ] `<script setup>` 导入了 `AdminLayout`
|
||
|
||
- [ ] **路由配置**
|
||
- [ ] 在 `pages.json` 中注册了路由
|
||
- [ ] `navigationStyle: "custom"` 设置正确
|
||
- [ ] `path` 与文件结构匹配
|
||
|
||
- [ ] **菜单配置**
|
||
- [ ] 在 `menu.uts` 中定义了菜单项
|
||
- [ ] menu id 唯一且命名规范
|
||
- [ ] 如果是叶子节点,设置 `children: []`
|
||
- [ ] 路径与 pages.json 匹配
|
||
|
||
- [ ] **页面文件**
|
||
- [ ] `currentPage` prop 值正确
|
||
- [ ] `currentPage` 与菜单配置中的 id 对应
|
||
|
||
- [ ] **缓存清理**
|
||
- [ ] 删除 `unpackage/dist`
|
||
- [ ] 删除 `.hbuilderx/cache`
|
||
- [ ] 清理浏览器缓存 (Ctrl+Shift+Delete)
|
||
- [ ] 强制刷新页面 (Ctrl+Shift+R)
|
||
|
||
---
|
||
|
||
## 调试技巧
|
||
|
||
### 验证导航匹配
|
||
|
||
```powershell
|
||
node test-system-info-nav.js
|
||
```
|
||
|
||
### 浏览器控制台诊断
|
||
|
||
```javascript
|
||
// 在浏览器 DevTools 中运行
|
||
const pages = getCurrentPages();
|
||
const route = pages[pages.length - 1]?.route;
|
||
console.log("当前路由:", route);
|
||
|
||
// 查看 AdminLayout 组件状态
|
||
// 打开 Vue DevTools 查看 activeMenuId, activeSubId
|
||
```
|
||
|
||
### 常见问题排查
|
||
|
||
| 问题 | 原因 | 解决 |
|
||
| ------------ | -------------------------- | -------------------------------- |
|
||
| 侧边栏不显示 | `currentPage` 未匹配到菜单 | 检查 nav.uts 匹配逻辑 |
|
||
| 高亮错误 | menu.id 不一致 | 确保 currentPage 与 menu id 相同 |
|
||
| 页面切换闪烁 | 缓存问题 | 清理缓存,强制刷新 |
|
||
| 编译错误 | 文件编码问题 | 重新创建文件,确保无 BOM |
|
||
| 组件无法解析 | @ 别名未配置 | 检查 tsconfig.json 的 paths |
|
||
|
||
---
|
||
|
||
## 总结:一句话版本
|
||
|
||
**确保 Admin 页面侧边栏一直显示的关键是**:
|
||
|
||
> 正确配置菜单结构 → 准确设置 currentPage → 实现导航匹配逻辑 → 同步状态到 AdminLayout → 支持叶子节点处理
|
||
|
||
---
|
||
|
||
## 参考文件
|
||
|
||
| 文件 | 职责 |
|
||
| ------------------------------------------------------- | ------------ |
|
||
| `pages/mall/admin/maintain/<category>/<page-name>.uvue` | 页面文件 |
|
||
| `pages.json` | 路由配置 |
|
||
| `layouts/admin/utils/menu.uts` | 菜单定义 |
|
||
| `layouts/admin/utils/nav.uts` | 导航匹配逻辑 |
|
||
| `layouts/admin/AdminLayout.uvue` | 布局容器 |
|
||
| `layouts/admin/components/AdminSubSider.uvue` | 二级侧边栏 |
|
||
| `layouts/admin/state.uts` | 全局状态 |
|
||
| `tsconfig.json` | 编译配置 |
|