index.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  6. <title>考试投屏时钟</title>
  7. <style>
  8. :root{
  9. --black:#050507;
  10. --white:#fff;
  11. --blue:#2997ff;
  12. --blue2:#0071e3;
  13. --text:rgba(255,255,255,.96);
  14. --sub:rgba(255,255,255,.68);
  15. --muted:rgba(255,255,255,.48);
  16. --glass:rgba(18,18,22,.56);
  17. --glass2:rgba(255,255,255,.08);
  18. --line:rgba(255,255,255,.14);
  19. --ok:#30d158;
  20. --warn:#ffd60a;
  21. --danger:#ff453a;
  22. }
  23. *{box-sizing:border-box}
  24. html,body{height:100%;margin:0}
  25. body{
  26. min-height:100%;
  27. overflow:hidden;
  28. color:var(--text);
  29. font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",Arial,sans-serif;
  30. background:#000;
  31. }
  32. body:before{
  33. content:"";
  34. position:fixed;inset:0;
  35. background:
  36. radial-gradient(circle at 50% -12%, rgba(41,151,255,.62), transparent 34%),
  37. radial-gradient(circle at 12% 30%, rgba(120,119,198,.30), transparent 30%),
  38. radial-gradient(circle at 88% 72%, rgba(48,209,88,.18), transparent 30%),
  39. linear-gradient(180deg,#08080c 0%,#000 58%,#08080b 100%);
  40. z-index:-3;
  41. }
  42. body:after{
  43. content:"";
  44. position:fixed;inset:0;
  45. background:
  46. linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
  47. linear-gradient(90deg, rgba(255,255,255,.028) 1px, transparent 1px);
  48. background-size:64px 64px;
  49. mask-image:radial-gradient(circle at center, black 0%, transparent 72%);
  50. z-index:-2;
  51. pointer-events:none;
  52. }
  53. .orb{position:fixed;border-radius:999px;filter:blur(12px);opacity:.62;z-index:-1;animation:float 10s ease-in-out infinite alternate}
  54. .orb.one{width:20vw;height:20vw;left:7vw;top:12vh;background:rgba(41,151,255,.24)}
  55. .orb.two{width:16vw;height:16vw;right:10vw;bottom:12vh;background:rgba(48,209,88,.14);animation-delay:-3s}
  56. @keyframes float{to{transform:translate3d(2vw,2vh,0) scale(1.08)}}
  57. .screen{height:100%;display:grid;grid-template-rows:auto 1fr auto;padding:clamp(18px,3.2vw,48px);gap:clamp(14px,2vw,28px)}
  58. .nav,.dock{
  59. display:flex;align-items:center;justify-content:space-between;gap:18px;
  60. min-height:58px;padding:10px 14px;border:1px solid var(--line);border-radius:999px;
  61. background:var(--glass);backdrop-filter:saturate(180%) blur(26px);-webkit-backdrop-filter:saturate(180%) blur(26px);
  62. box-shadow:0 18px 60px rgba(0,0,0,.26), inset 0 1px 0 rgba(255,255,255,.10);
  63. }
  64. .brand{display:flex;align-items:center;gap:12px;min-width:0}
  65. .logo{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(145deg,#fff,#b8ddff);color:#06101f;font-weight:800;box-shadow:0 0 34px rgba(41,151,255,.42)}
  66. .brand-title{font-size:clamp(15px,1.4vw,22px);font-weight:650;letter-spacing:-.02em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  67. .pill{display:inline-flex;align-items:center;gap:8px;border-radius:999px;padding:10px 16px;background:var(--glass2);border:1px solid var(--line);color:var(--sub);font-size:clamp(13px,1.15vw,18px);white-space:nowrap}
  68. .dot{width:8px;height:8px;border-radius:50%;background:var(--ok);box-shadow:0 0 20px var(--ok)}
  69. .hero{display:grid;place-items:center;text-align:center;min-height:0}
  70. .hero-inner{width:min(1500px,100%)}
  71. .eyebrow{margin:0 0 clamp(10px,1.4vw,22px);font-size:clamp(20px,2.25vw,40px);font-weight:520;color:var(--sub);letter-spacing:.16em;text-transform:uppercase}
  72. .clock-card{position:relative;margin:auto;padding:clamp(22px,4.8vw,72px) clamp(18px,5vw,82px);border-radius:clamp(28px,4vw,58px);background:linear-gradient(180deg,rgba(255,255,255,.14),rgba(255,255,255,.045));border:1px solid rgba(255,255,255,.18);box-shadow:0 34px 120px rgba(0,0,0,.42), inset 0 1px 0 rgba(255,255,255,.18);backdrop-filter:blur(28px);overflow:hidden}
  73. .clock-card:before{content:"";position:absolute;inset:-2px;background:linear-gradient(115deg,transparent 0 20%,rgba(255,255,255,.20) 35%,transparent 52%);transform:translateX(-55%);animation:shine 8s ease-in-out infinite;pointer-events:none}
  74. @keyframes shine{55%,100%{transform:translateX(75%)}}
  75. .clock{position:relative;margin:0;font-family:"DIN Alternate","Arial Narrow","Helvetica Neue",Arial,"PingFang SC","Microsoft YaHei",sans-serif;font-variant-numeric:tabular-nums;font-size:clamp(88px,17.2vw,268px);font-weight:700;line-height:.92;letter-spacing:.018em;word-spacing:.03em;text-shadow:0 12px 44px rgba(0,0,0,.32)}
  76. .seconds{display:inline;color:inherit;font-size:1em;margin-left:0;letter-spacing:inherit;vertical-align:baseline;text-shadow:inherit}
  77. .date{margin:clamp(16px,2vw,30px) 0 0;color:var(--sub);font-size:clamp(22px,2.6vw,46px);font-weight:500;letter-spacing:.06em}
  78. .timer{display:none;width:min(1160px,96%);margin:clamp(22px,3vh,44px) auto 0;padding:clamp(18px,2.6vw,34px);border-radius:32px;background:rgba(0,0,0,.38);border:1px solid var(--line);box-shadow:inset 0 1px 0 rgba(255,255,255,.10)}
  79. .timer-top{display:grid;grid-template-columns:1fr auto;align-items:end;gap:18px;text-align:left}
  80. .timer-label{font-size:clamp(18px,1.8vw,32px);color:var(--sub);letter-spacing:.12em;font-weight:620}
  81. .remain{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;font-variant-numeric:tabular-nums;font-size:clamp(44px,7.2vw,116px);font-weight:850;line-height:.9;letter-spacing:-.06em}
  82. .progress{height:clamp(12px,1vw,18px);margin-top:22px;border-radius:999px;background:rgba(255,255,255,.12);overflow:hidden;box-shadow:inset 0 1px 4px rgba(0,0,0,.4)}
  83. .bar{height:100%;width:100%;border-radius:999px;background:linear-gradient(90deg,var(--ok),var(--blue));box-shadow:0 0 34px rgba(41,151,255,.42);transition:width .35s linear,background .3s ease}
  84. .dock{border-radius:28px;padding:14px 18px;color:var(--sub)}
  85. .help{font-size:clamp(13px,1.2vw,18px);line-height:1.65}.help b{color:#fff;font-weight:650}
  86. .actions{display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end;align-items:center}
  87. button,a.btn{appearance:none;text-decoration:none;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.10);color:#fff;border-radius:999px;padding:12px 18px;font-size:15px;line-height:1;cursor:pointer;transition:.18s ease;box-shadow:inset 0 1px 0 rgba(255,255,255,.12)}
  88. .title-input{width:190px;max-width:32vw;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.10);color:#fff;border-radius:999px;padding:11px 16px;font-size:15px;line-height:1;outline:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.12);transition:.18s ease}
  89. .title-input::placeholder{color:rgba(255,255,255,.58)}
  90. .title-input:focus{border-color:rgba(41,151,255,.75);background:rgba(255,255,255,.16);box-shadow:0 0 0 3px rgba(41,151,255,.16), inset 0 1px 0 rgba(255,255,255,.12)}
  91. button.primary,a.primary{background:var(--blue2);border-color:var(--blue2);box-shadow:0 12px 36px rgba(0,113,227,.30)}
  92. button:hover,a.btn:hover{transform:translateY(-1px);background:rgba(255,255,255,.18)}button.primary:hover,a.primary:hover{background:#147ce5}
  93. body.warn .bar{background:linear-gradient(90deg,var(--warn),#ff9f0a);box-shadow:0 0 34px rgba(255,214,10,.34)}
  94. body.danger .bar{background:linear-gradient(90deg,var(--danger),#ff9f0a);box-shadow:0 0 38px rgba(255,69,58,.46)}
  95. body.danger .remain,body.danger .seconds{color:var(--danger);text-shadow:0 0 36px rgba(255,69,58,.52)}
  96. body.done .remain{color:var(--danger);letter-spacing:-.02em}
  97. @media(max-width:760px){
  98. body{overflow:auto}.screen{min-height:100%;height:auto}.nav,.dock{border-radius:24px;align-items:flex-start}.pill.hide-sm{display:none}.timer-top{grid-template-columns:1fr;text-align:center}.timer-label{text-align:center}.dock{display:block;text-align:center}.actions{justify-content:center;margin-top:14px}.title-input{width:100%;max-width:280px}.clock-card{border-radius:30px}.clock{font-size:clamp(78px,22vw,160px)}
  99. }
  100. </style>
  101. </head>
  102. <body>
  103. <div class="orb one"></div><div class="orb two"></div>
  104. <div class="screen">
  105. <header class="nav">
  106. <div class="brand"><div class="logo">考</div><div class="brand-title" id="examTitle">考试投屏时钟</div></div>
  107. <div style="display:flex;gap:10px;align-items:center;min-width:0">
  108. <div class="pill hide-sm"><span class="dot"></span><span id="statusText">北京时间 · 实时同步</span></div>
  109. <div class="pill" id="ampm">--</div>
  110. </div>
  111. </header>
  112. <main class="hero">
  113. <div class="hero-inner">
  114. <p class="eyebrow" id="pageTitle">EXAM CLOCK</p>
  115. <section class="clock-card">
  116. <p class="clock"><span id="hhmm">--:--</span>:<span class="seconds" id="ss">--</span></p>
  117. <div class="date" id="dateText">----</div>
  118. </section>
  119. <section class="timer" id="timerPanel">
  120. <div class="timer-top">
  121. <div class="timer-label" id="timerLabel">距离考试结束</div>
  122. <div class="remain" id="remainText">--:--:--</div>
  123. </div>
  124. <div class="progress"><div class="bar" id="bar"></div></div>
  125. </section>
  126. </div>
  127. </main>
  128. <footer class="dock">
  129. <div class="help" id="hintText">投屏建议点击 <b>全屏显示</b>;地址后加 <b>?duration=120</b> 可显示 120 分钟倒计时。</div>
  130. <div class="actions">
  131. <input class="title-input" id="titleInput" type="text" maxlength="30" placeholder="输入标题" autocomplete="off">
  132. <button type="button" onclick="applyTitle()">添加标题</button>
  133. <button class="primary" type="button" onclick="toggleFullScreen()">全屏显示</button>
  134. <a class="btn" href="?duration=120">120分钟倒计时</a>
  135. <button type="button" onclick="location.href=location.pathname">只显示时钟</button>
  136. </div>
  137. </footer>
  138. </div>
  139. <script>
  140. const $=(id)=>document.getElementById(id);
  141. const params=new URLSearchParams(location.search);
  142. const title=params.get('title')||'考试投屏时钟';
  143. const label=params.get('label')||'距离考试结束';
  144. const durationMin=Number(params.get('duration')||0);
  145. const endParam=params.get('end');
  146. const startTs=Date.now();
  147. let endTs=null;
  148. $('examTitle').textContent=title; document.title=title; $('timerLabel').textContent=label;
  149. $('titleInput').value=params.get('title')||'';
  150. if(params.get('title')) $('pageTitle').textContent=title; else $('pageTitle').textContent='EXAM CLOCK';
  151. if(endParam){const parsed=new Date(endParam.replace(' ','T')).getTime(); if(!Number.isNaN(parsed)) endTs=parsed;}
  152. else if(durationMin>0){endTs=startTs+durationMin*60*1000;}
  153. if(endTs){$('timerPanel').style.display='block';$('hintText').innerHTML='倒计时已开启;可用参数:<b>?duration=分钟&title=标题</b>,或点击全屏投放。';}
  154. function pad(n){return String(n).padStart(2,'0')}
  155. function formatRemain(ms){const total=Math.max(0,Math.ceil(ms/1000));const h=Math.floor(total/3600),m=Math.floor((total%3600)/60),s=total%60;return `${pad(h)}:${pad(m)}:${pad(s)}`}
  156. function tick(){
  157. const now=new Date(),w=['星期日','星期一','星期二','星期三','星期四','星期五','星期六'];
  158. $('hhmm').textContent=`${pad(now.getHours())}:${pad(now.getMinutes())}`;$('ss').textContent=pad(now.getSeconds());
  159. $('dateText').textContent=`${now.getFullYear()}年${pad(now.getMonth()+1)}月${pad(now.getDate())}日 ${w[now.getDay()]}`;
  160. $('ampm').textContent=now.getHours()<12?'上午':'下午';
  161. if(endTs){const remain=endTs-now.getTime(),total=Math.max(1,endTs-startTs),pct=Math.max(0,Math.min(100,remain/total*100));
  162. $('remainText').textContent=remain<=0?'考试结束':formatRemain(remain);$('bar').style.width=pct+'%';
  163. document.body.classList.toggle('warn',pct<=25&&pct>10);document.body.classList.toggle('danger',pct<=10&&pct>0);document.body.classList.toggle('done',remain<=0);
  164. $('statusText').textContent=remain<=0?'考试已结束':'倒计时进行中';
  165. }
  166. }
  167. function applyTitle(){
  168. const value=$('titleInput').value.trim();
  169. const url=new URL(location.href);
  170. if(value) url.searchParams.set('title',value); else url.searchParams.delete('title');
  171. location.href=url.pathname+url.search;
  172. }
  173. $('titleInput').addEventListener('keydown',function(e){if(e.key==='Enter')applyTitle();});
  174. function toggleFullScreen(){if(!document.fullscreenElement)document.documentElement.requestFullscreen?.();else document.exitFullscreen?.();}
  175. tick();setInterval(tick,250);
  176. </script>
  177. </body>
  178. </html>