This commit is contained in:
Your Name
2026-08-18 14:08:38 +08:00
parent 8b9df1154c
commit bc1228a310
77 changed files with 10763 additions and 1181 deletions
+27 -2
View File
@@ -31,6 +31,8 @@ class _ScreenshotDiagnosisDialog(QWidget):
consultations_module.DiagnosisDialog = _ScreenshotDiagnosisDialog
DENSITY_SIZES = ((1366, 768), (1710, 920))
def _row(identifier: int, variant: int) -> dict[str, Any]:
common: dict[str, Any] = {
@@ -335,7 +337,7 @@ def _save_with_payment_qr(
return path
def render() -> list[Path]:
def _application() -> QApplication:
app = QApplication.instance() or QApplication([])
# The offscreen Windows plugin does not enumerate system fonts. Register
# the same CJK face used by the production QSS when it is available.
@@ -345,12 +347,35 @@ def render() -> list[Path]:
families = QFontDatabase.applicationFontFamilies(font_id)
if families:
app.setFont(QFont(families[0], 9))
return app
def render_density() -> list[Path]:
"""Render only the two desktop-density acceptance sizes."""
app = _application()
root = Path(__file__).resolve().parents[1]
output = root / "artifacts" / "diagnosis_visual"
output.mkdir(parents=True, exist_ok=True)
paths: list[Path] = []
repository = ScreenshotRepository()
for width, height in ((1024, 640), (1440, 900)):
for width, height in DENSITY_SIZES:
page = _new_page(app, repository, width, height)
path = output / f"diagnosis_{width}x{height}.png"
paths.append(_save(page, path))
page.close()
app.processEvents()
return paths
def render() -> list[Path]:
app = _application()
root = Path(__file__).resolve().parents[1]
output = root / "artifacts" / "diagnosis_visual"
output.mkdir(parents=True, exist_ok=True)
paths: list[Path] = []
repository = ScreenshotRepository()
for width, height in ((1024, 640), *DENSITY_SIZES, (1440, 900)):
page = _new_page(app, repository, width, height)
path = output / f"diagnosis_{width}x{height}.png"
paths.append(_save(page, path))
@@ -0,0 +1,119 @@
"""Render patient-level AI report layout regressions with the offscreen Qt backend."""
from __future__ import annotations
import os
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtGui import QFontDatabase
from PySide6.QtWidgets import QApplication, QLabel, QScrollArea
from doctor_workstation.ui.pages.reception import _ReceptionAiAnalysisDialog
def _report_payload(model_key: str) -> dict[str, object]:
model_label = "OpenAI" if model_key == "openai" else "千问"
return {
"model_key": model_key,
"model_label": model_label,
"generated_at": "2026-08-17 10:20:00",
"diagnosis_advice": [
"2型糖尿病,HbA1c 7.5%,近期空腹血糖仍有波动。",
"建议:1. 监测空腹血糖 2. 记录餐后2小时血糖 3. 复核低血糖症状",
r"保留患者原始报告结构。\n结合复诊记录动态调整随访频率。",
],
"risk_assessment": [
{"label": "低血糖", "level": "high"},
{"label": "依从性风险", "level": "medium"},
{"label": "并发症筛查延误风险", "level": "low"},
{"label": "复诊中断风险", "level": "medium"},
{
"label": "肾功能变化可能影响二甲双胍方案,需要结合复查结果持续评估。",
"level": "high",
},
{"label": "饮食波动风险", "level": "low"},
],
"treatment_advice": [
r"二甲双胍 0.5g,每日2次。\n复查肾功能后再评估剂量。",
"继续糖尿病饮食教育,并记录运动后的血糖变化。",
"如出现心悸、出汗或意识异常,及时复测血糖并按流程处置。",
],
}
def _render(
app: QApplication,
output: Path,
*,
width: int,
height: int,
) -> None:
histories = {
"qwen": [_report_payload("qwen")],
"openai": [_report_payload("openai")],
}
dialog = _ReceptionAiAnalysisDialog(histories, preferred_model="qwen")
dialog.resize(width, height)
dialog.show()
for _index in range(3):
app.processEvents()
pixmap = dialog.grab()
if pixmap.width() != width or pixmap.height() != height:
raise RuntimeError(
f"unexpected render size: {pixmap.width()}x{pixmap.height()} "
f"(expected {width}x{height})"
)
output.parent.mkdir(parents=True, exist_ok=True)
if not pixmap.save(str(output), "PNG"):
raise RuntimeError(f"failed to save {output}")
scrolls = dialog.findChildren(QScrollArea)
risks = [
label
for label in dialog.findChildren(QLabel)
if label.property("dialogAiRisk")
]
print(
"PATIENT_AI_LAYOUT",
f"{width}x{height}",
f"scrolls={len(scrolls)}",
f"horizontal_max={dialog.scroll_area.horizontalScrollBar().maximum()}",
f"risk_rows={len({label.y() for label in risks})}",
f"body_height={dialog.scroll_area.widget().height()}",
)
print(output)
dialog.close()
app.processEvents()
def main() -> int:
app = QApplication.instance() or QApplication([])
font_path = Path(r"C:\Windows\Fonts\msyh.ttc")
if font_path.is_file():
QFontDatabase.addApplicationFont(str(font_path))
output_dir = (
Path(__file__).resolve().parents[1]
/ "artifacts"
/ "patient_ai_report_layout"
)
_render(
app,
output_dir / "patient_ai_report_920x760.png",
width=920,
height=760,
)
_render(
app,
output_dir / "patient_ai_report_720x560.png",
width=720,
height=560,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,182 @@
"""Render patient and appointment density gates in the real desktop shell."""
from __future__ import annotations
import os
from datetime import date
from pathlib import Path
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QThreadPool
from PySide6.QtGui import QFont, QFontDatabase
from PySide6.QtWidgets import QApplication
from doctor_workstation.services import DemoDoctorRepository
from doctor_workstation.ui import ShellWindow, apply_theme
def _drain(application: QApplication) -> None:
QThreadPool.globalInstance().waitForDone(5_000)
for _index in range(8):
application.processEvents()
def _patient_rows() -> list[dict[str, Any]]:
names = ("林晓岚", "赵明远", "吴诗雨", "周安然", "程静", "许清和")
rows: list[dict[str, Any]] = []
for index in range(15):
rows.append(
{
"id": 101 + index,
"diagnosis_id": 501 + index,
"patient_id": 301 + index,
"patient_name": names[index % len(names)],
"gender": 2 if index % 2 == 0 else 1,
"age": 34 + index,
"phone_masked": f"138****{1200 + index:04d}",
"assistant_id": 8,
"assistant_name": "周医助",
"appointment_id": 701 + index,
"appointment_status": 1,
"appointment_doctor_name": "陈医生",
"appointment_time_text": f"2026-08-{17 + index % 3:02d} {9 + index % 7:02d}:00",
"revisit_count": index % 4,
"confirmation_text": "已确认" if index % 2 == 0 else "待确认",
"diagnosis_date_text": "第 2 次复诊" if index % 3 else "初诊",
"has_id_card": index % 4 != 0,
}
)
return rows
def _appointment_rows() -> list[dict[str, Any]]:
names = ("林晓岚", "赵明远", "吴诗雨", "周安然", "程静", "许清和")
today = date.today().isoformat()
rows: list[dict[str, Any]] = []
for index in range(8):
rows.append(
{
"id": 801 + index,
"diagnosis_id": 901 + index,
"patient_id": 401 + index,
"patient_name": names[index % len(names)],
"patient_phone": f"1380013{8000 + index}",
"gender": 2 if index % 2 == 0 else 1,
"age": 38 + index,
"doctor_name": "陈医生",
"assistant_id": 8,
"assistant_name": "周医助",
"appointment_date": today,
"appointment_time": f"{9 + index:02d}:00",
"channel_name": "线上复诊",
"diagnosis_confirmed": index % 2 == 0,
"has_prescription": index % 3 == 0,
"status": 1,
"status_desc": "已挂号",
"revisit_time": "复诊" if index % 2 else "初诊",
"unserved_days": index,
}
)
return rows
def render() -> list[Path]:
application = QApplication.instance() or QApplication([])
apply_theme(application)
font_path = Path("C:/Windows/Fonts/msyh.ttc")
if font_path.is_file():
font_id = QFontDatabase.addApplicationFont(str(font_path))
families = QFontDatabase.applicationFontFamilies(font_id)
if families:
application.setFont(QFont(families[0], 9))
output = Path(__file__).resolve().parents[1] / "artifacts" / "patient_appointment_density"
output.mkdir(parents=True, exist_ok=True)
paths: list[Path] = []
for width, height in ((1366, 768), (1024, 640)):
repository = DemoDoctorRepository()
session = repository.login(repository.DEMO_ACCOUNT, repository.DEMO_PASSWORD)
shell = ShellWindow(
repository,
{"session": session, "demo_mode": True},
permissions=session.permissions,
)
shell.resize(width, height)
shell.show()
_drain(application)
if not shell.navigate("patients"):
raise RuntimeError("patients navigation is unavailable")
_drain(application)
patients = shell.pages["patients"]
patient_rows = _patient_rows()
patients.patient_workspace._apply_result(
{
"lists": patient_rows,
"count": len(patient_rows),
"extend": {
"scope": {"label": "当前医生与部门"},
"summary": {"today": 6, "tomorrow": 5, "day_after": 4},
},
},
patients.patient_workspace._generation,
)
application.processEvents()
patient_slots = patients.patient_workspace.table.viewport().height() // 40
if width == 1366 and patient_slots < 6:
raise RuntimeError(f"patient table only exposes {patient_slots} ordinary rows")
patient_path = output / f"patients_{width}x{height}.png"
if not shell.grab().save(str(patient_path), "PNG"):
raise RuntimeError(f"failed to save {patient_path}")
paths.append(patient_path)
if not shell.navigate("appointments"):
raise RuntimeError("appointments navigation is unavailable")
_drain(application)
appointments = shell.pages["appointments"]
appointments.poll_timer.stop()
appointment_rows = _appointment_rows()
appointments._loaded(
{
"lists": appointment_rows,
"count": len(appointment_rows),
"extend": {
"status_count": {"1": len(appointment_rows), "3": 0},
"unassigned_count": 0,
},
},
appointments._generation,
False,
)
application.processEvents()
row_heights = [
appointments.table.rowHeight(index)
for index in range(appointments.table.rowCount())
]
appointment_slots = appointments.table.viewport().height() // max(row_heights)
if width == 1366 and appointment_slots < 4:
raise RuntimeError(
f"appointment table only exposes {appointment_slots} ordinary rows"
)
if width == 1024 and not appointments.video_panel.isHidden():
raise RuntimeError("narrow appointment viewport did not collapse the video panel")
appointment_path = output / f"appointments_{width}x{height}.png"
if not shell.grab().save(str(appointment_path), "PNG"):
raise RuntimeError(f"failed to save {appointment_path}")
paths.append(appointment_path)
print(
f"{width}x{height}: patient_slots={patient_slots}, "
f"appointment_slots={appointment_slots}, video_hidden={appointments.video_panel.isHidden()}"
)
shell.close()
application.processEvents()
return paths
if __name__ == "__main__":
for rendered in render():
print(rendered)
@@ -0,0 +1,180 @@
"""Render deterministic prescription-list density acceptance screenshots."""
from __future__ import annotations
import os
from pathlib import Path
from types import SimpleNamespace
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtGui import QFont, QFontDatabase
from PySide6.QtWidgets import QApplication
from doctor_workstation.ui.pages import prescription_library as library_module
from doctor_workstation.ui.pages import prescriptions as prescriptions_module
from doctor_workstation.ui.pages.prescription_library import PrescriptionLibraryPage
from doctor_workstation.ui.pages.prescriptions import PrescriptionsPage
from doctor_workstation.ui.theme import apply_theme
DENSITY_SIZES = ((1366, 768), (1710, 920))
def _run_immediately(
function: Any,
*args: Any,
on_success: Any = None,
on_error: Any = None,
on_finished: Any = None,
**kwargs: Any,
) -> object:
try:
result = function(*args, **kwargs)
except Exception as error:
if on_error is not None:
on_error(error)
else:
if on_success is not None:
on_success(result)
finally:
if on_finished is not None:
on_finished()
return object()
prescriptions_module.run_async = _run_immediately
library_module.run_async = _run_immediately
def _issued_row(index: int) -> dict[str, Any]:
return {
"id": 1000 + index,
"sn": f"CF-202608-{1000 + index}",
"prescription_type": "汤剂",
"is_system_auto": index % 2,
"patient_name": ("林晓岚", "周明远", "许安然")[index % 3],
"gender": 2 if index % 2 else 1,
"age": 29 + index,
"audit_status": index % 3,
"void_status": 0,
"has_prescription_order": index % 2,
"creator_id": 7,
"doctor_name": "陈医生",
"assistant_name": "赵医助",
"create_time": f"2026-08-{(index % 9) + 10:02d} 09:30:00",
"herbs": [{"name": "黄芪", "dosage": 15}],
}
def _library_row(index: int) -> dict[str, Any]:
return {
"id": 2000 + index,
"prescription_name": ("益气养阴方", "清热祛湿方", "滋阴调和方")[index % 3],
"formula_type": "主方" if index % 3 else "辅方",
"herbs": [
{"name": "黄芪", "dosage": 15},
{"name": "党参", "dosage": 12},
],
"efficacy": ("益气养阴", "清热祛湿", "滋阴补肾")[index % 3],
"is_public": index % 2,
"disable_edit": 0,
"creator_id": 7,
"creator_name": "陈医生",
"create_time": f"2026-08-{(index % 9) + 10:02d} 08:20:00",
}
class ScreenshotRepository:
def __init__(self) -> None:
self.issued_rows = [_issued_row(index) for index in range(15)]
self.library_rows = [_library_row(index) for index in range(15)]
def list_diagnosis_doctors(self) -> list[dict[str, Any]]:
return [{"id": 7, "name": "陈医生"}, {"id": 8, "name": "孙医生"}]
def list_prescriptions(self, **_filters: Any) -> dict[str, Any]:
return {"lists": self.issued_rows, "count": 44}
def list_prescription_templates(self, **_filters: Any) -> dict[str, Any]:
return {"lists": self.library_rows, "count": 41}
def _application() -> QApplication:
app = QApplication.instance() or QApplication([])
font_path = Path("C:/Windows/Fonts/msyh.ttc")
if font_path.is_file():
font_id = QFontDatabase.addApplicationFont(str(font_path))
families = QFontDatabase.applicationFontFamilies(font_id)
if families:
app.setFont(QFont(families[0], 9))
apply_theme(app)
return app
def _settle(app: QApplication) -> None:
for _ in range(6):
app.processEvents()
def _visible_rows(page: PrescriptionsPage | PrescriptionLibraryPage) -> int:
viewport = page.table.viewport()
return sum(
1
for row in range(page.table.rowCount())
if (
(item := page.table.item(row, 0)) is not None
and (rect := page.table.visualItemRect(item)).isValid()
and rect.top() >= 0
and rect.bottom() < viewport.height()
)
)
def _new_page(
kind: str,
repository: ScreenshotRepository,
) -> PrescriptionsPage | PrescriptionLibraryPage:
current_user = SimpleNamespace(id=7, name="陈医生", root=1, role_ids=[0])
if kind == "prescriptions":
page: PrescriptionsPage | PrescriptionLibraryPage = PrescriptionsPage(
repository, {"*"}, current_user
)
else:
page = PrescriptionLibraryPage(repository, {"*"}, current_user)
page.refresh()
return page
def render() -> list[Path]:
app = _application()
root = Path(__file__).resolve().parents[1]
output = root / "artifacts" / "prescription_list_density"
output.mkdir(parents=True, exist_ok=True)
repository = ScreenshotRepository()
paths: list[Path] = []
for kind in ("prescriptions", "prescription_library"):
for width, height in DENSITY_SIZES:
page = _new_page(kind, repository)
page.resize(width, height)
page.show()
_settle(app)
minimum_rows = 6 if height == 768 else 9
visible_rows = _visible_rows(page)
if visible_rows < minimum_rows:
raise RuntimeError(
f"{kind} at {width}x{height} exposes only {visible_rows} full rows"
)
path = output / f"{kind}_{width}x{height}.png"
if not page.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
paths.append(path)
page.close()
_settle(app)
return paths
if __name__ == "__main__":
for rendered in render():
print(rendered)
@@ -0,0 +1,69 @@
"""Render the reception daily-record matrix for visual acceptance."""
from __future__ import annotations
import os
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QThreadPool
from PySide6.QtGui import QFontDatabase
from PySide6.QtWidgets import QApplication
from doctor_workstation.services.mock_repository import DemoDoctorRepository
from doctor_workstation.ui.shell import ShellWindow
from doctor_workstation.ui.theme import apply_theme
def main() -> int:
application = QApplication.instance() or QApplication([])
apply_theme(application)
font_path = Path(r"C:\Windows\Fonts\msyh.ttc")
if font_path.is_file():
QFontDatabase.addApplicationFont(str(font_path))
repository = DemoDoctorRepository()
session = repository.login("doctor", "doctor123")
window = ShellWindow(repository, session)
window.resize(1710, 920)
window.show()
if not window.navigate("reception"):
raise RuntimeError("reception navigation is unavailable")
for _index in range(5):
application.processEvents()
QThreadPool.globalInstance().waitForDone(10_000)
page = window.pages["reception"]
daily_index = next(
index
for index in range(page.detail_tabs.count())
if page.detail_tabs.tabText(index) == "日常记录"
)
page.detail_tabs.setCurrentIndex(daily_index)
for _index in range(3):
application.processEvents()
QThreadPool.globalInstance().waitForDone(10_000)
output = (
Path(__file__).resolve().parents[1]
/ "artifacts"
/ "reception_daily_records"
/ "reception_daily_records_1710x920.png"
)
output.parent.mkdir(parents=True, exist_ok=True)
if not window.grab().save(str(output), "PNG"):
raise RuntimeError(f"failed to save {output}")
print(output)
print(
"DAILY_MATRIX",
page.daily_panel.matrix.rowCount(),
page.daily_panel.matrix.columnCount(),
page.daily_panel.current_range(),
)
window.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())