2026-08-11 23:40:41 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="judge-panel">
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<el-tabs v-model="mode" class="judge-tabs" @tab-change="load">
|
|
|
|
|
|
<el-tab-pane label="裁判报名" name="eventReferees" />
|
|
|
|
|
|
<el-tab-pane label="场次分配" name="assignments" />
|
|
|
|
|
|
</el-tabs>
|
2026-08-11 23:40:41 +08:00
|
|
|
|
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<div v-if="mode === 'eventReferees'" class="panel-body">
|
|
|
|
|
|
<ProTable :data="eventReferees" :columns="eventRefereeColumns" :pagination="false" :tool-button="false" :loading="loading" row-key="eventRefereeId">
|
|
|
|
|
|
<template #tools><el-button type="success" icon="ele-Postcard" :loading="codeLoading" @click="generateCode">生成裁判报名码</el-button></template>
|
|
|
|
|
|
<template #certificateUrl="scope"><el-image v-if="scope.row.certificateUrl" :src="scope.row.certificateUrl" :preview-src-list="[scope.row.certificateUrl]" preview-teleported fit="cover" class="certificate" /><span v-else>-</span></template>
|
|
|
|
|
|
<template #roles="scope">{{ scope.row.roleNames?.join(' / ') || '-' }}</template>
|
|
|
|
|
|
</ProTable>
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="panel-body" v-loading="loading">
|
|
|
|
|
|
<el-empty v-if="!board.arrangementConfirmed" description="当前赛事尚未确认编排,请稍后再试" />
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<section v-for="project in board.projects" :key="project.eventProjectId" class="project-section">
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<div class="project-heading"><span class="project-name">{{ project.projectName }}</span><span class="project-rule">{{ project.ruleName }}</span></div>
|
|
|
|
|
|
<ProTable :data="project.sessions" :columns="assignmentColumns" :pagination="false" :tool-button="false" row-key="sessionId">
|
|
|
|
|
|
<template #assigned="scope"><el-tag :type="scope.row.assigned ? 'success' : 'info'">{{ scope.row.assigned ? '已分配' : '未分配' }}</el-tag></template>
|
|
|
|
|
|
<template #action="scope"><el-button text type="primary" :disabled="board.submitted" @click="openAssign(project, scope.row)">分配裁判</el-button><el-button text type="primary" :disabled="!scope.row.assigned" @click="openAssign(project, scope.row, true)">查看详情</el-button></template>
|
|
|
|
|
|
</ProTable>
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
<div class="submit-bar"><el-button @click="load">刷新</el-button><el-button type="primary" :disabled="board.submitted" @click="submitAll">{{ board.submitted ? '已提交' : '提交' }}</el-button></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<el-dialog v-model="assignVisible" title="分配裁判" width="min(860px, 94vw)" destroy-on-close>
|
|
|
|
|
|
<div :class="['assign-layout', { readonly: viewOnly }]">
|
2026-08-11 23:40:41 +08:00
|
|
|
|
<div class="role-area">
|
|
|
|
|
|
<div v-for="section in roleSections" :key="section.title" class="role-section">
|
|
|
|
|
|
<div class="role-title">{{ section.title }}</div>
|
|
|
|
|
|
<div class="role-slots">
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<div v-for="slot in section.slots" :key="slot.code" :class="['role-slot', { active: selectedRole === slot.code, readonly: viewOnly }]" @click="selectRole(slot.code)">
|
2026-08-11 23:40:41 +08:00
|
|
|
|
<strong>{{ slot.name }}</strong><el-tag v-if="assignmentMap[slot.code]" :closable="!viewOnly" @close.stop="clearRole(slot.code)">{{ candidateName(assignmentMap[slot.code]) }}</el-tag><span v-else>{{ viewOnly ? '未分配' : '点击后从右侧选择裁判' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<div v-if="!viewOnly" class="candidate-area"><div class="candidate-title">裁判报名</div><el-empty v-if="eligibleCandidates.length === 0" description="暂无符合岗位资格的报名裁判" :image-size="60" /><el-button v-for="item in eligibleCandidates" :key="item.eventRefereeId" class="candidate" @click="chooseCandidate(item)">{{ item.name }} / {{ item.mobile }}</el-button></div>
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</div>
|
2026-08-17 00:16:15 +08:00
|
|
|
|
<template #footer><el-button @click="assignVisible = false">{{ viewOnly ? '关闭' : '取消' }}</el-button><el-button v-if="!viewOnly" type="primary" @click="saveAssignments">确定</el-button></template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="codeVisible" title="裁判报名二维码" width="min(420px, 92vw)" align-center destroy-on-close>
|
|
|
|
|
|
<div class="code-dialog"><div ref="qrcodeRef" class="qrcode" aria-label="裁判报名二维码"></div><div class="registration-code">报名码:{{ registrationCode }}</div><el-button type="primary" @click="downloadCode">下载二维码</el-button></div>
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-08-17 00:16:15 +08:00
|
|
|
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
2026-08-11 23:40:41 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
2026-08-17 00:16:15 +08:00
|
|
|
|
import QRCode from 'qrcodejs2-fixes';
|
|
|
|
|
|
import ProTable from '/@/components/ProTable/index.vue';
|
|
|
|
|
|
import { judgeAssignmentBoardGet, judgeAssignmentSavePost, judgeAssignmentSubmitPost, judgeCandidatesGet, judgeEventRefereesGet, judgeRegistrationCodePost } from '/@/api/match/judgeassignment';
|
|
|
|
|
|
|
2026-08-11 23:40:41 +08:00
|
|
|
|
const props = defineProps({ eventId: { type: [String, Number], required: true }, active: Boolean });
|
2026-08-17 00:16:15 +08:00
|
|
|
|
const mode = ref('eventReferees');
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const eventReferees = ref([]);
|
|
|
|
|
|
const board = reactive({ arrangementConfirmed: false, submitted: false, projects: [] });
|
|
|
|
|
|
const assignVisible = ref(false);
|
|
|
|
|
|
const viewOnly = ref(false);
|
|
|
|
|
|
const currentSession = ref();
|
|
|
|
|
|
const candidates = ref([]);
|
|
|
|
|
|
const roleSections = ref([]);
|
|
|
|
|
|
const selectedRole = ref('');
|
|
|
|
|
|
const assignmentMap = reactive({});
|
|
|
|
|
|
const codeVisible = ref(false);
|
|
|
|
|
|
const codeLoading = ref(false);
|
|
|
|
|
|
const registrationCode = ref('');
|
|
|
|
|
|
const qrcodeRef = ref();
|
|
|
|
|
|
|
|
|
|
|
|
const eventRefereeColumns = [
|
|
|
|
|
|
{ prop: 'index', label: '序号', type: 'index', width: 80 },
|
|
|
|
|
|
{ prop: 'name', label: '裁判姓名', minWidth: 110 },
|
|
|
|
|
|
{ prop: 'regionName', label: '地区', minWidth: 190 },
|
|
|
|
|
|
{ prop: 'levelName', label: '级别', minWidth: 130 },
|
|
|
|
|
|
{ prop: 'certificateUrl', label: '证件', width: 90 },
|
|
|
|
|
|
{ prop: 'mobile', label: '手机号', minWidth: 130 },
|
|
|
|
|
|
{ prop: 'roles', label: '裁判角色', minWidth: 210 },
|
|
|
|
|
|
{ prop: 'createdTime', label: '报名时间', minWidth: 170 },
|
|
|
|
|
|
];
|
|
|
|
|
|
const assignmentColumns = [{ prop: 'sessionName', label: '场次', minWidth: 160 }, { prop: 'venueName', label: '场地', minWidth: 160 }, { prop: 'assigned', label: '状态', width: 110 }, { prop: 'action', label: '操作', width: 200, fixed: 'right' }];
|
|
|
|
|
|
const roleFlag = role => role === 'T' ? 1 : role === 'P' ? 2 : role === 'CJ' ? 8 : role === 'CQ' ? 16 : 4;
|
|
|
|
|
|
const eligibleCandidates = computed(() => candidates.value.filter(item => selectedRole.value && (item.roles & roleFlag(selectedRole.value)) !== 0));
|
|
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
if (!props.active || !props.eventId) return;
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (mode.value === 'eventReferees') eventReferees.value = (await judgeEventRefereesGet(props.eventId)).data || [];
|
|
|
|
|
|
else Object.assign(board, (await judgeAssignmentBoardGet(props.eventId)).data || { arrangementConfirmed: false, submitted: false, projects: [] });
|
|
|
|
|
|
} finally { loading.value = false; }
|
|
|
|
|
|
}
|
|
|
|
|
|
function candidateName(id) { return candidates.value.find(item => item.eventRefereeId === id)?.name || currentSession.value?.assignments?.find(item => item.eventRefereeId === id)?.refereeName || id; }
|
|
|
|
|
|
async function openAssign(project, session, readonly = false) {
|
|
|
|
|
|
viewOnly.value = readonly; currentSession.value = session;
|
|
|
|
|
|
roleSections.value = JSON.parse(project.judgeRoleSchemaJson || '[]');
|
|
|
|
|
|
Object.keys(assignmentMap).forEach(key => delete assignmentMap[key]);
|
|
|
|
|
|
session.assignments.forEach(item => assignmentMap[item.judgeRole] = item.eventRefereeId);
|
|
|
|
|
|
candidates.value = readonly ? [] : (await judgeCandidatesGet(props.eventId, session.sessionId)).data || [];
|
|
|
|
|
|
selectedRole.value = roleSections.value[0]?.slots?.[0]?.code || ''; assignVisible.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
function selectRole(role) { if (!viewOnly.value) selectedRole.value = role; }
|
|
|
|
|
|
function chooseCandidate(item) { if (!selectedRole.value) return; const occupied = Object.entries(assignmentMap).find(([role, id]) => id === item.eventRefereeId && role !== selectedRole.value); if (occupied) return ElMessage.warning('同一场次一名裁判只能分配一个岗位'); assignmentMap[selectedRole.value] = item.eventRefereeId; }
|
|
|
|
|
|
function clearRole(role) { delete assignmentMap[role]; }
|
|
|
|
|
|
async function saveAssignments() {
|
|
|
|
|
|
const slots = roleSections.value.flatMap(section => section.slots);
|
|
|
|
|
|
if (slots.some(slot => !assignmentMap[slot.code])) return ElMessage.warning('请完成全部裁判岗位分配');
|
|
|
|
|
|
await judgeAssignmentSavePost({ eventId: Number(props.eventId), sessionId: currentSession.value.sessionId, assignments: slots.map(slot => { const relation = candidates.value.find(item => item.eventRefereeId === assignmentMap[slot.code]); return { judgeRole: slot.code, eventRefereeId: relation.eventRefereeId, refereeId: relation.refereeId }; }) });
|
|
|
|
|
|
ElMessage.success('分配已保存'); assignVisible.value = false; load();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function submitAll() { await ElMessageBox.confirm('提交后分配任务即可同步到裁判端,是否继续?', '提交确认', { type: 'warning' }); await judgeAssignmentSubmitPost({ eventId: Number(props.eventId) }); ElMessage.success('提交成功'); load(); }
|
|
|
|
|
|
async function generateCode() {
|
|
|
|
|
|
codeLoading.value = true;
|
|
|
|
|
|
try { const result = await judgeRegistrationCodePost(props.eventId); if (!result?.succeeded || !result.data) return ElMessage.error('报名码生成失败,请稍后重试'); registrationCode.value = result.data; codeVisible.value = true; await nextTick(); qrcodeRef.value.innerHTML = ''; new QRCode(qrcodeRef.value, { text: registrationCode.value, width: 240, height: 240, colorDark: '#1d2129', colorLight: '#ffffff', correctLevel: QRCode.CorrectLevel.H }); } finally { codeLoading.value = false; }
|
|
|
|
|
|
}
|
|
|
|
|
|
function downloadCode() { const canvas = qrcodeRef.value?.querySelector('canvas'); const image = qrcodeRef.value?.querySelector('img'); const url = canvas?.toDataURL('image/png') || image?.src; if (!url) return ElMessage.error('二维码尚未生成,请稍后重试'); const link = document.createElement('a'); link.href = url; link.download = `裁判报名二维码-${registrationCode.value}.png`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
|
|
|
|
|
|
watch(() => props.active, value => value && load(), { immediate: true });
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-08-17 00:16:15 +08:00
|
|
|
|
.judge-panel { min-height: 100%; padding: 0 2px; color: var(--el-text-color-primary); }
|
|
|
|
|
|
.judge-tabs { margin-bottom: 12px; }
|
|
|
|
|
|
.panel-body { min-width: 0; }
|
|
|
|
|
|
.certificate { width: 48px; height: 36px; border-radius: 4px; }
|
|
|
|
|
|
.project-section { margin-bottom: 20px; }
|
|
|
|
|
|
.project-heading { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; padding: 0 4px; }
|
|
|
|
|
|
.project-name { font-size: 16px; font-weight: 600; }
|
|
|
|
|
|
.project-rule { padding: 3px 8px; font-size: 12px; color: var(--el-text-color-secondary); background: var(--el-fill-color-light); border-radius: 4px; }
|
|
|
|
|
|
.submit-bar { margin-top: 24px; text-align: center; }
|
|
|
|
|
|
.assign-layout { display: grid; grid-template-columns: minmax(0, 1fr) 210px; gap: 16px; }
|
|
|
|
|
|
.assign-layout.readonly { grid-template-columns: 1fr; }
|
|
|
|
|
|
.role-section { margin-bottom: 12px; overflow: hidden; border: 1px solid var(--el-border-color); border-radius: 5px; }
|
|
|
|
|
|
.role-title, .candidate-title { padding: 9px; font-weight: 600; color: var(--el-color-primary); text-align: center; background: var(--el-color-primary-light-9); }
|
|
|
|
|
|
.role-slots { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
|
|
|
|
.role-slot { display: flex; flex-direction: column; gap: 8px; min-height: 68px; padding: 10px; cursor: pointer; border: 1px solid var(--el-border-color-lighter); }
|
|
|
|
|
|
.role-slot.active { background: var(--el-color-primary-light-9); border-color: var(--el-color-primary); }
|
|
|
|
|
|
.role-slot.readonly { cursor: default; }
|
|
|
|
|
|
.role-slot span { color: var(--el-text-color-placeholder); }
|
|
|
|
|
|
.candidate-area { padding-bottom: 10px; overflow: hidden; border: 1px solid var(--el-border-color); border-radius: 5px; }
|
|
|
|
|
|
.candidate { width: calc(100% - 16px); margin: 8px 8px 0; }
|
|
|
|
|
|
.code-dialog { display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
|
|
|
|
|
.qrcode { width: 264px; height: 264px; padding: 12px; background: #fff; border: 1px solid var(--el-border-color-lighter); border-radius: 8px; box-shadow: var(--el-box-shadow-light); }
|
|
|
|
|
|
.qrcode :deep(img), .qrcode :deep(canvas) { display: block; width: 240px !important; height: 240px !important; }
|
|
|
|
|
|
.registration-code { font-size: 14px; color: var(--el-text-color-secondary); word-break: break-all; }
|
|
|
|
|
|
@media (max-width: 768px) { .judge-panel { padding: 0; } .project-heading { flex-direction: column; align-items: flex-start; gap: 6px; } .assign-layout { grid-template-columns: 1fr; } .role-slots { grid-template-columns: 1fr; } .submit-bar { position: sticky; bottom: 0; z-index: 2; padding: 12px; background: var(--el-bg-color); } }
|
2026-08-11 23:40:41 +08:00
|
|
|
|
</style>
|