skills/ppt-station-skill/ppt_station/composer/layouts/kpi.py
wangyitong a65adcc2e5 Initial commit: merged, deduplicated, and vetted skill collection
Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:47:12 +08:00

93 lines
3.6 KiB
Python
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.

"""KPI 卡片布局 — 投行级克制风格"""
from pptx.util import Inches, Pt
from pptx.enum.shapes import MSO_SHAPE
from ..helpers import set_slide_bg, add_text, add_rounded_rect, add_rect, add_page_header, setup_content_page
def layout_kpi_cards(slide, data, theme):
"""KPI 大数字卡片 — 3~6 个指标,分栏+极浅分割线
data keys:
title (str): 页面标题
subtitle (str, optional): 副标题/说明
cards (list[dict]): 每个 card 包含:
label (str): 指标名
value (str): 数值(已格式化)
change (str, optional): 变化(如 "+5.9%"
note (str, optional): 脚注
footnote (str, optional): 数据来源/脚注auto-footer 渲染)
"""
m, sw, content_y, content_w, footer_y = setup_content_page(slide, data, theme)
if data.get("subtitle"):
add_text(slide, data["subtitle"],
x=m, y=content_y, w=content_w, h=0.3,
font_size=theme["body_size"], color=theme["text_muted"],
font_name=theme["body_font"])
cards = data["cards"]
n = len(cards)
# 自动 2x3 网格6 卡时)或单行
if n <= 4:
_render_row(slide, cards, theme, m, sw, content_y + 0.5, footer_y)
else:
# 2x3 网格
row1 = cards[:3]
row2 = cards[3:]
mid_y = content_y + 0.5
row_h = (footer_y - mid_y - 0.3) / 2
_render_row(slide, row1, theme, m, sw, mid_y, mid_y + row_h)
_render_row(slide, row2, theme, m, sw, mid_y + row_h + 0.15, footer_y - 0.15)
def _render_row(slide, cards, theme, m, sw, start_y, end_y):
"""渲染一行 KPI 卡片 — 分栏+极浅分割线,无阴影/圆角"""
n = len(cards)
content_w = sw - 2 * m
col_w = content_w / n
card_h = end_y - start_y - 0.2
# KPI 字号: 收敛到 28-32pt原 44pt 太 SaaS 感)
kpi_num_size = min(theme.get("kpi_size", 44), 32)
change_size = min(theme["body_size"], 11)
for i, card in enumerate(cards):
cx = m + i * col_w
# 分割线(第一栏不画左边线)
if i > 0:
add_rect(slide, cx, start_y + 0.2, 0.008, card_h - 0.4,
fill=theme["border"])
# 指标名 — 小字、顶部
add_text(slide, card["label"],
x=cx + 0.25, y=start_y + 0.15, w=col_w - 0.5, h=0.35,
font_size=theme["body_size"], color=theme["text_muted"],
align="left", font_name=theme["body_font"])
# 大数字 — 28-32pt克制
add_text(slide, card["value"],
x=cx + 0.25, y=start_y + 0.55, w=col_w - 0.5, h=1.0,
font_size=kpi_num_size, color=theme["primary"],
bold=True, align="left", valign="middle",
font_name=theme["header_font"])
# 变化值 — 10-12pt + 次要色
if card.get("change"):
change = card["change"]
is_positive = change.startswith("+") or (not change.startswith("-") and "%" in change)
color = theme["positive"] if is_positive else theme["negative"]
add_text(slide, change,
x=cx + 0.25, y=start_y + 1.6, w=col_w - 0.5, h=0.3,
font_size=change_size, color=color,
font_name=theme["body_font"])
# 脚注
if card.get("note"):
add_text(slide, card["note"],
x=cx + 0.25, y=start_y + 2.0, w=col_w - 0.5, h=0.5,
font_size=theme.get("footer_size", 8), color=theme["text_muted"],
font_name=theme["body_font"])