663 lines
16 KiB
Markdown
663 lines
16 KiB
Markdown
|
|
# 校园新闻管理系统 - 完整技术文档
|
|||
|
|
|
|||
|
|
## 项目概述
|
|||
|
|
|
|||
|
|
校园新闻管理系统是一个基于 Vue 3 + TypeScript 的现代化前端应用,实现了基于角色和权限的动态路由、三层导航架构以及状态持久化功能。
|
|||
|
|
|
|||
|
|
## 目录
|
|||
|
|
|
|||
|
|
1. [快速开始](#快速开始)
|
|||
|
|
2. [系统架构](#系统架构)
|
|||
|
|
3. [导航系统](#导航系统)
|
|||
|
|
4. [权限控制](#权限控制)
|
|||
|
|
5. [开发指南](#开发指南)
|
|||
|
|
6. [配置示例](#配置示例)
|
|||
|
|
7. [常见问题](#常见问题)
|
|||
|
|
8. [技术栈](#技术栈)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 快速开始
|
|||
|
|
|
|||
|
|
### 环境要求
|
|||
|
|
|
|||
|
|
- Node.js 16+
|
|||
|
|
- npm 或 yarn
|
|||
|
|
|
|||
|
|
### 安装和运行
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装依赖
|
|||
|
|
yarn install
|
|||
|
|
|
|||
|
|
# 开发环境
|
|||
|
|
yarn serve
|
|||
|
|
|
|||
|
|
# 生产构建
|
|||
|
|
yarn build
|
|||
|
|
|
|||
|
|
# 代码检查
|
|||
|
|
yarn lint
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 项目结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
src/
|
|||
|
|
├── apis/ # API 接口
|
|||
|
|
├── assets/ # 静态资源
|
|||
|
|
├── components/ # 公共组件
|
|||
|
|
│ ├── TopNavigation.vue # 顶部导航栏
|
|||
|
|
│ ├── MenuNav.vue # 侧边栏菜单
|
|||
|
|
│ ├── MenuItem.vue # 菜单项
|
|||
|
|
│ ├── Breadcrumb.vue # 面包屑
|
|||
|
|
│ └── UserDropdown.vue # 用户下拉菜单
|
|||
|
|
├── directives/ # 自定义指令
|
|||
|
|
├── layouts/ # 布局组件
|
|||
|
|
│ ├── NavigationLayout.vue # 导航布局
|
|||
|
|
│ ├── BasicLayout.vue # 基础布局
|
|||
|
|
│ ├── BlankLayout.vue # 空白布局
|
|||
|
|
│ └── PageLayout.vue # 页面布局
|
|||
|
|
├── router/ # 路由配置
|
|||
|
|
├── store/ # Vuex 状态管理
|
|||
|
|
├── types/ # TypeScript 类型定义
|
|||
|
|
├── utils/ # 工具函数
|
|||
|
|
└── views/ # 页面组件
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 系统架构
|
|||
|
|
|
|||
|
|
### 核心设计理念
|
|||
|
|
|
|||
|
|
系统采用三层导航架构,通过 `MenuType` 枚举实现灵活的菜单显示控制:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export enum MenuType {
|
|||
|
|
SIDEBAR = 0, // 侧边栏菜单
|
|||
|
|
NAVIGATION = 1, // 顶部导航菜单
|
|||
|
|
BUTTON = 2, // 按钮(不生成路由)
|
|||
|
|
PAGE = 3 // 独立页面(不使用 NavigationLayout)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 布局判断逻辑
|
|||
|
|
|
|||
|
|
系统根据路由的 `meta.menuType` 自动决定布局:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// App.vue
|
|||
|
|
const useNavigationLayout = computed(() => {
|
|||
|
|
const menuType = route.meta?.menuType;
|
|||
|
|
return menuType !== MenuType.PAGE; // 只有 PAGE 类型不使用布局
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**规则**:
|
|||
|
|
- ✅ `menuType !== 3 (PAGE)` → 使用 NavigationLayout(有顶部导航栏)
|
|||
|
|
- ❌ `menuType === 3 (PAGE)` → 不使用布局(独立页面,如登录页)
|
|||
|
|
|
|||
|
|
### 页面渲染流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户访问页面
|
|||
|
|
↓
|
|||
|
|
App.vue 检查 route.meta.menuType
|
|||
|
|
↓
|
|||
|
|
┌─────────────────────┬──────────────────────┐
|
|||
|
|
│ menuType === 3 │ menuType !== 3 │
|
|||
|
|
│ (PAGE) │ (其他类型) │
|
|||
|
|
└─────────────────────┴──────────────────────┘
|
|||
|
|
↓ ↓
|
|||
|
|
独立渲染页面 使用 NavigationLayout
|
|||
|
|
(无导航栏) (有导航栏)
|
|||
|
|
↓ ↓
|
|||
|
|
router-view TopNavigation
|
|||
|
|
↓
|
|||
|
|
根据 menuType 显示:
|
|||
|
|
- NAVIGATION → 顶部导航项
|
|||
|
|
- SIDEBAR → 左侧边栏
|
|||
|
|
↓
|
|||
|
|
router-view
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 状态管理
|
|||
|
|
|
|||
|
|
#### 认证状态持久化
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 从 localStorage 恢复状态
|
|||
|
|
function getStoredState(): Partial<AuthState> {
|
|||
|
|
try {
|
|||
|
|
const token = localStorage.getItem('token');
|
|||
|
|
const loginDomainStr = localStorage.getItem('loginDomain');
|
|||
|
|
const menusStr = localStorage.getItem('menus');
|
|||
|
|
const permissionsStr = localStorage.getItem('permissions');
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
token: token || null,
|
|||
|
|
loginDomain: loginDomainStr ? JSON.parse(loginDomainStr) : null,
|
|||
|
|
menus: menusStr ? JSON.parse(menusStr) : [],
|
|||
|
|
permissions: permissionsStr ? JSON.parse(permissionsStr) : [],
|
|||
|
|
routesLoaded: false,
|
|||
|
|
};
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('从localStorage恢复状态失败:', error);
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 状态恢复机制
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
async restoreAuth({ state, commit, dispatch }) {
|
|||
|
|
try {
|
|||
|
|
if (!state.token) return false;
|
|||
|
|
|
|||
|
|
if (state.loginDomain && state.menus.length > 0) {
|
|||
|
|
console.log('从localStorage恢复登录状态');
|
|||
|
|
await dispatch('generateRoutes');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('恢复登录状态失败:', error);
|
|||
|
|
commit('CLEAR_AUTH');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 导航系统
|
|||
|
|
|
|||
|
|
### 三层导航架构
|
|||
|
|
|
|||
|
|
#### 第一层:顶部导航栏(MenuType.NAVIGATION)
|
|||
|
|
- 显示在所有页面的顶部
|
|||
|
|
- 作为主要的一级导航
|
|||
|
|
- 支持下拉菜单
|
|||
|
|
|
|||
|
|
#### 第二层:下拉菜单 / 侧边栏
|
|||
|
|
- **下拉菜单**:如果子菜单类型是 `MenuType.NAVIGATION`,则作为顶部导航的下拉选项
|
|||
|
|
- **侧边栏**:如果子菜单类型是 `MenuType.SIDEBAR`,则显示在页面左侧作为侧边栏
|
|||
|
|
|
|||
|
|
#### 第三层及更深:侧边栏子菜单
|
|||
|
|
- 在侧边栏中显示
|
|||
|
|
- 支持多层嵌套
|
|||
|
|
|
|||
|
|
### 菜单结构示例
|
|||
|
|
|
|||
|
|
#### 示例 1:新闻管理(带侧边栏)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"menuID": "news",
|
|||
|
|
"name": "新闻管理",
|
|||
|
|
"url": "/news",
|
|||
|
|
"type": 1, // NAVIGATION - 显示在顶部导航栏
|
|||
|
|
"component": "NavigationLayout",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"menuID": "news-article",
|
|||
|
|
"name": "文章管理",
|
|||
|
|
"url": "/news/article",
|
|||
|
|
"type": 0, // SIDEBAR - 显示在左侧边栏
|
|||
|
|
"component": "manage/news/ArticleManage.vue",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"menuID": "news-article-list",
|
|||
|
|
"name": "文章列表",
|
|||
|
|
"url": "/news/article/list",
|
|||
|
|
"type": 0, // SIDEBAR - 侧边栏子菜单
|
|||
|
|
"component": "manage/news/ArticleList.vue"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"menuID": "news-article-create",
|
|||
|
|
"name": "创建文章",
|
|||
|
|
"url": "/news/article/create",
|
|||
|
|
"type": 0, // SIDEBAR
|
|||
|
|
"component": "manage/news/ArticleCreate.vue"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"menuID": "news-category",
|
|||
|
|
"name": "分类管理",
|
|||
|
|
"url": "/news/category",
|
|||
|
|
"type": 0, // SIDEBAR - 显示在左侧边栏
|
|||
|
|
"component": "manage/news/CategoryManage.vue"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**效果**:
|
|||
|
|
- "新闻管理" 显示在顶部导航栏
|
|||
|
|
- 左侧侧边栏显示:
|
|||
|
|
- 文章管理
|
|||
|
|
- 文章列表
|
|||
|
|
- 创建文章
|
|||
|
|
- 分类管理
|
|||
|
|
|
|||
|
|
#### 示例 2:系统管理(带下拉菜单)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"menuID": "system",
|
|||
|
|
"name": "系统管理",
|
|||
|
|
"url": "/system",
|
|||
|
|
"type": 1, // NAVIGATION - 显示在顶部导航栏
|
|||
|
|
"component": "NavigationLayout",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"menuID": "system-user",
|
|||
|
|
"name": "用户管理",
|
|||
|
|
"url": "/system/user",
|
|||
|
|
"type": 1, // NAVIGATION - 作为下拉菜单
|
|||
|
|
"component": "manage/system/UserManage.vue"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"menuID": "system-role",
|
|||
|
|
"name": "角色管理",
|
|||
|
|
"url": "/system/role",
|
|||
|
|
"type": 1, // NAVIGATION - 作为下拉菜单
|
|||
|
|
"component": "manage/system/RoleManage.vue"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**效果**:
|
|||
|
|
- "系统管理" 显示在顶部导航栏,鼠标悬停显示下拉菜单
|
|||
|
|
- 下拉菜单包含:用户管理、角色管理
|
|||
|
|
- 没有侧边栏
|
|||
|
|
|
|||
|
|
### 路由生成流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
后端返回菜单数据
|
|||
|
|
↓
|
|||
|
|
Vuex Store 保存到 state.auth.menus
|
|||
|
|
↓
|
|||
|
|
持久化到 localStorage
|
|||
|
|
↓
|
|||
|
|
generateRoutes() 函数处理
|
|||
|
|
↓
|
|||
|
|
根据 type 和层级决定使用的布局
|
|||
|
|
↓
|
|||
|
|
生成 Vue Router 路由配置
|
|||
|
|
↓
|
|||
|
|
router.addRoute() 动态添加路由
|
|||
|
|
↓
|
|||
|
|
设置 routesLoaded = true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 静态路由与动态路由合并
|
|||
|
|
|
|||
|
|
#### buildMenuTree 流程
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function buildMenuTree(menus: SysMenu[]) {
|
|||
|
|
// 1. 将静态路由转换为菜单项
|
|||
|
|
const staticMenus = convertRoutesToMenus(routes);
|
|||
|
|
|
|||
|
|
// 2. 合并静态菜单和动态菜单
|
|||
|
|
const allMenus = [...staticMenus, ...menus];
|
|||
|
|
|
|||
|
|
// 3. 构建树结构并排序(按 orderNum)
|
|||
|
|
return sortMenus(rootMenus);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 避免路由重复
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
function convertRoutesToMenus(routes: RouteRecordRaw[]) {
|
|||
|
|
routes.forEach(route => {
|
|||
|
|
if (route.meta?.menuType !== undefined) {
|
|||
|
|
const menu: SysMenu = {
|
|||
|
|
// ...
|
|||
|
|
component: '__STATIC_ROUTE__', // 特殊标记
|
|||
|
|
};
|
|||
|
|
menus.push(menu);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function generateRouteFromMenu(menu: SysMenu) {
|
|||
|
|
// 跳过按钮类型
|
|||
|
|
if (menu.type === MenuType.BUTTON) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 跳过静态路由(避免重复添加)
|
|||
|
|
if (menu.component === '__STATIC_ROUTE__') {
|
|||
|
|
return null; // ✅ 不生成路由
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 正常生成动态路由
|
|||
|
|
return route;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 权限控制
|
|||
|
|
|
|||
|
|
### 权限指令使用
|
|||
|
|
|
|||
|
|
#### v-permission 指令
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<!-- 单个权限 -->
|
|||
|
|
<el-button v-permission="'user:create'">新增用户</el-button>
|
|||
|
|
|
|||
|
|
<!-- 多个权限(任意一个) -->
|
|||
|
|
<el-button v-permission="['user:create', 'user:edit']">操作</el-button>
|
|||
|
|
|
|||
|
|
<!-- 多个权限(必须全部拥有) -->
|
|||
|
|
<el-button v-permission.all="['user:create', 'user:edit']">操作</el-button>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### v-role 指令
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<!-- 单个角色 -->
|
|||
|
|
<div v-role="'admin'">管理员内容</div>
|
|||
|
|
|
|||
|
|
<!-- 多个角色 -->
|
|||
|
|
<div v-role="['admin', 'moderator']">管理内容</div>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Composition API 使用
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<script setup>
|
|||
|
|
import { usePermission } from '@/directives/permission';
|
|||
|
|
|
|||
|
|
const { hasPermission, hasAnyPermission, hasRole } = usePermission();
|
|||
|
|
|
|||
|
|
// 检查权限
|
|||
|
|
if (hasPermission('user:create')) {
|
|||
|
|
// 有权限的逻辑
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查角色
|
|||
|
|
if (hasRole('admin')) {
|
|||
|
|
// 管理员逻辑
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 路由守卫
|
|||
|
|
|
|||
|
|
系统自动设置了路由守卫:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
async function handleRouteGuard(
|
|||
|
|
to: RouteLocationNormalized,
|
|||
|
|
from: RouteLocationNormalized,
|
|||
|
|
next: NavigationGuardNext,
|
|||
|
|
store: Store<any>
|
|||
|
|
) {
|
|||
|
|
const authState: AuthState = store.state.auth;
|
|||
|
|
const isAuthenticated = store.getters['auth/isAuthenticated'];
|
|||
|
|
|
|||
|
|
if (isInWhiteList(to.path)) {
|
|||
|
|
return next();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (authState.token && !isAuthenticated) {
|
|||
|
|
try {
|
|||
|
|
const restored = await store.dispatch('auth/restoreAuth');
|
|||
|
|
if (restored) {
|
|||
|
|
return next({ ...to, replace: true });
|
|||
|
|
} else {
|
|||
|
|
return next({ path: '/login', query: { redirect: to.fullPath } });
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
return next({ path: '/login', query: { redirect: to.fullPath } });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!isAuthenticated) {
|
|||
|
|
return next({ path: '/login', query: { redirect: to.fullPath } });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!authState.routesLoaded) {
|
|||
|
|
try {
|
|||
|
|
await store.dispatch('auth/generateRoutes');
|
|||
|
|
return next({ ...to, replace: true });
|
|||
|
|
} catch (error) {
|
|||
|
|
store.commit('auth/CLEAR_AUTH');
|
|||
|
|
return next('/login');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const hasPermission = await checkPagePermission(to, store);
|
|||
|
|
if (!hasPermission) {
|
|||
|
|
return next('/403');
|
|||
|
|
}
|
|||
|
|
next();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 开发指南
|
|||
|
|
|
|||
|
|
### 组件说明
|
|||
|
|
|
|||
|
|
#### TopNavigation.vue
|
|||
|
|
顶部导航栏组件,功能包括:
|
|||
|
|
- 显示 Logo
|
|||
|
|
- 显示一级 NAVIGATION 菜单
|
|||
|
|
- 支持下拉菜单(二级 NAVIGATION 菜单)
|
|||
|
|
- 显示用户信息和登出功能
|
|||
|
|
|
|||
|
|
#### NavigationLayout.vue
|
|||
|
|
导航布局组件,功能包括:
|
|||
|
|
- 包含 TopNavigation
|
|||
|
|
- 根据当前路由动态显示侧边栏(SIDEBAR 类型的子菜单)
|
|||
|
|
- 支持侧边栏折叠
|
|||
|
|
- 显示面包屑导航
|
|||
|
|
|
|||
|
|
#### MenuNav.vue
|
|||
|
|
菜单导航组件,用于渲染侧边栏菜单树
|
|||
|
|
|
|||
|
|
### 开发建议
|
|||
|
|
|
|||
|
|
1. **第一层菜单**:使用 `MenuType.NAVIGATION`,这是顶部导航
|
|||
|
|
2. **第二层菜单**:
|
|||
|
|
- 如果是简单的操作页面(如用户管理、角色管理),使用 `MenuType.NAVIGATION` 作为下拉菜单
|
|||
|
|
- 如果需要更复杂的子菜单结构,使用 `MenuType.SIDEBAR` 显示侧边栏
|
|||
|
|
3. **第三层及更深**:使用 `MenuType.SIDEBAR`,在侧边栏中嵌套显示
|
|||
|
|
4. **按钮**:不需要路由的权限按钮使用 `MenuType.BUTTON`
|
|||
|
|
|
|||
|
|
### 菜单配置规则
|
|||
|
|
|
|||
|
|
1. **第一层菜单**:
|
|||
|
|
- 必须使用 `type: 1`(NAVIGATION)
|
|||
|
|
- 会显示在顶部导航栏
|
|||
|
|
|
|||
|
|
2. **第二层菜单**:
|
|||
|
|
- `type: 1`(NAVIGATION)→ 显示为下拉菜单选项
|
|||
|
|
- `type: 0`(SIDEBAR)→ 显示在侧边栏
|
|||
|
|
|
|||
|
|
3. **第三层及更深**:
|
|||
|
|
- 通常使用 `type: 0`(SIDEBAR)
|
|||
|
|
- 在侧边栏中嵌套显示
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 配置示例
|
|||
|
|
|
|||
|
|
### 静态路由配置(src/router/index.ts)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export const routes = [
|
|||
|
|
// PAGE 类型 - 不使用布局
|
|||
|
|
{
|
|||
|
|
path: "/login",
|
|||
|
|
meta: { menuType: 3 }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
path: "/404",
|
|||
|
|
meta: { menuType: 3 }
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// NAVIGATION 类型 - 使用布局,显示在导航栏
|
|||
|
|
{
|
|||
|
|
path: "/home",
|
|||
|
|
meta: { menuType: 1, orderNum: -1 }
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 动态路由数据(后端返回)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"menuID": "dashboard",
|
|||
|
|
"name": "工作台",
|
|||
|
|
"type": 1,
|
|||
|
|
"url": "/dashboard/workplace",
|
|||
|
|
"orderNum": 0
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"menuID": "news",
|
|||
|
|
"name": "新闻管理",
|
|||
|
|
"type": 1,
|
|||
|
|
"url": "/news",
|
|||
|
|
"orderNum": 1,
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"menuID": "news-article",
|
|||
|
|
"name": "文章管理",
|
|||
|
|
"type": 0,
|
|||
|
|
"url": "/news/article"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 数据库字段说明
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE sys_menu (
|
|||
|
|
menu_id VARCHAR(50) PRIMARY KEY COMMENT '菜单ID',
|
|||
|
|
parent_id VARCHAR(50) COMMENT '父菜单ID',
|
|||
|
|
name VARCHAR(50) NOT NULL COMMENT '菜单名称',
|
|||
|
|
description VARCHAR(200) COMMENT '菜单描述',
|
|||
|
|
url VARCHAR(200) COMMENT '菜单URL',
|
|||
|
|
component VARCHAR(200) COMMENT '组件路径',
|
|||
|
|
icon VARCHAR(50) COMMENT '菜单图标',
|
|||
|
|
order_num INT DEFAULT 0 COMMENT '排序号',
|
|||
|
|
type TINYINT NOT NULL DEFAULT 0 COMMENT '菜单类型:0-侧边栏 1-导航栏 2-按钮 3-独立页面',
|
|||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|||
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
|||
|
|
creator VARCHAR(50) COMMENT '创建人',
|
|||
|
|
updater VARCHAR(50) COMMENT '更新人'
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
### Q1: 顶部导航没有显示
|
|||
|
|
- 检查菜单数据是否正确加载到 Vuex store
|
|||
|
|
- 检查第一层菜单的 `type` 是否为 `1`(NAVIGATION)
|
|||
|
|
- 打开浏览器控制台,执行:
|
|||
|
|
```js
|
|||
|
|
console.log($store.getters['auth/menuTree'])
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q2: 侧边栏没有显示
|
|||
|
|
- 检查第二层菜单的 `type` 是否为 `0`(SIDEBAR)
|
|||
|
|
- 如果第二层都是 `type: 1`(NAVIGATION),则不会显示侧边栏(这是预期行为)
|
|||
|
|
|
|||
|
|
### Q3: 点击菜单没有跳转
|
|||
|
|
- 检查菜单的 `url` 字段是否正确
|
|||
|
|
- 检查对应的组件 `component` 字段是否存在
|
|||
|
|
- 检查组件文件是否真实存在
|
|||
|
|
|
|||
|
|
### Q4: 页面刷新后侧边栏消失
|
|||
|
|
- 检查 localStorage 中是否保存了菜单数据
|
|||
|
|
- 检查路由守卫是否正确恢复了登录状态
|
|||
|
|
- 打开控制台查看是否有错误信息
|
|||
|
|
|
|||
|
|
### Q5: 路由生成失败
|
|||
|
|
- 检查菜单数据结构是否正确
|
|||
|
|
- 检查 `component` 字段指向的组件文件是否存在
|
|||
|
|
- 查看控制台的错误信息
|
|||
|
|
|
|||
|
|
### Q6: 如何隐藏某个页面的侧边栏?
|
|||
|
|
A: 将该菜单及其兄弟菜单都设置为 `MenuType.NAVIGATION` 类型。
|
|||
|
|
|
|||
|
|
### Q7: 如何让某个菜单在顶部和侧边栏都显示?
|
|||
|
|
A: 在第二层使用 `MenuType.NAVIGATION`(显示在下拉菜单),然后它的子菜单使用 `MenuType.SIDEBAR`(显示在侧边栏)。
|
|||
|
|
|
|||
|
|
### Q8: 可以有多少层菜单?
|
|||
|
|
A: 理论上无限制,但建议不超过4层以保持良好的用户体验。
|
|||
|
|
|
|||
|
|
### Q9: 按钮类型的菜单有什么用?
|
|||
|
|
A: `MenuType.BUTTON` 类型的菜单不会生成路由,主要用于页面内的操作按钮权限控制,例如"删除"、"编辑"等按钮。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 技术栈
|
|||
|
|
|
|||
|
|
- **Vue 3**:Composition API + `<script setup>`
|
|||
|
|
- **TypeScript**:类型安全
|
|||
|
|
- **Vue Router 4**:动态路由
|
|||
|
|
- **Vuex**:状态管理
|
|||
|
|
- **SCSS**:样式预处理
|
|||
|
|
- **Element Plus**:UI 组件库
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 性能优化
|
|||
|
|
|
|||
|
|
1. **组件懒加载**:所有页面组件都使用动态导入
|
|||
|
|
2. **状态缓存**:登录信息缓存到 localStorage,减少重复请求
|
|||
|
|
3. **路由缓存**:动态路由只在必要时重新生成
|
|||
|
|
4. **CSS 优化**:使用 scoped 样式,避免全局污染
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 浏览器兼容性
|
|||
|
|
|
|||
|
|
- ✅ Chrome 90+
|
|||
|
|
- ✅ Firefox 88+
|
|||
|
|
- ✅ Safari 14+
|
|||
|
|
- ✅ Edge 90+
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 版本历史
|
|||
|
|
|
|||
|
|
### v1.0.0 (2025-10-08)
|
|||
|
|
- ✅ 初始版本发布
|
|||
|
|
- ✅ 实现三层导航架构
|
|||
|
|
- ✅ 支持动态路由生成
|
|||
|
|
- ✅ 支持状态持久化
|
|||
|
|
- ✅ 完整的文档和示例
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 许可证
|
|||
|
|
|
|||
|
|
请根据项目实际情况添加许可证信息。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**祝使用愉快!如有问题,请参考文档或联系开发团队。**
|