This commit is contained in:
Your Name
2026-11-06 11:48:42 +08:00
parent 0d36a6e840
commit 5926083221
7 changed files with 483 additions and 18 deletions
@@ -31,6 +31,7 @@ from PySide6.QtWidgets import (
QFrame,
QGridLayout,
QHBoxLayout,
QHeaderView,
QInputDialog,
QLabel,
QMessageBox,
@@ -977,6 +978,7 @@ class DiagnosisDialog(QDialog):
self.readonly_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.readonly_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
self.readonly_scroll.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.readonly_scroll.viewport().installEventFilter(self)
self.readonly_scroll.verticalScrollBar().setSingleStep(28)
content = QWidget()
self.readonly_content_layout = QVBoxLayout(content)
@@ -1001,6 +1003,25 @@ class DiagnosisDialog(QDialog):
self.readonly_content_layout.addWidget(self.readonly_error)
self.readonly_patient_card = self._build_readonly_patient_card()
self.readonly_content_layout.addWidget(self.readonly_patient_card)
self.readonly_prescriptions_table = self._new_table(
"prescription", "DiagnosisReadonlyPrescriptionsTable"
)
# Keep the open action visible beside the editor; full prescription
# contents remain available through the existing detail dialog.
self.readonly_prescriptions_table.setColumnHidden(1, True)
self.readonly_prescriptions_table.setColumnHidden(3, True)
self.readonly_prescriptions_table.horizontalHeaderItem(0).setText("开方日期")
self.readonly_prescriptions_table.horizontalHeaderItem(2).setText("临床诊断")
for column, width in ((0, 100), (4, 70), (5, 84), (6, 64)):
self.readonly_prescriptions_table.setColumnWidth(column, width)
self.readonly_prescriptions_table.horizontalHeader().setSectionResizeMode(
2, QHeaderView.ResizeMode.Stretch
)
self.readonly_prescriptions_table.setMinimumHeight(220)
self._add_readonly_section("prescription", "历史处方", self.readonly_prescriptions_table)
self.readonly_content_layout.setAlignment(
self._readonly_sections["prescription"], Qt.AlignmentFlag.AlignLeft
)
self.case_grid = CaseGrid()
self.readonly_content_layout.addWidget(self.case_grid)
@@ -1993,6 +2014,17 @@ class DiagnosisDialog(QDialog):
self._sync_host_geometry()
def eventFilter(self, watched: Any, event: QEvent) -> bool: # noqa: N802 - Qt API
if (
hasattr(self, "readonly_scroll")
and watched is self.readonly_scroll.viewport()
and event.type() == QEvent.Type.Resize
and "prescription" in self._readonly_sections
):
# Other patient cards can have a wider minimum size; the history
# actions must still fit inside the visible side pane.
self._readonly_sections["prescription"].setFixedWidth(
max(0, self.readonly_scroll.viewport().width() - 32)
)
if watched is self._owner and event.type() in {
QEvent.Type.Resize,
QEvent.Type.Move,
@@ -2747,7 +2779,7 @@ class DiagnosisDialog(QDialog):
def _load_visible_readonly_sections(self) -> None:
permission_map = {key: codes for key, _label, codes in _TAB_DEFINITIONS}
for key in ("notes", "daily", "orders", "video", "chat", "assign", "appointment"):
for key in ("prescription", "notes", "daily", "orders", "video", "chat", "assign", "appointment"):
if self._tab_allowed(permission_map.get(key, ())):
self._ensure_tab_loaded(key)
@@ -3853,12 +3885,15 @@ class DiagnosisDialog(QDialog):
first_value(row, "doctor_name"),
"已作废"
if voided
else first_value(row, "status_text", "audit_status_text", default="正常"),
else first_value(
row, "status_text", "audit_status_text",
default=_audit_text(first_value(row, "audit_status", default=None)),
),
"",
)
)
for table in self._table_registry["prescription"]:
table.set_empty_text("暂无病历记录,开方后会自动显示。")
table.set_empty_text("暂无历史处方,开方后会自动显示。")
table.set_matrix(matrix)
self._semanticize(table, 5)
for row_index, row in enumerate(rows):
@@ -3875,6 +3910,7 @@ class DiagnosisDialog(QDialog):
voided = _truthy(first_value(row, "void_status", "is_void", default=False))
can_edit = (
self._editable
and table is not self.readonly_prescriptions_table
and self._can_edit_prescription
and (not approved or voided)
)
@@ -3958,6 +3994,10 @@ class DiagnosisDialog(QDialog):
if actual_id != prescription_id:
self._show_message("服务端处方与当前行不一致,已停止打开。", "danger")
return
detail_diagnosis_id = _int(first_value(prescription, "diagnosis_id", default=0), 0)
if self._diagnosis_id > 0 and detail_diagnosis_id != self._diagnosis_id:
self._show_message("该处方不属于当前诊单,已停止打开。", "danger")
return
self._clear_message()
if edit:
self._open_prescription_edit_dialog(prescription, prescription_id)
@@ -40,6 +40,7 @@ from PySide6.QtWidgets import (
from ...core import UserProfile
from ...core.appointment_modes import appointment_type_value, can_appointment_video
from ...core.errors import ApiBusinessError
from .. import icons
from ..consultations_style import consultations_stylesheet
from ..diagnosis_index_widgets import (
@@ -2235,9 +2236,15 @@ class ConsultationsPage(QWidget):
self._departments_loading = False
self._departments_loaded = False
self.department_combo.clear()
self.department_combo.addItem("部门加载失败", "")
permission_denied = isinstance(error, ApiBusinessError) and any(
text in str(error) for text in ("权限不足", "无权限", "无权")
)
self.department_combo.addItem("暂无部门筛选权限" if permission_denied else "部门加载失败", "")
self.department_combo.setEnabled(False)
message = f"部门加载失败:{friendly_error(error)}。点击重试。"
message = (
"当前账号无权读取部门,请联系管理员检查部门筛选权限,处理后点击重试。"
if permission_denied else f"部门加载失败:{friendly_error(error)}。点击重试。"
)
self.department_combo.setToolTip(message)
self.department_retry_button.setToolTip(message)
self.department_retry_button.show()
@@ -8,12 +8,24 @@ from html import escape
from math import ceil
from typing import Any
from PySide6.QtCore import QDateTime, QEvent, QModelIndex, QRectF, QSize, Qt, QTimer
from PySide6.QtCore import (
QDateTime,
QEvent,
QModelIndex,
QPersistentModelIndex,
QPointF,
QRectF,
QSize,
Qt,
QTimer,
)
from PySide6.QtGui import (
QColor,
QFont,
QFontMetrics,
QIcon,
QKeySequence,
QMouseEvent,
QPainter,
QPen,
QTextDocument,
@@ -44,6 +56,7 @@ from PySide6.QtWidgets import (
QStyledItemDelegate,
QStyleOptionButton,
QStyleOptionViewItem,
QTextEdit,
QToolTip,
QVBoxLayout,
QWidget,
@@ -969,7 +982,184 @@ class _PrescriptionNumberBlueDelegate(_PrescriptionNumberDelegate):
return QSize(width, max(80, ceil(document.size().height()) + 28))
class _PrescriptionCopyText(QTextEdit):
"""One transient, read-only text surface for the active painted cell."""
def __init__(self, table: _PrescriptionBlueTable) -> None:
super().__init__(table.viewport())
self.table = table
self.setObjectName("PrescriptionCopyText")
self.setReadOnly(True)
self.setFrameShape(QFrame.Shape.NoFrame)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setStyleSheet(
f"QTextEdit#PrescriptionCopyText {{background:{PALETTE['accent_soft']};"
f"color:{PALETTE['text']};border:0;padding:0;"
f"selection-background-color:{PALETTE['accent']};selection-color:white;}}"
)
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(
lambda point: table.customContextMenuRequested.emit(self.viewport().mapTo(table.viewport(), point))
)
self.hide()
def keyPressEvent(self, event: Any) -> None: # noqa: N802
if event.matches(QKeySequence.StandardKey.Copy):
if self.textCursor().hasSelection():
self.copy()
else:
self.table.copy_cell()
event.accept()
elif event.key() == Qt.Key.Key_Escape:
self.table._hide_copy_text()
self.table.setFocus()
event.accept()
else:
super().keyPressEvent(event)
def wheelEvent(self, event: Any) -> None: # noqa: N802
self.table._hide_copy_text()
QApplication.sendEvent(self.table.viewport(), event)
class _PrescriptionBlueTable(SortableTable):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._copy_index = QPersistentModelIndex()
self._copy_drag = False
self.copy_text = _PrescriptionCopyText(self)
for signal in (
self.model().modelAboutToBeReset, self.model().layoutAboutToBeChanged,
self.model().rowsAboutToBeRemoved, self.model().rowsAboutToBeInserted,
self.horizontalScrollBar().valueChanged, self.verticalScrollBar().valueChanged,
self.horizontalHeader().sectionResized, self.horizontalHeader().sectionMoved,
):
signal.connect(self._hide_copy_text)
self.model().dataChanged.connect(self._copy_data_changed)
self.selectionModel().currentChanged.connect(self._copy_current_changed)
@staticmethod
def _copyable(index: QModelIndex) -> bool:
return index.isValid() and index.column() in (1, 3, 4, 5, 6, 7, 8, 9, 10)
def cell_copy_text(self, index: QModelIndex) -> str:
if not self._copyable(index):
return ""
if index.column() == 1:
return str(index.data(Qt.ItemDataRole.AccessibleTextRole) or index.data() or "")
return str(index.data() or "")
def row_copy_text(self, row: int) -> str:
header = self.horizontalHeader()
return "\t".join(
self.cell_copy_text(self.model().index(row, column)).replace("\n", " ")
for column in (header.logicalIndex(visual) for visual in range(self.columnCount()))
if column not in (0, 2) and not self.isColumnHidden(column)
)
def copy_cell(self) -> None:
text = self.cell_copy_text(self.currentIndex())
if text:
QApplication.clipboard().setText(text)
def _hide_copy_text(self, *_args: Any) -> None:
self.copy_text.hide()
self._copy_index = QPersistentModelIndex()
self._copy_drag = False
def _copy_current_changed(self, current: QModelIndex, _previous: QModelIndex) -> None:
if current != self._copy_index:
self._hide_copy_text()
def _copy_data_changed(self, top: QModelIndex, bottom: QModelIndex, *_args: Any) -> None:
if (self._copy_index.isValid()
and top.row() <= self._copy_index.row() <= bottom.row()
and top.column() <= self._copy_index.column() <= bottom.column()):
self._hide_copy_text()
def _show_copy_text(self, index: QModelIndex) -> None:
self._copy_index = QPersistentModelIndex(index)
self.copy_text.setGeometry(self.visualRect(index).adjusted(1, 1, -1, -1))
text = self.cell_copy_text(index)
if index.column() == 5:
parts = text.rsplit(" · ", 2)
text = parts[0] + ("\n" + " · ".join(parts[1:]) if len(parts) > 1 else "")
elif index.column() == 10:
text = "\n".join(text.rsplit(" ", 1))
font = QFont(body_family())
font.setPixelSize(14)
self.copy_text.setFont(font)
self.copy_text.setPlainText(text)
document = self.copy_text.document()
document.setDocumentMargin(0)
document.setTextWidth(max(1, self.copy_text.width() - 24))
top = max(11, round((self.copy_text.height() - document.size().height()) / 2))
self.copy_text.setViewportMargins(11, top, 11, 0)
self.copy_text.show()
self.copy_text.raise_()
self.copy_text.setFocus(Qt.FocusReason.MouseFocusReason)
def _forward_copy_mouse(self, event: QMouseEvent) -> None:
target = self.copy_text.viewport()
local = QPointF(target.mapFromGlobal(event.globalPosition().toPoint()))
forwarded = QMouseEvent(
event.type(), local, event.globalPosition(), event.button(), event.buttons(), event.modifiers()
)
QApplication.sendEvent(target, forwarded)
event.accept()
def mousePressEvent(self, event: QMouseEvent) -> None: # noqa: N802
index = self.indexAt(event.position().toPoint())
self._hide_copy_text()
super().mousePressEvent(event)
if event.button() == Qt.MouseButton.LeftButton and self._copyable(index):
self._show_copy_text(index)
self._copy_drag = True
self._forward_copy_mouse(event)
def mouseMoveEvent(self, event: QMouseEvent) -> None: # noqa: N802
if self._copy_drag:
self._forward_copy_mouse(event)
return
self.viewport().setCursor(
Qt.CursorShape.IBeamCursor if self._copyable(self.indexAt(event.position().toPoint()))
else Qt.CursorShape.ArrowCursor
)
super().mouseMoveEvent(event)
def mouseDoubleClickEvent(self, event: QMouseEvent) -> None: # noqa: N802
index = self.indexAt(event.position().toPoint())
if event.button() == Qt.MouseButton.LeftButton and self._copyable(index):
self.setCurrentIndex(index)
if self._copy_index != index:
self._show_copy_text(index)
self._forward_copy_mouse(event)
return
super().mouseDoubleClickEvent(event)
def mouseReleaseEvent(self, event: QMouseEvent) -> None: # noqa: N802
if self._copy_drag:
self._forward_copy_mouse(event)
self._copy_drag = False
return
super().mouseReleaseEvent(event)
def keyPressEvent(self, event: Any) -> None: # noqa: N802
if event.matches(QKeySequence.StandardKey.Copy):
if self._copyable(self.currentIndex()):
self.copy_cell()
elif self.currentRow() >= 0:
QApplication.clipboard().setText(self.row_copy_text(self.currentRow()))
event.accept()
return
super().keyPressEvent(event)
def resizeEvent(self, event: Any) -> None: # noqa: N802
if hasattr(self, "copy_text"):
self._hide_copy_text()
super().resizeEvent(event)
def setCellWidget(self, row: int, column: int, widget: QWidget) -> None: # noqa: N802
super().setCellWidget(row, column, widget)
if column == 2:
@@ -1827,18 +2017,26 @@ class PrescriptionsPage(QWidget):
present_issued_prescription_ai(self.repository, self.permissions, self, prescription_id=value)
def _open_ai_context_menu(self, position: Any) -> None:
if not can_open_issued_ai(self.permissions) or not callable(getattr(self.repository, "list_prescription_ai_reports", None)):
return
item = self.table.itemAt(position)
if item is None:
return
self.table.selectRow(item.row())
self.table.setCurrentItem(item)
row = self._selected()
if _is_blank_prescription(row):
return
menu = QMenu(self.table)
action = menu.addAction("AI 报告 / 逐味对照" if self._agreement_visible else "AI 报告")
action.triggered.connect(lambda: self._run_row_action(row, self._open_ai_report))
selected = self.table.copy_text.textCursor().selectedText() if self.table.copy_text.isVisible() else ""
if selected:
menu.addAction("复制选中文字", lambda: QApplication.clipboard().setText(selected.replace("\u2029", "\n")))
text = self.table.cell_copy_text(self.table.indexFromItem(item))
if text:
menu.addAction("复制单元格", lambda: QApplication.clipboard().setText(text))
row_text = self.table.row_copy_text(item.row())
menu.addAction("复制整行", lambda: QApplication.clipboard().setText(row_text))
if (can_open_issued_ai(self.permissions)
and callable(getattr(self.repository, "list_prescription_ai_reports", None))
and not _is_blank_prescription(row)):
menu.addSeparator()
action = menu.addAction("AI 报告 / 逐味对照" if self._agreement_visible else "AI 报告")
action.triggered.connect(lambda: self._run_row_action(row, self._open_ai_report))
self._ai_context_menu = menu
menu.popup(self.table.viewport().mapToGlobal(position))
+16 -3
View File
@@ -11,7 +11,7 @@ import pytest
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QApplication, QWidget
from doctor_workstation.core.errors import ApiBusinessError, ApiProtocolError
from doctor_workstation.core.errors import ApiBusinessError, ApiProtocolError, ApiTimeoutError
from doctor_workstation.services.repository import RemoteDoctorRepository
from doctor_workstation.ui.pages import consultations as module
@@ -160,8 +160,8 @@ def test_permission_error_stays_scoped_and_retry_recovers(page: Any, monkeypatch
widget._load_departments()
assert not widget._departments_loaded
assert not widget._departments_loading
assert widget.department_combo.currentText() == "部门加载失败"
assert "无权限" in widget.department_combo.toolTip()
assert widget.department_combo.currentText() == "暂无部门筛选权限"
assert "联系管理员" in widget.department_combo.toolTip()
assert not widget.department_retry_button.isHidden()
client.departments = TREE
widget.department_retry_button.click()
@@ -170,6 +170,19 @@ def test_permission_error_stays_scoped_and_retry_recovers(page: Any, monkeypatch
assert all(params == {"apply_data_scope": 1} for _, params in client.calls)
def test_network_failure_remains_retryable_and_is_not_reported_as_permission_denied(page: Any, monkeypatch: pytest.MonkeyPatch) -> None:
widget, client = page
monkeypatch.setattr(module, "run_async", immediate)
client.departments = ApiTimeoutError("request timed out")
widget._load_departments()
assert widget.department_combo.currentText() == "部门加载失败"
assert "超时" in widget.department_combo.toolTip()
assert "联系管理员" not in widget.department_combo.toolTip()
client.departments = TREE
widget.department_retry_button.click()
assert widget._departments_loaded
def test_inflight_dedup_and_stale_success_or_failure_cannot_override_selection(page: Any, monkeypatch: pytest.MonkeyPatch) -> None:
widget, _client = page
jobs: list[dict[str, Any]] = []
+110 -2
View File
@@ -7,8 +7,8 @@ from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QDialog
from PySide6.QtCore import QPoint, Qt
from PySide6.QtWidgets import QApplication, QDialog, QPushButton
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui import widgets
@@ -412,6 +412,114 @@ def _finish_queued(callback: dict[str, Any], result: Any) -> None:
callback["on_finished"]()
@pytest.mark.parametrize("owner_width", [1024, 1440])
def test_embedded_prescription_history_opens_full_detail_without_changing_draft(
application: QApplication, monkeypatch: pytest.MonkeyPatch, owner_width: int,
) -> None:
listed: list[int] = []
fetched: list[int] = []
shown: list[int] = []
class Repository(_DiagnosisRepository):
def get_doctor_notes(self, _diagnosis_id: int) -> list[Any]:
return []
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[dict[str, Any]]:
listed.append(diagnosis_id)
return [
{"id": 92, "diagnosis_id": diagnosis_id, "prescription_date": "2026-11-03",
"doctor_name": "陈医生", "clinical_diagnosis": "气阴两虚", "audit_status": 1},
{"id": 81, "diagnosis_id": diagnosis_id, "prescription_date": "2026-10-20",
"doctor_name": "李医生", "clinical_diagnosis": "脾虚", "void_status": 1},
]
def get_prescription(self, prescription_id: int) -> dict[str, Any]:
fetched.append(prescription_id)
return {"id": prescription_id, "diagnosis_id": 745, "patient_name": "测试患者",
"herbs": [{"name": "黄芪", "dosage": 15}], "usage_instruction": "水煎服"}
def inspect_detail(dialog: dialog_module.PrescriptionDetailDialog) -> int:
shown.append(dialog.prescription["id"])
assert "黄芪" in dialog.document.toPlainText()
assert "水煎服" in dialog.document.toPlainText()
assert dialog.diagnosis_button.isHidden()
assert dialog.orders_button.isHidden()
return QDialog.DialogCode.Rejected
monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async)
monkeypatch.setattr(dialog_module.PrescriptionDetailDialog, "exec", inspect_detail)
owner = QDialog()
owner.resize(owner_width, 800)
owner.show()
editor = PrescriptionEditorDialog(
Repository(), {"diagnosis_id": 745, "patient_name": "测试患者"}, mode="edit",
permissions=PermissionSet(["tcm.diagnosis/readonlyDetail", "tcm.diagnosis/chufang"]),
parent=owner,
)
editor.show()
application.processEvents()
editor.clinical_diagnosis.setPlainText("尚未保存的新诊断")
draft = editor.payload()
editor.diagnosis_button.click()
for _ in range(6):
application.processEvents()
detail = editor._diagnosis_view
assert detail is not None
table = detail.readonly_prescriptions_table
assert listed == [745]
assert table.rowCount() == 2
assert table.item(0, 5).text() == "审核通过"
assert table.item(1, 5).text() == "已作废"
assert table.horizontalScrollBar().maximum() == 0
assert table.mapToGlobal(QPoint(table.width(), 0)).x() <= detail.readonly_scroll.viewport().mapToGlobal(
QPoint(detail.readonly_scroll.viewport().width(), 0)
).x()
assert detail._readonly_sections["prescription"].y() < detail.case_grid.y()
actions = table.cellWidget(1, 6).findChildren(QPushButton)
assert [button.text() for button in actions] == ["查看"]
actions[0].click()
assert fetched == [81] and shown == [81]
assert editor.payload() == draft
assert editor.diagnosis_host.isVisibleTo(editor)
detail.readonly_close_button.click()
editor.diagnosis_button.click()
assert listed == [745, 745]
assert editor.payload() == draft
editor.reject()
owner.close()
application.processEvents()
def test_readonly_history_respects_permission_and_shows_empty_and_failure(
application: QApplication, monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[int] = []
class Repository(_DiagnosisRepository):
def get_doctor_notes(self, _diagnosis_id: int) -> list[Any]:
return []
def list_prescriptions_by_diagnosis(self, diagnosis_id: int) -> list[Any]:
calls.append(diagnosis_id)
if len(calls) > 1:
raise RuntimeError("测试加载失败")
return []
monkeypatch.setattr(diagnosis_module, "run_async", _immediate_async)
dialog = DiagnosisDialog(Repository(), permissions=PermissionSet(["tcm.diagnosis/readonlyDetail"]))
dialog.open_for(745, editable=False)
assert dialog._readonly_sections["prescription"].isHidden()
assert calls == []
dialog.refresh_permissions(PermissionSet(["tcm.diagnosis/readonlyDetail", "tcm.diagnosis/chufang"]))
dialog.open_for(745, editable=False)
assert "暂无历史处方" in dialog.readonly_prescriptions_table.empty_label.text()
dialog.open_for(745, editable=False)
assert "加载失败" in dialog.readonly_prescriptions_table.empty_label.text()
assert dialog.readonly_prescriptions_table.rowCount() == 0
dialog.close()
application.processEvents()
def test_prescription_lists_snapshot_queries_and_supersede_stale_refresh(
application: QApplication,
monkeypatch: pytest.MonkeyPatch,
+88
View File
@@ -246,6 +246,94 @@ def test_blue_icon_disabled_variant_does_not_modify_shared_icon_cache(applicatio
assert shared.pixmap(size, size, QIcon.Mode.Disabled).toImage() == original_disabled
@pytest.mark.parametrize("column", [1, 3, 4, 5, 6, 7, 8, 9, 10])
def test_all_prescription_data_cells_select_and_copy_without_editing(
shell_factory, application, column,
) -> None:
_shell, page = shell_factory()
table = page.table
item = table.item(0, column)
original = item.text()
row_id = _record_id(table, 0)
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(item).center())
assert table.copy_text.isVisible()
assert _record_id(table, table.currentRow()) == row_id
assert table.cellWidget(0, column) is None
QTest.keyClick(table.copy_text, Qt.Key.Key_A, Qt.KeyboardModifier.ControlModifier)
QTest.keyClick(table.copy_text, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
copied = application.clipboard().text()
if column == 1:
assert original in copied and f"ID: {row_id}" in copied
elif column == 5:
assert copied == original.replace(" · ", "\n", 1).replace(" · ", " · ")
elif column == 10:
assert copied == "\n".join(original.rsplit(" ", 1))
else:
assert copied == original
QTest.keyClicks(table.copy_text, "must-not-edit")
assert item.text() == original
assert table.item(0, 0).checkState() == Qt.CheckState.Unchecked
def test_text_drag_copy_and_double_click_do_not_open_prescription(shell_factory, application, monkeypatch):
_shell, page = shell_factory()
table = page.table
opened = []
monkeypatch.setattr(page, "_view_selected", lambda: opened.append(True))
item = table.item(1, 8)
rect = table.visualItemRect(item)
start = QPoint(rect.left() + 12, rect.center().y())
end = QPoint(rect.right() - 12, start.y())
QTest.mousePress(table.viewport(), Qt.MouseButton.LeftButton, pos=start)
QTest.mouseMove(table.viewport(), end, 30)
QTest.mouseRelease(table.viewport(), Qt.MouseButton.LeftButton, pos=end)
assert table.copy_text.textCursor().selectedText() == item.text()
QTest.keyClick(table.copy_text, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
assert application.clipboard().text() == item.text()
table.copy_text.customContextMenuRequested.emit(QPoint(4, 4))
menu = page._ai_context_menu
selected_action = next(action for action in menu.actions() if action.text() == "复制选中文字")
application.clipboard().clear()
selected_action.trigger()
assert application.clipboard().text() == item.text()
menu.close()
QTest.mouseDClick(table.viewport(), Qt.MouseButton.LeftButton, pos=start + QPoint(8, 0))
assert table.copy_text.textCursor().hasSelection()
assert opened == []
def test_copy_context_menu_and_refresh_follow_the_visible_row(shell_factory, application):
_shell, page = shell_factory()
table = page.table
# Copy remains available without AI/report permissions, including blank prescriptions.
page.permissions = PermissionSet([])
table.sortItems(1, Qt.SortOrder.DescendingOrder)
item = table.item(0, 1)
page._open_ai_context_menu(table.visualItemRect(item).center())
menu = page._ai_context_menu
actions = {action.text(): action for action in menu.actions()}
assert set(actions) == {"复制单元格", "复制整行"}
actions["复制单元格"].trigger()
assert item.text() in application.clipboard().text()
assert f"ID: {_record_id(table, 0)}" in application.clipboard().text()
actions["复制整行"].trigger()
copied = application.clipboard().text()
for column in range(3, 11):
assert table.item(0, column).text() in copied
assert "\t" in copied and "复制" not in copied
menu.close()
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(item).center())
assert table.copy_text.isVisible()
table.sortItems(1, Qt.SortOrder.AscendingOrder)
assert table.copy_text.isHidden()
table.setCurrentItem(table.item(0, 5))
QTest.keyClick(table, Qt.Key.Key_C, Qt.KeyboardModifier.ControlModifier)
assert application.clipboard().text() == table.item(0, 5).text()
QTest.mouseClick(table.viewport(), Qt.MouseButton.LeftButton, pos=table.visualItemRect(table.item(0, 5)).center())
page.refresh()
assert table.copy_text.isHidden()
def test_long_doctor_button_paints_compactly_without_truncating_real_value(shell_factory, application) -> None:
_shell, page = shell_factory(1024, 768)
page.filter_disclosure.set_expanded(True)
@@ -80,6 +80,17 @@ class AuthMiddleware
// 当前访问路径
$accessUri = strtolower($request->controller() . '/' . $request->action());
// 患者列表的部门筛选复用列表权限;只允许 GET 查询显式启用数据范围的部门树。
// 必须与 DeptController::all 读取同一个 GET 参数,避免请求体参数意外放行全量部门。
if ($accessUri === 'dept.dept/all'
&& $request->isGet()
&& in_array($request->get('apply_data_scope'), [1, '1'], true)) {
$uris = $this->formatUrl($adminAuthCache->getAdminUri() ?? []);
if (in_array('tcm.diagnosis/lists', $uris, true)) {
return $next($request);
}
}
// Screenshot analysis shares the assistant grant and must never use the
// unregistered-route bypass, even before menus have been synchronized.
if ($accessUri === 'tcm.diagnosis/aitonguescreenshot') {