更新
This commit is contained in:
@@ -0,0 +1,920 @@
|
||||
"""Patient diagnosis detail/editor used by the scoped patient workspace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPlainTextEdit,
|
||||
QScrollArea,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..widgets import (
|
||||
MessageBanner,
|
||||
display_text,
|
||||
first_value,
|
||||
friendly_error,
|
||||
gender_text,
|
||||
get_value,
|
||||
has_permission,
|
||||
invoke,
|
||||
page_items,
|
||||
page_total,
|
||||
run_async,
|
||||
section_title,
|
||||
)
|
||||
|
||||
_PHONE_PERMISSION = "tcm.diagnosis/phonePlain"
|
||||
_PATIENT_ORDERS_PERMISSION = "tcm.diagnosis/patientOrders"
|
||||
_PHONE_PATTERN = re.compile(r"^1[3-9]\d{9}$")
|
||||
_ID_CARD_PATTERN = re.compile(
|
||||
r"^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$"
|
||||
)
|
||||
|
||||
_DIAGNOSIS_FIELDS = (
|
||||
("患者姓名", "patient_name", "患者姓名"),
|
||||
("手机号", "phone", "11 位手机号"),
|
||||
("身份证号", "id_card", "18 位身份证号(可选)"),
|
||||
("性别", "gender", "1=男,0=女"),
|
||||
("年龄", "age", "患者年龄"),
|
||||
("诊断日期", "diagnosis_date", "YYYY-MM-DD"),
|
||||
("诊断类型", "diagnosis_type", "初诊/复诊类型"),
|
||||
("证型", "syndrome_type", "中医证型"),
|
||||
("糖尿病类型", "diabetes_type", "糖尿病分型"),
|
||||
("糖尿病发现年", "diabetes_discovery_year", "发现年份或病程描述"),
|
||||
("当地医院诊断", "local_hospital_diagnosis", "多项以顿号分隔"),
|
||||
("当地医院", "local_hospital_name", "当地就诊医院"),
|
||||
("婚姻状态", "marital_status", "婚姻状态"),
|
||||
("身高", "height", "cm"),
|
||||
("体重", "weight", "kg"),
|
||||
("地区", "region", "所在地区"),
|
||||
("收缩压", "systolic_pressure", "mmHg"),
|
||||
("舒张压", "diastolic_pressure", "mmHg"),
|
||||
("空腹血糖", "fasting_blood_sugar", "mmol/L"),
|
||||
("主诉", "chief_complaint", "患者此次就诊的主要诉求"),
|
||||
("现病史", "present_illness", "主要症状、持续时间与变化"),
|
||||
("症状", "symptoms", "当前主要症状"),
|
||||
("既往史", "past_history", "既往史;多项可用顿号分隔"),
|
||||
("外伤史", "trauma_history", "0/1 或具体说明"),
|
||||
("手术史", "surgery_history", "0/1 或具体说明"),
|
||||
("过敏史", "allergy_history", "0/1 或具体说明"),
|
||||
("家族史", "family_history", "0/1 或具体说明"),
|
||||
("妊娠史", "pregnancy_history", "0/1 或具体说明"),
|
||||
("食欲", "appetite", "多项以顿号分隔"),
|
||||
("饮水", "water_intake", "饮水情况"),
|
||||
("饮食", "diet_condition", "多项以顿号分隔"),
|
||||
("体重变化", "weight_change", "体重变化"),
|
||||
("身体感觉", "body_feeling", "多项以顿号分隔"),
|
||||
("睡眠", "sleep_condition", "多项以顿号分隔"),
|
||||
("眼部", "eye_condition", "多项以顿号分隔"),
|
||||
("头部感觉", "head_feeling", "多项以顿号分隔"),
|
||||
("出汗", "sweat_condition", "多项以顿号分隔"),
|
||||
("皮肤", "skin_condition", "多项以顿号分隔"),
|
||||
("小便", "urine_condition", "多项以顿号分隔"),
|
||||
("大便", "stool_condition", "多项以顿号分隔"),
|
||||
("肾脏情况", "kidney_condition", "多项以顿号分隔"),
|
||||
("脂肪肝程度", "fatty_liver_degree", "脂肪肝程度"),
|
||||
("舌象", "tongue", "舌质、舌苔等观察"),
|
||||
("舌苔", "tongue_coating", "舌苔记录"),
|
||||
("脉象", "pulse", "脉象记录"),
|
||||
("临床诊断", "clinical_diagnosis", "填写临床诊断"),
|
||||
("治则", "treatment_principle", "治疗原则"),
|
||||
("处方", "prescription", "处方摘要"),
|
||||
("处方意见", "prescription_opinion", "辨证与处方意见"),
|
||||
("医嘱", "doctor_advice", "医生嘱托"),
|
||||
("当前用药", "current_medications", "患者当前用药"),
|
||||
("补充备注", "remark", "其他需要记录的信息"),
|
||||
)
|
||||
|
||||
_LIST_FIELDS = {
|
||||
"local_hospital_diagnosis",
|
||||
"appetite",
|
||||
"diet_condition",
|
||||
"body_feeling",
|
||||
"sleep_condition",
|
||||
"eye_condition",
|
||||
"head_feeling",
|
||||
"sweat_condition",
|
||||
"skin_condition",
|
||||
"urine_condition",
|
||||
"stool_condition",
|
||||
"kidney_condition",
|
||||
}
|
||||
_INTEGER_FIELDS = {
|
||||
"gender",
|
||||
"age",
|
||||
"marital_status",
|
||||
"systolic_pressure",
|
||||
"diastolic_pressure",
|
||||
"trauma_history",
|
||||
"surgery_history",
|
||||
"allergy_history",
|
||||
"family_history",
|
||||
"pregnancy_history",
|
||||
}
|
||||
_FLOAT_FIELDS = {"height", "weight", "fasting_blood_sugar"}
|
||||
_PATIENT_BASIC_FIELDS = {"patient_name", "phone", "id_card", "gender", "age"}
|
||||
|
||||
|
||||
def _mask_phone(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
return f"{text[:3]}****{text[-4:]}" if len(text) == 11 else ("***" if text else "")
|
||||
|
||||
|
||||
def _mask_id_card(value: Any) -> str:
|
||||
text = str(value or "").strip()
|
||||
if len(text) == 18:
|
||||
return f"{text[:6]}********{text[-4:]}"
|
||||
if len(text) == 15:
|
||||
return f"{text[:6]}*****{text[-4:]}"
|
||||
return "***" if text else ""
|
||||
|
||||
|
||||
def _truthy(value: Any) -> bool:
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() not in {"", "0", "false", "no", "off"}
|
||||
return bool(value)
|
||||
|
||||
|
||||
def _editor_text(value: Any) -> str:
|
||||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
||||
return "、".join(str(item) for item in value)
|
||||
return "" if value is None else str(value)
|
||||
|
||||
|
||||
def _duplicate_found(value: Any) -> bool:
|
||||
for key in ("exists", "duplicate", "data.exists", "data.duplicate"):
|
||||
candidate = get_value(value, key, None)
|
||||
if candidate is not None:
|
||||
if isinstance(candidate, str):
|
||||
return candidate.strip().lower() not in {"", "0", "false", "no", "off"}
|
||||
return bool(candidate)
|
||||
return False
|
||||
|
||||
|
||||
def _field_value(key: str, text: str, original: Any) -> Any:
|
||||
value = text.strip()
|
||||
if isinstance(original, Sequence) and not isinstance(original, (str, bytes, bytearray)):
|
||||
return [item.strip() for item in re.split(r"[,,、]", value) if item.strip()]
|
||||
if isinstance(original, bool):
|
||||
return value.lower() not in {"", "0", "false", "no", "off"}
|
||||
if isinstance(original, int) or (original in (None, "") and key in _INTEGER_FIELDS):
|
||||
return _int(value, 0) if value else ""
|
||||
if isinstance(original, float) or (original in (None, "") and key in _FLOAT_FIELDS):
|
||||
try:
|
||||
return float(value) if value else ""
|
||||
except ValueError:
|
||||
return value
|
||||
if key in _LIST_FIELDS:
|
||||
return [item.strip() for item in re.split(r"[,,、]", value) if item.strip()]
|
||||
return value
|
||||
|
||||
|
||||
def _invoke_first(repository: Any, names: Sequence[str], **kwargs: Any) -> Any:
|
||||
"""Call the first repository method present while keeping keyword tolerance."""
|
||||
|
||||
for name in names:
|
||||
if callable(getattr(repository, name, None)):
|
||||
return invoke(repository, name, **kwargs)
|
||||
return invoke(repository, names[0], **kwargs)
|
||||
|
||||
|
||||
def _mapping(value: Any) -> Mapping[str, Any]:
|
||||
return value if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _int(value: Any, default: int = 0) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
class DiagnosisDialog(QDialog):
|
||||
"""Load a diagnosis and its server-backed histories without blocking Qt."""
|
||||
|
||||
saved = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repository: Any,
|
||||
parent: QWidget | None = None,
|
||||
*,
|
||||
permissions: Any = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.repository = repository
|
||||
self.permissions = (
|
||||
permissions
|
||||
if permissions is not None
|
||||
else getattr(parent, "permissions", None)
|
||||
if parent is not None
|
||||
else None
|
||||
)
|
||||
self._can_phone_plain = has_permission(self.permissions, _PHONE_PERMISSION, default=False)
|
||||
self._can_patient_orders = has_permission(
|
||||
self.permissions, _PATIENT_ORDERS_PERMISSION, default=False
|
||||
)
|
||||
self._diagnosis_id = 0
|
||||
self._patient_id = 0
|
||||
self._editable = False
|
||||
self._generation = 0
|
||||
self._save_generation = 0
|
||||
self._orders_generation = 0
|
||||
self._orders_page = 1
|
||||
self._orders_page_size = 10
|
||||
self._orders_total = 0
|
||||
self._detail: Any = None
|
||||
self._field_originals: dict[str, Any] = {}
|
||||
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("患者信息详情")
|
||||
self.setMinimumSize(680, 520)
|
||||
self.resize(880, 680)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(20, 18, 20, 18)
|
||||
root.setSpacing(12)
|
||||
heading = QHBoxLayout()
|
||||
identity = QVBoxLayout()
|
||||
identity.setSpacing(2)
|
||||
self.title_label = QLabel("患者信息详情")
|
||||
self.title_label.setProperty("role", "pageTitle")
|
||||
identity.addWidget(self.title_label)
|
||||
self.meta_label = QLabel("正在准备诊单…")
|
||||
self.meta_label.setProperty("role", "muted")
|
||||
identity.addWidget(self.meta_label)
|
||||
heading.addLayout(identity, 1)
|
||||
self.mode_label = QLabel("只读")
|
||||
self.mode_label.setObjectName("StatusBadge")
|
||||
self.mode_label.setProperty("kind", "neutral")
|
||||
heading.addWidget(self.mode_label, 0, Qt.AlignmentFlag.AlignTop)
|
||||
root.addLayout(heading)
|
||||
|
||||
self.banner = MessageBanner()
|
||||
root.addWidget(self.banner)
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setObjectName("DiagnosisTabs")
|
||||
self.tabs.addTab(self._build_overview_tab(), "患者与病历")
|
||||
self.tabs.addTab(self._build_notes_tab(), "医生备注")
|
||||
self.tabs.addTab(self._build_history_tab("appointment"), "挂号记录")
|
||||
self.tabs.addTab(self._build_history_tab("assign"), "指派记录")
|
||||
if self._can_patient_orders:
|
||||
self.tabs.addTab(self._build_orders_tab(), "患者订单")
|
||||
root.addWidget(self.tabs, 1)
|
||||
|
||||
self.buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
self.save_button = self.buttons.addButton(
|
||||
"保存病历", QDialogButtonBox.ButtonRole.AcceptRole
|
||||
)
|
||||
self.save_button.setProperty("variant", "primary")
|
||||
self.save_button.clicked.connect(self._save)
|
||||
self.buttons.rejected.connect(self.reject)
|
||||
root.addWidget(self.buttons)
|
||||
|
||||
def _build_overview_tab(self) -> QWidget:
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
content = QWidget()
|
||||
layout = QVBoxLayout(content)
|
||||
layout.setContentsMargins(4, 12, 4, 16)
|
||||
layout.setSpacing(12)
|
||||
|
||||
summary = QFrame()
|
||||
summary.setObjectName("SubtleCard")
|
||||
summary_layout = QGridLayout(summary)
|
||||
summary_layout.setContentsMargins(14, 12, 14, 12)
|
||||
summary_layout.setHorizontalSpacing(18)
|
||||
summary_layout.setVerticalSpacing(10)
|
||||
self.summary_fields: dict[str, QLabel] = {}
|
||||
fields = (
|
||||
("患者", "patient"),
|
||||
("手机", "phone"),
|
||||
("身份证", "id_card"),
|
||||
("性别 / 年龄", "gender_age"),
|
||||
("身高 / 体重", "body"),
|
||||
("预约", "appointment"),
|
||||
("医生 / 医助", "staff"),
|
||||
("血压", "pressure"),
|
||||
("空腹血糖", "blood_sugar"),
|
||||
)
|
||||
for index, (caption, key) in enumerate(fields):
|
||||
row, column = divmod(index, 2)
|
||||
box = QVBoxLayout()
|
||||
label = QLabel(caption)
|
||||
label.setProperty("role", "muted")
|
||||
value = QLabel("—")
|
||||
value.setWordWrap(True)
|
||||
value.setStyleSheet("font-weight:600;")
|
||||
box.addWidget(label)
|
||||
box.addWidget(value)
|
||||
summary_layout.addLayout(box, row, column)
|
||||
self.summary_fields[key] = value
|
||||
layout.addWidget(summary)
|
||||
|
||||
layout.addWidget(section_title("病历内容"))
|
||||
form_host = QFrame()
|
||||
form_host.setObjectName("SubtleCard")
|
||||
form = QFormLayout(form_host)
|
||||
form.setContentsMargins(14, 12, 14, 12)
|
||||
form.setHorizontalSpacing(16)
|
||||
form.setVerticalSpacing(10)
|
||||
self.edit_fields: dict[str, QPlainTextEdit] = {}
|
||||
for caption, key, placeholder in _DIAGNOSIS_FIELDS:
|
||||
edit = QPlainTextEdit()
|
||||
edit.setPlaceholderText(placeholder)
|
||||
edit.setMinimumHeight(46)
|
||||
edit.setMaximumHeight(82)
|
||||
form.addRow(caption, edit)
|
||||
self.edit_fields[key] = edit
|
||||
layout.addWidget(form_host)
|
||||
layout.addStretch(1)
|
||||
scroll.setWidget(content)
|
||||
return scroll
|
||||
|
||||
def _build_notes_tab(self) -> QWidget:
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(4, 12, 4, 12)
|
||||
self.notes_table = QTableWidget(0, 3)
|
||||
self.notes_table.setHorizontalHeaderLabels(["时间", "医生", "内容"])
|
||||
self.notes_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.notes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.notes_table.verticalHeader().setVisible(False)
|
||||
self.notes_table.horizontalHeader().setStretchLastSection(True)
|
||||
self.notes_table.setColumnWidth(0, 150)
|
||||
self.notes_table.setColumnWidth(1, 110)
|
||||
layout.addWidget(self.notes_table)
|
||||
return widget
|
||||
|
||||
def _build_history_tab(self, kind: str) -> QWidget:
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(4, 12, 4, 12)
|
||||
if kind == "appointment":
|
||||
table = QTableWidget(0, 7)
|
||||
table.setHorizontalHeaderLabels(
|
||||
["状态", "患者", "医生", "医助", "预约日期", "时段", "备注"]
|
||||
)
|
||||
widths = (88, 110, 100, 100, 105, 80)
|
||||
self.appointment_table = table
|
||||
else:
|
||||
table = QTableWidget(0, 6)
|
||||
table.setHorizontalHeaderLabels(
|
||||
["操作时间", "原医助", "新医助", "继承", "操作人", "账号"]
|
||||
)
|
||||
widths = (145, 100, 100, 70, 100)
|
||||
self.assign_table = table
|
||||
table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
for index, width in enumerate(widths):
|
||||
table.setColumnWidth(index, width)
|
||||
layout.addWidget(table)
|
||||
return widget
|
||||
|
||||
def _build_orders_tab(self) -> QWidget:
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(4, 12, 4, 12)
|
||||
self.orders_table = QTableWidget(0, 8)
|
||||
self.orders_table.setHorizontalHeaderLabels(
|
||||
["订单号", "处方", "患者/收货人", "手机", "金额", "发货", "状态", "创建时间"]
|
||||
)
|
||||
self.orders_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.orders_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.orders_table.verticalHeader().setVisible(False)
|
||||
self.orders_table.horizontalHeader().setStretchLastSection(True)
|
||||
for index, width in enumerate((130, 70, 120, 110, 90, 90, 100)):
|
||||
self.orders_table.setColumnWidth(index, width)
|
||||
layout.addWidget(self.orders_table, 1)
|
||||
footer = QHBoxLayout()
|
||||
self.orders_summary = QLabel("共 0 条")
|
||||
self.orders_summary.setProperty("role", "muted")
|
||||
footer.addWidget(self.orders_summary)
|
||||
footer.addStretch(1)
|
||||
self.orders_previous = QLabel('<a href="prev">上一页</a>')
|
||||
self.orders_previous.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.orders_previous.linkActivated.connect(lambda _link: self._change_orders_page(-1))
|
||||
footer.addWidget(self.orders_previous)
|
||||
self.orders_page_label = QLabel("1 / 1")
|
||||
footer.addWidget(self.orders_page_label)
|
||||
self.orders_next = QLabel('<a href="next">下一页</a>')
|
||||
self.orders_next.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.orders_next.linkActivated.connect(lambda _link: self._change_orders_page(1))
|
||||
footer.addWidget(self.orders_next)
|
||||
layout.addLayout(footer)
|
||||
return widget
|
||||
|
||||
def open_for(self, diagnosis_id: int, *, editable: bool = False, seed: Any = None) -> None:
|
||||
"""Open immediately, then replace the seed with authoritative server data."""
|
||||
|
||||
self._diagnosis_id = int(diagnosis_id)
|
||||
self._patient_id = 0
|
||||
self._editable = bool(
|
||||
editable and has_permission(self.permissions, "tcm.diagnosis/edit", default=True)
|
||||
)
|
||||
self._generation += 1
|
||||
self._save_generation += 1
|
||||
self._orders_generation += 1
|
||||
self._orders_page = 1
|
||||
self._orders_total = 0
|
||||
generation = self._generation
|
||||
self._detail = seed
|
||||
self.setWindowTitle("编辑患者病历" if self._editable else "患者信息详情")
|
||||
self.title_label.setText("编辑患者病历" if self._editable else "患者信息详情")
|
||||
self.mode_label.setText("可编辑" if self._editable else "只读")
|
||||
self.mode_label.setProperty("kind", "success" if self._editable else "neutral")
|
||||
self.mode_label.style().unpolish(self.mode_label)
|
||||
self.mode_label.style().polish(self.mode_label)
|
||||
self.save_button.setVisible(self._editable)
|
||||
for key, field in self.edit_fields.items():
|
||||
field.setReadOnly(
|
||||
not self._editable or (key in {"phone", "id_card"} and not self._can_phone_plain)
|
||||
)
|
||||
self._clear_tables()
|
||||
if seed is not None:
|
||||
self._render(seed, [], [])
|
||||
self.banner.show_message("正在加载完整诊单与历史记录…", "info")
|
||||
self.open()
|
||||
diagnosis_id_snapshot = self._diagnosis_id
|
||||
editable_snapshot = self._editable
|
||||
run_async(
|
||||
lambda: self._load_bundle(diagnosis_id_snapshot, editable_snapshot),
|
||||
on_success=lambda result: self._apply_bundle(result, generation),
|
||||
on_error=lambda error: self._load_error(error, generation),
|
||||
on_finished=lambda: None,
|
||||
)
|
||||
|
||||
def _load_bundle(self, diagnosis_id: int, editable: bool) -> dict[str, Any]:
|
||||
detail_names = (
|
||||
("get_diagnosis_detail", "patient_detail", "diagnosis_readonly_detail")
|
||||
if editable
|
||||
else ("patient_detail", "diagnosis_readonly_detail", "get_diagnosis_detail")
|
||||
)
|
||||
detail = _invoke_first(
|
||||
self.repository,
|
||||
detail_names,
|
||||
diagnosis_id=diagnosis_id,
|
||||
id=diagnosis_id,
|
||||
readonly=not editable,
|
||||
)
|
||||
appointments: list[Any] = []
|
||||
assignments: list[Any] = []
|
||||
history_errors: list[str] = []
|
||||
try:
|
||||
appointments = page_items(
|
||||
_invoke_first(
|
||||
self.repository,
|
||||
("appointment_history",),
|
||||
diagnosis_id=diagnosis_id,
|
||||
page_no=1,
|
||||
page_size=500,
|
||||
)
|
||||
)
|
||||
except Exception as error: # history failure must not hide the diagnosis
|
||||
history_errors.append(f"挂号记录:{friendly_error(error)}")
|
||||
try:
|
||||
assignments = page_items(
|
||||
_invoke_first(
|
||||
self.repository,
|
||||
("assign_history",),
|
||||
diagnosis_id=diagnosis_id,
|
||||
page_no=1,
|
||||
page_size=50,
|
||||
)
|
||||
)
|
||||
except Exception as error: # history failure must not hide the diagnosis
|
||||
history_errors.append(f"指派记录:{friendly_error(error)}")
|
||||
orders: list[Any] = []
|
||||
orders_total = 0
|
||||
diagnosis = get_value(detail, "diagnosis", None) or detail or {}
|
||||
patient = get_value(detail, "patient", None) or {}
|
||||
patient_id = _int(
|
||||
first_value(
|
||||
diagnosis,
|
||||
"patient_id",
|
||||
"source_patient_id",
|
||||
default=first_value(patient, "patient_id", "id", default=0),
|
||||
),
|
||||
0,
|
||||
)
|
||||
if self._can_patient_orders:
|
||||
try:
|
||||
order_result = self._query_orders(diagnosis_id, patient_id, 1)
|
||||
orders = page_items(order_result)
|
||||
orders_total = page_total(order_result, len(orders))
|
||||
except Exception as error: # order failure must not hide the diagnosis
|
||||
history_errors.append(f"患者订单:{friendly_error(error)}")
|
||||
return {
|
||||
"detail": detail,
|
||||
"appointments": appointments,
|
||||
"assignments": assignments,
|
||||
"patient_id": patient_id,
|
||||
"orders": orders,
|
||||
"orders_total": orders_total,
|
||||
"history_errors": history_errors,
|
||||
}
|
||||
|
||||
def _query_orders(self, diagnosis_id: int, patient_id: int, page: int) -> Any:
|
||||
filters: dict[str, Any] = {
|
||||
"context_diagnosis_id": diagnosis_id,
|
||||
"scene": "diagnosis_edit",
|
||||
}
|
||||
if patient_id > 0:
|
||||
filters["patient_id"] = patient_id
|
||||
return _invoke_first(
|
||||
self.repository,
|
||||
("list_prescription_orders",),
|
||||
page_no=page,
|
||||
page_size=self._orders_page_size,
|
||||
**filters,
|
||||
)
|
||||
|
||||
def _apply_bundle(self, result: Any, generation: int) -> None:
|
||||
if generation != self._generation:
|
||||
return
|
||||
detail = get_value(result, "detail", None)
|
||||
appointments = get_value(result, "appointments", []) or []
|
||||
assignments = get_value(result, "assignments", []) or []
|
||||
self._detail = detail
|
||||
self._patient_id = _int(get_value(result, "patient_id", 0), 0)
|
||||
self._render(detail, appointments, assignments)
|
||||
if self._can_patient_orders:
|
||||
self._orders_total = int(get_value(result, "orders_total", 0) or 0)
|
||||
self._fill_orders(get_value(result, "orders", []) or [])
|
||||
self._update_orders_pager()
|
||||
errors = get_value(result, "history_errors", []) or []
|
||||
if errors:
|
||||
self.banner.show_message(";".join(str(item) for item in errors), "warning")
|
||||
else:
|
||||
self.banner.clear()
|
||||
|
||||
def _load_error(self, error: Exception, generation: int) -> None:
|
||||
if generation == self._generation:
|
||||
self.banner.show_message(friendly_error(error), "danger")
|
||||
|
||||
def _render(self, detail: Any, appointments: Sequence[Any], assignments: Sequence[Any]) -> None:
|
||||
diagnosis = get_value(detail, "diagnosis", None) or detail or {}
|
||||
patient = get_value(detail, "patient", None) or {}
|
||||
appointment = get_value(detail, "appointment", None) or {}
|
||||
patient_name = first_value(
|
||||
diagnosis,
|
||||
"patient_name",
|
||||
"name",
|
||||
default=first_value(patient, "patient_name", "name", default="未命名患者"),
|
||||
)
|
||||
gender = gender_text(
|
||||
first_value(
|
||||
diagnosis,
|
||||
"gender_desc",
|
||||
"gender",
|
||||
default=first_value(patient, "gender_desc", "gender"),
|
||||
)
|
||||
)
|
||||
age = display_text(first_value(diagnosis, "age", default=first_value(patient, "age")))
|
||||
self.meta_label.setText(f"诊单 #{self._diagnosis_id} · {patient_name} · {gender} · {age}岁")
|
||||
self.summary_fields["patient"].setText(display_text(patient_name))
|
||||
self.summary_fields["phone"].setText(
|
||||
display_text(
|
||||
first_value(
|
||||
diagnosis,
|
||||
"phone",
|
||||
"patient_phone",
|
||||
default=first_value(
|
||||
patient,
|
||||
"phone",
|
||||
"patient_phone",
|
||||
default=first_value(appointment, "patient_phone"),
|
||||
),
|
||||
)
|
||||
if self._can_phone_plain
|
||||
else _mask_phone(
|
||||
first_value(
|
||||
diagnosis,
|
||||
"phone",
|
||||
"patient_phone",
|
||||
default=first_value(
|
||||
patient,
|
||||
"phone",
|
||||
"patient_phone",
|
||||
default=first_value(appointment, "patient_phone"),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
id_card = first_value(
|
||||
diagnosis,
|
||||
"id_card",
|
||||
default=first_value(patient, "id_card", default=""),
|
||||
)
|
||||
self.summary_fields["id_card"].setText(
|
||||
display_text(id_card if self._can_phone_plain else _mask_id_card(id_card))
|
||||
)
|
||||
self.summary_fields["gender_age"].setText(f"{gender} / {age}岁")
|
||||
self.summary_fields["body"].setText(
|
||||
f"{display_text(first_value(diagnosis, 'height', default=first_value(patient, 'height')))} cm / "
|
||||
f"{display_text(first_value(diagnosis, 'weight', default=first_value(patient, 'weight')))} kg"
|
||||
)
|
||||
appointment_text = " ".join(
|
||||
part
|
||||
for part in (
|
||||
display_text(first_value(appointment, "appointment_date"), ""),
|
||||
display_text(
|
||||
first_value(appointment, "appointment_time", "appointment_time_text"), ""
|
||||
),
|
||||
)
|
||||
if part
|
||||
)
|
||||
self.summary_fields["appointment"].setText(appointment_text or "暂无预约")
|
||||
self.summary_fields["staff"].setText(
|
||||
f"{display_text(first_value(appointment, 'doctor_name'))} / "
|
||||
f"{display_text(first_value(appointment, 'assistant_name'))}"
|
||||
)
|
||||
self.summary_fields["pressure"].setText(
|
||||
f"{display_text(first_value(diagnosis, 'systolic_pressure'))} / "
|
||||
f"{display_text(first_value(diagnosis, 'diastolic_pressure'))} mmHg"
|
||||
)
|
||||
self.summary_fields["blood_sugar"].setText(
|
||||
f"{display_text(first_value(diagnosis, 'fasting_blood_sugar'))} mmol/L"
|
||||
)
|
||||
basic_locked = _truthy(
|
||||
first_value(diagnosis, "patient_basic_locked", default=False)
|
||||
) or not _truthy(first_value(diagnosis, "can_edit_patient_basic", default=True))
|
||||
self._field_originals.clear()
|
||||
for key, field in self.edit_fields.items():
|
||||
raw = first_value(
|
||||
diagnosis,
|
||||
key,
|
||||
default=first_value(
|
||||
patient,
|
||||
key,
|
||||
default=first_value(
|
||||
diagnosis,
|
||||
"patient_phone" if key == "phone" else key,
|
||||
default="",
|
||||
),
|
||||
),
|
||||
)
|
||||
self._field_originals[key] = raw
|
||||
if key == "phone" and not self._can_phone_plain:
|
||||
rendered = _mask_phone(raw)
|
||||
elif key == "id_card" and not self._can_phone_plain:
|
||||
rendered = _mask_id_card(raw)
|
||||
else:
|
||||
rendered = _editor_text(raw)
|
||||
field.setPlainText(rendered)
|
||||
field.setReadOnly(
|
||||
not self._editable
|
||||
or (key in _PATIENT_BASIC_FIELDS and basic_locked)
|
||||
or (key in {"phone", "id_card"} and not self._can_phone_plain)
|
||||
)
|
||||
|
||||
notes = (
|
||||
get_value(detail, "doctor_notes", None)
|
||||
or get_value(diagnosis, "doctor_notes", None)
|
||||
or []
|
||||
)
|
||||
if not isinstance(notes, (list, tuple)):
|
||||
notes = [notes]
|
||||
self._fill_notes(notes)
|
||||
self._fill_appointments(appointments)
|
||||
self._fill_assignments(assignments)
|
||||
|
||||
def _clear_tables(self) -> None:
|
||||
self.notes_table.setRowCount(0)
|
||||
self.appointment_table.setRowCount(0)
|
||||
self.assign_table.setRowCount(0)
|
||||
if self._can_patient_orders:
|
||||
self.orders_table.setRowCount(0)
|
||||
self.orders_summary.setText("共 0 条")
|
||||
self.orders_page_label.setText("1 / 1")
|
||||
|
||||
@staticmethod
|
||||
def _set_row(table: QTableWidget, row: int, values: Sequence[Any]) -> None:
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(display_text(value))
|
||||
item.setToolTip(item.text() if len(item.text()) > 18 else "")
|
||||
table.setItem(row, column, item)
|
||||
|
||||
def _fill_notes(self, rows: Sequence[Any]) -> None:
|
||||
self.notes_table.setRowCount(len(rows))
|
||||
for index, row in enumerate(rows):
|
||||
self._set_row(
|
||||
self.notes_table,
|
||||
index,
|
||||
(
|
||||
first_value(row, "create_time", "created_at", "time"),
|
||||
first_value(row, "doctor_name", "creator_name", default="医生"),
|
||||
first_value(row, "content", "note", "remark"),
|
||||
),
|
||||
)
|
||||
|
||||
def _fill_appointments(self, rows: Sequence[Any]) -> None:
|
||||
self.appointment_table.setRowCount(len(rows))
|
||||
for index, row in enumerate(rows):
|
||||
self._set_row(
|
||||
self.appointment_table,
|
||||
index,
|
||||
(
|
||||
first_value(row, "status_desc", "status_text", "status"),
|
||||
first_value(row, "patient_name"),
|
||||
first_value(row, "doctor_name"),
|
||||
first_value(row, "assistant_name"),
|
||||
first_value(row, "appointment_date"),
|
||||
first_value(row, "appointment_time", "period"),
|
||||
first_value(row, "remark"),
|
||||
),
|
||||
)
|
||||
|
||||
def _fill_assignments(self, rows: Sequence[Any]) -> None:
|
||||
self.assign_table.setRowCount(len(rows))
|
||||
for index, row in enumerate(rows):
|
||||
self._set_row(
|
||||
self.assign_table,
|
||||
index,
|
||||
(
|
||||
first_value(row, "create_time_text", "create_time"),
|
||||
first_value(row, "from_assistant_name", default="—"),
|
||||
first_value(row, "to_assistant_name", "assistant_name", default="—"),
|
||||
"是" if bool(first_value(row, "is_inherit", default=False)) else "否",
|
||||
first_value(row, "operator_name"),
|
||||
first_value(row, "operator_account"),
|
||||
),
|
||||
)
|
||||
|
||||
def _fill_orders(self, rows: Sequence[Any]) -> None:
|
||||
self.orders_table.setRowCount(len(rows))
|
||||
for index, row in enumerate(rows):
|
||||
self._set_row(
|
||||
self.orders_table,
|
||||
index,
|
||||
(
|
||||
first_value(row, "order_no", "sn", "id"),
|
||||
first_value(row, "prescription_id"),
|
||||
first_value(row, "recipient_name", "patient_name"),
|
||||
first_value(row, "recipient_phone", "phone"),
|
||||
first_value(row, "amount"),
|
||||
first_value(row, "ship_mode", "express_company"),
|
||||
first_value(row, "status_text", "fulfillment_status_text", "status"),
|
||||
first_value(row, "create_time", "created_at"),
|
||||
),
|
||||
)
|
||||
|
||||
def _update_orders_pager(self) -> None:
|
||||
pages = max(1, (self._orders_total + self._orders_page_size - 1) // self._orders_page_size)
|
||||
self.orders_summary.setText(f"共 {self._orders_total} 条")
|
||||
self.orders_page_label.setText(f"{self._orders_page} / {pages}")
|
||||
self.orders_previous.setEnabled(self._orders_page > 1)
|
||||
self.orders_next.setEnabled(self._orders_page < pages)
|
||||
|
||||
def _change_orders_page(self, offset: int) -> None:
|
||||
if not self._can_patient_orders or self._diagnosis_id <= 0:
|
||||
return
|
||||
pages = max(1, (self._orders_total + self._orders_page_size - 1) // self._orders_page_size)
|
||||
target_page = self._orders_page + offset
|
||||
if target_page < 1 or target_page > pages:
|
||||
return
|
||||
self._orders_generation += 1
|
||||
generation = self._orders_generation
|
||||
diagnosis_id = self._diagnosis_id
|
||||
patient_id = self._patient_id
|
||||
self.banner.show_message("正在加载患者订单…", "info")
|
||||
run_async(
|
||||
lambda: self._query_orders(diagnosis_id, patient_id, target_page),
|
||||
on_success=lambda result: self._apply_orders_page(
|
||||
result, diagnosis_id, target_page, generation
|
||||
),
|
||||
on_error=lambda error: self._orders_error(error, diagnosis_id, generation),
|
||||
)
|
||||
|
||||
def _apply_orders_page(
|
||||
self,
|
||||
result: Any,
|
||||
diagnosis_id: int,
|
||||
page: int,
|
||||
generation: int,
|
||||
) -> None:
|
||||
if generation != self._orders_generation or diagnosis_id != self._diagnosis_id:
|
||||
return
|
||||
rows = page_items(result)
|
||||
self._orders_page = page
|
||||
self._orders_total = page_total(result, len(rows))
|
||||
self._fill_orders(rows)
|
||||
self._update_orders_pager()
|
||||
self.banner.clear()
|
||||
|
||||
def _orders_error(self, error: Exception, diagnosis_id: int, generation: int) -> None:
|
||||
if generation == self._orders_generation and diagnosis_id == self._diagnosis_id:
|
||||
self.banner.show_message(friendly_error(error), "danger")
|
||||
|
||||
def _save(self) -> None:
|
||||
if not self._editable or self._diagnosis_id <= 0:
|
||||
return
|
||||
diagnosis_id = self._diagnosis_id
|
||||
changes = {
|
||||
key: _field_value(key, field.toPlainText(), self._field_originals.get(key))
|
||||
for key, field in self.edit_fields.items()
|
||||
if key not in {"phone", "id_card"} or self._can_phone_plain
|
||||
}
|
||||
patient_name = str(changes.get("patient_name") or "").strip()
|
||||
if not patient_name:
|
||||
self.banner.show_message("请输入患者姓名。", "warning")
|
||||
self.edit_fields["patient_name"].setFocus()
|
||||
return
|
||||
phone = str(changes.get("phone") or "").strip()
|
||||
if self._can_phone_plain and not _PHONE_PATTERN.fullmatch(phone):
|
||||
self.banner.show_message("手机号格式不正确。", "warning")
|
||||
self.edit_fields["phone"].setFocus()
|
||||
return
|
||||
id_card = str(changes.get("id_card") or "").strip()
|
||||
if self._can_phone_plain and id_card and not _ID_CARD_PATTERN.fullmatch(id_card):
|
||||
self.banner.show_message("身份证号格式不正确。", "warning")
|
||||
self.edit_fields["id_card"].setFocus()
|
||||
return
|
||||
self._save_generation += 1
|
||||
generation = self._save_generation
|
||||
self.save_button.setEnabled(False)
|
||||
self.banner.show_message("正在保存病历…", "info")
|
||||
run_async(
|
||||
lambda: self._validate_and_save(diagnosis_id, changes),
|
||||
on_success=lambda _result: self._save_success(generation),
|
||||
on_error=lambda error: self._save_error(error, generation),
|
||||
on_finished=lambda: self._save_finished(generation),
|
||||
)
|
||||
|
||||
def _validate_and_save(self, diagnosis_id: int, changes: Mapping[str, Any]) -> Any:
|
||||
if self._can_phone_plain:
|
||||
phone = str(changes.get("phone") or "").strip()
|
||||
phone_result = _invoke_first(
|
||||
self.repository,
|
||||
("check_diagnosis_phone",),
|
||||
payload={"phone": phone, "id": diagnosis_id},
|
||||
)
|
||||
if _duplicate_found(phone_result):
|
||||
raise ValueError(
|
||||
str(
|
||||
first_value(
|
||||
phone_result, "message", "data.message", default="手机号已存在。"
|
||||
)
|
||||
)
|
||||
)
|
||||
id_card = str(changes.get("id_card") or "").strip()
|
||||
if id_card:
|
||||
id_card_result = _invoke_first(
|
||||
self.repository,
|
||||
("check_diagnosis_id_card",),
|
||||
payload={"id_card": id_card, "id": diagnosis_id},
|
||||
)
|
||||
if _duplicate_found(id_card_result):
|
||||
raise ValueError(
|
||||
str(
|
||||
first_value(
|
||||
id_card_result,
|
||||
"message",
|
||||
"data.message",
|
||||
default="身份证号已存在。",
|
||||
)
|
||||
)
|
||||
)
|
||||
return _invoke_first(
|
||||
self.repository,
|
||||
("update_diagnosis",),
|
||||
diagnosis=diagnosis_id,
|
||||
changes=dict(changes),
|
||||
)
|
||||
|
||||
def _save_success(self, generation: int) -> None:
|
||||
if generation != self._save_generation:
|
||||
return
|
||||
self.banner.show_message("病历已保存。", "success")
|
||||
self.saved.emit()
|
||||
|
||||
def _save_error(self, error: Exception, generation: int) -> None:
|
||||
if generation == self._save_generation:
|
||||
self.banner.show_message(friendly_error(error), "danger")
|
||||
|
||||
def _save_finished(self, generation: int) -> None:
|
||||
if generation == self._save_generation:
|
||||
self.save_button.setEnabled(True)
|
||||
|
||||
|
||||
__all__ = ["DiagnosisDialog"]
|
||||
Reference in New Issue
Block a user