Skip to content

Z01-05 前端常用-项目:vue3-ts

[TOC]

环境搭建

技术栈

  • Vue3:vue@3.3.2
  • TS5:
  • Vite4:vite@4.3.5。使用create-vue@3.6.4创建项目
  • Pinia2:pinia@2.1.3
  • VueRouter4: vue-router@4.2.2
  • Node16:node@16.19.0

vscode 插件

推荐: Vue - Officialv2.0.8

问题:Vue - Official的 2.0.x 早期版本有问题

解决: 目前版本(v2.0.8)已解决该问题

废弃插件: Vue Language Features (Volar)TypeScript Vue Plugin (Volar)

问题: 在启用 TypeScript Vue Plugin (Volar) 的情况下,vscode 不能识别 vue 文件的组件返回类型

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

项目初始化

创建项目

1、使用create-vue 工具创建mr-vue3-ts-cms项目。create-vue 是基于 vite 的脚手架工具

sh
$ pnpm create vue@latest

2、创建选项

image-20230602133349666

目录结构

sh
  .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
    └─views

说明: 3 个 tsconfig 文件之间的关系

image-20230602142445302

配置 icon,标题

1、配置 icon

直接复制自己的 icon 到 public 中

2、配置标题

  • 直接在index.html模版文件中修改

    html
    <!-- index.html -->
    <title>木头人 - 后台管理</title>
  • 通过 JS 动态修改

    js
    document.title = '木头人 - 后台管理'

重置 CSS 样式

1、normalize.css

依赖包: normalize.css

安装:npm i normalize.css

导入:main.ts 中导入

ts
import 'normalize.css'

2、reset.less

自定义 reset.less 重置样式

css
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
dl,
dd,
ul,
ol,
li,
form,
input,
textarea,
th,
td,
select,
div,
section,
nav,
span,
i {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
em {
  font-style: normal;
}
li {
  list-style: none;
}
a {
  text-decoration: none;
  color: #333;
}
img {
  border: none;
  vertical-align: top;
}
/* img { font-size: 0; } */
input,
textarea {
  outline: none;
}
textarea {
  resize: none;
  overflow: auto;
}
body {
  font-size: 14px;
}

3、common.less

公共样式:common.less

less
/* 设置主题样式 */
:root {
  --el-text-color-placeholder: #d3d5d8 !important;
}

/* 设置公共样式 */

/* 文字溢出省略号 */
.ellipsis-single {
  overflow: hidden; // 溢出隐藏
  text-overflow: ellipsis; // 溢出用省略号显示

  white-space: nowrap; // 规定段落中的文本不进行换行
}

.ellipsis-multi {
  overflow: hidden; // 溢出隐藏
  text-overflow: ellipsis; // 溢出用省略号显示

  display: -webkit-box; // -webkit-, 作为弹性伸缩盒子模型显示
  -webkit-box-orient: vertical; // -webkit-, 设置伸缩盒子的子元素排列方式:从上到下垂直排列
  -webkit-line-clamp: 2; // -webkit-, 显示的行数
}

4、vite 默认不能识别less文件,需要安装less

依赖包: less

安装: npm i less -D

vue 文件类型声明

问题: 项目本身的vue模块声明并不能识别出 App 是一个组件

js
import App from './App.vue'

解决: 重新声明 vue 模块,使得 ts 可以识别出 vue 是一个组件

ts
// env.d.ts
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const src: DefineComponent
  export default src
}

代码规范

配置 editorconfig

.editorconfig 有助于为不同 IDE 编辑器上处理同一项目的多个开发人员维护一致的编码风格

1、配置项

sh
# 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

2、安装 vscode 插件:EditorConfig for VS Code

配置 Prettier

Prettier 是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。

1、依赖包: prettier

2、安装: npm install prettier -D

3、配置: 配置.prettierrc或者.prettierrc.json文件

json
{
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 100,
  "singleQuote": true,
  "trailingComma": "none",
  "semi": false
}

说明:

  • useTabs:使用 tab 缩进还是空格缩进,选择 false;
  • tabWidth:tab 是空格的情况下,是几个空格,选择 2 个;
  • printWidth:当行字符的长度,推荐 80,也有人喜欢 100 或者 120;
  • singleQuote:使用单引号还是双引号,选择 true,使用单引号;
  • trailingComma:在多行输入的尾逗号是否添加,设置为 none 表示不加;
  • semi:语句末尾是否要加分号,默认值 true,选择 false 表示不加;

4、忽略文件: 创建.prettierignore忽略文件

/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*

5、插件: 安装 vscode 插件:Prettier - Code formatter

6、测试: 测试 prettier 是否生效

  • 测试一:在代码中保存代码;

    可以通过插件Prettier - Code formatter实现

  • 测试二:配置一次性修改的命令;

    在 package.json 中配置一个 scripts:

    sh
    "prettier": "prettier --write ."

7、自动格式化: 让 prettier 在保存时自动格式化

  • 1 在 vscode 中安装 Prettier 扩展
  • 2 在设置中搜索format on save ,选中Editor: Format On Save
  • 3 在设置中搜索default format,设置Editor: Default FormatterPrettier - Code formatter
  • 4 配置.prettierrc(见步骤 3)
  • 5 实现保存代码时自动格式化

配置 Eslint

