Skip to content

DSPlatform UniApp 前端架构详解

概述

DSPlatform 前端采用 UniApp 3.0 + Vue 3 + TypeScript + Pinia 技术栈开发,支持多端发布(H5、微信小程序、APP等)。项目采用模块化架构设计,支持多角色、多平台业务场景,包含用户端、商户端、店铺端、骑手端、师傅端、博主端等多个业务模块。

技术栈

核心技术

  • UniApp 3.0: 跨平台开发框架
  • Vue 3.4.21: 渐进式 JavaScript 框架
  • TypeScript 4.9.4: JavaScript 的超集
  • Pinia 2.1.7: Vue 3 状态管理库
  • Vite 5.2.8: 构建工具
  • Sass 1.69.5: CSS 预处理器

依赖库

  • vue-i18n 9.14.5: 国际化支持
  • weixin-js-sdk 1.6.5: 微信 JS SDK
  • qs 6.14.0: 查询字符串解析
  • z-paging: 分页组件

项目结构

目录结构

uniapp/
├── src/
│   ├── api/                    # API 接口定义
│   │   ├── system/            # 系统相关接口
│   │   ├── tbl-store/         # 店铺相关接口
│   │   └── wechat/            # 微信相关接口
│   ├── components/            # 公共组件
│   │   ├── ds-area-picker/    # 地区选择器
│   │   ├── ds-attachment/     # 附件组件
│   │   ├── ds-bottom-nav/     # 底部导航
│   │   ├── ds-empty/          # 空状态组件
│   │   ├── ds-lbs-picker/     # 位置选择器
│   │   ├── ds-map-chooser/    # 地图选择器
│   │   ├── ds-select/         # 选择器组件
│   │   └── ds-video-preview/  # 视频预览
│   ├── home/                  # 用户端模块
│   │   ├── api/              # 用户端 API
│   │   ├── components/       # 用户端组件
│   │   ├── pages/            # 用户端页面
│   │   └── platform/         # 平台相关页面
│   ├── merchant/             # 商户端模块
│   ├── rider/                # 骑手端模块
│   ├── store/                # 店铺端模块
│   ├── technician/           # 师傅端模块
│   ├── video/                # 短视频模块
│   ├── shared/               # 共享模块
│   │   ├── api/              # 共享 API
│   │   └── pages/            # 共享页面
│   ├── stores/               # 状态管理
│   │   └── modules/          # 状态模块
│   ├── utils/                # 工具函数
│   ├── hooks/                # 组合式函数
│   ├── styles/               # 样式文件
│   ├── static/               # 静态资源
│   ├── uni_modules/          # UniApp 插件
│   ├── App.vue               # 应用入口
│   ├── main.ts               # 主入口文件
│   ├── pages.json            # 页面配置
│   └── manifest.json         # 应用配置
├── package.json              # 项目配置
├── vite.config.ts            # Vite 配置
├── tsconfig.json             # TypeScript 配置
└── uni.scss                  # 全局样式

架构设计

整体架构图

┌─────────────────────────────────────────────────────────────┐
│                        应用层                                │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│   用户端     │   商户端     │   店铺端     │     其他端          │
│   Home      │  Merchant   │   Store     │  Rider/Technician  │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

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

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

┌─────────────────────────────────────────────────────────────┐
│                     平台层                                 │
├─────────────┬─────────────┬─────────────┬─────────────────────┤
│     H5      │   小程序     │     APP     │     其他平台        │
│   Web App   │ Mini Program │ Native App  │   Other Platforms  │
└─────────────┴─────────────┴─────────────┴─────────────────────┘

模块化设计

1. 用户端模块 (home)

typescript
// 用户端主要功能
interface HomeModule {
  // 页面
  pages: {
    index: '首页'
    editable: '可编辑页面'
    distributor: '分销相关'
    pointsGoods: '积分商品'
    store: '店铺相关'
    user: '用户中心'
    technician: '师傅相关'
  }
  
  // API
  api: {
    user: '用户相关接口'
    goods: '商品相关接口'
    order: '订单相关接口'
    store: '店铺相关接口'
    trade: '交易相关接口'
  }
  
