更新
This commit is contained in:
@@ -59,7 +59,12 @@ from ..diagnosis_drawer import (
|
||||
set_tag_item,
|
||||
)
|
||||
from ..diagnosis_editors import DailyRecordEditorDialog
|
||||
from ..diagnosis_media import RecordingPlaybackCell, RecordingPlayerDialog
|
||||
from ..diagnosis_media import (
|
||||
ImagePreviewDialog,
|
||||
RecordingPlaybackCell,
|
||||
RecordingPlayerDialog,
|
||||
safe_image_sources,
|
||||
)
|
||||
from ..infinite_list import InfiniteList
|
||||
from ..widgets import (
|
||||
display_text,
|
||||
@@ -832,6 +837,8 @@ class DiagnosisDialog(QDialog):
|
||||
self._saving = False
|
||||
self._load_mode = "readonly"
|
||||
self._generation = 0
|
||||
self._dictionary_requested_generation = -1
|
||||
self._dictionary_error_message = ""
|
||||
self._save_generation = 0
|
||||
self._orders_generation = 0
|
||||
self._order_detail_generation = 0
|
||||
@@ -846,6 +853,7 @@ class DiagnosisDialog(QDialog):
|
||||
self._loading_tabs: set[str] = set()
|
||||
self._daily_todo_status: int | None = None
|
||||
self._recording_players: list[RecordingPlayerDialog] = []
|
||||
self._image_preview: ImagePreviewDialog | None = None
|
||||
self._inline_recording_cells: list[RecordingPlaybackCell] = []
|
||||
self._last_order_detail_dialog: QDialog | None = None
|
||||
self._local_audio_dialog: LocalAudioQueueDialog | None = None
|
||||
@@ -965,6 +973,7 @@ class DiagnosisDialog(QDialog):
|
||||
self._add_readonly_section("daily", "日常记录", daily)
|
||||
notes = NotesTimeline(editable=False)
|
||||
notes.open_attachment_requested.connect(self._open_safe_resource)
|
||||
notes.preview_images_requested.connect(self._preview_note_images)
|
||||
self._notes_timelines.append(notes)
|
||||
self._add_readonly_section("notes", "医生备注 & 舌苔照片 & 检查报告", notes)
|
||||
orders = self._new_table("orders", "DiagnosisReadonlyOrdersTable")
|
||||
@@ -1437,6 +1446,7 @@ class DiagnosisDialog(QDialog):
|
||||
timeline.upload_requested.connect(self._upload_doctor_note_material)
|
||||
timeline.delete_attachment_requested.connect(self._delete_doctor_note_attachment)
|
||||
timeline.open_attachment_requested.connect(self._open_safe_resource)
|
||||
timeline.preview_images_requested.connect(self._preview_note_images)
|
||||
self.drawer_notes_timeline = timeline
|
||||
self._notes_timelines.append(timeline)
|
||||
return self._wrap_tab("DiagnosisTabNotes", timeline)
|
||||
@@ -1698,22 +1708,23 @@ class DiagnosisDialog(QDialog):
|
||||
if hasattr(self, "readonly_ai_button"):
|
||||
self.readonly_ai_button.setVisible(can_open_diagnosis_ai_report(self.permissions))
|
||||
self.readonly_ai_button.setEnabled(self._diagnosis_id > 0)
|
||||
current_key = self.tabs.tabBar().tabData(self.tabs.currentIndex())
|
||||
self.tabs.clear()
|
||||
for key, label, codes in _TAB_DEFINITIONS:
|
||||
if not self._tab_allowed(codes):
|
||||
continue
|
||||
index = self.tabs.addTab(self._tab_pages[key], label)
|
||||
self.tabs.tabBar().setTabData(index, key)
|
||||
target = next(
|
||||
(
|
||||
index
|
||||
for index in range(self.tabs.count())
|
||||
if self.tabs.tabBar().tabData(index) == current_key
|
||||
),
|
||||
0,
|
||||
)
|
||||
self.tabs.setCurrentIndex(target)
|
||||
previous_key = self._current_tab_key()
|
||||
allowed_tabs = [
|
||||
(key, label) for key, label, codes in _TAB_DEFINITIONS if self._tab_allowed(codes)
|
||||
]
|
||||
if [self.tabs.tabBar().tabData(i) for i in range(self.tabs.count())] != [
|
||||
key for key, _label in allowed_tabs
|
||||
]:
|
||||
blocked = self.tabs.blockSignals(True)
|
||||
self.tabs.clear()
|
||||
target = 0
|
||||
for key, label in allowed_tabs:
|
||||
index = self.tabs.addTab(self._tab_pages[key], label)
|
||||
self.tabs.tabBar().setTabData(index, key)
|
||||
if key == previous_key:
|
||||
target = index
|
||||
self.tabs.setCurrentIndex(target)
|
||||
self.tabs.blockSignals(blocked)
|
||||
section_codes = {key: codes for key, _label, codes in _TAB_DEFINITIONS}
|
||||
for key, section in self._readonly_sections.items():
|
||||
section.setVisible(self._tab_allowed(section_codes.get(key, ())))
|
||||
@@ -1743,6 +1754,9 @@ class DiagnosisDialog(QDialog):
|
||||
self._sync_order_offset_actions()
|
||||
for panel in self._chat_panels:
|
||||
panel.sync_button.setVisible(self._can_chat_sync)
|
||||
self._sync_save_button()
|
||||
if self._current_tab_key() != previous_key:
|
||||
self._tab_changed(self.tabs.currentIndex())
|
||||
|
||||
def _show_message(self, text: str, kind: str = "info", action_text: str = "") -> None:
|
||||
self.drawer_banner.show_message(text, kind, action_text)
|
||||
@@ -1751,6 +1765,9 @@ class DiagnosisDialog(QDialog):
|
||||
self.readonly_error.show()
|
||||
|
||||
def _clear_message(self) -> None:
|
||||
if self._dictionary_error_message and self._authoritative_detail_loaded:
|
||||
self._show_message(self._dictionary_error_message, "warning", action_text="重试")
|
||||
return
|
||||
self.drawer_banner.clear()
|
||||
self.readonly_error.hide()
|
||||
|
||||
@@ -1996,6 +2013,7 @@ class DiagnosisDialog(QDialog):
|
||||
)
|
||||
self._authoritative_detail_loaded = False
|
||||
self._saving = False
|
||||
self._dictionary_error_message = ""
|
||||
self._generation += 1
|
||||
self._save_generation += 1
|
||||
self._orders_generation += 1
|
||||
@@ -2125,14 +2143,26 @@ class DiagnosisDialog(QDialog):
|
||||
),
|
||||
0,
|
||||
)
|
||||
return {"detail": detail, "patient_id": patient_id}
|
||||
|
||||
def _load_dictionary_choices(self) -> dict[str, Any]:
|
||||
"""Load optional editor labels without extending the authoritative-detail gate."""
|
||||
dictionaries: dict[str, list[tuple[str, Any]]] = {}
|
||||
dictionary_errors: list[str] = []
|
||||
if callable(getattr(self.repository, "get_dictionary", None)):
|
||||
dictionary_types = {
|
||||
dictionary_types = sorted(
|
||||
{
|
||||
"diagnosis_type",
|
||||
*(item[0] for item in _CHOICE_DICTIONARIES.values()),
|
||||
}
|
||||
for dictionary_type in sorted(dictionary_types):
|
||||
)
|
||||
if callable(getattr(self.repository, "get_dictionaries", None)):
|
||||
raw = invoke(self.repository, "get_dictionaries", dictionary_types=dictionary_types)
|
||||
dictionaries = {
|
||||
key: _dictionary_choices(get_value(raw, key, []), key)
|
||||
for key in dictionary_types
|
||||
}
|
||||
elif callable(getattr(self.repository, "get_dictionary", None)):
|
||||
for dictionary_type in dictionary_types:
|
||||
try:
|
||||
raw = invoke(
|
||||
self.repository,
|
||||
@@ -2143,12 +2173,41 @@ class DiagnosisDialog(QDialog):
|
||||
except Exception as error:
|
||||
dictionary_errors.append(f"{dictionary_type} 字典:{friendly_error(error)}")
|
||||
return {
|
||||
"detail": detail,
|
||||
"patient_id": patient_id,
|
||||
"dictionaries": dictionaries,
|
||||
"dictionary_errors": dictionary_errors,
|
||||
}
|
||||
|
||||
def _ensure_dictionary_loaded(self, *, force: bool = False) -> None:
|
||||
if self._standalone_readonly or not self._authoritative_detail_loaded:
|
||||
return
|
||||
generation = self._generation
|
||||
if not force and self._dictionary_requested_generation == generation:
|
||||
return
|
||||
self._dictionary_requested_generation = generation
|
||||
clear_error = self.drawer_banner.label.text() == self._dictionary_error_message
|
||||
self._dictionary_error_message = ""
|
||||
if clear_error:
|
||||
self._clear_message()
|
||||
run_async(
|
||||
self._load_dictionary_choices,
|
||||
on_success=lambda result: self._dictionary_choices_loaded(result, generation),
|
||||
on_error=lambda error: self._dictionary_choices_loaded(
|
||||
{"dictionary_errors": [f"病历选项加载失败:{friendly_error(error)}"]}, generation
|
||||
),
|
||||
)
|
||||
|
||||
def _dictionary_choices_loaded(self, result: Any, generation: int) -> None:
|
||||
if generation != self._generation or not self._authoritative_detail_loaded:
|
||||
return
|
||||
# A clinician may already have edited fields while the optional labels load.
|
||||
# Update only choices; re-rendering the detail would overwrite that draft.
|
||||
self._apply_dictionary_choices(get_value(result, "dictionaries", {}) or {}, preserve=True)
|
||||
errors = get_value(result, "dictionary_errors", []) or []
|
||||
if errors:
|
||||
self._dictionary_requested_generation = -1
|
||||
self._dictionary_error_message = ";".join(str(item) for item in errors)
|
||||
self._show_message(self._dictionary_error_message, "warning", action_text="重试")
|
||||
|
||||
def _query_orders(self, diagnosis_id: int, patient_id: int, page: int) -> Any:
|
||||
filters: dict[str, Any] = {
|
||||
"context_diagnosis_id": diagnosis_id,
|
||||
@@ -2170,12 +2229,12 @@ class DiagnosisDialog(QDialog):
|
||||
detail = get_value(result, "detail", None)
|
||||
self._detail = detail
|
||||
self._patient_id = _int(get_value(result, "patient_id", 0), 0)
|
||||
self._apply_dictionary_choices(get_value(result, "dictionaries", {}) or {})
|
||||
self._authoritative_detail_loaded = True
|
||||
self._show_authoritative_content(True)
|
||||
self._render(detail, [], [])
|
||||
self._sync_form_interactivity()
|
||||
self._sync_save_button()
|
||||
self._set_loading(False)
|
||||
errors = get_value(result, "dictionary_errors", []) or []
|
||||
if errors:
|
||||
self._show_message(";".join(str(item) for item in errors), "warning")
|
||||
@@ -2202,7 +2261,9 @@ class DiagnosisDialog(QDialog):
|
||||
if generation == self._generation:
|
||||
self._set_loading(False)
|
||||
|
||||
def _apply_dictionary_choices(self, dictionaries: Mapping[str, Any]) -> None:
|
||||
def _apply_dictionary_choices(
|
||||
self, dictionaries: Mapping[str, Any], *, preserve: bool = False
|
||||
) -> None:
|
||||
diagnosis_type_editor = self.edit_fields.get("diagnosis_type")
|
||||
if isinstance(diagnosis_type_editor, DiagnosisComboBox):
|
||||
labels = {
|
||||
@@ -2219,15 +2280,17 @@ class DiagnosisDialog(QDialog):
|
||||
("会诊", "consultation"),
|
||||
)
|
||||
),
|
||||
preserve=False,
|
||||
preserve=preserve,
|
||||
)
|
||||
for key, (dictionary_type, _multiple) in _CHOICE_DICTIONARIES.items():
|
||||
if dictionary_type not in dictionaries:
|
||||
continue
|
||||
editor = self._choice_fields.get(key)
|
||||
choices = list(dictionaries.get(dictionary_type, []))
|
||||
if not _multiple:
|
||||
choices.insert(0, ("无", ""))
|
||||
if editor is not None:
|
||||
editor.set_choices(choices, preserve=False)
|
||||
editor.set_choices(choices, preserve=preserve)
|
||||
# Defer until the drawer has a real width so wrapped chips get height.
|
||||
QTimer.singleShot(0, self._sync_choice_field_heights)
|
||||
|
||||
@@ -2306,9 +2369,14 @@ class DiagnosisDialog(QDialog):
|
||||
if not self._authoritative_detail_loaded:
|
||||
self._retry_detail()
|
||||
return
|
||||
if (self._dictionary_error_message
|
||||
and self.drawer_banner.label.text() == self._dictionary_error_message):
|
||||
self._ensure_dictionary_loaded(force=True)
|
||||
return
|
||||
self._ensure_tab_loaded(self._current_tab_key(), force=True)
|
||||
|
||||
def _invalidate_requests(self) -> None:
|
||||
self._close_image_preview()
|
||||
self._stop_watching_local_audio_uploads()
|
||||
self._video_reload_pending = False
|
||||
self._generation += 1
|
||||
@@ -2627,7 +2695,10 @@ class DiagnosisDialog(QDialog):
|
||||
table.set_empty_text(message)
|
||||
|
||||
def _ensure_tab_loaded(self, key: str, *, force: bool = False) -> None:
|
||||
if key == "basic" or not self._authoritative_detail_loaded or self._diagnosis_id <= 0:
|
||||
if not self._authoritative_detail_loaded or self._diagnosis_id <= 0:
|
||||
return
|
||||
if key == "basic":
|
||||
self._ensure_dictionary_loaded(force=force)
|
||||
return
|
||||
if key == "daily":
|
||||
source = next(
|
||||
@@ -3421,6 +3492,27 @@ class DiagnosisDialog(QDialog):
|
||||
self._sync_order_offset_actions()
|
||||
self._mutation_error(error, diagnosis_id, generation, "offset")
|
||||
|
||||
def _close_image_preview(self) -> None:
|
||||
preview, self._image_preview = self._image_preview, None
|
||||
if preview is not None:
|
||||
with suppress(RuntimeError):
|
||||
preview.close()
|
||||
|
||||
def _image_preview_finished(self, _result: int) -> None:
|
||||
if self.sender() is self._image_preview:
|
||||
self._image_preview = None
|
||||
|
||||
def _preview_note_images(self, sources: Sequence[str], index: int) -> None:
|
||||
"""Keep the diagnosis open while viewing the clicked note's image group."""
|
||||
if not safe_image_sources(sources):
|
||||
QMessageBox.warning(self, "无法预览", "仅支持 HTTP(S) 服务端图片。")
|
||||
return
|
||||
self._close_image_preview()
|
||||
preview = ImagePreviewDialog(sources, index=index, title="舌象图片预览", parent=self)
|
||||
self._image_preview = preview
|
||||
preview.finished.connect(self._image_preview_finished)
|
||||
preview.open()
|
||||
|
||||
def _open_safe_resource(self, target: str) -> None:
|
||||
url = QUrl(str(target).strip())
|
||||
if not url.isValid() or url.scheme().lower() not in {"https", "http"} or not url.host():
|
||||
|
||||
Reference in New Issue
Block a user