Skip to content

DSPlatform Vue3 前端架构详解

概述

DSPlatform 管理后台采用 Vue 3 + Element Plus + TypeScript + Pinia + Vite 技术栈开发,支持多角色、多平台的管理界面。项目采用模块化架构设计,包含管理员端、店铺端、商户端、消费者端等多个业务模块,提供完整的后台管理功能。

技术栈

核心技术

  • Vue 3.4.29: 渐进式 JavaScript 框架
  • Element Plus 2.9.8: Vue 3 UI 组件库
  • TypeScript 5.4.0: JavaScript 的超集
  • Pinia 2.1.7: Vue 3 状态管理库
  • Vue Router 4.3.3: Vue 官方路由管理器
  • Vite 5.3.1: 构建工具
  • Axios 1.7.7: HTTP 客户端

依赖库

  • @vueuse/core 11.0.3: Vue 组合式 API 工具库
  • @wangeditor/editor-for-vue 5.1.12: 富文本编辑器
  • @amap/amap-jsapi-loader 1.0.1: 高德地图 API
  • vue-draggable-plus 0.6.0: 拖拽组件
  • nprogress 0.2.0: 进度条
  • lodash 4.17.21: 工具函数库
  • tailwindcss 3.4.17: CSS 框架

项目结构

目录结构

vue-element-admin/
├── src/
│   ├── api/                    # API 接口定义
│   │   ├── system/            # 系统相关接口
│   │   ├── tbl-store/         # 店铺相关接口
│   │   └── wechat/            # 微信相关接口
│   ├── assets/                # 静态资源
│   │   ├── icons/             # 图标资源
│   │   │   ├── local/        # 本地图标
│   │   │   └── svg/          # SVG 图标
│   │   └── login/             # 登录页背景图
│   ├── components/            # 公共组件
│   │   ├── attachment/        # 附件组件
│   │   ├── ds-area-picker/   # 地区选择器
│   │   ├── ds-lbs-picker/    # 位置选择器
│   │   ├── editor/           # 富文本编辑器
│   │   ├── icon/             # 图标组件
│   │   ├── icon-picker/      # 图标选择器
│   │   ├── popover-input/    # 弹出输入框
│   │   └── trade/            # 交易相关组件
│   ├── hooks/                # 组合式函数
│   │   ├── useCountdown.ts   # 倒计时 Hook
│   │   ├── useEnum.ts        # 枚举 Hook
│   │   └── usePagination.ts  # 分页 Hook
│   ├── layout/               # 布局组件
│   │   ├── components/       # 布局子组件
│   │   │   ├── lay-header/   # 头部组件
│   │   │   ├── lay-main/     # 主内容区
│   │   │   ├── lay-menu/     # 菜单组件
│   │   │   └── lay-sidebar/  # 侧边栏组件
│   │   └── index.vue         # 主布局
│   ├── pages-admin/          # 管理员端页面
│   │   ├── components/       # 管理员端组件
│   │   ├── main/            # 主要功能模块
│   │   │   ├── api/         # API 接口
│   │   │   └── views/       # 页面视图
│   │   └── platform/        # 平台相关页面
│   │       ├── food/        # 餐饮平台
│   │       ├── house/       # 家政平台
│   │       ├── kms/         # 知识管理平台
│   │       └── mall/        # 商城平台
│   ├── pages-consumer/       # 消费者端页面
│   ├── pages-merchant/       # 商户端页面
│   ├── pages-store/          # 店铺端页面
│   │   ├── components/      # 店铺端组件
│   │   ├── main/            # 主要功能模块
│   │   └── platform/        # 平台相关页面
│   ├── router/               # 路由配置
│   │   ├── index.ts         # 路由入口
│   │   ├── modules/         # 路由模块
│   │   └── utils.ts         # 路由工具函数
│   ├── stores/               # 状态管理
│   │   ├── index.ts         # Store 入口
│   │   └── modules/         # Store 模块
│   │       ├── config.ts    # 配置 Store
│   │       ├── editable.ts  # 可编辑 Store
│   │       ├── enum.ts      # 枚举 Store
│   │       ├── multiTags.ts # 多标签 Store
│   │       ├── system.ts    # 系统 Store
│   │       ├── themeConfig.ts # 主题配置 Store
│   │       └── userInfo.ts  # 用户信息 Store
│   ├── styles/               # 样式文件
│   │   ├── common.scss      # 通用样式
│   │   ├── element-plus.scss # Element Plus 样式
│   │   ├── index.scss       # 样式入口
│   │   ├── tailwind.css     # Tailwind CSS
│   │   └── themes/          # 主题样式
│   ├── utils/                # 工具函数
│   │   ├── auth.ts          # 认证工具
│   │   ├── request.ts       # 请求工具
│   │   ├── storage.ts       # 存储工具
│   │   └── util.ts          # 通用工具
│   ├── App.vue               # 应用入口
│   └── main.ts               # 主入口文件
├── public/                   # 公共资源
├── package.json              # 项目配置
├── vite.config.ts            # Vite 配置
├── tsconfig.json             # TypeScript 配置
├── tailwind.config.js        # Tailwind 配置
└── postcss.config.js         # PostCSS 配置

