Skip to content
geelevelgeelevel

🎨 前端知识库

Gin-Vue-Admin 前端基于 Vue 3 + Vite 4 + Element Plus 构建,采用现代化的前端开发技术栈,提供高效的开发体验和优秀的用户界面。

🚀 技术栈

核心框架

  • Vue 3 - 渐进式 JavaScript 框架
  • Vite 4 - 下一代前端构建工具
  • **Element Plus ** - 基于 Vue 3 的组件库

状态管理

  • Pinia - Vue 3 官方推荐的状态管理库
  • Vue Router 4 - Vue.js 官方路由管理器

开发工具

  • TypeScript - JavaScript 的超集(可选)
  • ESLint - 代码质量检查工具
  • Prettier - 代码格式化工具
  • Sass/SCSS - CSS 预处理器

构建优化

  • Vite Plugin - 丰富的插件生态
  • Tree Shaking - 自动移除未使用代码
  • Code Splitting - 代码分割优化
  • Hot Module Replacement - 热模块替换

📁 前端目录结构

web
 ├── babel.config.js
 ├── Dockerfile
 ├── favicon.ico
 ├── index.html                  -- 主页面
 ├── limit.js                    -- 助手代码
 ├── package.json                -- 包管理器代码
 ├── src                         -- 源代码
 │   ├── api                    -- api 组
 │   ├── App.vue                -- 主页面
 │   ├── assets                 -- 静态资源
 │   ├── components             -- 全局组件
 │   ├── core                   -- gva 组件包
 │   │   ├── config.js         -- gva网站配置文件
 │   │   ├── gin-vue-admin.js  -- 注册欢迎文件
 │   │   └── global.js         -- 统一导入文件
 │   ├── directive              -- v-auth 注册文件
 │   ├── main.js                -- 主文件
 │   ├── permission.js          -- 路由中间件
 │   ├── pinia                  -- pinia 状态管理器,取代vuex
 │   │   ├── index.js          -- 入口文件
 │   │   └── modules           -- modules
 │   │       ├── dictionary.js
 │   │       ├── router.js
 │   │       └── user.js
 │   ├── router                 -- 路由声明文件
 │   │   └── index.js
 │   ├── style                  -- 全局样式
 │   │   ├── base.scss
 │   │   ├── basics.scss
 │   │   ├── element_visiable.scss  -- 此处可以全局覆盖 element-plus 样式
 │   │   ├── iconfont.css           -- 顶部几个icon的样式文件
 │   │   ├── main.scss
 │   │   ├── mobile.scss
 │   │   └── newLogin.scss
 │   ├── utils                  -- 方法包库
 │   │   ├── asyncRouter.js    -- 动态路由相关
 │   │   ├── btnAuth.js        -- 动态权限按钮相关
 │   │   ├── bus.js            -- 全局mitt声明文件
 │   │   ├── date.js           -- 日期相关
 │   │   ├── dictionary.js     -- 获取字典方法 
 │   │   ├── downloadImg.js    -- 下载图片方法
 │   │   ├── format.js         -- 格式整理相关
 │   │   ├── image.js          -- 图片相关方法
 │   │   ├── page.js           -- 设置页面标题
 │   │   ├── request.js        -- 统一请求文件
 │   │   └── stringFun.js      -- 字符串文件
 |   ├── view                   -- 主要view代码
 |   |   ├── about              -- 关于我们
 |   |   ├── dashboard          -- 面板
 |   |   ├── error              -- 错误
 |   |   ├── example            -- 上传案例
 |   |   ├── iconList           -- icon列表
 |   |   ├── init               -- 初始化数据  
 |   |   ├── layout             -- layout约束页面 
 |   |   |   ├── aside          -- 侧边栏
 |   |   |   ├── bottomInfo     -- bottomInfo
 |   |   |   ├── screenfull     -- 全屏设置
 |   |   |   ├── setting        -- 系统设置
 |   |   |   └── index.vue      -- base 约束
 |   |   ├── login              --登录 
 |   |   ├── person             --个人中心 
 |   |   ├── superAdmin         -- 超级管理员操作
 |   |   ├── system             -- 系统检测页面
 |   |   ├── systemTools        -- 系统配置相关页面
 |   |   └── routerHolder.vue   -- page 入口页面 
 ├── vite.config.js             -- vite 配置文件
 └── yarn.lock

