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

41 KiB
Raw Blame History

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-caseuser-profile, product-list
  • 文件命名: 使用 index.vue 作为入口文件
  • 路径格式: pages/{module}/{feature}/index.vue

scPages 页面框架组件

重要提示: 所有页面必须使用 scPages 组件作为页面框架,确保统一的页面布局和交互体验。

scPages 组件结构

<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 组件

<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)

页面结构模板

<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>

页面生命周期

<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

{
  "easycom": {
    "autoscan": true,
    "custom": {
      "^sc-(.*)": "@/components/sc-$1/index.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>

组件命名规范

  • 目录命名: 使用 PascalCaseScButton, ScCard
  • 文件命名: 使用 index.vue 作为入口文件
  • 组件使用: 在模板中使用 kebab-case<sc-button></sc-button>
  • 前缀规范: 自定义组件统一使用 sc- 前缀

组件开发规范

组件结构模板

<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 中配置页面路由:

{
  "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"
      }
    ]
  }
}

路由跳转

// 保留当前页面,跳转到应用内的某个页面
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
})

页面传参

// 传递参数
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/ 目录下。

// 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 封装

// 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

使用示例

<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。

// 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 使用

<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. 工具函数开发规范

常用工具函数

// 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
}

表单验证工具

// 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 变量

// 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);

样式组织

<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>

响应式单位

// 使用 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

// 存储数据
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)
  }
})

封装存储工具

// 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 中渲染富文本内容。

安装依赖

npm install mp-html

基本使用

<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>

高级配置

<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. 图片资源规范

图片路径引用

<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>

图片模式

<!-- 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

// 选择图片
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 规范

网络请求

// 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)
  }
})

提示框

// 显示提示框
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)
  }
})

导航栏

// 设置导航栏标题
uni.setNavigationBarTitle({
  title: '新的标题'
})

// 设置导航栏颜色
uni.setNavigationBarColor({
  frontColor: '#ffffff',
  backgroundColor: '#000000'
})

// 显示导航栏加载动画
uni.showNavigationBarLoading()

// 隐藏导航栏加载动画
uni.hideNavigationBarLoading()

下拉刷新

// 开启下拉刷新
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. 跨平台兼容性处理

条件编译

// #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
<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. 性能优化建议

列表性能优化

<template>
  <!-- 使用 key 提高列表渲染性能 -->
  <scroll-view scroll-y>
    <view 
      v-for="item in list" 
      :key="item.id" 
      class="list-item"
    >
      {{ item.name }}
    </view>
  </scroll-view>
</template>

图片懒加载

<template>
  <!-- 使用 lazy-load 实现图片懒加载 -->
  <image 
    v-for="img in imageList" 
    :key="img.id"
    :src="img.url" 
    lazy-load 
    mode="aspectFill"
  ></image>
</template>

避免频繁更新

// 使用防抖避免频繁触发
import { debounce } from '@/utils'

const handleSearch = debounce((keyword) => {
  console.log('搜索:', keyword)
}, 300)

15. 常用命令

# 安装依赖
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 中配置版本号:

{
  "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:重大更新,不兼容旧版本