This commit is contained in:
ltx 2026-09-29 17:23:42 +08:00
commit 4cd0578673
3 changed files with 56 additions and 3 deletions

View File

@ -0,0 +1,42 @@
// 花样展示值来自后端评分单/计算快照,查询页面不重算历史总分。
export function isFactorRule(code) {
return /FREESTYLE|SHOWCASE/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);
}
export function judgeScoreText(sheet, ruleCode) {
if (!sheet) return '—';
if (sheet.isDisqualified) return 'DQ';
if (!isFactorRule(ruleCode)) return fixedScore(sheet.totalScore);
const role = String(sheet.judgeRoleType || '').toUpperCase();
if (role === 'T') {
if (sheet.deductionScore == null) return '—';
return fixedScore(Math.max(0, 1 - Number(sheet.deductionScore)), 3);
}
if (role === 'P') return fixedScore(sheet.performanceScore);
if (role.startsWith('D')) return fixedScore(sheet.difficultyScore);
return fixedScore(sheet.totalScore);
}
export function calculationSummary(json) {
if (!json) return '';
try {
const data = typeof json === 'string' ? JSON.parse(json) : json;
const factors = data.factors;
if (!factors) return '';
const lines = [`公式:${data.formula};D=${fixedScore(factors.d)},P=${fixedScore(factors.p)},Q=${fixedScore(factors.q)},M=${fixedScore(factors.m, 3)}`];
const failure = data.failureAggregation;
if (failure?.performanceAverageFailureCount != null) {
lines.push(`失误:表现裁判 [${failure.performanceFailureCounts.join('、')}],平均 ${failure.performanceAverageFailureCount};主裁 ${failure.chiefFailureCount};合计 ${failure.combinedFailureCount} → ${failure.roundedFailureCount} 次;失误扣分 ${fixedScore(failure.failureDeduction, 3)},其他扣分 ${fixedScore(failure.otherDeduction, 3)}`);
} else if (failure) {
lines.push(`失误(历史口径):表现裁判 [${failure.performanceFailureCounts.join('、')}],主裁 ${failure.chiefFailureCount};平均 ${failure.averageFailureCount} → ${failure.roundedFailureCount} 次;失误扣分 ${fixedScore(failure.failureDeduction, 3)},其他扣分 ${fixedScore(failure.otherDeduction, 3)}`);
}
lines.push(data.calculationVersion ? '按 BUG-71:因子先舍入,再计算总分。' : '历史计算快照:可能尚未按新规则重算。');
return lines.join('\n');
} catch { return ''; }
}

View File

