Files
hejiu/pages/event-detail/event-detail.vue
T
cg d335ec290a refactor(app): 重构应用主题和页面结构
- 移除动态主题切换功能,改为全局统一深色主题
- 实现自定义TabBar组件替换原生tabBar,解决iOS闪白问题
- 创建main容器页面统一管理三个tab页面的生命周期和状态
- 将所有页面的onShow/onHide等生命周期方法迁移到子组件内部
- 更新页面路径从index改为main,并调整路由跳转逻辑
- 移除theme-mixin和相关主题工具函数
- 统一页面背景色设置,优化用户体验一致性
2026-09-23 20:56:14 +08:00

737 lines
19 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container evt-detail-page">
<!-- 顶部导航 -->
<view class="evt-nav" :style="{ paddingTop: navPaddingTop }">
<view class="evt-nav-inner">
<view class="evt-back" @click="goBack">
<text class="evt-back-icon">‹</text>
</view>
<text class="evt-nav-title">酒局详情</text>
<view class="evt-nav-ph"></view>
</view>
</view>
<scroll-view class="evt-scroll" scroll-y>
<view class="evt-body" v-if="event">
<!-- 状态 -->
<view class="evt-status" :class="'status-' + statusKey">
<text class="evt-status-text">{{ statusLabel }}</text>
</view>
<!-- 标题 -->
<text class="evt-title">{{ event.title }}</text>
<!-- 信息卡片 -->
<view class="evt-info-card">
<view class="evt-info-row">
<text class="evt-info-icon">🕐</text>
<view class="evt-info-content">
<text class="evt-info-label">时间</text>
<text class="evt-info-value">{{ event.time }}</text>
</view>
</view>
<view class="evt-info-row">
<text class="evt-info-icon">📍</text>
<view class="evt-info-content">
<text class="evt-info-label">地点</text>
<text class="evt-info-value">{{ event.location }}</text>
</view>
</view>
<view class="evt-info-row">
<text class="evt-info-icon">👥</text>
<view class="evt-info-content">
<text class="evt-info-label">人数</text>
<text class="evt-info-value">{{ event.joined }}/{{ event.maxPeople }} 人已报名</text>
</view>
</view>
<view class="evt-info-row" v-if="event.note">
<text class="evt-info-icon">📝</text>
<view class="evt-info-content">
<text class="evt-info-label">备注</text>
<text class="evt-info-value">{{ event.note }}</text>
</view>
</view>
</view>
<!-- 地图展示 -->
<view class="evt-map-section" v-if="event.latitude">
<map
class="evt-map"
:latitude="event.latitude"
:longitude="event.longitude"
:markers="eventMarkers"
:scale="15"
:show-location="true"
></map>
<view class="evt-map-nav-btn" @click="openNavigation">
<text class="evt-map-nav-icon">🧭</text>
<text class="evt-map-nav-text">导航到这里</text>
</view>
</view>
<!-- 发起人 -->
<view class="evt-organizer">
<text class="evt-section-label">发起人</text>
<view class="evt-org-row">
<view class="evt-org-avatar">
<text class="evt-org-avatar-text">{{ event.organizer ? event.organizer.nickname[0] : '酒' }}</text>
</view>
<text class="evt-org-name">{{ event.organizer ? event.organizer.nickname : '' }}</text>
</view>
</view>
<!-- 待审核报名(发起人可见,可同意/拒绝) -->
<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 approvedParticipants" :key="p.id">
<view class="evt-person-avatar">
<text class="evt-person-text">{{ p.nickname[0] }}</text>
</view>
<text class="evt-person-name">{{ p.nickname }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="evt-action-bar" v-if="event">
<button
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 && myApprove !== 3 && !event.isOrganizer"
class="evt-btn evt-btn-ghost"
@click="handleQuit"
>
取消报名
</button>
<button
v-if="statusKey === 'ongoing' && event.isJoined && myApprove === 2"
class="evt-btn evt-btn-primary"
@click="handleCheckIn"
>
到场签到
</button>
<view v-if="statusKey === 'ended' && !event.isOrganizer" class="evt-ended-tip">
<text class="evt-ended-text">酒局已结束</text>
</view>
<!-- 发起人:编辑/删除自己的酒局 -->
<view v-if="event.isOrganizer" class="evt-org-actions">
<button class="evt-btn evt-btn-ghost" @click="goEdit">
✏️ 编辑酒局
</button>
<button class="evt-btn evt-btn-danger" @click="confirmDelete">
🗑️ 删除酒局
</button>
</view>
</view>
</view>
</template>
<script>
import { GetEventDetail, JoinEvent, QuitEvent, CheckInEvent, DeleteEvent, ApproveEvent } from '../../common/api'
import wsManager from '../../common/websocket'
export default {
data() {
const menuBtn = uni.getMenuButtonBoundingClientRect()
return {
navPaddingTop: menuBtn.top + 'px',
eventId: '',
event: null
}
},
computed: {
/** 后端状态为数字枚举:1=报名中 2=待开始 3=进行中 4=已结束(兼容字符串返回) */
statusKey() {
if (!this.event) return ''
const map = { 1: 'open', 2: 'upcoming', 3: 'ongoing', 4: 'ended' }
const raw = this.event.status
return map[Number(raw)] || String(raw).toLowerCase()
},
/** 报名中/待开始阶段可报名或取消 */
canJoin() {
return this.statusKey === 'open' || this.statusKey === 'upcoming'
},
/** 我的报名审核状态: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] || '报名中'
},
eventMarkers() {
if (!this.event || !this.event.latitude) return []
return [{
id: 1,
latitude: this.event.latitude,
longitude: this.event.longitude,
title: this.event.location,
width: 28,
height: 38
}]
}
},
onLoad(options) {
this.eventId = options.id || ''
this.loadDetail()
},
onShow() {
// 从编辑页返回时刷新详情
if (this.eventId && this._hasLoaded) {
this.loadDetail()
}
},
methods: {
goBack() {
uni.navigateBack()
},
async loadDetail() {
try {
const res = await GetEventDetail({ eventId: this.eventId })
this.event = res.data
this._hasLoaded = true
} catch (e) {
console.warn('加载酒局详情失败', e)
}
},
/** 发起人编辑酒局(复用发起页的编辑模式) */
goEdit() {
uni.navigateTo({ url: `/pages/event-create/event-create?id=${this.eventId}` })
},
/** 发起人删除酒局(二次确认,成功后返回上一页) */
confirmDelete() {
uni.showModal({
title: '删除酒局',
content: '确定要删除这场酒局吗?删除后不可恢复',
confirmText: '删除',
confirmColor: '#FF6B6B',
success: async (res) => {
if (!res.confirm) return
try {
await DeleteEvent({ eventId: this.eventId })
uni.showToast({ title: '已删除', icon: 'none' })
setTimeout(() => uni.navigateBack(), 800)
} catch (e) {
// httpRequest 已全局弹错误提示
}
}
})
},
async handleJoin() {
try {
await JoinEvent({ eventId: this.eventId })
uni.showToast({ title: '申请已提交,等待发起人审核', icon: 'none', duration: 2500 })
// 后端不会自动推送通知,前端主动给发起人发一条私信,
// 使其在消息列表(chat-list)看到未读提醒
this.notifyOrganizer()
this.loadDetail()
} catch (e) {
// 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() {
uni.showModal({
title: '取消报名',
content: '确定要退出这个酒局吗?',
confirmText: '退出',
confirmColor: '#FF6B6B',
success: async (res) => {
if (res.confirm) {
try {
await QuitEvent({ eventId: this.eventId })
uni.showToast({ title: '已取消报名', icon: 'none' })
this.loadDetail()
} catch (e) {
// httpRequest 已全局弹错误提示
}
}
}
})
},
async handleCheckIn() {
try {
await CheckInEvent({ eventId: this.eventId })
uni.showToast({ title: '签到成功 🍻', icon: 'none' })
} catch (e) {
uni.showToast({ title: '签到失败', icon: 'none' })
}
},
openNavigation() {
if (!this.event || !this.event.latitude) return
uni.openLocation({
latitude: this.event.latitude,
longitude: this.event.longitude,
name: this.event.location,
address: this.event.address || this.event.location,
fail: () => {
uni.showToast({ title: '无法打开地图', icon: 'none' })
}
})
}
}
}
</script>
<style lang="scss" scoped>
.evt-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
}
.evt-nav {
background: $bg-base;
padding-left: $sp-lg;
padding-right: $sp-lg;
}
.evt-nav-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
}
.evt-back {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: $bg-card;
&:active { background: $bg-card-alt; }
}
.evt-back-icon {
font-size: $fs-xl;
color: $text-primary;
font-weight: $fw-bold;
}
.evt-nav-title {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.evt-nav-ph {
width: 64rpx;
}
.evt-scroll {
flex: 1;
height: 0;
}
.evt-body {
padding: $sp-lg;
padding-bottom: $sp-xl;
}
.evt-status {
display: inline-flex;
padding: 8rpx 20rpx;
border-radius: $radius-full;
margin-bottom: $sp-lg;
&.status-open { background: rgba(94,198,160,0.12); }
&.status-upcoming { background: rgba(139,133,184,0.12); }
&.status-ongoing { background: rgba(232,168,56,0.12); }
&.status-ended { background: $bg-elevated; }
}
.evt-status-text {
font-size: $fs-sm;
font-weight: $fw-medium;
.status-open & { color: $mint; }
.status-upcoming & { color: $lavender; }
.status-ongoing & { color: $amber; }
.status-ended & { color: $text-tertiary; }
}
.evt-title {
display: block;
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
margin-bottom: $sp-xl;
line-height: $lh-tight;
}
/* 信息卡片 */
.evt-info-card {
background: $bg-card;
border-radius: $radius-xl;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
padding: $sp-xl;
margin-bottom: $sp-xl;
}
.evt-info-row {
display: flex;
align-items: flex-start;
gap: $sp-md;
padding: $sp-md 0;
&:not(:last-child) {
border-bottom: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
}
}
.evt-info-icon {
font-size: $fs-base;
width: 40rpx;
text-align: center;
flex-shrink: 0;
margin-top: 4rpx;
}
.evt-info-content {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.evt-info-label {
font-size: $fs-xs;
color: $text-tertiary;
}
.evt-info-value {
font-size: $fs-base;
color: $text-primary;
font-weight: $fw-medium;
}
/* 地图区域 */
.evt-map-section {
margin-bottom: $sp-xl;
border-radius: $radius-xl;
overflow: hidden;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
.evt-map {
width: 100%;
height: 320rpx;
}
.evt-map-nav-btn {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-sm;
padding: $sp-md;
background: $bg-card;
&:active {
background: $bg-card-alt;
}
}
.evt-map-nav-icon {
font-size: $fs-base;
}
.evt-map-nav-text {
font-size: $fs-base;
color: $amber;
font-weight: $fw-bold;
}
/* 发起人 & 参与者 */
.evt-section-label {
display: block;
font-size: $fs-sm;
color: $text-tertiary;
font-weight: $fw-medium;
margin-bottom: $sp-md;
}
.evt-organizer {
margin-bottom: $sp-xl;
}
.evt-org-row {
display: flex;
align-items: center;
gap: $sp-md;
}
.evt-org-avatar {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(232,168,56,0.15), rgba(139,133,184,0.15));
display: flex;
align-items: center;
justify-content: center;
}
.evt-org-avatar-text {
font-size: $fs-base;
font-weight: $fw-bold;
color: $amber;
}
.evt-org-name {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.evt-people-grid {
display: flex;
flex-wrap: wrap;
gap: $sp-lg;
}
.evt-person {
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
width: 100rpx;
}
.evt-person-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: $bg-elevated;
display: flex;
align-items: center;
justify-content: center;
}
.evt-person-text {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $lavender;
}
.evt-person-name {
font-size: $fs-xs;
color: $text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100rpx;
text-align: center;
}
/* 底部操作栏 */
.evt-action-bar {
padding: $sp-md $sp-lg;
padding-bottom: calc(env(safe-area-inset-bottom) + #{$sp-md});
background: $bg-card;
border-top: 1rpx solid var(--divider-color, rgba(255,255,255,0.08));
}
.evt-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
text-align: center;
border-radius: $radius-full;
font-size: $fs-lg;
font-weight: $fw-bold;
border: none;
&::after { border: none; }
&:active { transform: scale(0.98); }
}
.evt-btn-primary {
background: linear-gradient(135deg, $amber, $amber-deep);
color: $text-on-amber;
}
.evt-btn-ghost {
background: $bg-elevated;
color: $coral;
border: 1rpx solid rgba(255,107,107,0.2);
}
.evt-ended-tip {
text-align: center;
padding: $sp-md 0;
}
.evt-ended-text {
font-size: $fs-base;
color: $text-tertiary;
}
.evt-org-actions {
display: flex;
gap: $sp-md;
.evt-btn {
flex: 1;
}
}
.evt-btn-danger {
background: rgba(255,107,107,0.1);
color: $coral;
border: 1rpx solid rgba(255,107,107,0.25);
}
/* 待审核报名 */
.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>