2026-09-09 18:30:09 +08:00

83 lines
14 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',{embedded:!isStandalone}]" @mousemove="showControls">
<div class="screen-lines"></div><div class="skyline"></div>
<div class="screen-controls" :class="{ visible: controlsVisible }"><button @click="toggleFullscreen">{{ fullscreenButtonText }}</button><button @click="loadCast">刷新</button><span :class="{ offline }">{{ offline ? '连接异常' : '实时数据' }}</span></div>
<main v-if="cast" class="screen-content">
<h1><i></i>{{ title }}<i></i></h1>
<div class="meta-pill"><span class="people-icon">◎</span><b>项目-组别:{{ projectGroup }}</b><span class="meta-spacer"></span><span class="pin-icon">⌖</span><b>场次-场地:{{ sessionVenue }}</b></div>
<section v-if="cast.castType === 1" class="single-board">
<div class="board-label"><i></i>当前选手<i></i></div>
<div class="player-name">{{ singleName }}</div>
<div class="score-value">{{ value(payload.totalScore ?? payload.score) }}</div>
<div class="score-glow"></div>
</section>
<section v-else class="rank-board">
<div v-for="(row,index) in rankingRows" :key="`${row.entryNo}-${index}`" class="rank-row">
<div v-if="index < 3" :class="['medal',`medal-${index+1}`]"><span>{{ index+1 }}</span></div><div v-else class="rank-no">{{ index+1 }}</div>
<div class="rank-main"><strong><span class="people-icon small">◎</span>{{ row.teamName || row.participantName || '—' }}</strong><div class="bar"><i :style="{ width: rankWidth(row) }"></i></div></div>
<div class="rank-score">{{ value(row.totalScore ?? row.score) }}<small>分</small></div>
</div>
</section>
<div v-if="cast.castType === 1" class="score-cards" :style="{gridTemplateColumns:`repeat(${Math.max(scoreCards.length,1)},1fr)`}">
<div v-for="item in scoreCards" :key="item.key" :class="['score-card',item.key]"><span class="card-icon">{{ item.icon }}</span><b>{{ value(payload[item.field]) }}</b><i></i><strong>{{ item.label }}</strong></div>
</div>
</main>
<div v-else class="waiting"><span class="waiting-ring"></span><h1>等待成绩投屏</h1><p>请在打分工作台选择成绩或组别排名</p></div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { workbenchScreenCastCurrentGet } from '/@/api/match/workbench';
const route=useRoute(), router=useRouter();
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0, venueId=route.query.venueId?Number(route.query.venueId):(Number(savedContext.venueId)||undefined);
const cast=ref(null), offline=ref(false), isFullscreen=ref(false), controlsVisible=ref(true), rankPage=ref(0);
let pollTimer, controlTimer, rankTimer;
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)]));
}
const payload=computed(()=>{try{return cast.value?.payloadJson?normalizePayload(JSON.parse(cast.value.payloadJson)): {}}catch{return {}}});
const title=computed(()=>cast.value?.eventName||String(route.query.eventName||'赛事成绩大屏'));
const projectGroup=computed(()=>[cast.value?.projectName,cast.value?.groupName||payload.value.groupName].filter(Boolean).join('-')||'—');
const sessionVenue=computed(()=>[cast.value?.sessionNo,cast.value?.venueName].filter(Boolean).join('-')||'—');
const singleName=computed(()=>payload.value.teamName||payload.value.participantName||'—');
const rankingRows=computed(()=>{const all=Array.isArray(payload.value)?payload.value:[];const pages=Math.max(1,Math.ceil(all.length/6));const page=rankPage.value%pages;return all.slice(page*6,page*6+6)});
const scoreCards=computed(()=>[
{key:'difficulty',label:'难 度',field:'difficultyScore',icon:'✿'},
{key:'performance',label:'表 现',field:'performanceScore',icon:'◎'},
{key:'required',label:'规 定',field:'requiredScore',icon:'▣'},
{key:'deduction',label:'扣 分',field:'deductionScore',icon:'⚑'},
].filter(x=>x.key!=='difficulty'||!String(cast.value?.scoreRuleCode||'').toUpperCase().includes('PRESCRIBED')));
const maxRank=computed(()=>Math.max(...rankingRows.value.map(x=>Number(x.totalScore??x.score)||0),1));
const isStandalone=computed(()=>route.path==='/scoring/screen');
const fullscreenButtonText=computed(()=>isStandalone.value?(isFullscreen.value?'退出全屏':'全屏显示'):'打开独立大屏');
let castRequesting=false;
async function loadCast(){if(!eventId||castRequesting)return;castRequesting=true;try{const res=await workbenchScreenCastCurrentGet({eventId,venueId,_t:Date.now()});const body=res?.data??res;const current=body?.data??body;cast.value=current?.screenCastId?current:null;offline.value=false}catch{offline.value=true}finally{castRequesting=false}}
function value(x){if(x===undefined||x===null||x==='')return '0';const n=Number(x);return Number.isFinite(n)?n.toFixed(2).replace(/\.00$/,'').replace(/(\.\d)0$/,'$1'):x}
function rankWidth(row){return `${Math.max(18,Math.round((Number(row.totalScore??row.score)||0)/maxRank.value*86))}%`}
function toggleFullscreen(){if(!isStandalone.value)return window.open(router.resolve({path:'/scoring/screen',query:{eventId,venueId,eventName:title.value}}).href,'_blank');if(!document.fullscreenElement)document.documentElement.requestFullscreen?.();else document.exitFullscreen?.()}
function showControls(){controlsVisible.value=true;window.clearTimeout(controlTimer);controlTimer=window.setTimeout(()=>controlsVisible.value=false,3000)}
function fullscreenChange(){isFullscreen.value=!!document.fullscreenElement}
function setPreview(){
if(!route.query.preview)return;
const ranking=route.query.preview==='ranking';
const sample=ranking?[85.5,83.2,78,75.5,71,68.5].map((score,index)=>({entryNo:`A00${index+1}`,participantName:['崔一','崔二','崔三','崔四','崔五','崔六'][index],totalScore:score,rank:index+1})):{participantName:'崔一',groupName:'少儿组',difficultyScore:21,performanceScore:15,requiredScore:3,deductionScore:3,totalScore:85.5};
cast.value={castType:ranking?2:1,eventName:'2026年上海城市业余联赛跳绳联赛(杨浦站)',projectName:'30秒单摇',groupName:'少儿组',sessionNo:'2',venueName:'1',payloadJson:JSON.stringify(sample)};
}
onMounted(()=>{setPreview();if(!route.query.preview)loadCast();pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&!route.query.preview&&loadCast(),3000);rankTimer=window.setInterval(()=>rankPage.value++,9000);document.addEventListener('fullscreenchange',fullscreenChange);showControls()});
onBeforeUnmount(()=>{window.clearInterval(pollTimer);window.clearInterval(rankTimer);window.clearTimeout(controlTimer);document.removeEventListener('fullscreenchange',fullscreenChange)});
</script>
<style scoped lang="scss">
.score-screen{--cyan:#20dce8;position:relative;width:100vw;height:100vh;min-width:960px;min-height:620px;overflow:hidden;color:#fff;background:radial-gradient(circle at 30% 45%,#074dcc 0,#062d91 38%,#020a3c 78%);font-family:"Microsoft YaHei","PingFang SC",sans-serif}.score-screen.embedded{width:100%;height:calc(100vh - 85px)}.screen-lines{position:absolute;inset:0;opacity:.18;background-image:radial-gradient(ellipse at 50% 110%,transparent 0 36%,#38abf3 36.2% 36.35%,transparent 36.5% 39%,#38abf3 39.2% 39.35%,transparent 39.5%),linear-gradient(105deg,transparent 48%,rgba(41,167,242,.28) 48.2%,transparent 48.4%);background-size:100% 100%,24% 32%;background-position:center,bottom;background-repeat:no-repeat,repeat-x}.skyline{position:absolute;left:0;right:0;bottom:0;height:27%;opacity:.18;background:linear-gradient(90deg,transparent 0 3%,#26a6e4 3.1% 3.25%,transparent 3.35% 11%,#26a6e4 11.1% 11.2%,transparent 11.3% 84%,#26a6e4 84.1% 84.25%,transparent 84.35%);clip-path:polygon(0 100%,0 70%,3% 70%,3% 18%,4% 18%,4% 70%,10% 70%,10% 48%,12% 48%,12% 65%,17% 65%,17% 35%,19% 35%,19% 69%,24% 69%,24% 53%,27% 53%,27% 76%,72% 76%,72% 59%,75% 59%,75% 35%,77% 35%,77% 69%,83% 69%,83% 42%,85% 42%,85% 64%,91% 64%,91% 50%,94% 50%,94% 71%,100% 71%,100% 100%);background-color:#31adf3}.screen-content{position:relative;z-index:1;height:100%;box-sizing:border-box;padding:2.5vh 3.1vw 2.8vh;display:flex;flex-direction:column}.screen-content>h1{display:flex;align-items:center;justify-content:center;gap:2.2vw;margin:0 0 2.4vh;font-size:clamp(34px,3.7vw,74px);line-height:1.25;white-space:nowrap}.screen-content>h1 i{width:6vw;height:1.2vh;background:linear-gradient(90deg,transparent,#17dbe7);clip-path:polygon(0 35%,100% 0,85% 100%,0 70%)}.screen-content>h1 i:last-child{transform:scaleX(-1);background:linear-gradient(90deg,transparent,#c827bb)}.meta-pill{width:72%;align-self:center;display:flex;align-items:center;box-sizing:border-box;padding:1.2vh 2vw;margin-bottom:3.5vh;border:2px solid rgba(31,163,255,.55);border-radius:999px;background:rgba(0,8,56,.35);font-size:clamp(17px,1.55vw,31px)}.meta-pill b{font-weight:600}.meta-spacer{flex:1}.people-icon,.pin-icon{margin-right:.8vw;color:var(--cyan);font-size:1.5em}.single-board,.rank-board{position:relative;flex:1;min-height:0;border:2px solid rgba(80,186,255,.72);border-radius:1.4vw;background:linear-gradient(110deg,rgba(16,103,237,.82),rgba(0,20,105,.75));box-shadow:inset 0 0 40px rgba(4,99,238,.22)}.board-label{position:absolute;left:50%;top:0;transform:translate(-50%,-2px);min-width:20vw;padding:1.1vh 4vw 1.5vh;text-align:center;color:#9ee9ff;font-size:clamp(20px,2vw,39px);font-weight:700;border-bottom:2px solid #2b8ef2;clip-path:polygon(0 0,100% 0,86% 100%,14% 100%);background:rgba(0,35,132,.75)}.board-label i{display:inline-block;width:2vw;height:.65vh;margin:0 .8vw;background:repeating-linear-gradient(90deg,var(--cyan) 0 5px,transparent 5px 10px);transform:skew(30deg)}.player-name{position:absolute;left:0;right:0;top:20%;text-align:center;font-size:clamp(29px,3.2vw,60px);font-weight:700}.score-value{position:absolute;left:0;right:0;top:34%;text-align:center;font-size:clamp(110px,13vw,245px);font-weight:900;line-height:1;text-shadow:0 18px 38px rgba(0,0,0,.42),0 0 20px rgba(65,174,255,.3)}.score-glow{position:absolute;left:18%;right:18%;bottom:13%;height:3px;background:#bffcff;box-shadow:0 0 8px 4px #27bdff,0 0 25px 12px #1489f5}.score-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:1.7vw;margin-top:2.8vh;height:19vh;min-height:125px}.score-card{display:grid;grid-template-columns:auto 1fr;grid-template-rows:1fr auto auto;align-items:center;column-gap:1.6vw;padding:1.8vh 2.6vw 1.4vh;border:2px solid rgba(126,255,255,.62);border-radius:1.4vw;background:linear-gradient(135deg,#37ad6b,#277c70);box-shadow:inset 0 0 30px rgba(255,255,255,.08)}.score-card.performance{background:linear-gradient(135deg,#20b3aa,#146d8e)}.score-card.required{background:linear-gradient(135deg,#f2aa21,#b95c16)}.score-card.deduction{background:linear-gradient(135deg,#d53989,#6e2793)}.card-icon{display:grid;place-items:center;width:4.2vw;height:4.2vw;min-width:48px;min-height:48px;border:7px solid rgba(255,255,255,.32);border-radius:50%;background:rgba(75,255,157,.2);font-size:2.1vw;box-shadow:0 0 16px rgba(91,255,145,.8)}.score-card b{font-size:clamp(38px,5.6vw,98px);line-height:1}.score-card i{grid-column:1/-1;height:1px;margin-top:.5vh;background:rgba(255,255,255,.38)}.score-card strong{grid-column:1/-1;padding-top:1vh;text-align:center;font-size:clamp(17px,1.9vw,36px);letter-spacing:.25em}.rank-board{padding:3.2vh 3vw;display:flex;flex-direction:column;justify-content:space-around}.rank-row{height:14%;display:flex;align-items:center;gap:1.5vw}.medal,.rank-no{width:4vw;height:4vw;min-width:50px;min-height:50px;display:grid;place-items:center;border:2px solid #41d8ff;border-radius:50%;font-size:clamp(24px,2vw,40px);font-weight:800}.medal{position:relative;border:0;background:radial-gradient(circle,#fff6b6 0 12%,#e8a513 13% 43%,#8b4c00 45% 52%,#ffdb58 54% 100%);box-shadow:0 4px 12px rgba(0,0,0,.4)}.medal::before{content:"";position:absolute;top:-18%;width:45%;height:38%;z-index:-1;background:#102a6b}.medal-2{filter:grayscale(.75) brightness(1.35)}.medal-3{filter:hue-rotate(-25deg) saturate(.8)}.rank-main{min-width:0;flex:1;position:relative}.rank-main strong{position:absolute;left:0;top:-2.3vh;z-index:2;padding:.6vh 1.2vw;border-radius:.8vw .8vw 0 0;background:#052d9d;font-size:clamp(16px,1.25vw,25px);white-space:nowrap}.people-icon.small{font-size:1em}.bar{height:3.1vh;border:2px solid #3d95ee;border-radius:.7vw;background:#06216b;overflow:hidden}.bar i{display:block;height:100%;background:linear-gradient(90deg,#0878ff,#35e5e7);clip-path:polygon(0 0,97% 0,100% 50%,97% 100%,0 100%);box-shadow:inset 0 0 8px rgba(255,255,255,.35)}.rank-score{width:8vw;text-align:right;font-size:clamp(30px,3vw,62px);font-weight:800}.rank-score small{margin-left:.3vw;font-size:.36em}.waiting{position:relative;z-index:1;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}.waiting-ring{width:110px;height:110px;border:3px solid rgba(37,211,241,.25);border-top-color:#25d3f1;border-radius:50%;animation:spin 1.2s linear infinite}.waiting h1{font-size:44px;margin:30px 0 8px}.waiting p{color:#66b5db}.screen-controls{position:absolute;right:18px;top:16px;z-index:5;display:flex;align-items:center;gap:8px;padding:8px;border:1px solid rgba(255,255,255,.15);border-radius:9px;background:rgba(0,8,45,.72);opacity:0;transform:translateY(-10px);transition:.25s}.screen-controls.visible{opacity:1;transform:none}.screen-controls button{padding:6px 10px;border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#fff;background:transparent;cursor:pointer}.screen-controls span{color:#42d996;font-size:12px}.screen-controls span.offline{color:#ff6b6b}@keyframes spin{to{transform:rotate(360deg)}}
@media(max-aspect-ratio:16/10){.screen-content{padding-left:2vw;padding-right:2vw}.meta-pill{width:88%}.score-cards{gap:1vw}.score-card{padding-left:1.7vw;padding-right:1.7vw}}
</style>