# 校园新闻管理系统 - 完整技术文档

## 项目概述

校园新闻管理系统是一个基于 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)
- ✅ 初始版本发布
- ✅ 实现三层导航架构
- ✅ 支持动态路由生成
- ✅ 支持状态持久化
- ✅ 完整的文档和示例

---

## 许可证

请根据项目实际情况添加许可证信息。

---

**祝使用愉快！如有问题，请参考文档或联系开发团队。**