1、安装: 在前面创建项目的时候,我们就选择了 ESLint,所以 Vue 会默认帮助我们配置需要的 ESLint 环境。

2、插件: 安装 vscode 插件:ESLint

3、问题: 解决 eslint 和 prettier 冲突的问题

解决:

  • 1 安装插件:(vue 在创建项目时,如果选择 prettier,那么这两个插件会自动安装)

    • eslint-plugin-prettier(主要)
    • eslint-config-prettier
    sh
    pnpm i eslint-plugin-prettier eslint-config-prettier -D
  • 2 修改.eslintrc.cjs 配置

    js
      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 依然报错了

解决: 手动修改 eslint 检测规则,屏蔽报错

  • 1 需要修改的报错:

    • @typescript-eslint/no-unused-vars:未使用的变量名

    • vue/multi-word-component-names:检测当前的组件名称是否使用驼峰或多单词命名

  • 2 在出现提示的位置,复制出现的错误:vue/multi-word-component-names

    image-20240304094642473

  • 3 在.eslintrc.cjs 中关闭这些检测规则

    js
    module.exports = {
    +  rules: {
    +    '@typescript-eslint/no-unused-vars': 'off',
    +    'vue/multi-word-component-names': 'off'
    +  }
    }

配置 Git

husky

husky是一个 git hook 工具,可以帮助我们触发 git 提交的各个阶段钩子pre-commitcommit-msgpre-push

痛点: 虽然我们已经要求项目使用 eslint 了,但是不能保证组员提交代码之前都将 eslint 中的问题解决掉了。也就是我们希望保证代码仓库中的代码都是符合 eslint 规范的。就需要在组员执行 git commit 命令的时候对其进行校验,如果不符合 eslint 规范,那么自动通过规范进行修复。

安装:

0、依赖包: husky

1、使用自动配置命令安装husky

sh
# npm
npx husky-init && npm install

# pnpm(推荐)
pnpm dlx husky-init && pnpm install

注意: 在 windows 的 powershell 中需要给&&添加引号:pnpm dlx husky-init '&&' pnpm install

说明: 这里会做三件事:

  • 1 安装 husky 相关的依赖

    image-20230615162857405

  • 2 在项目目录下创建 .husky 文件夹

    image-20230615162908690

  • 3 在package.json中添加一个脚本

    image-20230615162943102

2、修改.husky/pre-commit文件,添加pnpm exec lint

image-20240529105202611

说明: 此时执行git commit的时候会自动对代码进行 lint 校验

优化: 暂存区 eslint 校验

由于使用pnpm lint校验时,会对所有文件都进行校验,耗时久。

为解决以上问题,就出现了 lint-staged 插件,它可以只对有改动的文件进行校验,从而大大优化了检验速度。

1、依赖包: lint-staged

2、安装: pnpm i lint-staged -D

3、配置: 配置lint-staged

  • 1 在package.json中配置lint-staged命令

    image-20240528100558788

  • 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文件

    image-20240529105642792

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

commitizen

commitizen 是一个帮助我们编写规范 commit message 的工具。

痛点: 通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。

image-20240529110013039

但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:commitizen

安装:

1、依赖包:

  • commitizen
  • cz-conventional-changelog

2、安装:

  • 1 安装commitizen

    sh
    #npm
    npm install commitizen -D
    
    #pnpm
    pnpm install commitizen -D
  • 2 安装并初始化cz-conventional-changelog

    sh
    # npm
    npx commitizen init cz-conventional-changelog --save-dev --save-exact
    
    # pnpm
    pnpm dlx commitizen init cz-conventional-changelog --save-dev --save-exact --pnpm

    说明: 该命令做了以下事情:

    • a 帮助安装cz-conventional-changelog

      image-20230615164205198

    • b 在package.json中进行配置

      image-20230615164358905

  • 3 在package.json中添加 scripts

    json
    scripts: {
        "commit": "cz"
    }
  • 4 提交 git 时使用以下命令:

    sh
    pnpm run commit

问题:commitizen 配置在package.json中时,进行 git 提交会报错

【补充:报错图片】

解决:commitizen的配置单独写入创建的.czrc配置文件中

json
{
  "path": "./node_modules/cz-conventional-changelog"
}

提交: 使用commitizen提交 git 时的步骤:

  • 输入命令: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发布新版本
  • 第二步选择本次修改的范围(作用域)
sh
? What is the scope of this change (e.g. component or file name): (press enter to skip) git
  • 第三步选择提交的信息
sh
? Write a short, imperative tense description of the change (max 89 chars): 安装了husky
  • 第四步提交详细的描述信息
sh
? Provide a longer description of the change: (press enter to skip)
  • 第五步是否是一次重大的更改
sh
? Are there any breaking changes? (y/N) n
  • 第六步是否影响某个 open issue
sh
? Does this change affect any open issues? (y/N) n
commitlint

commitlint 是一个 git commit 校验约束工具

就是当我们运行git commmit -m 'xxx'时,来检查'xxx'是不是满足团队约定好的提交规范的工具。

安装:

1、依赖包:

  • @commitlint/config-conventional
  • @commitlint/cli

2、安装: 安装 @commitlint/config-conventional@commitlint/cli

sh
# npm
npm i @commitlint/config-conventional @commitlint/cli -D

# pnpm
pnpm add @commitlint/config-conventional @commitlint/cli -D

