# -*- coding: utf-8 -*- """ 智慧医院项目答辩 PPT v2 结构:项目主题 → 项目需求 → 技术栈 → 实现功能 → 六人分工 要求:图文表并茂、色彩丰富、字体统一、≤20 页、布局防重叠 """ from __future__ import annotations from pathlib import Path from PIL import Image as PILImage from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.oxml import parse_xml from pptx.oxml.ns import qn from pptx.util import Inches, Pt ROOT = Path(__file__).resolve().parent ASSETS = ROOT / "assets_v2" OUT = ROOT / "智慧医院_项目答辩PPT.pptx" OUT_ALT = ROOT / "智慧医院_项目答辩PPT_v2.pptx" SLIDE_W = Inches(13.333) SLIDE_H = Inches(7.5) HEADER_H = 0.88 FOOTER_Y = 7.12 TOTAL = 18 # —— 统一字体 —— FONT = "微软雅黑" SIZE_H1 = 22 SIZE_H2 = 16 SIZE_BODY = 12 SIZE_SMALL = 11 SIZE_TABLE = 11 # —— 多彩配色 —— NAVY = RGBColor(0x0F, 0x20, 0x5A) BLUE = RGBColor(0x25, 0x63, 0xEB) CYAN = RGBColor(0x06, 0xB6, 0xD4) TEAL = RGBColor(0x14, 0xB8, 0xA6) PURPLE = RGBColor(0xA8, 0x55, 0xF7) VIOLET = RGBColor(0x8B, 0x5C, 0xF6) ORANGE = RGBColor(0xF9, 0x73, 0x16) CORAL = RGBColor(0xFB, 0x71, 0x85) PINK = RGBColor(0xEC, 0x48, 0x99) GREEN = RGBColor(0x22, 0xC5, 0x5E) INDIGO = RGBColor(0x63, 0x66, 0xF1) YELLOW = RGBColor(0xEA, 0xB3, 0x08) DARK = RGBColor(0x0F, 0x17, 0x2A) MUTED = RGBColor(0x64, 0x74, 0x8B) LIGHT = RGBColor(0xF8, 0xFA, 0xFC) WHITE = RGBColor(0xFF, 0xFF, 0xFF) SOFT = RGBColor(0xF1, 0xF5, 0xF9) PANEL = RGBColor(0x0C, 0x1A, 0x40) SECTION_COLORS = [BLUE, CORAL, PURPLE, TEAL, ORANGE] # 五大篇章色 def set_font(run, size=SIZE_BODY, bold=False, color=DARK): run.font.size = Pt(size) run.font.bold = bold run.font.color.rgb = color run.font.name = FONT rPr = run._r.get_or_add_rPr() ea = rPr.find(qn("a:ea")) if ea is None: ea = parse_xml( f'' ) rPr.append(ea) else: ea.set("typeface", FONT) def blank(prs): return prs.slides.add_slide(prs.slide_layouts[6]) def bg(slide, color=LIGHT): f = slide.background.fill f.solid() f.fore_color.rgb = color def shape(slide, l, t, w, h, fill, line=None): s = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(l), Inches(t), Inches(w), Inches(h)) s.fill.solid() s.fill.fore_color.rgb = fill if line is None: s.line.fill.background() else: s.line.color.rgb = line s.line.width = Pt(1.25) try: s.adjustments[0] = 0.08 except Exception: pass return s def shape_text(s, lines, size=SIZE_BODY, bold=False, color=WHITE, align=PP_ALIGN.CENTER): tf = s.text_frame tf.clear() tf.word_wrap = True if isinstance(lines, str): lines = lines.split("\n") for i, line in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align p.space_before = Pt(0) p.space_after = Pt(2) run = p.add_run() run.text = line set_font(run, size=size if i == 0 else max(size - 1, 10), bold=(bold and i == 0), color=color) def textbox(slide, l, t, w, h, text, size=SIZE_BODY, bold=False, color=DARK, align=PP_ALIGN.LEFT): box = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h)) tf = box.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.alignment = align p.space_before = Pt(0) p.space_after = Pt(0) # multi-line support first = True for line in str(text).split("\n"): p = tf.paragraphs[0] if first else tf.add_paragraph() first = False p.alignment = align p.space_before = Pt(0) p.space_after = Pt(2) run = p.add_run() run.text = line set_font(run, size=size, bold=bold, color=color) return box def paras(slide, l, t, w, h, lines, size=SIZE_BODY, color=DARK, spacing=4): box = slide.shapes.add_textbox(Inches(l), Inches(t), Inches(w), Inches(h)) tf = box.text_frame tf.word_wrap = True for i, line in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.LEFT p.space_before = Pt(0) p.space_after = Pt(spacing) run = p.add_run() run.text = line set_font(run, size=size, color=color) return box def table(slide, l, t, w, h, data, widths=None, fsize=SIZE_TABLE, header_color=NAVY): rows, cols = len(data), len(data[0]) ts = slide.shapes.add_table(rows, cols, Inches(l), Inches(t), Inches(w), Inches(h)) tb = ts.table if widths: for i, wi in enumerate(widths): tb.columns[i].width = Inches(wi) for r in range(rows): for c in range(cols): cell = tb.cell(r, c) cell.text = "" p = cell.text_frame.paragraphs[0] p.alignment = PP_ALIGN.LEFT p.space_before = Pt(0) p.space_after = Pt(0) run = p.add_run() run.text = str(data[r][c]) is_h = r == 0 set_font(run, size=fsize + (1 if is_h else 0), bold=is_h, color=WHITE if is_h else DARK) cell.fill.solid() # colorful header / zebra if is_h: cell.fill.fore_color.rgb = header_color else: cell.fill.fore_color.rgb = SOFT if r % 2 == 0 else WHITE return ts def header(slide, title, subtitle, section_idx=0): color = SECTION_COLORS[section_idx % len(SECTION_COLORS)] shape(slide, 0, 0, 13.333, HEADER_H, NAVY) # colorful accent strip shape(slide, 0, HEADER_H, 13.333, 0.06, color) # left color badge badge = shape(slide, 0.25, 0.18, 0.12, 0.52, color) box = slide.shapes.add_textbox(Inches(0.55), Inches(0.1), Inches(12.3), Inches(0.7)) tf = box.text_frame tf.word_wrap = True p = tf.paragraphs[0] run = p.add_run() run.text = title set_font(run, size=SIZE_H1, bold=True, color=WHITE) if subtitle: p2 = tf.add_paragraph() run2 = p2.add_run() run2.text = subtitle set_font(run2, size=SIZE_SMALL, color=RGBColor(0xB8, 0xD4, 0xF0)) def footer(slide, page): shape(slide, 0, FOOTER_Y, 13.333, 7.5 - FOOTER_Y, NAVY) # rainbow dots colors = [CORAL, ORANGE, YELLOW, TEAL, BLUE, PURPLE] for i, c in enumerate(colors): shape(slide, 0.25 + i * 0.22, FOOTER_Y + 0.1, 0.14, 0.14, c) textbox(slide, 1.7, FOOTER_Y + 0.05, 9.5, 0.28, "智慧医院 AI 影像诊断与电子病历辅助决策系统 · 项目答辩 · 仅供教学演示", size=10, color=RGBColor(0xC5, 0xD8, 0xE8)) textbox(slide, 11.4, FOOTER_Y + 0.05, 1.6, 0.28, f"{page} / {TOTAL}", size=10, bold=True, color=WHITE, align=PP_ALIGN.RIGHT) def fit_img(slide, path, l, t, max_w, max_h): path = Path(path) if not path.exists(): return None with PILImage.open(path) as im: iw, ih = im.size aspect = iw / float(ih) w, h = max_w, max_w / aspect if h > max_h: h = max_h w = h * aspect return slide.shapes.add_picture(str(path), Inches(l), Inches(t), width=Inches(w), height=Inches(h)) def card(slide, l, t, w, h, title, lines, color=BLUE, body_size=SIZE_BODY): shape(slide, l, t, w, h, WHITE, SOFT) band = shape(slide, l, t, w, 0.46, color) shape_text(band, [title], size=14, bold=True, color=WHITE) if lines: paras(slide, l + 0.15, t + 0.55, w - 0.3, h - 0.7, lines, size=body_size, color=DARK, spacing=4) # ===================== SLIDES ===================== def s01_cover(prs): slide = blank(prs) fit_img(slide, ASSETS / "cover.png", 0, 0, 13.333, 7.5) shape(slide, 1.0, 1.4, 11.3, 4.5, PANEL) textbox(slide, 1.3, 1.6, 10.7, 0.35, "PROJECT DEFENSE · SMART HOSPITAL", size=13, color=CYAN, align=PP_ALIGN.CENTER) textbox(slide, 1.3, 2.15, 10.7, 1.15, "智慧医院 AI 影像诊断与\n电子病历辅助决策系统", size=32, bold=True, color=WHITE, align=PP_ALIGN.CENTER) textbox(slide, 1.3, 3.45, 10.7, 0.4, "实训教学演示级 Web 应用 · 业务系统 + AI 微服务", size=15, color=RGBColor(0xB8, 0xD4, 0xF0), align=PP_ALIGN.CENTER) chips = [ ("主题", "智慧医院", CORAL), ("架构", "三端分离", ORANGE), ("AI", "YOLO+RAG", TEAL), ("前端", "Vue3", BLUE), ("后端", "Boot3", PURPLE), ("团队", "6 人协作", PINK), ] for i, (k, v, c) in enumerate(chips): x = 1.35 + i * 1.8 box = shape(slide, x, 4.15, 1.65, 0.95, c) shape_text(box, [k, v], size=12, bold=True, color=WHITE) textbox(slide, 1.2, 5.5, 10.9, 0.35, "文档 v1.0 · 2026-07-27 · 严格依据《项目详细文档》", size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER) textbox(slide, 1.2, 6.3, 10.9, 0.55, "声明:输出仅供教学实训与辅助决策演示,不能替代执业医师正式诊断与医疗文书。", size=11, color=RGBColor(0xE2, 0xEC, 0xF4), align=PP_ALIGN.CENTER) def s02_toc(prs): slide = blank(prs) bg(slide) header(slide, "目录 · 答辩叙事五篇章", "主题 → 需求 → 技术栈 → 功能 → 分工", 0) sections = [ ("01", "项目主题", "P3 – P4", "定位 · 双链路 · 目标 · 场景", BLUE, "主题"), ("02", "项目需求", "P5 – P7", "痛点 · 角色 · 优先级 · 非功能", CORAL, "需求"), ("03", "技术栈", "P8 – P10", "三层技术 · 架构 · 工程结构", PURPLE, "技术"), ("04", "实现功能", "P11 – P14", "全景 · 影像 · 决策 · 管理安全", TEAL, "功能"), ("05", "六人分工", "P15 – P16", "协作总览 · 职责 · 交付物", ORANGE, "分工"), ("06", "演示总结", "P17 – P18", "演示路径 · 验证 · 致谢", PINK, "收束"), ] for i, (num, title, pages, desc, color, tag) in enumerate(sections): col = i % 3 row = i // 3 x = 0.45 + col * 4.25 y = 1.2 + row * 2.7 shape(slide, x, y, 4.05, 2.45, WHITE, color) band = shape(slide, x, y, 4.05, 0.7, color) shape_text(band, [f"{num} {title}"], size=18, bold=True, color=WHITE) textbox(slide, x + 0.2, y + 0.9, 3.6, 0.35, pages, size=14, bold=True, color=color) textbox(slide, x + 0.2, y + 1.35, 3.6, 0.8, desc, size=13, color=DARK) footer(slide, 2) def s03_theme(prs): slide = blank(prs) bg(slide) header(slide, "一、项目主题 · 定位与双链路", "智慧医院 = 影像 AI 读片 + 病历 AI 决策", 0) fit_img(slide, ASSETS / "dual_chain.png", 0.3, 1.1, 12.7, 5.8) footer(slide, 3) def s04_goals_scenes(prs): slide = blank(prs) bg(slide) header(slide, "一、项目主题 · 建设目标与应用场景", "五维目标 + 四类演示场景", 0) fit_img(slide, ASSETS / "goals.png", 0.3, 1.1, 12.7, 2.5) fit_img(slide, ASSETS / "scenes.png", 0.3, 3.7, 12.7, 3.15) footer(slide, 4) def s05_req_pain(prs): slide = blank(prs) bg(slide) header(slide, "二、项目需求 · 背景痛点与改造", "解决传统教学 HIS 的五类不足", 1) fit_img(slide, ASSETS / "pain.png", 0.25, 1.1, 8.0, 4.0) data = [ ["问题", "传统表现", "本项目对策"], ["业务割裂", "缺统一视图", "患者 360° 档案"], ["AI 假数据", "随机/关键字", "YOLO + LLM/RAG"], ["技术陈旧", "Thymeleaf 耦合", "Vue3 前后端分离"], ["权限薄弱", "仅 Session", "JWT + 角色双端"], ["不可降级", "依赖失败中断", "规则/模板兜底"], ] table(slide, 8.4, 1.1, 4.55, 4.0, data, [1.2, 1.5, 1.85], fsize=10, header_color=CORAL) nfr_note = shape(slide, 0.3, 5.3, 12.7, 1.55, WHITE, TEAL) shape_text( nfr_note, [ "改造结论(文档 2.1)", "在原始 Spring Boot + Thymeleaf 原型上完成改造 → 当前「前后端分离 + AI 微服务」版本", "约束:实训/答辩级,非生产 HIS/PACS;影像以 JPG/PNG 为主;YOLO 为演示级需医师复核", ], size=12, color=DARK, align=PP_ALIGN.LEFT, ) footer(slide, 5) def s06_req_roles(prs): slide = blank(prs) bg(slide) header(slide, "二、项目需求 · 角色诉求与功能优先级", "三角色 + P0/P1/P2 需求看板", 1) roles = [ ["角色", "枚举", "核心诉求"], ["系统管理员", "ADMIN", "用户 · AI 配置 · YOLO 权重 · 运维"], ["临床医生", "DOCTOR", "患者病历 · 决策 · 预约 · 看报告"], ["影像医师", "RADIOLOGIST", "影像登记上传 · AI 诊断 · 审阅"], ["访客", "—", "仅登录页"], ] table(slide, 0.3, 1.1, 12.7, 2.0, roles, [2.2, 2.3, 8.2], fsize=12, header_color=BLUE) fit_img(slide, ASSETS / "priority.png", 0.3, 3.25, 12.7, 3.65) footer(slide, 6) def s07_req_nfr(prs): slide = blank(prs) bg(slide) header(slide, "二、项目需求 · 非功能需求", "性能 · 可用性 · 安全 · 可维护 · 可扩展 · 兼容", 1) fit_img(slide, ASSETS / "nfr.png", 0.3, 1.15, 12.7, 2.4) data = [ ["类别", "要求", "项目落地"], ["性能", "诊断不阻塞 HTTP", "@Async 线程池 + 前端有限轮询"], ["可用性", "无 MySQL 可演示", "默认 H2 + DataInitializer 种子"], ["安全", "鉴权/加密/CORS", "Security + JWT + BCrypt"], ["可维护", "统一响应异常", "Result + GlobalExceptionHandler"], ["可扩展", "AI 与业务解耦", "FastAPI 独立 · ai.service.base-url"], ["兼容", "大模型可切换", "OpenAI 兼容(DeepSeek/Qwen)"], ] table(slide, 0.3, 3.7, 12.7, 3.15, data, [1.6, 3.5, 7.6], fsize=11, header_color=PURPLE) footer(slide, 7) def s08_tech_stack(prs): slide = blank(prs) bg(slide) header(slide, "三、技术栈 · 总体一览", "版本号严格对齐文档附录 A", 2) fit_img(slide, ASSETS / "tech.png", 0.3, 1.1, 12.7, 3.5) data = [ ["层级", "关键组件", "版本"], ["前端", "Vue / Vite / Element Plus / Pinia / ECharts", "3.5.10 / 5.4.8 / 2.8.4 / 2.2.4 / 5.5.1"], ["业务端", "Spring Boot / Java / jjwt", "3.3.4 / 17 / 0.12.6"], ["数据库", "H2 默认 · MySQL 可选", "mem:smart_hospital / profile=mysql"], ["AI", "FastAPI / YOLO / LangChain / LLM", "≥0.110 / Ultralytics / ≥0.2 / DeepSeek"], ] table(slide, 0.3, 4.75, 12.7, 2.1, data, [1.5, 6.0, 5.2], fsize=11, header_color=PURPLE) footer(slide, 8) def s09_tech_arch(prs): slide = blank(prs) bg(slide) header(slide, "三、技术栈 · 系统逻辑架构", "Vue :5173 → Spring :8080 → H2/MySQL & FastAPI :8001", 2) fit_img(slide, ASSETS / "architecture.png", 0.25, 1.05, 12.8, 5.9) footer(slide, 9) def s10_tech_modules(prs): slide = blank(prs) bg(slide) header(slide, "三、技术栈 · 工程结构与数据落盘", "frontend · smart-hospital · ai-service", 2) fit_img(slide, ASSETS / "modules.png", 0.25, 1.1, 8.5, 5.7) card(slide, 8.95, 1.1, 4.0, 5.7, "关键配置与落盘", [ "• 前端代理 /api → :8080", "• jwt.* 密钥/过期/前缀", "• cors → localhost:5173", "• ai.service.base-url:8001", "• ./uploads/images 影像", "• ./data/ai/settings.json", "• chat-history.json 对话", "• ai-service/data/weights/", "• app/knowledge/ MD 知识", "• multipart 最大约 500MB", "• H2 控制台 /h2-console", "• 启动序:AI→业务→前端", ], VIOLET, body_size=12) footer(slide, 10) def s11_feat_panorama(prs): slide = blank(prs) bg(slide) header(slide, "四、实现功能 · 功能全景", "身份权限 · 主数据 · 诊疗业务 · 智能能力", 3) fit_img(slide, ASSETS / "features.png", 0.2, 1.05, 9.0, 5.85) routes = [ "/login 登录", "/dashboard 仪表盘", "/patients 患者", "/imaging 影像", "/emrs 电子病历", "/appointments 预约", "/ai-assistant 助手", "/ai-knowledge 知识库", "/ai-settings ADMIN", "/ai-yolo ADMIN", "/users ADMIN", ] card(slide, 9.4, 1.1, 3.55, 5.8, "前端路由一览", ["• " + r for r in routes], TEAL, body_size=11) footer(slide, 11) def s12_feat_imaging(prs): slide = blank(prs) bg(slide) header(slide, "四、实现功能 · 影像 AI 诊断(深度)", "状态机 · YOLO · 分段报告 · 可降级", 3) card(slide, 0.25, 1.1, 4.0, 2.2, "检查登记能力", [ "• X_RAY / CT / MRI / ULTRASOUND", "• 患者、部位、上传/路径", "• 关键字/状态/类型筛选", "• 状态 KPI 卡片快速过滤", ], BLUE, body_size=11) fit_img(slide, ASSETS / "state.png", 4.4, 1.1, 8.55, 2.2) fit_img(slide, ASSETS / "yolo.png", 0.25, 3.5, 5.4, 3.35) card(slide, 5.85, 3.5, 7.1, 3.35, "结果展示清单", [ "① 诊断印象 + 置信度仪表盘", "② 原图 vs YOLO 标注图对比", "③ 影像所见分段 + 建议列表", "④ 检测明细:类别/置信度/bbox", "⑤ 完整报告:头/所见/印象/建议/声明", "⑥ AI 不可用 → Spring 本地规则降级", ], CORAL, body_size=12) footer(slide, 12) def s13_feat_emr_biz(prs): slide = blank(prs) bg(slide) header(slide, "四、实现功能 · 病历决策与业务模块", "辅助决策七块输出 + 仪表盘/患者/预约", 3) fit_img(slide, ASSETS / "decision.png", 0.25, 1.1, 12.8, 2.7) cards = [ ("仪表盘", BLUE, ["KPI 多维统计", "近7日趋势 ECharts", "类型/状态分布饼图", "快捷入口与待办"]), ("患者 360°", TEAL, ["CRUD + 搜索分页", "证件/联系/既往史", "关联影像/病历/预约", "抽屉档案计数"]), ("预约挂号", ORANGE, ["新建编辑删除", "预约→确认→完成", "取消/未到诊", "按日筛选统计"]), ("关键词增强", PURPLE, ["高血压/糖尿病", "肺炎/结节", "模板+RAG 更明显", "LLM 可进一步增强"]), ] for i, (t, c, lines) in enumerate(cards): card(slide, 0.25 + i * 3.25, 4.0, 3.1, 2.85, t, ["• " + x for x in lines], c, body_size=11) footer(slide, 13) def s14_feat_admin(prs): slide = blank(prs) bg(slide) header(slide, "四、实现功能 · 管理端、安全与接口", "AI 配置 · 权限双端 · 统一响应 · 实体关系", 3) mods = [ ("AI 助手", ["Markdown 渲染", "DOMPurify 消毒", "历史文件持久化"], CYAN), ("知识库", ["文档 CRUD 启停", "RAG 管线配合", "内置医学片段"], PURPLE), ("AI 配置", ["仅 ADMIN", "URL/Key/Model", "同步 FastAPI"], ORANGE), ("YOLO 管理", ["权重上传激活", "real/demo 模式", "推理统计"], CORAL), ] for i, (t, lines, c) in enumerate(mods): card(slide, 0.25 + i * 3.25, 1.1, 3.1, 2.15, t, ["• " + x for x in lines], c, body_size=11) fit_img(slide, ASSETS / "er.png", 0.25, 3.4, 6.5, 3.45) data = [ ["接口/能力", "权限"], ["POST /api/auth/login", "公开"], ["患者/影像/病历/预约", "已登录"], ["AI 诊断 analyze/result", "已登录"], ["病历 ai-suggestions", "已登录"], ["用户管理 /users", "ADMIN"], ["AI 配置 / YOLO", "ADMIN"], ] table(slide, 6.95, 3.4, 6.0, 3.45, data, [4.0, 2.0], fsize=11, header_color=INDIGO) footer(slide, 14) def s15_team_overview(prs): slide = blank(prs) bg(slide) header(slide, "五、六人分工 · 协作总览", "按三端工程与业务模块划分(姓名可替换)", 4) fit_img(slide, ASSETS / "team.png", 0.25, 1.05, 12.8, 5.9) footer(slide, 15) def s16_team_detail(prs): """Balanced layout: compact table on top + full-width swimlane below (no tiny corner image).""" slide = blank(prs) bg(slide) header(slide, "五、六人分工 · 职责与交付物明细", "文档无具名分工 → 按模块可落地的 6 人方案", 4) data = [ ["成员", "角色定位", "主要职责", "关键交付物"], ["成员A", "前端基础/权限", "布局、登录、路由守卫、Pinia、个人中心", "BasicLayout · Login · router"], ["成员B", "前端业务", "仪表盘、患者360°、预约挂号、图表", "Dashboard · Patients · Appointments"], ["成员C", "前端智能交互", "影像报告弹窗、病历决策Dialog、AI助手/知识库页", "Imaging · EMR · AI views"], ["成员D", "后端主数据/安全", "JWT安全、用户、患者、预约、统一响应", "Security · User/Patient API"], ["成员E", "后端AI桥接", "异步诊断、AiClient、决策对接、降级逻辑", "AIDiagnosis · Decision 服务"], ["成员F", "AI微服务", "YOLO检测、报告/决策、RAG、权重与LLM配置", "ai-service 全模块"], ] # 上半:表格约占 2.9",行高更匀称 table(slide, 0.3, 1.08, 12.7, 2.95, data, [1.3, 2.2, 5.0, 4.2], fsize=12, header_color=ORANGE) # 下半:全宽泳道图(宽扁比例,贴满宽度,与上表约 1:1 视觉权重) fit_img(slide, ASSETS / "team_lane.png", 0.3, 4.15, 12.7, 2.8) footer(slide, 16) def s17_demo(prs): slide = blank(prs) bg(slide) header(slide, "演示路径 · 账号 · 验证清单", "答辩现场可按 7 步剧本走通", 5) accounts = [ ["用户名", "密码", "角色", "用途"], ["admin", "admin123", "ADMIN", "用户 / AI 配置 / YOLO"], ["doctor1", "pass123", "DOCTOR", "临床主演示"], ["radio1", "radio123", "RADIOLOGIST", "影像演示"], ] table(slide, 0.3, 1.1, 12.7, 1.7, accounts, [2.2, 2.5, 2.8, 5.2], fsize=12, header_color=TEAL) fit_img(slide, ASSETS / "demo.png", 0.3, 3.0, 12.7, 1.85) checks = [ ["验证项", "期望"], ["三端启动 5173 可登录", "通过"], ["admin 可见用户管理;doctor 访问 /users → 403", "通过"], ["影像 AI 至 COMPLETED,报告分段清晰", "通过"], ["病历 AI 建议含多类内容 + RAG 来源", "通过"], ["预约状态可流转;改密后需重登", "通过"], ["关闭 AI 后诊断仍返回降级结果", "通过"], ] table(slide, 0.3, 5.0, 12.7, 1.9, checks, [8.5, 4.2], fsize=11, header_color=BLUE) footer(slide, 17) def s18_end(prs): slide = blank(prs) fit_img(slide, ASSETS / "closing.png", 0, 0, 13.333, 7.5) textbox(slide, 1, 1.0, 11.3, 0.55, "总结与致谢", size=30, bold=True, color=WHITE, align=PP_ALIGN.CENTER) textbox(slide, 1.2, 1.65, 10.9, 0.45, "主题清晰 · 需求可验 · 技术可讲 · 功能可演示 · 分工可落地", size=14, color=CYAN, align=PP_ALIGN.CENTER) points = [ ("主题", "双链路智慧医院", CORAL), ("需求", "P0–P2 + 非功能", ORANGE), ("技术", "Vue3+Boot+FastAPI", PURPLE), ("功能", "YOLO·RAG·全业务", TEAL), ("协作", "6 人三端分工", BLUE), ("边界", "教学演示非临床", PINK), ] for i, (t, d, c) in enumerate(points): x = 0.7 + (i % 6) * 2.1 box = shape(slide, x, 2.4, 1.95, 1.5, c) shape_text(box, [t, d], size=12, bold=True, color=WHITE) textbox(slide, 1, 4.2, 11.3, 0.5, "谢谢聆听 · 欢迎提问", size=26, bold=True, color=WHITE, align=PP_ALIGN.CENTER) textbox(slide, 1.2, 5.0, 10.9, 0.9, "声明:仅供教学实训与辅助决策演示,不能替代执业医师正式诊断。\n" "依据:《项目详细文档.md》v1.0 · 分工表中成员名可按实际名单替换", size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER) # note about team textbox(slide, 1.2, 6.2, 10.9, 0.5, "六人姓名未在文档中给出,PPT 使用「成员A–F」占位,答辩前请替换为真实姓名。", size=11, color=YELLOW, align=PP_ALIGN.CENTER) def main(): if not ASSETS.exists(): raise SystemExit("请先运行 gen_assets_v2.py 生成 assets_v2/") prs = Presentation() prs.slide_width = SLIDE_W prs.slide_height = SLIDE_H s01_cover(prs) s02_toc(prs) s03_theme(prs) s04_goals_scenes(prs) s05_req_pain(prs) s06_req_roles(prs) s07_req_nfr(prs) s08_tech_stack(prs) s09_tech_arch(prs) s10_tech_modules(prs) s11_feat_panorama(prs) s12_feat_imaging(prs) s13_feat_emr_biz(prs) s14_feat_admin(prs) s15_team_overview(prs) s16_team_detail(prs) s17_demo(prs) s18_end(prs) try: prs.save(OUT) print("Saved:", OUT) except PermissionError: prs.save(OUT_ALT) print("原文件占用,另存:", OUT_ALT) print("Slides:", len(prs.slides)) if __name__ == "__main__": main()