feat: 优化项目级检录大屏展示

This commit is contained in:
ltx 2026-09-20 17:40:26 +08:00
parent 9787262e88
commit 989f9edd18

View File

@ -13,11 +13,11 @@
<aside class="schedule-panel">
<div class="section-title"><span class="section-icon"><el-icon><Calendar /></el-icon></span><div><small>ORDER OF EVENTS</small><h2>赛事检录顺序</h2></div></div>
<div class="stage-list">
<div v-for="(stage,index) in stages" :key="`${stage.eventProjectId}-${stage.groupId}`" :class="['stage-item',`status-${stage.stageStatus}`]">
<div v-for="(stage,index) in projectStages" :key="stage.eventProjectId" :class="['stage-item',`status-${stage.stageStatus}`]">
<div class="stage-index"><span>{{ String(index + 1).padStart(2,'0') }}</span></div>
<div class="stage-copy"><strong>{{ stage.projectName || '未命名项目' }}</strong><span>{{ stage.groupName || '混合组别' }}</span><em><i></i>{{ stage.stageStatusText || stageStatusLabel(stage.stageStatus) }}</em></div>
<div class="stage-copy"><strong>{{ stage.projectName || '未命名项目' }}</strong><span>{{ stage.checkedUnits }} / {{ stage.totalUnits }} 已检录</span><em><i></i>{{ stage.stageStatusText || stageStatusLabel(stage.stageStatus) }}</em></div>
</div>
<div v-if="!stages.length" class="empty-block"><el-icon><Calendar /></el-icon><b>暂无检录安排</b><span>等待后台发布检录场次</span></div>
<div v-if="!projectStages.length" class="empty-block"><el-icon><Calendar /></el-icon><b>暂无检录安排</b><span>等待后台发布检录场次</span></div>
</div>
<div class="schedule-foot"><span></span><p>请参赛人员提前做好准备<br>按现场引导有序检录</p></div>
</aside>
@ -28,16 +28,17 @@
<div class="live-badge"><i></i>实时更新</div>
</div>
<div class="checkin-panel">
<div class="project-ribbon"><span class="slash"></span><strong>{{ currentCheckin?.displayName || '等待检录项目' }}</strong><span class="slash reverse"></span></div>
<div class="unit-grid" :class="{ empty: !visibleUnits.length }">
<article v-for="(unit,index) in visibleUnits" :key="unit.unitKey" :class="['unit-card',{checked:unit.checkinStatus>=2}]" :style="{'--delay':`${index * 70}ms`}">
<div class="status-row"><span class="order-no">{{ String(unitPage * 8 + index + 1).padStart(2,'0') }}</span><em><i></i>{{ unit.checkinStatusText || (unit.checkinStatus>=2?'已检录':'待检录') }}</em></div>
<div class="person-icon"><el-icon><CircleCheckFilled v-if="unit.checkinStatus>=2"/><Clock v-else/></el-icon></div>
<div class="project-ribbon"><span class="slash"></span><strong>{{ currentCheckin?.projectName || currentCheckin?.displayName || '等待检录项目' }}</strong><span class="slash reverse"></span></div>
<Transition name="unit-scroll" mode="out-in">
<div :key="unitPage" class="unit-grid" :class="{ empty: !visibleUnits.length }">
<article v-for="(unit,index) in visibleUnits" :key="unit.unitKey" :class="['unit-card',{checked:unit.checkinStatus>=2}]" :style="{'--delay':`${index * 45}ms`}">
<div class="status-row"><span class="order-no">{{ String(unitPage * unitsPerPage + index + 1).padStart(2,'0') }}</span><span class="group-name">{{ unit.groupName || '默认组别' }}</span><em><i></i>{{ unit.checkinStatusText || (unit.checkinStatus>=2?'已检录':'待检录') }}</em></div>
<div class="athlete-names"><b v-for="name in unit.athleteNames" :key="name">{{ name }}</b><b v-if="!unit.athleteNames?.length">待确认</b></div>
<span class="team-name">{{ unit.teamName || unit.unitName || '参赛单位' }}</span>
</article>
<div v-if="!visibleUnits.length" class="empty-block board-empty"><el-icon><UserFilled /></el-icon><b>等待检录数据</b><span>后台发布后将在这里自动展示</span></div>
</div>
</Transition>
<div v-if="unitPageCount > 1" class="page-dots"><i v-for="page in unitPageCount" :key="page" :class="{active:page-1===unitPage}"></i></div>
</div>
@ -62,7 +63,7 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
import { Calendar, CircleCheckFilled, Clock, Flag, FullScreen, Refresh, Trophy, UserFilled } from '@element-plus/icons-vue';
import { Calendar, Flag, FullScreen, Refresh, Trophy, UserFilled } from '@element-plus/icons-vue';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { checkinScreenCastGet } from '/@/api/match/workbench';
@ -78,13 +79,30 @@ const stageStyle=computed(()=>({transform:`translate(-50%, -50%) scale(${scale.v
const data=ref({stages:[],units:[]}),offline=ref(false),clock=ref(new Date()),controlsVisible=ref(false),unitPage=ref(0);
let pollTimer,clockTimer,controlTimer,unitTimer,serverBaseMs=Date.now(),syncedAtMs=Date.now(),requesting=false;
const stages=computed(()=>data.value.stages||[]),currentCheckin=computed(()=>data.value.currentCheckin),currentCompeting=computed(()=>data.value.currentCompeting);
const unitPageCount=computed(()=>Math.max(1,Math.ceil((data.value.units||[]).length/8)));
const visibleUnits=computed(()=>{const all=data.value.units||[];const page=unitPage.value%unitPageCount.value;return all.slice(page*8,page*8+8)});
const projectStages=computed(()=>{
const map=new Map();
for(const stage of stages.value){
const key=String(stage.eventProjectId??stage.projectName??'');
const current=map.get(key);
if(!current){map.set(key,{...stage,groupId:null,order:Number(stage.order)||0,totalUnits:Number(stage.totalUnits)||0,checkedUnits:Number(stage.checkedUnits)||0,statuses:[Number(stage.stageStatus)||3]});continue}
current.order=Math.min(current.order,Number(stage.order)||current.order);
current.totalUnits+=Number(stage.totalUnits)||0;
current.checkedUnits+=Number(stage.checkedUnits)||0;
current.statuses.push(Number(stage.stageStatus)||3);
}
return [...map.values()].sort((a,b)=>a.order-b.order).map(stage=>{
const status=stage.statuses.every(item=>item===4)?4:stage.statuses.includes(1)?1:stage.statuses.includes(2)?2:3;
return {...stage,stageStatus:status,stageStatusText:stageStatusLabel(status)};
});
});
const unitsPerPage=15;
const unitPageCount=computed(()=>Math.max(1,Math.ceil((data.value.units||[]).length/unitsPerPage)));
const visibleUnits=computed(()=>{const all=data.value.units||[];const page=unitPage.value%unitPageCount.value;return all.slice(page*unitsPerPage,page*unitsPerPage+unitsPerPage)});
const pad=n=>String(n).padStart(2,'0');
const timeText=computed(()=>`${pad(clock.value.getHours())}:${pad(clock.value.getMinutes())}:${pad(clock.value.getSeconds())}`);
const dateText=computed(()=>`${clock.value.getFullYear()}.${pad(clock.value.getMonth()+1)}.${pad(clock.value.getDate())}`);
const week=computed(()=>data.value.weekDayText||['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][clock.value.getDay()]);
function stageStatusLabel(status){return Number(status)===1?'检录中':Number(status)===2?'即将开始':'等待中'}
function stageStatusLabel(status){return Number(status)===1?'检录中':Number(status)===2?'即将开始':Number(status)===4?'已完成':'等待中'}
function resize(){const host=viewportRef.value;if(!host)return;scale.value=Math.min(host.clientWidth/1920,host.clientHeight/1080)}
function tick(){clock.value=new Date(serverBaseMs+(Date.now()-syncedAtMs))}
function syncClock(){const raw=data.value.serverTime||data.value.serverTimeText;const parsed=raw?new Date(String(raw).replace(' ','T')):new Date();serverBaseMs=Number.isNaN(parsed.getTime())?Date.now():parsed.getTime();syncedAtMs=Date.now();tick()}
@ -92,7 +110,7 @@ async function load(){if(!eventId||requesting)return;requesting=true;try{const r
function toggleFullscreen(){if(fullscreenDocument.fullscreenElement)fullscreenDocument.exitFullscreen?.().catch(()=>{});else fullscreenDocument.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{});storesTagsViewRoutes.setCurrenFullscreen(true)}
function requestAutoFullscreen(){if(route.query.autofullscreen==='1'&&!document.fullscreenElement)document.documentElement.requestFullscreen?.({navigationUI:'hide'}).catch(()=>{})}
function showControls(){controlsVisible.value=true;window.clearTimeout(controlTimer);controlTimer=window.setTimeout(()=>controlsVisible.value=false,2600)}
function setPreview(){if(!route.query.preview)return;const names=[['凌予蓁','刘姝妍'],['乔韵宁','廖歆悦'],['程沐晞','陈禹舟'],['韩蕙阳','孟清和'],['廖翊博','张景泽'],['任家驹','周瑾瑜'],['张锦川','施宜程'],['苏栩宁','林若瑶']];data.value={eventName:'2026年上海城市业余联赛跳绳联赛(杨浦站)',serverTimeText:'2026-07-29 10:05:32',weekDayText:'星期三',stages:[{eventProjectId:1,groupId:1,projectName:'3分钟10人长绳“8”字跳',groupName:'少儿男子组',stageStatus:1,stageStatusText:'检录中'},{eventProjectId:2,groupId:2,projectName:'两人同步规定套路',groupName:'少儿组',stageStatus:2,stageStatusText:'即将开始'},{eventProjectId:3,groupId:3,projectName:'个人花样基础套路赛',groupName:'儿童组',stageStatus:3,stageStatusText:'等待中'}],currentCheckin:{displayName:'3分钟10人长绳“8”字跳 · 少儿男子组'},currentCompeting:{displayName:'个人花样基础套路赛 · 儿童组',unitName:'跃动之夏培训学校有限公司',teamName:'诺宝宝花样跳绳队',athleteNames:['张嘉果','王灵曦','英轩诚']},units:names.map((athleteNames,index)=>({unitKey:String(index),athleteNames,teamName:`第 ${index+1} 参赛队`,checkinStatus:index<6?2:1,checkinStatusText:index<6?'已检录':'待检录'}))};syncClock()}
function setPreview(){if(!route.query.preview)return;const names=['凌予蓁','刘姝妍','乔韵宁','廖歆悦','程沐晞','陈禹舟','韩蕙阳','孟清和','廖翊博','张景泽','任家驹','周瑾瑜','张锦川','施宜程','苏栩宁','林若瑶','陈星宇','徐梓涵','吴一诺','陆子昂'];data.value={eventName:'2026年上海城市业余联赛跳绳联赛(杨浦站)',serverTimeText:'2026-07-29 10:05:32',weekDayText:'星期三',stages:[{eventProjectId:1,groupId:1,projectName:'3分钟10人长绳“8”字跳',groupName:'少儿男子组',order:1,totalUnits:10,checkedUnits:6,stageStatus:1},{eventProjectId:1,groupId:2,projectName:'3分钟10人长绳“8”字跳',groupName:'少儿女子组',order:11,totalUnits:10,checkedUnits:4,stageStatus:1},{eventProjectId:2,groupId:3,projectName:'两人同步规定套路',groupName:'少儿组',order:21,totalUnits:8,checkedUnits:0,stageStatus:2},{eventProjectId:3,groupId:4,projectName:'个人花样基础套路赛',groupName:'儿童组',order:29,totalUnits:12,checkedUnits:0,stageStatus:3}],currentCheckin:{eventProjectId:1,projectName:'3分钟10人长绳“8”字跳',displayName:'3分钟10人长绳“8”字跳'},currentCompeting:{displayName:'个人花样基础套路赛 · 儿童组',unitName:'跃动之夏培训学校有限公司',teamName:'诺宝宝花样跳绳队',athleteNames:['张嘉果','王灵曦','英轩诚']},units:names.map((name,index)=>({unitKey:String(index),groupName:index<10?'少儿男子组':'少儿女子组',athleteNames:[name],teamName:`第 ${index+1} 参赛队`,checkinStatus:index<10?2:1,checkinStatusText:index<10?'已检录':'待检录'}))};syncClock()}
onMounted(async()=>{storesTagsViewRoutes.setCurrenFullscreen(true);setPreview();if(!route.query.preview)await load();await nextTick();resize();window.addEventListener('resize',resize);pollTimer=window.setInterval(()=>document.visibilityState==='visible'&&!route.query.preview&&load(),3000);clockTimer=window.setInterval(tick,1000);unitTimer=window.setInterval(()=>{unitPage.value=(unitPage.value+1)%unitPageCount.value},9000);requestAutoFullscreen()});
onBeforeUnmount(()=>{[pollTimer,clockTimer,unitTimer].forEach(window.clearInterval);window.clearTimeout(controlTimer);window.removeEventListener('resize',resize)});
</script>
@ -100,8 +118,8 @@ onBeforeUnmount(()=>{[pollTimer,clockTimer,unitTimer].forEach(window.clearInterv
<style scoped lang="scss">
.checkin-viewport{position:relative;width:100%;height:100%;min-height:100vh;overflow:hidden;background:#01051e;font-family:"Microsoft YaHei","PingFang SC",sans-serif}.checkin-stage{--cyan:#22d8ff;--blue:#148cff;position:absolute;left:50%;top:50%;width:1920px;height:1080px;overflow:hidden;color:#fff;background:radial-gradient(circle at 50% 30%,rgba(19,100,222,.23),transparent 42%),linear-gradient(145deg,#031867 0%,#071452 45%,#030b35 100%);transform-origin:center}.checkin-stage::before{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(37,218,255,.025) 1px,transparent 1px),linear-gradient(rgba(37,218,255,.025) 1px,transparent 1px);background-size:54px 54px;mask-image:linear-gradient(to bottom,transparent,black 30%,black)}.ambient{position:absolute;border-radius:50%;filter:blur(20px);opacity:.65}.ambient-one{left:-240px;top:220px;width:520px;height:520px;background:rgba(0,95,255,.22)}.ambient-two{right:-260px;bottom:-180px;width:650px;height:650px;background:rgba(0,194,255,.14)}.grid-glow{position:absolute;left:25%;right:0;bottom:-180px;height:420px;background:repeating-radial-gradient(ellipse at 50% 100%,transparent 0 28px,rgba(28,170,255,.13) 30px 32px,transparent 34px 52px);transform:perspective(300px) rotateX(54deg)}
.title-wing{display:flex;gap:8px}.title-wing i{display:block;width:64px;height:6px;background:linear-gradient(90deg,rgba(29,211,255,.15),#20d9ff);transform:skew(-34deg)}.title-wing i:nth-child(2){width:36px}.title-wing i:nth-child(3){width:14px}.wing-left{transform:scaleX(-1)}.checkin-stage>header{position:relative;z-index:2;height:138px;display:flex;align-items:center;justify-content:center;gap:35px;border-bottom:1px solid rgba(71,181,255,.24);background:linear-gradient(180deg,rgba(5,29,113,.8),rgba(5,20,78,.2))}.event-title{text-align:center}.event-title small{display:block;color:#4fcfff;font-size:13px;font-weight:700;letter-spacing:.32em}.event-title h1{max-width:1050px;margin:6px 0 4px;overflow:hidden;font-size:36px;line-height:1.25;text-overflow:ellipsis;white-space:nowrap;text-shadow:0 6px 20px rgba(0,61,205,.5)}.event-title>span{display:inline-flex;align-items:center;gap:10px;color:#7ee9ff;font-size:16px;letter-spacing:.3em}.event-title>span::before,.event-title>span::after{content:"";width:42px;height:1px;background:linear-gradient(90deg,transparent,#50defb)}.event-title>span::after{transform:scaleX(-1)}.time-box{position:absolute;right:50px;top:36px;text-align:right}.time-box b{display:block;font-size:28px;font-variant-numeric:tabular-nums;letter-spacing:.07em}.time-box span{display:block;margin-top:6px;color:#72aee2;font-size:13px}
.screen-content{position:relative;z-index:2;height:866px;display:grid;grid-template-columns:390px 1fr;gap:24px;padding:25px 34px 20px;box-sizing:border-box}.schedule{display:flex;flex-direction:column;padding:24px 22px;border:1px solid rgba(57,179,255,.28);border-radius:18px;background:linear-gradient(160deg,rgba(11,52,143,.62),rgba(2,20,77,.72));box-shadow:inset 0 1px rgba(119,224,255,.12),0 20px 50px rgba(0,0,26,.18);overflow:hidden}.section-title,.section-title+div,.section-title>div,.section-title{display:flex}.section-title,.section-title>div{align-items:center}.section-title>div{display:block}.section-icon{width:50px;height:50px;display:grid;place-items:center;margin-right:14px;border:1px solid rgba(47,213,255,.55);border-radius:14px;color:#62eaff;background:linear-gradient(145deg,rgba(18,179,255,.25),rgba(16,74,184,.16));box-shadow:inset 0 0 18px rgba(19,185,255,.13)}.section-icon .el-icon{font-size:27px}.section-title small,.schedule .section-title small{display:block;color:#4d92cd;font-size:11px;font-weight:700;letter-spacing:.2em}.section-title h2,.schedule h2{margin:4px 0 0;font-size:24px}.stage-list{flex:1;margin-top:26px}.stage-item{position:relative;display:grid;grid-template-columns:54px 1fr;gap:15px;min-height:152px}.stage-item::after{content:"";position:absolute;left:26px;top:53px;width:1px;height:88px;background:linear-gradient(#198eea,rgba(25,142,234,.12))}.stage-item:last-child::after{display:none}.stage-index{position:relative;width:54px;height:54px;display:grid;place-items:center;border:1px solid #2178c8;border-radius:50%;color:#66caff;background:#072f83;box-shadow:0 0 0 6px rgba(21,126,218,.08)}.stage-index::after{content:"";position:absolute;inset:7px;border:1px solid rgba(82,208,255,.25);border-radius:50%}.stage-index span{position:relative;z-index:1;font-size:16px;font-weight:700}.stage-copy{padding-top:2px}.stage-copy strong{display:-webkit-box;overflow:hidden;font-size:18px;line-height:1.45;-webkit-line-clamp:2;-webkit-box-orient:vertical}.stage-copy>span{display:block;margin-top:5px;color:#84a8d3;font-size:14px}.stage-copy em{display:inline-flex;align-items:center;gap:7px;margin-top:10px;padding:5px 11px;border-radius:999px;color:#67bfff;background:rgba(25,128,218,.16);font-size:12px;font-style:normal}.stage-copy em i,.live-badge i,.controls>span i{width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor}.status-1 .stage-index{border-color:#29e4ab;color:#62f3c7;background:#075c60;box-shadow:0 0 22px rgba(45,226,174,.2)}.status-1 .stage-copy em{color:#5af0bd;background:rgba(38,218,164,.12)}.status-2 .stage-copy em{color:#ffc75a;background:rgba(255,181,55,.12)}.schedule-foot{display:flex;align-items:center;gap:14px;padding:15px;border:1px solid rgba(43,163,235,.16);border-radius:12px;background:rgba(5,34,100,.45)}.schedule-foot>span{width:34px;height:34px;border:3px solid #2cb5ee;border-left-color:transparent;border-radius:50%}.schedule-foot p{margin:0;color:#6ea2d0;font-size:12px;line-height:1.7}
.main-board{min-width:0}.current-head,.competing-head{height:60px;display:flex;align-items:center;justify-content:space-between}.live-badge{display:flex;align-items:center;gap:8px;padding:8px 14px;border:1px solid rgba(48,230,180,.26);border-radius:999px;color:#58edc1;background:rgba(32,206,161,.08);font-size:12px}.live-badge i{animation:pulse 1.5s infinite}.checkin-panel{height:550px;margin-top:12px;padding:18px 20px 16px;box-sizing:border-box;border:1px solid rgba(51,185,255,.34);border-radius:20px;background:linear-gradient(150deg,rgba(12,55,150,.68),rgba(3,20,75,.72));box-shadow:inset 0 1px rgba(124,230,255,.14),0 22px 60px rgba(0,3,36,.25)}.project-ribbon{height:58px;display:flex;align-items:center;justify-content:center;gap:28px;border-radius:12px;color:#fff;background:linear-gradient(90deg,rgba(8,91,206,.22),rgba(14,130,239,.8),rgba(8,91,206,.22));font-size:25px;letter-spacing:.04em}.slash{width:72px;height:9px;background:repeating-linear-gradient(90deg,#38e2ff 0 8px,transparent 8px 14px);transform:skew(-28deg)}.slash.reverse{transform:skew(28deg)}.unit-grid{height:432px;display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(2,1fr);gap:13px;padding-top:16px;box-sizing:border-box}.unit-card{position:relative;display:grid;grid-template-columns:68px 1fr;grid-template-rows:auto 1fr auto;column-gap:13px;padding:16px;border:1px solid rgba(58,164,240,.22);border-radius:14px;background:linear-gradient(145deg,rgba(10,63,148,.72),rgba(7,36,101,.8));box-shadow:inset 0 1px rgba(142,224,255,.07);animation:card-in .55s ease both;animation-delay:var(--delay)}.unit-card.checked{border-color:rgba(44,224,177,.26);background:linear-gradient(145deg,rgba(7,98,124,.62),rgba(7,43,103,.78))}.status-row{grid-column:1/-1;display:flex;align-items:center;justify-content:space-between}.order-no{color:#3d88c3;font-size:12px;font-weight:700}.status-row em{display:flex;align-items:center;gap:6px;color:#5cb6f1;font-size:12px;font-style:normal}.status-row em i{width:6px;height:6px;border-radius:50%;background:currentColor}.checked .status-row em{color:#51e6b8}.person-icon{grid-row:2;align-self:center;width:58px;height:58px;display:grid;place-items:center;border:1px solid rgba(59,194,255,.38);border-radius:50%;color:#56cfff;background:rgba(19,145,225,.12);box-shadow:inset 0 0 0 7px rgba(38,161,230,.06)}.person-icon .el-icon{font-size:30px}.checked .person-icon{border-color:rgba(70,234,186,.46);color:#5bf1c4;background:rgba(45,212,164,.1)}.athlete-names{align-self:center;display:flex;flex-wrap:wrap;gap:5px 12px}.athlete-names b{font-size:17px;line-height:1.3}.team-name{grid-column:1/-1;overflow:hidden;padding-top:10px;border-top:1px solid rgba(74,151,213,.13);color:#77a4cd;font-size:11px;text-overflow:ellipsis;white-space:nowrap}.page-dots{height:12px;display:flex;justify-content:center;gap:7px}.page-dots i{width:8px;height:4px;border-radius:4px;background:#28598d}.page-dots i.active{width:25px;background:#24cfff}.unit-grid.empty{display:grid}.empty-block{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#4f86b7}.empty-block .el-icon{margin-bottom:12px;font-size:38px}.empty-block b{color:#7ba9ce;font-size:17px}.empty-block span{margin-top:7px;font-size:12px}.board-empty{grid-column:1/-1;grid-row:1/-1}
.screen-content{position:relative;z-index:2;height:866px;display:grid;grid-template-columns:390px 1fr;gap:24px;padding:25px 34px 20px;box-sizing:border-box}.schedule{display:flex;flex-direction:column;padding:24px 22px;border:1px solid rgba(57,179,255,.28);border-radius:18px;background:linear-gradient(160deg,rgba(11,52,143,.62),rgba(2,20,77,.72));box-shadow:inset 0 1px rgba(119,224,255,.12),0 20px 50px rgba(0,0,26,.18);overflow:hidden}.section-title,.section-title+div,.section-title>div,.section-title{display:flex}.section-title,.section-title>div{align-items:center}.section-title>div{display:block}.section-icon{width:50px;height:50px;display:grid;place-items:center;margin-right:14px;border:1px solid rgba(47,213,255,.55);border-radius:14px;color:#62eaff;background:linear-gradient(145deg,rgba(18,179,255,.25),rgba(16,74,184,.16));box-shadow:inset 0 0 18px rgba(19,185,255,.13)}.section-icon .el-icon{font-size:27px}.section-title small,.schedule .section-title small{display:block;color:#4d92cd;font-size:11px;font-weight:700;letter-spacing:.2em}.section-title h2,.schedule h2{margin:4px 0 0;font-size:24px}.stage-list{flex:1;margin-top:22px;overflow:hidden}.stage-item{position:relative;display:grid;grid-template-columns:46px 1fr;gap:13px;min-height:118px}.stage-item::after{content:"";position:absolute;left:22px;top:46px;width:1px;height:62px;background:linear-gradient(#198eea,rgba(25,142,234,.12))}.stage-item:last-child::after{display:none}.stage-index{position:relative;width:46px;height:46px;display:grid;place-items:center;border:1px solid #2178c8;border-radius:50%;color:#66caff;background:#072f83;box-shadow:0 0 0 5px rgba(21,126,218,.08)}.stage-index::after{content:"";position:absolute;inset:6px;border:1px solid rgba(82,208,255,.25);border-radius:50%}.stage-index span{position:relative;z-index:1;font-size:14px;font-weight:700}.stage-copy{padding-top:0}.stage-copy strong{display:-webkit-box;overflow:hidden;font-size:17px;line-height:1.35;-webkit-line-clamp:2;-webkit-box-orient:vertical}.stage-copy>span{display:block;margin-top:3px;color:#84a8d3;font-size:12px}.stage-copy em{display:inline-flex;align-items:center;gap:6px;margin-top:6px;padding:4px 9px;border-radius:999px;color:#67bfff;background:rgba(25,128,218,.16);font-size:11px;font-style:normal}.stage-copy em i,.live-badge i,.controls>span i{width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor}.status-1 .stage-index{border-color:#29e4ab;color:#62f3c7;background:#075c60;box-shadow:0 0 22px rgba(45,226,174,.2)}.status-1 .stage-copy em{color:#5af0bd;background:rgba(38,218,164,.12)}.status-2 .stage-copy em{color:#ffc75a;background:rgba(255,181,55,.12)}.status-4 .stage-index{border-color:#3f9fd5;color:#8bcfff;background:#074174}.status-4 .stage-copy em{color:#75bfe9;background:rgba(63,159,213,.12)}.schedule-foot{display:flex;align-items:center;gap:14px;padding:15px;border:1px solid rgba(43,163,235,.16);border-radius:12px;background:rgba(5,34,100,.45)}.schedule-foot>span{width:34px;height:34px;border:3px solid #2cb5ee;border-left-color:transparent;border-radius:50%}.schedule-foot p{margin:0;color:#6ea2d0;font-size:12px;line-height:1.7}
.main-board{min-width:0}.current-head,.competing-head{height:60px;display:flex;align-items:center;justify-content:space-between}.live-badge{display:flex;align-items:center;gap:8px;padding:8px 14px;border:1px solid rgba(48,230,180,.26);border-radius:999px;color:#58edc1;background:rgba(32,206,161,.08);font-size:12px}.live-badge i{animation:pulse 1.5s infinite}.checkin-panel{height:550px;margin-top:12px;padding:18px 20px 16px;box-sizing:border-box;overflow:hidden;border:1px solid rgba(51,185,255,.34);border-radius:20px;background:linear-gradient(150deg,rgba(12,55,150,.68),rgba(3,20,75,.72));box-shadow:inset 0 1px rgba(124,230,255,.14),0 22px 60px rgba(0,3,36,.25)}.project-ribbon{height:58px;display:flex;align-items:center;justify-content:center;gap:28px;border-radius:12px;color:#fff;background:linear-gradient(90deg,rgba(8,91,206,.22),rgba(14,130,239,.8),rgba(8,91,206,.22));font-size:25px;letter-spacing:.04em}.slash{width:72px;height:9px;background:repeating-linear-gradient(90deg,#38e2ff 0 8px,transparent 8px 14px);transform:skew(-28deg)}.slash.reverse{transform:skew(28deg)}.unit-grid{height:432px;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));grid-template-rows:repeat(3,1fr);gap:9px;padding-top:13px;box-sizing:border-box}.unit-card{position:relative;display:grid;grid-template-rows:auto 1fr auto;min-width:0;padding:10px 12px;border:1px solid rgba(58,164,240,.22);border-radius:11px;background:linear-gradient(145deg,rgba(10,63,148,.72),rgba(7,36,101,.8));box-shadow:inset 0 1px rgba(142,224,255,.07);animation:card-in .45s ease both;animation-delay:var(--delay)}.unit-card.checked{border-color:rgba(44,224,177,.26);background:linear-gradient(145deg,rgba(7,98,124,.62),rgba(7,43,103,.78))}.status-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:7px}.order-no{color:#3d88c3;font-size:10px;font-weight:700}.group-name{overflow:hidden;color:#6ca7d4;font-size:10px;text-align:center;text-overflow:ellipsis;white-space:nowrap}.status-row em{display:flex;align-items:center;gap:5px;color:#5cb6f1;font-size:10px;font-style:normal;white-space:nowrap}.status-row em i{width:5px;height:5px;border-radius:50%;background:currentColor}.checked .status-row em{color:#51e6b8}.athlete-names{align-self:center;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:3px 9px;max-height:48px;overflow:hidden;text-align:center}.athlete-names b{font-size:15px;line-height:1.3}.team-name{overflow:hidden;padding-top:7px;border-top:1px solid rgba(74,151,213,.13);color:#77a4cd;font-size:10px;text-align:center;text-overflow:ellipsis;white-space:nowrap}.page-dots{height:12px;display:flex;justify-content:center;gap:7px}.page-dots i{width:8px;height:4px;border-radius:4px;background:#28598d;transition:.25s}.page-dots i.active{width:25px;background:#24cfff}.unit-grid.empty{display:grid}.unit-scroll-enter-active,.unit-scroll-leave-active{transition:opacity .35s ease,transform .35s ease}.unit-scroll-enter-from{opacity:0;transform:translateY(28px)}.unit-scroll-leave-to{opacity:0;transform:translateY(-28px)}.empty-block{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#4f86b7}.empty-block .el-icon{margin-bottom:12px;font-size:38px}.empty-block b{color:#7ba9ce;font-size:17px}.empty-block span{margin-top:7px;font-size:12px}.board-empty{grid-column:1/-1;grid-row:1/-1}
.competing-head{margin-top:14px}.section-icon.gold{border-color:rgba(255,190,65,.48);color:#ffd06b;background:rgba(246,171,39,.12)}.competing-panel{position:relative;height:150px;display:grid;grid-template-columns:126px 1.3fr 1fr .8fr 1.15fr;align-items:center;gap:0;padding:0 25px;box-sizing:border-box;border:1px solid rgba(47,178,237,.27);border-radius:17px;background:linear-gradient(100deg,rgba(9,64,146,.74),rgba(3,25,83,.84));overflow:hidden}.competing-panel>div:not(.rope-graphic){position:relative;z-index:1}.competing-mark{height:88px;display:flex;flex-direction:column;align-items:center;justify-content:center;border-right:1px solid rgba(82,154,216,.18);color:#55d9ff}.competing-mark .el-icon{font-size:31px}.competing-mark span{margin-top:8px;font-size:10px;font-weight:700;letter-spacing:.18em}.competition-name,.info-cell{min-width:0;padding:0 23px}.competition-name small,.info-cell small{display:block;margin-bottom:9px;color:#5489b8;font-size:11px}.competition-name strong,.info-cell b{display:block;overflow:hidden;font-size:16px;line-height:1.45;text-overflow:ellipsis;white-space:nowrap}.competition-name strong{color:#72e4ff;font-size:20px}.info-cell{border-left:1px solid rgba(82,154,216,.14)}.athlete-cell b{white-space:normal}.rope-graphic{position:absolute;right:-18px;bottom:-24px;width:190px;height:140px;opacity:.13}.rope-graphic span{position:absolute;inset:0;border:9px solid #24ccff;border-left-color:transparent;border-bottom-color:transparent;border-radius:50%;transform:rotate(-18deg)}.rope-graphic i{position:absolute;bottom:0;width:18px;height:54px;border-radius:9px;background:#28cfff;transform:rotate(40deg)}.rope-graphic i:first-child{left:25px}.rope-graphic i:nth-child(2){right:3px}
.bottom-line{position:absolute;left:34px;right:34px;bottom:15px;display:flex;align-items:center;gap:16px;color:#326b9f;font-size:11px;letter-spacing:.15em}.bottom-line span{height:1px;flex:1;background:linear-gradient(90deg,transparent,#174d81)}.bottom-line span:last-child{transform:scaleX(-1)}.controls{position:absolute;right:22px;bottom:23px;z-index:8;display:flex;align-items:center;gap:8px;padding:7px;border:1px solid rgba(78,204,255,.2);border-radius:12px;background:rgba(2,15,60,.84);backdrop-filter:blur(10px);opacity:0;transform:translateY(8px);transition:.25s;pointer-events:none}.controls.visible{opacity:1;transform:none;pointer-events:auto}.controls button{display:flex;align-items:center;gap:6px;padding:8px 12px;border:1px solid rgba(67,173,237,.25);border-radius:8px;color:#9ddffc;background:rgba(12,75,145,.35);cursor:pointer}.controls>span{display:flex;align-items:center;gap:7px;padding:0 9px;color:#52e9b9;font-size:11px}.controls>span.offline{color:#ff7e86}
@keyframes pulse{50%{opacity:.35;box-shadow:0 0 15px currentColor}}@keyframes card-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}