3、配置: 在根目录创建commitlint.config.js文件,配置commitlint

js
module.exports = {
  extends: ['@commitlint/config-conventional']
}

4、使用: 使用 husky 生成 commit-msg 文件,验证提交信息:

sh
# 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"

image-20240529112251218

问题: commit-msg 中使用pnpm dlx时,会和--no-install 参数冲突

【补充:报错图片】

解决: 使用npx --no-install 代替 pnpm dlx

问题: vite 环境中的.js | .ts文件不识别module.exports这种 CJS 语法

image-20240304112335565

解决: 有 2 种解决方法:

  • 方法 1: 修改commitlint.config.js文件后缀为.cjs,此时就可以解析 commonJS 代码了

  • 方法 2:通过快速修复,暂时屏蔽 eslint 检测,因为这个是误报(该方法在 git 提交校验时会报错)

    js
    + // eslint-disable-next-line no-undef
    module.exports = {
      extends: ['@commitlint/config-conventional']
    }

区分环境

Vite 在一个特殊的 import.meta.env 对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:

  • import.meta.env.MODE: {string} 应用运行的模式。(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

image-20240722090148929

方法 3: 自定义环境常量

image-20240722090203876

1、创建.env.development.env.production文件

2、分别在文件中定义不同的常量(注意:常量名必须以VITE_开头)

image-20240722090220965

3、通过import.meta.env.VITE_XXX 获取定义的常量

第三方库

核心库

vue-router

1、安装 vue-router 的最新版本:

shell
npm install vue-router@next

2、创建 router 对象:

ts
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 router

3、安装 router:

ts
import router from './router'

createApp(App).use(router).mount('#app')

4、在 App.vue 中配置跳转:

html
<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

sh
npm i pinia

2、创建 pinia 对象

ts
import { createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

3、挂载 pinia

ts
+ import pinia from './store'

const app = createApp(App)
+ app.use(pinia)
app.mount('#app')

4、创建 store

ts
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 useCounterStore

5、使用 store

获取 counter

html
<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

html
<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:

shell
npm install vuex@next

2、创建 store 对象:

ts
import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      name: 'coderwhy'
    }
  }
})

export default store

3、安装 store:

ts
createApp(App).use(router).use(store).mount('#app')

4、在 App.vue 中使用:

html
<h2>{{ $store.state.name }}</h2>

UI 库

element-plus

Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库:

  • 相信很多同学在 Vue2 中都使用过 element-ui,而 element-plus 正是 element-ui 针对于 vue3 开发的一个 UI 组件库;
  • 它的使用方式和很多其他的组件库是一样的,所以学会 element-plus,其他类似于 ant-design-vue、NaiveUI、VantUI 都是差不多的;

安装 element-plus

shell
npm install element-plus
完整引入

一种引入 element-plus 的方式是全局引入,代表的含义是所有的组件和插件都会被自动注册:

js
import { createApp } from 'vue'
+ import ElementPlus from 'element-plus'
+ import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)

+ app.use(ElementPlus)
app.mount('#app')

volar 支持

如果您使用 Volar,请在 tsconfig.json 中通过 compilerOptions.type 指定全局组件类型。

json
// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": ["element-plus/global"]
  }
}
按需引入

也就是在开发中用到某个组件对某个组件进行引入:

vue
<template>
  <div id="app">
    <router-link to="/login">登录</router-link>
    <router-link to="/main">首页</router-link>
    <router-view></router-view>

    <h2>{{ $store.state.name }}</h2>

    <el-button>默认按钮</el-button>
    + <el-button type="primary">主要按钮</el-button> + <el-button type="success">成功按钮</el-button> +
    <el-button type="info">信息按钮</el-button> + <el-button type="warning">警告按钮</el-button> +
    <el-button type="danger">危险按钮</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

+ import { ElButton } from 'element-plus'

export default defineComponent({
  name: 'App',
+  components: {
+    ElButton
+  }
})
</script>

<style lang="less"></style>

但是我们会发现是没有对应的样式的,引入样式有两种方式:

  • 全局引用样式(像之前做的那样);

  • 局部引用样式(通过 babel 的插件);

    1.安装 babel 的插件:

shell
npm install babel-plugin-import -D

2.配置 babel.config.js

js
module.exports = {
  plugins: [
    [
      'import',
      {
        libraryName: 'element-plus',
        customStyleName: (name) => {
          return `element-plus/lib/theme-chalk/${name}.css`
        }
      }
    ]
  ],
  presets: ['@vue/cli-plugin-babel/preset']
}

但是这里依然有个弊端:

  • 这些组件我们在多个页面或者组件中使用的时候,都需要导入并且在 components 中进行注册;
  • 所以我们可以将它们在全局注册一次;
ts
import {
  ElButton,
  ElTable,
  ElAlert,
  ElAside,
  ElAutocomplete,
  ElAvatar,
  ElBacktop,
  ElBadge,
} from 'element-plus'

+ const app = createApp(App)

const components = [
  ElButton,
  ElTable,
  ElAlert,
  ElAside,
  ElAutocomplete,
  ElAvatar,
  ElBacktop,
  ElBadge
]

+ for (const cpn of components) {
+  app.component(cpn.name, cpn)
+ }
自动按需引入(推荐)

首先你需要安装unplugin-vue-componentsunplugin-auto-import这两款插件

