feat(event): 添加酒局报名审核功能并优化前端组件
- 新增 ApproveEvent API 接口用于审核报名用户
- 在 EventCard 组件中显示待审核状态(⏳ 待审核)
- 实现酒局发起人审核报名用户的完整流程
- 添加用户协议和隐私政策页面
- 优化 FeedCard 组件中的图片展示逻辑
- 修复连续打卡天数计算问题
- 更新 HaveADrink SDK 至 1.0.15 版本
This commit is contained in:
@@ -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) {
|
||||
|
||||
+38
-101
@@ -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,11 +344,7 @@ export default {
|
||||
},
|
||||
// === FAB ===
|
||||
handleFab() {
|
||||
if (this.currentTab === 2) {
|
||||
this.goCreateEvent()
|
||||
} else {
|
||||
this.goPublish()
|
||||
}
|
||||
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 {
|
||||
|
||||
+39
-17
@@ -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 重试)
|
||||
const token = uni.getStorageSync('auth_token')
|
||||
if (token) {
|
||||
wsManager.disconnect()
|
||||
wsManager.connect(token)
|
||||
try {
|
||||
const token = uni.getStorageSync('auth_token')
|
||||
if (token) {
|
||||
wsManager.disconnect()
|
||||
wsManager.connect(token)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('WS 重建失败,不影响登录:', e)
|
||||
}
|
||||
// 登录完成后补发分享邀请的好友请求(若有暂存的邀请人)
|
||||
handlePendingInvite()
|
||||
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}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+73
-50
@@ -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>
|
||||
<text class="streak-days-unit">天</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