项目篇-项目梳理(Vue3+TS实现后台管理系统)
写在前面
项目主要功能虽然都做完了,但是发现不会把它写在简历上,换句话说,我不知道如何介绍项目的功能与实现,这篇文章是我对项目的的梳理
完整的项目笔记见vue3+t后台管理系统项目笔记
项目STAR法则:
最好的技术表达,不是复述工具名称,而是说清:你为何这样选择,以及它解决了什么问题
(感谢大模型老师对我学习STAR法则的帮助)
技术栈
核心框架和工具:
Vue 3
TypeScript
pnpm 包管理器
Vite 构建工具
Pinia 状态管理
Vue Router 路由管理
UI 框架:
Element Plus
其他重要依赖:
axios HTTP请求
echarts 图表库
countup.js 数字动画
dayjs 日期处理
登录模块
核心实现
Element Plus组件化登录架构
- 采用组件化思想拆解登录页为
登录面板容器
+账号登录
+手机登录
三大模块 - 采用
el-tabs
实现账号/手机登录模式切换,通过v-model="activeName"
动态控制激活面板 el-link
el-checkbox
el-button
el-message
el-icon
+ 按需引入配置
- 采用组件化思想拆解登录页为
账号密码表单校验
- 基于
el-form
+el-input
组件构建表单体系,配置可扩展的rules
验证规则集
- 基于
组件通信机制
- 通过
defineExpose
暴露子组件方法,实现父组件触发登录验证,解决父子组件通信问题
- 通过
类型抽取与token常量安全
- 抽离
IAccount
核心对象类型,便于复用,约束账号密码结构 - 用常量管理关键字段(如
LOGIN_TOKEN
),降低维护成本
- 抽离
Token保存与Cache封装
- 设计
localStorage + Pinia
双缓存持久化Token - 封装通用缓存工具类
cache.ts
,实现JSON.stringify()`` JSON.parse()
自动处理对象数据,提升代码复用性与健壮性
- 设计
权限管理模块
核心实现:
- 通过 Axios 请求拦截器自动携带 token
- 避免手动添加 token 的冗余操作,提升开发效率
- 导航守卫设计
- 校验
/main
开头的路径,未登录用户强制跳转至登录页,防止越权访问\
- 校验
- 用户-角色-菜单映射机制
- 登录成功后,先根据用户ID获取用户信息,再通过角色ID请求菜单数据
- 对用户的userInfo和userMenu数据进行本地缓存和store缓存
- 动态路由(基于菜单管理)
- 通过
mapMenusToRoutes
工具将菜单路径转换为路由对象 - 使用
addRoute
动态将子路由添加到main路由下,实现不同角色加载不同菜单与路由
- 通过
STAR法则描述:
S(Situation)- 项目背景
系统需要实现基于角色的权限控制,用户登录后需根据角色动态加载菜单与路由,并保障敏感页面的访问权限
T(Task) - 任务目标
认证与权限校验:登录成功后自动携带token,拦截无权限访问;
动态路由生成:根据用户角色加载对应的菜单与路由;
数据持久化:缓存用户信息与菜单数据,避免重复请求
A(Action) - 技术实现
- Token 拦截与权限校验
- 登录成功后通过 Axios 拦截器自动携带 token,确保后续请求权限校验;
- 在 router.beforeEach 中校验 /main 开头的路径,未登录用户重定向至登录页。
- RBAC 权限控制
- 用户信息获取:根据用户 ID 请求用户详情(含角色 ID);
- 菜单权限加载:通过角色 ID 获取菜单数据(userMenus),结合 mapMenusToRoutes 将菜单路径映射为路由对象;
- 动态路由添加:使用 Vue Router 的 addRoute 方法将路由动态添加到 main 路由下。
- 数据缓存与状态管理
用户信息和菜单数据通过 Pinia 存储到 loginStore,并通过 localStorage 持久化,确保页面刷新后状态不丢失。
R(Result) - 成果与价值
- 数据缓存与状态管理
- 实现按角色动态加载菜单与路由,不同用户访问权限隔离;
- 权限实时性:切换角色时自动刷新路由,保障权限一致性;
- 开发效率提升:新增菜单只需维护文件结构,无需手动修改路由配置
动态路由权限系统
技术栈:Vue Router + Vite Glob API + 递归算法
核心实现
- 自动化路由收集
- 使用 Vite 的
import.meta.glob
动态加载路由模块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120const files = import.meta.glob('../router/main/**/*.ts', { eager: true })
for (const key in files) {
localRoutes.push(files[key].default)
}
```
- 实现路由模块 **零手工注册**,新增路由文件自动识别
2. **智能路由映射**
- 递归匹配用户菜单与本地路由路径:
``` ts
const route = localRoutes.find(item => item.path === submenu.url)
```
- 双层级路由注入策略:
```ts
// 一级菜单重定向
routes.push({ path: menu.url, redirect: route.path })
// 二级菜单路由
routes.push(route)
```
3. **路由辅助工具集**
- **路径映射菜单**:`mapPathToMenu()` 实现路由高亮联动
- **智能面包屑**:`mapPathToBreadcrumbs()` 自动生成导航路径
- **权限ID提取**:`mapMenuListToIds()` 递归获取叶子节点ID
- **按钮权限控制**:`mapMenusToPermissions()` 筛选 type=3 权限标识
4. **首屏优化机制**
- 记录 `firstMenu` 实现首页自动重定向
- 避免用户登录后卡在空白主页
### 动态路由:
**基于角色的动态路由**:缺点添加角色要重新生成新的角色对应菜单数组,前端不好生成。
**采用基于菜单的动态路由**:根据菜单动态添加(动态渲染菜单,再根据菜单动态添加路由)
在router/main文件夹下按照路径创建每个子文件夹,和页面的组织路径一致,每个里面只放单独的对象并导出。路由和页面路径组织一致
ts中路由对象数据类型RouterRecordRaw[]
具体菜单转路由的实现封装在了工具类utils/map-menus.ts的`mapMenusToRoutes`方法中
> 1. 获取菜单
> 2. 动态获取所有路由对象(目前路由对象都在独立的文件中),从文件中将所有的路由对象先读取到数组中
> 3. 根据菜单去匹配正确的路由`router.add('main',xxx)`
# 首页模块
技术栈
## 核心实现:
1. **组件化首页布局设计**
- 整体布局ElContainer; 侧边栏菜单Menu组件;头部Header组件
2. **组件化菜单交互机制**
- 通过Header子组件的图标的切换,实现动态控制菜单组件的折叠与展开
- 父子组件通信:Header组件通过`defineEmits`触发折叠事件,父组件监听事件并更新折叠状态`isFold`
- 父组件根据`isFold`状态控制菜单组件的宽度
- 父子组件通信:父组件通过`props`将`isFold`传递给Menu子组件,控制`el-menu`的`collapse`属性实现折叠与展开
3. **菜单与路由动态绑定**
- 实现菜单点击与页面跳转的无缝衔接,通过router.push(submenu.url)跳转
### STAR法则描述:
**S(Situation) - 项目背景**
需要构建可交互的首页界面,包含侧边栏菜单、头部导航及动态折叠功能,提升用户体验。
**T(Task) - 任务目标**
首页布局搭建:使用 ElContainer 构建容器布局;
菜单动态渲染:根据 userMenus 动态生成侧边栏菜单;
菜单折叠交互:通过组件通信实现菜单的折叠/展开控制。
**A(Action) - 技术实现**
- 首页布局设计
使用ElContainer 分为三部分:Header(顶部导航);Aside(侧边栏菜单);Main(内容区域)
- 动态菜单渲染
菜单数据驱动:通过 v-for 遍历 userMenus 动态生成菜单项;
菜单嵌套结构:使用 el-menu 和 el-sub-menu 实现多级菜单。
- 菜单折叠交互
Header 组件:声明 isFold 状态,通过 emit('fold-change') 通知父组件折叠状态;
父组件(main.vue):监听 fold-change 事件,动态调整 aside 宽度(60px 折叠 / 210px 展开);
Menu 组件:接收 isFold 属性,通过 `:collapse="isFold"` 控制菜单折叠状态;
图标切换优化:使用动态组件 `<component :is="isFold ? 'Expand' : 'Fold'" />` 根据 isFold 显示不同图标,减少冗余代码。
**R(Result) - 成果与价值**
- 布局灵活性:通过 ElContainer 快速搭建响应式首页结构;
- 交互流畅性:菜单折叠/展开操作流畅,提升用户体验;
- 组件解耦:通过 props 和 emit 实现组件间通信,代码结构清晰。
# 项目架构优化:组件复用与网络请求封装
## 核心实现:
1. **网络请求三层复用架构**
- 三层架构:页面 → Store → Service
- 页面层:通过pageName参数传递业务标识(如users/departments),触发增删查改操作
- Store 层:统一管理请求逻辑,携带pageName调用 Service
- Service 层:基于pageName动态拼接 URL,实现接口通用化
通过 config.ts 传递 pageName,实现不同业务模块的请求复用
2. **高阶组件复用方案**
- 消除重复代码:将搜索表单、表格内容、操作按钮等通用组件抽离为可配置组件
- Search 组件:动态生成搜索表单(支持 Input / 日期选择等组件)
- Content 组件:动态渲染表格列(支持自定义插槽、操作列)
- Modal 组件:通过配置文件定义表单字段及联动逻辑
- 动态配置驱动:通过search.config.ts和content.config.ts定义不同模块页面的展示规则
### STAR法则描述:
**Situation(情境)**
在项目初期,存在以下痛点:
- **重复开发问题**:用户/部门/角色等模块功能相似,但代码高度重复(搜索表单、表格、操作逻辑等均需手动复制粘贴)。
- **接口不规范**:不同业务模块的接口 URL 差异大,导致请求逻辑分散且难以维护。
- **开发效率低**:每次新增模块需重新编写通用功能(如分页查询、表单校验),耗时且易出错。
**Task(任务)**
目标是通过架构优化实现以下改进:
1. **统一网络请求逻辑**:封装通用请求层,支持多业务模块复用。
2. **组件化开发**:将通用功能(搜索、表格、表单)抽象为可配置组件。
3. **动态配置驱动**:通过配置文件控制不同模块的展示规则,减少硬编码。
**Action(行动)**
1. **网络请求三层架构设计**
- **页面层**:通过 `pageName` 参数标识业务模块(如 `users`、`departments`),触发增删改查操作。
- **Store 层**:封装请求逻辑,接收 `pageName` 并调用 Service 层接口,统一处理响应数据。
- **Service 层**:动态拼接 URL(如 `/${pageName}/list`),实现接口通用化。
- **动态配置**:通过 `config.ts` 传递 `pageName`,实现不同业务模块的请求复用。
```ts
// 示例:动态拼接 URL
export function postPageListData(pageName: string, queryInfo: any) {
return hyRequest.post(`/${pageName}/list`, queryInfo)
}
- 使用 Vite 的
- 高阶组件复用方案
Search 组件:
- 动态生成搜索表单(支持
Input
、DatePicker
等组件),通过search.config.ts
定义字段。 - 支持默认值、占位符、类型判断等配置项。
- 动态生成搜索表单(支持
Content 组件:
- 动态渲染表格列(支持自定义插槽、操作列、时间格式化等)。
- 通过
content.config.ts
定义列名、数据类型、操作按钮逻辑。
Modal 组件:
- 通过配置文件定义表单字段及联动逻辑(如下拉选项动态获取部门列表)。
1 | // 示例:动态配置搜索表单 |
- 动态配置驱动开发
- 所有动态内容(表单字段、表格列、按钮文本)通过配置文件定义,无需修改组件代码。
- 通过
ref
和props
实现组件间通信,统一处理查询、重置、新增、编辑等操作。
- Hook 抽离与逻辑复用
- 将兄弟组件通信逻辑(如
handleQueryClick
、handleEditClick
)抽离为usePageContent
Hook,提升代码复用性。
Result
1. 开发效率显著提升
- 新增模块仅需编写
xxx.vue
+ 3 个配置文件(search.config.ts
、content.config.ts
、modal.config.ts
)。 - **开发时间减少 50%+**:通过复用组件和动态配置,避免重复开发通用功能。
2. 维护成本大幅降低
- **网络请求复用率 > 90%**:统一的三层架构适配所有业务模块。
- 代码结构清晰:通过配置文件驱动 UI,业务逻辑与展示解耦,修改只需调整配置。
3. 可扩展性增强
- 新增模块无需修改核心组件:仅需调整配置文件即可适配新需求。
- 支持动态联动:如 Modal 中的下拉选项可动态获取部门数据,实现跨模块交互。
4. 技术价值
- 配置优先原则:所有动态内容通过配置文件驱动,符合现代前端工程化趋势。
- 组件解耦设计:业务逻辑与 UI 展示分离,组件职责单一,易于测试与维护。
数据可视化模块
核心实现:
- 动态数据展示
- 轻量级库 CountUp.js 实现数字滚动效果(如商品销量从起始值平滑过渡至目标值)
- 支持自定义前缀(如人民币符号¥),通过配置项动态切换展示格式
- 使用 Element Plus Tooltip 组件,鼠标悬停时展示数据说明
- ECharts图表三层封装
- 第一层封装:基础组件封装,统一图表初始化与更新逻辑
- 第二层封装:不同类型的echarts图标封装不同的子组件(如pie-echart,rose-echart)
- 第三层封装:抽离数据,把数据部分单独封装,传入第二层封装的options里(数据从service里面拿到,然后放到store里,再传到页面,页面再传递给它调用的组件里)
STAR法则描述:
Situation
- 数据展示需求:
需要构建一个 数据可视化仪表盘,展示商品销量、销售额等核心指标,并通过动态动画和图表增强数据表现力。 - 技术挑战:
静态数据问题:初期页面为静态展示,缺乏动态增长效果,用户体验单一。
图表开发重复:ECharts 图表需针对不同数据类型重复编写配置代码。
状态管理分散:数据从接口获取后未集中管理,导致页面逻辑复杂。
Task项目目标 - 实现动态数据可视化:
使用 CountUp.js 实现数字递增动画,增强用户感知。
通过 ECharts 展示饼图、柱状图等多类型图表。 - 组件化与复用:
封装通用图表组件,支持动态数据绑定。
通过 props 传递配置,实现组件灵活复用。 - 状态集中管理:
使用 Pinia Store 管理数据,分离数据获取与展示逻辑
Action
- 动态数字增长动画实现:
选择 CountUp.js(轻量级、无依赖)实现数字增长动画。 - ECharts 图表封装
- 三层封装架构:
- 基础组件(BaseECharts):封装 echarts.init() 和通用配置,支持响应式更新。
- 图表类型组件(PieECharts, BarECharts):针对不同图表类型扩展配置,示例(饼图组件)
- 数据驱动组件:通过 computed 计算图表配置项,并使用 watchEffect 监听数据变化:
- 状态管理与组件通信
数据从service里面拿到,然后放到store里,再传到页面,页面再传递给它调用的组件里
Result
- 用户体验提升:
动态增长动画:数字递增效果增强数据表现力,提升用户感知。
交互优化:Tooltip 提示和响应式图表提升操作友好度。 - 开发效率与可维护性:
组件复用率 > 90%:通过配置文件驱动图表和卡片组件,避免重复开发。
代码结构清晰:数据获取、状态管理、UI 展示解耦,维护成本降低。 - 技术价值:
轻量级动画库:CountUp.js 无依赖、性能优,适合快速实现数值动画。
ECharts 高度封装:支持多类型图表动态配置,适配不同业务场景。
大模型模拟的项目面试题
- https://www.doubao.com/thread/wbfa26b13c725686d
- https://lxblog.com/qianwen/share?shareId=44568676-1c0c-4273-88b3-2a7a1ead78de
其他:网络请求是如何处理的
项目采用老师封装好的Axios实例(诚实说明),包含请求拦截器自动添加Token,响应拦截器统一处理错误码
项目中网络请求层采用教学提供的封装方案,我的任务重点是:
业务逻辑对接:
1
2
3
4
5
6// 在Pinia store中调用封装好的post方法
loginAccountAction(account: IAccount) {
post('/login', account).then(res => {
localCache.setCache(LOGIN_TOKEN, res.token)
})
}
零碎梳理版(按项目实现顺序梳理)
登录模块
Element-Plus集成:
按需引入,要进行vite插件配置
ElMessage引入;图标引入
后续tab布局、按钮、span、复选选项、账号密码验证、小图标都是通过element-plus组件库组件实现的
登录页面Panel
整体界面搭建
标题,tabs,记住密码复选框、忘记密码链接,立即登录按钮
背景搭建:100vw/vh
登录tabs:
label插槽的使用
tab属性栏如何判断在哪个pane标签页:<el-tabs 绑定v-model="activeNave">
`const activeName=ref(‘account’) //默认选中account
两个el-tabs-pane 添加name属性
用activeName判断是账号登录还是手机登录
账号登录form
账号密码输入框:
表单组件el-form/el-form-item/el-form-input来搭建
form的校验规则
表单校验规则配置在el-form上在el-form的 :rules=”rules”属性上,校验规则rules模仿官方文档
ref为了拿到el-from
el-form label-with=”表单宽度px”
点击立即登录
父亲组件发生点击,执行子组件的函数
defineExpose()导出函数
重点代码:const accountRef=ref<instanceType<type PaneAccount>>
登录的操作
form通过校验
- 通过formRef.validate(回调)
1
2
3
4
5
6
7
8
9function loginAction(){
formRef.value?.validate((valid)=>{
if(valid){//获取用户输入的帐号和密码
const name = account.name
const password = account.password
//2.向服务器发送网络请求(携带账号和密码)
loginstore.loginAccountAction({ name,password })}
else {
ElMessage.error('oops,请您输入正确的格式后再操作')}})} - 登录接口调用post请求;
- data:{name,password}
将登录的操作放到store中
IAccount类型的定义(类型复用封装):
类型复用抽取:
一个对象类型在很多地方复用时,抽到type里
1 | export interface IAccount{ |
使用导入const type {IAccout} from '@/types'
function balabalafn(account:IAccount){}
细节—教训:token这种字符串常量,抽取成常量,避免拼写错误const LOGIN_TOKEN='login/token'
token缓存和cache封装
永久保存用户token
可以localStorage-会话关闭依然保留,除非手动删
sessionStorage-会话关闭会消失
store/state中toke:localStorage.getItem(‘token’)??’’
保存token图片()封装前
封装本地缓存工具:
utils/cache.ts,封装了localStorage和sessionStorage的方法
原因,如果要保存对象的话不可以直接localStorage.setItem('userInfo',{name:'Nie',password:'123456'})//{object:object}
需要localStorage.setItem('userInfo',JSON.stringify({}))
获取数据同理
这就是“自动序列化/反序列化”:工具内部自动处理对象与字符串的转换
进行封装方便处理对象数据
登录模块完善
在拦截器添加携带token
导航守卫(对main开头的页面进行token判断)
完善–记住秘密功能
main权限管理–RBAC基于角色访问控制(权限管理)
请求用户信息–根据用户获取用户信息;
请求菜单信息–根据用户信息中的角色id,获取菜单信息
对用户信息和用户菜单进行本地缓存
首页界面搭建
整体布局ElContainer
侧边栏菜单Menu组件
- 分组ElMenu组件的作用
- 手动搭建整个菜单结构
- 根据userMenu动态遍历渲染
头部Header组件
menu-icon图标(折叠菜单功能)
- 动态组件切换图标展示
- 父组件--切换aside宽度
- 兄弟组件--菜单的折叠与展开
折叠菜单详细梳理(组件通信)
header组件图标控制兄弟组件(菜单栏)的展开与折叠;控制父组件对menu组件宽度的调整
涉及的部分:
- header组件--声明isFold变量记录展开/折叠的状态
- header组件--用动态组件实现不同状态的图标切换(这样不需要v-if,优化代码)
<component :is="isFold ? 'Expand' : 'Fold'" />
- el-menu组件--有控制菜单展示与折叠的属性
:collapse="true/false"
, - 父组件--菜单的宽度是自己设置的,要根据header子组件的状态,动态设置菜单宽度
通信梳理:父组件main.vue中有header组件和menu组件,header组件中有动态图标,menu组件的菜单根据header图标的状态进行折叠/展开,同时父组件对menu组件动态进行菜单宽度设置
子组件内部发生改变,要改变父组件设置的宽度的值父组件监听;根据子组件值动态调整宽度1
2
3
4
5
6const isFold = ref(false)
const emit = defineEmits(['flodchange'])
unction HandleMuneIconClick(){
isFold.value = !isFole.value
emit('foldchange',isFold,value)
}<el-aside :width = "isFold ? '60px' : '210px' ">
<main-header @fold-change="handleFoldChaneg"/>
父组件把isFole作为属性传递给menu子组件1
2
3
4const idFold = ref(false)
function handleFoldChange(flag:boolean){
isFold.value=flag
}<main-menu :is-fold="isFold"/>
menu子组件接受父组件的属性1
2
3
4
5
6defineProps({
isFold:{
type:Boolean,
default:false
}
}):collapse="isFold"
路由注册与页面跳转(非动态路由)
注册所有的路由
根据角色动态渲染菜单