sh
npm install unplugin-vue-components unplugin-auto-import -D

然后把下列代码插入到你的 ViteWebpack 的配置文件中

Vite

1、设置vite.config.ts,添加插件ComponentsComponents

ts
// vite.config.ts
import { defineConfig } from 'vite'
+ import AutoImport from 'unplugin-auto-import/vite'
+ import Components from 'unplugin-vue-components/vite'
+ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  // ...
+  plugins: [
    // ...
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
++      dts: 'auto-imports.d.ts' // 重点
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
++      dts: 'components.d.ts' // 重点
+    }),
  ],
})

2、修改tsconfig.app.json,添加"auto-imports.d.ts", "components.d.ts"include

json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], // 重点
  "exclude": ["src/**/__tests__/*", "commitlint.config.js"],
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Webpack

ts
// webpack.config.js
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {
  // ...
  plugins: [
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
}

类型提示设置

tsconfig.json 中将安装的 2 个插件对应的类型是声明文件添加到include

image-20240722090247290

vant

安装

安装: 推荐使用 按需自动导入 的方式安装vant

0、 依赖包:

  • 核心包
  • vant
  • 自动导入插件
  • @vant/auto-import-resolver
  • unplugin-vue-components
  • unplugin-auto-import

1、安装:pnpm add vant

2、导入样式:main.ts中添加 vant 样式(最新版已经不需要该操作

ts
import 'vant/lib/index.css'

3、按需自动导入:(新版:2024-5-29)

  • 1 安装自动导入插件

    sh
    # 通过 pnpm 安装
    pnpm add @vant/auto-import-resolver unplugin-vue-components unplugin-auto-import -D
  • 2 配置插件

    如果是基于 Vite 的项目,在 vite.config.js 文件中配置插件

    ts
    import vue from '@vitejs/plugin-vue';
    
    + import AutoImport from 'unplugin-auto-import/vite';
    + import Components from 'unplugin-vue-components/vite';
    + import { VantResolver } from '@vant/auto-import-resolver';
    
    export default {
      plugins: [
        vue(),
    +    AutoImport({
    +      resolvers: [VantResolver()],
        }),
    +    Components({
    +      resolvers: [VantResolver()],
        }),
      ],
    };

4、配置 vant 组件类型提示: 修改tsconfig.app.json,添加"auto-imports.d.ts", "components.d.ts"include

json
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"], // 重点
  "exclude": ["src/**/__tests__/*", "commitlint.config.js"],
  "compilerOptions": {
    "composite": true,
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",

    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

5、使用: 使用组件和 API

html
<!-- 使用组件 -->
<template>
  <van-button type="primary" />
</template>
html
<!-- 使用API -->
<script>
  showToast('No need to import showToast')
</script>

说明: 可以直接在模板中使用 Vant 组件,unplugin-vue-components 会解析模板并自动注册对应的组件, @vant/auto-import-resolver 会自动引入对应的组件样式。

补充:

按需自动导入:(旧版)

  • 依赖包: unplugin-vue-components

  • 1 安装插件

    sh
    pnpm i unplugin-vue-components -D
  • 2 配置插件

    js
    // vite.config.js
    import vue from '@vitejs/plugin-vue';
    + import Components from 'unplugin-vue-components/vite';
    + import { VantResolver } from 'unplugin-vue-components/resolvers';
    
    export default {
      plugins: [
        vue(),
    +    Components({
    +      resolvers: [VantResolver()],
    +    }),
      ],
    };
  • 3 使用组件

    html
    <template>
      <van-button type="primary" />
    </template>

工具库

axios

1、安装 axios

shell
npm install axios

2、封装 axios

ts
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 HYRequest

postcss-px-to-viewport

postcss-px-to-viewport是将 px 单位转换为视口单位 (vw, vh, vmin, vmax) 的 PostCSS 插件

官网: https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md

安装:

依赖包:

  • postcss-px-to-viewport(废弃)
  • postcss-px-to-viewport-8-plugin

1、安装: pnpm i postcss-px-to-viewport -D

2、配置:postcss.config.cjs添加如下配置

js
// eslint-disable-next-line no-undef
module.exports = {
  plugins: {
    'postcss-px-to-viewport': {
      // options
      viewportWidth: 375 // 设备宽度375计算vw的值
    }
  }
}

注意: 在配置 postcss-loader 时,应避免 ignore node_modules 目录,否则将导致 Vant 样式无法被编译

问题: postcss-px-to-viewport插件已经废弃,控制台会报警告:

image-20240529141927210

解决: 使用postcss-px-to-viewport-8-plugin 代替

1、安装: pnpm i postcss-px-to-viewport-8-plugin -D

2、配置:postcss.config.cjs添加如下配置

js
// eslint-disable-next-line no-undef
module.exports = {
  plugins: {
    // 'postcss-px-to-viewport': {
    'postcss-px-to-viewport-8-plugin': {
      // options
      viewportWidth: 375 // 设备宽度375计算vw的值
    }
  }
}

pinia-plugin-persistedstate

pinia-plugin-persistedstate 丰富的功能可以使 Pinia Store 的持久化更易配置。本插件兼容 pinia^2.0.0

官网: https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/

安装:

依赖包: pinia-plugin-persistedstate

1、安装: pnpm i pinia-plugin-persistedstate

2、配置: 将插件添加到 pinia 实例上

js
import { createPinia } from 'pinia'
+ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
+ pinia.use(piniaPluginPersistedstate)

3、使用: 创建 Store 时,将 persist 选项设置为 true

js
// 组合式语法
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useStore = defineStore(
  'main',
  () => {
    const someState = ref('你好 pinia')
    return { someState }
  },
  {
+    persist: true,
  },
)
js
// 选项式语法
import { defineStore } from 'pinia'

export const useStore = defineStore('main', {
  state: () => {
    return {
      someState: '你好 pinia',
    }
  },
+  persist: true,
})

vite-plugin-svg-icons

vite-plugin-svg-icons用于生成 svg 雪碧图。根据 icons 文件 svg 图片打包到项目中,通过组件使用图标

安装:

依赖包: vite-plugin-svg-icons

1、安装: pnpm i vite-plugin-svg-icons -D

2、配置:vite.config.ts中的配置插件

js
+ import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+ import path from 'path'

export default defineConfig({
  plugins: [
    vue(),
+    createSvgIconsPlugin({
+      // 指定图标文件夹,绝对路径(NODE代码)
+      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')]
    })
  ],

3、导入到main.ts

ts
+ import 'virtual:svg-icons-register'

4、使用: 使用 svg 精灵地图

html
<svg aria-hidden="true">
  <!-- #icon-文件夹名称-图片名称 -->
  <use href="#icon-login-eye-off" />
</svg>

封装 svg 组件

1、定义全局组件CpIcon

html
<script setup lang="ts">
  // 提供name属性即可
  defineProps<{
    name: string
  }>()
</script>

<template>
  <svg aria-hidden="true" class="cp-icon">
    <use :href="`#icon-${name}`" />
  </svg>
</template>

<style lang="scss" scoped>
  .cp-icon {
    // 和字体一样大
    width: 1em;
    height: 1em;
  }
</style>

2、定义CpIcon组件类型

ts
import CpNavBar from '@/components/CpNavBar.vue'
+ import CpIcon from '@/components/CpIcon.vue'

declare module 'vue' {
  interface GlobalComponents {
    CpNavBar: typeof CpNavBar
+    CpIcon: typeof CpIcon
  }
}

3、使用组件

html
<van-field v-model="password" placeholder="请输入密码">
  <template #button> + <cp-icon :name="login-eye-off"></cp-icon> </template>
</van-field>

nprogress

ajax 请求的进度条

使用文档:https://juejin.cn/post/7136118122867752990#heading-11

安装

依赖包: nprogress

1、安装:

sh
# 导入nprogress
npm i nprogress
# 导入TS类型
pnpm add @types/nprogress -D

2、引入:

js
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

常见用法: 常见的用法是在 路由导航守卫拦截器 中使用

  • 路由导航守卫

    js
    router.beforeEach(async(to, from, next) => {
    +    NProgress.start();
    }
    router.afterEach(() => {
    +    NProgress.done();
    });
  • 拦截器

    js
    //请求拦截器
    instance.interceptors.request.use(
      (config) => {
        ;+NProgress.start()
        //...
      },
      (error) => {
        //...
      }
    )
    //响应拦截器
    instance.interceptors.response.use(
      (response) => {
        //响应成功
        ;+NProgress.done()
        //...
      },
      (error) => {
        ;+NProgress.done()
        //...
      }
    )

常用 API

  • NProgress.configure(opts)opts,配置项
    • showSpinnerboolean默认:true,控制是否显示进度条右下方加载的小圆圈动画
    • easingease | ease-out |... 默认:ease,CSS 缓动字符串
    • speednumber默认:200,动画速度,单位 ms
    • minimumnumber默认:0.08,更改启动时使用的最小百分比
    • parentCSS选择器默认:body,指定此选项可更改父容器

修改样式

css
/* common.scss */

/* 修改NProgress样式 */
#nprogress .bar {
  background: var(--cp-primary) !important;
}

vue-use

文档:https://vueuse.org/functions.html#category=State

组合式 Vue 的工具函数集

安装

1、依赖包: @vueuse/core

2、安装: npm i @vueuse/core

常用 API
  • Elements
  • useWindowSize(),获取实时的 window 尺寸。原生:window.innerWidth
    • 返回值{width, height},返回 window 的尺寸
      • width默认:,window 宽度
      • height默认:,window 高度
基本使用

image-20240607145214033

image-20240607145240729

socket.io-client【】

Socket.IO: 是一个库,可以在客户端和服务器之间实现 低延迟, 双向基于事件的 通信。

常用 API
基本使用

echarts

API
  • echarts
  • echarts.init(dom, theme?, opt?)
  • echarts.registerMap(mapName, opt | geoJSON)
  • echartsInstance
  • echartsInstance.setOption()
安装

依赖包: echarts

安装: pnpm add echarts

基本使用

1、指定 echarts 的容器

image-20240525154835097

image-20240525154853524

2、引入 echarts

image-20230615124105848

image-20240525155723490

封装 Echarts

1、目录结构

image-20230615124452436

组件:BaseEchart

1、页面布局

image-20230615125126415

image-20230615125420240

2、使用组件

image-20230615125334347

image-20230615125354875

组件:PieEchart

1、使用组件

image-20230615130452802

image-20230615130519004

2、使用base-echart组件,并传入option

image-20230615130128920

3、option 配置

image-20240525170835619

4、修改 BaseEchart,使用传递进来的 option

image-20230615130028473

5、统一导出

image-20240525163717360

组件:LineEchart

1、使用组件

image-20230615133840237

2、配置

image-20240525175317738

image-20240525174559657

3、请求商品销量数据

见:动态渲染数据

4、转化请求的商品销量数据

image-20230615134159958

5、传递数据

image-20240525175202646

组件:RoseEchart

1、使用组件

image-20240525173222944

2、页面布局

image-20230615132809567

3、修改 BaseEchart

image-20230615131911311

组件:BarEchart

1、store

image-20230615134353162

2、组件

image-20240525175502358

3、渲染数据

image-20230615134517788

4、配置

image-20240525175310459

image-20240525174518446

组件:MapEchart

1、注册 map

image-20230615153525832

2、页面布局

image-20240525181415572

image-20240525182118808

3、获取城市经纬度工具函数

image-20240525182147366

4、经纬度数据

image-20240525182151115

常见功能

TS 类型【】

全局类型【】

如果定义的类型,在 views、store、service 等多个地方都会用到,就放入src/types

StoreState

定义 store 中的 state 的类型

ts
+  interface ILoginState {
+    token: string
+    userInfo: any
+    roleMenuTreeInfo: any
+  }
  const useLoginStore = defineStore('login', {
+    state: (): ILoginState => ({
      token: localCache.getCache('token') ?? '',
      userInfo: {},
      roleMenuTreeInfo: {}
    }),
  })

icon

导入 icon

0、依赖包: @element-plus/icons-vue

1、安装图标:npm install @element-plus/icons-vue

2、全局注册图标:

ts
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

3、对上面的方法进行封装:

使用

ts
import registerIcons from './global/registerIcons'
const app = createApp(App) + app.use(registerIcons)

封装

ts
// 注册element-plus图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

function registerIcons(app: App<Element>) {
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
  }
}

export default registerIcons

4、在 label 插槽中添加图标

html
<!-- 登录表单 -->
<el-tabs v-model="activeName" class="tabs" type="border-card" stretch>
  <el-tab-pane label="帐号登录" name="account">
    +
    <template #label>
      <div class="label">
        + <el-icon><UserFilled /></el-icon> + <span class="text">帐号登录</span>
      </div>
    </template>
  </el-tab-pane>
  <el-tab-pane label="手机登录" name="phonse">
    +
    <template #label>
      <div class="label">
        + <el-icon><Iphone /></el-icon> + <span class="text">手机登录</span>
      </div>
    </template>
  </el-tab-pane>
</el-tabs>

icon 地图

见: vite-plugin-svg-icons

进度条

nprogress

见:nprogress

本地缓存

封装 Cache

1、本地缓存-基本使用

ts
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

ts
class Cache {
  setCache(key: string, value: any) {
    if (value) {
      localStorage.setItem(key, JSON.stringify(value))
    }
  }

  getCache(key: string) {
    const value = localStorage.getItem(key)
    if (value) {
      return JSON.parse(value)
    }
  }

  removeCache(key: string) {
    localStorage.removeItem(key)
  }

  clear() {
    localStorage.clear()
  }
}

export default new Cache()

3、本地缓存-封装-兼容 localStorage、sessionStorage

ts
enum TCache {
  Local,
  Session
}
class Cache {
  storage: Storage
  constructor(type: TCache) {
    this.storage = type === TCache.Local ? localStorage : sessionStorage
  }

  setCache(key: string, value: any) {
    if (value) {
      this.storage.setItem(key, JSON.stringify(value))
    }
  }

  getCache(key: string) {
    const value = this.storage.getItem(key)
    if (value) {
      return JSON.parse(value)
    }
  }

  removeCache(key: string) {
    this.storage.removeItem(key)
  }

  clear() {
    this.storage.clear()
  }
}

const localCache = new Cache(TCache.Local)
const sessionCache = new Cache(TCache.Session)

export { localCache, sessionCache }

4、本地缓存-封装-使用

ts
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)
    }
  }
})

