This commit is contained in:
Your Name
2026-08-11 17:39:41 +08:00
parent cfe4c82c90
commit 25467b9d91
350 changed files with 201115 additions and 132208 deletions
+352 -128
View File
@@ -8,14 +8,13 @@ from contextlib import suppress
from typing import Any
from PySide6.QtCore import QDate, QDateTime, QEvent, QPoint, QRect, Qt, QTimer, QUrl, Signal
from PySide6.QtGui import QColor, QDesktopServices
from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QAbstractItemView,
QBoxLayout,
QDialog,
QFileDialog,
QFrame,
QGraphicsDropShadowEffect,
QGridLayout,
QHBoxLayout,
QInputDialog,
@@ -173,7 +172,8 @@ _INTEGER_FIELDS = {
"status",
"show_card",
}
_FLOAT_FIELDS = {"height", "weight", "fasting_blood_sugar"}
# fasting_blood_sugar stays free text (supports ranges like "6-9").
_FLOAT_FIELDS = {"height", "weight"}
_PATIENT_BASIC_FIELDS = {"patient_name", "phone", "id_card", "gender", "age"}
_ORDER_OFFSET_HELP = (
@@ -184,75 +184,74 @@ _ORDER_OFFSET_HELP = (
_ORDER_DETAIL_QSS = """
QDialog#DiagnosisOrderDetailOverlay { background: transparent; }
QFrame#DiagnosisOrderDetailScrim { background: rgba(15, 23, 42, 0.32); border: 0; }
QFrame#DiagnosisOrderDetailScrim { background: rgba(8, 11, 20, 0.78); border: 0; }
QFrame#DiagnosisOrderDetailDrawer {
background: #F6F8FB;
background: #F5F7FB;
border: 0;
border-left: 1px solid #DDE3EA;
border-left: 1px solid #D8DEEA;
}
QFrame#DiagnosisOrderDetailHeader {
background: #FFFFFF;
border: 0;
border-bottom: 1px solid #E5EAF0;
border-bottom: 1px solid #D8DEEA;
}
QLabel#DiagnosisOrderDetailTitle { color: #172033; font-size: 19px; font-weight: 650; }
QLabel#DiagnosisOrderDetailMeta { color: #697386; font-size: 12px; }
QLabel#DiagnosisOrderDetailMeta { color: #667085; font-size: 12px; }
QLabel#DiagnosisOrderReadonlyBadge {
color: #526171;
background: #EEF2F6;
border: 1px solid #D8E0E8;
color: #667085;
background: #F7F8FC;
border: 1px solid #D8DEEA;
border-radius: 4px;
padding: 3px 7px;
font-size: 11px;
font-weight: 600;
}
QScrollArea#DiagnosisOrderDetailScroll { border: 0; background: #F6F8FB; }
QScrollArea#DiagnosisOrderDetailScroll > QWidget > QWidget { background: #F6F8FB; }
QScrollArea#DiagnosisOrderDetailScroll { border: 0; background: #F5F7FB; }
QScrollArea#DiagnosisOrderDetailScroll > QWidget > QWidget { background: #F5F7FB; }
QFrame[orderAmountCard="true"] {
background: #FFFFFF;
border: 1px solid #E4E9EF;
border: 1px solid #D8DEEA;
border-radius: 7px;
}
QLabel[orderAmountTitle="true"] { color: #7A8594; font-size: 11px; font-weight: 550; }
QLabel[orderAmountTitle="true"] { color: #667085; font-size: 11px; font-weight: 550; }
QLabel[orderAmountValue="true"] {
color: #172033;
font-size: 18px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
QLabel[orderAmountTone="danger"] { color: #D84A4A; }
QLabel[orderAmountTone="success"] { color: #28845A; }
QLabel[orderAmountTone="warning"] { color: #B46B16; }
QLabel[orderAmountTone="danger"] { color: #C43E55; }
QLabel[orderAmountTone="success"] { color: #16876C; }
QLabel[orderAmountTone="warning"] { color: #9A6813; }
QFrame[orderDetailSection="true"] {
background: #FFFFFF;
border: 1px solid #E2E7ED;
border: 1px solid #D8DEEA;
border-radius: 7px;
}
QLabel[orderSectionTitle="true"] { color: #253047; font-size: 15px; font-weight: 650; }
QLabel[orderSectionHint="true"] { color: #8A94A3; font-size: 11px; }
QLabel[orderSectionTitle="true"] { color: #172033; font-size: 15px; font-weight: 650; }
QLabel[orderSectionHint="true"] { color: #667085; font-size: 11px; }
QFrame[orderField="true"] {
background: #FAFBFC;
border: 1px solid #EDF0F4;
background: #F7F8FC;
border: 1px solid #D8DEEA;
border-radius: 5px;
}
QLabel[orderFieldLabel="true"] { color: #7A8594; font-size: 11px; }
QLabel[orderFieldValue="true"] { color: #263247; font-size: 13px; }
QLabel[orderFieldLabel="true"] { color: #667085; font-size: 11px; }
QLabel[orderFieldValue="true"] { color: #172033; font-size: 13px; }
QLabel[orderEmptyState="true"] {
color: #9099A8;
background: #FAFBFC;
border: 1px dashed #DDE3EA;
color: #667085;
background: #F7F8FC;
border: 1px dashed #D8DEEA;
border-radius: 5px;
padding: 18px 12px;
font-size: 12px;
}
QFrame#DiagnosisOrderTimelineItem { border: 0; border-left: 2px solid #D8E0E8; }
QLabel#DiagnosisOrderTimelineTime { color: #7A8594; font-size: 11px; }
QLabel#DiagnosisOrderTimelineTitle { color: #253047; font-size: 12px; font-weight: 600; }
QLabel#DiagnosisOrderTimelineBody { color: #5E6979; font-size: 12px; }
QFrame#DiagnosisOrderTimelineItem { border: 0; border-left: 2px solid #D8DEEA; }
QLabel#DiagnosisOrderTimelineTime { color: #667085; font-size: 11px; }
QLabel#DiagnosisOrderTimelineTitle { color: #172033; font-size: 12px; font-weight: 600; }
QLabel#DiagnosisOrderTimelineBody { color: #667085; font-size: 12px; }
QFrame#DiagnosisOrderDetailFooter {
background: #FFFFFF;
border: 0;
border-top: 1px solid #E5EAF0;
border-top: 1px solid #D8DEEA;
}
"""
@@ -265,34 +264,38 @@ _FORM_SECTIONS: tuple[tuple[str, tuple[tuple[tuple[str, str, int, str], ...], ..
("姓名", "patient_name", 12, "line"),
("身份证号", "id_card", 12, "line"),
),
(("手机号", "phone", 24, "line"),),
(
("手机号", "phone", 12, "line"),
("性别", "gender", 12, "gender"),
("年龄", "age", 12, "age_number"),
),
(("年龄", "age", 12, "age_number"),),
),
),
(
"生命体征",
(
(
("婚姻", "marital_status", 8, "marital"),
("身高", "height", 8, "height_number"),
("体重", "weight", 8, "weight_number"),
("婚姻", "marital_status", 12, "marital"),
("身高", "height", 12, "height_number"),
),
(
("地区", "region", 8, "line"),
("高压", "systolic_pressure", 8, "systolic_number"),
("低压", "diastolic_pressure", 8, "diastolic_number"),
("体重", "weight", 12, "weight_number"),
("地区", "region", 12, "line"),
),
(
("高压", "systolic_pressure", 12, "systolic_number"),
("低压", "diastolic_pressure", 12, "diastolic_number"),
),
(
("空腹血糖", "fasting_blood_sugar", 12, "line"),
("诊断类型", "diagnosis_type", 12, "diagnosis_type"),
),
(("空腹血糖", "fasting_blood_sugar", 8, "line"),),
(("诊断类型", "diagnosis_type", 12, "diagnosis_type"),),
(
("状态", "status", 12, "status"),
("渠道", "create_source", 12, "create_source"),
),
(("统计端就诊卡", "show_card", 12, "show_card"),),
(("在用药物", "current_medications", 12, "textarea3"),),
(("在用药物", "current_medications", 24, "textarea3"),),
),
),
(
@@ -311,18 +314,18 @@ _FORM_SECTIONS: tuple[tuple[str, tuple[tuple[tuple[str, str, int, str], ...], ..
(
(("口腔感觉", "appetite", 24, "appetite_choices"),),
(("每日饮水量", "water_intake", 24, "water_intake_choices"),),
(("近月体重变化", "weight_change", 24, "weight_change_choices"),),
(("脂肪肝", "fatty_liver_degree", 24, "fatty_liver_degree_choices"),),
(("饮食", "diet_condition", 24, "diet_condition_choices"),),
(("肢体", "body_feeling", 24, "body_feeling_choices"),),
(("睡眠", "sleep_condition", 24, "sleep_condition_choices"),),
(("眼睛", "eye_condition", 24, "eye_condition_choices"),),
(("头部", "head_feeling", 24, "head_feeling_choices"),),
(("出汗", "sweat_condition", 24, "sweat_condition_choices"),),
(("皮肤", "skin_condition", 24, "skin_condition_choices"),),
(("小便", "urine_condition", 24, "urine_condition_choices"),),
(("大便", "stool_condition", 24, "stool_condition_choices"),),
(("腰肾", "kidney_condition", 24, "kidney_condition_choices"),),
(("近一个月体重变化", "weight_change", 24, "weight_change_choices"),),
(("脂肪肝程度", "fatty_liver_degree", 24, "fatty_liver_degree_choices"),),
(("饮食情况", "diet_condition", 24, "diet_condition_choices"),),
(("肢体感觉", "body_feeling", 24, "body_feeling_choices"),),
(("睡眠情况", "sleep_condition", 24, "sleep_condition_choices"),),
(("眼睛情况", "eye_condition", 24, "eye_condition_choices"),),
(("头部感觉", "head_feeling", 24, "head_feeling_choices"),),
(("出汗情况", "sweat_condition", 24, "sweat_condition_choices"),),
(("皮肤情况", "skin_condition", 24, "skin_condition_choices"),),
(("小便情况", "urine_condition", 24, "urine_condition_choices"),),
(("大便情况", "stool_condition", 24, "stool_condition_choices"),),
(("腰肾情况", "kidney_condition", 24, "kidney_condition_choices"),),
(("其他补充", "symptoms", 24, "textarea3"),),
),
),
@@ -331,14 +334,14 @@ _FORM_SECTIONS: tuple[tuple[str, tuple[tuple[tuple[str, str, int, str], ...], ..
"其他病史",
(
(
("外伤", "trauma_history", 8, "boolean"),
("手术", "surgery_history", 8, "boolean"),
("过敏", "allergy_history", 8, "boolean"),
("外伤史", "trauma_history", 12, "boolean"),
("手术史", "surgery_history", 12, "boolean"),
),
(
("家族", "family_history", 12, "boolean"),
("妊娠", "pregnancy_history", 12, "boolean"),
("过敏史", "allergy_history", 12, "boolean"),
("家族史", "family_history", 12, "boolean"),
),
(("妊娠史", "pregnancy_history", 12, "boolean"),),
),
),
("诊断信息", ((("病史补充", "remark", 24, "textarea2"),),)),
@@ -710,7 +713,8 @@ class OrderDetailDrawer(QDialog):
self.drawer_panel.setFixedWidth(max(1, width))
def eventFilter(self, watched: object, event: QEvent) -> bool: # noqa: N802 - Qt API
if watched is self._owner and event.type() in {
owner = getattr(self, "_owner", None)
if watched is owner and event.type() in {
QEvent.Type.Move,
QEvent.Type.Resize,
QEvent.Type.Show,
@@ -734,9 +738,9 @@ class DiagnosisDialog(QDialog):
"""Diagnosis readonly page plus edit/view-only right drawer.
The public ``open_for`` contract and all repository calls remain compatible
with the earlier dialog. ``editable=False`` opens the standalone readonly
card flow; callers that explicitly need the admin ``viewOnly`` drawer can
pass ``view_only=True`` or call :meth:`open_view_only`.
with the earlier dialog. ``editable=False`` alone keeps the standalone
readonly card flow; list/detail “查看” should call :meth:`open_view_only`
so it matches admin ``openViewOnly`` (same 病历 form, chips stay selected).
"""
saved = Signal()
@@ -962,8 +966,8 @@ class DiagnosisDialog(QDialog):
back.setCursor(Qt.CursorShape.PointingHandCursor)
back.setStyleSheet(
"QPushButton{height:32px;padding:0 8px;border:0;background:transparent;"
"color:#409EFF;font-size:13px;font-weight:500;}"
"QPushButton:hover,QPushButton:focus{background:#ECF5FF;border-radius:6px;}"
"color:#78A7FF;font-size:13px;font-weight:500;}"
"QPushButton:hover,QPushButton:focus{background:#1B2440;border-radius:6px;}"
)
back.clicked.connect(self.reject)
left_layout.addWidget(back)
@@ -979,7 +983,7 @@ class DiagnosisDialog(QDialog):
self.readonly_hero_name.setObjectName("DiagnosisReadonlyPatientName")
right_layout.addWidget(self.readonly_hero_name)
self.readonly_hero_meta = QLabel("—")
self.readonly_hero_meta.setStyleSheet("color:#64748B; font-size:13px;")
self.readonly_hero_meta.setStyleSheet("color:#9AA7C0; font-size:13px;")
right_layout.addWidget(self.readonly_hero_meta)
self.readonly_status = QLabel("从未打卡")
self.readonly_status.setObjectName("DiagnosisReadonlyStatus")
@@ -1115,11 +1119,6 @@ class DiagnosisDialog(QDialog):
def _build_drawer_footer(self) -> QFrame:
footer = QFrame()
footer.setObjectName("DiagnosisDrawerFooter")
shadow = QGraphicsDropShadowEffect(footer)
shadow.setBlurRadius(18)
shadow.setOffset(0, -4)
shadow.setColor(QColor(15, 23, 42, 42))
footer.setGraphicsEffect(shadow)
layout = QHBoxLayout(footer)
layout.setContentsMargins(20, 14, 20, 18)
layout.setSpacing(12)
@@ -1161,42 +1160,58 @@ class DiagnosisDialog(QDialog):
scroll.setObjectName("DiagnosisDrawerBody")
scroll.setProperty("diagnosisTabKey", "basic")
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
scroll.verticalScrollBar().setSingleStep(28)
scroll.verticalScrollBar().setPageStep(120)
content = QWidget()
content.setObjectName("DiagnosisTabBasic")
content.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
content.setMinimumWidth(0)
content.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
layout = QVBoxLayout(content)
layout.setContentsMargins(16, 14, 16, 18)
layout.setSpacing(12)
layout.setContentsMargins(20, 16, 20, 20)
layout.setSpacing(8)
self.privacy_banner = QFrame()
self.privacy_banner.setObjectName("DiagnosisPrivacyBanner")
privacy_layout = QHBoxLayout(self.privacy_banner)
privacy_layout.setContentsMargins(12, 9, 12, 9)
self.privacy_label = QLabel("存在未完成的业务订单,患者基本信息不可修改")
self.privacy_label.setWordWrap(True)
self.privacy_label.setStyleSheet("color:#92400E; font-size:12px;")
self.privacy_label.setStyleSheet("color:#E4B967; font-size:12px;")
privacy_layout.addWidget(self.privacy_label)
self.privacy_banner.hide()
layout.addWidget(self.privacy_banner)
self.form_host = QFrame()
self.form_host.setObjectName("DiagnosisFormHost")
self.form_host.setProperty("locked", False)
self.form_host.setMinimumWidth(0)
form_layout = QVBoxLayout(self.form_host)
form_layout.setContentsMargins(0, 4, 0, 16)
form_layout.setSpacing(0)
form_layout.setContentsMargins(0, 0, 0, 8)
form_layout.setSpacing(12)
self.edit_fields: dict[str, Any] = {}
self.display_fields: dict[str, Any] = {}
for section_key, (title, rows) in enumerate(_FORM_SECTIONS):
form_layout.addWidget(section_heading(title, f"DiagnosisSection_{section_key}"))
for row_index, row_fields in enumerate(rows):
row_host = QWidget()
row_host.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
)
row_layout = QBoxLayout(QBoxLayout.Direction.LeftToRight, row_host)
row_layout.setContentsMargins(0, 0, 0, 8 if row_index == len(rows) - 1 else 18)
row_layout.setSpacing(12)
row_layout.setContentsMargins(0, 0, 0, 6 if row_index == len(rows) - 1 else 10)
row_layout.setSpacing(16)
fields: list[QWidget] = []
occupied = 0
for label, key, span, field_kind in row_fields:
editor = self._ensure_editor(key, field_kind)
container = self._field_container(label, editor, span)
display = (
editor.wrap_with_unit()
if isinstance(editor, DiagnosisNumberEdit)
else editor
)
container = self._field_container(label, display, span, editor=editor)
row_layout.addWidget(container, span)
fields.append(container)
occupied += span
@@ -1214,22 +1229,56 @@ class DiagnosisDialog(QDialog):
scroll.setWidget(content)
return scroll
def _field_container(self, caption: str, editor: QWidget, span: int) -> QWidget:
def _field_container(
self,
caption: str,
display: QWidget,
span: int,
*,
editor: QWidget | None = None,
) -> QWidget:
# `display` may wrap a number edit with an external unit label; stacked
# detection must use the semantic editor when provided.
semantic = editor if editor is not None else display
container = QWidget()
container.setMinimumWidth(0)
container.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(8)
container.setProperty("diagnosisFieldSpan", span)
policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
policy.setHorizontalStretch(max(1, span))
# Choice/flow editors need height-for-width; propagate so the form row grows.
if hasattr(semantic, "hasHeightForWidth") and semantic.hasHeightForWidth():
policy.setHeightForWidth(True)
container.setSizePolicy(policy)
# Multi-option chips sit under the label so wrapped rows stay visible.
stacked = isinstance(semantic, DiagnosisChoiceButtons) or (
hasattr(semantic, "hasHeightForWidth") and semantic.hasHeightForWidth()
)
layout = QBoxLayout(
QBoxLayout.Direction.TopToBottom if stacked else QBoxLayout.Direction.LeftToRight,
container,
)
layout.setContentsMargins(0, 2, 0, 2)
layout.setSpacing(6 if stacked else 8)
label = QLabel(caption)
label.setObjectName("DiagnosisFieldLabel")
label.setProperty("diagnosisFieldLabel", True)
label.setFixedWidth(62 if span <= 8 else 88 if span <= 12 else 120)
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
label.setContentsMargins(0, 8, 0, 0)
layout.addWidget(label)
editor.setMinimumWidth(0)
editor.setSizePolicy(QSizePolicy.Policy.Ignored, editor.sizePolicy().verticalPolicy())
layout.addWidget(editor, 1)
if stacked:
label.setMinimumWidth(0)
label.setMaximumWidth(16777215)
label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
else:
label.setFixedWidth(100)
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
label.setWordWrap(False)
layout.addWidget(label, 0, Qt.AlignmentFlag.AlignTop)
display.setMinimumWidth(0)
editor_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
if hasattr(semantic, "hasHeightForWidth") and semantic.hasHeightForWidth():
editor_policy.setHeightForWidth(True)
if not isinstance(semantic, DiagnosisSwitch):
display.setSizePolicy(editor_policy)
layout.addWidget(display, 1)
container.setProperty("diagnosisFieldStacked", stacked)
return container
def _ensure_editor(self, key: str, field_kind: str) -> QWidget:
@@ -1239,7 +1288,7 @@ class DiagnosisDialog(QDialog):
return self.display_fields[key]
if field_kind == "gender":
editor: Any = DiagnosisChoiceButtons(
(("男", 1), ("女", 0)), radio_buttons=True, columns=2
(("男", 1), ("女", 0)), multiple=False, columns=2
)
elif field_kind == "marital":
editor = DiagnosisComboBox((("未婚", 0), ("已婚", 1), ("离异", 2)))
@@ -1250,7 +1299,7 @@ class DiagnosisDialog(QDialog):
)
elif field_kind == "status":
editor = DiagnosisChoiceButtons(
(("启用", 1), ("禁用", 0)), radio_buttons=True, columns=2
(("启用", 1), ("禁用", 0)), multiple=False, columns=2
)
elif field_kind == "show_card":
editor = DiagnosisSwitch()
@@ -1266,13 +1315,13 @@ class DiagnosisDialog(QDialog):
elif field_kind == "age_number":
editor = DiagnosisNumberEdit(maximum=150)
elif field_kind == "height_number":
editor = DiagnosisNumberEdit(maximum=300, decimals=1, step=0.5, suffix=" cm")
editor = DiagnosisNumberEdit(maximum=300, decimals=1, step=0.5, suffix="cm")
elif field_kind == "weight_number":
editor = DiagnosisNumberEdit(maximum=500, decimals=1, step=0.1, suffix=" kg")
editor = DiagnosisNumberEdit(maximum=500, decimals=1, step=0.1, suffix="kg")
elif field_kind == "systolic_number":
editor = DiagnosisNumberEdit(maximum=250, suffix=" mmHg")
editor = DiagnosisNumberEdit(maximum=250, suffix="mmHg")
elif field_kind == "diastolic_number":
editor = DiagnosisNumberEdit(maximum=150, suffix=" mmHg")
editor = DiagnosisNumberEdit(maximum=150, suffix="mmHg")
elif field_kind == "date":
editor = DiagnosisDateEdit()
elif field_kind == "local_diagnosis":
@@ -1283,10 +1332,16 @@ class DiagnosisDialog(QDialog):
)
self._choice_fields[key] = editor
elif field_kind == "boolean":
editor = DiagnosisChoiceButtons((("有", 1), ("无", 0)), columns=2)
editor = DiagnosisChoiceButtons(
(("有", 1), ("无", 0)), multiple=False, columns=2
)
elif field_kind.endswith("_choices"):
multiple = _CHOICE_DICTIONARIES.get(key, (key, True))[1]
editor = DiagnosisChoiceButtons((), multiple=multiple, columns=4)
editor = DiagnosisChoiceButtons(
(),
multiple=multiple,
columns=4 if multiple else 5,
)
self._choice_fields[key] = editor
elif field_kind.startswith("textarea"):
rows = int(field_kind[-1])
@@ -1383,7 +1438,7 @@ class DiagnosisDialog(QDialog):
toolbar_layout.setContentsMargins(14, 10, 14, 10)
label = QLabel("复诊统计起始偏移")
label.setObjectName("DiagnosisOrderOffsetLabel")
label.setStyleSheet("color:#606266; font-size:12px; font-weight:500;")
label.setStyleSheet("color:#9AA7C0; font-size:12px; font-weight:500;")
label.setToolTip(_ORDER_OFFSET_HELP)
toolbar_layout.addWidget(label)
help_button = QPushButton("?")
@@ -1393,10 +1448,10 @@ class DiagnosisDialog(QDialog):
help_button.setFixedSize(22, 22)
help_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
help_button.setStyleSheet(
"QPushButton{color:#909399;border:1px solid #D9DEE5;border-radius:11px;"
"background:#FFFFFF;padding:0;font-size:12px;font-weight:600;}"
"QPushButton:hover,QPushButton:focus{color:#409EFF;border-color:#409EFF;"
"background:#ECF5FF;}"
"QPushButton{color:#9AA7C0;border:1px solid #29334F;border-radius:11px;"
"background:#151D31;padding:0;font-size:12px;font-weight:600;}"
"QPushButton:hover,QPushButton:focus{color:#EEF2FF;border-color:#6675F5;"
"background:#1B2440;}"
)
toolbar_layout.addWidget(help_button)
self.order_offset_help = help_button
@@ -1411,7 +1466,7 @@ class DiagnosisDialog(QDialog):
self.order_offset_preview = QLabel("第 1 笔实单计为一诊")
self.order_offset_preview.setObjectName("DiagnosisOrderOffsetPreview")
self.order_offset_preview.setToolTip(_ORDER_OFFSET_HELP)
self.order_offset_preview.setStyleSheet("color:#909399; font-size:12px;")
self.order_offset_preview.setStyleSheet("color:#9AA7C0; font-size:12px;")
toolbar_layout.addWidget(self.order_offset_preview)
offset_save = QPushButton("保存")
offset_save.setProperty("variant", "primary")
@@ -1424,7 +1479,7 @@ class DiagnosisDialog(QDialog):
layout.addWidget(self.orders_table, 1)
footer = QHBoxLayout()
self.orders_summary = QLabel("共 0 条")
self.orders_summary.setStyleSheet("color:#999999; font-size:12px;")
self.orders_summary.setStyleSheet("color:#9AA7C0; font-size:12px;")
footer.addWidget(self.orders_summary)
footer.addStretch(1)
self.orders_previous = QPushButton("上一页")
@@ -1649,19 +1704,49 @@ class DiagnosisDialog(QDialog):
overlay.sync_geometry()
def _reflow_form(self, viewport_width: int) -> None:
"""Mirror the admin breakpoint: desktop grid survives a 1024px viewport."""
"""Adapt the diagnosis form to the live drawer content width.
narrow = viewport_width <= 768
Wide/medium drawers keep a stable two-column grid. Only phone-narrow
drawers stack fields and flip labels above editors.
"""
content_w = max(240, int(viewport_width) - 56)
mode = "grid" if content_w >= 520 else "stack"
narrow = mode == "stack"
if narrow == self._form_narrow:
return
self._form_narrow = narrow
for row_layout, _fields, remainder in self._form_rows:
for row_layout, fields, remainder in self._form_rows:
row_layout.setDirection(
QBoxLayout.Direction.TopToBottom if narrow else QBoxLayout.Direction.LeftToRight
)
row_layout.setSpacing(18 if narrow else 12)
row_layout.setSpacing(12 if narrow else 16)
if remainder is not None:
remainder.setVisible(not narrow)
for container in fields:
self._orient_field_container(container, stacked=narrow)
def _orient_field_container(self, container: QWidget, *, stacked: bool) -> None:
# Choice fields stay label-above permanently so wrapped chips never clip.
if bool(container.property("diagnosisFieldStacked")):
return
layout = container.layout()
if not isinstance(layout, QBoxLayout):
return
label = container.findChild(QLabel, "DiagnosisFieldLabel")
layout.setDirection(
QBoxLayout.Direction.TopToBottom if stacked else QBoxLayout.Direction.LeftToRight
)
layout.setSpacing(6 if stacked else 8)
if label is None:
return
if stacked:
label.setMinimumWidth(0)
label.setMaximumWidth(16777215)
label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
else:
label.setFixedWidth(100)
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
def _sync_host_geometry(self) -> None:
owner = self._owner
@@ -1679,7 +1764,9 @@ class DiagnosisDialog(QDialog):
return
drawer_width = self.width() if self.width() <= 768 else max(456, round(self.width() * 0.60))
self.drawer_panel.setFixedWidth(drawer_width)
self._reflow_form(self.width())
# Reflow against the actual drawer content width, not the fullscreen overlay.
self._reflow_form(drawer_width)
self._sync_choice_field_heights()
def _reflow_readonly_hero(self) -> None:
if not hasattr(self, "readonly_hero_layout"):
@@ -2000,6 +2087,16 @@ class DiagnosisDialog(QDialog):
choices.insert(0, ("无", ""))
if editor is not None:
editor.set_choices(choices, preserve=False)
# Defer until the drawer has a real width so wrapped chips get height.
QTimer.singleShot(0, self._sync_choice_field_heights)
def _sync_choice_field_heights(self) -> None:
for editor in self._choice_fields.values():
if hasattr(editor, "_sync_flow_height"):
editor._sync_flow_height()
if hasattr(self, "form_host"):
self.form_host.updateGeometry()
self.form_host.adjustSize()
def _show_authoritative_content(self, visible: bool) -> None:
self.readonly_patient_card.setVisible(visible)
@@ -2265,6 +2362,12 @@ class DiagnosisDialog(QDialog):
raw = first_value(diagnosis, "oral_condition", "mouth_condition", default="")
if key == "diagnosis_type":
raw = _diagnosis_type_value(raw)
if key == "gender":
gender_token = str(raw).strip().lower() if raw not in (None, "") else ""
if gender_token in {"2", "f", "female", "女"}:
raw = 0
elif gender_token in {"1", "m", "male", "男"}:
raw = 1
self._field_originals[key] = raw
if key == "phone" and not self._can_phone_plain:
rendered = _mask_phone(raw)
@@ -2880,14 +2983,22 @@ class DiagnosisDialog(QDialog):
if not self._editable or not self._can_prescribe:
self._show_message("当前账号无开方权限或接口不可用。", "warning")
return
from .prescription import PrescriptionEditorDialog
from .prescription import (
PrescriptionEditorDialog,
build_prescription_clinical_diagnosis,
build_prescription_visit_no,
)
detail = self._detail or {}
diagnosis = get_value(detail, "diagnosis", None) or detail
appointment = get_value(detail, "appointment", None) or {}
case_record = first_value(diagnosis, "case_record", default={}) or {}
diagnosis_id = self._diagnosis_id
appointment_id = _int(first_value(appointment, "id", "appointment_id"), 0)
appointment_id = _int(
first_value(appointment, "id", "appointment_id", default=0)
or first_value(diagnosis, "appointment_id", default=0),
0,
)
seed = {
"diagnosis_id": diagnosis_id,
"appointment_id": appointment_id,
@@ -2895,12 +3006,13 @@ class DiagnosisDialog(QDialog):
"patient_name": first_value(diagnosis, "patient_name", "name"),
"gender": _int(first_value(diagnosis, "gender"), 0),
"age": _int(first_value(diagnosis, "age"), 0),
"visit_no": f"1K{appointment_id:08d}" if appointment_id > 0 else "",
"phone": first_value(diagnosis, "phone", default=""),
"visit_no": build_prescription_visit_no(
diagnosis_id=diagnosis_id, appointment_id=appointment_id
),
"tongue": first_value(diagnosis, "tongue", "tongue_coating"),
"pulse": first_value(diagnosis, "pulse"),
"clinical_diagnosis": first_value(
diagnosis, "clinical_diagnosis", "diagnosis", default=""
),
"clinical_diagnosis": build_prescription_clinical_diagnosis(diagnosis, case_record),
"doctor_name": first_value(
appointment, "doctor_name", default=first_value(diagnosis, "doctor_name")
),
@@ -3497,8 +3609,15 @@ class DiagnosisDialog(QDialog):
dialog.finished.connect(clear_dialog)
dialog.open()
def _build_order_detail_dialog(self, detail: Any, order_id: int) -> QDialog:
dialog = OrderDetailDrawer(self)
def _build_order_detail_dialog(
self,
detail: Any,
order_id: int,
*,
host: QWidget | None = None,
) -> QDialog:
owner = host if isinstance(host, QWidget) else self if isinstance(self, QWidget) else None
dialog = OrderDetailDrawer(owner)
header = QFrame()
header.setObjectName("DiagnosisOrderDetailHeader")
header_layout = QHBoxLayout(header)
@@ -4158,9 +4277,19 @@ class DiagnosisDialog(QDialog):
for key, field in self.edit_fields.items()
if key not in {"phone", "id_card"} or self._can_phone_plain
}
for key in ("patient_name", "phone", "id_card", "diagnosis_type"):
self.edit_fields[key].setProperty("invalid", False)
repolish(self.edit_fields[key])
for key in (
"patient_name",
"phone",
"id_card",
"gender",
"age",
"fasting_blood_sugar",
"diagnosis_type",
"local_hospital_name",
):
if key in self.edit_fields:
self.edit_fields[key].setProperty("invalid", False)
repolish(self.edit_fields[key])
patient_name = str(changes.get("patient_name") or "").strip()
if not patient_name:
self._show_message("请输入患者姓名。", "warning")
@@ -4182,10 +4311,37 @@ class DiagnosisDialog(QDialog):
repolish(self.edit_fields["id_card"])
self.edit_fields["id_card"].setFocus()
return
gender_text = str(self.edit_fields["gender"].toPlainText() or "").strip()
if gender_text == "":
self._show_message("请选择性别。", "warning")
self.edit_fields["gender"].setProperty("invalid", True)
repolish(self.edit_fields["gender"])
return
age_text = str(self.edit_fields["age"].toPlainText() or "").strip()
if age_text == "":
self._show_message("请输入年龄。", "warning")
self.edit_fields["age"].setProperty("invalid", True)
repolish(self.edit_fields["age"])
self.edit_fields["age"].setFocus()
return
fasting = str(changes.get("fasting_blood_sugar") or "").strip()
if not fasting:
self._show_message("请输入空腹血糖。", "warning")
self.edit_fields["fasting_blood_sugar"].setProperty("invalid", True)
repolish(self.edit_fields["fasting_blood_sugar"])
self.edit_fields["fasting_blood_sugar"].setFocus()
return
hospital = str(changes.get("local_hospital_name") or "").strip()
if not hospital:
self._show_message("请输入当地就诊医院名称。", "warning")
self.edit_fields["local_hospital_name"].setProperty("invalid", True)
repolish(self.edit_fields["local_hospital_name"])
self.edit_fields["local_hospital_name"].setFocus()
return
diagnosis_type_raw = str(changes.get("diagnosis_type") or "").strip()
diagnosis_type = _diagnosis_type_value(diagnosis_type_raw)
if diagnosis_type_raw and not diagnosis_type:
self._show_message("诊断类型参数无效,请重新选择。", "warning")
if not diagnosis_type:
self._show_message("请选择诊断类型。", "warning")
self.edit_fields["diagnosis_type"].setProperty("invalid", True)
repolish(self.edit_fields["diagnosis_type"])
self.edit_fields["diagnosis_type"].setFocus()
@@ -4262,4 +4418,72 @@ class DiagnosisDialog(QDialog):
self._sync_save_button()
__all__ = ["DiagnosisDialog"]
__all__ = ["DiagnosisDialog", "OrderDetailDrawer", "present_order_detail"]
def _unwrap_order_detail(detail: Any) -> Any:
payload = detail
if isinstance(detail, Mapping):
for key in ("data", "detail"):
nested = detail.get(key)
if isinstance(nested, Mapping) and any(
field in nested for field in ("id", "order_no", "prescription_id")
):
payload = nested
break
return payload
def present_order_detail(
parent: QWidget | None,
detail: Any,
*,
order_id: int = 0,
permissions: Any = None,
exec_: bool = True,
) -> OrderDetailDrawer:
"""Open the shared prescription-order detail drawer (admin drawer parity).
Used by diagnosis「业务订单」、处方「查看关联订单」and 我的患者「订单管理」.
"""
from types import MethodType
payload = _unwrap_order_detail(detail)
resolved_id = int(order_id or 0)
if resolved_id <= 0:
raw_id = first_value(payload, "id", "order_id", default=0)
try:
resolved_id = int(raw_id or 0)
except (TypeError, ValueError):
resolved_id = 0
class _OrderDetailShim:
pass
shim = _OrderDetailShim()
shim.permissions = permissions
shim._can_order_logs = has_permission(permissions, _ORDER_LOGS_PERMISSION, default=False)
for name in (
"_build_order_detail_dialog",
"_new_order_section",
"_add_order_empty",
"_add_order_field_grid",
"_add_order_amount_overview",
"_add_order_prescription_section",
"_build_order_payment_table",
"_add_order_payment_section",
"_add_order_fulfillment_section",
"_add_order_timeline_item",
"_add_order_logistics_section",
"_add_order_logs_section",
):
setattr(shim, name, MethodType(getattr(DiagnosisDialog, name), shim))
shim._status_kind = DiagnosisDialog._status_kind
dialog = shim._build_order_detail_dialog(payload, resolved_id, host=parent)
if exec_:
dialog.exec()
else:
dialog.open()
return dialog