Files
martial-mini/pages/event-register/event-register.vue
2025-12-12 01:44:41 +08:00

976 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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="event-register-page">
<!-- 步骤指示器 -->
<view class="steps-indicator">
<view class="step-item" :class="{ active: currentStep >= 1 }">
<image class="step-icon-img" :src="currentStep >= 1 ? '/static/images/选择选手信息@3x.png' : '/static/images/选择选手信息@3x.png'" mode="aspectFit"></image>
<text class="step-text">选择选手信息</text>
</view>
<view class="step-line" :class="{ active: currentStep >= 2 }"></view>
<view class="step-item" :class="{ active: currentStep >= 2 }">
<image class="step-icon-img" :src="currentStep >= 2 ? '/static/images/订单支付亮@3x.png' : '/static/images/订单支付灰@3x.png'" mode="aspectFit"></image>
<text class="step-text">订单支付</text>
</view>
<view class="step-line" :class="{ active: currentStep >= 3 }"></view>
<view class="step-item" :class="{ active: currentStep >= 3 }">
<image class="step-icon-img" :src="currentStep >= 3 ? '/static/images/提交报名成功亮@3x.png' : '/static/images/提交报名成功灰@3x.png'" mode="aspectFit"></image>
<text class="step-text">提交报名成功</text>
</view>
</view>
<!-- 步骤1选择选手信息 -->
<view class="step-content" v-if="currentStep === 1">
<view class="selected-count">已选<text class="count">{{ selectedCount }}</text> </view>
<view class="add-player-btn" @click="goToAddPlayer">
<text class="add-icon"></text>
<text>新增选手</text>
</view>
<view class="player-list">
<view class="player-item" v-for="(item, index) in playerList" :key="index">
<view class="player-checkbox" @click="togglePlayer(item)">
<image v-if="item.selected" class="checkbox-img" src="/static/images/选中@3x.png" mode="aspectFit"></image>
<image v-else class="checkbox-img" src="/static/images/未选中@3x.png" mode="aspectFit"></image>
</view>
<view class="player-info">
<view class="player-name">{{ item.name }}</view>
<view class="player-id">身份证{{ item.idCard }}</view>
</view>
<view class="player-actions">
<view class="action-btn edit-btn" @click.stop="handleEdit(item)">
<image class="action-icon" src="/static/images/编辑@3x.png" mode="aspectFit"></image>
<text>编辑</text>
</view>
<view class="action-btn delete-btn" @click.stop="handleDelete(item)">
<image class="action-icon" src="/static/images/删除@3x.png" mode="aspectFit"></image>
<text>删除</text>
</view>
</view>
</view>
</view>
<view class="next-btn-wrapper">
<view class="next-btn" @click="goToStep2">下一步</view>
</view>
</view>
<!-- 步骤2订单支付 -->
<view class="step-content" v-if="currentStep === 2">
<view class="order-title">订单支付</view>
<view class="event-info-card">
<view class="event-title">{{ eventInfo.title }}</view>
<view class="divider"></view>
<view class="info-item">
<text class="label">地点</text>
<text class="value">{{ eventInfo.location }}</text>
</view>
<view class="info-item">
<text class="label">比赛时间</text>
<text class="value">{{ eventInfo.matchTime }}</text>
</view>
<view class="info-item">
<text class="label">报名项目</text>
<text class="value">{{ eventInfo.projects }}</text>
</view>
<view class="info-item">
<text class="label">联系人</text>
<text class="value">{{ eventInfo.contact }}</text>
<text class="edit-icon">📋</text>
</view>
<view class="info-hint">(注意是否用此号码接收信息)</view>
<view class="info-item participants-item">
<text class="label">参赛选手</text>
<text class="value participants" style="color: #C93639; font-weight: bold;">
{{ eventInfo.participants || '未选择选手' }}
</text>
<view class="view-cert-btn" @click="showPlayers">
<text>查看证件</text>
<text class="arrow"></text>
</view>
</view>
</view>
<view class="payment-info">
<view class="payment-row">
<text class="label">人数</text>
<text class="value">{{ selectedCount }}</text>
</view>
<view class="payment-row total">
<text class="label">合计</text>
<text class="value price">¥ {{ totalPrice }}</text>
</view>
</view>
<view class="next-btn-wrapper">
<view class="next-btn" @click="goToStep3">去支付</view>
</view>
</view>
<!-- 步骤3报名成功 -->
<view class="step-content" v-if="currentStep === 3">
<view class="success-title">报名成功</view>
<view class="event-info-card">
<view class="event-title">{{ eventInfo.title }}</view>
<view class="divider"></view>
<view class="info-item">
<text class="label">地点</text>
<text class="value">{{ eventInfo.location }}</text>
</view>
<view class="info-item">
<text class="label">比赛时间</text>
<text class="value">{{ eventInfo.matchTime }}</text>
</view>
<view class="info-item">
<text class="label">报名项目</text>
<text class="value">{{ eventInfo.projects }}</text>
</view>
<view class="info-item">
<text class="label">联系人</text>
<text class="value">{{ eventInfo.contact }}</text>
</view>
<view class="participants-title">参赛选手{{ selectedPlayers.length }}</view>
<view class="participants-detail">
<view class="participant-item" v-for="(item, index) in selectedPlayers" :key="index">
<view class="participant-name">{{ item.name }}</view>
<view class="participant-id">身份证{{ item.idCard }}</view>
<view class="participant-number">编号{{ item.number }}</view>
</view>
</view>
</view>
<view class="close-btn-wrapper">
<view class="close-btn" @click="handleClose"></view>
</view>
</view>
<!-- 参赛选手弹窗 -->
<view class="player-modal" v-if="showPlayerModal" @click="showPlayerModal = false">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">参赛选手</text>
<text class="close-icon" @click="showPlayerModal = false"></text>
</view>
<scroll-view class="modal-body" scroll-y>
<view class="modal-player-item" v-for="(item, index) in selectedPlayers" :key="index">
<view class="player-name">{{ item.name }}</view>
<view class="player-id">身份证{{ item.idCard }}</view>
<view class="player-number" v-if="item.number">编号{{ item.number }}</view>
<view class="player-hint" v-if="index === 0">报名成功后生成唯一编号</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script>
import competitionAPI from '@/api/competition.js'
import athleteAPI from '@/api/athlete.js'
import registrationAPI from '@/api/registration.js'
export default {
data() {
return {
currentStep: 1,
eventId: '',
selectedProjects: [],
eventInfo: {
title: '',
location: '',
matchTime: '',
projects: '',
contact: '',
participants: ''
},
playerList: [],
selectedPlayers: [],
showPlayerModal: false,
totalPrice: 0,
registrationId: ''
};
},
computed: {
selectedCount() {
return this.playerList.filter(item => item.selected).length
},
participantsText() {
return this.playerList
.filter(item => item.selected)
.map(item => item.name)
.join('、')
}
},
onLoad(options) {
if (options.eventId) {
this.eventId = options.eventId
this.loadEventDetail(options.eventId)
}
if (options.projects) {
try {
// 尝试解码(可能被双重编码)
let projectsStr = decodeURIComponent(options.projects)
// 如果还包含 %,说明被双重编码了,再解码一次
if (projectsStr.includes('%')) {
projectsStr = decodeURIComponent(projectsStr)
}
this.selectedProjects = JSON.parse(projectsStr)
} catch (err) {
console.error('解析项目数据失败:', err)
}
}
// 加载选手列表
this.loadPlayerList()
},
onShow() {
// 从新增/编辑页面返回时重新加载列表
if (this.currentStep === 1) {
this.loadPlayerList()
}
},
methods: {
/**
* 加载赛事详情
*/
async loadEventDetail(id) {
try {
const res = await competitionAPI.getCompetitionDetail(id)
// 尝试多个可能的时间字段名
const startTime = res.startTime || res.competitionStartTime || res.beginTime || res.startDate
const endTime = res.endTime || res.competitionEndTime || res.finishTime || res.endDate
// 如果没有时间字段,尝试使用其他字段
let matchTime = this.formatTimeRange(startTime, endTime)
if (!matchTime && res.matchTime) {
matchTime = res.matchTime
} else if (!matchTime && res.competitionTime) {
matchTime = res.competitionTime
} else if (!matchTime) {
matchTime = '待定'
}
this.eventInfo = {
title: res.name || res.title || res.competitionName || '未命名赛事',
location: res.location || res.address || res.venue || '待定',
matchTime: matchTime,
projects: this.selectedProjects && this.selectedProjects.length > 0
? this.selectedProjects.map(p => p.name).join('、')
: '',
contact: res.contactPhone || res.contact || res.phone || '',
participants: ''
}
} catch (err) {
console.error('加载赛事详情失败:', err)
// 设置默认值,防止页面显示空白
this.eventInfo = {
title: '未命名赛事',
location: '待定',
matchTime: '待定',
projects: '',
contact: '',
participants: ''
}
}
},
/**
* 加载选手列表
*/
async loadPlayerList() {
try {
const res = await athleteAPI.getAthleteList({
current: 1,
size: 100
})
let list = []
if (res.records) {
list = res.records
} else if (Array.isArray(res)) {
list = res
}
// 数据映射 - 尝试多个可能的字段名
this.playerList = list.map(item => ({
id: item.id,
// 尝试多个可能的姓名字段
name: item.name || item.athleteName || item.playerName || item.realName || item.userName || '未命名',
// 尝试多个可能的身份证字段
idCard: item.idCard || item.idCardNumber || item.idCardNo || item.identityCard || '',
selected: false
}))
} catch (err) {
console.error('加载选手列表失败:', err)
uni.showToast({
title: '加载选手列表失败',
icon: 'none'
})
}
},
/**
* 格式化时间范围
*/
formatTimeRange(startTime, endTime) {
if (!startTime || !endTime) return ''
const formatDate = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}.${month}.${day}`
}
return `${formatDate(startTime)}-${formatDate(endTime)}`
},
/**
* 计算总价
*/
calculateTotalPrice() {
const count = this.selectedCount
if (!this.selectedProjects || this.selectedProjects.length === 0) {
return 0
}
// 计算所有项目的总价(将字符串转换为数字)
const pricePerProject = this.selectedProjects.reduce((sum, p) => {
const price = parseFloat(p.price || 0)
return sum + price
}, 0)
const total = count * pricePerProject
return total.toFixed(2)
},
togglePlayer(item) {
const index = this.playerList.findIndex(p => p.id === item.id)
if (index !== -1) {
const newValue = !this.playerList[index].selected
this.$set(this.playerList[index], 'selected', newValue)
}
},
goToAddPlayer() {
uni.navigateTo({
url: '/pages/add-player/add-player'
});
},
handleEdit(item) {
uni.navigateTo({
url: '/pages/edit-player/edit-player?id=' + item.id
});
},
async handleDelete(item) {
try {
const confirmRes = await new Promise((resolve) => {
uni.showModal({
title: '删除选手',
content: '确定要删除该选手吗?',
success: (res) => resolve(res)
})
})
if (confirmRes.confirm) {
await athleteAPI.removeAthlete(item.id)
const index = this.playerList.findIndex(p => p.id === item.id);
if (index > -1) {
this.playerList.splice(index, 1);
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
} catch (err) {
console.error('删除选手失败:', err)
uni.showToast({
title: '删除失败',
icon: 'none'
})
}
},
goToStep2() {
const selected = this.playerList.filter(item => item.selected)
if (selected.length === 0) {
uni.showToast({
title: '请至少选择一名选手',
icon: 'none'
})
return
}
// 更新参赛选手信息
const participantsText = selected.map(p => p.name).join('、')
// 使用 $set 确保响应式更新
this.$set(this.eventInfo, 'participants', participantsText)
this.totalPrice = this.calculateTotalPrice()
// 延迟切换步骤,确保数据更新完成
this.$nextTick(() => {
this.currentStep = 2
})
},
async goToStep3() {
try {
// 获取选中的选手
const selected = this.playerList.filter(item => item.selected)
// 检查必填字段
if (!this.eventId) {
uni.showToast({
title: '赛事ID缺失',
icon: 'none'
})
return
}
if (!this.selectedProjects || this.selectedProjects.length === 0) {
uni.showToast({
title: '请选择报名项目',
icon: 'none'
})
return
}
if (selected.length === 0) {
uni.showToast({
title: '请选择参赛选手',
icon: 'none'
})
return
}
// 生成订单号:格式 BMyyyyMMddHHmmss + 随机4位数
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
const random = String(Math.floor(Math.random() * 10000)).padStart(4, '0')
const orderNo = `BM${year}${month}${day}${hours}${minutes}${seconds}${random}`
// 构建提交数据 - 确保ID都是数字类型
const submitData = {
orderNo: orderNo,
competitionId: parseInt(this.eventId),
projectIds: this.selectedProjects.map(p => parseInt(p.id)),
athleteIds: selected.map(p => parseInt(p.id)),
contactPhone: this.eventInfo.contact || '',
totalAmount: parseFloat(this.totalPrice) || 0
}
console.log('=== 提交报名数据 ===')
console.log('订单号:', submitData.orderNo)
console.log('完整提交数据:', submitData)
console.log('赛事ID:', submitData.competitionId, typeof submitData.competitionId)
console.log('项目IDs:', submitData.projectIds)
console.log('选手IDs:', submitData.athleteIds)
console.log('联系电话:', submitData.contactPhone)
console.log('总金额:', submitData.totalAmount, typeof submitData.totalAmount)
// 提交报名订单
const res = await registrationAPI.submitRegistration(submitData)
// 保存报名ID
this.registrationId = res.id || res.registrationId
// 更新选中的选手列表(包含编号)
this.selectedPlayers = selected.map(item => ({
name: item.name,
idCard: item.idCard,
number: item.number || `${this.registrationId}-${item.id}`
}))
this.currentStep = 3;
uni.showToast({
title: '报名成功',
icon: 'success'
})
} catch (err) {
console.error('提交报名失败:', err)
uni.showToast({
title: '报名失败,请重试',
icon: 'none'
})
}
},
showPlayers() {
// 更新选中的选手列表
this.selectedPlayers = this.playerList
.filter(item => item.selected)
.map(item => ({
name: item.name,
idCard: item.idCard,
number: ''
}))
this.showPlayerModal = true;
},
handleClose() {
uni.navigateBack({
delta: 3
});
}
}
};
</script>
<style lang="scss" scoped>
.event-register-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 180rpx;
}
.steps-indicator {
background-color: #fff;
padding: 40rpx 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.step-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10rpx;
opacity: 0.4;
}
.step-item.active {
opacity: 1;
}
.step-icon {
width: 70rpx;
height: 70rpx;
border-radius: 50%;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.step-item.active .step-icon {
background-color: #C93639;
}
.step-icon-img {
width: 70rpx;
height: 70rpx;
}
.icon {
font-size: 36rpx;
}
.step-item.active .icon {
color: #fff;
}
.step-text {
font-size: 22rpx;
color: #666666;
white-space: nowrap;
}
.step-item.active .step-text {
color: #C93639;
font-weight: bold;
}
.step-line {
flex: 1;
height: 2rpx;
background-color: #eeeeee;
margin: 0 10rpx;
}
.step-line.active {
background-color: #C93639;
}
.step-content {
padding: 30rpx;
}
.selected-count {
font-size: 28rpx;
color: #666666;
margin-bottom: 20rpx;
}
.count {
color: #C93639;
font-weight: bold;
font-size: 32rpx;
}
.add-player-btn {
background-color: #fff;
padding: 30rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
margin-bottom: 20rpx;
color: #C93639;
font-size: 30rpx;
}
.add-icon {
font-size: 36rpx;
}
.player-list {
margin-bottom: 20rpx;
}
.player-item {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
gap: 20rpx;
}
.player-checkbox {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.checkbox-img {
width: 40rpx;
height: 40rpx;
}
.checked {
font-size: 36rpx;
color: #C93639;
}
.unchecked {
font-size: 36rpx;
color: #cccccc;
}
.action-icon {
width: 28rpx;
height: 28rpx;
}
.player-info {
flex: 1;
}
.player-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 10rpx;
}
.player-id {
font-size: 26rpx;
color: #666666;
}
.player-actions {
display: flex;
gap: 15rpx;
}
.action-btn {
display: flex;
align-items: center;
gap: 5rpx;
padding: 10rpx 20rpx;
border-radius: 8rpx;
font-size: 24rpx;
border: 2rpx solid;
}
.edit-btn {
color: #C93639;
border-color: #C93639;
}
.delete-btn {
color: #C93639;
border-color: #C93639;
}
.next-btn-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
background-color: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.next-btn {
background-color: #C93639;
color: #fff;
text-align: center;
padding: 30rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: bold;
}
.order-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
margin-bottom: 30rpx;
}
.event-info-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.event-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
line-height: 1.5;
margin-bottom: 20rpx;
}
.divider {
height: 6rpx;
background-color: #C93639;
width: 120rpx;
margin: 20rpx 0;
}
.info-item {
display: flex;
align-items: flex-start;
margin-bottom: 15rpx;
}
.label {
font-size: 26rpx;
color: #666666;
flex-shrink: 0;
}
.value {
font-size: 26rpx;
color: #666666;
flex: 1;
}
.edit-icon {
margin-left: 10rpx;
font-size: 28rpx;
}
.info-hint {
font-size: 22rpx;
color: #C93639;
margin: 10rpx 0;
}
.participants-item {
align-items: center;
}
.participants {
word-break: break-all;
}
.view-cert-btn {
display: flex;
align-items: center;
padding: 8rpx 20rpx;
border: 2rpx solid #C93639;
border-radius: 8rpx;
font-size: 24rpx;
color: #C93639;
margin-left: 20rpx;
white-space: nowrap;
}
.arrow {
font-size: 24rpx;
margin-left: 5rpx;
}
.payment-info {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
}
.payment-row {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
font-size: 28rpx;
}
.payment-row.total {
font-size: 32rpx;
font-weight: bold;
}
.price {
color: #C93639;
font-size: 36rpx;
}
.success-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
margin-bottom: 30rpx;
}
.participants-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin: 20rpx 0;
}
.participants-detail {
margin-top: 20rpx;
}
.participant-item {
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 15rpx;
}
.participant-name {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 10rpx;
}
.participant-id {
font-size: 24rpx;
color: #666666;
margin-bottom: 5rpx;
}
.participant-number {
font-size: 24rpx;
color: #666666;
}
.close-btn-wrapper {
position: fixed;
top: 100rpx;
left: 30rpx;
z-index: 999;
}
.close-btn {
width: 80rpx;
height: 80rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
color: #fff;
}
.player-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
width: 600rpx;
max-height: 80vh;
background-color: #fff;
border-radius: 24rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eeeeee;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.close-icon {
font-size: 40rpx;
color: #999999;
}
.modal-body {
max-height: 60vh;
padding: 30rpx;
}
.modal-player-item {
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 15rpx;
}
.player-hint {
font-size: 22rpx;
color: #C93639;
margin-top: 10rpx;
}
</style>