基于 vue-element-admin 升级的 Vue3 +TS +Element-Plus 版本的从0到1构建说明,有来开源组织又一精心开源力作
Posted 你好,旧时光
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 vue-element-admin 升级的 Vue3 +TS +Element-Plus 版本的从0到1构建说明,有来开源组织又一精心开源力作相关的知识,希望对你有一定的参考价值。
项目简介
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 等技术栈少花些时间,少走些弯路,这样团队在毫无保留开源才有些许意义。
功能清单
技术栈清单
技术栈 | 描述 | 官网 |
---|---|---|
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。
首页控制台
结构样式基本遵循 vue-element-admin , 首页模块均已做组件封装,可简单的实现替换。
国际化
已实现 Element Plus 组件和菜单路由的国际化,不过只做了少量国际化工作,国际化大部分是体力活,如果你有国际化的需求,会在下文从0到1实现Element Plus组件和菜单路由的国际化。
主题设置
大小切换
角色管理
菜单管理
商品上架
库存设置
微信小程序/ APP/ H5 显示上架商品效果
启动部署
- 项目启动
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.
项目初始化
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模板
3. 启动项目
cd vue3-element-admin
npm install
npm run dev
浏览器访问: http://localhost:3000
整合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 组件和图标
<!-- src/App.vue -->
<template>
<img src="./assets/logo.png"/>
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
<div >
<el-button :icon="Search" circle></el-button>
<el-button type="primary" :icon="Edit" circle></el-button>
<el-button type="success" :icon="Check" circle></el-button>
<el-button type="info" :icon="Message" circle></el-button>
<el-button type="warning" :icon="Star" circle></el-button>
<el-button type="danger" :icon="Delete" circle></el-button>
</div>
</template>
<script lang="ts" setup>
import HelloWorld from \'/src/components/HelloWorld.vue\'
import Search, Edit,Check,Message,Star, Delete from \'@element-plus/icons-vue\'
</script>
4. 效果预览
路径别名配置
使用 @ 代替 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\'
环境变量
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
后面在使用自定义环境变量就会有智能提示,环境变量使用请参考下一节。
浏览器跨域处理
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 viteSvgIcons 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. 组件封装
<!-- src/components/SvgIcon/index.vue -->
<template>
<svg aria-hidden="true" class="svg-icon">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang="ts">
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`);
</script>
<style scoped>
.svg-icon
width: 1em;
height: 1em;
vertical-align: -0.15em;
overflow: hidden;
fill: currentColor;
</style>
7. 使用案例
<template>
<svg-icon icon-class="menu"/>
</template>
<script setup lang="ts">
import SvgIcon from \'@/components/SvgIcon/index.vue\';
</script>
Pinia状态管理
Pinia 是 Vue.js 的轻量级状态管理库,Vuex 的替代方案。
尤雨溪于2021.11.24 在 Twitter 上宣布:Pinia 正式成为 vuejs 官方的状态库,意味着 Pinia 就是 Vuex 5 。
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<UserInfo>
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
...
)
动态权限路由
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<RouteRecordRaw> = [
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
<el-button v-hasPerm="[\'sys:user:add\']">新增</el-button>
<el-button v-hasPerm="[\'sys:user:delete\']">删除</el-button>
Element-Plus国际化
Element Plus 官方提供全局配置 Config Provider实现国际化
// src/App.vue
<template>
<el-config-provider :locale="locale">
<router-view />
</el-config-provider>
</template>
<script setup lang="ts">
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
);
</script>
自定义国际化
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 方法
<h3 class="title"> $t("login.title") </h3>
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
<template>
<a v-else @click.prevent="handleLink(item)">
generateTitle(item.meta.title)
</a>
</template>
<script setup lang="ts">
import generateTitle from \'@/utils/i18n\'
</script>
wangEditor富文本编辑器
1. 安装wangEditor和Vue3组件
npm install @wangeditor/editor
npm install @wangeditor/editor-for-vue@next
2. wangEditor组件封装
<!-- src/components/WangEditor/index.vue -->
<template>
<div >
<!-- 工具栏 -->
<Toolbar
:editorId="editorId"
:defaultConfig="toolbarConfig"
/>
<!-- 编辑器 -->
<Editor
:editorId="editorId"
:defaultConfig="editorConfig"
:defaultHtml="defaultHtml"
@onChange="handleChange"
/>
</div>
</template>
<script setup lang="ts">
import computed, onBeforeUnmount, reactive, toRefs from \'vue\'
import Editor, Toolbar, getEditor, removeEditor from \'@wangeditor/editor-for-vue\'
// API 引用
import uploadFile from "@/api/system/file";
const props = defineProps(
modelValue:
type: [String],
default: \'\'
,
)
const emit = defineEmits([\'update:modelValue\']);
const state = reactive(
editorId: `w-e-$Math.random().toString().slice(-5)`, //【注意】编辑器 id ,要全局唯一
toolbarConfig: ,
editorConfig:
placeholder: \'请输入内容...\',
MENU_CONF:
uploadImage:
// 自定义图片上传
// @link https://www.wangeditor.com/v5/guide/menu-config.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%9F%E8%83%BD
async customUpload(file:any, insertFn:any)
uploadFile(file).then(response =>
const url = response.data
insertFn(url)
)
,
defaultHtml: props.modelValue
)
const editorId, toolbarConfig, editorConfig,defaultHtml = toRefs(state)
function handleChange(editor:any)
emit(\'update:modelValue\', editor.getHtml())
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() =>
const editor = getEditor(state.editorId)
if (editor == null) return
editor.destroy()
removeEditor(state.editorId)
)
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
3. 使用案例
<template>
<div class="component-container">
<editor v-model="modelValue.detail" />
</div>
</template>
<script setup lang="ts">
import Editor from "@/components/WangEditor/index.vue";
</script>
Echarts图表
1. 安装 Echarts
npm install echarts
2. Echarts 自适应大小工具类
侧边栏、浏览器窗口大小切换都会触发图表的 resize() 方法来进行自适应
// src/utils/resize.ts
import ref from \'vue\'
export default function()
const chart = ref<any>()
const sidebarElm = ref<Element>()
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
3. Echarts使用
官方的示例文档丰富和详细,且涵盖了 JavaScript 和 TypeScript 版本,使用非常简单。
<!-- src/views/dashboard/components/Chart/BarChart.vue -->
<!-- 线 + 柱混合图 -->
<template>
<div
:id="id"
:class="className"
:
/>
</template>
<script setup lang="ts">
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()
)
)
</script>
项目源码
Gitee | Github | |
---|---|---|
vue3-element-admin | https://gitee.com/youlaiorg/vue3-element-admin | https://github.com/youlaitech/vue3-element-admin |
加入我们
如果有问题或有好的建议可以添加开发者微信,备注「有来」进入学习交流群,备注「无回」参与开发。
开发人员 | 开发人员 |
---|---|
以上是关于基于 vue-element-admin 升级的 Vue3 +TS +Element-Plus 版本的从0到1构建说明,有来开源组织又一精心开源力作的主要内容,如果未能解决你的问题,请参考以下文章
(一)基于 vue-element-admin 前端与后端框架搭建
vue+elementui导入Excel文件(基于vue-element-admin中的uploadExcel组件), 然后 go-zero进行逻辑处理功能