zcbot/skills/ppt/references/layouts.md

298 lines
13 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()` 注入,默认商务红。
## 通用起手 (每页 run_python 顶部)
```python
import sys
sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入的绝对路径替换
import pptx_helpers as P
# —— 第一页(创建 deck)——
prs = P.new_presentation("16:9") # 默认 16:9;可传 "4:3" / "9:16" / "3:4"
P.set_palette(spec_path="<task_dir>/<today>-<task_short_id>-<task_name>.spec.md")
slide = P.add_slide(prs)
# ... 见下面各 L 版式 ...
prs.save("<task_dir>/<topic>.pptx")
# —— 后续页(追加到已有 deck)——
prs = P.load("<task_dir>/<topic>.pptx") # 从文件实际尺寸回填画布常量
P.set_palette(spec_path="<task_dir>/<today>-<task_short_id>-<task_name>.spec.md") # 每页都重读 spec
slide = P.add_slide(prs)
# ... 见下面各 L 版式 ...
prs.save("<task_dir>/<topic>.pptx")
```
⚠️ 一律用 `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 一项**
```