This commit is contained in:
Your Name
2026-09-03 16:03:41 +08:00
parent b4c11881b4
commit 928f72ec3d
25 changed files with 1926 additions and 394 deletions
+2 -2
View File
@@ -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,
)