架构设计

整体架构图

┌─────────────────────────────────────────────────────────────┐
│                        应用层                                │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│   管理员端   │   店铺端     │   商户端     │     消费者端         │
│   Admin    │   Store     │  Merchant   │    Consumer        │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                      共享层                                 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│   组件库     │   工具函数   │   状态管理   │     样式系统        │
│ Components  │   Utils     │   Stores    │     Styles         │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                      API 层                                │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│   请求封装   │   接口定义   │   错误处理   │     拦截器          │
│   Request   │    API      │ Error Handle│  Interceptors      │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                     平台层                                 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│     Web     │   移动端     │    桌面端    │     其他平台        │
│   Browser   │  Mobile     │  Desktop    │   Other Platforms  │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

模块化设计

1. 管理员端模块 (pages-admin)

typescript
// 管理员端主要功能
interface AdminModule {
  // 页面
  pages: {
    home: '首页'
    user: '用户管理'
    merchant: '商户管理'
    store: '店铺管理'
    goods: '商品管理'
    order: '订单管理'
    system: '系统管理'
    setting: '系统设置'
    editable: '可编辑页面'
    distributor: '分销管理'
    pointsGoods: '积分商品'
    rider: '骑手管理'
    technician: '师傅管理'
    trade: '交易管理'
    video: '短视频管理'
  }
  
  // API
  api: {
    admin: '管理员相关接口'
    user: '用户相关接口'
    merchant: '商户相关接口'
    store: '店铺相关接口'
    goods: '商品相关接口'
    order: '订单相关接口'
    system: '系统相关接口'
    stat: '统计相关接口'
  }
  
  // 组件
  components: {
    merchant: '商户组件'
    goods: '商品组件'
    order: '订单组件'
    user: '用户组件'
  }
}

2. 店铺端模块 (pages-store)

typescript
// 店铺端主要功能
interface StoreModule {
  pages: {
    home: '店铺首页'
    goods: '商品管理'
    order: '订单管理'
    marketing: '营销管理'
    technician: '师傅管理'
    setting: '店铺设置'
  }
  
  api: {
    store: '店铺相关接口'
    goods: '商品相关接口'
    order: '订单相关接口'
    marketing: '营销相关接口'
    technician: '师傅相关接口'
  }
  
  components: {
    goods: '商品组件'
    order: '订单组件'
    marketing: '营销组件'
    technician: '师傅组件'
  }
}

3. 商户端模块 (pages-merchant)

typescript
// 商户端主要功能
interface MerchantModule {
  pages: {
    home: '商户首页'
    store: '店铺管理'
    stat: '数据统计'
    setting: '商户设置'
  }
  
  api: {
    merchant: '商户相关接口'
    store: '店铺相关接口'
    stat: '统计相关接口'
  }
}

4. 消费者端模块 (pages-consumer)

typescript
// 消费者端主要功能
interface ConsumerModule {
  pages: {
    home: '消费者首页'
    profile: '个人中心'
    order: '订单管理'
    setting: '设置'
  }
  
  api: {
    consumer: '消费者相关接口'
    order: '订单相关接口'
  }
}

核心功能实现

1. 请求封装

Request 类

typescript
// src/utils/request.ts
import axios from 'axios'
import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosRequestConfig, AxiosError } from 'axios'
import { getToken, setToken } from './auth'
import { isUrl } from './util'
import { ElMessage } from 'element-plus'
import storage from './storage'
import useUserInfoStore from '@/stores/modules/userInfo'
import { refreshToken } from '@/api/login'

