S11-11 Vue-项目:mr_vue3_ts_cms 
[TOC]
接口文档 
时间:2023-6-2
接口文档v1版本(失效)
https://documenter.getpostman.com/view/12387168/TzsfmQvw
baseURL的值:
http://152.136.185.210:5000设置全局token的方法:
const res = pm.response.json();
pm.globals.set("token", res.data.token);接口文档v2版本:(有部分更新)
https://documenter.getpostman.com/view/12387168/TzzDKb12
baseURL的值:
http://codercba.com:5000技术栈 
- Vue3:
@3.5.13 - TS5:
@5.6.3 - Vite6:
@6.0.5。使用create-vue@3.6.4创建项目 - Pinia2:
@2.1.3。 - VueRouter4: 
@4.2.2 - Node16:
@20.11.1 
创建项目 
创建项目 
使用 create-vue 或 create-vite 工具创建mr-vue3-ts-cms项目。create-vue 是基于vite的脚手架工具
$ pnpm create vue$ pnpm create vite创建选项

Git配置 
[core]
	repositoryformatversion = 0
	filemode = false
	bare = false
	logallrefupdates = true
	symlinks = false
	ignorecase = true
[remote "origin"]
	url = git@github.com:rmray/mr-vue3-ts-cms-2501.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[remote "gitee"]
	url = git@gitee.com:meRay/mr-vue3-ts-cms-2501.git
	fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
	remote = origin
	merge = refs/heads/main
	vscode-merge-base = origin/main安装vscode插件 
- Vue Language Features (Volar)(暂时停用)
 - TypeScript Vue Plugin (Volar)(被
Vue - Official替代) - Vue - Official
v1.8.27(2.x版本有问题) 
问题: 在启用 TypeScript Vue Plugin (Volar) 的情况下,vscode不能识别vue文件的组件返回类型
解决: (暂时)停用TypeScript Vue Plugin (Volar) 插件
目录结构 
│  .eslintrc.cjs # eslint检测配置
│  .gitignore # git忽略配置
│  .prettierrc.json # prettier格式化配置
│  env.d.ts	# ts声明全局变量的类型定义文件
│  index.html # 模板文件
│  package-lock.json # 包管理
│  package.json # 包管理
│  README.md # 项目文档
│  tsconfig.app.json
│  tsconfig.json # ts编译器的配置文件
│  tsconfig.node.json
│  vite.config.ts # vite配置文件
│  
├─.vscode
│      extensions.json # vscode推荐插件
│      
├─node_modules
│          
├─public
│      favicon.ico
│      
└─src
    │  App.vue
    │  main.ts
    │  
    ├─assets
    │  ├─css
    │  └─img
    ├─base-ui
    ├─components
    ├─hooks
    ├─router
    ├─service
    ├─store
    ├─utils
    └─views3个tsconfig文件之间的关系

配置vue文件类型声明 
问题: 项目本身的vue模块声明并不能识别出App是一个组件,但是也并不报错。


分析: 这是因为vscode插件 Vue - Official 中声明了vue组件的类型。但是它声明的类型并不好。不能完整显示组件的类型
解决:
1、重新声明vue模块,使得ts可以识别出vue是一个组件
// env.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}2、禁用 Vue - Official 插件,此时就可以正常显示组件类型了。

配置icon,标题 
直接复制自己的icon到public中
配置标题
<!-- index.html -->
<title>木头人 - 后台管理</title>重置CSS样式 
normalize.css
第三方包:normalize.css
1、安装:normalize.css
npm i normalize.css2、在main.ts 中导入
import 'normalize.css'reset.less
自定义重置:reset.less
common.less
公共样式:common.less
问题: vite默认不能识别less文件,需要安装less
npm i less -D工程化规范 
集成editorconfig配置 
.editorconfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格。
# http://editorconfig.org
root = true # 当前的配置在根目录中
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行尾的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off
trim_trailing_whitespace = false更新@2501:
root = true
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
 
[*.md]
trim_trailing_whitespace = false
max_line_length = offVSCode需要安装一个插件:EditorConfig for VS Code
使用prettier工具 
Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
1、安装prettier
npm install prettier -D2、配置.prettierrc或者.prettierrc.json文件:
- useTabs:使用tab缩进还是空格缩进,选择false;
 - tabWidth:tab是空格的情况下,是几个空格,选择2个;
 - printWidth:当行字符的长度,推荐80,也有人喜欢100或者120;
 - singleQuote:使用单引号还是双引号,选择true,使用单引号;
 - trailingComma:在多行输入的尾逗号是否添加,设置为 
none表示不加; - semi:语句末尾是否要加分号,默认值true,选择false表示不加;
 
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}更新@2501: 其他选项默认就是上面的值
{
  "$schema": "https://json.schemastore.org/prettierrc",
  "semi": false,
  "singleQuote": true,
  "printWidth": 120
}3、创建.prettierignore忽略文件
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*4、VSCode需要安装prettier的插件:Prettier - Code formatter
5、测试prettier是否生效
测试一:在代码中保存代码;
可以通过插件
Prettier - Code formatter实现测试二:配置一次性修改的命令;
在package.json中配置一个scripts:
sh"prettier": "prettier --write ."
让prettier在保存时自动格式化
- 1、在vscode中安装 Prettier 扩展
 - 2、在
设置中搜索format on save,选中Editor: Format On Save - 3、在
设置中搜索default format,设置Editor: Default Formatter为Prettier - Code formatter - 4、配置
.prettierrc - 5、实现保存代码时自动格式化
 
使用ESLint检测 
更新@2501: ESLint9中去除了eslint-plugin-prettier插件,已经没有和格式化相关的规则,这样就不会和prettier冲突了。
1、在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。
2、VSCode需要安装ESLint插件:ESLint
3、解决eslint和prettier冲突的问题:
3.1、安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
eslint-plugin-prettier:作用是将 Prettier 作为一个 ESLint 规则运行,使得 Prettier 的格式化检查成为 ESLint 检查的一部分。
- 安装:
pnpm i eslint-plugin-prettier -D 
- 安装:
 eslint-config-prettier:作用是禁用所有与 Prettier 格式化规则冲突的 ESLint 规则。
- 安装:
pnpm i eslint-config-prettier -D 
- 安装:
 
pnpm i eslint-plugin-prettier eslint-config-prettier -D3.2、添加prettier插件:
  extends: [
    "plugin:vue/vue3-essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
      
+    // "@vue/eslint-config-prettier/skip-formatting" // 该规范导致eslint没有提示
+    '@vue/eslint-config-prettier',
+    "plugin:prettier/recommended"
  ],4、手动修改eslint检测规则
需要修改的报错:
@typescript-eslint/no-unused-vars:未使用的变量名vue/multi-word-component-names:检测当前的组件名称是否使用驼峰或多单词命名
在出现提示的位置,复制出现的错误:
@typescript-eslint/no-unused-vars
在出现提示的位置,复制出现的错误:
vue/multi-word-component-names
在
.eslintrc.cjs中添加如下代码:jsmodule.exports = { + rules: { + '@typescript-eslint/no-unused-vars': 'off', + 'vue/multi-word-component-names': 'off' + } }
git-Husky和eslint 
虽然我们已经要求项目使用eslint了,但是不能保证组员提交代码之前都将eslint中的问题解决掉了:
也就是我们希望保证代码仓库中的代码都是符合eslint规范的;
那么我们需要在组员执行
git commit命令的时候对其进行校验,如果不符合eslint规范,那么自动通过规范进行修复;
那么如何做到这一点呢?可以通过Husky工具:
husky是一个git hook工具,可以帮助我们触发git提交的各个阶段:pre-commit、commit-msg、pre-push
如何使用husky呢?
这里我们可以使用自动配置命令:
# npm
npx husky-init && npm install
# pnpm
pnpx husky-init && pnpm install
# 推荐
pnpm dlx husky-init && pnpm install注意: 在windows的powershell中需要给&&添加引号
# npm
npx husky-init '&&' npm install
# pnpm
pnpx husky-init '&&' pnpm install这里会做三件事:
1.安装husky相关的依赖:

2.在项目目录下创建 .husky 文件夹:
npx huksy install
3.在package.json中添加一个脚本:

接下来,我们需要去完成一个操作:在进行commit时,执行lint脚本:

