# UniApp 移动端项目开发规范
## 项目概述
本项目是一个基于 UniApp 框架开发的跨平台移动应用,支持 H5、小程序、App 多端运行,采用 Vue 3 + Composition API 开发模式。
## 技术栈
- **UniApp**: 跨平台应用开发框架
- **Vue 3**: 渐进式 JavaScript 框架
- **JavaScript**: 主要开发语言(非 TypeScript)
- **Composition API**: Vue 3 组合式 API 开发模式
- **Pinia**: 状态管理库
- **easycom**: UniApp 组件自动引入机制
- **luch-request**: 基于 Promise 的 HTTP 请求库
- **mp-html**: 富文本渲染组件
- **uni.scss**: 全局样式变量
## 开发规范
### 1. 项目结构
```
resources/mobile/
├── pages/ # 页面目录
│ ├── index/ # 首页模块
│ │ └── index.vue # 首页
│ ├── auth/ # 认证相关页面
│ │ ├── login/ # 登录页
│ │ ├── register/ # 注册页
│ │ └── forgot-password/ # 忘记密码
│ ├── user/ # 用户中心页面
│ │ ├── profile/ # 个人资料
│ │ ├── settings/ # 设置
│ │ └── address/ # 地址管理
│ ├── shop/ # 商城模块
│ │ ├── product/ # 商品相关
│ │ ├── cart/ # 购物车
│ │ └── order/ # 订单相关
│ └── common/ # 公共页面
│ ├── webview/ # Webview 页面
│ └── error/ # 错误页面
├── components/ # 公共组件目录
│ ├── sc-button/ # 自定义按钮组件
│ ├── sc-card/ # 自定义卡片组件
│ ├── sc-list/ # 自定义列表组件
│ └── sc-upload/ # 上传组件
├── static/ # 静态资源
│ ├── images/ # 图片资源
│ ├── icons/ # 图标资源
│ └── fonts/ # 字体资源
├── api/ # API 接口层
│ ├── index.js # API 入口
│ ├── request.js # 请求封装
│ ├── auth.js # 认证相关接口
│ ├── user.js # 用户相关接口
│ └── shop.js # 商城相关接口
├── stores/ # 状态管理
│ ├── index.js # Store 入口
│ ├── user.js # 用户状态
│ ├── cart.js # 购物车状态
│ └── config.js # 配置状态
├── utils/ # 工具函数
│ ├── index.js # 工具函数入口
│ ├── validate.js # 表单验证
│ ├── format.js # 格式化工具
│ └── storage.js # 存储工具
├── config/ # 配置文件
│ ├── index.js # 主配置
│ ├── env.js # 环境配置
│ └── const.js # 常量配置
├── styles/ # 样式文件
│ ├── common.scss # 公共样式
│ ├── variables.scss # 样式变量
│ └── mixins.scss # 样式混入
├── App.vue # 根组件
├── main.js # 入口文件
├── manifest.json # 应用配置
├── pages.json # 页面路由配置
├── uni.scss # 全局样式变量
├── package.json # 依赖配置
└── README.md # 项目说明
```
### 2. 页面开发规范
#### 页面命名规范
- **目录命名**: 使用 kebab-case,如 `user-profile`, `product-list`
- **文件命名**: 使用 index.vue 作为入口文件
- **路径格式**: `pages/{module}/{feature}/index.vue`
#### scPages 页面框架组件
**重要提示**: 所有页面必须使用 `scPages` 组件作为页面框架,确保统一的页面布局和交互体验。
#### scPages 组件结构
```vue
←
{{ title }}
```
#### 使用 scPages 组件
```vue
share
{{ userInfo.name }}
{{ userInfo.role }}
{{ item.title }}
```
#### scPages 组件配置选项
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| title | String | '' | 页面标题 |
| showNavbar | Boolean | true | 是否显示导航栏 |
| showBack | Boolean | true | 是否显示返回按钮 |
| navbarBgColor | String | '#fff' | 导航栏背景色 |
| navbarTextColor | String | '#333' | 导航栏文字颜色 |
| bgColor | String | '#f5f5f5' | 页面背景色 |
#### 插槽
| 插槽名 | 说明 |
|--------|------|
| default | 页面内容 |
| navbar-right | 导航栏右侧内容 |
| footer | 底部操作栏 |
#### 事件
| 事件名 | 说明 | 返回值 |
|--------|------|--------|
| back | 返回上一页 | - |
| scrollToUpper | 滚动到顶部 | - |
| scrollToLower | 滚动到底部 | - |
#### 方法
| 方法名 | 说明 | 参数 |
|--------|------|------|
| scrollToTop | 滚动到指定位置 | top (默认: 0) |
#### 页面结构模板
```vue
back
{{ pageTitle }}
```
#### 页面生命周期
```javascript
```
### 3. 组件引入规范(easycom)
**重要提示**: 项目使用 UniApp 的 easycom 组件自动引入机制,无需手动 import 和注册组件,直接在模板中使用即可。
#### easycom 配置
在 `pages.json` 中配置 easycom:
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^sc-(.*)": "@/components/sc-$1/index.vue"
}
}
}
```
#### 组件使用
```vue
按钮
卡片内容
```
#### 组件命名规范
- **目录命名**: 使用 PascalCase,如 `ScButton`, `ScCard`
- **文件命名**: 使用 index.vue 作为入口文件
- **组件使用**: 在模板中使用 kebab-case,如 ``
- **前缀规范**: 自定义组件统一使用 `sc-` 前缀
#### 组件开发规范
#### 组件结构模板
```vue
```
### 4. 路由开发规范
#### 页面路由配置
在 `pages.json` 中配置页面路由:
```json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
{
"path": "pages/auth/login/index",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/user/profile/index",
"style": {
"navigationBarTitleText": "个人中心",
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "UniApp",
"navigationBarBackgroundColor": "#FFFFFF",
"backgroundColor": "#F5F5F5"
},
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/icons/home.png",
"selectedIconPath": "static/icons/home-active.png"
},
{
"pagePath": "pages/shop/index/index",
"text": "商城",
"iconPath": "static/icons/shop.png",
"selectedIconPath": "static/icons/shop-active.png"
},
{
"pagePath": "pages/user/profile/index",
"text": "我的",
"iconPath": "static/icons/user.png",
"selectedIconPath": "static/icons/user-active.png"
}
]
}
}
```
#### 路由跳转
```javascript
// 保留当前页面,跳转到应用内的某个页面
uni.navigateTo({
url: '/pages/user/detail/index?id=123'
})
// 关闭当前页面,跳转到应用内的某个页面
uni.redirectTo({
url: '/pages/auth/login/index'
})
// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
uni.switchTab({
url: '/pages/index/index'
})
// 关闭所有页面,打开到应用内的某个页面
uni.reLaunch({
url: '/pages/index/index'
})
// 返回上一页
uni.navigateBack({
delta: 1
})
// 获取页面参数
onLoad((options) => {
const { id, type } = options
})
```
#### 页面传参
```javascript
// 传递参数
uni.navigateTo({
url: `/pages/product/detail/index?id=${productId}&type=1`
})
// 或者使用对象形式
const params = {
id: productId,
type: 1,
name: '商品名称'
}
uni.navigateTo({
url: `/pages/product/detail/index?data=${encodeURIComponent(JSON.stringify(params))}`
})
// 接收参数
onLoad((options) => {
if (options.data) {
const data = JSON.parse(decodeURIComponent(options.data))
console.log(data)
}
})
```
### 5. API 接口开发规范(luch-request)
#### API 文件组织
每个业务模块对应一个 API 文件,统一放在 `api/` 目录下。
```javascript
// api/auth.js
import http from './http'
export default {
// 登录
login: (data) => {
return http.post('/api/auth/login', data)
},
// 注册
register: (data) => {
return http.post('/api/auth/register', data)
},
// 登出
logout: () => {
return http.post('/api/auth/logout')
},
// 获取用户信息
getUserInfo: () => {
return http.get('/api/auth/user')
},
// 刷新令牌
refreshToken: (refreshToken) => {
return http.post('/api/auth/refresh', { refresh_token: refreshToken })
}
}
```
#### luch-request 封装
```javascript
// api/http.js
import Request from 'luch-request'
import config from '@/config'
import { useUserStore } from '@/stores/user'
const http = new Request({
baseURL: config.apiBaseUrl,
timeout: 60000,
header: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
http.interceptors.request.use((config) => {
// 添加 Token
const userStore = useUserStore()
if (userStore.token) {
config.header = {
...config.header,
'Authorization': `Bearer ${userStore.token}`
}
}
// 添加时间戳防止缓存
if (config.method === 'GET') {
config.params = {
...config.params,
_t: Date.now()
}
}
// 显示加载提示
if (config.loading !== false) {
uni.showLoading({
title: config.loadingText || '加载中...',
mask: true
})
}
return config
}, (error) => {
return Promise.reject(error)
})
// 响应拦截器
http.interceptors.response.use((response) => {
// 隐藏加载提示
uni.hideLoading()
const { statusCode, data } = response
// HTTP 状态码判断
if (statusCode !== 200) {
handleError(statusCode)
return Promise.reject(response)
}
// 业务状态码判断
if (data.code !== 200) {
handleBusinessError(data)
return Promise.reject(data)
}
return data
}, (error) => {
// 隐藏加载提示
uni.hideLoading()
// 网络错误处理
if (error.errMsg) {
uni.showToast({
title: error.errMsg || '网络错误',
icon: 'none'
})
}
return Promise.reject(error)
})
// 错误处理
const handleError = (statusCode) => {
let message = '网络错误'
switch (statusCode) {
case 401:
message = '未授权,请登录'
// 跳转到登录页
uni.navigateTo({
url: '/pages/auth/login/index'
})
break
case 403:
message = '拒绝访问'
break
case 404:
message = '请求错误,未找到该资源'
break
case 500:
message = '服务器错误'
break
}
uni.showToast({
title: message,
icon: 'none'
})
}
const handleBusinessError = (data) => {
uni.showToast({
title: data.message || '操作失败',
icon: 'none'
})
}
export default http
```
#### 使用示例
```javascript
```
### 6. 状态管理规范
#### Pinia Store 定义
使用组合式 API 定义 Store。
```javascript
// stores/user.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
// State
const token = ref('')
const refreshToken = ref('')
const userInfo = ref(null)
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name || '')
// Actions
function setToken(newToken) {
token.value = newToken
uni.setStorageSync('token', newToken)
}
function setUserInfo(info) {
userInfo.value = info
uni.setStorageSync('userInfo', info)
}
function clearUserData() {
token.value = ''
refreshToken.value = ''
userInfo.value = null
uni.removeStorageSync('token')
uni.removeStorageSync('refreshToken')
uni.removeStorageSync('userInfo')
}
// 从本地存储初始化
function initFromStorage() {
const savedToken = uni.getStorageSync('token')
const savedUserInfo = uni.getStorageSync('userInfo')
if (savedToken) {
token.value = savedToken
}
if (savedUserInfo) {
userInfo.value = savedUserInfo
}
}
return {
token,
refreshToken,
userInfo,
isLoggedIn,
userName,
setToken,
setUserInfo,
clearUserData,
initFromStorage
}
})
```
#### Store 使用
```javascript
```
### 7. 工具函数开发规范
#### 常用工具函数
```javascript
// utils/index.js
// 深拷贝
export const deepClone = (obj) => {
return JSON.parse(JSON.stringify(obj))
}
// 防抖
export const debounce = (fn, delay = 300) => {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 节流
export const throttle = (fn, delay = 300) => {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
// 格式化时间
export const formatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
// 格式化金额
export const formatMoney = (amount, decimals = 2) => {
return Number(amount).toFixed(decimals)
}
// 手机号脱敏
export const maskPhone = (phone) => {
if (!phone) return ''
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
// 判断是否为空
export const isEmpty = (value) => {
if (value === null || value === undefined) return true
if (typeof value === 'string') return value.trim() === ''
if (Array.isArray(value)) return value.length === 0
if (typeof value === 'object') return Object.keys(value).length === 0
return false
}
```
#### 表单验证工具
```javascript
// utils/validate.js
// 手机号验证
export const isPhone = (phone) => {
return /^1[3-9]\d{9}$/.test(phone)
}
// 邮箱验证
export const isEmail = (email) => {
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)
}
// 身份证验证
export const isIdCard = (idCard) => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(idCard)
}
// 密码强度验证
export const checkPasswordStrength = (password) => {
let strength = 0
if (password.length >= 8) strength++
if (/[A-Z]/.test(password)) strength++
if (/[a-z]/.test(password)) strength++
if (/[0-9]/.test(password)) strength++
if (/[^A-Za-z0-9]/.test(password)) strength++
return strength
}
```
### 8. 样式开发规范
#### 使用 uni.scss 变量
```scss
// uni.scss 已内置以下变量,可以直接使用
/* 颜色变量 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333;
$uni-text-color-grey: #999;
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #ffffff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1;
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
$uni-spacing-col-sm: 5px;
$uni-spacing-col-base: 10px;
$uni-spacing-col-lg: 15px;
/* 字体大小 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16px;
$uni-font-size-title: 18px;
/* 图片尺寸 */
$uni-img-size-sm: 40px;
$uni-img-size-base: 52px;
$uni-img-size-lg: 80px;
/* Border Radius */
$uni-border-radius-sm: 4px;
$uni-border-radius-base: 6px;
$uni-border-radius-lg: 12px;
$uni-border-radius-circle: 50%;
/* 阴影 */
$uni-shadow-sm: 0 0 5px rgba(0, 0, 0, 0.1);
$uni-shadow-base: 0 1px 8px 1px rgba(0, 0, 0, 0.1);
$uni-shadow-lg: 0 4px 16px 1px rgba(0, 0, 0, 0.15);
```
#### 样式组织
```vue
```
#### 响应式单位
```scss
// 使用 rpx 单位实现屏幕适配
.container {
width: 750rpx; // 等于屏幕宽度
padding: 30rpx; // 根据屏幕宽度自动缩放
font-size: 28rpx;
}
// 混合使用 px 和 rpx
.button {
font-size: 28rpx;
border-radius: 8px; // 边框圆角建议使用 px
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); // 阴影建议使用 px
}
```
### 9. 本地存储规范
#### 使用 uni.storage
```javascript
// 存储数据
uni.setStorageSync('token', 'xxx')
uni.setStorageSync('userInfo', { name: '张三' })
// 异步存储
uni.setStorage({
key: 'token',
data: 'xxx',
success: () => {
console.log('存储成功')
}
})
// 读取数据
const token = uni.getStorageSync('token')
const userInfo = uni.getStorageSync('userInfo')
// 异步读取
uni.getStorage({
key: 'token',
success: (res) => {
console.log(res.data)
}
})
// 删除数据
uni.removeStorageSync('token')
// 异步删除
uni.removeStorage({
key: 'token',
success: () => {
console.log('删除成功')
}
})
// 清空所有数据
uni.clearStorageSync()
// 获取所有存储信息
uni.getStorageInfo({
success: (res) => {
console.log(res.keys)
console.log(res.currentSize)
console.log(res.limitSize)
}
})
```
#### 封装存储工具
```javascript
// utils/storage.js
const STORAGE_PREFIX = 'app_'
export default {
// 设置
set(key, value) {
try {
uni.setStorageSync(STORAGE_PREFIX + key, JSON.stringify(value))
return true
} catch (e) {
console.error('存储失败', e)
return false
}
},
// 获取
get(key, defaultValue = null) {
try {
const value = uni.getStorageSync(STORAGE_PREFIX + key)
if (value) {
return JSON.parse(value)
}
return defaultValue
} catch (e) {
console.error('读取失败', e)
return defaultValue
}
},
// 删除
remove(key) {
try {
uni.removeStorageSync(STORAGE_PREFIX + key)
return true
} catch (e) {
console.error('删除失败', e)
return false
}
},
// 清空
clear() {
try {
uni.clearStorageSync()
return true
} catch (e) {
console.error('清空失败', e)
return false
}
}
}
```
### 10. 富文本渲染规范(mp-html)
#### mp-html 简介
mp-html 是一个强大的富文本渲染组件,支持 HTML 标签和样式,适用于在 UniApp 中渲染富文本内容。
#### 安装依赖
```bash
npm install mp-html
```
#### 基本使用
```vue
```
#### 高级配置
```vue
```
#### 使用场景
1. **文章详情页**: 渲染后端返回的富文本内容
2. **商品描述**: 展示商品的详细描述
3. **公告通知**: 显示富文本格式的公告
4. **帮助文档**: 渲染帮助文档内容
#### 注意事项
1. **安全性**: 后端返回的 HTML 内容应经过过滤和消毒,防止 XSS 攻击
2. **性能**: 避免在列表中大量使用 mp-html,会影响性能
3. **样式**: 建议使用 `tag-style` 配置标签样式,而不是全局样式
4. **图片**: 处理图片点击事件,提供图片预览功能
5. **链接**: 处理链接点击事件,跳转到 webview 页面
### 11. 图片资源规范
#### 图片路径引用
```vue
```
#### 图片模式
```vue
```
### 11. 上传文件规范
#### 使用 uni.chooseImage
```javascript
// 选择图片
const chooseImage = () => {
uni.chooseImage({
count: 9, // 最多选择的图片数量
sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图
sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机
success: (res) => {
const tempFilePaths = res.tempFilePaths
console.log(tempFilePaths)
// 上传图片
uploadImages(tempFilePaths)
}
})
}
// 上传图片
const uploadImages = (files) => {
files.forEach((filePath, index) => {
uni.uploadFile({
url: 'https://example.com/api/upload',
filePath,
name: 'file',
formData: {
'type': 'avatar'
},
header: {
'Authorization': `Bearer ${userStore.token}`
},
success: (uploadFileRes) => {
const data = JSON.parse(uploadFileRes.data)
console.log('上传成功', data)
},
fail: (error) => {
console.error('上传失败', error)
}
})
})
}
```
### 12. 常用 API 规范
#### 网络请求
```javascript
// GET 请求
uni.request({
url: 'https://example.com/api/data',
method: 'GET',
data: {
page: 1,
pageSize: 10
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
console.log(res.data)
}
})
// POST 请求
uni.request({
url: 'https://example.com/api/save',
method: 'POST',
data: {
name: '张三',
age: 18
},
header: {
'Content-Type': 'application/json'
},
success: (res) => {
console.log(res.data)
}
})
```
#### 提示框
```javascript
// 显示提示框
uni.showToast({
title: '操作成功',
icon: 'success',
duration: 2000
})
// 显示加载中
uni.showLoading({
title: '加载中...',
mask: true
})
// 隐藏加载中
uni.hideLoading()
// 显示确认对话框
uni.showModal({
title: '提示',
content: '确定要删除吗?',
success: (res) => {
if (res.confirm) {
console.log('用户点击确定')
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
// 显示操作菜单
uni.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: (res) => {
console.log(res.tapIndex)
}
})
```
#### 导航栏
```javascript
// 设置导航栏标题
uni.setNavigationBarTitle({
title: '新的标题'
})
// 设置导航栏颜色
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000'
})
// 显示导航栏加载动画
uni.showNavigationBarLoading()
// 隐藏导航栏加载动画
uni.hideNavigationBarLoading()
```
#### 下拉刷新
```javascript
// 开启下拉刷新
uni.startPullDownRefresh()
// 停止下拉刷新
uni.stopPullDownRefresh()
// 在页面中配置下拉刷新
// pages.json
{
"path": "pages/index/index",
"style": {
"enablePullDownRefresh": true,
"backgroundColor": "#f5f5f5",
"backgroundTextStyle": "dark"
}
}
// 监听下拉刷新
onPullDownRefresh(() => {
console.log('下拉刷新')
// 执行刷新逻辑
setTimeout(() => {
uni.stopPullDownRefresh()
}, 1000)
})
```
### 13. 跨平台兼容性处理
#### 条件编译
```javascript
// #ifdef H5
console.log('只在 H5 平台执行')
// #endif
// #ifdef MP-WEIXIN
console.log('只在微信小程序执行')
// #endif
// #ifdef APP-PLUS
console.log('只在 App 平台执行')
// #endif
// #ifndef H5
console.log('除了 H5 平台都执行')
// #endif
```
```vue
H5 专属内容
小程序专属内容
App 专属内容
```
### 14. 性能优化建议
#### 列表性能优化
```vue
{{ item.name }}
```
#### 图片懒加载
```vue
```
#### 避免频繁更新
```javascript
// 使用防抖避免频繁触发
import { debounce } from '@/utils'
const handleSearch = debounce((keyword) => {
console.log('搜索:', keyword)
}, 300)
```
### 15. 常用命令
```bash
# 安装依赖
npm install
# 安装 luch-request(HTTP 请求库)
npm install luch-request
# 安装 mp-html(富文本渲染组件)
npm install mp-html
# 安装 pinia(状态管理库)
npm install pinia
# 开发环境运行 H5
npm run dev:h5
# 开发环境运行微信小程序
npm run dev:mp-weixin
# 开发环境运行 App
npm run dev:app
# 生产环境构建 H5
npm run build:h5
# 生产环境构建微信小程序
npm run build:mp-weixin
# 生产环境构建 App
npm run build:app
```
### 16. 注意事项
1. **使用 Composition API**: 新代码统一使用 `