基于 vue-element-admin 升级的 Vue 3 + TypeScript + Element-Plus 版本后台管理系统正式开源

简介: 基于 vue-element-admin 升级的 Vue 3 + TypeScript + Element-Plus 版本后台管理系统正式开源

新版本文档

【vue3-element-admin 】基于 Vue3 + Vite4 + TypeScript5+ Element-Plus 从0到1搭建企业级后台管理系统(前后端开源)_有来技术的博客-CSDN博客

vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,技术栈为 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等当前主流框架。本篇是 vue3-element-admin v2.x 版本从 0 到 1,相较于v1.x 版本增加了对原子CSS(UnoCSS)、按需自动导入、暗黑模式的支持。

https://blog.csdn.net/u013737132/article/details/130191394


项目简介

vue3-element-admin 是基于 vue-element-admin 升级的 Vue3 + Element Plus 版本的后台管理前端解决方案,是 有来技术团队 继 youlai-mall 全栈开源商城项目的又一开源力作。


项目使用 Vue3 + Vite2 + TypeScript + Element Plus + Vue Router + Pinia + Volar 等前端主流技术栈,基于此项目模板完成有来商城管理前端的 Vue3 版本。


本篇先对本项目功能、技术栈进行整体概述,再细节的讲述从0到1搭建 vue3-element-admin,在希望大家对本项目有个完完整整整了解的同时也能够在学 Vue3 + TypeScript 等技术栈少花些时间,少走些弯路,这样团队在毫无保留开源才有些许意义。


功能清单

微信图片_20230706084654.png


技术栈清单

技术栈 描述 官网

Vue3 渐进式 JavaScript 框架 https://v3.cn.vuejs.org/

TypeScript 微软新推出的一种语言,是 JavaScript 的超集 https://www.tslang.cn/

Vite2 前端开发与构建工具 https://cn.vitejs.dev/

Element Plus 基于 Vue 3,面向设计师和开发者的组件库 https://element-plus.gitee.io/zh-CN/

Pinia 新一代状态管理工具 https://pinia.vuejs.org/

Vue Router Vue.js 的官方路由 https://router.vuejs.org/zh/

wangEditor Typescript 开发的 Web 富文本编辑器 https://www.wangeditor.com/

Echarts 一个基于 JavaScript 的开源可视化图表库 https://echarts.apache.org/zh/


项目预览

在线预览地址:www.youlai.tech


以下截图是来自有来商城管理前端 mall-admin-web ,是基于 vue3-element-admin 为基础开发的具有一套完整的系统权限管理的商城管理系统,数据均为线上真实的而非Mock。


国际化

微信图片_20230706084706.gif

已实现 Element Plus 组件和菜单路由的国际化,不过只做了少量国际化工作,国际化大部分是体力活,如果你有国际化的需求,会在下文从0到1实现Element Plus组件和菜单路由的国际化。




主题设置


微信图片_20230706084711.gif大小切换


微信图片_20230706153752.gif


角色管理


微信图片_20230706153755.png


菜单管理

微信图片_20230706153758.png



商品上架

微信图片_20230706153801.png



库存设置

微信图片_20230706153803.png



微信小程序/ APP/ H5 显示上架商品效果

微信图片_20230706153807.png



启动部署

项目启动

npm install

npm run dev

浏览器访问 http://localhost:3000


项目部署

npm run build:prod

生成的静态文件在工程根目录 dist 文件夹


项目从0到1构建

安装第三方插件请注意项目源码的package.json版本号,有些升级不考虑兼容性的插件在 install 的时候我会带上具体版本号,例如 npm install vue-i18n@9.1.9 和 npm i vite-plugin-svg-icons@2.0.1 -D


环境准备

1. 运行环境Node


Node下载地址: http://nodejs.cn/download/


根据本机环境选择对应版本下载,安装过程可视化操作非常简便,静默安装即可。


安装完成后命令行终端 node -v 查看版本号以验证是否安装成功:




2. 开发工具VSCode


下载地址:https://code.visualstudio.com/Download


3. 必装插件Volar


