454 lines
8.8 KiB
Markdown
454 lines
8.8 KiB
Markdown
# uni-app 小程序模板
|
||
|
||
基于 uni-app + Vue 3 + Pinia 的微信小程序模板,内置即时通讯功能。
|
||
|
||
## 技术栈
|
||
|
||
- uni-app (Vue 3 + Composition API)
|
||
- Pinia 状态管理
|
||
- SignalR WebSocket 实时通讯
|
||
- SCSS 样式
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
├── api/ # 接口模块
|
||
│ ├── request.js # 请求封装(Token、重试、取消)
|
||
│ ├── auth.js # 认证接口
|
||
│ ├── user.js # 用户接口
|
||
│ ├── chat.js # 聊天接口
|
||
│ └── index.js # 统一导出
|
||
├── components/ # 公共组件
|
||
│ ├── Loading/ # 加载状态
|
||
│ ├── Empty/ # 空状态
|
||
│ ├── Popup/ # 弹窗
|
||
│ ├── EmojiPicker/ # 表情选择器
|
||
│ └── VoiceRecorder/ # 语音录制
|
||
├── config/
|
||
│ └── index.js # 环境配置
|
||
├── pages/ # 页面
|
||
│ ├── index/ # 首页
|
||
│ ├── message/ # 消息列表
|
||
│ ├── chat/ # 聊天页面
|
||
│ ├── mine/ # 我的
|
||
│ └── login/ # 登录
|
||
├── store/ # 状态管理
|
||
│ ├── user.js # 用户状态
|
||
│ ├── chat.js # 聊天状态
|
||
│ └── index.js # 统一导出
|
||
├── utils/ # 工具函数
|
||
│ ├── storage.js # 本地存储
|
||
│ ├── format.js # 格式化
|
||
│ ├── signalr.js # SignalR 客户端
|
||
│ └── emoji.js # 表情数据
|
||
└── static/ # 静态资源
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
### 安装依赖
|
||
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
### 开发运行
|
||
|
||
```bash
|
||
npm run dev:mp-weixin
|
||
```
|
||
|
||
### 生产构建
|
||
|
||
```bash
|
||
npm run build:mp-weixin
|
||
```
|
||
|
||
## 配置说明
|
||
|
||
### 环境配置
|
||
|
||
编辑 `config/index.js`:
|
||
|
||
```javascript
|
||
const ENV = {
|
||
development: {
|
||
API_BASE_URL: 'http://localhost:5000/api',
|
||
STATIC_BASE_URL: 'http://localhost:5000',
|
||
SIGNALR_URL: 'ws://localhost:5000/hubs/chat'
|
||
},
|
||
production: {
|
||
API_BASE_URL: 'https://your-domain.com/api',
|
||
STATIC_BASE_URL: 'https://your-domain.com',
|
||
SIGNALR_URL: 'wss://your-domain.com/hubs/chat'
|
||
}
|
||
}
|
||
|
||
// 切换环境
|
||
const CURRENT_ENV = 'development'
|
||
```
|
||
|
||
### 请求配置
|
||
|
||
| 配置项 | 默认值 | 说明 |
|
||
|--------|--------|------|
|
||
| REQUEST_TIMEOUT | 30000 | 请求超时时间(ms) |
|
||
| REQUEST_RETRY_COUNT | 2 | 失败重试次数 |
|
||
| REQUEST_RETRY_DELAY | 1000 | 重试间隔(ms) |
|
||
|
||
## 核心模块
|
||
|
||
### 请求封装 (api/request.js)
|
||
|
||
```javascript
|
||
import { get, post, put, del } from '@/api/request'
|
||
|
||
// GET 请求
|
||
const res = await get('/users/detail', { userId: 1 })
|
||
|
||
// POST 请求
|
||
const res = await post('/chat/send', { content: 'hello' })
|
||
|
||
// 带配置的请求
|
||
const res = await get('/data', {}, {
|
||
needAuth: false, // 是否需要 Token
|
||
retry: false, // 是否重试
|
||
cancelPrevious: true // 取消之前的相同请求
|
||
})
|
||
```
|
||
|
||
特性:
|
||
- 自动携带 JWT Token
|
||
- 401 状态码自动清除登录态
|
||
- 请求失败自动重试
|
||
- 支持取消请求
|
||
|
||
### 状态管理
|
||
|
||
#### 用户状态 (store/user.js)
|
||
|
||
```javascript
|
||
import { useUserStore } from '@/store'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
// 登录
|
||
userStore.login({ token, userInfo })
|
||
|
||
// 登出
|
||
userStore.logout()
|
||
|
||
// 判断登录状态
|
||
if (userStore.isLoggedIn) { ... }
|
||
|
||
// 从存储恢复
|
||
userStore.restoreFromStorage()
|
||
```
|
||
|
||
#### 聊天状态 (store/chat.js)
|
||
|
||
```javascript
|
||
import { useChatStore, MessageType, MessageStatus } from '@/store'
|
||
|
||
const chatStore = useChatStore()
|
||
|
||
// 设置会话列表
|
||
chatStore.setSessions(sessions)
|
||
|
||
// 发送消息
|
||
chatStore.sendMessage(sessionId, {
|
||
senderId: 1,
|
||
receiverId: 2,
|
||
messageType: MessageType.TEXT,
|
||
content: 'hello'
|
||
})
|
||
|
||
// 接收消息
|
||
chatStore.receiveMessage(message)
|
||
|
||
// 标记已读
|
||
chatStore.markSessionAsRead(sessionId)
|
||
```
|
||
|
||
消息类型:
|
||
```javascript
|
||
MessageType.TEXT // 1 - 文本
|
||
MessageType.VOICE // 2 - 语音
|
||
MessageType.IMAGE // 3 - 图片
|
||
```
|
||
|
||
消息状态:
|
||
```javascript
|
||
MessageStatus.SENDING // 0 - 发送中
|
||
MessageStatus.SENT // 1 - 已发送
|
||
MessageStatus.DELIVERED // 2 - 已送达
|
||
MessageStatus.READ // 3 - 已读
|
||
MessageStatus.FAILED // 4 - 失败
|
||
```
|
||
|
||
### SignalR 实时通讯 (utils/signalr.js)
|
||
|
||
```javascript
|
||
import signalR from '@/utils/signalr'
|
||
|
||
// 连接
|
||
await signalR.connect()
|
||
|
||
// 监听消息
|
||
signalR.on('ReceiveMessage', (message) => {
|
||
console.log('收到消息:', message)
|
||
})
|
||
|
||
// 加入会话
|
||
await signalR.joinSession(sessionId)
|
||
|
||
// 离开会话
|
||
await signalR.leaveSession(sessionId)
|
||
|
||
// 调用服务端方法
|
||
const result = await signalR.invoke('MethodName', arg1, arg2)
|
||
|
||
// 发送消息(不等待响应)
|
||
signalR.send('MethodName', arg1, arg2)
|
||
|
||
// 断开连接
|
||
signalR.disconnect()
|
||
```
|
||
|
||
特性:
|
||
- 自动重连(最多5次,指数退避)
|
||
- 心跳保活(15秒间隔)
|
||
- 支持方法调用和事件监听
|
||
|
||
### 本地存储 (utils/storage.js)
|
||
|
||
```javascript
|
||
import {
|
||
setToken, getToken, removeToken,
|
||
setUserInfo, getUserInfo, removeUserInfo,
|
||
setStorage, getStorage, removeStorage, clearStorage
|
||
} from '@/utils/storage'
|
||
|
||
// Token
|
||
setToken('xxx')
|
||
const token = getToken()
|
||
|
||
// 用户信息
|
||
setUserInfo({ userId: 1, nickname: '用户' })
|
||
const user = getUserInfo()
|
||
|
||
// 通用存储
|
||
setStorage('key', value)
|
||
const value = getStorage('key', defaultValue)
|
||
```
|
||
|
||
### 格式化工具 (utils/format.js)
|
||
|
||
```javascript
|
||
import { formatTimestamp, maskName, maskPhone } from '@/utils/format'
|
||
|
||
// 时间格式化
|
||
formatTimestamp('2024-01-15 10:30:00')
|
||
// 今天 10:30 / 昨天 / 前天 / 1月15号
|
||
|
||
// 姓名脱敏
|
||
maskName('张三') // 张*
|
||
maskName('张三丰') // 张*丰
|
||
|
||
// 手机脱敏
|
||
maskPhone('13812345678') // 138****5678
|
||
```
|
||
|
||
## 公共组件
|
||
|
||
### Loading
|
||
|
||
```vue
|
||
<Loading type="page" :loading="true" />
|
||
<Loading type="more" :loading="true" :noMore="false" />
|
||
```
|
||
|
||
| 属性 | 类型 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| type | string | 'page' | page/more |
|
||
| loading | boolean | false | 加载状态 |
|
||
| noMore | boolean | false | 没有更多 |
|
||
|
||
### Empty
|
||
|
||
```vue
|
||
<Empty text="暂无数据" :showButton="true" buttonText="去看看" @click="handleClick" />
|
||
```
|
||
|
||
### Popup
|
||
|
||
```vue
|
||
<Popup
|
||
:visible="show"
|
||
title="提示"
|
||
content="确定要执行此操作吗?"
|
||
buttonText="确定"
|
||
:showCancel="true"
|
||
@close="show = false"
|
||
@confirm="handleConfirm"
|
||
/>
|
||
```
|
||
|
||
### EmojiPicker
|
||
|
||
```vue
|
||
<EmojiPicker
|
||
:visible="showEmoji"
|
||
@close="showEmoji = false"
|
||
@select="handleEmojiSelect"
|
||
/>
|
||
```
|
||
|
||
### VoiceRecorder
|
||
|
||
```vue
|
||
<VoiceRecorder @send="handleSendVoice" />
|
||
|
||
// 回调数据
|
||
{
|
||
tempFilePath: '临时文件路径',
|
||
duration: 10 // 秒
|
||
}
|
||
```
|
||
|
||
## 页面开发
|
||
|
||
### 添加新页面
|
||
|
||
1. 在 `pages/` 下创建页面目录和 vue 文件
|
||
2. 在 `pages.json` 中注册页面:
|
||
|
||
```json
|
||
{
|
||
"pages": [
|
||
{
|
||
"path": "pages/new-page/index",
|
||
"style": {
|
||
"navigationStyle": "custom",
|
||
"navigationBarTitleText": "新页面"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 页面模板
|
||
|
||
```vue
|
||
<template>
|
||
<view class="page">
|
||
<!-- 自定义导航栏 -->
|
||
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||
<view class="navbar-content">
|
||
<text class="title">页面标题</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="content" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
|
||
<!-- 页面内容 -->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
|
||
const statusBarHeight = ref(20)
|
||
|
||
onMounted(() => {
|
||
uni.getSystemInfo({
|
||
success: (res) => {
|
||
statusBarHeight.value = res.statusBarHeight || 20
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
```
|
||
|
||
## 后端接口约定
|
||
|
||
### 响应格式
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
- code = 0 表示成功
|
||
- code != 0 表示失败,message 为错误信息
|
||
|
||
### 认证接口
|
||
|
||
```
|
||
POST /auth/login
|
||
Request: { code: "微信登录code" }
|
||
Response: {
|
||
token: "JWT Token",
|
||
userId: 1,
|
||
nickname: "用户名",
|
||
avatar: "头像URL"
|
||
}
|
||
```
|
||
|
||
### 聊天接口
|
||
|
||
```
|
||
GET /chat/sessions
|
||
Response: {
|
||
items: [{
|
||
sessionId: 1,
|
||
otherUserId: 2,
|
||
otherNickname: "对方昵称",
|
||
otherAvatar: "对方头像",
|
||
lastMessageContent: "最后消息",
|
||
lastMessageTime: "2024-01-15T10:30:00",
|
||
unreadCount: 5
|
||
}]
|
||
}
|
||
|
||
GET /chat/messages?sessionId=1&pageIndex=1&pageSize=20
|
||
Response: {
|
||
items: [{
|
||
messageId: 1,
|
||
senderId: 1,
|
||
receiverId: 2,
|
||
messageType: 1,
|
||
content: "消息内容",
|
||
createTime: "2024-01-15T10:30:00",
|
||
isSelf: true
|
||
}]
|
||
}
|
||
|
||
POST /chat/send
|
||
Request: {
|
||
sessionId: 1,
|
||
receiverId: 2,
|
||
messageType: 1,
|
||
content: "消息内容"
|
||
}
|
||
```
|
||
|
||
### SignalR Hub
|
||
|
||
Hub 地址: `/hubs/chat`
|
||
|
||
服务端方法:
|
||
- `JoinSession(sessionId)` - 加入会话
|
||
- `LeaveSession(sessionId)` - 离开会话
|
||
|
||
客户端事件:
|
||
- `ReceiveMessage` - 接收新消息
|
||
|
||
## License
|
||
|
||
MIT
|