diff --git a/src/views/match/manage/JudgeAssignmentPanel.vue b/src/views/match/manage/JudgeAssignmentPanel.vue index 5c48d92..c3c542e 100644 --- a/src/views/match/manage/JudgeAssignmentPanel.vue +++ b/src/views/match/manage/JudgeAssignmentPanel.vue @@ -19,6 +19,7 @@
{{ project.projectName }}{{ project.ruleName }}
+ @@ -27,19 +28,23 @@ - +
{{ section.title }}
-
- {{ slot.name }}{{ candidateName(assignmentMap[slot.code]) }}{{ viewOnly ? '未分配' : '点击后从右侧选择裁判' }} +
+
{{ slot.name }}{{ quotaText(slot) }}
+
+ {{ candidateName(id) }} +
+ {{ viewOnly ? '未分配' : '点击后从右侧选择裁判' }}
-
裁判报名
{{ item.name }} / {{ item.mobile }}
+
{{ selectedSlot ? `可选${selectedSlot.name}` : '裁判报名' }}
{{ item.name }} / {{ item.mobile }}
@@ -84,9 +89,15 @@ const eventRefereeColumns = [ { 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)); +const assignmentColumns = [{ prop: 'sessionName', label: '场次', minWidth: 160 }, { prop: 'venueName', label: '场地', minWidth: 160 }, { prop: 'judgeCount', label: '裁判人数', width: 100 }, { prop: 'assigned', label: '状态', width: 110 }, { prop: 'action', label: '操作', width: 200, fixed: 'right' }]; +// 一个裁判类型可分配多名裁判(主裁固定 1 人,其余类型不限人数,成绩按平均值计算)。 +const allSlots = computed(() => roleSections.value.flatMap(section => section.slots)); +const selectedSlot = computed(() => allSlots.value.find(slot => slot.code === selectedRole.value)); +const assignedIds = computed(() => new Set(Object.values(assignmentMap).flat())); +const eligibleCandidates = computed(() => selectedSlot.value ? candidates.value.filter(item => (item.roles & selectedSlot.value.roleFlag) !== 0 && !assignedIds.value.has(item.eventRefereeId)) : []); +const picked = code => assignmentMap[code] || []; +const minOf = slot => slot.minCount ?? 1; +const quotaText = slot => slot.maxCount ? `${picked(slot.code).length}/${slot.maxCount} 人` : `已选 ${picked(slot.code).length} 人(不限人数)`; async function load() { if (!props.active || !props.eventId) return; @@ -99,22 +110,37 @@ async function load() { 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 || '[]'); + const sections = (project.judgeRoleSections || []).map(section => ({ ...section, slots: section.slots || [] })); + // 计分规则调整后可能存在规则里已不存在的裁判类型,单独列出避免已分配裁判被静默丢弃。 + const known = new Set(sections.flatMap(section => section.slots.map(slot => slot.code))); + const orphans = [...new Set((session.assignments || []).map(item => item.judgeRoleType).filter(code => code && !known.has(code)))]; + if (orphans.length) sections.push({ title: '其他(计分规则已调整,请重新分配)', slots: orphans.map(code => ({ code, name: code, minCount: 0, maxCount: null, roleFlag: 0 })) }); + roleSections.value = sections; Object.keys(assignmentMap).forEach(key => delete assignmentMap[key]); - session.assignments.forEach(item => assignmentMap[item.judgeRole] = item.eventRefereeId); + sections.forEach(section => section.slots.forEach(slot => (assignmentMap[slot.code] = []))); + (session.assignments || []).forEach(item => assignmentMap[item.judgeRoleType]?.push(item.eventRefereeId)); candidates.value = readonly ? [] : (await judgeCandidatesGet(props.eventId, session.sessionId)).data || []; - selectedRole.value = roleSections.value[0]?.slots?.[0]?.code || ''; assignVisible.value = true; + selectedRole.value = sections[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]; } +function chooseCandidate(item) { + const slot = selectedSlot.value; + if (!slot) return ElMessage.warning('请先选择裁判类型'); + const list = assignmentMap[slot.code] || (assignmentMap[slot.code] = []); + if (slot.maxCount && list.length >= slot.maxCount) return ElMessage.warning(`${slot.name}最多分配 ${slot.maxCount} 名裁判`); + list.push(item.eventRefereeId); +} +function removeReferee(code, id) { const list = assignmentMap[code]; const index = list?.indexOf(id) ?? -1; if (index >= 0) list.splice(index, 1); } 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 }; }) }); + const incomplete = allSlots.value.find(slot => picked(slot.code).length < minOf(slot)); + if (incomplete) return ElMessage.warning(`${incomplete.name}至少需要分配 ${minOf(incomplete)} 名裁判`); + const assignments = allSlots.value.flatMap(slot => picked(slot.code).map(id => ({ judgeRole: slot.code, eventRefereeId: id, + refereeId: (candidates.value.find(item => item.eventRefereeId === id) || currentSession.value.assignments?.find(item => item.eventRefereeId === id))?.refereeId || 0 }))); + const result = await judgeAssignmentSavePost({ eventId: Number(props.eventId), sessionId: currentSession.value.sessionId, assignments }); + if (!result?.succeeded) return; 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 submitAll() { await ElMessageBox.confirm('提交后分配任务即可同步到裁判端,是否继续?', '提交确认', { type: 'warning' }); const result = await judgeAssignmentSubmitPost({ eventId: Number(props.eventId) }); if (!result?.succeeded) return; 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; } @@ -141,7 +167,10 @@ watch(() => props.active, value => value && load(), { immediate: true }); .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); } +.slot-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; } +.slot-quota { flex: none; font-size: 12px; color: var(--el-text-color-secondary); } +.slot-referees { display: flex; flex-wrap: wrap; gap: 6px; } +.slot-empty { 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; }