  // 组件
  components: {
    goods: '商品组件'
    order: '订单组件'
    store: '店铺组件'
    cart: '购物车组件'
  }
}

2. 商户端模块 (merchant)

typescript
// 商户端主要功能
interface MerchantModule {
  pages: {
    index: '商户首页'
    merchant: '商户管理'
  }
  
  api: {
    merchant: '商户相关接口'
    stat: '统计相关接口'
  }
}

3. 店铺端模块 (store)

typescript
// 店铺端主要功能
interface StoreModule {
  pages: {
    index: '店铺首页'
    order: '订单管理'
    setting: '店铺设置'
  }
  
  api: {
    store: '店铺相关接口'
    order: '订单相关接口'
    stat: '统计相关接口'
  }
}

4. 骑手端模块 (rider)

typescript
// 骑手端主要功能
interface RiderModule {
  pages: {
    index: '骑手首页'
    rider: '骑手管理'
    track: '轨迹管理'
  }
  
  api: {
    rider: '骑手相关接口'
    order: '订单相关接口'
    balance: '余额相关接口'
    comment: '评价相关接口'
    track: '轨迹相关接口'
  }
  
  components: {
    locationReporter: '位置上报组件'
  }
}

5. 师傅端模块 (technician)

typescript
// 师傅端主要功能
interface TechnicianModule {
  pages: {
    index: '师傅首页'
    orderDelivery: '订单配送'
    goods: '商品管理'
    balance: '余额管理'
    comment: '评价管理'
    track: '轨迹管理'
    setting: '设置'
  }
  
  api: {
    technician: '师傅相关接口'
    orderDelivery: '订单配送接口'
    goods: '商品相关接口'
    balance: '余额相关接口'
    comment: '评价相关接口'
    track: '轨迹相关接口'
  }
  
  components: {
    locationReporter: '位置上报组件'
  }
}

6. 短视频模块 (video)

typescript
// 短视频模块主要功能
interface VideoModule {
  pages: {
    // 短视频相关页面
  }
  
  api: {
    // 短视频相关接口
  }
  
  components: {
    // 短视频相关组件
  }
}

核心功能实现

1. 请求封装

Request 类

typescript
// src/utils/request.ts
class Request {
  private baseURL: string
  private messageCache: Map<string, { timestamp: number }>
  private isRefreshing: boolean = false
  private refreshSubscribers: Array<(token: string) => void> = []

  constructor() {
    // 设置基础URL
    this.baseURL = this.normalizeBaseUrl(import.meta.env.VITE_APP_BASE_URL || '')
    
    // 初始化消息缓存
    this.messageCache = new Map()
  }

  // 规范化基础URL
  private normalizeBaseUrl(url: string): string {
    return url.endsWith('/') ? url : `${url}/`
  }

  // 处理请求配置
  private processRequestConfig(config: RequestConfig): RequestConfig {
    // 合并默认配置
    const processedConfig: RequestConfig = {
      ...config,
      header: {
        'Content-Type': 'application/json',
        ...config.header
      },
      timeout: config.timeout || 30000
    }

    // 添加访问令牌
    const accessToken = getToken('access_token')
    if (accessToken) {
      processedConfig.header!['access-token'] = accessToken
    }

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

    return processedConfig
  }

  // 发送请求
  async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
    // 处理请求配置
    const processedConfig = this.processRequestConfig(config)
    
    // 发送请求
    const response = await uni.request({
      url: this.baseURL + processedConfig.url,
      method: processedConfig.method || 'GET',
      data: processedConfig.data,
      header: processedConfig.header,
      timeout: processedConfig.timeout,
      dataType: processedConfig.dataType,
      responseType: processedConfig.responseType
    })
    
