客户管理系统开发定制基于vue-element-admin升级的Vue3+TS+Element-Plus版本正式开源,有来开源组织又一精心力作

项目简介

 是基于  升级的 + Element Plus 客户管理系统开发定制版本的后台管理前端解决方案,是  继  客户管理系统开发定制开源商城项目的又一开源力作。

项目使用 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/

项目预览

在线预览地址:

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

国际化

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

主题设置

大小切换

角色管理

菜单管理

商品上架

库存设置

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

启动部署

  • 项目启动
  1. npm install
  2. npm run dev

浏览器访问 

  • 项目部署
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. 启动项目

  1. cd vue3-element-admin
  2. npm install
  3. npm run dev

浏览器访问: 

整合Element-Plus

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

  1. npm install element-plus
  2. npm install @element-plus/icons-vue

2.全局注册组件

  1. // main.ts
  2. import ElementPlus from 'element-plus'
  3. import 'element-plus/theme-chalk/index.css'
  4. createApp(App)
  5. .use(ElementPlus)
  6. .mount('#app')

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

  1. // tsconfig.json
  2. {
  3. "compilerOptions": {
  4. // ...
  5. "types": ["element-plus/global"]
  6. }
  7. }

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

  1. <!-- src/App.vue -->
  2. <template>
  3. <img alt="Vue logo" src="./assets/logo.png"/>
  4. <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
  5. <div style="text-align: center;margin-top: 10px">
  6. <el-button :icon="Search" circle></el-button>
  7. <el-button type="primary" :icon="Edit" circle></el-button>
  8. <el-button type="success" :icon="Check" circle></el-button>
  9. <el-button type="info" :icon="Message" circle></el-button>
  10. <el-button type="warning" :icon="Star" circle></el-button>
  11. <el-button type="danger" :icon="Delete" circle></el-button>
  12. </div>
  13. </template>
  14. <script lang="ts" setup>
  15. import HelloWorld from '/src/components/HelloWorld.vue'
  16. import {Search, Edit,Check,Message,Star, Delete} from '@element-plus/icons-vue'
  17. </script>

5. 效果预览

路径别名配置

使用 @ 代替 src

1. Vite配置

  1. // vite.config.ts
  2. import {defineConfig} from 'vite'
  3. import vue from '@vitejs/plugin-vue'
  4. import path from 'path'
  5. export default defineConfig({
  6. plugins: [vue()],
  7. resolve: {
  8. alias: {
  9. "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
  10. }
  11. }
  12. })

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 方式 , 需要配置允许默认导入的方式,还有路径别名的配置

  1. // tsconfig.json
  2. {
  3. "compilerOptions": {
  4. "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
  5. "paths": { //路径映射,相对于baseUrl
  6. "@/*": ["src/*"]
  7. },
  8. "allowSyntheticDefaultImports": true // 允许默认导入
  9. }
  10. }

4.别名使用

  1. // App.vue
  2. import HelloWorld from '/src/components/HelloWorld.vue'
  3. import HelloWorld from '@/components/HelloWorld.vue'

环境变量

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

1. env配置文件

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

  • 开发环境配置:.env.development

    1. # 变量必须以 VITE_ 为前缀才能暴露给外部读取
    2. VITE_APP_TITLE = 'vue3-element-admin'
    3. VITE_APP_PORT = 3000
    4. VITE_APP_BASE_API = '/dev-api'
  • 生产环境配置:.env.production

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

    1. VITE_APP_TITLE = 'vue3-element-admin'
    2. VITE_APP_PORT = 3000
    3. VITE_APP_BASE_API = '/prod--api'

2.环境变量智能提示

添加环境变量类型声明

  1. // src/ env.d.ts
  2. // 环境变量类型声明
  3. interface ImportMetaEnv {
  4. VITE_APP_TITLE: string,
  5. VITE_APP_PORT: string,
  6. VITE_APP_BASE_API: string
  7. }
  8. interface ImportMeta {
  9. readonly env: ImportMetaEnv
  10. }

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

浏览器跨域处理

