83 lines
14 KiB
Vue
Raw Normal View History

2026-09-06 22:36:26 +08:00
<template>
2026-09-09 18:30:09 +08:00
<div :class="['score-screen',{embedded:!isStandalone}]" @mousemove="showControls">
2026-09-06 22:36:26 +08:00
<div class="screen-lines"></div><div class="skyline"></div>
2026-09-09 18:30:09 +08:00
<div class="screen-controls" :class="{ visible: controlsVisible }"><button @click="toggleFullscreen">{{ fullscreenButtonText }}</button><button @click="loadCast">刷新</button><span :class="{ offline }">{{ offline ? '连接异常' : '实时数据' }}</span></div>
2026-09-06 22:36:26 +08:00
<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';
2026-09-09 18:30:09 +08:00
import { useRoute, useRouter } from 'vue-router';
2026-09-06 22:36:26 +08:00
import { workbenchScreenCastCurrentGet } from '/@/api/match/workbench';
2026-09-09 18:30:09 +08:00
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);
2026-09-06 22:36:26 +08:00
const cast=ref(null), offline=ref(false), isFullscreen=ref(false), controlsVisible=ref(true), rankPage=ref(0);
let pollTimer, controlTimer, rankTimer;
2026-09-09 18:30:09 +08:00
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 {}}});
2026-09-06 22:36:26 +08:00
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));
2026-09-09 18:30:09 +08:00
const isStandalone=computed(()=>route.path==='/scoring/screen');
const fullscreenButtonText=computed(()=>isStandalone.value?(isFullscreen.value?'退出全屏':'全屏显示'):'打开独立大屏');
2026-09-06 22:36:26 +08:00
let castRequesting=false;
2026-09-09 18:30:09 +08:00
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}}
2026-09-06 22:36:26 +08:00
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))}%`}
2026-09-09 18:30:09 +08:00
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?.()}
2026-09-06 22:36:26 +08:00
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">
2026-09-09 18:30:09 +08:00
.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
2026-09-06 22:36:26 +08:00
@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>