@ -8,7 +8,7 @@
<section class="context-bar"><div><span class="context-icon"><el-icon><User /></el-icon></span><small>项目 / 组别</small><b>{{ projectGroup }}</b></div><i></i><div><span class="context-icon"><el-icon><Location /></el-icon></span><small>场次 / 场地</small><b>{{ sessionVenue }}</b></div></section>
<section v-if="viewType === 'score'" class="score-layout">
<div class="hero-card"><div class="panel-shine"></div><div class="entity-chip"><el-icon><component :is="displayKind === 'team' ? UserFilled : User" /></el-icon><span>{{ displayKind === 'team' ? 'TEAM' : 'PLAYER' }}</span>{{ displayKindLabel }}成绩</div><div class="entity-name">{{ singleName }}</div><div v-if="singleAffiliation" class="entity-affiliation">{{ singleAffiliation }}</div><div class="hero-meta"><span v-if="payload.entryNo">出场号 <b>{{ payload.entryNo }}</b></span><span v-if="payload.rank">当前排名 <b>第 {{ payload.rank }} 名</b></span></div><div class="hero-score"><strong class="score-ticker">{{ animatedValue('hero', payload.totalScore ?? payload.score) }}</strong><small>分</small></div><div class="score-beam"><i></i></div><div v-if="payload.isDisqualified" class="dq-stamp">DQ</div></div>
<div class="score-metrics" :style="{ gridTemplateColumns: `repeat(${Math.max(scoreCards.length, 1)}, 1fr)` }"><article v-for="(item,index) in scoreCards" :key="item.key" :class="['metric-card', item.key]" :style="{ '--delay': `${index * 90}ms` }"><div class="metric-head"><span class="metric-icon"><el-icon><component :is="scoreIconComponents[item.key]" /></el-icon></span><small>{{ item.en }}</small></div><strong class="score-ticker">{{ animatedValue(`metric:${item.key}`, payload[item.field]) }}</strong><div class="metric-line"></div><b>{{ item.label }}</b></article></div>
<div class="score-metrics" :style="{ gridTemplateColumns: `repeat(${Math.max(scoreCards.length, 1)}, 1fr)` }"><article v-for="(item,index) in scoreCards" :key="item.key" :class="['metric-card', item.key]" :style="{ '--delay': `${index * 90}ms` }"><div class="metric-head"><span class="metric-icon"><el-icon><component :is="scoreIconComponents[item.key]" /></el-icon></span><small>{{ item.en }}</small></div><strong class="score-ticker">{{ item.field === 'deductionFactor' ? Number(payload[item.field]).toFixed(3) : animatedValue(`metric:${item.key}`, payload[item.field]) }}</strong><div class="metric-line"></div><b>{{ item.label }}</b></article></div>
</section>
<section v-else class="ranking-layout">
<div class="ranking-heading"><div><span><i></i>{{ displayKind === 'team' ? 'TEAM RANKING' : 'INDIVIDUAL RANKING' }}</span><h2>{{ displayKindLabel }}排名</h2></div><div class="ranking-status"><small>共 {{ rankingTotal }} 个{{ displayKind === 'team' ? '队伍' : '参赛者' }}<template v-if="rankingPageCount > 1"> · 第 {{ rankingPageIndex + 1 }}/{{ rankingPageCount }} 页</template></small><div v-if="rankingPageCount > 1" class="page-dots"><i v-for="page in rankingPageCount" :key="page" :class="{ active: page - 1 === rankingPageIndex }"></i></div></div></div>
@ -55,13 +55,13 @@ const sessionVenue=computed(()=>[formatScreenSession(effectiveCast.value?.sessio
const singleName=computed(()=>displayKind.value==='team'?(payload.value.teamName||payload.value.participantName||'未命名队伍'):(payload.value.participantName||payload.value.teamName||'未命名选手'));
const singleAffiliation=computed(()=>displayKind.value==='team'?(payload.value.unitName||''):(payload.value.teamName&&payload.value.teamName!==singleName.value?payload.value.teamName:(payload.value.unitName||'')));
const scoreIconComponents={difficulty:Setting,performance:Medal,required:DocumentChecked,deduction:Flag};
const scoreCards=computed(()=>[{key:'difficulty',label:'难度',en:'DIFFICULTY',field:'difficultyScore'},{key:'performance',label:'表现',en:'PERFORMANCE',field:'performanceScore'},{key:'required',label:'规定',en:'REQUIRED',field:'requiredScore'},{key:'deduction',label:'扣分',en:'DEDUCTION',field:'deductionScore'}].filter(item=>item.key!=='difficulty'||!String(effectiveCast.value?.scoreRuleCode||'').toUpperCase().includes('PRESCRIBED')));
const scoreCards=computed(()=>[{key:'difficulty',label:'难度',en:'DIFFICULTY',field:'difficultyScore'},{key:'performance',label:'表现',en:'PERFORMANCE',field:'performanceScore'},{key:'required',label:'规定',en:'REQUIRED',field:'requiredScore'},{key:'deduction',label:payload.value.deductionFactor!=null?'得分系数 M':'扣分',en:'DEDUCTION',field:payload.value.deductionFactor!=null?'deductionFactor':'deductionScore'}].filter(item=>item.key!=='difficulty'||!String(effectiveCast.value?.scoreRuleCode||'').toUpperCase().includes('PRESCRIBED')));
const rankingAll=computed(()=>Array.isArray(payload.value)?payload.value:[]),rankingTotal=computed(()=>rankingAll.value.length),rankingPageCount=computed(()=>Math.max(1,Math.ceil(rankingTotal.value/6))),rankingPageIndex=computed(()=>rankPage.value%rankingPageCount.value),rankingRows=computed(()=>rankingAll.value.slice(rankingPageIndex.value*6,rankingPageIndex.value*6+6)),maxRankScore=computed(()=>Math.max(...rankingAll.value.map(row=>Number(row.totalScore??row.score)||0),1));
function displayName(row){return displayKind.value==='team'?(row.teamName||row.participantName||'未命名队伍'):(row.participantName||row.teamName||'未命名选手')}
function displaySubName(row){if(displayKind.value==='team')return row.unitName||'';return row.teamName&&row.teamName!==displayName(row)?row.teamName:(row.unitName||'')}
function rankNumber(row,index){const rank=Number(row.rank);return Number.isFinite(rank)&&rank>0?rank:rankingPageIndex.value*6+index+1}
function rankWidth(row){return `${Math.max(12,Math.round((Number(row.totalScore??row.score)||0)/maxRankScore.value*100))}%`}
function value(input){if(input===undefined||input===null||input==='')return '0';const n=Number(input);return Number.isFinite(n)?n.toFixed(2).replace(/\.00$/,'').replace(/(\.\d)0$/,'$1'):input}
function value(input){if(input===undefined||input===null||input==='')return '0';const n=Number(input);return Number.isFinite(n)?n.toFixed(2):input}
function rankAnimationKey(row,index){return `rank:${row.entryNo||displayName(row)||rankingPageIndex.value*6+index}`}
function animatedValue(key,fallback){return Object.prototype.hasOwnProperty.call(animatedNumbers.value,key)?value(animatedNumbers.value[key]):value(fallback)}
function animateScoreNumbers(){

View File

@ -55,11 +55,20 @@
<el-table-column label="组别" min-width="112" fixed="left"><template #default="{ row }"><span class="group-cell">{{ row.groupName || row._session?.groupName || '混合组别' }}</span></template></el-table-column>
<el-table-column label="运动员姓名" min-width="132" fixed="left"><template #default="{ row }"><strong class="athlete-name">{{ row.teamName || row.participantName }}</strong><small class="subtext">{{ row.entryNo }}</small></template></el-table-column>
<el-table-column label="单位" min-width="180" fixed="left"><template #default="{ row }"><span class="unit-name">{{ row.unitName || row.teamName || '—' }}</span></template></el-table-column>
<el-table-column v-for="group in judgeMatrixGroups" :key="group.key" :label="group.label" align="center" header-align="center">
<el-table-column v-for="judge in group.judges" :key="judge.key" :label="judge.label" width="112" align="center">
<template #default="{ row }"><span v-if="judgeScoreFor(row, judge.key)" :class="['judge-score', { pending: judgeScoreFor(row, judge.key).sheetStatus < 10 }]">{{ judgeScoreFor(row, judge.key).sheetStatus >= 10 ? judgeScoreText(judgeScoreFor(row, judge.key), row._session?.scoreRuleCode) : '待提交' }}</span><span v-else class="muted">—</span></template>
</el-table-column>
</el-table-column>
<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="难度分(得分)" min-width="116" align="center"><template #default="{ row }">{{ score(row.result?.difficultyScore) }}</template></el-table-column>
<el-table-column :label="performanceScoreLabel" min-width="100" align="center"><template #default="{ row }">{{ score(row.result?.performanceScore) }}</template></el-table-column>
<el-table-column :label="requiredScoreLabel" min-width="100" align="center"><template #default="{ row }">{{ score(row.result?.requiredScore) }}</template></el-table-column>
<el-table-column :label="deductionScoreLabel" min-width="100" align="center"><template #default="{ row }">{{ score(row.result?.deductionScore) }}</template></el-table-column>
<el-table-column v-if="showDifficulty" label="难度分" width="88" align="center"><template #default="{ row }">{{ score(row.result?.difficultyScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '表现系数 P' : '表现分'" width="110" align="center"><template #default="{ row }">{{ score(row.result?.performanceScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '规定系数 Q' : '规定分'" width="110" align="center"><template #default="{ row }">{{ score(row.result?.requiredScore) }}</template></el-table-column>
<el-table-column :label="showFactorScores ? '得分系数 M' : '扣分'" 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 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>
@ -115,6 +124,7 @@
</template>
<script setup>
import { fixedScore, isFactorRule, 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';
@ -175,6 +185,7 @@ const castEntityLabel=computed(()=>castEntityKind.value==='team'?'团队':'个
const rankingKey=ref('');
const rankingOptions=computed(()=>{const map=new Map();for(const x of sessions.value){if(x.eventProjectId&&x.eventProjectGroupId){const key=`${x.eventProjectId}:${x.eventProjectGroupId}`;if(!map.has(key))map.set(key,{...x,key})}}return [...map.values()]});
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 showDifficulty=computed(()=>!isPrescribedProject.value);
const performanceScoreLabel=computed(()=>isPrescribedProject.value?'创意编排分':'表现分(系数)');