feat(event): 添加酒局报名审核功能并优化前端组件

- 新增 ApproveEvent API 接口用于审核报名用户
- 在 EventCard 组件中显示待审核状态(⏳ 待审核)
- 实现酒局发起人审核报名用户的完整流程
- 添加用户协议和隐私政策页面
- 优化 FeedCard 组件中的图片展示逻辑
- 修复连续打卡天数计算问题
- 更新 HaveADrink SDK 至 1.0.15 版本
This commit is contained in:
cg
2026-08-16 14:50:50 +08:00
parent 527b0e8a8d
commit 81e7a3afa5
20 changed files with 1470 additions and 249 deletions
+167 -16
View File
@@ -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>