26-7-31-1
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
# -*- 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'<a:ea xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" typeface="{FONT}"/>'
|
||||
)
|
||||
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<T> + 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()
|
||||
Reference in New Issue
Block a user