写在前面

项目主要功能虽然都做完了,但是发现不会把它写在简历上,换句话说,我不知道如何介绍项目的功能与实现,这篇文章是我对项目的的梳理
完整的项目笔记见vue3+t后台管理系统项目笔记

项目STAR法则:

最好的技术表达,不是复述工具名称,而是说清:你为何这样选择,以及它解决了什么问题
(感谢大模型老师对我学习STAR法则的帮助)

技术栈

核心框架和工具:
Vue 3
TypeScript
pnpm 包管理器
Vite 构建工具
Pinia 状态管理
Vue Router 路由管理
UI 框架:
Element Plus
其他重要依赖:
axios HTTP请求
echarts 图表库
countup.js 数字动画
dayjs 日期处理

登录模块

核心实现

  1. Element Plus组件化登录架构

    • 采用组件化思想拆解登录页为登录面板容器+账号登录+手机登录三大模块
    • 采用el-tabs实现账号/手机登录模式切换,通过v-model="activeName"动态控制激活面板
    • el-link el-checkbox el-button el-message el-icon + 按需引入配置
  2. 账号密码表单校验

    • 基于el-form+el-input组件构建表单体系,配置可扩展的rules验证规则集
  3. 组件通信机制

    • 通过defineExpose暴露子组件方法,实现父组件触发登录验证,解决父子组件通信问题
  4. 类型抽取与token常量安全

    • 抽离IAccount核心对象类型,便于复用,约束账号密码结构
    • 用常量管理关键字段(如LOGIN_TOKEN),降低维护成本
  5. Token保存与Cache封装

    • 设计localStorage + Pinia双缓存持久化Token
    • 封装通用缓存工具类cache.ts,实现JSON.stringify()`` JSON.parse()自动处理对象数据,提升代码复用性与健壮性

权限管理模块

核心实现:

  1. 通过 Axios 请求拦截器自动携带 token
    • 避免手动添加 token 的冗余操作,提升开发效率
  2. 导航守卫设计
    • 校验/main开头的路径,未登录用户强制跳转至登录页,防止越权访问\
  3. 用户-角色-菜单映射机制
    • 登录成功后,先根据用户ID获取用户信息,再通过角色ID请求菜单数据
    • 对用户的userInfo和userMenu数据进行本地缓存和store缓存
  4. 动态路由(基于菜单管理)
    • 通过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 + 递归算法

核心实现

  1. 自动化路由收集
    • 使用 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
      120
           const 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)
      }
  2. 高阶组件复用方案
  • Search 组件

    • 动态生成搜索表单(支持 InputDatePicker 等组件),通过 search.config.ts 定义字段。
    • 支持默认值、占位符、类型判断等配置项。
  • Content 组件

    • 动态渲染表格列(支持自定义插槽、操作列、时间格式化等)。
    • 通过 content.config.ts 定义列名、数据类型、操作按钮逻辑。
  • Modal 组件

    • 通过配置文件定义表单字段及联动逻辑(如下拉选项动态获取部门列表)。
1
2
3
4
5
6
7
// 示例:动态配置搜索表单
const searchConfig = {
formItems: [
{ prop: 'name', type: 'input', label: '部门名称' },
{ prop: 'createAt', type: 'date-picker', label: '创建时间' }
]
}
  1. 动态配置驱动开发
  • 所有动态内容(表单字段、表格列、按钮文本)通过配置文件定义,无需修改组件代码。
  • 通过 refprops 实现组件间通信,统一处理查询、重置、新增、编辑等操作。
  1. Hook 抽离与逻辑复用
  • 将兄弟组件通信逻辑(如 handleQueryClickhandleEditClick)抽离为 usePageContent Hook,提升代码复用性。

Result

1. 开发效率显著提升

  • 新增模块仅需编写 xxx.vue + 3 个配置文件(search.config.tscontent.config.tsmodal.config.ts)。
  • **开发时间减少 50%+**:通过复用组件和动态配置,避免重复开发通用功能。

2. 维护成本大幅降低

  • **网络请求复用率 > 90%**:统一的三层架构适配所有业务模块。
  • 代码结构清晰:通过配置文件驱动 UI,业务逻辑与展示解耦,修改只需调整配置。

3. 可扩展性增强

  • 新增模块无需修改核心组件:仅需调整配置文件即可适配新需求。
  • 支持动态联动:如 Modal 中的下拉选项可动态获取部门数据,实现跨模块交互。

4. 技术价值

  • 配置优先原则:所有动态内容通过配置文件驱动,符合现代前端工程化趋势。
  • 组件解耦设计:业务逻辑与 UI 展示分离,组件职责单一,易于测试与维护。

数据可视化模块

核心实现:

  1. 动态数据展示
    • 轻量级库 CountUp.js 实现数字滚动效果(如商品销量从起始值平滑过渡至目标值)
    • 支持自定义前缀(如人民币符号¥),通过配置项动态切换展示格式
    • 使用 Element Plus Tooltip 组件,鼠标悬停时展示数据说明
  2. ECharts图表三层封装
    • 第一层封装:基础组件封装,统一图表初始化与更新逻辑
    • 第二层封装:不同类型的echarts图标封装不同的子组件(如pie-echart,rose-echart)
    • 第三层封装:抽离数据,把数据部分单独封装,传入第二层封装的options里(数据从service里面拿到,然后放到store里,再传到页面,页面再传递给它调用的组件里)

