fix: venue tab filtering using String() comparison for large IDs

- Fixed JavaScript precision loss issue with large venue IDs
- Changed Number() to String() comparison in filteredCompetitionGroups
- Changed Number() to String() comparison in venueData computed property
- Added ExportPreview component

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
DevOps
2026-01-13 16:42:28 +08:00
co-authored by factory-droid[bot]
parent c04193bf84
commit d7af8d1394
3 changed files with 170 additions and 14 deletions
+10 -1
View File
@@ -188,6 +188,15 @@ export const saveDispatch = (data) => {
* 导出赛程表
* @param {Number} competitionId - 赛事ID
*/
// 预览赛程表数据
export const getScheduleExportPreview = (competitionId, template, venueId) => {
return request({
url: "/api/martial/export/schedule/preview",
method: "get",
params: { competitionId, template, venueId }
})
}
export const exportSchedule = (competitionId) => {
return request({
url: '/martial/export/schedule',
@@ -197,7 +206,7 @@ export const exportSchedule = (competitionId) => {
})
}
// Export schedule template 2 (competition time format)
export const exportScheduleTemplate2 = (competitionId, venueId, venueName, timeSlot) => {
return request({
url: '/martial/export/schedule2',
@@ -0,0 +1,145 @@
<template>
<el-dialog
title="导出预览"
:model-value="visible"
@update:model-value="handleVisibleChange"
width="90%"
top="5vh"
:before-close="handleClose"
>
<div class="export-options">
<el-form :inline="true" size="small">
<el-form-item label="导出模板">
<el-select v-model="exportTemplate" placeholder="选择模板" style="width: 200px">
<el-option label="模板1-详细赛程表" value="template1" />
<el-option label="模板2-比赛时间表" value="template2" />
</el-select>
</el-form-item>
<el-form-item label="场地" v-if="exportTemplate === 'template2'">
<el-select v-model="selectedVenue" placeholder="选择场地" style="width: 150px">
<el-option label="全部场地" :value="null" />
<el-option v-for="venue in venues" :key="venue.id" :label="venue.venueName" :value="venue.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadPreviewData" :loading="loading">刷新预览</el-button>
</el-form-item>
</el-form>
</div>
<div class="preview-table" v-loading="loading">
<el-table v-if="exportTemplate === 'template1'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="scheduleDate" label="比赛日期" width="120" />
<el-table-column prop="timeSlot" label="时间段" width="100" />
<el-table-column prop="venueName" label="场地" width="120" />
<el-table-column prop="projectName" label="项目名称" width="180" />
<el-table-column prop="category" label="组别" width="80" />
<el-table-column prop="athleteName" label="运动员姓名" width="120" />
<el-table-column prop="teamName" label="单位/队伍" width="150" />
<el-table-column prop="sortOrder" label="出场顺序" width="90" />
<el-table-column prop="status" label="状态" width="80" />
</el-table>
<el-table v-if="exportTemplate === 'template2'" :data="previewData" border stripe max-height="500" style="width: 100%">
<el-table-column prop="sequenceNo" label="序号" width="80" />
<el-table-column prop="projectName" label="项目" width="200" />
<el-table-column prop="participantCount" label="人数" width="80" />
<el-table-column prop="groupCount" label="组数" width="80" />
<el-table-column prop="durationMinutes" label="时间" width="80" />
<el-table-column prop="tableNo" label="表号" width="100" />
</el-table>
<div v-if="previewData.length === 0 && !loading" class="empty-tip">暂无数据</div>
</div>
<div class="preview-stats" v-if="previewData.length > 0">
<span> {{ previewData.length }} 条记录</span>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleExport" :loading="exporting">确认导出</el-button>
</template>
</el-dialog>
</template>
<script>
import { getScheduleExportPreview, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
export default {
name: 'ExportPreview',
props: {
visible: { type: Boolean, default: false },
competitionId: { type: [String, Number], required: true },
competitionName: { type: String, default: '' },
venues: { type: Array, default: () => [] }
},
emits: ['update:visible'],
data() {
return {
exportTemplate: 'template1',
selectedVenue: null,
previewData: [],
loading: false,
exporting: false
}
},
watch: {
visible(val) { if (val) this.loadPreviewData() },
exportTemplate() { this.loadPreviewData() },
selectedVenue() { if (this.exportTemplate === 'template2') this.loadPreviewData() }
},
methods: {
handleVisibleChange(val) { this.$emit('update:visible', val) },
async loadPreviewData() {
this.loading = true
try {
const res = await getScheduleExportPreview(this.competitionId, this.exportTemplate, this.selectedVenue)
this.previewData = res.data?.data || res.data || []
} catch (error) {
console.error('加载预览数据失败:', error)
this.$message.error('加载预览数据失败')
this.previewData = []
} finally {
this.loading = false
}
},
async handleExport() {
this.exporting = true
try {
let res, filename
if (this.exportTemplate === 'template1') {
res = await exportSchedule(this.competitionId)
filename = '赛程表_' + (this.competitionName || this.competitionId) + '.xlsx'
} else {
const venue = this.venues.find(v => v.id === this.selectedVenue)
const venueName = venue ? venue.venueName : null
res = await exportScheduleTemplate2(this.competitionId, this.selectedVenue, venueName, null)
filename = '比赛时间_' + (venueName || '全部场地') + '_' + (this.competitionName || this.competitionId) + '.xlsx'
}
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
link.click()
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
this.handleClose()
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败')
} finally {
this.exporting = false
}
},
handleClose() { this.$emit('update:visible', false) }
}
}
</script>
<style scoped>
.export-options { margin-bottom: 20px; padding: 15px; background: #f5f7fa; border-radius: 4px; }
.preview-table { min-height: 300px; }
.preview-stats { margin-top: 15px; color: #666; font-size: 14px; }
.empty-tip { text-align: center; padding: 60px 0; color: #999; }
</style>
+15 -13
View File
@@ -7,12 +7,13 @@
</div>
<div class="header-right">
<el-button
v-if="!isScheduleCompleted"
size="small"
type="primary"
:loading="autoArrangeLoading"
@click="handleAutoArrange"
>
{{ isScheduleCompleted ? '重新编排' : '自动编排' }}
自动编排
</el-button>
<el-button size="small" type="danger" @click="showExceptionDialog">
异常组 <el-badge :value="exceptionList.length" :hidden="exceptionList.length === 0" />
@@ -277,15 +278,7 @@
<div class="footer-actions">
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
<el-dropdown v-if="isScheduleCompleted" @command="handleExportCommand" trigger="click">
<el-button size="small">导出 <i class="el-icon-arrow-down el-icon--right"></i></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="template1">模板1 - 详细赛程表</el-dropdown-item>
<el-dropdown-item command="template2">模板2 - 比赛时间表</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button v-if="isScheduleCompleted" size="small" @click="exportPreviewVisible = true">导出</el-button>
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
</div>
@@ -382,6 +375,13 @@
</span>
</template>
</el-dialog>
<!-- 导出预览对话框 -->
<ExportPreview
v-model:visible="exportPreviewVisible"
:competition-id="competitionId"
:competition-name="competitionInfo.competitionName"
:venues="venues"
/>
</div>
</template>
@@ -391,10 +391,12 @@ import { getVenuesByCompetition } from '@/api/martial/venue'
import { getCompetitionDetail } from '@/api/martial/competition'
import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleTemplate2 } from '@/api/martial/activitySchedule'
import { updateCheckInStatus, getScheduleConfig } from '@/api/martial/schedulePlan'
import ExportPreview from './ExportPreview.vue'
export default {
name: 'MartialScheduleList',
components: {
ExportPreview,
ArrowDown,
ArrowRight
},
@@ -408,6 +410,7 @@ export default {
selectedVenueIdForVenueTab: null, // 选中的场地ID(场地Tab
confirmDialogVisible: false,
isScheduleCompleted: false, // 是否已完成编排
exportPreviewVisible: false, // 导出预览对话框
loading: false,
autoArrangeLoading: false, // 自动编排加载状态
venues: [], // 场地列表(从后端加载)
@@ -448,7 +451,7 @@ export default {
const filtered = this.competitionGroups.filter(group => {
console.log(`分组"${group.title}": venueId=${group.venueId}(${typeof group.venueId}), timeSlotIndex=${group.timeSlotIndex}(${typeof group.timeSlotIndex})`)
const venueMatch = group.venueId === this.selectedVenueId || Number(group.venueId) === Number(this.selectedVenueId)
const venueMatch = String(group.venueId) === String(this.selectedVenueId)
const timeMatch = group.timeSlotIndex === this.selectedTime
console.log(` 场地匹配:${venueMatch}, 时间匹配:${timeMatch}`)
return venueMatch && timeMatch
@@ -472,8 +475,7 @@ export default {
// 如果选中了场地,进一步过滤
if (this.selectedVenueIdForVenueTab) {
groupsInTimeSlot = groupsInTimeSlot.filter(group => {
return group.venueId === this.selectedVenueIdForVenueTab ||
Number(group.venueId) === Number(this.selectedVenueIdForVenueTab)
return String(group.venueId) === String(this.selectedVenueIdForVenueTab)
})
}