|
|
@@ -0,0 +1,461 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""生成六安裕洋MES实施进度汇报 PPT(含甘特图)"""
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import shutil
|
|
|
+from datetime import date, timedelta
|
|
|
+from pathlib import Path
|
|
|
+
|
|
|
+from PIL import Image
|
|
|
+from pptx import Presentation
|
|
|
+from pptx.dml.color import RGBColor
|
|
|
+from pptx.enum.shapes import MSO_SHAPE
|
|
|
+from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
|
|
+from pptx.oxml.ns import nsmap
|
|
|
+from pptx.oxml import parse_xml
|
|
|
+from pptx.util import Inches, Pt, Emu
|
|
|
+
|
|
|
+# ---------- 路径 ----------
|
|
|
+PIC_DIR = Path(r"C:\Users\j2742\Desktop\temp\大洋\pic")
|
|
|
+ASSETS_DIR = Path(r"C:\Users\j2742\.cursor\projects\c-work-demo-dayang-mescloud\assets")
|
|
|
+OUT1 = Path(r"C:\Users\j2742\Desktop\temp\大洋\实施文档\六安裕洋MES实施进度汇报.pptx")
|
|
|
+OUT2 = Path(r"c:\work\demo\dayang\mescloud\docs\六安裕洋MES实施进度汇报.pptx")
|
|
|
+CACHE_DIR = Path(r"c:\work\demo\dayang\mescloud\_ppt_img_cache")
|
|
|
+
|
|
|
+# 用户精选实时效果图(第2页)
|
|
|
+EFFECT_IMAGES = [
|
|
|
+ ("生产管理大屏", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-eb11b4ec-13e0-4792-833d-13a7d304a2a5.png"),
|
|
|
+ ("设备综合效率", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-74f8c1fc-7426-4472-9dda-a4abdb79cc23.png"),
|
|
|
+ ("能源监控大屏", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-1759c9f1-d702-4b17-b35b-0d8d8cfd66cf.png"),
|
|
|
+ ("ERP排产看板", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-220cd888-53b8-4247-b74f-9b3b5d56a8bf.png"),
|
|
|
+ ("生产追溯明细", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-a4a18a5e-f2ab-42a3-a9c5-2bcfae5d1233.png"),
|
|
|
+ ("白件气密管理", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-8dd92c21-a7aa-48d0-81a3-4a1107492a9a.png"),
|
|
|
+ ("弧焊过程参数", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-15f57559-f837-464f-af8a-b4e656767283.png"),
|
|
|
+ ("PDA产线扫码", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_60b4a34fc5a9f4ca3195ea55e3a2ace1-787d700c-4131-4806-abbf-ff01c10c9679.png"),
|
|
|
+]
|
|
|
+# ---------- 配色 ----------
|
|
|
+HEADER_BG = RGBColor(0x0B, 0x3D, 0x5C)
|
|
|
+ACCENT_GREEN = RGBColor(0x1B, 0x8A, 0x5A)
|
|
|
+TEAL_BAR = RGBColor(0x0D, 0x7A, 0x6F)
|
|
|
+TEAL_LIGHT = RGBColor(0x2A, 0xA8, 0x98)
|
|
|
+AXIS_GRAY = RGBColor(0x90, 0xA4, 0xAE)
|
|
|
+TEXT_DARK = RGBColor(0x1A, 0x2A, 0x3A)
|
|
|
+TEXT_MUTED = RGBColor(0x5A, 0x6A, 0x7A)
|
|
|
+WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
|
|
+CARD_BG = RGBColor(0xF4, 0xF7, 0xFA)
|
|
|
+ROW_ALT = RGBColor(0xEE, 0xF4, 0xF8)
|
|
|
+LINE_SOFT = RGBColor(0xD0, 0xDC, 0xE4)
|
|
|
+
|
|
|
+SLIDE_W = Inches(13.333)
|
|
|
+SLIDE_H = Inches(7.5)
|
|
|
+
|
|
|
+
|
|
|
+def set_run_font(run, name="微软雅黑", size=12, bold=False, color=TEXT_DARK):
|
|
|
+ run.font.name = name
|
|
|
+ run.font.size = Pt(size)
|
|
|
+ run.font.bold = bold
|
|
|
+ run.font.color.rgb = color
|
|
|
+ # 东亚字体
|
|
|
+ rPr = run._r.get_or_add_rPr()
|
|
|
+ ea = rPr.find("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rFonts")
|
|
|
+ # 使用 a:ea / a:latin
|
|
|
+ from pptx.oxml.ns import qn
|
|
|
+ for tag in ("latin", "ea", "cs"):
|
|
|
+ el = rPr.find(qn(f"a:{tag}"))
|
|
|
+ if el is None:
|
|
|
+ el = parse_xml(
|
|
|
+ f'<a:{tag} xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" typeface="{name}"/>'
|
|
|
+ )
|
|
|
+ rPr.append(el)
|
|
|
+ else:
|
|
|
+ el.set("typeface", name)
|
|
|
+
|
|
|
+
|
|
|
+def add_textbox(slide, left, top, width, height, text, size=12, bold=False, color=TEXT_DARK, align=PP_ALIGN.LEFT, font="微软雅黑"):
|
|
|
+ box = slide.shapes.add_textbox(left, top, width, height)
|
|
|
+ tf = box.text_frame
|
|
|
+ tf.word_wrap = True
|
|
|
+ p = tf.paragraphs[0]
|
|
|
+ p.alignment = align
|
|
|
+ run = p.add_run()
|
|
|
+ run.text = text
|
|
|
+ set_run_font(run, font, size, bold, color)
|
|
|
+ return box
|
|
|
+
|
|
|
+
|
|
|
+def add_rect(slide, left, top, width, height, fill, line=None):
|
|
|
+ shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
|
|
|
+ shape.fill.solid()
|
|
|
+ shape.fill.fore_color.rgb = fill
|
|
|
+ if line is None:
|
|
|
+ shape.line.fill.background()
|
|
|
+ else:
|
|
|
+ shape.line.color.rgb = line
|
|
|
+ shape.line.width = Pt(0.75)
|
|
|
+ return shape
|
|
|
+
|
|
|
+
|
|
|
+def add_round_rect(slide, left, top, width, height, fill, line=None):
|
|
|
+ shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
|
|
|
+ shape.fill.solid()
|
|
|
+ shape.fill.fore_color.rgb = fill
|
|
|
+ if line is None:
|
|
|
+ shape.line.fill.background()
|
|
|
+ else:
|
|
|
+ shape.line.color.rgb = line
|
|
|
+ shape.line.width = Pt(0.75)
|
|
|
+ # 较小圆角
|
|
|
+ try:
|
|
|
+ shape.adjustments[0] = 0.08
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+ return shape
|
|
|
+
|
|
|
+
|
|
|
+def date_to_x(d: date, axis_start: date, axis_end: date, chart_left, chart_width):
|
|
|
+ total = (axis_end - axis_start).days
|
|
|
+ if total <= 0:
|
|
|
+ return chart_left
|
|
|
+ ratio = max(0.0, min(1.0, (d - axis_start).days / total))
|
|
|
+ return chart_left + int(chart_width * ratio)
|
|
|
+
|
|
|
+
|
|
|
+def resize_image(src: Path, max_w: int = 1000) -> Path:
|
|
|
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
+ out = CACHE_DIR / f"{src.stem}_w{max_w}{src.suffix.lower()}"
|
|
|
+ if out.exists() and out.stat().st_mtime >= src.stat().st_mtime:
|
|
|
+ return out
|
|
|
+ img = Image.open(src)
|
|
|
+ if img.mode in ("RGBA", "P"):
|
|
|
+ img = img.convert("RGB")
|
|
|
+ w, h = img.size
|
|
|
+ if w > max_w:
|
|
|
+ nh = int(h * max_w / w)
|
|
|
+ img = img.resize((max_w, nh), Image.Resampling.LANCZOS)
|
|
|
+ # JPEG 统一
|
|
|
+ if out.suffix.lower() in (".jpg", ".jpeg"):
|
|
|
+ img.save(out, "JPEG", quality=88, optimize=True)
|
|
|
+ else:
|
|
|
+ img.save(out, "PNG", optimize=True)
|
|
|
+ return out
|
|
|
+
|
|
|
+
|
|
|
+def find_image(candidates: list[str]) -> Path | None:
|
|
|
+ for name in candidates:
|
|
|
+ p = PIC_DIR / name
|
|
|
+ if p.exists():
|
|
|
+ return p
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+# ---------- 甘特数据(实施阶段)----------
|
|
|
+# done=True 已完成;done=False 本月推进中
|
|
|
+GANTT_ROWS = [
|
|
|
+ ("阶段一 环境准备", date(2026, 4, 27), date(2026, 5, 6), True),
|
|
|
+ ("阶段二 软件预开发", date(2026, 4, 30), date(2026, 5, 6), True),
|
|
|
+ ("阶段三 蓝图设计", date(2026, 5, 7), date(2026, 6, 1), True),
|
|
|
+ ("阶段四 硬件与工位对接", date(2026, 5, 8), date(2026, 5, 16), True),
|
|
|
+ ("阶段五 流程与主数据", date(2026, 5, 18), date(2026, 5, 30), True),
|
|
|
+ ("阶段六 多系统对接", date(2026, 6, 1), date(2026, 6, 18), True),
|
|
|
+ ("阶段七 培训驻场", date(2026, 6, 22), date(2026, 6, 27), True),
|
|
|
+ ("七月 上线陪产", date(2026, 7, 1), date(2026, 7, 24), False),
|
|
|
+]
|
|
|
+
|
|
|
+AXIS_START = date(2026, 4, 27)
|
|
|
+AXIS_END = date(2026, 7, 24)
|
|
|
+TODAY = date(2026, 7, 21)
|
|
|
+ORANGE_BAR = RGBColor(0xE0, 0x8A, 0x2E)
|
|
|
+ORANGE_SOFT = RGBColor(0xF5, 0xE6, 0xD0)
|
|
|
+
|
|
|
+WEEKLY_PROGRESS = [
|
|
|
+ ("阶段三", "5/7-5/10", "五月第一周", "8", "8", "100%"),
|
|
|
+ ("阶段四", "5/11-5/16", "五月第二周", "9", "9", "100%"),
|
|
|
+ ("阶段四&五", "5/18-5/23", "五月第三周", "9", "9", "100%"),
|
|
|
+ ("阶段五", "5/25-5/30", "五月第四周", "7", "7", "100%"),
|
|
|
+ ("阶段六", "6/1-6/6", "六月第一周", "7", "7", "100%"),
|
|
|
+ ("阶段六", "6/8-6/13", "六月第二周", "6", "6", "100%"),
|
|
|
+ ("阶段六", "6/13-6/18", "六月第三周", "10", "10", "100%"),
|
|
|
+ ("阶段七", "6/22-6/27", "六月第四周", "8", "8", "100%"),
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+def build_slide1(prs: Presentation):
|
|
|
+ slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
|
|
+
|
|
|
+ # 顶栏
|
|
|
+ add_rect(slide, 0, 0, SLIDE_W, Inches(0.88), HEADER_BG)
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.28), Inches(0.10), Inches(8.8), Inches(0.40),
|
|
|
+ "六安裕洋MES项目实施进度汇报", size=24, bold=True, color=WHITE,
|
|
|
+ )
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.28), Inches(0.50), Inches(9.2), Inches(0.30),
|
|
|
+ "京宜安 · 截至2026年7月21日 · 5–6月周任务64项已100%完成 · 本月聚焦现场培训/实际上线/陪产",
|
|
|
+ size=11, bold=False, color=RGBColor(0xB8, 0xD4, 0xE8),
|
|
|
+ )
|
|
|
+
|
|
|
+ # 右上统计胶囊
|
|
|
+ stats = [("8周次", "5–6月"), ("64/64", "周任务完成"), ("100%", "合格率"), ("7月", "上线陪产")]
|
|
|
+ x0 = Inches(9.05)
|
|
|
+ for i, (v, lab) in enumerate(stats):
|
|
|
+ left = x0 + Inches(i * 1.02)
|
|
|
+ add_round_rect(slide, left, Inches(0.12), Inches(0.96), Inches(0.64), RGBColor(0x0E, 0x52, 0x78))
|
|
|
+ tone = ORANGE_BAR if i == 3 else (ACCENT_GREEN if i == 2 else WHITE)
|
|
|
+ add_textbox(slide, left, Inches(0.14), Inches(0.96), Inches(0.34), v, size=13, bold=True, color=tone, align=PP_ALIGN.CENTER)
|
|
|
+ add_textbox(slide, left, Inches(0.44), Inches(0.96), Inches(0.28), lab, size=8, color=RGBColor(0xA8, 0xC8, 0xDC), align=PP_ALIGN.CENTER)
|
|
|
+
|
|
|
+ # ===== 左侧:周任务明细表 =====
|
|
|
+ table_left = Inches(0.22)
|
|
|
+ table_top = Inches(1.00)
|
|
|
+ table_w = Inches(5.55)
|
|
|
+ table_h = Inches(4.55)
|
|
|
+ add_round_rect(slide, table_left, table_top, table_w, table_h, CARD_BG)
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.38), Inches(1.08), Inches(5.2), Inches(0.28),
|
|
|
+ "周任务明细(阶段三~七 · 全部完成)", size=12, bold=True, color=HEADER_BG,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 表头
|
|
|
+ headers = [("阶段", 0.95), ("起止", 1.15), ("任务周", 1.35), ("完成", 0.70), ("合格率", 0.85)]
|
|
|
+ hx = Inches(0.38)
|
|
|
+ hy = Inches(1.42)
|
|
|
+ add_rect(slide, Inches(0.35), hy, Inches(5.28), Inches(0.30), HEADER_BG)
|
|
|
+ cx = hx
|
|
|
+ for title, w in headers:
|
|
|
+ add_textbox(slide, cx, hy + Inches(0.02), Inches(w), Inches(0.26), title, size=9, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
|
|
|
+ cx += Inches(w)
|
|
|
+
|
|
|
+ row_h = Inches(0.38)
|
|
|
+ for i, (phase, span, week, plan, done, rate) in enumerate(WEEKLY_PROGRESS):
|
|
|
+ y = Inches(1.74) + row_h * i
|
|
|
+ bg = WHITE if i % 2 else ROW_ALT
|
|
|
+ add_rect(slide, Inches(0.35), y, Inches(5.28), row_h - Inches(0.02), bg)
|
|
|
+ vals = [phase, span, week, f"{done}/{plan}", rate]
|
|
|
+ widths = [0.95, 1.15, 1.35, 0.70, 0.85]
|
|
|
+ cx = hx
|
|
|
+ for j, (val, w) in enumerate(zip(vals, widths)):
|
|
|
+ col = ACCENT_GREEN if j >= 3 else TEXT_DARK
|
|
|
+ add_textbox(
|
|
|
+ slide, cx, y + Inches(0.04), Inches(w), Inches(0.30),
|
|
|
+ val, size=9, bold=(j >= 3), color=col, align=PP_ALIGN.CENTER,
|
|
|
+ )
|
|
|
+ cx += Inches(w)
|
|
|
+
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.38), Inches(4.90), Inches(5.2), Inches(0.45),
|
|
|
+ "合计:8 周次 · 任务 64 · 完成 64 · 合格率 100%",
|
|
|
+ size=11, bold=True, color=ACCENT_GREEN,
|
|
|
+ )
|
|
|
+
|
|
|
+ # ===== 右侧:甘特图 =====
|
|
|
+ gantt_left = Inches(5.90)
|
|
|
+ gantt_top = Inches(1.00)
|
|
|
+ gantt_w = Inches(7.15)
|
|
|
+ gantt_h = Inches(4.55)
|
|
|
+ add_round_rect(slide, gantt_left, gantt_top, gantt_w, gantt_h, CARD_BG)
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(6.05), Inches(1.08), Inches(5.0), Inches(0.28),
|
|
|
+ "实施阶段甘特图(2026-04-27 ~ 07-24)", size=12, bold=True, color=HEADER_BG,
|
|
|
+ )
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(10.4), Inches(1.10), Inches(2.4), Inches(0.26),
|
|
|
+ "今日 7/21", size=9, bold=True, color=ORANGE_BAR, align=PP_ALIGN.RIGHT,
|
|
|
+ )
|
|
|
+
|
|
|
+ label_w = Inches(2.15)
|
|
|
+ chart_left = Inches(6.05) + label_w
|
|
|
+ chart_right = Inches(12.85)
|
|
|
+ chart_width = chart_right - chart_left
|
|
|
+ axis_y = Inches(1.38)
|
|
|
+ row_h_g = Inches(0.38)
|
|
|
+ bar_h = Inches(0.20)
|
|
|
+ first_row_y = Inches(1.68)
|
|
|
+
|
|
|
+ ticks = [
|
|
|
+ date(2026, 4, 27), date(2026, 5, 11), date(2026, 5, 25),
|
|
|
+ date(2026, 6, 8), date(2026, 6, 22), date(2026, 7, 6), date(2026, 7, 21),
|
|
|
+ ]
|
|
|
+ for d in ticks:
|
|
|
+ x = date_to_x(d, AXIS_START, AXIS_END, chart_left, chart_width)
|
|
|
+ line = slide.shapes.add_shape(
|
|
|
+ MSO_SHAPE.RECTANGLE, x, first_row_y - Inches(0.04),
|
|
|
+ Pt(0.75), row_h_g * len(GANTT_ROWS) + Inches(0.08),
|
|
|
+ )
|
|
|
+ line.fill.solid()
|
|
|
+ line.fill.fore_color.rgb = LINE_SOFT
|
|
|
+ line.line.fill.background()
|
|
|
+ add_textbox(
|
|
|
+ slide, x - Inches(0.28), axis_y, Inches(0.56), Inches(0.26),
|
|
|
+ f"{d.month}/{d.day}", size=7, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 今日竖线
|
|
|
+ tx = date_to_x(TODAY, AXIS_START, AXIS_END, chart_left, chart_width)
|
|
|
+ today_line = slide.shapes.add_shape(
|
|
|
+ MSO_SHAPE.RECTANGLE, tx, first_row_y - Inches(0.04),
|
|
|
+ Pt(1.5), row_h_g * len(GANTT_ROWS) + Inches(0.08),
|
|
|
+ )
|
|
|
+ today_line.fill.solid()
|
|
|
+ today_line.fill.fore_color.rgb = ORANGE_BAR
|
|
|
+ today_line.line.fill.background()
|
|
|
+
|
|
|
+ for i, (name, start, end, done) in enumerate(GANTT_ROWS):
|
|
|
+ y = first_row_y + row_h_g * i
|
|
|
+ if i % 2 == 0:
|
|
|
+ add_rect(slide, Inches(6.00), y, Inches(6.95), row_h_g - Inches(0.02), ROW_ALT if done else ORANGE_SOFT)
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(6.05), y + Inches(0.05), label_w - Inches(0.08), Inches(0.28),
|
|
|
+ name, size=9, bold=True, color=TEXT_DARK,
|
|
|
+ )
|
|
|
+ x1 = date_to_x(start, AXIS_START, AXIS_END, chart_left, chart_width)
|
|
|
+ x2 = date_to_x(end, AXIS_START, AXIS_END, chart_left, chart_width)
|
|
|
+ bar_w = max(int(x2 - x1), int(Inches(0.10)))
|
|
|
+ by = y + (row_h_g - bar_h) // 2 - Inches(0.01)
|
|
|
+ fill = TEAL_BAR if done else ORANGE_BAR
|
|
|
+ bar = add_round_rect(slide, x1, by, bar_w, bar_h, fill)
|
|
|
+ try:
|
|
|
+ bar.adjustments[0] = 0.35
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+ span = f"{start.month}/{start.day}-{end.month}/{end.day}"
|
|
|
+ if bar_w > Inches(0.85):
|
|
|
+ add_textbox(slide, x1, by - Inches(0.02), bar_w, bar_h + Inches(0.04), span, size=7, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
|
|
|
+
|
|
|
+ # 图例
|
|
|
+ ly = Inches(5.15)
|
|
|
+ add_round_rect(slide, Inches(6.10), ly, Inches(0.22), Inches(0.14), TEAL_BAR)
|
|
|
+ add_textbox(slide, Inches(6.38), ly - Inches(0.04), Inches(1.4), Inches(0.24), "已完成", size=8, color=TEXT_MUTED)
|
|
|
+ add_round_rect(slide, Inches(7.85), ly, Inches(0.22), Inches(0.14), ORANGE_BAR)
|
|
|
+ add_textbox(slide, Inches(8.13), ly - Inches(0.04), Inches(2.6), Inches(0.24), "本月推进中(上线陪产)", size=8, color=TEXT_MUTED)
|
|
|
+
|
|
|
+ # ===== 底部:本月重点 =====
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.28), Inches(5.70), Inches(4), Inches(0.26),
|
|
|
+ "本月重点(2026年7月)", size=12, bold=True, color=HEADER_BG,
|
|
|
+ )
|
|
|
+ july_items = [
|
|
|
+ ("现场培训", "操作工/班组长/管理员\n工位扫码、异常处理、后台使用"),
|
|
|
+ ("实际上线", "HEV线正式跑生产\n数据进MES、大屏实时展示"),
|
|
|
+ ("陪产支持", "驻场观察产线运行\n问题闭环、系统优化"),
|
|
|
+ ]
|
|
|
+ by0 = Inches(6.00)
|
|
|
+ bw = Inches(4.20)
|
|
|
+ for i, (title, desc) in enumerate(july_items):
|
|
|
+ left = Inches(0.28) + bw * i
|
|
|
+ add_round_rect(slide, left, by0, bw - Inches(0.14), Inches(0.95), WHITE, ORANGE_BAR)
|
|
|
+ add_textbox(slide, left + Inches(0.14), by0 + Inches(0.08), bw - Inches(0.40), Inches(0.28), title, size=13, bold=True, color=ORANGE_BAR)
|
|
|
+ box = slide.shapes.add_textbox(left + Inches(0.14), by0 + Inches(0.38), bw - Inches(0.40), Inches(0.52))
|
|
|
+ tf = box.text_frame
|
|
|
+ tf.word_wrap = True
|
|
|
+ p = tf.paragraphs[0]
|
|
|
+ run = p.add_run()
|
|
|
+ run.text = desc
|
|
|
+ set_run_font(run, size=10, color=TEXT_MUTED)
|
|
|
+
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.28), Inches(7.15), Inches(12.7), Inches(0.25),
|
|
|
+ "六安裕洋 MES · 京宜安实施团队 · 进度汇报 · 第1页 · 2026-07-21",
|
|
|
+ size=8, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def build_slide2(prs: Presentation, used_images: list[tuple[str, str]]):
|
|
|
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
|
+
|
|
|
+ add_rect(slide, 0, 0, SLIDE_W, Inches(0.78), HEADER_BG)
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.35), Inches(0.18), Inches(10), Inches(0.42),
|
|
|
+ "效果展示 · 实时运行截图", size=22, bold=True, color=WHITE,
|
|
|
+ )
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(9.5), Inches(0.25), Inches(3.5), Inches(0.35),
|
|
|
+ "7月:现场培训 · 实际上线 · 陪产", size=11, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.RIGHT,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 2x4 网格 — 使用用户精选效果图
|
|
|
+ caps_and_files = EFFECT_IMAGES
|
|
|
+
|
|
|
+ cols = 4
|
|
|
+ margin_l = Inches(0.32)
|
|
|
+ margin_t = Inches(0.95)
|
|
|
+ gap_x, gap_y = Inches(0.18), Inches(0.22)
|
|
|
+ cell_w = (SLIDE_W - margin_l * 2 - gap_x * (cols - 1)) / cols
|
|
|
+ cell_h = Inches(2.95)
|
|
|
+ img_area_h = Inches(2.45)
|
|
|
+ cap_h = Inches(0.38)
|
|
|
+
|
|
|
+ for idx, (caption, src) in enumerate(caps_and_files):
|
|
|
+ row = idx // cols
|
|
|
+ col = idx % cols
|
|
|
+ left = margin_l + col * (cell_w + gap_x)
|
|
|
+ top = margin_t + row * (cell_h + gap_y)
|
|
|
+
|
|
|
+ add_round_rect(slide, left, top, cell_w, cell_h, WHITE, LINE_SOFT)
|
|
|
+
|
|
|
+ if not src.exists():
|
|
|
+ add_textbox(slide, left + Inches(0.1), top + Inches(0.8), cell_w - Inches(0.2), Inches(0.5),
|
|
|
+ "(图片缺失)", size=12, color=TEXT_MUTED, align=PP_ALIGN.CENTER)
|
|
|
+ used_images.append((caption, "MISSING"))
|
|
|
+ else:
|
|
|
+ cached = resize_image(src, 1200)
|
|
|
+ with Image.open(cached) as im:
|
|
|
+ iw, ih = im.size
|
|
|
+ max_w = cell_w - Inches(0.16)
|
|
|
+ max_h = img_area_h - Inches(0.12)
|
|
|
+ scale = min(max_w / iw, max_h / ih)
|
|
|
+ disp_w = int(iw * scale)
|
|
|
+ disp_h = int(ih * scale)
|
|
|
+ if disp_w > max_w:
|
|
|
+ disp_w = int(max_w)
|
|
|
+ if disp_h > max_h:
|
|
|
+ disp_h = int(max_h)
|
|
|
+ ix = left + (cell_w - disp_w) // 2
|
|
|
+ iy = top + Inches(0.08) + (max_h - disp_h) // 2
|
|
|
+ slide.shapes.add_picture(str(cached), ix, iy, width=disp_w, height=disp_h)
|
|
|
+ used_images.append((caption, src.name))
|
|
|
+
|
|
|
+ add_textbox(
|
|
|
+ slide, left + Inches(0.06), top + img_area_h, cell_w - Inches(0.12), cap_h,
|
|
|
+ caption, size=11, bold=True, color=HEADER_BG, align=PP_ALIGN.CENTER,
|
|
|
+ )
|
|
|
+
|
|
|
+ add_textbox(
|
|
|
+ slide, Inches(0.35), Inches(7.15), Inches(12.5), Inches(0.28),
|
|
|
+ "六安裕洋 MES · 京宜安实施团队 · 效果展示 · 第2页 · 2026-07-21",
|
|
|
+ size=8, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ OUT1.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ OUT2.parent.mkdir(parents=True, exist_ok=True)
|
|
|
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
+
|
|
|
+ prs = Presentation()
|
|
|
+ prs.slide_width = SLIDE_W
|
|
|
+ prs.slide_height = SLIDE_H
|
|
|
+
|
|
|
+ build_slide1(prs)
|
|
|
+ used: list[tuple[str, str]] = []
|
|
|
+ build_slide2(prs, used)
|
|
|
+
|
|
|
+ # 先写仓库副本(避免桌面文件被占用导致整次失败)
|
|
|
+ prs.save(str(OUT2))
|
|
|
+ print(f"OUT2: {OUT2} exists={OUT2.exists()} size={OUT2.stat().st_size}")
|
|
|
+
|
|
|
+ desktop_out = OUT1
|
|
|
+ try:
|
|
|
+ prs.save(str(OUT1))
|
|
|
+ except PermissionError:
|
|
|
+ desktop_out = OUT1.with_name("六安裕洋MES实施进度汇报_效果图更新.pptx")
|
|
|
+ prs.save(str(desktop_out))
|
|
|
+ print(f"桌面原文件被占用,已另存为: {desktop_out}")
|
|
|
+ print(f"OUT1: {desktop_out} exists={desktop_out.exists()} size={desktop_out.stat().st_size}")
|
|
|
+
|
|
|
+ print("=== 使用图片 ===")
|
|
|
+ for cap, name in used:
|
|
|
+ print(f" - {cap}: {name}")
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|