This commit is contained in:
Your Name
2026-08-12 11:03:28 +08:00
parent 09d3fcbf82
commit bc9ad1d3cd
32 changed files with 4586 additions and 2341 deletions
@@ -8,9 +8,10 @@ from datetime import date, timedelta
from pathlib import Path
from typing import Any
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QTextCursor
from PySide6.QtCore import Qt, QTimer, QUrl, Signal
from PySide6.QtGui import QDesktopServices, QPixmap, QTextCursor
from PySide6.QtWidgets import (
QDialog,
QFileDialog,
QFrame,
QGridLayout,
@@ -22,6 +23,7 @@ from PySide6.QtWidgets import (
QMessageBox,
QPushButton,
QScrollArea,
QSizePolicy,
QSplitter,
QStackedWidget,
QTabBar,
@@ -118,6 +120,13 @@ def _attachment_name(value: object) -> str:
return name or text
def _is_image_attachment(value: object) -> bool:
"""Return whether a server attachment can be previewed as an image."""
path = str(value or "").split("?", 1)[0].split("#", 1)[0].lower()
return path.endswith((".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"))
def _is_local_material_reference(value: str) -> bool:
"""Reject local filesystem references before a note JSON is constructed."""
@@ -139,12 +148,14 @@ class QueueRow(QWidget):
layout.setContentsMargins(12, 9, 12, 9)
layout.setSpacing(5)
top = QHBoxLayout()
top.setSpacing(8)
name = QLabel(
display_text(first_value(record, "patient_name", "name", default="未命名患者"))
)
name.setStyleSheet("font-size:14px; font-weight:700; color:#172033;")
top.addWidget(name)
top.addStretch(1)
name.setMinimumWidth(0)
name.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
top.addWidget(name, 1)
status_number = _as_int(first_value(record, "status", default=1), 1) or 1
badge = StatusBadge(
display_text(
@@ -157,7 +168,12 @@ class QueueRow(QWidget):
),
STATUS_KIND.get(status_number, "neutral"),
)
top.addWidget(badge)
badge.setMinimumWidth(max(54, badge.sizeHint().width()))
top.addWidget(
badge,
0,
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop,
)
layout.addLayout(top)
time = first_value(
record, "appointment_time_text", "appointment_time", "time", default="时间待确认"
@@ -168,6 +184,10 @@ class QueueRow(QWidget):
f"{display_text(first_value(record, 'age'))}岁"
)
meta.setProperty("role", "muted")
# Long timestamps must not dictate the minimum width of the whole
# queue row and push the status badge underneath the viewport edge.
meta.setMinimumWidth(0)
meta.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
layout.addWidget(meta)
assistant = first_value(record, "assistant_name", default=None)
if assistant:
@@ -211,6 +231,7 @@ class ReceptionPage(QWidget):
self._pending_tongue_images: list[str] = []
self._pending_report_files: list[str] = []
self._note_busy = False
self._attachment_preview_generation = 0
self._can_complete = has_permission(self.permissions, "doctor.appointment/complete")
self._can_note = has_permission(self.permissions, "doctor.appointment/addDoctorNote")
@@ -335,7 +356,8 @@ class ReceptionPage(QWidget):
self.notify_button = QPushButton("通知医助")
self.notify_button.clicked.connect(self._notify_assistant)
action_row.addWidget(self.notify_button)
self.video_button = QPushButton("发起视频")
self.video_button = QPushButton("IM 问诊")
self.video_button.setToolTip("打开患者 IM,可发送消息并从会话中发起视频")
self.video_button.setProperty("variant", "secondary")
self.video_button.clicked.connect(self._request_video)
action_row.addWidget(self.video_button)
@@ -1245,13 +1267,25 @@ class ReceptionPage(QWidget):
("report_files", "检查报告"),
):
for path in _sequence(first_value(note, image_type, default=[])):
path_text = str(path).strip()
attachment = QWidget()
attachment_layout = QHBoxLayout(attachment)
attachment_layout.setContentsMargins(0, 0, 0, 0)
label = QLabel(f"{caption}:{_attachment_name(path)}")
label.setToolTip(str(path))
label = QLabel(f"{caption}:{_attachment_name(path_text)}")
label.setToolTip(path_text)
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
attachment_layout.addWidget(label, 1)
preview_button = QPushButton(
"预览" if _is_image_attachment(path_text) else "打开"
)
preview_button.setProperty("variant", "secondary")
preview_button.setEnabled(bool(path_text))
preview_button.clicked.connect(
lambda _checked=False, current_path=path_text, button=preview_button: (
self._preview_note_attachment(current_path, button)
)
)
attachment_layout.addWidget(preview_button)
if self._can_note and note_id is not None:
delete_button = QPushButton("删除")
delete_button.setProperty("variant", "secondary")
@@ -1266,6 +1300,97 @@ class ReceptionPage(QWidget):
layout.addWidget(attachment)
self.notes_layout.addWidget(card)
def _preview_note_attachment(self, path: str, button: QPushButton) -> None:
"""Preview note images in-app and open non-image reports safely."""
target = str(path or "").strip()
if not target:
return
if not _is_image_attachment(target):
url = QUrl(target)
if (
not url.isValid()
or url.scheme().lower() not in {"https", "http"}
or not url.host()
):
show_toast(self, "附件地址无效,无法打开。", "warning", 4200)
return
if not QDesktopServices.openUrl(url):
show_toast(self, "系统未能打开该附件。", "danger", 4200)
return
download = getattr(self.repository, "download_public_image", None)
if not callable(download):
show_toast(self, "当前数据源不支持图片预览。", "warning", 4200)
return
self._attachment_preview_generation += 1
generation = self._attachment_preview_generation
button.setEnabled(False)
button.setText("加载中…")
run_async(
lambda: invoke(self.repository, "download_public_image", url=target),
on_success=lambda payload: self._show_note_image_preview(
target, payload, button, generation
),
on_error=lambda error: self._note_image_preview_failed(
error, button, generation
),
)
def _show_note_image_preview(
self,
path: str,
payload: Any,
button: QPushButton,
generation: int,
) -> None:
if generation != self._attachment_preview_generation:
return
button.setEnabled(True)
button.setText("预览")
content = bytes(payload or b"")
pixmap = QPixmap()
if not content or len(content) > 10 * 1024 * 1024 or not pixmap.loadFromData(content):
show_toast(self, "服务器返回的图片无法预览。", "danger", 4600)
return
dialog = QDialog(self)
dialog.setWindowTitle(f"预览 · {_attachment_name(path)}")
dialog.setModal(True)
dialog.resize(920, 680)
layout = QVBoxLayout(dialog)
layout.setContentsMargins(14, 14, 14, 14)
scroll = QScrollArea(dialog)
scroll.setWidgetResizable(True)
image = QLabel()
image.setAlignment(Qt.AlignmentFlag.AlignCenter)
image.setPixmap(
pixmap.scaled(
880,
620,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
)
scroll.setWidget(image)
layout.addWidget(scroll, 1)
close_button = QPushButton("关闭")
close_button.setProperty("variant", "primary")
close_button.clicked.connect(dialog.accept)
layout.addWidget(close_button, 0, Qt.AlignmentFlag.AlignRight)
dialog.exec()
def _note_image_preview_failed(
self,
error: Exception,
button: QPushButton,
generation: int,
) -> None:
if generation != self._attachment_preview_generation:
return
button.setEnabled(True)
button.setText("预览")
show_toast(self, f"图片预览失败:{friendly_error(error)}", "danger", 5200)
def _reset_detail_content(self, seed: Any = None) -> None:
self.patient_name_label.setText(
display_text(first_value(seed, "patient_name", "name", default="—"))
@@ -1753,6 +1878,7 @@ class ReceptionPage(QWidget):
"patient_name",
default=first_value(diagnosis, "patient_name", default="患者"),
),
"mode": "im",
"record": self._selected_record,
}
self.video_requested.emit(payload)