S11-10 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:
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
创建项目
创建项目
使用create-vue
工具创建mr-vue3-ts-cms
项目。create-vue
是基于vite的脚手架工具
$ pnpm create vue@latest
创建选项
安装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
└─views
3个tsconfig文件之间的关系
配置vue文件类型声明
问题: 项目本身的vue
模块声明并不能识别出App是一个组件
import App from './App.vue'
解决: 重新声明vue模块,使得ts可以识别出vue是一个组件
// env.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const src: DefineComponent
export default src
}
配置icon,标题
直接复制自己的icon到public中
配置标题
<!-- index.html -->
<title>木头人 - 后台管理</title>
重置CSS样式
normalize.css
第三方包:normalize.css
1、安装:normalize.css
npm i normalize.css
2、在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
VSCode需要安装一个插件:EditorConfig for VS Code
使用prettier工具
Prettier
是一款强大的代码格式化工具,支持 JavaScript、TypeScript、CSS、SCSS、Less、JSX、Angular、Vue、GraphQL、JSON、Markdown 等语言,基本上前端能用到的文件格式它都可以搞定,是当下最流行的代码格式化工具。
1、安装prettier
npm install prettier -D
2、配置.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
}
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检测
1、在前面创建项目的时候,我们就选择了ESLint,所以Vue会默认帮助我们配置需要的ESLint环境。
2、VSCode需要安装ESLint插件:ESLint
3、解决eslint和prettier冲突的问题:
安装插件:(vue在创建项目时,如果选择prettier,那么这两个插件会自动安装)
- eslint-plugin-prettier(主要)
- eslint-config-prettier
npm i eslint-plugin-prettier eslint-config-prettier -D
添加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规范
代码提交风格
通常我们的git commit会按照统一的风格来提交,这样可以快速定位每次提交的内容,方便之后对版本进行控制。
但是如果每次手动来编写这些是比较麻烦的事情,我们可以使用一个工具:commitizen
- commitizen 是一个帮助我们编写规范 commit message 的工具;
1.安装commitizen
#npm
npm install commitizen -D
#pnpm
pnpm install commitizen -D
2.安装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
配置文件中
{
"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 -D
2.在根目录创建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@next
2、创建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 router
3、安装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 pinia
2、创建pinia对象
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
3、挂载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 useCounterStore
5、使用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@next
2、创建store对象:
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
name: 'coderwhy'
}
}
})
export default store
3、安装store:
createApp(App).use(router).use(store).mount('#app')
4、在App.vue中使用:
<h2>{{ $store.state.name }}</h2>
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
npm install element-plus
完整引入
一种引入element-plus的方式是全局引入,代表的含义是所有的组件和插件都会被自动注册:
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
指定全局组件类型。
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
按需引入
也就是在开发中用到某个组件对某个组件进行引入:
<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的插件:
npm install babel-plugin-import -D
2.配置babel.config.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中进行注册;
- 所以我们可以将它们在全局注册一次;
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-components
和 unplugin-auto-import
这两款插件
npm install unplugin-vue-components unplugin-auto-import -D
然后把下列代码插入到你的 Vite
或 Webpack
的配置文件中
Vite
1、设置vite.config.ts
,添加插件Components
和Components
// 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
中
{
"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
// 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
中
axios集成
1、安装axios
npm install axios
2、封装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 HYRequest
VSCode配置
{
"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: {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
方法3: 自定义环境常量
1、创建.env.development
和 .env.production
文件
2、分别在文件中定义不同的常量(注意:常量名必须以VITE_
开头)
3、通过import.meta.env.VITE_XXX
获取定义的常量
Login
占满屏幕
.app {
+ width: 100vw;
+ height: 100vh;
background-col2or: #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
1、安装图标:npm install @element-plus/icons-vue
2、全局注册图标:
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、对上面的方法进行封装:
使用
import registerIcons from './global/registerIcons'
const app = createApp(App)
+ app.use(registerIcons)
封装
// 注册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插槽中添加图标
<!-- 登录表单 -->
<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>
获取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
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
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、本地缓存-封装-使用
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.roleMenuTreeInfo
2、给每个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 -g
3、工具命令
# 查看版本
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
动态加载路由对象
/* 动态加载路由 */
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)
}
根据菜单映射路由
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)
})
User
页面布局
<div class="user">
<div class="user-search">
<UserSearch />
</div>
<div class="content">
<UserContent />
</div>
</div>
组件:UserSearch
使用组件
<template>
<div class="user">
<div class="user-search">
+ <UserSearch />
</div>
<div class="content">
<UserContent />
</div>
</div>
</template>
<script setup lang="ts">
+ import UserSearch from './cpns/UserSearch/UserSearch.vue'
import UserContent from './cpns/UserContent/UserContent.vue'
</script>
页面布局
注意: 在element-plus中允许将多行的el-col
放到一个el-row
中,配合:span
属性,当span
满24份时会自动换行
<div class="user-search">
<el-form label-width="80px">
<el-row :gutter="120">
<el-col :span="8">
<el-form-item label="用户名">
<el-input placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="真实姓名">
<el-input placeholder="请输入真实姓名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="手机号码">
<el-input placeholder="请输入手机号码" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="状态">
+ <el-select class="m-2" placeholder="Select" style="width: 100%">
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="创建时间">
+ <el-date-picker
type="daterange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="btns">
<el-button icon="Refresh">重置</el-button>
<el-button icon="Search" type="primary">查询</el-button>
</el-form-item>
</el-form>
</div>
element国际化
方法一: 全局引入
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
app.use(ElementPlus, {
locale: zhCn,
})
方式二: 自动按需引入(推荐)
<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
文件声明
declare module '*.mjs'
效果:
重置、查询按钮
<el-form-item class="btns">
+ <el-button icon="Refresh">重置</el-button>
+ <el-button icon="Search" type="primary">查询</el-button>
</el-form-item>
重置功能
1、绑定表单数据
+ <el-form label-width="80px" :model="searchForm" ref="searchFormRef">
<el-row :gutter="120">
<el-col :span="8">
<el-form-item label="用户名" prop="name">
+ <el-input v-model="searchForm.name" placeholder="请输入用户名" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="真实姓名" prop="realname">
+ <el-input v-model="searchForm.realname" placeholder="请输入真实姓名" />
</el-form-item>
</el-col>
</el-row>
</el-form>
/* 表单数据 */
const searchForm = reactive({
name: '',
realname: '',
cellphone: '',
enable: 1,
creatAt: []
})
2、重置表单
<el-button icon="Refresh" @click="hdlReset">重置</el-button>
<el-form label-width="80px" :model="searchForm" ref="searchFormRef">
const searchFormRef = ref<InstanceType<typeof ElForm>>()
/* 重置搜索表单 */
function hdlReset() {
searchFormRef.value?.resetFields()
}
3、注意: 如果想让resetFields起作用,需要添加prop属性
+ <el-form-item label="用户名" prop="name"> <!-- prop="name" -->
<el-input v-model="searchForm.name" placeholder="请输入用户名" />
</el-form-item>
查询功能
<el-button icon="Search" type="primary" @click="hdlQuery">查询</el-button>
/* 根据搜索项查询 */
function hdlQuery() {
console.log('根据搜索项查询')
}
组件:UserContent
使用组件
<template>
<div class="user">
<div class="user-search">
<UserSearch />
</div>
<div class="content">
+ <UserContent />
</div>
</div>
</template>
<script setup lang="ts">
import UserSearch from './cpns/UserSearch/UserSearch.vue'
+ import UserContent from './cpns/UserContent/UserContent.vue'
</script>
页面布局
<div class="user-content">
<div class="header">header</div>
<div class="form">form</div>
<div class="navigation">navigation</div>
</div>
表格-请求用户列表数据
1、在services中发送网络请求
/* 请求用户列表数据 */
export function postUserList() {
return mrRequest.post({
url: '/users/list',
data: {
offset: 0,
size: 10
}
})
}
2、在pinia中调用网络请求,并保存返回结果
import { postUserList } from '@/service/main/system'
import { defineStore } from 'pinia'
interface ISystemState {
userList: any[]
totalCount: number
}
const useSystemStore = defineStore('system', {
+ state: (): ISystemState => ({
userList: [],
totalCount: 0
}),
actions: {
/* 请求用户列表数据 */
+ async postUserListAction() {
const res = await postUserList()
this.userList = res.data.list
this.totalCount = res.data.totalCount
}
}
})
export default useSystemStore
3、在UserContent组件中调用Action,
import useSystemStore from '@/store/main/system'
const systemStore = useSystemStore()
systemStore.postUserListAction()
表格-展示用户列表
1、获取数据
import useSystemStore from '@/store/main/system'
import { storeToRefs } from 'pinia'
const systemStore = useSystemStore()
systemStore.postUserListAction()
/* 获取用户列表 */
+ const { userList, totalCount } = storeToRefs(systemStore)
2、使用el-table展示数据
<el-table :data="userList" border style="width: 100%">
<el-table-column align="center" type="selection" />
<el-table-column align="center" type="index" label="序号" width="60px" />
<el-table-column align="center" prop="name" label="用户名" width="150px" />
<el-table-column align="center" prop="realname" label="真实姓名" width="150px" />
<el-table-column align="center" prop="cellphone" label="手机号码" width="150px" />
<el-table-column align="center" prop="enable" label="状态" width="60px" />
<el-table-column align="center" prop="createAt" label="创建时间" />
<el-table-column align="center" prop="updateAt" label="更新时间" />
<el-table-column align="center" label="操作" width="150px"></el-table-column>
</el-table>
4、调整表格样式-宽度、居中、高度、按钮样式
表格样式-宽度、居中
<el-table-column align="center" type="index" label="序号" width="60px" />
<el-table-column align="center" prop="name" label="用户名" width="150px" />
<el-table-column align="center" prop="realname" label="真实姓名" width="150px" />
<el-table-column align="center" prop="cellphone" label="手机号码" width="150px" />
<el-table-column align="center" prop="enable" label="状态" width="60px">
表格样式-高度
.el-table {
:deep(.el-table__cell) {
padding: 12px 0;
}
}
表格样式-按钮样式、给按钮添加图标、样式
<el-table-column align="center" label="操作" width="150px">
<div class="btns">
+ <el-button type="primary" text>
<el-icon><Edit /></el-icon>
<span>编辑</span>
</el-button>
+ <el-button type="danger" text>
<el-icon><Delete /></el-icon>
<span>删除</span>
</el-button>
</div>
</el-table-column>
表格-展示启用
知识点: 作用域插槽
视频: [220715] D064-18 Vue3-(理解)组件插槽-作用域插槽的使用.mp4
<el-table-column align="center" prop="enable" label="状态" width="60px">
+ <template #default="scope">
<div class="enable">
+ <el-button size="small" plain :type="scope.row.enable ? 'primary' : 'danger'">
+ {{ scope.row.enable ? '启用' : '禁用' }}
</el-button>
</div>
</template>
</el-table-column>
表格-时间格式化
知识点: 插件:dayjs
1、安装
npm i dayjs
2、封装dayjs
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、转成东八区时间
export function formatUTC(utcString: string, format: string = 'YYYY-MM-DD HH:mm:ss') {
+ return dayjs.utc(utcString).utcOffset(8).format(format)
}
4、使用封装的方法进行格式化
<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>
分页-展示
<div class="navigation">
+ <el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30]"
small="small"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
@size-change="hdlSizeChange"
@current-change="hdlCurrentChange"
/>
</div>
/* 分页 */
const currentPage = ref(1)
const pageSize = ref(10)
function hdlSizeChange() {}
function hdlCurrentChange() {}
分页-切换页码重新请求数据
1、修改网络请求函数
/* 请求用户列表数据 */
+ export function postUserList(query: any) {
return mrRequest.post({
url: '/users/list',
+ data: query
})
}
2、在store中调用请求函数
const useSystemStore = defineStore('system', {
state: (): ISystemState => ({
userList: [],
totalCount: 0
}),
actions: {
+ async postUserListAction(query: any) {
const res = await postUserList(query)
this.userList = res.data.list
this.totalCount = res.data.totalCount
}
}
})
3、在组件中封装调用请求的函数
/* 发送网络请求 */
function fetchUserList(searchForm: ISearchForm = {}) {
const offset = (currentPage.value - 1) * pageSize.value
const size = pageSize.value
const query = { offset, size }
+ const finalQuery = { ...query, ...searchForm }
systemStore.postUserListAction(finalQuery)
}
4、页码、每页大小发生变化时,调用fetchUserListData
/* 分页事件 */
function hdlSizeChange() {
fetchUserList()
}
function hdlPageChange() {
fetchUserList()
}
点击查询发送请求
难点: 查询和重置按钮属于UserSearch
,而数据请求时发生在UserContent
组件中,二者是兄弟组件
知识点:
- defineEmits(string:[]):
返回:emit
,定义向外发射的事件
1、发射事件queryClick
到外部
const emits = defineEmits(['search-form'])
/* 表单数据 */
const searchForm = reactive<ISearchForm>({
name: '',
realname: '',
cellphone: '',
enable: 1,
createAt: ''
})
/* 根据搜索项查询 */
function hdlQuery() {
emits('search-form', searchForm)
}
2、在外部监听事件,并接收数据
<UserSearch @search-form="hdlSearchForm" />
3、通过绑定ref调用另一个组件中的方法
<UserContent ref="contentRef" />
/* 调用UserContent组件中的方法,查询数据 */
const contentRef = ref<InstanceType<typeof UserContent>>()
function hdlSearchForm(searchForm: ISearchForm) {
if (contentRef.value) contentRef.value.fetchUserList(searchForm)
}
4、在另一个组件中暴露将被调用的方法,并根据传递的formData数据发送请求
defineExpose({ fetchUserList })
/* 发送网络请求 */
function fetchUserList(searchForm: ISearchForm = {}) {
const offset = (currentPage.value - 1) * pageSize.value
const size = pageSize.value
const query = { offset, size }
const finalQuery = { ...query, ...searchForm }
systemStore.postUserListAction(finalQuery)
}
点击重置发送请求
1、发射事件resetClick
到外部
/* 重置搜索表单 */
function hdlReset() {
+ emits('search-form', {})
searchFormRef.value?.resetFields()
}
2、在外部监听事件,并接收数据,重新发送请求
<UserSearch @search-form="hdlSearchForm" />
/* 调用UserContent组件中的方法,查询数据 */
const contentRef = ref<InstanceType<typeof UserContent>>()
function hdlSearchForm(searchForm: ISearchForm) {
if (contentRef.value) contentRef.value.fetchUserList(searchForm)
}
删除-根据id删除数据
1、监听删除按钮点击事件
+ <el-button type="danger" text @click="() => hdlDeleteUser(scope.row.id)">
<el-icon><Delete /></el-icon>
<span>删除</span>
</el-button>
2、添加作用域插槽,获取scope
<el-table-column align="center" label="操作" width="140px">
+ <template #default="scope">
<div class="btns">
<el-button type="primary" text>
<el-icon><Edit /></el-icon>
<span>编辑</span>
</el-button>
+ <el-button type="danger" text @click="() => hdlDeleteUser(scope.row.id)">
<el-icon><Delete /></el-icon>
<span>删除</span>
</el-button>
</div>
</template>
</el-table-column>
3、根据id发送请求删除数据
/* 根据id删除用户 */
function hdlDeleteUser(id: number) {
+ systemStore.delUserByIdAction(id)
}
4、在store中发送调用请求函数
/* 根据id删除用户 */
async delUserByIdAction(id: number) {
+ await delUserById(id)
ElMessage.success('哈哈,删除成功~')
this.postUserListAction({ offset: 0, size: 5 })
},
5、在service中定义发送网络请求函数
/* 根据id删除用户 */
export function delUserById(id: number) {
return mrRequest.delete({
url: `/users/${id}`
})
}
6、删除成功后重新请求数据
/* 根据id删除用户 */
async delUserByIdAction(id: number) {
await delUserById(id)
ElMessage.success('哈哈,删除成功~')
+ this.postUserListAction({ offset: 0, size: 5 })
},
新增用户
1、监听新增按钮点击
<el-button class="btn" type="primary" @click="hdlAddUser">新增用户</el-button>
组件:UserModal
使用组件
<div class="user-content">
+ <UserModal />
</div>
<script setup lang="ts">
+ import UserModal from '../UserModal/UserModal.vue'
</script>
页面布局
<div class="user-modal">
<el-dialog v-model="modalVisiable" title="新增用户" width="30%" center>
<span>
表单部分
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="modalVisiable = false">取消</el-button>
<el-button type="primary" @click="modalVisiable = false">确定</el-button>
</span>
</template>
</el-dialog>
</div>
显示、隐藏对话框
1、组件UserContent中
const emit = defineEmits(['change-visiable'])
/* 新增用户 */
function hdlAddUser() {
emit('change-visiable')
}
2、组件User中
<UserContent ref="contentRef" @change-visiable="hdlChangeVisiable" />
<UserModal ref="modalRef" />
/* 修改对话框是否显示 */
const modalRef = ref<InstanceType<typeof UserModal>>()
function hdlChangeVisiable() {
if (modalRef.value) modalRef.value.changeModalVisiable()
}
3、组件UserModal中
defineExpose({ changeModalVisiable })
const modalVisiable = ref(false)
/* 修改对话框是否显示 */
function changeModalVisiable() {
modalVisiable.value = true
}
表单布局
<div class="form">
<el-form label-position="right" label-width="100px" size="large">
<el-form-item label="用户名" prop="name">
<el-input placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="真实姓名" prop="realname">
<el-input placeholder="请输入真实姓名" />
</el-form-item>
<el-form-item label="密码" prop="password" show-password>
<el-input placeholder="请输入密码" />
</el-form-item>
<el-form-item label="电话号码" prop="cellphone">
<el-input placeholder="请输入电话号码" />
</el-form-item>
<el-form-item label="选择角色" prop="role">
<el-input placeholder="请选择角色" />
</el-form-item>
<el-form-item label="选择部门" prop="department">
<el-input placeholder="请选择部门" />
</el-form-item>
</el-form>
</div>
/* 表单数据 */
const addUserForm = reactive({
name: '',
realname: '',
password: '',
cellphone: '',
roleId: '',
departmentId: ''
})
角色和部门数据
注意: 由于角色和部门数据可能会在其他许多页面都有使用,应该提取出来,放在main/main.ts
中
1、在service中发送网络请求
/* 获取角色列表 */
export function postRoleLists() {
return mrRequest.post({
url: '/role/list'
})
}
/* 获取部门列表 */
export function postDepartmentLists() {
return mrRequest.post({
url: '/department/list'
})
}
2、在store中调用网络请求
import { postDepartmentLists, postRoleLists } from '@/service/main/main'
import { defineStore } from 'pinia'
interface IMainState {
roleLists: any[]
departmentLists: any[]
}
const useMainStore = defineStore('main', {
+ state: (): IMainState => ({
roleLists: [],
departmentLists: []
}),
actions: {
+ async postRoleListsAction() {
+ const res = await postRoleLists()
this.roleLists = res.data.list
},
+ async postDepartmentListsAction() {
+ const res = await postDepartmentLists()
this.departmentLists = res.data.list
}
}
})
export default useMainStore
3、在组件中发起action
// src\views\Main\Main.vue
/* 发送网络请求 */
mainStore.postRoleListsAction()
mainStore.postDepartmentListsAction()
展示角色和部门
1、从store中获取数据
/* 获取store中数据 */
const { roleLists, departmentLists } = storeToRefs(mainStore)
2、遍历展示数据
<el-form-item label="选择角色" prop="roleId">
<el-select v-model="addUserForm.roleId" class="m-2" placeholder="Select">
<el-option
+ v-for="item in roleLists"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择部门" prop="departmentId">
<el-select v-model="addUserForm.departmentId" class="m-2" placeholder="Select">
<el-option
+ v-for="item in departmentLists"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
/>
</el-select>
</el-form-item>
点击确定添加用户
1、监听按钮点击
<template #footer>
<span class="dialog-footer">
<el-button @click="modalVisiable = false">取消</el-button>
+ <el-button type="primary" @click="hdlAddUser">确定</el-button>
</span>
</template>
2、在service中发送添加用户的网络请求
/* 新增用户 */
export function addUser(userInfo: any) {
return mrRequest.post({
url: '/users',
data: userInfo
})
}
3、在store中调用网络请求
/* 新增用户 */
async addUserAction(userInfo: any) {
+ await addUser(userInfo)
}
4、在组件中,调用action,创建新用户
/* 添加用户 */
const formRef = ref<InstanceType<typeof ElForm>>()
function hdlAddUser() {
modalVisiable.value = false
// 验证表单
+ formRef.value?.validate((valid: any) => {
if (valid) {
// 验证成功
+ systemStore.addUserAction(addUserForm)
} else {
// 验证失败
ElMessage.error('呜呼,验证失败,请重新来过~')
}
})
}
5、新增用户后,重新请求用户数据
/* 新增用户 */
async addUserAction(userInfo: any) {
await addUser(userInfo)
+ this.postUserListAction({ offset: 0, size: 5 })
}
编辑用户
1、监听编辑按钮点击
+ <el-button type="primary" text @click="() => hdlEditUser(scope.row)">
<el-icon><Edit /></el-icon>
<span>编辑</span>
</el-button>
2、向外暴露事件,并传递数据
const emit = defineEmits(['change-visiable', 'edit-click'])
/* 编辑用户 */
function hdlEditUser(userItem: any) {
emit('edit-click', userItem)
}
3、在User父组件中,监听edit-click
事件,并调用弹出框组件中的方法
<UserContent
ref="contentRef"
@change-visiable="hdlChangeVisiable"
+ @edit-click="hdlEditClick"
/>
/* 调用模态组件内函数,修改用户 */
function hdlEditClick(userItem: any) {
if (modalRef.value) modalRef.value.changeModalVisiable(userItem)
}
4、在UserModal组件中,回显当前编辑的用户
/* 修改对话框是否显示 */
const isEdit = ref(false)
function changeModalVisiable(userItem: any = null) {
modalVisiable.value = true
userId.value = userItem?.id
if (userItem) {
// 编辑状态
isEdit.value = true
// 遍历回显
+ for (const key in userForm) {
+ userForm[key] = userItem[key]
+ }
}
}
注意: formData需要定义为any类型
/* 表单数据 */
+ const userForm = reactive<any>({ // reactive<any>
name: '',
realname: '',
password: '',
cellphone: '',
roleId: '',
departmentId: ''
})
5、不显示密码表单
- 全局记录isNew的值
/* 修改对话框是否显示 */
+ const isEdit = ref(false)
const userInfo = ref()
function changeModalVisiable(userItem: any = null) {
modalVisiable.value = true
userId.value = userItem?.id
if (userItem) {
// 编辑状态
+ isEdit.value = true
for (const key in userForm) {
userForm[key] = userItem[key]
}
userInfo.value = userForm
} else {
// 新增状态
+ isEdit.value = false
for (const key in userForm) {
userForm[key] = ''
}
userInfo.value = null
}
}
- 根据isNew的值,显示、隐藏密码表单
<el-form-item v-if="!isEdit" label="密码" prop="password">
<el-input v-model="userForm.password" placeholder="请输入密码" show-password />
</el-form-item>
6、在新增用户的情况下,初始化清空所有表单
function changeModalVisiable(userItem: any = null) {
modalVisiable.value = true
userId.value = userItem?.id
if (userItem) {
// 编辑状态
isEdit.value = true
for (const key in userForm) {
userForm[key] = userItem[key]
}
} else {
// 新增状态
isEdit.value = false
+ for (const key in userForm) {
+ userForm[key] = ''
+ }
}
}
点击确定编辑用户
1、service
/* 编辑用户 */
export function editUser(id: number, userInfo: any) {
return mrRequest.patch({
url: `/users/${id}`,
data: userInfo
})
}
2、store
/* 修改用户 */
async editUserAction(id: number, userInfo: any) {
await editUser(id, userInfo)
this.postUserListAction({ offset: 0, size: 5 })
}
3、组件
- 保存userInfo
+ const userInfo = ref()
function changeModalVisiable(userItem: any = null) {
modalVisiable.value = true
userId.value = userItem?.id
if (userItem) {
// 编辑状态
isEdit.value = true
for (const key in userForm) {
userForm[key] = userItem[key]
}
+ userInfo.value = userForm
} else {
// 新增状态
isEdit.value = false
for (const key in userForm) {
userForm[key] = ''
}
+ userInfo.value = null
}
}
- 调用action,执行编辑操作
function hdlSubmitUser() {
modalVisiable.value = false
// 验证表单
formRef.value?.validate((valid: any) => {
if (valid) {
// 验证成功
if (isEdit.value) {
+ systemStore.editUserAction(userId.value, userInfo)
+ ElMessage.success('哈哈,修改用户成功~')
} else {
systemStore.addUserAction(userInfo)
ElMessage.success('哈哈,新增用户成功~')
}
} else {
// 验证失败
ElMessage.error('呜呼,验证失败,请重新来过~')
}
})
}
问题集
声明之前已使用的块范围变量“__VLS_13”
问题: 刚刚使用pnpm create vue@latest
创建vue3项目,安装依赖包之后就出现以下报错:
解决思路:
怀疑是vscode插件引起,目前使用的插件:
Vue - Official@2.0.3
TypeScript 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 true
prettier保存时自动格式化
问题: 让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 -D
2、在
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 后执行。
示例: