"""Render deterministic offscreen diagnosis-index reference screenshots.""" from __future__ import annotations import os from pathlib import Path from typing import Any os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") from PySide6.QtCore import Qt, QThreadPool, Signal from PySide6.QtGui import QColor, QImage, QPainter, QPixmap from PySide6.QtWidgets import QApplication, QToolButton, QWidget from doctor_workstation.core import PermissionSet from doctor_workstation.ui import apply_theme from doctor_workstation.ui.pages import consultations as consultations_module from doctor_workstation.ui.pages.consultations import ConsultationsPage class _ScreenshotDiagnosisDialog(QWidget): """Invisible list-rendering seam; no detail drawer is exercised in this script.""" saved = Signal() def __init__(self, _repository: Any, parent: QWidget | None = None) -> None: super().__init__(parent) def open_for(self, *_args: Any, **_kwargs: Any) -> None: return None consultations_module.DiagnosisDialog = _ScreenshotDiagnosisDialog DENSITY_SIZES = ((1366, 768), (1710, 920)) def _row(identifier: int, variant: int) -> dict[str, Any]: common: dict[str, Any] = { "id": identifier, "diagnosis_id": identifier, "patient_id": identifier + 1000, "patient_name": ("林晓岚", "周明远", "许安然", "沈知夏")[variant % 4], "gender": 2 if variant % 2 else 1, "age": 28 + variant, "assistant_id": 8, "assistant_name": "赵医助", "assign_read_at": None if variant == 0 else "2026-08-10 08:30", "DiagnosisViewRecord": [{"is_confirmed": 1}], "has_appointment": 1, "appointment_id": identifier + 2000, "appointment_status": 1, "appointment_doctor_id": 18, "appointments": [ { "id": identifier + 2000, "status": 1, "doctor_id": 18, "doctor_name": "陈医生", "time_text": "今天 09:00-09:30", } ], "latest_appointment_channel_text": "健康顾问转介", "has_prescription": 1, "prescription_audit_status": 1, "prescription_void_status": 0, "followup_time_text": "2026-08-17 09:00", "followup_doctor_name": "陈医生", "followup_rx_voided": 0, "unserved_days": (1, 4, 8, 2)[variant % 4], "last_blood_record_at": "2026-08-09 20:10", "video_call_hint": "未在通话中", } if variant == 1: common.update( { "has_appointment": 0, "appointment_id": 0, "appointment_status": None, "appointments": [], "DiagnosisViewRecord": [{"is_confirmed": 0}], "assistant_id": 0, "assistant_name": "", "has_prescription": 0, "followup_time_text": "", "last_blood_record_at": "", "unserved_days": None, } ) elif variant == 2: common.update( { "appointment_status": 4, "appointments": [ { "id": identifier + 2000, "status": 4, "doctor_id": 18, "doctor_name": "陈医生", "time_text": "昨天 15:00-15:30", }, { "id": identifier + 2001, "status": 3, "doctor_id": 19, "doctor_name": "李医生", "time_text": "08-03 10:30-11:00", }, ], "followup_rx_voided": 1, } ) return common class ScreenshotRepository: """No-network repository used only by the visual renderer.""" def __init__(self) -> None: self.rows = [_row(501 + index, index % 4) for index in range(15)] def list_consultations( self, *, page_no: int = 1, page_size: int = 15, **_filters: Any, ) -> dict[str, Any]: start = max(0, page_no - 1) * page_size return {"lists": self.rows[start : start + page_size], "count": 42} def get_dictionary(self, dictionary_type: str) -> list[dict[str, Any]]: values = { "diagnosis_type": [("中医", "tcm"), ("中西医结合", "integrated")], "syndrome_type": [("痰湿", "phlegm_damp"), ("气虚", "qi_deficiency")], "channels": [("健康顾问", "advisor"), ("门诊", "clinic")], } return [{"name": name, "value": value} for name, value in values.get(dictionary_type, [])] def list_diagnosis_assistants(self) -> list[dict[str, Any]]: return [{"id": 8, "name": "赵医助"}, {"id": 9, "name": "王医助"}] def cancel_diagnosis_appointment(self, appointment_id: int) -> None: raise RuntimeError(f"visual fixture does not cancel appointment #{appointment_id}") def generate_video_qrcode( self, doctor_id: int, patient_id: int, share_user_id: int ) -> dict[str, str]: raise RuntimeError( f"visual fixture does not generate video QR: {doctor_id}/{patient_id}/{share_user_id}" ) def generate_diagnosis_qrcode( self, diagnosis_id: int, doctor_id: int, patient_id: int, share_user_id: int, ) -> dict[str, str]: raise RuntimeError( "visual fixture does not generate diagnosis QR: " f"{diagnosis_id}/{doctor_id}/{patient_id}/{share_user_id}" ) def list_appointment_logs(self, diagnosis_id: int) -> list[dict[str, Any]]: raise RuntimeError(f"visual fixture does not load logs for diagnosis #{diagnosis_id}") def create_diagnosis_order( self, patient_id: int, order_type: int, amount: float, *, remark: str = "", ) -> dict[str, Any]: raise RuntimeError( f"visual fixture does not create order: {patient_id}/{order_type}/{amount}/{remark}" ) def generate_order_qrcode(self, order_no: str) -> dict[str, Any]: raise RuntimeError(f"visual fixture does not generate payment QR: {order_no}") # The list only checks these methods as a fail-closed capability contract; # rendering never starts a call. def get_call_ticket(self, patient_id: int, diagnosis_id: int) -> None: raise RuntimeError(f"visual fixture does not issue tickets: {patient_id}/{diagnosis_id}") def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> None: raise RuntimeError( f"visual fixture does not start calls: {diagnosis_id}/{patient_id}/{call_type}" ) def bind_call_room(self, diagnosis_id: int, room_id: str) -> None: raise RuntimeError(f"visual fixture does not bind rooms: {diagnosis_id}/{room_id}") def end_call(self, diagnosis_id: int) -> None: raise RuntimeError(f"visual fixture does not end calls: {diagnosis_id}") def _settle(app: QApplication, page: ConsultationsPage) -> None: QThreadPool.globalInstance().waitForDone(3000) for _ in range(8): app.processEvents() page.poll_timer.stop() page.loading_overlay.stop() page.page_scroll.verticalScrollBar().setValue(0) app.processEvents() def _new_page( app: QApplication, repository: ScreenshotRepository, width: int, height: int, *, permissions: PermissionSet | None = None, ) -> ConsultationsPage: page = ConsultationsPage( repository, permissions=permissions or PermissionSet(["*"]), current_user={"id": 1, "name": "陈医生"}, ) page.resize(width, height) page.show() _settle(app, page) return page def _save(page: ConsultationsPage, path: Path) -> Path: if not page.grab().save(str(path), "PNG"): raise RuntimeError(f"failed to save {path}") return path def _save_with_menu(page: ConsultationsPage, path: Path) -> Path: """Composite the real QMenu popup over the real page grab for offscreen determinism.""" index = page.table_host.model.index(0, 11) cell = page.table_host.fixed.indexWidget(index) more = next(button for button in cell.findChildren(QToolButton) if button.menu()) menu = more.menu() menu.ensurePolished() menu.adjustSize() menu.resize(menu.sizeHint()) menu.show() QApplication.processEvents() page_pixmap = page.grab() menu_pixmap = menu.grab() painter = QPainter(page_pixmap) x = max(16, page.width() - menu_pixmap.width() - 24) y = min(page.height() - menu_pixmap.height() - 16, 330) painter.drawPixmap(x, max(16, y), menu_pixmap) painter.end() menu.hide() if not page_pixmap.save(str(path), "PNG"): raise RuntimeError(f"failed to save {path}") return path def _payment_qr_fixture() -> QPixmap: """Create only the renderer's stand-in for a server-fetched QR image.""" image = QImage(256, 256, QImage.Format.Format_ARGB32) image.fill(QColor("#FFFFFF")) painter = QPainter(image) painter.setPen(Qt.PenStyle.NoPen) module = 8 origin = 12 def draw_finder(left: int, top: int) -> None: painter.setBrush(QColor("#111111")) painter.drawRect(origin + left * module, origin + top * module, 7 * module, 7 * module) painter.setBrush(QColor("#FFFFFF")) painter.drawRect( origin + (left + 1) * module, origin + (top + 1) * module, 5 * module, 5 * module, ) painter.setBrush(QColor("#111111")) painter.drawRect( origin + (left + 2) * module, origin + (top + 2) * module, 3 * module, 3 * module, ) for y in range(29): for x in range(29): inside_finder = (x < 8 and y < 8) or (x > 20 and y < 8) or (x < 8 and y > 20) if not inside_finder and ((x * 11 + y * 7 + x * y) % 5 in {0, 2}): painter.setBrush(QColor("#111111")) painter.drawRect(origin + x * module, origin + y * module, module, module) draw_finder(0, 0) draw_finder(22, 0) draw_finder(0, 22) painter.end() return QPixmap.fromImage(image) def _save_with_payment_qr( page: ConsultationsPage, path: Path, ) -> Path: record = page.table_host.model.rows[0] dialog = consultations_module._DiagnosisOrderQrDialog( record, "ZYT202608100001", page, ) dialog.set_result("https://api.zyt.example/payment/ZYT202608100001/qrcode.png") dialog.preview.show_loading() dialog.preview.setText("") dialog.preview.setPixmap(_payment_qr_fixture()) dialog.ensurePolished() dialog.adjustSize() dialog.show() QApplication.processEvents() page_pixmap = page.grab() dialog_pixmap = dialog.grab() painter = QPainter(page_pixmap) painter.fillRect(page_pixmap.rect(), QColor(0, 0, 0, 52)) x = (page_pixmap.width() - dialog_pixmap.width()) // 2 y = max(18, (page_pixmap.height() - dialog_pixmap.height()) // 2) painter.fillRect( x - 8, y - 8, dialog_pixmap.width() + 16, dialog_pixmap.height() + 16, QColor(0, 0, 0, 34), ) painter.drawPixmap(x, y, dialog_pixmap) painter.end() dialog.close() if not page_pixmap.save(str(path), "PNG"): raise RuntimeError(f"failed to save {path}") return path def _application() -> QApplication: app = QApplication.instance() or QApplication([]) apply_theme(app) return app def render_density() -> list[Path]: """Render only the two desktop-density acceptance sizes.""" app = _application() root = Path(__file__).resolve().parents[1] output = root / "artifacts" / "diagnosis_visual" output.mkdir(parents=True, exist_ok=True) paths: list[Path] = [] repository = ScreenshotRepository() for width, height in DENSITY_SIZES: page = _new_page(app, repository, width, height) path = output / f"diagnosis_{width}x{height}.png" paths.append(_save(page, path)) page.close() app.processEvents() return paths def render() -> list[Path]: app = _application() root = Path(__file__).resolve().parents[1] output = root / "artifacts" / "diagnosis_visual" output.mkdir(parents=True, exist_ok=True) paths: list[Path] = [] repository = ScreenshotRepository() for width, height in ((1024, 640), *DENSITY_SIZES, (1440, 900)): page = _new_page(app, repository, width, height) path = output / f"diagnosis_{width}x{height}.png" paths.append(_save(page, path)) page.close() app.processEvents() scenarios: tuple[tuple[str, int, int, str], ...] = ( ("loading", 1280, 800, "loading"), ("empty", 1280, 800, "empty"), ("error", 1280, 800, "error"), ("hover_warning", 1280, 800, "hover"), ("focus", 1280, 800, "focus"), ("permissions_cropped", 1280, 800, "permissions"), ("horizontal_scroll", 1024, 640, "horizontal"), ("pending_assign", 1280, 800, "pending_assign"), ("advanced_filters", 1280, 800, "advanced"), ) for filename, width, height, state in scenarios: permissions = ( PermissionSet(["tcm.diagnosis/readonlyDetail"]) if state == "permissions" else PermissionSet(["*"]) ) page = _new_page(app, repository, width, height, permissions=permissions) if state == "loading": page.table_host.begin_loading() page.loading_overlay.start() elif state == "empty": page.table_host.set_rows([]) page.pager.update_state(1, 0) elif state == "error": page.table_host.set_rows([]) page.pager.update_state(1, 0) page.table_host.show_error("列表加载失败,请重试\n网络连接不可用") elif state == "hover": page.table_host.model.set_hover_row(1) page.table_host.main.viewport().update() page.table_host.fixed.viewport().update() elif state == "focus": page.activateWindow() page.keyword_edit.setFocus(Qt.FocusReason.TabFocusReason) elif state == "horizontal": scrollbar = page.table_host.main.horizontalScrollBar() scrollbar.setValue(scrollbar.maximum()) elif state == "pending_assign": page._appointment_date = "" page._pending_assign = "1" page._update_quick_buttons() elif state == "advanced": page.more_filter_button.setChecked(True) page._toggle_advanced_filters(True) app.processEvents() page.page_scroll.verticalScrollBar().setValue(0) if state != "focus": app.processEvents() path = output / f"diagnosis_{filename}_{width}x{height}.png" paths.append(_save(page, path)) page.loading_overlay.stop() page.close() app.processEvents() page = _new_page(app, repository, 1280, 800) full_menu = output / "diagnosis_full_menu_1280x800.png" paths.append(_save_with_menu(page, full_menu)) page.close() app.processEvents() page = _new_page(app, repository, 1280, 800) payment_qr = output / "diagnosis_order_qrcode_1280x800.png" paths.append(_save_with_payment_qr(page, payment_qr)) page.close() app.processEvents() page = _new_page(app, repository, 1280, 800) double = _row(880, 0) double.update( { "appointment_id": 2880, "appointment_status": 1, "appointments": [ { "id": 2880, "status": 1, "doctor_id": 18, "doctor_name": "陈医生", "time_text": "今天 09:00-09:30", }, { "id": 2881, "status": 4, "doctor_id": 19, "doctor_name": "李医生", "time_text": "昨天 15:00-15:30", }, ], } ) page.table_host.set_rows([double, _row(881, 1), _row(882, 3)]) page.pager.update_state(1, 3) for _ in range(4): page.page_scroll.verticalScrollBar().setValue(0) app.processEvents() double_cancel = output / "diagnosis_double_appointment_cancel_1280x800.png" paths.append(_save(page, double_cancel)) page.close() app.processEvents() return paths if __name__ == "__main__": for rendered in render(): print(rendered)