Files
schoolNews/schoolNewsWeb/.docs/完整系统文档.md
2025-10-08 14:11:54 +08:00

663 lines
16 KiB
Markdown
Raw Permalink 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.

# 校园新闻管理系统 - 完整技术文档
## 项目概述
校园新闻管理系统是一个基于 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)
- ✅ 初始版本发布
- ✅ 实现三层导航架构
- ✅ 支持动态路由生成
- ✅ 支持状态持久化
- ✅ 完整的文档和示例
---
## 许可证
请根据项目实际情况添加许可证信息。
---
**祝使用愉快!如有问题,请参考文档或联系开发团队。**