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) => {
const code = res.statusCode
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)
} else if (code === 401 || code === 418) {
} else if (code === 401 ) {
// token/refresh_token 过期或无效,清除登录态
uni.removeStorageSync('auth_token')
uni.removeStorageSync('refresh_token')
@@ -76,8 +85,8 @@ function uploadRequest(url, params) {
uni.uploadFile({
url: url,
filePath: params.data.fileName || params.data.filePath,
name: params.data.name || 'file',
filePath: params.data.image || params.data.fileName || params.data.filePath,
name: params.data.name || 'image',
header: headers,
formData: params.data.formData || {},
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) {
return new Promise(resolve => setTimeout(() => resolve({ data }), ms))
}
/** 获取动态信息流 */
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 async function GetCircleFeeds({ lastId = 0, pageSize = 10 } = {}) {
const res = await client.GetFeeds({ lastId, pageSize })
return { data: res }
}
/** 点赞动态 */
export function LikeFeed({ feedId }) {
return mockDelay({ success: true })
export async function LikeFeed({ feedId }) {
const res = await client.LikeFeed({ id: feedId })
return { data: res }
}
/** 取消点赞 */
export function UnlikeFeed({ feedId }) {
return mockDelay({ success: true })
export async function UnlikeFeed({ feedId }) {
const res = await client.UnlikeFeed({ id: feedId })
return { data: res }
}
/** 获取动态评论 */
export function GetFeedComments({ feedId }) {
const list = MOCK_COMMENTS[feedId] || []
return mockDelay({ list })
export async function GetFeedComments({ feedId }) {
const res = await client.GetFeedComments({ id: feedId })
return { data: res }
}
/** 发表评论 */
export function AddComment({ feedId, content }) {
return mockDelay({ success: true, comment: { id: 'c_' + Date.now(), content, time: '刚刚' } })
export async function AddComment({ feedId, content }) {
const res = await client.AddComment({ id: feedId, content })
return { data: res }
}
/** 发布动态 */
export function PublishFeed({ text, images, recordId, visibility }) {
return mockDelay({ success: true, feedId: 'feed_' + Date.now() })
export async function PublishFeed({ text, images, recordId, visibility }) {
const res = await client.PublishFeed({ text, images, recordId, visibility })
return { data: res }
}
/** 获取酒友列表 */
export function GetFriends({ keyword = '' } = {}) {
const list = keyword
? MOCK_FRIENDS.filter(f => f.nickname.includes(keyword))
: MOCK_FRIENDS
return mockDelay({ list })
export async function GetFriends({ keyword = '' } = {}) {
const res = await client.GetFriends({ keyword })
return { data: res }
}
/** 获取好友请求 */
export function GetFriendRequests() {
return mockDelay({ list: MOCK_FRIEND_REQUESTS })
export async function GetFriendRequests() {
const res = await client.GetFriendRequests({})
return { data: res }
}
/** 发送好友请求 */
export function SendFriendRequest({ userId }) {
return mockDelay({ success: true })
export async function SendFriendRequest({ userId, message = '' }) {
const res = await client.SendFriendRequest({ userId, message })
return { data: res }
}
/** 接受好友请求 */
export function AcceptFriendRequest({ requestId }) {
return mockDelay({ success: true })
export async function AcceptFriendRequest({ requestId }) {
const res = await client.AcceptFriendRequest({ id: requestId })
return { data: res }
}
/** 删除酒友 */
export function RemoveFriend({ userId }) {
return mockDelay({ success: true })
export async function RemoveFriend({ userId }) {
const res = await client.RemoveFriend({ userId })
return { data: res }
}
/** 获取酒局列表 */
export function GetEvents({ status = '' } = {}) {
const list = status ? MOCK_EVENTS.filter(e => e.status === status) : MOCK_EVENTS
return mockDelay({ list })
export async function GetEvents({ status = '' } = {}) {
const res = await client.GetEvents({ status })
return { data: res }
}
/** 获取酒局详情 */
export function GetEventDetail({ eventId }) {
const evt = MOCK_EVENTS.find(e => e.id === eventId) || MOCK_EVENTS[0]
return mockDelay(evt)
export async function GetEventDetail({ eventId }) {
const res = await client.GetEventDetail({ id: eventId })
return { data: res }
}
/** 发起酒局 */
export function CreateEvent({ title, time, location, maxPeople, note }) {
return mockDelay({ success: true, eventId: 'evt_' + Date.now() })
export async function CreateEvent({ title, time, location, maxPeople, note, latitude, longitude }) {
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 }) {
return mockDelay({ success: true })
export async function JoinEvent({ eventId }) {
const res = await client.JoinEvent({ id: eventId })
return { data: res }
}
/** 取消报名 */
export function QuitEvent({ eventId }) {
return mockDelay({ success: true })
export async function QuitEvent({ eventId }) {
const res = await client.QuitEvent({ id: eventId })
return { data: res }
}
/** 酒局签到 */
export function CheckInEvent({ eventId }) {
return mockDelay({ success: true })
export async function CheckInEvent({ eventId }) {
const res = await client.CheckInEvent({ id: eventId })
return { data: res }
}
// ==========================================
// 私信 API当前返回 Mock 数据,后端就绪后切换为 SDK 调用
// 私信 API基于 HaveADrink SDK 真实接口
// ==========================================
/** 获取会话列表 */
export function GetConversations() {
// TODO: 后端就绪后替换为 client.GetConversations(...)
return mockDelay({ list: MOCK_CONVERSATIONS })
export async function GetConversations() {
const res = await client.GetConversations({})
return { data: res }
}
/** 获取历史消息 */
export function GetMessages({ conversationId, page = 1, pageSize = 20 } = {}) {
// TODO: 后端就绪后替换为 client.GetMessages(...)
const all = MOCK_MESSAGES[conversationId] || []
const start = (page - 1) * pageSize
const list = all.slice(start, start + pageSize)
return mockDelay({ list, hasMore: start + pageSize < all.length })
/** 获取历史消息(游标分页) */
export async function GetMessages({ conversationId, lastID = 0, pageSize = 20 } = {}) {
const res = await client.GetMessages({ conversationId, lastID, pageSize })
return { data: res }
}
/** 标记会话已读 */
export function MarkRead({ conversationId }) {
// TODO: 后端就绪后替换为 client.MarkRead(...)
return mockDelay({ success: true })
export async function MarkRead({ conversationId }) {
const res = await client.MarkRead({ id: conversationId })
return { data: res }
}
/** 获取未读消息总数 */
export function GetUnreadCount() {
// TODO: 后端就绪后替换为 client.GetUnreadCount(...)
const total = MOCK_CONVERSATIONS.reduce((sum, c) => sum + c.unread, 0)
return mockDelay({ total })
export async function GetUnreadCount() {
const res = await client.GetUnreadCount({})
return { data: res }
}
// ==========================================
// 文件上传 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 管理器
* 单例模式,负责私信实时通信
* 协议格式与 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 {
constructor() {
@@ -21,6 +41,7 @@ class WebSocketManager {
this.heartbeatWait = 60000 // 60s 无 pong 则重连
this.manualClose = false
this.token = ''
this.userId = '' // 连接成功后服务端返回的用户ID
}
/**
@@ -37,11 +58,14 @@ class WebSocketManager {
this.manualClose = false
this.isConnecting = true
const url = `${WS_HOST}?token=${this.token}`
console.log('[WS] 正在连接...', url)
const url = WS_HOST
console.log('[WS] 正在连接...')
this.socketTask = uni.connectSocket({
url,
header: {
'Authorization': `Bearer ${this.token}`
},
complete: () => {}
})
@@ -95,17 +119,17 @@ class WebSocketManager {
}
/**
* 发送消息
* @param {string} type - 消息类型: chat | typing | read | ping
* 发送消息(与 SDK writeClient* 方法格式一致)
* @param {string} mesgType - 消息类型: chat | typing | read | ping
* @param {object} data - 消息数据
*/
send(type, data = {}) {
const payload = JSON.stringify({ type, data, ts: Date.now() })
send(mesgType, data = {}) {
const payload = JSON.stringify({ mesgType, data })
if (this.isConnected && this.socketTask) {
this.socketTask.send({ data: payload })
} else {
// 断线暂存队列
if (type !== 'ping') {
// 断线暂存队列ping 不暂存)
if (mesgType !== ClientMesgType.Ping) {
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
*/
on(event, callback) {
@@ -143,27 +167,38 @@ class WebSocketManager {
// ========== 内部方法 ==========
/** 处理收到的消息 */
/** 处理收到的消息SDK ServerMesgType 协议) */
_handleMessage(msg) {
const { type, data } = msg
switch (type) {
case 'pong':
this._onPong()
break
case 'chat':
const { mesgType, data } = msg
switch (mesgType) {
case ServerMesgType.Chat:
this._emit('message', data)
break
case 'typing':
case ServerMesgType.Typing:
this._emit('typing', data)
break
case 'read':
case ServerMesgType.Read:
this._emit('read', data)
break
case 'ack':
case ServerMesgType.Ack:
this._emit('ack', data)
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:
console.log('[WS] 未知消息类型', type)
console.log('[WS] 未知消息类型', mesgType)
}
}
@@ -181,7 +216,7 @@ class WebSocketManager {
_startHeartbeat() {
this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
this.send('ping')
this.send(ClientMesgType.Ping, {})
// 设置超时检测
this.heartbeatTimeout = setTimeout(() => {
console.warn('[WS] 心跳超时,触发重连')
+7 -2
View File
@@ -49,8 +49,9 @@ export default {
emits: ['item-click'],
computed: {
statusLabel() {
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' }
return map[this.event.status] || '报名中'
const val = String(this.event.status).toLowerCase()
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中'
}
}
}
@@ -80,6 +81,9 @@ export default {
&.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);
}
@@ -93,6 +97,7 @@ export default {
font-weight: $fw-medium;
.status-open & { color: $mint; }
.status-upcoming & { color: $lavender; }
.status-ongoing & { color: $amber; }
.status-ended & { color: $text-tertiary; }
}
+9 -2
View File
@@ -73,8 +73,15 @@ export default {
getCatIcon,
isIconPath,
feelingLabel(f) {
const map = { sober: '😌 清醒', buzzed: '😊 微醺', tipsy: '😄 小醉', drunk: '🥴 喝多了' }
return map[f] || '🍻 喝了点'
const val = String(f).toLowerCase()
const map = {
tipsy: '😌 微醺',
buzzed: '😊 到位',
drunk: '🥴 醉了',
blackout: '😵 断片',
sober: '😌 清醒'
}
return map[val] || '🍻 喝了点'
}
}
}
+1
View File
@@ -46,6 +46,7 @@
"desc" : "用于选择酒局地点和导航"
}
},
"requiredPrivateInfos" : ["chooseLocation", "getLocation"],
"optimization" : {
"subPackages" : true
}
+3 -3
View File
@@ -4,9 +4,9 @@
"requires": true,
"packages": {
"node_modules/HaveADrink": {
"version": "1.0.4",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz",
"integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==",
"version": "1.0.8",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
"license": "ISC"
}
}
+809 -55
View File
@@ -14,7 +14,7 @@
* 6. 社交模块 - 朋友圈动态、点赞
**版本:** v1.0.4
**版本:** v1.0.8
## 安装
@@ -350,58 +350,6 @@ const req = new GetFeedReq()
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**
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|category|`Category`|required| 酒类分类ID|
|category|`Category`|| 酒类分类ID|
|brand|`string`|required| 品牌名称|
|product|`string`|| 产品名|
|amount|`number`|required,gt=0| 饮用量数值|
@@ -532,7 +480,7 @@ client.UploadPhotos(req).then(...).catch(...)
**FoodInfo**
|名称|类型|校验规则|说明|
|:-|:-|:-|:-|
|category|`FoodCategory`|required| 配餐分类ID|
|category|`FoodCategory`|| 配餐分类ID|
|name|`string`|| 具体菜名|
@@ -1193,3 +1141,809 @@ client.UpdatePreferences(req).then(...).catch(...)
const req = new UploadImageReq()
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",
"type": "module",
"version": "v1.0.4",
"version": "v1.0.8",
"description": "喝酒了么 API 服务",
"main": "index.js",
"scripts": {
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": {
"": {
"dependencies": {
"HaveADrink": "^1.0.4"
"HaveADrink": "^1.0.8"
}
},
"node_modules/HaveADrink": {
"version": "1.0.4",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.4.tgz",
"integrity": "sha512-kSSpTZrQGjRueftSAopurZcwXGMS3oKQ0mvP3/sePJh7odO5ev9FT31isXX0LLmL1HojzB4nrIeDb/PTdGhYuA==",
"version": "1.0.8",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
"license": "ISC"
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"dependencies": {
"HaveADrink": "^1.0.4"
"HaveADrink": "^1.0.8"
}
}
+2 -2
View File
@@ -34,7 +34,6 @@
<script>
import DrinkCard from '../../components/DrinkCard.vue'
import client from '../../common/api'
import { getRecords } from '../../common/mock-data'
import { calcStandardCups, unitToMl } from '../../common/utils'
import { DRINK_CATEGORIES, FOOD_CATEGORIES, DRINK_QUOTES, FEELINGS } from '../../common/constants'
import themeMixin from '../../common/theme-mixin'
@@ -86,7 +85,8 @@ export default {
loadLocalRecord(id) {
if (!id) return null
try {
const records = getRecords()
let records = []
try { records = JSON.parse(uni.getStorageSync('drink_records') || '[]') } catch (e) { /* */ }
const found = records.find(r => r.id === id)
if (!found) return null
return {
+33
View File
@@ -67,6 +67,7 @@
<script>
import EmptyState from '../../components/EmptyState.vue'
import { GetConversations } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin'
export default {
@@ -83,6 +84,13 @@ export default {
},
onShow() {
this.loadConversations()
this.bindWsEvents()
},
onHide() {
this.unbindWsEvents()
},
onUnload() {
this.unbindWsEvents()
},
methods: {
async loadConversations() {
@@ -110,6 +118,31 @@ export default {
},
goCircle() {
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>
<script>
import { GetMessages, MarkRead } from '../../common/api'
import { GetMessages, MarkRead, GetConversations } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin'
@@ -140,7 +140,7 @@ export default {
isOnline: false,
peerTyping: false,
typingTimer: null,
page: 1,
lastID: 0,
hasMore: true
}
},
@@ -148,9 +148,7 @@ export default {
this.friendId = options.friendId || ''
this.nickname = decodeURIComponent(options.nickname || '')
this.conversationId = options.conversationId || ''
this.loadMessages()
this.markRead()
this.bindWsEvents()
this.initChat()
// 监听键盘高度变化,动态调整输入栏位置(避免键盘顶起整个页面)
uni.onKeyboardHeightChange(res => {
this.keyboardHeight = res.height
@@ -176,12 +174,32 @@ export default {
}
},
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() {
try {
const res = await GetMessages({ conversationId: this.conversationId, page: this.page })
this.messages = res.data.list
const res = await GetMessages({ conversationId: this.conversationId, lastID: this.lastID, pageSize: 20 })
this.messages = res.data.list || []
this.hasMore = res.data.hasMore
// 记录最早一条消息ID作为加载更多游标
if (this.messages.length) {
this.lastID = this.messages[0].id
}
this.$nextTick(() => {
this.scrollToBottom()
})
@@ -219,12 +237,12 @@ export default {
/** 收到新消息 */
onWsMessage(data) {
// 只处理当前会话的消息
if (data.senderId !== this.friendId) return
if (String(data.senderId) !== String(this.friendId)) return
const msg = {
id: data.msgId || 'msg_' + Date.now(),
conversationId: this.conversationId,
id: data.msgId ? String(data.msgId) : 'msg_' + Date.now(),
conversationId: data.conversationId || this.conversationId,
senderId: data.senderId,
receiverId: 'user_001',
receiverId: data.receiverId,
type: data.msgType || 'text',
content: data.content,
timestamp: data.timestamp || Date.now(),
@@ -242,7 +260,7 @@ export default {
/** 对方正在输入 */
onWsTyping(data) {
if (data.senderId !== this.friendId) return
if (String(data.senderId) !== String(this.friendId)) return
this.peerTyping = true
if (this.typingTimer) clearTimeout(this.typingTimer)
this.typingTimer = setTimeout(() => {
@@ -252,9 +270,9 @@ export default {
/** 消息送达确认 */
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) {
msg.id = data.msgId || msg.id
msg.id = data.msgId ? String(data.msgId) : msg.id
msg.status = data.status || 'sent'
}
},
@@ -268,7 +286,7 @@ export default {
const msg = {
id: clientMsgId,
conversationId: this.conversationId,
senderId: 'user_001',
senderId: wsManager.userId || 'self',
receiverId: this.friendId,
type: 'text',
content: text,
@@ -288,13 +306,8 @@ export default {
receiverId: this.friendId,
content: text,
msgType: 'text',
clientMsgId
clientMesgId: clientMsgId
})
// 模拟确认(Mock模式下直接标记为已发送)
setTimeout(() => {
msg.status = 'sent'
}, 500)
},
/** 重发失败消息 */
@@ -303,12 +316,9 @@ export default {
wsManager.send('chat', {
receiverId: this.friendId,
content: msg.content,
msgType: msg.type,
clientMsgId: msg.id
msgType: msg.type || 'text',
clientMesgId: msg.id
})
setTimeout(() => {
msg.status = 'sent'
}, 500)
},
/** 点击发送按钮时记录时间戳 */
@@ -334,7 +344,7 @@ export default {
// === 辅助方法 ===
isSelf(msg) {
return msg.senderId === 'user_001'
return String(msg.senderId) === String(wsManager.userId) || msg.senderId === 'self'
},
showTimeDivider(idx) {
+5 -4
View File
@@ -62,8 +62,7 @@
import FeedCard from '../../components/FeedCard.vue'
import CommentItem from '../../components/CommentItem.vue'
import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api'
import { MOCK_FEEDS } from '../../common/mock-data'
import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api'
import themeMixin from '../../common/theme-mixin'
export default {
@@ -89,8 +88,10 @@ export default {
uni.navigateBack()
},
async loadFeed() {
// 从 Mock 数据中查找对应动态
this.feed = MOCK_FEEDS.find(f => f.id === this.feedId) || MOCK_FEEDS[0]
// 从 globalData 中查找对应动态
const app = getApp()
const feeds = (app.globalData && app.globalData.circleFeeds) || []
this.feed = feeds.find(f => String(f.id) === String(this.feedId)) || null
},
async loadComments() {
try {
+24 -8
View File
@@ -1,7 +1,7 @@
<template>
<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-back" @click="goBack">
<text class="publish-back-icon"></text>
@@ -79,16 +79,18 @@
</template>
<script>
import { PublishFeed } from '../../common/api'
import { getRecords } from '../../common/mock-data'
import { PublishFeed, UploadImage } from '../../common/api'
import client from '../../common/api'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
data() {
const menuBtn = uni.getMenuButtonBoundingClientRect()
const sysInfo = uni.getSystemInfoSync()
return {
navPaddingTop: menuBtn.top + 'px',
navPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
text: '',
images: [],
visibility: 'friends',
@@ -115,10 +117,11 @@ export default {
goBack() {
uni.navigateBack()
},
loadLastRecord() {
async loadLastRecord() {
try {
const records = getRecords()
const drank = records.find(r => r.mode === 'drank')
const res = await client.GetRecords({ page: 1, pageSize: 5, month: '', mode: '' })
const list = res.list || []
const drank = list.find(r => String(r.mode) === 'drank' || String(r.mode) === 'Drank')
if (drank) this.lastRecord = drank
} catch (e) { /* ignore */ }
},
@@ -140,16 +143,29 @@ export default {
if (!this.canSubmit) return
this.publishing = true
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({
text: this.text,
images: this.images,
images: uploadedUrls,
recordId: this.linkRecord && this.lastRecord ? this.lastRecord.id : null,
visibility: this.visibility
})
uni.showToast({ title: '发布成功 🍻', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
} catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' })
uni.hideLoading()
// 全局拦截器已弹出错误提示
}
this.publishing = false
}
+22 -9
View File
@@ -121,6 +121,7 @@ import FriendItem from '../../components/FriendItem.vue'
import EventCard from '../../components/EventCard.vue'
import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api'
import wsManager from '../../common/websocket'
import themeMixin from '../../common/theme-mixin'
export default {
@@ -136,7 +137,7 @@ export default {
headerPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
// 动态
feeds: [],
page: 1,
lastId: 0,
hasMore: true,
loading: false,
refreshing: false,
@@ -154,6 +155,14 @@ export default {
this.loadFriends()
this.loadEvents()
this.loadUnread()
this._wsHandler = () => { this.unreadTotal++ }
wsManager.on('message', this._wsHandler)
},
onHide() {
if (this._wsHandler) {
wsManager.off('message', this._wsHandler)
this._wsHandler = null
}
},
methods: {
switchTab(i) {
@@ -163,20 +172,26 @@ export default {
async loadFeeds(reset = false) {
if (this.loading) return
if (reset) {
this.page = 1
this.lastId = 0
this.hasMore = true
}
this.loading = true
try {
const res = await GetCircleFeeds({ page: this.page, pageSize: 10 })
const res = await GetCircleFeeds({ lastId: this.lastId, pageSize: 10 })
const data = res.data
if (reset) {
this.feeds = data.list
this.feeds = data.list || []
} else {
this.feeds = [...this.feeds, ...data.list]
this.feeds = [...this.feeds, ...(data.list || [])]
}
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) {
console.warn('加载动态失败', e)
}
@@ -230,10 +245,8 @@ export default {
if (this._navLock) return
this._navLock = true
setTimeout(() => { this._navLock = false }, 600)
// 根据 friendId 查找对应会话 (f001 -> conv_001)
const convId = 'conv_' + friend.id.replace('f', '')
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() {
+8 -4
View File
@@ -115,12 +115,16 @@ export default {
mixins: [themeMixin],
data() {
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 {
navPaddingTop: menuBtn.top + 'px',
form: {
title: '',
date: '',
time: '',
date: today,
time: curTime,
location: '',
address: '',
latitude: null,
@@ -200,7 +204,7 @@ export default {
try {
await CreateEvent({
title: this.form.title,
time: `${this.form.date} ${this.form.time}`,
time: `${this.form.date} ${this.form.time}:00`,
location: this.form.location,
address: this.form.address,
latitude: this.form.latitude,
@@ -211,7 +215,7 @@ export default {
uni.showToast({ title: '酒局已发布 🎉', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
} catch (e) {
uni.showToast({ title: '发布失败', icon: 'none' })
// 全局拦截器已弹出错误提示
}
this.submitting = false
}
+7 -4
View File
@@ -98,14 +98,14 @@
<!-- 底部操作栏 -->
<view class="evt-action-bar" v-if="event">
<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"
@click="handleJoin"
>
报名参加
</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"
@click="handleQuit"
>
@@ -145,8 +145,9 @@ export default {
computed: {
statusLabel() {
if (!this.event) return ''
const map = { open: '报名中', ongoing: '进行中', ended: '已结束' }
return map[this.event.status] || '报名中'
const val = String(this.event.status).toLowerCase()
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中'
},
eventMarkers() {
if (!this.event || !this.event.latitude) return []
@@ -296,6 +297,7 @@ export default {
margin-bottom: $sp-lg;
&.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-ended { background: $bg-elevated; }
}
@@ -305,6 +307,7 @@ export default {
font-weight: $fw-medium;
.status-open & { color: $mint; }
.status-upcoming & { color: $lavender; }
.status-ongoing & { color: $amber; }
.status-ended & { color: $text-tertiary; }
}
+7 -2
View File
@@ -316,7 +316,6 @@ import {
} from '../../common/constants'
import { calcStandardCups, unitToMl, formatDate, getCatIcon, isIconPath } from '../../common/utils'
import client from '../../common/api'
import { addRecord } from '../../common/mock-data'
import themeMixin from '../../common/theme-mixin'
export default {
@@ -562,7 +561,13 @@ export default {
console.warn('创建记录失败,降级本地保存:', e)
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({
url: `/pages/card/card?recordId=${saved.id}`
})