秩序册导入和修复交互花样计算规则

This commit is contained in:
马梓彭 2026-10-10 11:08:06 +08:00
parent 2c139575f2
commit 4b53815079
4 changed files with 95 additions and 22 deletions

View File

@ -296,13 +296,14 @@ const handleNext = async () => {
// 处理提交
const handleSubmit = async () => {
if (!stepRef.value) return;
if (!stepRef.value || submitting.value) return;
successDialogVisible.value = false;
submitting.value = true;
try {
// 调用子组件的验证方法
const valid = await stepRef.value.validate();
if (valid) {
submitting.value = true;
const b = formData.value.basicInfo;
const rawProjects = formData.value.projectConfig?.projects ?? [];
const projects = rawProjects.map((p) => {
@ -346,9 +347,8 @@ const handleSubmit = async () => {
registrationLimit: formData.value.projectConfig?.registrationLimit,
feeConfig: formData.value.feeConfig,
};
if (payload.id) await eventPut(payload);
else await eventPost(payload);
submitting.value = false;
const response = payload.id ? await eventPut(payload) : await eventPost(payload);
if (response?.statusCode !== 200) return;
successDialogVisible.value = true;
} else {
ElMessage.warning('请完善当前步骤的信息');
@ -356,6 +356,7 @@ const handleSubmit = async () => {
} catch (error) {
console.error('提交失败:', error);
ElMessage.error('提交失败,请重试');
} finally {
submitting.value = false;
}
};

View File

@ -4,11 +4,11 @@
<el-alert v-if="!eventId" title="请先在打分系统工作台选择计分模式和赛事" type="warning" show-icon :closable="false"/>
<section v-else class="import-card">
<div class="steps"><div class="active"><b>1</b><span>下载模板</span></div><i></i><div :class="{active:file}"><b>2</b><span>上传解析</span></div><i></i><div :class="{active:result?.status===10}"><b>3</b><span>确认导入</span></div></div>
<div class="actions"><el-button type="primary" plain @click="downloadTemplate">下载秩序册模板</el-button><el-upload :auto-upload="false" :show-file-list="false" accept=".xls,.xlsx" :on-change="selectFile"><el-button type="primary">选择 Excel 文件</el-button></el-upload><span v-if="file">{{ file.name }}</span><el-button type="success" :loading="parsing" :disabled="!file" @click="parseFile">上传并解析</el-button></div>
<div class="actions"><el-button type="primary" plain @click="downloadTemplate">下载秩序册模板</el-button><el-upload :auto-upload="false" :show-file-list="false" accept=".xls,.xlsx" :disabled="parsing||confirming" :on-change="selectFile"><el-button type="primary" :disabled="parsing||confirming">选择 Excel 文件</el-button></el-upload><span v-if="file">{{ file.name }}</span><el-button type="success" :loading="parsing" :disabled="!file||parsing||confirming" @click="parseFile">上传并解析</el-button></div>
<template v-if="result">
<div class="summary"><div><small>总行数</small><strong>{{result.totalRows}}</strong></div><div><small>有效行</small><strong>{{result.validRows}}</strong></div><div><small>场次</small><strong>{{result.preview?.sessionCount||0}}</strong></div><div><small>出场记录</small><strong>{{result.preview?.entryCount||0}}</strong></div><div><small>裁判</small><strong>{{result.preview?.refereeCount||0}}</strong></div><div class="error"><small>错误 / 警告</small><strong>{{result.errorCount}} / {{result.warningCount}}</strong></div></div>
<el-table v-if="result.errors?.length" :data="result.errors" max-height="330"><el-table-column prop="row" label="行号" width="80"/><el-table-column prop="column" label="列名" width="150"/><el-table-column prop="message" label="问题说明" min-width="300"/><el-table-column label="级别" width="100"><template #default="{row}"><el-tag :type="row.level==='error'?'danger':'warning'">{{row.level==='error'?'错误':'警告'}}</el-tag></template></el-table-column></el-table>
<div class="confirm"><span v-if="result.errorCount">请修正错误后重新上传,存在错误时不能确认导入。</span><span v-else>解析通过,确认后将生成场地、场次、出场记录及裁判数据。</span><el-button type="primary" :loading="confirming" :disabled="result.status!==10||result.errorCount>0" @click="confirmImport">确认导入</el-button></div>
<el-table v-if="result.errors?.length" :data="result.errors" max-height="330"><el-table-column prop="sheet" label="工作表" width="110"/><el-table-column prop="row" label="行号" width="80"/><el-table-column prop="column" label="列名" width="150"/><el-table-column prop="message" label="问题说明" min-width="300"/><el-table-column label="级别" width="100"><template #default="{row}"><el-tag :type="row.level==='error'?'danger':'warning'">{{row.level==='error'?'错误':'警告'}}</el-tag></template></el-table-column></el-table>
<div class="confirm"><span v-if="confirmError">{{confirmError}}</span><span v-else-if="result.errorCount">请修正错误后重新上传,存在错误时不能确认导入。</span><span v-else>解析通过,确认后将生成场地、场次、出场记录及裁判数据。</span><el-button type="primary" :loading="confirming" :disabled="parsing||confirming||!!confirmError||result.status!==10||result.errorCount>0" @click="confirmImport">确认导入</el-button></div>
</template>
</section>
</div>
@ -30,15 +30,52 @@ onMounted(() => {
const currentEvent=resolveScoringEvent(route.query);
const eventId=ref(currentEvent.eventId),eventName=ref(currentEvent.eventName);
const file=ref(null),result=ref(null),parsing=ref(false),confirming=ref(false);
const confirmError = ref('');
let operationVersion = 0;
if(!eventId.value)router.replace({path:'/scoring/launcher',query:{selectEvent:'1'}});
function changeEvent(){router.push({path:'/scoring/launcher',query:{selectEvent:'1'}})}
function syncEvent(){const next=resolveScoringEvent(route.query);eventId.value=next.eventId;eventName.value=next.eventName;file.value=null;result.value=null}
function syncEvent(){const next=resolveScoringEvent(route.query);eventId.value=next.eventId;eventName.value=next.eventName;file.value=null;result.value=null;confirmError.value='';operationVersion++}
watch(()=>[route.query.eventId,route.query.eventName],syncEvent);
function selectFile(uploadFile){file.value=uploadFile.raw;result.value=null}
function selectFile(uploadFile){file.value=uploadFile.raw;result.value=null;confirmError.value='';operationVersion++}
function saveBlob(blob,name){const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
async function downloadTemplate(){saveBlob(await orderBookTemplateGet(),'秩序册导入模板.xlsx')}
async function parseFile(){parsing.value=true;try{const res=await orderBookImportPost(eventId.value,file.value);result.value=res?.data||null;if(result.value)ElMessage.success(result.value.errorCount?'解析完成,请处理错误项':'解析通过,请确认导入')}finally{parsing.value=false}}
async function confirmImport(){await ElMessageBox.confirm('确认导入当前秩序册?本次导入将覆盖原有编排,并清除旧场次的裁判分配;导入后可直接配置检录员。','确认导入',{type:'warning'});confirming.value=true;try{const res=await orderBookConfirmPost(result.value.batchId);ElMessage.success(`导入完成:${res?.data?.entryCount||0} 条出场记录`);result.value=null;file.value=null}finally{confirming.value=false}}
async function parseFile() {
if (!file.value || !eventId.value || parsing.value || confirming.value) return;
const version = ++operationVersion;
result.value = null; confirmError.value = ''; parsing.value = true;
try {
const res = await orderBookImportPost(eventId.value, file.value);
if (version !== operationVersion || res?.statusCode !== 200) return;
result.value = res.data || null;
if (!result.value) return;
if (result.value.errorCount > 0 || result.value.status === 20) ElMessage.warning('解析完成,请处理错误项后重新上传');
else ElMessage.success('解析通过,请确认导入');
} catch (error) {
console.error('秩序册解析失败', error); // 请求拦截器负责提示,保留文件供重试。
} finally { parsing.value = false; }
}
async function confirmImport() {
if (parsing.value || confirming.value || confirmError.value || result.value?.status !== 10 || result.value.errorCount > 0) return;
const version = operationVersion, batchId = result.value.batchId;
confirming.value = true;
try {
await ElMessageBox.confirm('确认导入当前秩序册?本次导入将覆盖原有编排,并清除旧场次的裁判分配;导入后可直接配置检录员。', '确认导入', { type: 'warning' });
if (version !== operationVersion) return;
const res = await orderBookConfirmPost(batchId);
if (version !== operationVersion) return;
if (res?.statusCode !== 200 || !res.data) {
confirmError.value = '确认导入失败,请根据错误提示处理后重新上传并解析。';
return;
}
ElMessage.success('导入完成:' + res.data.entryCount + ' 条出场记录');
result.value = null; file.value = null;
} catch (error) {
if (error !== 'cancel' && error !== 'close' && version === operationVersion)
confirmError.value = '确认导入未完成,请核实结果后重新上传并解析。';
} finally { confirming.value = false; }
}
</script>
<style scoped lang="scss">

View File

@ -3,12 +3,23 @@ export function isFactorRule(code) {
return /FREESTYLE|SHOWCASE/i.test(String(code || ''));
}
export function isWireRule(code) {
return /FREESTYLE_WIRE/i.test(String(code || ''));
}
export function fixedScore(value, decimals = 2) {
if (value === null || value === undefined || value === '') return '—';
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(decimals) : String(value);
}
// 列表和详情的汇总列均展示最终 M;主裁自己的扣分仍由 judgeScoreText 展示。
export function deductionScoreText(result, ruleCode) {
return isFactorRule(ruleCode)
? fixedScore(result?.deductionFactor, 3)
: fixedScore(result?.deductionScore);
}
export function judgeScoreText(sheet, ruleCode) {
if (!sheet) return '—';
if (sheet.isDisqualified) return 'DQ';

View File

@ -63,8 +63,8 @@
<el-table-column label="裁判提交" width="92" align="center"><template #default="{ row }"><span :class="['submit-progress', { done: allSubmitted(row) }]">{{ submittedCount(row) }}/{{ row.judgeScores?.length || 0 }}</span></template></el-table-column>
<el-table-column v-if="showDifficulty" label="难度分 D" min-width="100" align="center"><template #default="{ row }">{{ score(row.result?.difficultyScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '表现系数 P' : performanceScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ score(row.result?.performanceScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '规定系数 Q' : requiredScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ score(row.result?.requiredScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '得分系数 M' : deductionScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ isFactorRule(row._session?.scoreRuleCode) ? fixedScore(row.result?.deductionFactor, 3) : score(row.result?.deductionScore) }}</template></el-table-column>
<el-table-column v-if="showRequiredScore" :label="showFactorScores ? '规定系数 Q' : requiredScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ score(row.result?.requiredScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '得分系数 M' : deductionScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ deductionScoreText(row.result, row._session?.scoreRuleCode) }}</template></el-table-column>
<el-table-column label="总分" width="96" align="center"><template #default="{ row }"><b class="total" :class="{ dq: row.result?.isDisqualified }">{{ row.result?.isDisqualified ? 'DQ' : score(row.result?.totalScore ?? row.result?.scoreText) }}</b></template></el-table-column>
<el-table-column label="排名" width="72" align="center"><template #default="{ row }"><span v-if="row.result?.rank" :class="['rank-number', `rank-${row.result.rank}`]">{{ row.result.rank }}</span><span v-else class="muted">—</span></template></el-table-column>
<el-table-column label="状态" width="96" fixed="right" align="center"><template #default="{ row }"><el-tag v-if="row.result" :type="resultTag(row.result.resultStatus)" effect="light">{{ row.result.resultStatusName }}</el-tag><span v-else class="muted">计算中</span></template></el-table-column>
@ -88,9 +88,9 @@
<el-table :data="detailRow ? [detailRow] : []" border class="score-detail-matrix">
<el-table-column v-for="group in detailJudgeGroups" :key="group.key" :label="group.label" align="center" header-align="center">
<el-table-column v-for="judge in group.judges" :key="judge.scoreSheetId || `${judge.judgeRoleType}:${judge.judgeRole}`" :label="judgeColumnLabel(judge)" min-width="116" align="center">
<template #default><button class="judge-score-link" :disabled="judge.sheetStatus < 10" @click="showJudgeDetail(judge)"><strong>{{ judge.sheetStatus >= 10 ? score(judgeDisplayValue(judge, group.key)) : '待提交' }}</strong><small>{{ judge.judgeName || '未绑定裁判' }}</small></button></template>
<template #default><button class="judge-score-link" :disabled="judge.sheetStatus < 10" @click="showJudgeDetail(judge)"><strong>{{ judge.sheetStatus >= 10 ? judgeScoreText(judge, detailRow?._session?.scoreRuleCode) : '待提交' }}</strong><small>{{ judge.judgeName || '未绑定裁判' }}</small></button></template>
</el-table-column>
<el-table-column :label="group.summaryLabel" width="92" align="center"><template #default><b class="detail-group-total">{{ score(detailAggregateScore(group.key)) }}</b></template></el-table-column>
<el-table-column :label="group.summaryLabel" width="110" align="center"><template #default><b class="detail-group-total">{{ detailAggregateText(group.key) }}</b></template></el-table-column>
</el-table-column>
<el-table-column label="总得分" width="104" align="center"><template #default><b class="total">{{ detailRow?.result?.isDisqualified ? 'DQ' : score(detailRow?.result?.totalScore) }}</b></template></el-table-column>
<el-table-column label="排名" width="80" align="center"><template #default><span v-if="detailRow?.result?.rank" :class="['rank-number', `rank-${detailRow.result.rank}`]">{{ detailRow.result.rank }}</span><span v-else class="muted">—</span></template></el-table-column>
@ -120,7 +120,7 @@
</template>
<script setup>
import { fixedScore, isFactorRule, judgeScoreText, calculationSummary } from '../score-format.mjs';
import { deductionScoreText, isFactorRule, isWireRule, judgeScoreText, calculationSummary } from '../score-format.mjs';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
@ -183,22 +183,44 @@ const rankingOptions=computed(()=>{const map=new Map();for(const x of sessions.v
const rankingSelection=computed(()=>rankingOptions.value.find(x=>x.key===rankingKey.value));
const showFactorScores=computed(()=>selectedProjectSessions.value.some(session=>isFactorRule(session.scoreRuleCode)));
const isPrescribedProject=computed(()=>selectedProjectSessions.value.some(session=>String(session.scoreRuleCode||'').toUpperCase().includes('PRESCRIBED')));
const showRequiredScore=computed(()=>selectedProjectSessions.value.some(session=>!isWireRule(session.scoreRuleCode)));
const showDifficulty=computed(()=>!isPrescribedProject.value);
const performanceScoreLabel=computed(()=>isPrescribedProject.value?'创意编排分':'表现分(系数)');
const requiredScoreLabel=computed(()=>isPrescribedProject.value?'完成质量分':'规定(系数)');
const deductionScoreLabel=computed(()=>isPrescribedProject.value?'扣分':'扣分(系数)');
const difficultyRoles=new Set(['DA','DB','DJ','DT','DGA','DGB']);
const difficultyRoles=new Set(['DP','DM','DR','DA','DB','DJ','DT','DGA','DGB']);
function judgeGroupKey(sheet){const role=String(sheet?.judgeRoleType||'').toUpperCase();if(difficultyRoles.has(role))return 'difficulty';if(role==='P'||role==='CJ')return 'performance';if(role==='CQ')return 'required';if(role==='T')return 'deduction';return 'other'}
function judgeSeatKey(sheet){return `${String(sheet.judgeRoleType||'').toUpperCase()}:${sheet.judgeRole||sheet.judgeRoleType||''}`}
function judgeScoreFor(row,key){return (row.judgeScores||[]).find(sheet=>judgeSeatKey(sheet)===key)}
const judgeMatrixGroups=computed(()=>{
const seats=new Map();
for(const row of entries.value){
for(const sheet of row.judgeScores||[]){
const key=judgeSeatKey(sheet);
if(!seats.has(key))seats.set(key,{key,label:judgeColumnLabel(sheet),groupKey:judgeGroupKey(sheet)});
}
}
return [
{key:'difficulty',label:'难度裁判'},
{key:'performance',label:'表现 / 创意裁判'},
{key:'required',label:'完成质量裁判'},
{key:'deduction',label:'主裁判'},
{key:'other',label:'其他裁判'},
].map(group=>({...group,judges:[...seats.values()].filter(seat=>seat.groupKey===group.key).sort((a,b)=>a.label.localeCompare(b.label,'zh-CN',{numeric:true}))})).filter(group=>group.judges.length);
});
const detailJudgeGroups=computed(()=>{
const prescribed=String(detailRow.value?._session?.scoreRuleCode||'').toUpperCase().includes('PRESCRIBED');
const ruleCode=detailRow.value?._session?.scoreRuleCode;
const prescribed=String(ruleCode||'').toUpperCase().includes('PRESCRIBED');
const factor=isFactorRule(ruleCode);
const wire=isWireRule(ruleCode);
const sheets=[...(detailRow.value?.judgeScores||[])].sort((a,b)=>String(a.judgeRole||'').localeCompare(String(b.judgeRole||''),'zh-CN',{numeric:true}));
const groups=[
{key:'difficulty',label:'难度分(得分)',summaryLabel:'得分'},
{key:'performance',label:prescribed?'创意编排分':'表现分(系数)',summaryLabel:'得分'},
{key:'required',label:prescribed?'完成质量分':'规定(系数)',summaryLabel:'得分'},
{key:'deduction',label:prescribed?'扣分':'扣分明细',summaryLabel:prescribed?'合计':'综合扣分'},
{key:'deduction',label:prescribed?'扣分':'扣分明细',summaryLabel:factor?'得分系数 M':'合计'},
];
const result=groups.filter(group=>group.key!=='difficulty'||!prescribed).map(group=>({...group,judges:sheets.filter(sheet=>judgeGroupKey(sheet)===group.key)}));
const result=groups.filter(group=>(group.key!=='difficulty'||!prescribed)&&(group.key!=='required'||!wire)).map(group=>({...group,judges:sheets.filter(sheet=>judgeGroupKey(sheet)===group.key)}));
const others=sheets.filter(sheet=>judgeGroupKey(sheet)==='other');
if(others.length)result.push({key:'other',label:'其他评分',summaryLabel:'合计',judges:others});
return result;
@ -322,8 +344,8 @@ function submittedCount(row){return (row.judgeScores||[]).filter(x=>x.sheetStatu
function hasSubmittedJudgeScores(row){return (row.judgeScores||[]).some(x=>x.scoreSheetId>0&&x.sheetStatus>=10&&x.sheetStatus<90)}
function allSubmitted(row){return !!row.judgeScores?.length&&submittedCount(row)===row.judgeScores.length}
function judgeColumnLabel(sheet){return sheet?.judgeRole||sheet?.judgeRoleName||'裁判'}
function judgeDisplayValue(sheet,groupKey){if(!sheet)return null;if(groupKey==='difficulty')return sheet.difficultyScore;if(groupKey==='performance')return sheet.performanceScore;if(groupKey==='required')return sheet.requiredScore;if(groupKey==='deduction')return sheet.deductionScore;return sheet.totalScore}
function detailAggregateScore(groupKey){const result=detailRow.value?.result;if(!result)return null;return ({difficulty:result.difficultyScore,performance:result.performanceScore,required:result.requiredScore,deduction:result.deductionScore,other:null})[groupKey]}
function detailAggregateText(groupKey){return groupKey==='deduction'?deductionScoreText(detailRow.value?.result,detailRow.value?._session?.scoreRuleCode):score(detailAggregateScore(groupKey))}
function showJudgeDetail(sheet){if(!sheet||sheet.sheetStatus<10)return;selectedJudgeSheet.value=sheet;judgeDetailVisible.value=true}
async function viewJudgeScoreBoard(){
const sheetId=selectedJudgeSheet.value?.scoreSheetId;
@ -364,6 +386,8 @@ function formatSessionName(session, index) {
(session.projectName && rawName.includes(session.projectName))
);
if (rawName && !isAutoCompound) {
const numericMatch = rawName.match(/^(?:第\s*)?(\d+)(?:\s*场)?$/);
if (numericMatch) return `第 ${parseInt(numericMatch[1], 10)} 场`;
return rawName;
}
const no = String(session.sessionNo || '').trim();