HTML 演示文稿配置手册——从控件到缩放
基于 opencode + deepseekv4 的实践总结。适用于用 AI Agent 生成 HTML 演示文稿时的控件、缩放、字体等配置。
一、Canvas 尺寸
推荐尺寸:1200 × 675(16:9 宽屏比例)
这个比例适配大多数屏幕(16:9 投影、笔记本),也是 PPT 的标准比例。所有后续布局以此为基础。
+--{ canvas-wrapper: 全屏背景 }--+
| +--{ canvas-center: 1200px }--+ ← 固定宽度容器
| | +- presentation-container -+ | ← transform 作用域(1200×675)
| | | slides | |
| | +------------------------+ |
| | controls-bar |
| +------------------------------+
+---------------------------------+
二、HTML 层级结构(必记)
<body>
<div class="canvas-wrapper" id="canvasWrapper"> <!-- 全屏背景,padding 40px -->
<div class="canvas-center"> <!-- 固定 1200px,无 padding -->
<div class="presentation-container"> <!-- transform: scale() 作用域 -->
<div class="slide-viewport"> <!-- overflow: hidden -->
<div class="slide">...</div> <!-- 每页幻灯片 -->
...
</div>
<div class="controls-bar"> <!-- 工具栏按钮 -->
...
</div>
</div>
</div>
</div>
</body>
层级说明
| 层级 | 作用 | 关键属性 |
|---|---|---|
| canvas-wrapper | 全屏背景,padding 40px 提供视觉框架 | padding: 40px, overflow: auto |
| canvas-center | 固定宽度容器,保持 presContainer 宽度统一 | width: 1200px, padding: 0 |
| presentation-container | transform 缩放的目标元素 | width: 100%, height: 675px |
| slide-viewport | 裁剪区域,自适应模型下无滚动 | overflow: hidden |
| controls-bar | 按钮工具栏,必须在此位置 | flex-shrink: 0 |
三、自适应缩放模型(推荐)
核心概念
Canvas(1200×675)是设计基准,通过 transform: scale() 自适应填充视窗,无滚动条。
与地图模型的区别:
| 模型 | 行为 | 适用场景 |
|---|---|---|
| 地图模型 | Canvas 固定尺寸,视窗裁剪,滚动浏览 | 大画布、精细内容 |
| 自适应模型(推荐) | Canvas 缩放到视窗内,始终完全可见 | 演示文稿、PPT |
两者互斥,不可混合使用。
CSS 布局
.canvas-wrapper {
width: 100%;
min-width: 100vw;
height: 100vh;
overflow: auto;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
padding: 40px;
}
.canvas-center {
width: 1200px;
flex-shrink: 0;
padding: 0;
}
.presentation-container {
width: 100%;
height: 675px;
background: var(--slide-bg);
box-shadow: var(--shadow-lg);
overflow: hidden;
display: flex;
flex-direction: column;
}
.slide-viewport {
flex: 1;
position: relative;
overflow: hidden;
}
完整 JS 缩放逻辑
const CANVAS_WIDTH = 1200;
const CANVAS_HEIGHT = 675;
let wasInFullscreen = false;
// 自动缩放:窗口小于 Canvas 时缩小适配
function updateAutoScale() {
const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (isFS) {
// 全屏时不干扰,由 updateFullscreenUI 管理
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
presContainer.style.width = '';
presContainer.style.height = '';
return;
}
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const scaleX = screenW / CANVAS_WIDTH;
const scaleY = screenH / CANVAS_HEIGHT;
const fitScale = Math.min(scaleX, scaleY, 1.0);
if (fitScale < 1.0) {
presContainer.style.transform = `scale(${fitScale})`;
presContainer.style.transformOrigin = 'center center';
presContainer.style.width = CANVAS_WIDTH + 'px';
presContainer.style.height = CANVAS_HEIGHT + 'px';
} else {
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
presContainer.style.width = '';
presContainer.style.height = '';
}
}
// 全屏缩放:填满屏幕
function updateFullscreenUI() {
const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement);
btnFullscreen.style.display = isFS ? 'none' : '';
btnExitFs.style.display = isFS ? '' : 'none';
if (isFS) {
document.body.classList.add('fullscreen-mode');
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const scaleX = screenW / CANVAS_WIDTH;
const scaleY = screenH / CANVAS_HEIGHT;
const scale = Math.min(scaleX, scaleY);
presContainer.style.transformOrigin = 'center center';
presContainer.style.transform = `scale(${scale})`;
wasInFullscreen = true;
} else if (wasInFullscreen) {
document.body.classList.remove('fullscreen-mode');
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
wasInFullscreen = false;
}
}
window.addEventListener('resize', () => {
updateAutoScale();
updateFullscreenUI();
});
全屏 CSS
body.fullscreen-mode .canvas-wrapper {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0;
}
body.fullscreen-mode .slide-viewport {
overflow: hidden;
}
body.fullscreen-mode .canvas-center {
padding: 0;
}
body.fullscreen-mode .presentation-container {
transform-origin: center center;
}
四、全屏逻辑与 wasInFullscreen 防冲突
核心问题:全屏退出时,auto-scale 会检查窗口大小并尝试叠加另一个 transform,导致布局错乱。
解决方案:wasInFullscreen 标志位。
全屏进入时 wasInFullscreen = true,退出全屏时才清除 transform。auto-scale 检测到全屏状态时直接 return,不干扰全屏缩放。
let wasInFullscreen = false;
function updateFullscreenUI() {
// ...
if (isFS) {
presContainer.style.transform = `scale(${scale})`;
wasInFullscreen = true;
} else if (wasInFullscreen) {
presContainer.style.transform = '';
wasInFullscreen = false;
}
}
五、controls-bar 规范
位置(关键)
controls-bar 必须位于 presentation-container 内部、slide-viewport 外部。
<div class="presentation-container">
<div class="slide-viewport">
<!-- slides -->
</div>
<div class="controls-bar"> <!-- ← 在这里 -->
...
</div>
</div>
错误位置会导致:
- 工具栏随 slide 切换而消失
- 工具栏被裁剪或显示异常
按钮顺序
← Prev | 页码 | → Next | 分割线 | ⛶ 全屏 | ✕ Exit FS
| 按钮 | ID | 说明 |
|---|---|---|
| 上一页 | btnPrev |
或 ← Prev |
| 页码 | slideIndicator |
格式 "3 / 15" |
| 下一页 | btnNext |
或 Next → |
| 全屏 | btnFullscreen |
全屏时隐藏 |
| 退出全屏 | btnExitFs |
仅全屏时显示 |
CSS
.controls-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 10px 20px;
background: #e8e4dc;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.controls-bar button {
font-size: 14px;
padding: 8px 16px;
border: 1.5px solid var(--primary);
background: #fff;
color: var(--primary);
border-radius: 5px;
cursor: pointer;
font-weight: 600;
}
键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault(); showSlide(currentIndex - 1);
}
else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
e.preventDefault(); showSlide(currentIndex + 1);
}
else if (e.key === 'Home') { e.preventDefault(); showSlide(0); }
else if (e.key === 'End') { e.preventDefault(); showSlide(totalSlides - 1); }
else if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggleFullscreen(); }
});
支持翻页笔的 PageUp/PageDown 键映射。
六、Reference 页面:presContainer 宽度一致性
问题
全屏前后、浏览器 zoom、窗口 resize 都会使 Reference 页的可见条目数量变化(14→16 条)。
根因分析
不同模式下 presContainer 宽度不一致:
| 模式 | 宽度 | 文本宽度 | 行数(18条) | 效果 |
|---|---|---|---|---|
| 普通模式(有 padding) | 1120px | 1048px | ~38行 | 裁剪2-4条 |
| auto-scale / 全屏 | 1200px | 1128px | ~30行 | 全显示 |
80px 宽度差 → 改变文本换行 → ~138px 行高差异 → 4 条变化。
不是子像素四舍五入(±13px 只能解释 0-1 条)。
解决方案
所有模式下 presContainer 宽度统一为 1200px。
- 正常模式:canvas-wrapper
padding: 40px,canvas-centerwidth: 1200px; padding: 0→ presContainer100% = 1200px - Auto-scale:JS 设置
presContainer.style.width = 1200px - 全屏:canvas-center
padding: 0,presContainer100% = 1200px
Reference 页 CSS
.slide-body.ref-slide { gap: 0; font-family: 'Georgia', 'Times New Roman', serif; }
.slide-body.ref-slide p { font-size: 15px; line-height: 1.15; margin: 0; }
.slide-body.ref-slide .ref-item { margin-bottom: 2px; }
七、Zoom 禁用
自适应模型与浏览器 zoom(Ctrl+滚轮、双指缩放)互斥。auto-scale 不断抵消 zoom,体验极差。
必须彻底禁用 zoom。
方法一:meta viewport
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
阻止移动端双指缩放和浏览器键盘缩放的基础行为。
方法二:JS 阻止 Ctrl+滚轮
document.addEventListener('wheel', (e) => {
if (e.ctrlKey || e.metaKey) e.preventDefault();
}, { passive: false });
八、字体方案
字体分类
| 用途 | 推荐字体 | 说明 |
|---|---|---|
| 西文正文 | Palatino Linotype, Helvetica Neue, Segoe UI |
有衬线,学术感 |
| 西文图表 | Inter, Segoe UI |
无衬线,小字清晰 |
| 西文 Reference | Georgia, Times New Roman |
衬线,学术引用 |
| 中文正文 | Noto Serif SC, Source Han Serif SC |
思源宋体,学术感 |
| 中文表格 | Noto Sans SC, Source Han Sans SC |
思源黑体,小字清晰 |
Google Fonts 引入
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
CSS 变量
:root {
--font-zh-body: 'Noto Serif SC', 'Source Han Serif SC', 'SimSun', serif;
--font-zh-chart: 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;
--font-zh-table: 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;
}
.slide-body { font-family: var(--font-zh-body); }
.data-table { font-family: var(--font-zh-table); }
svg text { font-family: 'Inter', 'Segoe UI', sans-serif; }
九、经验清单
| # | 类别 | 教训 |
|---|---|---|
| 1 | 截图 | Playwright 截图受 CSS max-width 影响,需配合 transform: scale() 调整 |
| 2 | 图表 | Chart.js 截图时动画未完成,需等待 2.5s |
| 3 | SVG 图表 | SVG y 坐标从上往下,柱子 y = 基线 - 高度 |
| 4 | 编辑 | 大幅结构调整用 write 重写,不要多次 edit 拼凑 |
| 5 | 工具栏 | controls-bar 必须放在 presentation-container 内部、slide-viewport 外部 |
| 6 | 全屏 | wasInFullscreen 标志防退出冲突 |
| 7 | 缩放模型 | 地图模型与自适应模型互斥,选一个 |
| 8 | Reference | presContainer 宽度必须所有模式统一 |
| 9 | Zoom | 自适应模型必须禁用浏览器 zoom |
| 10 | 字号 | 用整数 px 值消除子像素四舍五入 |
| 11 | Canvas 比例 | 1200×675 是最佳 16:9 设计尺寸 |
| 12 | 翻页笔 | PageUp/PageDown 绑到上一页/下一页 |
附录:完整配置清单(AGENTS.md)
以下为项目全程积累的规范文档。如使用 AI Agent 进行 HTML 演示文稿开发,可直接将下方代码块粘贴至工作目录下的 AGENTS.md:
# OpenCode 任务处理记录
## 一、任务类型
### 1. 文件格式转换类
- **HTML → 图片 → PPTX**:从 HTML 幻灯片出发,通过浏览器截图生成高清图片,再合成 PPT。
- 关键工具:Playwright(浏览器截图)、python-pptx(合成 PPT)、Pillow(检查图片尺寸)。
### 2. 批量图片处理类
- 截取多页幻灯片,统一分辨率、宽高比,批量写入 PPT。
- 核心需求:高分辨率(避免字体模糊)、比例一致(避免拉伸)。
### 3. 前端 HTML 内容编辑类
- 结构调整:章节重编号、目录重组、页面增删。
- 样式调整:CSS 补全、布局改动(指导老师位置、目录结构、标题格式)。
- 内容清理:去除冗余注释、修正语义错误(如致谢页删除后又重新加上)。
### 4. 自动化脚本编写类
- Node.js 脚本:控制 Playwright 截取幻灯片。
- Python 脚本:生成 PPT、控制图片缩放比例和位置。
---
## 二、遇到的一些坑
### 1. Playwright 截图尺寸问题
- **现象**:viewport deviceScaleFactor=2 仍然只有 1100px 宽,图片没有按预期放大。
- **原因**:CSS 中 `.slide` 的 `max-width: 1100px` 限制了实际渲染宽度,截图只截到了被 max-width 约束后的内容。
- **解决**:通过 `element.style.transform = 'scale(N)'` 放大元素 + 设置对应宽的 viewport + deviceScaleFactor=2,三者配合才能得到大尺寸图片。
### 2. Chart.js 图表截取为空白
- **现象**:包含 Chart.js 图表的页面截图时,图表区域是空白或只有占位符。
- **原因**:截图时 Chart.js 动画尚未完成(尤其是饼图首次渲染),且 Pie Chart 在 slide 切换后需要重新实例化才能显示。
- **解决**:切换到目标 slide 后等待足够时间(如 2500ms),确保动画完成后再截图。
### 3. 多次编辑导致 HTML 结构错乱
- **现象**:重复编辑后出现重复的 slide 块、编号跳号、标签不匹配。
- **原因**:之前多次局部 edit 时新内容覆盖了旧内容但没完全清理干净,加上 grep 搜索只能定位行号而无法感知结构重叠。
- **解决**:面对大幅度结构调整时,优先重写整个文件而非逐块修改,避免积累性的编辑错误。
### 4. PowerShell 命令解析错误
- **现象**:`cd /d path && cmd` 报错;`head` 被当作 cmdlet 不存在。
- **原因**:PowerShell 5.1 不支持 `&&` 串联命令;`head` 是 Linux 命令在 PowerShell 中不存在。
- **解决**:使用 `;` 代替 `&&`,或拆分成多个 Bash 工具调用;用 Python 替代 `head` 等 shell 命令做数据检查。
### 5. PPT 生成时图片比例拉伸
- **现象**:图片放入 16:9 幻灯片后被严重拉伸或留白过多。
- **原因**:图片宽高比(3300:1650 = 2:1)与 PPT 比例(16:9 ≈ 1.78:1)不一致,`scale = min(scale_w, scale_h)` 让图片填不满。
- **解决**:以宽度为基准缩放(宽度撑满,左上对齐),或在 PPT 尺寸设置时改用更宽的幻灯片比例匹配图片。
### 6. Python pptx 库导入错误
- **现象**:`from pptx.dml.color import RgbColor` 报错,该模块不存在。
- **原因**:pptx 库的 `dml.color` 子模块中没有 `RgbColor`,是记忆性错误。
- **解决**:直接用 `from pptx import Presentation` + `from pptx.util import Inches`,不引用不存在的子模块。
### 7. 路径中包含中文字符
- **现象**:文件路径含中文时,Python 读取正常但 PowerShell 命令解析可能出错。
- **原因**:PowerShell 默认编码不是 UTF-8。
- **解决**:PowerShell 脚本开头加 `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`;Python 使用 `-X utf8` 参数或在代码中 `sys.stdout.reconfigure(encoding='utf-8')`。
### 8. controls-bar 位置错误导致工具栏消失
- **现象**:工具栏只在最后一页显示,或出现在 slide 上面而不是下面。
- **原因**:HTML 嵌套结构错误,closing tag 缺失导致 controls-bar 被包含在 slide 或 slide-viewport 内部。
- **解决**:controls-bar 必须位于 presentation-container 内部、slide-viewport 外部。正确结构:
```
presentation-container
slide-viewport
slide (所有页)
controls-bar ← 在这里
```
### 9. SVG 图表柱子高度计算错误
- **现象**:柱状图柱子高度与数据不成比例,柱子顶到顶部或低于基线。
- **原因**:SVG y 坐标是从上往下计算的,rect 的 y 是柱子顶部的位置而不是底部。需要用 `y = 基准线 - 柱子高度` 计算。
- **解决**:先确定 0% 基线位置(如 y=240),柱子高度 = 值 / 最大值 * 可用高度,柱子 y = 基准线 - 柱子高度。
### 10. 浏览器 zoom 导致内容溢出到负坐标
- **现象**:使用 Ctrl+/ 放大浏览器后,左边内容被裁切,无法滚动到负坐标区域。
- **原因**:flexbox 的 `justify-content: center` 在内容溢出时导致溢出部分不可访问。
- **解决**:不要用 flexbox center 配合 overflow。使用 `margin: 40px auto` 或 padding 配合固定宽度容器,确保内容始终在可滚动范围内。
### 11. Reference 页条目数量随 zoom/resize/fullscreen 变化
- **现象**:按下全屏按钮后,参考文献页可见条目从 14 条变为 16 条;浏览器 zoom 和 resize 也会改变条目数量。
- **原因**:不同模式下 presContainer 宽度不一致(普通模式 1120px vs 全屏/auto-scale 1200px),导致文本换行不同 → 总行数不同 → 被 overflow: hidden 裁剪的条目不同。
- 不是子像素四舍五入的主因(±13px 只能解释 0-1 条)
- 80px 宽度差改变了 7-8 个长条目的换行(每窄 80px 多 wrap 1 行),总差异 ~138px,足够解释 4 条变化
- **解决**:统一 presContainer 宽度——canvas-wrapper 加 padding: 40px,canvas-center 纯内容 1200px,presContainer 始终 1200px。
- 浏览器 zoom 的连锁反应:zoom 放大 → viewport 缩小 → auto-scale 触发 → 把 presContainer 设为 1200px(与普通模式一致)
### 12. Zoom 禁用实现
- **现象**:自适应模型下,浏览器 zoom 仍可通过 Ctrl+滚轮触发,导致 auto-scale 反复抵消,体验不佳。
- **解决**:
1. `<meta viewport>` 加 `maximum-scale=1.0, user-scalable=no`
2. JS 阻止 `Ctrl/Cmd + wheel` 事件
```javascript
document.addEventListener('wheel', (e) => {
if (e.ctrlKey || e.metaKey) e.preventDefault();
}, { passive: false });
```
---
## 三、常见解决办法
### 高分辨率截图
```
viewport: { width: N, height: M }
deviceScaleFactor: 2(2x 缩放)
配合 CSS transform: scale(1.9) + 相应调整 container 宽度
```
### 等待 Chart.js 渲染完成
```javascript
await page.waitForTimeout(2500); // 切换 slide 后等待 2.5 秒
const buf = await slide.screenshot({ type: 'png' });
```
### 批量截取多页幻灯片
```javascript
for (let i = 0; i < slideCount; i++) {
await page.evaluate((idx) => {
const slides = document.querySelectorAll('.slide');
slides.forEach((s, i) => s.classList.toggle('active', i === idx));
}, i);
await page.waitForTimeout(2500);
const slide = await page.locator('.slide.active');
await slide.screenshot({ type: 'png', path: `frames/slide_${i+1}.png` });
}
```
### 生成 PPT 并填充全宽图片
```python
from pptx import Presentation
from pptx.util import Inches
from PIL import Image
prs = Presentation()
prs.slide_width = Inches(13.33) # 宽屏比例,匹配 3300:1650 ≈ 2:1
prs.slide_height = Inches(7.5)
for i in range(1, 13):
img_path = f'frames/slide_{i:02d}.png'
with Image.open(img_path) as img:
w, h = img.size
scale = Inches(13.33) / w
pic_w = int(w * scale)
pic_h = int(h * scale)
slide.shapes.add_picture(img_path, 0, 0, pic_w, pic_h)
```
### 大幅结构调整时重写文件
不要依赖多次 `edit` 拼凑,直接 `write` 整文件,确保结构干净。
---
## 四、文件输出
- `capture_slides.js` — Playwright 批量截图脚本
- `make_pptx.py` — Python PPTX 生成脚本
- `slides_frames/` — 截图图片目录
---
## 五、PPT 生成规范(含按钮控制)
### 生成前:隐藏控制按钮
在 HTML 中通过 CSS 隐藏 `.controls-bar`,避免截图中出现"上一页/下一页/全屏"等按钮。
修改方案(临时注释掉):
```css
/* .controls-bar { display: none; } */
```
或者在截图脚本中通过 JavaScript 临时隐藏:
```javascript
document.getElementById('controlsBar').style.display = 'none';
// 截图
document.getElementById('controlsBar').style.display = '';
```
### 截图完成后:恢复控制按钮
恢复上述 CSS 或 JS 代码,使 HTML 恢复正常展示。
### 完整流程
1. 修改 HTML 隐藏 controls-bar
2. 运行 `capture_slides.js` 截图
3. 运行 `make_pptx.py` 生成 PPT
4. 恢复 HTML 中的 controls-bar 显示
---
## 六、自适应缩放模型(推荐)
### 核心概念
Canvas(1200×675,16:9)是设计基准,始终通过 `transform: scale()` 自适应填充视窗。
与地图模型的区别:
- 地图模型:Canvas 固定尺寸,视窗只能看到一部分,用滚动浏览
- **自适应模型(推荐)**:Canvas 始终缩放至完全可见,无滚动条,zoom 被禁用
### HTML 结构
```html
<body>
<div class="canvas-wrapper" id="canvasWrapper">
<div class="canvas-center">
<div class="presentation-container" id="presContainer">
<div class="slide-viewport" id="slideViewport">
<!-- 所有 slide 页面 -->
</div>
<div class="controls-bar" id="controlsBar">
<!-- 工具栏按钮 -->
</div>
</div>
</div>
</div>
</body>
```
### CSS 布局(自适应模型)
```css
*, *::before, *::after { box-sizing: border-box; }
.canvas-wrapper {
width: 100%;
min-width: 100vw;
height: 100vh;
overflow: auto;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
padding: 40px; /* 视觉 fram 放在 wrapper 上,不影响 presContainer 宽度 */
}
.canvas-center {
width: 1200px; /* 固定宽度 = presContainer 宽度 */
flex-shrink: 0;
padding: 0; /* padding 移到 wrapper */
}
.presentation-container {
width: 100%; /* = 1200px */
height: 675px;
background: var(--slide-bg);
box-shadow: var(--shadow-lg);
overflow: hidden;
display: flex;
flex-direction: column;
}
.slide-viewport {
flex: 1;
position: relative;
overflow: hidden; /* 自适应模型无滚动 */
}
/* 全屏模式 */
body.fullscreen-mode .canvas-wrapper {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 0; /* 全屏时去掉 fram */
}
body.fullscreen-mode .slide-viewport {
overflow: hidden;
}
body.fullscreen-mode .canvas-center {
padding: 0;
}
body.fullscreen-mode .presentation-container {
transform-origin: center center;
}
```
### 缩放逻辑(JS)
```javascript
const CANVAS_WIDTH = 1200;
const CANVAS_HEIGHT = 675;
let wasInFullscreen = false;
// 自动缩放:当视窗小于 Canvas 时缩小以适配
function updateAutoScale() {
const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (isFS) {
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
presContainer.style.width = '';
presContainer.style.height = '';
return;
}
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const scaleX = screenW / CANVAS_WIDTH;
const scaleY = screenH / CANVAS_HEIGHT;
const fitScale = Math.min(scaleX, scaleY, 1.0);
if (fitScale < 1.0) {
presContainer.style.transform = `scale(${fitScale})`;
presContainer.style.transformOrigin = 'center center';
presContainer.style.width = CANVAS_WIDTH + 'px';
presContainer.style.height = CANVAS_HEIGHT + 'px';
} else {
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
presContainer.style.width = '';
presContainer.style.height = '';
}
}
// 全屏缩放:填满屏幕
function updateFullscreenUI() {
const isFS = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (isFS) {
document.body.classList.add('fullscreen-mode');
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const scaleX = screenW / CANVAS_WIDTH;
const scaleY = screenH / CANVAS_HEIGHT;
const scale = Math.min(scaleX, scaleY);
presContainer.style.transformOrigin = 'center center';
presContainer.style.transform = `scale(${scale})`;
wasInFullscreen = true;
} else if (wasInFullscreen) {
document.body.classList.remove('fullscreen-mode');
presContainer.style.transform = '';
presContainer.style.transformOrigin = '';
wasInFullscreen = false;
}
}
window.addEventListener('resize', () => {
updateAutoScale();
updateFullscreenUI();
});
```
### 关键坑点(自适应模型)
1. **presContainer 宽度必须一致**:所有模式下 presContainer 必须是相同 CSS 宽度(1200px),否则文本换行变化导致 Reference 等页面条目数量变动。
2. **viewport 禁止 zoom**:自适应模型与浏览器 zoom 互斥,必须禁用。
3. **wasInFullscreen 标志**:防止退出全屏时 auto-scale 错误叠加 transform。
4. **不要用 flexbox center + overflow**:内容溢出到负坐标不可访问。
5. **controls-bar 必须严格位于 presentation-container 内部、slide-viewport 外部**。
6. **closing tag 缺失会导致整体结构错乱**。
---
## 七、工具栏(Controls Bar)规范
### 位置结构
工具栏必须位于 `presentation-container` 内部、`slide-viewport` 外部。
### HTML 结构
```html
<div class="presentation-container" id="presContainer">
<div class="slide-viewport" id="slideViewport">
<!-- 所有 slide 页面 -->
</div>
<div class="controls-bar" id="controlsBar">
<button id="btnPrev" title="Previous Slide">← Prev</button>
<span class="slide-indicator" id="slideIndicator">1 / 15</span>
<button id="btnNext" title="Next Slide">Next →</button>
<span class="divider"></span>
<button id="btnFullscreen" title="Toggle Fullscreen">⛶ Fullscreen</button>
<button id="btnExitFs" title="Exit Fullscreen" style="display:none;">✕ Exit FS</button>
</div>
</div>
```
### CSS 样式
```css
.controls-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 10px 20px;
background: #e8e4dc;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.controls-bar button {
font-family: 'Palatino Linotype', 'Helvetica Neue', 'Segoe UI', sans-serif;
font-size: 14px;
padding: 8px 16px;
border: 1.5px solid var(--primary);
background: #fff;
color: var(--primary);
border-radius: 5px;
cursor: pointer;
font-weight: 600;
transition: all 0.2s;
}
.controls-bar button:hover { background: var(--primary); color: #fff; }
.controls-bar button:active { transform: scale(0.96); }
.controls-bar .slide-indicator { font-size: 14px; color: #555; min-width: 60px; text-align: center; }
.controls-bar .divider { width: 1px; height: 20px; background: #bbb; }
```
### 键盘快捷键
```javascript
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp') {
e.preventDefault(); showSlide(currentIndex - 1);
}
else if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown') {
e.preventDefault(); showSlide(currentIndex + 1);
}
else if (e.key === 'Home') { e.preventDefault(); showSlide(0); }
else if (e.key === 'End') { e.preventDefault(); showSlide(totalSlides - 1); }
else if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggleFullscreen(); }
});
```
支持翻页笔的 PageUp/PageDown 键映射。
---
## 八、SVG 图表规范
### 柱状图结构
```svg
<svg viewBox="0 0 420 285" xmlns="...">
<rect x="0" y="0" width="420" height="285" fill="#fafaf8" rx="6"/>
<text x="210" y="18" text-anchor="middle" font-size="13" fill="var(--primary)" font-weight="700">Chart Title</text>
<line x1="50" y1="40" x2="390" y2="40" stroke="#e0dcd4" stroke-width="1"/>
<!-- ... 柱子 ... -->
<rect x="60" y="148" width="48" height="92" fill="var(--chart-1)" rx="4" opacity="0.85"/>
<text x="84" y="143" text-anchor="middle" font-size="11" fill="var(--primary)" font-weight="700">10.31</text>
<text x="84" y="256" text-anchor="middle" font-size="10" fill="#555">2019</text>
<line x1="50" y1="240" x2="390" y2="240" stroke="#999" stroke-width="1.5"/>
<text x="210" y="280" text-anchor="middle" font-size="9" fill="#777">Source: MOE [1]</text>
</svg>
```
### 关键计算公式
- **可用高度** = 基线y - 上边距(如 240 - 40 = 200)
- **柱子高度** = 值 / 最大值 * 可用高度
- **柱子y** = 基线y - 柱子高度
- **柱子x** = 起始x + 序号 * 间距
### 颜色规范
```css
--chart-1: #2c5f7c; /* 蓝色 - 主要数据 */
--chart-2: #b8860b; /* 金色 - 强调数据 */
--chart-3: #8b3a3a; /* 红色 - 危险/负面数据 */
--chart-4: #4a7c59; /* 绿色 - 正面数据 */
--danger: #8b1a1a; /* 深红 - 最高强调 */
```
---
## 九、Reference 页面规范
### CSS 样式
```css
.slide-body.ref-slide { gap: 0; font-family: 'Georgia', 'Times New Roman', serif; }
.slide-body.ref-slide p { font-size: 15px; line-height: 1.15; margin: 0; }
.slide-body.ref-slide .ref-item { margin-bottom: 2px; }
```
### 行间距关键点
- 使用 `font-size: 15px; line-height: 1.15` 保证所有 18 条参考文献刚好塞进 675px 高度
- **gap: 0** 防止 flex 间隙
- **margin-bottom: 2px** 段落间最小间距
---
## 十、版本历史
| 版本 | 主要功能 | 日期 |
|------|---------|------|
| v1.html | 初始版本 | 2026-04-21 |
| v2.html | 添加控制栏、修复图表 | 2026-04-24 |
| v3.html | 修复图表数据标注 | 2026-04-25 |
| v4.html | 添加缩放逻辑 | 2026-04-25 |
| v5.html | SVG viewBox 方案(已废弃) | 2026-04-25 |
| v6.html | 地图布局模型,修复工具栏位置 | 2026-04-25 |
| v7.html | 自适应缩放模型,禁用 zoom,统一 presContainer 宽度 | 2026-04-25 |
---
## 十一、字体规范
### 字体分类
| 用途 | 推荐字体 | 说明 |
|------|---------|------|
| **西文正文** | `'Palatino Linotype', 'Helvetica Neue', 'Segoe UI', sans-serif` | 西文无衬线,清晰易读 |
| **西文图表** | `'Inter', 'Segoe UI', sans-serif` | 专门用于 SVG 内文字 |
| **西文 Reference** | `'Georgia', 'Times New Roman', serif` | 衬线字体,适合学术引用 |
| **中文正文** | `'Source Han Serif SC', 'Noto Serif SC', 'SimSun', serif` | 思源宋体,优雅学术感 |
| **中文图表/表格** | `'Source Han Sans SC', 'Noto Sans SC', 'Microsoft YaHei', sans-serif` | 思源黑体,清晰小字号 |
### CSS 字体设置
```css
:root {
--font-zh-body: 'Noto Serif SC', 'Source Han Serif SC', 'SimSun', serif;
--font-zh-chart: 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;
--font-zh-table: 'Noto Sans SC', 'Source Han Sans SC', 'Microsoft YaHei', sans-serif;
}
.slide-body { font-family: var(--font-zh-body); } /* 正文宋体(学术) */
.data-table { font-family: var(--font-zh-table); } /* 表格黑体(清晰) */
svg text { font-family: 'Inter', 'Segoe UI', sans-serif; } /* SVG 数字 */
```
### Google Fonts 引入
```html
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
```
---
## 十二、关键原则
1. **HTML 结构优先**:修改前先确认 HTML 嵌套结构,closing tag 缺失会导致灾难性问题
2. **大幅修改用重写**:多次局部 edit 会导致结构错乱,大改用 `write` 整文件
3. **测试每个修改**:每次修改后截图验证,不要连续多个修改后再测试
4. **保留备份版本**:每个版本单独文件,方便回溯
5. **统一 presContainer 宽度**:所有模式下 presContainer 宽度必须一致,否则文本换行变化导致 Reference 页面条目数量变化
6. **缩放模型选择**:自适应模型(推荐)和地图模型互斥,不可混合使用
7. **Canvas 推荐比例**:1200 × 675(16:9),适配投影和笔记本屏幕
8. **禁用浏览器 zoom**:自适应模型必须禁用 Ctrl+滚轮 zoom,与 auto-scale 互斥
9. **整数字号防子像素误差**:用整数 px 值代替 14.86px 等分数值,消除 zoom 导致的四舍五入偏差
本文基于 opencode + deepseekv4 的实际项目经验撰写。所有配置已在实际 HTML 演示文稿中验证通过。
AI 生成声明:本文由 deepseek-v4-flash 模型生成,借助 opencode(AI Agent 框架)完成 HTML 调试、截图录制、版本迭代与文档撰写。全过程人机协作:人类把控需求与方向,AI 负责代码实现与文档输出。
Comments NOTHING