1. 跨域原理

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

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

  • 后端:开启 CORS 资源共享;
  • 前端:使用反向代理欺骗浏览器误认为是同源请求;

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

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

  1. // vite.config.ts
  2. import {UserConfig, ConfigEnv, loadEnv} from 'vite'
  3. import vue from '@vitejs/plugin-vue'
  4. import path from 'path'
  5. export default ({command, mode}: ConfigEnv): UserConfig => {
  6. // 获取 .env 环境配置文件
  7. const env = loadEnv(mode, process.cwd())
  8. return (
  9. {
  10. plugins: [
  11. vue()
  12. ],
  13. // 本地反向代理解决浏览器跨域限制
  14. server: {
  15. host: 'localhost',
  16. port: Number(env.VITE_APP_PORT),
  17. open: true, // 启动是否自动打开浏览器
  18. proxy: {
  19. [env.VITE_APP_BASE_API]: {
  20. target: 'http://www.youlai.tech:9999', // 有来商城线上接口地址
  21. changeOrigin: true,
  22. rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
  23. }
  24. }
  25. },
  26. resolve: {
  27. alias: {
  28. "@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
  29. }
  30. }
  31. }
  32. )
  33. }

SVG图标

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

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

1. 安装 vite-plugin-svg-icons

  1. npm i fast-glob@3.2.11 -D
  2. npm i vite-plugin-svg-icons@2.0.1 -D

2. 创建图标文件夹

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

3. main.ts 引入注册脚本

  1. // main.ts
  2. import 'virtual:svg-icons-register';

4. vite.config.ts 插件配置

  1. // vite.config.ts
  2. import {UserConfig, ConfigEnv, loadEnv} from 'vite'
  3. import vue from '@vitejs/plugin-vue'
  4. import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
  5. export default ({command, mode}: ConfigEnv): UserConfig => {
  6. // 获取 .env 环境配置文件
  7. const env = loadEnv(mode, process.cwd())
  8. return (
  9. {
  10. plugins: [
  11. vue(),
  12. createSvgIconsPlugin({
  13. // 指定需要缓存的图标文件夹
  14. iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
  15. // 指定symbolId格式
  16. symbolId: 'icon-[dir]-[name]',
  17. })
  18. ]
  19. }
  20. )
  21. }

5. TypeScript支持

  1. // tsconfig.json
  2. {
  3. "compilerOptions": {
  4. "types": ["vite-plugin-svg-icons/client"]
  5. }
  6. }

6. 组件封装

  1. <!-- src/components/SvgIcon/index.vue -->
  2. <template>
  3. <svg aria-hidden="true" class="svg-icon">
  4. <use :xlink:href="symbolId" :fill="color" />
  5. </svg>
  6. </template>
  7. <script setup lang="ts">
  8. import { computed } from 'vue';
  9. const props=defineProps({
  10. prefix: {
  11. type: String,
  12. default: 'icon',
  13. },
  14. iconClass: {
  15. type: String,
  16. required: true,
  17. },
  18. color: {
  19. type: String,
  20. default: ''
  21. }
  22. })
  23. const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
  24. </script>
  25. <style scoped>
  26. .svg-icon {
  27. width: 1em;
  28. height: 1em;
  29. vertical-align: -0.15em;
  30. overflow: hidden;
  31. fill: currentColor;
  32. }
  33. </style>

7. 使用案例

  1. <template>
  2. <svg-icon icon-class="menu"/>
  3. </template>
  4. <script setup lang="ts">
  5. import SvgIcon from '@/components/SvgIcon/index.vue';
  6. </script>

Pinia状态管理

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

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

1. 安装Pinia

npm install pinia

2. Pinia全局注册

  1. // src/main.ts
  2. import { createPinia } from "pinia"
  3. app.use(createPinia())
  4. .mount('#app')

3. Pinia模块封装

  1. // src/store/modules/user.ts
  2. // 用户状态模块
  3. import { defineStore } from "pinia";
  4. import { UserState } from "@/types"; // 用户state的TypeScript类型声明,文件路径 src/types/store/user.d.ts
  5. const useUserStore = defineStore({
  6. id: "user",
  7. state: (): UserState => ({
  8. token:'',
  9. nickname: ''
  10. }),
  11. actions: {
  12. getUserInfo() {
  13. return new Promise(((resolve, reject) => {
  14. ...
  15. resolve(data)
  16. ...
  17. }))
  18. }
  19. }
  20. })
  21. export default useUserStore;
  22. // src/store/index.ts
  23. import useUserStore from './modules/user'
  24. const useStore = () => ({
  25. user: useUserStore()
  26. })
  27. export default useStore

4. 使用Pinia

  1. import useStore from "@/store";
  2. const { user } = useStore()
  3. // state
  4. const token = user.token
  5. // action
  6. user.getUserInfo().then(({data})=>{
  7. console.log(data)
  8. })

Axios网络请求库封装

1. axios工具封装

  1. // src/utils/request.ts
  2. import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
  3. import { ElMessage, ElMessageBox } from "element-plus";
  4. import { localStorage } from "@/utils/storage";
  5. import useStore from "@/store"; // pinia
  6. // 创建 axios 实例
  7. const service = axios.create({
  8. baseURL: import.meta.env.VITE_APP_BASE_API,
  9. timeout: 50000,
  10. headers: { 'Content-Type': 'application/json;charset=utf-8' }
  11. })
  12. // 请求拦截器
  13. service.interceptors.request.use(
  14. (config: AxiosRequestConfig) => {
  15. if (!config.headers) {
  16. throw new Error(`Expected 'config' and 'config.headers' not to be undefined`);
  17. }
  18. const { user } = useStore()
  19. if (user.token) {
  20. config.headers.Authorization = `${localStorage.get('token')}`;
  21. }
  22. return config
  23. }, (error) => {
  24. return Promise.reject(error);
  25. }
  26. )
  27. // 响应拦截器
  28. service.interceptors.response.use(
  29. (response: AxiosResponse) => {
  30. const { code, msg } = response.data;
  31. if (code === '00000') {
  32. return response.data;
  33. } else {
  34. ElMessage({
  35. message: msg || '系统出错',
  36. type: 'error'
  37. })
  38. return Promise.reject(new Error(msg || 'Error'))
  39. }
  40. },
  41. (error) => {
  42. const { code, msg } = error.response.data
  43. if (code === 'A0230') { // token 过期
  44. localStorage.clear(); // 清除浏览器全部缓存
  45. window.location.href = '/'; // 跳转登录页
  46. ElMessageBox.alert('当前页面已失效,请重新登录', '提示', {})
  47. .then(() => {
  48. })
  49. .catch(() => {
  50. });
  51. } else {
  52. ElMessage({
  53. message: msg || '系统出错',
  54. type: 'error'
  55. })
  56. }
  57. return Promise.reject(new Error(msg || 'Error'))
  58. }
  59. );
  60. // 导出 axios 实例
  61. export default service

2. API封装

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

  1. // src/api/system/user.ts
  2. import request from "@/utils/request";
  3. import { AxiosPromise } from "axios";
  4. import { UserInfo } from "@/types"; // 用户信息返回数据的TypeScript类型声明,文件路径 src/types/api/system/user.d.ts
  5. /**
  6. * 登录成功后获取用户信息(昵称、头像、权限集合和角色集合)
  7. */
  8. export function getUserInfo(): AxiosPromise<UserInfo> {
  9. return request({
  10. url: '/youlai-admin/api/v1/users/me',
  11. method: 'get'
  12. })
  13. }

3. API调用

  1. // src/store/modules/user.ts
  2. import { getUserInfo } from "@/api/system/user";
  3. // 获取登录用户信息
  4. getUserInfo().then(({ data }) => {
  5. const { nickname, avatar, roles, perms } = data
  6. ...
  7. })

动态权限路由

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

1. 安装 vue-router

npm install vue-router@next

2. 创建路由实例

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

  1. // src/router/index.ts
  2. import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
  3. import useStore from "@/store";
  4. export const Layout = () => import('@/layout/index.vue')
  5. // 静态路由
  6. export const constantRoutes: Array<RouteRecordRaw> = [
  7. {
  8. path: '/redirect',
  9. component: Layout,
  10. meta: { hidden: true },
  11. children: [
  12. {
  13. path: '/redirect/:path(.*)',
  14. component: () => import('@/views/redirect/index.vue')
  15. }
  16. ]
  17. },
  18. {
  19. path: '/login',
  20. component: () => import('@/views/login/index.vue'),
  21. meta: { hidden: true }
  22. },
  23. {
  24. path: '/404',
  25. component: () => import('@/views/error-page/404.vue'),
  26. meta: { hidden: true }
  27. },
  28. {
  29. path: '/401',
  30. component: () => import('@/views/error-page/401.vue'),
  31. meta: { hidden: true }
  32. },
  33. {
  34. path: '/',
  35. component: Layout,
  36. redirect: '/dashboard',
  37. children: [
  38. {
  39. path: 'dashboard',
  40. component: () => import('@/views/dashboard/index.vue'),
  41. name: 'Dashboard',
  42. meta: { title: 'dashboard', icon: 'dashboard', affix: true }
  43. }
  44. ]
  45. }
  46. ]
  47. // 创建路由实例
  48. const router = createRouter({
  49. history: createWebHashHistory(),
  50. routes: constantRoutes as RouteRecordRaw[],
  51. // 刷新时,滚动条位置还原
  52. scrollBehavior: () => ({ left: 0, top: 0 })
  53. })
  54. // 重置路由
  55. export function resetRouter() {
  56. const { permission } = useStore()
  57. permission.routes.forEach((route) => {
  58. const name = route.name
  59. if (name) {
  60. router.hasRoute(name) && router.removeRoute(name)
  61. }
  62. })
  63. }
  64. export default router

3. 路由实例全局注册

  1. // main.ts
  2. import router from "@/router";
  3. app.use(router)
  4. .mount('#app')

4. 动态权限路由

  1. // src/permission.ts
  2. import router from "@/router";
  3. import { ElMessage } from "element-plus";
  4. import useStore from "@/store";
  5. import NProgress from 'nprogress';
  6. import 'nprogress/nprogress.css'
  7. NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏
  8. // 白名单路由
  9. const whiteList = ['/login', '/auth-redirect']
  10. router.beforeEach(async (to, form, next) => {
  11. NProgress.start()
  12. const { user, permission } = useStore()
  13. const hasToken = user.token
  14. if (hasToken) {
  15. // 登录成功,跳转到首页
  16. if (to.path === '/login') {
  17. next({ path: '/' })
  18. NProgress.done()
  19. } else {
  20. const hasGetUserInfo = user.roles.length > 0
  21. if (hasGetUserInfo) {
  22. next()
  23. } else {
  24. try {
  25. await user.getUserInfo()
  26. const roles = user.roles
  27. // 用户拥有权限的路由集合(accessRoutes)
  28. const accessRoutes: any = await permission.generateRoutes(roles)
  29. accessRoutes.forEach((route: any) => {
  30. router.addRoute(route)
  31. })
  32. next({ ...to, replace: true })
  33. } catch (error) {
  34. // 移除 token 并跳转登录页
  35. await user.resetToken()
  36. ElMessage.error(error as any || 'Has Error')
  37. next(`/login?redirect=${to.path}`)
  38. NProgress.done()
  39. }
  40. }
  41. }
  42. } else {
  43. // 未登录可以访问白名单页面(登录页面)
  44. if (whiteList.indexOf(to.path) !== -1) {
  45. next()
  46. } else {
  47. next(`/login?redirect=${to.path}`)
  48. NProgress.done()
  49. }
  50. }
  51. })
  52. router.afterEach(() => {
  53. NProgress.done()
  54. })

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

  1. // src/store/modules/permission.ts
  2. import { constantRoutes } from '@/router';
  3. import { listRoutes } from "@/api/system/menu";
  4. const usePermissionStore = defineStore({
  5. id: "permission",
  6. state: (): PermissionState => ({
  7. routes: [],
  8. addRoutes: []
  9. }),
  10. actions: {
  11. setRoutes(routes: RouteRecordRaw[]) {
  12. this.addRoutes = routes
  13. // 静态路由 + 动态路由
  14. this.routes = constantRoutes.concat(routes)
  15. },
  16. generateRoutes(roles: string[]) {
  17. return new Promise((resolve, reject) => {
  18. // API 获取动态路由
  19. listRoutes().then(response => {
  20. const asyncRoutes = response.data
  21. let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
  22. this.setRoutes(accessedRoutes)
  23. resolve(accessedRoutes)
  24. }).catch(error => {
  25. reject(error)
  26. })
  27. })
  28. }
  29. }
  30. })
  31. export default usePermissionStore;

按钮权限

1. Directive 自定义指令

  1. // src/directive/permission/index.ts
  2. import useStore from "@/store";
  3. import { Directive, DirectiveBinding } from "vue";
  4. /**
  5. * 按钮权限校验
  6. */
  7. export const hasPerm: Directive = {
  8. mounted(el: HTMLElement, binding: DirectiveBinding) {
  9. // 「超级管理员」拥有所有的按钮权限
  10. const { user } = useStore()
  11. const roles = user.roles;
  12. if (roles.includes('ROOT')) {
  13. return true
  14. }
  15. // 「其他角色」按钮权限校验
  16. const { value } = binding;
  17. if (value) {
  18. const requiredPerms = value; // DOM绑定需要的按钮权限标识
  19. const hasPerm = user.perms?.some(perm => {
  20. return requiredPerms.includes(perm)
  21. })
  22. if (!hasPerm) {
  23. el.parentNode && el.parentNode.removeChild(el);
  24. }
  25. } else {
  26. throw new Error("need perms! Like v-has-perm=\"['sys:user:add','sys:user:edit']\"");
  27. }
  28. }
  29. };

2. 自定义指令全局注册

  1. // src/main.ts
  2. const app = createApp(App)
  3. // 自定义指令
  4. import * as directive from "@/directive";
  5. Object.keys(directive).forEach(key => {
  6. app.directive(key, (directive as { [key: string]: Directive })[key]);
  7. });

3. 指令使用

  1. // src/views/system/user/index.vue
  2. <el-button v-hasPerm="['sys:user:add']">新增</el-button>
  3. <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实现国际化

  1. // src/App.vue
  2. <template>
  3. <el-config-provider :locale="locale">
  4. <router-view />
  5. </el-config-provider>
  6. </template>
  7. <script setup lang="ts">
  8. import { computed, onMounted, ref, watch } from "vue";
  9. import { ElConfigProvider } from "element-plus";
  10. import useStore from "@/store";
  11. // 导入 Element Plus 语言包
  12. import zhCn from "element-plus/es/locale/lang/zh-cn";
  13. import en from "element-plus/es/locale/lang/en";
  14. // 获取系统语言
  15. const { app } = useStore();
  16. const language = computed(() => app.language);
  17. const locale = ref();
  18. watch(
  19. language,
  20. (value) => {
  21. if (value == "en") {
  22. locale.value = en;
  23. } else { // 默认中文
  24. locale.value = zhCn;
  25. }
  26. },
  27. {
  28. // 初始化立即执行
  29. immediate: true
  30. }
  31. );
  32. </script>

自定义国际化

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

1. 安装 vue-i18n

npm install vue-i18n@9.1.9

2. 语言包

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

  1. // src/lang/en.ts
  2. export default {
  3. // 路由国际化
  4. route: {
  5. dashboard: 'Dashboard',
  6. document: 'Document'
  7. },
  8. // 登录页面国际化
  9. login: {
  10. title: 'youlai-mall management system',
  11. username: 'Username',
  12. password: 'Password',
  13. login: 'Login',
  14. code: 'Verification Code',
  15. copyright: 'Copyright © 2020 - 2022 youlai.tech All Rights Reserved. ',
  16. icp: ''
  17. },
  18. // 导航栏国际化
  19. navbar:{
  20. dashboard: 'Dashboard',
  21. logout:'Logout',
  22. document:'Document',
  23. gitee:'Gitee'
  24. }
  25. }

3. 创建i18n实例

  1. // src/lang/index.ts
  2. // 自定义国际化配置
  3. import {createI18n} from 'vue-i18n'
  4. import {localStorage} from '@/utils/storage'
  5. // 本地语言包
  6. import enLocale from './en'
  7. import zhCnLocale from './zh-cn'
  8. const messages = {
  9. 'zh-cn': {
  10. ...zhCnLocale
  11. },
  12. en: {
  13. ...enLocale
  14. }
  15. }
  16. /**
  17. * 获取当前系统使用语言字符串
  18. *
  19. * @returns zh-cn|en ...
  20. */
  21. export const getLanguage = () => {
  22. // 本地缓存获取
  23. let language = localStorage.get('language')
  24. if (language) {
  25. return language
  26. }
  27. // 浏览器使用语言
  28. language = navigator.language.toLowerCase()
  29. const locales = Object.keys(messages)
  30. for (const locale of locales) {
  31. if (language.indexOf(locale) > -1) {
  32. return locale
  33. }
  34. }
  35. return 'zh-cn'
  36. }
  37. const i18n = createI18n({
  38. locale: getLanguage(),
  39. messages: messages
  40. })
  41. export default i18n

4. i18n 全局注册

  1. // main.ts
  2. // 国际化
  3. import i18n from "@/lang/index";
  4. app.use(i18n)
  5. .mount('#app');

5. 静态页面国际化

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

  1. <h3 class="title">{{ $t("login.title") }}</h3>

6. 动态路由国际化

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

  1. // src/utils/i18n.ts
  2. import i18n from "@/lang/index";
  3. export function generateTitle(title: any) {
  4. // 判断是否存在国际化配置,如果没有原生返回
  5. const hasKey = i18n.global.te('route.' + title)
  6. if (hasKey) {
  7. const translatedTitle = i18n.global.t('route.' + title)
  8. return translatedTitle
  9. }
  10. return title
  11. }

页面使用

  1. ​// src/components/Breadcrumb/index.vue
  2. <template>
  3. <a v-else @click.prevent="handleLink(item)">
  4. {{ generateTitle(item.meta.title) }}
  5. </a>
  6. </template>
  7. <script setup lang="ts">
  8. import {generateTitle} from '@/utils/i18n'
  9. </script>

wangEditor富文本编辑器

推荐教程:

1. 安装wangEditor和Vue3组件

  1. ​npm install @wangeditor/editor --save
  2. npm install @wangeditor/editor-for-vue@next --save

2. wangEditor组件封装

  1. <!-- src/components/WangEditor/index.vue -->
  2. <template>
  3. <div style="border: 1px solid #ccc">
  4. <!-- 工具栏 -->
  5. <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #ccc" :mode="mode" />
  6. <!-- 编辑器 -->
  7. <Editor :defaultConfig="editorConfig" v-model="defaultHtml" @onChange="handleChange"
  8. style="height: 500px; overflow-y: hidden;" :mode="mode" @onCreated="handleCreated" />
  9. </div>
  10. </template>
  11. <script setup lang="ts">
  12. import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue'
  13. import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
  14. // API 引用
  15. import { uploadFile } from "@/api/system/file";
  16. const props = defineProps({
  17. modelValue: {
  18. type: [String],
  19. default: ''
  20. },
  21. })
  22. const emit = defineEmits(['update:modelValue']);
  23. // 编辑器实例,必须用 shallowRef
  24. const editorRef = shallowRef()
  25. const state = reactive({
  26. toolbarConfig: {},
  27. editorConfig: {
  28. placeholder: '请输入内容...',
  29. MENU_CONF: {
  30. uploadImage: {
  31. // 自定义图片上传
  32. async customUpload(file: any, insertFn: any) {
  33. console.log("上传图片")
  34. uploadFile(file).then(response => {
  35. const url = response.data
  36. insertFn(url)
  37. })
  38. }
  39. }
  40. }
  41. },
  42. defaultHtml: props.modelValue,
  43. mode: 'default'
  44. })
  45. const { toolbarConfig, editorConfig, defaultHtml, mode } = toRefs(state)
  46. const handleCreated = (editor: any) => {
  47. editorRef.value = editor // 记录 editor 实例,重要!
  48. }
  49. function handleChange(editor: any) {
  50. emit('update:modelValue', editor.getHtml())
  51. }
  52. // 组件销毁时,也及时销毁编辑器
  53. onBeforeUnmount(() => {
  54. const editor = editorRef.value
  55. if (editor == null) return
  56. editor.destroy()
  57. })
  58. </script>
  59. <style src="@wangeditor/editor/dist/css/style.css">
  60. </style>

