vue3-element-admin 是基於 vue-element-admin 升級的 Vue3 + Element Plus 版本的後臺管理前端解決方案,技術棧爲 Vue3 + Vite4 + TypeScript + Element Plus + Pinia + Vue Router 等當前主流框架。
相較於其他管理前端框架,vue3-element-admin 的優勢在於一有一無 (有配套後端、無複雜封裝):
配套完整 Java 後端 權限管理接口,開箱即用,提供 OpenAPI 文檔 搭配 Apifox 生成 Node、Python、Go等其他服務端代碼;
完全基於 vue-element-admin 升級的 Vue3 版本,沒有對框架(Element Plus)的組件再封裝,上手成本低和擴展性高。
前言
本篇是 vue3-element-admin v2.x 版本從 0 到 1,相較於 v1.x 版本 主要增加了對原子CSS(UnoCSS)、按需自動導入、暗黑模式的支持。
閱讀前的兩條聲明:
-
博客有時效性,源代碼會一直更新,本篇源碼
tag
版本 vue3-element-admin v2.2.0 ; -
各章節會有先後順序依賴關係,例如:安裝 Element Plus 需要先安裝自動導入等,建議按照順序完成0到1,當然也可各取所需。
項目預覽
在線預覽
首頁控制檯
接口文檔
權限管理系統
擴展生態
youlai-mall 有來開源商城:Spring Cloud微服務+ vue3-element-admin+uni-app
youlai-mall 商品管理 | mall-app 移動端 |
---|---|
項目指南
功能清單
技術棧&官網
技術棧 | 描述 | 官網 |
---|---|---|
Vue3 | 漸進式 JavaScript 框架 | https://cn.vuejs.org/ |
Element Plus | 基於 Vue 3,面向設計師和開發者的組件庫 | https://element-plus.gitee.io/zh-CN/ |
Vite | 前端開發與構建工具 | https://cn.vitejs.dev/ |
TypeScript | 微軟新推出的一種語言,是 JavaScript 的超集 | https://www.tslang.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/ |
vue-i18n | Vue 國際化多語言插件 | https://vue-i18n.intlify.dev/ |
VueUse | 基於Vue組合式API的實用工具集(類比HuTool工具) | http://www.vueusejs.com/ |
前/後端源碼
Gitee | Github | |
---|---|---|
前端 | vue3-element-admin | vue3-element-admin |
後端 | youlai-boot | youlai-boot |
接口文檔
- 接口調用地址:https://vapi.youlai.tech
- 接口文檔地址:在線接口文檔
- OpenAPI 3.0 文檔地址:http://vapi.youlai.tech/v3/api-docs
環境準備
名稱 | 備註 | |
---|---|---|
開發工具 | VSCode 下載 | - |
運行環境 | Node 16+ 下載 | |
VSCode插件(必裝) | 插件市場搜索 Vue Language Features (Volar) 和 TypeScript Vue Plugin (Volar) 安裝,且禁用 Vetur |
項目初始化
按照 🍃Vite 官方文檔 - 搭建第一個 Vite 項目 說明,執行以下命令完成 vue
、typescirpt
模板項目的初始化
npm init vite@latest vue3-element-admin --template vue-ts
-
vue3-element-admin
: 自定義的項目名稱 -
vue-ts
:vue
+typescript
模板的標識,查看 create-vite 以獲取每個模板的更多細節:vue,vue-ts,react,react-ts
初始化完成項目位於 D:\project\demo\vue3-element-admin
, 使用 VSCode 導入,執行以下命令啓動:
npm install
npm run dev
瀏覽器訪問 localhost:5173 預覽
路徑別名配置
相對路徑別名配置,使用 @ 代替 src
Vite 配置
TypeScirpt 編譯器配置
// tsconfig.json
"compilerOptions": {
...
"baseUrl": "./", // 解析非相對模塊的基地址,默認是當前目錄
"paths": { // 路徑映射,相對於baseUrl
"@/*": ["src/*"]
}
}
路徑別名使用
// src/App.vue
import HelloWorld from '/src/components/HelloWorld.vue'
↓
import HelloWorld from '@/components/HelloWorld.vue'
安裝自動導入
Element Plus 官方文檔中推薦
按需自動導入
的方式,而此需要使用額外的插件unplugin-auto-import
和unplugin-vue-components
來導入要使用的組件。所以在整合Element Plus
之前先了解下自動導入
的概念和作用
概念
爲了避免在多個頁面重複引入 API
或 組件
,由此而產生的自動導入插件來節省重複代碼和提高開發效率。
插件 | 概念 | 自動導入對象 |
---|---|---|
unplugin-auto-import | 按需自動導入API | ref,reactive,watch,computed 等API |
unplugin-vue-components | 按需自動導入組件 | Element Plus 等三方庫和指定目錄下的自定義組件 |
看下自動導入插件未使用和使用的區別:
插件名 | 未使用自動導入 | 使用自動導入 |
---|---|---|
unplugin-auto-import | ||
unplugin-vue-components |
安裝插件依賴
npm install -D unplugin-auto-import unplugin-vue-components
vite.config.ts - 自動導入配置
新建 /src/types
目錄用於存放自動導入函數和組件的TS類型聲明文件
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
plugins: [
AutoImport({
// 自動導入 Vue 相關函數,如:ref, reactive, toRef 等
imports: ["vue"],
eslintrc: {
enabled: true, // 是否自動生成 eslint 規則,建議生成之後設置 false
filepath: "./.eslintrc-auto-import.json", // 指定自動導入函數 eslint 規則的文件
},
dts: path.resolve(pathSrc, "types", "auto-imports.d.ts"), // 指定自動導入函數TS類型聲明文件路徑
}),
Components({
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 指定自動導入組件TS類型聲明文件路徑
}),
]
.eslintrc.cjs - 自動導入函數 eslint 規則引入
"extends": [
"./.eslintrc-auto-import.json"
],
tsconfig.json - 自動導入TS類型聲明文件引入
{
"include": ["src/**/*.d.ts"]
}
自動導入效果
運行項目 npm run dev
自動
整合 Element Plus
需要完成上面一節的 自動導入 的安裝和配置
安裝 Element Plus
npm install element-plus
安裝自動導入 Icon 依賴
npm i -D unplugin-icons
vite.config.ts 配置
參考: element-plus-best-practices - vite.config.ts
// vite.config.ts
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolve
export default ({ mode }: ConfigEnv): UserConfig => {
return {
plugins: [
// ...
AutoImport({
// ...
resolvers: [
// 自動導入 Element Plus 相關函數,如:ElMessage, ElMessageBox... (帶樣式)
ElementPlusResolver(),
// 自動導入圖標組件
IconsResolver({}),
]
vueTemplate: true, // 是否在 vue 模板中自動導入
dts: path.resolve(pathSrc, 'types', 'auto-imports.d.ts') // 自動導入組件類型聲明文件位置,默認根目錄
}),
Components({
resolvers: [
// 自動導入 Element Plus 組件
ElementPlusResolver(),
// 自動註冊圖標組件
IconsResolver({
enabledCollections: ["ep"] // element-plus圖標庫,其他圖標庫 https://icon-sets.iconify.design/
}),
],
dts: path.resolve(pathSrc, "types", "components.d.ts"), // 自動導入組件類型聲明文件位置,默認根目錄
}),
Icons({
// 自動安裝圖標庫
autoInstall: true,
}),
],
};
};
示例代碼
<!-- src/components/HelloWorld.vue -->
<div>
<el-button type="success"><i-ep-SuccessFilled />Success</el-button>
<el-button type="info"><i-ep-InfoFilled />Info</el-button>
<el-button type="warning"><i-ep-WarningFilled />Warning</el-button>
<el-button type="danger"><i-ep-WarnTriangleFilled />Danger</el-button>
</div>
效果預覽
整合 SVG 圖標
通過 vite-plugin-svg-icons 插件整合
Iconfont
第三方圖標庫實現本地圖標
參考: vite-plugin-svg-icons 安裝文檔
安裝依賴
npm install -D [email protected]
npm install -D [email protected]
創建 src/assets/icons
目錄 , 放入從 Iconfont 複製的 svg
圖標
main.ts 引入註冊腳本
// src/main.ts
import 'virtual:svg-icons-register';
vite.config.ts 配置插件
// vite.config.ts
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default ({command, mode}: ConfigEnv): UserConfig => {
return (
{
plugins: [
createSvgIconsPlugin({
// 指定需要緩存的圖標文件夾
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[dir]-[name]',
})
]
}
)
}
SVG 組件封裝
<!-- src/components/SvgIcon/index.vue -->
<script setup lang="ts">
const props = defineProps({
prefix: {
type: String,
default: "icon",
},
iconClass: {
type: String,
required: false,
},
color: {
type: String,
},
size: {
type: String,
default: "1em",
},
});
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
</script>
<template>
<svg
aria-hidden="true"
class="svg-icon"
:style="'width:' + size + ';height:' + size"
>
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<style scoped>
.svg-icon {
display: inline-block;
outline: none;
width: 1em;
height: 1em;
vertical-align: -0.15em; /* 因icon大小被設置爲和字體大小一致,而span等標籤的下邊緣會和字體的基線對齊,故需設置一個往下的偏移比例,來糾正視覺上的未對齊效果 */
fill: currentColor; /* 定義元素的顏色,currentColor是一個變量,這個變量的值就表示當前元素的color值,如果當前元素未設置color值,則從父元素繼承 */
overflow: hidden;
}
</style>
組件使用
<!-- src/components/HelloWorld.vue -->
<template>
<el-button type="info"><svg-icon icon-class="block"/>SVG 本地圖標</el-button>
</template>
整合 SCSS
一款CSS預處理語言,SCSS 是 Sass 3 引入新的語法,其語法完全兼容 CSS3,並且繼承了 Sass 的強大功能。
安裝依賴
npm i -D sass
創建 variables.scss
變量文件,添加變量 $bg-color
定義,注意規範變量以 $
開頭
// src/styles/variables.scss
$bg-color:#242424;
Vite
配置導入 SCSS
全局變量文件
// vite.config.ts
css: {
// CSS 預處理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
style
標籤使用SCSS
全局變量
<!-- src/components/HelloWorld.vue -->
<template>
<div class="box" />
</template>
<style lang="scss" scoped>
.box {
width: 100px;
height: 100px;
background-color: $bg-color;
}
</style>
上面導入的 SCSS
全局變量在 TypeScript
不生效的,需要創建一個以 .module.scss
結尾的文件
// src/styles/variables.module.scss
// 導出 variables.scss 文件的變量
:export{
bgColor:$bg-color
}
TypeScript
使用 SCSS
全局變量
<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
import variables from "@/styles/variables.module.scss";
console.log(variables.bgColor)
</script>
<template>
<div style="width:100px;height:100px" :style="{ 'background-color': variables.bgColor }" />
</template>
整合 UnoCSS
UnoCSS 是一個具有高性能且極具靈活性的即時原子化 CSS 引擎 。
安裝依賴
npm install -D unocss
vite.config.ts 配置
// vite.config.ts
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS({ /* options */ }),
],
}
main.ts
引入 uno.css
// src/main.ts
import 'uno.css'
VSCode
安裝 UnoCSS
插件
再看下具體使用方式和實際效果:
代碼 | 效果 |
---|---|
如果UnoCSS
插件智能提示不生效,請參考:VSCode插件UnoCSS智能提示不生效解決 。
整合 Pinia
Pinia 是 Vue 的專屬狀態管理庫,它允許你跨組件或頁面共享狀態。
參考:Pinia 官方文檔
安裝依賴
npm install pinia
main.ts
引入 pinia
// src/main.ts
import { createPinia } from "pinia";
import App from "./App.vue";
createApp(App).use(createPinia()).mount("#app");
定義 Store
根據 Pinia 官方文檔-核心概念 描述 ,Store 定義分爲選項式
和組合式
, 先比較下兩種寫法的區別:
選項式 Option Store | 組合式 Setup Store |
---|---|
至於如何選擇,官方給出的建議 :選擇你覺得最舒服的那一個就好
。
這裏選擇組合式,新建文件 src/store/counter.ts
// src/store/counter.ts
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", () => {
// ref變量 → state 屬性
const count = ref(0);
// computed計算屬性 → getters
const double = computed(() => {
return count.value * 2;
});
// function函數 → actions
function increment() {
count.value++;
}
return { count, double, increment };
});
父組件
<!-- src/App.vue -->
<script setup lang="ts">
import HelloWorld from "@/components/HelloWorld.vue";
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>
<template>
<h1 class="text-3xl">vue3-element-admin-父組件</h1>
<el-button type="primary" @click="counterStore.increment">count++</el-button>
<HelloWorld />
</template>
子組件
<!-- src/components/HelloWorld.vue -->
<script setup lang="ts">
import { useCounterStore } from "@/store/counter";
const counterStore = useCounterStore();
</script>
<template>
<el-card class="text-left text-white border-white border-1 border-solid mt-10 bg-[#242424]" >
<template #header> 子組件 HelloWorld.vue</template>
<el-form>
<el-form-item label="數字:"> {{ counterStore.count }}</el-form-item>
<el-form-item label="加倍:"> {{ counterStore.double }}</el-form-item>
</el-form>
</el-card>
</template>
效果預覽
環境變量
Vite 環境變量主要是爲了區分開發、測試、生產等環境的變量
參考: Vite 環境變量配置官方文檔
env配置文件
項目根目錄新建 .env.development
、.env.production
-
開發環境變量配置:.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'
環境變量智能提示
新建 src/types/env.d.ts
文件存放環境變量TS類型聲明
// src/types/env.d.ts
interface ImportMetaEnv {
/**
* 應用標題
*/
VITE_APP_TITLE: string;
/**
* 應用端口
*/
VITE_APP_PORT: number;
/**
* API基礎路徑(反向代理)
*/
VITE_APP_BASE_API: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
使用自定義環境變量就會有智能提示,環境變量的讀取和使用請看下一節的跨域處理中的 vite.config.ts
的配置。
跨域處理
跨域原理
瀏覽器同源策略: 協議、域名和端口都相同是同源,瀏覽器會限制非同源請求讀取響應結果。
本地開發環境通過 Vite
配置反向代理解決瀏覽器跨域問題,生產環境則是通過 nginx
配置反向代理 。
vite.config.ts
配置代理
表面肉眼看到的請求地址: http://localhost:3000/dev-api/api/v1/users/me
真實訪問的代理目標地址: http://vapi.youlai.tech/api/v1/users/me
整合 Axios
Axios 基於promise可以用於瀏覽器和node.js的網絡請求庫
參考: Axios 官方文檔
安裝依賴
npm install axios
Axios 工具類封裝
// src/utils/request.ts
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';
// 創建 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: InternalAxiosRequestConfig) => {
const userStore = useUserStoreHook();
if (userStore.token) {
config.headers.Authorization = userStore.token;
}
return config;
},
(error: any) => {
return Promise.reject(error);
}
);
// 響應攔截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data;
// 登錄成功
if (code === '00000') {
return response.data;
}
ElMessage.error(msg || '系統出錯');
return Promise.reject(new Error(msg || 'Error'));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data;
// token 過期,跳轉登錄頁
if (code === 'A0230') {
ElMessageBox.confirm('當前頁面已失效,請重新登錄', '提示', {
confirmButtonText: '確定',
type: 'warning'
}).then(() => {
localStorage.clear(); // @vueuse/core 自動導入
window.location.href = '/';
});
}else{
ElMessage.error(msg || '系統出錯');
}
}
return Promise.reject(error.message);
}
);
// 導出 axios 實例
export default service;
登錄接口實戰
訪問 vue3-element-admin 在線接口文檔, 查看登錄接口請求參數和響應數據類型
點擊 生成代碼 獲取登錄響應數據 TypeScript
類型定義
將類型定義複製到 src/api/auth/types.ts
文件中
/**
* 登錄請求參數
*/
export interface LoginData {
/**
* 用戶名
*/
username: string;
/**
* 密碼
*/
password: string;
}
/**
* 登錄響應
*/
export interface LoginResult {
/**
* 訪問token
*/
accessToken?: string;
/**
* 過期時間(單位:毫秒)
*/
expires?: number;
/**
* 刷新token
*/
refreshToken?: string;
/**
* token 類型
*/
tokenType?: string;
}
登錄 API 定義
// src/api/auth/index.ts
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { LoginData, LoginResult } from './types';
/**
* 登錄API
*
* @param data {LoginData}
* @returns
*/
export function loginApi(data: LoginData): AxiosPromise<LoginResult> {
return request({
url: '/api/v1/auth/login',
method: 'post',
params: data
});
}
登錄 API 調用
// src/store/modules/user.ts
import { loginApi } from '@/api/auth';
import { LoginData } from '@/api/auth/types';
/**
* 登錄調用
*
* @param {LoginData}
* @returns
*/
function login(loginData: LoginData) {
return new Promise<void>((resolve, reject) => {
loginApi(loginData)
.then(response => {
const { tokenType, accessToken } = response.data;
token.value = tokenType + ' ' + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
resolve();
})
.catch(error => {
reject(error);
});
});
}
動態路由
安裝 vue-router
npm install vue-router@next
路由實例
創建路由實例,順帶初始化靜態路由,而動態路由需要用戶登錄,根據用戶擁有的角色進行權限校驗後進行初始化
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
export const Layout = () => import('@/layout/index.vue');
// 靜態路由
export const constantRoutes: 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: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: { title: 'dashboard', icon: 'homepage', affix: true }
}
]
}
];
/**
* 創建路由
*/
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes as RouteRecordRaw[],
// 刷新時,滾動條位置還原
scrollBehavior: () => ({ left: 0, top: 0 })
});
/**
* 重置路由
*/
export function resetRouter() {
router.replace({ path: '/login' });
location.reload();
}
export default router;
全局註冊路由實例
// main.ts
import router from "@/router";
app.use(router).mount('#app')
動態權限路由
路由守衛 src/permission.ts
,獲取當前登錄用戶的角色信息進行動態路由的初始化
最終調用 permissionStore.generateRoutes(roles)
方法生成動態路由
// src/store/modules/permission.ts
import { listRoutes } from '@/api/menu';
export const usePermissionStore = defineStore('permission', () => {
const routes = ref<RouteRecordRaw[]>([]);
function setRoutes(newRoutes: RouteRecordRaw[]) {
routes.value = constantRoutes.concat(newRoutes);
}
/**
* 生成動態路由
*
* @param roles 用戶角色集合
* @returns
*/
function generateRoutes(roles: string[]) {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
// 接口獲取所有路由
listRoutes()
.then(({ data: asyncRoutes }) => {
// 根據角色獲取有訪問權限的路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
setRoutes(accessedRoutes);
resolve(accessedRoutes);
})
.catch(error => {
reject(error);
});
});
}
// 導出 store 的動態路由數據 routes
return { routes, setRoutes, generateRoutes };
});
接口獲取得到的路由數據
根據路由數據 (routes)生成菜單的關鍵代碼
src/layout/componets/Sidebar/index.vue | src/layout/componets/Sidebar/SidebarItem.vue |
---|---|
按鈕權限
除了 Vue 內置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 還允許你註冊自定義的指令 (Custom Directives),以下就通過自定義指令的方式實現按鈕權限控制。
**自定義指令 **
// src/directive/permission/index.ts
import { useUserStoreHook } from '@/store/modules/user';
import { Directive, DirectiveBinding } from 'vue';
/**
* 按鈕權限
*/
export const hasPerm: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 「超級管理員」擁有所有的按鈕權限
const { roles, perms } = useUserStoreHook();
if (roles.includes('ROOT')) {
return true;
}
// 「其他角色」按鈕權限校驗
const { value } = binding;
if (value) {
const requiredPerms = value; // DOM綁定需要的按鈕權限標識
const hasPerm = 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']\""
);
}
}
};
全局註冊自定義指令
// src/directive/index.ts
import type { App } from 'vue';
import { hasPerm } from './permission';
// 全局註冊 directive 方法
export function setupDirective(app: App<Element>) {
// 使 v-hasPerm 在所有組件中都可用
app.directive('hasPerm', hasPerm);
}
// src/main.ts
import { setupDirective } from '@/directive';
const app = createApp(App);
// 全局註冊 自定義指令(directive)
setupDirective(app);
組件使用自定義指令
// 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 框架國際化(官方提供了國際化方式)和自定義國際化(通過 vue-i18n 國際化插件)
Element Plus 國際化
簡單的使用方式請參考 Element Plus 官方文檔-國際化示例,以下介紹 vue3-element-admin
整合 pinia
實現國際化語言切換。
Element Plus 提供了一個 Vue 組件 ConfigProvider 用於全局配置國際化的設置。
<!-- src/App.vue -->
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
</script>
<template>
<el-config-provider :locale="appStore.locale" >
<router-view />
</el-config-provider>
</template>
定義 store
// src/store/modules/app.ts
import { defineStore } from 'pinia';
import { useStorage } from '@vueuse/core';
import defaultSettings from '@/settings';
// 導入 Element Plus 中英文語言包
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
// setup
export const useAppStore = defineStore('app', () => {
const language = useStorage('language', defaultSettings.language);
/**
* 根據語言標識讀取對應的語言包
*/
const locale = computed(() => {
if (language?.value == 'en') {
return en;
} else {
return zhCn;
}
});
/**
* 切換語言
*/
function changeLanguage(val: string) {
language.value = val;
}
return {
language,
locale,
changeLanguage
};
});
切換語言組件調用
<!-- src/components/LangSelect/index.vue -->
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import SvgIcon from '@/components/SvgIcon/index.vue';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
const { locale } = useI18n();
function handleLanguageChange(lang: string) {
locale.value = lang;
appStore.changeLanguage(lang);
if (lang == 'en') {
ElMessage.success('Switch Language Successful!');
} else {
ElMessage.success('切換語言成功!');
}
}
</script>
<template>
<el-dropdown trigger="click" @command="handleLanguageChange">
<div>
<svg-icon icon-class="language" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:disabled="appStore.language === 'zh-cn'"
command="zh-cn"
>
中文
</el-dropdown-item>
<el-dropdown-item :disabled="appStore.language === 'en'" command="en">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
從 Element Plus
分頁組件看下國際化的效果
vue-i18n 自定義國際化
i18n 英文全拼 internationalization ,國際化的意思,英文 i 和 n 中間18個英文字母
參考:vue-i18n 官方文檔 - installation
安裝 vue-i18n
npm install vue-i18n@9
自定義語言包
創建 src/lang
/package 語言包目錄,存放自定義的語言文件
中文語言包 zh-cn.ts | 英文語言包 en.ts |
---|---|
創建 i18n
實例
// src/lang/index.ts
import { createI18n } from 'vue-i18n';
import { useAppStore } from '@/store/modules/app';
const appStore = useAppStore();
// 本地語言包
import enLocale from './package/en';
import zhCnLocale from './package/zh-cn';
const messages = {
'zh-cn': {
...zhCnLocale
},
en: {
...enLocale
}
};
// 創建 i18n 實例
const i18n = createI18n({
legacy: false,
locale: appStore.language,
messages: messages
});
// 導出 i18n 實例
export default i18n;
i18n 全局註冊
// main.ts
// 國際化
import i18n from '@/lang/index';
app.use(i18n).mount('#app');
登錄頁面國際化使用
$t 是 i18n 提供的根據 key 從語言包翻譯對應的 value 方法
<span>{{ $t("login.title") }}</span>
在登錄頁面 src/view/login/index.vue
查看如何使用
效果預覽
暗黑模式
Element Plus 2.2.0 版本開始支持暗黑模式,啓用方式參考 Element Plus 官方文檔 - 暗黑模式, 官方也提供了示例 element-plus-vite-starter 模版 。
這裏根據官方文檔和示例講述 vue3-element-admin 是如何使用 VueUse 的 useDark 方法實現暗黑模式的動態切換。
導入 Element Plus 暗黑模式變量
// src/main.ts
import 'element-plus/theme-chalk/dark/css-vars.css'
切換暗黑模式設置
<!-- src/layout/components/Settings/index.vue -->
<script setup lang="ts">
import IconEpSunny from '~icons/ep/sunny';
import IconEpMoon from '~icons/ep/moon';
/**
* 暗黑模式
*/
const settingsStore = useSettingsStore();
const isDark = useDark();
const toggleDark = () => useToggle(isDark);
</script>
<template>
<div class="settings-container">
<h3 class="text-base font-bold">項目配置</h3>
<el-divider>主題</el-divider>
<div class="flex justify-center" @click.stop>
<el-switch
v-model="isDark"
@change="toggleDark"
inline-prompt
:active-icon="IconEpMoon"
:inactive-icon="IconEpSunny"
active-color="var(--el-fill-color-dark)"
inactive-color="var(--el-color-primary)"
/>
</div>
</div>
</template>
自定義變量
除了 Element Plus 組件樣式之外,應用中還有很多自定義的組件和樣式,像這樣的:
應對自定義組件樣式實現暗黑模式步驟如下:
新建 src/styles/dark.scss
html.dark {
/* 修改自定義元素的樣式 */
.navbar {
background-color: #141414;
}
}
在 Element Plus 的樣式之後導入它
// main.ts
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/styles/dark.scss';
效果預覽
組件封裝
wangEditor 富文本
參考: wangEditor 官方文檔
安裝 wangEditor
npm install @wangeditor/editor @wangeditor/editor-for-vue@next
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 { Editor, Toolbar } from "@wangeditor/editor-for-vue";
// API 引用
import { uploadFileApi } from "@/api/file";
const props = defineProps({
modelValue: {
type: [String],
default: "",
},
});
const emit = defineEmits(["update:modelValue"]);
const defaultHtml = useVModel(props, "modelValue", emit);
const editorRef = shallowRef(); // 編輯器實例,必須用 shallowRef
const mode = ref("default"); // 編輯器模式
const toolbarConfig = ref({}); // 工具條配置
// 編輯器配置
const editorConfig = ref({
placeholder: "請輸入內容...",
MENU_CONF: {
uploadImage: {
// 自定義圖片上傳
async customUpload(file: any, insertFn: any) {
uploadFileApi(file).then((response) => {
const url = response.data.url;
insertFn(url);
});
},
},
},
});
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>
使用案例
<!-- wangEditor富文本編輯器示例 -->
<script setup lang="ts">
import Editor from '@/components/WangEditor/index.vue';
const value = ref('初始內容');
</script>
<template>
<div class="app-container">
<editor v-model="value" style="height: 600px" />
</div>
</template>
效果預覽
Echarts 圖表
安裝 Echarts
npm install echarts
組件封裝
<!-- src/views/dashboard/components/Chart/BarChart.vue -->
<template>
<el-card>
<template #header> 線 + 柱混合圖 </template>
<div :id="id" :class="className" :style="{ height, width }" />
</el-card>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
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 options = {
grid: {
left: '2%',
right: '2%',
bottom: '10%',
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
}
},
legend: {
x: 'center',
y: 'bottom',
data: ['收入', '毛利潤', '收入增長率', '利潤增長率'],
textStyle: {
color: '#999'
}
},
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: [7000, 7100, 7200, 7300, 7400],
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: [8000, 8200, 8400, 8600, 8800],
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: [60, 65, 70, 75, 80],
itemStyle: {
color: '#67C23A'
}
},
{
name: '利潤增長率',
type: 'line',
yAxisIndex: 1,
data: [70, 75, 80, 85, 90],
itemStyle: {
color: '#409EFF'
}
}
]
};
onMounted(() => {
// 圖表初始化
const chart = echarts.init(
document.getElementById(props.id) as HTMLDivElement
);
chart.setOption(options);
// 大小自適應
window.addEventListener('resize', () => {
chart.resize();
});
});
</script>
組件使用
<script setup lang="ts">
import BarChart from './components/BarChart.vue';
</script>
<template>
<BarChart id="barChart" height="400px"width="300px" />
</template>
效果預覽
圖標選擇器
組件封裝
<!-- src/components/IconSelect/index.vue -->
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: String,
require: false
}
});
const emit = defineEmits(['update:modelValue']);
const inputValue = toRef(props, 'modelValue');
const visible = ref(false); // 彈窗顯示狀態
const iconNames: string[] = []; // 所有的圖標名稱集合
const filterValue = ref(''); // 篩選的值
const filterIconNames = ref<string[]>([]); // 過濾後的圖標名稱集合
const iconSelectorRef = ref(null);
/**
* 加載 ICON
*/
function loadIcons() {
const icons = import.meta.glob('../../assets/icons/*.svg');
for (const icon in icons) {
const iconName = icon.split('assets/icons/')[1].split('.svg')[0];
iconNames.push(iconName);
}
filterIconNames.value = iconNames;
}
/**
* 篩選圖標
*/
function handleFilter() {
if (filterValue.value) {
filterIconNames.value = iconNames.filter(iconName =>
iconName.includes(filterValue.value)
);
} else {
filterIconNames.value = iconNames;
}
}
/**
* 選擇圖標
*/
function handleSelect(iconName: string) {
emit('update:modelValue', iconName);
visible.value = false;
}
/**
* 點擊容器外的區域關閉彈窗 VueUse onClickOutside
*/
onClickOutside(iconSelectorRef, () => (visible.value = false));
onMounted(() => {
loadIcons();
});
</script>
<template>
<div class="iconselect-container" ref="iconSelectorRef">
<el-input
v-model="inputValue"
readonly
@click="visible = !visible"
placeholder="點擊選擇圖標"
>
<template #prepend>
<svg-icon :icon-class="inputValue" />
</template>
</el-input>
<el-popover
shadow="none"
:visible="visible"
placement="bottom-end"
trigger="click"
width="400"
>
<template #reference>
<div
@click="visible = !visible"
class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]"
>
<i-ep-caret-top v-show="visible"></i-ep-caret-top>
<i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
</div>
</template>
<!-- 下拉選擇彈窗 -->
<el-input
class="p-2"
v-model="filterValue"
placeholder="搜索圖標"
clearable
@input="handleFilter"
/>
<el-divider border-style="dashed" />
<el-scrollbar height="300px">
<ul class="icon-list">
<li
class="icon-item"
v-for="(iconName, index) in filterIconNames"
:key="index"
@click="handleSelect(iconName)"
>
<el-tooltip :content="iconName" placement="bottom" effect="light">
<svg-icon
color="var(--el-text-color-regular)"
:icon-class="iconName"
/>
</el-tooltip>
</li>
</ul>
</el-scrollbar>
</el-popover>
</div>
</template>
組件使用
<!-- src/views/demo/IconSelect.vue -->
<script setup lang="ts">
const iconName = ref('edit');
</script>
<template>
<div class="app-container">
<icon-select v-model="iconName" />
</div>
</template>
效果預覽
規範配置
代碼統一規範
【vue3-element-admin】ESLint+Prettier+Stylelint+EditorConfig 約束和統一前端代碼規範
- Eslint: JavaScript 語法規則和代碼風格檢查;
- Stylelint: CSS 統一規範和代碼檢測;
- Prettier:全局代碼格式化。
Git 提交規範
【vue3-element-admin】Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交規範
- Husky + Lint-staged 整合實現 Git 提交前代碼規範檢測/格式化;
- Husky + Commitlint + Commitizen + cz-git 整合實現生成規範化且高度自定義的 Git commit message。
啓動部署
項目啓動
# 安裝 pnpm
npm install pnpm -g
# 安裝依賴
pnpm install
# 項目運行
pnpm run dev
項目部署
# 項目打包
pnpm run build:prod
生成的靜態文件在工程根目錄 dist 文件夾
FAQ
1: defineProps is not defined
-
問題描述
'defineProps' is not defined.eslint no-undef
-
解決方案
根據 Eslint 官方解決方案描述,解析器使用
vue-eslint-parser
v9.0.0 + 版本安裝
vue-eslint-parser
解析器npm install -D vue-eslint-parser
.eslintrc.js
關鍵配置(v9.0.0
及以上版本無需配置編譯宏vue/setup-compiler-macros
)如下 :parser: 'vue-eslint-parser', extends: [ 'eslint:recommended', // ... ],
重啓
VSCode
已無報錯提示
2: Vite 首屏加載慢(白屏久)
-
問題描述
Vite 項目啓動很快,但首次打開界面加載慢?
參考文章:爲什麼有人說 vite 快,有人卻說 vite 慢
vite 啓動時,並不像 webpack 那樣做一個全量的打包構建,所以啓動速度非常快。啓動以後,瀏覽器發起請求時,
Dev Server
要把請求需要的資源發送給瀏覽器,中間需要經歷預構建、對請求文件做路徑解析、加載源文件、對源文件做轉換,然後才能把內容返回給瀏覽器,這個時間耗時蠻久的,導致白屏時間較長。 -
解決方案1
//vite.config.ts optimizeDeps: { include: [ 'vue', 'vue-router', 'pinia', 'axios', 'element-plus/es/components/form/style/css', 'element-plus/es/components/form-item/style/css' ] }
-
解決方案2
參考文章:服務冷啓動性能提升
vite-plugin-optimize-persist
通過持久化方式記錄Dev Server
運行時掃描到的依賴,從而讓首次構建便可感知到,避免二次預構建的發生。⚠ vite 2.9.x 有效,vite 4.x 驗證無效npm i -D vite-plugin-optimize-persist vite-plugin-package-config
// vite.config.ts import OptimizationPersist from 'vite-plugin-optimize-persist' import PkgConfig from 'vite-plugin-package-config' export default { plugins: [ PkgConfig(), OptimizationPersist() ] }
-
解決方案3
-
解決方案4
放棄。你沒看錯,因爲只有首次加載界面慢,忍一時風平浪靜,Vite 的優勢在於快速的冷啓動、即時熱更新和按需編譯,瑕不掩瑜。
關於我們
如果交流羣二維碼過期,請添加我的微信備註
前端
、全棧
拉你進羣
微信交流羣 | 我的微信 | 微信公衆號 |
---|---|---|