Store 持久化

见:pinia-plugin-persistedstate

记住密码

思路: 记住密码状态isRemPwd在父组件中,而帐号和密码在子组件中,可以将isRemPwd通过loginAction(isRemPwd)传递给子组件

1、传递 isRemPwd 到子组件

ts
function loginClickHdl() {
  if (activeName.value === 'account') {
    // 调用PaneAccount组件内部方法
    ;+accountRef.value?.loginHdl(isRemPwd.value) // isRemPwd.value
  } else {
    console.log('通过手机登录')
  }
}

2、登录成功后缓存帐号、密码

ts
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、修改初始化帐号、密码

ts
const account = reactive({
+  name: localCache.getCache('name') ?? '',
+  password: localCache.getCache('password') ?? ''
})

4、未勾选记住密码时,移除缓存

ts
// 登录成功,记住密码
if (isRemPwd) {
  localCache.setCache('name', account.name)
  localCache.setCache('password', account.password)
} else {
  ;+localCache.removeCache('name') + localCache.removeCache('password')
}

5、缓存记住密码状态

在父组件中 watch 监听 isRemPwd 的值

ts
/* 缓存记住密码状态 */
const isRemPwd = ref<boolean>(localCache.getCache('isRemPwd') ?? false)
watch(isRemPwd, (newValue) => {
  localCache.setCache('isRemPwd', newValue)
})