class Request {
  private instance: AxiosInstance
  private messageCache: Map<string, { timestamp: number }>
  private isRefreshing: boolean = false
  private refreshSubscribers: Array<(token: string) => void> = []

  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_APP_BASE_URL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    this.messageCache = new Map()
    this.setupInterceptors()
  }

  // 设置拦截器
  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        // 添加访问令牌
        const accessToken = getToken('access_token')
        if (accessToken) {
          config.headers['access-token'] = accessToken
        }

        // 添加店铺ID
        const storeId = storage.get('manage_store_id')
        if (storeId) {
          config.headers['manage-store-id'] = storeId
        }

        return config
      },
      (error: AxiosError) => {
        return Promise.reject(error)
      }
    )

    // 响应拦截器
    this.instance.interceptors.response.use(
      (response: AxiosResponse) => {
        const { data } = response

        // 业务状态码检查
        if (data.code === 401) {
          // Token 过期,尝试刷新
          return this.handleTokenRefresh(response.config)
        }

        if (data.code !== 10000) {
          // 显示错误消息
          if (response.config.showErrorMessage !== false) {
            this.showMessage(data.message || '请求失败', 'error')
          }
          return Promise.reject(new Error(data.message || '请求失败'))
        }

        // 显示成功消息
        if (response.config.showSuccessMessage && data.message) {
          this.showMessage(data.message, 'success')
        }

        return data
      },
      (error: AxiosError) => {
        this.handleError(error)
        return Promise.reject(error)
      }
    )
  }

  // Token 刷新处理
  private async handleTokenRefresh(originalConfig: InternalAxiosRequestConfig) {
    if (this.isRefreshing) {
      // 等待刷新完成
      return new Promise((resolve) => {
        this.refreshSubscribers.push((token: string) => {
          originalConfig.headers['access-token'] = token
          resolve(this.instance.request(originalConfig))
        })
      })
    }

    this.isRefreshing = true

    try {
      const refreshTokenValue = getToken('refresh_token')
      const response = await refreshToken({ refresh_token: refreshTokenValue })
      
      // 更新 token
      setToken(response.data.access_token, 'access_token')
      setToken(response.data.refresh_token, 'refresh_token')

      // 通知等待的请求
      this.refreshSubscribers.forEach(callback => callback(response.data.access_token))
      this.refreshSubscribers = []

      // 重新发送原请求
      originalConfig.headers['access-token'] = response.data.access_token
      return this.instance.request(originalConfig)
    } catch (error) {
      // 刷新失败,跳转登录
      this.redirectToLogin()
      throw error
    } finally {
      this.isRefreshing = false
    }
  }

  // 显示消息
  private showMessage(message: string, type: 'success' | 'error' | 'warning') {
    const cacheKey = `${type}_${message}`
    const now = Date.now()
    
    // 防止重复显示相同消息
    if (this.messageCache.has(cacheKey)) {
      const lastTime = this.messageCache.get(cacheKey)!.timestamp
      if (now - lastTime < 2000) return
    }
    
    this.messageCache.set(cacheKey, { timestamp: now })
    
    ElMessage({
      message,
      type,
      duration: 2000
    })
  }

  // 错误处理
  private handleError(error: AxiosError) {
    if (error.response) {
      const { status } = error.response
      switch (status) {
        case 401:
          this.redirectToLogin()
          break
        case 403:
          this.showMessage('没有权限访问', 'error')
          break
        case 404:
          this.showMessage('请求的资源不存在', 'error')
          break
        case 500:
          this.showMessage('服务器内部错误', 'error')
          break
        default:
          this.showMessage('网络错误', 'error')
      }
    } else if (error.request) {
      this.showMessage('网络连接失败', 'error')
    } else {
      this.showMessage('请求配置错误', 'error')
    }
  }

  // 跳转到登录页
  private redirectToLogin() {
    const userStore = useUserInfoStore()
    userStore.logout()
  }

  // 发送请求
  async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
    return this.instance.request(config)
  }
}

export default new Request()

2. 状态管理

Pinia Store 设计

typescript
// src/stores/modules/userInfo.ts
import { defineStore } from 'pinia'
import type { RouteRecordRaw } from 'vue-router'
import { transformMenuToRoutes, removeDynamicRoutes, filterRoutesByPermissions } from '@/router/utils'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { getCurrentUserMenus, getCurrentUserInfo } from '@/api/menuRoutes'
import { loginNormal, logout } from '@/api/login'
import storage from '@/utils/storage'
import router from '@/router'
import useMultiTagsStore from '@/stores/modules/multiTags'
import { getSystemType } from '@/utils/util'

interface UserState {
  access_token: string
  refresh_token: string
  userInfo: any
  userMenus: any
  menuRoutes: RouteRecordRaw[]
}

