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
    pnpx commitizen init cz-conventional-changelog --save-dev --save-exact
    
    # pnpm
    pnpx 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'

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

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

常见问题