VSCode 插件市场搜索 Volar (就排在第一位的骷髅头),且要禁用默认的 Vetur.


微信图片_20230706153919.png


项目初始化

1. Vite 是什么?


Vite是一种新型前端构建工具,能够显著提升前端开发体验。


Vite 官方中文文档:https://cn.vitejs.dev/guide/


2. 初始化项目


npm init vite@latest vue3-element-admin --template vue-ts

vue3-element-admin:项目名称

vue-ts : Vue + TypeScript 的模板,除此还有vue,react,react-ts模板


微信图片_20230706153935.png

3. 启动项目


cd vue3-element-admin

npm install

npm run dev

浏览器访问: http://localhost:3000

微信图片_20230706153937.png



整合Element-Plus

1.本地安装Element Plus和图标组件


npm install element-plus

npm install @element-plus/icons-vue

2.全局注册组件


// main.ts

import ElementPlus from 'element-plus'

import 'element-plus/theme-chalk/index.css'

createApp(App)

   .use(ElementPlus)

   .mount('#app')

3. Element Plus全局组件类型声明


// tsconfig.json

{

 "compilerOptions": {

   // ...

   "types": ["element-plus/global"]

 }

}

4. 页面使用 Element Plus 组件和图标





    import HelloWorld from '/src/components/HelloWorld.vue'

    import {Search, Edit,Check,Message,Star, Delete} from '@element-plus/icons-vue'



5. 效果预览



微信图片_20230706153958.png

路径别名配置

使用 @ 代替 src


1. Vite配置


// vite.config.ts

import {defineConfig} from 'vite'

import vue from '@vitejs/plugin-vue'

import path from 'path'

export default defineConfig({

   plugins: [vue()],

   resolve: {

       alias: {

           "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src

       }

   }

})

2. 安装@types/node


import path from 'path'编译器报错:TS2307: Cannot find module ‘path’ or its corresponding type declarations.


本地安装 Node 的 TypeScript 类型描述文件即可解决编译器报错


npm install @types/node --save-dev

3. TypeScript 编译配置


同样还是import path from 'path' 编译报错: TS1259: Module ‘“path”’ can only be default-imported using the ‘allowSyntheticDefaultImports’ flag


因为 typescript 特殊的 import 方式 , 需要配置允许默认导入的方式,还有路径别名的配置


// tsconfig.json

{

 "compilerOptions": {

   "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录

   "paths": { //路径映射,相对于baseUrl

     "@/*": ["src/*"]

   },

   "allowSyntheticDefaultImports": true // 允许默认导入

 }

}

4.别名使用


// App.vue

import HelloWorld from '/src/components/HelloWorld.vue'

           ↓

import HelloWorld from '@/components/HelloWorld.vue'

环境变量

官方教程: https://cn.vitejs.dev/guide/env-and-mode.html


1. env配置文件


项目根目录分别添加 开发、生产和模拟环境配置


开发环境配置:.env.development


# 变量必须以 VITE_ 为前缀才能暴露给外部读取

VITE_APP_TITLE = 'vue3-element-admin'

VITE_APP_PORT = 3000

VITE_APP_BASE_API = '/dev-api'

生产环境配置:.env.production


VITE_APP_TITLE = 'vue3-element-admin'

VITE_APP_PORT = 3000

VITE_APP_BASE_API = '/prod-api'

模拟生产环境配置:.env.staging


VITE_APP_TITLE = 'vue3-element-admin'

VITE_APP_PORT = 3000

VITE_APP_BASE_API = '/prod--api'

2.环境变量智能提示


添加环境变量类型声明


// src/ env.d.ts

// 环境变量类型声明

interface ImportMetaEnv {

 VITE_APP_TITLE: string,

 VITE_APP_PORT: string,

 VITE_APP_BASE_API: string

}

interface ImportMeta {

 readonly env: ImportMetaEnv

}

后面在使用自定义环境变量就会有智能提示,环境变量使用请参考下一节。


微信图片_20230706154017.png


浏览器跨域处理

1. 跨域原理


浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。


解决浏览器跨域限制大体分为后端和前端两个方向:


后端:开启 CORS 资源共享;

