This commit is contained in:
Your Name
2026-09-01 10:42:16 +08:00
parent 456dd667df
commit 5bd5eae62d
358 changed files with 2276 additions and 495 deletions
+400 -46
View File
@@ -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)