    // 处理响应
    return this.handleResponse(response, processedConfig)
  }

  // 处理响应
  private async handleResponse(response: any, config: RequestConfig) {
    const { data, statusCode } = response

    // HTTP 状态码检查
    if (statusCode !== 200) {
      throw new Error(`HTTP Error: ${statusCode}`)
    }

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

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

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

    return data
  }

  // Token 刷新处理
  private async handleTokenRefresh(originalConfig: RequestConfig) {
    if (this.isRefreshing) {
      // 等待刷新完成
      return new Promise((resolve) => {
        this.refreshSubscribers.push((token: string) => {
          originalConfig.header!['access-token'] = token
          resolve(this.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.header!['access-token'] = response.data.access_token
      return this.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 })
    
    uni.showToast({
      title: message,
      icon: type === 'success' ? 'success' : 'none',
      duration: 2000
    })
  }

  // 跳转到登录页
  private redirectToLogin() {
    uni.reLaunch({
      url: '/shared/pages/auth/login/index'
    })
  }
}

2. 状态管理

Pinia Store 设计

typescript
// src/stores/user.ts
interface UserSate {
  userInfo: Record<string, any>
  access_token: string | null
  refresh_token: string | null
  temToken: string | null
}

const useUserStore = defineStore('user', {
  state: (): UserSate => ({
    userInfo: {},
    access_token: getToken('access_token') || null,
    refresh_token: getToken('refresh_token') || null,
    temToken: null
  }),

  getters: {
    isLogin: (state) => !!state.access_token
  },

  actions: {
    // 获取用户信息
    getUserInfo(isRefresh: boolean = false) {
      if (this.userInfo.id && !isRefresh) {
        return
      }

      // 获取用户信息以及角色信息
      getUserInfoWithRoles().then((res: any) => {
        this.userInfo = res.data
      }).catch(() => {
        this.logout()
      })
    },

    // 登录后处理
    loginAfter(data: any) {
      this.access_token = data.access_token
      this.refresh_token = data.refresh_token
      this.userInfo = data.userinfo
      
      setToken(data.access_token, 'access_token')
      setToken(data.refresh_token, 'refresh_token')
    },

    // 登出
    logout() {
      // 获取refresh_token用于后端Token失效处理
      const refreshTokenValue = getToken('refresh_token')
      const logoutParams = refreshTokenValue ? { refresh_token: refreshTokenValue } : undefined
      
      // 调用退出登录API
      logout(logoutParams).then(() => {
        // 重置状态
        this.access_token = ''
        this.refresh_token = ''
        this.userInfo = {}
        removeToken('access_token')
        removeToken('refresh_token')
      }).catch(() => {
        // 即使API调用失败也要清理本地状态
        this.access_token = ''
        this.refresh_token = ''
        this.userInfo = {}
        removeToken('access_token')
        removeToken('refresh_token')
      })
    }
  }
})

其他 Store 模块

typescript
// src/stores/modules/config.ts
interface ConfigState {
  configCache: Record<string, string>
  typeCache: Record<string, Record<string, string>>
  loading: boolean
}

export const useConfigStore = defineStore('config', {
  state: (): ConfigState => {
    // 尝试从本地存储初始化数据
    try {
      const configData = storage.get('sys_config')
      const typeData = storage.get('sys_config_types')
      
      if (configData && typeData) {
        return {
          configCache: typeof configData === 'string' ? JSON.parse(configData) : configData,
          typeCache: typeof typeData === 'string' ? JSON.parse(typeData) : typeData,
          loading: false
        }
      }
    } catch (e) {
      console.error('读取本地配置失败', e)
    }
    
    return {
      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) // 1小时过期
        storage.set('sys_config_types', this.typeCache, 3600)
      } catch (e) {
        console.error('保存配置到本地存储失败', e)
      }
    }
  }
})

// src/stores/modules/enum.ts
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)
    }
  }
})

3. 认证系统

认证工具函数

typescript
// src/utils/auth.ts
import storage from './storage'

/**
 * 获取token
 * @param type - token类型,'access_token' 或 'refresh_token'
 */
export function getToken(type: 'access_token' | 'refresh_token'): null | string {
  return storage.get(type)
}

/**
 * 设置token
 * @param token - token值
 * @param type - token类型
 */
export function setToken(token: string, type: 'access_token' | 'refresh_token'): void {
  storage.set(type, token, 7 * 24 * 60 * 60) // 7天过期
}

/**
 * 移除token
 * @param type - token类型
 */
export function removeToken(type: 'access_token' | 'refresh_token'): void {
  storage.remove(type)
}

