# 校园新闻管理系统 - 完整技术文档 ## 项目概述 校园新闻管理系统是一个基于 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 { 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 新增用户 操作 操作 ``` #### v-role 指令 ```vue
管理员内容
管理内容
``` ### Composition API 使用 ```vue ``` ### 路由守卫 系统自动设置了路由守卫: ```typescript async function handleRouteGuard( to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, store: Store ) { 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 + `