feat: improve judge invite venue assignment UI

- 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 <noreply@anthropic.com>
This commit is contained in:
DevOps
2025-12-25 14:21:01 +08:00
parent 7f8c5c630b
commit 8493a7350c
4 changed files with 248 additions and 10 deletions

View File

@@ -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'
})
}

View File

@@ -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 }
})
}

View File

@@ -143,6 +143,11 @@
</el-button>
</template>
</el-table-column>
<el-table-column prop="venueName" label="负责场地" align="center">
<template #default="{ row }">
<span>{{ row.role === 'chief_judge' ? '所有场地' : (row.venueName || "-") }}</span>
</template>
</el-table-column>
<el-table-column prop="refereeType" label="裁判类型" align="center">
<template #default="{ row }">
<el-tag :type="row.refereeType === 1 ? 'danger' : 'primary'" size="small">
@@ -152,14 +157,19 @@
</el-table-column>
<el-table-column prop="contactPhone" label="联系电话" />
<el-table-column prop="contactEmail" label="擅长项目" show-overflow-tooltip />
<el-table-column label="操作" width="150" align="center" fixed="right">
<el-table-column label="操作" width="100" align="center" fixed="right">
<template #default="{ row }">
<div style="display: flex; flex-direction: column; align-items: center; gap: 4px;">
<el-button v-if="row.role !== 'chief_judge'" link type="warning" :icon="Edit" @click="handleAssignVenue(row)">
分配场地
</el-button>
<el-button link type="primary" :icon="View" @click="handleView(row)">
查看
</el-button>
<el-button link type="danger" :icon="Delete" @click="handleDelete(row)">
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>
@@ -220,6 +230,21 @@
</el-button>
<el-button :icon="Refresh" @click="handleJudgeReset">重置</el-button>
</el-form-item>
<el-form-item label="分配场地">
<el-select
v-model="selectedVenueId"
placeholder="请选择场地"
:loading="venueLoading"
style="width: 150px"
>
<el-option
v-for="venue in venueList"
:key="venue.id"
:label="venue.venueName"
:value="venue.id"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 裁判列表 -->
@@ -279,6 +304,49 @@
</div>
</template>
</el-dialog>
<!-- 分配场地对话框 -->
<el-dialog
v-model="venueDialogVisible"
title="分配场地"
width="400px"
:close-on-click-modal="false"
>
<el-form label-width="80px">
<el-form-item label="裁判姓名">
<span>{{ currentInvite?.judgeName }}</span>
</el-form-item>
<el-form-item label="当前场地">
<span>{{ currentInvite?.venueName || "-" }}</span>
</el-form-item>
<el-form-item label="选择场地" required>
<el-select
v-model="newVenueId"
placeholder="请选择场地"
:loading="venueLoading"
style="width: 100%"
>
<el-option
v-for="venue in venueList"
:key="venue.id"
:label="venue.venueName"
:value="venue.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="venueDialogVisible = false">取消</el-button>
<el-button
type="primary"
:disabled="!newVenueId"
:loading="venueUpdating"
@click="handleConfirmVenue"
>
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -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(

View File

@@ -276,7 +276,18 @@
<div class="footer-actions">
<el-button size="small" @click="handleSaveDraft" v-if="!isScheduleCompleted">保存草稿</el-button>
<el-button size="small" @click="handleExport" v-if="isScheduleCompleted">导出</el-button>
<el-dropdown v-if="isScheduleCompleted" @command="handleExportCommand" style="margin-right: 10px;">
<el-button size="small">
导出 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="summary">比赛时间汇总版</el-dropdown-item>
<el-dropdown-item command="detail">竞赛分组详细版</el-dropdown-item>
<el-dropdown-item command="schedule" divided>赛程表原版</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button size="small" type="primary" @click="handleConfirm" v-if="!isScheduleCompleted">完成编排</el-button>
</div>
@@ -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