前端:使用反向代理欺骗浏览器误认为是同源请求;

2. 前端反向代理解决跨域


Vite 配置反向代理解决跨域,因为需要读取环境变量,故写法和上文的出入较大,这里贴出完整的 vite.config.ts 配置。


// vite.config.ts

import {UserConfig, ConfigEnv, loadEnv} from 'vite'

import vue from '@vitejs/plugin-vue'

import path from 'path'

export default ({command, mode}: ConfigEnv): UserConfig => {

   // 获取 .env 环境配置文件

   const env = loadEnv(mode, process.cwd())

   return (

       {

           plugins: [

               vue()

           ],

           // 本地反向代理解决浏览器跨域限制

           server: {

               host: 'localhost',

               port: Number(env.VITE_APP_PORT),

               open: true, // 启动是否自动打开浏览器

               proxy: {

                   [env.VITE_APP_BASE_API]: {

                       target: 'http://www.youlai.tech:9999', // 有来商城线上接口地址

                       changeOrigin: true,

                       rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')

                   }

               }

           },

           resolve: {

               alias: {

                   "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src

               }

           }

       }

   )

}


SVG图标

官方教程: https://github.com/vbenjs/vite-plugin-svg-icons/blob/main/README.zh_CN.md


Element Plus 图标库往往满足不了实际开发需求,可以引用和使用第三方例如 iconfont 的图标,本节通过整合 vite-plugin-svg-icons 插件使用第三方图标库。


1. 安装 vite-plugin-svg-icons


npm i fast-glob@3.2.11 -D

npm i vite-plugin-svg-icons@2.0.1 -D

2. 创建图标文件夹


项目创建 src/assets/icons 文件夹,存放 iconfont 下载的 SVG 图标


3. main.ts 引入注册脚本


// main.ts

import 'virtual:svg-icons-register';

4. vite.config.ts 插件配置


// vite.config.ts

import {UserConfig, ConfigEnv, loadEnv} from 'vite'

import vue from '@vitejs/plugin-vue'

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

export default ({command, mode}: ConfigEnv): UserConfig => {

   // 获取 .env 环境配置文件

   const env = loadEnv(mode, process.cwd())

   return (

       {

           plugins: [

               vue(),

               createSvgIconsPlugin({

                   // 指定需要缓存的图标文件夹

                   iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],

                   // 指定symbolId格式

                   symbolId: 'icon-[dir]-[name]',

               })

           ]

       }

   )

}


5. TypeScript支持


// tsconfig.json

{

 "compilerOptions": {

   "types": ["vite-plugin-svg-icons/client"]

 }

}

6. 组件封装





import { computed } from 'vue';

const props=defineProps({

 prefix: {

   type: String,

   default: 'icon',

 },

 iconClass: {

   type: String,

   required: true,

 },

 color: {

   type: String,

   default: ''

 }

})

const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);



.svg-icon {

 width: 1em;

 height: 1em;

 vertical-align: -0.15em;

 overflow: hidden;

 fill: currentColor;

}



使用案例




import SvgIcon from '@/components/SvgIcon/index.vue';

 

Pinia状态管理

Pinia 是 Vue.js 的轻量级状态管理库,Vuex 的替代方案。


尤雨溪于2021.11.24 在 Twitter 上宣布:Pinia 正式成为 vuejs 官方的状态库,意味着 Pinia 就是 Vuex 5 。


微信图片_20230706154035.png


1. 安装Pinia


npm install pinia

2. Pinia全局注册


// src/main.ts

import { createPinia } from "pinia"

app.use(createPinia())

  .mount('#app')

3. Pinia模块封装


// src/store/modules/user.ts

// 用户状态模块

import { defineStore } from "pinia";

import { UserState } from "@/types"; // 用户state的TypeScript类型声明,文件路径 src/types/store/user.d.ts

const useUserStore = defineStore({

   id: "user",

   state: (): UserState => ({

       token:'',

       nickname: ''

   }),

   actions: {

     getUserInfo() {

      return new Promise(((resolve, reject) => {

         ...

         resolve(data)

         ...

       }))

     }

   }

})

