- 在loadVenues中添加venueType字段映射,确保从后端加载时正确回显 - 在saveVenues中添加venueType字段,确保保存时正确提交 - 修复附件上传headers认证问题 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
617 lines
20 KiB
Vue
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>
|