1618 lines
47 KiB
Markdown
1618 lines
47 KiB
Markdown
|
|
# NL-IM UniApp 移动端开发手册
|
|||
|
|
|
|||
|
|
> 基于 nl-im-vue-ts Web 端项目迁移,支持 App(Android/iOS)和微信小程序
|
|||
|
|
|
|||
|
|
## 目录
|
|||
|
|
|
|||
|
|
- [一、项目概述](#一项目概述)
|
|||
|
|
- [二、目录结构](#二目录结构)
|
|||
|
|
- [三、页面设计](#三页面设计qq-风格)
|
|||
|
|
- [四、API 接口](#四api-接口文档)
|
|||
|
|
- [五、类型定义](#五类型定义)
|
|||
|
|
- [六、状态管理](#六状态管理pinia-store)
|
|||
|
|
- [七、WebSocket](#七websocket-通讯)
|
|||
|
|
- [八、主题系统](#八主题系统)
|
|||
|
|
- [九、平台适配](#九平台适配)
|
|||
|
|
- [十、UI 组件](#十ui-组件wot-design-uni)
|
|||
|
|
- [十一、工具函数](#十一工具函数)
|
|||
|
|
- [十二、开发规范](#十二开发规范)
|
|||
|
|
- [十三、快速开始](#十三快速开始)
|
|||
|
|
- [十四、功能清单](#十四功能清单)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、项目概述
|
|||
|
|
|
|||
|
|
### 1.1 技术栈
|
|||
|
|
|
|||
|
|
| 技术 | 版本 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| UniApp | 3.0+ | 跨平台框架 |
|
|||
|
|
| Vue | 3.4.x | 前端框架 |
|
|||
|
|
| TypeScript | 5.x | 类型支持 |
|
|||
|
|
| Pinia | 2.2.x | 状态管理 |
|
|||
|
|
| wot-design-uni | 1.13+ | UI 组件库 |
|
|||
|
|
| Vite | 5.x | 构建工具 |
|
|||
|
|
|
|||
|
|
### 1.2 支持平台
|
|||
|
|
|
|||
|
|
| 平台 | 状态 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| Android | 支持 | App |
|
|||
|
|
| iOS | 支持 | App |
|
|||
|
|
| 微信小程序 | 支持 | 主要平台 |
|
|||
|
|
| H5 | 可选 | 移动端网页 |
|
|||
|
|
|
|||
|
|
### 1.3 设计风格
|
|||
|
|
|
|||
|
|
- UI 参考**手机 QQ**
|
|||
|
|
- 支持**浅色/暗色**两种主题
|
|||
|
|
- 适配不同屏幕尺寸
|
|||
|
|
- 遵循各平台设计规范
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
nl-im-uniapp/src/
|
|||
|
|
├── api/ # API 接口层
|
|||
|
|
│ ├── modules/ # 业务模块
|
|||
|
|
│ │ ├── auth.ts # 认证(登录/注册/验证码)
|
|||
|
|
│ │ ├── user.ts # 用户(信息/列表)
|
|||
|
|
│ │ ├── contact.ts # 联系人(好友/分组/申请)
|
|||
|
|
│ │ ├── conversation.ts # 会话(列表/未读/置顶)
|
|||
|
|
│ │ ├── message.ts # 消息(发送/历史/同步)
|
|||
|
|
│ │ ├── room.ts # 群聊(创建/成员/设置)
|
|||
|
|
│ │ ├── moment.ts # 朋友圈(动态/点赞/评论)
|
|||
|
|
│ │ ├── attachment.ts # 附件(上传/下载)
|
|||
|
|
│ │ └── system.ts # 系统(健康检查/配置)
|
|||
|
|
│ ├── request/ # 请求封装
|
|||
|
|
│ │ ├── index.ts # uni.request 封装
|
|||
|
|
│ │ └── types.ts # 请求/响应类型
|
|||
|
|
│ └── websocket/ # 实时通讯
|
|||
|
|
│ └── index.ts # WebSocket 管理器
|
|||
|
|
│
|
|||
|
|
├── components/ # 公共组件
|
|||
|
|
│ ├── chat/ # 聊天组件
|
|||
|
|
│ │ ├── MessageBubble.vue # 消息气泡容器
|
|||
|
|
│ │ ├── MessageInput.vue # 消息输入框
|
|||
|
|
│ │ ├── MessageList.vue # 消息列表
|
|||
|
|
│ │ └── bubble/ # 气泡类型
|
|||
|
|
│ │ ├── TextBubble.vue
|
|||
|
|
│ │ ├── ImageBubble.vue
|
|||
|
|
│ │ ├── AudioBubble.vue
|
|||
|
|
│ │ ├── VideoBubble.vue
|
|||
|
|
│ │ └── FileBubble.vue
|
|||
|
|
│ ├── common/ # 通用组件
|
|||
|
|
│ │ ├── AppNavBar.vue # 导航栏
|
|||
|
|
│ │ ├── AppTabBar.vue # 底部标签栏
|
|||
|
|
│ │ ├── AppDrawer.vue # 侧边抽屉
|
|||
|
|
│ │ ├── AppAvatar.vue # 头像
|
|||
|
|
│ │ ├── AppSearch.vue # 搜索框
|
|||
|
|
│ │ ├── AppEmpty.vue # 空状态
|
|||
|
|
│ │ └── AppLoading.vue # 加载状态
|
|||
|
|
│ ├── contact/ # 联系人组件
|
|||
|
|
│ │ ├── ContactItem.vue # 联系人项
|
|||
|
|
│ │ └── ContactGroup.vue # 联系人分组
|
|||
|
|
│ └── moment/ # 朋友圈组件
|
|||
|
|
│ ├── MomentCard.vue # 动态卡片
|
|||
|
|
│ ├── MomentPublisher.vue # 发布器
|
|||
|
|
│ └── MomentComment.vue # 评论组件
|
|||
|
|
│
|
|||
|
|
├── composables/ # 组合式函数
|
|||
|
|
│ ├── useTheme.ts # 主题管理
|
|||
|
|
│ ├── useAuth.ts # 认证状态
|
|||
|
|
│ ├── useSocket.ts # WebSocket
|
|||
|
|
│ └── useUpload.ts # 文件上传
|
|||
|
|
│
|
|||
|
|
├── pages/ # 页面
|
|||
|
|
│ ├── index/ # 首页(会话)
|
|||
|
|
│ │ └── index.vue
|
|||
|
|
│ ├── chat/ # 聊天
|
|||
|
|
│ │ ├── index.vue # 聊天详情
|
|||
|
|
│ │ └── group-info.vue # 群信息
|
|||
|
|
│ ├── contact/ # 联系人
|
|||
|
|
│ │ ├── index.vue # 联系人列表
|
|||
|
|
│ │ ├── detail.vue # 好友详情
|
|||
|
|
│ │ ├── add.vue # 添加好友
|
|||
|
|
│ │ ├── requests.vue # 好友申请
|
|||
|
|
│ │ └── groups.vue # 群聊列表
|
|||
|
|
│ ├── moment/ # 朋友圈
|
|||
|
|
│ │ ├── index.vue # 动态列表
|
|||
|
|
│ │ ├── detail.vue # 动态详情
|
|||
|
|
│ │ ├── publish.vue # 发布动态
|
|||
|
|
│ │ └── notify.vue # 消息通知
|
|||
|
|
│ ├── login/ # 登录
|
|||
|
|
│ │ └── index.vue
|
|||
|
|
│ ├── profile/ # 个人中心
|
|||
|
|
│ │ ├── index.vue # 个人资料
|
|||
|
|
│ │ └── edit.vue # 编辑资料
|
|||
|
|
│ ├── settings/ # 设置
|
|||
|
|
│ │ └── index.vue
|
|||
|
|
│ └── search/ # 搜索
|
|||
|
|
│ └── index.vue
|
|||
|
|
│
|
|||
|
|
├── stores/ # Pinia Store
|
|||
|
|
│ ├── auth.ts # 认证
|
|||
|
|
│ ├── chat.ts # 聊天
|
|||
|
|
│ ├── contact.ts # 联系人
|
|||
|
|
│ ├── conversation.ts # 会话
|
|||
|
|
│ ├── moment.ts # 朋友圈
|
|||
|
|
│ └── app.ts # 全局应用
|
|||
|
|
│
|
|||
|
|
├── types/ # 类型定义
|
|||
|
|
│ ├── api.ts # API 类型
|
|||
|
|
│ ├── conversation.ts # 会话类型
|
|||
|
|
│ ├── message.ts # 消息类型
|
|||
|
|
│ ├── moment.ts # 朋友圈类型
|
|||
|
|
│ └── user.ts # 用户类型
|
|||
|
|
│
|
|||
|
|
├── utils/ # 工具函数
|
|||
|
|
│ ├── storage.ts # 本地存储
|
|||
|
|
│ ├── format.ts # 格式化
|
|||
|
|
│ ├── message.ts # 消息处理
|
|||
|
|
│ └── platform.ts # 平台判断
|
|||
|
|
│
|
|||
|
|
├── styles/ # 样式
|
|||
|
|
│ ├── variables.scss # CSS 变量
|
|||
|
|
│ ├── theme.scss # 主题定义
|
|||
|
|
│ └── common.scss # 公共样式
|
|||
|
|
│
|
|||
|
|
├── static/ # 静态资源
|
|||
|
|
│ ├── images/
|
|||
|
|
│ └── icons/
|
|||
|
|
│
|
|||
|
|
├── App.vue # 根组件
|
|||
|
|
├── main.ts # 入口文件
|
|||
|
|
├── pages.json # 页面配置
|
|||
|
|
├── manifest.json # 应用配置
|
|||
|
|
├── theme.json # 主题配置
|
|||
|
|
└── uni.scss # uni 变量
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、页面设计(QQ 风格)
|
|||
|
|
|
|||
|
|
### 3.1 页面路由配置
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
// pages.json
|
|||
|
|
{
|
|||
|
|
"pages": [
|
|||
|
|
{ "path": "pages/index/index", "style": { "navigationStyle": "custom" } },
|
|||
|
|
{ "path": "pages/chat/index", "style": { "navigationBarTitleText": "聊天" } },
|
|||
|
|
{ "path": "pages/contact/index", "style": { "navigationStyle": "custom" } },
|
|||
|
|
{ "path": "pages/moment/index", "style": { "navigationStyle": "custom" } },
|
|||
|
|
{ "path": "pages/login/index", "style": { "navigationBarTitleText": "登录" } }
|
|||
|
|
],
|
|||
|
|
"tabBar": {
|
|||
|
|
"list": [
|
|||
|
|
{ "pagePath": "pages/index/index", "text": "消息", "iconPath": "...", "selectedIconPath": "..." },
|
|||
|
|
{ "pagePath": "pages/contact/index", "text": "联系人", "iconPath": "...", "selectedIconPath": "..." },
|
|||
|
|
{ "pagePath": "pages/moment/index", "text": "朋友圈", "iconPath": "...", "selectedIconPath": "..." }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 首页(会话页)
|
|||
|
|
|
|||
|
|
**路径**: `pages/index/index.vue`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ [头像] 消息 [+] │ ← 自定义导航栏
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ 🔍 搜索 │ ← 搜索框
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ ┌─────────────────────────────┐ │
|
|||
|
|
│ │ [头像] 张三 14:30 │ │
|
|||
|
|
│ │ 你好,在吗? [3] │ │ ← 会话项
|
|||
|
|
│ └─────────────────────────────┘ │
|
|||
|
|
│ ┌─────────────────────────────┐ │
|
|||
|
|
│ │ [头像] 工作群 昨天 │ │
|
|||
|
|
│ │ 李四: 收到 🔇 │ │
|
|||
|
|
│ └─────────────────────────────┘ │
|
|||
|
|
│ ... │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ 消息 联系人 朋友圈 │ ← TabBar
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**功能点**:
|
|||
|
|
- 点击头像 → 弹出左侧抽屉
|
|||
|
|
- 点击加号 → 显示菜单(创建群聊/添加好友/扫一扫)
|
|||
|
|
- 点击搜索 → 跳转搜索页
|
|||
|
|
- 长按会话 → 显示操作菜单(置顶/删除/免打扰)
|
|||
|
|
- 左滑会话 → 快捷操作
|
|||
|
|
|
|||
|
|
**左侧抽屉内容**:
|
|||
|
|
```
|
|||
|
|
┌────────────────────┐
|
|||
|
|
│ [大头像] │
|
|||
|
|
│ 昵称 │
|
|||
|
|
│ 个性签名 │
|
|||
|
|
├────────────────────┤
|
|||
|
|
│ 👤 个人资料 │
|
|||
|
|
│ ⚙️ 设置 │
|
|||
|
|
│ 🌙 深色模式 [开关]│
|
|||
|
|
├────────────────────┤
|
|||
|
|
│ 📤 退出登录 │
|
|||
|
|
└────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 聊天页
|
|||
|
|
|
|||
|
|
**路径**: `pages/chat/index.vue`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ [<] 张三 / 工作群(10) [···] │ ← 导航栏(群聊显示成员数和更多按钮)
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────┐ │
|
|||
|
|
│ │ 你好 │ 14:30 │ ← 对方消息(左侧)
|
|||
|
|
│ └──────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────┐ │
|
|||
|
|
│ 14:31 │ 在的 │ │ ← 我的消息(右侧)
|
|||
|
|
│ └──────────┘ │
|
|||
|
|
│ │
|
|||
|
|
│ 系统消息 │ ← 系统/通知消息(居中)
|
|||
|
|
│ │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ [😊] [📷] [📁] 输入消息... [发送] │ ← 输入栏
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**消息类型展示**:
|
|||
|
|
| 类型 | 展示方式 |
|
|||
|
|
|------|----------|
|
|||
|
|
| 文本 | 气泡+文字 |
|
|||
|
|
| 图片 | 缩略图,点击预览 |
|
|||
|
|
| 语音 | 播放条+时长 |
|
|||
|
|
| 视频 | 封面+播放按钮 |
|
|||
|
|
| 文件 | 图标+文件名+大小 |
|
|||
|
|
| 系统 | 居中灰色文字 |
|
|||
|
|
|
|||
|
|
### 3.4 联系人页
|
|||
|
|
|
|||
|
|
**路径**: `pages/contact/index.vue`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ 联系人 │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ 🔍 搜索 │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ ┌─────────────────────────────┐ │
|
|||
|
|
│ │ [👥] 新朋友 [2] │ │ ← 功能入口
|
|||
|
|
│ │ [👪] 群聊 │ │
|
|||
|
|
│ │ [🔔] 群通知 [1] │ │
|
|||
|
|
│ └─────────────────────────────┘ │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ A ───────────────────────── │
|
|||
|
|
│ │ [头像] 阿明 │ ← 按字母分组
|
|||
|
|
│ │ [头像] 阿强 │
|
|||
|
|
│ B ───────────────────────── │
|
|||
|
|
│ │ [头像] 白云 │
|
|||
|
|
│ ... │ ← 右侧字母索引
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.5 朋友圈页
|
|||
|
|
|
|||
|
|
**路径**: `pages/moment/index.vue`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────┐
|
|||
|
|
│ 朋友圈 │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ ┌─────────────────────────────┐ │
|
|||
|
|
│ │ [封面背景图] │ │
|
|||
|
|
│ │ [头像] │ │ ← 个人封面区
|
|||
|
|
│ │ 昵称 │ │
|
|||
|
|
│ └─────────────────────────────┘ │
|
|||
|
|
├─────────────────────────────────────┤
|
|||
|
|
│ ┌─────────────────────────────┐ │
|
|||
|
|
│ │ [头像] 张三 2小时前 │ │
|
|||
|
|
│ │ 今天天气真好! │ │
|
|||
|
|
│ │ [图片] [图片] [图片] │ │ ← 动态卡片
|
|||
|
|
│ │ ──────────────────── │ │
|
|||
|
|
│ │ ❤️ 李四、王五 │ │
|
|||
|
|
│ │ 李四: 确实不错 │ │
|
|||
|
|
│ │ [评论输入...] [❤️] │ │
|
|||
|
|
│ └─────────────────────────────┘ │
|
|||
|
|
│ ... │
|
|||
|
|
│ [📷] │ ← 悬浮发布按钮
|
|||
|
|
└─────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、API 接口文档
|
|||
|
|
|
|||
|
|
> 基础路径: `/api`,所有接口返回统一格式 `{ code: 0, message: "", result: {} }`
|
|||
|
|
|
|||
|
|
### 4.1 认证模块 `/auth`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 登录 | POST | `/login` | `{ account, password, remember? }` | 返回 token 和用户信息 |
|
|||
|
|
| 注册 | POST | `/register` | `{ email, phone, password, confirm_password, code, agree_terms }` | - |
|
|||
|
|
| 发送邮箱验证码 | POST | `/send-email-code` | `{ target, type: 'email' }` | - |
|
|||
|
|
| 发送短信验证码 | POST | `/send-sms-code` | `{ target, type: 'sms' }` | - |
|
|||
|
|
| 检查Token | GET | `/check-token` | - | 验证登录状态 |
|
|||
|
|
|
|||
|
|
### 4.2 用户模块 `/user`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 获取我的信息 | GET | `/user/my-info` | - | - |
|
|||
|
|
| 获取用户列表 | GET | `/user/list` | `?page=1&page_size=20` | 分页 |
|
|||
|
|
| 更新用户信息 | POST | `/user/update` | `{ id, updates: {...} }` | - |
|
|||
|
|
|
|||
|
|
### 4.3 联系人模块 `/contacts`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 获取联系人列表 | GET | `/contacts` | - | - |
|
|||
|
|
| 搜索用户 | GET | `/contacts/search` | `?keyword=xxx&limit=20` | - |
|
|||
|
|
| 添加好友 | POST | `/contacts/add-friend` | `{ to_user_id, message? }` | 发送申请 |
|
|||
|
|
| 获取好友申请 | GET | `/contacts/friend-requests` | - | - |
|
|||
|
|
| 接受申请 | POST | `/contacts/accept-request` | `{ request_id }` | - |
|
|||
|
|
| 拒绝申请 | POST | `/contacts/reject-request` | `{ request_id }` | - |
|
|||
|
|
| 获取好友详情 | GET | `/contacts/{id}` | - | - |
|
|||
|
|
| 更新好友信息 | POST | `/contacts/update/{id}` | `{ remark_name?, is_top?, is_muted?, ... }` | - |
|
|||
|
|
| 删除好友 | POST | `/contacts/delete/{id}` | - | - |
|
|||
|
|
| 获取分组列表 | GET | `/contacts/groups` | - | - |
|
|||
|
|
| 创建分组 | POST | `/contacts/groups` | `{ group_name }` | - |
|
|||
|
|
|
|||
|
|
### 4.4 会话模块 `/conversations`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 获取会话列表 | GET | `/conversations` | - | - |
|
|||
|
|
| 重置未读数 | POST | `/conversations/reset-unread` | `{ target_id }` | - |
|
|||
|
|
| 更新会话 | POST | `/conversations/update` | `{ target_id, is_top?, is_muted? }` | - |
|
|||
|
|
| 删除会话 | POST | `/conversations/delete` | `{ target_id }` | - |
|
|||
|
|
| 按房间获取会话 | GET | `/conversations/by-room/{roomId}` | - | - |
|
|||
|
|
|
|||
|
|
### 4.5 消息模块 `/messages`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 发送消息 | POST | `/send` | `{ room_id, message_type, content, duration?, extra? }` | - |
|
|||
|
|
| 获取历史消息 | GET | `/messages` | `?room_id=xxx&page=1&page_size=50` | - |
|
|||
|
|
| 同步消息 | GET | `/messages/sync` | `?room_id=xxx&page=1&page_size=50` | - |
|
|||
|
|
|
|||
|
|
### 4.6 群聊模块 `/groups`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 获取群列表 | GET | `/groups` | - | 带分组信息 |
|
|||
|
|
| 创建群聊 | POST | `/groups` | `{ name, avatar?, member_ids, admin_ids? }` | - |
|
|||
|
|
| 获取群信息 | GET | `/groups/{roomId}` | - | - |
|
|||
|
|
| 获取群成员 | GET | `/groups/{roomId}/members` | `?keyword=xxx` | 可搜索 |
|
|||
|
|
| 邀请成员 | POST | `/groups/{roomId}/members` | `{ member_ids: [] }` | - |
|
|||
|
|
| 移除成员 | POST | `/groups/{roomId}/members/{userId}/remove` | - | - |
|
|||
|
|
| 修改群信息 | POST | `/groups/{roomId}/update` | `{ name?, avatar? }` | - |
|
|||
|
|
| 修改成员角色 | POST | `/groups/{roomId}/members/{userId}/role` | `{ role: 0\|1\|2 }` | 0成员/1管理/2群主 |
|
|||
|
|
| 退出群聊 | POST | `/groups/{roomId}/quit` | - | - |
|
|||
|
|
| 解散群聊 | POST | `/groups/{roomId}/dissolve` | - | 仅群主 |
|
|||
|
|
| 获取群公告 | GET | `/groups/{roomId}/announcement` | - | - |
|
|||
|
|
| 更新群公告 | POST | `/groups/{roomId}/announcement` | `{ announcement }` | - |
|
|||
|
|
| 群通知列表 | GET | `/group-notifications` | `?page=1&page_size=20` | - |
|
|||
|
|
|
|||
|
|
### 4.7 朋友圈模块 `/moments`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 获取动态列表 | GET | `/moments` | `?page=1&page_size=20` | - |
|
|||
|
|
| 发布动态 | POST | `/moments` | `{ content, media_type, media_urls?, visibility, ... }` | - |
|
|||
|
|
| 获取动态详情 | GET | `/moments/{id}` | - | - |
|
|||
|
|
| 删除动态 | DELETE | `/moments/{id}` | - | 仅自己 |
|
|||
|
|
| 获取用户动态 | GET | `/moments/user/{userId}` | `?page=1&page_size=20` | - |
|
|||
|
|
| 点赞 | POST | `/moments/{id}/like` | - | - |
|
|||
|
|
| 取消点赞 | DELETE | `/moments/{id}/like` | - | - |
|
|||
|
|
| 获取评论 | GET | `/moments/{id}/comments` | - | - |
|
|||
|
|
| 发表评论 | POST | `/moments/{id}/comments` | `{ content, reply_to_comment_id? }` | - |
|
|||
|
|
| 删除评论 | DELETE | `/moments/comments/{commentId}` | - | - |
|
|||
|
|
| 获取通知 | GET | `/moments/notifications` | `?page=1&page_size=20` | - |
|
|||
|
|
| 标记已读 | POST | `/moments/notifications/read` | `{ ids?: [], all?: true }` | - |
|
|||
|
|
| 未读数 | GET | `/moments/notifications/unread-count` | - | - |
|
|||
|
|
|
|||
|
|
### 4.8 附件模块 `/attachments`
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 上传附件 | POST | `/attachments/upload` | FormData: `{ file, type }` | multipart |
|
|||
|
|
| 获取附件 | GET | `/attachments/{id}` | - | - |
|
|||
|
|
| 附件列表 | GET | `/attachments` | `?type=xxx&page=1&page_size=20` | - |
|
|||
|
|
| 删除附件 | POST | `/attachments/delete/{id}` | - | - |
|
|||
|
|
|
|||
|
|
### 4.9 系统模块
|
|||
|
|
|
|||
|
|
| 接口 | 方法 | 路径 | 参数 | 说明 |
|
|||
|
|
|------|------|------|------|------|
|
|||
|
|
| 健康检查 | GET | `/health` | - | - |
|
|||
|
|
| ICE服务器 | GET | `/ice-servers` | `?user_id=xxx` | WebRTC配置 |
|
|||
|
|
| 在线状态 | GET | `/check-user-online` | `?user_id=xxx` | - |
|
|||
|
|
| 绑定客户端 | POST | `/bind` | `{ user_id, client_id }` | WebSocket |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、类型定义
|
|||
|
|
|
|||
|
|
### 5.1 基础类型
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// types/api.ts
|
|||
|
|
|
|||
|
|
/** 用户 */
|
|||
|
|
interface User {
|
|||
|
|
id: string
|
|||
|
|
email: string
|
|||
|
|
phone: string
|
|||
|
|
name: string
|
|||
|
|
avatar: string
|
|||
|
|
desc: string
|
|||
|
|
region: string
|
|||
|
|
created_at: string
|
|||
|
|
updated_at: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 联系人 */
|
|||
|
|
interface Contact {
|
|||
|
|
id: string
|
|||
|
|
user_id: string
|
|||
|
|
contact_user_id: string
|
|||
|
|
room_id?: string
|
|||
|
|
room_type?: 'p2p' | 'group'
|
|||
|
|
is_group?: boolean
|
|||
|
|
member_count?: number
|
|||
|
|
owner_id?: string
|
|||
|
|
remark_name?: string
|
|||
|
|
group_id?: number
|
|||
|
|
is_top: boolean
|
|||
|
|
is_muted: boolean
|
|||
|
|
is_special_care?: boolean
|
|||
|
|
is_blocked?: boolean
|
|||
|
|
user?: User
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 好友申请 */
|
|||
|
|
interface FriendRequest {
|
|||
|
|
id: number
|
|||
|
|
from_user_id: string
|
|||
|
|
to_user_id: string
|
|||
|
|
message: string
|
|||
|
|
status: 'pending' | 'accepted' | 'rejected'
|
|||
|
|
created_at: string
|
|||
|
|
from_user?: User
|
|||
|
|
to_user?: User
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 分页响应 */
|
|||
|
|
interface PaginatedResponse<T> {
|
|||
|
|
data: T[]
|
|||
|
|
total: number
|
|||
|
|
page: number
|
|||
|
|
size: number
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 会话类型
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// types/conversation.ts
|
|||
|
|
|
|||
|
|
interface Conversation {
|
|||
|
|
id: number
|
|||
|
|
user_id: string
|
|||
|
|
target_id: string
|
|||
|
|
type: number // 1:私聊 2:群聊
|
|||
|
|
room_type?: 'p2p' | 'group'
|
|||
|
|
room_id?: string
|
|||
|
|
is_group?: boolean
|
|||
|
|
member_count?: number
|
|||
|
|
owner_id?: string
|
|||
|
|
name?: string
|
|||
|
|
avatar?: string
|
|||
|
|
unread_count: number
|
|||
|
|
is_top: boolean
|
|||
|
|
is_muted: boolean
|
|||
|
|
is_special_care?: boolean
|
|||
|
|
last_message: string
|
|||
|
|
last_time: number
|
|||
|
|
target_user?: User
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 消息类型
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// types/message.ts
|
|||
|
|
|
|||
|
|
/** 消息类型枚举 */
|
|||
|
|
enum MessageType {
|
|||
|
|
TEXT = 0, // 文本
|
|||
|
|
IMAGE = 1, // 图片
|
|||
|
|
AUDIO = 2, // 语音
|
|||
|
|
VIDEO = 3, // 视频
|
|||
|
|
SYSTEM = 4, // 系统消息
|
|||
|
|
FRIEND_NOTIFY = 5, // 好友通知
|
|||
|
|
SIGNAL = 6, // WebRTC信令
|
|||
|
|
GROUP_NOTIFY = 7, // 群通知
|
|||
|
|
FILE = 8, // 文件
|
|||
|
|
MOMENTS_NOTIFY = 9, // 朋友圈通知
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 聊天消息 */
|
|||
|
|
interface ChatMessage {
|
|||
|
|
id: number
|
|||
|
|
room_id: string
|
|||
|
|
sender_user_id: string
|
|||
|
|
receiver_user_id?: string
|
|||
|
|
message_type: number
|
|||
|
|
content: string
|
|||
|
|
duration: number
|
|||
|
|
extra?: string | Record<string, any>
|
|||
|
|
created_at: string
|
|||
|
|
isSelf?: boolean // 前端标记
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 消息扩展信息 */
|
|||
|
|
interface MessageExtra {
|
|||
|
|
url?: string // 媒体URL
|
|||
|
|
name?: string // 文件名
|
|||
|
|
size?: number // 文件大小
|
|||
|
|
duration?: number // 时长(秒)
|
|||
|
|
width?: number // 图片/视频宽
|
|||
|
|
height?: number // 图片/视频高
|
|||
|
|
attachment_id?: number
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 发送消息请求 */
|
|||
|
|
interface SendMessageRequest {
|
|||
|
|
room_id: string
|
|||
|
|
message_type: number
|
|||
|
|
content: string
|
|||
|
|
duration?: number
|
|||
|
|
extra?: string
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.4 朋友圈类型
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// types/moment.ts
|
|||
|
|
|
|||
|
|
/** 动态 */
|
|||
|
|
interface Moment {
|
|||
|
|
id: number
|
|||
|
|
user_id: string
|
|||
|
|
content: string
|
|||
|
|
media_type: 0 | 1 | 2 // 0=纯文字 1=图片 2=视频
|
|||
|
|
media_urls: string[]
|
|||
|
|
location?: string
|
|||
|
|
visibility: 0 | 1 | 2 | 3 // 0=公开 1=仅好友 2=部分可见 3=部分不可见
|
|||
|
|
visible_user_ids?: string[]
|
|||
|
|
mention_user_ids?: string[]
|
|||
|
|
like_count: number
|
|||
|
|
comment_count: number
|
|||
|
|
created_at: string
|
|||
|
|
user?: User
|
|||
|
|
is_liked: boolean
|
|||
|
|
likes?: MomentLike[]
|
|||
|
|
comments?: MomentComment[]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 点赞 */
|
|||
|
|
interface MomentLike {
|
|||
|
|
id: number
|
|||
|
|
moment_id: number
|
|||
|
|
user_id: string
|
|||
|
|
created_at: string
|
|||
|
|
user?: User
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 评论 */
|
|||
|
|
interface MomentComment {
|
|||
|
|
id: number
|
|||
|
|
moment_id: number
|
|||
|
|
user_id: string
|
|||
|
|
reply_to_comment_id?: number
|
|||
|
|
reply_to_user_id?: string
|
|||
|
|
content: string
|
|||
|
|
created_at: string
|
|||
|
|
user?: User
|
|||
|
|
reply_to_user?: User
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 通知 */
|
|||
|
|
interface MomentNotification {
|
|||
|
|
id: number
|
|||
|
|
user_id: string
|
|||
|
|
from_user_id: string
|
|||
|
|
moment_id: number
|
|||
|
|
type: 1 | 2 | 3 | 4 // 1=点赞 2=评论 3=回复 4=@提及
|
|||
|
|
comment_id?: number
|
|||
|
|
is_read: boolean
|
|||
|
|
created_at: string
|
|||
|
|
from_user?: User
|
|||
|
|
moment?: Moment
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、状态管理(Pinia Store)
|
|||
|
|
|
|||
|
|
### 6.1 Auth Store
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// stores/auth.ts
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref } from 'vue'
|
|||
|
|
import * as authApi from '@/api/modules/auth'
|
|||
|
|
import * as userApi from '@/api/modules/user'
|
|||
|
|
import { storage } from '@/utils/storage'
|
|||
|
|
import type { User, LoginRequest } from '@/types/api'
|
|||
|
|
|
|||
|
|
export const useAuthStore = defineStore('auth', () => {
|
|||
|
|
const token = ref<string | null>(storage.getToken())
|
|||
|
|
const user = ref<User | null>(storage.getUserInfo())
|
|||
|
|
const isAuthenticated = ref(!!token.value)
|
|||
|
|
|
|||
|
|
async function login(data: LoginRequest) {
|
|||
|
|
const res = await authApi.login(data)
|
|||
|
|
token.value = res.token
|
|||
|
|
user.value = res.user
|
|||
|
|
isAuthenticated.value = true
|
|||
|
|
storage.setToken(res.token)
|
|||
|
|
storage.setUserId(res.user.id)
|
|||
|
|
storage.setUserInfo(res.user)
|
|||
|
|
return res
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function register(data: any) {
|
|||
|
|
await authApi.register(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function logout() {
|
|||
|
|
token.value = null
|
|||
|
|
user.value = null
|
|||
|
|
isAuthenticated.value = false
|
|||
|
|
storage.clear()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function checkAuth() {
|
|||
|
|
if (!token.value) return false
|
|||
|
|
try {
|
|||
|
|
await authApi.checkToken()
|
|||
|
|
const userInfo = await userApi.getMyInfo()
|
|||
|
|
user.value = userInfo
|
|||
|
|
storage.setUserInfo(userInfo)
|
|||
|
|
return true
|
|||
|
|
} catch {
|
|||
|
|
logout()
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateUserInfo(userInfo: User) {
|
|||
|
|
user.value = userInfo
|
|||
|
|
storage.setUserInfo(userInfo)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
token, user, isAuthenticated,
|
|||
|
|
login, register, logout, checkAuth, updateUserInfo
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 Conversation Store
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// stores/conversation.ts
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
import type { Conversation } from '@/types/conversation'
|
|||
|
|
import * as conversationApi from '@/api/modules/conversation'
|
|||
|
|
|
|||
|
|
export const useConversationStore = defineStore('conversation', () => {
|
|||
|
|
const conversations = ref<Conversation[]>([])
|
|||
|
|
const loading = ref(false)
|
|||
|
|
|
|||
|
|
const totalUnread = computed(() =>
|
|||
|
|
conversations.value.reduce((acc, c) => acc + (c.is_muted ? 0 : c.unread_count), 0)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
async function loadConversations() {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const list = await conversationApi.getConversationList()
|
|||
|
|
conversations.value = list.map(processConversation).sort(sortByTopAndTime)
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleMessageUpdate(message: ChatMessage, isSelf: boolean, isCurrentChat: boolean) {
|
|||
|
|
// 更新会话最后消息和未读数
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function clearUnread(targetId: string) {
|
|||
|
|
const conv = conversations.value.find(c => c.target_id === targetId || c.room_id === targetId)
|
|||
|
|
if (conv && conv.unread_count > 0) {
|
|||
|
|
conv.unread_count = 0
|
|||
|
|
await conversationApi.resetUnread(targetId)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
conversations, loading, totalUnread,
|
|||
|
|
loadConversations, handleMessageUpdate, clearUnread
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 Chat Store
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// stores/chat.ts
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
import type { ChatMessage, Contact } from '@/types/api'
|
|||
|
|
|
|||
|
|
export const useChatStore = defineStore('chat', () => {
|
|||
|
|
const currentTarget = ref<Contact | null>(null)
|
|||
|
|
const messages = ref<Record<string, ChatMessage[]>>({})
|
|||
|
|
const contacts = ref<Contact[]>([])
|
|||
|
|
|
|||
|
|
const totalUnread = computed(() =>
|
|||
|
|
contacts.value.reduce((acc, c) => acc + (c.unread || 0), 0)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
function setCurrentTarget(contact: Contact | null) {
|
|||
|
|
currentTarget.value = contact
|
|||
|
|
if (contact) contact.unread = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addMessage(roomId: string, message: ChatMessage) {
|
|||
|
|
if (!messages.value[roomId]) messages.value[roomId] = []
|
|||
|
|
messages.value[roomId].push(message)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getRoomMessages(roomId: string): ChatMessage[] {
|
|||
|
|
return messages.value[roomId] || []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setRoomMessages(roomId: string, msgs: ChatMessage[]) {
|
|||
|
|
messages.value[roomId] = msgs
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setContacts(newContacts: Contact[]) {
|
|||
|
|
contacts.value = newContacts
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
currentTarget, messages, contacts, totalUnread,
|
|||
|
|
setCurrentTarget, addMessage, getRoomMessages, setRoomMessages, setContacts
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 Moment Store
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// stores/moment.ts
|
|||
|
|
import { defineStore } from 'pinia'
|
|||
|
|
import { ref, computed } from 'vue'
|
|||
|
|
import * as momentApi from '@/api/modules/moment'
|
|||
|
|
import type { Moment, MomentNotification } from '@/types/moment'
|
|||
|
|
|
|||
|
|
export const useMomentStore = defineStore('moment', () => {
|
|||
|
|
const moments = ref<Moment[]>([])
|
|||
|
|
const currentMoment = ref<Moment | null>(null)
|
|||
|
|
const notifications = ref<MomentNotification[]>([])
|
|||
|
|
const unreadCount = ref(0)
|
|||
|
|
const loading = ref(false)
|
|||
|
|
const pagination = ref({ page: 1, pageSize: 20, total: 0, hasMore: true })
|
|||
|
|
|
|||
|
|
const hasUnread = computed(() => unreadCount.value > 0)
|
|||
|
|
|
|||
|
|
async function fetchMoments() {
|
|||
|
|
loading.value = true
|
|||
|
|
try {
|
|||
|
|
const res = await momentApi.getMoments(1, pagination.value.pageSize)
|
|||
|
|
moments.value = res.data || []
|
|||
|
|
pagination.value.total = res.total
|
|||
|
|
pagination.value.hasMore = moments.value.length < res.total
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function toggleLike(momentId: number) {
|
|||
|
|
const moment = moments.value.find(m => m.id === momentId)
|
|||
|
|
if (!moment) return
|
|||
|
|
if (moment.is_liked) {
|
|||
|
|
await momentApi.unlikeMoment(momentId)
|
|||
|
|
moment.is_liked = false
|
|||
|
|
moment.like_count--
|
|||
|
|
} else {
|
|||
|
|
await momentApi.likeMoment(momentId)
|
|||
|
|
moment.is_liked = true
|
|||
|
|
moment.like_count++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function addComment(momentId: number, data: { content: string; reply_to_comment_id?: number }) {
|
|||
|
|
const comment = await momentApi.createComment(momentId, data)
|
|||
|
|
const moment = moments.value.find(m => m.id === momentId)
|
|||
|
|
if (moment) {
|
|||
|
|
if (!moment.comments) moment.comments = []
|
|||
|
|
moment.comments.push(comment)
|
|||
|
|
moment.comment_count++
|
|||
|
|
}
|
|||
|
|
return comment
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
moments, currentMoment, notifications, unreadCount, loading, pagination, hasUnread,
|
|||
|
|
fetchMoments, toggleLike, addComment
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、WebSocket 通讯
|
|||
|
|
|
|||
|
|
### 7.1 WebSocket 管理器
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// api/websocket/index.ts
|
|||
|
|
import type { ChatMessage } from '@/types/api'
|
|||
|
|
import type { MomentNotifPayload } from '@/types/moment'
|
|||
|
|
|
|||
|
|
export interface WebSocketMessage {
|
|||
|
|
request_type?: string
|
|||
|
|
clientId?: string
|
|||
|
|
data?: ChatMessage | MomentNotifPayload
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type MessageHandler = (message: ChatMessage) => void
|
|||
|
|
export type MomentNotifHandler = (payload: MomentNotifPayload) => void
|
|||
|
|
|
|||
|
|
class WebSocketManager {
|
|||
|
|
private socketTask: UniApp.SocketTask | null = null
|
|||
|
|
private clientId: string | null = null
|
|||
|
|
private userId: string | null = null
|
|||
|
|
private messageHandlers: MessageHandler[] = []
|
|||
|
|
private momentNotifHandlers: MomentNotifHandler[] = []
|
|||
|
|
private reconnectAttempts = 0
|
|||
|
|
private maxReconnectAttempts = 5
|
|||
|
|
private reconnectDelay = 3000
|
|||
|
|
|
|||
|
|
connect(userId: string): Promise<void> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
if (this.socketTask) {
|
|||
|
|
resolve()
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.userId = userId
|
|||
|
|
const wsUrl = `ws://your-server/ws?user_id=${userId}`
|
|||
|
|
|
|||
|
|
this.socketTask = uni.connectSocket({
|
|||
|
|
url: wsUrl,
|
|||
|
|
success: () => console.log('WebSocket connecting...'),
|
|||
|
|
fail: (err) => reject(err)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.socketTask.onOpen(() => {
|
|||
|
|
console.log('WebSocket connected')
|
|||
|
|
this.reconnectAttempts = 0
|
|||
|
|
resolve()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.socketTask.onMessage((res) => {
|
|||
|
|
try {
|
|||
|
|
const payload: WebSocketMessage = JSON.parse(res.data as string)
|
|||
|
|
if (payload.clientId) this.clientId = payload.clientId
|
|||
|
|
if (payload.request_type === 'receive_message' && payload.data) {
|
|||
|
|
this.messageHandlers.forEach(h => h(payload.data as ChatMessage))
|
|||
|
|
}
|
|||
|
|
if (payload.request_type === 'moment_notification' && payload.data) {
|
|||
|
|
this.momentNotifHandlers.forEach(h => h(payload.data as MomentNotifPayload))
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('Parse message error:', e)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.socketTask.onClose(() => {
|
|||
|
|
console.log('WebSocket closed')
|
|||
|
|
this.socketTask = null
|
|||
|
|
this.attemptReconnect()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
this.socketTask.onError((err) => {
|
|||
|
|
console.error('WebSocket error:', err)
|
|||
|
|
reject(err)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
disconnect() {
|
|||
|
|
if (this.socketTask) {
|
|||
|
|
this.socketTask.close({})
|
|||
|
|
this.socketTask = null
|
|||
|
|
}
|
|||
|
|
this.clientId = null
|
|||
|
|
this.userId = null
|
|||
|
|
this.messageHandlers = []
|
|||
|
|
this.momentNotifHandlers = []
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getClientId(): string | null {
|
|||
|
|
return this.clientId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMessage(handler: MessageHandler) {
|
|||
|
|
if (!this.messageHandlers.includes(handler)) {
|
|||
|
|
this.messageHandlers.push(handler)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
offMessage(handler: MessageHandler) {
|
|||
|
|
const index = this.messageHandlers.indexOf(handler)
|
|||
|
|
if (index > -1) this.messageHandlers.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMomentNotification(handler: MomentNotifHandler) {
|
|||
|
|
if (!this.momentNotifHandlers.includes(handler)) {
|
|||
|
|
this.momentNotifHandlers.push(handler)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
offMomentNotification(handler: MomentNotifHandler) {
|
|||
|
|
const index = this.momentNotifHandlers.indexOf(handler)
|
|||
|
|
if (index > -1) this.momentNotifHandlers.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private attemptReconnect() {
|
|||
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts || !this.userId) return
|
|||
|
|
this.reconnectAttempts++
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (this.userId) this.connect(this.userId).catch(console.error)
|
|||
|
|
}, this.reconnectDelay)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const wsManager = new WebSocketManager()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、主题系统
|
|||
|
|
|
|||
|
|
### 8.1 CSS 变量
|
|||
|
|
|
|||
|
|
```scss
|
|||
|
|
// styles/variables.scss
|
|||
|
|
page {
|
|||
|
|
// 浅色主题(默认)
|
|||
|
|
--color-primary: #07c160;
|
|||
|
|
--color-danger: #fa5151;
|
|||
|
|
--color-warning: #ffc300;
|
|||
|
|
--color-success: #07c160;
|
|||
|
|
|
|||
|
|
--bg-page: #ededed;
|
|||
|
|
--bg-content: #ffffff;
|
|||
|
|
--bg-hover: #f5f5f5;
|
|||
|
|
--bg-mask: rgba(0, 0, 0, 0.5);
|
|||
|
|
|
|||
|
|
--text-primary: #333333;
|
|||
|
|
--text-secondary: #666666;
|
|||
|
|
--text-tertiary: #999999;
|
|||
|
|
--text-placeholder: #cccccc;
|
|||
|
|
|
|||
|
|
--border-color: #e5e5e5;
|
|||
|
|
--divider-color: #f0f0f0;
|
|||
|
|
|
|||
|
|
--nav-bg: #ededed;
|
|||
|
|
--tabbar-bg: #f7f7f7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
page.dark {
|
|||
|
|
// 暗色主题
|
|||
|
|
--bg-page: #111111;
|
|||
|
|
--bg-content: #1e1e1e;
|
|||
|
|
--bg-hover: #2a2a2a;
|
|||
|
|
--bg-mask: rgba(0, 0, 0, 0.7);
|
|||
|
|
|
|||
|
|
--text-primary: #e5e5e5;
|
|||
|
|
--text-secondary: #a0a0a0;
|
|||
|
|
--text-tertiary: #666666;
|
|||
|
|
--text-placeholder: #444444;
|
|||
|
|
|
|||
|
|
--border-color: #333333;
|
|||
|
|
--divider-color: #2a2a2a;
|
|||
|
|
|
|||
|
|
--nav-bg: #1a1a1a;
|
|||
|
|
--tabbar-bg: #1a1a1a;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 主题切换
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// composables/useTheme.ts
|
|||
|
|
import { ref, watch } from 'vue'
|
|||
|
|
|
|||
|
|
const isDark = ref(false)
|
|||
|
|
|
|||
|
|
export function useTheme() {
|
|||
|
|
function initTheme() {
|
|||
|
|
// 读取缓存
|
|||
|
|
const cached = uni.getStorageSync('theme')
|
|||
|
|
if (cached) {
|
|||
|
|
isDark.value = cached === 'dark'
|
|||
|
|
} else {
|
|||
|
|
// 跟随系统
|
|||
|
|
const info = uni.getSystemInfoSync()
|
|||
|
|
isDark.value = info.theme === 'dark'
|
|||
|
|
}
|
|||
|
|
applyTheme()
|
|||
|
|
|
|||
|
|
// 监听系统主题变化
|
|||
|
|
uni.onThemeChange?.((res) => {
|
|||
|
|
if (!uni.getStorageSync('theme')) {
|
|||
|
|
isDark.value = res.theme === 'dark'
|
|||
|
|
applyTheme()
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleTheme() {
|
|||
|
|
isDark.value = !isDark.value
|
|||
|
|
uni.setStorageSync('theme', isDark.value ? 'dark' : 'light')
|
|||
|
|
applyTheme()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyTheme() {
|
|||
|
|
// 设置页面根元素 class
|
|||
|
|
// #ifdef H5
|
|||
|
|
document.documentElement.className = isDark.value ? 'dark' : ''
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// 设置导航栏颜色
|
|||
|
|
uni.setNavigationBarColor({
|
|||
|
|
frontColor: isDark.value ? '#ffffff' : '#000000',
|
|||
|
|
backgroundColor: isDark.value ? '#1a1a1a' : '#ededed'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 设置 TabBar 样式
|
|||
|
|
uni.setTabBarStyle({
|
|||
|
|
backgroundColor: isDark.value ? '#1a1a1a' : '#f7f7f7',
|
|||
|
|
borderStyle: isDark.value ? 'black' : 'white'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { isDark, initTheme, toggleTheme }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.3 使用示例
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="container">
|
|||
|
|
<text class="title">标题</text>
|
|||
|
|
<text class="desc">描述文字</text>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.container {
|
|||
|
|
background: var(--bg-content);
|
|||
|
|
padding: 20rpx;
|
|||
|
|
}
|
|||
|
|
.title {
|
|||
|
|
color: var(--text-primary);
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
}
|
|||
|
|
.desc {
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、平台适配
|
|||
|
|
|
|||
|
|
### 9.1 条件编译
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 代码中
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
console.log('这是 App')
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
console.log('这是微信小程序')
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef H5
|
|||
|
|
console.log('这是 H5')
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef MP-WEIXIN
|
|||
|
|
console.log('不是微信小程序的平台')
|
|||
|
|
// #endif
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<!-- 模板中 -->
|
|||
|
|
<!-- #ifdef APP-PLUS -->
|
|||
|
|
<view>仅 App 显示</view>
|
|||
|
|
<!-- #endif -->
|
|||
|
|
|
|||
|
|
<!-- #ifdef MP-WEIXIN -->
|
|||
|
|
<button open-type="share">分享</button>
|
|||
|
|
<!-- #endif -->
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 平台工具函数
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// utils/platform.ts
|
|||
|
|
|
|||
|
|
/** 是否 App */
|
|||
|
|
export function isApp(): boolean {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
return true
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
return false
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 是否微信小程序 */
|
|||
|
|
export function isWeixin(): boolean {
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
return true
|
|||
|
|
// #endif
|
|||
|
|
// #ifndef MP-WEIXIN
|
|||
|
|
return false
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 选择图片 */
|
|||
|
|
export function chooseImage(count = 9): Promise<string[]> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
uni.chooseImage({
|
|||
|
|
count,
|
|||
|
|
sizeType: ['compressed'],
|
|||
|
|
sourceType: ['album', 'camera'],
|
|||
|
|
success: (res) => resolve(res.tempFilePaths),
|
|||
|
|
fail: reject
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 选择文件 */
|
|||
|
|
export function chooseFile(): Promise<UniApp.ChooseFileSuccessCallbackResult> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
uni.chooseFile({
|
|||
|
|
count: 1,
|
|||
|
|
success: resolve,
|
|||
|
|
fail: reject
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef MP-WEIXIN
|
|||
|
|
wx.chooseMessageFile({
|
|||
|
|
count: 1,
|
|||
|
|
type: 'file',
|
|||
|
|
success: resolve,
|
|||
|
|
fail: reject
|
|||
|
|
})
|
|||
|
|
// #endif
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 扫码 */
|
|||
|
|
export function scanCode(): Promise<string> {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
uni.scanCode({
|
|||
|
|
onlyFromCamera: false,
|
|||
|
|
success: (res) => resolve(res.result),
|
|||
|
|
fail: reject
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.3 存储封装
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// utils/storage.ts
|
|||
|
|
const KEYS = {
|
|||
|
|
TOKEN: 'nl_im_token',
|
|||
|
|
USER_ID: 'nl_im_user_id',
|
|||
|
|
USER_INFO: 'nl_im_user_info',
|
|||
|
|
THEME: 'nl_im_theme',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const storage = {
|
|||
|
|
// Token
|
|||
|
|
setToken(token: string) {
|
|||
|
|
uni.setStorageSync(KEYS.TOKEN, token)
|
|||
|
|
},
|
|||
|
|
getToken(): string | null {
|
|||
|
|
return uni.getStorageSync(KEYS.TOKEN) || null
|
|||
|
|
},
|
|||
|
|
removeToken() {
|
|||
|
|
uni.removeStorageSync(KEYS.TOKEN)
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// User ID
|
|||
|
|
setUserId(id: string) {
|
|||
|
|
uni.setStorageSync(KEYS.USER_ID, id)
|
|||
|
|
},
|
|||
|
|
getUserId(): string | null {
|
|||
|
|
return uni.getStorageSync(KEYS.USER_ID) || null
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// User Info
|
|||
|
|
setUserInfo(info: any) {
|
|||
|
|
uni.setStorageSync(KEYS.USER_INFO, JSON.stringify(info))
|
|||
|
|
},
|
|||
|
|
getUserInfo(): any | null {
|
|||
|
|
const str = uni.getStorageSync(KEYS.USER_INFO)
|
|||
|
|
return str ? JSON.parse(str) : null
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// Theme
|
|||
|
|
setTheme(theme: 'light' | 'dark') {
|
|||
|
|
uni.setStorageSync(KEYS.THEME, theme)
|
|||
|
|
},
|
|||
|
|
getTheme(): 'light' | 'dark' | null {
|
|||
|
|
return uni.getStorageSync(KEYS.THEME) || null
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// Clear all
|
|||
|
|
clear() {
|
|||
|
|
Object.values(KEYS).forEach(key => uni.removeStorageSync(key))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十、UI 组件(wot-design-uni)
|
|||
|
|
|
|||
|
|
### 10.1 常用组件
|
|||
|
|
|
|||
|
|
| 组件 | 用途 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| `wd-button` | 按钮 | `<wd-button type="primary">提交</wd-button>` |
|
|||
|
|
| `wd-input` | 输入框 | `<wd-input v-model="value" placeholder="请输入" />` |
|
|||
|
|
| `wd-cell` | 单元格 | `<wd-cell title="标题" value="内容" />` |
|
|||
|
|
| `wd-popup` | 弹出层 | `<wd-popup v-model="show" position="bottom">...</wd-popup>` |
|
|||
|
|
| `wd-toast` | 轻提示 | `uni.$wot.toast('提示内容')` |
|
|||
|
|
| `wd-dialog` | 对话框 | `uni.$wot.confirm({ title: '提示', msg: '确定删除?' })` |
|
|||
|
|
| `wd-action-sheet` | 动作面板 | 底部弹出选项 |
|
|||
|
|
| `wd-search` | 搜索框 | `<wd-search v-model="keyword" />` |
|
|||
|
|
| `wd-badge` | 徽标 | `<wd-badge :value="99" />` |
|
|||
|
|
| `wd-icon` | 图标 | `<wd-icon name="chat" />` |
|
|||
|
|
| `wd-image` | 图片 | 支持懒加载、预览 |
|
|||
|
|
| `wd-swipe-action` | 滑动操作 | 左滑删除等 |
|
|||
|
|
|
|||
|
|
### 10.2 自动导入配置
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// vite.config.ts
|
|||
|
|
import Components from '@uni-helper/vite-plugin-uni-components'
|
|||
|
|
import { WotResolver } from '@uni-helper/vite-plugin-uni-components/resolvers'
|
|||
|
|
|
|||
|
|
export default defineConfig({
|
|||
|
|
plugins: [
|
|||
|
|
Components({
|
|||
|
|
resolvers: [WotResolver()]
|
|||
|
|
})
|
|||
|
|
]
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十一、工具函数
|
|||
|
|
|
|||
|
|
### 11.1 格式化函数
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// utils/format.ts
|
|||
|
|
|
|||
|
|
/** 格式化相对时间 */
|
|||
|
|
export function formatTime(timestamp: number | string): string {
|
|||
|
|
const date = new Date(timestamp)
|
|||
|
|
const now = new Date()
|
|||
|
|
const diff = now.getTime() - date.getTime()
|
|||
|
|
const seconds = Math.floor(diff / 1000)
|
|||
|
|
const minutes = Math.floor(seconds / 60)
|
|||
|
|
const hours = Math.floor(minutes / 60)
|
|||
|
|
const days = Math.floor(hours / 24)
|
|||
|
|
|
|||
|
|
if (seconds < 60) return '刚刚'
|
|||
|
|
if (minutes < 60) return `${minutes}分钟前`
|
|||
|
|
if (hours < 24) return `${hours}小时前`
|
|||
|
|
if (days === 1) return '昨天'
|
|||
|
|
if (days < 7) return `${days}天前`
|
|||
|
|
|
|||
|
|
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 格式化文件大小 */
|
|||
|
|
export function formatSize(bytes: number): string {
|
|||
|
|
if (!bytes) return '0 B'
|
|||
|
|
const k = 1024
|
|||
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|||
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 格式化时长(秒转 MM:SS) */
|
|||
|
|
export function formatDuration(seconds: number): string {
|
|||
|
|
const m = Math.floor(seconds / 60)
|
|||
|
|
const s = seconds % 60
|
|||
|
|
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.2 消息处理
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// utils/message.ts
|
|||
|
|
import { MessageType } from '@/types/message'
|
|||
|
|
import type { ChatMessage } from '@/types/api'
|
|||
|
|
|
|||
|
|
/** 获取消息摘要 */
|
|||
|
|
export function getMessageSummary(msg: ChatMessage): string {
|
|||
|
|
switch (msg.message_type) {
|
|||
|
|
case MessageType.TEXT:
|
|||
|
|
return msg.content || ''
|
|||
|
|
case MessageType.IMAGE:
|
|||
|
|
return '[图片]'
|
|||
|
|
case MessageType.AUDIO:
|
|||
|
|
return '[语音]'
|
|||
|
|
case MessageType.VIDEO:
|
|||
|
|
return '[视频]'
|
|||
|
|
case MessageType.FILE:
|
|||
|
|
return '[文件]'
|
|||
|
|
case MessageType.SYSTEM:
|
|||
|
|
case MessageType.GROUP_NOTIFY:
|
|||
|
|
return msg.content || '[系统消息]'
|
|||
|
|
default:
|
|||
|
|
return '[未知消息]'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 是否系统消息 */
|
|||
|
|
export function isSystemMessage(type: number): boolean {
|
|||
|
|
return [
|
|||
|
|
MessageType.SYSTEM,
|
|||
|
|
MessageType.FRIEND_NOTIFY,
|
|||
|
|
MessageType.GROUP_NOTIFY
|
|||
|
|
].includes(type)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十二、开发规范
|
|||
|
|
|
|||
|
|
### 12.1 命名规范
|
|||
|
|
|
|||
|
|
| 类型 | 规范 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 文件夹 | 小写连字符 | `chat-bubble/` |
|
|||
|
|
| 页面文件 | 小写 | `index.vue`, `detail.vue` |
|
|||
|
|
| 组件文件 | PascalCase | `MessageBubble.vue` |
|
|||
|
|
| 变量/函数 | camelCase | `handleSend`, `isLoading` |
|
|||
|
|
| 常量 | UPPER_SNAKE | `MESSAGE_TYPE`, `API_BASE` |
|
|||
|
|
| 类型/接口 | PascalCase | `User`, `ChatMessage` |
|
|||
|
|
| CSS 类名 | BEM 或小写连字符 | `.message-bubble__content` |
|
|||
|
|
|
|||
|
|
### 12.2 组件模板
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<template>
|
|||
|
|
<view class="component-name">
|
|||
|
|
<!-- 内容 -->
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
/**
|
|||
|
|
* 组件名称
|
|||
|
|
* @description 组件描述
|
|||
|
|
*/
|
|||
|
|
import { ref, computed, onMounted } from 'vue'
|
|||
|
|
|
|||
|
|
// Props
|
|||
|
|
interface Props {
|
|||
|
|
title: string
|
|||
|
|
disabled?: boolean
|
|||
|
|
}
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
disabled: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Emits
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
click: [id: string]
|
|||
|
|
change: [value: string]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
// State
|
|||
|
|
const loading = ref(false)
|
|||
|
|
|
|||
|
|
// Computed
|
|||
|
|
const displayTitle = computed(() => props.title.toUpperCase())
|
|||
|
|
|
|||
|
|
// Methods
|
|||
|
|
function handleClick() {
|
|||
|
|
emit('click', 'some-id')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Lifecycle
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 初始化
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.component-name {
|
|||
|
|
// 样式
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 12.3 Git 提交规范
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
<type>(<scope>): <subject>
|
|||
|
|
|
|||
|
|
类型:
|
|||
|
|
- feat: 新功能
|
|||
|
|
- fix: 修复 bug
|
|||
|
|
- docs: 文档变更
|
|||
|
|
- style: 代码格式(不影响功能)
|
|||
|
|
- refactor: 重构
|
|||
|
|
- perf: 性能优化
|
|||
|
|
- test: 测试
|
|||
|
|
- chore: 构建/工具变动
|
|||
|
|
|
|||
|
|
示例:
|
|||
|
|
feat(chat): 添加语音消息发送功能
|
|||
|
|
fix(contact): 修复好友列表不刷新问题
|
|||
|
|
docs: 更新开发手册
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十三、快速开始
|
|||
|
|
|
|||
|
|
### 13.1 环境准备
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装 Node.js 18+
|
|||
|
|
# 安装 pnpm
|
|||
|
|
npm install -g pnpm
|
|||
|
|
|
|||
|
|
# 安装 HBuilderX(可选,用于 App 开发)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 13.2 项目启动
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 进入项目
|
|||
|
|
cd nl-im-uniapp
|
|||
|
|
|
|||
|
|
# 安装依赖
|
|||
|
|
pnpm install
|
|||
|
|
|
|||
|
|
# 开发模式
|
|||
|
|
pnpm dev # H5
|
|||
|
|
pnpm dev:mp-weixin # 微信小程序
|
|||
|
|
pnpm dev:app # App
|
|||
|
|
|
|||
|
|
# 构建
|
|||
|
|
pnpm build:mp-weixin # 微信小程序
|
|||
|
|
pnpm build:app # App
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 13.3 微信小程序调试
|
|||
|
|
|
|||
|
|
1. 运行 `pnpm dev:mp-weixin`
|
|||
|
|
2. 打开**微信开发者工具**
|
|||
|
|
3. 导入 `dist/dev/mp-weixin` 目录
|
|||
|
|
4. 填写 AppID
|
|||
|
|
|
|||
|
|
### 13.4 App 调试
|
|||
|
|
|
|||
|
|
1. 运行 `pnpm dev:app`
|
|||
|
|
2. 打开 HBuilderX
|
|||
|
|
3. 运行到手机或模拟器
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 十四、功能清单
|
|||
|
|
|
|||
|
|
### 已规划功能
|
|||
|
|
|
|||
|
|
| 模块 | 功能 | 优先级 | 状态 |
|
|||
|
|
|------|------|--------|------|
|
|||
|
|
| 基础 | 项目框架搭建 | P0 | 待开发 |
|
|||
|
|
| 基础 | 主题系统(浅色/暗色) | P0 | 待开发 |
|
|||
|
|
| 基础 | API 请求封装 | P0 | 待开发 |
|
|||
|
|
| 基础 | WebSocket 适配 | P0 | 待开发 |
|
|||
|
|
| 基础 | Pinia Store | P0 | 待开发 |
|
|||
|
|
| 登录 | 登录/注册页 | P0 | 待开发 |
|
|||
|
|
| 首页 | 会话列表 | P0 | 待开发 |
|
|||
|
|
| 首页 | 左侧抽屉 | P1 | 待开发 |
|
|||
|
|
| 首页 | 加号菜单 | P1 | 待开发 |
|
|||
|
|
| 聊天 | 聊天详情页 | P0 | 待开发 |
|
|||
|
|
| 聊天 | 多类型消息展示 | P0 | 待开发 |
|
|||
|
|
| 聊天 | 消息输入框 | P0 | 待开发 |
|
|||
|
|
| 聊天 | 图片/文件发送 | P1 | 待开发 |
|
|||
|
|
| 聊天 | 语音消息 | P2 | 待开发 |
|
|||
|
|
| 联系人 | 联系人列表 | P0 | 待开发 |
|
|||
|
|
| 联系人 | 好友详情 | P1 | 待开发 |
|
|||
|
|
| 联系人 | 添加好友 | P1 | 待开发 |
|
|||
|
|
| 联系人 | 好友申请 | P1 | 待开发 |
|
|||
|
|
| 群聊 | 群聊列表 | P1 | 待开发 |
|
|||
|
|
| 群聊 | 创建群聊 | P1 | 待开发 |
|
|||
|
|
| 群聊 | 群信息/成员管理 | P2 | 待开发 |
|
|||
|
|
| 朋友圈 | 动态列表 | P1 | 待开发 |
|
|||
|
|
| 朋友圈 | 发布动态 | P1 | 待开发 |
|
|||
|
|
| 朋友圈 | 点赞/评论 | P1 | 待开发 |
|
|||
|
|
| 朋友圈 | 消息通知 | P2 | 待开发 |
|
|||
|
|
| 设置 | 个人资料 | P1 | 待开发 |
|
|||
|
|
| 设置 | 设置页 | P2 | 待开发 |
|
|||
|
|
| 高级 | 消息推送 | P2 | 待开发 |
|
|||
|
|
| 高级 | 语音/视频通话 | P3 | 待开发 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 附录
|
|||
|
|
|
|||
|
|
### A. 相关资源
|
|||
|
|
|
|||
|
|
- [UniApp 官方文档](https://uniapp.dcloud.net.cn/)
|
|||
|
|
- [wot-design-uni 组件库](https://wot-design-uni.cn/)
|
|||
|
|
- [Pinia 文档](https://pinia.vuejs.org/)
|
|||
|
|
- [Vue 3 文档](https://cn.vuejs.org/)
|
|||
|
|
|
|||
|
|
### B. 后端 API 文档
|
|||
|
|
|
|||
|
|
请参考 `nl-im-vue-ts` 项目或后端接口文档。
|
|||
|
|
|
|||
|
|
### C. 更新日志
|
|||
|
|
|
|||
|
|
| 日期 | 版本 | 更新内容 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| 2024-12-08 | v0.1.0 | 初始化开发手册 |
|