This commit is contained in:
Your Name
2026-08-10 17:29:05 +08:00
parent 2199887c07
commit 9add23e019
129 changed files with 34157 additions and 59 deletions
@@ -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"]