export default useUserStore;

// src/store/index.ts

import useUserStore from './modules/user'

const useStore = () => ({

   user: useUserStore()

})

export default useStore


4. 使用Pinia


import useStore from "@/store";

const { user } = useStore()

// state

const token = user.token

// action

user.getUserInfo().then(({data})=>{

console.log(data)

})

Axios网络请求库封装

1. axios工具封装


//  src/utils/request.ts

import axios, { AxiosRequestConfig, AxiosResponse } from "axios";

import { ElMessage, ElMessageBox } from "element-plus";

import { localStorage } from "@/utils/storage";

import useStore from "@/store"; // pinia

// 创建 axios 实例

const service = axios.create({

   baseURL: import.meta.env.VITE_APP_BASE_API,

   timeout: 50000,

   headers: { 'Content-Type': 'application/json;charset=utf-8' }

})

// 请求拦截器

service.interceptors.request.use(

   (config: AxiosRequestConfig) => {

       if (!config.headers) {

           throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);

       }

       const { user } = useStore()

       if (user.token) {

           config.headers.Authorization = `${localStorage.get('token')}`;

       }

       return config

   }, (error) => {

       return Promise.reject(error);

   }

)

// 响应拦截器

service.interceptors.response.use(

   (response: AxiosResponse) => {

       const { code, msg } = response.data;

       if (code === '00000') {

           return response.data;

       } else {

           ElMessage({

               message: msg || '系统出错',

               type: 'error'

           })

           return Promise.reject(new Error(msg || 'Error'))

       }

   },

   (error) => {

       const { code, msg } = error.response.data

       if (code === 'A0230') {  // token 过期

           localStorage.clear(); // 清除浏览器全部缓存

           window.location.href = '/'; // 跳转登录页

           ElMessageBox.alert('当前页面已失效,请重新登录', '提示', {})

               .then(() => {

               })

               .catch(() => {

               });

       } else {

           ElMessage({

               message: msg || '系统出错',

               type: 'error'

           })

       }

       return Promise.reject(new Error(msg || 'Error'))

   }

);

// 导出 axios 实例

export default service


2. API封装


以登录成功后获取用户信息(昵称、头像、角色集合和权限集合)的接口为案例,演示如何通过封装的 axios 工具类请求后端接口,其中响应数据


// src/api/system/user.ts

import request from "@/utils/request";

import { AxiosPromise } from "axios";

import { UserInfo } from "@/types"; // 用户信息返回数据的TypeScript类型声明,文件路径 src/types/api/system/user.d.ts

/**

* 登录成功后获取用户信息(昵称、头像、权限集合和角色集合)

*/

export function getUserInfo(): AxiosPromise {

   return request({

       url: '/youlai-admin/api/v1/users/me',

       method: 'get'

   })

}

3. API调用


// src/store/modules/user.ts

import { getUserInfo } from "@/api/system/user";

// 获取登录用户信息

getUserInfo().then(({ data }) => {

const { nickname, avatar, roles, perms } = data

 ...

})

动态权限路由

官方文档: https://router.vuejs.org/zh/api/


1. 安装 vue-router


npm install vue-router@next

2. 创建路由实例


创建路由实例并导出,其中包括静态路由数据,动态路由后面将通过接口从后端获取并整合用户角色的权限控制。


// src/router/index.ts

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

import useStore from "@/store";

export const Layout = () => import('@/layout/index.vue')

// 静态路由

export const constantRoutes: Array = [

   {

       path: '/redirect',

       component: Layout,

       meta: { hidden: true },

       children: [

           {

               path: '/redirect/:path(.*)',

               component: () => import('@/views/redirect/index.vue')

           }

       ]

   },

   {

       path: '/login',

       component: () => import('@/views/login/index.vue'),

       meta: { hidden: true }

   },

   {

       path: '/404',

       component: () => import('@/views/error-page/404.vue'),

       meta: { hidden: true }

   },

   {

       path: '/401',

       component: () => import('@/views/error-page/401.vue'),

       meta: { hidden: true }

   },

   {

       path: '/',

       component: Layout,

       redirect: '/dashboard',

       children: [

           {

               path: 'dashboard',

               component: () => import('@/views/dashboard/index.vue'),

               name: 'Dashboard',

               meta: { title: 'dashboard', icon: 'dashboard', affix: true }

           }

       ]

   }

]

