2032 lines
41 KiB
Markdown
2032 lines
41 KiB
Markdown
# 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-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**: 新代码统一使用 `<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.1:Bug 修复
|
||
- 1.0.1 → 1.1.0:新增功能
|
||
- 1.1.0 → 2.0.0:重大更新,不兼容旧版本
|