add shortcuts traner

This commit is contained in:
2026-10-28 09:36:55 +08:00
parent 0b2072a05f
commit 784a8e3e4b
12 changed files with 1423 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
const defs=[
{key:'P',title:'红色火星站',color:'#ff3b52',light:'#ff8a9b',dark:'#c40f31',deep:'#4a0416',desc:'火星基地监控台:检查氧气、通讯和火箭准备状态。',facts:['氧气储备 82%','基地温度 -63°C','通讯延迟 3.2 秒']},
{key:'Q',title:'蓝色深海舱',color:'#357bff',light:'#8ab4ff',dark:'#1146d6',deep:'#041348',desc:'深海实验舱控制台:观察压载、声呐和探照灯。',facts:['水深 3820 米','舱内压强 1.0 atm','声呐信号正常']},
{key:'R',title:'绿色能量站',color:'#00d164',light:'#7dffb4',dark:'#009e49',deep:'#02321a',desc:'绿色能源调度台:管理风机、储能和供电负载。',facts:['风机功率 46 MW','储能电量 73%','今日减排 128 吨']},
{key:'T',title:'黄色指挥所',color:'#ffdb00',light:'#fff176',dark:'#ff9800',deep:'#4a2c00',desc:'应急指挥所大屏:核对地图、队伍和命令下达。',facts:['在线队伍 6 组','待处理事件 2 件','指挥频道已加密']},
{key:'Y',title:'紫色观测台',color:'#c159ff',light:'#e4a6ff',dark:'#7c3aed',deep:'#22065a',desc:'星空观测台:记录星图、光谱和观测日志。',facts:['目标星等 8.4','光谱类型 K2','曝光时间 30 秒']},
{key:'U',title:'橙色发射场',color:'#ff7a18',light:'#ffb377',dark:'#e03a00',deep:'#451000',desc:'发射场控制台:确认倒计时、气象和外部通讯。',facts:['倒计时 T-12:00','风速 4.6 m/s','发射窗口 14:40']}
];
const pathPage=Number((location.pathname.match(/\/page\/([1-6])\/?$/)||[])[1]);
const no=pathPage||Number(document.body.dataset.trainerPage)||1;
const def=defs[no-1];
document.body.classList.add('training');
document.body.style.setProperty('--page-color',def.color);
document.body.style.setProperty('--page-light',def.light);
document.body.style.setProperty('--page-dark',def.dark);
document.body.style.setProperty('--page-deep',def.deep);
let themeMeta=document.querySelector('meta[name="theme-color"]');
if(!themeMeta){themeMeta=document.createElement('meta');themeMeta.name='theme-color';document.head.appendChild(themeMeta)}
themeMeta.content=def.dark;
document.body.dataset.trainerPage=String(no);
document.title=`${no}. ${def.title}`;
document.getElementById('pageTitle').textContent=`${no}. ${def.title}`;
document.getElementById('pageHeading').textContent=`${no}. ${def.title}`;
document.getElementById('pageDesc').textContent=def.desc;
const facts=document.getElementById('pageFacts');facts.innerHTML='';def.facts.forEach(text=>{const el=document.createElement('div');el.textContent=text;facts.appendChild(el)});
const mark=document.getElementById('mark');mark.textContent=no;mark.style.background='rgba(255,255,255,.18)';mark.style.boxShadow='inset 0 0 0 2px #ffffff70';
const buttons=['火箭准备','燃料检查','通讯开启','系统诊断','备用电源','外部链接'];
const ribbon=document.getElementById('ribbon');
let ribbonKey='K';
function openLink(item){
window._linkMenu=false;
window._panel=null;
const popup=window.open(item.url,'_blank','noopener');
renderRibbon('B',false);
flash(popup?`正在打开 ${item.name}`:'浏览器拦截了弹窗,请允许本站弹出式窗口');
reportEvent({type:'url',url:item.url,page:no});
}
function openLinkMenu(){
window._panel=null;
window._linkMenu=true;
renderRibbon('B',true);
flash('请按 D 打开百度,或按 F 打开 Bilibili(不必按着 Alt)');
}
function activateButton(index,via='click'){
const name=buttons[index-1];
if(name==='外部链接'){
flash(via==='key'?'已打开外部链接菜单':'已点击:外部链接');
reportEvent({type:'button',page:no,index,name});
openLinkMenu();
return;
}
flash(via==='key'?`快捷键已触发:${index}. ${name}`:`已点击:${name}`);
reportEvent({type:'button',page:no,index,name});
}
function renderRibbon(activeKey=ribbonKey,keyboardOn=!!window._panel||!!window._linkMenu){
ribbonKey=activeKey;
ribbon.className='ribbon'+(keyboardOn?' keyboard-on':'');
ribbon.innerHTML='';
const tabs=document.createElement('div');
tabs.className='ribbon-tabs';
RIBBON_PANELS.forEach(panel=>{
const tab=document.createElement('button');
tab.type='button';
tab.className='ribbon-tab'+(panel.key===activeKey?' active':'')+(window._panel===panel.key||(window._linkMenu&&panel.key==='B')?' armed':'');
tab.innerHTML=`<span class="keytip">Alt+${panel.key}</span><strong>${panel.title}</strong>`;
tab.onclick=()=>{window._panel=null;window._linkMenu=false;renderRibbon(panel.key,false)};
tabs.appendChild(tab);
});
const body=document.createElement('div');
body.className='ribbon-body';
if(window._linkMenu){
const pane=document.createElement('div');
pane.className='ribbon-panel active';
const title=document.createElement('div');
title.className='ribbon-subhint';
title.textContent='外部链接:按 D 打开百度,按 F 打开 Bilibili';
body.appendChild(title);
LINK_TARGETS.forEach(item=>{
const b=document.createElement('button');
b.type='button';
b.className='ribbon-cmd '+(item.key==='A'?'success':'ghost');
b.innerHTML=`<span class="keytip">${item.key}</span><span>${item.name}</span>`;
b.onclick=()=>openLink(item);
pane.appendChild(b);
});
body.appendChild(pane);
}else{
RIBBON_PANELS.forEach(panel=>{
const pane=document.createElement('div');
pane.className='ribbon-panel'+(panel.key===activeKey?' active':'');
panel.items.forEach(item=>{
const b=document.createElement('button');
b.type='button';
b.className='ribbon-cmd '+(item.index%2?'success':'ghost');
b.innerHTML=`<span class="keytip">${item.key}</span><span>${item.index}. ${item.name}</span>`;
b.onclick=()=>{window._panel=null;window._linkMenu=false;renderRibbon(panel.key,false);activateButton(item.index,'click')};
pane.appendChild(b);
});
body.appendChild(pane);
});
}
ribbon.appendChild(tabs);
ribbon.appendChild(body);
}
function openRibbonPanel(key){
const panel=RIBBON_PANELS.find(item=>item.key===key);
if(!panel)return;
window._linkMenu=false;
window._panel=key;
renderRibbon(key,true);
const letters=panel.items.map(item=>item.key).join(' 或 ');
flash(`已展开「${panel.title}」面板,请按 ${letters}`);
}
renderRibbon('K',false);
(async()=>{const s=await API.get('/api/student/state');if(s.authenticated){await applyStudentState(s);setInterval(refreshStudent,1000)}else location.href='/'})();
reportEvent({type:'page',page:no});
const ctx=document.getElementById('contextMenu');
document.addEventListener('keydown',e=>{
if(e.repeat)return;
const k=keyName(e);
const menu=!ctx.classList.contains('hidden');
if(menu){const map={E:'refresh',R:'rename',C:'copy'};if(map[k]){consumeKey(e);ctx.classList.add('hidden');flash(`快捷菜单:${map[k]}`);reportEvent({type:'menu',item:map[k],page:no});return}}
if(window._linkMenu){
const item=linkByKey(k);
if(item){consumeKey(e);openLink(item);return}
if(k==='ESCAPE'){consumeKey(e);window._linkMenu=false;renderRibbon('B',false);return}
if(!e.altKey){consumeKey(e);return}
}
if(e.altKey&&!e.ctrlKey&&!e.metaKey&&!e.shiftKey&&/^[1-6]$/.test(k)){consumeKey(e);window._panel=null;window._linkMenu=false;renderRibbon(ribbonKey,false);activateButton(Number(k),'key');return}
if(e.altKey&&!e.ctrlKey&&!e.metaKey&&RIBBON_PANELS.some(panel=>panel.key===k)){consumeKey(e);openRibbonPanel(k);return}
if(window._panel){
const panel=RIBBON_PANELS.find(item=>item.key===window._panel);
const item=panel.items.find(cmd=>cmd.key===k);
if(item){consumeKey(e);window._panel=null;renderRibbon(panel.key,false);activateButton(item.index,'key');return}
if(k==='ESCAPE'){consumeKey(e);window._panel=null;renderRibbon(panel.key,false);return}
}
},true);
document.addEventListener('contextmenu',e=>{e.preventDefault();ctx.style.left=Math.min(e.clientX,innerWidth-210)+'px';ctx.style.top=Math.min(e.clientY,innerHeight-160)+'px';ctx.classList.remove('hidden')});
document.addEventListener('click',e=>{if(!ctx.contains(e.target))ctx.classList.add('hidden');if((window._panel||window._linkMenu)&&ribbon&&!ribbon.contains(e.target)){window._panel=null;window._linkMenu=false;renderRibbon(ribbonKey,false)}});
document.querySelectorAll('[data-menu]').forEach(b=>b.onclick=async()=>{ctx.classList.add('hidden');flash('快捷菜单:'+b.dataset.menu);await reportEvent({type:'menu',item:b.dataset.menu,page:no})});