更新
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
__all__ = ["DEBUG_MODE", "ONLINE_API_BASE_URL", "__version__"]
|
||||
|
||||
# Single source of truth for runtime, package, installer, and executable versions.
|
||||
__version__ = "1.4.0"
|
||||
__version__ = "1.4.1"
|
||||
|
||||
# 调试模式开启时,登录页显示“演示模式”和“服务器设置”。
|
||||
# 正式发布请保持 False;此时程序只使用下面配置的线上域名。
|
||||
DEBUG_MODE = True
|
||||
DEBUG_MODE = False
|
||||
ONLINE_API_BASE_URL = "https://admin.zhenyangtang.com.cn"
|
||||
|
||||
@@ -758,9 +758,11 @@ class DiagnosisDialog(QDialog):
|
||||
parent: QWidget | None = None,
|
||||
*,
|
||||
permissions: Any = None,
|
||||
embedded: bool = False,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.repository = repository
|
||||
self._embedded = bool(embedded)
|
||||
self.permissions = (
|
||||
permissions
|
||||
if permissions is not None
|
||||
@@ -882,11 +884,17 @@ class DiagnosisDialog(QDialog):
|
||||
)
|
||||
|
||||
self.setObjectName("DiagnosisDialogRoot")
|
||||
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
if self._embedded:
|
||||
self.setWindowFlags(Qt.WindowType.Widget)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False)
|
||||
self.setMinimumSize(0, 0)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)
|
||||
else:
|
||||
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||
self.setMinimumSize(760, 520)
|
||||
self.resize(1024, 640)
|
||||
self.setWindowTitle("患者信息详情")
|
||||
self.setMinimumSize(760, 520)
|
||||
self.resize(1024, 640)
|
||||
self.setStyleSheet(DIAGNOSIS_QSS)
|
||||
|
||||
self.view_stack = QStackedLayout(self)
|
||||
@@ -911,16 +919,25 @@ class DiagnosisDialog(QDialog):
|
||||
root = QVBoxLayout(page)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
self.readonly_header = QWidget(page)
|
||||
self.readonly_header.setObjectName("DiagnosisReadonlyHeader")
|
||||
header_layout = QVBoxLayout(self.readonly_header)
|
||||
header_layout.setContentsMargins(16, 16, 16, 0)
|
||||
header_layout.setSpacing(0)
|
||||
self.readonly_hero = self._build_readonly_hero()
|
||||
header_layout.addWidget(self.readonly_hero)
|
||||
root.addWidget(self.readonly_header)
|
||||
self.readonly_scroll = QScrollArea()
|
||||
self.readonly_scroll.setObjectName("DiagnosisReadonlyScroll")
|
||||
self.readonly_scroll.setWidgetResizable(True)
|
||||
self.readonly_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.readonly_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
|
||||
self.readonly_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.readonly_scroll.verticalScrollBar().setSingleStep(28)
|
||||
content = QWidget()
|
||||
self.readonly_content_layout = QVBoxLayout(content)
|
||||
self.readonly_content_layout.setContentsMargins(16, 16, 16, 16)
|
||||
self.readonly_content_layout.setSpacing(16)
|
||||
self.readonly_hero = self._build_readonly_hero()
|
||||
self.readonly_content_layout.addWidget(self.readonly_hero)
|
||||
self.readonly_error = QFrame()
|
||||
self.readonly_error.setObjectName("DiagnosisReadonlyErrorCard")
|
||||
self.readonly_error.setProperty("diagnosisReadonlyCard", True)
|
||||
@@ -981,16 +998,16 @@ class DiagnosisDialog(QDialog):
|
||||
left_layout = QHBoxLayout(self.readonly_hero_left)
|
||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
left_layout.setSpacing(12)
|
||||
back = QPushButton("← 返回")
|
||||
back.setObjectName("DiagnosisReadonlyBack")
|
||||
back.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
back.setStyleSheet(
|
||||
self.readonly_back_button = QPushButton("← 收起资料" if self._embedded else "← 返回")
|
||||
self.readonly_back_button.setObjectName("DiagnosisReadonlyBack")
|
||||
self.readonly_back_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.readonly_back_button.setStyleSheet(
|
||||
"QPushButton{height:32px;padding:0 8px;border:0;background:transparent;"
|
||||
"color:#5265F6;font-size:13px;font-weight:500;}"
|
||||
"QPushButton:hover,QPushButton:focus{background:#F0F2FF;border-radius:6px;}"
|
||||
)
|
||||
back.clicked.connect(self.reject)
|
||||
left_layout.addWidget(back)
|
||||
self.readonly_back_button.clicked.connect(self.reject)
|
||||
left_layout.addWidget(self.readonly_back_button)
|
||||
title = QLabel("患者信息详情")
|
||||
title.setObjectName("DiagnosisReadonlyTitle")
|
||||
left_layout.addWidget(title)
|
||||
@@ -1009,6 +1026,13 @@ class DiagnosisDialog(QDialog):
|
||||
self.readonly_status.setObjectName("DiagnosisReadonlyStatus")
|
||||
self.readonly_status.setProperty("severity", "neutral")
|
||||
right_layout.addWidget(self.readonly_status)
|
||||
self.readonly_close_button = QPushButton("×")
|
||||
self.readonly_close_button.setObjectName("DiagnosisCloseButton")
|
||||
self.readonly_close_button.setToolTip("关闭诊单详情")
|
||||
self.readonly_close_button.setAccessibleName("关闭诊单详情")
|
||||
self.readonly_close_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.readonly_close_button.clicked.connect(self.reject)
|
||||
right_layout.addWidget(self.readonly_close_button)
|
||||
layout.addWidget(self.readonly_hero_left, 0, 0)
|
||||
layout.addWidget(self.readonly_hero_right, 0, 1, Qt.AlignmentFlag.AlignRight)
|
||||
layout.setColumnStretch(0, 1)
|
||||
@@ -1790,6 +1814,8 @@ class DiagnosisDialog(QDialog):
|
||||
label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
def _sync_host_geometry(self) -> None:
|
||||
if self._embedded:
|
||||
return
|
||||
owner = self._owner
|
||||
if owner is None:
|
||||
if self.width() < 760 or self.height() < 520:
|
||||
@@ -1823,6 +1849,8 @@ class DiagnosisDialog(QDialog):
|
||||
layout.addWidget(self.readonly_hero_right, 0, 1, Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
def _install_owner_filter(self) -> None:
|
||||
if self._embedded:
|
||||
return
|
||||
if self._owner is not None and not self._owner_filter_installed:
|
||||
self._owner.installEventFilter(self)
|
||||
self._owner_filter_installed = True
|
||||
@@ -1830,6 +1858,10 @@ class DiagnosisDialog(QDialog):
|
||||
def _rebind_owner(self) -> None:
|
||||
"""Resolve the live Shell window for every open/show cycle."""
|
||||
|
||||
if self._embedded:
|
||||
self._owner = None
|
||||
self._owner_filter_installed = False
|
||||
return
|
||||
parent = self.parentWidget()
|
||||
candidate = parent.window() if parent is not None else None
|
||||
if candidate is self:
|
||||
@@ -1852,8 +1884,9 @@ class DiagnosisDialog(QDialog):
|
||||
super().resizeEvent(event)
|
||||
|
||||
def showEvent(self, event: Any) -> None:
|
||||
self._rebind_owner()
|
||||
self._sync_host_geometry()
|
||||
if not self._embedded:
|
||||
self._rebind_owner()
|
||||
self._sync_host_geometry()
|
||||
self._update_drawer_geometry()
|
||||
self._reflow_readonly_hero()
|
||||
super().showEvent(event)
|
||||
@@ -1938,10 +1971,12 @@ class DiagnosisDialog(QDialog):
|
||||
*,
|
||||
editable: bool = False,
|
||||
seed: Any = None,
|
||||
authoritative_detail: Any = None,
|
||||
view_only: bool = False,
|
||||
modeless: bool = False,
|
||||
auto_show: bool = True,
|
||||
) -> None:
|
||||
"""Open immediately, then replace the seed with authoritative server data."""
|
||||
"""Prepare a diagnosis view, optionally showing it immediately."""
|
||||
|
||||
self._rebind_owner()
|
||||
for player in list(self._recording_players):
|
||||
@@ -1986,13 +2021,13 @@ class DiagnosisDialog(QDialog):
|
||||
self._daily_todo_status = None
|
||||
self._orders_page = 1
|
||||
self._orders_total = 0
|
||||
self._detail = seed
|
||||
self._detail = authoritative_detail if authoritative_detail is not None else seed
|
||||
self.save_button.set_state("idle")
|
||||
self.refresh_permissions()
|
||||
self.view_stack.setCurrentWidget(
|
||||
self.readonly_page if self._standalone_readonly else self.drawer_overlay
|
||||
)
|
||||
self.setModal(not modeless and not self._standalone_readonly)
|
||||
self.setModal(False if self._embedded else not modeless and not self._standalone_readonly)
|
||||
self.setWindowTitle(
|
||||
"患者信息详情"
|
||||
if self._standalone_readonly
|
||||
@@ -2018,15 +2053,38 @@ class DiagnosisDialog(QDialog):
|
||||
)
|
||||
self._clear_tables()
|
||||
self._clear_message()
|
||||
if seed is not None:
|
||||
self._render(seed, [], [])
|
||||
if self._detail is not None:
|
||||
self._render(self._detail, [], [])
|
||||
self._sync_form_interactivity()
|
||||
self._sync_save_button()
|
||||
self._sync_host_geometry()
|
||||
if auto_show:
|
||||
self.show()
|
||||
if not self._embedded:
|
||||
self.raise_()
|
||||
if authoritative_detail is not None:
|
||||
diagnosis = get_value(authoritative_detail, "diagnosis", None) or authoritative_detail
|
||||
patient = get_value(authoritative_detail, "patient", None) or {}
|
||||
self._patient_id = _int(
|
||||
first_value(
|
||||
diagnosis,
|
||||
"patient_id",
|
||||
"source_patient_id",
|
||||
default=first_value(patient, "patient_id", "id", default=0),
|
||||
),
|
||||
0,
|
||||
)
|
||||
self._authoritative_detail_loaded = True
|
||||
self._show_authoritative_content(True)
|
||||
self._clear_message()
|
||||
self._set_loading(False)
|
||||
if self._standalone_readonly:
|
||||
self._load_visible_readonly_sections()
|
||||
else:
|
||||
self._ensure_tab_loaded(self._current_tab_key())
|
||||
return
|
||||
self._show_message("正在加载权威诊单详情…", "info")
|
||||
self._set_loading(True)
|
||||
self._sync_host_geometry()
|
||||
self.show()
|
||||
self.raise_()
|
||||
self._start_detail_load()
|
||||
|
||||
def _start_detail_load(self) -> None:
|
||||
|
||||
@@ -16,7 +16,7 @@ import re
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
from typing import Any
|
||||
@@ -80,6 +80,7 @@ from PySide6.QtWidgets import (
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QSplitter,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTabWidget,
|
||||
@@ -104,6 +105,7 @@ from ..widgets import (
|
||||
run_async,
|
||||
show_toast,
|
||||
)
|
||||
from .diagnosis import DiagnosisDialog as _StructuredDiagnosisDialog
|
||||
|
||||
PRESCRIPTION_DRAWER_QSS = r"""
|
||||
QFrame#PrescriptionDrawerSurface {
|
||||
@@ -2697,6 +2699,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self._source = _mapping(prescription)
|
||||
self._loading_data = False
|
||||
self._linked_order_generation = 0
|
||||
self._diagnosis_view: _StructuredDiagnosisDialog | None = None
|
||||
self._prescribing_creator_id = _int(
|
||||
first_value(
|
||||
prescription,
|
||||
@@ -2709,17 +2712,37 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.setModal(True)
|
||||
self.setMinimumSize(420, 600)
|
||||
self.resize(self.DRAWER_WIDTH, 900)
|
||||
root = QVBoxLayout(self)
|
||||
root = QHBoxLayout(self)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
|
||||
self.workspace_splitter = QSplitter(Qt.Orientation.Horizontal, self)
|
||||
self.workspace_splitter.setObjectName("PrescriptionWorkspaceSplitter")
|
||||
self.workspace_splitter.setChildrenCollapsible(False)
|
||||
self.workspace_splitter.setHandleWidth(1)
|
||||
root.addWidget(self.workspace_splitter)
|
||||
|
||||
self.diagnosis_host = QFrame(self.workspace_splitter)
|
||||
self.diagnosis_host.setObjectName("PrescriptionDiagnosisPane")
|
||||
self.diagnosis_host.setMinimumWidth(300)
|
||||
diagnosis_layout = QVBoxLayout(self.diagnosis_host)
|
||||
diagnosis_layout.setContentsMargins(0, 0, 0, 0)
|
||||
diagnosis_layout.setSpacing(0)
|
||||
self.diagnosis_layout = diagnosis_layout
|
||||
self.workspace_splitter.addWidget(self.diagnosis_host)
|
||||
self.diagnosis_host.hide()
|
||||
|
||||
self.drawer_surface = QFrame()
|
||||
self.drawer_surface.setObjectName("PrescriptionDrawerSurface")
|
||||
self.drawer_surface.setStyleSheet(_prescription_drawer_qss())
|
||||
self.drawer_surface.setMinimumWidth(self.minimumWidth())
|
||||
self.drawer_surface.setMaximumWidth(self.DRAWER_WIDTH)
|
||||
surface_layout = QVBoxLayout(self.drawer_surface)
|
||||
surface_layout.setContentsMargins(0, 0, 0, 0)
|
||||
surface_layout.setSpacing(0)
|
||||
root.addWidget(self.drawer_surface)
|
||||
self.workspace_splitter.addWidget(self.drawer_surface)
|
||||
self.workspace_splitter.setStretchFactor(0, 1)
|
||||
self.workspace_splitter.setStretchFactor(1, 0)
|
||||
|
||||
self.header = self._build_header()
|
||||
surface_layout.addWidget(self.header)
|
||||
@@ -2920,7 +2943,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.diagnosis_button.setProperty("size", "small")
|
||||
self.diagnosis_button.setVisible(diagnosis_id > 0)
|
||||
self.diagnosis_button.clicked.connect(
|
||||
lambda _checked=False, value=diagnosis_id: self.diagnosis_requested.emit(value)
|
||||
lambda _checked=False, value=diagnosis_id: self._toggle_diagnosis_view(value)
|
||||
)
|
||||
diagnosis_layout.addWidget(self.diagnosis_button)
|
||||
self.diagnosis_id_hint = QLabel(f"关联诊单 #{diagnosis_id}" if diagnosis_id > 0 else "")
|
||||
@@ -3237,15 +3260,58 @@ class PrescriptionEditorDialog(QDialog):
|
||||
target = targets[max(0, min(len(targets) - 1, index))]
|
||||
self.body_scroll.ensureWidgetVisible(target, 0, 24)
|
||||
|
||||
def _toggle_diagnosis_view(self, diagnosis_id: int) -> None:
|
||||
if self.diagnosis_host.isVisible():
|
||||
if self._diagnosis_view is not None:
|
||||
self._diagnosis_view.reject()
|
||||
return
|
||||
if diagnosis_id <= 0:
|
||||
return
|
||||
if not has_permission(self.permissions, "tcm.diagnosis/readonlyDetail", default=True):
|
||||
self.context_banner.show_message("无权查看诊单详情。", "danger")
|
||||
return
|
||||
if self._diagnosis_view is None:
|
||||
self._diagnosis_view = _StructuredDiagnosisDialog(
|
||||
self.repository,
|
||||
self.diagnosis_host,
|
||||
permissions=self.permissions,
|
||||
embedded=True,
|
||||
)
|
||||
self._diagnosis_view.finished.connect(
|
||||
lambda _result, view=self._diagnosis_view: self._diagnosis_view_finished(view)
|
||||
)
|
||||
self.diagnosis_layout.addWidget(self._diagnosis_view)
|
||||
self.diagnosis_host.show()
|
||||
self.diagnosis_button.setText("收起患者诊单")
|
||||
self._fit_drawer_geometry()
|
||||
self._diagnosis_view.open_for(diagnosis_id, editable=False)
|
||||
|
||||
def _diagnosis_view_finished(self, view: _StructuredDiagnosisDialog) -> None:
|
||||
if view is not self._diagnosis_view:
|
||||
return
|
||||
self.diagnosis_host.hide()
|
||||
self.diagnosis_button.setText("查看患者诊单详情")
|
||||
self._fit_drawer_geometry()
|
||||
|
||||
def _fit_drawer_geometry(self) -> None:
|
||||
parent = self.parentWidget()
|
||||
if parent is None:
|
||||
return
|
||||
anchor = parent.window()
|
||||
origin = anchor.mapToGlobal(QPoint(0, 0))
|
||||
width = min(self.DRAWER_WIDTH, max(self.minimumWidth(), anchor.width()))
|
||||
expanded = self.diagnosis_host.isVisible()
|
||||
width = (
|
||||
anchor.width()
|
||||
if expanded
|
||||
else min(self.DRAWER_WIDTH, max(self.minimumWidth(), anchor.width()))
|
||||
)
|
||||
height = max(self.minimumHeight(), anchor.height())
|
||||
self.setGeometry(origin.x() + anchor.width() - width, origin.y(), width, height)
|
||||
if expanded:
|
||||
editor_width = min(self.DRAWER_WIDTH, max(520, round(width * 0.55)))
|
||||
self.workspace_splitter.setSizes([max(300, width - editor_width - 1), editor_width])
|
||||
else:
|
||||
self.workspace_splitter.setSizes([0, width])
|
||||
|
||||
def showEvent(self, event: Any) -> None:
|
||||
super().showEvent(event)
|
||||
@@ -3836,6 +3902,11 @@ class PrescriptionEditorDialog(QDialog):
|
||||
self.validation.clear()
|
||||
super().accept()
|
||||
|
||||
def done(self, result: int) -> None:
|
||||
if self._diagnosis_view is not None and self._diagnosis_view.isVisible():
|
||||
self._diagnosis_view.reject()
|
||||
super().done(result)
|
||||
|
||||
|
||||
class PatchPatientDialog(QDialog):
|
||||
"""Narrow patient identity correction that preserves audit state."""
|
||||
@@ -5102,8 +5173,8 @@ class PrescriptionDetailDialog(QDialog):
|
||||
painter.end()
|
||||
|
||||
|
||||
class DiagnosisDetailDialog(QDialog):
|
||||
"""Read-only diagnosis view preserving the important admin tab boundaries."""
|
||||
class DiagnosisDetailDialog(_StructuredDiagnosisDialog):
|
||||
"""Compatibility entry that renders prescription-linked diagnoses with the shared UI."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -5113,180 +5184,29 @@ class DiagnosisDetailDialog(QDialog):
|
||||
repository: Any = None,
|
||||
permissions: Any = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
source = _mapping(diagnosis)
|
||||
self.repository = repository
|
||||
self.permissions = permissions
|
||||
self._order_detail_generation = 0
|
||||
self._order_detail_order_id = 0
|
||||
self._order_detail_table: QTableWidget | None = None
|
||||
self._order_detail_button: QPushButton | None = None
|
||||
if repository is None:
|
||||
raise ValueError("repository is required to show diagnosis details")
|
||||
super().__init__(repository, parent, permissions=permissions)
|
||||
self.setWindowTitle("诊单详情(只读)")
|
||||
self.resize(880, 700)
|
||||
root = QVBoxLayout(self)
|
||||
tabs = QTabWidget()
|
||||
groups = (
|
||||
(
|
||||
"病历",
|
||||
(
|
||||
"id",
|
||||
"patient_id",
|
||||
"patient_name",
|
||||
"gender",
|
||||
"age",
|
||||
"phone",
|
||||
"chief_complaint",
|
||||
"present_illness",
|
||||
"past_history",
|
||||
"diagnosis",
|
||||
"syndrome",
|
||||
"treatment",
|
||||
),
|
||||
source = _mapping(diagnosis)
|
||||
nested = _mapping(source.get("diagnosis"))
|
||||
diagnosis_id = _int(
|
||||
first_value(
|
||||
nested,
|
||||
"id",
|
||||
"diagnosis_id",
|
||||
default=first_value(source, "id", "diagnosis_id"),
|
||||
),
|
||||
("医生备注", ("doctor_notes", "doctor_note", "notes")),
|
||||
("日常记录", ("daily_records", "blood_records")),
|
||||
("处方", ("prescriptions", "case_records")),
|
||||
("沟通与指派", ("call_records", "chat_records", "assign_logs", "appointments")),
|
||||
0,
|
||||
)
|
||||
for title, keys in groups:
|
||||
browser = QTextBrowser()
|
||||
rows = []
|
||||
for key in keys:
|
||||
value = source.get(key)
|
||||
if value in (None, "", [], {}):
|
||||
continue
|
||||
rendered = (
|
||||
json.dumps(value, ensure_ascii=False, indent=2, default=str)
|
||||
if isinstance(value, (Mapping, list, tuple))
|
||||
else str(value)
|
||||
)
|
||||
rows.append(f"<h3>{html.escape(key)}</h3><pre>{html.escape(rendered)}</pre>")
|
||||
browser.setHtml("".join(rows) or "<p>暂无数据</p>")
|
||||
tabs.addTab(browser, title)
|
||||
tabs.addTab(self._build_orders_tab(source), "业务订单")
|
||||
root.addWidget(tabs, 1)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
buttons.rejected.connect(self.reject)
|
||||
root.addWidget(buttons)
|
||||
|
||||
def _build_orders_tab(self, source: Mapping[str, Any]) -> QWidget:
|
||||
host = QWidget()
|
||||
layout = QVBoxLayout(host)
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(8)
|
||||
rows: list[Any] = []
|
||||
for key in ("prescription_orders", "orders"):
|
||||
value = source.get(key)
|
||||
if isinstance(value, list):
|
||||
rows.extend(value)
|
||||
latest = source.get("latest_prescription_order")
|
||||
if isinstance(latest, Mapping) and latest:
|
||||
latest_id = _int(first_value(latest, "id", "order_id"), 0)
|
||||
if latest_id and not any(
|
||||
_int(first_value(row, "id", "order_id"), 0) == latest_id for row in rows
|
||||
):
|
||||
rows.insert(0, latest)
|
||||
if not rows:
|
||||
empty = QLabel("暂无关联业务订单")
|
||||
empty.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(empty, 1)
|
||||
return host
|
||||
table = QTableWidget(len(rows), 6)
|
||||
table.setHorizontalHeaderLabels(
|
||||
["订单号", "金额", "履约状态", "收货人", "手机", "创建时间"]
|
||||
)
|
||||
table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
table.verticalHeader().hide()
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
self._order_detail_table = table
|
||||
for row_index, row in enumerate(rows):
|
||||
values = (
|
||||
first_value(row, "order_no", "sn", "id"),
|
||||
first_value(row, "amount", "effective_amount"),
|
||||
first_value(row, "fulfillment_status_text", "status_text", "status"),
|
||||
first_value(row, "recipient_name", "patient_name"),
|
||||
first_value(row, "recipient_phone", "phone"),
|
||||
first_value(row, "create_time_text", "create_time"),
|
||||
)
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(display_text(value))
|
||||
item.setData(Qt.ItemDataRole.UserRole, row)
|
||||
table.setItem(row_index, column, item)
|
||||
layout.addWidget(table, 1)
|
||||
actions = QHBoxLayout()
|
||||
view = QPushButton("查看订单详情")
|
||||
view.setProperty("variant", "primary")
|
||||
self._order_detail_button = view
|
||||
view.clicked.connect(self._open_selected_order)
|
||||
table.itemDoubleClicked.connect(lambda _item: self._open_selected_order())
|
||||
actions.addWidget(view)
|
||||
actions.addStretch(1)
|
||||
layout.addLayout(actions)
|
||||
return host
|
||||
|
||||
def _set_order_detail_loading(self, loading: bool) -> None:
|
||||
if self._order_detail_table is not None:
|
||||
self._order_detail_table.setEnabled(not loading)
|
||||
if self._order_detail_button is not None:
|
||||
self._order_detail_button.setEnabled(not loading)
|
||||
|
||||
def _open_selected_order(self) -> None:
|
||||
table = self._order_detail_table
|
||||
if table is None:
|
||||
return
|
||||
row = table.currentRow()
|
||||
item = table.item(row, 0) if row >= 0 else None
|
||||
order = item.data(Qt.ItemDataRole.UserRole) if item is not None else None
|
||||
if order is None:
|
||||
return
|
||||
order_id = _int(first_value(order, "id", "order_id"), 0)
|
||||
self._order_detail_generation += 1
|
||||
generation = self._order_detail_generation
|
||||
self._order_detail_order_id = order_id
|
||||
getter = getattr(self.repository, "get_prescription_order", None)
|
||||
if order_id <= 0 or not callable(getter):
|
||||
self._set_order_detail_loading(False)
|
||||
self._present_order_detail(order, order_id)
|
||||
return
|
||||
|
||||
self._set_order_detail_loading(True)
|
||||
run_async(
|
||||
lambda: getter(order_id),
|
||||
on_success=lambda result: self._order_detail_success(result, order_id, generation),
|
||||
on_error=lambda error: self._order_detail_error(error, order, order_id, generation),
|
||||
on_finished=lambda: self._order_detail_finished(order_id, generation),
|
||||
)
|
||||
|
||||
def _order_detail_success(self, order: Any, order_id: int, generation: int) -> None:
|
||||
if generation != self._order_detail_generation or order_id != self._order_detail_order_id:
|
||||
return
|
||||
self._present_order_detail(order, order_id)
|
||||
|
||||
def _order_detail_error(
|
||||
self,
|
||||
_error: Exception,
|
||||
fallback_order: Any,
|
||||
order_id: int,
|
||||
generation: int,
|
||||
) -> None:
|
||||
if generation != self._order_detail_generation or order_id != self._order_detail_order_id:
|
||||
return
|
||||
self._present_order_detail(fallback_order, order_id)
|
||||
|
||||
def _order_detail_finished(self, order_id: int, generation: int) -> None:
|
||||
if generation == self._order_detail_generation and order_id == self._order_detail_order_id:
|
||||
self._set_order_detail_loading(False)
|
||||
|
||||
def _present_order_detail(self, order: Any, order_id: int) -> None:
|
||||
from .diagnosis import present_order_detail
|
||||
|
||||
present_order_detail(
|
||||
self.window() if self.window() is not None else self,
|
||||
order,
|
||||
order_id=order_id,
|
||||
permissions=self.permissions,
|
||||
exec_=True,
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis detail is missing a valid diagnosis id")
|
||||
self.open_for(
|
||||
diagnosis_id,
|
||||
editable=False,
|
||||
seed=source,
|
||||
authoritative_detail=source,
|
||||
auto_show=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -639,6 +639,23 @@ def test_readonly_is_an_independent_vertical_page_flow(
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_readonly_scroll_keeps_close_controls_reachable(application: QApplication) -> None:
|
||||
dialog = _open_dialog(application, (760, 520), mode="readonly")
|
||||
header_before = dialog.readonly_header.geometry()
|
||||
scroll_bar = dialog.readonly_scroll.verticalScrollBar()
|
||||
|
||||
assert scroll_bar.maximum() > 0
|
||||
assert dialog.readonly_close_button.isVisibleTo(dialog)
|
||||
scroll_bar.setValue(scroll_bar.maximum())
|
||||
application.processEvents()
|
||||
|
||||
assert dialog.readonly_header.geometry() == header_before
|
||||
assert dialog.readonly_close_button.isVisibleTo(dialog)
|
||||
dialog.readonly_close_button.click()
|
||||
application.processEvents()
|
||||
assert not dialog.isVisible()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [(1024, 640), (1440, 900)])
|
||||
@pytest.mark.parametrize("mode", ["edit", "viewOnly"])
|
||||
def test_drawer_is_full_height_rtl_and_sixty_percent_wide(
|
||||
|
||||
@@ -288,122 +288,121 @@ def test_paid_order_response_is_bound_to_active_diagnosis_and_blocks_save(
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_diagnosis_order_detail_lookup_is_queued_before_repository_call(
|
||||
def test_prescription_diagnosis_detail_uses_shared_structured_readonly_ui(
|
||||
application: QApplication,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
queued: list[tuple[Any, dict[str, Any]]] = []
|
||||
requested: list[int] = []
|
||||
shown: list[tuple[int, str]] = []
|
||||
|
||||
class Repository:
|
||||
def get_prescription_order(self, order_id: int) -> dict[str, Any]:
|
||||
requested.append(order_id)
|
||||
return {"id": order_id, "order_no": f"DETAIL-{order_id}"}
|
||||
def get_doctor_notes(self, diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
assert diagnosis_id == 745
|
||||
return []
|
||||
|
||||
def queue_async(function: Any, **options: Any) -> object:
|
||||
queued.append((function, options))
|
||||
return object()
|
||||
|
||||
def present_order_detail(
|
||||
_host: Any,
|
||||
order: dict[str, Any],
|
||||
*,
|
||||
order_id: int,
|
||||
permissions: Any,
|
||||
exec_: bool,
|
||||
) -> None:
|
||||
del permissions, exec_
|
||||
shown.append((order_id, order["order_no"]))
|
||||
|
||||
monkeypatch.setattr(dialog_module, "run_async", queue_async)
|
||||
monkeypatch.setattr(diagnosis_module, "present_order_detail", present_order_detail)
|
||||
monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async)
|
||||
dialog = DiagnosisDetailDialog(
|
||||
{"orders": [{"id": 17, "order_no": "ROW-17"}]},
|
||||
{
|
||||
"diagnosis": {
|
||||
"id": 745,
|
||||
"patient_id": 745,
|
||||
"patient_name": "庄志芳",
|
||||
"phone": "13823549442",
|
||||
"id_card": "440305196701011234",
|
||||
"gender": 0,
|
||||
"age": 59,
|
||||
"chief_complaint": "睡眠不好、出汗多",
|
||||
"past_history": ["高血压", "高脂血症"],
|
||||
},
|
||||
"patient": {"id": 745},
|
||||
},
|
||||
repository=Repository(),
|
||||
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]),
|
||||
)
|
||||
table = dialog._order_detail_table
|
||||
button = dialog._order_detail_button
|
||||
assert table is not None
|
||||
assert button is not None
|
||||
table.setCurrentCell(0, 0)
|
||||
|
||||
button.click()
|
||||
|
||||
assert len(queued) == 1
|
||||
assert requested == []
|
||||
assert shown == []
|
||||
assert not table.isEnabled()
|
||||
assert not button.isEnabled()
|
||||
|
||||
function, options = queued[0]
|
||||
options["on_success"](function())
|
||||
options["on_finished"]()
|
||||
assert requested == [17]
|
||||
assert shown == [(17, "DETAIL-17")]
|
||||
assert table.isEnabled()
|
||||
assert button.isEnabled()
|
||||
assert isinstance(dialog, DiagnosisDialog)
|
||||
assert dialog.view_stack.currentWidget() is dialog.readonly_page
|
||||
assert dialog.edit_fields["chief_complaint"].toPlainText() == "睡眠不好、出汗多"
|
||||
assert dialog.summary_fields["phone"].text() == "138****9442"
|
||||
assert dialog.case_grid.isVisibleTo(dialog)
|
||||
assert not dialog.findChildren(dialog_module.QTextBrowser)
|
||||
dialog.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_diagnosis_order_detail_ignores_stale_result_and_keeps_row_fallback(
|
||||
@pytest.mark.parametrize("owner_width", [1024, 1440, 1710])
|
||||
def test_prescription_editor_embeds_scrollable_diagnosis_beside_editable_form(
|
||||
application: QApplication,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
owner_width: int,
|
||||
) -> None:
|
||||
queued: list[dict[str, Any]] = []
|
||||
shown: list[tuple[int, str]] = []
|
||||
class Repository:
|
||||
def get_diagnosis_detail(
|
||||
self, diagnosis_id: int, *, readonly: bool = False
|
||||
) -> dict[str, Any]:
|
||||
assert readonly
|
||||
return {
|
||||
"diagnosis": {
|
||||
"id": diagnosis_id,
|
||||
"patient_id": 745,
|
||||
"patient_name": "庄志芳",
|
||||
"chief_complaint": "睡眠不好、出汗多",
|
||||
}
|
||||
}
|
||||
|
||||
def queue_async(_function: Any, **options: Any) -> object:
|
||||
queued.append(options)
|
||||
return object()
|
||||
def get_doctor_notes(self, _diagnosis_id: int) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
def present_order_detail(
|
||||
_host: Any,
|
||||
order: dict[str, Any],
|
||||
*,
|
||||
order_id: int,
|
||||
permissions: Any,
|
||||
exec_: bool,
|
||||
) -> None:
|
||||
del permissions, exec_
|
||||
shown.append((order_id, order["order_no"]))
|
||||
|
||||
repository = SimpleNamespace(get_prescription_order=lambda order_id: {"id": order_id})
|
||||
monkeypatch.setattr(dialog_module, "run_async", queue_async)
|
||||
monkeypatch.setattr(diagnosis_module, "present_order_detail", present_order_detail)
|
||||
dialog = DiagnosisDetailDialog(
|
||||
{
|
||||
"orders": [
|
||||
{"id": 21, "order_no": "ROW-21"},
|
||||
{"id": 22, "order_no": "ROW-22"},
|
||||
]
|
||||
},
|
||||
repository=repository,
|
||||
monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async)
|
||||
owner = QDialog()
|
||||
owner.resize(owner_width, 720)
|
||||
owner.show()
|
||||
repository = Repository()
|
||||
editor = PrescriptionEditorDialog(
|
||||
repository,
|
||||
{"diagnosis_id": 745, "patient_name": "庄志芳"},
|
||||
mode="edit",
|
||||
current_user=SimpleNamespace(id=9, name="周医生"),
|
||||
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]),
|
||||
parent=owner,
|
||||
)
|
||||
table = dialog._order_detail_table
|
||||
button = dialog._order_detail_button
|
||||
assert table is not None
|
||||
assert button is not None
|
||||
editor.show()
|
||||
application.processEvents()
|
||||
assert QApplication.activeModalWidget() is editor
|
||||
assert editor.width() == editor.DRAWER_WIDTH
|
||||
|
||||
table.setCurrentCell(0, 0)
|
||||
dialog._open_selected_order()
|
||||
table.setCurrentCell(1, 0)
|
||||
dialog._open_selected_order()
|
||||
assert len(queued) == 2
|
||||
editor.diagnosis_button.click()
|
||||
application.processEvents()
|
||||
|
||||
queued[0]["on_success"]({"id": 21, "order_no": "STALE-21"})
|
||||
queued[0]["on_finished"]()
|
||||
assert shown == []
|
||||
assert not table.isEnabled()
|
||||
assert not button.isEnabled()
|
||||
detail = editor._diagnosis_view
|
||||
assert detail is not None
|
||||
assert QApplication.activeModalWidget() is editor
|
||||
assert not detail.isWindow()
|
||||
assert detail.parentWidget() is editor.diagnosis_host
|
||||
assert editor.diagnosis_host.isVisibleTo(editor)
|
||||
assert editor.drawer_surface.isVisibleTo(editor)
|
||||
assert editor.width() == owner.width()
|
||||
assert editor.diagnosis_host.geometry().right() < editor.drawer_surface.geometry().left()
|
||||
scroll_bar = detail.readonly_scroll.verticalScrollBar()
|
||||
assert scroll_bar.maximum() > 0
|
||||
scroll_bar.setValue(scroll_bar.maximum())
|
||||
assert scroll_bar.value() == scroll_bar.maximum()
|
||||
|
||||
queued[1]["on_error"](RuntimeError("detail unavailable"))
|
||||
queued[1]["on_finished"]()
|
||||
assert shown == [(22, "ROW-22")]
|
||||
assert table.isEnabled()
|
||||
assert button.isEnabled()
|
||||
dialog.close()
|
||||
editor.patient_name.setText("庄志芳(已核对)")
|
||||
assert editor.patient_name.isEnabled()
|
||||
assert editor.payload()["patient_name"] == "庄志芳(已核对)"
|
||||
|
||||
detail.readonly_close_button.click()
|
||||
application.processEvents()
|
||||
assert editor.isVisible()
|
||||
assert not editor.diagnosis_host.isVisible()
|
||||
assert editor.width() == editor.DRAWER_WIDTH
|
||||
assert editor.patient_name.text() == "庄志芳(已核对)"
|
||||
editor.diagnosis_button.click()
|
||||
application.processEvents()
|
||||
assert editor.diagnosis_host.isVisibleTo(editor)
|
||||
editor.reject()
|
||||
application.processEvents()
|
||||
assert not editor.isVisible()
|
||||
assert not detail.isVisible()
|
||||
owner.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user