fix(ppt): footer→bottom, font spec (Courier New/Microsoft JhengHei), 50-item paginated table, remove old single-page remnant
All checks were successful
CD Pipeline / deploy (push) Successful in 2m41s

This commit is contained in:
OoO
2026-05-02 15:32:54 +08:00
parent 0b82350745
commit 76304602b1

View File

@@ -74,6 +74,20 @@ _STRAT_COLORS = {
_STRAT_ORDER = ['加碼', '機會', '收割', '觀察', '持穩']
# ── 字體規範 (對齊 MOMO Pro design-tokens.css) ─────────────────────────────────
# momo-font-display: JetBrains Mono / Space Mono (點陣等寬機械感—數字/標驔)
# momo-font-family: Inter + PingFang TC / Microsoft JhengHei (中英混排內文)
_FONT_MONO = "Courier New" # 時作最靠論的等寬 fallback (點陣風格)
_FONT_BODY = "Microsoft JhengHei" # 中文圓體 / 黑體 (內文、表格)
_FONT_LABEL = "Arial" # 英文標籤 (OPENCLAW badge etc.)
_SLIDE_H = 19.05 # 投影片體高 cm
_FOOTER_Y = 18.38 # footer 起始位置(底部對齊)
_FOOTER_H = 0.67 # footer 高度
_CONTENT_Y = 1.85 # 內容源頂(頁首後)
_CONTENT_B = 18.2 # 內容底面footer 前 0.18cm
def check_pptx_available() -> bool:
try:
import pptx # noqa: F401
@@ -117,7 +131,7 @@ def _add_rect(slide, l, t, w, h, fill_hex, line_hex=None):
def _add_text(slide, text, l, t, w, h,
bold=False, size=14, color=_WHITE,
align="left", valign="top", wrap=True):
align="left", valign="top", wrap=True, font_name=None):
from pptx.util import Pt
from pptx.enum.text import PP_ALIGN
from pptx.dml.color import RGBColor
@@ -140,58 +154,66 @@ def _add_text(slide, text, l, t, w, h,
p.alignment = PP_ALIGN.CENTER
elif align == "right":
p.alignment = PP_ALIGN.RIGHT
for run in p.runs:
run.font.bold = bold
run.font.size = Pt(size)
if font_name:
run.font.name = font_name
run.font.color.rgb = RGBColor(
int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
return txb
def _add_footer(slide, prs_w_cm=33.87):
"""墨底色頁腳 + 焦糖橘品牌文字"""
_add_rect(slide, 0, 13.5, prs_w_cm, 0.79, _FOOTER_BG)
# 左焦糖橘細條
_add_rect(slide, 0, 13.5, 0.4, 0.79, _BRAND_OG)
"""墨底色頁腳,固定在投影片最底部"""
_add_rect(slide, 0, _FOOTER_Y, prs_w_cm, _FOOTER_H, _FOOTER_BG)
_add_rect(slide, 0, _FOOTER_Y, 0.4, _FOOTER_H, _BRAND_OG) # 左焦糖橘細條
_add_text(slide, "♥ Powered by OpenClaw",
prs_w_cm - 7, 13.55, 6.8, 0.68,
size=9, color=_BRAND_OG, align="right")
prs_w_cm - 7.5, _FOOTER_Y + 0.05, 7.2, _FOOTER_H - 0.08,
size=8, color=_BRAND_OG, align="right",
font_name=_FONT_LABEL)
def _add_header(slide, title_text, prs_w_cm=33.87):
"""焦糖橘頁首帶,高度 1.7cm左側深色加深條"""
"""焦糖橘頁首帶 1.7cm中文圓體標題"""
_add_rect(slide, 0, 0, prs_w_cm, 1.7, _BRAND_OG)
_add_rect(slide, 0, 0, 0.5, 1.7, _BRAND_OG2) # 左加深條
_add_rect(slide, 0, 0, 0.5, 1.7, _BRAND_OG2)
_add_text(slide, title_text, 0.8, 0.1, prs_w_cm - 1.2, 1.5,
bold=True, size=18, color=_WHITE, valign="middle")
bold=True, size=18, color=_WHITE, valign="middle",
font_name=_FONT_BODY)
def _add_empty_state(slide, title, detail, W=33.87):
"""避免產出視覺空白頁;用明確診斷文字說明缺哪一段資料。"""
"""避免產出視覺空白頁"""
_add_rect(slide, 2.2, 5.0, W - 4.4, 3.8, _LIGHT_GRAY, line_hex=_SUBTLE)
_add_rect(slide, 2.2, 5.0, 0.4, 3.8, _BRAND_OG) # 左色條
_add_rect(slide, 2.2, 5.0, 0.4, 3.8, _BRAND_OG)
_add_text(slide, title, 3.0, 5.65, W - 5.6, 0.75,
bold=True, size=18, color=_DARK_TEXT, align="center")
bold=True, size=18, color=_DARK_TEXT, align="center",
font_name=_FONT_BODY)
_add_text(slide, detail, 3.2, 6.75, W - 6.4, 1.0,
size=12, color=_SUBTEXT, align="center")
size=12, color=_SUBTEXT, align="center",
font_name=_FONT_BODY)
def _kpi_card(slide, l, t, w, h, fill, label, value, sub=""):
"""暖色 KPI 卡label 10pt 上方value 30pt 粗體置中,底部輔助文字 9pt"""
"""暖色 KPI 卡label 圓體小字value 點陣等寬大字置中"""
_add_rect(slide, l, t, w, h, fill)
# 左側白色細透明條,增加層次
_add_rect(slide, l, t, 0.12, h, "FFFFFF")
_add_rect(slide, l, t, 0.12, h, "FFFFFF") # 左田白透明條
_add_text(slide, label,
l + 0.35, t + 0.25, w - 0.55, 0.65,
bold=False, size=10, color="FAF7F0")
bold=False, size=10, color="FAF7F0",
font_name=_FONT_BODY)
_add_text(slide, value,
l + 0.2, t + 0.85, w - 0.4, 1.6,
bold=True, size=30, color="FFFFFF", align="center", valign="middle")
l + 0.2, t + 0.85, w - 0.4, h - 1.55,
bold=True, size=30, color="FFFFFF",
align="center", valign="middle",
font_name=_FONT_MONO) # 數字用點陣等寬字體
if sub:
_add_text(slide, sub,
l + 0.2, t + h - 0.7, w - 0.4, 0.6,
size=9, color="FAF7F0", align="center")
size=9, color="FAF7F0", align="center",
font_name=_FONT_BODY)
def _horiz_bar(slide, l, t, h_row, label, value, total, fill_hex, max_w=14.0):
@@ -393,84 +415,104 @@ def _cover_slide(prs, big_title: str, sub1: str, sub2: str = ""):
return slide
# ── 通用商品表格投影片(含業績佔比視覺條)────────────────────────────────────────
def _product_table_slide(prs, header_text, products, W=33.87):
slide = prs.slides.add_slide(prs.slide_layouts[6])
_add_rect(slide, 0, 0, W, 19.05, _WHITE)
_add_header(slide, header_text)
# ── 分頁商品表格(支援 50 項,多頁)───────────────────────────────────────────────────
def _product_table_slide(prs, header_text, products, W=33.87, max_items=50):
"""分頁顯示商品表格,每頁最多 18 項max_items 附預設 50 項"""
import math
HEADER_END = 1.85 # 頁首帶底部
TABLE_HDR_H = 0.72 # 表頭高
ROW_H = 0.88 # 標準行高
AVAIL_H = _CONTENT_B - HEADER_END - TABLE_HDR_H # 每頁可用高度
ROWS_PER_PAGE = max(1, int(AVAIL_H / ROW_H)) # 每頁行數≈ 18
if not products:
_add_empty_state(
slide,
"本頁沒有可顯示的商品資料",
"請確認該日期或期間是否已有匯入業績資料,或改查最新有資料日期。",
W,
)
all_prods = products[:max_items]
if not all_prods:
slide = prs.slides.add_slide(prs.slide_layouts[6])
_add_rect(slide, 0, 0, W, _SLIDE_H, _WHITE)
_add_header(slide, header_text)
_add_empty_state(slide, "本頁沒有可顯示的商品資料",
"請確認該期間是否已有匯入業績資料。", W)
_add_footer(slide, W)
return slide
return
display = products[:10]
top_rev = max(float(p.get('revenue', 1)) for p in display) or 1
top_rev = max(float(p.get('revenue', 1)) for p in all_prods) or 1
total_pages = math.ceil(len(all_prods) / ROWS_PER_PAGE)
# ── 表頭
_add_rect(slide, 0.4, 1.85, W - 0.8, 0.68, _BRAND_OG)
_add_rect(slide, 0.4, 1.85, 0.3, 0.68, _BRAND_OG2) # 左加深條
_add_text(slide, "排名", 0.5, 1.89, 1.3, 0.60, bold=True, size=10, color=_WHITE, align="center")
_add_text(slide, "商品名稱", 1.95, 1.89, W - 18.5, 0.60, bold=True, size=10, color=_WHITE)
_add_text(slide, "業績佔比", W - 16.2, 1.89, 5.8, 0.60, bold=True, size=9, color=_WHITE, align="center")
_add_text(slide, "月業績", W - 9.8, 1.89, 5.5, 0.60, bold=True, size=10, color=_WHITE, align="right")
_add_text(slide, "訂單", W - 3.8, 1.89, 3.2, 0.60, bold=True, size=10, color=_WHITE, align="right")
for page in range(total_pages):
page_prods = all_prods[page * ROWS_PER_PAGE:(page + 1) * ROWS_PER_PAGE]
page_label = f" ({page + 1}/{total_pages})"
slide = prs.slides.add_slide(prs.slide_layouts[6])
_add_rect(slide, 0, 0, W, _SLIDE_H, _WHITE)
_add_header(slide, header_text + page_label)
row_h = (13.5 - 2.65) / max(len(display), 1) # 動態行高,撐滿可用區
row_h = max(0.75, min(row_h, 1.08)) # 限制 0.75~1.08cm
# ── 表頭
tbl_y = HEADER_END
_add_rect(slide, 0.4, tbl_y, W - 0.8, TABLE_HDR_H, _BRAND_OG)
_add_rect(slide, 0.4, tbl_y, 0.3, TABLE_HDR_H, _BRAND_OG2)
_add_text(slide, "排名", 0.5, tbl_y + 0.06, 1.3, TABLE_HDR_H - 0.12,
bold=True, size=10, color=_WHITE, align="center", font_name=_FONT_BODY)
_add_text(slide, "商品名稱", 1.95, tbl_y + 0.06, W - 19.5, TABLE_HDR_H - 0.12,
bold=True, size=10, color=_WHITE, font_name=_FONT_BODY)
_add_text(slide, "業績佔比", W - 17.2, tbl_y + 0.06, 5.5, TABLE_HDR_H - 0.12,
bold=True, size=9, color=_WHITE, align="center", font_name=_FONT_BODY)
_add_text(slide, "月業績", W - 11.3, tbl_y + 0.06, 5.8, TABLE_HDR_H - 0.12,
bold=True, size=10, color=_WHITE, align="right", font_name=_FONT_BODY)
_add_text(slide, "訂單", W - 5.0, tbl_y + 0.06, 4.4, TABLE_HDR_H - 0.12,
bold=True, size=10, color=_WHITE, align="right", font_name=_FONT_BODY)
for i, p in enumerate(display):
bg = _LIGHT_GRAY if i % 2 == 0 else _WHITE
row_t = 2.65 + i * row_h
rev = float(p.get('revenue', 0))
pct = rev / top_rev if top_rev else 0
ord_v = p.get('orders', p.get('order_count', 0))
font_sz = 10 if row_h >= 0.85 else 9
# ── 資料行
for j, p in enumerate(page_prods):
actual_rank = page * ROWS_PER_PAGE + j + 1
bg = _LIGHT_GRAY if j % 2 == 0 else _WHITE
row_t = tbl_y + TABLE_HDR_H + j * ROW_H
rev = float(p.get('revenue', 0))
pct = rev / top_rev if top_rev else 0
ord_v = p.get('orders', p.get('order_count', 0))
_add_rect(slide, 0.4, row_t, W - 0.8, row_h - 0.05, bg)
_add_rect(slide, 0.4, row_t, W - 0.8, ROW_H - 0.04, bg)
# 排名TOP3 焦糖橘圓底
rank_fill = _BRAND_OG if i < 3 else _SUBTLE
rank_color = _WHITE if i < 3 else _SUBTEXT
_add_rect(slide, 0.55, row_t + 0.1, 1.0, row_h - 0.25, rank_fill)
_add_text(slide, str(i + 1),
0.55, row_t + 0.1, 1.0, row_h - 0.25,
bold=(i < 3), size=font_sz, color=rank_color, align="center", valign="middle")
# 排名圓框TOP3 焦糖橘)
rank_fill = _BRAND_OG if actual_rank <= 3 else (_KPI_HONEY if actual_rank <= 10 else _SUBTLE)
rank_color = _WHITE if actual_rank <= 10 else _SUBTEXT
_add_rect(slide, 0.55, row_t + 0.1, 0.95, ROW_H - 0.22, rank_fill)
_add_text(slide, str(actual_rank),
0.55, row_t + 0.1, 0.95, ROW_H - 0.22,
bold=(actual_rank <= 3), size=10, color=rank_color,
align="center", valign="middle", font_name=_FONT_MONO)
# 商品名稱
_add_text(slide, str(p.get('name', ''))[:45],
1.95, row_t + 0.1, W - 18.7, row_h - 0.2,
size=font_sz, color=_DARK_TEXT)
# 商品名稱
_add_text(slide, str(p.get('name', ''))[:44],
1.95, row_t + 0.1, W - 19.7, ROW_H - 0.2,
size=10, color=_DARK_TEXT, font_name=_FONT_BODY)
# 業績佔比視覺條
bar_max_w = 5.0
bar_w = max(0.08, pct * bar_max_w)
bar_y = row_t + ROW_H * 0.38
bar_h = 0.25
_add_rect(slide, W - 17.0, bar_y, bar_max_w, bar_h, _SUBTLE)
_add_rect(slide, W - 17.0, bar_y, bar_w, bar_h, _BRAND_OG)
_add_text(slide, f"{pct*100:.0f}%",
W - 17.0, row_t + 0.08, bar_max_w, ROW_H - 0.2,
size=8, color=_TERTIARY, align="right", font_name=_FONT_MONO)
# 月業績
_add_text(slide, f"NT${rev:,.0f}",
W - 11.3, row_t + 0.1, 5.8, ROW_H - 0.2,
bold=(actual_rank == 1), size=10, color=_DARK_TEXT,
align="right", font_name=_FONT_MONO)
# 訂單
if ord_v:
_add_text(slide, f"{int(ord_v):,}",
W - 5.0, row_t + 0.1, 4.4, ROW_H - 0.2,
size=9, color=_SUBTEXT, align="right", font_name=_FONT_MONO)
_add_footer(slide, W)
# 業績佔比視覺條
bar_max_w = 5.3
bar_filled = max(0.1, pct * bar_max_w)
bar_y = row_t + row_h * 0.35
bar_h_val = 0.28
_add_rect(slide, W - 16.0, bar_y, bar_max_w, bar_h_val, _SUBTLE)
_add_rect(slide, W - 16.0, bar_y, bar_filled, bar_h_val, _BRAND_OG)
_add_text(slide, f"{pct*100:.0f}%",
W - 16.0, row_t + 0.08, bar_max_w, row_h - 0.2,
size=8, color=_SUBTEXT, align="right")
# 月業績
_add_text(slide, f"NT${rev:,.0f}",
W - 9.8, row_t + 0.1, 5.5, row_h - 0.2,
bold=(i == 0), size=font_sz, color=_DARK_TEXT, align="right")
# 訂單
if ord_v:
_add_text(slide, f"{int(ord_v):,}",
W - 3.8, row_t + 0.1, 3.2, row_h - 0.2,
size=font_sz - 1, color=_SUBTEXT, align="right")
_add_footer(slide, W)
return slide
# ── 日報 PPT4頁────────────────────────────────────────────────────────────