/**
 * 统一处理认证页面跳转
 * @param type 认证类型:login/register/reset
 * @param redirect 重定向URL
 */
export function navigateToAuth(type: 'login' | 'register' | 'reset', redirect?: string) {
  const pages = {
    login: '/shared/pages/auth/login/index',
    register: '/shared/pages/auth/register/index',
    reset: '/shared/pages/auth/resetpwd/index'
  }

  let url = pages[type]

  // 如果没有提供重定向URL,则获取当前页面URL
  if (!redirect) {
    let systemType = import.meta.env.VITE_APP_SYSTEM_TYPE
    if (systemType == 'user') {
      redirect = '/home/pages/index'
    } else if (systemType == 'rider') {
      redirect = '/rider/pages/index'
    } else if (systemType == 'merchant') {
      redirect = '/merchant/pages/index'
    } else {
      redirect = '/home/pages/index'
    }
  }

  // 添加重定向参数
  url += `?redirect=${encodeURIComponent(redirect)}`

  uni.navigateTo({ url })
}

/**
 * 获取当前页面完整路径(包含参数)
 */
export function getCurrentPageUrl(): string {
  const pages = getCurrentPages()
  const currentPage = pages[pages.length - 1]
  const url = `/${currentPage.route}`

  const queryParams = currentPage.options
  if (Object.keys(queryParams).length > 0) {
    const queryString = Object.keys(queryParams)
      .map(key => `${key}=${queryParams[key]}`)
      .join('&')
    return `${url}?${queryString}`
  }

  return url
}

4. 路由配置

pages.json 配置

json
{
  "pages": [
    {
      "path": "home/pages/index",
      "style": {
        "navigationBarTitleText": "首页"
      }
    }
  ],
  "subPackages": [
    {
      "root": "shared/pages",
      "pages": [
        {
          "path": "auth/login/index",
          "style": {
            "navigationBarTitleText": "登录"
          }
        }
      ]
    },
    {
      "root": "home/pages",
      "pages": [
        {
          "path": "user/profile/index",
          "style": {
            "navigationBarTitleText": "个人资料"
          }
        }
      ]
    }
  ],
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "home/pages/index",
        "iconPath": "static/tab-home.png",
        "selectedIconPath": "static/tab-home-current.png",
        "text": "首页"
      }
    ]
  }
}

5. 组件设计

公共组件示例

vue
<!-- src/components/ds-empty/index.vue -->
<template>
  <view class="ds-empty">
    <image :src="image || defaultImage" class="empty-image" mode="aspectFit" />
    <text class="empty-text">{{ text }}</text>
    <view class="empty-description" v-if="description">
      <text class="description-text">{{ description }}</text>
    </view>
    <view class="empty-action" v-if="showButton">
      <button class="action-button" :class="buttonType" @click="handleButtonClick">
        {{ buttonText }}
      </button>
    </view>
  </view>
</template>

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

defineOptions({
  name: 'DsEmpty'
})

// 组件接收的属性
const props = defineProps({
  // 空状态图片
  image: {
    type: String,
    default: ''
  },
  // 空状态文字
  text: {
    type: String,
    default: '暂无数据'
  },
  // 附加描述文字
  description: {
    type: String,
    default: ''
  },
  // 是否显示按钮
  showButton: {
    type: Boolean,
    default: false
  },
  // 按钮文字
  buttonText: {
    type: String,
    default: '点击刷新'
  },
  // 按钮类型:primary, secondary, plain
  buttonType: {
    type: String,
    default: 'primary'
  }
})

const emit = defineEmits(['buttonClick'])

// 默认图片
const defaultImage = ref('/static/images/empty/default.png')

const handleButtonClick = () => {
  emit('buttonClick')
}
</script>

