feat(circle): 集成HaveADrink SDK实现酒友圈功能

- 集成HaveADrink SDK版本从1.0.4升级至1.0.8
- 实现酒友圈API接口,替换原有的mock数据
- 添加地理位置权限配置(requiredPrivateInfos)
- 重构API调用方式,统一使用client实例调用真实接口
- 更新WebSocket协议与SDK保持一致,支持聊天、输入状态、已读回执等功能
- 实现游标分页获取动态信息流
- 添加文件上传API支持图片上传
- 优化API错误处理,支持业务级错误提示
- 调整酒局状态显示,新增upcoming待开始状态
- 优化自我感觉标签映射逻辑,支持更多状态类型
- 更新websocket连接认证方式,使用Authorization header传递token
- 添加服务端连接确认和被踢下线事件处理
This commit is contained in:
cg
2026-07-22 23:03:40 +08:00
parent 9052a6f4da
commit c7c023975d
21 changed files with 5677 additions and 385 deletions
+90 -72
View File
@@ -32,8 +32,17 @@ function httpRequest(url, params) {
success: (res) => { success: (res) => {
const code = res.statusCode const code = res.statusCode
if (code === 200 || code === 201 || code === 204) { if (code === 200 || code === 201 || code === 204) {
// 业务级错误:HTTP 200 但 fail:true
if (res.data && res.data.fail === true) {
const bizMsg = res.data.msg || '操作失败'
if (!silent) {
uni.showToast({ title: bizMsg, icon: 'none', duration: 2500 })
}
reject({ msg: bizMsg, code })
return
}
resolve(res.data) resolve(res.data)
} else if (code === 401 || code === 418) { } else if (code === 401 ) {
// token/refresh_token 过期或无效,清除登录态 // token/refresh_token 过期或无效,清除登录态
uni.removeStorageSync('auth_token') uni.removeStorageSync('auth_token')
uni.removeStorageSync('refresh_token') uni.removeStorageSync('refresh_token')
@@ -76,8 +85,8 @@ function uploadRequest(url, params) {
uni.uploadFile({ uni.uploadFile({
url: url, url: url,
filePath: params.data.fileName || params.data.filePath, filePath: params.data.image || params.data.fileName || params.data.filePath,
name: params.data.name || 'file', name: params.data.name || 'image',
header: headers, header: headers,
formData: params.data.formData || {}, formData: params.data.formData || {},
success: (uploadRes) => { success: (uploadRes) => {
@@ -156,137 +165,146 @@ export async function refreshAuthToken() {
} }
// ========================================== // ==========================================
// 酒友圈 API当前返回 Mock 数据,后端就绪后切换为 SDK 调用 // 酒友圈 API基于 HaveADrink SDK 真实接口
// ========================================== // ==========================================
import { MOCK_FEEDS, MOCK_COMMENTS, MOCK_FRIENDS, MOCK_FRIEND_REQUESTS, MOCK_EVENTS, MOCK_CONVERSATIONS, MOCK_MESSAGES } from './mock-data'
/** 模拟网络延迟 */ /** 获取动态信息流(游标分页) */
function mockDelay(data, ms = 300) { export async function GetCircleFeeds({ lastId = 0, pageSize = 10 } = {}) {
return new Promise(resolve => setTimeout(() => resolve({ data }), ms)) const res = await client.GetFeeds({ lastId, pageSize })
} return { data: res }
/** 获取动态信息流 */
export function GetCircleFeeds({ page = 1, pageSize = 10 } = {}) {
// TODO: 后端就绪后替换为 client.GetCircleFeeds(...)
const start = (page - 1) * pageSize
const list = MOCK_FEEDS.slice(start, start + pageSize)
return mockDelay({ list, total: MOCK_FEEDS.length, hasMore: start + pageSize < MOCK_FEEDS.length })
} }
/** 点赞动态 */ /** 点赞动态 */
export function LikeFeed({ feedId }) { export async function LikeFeed({ feedId }) {
return mockDelay({ success: true }) const res = await client.LikeFeed({ id: feedId })
return { data: res }
} }
/** 取消点赞 */ /** 取消点赞 */
export function UnlikeFeed({ feedId }) { export async function UnlikeFeed({ feedId }) {
return mockDelay({ success: true }) const res = await client.UnlikeFeed({ id: feedId })
return { data: res }
} }
/** 获取动态评论 */ /** 获取动态评论 */
export function GetFeedComments({ feedId }) { export async function GetFeedComments({ feedId }) {
const list = MOCK_COMMENTS[feedId] || [] const res = await client.GetFeedComments({ id: feedId })
return mockDelay({ list }) return { data: res }
} }
/** 发表评论 */ /** 发表评论 */
export function AddComment({ feedId, content }) { export async function AddComment({ feedId, content }) {
return mockDelay({ success: true, comment: { id: 'c_' + Date.now(), content, time: '刚刚' } }) const res = await client.AddComment({ id: feedId, content })
return { data: res }
} }
/** 发布动态 */ /** 发布动态 */
export function PublishFeed({ text, images, recordId, visibility }) { export async function PublishFeed({ text, images, recordId, visibility }) {
return mockDelay({ success: true, feedId: 'feed_' + Date.now() }) const res = await client.PublishFeed({ text, images, recordId, visibility })
return { data: res }
} }
/** 获取酒友列表 */ /** 获取酒友列表 */
export function GetFriends({ keyword = '' } = {}) { export async function GetFriends({ keyword = '' } = {}) {
const list = keyword const res = await client.GetFriends({ keyword })
? MOCK_FRIENDS.filter(f => f.nickname.includes(keyword)) return { data: res }
: MOCK_FRIENDS
return mockDelay({ list })
} }
/** 获取好友请求 */ /** 获取好友请求 */
export function GetFriendRequests() { export async function GetFriendRequests() {
return mockDelay({ list: MOCK_FRIEND_REQUESTS }) const res = await client.GetFriendRequests({})
return { data: res }
} }
/** 发送好友请求 */ /** 发送好友请求 */
export function SendFriendRequest({ userId }) { export async function SendFriendRequest({ userId, message = '' }) {
return mockDelay({ success: true }) const res = await client.SendFriendRequest({ userId, message })
return { data: res }
} }
/** 接受好友请求 */ /** 接受好友请求 */
export function AcceptFriendRequest({ requestId }) { export async function AcceptFriendRequest({ requestId }) {
return mockDelay({ success: true }) const res = await client.AcceptFriendRequest({ id: requestId })
return { data: res }
} }
/** 删除酒友 */ /** 删除酒友 */
export function RemoveFriend({ userId }) { export async function RemoveFriend({ userId }) {
return mockDelay({ success: true }) const res = await client.RemoveFriend({ userId })
return { data: res }
} }
/** 获取酒局列表 */ /** 获取酒局列表 */
export function GetEvents({ status = '' } = {}) { export async function GetEvents({ status = '' } = {}) {
const list = status ? MOCK_EVENTS.filter(e => e.status === status) : MOCK_EVENTS const res = await client.GetEvents({ status })
return mockDelay({ list }) return { data: res }
} }
/** 获取酒局详情 */ /** 获取酒局详情 */
export function GetEventDetail({ eventId }) { export async function GetEventDetail({ eventId }) {
const evt = MOCK_EVENTS.find(e => e.id === eventId) || MOCK_EVENTS[0] const res = await client.GetEventDetail({ id: eventId })
return mockDelay(evt) return { data: res }
} }
/** 发起酒局 */ /** 发起酒局 */
export function CreateEvent({ title, time, location, maxPeople, note }) { export async function CreateEvent({ title, time, location, maxPeople, note, latitude, longitude }) {
return mockDelay({ success: true, eventId: 'evt_' + Date.now() }) const geo = (latitude != null && longitude != null) ? { latitude, longitude } : undefined
const res = await client.CreateEvent({ title, time, location, maxPeople, geo, note })
return { data: res }
} }
/** 报名酒局 */ /** 报名酒局 */
export function JoinEvent({ eventId }) { export async function JoinEvent({ eventId }) {
return mockDelay({ success: true }) const res = await client.JoinEvent({ id: eventId })
return { data: res }
} }
/** 取消报名 */ /** 取消报名 */
export function QuitEvent({ eventId }) { export async function QuitEvent({ eventId }) {
return mockDelay({ success: true }) const res = await client.QuitEvent({ id: eventId })
return { data: res }
} }
/** 酒局签到 */ /** 酒局签到 */
export function CheckInEvent({ eventId }) { export async function CheckInEvent({ eventId }) {
return mockDelay({ success: true }) const res = await client.CheckInEvent({ id: eventId })
return { data: res }
} }
// ========================================== // ==========================================
// 私信 API当前返回 Mock 数据,后端就绪后切换为 SDK 调用 // 私信 API基于 HaveADrink SDK 真实接口
// ========================================== // ==========================================
/** 获取会话列表 */ /** 获取会话列表 */
export function GetConversations() { export async function GetConversations() {
// TODO: 后端就绪后替换为 client.GetConversations(...) const res = await client.GetConversations({})
return mockDelay({ list: MOCK_CONVERSATIONS }) return { data: res }
} }
/** 获取历史消息 */ /** 获取历史消息(游标分页) */
export function GetMessages({ conversationId, page = 1, pageSize = 20 } = {}) { export async function GetMessages({ conversationId, lastID = 0, pageSize = 20 } = {}) {
// TODO: 后端就绪后替换为 client.GetMessages(...) const res = await client.GetMessages({ conversationId, lastID, pageSize })
const all = MOCK_MESSAGES[conversationId] || [] return { data: res }
const start = (page - 1) * pageSize
const list = all.slice(start, start + pageSize)
return mockDelay({ list, hasMore: start + pageSize < all.length })
} }
/** 标记会话已读 */ /** 标记会话已读 */
export function MarkRead({ conversationId }) { export async function MarkRead({ conversationId }) {
// TODO: 后端就绪后替换为 client.MarkRead(...) const res = await client.MarkRead({ id: conversationId })
return mockDelay({ success: true }) return { data: res }
} }
/** 获取未读消息总数 */ /** 获取未读消息总数 */
export function GetUnreadCount() { export async function GetUnreadCount() {
// TODO: 后端就绪后替换为 client.GetUnreadCount(...) const res = await client.GetUnreadCount({})
const total = MOCK_CONVERSATIONS.reduce((sum, c) => sum + c.unread, 0) return { data: res }
return mockDelay({ total }) }
// ==========================================
// 文件上传 API
// ==========================================
/** 上传图片(返回 url */
export async function UploadImage({ filePath }) {
const res = await client.UploadImage({ image: filePath })
return { data: res }
} }
+57 -22
View File
@@ -1,9 +1,29 @@
/* 碰盏日记 - WebSocket 管理器 /* 碰盏日记 - WebSocket 管理器
* 单例模式,负责私信实时通信 * 单例模式,负责私信实时通信
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
* 支持心跳、自动重连、消息队列 * 支持心跳、自动重连、消息队列
*/ */
const WS_HOST = 'wss://dev.wash-painting.cn/ws' const WS_HOST = 'wss://dev.wash-painting.cn/api/have_a_drink/v1/chat/chat'
// 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
const ClientMesgType = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ping: 'ping'
}
// 服务端消息类型(与 SDK ServerMesgType 枚举值一致)
const ServerMesgType = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ack: 'ack',
Pong: 'pong',
Connected: 'connected',
Kicked: 'kicked'
}
class WebSocketManager { class WebSocketManager {
constructor() { constructor() {
@@ -21,6 +41,7 @@ class WebSocketManager {
this.heartbeatWait = 60000 // 60s 无 pong 则重连 this.heartbeatWait = 60000 // 60s 无 pong 则重连
this.manualClose = false this.manualClose = false
this.token = '' this.token = ''
this.userId = '' // 连接成功后服务端返回的用户ID
} }
/** /**
@@ -37,11 +58,14 @@ class WebSocketManager {
this.manualClose = false this.manualClose = false
this.isConnecting = true this.isConnecting = true
const url = `${WS_HOST}?token=${this.token}` const url = WS_HOST
console.log('[WS] 正在连接...', url) console.log('[WS] 正在连接...')
this.socketTask = uni.connectSocket({ this.socketTask = uni.connectSocket({
url, url,
header: {
'Authorization': `Bearer ${this.token}`
},
complete: () => {} complete: () => {}
}) })
@@ -95,17 +119,17 @@ class WebSocketManager {
} }
/** /**
* 发送消息 * 发送消息(与 SDK writeClient* 方法格式一致)
* @param {string} type - 消息类型: chat | typing | read | ping * @param {string} mesgType - 消息类型: chat | typing | read | ping
* @param {object} data - 消息数据 * @param {object} data - 消息数据
*/ */
send(type, data = {}) { send(mesgType, data = {}) {
const payload = JSON.stringify({ type, data, ts: Date.now() }) const payload = JSON.stringify({ mesgType, data })
if (this.isConnected && this.socketTask) { if (this.isConnected && this.socketTask) {
this.socketTask.send({ data: payload }) this.socketTask.send({ data: payload })
} else { } else {
// 断线暂存队列 // 断线暂存队列ping 不暂存)
if (type !== 'ping') { if (mesgType !== ClientMesgType.Ping) {
this.messageQueue.push(payload) this.messageQueue.push(payload)
} }
// 尝试重连 // 尝试重连
@@ -117,7 +141,7 @@ class WebSocketManager {
/** /**
* 订阅事件 * 订阅事件
* @param {string} event - message | typing | read | ack | connect | disconnect * @param {string} event - message | typing | read | ack | connected | kicked | connect | disconnect
* @param {function} callback * @param {function} callback
*/ */
on(event, callback) { on(event, callback) {
@@ -143,27 +167,38 @@ class WebSocketManager {
// ========== 内部方法 ========== // ========== 内部方法 ==========
/** 处理收到的消息 */ /** 处理收到的消息SDK ServerMesgType 协议) */
_handleMessage(msg) { _handleMessage(msg) {
const { type, data } = msg const { mesgType, data } = msg
switch (type) { switch (mesgType) {
case 'pong': case ServerMesgType.Chat:
this._onPong()
break
case 'chat':
this._emit('message', data) this._emit('message', data)
break break
case 'typing': case ServerMesgType.Typing:
this._emit('typing', data) this._emit('typing', data)
break break
case 'read': case ServerMesgType.Read:
this._emit('read', data) this._emit('read', data)
break break
case 'ack': case ServerMesgType.Ack:
this._emit('ack', data) this._emit('ack', data)
break break
case ServerMesgType.Pong:
this._onPong()
break
case ServerMesgType.Connected:
// 服务端验证 token 成功后推送,携带 userId
this.userId = data && data.userId ? data.userId : ''
console.log('[WS] 服务端确认连接, userId:', this.userId)
this._emit('connected', data)
break
case ServerMesgType.Kicked:
console.warn('[WS] 被踢下线')
this._emit('kicked', data)
this.disconnect()
break
default: default:
console.log('[WS] 未知消息类型', type) console.log('[WS] 未知消息类型', mesgType)
} }
} }
@@ -181,7 +216,7 @@ class WebSocketManager {
_startHeartbeat() { _startHeartbeat() {
this._stopHeartbeat() this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
this.send('ping') this.send(ClientMesgType.Ping, {})
// 设置超时检测 // 设置超时检测
this.heartbeatTimeout = setTimeout(() => { this.heartbeatTimeout = setTimeout(() => {
console.warn('[WS] 心跳超时,触发重连') console.warn('[WS] 心跳超时,触发重连')
+7 -2
View File
@@ -49,8 +49,9 @@ export default {
emits: ['item-click'], emits: ['item-click'],
computed: { computed: {
statusLabel() { statusLabel() {
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' } const val = String(this.event.status).toLowerCase()
return map[this.event.status] || '报名中' const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中'
} }
} }
} }
@@ -80,6 +81,9 @@ export default {
&.status-open { &.status-open {
background: rgba(94,198,160,0.12); background: rgba(94,198,160,0.12);
} }
&.status-upcoming {
background: rgba(139,133,184,0.12);
}
&.status-ongoing { &.status-ongoing {
background: rgba(232,168,56,0.12); background: rgba(232,168,56,0.12);
} }
@@ -93,6 +97,7 @@ export default {
font-weight: $fw-medium; font-weight: $fw-medium;
.status-open & { color: $mint; } .status-open & { color: $mint; }
.status-upcoming & { color: $lavender; }
.status-ongoing & { color: $amber; } .status-ongoing & { color: $amber; }
.status-ended & { color: $text-tertiary; } .status-ended & { color: $text-tertiary; }
} }
+9 -2
View File
@@ -73,8 +73,15 @@ export default {
getCatIcon, getCatIcon,
isIconPath, isIconPath,
feelingLabel(f) { feelingLabel(f) {
const map = { sober: '😌 清醒', buzzed: '😊 微醺', tipsy: '😄 小醉', drunk: '🥴 喝多了' } const val = String(f).toLowerCase()
return map[f] || '🍻 喝了点' const map = {
tipsy: '😌 微醺',
buzzed: '😊 到位',
drunk: '🥴 醉了',
blackout: '😵 断片',
sober: '😌 清醒'
}
return map[val] || '🍻 喝了点'
} }
} }
} }
+1
View File
@@ -46,6 +46,7 @@
"desc" : "用于选择酒局地点和导航" "desc" : "用于选择酒局地点和导航"
} }
}, },
"requiredPrivateInfos" : ["chooseLocation", "getLocation"],
"optimization" : { "optimization" : {
"subPackages" : true "subPackages" : true
} }
+3 -3
View File
@@ -4,9 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/HaveADrink": { "node_modules/HaveADrink": {
"version": "1.0.4", "version": "1.0.8",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz", "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
"integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==", "integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
"license": "ISC" "license": "ISC"
} }
} }
+809 -55
View File
@@ -14,7 +14,7 @@
* 6. 社交模块 - 朋友圈动态、点赞 * 6. 社交模块 - 朋友圈动态、点赞
**版本:** v1.0.4 **版本:** v1.0.8
## 安装 ## 安装
@@ -350,58 +350,6 @@ const req = new GetFeedReq()
client.GetFeed(req).then(...).catch(...) client.GetFeed(req).then(...).catch(...)
``` ```
### 点赞
<font color="green">POST</font> `/api/have_a_drink/v1/communication/records/like`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|required| 记录ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
```javascript
const req = new LikeRecordReq()
client.LikeRecord(req).then(...).catch(...)
```
### 取消点赞
<font color="green">POST</font> `/api/have_a_drink/v1/communication/records/unlike`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|required| 记录ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
```javascript
const req = new LikeRecordReq()
client.UnlikeRecord(req).then(...).catch(...)
```
### 上传照片 ### 上传照片
@@ -479,7 +427,7 @@ client.UploadPhotos(req).then(...).catch(...)
**DrinkItem** **DrinkItem**
|名称|类型|校验规则|说明| |名称|类型|校验规则|说明|
|:-|:-|:-|:-| |:-|:-|:-|:-|
|category|`Category`|required| 酒类分类ID| |category|`Category`|| 酒类分类ID|
|brand|`string`|required| 品牌名称| |brand|`string`|required| 品牌名称|
|product|`string`|| 产品名| |product|`string`|| 产品名|
|amount|`number`|required,gt=0| 饮用量数值| |amount|`number`|required,gt=0| 饮用量数值|
@@ -532,7 +480,7 @@ client.UploadPhotos(req).then(...).catch(...)
**FoodInfo** **FoodInfo**
|名称|类型|校验规则|说明| |名称|类型|校验规则|说明|
|:-|:-|:-|:-| |:-|:-|:-|:-|
|category|`FoodCategory`|required| 配餐分类ID| |category|`FoodCategory`|| 配餐分类ID|
|name|`string`|| 具体菜名| |name|`string`|| 具体菜名|
@@ -1193,3 +1141,809 @@ client.UpdatePreferences(req).then(...).catch(...)
const req = new UploadImageReq() const req = new UploadImageReq()
client.UploadImage(req).then(...).catch(...) client.UploadImage(req).then(...).catch(...)
``` ```
### 动态信息流
<font color="green">GET</font> `/api/have_a_drink/v1/moments/feeds`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|lastId|`string\|number`|| 最后一条动态ID,用于分页|
|pageSize|`number`|| 每页数量|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Feed>`| 动态列表<br>|
|hasMore|`boolean`| 是否还有更多动态<br>|
**Feed**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 动态ID|
|userId|`string\|number`| 用户ID|
|nickname|`string`| 昵称|
|avatar|`string`| 头像|
|time|`string`| 发布时间|
|text|`string`| 动态文本|
|drinks|`Array<DrinkTag>`| 酒水标签|
|feeling|`Feeling`| 自我感觉|
|images|`Array<string>`| 图片列表|
|likes|`number`| 点赞数|
|liked|`boolean`| 是否点赞|
|comments|`number`| 评论数|
**DrinkTag**
|名称|类型|说明|
|:-|:-|:-|
|category|`Category`| 酒类分类|
|name|`string`| 酒类名称|
|amount|`number`| 酒类数量|
**Feeling**
|名称|值|类型|说明|
|:-|:-:|:-|:-|
|Tipsy|"tipsy"|`Feeling`|微醺|
|Buzzed|"buzzed"|`Feeling`|到位|
|Drunk|"drunk"|`Feeling`|醉了|
|Blackout|"blackout"|`Feeling`|断片|
```javascript
const req = new GetFeedsReq()
client.GetFeeds(req).then(...).catch(...)
```
### 发布动态
<font color="green">POST</font> `/api/have_a_drink/v1/moments/feeds`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|text|`string`|| 动态文本|
|images|`Array<string>`|| 图片列表|
|recordId|`string\|number`|| 关联的记录ID|
|visibility|`Visibility`|公开: Visibility.Public = "public"<br>仅好友: Visibility.Friends = "friends"<br>仅自己: Visibility.Private = "private"| 可见性|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|success|`boolean`| 是否成功<br>|
|feedId|`string\|number`| 动态ID<br>|
```javascript
const req = new PublishFeedReq()
client.PublishFeed(req).then(...).catch(...)
```
### 点赞
<font color="green">POST</font> `/api/have_a_drink/v1/moments/feeds/like`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 动态ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|ok|`boolean`| 是否成功<br>|
```javascript
const req = new LikeFeedReq()
client.LikeFeed(req).then(...).catch(...)
```
### 取消点赞
<font color="green">POST</font> `/api/have_a_drink/v1/moments/feeds/unlike`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 动态ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|ok|`boolean`| 是否成功<br>|
```javascript
const req = new UnlikeFeedReq()
client.UnlikeFeed(req).then(...).catch(...)
```
### 获取评论
<font color="green">GET</font> `/api/have_a_drink/v1/moments/feeds/comments`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 动态ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Comment>`| 评论列表<br>|
**Comment**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 评论ID|
|userId|`string\|number`| 用户ID|
|nickname|`string`| 昵称|
|avatar|`string`| 头像|
|content|`string`| 评论内容|
|time|`string`| 评论时间|
```javascript
const req = new GetFeedCommentsReq()
client.GetFeedComments(req).then(...).catch(...)
```
### 发表评论
<font color="green">POST</font> `/api/have_a_drink/v1/moments/feeds/comments`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 动态ID|
|content|`string`|| 评论内容|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|id|`string\|number`| 评论ID<br>|
|userId|`string\|number`| 用户ID<br>|
|nickname|`string`| 昵称<br>|
|avatar|`string`| 头像<br>|
|content|`string`| 评论内容<br>|
|time|`string`| 评论时间<br>|
```javascript
const req = new AddCommentFullReq()
client.AddComment(req).then(...).catch(...)
```
### 酒友管理
<font color="green">GET</font> `/api/have_a_drink/v1/friends/friends`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|keyword|`string`|| 搜索关键词|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Friend>`| 分页数据<br>|
**Friend**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 酒友ID|
|nickname|`string`| 昵称|
|avatar|`string`| 头像|
|lastDrink|`string\|number`| 最近一次喝的酒|
|lastDrinkTime|`string`| 最近一次喝的酒时间|
|online|`boolean`| 是否在线|
```javascript
const req = new GetFriendsReq()
client.GetFriends(req).then(...).catch(...)
```
### 好友请求管理
<font color="green">GET</font> `/api/have_a_drink/v1/friends/friend-requests`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<FriendRequest>`| 分页数据<br>|
**FriendRequest**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 好友请求ID|
|userId|`string\|number`| 酒友ID|
|nickname|`string`| 昵称|
|avatar|`string`| 头像|
|message|`string`| 消息|
|time|`string`| 创建时间|
```javascript
const req = new GetFriendRequestsReq()
client.GetFriendRequests(req).then(...).catch(...)
```
### 发送好友请求
<font color="green">POST</font> `/api/have_a_drink/v1/friends/friend-requests`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|userId|`string\|number`|| 酒友ID|
|message|`string`|| 消息|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
```javascript
const req = new SendFriendRequestReq()
client.SendFriendRequest(req).then(...).catch(...)
```
### 接受好友请求
<font color="green">POST</font> `/api/have_a_drink/v1/friends/friend-requests/accept`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 好友请求ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
```javascript
const req = new AcceptFriendRequestReq()
client.AcceptFriendRequest(req).then(...).catch(...)
```
### 删除酒友
<font color="green">DELETE</font> `/api/have_a_drink/v1/friends/friends`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|userId|`string\|number`|| 酒友ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
```javascript
const req = new RemoveFriendReq()
client.RemoveFriend(req).then(...).catch(...)
```
### 约酒活动
<font color="green">GET</font> `/api/have_a_drink/v1/events/events`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|status|`EventStatus`|开放报名: EventStatus.Open = 1<br>待开始: EventStatus.Upcoming = 2<br>进行中: EventStatus.Ongoing = 3<br>已结束: EventStatus.Ended = 4| 状态筛选|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Event>`| 分页数据<br>|
**Event**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 酒局ID|
|title|`string`| 酒局标题|
|organizer|`User`| 酒局组织者|
|time|`string`| 酒局时间|
|location|`string`| 酒局地点|
|maxPeople|`number`| 最大人数|
|joined|`number`| 已报名人数|
|participants|`Array<User>`| 已报名用户|
|status|`EventStatus`| 酒局状态|
|note|`string`| 酒局备注|
|isJoined|`boolean`| 是否已报名|
|isOrganizer|`boolean`| 是否为组织者|
**User**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 用户ID|
|nickname|`string`| 用户昵称|
|avatar|`string`| 头像URL|
|joinedAt|`string`| 加入时间|
|preferences|`UserPreferences`| 用户偏好|
**UserPreferences**
|名称|类型|说明|
|:-|:-|:-|
|favoriteCategories|`Array<Category>`| 偏好酒类ID列表|
|frequency|`Frequency`| 饮酒频率|
**Frequency**
|名称|值|类型|说明|
|:-|:-:|:-|:-|
|Rarely|"rarely"|`Frequency`|很少|
|Sometimes|"sometimes"|`Frequency`|有时|
|Often|"often"|`Frequency`|经常|
**EventStatus**
|名称|值|类型|说明|
|:-|:-:|:-|:-|
|Open|1|`EventStatus`|开放报名|
|Upcoming|2|`EventStatus`|待开始|
|Ongoing|3|`EventStatus`|进行中|
|Ended|4|`EventStatus`|已结束|
```javascript
const req = new GetEventsReq()
client.GetEvents(req).then(...).catch(...)
```
### 获取酒局详情
<font color="green">GET</font> `/api/have_a_drink/v1/events/events/detail`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 酒局ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|id|`string\|number`| 酒局ID<br>|
|title|`string`| 酒局标题<br>|
|organizer|`User`| 酒局组织者<br>|
|time|`string`| 酒局时间<br>|
|location|`string`| 酒局地点<br>|
|maxPeople|`number`| 最大人数<br>|
|joined|`number`| 已报名人数<br>|
|participants|`Array<User>`| 已报名用户<br>|
|status|`EventStatus`| 酒局状态<br>开放报名: EventStatus.Open = 1<br>待开始: EventStatus.Upcoming = 2<br>进行中: EventStatus.Ongoing = 3<br>已结束: EventStatus.Ended = 4|
|note|`string`| 酒局备注<br>|
|isJoined|`boolean`| 是否已报名<br>|
|isOrganizer|`boolean`| 是否为组织者<br>|
**User**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 用户ID|
|nickname|`string`| 用户昵称|
|avatar|`string`| 头像URL|
|joinedAt|`string`| 加入时间|
|preferences|`UserPreferences`| 用户偏好|
**UserPreferences**
|名称|类型|说明|
|:-|:-|:-|
|favoriteCategories|`Array<Category>`| 偏好酒类ID列表|
|frequency|`Frequency`| 饮酒频率|
```javascript
const req = new GetEventDetailReq()
client.GetEventDetail(req).then(...).catch(...)
```
### 发起酒局
<font color="green">POST</font> `/api/have_a_drink/v1/events/events`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|title|`string`|| 酒局标题|
|time|`string`|| 酒局时间|
|location|`string`|| 酒局地点|
|maxPeople|`number`|| 最大人数|
|geo|`GeoPoint`|| 坐标信息|
|note|`string`|| 酒局备注|
**GeoPoint**
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|latitude|`number`|| 纬度|
|longitude|`number`|| 经度|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|eventId|`string\|number`| 酒局ID<br>|
```javascript
const req = new CreateEventReq()
client.CreateEvent(req).then(...).catch(...)
```
### 报名酒局
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/join`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 酒局ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|ok|`boolean`| 是否成功<br>|
```javascript
const req = new JoinEventReq()
client.JoinEvent(req).then(...).catch(...)
```
### 取消报名酒局
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/quit`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 酒局ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|ok|`boolean`| 是否成功<br>|
```javascript
const req = new QuitEventReq()
client.QuitEvent(req).then(...).catch(...)
```
### 签到酒局
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/checkin`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string\|number`|| 酒局ID|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|ok|`boolean`| 是否成功<br>|
```javascript
const req = new CheckInEventReq()
client.CheckInEvent(req).then(...).catch(...)
```
### 获取会话列表
<font color="green">GET</font> `/api/have_a_drink/v1/chat/conversations`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Conversation>`| 会话列表<br>|
**Conversation**
|名称|类型|说明|
|:-|:-|:-|
|id|`string`| 会话ID|
|friendId|`string\|number`| 对方用户ID|
|nickname|`string`| 对方昵称|
|avatar|`string`| 对方头像URL(可为空)|
|lastMessage|`string`| 最后一条消息摘要|
|lastTime|`string`| 最后消息时间(ISO 8601|
|unread|`number`| 未读消息数|
|online|`boolean`| 对方是否在线|
```javascript
const req = new GetConversationsReq()
client.GetConversations(req).then(...).catch(...)
```
### 获取历史消息
<font color="green">GET</font> `/api/have_a_drink/v1/chat/messages`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|conversationId|`string`|required| 会话ID|
|lastID|`string\|number`|| 游标分页|
|pageSize|`number`|gt=0,lte=100| 每页数量,默认20|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|list|`Array<Message>`| 消息列表<br>|
|hasMore|`boolean`| 是否还有更多消息<br>|
**Message**
|名称|类型|说明|
|:-|:-|:-|
|id|`string\|number`| 消息ID|
|conversationId|`string`| 会话ID|
|senderId|`string\|number`| 发送者ID|
|receiverId|`string\|number`| 接收者ID|
|type|`MessageType`| 消息类型|
|content|`string`| 消息内容|
|timestamp|`number`| 发送时间(毫秒时间戳)|
|status|`MessageStatus`| 消息状态|
**MessageType**
|名称|值|类型|说明|
|:-|:-:|:-|:-|
|Text|"text"|`MessageType`|文本消息|
|Image|"image"|`MessageType`|图片消息|
**MessageStatus**
|名称|值|类型|说明|
|:-|:-:|:-|:-|
|Sending|"sending"|`MessageStatus`|发送中|
|Sent|"sent"|`MessageStatus`|已发送|
|Delivered|"delivered"|`MessageStatus`|已送达|
|Read|"read"|`MessageStatus`|已读|
```javascript
const req = new GetMessagesReq()
client.GetMessages(req).then(...).catch(...)
```
### 标记会话已读
<font color="green">POST</font> `/api/have_a_drink/v1/chat/conversations/read`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|id|`string`|required| 会话ID(路径参数)|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|success|`boolean`| 是否成功<br>|
```javascript
const req = new MarkReadReq()
client.MarkRead(req).then(...).catch(...)
```
### 获取未读消息总数
<font color="green">GET</font> `/api/have_a_drink/v1/chat/unread-count`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|total|`number`| 未读总数<br>|
```javascript
const req = new GetUnreadCountReq()
client.GetUnreadCount(req).then(...).catch(...)
```
### 注意:连接建立后,服务端会先验证 token,然后推送 connected 事件
<font color="green">WS</font> `/api/have_a_drink/v1/chat/chat`
#### 请求参数
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|mesgType|`ClientMesgType`|发送消息: ClientMesgType.Chat = "chat"<br>输入状态: ClientMesgType.Typing = "typing"<br>已读回执: ClientMesgType.Read = "read"<br>心跳: ClientMesgType.Ping = "ping"| 消息类型|
|data|`ClientMessage`|| 消息数据|
#### 返回值
|名称|类型|说明|
|:-|:-:|:-|
|mesgType|`ServerMesgType`| 消息类型<br>推送新消息: ServerMesgType.Chat = "chat"<br>对方输入状态: ServerMesgType.Typing = "typing"<br>对方已读回执: ServerMesgType.Read = "read"<br>消息送达确认: ServerMesgType.Ack = "ack"<br>心跳响应: ServerMesgType.Pong = "pong"<br>连接成功: ServerMesgType.Connected = "connected"<br>被踢下线: ServerMesgType.Kicked = "kicked"|
|data|`ServerMessage`| 消息数据<br>|
```javascript
const req = new ChatWebSocket()
client.ChatWebSocket(req).then(...).catch(...)
```
+2284 -63
View File
File diff suppressed because it is too large Load Diff
+2266 -100
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "HaveADrink", "name": "HaveADrink",
"type": "module", "type": "module",
"version": "v1.0.4", "version": "v1.0.8",
"description": "喝酒了么 API 服务", "description": "喝酒了么 API 服务",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"HaveADrink": "^1.0.4" "HaveADrink": "^1.0.8"
} }
}, },
"node_modules/HaveADrink": { "node_modules/HaveADrink": {
"version": "1.0.4", "version": "1.0.8",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz", "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
"integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==", "integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
"license": "ISC" "license": "ISC"
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"dependencies": { "dependencies": {
"HaveADrink": "^1.0.4" "HaveADrink": "^1.0.8"
} }
} }
+2 -2
View File
@@ -34,7 +34,6 @@
<script> <script>
import DrinkCard from '../../components/DrinkCard.vue' import DrinkCard from '../../components/DrinkCard.vue'
import client from '../../common/api' import client from '../../common/api'
import { getRecords } from '../../common/mock-data'
import { calcStandardCups, unitToMl } from '../../common/utils' import { calcStandardCups, unitToMl } from '../../common/utils'
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants' import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
@@ -86,7 +85,8 @@ export default {
loadLocalRecord(id) { loadLocalRecord(id) {
if (!id) return null if (!id) return null
try { try {
const records = getRecords() let records = []
try { records = JSON.parse(uni.getStorageSync('drink_records') || '[]') } catch (e) { /* */ }
const found = records.find(r => r.id === id) const found = records.find(r => r.id === id)
if (!found) return null if (!found) return null
return { return {
+33
View File
@@ -67,6 +67,7 @@
<script> <script>
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetConversations } from '../../common/api' import { GetConversations } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -83,6 +84,13 @@ export default {
}, },
onShow() { onShow() {
this.loadConversations() this.loadConversations()
this.bindWsEvents()
},
onHide() {
this.unbindWsEvents()
},
onUnload() {
this.unbindWsEvents()
}, },
methods: { methods: {
async loadConversations() { async loadConversations() {
@@ -110,6 +118,31 @@ export default {
}, },
goCircle() { goCircle() {
uni.switchTab({ url: '/pages/circle/circle' }) uni.switchTab({ url: '/pages/circle/circle' })
},
// === WebSocket ===
bindWsEvents() {
wsManager.on('message', this.onWsMessage)
},
unbindWsEvents() {
wsManager.off('message', this.onWsMessage)
},
/** 收到新消息时更新会话列表 */
onWsMessage(data) {
const convId = data.conversationId
const idx = this.conversations.findIndex(c => String(c.id) === String(convId))
if (idx >= 0) {
const conv = this.conversations[idx]
conv.lastMessage = data.content || '[新消息]'
conv.lastTime = '刚刚'
conv.unread = (conv.unread || 0) + 1
//
this.conversations.splice(idx, 1)
this.conversations.unshift(conv)
} else {
//
this.loadConversations()
}
} }
} }
} }
+37 -27
View File
@@ -119,7 +119,7 @@
</template> </template>
<script> <script>
import { GetMessages, MarkRead } from '../../common/api' import { GetMessages, MarkRead, GetConversations } from '../../common/api'
import wsManager from '../../common/websocket' import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
@@ -140,7 +140,7 @@ export default {
isOnline: false, isOnline: false,
peerTyping: false, peerTyping: false,
typingTimer: null, typingTimer: null,
page: 1, lastID: 0,
hasMore: true hasMore: true
} }
}, },
@@ -148,9 +148,7 @@ export default {
this.friendId = options.friendId || '' this.friendId = options.friendId || ''
this.nickname = decodeURIComponent(options.nickname || '') this.nickname = decodeURIComponent(options.nickname || '')
this.conversationId = options.conversationId || '' this.conversationId = options.conversationId || ''
this.loadMessages() this.initChat()
this.markRead()
this.bindWsEvents()
// //
uni.onKeyboardHeightChange(res => { uni.onKeyboardHeightChange(res => {
this.keyboardHeight = res.height this.keyboardHeight = res.height
@@ -176,12 +174,32 @@ export default {
} }
}, },
methods: { methods: {
// === ===
async initChat() {
// conversationId
if (!this.conversationId && this.friendId) {
try {
const res = await GetConversations()
const conv = (res.data.list || []).find(c => String(c.friendId) === String(this.friendId))
if (conv) {
this.conversationId = conv.id
}
} catch (e) { /* ignore */ }
}
this.loadMessages()
this.markRead()
this.bindWsEvents()
},
// === === // === ===
async loadMessages() { async loadMessages() {
try { try {
const res = await GetMessages({ conversationId: this.conversationId, page: this.page }) const res = await GetMessages({ conversationId: this.conversationId, lastID: this.lastID, pageSize: 20 })
this.messages = res.data.list this.messages = res.data.list || []
this.hasMore = res.data.hasMore this.hasMore = res.data.hasMore
// ID
if (this.messages.length) {
this.lastID = this.messages[0].id
}
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToBottom() this.scrollToBottom()
}) })
@@ -219,12 +237,12 @@ export default {
/** 收到新消息 */ /** 收到新消息 */
onWsMessage(data) { onWsMessage(data) {
// //
if (data.senderId !== this.friendId) return if (String(data.senderId) !== String(this.friendId)) return
const msg = { const msg = {
id: data.msgId || 'msg_' + Date.now(), id: data.msgId ? String(data.msgId) : 'msg_' + Date.now(),
conversationId: this.conversationId, conversationId: data.conversationId || this.conversationId,
senderId: data.senderId, senderId: data.senderId,
receiverId: 'user_001', receiverId: data.receiverId,
type: data.msgType || 'text', type: data.msgType || 'text',
content: data.content, content: data.content,
timestamp: data.timestamp || Date.now(), timestamp: data.timestamp || Date.now(),
@@ -242,7 +260,7 @@ export default {
/** 对方正在输入 */ /** 对方正在输入 */
onWsTyping(data) { onWsTyping(data) {
if (data.senderId !== this.friendId) return if (String(data.senderId) !== String(this.friendId)) return
this.peerTyping = true this.peerTyping = true
if (this.typingTimer) clearTimeout(this.typingTimer) if (this.typingTimer) clearTimeout(this.typingTimer)
this.typingTimer = setTimeout(() => { this.typingTimer = setTimeout(() => {
@@ -252,9 +270,9 @@ export default {
/** 消息送达确认 */ /** 消息送达确认 */
onWsAck(data) { onWsAck(data) {
const msg = this.messages.find(m => m.id === data.clientMsgId) const msg = this.messages.find(m => m.id === data.clientMsgId || m.id === String(data.clientMsgId))
if (msg) { if (msg) {
msg.id = data.msgId || msg.id msg.id = data.msgId ? String(data.msgId) : msg.id
msg.status = data.status || 'sent' msg.status = data.status || 'sent'
} }
}, },
@@ -268,7 +286,7 @@ export default {
const msg = { const msg = {
id: clientMsgId, id: clientMsgId,
conversationId: this.conversationId, conversationId: this.conversationId,
senderId: 'user_001', senderId: wsManager.userId || 'self',
receiverId: this.friendId, receiverId: this.friendId,
type: 'text', type: 'text',
content: text, content: text,
@@ -288,13 +306,8 @@ export default {
receiverId: this.friendId, receiverId: this.friendId,
content: text, content: text,
msgType: 'text', msgType: 'text',
clientMsgId clientMesgId: clientMsgId
}) })
// Mock
setTimeout(() => {
msg.status = 'sent'
}, 500)
}, },
/** 重发失败消息 */ /** 重发失败消息 */
@@ -303,12 +316,9 @@ export default {
wsManager.send('chat', { wsManager.send('chat', {
receiverId: this.friendId, receiverId: this.friendId,
content: msg.content, content: msg.content,
msgType: msg.type, msgType: msg.type || 'text',
clientMsgId: msg.id clientMesgId: msg.id
}) })
setTimeout(() => {
msg.status = 'sent'
}, 500)
}, },
/** 点击发送按钮时记录时间戳 */ /** 点击发送按钮时记录时间戳 */
@@ -334,7 +344,7 @@ export default {
// === === // === ===
isSelf(msg) { isSelf(msg) {
return msg.senderId === 'user_001' return String(msg.senderId) === String(wsManager.userId) || msg.senderId === 'self'
}, },
showTimeDivider(idx) { showTimeDivider(idx) {
+5 -4
View File
@@ -62,8 +62,7 @@
import FeedCard from '../../components/FeedCard.vue' import FeedCard from '../../components/FeedCard.vue'
import CommentItem from '../../components/CommentItem.vue' import CommentItem from '../../components/CommentItem.vue'
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api' import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api'
import { MOCK_FEEDS } from '../../common/mock-data'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -89,8 +88,10 @@ export default {
uni.navigateBack() uni.navigateBack()
}, },
async loadFeed() { async loadFeed() {
// Mock // globalData
this.feed = MOCK_FEEDS.find(f => f.id === this.feedId) || MOCK_FEEDS[0] const app = getApp()
const feeds = (app.globalData && app.globalData.circleFeeds) || []
this.feed = feeds.find(f => String(f.id) === String(this.feedId)) || null
}, },
async loadComments() { async loadComments() {
try { try {
+24 -8
View File
@@ -1,7 +1,7 @@
<template> <template>
<view :class="themeClass" class="page-container publish-page"> <view :class="themeClass" class="page-container publish-page">
<!-- 顶部导航 --> <!-- 顶部导航 -->
<view class="publish-nav" :style="{ paddingTop: navPaddingTop }"> <view class="publish-nav" :style="{ paddingTop: navPaddingTop, paddingRight: navPaddingRight }">
<view class="publish-nav-inner"> <view class="publish-nav-inner">
<view class="publish-back" @click="goBack"> <view class="publish-back" @click="goBack">
<text class="publish-back-icon"></text> <text class="publish-back-icon"></text>
@@ -79,16 +79,18 @@
</template> </template>
<script> <script>
import { PublishFeed } from '../../common/api' import { PublishFeed, UploadImage } from '../../common/api'
import { getRecords } from '../../common/mock-data' import client from '../../common/api'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
mixins: [themeMixin], mixins: [themeMixin],
data() { data() {
const menuBtn = uni.getMenuButtonBoundingClientRect() const menuBtn = uni.getMenuButtonBoundingClientRect()
const sysInfo = uni.getSystemInfoSync()
return { return {
navPaddingTop: menuBtn.top + 'px', navPaddingTop: menuBtn.top + 'px',
navPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
text: '', text: '',
images: [], images: [],
visibility: 'friends', visibility: 'friends',
@@ -115,10 +117,11 @@ export default {
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
}, },
loadLastRecord() { async loadLastRecord() {
try { try {
const records = getRecords() const res = await client.GetRecords({ page: 1, pageSize: 5, month: '', mode: '' })
const drank = records.find(r => r.mode === 'drank') const list = res.list || []
const drank = list.find(r => String(r.mode) === 'drank' || String(r.mode) === 'Drank')
if (drank) this.lastRecord = drank if (drank) this.lastRecord = drank
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, },
@@ -140,16 +143,29 @@ export default {
if (!this.canSubmit) return if (!this.canSubmit) return
this.publishing = true this.publishing = true
try { try {
// URL
let uploadedUrls = []
if (this.images.length) {
uni.showLoading({ title: '上传图片中...' })
for (const img of this.images) {
const upRes = await UploadImage({ filePath: img })
if (upRes.data && upRes.data.url) {
uploadedUrls.push(upRes.data.url)
}
}
uni.hideLoading()
}
await PublishFeed({ await PublishFeed({
text: this.text, text: this.text,
images: this.images, images: uploadedUrls,
recordId: this.linkRecord && this.lastRecord ? this.lastRecord.id : null, recordId: this.linkRecord && this.lastRecord ? this.lastRecord.id : null,
visibility: this.visibility visibility: this.visibility
}) })
uni.showToast({ title: '发布成功 🍻', icon: 'none' }) uni.showToast({ title: '发布成功 🍻', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800) setTimeout(() => uni.navigateBack(), 800)
} catch (e) { } catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' }) uni.hideLoading()
//
} }
this.publishing = false this.publishing = false
} }
+22 -9
View File
@@ -121,6 +121,7 @@ import FriendItem from '../../components/FriendItem.vue'
import EventCard from '../../components/EventCard.vue' import EventCard from '../../components/EventCard.vue'
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api' import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -136,7 +137,7 @@ export default {
headerPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px', headerPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
// //
feeds: [], feeds: [],
page: 1, lastId: 0,
hasMore: true, hasMore: true,
loading: false, loading: false,
refreshing: false, refreshing: false,
@@ -154,6 +155,14 @@ export default {
this.loadFriends() this.loadFriends()
this.loadEvents() this.loadEvents()
this.loadUnread() this.loadUnread()
this._wsHandler = () => { this.unreadTotal++ }
wsManager.on('message', this._wsHandler)
},
onHide() {
if (this._wsHandler) {
wsManager.off('message', this._wsHandler)
this._wsHandler = null
}
}, },
methods: { methods: {
switchTab(i) { switchTab(i) {
@@ -163,20 +172,26 @@ export default {
async loadFeeds(reset = false) { async loadFeeds(reset = false) {
if (this.loading) return if (this.loading) return
if (reset) { if (reset) {
this.page = 1 this.lastId = 0
this.hasMore = true this.hasMore = true
} }
this.loading = true this.loading = true
try { try {
const res = await GetCircleFeeds({ page: this.page, pageSize: 10 }) const res = await GetCircleFeeds({ lastId: this.lastId, pageSize: 10 })
const data = res.data const data = res.data
if (reset) { if (reset) {
this.feeds = data.list this.feeds = data.list || []
} else { } else {
this.feeds = [...this.feeds, ...data.list] this.feeds = [...this.feeds, ...(data.list || [])]
} }
this.hasMore = data.hasMore this.hasMore = data.hasMore
this.page++ // ID
if (data.list && data.list.length) {
this.lastId = data.list[data.list.length - 1].id
}
// globalData
getApp().globalData = getApp().globalData || {}
getApp().globalData.circleFeeds = this.feeds
} catch (e) { } catch (e) {
console.warn('加载动态失败', e) console.warn('加载动态失败', e)
} }
@@ -230,10 +245,8 @@ export default {
if (this._navLock) return if (this._navLock) return
this._navLock = true this._navLock = true
setTimeout(() => { this._navLock = false }, 600) setTimeout(() => { this._navLock = false }, 600)
// friendId (f001 -> conv_001)
const convId = 'conv_' + friend.id.replace('f', '')
uni.navigateTo({ uni.navigateTo({
url: `/pages/chat/chat?friendId=${friend.id}&nickname=${encodeURIComponent(friend.nickname)}&conversationId=${convId}` url: `/pages/chat/chat?friendId=${friend.id}&nickname=${encodeURIComponent(friend.nickname)}`
}) })
}, },
goChatList() { goChatList() {
+8 -4
View File
@@ -115,12 +115,16 @@ export default {
mixins: [themeMixin], mixins: [themeMixin],
data() { data() {
const menuBtn = uni.getMenuButtonBoundingClientRect() const menuBtn = uni.getMenuButtonBoundingClientRect()
const now = new Date()
const pad = n => String(n).padStart(2, '0')
const today = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`
const curTime = `${pad(now.getHours())}:${pad(now.getMinutes())}`
return { return {
navPaddingTop: menuBtn.top + 'px', navPaddingTop: menuBtn.top + 'px',
form: { form: {
title: '', title: '',
date: '', date: today,
time: '', time: curTime,
location: '', location: '',
address: '', address: '',
latitude: null, latitude: null,
@@ -200,7 +204,7 @@ export default {
try { try {
await CreateEvent({ await CreateEvent({
title: this.form.title, title: this.form.title,
time: `${this.form.date} ${this.form.time}`, time: `${this.form.date} ${this.form.time}:00`,
location: this.form.location, location: this.form.location,
address: this.form.address, address: this.form.address,
latitude: this.form.latitude, latitude: this.form.latitude,
@@ -211,7 +215,7 @@ export default {
uni.showToast({ title: '酒局已发布 🎉', icon: 'none' }) uni.showToast({ title: '酒局已发布 🎉', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800) setTimeout(() => uni.navigateBack(), 800)
} catch (e) { } catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' }) //
} }
this.submitting = false this.submitting = false
} }
+7 -4
View File
@@ -98,14 +98,14 @@
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="evt-action-bar" v-if="event"> <view class="evt-action-bar" v-if="event">
<button <button
v-if="event.status === 'open' && !event.isJoined && !event.isOrganizer" v-if="(event.status === 'open' || event.status === 'upcoming') && !event.isJoined && !event.isOrganizer"
class="evt-btn evt-btn-primary" class="evt-btn evt-btn-primary"
@click="handleJoin" @click="handleJoin"
> >
报名参加 报名参加
</button> </button>
<button <button
v-if="event.status === 'open' && event.isJoined && !event.isOrganizer" v-if="(event.status === 'open' || event.status === 'upcoming') && event.isJoined && !event.isOrganizer"
class="evt-btn evt-btn-ghost" class="evt-btn evt-btn-ghost"
@click="handleQuit" @click="handleQuit"
> >
@@ -145,8 +145,9 @@ export default {
computed: { computed: {
statusLabel() { statusLabel() {
if (!this.event) return '' if (!this.event) return ''
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' } const val = String(this.event.status).toLowerCase()
return map[this.event.status] || '报名中' const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中'
}, },
eventMarkers() { eventMarkers() {
if (!this.event || !this.event.latitude) return [] if (!this.event || !this.event.latitude) return []
@@ -296,6 +297,7 @@ export default {
margin-bottom: $sp-lg; margin-bottom: $sp-lg;
&.status-open { background: rgba(94,198,160,0.12); } &.status-open { background: rgba(94,198,160,0.12); }
&.status-upcoming { background: rgba(139,133,184,0.12); }
&.status-ongoing { background: rgba(232,168,56,0.12); } &.status-ongoing { background: rgba(232,168,56,0.12); }
&.status-ended { background: $bg-elevated; } &.status-ended { background: $bg-elevated; }
} }
@@ -305,6 +307,7 @@ export default {
font-weight: $fw-medium; font-weight: $fw-medium;
.status-open & { color: $mint; } .status-open & { color: $mint; }
.status-upcoming & { color: $lavender; }
.status-ongoing & { color: $amber; } .status-ongoing & { color: $amber; }
.status-ended & { color: $text-tertiary; } .status-ended & { color: $text-tertiary; }
} }
+7 -2
View File
@@ -316,7 +316,6 @@ import {
} from '../../common/constants' } from '../../common/constants'
import { calcStandardCups, unitToMl, formatDate, getCatIcon, isIconPath } from '../../common/utils' import { calcStandardCups, unitToMl, formatDate, getCatIcon, isIconPath } from '../../common/utils'
import client from '../../common/api' import client from '../../common/api'
import { addRecord } from '../../common/mock-data'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -562,7 +561,13 @@ export default {
console.warn('创建记录失败,降级本地保存:', e) console.warn('创建记录失败,降级本地保存:', e)
uni.showToast({ title: e.msg || '保存失败,已本地保存', icon: 'none', duration: 2000 }) uni.showToast({ title: e.msg || '保存失败,已本地保存', icon: 'none', duration: 2000 })
// //
const saved = addRecord(recordData) recordData.id = `record_${Date.now()}`
recordData.createdAt = new Date().toISOString()
let localRecords = []
try { localRecords = JSON.parse(uni.getStorageSync('drink_records') || '[]') } catch (e) { /* */ }
localRecords.unshift(recordData)
uni.setStorageSync('drink_records', JSON.stringify(localRecords))
const saved = recordData
uni.navigateTo({ uni.navigateTo({
url: `/pages/card/card?recordId=${saved.id}` url: `/pages/card/card?recordId=${saved.id}`
}) })