// 创建路由实例

const router = createRouter({

   history: createWebHashHistory(),

   routes: constantRoutes as RouteRecordRaw[],

   // 刷新时,滚动条位置还原

   scrollBehavior: () => ({ left: 0, top: 0 })

})

// 重置路由

export function resetRouter() {

   const { permission } = useStore()

   permission.routes.forEach((route) => {

       const name = route.name

       if (name) {

           router.hasRoute(name) && router.removeRoute(name)

       }

   })

}

export default router


3. 路由实例全局注册


// main.ts

import router from "@/router";

app.use(router)

  .mount('#app')

4. 动态权限路由


// src/permission.ts

import router from "@/router";

import { ElMessage } from "element-plus";

import useStore from "@/store";

import NProgress from 'nprogress';

import 'nprogress/nprogress.css'

NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏

// 白名单路由

const whiteList = ['/login', '/auth-redirect']

router.beforeEach(async (to, form, next) => {

   NProgress.start()

   const { user, permission } = useStore()

   const hasToken = user.token

   if (hasToken) {

       // 登录成功,跳转到首页

       if (to.path === '/login') {

           next({ path: '/' })

           NProgress.done()

       } else {

           const hasGetUserInfo = user.roles.length > 0

           if (hasGetUserInfo) {

               next()

           } else {

               try {

                   await user.getUserInfo()

                   const roles = user.roles

                   // 用户拥有权限的路由集合(accessRoutes)

                   const accessRoutes: any = await permission.generateRoutes(roles)

                   accessRoutes.forEach((route: any) => {

                       router.addRoute(route)

                   })

                   next({ ...to, replace: true })

               } catch (error) {

                   // 移除 token 并跳转登录页

                   await user.resetToken()

                   ElMessage.error(error as any || 'Has Error')

                   next(`/login?redirect=${to.path}`)

                   NProgress.done()

               }

           }

       }

   } else {

       // 未登录可以访问白名单页面(登录页面)

       if (whiteList.indexOf(to.path) !== -1) {

           next()

       } else {

           next(`/login?redirect=${to.path}`)

           NProgress.done()

       }

   }

})

router.afterEach(() => {

   NProgress.done()

})


其中 const accessRoutes: any = await permission.generateRoutes(roles)是根据用户角色获取拥有权限的路由(静态路由+动态路由),核心代码如下:


// src/store/modules/permission.ts

import { constantRoutes } from '@/router';

import { listRoutes } from "@/api/system/menu";

const usePermissionStore = defineStore({

   id: "permission",

   state: (): PermissionState => ({

       routes: [],

       addRoutes: []

   }),

   actions: {

       setRoutes(routes: RouteRecordRaw[]) {

           this.addRoutes = routes

          // 静态路由 + 动态路由

           this.routes = constantRoutes.concat(routes)

       },

       generateRoutes(roles: string[]) {

           return new Promise((resolve, reject) => {

              // API 获取动态路由

               listRoutes().then(response => {

                   const asyncRoutes = response.data

                   let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)

                   this.setRoutes(accessedRoutes)

                   resolve(accessedRoutes)

               }).catch(error => {

                   reject(error)

               })

           })

       }

   }

})

export default usePermissionStore;


按钮权限

1. Directive 自定义指令


// src/directive/permission/index.ts

import useStore from "@/store";

import { Directive, DirectiveBinding } from "vue";

/**

* 按钮权限校验

*/

export const hasPerm: Directive = {

   mounted(el: HTMLElement, binding: DirectiveBinding) {

       // 「超级管理员」拥有所有的按钮权限

       const { user } = useStore()

       const roles = user.roles;

       if (roles.includes('ROOT')) {

           return true

       }

       // 「其他角色」按钮权限校验

       const { value } = binding;

       if (value) {

           const requiredPerms = value; // DOM绑定需要的按钮权限标识

           const hasPerm = user.perms?.some(perm => {

               return requiredPerms.includes(perm)

           })

           if (!hasPerm) {

               el.parentNode && el.parentNode.removeChild(el);

           }

       } else {

           throw new Error("need perms! Like v-has-perm=\"['sys:user:add','sys:user:edit']\"");

       }

   }

};


