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
All checks were successful
CD Pipeline / deploy (push) Successful in 2m41s
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
# ── 日報 PPT(4頁)────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user