Files
martial-web/src/views/martial/registration/index.vue
T
Z-WICKandfactory-droid[bot] b2643c966d fix: 修复场地类型(venueType)加载和保存问题
- 在loadVenues中添加venueType字段映射,确保从后端加载时正确回显
- 在saveVenues中添加venueType字段,确保保存时正确提交
- 修复附件上传headers认证问题

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-01-06 15:17:28 +08:00

617 lines
20 KiB
Vue

<template>
<div class="martial-registration-container" v-loading="loading">
<div class="page-header">
<el-button icon="el-icon-back" size="small" @click="goBack">返回</el-button>
<h2 class="page-title">报名详情</h2>
</div>
<div class="competition-info">
<h3 class="section-title">{{ competitionInfo.competitionName || '赛事信息' }}</h3>
<p class="detail-id">ID: {{ competitionInfo.id }}</p>
<el-row :gutter="15" class="info-row">
<el-col :span="6">
<div class="info-item">
<label>主办单位</label>
<div class="info-value">{{ competitionInfo.organizer }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>地区</label>
<div class="info-value">{{ competitionInfo.location }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>详细地点</label>
<div class="info-value">{{ competitionInfo.venue }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>报名时间</label>
<div class="info-value">{{ competitionInfo.registrationTime }}</div>
</div>
</el-col>
</el-row>
<el-row :gutter="15" class="info-row">
<el-col :span="6">
<div class="info-item">
<label>比赛时间</label>
<div class="info-value">{{ competitionInfo.competitionTime }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>报名人数</label>
<div class="info-value">{{ competitionInfo.totalParticipants }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>总金额</label>
<div class="info-value amount">¥{{ competitionInfo.totalAmount }}</div>
</div>
</el-col>
<el-col :span="6">
<div class="info-item">
<label>状态</label>
<div class="info-value">
<el-tag type="success" size="small">{{ competitionInfo.status }}</el-tag>
</div>
</div>
</el-col>
</el-row>
</div>
<div class="tabs-section">
<div class="tab-buttons">
<el-button
size="small"
:type="activeTab === 'participants' ? 'primary' : ''"
@click="activeTab = 'participants'"
>
参赛人数统计
</el-button>
<el-button
size="small"
:type="activeTab === 'projectTime' ? 'primary' : ''"
@click="activeTab = 'projectTime'"
>
项目时间统计
</el-button>
<el-button
size="small"
:type="activeTab === 'amountStats' ? 'primary' : ''"
@click="activeTab = 'amountStats'"
>
全额统计
</el-button>
</div>
<!-- 参赛人数统计Tab -->
<div v-show="activeTab === 'participants'" class="tab-content">
<el-table :data="participantsData" border stripe size="small">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="schoolUnit" label="单位" min-width="180">
<template #default="scope">
<span>{{ scope.row.schoolUnit }}</span>
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
</template>
</el-table-column>
<el-table-column prop="singleCount" label="单人项目" width="100" align="center"></el-table-column>
<el-table-column prop="teamCount" label="集体项目" width="100" align="center"></el-table-column>
<el-table-column prop="male" label="男" width="60" align="center"></el-table-column>
<el-table-column prop="female" label="女" width="60" align="center"></el-table-column>
<el-table-column prop="total" label="合计" width="60" align="center">
<template #default="scope">
<span class="total-num">+{{ scope.row.total }}</span>
</template>
</el-table-column>
</el-table>
</div>
<!-- 项目时间统计Tab -->
<div v-show="activeTab === 'projectTime'" class="tab-content">
<el-table :data="projectTimeData" border stripe size="small">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="projectName" label="项目" min-width="150">
<template #default="scope">
<span>{{ scope.row.projectName }}</span>
<div v-if="scope.row.hint" class="row-hint">{{ scope.row.hint }}</div>
</template>
</el-table-column>
<el-table-column prop="projectType" label="单人/集体" width="100" align="center"></el-table-column>
<el-table-column prop="athleteCount" label="人数/集体" width="100" align="center"></el-table-column>
<el-table-column prop="groupCount" label="组数" width="80" align="center"></el-table-column>
<el-table-column prop="estimatedDuration" label="时长(分)" width="100" align="center"></el-table-column>
<el-table-column prop="projectCode" label="项目编码" width="120" align="center"></el-table-column>
</el-table>
</div>
<!-- 全额统计Tab -->
<div v-show="activeTab === 'amountStats'" class="tab-content">
<el-table :data="amountStatsData" border stripe size="small">
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
<el-table-column prop="schoolUnit" label="学校/单位" min-width="200"></el-table-column>
<el-table-column prop="projectCount" label="项目" width="120" align="center"></el-table-column>
<el-table-column prop="totalAmount" label="全额" width="150" align="center">
<template #default="scope">
<span class="amount">¥{{ scope.row.totalAmount }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div class="footer-actions">
<el-button size="small" @click="handleExport">导出</el-button>
</div>
</div>
</template>
<script>
import {
getOrderDetail,
getOrderRegistrationDetail,
getOrderProjectStats,
getOrderAmountStats
} from '@/api/martial/order'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getProjectDetail } from '@/api/martial/project'
import { getParticipantList } from '@/api/martial/participant'
export default {
name: 'MartialRegistrationDetail',
data() {
return {
competitionId: null, // 改为赛事ID
loading: false,
activeTab: 'participants',
competitionInfo: {
id: '',
competitionName: '', // 添加赛事名称
organizer: '',
location: '',
venue: '',
registrationTime: '',
competitionTime: '',
totalParticipants: 0,
totalAmount: 0,
status: ''
},
participantsData: [],
projectTimeData: [],
amountStatsData: [],
projectCache: new Map(), // 添加项目信息缓存,避免重复API调用
participantsCache: null // 缓存参赛者列表,避免重复查询
}
},
mounted() {
this.competitionId = this.$route.query.competitionId // 改为获取赛事ID
if (this.competitionId) {
this.loadCompetitionInfo(this.competitionId)
this.loadRegistrationStats()
this.loadParticipantsStats()
this.loadProjectTimeStats()
this.loadAmountStats()
} else {
this.$message.warning('未获取到赛事ID')
}
},
methods: {
// 统一获取参赛者列表(带缓存)
async getParticipants() {
if (this.participantsCache !== null) {
return this.participantsCache
}
try {
const res = await getParticipantList(this.competitionId, 1, 10000)
const participants = res.data?.data?.records || res.data?.data || []
this.participantsCache = participants
return participants
} catch (err) {
console.error('查询参赛者列表失败:', err)
return []
}
},
// 统一的项目信息获取方法(带缓存)
async getProjectInfo(projectId) {
if (!projectId) return null
// 先从缓存中查找
if (this.projectCache.has(projectId)) {
return this.projectCache.get(projectId)
}
// 缓存中没有,则调用API
try {
const projectRes = await getProjectDetail(projectId)
const projectInfo = projectRes.data?.data
if (projectInfo) {
// 存入缓存
this.projectCache.set(projectId, projectInfo)
return projectInfo
}
} catch (err) {
console.error(`查询项目${projectId}详情失败:`, err)
}
return null
},
// 批量预加载项目信息(一次性加载所有需要的项目)
async preloadProjectInfo(participants) {
const projectIds = new Set()
participants.forEach(p => {
const projectId = p.projectId || p.project_id
if (projectId && !this.projectCache.has(projectId)) {
projectIds.add(projectId)
}
})
// 并行加载所有项目信息
if (projectIds.size > 0) {
const promises = Array.from(projectIds).map(id => this.getProjectInfo(id))
await Promise.all(promises)
}
},
// 加载赛事信息
async loadCompetitionInfo(competitionId) {
try {
const res = await getCompetitionDetail(competitionId)
console.log('赛事详情返回:', res)
const compData = res.data?.data
if (compData) {
this.competitionInfo.id = compData.id
this.competitionInfo.competitionName = compData.competitionName || compData.competition_name || ''
this.competitionInfo.organizer = compData.organizer || ''
this.competitionInfo.location = compData.location || ''
this.competitionInfo.venue = compData.venue || ''
// 格式化时间范围(兼容驼峰和下划线命名)
const regStartTime = compData.registrationStartTime || compData.registration_start_time
const regEndTime = compData.registrationEndTime || compData.registration_end_time
const compStartTime = compData.competitionStartTime || compData.competition_start_time
const compEndTime = compData.competitionEndTime || compData.competition_end_time
if (regStartTime && regEndTime) {
this.competitionInfo.registrationTime = `${regStartTime} - ${regEndTime}`
}
if (compStartTime && compEndTime) {
this.competitionInfo.competitionTime = `${compStartTime} - ${compEndTime}`
}
// 设置赛事状态
this.competitionInfo.status = this.getCompetitionStatus(compData.status)
}
} catch (err) {
console.error('加载赛事信息失败', err)
this.$message.error('加载赛事信息失败')
}
},
// 加载报名统计信息(统计该赛事下所有订单)
async loadRegistrationStats() {
try {
// 使用缓存的参赛者列表
const participants = await this.getParticipants()
console.log('运动员列表返回:', participants)
this.competitionInfo.totalParticipants = participants.length
// 预加载所有项目信息(一次性并行加载)
await this.preloadProjectInfo(participants)
// 计算总金额(从缓存中获取)
let totalAmount = 0
for (const athlete of participants) {
const projectId = athlete.projectId || athlete.project_id
if (projectId) {
const project = this.projectCache.get(projectId)
if (project) {
totalAmount += parseFloat(project.price || 0)
}
}
}
this.competitionInfo.totalAmount = totalAmount.toFixed(2)
} catch (err) {
console.error('加载报名统计失败', err)
this.$message.warning('加载报名统计数据失败,请稍后重试')
}
},
// 加载参赛人数统计(该赛事的所有运动员)
async loadParticipantsStats() {
try {
// 使用缓存的参赛者列表
const participants = await this.getParticipants()
console.log('参赛人员列表返回:', participants)
// 预加载项目信息
await this.preloadProjectInfo(participants)
// 按单位分组统计
const unitMap = new Map()
participants.forEach(p => {
// 兼容驼峰和下划线命名
const unit = p.organization || p.teamName || p.team_name || '未知单位'
const projectId = p.projectId || p.project_id
const project = this.projectCache.get(projectId)
const projectType = project ? (project.type || 1) : 1 // 1=单人, 2=集体
if (!unitMap.has(unit)) {
unitMap.set(unit, {
schoolUnit: unit,
singleCount: 0,
teamCount: 0,
male: 0,
female: 0,
total: 0
})
}
const stat = unitMap.get(unit)
stat.total++
// 按项目类型统计
if (projectType === 1) {
stat.singleCount++
} else {
stat.teamCount++
}
// 按性别统计
if (p.gender === 1) {
stat.male++
} else if (p.gender === 2) {
stat.female++
}
})
this.participantsData = Array.from(unitMap.values())
} catch (err) {
console.error('加载参赛人员统计失败', err)
this.$message.warning('加载参赛人员统计失败')
this.participantsData = []
}
},
// 加载项目时间统计(该赛事的所有项目及参赛人数)
async loadProjectTimeStats() {
try {
// 使用缓存的参赛者列表
const participants = await this.getParticipants()
// 预加载项目信息
await this.preloadProjectInfo(participants)
// 按项目ID分组统计人数
const projectAthleteCount = new Map()
participants.forEach(athlete => {
const projectId = athlete.projectId || athlete.project_id
if (projectId) {
projectAthleteCount.set(projectId, (projectAthleteCount.get(projectId) || 0) + 1)
}
})
// 从缓存中获取项目信息并构建统计数据
const projectStats = []
for (const [projectId, count] of projectAthleteCount) {
const project = this.projectCache.get(projectId)
if (project) {
const projectType = project.type || 1 // 1=单人, 2=集体
projectStats.push({
projectName: project.projectName || project.project_name || '未知项目',
projectType: projectType === 1 ? '单人' : '集体',
athleteCount: count,
groupCount: projectType === 2 ? count : '-', // 集体项目显示组数,单人显示-
estimatedDuration: project.estimatedDuration || project.estimated_duration || 0,
projectCode: project.projectCode || project.project_code || ''
})
}
}
this.projectTimeData = projectStats
} catch (err) {
console.error('加载项目统计失败', err)
this.$message.warning('加载项目统计失败')
this.projectTimeData = []
}
},
// 加载金额统计(该赛事所有单位的报名金额)
async loadAmountStats() {
try {
// 使用缓存的参赛者列表
const participants = await this.getParticipants()
// 2. 按单位分组并统计
const unitMap = new Map()
for (const athlete of participants) {
// 兼容驼峰和下划线命名
const unit = athlete.organization || '未知单位'
const projectId = athlete.projectId || athlete.project_id
if (!unitMap.has(unit)) {
unitMap.set(unit, {
projectIds: new Set(),
totalAmount: 0
})
}
const stat = unitMap.get(unit)
// 添加项目ID(Set自动去重,用于统计项目数量)
if (projectId) {
stat.projectIds.add(projectId)
// 从缓存中获取价格并累加到总金额
const project = this.projectCache.get(projectId)
const price = project ? (project.price || 0) : 0
stat.totalAmount += parseFloat(price)
}
}
// 3. 计算每个单位的总金额
const amountStats = []
for (const [unit, stat] of unitMap) {
amountStats.push({
schoolUnit: unit,
projectCount: stat.projectIds.size,
totalAmount: stat.totalAmount.toFixed(2)
})
}
this.amountStatsData = amountStats
} catch (err) {
console.error('加载金额统计失败', err)
this.$message.warning('加载金额统计失败')
this.amountStatsData = []
}
},
getCompetitionStatus(status) {
const statusMap = {
1: '未开始',
2: '报名中',
3: '进行中',
4: '已结束'
}
return statusMap[status] || '未知'
},
goBack() {
this.$router.go(-1)
},
handleExport() {
this.$message.success('导出功能开发中')
}
}
}
</script>
<style lang="scss" scoped>
.martial-registration-container {
padding: 15px;
background: #fff;
.page-header {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 10px;
.page-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
.competition-info {
margin-bottom: 15px;
padding: 15px;
background: #fff;
border: 1px solid #dcdfe6;
.section-title {
margin-bottom: 8px;
font-size: 15px;
font-weight: 600;
padding-left: 8px;
border-left: 3px solid #409eff;
}
.detail-id {
margin-bottom: 15px;
padding-left: 8px;
color: #909399;
font-size: 11px;
}
.info-row {
margin-bottom: 12px;
}
.info-item {
padding: 8px;
background: #f5f7fa;
label {
display: block;
margin-bottom: 6px;
font-size: 11px;
color: #909399;
}
.info-value {
font-size: 12px;
color: #606266;
&.amount {
color: #e6a23c;
font-weight: 600;
}
}
}
}
.tabs-section {
margin-bottom: 15px;
.tab-buttons {
margin-bottom: 15px;
padding: 12px;
background: #fff;
border: 1px solid #dcdfe6;
}
.tab-content {
min-height: 300px;
padding: 15px;
background: #fff;
border: 1px solid #dcdfe6;
.tab-hint {
padding: 8px 12px;
margin-bottom: 12px;
background: #fff3e0;
border-left: 3px solid #ff9800;
color: #e65100;
font-size: 11px;
}
.row-hint {
margin-top: 4px;
padding: 2px 5px;
color: #f50057;
font-size: 11px;
background: rgba(245, 0, 87, 0.05);
display: inline-block;
}
}
}
.total-num,
.amount {
color: #e6a23c;
font-weight: 600;
}
.footer-actions {
text-align: center;
padding: 15px;
background: #fff;
border: 1px solid #dcdfe6;
}
}
</style>