2. 自定义指令全局注册


// src/main.ts

const app = createApp(App)

// 自定义指令

import * as directive from "@/directive";

Object.keys(directive).forEach(key => {

   app.directive(key, (directive as { [key: string]: Directive })[key]);

});

3. 指令使用


// src/views/system/user/index.vue

新增

删除

Element-Plus国际化

官方教程:https://element-plus.gitee.io/zh-CN/guide/i18n.html


Element Plus 官方提供全局配置 Config Provider实现国际化


//  src/App.vue



import { computed, onMounted, ref, watch } from "vue";

import { ElConfigProvider } from "element-plus";

import useStore from "@/store";

// 导入 Element Plus 语言包

import zhCn from "element-plus/es/locale/lang/zh-cn";

import en from "element-plus/es/locale/lang/en";

// 获取系统语言

const { app } = useStore();

const language = computed(() => app.language);

const locale = ref();

watch(

 language,

 (value) => {

   if (value == "en") {

     locale.value = en;

   } else { // 默认中文

     locale.value = zhCn;

   }

 },

 {

   // 初始化立即执行

   immediate: true

 }

);



自定义国际化

i18n 英文全拼 internationalization ,国际化的意思,英文 i 和 n 中间18个英文字母


1. 安装 vue-i18n


npm install vue-i18n@9.1.9

2. 语言包


创建 src/lang 语言包目录,中文语言包 zh-cn.ts,英文语言包 en.ts


// src/lang/en.ts

export default {

   // 路由国际化

   route: {

       dashboard: 'Dashboard',

       document: 'Document'

   },

   // 登录页面国际化

   login: {

       title: 'youlai-mall management system',

       username: 'Username',

       password: 'Password',

       login: 'Login',

       code: 'Verification Code',

       copyright: 'Copyright © 2020 - 2022 youlai.tech All Rights Reserved. ',

       icp: ''

   },

   // 导航栏国际化

   navbar:{

       dashboard: 'Dashboard',

       logout:'Logout',

       document:'Document',

       gitee:'Gitee'

   }

}


3. 创建i18n实例


// src/lang/index.ts

// 自定义国际化配置

import {createI18n} from 'vue-i18n'

import {localStorage} from '@/utils/storage'

// 本地语言包

import enLocale from './en'

import zhCnLocale from './zh-cn'

const messages = {

   'zh-cn': {

       ...zhCnLocale

   },

   en: {

       ...enLocale

   }

}

/**

* 获取当前系统使用语言字符串

*

* @returns zh-cn|en ...

*/

export const getLanguage = () => {

   // 本地缓存获取

   let language = localStorage.get('language')

   if (language) {

       return language

   }

    // 浏览器使用语言

   language = navigator.language.toLowerCase()

   const locales = Object.keys(messages)

   for (const locale of locales) {

       if (language.indexOf(locale) > -1) {

           return locale

       }

   }

   return 'zh-cn'

}

const i18n = createI18n({

   locale: getLanguage(),

   messages: messages

})

export default i18n


4. i18n 全局注册


// main.ts

// 国际化

import i18n from "@/lang/index";

app.use(i18n)

  .mount('#app');

5. 静态页面国际化


$t 是 i18n 提供的根据 key 从语言包翻译对应的 value 方法


{{ $t("login.title") }}

6. 动态路由国际化


i18n 工具类,主要使用 i18n 的 te (判断语言包是否存在key) 和 t (翻译) 两个方法


//  src/utils/i18n.ts

import i18n from "@/lang/index";

export function generateTitle(title: any) {

   // 判断是否存在国际化配置,如果没有原生返回

   const hasKey = i18n.global.te('route.' + title)

   if (hasKey) {

       const translatedTitle = i18n.global.t('route.' + title)

       return translatedTitle

   }

   return title

}

