# 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 ``` #### 使用 scPages 组件 ```vue ``` #### 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 ``` #### 页面生命周期 ```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 ``` ### 14. 性能优化建议 #### 列表性能优化 ```vue ``` #### 图片懒加载 ```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**: 新代码统一使用 `