# -*- coding: utf-8 -*-
"""Build Smart Hospital defense PPT (19 slides) — layout-safe revision."""
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"
OUT = ROOT / "智慧医院_AI影像诊断与电子病历辅助决策系统_答辩PPT.pptx"
OUT_ALT = ROOT / "智慧医院_答辩PPT_优化版.pptx"
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
# Safe content band (below header, above footer)
HEADER_H = 0.92
CONTENT_TOP = 1.05
FOOTER_TOP = 7.12
CONTENT_BOTTOM = 7.05
CONTENT_H = CONTENT_BOTTOM - CONTENT_TOP # ~6.0"
NAVY = RGBColor(0x0B, 0x3A, 0x5C)
TEAL = RGBColor(0x1A, 0x7A, 0x9C)
ACCENT = RGBColor(0x2B, 0xBB, 0xAD)
LIGHT = RGBColor(0xF4, 0xF8, 0xFB)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
DARK = RGBColor(0x1E, 0x29, 0x3B)
MUTED = RGBColor(0x64, 0x74, 0x8B)
SOFT = RGBColor(0xE2, 0xEC, 0xF4)
ORANGE = RGBColor(0xF5, 0x9E, 0x0B)
GREEN = RGBColor(0x22, 0xC5, 0x5E)
RED = RGBColor(0xEF, 0x44, 0x44)
CHIP = RGBColor(0x12, 0x4A, 0x6A)
PANEL = RGBColor(0x0B, 0x2A, 0x45)
FONT = "微软雅黑"
TOTAL = 19
def set_run_font(run, size=14, bold=False, color=DARK, name=FONT):
run.font.size = Pt(size)
run.font.bold = bold
run.font.color.rgb = color
run.font.name = name
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", name)
def add_textbox(
slide,
left,
top,
width,
height,
text,
size=14,
bold=False,
color=DARK,
align=PP_ALIGN.LEFT,
anchor=MSO_ANCHOR.TOP,
):
box = slide.shapes.add_textbox(left, top, width, height)
tf = box.text_frame
tf.word_wrap = True
tf.auto_size = None
try:
tf.paragraphs[0].space_before = Pt(0)
tf.paragraphs[0].space_after = Pt(0)
except Exception:
pass
p = tf.paragraphs[0]
p.alignment = align
run = p.add_run()
run.text = text
set_run_font(run, size=size, bold=bold, color=color)
return box
def add_paras(slide, left, top, width, height, lines, size=12, color=DARK, spacing=4, bold=False):
box = slide.shapes.add_textbox(left, top, width, height)
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_run_font(run, size=size, bold=bold, color=color)
return box
def add_shape(slide, left, top, width, height, fill=NAVY, 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(1)
try:
shape.adjustments[0] = 0.08
except Exception:
pass
return shape
def fill_shape_text(shape, lines, size=13, bold=False, color=WHITE, align=PP_ALIGN.CENTER, v_anchor=MSO_ANCHOR.MIDDLE):
"""Put multi-line text inside a shape (avoids separate overlapping textboxes)."""
tf = shape.text_frame
tf.clear()
tf.word_wrap = True
try:
tf.auto_size = None
except Exception:
pass
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_run_font(run, size=size, bold=bold if i == 0 else bold, color=color)
return shape
def add_table(slide, left, top, width, height, data, col_widths=None, font_size=11):
rows, cols = len(data), len(data[0])
table_shape = slide.shapes.add_table(rows, cols, left, top, width, height)
table = table_shape.table
if col_widths:
for i, w in enumerate(col_widths):
table.columns[i].width = w
for r in range(rows):
for c in range(cols):
cell = table.cell(r, c)
cell.text = ""
tf = cell.text_frame
tf.word_wrap = True
p = tf.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_header = r == 0
set_run_font(run, size=font_size if not is_header else font_size + 1, bold=is_header, color=WHITE if is_header else DARK)
cell.fill.solid()
cell.fill.fore_color.rgb = NAVY if is_header else (SOFT if r % 2 == 0 else WHITE)
return table_shape
def set_slide_bg(slide, color=LIGHT):
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def header_bar(slide, title, subtitle=None):
"""Single text frame for title+subtitle to avoid stacked textbox overlap."""
add_shape(slide, Inches(0), Inches(0), SLIDE_W, Inches(HEADER_H), NAVY)
add_shape(slide, Inches(0), Inches(HEADER_H), SLIDE_W, Inches(0.05), ACCENT)
box = slide.shapes.add_textbox(Inches(0.4), Inches(0.12), Inches(12.5), Inches(0.72))
tf = box.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
p.space_before = Pt(0)
p.space_after = Pt(2)
run = p.add_run()
run.text = title
set_run_font(run, size=22, bold=True, color=WHITE)
if subtitle:
p2 = tf.add_paragraph()
p2.alignment = PP_ALIGN.LEFT
p2.space_before = Pt(0)
p2.space_after = Pt(0)
run2 = p2.add_run()
run2.text = subtitle
set_run_font(run2, size=11, bold=False, color=RGBColor(0xB8, 0xD4, 0xE8))
def footer(slide, page):
add_shape(slide, Inches(0), Inches(FOOTER_TOP), SLIDE_W, Inches(7.5 - FOOTER_TOP), NAVY)
box = slide.shapes.add_textbox(Inches(0.3), Inches(FOOTER_TOP + 0.04), Inches(10.5), Inches(0.28))
tf = box.text_frame
p = tf.paragraphs[0]
run = p.add_run()
run.text = "智慧医院 AI 影像诊断与电子病历辅助决策系统 · 实训答辩 · 仅供教学演示"
set_run_font(run, size=10, color=RGBColor(0xC5, 0xD8, 0xE8))
box2 = slide.shapes.add_textbox(Inches(11.3), Inches(FOOTER_TOP + 0.04), Inches(1.7), Inches(0.28))
p2 = box2.text_frame.paragraphs[0]
p2.alignment = PP_ALIGN.RIGHT
run2 = p2.add_run()
run2.text = f"{page} / {TOTAL}"
set_run_font(run2, size=10, bold=True, color=WHITE)
def fit_image(slide, path, left, top, max_width, max_height):
"""Place image scaled to fit inside max box; never exceed max_height (prevents footer crash)."""
path = Path(path)
if not path.exists():
return None
with PILImage.open(path) as im:
iw, ih = im.size
aspect = iw / ih
# convert max to inches floats
mw = max_width if isinstance(max_width, float) else max_width / 914400
mh = max_height if isinstance(max_height, float) else max_height / 914400
# left/top may be Inches
def to_in(v):
return v if isinstance(v, float) else v / 914400
l = to_in(left)
t = to_in(top)
w = mw
h = w / aspect
if h > mh:
h = mh
w = h * aspect
return slide.shapes.add_picture(str(path), Inches(l), Inches(t), width=Inches(w), height=Inches(h))
def blank_slide(prs):
return prs.slides.add_slide(prs.slide_layouts[6])
def card(slide, left, top, width, height, title, body_lines, color=TEAL, title_size=14, body_size=12):
"""Card with colored title band + body text, all non-overlapping zones."""
add_shape(slide, Inches(left), Inches(top), Inches(width), Inches(height), WHITE, SOFT)
title_h = 0.48
band = add_shape(slide, Inches(left), Inches(top), Inches(width), Inches(title_h), color)
fill_shape_text(band, [title], size=title_size, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
# body region strictly below band
body_top = top + title_h + 0.12
body_h = height - title_h - 0.2
if body_lines:
add_paras(
slide,
Inches(left + 0.15),
Inches(body_top),
Inches(width - 0.3),
Inches(body_h),
body_lines,
size=body_size,
color=DARK,
spacing=5,
)
# ---------------- slides ----------------
def slide_01_cover(prs):
slide = blank_slide(prs)
fit_image(slide, ASSETS / "cover_bg.png", 0, 0, 13.333, 7.5)
# Center panel — leave room for disclaimer
add_shape(slide, Inches(1.1), Inches(1.35), Inches(11.1), Inches(4.35), PANEL)
add_textbox(slide, Inches(1.4), Inches(1.55), Inches(10.5), Inches(0.35),
"SMART HOSPITAL · DEFENSE PRESENTATION", size=13, color=ACCENT, align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1.4), Inches(2.05), Inches(10.5), Inches(1.1),
"智慧医院 AI 影像诊断与\n电子病历辅助决策系统", size=32, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1.4), Inches(3.25), Inches(10.5), Inches(0.4),
"实训教学演示级 Web 应用 · 业务系统 + AI 微服务混合架构",
size=15, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.CENTER)
chips = [("文档版本", "v1.0"), ("日期", "2026-07-27"), ("前端", "Vue3 :5173"),
("业务端", "Boot :8080"), ("AI", "FastAPI :8001")]
for i, (k, v) in enumerate(chips):
x = 1.5 + i * 2.05
chip = add_shape(slide, Inches(x), Inches(4.0), Inches(1.9), Inches(0.95), CHIP)
fill_shape_text(chip, [k, v], size=12, bold=False, color=WHITE, align=PP_ALIGN.CENTER)
# make first line accent-ish by rewriting as single centered block is OK
add_textbox(slide, Inches(1.2), Inches(6.2), Inches(10.9), Inches(0.7),
"声明:本系统输出仅供教学实训与辅助决策演示,不能替代执业医师的正式诊断与医疗文书。",
size=12, color=RGBColor(0xE2, 0xEC, 0xF4), align=PP_ALIGN.CENTER)
def slide_02_toc(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "目录导览", "课程/实训答辩叙事结构")
sections = [
("01", "主题与需求", "P3 – P6", "主题定位 · 场景角色 · 痛点改造 · 需求优先级", TEAL),
("02", "功能与业务", "P7 – P11", "功能全景 · 核心模块 · 影像诊断 · 病历决策 · 管理端", ACCENT),
("03", "架构与技术", "P12 – P16", "技术栈 · 逻辑架构 · 工程结构 · 核心流程 · 数据与安全", NAVY),
("04", "部署与总结", "P17 – P19", "运行部署 · 演示路径 · 设计取舍 · 边界与展望", ORANGE),
]
# 4 rows fit in 1.15 ~ 6.9
row_h = 1.25
gap = 0.12
start_y = 1.15
for i, (num, title, pages, desc, color) in enumerate(sections):
y = start_y + i * (row_h + gap)
add_shape(slide, Inches(0.55), Inches(y), Inches(12.2), Inches(row_h), WHITE, SOFT)
num_box = add_shape(slide, Inches(0.55), Inches(y), Inches(1.25), Inches(row_h), color)
fill_shape_text(num_box, [num], size=26, bold=True, color=WHITE)
add_textbox(slide, Inches(2.05), Inches(y + 0.22), Inches(7.2), Inches(0.4),
title, size=20, bold=True, color=NAVY)
add_textbox(slide, Inches(2.05), Inches(y + 0.7), Inches(7.5), Inches(0.35),
desc, size=12, color=MUTED)
page_box = add_shape(slide, Inches(10.4), Inches(y + 0.35), Inches(1.9), Inches(0.55), color)
fill_shape_text(page_box, [pages], size=13, bold=True, color=WHITE)
footer(slide, 2)
def slide_03_theme(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "项目主题与建设目标", "围绕医院诊疗两条核心链路展开")
# Left image max height keeps clear of footer
fit_image(slide, ASSETS / "dual_chain.png", 0.3, 1.12, 8.0, 5.7)
# Right goals — fixed band, no overflow
gx, gy, gw, gh = 8.5, 1.12, 4.4, 5.7
add_shape(slide, Inches(gx), Inches(gy), Inches(gw), Inches(gh), WHITE, SOFT)
band = add_shape(slide, Inches(gx), Inches(gy), Inches(gw), Inches(0.45), NAVY)
fill_shape_text(band, ["建设目标五维"], size=14, bold=True, color=WHITE)
goals = [
("业务闭环", "登录、患者、影像、病历、预约、用户管理"),
("AI 可演示", "YOLO 实检 + DeepSeek 兼容 / 模板降级"),
("前后端分离", "Vue3 SPA + Spring Boot + FastAPI"),
("可离线实训", "默认 H2,AI 宕机业务仍可演示"),
("安全可讲", "JWT · 角色权限 · BCrypt · 双端守卫"),
]
item_h = 0.95
for i, (t, d) in enumerate(goals):
y = gy + 0.55 + i * item_h
item = add_shape(slide, Inches(gx + 0.15), Inches(y), Inches(gw - 0.3), Inches(0.85), SOFT)
fill_shape_text(item, [t, d], size=12, bold=False, color=DARK, align=PP_ALIGN.LEFT)
footer(slide, 3)
def slide_04_scenes(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "应用场景与用户角色", "教学/演示四类场景 + 三角色权限模型")
# Image top region
fit_image(slide, ASSETS / "scene_cards.png", 0.4, 1.1, 12.5, 2.7)
data = [
["角色", "代码枚举", "核心诉求"],
["系统管理员", "ADMIN", "用户管理、AI 大模型配置、YOLO 权重、全局运维"],
["临床医生", "DOCTOR", "患者与病历、辅助决策、预约、查看影像报告"],
["影像医师", "RADIOLOGIST", "影像登记、上传、触发 AI 诊断、审阅标注与报告"],
["访客/未登录", "—", "仅可访问登录页"],
]
add_table(slide, Inches(0.4), Inches(4.0), Inches(12.5), Inches(2.85), data,
col_widths=[Inches(2.2), Inches(2.4), Inches(7.9)], font_size=12)
footer(slide, 4)
def slide_05_pain(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "需求分析:痛点与改造", "从教学型 CRUD 到可演示的 AI 混合架构")
data = [
["问题", "传统表现", "本项目对策"],
["业务割裂", "缺统一档案视图", "患者 360° 档案关联全业务"],
["AI 仅假数据", "随机文案/关键字", "YOLO 实检 + LLM/RAG,可降级"],
["技术栈陈旧", "Thymeleaf 耦合", "Vue3 SPA + REST 分离"],
["权限薄弱", "仅 Session 登录", "JWT + 角色 + 双端守卫"],
["不可降级", "依赖失败即中断", "Spring 规则/模板兜底"],
]
add_table(slide, Inches(0.35), Inches(1.15), Inches(7.0), Inches(3.55), data,
col_widths=[Inches(1.5), Inches(2.5), Inches(3.0)], font_size=11)
fit_image(slide, ASSETS / "transform.png", 7.55, 1.15, 5.4, 3.55)
nfr = add_shape(slide, Inches(0.35), Inches(4.9), Inches(12.6), Inches(1.95), WHITE, TEAL)
fill_shape_text(
nfr,
[
"非功能需求落地",
"性能:@Async 异步诊断 + 有限轮询 | 可用性:默认 H2 + 种子数据 | 安全:JWT / BCrypt / CORS",
"可维护:Result + 全局异常 | 可扩展:AI 独立进程 | 兼容:OpenAI 协议(DeepSeek / Qwen)",
],
size=12,
color=DARK,
align=PP_ALIGN.LEFT,
)
footer(slide, 5)
def slide_06_priority(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "功能需求优先级", "P0 必须 · P1 增强 · P2 体验(文档 2.3)")
fit_image(slide, ASSETS / "priority_board.png", 0.35, 1.1, 12.6, 5.8)
footer(slide, 6)
def slide_07_panorama(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "功能全景", "身份权限 · 主数据 · 诊疗业务 · 智能能力")
fit_image(slide, ASSETS / "feature_panorama.png", 0.3, 1.1, 8.7, 5.8)
routes = [
"/login 登录",
"/dashboard 仪表盘",
"/patients 患者",
"/imaging 影像诊断",
"/emrs 电子病历",
"/appointments 预约",
"/ai-assistant 助手",
"/ai-knowledge 知识库",
"/ai-settings ADMIN",
"/ai-yolo ADMIN",
"/users ADMIN",
]
card(slide, 9.2, 1.1, 3.7, 5.8, "前端路由一览", ["• " + r for r in routes], TEAL, body_size=12)
footer(slide, 7)
def slide_08_modules(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "核心业务模块", "仪表盘 · 患者 360° · 预约状态机")
cards = [
("仪表盘 Dashboard", TEAL, [
"• KPI:患者 / 影像 / 病历 / 预约及状态分布",
"• 近 7 日业务趋势(ECharts)",
"• 检查类型、诊断状态分布饼图",
"• 快捷入口与待办,便于演示导览",
]),
("患者管理 + 360° 档案", ACCENT, [
"• 分页列表、姓名等关键字搜索",
"• 证件、电话、地址、既往史",
"• 抽屉关联:影像 / 病历 / 预约",
"• 表单等宽两列,弹窗体验优化",
]),
("预约挂号", ORANGE, [
"• 新建 / 编辑 / 删除预约",
"• 状态:预约 → 确认 → 完成",
"• 亦可:取消 / 未到诊",
"• 按日筛选、统计卡片、详情抽屉",
]),
]
for i, (title, color, items) in enumerate(cards):
card(slide, 0.35 + i * 4.3, 1.15, 4.1, 5.7, title, items, color, title_size=15, body_size=13)
footer(slide, 8)
def slide_09_imaging(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "影像诊断模块(深度)", "检查登记 · 状态机 · YOLO 结果展示 · 可降级")
# Row 1: capability + state machine
card(slide, 0.3, 1.1, 4.0, 2.35, "检查登记能力", [
"• 类型:X_RAY / CT / MRI / ULTRASOUND",
"• 选择患者、部位、上传或路径",
"• 筛选:关键字 / 状态 / 类型",
"• 状态 KPI 卡片可快速过滤",
], TEAL, body_size=11)
fit_image(slide, ASSETS / "state_machine.png", 4.5, 1.1, 8.4, 2.35)
# Row 2: yolo + results — constrained heights
fit_image(slide, ASSETS / "yolo_concept.png", 0.3, 3.6, 5.3, 3.3)
card(slide, 5.8, 3.6, 7.1, 3.3, "诊断结果展示清单", [
"① 诊断印象 + 置信度仪表盘",
"② 原始影像 vs YOLO 标注图对比",
"③ 影像所见(分段)+ 建议编号列表",
"④ 检测明细:类别 / 置信度 / bbox",
"⑤ 完整报告:头 / 所见 / 印象 / 建议 / 声明",
"⑥ AI 不可用 → Spring 本地规则降级",
], ACCENT, body_size=12)
footer(slide, 9)
def slide_10_emr(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "电子病历与 AI 辅助决策(深度)", "结构化录入 → RAG/LLM 决策 → 分节 Dialog")
fit_image(slide, ASSETS / "decision_flow.png", 0.35, 1.1, 12.6, 2.5)
# Bottom two cards
fields = ["主诉", "现病史", "体格检查", "诊断", "治疗方案", "用药", "随访"]
add_shape(slide, Inches(0.35), Inches(3.8), Inches(6.2), Inches(3.05), WHITE, SOFT)
band = add_shape(slide, Inches(0.35), Inches(3.8), Inches(6.2), Inches(0.45), TEAL)
fill_shape_text(band, ["病历字段链路"], size=13, bold=True, color=WHITE)
for i, f in enumerate(fields):
x = 0.55 + (i % 4) * 1.45
y = 4.45 + (i // 4) * 0.95
chip = add_shape(slide, Inches(x), Inches(y), Inches(1.35), Inches(0.7), TEAL if i < 4 else ACCENT)
fill_shape_text(chip, [f], size=12, bold=True, color=WHITE)
card(slide, 6.8, 3.8, 6.15, 3.05, "决策输出七块 + 关键词增强", [
"• 输出:治疗 / 用药 / 护理 / 随访 / 风险 / 冲突 / RAG 来源",
"• 关键词:高血压 · 糖尿病 · 肺炎 · 结节",
"• 配置 LLM 后由大模型增强;失败回退模板",
"• 保存后可自动弹出;列表可再次查看",
], ACCENT, body_size=12)
footer(slide, 10)
def slide_11_admin(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "智能运营与管理端", "AI 助手 · 知识库 · LLM 配置 · YOLO 权重 · 用户权限")
mods = [
("AI 助手", ["多轮对话 · Markdown", "marked + DOMPurify", "history → chat-history.json"], TEAL),
("知识库", ["文档 CRUD · 启停", "配合 RAG 管线", "内置医学 MD 片段"], ACCENT),
("AI 配置", ["仅 ADMIN", "Base URL / Key / Model", "settings.json 持久化"], NAVY),
("YOLO 管理", ["仅 ADMIN", "权重列表/激活/上传", "real / demo 模式"], ORANGE),
]
for i, (t, lines, c) in enumerate(mods):
card(slide, 0.3 + i * 3.25, 1.1, 3.1, 2.4, t, ["• " + x for x in lines], c, body_size=11)
fit_image(slide, ASSETS / "llm_sync.png", 0.3, 3.7, 7.9, 3.15)
card(slide, 8.4, 3.7, 4.5, 3.15, "权限双端防护", [
"• 前端:路由 meta.roles 隐藏/拦截",
"• 后端:@PreAuthorize 接口鉴权",
"• 用户 CRUD:角色/科室/启停",
"• 禁止停用当前登录账号",
"• doctor 访问 /users → 403",
], NAVY, body_size=12)
footer(slide, 11)
def slide_12_tech(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "技术栈总览", "版本号与文档附录 A 对齐")
# Image top, table bottom — no overlap
fit_image(slide, ASSETS / "tech_layers.png", 0.4, 1.1, 12.5, 2.7)
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 / JPA", "3.3.4 / 17 / 0.12.6"],
["数据库", "H2 默认 · MySQL 可选 profile", "mem:smart_hospital / application-mysql.yml"],
["AI 服务", "FastAPI / YOLO / LangChain / LLM", "≥0.110 / Ultralytics / ≥0.2 / DeepSeek 兼容"],
["工程", "前端代理 /api→8080;AI base-url 8001", "uploads/ · data/ai/ · data/weights/"],
]
add_table(slide, Inches(0.4), Inches(4.0), Inches(12.5), Inches(2.9), data,
col_widths=[Inches(1.5), Inches(5.6), Inches(5.4)], font_size=11)
footer(slide, 12)
def slide_13_arch(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "系统逻辑架构", "浏览器 → 业务端 → 数据库 / AI 微服务")
fit_image(slide, ASSETS / "architecture.png", 0.35, 1.1, 12.6, 5.85)
footer(slide, 13)
def slide_14_modules_dir(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "工程目录与模块结构", "frontend · smart-hospital · ai-service")
fit_image(slide, ASSETS / "module_tree.png", 0.25, 1.1, 8.3, 5.8)
card(slide, 8.7, 1.1, 4.2, 5.8, "关键落盘与配置", [
"• ./uploads/images 影像文件",
"• ./uploads 头像等",
"• ./data/ai/settings.json",
"• ./data/ai/chat-history.json",
"• ai-service/data/weights/",
"• ai-service/app/knowledge/",
"• jwt.* 过期与前缀",
"• cors → localhost:5173",
"• ai.service.base-url:8001",
"• multipart 最大约 500MB",
"• H2 控制台 /h2-console",
], NAVY, body_size=12)
footer(slide, 14)
def slide_15_flow(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "核心流程:AI 影像诊断", "异步状态机 + FastAPI 检测报告 + 前端轮询")
fit_image(slide, ASSETS / "imaging_flow.png", 0.3, 1.1, 12.7, 5.85)
footer(slide, 15)
def slide_16_data_api(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "数据模型 · 接口 · 安全", "实体关系、统一响应、REST 与鉴权要点")
fit_image(slide, ASSETS / "er_diagram.png", 0.25, 1.1, 6.3, 4.0)
# Right column stacked, no collision
card(slide, 6.7, 1.1, 6.2, 1.35, "统一响应 Result", [
'{ "code": 0, "message": "OK", "data": {} }',
"code != 0 → Axios 拦截器 ElMessage 提示",
], TEAL, body_size=11)
data = [
["方法/路径", "说明", "权限"],
["POST /api/auth/login", "登录签发 JWT", "公开"],
["GET /api/stats/overview", "仪表盘统计", "登录"],
["POST .../analyze/{id}", "触发诊断", "登录"],
["GET .../ai-suggestions", "辅助决策", "登录"],
["* /api/users/**", "用户管理", "ADMIN"],
["AI /imaging/analyze 等", "检测/决策/RAG", "经业务端"],
]
add_table(slide, Inches(6.7), Inches(2.6), Inches(6.2), Inches(2.5), data,
col_widths=[Inches(2.7), Inches(2.0), Inches(1.5)], font_size=10)
card(slide, 0.25, 5.25, 12.65, 1.6, "安全要点", [
"JWT 无状态 · BCrypt 密码 · CORS 白名单 · 角色 ADMIN / DOCTOR / RADIOLOGIST",
"前端路由守卫 + 后端 @PreAuthorize · 修改密码后需重新登录",
], NAVY, body_size=12)
footer(slide, 16)
def slide_17_deploy(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "部署运行与演示账号", "启动顺序 · 健康检查 · 内置账号 · 演示剧本")
env = [
["组件", "要求"],
["JDK", "17+"],
["Node.js", "18+"],
["Python", "3.10+(推荐 3.11/3.12)"],
["可选", "MySQL 8.x;GPU 非必须"],
]
add_table(slide, Inches(0.3), Inches(1.1), Inches(3.9), Inches(2.15), env,
col_widths=[Inches(1.2), Inches(2.7)], font_size=11)
health = [
["服务", "地址"],
["前端", "http://localhost:5173"],
["业务 API", "http://localhost:8080/api/..."],
["H2 控制台", "http://localhost:8080/h2-console"],
["AI 健康", "http://127.0.0.1:8001/health"],
["AI 文档", "http://127.0.0.1:8001/docs"],
]
add_table(slide, Inches(4.4), Inches(1.1), Inches(4.5), Inches(2.15), health,
col_widths=[Inches(1.4), Inches(3.1)], font_size=10)
card(slide, 9.1, 1.1, 3.85, 2.15, "启动顺序", [
"1) ai-service :8001",
"2) smart-hospital :8080",
"3) frontend :5173",
"推荐:先 AI → 业务 → 前端",
], ACCENT, body_size=11)
accounts = [
["用户名", "密码", "角色", "说明"],
["admin", "admin123", "ADMIN", "用户 / AI 配置 / YOLO"],
["doctor1", "pass123", "DOCTOR", "临床主演示"],
["radio1", "radio123", "RADIOLOGIST", "影像演示"],
]
add_table(slide, Inches(0.3), Inches(3.45), Inches(12.7), Inches(1.55), accounts,
col_widths=[Inches(2.2), Inches(2.5), Inches(2.8), Inches(5.2)], font_size=12)
fit_image(slide, ASSETS / "demo_timeline.png", 0.3, 5.2, 12.7, 1.7)
footer(slide, 17)
def slide_18_design(prs):
slide = blank_slide(prs)
set_slide_bg(slide)
header_bar(slide, "设计取舍 · 边界 · 展望", "答辩可讲清「为什么这样设计」")
cols = [
("关键设计取舍", TEAL, [
"• 默认 H2 → 降低实训门槛",
"• AI 独立进程 → 隔离 Python 依赖",
"• JWT 无状态 → 适配前后端分离",
"• LLM 可关 → 无 Key 仍可演示",
"• 报告强制分段 → 避免墙文本",
"• Dialog append-to-body → 防裁切",
]),
("已知边界(非缺陷)", ORANGE, [
"• 非完整 HIS/PACS/RIS 产品",
"• 无医保 / 收费 / CA / 电子签",
"• YOLO 为演示级权重与类别",
"• H2 重启丢失业务表数据",
"• 外网 LLM 受网络/额度影响",
"• DICOM 完整工作流非重点",
]),
("后续可扩展", ACCENT, [
"• DICOM 解析与序列阅片",
"• 报告 PDF 与医师签收",
"• 更细科室权限与审计",
"• 向量库增强 RAG",
"• Docker Compose 一键拉起",
"• 接口自动化测试与 CI",
]),
]
for i, (title, color, items) in enumerate(cols):
card(slide, 0.3 + i * 4.3, 1.1, 4.15, 4.35, title, items, color, body_size=12)
cmp_data = [
["维度", "早期", "当前"],
["前端", "Thymeleaf + Bootstrap", "Vue3 + Element Plus + ECharts"],
["安全", "HttpSession", "Spring Security + JWT"],
["AI 影像", "规则/随机模拟", "YOLO 实检 + LLM/模板报告"],
]
add_table(slide, Inches(0.3), Inches(5.6), Inches(12.7), Inches(1.3), cmp_data,
col_widths=[Inches(2.0), Inches(5.2), Inches(5.5)], font_size=11)
footer(slide, 18)
def slide_19_end(prs):
slide = blank_slide(prs)
fit_image(slide, ASSETS / "closing_bg.png", 0, 0, 13.333, 7.5)
add_textbox(slide, Inches(1), Inches(1.1), Inches(11.3), Inches(0.55),
"总结与致谢", size=30, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1.2), Inches(1.7), Inches(10.9), Inches(0.45),
"智慧医院 = 业务闭环 + 真 AI 链路 + 可降级演示 + 可讲清的工程与安全",
size=14, color=ACCENT, align=PP_ALIGN.CENTER)
values = [
("业务闭环", "患者—影像—病历—预约统一"),
("真 AI 链路", "YOLO 检测 + RAG/LLM 决策"),
("可降级", "无 Key / AI 宕机仍可答辩"),
("工程可讲", "三端分离 · JWT · 异步状态机"),
]
for i, (t, d) in enumerate(values):
x = 1.15 + i * 2.85
box = add_shape(slide, Inches(x), Inches(2.4), Inches(2.65), Inches(1.45), CHIP)
fill_shape_text(box, [t, d], size=12, bold=False, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1.0), Inches(4.15), Inches(11.3), Inches(0.55),
"验证要点:三端可登录 · 角色 403 正确 · 影像 COMPLETED 报告分段 · 病历建议多类 · 预约可流转 · 改密需重登",
size=12, color=RGBColor(0xC5, 0xD8, 0xE8), align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1), Inches(4.9), Inches(11.3), Inches(0.55),
"谢谢聆听 · 欢迎提问", size=26, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_textbox(slide, Inches(1.2), Inches(5.7), Inches(10.9), Inches(0.9),
"声明:输出内容仅供教学实训与辅助决策演示,不能替代执业医师正式诊断与医疗文书。\n"
"文档依据:项目详细文档.md v1.0 · 2026-07-27",
size=11, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.CENTER)
def main():
prs = Presentation()
prs.slide_width = SLIDE_W
prs.slide_height = SLIDE_H
slide_01_cover(prs)
slide_02_toc(prs)
slide_03_theme(prs)
slide_04_scenes(prs)
slide_05_pain(prs)
slide_06_priority(prs)
slide_07_panorama(prs)
slide_08_modules(prs)
slide_09_imaging(prs)
slide_10_emr(prs)
slide_11_admin(prs)
slide_12_tech(prs)
slide_13_arch(prs)
slide_14_modules_dir(prs)
slide_15_flow(prs)
slide_16_data_api(prs)
slide_17_deploy(prs)
slide_18_design(prs)
slide_19_end(prs)
try:
prs.save(OUT)
print(f"Saved: {OUT}")
except PermissionError:
prs.save(OUT_ALT)
print(f"原文件被占用,已另存: {OUT_ALT}")
print(f"Slides: {len(prs.slides)}")
if __name__ == "__main__":
main()