这个时候我们执行git commit的时候会自动对代码进行lint校验。

暂存区 eslint 校验
由于使用pnpm lint校验时,会对所有文件都进行校验,耗时久。
为解决以上问题,就出现了 lint-staged 插件,它可以只对有改动的文件进行校验
依赖包: lint-staged
安装: pnpm i lint-staged -D
使用:
1、在package.json中配置lint-staged命令

2、配置lint-staged
方法一:在
.lintstagedrc文件中配置json{ "*.{js,ts,vue}": "eslint" }方法二:在
package.json中配置json"scripts": { ... + "lint-staged": "lint-staged" }, + "lint-staged": { + "*.{js,ts,vue}": [ + "prettier --write", + "eslint" + ] },
3、修改.husky/pre-commit文件

4、通过git commit -m "xxx"提交git时会使用lint-staged检测

git-commit规范 
代码提交风格 
插件:
- commitizen: 帮助开发者按照约定的格式(如 Conventional Commits)来编写提交信息,从而确保项目中所有的 Git 提交都遵循统一的规范。有助于生成自动化的变更日志以及版本管理。
 - cz-conventional-changelog:是一个 Commitizen 适配器,它实现了 Conventional Commits 规范。确保提交信息符合 Conventional Commits 规范。
 
通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。

但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:commitizen
1.安装commitizen
#npm 
npm install commitizen -D
#pnpm 
pnpm install commitizen -D2.安装cz-conventional-changelog,并且初始化cz-conventional-changelog:
# npm
pnpx commitizen init cz-conventional-changelog --save-dev --save-exact
# pnpm
pnpx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm这个命令会帮助我们安装cz-conventional-changelog:

并且在package.json中进行配置:

3、将commitizen的配置单独写入.czrc配置文件中
更新@2501: commitizen@4.3.1和cz-conventional-changelog@3.3.0已经无需将配置单独写入.czrc文件中。
{
  "path": "./node_modules/cz-conventional-changelog"
}4、这个时候我们提交代码需要使用:
#npm
npx cz
#pnpm
# 1. 在`package.json`中添加scripts: `"commit": "cz"`
# 2. 在命令行运行:`pnpm run commit`- 第一步是选择type,本次更新的类型
 
| Type | 作用 | 
|---|---|
| feat | 新增特性 (feature) | 
| fix | 修复 Bug(bug fix) | 
| docs | 修改文档 (documentation) | 
| style | 代码格式修改(white-space, formatting, missing semi colons, etc) | 
| refactor | 代码重构(refactor) | 
| perf | 改善性能(A code change that improves performance) | 
| test | 测试(when adding missing tests) | 
| build | 变更项目构建或外部依赖(例如 scopes: webpack、gulp、npm 等) | 
| ci | 更改持续集成软件的配置文件和 package 中的 scripts 命令,例如 scopes: Travis, Circle 等 | 
| chore | 变更构建流程或辅助工具(比如更改测试环境) | 
| revert | 代码回退 | 
| release | 发布新版本 | 
- 第二步选择本次修改的范围(作用域)
 
? What is the scope of this change (e.g. component or file name): (press enter to skip) git- 第三步选择提交的信息
 
? Write a short, imperative tense description of the change (max 89 chars): 安装了husky- 第四步提交详细的描述信息
 
? Provide a longer description of the change: (press enter to skip)- 第五步是否是一次重大的更改
 
? Are there any breaking changes? (y/N) n- 第六步是否影响某个open issue
 
? Does this change affect any open issues? (y/N) n我们也可以在scripts中构建一个命令来执行 cz:

代码提交验证(无效) 
如果我们按照cz来规范了提交风格,但是依然有同事通过 git commit 按照不规范的格式提交应该怎么办呢?
- 我们可以通过 commitlint 来限制提交;
 
1.安装 @commitlint/config-conventional 和 @commitlint/cli
# npm
npm i @commitlint/config-conventional @commitlint/cli -D
# pnpm
pnpm add @commitlint/config-conventional @commitlint/cli -D2.在根目录创建commitlint.config.js文件,配置commitlint
module.exports = {
  extends: ['@commitlint/config-conventional']
}问题: 报错如下:

解决:
方法一:通过快速修复,暂时屏蔽eslint检测,因为这个是误报
效果如下:
js// eslint-disable-next-line no-undef module.exports = { extends: ['@commitlint/config-conventional'] }方法二:修改
commitlint.config.js文件后缀为.cjs,此时就可以解析commonJS代码了
3.使用husky生成commit-msg文件,验证提交信息:
# npm
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
# pnpm (无效)
pnpx husky add .husky/commit-msg "pnpx --no-install commitlint --edit $1"第三方库集成 
vue.config.js配置 
vue.config.js有三种配置方式:
- 方式一:直接通过CLI提供给我们的选项来配置: 
- 比如publicPath:配置应用程序部署的子目录(默认是 
/,相当于部署在https://www.my-app.com/); - 比如outputDir:修改输出的文件夹;
 
 - 比如publicPath:配置应用程序部署的子目录(默认是 
 - 方式二:通过configureWebpack修改webpack的配置: 
- 可以是一个对象,直接会被合并;
 - 可以是一个函数,会接收一个config,可以通过config来修改配置;
 
 - 方式三:通过chainWebpack修改webpack的配置: 
- 是一个函数,会接收一个基于 webpack-chain 的config对象,可以对配置进行修改;
 
 
const path = require('path')
module.exports = {
  outputDir: './build',
  // configureWebpack: {
  //   resolve: {
  //     alias: {
  //       views: '@/views'
  //     }
  //   }
  // }
  // configureWebpack: (config) => {
  //   config.resolve.alias = {
  //     '@': path.resolve(__dirname, 'src'),
  //     views: '@/views'
  //   }
  // },
  chainWebpack: (config) => {
    config.resolve.alias.set('@', path.resolve(__dirname, 'src')).set('views', '@/views')
  }
}vue-router集成 
1、安装vue-router的最新版本:
npm install vue-router@next2、创建router对象:
import { createRouter, createWebHashHistory } from 'vue-router'
import { RouteRecordRaw } from 'vue-router'
// 映射关系
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/main'
  },
  {
    path: '/main',
    component: () => import('../views/main/main.vue')
  },
  {
    path: '/login',
    component: () => import('../views/login/login.vue')
  }
]
const router = createRouter({
  routes,
  history: createWebHashHistory()
})
export default router3、安装router:
import router from './router'
createApp(App).use(router).mount('#app')4、在App.vue中配置跳转:
<template>
  <div id="app">
    <router-link to="/login">登录</router-link>
    <router-link to="/main">首页</router-link>
    <router-view></router-view>
  </div>
</template>pinia集成 
1、安装pinia
npm i pinia2、创建pinia对象
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia3、挂载pinia
+ import pinia from './store'
const app = createApp(App)
+ app.use(pinia)
app.mount('#app')4、创建store
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
  state: () => ({
    counter: 10
  }),
  getters: {
    doubleCounter(state) {
      return state.counter * 2
    }
  },
  actions: {
    changeCounterAction(payload: number) {
      this.counter = payload
    }
  }
})
export default useCounterStore5、使用store
获取counter
<template>
  <div class="test">
+    <div>计数: {{ counterStore.counter }} - {{ counterStore.doubleCounter }}</div>
  </div>
</template>
<script setup lang="ts">
+ import useCounterStore from '@/store/counter'
+ const counterStore = useCounterStore()
</script>修改counter
<template>
  <div class="test">
+    <button @click="setCounter">修改counter</button>
  </div>
