Files
laravel_swoole/.clinerules/mobile-rule.md
T
2026-02-21 14:47:54 +08:00

2032 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
<template>
<view class="sc-pages">
<!-- 自定义导航栏可选 -->
<view v-if="showNavbar" class="sc-pages-navbar" :style="navbarStyle">
<view class="sc-pages-navbar-left">
<view v-if="showBack" class="sc-pages-back" @click="handleBack">
<text class="iconfont"></text>
</view>
</view>
<view class="sc-pages-navbar-title">{{ title }}</view>
<view class="sc-pages-navbar-right">
<slot name="navbar-right"></slot>
</view>
</view>
<!-- 页面内容区域 -->
<scroll-view
class="sc-pages-content"
scroll-y
:scroll-top="scrollTop"
@scrolltoupper="handleScrollToUpper"
@scrolltolower="handleScrollToLower"
>
<slot></slot>
</scroll-view>
<!-- 底部操作栏可选 -->
<view v-if="$slots.footer" class="sc-pages-footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
// 页面标题
title: {
type: String,
default: ''
},
// 是否显示导航栏
showNavbar: {
type: Boolean,
default: true
},
// 是否显示返回按钮
showBack: {
type: Boolean,
default: true
},
// 导航栏背景色
navbarBgColor: {
type: String,
default: '#fff'
},
// 导航栏文字颜色
navbarTextColor: {
type: String,
default: '#333'
},
// 页面背景色
bgColor: {
type: String,
default: '#f5f5f5'
}
})
const emit = defineEmits(['back', 'scrollToUpper', 'scrollToLower'])
// 滚动位置
const scrollTop = ref(0)
// 计算导航栏样式
const navbarStyle = computed(() => ({
backgroundColor: props.navbarBgColor,
color: props.navbarTextColor
}))
// 返回上一页
const handleBack = () => {
emit('back')
uni.navigateBack()
}
// 滚动到顶部
const handleScrollToUpper = () => {
emit('scrollToUpper')
}
// 滚动到底部
const handleScrollToLower = () => {
emit('scrollToLower')
}
// 滚动到指定位置
const scrollToTop = (top = 0) => {
scrollTop.value = top
}
// 暴露方法给父组件
defineExpose({
scrollToTop
})
</script>
<style scoped lang="scss">
.sc-pages {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
&-navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #e5e5e5;
position: relative;
z-index: 100;
&-left {
display: flex;
align-items: center;
width: 60px;
}
&-back {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
transition: background 0.3s;
&:active {
background: rgba(0, 0, 0, 0.05);
}
.iconfont {
font-size: 20px;
}
}
&-title {
flex: 1;
font-size: 18px;
font-weight: 500;
text-align: center;
}
&-right {
display: flex;
align-items: center;
width: 60px;
justify-content: flex-end;
}
}
&-content {
flex: 1;
overflow: hidden;
}
&-footer {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #e5e5e5;
}
}
</style>
```
#### 使用 scPages 组件
```vue
<template>
<sc-pages
title="用户详情"
:show-navbar="true"
:show-back="true"
@back="handleBack"
@scrollToUpper="handleRefresh"
@scrollToLower="handleLoadMore"
>
<!-- 导航栏右侧按钮 -->
<template #navbar-right>
<view class="navbar-action" @click="handleShare">
<text class="iconfont">share</text>
</view>
</template>
<!-- 页面内容 -->
<view class="page-content">
<view class="user-info">
<image class="avatar" :src="userInfo.avatar" mode="aspectFill" />
<view class="user-name">{{ userInfo.name }}</view>
<view class="user-role">{{ userInfo.role }}</view>
</view>
<!-- 列表内容 -->
<view class="user-list">
<view v-for="item in list" :key="item.id" class="list-item">
{{ item.title }}
</view>
</view>
</view>
<!-- 底部操作栏 -->
<template #footer>
<view class="footer-actions">
<button class="btn-primary" @click="handleEdit">编辑</button>
<button class="btn-danger" @click="handleDelete">删除</button>
</view>
</template>
</sc-pages>
</template>
<script setup>
import { ref } from 'vue'
const userInfo = ref({
avatar: 'https://example.com/avatar.jpg',
name: '张三',
role: '管理员'
})
const list = ref([
{ id: 1, title: '个人信息' },
{ id: 2, title: '修改密码' },
{ id: 3, title: '权限设置' }
])
const handleBack = () => {
console.log('返回上一页')
}
const handleShare = () => {
console.log('分享')
}
const handleEdit = () => {
console.log('编辑用户')
}
const handleDelete = () => {
console.log('删除用户')
}
const handleRefresh = () => {
console.log('刷新数据')
}
const handleLoadMore = () => {
console.log('加载更多')
}
</script>
<style scoped lang="scss">
.page-content {
padding: 16px;
.user-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 32px 0;
background: #fff;
border-radius: 8px;
margin-bottom: 16px;
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 16px;
}
.user-name {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.user-role {
font-size: 14px;
color: #999;
}
}
.user-list {
background: #fff;
border-radius: 8px;
padding: 16px;
.list-item {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
color: #333;
&:last-child {
border-bottom: none;
}
}
}
}
.footer-actions {
display: flex;
gap: 12px;
button {
flex: 1;
height: 44px;
border-radius: 8px;
font-size: 16px;
border: none;
}
.btn-primary {
background: #007AFF;
color: #fff;
}
.btn-danger {
background: #dd524d;
color: #fff;
}
}
.navbar-action {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
transition: background 0.3s;
&:active {
background: rgba(0, 0, 0, 0.05);
}
.iconfont {
font-size: 20px;
}
}
</style>
```
#### 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
<template>
<view class="page {module}-{feature}-page">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="navbar-left">
<view class="icon-back" @click="handleBack">
<text class="iconfont">back</text>
</view>
</view>
<view class="navbar-title">{{ pageTitle }}</view>
<view class="navbar-right">
<!-- 右侧操作 -->
</view>
</view>
<!-- 页面内容 -->
<scroll-view class="page-content" scroll-y>
<!-- 页面主体内容 -->
</scroll-view>
<!-- 底部操作栏可选 -->
<view class="page-footer">
<!-- 底部按钮等 -->
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 页面标题
const pageTitle = ref('页面标题')
// 返回上一页
const handleBack = () => {
uni.navigateBack()
}
// 生命周期
onMounted(() => {
// 页面加载完成
})
onUnmounted(() => {
// 页面卸载
})
</script>
<style scoped lang="scss">
.{module}-{feature}-page {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f5f5;
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 16px;
background: #fff;
border-bottom: 1px solid #e5e5e5;
.navbar-title {
font-size: 18px;
font-weight: 500;
}
}
.page-content {
flex: 1;
padding: 16px;
}
.page-footer {
padding: 12px 16px;
background: #fff;
border-top: 1px solid #e5e5e5;
}
}
</style>
```
#### 页面生命周期
```javascript
<script setup>
import { onMounted, onUnmounted, onLoad, onShow, onHide, onUnload } from 'vue'
// 页面加载(只触发一次)
onLoad((options) => {
console.log('页面加载', options)
})
// 页面显示(每次进入页面都会触发)
onShow(() => {
console.log('页面显示')
})
// 页面隐藏
onHide(() => {
console.log('页面隐藏')
})
// 页面卸载
onUnload(() => {
console.log('页面卸载')
})
// Vue 生命周期(推荐使用)
onMounted(() => {
console.log('组件挂载')
})
onUnmounted(() => {
console.log('组件卸载')
})
</script>
```
### 3. 组件引入规范(easycom
**重要提示**: 项目使用 UniApp 的 easycom 组件自动引入机制,无需手动 import 和注册组件,直接在模板中使用即可。
#### easycom 配置
`pages.json` 中配置 easycom
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^sc-(.*)": "@/components/sc-$1/index.vue"
}
}
}
```
#### 组件使用
```vue
<template>
<view class="page">
<!-- 直接使用组件无需 import -->
<sc-button type="primary" @click="handleClick">按钮</sc-button>
<sc-card title="卡片标题">
卡片内容
</sc-card>
<sc-list :items="list" />
</view>
</template>
<script setup>
// 无需 import 组件
const handleClick = () => {
console.log('点击按钮')
}
</script>
```
#### 组件命名规范
- **目录命名**: 使用 PascalCase,如 `ScButton`, `ScCard`
- **文件命名**: 使用 index.vue 作为入口文件
- **组件使用**: 在模板中使用 kebab-case,如 `<sc-button></sc-button>`
- **前缀规范**: 自定义组件统一使用 `sc-` 前缀
#### 组件开发规范
#### 组件结构模板
```vue
<template>
<view class="sc-{component-name}" :class="{ '{modifier}': {condition} }">
<!-- 组件内容 -->
<slot></slot>
</view>
</template>
<script setup>
// 定义 Props
const props = defineProps({
// 基础类型
text: String,
count: Number,
isActive: Boolean,
// 复杂类型
data: {
type: Object,
default: () => ({})
},
list: {
type: Array,
default: () => []
},
// 默认值
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'danger'].includes(value)
}
})
// 定义 Emits
const emit = defineEmits(['click', 'change', 'update:modelValue'])
// 响应式数据
const localValue = ref(props.modelValue)
// 计算属性
const computedClass = computed(() => {
return `sc-{component-name}--${props.type}`
})
// 方法
const handleClick = () => {
emit('click', props.data)
}
</script>
<style scoped lang="scss">
.sc-{component-name} {
// 基础样式
&--primary {
// 主题样式
}
&--danger {
// 危险样式
}
&.{modifier} {
// 条件样式
}
}
</style>
```
### 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
<script setup>
import { ref, onMounted } from 'vue'
import authApi from '@/api/auth'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 登录
const handleLogin = async () => {
try {
const res = await authApi.login({
username: 'admin',
password: '123456'
})
// 保存 Token
userStore.setToken(res.data.token)
userStore.setUserInfo(res.data.user)
// 跳转首页
uni.switchTab({
url: '/pages/index/index'
})
} catch (error) {
console.error('登录失败', error)
}
}
// 获取用户信息
const getUserInfo = async () => {
try {
const res = await authApi.getUserInfo()
userStore.setUserInfo(res.data)
} catch (error) {
console.error('获取用户信息失败', error)
}
}
</script>
```
### 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
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 使用 state
console.log(userStore.token)
console.log(userStore.userInfo)
// 使用 getters
console.log(userStore.isLoggedIn)
console.log(userStore.userName)
// 调用 action
userStore.setToken('xxx')
userStore.setUserInfo(userInfo)
// 初始化存储
onMounted(() => {
userStore.initFromStorage()
})
</script>
```
### 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
<style scoped lang="scss">
// 引入公共样式变量
@import "@/styles/variables.scss";
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
background: $uni-bg-color-grey;
// 嵌套样式
.header {
padding: $uni-spacing-col-lg;
background: $uni-bg-color;
.title {
font-size: $uni-font-size-title;
color: $uni-text-color;
}
}
// 修饰符类
&--loading {
opacity: 0.6;
}
// 伪类
&::after {
content: '';
display: block;
clear: both;
}
}
</style>
```
#### 响应式单位
```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
<template>
<view class="rich-text-container">
<mp-html :content="htmlContent" />
</view>
</template>
<script setup>
import MpHtml from 'mp-html'
import { ref } from 'vue'
const htmlContent = ref(`
<div style="color: #333;">
<h2>文章标题</h2>
<p>这是一段<strong>富文本</strong>内容。</p>
<ul>
<li>列表项 1</li>
<li>列表项 2</li>
</ul>
<img src="https://example.com/image.jpg" alt="图片" />
</div>
`)
</script>
```
#### 高级配置
```vue
<template>
<view class="rich-text-container">
<mp-html
:content="htmlContent"
:tag-style="tagStyle"
:container-style="containerStyle"
:selectable="true"
@load="handleLoad"
@ready="handleReady"
@imgtap="handleImgTap"
@linktap="handleLinkTap"
/>
</view>
</template>
<script setup>
import MpHtml from 'mp-html'
import { ref } from 'vue'
const htmlContent = ref('')
// 标签样式配置
const tagStyle = {
p: 'margin: 10px 0; line-height: 1.6;',
img: 'max-width: 100%; height: auto;',
a: 'color: #007AFF; text-decoration: underline;'
}
// 容器样式
const containerStyle = 'padding: 16px; background: #fff;'
// 图片点击事件
const handleImgTap = (e) => {
const { src } = e.detail
// 预览图片
uni.previewImage({
urls: [src],
current: src
})
}
// 链接点击事件
const handleLinkTap = (e) => {
const { href } = e.detail
uni.navigateTo({
url: `/pages/common/webview/index?url=${encodeURIComponent(href)}`
})
}
// 加载完成
const handleLoad = () => {
console.log('富文本加载完成')
}
// 准备就绪
const handleReady = () => {
console.log('富文本准备就绪')
}
</script>
<style scoped lang="scss">
.rich-text-container {
background: #fff;
border-radius: 8px;
overflow: hidden;
}
</style>
```
#### 使用场景
1. **文章详情页**: 渲染后端返回的富文本内容
2. **商品描述**: 展示商品的详细描述
3. **公告通知**: 显示富文本格式的公告
4. **帮助文档**: 渲染帮助文档内容
#### 注意事项
1. **安全性**: 后端返回的 HTML 内容应经过过滤和消毒,防止 XSS 攻击
2. **性能**: 避免在列表中大量使用 mp-html,会影响性能
3. **样式**: 建议使用 `tag-style` 配置标签样式,而不是全局样式
4. **图片**: 处理图片点击事件,提供图片预览功能
5. **链接**: 处理链接点击事件,跳转到 webview 页面
### 11. 图片资源规范
#### 图片路径引用
```vue
<template>
<!-- 静态图片 -->
<image src="/static/logo.png" mode="aspectFit"></image>
<!-- 动态图片 -->
<image :src="imageUrl" mode="aspectFill"></image>
<!-- 网络图片 -->
<image src="https://example.com/image.jpg" mode="widthFix"></image>
</template>
<script setup>
// 静态图片引入
import logoImage from '@/static/logo.png'
const imageUrl = ref('https://example.com/image.jpg')
</script>
```
#### 图片模式
```vue
<!-- scaleToFill不保持纵横比缩放图片使图片的宽高完全拉伸至填满 image 元素 -->
<image src="/static/image.jpg" mode="scaleToFill"></image>
<!-- aspectFit保持纵横比缩放图片使图片的长边能完全显示出来 -->
<image src="/static/image.jpg" mode="aspectFit"></image>
<!-- aspectFill保持纵横比缩放图片只保证图片的短边能完全显示出来 -->
<image src="/static/image.jpg" mode="aspectFill"></image>
<!-- widthFix宽度不变高度自动变化保持原图宽高比不变 -->
<image src="/static/image.jpg" mode="widthFix"></image>
<!-- heightFix高度不变宽度自动变化保持原图宽高比不变 -->
<image src="/static/image.jpg" mode="heightFix"></image>
```
### 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
<template>
<view>
<!-- H5 特有内容 -->
<!-- #ifdef H5 -->
<view class="h5-only">H5 专属内容</view>
<!-- #endif -->
<!-- 小程序特有内容 -->
<!-- #ifdef MP-WEIXIN -->
<view class="mp-only">小程序专属内容</view>
<!-- #endif -->
<!-- App 特有内容 -->
<!-- #ifdef APP-PLUS -->
<view class="app-only">App 专属内容</view>
<!-- #endif -->
</view>
</template>
<style>
/* H5 特有样式 */
/* #ifdef H5 */
.h5-only {
background: #ff0000;
}
/* #endif */
/* App 特有样式 */
/* #ifdef APP-PLUS */
.app-only {
background: #00ff00;
}
/* #endif */
</style>
```
### 14. 性能优化建议
#### 列表性能优化
```vue
<template>
<!-- 使用 key 提高列表渲染性能 -->
<scroll-view scroll-y>
<view
v-for="item in list"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
</scroll-view>
</template>
```
#### 图片懒加载
```vue
<template>
<!-- 使用 lazy-load 实现图片懒加载 -->
<image
v-for="img in imageList"
:key="img.id"
:src="img.url"
lazy-load
mode="aspectFill"
></image>
</template>
```
#### 避免频繁更新
```javascript
// 使用防抖避免频繁触发
import { debounce } from '@/utils'
const handleSearch = debounce((keyword) => {
console.log('搜索:', keyword)
}, 300)
```
### 15. 常用命令
```bash
# 安装依赖
npm install
# 安装 luch-requestHTTP 请求库)
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**: 新代码统一使用 `<script setup>` 语法
2. **避免使用浏览器特有 API**: UniApp 不支持所有浏览器 API,请使用 UniApp 提供的 API
3. **组件复用**: 提取公共组件,避免重复代码
4. **API 统一管理**: 所有接口统一在 `api/` 目录下管理
5. **环境变量**: 通过 `config/env.js` 配置不同环境的 API 地址
6. **跨平台兼容**: 使用条件编译处理平台差异
7. **不要编写 demo**: 开发过程中不编写示例代码
8. **测试提示**: 如需测试,提示用户是否运行测试,不主动运行
### 17. 代码质量
- 遵循 Vue 3 官方风格指南
- 保持代码简洁、可读
- 适当添加注释说明复杂逻辑
- 使用语义化的变量和函数命名
- 避免过多的嵌套层级
### 18. 安全规范
1. **敏感数据加密**: 本地存储的敏感数据必须加密
2. **Token 安全**: Token 不要存储在 URL 参数中
3. **输入验证**: 所有用户输入必须进行验证
4. **HTTPS**: 生产环境必须使用 HTTPS
5. **XSS 防护**: 避免直接渲染用户输入的 HTML
### 19. 版本管理
#### 应用版本号
`manifest.json` 中配置版本号:
```json
{
"versionName": "1.0.0",
"versionCode": "100"
}
```
- `versionName`: 版本名称,如 "1.0.0"
- `versionCode`: 版本号,必须是整数,如 100
#### 版本号规范
- 主版本号:不兼容的 API 修改
- 次版本号:向下兼容的功能性新增
- 修订号:向下兼容的问题修正
示例:
- 1.0.0 → 1.0.1Bug 修复
- 1.0.1 → 1.1.0:新增功能
- 1.1.0 → 2.0.0:重大更新,不兼容旧版本