238 lines
8.0 KiB
HTML
238 lines
8.0 KiB
HTML
<!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>
|