秩序册导入和修复交互花样计算规则
This commit is contained in:
parent
2c139575f2
commit
4b53815079
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user