const useUserInfoStore = defineStore('userInfo', {
  state: (): UserState => ({
    access_token: getToken('access_token') || '',
    refresh_token: getToken('refresh_token') || '',
    userInfo: {},
    userMenus: [],
    menuRoutes: []
  }),

  getters: {
    isLogin: (state) => !!state.access_token,
    userRole: (state) => state.userInfo.role || 'user'
  },

  actions: {
    // 登录
    login(payload: { username: string; password: string }) {
      const { username, password } = payload
      return new Promise((resolve, reject) => {
        loginNormal({
          username: username.trim(),
          password
        })
          .then(res => {
            // 存储token和用户信息
            setToken(res.data.access_token, 'access_token')
            setToken(res.data.refresh_token, 'refresh_token')
            
            if (res.data.userinfo.manage_store_list && res.data.userinfo.manage_store_list.length > 0) {
              // 设置默认店铺id
              storage.set('manage_store_id', res.data.userinfo.manage_store_list[0].id, 7 * 24 * 60 * 60)
            }
            
            this.access_token = res.data.access_token
            this.refresh_token = res.data.refresh_token
            this.userInfo = res.data.userinfo
            
            resolve(res)
          })
          .catch(reject)
      })
    },

    // 获取用户信息
    async getUserInfo() {
      try {
        const res = await getCurrentUserInfo()
        this.userInfo = res.data
        return res.data
      } catch (error) {
        console.error('获取用户信息失败:', error)
        throw error
      }
    },

    // 获取用户菜单
    async getUserMenus() {
      try {
        const res = await getCurrentUserMenus()
        this.userMenus = res.data
        
        // 转换菜单为路由
        const routes = transformMenuToRoutes(res.data)
        this.menuRoutes = routes
        
        // 添加动态路由
        addRoutesRecursively(router, routes)
        
        return res.data
      } catch (error) {
        console.error('获取用户菜单失败:', error)
        throw error
      }
    },

    // 登出
    async logout() {
      try {
        const refreshTokenValue = getToken('refresh_token')
        const logoutParams = refreshTokenValue ? { refresh_token: refreshTokenValue } : undefined
        
        await logout(logoutParams)
      } catch (error) {
        console.error('登出失败:', error)
      } finally {
        // 清理状态
        this.access_token = ''
        this.refresh_token = ''
        this.userInfo = {}
        this.userMenus = []
        this.menuRoutes = []
        
        // 清理存储
        removeToken('access_token')
        removeToken('refresh_token')
        storage.remove('manage_store_id')
        
        // 清理多标签
        const multiTagsStore = useMultiTagsStore()
        multiTagsStore.clearTags()
        
        // 移除动态路由
        removeDynamicRoutes(router)
        
        // 跳转到登录页
        const systemType = getSystemType()
        const loginPath = systemType === 'admin' ? '/admin/login' : '/store/login'
        router.push(loginPath)
      }
    }
  }
})

export default useUserInfoStore

其他 Store 模块

typescript
// src/stores/modules/config.ts
import { defineStore } from 'pinia'
import { getSysConfigByKey, getConfigsByType } from '@/api/system/sysConfig'
import storage from '@/utils/storage'

interface ConfigState {
  configCache: Record<string, string>
  typeCache: Record<string, Record<string, string>>
  loading: boolean
}

export const useConfigStore = defineStore('config', {
  state: (): ConfigState => ({
    configCache: {},
    typeCache: {},
    loading: false
  }),

  getters: {
    getConfigValue: (state) => (key: string): string | null => {
      return state.configCache[key] || null
    },
    
    getTypeConfigs: (state) => (type: string): Record<string, string> => {
      return state.typeCache[type] || {}
    }
  },

  actions: {
    // 获取单个配置
    async fetchConfig(key: string): Promise<string | null> {
      if (this.configCache[key] !== undefined) {
        return this.configCache[key]
      }
      
      this.loading = true
      
      try {
        const res = await getSysConfigByKey(key)
        if (res.code === 10000 && res.data) {
          const value = String(res.data)
          this.configCache[key] = value
          this.saveToStorage()
          return value
        }
        return null
      } catch (error) {
        console.error(`获取配置[${key}]失败:`, error)
        return null
      } finally {
        this.loading = false
      }
    },

    // 保存到本地存储
    saveToStorage() {
      try {
        storage.set('sys_config', this.configCache, 3600)
        storage.set('sys_config_types', this.typeCache, 3600)
      } catch (e) {
        console.error('保存配置到本地存储失败', e)
      }
    }
  }
})

// src/stores/modules/enum.ts
import { defineStore } from 'pinia'
import storage from '@/utils/storage'
import { getEnumData } from '@/api/enum'

interface EnumData {
  [key: string]: Record<string | number, string>
}

const useEnumStore = defineStore('enum', {
  state: () => ({
    enumData: {} as EnumData,
    loadingStates: {} as Record<string, boolean>,
    isLoaded: false
  }),
  
  actions: {
    // 获取枚举数据
    async getEnum(type: string, forceRefresh = false): Promise<Record<string | number, string>> {
      if (!this.isLoaded) {
        this.loadFromStorage()
      }
      
      if (!forceRefresh && this.enumData[type]) {
        return this.enumData[type]
      }
      
      if (this.loadingStates[type]) {
        return new Promise((resolve) => {
          const checkInterval = setInterval(() => {
            if (!this.loadingStates[type]) {
              clearInterval(checkInterval)
              resolve(this.enumData[type] || {})
            }
          }, 50)
        })
      }
      
      this.loadingStates[type] = true
      
      try {
        const response = await getEnumData({ type: type })
        
        if (response.code === 10000) {
          const { data } = response
          
          if (data && data[type]) {
            if (Array.isArray(data[type])) {
              const enumObj: Record<number, string> = {}
              data[type].forEach((value: string, index: number) => {
                enumObj[index] = value
              })
              this.enumData[type] = enumObj
            } else {
              this.enumData[type] = data[type] as Record<string | number, string>
            }
            
            this.saveEnumData()
          }
        }
        
        return this.enumData[type] || {}
      } catch (error) {
        console.error(`加载枚举数据[${type}]失败:`, error)
        return this.enumData[type] || {}
      } finally {
        this.loadingStates[type] = false
      }
    },

    // 从本地存储加载
    loadFromStorage(): void {
      if (this.isLoaded) return
      
      const storedData = storage.get('enumData')
      if (storedData && typeof storedData === 'object') {
        this.enumData = storedData
      }
      
      this.isLoaded = true
    },

    // 保存到本地存储
    saveEnumData(): void {
      storage.set('enumData', this.enumData)
    }
  }
})

