Compare commits

...
2 Commits
Author SHA1 Message Date
cg 9f23ccae46 Merge branch 'main' of https://git.wash-painting.cn/cg/hejiu 2026-08-04 23:32:52 +08:00
cg 57eef74eb1 feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本
- 集成邀请码功能,新增common/invite.js处理邀请链路
- 实现发送好友请求返回邀请码的新流程
- 添加删除动态功能,新增DeleteFeed接口
- 集成UniAppWebSocket适配器,重构WebSocket连接管理
- 优化好友请求支持关键词搜索功能
- 在FeedCard组件中添加删除按钮和分享按钮
- 更新SDK类型定义文件以匹配新接口规范
2026-08-04 23:32:45 +08:00
19 changed files with 1460 additions and 223 deletions
+15 -8
View File
@@ -2,11 +2,12 @@
* 基于 HaveADrink SDK 封装后端接口 * 基于 HaveADrink SDK 封装后端接口
*/ */
import HaveADrink from 'HaveADrink' 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 内部调用) // HTTP 请求函数(供 SDK 内部调用)
@@ -323,6 +324,12 @@ export async function UnlikeFeed({ feedId }) {
return { data: res } return { data: res }
} }
/** 删除动态(仅本人可删,后端会校验归属) */
export async function DeleteFeed({ feedId }) {
const res = await client.DeleteFeed({ id: feedId })
return { data: res }
}
/** 获取动态评论 */ /** 获取动态评论 */
export async function GetFeedComments({ feedId }) { export async function GetFeedComments({ feedId }) {
const res = await client.GetFeedComments({ id: feedId }) const res = await client.GetFeedComments({ id: feedId })
@@ -347,19 +354,19 @@ export async function GetFriends({ keyword = '' } = {}) {
return { data: res } return { data: res }
} }
/** 获取好友请求 */ /** 获取好友请求(支持关键词搜索) */
export async function GetFriendRequests() { export async function GetFriendRequests({ keyword = '' } = {}) {
const res = await client.GetFriendRequests({}) const res = await client.GetFriendRequests({ keyword })
return { data: res } return { data: res }
} }
/** 发送好友请求 */ /** 发送好友请求(创建邀请,SDK v1.0.12:无需 userId,响应返回 inviteCode 邀请码) */
export async function SendFriendRequest({ userId, message = '' }) { export async function SendFriendRequest({ message = '' } = {}) {
const res = await client.SendFriendRequest({ userId, message }) const res = await client.SendFriendRequest({ message })
return { data: res } return { data: res }
} }
/** 接受好友请求 */ /** 接受好友请求(id 为好友请求ID,即邀请码 inviteCode */
export async function AcceptFriendRequest({ requestId }) { export async function AcceptFriendRequest({ requestId }) {
const res = await client.AcceptFriendRequest({ id: requestId }) const res = await client.AcceptFriendRequest({ id: requestId })
return { data: res } return { data: res }
+86
View File
@@ -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
}
}
+156
View File
@@ -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) => {
// 诊断:打印关闭码/原因,用于判断是服务端拒连还是网络问题
// 常见 code1006 异常断开、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
View File
@@ -1,10 +1,34 @@
/* 碰盏日记 - WebSocket 管理器 /* 碰盏日记 - WebSocket 管理器
* 单例模式,负责私信实时通信 * 单例模式,负责私信实时通信
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data } * 基于 HaveADrink SDK ChatWebSocketChatWebSocketConn)实现:
* 支持心跳、自动重连、消息队列 * - 底层 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 -> wssquery 键值对拼接
* @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 枚举值一致) // 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
const ClientMesgType = { const ClientMesgType = {
@@ -14,42 +38,26 @@ const ClientMesgType = {
Ping: 'ping' Ping: 'ping'
} }
// 服务端消息类型(与 SDK ServerMesgType 枚举值一致)
const ServerMesgType = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ack: 'ack',
Pong: 'pong',
Connected: 'connected',
Kicked: 'kicked'
}
class WebSocketManager { class WebSocketManager {
constructor() { constructor() {
this.socketTask = null this.conn = null // SDK ChatWebSocketConn 实例
this.isConnected = false this.isConnected = false
this.isConnecting = false this.isConnecting = false
this.listeners = {} // 事件订阅 { event: [callbacks] } this.listeners = {} // 事件订阅 { event: [callbacks] }
this.messageQueue = [] // 断线期间暂存消息 this.messageQueue = [] // 断线期间暂存消息 { mesgType, data }
this.heartbeatTimer = null this.heartbeatTimer = null
this.heartbeatTimeout = null
this.reconnectTimer = null
this.reconnectAttempts = 0
this.maxReconnectDelay = 30000
this.heartbeatInterval = 30000 // 30s 发一次 ping this.heartbeatInterval = 30000 // 30s 发一次 ping
this.heartbeatWait = 60000 // 60s 无 pong 则重连
this.manualClose = false this.manualClose = false
this.token = '' this.token = ''
this.userId = '' // 连接成功后服务端返回的用户ID this.userId = '' // 服务端 connected 事件返回的用户ID
} }
/** /**
* 建立 WebSocket 连接 * 建立 WebSocket 连接(走 SDK ChatWebSocket
* @param {string} token - 用户认证 token * @param {string} token - 用户认证 token
*/ */
connect(token) { connect(token) {
if (this.isConnected || this.isConnecting) return if (this.conn || this.isConnecting) return
this.token = token || uni.getStorageSync('auth_token') || '' this.token = token || uni.getStorageSync('auth_token') || ''
if (!this.token) { if (!this.token) {
console.warn('[WS] 无 token,跳过连接') console.warn('[WS] 无 token,跳过连接')
@@ -57,65 +65,80 @@ class WebSocketManager {
} }
this.manualClose = false this.manualClose = false
this.isConnecting = true this.isConnecting = true
console.log('[WS] 正在连接(SDK ChatWebSocket...')
const url = WS_HOST // SDK 内部:new websocket(`${host}/api/have_a_drink/v1/chat/chat?token=xxx`)
console.log('[WS] 正在连接...') // websocket 即注入的 UniAppWebSocket 适配器,重连由 SDK 指数退避管理
// 注意:不用 client.ChatWebSocket()(其内部 new URL 在小程序沙箱不可用),
// 直接用 SDK 导出的 ChatWebSocketConn 构造,等价实现
const conn = createChatConn({ token: this.token })
this.socketTask = uni.connectSocket({ conn.onopen = () => {
url,
header: {
'Authorization': `Bearer ${this.token}`
},
complete: () => {}
})
this.socketTask.onOpen(() => {
console.log('[WS] 连接成功') console.log('[WS] 连接成功')
this.isConnected = true this.isConnected = true
this.isConnecting = false this.isConnecting = false
this.reconnectAttempts = 0
this._startHeartbeat() this._startHeartbeat()
this._flushQueue() this._flushQueue()
this._emit('connect', {}) 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] 连接关闭') console.log('[WS] 连接关闭')
this.isConnected = false this.isConnected = false
this.isConnecting = false this.isConnecting = false
this._stopHeartbeat() this._stopHeartbeat()
this._emit('disconnect', {}) this._emit('disconnect', {})
if (!this.manualClose) { // 非主动关闭时,SDK 已自动安排指数退避重连,无需手动处理
this._scheduleReconnect()
} }
})
this.socketTask.onError((err) => { conn.onerror = (err) => {
console.warn('[WS] 连接错误', err) console.warn('[WS] 连接错误', err)
this.isConnected = false
this.isConnecting = 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() { disconnect() {
this.manualClose = true this.manualClose = true
this._stopHeartbeat() this._stopHeartbeat()
this._clearReconnect() if (this.conn) {
if (this.socketTask) { this.conn.close()
this.socketTask.close() this.conn = null
this.socketTask = null
} }
this.isConnected = false this.isConnected = false
this.isConnecting = false
} }
/** /**
@@ -124,17 +147,16 @@ class WebSocketManager {
* @param {object} data - 消息数据 * @param {object} data - 消息数据
*/ */
send(mesgType, data = {}) { send(mesgType, data = {}) {
const payload = JSON.stringify({ mesgType, data }) if (this.isConnected && this.conn) {
if (this.isConnected && this.socketTask) { this._write(mesgType, data)
this.socketTask.send({ data: payload })
} else { } else {
// 断线暂存队列(ping 不暂存) // 断线暂存队列(ping 不暂存)
if (mesgType !== ClientMesgType.Ping) { if (mesgType !== ClientMesgType.Ping) {
this.messageQueue.push(payload) this.messageQueue.push({ mesgType, data })
} }
// 尝试重连 // 未连接时触发(重)连接
if (!this.isConnecting && !this.manualClose) { if (!this.conn && !this.manualClose) {
this._scheduleReconnect() this.connect(this.token)
} }
} }
} }
@@ -167,38 +189,31 @@ class WebSocketManager {
// ========== 内部方法 ========== // ========== 内部方法 ==========
/** 处理收到的消息(SDK ServerMesgType 协议) */ /** 通过 SDK writeClient* 方法写入消息 */
_handleMessage(msg) { _write(mesgType, data) {
const { mesgType, data } = msg try {
switch (mesgType) { switch (mesgType) {
case ServerMesgType.Chat: case ClientMesgType.Chat:
this._emit('message', data) this.conn.writeClientChatData(data)
break break
case ServerMesgType.Typing: case ClientMesgType.Typing:
this._emit('typing', data) this.conn.writeClientTypingData(data)
break break
case ServerMesgType.Read: case ClientMesgType.Read:
this._emit('read', data) this.conn.writeClientReadData(data)
break break
case ServerMesgType.Ack: case ClientMesgType.Ping:
this._emit('ack', data) this.conn.writeClientPingData(data)
break
case ServerMesgType.Pong:
this._onPong()
break
case ServerMesgType.Connected:
// 服务端验证 token 成功后推送,携带 userId
this.userId = data && data.userId ? data.userId : ''
console.log('[WS] 服务端确认连接, userId:', this.userId)
this._emit('connected', data)
break
case ServerMesgType.Kicked:
console.warn('[WS] 被踢下线')
this._emit('kicked', data)
this.disconnect()
break break
default: 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() { _startHeartbeat() {
this._stopHeartbeat() this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => { this.heartbeatTimer = setInterval(() => {
this.send(ClientMesgType.Ping, {}) this.send(ClientMesgType.Ping, {})
// 设置超时检测
this.heartbeatTimeout = setTimeout(() => {
console.warn('[WS] 心跳超时,触发重连')
if (this.socketTask) {
this.socketTask.close()
}
}, this.heartbeatWait)
}, this.heartbeatInterval) }, this.heartbeatInterval)
} }
/** 收到 pong,清除超时 */
_onPong() {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout)
this.heartbeatTimeout = null
}
}
/** 停止心跳 */ /** 停止心跳 */
_stopHeartbeat() { _stopHeartbeat() {
if (this.heartbeatTimer) { if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer) clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null 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} 条暂存消息`) console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
const queue = [...this.messageQueue] const queue = [...this.messageQueue]
this.messageQueue = [] this.messageQueue = []
queue.forEach(payload => { queue.forEach(({ mesgType, data }) => this._write(mesgType, data))
if (this.socketTask && this.isConnected) {
this.socketTask.send({ data: payload })
}
})
} }
} }
+35 -4
View File
@@ -53,8 +53,13 @@
<text class="feed-action-icon">💬</text> <text class="feed-action-icon">💬</text>
<text class="feed-action-num">{{ feed.comments || '' }}</text> <text class="feed-action-num">{{ feed.comments || '' }}</text>
</view> </view>
<view class="feed-action" @click.stop="$emit('share', feed)"> <button class="feed-action share-btn" open-type="share" @click.stop="$emit('share', feed)">
<text class="feed-action-icon"></text> <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> </view>
</view> </view>
@@ -66,9 +71,11 @@ import { getCatIcon, isIconPath } from '../common/utils'
export default { export default {
name: 'FeedCard', name: 'FeedCard',
props: { 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: { methods: {
getCatIcon, getCatIcon,
isIconPath, isIconPath,
@@ -228,6 +235,16 @@ export default {
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08)); 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 { .feed-action {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -251,4 +268,18 @@ export default {
font-size: $fs-sm; font-size: $fs-sm;
color: $text-tertiary; 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> </style>
+3 -3
View File
@@ -4,9 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/HaveADrink": { "node_modules/HaveADrink": {
"version": "1.0.8", "version": "1.0.12",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz", "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==", "integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
"license": "ISC" "license": "ISC"
} }
} }
+99 -2
View File
@@ -14,7 +14,7 @@
* 6. 社交模块 - 朋友圈动态、点赞 * 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` <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` <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(...) 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`|| 消息| |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(...) 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
View File
@@ -17,6 +17,15 @@ export declare interface option {
text: string; 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; 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 { export declare class GetFriendRequestsReq {
[key: string]: any; [key: string]: any;
/**
*
*/
keyword: string;
/** /**
* @param keyword string
*/ */
constructor(); constructor(keyword: string,);
/** /**
* GetFriendRequestsReq * GetFriendRequestsReq
* *
* @param o Object * @param o Object
* - keyword: string, // 搜索关键词
*/ */
static fromObject(o: Object): GetFriendRequestsReq; static fromObject(o: Object): GetFriendRequestsReq;
} }
@@ -3405,24 +3466,18 @@ export declare class FriendRequestListData {
export declare class SendFriendRequestReq { export declare class SendFriendRequestReq {
[key: string]: any; [key: string]: any;
/** /**
* ID
*/
userId: string|number;
/**
* *
*/ */
message: string; message: string;
/** /**
* @param userId string|number ID
* @param message string * @param message string
*/ */
constructor(userId: string|number,message: string,); constructor(message: string,);
/** /**
* SendFriendRequestReq * SendFriendRequestReq
* *
* @param o Object * @param o Object
* - userId: string|number, // 酒友ID
* - message: string, // 消息 * - message: string, // 消息
*/ */
static fromObject(o: Object): SendFriendRequestReq; static fromObject(o: Object): SendFriendRequestReq;
@@ -3433,14 +3488,20 @@ export declare class SendFriendRequestReq {
*/ */
export declare class SendFriendRequestResp { export declare class SendFriendRequestResp {
[key: string]: any; [key: string]: any;
/**
* ID
*/
inviteCode: string|number;
/** /**
* @param inviteCode string|number ID
*/ */
constructor(); constructor(inviteCode: string|number,);
/** /**
* SendFriendRequestResp * SendFriendRequestResp
* *
* @param o Object * @param o Object
* - inviteCode: string|number, // 好友请求ID
*/ */
static fromObject(o: Object): SendFriendRequestResp; static fromObject(o: Object): SendFriendRequestResp;
} }
@@ -3788,6 +3849,134 @@ export declare class CreateEventResp {
static fromObject(o: Object): 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; 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 ClientMessage = ClientChatData | ClientTypingData | ClientReadData | ClientPingData | null
export type ServerMessage = ServerChatData | ServerTypingData | ServerReadData | ServerAckData | ServerPongData | ServerConnectedData | ServerKickedData | null export type ServerMessage = ServerChatData | ServerTypingData | ServerReadData | ServerAckData | ServerPongData | ServerConnectedData | ServerKickedData | null
@@ -4645,6 +4857,12 @@ export class ChatWebSocketConn {
public close(): void; 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 * * - API
* uni-app * uni-app
@@ -4660,9 +4878,9 @@ export class ChatWebSocketConn {
*/ */
export default class HaveADrink { export default class HaveADrink {
host: string; host: string;
http_request: any; http_request: (url: string, params: any) => Promise<any>;
upload: any; upload: (url: string, params: any) => Promise<any>;
constructor(conf: any); constructor(conf: Config);
/** /**
* token * token
@@ -4682,6 +4900,7 @@ export default class HaveADrink {
/** /**
* *
* *
* @deprecated 使
* @param req WechatGetPhoneNumberReq * @param req WechatGetPhoneNumberReq
* @return WechatGetPhoneNumberResp * @return WechatGetPhoneNumberResp
*/ */
@@ -4706,6 +4925,7 @@ export default class HaveADrink {
/** /**
* *
* *
* @deprecated 使
* @param req GetFeedReq * @param req GetFeedReq
* @return GetFeedResp * @return GetFeedResp
*/ */
@@ -4831,6 +5051,14 @@ export default class HaveADrink {
*/ */
public PublishFeed(req: PublishFeedReq) : Promise<PublishFeedResp>; 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>; 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 * @param req ChatWebSocket
* @return ChatWebSocketConn * @return ChatWebSocketConn
*/ */
public ChatWebSocket() : ChatWebSocketConn; public ChatWebSocket(input: ChatWebSocketReq) : ChatWebSocketConn;
} }
+250 -12
View File
@@ -13,6 +13,8 @@
* 6. 社交模块 - 朋友圈动态点赞 * 6. 社交模块 - 朋友圈动态点赞
*/ */
let websocket = WebSocket;
export class Enum { export class Enum {
constructor(value) { constructor(value) {
this.v = 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 { export class GetFriendRequestsReq {
/** /**
* @param keyword: string 搜索关键词
*/ */
constructor() { constructor(keyword,) {
this.keyword = keyword;
} }
static fromObject(o) { static fromObject(o) {
return new GetFriendRequestsReq(); return new GetFriendRequestsReq(o.keyword,);
} }
} }
@@ -2336,16 +2372,14 @@ export class FriendRequestListData {
*/ */
export class SendFriendRequestReq { export class SendFriendRequestReq {
/** /**
* @param userId: string|number 酒友ID
* @param message: string 消息 * @param message: string 消息
*/ */
constructor(userId,message,) { constructor(message,) {
this.userId = userId;
this.message = message; this.message = message;
} }
static fromObject(o) { 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 { export class SendFriendRequestResp {
/** /**
* @param inviteCode: string|number 好友请求ID
*/ */
constructor() { constructor(inviteCode,) {
this.inviteCode = inviteCode;
} }
static fromObject(o) { 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 { export class ChatWebSocketConn {
/** /**
* 构造函数 * 构造函数
* *
* @param host string 主机名 * @param host string 主机名
*/ */
constructor(host) { constructor(host, query) {
this.host = host; this.host = host;
this.reconnect = true; this.reconnect = true;
this.reconnectDelay = 1000; this.reconnectDelay = 1000;
this.maxReconnectDelay = 5000; this.maxReconnectDelay = 5000;
this.randomizationFactor = 0.5; this.randomizationFactor = 0.5;
this.attempt = 0; this.attempt = 0;
this.query = query;
this.connect(); this.connect();
} }
backoff(attempt) { backoff(attempt) {
@@ -3115,7 +3244,7 @@ export class ChatWebSocketConn {
return delayWithJitter; return delayWithJitter;
} }
connect() { 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) => { socket.onopen = (event) => {
this.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 * * 喝酒了么 - 后端接口 API
* 小程序喝酒了么uni-app 微信小程序 * 小程序喝酒了么uni-app 微信小程序
@@ -3296,6 +3431,9 @@ export class ChatWebSocketConn {
*/ */
export default class HaveADrink { export default class HaveADrink {
constructor(conf) { constructor(conf) {
if (conf.websocket) {
websocket = conf.websocket;
}
this.host = conf.host; this.host = conf.host;
this.http_request = conf.http_request; this.http_request = conf.http_request;
this.upload = conf.upload; 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 事件 * 注意连接建立后服务端会先验证 token然后推送 connected 事件
*/ */
ChatWebSocket() { ChatWebSocket(input) {
const url = new URL(this.host); 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
View File
@@ -1,7 +1,7 @@
{ {
"name": "HaveADrink", "name": "HaveADrink",
"type": "module", "type": "module",
"version": "v1.0.8", "version": "v1.0.12",
"description": "喝酒了么 API 服务", "description": "喝酒了么 API 服务",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+4 -4
View File
@@ -5,13 +5,13 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"HaveADrink": "^1.0.8" "HaveADrink": "^1.0.12"
} }
}, },
"node_modules/HaveADrink": { "node_modules/HaveADrink": {
"version": "1.0.8", "version": "1.0.12",
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.8.tgz", "resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
"integrity": "sha512-YOaqEZg+T1MKz1OtRJc09niyxEwm+52g91Yj0L2aGEy6bK6pTLJYlmREdneIStiniUGHVjL2/U5Qk57lTBAuFw==", "integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
"license": "ISC" "license": "ISC"
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"dependencies": { "dependencies": {
"HaveADrink": "^1.0.8" "HaveADrink": "^1.0.12"
} }
} }
+35 -8
View File
@@ -178,20 +178,30 @@ export default {
async initChat() { async initChat() {
// conversationId // conversationId
if (!this.conversationId && this.friendId) { if (!this.conversationId && this.friendId) {
try { await this.ensureConversationId()
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.loadMessages()
this.markRead() this.markRead()
this.bindWsEvents() 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() { async loadMessages() {
// ID
if (!this.conversationId) return
try { try {
const res = await GetMessages({ conversationId: this.conversationId, lastID: this.lastID, pageSize: 20 }) const res = await GetMessages({ conversationId: this.conversationId, lastID: this.lastID, pageSize: 20 })
this.messages = res.data.list || [] this.messages = res.data.list || []
@@ -209,6 +219,8 @@ export default {
}, },
async markRead() { async markRead() {
// ID
if (!this.conversationId) return
try { try {
await MarkRead({ conversationId: this.conversationId }) await MarkRead({ conversationId: this.conversationId })
// WebSocket // WebSocket
@@ -238,6 +250,10 @@ export default {
onWsMessage(data) { onWsMessage(data) {
// //
if (String(data.senderId) !== String(this.friendId)) return if (String(data.senderId) !== String(this.friendId)) return
// conversationId
if (!this.conversationId && data.conversationId) {
this.conversationId = data.conversationId
}
const msg = { const msg = {
id: data.msgId ? String(data.msgId) : 'msg_' + Date.now(), id: data.msgId ? String(data.msgId) : 'msg_' + Date.now(),
conversationId: data.conversationId || this.conversationId, conversationId: data.conversationId || this.conversationId,
@@ -251,11 +267,13 @@ export default {
this.messages.push(msg) this.messages.push(msg)
this.peerTyping = false this.peerTyping = false
this.$nextTick(() => this.scrollToBottom()) this.$nextTick(() => this.scrollToBottom())
// // ID
if (this.conversationId) {
wsManager.send('read', { wsManager.send('read', {
conversationId: this.conversationId, conversationId: this.conversationId,
lastMsgId: msg.id lastMsgId: msg.id
}) })
}
}, },
/** 对方正在输入 */ /** 对方正在输入 */
@@ -270,6 +288,10 @@ export default {
/** 消息送达确认 */ /** 消息送达确认 */
onWsAck(data) { 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)) const msg = this.messages.find(m => m.id === data.clientMsgId || m.id === String(data.clientMsgId))
if (msg) { if (msg) {
msg.id = data.msgId ? String(data.msgId) : msg.id msg.id = data.msgId ? String(data.msgId) : msg.id
@@ -308,6 +330,11 @@ export default {
msgType: 'text', msgType: 'text',
clientMesgId: clientMsgId clientMesgId: clientMsgId
}) })
// ID
if (!this.conversationId) {
setTimeout(() => { this.ensureConversationId() }, 800)
}
}, },
/** 重发失败消息 */ /** 重发失败消息 */
+79 -4
View File
@@ -17,7 +17,10 @@
<FeedCard <FeedCard
v-if="feed" v-if="feed"
:feed="feed" :feed="feed"
:can-delete="isMyFeed(feed)"
@like="handleLike" @like="handleLike"
@share="handleShare"
@delete="confirmDeleteFeed"
/> />
<!-- 评论区 --> <!-- 评论区 -->
@@ -62,7 +65,7 @@
import FeedCard from '../../components/FeedCard.vue' import FeedCard from '../../components/FeedCard.vue'
import CommentItem from '../../components/CommentItem.vue' import CommentItem from '../../components/CommentItem.vue'
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed } from '../../common/api' import { GetFeedComments, AddComment, LikeFeed, UnlikeFeed, DeleteFeed } from '../../common/api'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -75,11 +78,16 @@ export default {
feedId: '', feedId: '',
feed: null, feed: null,
comments: [], comments: [],
commentText: '' commentText: '',
//
shareFeed: null,
//
shareOptions: {}
} }
}, },
onLoad(options) { onLoad(options) {
this.feedId = options.id || '' this.feedId = options.id || ''
this.shareOptions = options || {}
this.loadFeed() this.loadFeed()
this.loadComments() this.loadComments()
}, },
@@ -88,10 +96,27 @@ export default {
uni.navigateBack() uni.navigateBack()
}, },
async loadFeed() { async loadFeed() {
// globalData // globalData
const app = getApp() const app = getApp()
const feeds = (app.globalData && app.globalData.circleFeeds) || [] 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() { async loadComments() {
try { try {
@@ -109,6 +134,39 @@ export default {
else await UnlikeFeed({ feedId: feed.id }) else await UnlikeFeed({ feedId: feed.id })
} catch (e) { /* ignore */ } } 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() { async submitComment() {
const text = this.commentText.trim() const text = this.commentText.trim()
if (!text) return if (!text) return
@@ -128,6 +186,23 @@ export default {
uni.showToast({ title: '发送失败', icon: 'none' }) 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> </script>
+223 -8
View File
@@ -41,8 +41,11 @@
v-for="feed in feeds" v-for="feed in feeds"
:key="feed.id" :key="feed.id"
:feed="feed" :feed="feed"
:can-delete="isMyFeed(feed)"
@like="handleLike" @like="handleLike"
@comment="goDetail" @comment="goDetail"
@share="handleShare"
@delete="confirmDeleteFeed"
@item-click="goDetail" @item-click="goDetail"
/> />
<view v-if="loading" class="circle-loading"> <view v-if="loading" class="circle-loading">
@@ -71,12 +74,23 @@
<text class="request-text">{{ friendRequests.length }} 条新的好友请求</text> <text class="request-text">{{ friendRequests.length }} 条新的好友请求</text>
<text class="request-arrow"></text> <text class="request-arrow"></text>
</view> </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 <FriendItem
v-for="f in friends" v-for="f in friends"
:key="f.id" :key="f.id"
:friend="f" :friend="f"
@item-click="goChat(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 <EmptyState
v-if="!friends.length" v-if="!friends.length"
icon="👥" icon="👥"
@@ -120,8 +134,9 @@ import FeedCard from '../../components/FeedCard.vue'
import FriendItem from '../../components/FriendItem.vue' import FriendItem from '../../components/FriendItem.vue'
import EventCard from '../../components/EventCard.vue' import EventCard from '../../components/EventCard.vue'
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetCircleFeeds, LikeFeed, UnlikeFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api' import { GetCircleFeeds, LikeFeed, UnlikeFeed, DeleteFeed, GetFriends, GetFriendRequests, GetEvents, GetUnreadCount } from '../../common/api'
import wsManager from '../../common/websocket' import wsManager from '../../common/websocket'
import { getMyInviteCode } from '../../common/invite'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -147,7 +162,15 @@ export default {
// //
events: [], events: [],
// //
unreadTotal: 0 unreadTotal: 0,
//
shareFeed: null,
// feed() / invite() / default
shareMode: 'default',
//
inviteCode: '',
// ID
myUserId: ''
} }
}, },
onShow() { onShow() {
@@ -155,16 +178,56 @@ export default {
this.loadFriends() this.loadFriends()
this.loadEvents() this.loadEvents()
this.loadUnread() this.loadUnread()
this._wsHandler = () => { this.unreadTotal++ } this.bindWsEvents()
wsManager.on('message', this._wsHandler) // 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() { onHide() {
if (this._wsHandler) { this.unbindWsEvents()
wsManager.off('message', this._wsHandler) },
this._wsHandler = null onUnload() {
} this.unbindWsEvents()
}, },
methods: { 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) { switchTab(i) {
this.currentTab = i this.currentTab = i
}, },
@@ -218,9 +281,49 @@ export default {
} }
} catch (e) { /* ignore */ } } 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) { goDetail(feed) {
uni.navigateTo({ url: `/pages/circle-detail/circle-detail?id=${feed.id}` }) 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() { goPublish() {
uni.navigateTo({ url: '/pages/circle-publish/circle-publish' }) uni.navigateTo({ url: '/pages/circle-publish/circle-publish' })
}, },
@@ -255,7 +358,9 @@ export default {
async loadUnread() { async loadUnread() {
try { try {
const res = await GetUnreadCount() const res = await GetUnreadCount()
if (res.data && typeof res.data.total === 'number') {
this.unreadTotal = res.data.total this.unreadTotal = res.data.total
}
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}, },
// === === // === ===
@@ -281,6 +386,49 @@ export default {
this.goPublish() 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> </script>
@@ -443,6 +591,73 @@ export default {
opacity: 0.6; 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 */
.fab { .fab {
position: fixed; position: fixed;
+73 -4
View File
@@ -64,6 +64,11 @@
</view> </view>
</template> </template>
</FriendItem> </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> </view>
<!-- 空状态 --> <!-- 空状态 -->
@@ -90,6 +95,7 @@
import FriendItem from '../../components/FriendItem.vue' import FriendItem from '../../components/FriendItem.vue'
import EmptyState from '../../components/EmptyState.vue' import EmptyState from '../../components/EmptyState.vue'
import { GetFriends, GetFriendRequests, AcceptFriendRequest, RemoveFriend } from '../../common/api' import { GetFriends, GetFriendRequests, AcceptFriendRequest, RemoveFriend } from '../../common/api'
import { savePendingInvite, handlePendingInvite, getMyInviteCode } from '../../common/invite'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -102,13 +108,30 @@ export default {
keyword: '', keyword: '',
friends: [], friends: [],
requests: [], requests: [],
searchTimer: null searchTimer: null,
//
inviteCode: ''
} }
}, },
onLoad() { onLoad(options) {
this.loadData() 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: { methods: {
/** 生成新的邀请码(每次分享后调用,保证下次分享用新码) */
refreshInviteCode() {
getMyInviteCode().then(code => {
this.inviteCode = code
})
},
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
}, },
@@ -173,9 +196,21 @@ export default {
} }
}, },
onShareAppMessage() { 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 { return {
title: '来「碰盏日记」一起记录饮酒生活吧!', title: `${nick}邀请你成为酒友,一起记录饮酒生活 🍻`,
path: '/pages/index/index' path: `/pages/index/index${query}`
} }
} }
} }
@@ -375,4 +410,38 @@ export default {
font-size: $fs-xs; font-size: $fs-xs;
color: $coral; 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> </style>
+10
View File
@@ -134,6 +134,7 @@ import client from '../../common/api'
import { GetCalendarReq, DeleteRecordReq, CreateRecordReq, GetRecordDetailReq } from 'HaveADrink' import { GetCalendarReq, DeleteRecordReq, CreateRecordReq, GetRecordDetailReq } from 'HaveADrink'
import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath } from '../../common/utils' import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath } from '../../common/utils'
import { FEELINGS, DRINK_CATEGORIES, DRINK_UNITS } from '../../common/constants' import { FEELINGS, DRINK_CATEGORIES, DRINK_UNITS } from '../../common/constants'
import { savePendingInvite, handlePendingInvite } from '../../common/invite'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -170,6 +171,15 @@ export default {
return this.selectedDate === formatDate(new Date(), 'YYYY-MM-DD') 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() { onShow() {
// //
const isFirst = uni.getStorageSync('is_first_launch') const isFirst = uni.getStorageSync('is_first_launch')
+11
View File
@@ -91,6 +91,8 @@
<script> <script>
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
import client, { saveAuthTokens } from '../../common/api' import client, { saveAuthTokens } from '../../common/api'
import { handlePendingInvite } from '../../common/invite'
import wsManager from '../../common/websocket'
export default { export default {
mixins: [themeMixin], mixins: [themeMixin],
@@ -205,6 +207,15 @@ export default {
finishLogin() { finishLogin() {
uni.setStorageSync('is_logged_in', 'true') uni.setStorageSync('is_logged_in', 'true')
uni.setStorageSync('is_first_launch', 'false') 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' }) uni.switchTab({ url: '/pages/index/index' })
}, },
+2 -1
View File
@@ -71,7 +71,7 @@
</view> </view>
</view> </view>
<!-- 连续打卡卡片 --> <!-- 连续打卡卡片含连续戒酒状态暂时注释隐藏
<view class="streak-section"> <view class="streak-section">
<view class="streak-card" :class="stats.streakType === 'drank' ? 'streak-drank' : 'streak-abstain'"> <view class="streak-card" :class="stats.streakType === 'drank' ? 'streak-drank' : 'streak-abstain'">
<view class="streak-left"> <view class="streak-left">
@@ -89,6 +89,7 @@
</view> </view>
</view> </view>
</view> </view>
-->
<!-- 饮酒人格卡片 --> <!-- 饮酒人格卡片 -->
<view class="persona-card"> <view class="persona-card">