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:
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user