export default useEnumStore

3. 路由配置

路由入口

typescript
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

import useUserInfoStore from '@/stores/modules/userInfo'
import useMultiTagsStore from '@/stores/modules/multiTags'
import { findFirstAccessibleRoute, addRoutesRecursively } from './utils'

export const DEFAULT_LAYOUT = () => import('@/layout/index.vue')

// 导入消费者端路由
import { consumerRoutes } from './modules/consumer'

// 根据系统类型获取默认路由
import { getSystemType } from '@/utils/util'
const systemType = getSystemType()

// 根据系统类型获取默认路由
function getDefaultRoutes() {
  if (systemType == 'admin') {
    return [
      {
        path: '/:pathMatch(.*)*',
        component: () => import('@/pages-admin/main/views/not-found/index.vue'),
        meta: { title: '', show: false }
      },
      {
        path: '/admin/login',
        name: 'login',
        component: () => import('@/pages-admin/main/views/login/index.vue'),
        meta: { title: '登录', show: false }
      }
    ]
  } else if (systemType == 'store') {
    return [
      {
        path: '/:pathMatch(.*)*',
        component: () => import('@/pages-store/main/views/not-found/index.vue'),
        meta: { title: '', show: false }
      },
      {
        path: '/store/login',
        name: 'login',
        component: () => import('@/pages-store/main/views/login/index.vue'),
        meta: { title: '登录', show: false }
      }
    ]
  } else if (systemType == 'merchant') {
    return [
      {
        path: '/:pathMatch(.*)*',
        component: () => import('@/pages-merchant/main/views/not-found/index.vue'),
        meta: { title: '', show: false }
      },
      {
        path: '/merchant/login',
        name: 'login',
        component: () => import('@/pages-merchant/main/views/login/index.vue'),
        meta: { title: '登录', show: false }
      }
    ]
  } else {
    return consumerRoutes
  }
}

const router = createRouter({
  history: createWebHistory(),
  routes: getDefaultRoutes()
})

// 路由守卫
router.beforeEach(async (to, from, next) => {
  NProgress.start()
  
  const userStore = useUserInfoStore()
  const multiTagsStore = useMultiTagsStore()
  
  // 检查是否需要登录
  if (to.meta.requiresAuth !== false) {
    if (!userStore.isLogin) {
      // 未登录,跳转到登录页
      const systemType = getSystemType()
      const loginPath = systemType === 'admin' ? '/admin/login' : '/store/login'
      next(loginPath)
      return
    }
    
    // 已登录,检查是否有用户信息
    if (!userStore.userInfo.id) {
      try {
        await userStore.getUserInfo()
      } catch (error) {
        userStore.logout()
        return
      }
    }
    
    // 检查是否有菜单路由
    if (userStore.menuRoutes.length === 0) {
      try {
        await userStore.getUserMenus()
      } catch (error) {
        userStore.logout()
        return
      }
    }
  }
  
  // 添加标签
  if (to.meta.title && to.meta.show !== false) {
    multiTagsStore.addTag({
      name: to.name as string,
      title: to.meta.title as string,
      path: to.path,
      query: to.query,
      params: to.params
    })
  }
  
  next()
})

router.afterEach(() => {
  NProgress.done()
})

export default router

4. 布局系统

主布局组件

vue
<!-- src/layout/index.vue -->
<template>
  <div class="layout-container">
    <el-container>
      <!-- 头部 -->
      <el-header class="layout-header">
        <lay-header />
      </el-header>
      
      <el-container>
        <!-- 侧边栏 -->
        <el-aside class="layout-sidebar" :width="sidebarWidth">
          <lay-sidebar />
        </el-aside>
        
        <!-- 主内容区 -->
        <el-main class="layout-main">
          <lay-main />
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useThemeConfigStore } from '@/stores/modules/themeConfig'
import LayHeader from './components/lay-header/index.vue'
import LaySidebar from './components/lay-sidebar/index.vue'
import LayMain from './components/lay-main/index.vue'

const themeConfigStore = useThemeConfigStore()

const sidebarWidth = computed(() => {
  return themeConfigStore.isCollapse ? '64px' : '200px'
})
</script>

