Element-Plus项目搭建
Element-Plus项目搭建
项目使用 Vue3 + Vite + TypeScript + Element Plus + Vue Router + Pinia 等前端主流技术栈
环境准备
1. 运行环境Node
Node下载地址: http://nodejs.cn/download/
根据本机环境选择对应版本下载,安装过程可视化操作非常简便,静默安装即可。
安装完成后命令行终端 node -v 查看版本号以验证是否安装成功:

2.pnpm安装
npm install -g pnpm //通过 npm 安装
3. 开发工具VSCode
下载地址:https://code.visualstudio.com/Download
4. 必装插件Volar
VSCode 插件市场搜索 Volar (就排在第一位的骷髅头),且要禁用默认的 Vetur.

项目初始化
1. Vite 是什么?
Vite是一种新型前端构建工具,能够显著提升前端开发体验。
Vite 官方中文文档:https://cn.vitejs.dev/guide/
2. 初始化项目
npm create vite [vue3-element-admin]
vue3-element-admin:项目名称
vue-ts : Vue + TypeScript 的模板,除此还有vue,react,react-ts模板
3. 启动项目
cd vue3-element-admin
npm install
npm run dev
安装SCSS
npm install sass sass-loader -d
浏览器访问: 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/dist/index.css'
// 全局注册icons
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
3. Element Plus全局组件类型声明
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
4. 页面使用 Element Plus 组件和图标
引入icons
import {Search, Edit,Check,Message,Star, Delete} from '@element-plus/icons-vue'
代码如下
<!-- 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. 效果预览

