新增
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user