This commit is contained in:
Your Name
2026-09-09 12:18:17 +08:00
parent 0cea43b027
commit 40319eea41
76 changed files with 4821 additions and 268 deletions
@@ -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():