ltx 77180eaeb8 fix(scoring): 明确成绩投屏评分卡的得分与系数标签
成绩投屏的系数赛制评分卡改为展示“难度得分”“表现系数”“规定系数”“扣分系数 M”,让标签与评分含义一致。沿用赛制判断,规定赛继续显示“创意编排分”“完成质量分”和实际扣分,不误标为系数;交互绳等赛制的评分项隐藏规则保持不变。

仅修改前端成绩大屏页面,不涉及后端接口或计分计算。验证:npm run build:local 通过,git diff --check 通过。业务契约脚本因硬编码的后端路径在本机不存在而未能执行。
2026-10-19 15:50:18 +08:00

398 lines
40 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div :class="['score-screen', `view-${viewType}`, `entity-${displayKind}`, { embedded: !isBigScreen, offline }]">
<div class="design-stage" :style="{ '--screen-scale': screenScale }">
<div class="ambient ambient-one"></div><div class="ambient ambient-two"></div><div class="aurora-strip"></div><div class="screen-grid"></div><div class="horizon"></div><div class="frame-corners"><i></i><i></i><i></i><i></i></div>
<Transition name="cast" mode="out-in">
<main v-if="effectiveCast" :key="castKey" class="screen-content">
<header class="screen-header"><div class="title-wing left"><i></i><i></i><i></i></div><div class="event-title"><span>LIVE RESULTS</span><h1>{{ title }}</h1></div><div class="title-wing right"><i></i><i></i><i></i></div></header>
<section class="context-bar"><div><span class="context-icon"><el-icon><User /></el-icon></span><small>项目 / 组别</small><b>{{ projectGroup }}</b></div><i></i><div><span class="context-icon"><el-icon><Location /></el-icon></span><small>场次 / 场地</small><b>{{ sessionVenue }}</b></div></section>
<section v-if="viewType === 'score'" class="score-layout">
<div class="hero-card"><div class="panel-shine"></div><div class="entity-chip"><el-icon><component :is="displayKind === 'team' ? UserFilled : User" /></el-icon><span>{{ displayKind === 'team' ? 'TEAM' : 'PLAYER' }}</span>{{ displayKindLabel }}成绩</div><div class="entity-name">{{ singleName }}</div><div v-if="singleAffiliation" class="entity-affiliation">{{ singleAffiliation }}</div><div class="hero-meta"><span v-if="payload.entryNo">出场号 <b>{{ payload.entryNo }}</b></span><span v-if="payload.rank">当前排名 <b>第 {{ payload.rank }} 名</b></span></div><div class="hero-score"><strong class="score-ticker">{{ animatedValue('hero', payload.totalScore ?? payload.score) }}</strong><small>分</small></div><div class="score-beam"><i></i></div><div v-if="payload.isDisqualified" class="dq-stamp">DQ</div></div>
<div class="score-metrics" :style="{ gridTemplateColumns: `repeat(${Math.max(scoreCards.length, 1)}, 1fr)` }"><article v-for="(item,index) in scoreCards" :key="item.key" :class="['metric-card', item.key]" :style="{ '--delay': `${index * 90}ms` }"><div class="metric-head"><span class="metric-icon"><el-icon><component :is="scoreIconComponents[item.key]" /></el-icon></span><small>{{ item.en }}</small></div><strong class="score-ticker">{{ item.field === 'deductionFactor' ? Number(payload[item.field]).toFixed(3) : animatedValue(`metric:${item.key}`, payload[item.field]) }}</strong><div class="metric-line"></div><b>{{ item.label }}</b></article></div>
</section>
<section v-else class="ranking-layout">
<div class="ranking-heading"><div><span><i></i>{{ displayKind === 'team' ? 'TEAM RANKING' : 'INDIVIDUAL RANKING' }}</span><h2>{{ displayKindLabel }}排名</h2></div><div class="ranking-status"><small>共 {{ rankingTotal }} 个{{ displayKind === 'team' ? '队伍' : '参赛者' }}<template v-if="rankingPageCount > 1"> · 第 {{ rankingPageIndex + 1 }}/{{ rankingPageCount }} 页</template></small><div v-if="rankingPageCount > 1" class="page-dots"><i v-for="page in rankingPageCount" :key="page" :class="{ active: page - 1 === rankingPageIndex }"></i></div></div></div>
<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><small>分</small></div></article></div>
<div v-else class="ranking-empty">当前组别暂无可展示排名</div>
</section>
<footer class="screen-footer"><span></span><b>跃动赛事 · 实时成绩系统</b><span></span></footer>
</main>
<section v-else key="waiting" class="waiting"><div class="waiting-orbit"><i></i><i></i><i></i><span>YD</span></div><small>LIVE RESULTS SYSTEM</small><h1>等待后台投屏</h1><p>请在赛事控制台选择组别,并投出个人、团队成绩或排名</p></section>
</Transition>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { DocumentChecked, Flag, Location, Medal, Setting, Trophy, User, UserFilled } from '@element-plus/icons-vue';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { workbenchScreenCastCurrentGet } from '/@/api/match/workbench';
import { isFactorRule, isWireRule } from '../score-format.mjs';
const route=useRoute(),storesTagsViewRoutes=useTagsViewRoutes();
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0;
const venueId=route.query.venueId?Number(route.query.venueId):(Number(savedContext.venueId)||undefined);
const cast=ref(null),offline=ref(false),rankPage=ref(0),viewport=ref({width:window.innerWidth,height:window.innerHeight}),animatedNumbers=ref({});
let pollTimer,rankTimer,castChannel,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}
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}
const effectiveCast=computed(()=>cast.value);
const payload=computed(()=>{try{const raw=effectiveCast.value?.payloadJson;if(!raw)return {};return normalizePayload(typeof raw==='string'?JSON.parse(raw):raw)}catch{return {}}});
const viewType=computed(()=>effectiveCast.value?.castType===2?'ranking':'score');
const displayKind=computed(()=>{const configured=String(effectiveCast.value?.displayKind||route.query.displayKind||'').toLowerCase();if(configured==='team'||configured==='individual')return configured;const sample=Array.isArray(payload.value)?payload.value[0]:payload.value;return sample?.teamName&&!sample?.participantName?'team':'individual'});
const displayKindLabel=computed(()=>displayKind.value==='team'?'团队':'个人');
const castKey=computed(()=>`${effectiveCast.value?.screenCastId||effectiveCast.value?.castTime||'cast'}-${viewType.value}-${displayKind.value}`);
const title=computed(()=>effectiveCast.value?.eventName||String(route.query.eventName||'赛事成绩大屏'));
const projectGroup=computed(()=>[effectiveCast.value?.projectName,effectiveCast.value?.groupName||(!Array.isArray(payload.value)&&payload.value.groupName)].filter(Boolean).join(' · ')||'未设置');
function formatScreenSession(sessionNo){if(!sessionNo)return '';const tail=String(sessionNo).match(/-(\d+)$/);if(tail&&Number(tail[1])>0)return `第${Number(tail[1])}场`;if(/^\d+$/.test(String(sessionNo)))return `第${Number(sessionNo)}场`;return String(sessionNo)}
const sessionVenue=computed(()=>[formatScreenSession(effectiveCast.value?.sessionNo),effectiveCast.value?.venueName].filter(Boolean).join(' · ')||'全场地');
const singleName=computed(()=>displayKind.value==='team'?(payload.value.teamName||payload.value.participantName||'未命名队伍'):(payload.value.participantName||payload.value.teamName||'未命名选手'));
const singleAffiliation=computed(()=>displayKind.value==='team'?(payload.value.unitName||''):(payload.value.teamName&&payload.value.teamName!==singleName.value?payload.value.teamName:(payload.value.unitName||'')));
const scoreIconComponents={difficulty:Setting,performance:Medal,required:DocumentChecked,deduction:Flag};
const scoreCards=computed(()=>{
const scoreRuleCode=effectiveCast.value?.scoreRuleCode;
const prescribed=String(scoreRuleCode||'').toUpperCase().includes('PRESCRIBED');
const factor=isFactorRule(scoreRuleCode);
const wire=isWireRule(scoreRuleCode);
return [{key:'difficulty',label:'难度得分',en:'DIFFICULTY',field:'difficultyScore'},{key:'performance',label:prescribed?'创意编排分':factor?'表现系数':'表现',en:'PERFORMANCE',field:'performanceScore'},{key:'required',label:prescribed?'完成质量分':factor?'规定系数':'规定',en:'REQUIRED',field:'requiredScore'},{key:'deduction',label:factor?'扣分系数 M':'扣分',en:'DEDUCTION',field:payload.value.deductionFactor!=null?'deductionFactor':'deductionScore'}]
.filter(item=>(item.key!=='difficulty'||!prescribed)&&(item.key!=='required'||!wire));
});
const rankingAll=computed(()=>Array.isArray(payload.value)?payload.value:[]),rankingTotal=computed(()=>rankingAll.value.length),rankingPageCount=computed(()=>Math.max(1,Math.ceil(rankingTotal.value/6))),rankingPageIndex=computed(()=>rankPage.value%rankingPageCount.value),rankingRows=computed(()=>rankingAll.value.slice(rankingPageIndex.value*6,rankingPageIndex.value*6+6)),maxRankScore=computed(()=>Math.max(...rankingAll.value.map(row=>Number(row.totalScore??row.score)||0),1));
function displayName(row){return displayKind.value==='team'?(row.teamName||row.participantName||'未命名队伍'):(row.participantName||row.teamName||'未命名选手')}
function displaySubName(row){if(displayKind.value==='team')return row.unitName||'';return row.teamName&&row.teamName!==displayName(row)?row.teamName:(row.unitName||'')}
function rankNumber(row,index){const rank=Number(row.rank);return Number.isFinite(rank)&&rank>0?rank:rankingPageIndex.value*6+index+1}
function rankWidth(row){return `${Math.max(12,Math.round((Number(row.totalScore??row.score)||0)/maxRankScore.value*100))}%`}
function value(input){if(input===undefined||input===null||input==='')return '0';const n=Number(input);return Number.isFinite(n)?n.toFixed(2):input}
function rankAnimationKey(row,index){return `rank:${row.entryNo||displayName(row)||rankingPageIndex.value*6+index}`}
function animatedValue(key,fallback){return Object.prototype.hasOwnProperty.call(animatedNumbers.value,key)?value(animatedNumbers.value[key]):value(fallback)}
function animateScoreNumbers(){
window.cancelAnimationFrame(numberAnimationFrame);
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];
}else{
rankingRows.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);
animatedNumbers.value={...staticTargets,...Object.fromEntries(Object.entries(numericTargets).map(([key,target])=>[key,target*eased]))};
if(progress<1)numberAnimationFrame=window.requestAnimationFrame(tick);
};
numberAnimationFrame=window.requestAnimationFrame(tick);
}
const isBigScreen=computed(()=>storesTagsViewRoutes.isTagsViewCurrenFull);
const screenScale=computed(()=>Math.min(viewport.value.width/1920,viewport.value.height/1080));
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}}
watch([castKey,rankingPageIndex],()=>nextTick(animateScoreNumbers),{immediate:true,flush:'post'});
onMounted(()=>{storesTagsViewRoutes.setCurrenFullscreen(true);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);window.addEventListener('resize',onViewportResize);onViewportResize()});
onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTimer);window.cancelAnimationFrame(numberAnimationFrame);castChannel?.close();window.removeEventListener('resize',onViewportResize)});
</script>
<style scoped lang="scss">
.score-screen{--cyan:#54ecff;--navy:#061847;position:relative;width:100vw;height:100vh;min-width:0;min-height:0;overflow:hidden;color:#fff;background:radial-gradient(circle at 50% -18%,#175dcc 0,rgba(11,52,143,.92) 35%,#061847 73%,#030b29 100%);font-family:"Microsoft YaHei","PingFang SC",sans-serif}.score-screen.view-ranking{background:radial-gradient(circle at 50% -20%,#174ba9 0,rgba(8,38,112,.96) 40%,#030b2a 100%)}.ambient{position:absolute;border-radius:50%;filter:blur(80px);opacity:.25}.ambient-one{left:-12vw;top:10vh;width:42vw;height:42vw;background:#008cff}.ambient-two{right:-14vw;bottom:-20vh;width:42vw;height:42vw;background:#0963e8}.screen-grid{position:absolute;inset:0;opacity:.14;background-image:linear-gradient(rgba(75,192,255,.18) 1px,transparent 1px),linear-gradient(90deg,rgba(75,192,255,.18) 1px,transparent 1px);background-size:5vw 5vw;mask-image:linear-gradient(to bottom,transparent 4%,#000 48%,transparent 100%);transform:perspective(600px) rotateX(55deg) scale(1.6);transform-origin:center bottom}.horizon{position:absolute;left:4%;right:4%;bottom:-12%;height:42%;border-top:1px solid rgba(91,216,255,.38);border-radius:50%;box-shadow:0 -12px 90px rgba(30,150,255,.28)}
.screen-content{position:relative;z-index:2;display:flex;flex-direction:column;width:100%;height:100%;box-sizing:border-box;padding:clamp(18px,2.3vh,42px) clamp(24px,3.1vw,82px) clamp(12px,1.8vh,32px)}.screen-header{display:flex;align-items:center;justify-content:center;gap:clamp(20px,2.3vw,60px);flex:none}.event-title{text-align:center}.event-title span{display:block;margin-bottom:.25vh;color:var(--cyan);font-size:clamp(9px,.65vw,17px);font-weight:700;letter-spacing:.42em}.event-title h1{max-width:70vw;margin:0;font-size:clamp(30px,3.1vw,80px);line-height:1.16;letter-spacing:.02em;text-shadow:0 8px 28px rgba(0,4,34,.5)}.title-wing{display:flex;align-items:center;gap:5px}.title-wing i{display:block;width:clamp(20px,2vw,54px);height:4px;clip-path:polygon(0 35%,100% 0,82% 100%,0 72%);background:linear-gradient(90deg,transparent,var(--cyan))}.title-wing i:nth-child(2){width:clamp(12px,1.1vw,30px);opacity:.72}.title-wing i:nth-child(3){width:clamp(6px,.55vw,15px);opacity:.45}.title-wing.right{transform:scaleX(-1)}
.context-bar{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;width:min(76%,1900px);min-height:clamp(40px,5.2vh,86px);box-sizing:border-box;margin:clamp(12px,1.6vh,28px) auto clamp(13px,1.8vh,32px);padding:0 clamp(16px,1.6vw,44px);border:1px solid rgba(87,195,255,.3);border-radius:999px;background:linear-gradient(90deg,rgba(4,25,77,.18),rgba(15,82,168,.38),rgba(4,25,77,.18));box-shadow:inset 0 1px rgba(255,255,255,.05)}.context-bar>div{display:grid;grid-template-columns:auto 1fr;column-gap:clamp(7px,.7vw,18px);align-items:center}.context-bar>div:last-child{justify-content:end}.context-icon{grid-row:1/3;color:var(--cyan);font-size:clamp(18px,1.25vw,34px)}.context-bar small{color:#81b9e2;font-size:clamp(9px,.65vw,17px);letter-spacing:.08em}.context-bar b{font-size:clamp(13px,1vw,27px);font-weight:600}.context-bar>i{width:1px;height:56%;margin:0 clamp(22px,3vw,80px);background:linear-gradient(transparent,rgba(91,210,255,.5),transparent)}
.score-layout{display:grid;grid-template-rows:minmax(0,1fr) clamp(120px,19vh,320px);gap:clamp(12px,1.7vh,30px);min-height:0;flex:1}.hero-card{position:relative;min-height:0;overflow:hidden;border:1px solid rgba(86,197,255,.52);border-radius:clamp(17px,1.45vw,38px);background:radial-gradient(circle at 50% 45%,rgba(24,111,233,.62),transparent 48%),linear-gradient(135deg,rgba(14,77,182,.74),rgba(4,24,90,.82));box-shadow:inset 0 1px rgba(255,255,255,.08),inset 0 -70px 120px rgba(0,10,57,.22),0 24px 70px rgba(0,5,40,.28)}.panel-shine{position:absolute;inset:0;background:linear-gradient(115deg,rgba(255,255,255,.07),transparent 24% 76%,rgba(38,194,255,.05))}.entity-chip{position:absolute;left:50%;top:clamp(10px,1.5vh,28px);transform:translateX(-50%);min-width:clamp(150px,16vw,430px);padding:clamp(7px,.75vh,14px) clamp(24px,2vw,52px);border:1px solid rgba(91,221,255,.45);border-radius:999px;color:#b9f4ff;background:rgba(6,46,133,.78);font-size:clamp(14px,1.2vw,31px);font-weight:700;text-align:center;letter-spacing:.12em}.entity-chip span{margin-right:.8em;color:var(--cyan);font-size:.62em;letter-spacing:.22em}.entity-name{position:absolute;left:6%;right:6%;top:22%;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;font-size:clamp(30px,3vw,78px);font-weight:800;letter-spacing:.08em;text-shadow:0 7px 24px rgba(0,4,45,.46)}.entity-affiliation{position:absolute;left:8%;right:8%;top:35%;color:#aad8f4;text-align:center;font-size:clamp(13px,1vw,26px);letter-spacing:.1em}.hero-score{position:absolute;left:0;right:0;top:39%;display:flex;align-items:flex-end;justify-content:center}.hero-score strong{font-size:clamp(104px,12.2vw,315px);font-weight:900;line-height:1;font-variant-numeric:tabular-nums;letter-spacing:-.055em;text-shadow:0 18px 42px rgba(0,5,46,.55),0 0 28px rgba(84,236,255,.16)}.hero-score small{margin:0 0 1.5em .7em;color:#b8dff5;font-size:clamp(14px,1vw,28px)}.score-beam{position:absolute;left:25%;right:25%;bottom:11%;height:2px;background:#d6fdff;box-shadow:0 0 9px 3px rgba(75,226,255,.9),0 0 42px 15px rgba(24,126,255,.72)}.score-beam i{position:absolute;left:50%;top:50%;width:18px;height:18px;transform:translate(-50%,-50%) rotate(45deg);border:2px solid #d8fdff;background:#35d9ff;box-shadow:0 0 18px #2bdfff}.dq-stamp{position:absolute;right:8%;top:28%;transform:rotate(-10deg);padding:.15em .35em;border:5px solid #ff7c8e;border-radius:12px;color:#ff9aa7;font-size:clamp(50px,6vw,150px);font-weight:900}
.score-metrics{display:grid;gap:clamp(8px,1.35vw,36px)}.metric-card{position:relative;display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr auto auto;align-items:center;column-gap:clamp(9px,1vw,26px);overflow:hidden;padding:clamp(11px,1.25vh,23px) clamp(12px,1.55vw,40px) clamp(9px,1vh,19px);border:1px solid rgba(255,255,255,.28);border-radius:clamp(13px,1.2vw,32px);background:linear-gradient(145deg,#36ac7a,#146e6b);box-shadow:inset 0 1px rgba(255,255,255,.18),0 14px 36px rgba(0,8,48,.24);animation:metric-in .55s both;animation-delay:var(--delay)}.metric-card::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 13% 0,rgba(255,255,255,.23),transparent 42%)}.metric-card.performance{background:linear-gradient(145deg,#1eaaa9,#116087)}.metric-card.required{background:linear-gradient(145deg,#e9a32b,#a9561d)}.metric-card.deduction{background:linear-gradient(145deg,#cc3e81,#592588)}.metric-head{position:relative;z-index:1;display:flex;align-items:center;gap:.6vw}.metric-icon{display:grid;place-items:center;width:clamp(38px,3.9vw,104px);height:clamp(38px,3.9vw,104px);border:3px solid rgba(255,255,255,.24);border-radius:50%;font-size:clamp(20px,1.7vw,44px);box-shadow:inset 0 0 0 1px rgba(255,255,255,.5),0 0 20px rgba(111,255,232,.24)}.metric-head small{display:none}.metric-card>strong{position:relative;z-index:1;font-size:clamp(35px,4.25vw,110px);font-variant-numeric:tabular-nums}.metric-line{position:relative;z-index:1;grid-column:1/-1;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.46),transparent)}.metric-card>b{position:relative;z-index:1;grid-column:1/-1;padding-top:.55vh;text-align:center;font-size:clamp(14px,1.4vw,36px);letter-spacing:.28em}
.ranking-layout{display:flex;flex-direction:column;min-height:0;flex:1;padding:clamp(18px,2.2vh,38px) clamp(18px,2vw,52px);border:1px solid rgba(85,193,255,.38);border-radius:clamp(17px,1.5vw,40px);background:linear-gradient(145deg,rgba(8,54,151,.72),rgba(3,22,82,.82));box-shadow:inset 0 1px rgba(255,255,255,.07),0 22px 65px rgba(0,4,36,.3)}.ranking-heading{display:flex;align-items:end;justify-content:space-between;flex:none;margin-bottom:clamp(12px,1.8vh,30px);padding:0 clamp(5px,.5vw,14px)}.ranking-heading span{color:var(--cyan);font-size:clamp(9px,.65vw,17px);font-weight:700;letter-spacing:.3em}.ranking-heading h2{margin:.2vh 0 0;font-size:clamp(23px,2vw,52px);letter-spacing:.08em}.ranking-heading>small{color:#8fbddb;font-size:clamp(11px,.8vw,20px)}.ranking-list{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,minmax(0,1fr));gap:clamp(8px,1.1vh,20px) clamp(14px,1.5vw,38px);min-height:0;flex:1}.rank-row{--rank-color:#28bdf3;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:clamp(10px,1vw,26px);min-height:0;padding:clamp(8px,.8vh,15px) clamp(11px,1.05vw,27px);border:1px solid rgba(75,164,235,.28);border-radius:clamp(12px,1vw,26px);background:linear-gradient(90deg,rgba(10,58,151,.72),rgba(5,31,101,.46));animation:rank-in .55s both;animation-delay:var(--delay)}.rank-row.podium{border-color:rgba(84,220,255,.5);box-shadow:inset 4px 0 var(--rank-color),0 8px 26px rgba(0,7,48,.2)}.rank-row:has(.rank-1){--rank-color:#ffd65c}.rank-row:has(.rank-2){--rank-color:#bde4ff}.rank-row:has(.rank-3){--rank-color:#f3a769}.rank-badge{display:grid;place-items:center;width:clamp(42px,3.5vw,90px);height:clamp(42px,3.5vw,90px);border:1px solid rgba(80,207,255,.62);border-radius:50%;color:#d5f7ff;background:rgba(5,39,119,.78);font-size:clamp(18px,1.5vw,38px);font-weight:800}.rank-badge.rank-1,.rank-badge.rank-2,.rank-badge.rank-3{color:#071743;border-color:rgba(255,255,255,.4);background:linear-gradient(145deg,var(--rank-color),#9d6b18);box-shadow:0 0 24px rgba(255,215,92,.25)}.rank-person{min-width:0}.rank-title{display:flex;align-items:center;gap:.8vw;margin-bottom:clamp(5px,.6vh,11px)}.rank-title strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:clamp(14px,1.1vw,28px)}.rank-title small{overflow:hidden;color:#82b6dc;font-size:clamp(9px,.7vw,17px);text-overflow:ellipsis;white-space:nowrap}.rank-track{height:clamp(7px,.75vh,14px);overflow:hidden;border-radius:999px;background:rgba(2,15,61,.72);box-shadow:inset 0 2px 6px rgba(0,0,0,.28)}.rank-track i{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,#0879ff,var(--rank-color));box-shadow:0 0 14px rgba(40,189,243,.4);transition:width .8s cubic-bezier(.2,.8,.2,1)}.rank-value{display:flex;align-items:baseline;justify-content:end;width:clamp(70px,5.4vw,145px)}.rank-value strong{font-size:clamp(25px,2.25vw,58px);font-variant-numeric:tabular-nums;letter-spacing:-.04em}.rank-value small{margin-left:.25em;color:#8ebbd8;font-size:clamp(9px,.65vw,17px)}.ranking-empty{display:grid;place-items:center;flex:1;color:#8ebbd8;font-size:clamp(17px,1.4vw,36px)}
.screen-footer{display:flex;align-items:center;justify-content:center;gap:clamp(10px,1.2vw,28px);height:clamp(20px,2.8vh,46px);flex:none;color:#609ac6;font-size:clamp(9px,.6vw,16px);letter-spacing:.16em}.screen-footer span{width:clamp(40px,5vw,130px);height:1px;background:linear-gradient(90deg,transparent,#277ebc)}.screen-footer span:last-child{transform:scaleX(-1)}
.waiting{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;text-align:center}.waiting-orbit{position:relative;display:grid;place-items:center;width:clamp(100px,9vw,220px);height:clamp(100px,9vw,220px);margin-bottom:clamp(26px,3vh,50px);border:1px solid rgba(86,222,255,.26);border-radius:50%;box-shadow:0 0 70px rgba(32,154,255,.2)}.waiting-orbit::before,.waiting-orbit::after{content:"";position:absolute;border:1px solid rgba(83,222,255,.18);border-radius:50%;animation:orbit 6s linear infinite}.waiting-orbit::before{inset:-18%}.waiting-orbit::after{inset:18%;animation-direction:reverse;animation-duration:4s}.waiting-orbit span{color:#d7fbff;font-size:clamp(28px,2.4vw,58px);font-weight:900;letter-spacing:.08em}.waiting-orbit i{position:absolute;width:9px;height:9px;border-radius:50%;background:var(--cyan);box-shadow:0 0 14px var(--cyan)}.waiting-orbit i:first-child{top:-5px}.waiting-orbit i:nth-child(2){right:2%;bottom:19%}.waiting-orbit i:nth-child(3){left:8%;bottom:8%}.waiting>small{color:var(--cyan);font-size:clamp(10px,.75vw,18px);font-weight:700;letter-spacing:.42em}.waiting h1{margin:clamp(11px,1.4vh,24px) 0 clamp(7px,.8vh,14px);font-size:clamp(34px,3.2vw,82px);letter-spacing:.1em}.waiting p{margin:0;color:#78aace;font-size:clamp(13px,1vw,25px)}
.cast-enter-active,.cast-leave-active{transition:opacity .34s ease,transform .42s cubic-bezier(.2,.8,.2,1);transform-origin:center}.cast-enter-from{opacity:0;transform:scale(.975) translateY(12px)}.cast-leave-to{opacity:0;transform:scale(1.015)}@keyframes metric-in{from{opacity:0;transform:translateY(18px) scale(.97)}to{opacity:1;transform:none}}@keyframes rank-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:none}}@keyframes orbit{to{transform:rotate(360deg)}}
@media(max-width:1100px){.context-bar{width:92%}.ranking-list{grid-template-columns:1fr}.rank-row:nth-child(n+4){display:none}.event-title h1{max-width:78vw}.title-wing{display:none}}@media(max-height:720px){.screen-content{padding-top:10px;padding-bottom:7px}.context-bar{margin-top:7px;margin-bottom:8px}.score-layout{grid-template-rows:minmax(0,1fr) 126px;gap:9px}.entity-chip{top:7px}.entity-name{top:20%}.entity-affiliation{top:33%}.hero-score{top:37%}.metric-card{padding-top:8px;padding-bottom:6px}.screen-footer{height:18px}.ranking-layout{padding-top:12px;padding-bottom:12px}.ranking-heading{margin-bottom:8px}}
/* Pixso 大屏画板:以 1920 × 1080 为唯一设计基准,外层只做等比例缩放。 */
.score-screen {
display: grid;
place-items: center;
background: #020829;
}
.design-stage {
position: absolute;
left: 50%;
top: 50%;
width: 1920px;
height: 1080px;
overflow: hidden;
transform: translate(-50%, -50%) scale(var(--screen-scale));
transform-origin: center;
background:
radial-gradient(circle at 50% 36%, rgba(18, 94, 224, 0.7), transparent 44%),
linear-gradient(145deg, #0a47c8 0%, #082b89 42%, #061f70 72%, #04164f 100%);
}
.score-screen.view-ranking .design-stage {
background:
radial-gradient(circle at 50% 24%, rgba(20, 99, 226, 0.58), transparent 43%),
linear-gradient(145deg, #0a43bd, #072878 56%, #04164e);
}
.design-stage .ambient { filter: blur(110px); }
.design-stage .ambient-one { left: -180px; top: 120px; width: 720px; height: 720px; }
.design-stage .ambient-two { right: -180px; bottom: -250px; width: 760px; height: 760px; }
.design-stage .screen-grid { background-size: 96px 96px; }
.design-stage .horizon { left: 70px; right: 70px; bottom: -150px; height: 430px; }
.screen-content {
width: 1920px;
height: 1080px;
padding: 28px 50px 20px;
}
.screen-header { height: 74px; gap: 42px; }
.event-title span { margin-bottom: 2px; font-size: 11px; letter-spacing: 0.46em; }
.event-title h1 { max-width: 1260px; font-size: 42px; line-height: 1.2; }
.title-wing { display: flex; }
.title-wing i { width: 54px; height: 4px; }
.title-wing i:nth-child(2) { width: 30px; }
.title-wing i:nth-child(3) { width: 15px; }
.context-bar {
width: 100%;
height: 78px;
min-height: 78px;
margin: 10px 0 18px;
padding: 0 42px;
border-radius: 14px;
background: linear-gradient(90deg, rgba(2, 33, 112, 0.5), rgba(14, 83, 184, 0.62), rgba(2, 33, 112, 0.5));
}
.context-bar > div { column-gap: 13px; }
.context-icon { font-size: 24px; }
.context-bar small { font-size: 12px; }
.context-bar b { font-size: 19px; }
.context-bar > i { height: 46px; margin: 0 90px; }
.score-layout { grid-template-rows: minmax(0, 1fr) 218px; gap: 18px; }
.hero-card {
border-radius: 18px;
background:
radial-gradient(ellipse at 50% 49%, rgba(25, 123, 245, 0.68), transparent 46%),
linear-gradient(135deg, rgba(13, 74, 181, 0.78), rgba(3, 26, 99, 0.86));
box-shadow: inset 0 1px rgba(255, 255, 255, 0.1), inset 0 -80px 130px rgba(0, 11, 68, 0.25), 0 18px 52px rgba(0, 6, 43, 0.28);
}
.entity-chip {
top: 17px;
min-width: 310px;
padding: 10px 36px;
font-size: 20px;
}
.entity-name { top: 22%; font-size: 50px; }
.entity-affiliation { top: 35%; font-size: 18px; }
.hero-score { top: 39%; }
.hero-score strong { font-size: 168px; }
.hero-score small { margin: 0 0 25px 14px; font-size: 18px; }
.score-beam { left: 31%; right: 31%; bottom: 11%; }
.score-beam i { width: 16px; height: 16px; }
.dq-stamp { font-size: 94px; }
.score-metrics { gap: 18px; }
.metric-card {
grid-template-columns: auto 1fr;
grid-template-rows: 1fr auto auto;
column-gap: 16px;
padding: 22px 26px 16px;
border-radius: 17px;
}
.metric-icon { width: 66px; height: 66px; font-size: 30px; }
.metric-card > strong { font-size: 60px; }
.metric-line { margin-top: 8px; }
.metric-card > b { padding-top: 12px; font-size: 22px; letter-spacing: 0.25em; }
.ranking-layout {
padding: 20px 34px 22px;
border-radius: 18px;
background: linear-gradient(145deg, rgba(7, 50, 151, 0.76), rgba(3, 24, 91, 0.88));
}
.ranking-heading { margin-bottom: 15px; padding: 0 8px; }
.ranking-heading span { font-size: 11px; }
.ranking-heading h2 { margin-top: 4px; font-size: 32px; }
.ranking-heading > small { font-size: 14px; }
.ranking-list {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(6, minmax(0, 1fr));
gap: 10px;
}
.rank-row,
.rank-row:nth-child(n+4) {
display: grid;
grid-template-columns: 70px minmax(0, 1fr) 150px;
gap: 20px;
padding: 9px 24px 9px 13px;
border-radius: 11px;
background: linear-gradient(90deg, rgba(8, 77, 193, 0.78), rgba(4, 42, 129, 0.65));
}
.rank-row.podium { box-shadow: inset 4px 0 var(--rank-color), 0 6px 18px rgba(0, 7, 48, 0.18); }
.rank-badge { width: 54px; height: 54px; font-size: 23px; }
.rank-title { margin-bottom: 8px; gap: 18px; }
.rank-title strong { font-size: 22px; }
.rank-title small { font-size: 14px; }
.rank-track { height: 10px; }
.rank-value { width: 150px; }
.rank-value strong { font-size: 36px; }
.rank-value small { font-size: 13px; }
.screen-footer { height: 27px; font-size: 11px; }
.screen-footer span { width: 110px; }
.waiting { width: 1920px; height: 1080px; }
.waiting-orbit { width: 170px; height: 170px; margin-bottom: 38px; }
.waiting-orbit span { font-size: 44px; }
.waiting > small { font-size: 13px; }
.waiting h1 { margin: 20px 0 12px; font-size: 56px; }
.waiting p { font-size: 18px; }
/* 统一 SVG 图标语言与奖牌细节,避免依赖系统字体符号。 */
.title-wing { filter: drop-shadow(0 0 8px rgba(59, 225, 255, 0.55)); }
.title-wing i {
border-radius: 2px;
background: linear-gradient(90deg, transparent 2%, #1ac7ff 48%, #77f4ff 100%);
}
.context-icon {
display: grid;
grid-row: 1 / 3;
place-items: center;
width: 38px;
height: 38px;
border: 1px solid rgba(92, 225, 255, 0.45);
border-radius: 50%;
background: rgba(13, 111, 210, 0.25);
box-shadow: inset 0 0 12px rgba(79, 222, 255, 0.12), 0 0 15px rgba(40, 184, 255, 0.1);
}
.context-icon .el-icon { font-size: 21px; stroke-width: 1.4; }
.entity-chip {
display: flex;
align-items: center;
justify-content: center;
gap: 9px;
box-sizing: border-box;
box-shadow: inset 0 1px rgba(255, 255, 255, 0.08), 0 7px 22px rgba(0, 15, 70, 0.25);
}
.entity-chip .el-icon { color: #66eaff; font-size: 22px; }
.entity-chip span { margin-right: 4px; }
.metric-card {
border-color: rgba(255, 255, 255, 0.32);
box-shadow: inset 0 1px rgba(255, 255, 255, 0.2), inset 0 -45px 80px rgba(0, 16, 55, 0.12), 0 14px 32px rgba(0, 7, 45, 0.25);
}
.metric-card::after {
content: '';
position: absolute;
right: -28px;
top: -45px;
width: 145px;
height: 145px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 50%;
}
.metric-icon {
border-width: 2px;
background: rgba(255, 255, 255, 0.07);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.4), 0 0 22px rgba(109, 245, 255, 0.2);
}
.metric-icon .el-icon { font-size: 34px; stroke-width: 1.25; }
.rank-badge { position: relative; overflow: visible; isolation: isolate; }
.rank-badge > b { position: relative; z-index: 3; line-height: 1; }
.rank-badge > .el-icon {
position: absolute;
z-index: 1;
font-size: 37px;
opacity: 0.2;
}
.rank-badge.rank-1,
.rank-badge.rank-2,
.rank-badge.rank-3 { color: #13254c; text-shadow: 0 1px rgba(255, 255, 255, 0.45); }
.rank-badge.rank-1 { background: linear-gradient(145deg, #fff2a3 0%, #ffc93f 48%, #d68a12 100%); }
.rank-badge.rank-2 { background: linear-gradient(145deg, #f7fbff 0%, #b9dcf2 48%, #6f9fbd 100%); }
.rank-badge.rank-3 { background: linear-gradient(145deg, #ffd0a1 0%, #e9954f 48%, #a45128 100%); }
.medal-ribbon {
position: absolute;
z-index: -1;
top: 39px;
width: 16px;
height: 22px;
clip-path: polygon(0 0, 100% 0, 82% 100%, 50% 73%, 18% 100%);
background: #2087e2;
}
.medal-ribbon.left { left: 12px; transform: rotate(8deg); }
.medal-ribbon.right { right: 12px; transform: rotate(-8deg); }
.rank-1 .medal-ribbon { background: #f0aa1f; }
.rank-2 .medal-ribbon { background: #7faecc; }
.rank-3 .medal-ribbon { background: #bb6d3d; }
.rank-row.podium { background: linear-gradient(90deg, rgba(12, 86, 206, 0.92), rgba(5, 42, 130, 0.67)); }
.rank-row:has(.rank-1) { border-color: rgba(255, 215, 91, 0.45); }
.rank-row:has(.rank-2) { border-color: rgba(189, 228, 255, 0.42); }
.rank-row:has(.rank-3) { border-color: rgba(243, 167, 105, 0.42); }
/* 更有层次的动态舞台;所有动效只改变合成层,确保大屏持续轮播时保持流畅。 */
.aurora-strip {
position: absolute;
left: 12%;
right: 12%;
top: -145px;
height: 300px;
border-radius: 50%;
background: conic-gradient(from 90deg at 50% 100%, transparent, rgba(78, 235, 255, 0.42), rgba(40, 110, 255, 0.18), transparent 72%);
filter: blur(48px);
opacity: 0.58;
animation: aurora-breathe 8s ease-in-out infinite alternate;
}
.frame-corners { position: absolute; z-index: 1; inset: 22px; pointer-events: none; }
.frame-corners i { position: absolute; width: 54px; height: 54px; opacity: 0.52; }
.frame-corners i::before,
.frame-corners i::after { content: ''; position: absolute; background: linear-gradient(90deg, #63ebff, rgba(99, 235, 255, 0.08)); box-shadow: 0 0 10px rgba(69, 219, 255, 0.35); }
.frame-corners i::before { width: 54px; height: 2px; }
.frame-corners i::after { width: 2px; height: 54px; }
.frame-corners i:nth-child(1) { left: 0; top: 0; }
.frame-corners i:nth-child(2) { right: 0; top: 0; transform: rotate(90deg); }
.frame-corners i:nth-child(3) { right: 0; bottom: 0; transform: rotate(180deg); }
.frame-corners i:nth-child(4) { left: 0; bottom: 0; transform: rotate(270deg); }
.design-stage .ambient-one { animation: ambient-float-one 12s ease-in-out infinite alternate; }
.design-stage .ambient-two { animation: ambient-float-two 14s ease-in-out infinite alternate; }
.design-stage .screen-grid { animation: grid-flow 18s linear infinite; }
.panel-shine::after {
content: '';
position: absolute;
top: -35%;
bottom: -35%;
left: -24%;
width: 14%;
transform: rotate(15deg);
background: linear-gradient(90deg, transparent, rgba(132, 239, 255, 0.12), transparent);
animation: panel-sweep 7s ease-in-out infinite;
}
.hero-meta { position: absolute; z-index: 2; left: 28px; bottom: 20px; display: flex; gap: 10px; }
.hero-meta span { padding: 7px 12px; border: 1px solid rgba(100, 220, 255, 0.22); border-radius: 999px; color: #78b9e7; background: rgba(3, 28, 91, 0.5); font-size: 12px; backdrop-filter: blur(8px); }
.hero-meta b { margin-left: 5px; color: #d7f9ff; font-size: 13px; }
.score-ticker { display: inline-block; will-change: transform, contents; }
.hero-score strong { animation: score-arrive 0.72s cubic-bezier(0.18, 0.82, 0.3, 1) both; }
.metric-card .score-ticker { animation: number-pop 0.72s cubic-bezier(0.18, 0.9, 0.32, 1.18) both; animation-delay: calc(var(--delay) + 100ms); }
.rank-row .score-ticker { animation: number-pop 0.62s cubic-bezier(0.18, 0.9, 0.32, 1.18) both; animation-delay: var(--delay); }
.score-beam { animation: beam-breathe 2.8s ease-in-out infinite; }
.ranking-heading span { display: flex; align-items: center; gap: 9px; }
.ranking-heading span > i { width: 7px; height: 7px; border-radius: 50%; background: #49e6ff; box-shadow: 0 0 12px #49e6ff; animation: live-pulse 1.8s ease-in-out infinite; }
.ranking-status { display: flex; flex-direction: column; align-items: flex-end; gap: 9px; }
.ranking-status small { color: #8fbddb; font-size: 14px; }
.page-dots { display: flex; gap: 6px; }
.page-dots i { width: 18px; height: 3px; border-radius: 99px; background: rgba(111, 190, 238, 0.28); transition: width 0.35s ease, background 0.35s ease, box-shadow 0.35s ease; }
.page-dots i.active { width: 34px; background: #56e7ff; box-shadow: 0 0 10px rgba(86, 231, 255, 0.6); }
.rank-row { transition: border-color 0.3s ease, background 0.3s ease, transform 0.3s ease; }
@keyframes aurora-breathe { from { opacity: 0.36; transform: scaleX(0.9); } to { opacity: 0.68; transform: scaleX(1.08); } }
@keyframes ambient-float-one { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(90px, -35px, 0) scale(1.12); } }
@keyframes ambient-float-two { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(-75px, -25px, 0) scale(1.1); } }
@keyframes grid-flow { to { background-position: 96px 96px; } }
@keyframes panel-sweep { 0%, 18% { transform: translateX(0) rotate(15deg); opacity: 0; } 38% { opacity: 1; } 68%, 100% { transform: translateX(980%) rotate(15deg); opacity: 0; } }
@keyframes score-arrive { from { opacity: 0; transform: translateY(22px) scale(0.9); filter: blur(7px); } to { opacity: 1; transform: none; filter: blur(0); } }
@keyframes number-pop { 0% { opacity: 0; transform: translateY(15px) scale(0.86); } 68% { opacity: 1; transform: translateY(-4px) scale(1.045); } 100% { opacity: 1; transform: none; } }
@keyframes beam-breathe { 0%, 100% { opacity: 0.7; transform: scaleX(0.94); } 50% { opacity: 1; transform: scaleX(1.03); } }
@keyframes live-pulse { 0%, 100% { opacity: 0.58; transform: scale(0.78); } 50% { opacity: 1; transform: scale(1.18); } }
@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 { animation: none !important; }
.rank-track i, .page-dots i, .cast-enter-active, .cast-leave-active { transition: none !important; }
}
</style>