路径别名配置
1.使用 @ 代替 src
tsconfig.json
{
"compilerOptions":{
/** baseUrl 用来告诉编译器到哪里去查找模块,使用非相对模块时必须配置此项 */
"baseUrl": ".",
/** 非相对模块导入的路径映射配置,根据 baseUrl 配置进行路径计算 */
"paths": {
"@/*": ["src/*"]
},
}
}
// vite.config.ts
resolve: {
alias: {
/** @ 符号指向 src 目录 */
"@": resolve(__dirname, "./src")
}
},
2. 安装@types/node
本地安装 Node 的 TypeScript 类型描述文件即可解决编译器报错
npm install @types/node --save-dev
3. Vite配置
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path, { resolve } from "path"
// vite配置地址:https://cn.vitejs.dev/config
export default ({ mode }: any) => {
return defineConfig({
plugins: [vue()],
resolve: {
alias: {
/** @ 符号指向 src 目录 */
"@": resolve(__dirname, "./src")
}
},
})
}
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_BASE_API = 'https://mock.liuj.com/mock/api/v1'
VITE_APP_PORT = 3000
VITE_PUBLIC_PATH = '/dev-api'
- 生产环境配置:.env.production
- 变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_BASE_API = 'https://mock.liuj.com/mock/api/v1'
VITE_APP_PORT = 3000
VITE_PUBLIC_PATH = '/prod-api'
- 模拟生产环境配置:.env.staging
- 变量必须以 VITE_ 为前缀才能暴露给外部读取
VITE_BASE_API = 'https://mock.liuj.com/mock/api/v1'
VITE_APP_PORT = 3000
VITE_PUBLIC_PATH = '/prod--api'
2.环境变量智能提示
添加环境变量类型声明
// src/ env.d.ts
// 环境变量类型声明
interface ImportMetaEnv {
VITE_BASE_API: string,
VITE_APP_PORT: string,
VITE_PUBLIC_PATH: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
后面在使用自定义环境变量就会有智能提示,环境变量使用请参考下一节。

<template>
<router-view></router-view>
</template>
<script setup lang="ts">
console.log(import.meta.env.VITE_APP_PORT)
console.log(import.meta.env.VITE_BASE_API)
console.log(import.meta.env.VITE_PUBLIC_PATH)
</script>
<style lang="scss" scoped>
</style>

3.模式
默认情况下,开发服务器 (dev
命令) 运行在 development
(开发) 模式,而 build
命令则运行在 production
(生产) 模式。
这意味着当执行 vite build
时,它会自动加载 .env.production
中可能存在的环境变量:
在你的应用中,你可以使用 import.meta.env.VITE_APP_TITLE
渲染标题。
你可以通过传递 --mode
选项标志来覆盖命令使用的默认模式。例如,如果你想为我们假设的 staging 模式构建应用:
vite build --mode staging
4.在配置中使用环境变量
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path, { resolve } from "path"
// vite配置地址:https://cn.vitejs.dev/config
export default ({ mode }: any) => {
const viteEnv = loadEnv(mode, process.cwd(), '')
const { VITE_APP_PORT } = viteEnv
console.log(VITE_APP_PORT);
return defineConfig({
plugins: [vue()],
resolve: {
alias: {
/** @ 符号指向 src 目录 */
"@": resolve(__dirname, "./src")
}
},
})
}
浏览器跨域处理
1. 跨域原理
浏览器同源策略: 协议、域名和端口都相同是同源,浏览器会限制非同源请求读取响应结果。
解决浏览器跨域限制大体分为后端和前端两个方向:
- 后端:开启 CORS 资源共享;
- 前端:使用反向代理欺骗浏览器误认为是同源请求;
2. 前端反向代理解决跨域
Vite 配置反向代理解决跨域,因为需要读取环境变量,故写法和上文的出入较大,这里贴出完整的 vite.config.ts 配置。
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path, { resolve } from "path"
// vite配置地址:https://cn.vitejs.dev/config
export default ({ mode }: any) => {
// 获取环境变量中的数据
const viteEnv = loadEnv(mode, process.cwd(), '')
const { VITE_APP_PORT } = viteEnv
// console.log(loadEnv(mode, process.cwd()));
return defineConfig({
plugins: [vue()],
resolve: {
alias: {
/** @ 符号指向 src 目录 */
"@": resolve(__dirname, "./src")
}
},
server: {
/** 是否开启 HTTPS */
https: false,
/** 设置 host: true 才可以使用 Network 的形式,以 IP 访问项目 */
host: true, // host: "0.0.0.0"
/** 端口号 */
port: VITE_APP_PORT,
/** 是否自动打开浏览器 */
open: false,
/** 跨域设置允许 */
cors: true,
/** 端口被占用时,是否直接退出 */
strictPort: false,
/** 接口代理 */
proxy: {
"/api/v1": {
target: "https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1",
ws: true,
/** 是否允许跨域 */
changeOrigin: true,
rewrite: (path) => path.replace("/api/v1", "")
}
}
},
build: {
sourcemap: false,
/** Vite 2.6.x 以上需要配置 minify: "terser", terserOptions 才能生效 */
minify: 'terser',
/** 消除打包大小超过 500kb 警告 */
chunkSizeWarningLimit: 1500,
/** 在打包代码时移除 console.log、debugger */
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id
.toString()
.split('node_modules/')[1]
.split('/')[0]
.toString();
}
},
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
? chunkInfo.facadeModuleId.split('/')
: [];
const fileName =
facadeModuleId[facadeModuleId.length - 2] || '[name]';
return `js/${fileName}/[name].[hash].js`;
}
}
}
}
})
}
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 { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path, { resolve } from "path"
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import path from 'path'
export default ({ mode }: any) => {
const viteEnv = loadEnv(mode, process.cwd(), '')
const { VITE_APP_PORT } = viteEnv
// console.log(loadEnv(mode, process.cwd()));
return defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
})
],
resolve: {
alias: {
/** @ 符号指向 src 目录 */
"@": resolve(__dirname, "./src")
}
},
})
}
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. 定义Store
使用defineStore
定义store
,第一个参数必须是全局唯一的id,可以使用Symbol
// src/store/index.ts
import { defineStore} from 'pinia'
export const useStore = defineStore('main',{
state:()=>{
return {
counter: 10
}
},
getters:{
doubleCount: (state) => state.counter * 2
},
actions:{
increment() {
this.counter++
}
}
})
4. 使用Pinia
在vue页面中使用
//HelloWorld.vue
<script setup lang="ts">
import { useStore } from "@/store";
const store = useStore();
//不推荐下面这种写法
const counter = store.counter
const doubleCount = store.doubleCount
</script>
<template>
//直接调用store中的值
<h1>{{store.counter}}</h1>
//做逻辑操作后调用store中的值
<h1>{{store.doubleCount}}</h1>
//调用store中的方法
<el-button @click="store.increment" type="primary">Primary</el-button>
<br>
//如果store中有改变或点击increment事件,那么下面这种方式不会在页面更新,不推荐
<h1>{{counter}}</h1>
<h1>{{doubleCount}}</h1>
</template>
<style scoped>
</style>
注意:在页面中使用引入store会出现找不到模块,这个时候不用管,实际上是能找到的

界面显示

