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