This commit is contained in:
Your Name
2026-08-11 09:12:51 +08:00
parent c3ceb0dd0f
commit cfe4c82c90
111 changed files with 26110 additions and 826 deletions
@@ -0,0 +1,694 @@
"""Render deterministic diagnosis readonly, edit, and view-only references."""
from __future__ import annotations
import os
from pathlib import Path
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.core import PermissionSet
from doctor_workstation.ui.diagnosis_editors import DailyRecordEditorDialog
from doctor_workstation.ui.diagnosis_media import RecordingPlayerDialog
from doctor_workstation.ui.dialogs import diagnosis as diagnosis_module
from doctor_workstation.ui.dialogs.diagnosis import DiagnosisDialog
from doctor_workstation.ui.dialogs.prescription import PrescriptionEditorDialog
def _detail(*, locked: bool) -> dict[str, Any]:
diagnosis: dict[str, Any] = {
"id": 501,
"patient_id": 1501,
"patient_name": "林晓岚",
"phone": "13800138000",
"id_card": "110105199203071234",
"gender": 0,
"age": 34,
"marital_status": 1,
"height": 162,
"weight": 54.5,
"region": "浙江省杭州市",
"systolic_pressure": 146,
"diastolic_pressure": 92,
"fasting_blood_sugar": 8.2,
"diagnosis_type": "follow_up",
"diagnosis_date": "2026-08-10",
"status_desc": "诊疗中",
"channel_name": "健康顾问转介",
"statistical_visit_card": "第 4 次",
"current_medications": "二甲双胍",
"diabetes_discovery_year": "2019 年",
"local_hospital_diagnosis": ["2 型糖尿病", "高血压"],
"local_hospital_name": "杭州市第一人民医院",
"oral_condition": "口干",
"water_intake": "约 2200 ml",
"weight_change": "近月下降 1 kg",
"fatty_liver_degree": "轻度",
"diet_condition": ["偏甜", "夜宵"],
"body_feeling": ["乏力", "四肢沉重"],
"sleep_condition": "入睡稍慢",
"eye_condition": "偶有干涩",
"head_feeling": "午后头昏",
"sweat_condition": "易出汗",
"skin_condition": "皮肤偏干",
"urine_condition": "夜尿 2 次",
"stool_condition": "每日一次",
"kidney_condition": "腰酸",
"present_illness": "口渴、乏力反复半年,近期血糖波动。",
"past_history": "高血压病史 5 年。",
"trauma_history": 0,
"surgery_history": 0,
"allergy_history": 1,
"family_history": 1,
"pregnancy_history": 0,
"remark": "建议持续记录空腹及餐后血糖。",
"clinical_diagnosis": "气阴两虚",
"prescription_opinion": "益气养阴,兼顾活血。",
"has_prescription": 1,
"unserved_days": 4,
"patient_basic_locked": locked,
"can_edit_patient_basic": not locked,
"latest_prescription_order": {
"id": "RX-240810-09",
"fulfillment_status_text": "待配药",
},
}
return {
"diagnosis": diagnosis,
"patient": {
"id": 1501,
"patient_name": "林晓岚",
"phone": "13800138000",
"id_card": "110105199203071234",
"gender": 0,
"age": 34,
},
"appointment": {
"id": 2501,
"appointment_date": "2026-08-10",
"appointment_time": "09:00-09:30",
"period_text": "上午",
"doctor_name": "陈医生",
"assistant_name": "赵医助",
"status_desc": "已到诊",
"remark": "复诊评估",
},
}
class ScreenshotRepository:
"""No-network data source for visual artifact generation."""
def __init__(self, *, locked: bool) -> None:
self.detail = _detail(locked=locked)
def get_diagnosis_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
return self.detail
diagnosis_readonly_detail = get_diagnosis_detail
patient_detail = get_diagnosis_detail
def get_dictionary(self, dictionary_type: str) -> list[dict[str, Any]]:
if dictionary_type == "diagnosis_type":
return [
{"name": "初诊", "value": "first_visit"},
{"name": "复诊", "value": "follow_up"},
{"name": "会诊", "value": "consultation"},
]
if dictionary_type == "appetite":
return [
{"name": "口干", "value": "口干"},
{"name": "口苦", "value": "口苦"},
]
return []
def get_doctor_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
return [
{
"id": 7001,
"create_time": "2026-08-10 09:20",
"doctor_name": "陈医生",
"content": "面色稍淡,舌淡红,苔薄白。",
"tongue_images": [{"url": "tongue.jpg"}],
"report_files": [
{
"name": "近期血糖趋势.pdf",
"url": "https://media.example.invalid/report.pdf",
}
],
}
]
def get_tracking_window(
self,
diagnosis_id: int,
*,
start_date: str = "",
end_date: str = "",
) -> dict[str, Any]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
del start_date, end_date
return {
"blood_records": [
{
"id": 6101,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-10",
"fasting_blood_sugar": 8.2,
"systolic_pressure": 146,
"source": 1,
},
{
"id": 6102,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-10",
"postprandial_blood_sugar": 12.4,
"diastolic_pressure": 92,
"western_medicine": "二甲双胍",
},
{
"id": 6103,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-09",
"fasting_blood_sugar": 7.6,
"postprandial_blood_sugar": 10.8,
},
],
"diet_records": [
{
"id": 6201,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-10",
"breakfast_foods": "燕麦、鸡蛋",
"lunch_foods": "杂粮饭",
}
],
"exercise_records": [
{
"id": 6301,
"diagnosis_id": 501,
"patient_id": 1501,
"record_date": "2026-08-09",
"exercise_type": "散步",
"duration": 35,
"intensity": 2,
}
],
}
def list_tracking_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
return [{"note_date": "2026-08-10", "content": "饭后散步,继续观察。"}]
def list_diagnosis_todos(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
return {
"lists": [
{
"remind_time_text": "2026-08-12 09:00",
"content": "回访复测餐后血糖",
"status": 0,
"status_text": "待执行",
"creator_name": "陈医生",
"can_cancel": True,
"id": 7101,
}
],
"count": 1,
}
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
if diagnosis_id != 501:
raise LookupError(diagnosis_id)
return [
{
"id": 901,
"diagnosis_id": 501,
"prescription_date": "2026-08-02",
"global_visit_sequence": 4,
"prescription_type_text": "中药",
"prescription_summary": "玉泉丸加减",
"doctor_name": "陈医生",
"status_text": "已审核",
}
]
def appointment_history(self, **_kwargs: Any) -> dict[str, Any]:
return {
"lists": [
{
"id": 2501,
"status_desc": "已到诊",
"patient_name": "林晓岚",
"patient_phone": "13800138000",
"doctor_name": "陈医生",
"assistant_name": "赵医助",
"appointment_date": "2026-08-10",
"appointment_time": "09:00-09:30",
"appointment_type_text": "复诊",
"channel_name": "健康顾问",
"is_confirmed": 1,
"has_prescription": 1,
"remark": "复诊评估",
"create_time": "2026-08-08 11:20",
}
]
}
def assign_history(self, **_kwargs: Any) -> dict[str, Any]:
return {
"lists": [
{
"create_time": "2026-08-08 11:22",
"from_assistant_name": "王医助",
"to_assistant_name": "赵医助",
"is_inherit": 1,
"snapshot_order_creator_name": "王医助",
"snapshot_order_create_time": "2026-08-01 10:00",
"operator_name": "陈医生",
"operator_account": "doctor.chen",
"ip": "10.1.2.8",
}
]
}
def list_prescription_orders(self, **_kwargs: Any) -> dict[str, Any]:
return {
"lists": [
{
"id": 801,
"order_no": "RX-240810-09",
"global_visit_sequence": 4,
"statistics_count": 1,
"amount": "¥ 286.00",
"doctor_name": "陈医生",
"assistant_name": "赵医助",
"fulfillment_status_text": "待配药",
"create_time": "2026-08-10 09:24",
}
],
"count": 1,
}
def get_prescription_order(self, order_id: int) -> dict[str, Any]:
if order_id != 801:
raise LookupError(order_id)
return {
"id": order_id,
"order_no": "RX-240810-09",
"patient_name": "林晓岚",
"amount": 286,
"fulfillment_status_text": "待配药",
}
def add_blood_record(self, payload: dict[str, Any]) -> dict[str, Any]:
return {"id": 7201, **payload}
def add_diet_record(self, payload: dict[str, Any]) -> dict[str, Any]:
return {"id": 7202, **payload}
def add_exercise_record(self, payload: dict[str, Any]) -> dict[str, Any]:
return {"id": 7203, **payload}
def update_blood_record(
self, record: dict[str, Any], changes: dict[str, Any] | None = None
) -> dict[str, Any]:
return {**record, **(changes or {})}
def update_diet_record(
self, record: dict[str, Any], changes: dict[str, Any] | None = None
) -> dict[str, Any]:
return {**record, **(changes or {})}
def update_exercise_record(
self, record: dict[str, Any], changes: dict[str, Any] | None = None
) -> dict[str, Any]:
return {**record, **(changes or {})}
def add_tracking_note(self, diagnosis_id: int, content: str) -> dict[str, Any]:
return {"id": 7204, "diagnosis_id": diagnosis_id, "content": content}
def add_diagnosis_todo(
self, diagnosis_id: int, content: str, remind_time: int
) -> dict[str, Any]:
return {
"id": 7205,
"diagnosis_id": diagnosis_id,
"content": content,
"remind_time": remind_time,
}
def cancel_diagnosis_todo(self, todo_id: int) -> dict[str, Any]:
return {"id": todo_id, "status": 2}
def add_doctor_note(self, diagnosis_id: int, content: str = "", **_kwargs: Any) -> Any:
return {"id": 7206, "diagnosis_id": diagnosis_id, "content": content}
def delete_doctor_note_image(self, note_id: int, image_type: str, image_path: str) -> Any:
return {"note_id": note_id, "image_type": image_type, "image_path": image_path}
def upload_material(self, path: str, material_type: str) -> str:
return f"https://media.example.invalid/{material_type}/{Path(path).name}"
def create_prescription(self, prescription: dict[str, Any]) -> Any:
return {"id": 7207, **prescription}
def set_revisit_slot_start_offset(self, diagnosis_id: int, offset: int) -> Any:
return {"id": diagnosis_id, "revisit_slot_start_offset": offset}
def list_call_records(self, diagnosis_id: int) -> list[dict[str, Any]]:
return [
{
"id": 7208,
"diagnosis_id": diagnosis_id,
"recording_urls_list": [
"https://media.example.invalid/replay.mp4",
"https://media.example.invalid/replay-backup.m3u8",
"https://media.example.invalid/replay.webm",
],
"start_time_text": "2026-08-10 09:10:00",
"end_time_text": "2026-08-10 09:22:00",
"call_type_text": "视频",
"room_id": "demo-room-501",
"duration_text": "12分00秒",
"status_text": "已结束",
"recording_status_text": "录制完成",
}
]
def upload_call_recording(
self, path: str, diagnosis_id: int, *, call_record_id: int | None = None
) -> dict[str, Any]:
return {
"diagnosis_id": diagnosis_id,
"call_record_id": call_record_id,
"file_url": Path(path).name,
}
def list_im_chat_messages(
self, diagnosis_id: int, *, only_archived: bool = True
) -> dict[str, Any]:
del diagnosis_id
return {
"only_archived": only_archived,
"lists": [
{
"msg_id": "chat-1",
"msg_type": "text",
"text": "今天空腹血糖 5.8。",
"is_from_doctor": False,
"time": "2026-08-10 08:30",
},
{
"msg_id": "chat-2",
"msg_type": "image",
"image_url": "https://media.example.invalid/blood.jpg",
"is_from_doctor": False,
"time": "2026-08-10 08:31",
},
{
"msg_id": "chat-3",
"msg_type": "file",
"file_url": "https://media.example.invalid/report.pdf",
"file_name": "复查报告.pdf",
"is_from_doctor": True,
"from_staff_name": "陈医生",
"time": "2026-08-10 08:40",
},
],
}
def sync_im_chat_messages(self, diagnosis_id: int) -> dict[str, Any]:
return {"diagnosis_id": diagnosis_id, "queued": True}
class EmptyScreenshotRepository(ScreenshotRepository):
def get_doctor_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
del diagnosis_id
return []
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
del diagnosis_id
return []
def list_prescription_orders(self, **_kwargs: Any) -> dict[str, Any]:
return {"lists": [], "count": 0}
class FailingScreenshotRepository(ScreenshotRepository):
def get_diagnosis_detail(self, diagnosis_id: int, **_kwargs: Any) -> dict[str, Any]:
del diagnosis_id
raise RuntimeError("诊单详情加载失败,请检查网络后重试")
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)
except Exception as error:
if on_error:
on_error(error)
else:
if on_success:
on_success(result)
finally:
if on_finished:
on_finished()
return object()
def render() -> list[Path]:
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
root = Path(__file__).resolve().parents[1]
output = root / "artifacts" / "diagnosis_visual"
output.mkdir(parents=True, exist_ok=True)
rendered: list[Path] = []
for width, height in ((1024, 640), (1440, 900)):
for mode in ("readonly", "edit", "viewonly"):
dialog = DiagnosisDialog(
ScreenshotRepository(locked=mode == "viewonly"),
permissions=PermissionSet(["*"]),
)
dialog.resize(width, height)
if mode == "readonly":
dialog.open_for(501, editable=False)
elif mode == "edit":
dialog.open_for(501, editable=True)
else:
dialog.open_view_only(501)
for _ in range(8):
app.processEvents()
if mode == "readonly":
dialog.readonly_scroll.verticalScrollBar().setValue(0)
else:
dialog.tabs.setCurrentIndex(0)
app.processEvents()
path = output / f"diagnosis_{mode}_{width}x{height}.png"
if not dialog.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
dialog.close()
app.processEvents()
def save_state(dialog: DiagnosisDialog, name: str, *, close: bool = True) -> None:
for _ in range(8):
app.processEvents()
path = output / f"diagnosis_state_{name}_1024x640.png"
if not dialog.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
if close:
dialog.close()
app.processEvents()
def save_widget(widget: Any, name: str) -> None:
widget.show()
for _ in range(6):
app.processEvents()
path = output / f"diagnosis_state_{name}.png"
if not widget.grab().save(str(path), "PNG"):
raise RuntimeError(f"failed to save {path}")
rendered.append(path)
widget.close()
app.processEvents()
pending: list[dict[str, Any]] = []
def queue_async(function: Any, **options: Any) -> object:
pending.append({"function": function, **options})
return object()
diagnosis_module.run_async = queue_async
loading = DiagnosisDialog(ScreenshotRepository(locked=False), permissions=PermissionSet(["*"]))
loading.resize(1024, 640)
loading.open_for(501, editable=True)
save_state(loading, "loading")
diagnosis_module.run_async = _run_immediately
error = DiagnosisDialog(
FailingScreenshotRepository(locked=False), permissions=PermissionSet(["*"])
)
error.resize(1024, 640)
error.open_for(501, editable=True, seed=_detail(locked=False))
save_state(error, "error")
empty = DiagnosisDialog(
EmptyScreenshotRepository(locked=False), permissions=PermissionSet(["*"])
)
empty.resize(1024, 640)
empty.open_for(501, editable=True)
empty_index = next(
index
for index in range(empty.tabs.count())
if empty.tabs.tabBar().tabData(index) == "prescription"
)
empty.tabs.setCurrentIndex(empty_index)
save_state(empty, "empty")
permission = DiagnosisDialog(
ScreenshotRepository(locked=True),
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]),
)
permission.resize(1024, 640)
permission.open_view_only(501)
save_state(permission, "permission")
daily = DiagnosisDialog(ScreenshotRepository(locked=False), permissions=PermissionSet(["*"]))
daily.resize(1024, 640)
daily.open_for(501, editable=True)
daily_index = next(
index
for index in range(daily.tabs.count())
if daily.tabs.tabBar().tabData(index) == "daily"
)
daily.tabs.setCurrentIndex(daily_index)
save_state(daily, "daily", close=False)
daily_page = daily._tab_pages["daily"]
daily_page.verticalScrollBar().setValue(daily_page.verticalScrollBar().maximum())
save_state(daily, "daily_lower")
focus = DiagnosisDialog(ScreenshotRepository(locked=False), permissions=PermissionSet(["*"]))
focus.resize(1024, 640)
focus.open_for(501, editable=True)
focus.edit_fields["patient_name"].setFocus()
save_state(focus, "focus")
save_states = DiagnosisDialog(
ScreenshotRepository(locked=False), permissions=PermissionSet(["*"])
)
save_states.resize(1024, 640)
save_states.open_for(501, editable=True)
save_states.save_button.set_state("loading")
save_state(save_states, "save_loading", close=False)
save_states.save_button.set_state("success")
save_state(save_states, "save_success", close=False)
save_states.save_button.set_state("error")
save_state(save_states, "save_failure")
for tab_key, state_name in (
("notes", "notes_actions"),
("video", "video_replay"),
("chat", "chat_archive"),
("orders", "order_offset"),
):
dialog = DiagnosisDialog(
ScreenshotRepository(locked=False), permissions=PermissionSet(["*"])
)
dialog.resize(1024, 640)
dialog.open_for(501, editable=True)
index = next(
item
for item in range(dialog.tabs.count())
if dialog.tabs.tabBar().tabData(item) == tab_key
)
dialog.tabs.setCurrentIndex(index)
save_state(dialog, state_name, close=False)
if tab_key == "orders":
order_detail = dialog._build_order_detail_dialog(
dialog.repository.get_prescription_order(801), 801
)
save_widget(order_detail, "order_detail_640x540")
elif tab_key == "video":
video_table = dialog._table_registry["video"][1]
video_table.horizontalScrollBar().setValue(video_table.horizontalScrollBar().maximum())
save_state(dialog, "video_upload_action", close=False)
player = RecordingPlayerDialog(
"https://media.example.invalid/replay.mp4", parent=dialog
)
save_widget(player, "video_player_820x560")
dialog.close()
app.processEvents()
daily_editor = 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_editor.setStyleSheet(diagnosis_module.DIAGNOSIS_QSS)
save_widget(daily_editor, "daily_blood_edit_650x620")
prescription = PrescriptionEditorDialog(
ScreenshotRepository(locked=False),
{
"diagnosis_id": 501,
"appointment_id": 2501,
"patient_name": "林晓岚",
"gender": 0,
"age": 34,
"visit_no": "1K00002501",
"tongue": "舌淡红、苔薄白",
"pulse": "脉细",
"clinical_diagnosis": "气阴两虚",
"doctor_name": "陈医生",
},
mode="add",
current_user={"id": 1, "name": "陈医生"},
)
prescription.setObjectName("DiagnosisPrescriptionEditor")
prescription.setStyleSheet(diagnosis_module.DIAGNOSIS_QSS)
save_widget(prescription, "prescription_editor_920x780")
return rendered
if __name__ == "__main__":
for path in render():
print(path)