<style lang="scss" scoped>
.ds-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60rpx 40rpx;
  
  .empty-image {
    width: 200rpx;
    height: 200rpx;
    margin-bottom: 30rpx;
  }
  
  .empty-text {
    font-size: 28rpx;
    color: #999;
    margin-bottom: 20rpx;
  }
  
  .empty-description {
    margin-bottom: 30rpx;
    
    .description-text {
      font-size: 24rpx;
      color: #ccc;
      text-align: center;
    }
  }
  
  .empty-action {
    .action-button {
      padding: 20rpx 40rpx;
      border-radius: 8rpx;
      font-size: 28rpx;
      
      &.primary {
        background-color: #007aff;
        color: white;
      }
      
      &.secondary {
        background-color: #f0f0f0;
        color: #333;
      }
      
      &.plain {
        background-color: transparent;
        color: #007aff;
        border: 1rpx solid #007aff;
      }
    }
  }
}
</style>

6. 工具函数

通用工具函数

typescript
// src/utils/util.ts
/**
 * 格式化时间
 * @param timestamp - 时间戳
 * @param format - 格式
 */
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 - 小数位数
 */
export function formatPrice(price: number, decimals: number = 2): string {
  return Number(price).toFixed(decimals)
}

/**
 * 防抖函数
 * @param func - 函数
 * @param delay - 延迟时间
 */
export function debounce<T extends (...args: any[]) => any>(
  func: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: NodeJS.Timeout
  
  return (...args: Parameters<T>) => {
    clearTimeout(timeoutId)
    timeoutId = setTimeout(() => func(...args), delay)
  }
}

/**
 * 节流函数
 * @param func - 函数
 * @param delay - 延迟时间
 */
export function throttle<T extends (...args: any[]) => any>(
  func: T,
  delay: number
): (...args: Parameters<T>) => void {
  let lastCall = 0
  
  return (...args: Parameters<T>) => {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      func(...args)
    }
  }
}

/**
 * 深度克隆对象
 * @param obj - 要克隆的对象
 */
export function deepClone<T>(obj: T): T {
  if (obj === null || typeof obj !== 'object') {
    return obj
  }
  
  if (obj instanceof Date) {
    return new Date(obj.getTime()) as T
  }
  
  if (obj instanceof Array) {
    return obj.map(item => deepClone(item)) as T
  }
  
  if (typeof obj === 'object') {
    const clonedObj = {} as T
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        clonedObj[key] = deepClone(obj[key])
      }
    }
    return clonedObj
  }
  
  return obj
}

平台工具函数

typescript
// src/utils/platform.ts
/**
 * 获取平台信息
 */
export function getPlatformInfo() {
  const systemInfo = uni.getSystemInfoSync()
  return {
    platform: systemInfo.platform,
    system: systemInfo.system,
    version: systemInfo.version,
    model: systemInfo.model,
    brand: systemInfo.brand
  }
}

/**
 * 检查是否为微信小程序
 */
export function isWeixinMiniProgram(): boolean {
  return uni.getSystemInfoSync().platform === 'devtools' || 
         uni.getSystemInfoSync().platform === 'ios' || 
         uni.getSystemInfoSync().platform === 'android'
}

/**
 * 检查是否为 H5
 */
export function isH5(): boolean {
  return process.env.UNI_PLATFORM === 'h5'
}

/**
 * 检查是否为 APP
 */
export function isApp(): boolean {
  return process.env.UNI_PLATFORM === 'app-plus'
}

/**
 * 获取系统类型
 */
export function getSystemType(): string {
  return import.meta.env.VITE_APP_SYSTEM_TYPE || 'user'
}

/**
 * 检查是否为指定系统类型
 */
export function isSystemType(type: string): boolean {
  return getSystemType() === type
}

7. 样式系统

全局样式

scss
// src/styles/index.scss
@import './common.scss';

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

page {
  background-color: #f5f5f5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

// 通用类
.ds-container {
  padding: 0 30rpx;
}

.ds-card {
  background-color: white;
  border-radius: 16rpx;
  padding: 30rpx;
  margin-bottom: 20rpx;
}

.ds-button {
  background-color: #007aff;
  color: white;
  border-radius: 8rpx;
  padding: 20rpx 40rpx;
  border: none;
  font-size: 28rpx;
  
  &.disabled {
    background-color: #ccc;
    color: #999;
  }
}

.ds-input {
  border: 1rpx solid #ddd;
  border-radius: 8rpx;
  padding: 20rpx;
  font-size: 28rpx;
  
  &:focus {
    border-color: #007aff;
  }
}

通用样式

scss
// src/styles/common.scss
// 颜色变量
$primary-color: #007aff;
$success-color: #4cd964;
$warning-color: #f0ad4e;
$danger-color: #dd524d;
$info-color: #909399;

// 尺寸变量
$border-radius: 8rpx;
$border-radius-lg: 16rpx;
$spacing-xs: 10rpx;
$spacing-sm: 20rpx;
$spacing-md: 30rpx;
$spacing-lg: 40rpx;
$spacing-xl: 60rpx;

// 字体大小
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-md: 28rpx;
$font-size-lg: 32rpx;
$font-size-xl: 36rpx;

// 阴影
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
$shadow-md: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
$shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);

