Compare commits

...
3 Commits
Author SHA1 Message Date
cg 527b0e8a8d feat(event): 添加酒局编辑删除功能并优化状态管理
- 移除 UniAppWebSocket 类实现,使用独立的 websocket 模块
- 添加 UpdateEvent 和 DeleteEvent API 接口
- 在 EventCard 组件中添加发起人管理操作按钮
- 实现酒局状态数字枚举到字符串的映射转换
- 在 circle 页面集成编辑删除事件处理
- 重构 event-create 页面支持编辑模式
- 在 event-detail 页面添加发起人操作区域
- 实现删除确认对话框和页面返回刷新逻辑
2026-08-05 22:58:26 +08:00
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
22 changed files with 1682 additions and 394 deletions
+29 -144
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 内部调用)
@@ -106,144 +107,9 @@ function uploadRequest(url, params) {
}) })
} }
/**
* 使用 uni-app WebSocket API 实现的 WebSocket 类
* 符合之前定义的 WebsocketInterface(无类型约束)
*/
class UniAppWebSocket {
/**
* 构造函数:传入 WebSocket 服务器地址
* @param {string} host - 完整的 WebSocket URL,例如 'wss://example.com/ws'
*/
constructor(host) {
this.host = host;
// 事件回调属性(外部可赋值)
this.close = null; // 可赋值为函数,同时也可用主动关闭连接
this.onopen = null;
this.onmessage = null;
// 内部 SocketTask 实例
this.socketTask = null;
// 初始化连接
this._initSocket();
}
/**
* 初始化 WebSocket 连接(私有方法,约定以 _ 开头)
*/
_initSocket() {
const self = this;
// 创建 SocketTask
this.socketTask = uni.connectSocket({
url: this.host,
success() {
// 连接创建成功(但尚未打开)
},
fail(err) {
// 连接创建失败,触发错误事件
const errorEvent = { type: 'error', error: err, target: self };
self.onerror(errorEvent);
}
});
// 监听打开事件
this.socketTask.onOpen((res) => {
if (self.onopen) {
// 构造一个简单的事件对象
const event = { type: 'open', target: self, ...res };
self.onopen(event);
}
});
// 监听消息事件
this.socketTask.onMessage((res) => {
if (self.onmessage) {
// res.data 是消息数据
const event = {
type: 'message',
data: res.data,
target: self
};
self.onmessage(event);
}
});
// 监听关闭事件
this.socketTask.onClose((res) => {
// 构造关闭事件
const event = {
type: 'close',
code: res.code,
reason: res.reason,
wasClean: res.wasClean,
target: self
};
// 调用 onclose 方法(可被重写)
self.onclose(event);
});
// 监听错误事件
this.socketTask.onError((err) => {
const errorEvent = { type: 'error', error: err, target: self };
self.onerror(errorEvent);
});
// 将 close 属性设置为一个可调用函数,用于主动断开连接
// 同时也满足接口的 “回调类型” 定义
this.close = function(ev) {
if (self.socketTask) {
const code = ev && ev.code ? ev.code : 1000;
const reason = ev && ev.reason ? ev.reason : '';
self.socketTask.close({ code, reason });
}
// 如果作为回调函数,返回 null 即可(无实际意义)
return null;
};
}
/**
* 关闭事件处理方法(可被外部重写)
* @param {Object} event - 关闭事件对象
*/
onclose(event) {
// 默认空实现,你可以在外部覆盖此方法
// console.log('连接已关闭', event);
}
/**
* 错误事件处理方法(可被外部重写)
* @param {Object} event - 错误事件对象
*/
onerror(event) {
// 默认空实现
// console.error('连接错误', event);
}
/**
* 发送数据
* @param {string|ArrayBuffer|Blob} data - 要发送的数据
* @throws {Error} 如果 WebSocket 未初始化
*/
send(data) {
if (!this.socketTask) {
throw new Error('WebSocket 未初始化,无法发送数据');
}
this.socketTask.send({
data: data,
success() {
// 发送成功,可添加日志
},
fail(err) {
// 发送失败,触发错误事件
const errorEvent = { type: 'error', error: err, target: this };
this.onerror(errorEvent);
}
});
}
}
// ========================================== // ==========================================
// 初始化 SDK 实例 // 初始化 SDK 实例
// WebSocket 适配器使用 common/uni-websocket.js,已修复 wx.connectSocket 降级问题)
// ========================================== // ==========================================
const client = new HaveADrink({ const client = new HaveADrink({
host: API_HOST, host: API_HOST,
@@ -323,6 +189,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 +219,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 }
@@ -390,6 +262,19 @@ export async function CreateEvent({ title, time, location, maxPeople, note, lati
return { data: res } return { data: res }
} }
/** 修改酒局(仅发起人可改,后端会校验归属) */
export async function UpdateEvent({ eventId, title, time, location, maxPeople, note, latitude, longitude }) {
const geo = (latitude != null && longitude != null) ? { latitude, longitude } : undefined
const res = await client.UpdateEvent({ id: eventId, title, time, location, maxPeople, geo, note })
return { data: res }
}
/** 删除酒局(仅发起人可删,后端会校验归属) */
export async function DeleteEvent({ eventId }) {
const res = await client.DeleteEvent({ id: eventId })
return { data: res }
}
/** 报名酒局 */ /** 报名酒局 */
export async function JoinEvent({ eventId }) { export async function JoinEvent({ eventId }) {
const res = await client.JoinEvent({ id: eventId }) const res = await client.JoinEvent({ id: eventId })
+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 })
}
})
} }
} }
+47 -6
View File
@@ -1,7 +1,7 @@
<template> <template>
<view class="event-card" @click="$emit('item-click', event)"> <view class="event-card" @click="$emit('item-click', event)">
<!-- 状态标签 --> <!-- 状态标签 -->
<view class="event-status" :class="'status-' + event.status"> <view class="event-status" :class="'status-' + statusKey">
<text class="event-status-text">{{ statusLabel }}</text> <text class="event-status-text">{{ statusLabel }}</text>
</view> </view>
@@ -32,7 +32,16 @@
</view> </view>
<text class="event-org-name">{{ event.organizer ? event.organizer.nickname : '' }}</text> <text class="event-org-name">{{ event.organizer ? event.organizer.nickname : '' }}</text>
</view> </view>
<view class="event-action" v-if="event.status === 'open'"> <!-- 发起人编辑/删除 -->
<view class="event-manage" v-if="canManage" @click.stop>
<view class="event-manage-btn" @click.stop="$emit('edit', event)">
<text class="event-manage-text"> 编辑</text>
</view>
<view class="event-manage-btn" @click.stop="$emit('delete', event)">
<text class="event-manage-text event-manage-del">🗑 删除</text>
</view>
</view>
<view class="event-action" v-else-if="statusKey === 'open' || statusKey === 'upcoming'">
<text class="event-action-text" v-if="event.isJoined"> 已报名</text> <text class="event-action-text" v-if="event.isJoined"> 已报名</text>
<text class="event-action-text event-action-join" v-else>报名</text> <text class="event-action-text event-action-join" v-else>报名</text>
</view> </view>
@@ -44,14 +53,21 @@
export default { export default {
name: 'EventCard', name: 'EventCard',
props: { props: {
event: { type: Object, default: () => ({}) } event: { type: Object, default: () => ({}) },
/** 是否为发起人(可编辑/删除) */
canManage: { type: Boolean, default: false }
}, },
emits: ['item-click'], emits: ['item-click', 'edit', 'delete'],
computed: { computed: {
/** 后端状态为数字枚举:1=报名中 2=待开始 3=进行中 4=已结束(兼容字符串返回) */
statusKey() {
const map = { 1: 'open', 2: 'upcoming', 3: 'ongoing', 4: 'ended' }
const raw = this.event.status
return map[Number(raw)] || String(raw).toLowerCase()
},
statusLabel() { statusLabel() {
const val = String(this.event.status).toLowerCase()
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' } const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中' return map[this.statusKey] || '报名中'
} }
} }
} }
@@ -182,4 +198,29 @@ export default {
background: linear-gradient(135deg, $amber, $amber-deep); background: linear-gradient(135deg, $amber, $amber-deep);
font-weight: $fw-bold; font-weight: $fw-bold;
} }
.event-manage {
display: flex;
align-items: center;
gap: $sp-md;
}
.event-manage-btn {
padding: 8rpx 20rpx;
border-radius: $radius-full;
background: $bg-elevated;
&:active {
opacity: 0.7;
}
}
.event-manage-text {
font-size: $fs-sm;
color: $text-secondary;
}
.event-manage-del {
color: $coral;
}
</style> </style>
+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>
+249 -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="👥"
@@ -95,6 +109,9 @@
v-for="evt in events" v-for="evt in events"
:key="evt.id" :key="evt.id"
:event="evt" :event="evt"
:can-manage="evt.isOrganizer === true"
@edit="goEditEvent"
@delete="confirmDeleteEvent"
@item-click="goEventDetail" @item-click="goEventDetail"
/> />
<EmptyState <EmptyState
@@ -120,8 +137,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, DeleteEvent, 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 +165,15 @@ export default {
// 酒局 // 酒局
events: [], events: [],
// 私信未读 // 私信未读
unreadTotal: 0 unreadTotal: 0,
// 当前分享的动态
shareFeed: null,
// 分享模式:feed(分享动态) / invite(邀请好友) / default
shareMode: 'default',
// 我的邀请码(分享时携带,一次性)
inviteCode: '',
// 当前登录用户ID(用于判断动态是否为自己发布)
myUserId: ''
} }
}, },
onShow() { onShow() {
@@ -155,16 +181,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 +284,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 +361,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 */ }
}, },
// === 酒局 === // === 酒局 ===
@@ -270,6 +378,29 @@ export default {
goEventDetail(evt) { goEventDetail(evt) {
uni.navigateTo({ url: `/pages/event-detail/event-detail?id=${evt.id}` }) uni.navigateTo({ url: `/pages/event-detail/event-detail?id=${evt.id}` })
}, },
/** 编辑自己发起的酒局(复用发起页的编辑模式) */
goEditEvent(evt) {
uni.navigateTo({ url: `/pages/event-create/event-create?id=${evt.id}` })
},
/** 删除自己发起的酒局(二次确认) */
confirmDeleteEvent(evt) {
uni.showModal({
title: '删除酒局',
content: '确定要删除这场酒局吗?删除后不可恢复',
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (!res.confirm) return
try {
await DeleteEvent({ eventId: evt.id })
this.events = this.events.filter(e => String(e.id) !== String(evt.id))
uni.showToast({ title: '已删除', icon: 'none' })
} catch (e) {
// httpRequest 已全局弹错误提示(如非本人酒局后端会拒绝)
}
}
})
},
goCreateEvent() { goCreateEvent() {
uni.navigateTo({ url: '/pages/event-create/event-create' }) uni.navigateTo({ url: '/pages/event-create/event-create' })
}, },
@@ -281,6 +412,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 +617,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;
+56 -9
View File
@@ -6,7 +6,7 @@
<view class="create-back" @click="goBack"> <view class="create-back" @click="goBack">
<text class="create-back-icon"></text> <text class="create-back-icon"></text>
</view> </view>
<text class="create-nav-title">发起酒局</text> <text class="create-nav-title">{{ isEdit ? '编辑酒局' : '发起酒局' }}</text>
<view class="create-nav-ph"></view> <view class="create-nav-ph"></view>
</view> </view>
</view> </view>
@@ -98,9 +98,9 @@
/> />
</view> </view>
<!-- 发布按钮 --> <!-- 发布/保存按钮 -->
<button class="create-submit" :class="{ disabled: !canSubmit }" @click="handleSubmit"> <button class="create-submit" :class="{ disabled: !canSubmit }" @click="handleSubmit">
发布酒局 {{ isEdit ? '保存修改' : '发布酒局' }}
</button> </button>
</view> </view>
</scroll-view> </scroll-view>
@@ -108,7 +108,7 @@
</template> </template>
<script> <script>
import { CreateEvent } from '../../common/api' import { CreateEvent, UpdateEvent, GetEventDetail } from '../../common/api'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -132,12 +132,24 @@ export default {
maxPeople: 6, maxPeople: 6,
note: '' note: ''
}, },
submitting: false submitting: false,
// 编辑模式:携带酒局ID时进入编辑态
editId: '',
loadingDetail: false
}
},
onLoad(options) {
if (options && options.id) {
this.editId = options.id
this.loadEventDetail()
} }
}, },
computed: { computed: {
isEdit() {
return !!this.editId
},
canSubmit() { canSubmit() {
return this.form.title.trim() && this.form.date && this.form.time && this.form.location.trim() && !this.submitting return this.form.title.trim() && this.form.date && this.form.time && this.form.location.trim() && !this.submitting && !this.loadingDetail
}, },
locationMarkers() { locationMarkers() {
if (!this.form.latitude) return [] if (!this.form.latitude) return []
@@ -155,6 +167,35 @@ export default {
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
}, },
/** 编辑模式:拉取酒局详情回填表单 */
async loadEventDetail() {
this.loadingDetail = true
try {
const res = await GetEventDetail({ eventId: this.editId })
const evt = res.data || {}
this.form.title = evt.title || ''
// 后端 time 格式如 "2026-08-08 20:00:00",拆分为日期/时间两个选择器字段
const timeStr = String(evt.time || '')
const m = timeStr.match(/^(\d{4}-\d{2}-\d{2})[ T]?(\d{2}:\d{2})?/)
if (m) {
this.form.date = m[1]
this.form.time = m[2] || ''
}
this.form.location = evt.location || ''
this.form.address = evt.address || ''
// 兼容顶层字段与 geo 对象两种返回
const geo = evt.geo || {}
this.form.latitude = evt.latitude != null ? evt.latitude : (geo.latitude != null ? geo.latitude : null)
this.form.longitude = evt.longitude != null ? evt.longitude : (geo.longitude != null ? geo.longitude : null)
this.form.maxPeople = Number(evt.maxPeople) || this.form.maxPeople
this.form.note = evt.note || ''
} catch (e) {
// 加载失败时返回上一页,避免空表单误改
uni.showToast({ title: '加载酒局失败', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
}
this.loadingDetail = false
},
onDateChange(e) { onDateChange(e) {
this.form.date = e.detail.value this.form.date = e.detail.value
}, },
@@ -201,8 +242,7 @@ export default {
async handleSubmit() { async handleSubmit() {
if (!this.canSubmit) return if (!this.canSubmit) return
this.submitting = true this.submitting = true
try { const payload = {
await CreateEvent({
title: this.form.title, title: this.form.title,
time: `${this.form.date} ${this.form.time}:00`, time: `${this.form.date} ${this.form.time}:00`,
location: this.form.location, location: this.form.location,
@@ -211,8 +251,15 @@ export default {
longitude: this.form.longitude, longitude: this.form.longitude,
maxPeople: this.form.maxPeople, maxPeople: this.form.maxPeople,
note: this.form.note note: this.form.note
}) }
try {
if (this.isEdit) {
await UpdateEvent({ eventId: this.editId, ...payload })
uni.showToast({ title: '修改已保存 ✅', icon: 'none' })
} else {
await CreateEvent(payload)
uni.showToast({ title: '酒局已发布 🎉', icon: 'none' }) uni.showToast({ title: '酒局已发布 🎉', icon: 'none' })
}
setTimeout(() => uni.navigateBack(), 800) setTimeout(() => uni.navigateBack(), 800)
} catch (e) { } catch (e) {
// 全局拦截器已弹出错误提示 // 全局拦截器已弹出错误提示
+71 -12
View File
@@ -14,7 +14,7 @@
<scroll-view class="evt-scroll" scroll-y> <scroll-view class="evt-scroll" scroll-y>
<view class="evt-body" v-if="event"> <view class="evt-body" v-if="event">
<!-- 状态 --> <!-- 状态 -->
<view class="evt-status" :class="'status-' + event.status"> <view class="evt-status" :class="'status-' + statusKey">
<text class="evt-status-text">{{ statusLabel }}</text> <text class="evt-status-text">{{ statusLabel }}</text>
</view> </view>
@@ -94,42 +94,47 @@
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="evt-action-bar" v-if="event"> <view class="evt-action-bar" v-if="event">
<button <button
v-if="(event.status === 'open' || event.status === 'upcoming') && !event.isJoined && !event.isOrganizer" v-if="canJoin && !event.isJoined && !event.isOrganizer"
class="evt-btn evt-btn-primary" class="evt-btn evt-btn-primary"
@click="handleJoin" @click="handleJoin"
> >
报名参加 报名参加
</button> </button>
<button <button
v-if="(event.status === 'open' || event.status === 'upcoming') && event.isJoined && !event.isOrganizer" v-if="canJoin && event.isJoined && !event.isOrganizer"
class="evt-btn evt-btn-ghost" class="evt-btn evt-btn-ghost"
@click="handleQuit" @click="handleQuit"
> >
取消报名 取消报名
</button> </button>
<button <button
v-if="event.status === 'ongoing' && event.isJoined" v-if="statusKey === 'ongoing' && event.isJoined"
class="evt-btn evt-btn-primary" class="evt-btn evt-btn-primary"
@click="handleCheckIn" @click="handleCheckIn"
> >
到场签到 到场签到
</button> </button>
<view v-if="event.status === 'ended'" class="evt-ended-tip"> <view v-if="statusKey === 'ended' && !event.isOrganizer" class="evt-ended-tip">
<text class="evt-ended-text">酒局已结束</text> <text class="evt-ended-text">酒局已结束</text>
</view> </view>
<view v-if="event.isOrganizer" class="evt-ended-tip"> <!-- 发起人编辑/删除自己的酒局 -->
<text class="evt-ended-text">你是发起人</text> <view v-if="event.isOrganizer" class="evt-org-actions">
<button class="evt-btn evt-btn-ghost" @click="goEdit">
编辑酒局
</button>
<button class="evt-btn evt-btn-danger" @click="confirmDelete">
🗑 删除酒局
</button>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<script> <script>
import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent } from '../../common/api' import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent, DeleteEvent } from '../../common/api'
import themeMixin from '../../common/theme-mixin' import themeMixin from '../../common/theme-mixin'
export default { export default {
@@ -143,11 +148,20 @@ export default {
} }
}, },
computed: { computed: {
statusLabel() { /** 后端状态为数字枚举:1=报名中 2=待开始 3=进行中 4=已结束(兼容字符串返回) */
statusKey() {
if (!this.event) return '' if (!this.event) return ''
const val = String(this.event.status).toLowerCase() const map = { 1: 'open', 2: 'upcoming', 3: 'ongoing', 4: 'ended' }
const raw = this.event.status
return map[Number(raw)] || String(raw).toLowerCase()
},
/** 报名中/待开始阶段可报名或取消 */
canJoin() {
return this.statusKey === 'open' || this.statusKey === 'upcoming'
},
statusLabel() {
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' } const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
return map[val] || '报名中' return map[this.statusKey] || '报名中'
}, },
eventMarkers() { eventMarkers() {
if (!this.event || !this.event.latitude) return [] if (!this.event || !this.event.latitude) return []
@@ -165,6 +179,12 @@ export default {
this.eventId = options.id || '' this.eventId = options.id || ''
this.loadDetail() this.loadDetail()
}, },
onShow() {
//
if (this.eventId && this._hasLoaded) {
this.loadDetail()
}
},
methods: { methods: {
goBack() { goBack() {
uni.navigateBack() uni.navigateBack()
@@ -173,10 +193,34 @@ export default {
try { try {
const res = await GetEventDetail({ eventId: this.eventId }) const res = await GetEventDetail({ eventId: this.eventId })
this.event = res.data this.event = res.data
this._hasLoaded = true
} catch (e) { } catch (e) {
console.warn('加载酒局详情失败', e) console.warn('加载酒局详情失败', e)
} }
}, },
/** 发起人编辑酒局(复用发起页的编辑模式) */
goEdit() {
uni.navigateTo({ url: `/pages/event-create/event-create?id=${this.eventId}` })
},
/** 发起人删除酒局(二次确认,成功后返回上一页) */
confirmDelete() {
uni.showModal({
title: '删除酒局',
content: '确定要删除这场酒局吗?删除后不可恢复',
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (!res.confirm) return
try {
await DeleteEvent({ eventId: this.eventId })
uni.showToast({ title: '已删除', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
} catch (e) {
// httpRequest
}
}
})
},
async handleJoin() { async handleJoin() {
try { try {
await JoinEvent({ eventId: this.eventId }) await JoinEvent({ eventId: this.eventId })
@@ -525,4 +569,19 @@ export default {
font-size: $fs-base; font-size: $fs-base;
color: $text-tertiary; color: $text-tertiary;
} }
.evt-org-actions {
display: flex;
gap: $sp-md;
.evt-btn {
flex: 1;
}
}
.evt-btn-danger {
background: rgba(255,107,107,0.1);
color: $coral;
border: 1rpx solid rgba(255,107,107,0.25);
}
</style> </style>
+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">