</template>
<script setup lang="ts">
import useCounterStore from '@/store/counter'
const counterStore = useCounterStore()
// 修改store
+ function setCounter() {
+   counterStore.changeCounterAction(900)
+ }
</script>vuex集成 
1、安装vuex:
npm install vuex@next2、创建store对象:
import { createStore } from 'vuex'
const store = createStore({
  state() {
    return {
      name: 'coderwhy'
    }
  }
})
export default store3、安装store:
createApp(App).use(router).use(store).mount('#app')4、在App.vue中使用:
<h2>{{ $store.state.name }}</h2>element-plus集成 
axios集成 
1、安装axios
npm install axios2、封装axios
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { Result } from './types'
import { useUserStore } from '/@/store/modules/user'
class HYRequest {
  private instance: AxiosInstance
  private readonly options: AxiosRequestConfig
  constructor(options: AxiosRequestConfig) {
    this.options = options
    this.instance = axios.create(options)
    this.instance.interceptors.request.use(
      (config) => {
        const token = useUserStore().getToken
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (err) => {
        return err
      }
    )
    this.instance.interceptors.response.use(
      (res) => {
        // 拦截响应的数据
        if (res.data.code === 0) {
          return res.data.data
        }
        return res.data
      },
      (err) => {
        return err
      }
    )
  }
  request<T = any>(config: AxiosRequestConfig): Promise<T> {
    return new Promise((resolve, reject) => {
      this.instance
        .request<any, AxiosResponse<Result<T>>>(config)
        .then((res) => {
          resolve((res as unknown) as Promise<T>)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }
  get<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request({ ...config, method: 'GET' })
  }
  post<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request({ ...config, method: 'POST' })
  }
  patch<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request({ ...config, method: 'PATCH' })
  }
  delete<T = any>(config: AxiosRequestConfig): Promise<T> {
    return this.request({ ...config, method: 'DELETE' })
  }
}
export default HYRequestVSCode配置 
{
  "workbench.iconTheme": "vscode-great-icons",
  "editor.fontSize": 17,
  "eslint.migration.2_x": "off",
  "[javascript]": {
    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  },
  "files.autoSave": "afterDelay",
  "editor.tabSize": 2,
  "terminal.integrated.fontSize": 16,
  "editor.renderWhitespace": "all",
  "editor.quickSuggestions": {
    "strings": true
  },
  "debug.console.fontSize": 15,
  "window.zoomLevel": 1,
  "emmet.includeLanguages": {
    "javascript": "javascriptreact"
  },
  "explorer.confirmDragAndDrop": false,
  "workbench.tree.indent": 16,
  "javascript.updateImportsOnFileMove.enabled": "always",
  "editor.wordWrap": "on",
  "path-intellisense.mappings": {
    "@": "${workspaceRoot}/src"
  },
  "hediet.vscode-drawio.local-storage": "eyIuZHJhd2lvLWNvbmZpZyI6IntcImxhbmd1YWdlXCI6XCJcIixcImN1c3RvbUZvbnRzXCI6W10sXCJsaWJyYXJpZXNcIjpcImdlbmVyYWw7YmFzaWM7YXJyb3dzMjtmbG93Y2hhcnQ7ZXI7c2l0ZW1hcDt1bWw7YnBtbjt3ZWJpY29uc1wiLFwiY3VzdG9tTGlicmFyaWVzXCI6W1wiTC5zY3JhdGNocGFkXCJdLFwicGx1Z2luc1wiOltdLFwicmVjZW50Q29sb3JzXCI6W1wiRkYwMDAwXCIsXCIwMENDNjZcIixcIm5vbmVcIixcIkNDRTVGRlwiLFwiNTI1MjUyXCIsXCJGRjMzMzNcIixcIjMzMzMzM1wiLFwiMzMwMDAwXCIsXCIwMENDQ0NcIixcIkZGNjZCM1wiLFwiRkZGRkZGMDBcIl0sXCJmb3JtYXRXaWR0aFwiOjI0MCxcImNyZWF0ZVRhcmdldFwiOmZhbHNlLFwicGFnZUZvcm1hdFwiOntcInhcIjowLFwieVwiOjAsXCJ3aWR0aFwiOjExNjksXCJoZWlnaHRcIjoxNjU0fSxcInNlYXJjaFwiOnRydWUsXCJzaG93U3RhcnRTY3JlZW5cIjp0cnVlLFwiZ3JpZENvbG9yXCI6XCIjZDBkMGQwXCIsXCJkYXJrR3JpZENvbG9yXCI6XCIjNmU2ZTZlXCIsXCJhdXRvc2F2ZVwiOnRydWUsXCJyZXNpemVJbWFnZXNcIjpudWxsLFwib3BlbkNvdW50ZXJcIjowLFwidmVyc2lvblwiOjE4LFwidW5pdFwiOjEsXCJpc1J1bGVyT25cIjpmYWxzZSxcInVpXCI6XCJcIn0ifQ==",
  "hediet.vscode-drawio.theme": "Kennedy",
  "editor.fontFamily": "Source Code Pro, 'Courier New', monospace",
  "editor.smoothScrolling": true,
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "workbench.colorTheme": "Atom One Dark",
  "vetur.completion.autoImport": false,
  "security.workspace.trust.untrustedFiles": "open",
  "eslint.lintTask.enable": true,
  "eslint.alwaysShowStatus": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}区分开发、生产环境 
Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
- import.meta.env.MODE: 
development | production,应用运行的模式。 - import.meta.env.PROD: 
boolean,应用是否运行在生产环境。 - import.meta.env.DEV: 
boolean,应用是否运行在开发环境 (永远与import.meta.env.PROD相反)。 - import.meta.env.BASE_URL: 
string,部署应用时的基本 URL。他由base配置项决定。 - import.meta.env.SSR: 
boolean,应用是否运行在 server 上。 
方法1: 手动决定使用哪个BASE_URL
方法2: 根据 import.meta.env.MODE 判断处于哪个环境,使用不同的BASE_URL

方法3: 自定义环境常量

1、创建.env.development 和 .env.production文件
2、分别在文件中定义不同的常量(注意:常量名必须以VITE_开头)

3、通过import.meta.env.VITE_XXX 获取定义的常量
路由前缀 
依赖包:
- dotenv:将 
.env文件中的键值对加载到process.env中。通过这种方式,应用程序可以根据环境的不同(开发、测试、生产等)来动态加载不同的配置。- dotenv.config:
({path?,encoding?,debug?,override?}),加载.env文件,并将其中的键值对加入到process.env中。 - dotenv.parse:
(contents),用于手动解析.env文件的内容。 
 - dotenv.config:
 
思路: 通过在.env.production和.env.development中设置不同的环境变量VITE_BASE。并在vite.config.ts配置中的defineConfig({})的base选项中指定不同环境中的路由前缀。
实现:
1、在.env.xxx文件中设置环境变量
# axios
VITE_BASE_URL = 'http://codercba.com:5000'
VITE_TIME_OUT = 10000
# 路由前缀
VITE_BASE = '/cms2501'# axios
VITE_BASE_URL = 'http://codercba.com:5000'
VITE_TIME_OUT = 10000
# 路由前缀
# VITE_BASE = '/cms2501'2、在vite.config.ts配置文件中导入 dotenv,通过它的 dotenv.config({path}) 方法将 .env 文件中的键值对加载到 process.env 中,并赋值给base选项。
注意: 此处需要将 export default 导出又对象{}改成({mode}) => {}函数写法。才能使用 dotenv.config({path})。
import dotenv from 'dotenv'
export default ({ mode }: any) => {
  // 导入环境变量配置文件:.env.xxx
  dotenv.config({ path: `.env.${mode}` })
  // 返回配置项
  return defineConfig({
    // ...
    // 路由前缀
    base: process.env.VITE_BASE,
  })
}3、现在可以通过http://localhost:5174/cms2501/#/main访问项目。
Login 
占满屏幕 
.app {
+  width: 100vw;
+  height: 100vh;
  background-color: #999;
}组件:LoginPanel 

使用组件 
<template>
  <div class="login">
+    <LoginPannel />
  </div>
</template>
<script setup lang="ts">
+ import LoginPannel from './cpns/LoginPanel/LoginPanel.vue'
</script>页面布局 
1、封装组件-基础
<template>
  <div class="login-panel">
    <!-- 标题 -->
    <h3 class="title">木人-后台管理系统</h3>
    <!-- 登录表单 -->
+    <div class="tabs">tabs</div>
    <!-- 密码管理 -->
    <div class="pwd-control">
      <el-checkbox v-model="isRemPwd" label="记住密码" />
      <el-link type="primary">忘记密码</el-link>
    </div>
    <!-- 登录提交 -->
    <el-button class="login-btn" size="large" type="primary">立即登录</el-button>
  </div>
</template>.login-panel {
  width: 330px;
}2、封装组件-密码管理
    <!-- 密码管理 -->
    <div class="pwd-control">
+      <el-checkbox v-model="isRemPwd" label="记住密码" />
      <el-link type="primary">忘记密码</el-link>
    </div>import { ref } from 'vue'
const isRemPwd = ref(true)4、封装组件-立即登录
    <!-- 登录提交 -->
    <el-button class="login-btn" size="large" type="primary">立即登录</el-button>5、封装组件-tabs
    <!-- 登录表单 -->
    <el-tabs v-model="activeName" class="tabs" type="border-card" stretch>
      <el-tab-pane label="帐号登录" name="account">
	 	...
      </el-tab-pane>
      <el-tab-pane label="手机登录" name="phonse">
		...
      </el-tab-pane>
    </el-tabs>导入Icon@ 
获取tabs当前标签项 
- v-model: 绑定值,选中选项卡的 name
 
+	<!-- @click="loginClickHdl" -->
+    <el-button class="login-btn" size="large" type="primary" @click="loginClickHdl">
      立即登录
    </el-button>+ 	 <!-- v-model="activeName" -->
+    <el-tabs v-model="activeName" class="tabs" type="border-card" stretch> 
+      <el-tab-pane label="帐号登录" name="account"> <!-- name="account" -->
          ...
      </el-tab-pane>
+      <el-tab-pane label="手机登录" name="phone"> <!-- name="phone" -->
          ...
      </el-tab-pane>
    </el-tabs>
...
<script setup lang="ts">
+	const activeName = ref('account')
</script>function loginClickHdl() {
  if (activeName.value === 'account') {
    console.log('通过帐号登录')
  } else {
    console.log('通过手机登录')
  }
}组件:paneAccount 
使用组件 
      <el-tab-pane label="帐号登录" name="account">
        <template #label>
            ...
        </template>
+        <PaneAccount />
      </el-tab-pane>import PaneAccount from '../PaneAccount/PaneAccount.vue'页面布局 
封装组件-基础
  <div class="pane-account">
    <el-form label-width="60px" size="large" status-icon>
      <el-form-item label="帐号">
        <el-input />
      </el-form-item>
      <el-form-item label="密码">
        <el-input show-password />
      </el-form-item>
    </el-form>
  </div>绑定表单数据 
  <div class="pane-account">
+    <el-form label-width="60px" :model="account" size="large" status-icon> <!-- :model="account" -->
      <el-form-item label="帐号">
+        <el-input v-model="account.name" /> <!-- v-model="account.name" -->
      </el-form-item>
      <el-form-item label="密码">
+        <el-input v-model="account.password" show-password /> <!-- v-model="account.password" -->
      </el-form-item>
    </el-form>
  </div>import { reactive } from 'vue'
const account = reactive({
  name: '',
  password: ''
})校验规则 
+	 <!-- :rules="accountRules" -->
+    <el-form label-width="60px" :model="account" :rules="accountRules" size="large" status-icon>
+      <el-form-item label="帐号" prop="name"> <!-- prop="name" -->
        <el-input v-model="account.name" />
      </el-form-item>
+      <el-form-item label="密码" prop="password"> <!-- prop="password" -->
        <el-input v-model="account.password" show-password />
      </el-form-item>
    </el-form>// 验证规则
const accountRules: FormRules = {
  name: [
    { required: true, message: '帐号不能为空', trigger: 'blur' },
    { pattern: /^[0-9a-zA-Z]{6,20}$/, message: '帐号必须是6~20个字符或数字', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '密码不能为空', trigger: 'blur' },
    { pattern: /^[0-9a-zA-Z]{3,}$/, message: '密码必须是3位以上的字符或数字', trigger: 'blur' }
  ]
}点击登录 
思路: 由于点击操作是在父组件中进行的,而帐号信息是在子组件中。可以考虑在父组件中调用子组件的方法
父组件
1、为el-button添加点击事件
+    <!-- @click="loginClickHdl" -->
+    <el-button class="login-btn" size="large" type="primary" @click="loginClickHdl">
      立即登录
    </el-button>2、获取组件PaneAccount的ref
InstanceType<typeof PaneAccount> 返回的是PaneAccount组件的 实例对象的类型。
<PaneAccount ref="accountRef" />const accountRef = ref<InstanceType<typeof PaneAccount>>()3、调用组件PaneAccount 中的方法
/* 点击登录 */
function loginClickHdl() {
  if (activeName.value === 'account') {
    // 调用PaneAccount组件内部方法
+    accountRef.value?.loginAction()
    console.log('通过帐号登录')
  } else {
    console.log('通过手机登录')
  }
}子组件
注意: 父组价想访问到子组件的方法,一定要先通过defineExpose 将方法暴露出去
/* 实现登录 */
function loginAction() {
  console.log('loginAction')
}
+ defineExpose({
+   loginAction
+ })1、实现登录功能-登录前校验
    <el-form
      label-width="60px"
      :model="account"
      :rules="accountRules"
+      ref="accountRef"
      size="large"
      status-icon
    >
        ...
    </el-form>import type { FormInstance } from 'element-plus/lib/components/index.js'
+ const accountRef = ref<FormInstance | undefined>()
/* 实现登录 */
function loginAction() {
  // 登录前校验
+  accountRef.value?.validate((valid) => {
    if (valid) {
      console.log('校验成功')
    } else {
      console.log('校验失败')
    }
  })
}2、实现登录功能-登录前校验-验证失败提示
import { ElMessage } from 'element-plus'
function loginAction() {
  // 登录前校验
  accountRef.value?.validate((valid) => {
    if (valid) {
      console.log('校验成功')
    } else {
+      ElMessage.error('呜~, 校验失败,请输入正确的账号密码格式~')
    }
  })
}3、实现登录功能-登录前校验-验证成功实现登录

service/login/login.ts
export function loginRequest(account: IAccount) {
  return mrRequest.post({
    url: '/login',
    data: account
  })
}4、优化:在pinia中发送网络请求
组件中
+ const loginStore = useLoginStore()
function loginHdl() {
  // 登录前校验
  accountRef.value?.validate((valid) => {
    if (valid) {
+      loginStore.loginAction({ name: account.name, password: account.password })
    } else {
      ElMessage.error('呜~, 校验失败,请输入正确的账号密码格式~')
    }
  })
}pinia中
const useLoginStore = defineStore('login', {
  state: () => ({
    token: localCache.getCache('token') ?? ''
  }),
  actions: {
+    async loginAction(account: IAccount) {
+      // 发送网络请求
+      const loginRes = await loginRequest(account)
+      console.log(loginRes)
+      // 保存请求数据到store
+      this.token = loginRes.data.token
+      // 保存请求数据到本地
+      localCache.setCache('token', this.token)
+    }
  }
})IAccount对象类型定义@ 
关于类型文件存放的位置:
思路一: 在每个页面组件里建一个types文件夹,保存定义的类型
export interface IAccount {
  name: string
  password: string
}使用类型
import type { IAccount } from '@/types/login'思路二: 如果定义的类型,在views、store、service等多个地方都会用到,就放入src/types 中
src/types/login.d.ts
export interface IAccount {
  name: string
  password: string
}定义统一的出口文件 src/types/index.d.ts
export * from './login'使用类型
+ import type { IAccount } from '@/types'
const useLoginStore = defineStore('login', {
  state: () => ({
      ...
  }),
  actions: {
+    async loginAction(account: IAccount) {
      // 发送网络请求
      // 保存请求数据到store
      // 保存请求数据到本地
    }
  }登录-本地缓存登录信息@ 
1、本地缓存-基本使用
const useLoginStore = defineStore('login', {
  state: () => ({
+    token: localStorage.getItem('token') ?? ''
  }),
  actions: {
    async loginAction(account: IAccount) {
      // 发送网络请求
      const loginRes = await loginRequest(account)
      // 保存请求数据到store
      this.token = loginRes.data.token
      // 保存请求数据到本地
+      localStorage.setItem('token', this.token)
    }
  }
})2、本地缓存-封装
在utils/cache/index.ts 中封装操作LocalStorage的类Cache
4、本地缓存-封装-使用
const useLoginStore = defineStore('login', {
  state: () => ({
+    token: localCache.getCache('token') ?? ''
  }),
  actions: {
    async loginAction(account: IAccount) {
      // 发送网络请求
      const loginRes = await loginRequest(account)
      // 保存请求数据到store
      this.token = loginRes.data.token
      // 保存请求数据到本地
+      localCache.setCache('token', this.token)
    }
  }
})登录成功跳转@ 
async loginAction(account: IAccount) {
    // 发送网络请求
    const loginRes = await loginRequest(account)
    console.log(loginRes)
    // 保存请求数据到store
    this.token = loginRes.data.token
    // 保存请求数据到本地
    localCache.setCache('token', this.token)
    // 登录成功跳转
+   router.push('/main')
}路由导航守卫@ 
1、在router/index.ts中添加导航守卫
/* 路由导航守卫 */
router.beforeEach((to, from) => {
  const token = localCache.getCache('token')
  if (to.path === '/main' && !token) {
    return '/login'
  }
})退出登录 
  <div class="main">
    <div>main</div>
+      <!-- @click="logoutClickHdl" -->
+    <el-button type="primary" @click="logoutClickHdl">退出登录</el-button> 
  </div>/* 点击退出登录 */
function logoutClickHdl() {
  // 清除storage
  localCache.removeCache('token')
  // 跳转login
  router.push('/login')
}记住密码 
思路: 记住密码状态isRemPwd在父组件中,而帐号和密码在子组件中,可以将isRemPwd通过loginAction(isRemPwd)传递给子组件
1、传递isRemPwd到子组件
function loginClickHdl() {
  if (activeName.value === 'account') {
    // 调用PaneAccount组件内部方法
+    accountRef.value?.loginHdl(isRemPwd.value) // isRemPwd.value
  } else {
    console.log('通过手机登录')
  }
}2、登录成功后缓存帐号、密码
function loginHdl(isRemPwd: boolean) {
  // 登录前校验
  accountRef.value?.validate((valid) => {
    if (valid) {
      loginStore.loginAction({ name: account.name, password: account.password }).then(() => {
        // 登录成功,记住密码
        if (isRemPwd) {
+         localCache.setCache('name', account.name)
+         localCache.setCache('password', account.password)
        }
      })
    } else {
      ElMessage.error('呜~, 校验失败,请输入正确的账号密码格式~')
    }
  })
}3、修改初始化帐号、密码
const account = reactive({
+  name: localCache.getCache('name') ?? '',
+  password: localCache.getCache('password') ?? ''
})4、未勾选记住密码时,移除缓存
        // 登录成功,记住密码
        if (isRemPwd) {
          localCache.setCache('name', account.name)
          localCache.setCache('password', account.password)
        } else {
+          localCache.removeCache('name')
+          localCache.removeCache('password')
        }5、缓存记住密码状态
在父组件中watch监听isRemPwd的值
/* 缓存记住密码状态 */
const isRemPwd = ref<boolean>(localCache.getCache('isRemPwd') ?? false)
watch(isRemPwd, (newValue) => {
  localCache.setCache('isRemPwd', newValue)
})获取用户详细信息 
1、发送网络请求
/* 获取用户详细信息 */
export function getUserInfo(id: number) {
  return mrRequest.get({
    url: `/users/${id}`
  })
}2、登录成功后获取用户详细信息
const useLoginStore = defineStore('login', {
  state: (): ILoginState => ({
    token: localCache.getCache('token') ?? '',
+    userInfo: {}
  }),
  actions: {
    /* 用户登录 */
    async loginAction(account: IAccount) {
        ...
      // 获取用户详细信息
+      this.fetchUserInfo(loginRes.data.id)
    },
    /* 获取用户详细信息 */
+    async fetchUserInfo(id: number) {
+      const userRes = await getUserInfo(id)
+      this.userInfo = userRes.data
+    },
})3、保存userInfo到pinia中
    async fetchUserInfo(id: number) {
      const userRes = await getUserInfo(id)
+      this.userInfo = userRes.data
    },携带token@ 
1、方法一: 在每次请求中添加headers.Authorization
/* 获取用户详细信息 */
export function getUserInfo(id: number) {
  return mrRequest.get({
    url: `/users/${id}`
+    headers: {
+      Authorization: 'Bearer ' + localCache.getCache('token')
+    }
  })
}2、方法二: 在拦截器中添加headers.Authorization
const mrRequest = new MrRequest({
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {
    requestSuccessFn: (config) => {
      // 携带token
+      if (config.headers) {
+        config.headers.Authorization = 'Bearer ' + localCache.getCache('token')
+      }
      return config as InternalAxiosRequestConfig
    },
    ...
  }
})定义state中的类型@ 
+  interface ILoginState {
+    token: string
+    userInfo: any
+    roleMenuTreeInfo: any
+  }
  const useLoginStore = defineStore('login', {
+    state: (): ILoginState => ({
      token: localCache.getCache('token') ?? '',
      userInfo: {},
      roleMenuTreeInfo: {}
    }),
  })获取用户角色权限菜单树 
1、发送网络请求
/* 获取角色菜单树 */
export function getRoleMenuTreeInfo(id: number) {
  return mrRequest.get({
    url: `/role/${id}/menu`
  })
}2、在store中获取数据
  actions: {
    /* 用户登录 */
    async loginAction(account: IAccount) {
        ...
      // 获取角色菜单树
+      this.fetchRoleMenuTreeInfo(loginRes.data.id)
    },
    /* 获取角色菜单树 */
+    async fetchRoleMenuTreeInfo(id: number) {
+      const roleRes = await getRoleMenuTreeInfo(id)
+      this.roleMenuTreeInfo = roleRes.data
+    }
  }组件:PanePhone 
使用组件 
页面布局 
<template>
  <div class="pane-phone">
    <el-form label-width="60px" size="large" :model="phone">
      <el-form-item label="手机号">
        <el-input v-model="phone.phone" />
      </el-form-item>
      <el-form-item label="验证码">
        <div class="code">
          <el-input v-model="phone.code" />
          <el-button class="get-code" type="primary">获取验证码</el-button>
        </div>
      </el-form-item>
    </el-form>
  </div>
</template>手机验证码【 
校验规则选项@ 
type:指定输入数据的类型
required:表示是否必填,值为 true 或 false。
message:表示校验不通过时的提示消息。
trigger:表示触发校验的事件类型,可以是 blur、change 等。
min 和 max:分别表示输入值的最小值和最大值。例如
min:3表示输入的值必须大于等于 3。pattern:表示输入值的正则表达式,例如
pattern:/^[a-z]+$/i表示输入的值必须由字母构成。len:表示输入值的长度,例如
len:6表示输入的值必须恰好为 6 个字符。
权限管理系统-RBAC@ 
RBAC (Role-Based Access Control) 即基于角色的访问控制,是一种常见的访问控制机制,用于管理用户和资源之间的访问权限。RBAC 基于角色对用户进行授权,而不是直接授予用户特定的权限集合。通过这种方式,RBAC 可以使权限管理更易于管理和扩展。
在 RBAC 中,有三个核心概念:用户、角色和权限。其中:
- 用户:系统中的每个用户都有唯一的标识符,可以被分配到一个或多个角色。
 - 角色:每个角色代表了一组权限集合,包含了访问系统中的某些资源所需的权限。角色可以被分配给一个或多个用户。
 - 权限:权限是指访问系统中某些资源所需的能力,例如读取、写入或执行某些操作等。
 
通过将用户分配到角色,并为每个角色分配适当的权限,RBAC 可以简化权限管理过程,减少管理员管理的工作量,同时也可以帮助保证系统安全性。
Main 
页面布局 
<template>
  <div class="main">
    <el-container class="main-content">
      <el-aside class="aside" width="210px">
          Aside
      </el-aside>
      <el-container>
        <el-header>
            Header
        </el-header>
        <el-main>Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>占满屏幕
  .main {
+    height: 100%;
    .main-content {
+      height: 100%;
      background-color: #ccc;
    }
  }
  .aside {
    &::-webkit-scrollbar {
      display: none;
    }
  }组件:MainMenu 

使用组件 
import MainMenu from '@/components/MainMenu/MainMenu.vue'
import MainHeader from '@/components/MainHeader/MainHeader.vue'  <div class="main">
    <el-container class="main-content">
      <el-aside class="aside" width="210px">
+        <MainMenu />
      </el-aside>
      <el-container>
        <el-header>
+          <MainHeader />
        </el-header>
        <el-main>Main</el-main>
      </el-container>
    </el-container>
  </div>页面布局 
1、element-plus布局
<template>
  <div class="main-menu">
    <!-- logo -->
    <div class="logo">
      <span>
        <img src="@/assets/img/logo.svg" alt="" />
      </span>
      <h2 class="title">木人后台管理</h2>
    </div>
    <!-- menu -->
    <div class="menu">
+      <el-menu
        default-active="2"
        class="el-menu-vertical-demo"
        background-color="#02142f"
        active-text-color="--active-color"
      >
+        <el-sub-menu>
+          <template #title>
            <el-icon><Monitor /></el-icon>
            <span>系统总览</span>
+          </template>
+          <el-menu-item index="1">核心技术</el-menu-item>
+          <el-menu-item index="1">商品统计</el-menu-item>
+        </el-sub-menu>
+        <el-sub-menu>
          <template #title>
            <el-icon><Monitor /></el-icon>
            <span>系统管理</span>
          </template>
          <el-menu-item index="1">用户管理</el-menu-item>
          <el-menu-item index="1">部门管理</el-menu-item>
          <el-menu-item index="1">菜单</el-menu-item>
          <el-menu-item index="1">角色统计</el-menu-item>
+        </el-sub-menu>
+        <el-sub-menu>
          <template #title>
            <el-icon><Monitor /></el-icon>
            <span>商品中心</span>
          </template>
          <el-menu-item index="1">商品类别</el-menu-item>
          <el-menu-item index="1">商品信息</el-menu-item>
+        </el-sub-menu>
+        <el-sub-menu>
          <template #title>
            <el-icon><Monitor /></el-icon>
            <span>随便聊聊</span>
          </template>
          <el-menu-item index="1">意见中心</el-menu-item>
          <el-menu-item index="1">信息分享</el-menu-item>
+        </el-sub-menu>
+      </el-menu>
    </div>
  </div>
</template>2、分析:
- ElMenu:整个菜单
 - ElSubMenu:可以有子菜单,并且可以展开
 - ElMenuItemGroup:对子菜单进行分组,但是不能展开。目的时给子菜单加上组名,但是不能交互
 - ElMenuItem:可以点击的每一个item
 
3、样式颜色
<el-menu default-active="2" background-color="#02142f">.menu {
  /** 去除右侧突出的边 */
  .el-menu {
    border-right: none;
  }
    
  .el-sub-menu {
    background-color: #0c1f36;
    .el-sub-menu__title span,
    .el-sub-menu__title .el-icon {
      color: #b0bccf !important;
    }
    :deep(.el-sub-menu__icon-arrow) {
      color: #b0bccf;
    }
  }
  .el-menu-item {
    background-color: #0c1f36;
    color: #b0bccf;
    &.is-active {
      background-color: #0b5dbe;
      color: #fff;
    }
  }
}缓存用户信息等 
缓存登录时请求的用户详细信息、角色权限菜单树
  state: (): ILoginState => ({
    token: localCache.getCache('token') ?? '',
+    userInfo: localCache.getCache('userInfo') ?? {},
+    roleMenuTreeInfo: localCache.getCache('roleMenuTreeInfo') ?? []
  }),
    /* 获取用户详细信息 */
    async fetchUserInfo(id: number) {
      const userRes = await getUserInfo(id)
      this.userInfo = userRes.data
      // 缓存用户详细信息
+      localCache.setCache('userInfo', userRes.data)
    },
    /* 获取角色菜单树 */
    async fetchRoleMenuTreeInfo(id: number) {
      const roleRes = await getRoleMenuTreeInfo(id)
      this.roleMenuTreeInfo = roleRes.data
      // 缓存角色菜单数
+      localCache.setCache('roleMenuTreeInfo', roleRes.data)
    }动态展示菜单 
1、基本展示
  <el-menu default-active="39" background-color="#02142f">
+    <template v-for="item in menus" :key="item.id">
      <el-sub-menu :index="item.id + ''">
        <template #title>
          <el-icon>
+            <component :is="item.icon.split('-icon-')[1]" />
          </el-icon>
          <span>{{ item.name }}</span>
        </template>
++        <template v-for="subitem in item.children" :key="subitem.id">
          <el-menu-item :index="subitem.id + ''">{{ subitem.name }}</el-menu-item>
++        </template>
      </el-sub-menu>
+    </template>
  </el-menu>import useLoginStore from '@/store/login'
/* 动态展示菜单 */
const loginStore = useLoginStore()
const menus = loginStore.roleMenuTreeInfo2、给每个item添加唯一标识index
  <el-menu default-active="39" background-color="#02142f">
    <template v-for="item in menus" :key="item.id">
+      <el-sub-menu :index="item.id + ''">
        <template #title>
          <el-icon>
            <component :is="item.icon.split('-icon-')[1]" />
          </el-icon>
          <span>{{ item.name }}</span>
        </template>
        <template v-for="subitem in item.children" :key="subitem.id">
+          <el-menu-item :index="subitem.id + ''">{{ subitem.name }}</el-menu-item>
        </template>
      </el-sub-menu>
    </template>
  </el-menu>3、初始打开第一个item
<!-- default-active="39" -->
<el-menu default-active="39" background-color="#02142f">保持一个子菜单展开 
- Menu[ unique-opened ] : 
boolean,是否只保持一个子菜单的展开,默认false 
动态展示图标 
思路: 利用动态组件<component :is="xxx"> 通过字符串生成组件
  <el-icon>
+    <component :is="item.icon.split('-icon-')[1]" />
  </el-icon>组件:MainHeader 

使用组件 
    <el-container class="main-content">
      <el-aside class="aside" width="210px">
        <MainMenu />
      </el-aside>
      <el-container>
        <el-header>
+          <MainHeader />
        </el-header>
        <el-main>Main</el-main>
      </el-container>
    </el-container>
<script setup lang="ts">
+   import MainHeader from '@/components/MainHeader/MainHeader.vue'
</script>页面布局 
  <div class="main-header">
    <!-- 折叠图标 -->
    <div class="menu-icon" @click="flodMenuHdl">
      <el-icon>
        <component :is="isFold ? 'Expand' : 'Fold'" />
      </el-icon>
    </div>
    <div class="content">
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <el-breadcrumb :separator-icon="ArrowRight">
          <el-breadcrumb-item :to="{ path: '/' }">系统总览</el-breadcrumb-item>
          <el-breadcrumb-item>核心技术</el-breadcrumb-item>
        </el-breadcrumb>
      </div>
      <!-- 用户信息 -->
      <div class="info">
        <!-- 消息互动 -->
        <div class="interactive">
          <span>
            <el-icon><Bell /></el-icon>
          </span>
          <span>
            <el-icon><ChatDotRound /></el-icon>
          </span>
          <span>
            <el-icon><ChatLineSquare /></el-icon>
          </span>
        </div>
        <!-- 用户中心 -->
        <div class="user">
			用户中心
        </div>
      </div>
    </div>
  </div>点击折叠菜单@ 
1、监听图标点击事件
    <!-- 折叠图标 -->
+    <div class="menu-icon" @click="flodMenuHdl">
      <el-icon>
        <Fold />
      </el-icon>
    </div>2、切换图标
修改父组件中的isFold
- 向父组件发送事件changeFold
 
const emits = defineEmits(['changeFold'])
/* 点击折叠/展开菜单 */
function flodMenuHdl() {
  emits('changeFold')
}- 在父组件中修改isFold
 
<MainHeader @change-fold="changeFoldHdl" />/* 修改isFold */
function changeFoldHdl() {
  isFold.value = !isFold.value
}获取父组件的isFold
- 父组件传递isFold到子组件
 
<MainHeader :is-fold="isFold" />- 在子组件接收isFold,并根据isFold切换图标
 
defineProps({
  isFold: {
    type: Boolean,
    default: false
  }
})    <!-- 折叠图标 -->
    <div class="menu-icon" @click="flodMenuHdl">
      <el-icon>
+        <component :is="isFold ? 'Expand' : 'Fold'" />
      </el-icon>
    </div>3、折叠菜单-修改aside宽度
在父组件定义isFold,根据isFlod切换宽度
const isFold = ref<boolean>(false)+      <el-aside class="aside" :width="isFold ? '60px' : '210px'">
        <MainMenu :is-fold="isFold" />
      </el-aside>4、添加折叠时的动画
.aside {
+  transition: width 300ms ease;
  &::-webkit-scrollbar {
    display: none;
  }
}5、折叠菜单-折叠el-menu
- 在main中将isFold传递给main-menu
 
<MainMenu :is-fold="isFold" />- 在main-menu中接收isFold
 
defineProps({
  isFold: {
    type: Boolean,
    default: false
  }
})- 通过collapse控制菜单折叠
 
<el-menu default-active="39" :collapse="isFold" background-color="#02142f">6、根据isFold显示、隐藏title
<h2 class="title" v-show="!isFold">木人后台管理</h2>组件:HeaderInfo 
使用组件 
    <div class="content">
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <el-breadcrumb :separator-icon="ArrowRight">
          <el-breadcrumb-item :to="{ path: '/' }">系统总览</el-breadcrumb-item>
          <el-breadcrumb-item>核心技术</el-breadcrumb-item>
        </el-breadcrumb>
      </div>
      <!-- 用户信息 -->
+      <HeaderInfo />
    </div>
<script setup lang="ts">
+   import HeaderInfo from './cpns/HeaderInfo.vue'
</script>页面布局 
  <div class="header-info">
    <!-- 消息互动 -->
    <div class="interactive">
      <span>
        <el-icon><Bell /></el-icon>
      </span>
      <span>
        <el-icon><ChatDotRound /></el-icon>
      </span>
      <span>
        <el-icon><ChatLineSquare /></el-icon>
      </span>
    </div>
    <!-- 用户中心 -->
    <div class="user">
      <div class="avatar">
        <el-avatar
          :size="30"
          src="https://foruda.gitee.com/avatar/1672726238822211763/5636878_meray_1672726238.png"
        />
      </div>
+      <el-dropdown>
        <span class="name">
          {{ userInfo.name }}
          <el-icon><ArrowDown /></el-icon>
        </span>
        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item @click="logoutClickHdl">
              <el-icon><Close /></el-icon>
              <span>退出系统</span>
            </el-dropdown-item>
            <el-dropdown-item divided>
              <el-icon><InfoFilled /></el-icon>
              <span>个人信息</span>
            </el-dropdown-item>
            <el-dropdown-item>
              <el-icon><Lock /></el-icon>
              <span>修改密码</span>
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </div>重置弹框的样式
注意: 此处由于弹框的根元素在#app元素外面,所以:deep() 不起效果,必须使用:global

退出登录 
+            <el-dropdown-item @click="logoutClickHdl">
              <el-icon><Close /></el-icon>
              <span>退出系统</span>
            </el-dropdown-item>import router from '@/router'
import { localCache } from '@/utils/cache'
/* 点击退出登录 */
function logoutClickHdl() {
  // 清除storage
  localCache.removeCache('token')
  // 跳转login
  router.push('/login')
}动态路由@ 
添加路由占位 
  <el-main>
+    <RouterView />
  </el-main>注册所有路由 
注意: 该方法有通过路由泄露权限的风险,用户可以通过url访问自己不具备的权限页面

点击菜单跳转路由 


动态路由分析 

动态路由存放位置 
方式一: 所有的路由放在统一的地方


方式二: 每个路由建一个单独的文件夹保存,这些路由和views页面目录一一对应,可以通过自动化工具创建路由和组件
1、定义单个路由

2、安装自动化工具
npm i coderwhy -g3、工具命令
# 查看版本
coderwhy --version
# 添加页面
coderwhy addpage <pagename> -d <path> # 添加vue2页面
coderwhy add3page <pagename> -d <path> # 添加vue3页面,如:export default {}
coderwhy add3page_setup <pagename> -d <path> # 添加vue3的setup页面,如:<script setup>
# <pagename>: 页面名称
# <path>: 添加到的位置路径,如 src/views/main/system/dashboard
# 示例: coderwhy add3page_setup menu -d src/views/main/system/menu动态加载路由对象 
// @/store/login/login.ts
/* 动态加载路由 */
const localRoutes: RouteRecordRaw[] = []
// 1. 获取所有的路由文件
const files: Record<string, any> = import.meta.glob('./main/**/*.ts', { eager: true })
for (const key in files) {
  localRoutes.push(files[key].default)
}根据菜单映射路由 
// @/store/login/login.ts
const loginStore = useLoginStore(pinia)
const roleMenuTreeInfo = loginStore.roleMenuTreeInfo
// 2. 遍历localRoutes,并和menus匹配
for (const menu of roleMenuTreeInfo) {
  for (const submenu of menu.children) {
+    const route = localRoutes.find((route) => route.path === submenu.url)
+    if (route) router.addRoute('main', route)
  }
}封装映射路由 
// src\utils\mapMenus\index.ts
import type { RouteRecordRaw } from 'vue-router'
/**
 * 根据菜单动态加载路由
 * @param roleMenuTreeInfo 菜单树
 */
export function mapMenus(roleMenuTreeInfo: any[]) {
  const routes: RouteRecordRaw[] = []
  // 1. 获取所有的路由文件
  const localRoutes = loadLocalRoutes()
  // 2. 遍历localRoutes,并和menus匹配
  for (const menu of roleMenuTreeInfo) {
    for (const submenu of menu.children) {
      const route = localRoutes.find((route) => route.path === submenu.url)
      if (route) routes.push(route)
    }
  }
  return routes
}
function loadLocalRoutes() {
  const localRoutes: RouteRecordRaw[] = []
  const files: Record<string, any> = import.meta.glob('../../router/main/**/*.ts', { eager: true })
  for (const key in files) {
    localRoutes.push(files[key].default)
  }
  return localRoutes
}使用
// store/login/login.ts
/* 动态加载路由 */
const loginStore = useLoginStore(pinia)
const roleMenuTreeInfo = loginStore.roleMenuTreeInfo
const routes = mapMenus(roleMenuTreeInfo)
for (const route of routes) {
  router.addRoute('main', route)
}页面刷新保留路由注册 
1、在actions中动态加载路由

2、在main中调用action

3、优化: 调用action


匹配main的第一个子页面 
1、在匹配动态路由时,记录第一个被匹配到的菜单
/* 导出第一个子目录 */
+ export let firstSubItem: any = null
/**
 * 根据菜单动态加载路由
 * @param roleMenuTreeInfo 菜单树
 */
export function mapMenus(roleMenuTreeInfo: any[]) {
  const routes: RouteRecordRaw[] = []
  // 1. 获取所有的路由文件
  const localRoutes = loadLocalRoutes()
  // 2. 遍历localRoutes,并和menus匹配
  for (const menu of roleMenuTreeInfo) {
    for (const submenu of menu.children) {
      const route = localRoutes.find((route) => route.path === submenu.url)
      if (route) routes.push(route)
+      if (!firstSubItem && route) firstSubItem = submenu
    }
  }
  return routes
}2、在路由导航守卫中,重定向/main到第一个路由地址
/* 路由导航守卫 */
router.beforeEach((to) => {
  const token = localCache.getCache('token')
  // 1. 如果跳转到main页面及其子页面,并且没有token的话,则跳转到login页面
  if (to.path.startsWith('/main') && !token) {
    return '/login'
  }
+  // 2. 如果跳转到/main页面,则重定向到/main下的第一个子页面
+  if (to.path === '/main' || to.path === '/main/') {
+    return firstSubItem?.url
+  }
})根据路由匹配当前菜单 
/**
 * 根据当前路由匹配正确的菜单项
 * @param path 当前路由
 * @param menus 菜单列表
 */
export function mapPathToMenu(path: string, menus: any[]) {
  for (const menu of menus) {
    for (const submenu of menu.children) {
      if (submenu.url === path) {
        return submenu
      }
    }
  }
}/* 根据当前路由匹配正确的菜单项 */
const path = useRoute().path
const currMenu = mapPathToMenu(path, menus)<el-menu :default-active="currMenu.id + ''" :collapse="isFold" background-color="#02142f">组件:HeaderCrumb 
使用组件 
  <div class="main-header">
    <div class="content">
      <!-- 面包屑 -->
+      <HeaderCrumb />
      <!-- 用户信息 -->
      <HeaderInfo />
    </div>
  </div>
</template>
<script setup lang="ts">
import HeaderInfo from './cpns/HeaderInfo.vue'
+ import HeaderCrumb from './cpns/HeaderCrumb.vue'
</script>页面布局 
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <el-breadcrumb separator-icon="ArrowRight">
          <el-breadcrumb-item :to="{ path: '/' }">系统总览</el-breadcrumb-item>
          <el-breadcrumb-item>核心技术</el-breadcrumb-item>
        </el-breadcrumb>
      </div>根据路由匹配菜单及父菜单@ 
1、根据路由匹配菜单及父菜单
/**
 * 根据路由匹配面包屑
 * @param path 当前路由
 * @param menus 菜单列表
 * @returns 面包屑菜单列表
 */
export function mapPathToCrumb(path: string, menus: any[]) {
  const crumbs: any[] = []
  for (const menu of menus) {
    for (const submenu of menu.children) {
      if (submenu.url === path) {
+        crumbs.push({ path: menu.url, name: menu.name }) // 父菜单
        crumbs.push({ path: submenu.url, name: submenu.name })
      }
    }
  }
  return crumbs
}2、调用方法,获取面包屑数据
/* 根据路由匹配面包屑 */
const path = route.path
const menus = loginStore.roleMenuTreeInfo
const crumbs = mapPathToCrumb(path, menus)3、遍历面包屑
    <div class="breadcrumb">
      <el-breadcrumb :separator-icon="ArrowRight">
+        <template v-for="crumb in crumbs" :key="crumb.path">
+          <el-breadcrumb-item :to="crumb.path">{{ crumb.name }}</el-breadcrumb-item>
+        </template>
      </el-breadcrumb>
    </div>4、监听路径的改变,实时更新面包屑
  /* 根据路由匹配面包屑 */
  const menus = loginStore.roleMenuTreeInfo
+  const crumbs = computed(() => {
+    const path = route.path
+    return mapPathToCrumb(path, menus)
  })点击一级面包屑跳转@ 
1、添加父菜单到动态路由中
export function mapMenus(roleMenuTreeInfo: any[]) {
  const routes: RouteRecordRaw[] = []
  // 1. 获取所有的路由文件
  const localRoutes = loadLocalRoutes()
  // 2. 遍历localRoutes,并和menus匹配
  for (const menu of roleMenuTreeInfo) {
    for (const submenu of menu.children) {
      const route = localRoutes.find((route) => route.path === submenu.url)
      if (route) {
+        // 添加父菜单到动态路由中
+        if (!routes.find((item) => item.path === menu.url)) {
+          routes.push({ path: menu.url, redirect: route.path })
+        }
        // 添加子菜单到动态路由中
        routes.push(route)
      }
      if (!firstSubItem && route) firstSubItem = submenu
    }
  }
  console.log('routes: ', routes)
  return routes
}2、监听路由变化

/* 根据当前路由匹配正确的菜单项 */
+  const currMenu = computed(() => {
  const path = route.path
  return mapPathToMenu(path, menus)
})问题集 
声明之前已使用的块范围变量“__VLS_13” 
问题: 刚刚使用pnpm create vue@latest创建vue3项目,安装依赖包之后就出现以下报错:

解决思路:
怀疑是vscode插件引起,目前使用的插件:
Vue - Official@2.0.3TypeScript Vue Plugin (Volar)@1.8.27
尝试1:(失败),卸载
Vue - Official@2.0.3,只用volar- 问题:会出现vue代码没有高亮提示
 
尝试2:(失败),装回
Vue - Official@2.0.3,此时提示:
- 根据提示卸载
TypeScript Vue Plugin (Volar)@1.8.27 
- 根据提示卸载
 尝试3:(成功),怀疑是
Vue - Official因为是2.x版本,比较新出现的BUG,安装Vue - Official@1.8.27版本看看- 问题解决
 
目前(2024-5-23):更新到
Vue - Official的v2.0.8版本,该问题已经解决
不识别vue文件类型 
问题:
vscode不能识别vue文件。

此时App是any类型

解决: 需要手动添加vue文件的类型声明
declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
  const component: DefineComponent
  export default component
}注意: import type xxx 需要写在 declare {} 内部
问题: 在安装了TypeScript Vue Plugin (Volar) 插件时,不能正确报错:不能识别vue文件的错误
虽然没有报错,但是App不能识别正确的类型,此时App是any类型

解决: (暂时)停用TypeScript Vue Plugin (Volar) 插件

打包出错 
问题: 通过pnpm run build打包项目时,报以下错误:
D:\2024\Work\Vue\mr-vue3-ts-cms-2403>pnpm run build
> mr-vue3-ts-cms-2403@0.0.0 build D:\2024\Work\Vue\mr-vue3-ts-cms-2403
> run-p type-check "build-only {@}" --
 ERROR  Unknown option: 'mr-vue3-ts-cms-2403:commitizen_path'
For help, run: pnpm help run
 ERROR  Unknown option: 'mr-vue3-ts-cms-2403:commitizen_path'
For help, run: pnpm help run
ERROR: "type-check" exited with 1.
 ELIFECYCLE  Command failed with exit code 1.原因: 这是因为package.json配置中有以下配置,和打包冲突
{
+  "config": {
+    "commitizen": {
+      "path": "./node_modules/cz-conventional-changelog"
+    }
+  }
}解决: 删除package.json中上面的"commitizen": {path}配置,将它移到新建的.czrc配置文件中

git提交时换行符提示 
问题: 在设置了.editconfig 文件的end_of_line = lf 后,每次git提交时都会提示:LF will be replaced by CRLF the next time Git touches it 报错
解决: (尝试)在终端设置如下命令:
# 会重新配置 Git,将其默认的换行符格式设置为 lf
git config --global core.eol lf其他关于换行符的命令:
# 重新配置 Git,让其能够自动处理行尾符并转换为正确的格式。
git config --global core.autocrlf trueprettier保存时自动格式化 
问题: 让prettier在保存时自动格式化
- 1、在vscode中安装 Prettier 扩展
 - 2、在
设置中搜索format on save,选中Editor: Format On Save - 3、在
设置中搜索default format,设置Editor: Default Formatter为Prettier - Code formatter - 4、配置
.prettierrc - 5、实现保存代码时自动格式化
 
element-plus 没有类型提示 
问题: 使用自动导入之后,无法正确获取到element-plus的类型提示

尝试: 通过在tsconfig.app.json 中添加如下类型include依然无效
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"],解决: 通过在vite.config.ts中导入插件配置时,添加dts属性,可以解决该问题

针对ElMessage等组件引入样式 
问题: ElMessage等组件无法自动引入,只能手动引入,但是引入后样式也需要另外引入
解决:
方法一: 在main.ts中手动引入样式

方法二: 使用插件 vite-plugin-style-import
1、安装:
npm i vite-plugin-style-import -D2、在
vite.config.ts中配置

3、由于缺失了
consola插件,需要另外安装shnpm i consola -D
ElMessage缺少声明 
通过import { ElMessage } from 'element-plus'导入ElMessage 组件时报如下错误:
报错: 模块 ""element-plus"" 没有导出的成员 "ElMessage"。
分析: 这是由于 IDE无法识别到element-plus中的ElMessage 类型,需要手动添加类型声明
在env.d.ts中添加类型声明
// element-plus类型声明
declare module 'element-plus' {
  import { ElMessage } from 'element-plus'
  export class ElMessage {
    static success(message: string): void
    static warning(message: string): void
    static info(message: string): void
    static error(message: string): void
  }
}注意: 目前在使用自动导入的情况下已没有该问题
vue中deep的使用场景@ 
vue中如果在组件的根元素上的样式可以不用:deep() ,但是如果想选中组件内部的元素就需要:deep()了


nextTick@ 
在 Vue 3 中,nextTick() 是一个全局 API,用于在下一轮 DOM 更新循环之后执行延迟回调。它的语法如下:
Vue.nextTick(callback)其中,callback 是一个函数,它将在下一轮 DOM 更新循环中被调用。
由于在 Vue.js 中,数据更新是异步的,所以在某些情况下,您可能无法立即得到数据更新的最新值。在这种情况下,你可以使用 nextTick() 来确保你的回调在 Vue.js 更新 DOM 后执行。
示例:
