项目简介
是基于 升级的 + 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 Router | Vue.js 的官方路由 | https://router.vuejs.org/zh/ |
wangEditor | Typescript 开发的 Web 富文本编辑器 | https://www.wangeditor.com/ |
Echarts | 一个基于 JavaScript 的开源可视化图表库 | https://echarts.apache.org/zh/ |
项目预览
在线预览地址:
以下截图是来自有来商城管理前端 ,是基于 为基础开发的具有一套完整的系统权限管理的商城管理系统,数据均为线上真实的而非Mock。
国际化
已实现 Element Plus 组件和菜单路由的国际化,不过只做了少量国际化工作,国际化大部分是体力活,如果你有国际化的需求,会在下文从0到1实现Element Plus组件和菜单路由的国际化。
主题设置
大小切换
角色管理
菜单管理
商品上架
库存设置
微信小程序/ APP/ H5 显示上架商品效果
启动部署
- 项目启动
- npm install
- 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. 启动项目
- cd vue3-element-admin
- npm install
- npm run dev
浏览器访问:
整合Element-Plus
1.本地安装Element Plus和图标组件
- npm install element-plus
- npm install @element-plus/icons-vue
2.全局注册组件
- // main.ts
- import ElementPlus from 'element-plus'
- import 'element-plus/theme-chalk/index.css'
-
- createApp(App)
- .use(ElementPlus)
- .mount('#app')
3. Element Plus全局组件类型声明
- // tsconfig.json
- {
- "compilerOptions": {
- // ...
- "types": ["element-plus/global"]
- }
- }
4. 页面使用 Element Plus 组件和图标
- <!-- src/App.vue -->
- <template>
- <img alt="Vue logo" src="./assets/logo.png"/>
- <HelloWorld msg="Hello Vue 3 + TypeScript + Vite"/>
- <div style="text-align: center;margin-top: 10px">
- <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>
5. 效果预览
路径别名配置
使用 @ 代替 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 的图标,本节通过整合 插件使用第三方图标库。
1. 安装 vite-plugin-svg-icons
- npm i fast-glob@3.2.11 -D
- npm i vite-plugin-svg-icons@2.0.1 -D
2. 创建图标文件夹
项目创建 src/assets/icons
文件夹,存放 iconfont 下载的 SVG 图标
3. main.ts 引入注册脚本
- // main.ts
- import 'virtual:svg-icons-register';
4. vite.config.ts 插件配置
- // vite.config.ts
- import {UserConfig, ConfigEnv, loadEnv} from 'vite'
- import vue from '@vitejs/plugin-vue'
- import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
-
- export default ({command, mode}: ConfigEnv): UserConfig => {
- // 获取 .env 环境配置文件
- const env = loadEnv(mode, process.cwd())
-
- return (
- {
- plugins: [
- vue(),
- createSvgIconsPlugin({
- // 指定需要缓存的图标文件夹
- iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
- // 指定symbolId格式
- symbolId: 'icon-[dir]-[name]',
- })
- ]
- }
- )
- }
5. TypeScript支持
- // tsconfig.json
- {
- "compilerOptions": {
- "types": ["vite-plugin-svg-icons/client"]
- }
- }
6. 组件封装
- <!-- 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富文本编辑器
推荐教程:
1. 安装wangEditor和Vue3组件
- npm install @wangeditor/editor --save
- npm install @wangeditor/editor-for-vue@next --save
2. wangEditor组件封装
-
- <!-- src/components/WangEditor/index.vue -->
- <template>
- <div style="border: 1px solid #ccc">
- <!-- 工具栏 -->
- <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" style="border-bottom: 1px solid #ccc" :mode="mode" />
- <!-- 编辑器 -->
- <Editor :defaultConfig="editorConfig" v-model="defaultHtml" @onChange="handleChange"
- style="height: 500px; overflow-y: hidden;" :mode="mode" @onCreated="handleCreated" />
- </div>
- </template>
-
- <script setup lang="ts">
- import { onBeforeUnmount, shallowRef, reactive, toRefs } from 'vue'
- import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
-
- // API 引用
- import { uploadFile } from "@/api/system/file";
-
- const props = defineProps({
- modelValue: {
- type: [String],
- default: ''
- },
- })
-
- const emit = defineEmits(['update:modelValue']);
-
- // 编辑器实例,必须用 shallowRef
- const editorRef = shallowRef()
-
- const state = reactive({
- toolbarConfig: {},
- editorConfig: {
- placeholder: '请输入内容...',
- MENU_CONF: {
- uploadImage: {
- // 自定义图片上传
- async customUpload(file: any, insertFn: any) {
- console.log("上传图片")
- uploadFile(file).then(response => {
- const url = response.data
- insertFn(url)
- })
- }
- }
- }
- },
- defaultHtml: props.modelValue,
- mode: 'default'
- })
-
- const { toolbarConfig, editorConfig, defaultHtml, mode } = toRefs(state)
-
-
- const handleCreated = (editor: any) => {
- editorRef.value = editor // 记录 editor 实例,重要!
- }
-
-
- function handleChange(editor: any) {
- emit('update:modelValue', editor.getHtml())
- }
-
- // 组件销毁时,也及时销毁编辑器
- onBeforeUnmount(() => {
- const editor = editorRef.value
- if (editor == null) return
- editor.destroy()
- })
-
- </script>
-
- <style src="@wangeditor/editor/dist/css/style.css">
- </style>
-
-
3. 使用案例
- <template>
- <div class="component-container">
- <editor v-model="modelValue.detail" style="height: 600px" />
- </div>
- </template>
-
- <script setup lang="ts">
- import Editor from "@/components/WangEditor/index.vue";
- </script>
-
-
- <template>
- <div class="component-container">
- <editor v-model="modelValue.detail" style="height: 600px" />
- </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"
- :style="{height, width}"
- />
- </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 |