update:分页
This commit is contained in:
parent
5a6ddc9e0c
commit
4dc07ebd6c
@ -7,6 +7,12 @@ export const workbenchSessionsGet = (params) => http.get('/workbench/sessions',
|
||||
export const workbenchSessionEntriesGet = (sessionId, params = {}, config = {}) =>
|
||||
http.get(`/workbench/sessions/${sessionId}/entries`, { ...config, params });
|
||||
|
||||
export const workbenchProjectEntriesGet = (eventProjectId, params = {}, config = {}) =>
|
||||
http.get(`/workbench/projects/${eventProjectId}/entries`, { ...config, params });
|
||||
|
||||
export const workbenchEntryDetailGet = (sessionEntryId, config = {}) =>
|
||||
http.get(`/workbench/entries/${sessionEntryId}/detail`, config);
|
||||
|
||||
export const workbenchScreenCastPost = (data) => http.post('/workbench/screencast', data);
|
||||
|
||||
export const workbenchScreenCastCurrentGet = (params, config = {}) =>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<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 * 105 + 340}ms` }"><div class="metric-head"><div class="metric-title"><b>{{ item.label }}</b><small>{{ item.en }}</small></div><span class="metric-icon"><el-icon><component :is="scoreIconComponents[item.key]" /></el-icon></span></div><strong class="score-ticker">{{ animatedValue(`metric:${item.key}`, payload[item.field], item.field === 'deductionFactor' ? 3 : 2) }}</strong><div class="metric-line"></div></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 * 105 + 340}ms` }"><div class="metric-head"><div class="metric-title"><b>{{ item.label }}</b><small>{{ item.en }}</small></div><span class="metric-icon"><el-icon><component :is="scoreIconComponents[item.key]" /></el-icon></span></div><strong class="score-ticker">{{ animatedValue(`metric:${item.key}`, metricDisplayValue(item), item.field === 'deductionFactor' ? 3 : 2) }}</strong><div class="metric-line"></div></article></div>
|
||||
</section>
|
||||
<section v-else class="ranking-layout">
|
||||
<div class="ranking-heading"><div><h2>{{ displayKindLabel }}排名</h2><span><i></i>{{ displayKind === 'team' ? 'TEAM RANKING' : 'INDIVIDUAL RANKING' }}</span></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>
|
||||
@ -58,7 +58,17 @@ function unwrapApiData(response){let value=response;for(let i=0;i<3&&value&&type
|
||||
function normalizeCast(value){const normalized=normalizePayload(value),castType=Number(normalized?.castType);return normalized&&(castType===1||castType===2)&&normalized.payloadJson!=null?{...normalized,castType}:null}
|
||||
function cacheKey(scope=venueId){return `yd-event-screen-cast:${eventId}:${scope??'all'}`}
|
||||
function readCachedCast(){try{const scoped=localStorage.getItem(cacheKey()),fallback=localStorage.getItem(cacheKey(null));const cached=normalizeCast(JSON.parse(scoped||fallback||'null'));return cached&&Date.now()-Number(cached.cachedAt||0)<30000?cached:null}catch{return null}}
|
||||
function acceptPublishedCast(value){const next=normalizeCast(value);if(next){cast.value=next;offline.value=false;rankPage.value=0}else cast.value=null}
|
||||
function castIdentity(value){const id=value?.screenCastId||value?.castTime;return id==null?null:String(id)}
|
||||
function acceptPublishedCast(value){
|
||||
const next=normalizeCast(value);
|
||||
if(!next){cast.value=null;rankPage.value=0;return}
|
||||
offline.value=false;
|
||||
// 同一次投屏的轮询回包不替换画面,也不重播成绩动画或重置排名页码。
|
||||
const sameCast=cast.value&&castIdentity(cast.value)&&castIdentity(cast.value)===castIdentity(next)&&cast.value.castType===next.castType;
|
||||
if(sameCast)return;
|
||||
cast.value=next;
|
||||
rankPage.value=0;
|
||||
}
|
||||
function localPreviewCast(){
|
||||
const common={screenCastId:'local-screen-preview',eventName:'2026 跃动跳绳公开赛',projectName:'个人花样',groupName:'青年组',sessionNo:'1',venueName:'主赛场 A',scoreRuleCode:'FREESTYLE_SINGLE',displayKind:'individual'};
|
||||
if(route.query.preview==='ranking')return {...common,castType:2,payloadJson:JSON.stringify([
|
||||
@ -92,6 +102,13 @@ const scoreCards=computed(()=>{
|
||||
return [{key:'difficulty',label:'难度得分',en:'DIFFICULTY SCORE',field:'difficultyScore'},{key:'performance',label:prescribed?'创意编排分':factor?'表现系数':'表现',en:prescribed?'CREATIVE COMPOSITION':factor?'PERFORMANCE FACTOR':'PERFORMANCE SCORE',field:'performanceScore'},{key:'required',label:prescribed?'完成质量分':factor?'规定系数':'规定',en:prescribed?'EXECUTION QUALITY':factor?'REQUIRED FACTOR':'REQUIRED SCORE',field:'requiredScore'},{key:'deduction',label:factor?'扣分系数':'扣分',en:factor?'DEDUCTION FACTOR':'DEDUCTIONS',field:payload.value.deductionFactor!=null?'deductionFactor':'deductionScore'}]
|
||||
.filter(item=>(item.key!=='difficulty'||!prescribed)&&(item.key!=='required'||!wire));
|
||||
});
|
||||
// 表现原值 P 参与既有总分计算;大屏只把系数类项目的展示值转换为 1+P。
|
||||
function metricDisplayValue(item){
|
||||
const raw=payload.value[item.field];
|
||||
if(item.key!=='performance'||!isFactorRule(effectiveCast.value?.scoreRuleCode)||raw===null||raw===undefined||raw==='')return raw;
|
||||
const value=Number(raw);
|
||||
return Number.isFinite(value)?value+1:raw;
|
||||
}
|
||||
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||'')}
|
||||
@ -105,7 +122,7 @@ function animateScoreNumbers(){
|
||||
const targets={};
|
||||
if(viewType.value==='score'){
|
||||
targets.hero=payload.value.totalScore??payload.value.score;
|
||||
for(const item of scoreCards.value)targets[`metric:${item.key}`]=payload.value[item.field];
|
||||
for(const item of scoreCards.value)targets[`metric:${item.key}`]=metricDisplayValue(item);
|
||||
}else{
|
||||
rankingRows.value.forEach((row,index)=>{targets[rankAnimationKey(row,index)]=row.totalScore??row.score});
|
||||
}
|
||||
@ -133,7 +150,7 @@ async function loadCast(){if(!eventId||castRequesting)return;castRequesting=true
|
||||
function onViewportResize(){viewport.value={width:window.innerWidth,height:window.innerHeight}}
|
||||
function onThemeStorage(event){if(event.key===scoreScreenThemeKey(eventId)||event.key===null)scoreTheme.value=readScoreScreenTheme(eventId)}
|
||||
watch([castKey,rankingPageIndex],()=>nextTick(animateScoreNumbers),{immediate:true,flush:'post'});
|
||||
onMounted(()=>{storesTagsViewRoutes.setCurrenFullscreen(true);if(isLocalPreview)acceptPublishedCast(localPreviewCast());else{acceptPublishedCast(readCachedCast());if('BroadcastChannel' in window){castChannel=new BroadcastChannel(`yd-event-score-screen-${eventId}`);castChannel.onmessage=event=>acceptPublishedCast(event.data)}loadCast();pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&loadCast(),3000);rankTimer=window.setInterval(()=>rankPage.value++,9000)}if('BroadcastChannel' in window){themeChannel=new BroadcastChannel(scoreScreenThemeChannel(eventId));themeChannel.onmessage=event=>{scoreTheme.value=normalizeScoreScreenTheme(event.data)}}window.addEventListener('storage',onThemeStorage);window.addEventListener('resize',onViewportResize);onViewportResize()});
|
||||
onMounted(()=>{storesTagsViewRoutes.setCurrenFullscreen(true);if(isLocalPreview)acceptPublishedCast(localPreviewCast());else{acceptPublishedCast(readCachedCast());if('BroadcastChannel' in window){castChannel=new BroadcastChannel(`yd-event-score-screen-${eventId}`);castChannel.onmessage=event=>acceptPublishedCast(event.data)}loadCast();pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&loadCast(),3000);rankTimer=window.setInterval(()=>{if(document.visibilityState==='visible'&&viewType.value==='ranking'&&rankingPageCount.value>1)rankPage.value=(rankPage.value+1)%rankingPageCount.value},9000)}if('BroadcastChannel' in window){themeChannel=new BroadcastChannel(scoreScreenThemeChannel(eventId));themeChannel.onmessage=event=>{scoreTheme.value=normalizeScoreScreenTheme(event.data)}}window.addEventListener('storage',onThemeStorage);window.addEventListener('resize',onViewportResize);onViewportResize()});
|
||||
onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTimer);window.cancelAnimationFrame(numberAnimationFrame);castChannel?.close();themeChannel?.close();window.removeEventListener('storage',onThemeStorage);window.removeEventListener('resize',onViewportResize)});
|
||||
</script>
|
||||
|
||||
|
||||
@ -40,12 +40,11 @@
|
||||
<div class="entry-count"><b>{{ entries.length }}</b><small>当前列表</small></div>
|
||||
</div>
|
||||
<div class="filterbar">
|
||||
<div class="filter-fields"><el-input v-model="entryKeyword" clearable placeholder="搜索姓名、队伍或出场编号" @keyup.enter="() => loadEntries()" /><el-select v-model="resultStatus" clearable placeholder="全部成绩状态" @change="() => loadEntries()"><el-option v-for="x in resultStatuses" :key="x.value" :label="x.label" :value="x.value" /></el-select><el-button type="primary" @click="() => loadEntries()">查询</el-button><el-button text @click="resetFilters">重置</el-button></div>
|
||||
<div class="filter-fields"><el-input v-model="entryKeyword" clearable placeholder="搜索姓名、队伍或出场编号" @keyup.enter="applyFilters" @clear="applyFilters" /><el-select v-model="resultStatus" clearable placeholder="全部成绩状态" @change="applyFilters"><el-option v-for="x in resultStatuses" :key="x.value" :label="x.label" :value="x.value" /></el-select><el-button type="primary" @click="applyFilters">查询</el-button><el-button text @click="resetFilters">重置</el-button></div>
|
||||
<span class="filter-spacer"></span>
|
||||
<el-button v-if="activeTab === 'results'" type="success" :disabled="!rankingOptions.length" @click="openRanking">投出排名</el-button>
|
||||
<template v-if="activeTab === 'results'">
|
||||
<el-button type="primary" :loading="actionLoading" :disabled="!selectedResultIds.length" @click="batchConfirm">批量确认({{ selectedResultIds.length }})</el-button>
|
||||
<el-button @click="exportResults">成绩导出</el-button>
|
||||
<el-button :disabled="!selectedProject" @click="exportResults">成绩导出</el-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -53,14 +52,13 @@
|
||||
<span>LIVE</span><div><b>当前正在展示:{{ currentCastSummary }}</b><small>{{ formatTime(currentCast.castTime) }}</small></div><el-button text type="primary" @click="openScoreScreen">查看大屏</el-button><el-button text type="danger" @click="withdrawCast">撤回投屏</el-button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap"><el-table ref="workbenchTableRef" v-loading="loadingEntries" :data="entries" row-key="sessionEntryId" height="100%" border stripe scrollbar-always-on class="workbench-table score-matrix-table" highlight-current-row @selection-change="onSelection">
|
||||
<el-table-column v-if="activeTab === 'results'" type="selection" width="44" fixed="left" :selectable="canConfirm" :reserve-selection="true" />
|
||||
<div class="table-wrap"><el-table v-loading="loadingEntries" :data="entries" row-key="sessionEntryId" height="100%" border stripe scrollbar-always-on class="workbench-table score-matrix-table" highlight-current-row>
|
||||
<el-table-column label="场次" width="78" fixed="left" align="center"><template #default="{ row }">{{ rowSessionDisplay(row) }}</template></el-table-column>
|
||||
<el-table-column label="场地" width="76" fixed="left" align="center"><template #default="{ row }">{{ row._session?.venueName || '—' }}</template></el-table-column>
|
||||
<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 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 label="裁判提交" width="126" align="center"><template #default="{ row }"><span :class="['submit-progress', { done: allSubmitted(row) }]">{{ submittedCount(row) }}/{{ row.judgeScores?.length || 0 }}</span><small v-if="hasResubmission(row)" class="resubmission-hint">待重新提交</small></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 v-if="showRequiredScore" :label="showFactorScores ? '规定系数 Q' : requiredScoreLabel" min-width="110" align="center"><template #default="{ row }">{{ score(row.result?.requiredScore) }}</template></el-table-column>
|
||||
@ -68,10 +66,15 @@
|
||||
<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="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>
|
||||
<el-table-column label="操作" :width="activeTab === 'screen' ? 190 : 250" fixed="right" align="center"><template #default="{ row }">
|
||||
<div v-if="activeTab === 'screen'" class="row-actions"><el-tooltip :content="castDisabledReason(row)" :disabled="canCastForKind(row)"><span><el-button size="small" type="primary" :loading="actionLoading && rowActionId === row.sessionEntryId" :disabled="!canCastForKind(row)" @click="castScore(row)">投出{{ castEntityLabel }}成绩</el-button></span></el-tooltip><el-button size="small" @click="showDetail(row)">评分详情</el-button></div>
|
||||
<div v-else class="row-actions"><el-button v-if="canConfirm(row)" size="small" type="success" :loading="actionLoading && rowActionId === row.sessionEntryId" @click="confirmOne(row)">确认成绩</el-button><el-button v-else-if="canCast(row)" size="small" type="primary" :loading="actionLoading && rowActionId === row.sessionEntryId" @click="castScore(row)">投出成绩</el-button><el-button v-else size="small" disabled>等待成绩</el-button><el-button size="small" @click="showDetail(row)">详情</el-button><el-dropdown trigger="click" @command="command => handleRowCommand(command, row)"><el-button size="small" class="more-button"><span class="more-button-content">更多<el-icon><ArrowDown /></el-icon></span></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="correction" :disabled="!hasSubmittedJudgeScores(row)">发起成绩修正</el-dropdown-item><el-dropdown-item command="rematch" divided>安排重赛</el-dropdown-item><el-dropdown-item command="export">导出本场评分单</el-dropdown-item><el-dropdown-item command="void" divided class="danger-menu-item">作废成绩</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div>
|
||||
<div v-if="activeTab === 'screen'" class="row-actions"><el-tooltip :content="castDisabledReason(row)" :disabled="canCastForKind(row)"><span><el-button size="small" type="primary" :loading="actionLoading && rowActionId === row.sessionEntryId" :disabled="!canCastForKind(row)" @click="castScore(row)">投出{{ castEntityLabel }}成绩</el-button></span></el-tooltip><el-button size="small" :loading="detailLoadingId===row.sessionEntryId" @click="showDetail(row)">评分详情</el-button></div>
|
||||
<div v-else class="row-actions"><el-button v-if="canConfirm(row)" size="small" type="success" :loading="actionLoading && rowActionId === row.sessionEntryId" @click="confirmOne(row)">确认成绩</el-button><el-button v-else-if="canCast(row)" size="small" type="primary" :loading="actionLoading && rowActionId === row.sessionEntryId" @click="castScore(row)">投出成绩</el-button><el-button v-else size="small" disabled>等待成绩</el-button><el-button size="small" :loading="detailLoadingId===row.sessionEntryId" @click="showDetail(row)">详情</el-button><el-dropdown trigger="click" @command="command => handleRowCommand(command, row)"><el-button size="small" class="more-button"><span class="more-button-content">更多<el-icon><ArrowDown /></el-icon></span></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="correction" :disabled="!hasSubmittedJudgeScores(row)">发起成绩修正</el-dropdown-item><el-dropdown-item command="rematch" divided>安排重赛</el-dropdown-item><el-dropdown-item command="export">导出本场评分单</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div>
|
||||
</template></el-table-column>
|
||||
</el-table></div>
|
||||
<div class="workbench-pagination">
|
||||
<span class="page-total">共 {{ entryTotal }} 条 · 每页</span>
|
||||
<el-select v-model="pageSize" class="page-size" @change="changePageSize"><el-option v-for="size in [20,50,100]" :key="size" :value="size" :label="`${size} 条`" /></el-select>
|
||||
<div class="page-controls"><el-button size="small" :disabled="page===1" @click="changePage(page-1)">上一页</el-button><button v-for="range in visiblePageRanges" :key="range.page" type="button" class="page-range-button" :class="{active:range.page===page}" @click="changePage(range.page)"><b>第 {{ range.page }} 页</b><small>{{ formatPageSession(range.firstSessionOrder) }} → {{ formatPageSession(range.lastSessionOrder) }}</small></button><el-button size="small" :disabled="page>=pageCount" @click="changePage(page+1)">下一页</el-button></div><div v-if="pageCount>5" class="page-jump"><span>跳至</span><el-input-number v-model="jumpPage" :min="1" :max="pageCount" :controls="false" size="small" aria-label="跳转页码" @keyup.enter="jumpToPage"/><span>页</span><el-button size="small" @click="jumpToPage">跳转</el-button></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@ -87,7 +90,7 @@
|
||||
<el-table :data="detailRow ? [detailRow] : []" border class="score-detail-matrix">
|
||||
<el-table-column v-for="group in detailJudgeGroups" :key="group.key" :label="group.label" :class-name="`score-group-${group.key}`" :label-class-name="`score-group-${group.key}`" align="center" header-align="center">
|
||||
<el-table-column v-for="judge in group.judges" :key="judge.scoreSheetId || `${judge.judgeRoleType}:${judge.judgeRole}`" :label="judgeColumnLabel(judge)" :class-name="`score-group-${group.key}`" :label-class-name="`score-group-${group.key}`" min-width="116" align="center">
|
||||
<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>
|
||||
<template #default><button class="judge-score-link" :disabled="judge.sheetStatus < 10" @click="showJudgeDetail(judge)"><strong>{{ judge.requiresResubmission ? '待重新提交' : judge.sheetStatus >= 10 ? judgeScoreText(judge, detailRow?._session?.scoreRuleCode) : '待提交' }}</strong><small>{{ judge.requiresResubmission ? `原分 ${judgeScoreText(judge, detailRow?._session?.scoreRuleCode)} · ` : '' }}{{ judge.judgeName || '未绑定裁判' }}</small></button></template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="group.summaryLabel" :class-name="`score-group-${group.key} score-group-summary`" :label-class-name="`score-group-${group.key} score-group-summary`" width="110" align="center"><template #default><b class="detail-group-total">{{ detailAggregateText(group.key) }}</b></template></el-table-column>
|
||||
</el-table-column>
|
||||
@ -99,7 +102,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="judgeDetailVisible" :title="`${selectedJudgeSheet?.judgeRoleName || '裁判'}评分项详情`" width="760px" append-to-body destroy-on-close>
|
||||
<div v-if="selectedJudgeSheet" class="judge-detail-head"><div><small>裁判席位</small><b>{{ judgeColumnLabel(selectedJudgeSheet) }}</b></div><div><small>裁判姓名</small><b>{{ selectedJudgeSheet.judgeName || '—' }}</b></div><div class="judge-status-card"><small>评分单状态</small><b>{{ selectedJudgeSheet.sheetStatusName || '—' }}</b><el-button size="small" type="primary" plain @click="viewJudgeScoreBoard"><el-icon><Picture /></el-icon>查看提交的计分板</el-button></div></div>
|
||||
<div v-if="selectedJudgeSheet" class="judge-detail-head"><div><small>裁判席位</small><b>{{ judgeColumnLabel(selectedJudgeSheet) }}</b></div><div><small>裁判姓名</small><b>{{ selectedJudgeSheet.judgeName || '—' }}</b></div><div class="judge-status-card"><small>评分单状态</small><b>{{ selectedJudgeSheet.requiresResubmission ? '待重新提交(以下为原评分)' : selectedJudgeSheet.sheetStatusName || '—' }}</b><el-button size="small" type="primary" plain @click="viewJudgeScoreBoard"><el-icon><Picture /></el-icon>查看提交的计分板</el-button></div></div>
|
||||
<div v-if="isDifficultyJudge" class="judge-item-sections">
|
||||
<section v-for="section in difficultyItemSections" :key="section.key" class="judge-item-section">
|
||||
<div class="judge-item-section-title" :class="`section-${section.key}`"><b>{{ section.title }}</b><span>{{ section.items.length }} 项</span></div>
|
||||
@ -125,7 +128,7 @@
|
||||
<template #footer><el-button @click="resetScoreTheme">恢复默认</el-button><el-button type="primary" @click="themeSettingsVisible=false">完成</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="reasonVisible" :title="reasonAction === 'rematch' ? '安排重赛' : '作废成绩'" width="520px"><el-form label-position="top" class="reason-form"><el-form-item :label="reasonAction === 'rematch' ? '重赛原因' : '作废原因'" required><el-input v-model="reasonText" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="请填写 2-200 个字符"/></el-form-item></el-form><template #footer><el-button @click="reasonVisible=false">取消</el-button><el-button type="danger" :loading="actionLoading" @click="submitReason">{{ reasonAction === 'rematch' ? '确认重赛' : '确认作废' }}</el-button></template></el-dialog>
|
||||
<el-dialog v-model="reasonVisible" title="安排重赛" width="520px"><el-form label-position="top" class="reason-form"><el-form-item label="重赛原因" required><el-input v-model="reasonText" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="请填写 2-200 个字符"/></el-form-item></el-form><template #footer><el-button @click="reasonVisible=false">取消</el-button><el-button type="danger" :loading="actionLoading" @click="submitReason">确认重赛</el-button></template></el-dialog>
|
||||
|
||||
<el-dialog v-model="correctionVisible" title="发起成绩修正" width="560px"><el-form label-position="top"><el-form-item label="裁判类型" required><el-select v-model="correctionForm.judgeRoleType" placeholder="请选择裁判类型" style="width:100%" @change="onCorrectionRoleChange"><el-option v-for="group in correctionGroups" :key="group.judgeRoleType" :label="group.judgeRoleName" :value="group.judgeRoleType"/></el-select></el-form-item><el-form-item label="评分单" required><el-select v-model="correctionForm.scoreSheetId" :disabled="!correctionForm.judgeRoleType" placeholder="请选择已提交评分单" style="width:100%"><el-option v-for="x in correctionSheetOptions" :key="x.scoreSheetId" :disabled="!isCorrectionSheetAvailable(x)" :label="`${x.judgeRole}号位 · ${x.judgeName} · ${score(x.totalScore)}分 · ${x.sheetStatusName}${x.hasPendingCorrection?'(处理中)':''}`" :value="x.scoreSheetId"/></el-select></el-form-item><el-form-item label="修正原因" required><el-input v-model="correctionForm.reason" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请填写 2-200 个字符"/></el-form-item></el-form><template #footer><el-button @click="correctionVisible=false">取消</el-button><el-button type="primary" :loading="actionLoading" @click="submitCorrection">发送修正任务</el-button></template></el-dialog>
|
||||
</div>
|
||||
@ -133,17 +136,17 @@
|
||||
|
||||
<script setup>
|
||||
import { deductionScoreText, isFactorRule, isWireRule, judgeScoreText, calculationSummary } from '../score-format.mjs';
|
||||
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { ArrowDown, Picture } from '@element-plus/icons-vue';
|
||||
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import { defaultScoreScreenTheme, normalizeScoreScreenTheme, readScoreScreenTheme, saveScoreScreenTheme, scoreScreenModeTextColors, scoreScreenThemeChannel, scoreScreenThemeKey } from '../score-screen-theme.mjs';
|
||||
import {
|
||||
workbenchSessionsGet, workbenchSessionEntriesGet, workbenchScreenCastPost,
|
||||
workbenchSessionsGet, workbenchProjectEntriesGet, workbenchEntryDetailGet, workbenchScreenCastPost,
|
||||
workbenchScreenCastCurrentGet, workbenchScreenCastWithdrawPost, workbenchCorrectionsGet,
|
||||
workbenchCorrectionDetailGet, workbenchCorrectionApprovePost, workbenchResultConfirmPost,
|
||||
workbenchResultsBatchConfirmPost, workbenchEntryRematchPost, workbenchResultVoidPost,
|
||||
workbenchEntryRematchPost,
|
||||
workbenchCorrectionInitiatePost, workbenchResultsExportGet, workbenchScoreSheetsExportGet,
|
||||
} from '/@/api/match/workbench';
|
||||
|
||||
@ -154,15 +157,20 @@ const eventId=ref(Number(route.query.eventId)||Number(savedContext.eventId)||0);
|
||||
const eventName=ref(String(route.query.eventName||savedContext.eventName||'未选择赛事'));
|
||||
if(eventId.value)sessionStorage.setItem('scoring-context',JSON.stringify({...savedContext,eventId:eventId.value,eventName:eventName.value,mode:'freestyle'}));
|
||||
const activeTab=ref('screen'), sessions=ref([]), selectedProjectId=ref(''), selectedSession=ref(null), entries=ref([]), currentCast=ref(null);
|
||||
const loadingSessions=ref(false), loadingEntries=ref(false), entriesSyncError=ref(false), castSyncError=ref(false), retryLoading=ref(false), entryKeyword=ref(''), resultStatus=ref(null), selectedResultIds=ref([]), selectedEntryIds=ref([]);
|
||||
const loadingSessions=ref(false), loadingEntries=ref(false), entriesSyncError=ref(false), castSyncError=ref(false), retryLoading=ref(false), entryKeyword=ref(''), resultStatus=ref(null);
|
||||
const page=ref(1),jumpPage=ref(1),pageSize=ref(20),entryTotal=ref(0),pageRanges=ref([]);
|
||||
const pageCount=computed(()=>Math.max(1,Math.ceil(entryTotal.value/pageSize.value)));
|
||||
const visiblePageRanges=computed(()=>{
|
||||
const start=Math.max(1,Math.min(page.value-2,pageCount.value-4));
|
||||
return pageRanges.value.slice(start-1,start+4);
|
||||
});
|
||||
const offline=computed(()=>entriesSyncError.value||castSyncError.value);
|
||||
const syncStatusText=computed(()=>entriesSyncError.value&&castSyncError.value?'成绩与投屏状态同步异常':entriesSyncError.value?'成绩同步异常':castSyncError.value?'投屏状态同步异常':'实时同步');
|
||||
const workbenchTableRef=ref(null), batchConfirmDialogOpen=ref(false);
|
||||
const detailVisible=ref(false), detailRow=ref(null), judgeDetailVisible=ref(false), selectedJudgeSheet=ref(null), annotationVisible=ref(false), annotationUrl=ref(''), rankingVisible=ref(false), themeSettingsVisible=ref(false), reasonVisible=ref(false), reasonAction=ref(''), reasonText=ref(''), reasonRow=ref(null), actionLoading=ref(false), rowActionId=ref(null);
|
||||
const detailVisible=ref(false), detailRow=ref(null), detailLoadingId=ref(null), judgeDetailVisible=ref(false), selectedJudgeSheet=ref(null), annotationVisible=ref(false), annotationUrl=ref(''), rankingVisible=ref(false), themeSettingsVisible=ref(false), reasonVisible=ref(false), reasonText=ref(''), reasonRow=ref(null), actionLoading=ref(false), rowActionId=ref(null);
|
||||
const correctionVisible=ref(false), correctionRow=ref(null), correctionForm=ref({judgeRoleType:null,scoreSheetId:null,reason:''});
|
||||
const corrections=ref([]), loadingCorrections=ref(false), correctionKeyword=ref(''), correctionStatus=ref(null);
|
||||
let pollTimer, entriesController=null, castController=null;
|
||||
let entriesRequestId=0, castRequestId=0, restoringSelection=false;
|
||||
let entriesRequestId=0, castRequestId=0;
|
||||
let mountedReady=false, componentActive=false, destroyed=false;
|
||||
let entriesFailureCount=0, castFailureCount=0, entriesNextRetryAt=0, castNextRetryAt=0;
|
||||
const retryDelays=[10000,20000,40000,60000];
|
||||
@ -349,38 +357,21 @@ async function loadSessions(){
|
||||
const fallbackSessionId=sessionGroups.value.find(project=>project.key===previousProjectId)?.sessions[0]?.sessionId;
|
||||
const currentSession=sessions.value.find(session=>session.sessionId===previousSessionId)||sessions.value.find(session=>session.sessionId===fallbackSessionId)||sessions.value[0]||null;
|
||||
const currentProject=sessionGroups.value.find(project=>project.sessions.some(session=>session.sessionId===currentSession?.sessionId));
|
||||
if(selectedProjectId.value!==currentProject?.key){clearResultSelection();entries.value=[];entriesSyncError.value=false;entriesFailureCount=0;entriesNextRetryAt=0}
|
||||
if(selectedProjectId.value!==currentProject?.key){page.value=1;jumpPage.value=1;entryTotal.value=0;pageRanges.value=[];entries.value=[];entriesSyncError.value=false;entriesFailureCount=0;entriesNextRetryAt=0}
|
||||
selectedProjectId.value=currentProject?.key||'';
|
||||
selectedSession.value=currentSession;
|
||||
if(currentSession)await loadEntries();
|
||||
else{cancelEntriesRequest();entries.value=[];clearResultSelection();entriesSyncError.value=false}
|
||||
else{cancelEntriesRequest();entries.value=[];entryTotal.value=0;pageRanges.value=[];entriesSyncError.value=false}
|
||||
return true;
|
||||
}catch(error){
|
||||
entriesSyncError.value=true;entriesFailureCount++;entriesNextRetryAt=Date.now()+retryDelay(entriesFailureCount);
|
||||
return false;
|
||||
}finally{loadingSessions.value=false}
|
||||
}
|
||||
function syncSelection(rows){selectedEntryIds.value=rows.map(x=>x.sessionEntryId).filter(Boolean);selectedResultIds.value=rows.map(x=>x.result?.resultId).filter(Boolean)}
|
||||
function clearResultSelection(){selectedEntryIds.value=[];selectedResultIds.value=[];workbenchTableRef.value?.clearSelection()}
|
||||
async function restoreResultSelection(previousEntryIds,isCurrent=()=>true){
|
||||
await nextTick();
|
||||
if(!isCurrent())return;
|
||||
const table=workbenchTableRef.value;
|
||||
if(!table)return;
|
||||
const selectedSet=new Set(previousEntryIds);
|
||||
const validRows=entries.value.filter(row=>selectedSet.has(row.sessionEntryId)&&canConfirm(row));
|
||||
restoringSelection=true;
|
||||
table.clearSelection();
|
||||
validRows.forEach(row=>table.toggleRowSelection(row,true));
|
||||
restoringSelection=false;
|
||||
syncSelection(validRows);
|
||||
const removedCount=selectedSet.size-validRows.length;
|
||||
if(removedCount>0)ElMessage.info(`${removedCount} 条已选成绩状态发生变化,已从批量确认中移除`);
|
||||
}
|
||||
async function selectSession(session){
|
||||
const project=sessionGroups.value.find(item=>item.sessions.some(candidate=>candidate.sessionId===session.sessionId));
|
||||
if(!project)return;
|
||||
if(selectedProjectId.value!==project.key){clearResultSelection();entries.value=[];entriesSyncError.value=false;entriesFailureCount=0;entriesNextRetryAt=0}
|
||||
if(selectedProjectId.value!==project.key){page.value=1;jumpPage.value=1;entryTotal.value=0;pageRanges.value=[];entries.value=[];entriesSyncError.value=false;entriesFailureCount=0;entriesNextRetryAt=0}
|
||||
selectedProjectId.value=project.key;selectedSession.value=session;await loadEntries();
|
||||
}
|
||||
function selectProject(project){
|
||||
@ -389,50 +380,37 @@ function selectProject(project){
|
||||
if(firstSession)void selectSession(firstSession);
|
||||
}
|
||||
function selectSessionById(sessionId){const session=sessions.value.find(item=>item.sessionId===sessionId);if(session)selectSession(session)}
|
||||
function compareIntegerText(left,right){
|
||||
const a=String(left??'').replace(/^0+/,'')||'0',b=String(right??'').replace(/^0+/,'')||'0';
|
||||
return a.length-b.length||a.localeCompare(b);
|
||||
}
|
||||
function compareEntrySchedule(a,b){
|
||||
const aOrder=a.scheduleOrder,bOrder=b.scheduleOrder;
|
||||
if(aOrder&&bOrder){const result=compareIntegerText(aOrder,bOrder);if(result)return result}
|
||||
return a._sessionOrder-b._sessionOrder||(Number(a.playOrder)||a._rowOrder)-(Number(b.playOrder)||b._rowOrder);
|
||||
}
|
||||
function changePage(target){if(target<1||target>pageCount.value)return;jumpPage.value=target;if(target===page.value)return;page.value=target;void loadEntries()}
|
||||
function jumpToPage(){const target=Number(jumpPage.value);if(!Number.isInteger(target)||target<1||target>pageCount.value){ElMessage.warning(`请输入 1 至 ${pageCount.value} 的页码`);return}changePage(target)}
|
||||
function changePageSize(){page.value=1;jumpPage.value=1;void loadEntries()}
|
||||
function retryDelay(failureCount){return retryDelays[Math.min(failureCount-1,retryDelays.length-1)]}
|
||||
function cancelEntriesRequest(){++entriesRequestId;entriesController?.abort();entriesController=null;loadingEntries.value=false}
|
||||
function cancelCastRequest(){++castRequestId;castController?.abort();castController=null}
|
||||
function validResponse(res){if(res?.statusCode!==200)throw new Error('同步接口返回失败');return res.data}
|
||||
async function loadEntries(silent=false,{background=false}={}){
|
||||
const projectSessions=[...selectedProjectSessions.value];
|
||||
if(!projectSessions.length)return false;
|
||||
const projectSessions=[...selectedProjectSessions.value],eventProjectId=selectedSession.value?.eventProjectId;
|
||||
if(!projectSessions.length||!eventProjectId)return false;
|
||||
// 定时轮询只跳过忙碌中的轮次;主动查询、切换项目和操作后的刷新则废弃旧请求。
|
||||
if(background&&entriesController)return false;
|
||||
if(!background)cancelEntriesRequest();
|
||||
const requestId=++entriesRequestId;
|
||||
const controller=new AbortController();
|
||||
entriesController=controller;
|
||||
const keyword=entryKeyword.value||undefined,status=resultStatus.value??undefined;
|
||||
const params={eventId:eventId.value,page:page.value,pageSize:pageSize.value,keyword:entryKeyword.value||undefined,resultStatus:resultStatus.value??undefined};
|
||||
if(!silent)loadingEntries.value=true;
|
||||
try{
|
||||
const responses=new Array(projectSessions.length);
|
||||
let nextSession=0;
|
||||
// 同项目可有许多场次,但任一时刻最多发出两个成绩请求。
|
||||
await Promise.all(Array.from({length:Math.min(2,projectSessions.length)},async()=>{
|
||||
while(nextSession<projectSessions.length&&!controller.signal.aborted){
|
||||
const sessionOrder=nextSession++;
|
||||
const session=projectSessions[sessionOrder];
|
||||
const res=await workbenchSessionEntriesGet(session.sessionId,{keyword,resultStatus:status},{signal:controller.signal,silentError:background,timeout:background?15000:50000});
|
||||
const rows=validResponse(res);
|
||||
if(!Array.isArray(rows))throw new Error('成绩列表数据格式错误');
|
||||
responses[sessionOrder]=rows.map((row,rowOrder)=>({...row,_session:session,_sessionOrder:sessionOrder,_rowOrder:rowOrder}));
|
||||
}
|
||||
}));
|
||||
const res=await workbenchProjectEntriesGet(eventProjectId,params,{signal:controller.signal,silentError:background,timeout:background?15000:50000});
|
||||
const body=validResponse(res);
|
||||
if(!Array.isArray(body?.items)||!Array.isArray(body?.pageRanges))throw new Error('成绩分页数据格式错误');
|
||||
if(requestId!==entriesRequestId||controller.signal.aborted)return false;
|
||||
// 所有场次成功后一次性提交,避免半个项目的新数据覆盖完整旧列表。
|
||||
const previousEntryIds=[...selectedEntryIds.value];
|
||||
entries.value=responses.flat().sort(compareEntrySchedule);
|
||||
const sessionById=new Map(projectSessions.map(session=>[String(session.sessionId),session]));
|
||||
entries.value=body.items.map(row=>({...row,_session:sessionById.get(String(row.sessionId))||null}));
|
||||
entryTotal.value=Number(body.total)||0;
|
||||
const returnedPage=Number(body.page)||1;
|
||||
if(returnedPage!==page.value)jumpPage.value=returnedPage;
|
||||
page.value=returnedPage;
|
||||
pageRanges.value=body.pageRanges;
|
||||
entriesSyncError.value=false;entriesFailureCount=0;entriesNextRetryAt=0;
|
||||
await restoreResultSelection(previousEntryIds,()=>requestId===entriesRequestId&&!controller.signal.aborted);
|
||||
return true;
|
||||
}catch(error){
|
||||
controller.abort();
|
||||
@ -464,9 +442,12 @@ async function retrySync(){
|
||||
finally{retryLoading.value=false}
|
||||
}
|
||||
async function loadCorrections(){if(!eventId.value)return;loadingCorrections.value=true;try{const res=await workbenchCorrectionsGet({eventId:eventId.value,keyword:correctionKeyword.value||undefined,correctionStatus:correctionStatus.value??undefined});corrections.value=res?.data||[]}finally{loadingCorrections.value=false}}
|
||||
function resetFilters(){entryKeyword.value='';resultStatus.value=null;loadEntries()}
|
||||
function applyFilters(){page.value=1;jumpPage.value=1;void loadEntries()}
|
||||
function resetFilters(){entryKeyword.value='';resultStatus.value=null;applyFilters()}
|
||||
function progress(x){return x.totalEntries?Math.round(x.completedEntries*100/x.totalEntries):0}
|
||||
function submittedCount(row){return (row.judgeScores||[]).filter(x=>x.sheetStatus>=10&&x.sheetStatus<90).length}
|
||||
function isJudgeSubmitted(sheet){return sheet.sheetStatus>=10&&sheet.sheetStatus<90&&!sheet.requiresResubmission}
|
||||
function submittedCount(row){return (row.judgeScores||[]).filter(isJudgeSubmitted).length}
|
||||
function hasResubmission(row){return (row.judgeScores||[]).some(x=>x.requiresResubmission)}
|
||||
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||'裁判'}
|
||||
@ -486,7 +467,7 @@ function scoreItemInput(item){const value=item?.countValue??item?.inputValue;ret
|
||||
function canCast(row){return !!row.result&&allSubmitted(row)&&row.result.resultStatus>=20&&row.result.resultStatus<90&&!row.result.isDisqualified}
|
||||
function canCastForKind(row){return canCast(row)&&(castEntityKind.value!=='team'||Boolean(row.teamName))}
|
||||
function castDisabledReason(row){if(!row.result)return '尚未生成成绩';if(!allSubmitted(row))return '裁判成绩尚未全部提交';if(row.result.isDisqualified)return 'DQ 成绩不可投屏';if(row.result.resultStatus<20)return '成绩尚未确认';if(castEntityKind.value==='team'&&!row.teamName)return '该记录没有团队名称,请切换为个人展示';return '该成绩不可投屏'}
|
||||
function canConfirm(row){return row.result?.resultStatus===10}
|
||||
function canConfirm(row){return row.result?.resultStatus===10&&!(row.judgeScores||[]).some(x=>x.requiresResubmission)}
|
||||
function score(value){if(value===null||value===undefined||value==='')return '—';const n=Number(value);if(!Number.isFinite(n))return value;return String(n)}
|
||||
function resultTag(status){return ({10:'warning',20:'success',30:'primary',40:'success',90:'danger'})[status]||'info'}
|
||||
function formatTime(value){return value?String(value).replace('T',' ').slice(0,19):'—'}
|
||||
@ -542,43 +523,43 @@ function rowSessionDisplay(row){
|
||||
if(Number.isInteger(sessionOrder)&&sessionOrder>0)return `第${sessionOrder}场`;
|
||||
const session=row?._session;if(!session)return '—';return formatSessionName(session,session._sourceIndex).replace(/\s+/g,'');
|
||||
}
|
||||
function onSelection(rows){if(!restoringSelection)syncSelection(rows)}
|
||||
function showDetail(row){selectedJudgeSheet.value=null;judgeDetailVisible.value=false;detailRow.value=row;detailVisible.value=true}
|
||||
function handleRowCommand(command,row){if(command==='correction')openCorrection(row);else if(command==='rematch')openReason('rematch',row);else if(command==='void')openReason('void',row);else if(command==='export')exportSheets(row)}
|
||||
function formatPageSession(order){return Number.isInteger(Number(order))&&Number(order)>0&&Number(order)<2147483647?`第 ${order} 场`:'场次待定'}
|
||||
async function fetchEntryDetail(row){
|
||||
const res=await workbenchEntryDetailGet(row.sessionEntryId);
|
||||
const detail=validResponse(res);
|
||||
if(!detail||detail.sessionEntryId!==row.sessionEntryId)throw new Error('成绩详情已变化,请刷新后重试');
|
||||
return {...detail,_session:row._session};
|
||||
}
|
||||
async function showDetail(row){
|
||||
if(detailLoadingId.value)return;
|
||||
detailLoadingId.value=row.sessionEntryId;
|
||||
try{detailRow.value=await fetchEntryDetail(row);selectedJudgeSheet.value=null;judgeDetailVisible.value=false;detailVisible.value=true}
|
||||
catch(error){ElMessage.error(error?.message||'成绩详情加载失败')}
|
||||
finally{detailLoadingId.value=null}
|
||||
}
|
||||
function handleRowCommand(command,row){if(command==='correction')openCorrection(row);else if(command==='rematch')openReason(row);else if(command==='export')exportSheets(row)}
|
||||
|
||||
async function castScore(row){if(actionLoading.value)return;const session=row._session||selectedSession.value;if(!session)return ElMessage.warning('未找到成绩所属场次');const venueId=session.venueId;rememberCastKind();const presentation=prepareScoreScreen(venueId);rowActionId.value=row.sessionEntryId;actionLoading.value=true;try{const response=ensureCastSucceeded(await workbenchScreenCastPost({eventId:eventId.value,castType:1,venueId,sessionId:session.sessionId,sessionEntryId:row.sessionEntryId,eventResultId:row.result.resultId,resultVersion:row.result.resultVersion}));publishScreenCast(buildSingleCastSnapshot(response,row,session),venueId);showScoreScreen(presentation,venueId,castEntityKind.value);ElMessage.success(`${castEntityLabel.value}成绩已投出`);await Promise.all([loadEntries(true),loadCurrentCast()])}catch(error){closeUnusedScreen(presentation);throw error}finally{actionLoading.value=false;rowActionId.value=null}}
|
||||
async function castScore(row){if(actionLoading.value)return;const session=row._session||selectedSession.value;if(!session)return ElMessage.warning('未找到成绩所属场次');const venueId=session.venueId;rememberCastKind();const presentation=prepareScoreScreen(venueId);rowActionId.value=row.sessionEntryId;actionLoading.value=true;try{const response=ensureCastSucceeded(await workbenchScreenCastPost({eventId:eventId.value,castType:1,venueId,sessionId:session.sessionId,sessionEntryId:row.sessionEntryId,eventResultId:row.result.resultId,resultVersion:row.result.resultVersion}));publishScreenCast(buildSingleCastSnapshot(response,row,session),venueId);showScoreScreen(presentation,venueId,castEntityKind.value);if(row.result){row.result.resultStatus=30;row.result.resultStatusName='已投屏'}ElMessage.success(`${castEntityLabel.value}成绩已投出`);await loadCurrentCast()}catch(error){closeUnusedScreen(presentation);throw error}finally{actionLoading.value=false;rowActionId.value=null}}
|
||||
async function castDetailScore(){const row=detailRow.value;if(!row)return;await castScore(row);detailVisible.value=false}
|
||||
function openRanking(){rankingKey.value=selectedSession.value?.eventProjectGroupId?`${selectedSession.value.eventProjectId}:${selectedSession.value.eventProjectGroupId}`:'';rankingVisible.value=true}
|
||||
async function castRanking(){const target=rankingSelection.value;if(!target)return ElMessage.warning('请选择项目和组别');if(actionLoading.value)return;const venueId=target.venueId??selectedSession.value?.venueId;rememberCastKind();const presentation=prepareScoreScreen(venueId);actionLoading.value=true;try{const response=ensureCastSucceeded(await workbenchScreenCastPost({eventId:eventId.value,castType:2,venueId,eventProjectId:target.eventProjectId,eventProjectGroupId:target.eventProjectGroupId}));publishScreenCast(buildRankingCastSnapshot(response,target),venueId);showScoreScreen(presentation,venueId,castEntityKind.value);rankingVisible.value=false;ElMessage.success(`${castEntityLabel.value}排名已投出`);await Promise.all([loadEntries(true),loadCurrentCast()])}catch(error){closeUnusedScreen(presentation);throw error}finally{actionLoading.value=false}}
|
||||
async function withdrawCast(){if(actionLoading.value)return;await ElMessageBox.confirm('撤回后大屏将回到等待状态,是否继续?','撤回投屏',{type:'warning'});actionLoading.value=true;try{await workbenchScreenCastWithdrawPost(currentCast.value.screenCastId);clearPublishedScreenCast(selectedSession.value?.venueId);currentCast.value=null;ElMessage.success('已撤回');await Promise.all([loadEntries(true),loadCurrentCast()])}finally{actionLoading.value=false}}
|
||||
async function confirmOne(row){if(actionLoading.value)return;rowActionId.value=row.sessionEntryId;actionLoading.value=true;try{await workbenchResultConfirmPost(row.result.resultId);ElMessage.success('成绩已确认');await loadEntries()}finally{actionLoading.value=false;rowActionId.value=null}}
|
||||
async function batchConfirm(){
|
||||
if(actionLoading.value||batchConfirmDialogOpen.value)return;
|
||||
const entryIdsSnapshot=[...selectedEntryIds.value];
|
||||
if(!entryIdsSnapshot.length)return;
|
||||
batchConfirmDialogOpen.value=true;
|
||||
try{
|
||||
try{await ElMessageBox.confirm(`确认选中的 ${entryIdsSnapshot.length} 条成绩?`,'批量确认')}catch(reason){if(reason==='cancel'||reason==='close')return;throw reason}
|
||||
const selectedSet=new Set(entryIdsSnapshot);
|
||||
const validRows=entries.value.filter(row=>selectedSet.has(row.sessionEntryId)&&canConfirm(row));
|
||||
if(validRows.length!==entryIdsSnapshot.length){await restoreResultSelection(entryIdsSnapshot);ElMessage.warning('部分成绩状态已变化,请检查当前选择后重新确认');return}
|
||||
const resultIdsSnapshot=validRows.map(row=>row.result?.resultId).filter(Boolean);
|
||||
if(resultIdsSnapshot.length!==validRows.length){ElMessage.warning('部分成绩缺少有效结果,请刷新后重试');return}
|
||||
const requestsBySession=new Map();
|
||||
for(const row of validRows){const sessionId=row._session?.sessionId;if(!sessionId){ElMessage.warning('部分成绩缺少场次信息,请刷新后重试');return}if(!requestsBySession.has(sessionId))requestsBySession.set(sessionId,[]);requestsBySession.get(sessionId).push(row.result.resultId)}
|
||||
actionLoading.value=true;
|
||||
try{await Promise.all([...requestsBySession].map(([sessionId,resultIds])=>workbenchResultsBatchConfirmPost({sessionId,resultIds})));clearResultSelection();ElMessage.success('批量确认完成');await loadEntries()}finally{actionLoading.value=false}
|
||||
}finally{batchConfirmDialogOpen.value=false}
|
||||
function openReason(row){reasonRow.value=row;reasonText.value='';reasonVisible.value=true}
|
||||
async function submitReason(){const text=reasonText.value.trim();if(text.length<2)return ElMessage.warning('请填写至少 2 个字符的原因');actionLoading.value=true;try{await workbenchEntryRematchPost({originalSessionEntryId:reasonRow.value.sessionEntryId,rematchReason:text});reasonVisible.value=false;ElMessage.success('重赛已安排到场次末尾');await loadEntries()}finally{actionLoading.value=false}}
|
||||
async function openCorrection(row){
|
||||
if(detailLoadingId.value)return;
|
||||
detailLoadingId.value=row.sessionEntryId;
|
||||
try{correctionRow.value=await fetchEntryDetail(row);const firstRoleType=(correctionRow.value.judgeScores||[])[0]?.judgeRoleType??null;correctionForm.value={judgeRoleType:firstRoleType,scoreSheetId:null,reason:''};correctionVisible.value=true}
|
||||
catch(error){ElMessage.error(error?.message||'评分单加载失败')}
|
||||
finally{detailLoadingId.value=null}
|
||||
}
|
||||
function openReason(action,row){reasonAction.value=action;reasonRow.value=row;reasonText.value='';reasonVisible.value=true}
|
||||
async function submitReason(){const text=reasonText.value.trim();if(text.length<2)return ElMessage.warning('请填写至少 2 个字符的原因');actionLoading.value=true;try{if(reasonAction.value==='rematch')await workbenchEntryRematchPost({originalSessionEntryId:reasonRow.value.sessionEntryId,rematchReason:text});else await workbenchResultVoidPost({sessionEntryId:reasonRow.value.sessionEntryId,voidReason:text});reasonVisible.value=false;ElMessage.success(reasonAction.value==='rematch'?'重赛已安排到场次末尾':'成绩已作废');await loadEntries()}finally{actionLoading.value=false}}
|
||||
function openCorrection(row){correctionRow.value=row;const firstRoleType=(row.judgeScores||[])[0]?.judgeRoleType??null;correctionForm.value={judgeRoleType:firstRoleType,scoreSheetId:null,reason:''};correctionVisible.value=true}
|
||||
async function submitCorrection(){const reason=correctionForm.value.reason.trim();if(!correctionForm.value.scoreSheetId)return ElMessage.warning('请选择裁判评分单');if(reason.length<2)return ElMessage.warning('请填写至少 2 个字符的修正原因');actionLoading.value=true;try{await workbenchCorrectionInitiatePost({sessionEntryId:correctionRow.value.sessionEntryId,scoreSheetIds:[correctionForm.value.scoreSheetId],reason});correctionVisible.value=false;ElMessage.success('修正任务已发送给裁判');await loadEntries()}finally{actionLoading.value=false}}
|
||||
async function approveCorrection(row,isApproved){let remark='';if(!isApproved)remark=await ElMessageBox.prompt('请输入驳回原因','驳回修正申请',{inputValidator:v=>String(v||'').trim().length>=2||'至少输入2个字符'}).then(x=>x.value);else await ElMessageBox.confirm('批准后将向裁判下发修正任务,是否继续?','批准申请');await workbenchCorrectionApprovePost({correctionId:row.correctionId,isApproved,approveRemark:remark});ElMessage.success('申请已处理');loadCorrections()}
|
||||
async function showCorrectionDetail(row){const res=await workbenchCorrectionDetailGet(row.correctionId);const x=res?.data;if(!x)return;ElMessageBox.alert(`选手:${x.entry?.participantName||'-'}\n裁判:${x.originalScore?.judgeName||'-'}(${x.originalScore?.judgeRoleName||'-'})\n原分:${score(x.originalScore?.totalScore)}\n原因:${x.applyReason}`,'修正详情',{confirmButtonText:'关闭'})}
|
||||
|
||||
async function download(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 exportResults(){const blob=await workbenchResultsExportGet({eventId:eventId.value,eventProjectId:selectedSession.value?.eventProjectId,keyword:entryKeyword.value||undefined,resultStatus:resultStatus.value??undefined});download(blob,`成绩册_${selectedProject.value?.projectName||eventId.value}.xlsx`)}
|
||||
async function exportResults(){if(!selectedProject.value)return;const blob=await workbenchResultsExportGet({eventId:eventId.value,eventProjectId:selectedSession.value?.eventProjectId,includeUnscored:true,includeDq:true});download(blob,`成绩册全名单_${selectedProject.value?.projectName||eventId.value}.xlsx`)}
|
||||
async function exportSheets(row){const session=row?._session||selectedSession.value;if(!session)return;const blob=await workbenchScoreSheetsExportGet({eventId:eventId.value,sessionId:session.sessionId});download(blob,`评分单_${session.projectName || ''}_${formatSessionName(session,session._sourceIndex)}.xlsx`)}
|
||||
const presentationFrameId='yd-event-presentation-frame';
|
||||
function scoreScreenHref(venueId,displayKind=castEntityKind.value,castMode='live',eventProjectId){return router.resolve({path:'/scoring/screen',query:{eventId:eventId.value,venueId,eventName:eventName.value,mode:'freestyle',castMode,displayKind,eventProjectId,autofullscreen:'1',presentation:'1'}}).href}
|
||||
@ -686,7 +667,7 @@ function pollNow(force=false){
|
||||
if(!componentActive||document.visibilityState!=='visible')return;
|
||||
const now=Date.now();
|
||||
if((force||now>=castNextRetryAt)&&!castController)void loadCurrentCast({background:true});
|
||||
if(activeTab.value!=='corrections'&&!batchConfirmDialogOpen.value&&selectedProjectSessions.value.length&&(force||now>=entriesNextRetryAt)&&!entriesController)void loadEntries(true,{background:true});
|
||||
if(activeTab.value!=='corrections'&&selectedProjectSessions.value.length&&(force||now>=entriesNextRetryAt)&&!entriesController)void loadEntries(true,{background:true});
|
||||
}
|
||||
function stopPolling(){
|
||||
if(pollTimer){window.clearInterval(pollTimer);pollTimer=null}
|
||||
@ -702,7 +683,6 @@ function onVisibilityChange(){
|
||||
else stopPolling();
|
||||
}
|
||||
watch(()=>selectedSession.value?.venueId,()=>{if(componentActive&&document.visibilityState==='visible')void loadCurrentCast()});
|
||||
watch(batchConfirmDialogOpen,opened=>{if(opened)cancelEntriesRequest()});
|
||||
watch(castEntityKind,rememberCastKind);
|
||||
watch(()=>route.path,(path)=>{activeTab.value=path==='/scoring/cast'?'screen':'results'});
|
||||
onMounted(async ()=>{
|
||||
@ -1093,4 +1073,18 @@ onBeforeUnmount(()=>{destroyed=true;componentActive=false;stopPolling();themeCha
|
||||
.annotation-error { display: grid; place-items: center; width: 100%; height: 420px; color: #9aa8b8; background: #f7f9fc; }
|
||||
.annotation-dialog :deep(.el-dialog__footer) { display: flex; align-items: center; justify-content: space-between; }
|
||||
.annotation-tip { color: #8b9aac; font-size: 12px; }
|
||||
.resubmission-hint { display:block;margin-top:4px;color:#c67323;font-size:10px;white-space:nowrap; }
|
||||
.workbench-pagination { display:flex;align-items:center;gap:10px;min-height:74px;padding:8px 16px;border-top:1px solid #e8eef5;background:#fbfdff; }
|
||||
.page-total { flex:none;color:#687d96;font-size:12px;white-space:nowrap; }
|
||||
.page-size { width:88px;flex:none; }
|
||||
.page-controls { min-width:0;flex:1;display:flex;align-items:center;justify-content:flex-end;gap:6px;overflow-x:auto;padding:3px 0; }
|
||||
.page-controls :deep(.el-button) { flex:none;margin:0; }
|
||||
.page-range-button { flex:none;min-width:116px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;padding:6px 9px;border:1px solid #dbe6f2;border-radius:8px;color:#365473;background:#fff;cursor:pointer;transition:.2s; }
|
||||
.page-range-button:hover,.page-range-button.active { border-color:#398fe0;color:#1768ae;background:#edf7ff; }
|
||||
.page-range-button b { font-size:12px; }
|
||||
.page-range-button small { color:#7188a2;font-size:10px;white-space:nowrap; }
|
||||
.page-jump { display:flex;flex:none;align-items:center;gap:6px;color:#687d96;font-size:12px;white-space:nowrap; }
|
||||
.page-jump :deep(.el-input-number) { width:68px; }
|
||||
.page-jump :deep(.el-input__inner) { text-align:center; }
|
||||
@media(max-width:1000px){.workbench-pagination{flex-wrap:wrap}.page-controls{justify-content:flex-start;flex-basis:100%}}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user