STAR法则描述:

Situation

  • 数据展示需求:
    需要构建一个 数据可视化仪表盘,展示商品销量、销售额等核心指标,并通过动态动画和图表增强数据表现力。
  • 技术挑战:
    静态数据问题:初期页面为静态展示,缺乏动态增长效果,用户体验单一。
    图表开发重复:ECharts 图表需针对不同数据类型重复编写配置代码。
    状态管理分散:数据从接口获取后未集中管理,导致页面逻辑复杂。
    Task项目目标
  • 实现动态数据可视化:
    使用 CountUp.js 实现数字递增动画,增强用户感知。
    通过 ECharts 展示饼图、柱状图等多类型图表。
  • 组件化与复用:
    封装通用图表组件,支持动态数据绑定。
    通过 props 传递配置,实现组件灵活复用。
  • 状态集中管理:
    使用 Pinia Store 管理数据,分离数据获取与展示逻辑
    Action
  1. 动态数字增长动画实现:
    选择 CountUp.js(轻量级、无依赖)实现数字增长动画。
  2. ECharts 图表封装
    • 三层封装架构:
    • 基础组件(BaseECharts):封装 echarts.init() 和通用配置,支持响应式更新。
    • 图表类型组件(PieECharts, BarECharts):针对不同图表类型扩展配置,示例(饼图组件)
    • 数据驱动组件:通过 computed 计算图表配置项,并使用 watchEffect 监听数据变化:
  3. 状态管理与组件通信
    数据从service里面拿到,然后放到store里,再传到页面,页面再传递给它调用的组件里
    Result
  • 用户体验提升:
    动态增长动画:数字递增效果增强数据表现力,提升用户感知。
    交互优化:Tooltip 提示和响应式图表提升操作友好度。
  • 开发效率与可维护性:
    组件复用率 > 90%:通过配置文件驱动图表和卡片组件,避免重复开发。
    代码结构清晰:数据获取、状态管理、UI 展示解耦,维护成本降低。
  • 技术价值:
    轻量级动画库:CountUp.js 无依赖、性能优,适合快速实现数值动画。
    ECharts 高度封装:支持多类型图表动态配置,适配不同业务场景。

大模型模拟的项目面试题

  1. https://www.doubao.com/thread/wbfa26b13c725686d
  2. 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通过校验

  1. 通过formRef.validate(回调)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function loginAction(){
    formRef.value?.validate((valid)=>{
    if(valid){//获取用户输入的帐号和密码
    const name = account.name
    const password = account.password
    //2.向服务器发送网络请求(携带账号和密码)
    loginstore.loginAccountAction({ name,password })}
    else {
    ElMessage.error('oops,请您输入正确的格式后再操作')}})}
  2. 登录接口调用post请求;
  3. data:{name,password}

将登录的操作放到store中

IAccount类型的定义(类型复用封装):

类型复用抽取:
一个对象类型在很多地方复用时,抽到type里

1
2
3
4
export interface IAccount{
name:string;
password:string
}

使用导入
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组件

  1. 分组ElMenu组件的作用
  2. 手动搭建整个菜单结构
  3. 根据userMenu动态遍历渲染

头部Header组件

  1. 动态组件切换图标展示
  2. 父组件--切换aside宽度
  3. 兄弟组件--菜单的折叠与展开

折叠菜单详细梳理(组件通信)

header组件图标控制兄弟组件(菜单栏)的展开与折叠;控制父组件对menu组件宽度的调整
涉及的部分:

  1. header组件--声明isFold变量记录展开/折叠的状态
  2. header组件--用动态组件实现不同状态的图标切换(这样不需要v-if,优化代码)
    <component :is="isFold ? 'Expand' : 'Fold'" />
  3. el-menu组件--有控制菜单展示与折叠的属性:collapse="true/false"
  4. 父组件--菜单的宽度是自己设置的,要根据header子组件的状态,动态设置菜单宽度
    通信梳理:父组件main.vue中有header组件和menu组件,header组件中有动态图标,menu组件的菜单根据header图标的状态进行折叠/展开,同时父组件对menu组件动态进行菜单宽度设置
    子组件内部发生改变,要改变父组件设置的宽度的值
    1
    2
    3
    4
    5
    6
    const 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"/>
    1
    2
    3
    4
    const idFold = ref(false)
    function handleFoldChange(flag:boolean){
    isFold.value=flag
    }
    父组件把isFole作为属性传递给menu子组件
    <main-menu :is-fold="isFold"/>
    menu子组件接受父组件的属性
    1
    2
    3
    4
    5
    6
    defineProps({
    isFold:{
    type:Boolean,
    default:false
    }
    })
    :collapse="isFold"

路由注册与页面跳转(非动态路由)

注册所有的路由
根据角色动态渲染菜单