deck_index.html 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Deck · Multi-file Slide Index</title>
  6. <!--
  7. deck_index.html — 多文件 slide deck 的拼接器
  8. 配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
  9. · 每页独立作用域(CSS/JS 都隔离),一页出 bug 不影响其他页
  10. · 单页可直接在浏览器打开验证,不依赖 JS goTo()
  11. · 多 agent 可并行做不同页,merge 时零冲突
  12. · 适合 ≥15 页的讲座/课件/长 deck
  13. 用法:
  14. 1. 把本文件复制到 deck 根目录,重命名 index.html
  15. 2. 在同目录建 slides/ 子目录,放每一页独立 HTML
  16. 3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
  17. 4. 每张 slide HTML 建议尺寸 1920×1080,自带背景/字体;不要依赖外层 CSS
  18. 共享资源(如果需要):
  19. · shared/tokens.css — 跨页 CSS 变量(色板/字号)
  20. · shared/chrome.html — 页眉页脚可复用片段
  21. · 每页 HTML 自己 <link> 进去即可
  22. 键盘:← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印
  23. -->
  24. <!-- ═══════════════════════════════════════════════════════ -->
  25. <!-- EDIT THIS — deck 所有页按顺序列出 -->
  26. <!-- ═══════════════════════════════════════════════════════ -->
  27. <script>
  28. window.DECK_MANIFEST = [
  29. { file: "slides/01-cover.html", label: "Cover" },
  30. { file: "slides/02-quote.html", label: "Opening Quote" },
  31. { file: "slides/03-intro.html", label: "Self-intro" },
  32. // 继续往下加。file 是相对本文件的路径,label 用于计数器
  33. ];
  34. // 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
  35. window.DECK_WIDTH = 1920;
  36. window.DECK_HEIGHT = 1080;
  37. </script>
  38. <style>
  39. * { box-sizing: border-box; margin: 0; padding: 0; }
  40. html, body {
  41. height: 100%;
  42. background: #0a0a0a;
  43. overflow: hidden;
  44. font-family: -apple-system, "PingFang SC", sans-serif;
  45. }
  46. #stage {
  47. position: fixed;
  48. top: 50%; left: 50%;
  49. transform-origin: top left;
  50. will-change: transform;
  51. background: #fff;
  52. box-shadow: 0 10px 60px rgba(0,0,0,0.4);
  53. /* size set by JS from DECK_WIDTH/HEIGHT */
  54. }
  55. iframe {
  56. width: 100%;
  57. height: 100%;
  58. border: 0;
  59. display: block;
  60. background: #fff;
  61. }
  62. .counter {
  63. position: fixed;
  64. bottom: 20px;
  65. right: 20px;
  66. background: rgba(0,0,0,0.65);
  67. color: #fff;
  68. padding: 6px 14px;
  69. border-radius: 999px;
  70. font-size: 13px;
  71. letter-spacing: 0.05em;
  72. font-variant-numeric: tabular-nums;
  73. z-index: 100;
  74. user-select: none;
  75. opacity: 0.7;
  76. transition: opacity 0.2s;
  77. }
  78. .counter:hover { opacity: 1; }
  79. .counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
  80. .nav-zone {
  81. position: fixed;
  82. top: 0; bottom: 0;
  83. width: 15%;
  84. cursor: pointer;
  85. z-index: 50;
  86. }
  87. .nav-zone.left { left: 0; }
  88. .nav-zone.right { right: 0; }
  89. .nav-hint {
  90. position: absolute;
  91. top: 50%; transform: translateY(-50%);
  92. width: 44px; height: 44px;
  93. border-radius: 999px;
  94. background: rgba(255,255,255,0.08);
  95. color: rgba(255,255,255,0.6);
  96. display: flex;
  97. align-items: center;
  98. justify-content: center;
  99. font-size: 22px;
  100. opacity: 0;
  101. transition: opacity 0.2s;
  102. }
  103. .nav-zone.left .nav-hint { left: 20px; }
  104. .nav-zone.right .nav-hint { right: 20px; }
  105. .nav-zone:hover .nav-hint { opacity: 1; }
  106. /* Print: one slide per page, no navigation UI */
  107. @media print {
  108. @page { size: 1920px 1080px; margin: 0; }
  109. html, body { background: #fff; overflow: visible; height: auto; }
  110. #stage { position: static; transform: none !important; box-shadow: none; }
  111. .counter, .nav-zone { display: none !important; }
  112. /* In print mode we render all slides sequentially — see JS */
  113. .print-stack { display: block; }
  114. .print-stack iframe {
  115. width: 1920px;
  116. height: 1080px;
  117. page-break-after: always;
  118. display: block;
  119. }
  120. }
  121. </style>
  122. </head>
  123. <body>
  124. <div id="stage">
  125. <iframe id="frame" src="about:blank"></iframe>
  126. </div>
  127. <div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
  128. <div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
  129. <div class="counter" id="counter">1 / 1</div>
  130. <!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
  131. <div class="print-stack" id="printStack" style="display:none;"></div>
  132. <script>
  133. (function () {
  134. const W = window.DECK_WIDTH || 1920;
  135. const H = window.DECK_HEIGHT || 1080;
  136. const deck = window.DECK_MANIFEST || [];
  137. const stage = document.getElementById('stage');
  138. const frame = document.getElementById('frame');
  139. const counter = document.getElementById('counter');
  140. const printStack = document.getElementById('printStack');
  141. const storageKey = 'deck-index-' + location.pathname;
  142. let current = 0;
  143. stage.style.width = W + 'px';
  144. stage.style.height = H + 'px';
  145. function fit() {
  146. const s = Math.min(window.innerWidth / W, window.innerHeight / H);
  147. const x = (window.innerWidth - W * s) / 2;
  148. const y = (window.innerHeight - H * s) / 2;
  149. stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
  150. stage.style.top = '0';
  151. stage.style.left = '0';
  152. }
  153. function show(idx) {
  154. if (idx < 0 || idx >= deck.length) return;
  155. current = idx;
  156. frame.src = deck[idx].file;
  157. counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
  158. try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
  159. if (location.hash !== '#' + (idx + 1)) {
  160. history.replaceState(null, '', '#' + (idx + 1));
  161. }
  162. }
  163. function next() { show(Math.min(current + 1, deck.length - 1)); }
  164. function prev() { show(Math.max(current - 1, 0)); }
  165. // Keyboard
  166. document.addEventListener('keydown', (e) => {
  167. if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
  168. switch (e.key) {
  169. case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
  170. case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
  171. case 'Home': e.preventDefault(); show(0); break;
  172. case 'End': e.preventDefault(); show(deck.length - 1); break;
  173. case 'p': case 'P': window.print(); break;
  174. default:
  175. if (e.key >= '1' && e.key <= '9') {
  176. const i = parseInt(e.key, 10) - 1;
  177. if (i < deck.length) { e.preventDefault(); show(i); }
  178. }
  179. }
  180. });
  181. document.getElementById('navL').addEventListener('click', prev);
  182. document.getElementById('navR').addEventListener('click', next);
  183. window.addEventListener('resize', fit);
  184. window.addEventListener('hashchange', () => {
  185. const m = location.hash.match(/^#(\d+)$/);
  186. if (m) show(parseInt(m[1], 10) - 1);
  187. });
  188. // Initial: hash > localStorage > 0
  189. const hashMatch = location.hash.match(/^#(\d+)$/);
  190. if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
  191. else try {
  192. const v = parseInt(localStorage.getItem(storageKey), 10);
  193. if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
  194. } catch (_) {}
  195. fit();
  196. show(current);
  197. // Print: build a stack of all iframes so browser prints every slide
  198. window.addEventListener('beforeprint', () => {
  199. printStack.innerHTML = '';
  200. deck.forEach(item => {
  201. const f = document.createElement('iframe');
  202. f.src = item.file;
  203. printStack.appendChild(f);
  204. });
  205. printStack.style.display = 'block';
  206. document.getElementById('stage').style.display = 'none';
  207. });
  208. window.addEventListener('afterprint', () => {
  209. printStack.innerHTML = '';
  210. printStack.style.display = 'none';
  211. document.getElementById('stage').style.display = '';
  212. });
  213. })();
  214. </script>
  215. </body>
  216. </html>