148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
|
|
"""内容型布局 — 要点列表、章节分隔"""
|
|||
|
|
|
|||
|
|
from pptx.util import Inches
|
|||
|
|
from pptx.enum.shapes import MSO_SHAPE
|
|||
|
|
from ..helpers import (set_slide_bg, add_text, add_rect, add_rounded_rect,
|
|||
|
|
add_rich_text, add_shape, add_page_header, setup_content_page)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def layout_bullet_points(slide, data, theme):
|
|||
|
|
"""带图标/标记的要点列表 — 3~5 条要点,每条含标题+描述
|
|||
|
|
|
|||
|
|
data keys:
|
|||
|
|
title (str): 页面标题
|
|||
|
|
subtitle (str, optional): 副标题
|
|||
|
|
items (list[dict]): 每项包含:
|
|||
|
|
icon (str): 图标文字(如 "01", 图标字符)
|
|||
|
|
label (str): 要点标题
|
|||
|
|
desc (str): 要点描述
|
|||
|
|
color (str, optional): 图标颜色 hex
|
|||
|
|
footnote (str, optional): 数据来源/脚注(auto-footer 渲染)
|
|||
|
|
"""
|
|||
|
|
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
|
|||
|
|
|
|||
|
|
items = data["items"]
|
|||
|
|
n = len(items)
|
|||
|
|
|
|||
|
|
# 两列布局(>3 条时)或单列
|
|||
|
|
if n <= 3:
|
|||
|
|
_render_single_column(slide, items, theme, m, sw, content_y)
|
|||
|
|
else:
|
|||
|
|
_render_two_columns(slide, items, theme, m, sw, content_y)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _render_single_column(slide, items, theme, m, sw, start_y):
|
|||
|
|
"""单列布局"""
|
|||
|
|
item_h = 1.3
|
|||
|
|
footer_y = theme.get("footer_y", 7.10)
|
|||
|
|
# 自动调整间距
|
|||
|
|
n = len(items)
|
|||
|
|
avail = footer_y - start_y - 0.3
|
|||
|
|
gap = min(0.15, (avail - n * item_h) / max(n - 1, 1))
|
|||
|
|
|
|||
|
|
for i, item in enumerate(items):
|
|||
|
|
y = start_y + i * (item_h + gap)
|
|||
|
|
icon_color = item.get("color", theme["primary"])
|
|||
|
|
|
|||
|
|
# 图标圆圈
|
|||
|
|
add_shape(slide, MSO_SHAPE.OVAL,
|
|||
|
|
m + 0.1, y + 0.1, 0.7, 0.7,
|
|||
|
|
fill=icon_color)
|
|||
|
|
|
|||
|
|
# 图标文字
|
|||
|
|
add_text(slide, item["icon"],
|
|||
|
|
x=m + 0.1, y=y + 0.1, w=0.7, h=0.7,
|
|||
|
|
font_size=14, color="FFFFFF", bold=True,
|
|||
|
|
align="center", valign="middle",
|
|||
|
|
font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_text(slide, item["label"],
|
|||
|
|
x=m + 1.1, y=y + 0.05, w=sw - 2 * m - 1.3, h=0.4,
|
|||
|
|
font_size=theme["subtitle_size"] - 2, color=theme["text_dark"],
|
|||
|
|
bold=True, font_name=theme["header_font"])
|
|||
|
|
|
|||
|
|
# 描述
|
|||
|
|
add_text(slide, item["desc"],
|
|||
|
|
x=m + 1.1, y=y + 0.5, w=sw - 2 * m - 1.3, h=0.75,
|
|||
|
|
font_size=theme["body_size"], color=theme["text_muted"],
|
|||
|
|
font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _render_two_columns(slide, items, theme, m, sw, start_y):
|
|||
|
|
"""双列布局"""
|
|||
|
|
col_w = (sw - 2 * m - 0.5) / 2
|
|||
|
|
item_h = 1.2
|
|||
|
|
|
|||
|
|
for i, item in enumerate(items):
|
|||
|
|
col = i % 2
|
|||
|
|
row = i // 2
|
|||
|
|
x_base = m + col * (col_w + 0.5)
|
|||
|
|
y = start_y + row * (item_h + 0.15)
|
|||
|
|
|
|||
|
|
icon_color = item.get("color", theme["primary"])
|
|||
|
|
|
|||
|
|
# 图标圆圈
|
|||
|
|
add_shape(slide, MSO_SHAPE.OVAL,
|
|||
|
|
x_base + 0.1, y + 0.1, 0.6, 0.6,
|
|||
|
|
fill=icon_color)
|
|||
|
|
|
|||
|
|
add_text(slide, item["icon"],
|
|||
|
|
x=x_base + 0.1, y=y + 0.1, w=0.6, h=0.6,
|
|||
|
|
font_size=12, color="FFFFFF", bold=True,
|
|||
|
|
align="center", valign="middle",
|
|||
|
|
font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
add_text(slide, item["label"],
|
|||
|
|
x=x_base + 0.9, y=y + 0.0, w=col_w - 1.1, h=0.35,
|
|||
|
|
font_size=theme["subtitle_size"] - 4, color=theme["text_dark"],
|
|||
|
|
bold=True, font_name=theme["header_font"])
|
|||
|
|
|
|||
|
|
# 描述
|
|||
|
|
add_text(slide, item["desc"],
|
|||
|
|
x=x_base + 0.9, y=y + 0.4, w=col_w - 1.1, h=0.7,
|
|||
|
|
font_size=theme["body_size"] - 1, color=theme["text_muted"],
|
|||
|
|
font_name=theme["body_font"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def layout_section_divider(slide, data, theme):
|
|||
|
|
"""章节分隔页 — 大序号 + 章节标题
|
|||
|
|
|
|||
|
|
data keys:
|
|||
|
|
number (str): 序号 (如 "01", "02")
|
|||
|
|
title (str): 章节标题
|
|||
|
|
subtitle (str, optional): 章节副标题
|
|||
|
|
"""
|
|||
|
|
m = theme["margin"]
|
|||
|
|
sw = theme.get("slide_w", 13.333)
|
|||
|
|
sh = theme.get("slide_h", 7.5)
|
|||
|
|
|
|||
|
|
set_slide_bg(slide, theme["bg_light"])
|
|||
|
|
|
|||
|
|
# 左侧深色块(比例化)
|
|||
|
|
block_w = sw * 0.375
|
|||
|
|
add_rect(slide, 0, 0, block_w, sh, fill=theme["primary"])
|
|||
|
|
|
|||
|
|
# 大序号
|
|||
|
|
add_text(slide, data["number"],
|
|||
|
|
x=0.5, y=1.5, w=block_w - 1.0, h=3.0,
|
|||
|
|
font_size=96, color=theme["accent"],
|
|||
|
|
bold=True, align="center", valign="middle",
|
|||
|
|
font_name=theme["header_font"])
|
|||
|
|
|
|||
|
|
# 右侧标题
|
|||
|
|
right_x = block_w + 0.8
|
|||
|
|
right_w = sw - right_x - m
|
|||
|
|
cover_title_size = theme.get("cover_title_size", 36)
|
|||
|
|
add_text(slide, data["title"],
|
|||
|
|
x=right_x, y=2.5, w=right_w, h=1.5,
|
|||
|
|
font_size=cover_title_size - 4, color=theme["text_dark"],
|
|||
|
|
bold=True, font_name=theme["header_font"])
|
|||
|
|
|
|||
|
|
if data.get("subtitle"):
|
|||
|
|
add_text(slide, data["subtitle"],
|
|||
|
|
x=right_x, y=4.2, w=right_w, h=0.8,
|
|||
|
|
font_size=theme["subtitle_size"], color=theme["text_muted"],
|
|||
|
|
font_name=theme["body_font"])
|