更新::

This commit is contained in:
Your Name
2026-11-06 13:35:33 +08:00
parent b46fb901ca
commit 0c43d54c08
8 changed files with 633 additions and 69 deletions
@@ -1858,8 +1858,13 @@ class ConsultationsPage(QWidget):
self.latest_appointment_end_date.setDate(_OPTIONAL_DATE_MINIMUM)
self.channel_combo.setCurrentIndex(0)
def _reset(self) -> None:
self.keyword_edit.clear()
def search_patient(self, query: str) -> None:
"""Search authorized diagnoses independently of the current queue filters."""
self.pager.invalidate()
self._reset(keyword=query.strip())
def _reset(self, *, keyword: str = "") -> None:
self.keyword_edit.setText(keyword)
self.patient_name_edit.clear()
self.doctor_edit.clear()
for combo in (
@@ -1872,7 +1877,10 @@ class ConsultationsPage(QWidget):
self.channel_combo,
self.unserved_sort_combo,
):
blocked = combo.blockSignals(True)
combo.setCurrentIndex(0)
combo.blockSignals(blocked)
self.table_host.set_sort_direction("")
for editor in (
self.latest_appointment_start_date,
self.latest_appointment_end_date,
@@ -1996,13 +1996,15 @@ class PatientListWorkspace(QWidget):
self.custom_date_button.setChecked(True)
self.search()
def reset_filters(self) -> None:
self.keyword_edit.clear()
def reset_filters(self, *, keyword: str = "", refresh: bool = True) -> None:
self.keyword_edit.setText(keyword.strip())
self.status_combo.setCurrentIndex(0)
for key, button in self.status_buttons.items():
button.setChecked(key == "")
self.set_date_mode("all", refresh=False)
self.search()
self._page = 1
if refresh:
self.refresh()
def search(self) -> None:
if self._date_mode != "all" and self.start_date.date() > self.end_date.date():
@@ -3256,7 +3258,7 @@ class PatientsPage(QWidget):
if current is self.sender() or getattr(current, "scope", None) == text:
self.scope_badge.set_status(text, "neutral")
def _workspace_changed(self, index: int) -> None:
def _workspace_changed(self, index: int, *, refresh: bool = True) -> None:
for workspace_index, disclosure in enumerate(self.filter_disclosures):
disclosure.button.setVisible(workspace_index == index)
self.scope_badge.setVisible(False)
@@ -3264,9 +3266,9 @@ class PatientsPage(QWidget):
workspace = self.tabs.widget(index)
scope = getattr(workspace, "scope", "按权限加载")
self.scope_badge.set_status(str(scope), "neutral")
refresh = getattr(workspace, "refresh", None)
if callable(refresh) and self.isVisible():
refresh()
reload_workspace = getattr(workspace, "refresh", None)
if refresh and callable(reload_workspace) and self.isVisible():
reload_workspace()
@property
def filter_disclosure(self) -> FilterDisclosure:
@@ -3283,6 +3285,19 @@ class PatientsPage(QWidget):
if expanded:
splitter.setSizes(self._progress_splitter_sizes)
def search_patient(self, query: str) -> None:
"""Search the patient workspace using its normal permission-scoped endpoint."""
blocked = self.tabs.blockSignals(True)
try:
self.tabs.setCurrentWidget(self.patient_workspace)
finally:
self.tabs.blockSignals(blocked)
self._workspace_changed(0, refresh=False)
# A global search starts a fresh result list, even for the same keyword.
self.patient_workspace.pager.invalidate()
self.patient_workspace.reset_filters(keyword=query, refresh=False)
self.refresh()
def refresh(self) -> None:
workspace = self.tabs.currentWidget()
refresh = getattr(workspace, "refresh", None)
@@ -1966,7 +1966,7 @@ def _queue_shows_status_badge(status_number: int, status_text: str) -> bool:
text = str(status_text or "")
if "问诊中" in text or "已完成" in text or "已过号" in text:
return True
return status_number in {3, 4}
return status_number in {2, 3, 4}
def _queue_badge_kind(status_number: int, status_text: str) -> str:
@@ -3013,8 +3013,8 @@ class _ReceptionDateButton(QPushButton):
)
self._calendar.setFixedSize(292, 280)
self._calendar.hide()
self._calendar.clicked.connect(self._pick_date)
self._calendar.activated.connect(self._pick_date)
self._calendar.clicked.connect(self._pick_calendar_date)
self._calendar.activated.connect(self._pick_calendar_date)
self.clicked.connect(self._toggle_calendar)
def iso_date(self) -> str:
@@ -3040,13 +3040,16 @@ class _ReceptionDateButton(QPushButton):
self._calendar.show()
self._calendar.raise_()
def _pick_calendar_date(self, value: QDate) -> None:
# clicked and activated may describe the same selection; consume one popup.
if self._calendar.isVisible():
self._pick_date(value)
def _pick_date(self, value: QDate) -> None:
self._calendar.hide()
if not value.isValid():
return
iso_date = value.toString(Qt.DateFormat.ISODate)
if iso_date == self._iso_date:
return
self.set_iso_date(iso_date)
self.date_selected.emit(iso_date)
@@ -4264,9 +4267,9 @@ class ReceptionPage(QWidget):
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(2, 0, 0, 0)
header_layout.setSpacing(8)
title = QLabel("今日接诊", header)
title.setObjectName("ReceptionQueueTitle")
header_layout.addWidget(title)
self.queue_title = QLabel("今日接诊", header)
self.queue_title.setObjectName("ReceptionQueueTitle")
header_layout.addWidget(self.queue_title)
header_layout.addStretch(1)
self.refresh_button = QPushButton("", header)
self.refresh_button.setIcon(icons.icon("refresh", PALETTE["muted"], 18))
@@ -4311,7 +4314,7 @@ class ReceptionPage(QWidget):
filter_row = QGridLayout()
filter_row.setSpacing(6)
for status, label in ((None, "全部"), (1, "待接诊"), (2, "问诊中"), (3, "已完成")):
for status, label in ((None, "全部"), (1, "待接诊"), (2, "已取消"), (3, "已完成")):
button = QPushButton(label, self.queue_filter_panel)
button.setFixedHeight(32)
button.setCheckable(True)
@@ -4350,7 +4353,12 @@ class ReceptionPage(QWidget):
self.queue_list.currentItemChanged.connect(self._on_queue_selection)
self.queue_list.verticalScrollBar().valueChanged.connect(self._on_queue_scroll)
self.queue_stack.addWidget(self.queue_list)
self.queue_empty = EmptyState("队列为空", "当前筛选下没有待处理患者。", "重新加载")
self.queue_empty = EmptyState("暂无挂号记录", "当前筛选下没有挂号记录。", "重新加载")
self.queue_empty_description = next(
label for label in self.queue_empty.findChildren(QLabel)
if label.property("role") == "muted"
)
self._sync_queue_filter_summary()
self.queue_empty.action_requested.connect(self._refresh_workspace)
self.queue_stack.addWidget(self.queue_empty)
layout.addWidget(self.queue_stack, 1)
@@ -6941,13 +6949,21 @@ class ReceptionPage(QWidget):
self.refresh()
def _sync_queue_filter_summary(self) -> None:
status = {
None: "全部状态", 1: "待接诊", 2: "问诊中", 3: "已完成", 4: "已过号",
}.get(self._queue_filter_status, "全部状态")
status = STATUS_TEXT.get(self._queue_filter_status, "全部状态")
today = date.today().isoformat()
self.queue_title.setText(
"历史接诊" if self._queue_date < today else
"今日接诊" if self._queue_date == today else "预约接诊"
)
summary = f"{self._queue_date} · {status}"
keyword = self.search_edit.text().strip()
self.queue_filter_summary.setText(summary + (" · 姓名已筛选" if keyword else ""))
self.queue_filter_summary.setToolTip(summary + (f" · 姓名:{keyword}" if keyword else ""))
if hasattr(self, "queue_empty_description"):
conditions = f"{status}、当前姓名" if keyword else status
self.queue_empty_description.setText(
f"{self._queue_date}\n当前账号在“{conditions}”筛选下没有挂号记录。"
)
def _search_changed(self, value: str) -> None:
if not value and self.isVisible():
@@ -6964,6 +6980,15 @@ class ReceptionPage(QWidget):
self.refresh()
def _queue_date_changed(self, iso_date: str) -> None:
# Selecting a day opens that doctor's complete daily appointment list.
self._queue_filter_status = None
for value, button in self._queue_filter_buttons.items():
button.setChecked(value is None)
blocked = self.search_edit.blockSignals(True)
try:
self.search_edit.clear()
finally:
self.search_edit.blockSignals(blocked)
self._queue_date = iso_date
self._clear_selection()
self._reset_queue_state()
@@ -7401,7 +7426,7 @@ class ReceptionPage(QWidget):
labels = {
None: f"全部 {total}",
1: f"待接诊 {counts.get(1, 0)}",
2: f"问诊中 {counts.get(2, 0)}",
2: f"已取消 {counts.get(2, 0)}",
3: f"已完成 {counts.get(3, 0)}",
}
for status, button in self._queue_filter_buttons.items():
+35 -44
View File
@@ -1319,9 +1319,15 @@ class ShellWindow(QMainWindow):
search_layout = QHBoxLayout(search_host)
search_layout.setContentsMargins(4, 0, 7, 0)
search_layout.setSpacing(3)
self.search_mark = QLabel(search_host)
self.search_mark = QToolButton(search_host)
self.search_mark.setObjectName("ShellSearchMark")
self.search_mark.setFixedSize(20, 20)
self.search_mark.setAutoRaise(True)
self.search_mark.setCursor(Qt.CursorShape.PointingHandCursor)
self.search_mark.setAccessibleName("搜索患者")
self.search_mark.setToolTip("搜索患者(回车)")
self.search_mark.setStyleSheet("background: transparent; border: 0; padding: 0;")
self.search_mark.clicked.connect(self._run_global_search)
self.search_mark.hide()
search_layout.addWidget(self.search_mark)
self.global_search = QLineEdit(search_host)
@@ -1329,12 +1335,15 @@ class ShellWindow(QMainWindow):
self.global_search.setSizePolicy(
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed
)
self.global_search.setPlaceholderText("搜索患者姓名、手机号、病历号")
self.global_search.setPlaceholderText("搜索患者姓名、手机号...")
self.global_search.setToolTip("输入患者姓名或手机号,按回车或点击放大镜搜索")
self.global_search.setClearButtonEnabled(True)
self._global_search_icon_action = self.global_search.addAction(
_painted_shell_icon("search", 16),
QLineEdit.ActionPosition.LeadingPosition,
)
self._global_search_icon_action.setToolTip("搜索患者(回车)")
self._global_search_icon_action.triggered.connect(self._run_global_search)
self.global_search.returnPressed.connect(self._run_global_search)
search_layout.addWidget(self.global_search, 1)
shortcut_hint = QLabel("Ctrl K", search_host)
@@ -1614,7 +1623,7 @@ class ShellWindow(QMainWindow):
self.search_host.layout().setSpacing(6 if active else 3)
self.search_mark.setVisible(active)
self._global_search_icon_action.setVisible(not active)
self.global_search.setPlaceholderText("搜索患者姓名、手机号..." if active else "搜索患者姓名、手机号、病历号")
self.global_search.setPlaceholderText("搜索患者姓名、手机号...")
self.search_shortcut_hint.setText("Ctrl K")
self.ai_top_button.setVisible(self._can_ai_assistant and not active)
self.menu_sidebar_action.setVisible(active)
@@ -1789,7 +1798,8 @@ class ShellWindow(QMainWindow):
self._global_search_icon_action.setIcon(
_painted_shell_icon("search", 20 if active else 16, reception=active)
)
self.search_mark.setPixmap(_painted_shell_icon("search", 20, reception=True).pixmap(20, 20))
self.search_mark.setIcon(_painted_shell_icon("search", 20, reception=True))
self.search_mark.setIconSize(QSize(20, 20))
for key, button in self.nav_buttons.items():
size = 20 if active else 18
button.setProperty("receptionTechBlue", active)
@@ -1819,46 +1829,20 @@ class ShellWindow(QMainWindow):
def _run_global_search(self) -> None:
query = self.global_search.text().strip()
if not query:
self._focus_global_search()
keys = ("patients", "consultations") if self._active_page_key == "patients" else (
"consultations", "patients",
)
for key in keys:
# Only use patient pages already registered by permission-aware navigation.
page = self.pages.get(key)
search = getattr(page, "search_patient", None)
if page is None or key not in self.nav_buttons or not callable(search):
continue
self._navigate(
self.stack.indexOf(page), key, on_activate=lambda search=search: search(query),
)
return
page = self.stack.currentWidget()
if page is None:
return
target: QLineEdit | None = None
for attribute in (
"patient_input",
"keyword_edit",
"search_edit",
"patient_filter",
"name_filter",
"sn_filter",
):
candidate = getattr(page, attribute, None)
if isinstance(candidate, QLineEdit) and not candidate.isHidden():
target = candidate
break
if target is None:
for candidate in page.findChildren(QLineEdit):
placeholder = candidate.placeholderText()
if not candidate.isHidden() and any(
marker in placeholder
for marker in ("患者", "姓名", "手机号", "处方")
):
target = candidate
break
if target is None:
show_toast(self, "当前页面没有可用的快捷搜索条件。", "warning")
return
target.setText(query)
search = getattr(page, "_search", None)
if callable(search):
search()
else:
refresh = getattr(page, "refresh", None)
if callable(refresh):
refresh()
show_toast(self, f"正在当前页面搜索“{query}”。", "info")
show_toast(self, "当前账号没有可用的患者搜索页面。", "warning")
def _open_ai_assistant(self) -> None:
if not self._can_ai_assistant:
@@ -2109,18 +2093,25 @@ class ShellWindow(QMainWindow):
self._ensure_activation_refresh(page, generation)
self._activation_page = None
def _navigate(self, index: int, key: str) -> None:
def _navigate(
self, index: int, key: str, *, on_activate: Callable[[], None] | None = None,
) -> None:
if index < 0 or index >= self.stack.count():
return
page = self.stack.widget(index)
if page is None:
return
if self._active_page_key == key and self.stack.currentWidget() is page:
if on_activate is not None:
self._activation_refreshed = False
on_activate()
return
self._activation_generation += 1
activation_generation = self._activation_generation
self._activation_page = page
self._activation_refreshed = False
if on_activate is not None:
on_activate()
# A stack swap is otherwise a hard cut between two frames: this is the
# transition a doctor sees on every sidebar click all day.
motion.switch_stack(self.stack, index)
@@ -0,0 +1,109 @@
"""Global patient search must not inherit a narrow queue or stale pagination."""
from __future__ import annotations
import os
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QDate, Signal
from PySide6.QtWidgets import QApplication, QWidget
from doctor_workstation.services.repository import RemoteDoctorRepository
from doctor_workstation.ui.pages import consultations as module
class Client:
def __init__(self) -> None:
self.calls = []
def get(self, endpoint: str, params: Any = None) -> Any:
params = dict(params or {})
self.calls.append((endpoint, params))
page = params.get("page_no", 1)
return {"lists": [{"id": page, "patient_name": "测试患者"}], "count": 2}
class DetailStub(QWidget):
saved = Signal()
def __init__(self, _repository: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
@pytest.fixture
def page(application: QApplication, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(module, "DiagnosisDialog", DetailStub)
def immediate(function: Any, **callbacks: Any) -> None:
callbacks["on_success"](function())
if callbacks.get("on_finished"):
callbacks["on_finished"]()
monkeypatch.setattr(module, "run_async", immediate)
client = Client()
view = module.ConsultationsPage(RemoteDoctorRepository(client))
monkeypatch.setattr(view, "_refresh_counts", lambda: None)
yield view, client
view.poll_timer.stop()
view.pager.invalidate()
view.close()
view.deleteLater()
application.processEvents()
@pytest.mark.parametrize("query", [" 林患者 ", "13800138000", "138", "", " "])
def test_global_search_submits_only_new_keyword_and_normal_scoped_list(page, query):
view, client = page
view.patient_name_edit.setText("旧患者")
view.doctor_edit.setText("旧医生")
view.keyword_edit.setText("旧手机号")
view.pending_assign_keyword.setText("旧待分配关键词")
view._pending_assign = "1"
view._pending_booking = "1"
view._completed_appointment = "1"
for combo in (view.department_combo, view.assistant_combo, view.channel_combo):
combo.addItem("旧筛选", 123)
combo.setCurrentIndex(combo.count() - 1)
view.latest_appointment_start_date.setDate(QDate(2026, 1, 3))
view.latest_appointment_end_date.setDate(QDate(2026, 1, 1))
view.latest_assign_start_date.setDate(QDate(2026, 1, 3))
view.latest_assign_end_date.setDate(QDate(2026, 1, 1))
view.unserved_sort_combo.setCurrentIndex(1)
client.calls.clear()
view.search_patient(query)
assert len(client.calls) == 1
endpoint, params = client.calls[0]
assert endpoint == "tcm.diagnosis/lists"
assert params.get("keyword", "") == query.strip()
assert params["page_no"] == 1
assert all(not value for key, value in params.items() if key not in {
"keyword", "page_no", "page_size",
})
assert view.keyword_edit.text() == query.strip()
assert view.patient_name_edit.text() == view.pending_assign_keyword.text() == ""
assert view._appointment_date == view._appointment_status_filter == ""
assert view._pending_assign == ""
assert view.table_host.model._sort_direction == ""
assert view.table_host.model.rowCount() == 1
def test_repeating_global_search_starts_at_first_page(page):
view, client = page
view.search_patient("林患者")
view.pager.load_more()
assert view.pager.page == 2
client.calls.clear()
view.search_patient("林患者")
assert [params["page_no"] for _, params in client.calls] == [1]
assert view.pager.page == 1
assert view.table_host.model.rowCount() == 1
@@ -0,0 +1,172 @@
"""Global patient searches enter the patient list with one scoped request."""
from __future__ import annotations
import os
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication
from doctor_workstation.core import PermissionSet
from doctor_workstation.ui.pages import patients as patients_module
from doctor_workstation.ui.pages.patients import PatientsPage
class Repository:
def __init__(self) -> None:
self.calls: list[tuple[str, dict[str, Any]]] = []
def _result(self, kind: str, query: dict[str, Any]) -> dict[str, Any]:
self.calls.append((kind, dict(query)))
start = (query["page_no"] - 1) * query["page_size"]
return {
"lists": [
{
"id": index + 100,
"diagnosis_id": index + 100,
"patient_name": f"患者 {index}",
"appointment_status": 1,
"queue_status": "waiting",
}
for index in range(start, min(start + query["page_size"], 37))
],
"count": 37,
"extend": {"scope": {"label": "本人患者"}, "summary": {}},
}
def list_patients(self, **query: Any) -> dict[str, Any]:
return self._result("patients", query)
def patient_orders(self, **query: Any) -> dict[str, Any]:
return self._result("orders", query)
def patient_progress(self, **query: Any) -> dict[str, Any]:
return self._result("progress", query)
@pytest.fixture(scope="module")
def application() -> QApplication:
return QApplication.instance() or QApplication([])
@pytest.fixture
def page(application: QApplication, monkeypatch: pytest.MonkeyPatch):
def immediate(function: Any, **callbacks: Any) -> object:
result = function()
callbacks["on_success"](result)
if callbacks.get("on_finished"):
callbacks["on_finished"]()
return object()
monkeypatch.setattr(patients_module, "run_async", immediate)
repository = Repository()
widget = PatientsPage(repository, PermissionSet(["firstvisit.myPatient/lists"]))
widget.resize(1280, 900)
widget.show()
application.processEvents()
yield widget, repository
for timer in widget.findChildren(QTimer):
timer.stop()
widget.close()
widget.deleteLater()
application.processEvents()
def assert_patient_query(repository: Repository, keyword: str) -> None:
assert repository.calls == [
(
"patients",
{
"page_no": 1,
"page_size": 15,
"keyword": keyword,
"status": "",
"start_date": "",
"end_date": "",
},
)
]
@pytest.mark.parametrize("tab_index", [0, 1, 2], ids=["patients", "orders", "progress"])
@pytest.mark.parametrize("expanded", [False, True], ids=["collapsed", "expanded"])
def test_search_enters_patient_list_and_clears_old_filters(
page, monkeypatch: pytest.MonkeyPatch, tab_index: int, expanded: bool
) -> None:
widget, repository = page
workspace = widget.patient_workspace
workspace.keyword_edit.setText("旧患者")
workspace.status_combo.setCurrentIndex(2)
workspace.set_date_mode("today", refresh=False)
widget.filter_disclosures[0].set_expanded(expanded)
widget.order_workspace.keyword_edit.setText("旧订单")
widget.tabs.setCurrentIndex(tab_index)
repository.calls.clear()
refreshes: list[bool] = []
original_refresh = widget.refresh
def refresh() -> None:
refreshes.append(True)
original_refresh()
monkeypatch.setattr(widget, "refresh", refresh)
widget.search_patient(" 13800138000 ")
assert widget.tabs.currentWidget() is workspace
assert_patient_query(repository, "13800138000")
assert refreshes == [True]
assert workspace.keyword_edit.text() == "13800138000"
assert workspace._date_mode == "all"
assert workspace.status_buttons[""].isChecked()
assert workspace._page == workspace.pager.page == 1
assert widget.filter_disclosures[0].expanded is expanded
assert widget.order_workspace.keyword_edit.text() == "旧订单"
assert not widget.progress_workspace.timer.isActive()
@pytest.mark.parametrize("query", ["", " "], ids=["empty", "whitespace"])
def test_empty_search_clears_keyword_even_when_patient_page_is_hidden(page, query: str) -> None:
widget, repository = page
widget.patient_workspace.keyword_edit.setText("旧患者")
widget.patient_workspace.status_combo.setCurrentIndex(3)
widget.tabs.setCurrentIndex(1)
widget.hide()
repository.calls.clear()
widget.search_patient(query)
assert widget.tabs.currentIndex() == 0
assert widget.patient_workspace.keyword_edit.text() == ""
assert_patient_query(repository, "")
def test_repeated_search_discards_previously_loaded_pages(page) -> None:
widget, repository = page
widget.search_patient("患者")
workspace = widget.patient_workspace
workspace.pager.load_more()
assert workspace._page == workspace.pager.page == 2
repository.calls.clear()
widget.search_patient("患者")
assert_patient_query(repository, "患者")
assert workspace._page == workspace.pager.page == 1
assert workspace.table.rowCount() == 15
def test_reset_button_still_accepts_qt_clicked_signal(page) -> None:
widget, repository = page
workspace = widget.patient_workspace
workspace.keyword_edit.setText("旧患者")
workspace.status_combo.setCurrentIndex(2)
repository.calls.clear()
workspace.reset_button.click()
assert workspace.keyword_edit.text() == ""
assert_patient_query(repository, "")
+176
View File
@@ -0,0 +1,176 @@
"""Historical reception queries retain their day and include finished appointments."""
from __future__ import annotations
import os
from datetime import date
from typing import Any
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QDate, QTimer
from PySide6.QtWidgets import QApplication
from doctor_workstation.core import PermissionSet
from doctor_workstation.services.repository import RemoteDoctorRepository
from doctor_workstation.ui.pages import reception as module
from doctor_workstation.ui.widgets import StatusBadge
class Today(date):
@classmethod
def today(cls):
return cls(2026, 11, 6)
class Client:
def __init__(self):
self.calls = []
def get(self, endpoint: str, params: Any = None):
params = dict(params or {})
self.calls.append((endpoint, params))
assert endpoint == "doctor.appointment/lists"
day = params["start_date"]
statuses = [2, 3, 4] if day == "2026-10-17" else []
rows = [
{"id": status, "patient_name": "测试患者", "status": status,
"status_desc": module.STATUS_TEXT[status], "appointment_date": day}
for status in statuses
if params.get("status") in (None, "", status)
]
offset = (params["page_no"] - 1) * params["page_size"]
return {
"lists": rows[offset:offset + params["page_size"]], "count": len(rows),
"extend": {"status_count": {str(status): statuses.count(status) for status in range(1, 5)}},
}
@pytest.fixture(scope="module")
def application():
return QApplication.instance() or QApplication([])
@pytest.fixture
def page(application, monkeypatch):
def immediate(function, **callbacks):
callbacks["on_success"](function())
callbacks["on_finished"]()
monkeypatch.setattr(module, "date", Today)
monkeypatch.setattr(module, "run_async", immediate)
client = Client()
widget = module.ReceptionPage(RemoteDoctorRepository(client), PermissionSet([]))
# Keep this test on the list boundary, without loading patient clinical details.
monkeypatch.setattr(widget, "_select_record", lambda *_args, **_kwargs: None)
monkeypatch.setattr(widget, "_maybe_load_more_queue", lambda: None)
yield widget, client
for timer in widget.findChildren(QTimer):
timer.stop()
widget.close()
widget.deleteLater()
application.processEvents()
def test_historical_day_opens_all_statuses_and_labels_cancelled_correctly(page):
widget, client = page
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
assert widget.queue_title.text() == "历史接诊"
assert widget._queue_filter_status is None
assert widget._queue_filter_buttons[None].isChecked()
assert [record.status for record in widget._queue_records] == [2, 3, 4]
assert widget._queue_filter_buttons[None].text() == "全部 3"
assert widget._queue_filter_buttons[2].text() == "已取消 1"
row = widget.queue_list.itemWidget(widget.queue_list.item(0))
assert any(badge.text() == "已取消" and not badge.isHidden() for badge in row.findChildren(StatusBadge))
assert client.calls[-1][1]["start_date"] == client.calls[-1][1]["end_date"] == "2026-10-17"
assert not client.calls[-1][1].get("status")
assert "progress_board" not in client.calls[-1][1]
assert "diag_scope_relax" not in client.calls[-1][1]
@pytest.mark.parametrize("status", [None, 1, 2, 3, 4])
def test_history_status_refresh_and_poll_keep_exact_date(page, status):
widget, client = page
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
widget._set_queue_filter(status)
widget.refresh(workspace_refresh=True)
widget._poll_queue()
assert all(params["start_date"] == params["end_date"] == "2026-10-17" for _, params in client.calls)
assert client.calls[-1][1].get("status") == status
assert widget._queue_filter_buttons[None].text() == "全部 3"
if status == 2:
assert "已取消" in widget.queue_filter_summary.text()
def test_history_pagination_keeps_date_and_status(page):
widget, client = page
widget._queue_page_size = 2
widget.queue_date_button._pick_date(QDate(2026, 10, 17))
widget._load_more()
assert [params["page_no"] for _, params in client.calls] == [1, 2]
assert len(widget._queue_records) == 3
assert client.calls[-1][1]["start_date"] == "2026-10-17"
assert not client.calls[-1][1].get("status")
def test_empty_day_explains_selected_date_and_explicit_today_selection_shows_all(page):
widget, _client = page
assert widget._queue_filter_status == 1
widget.queue_date_button._pick_date(QDate(2026, 10, 16))
assert widget.queue_stack.currentWidget() is widget.queue_empty
assert "2026-10-16" in widget.queue_empty_description.text()
assert "当前账号" in widget.queue_empty_description.text()
assert "挂号记录" in widget.queue_empty_description.text()
widget.queue_date_button._pick_date(QDate(2026, 11, 6))
assert widget.queue_title.text() == "今日接诊"
assert widget._queue_filter_status is None
widget.queue_date_button._pick_date(QDate(2026, 11, 7))
assert widget.queue_title.text() == "预约接诊"
@pytest.mark.parametrize("selected_date", ["2026-10-17", "2026-11-06", "2026-11-07"])
def test_selecting_any_day_clears_old_name_and_status_with_one_request(page, application, selected_date):
widget, client = page
widget.show()
application.processEvents()
widget.search_edit.setText("之前查过的患者")
widget._set_queue_filter(3)
client.calls.clear()
widget.queue_date_button._pick_date(QDate.fromString(selected_date, "yyyy-MM-dd"))
assert len(client.calls) == 1
endpoint, params = client.calls[0]
assert endpoint == "doctor.appointment/lists"
assert params["start_date"] == params["end_date"] == selected_date
assert params["page_no"] == 1
assert not params.get("patient_name")
assert not params.get("status")
assert widget.search_edit.text() == ""
assert widget._queue_filter_buttons[None].isChecked()
assert not any(key in params for key in ("progress_board", "diag_scope_relax", "assistant_id"))
def test_calendar_reselects_same_day_and_deduplicates_click_activation(page, application):
widget, client = page
widget.show()
application.processEvents()
widget._set_queue_filter(3)
widget.search_edit.setText("旧姓名")
client.calls.clear()
button = widget.queue_date_button
calendar = button.calendarWidget()
selected = QDate(2026, 11, 6)
button.click()
assert calendar.isVisible()
calendar.clicked.emit(selected)
calendar.activated.emit(selected)
assert len(client.calls) == 1
assert not calendar.isVisible()
assert widget.search_edit.text() == ""
assert widget._queue_filter_status is None
button.click()
calendar.activated.emit(selected)
assert len(client.calls) == 2
+70 -2
View File
@@ -7,7 +7,8 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import pytest
from PySide6.QtCore import QSize, Qt
from PySide6.QtWidgets import QApplication, QDialog, QFrame, QToolButton, QWidget
from PySide6.QtTest import QTest
from PySide6.QtWidgets import QApplication, QDialog, QFrame, QLineEdit, QToolButton, QWidget
from doctor_workstation.ui import shell as shell_module
from doctor_workstation.ui.shell import NavigationItem, ShellWindow
@@ -35,10 +36,15 @@ class _ShellPageDouble(QWidget):
self.show_count = 0
self.ai_context_available = False
self.ai_open_count = 0
self.search_queries: list[str] = []
def refresh(self) -> None:
self.refresh_count += 1
def search_patient(self, query: str) -> None:
self.search_queries.append(query)
self.refresh()
def open_selected_ai_consult(self) -> bool:
self.ai_open_count += 1
return self.ai_context_available
@@ -243,13 +249,75 @@ def test_registered_pages_are_not_top_level_windows(shell_window: ShellWindow) -
assert not page.isWindow()
@pytest.mark.parametrize("origin,trigger,target", [
("consultations", "enter", "consultations"),
("appointments", "button", "consultations"),
("prescriptions", "enter", "consultations"),
("prescription_library", "button", "consultations"),
("patients", "enter", "patients"),
("legacy_reference", "action", "consultations"),
])
def test_topbar_search_routes_to_patient_page_once(
shell_window: ShellWindow, application: QApplication, origin: str, trigger: str, target: str,
) -> None:
assert shell_window.navigate(origin)
application.processEvents()
unrelated = QLineEdit(shell_window.pages[origin])
unrelated.setPlaceholderText("患者姓名")
unrelated.setText("原页面条件")
page = shell_window.pages[target]
before = page.refresh_count
shell_window.global_search.setText(" 13800138000 ")
if trigger == "enter":
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
elif trigger == "button":
QTest.mouseClick(shell_window.search_mark, Qt.MouseButton.LeftButton)
else:
shell_window._global_search_icon_action.trigger()
application.processEvents()
assert shell_window.stack.currentWidget() is page
assert page.search_queries == ["13800138000"]
assert page.refresh_count == before + 1
assert unrelated.text() == "原页面条件"
def test_topbar_search_uses_available_patient_page_and_empty_query_clears(
shell_window: ShellWindow, application: QApplication,
) -> None:
shell_window.pages.pop("consultations")
shell_window.global_search.setText("林患者")
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
application.processEvents()
patient_page = shell_window.pages["patients"]
assert shell_window.stack.currentWidget() is patient_page
shell_window.global_search.clear()
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
assert patient_page.search_queries == ["林患者", ""]
def test_topbar_search_without_patient_page_does_not_query_unrelated_pages(
shell_window: ShellWindow, monkeypatch: pytest.MonkeyPatch,
) -> None:
shell_window.pages.pop("consultations")
shell_window.pages.pop("patients")
current = shell_window.stack.currentWidget()
before = current.refresh_count
messages = []
monkeypatch.setattr(shell_module, "show_toast", lambda *args: messages.append(args))
shell_window.global_search.setText("林患者")
QTest.keyClick(shell_window.global_search, Qt.Key.Key_Return)
assert current.refresh_count == before
assert shell_window.stack.currentWidget() is current
assert "没有可用的患者搜索页面" in messages[0][1]
def test_reference_shell_has_integrated_search_ai_card_and_window_controls(
shell_window: ShellWindow,
) -> None:
assert shell_window.navigate("legacy_reference")
assert shell_window.windowFlags() & Qt.WindowType.FramelessWindowHint
assert (
shell_window.global_search.placeholderText() == "搜索患者姓名、手机号、病历号"
shell_window.global_search.placeholderText() == "搜索患者姓名、手机号..."
)
assert shell_window.assistant_card.isVisible()
assert shell_window.assistant_button.text() == "开始对话"