优化大屏显示
This commit is contained in:
parent
a6f520a787
commit
9aad0a02f1
@ -175,8 +175,19 @@ function normalizeScoringMenuRoutes(routes: any[]) {
|
||||
const routeName = String(item?.name || item?.routeName || '');
|
||||
const menuCode = String(item?.code || item?.meta?.code || '');
|
||||
const title = String(item?.meta?.title || '');
|
||||
const path = String(item?.path || '');
|
||||
const isScoringOrderBookTitle = title === '秩序册' && String(item?.path || '').startsWith('/scoring/');
|
||||
const isOrderBook = routeName === 'scoringOrderBook' || menuCode === 'scoring:orderbook' || isScoringOrderBookTitle;
|
||||
const isScoringCastTitle = title === '成绩投屏' && path.startsWith('/scoring/');
|
||||
const isScoringCast = routeName === 'scoringCast' || menuCode === 'scoring:cast' || path === '/scoring/cast' || isScoringCastTitle;
|
||||
if (isScoringCast) {
|
||||
return {
|
||||
...item,
|
||||
path: '/scoring/cast',
|
||||
component: 'scoring/screen/index',
|
||||
children,
|
||||
};
|
||||
}
|
||||
if (!isOrderBook) return children === item?.children ? item : { ...item, children };
|
||||
return {
|
||||
...item,
|
||||
@ -201,7 +212,7 @@ function ensureScoringOperationalRoutes(routes: any[]) {
|
||||
{ path: '/scoring/launcher', name: 'scoringLauncherFallback', component: 'scoring/launcher/index', meta: { title: '工作台', isKeepAlive: true } },
|
||||
{ path: '/scoring/orderbook', name: 'scoringOrderBookFallback', component: 'scoring/orderbook/index', meta: { title: '秩序册', isHide: true, isKeepAlive: true } },
|
||||
{ path: '/scoring/workbench', name: 'scoringWorkbenchFallback', component: 'scoring/workbench/index', meta: { title: '成绩管理', isHide: true, isKeepAlive: true } },
|
||||
{ path: '/scoring/cast', name: 'scoringCastFallback', component: 'scoring/workbench/index', meta: { title: '成绩投屏', isHide: true, isKeepAlive: true } },
|
||||
{ path: '/scoring/cast', name: 'scoringCastFallback', component: 'scoring/screen/index', meta: { title: '成绩投屏', isHide: true, isKeepAlive: true } },
|
||||
].filter((item) => !paths.has(item.path));
|
||||
return [...routes, ...fallbacks];
|
||||
}
|
||||
|
||||
@ -602,10 +602,19 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
title: '登录',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/scoring/cast',
|
||||
name: 'scoringCast',
|
||||
component: () => import('/@/views/scoring/cast/index.vue'),
|
||||
meta: {
|
||||
title: '赛事大屏',
|
||||
isPublic: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/scoring/screen',
|
||||
name: 'scoringScreen',
|
||||
component: () => import('/@/views/scoring/screen/index.vue'),
|
||||
redirect: (to) => ({ path: '/scoring/cast', query: { ...to.query, castMode: 'score' } }),
|
||||
meta: {
|
||||
title: '成绩大屏',
|
||||
isPublic: true,
|
||||
@ -614,7 +623,7 @@ export const staticRoutes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/scoring/checkin-screen',
|
||||
name: 'scoringCheckinScreen',
|
||||
component: () => import('/@/views/scoring/checkin-screen/index.vue'),
|
||||
redirect: (to) => ({ path: '/scoring/cast', query: { ...to.query, castMode: 'checkin' } }),
|
||||
meta: {
|
||||
title: '检录进度大屏',
|
||||
isPublic: true,
|
||||
|
||||
116
src/views/scoring/cast/index.vue
Normal file
116
src/views/scoring/cast/index.vue
Normal file
@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="scoring-cast-container" @mousemove="showSwitch">
|
||||
<!-- 模式切换浮动按钮 -->
|
||||
<div class="cast-switch-bar" :class="{ visible: switchVisible }">
|
||||
<div class="switch-group">
|
||||
<button :class="{ active: currentMode === 'score' }" @click="switchMode('score')">成绩大屏</button>
|
||||
<button :class="{ active: currentMode === 'checkin' }" @click="switchMode('checkin')">检录大屏</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 大屏内容 -->
|
||||
<ScoreScreen v-if="currentMode === 'score'" />
|
||||
<CheckinScreen v-else-if="currentMode === 'checkin'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="scoringCast">
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import ScoreScreen from '/@/views/scoring/screen/index.vue';
|
||||
import CheckinScreen from '/@/views/scoring/checkin-screen/index.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentMode = ref(route.query.castMode === 'checkin' || route.query.mode === 'checkin' ? 'checkin' : 'score');
|
||||
const switchVisible = ref(true);
|
||||
let hideTimer = null;
|
||||
|
||||
function showSwitch() {
|
||||
switchVisible.value = true;
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(() => {
|
||||
switchVisible.value = false;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
function switchMode(mode) {
|
||||
currentMode.value = mode;
|
||||
router.replace({ query: { ...route.query, castMode: mode } });
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.castMode,
|
||||
(val) => {
|
||||
if (val === 'checkin' || val === 'score') {
|
||||
currentMode.value = val;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
showSwitch();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.scoring-cast-container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cast-switch-bar {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 99999;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.switch-group {
|
||||
display: flex;
|
||||
background: rgba(0, 15, 50, 0.78);
|
||||
border: 1px solid rgba(64, 158, 255, 0.45);
|
||||
border-radius: 20px;
|
||||
padding: 3px;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
|
||||
button {
|
||||
padding: 6px 18px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
background: transparent;
|
||||
color: #b0c8e8;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #1890ff, #0050b3);
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -43,13 +43,15 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { checkinScreenCastGet } from '/@/api/match/workbench';
|
||||
|
||||
const route=useRoute(), eventId=Number(route.query.eventId)||0;
|
||||
const route=useRoute();
|
||||
let savedContext={};try{savedContext=JSON.parse(sessionStorage.getItem('scoring-context')||'{}')}catch{savedContext={}}
|
||||
const eventId=Number(route.query.eventId)||Number(savedContext.eventId)||0;
|
||||
const data=ref({stages:[],units:[]}), offline=ref(false), clock=ref(''), week=ref(''), fullscreen=ref(false), controlsVisible=ref(true);
|
||||
let pollTimer,clockTimer,controlTimer,unitPage=ref(0),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 visibleUnits=computed(()=>{const all=data.value.units||[];const pages=Math.max(1,Math.ceil(all.length/7));const page=unitPage.value%pages;return all.slice(page*7,page*7+7)});
|
||||
const currentClockDate=()=>new Date(serverBaseMs+(Date.now()-syncedAtMs));
|
||||
async function load(){if(!eventId||requesting)return;requesting=true;try{const res=await checkinScreenCastGet({eventId});data.value=res?.data||{stages:[],units:[]};offline.value=false;syncClock()}catch{offline.value=true}finally{requesting=false}}
|
||||
async function load(){if(!eventId||requesting)return;requesting=true;try{const res=await checkinScreenCastGet({eventId,_t:Date.now()});const body=res?.data??res;data.value=body?.data??body??{stages:[],units:[]};offline.value=false;syncClock()}catch{offline.value=true}finally{requesting=false}}
|
||||
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()}
|
||||
function tick(){const now=currentClockDate();clock.value=formatDate(now);week.value=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][now.getDay()]}
|
||||
function formatDate(d){const p=n=>String(n).padStart(2,'0');return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`}
|
||||
|
||||
@ -15,22 +15,24 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { orderBookTemplateGet, orderBookImportPost, orderBookConfirmPost } from '/@/api/match/workbench';
|
||||
import { resolveScoringEvent } from '/@/utils/scoringContext';
|
||||
|
||||
const route=useRoute(),router=useRouter();
|
||||
const scoringEvent=resolveScoringEvent(route.query);
|
||||
const eventId=scoringEvent.eventId,eventName=scoringEvent.eventName;
|
||||
const currentEvent=resolveScoringEvent(route.query);
|
||||
const eventId=ref(currentEvent.eventId),eventName=ref(currentEvent.eventName);
|
||||
const file=ref(null),result=ref(null),parsing=ref(false),confirming=ref(false);
|
||||
if(!eventId)router.replace({path:'/scoring/launcher',query:{sourceMode:'orderbook'}});
|
||||
function changeEvent(){router.push({path:'/scoring/launcher',query:{sourceMode:'orderbook'}})}
|
||||
if(!eventId.value)router.replace({path:'/scoring/launcher',query:{selectEvent:'1'}});
|
||||
function changeEvent(){router.push({path:'/scoring/launcher',query:{selectEvent:'1'}})}
|
||||
function syncEvent(){const next=resolveScoringEvent(route.query);eventId.value=next.eventId;eventName.value=next.eventName;file.value=null;result.value=null}
|
||||
watch(()=>[route.query.eventId,route.query.eventName],syncEvent);
|
||||
function selectFile(uploadFile){file.value=uploadFile.raw;result.value=null}
|
||||
function saveBlob(blob,name){const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url)}
|
||||
async function downloadTemplate(){saveBlob(await orderBookTemplateGet(),'秩序册导入模板.xlsx')}
|
||||
async function parseFile(){parsing.value=true;try{const res=await orderBookImportPost(eventId,file.value);result.value=res?.data||null;if(result.value)ElMessage.success(result.value.errorCount?'解析完成,请处理错误项':'解析通过,请确认导入')}finally{parsing.value=false}}
|
||||
async function parseFile(){parsing.value=true;try{const res=await orderBookImportPost(eventId.value,file.value);result.value=res?.data||null;if(result.value)ElMessage.success(result.value.errorCount?'解析完成,请处理错误项':'解析通过,请确认导入')}finally{parsing.value=false}}
|
||||
async function confirmImport(){await ElMessageBox.confirm('确认导入当前秩序册?同一赛事原有编排数据将按导入规则处理。','确认导入',{type:'warning'});confirming.value=true;try{const res=await orderBookConfirmPost(result.value.batchId);ElMessage.success(`导入完成:${res?.data?.entryCount||0} 条出场记录`);result.value=null;file.value=null}finally{confirming.value=false}}
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@
|
||||
<section class="workbench-main">
|
||||
<header class="topbar">
|
||||
<div><h1>{{ tabTitle }}</h1><p>{{ eventName }} · {{ tabDescription }}</p></div>
|
||||
<div class="top-actions"><span :class="['live-dot', { offline }]">● {{ offline ? '连接异常' : '实时同步' }}</span><el-button v-if="activeTab !== 'screen'" :type="activeTab==='corrections'?'primary':'default'" @click="toggleCorrections">{{activeTab==='corrections'?'返回成绩列表':'修正申请'}}</el-button><el-button @click="openCheckinScreen">检录大屏</el-button><el-button @click="goLauncher">切换赛事</el-button><el-button @click="toggleFullscreen">全屏</el-button></div>
|
||||
<div class="top-actions"><span :class="['live-dot', { offline }]">● {{ offline ? '连接异常' : '实时同步' }}</span><el-button v-if="activeTab !== 'screen'" :type="activeTab==='corrections'?'primary':'default'" @click="toggleCorrections">{{activeTab==='corrections'?'返回成绩列表':'修正申请'}}</el-button><el-button @click="openCheckinScreen">检录大屏</el-button><el-button @click="goLauncher">切换赛事</el-button></div>
|
||||
</header>
|
||||
|
||||
<template v-if="activeTab !== 'corrections'">
|
||||
@ -30,7 +30,7 @@
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
<span class="filter-spacer"></span>
|
||||
<el-button v-if="activeTab === 'screen' || activeTab === 'results'" type="success" :disabled="!rankingOptions.length" @click="openRanking">投出排名</el-button>
|
||||
<el-button v-if="activeTab === 'screen'" @click="openScoreScreen">打开成绩大屏</el-button>
|
||||
<el-button v-if="activeTab === 'screen'" @click="openScoreScreen">全屏显示</el-button>
|
||||
<template v-if="activeTab === 'results'">
|
||||
<el-button type="primary" :loading="actionLoading" :disabled="!selectedResultIds.length" @click="batchConfirm">批量确认</el-button>
|
||||
<el-button @click="exportResults">成绩导出</el-button>
|
||||
@ -163,7 +163,6 @@ function openScoreScreen(){window.open(router.resolve({path:'/scoring/screen',qu
|
||||
function openCheckinScreen(){window.open(router.resolve({path:'/scoring/checkin-screen',query:{eventId:eventId.value}}).href,'_blank')}
|
||||
function goLauncher(){router.push({path:'/scoring/launcher'})}
|
||||
function toggleCorrections(){activeTab.value=activeTab.value==='corrections'?'results':'corrections';if(activeTab.value==='corrections')loadCorrections()}
|
||||
function toggleFullscreen(){if(!document.fullscreenElement)document.documentElement.requestFullscreen?.();else document.exitFullscreen?.()}
|
||||
function startPolling(){pollTimer=window.setInterval(()=>{if(document.visibilityState==='visible'){loadCurrentCast();if(activeTab.value!=='corrections')loadEntries(true)}},5000)}
|
||||
watch(()=>selectedSession.value?.venueId,loadCurrentCast);
|
||||
watch(()=>route.path,(path)=>{activeTab.value=path==='/scoring/cast'?'screen':'results'});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user