zcbot/skills/ppt/references/layouts.md

313 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 9 种常用版式 (16:9, 13.33×7.5 in)
> **要点**:版式 helper 已全部收进 `scripts/pptx_helpers.py`,**不要再把 helper 源码默写进每页的 run_python** —— 每页只 `import pptx_helpers as P` 然后调用。这样长 deck 里不会出现第 7 页和第 2 页的 `apply_brand` 坐标对不上的漂移,也省 token。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex —— 通过 `P.set_palette()` 注入,默认商务红。
## 通用起手(整 deck 单脚本 — 默认路径)
阶段二写一个 `build_deck.py`,一个进程内建完整份 deck、末尾 `save` 一次(**不逐页 run_python**)。每页一个小函数,主流程按逐页大纲依次调用:
```python
import sys
sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入的绝对路径替换
import pptx_helpers as P
SPEC = "<task_dir>/<today>-<task_short_id>-<task_name>.spec.md"
OUT = "<task_dir>/<topic>.pptx"
def page_1_cover(prs):
slide = P.add_slide(prs)
P.apply_brand(slide, "cover")
# ... 见 L1 封面 ...
def page_2(prs):
slide = P.add_slide(prs)
# ... 见对应 Lx 版式 ...
# ... 按大纲补齐 page_3 … page_N ...
def main():
prs = P.new_presentation("16:9") # 默认 16:9;可传 "4:3" / "9:16" / "3:4"
P.set_palette(spec_path=SPEC) # 整 deck 设一次配色(同进程内常驻)
for build in (page_1_cover, page_2, ...): # 按逐页大纲顺序
build(prs)
prs.save(OUT)
main()
```
跑法:先 `write` 这个脚本到 `<task_dir>/build_deck.py`,再 `run_python(script_path="<task_dir>/build_deck.py")`。要改(quality_check 报错 / 用户要调)→ 改脚本里对应 `page_x` 函数,重跑整脚本(可复现,不 edit 成品 .pptx)。
> **风格探针 / 增量补页**:要先看封面 + 1 页观感,把 `main()` 的循环临时缩到前 2 个函数跑一遍,确认后放开重跑;或对已存在的 deck 追加单页时用 `prs = P.load(OUT)` 载入再 `add_slide`(`load` 按文件实际尺寸回填画布常量)。**常规整建不用 `load`**。
⚠️ 一律用 `P.xxx`(不要 `from pptx_helpers import *`)—— `set_palette` 靠改模块属性覆盖配色,`import *` 会把旧绑定拷进页面命名空间导致覆盖不生效。
## Helper API 速查 (都在 `P.` 命名空间下)
**画布 / 配色入口**
- `P.new_presentation(canvas="16:9")` → 建空 deck,设画布,回填 `P.SLIDE_W/H` 与安全区
- `P.load(path)` → 载入已有 deck,按文件实际尺寸回填画布常量(逐页进程间自动同步)
- `P.add_slide(prs)` → 追加一张空白版式(layout 6)slide
- `P.set_palette(primary=, secondary=, accent=, cn_font=, en_font=, spec_path=)` → 覆盖主题色/字体;传 `spec_path` 自动从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调;**默认商务红,什么都不传无副作用**
**颜色常量**:`P.PRIMARY` `P.SECONDARY` `P.ACCENT` `P.INK` `P.GREY` `P.GREY_LIGHT` `P.HAIRLINE` `P.BG` `P.WHITE`
**字体常量**:`P.CN_FONT`(微软雅黑) `P.EN_FONT`(Arial)
**画布常量**:`P.SLIDE_W` `P.SLIDE_H` `P.SAFE_LEFT/TOP/RIGHT/BOTTOM` `P.SAFE_W` `P.SAFE_H`
**放置 helper**(全部内置 `assert_inside` 越界即报错)
- `P.add_textbox(slide, left, top, w, h, text, size, bold=False, color=P.INK, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font=None, shrink=True, name=...)` → 文本框;`font=None` 自动 latin=Arial + 东亚=微软雅黑(**中文真落到雅黑靠这个**),传 `font` 则两槽都用它(纯英文大字/数字)
- `P.add_rect(slide, left, top, w, h, fill, name=...)` → 无边线实心矩形
- `P.add_shape(slide, kind, left, top, w, h, fill, name=...)` → 任意 MSO_SHAPE(`kind` 用 `MSO_SHAPE.XXX`)
- `P.add_dot(slide, x, y, size=0.18, color=P.ACCENT)` → 圆点(bullet 前缀)
- `P.add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=P.ACCENT)` → 强调短线
- `P.add_badge(slide, x, y, num, diameter=0.7, fill=P.PRIMARY, fg=P.WHITE)` → 编号徽章(圆+数字)
- `P.page_title(slide, text, page_num=None, total=None, footer="项目汇报")` → 内页标题+强调线(+可选页脚页码)
- `P.apply_brand(slide, kind)` → 品牌锚点,`kind` ∈ `"cover"/"inner"/"section"/"end"`;**每页第一行必调**
- `P.assert_inside(left, top, w, h, name="")` → 手动越界校验(上面的 helper 已内置)
> `MSO_SHAPE` / `PP_ALIGN` / `MSO_ANCHOR` 等枚举若页面里要直接用,自行 `from pptx.enum.shapes import MSO_SHAPE` 等(`pptx_helpers` 内部已 import,但不重导出)。
---
## L1 · 封面 (Cover) —— 主色长竖条锚点
```python
slide = P.add_slide(prs)
P.apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横
# 主标题 (避开左竖条)
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
44, bold=True, color=P.INK, name="cover_title")
# 副标题 (灰色,弱化)
P.add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位",
22, color=P.GREY, name="cover_sub")
# 汇报人 / 日期
P.add_textbox(slide, 0.9, 6.4, 11.9, 0.4,
"汇报人 · 部门 · 2026-05-06", 14, color=P.GREY_LIGHT,
name="cover_meta")
# 右下角小图标点缀 (五角星,可选)
from pptx.enum.shapes import MSO_SHAPE
P.add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, P.ACCENT,
"deco_star")
```
---
## L2 · 目录 (Agenda) —— 编号徽章 + 文字
```python
from pptx.enum.text import MSO_ANCHOR
slide = P.add_slide(prs)
P.apply_brand(slide, "inner")
P.page_title(slide, "目录")
items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"]
for i, item in enumerate(items):
y = 1.9 + i * 0.95
P.add_badge(slide, P.SAFE_LEFT, y, i + 1, diameter=0.65)
P.add_textbox(slide, P.SAFE_LEFT + 1.0, y, P.SAFE_W - 1.0, 0.65,
item, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
name=f"agenda_{i}")
```
---
## L3 · 章节分隔 (Section Divider) —— 浅色背景 + 大字编号
```python
from pptx.enum.text import MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
slide = P.add_slide(prs)
P.apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰
# 大编号 (主色;font=EN_FONT 让数字走 Arial)
P.add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True,
color=P.PRIMARY, font=P.EN_FONT, name="sec_num")
# 章节名
P.add_textbox(slide, 5.5, 2.8, 7, 1.0, "背景与现状",
44, bold=True, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
name="sec_title")
# 引言
P.add_textbox(slide, 5.5, 4.0, 7, 0.6,
"本章讨论行业现状与机会窗口", 18, color=P.GREY,
name="sec_lead")
# 装饰小图标
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, P.ACCENT,
"sec_arrow")
```
---
## L4 · 要点 (Bullets) —— 圆点 + 文字,无大块色
```python
from pptx.enum.text import MSO_ANCHOR
slide = P.add_slide(prs)
P.apply_brand(slide, "inner")
P.page_title(slide, "核心结论")
bullets = [
"结论一:用一句话讲清楚",
"结论二:具体数据支撑,如增长 27%",
"结论三:对未来的判断,简洁有力",
"结论四:可选第四条,不要超过 5 条",
]
for i, b in enumerate(bullets):
y = 2.0 + i * 0.95
P.add_dot(slide, P.SAFE_LEFT + 0.05, y + 0.22, size=0.18)
P.add_textbox(slide, P.SAFE_LEFT + 0.45, y, P.SAFE_W - 0.45, 0.6,
b, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
name=f"bullet_{i}")
```
---
## L5 · 双栏对比 (Two-Column) —— 中线分隔,小色块标签
```python
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
slide = P.add_slide(prs)
P.apply_brand(slide, "inner")
P.page_title(slide, "现状 vs 改进后")
mid_x = P.SLIDE_W / 2
# 中间细分隔线 (替代两块大矩形)
P.add_rect(slide, mid_x - 0.02, 2.0, 0.04, 4.8, P.HAIRLINE, "divider")
# 左栏小标签 (色块只占小区域)
P.add_rect(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, P.GREY, "left_tag")
P.add_textbox(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, "现状", 14, bold=True,
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
shrink=False, name="left_label")
left_pts = ["问题 A: 描述", "问题 B: 描述", "问题 C: 描述"]
for i, p in enumerate(left_pts):
P.add_dot(slide, P.SAFE_LEFT + 0.05, 2.7 + i * 0.7 + 0.18, color=P.GREY)
P.add_textbox(slide, P.SAFE_LEFT + 0.45, 2.7 + i * 0.7,
mid_x - P.SAFE_LEFT - 0.7, 0.55, p, 18, color=P.INK,
anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
# 右栏小标签
P.add_rect(slide, mid_x + 0.3, 2.0, 0.8, 0.35, P.PRIMARY, "right_tag")
P.add_textbox(slide, mid_x + 0.3, 2.0, 0.8, 0.35, "改进后", 14, bold=True,
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
shrink=False, name="right_label")
right_pts = ["改善 A: 描述", "改善 B: 描述", "改善 C: 描述"]
for i, p in enumerate(right_pts):
P.add_dot(slide, mid_x + 0.35, 2.7 + i * 0.7 + 0.18, color=P.ACCENT)
P.add_textbox(slide, mid_x + 0.75, 2.7 + i * 0.7,
P.SAFE_RIGHT - mid_x - 0.75, 0.55, p, 18, color=P.INK,
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
```
---
## L6 · 图表为主 (Chart-focus) —— 标题 + 一句结论 + 大图
```python
from pptx.util import Inches
from pptx.enum.text import PP_ALIGN
# chart.png 已用 matplotlib 生成 (见 design_principles.md §7)
slide = P.add_slide(prs)
P.apply_brand(slide, "inner")
P.page_title(slide, "季度营收持续增长")
# 一句话结论
P.add_textbox(slide, P.SAFE_LEFT, P.SAFE_TOP + 1.1, P.SAFE_W, 0.5,
"Q4 同比增长 158%,创历史新高", 18, color=P.GREY, name="lead")
# 图表 (居中,占 8.9 寸宽,高度自适应 —— 只给 width 等比缩放)
slide.shapes.add_picture("<task_dir>/slides/chart.png", Inches(2.2),
Inches(2.4), width=Inches(8.9))
# 数据来源 (右下角弱化)
P.add_textbox(slide, P.SAFE_LEFT, 6.95, P.SAFE_W, 0.4,
"数据来源: 公司年报 2025", 11, color=P.GREY_LIGHT,
align=PP_ALIGN.RIGHT, shrink=False, name="source")
```
---
## L7 · 图片为主 (Image-focus) —— 文字在图旁,不压图
> 之前用满铺图 + 半透明遮罩,效果不稳定。改成"图占 60% + 文字独立区"。
```python
from pptx.util import Inches
from pptx.enum.shapes import MSO_SHAPE
slide = P.add_slide(prs)
# 左侧图占 60% 宽 (只给 width 或 height 一项,避免变形;此处图需正好铺满左 8 寸高 7.5 寸时按素材比例取舍)
slide.shapes.add_picture("<task_dir>/slides/hero.jpg", Inches(0), Inches(0),
height=Inches(7.5))
# 右侧浅灰背景区放文字
P.add_rect(slide, 8, 0, 5.33, 7.5, P.BG, "text_panel")
P.add_rect(slide, 8.4, 1.0, 0.06, 0.8, P.ACCENT, "deco_bar") # 装饰短线
P.add_textbox(slide, 8.4, 2.0, 4.6, 1.6, "走进未来", 36,
bold=True, color=P.INK, name="img_title")
P.add_textbox(slide, 8.4, 3.8, 4.6, 1.5,
"用一两句话点出主旨,不要把演讲稿搬上来。",
18, color=P.GREY, name="img_caption")
# 图标:右下角的箭头,引导视线
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, P.ACCENT,
"img_cta")
```
---
## L8 · 金句 / 大字 (Quote) —— 留白主导,装饰极简
```python
from pptx.enum.text import MSO_ANCHOR
slide = P.add_slide(prs)
P.apply_brand(slide, "inner")
# 左上大引号 (用字形;font=EN_FONT 走 Arial)
P.add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True,
color=P.ACCENT, font=P.EN_FONT, shrink=False, name="quote_mark")
# 金句 (深色,留白多)
P.add_textbox(slide, 1.5, 2.7, 10.5, 2.0,
"把复杂留给我们,把简单留给用户。", 36, bold=True,
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
# 装饰短线
P.add_accent_line(slide, 1.5, 5.0, length=0.5)
# 出处
P.add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
16, color=P.GREY, name="quote_attr")
```
---
## L9 · 结尾 / Q&A —— 浅底 + 大字,**强制必有**
> **不是可选** —— 任何 deck 都必须以这页收尾。无论是汇报、提案、路演,缺尾页等于"话没说完"。
```python
from pptx.enum.text import PP_ALIGN
slide = P.add_slide(prs)
P.apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线
P.add_textbox(slide, 0, 2.5, P.SLIDE_W, 1.6, "Thank You", 80, bold=True,
color=P.PRIMARY, align=PP_ALIGN.CENTER, font=P.EN_FONT,
name="thanks")
P.add_textbox(slide, 0, 4.3, P.SLIDE_W, 0.6, "欢迎提问与讨论",
22, color=P.ACCENT, align=PP_ALIGN.CENTER, name="qa")
P.add_textbox(slide, 0, 6.2, P.SLIDE_W, 0.5,
"联系方式 / 邮箱 / 公众号", 14, color=P.GREY_LIGHT,
align=PP_ALIGN.CENTER, name="contact")
```
---
## 选版式速查
```
有数据 ≥ 3 点 → L6 (Chart-focus)
对比类 (前/后, A/B) → L5 (Two-Column)
要点 ≤ 5 条 → L4 (Bullets)
转场 / 换章 → L3 (Section Divider)
首页 → L1 (Cover)
末页 → L9 (Q&A)
有大图 / 视觉优先 → L7 (Image-focus)
观点强调 / 名言 → L8 (Quote)
```
## 三个常犯的越界场景
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字。超过 1 行就溢出 0.7 in 高的框。`add_textbox` 内置 `assert_inside` + shrink-to-fit 兜底;但根本解法是**字数压缩**(见 design_principles.md §字数预算)
2. **标题占两行** —— 标题在 0.7 in 高的框里,32pt 单行高约 0.45 in,**两行就溢出**。中文标题 ≤ 30 字
3. **图片不等比拉伸** —— `add_picture(width=, height=)` 同时给会变形;**只给 width 或 height 一项**
```