多端适配

1. 平台差异处理

条件编译

vue
<template>
  <view class="page">
    <!-- #ifdef H5 -->
    <view class="h5-only">H5 专用内容</view>
    <!-- #endif -->
    
    <!-- #ifdef MP-WEIXIN -->
    <view class="weixin-only">微信小程序专用内容</view>
    <!-- #endif -->
    
    <!-- #ifdef APP-PLUS -->
    <view class="app-only">APP 专用内容</view>
    <!-- #endif -->
  </view>
</template>

<script setup lang="ts">
// #ifdef H5
import { h5SpecificFunction } from '@/utils/h5'
// #endif

// #ifdef MP-WEIXIN
import { weixinSpecificFunction } from '@/utils/weixin'
// #endif

// #ifdef APP-PLUS
import { appSpecificFunction } from '@/utils/app'
// #endif
</script>

平台特定样式

scss
// 微信小程序样式
/* #ifdef MP-WEIXIN */
.weixin-specific {
  padding: 20rpx;
}
/* #endif */

// H5 样式
/* #ifdef H5 */
.h5-specific {
  padding: 10px;
}
/* #endif */

// APP 样式
/* #ifdef APP-PLUS */
.app-specific {
  padding: 20rpx;
}
/* #endif */

2. 响应式设计

屏幕适配

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

// 媒体查询
@media screen and (max-width: $breakpoint-sm) {
  .container {
    padding: 20rpx;
  }
}

@media screen and (min-width: $breakpoint-md) {
  .container {
    padding: 40rpx;
  }
}

性能优化

1. 代码分割

分包配置

json
{
  "subPackages": [
    {
      "root": "home/pages",
      "pages": [
        "user/profile/index",
        "user/settings/index"
      ]
    },
    {
      "root": "merchant/pages",
      "pages": [
        "merchant/index",
        "merchant/settings/index"
      ]
    }
  ]
}

2. 图片优化

图片懒加载

vue
<template>
  <image 
    :src="imageSrc" 
    :lazy-load="true"
    mode="aspectFit"
    @load="handleImageLoad"
    @error="handleImageError"
  />
</template>

<script setup lang="ts">
const handleImageLoad = () => {
  console.log('图片加载完成')
}

const handleImageError = () => {
  console.log('图片加载失败')
}
</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/           # 组件目录
│   ├── ds-button/       # 组件名使用 kebab-case
│   │   ├── index.vue    # 主组件文件
│   │   └── types.ts     # 类型定义
│   └── ds-input/
├── pages/               # 页面目录
│   ├── user/           # 模块名使用 kebab-case
│   │   ├── profile/    # 页面名使用 kebab-case
│   │   │   └── index.vue
│   │   └── settings/
├── 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 {
    NODE_ENV: 'development' | 'production' | 'test'
    UNI_PLATFORM: 'h5' | 'mp-weixin' | 'app-plus'
    VITE_API_BASE_URL: string
    VITE_APP_TITLE: string
  }
}

配置文件

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

const config: Config = {
  apiBaseUrl: process.env.VITE_API_BASE_URL || 'http://localhost:8080',
  appTitle: process.env.VITE_APP_TITLE || 'DSPlatform',
  version: '3.1.3'
}

export default config

2. 构建配置

Vite 配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'

export default defineConfig({
  plugins: [uni()],
  
  // 开发服务器配置
  server: {
    port: 3000,
    host: '0.0.0.0',
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'static',
    sourcemap: false,
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})

相关链接


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