This commit is contained in:
Your Name
2026-08-14 14:37:30 +08:00
parent 21790e35f4
commit 18c15d1262
117 changed files with 28157 additions and 8080 deletions
@@ -12,20 +12,65 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtGui import QFont, QFontDatabase
from PySide6.QtWidgets import QApplication, QWidget
from doctor_workstation.core.permissions import PermissionSet
from doctor_workstation.ui import apply_theme
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module
from doctor_workstation.ui.dialogs.prescription import PrescriptionEditorDialog
from doctor_workstation.ui.dialogs.prescription import (
PrescriptionDetailDialog,
PrescriptionEditorDialog,
)
from doctor_workstation.ui.dialogs.prescription_ai import PrescriptionAiReportDialog
class VisualRepository:
def list_medicines(self, **_filters: Any) -> dict[str, Any]:
return {"lists": [], "count": 0}
def list_prescription_template_ai_reports(self, template_id: int) -> dict[str, Any]:
report = {
"summary": "益气健脾为主,兼顾养阴安神,适合脾气不足、气阴两虚方向的复核参考。",
"possible_symptoms": ["乏力气短", "食少便溏", "睡眠不安"],
"main_indications": "气阴两虚、脾气不足所见的倦怠与纳差。",
"efficacy": ["益气健脾", "养阴生津", "宁心安神"],
"suitable_people": ["辨证属气阴两虚者", "需由医师结合四诊确认"],
"compatibility_analysis": "黄芪、党参、白术与茯苓协同补气健脾,麦冬、五味子兼顾养阴敛津,酸枣仁与远志用于宁心安神。",
"cautions": ["仍需核对过敏史与现用药", "症状变化时及时复诊"],
"disclaimer": "仅供专业人员辅助审方,不替代辨证、诊断和处方审核。",
}
return {
"prescription_id": template_id,
"can_refresh": True,
"can_edit": True,
"reports": [
{
"report_id": 21,
"model_key": "qwen",
"model_label": "千问",
"model_name": "qwen3.6-35b",
"generated_at": "2026-08-13 10:32:00",
"report": report,
"is_stale": False,
"is_edited": True,
},
{
"report_id": 22,
"model_key": "openai",
"model_label": "OpenAI",
"model_name": "gpt-5.6-sol",
"generated_at": "2026-08-13 10:32:00",
"report": report,
"is_stale": False,
"is_edited": False,
},
],
}
def _seed() -> dict[str, Any]:
return {
"diagnosis_id": 501,
"id": 2501,
"appointment_id": 2501,
"prescription_no": "RX202608130021",
"patient_name": "林晓岚",
"phone": "13800138000",
"gender": 0,
@@ -57,6 +102,8 @@ def _seed() -> dict[str, Any]:
"prescription_name": "宁心辅方",
},
"doctor_name": "陈医生",
"audit_status": 1,
"create_time": "2026-08-13 10:26:00",
"herbs": [
{"medicine_id": 11, "name": "黄芪", "dosage": 15, "formula_type": "主方"},
{"medicine_id": 12, "name": "党参", "dosage": 12, "formula_type": "主方"},
@@ -78,11 +125,6 @@ def _make_editor(host: QWidget) -> PrescriptionEditorDialog:
current_user=SimpleNamespace(id=1, name="陈医生"),
parent=host,
)
# Diagnosis-detail callers apply their scoped sheet after construction. The
# prescription surface owns its own scoped rules, so this also verifies that
# real entry path rather than a renderer-only appearance.
editor.setObjectName("DiagnosisPrescriptionEditor")
editor.setStyleSheet(diagnosis_module.DIAGNOSIS_QSS)
return editor
@@ -101,43 +143,59 @@ def render() -> list[Path]:
if families:
app.setFont(QFont(families[0], 9))
output = Path(__file__).resolve().parents[1] / "artifacts" / "diagnosis_visual"
output = Path(__file__).resolve().parents[1] / "artifacts" / "subwindow_exact"
output.mkdir(parents=True, exist_ok=True)
rendered: list[Path] = []
for host_width, height in ((1440, 900), (1024, 768), (920, 780)):
host = QWidget()
host.resize(host_width, height)
host.show()
editor = _make_editor(host)
editor.show()
_settle(app)
editor.body_scroll.verticalScrollBar().setValue(0)
_settle(app, 3)
width = min(PrescriptionEditorDialog.DRAWER_WIDTH, host_width)
path = output / f"diagnosis_state_prescription_editor_{width}x{height}.png"
if not editor.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
editor.close()
host.close()
_settle(app, 2)
host = QWidget()
host.resize(1440, 900)
host.show()
editor = _make_editor(host)
editor.show()
_settle(app)
for index, state in ((1, "herbs"), (2, "usage"), (3, "signature")):
editor.tabs.setCurrentIndex(index)
_settle(app, 4)
path = output / f"diagnosis_state_prescription_editor_{state}_1200x900.png"
if not editor.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
editor.body_scroll.verticalScrollBar().setValue(0)
_settle(app, 4)
path = output / "prescription_editor_860x900.png"
if not editor.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
editor.close()
host.close()
detail = PrescriptionDetailDialog(
_seed(),
can_open_diagnosis=True,
can_open_orders=True,
)
detail.show()
_settle(app, 10)
path = output / "prescription_detail_920x780.png"
if not detail.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
detail.close()
_settle(app, 2)
ai_dialog = PrescriptionAiReportDialog(
VisualRepository(),
PermissionSet(["*", "tcm.prescriptionLibrary/editAiReport"]),
)
ai_dialog.open_for(
{
"id": 2501,
"prescription_name": "益气养阴安神方",
"formula_type": "主方",
"herbs": _seed()["herbs"],
}
)
ai_dialog.show()
_settle(app, 16)
path = output / "prescription_ai_report_920x760.png"
if not ai_dialog.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
ai_dialog.close()
_settle(app, 2)
return rendered
+78
View File
@@ -0,0 +1,78 @@
"""Render the 1710×920 reception AI acceptance artifact offscreen."""
from __future__ import annotations
import os
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QPoint, 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:
app = QApplication.instance() or QApplication([])
apply_theme(app)
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(4):
app.processEvents()
QThreadPool.globalInstance().waitForDone(10_000)
app.processEvents()
output = Path(__file__).resolve().parents[1] / "artifacts" / "reception_ai_exact" / "reception.png"
output.parent.mkdir(parents=True, exist_ok=True)
pixmap = window.grab()
if pixmap.size().width() != 1710 or pixmap.size().height() != 920:
raise RuntimeError(f"unexpected render size: {pixmap.size().width()}x{pixmap.size().height()}")
if not pixmap.save(str(output), "PNG"):
raise RuntimeError(f"failed to save {output}")
page = window.pages.get("reception")
if page is not None:
left = page.ai_analysis_card.geometry()
right = page.ai_assistant_card.geometry()
left_global = page.ai_analysis_card.mapTo(window, QPoint(0, 0))
right_global = page.ai_assistant_card.mapTo(window, QPoint(0, 0))
print(
"AI_CARDS",
left.x(),
left.y(),
left.width(),
left.height(),
right.x(),
right.y(),
right.width(),
right.height(),
page._ai_analysis_state,
)
print(
"AI_CARDS_GLOBAL",
left_global.x(),
left_global.y(),
right_global.x(),
right_global.y(),
)
print(output)
window.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,93 @@
"""Render the structured reception medication/case card for visual review."""
from __future__ import annotations
import os
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QThreadPool
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:
app = QApplication.instance() or QApplication([])
apply_theme(app)
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(4):
app.processEvents()
QThreadPool.globalInstance().waitForDone(10_000)
page = window.pages.get("reception")
if page is None:
raise RuntimeError("reception page was not created")
page.detail_stack.setCurrentIndex(1)
medication_index = next(
index
for index in range(page.detail_tabs.count())
if page.detail_tabs.tabText(index) == "用药记录"
)
page.detail_tabs.setCurrentIndex(medication_index)
page._render_case(
{"has_prescription": True},
{"age": 56, "gender": 1},
{
"diagnosis_date": "2026-08-14",
"diagnosis_type_text": "复诊",
"systolic": 148,
"diastolic": 92,
"blood_pressure_status": "偏高",
"fasting_blood_sugar": "6.8",
"fasting_blood_sugar_status": "偏高",
"clinical_diagnosis": "2 型糖尿病(血糖控制未达标),合并高血压与轻度脂肪肝",
"current_medications": [
"二甲双胍缓释片 0.5 g,早晚餐后各一次",
"格列吡嗪片 5 mg,早餐前一次",
],
"allergy_history_text": "青霉素过敏",
"chief_complaint": "口干口苦、多饮多汗,近期睡眠欠佳",
"present_illness": (
"近两周空腹血糖波动,伴头昏、腰膝酸软及夜间多尿;"
"未发生明确低血糖,服药依从性一般。"
),
"tongue": "舌红,苔黄腻",
"pulse": "脉弦滑",
"treatment_principle": "益气养阴、清热利湿,兼顾健脾化痰",
"diabetes_history_text": "10 年",
"past_history_text": "高血压 6 年、高脂血症 3 年",
"family_history_text": "父亲患 2 型糖尿病",
"sleep_condition_text": "多梦、入睡困难",
"diet_condition_text": "主食量偏多,晚餐较晚",
"remark": "重点核对降糖药剂量,复查肝肾功能,并关注夜间低血糖风险。",
},
)
for _index in range(6):
app.processEvents()
output = (
Path(__file__).resolve().parents[1]
/ "artifacts"
/ "reception_medication_case"
/ "reception_medication_case_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)
window.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+121
View File
@@ -0,0 +1,121 @@
"""Render focused blue-white diagnosis subwindow acceptance references."""
from __future__ import annotations
import os
import sys
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
from PySide6.QtGui import QFont, QFontDatabase # noqa: E402
from PySide6.QtWidgets import QApplication # noqa: E402
from render_diagnosis_detail_visual import ( # noqa: E402
ScreenshotRepository,
_run_immediately,
)
from render_diagnosis_order_video_visual import order_detail # noqa: E402
from doctor_workstation.core import PermissionSet # noqa: E402
from doctor_workstation.services import DemoDoctorRepository # noqa: E402
from doctor_workstation.ui.diagnosis_editors import DailyRecordEditorDialog # noqa: E402
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module # noqa: E402
from doctor_workstation.ui.dialogs import prescription_ai as ai_module # noqa: E402
from doctor_workstation.ui.dialogs.diagnosis import DiagnosisDialog # noqa: E402
from doctor_workstation.ui.dialogs.prescription_ai import ( # noqa: E402
DIAGNOSIS_AI_KIND,
PrescriptionAiReportDialog,
)
def _save(widget: object, path: Path, app: QApplication) -> None:
widget.show()
for _ in range(8):
app.processEvents()
if not widget.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
def main() -> int:
output = ROOT / "artifacts" / "subwindow_exact"
output.mkdir(parents=True, exist_ok=True)
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))
diagnosis_module.run_async = _run_immediately
ai_module.run_async = _run_immediately
host = DiagnosisDialog(ScreenshotRepository(locked=False), permissions=PermissionSet(["*"]))
host.resize(1280, 800)
host.open_for(501, editable=True)
_save(host, output / "diagnosis_edit_drawer_1280x800.png", app)
order = host._build_order_detail_dialog(order_detail(), 801)
_save(order, output / "diagnosis_order_detail_1280x800.png", app)
order.close()
host.close()
app.processEvents()
daily = DailyRecordEditorDialog(
"blood",
{
"id": 6101,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-10",
"record_time": "08:20",
"fasting_blood_sugar": 8.2,
"postprandial_blood_sugar": 12.4,
"systolic_pressure": 146,
"diastolic_pressure": 92,
"western_medicine": "二甲双胍",
"remark": "继续观察餐后波动。",
},
)
daily.setStyleSheet(diagnosis_module.DIAGNOSIS_QSS)
daily.resize(680, 660)
_save(daily, output / "diagnosis_daily_editor_680x660.png", app)
daily.close()
app.processEvents()
ai_report = PrescriptionAiReportDialog(
DemoDoctorRepository(),
permissions=PermissionSet(["*"]),
kind=DIAGNOSIS_AI_KIND,
)
ai_report.open_for(
{
"id": 501,
"diagnosis_id": 501,
"patient_name": "林晓岚",
"diagnosis_date": "2026-08-10",
"diagnosis_type": "复诊",
"syndrome_type": "气阴两虚",
"chief_complaint": "口干乏力,餐后血糖波动",
"present_illness": "近两周睡眠改善,仍需记录空腹与餐后血糖。",
"clinical_diagnosis": "2 型糖尿病",
"treatment_principle": "益气养阴,兼顾饮食与运动管理",
"doctor_advice": "连续记录七日血糖并按时复诊",
}
)
ai_report.resize(920, 760)
_save(ai_report, output / "diagnosis_ai_report_920x760.png", app)
ai_report.close()
app.processEvents()
for path in sorted(output.glob("*.png")):
print(path)
return 0
if __name__ == "__main__":
raise SystemExit(main())