新版本文檔
【vue3-element-admin 】基于 Vue3 + Vite4 + TypeScript5+ Element-Plus 從0到1搭建企業(yè)級(jí)后臺(tái)管理系統(tǒng)(前后端開源)_有來(lái)技術(shù)的博客-CSDN博客vue3-element-admin 是基于 vue-element-admin 升級(jí)的 Vue3 + Element Plus 版本的后臺(tái)管理前端解決方案,技術(shù)棧為 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等當(dāng)前主流框架。本篇是 vue3-element-admin v2.x 版本從 0 到 1,相較于v1.x 版本增加了對(duì)原子CSS(UnoCSS)、按需自動(dòng)導(dǎo)入、暗黑模式的支持。https://blog.csdn.net/u013737132/article/details/130191394
項(xiàng)目簡(jiǎn)介
vue3-element-admin?是基于?vue-element-admin?升級(jí)的 Vue3 + Element Plus 版本的后臺(tái)管理前端解決方案,是?有來(lái)技術(shù)團(tuán)隊(duì)?繼?youlai-mall?全棧開源商城項(xiàng)目的又一開源力作。
項(xiàng)目使用 Vue3 + Vite2 + TypeScript + Element Plus + Vue Router + Pinia + Volar 等前端主流技術(shù)棧,基于此項(xiàng)目模板完成有來(lái)商城管理前端的 Vue3 版本。
本篇先對(duì)本項(xiàng)目功能、技術(shù)棧進(jìn)行整體概述,再細(xì)節(jié)的講述從0到1搭建 vue3-element-admin,在希望大家對(duì)本項(xiàng)目有個(gè)完完整整整了解的同時(shí)也能夠在學(xué) Vue3 + TypeScript 等技術(shù)棧少花些時(shí)間,少走些彎路,這樣團(tuán)隊(duì)在毫無(wú)保留開源才有些許意義。
功能清單
技術(shù)棧清單
技術(shù)棧 | 描述 | 官網(wǎng) |
---|---|---|
Vue3 | 漸進(jìn)式 JavaScript 框架 | https://v3.cn.vuejs.org/ |
TypeScript | 微軟新推出的一種語(yǔ)言,是 JavaScript 的超集 | https://www.tslang.cn/ |
Vite2 | 前端開發(fā)與構(gòu)建工具 | https://cn.vitejs.dev/ |
Element Plus | 基于 Vue 3,面向設(shè)計(jì)師和開發(fā)者的組件庫(kù) | https://element-plus.gitee.io/zh-CN/ |
Pinia | 新一代狀態(tài)管理工具 | https://pinia.vuejs.org/ |
Vue Router | Vue.js 的官方路由 | https://router.vuejs.org/zh/ |
wangEditor | Typescript 開發(fā)的 Web 富文本編輯器 | https://www.wangeditor.com/ |
Echarts | 一個(gè)基于 JavaScript 的開源可視化圖表庫(kù) | https://echarts.apache.org/zh/ |
項(xiàng)目預(yù)覽
在線預(yù)覽地址:www.youlai.tech
以下截圖是來(lái)自有來(lái)商城管理前端?mall-admin-web?,是基于?vue3-element-admin?為基礎(chǔ)開發(fā)的具有一套完整的系統(tǒng)權(quán)限管理的商城管理系統(tǒng),數(shù)據(jù)均為線上真實(shí)的而非Mock。
國(guó)際化
已實(shí)現(xiàn) Element Plus 組件和菜單路由的國(guó)際化,不過只做了少量國(guó)際化工作,國(guó)際化大部分是體力活,如果你有國(guó)際化的需求,會(huì)在下文從0到1實(shí)現(xiàn)Element Plus組件和菜單路由的國(guó)際化。
主題設(shè)置
大小切換
角色管理
菜單管理
商品上架
庫(kù)存設(shè)置
微信小程序/ APP/ H5 顯示上架商品效果
啟動(dòng)部署
- 項(xiàng)目啟動(dòng)
npm install
npm run dev
瀏覽器訪問?http://localhost:3000
- 項(xiàng)目部署
npm run build:prod
生成的靜態(tài)文件在工程根目錄 dist 文件夾
項(xiàng)目從0到1構(gòu)建
安裝第三方插件請(qǐng)注意項(xiàng)目源碼的
package.json
版本號(hào),有些升級(jí)不考慮兼容性的插件在 install 的時(shí)候我會(huì)帶上具體版本號(hào),例如?npm install vue-i18n@9.1.9
?和?npm i vite-plugin-svg-icons@2.0.1 -D
環(huán)境準(zhǔn)備
1. 運(yùn)行環(huán)境Node
Node下載地址: http://nodejs.cn/download/
根據(jù)本機(jī)環(huán)境選擇對(duì)應(yīng)版本下載,安裝過程可視化操作非常簡(jiǎn)便,靜默安裝即可。
安裝完成后命令行終端?node -v
?查看版本號(hào)以驗(yàn)證是否安裝成功:
2. 開發(fā)工具VSCode
下載地址:https://code.visualstudio.com/Download
3. 必裝插件Volar
VSCode 插件市場(chǎng)搜索 Volar (就排在第一位的骷髏頭),且要禁用默認(rèn)的 Vetur.
項(xiàng)目初始化
1. Vite 是什么?
Vite是一種新型前端構(gòu)建工具,能夠顯著提升前端開發(fā)體驗(yàn)。
Vite 官方中文文檔:https://cn.vitejs.dev/guide/
2. 初始化項(xiàng)目
npm init vite@latest vue3-element-admin --template vue-ts
- vue3-element-admin:項(xiàng)目名稱
- vue-ts : Vue + TypeScript 的模板,除此還有vue,react,react-ts模板
3. 啟動(dòng)項(xiàng)目
cd vue3-element-admin
npm install
npm run dev
瀏覽器訪問:?http://localhost:3000
整合Element-Plus
1.本地安裝Element Plus和圖標(biāo)組件
npm install element-plus
npm install @element-plus/icons-vue
2.全局注冊(cè)組件
// 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. 頁(yè)面使用 Element Plus 組件和圖標(biāo)
<!-- 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. 效果預(yù)覽
路徑別名配置
使用 @ 代替 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") // 相對(duì)路徑別名配置,使用 @ 代替 src
}
}
})
2. 安裝@types/node
import path from 'path'
編譯器報(bào)錯(cuò):TS2307: Cannot find module ‘path’ or its corresponding type declarations.
本地安裝 Node 的 TypeScript 類型描述文件即可解決編譯器報(bào)錯(cuò)
npm install @types/node --save-dev
3. TypeScript 編譯配置
同樣還是import path from 'path'
?編譯報(bào)錯(cuò): TS1259: Module ‘“path”’ can only be default-imported using the ‘a(chǎn)llowSyntheticDefaultImports’ flag
因?yàn)?typescript 特殊的 import 方式 , 需要配置允許默認(rèn)導(dǎo)入的方式,還有路徑別名的配置
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./", // 解析非相對(duì)模塊的基地址,默認(rèn)是當(dāng)前目錄
"paths": { //路徑映射,相對(duì)于baseUrl
"@/*": ["src/*"]
},
"allowSyntheticDefaultImports": true // 允許默認(rèn)導(dǎo)入
}
}
4.別名使用
// App.vue
import HelloWorld from '/src/components/HelloWorld.vue'
↓
import HelloWorld from '@/components/HelloWorld.vue'
環(huán)境變量
官方教程: https://cn.vitejs.dev/guide/env-and-mode.html
1. env配置文件
項(xiàng)目根目錄分別添加 開發(fā)、生產(chǎn)和模擬環(huán)境配置
-
開發(fā)環(huán)境配置:.env.development
# 變量必須以 VITE_ 為前綴才能暴露給外部讀取 VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/dev-api'
-
生產(chǎn)環(huán)境配置:.env.production
VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/prod-api'
-
模擬生產(chǎn)環(huán)境配置:.env.staging
VITE_APP_TITLE = 'vue3-element-admin' VITE_APP_PORT = 3000 VITE_APP_BASE_API = '/prod--api'
2.環(huán)境變量智能提示
添加環(huán)境變量類型聲明
// src/ env.d.ts
// 環(huán)境變量類型聲明
interface ImportMetaEnv {
VITE_APP_TITLE: string,
VITE_APP_PORT: string,
VITE_APP_BASE_API: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
后面在使用自定義環(huán)境變量就會(huì)有智能提示,環(huán)境變量使用請(qǐng)參考下一節(jié)。
瀏覽器跨域處理
1. 跨域原理
瀏覽器同源策略: 協(xié)議、域名和端口都相同是同源,瀏覽器會(huì)限制非同源請(qǐng)求讀取響應(yīng)結(jié)果。
解決瀏覽器跨域限制大體分為后端和前端兩個(gè)方向:
- 后端:開啟 CORS 資源共享;
- 前端:使用反向代理欺騙瀏覽器誤認(rèn)為是同源請(qǐng)求;
2. 前端反向代理解決跨域
Vite 配置反向代理解決跨域,因?yàn)樾枰x取環(huán)境變量,故寫法和上文的出入較大,這里貼出完整的 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 環(huán)境配置文件
const env = loadEnv(mode, process.cwd())
return (
{
plugins: [
vue()
],
// 本地反向代理解決瀏覽器跨域限制
server: {
host: 'localhost',
port: Number(env.VITE_APP_PORT),
open: true, // 啟動(dòng)是否自動(dòng)打開瀏覽器
proxy: {
[env.VITE_APP_BASE_API]: {
target: 'http://www.youlai.tech:9999', // 有來(lái)商城線上接口地址
changeOrigin: true,
rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')
}
}
},
resolve: {
alias: {
"@": path.resolve("./src") // 相對(duì)路徑別名配置,使用 @ 代替 src
}
}
}
)
}
SVG圖標(biāo)
官方教程:?https://github.com/vbenjs/vite-plugin-svg-icons/blob/main/README.zh_CN.md
Element Plus 圖標(biāo)庫(kù)往往滿足不了實(shí)際開發(fā)需求,可以引用和使用第三方例如 iconfont 的圖標(biāo),本節(jié)通過整合?vite-plugin-svg-icons?插件使用第三方圖標(biāo)庫(kù)。
1. 安裝 vite-plugin-svg-icons
npm i fast-glob@3.2.11 -D
npm i vite-plugin-svg-icons@2.0.1 -D
2. 創(chuàng)建圖標(biāo)文件夾
? 項(xiàng)目創(chuàng)建?src/assets/icons
?文件夾,存放 iconfont 下載的 SVG 圖標(biāo)
3. main.ts 引入注冊(cè)腳本
// 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 環(huán)境配置文件
const env = loadEnv(mode, process.cwd())
return (
{
plugins: [
vue(),
createSvgIconsPlugin({
// 指定需要緩存的圖標(biāo)文件夾
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狀態(tài)管理
Pinia 是 Vue.js 的輕量級(jí)狀態(tài)管理庫(kù),Vuex 的替代方案。
尤雨溪于2021.11.24 在 Twitter 上宣布:Pinia 正式成為 vuejs 官方的狀態(tài)庫(kù),意味著 Pinia 就是 Vuex 5 。
1. 安裝Pinia
npm install pinia
2. Pinia全局注冊(cè)
// src/main.ts
import { createPinia } from "pinia"
app.use(createPinia())
.mount('#app')
3. Pinia模塊封裝
// src/store/modules/user.ts
// 用戶狀態(tài)模塊
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網(wǎng)絡(luò)請(qǐng)求庫(kù)封裝
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
// 創(chuàng)建 axios 實(shí)例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
// 請(qǐng)求攔截器
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);
}
)
// 響應(yīng)攔截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data;
if (code === '00000') {
return response.data;
} else {
ElMessage({
message: msg || '系統(tǒng)出錯(cuò)',
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 = '/'; // 跳轉(zhuǎn)登錄頁(yè)
ElMessageBox.alert('當(dāng)前頁(yè)面已失效,請(qǐng)重新登錄', '提示', {})
.then(() => {
})
.catch(() => {
});
} else {
ElMessage({
message: msg || '系統(tǒng)出錯(cuò)',
type: 'error'
})
}
return Promise.reject(new Error(msg || 'Error'))
}
);
// 導(dǎo)出 axios 實(shí)例
export default service
2. API封裝
以登錄成功后獲取用戶信息(昵稱、頭像、角色集合和權(quán)限集合)的接口為案例,演示如何通過封裝的 axios 工具類請(qǐng)求后端接口,其中響應(yīng)數(shù)據(jù)
// src/api/system/user.ts
import request from "@/utils/request";
import { AxiosPromise } from "axios";
import { UserInfo } from "@/types"; // 用戶信息返回?cái)?shù)據(jù)的TypeScript類型聲明,文件路徑 src/types/api/system/user.d.ts
/**
* 登錄成功后獲取用戶信息(昵稱、頭像、權(quán)限集合和角色集合)
*/
export function getUserInfo(): AxiosPromise<UserInfo> {
return request({
url: '/youlai-admin/api/v1/users/me',
method: 'get'
})
}
3. API調(diào)用
// src/store/modules/user.ts
import { getUserInfo } from "@/api/system/user";
// 獲取登錄用戶信息
getUserInfo().then(({ data }) => {
const { nickname, avatar, roles, perms } = data
...
})
動(dòng)態(tài)權(quán)限路由
官方文檔: https://router.vuejs.org/zh/api/
1. 安裝 vue-router
npm install vue-router@next
2. 創(chuàng)建路由實(shí)例
創(chuàng)建路由實(shí)例并導(dǎo)出,其中包括靜態(tài)路由數(shù)據(jù),動(dòng)態(tài)路由后面將通過接口從后端獲取并整合用戶角色的權(quán)限控制。
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import useStore from "@/store";
export const Layout = () => import('@/layout/index.vue')
// 靜態(tài)路由
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 }
}
]
}
]
// 創(chuàng)建路由實(shí)例
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新時(shí),滾動(dòng)條位置還原
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. 路由實(shí)例全局注冊(cè)
// main.ts
import router from "@/router";
app.use(router)
.mount('#app')
4. 動(dòng)態(tài)權(quán)限路由
// 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 }) // 進(jìn)度環(huán)顯示/隱藏
// 白名單路由
const whiteList = ['/login', '/auth-redirect']
router.beforeEach(async (to, form, next) => {
NProgress.start()
const { user, permission } = useStore()
const hasToken = user.token
if (hasToken) {
// 登錄成功,跳轉(zhuǎn)到首頁(yè)
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
// 用戶擁有權(quán)限的路由集合(accessRoutes)
const accessRoutes: any = await permission.generateRoutes(roles)
accessRoutes.forEach((route: any) => {
router.addRoute(route)
})
next({ ...to, replace: true })
} catch (error) {
// 移除 token 并跳轉(zhuǎn)登錄頁(yè)
await user.resetToken()
ElMessage.error(error as any || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
// 未登錄可以訪問白名單頁(yè)面(登錄頁(yè)面)
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)
是根據(jù)用戶角色獲取擁有權(quán)限的路由(靜態(tài)路由+動(dòng)態(tài)路由),核心代碼如下:
// 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
// 靜態(tài)路由 + 動(dòng)態(tài)路由
this.routes = constantRoutes.concat(routes)
},
generateRoutes(roles: string[]) {
return new Promise((resolve, reject) => {
// API 獲取動(dòng)態(tài)路由
listRoutes().then(response => {
const asyncRoutes = response.data
let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
this.setRoutes(accessedRoutes)
resolve(accessedRoutes)
}).catch(error => {
reject(error)
})
})
}
}
})
export default usePermissionStore;
按鈕權(quán)限
1. Directive 自定義指令
// src/directive/permission/index.ts
import useStore from "@/store";
import { Directive, DirectiveBinding } from "vue";
/**
* 按鈕權(quán)限校驗(yàn)
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超級(jí)管理員」擁有所有的按鈕權(quán)限
const { user } = useStore()
const roles = user.roles;
if (roles.includes('ROOT')) {
return true
}
// 「其他角色」按鈕權(quán)限校驗(yàn)
const { value } = binding;
if (value) {
const requiredPerms = value; // DOM綁定需要的按鈕權(quán)限標(biāo)識(shí)
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. 自定義指令全局注冊(cè)
?
// 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國(guó)際化
官方教程:https://element-plus.gitee.io/zh-CN/guide/i18n.html
Element Plus 官方提供全局配置 Config Provider實(shí)現(xiàn)國(guó)際化
?
// 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";
// 導(dǎo)入 Element Plus 語(yǔ)言包
import zhCn from "element-plus/es/locale/lang/zh-cn";
import en from "element-plus/es/locale/lang/en";
// 獲取系統(tǒng)語(yǔ)言
const { app } = useStore();
const language = computed(() => app.language);
const locale = ref();
watch(
language,
(value) => {
if (value == "en") {
locale.value = en;
} else { // 默認(rèn)中文
locale.value = zhCn;
}
},
{
// 初始化立即執(zhí)行
immediate: true
}
);
</script>
自定義國(guó)際化
i18n 英文全拼 internationalization ,國(guó)際化的意思,英文 i 和 n 中間18個(gè)英文字母
1. 安裝 vue-i18n
npm install vue-i18n@9.1.9
2. 語(yǔ)言包
創(chuàng)建 src/lang 語(yǔ)言包目錄,中文語(yǔ)言包 zh-cn.ts,英文語(yǔ)言包 en.ts
?// src/lang/en.ts
export default {
// 路由國(guó)際化
route: {
dashboard: 'Dashboard',
document: 'Document'
},
// 登錄頁(yè)面國(guó)際化
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: ''
},
// 導(dǎo)航欄國(guó)際化
navbar:{
dashboard: 'Dashboard',
logout:'Logout',
document:'Document',
gitee:'Gitee'
}
}
3. 創(chuàng)建i18n實(shí)例
?
// src/lang/index.ts
// 自定義國(guó)際化配置
import {createI18n} from 'vue-i18n'
import {localStorage} from '@/utils/storage'
// 本地語(yǔ)言包
import enLocale from './en'
import zhCnLocale from './zh-cn'
const messages = {
'zh-cn': {
...zhCnLocale
},
en: {
...enLocale
}
}
/**
* 獲取當(dāng)前系統(tǒng)使用語(yǔ)言字符串
*
* @returns zh-cn|en ...
*/
export const getLanguage = () => {
// 本地緩存獲取
let language = localStorage.get('language')
if (language) {
return language
}
// 瀏覽器使用語(yǔ)言
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 全局注冊(cè)
?// main.ts
// 國(guó)際化
import i18n from "@/lang/index";
app.use(i18n)
.mount('#app');
5. 靜態(tài)頁(yè)面國(guó)際化
$t 是 i18n 提供的根據(jù) key 從語(yǔ)言包翻譯對(duì)應(yīng)的 value 方法
<h3 class="title">{{ $t("login.title") }}</h3>
6. 動(dòng)態(tài)路由國(guó)際化
i18n 工具類,主要使用 i18n 的 te (判斷語(yǔ)言包是否存在key) 和 t (翻譯) 兩個(gè)方法
// src/utils/i18n.ts
import i18n from "@/lang/index";
export function generateTitle(title: any) {
// 判斷是否存在國(guó)際化配置,如果沒有原生返回
const hasKey = i18n.global.te('route.' + title)
if (hasKey) {
const translatedTitle = i18n.global.t('route.' + title)
return translatedTitle
}
return title
}
頁(yè)面使用
?// 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']);
// 編輯器實(shí)例,必須用 shallowRef
const editorRef = shallowRef()
const state = reactive({
toolbarConfig: {},
editorConfig: {
placeholder: '請(qǐng)輸入內(nèi)容...',
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 實(shí)例,重要!
}
function handleChange(editor: any) {
emit('update:modelValue', editor.getHtml())
}
// 組件銷毀時(shí),也及時(shí)銷毀編輯器
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 自適應(yīng)大小工具類
側(cè)邊欄、瀏覽器窗口大小切換都會(huì)觸發(fā)圖表的 resize() 方法來(lái)進(jìn)行自適應(yīng)
?
// 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
官方的示例文檔豐富和詳細(xì),且涵蓋了 JavaScript 和 TypeScript 版本,使用非常簡(jiǎn)單。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-536407.html
?
<!-- 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: '業(yè)績(jī)總覽(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: ['收入', '毛利潤(rùn)', '收入增長(zhǎng)率', '利潤(rùn)增長(zhǎng)率']
},
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: '毛利潤(rùn)',
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: '收入增長(zhǎng)率',
type: 'line',
yAxisIndex: 1,
data: [65, 67, 65, 53, 47, 45, 43, 42, 41],
itemStyle: {
color: '#67C23A'
}
},
{
name: '利潤(rùn)增長(zhǎng)率',
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>
?
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-536407.html
項(xiàng)目源碼
Gitee | Github | |
---|---|---|
vue3-element-admin | vue3-element-admin: 基于 vue-element-admin 升級(jí)的 Vue3 版本管理前端解決方案,技術(shù)棧: Vue3 + Vite2 + TypeScript + Element Plus + Pinia 。 | GitHub - youlaitech/vue3-element-admin: 基于 vue-element-admin 升級(jí)的 Vue3 版本管理前端解決方案,技術(shù)棧: Vue3 + Vite2 + TypeScript + Element Plus + Pinia 。 |
到了這里,關(guān)于基于 vue-element-admin 升級(jí)的 Vue 3 + TypeScript + Element-Plus 版本后臺(tái)管理系統(tǒng)正式開源的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!