Appearance
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技术团队