feat(event): 添加酒局报名审核功能并优化前端组件
- 新增 ApproveEvent API 接口用于审核报名用户
- 在 EventCard 组件中显示待审核状态(⏳ 待审核)
- 实现酒局发起人审核报名用户的完整流程
- 添加用户协议和隐私政策页面
- 优化 FeedCard 组件中的图片展示逻辑
- 修复连续打卡天数计算问题
- 更新 HaveADrink SDK 至 1.0.15 版本
This commit is contained in:
@@ -293,6 +293,12 @@ export async function CheckInEvent({ eventId }) {
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
/** 审核报名(仅发起人可操作;approve: 1=待审核 2=通过 3=拒绝) */
|
||||
export async function ApproveEvent({ eventId, userId, approve }) {
|
||||
const res = await client.ApproveEvent({ id: eventId, userId, approve })
|
||||
return { data: res }
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 私信 API(基于 HaveADrink SDK 真实接口)
|
||||
// ==========================================
|
||||
|
||||
@@ -205,6 +205,69 @@ export function calcStats(records) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地计算连续打卡天数(修正后端 streak 不区分饮酒/戒酒模式的问题)
|
||||
* 双轨计算:
|
||||
* - 连续饮酒:从今天的饮酒记录向前追溯;今天未饮酒(戒酒/未打卡)则从昨天算起
|
||||
* - 连续戒酒:从今天的戒酒记录向前追溯;今天饮酒/未打卡则从昨天算起
|
||||
* @param {Array} records 记录列表,每项需含 date 和 mode 字段
|
||||
* @returns {{ streak, streakType, drankStreak, abstainStreak }}
|
||||
* streak/streakType:主展示值(今天有记录取今天模式,否则取最近记录模式)
|
||||
*/
|
||||
export function calcLocalStreak(records) {
|
||||
if (!records || !records.length) {
|
||||
return { streak: 0, streakType: 'drank', drankStreak: 0, abstainStreak: 0 }
|
||||
}
|
||||
const today = formatDate(new Date(), 'YYYY-MM-DD')
|
||||
// 按日期去重(同一天多条取第一条)
|
||||
const byDate = {}
|
||||
records.forEach(r => {
|
||||
if (r && r.date && byDate[r.date] === undefined) byDate[r.date] = r
|
||||
})
|
||||
const dates = Object.keys(byDate).sort()
|
||||
// 单模式连续链:优先从今天起算,今天非该模式则宽限到昨天
|
||||
const chainOf = (mode) => {
|
||||
const todayMode = byDate[today] ? byDate[today].mode : null
|
||||
let base = null
|
||||
if (todayMode === mode) {
|
||||
base = today
|
||||
} else {
|
||||
const y = new Date()
|
||||
y.setDate(y.getDate() - 1)
|
||||
const yStr = formatDate(y, 'YYYY-MM-DD')
|
||||
if (byDate[yStr] && byDate[yStr].mode === mode) base = yStr
|
||||
}
|
||||
if (!base) return 0
|
||||
let count = 1
|
||||
const d = new Date(base)
|
||||
for (let i = 1; i < 366; i++) {
|
||||
const prev = new Date(d)
|
||||
prev.setDate(prev.getDate() - i)
|
||||
const r = byDate[formatDate(prev, 'YYYY-MM-DD')]
|
||||
if (r && r.mode === mode) {
|
||||
count++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
const drankStreak = chainOf('drank')
|
||||
const abstainStreak = chainOf('abstain')
|
||||
// 主展示值:今天有记录取今天模式,否则取最近一次记录的模式
|
||||
let streak
|
||||
let streakType
|
||||
if (byDate[today]) {
|
||||
streakType = byDate[today].mode || 'drank'
|
||||
streak = streakType === 'drank' ? drankStreak : abstainStreak
|
||||
} else {
|
||||
const lastDate = dates[dates.length - 1]
|
||||
streakType = byDate[lastDate].mode || 'drank'
|
||||
streak = streakType === 'drank' ? drankStreak : abstainStreak
|
||||
}
|
||||
return { streak, streakType, drankStreak, abstainStreak }
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题系统:日夜自动切换
|
||||
* 白天:6:00-16:00 → light
|
||||
|
||||
+3
-1
@@ -97,7 +97,9 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
// ===== 服务端协议消息(SDK 已解析 { mesgType, data }) =====
|
||||
conn.onServerChatData = (data) => {
|
||||
conn.onServerChatData = (data) =>
|
||||
console.log(data,'===============');
|
||||
{
|
||||
this._emit('message', data)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
</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 event-action-pending" v-if="event.isJoined && isPending">⏳ 待审核</text>
|
||||
<text class="event-action-text" v-else-if="event.isJoined">✓ 已报名</text>
|
||||
<text class="event-action-text event-action-join" v-else>报名</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,6 +69,10 @@ export default {
|
||||
statusLabel() {
|
||||
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
|
||||
return map[this.statusKey] || '报名中'
|
||||
},
|
||||
/** 已报名但审核状态为待审核(1) */
|
||||
isPending() {
|
||||
return Number(this.event.approveStatus) === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +204,11 @@ export default {
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
.event-action-pending {
|
||||
color: $amber;
|
||||
background: rgba(232,168,56,0.12);
|
||||
}
|
||||
|
||||
.event-manage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
+86
-16
@@ -31,15 +31,16 @@
|
||||
<text class="feed-feeling-text">{{ feelingLabel(feed.feeling) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 图片 -->
|
||||
<!-- 图片:按原图比例自适应高度,极端比例才裁剪 -->
|
||||
<view class="feed-images" v-if="feed.images && feed.images.length">
|
||||
<image
|
||||
v-for="(img, i) in feed.images"
|
||||
:key="i"
|
||||
class="feed-img"
|
||||
:class="'feed-img-' + Math.min(feed.images.length, 3)"
|
||||
:src="img"
|
||||
mode="aspectFill"
|
||||
:style="imgStyle(i)"
|
||||
@click.stop="previewImage(i)"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
@@ -76,9 +77,87 @@ export default {
|
||||
canDelete: { type: Boolean, default: false }
|
||||
},
|
||||
emits: ['item-click', 'like', 'comment', 'share', 'delete'],
|
||||
data() {
|
||||
return {
|
||||
// 图片宽高比缓存:url -> width/height
|
||||
imgRatios: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'feed.images': {
|
||||
handler(list) { this.loadImgMeta(list) },
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCatIcon,
|
||||
isIconPath,
|
||||
/** 拉取图片真实尺寸,用于按比例计算展示高度 */
|
||||
loadImgMeta(list) {
|
||||
if (!list || !list.length) return
|
||||
list.forEach(url => {
|
||||
if (!url || this.imgRatios[url] !== undefined) return
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: res => {
|
||||
if (res.width && res.height) {
|
||||
this.imgRatios[url] = res.width / res.height
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 每行图片数量:4张用2x2,其余每行最多3张 */
|
||||
perRow() {
|
||||
const n = (this.feed.images || []).length
|
||||
return n === 4 ? 2 : Math.min(n || 1, 3)
|
||||
},
|
||||
/** 列宽(rpx):750 - 页面边距64 - 卡片内边距96 - 行间gap */
|
||||
colWidth() {
|
||||
const gap = 16
|
||||
const total = 750 - 64 - 96
|
||||
const cols = this.perRow()
|
||||
return (total - gap * (cols - 1)) / cols
|
||||
},
|
||||
/** 单张图片的目标高度:按真实比例,夹在上下限内 */
|
||||
targetHeight(ratio, colW, minH, maxH) {
|
||||
const r = ratio && ratio > 0 ? ratio : 1
|
||||
return Math.max(minH, Math.min(colW / r, maxH))
|
||||
},
|
||||
/** 同行图片统一高度(取同行目标高度的均值) */
|
||||
rowHeight(rowIdx) {
|
||||
const list = this.feed.images || []
|
||||
const cols = this.perRow()
|
||||
const colW = this.colWidth()
|
||||
const single = cols === 1
|
||||
const minH = single ? 300 : 180
|
||||
const maxH = single ? 720 : 400
|
||||
const start = rowIdx * cols
|
||||
const items = list.slice(start, start + cols)
|
||||
let sum = 0
|
||||
items.forEach(url => {
|
||||
sum += this.targetHeight(this.imgRatios[url], colW, minH, maxH)
|
||||
})
|
||||
return Math.round(sum / items.length)
|
||||
},
|
||||
/** 点击图片预览大图(支持左右滑动切换) */
|
||||
previewImage(i) {
|
||||
const urls = this.feed.images || []
|
||||
if (!urls.length) return
|
||||
uni.previewImage({
|
||||
urls,
|
||||
current: urls[i]
|
||||
})
|
||||
},
|
||||
imgStyle(i) {
|
||||
const cols = this.perRow()
|
||||
const gap = 16
|
||||
const width = cols === 1 ? '100%' : `calc(${(100 / cols).toFixed(4)}% - ${((gap * (cols - 1)) / cols).toFixed(2)}rpx)`
|
||||
return {
|
||||
width,
|
||||
height: this.rowHeight(Math.floor(i / cols)) + 'rpx'
|
||||
}
|
||||
},
|
||||
feelingLabel(f) {
|
||||
const val = String(f).toLowerCase()
|
||||
const map = {
|
||||
@@ -211,20 +290,11 @@ export default {
|
||||
|
||||
.feed-img {
|
||||
border-radius: $radius-md;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.feed-img-1 {
|
||||
width: 100%;
|
||||
height: 320rpx;
|
||||
}
|
||||
|
||||
.feed-img-2 {
|
||||
width: calc(50% - 8rpx);
|
||||
}
|
||||
|
||||
.feed-img-3 {
|
||||
width: calc(33.33% - 11rpx);
|
||||
display: block;
|
||||
/* 比例未获取前的占位尺寸,避免布局跳动 */
|
||||
width: 33%;
|
||||
height: 220rpx;
|
||||
background: $bg-elevated;
|
||||
}
|
||||
|
||||
.feed-actions {
|
||||
|
||||
+3
-3
@@ -4,9 +4,9 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/HaveADrink": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
|
||||
"integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.15.tgz",
|
||||
"integrity": "sha512-ZKgrIKuwXXuKRY2D34FRNUwlr1Y4bAUtXwrjpoHwLPa30KNGVvuXhfYvxAWf5cDvTJw3b/lm8JSSYwCqcIZ3Bw==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
+62
-1
@@ -14,7 +14,7 @@
|
||||
* 6. 社交模块 - 朋友圈动态、点赞
|
||||
|
||||
|
||||
**版本:** v1.0.12
|
||||
**版本:** v1.0.15
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -1116,6 +1116,34 @@ const req = new UpdatePreferencesReq()
|
||||
client.UpdatePreferences(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 获取用户基础信息
|
||||
|
||||
|
||||
|
||||
<font color="green">GET</font> `/api/have_a_drink/v1/user/user/basic_info`
|
||||
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|id|`string\|number`|| 用户ID|
|
||||
|
||||
|
||||
|
||||
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|nickname|`string`| 用户昵称<br>|
|
||||
|avatar|`string`| 头像URL<br>|
|
||||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
const req = new GetUserBasicInfoReq()
|
||||
client.GetUserBasicInfo(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 上传文件
|
||||
|
||||
|
||||
@@ -1865,6 +1893,35 @@ const req = new CheckInEventReq()
|
||||
client.CheckInEvent(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 酒局发起者审核报名用户
|
||||
|
||||
|
||||
|
||||
<font color="green">POST</font> `/api/have_a_drink/v1/events/events/approve`
|
||||
|
||||
#### 请求参数
|
||||
|名称|类型|校验规则|说明|
|
||||
|:-|:-|:-|:-|
|
||||
|id|`string\|number`|| 酒局ID|
|
||||
|userId|`string\|number`|| 报名用户ID|
|
||||
|approve|`ApproveStatus`|待审核: ApproveStatus.Pending = 1<br>已通过: ApproveStatus.Approved = 2<br>已拒绝: ApproveStatus.Rejected = 3| 审核结果|
|
||||
|
||||
|
||||
|
||||
|
||||
#### 返回值
|
||||
|名称|类型|说明|
|
||||
|:-|:-:|:-|
|
||||
|ok|`boolean`| 是否成功<br>|
|
||||
|
||||
|
||||
|
||||
|
||||
```javascript
|
||||
const req = new ApproveEventReq()
|
||||
client.ApproveEvent(req).then(...).catch(...)
|
||||
```
|
||||
|
||||
### 获取会话列表
|
||||
|
||||
|
||||
@@ -1935,6 +1992,10 @@ client.GetConversations(req).then(...).catch(...)
|
||||
|conversationId|`string`| 会话ID|
|
||||
|senderId|`string\|number`| 发送者ID|
|
||||
|receiverId|`string\|number`| 接收者ID|
|
||||
|senderName|`string`| 发送者昵称|
|
||||
|receiverName|`string`| 接收者昵称|
|
||||
|senderAvatar|`string`| 发送者头像URL|
|
||||
|receiverAvatar|`string`| 接收者头像URL|
|
||||
|type|`MessageType`| 消息类型|
|
||||
|content|`string`| 消息内容|
|
||||
|timestamp|`number`| 发送时间(毫秒时间戳)|
|
||||
|
||||
+209
-9
@@ -297,6 +297,27 @@ export declare class EventStatus extends Enum {
|
||||
static getLabel(v: number|string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名审核状态
|
||||
*/
|
||||
export declare class ApproveStatus extends Enum {
|
||||
/**
|
||||
* 待审核
|
||||
*/
|
||||
static Pending: ApproveStatus;
|
||||
/**
|
||||
* 已通过
|
||||
*/
|
||||
static Approved: ApproveStatus;
|
||||
/**
|
||||
* 已拒绝
|
||||
*/
|
||||
static Rejected: ApproveStatus;
|
||||
|
||||
static entries(): Array<option>;
|
||||
static getLabel(v: number|string): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型(文本/图片)
|
||||
*/
|
||||
@@ -2471,6 +2492,87 @@ export declare class UpdatePreferencesResp {
|
||||
static fromObject(o: Object): UpdatePreferencesResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基础信息请求
|
||||
*/
|
||||
export declare class GetUserBasicInfoReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
id: string|number;
|
||||
|
||||
/**
|
||||
* @param id string|number 用户ID
|
||||
*/
|
||||
constructor(id: string|number,);
|
||||
/**
|
||||
* 从对象创建 GetUserBasicInfoReq
|
||||
*
|
||||
* @param o Object
|
||||
* - id: string|number, // 用户ID
|
||||
*/
|
||||
static fromObject(o: Object): GetUserBasicInfoReq;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export declare class UserBasicInfo {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
nickname: string;
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
avatar: string;
|
||||
|
||||
/**
|
||||
* @param nickname string 用户昵称
|
||||
* @param avatar string 头像URL
|
||||
*/
|
||||
constructor(nickname: string,avatar: string,);
|
||||
/**
|
||||
* 从对象创建 UserBasicInfo
|
||||
*
|
||||
* @param o Object
|
||||
* - nickname: string, // 用户昵称
|
||||
* - avatar: string, // 头像URL
|
||||
*/
|
||||
static fromObject(o: Object): UserBasicInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基础信息响应
|
||||
*/
|
||||
export declare class GetUserBasicInfoResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
nickname: string;
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
avatar: string;
|
||||
|
||||
/**
|
||||
* @param nickname string 用户昵称
|
||||
* @param avatar string 头像URL
|
||||
*/
|
||||
constructor(nickname: string,avatar: string,);
|
||||
/**
|
||||
* 从对象创建 GetUserBasicInfoResp
|
||||
*
|
||||
* @param o Object
|
||||
* - nickname: string, // 用户昵称
|
||||
* - avatar: string, // 头像URL
|
||||
*/
|
||||
static fromObject(o: Object): GetUserBasicInfoResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片参数
|
||||
*/
|
||||
@@ -4115,6 +4217,64 @@ export declare class CheckInEventResp {
|
||||
static fromObject(o: Object): CheckInEventResp;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export declare class ApproveEventReq {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 酒局ID
|
||||
*/
|
||||
id: string|number;
|
||||
/**
|
||||
* 报名用户ID
|
||||
*/
|
||||
userId: string|number;
|
||||
/**
|
||||
* 审核结果
|
||||
*/
|
||||
approve: ApproveStatus;
|
||||
|
||||
/**
|
||||
* @param id string|number 酒局ID
|
||||
* @param userId string|number 报名用户ID
|
||||
* @param approve ApproveStatus 审核结果
|
||||
*/
|
||||
constructor(id: string|number,userId: string|number,approve: ApproveStatus,);
|
||||
/**
|
||||
* 从对象创建 ApproveEventReq
|
||||
*
|
||||
* @param o Object
|
||||
* - id: string|number, // 酒局ID
|
||||
* - userId: string|number, // 报名用户ID
|
||||
* - approve: ApproveStatus, // 审核结果
|
||||
*/
|
||||
static fromObject(o: Object): ApproveEventReq;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export declare class ApproveEventResp {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
ok: boolean;
|
||||
|
||||
/**
|
||||
* @param ok boolean 是否成功
|
||||
*/
|
||||
constructor(ok: boolean,);
|
||||
/**
|
||||
* 从对象创建 ApproveEventResp
|
||||
*
|
||||
* @param o Object
|
||||
* - ok: boolean, // 是否成功
|
||||
*/
|
||||
static fromObject(o: Object): ApproveEventResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
@@ -4277,6 +4437,22 @@ export declare class Message {
|
||||
*/
|
||||
receiverId: string|number;
|
||||
/**
|
||||
* 发送者昵称
|
||||
*/
|
||||
senderName: string;
|
||||
/**
|
||||
* 接收者昵称
|
||||
*/
|
||||
receiverName: string;
|
||||
/**
|
||||
* 发送者头像URL
|
||||
*/
|
||||
senderAvatar: string;
|
||||
/**
|
||||
* 接收者头像URL
|
||||
*/
|
||||
receiverAvatar: string;
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
type: MessageType;
|
||||
@@ -4298,12 +4474,16 @@ export declare class Message {
|
||||
* @param conversationId string 会话ID
|
||||
* @param senderId string|number 发送者ID
|
||||
* @param receiverId string|number 接收者ID
|
||||
* @param senderName string 发送者昵称
|
||||
* @param receiverName string 接收者昵称
|
||||
* @param senderAvatar string 发送者头像URL
|
||||
* @param receiverAvatar string 接收者头像URL
|
||||
* @param type MessageType 消息类型
|
||||
* @param content string 消息内容
|
||||
* @param timestamp number 发送时间(毫秒时间戳)
|
||||
* @param status MessageStatus 消息状态
|
||||
*/
|
||||
constructor(id: string|number,conversationId: string,senderId: string|number,receiverId: string|number,type: MessageType,content: string,timestamp: number,status: MessageStatus,);
|
||||
constructor(id: string|number,conversationId: string,senderId: string|number,receiverId: string|number,senderName: string,receiverName: string,senderAvatar: string,receiverAvatar: string,type: MessageType,content: string,timestamp: number,status: MessageStatus,);
|
||||
/**
|
||||
* 从对象创建 Message
|
||||
*
|
||||
@@ -4312,6 +4492,10 @@ export declare class Message {
|
||||
* - conversationId: string, // 会话ID
|
||||
* - senderId: string|number, // 发送者ID
|
||||
* - receiverId: string|number, // 接收者ID
|
||||
* - senderName: string, // 发送者昵称
|
||||
* - receiverName: string, // 接收者昵称
|
||||
* - senderAvatar: string, // 发送者头像URL
|
||||
* - receiverAvatar: string, // 接收者头像URL
|
||||
* - type: MessageType, // 消息类型
|
||||
* - content: string, // 消息内容
|
||||
* - timestamp: number, // 发送时间(毫秒时间戳)
|
||||
@@ -4455,15 +4639,15 @@ export declare class ClientChatData {
|
||||
/**
|
||||
* 客户端消息ID
|
||||
*/
|
||||
clientMesgId: string|number;
|
||||
clientMesgId: string;
|
||||
|
||||
/**
|
||||
* @param receiverId string|number 接收者ID
|
||||
* @param content string 消息内容
|
||||
* @param msgType MessageType 消息类型
|
||||
* @param clientMesgId string|number 客户端消息ID
|
||||
* @param clientMesgId string 客户端消息ID
|
||||
*/
|
||||
constructor(receiverId: string|number,content: string,msgType: MessageType,clientMesgId: string|number,);
|
||||
constructor(receiverId: string|number,content: string,msgType: MessageType,clientMesgId: string,);
|
||||
/**
|
||||
* 从对象创建 ClientChatData
|
||||
*
|
||||
@@ -4471,7 +4655,7 @@ export declare class ClientChatData {
|
||||
* - receiverId: string|number, // 接收者ID
|
||||
* - content: string, // 消息内容
|
||||
* - msgType: MessageType, // 消息类型
|
||||
* - clientMesgId: string|number, // 客户端消息ID
|
||||
* - clientMesgId: string, // 客户端消息ID
|
||||
*/
|
||||
static fromObject(o: Object): ClientChatData;
|
||||
}
|
||||
@@ -4664,7 +4848,7 @@ export declare class ServerAckData {
|
||||
/**
|
||||
* 客户端消息ID
|
||||
*/
|
||||
clientMsgId: string|number;
|
||||
clientMsgId: string;
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@@ -4675,16 +4859,16 @@ export declare class ServerAckData {
|
||||
status: MessageStatus;
|
||||
|
||||
/**
|
||||
* @param clientMsgId string|number 客户端消息ID
|
||||
* @param clientMsgId string 客户端消息ID
|
||||
* @param msgId string|number 消息ID
|
||||
* @param status MessageStatus 消息状态
|
||||
*/
|
||||
constructor(clientMsgId: string|number,msgId: string|number,status: MessageStatus,);
|
||||
constructor(clientMsgId: string,msgId: string|number,status: MessageStatus,);
|
||||
/**
|
||||
* 从对象创建 ServerAckData
|
||||
*
|
||||
* @param o Object
|
||||
* - clientMsgId: string|number, // 客户端消息ID
|
||||
* - clientMsgId: string, // 客户端消息ID
|
||||
* - msgId: string|number, // 消息ID
|
||||
* - status: MessageStatus, // 消息状态
|
||||
*/
|
||||
@@ -5027,6 +5211,14 @@ export default class HaveADrink {
|
||||
*/
|
||||
public UpdatePreferences(req: UpdatePreferencesReq) : Promise<UpdatePreferencesResp>;
|
||||
|
||||
/**
|
||||
* 获取用户基础信息
|
||||
*
|
||||
* @param req GetUserBasicInfoReq 获取用户基础信息请求
|
||||
* @return GetUserBasicInfoResp 获取用户基础信息响应
|
||||
*/
|
||||
public GetUserBasicInfo(req: GetUserBasicInfoReq) : Promise<GetUserBasicInfoResp>;
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
@@ -5195,6 +5387,14 @@ export default class HaveADrink {
|
||||
*/
|
||||
public CheckInEvent(req: CheckInEventReq) : Promise<CheckInEventResp>;
|
||||
|
||||
/**
|
||||
* 酒局发起者审核报名用户
|
||||
*
|
||||
* @param req ApproveEventReq
|
||||
* @return ApproveEventResp
|
||||
*/
|
||||
public ApproveEvent(req: ApproveEventReq) : Promise<ApproveEventResp>;
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
|
||||
+197
-4
@@ -436,6 +436,39 @@ export class EventStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 报名审核状态
|
||||
*/
|
||||
export class ApproveStatus {
|
||||
/**
|
||||
* 待审核
|
||||
*/
|
||||
static Pending = 1;
|
||||
/**
|
||||
* 已通过
|
||||
*/
|
||||
static Approved = 2;
|
||||
/**
|
||||
* 已拒绝
|
||||
*/
|
||||
static Rejected = 3;
|
||||
|
||||
static entries() {
|
||||
return [
|
||||
{ value: ApproveStatus.Pending, label: '待审核' },
|
||||
{ value: ApproveStatus.Approved, label: '已通过' },
|
||||
{ value: ApproveStatus.Rejected, label: '已拒绝' },
|
||||
];
|
||||
}
|
||||
static getLabel(v) {
|
||||
switch(v) {
|
||||
case ApproveStatus.Pending: return '待审核';
|
||||
case ApproveStatus.Approved: return '已通过';
|
||||
case ApproveStatus.Rejected: return '已拒绝';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息类型(文本/图片)
|
||||
*/
|
||||
@@ -1779,6 +1812,58 @@ export class UpdatePreferencesResp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基础信息请求
|
||||
*/
|
||||
export class GetUserBasicInfoReq {
|
||||
/**
|
||||
* @param id: string|number 用户ID
|
||||
*/
|
||||
constructor(id,) {
|
||||
this.id = id;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new GetUserBasicInfoReq(o.id,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class UserBasicInfo {
|
||||
/**
|
||||
* @param nickname: string 用户昵称
|
||||
* @param avatar: string 头像URL
|
||||
*/
|
||||
constructor(nickname,avatar,) {
|
||||
this.nickname = nickname;
|
||||
this.avatar = avatar;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new UserBasicInfo(o.nickname,o.avatar,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基础信息响应
|
||||
*/
|
||||
export class GetUserBasicInfoResp {
|
||||
/**
|
||||
* @param nickname: string 用户昵称
|
||||
* @param avatar: string 头像URL
|
||||
*/
|
||||
constructor(nickname,avatar,) {
|
||||
this.nickname = nickname;
|
||||
this.avatar = avatar;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new GetUserBasicInfoResp(o.nickname,o.avatar,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片参数
|
||||
*/
|
||||
@@ -2777,6 +2862,42 @@ export class CheckInEventResp {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class ApproveEventReq {
|
||||
/**
|
||||
* @param id: string|number 酒局ID
|
||||
* @param userId: string|number 报名用户ID
|
||||
* @param approve: ApproveStatus 审核结果
|
||||
*/
|
||||
constructor(id,userId,approve,) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.approve = approve;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new ApproveEventReq(o.id,o.userId,o.approve,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export class ApproveEventResp {
|
||||
/**
|
||||
* @param ok: boolean 是否成功
|
||||
*/
|
||||
constructor(ok,) {
|
||||
this.ok = ok;
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new ApproveEventResp(o.ok,);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话信息
|
||||
*/
|
||||
@@ -2866,16 +2987,24 @@ export class Message {
|
||||
* @param conversationId: string 会话ID
|
||||
* @param senderId: string|number 发送者ID
|
||||
* @param receiverId: string|number 接收者ID
|
||||
* @param senderName: string 发送者昵称
|
||||
* @param receiverName: string 接收者昵称
|
||||
* @param senderAvatar: string 发送者头像URL
|
||||
* @param receiverAvatar: string 接收者头像URL
|
||||
* @param type: MessageType 消息类型
|
||||
* @param content: string 消息内容
|
||||
* @param timestamp: number 发送时间(毫秒时间戳)
|
||||
* @param status: MessageStatus 消息状态
|
||||
*/
|
||||
constructor(id,conversationId,senderId,receiverId,type,content,timestamp,status,) {
|
||||
constructor(id,conversationId,senderId,receiverId,senderName,receiverName,senderAvatar,receiverAvatar,type,content,timestamp,status,) {
|
||||
this.id = id;
|
||||
this.conversationId = conversationId;
|
||||
this.senderId = senderId;
|
||||
this.receiverId = receiverId;
|
||||
this.senderName = senderName;
|
||||
this.receiverName = receiverName;
|
||||
this.senderAvatar = senderAvatar;
|
||||
this.receiverAvatar = receiverAvatar;
|
||||
this.type = type;
|
||||
this.content = content;
|
||||
this.timestamp = timestamp;
|
||||
@@ -2883,7 +3012,7 @@ export class Message {
|
||||
|
||||
}
|
||||
static fromObject(o) {
|
||||
return new Message(o.id,o.conversationId,o.senderId,o.receiverId,o.type,o.content,o.timestamp,o.status,);
|
||||
return new Message(o.id,o.conversationId,o.senderId,o.receiverId,o.senderName,o.receiverName,o.senderAvatar,o.receiverAvatar,o.type,o.content,o.timestamp,o.status,);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2975,7 +3104,7 @@ export class ClientChatData {
|
||||
* @param receiverId: string|number 接收者ID
|
||||
* @param content: string 消息内容
|
||||
* @param msgType: MessageType 消息类型
|
||||
* @param clientMesgId: string|number 客户端消息ID
|
||||
* @param clientMesgId: string 客户端消息ID
|
||||
*/
|
||||
constructor(receiverId,content,msgType,clientMesgId,) {
|
||||
this.receiverId = receiverId;
|
||||
@@ -3104,7 +3233,7 @@ export class ServerReadData {
|
||||
*/
|
||||
export class ServerAckData {
|
||||
/**
|
||||
* @param clientMsgId: string|number 客户端消息ID
|
||||
* @param clientMsgId: string 客户端消息ID
|
||||
* @param msgId: string|number 消息ID
|
||||
* @param status: MessageStatus 消息状态
|
||||
*/
|
||||
@@ -3991,6 +4120,38 @@ export default class HaveADrink {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户基础信息
|
||||
*/
|
||||
GetUserBasicInfo(req) {
|
||||
return new Promise((reslove, reject)=>{
|
||||
let data = req;
|
||||
let url = `${this.host}/api/have_a_drink/v1/user/user/basic_info`;
|
||||
const query = Object.keys(data).map((x)=>(`${x}=${data[x]}`)).join('&');
|
||||
|
||||
this.http_request(url, {
|
||||
uri: '/api/have_a_drink/v1/user/user/basic_info',
|
||||
method: 'GET',
|
||||
query: query,
|
||||
data: data,
|
||||
responseType: 'json',
|
||||
headers: {
|
||||
},
|
||||
}).then((data)=>{
|
||||
if (data.hasOwnProperty("fail")) {
|
||||
if (data.fail) {
|
||||
reject(data.msg);
|
||||
} else {
|
||||
reslove(data.data);
|
||||
}
|
||||
} else {
|
||||
reslove(data);
|
||||
}
|
||||
}).catch((err)=>reject(err));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
@@ -4659,6 +4820,38 @@ export default class HaveADrink {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 酒局发起者审核报名用户
|
||||
*/
|
||||
ApproveEvent(req) {
|
||||
return new Promise((reslove, reject)=>{
|
||||
let data = req;
|
||||
let url = `${this.host}/api/have_a_drink/v1/events/events/approve`;
|
||||
|
||||
|
||||
this.http_request(url, {
|
||||
uri: '/api/have_a_drink/v1/events/events/approve',
|
||||
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));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "HaveADrink",
|
||||
"type": "module",
|
||||
"version": "v1.0.12",
|
||||
"version": "v1.0.15",
|
||||
"description": "喝酒了么 API 服务",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
Generated
+4
-4
@@ -5,13 +5,13 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"HaveADrink": "^1.0.12"
|
||||
"HaveADrink": "^1.0.15"
|
||||
}
|
||||
},
|
||||
"node_modules/HaveADrink": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.12.tgz",
|
||||
"integrity": "sha512-B9/2sOCIvMczQzsWA1qqzEOlwKxl9eC5OEqvRgh1dewgsbA4zzOkCQztuJxZOq9Sz5mwzEBM6/bcYdJyGAox3A==",
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://npm.wash-painting.cn/HaveADrink/-/HaveADrink-1.0.15.tgz",
|
||||
"integrity": "sha512-ZKgrIKuwXXuKRY2D34FRNUwlr1Y4bAUtXwrjpoHwLPa30KNGVvuXhfYvxAWf5cDvTJw3b/lm8JSSYwCqcIZ3Bw==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"HaveADrink": "^1.0.12"
|
||||
"HaveADrink": "^1.0.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,13 @@
|
||||
"backgroundColorTop": "#0B0B14",
|
||||
"backgroundColorBottom": "#0B0B14"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/agreement/agreement",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<view :class="themeClass" class="page-container agreement-page">
|
||||
<!-- 自定义导航栏(避让胶囊) -->
|
||||
<view class="agree-nav" :style="{ paddingTop: navPaddingTop }">
|
||||
<view class="agree-nav-row" :style="{ paddingRight: navPaddingRight }">
|
||||
<view class="agree-back" @click="goBack">
|
||||
<text class="agree-back-icon">‹</text>
|
||||
</view>
|
||||
<text class="agree-nav-title">{{ title }}</text>
|
||||
<view class="agree-back agree-back-ph"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="agree-scroll" scroll-y :show-scrollbar="false">
|
||||
<view class="agree-content">
|
||||
<text class="agree-update">更新日期:2026年8月1日 生效日期:2026年8月1日</text>
|
||||
<view v-for="(sec, i) in sections" :key="i" class="agree-section">
|
||||
<text class="agree-section-title">{{ sec.title }}</text>
|
||||
<text v-for="(p, j) in sec.paras" :key="j" class="agree-p">{{ p }}</text>
|
||||
</view>
|
||||
<view class="agree-footer">
|
||||
<text class="agree-footer-text">碰盏日记团队</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
mixins: [themeMixin],
|
||||
data() {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
return {
|
||||
type: 'user',
|
||||
navPaddingTop: menuBtn.top + 'px',
|
||||
navPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
|
||||
sections: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.type === 'privacy' ? '隐私政策' : '用户协议'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.type = options && options.type === 'privacy' ? 'privacy' : 'user'
|
||||
this.sections = this.type === 'privacy' ? this.privacySections() : this.userSections()
|
||||
uni.setNavigationBarTitle({ title: this.title })
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
} else {
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
userSections() {
|
||||
return [
|
||||
{
|
||||
title: '一、服务说明',
|
||||
paras: [
|
||||
'「碰盏日记」是一款个人饮酒记录与生活管理工具,为用户提供饮酒打卡、日历回顾、统计成就、酒友圈动态、私聊互动等功能。',
|
||||
'本小程序仅提供记录与社交工具,不从事任何酒类的销售、推广或推荐行为,亦不接受任何形式的酒类广告投放。',
|
||||
'我们倡导理性饮酒。过量饮酒有害健康,请您在记录的同时关注自身身体状况,未成年人请勿饮酒。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '二、账号注册与使用',
|
||||
paras: [
|
||||
'您通过微信授权登录并填写昵称、头像完成注册。您应保证所填资料真实、合法,不使用他人身份或侵权内容。',
|
||||
'您应妥善保管自己的账号。因您个人原因导致的账号信息泄露,由您自行承担相应责任。',
|
||||
'本小程序面向年满18周岁的成年人提供服务。若您未满18周岁,请勿注册和使用。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '三、用户行为规范',
|
||||
paras: [
|
||||
'您在酒友圈发布的动态、评论、图片及私聊内容,应遵守国家法律法规,不得包含色情、暴力、赌博、欺诈、侵权或其他违法和不良信息。',
|
||||
'您不得发布诱导饮酒、劝酒、拼酒等可能对他人造成伤害的内容,不得向未成年人传播任何饮酒相关信息。',
|
||||
'您不得利用本小程序从事任何干扰服务正常运行、侵害他人合法权益的行为。若违反上述约定,我们有权对相关内容进行处理,并视情况暂停或终止您的账号。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '四、知识产权',
|
||||
paras: [
|
||||
'本小程序的软件、界面设计、图标、文案等知识产权归开发者所有,未经许可不得复制、传播或用于商业用途。',
|
||||
'您发布的内容(文字、图片等)著作权归您所有。您同意授予我们在提供服务所必需的范围内(如向您的酒友展示动态)使用上述内容。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '五、免责声明',
|
||||
paras: [
|
||||
'本小程序记录的饮酒数据仅作为个人生活记录参考,不构成任何医疗、健康或营养建议。如您有饮酒相关的健康困扰,请咨询专业医疗机构。',
|
||||
'因不可抗力、网络故障、微信平台服务调整等非我们可控因素导致的服务中断或数据延迟,我们不承担责任,但会尽力恢复服务。',
|
||||
'请您自行备份重要记录。因您主动删除或账号注销导致的数据丢失,由您自行承担。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '六、协议的变更与终止',
|
||||
paras: [
|
||||
'我们可能根据法律法规变化或功能调整修订本协议,修订后将在小程序内公示。若您继续使用,视为接受修订后的协议。',
|
||||
'您可随时停止使用本小程序。我们也可在法律法规允许的范围内终止提供服务。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '七、其他',
|
||||
paras: [
|
||||
'本协议的订立、执行与解释均适用中华人民共和国法律。因本协议产生的争议,双方应友好协商解决。',
|
||||
'如您对本协议有任何疑问,可通过小程序内的意见反馈渠道与我们联系。'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
privacySections() {
|
||||
return [
|
||||
{
|
||||
title: '引言',
|
||||
paras: [
|
||||
'「碰盏日记」(以下简称"我们")深知个人信息对您的重要性,将严格遵守法律法规,审慎处理您的个人信息。本政策将说明我们如何收集、使用、存储和保护您的信息,请您仔细阅读。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '一、我们收集的信息',
|
||||
paras: [
|
||||
'1. 注册资料:您主动填写的昵称、头像,用于展示您的个人身份。',
|
||||
'2. 打卡记录:您记录的日期、饮酒/戒酒状态、酒水种类与用量、配餐、感受、酒言酒语等,用于日历展示、统计与成就功能。',
|
||||
'3. 图片信息:您主动上传的饮酒照片,仅随对应记录存储展示。',
|
||||
'4. 位置信息:仅在您创建酒局并主动选择地点时,经您授权获取位置用于酒局展示;我们不会持续追踪您的位置。',
|
||||
'5. 社交信息:您添加酒友、发布动态、发送私信时产生的关系与内容数据。',
|
||||
'6. 设备与日志信息:微信登录产生的必要标识(如 openid),用于识别您的账号身份,我们不会获取您的微信号、手机号等额外信息。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '二、我们如何使用信息',
|
||||
paras: [
|
||||
'您的信息仅用于提供本小程序的核心功能:记录打卡、日历回顾、统计成就、酒友圈展示与私信互动。',
|
||||
'动态可见范围由您控制:设为"仅酒友可见"的内容只有与您互加酒友的用户可见。',
|
||||
'我们不会将您的信息用于酒类营销、广告推送或任何商业推广。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '三、信息的共享与披露',
|
||||
paras: [
|
||||
'我们不会向任何第三方出售您的个人信息。',
|
||||
'除以下情形外,我们不会向第三方提供您的信息:(1)经您明确同意;(2)根据法律法规或主管部门要求;(3)为保护用户或公众的人身财产安全所必需。',
|
||||
'分享名片功能会生成您主动选择的公开信息(昵称、统计数字等),是否分享由您自行决定。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '四、信息的存储与保护',
|
||||
paras: [
|
||||
'您的数据存储于服务器,传输过程采用加密方式。我们采取访问控制、安全审计等措施保护数据安全。',
|
||||
'我们仅在实现服务目的所必需的期限内保存您的信息。账号注销后,我们将删除或匿名化处理您的个人信息,法律法规另有规定的除外。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '五、您的权利',
|
||||
paras: [
|
||||
'您可以随时查看、修改自己的资料与记录;可以删除任意一条打卡记录或动态。',
|
||||
'您可以删除与酒友的关系、撤回不当内容。',
|
||||
'如需注销账号或删除全部数据,请通过小程序内的反馈渠道联系我们,我们将在核实身份后15个工作日内处理。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '六、未成年人保护',
|
||||
paras: [
|
||||
'本小程序不面向未成年人提供服务。若我们发现误收了未成年人的个人信息,将及时删除。'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '七、政策更新与联系我们',
|
||||
paras: [
|
||||
'本政策可能随功能调整而更新,更新后将在小程序内公示。',
|
||||
'如您对本政策或个人信息处理有任何疑问、投诉,可通过小程序内的意见反馈渠道与我们联系,我们将尽快答复。'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.agreement-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agree-nav {
|
||||
background: $bg-base;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
}
|
||||
|
||||
.agree-nav-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.agree-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: $bg-card-alt;
|
||||
}
|
||||
}
|
||||
|
||||
.agree-back-ph {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.agree-back-icon {
|
||||
font-size: 48rpx;
|
||||
color: $text-primary;
|
||||
line-height: 1;
|
||||
margin-top: -6rpx;
|
||||
}
|
||||
|
||||
.agree-nav-title {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.agree-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.agree-content {
|
||||
padding: $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 64rpx);
|
||||
}
|
||||
|
||||
.agree-update {
|
||||
display: block;
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.agree-section {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.agree-section-title {
|
||||
display: block;
|
||||
font-size: $fs-md;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.agree-p {
|
||||
display: block;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
line-height: $lh-loose;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.agree-footer {
|
||||
padding: $sp-xl 0 $sp-lg;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agree-footer-text {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
</style>
|
||||
+20
-2
@@ -131,6 +131,7 @@ export default {
|
||||
headerPaddingTop: (menuBtn.top + 8) + 'px',
|
||||
friendId: '',
|
||||
nickname: '',
|
||||
myUserId: '',
|
||||
conversationId: '',
|
||||
messages: [],
|
||||
inputText: '',
|
||||
@@ -148,6 +149,11 @@ export default {
|
||||
this.friendId = options.friendId || ''
|
||||
this.nickname = decodeURIComponent(options.nickname || '')
|
||||
this.conversationId = options.conversationId || ''
|
||||
// 读取当前用户ID(与 circle/profile 页同源:user_info.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.initChat()
|
||||
// 监听键盘高度变化,动态调整输入栏位置(避免键盘顶起整个页面)
|
||||
uni.onKeyboardHeightChange(res => {
|
||||
@@ -308,7 +314,7 @@ export default {
|
||||
const msg = {
|
||||
id: clientMsgId,
|
||||
conversationId: this.conversationId,
|
||||
senderId: wsManager.userId || 'self',
|
||||
senderId: this.myUserId || wsManager.userId || 'self',
|
||||
receiverId: this.friendId,
|
||||
type: 'text',
|
||||
content: text,
|
||||
@@ -370,8 +376,20 @@ export default {
|
||||
},
|
||||
|
||||
// === 辅助方法 ===
|
||||
/**
|
||||
* 判断消息是否自己发的:直接靠 senderId/receiverId 与当前用户ID(user_info.id)对比。
|
||||
* 不能用 wsManager.userId:WS 连接成功前为空,会导致历史消息全部误判。
|
||||
*/
|
||||
isSelf(msg) {
|
||||
return String(msg.senderId) === String(wsManager.userId) || msg.senderId === 'self'
|
||||
const myId = this.myUserId || (wsManager.userId ? String(wsManager.userId) : '')
|
||||
if (myId) {
|
||||
if (String(msg.senderId) === myId) return true
|
||||
if (String(msg.receiverId) === myId) return false
|
||||
}
|
||||
// 兜底(无当前用户ID时):会话内发送者只会是双方之一
|
||||
if (msg.senderId === 'self') return true
|
||||
if (this.friendId && String(msg.senderId) === String(this.friendId)) return false
|
||||
return true
|
||||
},
|
||||
|
||||
showTimeDivider(idx) {
|
||||
|
||||
+37
-100
@@ -4,15 +4,20 @@
|
||||
<view class="circle-header" :style="{ paddingTop: headerPaddingTop }">
|
||||
<view class="circle-title-row" :style="{ paddingRight: headerPaddingRight }">
|
||||
<text class="circle-title">酒友圈</text>
|
||||
<view class="msg-entry" @click="goChatList">
|
||||
<!-- 发布入口:标题栏右侧胶囊按钮,替代悬浮 FAB 彻底避免遮挡 -->
|
||||
<view class="publish-btn" @click="goPublish">
|
||||
<text class="publish-btn-icon">✏️</text>
|
||||
<text class="publish-btn-text">发布</text>
|
||||
</view>
|
||||
<!-- <view class="msg-entry" @click="goChatList">
|
||||
<text class="msg-entry-icon">💬</text>
|
||||
<view v-if="unreadTotal > 0" class="msg-badge">
|
||||
<text class="msg-badge-text">{{ unreadTotal > 99 ? '99+' : unreadTotal }}</text>
|
||||
</view> -->
|
||||
<!-- </view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- Tab 切换 -->
|
||||
<view class="circle-tabs">
|
||||
<!-- Tab 切换(暂只保留动态) -->
|
||||
<!-- <view class="circle-tabs">
|
||||
<view
|
||||
v-for="(tab, i) in tabs"
|
||||
:key="i"
|
||||
@@ -23,12 +28,13 @@
|
||||
<text class="circle-tab-text">{{ tab }}</text>
|
||||
<view v-if="currentTab === i" class="circle-tab-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<!-- 动态 Tab -->
|
||||
<!-- v-if="currentTab === 0" -->
|
||||
<scroll-view
|
||||
v-if="currentTab === 0"
|
||||
|
||||
class="circle-scroll"
|
||||
scroll-y
|
||||
:refresher-enabled="true"
|
||||
@@ -64,71 +70,6 @@
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 酒友 Tab -->
|
||||
<scroll-view v-if="currentTab === 1" class="circle-scroll" scroll-y>
|
||||
<view class="circle-content">
|
||||
<!-- 好友请求提醒 -->
|
||||
<view v-if="friendRequests.length" class="request-banner" @click="goFriends">
|
||||
<text class="request-icon">🔔</text>
|
||||
<text class="request-text">{{ friendRequests.length }} 条新的好友请求</text>
|
||||
<text class="request-arrow">›</text>
|
||||
</view>
|
||||
<!-- 酒友管理入口(有好友时也可进入:处理请求/删除酒友) -->
|
||||
<view v-if="friends.length && !friendRequests.length" class="friends-entry" @click="goFriends">
|
||||
<text class="friends-entry-icon">👥</text>
|
||||
<text class="friends-entry-text">管理酒友 · 处理好友请求</text>
|
||||
<text class="friends-entry-arrow">›</text>
|
||||
</view>
|
||||
<FriendItem
|
||||
v-for="f in friends"
|
||||
:key="f.id"
|
||||
:friend="f"
|
||||
@item-click="goChat(f)"
|
||||
/>
|
||||
<!-- 有好友时也保留邀请入口,直接触发带邀请码的转发 -->
|
||||
<button v-if="friends.length" class="invite-btn" open-type="share" @click="prepareInviteShare">
|
||||
<text class="invite-btn-icon">👥</text>
|
||||
<text class="invite-btn-text">邀请更多酒友</text>
|
||||
</button>
|
||||
<EmptyState
|
||||
v-if="!friends.length"
|
||||
icon="👥"
|
||||
title="还没有酒友"
|
||||
desc="邀请好友一起记录饮酒生活"
|
||||
actionText="邀请好友"
|
||||
@action="goFriends"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 酒局 Tab -->
|
||||
<scroll-view v-if="currentTab === 2" class="circle-scroll" scroll-y>
|
||||
<view class="circle-content">
|
||||
<EventCard
|
||||
v-for="evt in events"
|
||||
:key="evt.id"
|
||||
:event="evt"
|
||||
:can-manage="evt.isOrganizer === true"
|
||||
@edit="goEditEvent"
|
||||
@delete="confirmDeleteEvent"
|
||||
@item-click="goEventDetail"
|
||||
/>
|
||||
<EmptyState
|
||||
v-if="!events.length"
|
||||
icon="🎉"
|
||||
title="暂无酒局"
|
||||
desc="发起一场酒局,约上酒友一起喝"
|
||||
actionText="发起酒局"
|
||||
@action="goCreateEvent"
|
||||
/>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- FAB 发布按钮 -->
|
||||
<view class="fab" @click="handleFab">
|
||||
<text class="fab-icon">{{ currentTab === 2 ? '🎉' : '✏️' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -149,7 +90,7 @@ export default {
|
||||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||||
const sysInfo = uni.getSystemInfoSync()
|
||||
return {
|
||||
tabs: ['动态', '酒友', '酒局'],
|
||||
tabs: ['动态'],
|
||||
currentTab: 0,
|
||||
headerPaddingTop: (menuBtn.top + 8) + 'px',
|
||||
headerPaddingRight: (sysInfo.windowWidth - menuBtn.left + 8) + 'px',
|
||||
@@ -178,17 +119,14 @@ export default {
|
||||
},
|
||||
onShow() {
|
||||
this.loadFeeds(true)
|
||||
this.loadFriends()
|
||||
this.loadEvents()
|
||||
this.loadUnread()
|
||||
this.bindWsEvents()
|
||||
// 消息入口已隐藏,未读角标暂不展示,空转请求先停掉(恢复入口时打开)
|
||||
// this.loadUnread()
|
||||
// this.bindWsEvents()
|
||||
// 读取当前用户ID(删除自己的动态时用)
|
||||
try {
|
||||
const user = JSON.parse(uni.getStorageSync('user_info') || '{}')
|
||||
this.myUserId = user.id !== undefined && user.id !== null ? String(user.id) : ''
|
||||
} catch (e) { this.myUserId = '' }
|
||||
// 预生成邀请码(仅首次/用完后生成,避免每次进页都创建无效邀请)
|
||||
this.ensureInviteCode()
|
||||
// 页面显示时确保 WS 已连接(登录后/断线后兼容)
|
||||
wsManager.connect()
|
||||
},
|
||||
@@ -406,12 +344,8 @@ export default {
|
||||
},
|
||||
// === FAB ===
|
||||
handleFab() {
|
||||
if (this.currentTab === 2) {
|
||||
this.goCreateEvent()
|
||||
} else {
|
||||
this.goPublish()
|
||||
}
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
// 邀请好友分享:链接携带一次性邀请码,好友点开后自动接受邀请
|
||||
@@ -470,7 +404,6 @@ export default {
|
||||
.circle-header {
|
||||
padding-left: $sp-lg;
|
||||
padding-right: $sp-lg;
|
||||
padding-bottom: $sp-md;
|
||||
background: $bg-base;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
@@ -569,7 +502,7 @@ export default {
|
||||
|
||||
.circle-content {
|
||||
padding: $sp-md $sp-lg;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 140rpx);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 48rpx);
|
||||
}
|
||||
|
||||
.circle-loading,
|
||||
@@ -684,27 +617,31 @@ export default {
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
|
||||
/* FAB */
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: $sp-xl;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 180rpx);
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
/* 发布入口:标题栏右侧胶囊按钮(替代悬浮 FAB,不遮挡任何内容) */
|
||||
.publish-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-amber;
|
||||
z-index: 50;
|
||||
gap: 6rpx;
|
||||
height: 60rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: $radius-full;
|
||||
background: rgba(232,168,56,0.12);
|
||||
border: 1rpx solid rgba(232,168,56,0.30);
|
||||
transition: transform $duration-fast $ease-out;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
background: rgba(232,168,56,0.20);
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: 44rpx;
|
||||
.publish-btn-icon {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.publish-btn-text {
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
font-weight: $fw-bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,11 +80,32 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 参与者 -->
|
||||
<view class="evt-participants" v-if="event.participants && event.participants.length">
|
||||
<text class="evt-section-label">已报名 ({{ event.participants.length }})</text>
|
||||
<!-- 待审核报名(发起人可见,可同意/拒绝) -->
|
||||
<view class="evt-pending" v-if="event.isOrganizer && pendingParticipants.length">
|
||||
<text class="evt-section-label">待审核报名 ({{ pendingParticipants.length }})</text>
|
||||
<view class="pending-item" v-for="p in pendingParticipants" :key="p.id">
|
||||
<view class="pending-user">
|
||||
<view class="evt-person-avatar">
|
||||
<text class="evt-person-text">{{ p.nickname[0] }}</text>
|
||||
</view>
|
||||
<text class="pending-name">{{ p.nickname }}</text>
|
||||
</view>
|
||||
<view class="pending-actions">
|
||||
<view class="pending-btn pending-approve" @click="handleApprove(p, 2)">
|
||||
<text class="pending-btn-text">✓ 同意</text>
|
||||
</view>
|
||||
<view class="pending-btn pending-reject" @click="handleApprove(p, 3)">
|
||||
<text class="pending-btn-text">✕ 拒绝</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 参与者(已通过审核) -->
|
||||
<view class="evt-participants" v-if="approvedParticipants.length">
|
||||
<text class="evt-section-label">已报名 ({{ approvedParticipants.length }})</text>
|
||||
<view class="evt-people-grid">
|
||||
<view class="evt-person" v-for="p in event.participants" :key="p.id">
|
||||
<view class="evt-person" v-for="p in approvedParticipants" :key="p.id">
|
||||
<view class="evt-person-avatar">
|
||||
<text class="evt-person-text">{{ p.nickname[0] }}</text>
|
||||
</view>
|
||||
@@ -97,21 +118,33 @@
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="evt-action-bar" v-if="event">
|
||||
<button
|
||||
v-if="canJoin && !event.isJoined && !event.isOrganizer"
|
||||
v-if="canJoin && !event.isOrganizer && (!event.isJoined || myApprove === 3)"
|
||||
class="evt-btn evt-btn-primary"
|
||||
@click="handleJoin"
|
||||
>
|
||||
报名参加
|
||||
{{ myApprove === 3 ? '重新报名' : '报名参加' }}
|
||||
</button>
|
||||
<view
|
||||
v-if="event.isJoined && myApprove === 1 && !event.isOrganizer"
|
||||
class="evt-ended-tip"
|
||||
>
|
||||
<text class="evt-ended-text">⏳ 已提交报名,等待发起人审核</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="event.isJoined && myApprove === 3 && !event.isOrganizer"
|
||||
class="evt-ended-tip"
|
||||
>
|
||||
<text class="evt-ended-text">报名未通过,可重新申请</text>
|
||||
</view>
|
||||
<button
|
||||
v-if="canJoin && event.isJoined && !event.isOrganizer"
|
||||
v-if="canJoin && event.isJoined && myApprove !== 3 && !event.isOrganizer"
|
||||
class="evt-btn evt-btn-ghost"
|
||||
@click="handleQuit"
|
||||
>
|
||||
取消报名
|
||||
</button>
|
||||
<button
|
||||
v-if="statusKey === 'ongoing' && event.isJoined"
|
||||
v-if="statusKey === 'ongoing' && event.isJoined && myApprove === 2"
|
||||
class="evt-btn evt-btn-primary"
|
||||
@click="handleCheckIn"
|
||||
>
|
||||
@@ -134,7 +167,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent, DeleteEvent } from '../../common/api'
|
||||
import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent, DeleteEvent, ApproveEvent } from '../../common/api'
|
||||
import wsManager from '../../common/websocket'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
export default {
|
||||
@@ -159,6 +193,23 @@ export default {
|
||||
canJoin() {
|
||||
return this.statusKey === 'open' || this.statusKey === 'upcoming'
|
||||
},
|
||||
/** 我的报名审核状态:1=待审核 2=通过 3=拒绝(兼容后端未返回字段的旧数据) */
|
||||
myApprove() {
|
||||
if (!this.event) return 0
|
||||
const raw = this.event.approveStatus
|
||||
if (raw !== undefined && raw !== null && raw !== '') return Number(raw)
|
||||
return this.event.isJoined ? 2 : 0
|
||||
},
|
||||
/** 待审核报名列表(发起人审核用) */
|
||||
pendingParticipants() {
|
||||
if (!this.event || !this.event.participants) return []
|
||||
return this.event.participants.filter(p => Number(p.approveStatus) === 1)
|
||||
},
|
||||
/** 已通过审核的参与者(无字段的旧数据默认视为已通过) */
|
||||
approvedParticipants() {
|
||||
if (!this.event || !this.event.participants) return []
|
||||
return this.event.participants.filter(p => Number(p.approveStatus) !== 1)
|
||||
},
|
||||
statusLabel() {
|
||||
const map = { open: '报名中', upcoming: '待开始', ongoing: '进行中', ended: '已结束' }
|
||||
return map[this.statusKey] || '报名中'
|
||||
@@ -224,11 +275,46 @@ export default {
|
||||
async handleJoin() {
|
||||
try {
|
||||
await JoinEvent({ eventId: this.eventId })
|
||||
this.event.isJoined = true
|
||||
this.event.joined++
|
||||
uni.showToast({ title: '报名成功 🎉', icon: 'none' })
|
||||
uni.showToast({ title: '申请已提交,等待发起人审核', icon: 'none', duration: 2500 })
|
||||
// 后端不会自动推送通知,前端主动给发起人发一条私信,
|
||||
// 使其在消息列表(chat-list)看到未读提醒
|
||||
this.notifyOrganizer()
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '报名失败', icon: 'none' })
|
||||
// httpRequest 已全局弹错误提示
|
||||
}
|
||||
},
|
||||
/** 通过 WebSocket 给发起人发送报名提醒私信 */
|
||||
notifyOrganizer() {
|
||||
const organizer = this.event && this.event.organizer
|
||||
if (!organizer || organizer.id === undefined || organizer.id === null) return
|
||||
wsManager.send('chat', {
|
||||
receiverId: organizer.id,
|
||||
content: `我报名了你的酒局「${this.event.title}」,请审核 🍻`,
|
||||
msgType: 'text',
|
||||
clientMesgId: 'msg_' + Date.now()
|
||||
})
|
||||
},
|
||||
/** 发起人审核报名:approve 2=同意 3=拒绝 */
|
||||
async handleApprove(user, approve) {
|
||||
try {
|
||||
await ApproveEvent({ eventId: this.eventId, userId: user.id, approve })
|
||||
uni.showToast({ title: approve === 2 ? '已同意报名' : '已拒绝报名', icon: 'none' })
|
||||
// 审核结果私信通知报名用户
|
||||
if (user && user.id !== undefined && user.id !== null) {
|
||||
const resultText = approve === 2
|
||||
? `你报名的酒局「${this.event.title}」已通过审核 🎉`
|
||||
: `很遗憾,你报名的酒局「${this.event.title}」未通过审核`
|
||||
wsManager.send('chat', {
|
||||
receiverId: user.id,
|
||||
content: resultText,
|
||||
msgType: 'text',
|
||||
clientMesgId: 'msg_' + Date.now()
|
||||
})
|
||||
}
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
// httpRequest 已全局弹错误提示
|
||||
}
|
||||
},
|
||||
async handleQuit() {
|
||||
@@ -241,11 +327,10 @@ export default {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await QuitEvent({ eventId: this.eventId })
|
||||
this.event.isJoined = false
|
||||
this.event.joined--
|
||||
uni.showToast({ title: '已取消报名', icon: 'none' })
|
||||
this.loadDetail()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
// httpRequest 已全局弹错误提示
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,4 +669,70 @@ export default {
|
||||
color: $coral;
|
||||
border: 1rpx solid rgba(255,107,107,0.25);
|
||||
}
|
||||
|
||||
/* 待审核报名 */
|
||||
.evt-pending {
|
||||
margin-bottom: $sp-xl;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $sp-md $sp-lg;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid rgba(232,168,56,0.25);
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $sp-sm;
|
||||
}
|
||||
|
||||
.pending-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pending-name {
|
||||
font-size: $fs-base;
|
||||
font-weight: $fw-medium;
|
||||
color: $text-primary;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pending-actions {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pending-btn {
|
||||
padding: 10rpx 24rpx;
|
||||
border-radius: $radius-full;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.pending-approve {
|
||||
background: linear-gradient(135deg, $amber, $amber-deep);
|
||||
}
|
||||
|
||||
.pending-reject {
|
||||
background: rgba(255,107,107,0.1);
|
||||
border: 1rpx solid rgba(255,107,107,0.25);
|
||||
}
|
||||
|
||||
.pending-btn-text {
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-on-amber;
|
||||
|
||||
.pending-reject & {
|
||||
color: $coral;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+190
-22
@@ -32,10 +32,12 @@
|
||||
<text class="stat-label">标准杯</text>
|
||||
</view>
|
||||
<view class="stat-card card">
|
||||
<text class="stat-value text-num" :class="stats.streakType === 'drank' ? 'text-amber' : 'text-mint'">
|
||||
{{ stats.streak }}
|
||||
</text>
|
||||
<text class="stat-label">{{ stats.streakType === 'drank' ? '连喝天数' : '戒酒天数' }}</text>
|
||||
<text class="stat-value text-num text-amber">{{ stats.drankStreak }}</text>
|
||||
<text class="stat-label">连喝天数</text>
|
||||
</view>
|
||||
<view class="stat-card card">
|
||||
<text class="stat-value text-num text-mint">{{ stats.abstainStreak }}</text>
|
||||
<text class="stat-label">戒酒天数</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -83,9 +85,10 @@
|
||||
<!-- 日期记录弹窗 -->
|
||||
<view v-if="showDayDetail" class="day-overlay" @click="showDayDetail = false">
|
||||
<view class="day-detail card-elevated" @click.stop>
|
||||
<view class="day-detail-handle"></view>
|
||||
<view class="day-detail-header flex-between">
|
||||
<text class="text-h3">{{ selectedDate }}</text>
|
||||
<text class="btn-text" @click="showDayDetail = false">关闭</text>
|
||||
<text class="day-detail-date text-h3">{{ selectedDate }}</text>
|
||||
<text class="day-close btn-text" @click="showDayDetail = false">关闭</text>
|
||||
</view>
|
||||
<view v-if="selectedRecord">
|
||||
<view v-if="selectedRecord.mode === 'drank'" class="day-record-info">
|
||||
@@ -101,9 +104,14 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="day-meta flex-between">
|
||||
<text class="text-caption">{{ getFeelingName(selectedRecord.feeling) }}</text>
|
||||
<text class="text-caption text-amber text-num">{{ selectedRecord.standardCupsTotal }} 标准杯</text>
|
||||
<view class="day-meta">
|
||||
<view class="day-meta-chip day-feeling-chip">
|
||||
<text class="day-feeling-text">{{ getFeelingName(selectedRecord.feeling) }}</text>
|
||||
</view>
|
||||
<view class="day-meta-chip day-cups-chip">
|
||||
<text class="day-cups-num text-num">{{ selectedRecord.standardCupsTotal }}</text>
|
||||
<text class="day-cups-unit">标准杯</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="day-action-row">
|
||||
<button class="btn-ghost day-action-btn" @click="editRecord">修改</button>
|
||||
@@ -120,8 +128,9 @@
|
||||
</view>
|
||||
<view v-else class="day-empty">
|
||||
<text class="text-caption">当天无记录</text>
|
||||
<button v-if="isSelectedToday" class="btn-primary day-record-btn" @click="goRecordToday">去记录</button>
|
||||
<button v-else class="btn-ghost" @click="backfillDate">补打卡</button>
|
||||
<button v-if="isSelectedToday" class="btn-primary day-record-btn" @click="goRecordToday">记录饮酒</button>
|
||||
<button v-else class="btn-ghost day-record-btn" @click="backfillDate">补打卡饮酒</button>
|
||||
<button class="btn-ghost day-abstain-btn" @click="markAbstain">未饮酒</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -132,7 +141,7 @@
|
||||
import DrinkCalendar from '../../components/DrinkCalendar.vue'
|
||||
import client from '../../common/api'
|
||||
import { GetCalendarReq, DeleteRecordReq, CreateRecordReq, GetRecordDetailReq } from 'HaveADrink'
|
||||
import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath } from '../../common/utils'
|
||||
import { getGreeting, formatDate, getWeekStart, getCatIcon, isIconPath, calcLocalStreak } from '../../common/utils'
|
||||
import { FEELINGS, DRINK_CATEGORIES, DRINK_UNITS } from '../../common/constants'
|
||||
import { savePendingInvite, handlePendingInvite } from '../../common/invite'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
@@ -236,6 +245,8 @@ export default {
|
||||
favCategory: s.favCategory ? String(s.favCategory) : null,
|
||||
categoryVariety: s.categoryVariety || 0
|
||||
}
|
||||
// 后端 streak 不区分饮酒/戒酒模式,拉取记录本地重算修正
|
||||
this.refreshLocalStreak()
|
||||
}
|
||||
|
||||
// 日历数据
|
||||
@@ -270,6 +281,22 @@ export default {
|
||||
// 切换月份时重新加载记录
|
||||
this.loadRecordsForMonth(year, month)
|
||||
},
|
||||
/** 后端 streak 不区分饮酒/戒酒模式,拉取全部记录后本地重算连续天数 */
|
||||
async refreshLocalStreak() {
|
||||
try {
|
||||
const resp = await client.GetRecords({ page: 1, pageSize: 100 })
|
||||
const payload = resp.data || resp
|
||||
const list = payload.list || []
|
||||
if (!list.length) return
|
||||
const { streak, streakType, drankStreak, abstainStreak } = calcLocalStreak(list)
|
||||
this.stats.streak = streak
|
||||
this.stats.streakType = streakType
|
||||
this.stats.drankStreak = drankStreak
|
||||
this.stats.abstainStreak = abstainStreak
|
||||
} catch (e) {
|
||||
console.warn('本地计算连续天数失败:', e)
|
||||
}
|
||||
},
|
||||
async loadRecordsForMonth(year, month) {
|
||||
try {
|
||||
const resp = await client.GetCalendar(new GetCalendarReq(year, month))
|
||||
@@ -415,7 +442,31 @@ export default {
|
||||
this.showDayDetail = false
|
||||
uni.navigateTo({ url: '/pages/record/record' })
|
||||
},
|
||||
|
||||
// 标记为未饮酒(直接创建 abstain 记录)
|
||||
async markAbstain() {
|
||||
const recordData = {
|
||||
date: this.selectedDate,
|
||||
mode: 'abstain',
|
||||
drinks: [],
|
||||
food: null,
|
||||
feeling: null,
|
||||
photos: [],
|
||||
visibility: 'friends',
|
||||
quote: ''
|
||||
}
|
||||
try {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
await client.CreateRecord(recordData)
|
||||
uni.hideLoading()
|
||||
this.showDayDetail = false
|
||||
uni.showToast({ title: '已记录未饮酒', icon: 'success' })
|
||||
this.loadData()
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
console.warn('标记未饮酒失败:', e)
|
||||
uni.showToast({ title: (e && e.msg) || '保存失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -434,7 +485,7 @@ export default {
|
||||
|
||||
.greeting-text {
|
||||
display: block;
|
||||
font-size:38rpx;
|
||||
font-size:42rpx;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
@@ -632,24 +683,83 @@ export default {
|
||||
align-items: flex-end;
|
||||
z-index: 100;
|
||||
padding: $sp-lg;
|
||||
animation: overlayIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes overlayIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.day-detail {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: $sp-xl;
|
||||
padding: $sp-md $sp-xl $sp-xl;
|
||||
color: $text-primary;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
border-radius: $radius-xl;
|
||||
/* 背景层次:底部卡片色 + 顶部琥珀光晕,日夜主题自适应 */
|
||||
background:
|
||||
radial-gradient(120% 60% at 50% 0%, var(--amber-glow, rgba(232, 168, 56, 0.15)) 0%, transparent 60%),
|
||||
linear-gradient(180deg, var(--bg-elevated, #28283E) 0%, var(--bg-card, #1A1A28) 55%);
|
||||
border: 2rpx solid var(--border-faint, rgba(255, 255, 255, 0.08));
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: sheetIn 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@keyframes sheetIn {
|
||||
from { opacity: 0; transform: translateY(48rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* 顶部拖拽把手 */
|
||||
.day-detail-handle {
|
||||
width: 64rpx;
|
||||
height: 8rpx;
|
||||
border-radius: $radius-full;
|
||||
background: var(--border-active, rgba(255, 255, 255, 0.12));
|
||||
margin: 0 auto $sp-md;
|
||||
}
|
||||
|
||||
.day-detail-header {
|
||||
margin-bottom: $sp-lg;
|
||||
}
|
||||
|
||||
.day-detail-date {
|
||||
position: relative;
|
||||
padding-left: $sp-md;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 70%;
|
||||
border-radius: $radius-full;
|
||||
background: linear-gradient(180deg, var(--amber-light, #F5C563), var(--amber-deep, #C47F17));
|
||||
}
|
||||
}
|
||||
|
||||
.day-close {
|
||||
padding: $sp-xs $sp-sm;
|
||||
}
|
||||
|
||||
.day-drinks {
|
||||
background: var(--bg-subtle, rgba(255, 255, 255, 0.03));
|
||||
border-radius: $radius-md;
|
||||
padding: $sp-xs $sp-md;
|
||||
}
|
||||
|
||||
.day-drink-item {
|
||||
padding: $sp-sm 0;
|
||||
padding: $sp-md 0;
|
||||
font-size: $fs-base;
|
||||
color: $text-primary;
|
||||
border-bottom: 2rpx solid var(--border-micro, rgba(255, 255, 255, 0.05));
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.day-drink-row {
|
||||
@@ -680,9 +790,44 @@ export default {
|
||||
}
|
||||
|
||||
.day-meta {
|
||||
display: flex;
|
||||
gap: $sp-sm;
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
|
||||
.day-meta-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
padding: $sp-sm $sp-md;
|
||||
border-radius: $radius-full;
|
||||
}
|
||||
|
||||
.day-feeling-chip {
|
||||
background: var(--bg-hover, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
.day-feeling-text {
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.day-cups-chip {
|
||||
background: var(--amber-glow, rgba(232, 168, 56, 0.15));
|
||||
border: 2rpx solid rgba(232, 168, 56, 0.25);
|
||||
}
|
||||
|
||||
.day-cups-num {
|
||||
font-size: $fs-lg;
|
||||
font-weight: $fw-bold;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.day-cups-unit {
|
||||
font-size: $fs-xs;
|
||||
color: $amber;
|
||||
}
|
||||
|
||||
.day-abstain {
|
||||
text-align: center;
|
||||
padding: $sp-xl 0;
|
||||
@@ -702,6 +847,12 @@ export default {
|
||||
margin-top: $sp-lg;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.day-abstain-btn {
|
||||
color: $mint;
|
||||
border-color: rgba(94, 198, 160, 0.35);
|
||||
background: rgba(94, 198, 160, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.day-action-row {
|
||||
@@ -710,20 +861,37 @@ export default {
|
||||
margin-top: $sp-lg;
|
||||
}
|
||||
|
||||
/* 修改:琥珀主操作 */
|
||||
.day-action-btn {
|
||||
flex: 1;
|
||||
font-size: $fs-sm;
|
||||
color: $text-secondary;
|
||||
border-color: var(--border-active, rgba(255, 255, 255, 0.12));
|
||||
|
||||
&:first-child {
|
||||
color: $text-on-amber;
|
||||
background: linear-gradient(135deg, var(--amber-light, #F5C563), var(--amber-deep, #C47F17));
|
||||
border: none;
|
||||
box-shadow: var(--shadow-amber);
|
||||
}
|
||||
|
||||
/* 改为未饮酒:薄荷淡底 */
|
||||
&:last-child {
|
||||
color: $mint;
|
||||
background: rgba(94, 198, 160, 0.08);
|
||||
border: 2rpx solid rgba(94, 198, 160, 0.30);
|
||||
}
|
||||
}
|
||||
|
||||
.day-delete-btn {
|
||||
margin-top: $sp-md;
|
||||
width: 100%;
|
||||
font-size: $fs-sm;
|
||||
color: #e85d3a;
|
||||
background: rgba(232, 93, 58, 0.08);
|
||||
border: 2rpx solid rgba(232, 93, 58, 0.2);
|
||||
color: #E85D3A;
|
||||
background: rgba(232, 93, 58, 0.06);
|
||||
border: 2rpx solid rgba(232, 93, 58, 0.30);
|
||||
|
||||
&:active {
|
||||
background: rgba(232, 93, 58, 0.14);
|
||||
}
|
||||
}
|
||||
|
||||
.day-toggle-btn {
|
||||
|
||||
+34
-12
@@ -81,9 +81,9 @@
|
||||
<!-- 底部协议(非登录状态时显示) -->
|
||||
<view v-if="showNotice" class="agreement">
|
||||
<text class="text-tiny">登录即代表同意</text>
|
||||
<text class="text-tiny text-amber">《用户协议》</text>
|
||||
<text class="text-tiny text-amber" @click="openAgreement('user')">《用户协议》</text>
|
||||
<text class="text-tiny">和</text>
|
||||
<text class="text-tiny text-amber">《隐私政策》</text>
|
||||
<text class="text-tiny text-amber" @click="openAgreement('privacy')">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -149,6 +149,8 @@ export default {
|
||||
return
|
||||
}
|
||||
this.loginLoading = true
|
||||
let loginOk = false
|
||||
let loginErr = null
|
||||
try {
|
||||
// 1. 获取微信登录 code
|
||||
const loginRes = await this.wxLogin()
|
||||
@@ -162,6 +164,13 @@ export default {
|
||||
avatarUrl: this.avatarUrl || ''
|
||||
}
|
||||
const resp = await client.WechatLogin(loginParams)
|
||||
// errcode 兜底:后端业务错误码非 0 时视为登录失败
|
||||
if (resp && resp.errcode && resp.errcode !== 0) {
|
||||
throw { msg: resp.errmsg || '登录失败' }
|
||||
}
|
||||
if (!resp || !resp.token) {
|
||||
throw { msg: '登录响应缺少 token' }
|
||||
}
|
||||
|
||||
// 3. 保存 token 和用户信息
|
||||
saveAuthTokens(resp)
|
||||
@@ -171,11 +180,16 @@ export default {
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_guest', 'false')
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
|
||||
this.finishLogin()
|
||||
loginOk = true
|
||||
} catch (e) {
|
||||
console.warn('后端登录失败,降级为本地模式:', e)
|
||||
uni.showToast({ title: e.msg || '登录失败,使用本地模式', icon: 'none', duration: 2000 })
|
||||
// 打印完整错误,方便定位具体失败环节
|
||||
console.warn('后端登录失败,降级为本地模式:', e && e.stack ? e.stack : e)
|
||||
loginErr = e
|
||||
}
|
||||
|
||||
if (!loginOk) {
|
||||
const msg = (loginErr && (loginErr.msg || loginErr.message)) || ''
|
||||
uni.showToast({ title: msg ? `登录失败:${msg}` : '登录失败,使用本地模式', icon: 'none', duration: 2000 })
|
||||
// 降级为本地登录
|
||||
const userInfo = {
|
||||
id: `user_${Date.now()}`,
|
||||
@@ -187,10 +201,11 @@ export default {
|
||||
uni.setStorageSync('user_info', JSON.stringify(userInfo))
|
||||
uni.setStorageSync('is_logged_in', 'true')
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
this.finishLogin()
|
||||
} finally {
|
||||
this.loginLoading = false
|
||||
}
|
||||
this.loginLoading = false
|
||||
// 收尾逻辑移出 try/catch:避免 WS 重建/邀请补发等非登录环节异常
|
||||
// 被误判为登录失败(后端已成功却弹"登录失败"的根因)
|
||||
this.finishLogin()
|
||||
},
|
||||
|
||||
// 微信login获取code
|
||||
@@ -209,20 +224,27 @@ export default {
|
||||
uni.setStorageSync('is_first_launch', 'false')
|
||||
// 登录完成后用新 token 重建 WebSocket 连接
|
||||
// (启动时可能用旧 token 连接被拒,不重建会一直用旧 token 重试)
|
||||
try {
|
||||
const token = uni.getStorageSync('auth_token')
|
||||
if (token) {
|
||||
wsManager.disconnect()
|
||||
wsManager.connect(token)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('WS 重建失败,不影响登录:', e)
|
||||
}
|
||||
// 登录完成后补发分享邀请的好友请求(若有暂存的邀请人)
|
||||
try {
|
||||
handlePendingInvite()
|
||||
} catch (e) {
|
||||
console.warn('补发邀请失败,不影响登录:', e)
|
||||
}
|
||||
uni.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
|
||||
// ===== 协议查看 =====
|
||||
// ===== 协议查看:跳转协议页(user=用户协议 / privacy=隐私政策)=====
|
||||
openAgreement(type) {
|
||||
const title = type === 'user' ? '用户协议' : '隐私政策'
|
||||
uni.showToast({ title: `${title}页面开发中`, icon: 'none' })
|
||||
uni.navigateTo({ url: `/pages/agreement/agreement?type=${type}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+72
-49
@@ -9,7 +9,7 @@
|
||||
</view>
|
||||
<!-- 右上角分享按钮(避让胶囊) -->
|
||||
<button class="hero-share-btn" open-type="share" :style="shareBtnStyle">
|
||||
<text class="hero-share-icon">↗</text>
|
||||
<text class="hero-share-icon">分享</text>
|
||||
</button>
|
||||
<view class="hero-content">
|
||||
<view class="avatar-ring">
|
||||
@@ -71,25 +71,33 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连续打卡卡片(含连续戒酒状态,暂时注释隐藏)
|
||||
<view class="streak-section">
|
||||
<view class="streak-card" :class="stats.streakType === 'drank' ? 'streak-drank' : 'streak-abstain'">
|
||||
<view class="streak-left">
|
||||
<view class="streak-badge">
|
||||
<text class="streak-badge-emoji">{{ stats.streakType === 'drank' ? '🔥' : '💪' }}</text>
|
||||
<view class="streak-row">
|
||||
<view class="streak-card streak-drank">
|
||||
<view class="streak-card-top">
|
||||
<text class="streak-badge-emoji">🔥</text>
|
||||
<text class="streak-title">连续饮酒</text>
|
||||
</view>
|
||||
<view class="streak-info">
|
||||
<text class="streak-title">{{ stats.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }}</text>
|
||||
<text class="streak-sub">{{ stats.streakType === 'drank' ? '保持记录,理性饮酒' : '自律即自由,继续坚持' }}</text>
|
||||
<text class="streak-sub">保持记录,理性饮酒</text>
|
||||
<view class="streak-num-row">
|
||||
<text class="streak-days text-num">{{ stats.drankStreak || 0 }}</text>
|
||||
<text class="streak-days-unit">天</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="streak-right">
|
||||
<text class="streak-days text-num">{{ stats.streak }}</text>
|
||||
<view class="streak-card streak-abstain">
|
||||
<view class="streak-card-top">
|
||||
<text class="streak-badge-emoji">💪</text>
|
||||
<text class="streak-title">连续戒酒</text>
|
||||
</view>
|
||||
<text class="streak-sub">自律即自由,继续坚持</text>
|
||||
<view class="streak-num-row">
|
||||
<text class="streak-days text-num">{{ stats.abstainStreak || 0 }}</text>
|
||||
<text class="streak-days-unit">天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
-->
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 饮酒人格卡片 -->
|
||||
<view class="persona-card">
|
||||
@@ -166,7 +174,7 @@
|
||||
<script>
|
||||
import AchievementBadge from '../../components/AchievementBadge.vue'
|
||||
import client, { clearAuth } from '../../common/api'
|
||||
import { checkAchievements, getCatIcon, isIconPath } from '../../common/utils'
|
||||
import { checkAchievements, getCatIcon, isIconPath, calcLocalStreak } from '../../common/utils'
|
||||
import { ACHIEVEMENTS, DRINK_CATEGORIES } from '../../common/constants'
|
||||
import themeMixin from '../../common/theme-mixin'
|
||||
|
||||
@@ -309,6 +317,8 @@ export default {
|
||||
favCategory: s.favCategory ? String(s.favCategory) : null,
|
||||
categoryVariety: s.categoryVariety || 0
|
||||
}
|
||||
// 后端 streak 不区分饮酒/戒酒模式,拉取记录本地重算修正
|
||||
this.refreshLocalStreak()
|
||||
}
|
||||
|
||||
// 成就列表
|
||||
@@ -346,6 +356,22 @@ export default {
|
||||
this.detailAchievement = a
|
||||
this.showDetail = true
|
||||
},
|
||||
/** 后端 streak 不区分饮酒/戒酒模式,拉取全部记录后本地重算连续天数 */
|
||||
async refreshLocalStreak() {
|
||||
try {
|
||||
const resp = await client.GetRecords({ page: 1, pageSize: 100 })
|
||||
const payload = resp.data || resp
|
||||
const list = payload.list || []
|
||||
if (!list.length) return
|
||||
const { streak, streakType, drankStreak, abstainStreak } = calcLocalStreak(list)
|
||||
this.stats.streak = streak
|
||||
this.stats.streakType = streakType
|
||||
this.stats.drankStreak = drankStreak
|
||||
this.stats.abstainStreak = abstainStreak
|
||||
} catch (e) {
|
||||
console.warn('本地计算连续天数失败:', e)
|
||||
}
|
||||
},
|
||||
// 后端返回的 icon 是无效图片路径,按索引复用本地成就的 emoji 图标(超出后循环取)
|
||||
resolveAchievementIcon(index) {
|
||||
if (!ACHIEVEMENTS.length) return '🏅'
|
||||
@@ -417,30 +443,36 @@ export default {
|
||||
.hero-share-btn {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
padding: 0 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border: 1rpx solid rgba(255,255,255,0.10);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10px);
|
||||
/* 玻璃模态效果:半透明白 + 强模糊 + 高光描边 */
|
||||
background: rgba(255,255,255,0.14);
|
||||
border: 1rpx solid rgba(255,255,255,0.30);
|
||||
border-radius: $radius-full;
|
||||
backdrop-filter: blur(20px) saturate(160%);
|
||||
box-shadow: inset 0 1rpx 0 rgba(255,255,255,0.35), 0 8rpx 24rpx rgba(0,0,0,0.18);
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(255,255,255,0.12);
|
||||
transform: scale(0.9);
|
||||
background: rgba(255,255,255,0.22);
|
||||
transform: scale(0.94);
|
||||
}
|
||||
}
|
||||
|
||||
/* 分享胶囊按钮文字:与微信胶囊同构 */
|
||||
.hero-share-icon {
|
||||
font-size: $fs-base;
|
||||
color: $text-secondary;
|
||||
font-size: $fs-sm;
|
||||
color: $amber;
|
||||
font-weight: $fw-bold;
|
||||
line-height: 1;
|
||||
letter-spacing: 4rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
@@ -610,12 +642,17 @@ export default {
|
||||
padding: $sp-lg $sp-lg 0;
|
||||
}
|
||||
|
||||
.streak-card {
|
||||
.streak-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $sp-xl;
|
||||
border-radius: $radius-xl;
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.streak-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $sp-lg;
|
||||
border-radius: $radius-lg;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -630,34 +667,19 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.streak-left {
|
||||
.streak-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-md;
|
||||
}
|
||||
|
||||
.streak-badge {
|
||||
width: 88rpx; height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: $bg-card;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-sm;
|
||||
gap: $sp-xs;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.streak-badge-emoji {
|
||||
font-size: 44rpx;
|
||||
}
|
||||
|
||||
.streak-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.streak-title {
|
||||
font-size: $fs-base;
|
||||
font-size: $fs-sm;
|
||||
font-weight: $fw-bold;
|
||||
color: $text-primary;
|
||||
}
|
||||
@@ -665,16 +687,17 @@ export default {
|
||||
.streak-sub {
|
||||
font-size: $fs-xs;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: $sp-md;
|
||||
}
|
||||
|
||||
.streak-right {
|
||||
.streak-num-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.streak-days {
|
||||
font-size: $fs-hero;
|
||||
font-size: $fs-3xl;
|
||||
font-weight: $fw-black;
|
||||
color: $amber;
|
||||
line-height: 1;
|
||||
|
||||
Reference in New Issue
Block a user