<style lang="scss" scoped>
.layout-container {
  height: 100vh;
  
  .layout-header {
    height: 60px;
    padding: 0;
    background-color: #fff;
    border-bottom: 1px solid #e6e6e6;
  }
  
  .layout-sidebar {
    background-color: #001529;
    transition: width 0.3s;
  }
  
  .layout-main {
    padding: 0;
    background-color: #f0f2f5;
  }
}
</style>

5. 组件设计

公共组件示例

vue
<!-- src/components/attachment/picker-image.vue -->
<template>
  <div class="image-picker">
    <el-upload
      :action="uploadUrl"
      :headers="uploadHeaders"
      :data="uploadData"
      :file-list="fileList"
      :on-success="handleSuccess"
      :on-error="handleError"
      :before-upload="beforeUpload"
      :on-remove="handleRemove"
      list-type="picture-card"
      :limit="limit"
      :accept="accept"
    >
      <el-icon><Plus /></el-icon>
    </el-upload>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getToken } from '@/utils/auth'

interface Props {
  modelValue?: string[]
  limit?: number
  accept?: string
}

const props = withDefaults(defineProps<Props>(), {
  limit: 9,
  accept: 'image/*'
})

const emit = defineEmits<{
  'update:modelValue': [value: string[]]
}>()

const fileList = ref<any[]>([])

const uploadUrl = computed(() => {
  return import.meta.env.VITE_APP_BASE_URL + '/api/attachment/upload'
})

const uploadHeaders = computed(() => {
  const token = getToken('access_token')
  return {
    'access-token': token || ''
  }
})

const uploadData = computed(() => {
  return {
    type: 'image'
  }
})

const handleSuccess = (response: any, file: any) => {
  if (response.code === 10000) {
    const currentValue = props.modelValue || []
    const newValue = [...currentValue, response.data.url]
    emit('update:modelValue', newValue)
    ElMessage.success('上传成功')
  } else {
    ElMessage.error(response.message || '上传失败')
  }
}

const handleError = (error: any) => {
  console.error('上传失败:', error)
  ElMessage.error('上传失败')
}

const beforeUpload = (file: File) => {
  const isValidType = file.type.startsWith('image/')
  if (!isValidType) {
    ElMessage.error('只能上传图片文件')
    return false
  }
  
  const isValidSize = file.size / 1024 / 1024 < 10
  if (!isValidSize) {
    ElMessage.error('图片大小不能超过 10MB')
    return false
  }
  
  return true
}

const handleRemove = (file: any) => {
  const currentValue = props.modelValue || []
  const newValue = currentValue.filter(url => url !== file.response?.data?.url)
  emit('update:modelValue', newValue)
}
</script>

<style lang="scss" scoped>
.image-picker {
  :deep(.el-upload--picture-card) {
    width: 100px;
    height: 100px;
    line-height: 100px;
  }
}
</style>

6. 工具函数

通用工具函数

typescript
// src/utils/util.ts
import { cloneDeep } from 'lodash'

// 通过方法获取类型 方便后期使用多入口
export function getSystemType() {
  return import.meta.env.VITE_SYSTEM_TYPE
}

/**
 * 处理数字精度,避免浮点数计算误差
 * @param num 需要处理的数字
 * @param precision 精度位数,默认2位小数
 */
export function toFixed(num: number, precision: number = 2): number {
  return Number(Number(num).toFixed(precision))
}

/**
 * 判断是否是url
 * @param str
 * @returns
 */
export function isUrl(str: string): boolean {
  return str.indexOf('http://') != -1 || str.indexOf('https://') != -1
}

// 获取服务器ICON地址
export function fetchRemoteIconUrl(path: string): string {
  // 服务器ICON地址
  const baseUrl = import.meta.env.VITE_APP_BASE_URL + '/static/'
  // 参数校验
  if (typeof path !== 'string' || path.trim() === '') {
    throw new Error('图片路径不能为空')
  }
  // 确保 path 以斜杠开头
  const normalizedPath = path.startsWith('/') ? path : `/${path}`
  // 手动拼接完整的 URL
  const imageUrl = `${baseUrl}${normalizedPath}`
  // 返回完整的 URL
  return imageUrl
}

/**
 * 处理文件附件URL,根据存储类型补全完整路径
 * @param path 文件路径
 * @param storageType 存储类型
 * @returns 完整的文件URL
 */
export function processAttachmentUrl(path: string, storageType: string = 'local'): string {
  if (!path) return ''
  
  // 如果是完整URL,直接返回
  if (isUrl(path)) {
    return path
  }
  
  // 根据存储类型处理
  switch (storageType) {
    case 'local':
      return fetchRemoteIconUrl(path)
    case 'oss':
      // OSS存储,直接返回路径
      return path
    case 'qiniu':
      // 七牛云存储,直接返回路径
      return path
    default:
      return fetchRemoteIconUrl(path)
  }
}

