๐จ Frontend Knowledge Base โ
Gin-Vue-Admin frontend is built on Vue 3 + Vite 4 + Element Plus, using modern frontend development technology stack, providing efficient development experience and excellent user interface.
๐ Technology Stack โ
Core Frameworks โ
- Vue 3 - Progressive JavaScript framework
- Vite 4 - Next-generation frontend build tool
- Element Plus - Component library based on Vue 3
State Management โ
- Pinia - Official Vue 3 recommended state management library
- Vue Router 4 - Official Vue.js router manager
Development Tools โ
- TypeScript - Superset of JavaScript (optional)
- ESLint - Code quality checking tool
- Prettier - Code formatting tool
- Sass/SCSS - CSS preprocessor
Build Optimization โ
- Vite Plugin - Rich plugin ecosystem
- Tree Shaking - Automatically remove unused code
- Code Splitting - Code splitting optimization
- Hot Module Replacement - Hot module replacement
๐ Frontend Directory Structure โ
web
โโโ babel.config.js
โโโ Dockerfile
โโโ favicon.ico
โโโ index.html -- Main page
โโโ limit.js -- Assistant code
โโโ package.json -- Package manager code
โโโ src -- Source code
โ โโโ api -- API group
โ โโโ App.vue -- Main page
โ โโโ assets -- Static resources
โ โโโ components -- Global components
โ โโโ core -- GVA component package
โ โ โโโ config.js -- GVA website configuration file
โ โ โโโ gin-vue-admin.js -- Registration welcome file
โ โ โโโ global.js -- Unified import file
โ โโโ directive -- v-auth registration file
โ โโโ main.js -- Main file
โ โโโ permission.js -- Route middleware
โ โโโ pinia -- Pinia state manager, replacing vuex
โ โ โโโ index.js -- Entry file
โ โ โโโ modules -- Modules
โ โ โโโ dictionary.js
โ โ โโโ router.js
โ โ โโโ user.js
โ โโโ router -- Route declaration file
โ โ โโโ index.js
โ โโโ style -- Global styles
โ โ โโโ base.scss
โ โ โโโ basics.scss
โ โ โโโ element_visiable.scss -- Can globally override element-plus styles here
โ โ โโโ iconfont.css -- Style file for top icons
โ โ โโโ main.scss
โ โ โโโ mobile.scss
โ โ โโโ newLogin.scss
โ โโโ utils -- Method package library
โ โ โโโ asyncRouter.js -- Dynamic routing related
โ โ โโโ btnAuth.js -- Dynamic permission button related
โ โ โโโ bus.js -- Global mitt declaration file
โ โ โโโ date.js -- Date related
โ โ โโโ dictionary.js -- Dictionary retrieval method
โ โ โโโ downloadImg.js -- Image download method
โ โ โโโ format.js -- Format organization related
โ โ โโโ image.js -- Image related methods
โ โ โโโ page.js -- Set page title
โ โ โโโ request.js -- Unified request file
โ โ โโโ stringFun.js -- String file
| โโโ view -- Main view code
| | โโโ about -- About us
| | โโโ dashboard -- Dashboard
| | โโโ error -- Error
| | โโโ example -- Upload example
| | โโโ iconList -- Icon list
| | โโโ init -- Initialize data
| | โโโ layout -- Layout constraint page
| | | โโโ aside -- Sidebar
| | | โโโ bottomInfo -- Bottom info
| | | โโโ screenfull -- Full screen settings
| | | โโโ setting -- System settings
| | | โโโ index.vue -- Base constraint
| | โโโ login -- Login
| | โโโ person -- Personal center
| | โโโ superAdmin -- Super administrator operations
| | โโโ system -- System detection page
| | โโโ systemTools -- System configuration related pages
| | โโโ routerHolder.vue -- Page entry page
โโโ vite.config.js -- Vite configuration file
โโโ yarn.lock๐ ๏ธ Development Environment Configuration โ
Environment Requirements โ
- Node.js >= 16.0.0
- npm >= 8.0.0 or yarn >= 1.22.0
- Git Version control tool
Install Dependencies โ
bash
# Enter the frontend directory
cd web
# Install using npm
npm install
# Or install using yarn
yarn installDevelopment Commands โ
bash
# Start the development server
npm run serve
# Or
yarn serve
# Build the production version
npm run build
# Or
yarn build
# Code linting
npm run lint
# Or
yarn lint
# Code formatting
npm run format
# Or
yarn format๐ฏ Core Configuration Files โ
Vite Configuration (vite.config.js) โ
javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
iconDirs: [resolve(process.cwd(), 'src/assets/icons')],
symbolId: 'icon-[dir]-[name]'
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://127.0.0.1:8888',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
rollupOptions: {
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
}
}
})Project Configuration (src/core/config.js) โ
javascript
// Global system configuration
export const config = {
appName: 'Gin-Vue-Admin',
appLogo: 'logoIco.png',
showProgressBar: true,
progressBarColor: '#409EFF',
showInfoTip: true,
// Layout configuration
layout: {
showTagsView: true,
showSidebarLogo: true,
fixedHeader: true,
sidebarTextTheme: true,
showGreyMode: false,
showColorWeakness: false
},
// Theme configuration
theme: {
primaryColor: '#409EFF',
successColor: '#67C23A',
warningColor: '#E6A23C',
dangerColor: '#F56C6C',
infoColor: '#909399'
}
}๐๏ธ Core Architecture โ
1. Routing System โ
Static Route Configuration โ
javascript
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { asyncRouterHandle } from '@/utils/asyncRouter'
// ้ๆ่ทฏ็ฑ
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/view/login/index.vue')
},
{
path: '/',
redirect: '/dashboard'
},
{
path: '/layout',
name: 'Layout',
component: () => import('@/view/layout/index.vue'),
children: [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/view/dashboard/index.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default routerDynamic Route Handling โ
javascript
// src/utils/asyncRouter.js
import { asyncRoutes } from '@/router/asyncRouter'
// ๅจๆ่ทฏ็ฑๅค็
export function asyncRouterHandle(asyncRouter) {
asyncRouter.forEach(item => {
if (item.component) {
if (item.component === 'view/routerHolder.vue') {
item.component = () => import('@/view/routerHolder.vue')
} else {
const component = item.component
item.component = () => import(`@/view/${component}`)
}
}
if (item.children) {
asyncRouterHandle(item.children)
}
})
return asyncRouter
}
// ๆ ผๅผๅ่ทฏ็ฑ
export function formatRouter(routes, routeMap) {
const newRoutes = []
routes.forEach(item => {
if (item.path === 'dashboard') {
item.component = () => import('@/view/dashboard/index.vue')
} else if (item.component) {
item.component = routeMap[item.component] || (() => import(`@/view/error/404.vue`))
}
if (item.children && item.children.length > 0) {
item.children = formatRouter(item.children, routeMap)
}
newRoutes.push(item)
})
return newRoutes
}2. State Management (Pinia) โ
User State Management โ
javascript
// src/pinia/modules/user.js
import { defineStore } from 'pinia'
import { login, getUserInfo, logout } from '@/api/user'
import { jsonInBlacklist } from '@/api/jwt'
import router from '@/router/index'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: {
uuid: '',
nickName: '',
headerImg: '',
authority: {},
sideMode: 'dark',
activeColor: '#1890ff',
baseColor: '#fff'
},
token: '',
mode: 'light'
}),
getters: {
// ่ทๅ็จๆทไฟกๆฏ
getUserInfo: (state) => state.userInfo,
// ่ทๅtoken
getToken: (state) => state.token,
// ่ทๅๆจกๅผ
getMode: (state) => state.mode
},
actions: {
// ็ปๅฝ
async LoginIn(loginInfo) {
try {
const res = await login(loginInfo)
if (res.code === 0) {
this.setUserInfo(res.data.user)
this.setToken(res.data.token)
await this.GetUserInfo()
return true
}
} catch (error) {
console.error('็ปๅฝๅคฑ่ดฅ:', error)
return false
}
},
// ่ทๅ็จๆทไฟกๆฏ
async GetUserInfo() {
try {
const res = await getUserInfo()
if (res.code === 0) {
this.setUserInfo(res.data.userInfo)
}
return res
} catch (error) {
console.error('่ทๅ็จๆทไฟกๆฏๅคฑ่ดฅ:', error)
}
},
// ็ปๅบ
async LoginOut() {
try {
const res = await logout()
if (res.code === 0) {
this.userInfo = {}
this.token = ''
localStorage.clear()
router.push({ name: 'Login' })
}
} catch (error) {
console.error('็ปๅบๅคฑ่ดฅ:', error)
}
},
// ่ฎพ็ฝฎ็จๆทไฟกๆฏ
setUserInfo(userInfo) {
this.userInfo = { ...this.userInfo, ...userInfo }
},
// ่ฎพ็ฝฎtoken
setToken(token) {
this.token = token
localStorage.setItem('token', token)
},
// ่ฎพ็ฝฎๆจกๅผ
setMode(mode) {
this.mode = mode
localStorage.setItem('mode', mode)
}
}
})Router State Management โ
javascript
// src/pinia/modules/router.js
import { defineStore } from 'pinia'
import { asyncRouterHandle } from '@/utils/asyncRouter'
import { getMenu } from '@/api/menu'
export const useRouterStore = defineStore('router', {
state: () => ({
asyncRouters: [],
keepAliveRouters: [],
routerList: [],
addRouters: [],
routerMap: {}
}),
actions: {
// ่ฎพ็ฝฎๅจๆ่ทฏ็ฑ
async SetAsyncRouter() {
try {
const res = await getMenu()
if (res.code === 0) {
const asyncRouter = res.data.menus || []
this.asyncRouters = asyncRouterHandle(asyncRouter)
this.routerList = res.data.menus || []
return this.asyncRouters
}
} catch (error) {
console.error('่ทๅ่ๅๅคฑ่ดฅ:', error)
}
},
// ่ฎพ็ฝฎkeep-alive่ทฏ็ฑ
setKeepAliveRouters(history) {
this.keepAliveRouters = history
}
}
})3. HTTP Request Encapsulation โ
javascript
// src/utils/request.js
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
import router from '@/router/index'
// ๅๅปบaxiosๅฎไพ
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 99999
})
// ่ฏทๆฑๆฆๆชๅจ
service.interceptors.request.use(
config => {
const userStore = useUserStore()
// ๆทปๅ token
if (userStore.token) {
config.headers['x-token'] = userStore.token
}
// ๆทปๅ ่ฏทๆฑๆถ้ดๆณ
config.headers['x-timestamp'] = Date.now()
return config
},
error => {
console.error('่ฏทๆฑ้่ฏฏ:', error)
return Promise.reject(error)
}
)
// ๅๅบๆฆๆชๅจ
service.interceptors.response.use(
response => {
const res = response.data
// ๅค็ๆไปถไธ่ฝฝ
if (response.headers['content-type'] === 'application/octet-stream') {
return response
}
// ไธๅก้่ฏฏๅค็
if (res.code !== 0) {
ElMessage({
message: res.msg || '่ฏทๆฑๅคฑ่ดฅ',
type: 'error',
duration: 5 * 1000
})
// token่ฟๆๅค็
if (res.code === 1004 || res.code === 1005) {
const userStore = useUserStore()
userStore.LoginOut()
return Promise.reject(new Error(res.msg || '็ปๅฝ่ฟๆ'))
}
return Promise.reject(new Error(res.msg || '่ฏทๆฑๅคฑ่ดฅ'))
}
return res
},
error => {
console.error('ๅๅบ้่ฏฏ:', error)
let message = '็ฝ็ป้่ฏฏ'
if (error.response) {
switch (error.response.status) {
case 401:
message = 'ๆชๆๆ๏ผ่ฏท้ๆฐ็ปๅฝ'
break
case 403:
message = 'ๆ้ไธ่ถณ'
break
case 404:
message = '่ฏทๆฑ็่ตๆบไธๅญๅจ'
break
case 500:
message = 'ๆๅกๅจๅ
้จ้่ฏฏ'
break
default:
message = `่ฟๆฅ้่ฏฏ${error.response.status}`
}
}
ElMessage({
message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service๐จ Component Development โ
Global Component Registration โ
javascript
// src/core/global.js
import GvaIcon from '@/components/gva-icon/index.vue'
import GvaTable from '@/components/gva-table/index.vue'
import GvaForm from '@/components/gva-form/index.vue'
import GvaUpload from '@/components/gva-upload/index.vue'
// ๅ
จๅฑ็ปไปถๅ่กจ
const components = {
GvaIcon,
GvaTable,
GvaForm,
GvaUpload
}
// ๆณจๅๅ
จๅฑ็ปไปถ
export function setupGlobalComponents(app) {
Object.keys(components).forEach(key => {
app.component(key, components[key])
})
}Custom Component Example โ
vue
<!-- src/components/gva-table/index.vue -->
<template>
<div class="gva-table">
<el-table
ref="tableRef"
v-loading="loading"
:data="tableData"
:height="height"
:max-height="maxHeight"
:stripe="stripe"
:border="border"
:size="size"
:fit="fit"
:show-header="showHeader"
:highlight-current-row="highlightCurrentRow"
:current-row-key="currentRowKey"
:row-class-name="rowClassName"
:row-style="rowStyle"
:cell-class-name="cellClassName"
:cell-style="cellStyle"
:header-row-class-name="headerRowClassName"
:header-row-style="headerRowStyle"
:header-cell-class-name="headerCellClassName"
:header-cell-style="headerCellStyle"
:row-key="rowKey"
:empty-text="emptyText"
:default-expand-all="defaultExpandAll"
:expand-row-keys="expandRowKeys"
:default-sort="defaultSort"
:tooltip-effect="tooltipEffect"
:show-summary="showSummary"
:sum-text="sumText"
:summary-method="summaryMethod"
:span-method="spanMethod"
:select-on-indeterminate="selectOnIndeterminate"
:indent="indent"
:lazy="lazy"
:load="load"
:tree-props="treeProps"
@select="handleSelect"
@select-all="handleSelectAll"
@selection-change="handleSelectionChange"
@cell-mouse-enter="handleCellMouseEnter"
@cell-mouse-leave="handleCellMouseLeave"
@cell-click="handleCellClick"
@cell-dblclick="handleCellDblclick"
@row-click="handleRowClick"
@row-contextmenu="handleRowContextmenu"
@row-dblclick="handleRowDblclick"
@header-click="handleHeaderClick"
@header-contextmenu="handleHeaderContextmenu"
@sort-change="handleSortChange"
@filter-change="handleFilterChange"
@current-change="handleCurrentChange"
@header-dragend="handleHeaderDragend"
@expand-change="handleExpandChange"
>
<slot />
</el-table>
<!-- ๅ้กต็ปไปถ -->
<div v-if="showPagination" class="gva-pagination">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="pageSizes"
:small="small"
:disabled="disabled"
:background="background"
:layout="layout"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// Propsๅฎไน
const props = defineProps({
// ่กจๆ ผๆฐๆฎ
tableData: {
type: Array,
default: () => []
},
// ๅ ่ฝฝ็ถๆ
loading: {
type: Boolean,
default: false
},
// ๆฏๅฆๆพ็คบๅ้กต
showPagination: {
type: Boolean,
default: true
},
// ๅ้กต้
็ฝฎ
total: {
type: Number,
default: 0
},
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
pageSizes: {
type: Array,
default: () => [10, 20, 50, 100]
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
// ่กจๆ ผ้
็ฝฎ
height: [String, Number],
maxHeight: [String, Number],
stripe: Boolean,
border: Boolean,
size: String,
fit: {
type: Boolean,
default: true
},
showHeader: {
type: Boolean,
default: true
},
highlightCurrentRow: Boolean,
currentRowKey: [String, Number],
rowClassName: [String, Function],
rowStyle: [Object, Function],
cellClassName: [String, Function],
cellStyle: [Object, Function],
headerRowClassName: [String, Function],
headerRowStyle: [Object, Function],
headerCellClassName: [String, Function],
headerCellStyle: [Object, Function],
rowKey: [String, Function],
emptyText: String,
defaultExpandAll: Boolean,
expandRowKeys: Array,
defaultSort: Object,
tooltipEffect: String,
showSummary: Boolean,
sumText: String,
summaryMethod: Function,
spanMethod: Function,
selectOnIndeterminate: {
type: Boolean,
default: true
},
indent: {
type: Number,
default: 16
},
lazy: Boolean,
load: Function,
treeProps: {
type: Object,
default: () => ({
hasChildren: 'hasChildren',
children: 'children'
})
},
small: Boolean,
disabled: Boolean,
background: {
type: Boolean,
default: true
}
})
// Emitsๅฎไน
const emit = defineEmits([
'select',
'select-all',
'selection-change',
'cell-mouse-enter',
'cell-mouse-leave',
'cell-click',
'cell-dblclick',
'row-click',
'row-contextmenu',
'row-dblclick',
'header-click',
'header-contextmenu',
'sort-change',
'filter-change',
'current-change',
'header-dragend',
'expand-change',
'size-change',
'page-change'
])
// ่กจๆ ผๅผ็จ
const tableRef = ref()
// ไบไปถๅค็
const handleSelect = (selection, row) => emit('select', selection, row)
const handleSelectAll = (selection) => emit('select-all', selection)
const handleSelectionChange = (selection) => emit('selection-change', selection)
const handleCellMouseEnter = (row, column, cell, event) => emit('cell-mouse-enter', row, column, cell, event)
const handleCellMouseLeave = (row, column, cell, event) => emit('cell-mouse-leave', row, column, cell, event)
const handleCellClick = (row, column, cell, event) => emit('cell-click', row, column, cell, event)
const handleCellDblclick = (row, column, cell, event) => emit('cell-dblclick', row, column, cell, event)
const handleRowClick = (row, column, event) => emit('row-click', row, column, event)
const handleRowContextmenu = (row, column, event) => emit('row-contextmenu', row, column, event)
const handleRowDblclick = (row, column, event) => emit('row-dblclick', row, column, event)
const handleHeaderClick = (column, event) => emit('header-click', column, event)
const handleHeaderContextmenu = (column, event) => emit('header-contextmenu', column, event)
const handleSortChange = (data) => emit('sort-change', data)
const handleFilterChange = (filters) => emit('filter-change', filters)
const handleCurrentChange = (currentRow, oldCurrentRow) => emit('current-change', currentRow, oldCurrentRow)
const handleHeaderDragend = (newWidth, oldWidth, column, event) => emit('header-dragend', newWidth, oldWidth, column, event)
const handleExpandChange = (row, expandedRows) => emit('expand-change', row, expandedRows)
// ๅ้กตไบไปถๅค็
const handleSizeChange = (size) => emit('size-change', size)
const handlePageChange = (page) => emit('page-change', page)
// ๆด้ฒ่กจๆ ผๆนๆณ
defineExpose({
tableRef,
clearSelection: () => tableRef.value?.clearSelection(),
toggleRowSelection: (row, selected) => tableRef.value?.toggleRowSelection(row, selected),
toggleAllSelection: () => tableRef.value?.toggleAllSelection(),
toggleRowExpansion: (row, expanded) => tableRef.value?.toggleRowExpansion(row, expanded),
setCurrentRow: (row) => tableRef.value?.setCurrentRow(row),
clearSort: () => tableRef.value?.clearSort(),
clearFilter: (columnKeys) => tableRef.value?.clearFilter(columnKeys),
doLayout: () => tableRef.value?.doLayout(),
sort: (prop, order) => tableRef.value?.sort(prop, order)
})
</script>
<style lang="scss" scoped>
.gva-table {
.gva-pagination {
margin-top: 20px;
text-align: right;
}
}
</style>๐ Permission Control โ
Permission Directive โ
javascript
// src/directive/auth.js
import { useUserStore } from '@/pinia/modules/user'
// ๆ้ๆฃๆฅๅฝๆฐ
function checkPermission(el, binding) {
const { value } = binding
const userStore = useUserStore()
const roles = userStore.userInfo.authority?.defaultRouter || []
if (value && value instanceof Array && value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error('ๆ้ๆไปค้่ฆไผ ๅ
ฅๆฐ็ปๅๆฐ')
}
}
// ๆ้ๆไปค
export default {
mounted(el, binding) {
checkPermission(el, binding)
},
updated(el, binding) {
checkPermission(el, binding)
}
}Button Permission Control โ
javascript
// src/utils/btnAuth.js
import { useUserStore } from '@/pinia/modules/user'
// ๆฃๆฅๆ้ฎๆ้
export function checkBtnPermission(btnName) {
const userStore = useUserStore()
const btnAuth = userStore.userInfo.authority?.btns || []
return btnAuth.includes(btnName)
}
// ๆ้ๆ้ฎ็ปไปถ
export function useBtnAuth() {
const userStore = useUserStore()
const hasAuth = (btnName) => {
const btnAuth = userStore.userInfo.authority?.btns || []
return btnAuth.includes(btnName)
}
return {
hasAuth
}
}๐จ Theme Customization โ
Element Plus Theme Customization โ
scss
// src/style/element_variables.scss
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #409eff,
),
'success': (
'base': #67c23a,
),
'warning': (
'base': #e6a23c,
),
'danger': (
'base': #f56c6c,
),
'error': (
'base': #f56c6c,
),
'info': (
'base': #909399,
),
)
);
// ่ชๅฎไน็ปไปถๆ ทๅผ
.el-button {
border-radius: 4px;
&.is-round {
border-radius: 20px;
}
}
.el-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.el-table {
.el-table__header {
th {
background-color: #fafafa;
color: #606266;
font-weight: 500;
}
}
}Dark Mode Support โ
scss
// src/style/dark.scss
[data-theme='dark'] {
--el-bg-color: #141414;
--el-bg-color-page: #0a0a0a;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
--el-text-color-secondary: #a3a6ad;
--el-text-color-placeholder: #8d9095;
--el-text-color-disabled: #6c6e72;
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2b2b2c;
--el-border-color-dark: #58585b;
--el-border-color-darker: #636466;
--el-fill-color: #303133;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1d1d1d;
--el-fill-color-extra-light: #191919;
--el-fill-color-dark: #39393a;
--el-fill-color-darker: #424243;
--el-fill-color-blank: transparent;
// ่ชๅฎไน็ปไปถๆ่ฒๆ ทๅผ
.layout-container {
background-color: var(--el-bg-color-page);
}
.gva-card {
background-color: var(--el-bg-color);
border-color: var(--el-border-color);
}
}๐ฑ Responsive Design โ
Mobile Adaptation โ
scss
// src/style/mobile.scss
@media screen and (max-width: 768px) {
.layout-container {
.aside {
position: fixed;
top: 0;
left: -210px;
z-index: 1001;
transition: left 0.3s;
&.mobile-show {
left: 0;
}
}
.main-container {
margin-left: 0;
.navbar {
.hamburger-container {
display: block;
}
}
}
}
.gva-table {
.el-table {
font-size: 12px;
}
.gva-pagination {
.el-pagination {
justify-content: center;
.el-pagination__sizes,
.el-pagination__jump {
display: none;
}
}
}
}
.gva-form {
.el-form-item {
margin-bottom: 15px;
.el-form-item__label {
line-height: 20px;
margin-bottom: 5px;
}
}
}
}
@media screen and (max-width: 480px) {
.gva-search-box {
.el-form {
.el-form-item {
width: 100%;
margin-right: 0;
margin-bottom: 10px;
}
}
}
.gva-btn-list {
.el-button {
margin-bottom: 10px;
width: 100%;
}
}
}๐ Performance Optimization โ
Route Lazy Loading โ
javascript
// ่ทฏ็ฑๆๅ ่ฝฝ้
็ฝฎ
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '@/view/dashboard/index.vue')
},
{
path: '/system',
name: 'System',
component: () => import(/* webpackChunkName: "system" */ '@/view/system/index.vue'),
children: [
{
path: 'user',
name: 'User',
component: () => import(/* webpackChunkName: "system-user" */ '@/view/system/user/index.vue')
}
]
}
]Component Lazy Loading โ
vue
<template>
<div>
<!-- ไฝฟ็จ Suspense ๅ
่ฃ
ๅผๆญฅ็ปไปถ -->
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">ๅ ่ฝฝไธญ...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// ๅผๆญฅ็ปไปถ
const AsyncComponent = defineAsyncComponent({
loader: () => import('@/components/heavy-component.vue'),
loadingComponent: () => import('@/components/loading.vue'),
errorComponent: () => import('@/components/error.vue'),
delay: 200,
timeout: 3000
})
</script>Image Lazy Loading โ
vue
<template>
<div class="image-container">
<img
v-lazy="imageSrc"
:alt="imageAlt"
class="lazy-image"
@load="handleImageLoad"
@error="handleImageError"
>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
imageSrc: {
type: String,
required: true
},
imageAlt: {
type: String,
default: ''
}
})
const imageLoaded = ref(false)
const imageError = ref(false)
const handleImageLoad = () => {
imageLoaded.value = true
}
const handleImageError = () => {
imageError.value = true
}
</script>
<style scoped>
.image-container {
position: relative;
overflow: hidden;
}
.lazy-image {
width: 100%;
height: auto;
transition: opacity 0.3s;
}
.lazy-image[lazy=loading] {
opacity: 0.3;
}
.lazy-image[lazy=loaded] {
opacity: 1;
}
.lazy-image[lazy=error] {
opacity: 0.3;
}
</style>๐งช Testing โ
Unit Test Configuration โ
javascript
// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js']
},
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
}
})Component Test Example โ
javascript
// tests/components/GvaTable.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import GvaTable from '@/components/gva-table/index.vue'
import { ElTable, ElPagination } from 'element-plus'
describe('GvaTable', () => {
it('renders table with data', () => {
const tableData = [
{ id: 1, name: 'ๅผ ไธ', age: 25 },
{ id: 2, name: 'ๆๅ', age: 30 }
]
const wrapper = mount(GvaTable, {
props: {
tableData,
total: 2,
currentPage: 1,
pageSize: 10
},
global: {
components: {
ElTable,
ElPagination
}
}
})
expect(wrapper.find('.gva-table').exists()).toBe(true)
expect(wrapper.find('.gva-pagination').exists()).toBe(true)
})
it('emits page-change event when page changes', async () => {
const wrapper = mount(GvaTable, {
props: {
tableData: [],
total: 100,
currentPage: 1,
pageSize: 10
}
})
await wrapper.vm.handlePageChange(2)
expect(wrapper.emitted('page-change')).toBeTruthy()
expect(wrapper.emitted('page-change')[0]).toEqual([2])
})
})๐ Best Practices โ
1. Code Specification โ
- Use ESLint + Prettier to ensure code quality
- Follow Vue 3 Composition API best practices
- Component naming uses PascalCase
- File naming uses kebab-case
2. Performance Optimization โ
- Reasonably use v-memo and v-once
- Avoid complex calculations in templates
- Use shallowRef and shallowReactive to optimize reactivity
- Reasonably split components to avoid overly large components
3. Security Protection โ
- Validate and filter user input
- Be cautious of XSS protection when using v-html
- Do not store sensitive information on the front end
- Use HTTPS to transmit data
4. User Experience โ
- Provide loading status prompts
- Reasonable error handling and prompts
- Responsive design adapts to mobile devices
- Accessibility support
๐ Common Issues โ
Q: How to solve route lazy loading failure? โ
A: Check if the path is correct, ensure the component file exists, and add error handling.
Q: Element Plus styles not taking effect? โ
A: Ensure the style file is correctly imported, and check CSS specificity and scope.
Q: Pinia state loss? โ
A: Check if the state is correctly persisted, and reinitialize the state on page refresh.
Q: Static resource path error after packaging? โ
A: Check the base path and publicPath settings in the Vite configuration.


