From 8493a7350cda10e725b59b796a1078e3336c7b94 Mon Sep 17 00:00:00 2001 From: DevOps Date: Thu, 25 Dec 2025 14:21:01 +0800 Subject: [PATCH] feat: improve judge invite venue assignment UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add venue selection in batch import dialog - Add individual venue assignment button per row - Display '所有场地' for chief_judge role - Hide venue assignment button for chief_judge - Change action column to vertical layout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/api/martial/activitySchedule.js | 31 +++++ src/api/martial/judgeInvite.js | 13 ++ src/views/martial/judgeInvite/index.vue | 163 ++++++++++++++++++++++-- src/views/martial/schedule/index.vue | 51 +++++++- 4 files changed, 248 insertions(+), 10 deletions(-) diff --git a/src/api/martial/activitySchedule.js b/src/api/martial/activitySchedule.js index 81ddcf2..0c76108 100644 --- a/src/api/martial/activitySchedule.js +++ b/src/api/martial/activitySchedule.js @@ -196,3 +196,34 @@ export const exportSchedule = (competitionId) => { responseType: 'blob' }) } + + +/** + * 导出比赛时间汇总版 + * @param {Number} competitionId - 赛事ID + * @param {Number} venueId - 场地ID(可选) + * @param {String} scheduleDate - 比赛日期(可选) + */ +export const exportScheduleSummary = (competitionId, venueId, scheduleDate) => { + return request({ + url: '/martial/export/schedule/summary', + method: 'get', + params: { competitionId, venueId, scheduleDate }, + responseType: 'blob' + }) +} + +/** + * 导出竞赛分组详细版 + * @param {Number} competitionId - 赛事ID + * @param {Number} venueId - 场地ID(可选) + * @param {String} scheduleDate - 比赛日期(可选) + */ +export const exportScheduleDetail = (competitionId, venueId, scheduleDate) => { + return request({ + url: '/martial/export/schedule/detail', + method: 'get', + params: { competitionId, venueId, scheduleDate }, + responseType: 'blob' + }) +} diff --git a/src/api/martial/judgeInvite.js b/src/api/martial/judgeInvite.js index 42c8c87..1ccf24f 100644 --- a/src/api/martial/judgeInvite.js +++ b/src/api/martial/judgeInvite.js @@ -264,3 +264,16 @@ export const getInviteByJudge = (competitionId, judgeId) => { params: { competitionId, judgeId } }) } + +/** + * 更新裁判场地 + * @param {Number} inviteId - 邀请ID + * @param {Number} venueId - 场地ID + */ +export const updateInviteVenue = (inviteId, venueId) => { + return request({ + url: `/api/martial/judgeInvite/updateVenue/${inviteId}`, + method: 'put', + params: { venueId } + }) +} diff --git a/src/views/martial/judgeInvite/index.vue b/src/views/martial/judgeInvite/index.vue index 3950b8d..df83ae7 100644 --- a/src/views/martial/judgeInvite/index.vue +++ b/src/views/martial/judgeInvite/index.vue @@ -143,6 +143,11 @@ + + + + + + + + + {{ currentInvite?.judgeName }} + + + {{ currentInvite?.venueName || "-" }} + + + + + + + + + @@ -292,6 +360,7 @@ import { Download, FolderOpened, View, + Edit, } from '@element-plus/icons-vue' import { getJudgeInviteList, @@ -300,10 +369,12 @@ import { generateInviteCode, batchGenerateInviteCode, regenerateInviteCode, - removeInvite + removeInvite, + updateInviteVenue } from '@/api/martial/judgeInvite' import { getCompetitionList } from '@/api/martial/competition' import { getRefereeList } from '@/api/martial/referee' +import { getVenuesByCompetition } from '@/api/martial/venue' import dayjs from 'dayjs' // 数据状态 @@ -328,6 +399,17 @@ const judgeQueryParams = reactive({ refereeType: null }) +// 场地选择 +const venueList = ref([]) +const selectedVenueId = ref(null) +const venueLoading = ref(false) + +// 分配场地对话框 +const venueDialogVisible = ref(false) +const currentInvite = ref(null) +const newVenueId = ref(null) +const venueUpdating = ref(false) + // 统计数据 const statistics = ref({ totalInvites: 0, @@ -456,6 +538,8 @@ const handleImportFromPool = async () => { // 打开裁判选择对话框 judgeDialogVisible.value = true + selectedVenueId.value = null + loadVenueList() selectedJudges.value = [] loadJudgeList() } @@ -503,6 +587,26 @@ const handleJudgeSearch = () => { } // 裁判搜索重置 +// 加载场地列表 +const loadVenueList = async () => { + if (!queryParams.competitionId) return + venueLoading.value = true + try { + const res = await getVenuesByCompetition(queryParams.competitionId) + const data = res.data?.data || res.data || {} + venueList.value = data.records || [] + // 默认选择第一个场地 + if (venueList.value.length > 0 && !selectedVenueId.value) { + selectedVenueId.value = venueList.value[0].id + } + } catch (error) { + console.error("加载场地列表失败:", error) + ElMessage.error("加载场地列表失败") + } finally { + venueLoading.value = false + } +} + const handleJudgeReset = () => { judgeQueryParams.name = '' judgeQueryParams.phone = '' @@ -541,6 +645,7 @@ const handleConfirmImport = async () => { competitionId: queryParams.competitionId, judgeIds: judgeIds, role: 'judge', + venueId: selectedVenueId.value, expireDays: 30 }) @@ -690,6 +795,48 @@ const handleRegenerateCode = async (row) => { /** * 删除邀请记录 */ + +/** + * 打开分配场地对话框 + */ +const handleAssignVenue = async (row) => { + currentInvite.value = row + newVenueId.value = row.venueId > 0 ? row.venueId : null + venueDialogVisible.value = true + // 加载场地列表 + if (venueList.value.length === 0) { + await loadVenueList() + } +} + +/** + * 确认分配场地 + */ +const handleConfirmVenue = async () => { + if (!newVenueId.value) { + ElMessage.warning("请选择场地") + return + } + + try { + venueUpdating.value = true + const res = await updateInviteVenue(currentInvite.value.id, newVenueId.value) + + if (res.data?.data || res.data?.success) { + ElMessage.success("场地分配成功") + venueDialogVisible.value = false + await fetchData() + } else { + ElMessage.error(res.data?.msg || "分配失败") + } + } catch (error) { + console.error("分配场地失败:", error) + ElMessage.error(error.response?.data?.msg || error.message || "分配场地失败") + } finally { + venueUpdating.value = false + } +} + const handleDelete = async (row) => { try { await ElMessageBox.confirm( diff --git a/src/views/martial/schedule/index.vue b/src/views/martial/schedule/index.vue index 814bbd9..b47f49a 100644 --- a/src/views/martial/schedule/index.vue +++ b/src/views/martial/schedule/index.vue @@ -276,7 +276,18 @@ @@ -380,7 +391,7 @@ import { ArrowDown, ArrowRight } from '@element-plus/icons-vue' import { getVenuesByCompetition } from '@/api/martial/venue' import { getCompetitionDetail } from '@/api/martial/competition' -import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule } from '@/api/martial/activitySchedule' +import { getScheduleResult, saveAndLockSchedule, saveDraftSchedule, triggerAutoArrange, moveScheduleGroup, exportSchedule, exportScheduleSummary, exportScheduleDetail } from '@/api/martial/activitySchedule' export default { name: 'MartialScheduleList', @@ -1113,6 +1124,42 @@ export default { group.items.splice(itemIndex + 1, 0, temp) this.$message.success('下移成功') }, + async handleExportCommand(command) { + try { + this.loading = true + let res, filename + const competitionName = this.competitionInfo.competitionName || this.competitionId + + switch (command) { + case "summary": + res = await exportScheduleSummary(this.competitionId) + filename = `比赛时间_${competitionName}.xlsx` + break + case "detail": + res = await exportScheduleDetail(this.competitionId) + filename = `竞赛分组_${competitionName}.xlsx` + break + case "schedule": + default: + res = await exportSchedule(this.competitionId) + filename = `赛程表_${competitionName}.xlsx` + break + } + + 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("导出成功") + } catch (error) { + console.error("导出失败:", error) + this.$message.error("导出失败,请稍后重试") + } finally { + this.loading = false + } + }, async handleExport() { try { this.loading = true