/**
 * 格式化时间
 * @param timestamp 时间戳
 * @param format 格式
 * @returns 格式化后的时间字符串
 */
export function formatTime(timestamp: number, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
  const date = new Date(timestamp * 1000)
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  const hour = String(date.getHours()).padStart(2, '0')
  const minute = String(date.getMinutes()).padStart(2, '0')
  const second = String(date.getSeconds()).padStart(2, '0')
  
  return format
    .replace('YYYY', String(year))
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hour)
    .replace('mm', minute)
    .replace('ss', second)
}

/**
 * 格式化价格
 * @param price 价格
 * @param decimals 小数位数
 * @returns 格式化后的价格字符串
 */
export function formatPrice(price: number, decimals: number = 2): string {
  return Number(price).toFixed(decimals)
}

/**
 * 深度克隆对象
 * @param obj 要克隆的对象
 * @returns 克隆后的对象
 */
export function deepClone<T>(obj: T): T {
  return cloneDeep(obj)
}

7. 样式系统

全局样式

scss
// src/styles/index.scss
@import './common.scss';
@import './element-plus.scss';
@import './tailwind.css';

// 全局样式重置
* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

#app {
  height: 100%;
}

// 通用类
.ds-container {
  padding: 20px;
}

.ds-card {
  background-color: white;
  border-radius: 8px;
  padding: 20px;
  margin-bottom: 20px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.ds-button {
  background-color: #409eff;
  color: white;
  border-radius: 4px;
  padding: 8px 16px;
  border: none;
  cursor: pointer;
  
  &:hover {
    background-color: #66b1ff;
  }
  
  &.disabled {
    background-color: #c0c4cc;
    color: #999;
    cursor: not-allowed;
  }
}

.ds-input {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  padding: 8px 12px;
  font-size: 14px;
  
  &:focus {
    border-color: #409eff;
    outline: none;
  }
}

Element Plus 样式

scss
// src/styles/element-plus.scss
// Element Plus 主题定制
:root {
  --el-color-primary: #409eff;
  --el-color-success: #67c23a;
  --el-color-warning: #e6a23c;
  --el-color-danger: #f56c6c;
  --el-color-info: #909399;
}

// 自定义 Element Plus 组件样式
.el-table {
  .el-table__header {
    th {
      background-color: #fafafa;
      color: #606266;
      font-weight: 500;
    }
  }
}

.el-form {
  .el-form-item__label {
    font-weight: 500;
  }
}

.el-button {
  &.el-button--primary {
    background-color: var(--el-color-primary);
    border-color: var(--el-color-primary);
  }
}

主题样式

scss
// src/styles/themes/light.css
:root {
  --bg-color: #ffffff;
  --text-color: #303133;
  --border-color: #dcdfe6;
  --sidebar-bg: #001529;
  --sidebar-text: #ffffff;
}

// src/styles/themes/dark.css
:root {
  --bg-color: #1a1a1a;
  --text-color: #e6e6e6;
  --border-color: #4c4d4f;
  --sidebar-bg: #001529;
  --sidebar-text: #ffffff;
}

多端适配

1. 系统类型配置

环境变量配置

typescript
// 环境变量类型定义
declare namespace NodeJS {
  interface ProcessEnv {
    VITE_SYSTEM_TYPE: 'admin' | 'store' | 'merchant' | 'consumer'
    VITE_APP_BASE_URL: string
    VITE_APP_TITLE: string
  }
}

系统类型判断

typescript
// src/utils/util.ts
export function getSystemType() {
  return import.meta.env.VITE_SYSTEM_TYPE
}

export function isAdminSystem(): boolean {
  return getSystemType() === 'admin'
}

export function isStoreSystem(): boolean {
  return getSystemType() === 'store'
}

export function isMerchantSystem(): boolean {
  return getSystemType() === 'merchant'
}

export function isConsumerSystem(): boolean {
  return getSystemType() === 'consumer'
}

2. 响应式设计

屏幕适配

scss
// 响应式断点
$breakpoint-xs: 480px;
$breakpoint-sm: 768px;
$breakpoint-md: 1024px;
$breakpoint-lg: 1200px;
$breakpoint-xl: 1920px;

// 媒体查询
@media screen and (max-width: $breakpoint-sm) {
  .layout-sidebar {
    display: none;
  }
  
  .layout-main {
    margin-left: 0;
  }
}

@media screen and (min-width: $breakpoint-md) {
  .layout-container {
    .layout-sidebar {
      display: block;
    }
  }
}

性能优化

1. 代码分割

路由懒加载

typescript
// 路由懒加载配置
export const DEFAULT_LAYOUT = () => import('@/layout/index.vue')

const routes = [
  {
    path: '/admin',
    component: DEFAULT_LAYOUT,
    children: [
      {
        path: 'user',
        component: () => import('@/pages-admin/main/views/user/index.vue')
      }
    ]
  }
]

2. 组件优化

组件懒加载

vue
<template>
  <div>
    <component :is="dynamicComponent" v-if="showComponent" />
  </div>
</template>

<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'

const showComponent = ref(false)

const dynamicComponent = defineAsyncComponent(() => 
  import('@/components/heavy-component.vue')
)
</script>

3. 缓存策略

数据缓存

typescript
// src/utils/cache.ts
class CacheManager {
  private cache = new Map<string, { data: any; timestamp: number; ttl: number }>()

  set(key: string, data: any, ttl: number = 300000) { // 默认5分钟
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
      ttl
    })
  }

  get(key: string): any | null {
    const item = this.cache.get(key)
    if (!item) return null

    if (Date.now() - item.timestamp > item.ttl) {
      this.cache.delete(key)
      return null
    }

    return item.data
  }

  clear() {
    this.cache.clear()
  }
}