token

携带 token

1、*方法一:*在每次请求中添加headers.Authorization

ts
/* 获取用户详细信息 */
export function getUserInfo(id: number) {
  return mrRequest.get({
    url: `/users/${id}`
+    headers: {
+      Authorization: 'Bearer ' + localCache.getCache('token')
+    }
  })
}

2、方法二: 在拦截器中添加headers.Authorization推荐

ts
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
    },
    ...
  }
})

表单校验

校验规则

校验规则选项

  • type:指定输入数据的类型

  • required:表示是否必填,值为 true 或 false。

  • message:表示校验不通过时的提示消息。

  • trigger:表示触发校验的事件类型,可以是 blur、change 等。

  • min 和 max:分别表示输入值的最小值和最大值。例如 min:3 表示输入的值必须大于等于 3。

  • pattern:表示输入值的正则表达式,例如 pattern:/^[a-z]+$/i 表示输入的值必须由字母构成。

  • len:表示输入值的长度,例如 len:6 表示输入的值必须恰好为 6 个字符。

工具函数

时间格式化

依赖包:dayjs

1、安装

sh
npm i dayjs

2、封装 dayjs

ts
import dayjs from 'dayjs'
+ import utc from 'dayjs/plugin/utc'

// 继承utc
+ dayjs.extend(utc)