3. 使用案例

  1. <template>
  2. <div class="component-container">
  3. <editor v-model="modelValue.detail" style="height: 600px" />
  4. </div>
  5. </template>
  6. <script setup lang="ts">
  7. import Editor from "@/components/WangEditor/index.vue";
  8. </script>
  1. <template>
  2. <div class="component-container">
  3. <editor v-model="modelValue.detail" style="height: 600px" />
  4. </div>
  5. </template>
  6. <script setup lang="ts">
  7. import Editor from "@/components/WangEditor/index.vue";
  8. </script>

Echarts图表

1. 安装 Echarts

npm install echarts

2. Echarts 自适应大小工具类

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

  1. // src/utils/resize.ts
  2. import { ref } from 'vue'
  3. export default function() {
  4. const chart = ref<any>()
  5. const sidebarElm = ref<Element>()
  6. const chartResizeHandler = () => {
  7. if (chart.value) {
  8. chart.value.resize()
  9. }
  10. }
  11. const sidebarResizeHandler = (e: TransitionEvent) => {
  12. if (e.propertyName === 'width') {
  13. chartResizeHandler()
  14. }
  15. }
  16. const initResizeEvent = () => {
  17. window.addEventListener('resize', chartResizeHandler)
  18. }
  19. const destroyResizeEvent = () => {
  20. window.removeEventListener('resize', chartResizeHandler)
  21. }
  22. const initSidebarResizeEvent = () => {
  23. sidebarElm.value = document.getElementsByClassName('sidebar-container')[0]
  24. if (sidebarElm.value) {
  25. sidebarElm.value.addEventListener('transitionend', sidebarResizeHandler as EventListener)
  26. }
  27. }
  28. const destroySidebarResizeEvent = () => {
  29. if (sidebarElm.value) {
  30. sidebarElm.value.removeEventListener('transitionend', sidebarResizeHandler as EventListener)
  31. }
  32. }
  33. const mounted = () => {
  34. initResizeEvent()
  35. initSidebarResizeEvent()
  36. }
  37. const beforeDestroy = () => {
  38. destroyResizeEvent()
  39. destroySidebarResizeEvent()
  40. }
  41. const activated = () => {
  42. initResizeEvent()
  43. initSidebarResizeEvent()
  44. }
  45. const deactivated = () => {
  46. destroyResizeEvent()
  47. destroySidebarResizeEvent()
  48. }
  49. return {
  50. chart,
  51. mounted,
  52. beforeDestroy,
  53. activated,
  54. deactivated
  55. }
  56. }

