Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f23ccae46 | ||
|
|
57eef74eb1 |
+15
-8
@@ -2,11 +2,12 @@
|
||||
* 基于 HaveADrink SDK 封装后端接口
|
||||
*/
|
||||
import HaveADrink from 'HaveADrink'
|
||||
import UniAppWebSocket from './uni-websocket'
|
||||
|
||||
// ==========================================
|
||||
// 后端服务地址(切换环境只需改这里)
|
||||
// ==========================================
|
||||
const API_HOST = 'https://dev.wash-painting.cn'
|
||||
export const API_HOST = 'https://dev.wash-painting.cn'
|
||||
|
||||
// ==========================================
|
||||
// HTTP 请求函数(供 SDK 内部调用)
|
||||
@@ -323,6 +324,12 @@ export async function UnlikeFeed({ feedId }) {
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 删除动态(仅本人可删,后端会校验归属) */
|
||||
export async function DeleteFeed({ feedId }) {
|
||||
const res = await client.DeleteFeed({ id: feedId })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 获取动态评论 */
|
||||
export async function GetFeedComments({ feedId }) {
|
||||
const res = await client.GetFeedComments({ id: feedId })
|
||||
@@ -347,19 +354,19 @@ export async function GetFriends({ keyword = '' } = {}) {
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 获取好友请求 */
|
||||
export async function GetFriendRequests() {
|
||||
const res = await client.GetFriendRequests({})
|
||||
/** 获取好友请求(支持关键词搜索) */
|
||||
export async function GetFriendRequests({ keyword = '' } = {}) {
|
||||
const res = await client.GetFriendRequests({ keyword })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 发送好友请求 */
|
||||
export async function SendFriendRequest({ userId, message = '' }) {
|
||||
const res = await client.SendFriendRequest({ userId, message })
|
||||
/** 发送好友请求(创建邀请,SDK v1.0.12:无需 userId,响应返回 inviteCode 邀请码) */
|
||||
export async function SendFriendRequest({ message = '' } = {}) {
|
||||
const res = await client.SendFriendRequest({ message })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 接受好友请求 */
|
||||
/** 接受好友请求(id 为好友请求ID,即邀请码 inviteCode) */
|
||||
export async function AcceptFriendRequest({ requestId }) {
|
||||
const res = await client.AcceptFriendRequest({ id: requestId })
|
||||
return { data: res }
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* 碰盏日记 - 邀请链路处理(邀请码模式,SDK v1.0.12)
|
||||
*
|
||||
* 后端规则:每次邀请都生成新的邀请码(一次性,不复用)
|
||||
*
|
||||
* 新流程:
|
||||
* 1. 邀请人调用 SendFriendRequest({ message }) 创建邀请 → 后端返回新的 inviteCode(好友请求ID)
|
||||
* 2. 分享链接携带 inviteCode、inviterNick;每次分享后重新生成新码供下次使用
|
||||
* 3. 被邀请人打开小程序 → index.vue / friends.vue onLoad 收到 inviteCode → savePendingInvite 暂存
|
||||
* 4. 被邀请人登录后调用 AcceptFriendRequest({ id: inviteCode }) 接受邀请,双方成为酒友
|
||||
*/
|
||||
|
||||
import { SendFriendRequest, AcceptFriendRequest } from './api'
|
||||
|
||||
const INVITE_KEY = 'pending_invite' // 我收到的邀请(待接受)
|
||||
|
||||
// 内存记录最近一次自己生成的邀请码(仅用于过滤自己点开自己的分享)
|
||||
let myLastCode = ''
|
||||
|
||||
// 清理旧版本遗留的永久缓存键(旧方案曾永久缓存邀请码,新方案每次生成新码)
|
||||
try { uni.removeStorageSync('my_invite_code') } catch (e) { /* ignore */ }
|
||||
|
||||
/**
|
||||
* 生成新的邀请码(用于分享)
|
||||
* 后端每次邀请都生成新码,故不做持久化缓存,每次调用都创建新邀请
|
||||
* @returns {Promise<string>} 邀请码,未登录或失败返回空字符串
|
||||
*/
|
||||
export async function getMyInviteCode() {
|
||||
if (uni.getStorageSync('is_logged_in') !== 'true') return ''
|
||||
try {
|
||||
const res = await SendFriendRequest({ message: '邀请你成为酒友,一起记录饮酒生活 🍻' })
|
||||
const code = res.data && res.data.inviteCode
|
||||
if (code !== undefined && code !== null && code !== '') {
|
||||
myLastCode = String(code)
|
||||
return myLastCode
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[invite] 创建邀请码失败', e)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂存收到的邀请(由分享落地页 onLoad 调用)
|
||||
* @param {string|number} inviteCode - 邀请码(好友请求ID)
|
||||
* @param {string} inviterNick - 邀请人昵称(用于提示文案)
|
||||
*/
|
||||
export function savePendingInvite(inviteCode, inviterNick) {
|
||||
if (!inviteCode) return
|
||||
try {
|
||||
// 自己分享给自己的邀请码,不处理(同一会话内自己点开自己的分享卡片)
|
||||
if (myLastCode && String(myLastCode) === String(inviteCode)) return
|
||||
uni.setStorageSync(INVITE_KEY, JSON.stringify({
|
||||
inviteCode: String(inviteCode),
|
||||
// uni-app onLoad options 已自动解码,此处直接存储
|
||||
inviterNick: inviterNick || ''
|
||||
}))
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* 接受暂存的邀请(已登录时调用)
|
||||
* 用邀请码调用 AcceptFriendRequest,成功后清除暂存
|
||||
* @returns {Promise<boolean>} 是否成功接受
|
||||
*/
|
||||
export async function handlePendingInvite() {
|
||||
// 未登录时不处理,等登录完成后再调用
|
||||
if (uni.getStorageSync('is_logged_in') !== 'true') return false
|
||||
let invite = null
|
||||
try {
|
||||
invite = JSON.parse(uni.getStorageSync(INVITE_KEY) || '')
|
||||
} catch (e) { /* ignore */ }
|
||||
if (!invite || !invite.inviteCode) return false
|
||||
|
||||
try {
|
||||
await AcceptFriendRequest({ requestId: invite.inviteCode })
|
||||
uni.removeStorageSync(INVITE_KEY)
|
||||
const nick = invite.inviterNick || '邀请人'
|
||||
uni.showToast({ title: `已接受邀请,和「${nick}」成为酒友 🍻`, icon: 'none', duration: 2500 })
|
||||
return true
|
||||
} catch (e) {
|
||||
// 后端可能返回"已是好友/邀请已失效"等业务失败,同样清除暂存避免重复提示
|
||||
uni.removeStorageSync(INVITE_KEY)
|
||||
console.warn('[invite] 接受邀请失败', e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/* 碰盏日记 - UniAppWebSocket 适配器
|
||||
* 将 uni-app 的 uni.connectSocket 适配为 HaveADrink SDK WebsocketInterface
|
||||
* (标准 WebSocket 风格:constructor(host)、onopen/onmessage/onclose/onerror、send、close)
|
||||
* 用法:在 SDK 初始化时传入 websocket: UniAppWebSocket
|
||||
*/
|
||||
|
||||
export default class UniAppWebSocket {
|
||||
/**
|
||||
* @param {string} host - 完整 ws/wss 连接地址(SDK 已处理 https→wss 协议转换)
|
||||
*/
|
||||
constructor(host) {
|
||||
this.url = host
|
||||
// 可赋值的事件回调(SDK ChatWebSocketConn.connect() 中直接赋值)
|
||||
this.onopen = null
|
||||
this.onmessage = null
|
||||
this.onclose = null
|
||||
this.onerror = null
|
||||
// 连接状态: connecting | open | closed
|
||||
this.readyState = 0
|
||||
this._sendQueue = []
|
||||
this._closedByUser = false
|
||||
|
||||
this._connect()
|
||||
}
|
||||
|
||||
_connect() {
|
||||
// 微信小程序下直接用 wx.connectSocket:
|
||||
// uni 的 API 运行时(uni.api.esm.js)会向 connectSocket 注入 success/fail/complete 回调,
|
||||
// 而微信规则是“传入回调后不再返回 SocketTask”,导致 uni.connectSocket 永远拿不到 task。
|
||||
// wx.connectSocket 不传回调时正常返回 SocketTask,避免降级到全局 socket API
|
||||
// (全局 API 重连时会重复注册监听器,引发重连风暴)
|
||||
console.log('[UniAppWebSocket] 发起连接:', this.url)
|
||||
const rawConnect = (typeof wx !== 'undefined' && typeof wx.connectSocket === 'function')
|
||||
? wx.connectSocket.bind(wx)
|
||||
: uni.connectSocket
|
||||
const task = rawConnect({ url: this.url })
|
||||
|
||||
// 部分平台 uni.connectSocket 无返回值,降级为全局 API 监听
|
||||
if (task && typeof task.onOpen === 'function') {
|
||||
this.socketTask = task
|
||||
|
||||
task.onOpen((res) => {
|
||||
console.log('[UniAppWebSocket] 连接已打开(open)')
|
||||
this.readyState = 1
|
||||
this._flush()
|
||||
if (typeof this.onopen === 'function') {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
})
|
||||
|
||||
task.onMessage((res) => {
|
||||
if (typeof this.onmessage === 'function') {
|
||||
// 与浏览器 MessageEvent 对齐:消息内容放在 event.data
|
||||
this.onmessage({ type: 'message', data: res.data })
|
||||
}
|
||||
})
|
||||
|
||||
task.onClose((event) => {
|
||||
// 诊断:打印关闭码/原因,用于判断是服务端拒连还是网络问题
|
||||
// 常见 code:1006 异常断开、1000 正常关闭;reason 由服务端给出
|
||||
console.warn('[UniAppWebSocket] 连接被关闭(close) code:', event && event.code,
|
||||
'reason:', event && event.reason)
|
||||
this.readyState = 3
|
||||
if (!this._closedByUser && typeof this.onclose === 'function') {
|
||||
this.onclose(Object.assign({ type: 'close' }, event || {}))
|
||||
}
|
||||
})
|
||||
|
||||
task.onError((err) => {
|
||||
// 诊断:微信会在 errMsg 中给出具体原因(域名不在合法列表、TLS 失败等)
|
||||
console.warn('[UniAppWebSocket] 连接错误(error):', err && (err.errMsg || err.message || JSON.stringify(err)))
|
||||
if (typeof this.onerror === 'function') {
|
||||
this.onerror(Object.assign({ type: 'error' }, err || {}))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.warn('[UniAppWebSocket] 未获得 SocketTask,降级到全局 socket API')
|
||||
// 兜底:旧版小程序全局 socket API
|
||||
uni.onSocketOpen(() => {
|
||||
this.readyState = 1
|
||||
this._flush()
|
||||
if (typeof this.onopen === 'function') {
|
||||
this.onopen({ type: 'open' })
|
||||
}
|
||||
})
|
||||
uni.onSocketMessage((res) => {
|
||||
if (typeof this.onmessage === 'function') {
|
||||
this.onmessage({ type: 'message', data: res.data })
|
||||
}
|
||||
})
|
||||
uni.onSocketClose((event) => {
|
||||
this.readyState = 3
|
||||
if (!this._closedByUser && typeof this.onclose === 'function') {
|
||||
this.onclose(Object.assign({ type: 'close' }, event || {}))
|
||||
}
|
||||
})
|
||||
uni.onSocketError((err) => {
|
||||
if (typeof this.onerror === 'function') {
|
||||
this.onerror(Object.assign({ type: 'error' }, err || {}))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据(与浏览器 WebSocket.send 签名一致)
|
||||
* @param {string} data
|
||||
*/
|
||||
send(data) {
|
||||
if (this.readyState !== 1) {
|
||||
// 连接未就绪时暂存,连接建立后补发
|
||||
this._sendQueue.push(data)
|
||||
return
|
||||
}
|
||||
if (this.socketTask) {
|
||||
this.socketTask.send({
|
||||
data,
|
||||
fail: (err) => {
|
||||
console.warn('[UniAppWebSocket] 发送失败', err)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 兜底:旧版小程序全局 socket API
|
||||
uni.sendSocketMessage({
|
||||
data,
|
||||
fail: (err) => {
|
||||
console.warn('[UniAppWebSocket] 发送失败', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 主动关闭连接(主动关闭不再触发 onclose,由 SDK 自行控制重连) */
|
||||
close() {
|
||||
this._closedByUser = true
|
||||
this.readyState = 3
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close({
|
||||
complete: () => {}
|
||||
})
|
||||
this.socketTask = null
|
||||
} else {
|
||||
uni.closeSocket({
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 连接建立后补发暂存消息 */
|
||||
_flush() {
|
||||
if (!this._sendQueue.length) return
|
||||
const queue = [...this._sendQueue]
|
||||
this._sendQueue = []
|
||||
queue.forEach((data) => this.send(data))
|
||||
}
|
||||
}
|
||||
+107
-137
@@ -1,10 +1,34 @@
|
||||
/* 碰盏日记 - WebSocket 管理器
|
||||
* 单例模式,负责私信实时通信
|
||||
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
|
||||
* 支持心跳、自动重连、消息队列
|
||||
* 基于 HaveADrink SDK 的 ChatWebSocket(ChatWebSocketConn)实现:
|
||||
* - 底层 socket 通过 UniAppWebSocket 适配器注入(common/uni-websocket.js)
|
||||
* - 连接地址、token query、指数退避重连均由 SDK 管理
|
||||
* - 消息协议:{ mesgType, data }
|
||||
* 对外保持 on/off/send/connect/disconnect/userId 接口,页面层无感知
|
||||
*/
|
||||
|
||||
const WS_HOST = 'wss://dev.wash-painting.cn/api/have_a_drink/v1/chat/chat'
|
||||
import client, { API_HOST } from './api'
|
||||
// SDK 具名导出的连接类(绕开 client.ChatWebSocket 方法内部使用的浏览器 API)
|
||||
import { ChatWebSocketConn } from 'HaveADrink'
|
||||
|
||||
/**
|
||||
* 自行解析 host 并构造 ChatWebSocketConn
|
||||
* SDK 的 client.ChatWebSocket() 内部使用 new URL() / new URLSearchParams(),
|
||||
* 微信小程序沙箱中裸标识符 URL 解析到不可构造的空壳,全局 polyfill 无法生效,
|
||||
* 因此这里手动完成同样的逻辑:https -> wss,query 键值对拼接
|
||||
* @param {object} input - { token }
|
||||
* @returns {ChatWebSocketConn}
|
||||
*/
|
||||
function createChatConn(input) {
|
||||
const m = String(API_HOST).match(/^(https?|wss?):\/\/([^/?#\s]+)/i)
|
||||
const scheme = m && m[1].toLowerCase() === 'https' ? 'wss' : 'ws'
|
||||
const host = `${scheme}://${m ? m[2] : API_HOST}`
|
||||
const query = Object.keys(input || {})
|
||||
.filter((k) => input[k] !== '' && input[k] !== null && input[k] !== undefined)
|
||||
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(input[k])}`)
|
||||
.join('&')
|
||||
return new ChatWebSocketConn(host, query)
|
||||
}
|
||||
|
||||
// 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
|
||||
const ClientMesgType = {
|
||||
@@ -14,42 +38,26 @@ const ClientMesgType = {
|
||||
Ping: 'ping'
|
||||
}
|
||||
|
||||
// 服务端消息类型(与 SDK ServerMesgType 枚举值一致)
|
||||
const ServerMesgType = {
|
||||
Chat: 'chat',
|
||||
Typing: 'typing',
|
||||
Read: 'read',
|
||||
Ack: 'ack',
|
||||
Pong: 'pong',
|
||||
Connected: 'connected',
|
||||
Kicked: 'kicked'
|
||||
}
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socketTask = null
|
||||
this.conn = null // SDK ChatWebSocketConn 实例
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this.listeners = {} // 事件订阅 { event: [callbacks] }
|
||||
this.messageQueue = [] // 断线期间暂存消息
|
||||
this.messageQueue = [] // 断线期间暂存的消息 { mesgType, data }
|
||||
this.heartbeatTimer = null
|
||||
this.heartbeatTimeout = null
|
||||
this.reconnectTimer = null
|
||||
this.reconnectAttempts = 0
|
||||
this.maxReconnectDelay = 30000
|
||||
this.heartbeatInterval = 30000 // 30s 发一次 ping
|
||||
this.heartbeatWait = 60000 // 60s 无 pong 则重连
|
||||
this.manualClose = false
|
||||
this.token = ''
|
||||
this.userId = '' // 连接成功后服务端返回的用户ID
|
||||
this.userId = '' // 服务端 connected 事件返回的用户ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 WebSocket 连接
|
||||
* 建立 WebSocket 连接(走 SDK ChatWebSocket)
|
||||
* @param {string} token - 用户认证 token
|
||||
*/
|
||||
connect(token) {
|
||||
if (this.isConnected || this.isConnecting) return
|
||||
if (this.conn || this.isConnecting) return
|
||||
this.token = token || uni.getStorageSync('auth_token') || ''
|
||||
if (!this.token) {
|
||||
console.warn('[WS] 无 token,跳过连接')
|
||||
@@ -57,65 +65,80 @@ class WebSocketManager {
|
||||
}
|
||||
this.manualClose = false
|
||||
this.isConnecting = true
|
||||
console.log('[WS] 正在连接(SDK ChatWebSocket)...')
|
||||
|
||||
const url = WS_HOST
|
||||
console.log('[WS] 正在连接...')
|
||||
// SDK 内部:new websocket(`${host}/api/have_a_drink/v1/chat/chat?token=xxx`)
|
||||
// websocket 即注入的 UniAppWebSocket 适配器,重连由 SDK 指数退避管理
|
||||
// 注意:不用 client.ChatWebSocket()(其内部 new URL 在小程序沙箱不可用),
|
||||
// 直接用 SDK 导出的 ChatWebSocketConn 构造,等价实现
|
||||
const conn = createChatConn({ token: this.token })
|
||||
|
||||
this.socketTask = uni.connectSocket({
|
||||
url,
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
complete: () => {}
|
||||
})
|
||||
|
||||
this.socketTask.onOpen(() => {
|
||||
conn.onopen = () => {
|
||||
console.log('[WS] 连接成功')
|
||||
this.isConnected = true
|
||||
this.isConnecting = false
|
||||
this.reconnectAttempts = 0
|
||||
this._startHeartbeat()
|
||||
this._flushQueue()
|
||||
this._emit('connect', {})
|
||||
})
|
||||
|
||||
this.socketTask.onMessage((res) => {
|
||||
try {
|
||||
const msg = JSON.parse(res.data)
|
||||
this._handleMessage(msg)
|
||||
} catch (e) {
|
||||
console.warn('[WS] 消息解析失败', res.data)
|
||||
}
|
||||
})
|
||||
|
||||
this.socketTask.onClose(() => {
|
||||
conn.onclose = () => {
|
||||
console.log('[WS] 连接关闭')
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this._stopHeartbeat()
|
||||
this._emit('disconnect', {})
|
||||
if (!this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
// 非主动关闭时,SDK 已自动安排指数退避重连,无需手动处理
|
||||
}
|
||||
})
|
||||
|
||||
this.socketTask.onError((err) => {
|
||||
conn.onerror = (err) => {
|
||||
console.warn('[WS] 连接错误', err)
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
})
|
||||
}
|
||||
|
||||
/** 主动断开连接 */
|
||||
// ===== 服务端协议消息(SDK 已解析 { mesgType, data }) =====
|
||||
conn.onServerChatData = (data) => {
|
||||
this._emit('message', data)
|
||||
}
|
||||
|
||||
conn.onServerTypingData = (data) => {
|
||||
this._emit('typing', data)
|
||||
}
|
||||
|
||||
conn.onServerReadData = (data) => {
|
||||
this._emit('read', data)
|
||||
}
|
||||
|
||||
conn.onServerAckData = (data) => {
|
||||
this._emit('ack', data)
|
||||
}
|
||||
|
||||
conn.onServerConnectedData = (data) => {
|
||||
// 服务端验证 token 成功后推送,携带 userId
|
||||
this.userId = data && data.userId ? data.userId : ''
|
||||
console.log('[WS] 服务端确认连接, userId:', this.userId)
|
||||
this._emit('connected', data)
|
||||
}
|
||||
|
||||
conn.onServerKickedData = (data) => {
|
||||
console.warn('[WS] 被踢下线')
|
||||
this._emit('kicked', data)
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.conn = conn
|
||||
}
|
||||
|
||||
/** 主动断开连接(SDK close() 会关闭重连标志并断开 socket) */
|
||||
disconnect() {
|
||||
this.manualClose = true
|
||||
this._stopHeartbeat()
|
||||
this._clearReconnect()
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close()
|
||||
this.socketTask = null
|
||||
if (this.conn) {
|
||||
this.conn.close()
|
||||
this.conn = null
|
||||
}
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,17 +147,16 @@ class WebSocketManager {
|
||||
* @param {object} data - 消息数据
|
||||
*/
|
||||
send(mesgType, data = {}) {
|
||||
const payload = JSON.stringify({ mesgType, data })
|
||||
if (this.isConnected && this.socketTask) {
|
||||
this.socketTask.send({ data: payload })
|
||||
if (this.isConnected && this.conn) {
|
||||
this._write(mesgType, data)
|
||||
} else {
|
||||
// 断线暂存队列(ping 不暂存)
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push(payload)
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
// 尝试重连
|
||||
if (!this.isConnecting && !this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
// 未连接时触发(重)连接
|
||||
if (!this.conn && !this.manualClose) {
|
||||
this.connect(this.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,38 +189,31 @@ class WebSocketManager {
|
||||
|
||||
// ========== 内部方法 ==========
|
||||
|
||||
/** 处理收到的消息(SDK ServerMesgType 协议) */
|
||||
_handleMessage(msg) {
|
||||
const { mesgType, data } = msg
|
||||
/** 通过 SDK writeClient* 方法写入消息 */
|
||||
_write(mesgType, data) {
|
||||
try {
|
||||
switch (mesgType) {
|
||||
case ServerMesgType.Chat:
|
||||
this._emit('message', data)
|
||||
case ClientMesgType.Chat:
|
||||
this.conn.writeClientChatData(data)
|
||||
break
|
||||
case ServerMesgType.Typing:
|
||||
this._emit('typing', data)
|
||||
case ClientMesgType.Typing:
|
||||
this.conn.writeClientTypingData(data)
|
||||
break
|
||||
case ServerMesgType.Read:
|
||||
this._emit('read', data)
|
||||
case ClientMesgType.Read:
|
||||
this.conn.writeClientReadData(data)
|
||||
break
|
||||
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()
|
||||
case ClientMesgType.Ping:
|
||||
this.conn.writeClientPingData(data)
|
||||
break
|
||||
default:
|
||||
console.log('[WS] 未知消息类型', mesgType)
|
||||
console.warn('[WS] 未知发送类型', mesgType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] 发送异常', e)
|
||||
// 发送失败时暂存,待重连后补发
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,61 +227,20 @@ class WebSocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动心跳 */
|
||||
/** 启动心跳(保活,防运营商/小程序后台掐连接) */
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.send(ClientMesgType.Ping, {})
|
||||
// 设置超时检测
|
||||
this.heartbeatTimeout = setTimeout(() => {
|
||||
console.warn('[WS] 心跳超时,触发重连')
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close()
|
||||
}
|
||||
}, this.heartbeatWait)
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/** 收到 pong,清除超时 */
|
||||
_onPong() {
|
||||
if (this.heartbeatTimeout) {
|
||||
clearTimeout(this.heartbeatTimeout)
|
||||
this.heartbeatTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 停止心跳 */
|
||||
_stopHeartbeat() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
if (this.heartbeatTimeout) {
|
||||
clearTimeout(this.heartbeatTimeout)
|
||||
this.heartbeatTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
/** 指数退避重连 */
|
||||
_scheduleReconnect() {
|
||||
if (this.manualClose || this.reconnectTimer) return
|
||||
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay)
|
||||
this.reconnectAttempts++
|
||||
console.log(`[WS] ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`)
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.isConnecting = false
|
||||
this.connect(this.token)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/** 清除重连计时器 */
|
||||
_clearReconnect() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
/** 重连成功后重发队列消息 */
|
||||
@@ -275,11 +249,7 @@ class WebSocketManager {
|
||||
console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
|
||||
const queue = [...this.messageQueue]
|
||||
this.messageQueue = []
|
||||
queue.forEach(payload => {
|
||||
if (this.socketTask && this.isConnected) {
|
||||
this.socketTask.send({ data: payload })
|
||||
}
|
||||
})
|
||||
queue.forEach(({ mesgType, data }) => this._write(mesgType, data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+35
-4
@@ -53,8 +53,13 @@
|
||||
<text class="feed-action-icon">💬</text>
|
||||
<text class="feed-action-num">{{ feed.comments || '' }}</text>
|
||||
</view>
|
||||
<view class="feed-action" @click.stop="$emit('share', feed)">
|
||||
<text class="feed-action-icon">↗</text>
|
||||
<button class="feed-action share-btn" open-type="share" @click.stop="$emit('share', feed)">
|
||||
<text class="feed-action-icon">📤</text>
|
||||
</button>
|
||||
<!-- 删除:仅自己的动态显示 -->
|
||||
<view v-if="canDelete" class="feed-action feed-delete" @click.stop="$emit('delete', feed)">
|
||||
<text class="feed-action-icon">🗑️</text>
|
||||
<text class="feed-delete-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -66,9 +71,11 @@ import { getCatIcon, isIconPath } from '../common/utils'
|
||||
export default {
|
||||
name: 'FeedCard',
|
||||
props: {
|
||||
feed: { type: Object, default: () => ({}) }
|
||||
feed: { type: Object, default: () => ({}) },
|
||||
// 是否允许删除(仅自己的动态传 true)
|
||||
canDelete: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['item-click', 'like', 'comment', 'share'],
|
||||
emits: ['item-click', 'like', 'comment', 'share', 'delete'],
|
||||
methods: {
|
||||
getCatIcon,
|
||||
isIconPath,
|
||||
@@ -228,6 +235,16 @@ export default {
|
||||
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
|
||||
}
|
||||
|
||||
/* 删除按钮靠右 */
|
||||
.feed-delete {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.feed-delete-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.feed-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -251,4 +268,18 @@ export default {
|
||||
font-size: $fs-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* 分享按钮:重置 button 默认样式,保持与其他操作项视觉一致 */
|
||||
.feed-action.share-btn {
|
||||
margin: 0;
|
||||
padding: 8rpx 0;
|
||||
background: transparent;
|
||||
line-height: 1;
|
||||
border-radius: 0;
|
||||
font-size: inherit;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/HaveADrink": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
|
||||
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
|
||||
"integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
+99
-2
@@ -14,7 +14,7 @@
|
||||
* 6. 社交模块 - 朋友圈动态、点赞
|
||||
|
||||
|
||||
**版本:** v1.0.8
|
||||
**版本:** v1.0.12
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -100,6 +100,7 @@ client.WechatLogin(req).then(...).catch(...)
|
||||
### 获取微信手机号
|
||||
|
||||
|
||||
**已废弃:** 未使用
|
||||
|
||||
<font color="green">POST</font> `/api/have_a_drink/v1/auth/wechat/get_phone_number`
|
||||
|
||||
@@ -222,6 +223,7 @@ client.GetAchievements(req).then(...).catch(...)
|
||||
### 获取朋友圈动态
|
||||
|
||||
|
||||
**已废弃:** 不使用,待移除
|
||||
|
||||
<font color="green">GET</font> `/api/have_a_drink/v1/communication/feed`
|
||||
|
||||
@@ -1236,6 +1238,33 @@ const req = new PublishFeedReq()
|
||||
client.PublishFeed(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 删除动态
|
||||
|
||||
|
||||
|
||||
<font color="green">DELETE</font> `/api/have_a_drink/v1/moments/feeds`
|
||||
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|id|`string\|number`|required| 动态ID|
|
||||
|
||||
|
||||
|
||||
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|ok|`boolean`| 是否成功<br>|
|
||||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
const req = new DeleteFeedReq()
|
||||
client.DeleteFeed(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 点赞
|
||||
|
||||
|
||||
@@ -1408,6 +1437,7 @@ client.GetFriends(req).then(...).catch(...)
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|keyword|`string`|| 搜索关键词|
|
||||
|
||||
|
||||
|
||||
@@ -1445,7 +1475,6 @@ client.GetFriendRequests(req).then(...).catch(...)
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|userId|`string\|number`|| 酒友ID|
|
||||
|message|`string`|| 消息|
|
||||
|
||||
|
||||
@@ -1454,6 +1483,7 @@ client.GetFriendRequests(req).then(...).catch(...)
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|inviteCode|`string\|number`| 好友请求ID<br>|
|
||||
|
||||
|
||||
|
||||
@@ -1687,6 +1717,73 @@ const req = new CreateEventReq()
|
||||
client.CreateEvent(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 修改酒局
|
||||
|
||||
|
||||
|
||||
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/update`
|
||||
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|id|`string\|number`|required| 酒局ID|
|
||||
|title|`string`|| 酒局标题|
|
||||
|time|`string`|| 酒局时间|
|
||||
|location|`string`|| 酒局地点|
|
||||
|maxPeople|`number`|| 最大人数|
|
||||
|geo|`GeoPoint`|| 坐标信息|
|
||||
|note|`string`|| 酒局备注|
|
||||
|
||||
|
||||
|
||||
**GeoPoint**
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|latitude|`number`|| 纬度|
|
||||
|longitude|`number`|| 经度|
|
||||
|
||||
|
||||
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|ok|`boolean`| 是否成功<br>|
|
||||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
const req = new UpdateEventReq()
|
||||
client.UpdateEvent(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 删除酒局
|
||||
|
||||
|
||||
|
||||
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/delete`
|
||||
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|id|`string\|number`|required| 酒局ID|
|
||||
|
||||
|
||||
|
||||
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|ok|`boolean`| 是否成功<br>|
|
||||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
const req = new DeleteEventReq()
|
||||
client.DeleteEvent(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 报名酒局
|
||||
|
||||
|
||||
|
||||
+257
-13
@@ -17,6 +17,15 @@ export declare interface option {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WebsocketInterface {
|
||||
close: ((this: WebsocketInterface, ev: CloseEvent) => any) | null;
|
||||
onopen: ((this: WebsocketInterface, ev: Event) => any) | null;
|
||||
onmessage: ((this: WebsocketInterface, ev: MessageEvent) => any) | null;
|
||||
onclose(event: CloseEvent): void;
|
||||
onerror(event: ErrorEvent): void;
|
||||
send(data: string | BufferSource | Blob): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举类型基类
|
||||
*/
|
||||
@@ -2934,6 +2943,52 @@ export declare class PublishFeedResp {
|
||||
static fromObject(o: Object): PublishFeedResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动态请求(路径参数)
|
||||
*/
|
||||
export declare class DeleteFeedReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 动态ID
|
||||
*/
|
||||
id: string|number;
|
||||
|
||||
/**
|
||||
* @param id string|number 动态ID
|
||||
*/
|
||||
constructor(id: string|number,);
|
||||
/**
|
||||
* 从对象创建 DeleteFeedReq
|
||||
*
|
||||
* @param o Object
|
||||
* - id: string|number, // 动态ID
|
||||
*/
|
||||
static fromObject(o: Object): DeleteFeedReq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动态响应
|
||||
*/
|
||||
export declare class DeleteFeedResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
ok: boolean;
|
||||
|
||||
/**
|
||||
* @param ok boolean 是否成功
|
||||
*/
|
||||
constructor(ok: boolean,);
|
||||
/**
|
||||
* 从对象创建 DeleteFeedResp
|
||||
*
|
||||
* @param o Object
|
||||
* - ok: boolean, // 是否成功
|
||||
*/
|
||||
static fromObject(o: Object): DeleteFeedResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞/取消点赞请求(路径参数)
|
||||
*/
|
||||
@@ -3265,14 +3320,20 @@ export declare class Friend {
|
||||
*/
|
||||
export declare class GetFriendRequestsReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
keyword: string;
|
||||
|
||||
/**
|
||||
* @param keyword string 搜索关键词
|
||||
*/
|
||||
constructor();
|
||||
constructor(keyword: string,);
|
||||
/**
|
||||
* 从对象创建 GetFriendRequestsReq
|
||||
*
|
||||
* @param o Object
|
||||
* - keyword: string, // 搜索关键词
|
||||
*/
|
||||
static fromObject(o: Object): GetFriendRequestsReq;
|
||||
}
|
||||
@@ -3405,24 +3466,18 @@ export declare class FriendRequestListData {
|
||||
export declare class SendFriendRequestReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 酒友ID
|
||||
*/
|
||||
userId: string|number;
|
||||
/**
|
||||
* 消息
|
||||
*/
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* @param userId string|number 酒友ID
|
||||
* @param message string 消息
|
||||
*/
|
||||
constructor(userId: string|number,message: string,);
|
||||
constructor(message: string,);
|
||||
/**
|
||||
* 从对象创建 SendFriendRequestReq
|
||||
*
|
||||
* @param o Object
|
||||
* - userId: string|number, // 酒友ID
|
||||
* - message: string, // 消息
|
||||
*/
|
||||
static fromObject(o: Object): SendFriendRequestReq;
|
||||
@@ -3433,14 +3488,20 @@ export declare class SendFriendRequestReq {
|
||||
*/
|
||||
export declare class SendFriendRequestResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 好友请求ID
|
||||
*/
|
||||
inviteCode: string|number;
|
||||
|
||||
/**
|
||||
* @param inviteCode string|number 好友请求ID
|
||||
*/
|
||||
constructor();
|
||||
constructor(inviteCode: string|number,);
|
||||
/**
|
||||
* 从对象创建 SendFriendRequestResp
|
||||
*
|
||||
* @param o Object
|
||||
* - inviteCode: string|number, // 好友请求ID
|
||||
*/
|
||||
static fromObject(o: Object): SendFriendRequestResp;
|
||||
}
|
||||
@@ -3788,6 +3849,134 @@ export declare class CreateEventResp {
|
||||
static fromObject(o: Object): CreateEventResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改酒局请求
|
||||
*/
|
||||
export declare class UpdateEventReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 酒局ID
|
||||
*/
|
||||
id: string|number;
|
||||
/**
|
||||
* 酒局标题
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 酒局时间
|
||||
*/
|
||||
time: string;
|
||||
/**
|
||||
* 酒局地点
|
||||
*/
|
||||
location: string;
|
||||
/**
|
||||
* 最大人数
|
||||
*/
|
||||
maxPeople: number;
|
||||
/**
|
||||
* 坐标信息
|
||||
*/
|
||||
geo: GeoPoint;
|
||||
/**
|
||||
* 酒局备注
|
||||
*/
|
||||
note: string;
|
||||
|
||||
/**
|
||||
* @param id string|number 酒局ID
|
||||
* @param title string 酒局标题
|
||||
* @param time string 酒局时间
|
||||
* @param location string 酒局地点
|
||||
* @param maxPeople number 最大人数
|
||||
* @param geo GeoPoint 坐标信息
|
||||
* @param note string 酒局备注
|
||||
*/
|
||||
constructor(id: string|number,title: string,time: string,location: string,maxPeople: number,geo: GeoPoint,note: string,);
|
||||
/**
|
||||
* 从对象创建 UpdateEventReq
|
||||
*
|
||||
* @param o Object
|
||||
* - id: string|number, // 酒局ID
|
||||
* - title: string, // 酒局标题
|
||||
* - time: string, // 酒局时间
|
||||
* - location: string, // 酒局地点
|
||||
* - maxPeople: number, // 最大人数
|
||||
* - geo: GeoPoint, // 坐标信息
|
||||
* - note: string, // 酒局备注
|
||||
*/
|
||||
static fromObject(o: Object): UpdateEventReq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改酒局响应
|
||||
*/
|
||||
export declare class UpdateEventResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
ok: boolean;
|
||||
|
||||
/**
|
||||
* @param ok boolean 是否成功
|
||||
*/
|
||||
constructor(ok: boolean,);
|
||||
/**
|
||||
* 从对象创建 UpdateEventResp
|
||||
*
|
||||
* @param o Object
|
||||
* - ok: boolean, // 是否成功
|
||||
*/
|
||||
static fromObject(o: Object): UpdateEventResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除酒局请求
|
||||
*/
|
||||
export declare class DeleteEventReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 酒局ID
|
||||
*/
|
||||
id: string|number;
|
||||
|
||||
/**
|
||||
* @param id string|number 酒局ID
|
||||
*/
|
||||
constructor(id: string|number,);
|
||||
/**
|
||||
* 从对象创建 DeleteEventReq
|
||||
*
|
||||
* @param o Object
|
||||
* - id: string|number, // 酒局ID
|
||||
*/
|
||||
static fromObject(o: Object): DeleteEventReq;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除酒局响应
|
||||
*/
|
||||
export declare class DeleteEventResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
ok: boolean;
|
||||
|
||||
/**
|
||||
* @param ok boolean 是否成功
|
||||
*/
|
||||
constructor(ok: boolean,);
|
||||
/**
|
||||
* 从对象创建 DeleteEventResp
|
||||
*
|
||||
* @param o Object
|
||||
* - ok: boolean, // 是否成功
|
||||
*/
|
||||
static fromObject(o: Object): DeleteEventResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名/取消/签到请求(路径参数)
|
||||
*/
|
||||
@@ -4617,6 +4806,29 @@ export declare class ChatWebSocketResp {
|
||||
static fromObject(o: Object): ChatWebSocketResp;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export declare class ChatWebSocketReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 连接 token
|
||||
*/
|
||||
token: string;
|
||||
|
||||
/**
|
||||
* @param token string 连接 token
|
||||
*/
|
||||
constructor(token: string,);
|
||||
/**
|
||||
* 从对象创建 ChatWebSocketReq
|
||||
*
|
||||
* @param o Object
|
||||
* - token: string, // 连接 token
|
||||
*/
|
||||
static fromObject(o: Object): ChatWebSocketReq;
|
||||
}
|
||||
|
||||
export type ClientMessage = ClientChatData | ClientTypingData | ClientReadData | ClientPingData | null
|
||||
export type ServerMessage = ServerChatData | ServerTypingData | ServerReadData | ServerAckData | ServerPongData | ServerConnectedData | ServerKickedData | null
|
||||
|
||||
@@ -4645,6 +4857,12 @@ export class ChatWebSocketConn {
|
||||
public close(): void;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
host: string;
|
||||
http_request: (url: string, params: any) => Promise<any>;
|
||||
upload: (url: string, params: any) => Promise<any>;
|
||||
websocket: WebsocketInterface | undefined;
|
||||
}
|
||||
/**
|
||||
* * 喝酒了么 - 后端接口 API
|
||||
* 小程序:喝酒了么(uni-app 微信小程序)
|
||||
@@ -4660,9 +4878,9 @@ export class ChatWebSocketConn {
|
||||
*/
|
||||
export default class HaveADrink {
|
||||
host: string;
|
||||
http_request: any;
|
||||
upload: any;
|
||||
constructor(conf: any);
|
||||
http_request: (url: string, params: any) => Promise<any>;
|
||||
upload: (url: string, params: any) => Promise<any>;
|
||||
constructor(conf: Config);
|
||||
|
||||
/**
|
||||
* 设置 token
|
||||
@@ -4682,6 +4900,7 @@ export default class HaveADrink {
|
||||
/**
|
||||
* 获取微信手机号
|
||||
*
|
||||
* @deprecated 未使用
|
||||
* @param req WechatGetPhoneNumberReq 获取微信手机号
|
||||
* @return WechatGetPhoneNumberResp 获取微信手机号返回值
|
||||
*/
|
||||
@@ -4706,6 +4925,7 @@ export default class HaveADrink {
|
||||
/**
|
||||
* 获取朋友圈动态
|
||||
*
|
||||
* @deprecated 不使用,待移除
|
||||
* @param req GetFeedReq 获取朋友圈动态请求
|
||||
* @return GetFeedResp 获取朋友圈动态响应
|
||||
*/
|
||||
@@ -4831,6 +5051,14 @@ export default class HaveADrink {
|
||||
*/
|
||||
public PublishFeed(req: PublishFeedReq) : Promise<PublishFeedResp>;
|
||||
|
||||
/**
|
||||
* 删除动态
|
||||
*
|
||||
* @param req DeleteFeedReq 删除动态请求(路径参数)
|
||||
* @return DeleteFeedResp 删除动态响应
|
||||
*/
|
||||
public DeleteFeed(req: DeleteFeedReq) : Promise<DeleteFeedResp>;
|
||||
|
||||
/**
|
||||
* 点赞
|
||||
*
|
||||
@@ -4927,6 +5155,22 @@ export default class HaveADrink {
|
||||
*/
|
||||
public CreateEvent(req: CreateEventReq) : Promise<CreateEventResp>;
|
||||
|
||||
/**
|
||||
* 修改酒局
|
||||
*
|
||||
* @param req UpdateEventReq 修改酒局请求
|
||||
* @return UpdateEventResp 修改酒局响应
|
||||
*/
|
||||
public UpdateEvent(req: UpdateEventReq) : Promise<UpdateEventResp>;
|
||||
|
||||
/**
|
||||
* 删除酒局
|
||||
*
|
||||
* @param req DeleteEventReq 删除酒局请求
|
||||
* @return DeleteEventResp 删除酒局响应
|
||||
*/
|
||||
public DeleteEvent(req: DeleteEventReq) : Promise<DeleteEventResp>;
|
||||
|
||||
/**
|
||||
* 报名酒局
|
||||
*
|
||||
@@ -4989,6 +5233,6 @@ export default class HaveADrink {
|
||||
* @param req ChatWebSocket
|
||||
* @return ChatWebSocketConn
|
||||
*/
|
||||
public ChatWebSocket() : ChatWebSocketConn;
|
||||
public ChatWebSocket(input: ChatWebSocketReq) : ChatWebSocketConn;
|
||||
|
||||
}
|
||||
|
||||
+250
-12
@@ -13,6 +13,8 @@
|
||||
* 6. 社交模块 - 朋友圈动态、点赞
|
||||
*/
|
||||
|
||||
let websocket = WebSocket;
|
||||
|
||||
export class Enum {
|
||||
constructor(value) {
|
||||
this.v = value;
|
||||
@@ -2051,6 +2053,38 @@ export class PublishFeedResp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动态请求(路径参数)
|
||||
*/
|
||||
export class DeleteFeedReq {
|
||||
/**
|
||||
* @param id: string|number 动态ID
|
||||
*/
|
||||
constructor(id,) {
|
||||
this.id = id;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new DeleteFeedReq(o.id,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动态响应
|
||||
*/
|
||||
export class DeleteFeedResp {
|
||||
/**
|
||||
* @param ok: boolean 是否成功
|
||||
*/
|
||||
constructor(ok,) {
|
||||
this.ok = ok;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new DeleteFeedResp(o.ok,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞/取消点赞请求(路径参数)
|
||||
*/
|
||||
@@ -2248,12 +2282,14 @@ export class Friend {
|
||||
*/
|
||||
export class GetFriendRequestsReq {
|
||||
/**
|
||||
* @param keyword: string 搜索关键词
|
||||
*/
|
||||
constructor() {
|
||||
constructor(keyword,) {
|
||||
this.keyword = keyword;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new GetFriendRequestsReq();
|
||||
return new GetFriendRequestsReq(o.keyword,);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2336,16 +2372,14 @@ export class FriendRequestListData {
|
||||
*/
|
||||
export class SendFriendRequestReq {
|
||||
/**
|
||||
* @param userId: string|number 酒友ID
|
||||
* @param message: string 消息
|
||||
*/
|
||||
constructor(userId,message,) {
|
||||
this.userId = userId;
|
||||
constructor(message,) {
|
||||
this.message = message;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new SendFriendRequestReq(o.userId,o.message,);
|
||||
return new SendFriendRequestReq(o.message,);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2354,12 +2388,14 @@ export class SendFriendRequestReq {
|
||||
*/
|
||||
export class SendFriendRequestResp {
|
||||
/**
|
||||
* @param inviteCode: string|number 好友请求ID
|
||||
*/
|
||||
constructor() {
|
||||
constructor(inviteCode,) {
|
||||
this.inviteCode = inviteCode;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new SendFriendRequestResp();
|
||||
return new SendFriendRequestResp(o.inviteCode,);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2569,6 +2605,82 @@ export class CreateEventResp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改酒局请求
|
||||
*/
|
||||
export class UpdateEventReq {
|
||||
/**
|
||||
* @param id: string|number 酒局ID
|
||||
* @param title: string 酒局标题
|
||||
* @param time: string 酒局时间
|
||||
* @param location: string 酒局地点
|
||||
* @param maxPeople: number 最大人数
|
||||
* @param geo: GeoPoint 坐标信息
|
||||
* @param note: string 酒局备注
|
||||
*/
|
||||
constructor(id,title,time,location,maxPeople,geo,note,) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.time = time;
|
||||
this.location = location;
|
||||
this.maxPeople = maxPeople;
|
||||
this.geo = geo;
|
||||
this.note = note;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new UpdateEventReq(o.id,o.title,o.time,o.location,o.maxPeople,o.geo,o.note,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改酒局响应
|
||||
*/
|
||||
export class UpdateEventResp {
|
||||
/**
|
||||
* @param ok: boolean 是否成功
|
||||
*/
|
||||
constructor(ok,) {
|
||||
this.ok = ok;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new UpdateEventResp(o.ok,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除酒局请求
|
||||
*/
|
||||
export class DeleteEventReq {
|
||||
/**
|
||||
* @param id: string|number 酒局ID
|
||||
*/
|
||||
constructor(id,) {
|
||||
this.id = id;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new DeleteEventReq(o.id,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除酒局响应
|
||||
*/
|
||||
export class DeleteEventResp {
|
||||
/**
|
||||
* @param ok: boolean 是否成功
|
||||
*/
|
||||
constructor(ok,) {
|
||||
this.ok = ok;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new DeleteEventResp(o.ok,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名/取消/签到请求(路径参数)
|
||||
*/
|
||||
@@ -3087,19 +3199,36 @@ export class ChatWebSocketResp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class ChatWebSocketReq {
|
||||
/**
|
||||
* @param token: string 连接 token
|
||||
*/
|
||||
constructor(token,) {
|
||||
this.token = token;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new ChatWebSocketReq(o.token,);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatWebSocketConn {
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param host string 主机名
|
||||
*/
|
||||
constructor(host) {
|
||||
constructor(host, query) {
|
||||
this.host = host;
|
||||
this.reconnect = true;
|
||||
this.reconnectDelay = 1000;
|
||||
this.maxReconnectDelay = 5000;
|
||||
this.randomizationFactor = 0.5;
|
||||
this.attempt = 0;
|
||||
this.query = query;
|
||||
this.connect();
|
||||
}
|
||||
backoff(attempt) {
|
||||
@@ -3115,7 +3244,7 @@ export class ChatWebSocketConn {
|
||||
return delayWithJitter;
|
||||
}
|
||||
connect() {
|
||||
let socket = new WebSocket(`ws://${this.host}/api/have_a_drink/v1/chat/chat`);
|
||||
let socket = new websocket(`${this.host}/api/have_a_drink/v1/chat/chat?${this.query}`);
|
||||
socket.onopen = (event) => {
|
||||
this.onopen(event);
|
||||
};
|
||||
@@ -3281,6 +3410,12 @@ export class ChatWebSocketConn {
|
||||
}
|
||||
}
|
||||
|
||||
export class Config {
|
||||
host = "";
|
||||
http_request = (url, params) => {};
|
||||
upload = (url, params) => {};
|
||||
websocket = undefined;
|
||||
}
|
||||
/**
|
||||
* * 喝酒了么 - 后端接口 API
|
||||
* 小程序:喝酒了么(uni-app 微信小程序)
|
||||
@@ -3296,6 +3431,9 @@ export class ChatWebSocketConn {
|
||||
*/
|
||||
export default class HaveADrink {
|
||||
constructor(conf) {
|
||||
if (conf.websocket) {
|
||||
websocket = conf.websocket;
|
||||
}
|
||||
this.host = conf.host;
|
||||
this.http_request = conf.http_request;
|
||||
this.upload = conf.upload;
|
||||
@@ -3945,6 +4083,38 @@ export default class HaveADrink {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除动态
|
||||
*/
|
||||
DeleteFeed(req) {
|
||||
return new Promise((reslove, reject)=>{
|
||||
let data = req;
|
||||
let url = `${this.host}/api/have_a_drink/v1/moments/feeds`;
|
||||
|
||||
|
||||
this.http_request(url, {
|
||||
uri: '/api/have_a_drink/v1/moments/feeds',
|
||||
method: 'DELETE',
|
||||
data: data,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((data)=>{
|
||||
if (data.hasOwnProperty("fail")) {
|
||||
if (data.fail) {
|
||||
reject(data.msg);
|
||||
} else {
|
||||
reslove(data.data);
|
||||
}
|
||||
} else {
|
||||
reslove(data);
|
||||
}
|
||||
}).catch((err)=>reject(err));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞
|
||||
*/
|
||||
@@ -4329,6 +4499,70 @@ export default class HaveADrink {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改酒局
|
||||
*/
|
||||
UpdateEvent(req) {
|
||||
return new Promise((reslove, reject)=>{
|
||||
let data = req;
|
||||
let url = `${this.host}/api/have_a_drink/v1/events/events/update`;
|
||||
|
||||
|
||||
this.http_request(url, {
|
||||
uri: '/api/have_a_drink/v1/events/events/update',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((data)=>{
|
||||
if (data.hasOwnProperty("fail")) {
|
||||
if (data.fail) {
|
||||
reject(data.msg);
|
||||
} else {
|
||||
reslove(data.data);
|
||||
}
|
||||
} else {
|
||||
reslove(data);
|
||||
}
|
||||
}).catch((err)=>reject(err));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除酒局
|
||||
*/
|
||||
DeleteEvent(req) {
|
||||
return new Promise((reslove, reject)=>{
|
||||
let data = req;
|
||||
let url = `${this.host}/api/have_a_drink/v1/events/events/delete`;
|
||||
|
||||
|
||||
this.http_request(url, {
|
||||
uri: '/api/have_a_drink/v1/events/events/delete',
|
||||
method: 'POST',
|
||||
data: data,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then((data)=>{
|
||||
if (data.hasOwnProperty("fail")) {
|
||||
if (data.fail) {
|
||||
reject(data.msg);
|
||||
} else {
|
||||
reslove(data.data);
|
||||
}
|
||||
} else {
|
||||
reslove(data);
|
||||
}
|
||||
}).catch((err)=>reject(err));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名酒局
|
||||
*/
|
||||
@@ -4556,9 +4790,13 @@ export default class HaveADrink {
|
||||
/**
|
||||
* 注意:连接建立后,服务端会先验证 token,然后推送 connected 事件
|
||||
*/
|
||||
ChatWebSocket() {
|
||||
ChatWebSocket(input) {
|
||||
const url = new URL(this.host);
|
||||
return new ChatWebSocketConn(url.host);
|
||||
let host = "ws://" + url.host;
|
||||
if (url.protocol == 'https:') {
|
||||
host = "wss://" + url.host;
|
||||
}
|
||||
return new ChatWebSocketConn(host, new URLSearchParams(Object.fromEntries(Object.entries(input).filter(([_, v]) => v !== '' && v !== null && v !== undefined))).toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "HaveADrink",
|
||||
"type": "module",
|
||||
"version": "v1.0.8",
|
||||
"version": "v1.0.12",
|
||||
"description": "喝酒了么 API 服务",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
Generated
+4
-4
@@ -5,13 +5,13 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"HaveADrink": "^1.0.8"
|
||||
"HaveADrink": "^1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/HaveADrink": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz",
|
||||
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==",
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
|
||||
"integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"HaveADrink": "^1.0.8"
|
||||
"HaveADrink": "^1.0.12"
|
||||
}
|
||||
}
|
||||
|
||||
+35
-8
@@ -178,20 +178,30 @@ export default {
|
||||
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 */ }
|
||||
await this.ensureConversationId()
|
||||
}
|
||||
this.loadMessages()
|
||||
this.markRead()
|
||||
this.bindWsEvents()
|
||||
},
|
||||
/** 从会话列表按 friendId 查找 conversationId(首次聊天时会话尚未创建,返回 false) */
|
||||
async ensureConversationId() {
|
||||
if (this.conversationId) return true
|
||||
if (!this.friendId) return false
|
||||
try {
|
||||
const res = await GetConversations()
|
||||
const conv = (res.data.list || []).find(c => String(c.friendId) === String(this.friendId))
|
||||
if (conv) {
|
||||
this.conversationId = conv.id
|
||||
return true
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return false
|
||||
},
|
||||
// === 数据加载 ===
|
||||
async loadMessages() {
|
||||
// 无会话ID不发请求(首次聊天无历史消息,避免空参请求)
|
||||
if (!this.conversationId) return
|
||||
try {
|
||||
const res = await GetMessages({ conversationId: this.conversationId, lastID: this.lastID, pageSize: 20 })
|
||||
this.messages = res.data.list || []
|
||||
@@ -209,6 +219,8 @@ export default {
|
||||
},
|
||||
|
||||
async markRead() {
|
||||
// 无会话ID无需标记已读
|
||||
if (!this.conversationId) return
|
||||
try {
|
||||
await MarkRead({ conversationId: this.conversationId })
|
||||
// 通过 WebSocket 发送已读回执
|
||||
@@ -238,6 +250,10 @@ export default {
|
||||
onWsMessage(data) {
|
||||
// 只处理当前会话的消息
|
||||
if (String(data.senderId) !== String(this.friendId)) return
|
||||
// 首次会话:后端创建会话后消息携带 conversationId,回填本地
|
||||
if (!this.conversationId && data.conversationId) {
|
||||
this.conversationId = data.conversationId
|
||||
}
|
||||
const msg = {
|
||||
id: data.msgId ? String(data.msgId) : 'msg_' + Date.now(),
|
||||
conversationId: data.conversationId || this.conversationId,
|
||||
@@ -251,11 +267,13 @@ export default {
|
||||
this.messages.push(msg)
|
||||
this.peerTyping = false
|
||||
this.$nextTick(() => this.scrollToBottom())
|
||||
// 发送已读回执
|
||||
// 发送已读回执(需已有会话ID)
|
||||
if (this.conversationId) {
|
||||
wsManager.send('read', {
|
||||
conversationId: this.conversationId,
|
||||
lastMsgId: msg.id
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 对方正在输入 */
|
||||
@@ -270,6 +288,10 @@ export default {
|
||||
|
||||
/** 消息送达确认 */
|
||||
onWsAck(data) {
|
||||
// ack 若携带会话ID,回填本地(首次会话兼容)
|
||||
if (!this.conversationId && data.conversationId) {
|
||||
this.conversationId = data.conversationId
|
||||
}
|
||||
const msg = this.messages.find(m => m.id === data.clientMsgId || m.id === String(data.clientMsgId))
|
||||
if (msg) {
|
||||
msg.id = data.msgId ? String(data.msgId) : msg.id
|
||||
@@ -308,6 +330,11 @@ export default {
|
||||
msgType: 'text',
|
||||
clientMesgId: clientMsgId
|
||||
})
|
||||
|
||||
// 首次聊天无会话ID:后端收到首条消息后会创建会话,延迟查找回填
|
||||
if (!this.conversationId) {
|
||||
setTimeout(() => { this.ensureConversationId() }, 800)
|
||||
}
|
||||
},
|
||||
|
||||
/** 重发失败消息 */
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
<FeedCard
|
||||
v-if="feed"
|
||||
:feed="feed"
|
||||
:can-delete="isMyFeed(feed)"
|
||||
@like="handleLike"
|
||||
@share="handleShare"
|
||||
@delete="confirmDeleteFeed"
|
||||
/>
|
||||
|
||||
<!-- 评论区 -->
|
||||
@@ -62,7 +65,7 @@
|
||||
import FeedCard from '../../components/FeedCard.vue'
|
||||
import CommentItem from '../../components/CommentItem.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api'
|
||||
import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed, DeleteFeed } from '../../common/api'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -75,11 +78,16 @@ export default {
|
||||
feedId: '',
|
||||
feed: null,
|
||||
comments: [],
|
||||
commentText: ''
|
||||
commentText: '',
|
||||
// 当前分享的动态
|
||||
shareFeed: null,
|
||||
// 分享落地页携带的动态快照参数(好友打开分享链接时兼容展示)
|
||||
shareOptions: {}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.feedId = options.id || ''
|
||||
this.shareOptions = options || {}
|
||||
this.loadFeed()
|
||||
this.loadComments()
|
||||
},
|
||||
@@ -88,10 +96,27 @@ export default {
|
||||
uni.navigateBack()
|
||||
},
|
||||
async loadFeed() {
|
||||
// 从 globalData 中查找对应动态
|
||||
// 优先从 globalData 中查找对应动态
|
||||
const app = getApp()
|
||||
const feeds = (app.globalData && app.globalData.circleFeeds) || []
|
||||
this.feed = feeds.find(f => String(f.id) === String(this.feedId)) || null
|
||||
const found = feeds.find(f => String(f.id) === String(this.feedId)) || null
|
||||
if (found) {
|
||||
this.feed = found
|
||||
return
|
||||
}
|
||||
// 分享落地页兼容:本地无缓存时用分享链接携带的快照参数展示
|
||||
const o = this.shareOptions
|
||||
if (o.nick || o.text) {
|
||||
this.feed = {
|
||||
id: this.feedId,
|
||||
nickname: decodeURIComponent(o.nick || ''),
|
||||
text: decodeURIComponent(o.text || ''),
|
||||
time: decodeURIComponent(o.time || ''),
|
||||
avatar: '',
|
||||
likes: 0,
|
||||
comments: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadComments() {
|
||||
try {
|
||||
@@ -109,6 +134,39 @@ export default {
|
||||
else await UnlikeFeed({ feedId: feed.id })
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
/** 记录当前要分享的动态,供 onShareAppMessage 读取 */
|
||||
handleShare(feed) {
|
||||
this.shareFeed = feed
|
||||
},
|
||||
/** 判断是否为自己发布的动态(仅自己可删) */
|
||||
isMyFeed(feed) {
|
||||
let myId = ''
|
||||
try {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
myId = user.id !== undefined && user.id !== null ? String(user.id) : ''
|
||||
} catch (e) { /* ignore */ }
|
||||
if (!myId) return false
|
||||
return feed && feed.userId !== undefined && feed.userId !== null && String(feed.userId) === myId
|
||||
},
|
||||
/** 删除自己的动态(二次确认),成功后返回列表 */
|
||||
confirmDeleteFeed(feed) {
|
||||
uni.showModal({
|
||||
title: '删除动态',
|
||||
content: '确定要删除这条动态吗?删除后不可恢复',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#FF6B6B',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await DeleteFeed({ feedId: feed.id })
|
||||
uni.showToast({ title: '已删除', icon: 'none' })
|
||||
setTimeout(() => uni.navigateBack(), 600)
|
||||
} catch (e) {
|
||||
// httpRequest 已全局弹错误提示(如非本人动态后端会拒绝)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
async submitComment() {
|
||||
const text = this.commentText.trim()
|
||||
if (!text) return
|
||||
@@ -128,6 +186,23 @@ export default {
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
const feed = this.shareFeed || this.feed
|
||||
if (feed && feed.id) {
|
||||
const text = (feed.text || '').replace(/\s+/g, ' ').trim()
|
||||
const title = text
|
||||
? `${feed.nickname || '酒友'}:${text.length > 24 ? text.slice(0, 24) + '…' : text}`
|
||||
: `${feed.nickname || '酒友'}的动态 - 碰盏日记`
|
||||
return {
|
||||
title,
|
||||
path: `/pages/circle-detail/circle-detail?id=${feed.id}&nick=${encodeURIComponent(feed.nickname || '')}&text=${encodeURIComponent(text.slice(0, 60))}&time=${encodeURIComponent(feed.time || '')}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: '酒友圈 - 碰盏日记',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+223
-8
@@ -41,8 +41,11 @@
|
||||
v-for="feed in feeds"
|
||||
:key="feed.id"
|
||||
:feed="feed"
|
||||
:can-delete="isMyFeed(feed)"
|
||||
@like="handleLike"
|
||||
@comment="goDetail"
|
||||
@share="handleShare"
|
||||
@delete="confirmDeleteFeed"
|
||||
@item-click="goDetail"
|
||||
/>
|
||||
<view v-if="loading" class="circle-loading">
|
||||
@@ -71,12 +74,23 @@
|
||||
<text class="request-text">{{ friendRequests.length }} 条新的好友请求</text>
|
||||
<text class="request-arrow">›</text>
|
||||
</view>
|
||||
<!-- 酒友管理入口(有好友时也可进入:处理请求/删除酒友) -->
|
||||
<view v-if="friends.length && !friendRequests.length" class="friends-entry" @click="goFriends">
|
||||
<text class="friends-entry-icon">👥</text>
|
||||
<text class="friends-entry-text">管理酒友 · 处理好友请求</text>
|
||||
<text class="friends-entry-arrow">›</text>
|
||||
</view>
|
||||
<FriendItem
|
||||
v-for="f in friends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
@item-click="goChat(f)"
|
||||
/>
|
||||
<!-- 有好友时也保留邀请入口,直接触发带邀请码的转发 -->
|
||||
<button v-if="friends.length" class="invite-btn" open-type="share" @click="prepareInviteShare">
|
||||
<text class="invite-btn-icon">👥</text>
|
||||
<text class="invite-btn-text">邀请更多酒友</text>
|
||||
</button>
|
||||
<EmptyState
|
||||
v-if="!friends.length"
|
||||
icon="👥"
|
||||
@@ -120,8 +134,9 @@ import FeedCard from '../../components/FeedCard.vue'
|
||||
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 { GetCircleFeeds, LikeFeed, UnlikeFeed, DeleteFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api'
|
||||
import wsManager from '../../common/websocket'
|
||||
import { getMyInviteCode } from '../../common/invite'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -147,7 +162,15 @@ export default {
|
||||
// 酒局
|
||||
events: [],
|
||||
// 私信未读
|
||||
unreadTotal: 0
|
||||
unreadTotal: 0,
|
||||
// 当前分享的动态
|
||||
shareFeed: null,
|
||||
// 分享模式:feed(分享动态) / invite(邀请好友) / default
|
||||
shareMode: 'default',
|
||||
// 我的邀请码(分享时携带,一次性)
|
||||
inviteCode: '',
|
||||
// 当前登录用户ID(用于判断动态是否为自己发布)
|
||||
myUserId: ''
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -155,16 +178,56 @@ export default {
|
||||
this.loadFriends()
|
||||
this.loadEvents()
|
||||
this.loadUnread()
|
||||
this._wsHandler = () => { this.unreadTotal++ }
|
||||
wsManager.on('message', this._wsHandler)
|
||||
this.bindWsEvents()
|
||||
// 读取当前用户ID(删除自己的动态时用)
|
||||
try {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
this.myUserId = user.id !== undefined && user.id !== null ? String(user.id) : ''
|
||||
} catch (e) { this.myUserId = '' }
|
||||
// 预生成邀请码(仅首次/用完后生成,避免每次进页都创建无效邀请)
|
||||
this.ensureInviteCode()
|
||||
// 页面显示时确保 WS 已连接(登录后/断线后兼容)
|
||||
wsManager.connect()
|
||||
},
|
||||
onHide() {
|
||||
if (this._wsHandler) {
|
||||
wsManager.off('message', this._wsHandler)
|
||||
this._wsHandler = null
|
||||
}
|
||||
this.unbindWsEvents()
|
||||
},
|
||||
onUnload() {
|
||||
this.unbindWsEvents()
|
||||
},
|
||||
methods: {
|
||||
// === WebSocket 事件 ===
|
||||
bindWsEvents() {
|
||||
if (this._wsBound) return
|
||||
this._wsBound = true
|
||||
this._wsMsgHandler = this.onWsMessage
|
||||
this._wsConnectHandler = this.onWsConnect
|
||||
wsManager.on('message', this._wsMsgHandler)
|
||||
wsManager.on('connect', this._wsConnectHandler)
|
||||
},
|
||||
unbindWsEvents() {
|
||||
if (!this._wsBound) return
|
||||
this._wsBound = false
|
||||
if (this._wsMsgHandler) {
|
||||
wsManager.off('message', this._wsMsgHandler)
|
||||
this._wsMsgHandler = null
|
||||
}
|
||||
if (this._wsConnectHandler) {
|
||||
wsManager.off('connect', this._wsConnectHandler)
|
||||
this._wsConnectHandler = null
|
||||
}
|
||||
},
|
||||
/** 收到新私信:未读角标实时 +1(自己发的/自己读的不计数) */
|
||||
onWsMessage(data) {
|
||||
if (!data) return
|
||||
const senderId = data.senderId
|
||||
if (senderId && wsManager.userId && String(senderId) === String(wsManager.userId)) return
|
||||
this.unreadTotal++
|
||||
},
|
||||
/** (重)连成功后拉取真实未读数,修正断线期间的漏计/多计 */
|
||||
onWsConnect() {
|
||||
this.loadUnread()
|
||||
},
|
||||
switchTab(i) {
|
||||
this.currentTab = i
|
||||
},
|
||||
@@ -218,9 +281,49 @@ export default {
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
/** 判断是否为自己发布的动态(仅自己可删) */
|
||||
isMyFeed(feed) {
|
||||
if (!this.myUserId) return false
|
||||
return feed && feed.userId !== undefined && feed.userId !== null && String(feed.userId) === this.myUserId
|
||||
},
|
||||
/** 删除自己的动态(二次确认) */
|
||||
confirmDeleteFeed(feed) {
|
||||
uni.showModal({
|
||||
title: '删除动态',
|
||||
content: '确定要删除这条动态吗?删除后不可恢复',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#FF6B6B',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
try {
|
||||
await DeleteFeed({ feedId: feed.id })
|
||||
this.feeds = this.feeds.filter(f => String(f.id) !== String(feed.id))
|
||||
uni.showToast({ title: '已删除', icon: 'none' })
|
||||
} catch (e) {
|
||||
// httpRequest 已全局弹错误提示(如非本人动态后端会拒绝)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
goDetail(feed) {
|
||||
uni.navigateTo({ url: `/pages/circle-detail/circle-detail?id=${feed.id}` })
|
||||
},
|
||||
/** 记录当前要分享的动态,供 onShareAppMessage 读取 */
|
||||
handleShare(feed) {
|
||||
this.shareMode = 'feed'
|
||||
this.shareFeed = feed
|
||||
},
|
||||
/** 点击邀请按钮:标记本次分享为邀请模式 */
|
||||
prepareInviteShare() {
|
||||
this.shareMode = 'invite'
|
||||
},
|
||||
/** 确保邀请码已生成(仅缺失时拉取,避免频繁创建一次性邀请) */
|
||||
ensureInviteCode() {
|
||||
if (this.inviteCode) return
|
||||
getMyInviteCode().then(code => {
|
||||
if (code) this.inviteCode = code
|
||||
})
|
||||
},
|
||||
goPublish() {
|
||||
uni.navigateTo({ url: '/pages/circle-publish/circle-publish' })
|
||||
},
|
||||
@@ -255,7 +358,9 @@ export default {
|
||||
async loadUnread() {
|
||||
try {
|
||||
const res = await GetUnreadCount()
|
||||
if (res.data && typeof res.data.total === 'number') {
|
||||
this.unreadTotal = res.data.total
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
// === 酒局 ===
|
||||
@@ -281,6 +386,49 @@ export default {
|
||||
this.goPublish()
|
||||
}
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
// 邀请好友分享:链接携带一次性邀请码,好友点开后自动接受邀请
|
||||
if (this.shareMode === 'invite') {
|
||||
let user = {}
|
||||
try {
|
||||
user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
} catch (e) { /* ignore */ }
|
||||
const parts = []
|
||||
if (this.inviteCode) parts.push(`inviteCode=${encodeURIComponent(this.inviteCode)}`)
|
||||
if (user.nickname) parts.push(`inviterNick=${encodeURIComponent(user.nickname)}`)
|
||||
const query = parts.length ? `?${parts.join('&')}` : ''
|
||||
const nick = user.nickname ? `「${user.nickname}」` : ''
|
||||
// 本次分享已用掉当前邀请码,异步生成新码供下次使用
|
||||
this.inviteCode = ''
|
||||
this.ensureInviteCode()
|
||||
this.shareMode = 'default'
|
||||
return {
|
||||
title: `${nick}邀请你成为酒友,一起记录饮酒生活 🍻`,
|
||||
path: `/pages/index/index${query}`
|
||||
}
|
||||
}
|
||||
const feed = this.shareFeed
|
||||
if (this.shareMode === 'feed' && feed && feed.id) {
|
||||
this.shareMode = 'default'
|
||||
const text = (feed.text || '').replace(/\s+/g, ' ').trim()
|
||||
const title = text
|
||||
? `${feed.nickname || '酒友'}:${text.length > 24 ? text.slice(0, 24) + '…' : text}`
|
||||
: `${feed.nickname || '酒友'}的动态 - 碰盏日记`
|
||||
return {
|
||||
title,
|
||||
path: `/pages/circle-detail/circle-detail?id=${feed.id}&nick=${encodeURIComponent(feed.nickname || '')}&text=${encodeURIComponent(text.slice(0, 60))}&time=${encodeURIComponent(feed.time || '')}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: '酒友圈 - 碰盏日记',
|
||||
path: '/pages/index/index'
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: '酒友圈 - 碰盏日记'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -443,6 +591,73 @@ export default {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 酒友管理入口 */
|
||||
.friends-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-sm;
|
||||
padding: $sp-md $sp-lg;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $sp-md;
|
||||
|
||||
&:active {
|
||||
background: $bg-card-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.friends-entry-icon {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.friends-entry-text {
|
||||
flex: 1;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
font-weight: $fw-medium;
|
||||
}
|
||||
|
||||
.friends-entry-arrow {
|
||||
font-size: $fs-xl;
|
||||
color: $text-tertiary;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 邀请更多酒友按钮(重置 button 默认样式) */
|
||||
.invite-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $sp-sm;
|
||||
width: 100%;
|
||||
margin: $sp-md 0 0;
|
||||
padding: $sp-lg 0;
|
||||
background: rgba(232,168,56,0.08);
|
||||
border: 1rpx dashed rgba(232,168,56,0.35);
|
||||
border-radius: $radius-lg;
|
||||
line-height: 1;
|
||||
font-size: inherit;
|
||||
|
||||
&:active {
|
||||
background: rgba(232,168,56,0.14);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-btn-icon {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.invite-btn-text {
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
/* FAB */
|
||||
.fab {
|
||||
position: fixed;
|
||||
|
||||
@@ -64,6 +64,11 @@
|
||||
</view>
|
||||
</template>
|
||||
</FriendItem>
|
||||
<!-- 有好友时也保留邀请入口,直接触发带邀请码的转发 -->
|
||||
<button v-if="!keyword" class="invite-btn" open-type="share">
|
||||
<text class="invite-btn-icon">👥</text>
|
||||
<text class="invite-btn-text">邀请更多酒友</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
@@ -90,6 +95,7 @@
|
||||
import FriendItem from '../../components/FriendItem.vue'
|
||||
import EmptyState from '../../components/EmptyState.vue'
|
||||
import { GetFriends, GetFriendRequests, AcceptFriendRequest, RemoveFriend } from '../../common/api'
|
||||
import { savePendingInvite, handlePendingInvite, getMyInviteCode } from '../../common/invite'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -102,13 +108,30 @@ export default {
|
||||
keyword: '',
|
||||
friends: [],
|
||||
requests: [],
|
||||
searchTimer: null
|
||||
searchTimer: null,
|
||||
// 我的邀请码(分享时携带,页面加载时预生成)
|
||||
inviteCode: ''
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
this.loadData()
|
||||
// 被分享进入本页时,若携带邀请码参数,继续处理邀请链路
|
||||
if (options && options.inviteCode) {
|
||||
savePendingInvite(options.inviteCode, options.inviterNick)
|
||||
if (uni.getStorageSync('is_logged_in') === 'true') {
|
||||
handlePendingInvite().then(() => this.loadData())
|
||||
}
|
||||
}
|
||||
// 预生成新的邀请码,供右上角分享使用(后端每次邀请都生成新码)
|
||||
this.refreshInviteCode()
|
||||
},
|
||||
methods: {
|
||||
/** 生成新的邀请码(每次分享后调用,保证下次分享用新码) */
|
||||
refreshInviteCode() {
|
||||
getMyInviteCode().then(code => {
|
||||
this.inviteCode = code
|
||||
})
|
||||
},
|
||||
goBack() {
|
||||
uni.navigateBack()
|
||||
},
|
||||
@@ -173,9 +196,21 @@ export default {
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
// 分享链接携带当前邀请码(一次性),好友点开后接受邀请即可成为酒友
|
||||
let user = {}
|
||||
try {
|
||||
user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
} catch (e) { /* ignore */ }
|
||||
const parts = []
|
||||
if (this.inviteCode) parts.push(`inviteCode=${encodeURIComponent(this.inviteCode)}`)
|
||||
if (user.nickname) parts.push(`inviterNick=${encodeURIComponent(user.nickname)}`)
|
||||
const query = parts.length ? `?${parts.join('&')}` : ''
|
||||
const nick = user.nickname ? `「${user.nickname}」` : ''
|
||||
// 本次分享已用掉当前邀请码,异步生成新码供下次分享
|
||||
this.refreshInviteCode()
|
||||
return {
|
||||
title: '来「碰盏日记」一起记录饮酒生活吧!',
|
||||
path: '/pages/index/index'
|
||||
title: `${nick}邀请你成为酒友,一起记录饮酒生活 🍻`,
|
||||
path: `/pages/index/index${query}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,4 +410,38 @@ export default {
|
||||
font-size: $fs-xs;
|
||||
color: $coral;
|
||||
}
|
||||
|
||||
/* 邀请更多酒友按钮(重置 button 默认样式) */
|
||||
.invite-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $sp-sm;
|
||||
width: 100%;
|
||||
margin: $sp-lg 0 0;
|
||||
padding: $sp-lg 0;
|
||||
background: rgba(232,168,56,0.08);
|
||||
border: 1rpx dashed rgba(232,168,56,0.35);
|
||||
border-radius: $radius-lg;
|
||||
line-height: 1;
|
||||
font-size: inherit;
|
||||
|
||||
&:active {
|
||||
background: rgba(232,168,56,0.14);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-btn-icon {
|
||||
font-size: $fs-base;
|
||||
}
|
||||
|
||||
.invite-btn-text {
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -134,6 +134,7 @@ import client from '../../common/api'
|
||||
import { GetCalendarReq, DeleteRecordReq, CreateRecordReq, GetRecordDetailReq } from 'HaveADrink'
|
||||
import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath } from '../../common/utils'
|
||||
import { FEELINGS, DRINK_CATEGORIES, DRINK_UNITS } from '../../common/constants'
|
||||
import { savePendingInvite, handlePendingInvite } from '../../common/invite'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -170,6 +171,15 @@ export default {
|
||||
return this.selectedDate === formatDate(new Date(), 'YYYY-MM-DD')
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
// 分享邀请落地:携带 inviteCode 时暂存邀请,已登录则立即接受邀请
|
||||
if (options && options.inviteCode) {
|
||||
savePendingInvite(options.inviteCode, options.inviterNick)
|
||||
if (uni.getStorageSync('is_logged_in') === 'true') {
|
||||
handlePendingInvite()
|
||||
}
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
// 首次启动跳转引导页
|
||||
const isFirst = uni.getStorageSync('is_first_launch')
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
<script>
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
import client, { saveAuthTokens } from '../../common/api'
|
||||
import { handlePendingInvite } from '../../common/invite'
|
||||
import wsManager from '../../common/websocket'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
@@ -205,6 +207,15 @@ export default {
|
||||
finishLogin() {
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
// 登录完成后用新 token 重建 WebSocket 连接
|
||||
// (启动时可能用旧 token 连接被拒,不重建会一直用旧 token 重试)
|
||||
const token = uni.getStorageSync('auth_token')
|
||||
if (token) {
|
||||
wsManager.disconnect()
|
||||
wsManager.connect(token)
|
||||
}
|
||||
// 登录完成后补发分享邀请的好友请求(若有暂存的邀请人)
|
||||
handlePendingInvite()
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连续打卡卡片 -->
|
||||
<!-- 连续打卡卡片(含连续戒酒状态,暂时注释隐藏)
|
||||
<view class="streak-section">
|
||||
<view class="streak-card" :class="stats.streakType === 'drank' ? 'streak-drank' : 'streak-abstain'">
|
||||
<view class="streak-left">
|
||||
@@ -89,6 +89,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
-->
|
||||
|
||||
<!-- 饮酒人格卡片 -->
|
||||
<view class="persona-card">
|
||||
|
||||
Reference in New Issue
Block a user