export function formatUTC(utcString: string, format: string = 'YYYY-MM-DD HH:mm:ss') {
+  return dayjs.utc(utcString).format(format)
}

3、转成东八区时间

ts
export function formatUTC(utcString: string, format: string = 'YYYY-MM-DD HH:mm:ss') {
+  return dayjs.utc(utcString).utcOffset(8).format(format)
}

4、使用封装的方法进行格式化

html
<script setup lang="ts">
  + import { formatUTC } from '@/utils/format
</script>

<el-table-column align="center" prop="createAt" label="创建时间">
  <template #default="scope">{{ formatUTC(scope.row.createAt) }}</template>
</el-table-column>
<el-table-column align="center" prop="updateAt" label="更新时间">
  <template #default="scope">{{ formatUTC(scope.row.updateAt) }}</template>
</el-table-column>

map 函数【】

element-plus

element 国际化

方法一: 全局引入

ts
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'

app.use(ElementPlus, {
  locale: zhCn
})

方式二: 自动按需引入(推荐)

html
<template>
  <div class="app">
    +
    <el-config-provider :locale="zhCn">
      <RouterView />
    </el-config-provider>
  </div>
</template>

<script setup lang="ts">
  +  import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

添加.mjs文件声明

ts
declare module '*.mjs'

效果:

image-20240322161522498

动画

数据动画

1、动画插件:npm i countup.js

2、使用 countup 添加动画

image-20230615122630807

3、添加人民币符号

image-20230615123017431

权限管理

权限管理系统-RBAC

RBAC (Role-Based Access Control) 即基于角色的访问控制,是一种常见的访问控制机制,用于管理用户和资源之间的访问权限。RBAC 基于角色对用户进行授权,而不是直接授予用户特定的权限集合。通过这种方式,RBAC 可以使权限管理更易于管理和扩展。

在 RBAC 中,有三个核心概念:用户、角色和权限。其中:

  • 用户:系统中的每个用户都有唯一的标识符,可以被分配到一个或多个角色。
  • 角色:每个角色代表了一组权限集合,包含了访问系统中的某些资源所需的权限。角色可以被分配给一个或多个用户。
  • 权限:权限是指访问系统中某些资源所需的能力,例如读取、写入或执行某些操作等。

通过将用户分配到角色,并为每个角色分配适当的权限,RBAC 可以简化权限管理过程,减少管理员管理的工作量,同时也可以帮助保证系统安全性。

页面访问权限控制

1、在 router/index.ts 中添加导航守卫

ts
/* 路由导航守卫 */
router.beforeEach((to, from) => {
  const token = localCache.getCache('token')
  if (to.path === '/main' && !token) {
    return '/login'
  }
})

动态路由菜单【】

分配权限

新建角色时分配权限

定义配置
ts
formItems: [
  { type: 'input', label: '角色名称', prop: 'name', placeholder: '请输入角色名称' },
  { type: 'input', label: '权限介绍', prop: 'intro', placeholder: '请输入权限介绍' },
  +{ type: 'custom', label: '分配权限', slotName: 'menuList' }
]
自定义插槽
html
<template v-else-if="item.type === 'custom'">
  <slot :name="item.slotName"></slot>
</template>
请求完整菜单树数据

1、service

ts
/* 获取权限列表 */
export function postMenuLists() {
  return mrRequest.post({
    url: '/menu/list'
  })
}

2、store

ts
    async postMenuListsAction() {
      const res = await postMenuLists()
      this.menuLists = res.data.list
    }

3、组件 Role 中

ts
const mainStore = useMainStore()
const { menuLists } = storeToRefs(mainStore)
const defaultProps = {
  children: 'children',
  label: 'name'
}
html
<PageModal :modal-config="modalConfig" :other-info="otherInfo" ref="modalRef">
  +
  <template #menuList>
    <el-tree
      +
      :data="menuLists"
      +
      show-checkbox
      +
      node-key="id"
      +
      :props="defaultProps"
      ref="treeRef"
      @check="hdlSelectChecked"
    />
  </template>