页面使用


// src/components/Breadcrumb/index.vue



import {generateTitle} from '@/utils/i18n'

wangEditor富文本编辑器

推荐教程:Vue3 官方示例


1. 安装wangEditor和Vue3组件


npm install @wangeditor/editor --save

npm install @wangeditor/editor-for-vue@next --save

2. wangEditor组件封装





import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue'

import { Editor, Toolbar } from '@wangeditor/editor-for-vue'

// API 引用

import { uploadFile } from "@/api/system/file";

const props = defineProps({

 modelValue: {

   type: [String],

   default: ''

 },

})

const emit = defineEmits(['update:modelValue']);

// 编辑器实例,必须用 shallowRef

const editorRef = shallowRef()

const state = reactive({

 toolbarConfig: {},

 editorConfig: {

   placeholder: '请输入内容...',

   MENU_CONF: {

     uploadImage: {

       // 自定义图片上传

       async customUpload(file: any, insertFn: any) {

         console.log("上传图片")

         uploadFile(file).then(response => {

           const url = response.data

           insertFn(url)

         })

       }

     }

   }

 },

 defaultHtml: props.modelValue,

 mode: 'default'

})

const { toolbarConfig, editorConfig, defaultHtml, mode } = toRefs(state)

const handleCreated = (editor: any) => {

 editorRef.value = editor // 记录 editor 实例,重要!

}

function handleChange(editor: any) {

 emit('update:modelValue', editor.getHtml())

}

// 组件销毁时,也及时销毁编辑器

onBeforeUnmount(() => {

 const editor = editorRef.value

 if (editor == null) return

 editor.destroy()

})





3. 使用案例




import Editor from "@/components/WangEditor/index.vue";




import Editor from "@/components/WangEditor/index.vue";


微信图片_20230706154100.png


Echarts图表

1. 安装 Echarts


npm install echarts

2. Echarts 自适应大小工具类


侧边栏、浏览器窗口大小切换都会触发图表的 resize() 方法来进行自适应


// src/utils/resize.ts

import { ref } from 'vue'

export default function() {

   const chart = ref()

   const sidebarElm = ref()

   const chartResizeHandler = () => {

       if (chart.value) {

           chart.value.resize()

       }

   }

   const sidebarResizeHandler = (e: TransitionEvent) => {

       if (e.propertyName === 'width') {

           chartResizeHandler()

       }

   }

   const initResizeEvent = () => {

       window.addEventListener('resize', chartResizeHandler)

   }

   const destroyResizeEvent = () => {

       window.removeEventListener('resize', chartResizeHandler)

   }

   const initSidebarResizeEvent = () => {

       sidebarElm.value = document.getElementsByClassName('sidebar-container')[0]

       if (sidebarElm.value) {

           sidebarElm.value.addEventListener('transitionend', sidebarResizeHandler as EventListener)

       }

   }

   const destroySidebarResizeEvent = () => {

       if (sidebarElm.value) {

           sidebarElm.value.removeEventListener('transitionend', sidebarResizeHandler as EventListener)

       }

   }

   const mounted = () => {

       initResizeEvent()

       initSidebarResizeEvent()

   }

   const beforeDestroy = () => {

       destroyResizeEvent()

       destroySidebarResizeEvent()

   }

   const activated = () => {

       initResizeEvent()

       initSidebarResizeEvent()

   }

   const deactivated = () => {

       destroyResizeEvent()

       destroySidebarResizeEvent()

   }

   return {

       chart,

       mounted,

       beforeDestroy,

       activated,

       deactivated

   }

}

微信图片_20230706154129.png

3. Echarts使用


官方示例: https://echarts.apache.org/examples/zh/index.html


官方的示例文档丰富和详细,且涵盖了 JavaScript 和 TypeScript 版本,使用非常简单。





import {nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted} from "vue";

import {init, EChartsOption} from 'echarts'

import * as echarts from 'echarts';

import resize from '@/utils/resize'