3. Echarts使用

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

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

  1. <!-- src/views/dashboard/components/Chart/BarChart.vue -->
  2. <!-- 线 + 柱混合图 -->
  3. <template>
  4. <div
  5. :id="id"
  6. :class="className"
  7. :style="{height, width}"
  8. />
  9. </template>
  10. <script setup lang="ts">
  11. import {nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted} from "vue";
  12. import {init, EChartsOption} from 'echarts'
  13. import * as echarts from 'echarts';
  14. import resize from '@/utils/resize'
  15. const props = defineProps({
  16. id: {
  17. type: String,
  18. default: 'barChart'
  19. },
  20. className: {
  21. type: String,
  22. default: ''
  23. },
  24. width: {
  25. type: String,
  26. default: '200px',
  27. required: true
  28. },
  29. height: {
  30. type: String,
  31. default: '200px',
  32. required: true
  33. }
  34. })
  35. const {
  36. mounted,
  37. chart,
  38. beforeDestroy,
  39. activated,
  40. deactivated
  41. } = resize()
  42. function initChart() {
  43. const barChart = init(document.getElementById(props.id) as HTMLDivElement)
  44. barChart.setOption({
  45. title: {
  46. show: true,
  47. text: '业绩总览(2021年)',
  48. x: 'center',
  49. padding: 15,
  50. textStyle: {
  51. fontSize: 18,
  52. fontStyle: 'normal',
  53. fontWeight: 'bold',
  54. color: '#337ecc'
  55. }
  56. },
  57. grid: {
  58. left: '2%',
  59. right: '2%',
  60. bottom: '10%',
  61. containLabel: true
  62. },
  63. tooltip: {
  64. trigger: 'axis',
  65. axisPointer: {
  66. type: 'cross',
  67. crossStyle: {
  68. color: '#999'
  69. }
  70. }
  71. },
  72. legend: {
  73. x: 'center',
  74. y: 'bottom',
  75. data: ['收入', '毛利润', '收入增长率', '利润增长率']
  76. },
  77. xAxis: [
  78. {
  79. type: 'category',
  80. data: ['上海', '北京', '浙江', '广东', '深圳', '四川', '湖北', '安徽'],
  81. axisPointer: {
  82. type: 'shadow'
  83. }
  84. }
  85. ],
  86. yAxis: [
  87. {
  88. type: 'value',
  89. min: 0,
  90. max: 10000,
  91. interval: 2000,
  92. axisLabel: {
  93. formatter: '{value} '
  94. }
  95. },
  96. {
  97. type: 'value',
  98. min: 0,
  99. max: 100,
  100. interval: 20,
  101. axisLabel: {
  102. formatter: '{value}%'
  103. }
  104. }
  105. ],
  106. series: [
  107. {
  108. name: '收入',
  109. type: 'bar',
  110. data: [
  111. 8000, 8200, 7000, 6200, 6500, 5500, 4500, 4200, 3800,
  112. ],
  113. barWidth: 20,
  114. itemStyle: {
  115. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  116. { offset: 0, color: '#83bff6' },
  117. { offset: 0.5, color: '#188df0' },
  118. { offset: 1, color: '#188df0' }
  119. ])
  120. }
  121. },
  122. {
  123. name: '毛利润',
  124. type: 'bar',
  125. data: [
  126. 6700, 6800, 6300, 5213, 4500, 4200, 4200, 3800
  127. ],
  128. barWidth: 20,
  129. itemStyle: {
  130. color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
  131. { offset: 0, color: '#25d73c' },
  132. { offset: 0.5, color: '#1bc23d' },
  133. { offset: 1, color: '#179e61' }
  134. ])
  135. }
  136. },
  137. {
  138. name: '收入增长率',
  139. type: 'line',
  140. yAxisIndex: 1,
  141. data: [65, 67, 65, 53, 47, 45, 43, 42, 41],
  142. itemStyle: {
  143. color: '#67C23A'
  144. }
  145. },
  146. {
  147. name: '利润增长率',
  148. type: 'line',
  149. yAxisIndex: 1,
  150. data: [80, 81, 78, 67, 65, 60, 56,51, 45 ],
  151. itemStyle: {
  152. color: '#409EFF'
  153. }
  154. }
  155. ]
  156. } as EChartsOption)
  157. chart.value = barChart
  158. }
  159. onBeforeUnmount(() => {
  160. beforeDestroy()
  161. })
  162. onActivated(() => {
  163. activated()
  164. })
  165. onDeactivated(() => {
  166. deactivated()
  167. })
  168. onMounted(() => {
  169. mounted()
  170. nextTick(() => {
  171. initChart()
  172. })
  173. })
  174. </script>

项目源码

GiteeGithub
vue3-element-admin
网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发