| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <title>Deck · Multi-file Slide Index</title>
- <!--
- deck_index.html — 多文件 slide deck 的拼接器
- 配合「每页一个独立 HTML」架构使用。与单文件 deck_stage.js 对比:
- · 每页独立作用域(CSS/JS 都隔离),一页出 bug 不影响其他页
- · 单页可直接在浏览器打开验证,不依赖 JS goTo()
- · 多 agent 可并行做不同页,merge 时零冲突
- · 适合 ≥15 页的讲座/课件/长 deck
- 用法:
- 1. 把本文件复制到 deck 根目录,重命名 index.html
- 2. 在同目录建 slides/ 子目录,放每一页独立 HTML
- 3. 编辑下方 MANIFEST 数组,按顺序列出文件名和人类可读标签
- 4. 每张 slide HTML 建议尺寸 1920×1080,自带背景/字体;不要依赖外层 CSS
- 共享资源(如果需要):
- · shared/tokens.css — 跨页 CSS 变量(色板/字号)
- · shared/chrome.html — 页眉页脚可复用片段
- · 每页 HTML 自己 <link> 进去即可
- 键盘:← / → / Space / PgUp / PgDown / Home / End / 1-9 跳页 / P 打印
- -->
- <!-- ═══════════════════════════════════════════════════════ -->
- <!-- EDIT THIS — deck 所有页按顺序列出 -->
- <!-- ═══════════════════════════════════════════════════════ -->
- <script>
- window.DECK_MANIFEST = [
- { file: "slides/01-cover.html", label: "Cover" },
- { file: "slides/02-quote.html", label: "Opening Quote" },
- { file: "slides/03-intro.html", label: "Self-intro" },
- // 继续往下加。file 是相对本文件的路径,label 用于计数器
- ];
- // 固定 canvas 尺寸。每页 HTML 都应该按这个尺寸设计。
- window.DECK_WIDTH = 1920;
- window.DECK_HEIGHT = 1080;
- </script>
- <style>
- * { box-sizing: border-box; margin: 0; padding: 0; }
- html, body {
- height: 100%;
- background: #0a0a0a;
- overflow: hidden;
- font-family: -apple-system, "PingFang SC", sans-serif;
- }
- #stage {
- position: fixed;
- top: 50%; left: 50%;
- transform-origin: top left;
- will-change: transform;
- background: #fff;
- box-shadow: 0 10px 60px rgba(0,0,0,0.4);
- /* size set by JS from DECK_WIDTH/HEIGHT */
- }
- iframe {
- width: 100%;
- height: 100%;
- border: 0;
- display: block;
- background: #fff;
- }
- .counter {
- position: fixed;
- bottom: 20px;
- right: 20px;
- background: rgba(0,0,0,0.65);
- color: #fff;
- padding: 6px 14px;
- border-radius: 999px;
- font-size: 13px;
- letter-spacing: 0.05em;
- font-variant-numeric: tabular-nums;
- z-index: 100;
- user-select: none;
- opacity: 0.7;
- transition: opacity 0.2s;
- }
- .counter:hover { opacity: 1; }
- .counter .label { color: rgba(255,255,255,0.7); margin-left: 8px; }
- .nav-zone {
- position: fixed;
- top: 0; bottom: 0;
- width: 15%;
- cursor: pointer;
- z-index: 50;
- }
- .nav-zone.left { left: 0; }
- .nav-zone.right { right: 0; }
- .nav-hint {
- position: absolute;
- top: 50%; transform: translateY(-50%);
- width: 44px; height: 44px;
- border-radius: 999px;
- background: rgba(255,255,255,0.08);
- color: rgba(255,255,255,0.6);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22px;
- opacity: 0;
- transition: opacity 0.2s;
- }
- .nav-zone.left .nav-hint { left: 20px; }
- .nav-zone.right .nav-hint { right: 20px; }
- .nav-zone:hover .nav-hint { opacity: 1; }
- /* Print: one slide per page, no navigation UI */
- @media print {
- @page { size: 1920px 1080px; margin: 0; }
- html, body { background: #fff; overflow: visible; height: auto; }
- #stage { position: static; transform: none !important; box-shadow: none; }
- .counter, .nav-zone { display: none !important; }
- /* In print mode we render all slides sequentially — see JS */
- .print-stack { display: block; }
- .print-stack iframe {
- width: 1920px;
- height: 1080px;
- page-break-after: always;
- display: block;
- }
- }
- </style>
- </head>
- <body>
- <div id="stage">
- <iframe id="frame" src="about:blank"></iframe>
- </div>
- <div class="nav-zone left" id="navL"><div class="nav-hint">‹</div></div>
- <div class="nav-zone right" id="navR"><div class="nav-hint">›</div></div>
- <div class="counter" id="counter">1 / 1</div>
- <!-- Print-only stack: populated on beforeprint, stripped on afterprint -->
- <div class="print-stack" id="printStack" style="display:none;"></div>
- <script>
- (function () {
- const W = window.DECK_WIDTH || 1920;
- const H = window.DECK_HEIGHT || 1080;
- const deck = window.DECK_MANIFEST || [];
- const stage = document.getElementById('stage');
- const frame = document.getElementById('frame');
- const counter = document.getElementById('counter');
- const printStack = document.getElementById('printStack');
- const storageKey = 'deck-index-' + location.pathname;
- let current = 0;
- stage.style.width = W + 'px';
- stage.style.height = H + 'px';
- function fit() {
- const s = Math.min(window.innerWidth / W, window.innerHeight / H);
- const x = (window.innerWidth - W * s) / 2;
- const y = (window.innerHeight - H * s) / 2;
- stage.style.transform = `translate(${x}px, ${y}px) scale(${s})`;
- stage.style.top = '0';
- stage.style.left = '0';
- }
- function show(idx) {
- if (idx < 0 || idx >= deck.length) return;
- current = idx;
- frame.src = deck[idx].file;
- counter.innerHTML = `${idx + 1} / ${deck.length} <span class="label">${deck[idx].label || ''}</span>`;
- try { localStorage.setItem(storageKey, String(idx)); } catch (_) {}
- if (location.hash !== '#' + (idx + 1)) {
- history.replaceState(null, '', '#' + (idx + 1));
- }
- }
- function next() { show(Math.min(current + 1, deck.length - 1)); }
- function prev() { show(Math.max(current - 1, 0)); }
- // Keyboard
- document.addEventListener('keydown', (e) => {
- if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
- switch (e.key) {
- case 'ArrowRight': case ' ': case 'PageDown': e.preventDefault(); next(); break;
- case 'ArrowLeft': case 'PageUp': e.preventDefault(); prev(); break;
- case 'Home': e.preventDefault(); show(0); break;
- case 'End': e.preventDefault(); show(deck.length - 1); break;
- case 'p': case 'P': window.print(); break;
- default:
- if (e.key >= '1' && e.key <= '9') {
- const i = parseInt(e.key, 10) - 1;
- if (i < deck.length) { e.preventDefault(); show(i); }
- }
- }
- });
- document.getElementById('navL').addEventListener('click', prev);
- document.getElementById('navR').addEventListener('click', next);
- window.addEventListener('resize', fit);
- window.addEventListener('hashchange', () => {
- const m = location.hash.match(/^#(\d+)$/);
- if (m) show(parseInt(m[1], 10) - 1);
- });
- // Initial: hash > localStorage > 0
- const hashMatch = location.hash.match(/^#(\d+)$/);
- if (hashMatch) current = Math.min(parseInt(hashMatch[1], 10) - 1, deck.length - 1);
- else try {
- const v = parseInt(localStorage.getItem(storageKey), 10);
- if (!isNaN(v) && v >= 0 && v < deck.length) current = v;
- } catch (_) {}
- fit();
- show(current);
- // Print: build a stack of all iframes so browser prints every slide
- window.addEventListener('beforeprint', () => {
- printStack.innerHTML = '';
- deck.forEach(item => {
- const f = document.createElement('iframe');
- f.src = item.file;
- printStack.appendChild(f);
- });
- printStack.style.display = 'block';
- document.getElementById('stage').style.display = 'none';
- });
- window.addEventListener('afterprint', () => {
- printStack.innerHTML = '';
- printStack.style.display = 'none';
- document.getElementById('stage').style.display = '';
- });
- })();
- </script>
- </body>
- </html>
|