feat(scoring): 完善成绩工作台与排名投屏展示
- 成绩工作台移除顶部标题和当前投屏状态栏,将实时同步、大屏配色与检录入口移到项目栏;展示接口返回的当前赛事名称,移除切换赛事按钮。 - 排名大屏展示前三名领奖台与第 4—8 名列表;使用排名行序号区分共用出场编号的选手,修复姓名和分数动画串位,改善长姓名布局。 - 排名持续展示期间增加领奖台光晕、台阶流光及列表扫光,并遵从系统的减少动态效果设置。 - 裁判分配保存及提交按钮增加加载状态和重复点击保护。 - 增加限定 QA 测试库的赛事 59 成绩补齐脚本,仅插入缺失成绩及对应版本,保留已有裁判评分单。 验证:npm run build:local 通过;git diff --check 无异常。
This commit is contained in:
parent
635d7557a5
commit
be8bafe1e0
100
scripts/dev/seed-event-59-results.sql
Normal file
100
scripts/dev/seed-event-59-results.sql
Normal file
@ -0,0 +1,100 @@
|
||||
-- 仅为测试库中的赛事“59”补齐正式成绩,用于工作台和排名大屏调试。
|
||||
-- 只插入尚无有效成绩的出场记录;不修改裁判评分单及已有成绩。
|
||||
SET XACT_ABORT ON;
|
||||
SET NOCOUNT ON;
|
||||
|
||||
BEGIN TRY
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @EventId bigint = 491967903178757;
|
||||
IF DB_NAME() <> N'EventServerDB_QA'
|
||||
THROW 51000, N'仅允许在 EventServerDB_QA 执行', 1;
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.EventInfo WHERE Id = @EventId AND EventName = N'59' AND DeleteMark = 0)
|
||||
THROW 51001, N'目标赛事不存在或名称不符', 1;
|
||||
|
||||
DECLARE @EntryCount int = (
|
||||
SELECT COUNT(*) FROM dbo.EventSessionEntry e
|
||||
JOIN dbo.EventSession s ON s.Id = e.SessionId
|
||||
WHERE s.EventId = @EventId AND s.DeleteMark = 0 AND e.DeleteMark = 0
|
||||
);
|
||||
IF @EntryCount <> 10
|
||||
THROW 51002, N'赛事出场人数已变化,请先复核脚本', 1;
|
||||
|
||||
-- 与应用相同的 2023-01-01 UTC 基准和 12 位尾段;预留调试专用 worker 63。
|
||||
DECLARE @IdBase bigint = DATEDIFF_BIG(millisecond, '2023-01-01T00:00:00', SYSUTCDATETIME()) * 4096 + 63 * 64;
|
||||
|
||||
;WITH OrderedEntries AS (
|
||||
SELECT e.Id AS EntryId, e.SessionId, s.EventProjectId, s.ScoreRuleId,
|
||||
e.ParticipantName, e.GroupName, e.ParticipationRecordId,
|
||||
ROW_NUMBER() OVER (PARTITION BY s.EventProjectId, e.GroupName ORDER BY s.CreatedTime, s.Id, e.PlayOrder, e.Id) AS GroupRank,
|
||||
s.CreatedTime AS SessionCreatedTime, s.Id AS SessionOrderId, e.PlayOrder
|
||||
FROM dbo.EventSessionEntry e
|
||||
JOIN dbo.EventSession s ON s.Id = e.SessionId AND s.EventId = @EventId AND s.DeleteMark = 0
|
||||
WHERE e.DeleteMark = 0 AND e.IsVoided = 0
|
||||
), Missing AS (
|
||||
SELECT o.*, ROW_NUMBER() OVER (ORDER BY o.SessionCreatedTime, o.SessionOrderId, o.PlayOrder, o.EntryId) AS SeedNo
|
||||
FROM OrderedEntries o
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dbo.EventResult r WHERE r.SessionEntryId = o.EntryId AND r.DeleteMark = 0)
|
||||
), Factors AS (
|
||||
SELECT m.*, CAST(9.75 - (m.GroupRank - 1) * 0.60 AS decimal(18,4)) AS D,
|
||||
CAST(0.64 - (m.GroupRank - 1) * 0.02 AS decimal(18,4)) AS P,
|
||||
CAST(0.97 - (m.GroupRank - 1) * 0.01 AS decimal(18,4)) AS Q,
|
||||
CAST(0.08 + (m.GroupRank - 1) * 0.02 AS decimal(18,4)) AS Deduction
|
||||
FROM Missing m
|
||||
)
|
||||
SELECT f.*, ep.ProjectId, sr.RuleVersion,
|
||||
CAST(ROUND(f.D * (1 + f.P) * f.Q * (1 - f.Deduction), 2) AS decimal(18,2)) AS FinalScore,
|
||||
@IdBase + f.SeedNo AS ResultId,
|
||||
@IdBase + 32 + f.SeedNo AS VersionId
|
||||
INTO #Seed FROM Factors f
|
||||
JOIN dbo.EventProject ep ON ep.Id = f.EventProjectId
|
||||
JOIN dbo.ScoreRule sr ON sr.Id = f.ScoreRuleId;
|
||||
|
||||
IF (SELECT COUNT(*) FROM #Seed) > 10
|
||||
THROW 51003, N'待补成绩数量异常', 1;
|
||||
IF EXISTS (SELECT 1 FROM #Seed WHERE D <= 0 OR P < 0 OR Q <= 0 OR Deduction >= 1)
|
||||
THROW 51004, N'模拟成绩因子超出允许范围', 1;
|
||||
IF EXISTS (SELECT 1 FROM dbo.EventResult WHERE Id IN (SELECT ResultId FROM #Seed))
|
||||
OR EXISTS (SELECT 1 FROM dbo.EventResultVersion WHERE Id IN (SELECT VersionId FROM #Seed))
|
||||
THROW 51005, N'生成的主键发生冲突,请重试', 1;
|
||||
|
||||
INSERT dbo.EventResult (
|
||||
Id, ParticipationRecordId, RegistrationId, RegistrationPersonId, EventId, ProjectId,
|
||||
ParticipantName, GroupName, Score, Rank, AwardLevel, AwardStatus, CreatedTime,
|
||||
ProjectTypeName, DeleteMark, CreatedUserId, Status, SessionId, SessionEntryId,
|
||||
DifficultyScore, PerformanceScore, RequiredScore, DeductionScore, TotalScore,
|
||||
IsDisqualified, ResultStatus, ResultVersion, CalculationJson
|
||||
)
|
||||
SELECT ResultId, COALESCE(ParticipationRecordId, 0), 0, 0, @EventId, ProjectId,
|
||||
ParticipantName, GroupName, CONVERT(varchar(32), FinalScore), GroupRank, 0, 0, GETDATE(),
|
||||
N'个人花样', 0, 0, 0, SessionId, EntryId,
|
||||
D, P, Q, Deduction, FinalScore, 0, 20, 1,
|
||||
CONCAT('{"ruleType":"FREESTYLE_SINGLE","totalMode":"factor","formula":"D × (1 + P) × Q × M","scoreDecimals":2,"debugSeed":true,"totalScore":',
|
||||
CONVERT(varchar(32), FinalScore), ',"factors":{"d":', CONVERT(varchar(32), D),
|
||||
',"p":', CONVERT(varchar(32), P), ',"q":', CONVERT(varchar(32), Q),
|
||||
',"m":', CONVERT(varchar(32), CAST(1 - Deduction AS decimal(18,4))), '}}')
|
||||
FROM #Seed;
|
||||
|
||||
INSERT dbo.EventResultVersion (
|
||||
Id, EventResultId, EventId, SessionEntryId, ResultVersion,
|
||||
DifficultyScore, PerformanceScore, RequiredScore, DeductionScore, TotalScore, Rank,
|
||||
IsDisqualified, ResultStatus, SourceType, ScoreRuleId, RuleVersion, CalculationJson,
|
||||
Remark, DeleteMark, CreatedUserId, CreatedTime
|
||||
)
|
||||
SELECT VersionId, ResultId, @EventId, EntryId, 1,
|
||||
D, P, Q, Deduction, FinalScore, GroupRank,
|
||||
0, 20, 0, ScoreRuleId, RuleVersion,
|
||||
CONCAT('{"ruleType":"FREESTYLE_SINGLE","totalMode":"factor","formula":"D × (1 + P) × Q × M","scoreDecimals":2,"debugSeed":true,"totalScore":',
|
||||
CONVERT(varchar(32), FinalScore), ',"factors":{"d":', CONVERT(varchar(32), D),
|
||||
',"p":', CONVERT(varchar(32), P), ',"q":', CONVERT(varchar(32), Q),
|
||||
',"m":', CONVERT(varchar(32), CAST(1 - Deduction AS decimal(18,4))), '}}'),
|
||||
N'仅供调试的模拟成绩', 0, 0, SYSDATETIME()
|
||||
FROM #Seed;
|
||||
|
||||
SELECT COUNT(*) AS InsertedResults FROM #Seed;
|
||||
COMMIT TRANSACTION;
|
||||
END TRY
|
||||
BEGIN CATCH
|
||||
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
|
||||
THROW;
|
||||
END CATCH;
|
||||
@ -24,7 +24,7 @@
|
||||
<template #action="scope"><el-button text type="primary" :disabled="board.submitted" @click="openAssign(project, scope.row)">分配裁判</el-button><el-button text type="primary" :disabled="!scope.row.assigned" @click="openAssign(project, scope.row, true)">查看详情</el-button></template>
|
||||
</ProTable>
|
||||
</section>
|
||||
<div class="submit-bar"><el-button @click="load">刷新</el-button><el-button type="primary" :disabled="board.submitted" @click="submitAll">{{ board.submitted ? '已提交' : '提交' }}</el-button></div>
|
||||
<div class="submit-bar"><el-button @click="load">刷新</el-button><el-button type="primary" :loading="submitLoading" :disabled="board.submitted || submitLoading" @click="submitAll">{{ board.submitted ? '已提交' : '提交' }}</el-button></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer><el-button @click="assignVisible = false">{{ viewOnly ? '关闭' : '取消' }}</el-button><el-button v-if="!viewOnly" type="primary" @click="saveAssignments">确定</el-button></template>
|
||||
<template #footer><el-button @click="assignVisible = false">{{ viewOnly ? '关闭' : '取消' }}</el-button><el-button v-if="!viewOnly" type="primary" :loading="saveLoading" :disabled="saveLoading" @click="saveAssignments">确定</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="codeVisible" title="裁判报名二维码" width="min(420px, 92vw)" align-center destroy-on-close>
|
||||
@ -76,6 +76,8 @@ import { judgeAssignmentBoardGet, judgeAssignmentSavePost, judgeAssignmentSubmit
|
||||
const props = defineProps({ eventId: { type: [String, Number], required: true }, active: Boolean });
|
||||
const mode = ref('eventReferees');
|
||||
const loading = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const eventReferees = ref([]);
|
||||
const board = reactive({ arrangementConfirmed: false, submitted: false, projects: [] });
|
||||
const assignVisible = ref(false);
|
||||
@ -143,15 +145,30 @@ function chooseCandidate(item) {
|
||||
}
|
||||
function removeReferee(code, id) { const list = assignmentMap[code]; const index = list?.indexOf(id) ?? -1; if (index >= 0) list.splice(index, 1); }
|
||||
async function saveAssignments() {
|
||||
if (saveLoading.value) return;
|
||||
const incomplete = allSlots.value.find(slot => picked(slot.code).length < minOf(slot));
|
||||
if (incomplete) return ElMessage.warning(`${incomplete.name}至少需要分配 ${minOf(incomplete)} 名裁判`);
|
||||
const assignments = allSlots.value.flatMap(slot => picked(slot.code).map(id => ({ judgeRole: slot.code, eventRefereeId: id,
|
||||
refereeId: (candidates.value.find(item => item.eventRefereeId === id) || currentSession.value.assignments?.find(item => item.eventRefereeId === id))?.refereeId || 0 })));
|
||||
const result = await judgeAssignmentSavePost({ eventId: Number(props.eventId), sessionId: currentSession.value.sessionId, assignments });
|
||||
if (!result?.succeeded) return;
|
||||
ElMessage.success('分配已保存'); assignVisible.value = false; load();
|
||||
saveLoading.value = true;
|
||||
try {
|
||||
const result = await judgeAssignmentSavePost({ eventId: Number(props.eventId), sessionId: currentSession.value.sessionId, assignments });
|
||||
if (!result?.succeeded) return;
|
||||
ElMessage.success('分配已保存'); assignVisible.value = false; await load();
|
||||
} finally { saveLoading.value = false; }
|
||||
}
|
||||
async function submitAll() {
|
||||
if (submitLoading.value) return;
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await ElMessageBox.confirm('提交后分配任务即可同步到裁判端,是否继续?', '提交确认', { type: 'warning' });
|
||||
const result = await judgeAssignmentSubmitPost({ eventId: Number(props.eventId) });
|
||||
if (!result?.succeeded) return;
|
||||
ElMessage.success('提交成功'); await load();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') throw error;
|
||||
} finally { submitLoading.value = false; }
|
||||
}
|
||||
async function submitAll() { await ElMessageBox.confirm('提交后分配任务即可同步到裁判端,是否继续?', '提交确认', { type: 'warning' }); const result = await judgeAssignmentSubmitPost({ eventId: Number(props.eventId) }); if (!result?.succeeded) return; ElMessage.success('提交成功'); load(); }
|
||||
async function generateCode() {
|
||||
codeLoading.value = true;
|
||||
try {
|
||||
|
||||
@ -23,8 +23,16 @@
|
||||
<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>
|
||||
<div v-if="rankingRows.length" class="ranking-list"><article v-for="(row,index) in rankingRows" :key="`${row.entryNo || displayName(row)}-${index}`" :class="['rank-row', { podium: rankNumber(row,index) <= 3 }]" :style="{ '--delay': `${index * 85}ms` }"><div :class="['rank-badge', `rank-${rankNumber(row,index)}`]"><template v-if="rankNumber(row,index) <= 3"><span class="medal-ribbon left"></span><span class="medal-ribbon right"></span><el-icon><Trophy /></el-icon><b>{{ rankNumber(row,index) }}</b></template><b v-else>{{ rankNumber(row,index) }}</b></div><div class="rank-person"><div class="rank-title"><strong>{{ displayName(row) }}</strong><small v-if="displaySubName(row)">{{ displaySubName(row) }}</small></div><div class="rank-track"><i :style="{ width: rankWidth(row) }"></i></div></div><div class="rank-value"><strong class="score-ticker">{{ animatedValue(rankAnimationKey(row,index), row.totalScore ?? row.score) }}</strong><span>分<small>PTS</small></span></div></article></div>
|
||||
<div class="ranking-heading"><div><h2>{{ displayKindLabel }}排名</h2><span><i></i>{{ displayKind === 'team' ? 'TEAM RANKING' : 'INDIVIDUAL RANKING' }}</span></div><div class="ranking-status"><small>展示前 {{ rankingTopEight.length }} 名 · 共 {{ rankingTotal }} 个{{ displayKind === 'team' ? '队伍' : '参赛者' }}</small><b>TOP {{ String(rankingTopEight.length).padStart(2,'0') }}</b></div></div>
|
||||
<div v-if="rankingTopEight.length" class="ranking-showcase" :class="{ 'podium-only': !otherRankingRows.length }">
|
||||
<div class="podium-stage">
|
||||
<article v-for="entry in podiumDisplay" :key="`podium-${entry.index}`" :class="['podium-place', `place-${entry.place}`]" :style="{ '--delay': `${entry.index * 130}ms` }">
|
||||
<div class="podium-person"><span class="podium-medal"><el-icon><Trophy /></el-icon></span><span class="podium-label"><b>{{ entry.place === 1 ? '冠军' : entry.place === 2 ? '亚军' : '季军' }}</b><small>{{ entry.place === 1 ? 'CHAMPION' : entry.place === 2 ? 'RUNNER-UP' : 'THIRD PLACE' }}</small></span><strong class="podium-name" :class="{ compact: displayName(entry.row).length > 12 }" :title="displayName(entry.row)">{{ displayName(entry.row) }}</strong><small class="podium-unit" :title="displaySubName(entry.row)">{{ displaySubName(entry.row) || ' ' }}</small><div class="podium-score"><strong class="score-ticker">{{ animatedValue(rankAnimationKey(entry.row,entry.index), entry.row.totalScore ?? entry.row.score) }}</strong><span>分<small>PTS</small></span></div></div>
|
||||
<div class="podium-step"><span>0{{ entry.place }}</span></div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-if="otherRankingRows.length" class="ranking-followers"><div class="followers-heading"><b>第 4—{{ rankingTopEight.length }} 名</b><small>RANK 04—{{ String(rankingTopEight.length).padStart(2,'0') }}</small></div><div class="followers-list" :style="{ gridTemplateRows: `repeat(${otherRankingRows.length},minmax(0,1fr))` }"><article v-for="(row,index) in otherRankingRows" :key="`follower-${index}`" class="follower-row" :style="{ '--delay': `${index * 75 + 240}ms`, '--glint-delay': `${index * 1.2}s` }"><span class="follower-rank">{{ rankNumber(row,index+3) }}</span><div class="follower-person"><strong :title="displayName(row)">{{ displayName(row) }}</strong><small v-if="displaySubName(row)" :title="displaySubName(row)">{{ displaySubName(row) }}</small></div><div class="follower-score"><strong class="score-ticker">{{ animatedValue(rankAnimationKey(row,index+3), row.totalScore ?? row.score) }}</strong><span>分 <small>PTS</small></span></div></article></div></div>
|
||||
</div>
|
||||
<div v-else class="ranking-empty">当前组别暂无可展示排名</div>
|
||||
</section>
|
||||
<footer class="screen-footer"><span></span><b>跃动赛事 · 实时{{ viewType === 'score' ? '成绩' : '排名' }}系统<em>YD EVENT · LIVE {{ viewType === 'score' ? 'RESULTS' : 'RANKING' }}</em></b><span></span></footer>
|
||||
@ -49,10 +57,10 @@ let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-
|
||||
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0;
|
||||
const venueId=route.query.venueId?Number(route.query.venueId):(Number(savedContext.venueId)||undefined);
|
||||
const isLocalPreview=import.meta.env.DEV&&['score','ranking'].includes(String(route.query.preview));
|
||||
const cast=ref(null),offline=ref(false),rankPage=ref(0),viewport=ref({width:window.innerWidth,height:window.innerHeight}),animatedNumbers=ref({});
|
||||
const cast=ref(null),offline=ref(false),viewport=ref({width:window.innerWidth,height:window.innerHeight}),animatedNumbers=ref({});
|
||||
const scoreTheme=ref(readScoreScreenTheme(eventId));
|
||||
const screenThemeStyle=computed(()=>({'--screen-theme':scoreTheme.value.themeColor,'--screen-text':scoreTheme.value.textColor}));
|
||||
let pollTimer,rankTimer,castChannel,themeChannel,numberAnimationFrame;
|
||||
let pollTimer,castChannel,themeChannel,numberAnimationFrame;
|
||||
function normalizePayload(value){if(Array.isArray(value))return value.map(normalizePayload);if(!value||typeof value!=='object')return value;return Object.fromEntries(Object.entries(value).map(([key,item])=>[key.charAt(0).toLowerCase()+key.slice(1),normalizePayload(item)]))}
|
||||
function unwrapApiData(response){let value=response;for(let i=0;i<3&&value&&typeof value==='object'&&Object.prototype.hasOwnProperty.call(value,'data');i++)value=value.data;return value}
|
||||
function normalizeCast(value){const normalized=normalizePayload(value),castType=Number(normalized?.castType);return normalized&&(castType===1||castType===2)&&normalized.payloadJson!=null?{...normalized,castType}:null}
|
||||
@ -61,23 +69,24 @@ function readCachedCast(){try{const scoped=localStorage.getItem(cacheKey()),fall
|
||||
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}
|
||||
if(!next){cast.value=null;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([
|
||||
{rank:1,participantName:'陈一',teamName:'跃动队',entryNo:'A-017',totalScore:85.5},
|
||||
{rank:2,participantName:'林嘉怡',teamName:'星跃队',entryNo:'A-019',totalScore:82.4},
|
||||
{rank:3,participantName:'周子涵',teamName:'青云队',entryNo:'A-021',totalScore:79.8},
|
||||
{rank:4,participantName:'许晨',teamName:'飞跃队',entryNo:'A-024',totalScore:76.2},
|
||||
{rank:5,participantName:'王心悦',teamName:'跃动队',entryNo:'A-028',totalScore:73.6},
|
||||
{rank:6,participantName:'李奕辰',teamName:'星跃队',entryNo:'A-030',totalScore:71.9},
|
||||
{rank:2,participantName:'林嘉怡',teamName:'星跃队',entryNo:'A-017',totalScore:82.4},
|
||||
{rank:3,participantName:'周子涵',teamName:'青云队',entryNo:'A-017',totalScore:79.8},
|
||||
{rank:4,participantName:'许晨',teamName:'飞跃队',entryNo:'A-017',totalScore:76.2},
|
||||
{rank:5,participantName:'王心悦',teamName:'跃动队',entryNo:'A-017',totalScore:73.6},
|
||||
{rank:6,participantName:'李奕辰',teamName:'星跃队',entryNo:'A-017',totalScore:71.9},
|
||||
{rank:7,participantName:'宋佳宁',teamName:'青云队',entryNo:'A-017',totalScore:69.4},
|
||||
{rank:8,participantName:'赵嘉禾',teamName:'飞跃队',entryNo:'A-017',totalScore:67.8},
|
||||
])};
|
||||
return {...common,castType:1,payloadJson:JSON.stringify({participantName:'陈一',teamName:'跃动队',entryNo:'A-017',rank:1,totalScore:85.5,difficultyScore:21,performanceScore:15,requiredScore:3,deductionFactor:.89})};
|
||||
}
|
||||
@ -109,13 +118,17 @@ function metricDisplayValue(item){
|
||||
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));
|
||||
const rankingAll=computed(()=>Array.isArray(payload.value)?payload.value:[]);
|
||||
const rankingTotal=computed(()=>rankingAll.value.length);
|
||||
const rankingTopEight=computed(()=>rankingAll.value.slice(0,8));
|
||||
const podiumDisplay=computed(()=>[1,0,2].filter(index=>rankingTopEight.value[index]).map(index=>({row:rankingTopEight.value[index],index,place:index+1})));
|
||||
const otherRankingRows=computed(()=>rankingTopEight.value.slice(3));
|
||||
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 rankNumber(row,index){const rank=Number(row.rank);return Number.isFinite(rank)&&rank>0?rank:index+1}
|
||||
function value(input,decimals=2){if(input===undefined||input===null||input==='')return '0';const n=Number(input);return Number.isFinite(n)?String(Number(n.toFixed(decimals))):input}
|
||||
function rankAnimationKey(row,index){return `rank:${row.entryNo||displayName(row)||rankingPageIndex.value*6+index}`}
|
||||
// 同组运动员可能共用出场编号;排名快照的行序号才是此画面内稳定且唯一的标识。
|
||||
function rankAnimationKey(_row,index){return `rank:${index}`}
|
||||
function animatedValue(key,fallback,decimals=2){return Object.prototype.hasOwnProperty.call(animatedNumbers.value,key)?value(animatedNumbers.value[key],decimals):value(fallback,decimals)}
|
||||
function animateScoreNumbers(){
|
||||
window.cancelAnimationFrame(numberAnimationFrame);
|
||||
@ -124,14 +137,14 @@ function animateScoreNumbers(){
|
||||
targets.hero=payload.value.totalScore??payload.value.score;
|
||||
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});
|
||||
rankingTopEight.value.forEach((row,index)=>{targets[rankAnimationKey(row,index)]=row.totalScore??row.score});
|
||||
}
|
||||
if(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches){animatedNumbers.value=targets;return}
|
||||
const numericTargets=Object.fromEntries(Object.entries(targets).filter(([,target])=>Number.isFinite(Number(target))).map(([key,target])=>[key,Number(target)]));
|
||||
const staticTargets=Object.fromEntries(Object.entries(targets).filter(([,target])=>!Number.isFinite(Number(target))));
|
||||
const startedAt=performance.now(),duration=viewType.value==='score'?1280:960;
|
||||
const tick=(now)=>{
|
||||
const progress=Math.min(1,(now-startedAt)/duration),eased=1-Math.pow(1-progress,3);
|
||||
const progress=Math.max(0,Math.min(1,(now-startedAt)/duration)),eased=1-Math.pow(1-progress,3);
|
||||
animatedNumbers.value={...staticTargets,...Object.fromEntries(Object.entries(numericTargets).map(([key,target])=>[key,target*eased]))};
|
||||
if(progress<1)numberAnimationFrame=window.requestAnimationFrame(tick);
|
||||
};
|
||||
@ -149,9 +162,9 @@ let castRequesting=false;
|
||||
async function loadCast(){if(!eventId||castRequesting)return;castRequesting=true;try{const current=normalizeCast(unwrapApiData(await workbenchScreenCastCurrentGet({eventId,venueId,_t:Date.now()})));if(current)acceptPublishedCast({...current,displayKind:current.displayKind||route.query.displayKind});else if(!cast.value||Date.now()-Number(cast.value.cachedAt||0)>=30000)cast.value=null;offline.value=false}catch{offline.value=true}finally{castRequesting=false}}
|
||||
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(()=>{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)});
|
||||
watch(castKey,()=>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)}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.cancelAnimationFrame(numberAnimationFrame);castChannel?.close();themeChannel?.close();window.removeEventListener('storage',onThemeStorage);window.removeEventListener('resize',onViewportResize)});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -689,6 +702,77 @@ onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTi
|
||||
.score-screen.theme-light .waiting-orbit::after { border-color: color-mix(in srgb,var(--screen-theme) 20%,transparent); }
|
||||
.score-screen.theme-light .screen-footer > span { background: linear-gradient(90deg,transparent,color-mix(in srgb,var(--screen-theme) 35%,transparent),transparent); }
|
||||
|
||||
/* 排名前八名同屏展示:前三名为领奖台,其余名次为紧凑列表。 */
|
||||
.view-ranking .ranking-layout { padding: 22px 30px 20px; }
|
||||
.view-ranking .ranking-heading { margin-bottom: 8px; }
|
||||
.ranking-status b { color: var(--cyan); font-size: 18px; letter-spacing: .28em; }
|
||||
.ranking-showcase { display: grid; grid-template-rows: minmax(0,1fr) 315px; gap: 14px; flex: 1; min-height: 0; }
|
||||
.ranking-showcase.podium-only { grid-template-rows: minmax(0,1fr); }
|
||||
.podium-stage { position: relative; display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); align-items: end; gap: 24px; min-height: 0; padding: 10px 48px 0; }
|
||||
.podium-stage::before { content: ''; position: absolute; z-index: 0; left: 5%; right: 5%; bottom: 0; height: 55%; border-radius: 50%; background: radial-gradient(ellipse at 50% 100%,color-mix(in srgb,var(--screen-theme) 38%,transparent),transparent 68%); pointer-events: none; animation: podium-ambient 8s ease-in-out infinite alternate; }
|
||||
.podium-place { --podium-accent: #b8d9ee; position: relative; z-index: 1; grid-row: 1; display: flex; flex-direction: column; align-items: center; justify-content: end; min-width: 0; animation: podium-rise .72s cubic-bezier(.18,.78,.24,1) both; animation-delay: var(--delay); }
|
||||
.podium-place.place-1 { --podium-accent: #ffd56c; grid-column: 2; }
|
||||
.podium-place.place-2 { --podium-accent: #c6e5f5; grid-column: 1; }
|
||||
.podium-place.place-3 { --podium-accent: #f0ae7d; grid-column: 3; }
|
||||
.podium-person { box-sizing: border-box; position: relative; display: flex; flex-direction: column; align-items: center; width: 94%; height: 285px; min-width: 0; overflow: hidden; padding: 16px 20px 15px; border: 1px solid color-mix(in srgb,var(--podium-accent) 52%,transparent); border-radius: 18px 18px 9px 9px; background: radial-gradient(circle at 50% 0,color-mix(in srgb,var(--podium-accent) 19%,transparent),transparent 58%),linear-gradient(160deg,color-mix(in srgb,var(--screen-theme) 45%,#06183d),#071537); box-shadow: inset 0 1px color-mix(in srgb,var(--podium-accent) 26%,transparent),0 16px 30px rgba(2,8,41,.25); text-align: center; }
|
||||
.podium-person::before { content: ''; position: absolute; inset: -40% auto -40% -38%; width: 34%; transform: translateX(-250%) rotate(18deg); background: linear-gradient(90deg,transparent,color-mix(in srgb,var(--podium-accent) 22%,transparent),transparent); animation: podium-sheen 1.25s ease-out both; animation-delay: calc(var(--delay) + 350ms); pointer-events: none; }
|
||||
.place-1 .podium-person { height: 284px; border-color: color-mix(in srgb,var(--podium-accent) 75%,transparent); box-shadow: inset 0 1px color-mix(in srgb,var(--podium-accent) 50%,transparent),0 0 34px color-mix(in srgb,var(--podium-accent) 16%,transparent),0 20px 36px rgba(2,8,41,.3); }
|
||||
.podium-medal { display: grid; place-items: center; flex: none; width: 43px; height: 43px; margin-bottom: 9px; border: 1px solid color-mix(in srgb,var(--podium-accent) 72%,transparent); border-radius: 50%; color: var(--podium-accent); background: color-mix(in srgb,var(--podium-accent) 13%,transparent); font-size: 25px; }
|
||||
.place-1 .podium-medal { width: 51px; height: 51px; font-size: 30px; }
|
||||
.place-1 .podium-medal { animation: champion-glow 3.8s ease-in-out infinite alternate; }
|
||||
.podium-label { flex: none; }
|
||||
.podium-label b,.podium-label small { display: block; }
|
||||
.podium-label b { color: var(--podium-accent); font-size: 20px; line-height: 1.1; letter-spacing: .15em; }
|
||||
.podium-label small { margin-top: 3px; color: color-mix(in srgb,var(--screen-text) 68%,transparent); font-size: 10px; letter-spacing: .2em; }
|
||||
.podium-name { flex: none; overflow: hidden; display: block; max-width: 100%; max-height: 2.5em; margin-top: 9px; color: var(--screen-text); font-size: 32px; line-height: 1.25; overflow-wrap: anywhere; text-shadow: 0 3px 18px color-mix(in srgb,var(--podium-accent) 22%,transparent); }
|
||||
.place-1 .podium-name { margin-top: 9px; font-size: 37px; }
|
||||
.podium-name.compact { font-size: 26px; }
|
||||
.place-1 .podium-name.compact { font-size: 29px; }
|
||||
.podium-unit { flex: none; overflow: hidden; display: block; max-width: 100%; min-height: 20px; margin-top: 5px; color: color-mix(in srgb,var(--screen-text) 74%,transparent); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.podium-score { display: flex; align-items: baseline; justify-content: center; gap: 8px; width: 100%; margin-top: auto; color: var(--screen-text); }
|
||||
.podium-score strong { font-size: 42px; font-variant-numeric: tabular-nums; line-height: 1; letter-spacing: -.04em; }
|
||||
.place-1 .podium-score strong { font-size: 50px; }
|
||||
.podium-score > span { font-size: 15px; font-weight: 700; }
|
||||
.podium-score > span small { display: block; color: color-mix(in srgb,var(--screen-text) 62%,transparent); font-size: 9px; letter-spacing: .1em; }
|
||||
.podium-step { position: relative; display: grid; place-items: center; box-sizing: border-box; width: 94%; height: 82px; overflow: hidden; border: 1px solid color-mix(in srgb,var(--podium-accent) 63%,transparent); border-top: 3px solid var(--podium-accent); border-radius: 7px 7px 2px 2px; background: linear-gradient(180deg,color-mix(in srgb,var(--podium-accent) 34%,var(--screen-theme)),color-mix(in srgb,var(--podium-accent) 12%,#07163a)); box-shadow: 0 15px 30px rgba(2,8,41,.2); }
|
||||
.podium-step::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 42%; transform: translateX(-140%); background: linear-gradient(90deg,transparent,color-mix(in srgb,var(--podium-accent) 25%,transparent),transparent); pointer-events: none; animation: podium-step-glint 7s ease-in-out infinite; animation-delay: calc(var(--delay) + 800ms); }
|
||||
.place-1 .podium-step { height: 118px; }
|
||||
.place-3 .podium-step { height: 61px; }
|
||||
.podium-step::after { content: ''; position: absolute; inset: 0; background: linear-gradient(105deg,rgba(255,255,255,.16),transparent 35% 65%,rgba(0,0,0,.12)); pointer-events: none; }
|
||||
.podium-step span { position: relative; z-index: 1; color: color-mix(in srgb,var(--podium-accent) 78%,white); font-size: 58px; font-weight: 900; line-height: 1; letter-spacing: -.05em; opacity: .85; }
|
||||
.place-1 .podium-step span { font-size: 82px; }
|
||||
.ranking-followers { display: flex; flex-direction: column; min-height: 0; padding: 9px 13px 12px; border: 1px solid color-mix(in srgb,var(--screen-theme) 46%,#8ec9f2); border-radius: 15px; background: color-mix(in srgb,var(--screen-theme) 24%,#061437); }
|
||||
.followers-heading { display: flex; align-items: baseline; gap: 12px; flex: none; height: 27px; padding: 0 6px; }
|
||||
.followers-heading b { color: var(--screen-text); font-size: 14px; }
|
||||
.followers-heading small { color: color-mix(in srgb,var(--screen-text) 62%,transparent); font-size: 10px; letter-spacing: .2em; }
|
||||
.followers-list { display: grid; grid-template-rows: repeat(5,minmax(0,1fr)); gap: 5px; flex: 1; min-height: 0; }
|
||||
.follower-row { position: relative; display: grid; grid-template-columns: 50px minmax(0,1fr) auto; align-items: center; gap: 13px; min-height: 0; overflow: hidden; padding: 4px 15px 4px 7px; border: 1px solid color-mix(in srgb,var(--screen-theme) 38%,#678eb9); border-radius: 9px; background: color-mix(in srgb,var(--screen-theme) 28%,#071738); animation: follower-enter .55s ease both; animation-delay: var(--delay); }
|
||||
.follower-row::after { content: ''; position: absolute; inset: 0 auto 0 0; width: 28%; transform: translateX(-140%); background: linear-gradient(90deg,transparent,rgba(255,255,255,.14),transparent); pointer-events: none; animation: follower-glint 8s ease-in-out infinite; animation-delay: var(--glint-delay); }
|
||||
.follower-row > * { position: relative; z-index: 1; }
|
||||
.follower-rank { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 7px; color: var(--screen-text); background: color-mix(in srgb,var(--screen-theme) 58%,#173e6b); font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
|
||||
.follower-person { display: flex; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; }
|
||||
.follower-person strong { overflow: hidden; display: block; max-width: 100%; color: var(--screen-text); font-size: 21px; line-height: 1.15; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.follower-person small { overflow: hidden; display: block; max-width: 100%; color: color-mix(in srgb,var(--screen-text) 68%,transparent); font-size: 12px; line-height: 1.1; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.follower-score { display: flex; align-items: baseline; gap: 7px; color: var(--screen-text); }
|
||||
.follower-score strong { font-size: 26px; font-variant-numeric: tabular-nums; line-height: 1; }
|
||||
.follower-score > span { font-size: 13px; }
|
||||
.follower-score > span small { color: color-mix(in srgb,var(--screen-text) 62%,transparent); font-size: 9px; }
|
||||
.score-screen.theme-light .podium-person { border-color: color-mix(in srgb,var(--podium-accent) 65%,#d5e2ee); background: radial-gradient(circle at 50% 0,color-mix(in srgb,var(--podium-accent) 19%,white),transparent 60%),linear-gradient(160deg,#fff,#f3f8fc); box-shadow: 0 15px 30px rgba(30,70,115,.1); }
|
||||
.score-screen.theme-light .podium-label b { color: color-mix(in srgb,var(--podium-accent) 52%,#214b6c); }
|
||||
.score-screen.theme-light .podium-step { background: linear-gradient(180deg,color-mix(in srgb,var(--podium-accent) 45%,#fff),color-mix(in srgb,var(--podium-accent) 17%,#e9f2fb)); box-shadow: 0 12px 24px rgba(30,70,115,.1); }
|
||||
.score-screen.theme-light .podium-step span { color: color-mix(in srgb,var(--podium-accent) 48%,#284765); }
|
||||
.score-screen.theme-light .ranking-followers { border-color: #dbe7f1; background: rgba(239,246,252,.78); }
|
||||
.score-screen.theme-light .follower-row { border-color: #e1eaf2; background: #fff; }
|
||||
.score-screen.theme-light .follower-row::after { background: linear-gradient(90deg,transparent,color-mix(in srgb,var(--screen-theme) 12%,transparent),transparent); }
|
||||
.score-screen.theme-light .follower-rank { background: color-mix(in srgb,var(--screen-theme) 12%,#fff); }
|
||||
@keyframes podium-rise { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes follower-enter { from { opacity: 0; transform: translateX(-14px); } to { opacity: 1; transform: none; } }
|
||||
@keyframes podium-sheen { from { transform: translateX(-250%) rotate(18deg); opacity: 0; } 35% { opacity: 1; } to { transform: translateX(1100%) rotate(18deg); opacity: 0; } }
|
||||
@keyframes champion-glow { from { box-shadow: 0 0 12px color-mix(in srgb,var(--podium-accent) 12%,transparent); } to { box-shadow: 0 0 27px color-mix(in srgb,var(--podium-accent) 38%,transparent); } }
|
||||
@keyframes podium-ambient { from { opacity: .55; transform: scale(.88); } to { opacity: 1; transform: scale(1.12); } }
|
||||
@keyframes podium-step-glint { 0%, 12% { transform: translateX(-140%); opacity: 0; } 24% { opacity: .75; } 42%, 100% { transform: translateX(340%); opacity: 0; } }
|
||||
@keyframes follower-glint { 0%, 5% { transform: translateX(-140%); opacity: 0; } 16% { opacity: .55; } 30%, 100% { transform: translateX(460%); opacity: 0; } }
|
||||
|
||||
.preview-badge { position: absolute; z-index: 5; top: 31px; right: 51px; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 7px 12px; border: 1px solid rgba(245,200,108,.5); border-radius: 6px; color: #ffe3a5; background: rgba(42,31,33,.75); font-size: 12px; font-weight: 700; letter-spacing: .06em; }
|
||||
.preview-badge span { color: #d8ba8d; font-size: 9px; font-weight: 500; letter-spacing: .12em; }
|
||||
@keyframes header-reveal { from { opacity: 0; transform: translateY(-16px); } to { opacity: 1; transform: none; } }
|
||||
@ -698,7 +782,7 @@ onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTi
|
||||
@keyframes halo-breathe { from { opacity: .56; } to { opacity: 1; } }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.aurora-strip, .ambient, .screen-grid, .panel-shine::after, .score-ticker, .hero-score strong, .score-beam, .ranking-heading span > i, .metric-card, .rank-row, .waiting-orbit::before, .waiting-orbit::after, .view-score .screen-header, .view-score .context-bar, .view-score .hero-card, .view-score .hero-halo, .view-score .entity-chip, .view-score .entity-name { animation: none !important; }
|
||||
.aurora-strip, .ambient, .screen-grid, .panel-shine::after, .score-ticker, .hero-score strong, .score-beam, .ranking-heading span > i, .metric-card, .rank-row, .podium-stage::before, .podium-place, .podium-person::before, .podium-medal, .podium-step::before, .follower-row, .follower-row::after, .waiting-orbit::before, .waiting-orbit::after, .view-score .screen-header, .view-score .context-bar, .view-score .hero-card, .hero-halo, .view-score .entity-chip, .view-score .entity-name { animation: none !important; }
|
||||
.rank-track i, .page-dots i, .cast-enter-active, .cast-leave-active { transition: none !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<div class="workbench-page">
|
||||
<section class="workbench-main">
|
||||
<header class="topbar">
|
||||
<div><h1>{{ tabTitle }}</h1><p>{{ eventName }} · {{ tabDescription }}</p></div>
|
||||
<div class="top-actions"><span :class="['live-dot', { offline }]">● {{ syncStatusText }}</span><el-button v-if="offline" link type="primary" :loading="retryLoading" @click="retrySync">重试同步</el-button><el-button v-if="activeTab !== 'corrections'" @click="themeSettingsVisible=true">大屏配色</el-button><el-button v-if="activeTab !== 'screen'" class="top-checkin-button" @click="openCheckinScreen">检录大屏</el-button><el-button @click="goLauncher">切换赛事</el-button></div>
|
||||
</header>
|
||||
|
||||
<template v-if="activeTab !== 'corrections'">
|
||||
<div class="workspace">
|
||||
<aside class="session-panel">
|
||||
<div class="current-event"><span>当前赛事</span><strong :title="eventName">{{ eventName }}</strong></div>
|
||||
<div class="session-head"><b>比赛项目</b><el-button text type="primary" :loading="loadingSessions" @click="loadSessions">刷新</el-button></div>
|
||||
<div class="session-list">
|
||||
<button v-for="(project, index) in sessionGroups" :key="project.key" class="session-switch" :class="{ active: selectedProjectId === project.key }" @click="selectProject(project)">
|
||||
@ -19,6 +15,10 @@
|
||||
</button>
|
||||
<el-empty v-if="!loadingSessions && !sessionGroups.length" :image-size="70" description="暂无项目" />
|
||||
</div>
|
||||
<div class="session-utilities">
|
||||
<div class="sync-line"><span :class="['live-dot', { offline }]">● {{ syncStatusText }}</span><el-button v-if="offline" link type="primary" :loading="retryLoading" @click="retrySync">重试</el-button></div>
|
||||
<div class="utility-buttons"><el-button @click="themeSettingsVisible=true">大屏配色</el-button><el-button :disabled="!selectedSession" @click="openCheckinScreen">检录大屏</el-button></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="result-panel">
|
||||
@ -26,14 +26,7 @@
|
||||
<div class="control-copy"><span>大屏控制</span><b>选择展示对象与比赛组别</b><small>单条成绩从下方列表投出;排名按当前选择的项目组别投出</small></div>
|
||||
<div class="control-field"><label>展示对象</label><el-radio-group v-model="castEntityKind" size="default"><el-radio-button label="individual">个人</el-radio-button><el-radio-button label="team">团队</el-radio-button></el-radio-group></div>
|
||||
<div class="control-field group-field"><label>项目 / 组别 / 场次</label><el-select :model-value="selectedSession?.sessionId" placeholder="请选择需要控制的组别" filterable @change="selectSessionById"><el-option v-for="(session,index) in sessions" :key="session.sessionId" :label="`${session.projectName} / ${session.groupName || '混合组别'} / ${formatSessionName(session,index)}`" :value="session.sessionId"/></el-select></div>
|
||||
<div class="control-buttons"><el-button @click="openScoreScreen">显示成绩大屏</el-button><el-button class="checkin-button" @click="openCheckinScreen">切换检录大屏</el-button><el-button type="primary" :disabled="!selectedSession" @click="openRanking">投出{{ castEntityLabel }}排名</el-button></div>
|
||||
<div class="screen-theme-control">
|
||||
<div class="theme-control-copy"><b>大屏配色</b><small>成绩和排名大屏实时生效,设置保存在本机</small></div>
|
||||
<el-radio-group :model-value="scoreTheme.mode" size="small" aria-label="大屏主题" @change="updateScoreThemeMode"><el-radio-button label="light">亮色</el-radio-button><el-radio-button label="dark">暗色</el-radio-button></el-radio-group>
|
||||
<div class="theme-color-fields"><label>主题色<el-color-picker :model-value="scoreTheme.themeColor" :predefine="themeColorPresets" @change="color => updateScoreTheme('themeColor', color)" /></label><label>文字颜色<el-color-picker :model-value="scoreTheme.textColor" :predefine="textColorPresets" @change="color => updateScoreTheme('textColor', color)" /></label></div>
|
||||
<div :class="['theme-preview', `theme-preview-${scoreTheme.mode}`]" :style="{ '--preview-theme': scoreTheme.themeColor, '--preview-text': scoreTheme.textColor }"><strong>85.5</strong><span>比赛成绩<small>LIVE RESULTS</small></span></div>
|
||||
<el-button text type="primary" @click="resetScoreTheme">恢复默认</el-button>
|
||||
</div>
|
||||
<div class="control-buttons"><el-button @click="openScoreScreen">显示成绩大屏</el-button><el-button type="primary" :disabled="!selectedSession" @click="openRanking">投出{{ castEntityLabel }}排名</el-button></div>
|
||||
</section>
|
||||
<div v-if="selectedProject" class="result-heading">
|
||||
<div><b>{{ selectedProject.projectName }}</b><span>{{ selectedProject.groupCount }} 个组别</span></div>
|
||||
@ -48,10 +41,6 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="currentCast" class="cast-banner">
|
||||
<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 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>
|
||||
@ -80,8 +69,9 @@
|
||||
</template>
|
||||
|
||||
<section v-else class="correction-page">
|
||||
<div class="current-event correction-event"><span>当前赛事</span><strong :title="eventName">{{ eventName }}</strong></div>
|
||||
<div class="filterbar correction-filter"><el-input v-model="correctionKeyword" clearable placeholder="搜索修正原因" @keyup.enter="loadCorrections"/><el-select v-model="correctionStatus" clearable placeholder="申请状态" @change="loadCorrections"><el-option v-for="x in correctionStatuses" :key="x.value" :label="x.label" :value="x.value"/></el-select><el-button type="primary" @click="loadCorrections">查询</el-button></div>
|
||||
<el-table v-loading="loadingCorrections" :data="corrections" height="calc(100vh - 175px)"><el-table-column label="场次" width="120"><template #default="{row}">{{formatSessionText(row.sessionNo)}}</template></el-table-column><el-table-column prop="entryNo" label="出场编号" width="130"/><el-table-column prop="participantName" label="运动员" min-width="140"/><el-table-column prop="judgeRole" label="裁判席位" width="110"/><el-table-column prop="applyUserName" label="申请人" width="110"/><el-table-column prop="applyReason" label="修正原因" min-width="240" show-overflow-tooltip/><el-table-column prop="createdTime" label="申请时间" width="175"><template #default="{row}">{{formatTime(row.createdTime)}}</template></el-table-column><el-table-column label="状态" width="105"><template #default="{row}"><el-tag>{{row.correctionStatusName}}</el-tag></template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{row}"><el-button text @click="showCorrectionDetail(row)">详情</el-button><el-button v-if="row.correctionStatus===0" text type="success" @click="approveCorrection(row,true)">批准</el-button><el-button v-if="row.correctionStatus===0" text type="danger" @click="approveCorrection(row,false)">驳回</el-button></template></el-table-column></el-table>
|
||||
<el-table v-loading="loadingCorrections" :data="corrections" height="calc(100vh - 215px)"><el-table-column label="场次" width="120"><template #default="{row}">{{formatSessionText(row.sessionNo)}}</template></el-table-column><el-table-column prop="entryNo" label="出场编号" width="130"/><el-table-column prop="participantName" label="运动员" min-width="140"/><el-table-column prop="judgeRole" label="裁判席位" width="110"/><el-table-column prop="applyUserName" label="申请人" width="110"/><el-table-column prop="applyReason" label="修正原因" min-width="240" show-overflow-tooltip/><el-table-column prop="createdTime" label="申请时间" width="175"><template #default="{row}">{{formatTime(row.createdTime)}}</template></el-table-column><el-table-column label="状态" width="105"><template #default="{row}"><el-tag>{{row.correctionStatusName}}</el-tag></template></el-table-column><el-table-column label="操作" width="180" fixed="right"><template #default="{row}"><el-button text @click="showCorrectionDetail(row)">详情</el-button><el-button v-if="row.correctionStatus===0" text type="success" @click="approveCorrection(row,true)">批准</el-button><el-button v-if="row.correctionStatus===0" text type="danger" @click="approveCorrection(row,false)">驳回</el-button></template></el-table-column></el-table>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@ -144,7 +134,7 @@ import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
|
||||
import { defaultScoreScreenTheme, normalizeScoreScreenTheme, readScoreScreenTheme, saveScoreScreenTheme, scoreScreenModeTextColors, scoreScreenThemeChannel, scoreScreenThemeKey } from '../score-screen-theme.mjs';
|
||||
import {
|
||||
workbenchSessionsGet, workbenchProjectEntriesGet, workbenchEntryDetailGet, workbenchScreenCastPost,
|
||||
workbenchScreenCastCurrentGet, workbenchScreenCastWithdrawPost, workbenchCorrectionsGet,
|
||||
workbenchCorrectionsGet,
|
||||
workbenchCorrectionDetailGet, workbenchCorrectionApprovePost, workbenchResultConfirmPost,
|
||||
workbenchEntryRematchPost,
|
||||
workbenchCorrectionInitiatePost, workbenchResultsExportGet, workbenchScoreSheetsExportGet,
|
||||
@ -154,25 +144,25 @@ const route=useRoute(), router=useRouter();
|
||||
const storesTagsViewRoutes = useTagsViewRoutes();
|
||||
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
|
||||
const eventId=ref(Number(route.query.eventId)||Number(savedContext.eventId)||0);
|
||||
const eventName=ref(String(route.query.eventName||savedContext.eventName||'未选择赛事'));
|
||||
const eventName=ref(String(route.query.eventName||(Number(savedContext.eventId)===eventId.value?savedContext.eventName:'')||`赛事 ${eventId.value}`));
|
||||
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);
|
||||
const activeTab=ref('screen'), sessions=ref([]), selectedProjectId=ref(''), selectedSession=ref(null), entries=ref([]);
|
||||
const loadingSessions=ref(false), loadingEntries=ref(false), entriesSyncError=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 offline=computed(()=>entriesSyncError.value);
|
||||
const syncStatusText=computed(()=>entriesSyncError.value?'成绩同步异常':'实时同步');
|
||||
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;
|
||||
let pollTimer, entriesController=null;
|
||||
let entriesRequestId=0;
|
||||
let mountedReady=false, componentActive=false, destroyed=false;
|
||||
let entriesFailureCount=0, castFailureCount=0, entriesNextRetryAt=0, castNextRetryAt=0;
|
||||
let entriesFailureCount=0, entriesNextRetryAt=0;
|
||||
const retryDelays=[10000,20000,40000,60000];
|
||||
let scoreScreenWindow=null;
|
||||
let themeChannel=null;
|
||||
@ -297,17 +287,6 @@ const correctionGroups=computed(()=>{const map=new Map();for(const s of correcti
|
||||
const correctionSheetOptions=computed(()=>(correctionRow.value?.judgeScores||[]).filter(s=>s.scoreSheetId>0&&s.sheetStatus>=10&&s.judgeRoleType===correctionForm.value.judgeRoleType));
|
||||
function isCorrectionSheetAvailable(x){return Boolean(x&&x.sheetStatus>=10&&x.sheetStatus<90&&!x.hasPendingCorrection)}
|
||||
function onCorrectionRoleChange(){correctionForm.value.scoreSheetId=null}
|
||||
const tabTitle=computed(()=>({screen:'成绩投屏',results:'成绩管理',corrections:'成绩修正申请'})[activeTab.value]);
|
||||
const tabDescription=computed(()=>({screen:'选择已完成成绩投到现场大屏',results:'复核成绩、查看评分单并处理异常比赛',corrections:'审批裁判发起的修正申请并追踪处理进度'})[activeTab.value]);
|
||||
const currentCastPayload=computed(()=>{try{const value=JSON.parse(currentCast.value?.payloadJson||'{}');return Array.isArray(value)?value.map(normalizeCastValue):normalizeCastValue(value)}catch{return {}}});
|
||||
const currentCastSummary=computed(()=>{
|
||||
if(!currentCast.value)return '—';
|
||||
if(currentCast.value.castType===2)return `${currentCast.value.projectName||'当前项目'} / ${currentCast.value.groupName||'当前组别'}排名`;
|
||||
const data=currentCastPayload.value;
|
||||
const name=data.participantName||data.teamName||'未命名选手';
|
||||
return `${name} · ${score(data.totalScore??data.score)}分`;
|
||||
});
|
||||
|
||||
function normalizeCastValue(value){if(!value||typeof value!=='object')return value;return Object.fromEntries(Object.entries(value).map(([key,item])=>[key.charAt(0).toLowerCase()+key.slice(1),item]))}
|
||||
function rememberCastKind(){localStorage.setItem(`yd-event-cast-kind:${eventId.value}`,castEntityKind.value)}
|
||||
function unwrapApiData(response){let value=response;for(let i=0;i<3&&value&&typeof value==='object'&&Object.prototype.hasOwnProperty.call(value,'data');i++)value=value.data;return value}
|
||||
@ -322,12 +301,6 @@ function publishScreenCast(value,venueId){
|
||||
}catch{}
|
||||
return snapshot;
|
||||
}
|
||||
function clearPublishedScreenCast(venueId){
|
||||
try{
|
||||
localStorage.removeItem(screenCastCacheKey(venueId));localStorage.removeItem(screenCastCacheKey(null));
|
||||
const channel=new BroadcastChannel(scoreScreenChannelName.value);channel.postMessage(null);channel.close();
|
||||
}catch{}
|
||||
}
|
||||
function buildSingleCastSnapshot(response,row,rowSession){
|
||||
const result=normalizeCastValue(unwrapApiData(response)||{}),session=rowSession||row?._session||selectedSession.value||{};
|
||||
return {...result,castType:1,displayKind:castEntityKind.value,eventName:eventName.value,projectName:session.projectName||session.eventProjectName||'',groupName:session.groupName||session.eventProjectGroupName||row.groupName||'',venueName:session.venueName||session.venueNo||'',sessionNo:session.sessionNo||'',scoreRuleCode:session.scoreRuleCode||'',payloadJson:result.payloadJson||JSON.stringify({...row,...row.result})};
|
||||
@ -351,6 +324,8 @@ async function loadSessions(){
|
||||
const res=await workbenchSessionsGet({eventId:eventId.value});
|
||||
const sessionRows=validResponse(res);
|
||||
if(!Array.isArray(sessionRows))throw new Error('比赛场次数据格式错误');
|
||||
// 以接口返回的赛事名称为准,避免 URL 只带 eventId 时沿用上一个赛事的名称。
|
||||
if(sessionRows[0]?.eventName){eventName.value=sessionRows[0].eventName;sessionStorage.setItem('scoring-context',JSON.stringify({...savedContext,eventId:eventId.value,eventName:eventName.value,mode:'freestyle'}))}
|
||||
// 刷新全部场次时保留当前场次;场次消失才回退到列表首项。
|
||||
const previousSessionId=selectedSession.value?.sessionId,previousProjectId=selectedProjectId.value;
|
||||
sessions.value=sessionRows;
|
||||
@ -385,7 +360,6 @@ function jumpToPage(){const target=Number(jumpPage.value);if(!Number.isInteger(t
|
||||
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],eventProjectId=selectedSession.value?.eventProjectId;
|
||||
@ -418,27 +392,10 @@ async function loadEntries(silent=false,{background=false}={}){
|
||||
return false;
|
||||
}finally{if(requestId===entriesRequestId){entriesController=null;loadingEntries.value=false}}
|
||||
}
|
||||
async function loadCurrentCast({background=false}={}){
|
||||
if(!eventId.value)return false;
|
||||
if(background&&castController)return false;
|
||||
if(!background)cancelCastRequest();
|
||||
const requestId=++castRequestId,controller=new AbortController();
|
||||
castController=controller;
|
||||
try{
|
||||
const res=await workbenchScreenCastCurrentGet({eventId:eventId.value,venueId:selectedSession.value?.venueId},{signal:controller.signal,silentError:background,timeout:background?15000:50000});
|
||||
const cast=validResponse(res);
|
||||
if(requestId!==castRequestId||controller.signal.aborted)return false;
|
||||
currentCast.value=cast||null;castSyncError.value=false;castFailureCount=0;castNextRetryAt=0;
|
||||
return true;
|
||||
}catch(error){
|
||||
if(requestId===castRequestId&&!destroyed){castSyncError.value=true;castFailureCount++;castNextRetryAt=Date.now()+retryDelay(castFailureCount)}
|
||||
return false;
|
||||
}finally{if(requestId===castRequestId)castController=null}
|
||||
}
|
||||
async function retrySync(){
|
||||
if(retryLoading.value)return;
|
||||
retryLoading.value=true;
|
||||
try{await Promise.all([entriesSyncError.value?(sessions.value.length?loadEntries(true):loadSessions()):Promise.resolve(),castSyncError.value?loadCurrentCast():Promise.resolve()])}
|
||||
try{if(entriesSyncError.value)await (sessions.value.length?loadEntries(true):loadSessions())}
|
||||
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}}
|
||||
@ -539,11 +496,10 @@ async function showDetail(row){
|
||||
}
|
||||
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);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 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}成绩已投出`)}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 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 loadEntries(true)}catch(error){closeUnusedScreen(presentation);throw error}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}}
|
||||
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}}
|
||||
@ -661,17 +617,15 @@ function showScoreScreen(presentation,venueId,displayKind=castEntityKind.value){
|
||||
function closeUnusedScreen(presentation){if(presentation?.created&&presentation.popup&&!presentation.popup.closed)presentation.popup.close()}
|
||||
function openScoreScreen(){rememberCastKind();const presentation=prepareScoreScreen(selectedSession.value?.venueId);if(presentation)showScoreScreen(presentation,selectedSession.value?.venueId,castEntityKind.value)}
|
||||
function openCheckinScreen(){const venueId=selectedSession.value?.venueId;const eventProjectId=selectedSession.value?.eventProjectId;const presentation=prepareScoreScreen();if(presentation)showPresentationScreen(presentation,scoreScreenHref(venueId,castEntityKind.value,'checkin',eventProjectId))}
|
||||
function goLauncher(){router.push({path:'/scoring/launcher'})}
|
||||
function toggleCorrections(){activeTab.value=activeTab.value==='corrections'?'results':'corrections';if(activeTab.value==='corrections'){cancelEntriesRequest();void loadCorrections()}else pollNow(true)}
|
||||
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'&&selectedProjectSessions.value.length&&(force||now>=entriesNextRetryAt)&&!entriesController)void loadEntries(true,{background:true});
|
||||
}
|
||||
function stopPolling(){
|
||||
if(pollTimer){window.clearInterval(pollTimer);pollTimer=null}
|
||||
cancelEntriesRequest();cancelCastRequest();
|
||||
cancelEntriesRequest();
|
||||
}
|
||||
function startPolling(refreshNow=false){
|
||||
if(pollTimer||!componentActive||document.visibilityState!=='visible')return;
|
||||
@ -682,7 +636,6 @@ function onVisibilityChange(){
|
||||
if(document.visibilityState==='visible')startPolling(true);
|
||||
else stopPolling();
|
||||
}
|
||||
watch(()=>selectedSession.value?.venueId,()=>{if(componentActive&&document.visibilityState==='visible')void loadCurrentCast()});
|
||||
watch(castEntityKind,rememberCastKind);
|
||||
watch(()=>route.path,(path)=>{activeTab.value=path==='/scoring/cast'?'screen':'results'});
|
||||
onMounted(async ()=>{
|
||||
@ -695,7 +648,6 @@ onMounted(async ()=>{
|
||||
await loadSessions();
|
||||
mountedReady=true;
|
||||
if(!componentActive||document.visibilityState!=='visible')return;
|
||||
void loadCurrentCast();
|
||||
startPolling();
|
||||
});
|
||||
onActivated(()=>{componentActive=true;scoreTheme.value=readScoreScreenTheme(eventId.value);if(mountedReady)startPolling(true)});
|
||||
@ -857,7 +809,16 @@ onBeforeUnmount(()=>{destroyed=true;componentActive=false;stopPolling();themeCha
|
||||
.session-panel { width: 292px; padding: 18px 16px; }
|
||||
.session-head { margin-bottom: 14px; }
|
||||
.session-head b { color: var(--wb-text); font-size: 15px; }
|
||||
.session-list { margin-top: 14px; padding-right: 3px; }
|
||||
.session-list { flex: 1; margin-top: 14px; padding-right: 3px; }
|
||||
.session-utilities { flex: none; display: grid; gap: 10px; padding-top: 14px; border-top: 1px solid #e8eef5; }
|
||||
.sync-line { display: flex; align-items: center; justify-content: space-between; min-height: 24px; }
|
||||
.sync-line .live-dot { font-weight: 600; }
|
||||
.utility-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.utility-buttons :deep(.el-button) { width: 100%; margin: 0; border-radius: 8px; }
|
||||
.current-event { display: grid; gap: 5px; min-width: 0; margin-bottom: 16px; padding: 12px 13px; border: 1px solid #dbe9fa; border-radius: 10px; background: linear-gradient(120deg, #f1f7ff, #f8fbff); }
|
||||
.current-event span { color: #607c9d; font-size: 11px; }
|
||||
.current-event strong { overflow: hidden; color: #173b66; font-size: 15px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.correction-event { margin-bottom: 14px; }
|
||||
.session-list > .session-switch {
|
||||
grid-template-columns: 34px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user