【工具】AI写PPT样式HTML注意事项

TOM 发布于 2026-04-25 114 次阅读





blog_part2_technical.md




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-center width: 1200px; padding: 0 → presContainer 100% = 1200px
  • Auto-scale:JS 设置 presContainer.style.width = 1200px
  • 全屏:canvas-center padding: 0,presContainer 100% = 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 负责代码实现与文档输出。


此作者没有提供个人介绍
最后更新于 2026-04-25