Axios网络请求库封装
1. axios工具封装
// src/utils/request.ts
import axios 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_PUBLIC_PATH,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
// 请求拦截器
service.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
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
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// 添加响应拦截器
service.interceptors.response.use(
function (response) {
console.log(response)
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
const { code, msg } = response.data;
if (code === '00000') {
return response.data;
} else {
ElMessage({
message: msg || '系统出错',
type: 'error'
})
return Promise.reject(new Error(msg || 'Error'))
}
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
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/login.ts
// 导入axios实例
import httpRequest from '@/utils/request'
// 定义接口的传参
interface UserInfoParam {
userID: string,
userName: string
}
// 获取用户信息
export function apiGetUserInfo(param: UserInfoParam) {
return httpRequest({
url: '/users',
method: 'post',
data: param,
})
}
3. API调用
// src/login/login.ts
import { apiGetUserInfo } from '@/api/login'
// 获取登录用户信息
const param = {
userID: '10001',
userName: 'Mike',
}
apiGetUserInfo(param).then((res) => {
console.log(res)
})
动态权限路由
官方文档: https://router.vuejs.org/zh/api/
1. 安装 vue-router
npm install vue-router@4
2. 创建路由实例
创建路由实例并导出,其中包括静态路由数据,动态路由后面将通过接口从后端获取并整合用户角色的权限控制。
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
// 创建路由规则
const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/dashboard',
},
{
path: '/',
name: 'home',
component: () => import(/* webpackChunkName: "login" */'../views/home/home.vue'),
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: {
title: '系统首页',
permiss: '1',
},
component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard/dashboard.vue'),
},
{
path: '/user',
name: 'user',
meta: {
title: '个人中心',
},
component: () => import(/* webpackChunkName: "user" */ '../views/user/user.vue'),
},
{
path: '/form',
name: 'baseform',
meta: {
title: '表单',
permiss: '5',
},
component: () => import(/* webpackChunkName: "form" */ '../views/form/form.vue'),
},
{
path: '/tabs',
name: 'tabs',
meta: {
title: 'tab标签',
permiss: '3',
},
component: () => import(/* webpackChunkName: "tabs" */ '../views/tabs/tabs.vue'),
},
{
path: '/permission',
name: 'permission',
meta: {
title: '权限管理',
permiss: '13',
},
component: () => import(/* webpackChunkName: "permission" */ '../views/permission/permission.vue'),
},
{
path: '/icon',
name: 'icon',
meta: {
title: '自定义图标',
permiss: '10',
},
component: () => import(/* webpackChunkName: "icon" */ '../views/icon/icon.vue'),
},
],
},
{
path: '/login',
name: 'Login',
meta: {
title: '登录',
},
component: () => import(/* webpackChunkName: "login" */ '../views/login/login.vue'),
},
{
path: '/404',
name: '404',
meta: {
title: '没有权限',
},
component: () => import(/* webpackChunkName: "404" */ '../views/404/404.vue'),
},
];
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新时,滚动条位置还原
scrollBehavior: () => ({ left: 0, top: 0 })
});
router.beforeEach((to, from, next) => {
console.log(to.name)
console.log(to.meta.permiss)
const role = localStorage.getItem('ms_username');
if (!role && to.path !== '/login') {
next('/login');
}else {
next();
}
})
// 将路由对象暴露出去
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
}
}
//获取当前系统使用语言字符串
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富文本编辑器
推荐教程:Vue3 官方示例
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>

国际化il8n
安装 vue-i18n
npm i vue-i18n
新建 locales 文件夹
以简体中文和英文为例
在 src 下新建 locales 文件夹
在 locales 文件夹下新建 zh-cn.ts
export default {
buttons: {
login: '登录'
},
menus: {
home: '首页'
}
}
在 locales 文件夹下新建 en.ts
export default {
buttons: {
login: 'Login'
},
menus: {
home: 'Home'
}
}
在 locales 文件夹下新建 index.ts
import { createI18n } from 'vue-i18n'
import zhCn from './zh-cn'
import en from './en'
// 创建 i18n
const i18n = createI18n({
legacy: false,
globalInjection: true, // 全局模式,可以直接使用 $t
locale: localStorage.getItem('lang') || 'zhCn',
messages: {
zhCn,
en
}
})
export default i18n
注册 i18n
在 main.ts 文件下注册 i18n
import { createApp } from 'vue'
import App from './App.vue'
import i18n from './locales'
const app = createApp(App)
app.use(i18n)
app.mount('#app')
使用方法
在 template 中的使用
{{ $t('menus.home') }}
在 ts 中的使用
import i18n from './locales'
console.log(i18n.global.t('menus.home'))
Element Plus 国际化
Element Plus 官方提供了一个 Vue 组件 ConfigProvider 用于全局配置国际化的设置 el-config-provider 由 Element Plus 按需引入 - 自动导入 el-config-provider 手动导入:import { ElConfigProvider } from 'element-plus'
<template>
<el-config-provider :locale="useAppStoreHook().locale === 'zhCn' ? zhCn : en">
<app />
</el-config-provider>
</template>
<script lang="ts" setup>
import zhCn from 'element-plus/lib/locale/lang/zh-cn'
import en from 'element-plus/lib/locale/lang/en'
import { useAppStoreHook } from '@/store/modules/app' //store存放语言配置
</script>
语言切换 切换语言时,修改 store 、 localstorage 和 i18n 中的语言配置
// store/modules/app
import { defineStore } from 'pinia'
import { store } from '@/store'
import i18n from '@/locales'
const useAppStore = defineStore('app', {
state: () => {
return {
locale: localStorage.getItem('lang') || 'zhCn'
}
},
actions: {
SET_LOCALE(locale: string) { //语言切换
this.locale = locale
storageLocal.setItem('lang', locale)
i18n.global.locale.value = locale
}
}
})
export function useAppStoreHook() {
return useAppStore(store)
}
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>