const props = defineProps({

 id: {

   type: String,

   default: 'barChart'

 },

 className: {

   type: String,

   default: ''

 },

 width: {

   type: String,

   default: '200px',

   required: true

 },

 height: {

   type: String,

   default: '200px',

   required: true

 }

})

const {

 mounted,

 chart,

 beforeDestroy,

 activated,

 deactivated

} = resize()

function initChart() {

 const barChart = init(document.getElementById(props.id) as HTMLDivElement)

 barChart.setOption({

   title: {

     show: true,

     text: '业绩总览(2021年)',

     x: 'center',

     padding: 15,

     textStyle: {

       fontSize: 18,

       fontStyle: 'normal',

       fontWeight: 'bold',

       color: '#337ecc'

     }

   },

   grid: {

     left: '2%',

     right: '2%',

     bottom: '10%',

     containLabel: true

   },

   tooltip: {

     trigger: 'axis',

     axisPointer: {

       type: 'cross',

       crossStyle: {

         color: '#999'

       }

     }

   },

   legend: {

     x: 'center',

     y: 'bottom',

     data: ['收入', '毛利润', '收入增长率', '利润增长率']

   },

   xAxis: [

     {

       type: 'category',

       data: ['上海', '北京', '浙江', '广东', '深圳', '四川', '湖北', '安徽'],

       axisPointer: {

         type: 'shadow'

       }

     }

   ],

   yAxis: [

     {

       type: 'value',

       min: 0,

       max: 10000,

       interval: 2000,

       axisLabel: {

         formatter: '{value} '

       }

     },

     {

       type: 'value',

       min: 0,

       max: 100,

       interval: 20,

       axisLabel: {

         formatter: '{value}%'

       }

     }

   ],

   series: [

     {

       name: '收入',

       type: 'bar',

       data: [

         8000, 8200, 7000, 6200, 6500, 5500, 4500, 4200, 3800,

       ],

       barWidth: 20,

       itemStyle: {

         color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [

           { offset: 0, color: '#83bff6' },

           { offset: 0.5, color: '#188df0' },

           { offset: 1, color: '#188df0' }

         ])

       }

     },

     {

       name: '毛利润',

       type: 'bar',

       data: [

         6700, 6800, 6300, 5213, 4500, 4200, 4200, 3800

       ],

       barWidth: 20,

       itemStyle: {

         color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [

           { offset: 0, color: '#25d73c' },

           { offset: 0.5, color: '#1bc23d' },

           { offset: 1, color: '#179e61' }

         ])

       }

     },

     {

       name: '收入增长率',

       type: 'line',

       yAxisIndex: 1,

       data: [65, 67, 65, 53, 47, 45, 43, 42, 41],

       itemStyle: {

         color: '#67C23A'

       }

     },

     {

       name: '利润增长率',

       type: 'line',

       yAxisIndex: 1,

       data: [80, 81, 78, 67, 65, 60, 56,51, 45 ],

       itemStyle: {

         color: '#409EFF'

       }

     }

   ]

 } as EChartsOption)

 chart.value = barChart

}

onBeforeUnmount(() => {

 beforeDestroy()

})

onActivated(() => {

 activated()

})

onDeactivated(() => {

 deactivated()

})

onMounted(() => {

 mounted()

 nextTick(() => {

   initChart()

 })

})





项目源码

Gitee Github

vue3-element-admin vue3-element-admin: 基于 vue-element-admin 升级的 Vue3 版本管理前端解决方案,技术栈: Vue3 + Vite2 + TypeScript + Element Plus + Pinia 。 GitHub - youlaitech/vue3-element-admin: 基于 vue-element-admin 升级的 Vue3 版本管理前端解决方案,技术栈: Vue3 + Vite2 + TypeScript + Element Plus + Pinia 。


相关文章
|
20天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
122 64
|
20天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
100 60
|
20天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
28 8
|
19天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
20 1
|
19天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
30 1
|
20天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
23天前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
35 2
|
29天前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
72 7
|
1月前
|
前端开发 数据库
芋道框架审批流如何实现(Cloud+Vue3)
芋道框架审批流如何实现(Cloud+Vue3)
90 3