| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- # -*- 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()
|