基vue-element-admin升级的Vue3+TS+Element-Plus版本的后端管理前端解决方案vue3-element-admin正式对外发布,有来开源组织又一精心力作,毫无保留开放从0到1构建过程

博客 分享
0 203
优雅殿下
优雅殿下 2022-04-02 08:57:27
悬赏:0 积分 收藏

基 vue-element-admin升级的Vue3 +TS +Element-Plus 版本的后端管理前端解决方案 vue3-element-admin 正式对外发布,有来开源组织又一精心力作,毫无保留开放从0到1构建过程

基 vue-element-admin升级的Vue3 +TS +Element-Plus 版本的后端管理前端解决方案 vue3-element-admin 正式对外发布,有来开源组织又一精心力作,毫无保留开放从0到1构建过程 项目代码结构基本完全保留 vue-element-admin ,代码风格参考Vue.js社区,CSS遵守BEM规范 ,站在巨人的肩膀不仅是为了看的更远,更多的是一种致敬、延续和希望走的更远。

项目简介

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 RouterVue.js 的官方路由https://router.vuejs.org/zh/
wangEditorTypescript 开发的 Web 富文本编辑器https://www.wangeditor.com/
Echarts一个基于 JavaScript 的开源可视化图表库https://echarts.apache.org/zh/

项目起源

首先说下为什么会有此开源项目:

  • vue-element-admin 过于优秀,但可惜不更新了,停滞在 Vue2 版本 ;

  • 场面上基于 Vue3 + Element Plus 组合的框架封装复杂,后端接入困难,和 vue-element-admin 相差甚远;

  • 新技术栈、新特性支持不够,举例 TypeScript 、Vue3.2 的 setup 语法糖;

  • 开源项目不稳定,很好的项目悄无声息的不更新了,所以团队想把管理前端掌握在自己手里。

开始为了给有来商城的管理前端找到合适的 Vue3 升级的替代方案,花了不少时间去研究市面对应的开源框架,尝试过接入有来商城线上微服务接口,但结果都不尽人意,很大部分原因是接入的复杂程度远远大于当初接入 vue-element-admin ,也不排除是先入为主的原因。但接入 vue-element-admin 确实是很轻松的,有兴趣的可以了解下接入过程:

vue-element-admin实战 | 第一篇:移除mock接入后台微服务接口

vue-element-admin实战 | 第二篇:最小改动接入后台实现动态路由菜单加载

因为相信大多数同学在 Vue2 学习阶段同时学习了 vue-element-admin 这款优秀的开源框架,但随着时间的脚步也都慢慢被卷入 Vue3 + TypeScript 的学习浪潮,前端技术栈更新迭代太快让人直呼学不动了,为了减少大家学习成本,便基于 vue-element-admin 升级改造适配当前 Vue3 生态技术栈的 vue3-element-admin,站在巨人的肩膀不仅是为了看的更远,更多的是一种致敬、延续和希望走的更远。

项目预览

在线预览地址: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构建

大家在整合第三方插件的时候一定要注意版本,不同版本的插件整合会有区别

环境准备

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-adminnpm installnpm run dev

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

?

整合Element-Plus

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

npm install element-plusnpm install @element-plus/icons-vue

2.全局注册组件

// main.tsimport 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 alt="Vue logo" 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.tsimport {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.vueimport 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 = 3000VITE_APP_BASE_API = '/dev-api'
  • 生产环境配置:.env.production

    VITE_APP_TITLE = 'vue3-element-admin'VITE_APP_PORT = 3000VITE_APP_BASE_API = '/prod-api'
  • 模拟生产环境配置:.env.staging

    VITE_APP_TITLE = 'vue3-element-admin'VITE_APP_PORT = 3000VITE_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.tsimport {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 vite-plugin-svg-icons -D

2. 创建图标文件夹

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

3. main.ts 注册脚本

// main.tsimport 'virtual:svg-icons-register';

4. vite.config.ts 插件配置

// vite.config.tsimport {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(),                viteSvgIcons({                    // 指定需要缓存的图标文件夹                    iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],                    // 指定symbolId格式                    symbolId: 'icon-[dir]-[name]',                })            ]        }    )}

5. 组件封装

<!-- src/components/SvgIcon/index.vue --><template>  <`svg aria-hidden="true" >    <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>

6. 使用案例

<template>  <svg-icon icon-/></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.tsimport { 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.tsconst useUserStore = defineStore({    id: "user",    state: (): UserState => ({        token:'',        nickname: ''    }),    actions: {      getUserInfo() {      	return new Promise(((resolve, reject) => {          ...          resolve(data)          ...        }))      }    }})export default useUserStore;// src/store/index.tsimport useUserStore from './modules/user'const useStore = () => ({    user: useUserStore()})export default useStore

4. 使用Pinia

import useStore from "@/store";const { user } = useStore()// stateconst token = user.token// actionuser.getUserInfo().then(({data})=>{	console.log(data)})

Axios网络请求库封装

1. axios工具封装

//  src/utils/request.tsimport 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.tsimport 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.tsimport { 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.tsimport { 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.tsimport router from "@/router";app.use(router)   .mount('#app')

4. 动态权限路由

// src/permission.tsimport 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.tsimport 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.tsconst 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国际化

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

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@next

2. 语言包

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

// src/lang/en.tsexport 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 >{{ $t("login.title") }}</h3>

6. 动态路由国际化

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

//  src/utils/i18n.tsimport 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富文本编辑器

推荐教程:50 行代码 Vue3 中使用 wangEditor 富文本编辑器

1. 安装wangEditor和Vue3组件

npm install @wangeditor/editornpm 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 >  	<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.tsimport { 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使用

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

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

<!-- src/views/dashboard/components/Chart/BarChart.vue --> <!-- 线 + 柱混合图 --><template>  <div      :id="id"      :      :  /></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>

项目源码

GiteeGithub
vue3-element-adminhttps://gitee.com/youlaiorg/vue3-element-adminhttps://github.com/youlaitech/vue3-element-admin

加入我们

如果有问题或有好的建议可以添加开发者微信,备注「有来」进入学习交流群,备注「无回」参与开发。

开发人员开发人员
ruichuan
posted @ 2022-04-02 08:34 有来技术团队 阅读(88) 评论(0) 编辑 收藏 举报
回帖
    优雅殿下

    优雅殿下 (王者 段位)

    2018 积分 (2)粉丝 (47)源码

    小小码农,大大世界

     

    温馨提示

    亦奇源码

    最新会员