313 lines
14 KiB
Markdown
313 lines
14 KiB
Markdown
# 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 一项**
|
||
```
|