export const cacheManager = new CacheManager()

开发规范

1. 代码规范

TypeScript 规范

typescript
// 接口定义
interface UserInfo {
  id: number
  username: string
  nickname: string
  avatar: string
  mobile: string
  email: string
  status: number
  create_at: number
  update_at: number
}

// 类型别名
type ApiResponse<T> = {
  code: number
  message: string
  data: T
}

// 枚举定义
enum OrderStatus {
  PENDING = 0,
  PAID = 1,
  SHIPPED = 2,
  DELIVERED = 3,
  CANCELLED = 4
}

Vue 组件规范

vue
<template>
  <!-- 模板内容 -->
</template>

<script setup lang="ts">
// 1. 导入
import { ref, computed, onMounted } from 'vue'
import type { PropType } from 'vue'

// 2. 接口定义
interface Props {
  title: string
  data: UserInfo[]
  loading?: boolean
}

// 3. Props 定义
const props = withDefaults(defineProps<Props>(), {
  loading: false
})

// 4. Emits 定义
const emit = defineEmits<{
  change: [value: string]
  submit: [data: UserInfo]
}>()

// 5. 响应式数据
const visible = ref(false)
const formData = ref<UserInfo>({})

// 6. 计算属性
const filteredData = computed(() => {
  return props.data.filter(item => item.status === 1)
})

// 7. 方法
const handleSubmit = () => {
  emit('submit', formData.value)
}

// 8. 生命周期
onMounted(() => {
  // 初始化逻辑
})
</script>

<style lang="scss" scoped>
// 样式内容
</style>

2. 文件命名规范

目录结构

src/
├── components/           # 组件目录
│   ├── user-select/     # 组件名使用 kebab-case
│   │   ├── index.vue    # 主组件文件
│   │   └── types.ts     # 类型定义
│   └── goods-list/
├── pages/               # 页面目录
│   ├── admin/           # 模块名使用 kebab-case
│   │   ├── user/        # 页面名使用 kebab-case
│   │   │   └── index.vue
│   │   └── merchant/
├── api/                 # API 目录
│   ├── user.ts         # 文件名使用 camelCase
│   └── goods.ts
├── utils/               # 工具目录
│   ├── request.ts      # 文件名使用 camelCase
│   └── auth.ts
└── stores/              # 状态管理目录
    ├── user.ts         # 文件名使用 camelCase
    └── config.ts

3. 提交规范

Git 提交信息

feat: 添加用户管理功能
fix: 修复订单状态显示问题
docs: 更新 API 文档
style: 调整按钮样式
refactor: 重构用户状态管理
test: 添加单元测试
chore: 更新依赖包

部署配置

1. 环境配置

环境变量

typescript
// src/env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    VITE_SYSTEM_TYPE: 'admin' | 'store' | 'merchant' | 'consumer'
    VITE_APP_BASE_URL: string
    VITE_APP_TITLE: string
  }
}

配置文件

typescript
// src/config/index.ts
interface Config {
  systemType: string
  apiBaseUrl: string
  appTitle: string
  version: string
}

const config: Config = {
  systemType: import.meta.env.VITE_SYSTEM_TYPE || 'admin',
  apiBaseUrl: import.meta.env.VITE_APP_BASE_URL || 'http://localhost:8080',
  appTitle: import.meta.env.VITE_APP_TITLE || 'DSPlatform',
  version: '3.1.3'
}

export default config

2. 构建配置

Vite 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
import sassImplementation from 'sass'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
      symbolId: '[name]'
    })
  ],
  
  server: {
    port: 8080
  },
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  css: {
    preprocessorOptions: {
      scss: {
        implementation: sassImplementation,
        api: 'modern-compiler',
        sassOptions: {
          warnLegacyJsApi: false
        }
      }
    },
    devSourcemap: true
  },
  
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'static',
    sourcemap: false,
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})

相关链接


最后更新:2024-01-20
维护者:DSPlatform技术团队