🛠️ 开发环境配置

环境要求

  • Node.js >= 16.0.0
  • npm >= 8.0.0 或 yarn >= 1.22.0
  • Git 版本控制工具

安装依赖

bash
# 进入前端目录
cd web

# 使用 npm 安装
npm install

# 或使用 yarn 安装
yarn install

开发命令

bash
# 启动开发服务器
npm run serve
# 或
yarn serve

# 构建生产版本
npm run build
# 或
yarn build

# 代码检查
npm run lint
# 或
yarn lint

# 代码格式化
npm run format
# 或
yarn format

🎯 核心配置文件

Vite 配置 (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]'
      }
    }
  }
})

项目配置 (src/core/config.js)

javascript
// 系统全局配置
export const config = {
  appName: 'Gin-Vue-Admin',
  appLogo: 'logoIco.png',
  showProgressBar: true,
  progressBarColor: '#409EFF',
  showInfoTip: true,
  
  // 布局配置
  layout: {
    showTagsView: true,
    showSidebarLogo: true,
    fixedHeader: true,
    sidebarTextTheme: true,
    showGreyMode: false,
    showColorWeakness: false
  },
  
  // 主题配置
  theme: {
    primaryColor: '#409EFF',
    successColor: '#67C23A',
    warningColor: '#E6A23C',
    dangerColor: '#F56C6C',
    infoColor: '#909399'
  }
}

🏗️ 核心架构

1. 路由系统

静态路由配置

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 router

动态路由处理

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. 状态管理 (Pinia)

用户状态管理

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)
    }
  }
})

路由状态管理

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 请求封装

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

🎨 组件开发

全局组件注册

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])
  })
}

自定义组件示例

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>

🔐 权限控制

权限指令

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)
  }
}

按钮权限控制

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
  }
}

🎨 主题定制

Element Plus 主题定制

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;
    }
  }
}

暗色主题支持

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);
  }
}

📱 响应式设计

移动端适配

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%;
    }
  }
}

🚀 性能优化

路由懒加载

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')
      }
    ]
  }
]

组件懒加载

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>

图片懒加载

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>

🧪 测试

单元测试配置

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')
    }
  }
})

组件测试示例

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])
  })
})

📚 最佳实践

1. 代码规范

  • 使用 ESLint + Prettier 保证代码质量
  • 遵循 Vue 3 Composition API 最佳实践
  • 组件命名使用 PascalCase
  • 文件命名使用 kebab-case

2. 性能优化

  • 合理使用 v-memo 和 v-once
  • 避免在模板中使用复杂计算
  • 使用 shallowRef 和 shallowReactive 优化响应式
  • 合理拆分组件,避免组件过大

3. 安全防护

  • 对用户输入进行验证和过滤
  • 使用 v-html 时注意 XSS 防护
  • 敏感信息不要存储在前端
  • 使用 HTTPS 传输数据

4. 用户体验

  • 提供加载状态提示
  • 合理的错误处理和提示
  • 响应式设计适配移动端
  • 无障碍访问支持

🐛 常见问题

Q: 如何解决路由懒加载失败?

A: 检查路径是否正确,确保组件文件存在,可以添加错误处理。

Q: Element Plus 样式不生效?

A: 确保正确导入样式文件,检查 CSS 优先级和作用域。

Q: Pinia 状态丢失?

A: 检查是否正确持久化状态,页面刷新时重新初始化状态。

Q: 打包后静态资源路径错误?

A: 检查 Vite 配置中的 base 路径和 publicPath 设置。

📚 相关文档