基于 vue-element-admin 升级的 Vue3 +TS +Element-Plus 版本的从0到1构建说明,有来开源组织又一精心开源力作

Posted 你好,旧时光

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 vue-element-admin 升级的 Vue3 +TS +Element-Plus 版本的从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 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.9npm 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\'

环境变量

官方教程: 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

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

浏览器跨域处理

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
  ...
)

动态权限路由

官方文档: 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<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国际化

官方教程: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@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富文本编辑器

推荐教程:50 行代码 Vue3 中使用 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使用

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

官方的示例文档丰富和详细,且涵盖了 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-element-admin 前端与后端框架搭建

vue-element-admin--使用体验

vue-element-admin初窥

使用vue-element-admin进行二次开发

vue+elementui导入Excel文件(基于vue-element-admin中的uploadExcel组件), 然后 go-zero进行逻辑处理功能