更新
This commit is contained in:
@@ -7,5 +7,5 @@ __version__ = "1.2.0"
|
||||
|
||||
# 调试模式开启时,登录页显示“演示模式”和“服务器设置”。
|
||||
# 正式发布请保持 False;此时程序只使用下面配置的线上域名。
|
||||
DEBUG_MODE = False
|
||||
DEBUG_MODE = True
|
||||
ONLINE_API_BASE_URL = "https://admin.zhenyangtang.com.cn"
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Confirmation and optional doctor note for completing an appointment."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QLabel,
|
||||
QPlainTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..theme import mark_business_dialog
|
||||
from ..widgets import MessageBanner
|
||||
|
||||
COMPLETION_NOTE_LIMIT = 500
|
||||
|
||||
|
||||
class AppointmentCompleteDialog(QDialog):
|
||||
"""Keep an unsaved note available if completion or note saving fails."""
|
||||
|
||||
submitted = Signal(str)
|
||||
|
||||
def __init__(self, *, can_note: bool, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._can_note = can_note
|
||||
self._busy = False
|
||||
self._completed = False
|
||||
self.setWindowTitle("完成问诊")
|
||||
self.resize(460, 350 if can_note else 230)
|
||||
self.setMinimumWidth(420)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(24, 22, 24, 18)
|
||||
layout.setSpacing(10)
|
||||
title = QLabel("完成问诊", self)
|
||||
title.setProperty("dialogRole", "title")
|
||||
layout.addWidget(title)
|
||||
prompt = QLabel("是否添加医生备注?" if can_note else "确认完成该挂号吗?", self)
|
||||
layout.addWidget(prompt)
|
||||
self.note_edit = QPlainTextEdit(self)
|
||||
self.note_edit.setObjectName("AppointmentCompleteNote")
|
||||
self.note_edit.setAccessibleName("完成问诊医生备注")
|
||||
self.note_edit.setPlaceholderText("填写完成备注,将追加到医生备注时间轴")
|
||||
self.note_edit.setMinimumHeight(100)
|
||||
self.note_edit.setVisible(can_note)
|
||||
self.note_edit.textChanged.connect(self._limit_note)
|
||||
layout.addWidget(self.note_edit)
|
||||
self.note_counter = QLabel(f"0 / {COMPLETION_NOTE_LIMIT}", self)
|
||||
self.note_counter.setObjectName("AppointmentCompleteNoteCounter")
|
||||
self.note_counter.setProperty("dialogRole", "subtitle")
|
||||
self.note_counter.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
self.note_counter.setVisible(can_note)
|
||||
layout.addWidget(self.note_counter)
|
||||
hint = QLabel("系统会再次核对服务端挂号状态;完成后不可撤销。", self)
|
||||
hint.setProperty("dialogRole", "subtitle")
|
||||
hint.setWordWrap(True)
|
||||
layout.addWidget(hint)
|
||||
self.banner = MessageBanner(parent=self)
|
||||
layout.addWidget(self.banner)
|
||||
self.buttons = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self
|
||||
)
|
||||
self.confirm_button = self.buttons.button(QDialogButtonBox.StandardButton.Ok)
|
||||
self.confirm_button.setText("确认完成")
|
||||
self.confirm_button.setProperty("variant", "primary")
|
||||
self.confirm_button.setAutoDefault(False)
|
||||
self.cancel_button = self.buttons.button(QDialogButtonBox.StandardButton.Cancel)
|
||||
self.cancel_button.setText("取消")
|
||||
self.cancel_button.setAutoDefault(False)
|
||||
self.buttons.accepted.connect(self._submit)
|
||||
self.buttons.rejected.connect(self.reject)
|
||||
layout.addWidget(self.buttons)
|
||||
mark_business_dialog(self, "AppointmentCompleteDialog")
|
||||
|
||||
def _limit_note(self) -> None:
|
||||
text = self.note_edit.toPlainText()
|
||||
if len(text) > COMPLETION_NOTE_LIMIT:
|
||||
text = text[:COMPLETION_NOTE_LIMIT]
|
||||
cursor_position = self.note_edit.textCursor().position()
|
||||
self.note_edit.blockSignals(True)
|
||||
self.note_edit.setPlainText(text)
|
||||
cursor = self.note_edit.textCursor()
|
||||
cursor.setPosition(min(cursor_position, self.note_edit.document().characterCount() - 1))
|
||||
self.note_edit.setTextCursor(cursor)
|
||||
self.note_edit.blockSignals(False)
|
||||
self.note_counter.setText(f"{len(text)} / {COMPLETION_NOTE_LIMIT}")
|
||||
|
||||
def _submit(self) -> None:
|
||||
if not self._busy and not self._completed:
|
||||
self.submitted.emit(self.note_edit.toPlainText().strip() if self._can_note else "")
|
||||
|
||||
def set_busy(self, busy: bool) -> None:
|
||||
self._busy = busy
|
||||
self.note_edit.setReadOnly(busy or self._completed)
|
||||
self.confirm_button.setEnabled(not busy and not self._completed)
|
||||
self.confirm_button.setText("正在完成…" if busy else "确认完成")
|
||||
self.cancel_button.setEnabled(not busy)
|
||||
if busy:
|
||||
self.banner.clear()
|
||||
|
||||
def show_error(self, message: str) -> None:
|
||||
self.set_busy(False)
|
||||
self.banner.show_message(message, "danger")
|
||||
|
||||
def show_completed_warning(self, message: str) -> None:
|
||||
self._completed = True
|
||||
self.set_busy(False)
|
||||
self.confirm_button.hide()
|
||||
self.cancel_button.setText("关闭")
|
||||
self.banner.show_message(f"{message}。可复制上方备注,稍后在医生备注中补录。", "warning")
|
||||
|
||||
def reject(self) -> None:
|
||||
if not self._busy:
|
||||
super().reject()
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802 - Qt API
|
||||
if self._busy:
|
||||
event.ignore()
|
||||
else:
|
||||
super().closeEvent(event)
|
||||
@@ -4,10 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, timedelta
|
||||
from html import escape
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QDateTime, QRectF, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap
|
||||
from PySide6.QtCore import QDateTime, QModelIndex, QRectF, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap, QTextDocument, QTextOption
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDateTimeEdit,
|
||||
@@ -23,6 +25,9 @@ from PySide6.QtWidgets import (
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QStackedWidget,
|
||||
QStyle,
|
||||
QStyledItemDelegate,
|
||||
QStyleOptionViewItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
@@ -381,26 +386,81 @@ def _formula(value: Any) -> str:
|
||||
|
||||
|
||||
def _order_warnings(row: Any) -> list[str]:
|
||||
if not _truthy(first_value(row, "has_prescription_order", default=False)):
|
||||
"""Match the PC list's linked-order checks, including blank herb rows."""
|
||||
|
||||
raw = getattr(row, "raw", None)
|
||||
source = raw if isinstance(raw, Mapping) and raw else row
|
||||
try:
|
||||
has_order = float(get_value(source, "has_prescription_order", 0) or 0) == 1
|
||||
except (TypeError, ValueError):
|
||||
has_order = False
|
||||
if not has_order:
|
||||
return []
|
||||
herbs = get_value(row, "herbs", None) or []
|
||||
if not isinstance(herbs, (list, tuple)) or not herbs:
|
||||
herbs = get_value(source, "herbs", None)
|
||||
herbs = herbs if isinstance(herbs, (list, tuple)) else []
|
||||
names = [str(get_value(herb, "name", "") or "").strip() for herb in herbs]
|
||||
names = [name for name in names if name]
|
||||
if not names:
|
||||
return ["请开方,当前处方药材为空白"]
|
||||
seen: set[str] = set()
|
||||
duplicate: list[str] = []
|
||||
for herb in herbs:
|
||||
name = str(first_value(herb, "name", "medicine_name", default="")).strip()
|
||||
for name in names:
|
||||
key = "".join(name.split()).lower()
|
||||
if key and key in seen and name not in duplicate:
|
||||
duplicate.append(name)
|
||||
seen.add(key)
|
||||
return [f"已有关联业务订单,存在重复药材:{'、'.join(duplicate)}"] if duplicate else []
|
||||
return [f"已有关联业务订单,当前处方存在重复药材:{'、'.join(duplicate)}"] if duplicate else []
|
||||
|
||||
|
||||
def _sn_cell(_value: Any, row: Any) -> str:
|
||||
return str(first_value(row, "sn", "prescription_no", "id", default="—"))
|
||||
|
||||
|
||||
class _PrescriptionNumberDelegate(QStyledItemDelegate):
|
||||
"""Paint visible PC-style reminders without changing the sortable SN value."""
|
||||
|
||||
def document(self, option: QStyleOptionViewItem, index: QModelIndex, width: int) -> QTextDocument:
|
||||
row = index.data(Qt.ItemDataRole.UserRole)
|
||||
number = str(index.data(Qt.ItemDataRole.DisplayRole) or "—")
|
||||
record_id = display_text(first_value(row, "id", "prescription_id", default="—"))
|
||||
document = QTextDocument()
|
||||
document.setDocumentMargin(0)
|
||||
document.setDefaultFont(option.font)
|
||||
text_option = document.defaultTextOption()
|
||||
text_option.setWrapMode(QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere)
|
||||
document.setDefaultTextOption(text_option)
|
||||
paragraphs = [
|
||||
f'<p style="margin:0;color:#315CF4;font-size:12px;font-weight:600">{escape(number)}</p>',
|
||||
f'<p style="margin:2px 0 0;color:#7481A3;font-size:11px;font-weight:400">ID: {escape(record_id)}</p>',
|
||||
]
|
||||
paragraphs.extend(
|
||||
'<p style="margin:2px 0 0;color:#DC2626;font-size:12px;font-weight:400">'
|
||||
+ escape(warning)
|
||||
+ "</p>"
|
||||
for warning in _order_warnings(row)
|
||||
)
|
||||
document.setHtml("".join(paragraphs))
|
||||
document.setTextWidth(max(1, width - 16))
|
||||
return document
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex) -> None:
|
||||
styled = QStyleOptionViewItem(option)
|
||||
self.initStyleOption(styled, index)
|
||||
styled.text = ""
|
||||
self.parent().style().drawControl(QStyle.ControlElement.CE_ItemViewItem, styled, painter)
|
||||
document = self.document(option, index, option.rect.width())
|
||||
painter.save()
|
||||
painter.setClipRect(option.rect)
|
||||
painter.translate(option.rect.left() + 8, option.rect.top() + 6)
|
||||
document.drawContents(painter)
|
||||
painter.restore()
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize: # noqa: N802
|
||||
width = self.parent().columnWidth(index.column())
|
||||
document = self.document(option, index, width)
|
||||
return QSize(width, max(36, ceil(document.size().height()) + 12))
|
||||
|
||||
|
||||
def _patient_cell(_value: Any, row: Any) -> str:
|
||||
gender = first_value(row, "gender", default=None)
|
||||
gender_text = "男" if gender in (1, "1") else "女" if gender in (0, "0") else "未知"
|
||||
@@ -698,7 +758,7 @@ class PrescriptionsPage(QWidget):
|
||||
self.table = SortableTable(
|
||||
[
|
||||
TableColumn("__selected__", "", 46, lambda _value, _row: ""),
|
||||
TableColumn("sn", "处方编号", 174, _sn_cell),
|
||||
TableColumn("sn", "处方编号", 260, _sn_cell),
|
||||
TableColumn("__actions__", "操作", 150, lambda _value, _row: ""),
|
||||
TableColumn("prescription_type", "处方类型", 96),
|
||||
TableColumn("is_system_auto", "来源", 88, _source_cell),
|
||||
@@ -713,6 +773,9 @@ class PrescriptionsPage(QWidget):
|
||||
self.table.verticalHeader().setDefaultSectionSize(36)
|
||||
self.table.horizontalHeader().setFixedHeight(38)
|
||||
self.table.setWordWrap(False)
|
||||
self.table.setItemDelegateForColumn(1, _PrescriptionNumberDelegate(self.table))
|
||||
self.table.horizontalHeader().sectionResized.connect(self._number_column_resized)
|
||||
self.table.model().layoutChanged.connect(self.table.resizeRowsToContents)
|
||||
self.table.horizontalHeaderItem(0).setIcon(_painted_icon("checkbox", "#AEB9D4", 14))
|
||||
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.table.itemSelectionChanged.connect(self._selection_changed)
|
||||
@@ -729,6 +792,10 @@ class PrescriptionsPage(QWidget):
|
||||
layout.addWidget(self.stack, 1)
|
||||
return card
|
||||
|
||||
def _number_column_resized(self, column: int, _old_size: int, _new_size: int) -> None:
|
||||
if column == 1:
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def _action_button(
|
||||
self,
|
||||
text: str,
|
||||
@@ -910,8 +977,11 @@ class PrescriptionsPage(QWidget):
|
||||
font.setWeight(QFont.Weight.DemiBold)
|
||||
sn_item.setFont(font)
|
||||
warnings = _order_warnings(row)
|
||||
if warnings:
|
||||
sn_item.setToolTip("\n".join(warnings))
|
||||
description = "\n".join(
|
||||
[sn_item.text(), f"ID: {first_value(row, 'id', 'prescription_id', default='—')}", *warnings]
|
||||
)
|
||||
sn_item.setToolTip(description)
|
||||
sn_item.setData(Qt.ItemDataRole.AccessibleTextRole, description)
|
||||
|
||||
prescription_type = display_text(
|
||||
first_value(row, "prescription_type", default="—")
|
||||
@@ -1002,6 +1072,7 @@ class PrescriptionsPage(QWidget):
|
||||
actions.addStretch(1)
|
||||
self.table.setCellWidget(row_index, 2, actions_host)
|
||||
self._sync_row_mutation_actions()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def _run_row_action(self, row: Any, callback: Callable[[], None]) -> None:
|
||||
target_id = _int(first_value(row, "id", "prescription_id", default=None), 0)
|
||||
|
||||
@@ -34,10 +34,12 @@ from PySide6.QtGui import (
|
||||
QFont,
|
||||
QFontMetrics,
|
||||
QIcon,
|
||||
QKeySequence,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
QPen,
|
||||
QPixmap,
|
||||
QShortcut,
|
||||
QTextCursor,
|
||||
QTextLayout,
|
||||
QTextOption,
|
||||
@@ -72,6 +74,7 @@ from ..diagnosis_drawer import DailyRecordPanel
|
||||
from ..diagnosis_editors import FlowLayout
|
||||
from ..dialogs import DiagnosisDialog
|
||||
from ..dialogs.ai_consult import present_ai_consult
|
||||
from ..dialogs.appointment_complete import COMPLETION_NOTE_LIMIT, AppointmentCompleteDialog
|
||||
from ..dialogs.prescription_ai import (
|
||||
can_open_diagnosis_ai_report,
|
||||
can_use_diagnosis_ai_assistant,
|
||||
@@ -112,6 +115,12 @@ _AI_AUTOMATIC_REQUEST_SLOTS = BoundedSemaphore(2)
|
||||
_AI_GENERATION_POOL = QThreadPool()
|
||||
_AI_GENERATION_POOL.setMaxThreadCount(4)
|
||||
_AI_GENERATION_POOL.setExpiryTimeout(30_000)
|
||||
# Keep explicit recovery reads independent of automatic analysis workers.
|
||||
_RECEPTION_REFRESH_POOL = QThreadPool()
|
||||
_RECEPTION_REFRESH_POOL.setMaxThreadCount(3)
|
||||
_RECEPTION_REFRESH_POOL.setExpiryTimeout(30_000)
|
||||
_REFRESH_TIMEOUT_MS = 30_000
|
||||
_REFRESH_COOLDOWN_MS = 1_500
|
||||
AI_MEDICAL_DISCLAIMER = (
|
||||
"仅供临床辅助参考,不可替代医生诊断,不得直接用于开方、用药调整或其他医疗决策。"
|
||||
"系统未对舌像、报告附件或视频画面进行视觉诊断;仅分析已录入、归档或转写的文字及附件元数据。"
|
||||
@@ -157,6 +166,32 @@ QCalendarWidget#ReceptionDateCalendar {
|
||||
border: 1px solid #DDE5FA;
|
||||
border-radius: 10px;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionRefreshButton {
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
padding: 0;
|
||||
color: #5469F0;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E4E9F6;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionRefreshButton:enabled:hover {
|
||||
background-color: #EEF1FF;
|
||||
border-color: #C5CEFF;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionRefreshButton:enabled:pressed {
|
||||
background-color: #E2E7FF;
|
||||
border-color: #9EACFF;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionRefreshButton:focus {
|
||||
border-color: #5469F0;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionRefreshButton:disabled {
|
||||
color: #8A93A8;
|
||||
background-color: #F7F8FB;
|
||||
border-color: #E4E9F6;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton[receptionQueueChip="true"] {
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
@@ -306,6 +341,24 @@ QWidget#ReceptionPage QPushButton#ReceptionCompleteButton {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionCompleteButton:enabled:hover {
|
||||
color: #CF4656;
|
||||
background-color: #FFF0F2;
|
||||
border-color: #EFA3AD;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionCompleteButton:enabled:pressed {
|
||||
color: #BF3949;
|
||||
background-color: #FFE4E8;
|
||||
border-color: #E58A98;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionCompleteButton:enabled:focus {
|
||||
border-color: #CF4656;
|
||||
}
|
||||
QWidget#ReceptionPage QPushButton#ReceptionCompleteButton:disabled {
|
||||
color: #A8AFBF;
|
||||
background-color: #F7F8FB;
|
||||
border-color: #E5E8EF;
|
||||
}
|
||||
QWidget#ReceptionPage QTabBar#ReceptionDetailTabs {
|
||||
background-color: #FFFFFF;
|
||||
border-bottom: 1px solid #E6EAF5;
|
||||
@@ -2090,6 +2143,26 @@ def _is_local_material_reference(value: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
class _ReceptionCompleteButton(QPushButton):
|
||||
"""Completion action with a pointer only while it can be activated."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__("结束问诊", parent)
|
||||
self.setObjectName("ReceptionCompleteButton")
|
||||
self.setIcon(_painted_reception_action_icon("stop", "#F15B67"))
|
||||
self.setIconSize(QSize(14, 14))
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
def changeEvent(self, event: QEvent) -> None: # noqa: N802 - Qt API
|
||||
super().changeEvent(event)
|
||||
if event.type() == QEvent.Type.EnabledChange:
|
||||
self.setCursor(
|
||||
Qt.CursorShape.PointingHandCursor
|
||||
if self.isEnabled()
|
||||
else Qt.CursorShape.ArrowCursor
|
||||
)
|
||||
|
||||
|
||||
class _NoteAttachmentPreview(QPushButton):
|
||||
"""Responsive inline thumbnail that remains clickable for a full preview."""
|
||||
|
||||
@@ -3587,6 +3660,8 @@ class ReceptionPage(QWidget):
|
||||
self._queue_query: dict[str, Any] | None = None
|
||||
self._queue_query_key: tuple[Any, ...] | None = None
|
||||
self._detail_loading = False
|
||||
self._completion_pending = False
|
||||
self._completion_dialog: AppointmentCompleteDialog | None = None
|
||||
self._detail_requests: set[tuple[int, int]] = set()
|
||||
self._detail_cancel_events: dict[tuple[int, int], Event] = {}
|
||||
self._detail_failed_requests: set[tuple[int, int]] = set()
|
||||
@@ -3679,6 +3754,12 @@ class ReceptionPage(QWidget):
|
||||
self.poll_timer = QTimer(self)
|
||||
self.poll_timer.setInterval(5_000)
|
||||
self.poll_timer.timeout.connect(self._poll_queue)
|
||||
self._refresh_cooldown = QTimer(self)
|
||||
self._refresh_cooldown.setSingleShot(True)
|
||||
self._refresh_cooldown.timeout.connect(self._finish_refresh_cooldown)
|
||||
self.refresh_shortcut = QShortcut(QKeySequence("F5"), self)
|
||||
self.refresh_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
self.refresh_shortcut.activated.connect(self._refresh_workspace)
|
||||
|
||||
def _ensure_diagnosis_dialog(self) -> DiagnosisDialog:
|
||||
dialog = getattr(self, "diagnosis_dialog", None)
|
||||
@@ -3705,6 +3786,14 @@ class ReceptionPage(QWidget):
|
||||
title.setObjectName("ReceptionQueueTitle")
|
||||
header_layout.addWidget(title)
|
||||
header_layout.addStretch(1)
|
||||
self.refresh_button = QPushButton("刷新", header)
|
||||
self.refresh_button.setObjectName("ReceptionRefreshButton")
|
||||
self.refresh_button.setFixedWidth(58)
|
||||
self.refresh_button.setAccessibleName("刷新接诊台")
|
||||
self.refresh_button.setToolTip("刷新队列、当前患者和已保存的 AI 报告(F5)")
|
||||
self.refresh_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.refresh_button.clicked.connect(self._refresh_workspace)
|
||||
header_layout.addWidget(self.refresh_button)
|
||||
self.queue_date_button = _ReceptionDateButton(self._queue_date, header)
|
||||
self.queue_date_button.setObjectName("ReceptionDateButton")
|
||||
self.queue_date_button.setFixedWidth(118)
|
||||
@@ -3756,7 +3845,7 @@ class ReceptionPage(QWidget):
|
||||
self.queue_list.verticalScrollBar().valueChanged.connect(self._on_queue_scroll)
|
||||
self.queue_stack.addWidget(self.queue_list)
|
||||
self.queue_empty = EmptyState("队列为空", "当前筛选下没有待处理患者。", "重新加载")
|
||||
self.queue_empty.action_requested.connect(lambda: self.refresh())
|
||||
self.queue_empty.action_requested.connect(self._refresh_workspace)
|
||||
self.queue_stack.addWidget(self.queue_empty)
|
||||
layout.addWidget(self.queue_stack, 1)
|
||||
self.queue_loading_indicator = _ReceptionQueueLoading(panel)
|
||||
@@ -3827,10 +3916,7 @@ class ReceptionPage(QWidget):
|
||||
patient_head.addLayout(identity, 1)
|
||||
patient_head.addStretch(1)
|
||||
|
||||
self.complete_button = QPushButton("结束问诊", hero)
|
||||
self.complete_button.setObjectName("ReceptionCompleteButton")
|
||||
self.complete_button.setIcon(_painted_reception_action_icon("stop", "#F15B67"))
|
||||
self.complete_button.setIconSize(QSize(14, 14))
|
||||
self.complete_button = _ReceptionCompleteButton(hero)
|
||||
self.complete_button.clicked.connect(self._complete_appointment)
|
||||
self.complete_button.setVisible(self._can_complete)
|
||||
patient_head.addWidget(self.complete_button)
|
||||
@@ -5267,6 +5353,7 @@ class ReceptionPage(QWidget):
|
||||
appointment_id: int,
|
||||
diagnosis_id: int | None,
|
||||
force: bool,
|
||||
read_only_refresh: bool = False,
|
||||
) -> None:
|
||||
if not force and _same_id(patient_id, self._ai_analysis_patient_id):
|
||||
qwen_state = self._ai_analysis_model_states["qwen"]
|
||||
@@ -5313,7 +5400,7 @@ class ReceptionPage(QWidget):
|
||||
self._patient_ai_list_requests.discard(cancelled_key)
|
||||
self._patient_ai_list_cancel_events.pop(cancelled_key, None)
|
||||
self._patient_ai_list_epochs.pop(cancelled_key, None)
|
||||
if self._patient_ai_request_pending(patient_id, "qwen"):
|
||||
if not read_only_refresh and self._patient_ai_request_pending(patient_id, "qwen"):
|
||||
self._sync_ai_analysis_view()
|
||||
return
|
||||
request_key = (request_generation, appointment_id, patient_id)
|
||||
@@ -5333,8 +5420,10 @@ class ReceptionPage(QWidget):
|
||||
patient_id,
|
||||
) or cancel_event.is_set():
|
||||
return _ASYNC_REQUEST_CANCELLED
|
||||
automatic_slot = _AI_AUTOMATIC_REQUEST_SLOTS.acquire(blocking=False)
|
||||
if not automatic_slot:
|
||||
automatic_slot = not read_only_refresh and _AI_AUTOMATIC_REQUEST_SLOTS.acquire(
|
||||
blocking=False
|
||||
)
|
||||
if not read_only_refresh and not automatic_slot:
|
||||
return _ASYNC_REQUEST_DEFERRED
|
||||
try:
|
||||
self._patient_ai_list_started.add(request_key)
|
||||
@@ -5347,15 +5436,18 @@ class ReceptionPage(QWidget):
|
||||
return _ASYNC_REQUEST_CANCELLED
|
||||
return method(patient_id)
|
||||
finally:
|
||||
_AI_AUTOMATIC_REQUEST_SLOTS.release()
|
||||
if automatic_slot:
|
||||
_AI_AUTOMATIC_REQUEST_SLOTS.release()
|
||||
|
||||
run_async(
|
||||
runner = self._run_refresh_read if read_only_refresh else run_async
|
||||
runner(
|
||||
request,
|
||||
on_success=lambda result: self._apply_patient_ai_report_list(
|
||||
result,
|
||||
request_generation,
|
||||
appointment_id,
|
||||
patient_id,
|
||||
read_only_refresh=read_only_refresh,
|
||||
),
|
||||
on_error=lambda error: self._patient_ai_report_list_error(
|
||||
error,
|
||||
@@ -5377,6 +5469,8 @@ class ReceptionPage(QWidget):
|
||||
request_generation: int,
|
||||
appointment_id: int,
|
||||
patient_id: int,
|
||||
*,
|
||||
read_only_refresh: bool = False,
|
||||
) -> None:
|
||||
request_key = (request_generation, appointment_id, patient_id)
|
||||
authoritative = request_key in self._patient_ai_list_requests
|
||||
@@ -5426,6 +5520,12 @@ class ReceptionPage(QWidget):
|
||||
)
|
||||
return
|
||||
|
||||
if read_only_refresh:
|
||||
self._set_ai_analysis_state(
|
||||
"missing", "暂无已保存的患者报告;刷新不会自动生成,可点击重新分析。"
|
||||
)
|
||||
return
|
||||
|
||||
if not self._can_ai_regenerate:
|
||||
self._set_ai_analysis_state(
|
||||
"missing",
|
||||
@@ -6217,14 +6317,111 @@ class ReceptionPage(QWidget):
|
||||
self._reset_queue_state()
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, silent: bool = False) -> None:
|
||||
def _run_refresh_read(
|
||||
self,
|
||||
function: Any,
|
||||
*,
|
||||
on_success: Any,
|
||||
on_error: Any,
|
||||
on_finished: Any,
|
||||
priority: int = 0,
|
||||
) -> None:
|
||||
"""Bound GUI waiting; a timed-out worker may finish but cannot apply data.
|
||||
|
||||
Running HTTP calls are not forcibly terminated. The fixed-size pool
|
||||
bounds concurrency, and expired queued reads never call the backend.
|
||||
"""
|
||||
|
||||
settled = Event()
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.destroyed.connect(lambda _object=None: settled.set())
|
||||
|
||||
def settle(callback: Any, result: Any) -> None:
|
||||
if settled.is_set():
|
||||
return
|
||||
settled.set()
|
||||
timer.stop()
|
||||
timer.deleteLater()
|
||||
try:
|
||||
callback(result)
|
||||
finally:
|
||||
on_finished()
|
||||
|
||||
def request() -> Any:
|
||||
if settled.is_set():
|
||||
return _ASYNC_REQUEST_CANCELLED
|
||||
return function()
|
||||
|
||||
timer.timeout.connect(
|
||||
lambda: settle(on_error, TimeoutError("刷新超时,请检查网络后再次点击刷新。"))
|
||||
)
|
||||
timer.start(_REFRESH_TIMEOUT_MS)
|
||||
run_async(
|
||||
request,
|
||||
on_success=lambda result: settle(on_success, result),
|
||||
on_error=lambda error: settle(on_error, error),
|
||||
on_finished=lambda: settle(
|
||||
on_error, RuntimeError("刷新未返回有效结果,请重试。")
|
||||
),
|
||||
pool=_RECEPTION_REFRESH_POOL,
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
def _cancel_patient_ai_reads(self, patient_id: int | None) -> None:
|
||||
# Reads are normally shared across selection generations. Explicit
|
||||
# refresh revokes their authority, but never forgets an in-flight POST.
|
||||
for key in list(self._patient_ai_list_requests):
|
||||
if patient_id is not None and not _same_id(key[2], patient_id):
|
||||
continue
|
||||
cancel_event = self._patient_ai_list_cancel_events.pop(key, None)
|
||||
if cancel_event is not None:
|
||||
cancel_event.set()
|
||||
self._patient_ai_list_requests.discard(key)
|
||||
self._patient_ai_list_epochs.pop(key, None)
|
||||
self._patient_ai_list_started.discard(key)
|
||||
|
||||
def _refresh_workspace(self) -> None:
|
||||
"""Recover reads without replaying writes or clearing the doctor's draft."""
|
||||
|
||||
if self._refresh_cooldown.isActive():
|
||||
return
|
||||
if self._note_busy or self._completion_pending:
|
||||
show_toast(self, "正在提交,请等待提交结束后再刷新。", "warning")
|
||||
return
|
||||
if self._completion_dialog is not None and self._completion_dialog.isVisible():
|
||||
show_toast(self, "请先关闭完成问诊窗口,再刷新接诊台。", "warning")
|
||||
return
|
||||
if self._selected_appointment_id is not None and not self.notify_button.isEnabled():
|
||||
show_toast(self, "正在通知医助,请稍后刷新。", "warning")
|
||||
return
|
||||
self.refresh_button.setEnabled(False)
|
||||
self.refresh_button.setCursor(Qt.CursorShape.ArrowCursor)
|
||||
self._refresh_cooldown.start(_REFRESH_COOLDOWN_MS)
|
||||
context = self._selection_context()
|
||||
self._cancel_patient_ai_reads(context[3] if context is not None else None)
|
||||
# Invalidate a pending daily-range result before the fresh detail arrives.
|
||||
self._daily_generation += 1
|
||||
self._daily_loading = False
|
||||
self.daily_panel.set_loading(False)
|
||||
record = self._selected_record
|
||||
if record is not None:
|
||||
self._load_detail(record, clear=False, read_only_refresh=True)
|
||||
# A stuck queue must not prevent refreshing the selected patient's detail.
|
||||
self.refresh(workspace_refresh=True)
|
||||
|
||||
def _finish_refresh_cooldown(self) -> None:
|
||||
self.refresh_button.setEnabled(True)
|
||||
self.refresh_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
def refresh(self, silent: bool = False, *, workspace_refresh: bool = False) -> None:
|
||||
"""Replace the queue with page one using a GUI-thread query snapshot."""
|
||||
|
||||
if silent and self._queue_loading:
|
||||
if silent and self._queue_loading and not workspace_refresh:
|
||||
return
|
||||
selected_date = self._queue_date or date.today().isoformat()
|
||||
page_size = self._queue_page_size
|
||||
if silent:
|
||||
if silent or workspace_refresh:
|
||||
loaded = max(len(self._queue_records), self._queue_page * self._queue_page_size)
|
||||
if loaded > page_size:
|
||||
page_size = loaded
|
||||
@@ -6239,12 +6436,15 @@ class ReceptionPage(QWidget):
|
||||
}
|
||||
query_key = self._query_key(query)
|
||||
if query_key != self._queue_query_key:
|
||||
self._clear_selection()
|
||||
if not workspace_refresh:
|
||||
self._clear_selection()
|
||||
self._reset_queue_state()
|
||||
self._queue_query = dict(query)
|
||||
self._queue_query["page_size"] = self._queue_page_size
|
||||
self._queue_query_key = query_key
|
||||
self._request_queue_page(query, append=False, silent=silent)
|
||||
self._request_queue_page(
|
||||
query, append=False, silent=silent, workspace_refresh=workspace_refresh
|
||||
)
|
||||
|
||||
def _poll_queue(self) -> None:
|
||||
"""Do not let the timer supersede an explicit or slower queue request."""
|
||||
@@ -6401,6 +6601,7 @@ class ReceptionPage(QWidget):
|
||||
*,
|
||||
append: bool,
|
||||
silent: bool,
|
||||
workspace_refresh: bool = False,
|
||||
) -> None:
|
||||
self._queue_generation += 1
|
||||
generation = self._queue_generation
|
||||
@@ -6412,12 +6613,18 @@ class ReceptionPage(QWidget):
|
||||
frozen_query = dict(query)
|
||||
page_no = int(frozen_query["page_no"])
|
||||
query_key = self._query_key(frozen_query)
|
||||
run_async(
|
||||
lambda frozen_query=frozen_query: invoke(
|
||||
def request() -> Any:
|
||||
if generation != self._queue_generation:
|
||||
return _ASYNC_REQUEST_CANCELLED
|
||||
return invoke(
|
||||
self.repository,
|
||||
"list_appointments",
|
||||
**frozen_query,
|
||||
),
|
||||
)
|
||||
|
||||
runner = self._run_refresh_read if workspace_refresh else run_async
|
||||
runner(
|
||||
request,
|
||||
on_success=lambda result: self._apply_queue(
|
||||
result,
|
||||
generation,
|
||||
@@ -6425,6 +6632,7 @@ class ReceptionPage(QWidget):
|
||||
append=append,
|
||||
query_key=query_key,
|
||||
silent=silent,
|
||||
workspace_refresh=workspace_refresh,
|
||||
),
|
||||
on_error=lambda error: self._queue_error(error, generation, silent=silent),
|
||||
on_finished=lambda: self._queue_finished(generation),
|
||||
@@ -6439,6 +6647,7 @@ class ReceptionPage(QWidget):
|
||||
append: bool = False,
|
||||
query_key: tuple[Any, ...] | None = None,
|
||||
silent: bool = False,
|
||||
workspace_refresh: bool = False,
|
||||
) -> None:
|
||||
if generation != self._queue_generation or query_key not in (None, self._queue_query_key):
|
||||
return
|
||||
@@ -6480,27 +6689,38 @@ class ReceptionPage(QWidget):
|
||||
),
|
||||
-1,
|
||||
)
|
||||
if row_to_select < 0 and records:
|
||||
preserve_selection = (workspace_refresh or silent) and selected_id is not None
|
||||
if row_to_select < 0 and records and not preserve_selection:
|
||||
row_to_select = 0
|
||||
self.queue_list.blockSignals(True)
|
||||
self._sync_queue_rows(records, append=append)
|
||||
if row_to_select >= 0 and self.queue_list.currentRow() != row_to_select:
|
||||
self.queue_list.setCurrentRow(row_to_select)
|
||||
elif row_to_select < 0:
|
||||
self.queue_list.setCurrentRow(-1)
|
||||
self.queue_list.blockSignals(False)
|
||||
self._sync_queue_row_selection()
|
||||
self.queue_summary.setText(f"已加载 {len(records)} / 共 {self._queue_total} 位患者")
|
||||
self.queue_stack.setCurrentIndex(0 if records else 1)
|
||||
self.queue_banner.clear()
|
||||
if not records:
|
||||
self._clear_selection()
|
||||
if row_to_select < 0:
|
||||
# The patient may have left the queue. Keep the detail and unsaved
|
||||
# draft attached to that patient until the doctor selects another.
|
||||
if preserve_selection:
|
||||
self.queue_banner.show_message(
|
||||
"当前患者已不在筛选队列,已保留详情与未保存内容,可手动选择其他患者。",
|
||||
"info",
|
||||
)
|
||||
else:
|
||||
self._clear_selection()
|
||||
return
|
||||
chosen = records[row_to_select]
|
||||
if selected_id is not None and _same_id(_record_id(chosen), selected_id):
|
||||
self._selected_record = chosen
|
||||
if not append and not silent:
|
||||
if not append and not silent and not workspace_refresh:
|
||||
self._load_detail(chosen, silent=True, clear=False)
|
||||
else:
|
||||
self._select_record(chosen, silent=True)
|
||||
self._select_record(chosen, silent=True, read_only_refresh=workspace_refresh)
|
||||
|
||||
def _update_queue_filter_counts(self, result: Any, records: list[Any]) -> None:
|
||||
extend = get_value(result, "extend", None)
|
||||
@@ -6573,7 +6793,9 @@ class ReceptionPage(QWidget):
|
||||
if isinstance(row, QueueRow):
|
||||
row.set_selected(item is current)
|
||||
|
||||
def _select_record(self, record: Any, *, silent: bool = False) -> None:
|
||||
def _select_record(
|
||||
self, record: Any, *, silent: bool = False, read_only_refresh: bool = False
|
||||
) -> None:
|
||||
appointment_id = _record_id(record)
|
||||
if appointment_id is None:
|
||||
self._clear_selection()
|
||||
@@ -6589,9 +6811,16 @@ class ReceptionPage(QWidget):
|
||||
self._selected_record = record
|
||||
self._selected_appointment_id = appointment_id
|
||||
self._selected_detail = None
|
||||
self._load_detail(record, silent=silent, clear=True)
|
||||
self._load_detail(record, silent=silent, clear=True, read_only_refresh=read_only_refresh)
|
||||
|
||||
def _load_detail(self, record: Any, silent: bool = False, *, clear: bool = True) -> None:
|
||||
def _load_detail(
|
||||
self,
|
||||
record: Any,
|
||||
silent: bool = False,
|
||||
*,
|
||||
clear: bool = True,
|
||||
read_only_refresh: bool = False,
|
||||
) -> None:
|
||||
"""Start a new detail generation even while an older request is running."""
|
||||
|
||||
appointment_id = _record_id(record)
|
||||
@@ -6614,9 +6843,12 @@ class ReceptionPage(QWidget):
|
||||
self.detail_banner.show_message("正在加载患者详情…", "info")
|
||||
self._detail_loading = True
|
||||
self._detail_requests.add(request_key)
|
||||
run_async(
|
||||
runner = self._run_refresh_read if read_only_refresh else run_async
|
||||
runner(
|
||||
lambda: self._fetch_detail_bundle(record, appointment_id, cancel_event),
|
||||
on_success=lambda bundle: self._apply_detail(bundle, generation, appointment_id),
|
||||
on_success=lambda bundle: self._apply_detail(
|
||||
bundle, generation, appointment_id, read_only_refresh=read_only_refresh
|
||||
),
|
||||
on_error=lambda error: self._detail_error(error, generation, appointment_id),
|
||||
on_finished=lambda: self._detail_finished(generation, appointment_id),
|
||||
priority=generation,
|
||||
@@ -6877,7 +7109,12 @@ class ReceptionPage(QWidget):
|
||||
self.daily_panel.set_loading(False)
|
||||
|
||||
def _apply_detail(
|
||||
self, bundle: Any, generation: int, appointment_id: int | None = None
|
||||
self,
|
||||
bundle: Any,
|
||||
generation: int,
|
||||
appointment_id: int | None = None,
|
||||
*,
|
||||
read_only_refresh: bool = False,
|
||||
) -> None:
|
||||
expected_id = appointment_id or self._selected_appointment_id
|
||||
if (
|
||||
@@ -6966,7 +7203,9 @@ class ReceptionPage(QWidget):
|
||||
supports_patient_reports = self._can_patient_ai_read and callable(
|
||||
getattr(self.repository, "list_patient_ai_reports", None)
|
||||
) and callable(getattr(self.repository, "generate_patient_ai_report", None))
|
||||
if patient_id is None and supports_patient_reports:
|
||||
if read_only_refresh:
|
||||
self._refresh_saved_ai_reports(expected_id, diagnosis_id, patient_id)
|
||||
elif patient_id is None and supports_patient_reports:
|
||||
self._ai_analysis_generation += 1
|
||||
self._ai_analysis_loading = False
|
||||
self._ai_analysis_diagnosis_id = None
|
||||
@@ -6988,6 +7227,37 @@ class ReceptionPage(QWidget):
|
||||
patient_id=patient_id,
|
||||
)
|
||||
|
||||
def _refresh_saved_ai_reports(
|
||||
self, appointment_id: int, diagnosis_id: int | None, patient_id: int | None
|
||||
) -> None:
|
||||
self._cancel_patient_ai_reads(patient_id)
|
||||
if self._can_patient_ai_read and patient_id is not None and callable(
|
||||
getattr(self.repository, "list_patient_ai_reports", None)
|
||||
):
|
||||
# A generation is a write with an uncertain outcome while running.
|
||||
# Keep its single-flight identity and do not race it with an older GET.
|
||||
if any(
|
||||
self._patient_ai_request_pending(patient_id, model)
|
||||
for model in AI_ANALYSIS_MODELS
|
||||
):
|
||||
self._ai_analysis_operation_error = "报告生成任务仍在处理中,刷新不会重复生成。"
|
||||
self._sync_ai_analysis_view()
|
||||
return
|
||||
self._load_patient_ai_reports(
|
||||
patient_id,
|
||||
appointment_id=appointment_id,
|
||||
diagnosis_id=diagnosis_id,
|
||||
force=True,
|
||||
read_only_refresh=True,
|
||||
)
|
||||
elif not self._can_ai_analysis:
|
||||
self._set_ai_analysis_state("permission")
|
||||
elif not self._ai_analysis_payloads and not self._ai_analysis_requests:
|
||||
# The legacy analysis endpoint is a POST despite its get_* name.
|
||||
self._set_ai_analysis_state(
|
||||
"error", "当前数据源无法只读刷新 AI 报告;如需生成分析,请点击重试。"
|
||||
)
|
||||
|
||||
def _render_identity(self, appointment: Any, patient: Any, diagnosis: Any) -> None:
|
||||
patient_name = first_value(
|
||||
appointment,
|
||||
@@ -8168,7 +8438,10 @@ class ReceptionPage(QWidget):
|
||||
self.history_button.setEnabled(appointment_id is not None)
|
||||
self.more_button.setEnabled(appointment_id is not None)
|
||||
self.complete_button.setEnabled(
|
||||
self._can_complete and appointment_id is not None and status in RECEPTION_STATUSES
|
||||
self._can_complete
|
||||
and not self._completion_pending
|
||||
and appointment_id is not None
|
||||
and status in RECEPTION_STATUSES
|
||||
)
|
||||
note_enabled = self._can_note and diagnosis_id is not None and not self._note_busy
|
||||
self.note_edit.setEnabled(note_enabled)
|
||||
@@ -8180,6 +8453,10 @@ class ReceptionPage(QWidget):
|
||||
self, error: Exception, generation: int, appointment_id: int | None = None
|
||||
) -> None:
|
||||
expected_id = appointment_id or self._selected_appointment_id
|
||||
if expected_id is not None:
|
||||
cancel_event = self._detail_cancel_events.get((generation, expected_id))
|
||||
if cancel_event is not None:
|
||||
cancel_event.set()
|
||||
if (
|
||||
generation == self._detail_generation
|
||||
and expected_id is not None
|
||||
@@ -8734,6 +9011,12 @@ class ReceptionPage(QWidget):
|
||||
self._load_detail(self._selected_record, silent=True, clear=False)
|
||||
|
||||
def _complete_appointment(self) -> None:
|
||||
if self._completion_pending:
|
||||
return
|
||||
if self._completion_dialog is not None:
|
||||
self._completion_dialog.raise_()
|
||||
self._completion_dialog.activateWindow()
|
||||
return
|
||||
if not self._can_complete:
|
||||
show_toast(self, "当前账号没有完成接诊权限。", "danger")
|
||||
return
|
||||
@@ -8748,24 +9031,75 @@ class ReceptionPage(QWidget):
|
||||
if status not in RECEPTION_STATUSES:
|
||||
show_toast(self, "仅待接诊或已过号记录可以完成接诊。", "danger")
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"确认完成接诊",
|
||||
"系统会再次核对服务端挂号状态;完成后不可撤销。确认继续吗?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
|
||||
QMessageBox.StandardButton.Cancel,
|
||||
dialog = AppointmentCompleteDialog(can_note=self._can_note, parent=self)
|
||||
self._completion_dialog = dialog
|
||||
dialog.submitted.connect(
|
||||
lambda note: self._submit_completion(dialog, generation, appointment_id, note)
|
||||
)
|
||||
if answer != QMessageBox.StandardButton.Yes:
|
||||
dialog.finished.connect(lambda _result: self._close_completion_dialog(dialog))
|
||||
dialog.open()
|
||||
|
||||
def _close_completion_dialog(self, dialog: AppointmentCompleteDialog) -> None:
|
||||
if self._completion_dialog is dialog:
|
||||
self._completion_dialog = None
|
||||
dialog.deleteLater()
|
||||
|
||||
def _submit_completion(
|
||||
self,
|
||||
dialog: AppointmentCompleteDialog,
|
||||
generation: int,
|
||||
appointment_id: int,
|
||||
note: str,
|
||||
) -> None:
|
||||
if self._completion_pending or self._completion_dialog is not dialog:
|
||||
return
|
||||
if not self._context_current(generation, appointment_id):
|
||||
dialog.show_error("当前患者已切换或详情已更新,请关闭窗口后重新操作。")
|
||||
return
|
||||
self._completion_pending = True
|
||||
dialog.set_busy(True)
|
||||
self.complete_button.setEnabled(False)
|
||||
run_async(
|
||||
lambda: self._complete_after_revalidation(appointment_id),
|
||||
on_success=lambda _result: self._appointment_completed(generation, appointment_id),
|
||||
on_error=lambda error: show_toast(self, friendly_error(error), "danger", 4200),
|
||||
on_finished=lambda: self._restore_action_state(generation, appointment_id),
|
||||
lambda: self._complete_after_revalidation(appointment_id, note),
|
||||
on_success=lambda warning: self._completion_succeeded(
|
||||
dialog, generation, appointment_id, warning
|
||||
),
|
||||
on_error=lambda error: self._completion_failed(dialog, error),
|
||||
on_finished=self._completion_finished,
|
||||
)
|
||||
|
||||
def _complete_after_revalidation(self, appointment_id: int) -> Any:
|
||||
def _completion_succeeded(
|
||||
self,
|
||||
dialog: AppointmentCompleteDialog,
|
||||
generation: int,
|
||||
appointment_id: int,
|
||||
warning: str,
|
||||
) -> None:
|
||||
if self._completion_dialog is dialog:
|
||||
if warning:
|
||||
dialog.show_completed_warning(warning)
|
||||
else:
|
||||
dialog.set_busy(False)
|
||||
dialog.accept()
|
||||
self._appointment_completed(generation, appointment_id, warning)
|
||||
|
||||
def _completion_failed(self, dialog: AppointmentCompleteDialog, error: Exception) -> None:
|
||||
if self._completion_dialog is dialog:
|
||||
dialog.show_error(friendly_error(error))
|
||||
|
||||
def _completion_finished(self) -> None:
|
||||
self._completion_pending = False
|
||||
if self._selected_appointment_id is not None:
|
||||
self._restore_action_state(self._detail_generation, self._selected_appointment_id)
|
||||
|
||||
def _complete_after_revalidation(self, appointment_id: int, note: str = "") -> str:
|
||||
if not self._can_complete:
|
||||
raise ValueError("当前账号没有完成接诊权限。")
|
||||
note = note.strip()
|
||||
if note and not self._can_note:
|
||||
raise ValueError("当前账号没有添加医生备注权限。")
|
||||
if len(note) > COMPLETION_NOTE_LIMIT:
|
||||
raise ValueError(f"备注不能超过 {COMPLETION_NOTE_LIMIT} 字。")
|
||||
detail = invoke(
|
||||
self.repository,
|
||||
"reception_detail",
|
||||
@@ -8784,15 +9118,35 @@ class ReceptionPage(QWidget):
|
||||
raise ValueError("服务端挂号记录与当前患者不一致,已停止完成操作")
|
||||
if status not in RECEPTION_STATUSES:
|
||||
raise ValueError("挂号状态已变化,请刷新队列后重试")
|
||||
return invoke(
|
||||
invoke(
|
||||
self.repository,
|
||||
"complete_appointment",
|
||||
appointment_id=appointment_id,
|
||||
id=appointment_id,
|
||||
)
|
||||
|
||||
def _appointment_completed(self, generation: int, appointment_id: int) -> None:
|
||||
show_toast(self, "接诊已完成。", "success")
|
||||
if note:
|
||||
# Use the freshly validated diagnosis, never the patient's ID or
|
||||
# mutable selection: notes belong to the diagnosis timeline.
|
||||
diagnosis = get_value(detail, "diagnosis", None) or {}
|
||||
diagnosis_id = _as_int(first_value(diagnosis, "id", "diagnosis_id", default=None))
|
||||
if diagnosis_id is None or diagnosis_id <= 0:
|
||||
return "问诊已完成,但该预约没有关联诊单,备注未保存"
|
||||
try:
|
||||
invoke(
|
||||
self.repository,
|
||||
"add_doctor_note",
|
||||
diagnosis_id=diagnosis_id,
|
||||
content=note,
|
||||
)
|
||||
except Exception:
|
||||
return "问诊已完成,但备注未保存"
|
||||
return ""
|
||||
|
||||
def _appointment_completed(
|
||||
self, generation: int, appointment_id: int, warning: str = ""
|
||||
) -> None:
|
||||
show_toast(self, warning or "接诊已完成。", "warning" if warning else "success")
|
||||
if self._context_current(generation, appointment_id):
|
||||
self._clear_selection()
|
||||
self.refresh(silent=True)
|
||||
|
||||
Reference in New Issue
Block a user