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
+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}`
})