</PageModal>
创建角色时带权限

1、点击权限树后获取选中项的 id

html
<el-tree
  :data="menuLists"
  show-checkbox
  node-key="id"
  :props="defaultProps"
  ref="treeRef"
  +
  @check="hdlSelectChecked"
/>
ts
/* 获取选中的权限节点 */
const otherInfo = ref({})
function hdlSelectChecked(param1: any, param2: any) {
  const menuList = [...param2.checkedKeys, ...param2.halfCheckedKeys]
  otherInfo.value = { menuList }
}

hdlSelectChecked的 2 个参数:

image-20240524180919888

2、传递额外的数据 otherInfo 到 PageModal 组件中

html
<PageModal :modal-config="modalConfig" + :other-info="otherInfo" ref="modalRef"></PageModal>

3、在 PageModal 组件中接收数据

ts
export interface IModalProps {
  modalConfig: IModalConfig
+  otherInfo?: any
}
const props = defineProps<IModalProps>()

4、合并 otherInfo 和 formData

ts
function hdlSubmitUser() {
  modalVisiable.value = false
+  pageInfo.value = { ...pageForm, ...props.otherInfo }
  console.log('pageInfo', pageInfo.value)
  // 验证表单
  formRef.value?.validate((valid: any) => {
    if (valid) {
      // 验证成功
      if (isEdit.value) {
+        systemStore.editPageAction(props.modalConfig.pageName, pageId.value, pageInfo.value)
        ElMessage.success('哈哈,修改用户成功~')
      } else {
+        systemStore.addPageAction(props.modalConfig.pageName, pageInfo.value)
        ElMessage.success('哈哈,新增用户成功~')
      }
    } else {
      // 验证失败
      ElMessage.error('呜呼,验证失败,请重新来过~')
    }
  })
}
权限菜单回显

1、绑定 ElTree 的 ref

html
<el-tree
  :data="menuLists"
  show-checkbox
  node-key="id"
  :props="defaultProps"
  +
  ref="treeRef"
  @check="hdlSelectChecked"
/>

2、在 Role 组件中定义回调函数,并将其传递给 usePageModal 中,目的是为了获取数据 itemData

ts
+  const { modalRef, hdlChangeVisiable, hdlEditClick } = usePageModal(editCB)
/* 点击编辑,回显权限 */
const treeRef = ref<InstanceType<typeof ElTree>>()
+  function editCB(pageItem: any) {
  // console.log('pageItem: ', pageItem.menuList)
  nextTick(() => {
    const checkedKeys = mapMenuListToIds(pageItem.menuList)
    treeRef.value?.setCheckedKeys(checkedKeys)
  })
}

3、在 Hook 中接收回调函数

ts
function usePageModal(editCB: (pageItem: any) => void) {
  /* 修改对话框是否显示 */
  // const modalRef = ref<InstanceType<typeof PageModal>>()
  const modalRef = ref<any>()
  function hdlChangeVisiable() {
    modalRef.value?.changeModalVisiable()
  }

  /* 调用模态组件内函数,修改用户 */
  function hdlEditClick(pageItem: any) {
    modalRef.value?.changeModalVisiable(pageItem)
+    if (editCB) editCB(pageItem)
  }

  return {
    modalRef,
    hdlChangeVisiable,
    hdlEditClick
  }
}

4、定义一个获取数组中 id 的工具函数

ts
/**
 * 根据权限列表获取其所有根节点id
 * @param menuList 权限列表
 * @returns 权限列表的所有根节点id
 */
export function mapMenuListToIds(menuList: any[]) {
  const ids: number[] = []

  function recurseGetId(menuList: any[]) {
    for (const item of menuList) {
      if (item.children) {
        recurseGetId(item.children)
      } else {
        ids.push(item.id)
      }
    }
  }
  recurseGetId(menuList)
  console.log('ids', ids)
  return ids
}

5、使用获取到的 id,调用 ElTree 的setCheckedKeys方法,给控件添加初始值

ts
/* 点击编辑,回显权限 */
const treeRef = ref<InstanceType<typeof ElTree>>()
function editCB(pageItem: any) {
  // console.log('pageItem: ', pageItem.menuList)
+  nextTick(() => {
+    const checkedKeys = mapMenuListToIds(pageItem.menuList)
+    treeRef.value?.setCheckedKeys(checkedKeys)
  })
}
新增重置权限菜单

思路: 在新增角色时,同样添加一个 callback,并在setCheckedKeys 时传递一个空数组

image-20230615103119219

image-20230615103433959

image-20230615103422203

按钮权限

获取用户所有按钮权限

1、定义获取 userMenus 中所有 permissions 的工具函数

image-20230614175904783

2、使用工具函数,在@store/login/login.ts中的loginActionloadLocalCacheAction中都获取 permissions

image-20230614175447396

根据权限展示按钮

1、在 PageContent 中获取对应的增删改查的权限

image-20230614181340039

2、根据权限展示、隐藏按钮

image-20230614180728804

image-20230614180732693

image-20230614180735483

image-20230614180842408

3、封装权限判断

image-20230614181956584

4、使用封装的 hook

image-20230614181915980

5、如果没有 query 权限,不展示 PageSearch 组件

PageSearch.vue

image-20230614182251390

image-20230614182325703

常见问题