This commit is contained in:
Your Name
2026-08-14 14:37:30 +08:00
parent 21790e35f4
commit 18c15d1262
117 changed files with 28157 additions and 8080 deletions
+19 -16
View File
@@ -138,17 +138,20 @@ class DemoVideoDialog(QDialog):
self.setWindowTitle("视频面诊 · 演示模式")
self.setMinimumSize(760, 520)
self.resize(980, 660)
self.setModal(False)
self.setStyleSheet(
"QDialog{background:#0B1210;}"
"QLabel{color:#EAF2EE;}"
"QFrame#RemoteStage{background:#14211E;border:1px solid #2C403A;border-radius:18px;}"
"QFrame#LocalStage{background:#20312C;border:1px solid #3C554D;border-radius:14px;}"
"QPushButton{min-width:96px;min-height:42px;border-radius:21px;background:#253A34;"
"color:#F4F8F6;border:1px solid #3C554D;}"
"QPushButton:hover{background:#304A42;}"
"QPushButton#Hangup{background:#B94B44;border-color:#CF625B;}"
)
self.setModal(False)
self.setStyleSheet(
"QDialog{background:#F7F9FE;color:#111F46;}"
"QLabel{color:#111F46;}"
"QFrame#RemoteStage{background:#0E1421;border:1px solid #29334F;border-radius:16px;}"
"QFrame#RemoteStage QLabel{color:#F7F9FE;}"
"QFrame#LocalStage{background:#151D31;border:1px solid #3F4E75;border-radius:12px;}"
"QPushButton{min-width:96px;min-height:40px;padding:0 16px;border-radius:9px;"
"background:#FFFFFF;color:#3F4E75;border:1px solid #E6EAF5;font-weight:600;}"
"QPushButton:hover{color:#4451E2;background:#F0F2FF;border-color:#5761F4;}"
"QPushButton:checked{color:#FFFFFF;background:#5761F4;border-color:#5761F4;}"
"QPushButton#Hangup{color:#FFFFFF;background:#F15B67;border-color:#F15B67;}"
"QPushButton#Hangup:hover{background:#D94857;border-color:#D94857;}"
)
root = QVBoxLayout(self)
root.setContentsMargins(22, 18, 22, 22)
@@ -159,7 +162,7 @@ class DemoVideoDialog(QDialog):
header.addWidget(title)
header.addStretch(1)
demo = QLabel("● 演示模式 · 未连接腾讯云")
demo.setStyleSheet("color:#91B9AC;font-size:12px;")
demo.setStyleSheet("color:#7886AA;font-size:12px;")
header.addWidget(demo)
self.duration_label = QLabel("00:00")
self.duration_label.setStyleSheet("font-weight:700;")
@@ -174,8 +177,8 @@ class DemoVideoDialog(QDialog):
avatar = QLabel((patient_name or "")[:1])
avatar.setAlignment(Qt.AlignmentFlag.AlignCenter)
avatar.setFixedSize(104, 104)
avatar.setStyleSheet(
"background:#DDF1EC;color:#0F6D64;border-radius:52px;font-size:42px;font-weight:700;"
avatar.setStyleSheet(
"background:#F0F2FF;color:#5761F4;border-radius:52px;font-size:42px;font-weight:700;"
)
stage_layout.addWidget(avatar, 0, Qt.AlignmentFlag.AlignHCenter)
waiting = QLabel("等待患者接听…")
@@ -184,7 +187,7 @@ class DemoVideoDialog(QDialog):
stage_layout.addWidget(waiting)
hint = QLabel("生产模式将通过后端短时 UserSig 初始化腾讯 TUICallKit")
hint.setAlignment(Qt.AlignmentFlag.AlignCenter)
hint.setStyleSheet("color:#80948D;font-size:12px;")
hint.setStyleSheet("color:#A4ADC3;font-size:12px;")
stage_layout.addWidget(hint)
stage_layout.addStretch(1)
@@ -194,7 +197,7 @@ class DemoVideoDialog(QDialog):
local_layout = QVBoxLayout(local)
local_label = QLabel("医生画面")
local_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
local_label.setStyleSheet("color:#A9BDB6;font-weight:600;")
local_label.setStyleSheet("color:#D9E0F2;font-weight:600;")
local_layout.addWidget(local_label)
root.addWidget(stage, 1)
@@ -4,6 +4,7 @@ from .api_client import ApiClient
from .factory import build_repository
from .mock_repository import DEMO_PERMISSIONS, DemoDoctorRepository
from .repository import (
DIAGNOSIS_AI_PERMISSIONS,
PRESCRIPTION_LIBRARY_PERMISSIONS,
PRESCRIPTION_PERMISSIONS,
AuditAction,
@@ -17,6 +18,7 @@ __all__ = [
"AuditAction",
"DEMO_PERMISSIONS",
"DemoDoctorRepository",
"DIAGNOSIS_AI_PERMISSIONS",
"DoctorRepository",
"KeyringLike",
"PRESCRIPTION_LIBRARY_PERMISSIONS",
@@ -111,10 +111,13 @@ class ApiClient:
params: Mapping[str, Any] | None = None,
*,
headers: Mapping[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
) -> Any:
"""Issue a GET request and return the unwrapped envelope data."""
return self.request("GET", endpoint, params=params, headers=headers)
return self.request(
"GET", endpoint, params=params, headers=headers, timeout=timeout
)
def post(
self,
@@ -123,13 +126,16 @@ class ApiClient:
*,
json: Mapping[str, Any] | None = None,
headers: Mapping[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
) -> Any:
"""Issue a non-retried JSON POST and return the unwrapped data."""
if payload is not None and json is not None:
raise ValueError("pass either payload or json, not both")
body = json if json is not None else payload
return self.request("POST", endpoint, json=body or {}, headers=headers)
return self.request(
"POST", endpoint, json=body or {}, headers=headers, timeout=timeout
)
def post_multipart(
self,
@@ -226,6 +232,7 @@ class ApiClient:
data: Mapping[str, Any] | None = None,
files: Mapping[str, Any] | None = None,
headers: Mapping[str, str] | None = None,
timeout: float | httpx.Timeout | None = None,
) -> Any:
"""Issue one API request with structured transport/envelope errors."""
@@ -245,6 +252,7 @@ class ApiClient:
)
attempts = self.max_retries + 1 if verb == "GET" else 1
response: httpx.Response | None = None
request_timeout = self.timeout if timeout is None else timeout
for attempt in range(attempts):
try:
response = self._client.request(
@@ -255,7 +263,7 @@ class ApiClient:
data=dict(data) if verb == "POST" and data is not None else None,
files=dict(files) if files is not None else None,
headers=request_headers,
timeout=self.timeout,
timeout=request_timeout,
)
break
except httpx.TimeoutException as exc:
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
"""Compatibility module exporting the production doctor repository."""
from .repository import (
DIAGNOSIS_AI_PERMISSIONS,
PRESCRIPTION_LIBRARY_PERMISSIONS,
PRESCRIPTION_PERMISSIONS,
AuditAction,
@@ -11,6 +12,7 @@ from .repository import (
__all__ = [
"AuditAction",
"DoctorRepository",
"DIAGNOSIS_AI_PERMISSIONS",
"PRESCRIPTION_LIBRARY_PERMISSIONS",
"PRESCRIPTION_PERMISSIONS",
"RemoteDoctorRepository",
+450 -176
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import mimetypes
import re
import time
from collections.abc import Mapping, Sequence
from collections.abc import Mapping, Sequence
from contextlib import suppress
from datetime import date
from io import BytesIO
@@ -41,9 +41,21 @@ PRESCRIPTION_LIBRARY_PERMISSIONS: Final[dict[str, str]] = {
"read": "wcf.prescription/read",
"update": "wcf.prescription/edit",
"delete": "wcf.prescription/delete",
"ai_reports": "tcm.prescriptionLibrary/aiReports",
"generate_ai_reports": "tcm.prescriptionLibrary/generateAiReports",
"edit_ai_report": "tcm.prescriptionLibrary/editAiReport",
}
"""Canonical permissions used by the routed prescription-library view."""
DIAGNOSIS_AI_PERMISSIONS: Final[dict[str, str]] = {
"ai_reports": "tcm.diagnosis/aiReports",
"generate_ai_reports": "tcm.diagnosis/generateAiReports",
"edit_ai_report": "tcm.diagnosis/editAiReport",
"analysis": "tcm.diagnosis/aiAnalysis",
"assistant": "tcm.diagnosis/aiAssistant",
}
"""Canonical permissions used by reception and patient-profile AI reports."""
PRESCRIPTION_PERMISSIONS: Final[dict[str, str]] = {
"create": "cf.prescription/add",
"read": "cf.prescription/read",
@@ -140,6 +152,64 @@ class DoctorRepository(Protocol):
def delete_prescription_template(self, template_id: int) -> Any:
"""Delete a prescription-library record."""
def list_prescription_template_ai_reports(self, template_id: int) -> dict[str, Any]:
"""Return saved AI interpretation reports for one library template."""
def generate_prescription_template_ai_reports(self, template_id: int) -> dict[str, Any]:
"""Regenerate every model report for one library template."""
def edit_prescription_template_ai_report(
self,
template_id: int,
*,
report_id: int,
content: str,
) -> dict[str, Any]:
"""Save a manually edited AI report for one library template."""
def list_diagnosis_ai_reports(self, diagnosis_id: int) -> dict[str, Any]:
"""Return saved AI reports for one diagnosis / patient profile."""
def generate_diagnosis_ai_reports(self, diagnosis_id: int) -> dict[str, Any]:
"""Regenerate every model report for one diagnosis."""
def edit_diagnosis_ai_report(
self,
diagnosis_id: int,
*,
report_id: int,
content: str,
) -> dict[str, Any]:
"""Save a manually edited AI report for one diagnosis."""
def analyze_diagnosis_ai(
self,
diagnosis_id: int,
prompt: str,
*,
task: str = "custom",
) -> dict[str, Any]:
"""Ask the first-party diagnosis assistant; the server selects the model."""
def get_diagnosis_ai_analysis(
self,
diagnosis_id: int,
*,
model: Literal["qwen", "openai"] = "qwen",
) -> dict[str, Any]:
"""Generate one model's structured reception analysis for a diagnosis."""
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
"""Return every persisted AI diagnosis snapshot for one patient."""
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: Literal["qwen", "openai"],
) -> dict[str, Any]:
"""Append one model-specific AI diagnosis snapshot for one patient."""
def get_prescription(self, prescription_id: int) -> Prescription:
"""Return one issued prescription."""
@@ -619,38 +689,38 @@ class DoctorRepository(Protocol):
def end_call(self, diagnosis_id: int) -> Any:
"""End the active diagnosis call."""
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind a TRTC room to the active call."""
def start_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
*,
language: str = "zh-CN",
) -> Any:
"""Start a transcript stored on one exact call record."""
def upsert_call_transcript_segments(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
segments: Sequence[Mapping[str, Any]],
) -> Any:
"""Idempotently persist completed transcript segments."""
def finish_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
expected_segment_count: int,
*,
status: str = "completed",
) -> Any:
"""Finalize and materialize the transcript text on a call record."""
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind a TRTC room to the active call."""
def start_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
*,
language: str = "zh-CN",
) -> Any:
"""Start a transcript stored on one exact call record."""
def upsert_call_transcript_segments(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
segments: Sequence[Mapping[str, Any]],
) -> Any:
"""Idempotently persist completed transcript segments."""
def finish_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
expected_segment_count: int,
*,
status: str = "completed",
) -> Any:
"""Finalize and materialize the transcript text on a call record."""
def my_self(self) -> Session:
"""Compatibility alias for :meth:`get_session`."""
@@ -1165,7 +1235,191 @@ class RemoteDoctorRepository:
return self.client.post("tcm.prescriptionLibrary/delete", {"id": template_id})
def list_medicines(
def list_prescription_template_ai_reports(self, template_id: int) -> dict[str, Any]:
"""Read persisted AI reports without triggering model generation."""
payload = _client_request(
self.client,
"get",
"tcm.prescriptionLibrary/aiReports",
{"id": template_id},
timeout=30.0,
)
return dict(_require_mapping(payload, "tcm.prescriptionLibrary/aiReports"))
def generate_prescription_template_ai_reports(self, template_id: int) -> dict[str, Any]:
"""Regenerate the multi-model diagnosis report; not retried by the client."""
payload = _client_request(
self.client,
"post",
"tcm.prescriptionLibrary/generateAiReports",
{"id": template_id},
timeout=210.0,
)
return dict(_require_mapping(payload, "tcm.prescriptionLibrary/generateAiReports"))
def edit_prescription_template_ai_report(
self,
template_id: int,
*,
report_id: int,
content: str,
) -> dict[str, Any]:
"""Persist a manual edit of one saved model report."""
payload = _client_request(
self.client,
"post",
"tcm.prescriptionLibrary/editAiReport",
{"id": template_id, "report_id": report_id, "content": content},
timeout=30.0,
)
return dict(_require_mapping(payload, "tcm.prescriptionLibrary/editAiReport"))
def list_diagnosis_ai_reports(self, diagnosis_id: int) -> dict[str, Any]:
"""Read persisted diagnosis AI reports without triggering model generation."""
payload = _client_request(
self.client,
"get",
"tcm.diagnosis/aiReports",
{"id": diagnosis_id},
timeout=30.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/aiReports"))
def generate_diagnosis_ai_reports(self, diagnosis_id: int) -> dict[str, Any]:
"""Regenerate the multi-model diagnosis report; not retried by the client."""
payload = _client_request(
self.client,
"post",
"tcm.diagnosis/generateAiReports",
{"id": diagnosis_id},
timeout=210.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/generateAiReports"))
def edit_diagnosis_ai_report(
self,
diagnosis_id: int,
*,
report_id: int,
content: str,
) -> dict[str, Any]:
"""Persist a manual edit of one saved diagnosis report."""
payload = _client_request(
self.client,
"post",
"tcm.diagnosis/editAiReport",
{"id": diagnosis_id, "report_id": report_id, "content": content},
timeout=30.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/editAiReport"))
def analyze_diagnosis_ai(
self,
diagnosis_id: int,
prompt: str,
*,
task: str = "custom",
) -> dict[str, Any]:
"""Submit a diagnosis question to the first-party server assistant."""
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
clean_prompt = prompt.strip()
if not clean_prompt:
raise ValueError("prompt is required")
if len(clean_prompt) > 500:
raise ValueError("prompt must not exceed 500 characters")
clean_task = task.strip().lower() or "custom"
if clean_task not in {
"summary",
"tcm_pattern",
"prescription_review",
"medication_review",
"exam_review",
"complication_risk",
"guideline_review",
"custom",
}:
raise ValueError("task is not supported")
payload = _client_request(
self.client,
"post",
"tcm.diagnosis/aiAssistant",
{"id": diagnosis_id, "prompt": clean_prompt, "task": clean_task},
# The upstream is allowed 90 seconds by server configuration. Keep a
# small transport buffer so the desktop can receive the server's own
# timeout response instead of racing it.
timeout=105.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/aiAssistant"))
def get_diagnosis_ai_analysis(
self,
diagnosis_id: int,
*,
model: Literal["qwen", "openai"] = "qwen",
) -> dict[str, Any]:
"""Generate one model's structured analysis via the first-party API."""
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
clean_model = str(model).strip().lower()
if clean_model not in {"qwen", "openai"}:
raise ValueError("model must be qwen or openai")
payload = _client_request(
self.client,
"post",
"tcm.diagnosis/aiAnalysis",
{"id": diagnosis_id, "model": clean_model},
timeout=105.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/aiAnalysis"))
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
"""Read patient-level snapshots without triggering model generation."""
if patient_id <= 0:
raise ValueError("patient_id must be positive")
payload = _client_request(
self.client,
"get",
"tcm.diagnosis/patientAiReports",
{"patient_id": patient_id},
timeout=30.0,
)
return dict(_require_mapping(payload, "tcm.diagnosis/patientAiReports"))
def generate_patient_ai_report(
self,
patient_id: int,
*,
model: Literal["qwen", "openai"],
) -> dict[str, Any]:
"""Append one patient snapshot; the desktop never sends provider secrets."""
if patient_id <= 0:
raise ValueError("patient_id must be positive")
clean_model = str(model).strip().lower()
if clean_model not in {"qwen", "openai"}:
raise ValueError("model must be qwen or openai")
payload = _client_request(
self.client,
"post",
"tcm.diagnosis/generatePatientAiReport",
{"patient_id": patient_id, "model": clean_model},
timeout=105.0,
)
return dict(
_require_mapping(payload, "tcm.diagnosis/generatePatientAiReport")
)
def list_medicines(
self,
*,
name: str = "",
@@ -2201,157 +2455,157 @@ class RemoteDoctorRepository:
ticket.diagnosis_id = diagnosis_id
return ticket
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
"""Create the server-side call record before ringing participants."""
payload = self.client.post(
"tcm.diagnosis/startCall",
{
"diagnosis_id": diagnosis_id,
"patient_id": patient_id,
"call_type": call_type,
},
)
if not isinstance(payload, Mapping):
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id object", data=payload
)
raw_id = next(
(
payload[key]
for key in ("call_record_id", "callRecordId", "id")
if key in payload
),
None,
)
try:
if isinstance(raw_id, bool):
raise ValueError
call_record_id = int(raw_id)
except (TypeError, ValueError) as exc:
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id",
data=dict(payload),
) from exc
if call_record_id <= 0:
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id",
data=dict(payload),
)
return {"call_record_id": call_record_id}
def start_call(self, diagnosis_id: int, patient_id: int, *, call_type: int = 2) -> Any:
"""Create the server-side call record before ringing participants."""
payload = self.client.post(
"tcm.diagnosis/startCall",
{
"diagnosis_id": diagnosis_id,
"patient_id": patient_id,
"call_type": call_type,
},
)
if not isinstance(payload, Mapping):
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id object", data=payload
)
raw_id = next(
(
payload[key]
for key in ("call_record_id", "callRecordId", "id")
if key in payload
),
None,
)
try:
if isinstance(raw_id, bool):
raise ValueError
call_record_id = int(raw_id)
except (TypeError, ValueError) as exc:
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id",
data=dict(payload),
) from exc
if call_record_id <= 0:
raise ApiProtocolError(
"tcm.diagnosis/startCall returned no valid call_record_id",
data=dict(payload),
)
return {"call_record_id": call_record_id}
def end_call(self, diagnosis_id: int) -> Any:
"""End the active call/recording associated with a diagnosis."""
return self.client.post("tcm.diagnosis/endCall", {"diagnosis_id": diagnosis_id})
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind the actual TRTC room to the active call record."""
def bind_call_room(self, diagnosis_id: int, room_id: str) -> Any:
"""Bind the actual TRTC room to the active call record."""
if not room_id.strip():
raise ValueError("room_id is required")
return self.client.post(
"tcm.diagnosis/bindCallRoom",
{"diagnosis_id": diagnosis_id, "room_id": room_id.strip()},
)
@staticmethod
def _transcription_identity(
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
) -> dict[str, Any]:
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
if isinstance(call_record_id, bool) or not str(call_record_id).strip():
raise ValueError("call_record_id is required")
clean_session = transcription_session_id.strip()
if not clean_session or len(clean_session) > 128:
raise ValueError("transcription_session_id must contain 1 to 128 characters")
return {
"diagnosis_id": diagnosis_id,
"call_record_id": call_record_id,
"transcription_session_id": clean_session,
}
def start_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
*,
language: str = "zh-CN",
) -> Any:
"""Create the server transcript session for one call record."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
body["language"] = language.strip()[:32] or "zh-CN"
return self.client.post("tcm.diagnosis/startCallTranscription", body)
def upsert_call_transcript_segments(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
segments: Sequence[Mapping[str, Any]],
) -> Any:
"""Upsert a bounded batch using each segment_id as the idempotency key."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
normalized: list[dict[str, Any]] = []
if len(segments) > 50:
raise ValueError("at most 50 transcript segments may be submitted at once")
for segment in segments:
segment_id = str(segment.get("segment_id") or "").strip()
text = str(segment.get("text") or "").strip()
if not segment_id or len(segment_id) > 160:
raise ValueError("transcript segment_id is invalid")
if not text or len(text) > 4_000:
raise ValueError("transcript text is invalid")
normalized.append(
{
"segment_id": segment_id,
"speaker_user_id": str(segment.get("speaker_user_id") or "")[:160],
"speaker_role": str(segment.get("speaker_role") or "unknown")[:20],
"timestamp": max(int(segment.get("timestamp") or 0), 0),
"text": text,
}
)
if not normalized:
raise ValueError("at least one transcript segment is required")
body["segments"] = normalized
return self.client.post("tcm.diagnosis/upsertCallTranscriptSegments", body)
def finish_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
expected_segment_count: int,
*,
status: str = "completed",
) -> Any:
"""Finalize a transcript; repeated requests use the same session identity."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
clean_status = status.strip().lower()
if clean_status not in {"completed", "partial", "failed"}:
raise ValueError("transcription status is invalid")
if expected_segment_count < 0:
raise ValueError("expected_segment_count must not be negative")
body.update(
{
"expected_segment_count": expected_segment_count,
"status": clean_status,
}
)
return self.client.post("tcm.diagnosis/finishCallTranscription", body)
return self.client.post(
"tcm.diagnosis/bindCallRoom",
{"diagnosis_id": diagnosis_id, "room_id": room_id.strip()},
)
@staticmethod
def _transcription_identity(
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
) -> dict[str, Any]:
if diagnosis_id <= 0:
raise ValueError("diagnosis_id must be positive")
if isinstance(call_record_id, bool) or not str(call_record_id).strip():
raise ValueError("call_record_id is required")
clean_session = transcription_session_id.strip()
if not clean_session or len(clean_session) > 128:
raise ValueError("transcription_session_id must contain 1 to 128 characters")
return {
"diagnosis_id": diagnosis_id,
"call_record_id": call_record_id,
"transcription_session_id": clean_session,
}
def start_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
*,
language: str = "zh-CN",
) -> Any:
"""Create the server transcript session for one call record."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
body["language"] = language.strip()[:32] or "zh-CN"
return self.client.post("tcm.diagnosis/startCallTranscription", body)
def upsert_call_transcript_segments(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
segments: Sequence[Mapping[str, Any]],
) -> Any:
"""Upsert a bounded batch using each segment_id as the idempotency key."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
normalized: list[dict[str, Any]] = []
if len(segments) > 50:
raise ValueError("at most 50 transcript segments may be submitted at once")
for segment in segments:
segment_id = str(segment.get("segment_id") or "").strip()
text = str(segment.get("text") or "").strip()
if not segment_id or len(segment_id) > 160:
raise ValueError("transcript segment_id is invalid")
if not text or len(text) > 4_000:
raise ValueError("transcript text is invalid")
normalized.append(
{
"segment_id": segment_id,
"speaker_user_id": str(segment.get("speaker_user_id") or "")[:160],
"speaker_role": str(segment.get("speaker_role") or "unknown")[:20],
"timestamp": max(int(segment.get("timestamp") or 0), 0),
"text": text,
}
)
if not normalized:
raise ValueError("at least one transcript segment is required")
body["segments"] = normalized
return self.client.post("tcm.diagnosis/upsertCallTranscriptSegments", body)
def finish_call_transcription(
self,
diagnosis_id: int,
call_record_id: int | str,
transcription_session_id: str,
expected_segment_count: int,
*,
status: str = "completed",
) -> Any:
"""Finalize a transcript; repeated requests use the same session identity."""
body = self._transcription_identity(
diagnosis_id, call_record_id, transcription_session_id
)
clean_status = status.strip().lower()
if clean_status not in {"completed", "partial", "failed"}:
raise ValueError("transcription status is invalid")
if expected_segment_count < 0:
raise ValueError("expected_segment_count must not be negative")
body.update(
{
"expected_segment_count": expected_segment_count,
"status": clean_status,
}
)
return self.client.post("tcm.diagnosis/finishCallTranscription", body)
# Compatibility aliases keep UI naming independent from endpoint history.
def my_self(self) -> Session:
@@ -2402,6 +2656,26 @@ def _require_mapping(value: object, endpoint: str) -> Mapping[str, Any]:
return value
def _client_request(
client: Any,
method: str,
endpoint: str,
payload: Mapping[str, Any] | None = None,
*,
timeout: float | None = None,
) -> Any:
"""Call get/post, ignoring timeout kwargs that test doubles do not accept."""
fn = getattr(client, method)
params = dict(payload or {})
try:
if timeout is None:
return fn(endpoint, params)
return fn(endpoint, params, timeout=timeout)
except TypeError:
return fn(endpoint, params)
def _material_kind(
material_type: str,
) -> Literal["image", "video", "file"]:
@@ -53,28 +53,28 @@ from .widgets import (
)
APPOINTMENT_DRAWER_QSS = r"""
QDialog#AppointmentDrawerOverlay {
background-color: transparent;
color: #134E4A;
font-family: "PingFang SC", Arial, "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px;
}
QDialog#AppointmentDrawerOverlay {
background-color: transparent;
color: #111F46;
font-family: "Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-size: 13px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerPanel {
background-color: #FFFFFF;
border-left: 1px solid #DCDFE6;
border-left: 1px solid #E6EAF5;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerHeader {
background-color: #FFFFFF;
border: 0;
border-bottom: 1px solid #E2EBE8;
border-bottom: 1px solid #E6EAF5;
}
QDialog#AppointmentDrawerOverlay QLabel#AppointmentDrawerTitle {
color: #134E4A;
font-size: 18px;
font-weight: 600;
color: #111F46;
font-size: 18px;
font-weight: 700;
}
QDialog#AppointmentDrawerOverlay QToolButton#AppointmentDrawerClose {
@@ -86,14 +86,14 @@ QDialog#AppointmentDrawerOverlay QToolButton#AppointmentDrawerClose {
border: 0;
border-radius: 4px;
background-color: transparent;
color: #5B7A76;
color: #7886AA;
font-size: 22px;
font-weight: 400;
}
QDialog#AppointmentDrawerOverlay QToolButton#AppointmentDrawerClose:hover {
color: #0891B2;
background-color: #ECFEFF;
color: #4451E2;
background-color: #F0F2FF;
}
QDialog#AppointmentDrawerOverlay QScrollArea#AppointmentDrawerBody,
@@ -109,13 +109,13 @@ QDialog#AppointmentDrawerOverlay QWidget#AppointmentDrawerBodyContent {
}
QDialog#AppointmentDrawerOverlay QLabel[appointmentLabel="true"] {
color: #5B7A76;
color: #3F4E75;
font-size: 14px;
font-weight: 500;
}
QDialog#AppointmentDrawerOverlay QLabel[appointmentMuted="true"] {
color: #5B7A76;
color: #7886AA;
}
QDialog#AppointmentDrawerOverlay QComboBox,
@@ -123,12 +123,12 @@ QDialog#AppointmentDrawerOverlay QLineEdit,
QDialog#AppointmentDrawerOverlay QPlainTextEdit {
min-height: 30px;
padding: 0 11px;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #E6EAF5;
border-radius: 9px;
background-color: #FFFFFF;
color: #134E4A;
selection-background-color: #A0CFFF;
selection-color: #134E4A;
color: #111F46;
selection-background-color: #5761F4;
selection-color: #FFFFFF;
}
QDialog#AppointmentDrawerOverlay QPlainTextEdit {
@@ -138,13 +138,13 @@ QDialog#AppointmentDrawerOverlay QPlainTextEdit {
QDialog#AppointmentDrawerOverlay QComboBox:hover,
QDialog#AppointmentDrawerOverlay QLineEdit:hover,
QDialog#AppointmentDrawerOverlay QPlainTextEdit:hover {
border-color: #C0C4CC;
border-color: #5761F4;
}
QDialog#AppointmentDrawerOverlay QComboBox:focus,
QDialog#AppointmentDrawerOverlay QLineEdit:focus,
QDialog#AppointmentDrawerOverlay QPlainTextEdit:focus {
border: 2px solid #79BBFF;
border: 2px solid #8D9BFF;
}
QDialog#AppointmentDrawerOverlay QComboBox::drop-down {
@@ -154,77 +154,79 @@ QDialog#AppointmentDrawerOverlay QComboBox::drop-down {
QDialog#AppointmentDrawerOverlay QComboBox QAbstractItemView {
background-color: #FFFFFF;
color: #134E4A;
border: 1px solid #D5E5E2;
selection-background-color: #ECFEFF;
selection-color: #0891B2;
color: #111F46;
border: 1px solid #E6EAF5;
selection-background-color: #5761F4;
selection-color: #FFFFFF;
outline: 0;
}
QDialog#AppointmentDrawerOverlay QRadioButton {
min-height: 24px;
spacing: 8px;
color: #134E4A;
color: #3F4E75;
}
QDialog#AppointmentDrawerOverlay QRadioButton::indicator {
width: 12px;
height: 12px;
border-radius: 7px;
border: 1px solid #DCDFE6;
border: 1px solid #E6EAF5;
background-color: #FFFFFF;
}
QDialog#AppointmentDrawerOverlay QRadioButton::indicator:hover {
border-color: #0891B2;
border-color: #5761F4;
}
QDialog#AppointmentDrawerOverlay QRadioButton::indicator:checked {
width: 4px;
height: 4px;
border: 5px solid #0891B2;
border: 5px solid #5761F4;
border-radius: 7px;
background-color: #FFFFFF;
}
QDialog#AppointmentDrawerOverlay QRadioButton:focus {
color: #0891B2;
color: #4451E2;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentDate="true"] {
padding: 0;
border: 1px solid #DCDFE6;
QDialog#AppointmentDrawerOverlay QPushButton[appointmentDate="true"] {
min-height: 38px;
max-height: 38px;
padding: 0;
border: 1px solid #E6EAF5;
border-radius: 8px;
background-color: #FFFFFF;
color: #5B7A76;
color: #3F4E75;
font-size: 14px;
font-weight: 500;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentDate="true"]:hover {
color: #0891B2;
border-color: #0891B2;
background-color: #ECFEFF;
color: #4451E2;
border-color: #5761F4;
background-color: #F0F2FF;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentDate="true"]:focus {
border-color: #79BBFF;
border-color: #8D9BFF;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentDate="true"]:checked {
color: #FFFFFF;
border-color: #0891B2;
background-color: #0891B2;
border-color: #5761F4;
background-color: #5761F4;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentSlotsPanel {
background-color: #F8F9FA;
background-color: #F7F9FE;
border: 0;
border-radius: 8px;
}
QDialog#AppointmentDrawerOverlay QLabel#AppointmentSlotsTitle {
color: #134E4A;
color: #111F46;
font-size: 15px;
font-weight: 600;
}
@@ -236,27 +238,27 @@ QDialog#AppointmentDrawerOverlay QPushButton#AppointmentRefreshSlots {
border: 0;
border-radius: 4px;
background-color: transparent;
color: #0891B2;
color: #4451E2;
font-size: 13px;
font-weight: 500;
}
QDialog#AppointmentDrawerOverlay QPushButton#AppointmentRefreshSlots:hover {
background-color: #ECFEFF;
background-color: #F0F2FF;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"] {
min-width: 110px;
min-height: 70px;
padding: 0 8px;
border: 2px solid #D5E5E2;
border: 2px solid #E6EAF5;
border-radius: 8px;
background-color: #FFFFFF;
color: #134E4A;
color: #111F46;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"] QLabel#AppointmentSlotTime {
color: #134E4A;
color: #111F46;
font-size: 15px;
font-weight: 600;
}
@@ -265,33 +267,33 @@ QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"] QLabel#Appo
padding: 0 8px;
border-radius: 4px;
background-color: #F4F4F5;
color: #5B7A76;
color: #7886AA;
font-size: 12px;
font-weight: 400;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"][availability="available"] QLabel#AppointmentSlotStatus {
color: #67C23A;
background-color: #F0F9FF;
color: #17A77D;
background-color: #EAF9F3;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:hover:enabled {
color: #0891B2;
border-color: #0891B2;
background-color: #ECFEFF;
color: #4451E2;
border-color: #5761F4;
background-color: #F0F2FF;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:focus:enabled {
border-color: #79BBFF;
border-color: #8D9BFF;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:checked {
color: #FFFFFF;
border-color: #0891B2;
background: qlineargradient(
x1:0, y1:0, x2:1, y2:1,
stop:0 #0891B2,
stop:1 #66B1FF
border-color: #5761F4;
background: qlineargradient(
x1:0, y1:0, x2:1, y2:1,
stop:0 #5761F4,
stop:1 #7769F7
);
}
@@ -305,18 +307,18 @@ QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:checked QLa
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:disabled {
color: #C0C4CC;
border-color: #D5E5E2;
background-color: #F5F7FA;
color: #A4ADC3;
border-color: #E6EAF5;
background-color: #F0F2F8;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:disabled QLabel#AppointmentSlotTime,
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:disabled QLabel#AppointmentSlotStatus {
color: #C0C4CC;
color: #A4ADC3;
}
QDialog#AppointmentDrawerOverlay QPushButton[appointmentSlot="true"]:disabled QLabel#AppointmentSlotStatus {
background-color: #F5F7FA;
background-color: #F0F2F8;
}
QDialog#AppointmentDrawerOverlay QWidget#AppointmentInlineEmpty {
@@ -324,94 +326,94 @@ QDialog#AppointmentDrawerOverlay QWidget#AppointmentInlineEmpty {
}
QDialog#AppointmentDrawerOverlay QLabel#AppointmentEmptyText {
color: #5B7A76;
color: #7886AA;
font-size: 14px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="info"] {
background-color: #ECFEFF;
border: 1px solid #D9ECFF;
border-radius: 4px;
background-color: #F0F4FF;
border: 1px solid #DDE5FF;
border-radius: 9px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="warning"] {
background-color: #FDF6EC;
border: 1px solid #FAECD8;
border-radius: 4px;
background-color: #FFF5E6;
border: 1px solid #F6E3C4;
border-radius: 9px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="danger"] {
background-color: #FEF0F0;
border: 1px solid #FDE2E2;
border-radius: 4px;
background-color: #FFF1F3;
border: 1px solid #F7D7DC;
border-radius: 9px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="success"] {
background-color: #F0F9EB;
border: 1px solid #E1F3D8;
border-radius: 4px;
background-color: #EAF9F3;
border: 1px solid #D4F0E5;
border-radius: 9px;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="info"] QLabel {
color: #0891B2;
color: #4D69ED;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="warning"] QLabel {
color: #E6A23C;
color: #D38625;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="danger"] QLabel {
color: #F56C6C;
color: #F15B67;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentAlert[kind="success"] QLabel {
color: #67C23A;
color: #17A77D;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter {
background-color: #FFFFFF;
border: 0;
border-top: 1px solid #E2EBE8;
border-top: 1px solid #E6EAF5;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton {
min-height: 30px;
max-height: 30px;
padding: 0 15px;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #E6EAF5;
border-radius: 9px;
background-color: #FFFFFF;
color: #5B7A76;
font-size: 14px;
font-weight: 500;
color: #3F4E75;
font-size: 13px;
font-weight: 600;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton:hover {
color: #0891B2;
border-color: #A5F3FC;
background-color: #ECFEFF;
color: #4451E2;
border-color: #5761F4;
background-color: #F0F2FF;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton:focus {
border-color: #79BBFF;
border-color: #8D9BFF;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton[primary="true"] {
color: #FFFFFF;
border-color: #0891B2;
background-color: #0891B2;
border-color: #5761F4;
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #5761F4, stop:1 #7769F7);
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton[primary="true"]:hover {
color: #FFFFFF;
border-color: #66B1FF;
background-color: #66B1FF;
border-color: #4C57E9;
background-color: #4C57E9;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentDrawerFooter QPushButton:disabled {
color: #FFFFFF;
border-color: #A0CFFF;
background-color: #A0CFFF;
border-color: #E6EAF5;
background-color: #A4ADC3;
}
QDialog#AppointmentDrawerOverlay QFrame#AppointmentLoadingOverlay {
@@ -420,7 +422,7 @@ QDialog#AppointmentDrawerOverlay QFrame#AppointmentLoadingOverlay {
}
QDialog#AppointmentDrawerOverlay QLabel#AppointmentLoadingText {
color: #5B7A76;
color: #7886AA;
font-size: 14px;
}
@@ -434,11 +436,11 @@ QDialog#AppointmentDrawerOverlay QScrollBar:vertical {
QDialog#AppointmentDrawerOverlay QScrollBar::handle:vertical {
min-height: 30px;
border-radius: 3px;
background-color: #DCDFE6;
background-color: #E6EAF5;
}
QDialog#AppointmentDrawerOverlay QScrollBar::handle:vertical:hover {
background-color: #C0C4CC;
background-color: #8D9BFF;
}
QDialog#AppointmentDrawerOverlay QScrollBar::add-line:vertical,
+204 -154
View File
@@ -1,9 +1,9 @@
"""Scoped visual primitives for diagnosis readonly pages and edit drawers.
Diagnosis chrome uses the workstation clinical teal palette (not Element blue)
so the drawer feels native to the desktop app while remaining visually isolated
from other pages via object-name selectors.
"""
"""Scoped visual primitives for diagnosis readonly pages and edit drawers.
The diagnosis workspace uses the same cool white, indigo and blue-gray visual
language as the desktop shell. Every rule remains isolated behind diagnosis
object names and dynamic properties so adjacent pages keep their own styling.
"""
from __future__ import annotations
@@ -40,7 +40,6 @@ from PySide6.QtWidgets import (
QLineEdit,
QPlainTextEdit,
QPushButton,
QRadioButton,
QScrollBar,
QSizePolicy,
QTableWidget,
@@ -384,9 +383,11 @@ QLabel#DiagnosisUnitLabel {
}
QLineEdit[diagnosisField="true"],
QComboBox[diagnosisField="true"],
QPlainTextEdit[diagnosisField="true"],
QDateEdit[diagnosisField="true"],
QDoubleSpinBox[diagnosisField="true"] {
QPlainTextEdit[diagnosisField="true"],
QDateEdit[diagnosisField="true"],
QDoubleSpinBox[diagnosisField="true"],
QSpinBox[diagnosisField="true"],
QTimeEdit[diagnosisField="true"] {
color: #134E4A;
background-color: #FFFFFF;
border: 1px solid #D5E5E2;
@@ -405,9 +406,11 @@ QPlainTextEdit[diagnosisField="true"] {
}
QLineEdit[diagnosisField="true"]:focus,
QComboBox[diagnosisField="true"]:focus,
QPlainTextEdit[diagnosisField="true"]:focus,
QDateEdit[diagnosisField="true"]:focus,
QDoubleSpinBox[diagnosisField="true"]:focus {
QPlainTextEdit[diagnosisField="true"]:focus,
QDateEdit[diagnosisField="true"]:focus,
QDoubleSpinBox[diagnosisField="true"]:focus,
QSpinBox[diagnosisField="true"]:focus,
QTimeEdit[diagnosisField="true"]:focus {
border: 1px solid #0891B2;
background-color: #F0FDFA;
}
@@ -421,9 +424,11 @@ QLineEdit[diagnosisField="true"]:read-only,
QPlainTextEdit[diagnosisField="true"]:read-only,
QLineEdit[diagnosisField="true"]:disabled,
QComboBox[diagnosisField="true"]:disabled,
QPlainTextEdit[diagnosisField="true"]:disabled,
QDateEdit[diagnosisField="true"]:disabled,
QDoubleSpinBox[diagnosisField="true"]:disabled {
QPlainTextEdit[diagnosisField="true"]:disabled,
QDateEdit[diagnosisField="true"]:disabled,
QDoubleSpinBox[diagnosisField="true"]:disabled,
QSpinBox[diagnosisField="true"]:disabled,
QTimeEdit[diagnosisField="true"]:disabled {
color: #5B7A76;
background-color: #F0F2EF;
border-color: #D9DEDA;
@@ -460,11 +465,40 @@ QLabel#DiagnosisReadonlyTitle {
font-size: 16px;
font-weight: 700;
}
QLabel#DiagnosisReadonlyPatientName {
color: #0F172A;
font-size: 18px;
font-weight: 700;
}
QLabel#DiagnosisReadonlyPatientName {
color: #0F172A;
font-size: 18px;
font-weight: 700;
}
QLabel#DiagnosisReadonlyHeroMeta,
QLabel#DiagnosisPrivacyText,
QLabel#DiagnosisOrderOffsetPreview,
QLabel#DiagnosisOrdersSummary {
color: #64748B;
font-size: 12px;
}
QLabel#DiagnosisOrderOffsetLabel {
color: #1F2937;
font-size: 13px;
font-weight: 600;
}
QPushButton#DiagnosisOrderOffsetHelp {
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
padding: 0;
color: #0E7490;
background-color: #CFFAFE;
border: 1px solid #A5F3FC;
border-radius: 11px;
font-weight: 700;
}
QPushButton#DiagnosisOrderOffsetHelp:hover,
QPushButton#DiagnosisOrderOffsetHelp:focus {
background-color: #A5F3FC;
border-color: #22D3EE;
}
QLabel#DiagnosisReadonlyStatus[severity="neutral"] {
color: #9AA39E;
background-color: rgba(255, 255, 255, 190);
@@ -775,11 +809,35 @@ QLabel#DiagnosisUnsupportedState {
padding: 22px;
font-size: 13px;
}
QDialog#DiagnosisDailyEditor,
QDialog#DiagnosisOrderDetailDialog,
QDialog#DiagnosisRecordingPlayer {
background-color: #FFFFFF;
}
QDialog#DiagnosisDailyEditor,
QDialog#DiagnosisOrderDetailDialog,
QDialog#DiagnosisRecordingPlayer {
background-color: #FFFFFF;
}
QDialog#DiagnosisDailyEditor {
background-color: #F7F9FE;
}
QFrame#DiagnosisEditorHeader,
QFrame#DiagnosisEditorFooter {
background-color: #FFFFFF;
border: 0;
}
QFrame#DiagnosisEditorHeader {
border-bottom: 1px solid #E6EAF5;
}
QFrame#DiagnosisEditorFooter {
border-top: 1px solid #E6EAF5;
}
QWidget#DiagnosisEditorContent {
background-color: #FFFFFF;
border: 1px solid #E6EAF5;
border-radius: 12px;
}
QWidget#DiagnosisEditorContent QLabel {
color: #3F4E75;
font-size: 12px;
font-weight: 600;
}
QLabel#DiagnosisDialogHeading {
color: #0F172A;
font-size: 18px;
@@ -797,12 +855,17 @@ QLabel#DiagnosisEditorError {
border-radius: 7px;
padding: 8px 10px;
}
QScrollArea#DiagnosisEditorScroll,
QScrollArea#DiagnosisOrderDetailScroll {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 10px;
}
QScrollArea#DiagnosisEditorScroll,
QScrollArea#DiagnosisOrderDetailScroll {
background-color: #F8FAFC;
border: 1px solid #E2E8F0;
border-radius: 10px;
}
QScrollArea#DiagnosisEditorScroll,
QScrollArea#DiagnosisEditorScroll > QWidget > QWidget {
background-color: #F7F9FE;
border: 0;
}
QFrame#DiagnosisOrderOffsetBar,
QFrame#DiagnosisOrderDetailHero {
background-color: #F8FAFC;
@@ -875,89 +938,76 @@ QDialog#DiagnosisPrescriptionEditor QDoubleSpinBox:focus {
}
"""
# Keep the component-scoped rules authoritative while applying one restrained
# dark indigo visual system across every nested editor, table, state and card.
_DIAGNOSIS_DARK_REPLACEMENTS = (
("color: #FFFFFF", "color: #EEF2FF"),
("color:#FFFFFF", "color:#EEF2FF"),
("selection-color: #134E4A", "selection-color: #EEF2FF"),
("selection-background-color: #CFFAFE", "selection-background-color: #29334F"),
("selection-background-color: #A5F3FC", "selection-background-color: #6675F5"),
("background-color: #0F172A", "background-color: #080B14"),
("color: #CBD5E1", "color: #9AA7C0"),
("rgba(15, 23, 42, 112)", "rgba(8, 11, 20, 196)"),
("rgba(255, 255, 255, 218)", "rgba(8, 11, 20, 224)"),
("rgba(255, 255, 255, 210)", "rgba(8, 11, 20, 216)"),
("rgba(255, 255, 255, 190)", "rgba(21, 29, 49, 230)"),
("#FFFFFF", "#101626"),
("#F5F8F7", "#080B14"),
("#F6F6F6", "#080B14"),
("#F8FAFC", "#151D31"),
("#F1F5F9", "#151D31"),
("#F0F2EF", "#151D31"),
("#F0F5F3", "#151D31"),
("#F3F4F6", "#151D31"),
("#FAFAFA", "#151D31"),
("#ECFEFF", "#151D31"),
("#CFFAFE", "#151D31"),
("#FFFBEB", "#151D31"),
("#F0FDF4", "#151D31"),
("#FEF2F2", "#151D31"),
("#FEE2E2", "#1B2440"),
("#ECFDF5", "#151D31"),
("#FFF7F7", "#151D31"),
("#FFF7ED", "#151D31"),
("#A5F3FC", "#1B2440"),
("#67E8F9", "#1B2440"),
("#E2E8F0", "#29334F"),
("#DCE3EC", "#29334F"),
("#CBD5E1", "#29334F"),
("#FCD34D", "#29334F"),
("#BBF7D0", "#29334F"),
("#FECACA", "#29334F"),
("#D5E5E2", "#29334F"),
("#E2EBE8", "#29334F"),
("#D9DEDA", "#29334F"),
("#CBD3CE", "#29334F"),
("#E5E7EB", "#29334F"),
("#FED7AA", "#29334F"),
("#D9ECFF", "#29334F"),
("#E6EBF2", "#29334F"),
("#0F172A", "#EEF2FF"),
("#134E4A", "#EEF2FF"),
("#1F2937", "#EEF2FF"),
("#333333", "#EEF2FF"),
("#64748B", "#9AA7C0"),
("#5B7A76", "#9AA7C0"),
("#475569", "#9AA7C0"),
("#2A6B64", "#9AA7C0"),
("#66736D", "#9AA7C0"),
("#6B7280", "#9AA7C0"),
("#999999", "#9AA7C0"),
("#9AA39E", "#9AA7C0"),
("#94A8A4", "#9AA7C0"),
("#C0C4CC", "#9AA7C0"),
("#0891B2", "#6675F5"),
("#0E7490", "#6675F5"),
("#22D3EE", "#78A7FF"),
("#16A34A", "#49C6A5"),
("#15803D", "#49C6A5"),
("#B45309", "#E4B967"),
("#EA580C", "#E4B967"),
("#F97316", "#E4B967"),
("#DC2626", "#F07886"),
("#B91C1C", "#F07886"),
("#F56C6C", "#F07886"),
("#FCA5A5", "#F07886"),
("#F0FDFA", "#1B2440"),
("#F8F8F8", "#151D31"),
# Run last so foreground white remains readable while former white
# surfaces stay in the dark elevation system.
("background-color: #EEF2FF", "background-color: #101626"),
("background: #EEF2FF", "background: #101626"),
)
# Light is the default application theme. Keep the replacement table available
# for a future explicit dark-mode switch, but do not mutate the light source QSS.
# Normalize the legacy component palette in one place. Keeping the selectors
# untouched protects the mature drawer behavior while aligning every nested
# editor, table and state with the shell's current blue-white theme.
_DIAGNOSIS_BLUE_REPLACEMENTS = (
("#0891B2", "#5265F6"),
("#0E7490", "#4D57D8"),
("#22D3EE", "#6871F6"),
("#67E8F9", "#C9CEFF"),
("#A5F3FC", "#D8DCFF"),
("#CFFAFE", "#F0F2FF"),
("#ECFEFF", "#F5F8FF"),
("#D9ECFF", "#DDE7FF"),
("#134E4A", "#15224A"),
("#2A6B64", "#3F4E75"),
("#5B7A76", "#7481A3"),
("#66736D", "#7481A3"),
("#94A8A4", "#A4ADC3"),
("#67B8C9", "#6871F6"),
("#D5E5E2", "#E2E7F4"),
("#E2EBE8", "#E2E7F4"),
("#D9DEDA", "#E2E7F4"),
("#CBD3CE", "#DCE3F2"),
("#F5F8F7", "#FCFDFE"),
("#F0F2EF", "#F2F6FE"),
("#F0F5F3", "#F7F9FE"),
("#F0FDFA", "#F7F9FE"),
("#0F172A", "#15224A"),
("#1F2937", "#15224A"),
("#333333", "#15224A"),
("#475569", "#3F4E75"),
("#64748B", "#7481A3"),
("#6B7280", "#7481A3"),
("#999999", "#A4ADC3"),
("#9AA39E", "#A4ADC3"),
("#C0C4CC", "#A4ADC3"),
("#F1F5F9", "#F7F9FE"),
("#E2E8F0", "#E2E7F4"),
("#DCE3EC", "#E2E7F4"),
("#CBD5E1", "#DCE3F2"),
("#E5E7EB", "#E2E7F4"),
("#E6EBF2", "#E2E7F4"),
("#F8FAFC", "#FCFDFE"),
("#F6F6F6", "#FCFDFE"),
("#FAFAFA", "#FCFDFE"),
("#F8F8F8", "#F7F9FE"),
("#16A34A", "#17A77D"),
("#15803D", "#17A77D"),
("#F0FDF4", "#EAF9F3"),
("#ECFDF5", "#EAF9F3"),
("#BBF7D0", "#BFE9DC"),
("#A7F3D0", "#BFE9DC"),
("#B45309", "#D38625"),
("#EA580C", "#D38625"),
("#F97316", "#D38625"),
("#FFFBEB", "#FFF5E6"),
("#FFF7ED", "#FFF5E6"),
("#FCD34D", "#F3D6AC"),
("#FDE68A", "#F3D6AC"),
("#FED7AA", "#F3D6AC"),
("#DC2626", "#F15B67"),
("#B91C1C", "#D94856"),
("#F56C6C", "#F15B67"),
("#FEF2F2", "#FFF1F3"),
("#FFF7F7", "#FFF1F3"),
("#FEE2E2", "#FFE4E8"),
("#FECACA", "#F7C8CD"),
("#FCA5A5", "#F19BA4"),
)
for _source_color, _theme_color in _DIAGNOSIS_BLUE_REPLACEMENTS:
DIAGNOSIS_QSS = DIAGNOSIS_QSS.replace(_source_color, _theme_color)
def _text(value: Any, default: str = "") -> str:
@@ -1037,7 +1087,7 @@ class DiagnosisSwitch(QAbstractButton):
track = QRectF(1, 3, self.width() - 2, self.height() - 6)
checked = self.isChecked()
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor("#4F63D9" if checked else "#D8DEEA"))
painter.setBrush(QColor("#5761F4" if checked else "#D8DEEE"))
painter.drawRoundedRect(track, track.height() / 2, track.height() / 2)
diameter = track.height() - 4
x = track.right() - diameter - 2 if checked else track.left() + 2
@@ -1081,9 +1131,9 @@ class SaveStateButton(QPushButton):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
enabled = self.isEnabled()
color = {
"success": "#16876C",
"error": "#C43E55",
}.get(self._state, "#4F63D9" if enabled else "#98A2B3")
"success": "#17A77D",
"error": "#F15B67",
}.get(self._state, "#5761F4" if enabled else "#A4ADC3")
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor(color))
painter.drawRoundedRect(QRectF(self.rect()).adjusted(1, 1, -1, -1), 10, 10)
@@ -1602,18 +1652,18 @@ class MessageStrip(QFrame):
self.glyph = QLabel("i")
self.glyph.setFixedWidth(18)
self.glyph.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.glyph.setStyleSheet("font-weight:700; color:#2F6EDB;")
self.glyph.setStyleSheet("font-weight:700; color:#4D69ED;")
self.label = QLabel()
self.label.setWordWrap(True)
self.label.setStyleSheet("color:#667085; font-size:12px;")
self.label.setStyleSheet("color:#3F4E75; font-size:12px;")
layout.addWidget(self.glyph)
layout.addWidget(self.label, 1)
self.action_button = QPushButton()
self.action_button.setObjectName("DiagnosisMessageAction")
self.action_button.setStyleSheet(
"QPushButton{min-height:28px;padding:0 10px;color:#3446AF;background:#E9EDFF;"
"border:1px solid #C8D1FF;border-radius:7px;font-weight:600;}"
"QPushButton:hover,QPushButton:focus{background:#DCE3FF;border-color:#4F63D9;}"
"QPushButton{min-height:28px;padding:0 10px;color:#4451E2;background:#F0F2FF;"
"border:1px solid #D3D8FF;border-radius:7px;font-weight:600;}"
"QPushButton:hover,QPushButton:focus{background:#E4E7FF;border-color:#5761F4;}"
)
self.action_button.clicked.connect(self.action_requested)
self.action_button.hide()
@@ -1772,15 +1822,15 @@ class RecordTable(QTableWidget):
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
)
if column_index in danger_columns:
item.setForeground(QColor("#F07886"))
item.setForeground(QColor("#D94856"))
kind = semantic.get(column_index)
if kind:
palette = {
"success": ("#49C6A5", "#1B2440"),
"warning": ("#E4B967", "#1B2440"),
"danger": ("#F07886", "#1B2440"),
"info": ("#78A7FF", "#1B2440"),
"neutral": ("#9AA7C0", "#1B2440"),
"success": ("#137A61", "#EAF9F3"),
"warning": ("#A86616", "#FFF5E6"),
"danger": ("#D94856", "#FFF1F3"),
"info": ("#4059D8", "#F0F4FF"),
"neutral": ("#64739A", "#F5F7FC"),
}
foreground, background = palette.get(kind, palette["neutral"])
item.setForeground(QColor(foreground))
@@ -1820,20 +1870,20 @@ class BloodTrendChart(QWidget):
plot = self.rect().adjusted(48, 24, -22, -38)
values = [value for value in (*self._fasting, *self._postprandial) if value is not None]
if not values or plot.width() <= 0 or plot.height() <= 0:
painter.setPen(QColor("#9AA7C0"))
painter.setPen(QColor("#7886AA"))
painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "当前时间范围暂无血糖数据")
painter.end()
return
upper = max(12.0, max(values) * 1.15)
painter.setPen(QPen(QColor("#29334F"), 1))
painter.setPen(QPen(QColor("#E6EAF5"), 1))
for step in range(5):
y = plot.bottom() - round(plot.height() * step / 4)
painter.drawLine(plot.left(), y, plot.right(), y)
painter.setPen(QColor("#9AA7C0"))
painter.setPen(QColor("#7886AA"))
painter.drawText(
4, y - 8, 40, 16, Qt.AlignmentFlag.AlignRight, f"{upper * step / 4:.0f}"
)
painter.setPen(QPen(QColor("#29334F"), 1))
painter.setPen(QPen(QColor("#E6EAF5"), 1))
count = max(1, len(self._dates) - 1)
def point(index: int, value: float) -> QPointF:
@@ -1843,8 +1893,8 @@ class BloodTrendChart(QWidget):
)
for series, color in (
(self._fasting, QColor("#6675F5")),
(self._postprandial, QColor("#E4B967")),
(self._fasting, QColor("#5761F4")),
(self._postprandial, QColor("#D38625")),
):
previous: QPointF | None = None
painter.setPen(QPen(color, 2.5))
@@ -1858,12 +1908,12 @@ class BloodTrendChart(QWidget):
painter.drawLine(previous, current)
painter.drawEllipse(current, 3.2, 3.2)
previous = current
painter.setPen(QColor("#6675F5"))
painter.setPen(QColor("#5761F4"))
painter.drawText(plot.left(), 4, 94, 18, Qt.AlignmentFlag.AlignLeft, "● 空腹血糖")
painter.setPen(QColor("#E4B967"))
painter.setPen(QColor("#D38625"))
painter.drawText(plot.left() + 100, 4, 110, 18, Qt.AlignmentFlag.AlignLeft, "● 餐后血糖")
if self._dates:
painter.setPen(QColor("#9AA7C0"))
painter.setPen(QColor("#7886AA"))
painter.drawText(
plot.left(),
plot.bottom() + 8,
@@ -2061,7 +2111,7 @@ class DailyRecordPanel(QFrame):
self.todo_table.setMinimumHeight(180)
todo_layout.addWidget(self.todo_table)
self.todo_summary = QLabel("共 0 条")
self.todo_summary.setStyleSheet("color:#9AA7C0; font-size:12px;")
self.todo_summary.setStyleSheet("color:#7886AA; font-size:12px;")
todo_layout.addWidget(self.todo_summary, 0, Qt.AlignmentFlag.AlignRight)
root.addWidget(todo_card)
self.clear()
@@ -2215,8 +2265,8 @@ class DailyRecordPanel(QFrame):
has_records = False
for row_index, (metric, label) in enumerate(self.METRICS):
label_item = QTableWidgetItem(label)
label_item.setForeground(QColor("#9AA7C0"))
label_item.setBackground(QColor("#151D31"))
label_item.setForeground(QColor("#64739A"))
label_item.setBackground(QColor("#F5F7FC"))
self.matrix.setItem(row_index, 0, label_item)
for date_index, date in enumerate(dates, 1):
b = blood.get(date)
@@ -2269,11 +2319,11 @@ class DailyRecordPanel(QFrame):
item = QTableWidgetItem(value)
item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
if high:
item.setForeground(QColor("#F07886"))
item.setBackground(QColor("#1B2440"))
item.setForeground(QColor("#D94856"))
item.setBackground(QColor("#FFF1F3"))
elif value.endswith("· 自录"):
item.setForeground(QColor("#78A7FF"))
item.setBackground(QColor("#1B2440"))
item.setForeground(QColor("#4059D8"))
item.setBackground(QColor("#F0F4FF"))
edit_kind = ""
edit_source: Any = None
if metric in {"fasting", "postprandial", "other", "bp", "western", "insulin"}:
@@ -2764,7 +2814,7 @@ class ChatPanel(QWidget):
toolbar_layout.setContentsMargins(0, 0, 0, 0)
toolbar_layout.setSpacing(8)
archive = QLabel("仅展示服务端已归档消息(only_archived=1")
archive.setStyleSheet("color:#9AA7C0;font-size:12px;")
archive.setStyleSheet("color:#7886AA;font-size:12px;")
toolbar_layout.addWidget(archive)
toolbar_layout.addStretch(1)
self.sync_button = QPushButton("同步最新(后台异步)")
@@ -2992,7 +3042,7 @@ class CaseGrid(QFrame):
title_row.addWidget(self.title_label)
self.subtitle = QLabel("病例 · 诊断日期 —")
self.subtitle.setStyleSheet(
'color:#9AA7C0;font-size:12px;font-family:"IBM Plex Mono",Consolas,monospace;'
'color:#7886AA;font-size:12px;font-family:"IBM Plex Mono",Consolas,monospace;'
)
title_row.addWidget(self.subtitle)
title_row.addStretch(1)
@@ -3002,7 +3052,7 @@ class CaseGrid(QFrame):
if group_index:
divider = QFrame()
divider.setFrameShape(QFrame.Shape.HLine)
divider.setStyleSheet("border:0; border-top:1px dashed #29334F;")
divider.setStyleSheet("border:0; border-top:1px dashed #D8DEEE;")
self.root.addWidget(divider)
group_title = QLabel(f"{group_name}")
group_title.setProperty("diagnosisCaseGroup", True)
@@ -3155,11 +3205,11 @@ def readonly_card(title: str, object_name: str, body: QWidget) -> QFrame:
def set_tag_item(item: QTableWidgetItem, kind: str) -> None:
palette = {
"success": ("#49C6A5", "#1B2440"),
"warning": ("#E4B967", "#1B2440"),
"danger": ("#F07886", "#1B2440"),
"info": ("#78A7FF", "#1B2440"),
"neutral": ("#9AA7C0", "#1B2440"),
"success": ("#137A61", "#EAF9F3"),
"warning": ("#A86616", "#FFF5E6"),
"danger": ("#D94856", "#FFF1F3"),
"info": ("#4059D8", "#F0F4FF"),
"neutral": ("#64739A", "#F5F7FC"),
}
foreground, background = palette.get(kind, palette["neutral"])
item.setForeground(QColor(foreground))
@@ -13,6 +13,7 @@ from PySide6.QtWidgets import (
QDialog,
QDoubleSpinBox,
QFormLayout,
QFrame,
QHBoxLayout,
QLabel,
QLayout,
@@ -161,27 +162,45 @@ class DailyRecordEditorDialog(QDialog):
self.resize(650, 620 if kind == "blood" else 590)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setSpacing(14)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
header = QFrame()
header.setObjectName("DiagnosisEditorHeader")
header_layout = QVBoxLayout(header)
header_layout.setContentsMargins(22, 18, 22, 16)
header_layout.setSpacing(5)
heading = QLabel(self.windowTitle())
heading.setObjectName("DiagnosisDialogHeading")
root.addWidget(heading)
header_layout.addWidget(heading)
guidance = QLabel("保存后将重新加载当前日期范围;带 * 的字段为必填项。")
guidance.setObjectName("DiagnosisDialogGuidance")
guidance.setWordWrap(True)
root.addWidget(guidance)
header_layout.addWidget(guidance)
root.addWidget(header)
scroll = QScrollArea()
scroll.setObjectName("DiagnosisEditorScroll")
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
content = QWidget()
content.setObjectName("DiagnosisEditorContent")
self.form = QFormLayout(content)
self.form.setContentsMargins(8, 8, 12, 8)
self.form.setHorizontalSpacing(18)
self.form.setVerticalSpacing(11)
self.form.setContentsMargins(22, 20, 22, 22)
self.form.setHorizontalSpacing(20)
self.form.setVerticalSpacing(12)
self.form.setLabelAlignment(
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
)
self.form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
scroll.setWidget(content)
root.addWidget(scroll, 1)
footer = QFrame()
footer.setObjectName("DiagnosisEditorFooter")
footer_layout = QVBoxLayout(footer)
footer_layout.setContentsMargins(22, 12, 22, 14)
footer_layout.setSpacing(10)
self.date_edit = QDateEdit()
self.date_edit.setCalendarPopup(True)
self.date_edit.setDisplayFormat("yyyy-MM-dd")
@@ -202,8 +221,10 @@ class DailyRecordEditorDialog(QDialog):
self.error_label.setObjectName("DiagnosisEditorError")
self.error_label.setWordWrap(True)
self.error_label.hide()
root.addWidget(self.error_label)
footer_layout.addWidget(self.error_label)
actions = QHBoxLayout()
actions.setContentsMargins(0, 0, 0, 0)
actions.setSpacing(8)
actions.addStretch(1)
cancel = QPushButton("取消")
cancel.setProperty("variant", "ghost")
@@ -215,7 +236,8 @@ class DailyRecordEditorDialog(QDialog):
save.setDefault(True)
save.clicked.connect(self.accept)
actions.addWidget(save)
root.addLayout(actions)
footer_layout.addLayout(actions)
root.addWidget(footer)
@staticmethod
def _field(widget: QWidget) -> QWidget:
@@ -63,10 +63,10 @@ from PySide6.QtWidgets import (
from .widgets import display_text, first_value, gender_text, get_value
PRIMARY = QColor("#4F63D9")
TEXT = QColor("#172033")
SECONDARY = QColor("#667085")
PLACEHOLDER = QColor("#98A2B3")
PRIMARY = QColor("#5265F6")
TEXT = QColor("#15224A")
SECONDARY = QColor("#7481A3")
PLACEHOLDER = QColor("#A4ADC3")
_INVALID_INDEX = QModelIndex()
_TABLE_COLUMN_WIDTHS = (48, 70, 60, 100, 175, 88, 120, 100, 72, 110, 120, 340)
@@ -79,7 +79,7 @@ def _menu_action_icon(kind: str, *, danger: bool = False) -> QIcon:
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
color = QColor("#C43E55" if danger else "#667085")
color = QColor("#C43E55" if danger else "#667085")
pen = QPen(color, 1.6)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
@@ -146,16 +146,14 @@ class _DiagnosisActionMenu(QMenu):
return
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
painter.fillRect(rect, QColor("#FFFFFF"))
painter.fillRect(rect, QColor("#FFFFFF"))
item_rect = rect
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(
QColor("#EEF2F8") if self.activeAction() is action else QColor("#FFFFFF")
)
painter.setBrush(QColor("#EEF2F8") if self.activeAction() is action else QColor("#FFFFFF"))
painter.drawRoundedRect(item_rect, 3, 3)
icon_rect = QRect(item_rect.left() + 8, item_rect.center().y() - 9, 18, 18)
action.icon().paint(painter, icon_rect)
painter.setPen(QColor("#C43E55" if action.isEnabled() else "#98A2B3"))
painter.setPen(QColor("#C43E55" if action.isEnabled() else "#98A2B3"))
painter.drawText(
item_rect.adjusted(34, 0, -10, 0),
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft,
@@ -684,7 +682,7 @@ class DiagnosisHeader(QHeaderView):
center_y = rect.center().y()
painter.save()
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(PRIMARY if direction == "asc" else QColor("#667085"))
painter.setBrush(PRIMARY if direction == "asc" else QColor("#667085"))
painter.drawPolygon(
QPolygon(
(
@@ -694,7 +692,7 @@ class DiagnosisHeader(QHeaderView):
)
)
)
painter.setBrush(PRIMARY if direction == "desc" else QColor("#667085"))
painter.setBrush(PRIMARY if direction == "desc" else QColor("#667085"))
painter.drawPolygon(
QPolygon(
(
@@ -742,15 +740,15 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
return
record = model.record(index.row())
painter.save()
self._paint_row_background(
painter,
option.rect,
record,
index.row(),
index.column(),
model,
selected=bool(option.state & QStyle.StateFlag.State_Selected),
)
self._paint_row_background(
painter,
option.rect,
record,
index.row(),
index.column(),
model,
selected=bool(option.state & QStyle.StateFlag.State_Selected),
)
if index.column() == 0:
self._paint_checkbox(painter, option, index)
elif index.column() in {1, 2}:
@@ -762,7 +760,7 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
painter,
option.rect,
"已确认" if _confirmed(record) else "未确认",
QColor("#16876C") if _confirmed(record) else QColor("#9A6813"),
QColor("#16876C") if _confirmed(record) else QColor("#9A6813"),
bold=not _confirmed(record),
)
elif index.column() == 6:
@@ -821,22 +819,22 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
return super().editorEvent(event, model, option, index)
@staticmethod
def _paint_row_background(
def _paint_row_background(
painter: QPainter,
rect: QRect,
record: Any,
row: int,
column: int,
model: DiagnosisTableModel,
*,
selected: bool = False,
) -> None:
hovered = model.hover_row == row
base = (
QColor("#DCE3FF")
if selected
else QColor("#EEF2F8" if hovered else "#F7F8FC" if row % 2 else "#FFFFFF")
)
column: int,
model: DiagnosisTableModel,
*,
selected: bool = False,
) -> None:
hovered = model.hover_row == row
base = (
QColor("#DCE3FF")
if selected
else QColor("#EEF2F8" if hovered else "#F7F8FC" if row % 2 else "#FFFFFF")
)
special = not _confirmed(record) or not _has_appointment(record)
if special:
global_left = sum(_TABLE_COLUMN_WIDTHS[:column])
@@ -848,19 +846,19 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
rect.top(),
)
if not _confirmed(record):
gradient.setColorAt(0, QColor(228, 185, 103, 48 if hovered or selected else 30))
gradient.setColorAt(0, QColor(228, 185, 103, 48 if hovered or selected else 30))
gradient.setColorAt(1, base)
stripe = QColor("#9A6813")
stripe = QColor("#9A6813")
else:
gradient.setColorAt(0, QColor(120, 167, 255, 42 if hovered or selected else 24))
gradient.setColorAt(0, QColor(120, 167, 255, 42 if hovered or selected else 24))
gradient.setColorAt(1, base)
stripe = QColor("#2F6EDB")
stripe = QColor("#2F6EDB")
painter.fillRect(rect, gradient)
if column == 0:
painter.fillRect(QRect(rect.left(), rect.top(), 3, rect.height()), stripe)
else:
painter.fillRect(rect, base)
painter.setPen(QPen(QColor("#D8DEEA"), 1))
painter.setPen(QPen(QColor("#D8DEEA"), 1))
painter.drawLine(QLine(rect.bottomLeft(), rect.bottomRight()))
@staticmethod
@@ -886,7 +884,7 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
"",
)
content = rect.adjusted(8, 4, -6, -4)
self._draw_text(painter, content, text, TEXT, 14, bold=True)
self._draw_text(painter, content, text, TEXT, 14, bold=True)
if (
column == 1
and get_value(record, "assign_read_at", "sentinel") is None
@@ -898,8 +896,8 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
painter,
QRect(left, content.center().y() - 10, 31, 20),
"NEW",
QColor("#C43E55"),
QColor("#FFFFFF"),
QColor("#C43E55"),
QColor("#FFFFFF"),
10,
)
@@ -911,17 +909,17 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
rows = _appointments(record)
statuses = {_as_int(first_value(row, "status", "appointment_status"), -1) for row in rows}
if 1 in statuses:
border = QColor(102, 117, 245, 112)
start = QColor(102, 117, 245, 58)
end = QColor(102, 117, 245, 24)
border = QColor(102, 117, 245, 112)
start = QColor(102, 117, 245, 58)
end = QColor(102, 117, 245, 24)
elif 4 in statuses:
border = QColor(228, 185, 103, 112)
start = QColor(228, 185, 103, 54)
end = QColor(228, 185, 103, 22)
border = QColor(228, 185, 103, 112)
start = QColor(228, 185, 103, 54)
end = QColor(228, 185, 103, 22)
else:
border = QColor(154, 167, 192, 92)
start = QColor(154, 167, 192, 42)
end = QColor(154, 167, 192, 18)
border = QColor(154, 167, 192, 92)
start = QColor(154, 167, 192, 42)
end = QColor(154, 167, 192, 18)
gradient = QLinearGradient(content.topLeft(), content.bottomRight())
gradient.setColorAt(0, start)
gradient.setColorAt(1, end)
@@ -932,10 +930,10 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
for position, appointment in enumerate(rows or [{}]):
status = _as_int(first_value(appointment, "status", "appointment_status"), -1)
label, fg, bg = {
1: ("已预约", QColor("#2F6EDB"), QColor("#EAF2FF")),
3: ("已完成", QColor("#16876C"), QColor("#E8F6F1")),
4: ("已过号", QColor("#9A6813"), QColor("#FFF4D8")),
}.get(status, ("未知", SECONDARY, QColor("#F7F8FC")))
1: ("已预约", QColor("#2F6EDB"), QColor("#EAF2FF")),
3: ("已完成", QColor("#16876C"), QColor("#E8F6F1")),
4: ("已过号", QColor("#9A6813"), QColor("#FFF4D8")),
}.get(status, ("未知", SECONDARY, QColor("#F7F8FC")))
self._draw_tag(painter, QRect(content.left() + 6, y, 46, 18), label, bg, fg, 11)
doctor = display_text(first_value(appointment, "doctor_name"), "-")
time_text = display_text(first_value(appointment, "time_text", "appointment_time"), "-")
@@ -951,12 +949,12 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
painter,
QRect(content.left() + 6, y + 19, max(20, content.width() - 12), 17),
time_text,
PRIMARY if status == 1 else (QColor("#9A6813") if status == 4 else SECONDARY),
PRIMARY if status == 1 else (QColor("#9A6813") if status == 4 else SECONDARY),
12,
)
y += 42
if position < len(rows) - 1:
painter.setPen(QPen(QColor("#D8DEEA"), 1, Qt.PenStyle.DashLine))
painter.setPen(QPen(QColor("#D8DEEA"), 1, Qt.PenStyle.DashLine))
painter.drawLine(content.left() + 6, y - 2, content.right() - 6, y - 2)
channel = first_value(
record,
@@ -999,7 +997,7 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
painter,
QRect(content.left(), content.top() + 38, 72, 18),
"处方已作废",
QColor("#F7F8FC"),
QColor("#F7F8FC"),
SECONDARY,
11,
outlined=True,
@@ -1012,11 +1010,11 @@ class DiagnosisItemDelegate(QStyledItemDelegate):
else:
days = _as_int(value)
color = (
QColor("#16876C")
QColor("#16876C")
if days <= 2
else QColor("#9A6813")
else QColor("#9A6813")
if days <= 6
else QColor("#C43E55")
else QColor("#C43E55")
)
text, bold = str(days), True
self._draw_text(
@@ -1216,9 +1214,9 @@ class _FixedColumnShadow(QWidget):
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter(self)
gradient = QLinearGradient(self.rect().left(), 0, self.rect().right(), 0)
gradient.setColorAt(0, QColor(8, 11, 20, 150))
gradient.setColorAt(0.45, QColor(8, 11, 20, 72))
gradient.setColorAt(1, QColor(8, 11, 20, 0))
gradient.setColorAt(0, QColor(8, 11, 20, 150))
gradient.setColorAt(0.45, QColor(8, 11, 20, 72))
gradient.setColorAt(1, QColor(8, 11, 20, 0))
painter.fillRect(self.rect(), gradient)
super().paintEvent(event)
@@ -1493,13 +1491,13 @@ class DiagnosisTableHost(QFrame):
more.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
menu = _DiagnosisActionMenu(more)
menu.setStyleSheet(
"QMenu{background:#FFFFFF;color:#172033;border:1px solid #D8DEEA;"
"QMenu{background:#FFFFFF;color:#172033;border:1px solid #D8DEEA;"
"border-radius:4px;padding:6px;font-size:13px;}"
"QMenu::item{min-width:118px;min-height:30px;padding:0 12px 0 10px;"
"border-radius:3px;}"
"QMenu::item:selected{color:#3446AF;background:#E9EDFF;}"
"QMenu::item:disabled{color:#98A2B3;}"
"QMenu::separator{height:1px;background:#D8DEEA;margin:5px 8px;}"
"QMenu::item:selected{color:#3446AF;background:#E9EDFF;}"
"QMenu::item:disabled{color:#98A2B3;}"
"QMenu::separator{height:1px;background:#D8DEEA;margin:5px 8px;}"
)
if self.action_policy.get("assign", False):
_add_menu_action(
@@ -1769,7 +1767,7 @@ class DiagnosisLoadingOverlay(QWidget):
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.fillRect(self.rect(), QColor(8, 11, 20, 196))
painter.fillRect(self.rect(), QColor(8, 11, 20, 196))
visible = self.visibleRegion().boundingRect()
center = (
visible.center()
@@ -1793,16 +1791,16 @@ class DiagnosisLoadingOverlay(QWidget):
DIAGNOSIS_INDEX_QSS = """
#DiagnosisIndex {
background: #080B14;
color: #EEF2FF;
#DiagnosisIndex {
background: #080B14;
color: #EEF2FF;
font-family: "PingFang SC", Arial, "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px;
}
#DiagnosisIndex QFrame#DiagnosisFilterCard,
#DiagnosisIndex QFrame#DiagnosisListCard {
background: #101626;
border: 0;
background: #101626;
border: 0;
border-radius: 4px;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"] {
@@ -1810,77 +1808,77 @@ DIAGNOSIS_INDEX_QSS = """
padding: 6px 12px;
border: 1px solid transparent;
border-radius: 6px;
background: #151D31;
color: #9AA7C0;
background: #151D31;
color: #9AA7C0;
font-size: 14px;
font-weight: 500;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:hover {
background: #1B2440;
background: #1B2440;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:focus {
border-color: #6675F5;
border-color: #6675F5;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="info"] {
color: #78A7FF;
background: #151D31;
color: #78A7FF;
background: #151D31;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="info"]:hover {
background: #1B2440;
background: #1B2440;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="success"] {
color: #49C6A5;
background: #151D31;
color: #49C6A5;
background: #151D31;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="success"]:hover {
background: #1B2440;
background: #1B2440;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="warning"] {
color: #E4B967;
background: #151D31;
color: #E4B967;
background: #151D31;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][semantic="warning"]:hover {
background: #1B2440;
background: #1B2440;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="primary"] {
color: #EEF2FF;
background: #6675F5;
color: #EEF2FF;
background: #6675F5;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="info"] {
color: #EEF2FF;
background: #78A7FF;
color: #EEF2FF;
background: #78A7FF;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="success"] {
color: #080B14;
background: #49C6A5;
color: #080B14;
background: #49C6A5;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="warning"] {
color: #080B14;
background: #E4B967;
color: #080B14;
background: #E4B967;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][small="true"] {
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
color: #9AA7C0;
color: #9AA7C0;
background: transparent;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][small="true"]:hover {
color: #EEF2FF;
background: #1B2440;
color: #EEF2FF;
background: #1B2440;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"][small="true"]:checked {
color: #EEF2FF;
background: #1B2440;
color: #EEF2FF;
background: #1B2440;
}
#DiagnosisIndex QLabel[filterGroup="true"] {
color: #9AA7C0;
color: #9AA7C0;
font-size: 13px;
}
#DiagnosisIndex QFrame#DiagnosisQuickFilters,
#DiagnosisIndex QFrame#DiagnosisAdvancedFilters {
border: 0;
border-top: 1px solid #29334F;
border-top: 1px solid #29334F;
background: transparent;
}
#DiagnosisIndex QFrame#DiagnosisFilterDivider {
@@ -1889,15 +1887,15 @@ DIAGNOSIS_INDEX_QSS = """
min-height: 14px;
max-height: 14px;
margin: 0 4px;
background: #29334F;
background: #29334F;
border: 0;
}
#DiagnosisIndex QFrame#DiagnosisDateRange {
min-height: 30px;
max-height: 30px;
border: 1px solid #29334F;
border: 1px solid #29334F;
border-radius: 4px;
background: #151D31;
background: #151D31;
}
#DiagnosisIndex QFrame#DiagnosisDateRange QDateEdit[diagnosisRangePart="true"] {
min-height: 28px;
@@ -1906,98 +1904,98 @@ DIAGNOSIS_INDEX_QSS = """
padding: 0 5px;
}
#DiagnosisIndex QLabel[diagnosisRangeSeparator="true"] {
color: #9AA7C0;
color: #9AA7C0;
}
#DiagnosisIndex QFrame#DiagnosisAdvancedFilters {
border-top: 1px dashed #29334F;
border-top: 1px dashed #29334F;
}
#DiagnosisIndex QToolButton#DiagnosisMoreFilter {
border: 0;
background: transparent;
color: #78A7FF;
color: #78A7FF;
padding: 4px 6px;
font-size: 13px;
}
#DiagnosisIndex QLineEdit,
#DiagnosisIndex QComboBox,
#DiagnosisIndex QDateEdit,
#DiagnosisIndex QSpinBox {
#DiagnosisIndex QLineEdit,
#DiagnosisIndex QComboBox,
#DiagnosisIndex QDateEdit,
#DiagnosisIndex QSpinBox {
min-height: 30px;
border: 1px solid #29334F;
border: 1px solid #29334F;
border-radius: 4px;
padding: 0 8px;
background: #151D31;
color: #EEF2FF;
selection-background-color: #6675F5;
background: #151D31;
color: #EEF2FF;
selection-background-color: #6675F5;
}
#DiagnosisIndex QLineEdit:focus,
#DiagnosisIndex QComboBox:focus,
#DiagnosisIndex QDateEdit:focus,
#DiagnosisIndex QSpinBox:focus {
border-color: #6675F5;
border-color: #6675F5;
}
#DiagnosisIndex QComboBox[smallControl="true"],
#DiagnosisIndex QPushButton[smallControl="true"] {
min-height: 22px;
max-height: 22px;
}
#DiagnosisIndex QPushButton {
#DiagnosisIndex QPushButton {
min-height: 30px;
padding: 0 12px;
border-radius: 4px;
border: 1px solid #29334F;
background: #151D31;
color: #EEF2FF;
}
#DiagnosisIndex QPushButton:hover,
#DiagnosisIndex QPushButton:focus {
border-color: #6675F5;
background: #1B2440;
}
#DiagnosisIndex QPushButton:disabled {
color: #9AA7C0;
background: #101626;
}
#DiagnosisIndex QPushButton[variant="primary"] {
border-color: #6675F5;
background: #6675F5;
color: #EEF2FF;
}
#DiagnosisIndex QPushButton[variant="success"] {
border-color: #49C6A5;
background: #49C6A5;
color: #080B14;
}
#DiagnosisIndex QPushButton[variant="warning"] {
border-color: #E4B967;
color: #E4B967;
}
#DiagnosisIndex QPushButton[variant="danger"] {
border-color: #F07886;
color: #F07886;
}
border: 1px solid #29334F;
background: #151D31;
color: #EEF2FF;
}
#DiagnosisIndex QPushButton:hover,
#DiagnosisIndex QPushButton:focus {
border-color: #6675F5;
background: #1B2440;
}
#DiagnosisIndex QPushButton:disabled {
color: #9AA7C0;
background: #101626;
}
#DiagnosisIndex QPushButton[variant="primary"] {
border-color: #6675F5;
background: #6675F5;
color: #EEF2FF;
}
#DiagnosisIndex QPushButton[variant="success"] {
border-color: #49C6A5;
background: #49C6A5;
color: #080B14;
}
#DiagnosisIndex QPushButton[variant="warning"] {
border-color: #E4B967;
color: #E4B967;
}
#DiagnosisIndex QPushButton[variant="danger"] {
border-color: #F07886;
color: #F07886;
}
#DiagnosisIndex QFrame#DiagnosisListToolbar {
border: 0;
border-bottom: 1px solid #29334F;
background: #101626;
}
#DiagnosisIndex QTableView {
border: 0;
background: #101626;
border-bottom: 1px solid #29334F;
background: #101626;
}
#DiagnosisIndex QTableView {
border: 0;
background: #101626;
selection-background-color: transparent;
outline: 0;
}
#DiagnosisIndex QTableView:focus {
border: 1px solid #6675F5;
border: 1px solid #6675F5;
}
#DiagnosisIndex QTableView#DiagnosisFixedTable {
border-left: 1px solid #29334F;
border-left: 1px solid #29334F;
}
#DiagnosisIndex QHeaderView::section {
background: #151D31;
color: #9AA7C0;
background: #151D31;
color: #9AA7C0;
border: 0;
border-bottom: 1px solid #29334F;
border-bottom: 1px solid #29334F;
padding: 0 8px;
font-size: 13px;
font-weight: 600;
@@ -2013,43 +2011,43 @@ DIAGNOSIS_INDEX_QSS = """
}
#DiagnosisIndex QToolButton[rowLink]:hover,
#DiagnosisIndex QToolButton[rowLink]:focus {
background: #1B2440;
background: #1B2440;
border-radius: 3px;
}
#DiagnosisIndex QToolButton[rowLink]:disabled {
color: #9AA7C0;
color: #9AA7C0;
background: transparent;
}
#DiagnosisIndex QToolButton[rowLink="primary"] { color: #78A7FF; }
#DiagnosisIndex QToolButton[rowLink="success"] { color: #49C6A5; }
#DiagnosisIndex QToolButton[rowLink="warning"] { color: #E4B967; }
#DiagnosisIndex QToolButton[rowLink="muted"] { color: #9AA7C0; }
#DiagnosisIndex QToolButton[rowLink="primary"] { color: #78A7FF; }
#DiagnosisIndex QToolButton[rowLink="success"] { color: #49C6A5; }
#DiagnosisIndex QToolButton[rowLink="warning"] { color: #E4B967; }
#DiagnosisIndex QToolButton[rowLink="muted"] { color: #9AA7C0; }
#DiagnosisIndex QToolButton[appointmentCancel="true"] {
border: 0;
border-radius: 3px;
padding: 0 3px;
background: #1B2440;
color: #F07886;
background: #1B2440;
color: #F07886;
font-size: 11px;
}
#DiagnosisIndex QToolButton[appointmentCancel="true"]:hover,
#DiagnosisIndex QToolButton[appointmentCancel="true"]:focus {
background: #29334F;
color: #F07886;
background: #29334F;
color: #F07886;
}
#DiagnosisIndex QLabel[fixedMuted="true"],
#DiagnosisIndex QLabel[pagerMuted="true"] {
color: #9AA7C0;
color: #9AA7C0;
font-size: 13px;
}
#DiagnosisIndex QLabel#DiagnosisTableEmpty {
color: #9AA7C0;
background: rgba(16, 22, 38, 0.96);
color: #9AA7C0;
background: rgba(16, 22, 38, 0.96);
font-size: 14px;
}
#DiagnosisIndex QLabel#DiagnosisTableEmpty[stateKind="error"] {
color: #F07886;
background: rgba(16, 22, 38, 0.98);
color: #F07886;
background: rgba(16, 22, 38, 0.98);
}
#DiagnosisIndex QToolButton[pagerButton="true"] {
min-width: 32px;
@@ -2058,15 +2056,15 @@ DIAGNOSIS_INDEX_QSS = """
border: 0;
border-radius: 2px;
background: transparent;
color: #EEF2FF;
color: #EEF2FF;
font-size: 14px;
}
#DiagnosisIndex QToolButton[pagerButton="true"]:hover,
#DiagnosisIndex QToolButton[pagerButton="true"][active="true"] {
color: #78A7FF;
color: #78A7FF;
font-weight: 600;
}
#DiagnosisIndex QToolButton[pagerButton="true"]:disabled { color: #9AA7C0; }
#DiagnosisIndex QToolButton[pagerButton="true"]:disabled { color: #9AA7C0; }
#DiagnosisIndex QComboBox#DiagnosisPageSize,
#DiagnosisIndex QSpinBox#DiagnosisPageJumper {
min-height: 30px;
@@ -2074,47 +2072,64 @@ DIAGNOSIS_INDEX_QSS = """
}
#DiagnosisIndex QScrollBar:horizontal {
height: 12px;
background: #101626;
background: #101626;
}
#DiagnosisIndex QScrollBar::handle:horizontal {
min-width: 32px;
border-radius: 5px;
background: #29334F;
}
"""
_DIAGNOSIS_INDEX_LIGHT_REPLACEMENTS = (
("#080B14", "#F5F7FB"),
("#101626", "#FFFFFF"),
("#151D31", "#F7F8FC"),
("#1B2440", "#EEF2F8"),
("#29334F", "#D8DEEA"),
("#EEF2FF", "#172033"),
("#9AA7C0", "#667085"),
("#6675F5", "#4F63D9"),
("#78A7FF", "#2F6EDB"),
("#49C6A5", "#16876C"),
("#E4B967", "#9A6813"),
("#F07886", "#C43E55"),
("rgba(16, 22, 38, 0.96)", "rgba(255, 255, 255, 0.96)"),
("rgba(16, 22, 38, 0.98)", "rgba(255, 255, 255, 0.98)"),
)
for _source, _target in _DIAGNOSIS_INDEX_LIGHT_REPLACEMENTS:
DIAGNOSIS_INDEX_QSS = DIAGNOSIS_INDEX_QSS.replace(_source, _target)
del _source, _target
# Filled actions remain white-on-accent after the general foreground rewrite.
DIAGNOSIS_INDEX_QSS += """
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="primary"],
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="info"],
#DiagnosisIndex QPushButton[variant="primary"],
#DiagnosisIndex QPushButton[variant="success"] {
color: #FFFFFF;
}
"""
__all__ = [
#DiagnosisIndex QScrollBar::handle:horizontal {
min-width: 32px;
border-radius: 5px;
background: #29334F;
}
"""
_DIAGNOSIS_INDEX_LIGHT_REPLACEMENTS = (
("#080B14", "#F8FAFF"),
("#101626", "#FFFFFF"),
("#151D31", "#F8FAFF"),
("#1B2440", "#F0F3FC"),
("#29334F", "#E2E7F4"),
("#EEF2FF", "#15224A"),
("#9AA7C0", "#7481A3"),
("#6675F5", "#5265F6"),
("#78A7FF", "#4776EE"),
("#49C6A5", "#159C79"),
("#E4B967", "#C17A16"),
("#F07886", "#EC5266"),
("rgba(16, 22, 38, 0.96)", "rgba(255, 255, 255, 0.96)"),
("rgba(16, 22, 38, 0.98)", "rgba(255, 255, 255, 0.98)"),
)
for _source, _target in _DIAGNOSIS_INDEX_LIGHT_REPLACEMENTS:
DIAGNOSIS_INDEX_QSS = DIAGNOSIS_INDEX_QSS.replace(_source, _target)
del _source, _target
# Filled actions remain white-on-accent after the general foreground rewrite.
DIAGNOSIS_INDEX_QSS += """
#DiagnosisIndex QFrame#DiagnosisFilterCard,
#DiagnosisIndex QFrame#DiagnosisListCard {
border: 1px solid #E2E7F4;
border-radius: 13px;
}
#DiagnosisIndex QLineEdit,
#DiagnosisIndex QComboBox,
#DiagnosisIndex QDateEdit,
#DiagnosisIndex QSpinBox,
#DiagnosisIndex QPushButton {
border-radius: 8px;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"] { border-radius: 8px; }
#DiagnosisIndex QFrame#DiagnosisListToolbar {
border-top-left-radius: 13px;
border-top-right-radius: 13px;
}
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="primary"],
#DiagnosisIndex QToolButton[diagnosisChip="true"]:checked[semantic="info"],
#DiagnosisIndex QPushButton[variant="primary"],
#DiagnosisIndex QPushButton[variant="success"] {
color: #FFFFFF;
}
"""
__all__ = [
"DIAGNOSIS_INDEX_QSS",
"DiagnosisChip",
"DiagnosisLoadingOverlay",
@@ -128,48 +128,64 @@ def open_safe_http_url(target: str) -> bool:
_INLINE_PLAYER_QSS = """
QWidget#DiagnosisInlineRecordingPlayer {
background: #101626;
border: 1px solid #29334F;
border-radius: 6px;
background: #FFFFFF;
border: 1px solid #E6EAF5;
border-radius: 9px;
}
QFrame#DiagnosisInlineRecordingSurface {
background: #080B14;
background: #11182E;
border: 0;
border-radius: 5px 5px 0 0;
border-radius: 8px 8px 0 0;
}
QLabel#DiagnosisInlineRecordingPlaceholder {
color: #9AA7C0;
color: #C7D0E8;
font-size: 12px;
line-height: 1.4;
}
QLabel#DiagnosisInlineRecordingTime {
color: #9AA7C0;
color: #64739A;
font-size: 11px;
}
QPushButton[recordingControl="true"] {
min-height: 24px;
max-height: 24px;
padding: 0 8px;
color: #EEF2FF;
background: #151D31;
border: 1px solid #29334F;
border-radius: 4px;
color: #3F4E75;
background: #FAFBFE;
border: 1px solid #D8DEEE;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
QPushButton[recordingControl="true"]:hover,
QPushButton[recordingControl="true"]:focus {
color: #EEF2FF;
background: #6675F5;
border-color: #6675F5;
color: #4451E2;
background: #F0F2FF;
border-color: #8D9BFF;
}
QPushButton[recordingControl="true"]:disabled { color: #9AA7C0; background:#101626; }
QSlider::groove:horizontal { height: 3px; background: #29334F; border-radius: 1px; }
QSlider::sub-page:horizontal { background: #6675F5; border-radius: 1px; }
QPushButton#DiagnosisInlineRecordingPlay {
color: #FFFFFF;
background: #5761F4;
border-color: #5761F4;
}
QPushButton#DiagnosisInlineRecordingPlay:hover,
QPushButton#DiagnosisInlineRecordingPlay:focus {
color: #FFFFFF;
background: #4C57E9;
border-color: #4C57E9;
}
QPushButton[recordingControl="true"]:disabled {
color: #A4ADC3;
background: #F0F2F8;
border-color: #E6EAF5;
}
QSlider::groove:horizontal { height: 3px; background: #D8DEEE; border-radius: 1px; }
QSlider::sub-page:horizontal { background: #5761F4; border-radius: 1px; }
QSlider::handle:horizontal {
width: 10px;
margin: -4px 0;
background: #EEF2FF;
border: 1px solid #9AA7C0;
background: #FFFFFF;
border: 1px solid #5761F4;
border-radius: 5px;
}
"""
@@ -445,11 +461,11 @@ class RecordingPlaybackCell(QWidget):
separator = QFrame()
separator.setObjectName("DiagnosisRecordingAlternateSeparator")
separator.setFrameShape(QFrame.Shape.HLine)
separator.setStyleSheet("color:#29334F;")
separator.setStyleSheet("color:#E6EAF5;")
layout.addWidget(separator)
label = QLabel("备用地址")
label.setObjectName("DiagnosisRecordingAlternateLabel")
label.setStyleSheet("color:#9AA7C0; font-size:12px;")
label.setStyleSheet("color:#7886AA; font-size:12px;")
layout.addWidget(label)
links = QHBoxLayout()
links.setContentsMargins(0, 0, 0, 0)
@@ -469,7 +485,7 @@ class RecordingPlaybackCell(QWidget):
layout.addLayout(links)
self.link_status = QLabel("")
self.link_status.setObjectName("DiagnosisRecordingLinkStatus")
self.link_status.setStyleSheet("color:#F07886; font-size:11px;")
self.link_status.setStyleSheet("color:#D94856; font-size:11px;")
self.link_status.setWordWrap(True)
self.link_status.hide()
layout.addWidget(self.link_status)
@@ -69,6 +69,7 @@ from ..widgets import (
page_total,
run_async,
)
from .prescription_ai import can_open_diagnosis_ai_report, present_diagnosis_ai_report
_PHONE_PERMISSION = "tcm.diagnosis/phonePlain"
_PATIENT_ORDERS_PERMISSION = "tcm.diagnosis/patientOrders"
@@ -186,38 +187,38 @@ _ORDER_OFFSET_HELP = (
_ORDER_DETAIL_QSS = """
QDialog#DiagnosisOrderDetailOverlay { background: transparent; }
QFrame#DiagnosisOrderDetailScrim { background: rgba(8, 11, 20, 0.78); border: 0; }
QFrame#DiagnosisOrderDetailScrim { background: rgba(30, 64, 175, 0.18); border: 0; }
QFrame#DiagnosisOrderDetailDrawer {
background: #F5F7FB;
background: #F7F9FE;
border: 0;
border-left: 1px solid #D8DEEA;
border-left: 1px solid #DDE7FF;
}
QFrame#DiagnosisOrderDetailHeader {
background: #FFFFFF;
border: 0;
border-bottom: 1px solid #D8DEEA;
border-bottom: 1px solid #DDE7FF;
}
QLabel#DiagnosisOrderDetailTitle { color: #172033; font-size: 19px; font-weight: 650; }
QLabel#DiagnosisOrderDetailMeta { color: #667085; font-size: 12px; }
QLabel#DiagnosisOrderDetailTitle { color: #15224A; font-size: 19px; font-weight: 650; }
QLabel#DiagnosisOrderDetailMeta { color: #7481A3; font-size: 12px; }
QLabel#DiagnosisOrderReadonlyBadge {
color: #667085;
background: #F7F8FC;
border: 1px solid #D8DEEA;
color: #3F4E75;
background: #F7F9FE;
border: 1px solid #E2E7F4;
border-radius: 4px;
padding: 3px 7px;
font-size: 11px;
font-weight: 600;
}
QScrollArea#DiagnosisOrderDetailScroll { border: 0; background: #F5F7FB; }
QScrollArea#DiagnosisOrderDetailScroll > QWidget > QWidget { background: #F5F7FB; }
QScrollArea#DiagnosisOrderDetailScroll { border: 0; background: #F7F9FE; }
QScrollArea#DiagnosisOrderDetailScroll > QWidget > QWidget { background: #F7F9FE; }
QFrame[orderAmountCard="true"] {
background: #FFFFFF;
border: 1px solid #D8DEEA;
border-radius: 7px;
border: 1px solid #DDE7FF;
border-radius: 9px;
}
QLabel[orderAmountTitle="true"] { color: #667085; font-size: 11px; font-weight: 550; }
QLabel[orderAmountTitle="true"] { color: #7481A3; font-size: 11px; font-weight: 550; }
QLabel[orderAmountValue="true"] {
color: #172033;
color: #15224A;
font-size: 18px;
font-weight: 700;
}
@@ -226,34 +227,34 @@ QLabel[orderAmountTone="success"] { color: #16876C; }
QLabel[orderAmountTone="warning"] { color: #9A6813; }
QFrame[orderDetailSection="true"] {
background: #FFFFFF;
border: 1px solid #D8DEEA;
border: 1px solid #E2E7F4;
border-radius: 10px;
}
QLabel[orderSectionTitle="true"] { color: #15224A; font-size: 15px; font-weight: 650; }
QLabel[orderSectionHint="true"] { color: #7481A3; font-size: 11px; }
QFrame[orderField="true"] {
background: #F2F6FE;
border: 1px solid #E2E7F4;
border-radius: 7px;
}
QLabel[orderSectionTitle="true"] { color: #172033; font-size: 15px; font-weight: 650; }
QLabel[orderSectionHint="true"] { color: #667085; font-size: 11px; }
QFrame[orderField="true"] {
background: #F7F8FC;
border: 1px solid #D8DEEA;
border-radius: 5px;
}
QLabel[orderFieldLabel="true"] { color: #667085; font-size: 11px; }
QLabel[orderFieldValue="true"] { color: #172033; font-size: 13px; }
QLabel[orderFieldLabel="true"] { color: #7481A3; font-size: 11px; }
QLabel[orderFieldValue="true"] { color: #15224A; font-size: 13px; }
QLabel[orderEmptyState="true"] {
color: #667085;
background: #F7F8FC;
border: 1px dashed #D8DEEA;
color: #7481A3;
background: #F2F6FE;
border: 1px dashed #C9D8F2;
border-radius: 5px;
padding: 18px 12px;
font-size: 12px;
}
QFrame#DiagnosisOrderTimelineItem { border: 0; border-left: 2px solid #D8DEEA; }
QLabel#DiagnosisOrderTimelineTime { color: #667085; font-size: 11px; }
QLabel#DiagnosisOrderTimelineTitle { color: #172033; font-size: 12px; font-weight: 600; }
QLabel#DiagnosisOrderTimelineBody { color: #667085; font-size: 12px; }
QFrame#DiagnosisOrderTimelineItem { border: 0; border-left: 2px solid #93B4F4; }
QLabel#DiagnosisOrderTimelineTime { color: #7481A3; font-size: 11px; }
QLabel#DiagnosisOrderTimelineTitle { color: #15224A; font-size: 12px; font-weight: 600; }
QLabel#DiagnosisOrderTimelineBody { color: #7481A3; font-size: 12px; }
QFrame#DiagnosisOrderDetailFooter {
background: #FFFFFF;
border: 0;
border-top: 1px solid #D8DEEA;
border-top: 1px solid #DDE7FF;
}
"""
@@ -973,8 +974,8 @@ class DiagnosisDialog(QDialog):
back.setCursor(Qt.CursorShape.PointingHandCursor)
back.setStyleSheet(
"QPushButton{height:32px;padding:0 8px;border:0;background:transparent;"
"color:#78A7FF;font-size:13px;font-weight:500;}"
"QPushButton:hover,QPushButton:focus{background:#1B2440;border-radius:6px;}"
"color:#5265F6;font-size:13px;font-weight:500;}"
"QPushButton:hover,QPushButton:focus{background:#F0F2FF;border-radius:6px;}"
)
back.clicked.connect(self.reject)
left_layout.addWidget(back)
@@ -990,7 +991,7 @@ class DiagnosisDialog(QDialog):
self.readonly_hero_name.setObjectName("DiagnosisReadonlyPatientName")
right_layout.addWidget(self.readonly_hero_name)
self.readonly_hero_meta = QLabel("")
self.readonly_hero_meta.setStyleSheet("color:#9AA7C0; font-size:13px;")
self.readonly_hero_meta.setObjectName("DiagnosisReadonlyHeroMeta")
right_layout.addWidget(self.readonly_hero_meta)
self.readonly_status = QLabel("从未打卡")
self.readonly_status.setObjectName("DiagnosisReadonlyStatus")
@@ -1008,9 +1009,18 @@ class DiagnosisDialog(QDialog):
layout = QVBoxLayout(card)
layout.setContentsMargins(18, 18, 18, 18)
layout.setSpacing(14)
heading_row = QHBoxLayout()
heading_row.setContentsMargins(0, 0, 0, 0)
heading_row.setSpacing(12)
heading = QLabel("患者信息")
heading.setObjectName("DiagnosisReadonlyCardTitle")
layout.addWidget(heading)
heading_row.addWidget(heading, 1)
self.readonly_ai_button = QPushButton("AI 报告", card)
self.readonly_ai_button.setProperty("variant", "secondary")
self.readonly_ai_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.readonly_ai_button.clicked.connect(self._open_ai_report)
heading_row.addWidget(self.readonly_ai_button, 0)
layout.addLayout(heading_row)
patient_hero = QFrame()
patient_hero.setObjectName("DiagnosisReadonlyPatientHero")
hero_layout = QVBoxLayout(patient_hero)
@@ -1186,7 +1196,7 @@ class DiagnosisDialog(QDialog):
privacy_layout.setContentsMargins(12, 9, 12, 9)
self.privacy_label = QLabel("存在未完成的业务订单,患者基本信息不可修改")
self.privacy_label.setWordWrap(True)
self.privacy_label.setStyleSheet("color:#E4B967; font-size:12px;")
self.privacy_label.setObjectName("DiagnosisPrivacyText")
privacy_layout.addWidget(self.privacy_label)
self.privacy_banner.hide()
layout.addWidget(self.privacy_banner)
@@ -1445,7 +1455,6 @@ class DiagnosisDialog(QDialog):
toolbar_layout.setContentsMargins(14, 10, 14, 10)
label = QLabel("复诊统计起始偏移")
label.setObjectName("DiagnosisOrderOffsetLabel")
label.setStyleSheet("color:#9AA7C0; font-size:12px; font-weight:500;")
label.setToolTip(_ORDER_OFFSET_HELP)
toolbar_layout.addWidget(label)
help_button = QPushButton("?")
@@ -1454,12 +1463,6 @@ class DiagnosisDialog(QDialog):
help_button.setToolTip(_ORDER_OFFSET_HELP)
help_button.setFixedSize(22, 22)
help_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
help_button.setStyleSheet(
"QPushButton{color:#9AA7C0;border:1px solid #29334F;border-radius:11px;"
"background:#151D31;padding:0;font-size:12px;font-weight:600;}"
"QPushButton:hover,QPushButton:focus{color:#EEF2FF;border-color:#6675F5;"
"background:#1B2440;}"
)
toolbar_layout.addWidget(help_button)
self.order_offset_help = help_button
self.order_offset = QSpinBox()
@@ -1473,7 +1476,6 @@ class DiagnosisDialog(QDialog):
self.order_offset_preview = QLabel("第 1 笔实单计为一诊")
self.order_offset_preview.setObjectName("DiagnosisOrderOffsetPreview")
self.order_offset_preview.setToolTip(_ORDER_OFFSET_HELP)
self.order_offset_preview.setStyleSheet("color:#9AA7C0; font-size:12px;")
toolbar_layout.addWidget(self.order_offset_preview)
offset_save = QPushButton("保存")
offset_save.setProperty("variant", "primary")
@@ -1486,7 +1488,7 @@ class DiagnosisDialog(QDialog):
layout.addWidget(self.orders_table, 1)
footer = QHBoxLayout()
self.orders_summary = QLabel("共 0 条")
self.orders_summary.setStyleSheet("color:#9AA7C0; font-size:12px;")
self.orders_summary.setObjectName("DiagnosisOrdersSummary")
footer.addWidget(self.orders_summary)
footer.addStretch(1)
self.orders_previous = QPushButton("上一页")
@@ -1655,6 +1657,9 @@ class DiagnosisDialog(QDialog):
self._can_note_delete = has_permission(
self.permissions, _NOTE_DELETE_PERMISSION, default=False
) and callable(getattr(self.repository, "delete_doctor_note_image", None))
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:
@@ -1862,6 +1867,30 @@ class DiagnosisDialog(QDialog):
self._owner_filter_installed = False
super().done(result)
def _open_ai_report(self) -> None:
if not can_open_diagnosis_ai_report(self.permissions):
return
if self._diagnosis_id <= 0:
return
detail = self._detail if isinstance(self._detail, Mapping) else {}
diagnosis = get_value(detail, "diagnosis", None)
row = dict(diagnosis) if isinstance(diagnosis, Mapping) else dict(detail)
row.update(
{
"id": self._diagnosis_id,
"diagnosis_id": self._diagnosis_id,
"patient_name": first_value(
row,
"patient_name",
"name",
default=self.readonly_patient_title.text()
if hasattr(self, "readonly_patient_title")
else "患者",
),
}
)
present_diagnosis_ai_report(self.repository, self.permissions, self, row)
def open_view_only(self, diagnosis_id: int, *, seed: Any = None) -> None:
self.open_for(diagnosis_id, editable=False, seed=seed, view_only=True)
@@ -2373,7 +2402,9 @@ class DiagnosisDialog(QDialog):
if key == "appetite" and raw in (None, "", [], ()):
raw = first_value(diagnosis, "oral_condition", "mouth_condition", default="")
if key == "diagnosis_type":
raw = _diagnosis_type_value(raw)
raw = _diagnosis_type_value(raw) or _diagnosis_type_value(
first_value(diagnosis, "consultation_type", default="")
)
if key == "gender":
gender_token = str(raw).strip().lower() if raw not in (None, "") else ""
if gender_token in {"2", "f", "female", ""}:
@@ -105,28 +105,28 @@ from ..widgets import (
PRESCRIPTION_DRAWER_QSS = r"""
QFrame#PrescriptionDrawerSurface {
color: #134E4A;
color: #17203F;
background-color: #FFFFFF;
border-left: 1px solid #D9E0E7;
border-left: 1px solid #DCE3F2;
font-family: "Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-size: 13px;
}
QFrame#PrescriptionDrawerHeader {
background-color: #FFFFFF;
border: 0;
border-bottom: 1px solid #E5E7EB;
border-bottom: 1px solid #E7ECF5;
}
QLabel#PrescriptionDrawerTitle {
color: #303133;
font-size: 16px;
font-weight: 600;
color: #17203F;
font-size: 18px;
font-weight: 700;
}
QLabel#PrescriptionFormLabel {
color: #606266;
font-size: 14px;
color: #56617A;
font-size: 13px;
}
QLabel#PrescriptionFormHint {
color: #909399;
color: #8A94AA;
font-size: 12px;
}
QLabel#PrescriptionLockTag {
@@ -138,31 +138,31 @@ QLabel#PrescriptionLockTag {
font-size: 12px;
}
QWidget#PrescriptionHerbTableHeader {
background-color: #F5F7FA;
border: 1px solid #EBEEF5;
background-color: #F5F7FC;
border: 1px solid #E2E8F4;
border-bottom: 0;
}
QLabel#PrescriptionHerbTableHead {
color: #909399;
color: #7D879E;
font-size: 12px;
font-weight: 600;
}
QFrame#PrescriptionHerbTableRow {
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border: 1px solid #E2E8F4;
border-top: 0;
}
QFrame#PrescriptionHerbTableRow[duplicate="true"] {
background-color: #FDF6EC;
}
QLabel#PrescriptionDrawerSubtitle {
color: #5B7A76;
color: #78849D;
font-size: 12px;
}
QLabel#PrescriptionDrawerMode {
color: #1677A3;
background-color: #EAF6FB;
border: 1px solid #B9DEEC;
color: #4D57D8;
background-color: #F0F2FF;
border: 1px solid #D8DCFF;
border-radius: 5px;
padding: 4px 9px;
font-size: 12px;
@@ -174,7 +174,7 @@ QPushButton#PrescriptionDrawerClose {
min-height: 34px;
max-height: 34px;
padding: 0;
color: #5B7A76;
color: #78849D;
background-color: transparent;
border: 0;
border-radius: 7px;
@@ -182,8 +182,8 @@ QPushButton#PrescriptionDrawerClose {
font-weight: 400;
}
QPushButton#PrescriptionDrawerClose:hover {
color: #134E4A;
background-color: #F2F4F7;
color: #5761F4;
background-color: #F0F2FF;
}
QScrollArea#PrescriptionDrawerBody,
QScrollArea#PrescriptionDrawerBody > QWidget > QWidget {
@@ -193,30 +193,30 @@ QScrollArea#PrescriptionDrawerBody > QWidget > QWidget {
QWidget#PrescriptionDrawerContent {
background-color: #FFFFFF;
}
QFrame#PrescriptionSection {
background-color: #F5F7FA;
border: 0;
border-radius: 7px;
QFrame#PrescriptionFormSection {
background-color: #FAFBFE;
border: 1px solid #E3E8F4;
border-radius: 10px;
}
QLabel#PrescriptionSectionTitle {
color: #134E4A;
color: #17203F;
font-size: 15px;
font-weight: 600;
}
QLabel#PrescriptionSectionHint {
color: #5B7A76;
color: #8A94AA;
font-size: 12px;
}
QWidget#PrescriptionField {
background-color: transparent;
}
QLabel#PrescriptionFieldLabel {
color: #5B7A76;
color: #56617A;
font-size: 12px;
font-weight: 500;
}
QLabel#PrescriptionUsageMain {
color: #409EFF;
color: #5761F4;
font-size: 13px;
font-weight: 600;
padding: 8px 0 4px 0;
@@ -229,21 +229,21 @@ QLabel#PrescriptionUsageAux {
}
QFrame#PrescriptionRpToolbar {
background-color: #FFFFFF;
border: 1px solid #E2E8F0;
border: 1px solid #DCE3F2;
border-radius: 10px;
}
QFrame#PrescriptionRpMarker {
background-color: #0891B2;
background-color: #5761F4;
border: 0;
border-radius: 2px;
}
QLabel#PrescriptionRpHeading {
color: #134E4A;
color: #17203F;
font-size: 16px;
font-weight: 600;
}
QLabel#PrescriptionRpMeta {
color: #7A8492;
color: #8A94AA;
font-size: 12px;
}
QLabel#PrescriptionRpLock {
@@ -261,9 +261,9 @@ QLabel#PrescriptionFormulaTag {
font-weight: 600;
}
QLabel#PrescriptionFormulaTag[formula="main"] {
color: #0E7490;
background-color: #ECFEFF;
border: 1px solid #A0CFFF;
color: #4D57D8;
background-color: #F0F2FF;
border: 1px solid #D8DCFF;
}
QLabel#PrescriptionFormulaTag[formula="aux"] {
color: #B88230;
@@ -271,24 +271,24 @@ QLabel#PrescriptionFormulaTag[formula="aux"] {
border: 1px solid #F3D19E;
}
QLabel#PrescriptionFormulaEmpty {
color: #909399;
color: #8A94AA;
background-color: #FFFFFF;
border: 1px solid #EBEEF5;
border: 1px solid #E2E8F4;
border-top: 0;
padding: 16px 8px;
font-size: 13px;
}
QFrame#PrescriptionHerbCard {
background-color: #FFFFFF;
border: 1px solid #D5E5E2;
border-radius: 7px;
border: 1px solid #DCE3F2;
border-radius: 8px;
}
QFrame#PrescriptionHerbCard[duplicate="true"] {
background-color: #FDF6EC;
border: 1px solid #E6A23C;
}
QLabel#PrescriptionHerbCardTitle {
color: #5B7A76;
color: #78849D;
font-size: 12px;
}
QPushButton#PrescriptionHerbDelete {
@@ -306,29 +306,29 @@ QPushButton#PrescriptionHerbDelete:hover {
QFrame#PrescriptionDrawerFooter {
background-color: #FFFFFF;
border: 0;
border-top: 1px solid #E5E7EB;
border-top: 1px solid #E7ECF5;
}
QFrame#PrescriptionDrawerSurface QLineEdit,
QFrame#PrescriptionDrawerSurface QTextEdit,
QFrame#PrescriptionDrawerSurface QSpinBox,
QFrame#PrescriptionDrawerSurface QDoubleSpinBox {
min-height: 32px;
color: #303133;
color: #17203F;
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #DCE3F2;
border-radius: 7px;
padding: 0 11px;
selection-background-color: #D9ECFF;
selection-background-color: #E3E6FF;
}
QFrame#PrescriptionDrawerSurface QComboBox {
min-height: 32px;
color: #303133;
color: #17203F;
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #DCE3F2;
border-radius: 7px;
padding-left: 11px;
padding-right: 28px;
selection-background-color: #D9ECFF;
selection-background-color: #E3E6FF;
}
QFrame#PrescriptionDrawerSurface QTextEdit {
padding: 7px 9px;
@@ -338,28 +338,28 @@ QFrame#PrescriptionDrawerSurface QTextEdit:focus,
QFrame#PrescriptionDrawerSurface QComboBox:focus,
QFrame#PrescriptionDrawerSurface QSpinBox:focus,
QFrame#PrescriptionDrawerSurface QDoubleSpinBox:focus {
border: 1px solid #409EFF;
border: 1px solid #5761F4;
}
QFrame#PrescriptionDrawerSurface QLineEdit:read-only {
color: #5B7A76;
background-color: #F0F2F5;
color: #78849D;
background-color: #F5F7FC;
}
QFrame#PrescriptionDatePicker {
min-height: 32px;
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #DCE3F2;
border-radius: 7px;
}
QFrame#PrescriptionDatePicker:hover {
border-color: #C0C4CC;
}
QFrame#PrescriptionDatePicker[active="true"] {
border: 1px solid #409EFF;
border: 1px solid #5761F4;
}
QFrame#PrescriptionDrawerSurface QFrame#PrescriptionDatePicker QLineEdit,
QFrame#PrescriptionDrawerSurface QFrame#PrescriptionDatePicker QLineEdit:read-only {
min-height: 30px;
color: #303133;
color: #17203F;
background-color: transparent;
border: 0;
padding: 0 4px 0 11px;
@@ -370,14 +370,14 @@ QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDateButton {
min-height: 30px;
max-height: 30px;
padding: 0;
color: #909399;
color: #8A94AA;
background-color: transparent;
border: 0;
border-radius: 0;
}
QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDateButton:hover,
QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDateButton:pressed {
color: #409EFF;
color: #5761F4;
background-color: transparent;
border: 0;
}
@@ -398,30 +398,30 @@ QFrame#PrescriptionDrawerSurface QComboBox:hover {
}
QFrame#PrescriptionDrawerSurface QComboBox QAbstractItemView {
outline: 0;
color: #303133;
color: #17203F;
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
selection-background-color: #ECF5FF;
selection-color: #409EFF;
border: 1px solid #DCE3F2;
selection-background-color: #F0F2FF;
selection-color: #4D57D8;
}
QFrame#PrescriptionDrawerSurface QRadioButton {
min-height: 32px;
spacing: 6px;
color: #606266;
color: #56617A;
}
QFrame#PrescriptionDrawerSurface QPushButton {
min-height: 32px;
padding: 0 15px;
color: #606266;
color: #4F5B75;
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
border-radius: 4px;
border: 1px solid #DCE3F2;
border-radius: 7px;
font-weight: 500;
}
QFrame#PrescriptionDrawerSurface QPushButton:hover {
color: #409EFF;
background-color: #ECF5FF;
border-color: #C6E2FF;
color: #4D57D8;
background-color: #F0F2FF;
border-color: #D8DCFF;
}
QFrame#PrescriptionDrawerSurface QPushButton[size="small"] {
min-height: 24px;
@@ -434,7 +434,7 @@ QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDrawerClose {
min-height: 34px;
max-height: 34px;
padding: 0;
color: #5B7A76;
color: #78849D;
background-color: transparent;
border: 0;
border-radius: 7px;
@@ -442,27 +442,27 @@ QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDrawerClose {
font-weight: 400;
}
QFrame#PrescriptionDrawerSurface QPushButton#PrescriptionDrawerClose:hover {
color: #134E4A;
background-color: #F2F4F7;
color: #5761F4;
background-color: #F0F2FF;
border: 0;
}
QFrame#PrescriptionDrawerSurface QPushButton:pressed {
background-color: #0E7490;
border-color: #0E7490;
background-color: #4D57D8;
border-color: #4D57D8;
}
QFrame#PrescriptionDrawerSurface QPushButton:disabled {
color: #A8ABB2;
background-color: #F5F7FA;
border-color: #D5E5E2;
color: #A1A9BA;
background-color: #F5F7FC;
border-color: #E2E8F4;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="primary"] {
color: #FFFFFF;
background-color: #409EFF;
border-color: #409EFF;
background-color: #5761F4;
border-color: #5761F4;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="primary"]:hover {
background-color: #66B1FF;
border-color: #66B1FF;
background-color: #6871F6;
border-color: #6871F6;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="success"] {
color: #FFFFFF;
@@ -484,23 +484,23 @@ QFrame#PrescriptionDrawerSurface QPushButton[variant="warning"]:hover {
border-color: #E6A23C;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="secondary"] {
color: #409EFF;
color: #4D57D8;
background-color: #FFFFFF;
border-color: #B3D8FF;
border-color: #D8DCFF;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="secondary"]:hover {
color: #FFFFFF;
background-color: #409EFF;
border-color: #409EFF;
background-color: #5761F4;
border-color: #5761F4;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="ghost"] {
color: #5B7A76;
color: #78849D;
background-color: transparent;
border-color: transparent;
}
QFrame#PrescriptionDrawerSurface QPushButton[variant="ghost"]:hover {
color: #134E4A;
background-color: #F2F4F7;
color: #17203F;
background-color: #F0F2FF;
}
QFrame#PrescriptionDrawerSurface QScrollBar:vertical {
width: 10px;
@@ -639,31 +639,219 @@ def _prescription_calendar_qss() -> str:
return """
QCalendarWidget {
background-color: #FFFFFF;
border: 1px solid #DCDFE6;
border: 1px solid #DCE3F2;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #FFFFFF;
border: 0;
}
QCalendarWidget QToolButton {
color: #303133;
color: #17203F;
background-color: transparent;
border: 0;
padding: 4px 8px;
font-size: 13px;
}
QCalendarWidget QToolButton:hover {
color: #409EFF;
color: #5761F4;
}
QCalendarWidget QAbstractItemView:enabled {
color: #303133;
selection-background-color: #409EFF;
color: #17203F;
selection-background-color: #5761F4;
selection-color: #FFFFFF;
outline: 0;
}
"""
PRESCRIPTION_DIALOG_QSS = """
QDialog#PrescriptionDialogSurface {
color: #17203F;
background-color: #F7F9FE;
font-family: "Microsoft YaHei UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-size: 13px;
}
QDialog#PrescriptionDialogSurface QLabel {
color: #4F5B75;
}
QDialog#PrescriptionDialogSurface QLabel[role="pageTitle"] {
color: #17203F;
font-size: 20px;
font-weight: 700;
}
QDialog#PrescriptionDialogSurface QLabel[role="muted"] {
color: #8A94AA;
}
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="neutral"] {
color: #78849D;
background-color: #F5F7FC;
border-color: #E2E8F4;
}
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="success"] {
color: #319F84;
background-color: #ECFBF6;
border-color: #BDEBDD;
}
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="warning"] {
color: #B48739;
background-color: #FFF5E6;
border-color: #F3DFB5;
}
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="danger"] {
color: #D84E5B;
background-color: #FFF1F3;
border-color: #F7C9CF;
}
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="info"],
QDialog#PrescriptionDialogSurface QLabel#StatusBadge[kind="accent"] {
color: #4D57D8;
background-color: #F0F2FF;
border-color: #D8DCFF;
}
QDialog#PrescriptionDialogSurface QFrame#MessageBanner[kind="info"] QLabel {
color: #4D57D8;
}
QDialog#PrescriptionDialogSurface QFrame#MessageBanner[kind="success"] QLabel {
color: #319F84;
}
QDialog#PrescriptionDialogSurface QFrame#MessageBanner[kind="warning"] QLabel {
color: #B48739;
}
QDialog#PrescriptionDialogSurface QFrame#MessageBanner[kind="danger"] QLabel {
color: #D84E5B;
}
QDialog#PrescriptionDialogSurface QLineEdit,
QDialog#PrescriptionDialogSurface QTextEdit,
QDialog#PrescriptionDialogSurface QComboBox,
QDialog#PrescriptionDialogSurface QSpinBox,
QDialog#PrescriptionDialogSurface QDoubleSpinBox,
QDialog#PrescriptionDialogSurface QListWidget,
QDialog#PrescriptionDialogSurface QTextBrowser {
color: #17203F;
background-color: #FFFFFF;
border: 1px solid #DCE3F2;
border-radius: 7px;
selection-background-color: #E3E6FF;
selection-color: #17203F;
}
QDialog#PrescriptionDialogSurface QLineEdit,
QDialog#PrescriptionDialogSurface QComboBox,
QDialog#PrescriptionDialogSurface QSpinBox,
QDialog#PrescriptionDialogSurface QDoubleSpinBox {
min-height: 34px;
padding: 0 10px;
}
QDialog#PrescriptionDialogSurface QTextEdit,
QDialog#PrescriptionDialogSurface QTextBrowser {
padding: 8px;
}
QDialog#PrescriptionDialogSurface QLineEdit:focus,
QDialog#PrescriptionDialogSurface QTextEdit:focus,
QDialog#PrescriptionDialogSurface QComboBox:focus,
QDialog#PrescriptionDialogSurface QSpinBox:focus,
QDialog#PrescriptionDialogSurface QDoubleSpinBox:focus {
border: 1px solid #5761F4;
}
QDialog#PrescriptionDialogSurface QPushButton {
min-height: 34px;
padding: 0 15px;
color: #4F5B75;
background-color: #FFFFFF;
border: 1px solid #DCE3F2;
border-radius: 7px;
font-weight: 500;
}
QDialog#PrescriptionDialogSurface QPushButton:hover {
color: #4D57D8;
background-color: #F0F2FF;
border-color: #D8DCFF;
}
QDialog#PrescriptionDialogSurface QPushButton:pressed {
color: #FFFFFF;
background-color: #4D57D8;
border-color: #4D57D8;
}
QDialog#PrescriptionDialogSurface QPushButton[variant="primary"] {
color: #FFFFFF;
background-color: #5761F4;
border-color: #5761F4;
}
QDialog#PrescriptionDialogSurface QPushButton[variant="primary"]:hover {
background-color: #6871F6;
border-color: #6871F6;
}
QDialog#PrescriptionDialogSurface QPushButton:disabled {
color: #A1A9BA;
background-color: #F0F2F8;
border-color: #E2E8F4;
}
QDialog#PrescriptionDialogSurface QTabWidget::pane {
background-color: #FFFFFF;
border: 1px solid #DCE3F2;
border-radius: 9px;
top: -1px;
}
QDialog#PrescriptionDialogSurface QTabBar::tab {
min-height: 34px;
padding: 0 16px;
color: #78849D;
background-color: transparent;
border: 0;
border-bottom: 2px solid transparent;
}
QDialog#PrescriptionDialogSurface QTabBar::tab:hover {
color: #4D57D8;
background-color: #F5F7FC;
}
QDialog#PrescriptionDialogSurface QTabBar::tab:selected {
color: #4D57D8;
background-color: #F0F2FF;
border-bottom: 2px solid #5761F4;
font-weight: 600;
}
QDialog#PrescriptionDialogSurface QTableWidget {
color: #17203F;
background-color: #FFFFFF;
alternate-background-color: #FAFBFE;
border: 1px solid #DCE3F2;
border-radius: 8px;
gridline-color: #E7ECF5;
selection-background-color: #F0F2FF;
selection-color: #17203F;
}
QDialog#PrescriptionDialogSurface QHeaderView::section {
min-height: 36px;
color: #78849D;
background-color: #F5F7FC;
border: 0;
border-bottom: 1px solid #E2E8F4;
padding: 0 9px;
font-weight: 600;
}
QDialog#PrescriptionDialogSurface QScrollBar:vertical {
width: 10px;
margin: 2px;
background-color: transparent;
}
QDialog#PrescriptionDialogSurface QScrollBar::handle:vertical {
min-height: 32px;
background-color: #C8D0E0;
border-radius: 4px;
}
QDialog#PrescriptionDialogSurface QScrollBar::add-line:vertical,
QDialog#PrescriptionDialogSurface QScrollBar::sub-line:vertical {
height: 0;
}
"""
def _apply_prescription_dialog_style(dialog: QDialog) -> None:
"""Keep prescription-only windows aligned with the blue-white desktop shell."""
dialog.setObjectName("PrescriptionDialogSurface")
dialog.setStyleSheet(PRESCRIPTION_DIALOG_QSS)
_PRESCRIPTION_DARK_REPLACEMENTS = (
("color: #FFFFFF", "color: #EEF2FF"),
("selection-background-color: #D9ECFF", "selection-background-color: #6675F5"),
@@ -1968,6 +2156,7 @@ class PrescriptionTemplateDialog(QDialog):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.repository = repository
self.template = template
self.mode = mode
@@ -2097,6 +2286,7 @@ class TemplateImportDialog(QDialog):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.repository = repository
self.prescribing_creator_id = prescribing_creator_id
self._page = 1
@@ -2333,6 +2523,7 @@ class PasteHerbsDialog(QDialog):
def __init__(self, repository: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.repository = repository
self.resolved: list[dict[str, Any]] = []
self.skipped: list[str] = []
@@ -2608,7 +2799,11 @@ class PrescriptionEditorDialog(QDialog):
layout.setSpacing(12)
title = QLabel("新增处方" if self.mode == "add" else "编辑处方")
title.setObjectName("PrescriptionDrawerTitle")
layout.addWidget(title, 1)
layout.addWidget(title)
mode = QLabel("新增" if self.mode == "add" else "编辑")
mode.setObjectName("PrescriptionDrawerMode")
layout.addWidget(mode)
layout.addStretch(1)
close_button = QPushButton("×", header)
close_button.setObjectName("PrescriptionDrawerClose")
close_button.setToolTip("关闭")
@@ -2621,8 +2816,8 @@ class PrescriptionEditorDialog(QDialog):
section = QFrame()
section.setObjectName("PrescriptionFormSection")
layout = QVBoxLayout(section)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(10)
layout.setContentsMargins(16, 14, 16, 16)
layout.setSpacing(11)
if title:
heading_row = QHBoxLayout()
heading_row.setContentsMargins(0, 0, 0, 0)
@@ -2652,7 +2847,7 @@ class PrescriptionEditorDialog(QDialog):
caption.setText(f'<span style="color:#F56C6C">*</span> {label}')
else:
caption.setText(label)
caption.setFixedWidth(12 if blank_label else 120)
caption.setFixedWidth(12 if blank_label else 92)
caption.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
tall = isinstance(widget, (QTextEdit, SignaturePad))
layout.addWidget(
@@ -2716,7 +2911,7 @@ class PrescriptionEditorDialog(QDialog):
diagnosis_id = _int(self._source.get("diagnosis_id"), 0)
diagnosis_row = QWidget(section)
diagnosis_layout = QHBoxLayout(diagnosis_row)
diagnosis_layout.setContentsMargins(120, 0, 0, 0)
diagnosis_layout.setContentsMargins(92, 0, 0, 0)
diagnosis_layout.setSpacing(8)
self.diagnosis_button = QPushButton("查看患者诊单详情", diagnosis_row)
self.diagnosis_button.setProperty("variant", "primary")
@@ -3640,6 +3835,7 @@ class PatchPatientDialog(QDialog):
def __init__(self, prescription: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.prescription_id = _int(first_value(prescription, "id", "prescription_id"))
self.setWindowTitle("修正姓名、性别与手机号")
self.resize(460, 300)
@@ -3693,6 +3889,7 @@ class AuditPrescriptionDialog(QDialog):
def __init__(self, prescription: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.prescription_id = _int(first_value(prescription, "id", "prescription_id"))
self.action = ""
self.setWindowTitle("处方审核")
@@ -4709,6 +4906,7 @@ class PrescriptionDetailDialog(QDialog):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.prescription = prescription
self.document = QTextDocument(self)
self.document.setDocumentMargin(0)
@@ -4808,6 +5006,7 @@ class PrescriptionDetailDialog(QDialog):
self.tabs.setCurrentIndex(case_index)
root.addWidget(self.tabs, 1)
close = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
close.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
close.rejected.connect(self.reject)
root.addWidget(close)
@@ -5022,7 +5221,7 @@ class DiagnosisDetailDialog(QDialog):
return
order_id = _int(first_value(order, "id", "order_id"), 0)
if order_id > 0 and callable(getattr(self.repository, "get_prescription_order", None)):
try:
try: # noqa: SIM105 - retain the embedded row if detail lookup fails
order = self.repository.get_prescription_order(order_id)
except Exception: # noqa: BLE001 - fall back to embedded row
pass
@@ -5058,6 +5257,7 @@ class PrescriptionOrderDialog(QDialog):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.repository = repository
self.prescription = prescription
self.can_select_ship_mode = can_select_ship_mode
@@ -5400,6 +5600,7 @@ class PrescriptionOrderListDialog(QDialog):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
_apply_prescription_dialog_style(self)
self.repository = repository
self.prescription_id = prescription_id
self.patient_id = patient_id
@@ -5445,6 +5646,7 @@ class PrescriptionOrderListDialog(QDialog):
self.next = next_button
root.addLayout(footer)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
QTimer.singleShot(0, self.load)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+422 -86
View File
@@ -7,8 +7,8 @@ from collections.abc import Mapping, Sequence
from types import MappingProxyType
from typing import Any
from PySide6.QtCore import QDate, Qt, QTime, QTimer, Signal
from PySide6.QtGui import QAction, QBrush, QColor
from PySide6.QtCore import QDate, QSize, Qt, QTime, QTimer, Signal
from PySide6.QtGui import QAction, QBrush, QColor, QIcon, QPainter, QPen, QPixmap
from PySide6.QtWidgets import (
QButtonGroup,
QCheckBox,
@@ -39,6 +39,7 @@ from PySide6.QtWidgets import (
from ..appointment_drawer import AppointmentDrawer
from ..dialogs import DiagnosisDialog, present_order_detail
from ..dialogs.prescription import PrescriptionOrderListDialog
from ..theme import mark_business_dialog
from ..widgets import (
EmptyState,
MessageBanner,
@@ -88,21 +89,171 @@ FULFILLMENT_TEXT = {
}
_SEMANTIC_COLORS = {
"primary": "#4F63D9",
"success": "#16876C",
"warning": "#9A6813",
"danger": "#C43E55",
"info": "#2F6EDB",
"muted": "#667085",
"primary": "#5265F6",
"success": "#159C79",
"warning": "#C17A16",
"danger": "#EC5266",
"info": "#4776EE",
"muted": "#7481A3",
}
PATIENTS_LIGHT_QSS = """
#PatientsPage QPushButton[variant="ghost"]:checked {
color: #3446AF;
background-color: #E9EDFF;
border: 1px solid #4F63D9;
#PatientsPage QWidget#PageHeader { min-height: 88px; max-height: 88px; }
#PatientsPage QWidget#PageHeader QLabel[role="pageTitle"] {
color: #10204A;
font-size: 20px;
font-weight: 700;
}
#PatientsPage QPushButton[variant="ghost"]:checked:hover { background-color: #DCE3FF; }
#PatientsPage QWidget#PageHeader QLabel[role="muted"] {
color: #6F7FA5;
font-size: 12px;
}
#PatientsPage QPushButton[patientSearchAction="true"] {
min-height: 34px;
max-height: 34px;
color: #FFFFFF;
background-color: #5265F6;
border: 1px solid #5265F6;
border-radius: 7px;
font-weight: 600;
}
#PatientsPage QPushButton[patientSearchAction="true"]:hover {
background-color: #4557E7;
border-color: #4557E7;
}
#PatientsPage QFrame[patientListFilter="true"] {
min-height: 112px;
max-height: 112px;
background-color: #FFFFFF;
border: 1px solid #E6EAF5;
border-radius: 11px;
}
#PatientsPage QFrame[patientListFilter="true"] QLineEdit,
#PatientsPage QFrame[patientListFilter="true"] QDateEdit {
min-height: 34px;
max-height: 34px;
background-color: #FFFFFF;
border: 1px solid #E2E7F4;
border-radius: 7px;
}
#PatientsPage QPushButton[patientStatusChip="true"] {
min-height: 34px;
max-height: 34px;
padding: 0 13px;
color: #405074;
background-color: #F8F9FD;
border: 0;
border-radius: 5px;
}
#PatientsPage QPushButton[patientStatusChip="true"]:hover {
color: #5265F6;
background-color: #F0F2FF;
}
#PatientsPage QPushButton[patientStatusChip="true"]:checked {
color: #5265F6;
background-color: #F0F1FF;
border: 1px solid #9EA8FF;
}
#PatientsPage QPushButton[patientQuickDate="true"] {
min-height: 34px;
max-height: 34px;
padding: 0 12px;
color: #29365C;
background-color: transparent;
border: 1px solid transparent;
border-radius: 7px;
}
#PatientsPage QPushButton[patientQuickDate="true"]:hover {
color: #5265F6;
background-color: #F7F8FF;
}
#PatientsPage QPushButton[patientQuickDate="true"]:checked {
color: #5265F6;
background-color: #F8F8FF;
border-color: #7C85FF;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs::pane {
border: 0;
border-top: 1px solid #E6EAF5;
background-color: transparent;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs QTabBar::tab {
min-width: 86px;
min-height: 40px;
padding: 0 6px;
margin-right: 8px;
color: #59698E;
background: transparent;
border: 0;
border-bottom: 2px solid transparent;
font-weight: 600;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs QTabBar::tab:selected {
color: #5265F6;
border-bottom-color: #5265F6;
}
#PatientsPage QPushButton[summaryCard="true"] {
min-height: 54px;
max-height: 54px;
padding: 0 13px;
color: #5265F6;
background-color: #F7F8FF;
border: 1px solid #E1E5FF;
border-radius: 9px;
text-align: left;
font-weight: 600;
}
#PatientsPage QPushButton[summaryCard="true"]:hover {
color: #3C4FD9;
background-color: #EEF1FF;
border-color: #AEB9FF;
}
#PatientsPage QPushButton[rowAction="true"] {
min-height: 26px;
max-height: 26px;
padding: 0 7px;
color: #5265F6;
background-color: #FFFFFF;
border: 1px solid #E2E7F4;
border-radius: 6px;
font-size: 10px;
}
#PatientsPage QPushButton[rowAction="true"]:hover {
background-color: #EEF1FF;
border-color: #AEB9FF;
}
#PatientsPage QPushButton[rowAction="true"][variant="danger"] {
color: #EC5266;
background-color: #FFF7F8;
border-color: #F4C4CC;
}
#PatientsPage QTableWidget#PatientTable QHeaderView::section {
min-height: 34px;
background-color: #F8FAFF;
}
#PatientsPage QTableWidget#PatientTable::item { padding: 5px 7px; }
#PatientsPage QTableWidget#PatientTable {
gridline-color: #E8ECF5;
selection-background-color: #F7F8FF;
selection-color: #15224A;
}
#PatientsPage QCheckBox[patientSelector="true"]::indicator {
width: 13px;
height: 13px;
background-color: #FFFFFF;
border: 1px solid #CBD3E7;
border-radius: 2px;
}
#PatientsPage QCheckBox[patientSelector="true"]::indicator:checked {
background-color: #5265F6;
border-color: #5265F6;
}
#PatientsPage QPushButton[variant="ghost"]:checked {
color: #3C4FD9;
background-color: #EEF1FF;
border: 1px solid #5265F6;
}
#PatientsPage QPushButton[variant="ghost"]:checked:hover { background-color: #E8ECFF; }
#PatientsPage QPushButton[statusKind="info"]:checked {
color: #2F6EDB;
border-color: #2F6EDB;
@@ -119,35 +270,57 @@ PATIENTS_LIGHT_QSS = """
color: #C43E55;
border-color: #C43E55;
}
#PatientsPage QLabel[metricKind="primary"] { color: #4F63D9; }
#PatientsPage QLabel[metricKind="info"] { color: #2F6EDB; }
#PatientsPage QLabel[metricKind="success"] { color: #16876C; }
#PatientsPage QLabel[metricKind="warning"] { color: #9A6813; }
#PatientsPage QLabel[metricKind="danger"] { color: #C43E55; }
#PatientsPage QLabel[metricKind="primary"] { color: #5265F6; }
#PatientsPage QLabel[metricKind="info"] { color: #4776EE; }
#PatientsPage QLabel[metricKind="success"] { color: #159C79; }
#PatientsPage QLabel[metricKind="warning"] { color: #C17A16; }
#PatientsPage QLabel[metricKind="danger"] { color: #EC5266; }
#PatientsPage QTabWidget#PatientWorkspaceTabs::pane {
background-color: transparent;
border: 1px solid #D8DEEA;
border-radius: 10px;
border: 0;
top: -1px;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs QTabBar::tab {
color: #667085;
color: #7481A3;
background-color: transparent;
border: 0;
border-bottom: 2px solid transparent;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs QTabBar::tab:hover {
color: #172033;
background-color: #F7F8FC;
color: #15224A;
background-color: #F8FAFF;
}
#PatientsPage QTabWidget#PatientWorkspaceTabs QTabBar::tab:selected {
color: #3446AF;
background-color: #F7F8FC;
border-bottom-color: #4F63D9;
color: #3C4FD9;
background-color: transparent;
border-bottom-color: #5265F6;
}
"""
def _summary_calendar_icon() -> QIcon:
"""Paint the compact calendar tile from the reference without a font glyph."""
pixmap = QPixmap(34, 34)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(QColor("#EFEEFF"))
painter.drawRoundedRect(0, 0, 34, 34, 9, 9)
pen = QPen(QColor("#5B59F7"), 1.7)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
painter.setPen(pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawRoundedRect(9, 10, 16, 15, 3, 3)
painter.drawLine(9, 15, 25, 15)
painter.drawLine(13, 8, 13, 12)
painter.drawLine(21, 8, 21, 12)
painter.end()
return QIcon(pixmap)
def _style_table_cell(table: SortableTable, row: int, column: int, kind: str) -> None:
item = table.item(row, column)
color = _SEMANTIC_COLORS.get(kind, _SEMANTIC_COLORS["muted"])
@@ -311,15 +484,15 @@ class _LegacyAppointmentDialog(QDialog):
self.setModal(True)
self.resize(540, 560)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setSpacing(12)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
patient = QLabel(display_text(first_value(row, "patient_name", "name", default="患者")))
patient.setProperty("role", "pageTitle")
patient.setProperty("dialogRole", "title")
root.addWidget(patient)
identity = QLabel(
f"诊单 #{display_text(self.diagnosis_id)} · 患者号仅用于视频,不参与本次预约"
)
identity.setProperty("role", "muted")
identity.setProperty("dialogRole", "subtitle")
root.addWidget(identity)
form = QFormLayout()
form.setVerticalSpacing(10)
@@ -362,9 +535,11 @@ class _LegacyAppointmentDialog(QDialog):
self.ok_button.setText("确认预约")
self.ok_button.setProperty("variant", "primary")
self.ok_button.setEnabled(False)
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "LegacyAppointmentDialog")
self._load_initial_options()
@staticmethod
@@ -737,15 +912,21 @@ class _PaymentDialog(QDialog):
def __init__(self, row: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setWindowTitle("补齐支付单")
self.resize(500, 430)
self.resize(520, 450)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
title = QLabel("补齐支付单")
title.setProperty("dialogRole", "title")
root.addWidget(title)
remaining = max(
0.0,
_as_float(first_value(row, "amount"))
- _as_float(first_value(row, "linked_pay_paid_total")),
)
root.addWidget(QLabel(f"待补齐参考金额:{_money(remaining)}"))
amount_hint = QLabel(f"待补齐参考金额:{_money(remaining)}")
amount_hint.setProperty("dialogRole", "subtitle")
root.addWidget(amount_hint)
form = QFormLayout()
self.order_type = QComboBox()
self.order_type.addItem("药品", 3)
@@ -771,10 +952,14 @@ class _PaymentDialog(QDialog):
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok
)
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("确认新增")
primary_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
primary_button.setText("确认新增")
primary_button.setProperty("variant", "primary")
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "PatientPaymentDialog")
def payload(self) -> dict[str, Any]:
return {
@@ -790,9 +975,13 @@ class _RefundDialog(QDialog):
def __init__(self, row: Any, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setWindowTitle("订单退款")
self.resize(500, 340)
self.resize(520, 380)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
title = QLabel("订单退款")
title.setProperty("dialogRole", "title")
root.addWidget(title)
form = QFormLayout()
self.reason = QPlainTextEdit()
self.reason.setMaximumHeight(100)
@@ -813,10 +1002,14 @@ class _RefundDialog(QDialog):
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok
)
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("确认退款")
danger_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
danger_button.setText("确认退款")
danger_button.setProperty("variant", "danger")
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "PatientRefundDialog")
def _toggle_amount(self, checked: bool) -> None:
self.refund_amount.setEnabled(checked)
@@ -841,12 +1034,12 @@ class _AssignDialog(QDialog):
super().__init__(parent)
self.setWindowTitle("指派医助")
self.setModal(True)
self.resize(430, 230)
self.resize(460, 260)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setSpacing(12)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
title = QLabel(display_text(first_value(row, "patient_name", default="患者")))
title.setProperty("role", "sectionTitle")
title.setProperty("dialogRole", "title")
root.addWidget(title)
form = QFormLayout()
self.assistant_combo = QComboBox()
@@ -871,10 +1064,14 @@ class _AssignDialog(QDialog):
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok
)
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("确认指派")
primary_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
primary_button.setText("确认指派")
primary_button.setProperty("variant", "primary")
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "PatientAssignDialog")
@property
def assistant_id(self) -> int:
@@ -888,10 +1085,10 @@ class _OrderDetailDialog(QDialog):
self.setModal(True)
self.resize(620, 520)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setSpacing(12)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
title = QLabel(f"订单 {display_text(first_value(detail, 'order_no', 'id'))}")
title.setProperty("role", "pageTitle")
title.setProperty("dialogRole", "title")
root.addWidget(title)
card = QFrame()
card.setObjectName("SubtleCard")
@@ -921,8 +1118,10 @@ class _OrderDetailDialog(QDialog):
form.addRow(caption, label)
root.addWidget(card, 1)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.button(QDialogButtonBox.StandardButton.Close).setText("关闭")
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "PatientOrderDetailDialog")
class _OrderEditDialog(QDialog):
@@ -933,12 +1132,12 @@ class _OrderEditDialog(QDialog):
self.detail = detail
self.setWindowTitle("编辑订单")
self.setModal(True)
self.resize(580, 560)
self.resize(600, 580)
root = QVBoxLayout(self)
root.setContentsMargins(20, 18, 20, 18)
root.setSpacing(12)
root.setContentsMargins(24, 22, 24, 18)
root.setSpacing(14)
title = QLabel(f"订单 {display_text(first_value(detail, 'order_no', 'id'))}")
title.setProperty("role", "pageTitle")
title.setProperty("dialogRole", "title")
root.addWidget(title)
self.error_label = QLabel()
self.error_label.setProperty("role", "danger")
@@ -1007,10 +1206,14 @@ class _OrderEditDialog(QDialog):
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok
)
buttons.button(QDialogButtonBox.StandardButton.Ok).setText("保存订单")
primary_button = buttons.button(QDialogButtonBox.StandardButton.Ok)
primary_button.setText("保存订单")
primary_button.setProperty("variant", "primary")
buttons.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
root.addWidget(buttons)
mark_business_dialog(self, "PatientOrderEditDialog")
def accept(self) -> None:
if not all(
@@ -1088,8 +1291,8 @@ class PatientListWorkspace(QWidget):
self._setting_dates = False
root = QVBoxLayout(self)
root.setContentsMargins(0, 10, 0, 0)
root.setSpacing(12)
root.setContentsMargins(0, 2, 0, 0)
root.setSpacing(10)
root.addWidget(self._build_filters())
root.addLayout(self._build_summary())
self.banner = MessageBanner()
@@ -1103,15 +1306,22 @@ class PatientListWorkspace(QWidget):
def _build_filters(self) -> QWidget:
card = QFrame()
card.setObjectName("FilterBar")
grid = QGridLayout(card)
grid.setContentsMargins(14, 12, 14, 12)
grid.setHorizontalSpacing(8)
grid.setVerticalSpacing(8)
card.setProperty("patientListFilter", True)
panel = QVBoxLayout(card)
panel.setContentsMargins(18, 18, 18, 12)
panel.setSpacing(10)
top_row = QHBoxLayout()
top_row.setContentsMargins(0, 0, 0, 0)
top_row.setSpacing(0)
self.keyword_edit = QLineEdit()
self.keyword_edit.setPlaceholderText("患者姓名 / 手机号 / 助理 / 医生")
self.keyword_edit.setClearButtonEnabled(True)
self.keyword_edit.setMinimumWidth(300)
self.keyword_edit.setMaximumWidth(605)
self.keyword_edit.returnPressed.connect(self.search)
grid.addWidget(self.keyword_edit, 0, 0, 1, 3)
top_row.addWidget(self.keyword_edit, 1)
top_row.addSpacing(21)
# Compatibility-only control: it is intentionally hidden and never
# inserted into a layout, so it needs an explicit parent to avoid
# becoming a transient top-level Windows HWND during page creation.
@@ -1123,9 +1333,10 @@ class PatientListWorkspace(QWidget):
self.status_combo.addItem("已过号", "missed")
self.status_combo.hide()
status_host = QWidget()
status_host.setFixedWidth(396)
status_row = QHBoxLayout(status_host)
status_row.setContentsMargins(0, 0, 0, 0)
status_row.setSpacing(6)
status_row.setSpacing(0)
self.status_group = QButtonGroup(self)
self.status_group.setExclusive(True)
self.status_buttons: dict[str, QPushButton] = {}
@@ -1140,6 +1351,7 @@ class PatientListWorkspace(QWidget):
button.setCheckable(True)
button.setCursor(Qt.CursorShape.PointingHandCursor)
button.setProperty("variant", "ghost")
button.setProperty("patientStatusChip", True)
button.setProperty(
"statusKind",
{
@@ -1154,22 +1366,32 @@ class PatientListWorkspace(QWidget):
)
self.status_group.addButton(button)
self.status_buttons[value] = button
status_row.addWidget(button)
status_row.addWidget(button, 1)
self.status_buttons[""].setChecked(True)
grid.addWidget(status_host, 0, 3)
top_row.addWidget(status_host)
top_row.addStretch(1)
search = QPushButton("查询")
search.setProperty("variant", "secondary")
search.setProperty("variant", "primary")
search.setProperty("patientSearchAction", True)
search.setFixedWidth(72)
search.clicked.connect(self.search)
grid.addWidget(search, 0, 4)
top_row.addWidget(search)
top_row.addSpacing(12)
reset = QPushButton("重置")
reset.setProperty("variant", "ghost")
reset.setFixedWidth(72)
reset.clicked.connect(self.reset_filters)
grid.addWidget(reset, 0, 5)
top_row.addWidget(reset)
panel.addLayout(top_row)
bottom_row = QHBoxLayout()
bottom_row.setContentsMargins(0, 0, 0, 0)
bottom_row.setSpacing(0)
quick_host = QWidget()
quick_host.setFixedWidth(730)
quick = QHBoxLayout(quick_host)
quick.setContentsMargins(0, 0, 0, 0)
quick.setSpacing(4)
quick.setSpacing(0)
self.quick_group = QButtonGroup(self)
self.quick_group.setExclusive(True)
self.quick_buttons: dict[str, QPushButton] = {}
@@ -1184,30 +1406,47 @@ class PatientListWorkspace(QWidget):
button = QPushButton(label)
button.setCheckable(True)
button.setProperty("variant", "ghost")
button.setProperty("patientQuickDate", True)
button.clicked.connect(lambda _checked=False, value=mode: self.set_date_mode(value))
self.quick_group.addButton(button)
self.quick_buttons[mode] = button
quick.addWidget(button)
quick.addWidget(button, 1)
self.quick_buttons["all"].setChecked(True)
grid.addWidget(quick_host, 1, 0, 1, 3)
bottom_row.addWidget(quick_host)
bottom_row.addSpacing(20)
date_host = QWidget()
date_host.setMinimumWidth(390)
date_host.setMaximumWidth(476)
dates = QHBoxLayout(date_host)
dates.setContentsMargins(0, 0, 0, 0)
dates.setSpacing(10)
self.start_date = QDateEdit(QDate.currentDate())
self.start_date.setCalendarPopup(True)
self.start_date.setDisplayFormat("yyyy-MM-dd")
self.start_date.setEnabled(False)
self.start_date.editingFinished.connect(self._custom_date_changed)
grid.addWidget(self.start_date, 1, 3)
dates.addWidget(self.start_date, 1)
separator = QLabel("~")
separator.setAlignment(Qt.AlignmentFlag.AlignCenter)
separator.setProperty("role", "muted")
separator.setFixedWidth(24)
dates.addWidget(separator)
self.end_date = QDateEdit(QDate.currentDate())
self.end_date.setCalendarPopup(True)
self.end_date.setDisplayFormat("yyyy-MM-dd")
self.end_date.setEnabled(False)
self.end_date.editingFinished.connect(self._custom_date_changed)
grid.addWidget(self.end_date, 1, 4)
dates.addWidget(self.end_date, 1)
bottom_row.addWidget(date_host, 1)
bottom_row.addSpacing(20)
custom = QPushButton("自定义")
custom.setProperty("variant", "ghost")
custom.setFixedWidth(100)
custom.clicked.connect(lambda: self.set_date_mode("custom"))
grid.addWidget(custom, 1, 5)
grid.setColumnStretch(0, 1)
bottom_row.addWidget(custom)
bottom_row.addStretch(1)
panel.addLayout(bottom_row)
return card
def _build_summary(self) -> QHBoxLayout:
@@ -1220,8 +1459,10 @@ class PatientListWorkspace(QWidget):
("day_after", "后天预约", "day_after"),
):
button = QPushButton(f"{label}\n0 人")
button.setProperty("variant", "secondary")
button.setMinimumHeight(58)
button.setProperty("summaryCard", True)
button.setFixedHeight(56)
button.setIcon(_summary_calendar_icon())
button.setIconSize(QSize(34, 34))
button.clicked.connect(lambda _checked=False, value=mode: self.set_date_mode(value))
self.summary_buttons[key] = button
layout.addWidget(button, 1)
@@ -1236,6 +1477,7 @@ class PatientListWorkspace(QWidget):
heading = QHBoxLayout()
title = QLabel("患者列表")
title.setProperty("role", "sectionTitle")
title.setMinimumHeight(20)
heading.addWidget(title)
heading.addStretch(1)
self.scope_label = QLabel(self._scope)
@@ -1249,39 +1491,45 @@ class PatientListWorkspace(QWidget):
host_layout.setSpacing(8)
self.table = SortableTable(
[
TableColumn("_selected", "", 40, alignment=Qt.AlignmentFlag.AlignCenter),
TableColumn(
"patient_name",
"患者",
160,
"患者信息",
180,
lambda _value, row: (
f"{display_text(first_value(row, 'patient_name', 'name'))} · "
f"{gender_text(first_value(row, 'gender_desc', 'gender'))} · "
f"{display_text(first_value(row, 'age'))}"
f"{display_text(first_value(row, 'age'))}\n"
f"{display_text(first_value(row, 'phone_masked', 'phone'), '')}"
),
),
TableColumn("assistant_name", "归属助理", 105),
TableColumn("assistant_name", "归属助理", 84),
TableColumn(
"appointment_doctor_name",
"预约医生",
125,
108,
lambda _value, row: (
f"{display_text(first_value(row, 'appointment_doctor_name'), '未预约')} / "
f"{_patient_status(row)[0]}"
),
),
TableColumn("appointment_time_text", "预约时间", 145),
TableColumn("appointment_time_text", "预约时间", 116),
TableColumn(
"revisit_count",
"复诊",
65,
72,
lambda value, _row: f"{display_text(value, '0')}",
Qt.AlignmentFlag.AlignCenter,
),
TableColumn("confirmation_text", "确认信息", 95),
TableColumn("diagnosis_date_text", "诊单日期", 105),
TableColumn("phone_masked", "手机", 118),
TableColumn("confirmation_text", "确认信息", 88),
TableColumn("diagnosis_date_text", "诊单日期", 96),
TableColumn("phone_masked", "手机", 116),
TableColumn("_actions", "操作", 430),
]
)
self.table.setObjectName("PatientTable")
self.table.horizontalHeader().setFixedHeight(38)
self.table.verticalHeader().setDefaultSectionSize(40)
self.table.itemSelectionChanged.connect(self._update_actions)
self.table.itemDoubleClicked.connect(lambda _item: self._open_selected_diagnosis())
host_layout.addWidget(self.table, 1)
@@ -1294,6 +1542,93 @@ class PatientListWorkspace(QWidget):
layout.addWidget(self.content_stack, 1)
return card
def _install_row_actions(self) -> None:
editable = _canonical_allowed(self.permissions, "tcm.diagnosis/edit")
readable = _canonical_allowed(self.permissions, "tcm.diagnosis/readonlyDetail")
can_book = _canonical_allowed(self.permissions, "tcm.diagnosis/guahao")
can_assign = _canonical_allowed(self.permissions, "tcm.diagnosis/assign")
can_orders = _canonical_allowed(self.permissions, "tcm.prescriptionOrder/lists")
action_column = self.table.columnCount() - 1
for row_index in range(self.table.rowCount()):
source_item = self.table.item(row_index, 0)
row = source_item.data(Qt.ItemDataRole.UserRole) if source_item is not None else None
selector_host = QWidget(self.table)
selector_layout = QHBoxLayout(selector_host)
selector_layout.setContentsMargins(0, 0, 0, 0)
selector_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
selector = QCheckBox(selector_host)
selector.setProperty("patientSelector", True)
selector.setToolTip("选择患者")
selector.clicked.connect(
lambda checked=False, index=row_index: self.table.selectRow(index) if checked else None
)
selector_layout.addWidget(selector)
self.table.setCellWidget(row_index, 0, selector_host)
selector_item = self.table.item(row_index, 0)
if selector_item is not None:
selector_item.setText("")
host = QWidget(self.table)
layout = QHBoxLayout(host)
layout.setContentsMargins(5, 3, 5, 3)
layout.setSpacing(4)
def add_action(
label: str,
callback: Any,
*,
danger: bool = False,
_host: QWidget = host,
_layout: QHBoxLayout = layout,
) -> None:
button = QPushButton(label, _host)
button.setProperty("rowAction", True)
if danger:
button.setProperty("variant", "danger")
button.clicked.connect(callback)
_layout.addWidget(button)
if editable or readable:
add_action(
"诊单" if editable else "查看",
lambda _checked=False, value=row, can_edit=editable: self.diagnosis_requested.emit(
value, can_edit
),
)
if can_book:
add_action(
"预约",
lambda _checked=False, value=row: self.appointment_requested.emit(value),
)
if can_assign:
add_action(
"重新指派" if _as_int(first_value(row, "assistant_id")) > 0 else "指派医助",
lambda _checked=False, value=row: self.assign_requested.emit(value),
)
if editable and not _as_bool(first_value(row, "has_id_card", default=False)):
add_action(
"补全身份证",
lambda _checked=False, value=row: self.fill_id_requested.emit(value),
)
if can_orders:
add_action(
"关联订单",
lambda _checked=False, value=row: self.orders_requested.emit(value),
)
status = _as_int(first_value(row, "appointment_status"), -1)
appointment_id = _as_int(first_value(row, "appointment_id"))
if can_book and appointment_id > 0 and status in {1, 4}:
add_action(
"取消",
lambda _checked=False, value=row: self.cancel_requested.emit(value),
danger=True,
)
layout.addStretch(1)
self.table.setCellWidget(row_index, action_column, host)
action_item = self.table.item(row_index, action_column)
if action_item is not None:
action_item.setText("")
self.table.setRowHeight(row_index, 40)
def _build_actions(self) -> QHBoxLayout:
layout = QHBoxLayout()
layout.setSpacing(6)
@@ -1465,10 +1800,11 @@ class PatientListWorkspace(QWidget):
return
rows = page_items(result)
self.table.set_rows(rows)
self._install_row_actions()
for row_index in range(self.table.rowCount()):
source_item = self.table.item(row_index, 0)
source = source_item.data(Qt.ItemDataRole.UserRole) if source_item is not None else None
_style_table_cell(self.table, row_index, 2, _patient_status(source)[1])
_style_table_cell(self.table, row_index, 3, _patient_status(source)[1])
self.pager.update_state(self._page, page_total(result, len(rows)))
self.content_stack.setCurrentIndex(0 if rows else 1)
extend = _page_extend(result)
@@ -2270,13 +2606,13 @@ class PatientsPage(QWidget):
self._assistant_generation = 0
root = QVBoxLayout(self)
root.setContentsMargins(24, 18, 24, 20)
root.setSpacing(10)
root.setContentsMargins(25, 4, 30, 14)
root.setSpacing(0)
header = PageHeader("我的患者", "患者、挂号与诊单按当前角色和部门数据范围展示。")
self.scope_badge = StatusBadge("按权限加载", "neutral")
header.add_action(self.scope_badge)
refresh = QPushButton("刷新")
refresh.setProperty("variant", "secondary")
refresh.setProperty("variant", "primary")
refresh.clicked.connect(self.refresh)
header.add_action(refresh)
root.addWidget(header)
@@ -2,9 +2,13 @@
from __future__ import annotations
from datetime import datetime
from typing import Any
from PySide6.QtCore import QSize, Qt
from PySide6.QtGui import QColor, QFont
from PySide6.QtWidgets import (
QAbstractItemView,
QComboBox,
QDialog,
QFrame,
@@ -20,11 +24,12 @@ from PySide6.QtWidgets import (
)
from ..dialogs.prescription import PrescriptionTemplateDialog
from ..dialogs.prescription_ai import PrescriptionAiReportDialog, can_open_ai_explain
from ..widgets import (
EmptyState,
MessageBanner,
MetricCard,
PageHeader,
Pager,
SortableTable,
TableColumn,
display_text,
@@ -38,6 +43,129 @@ from ..widgets import (
run_async,
show_toast,
)
from .prescriptions import (
BusinessPager,
_cell_host,
_painted_icon,
_row_action_button,
_style_row_host,
_tag_label,
)
PRESCRIPTION_LIBRARY_PAGE_QSS = """
#PrescriptionLibraryPage { background: #F8FAFF; }
#PrescriptionLibraryPage QWidget#PageHeader { min-height: 84px; max-height: 84px; }
#PrescriptionLibraryPage QWidget#PageHeader QLabel[role="breadcrumb"],
#PrescriptionLibraryPage QWidget#PageHeader QLabel[role="breadcrumbSeparator"],
#PrescriptionLibraryPage QWidget#PageHeader QLabel[role="breadcrumbCurrent"] {
min-height: 16px; max-height: 16px;
}
#PrescriptionLibraryPage QLabel[role="pageTitle"] {
color: #15224A; font-size: 20px; font-weight: 700;
}
#PrescriptionLibraryPage QWidget#PageHeader QLabel[role="muted"] {
color: #7481A3; font-size: 12px; padding-top: 5px;
}
#PrescriptionLibraryPage QFrame#MetricCard {
min-height: 80px; max-height: 80px;
border: 1px solid #E2E7F4; border-radius: 12px; background: #FFFFFF;
}
#PrescriptionLibraryPage QFrame#MetricCard QLabel[role="metricTitle"] {
color: #405074; font-size: 12px; font-weight: 600;
}
#PrescriptionLibraryPage QFrame#MetricCard QLabel[role="metricValue"] {
color: #5265F6; font-size: 22px; font-weight: 700;
}
#PrescriptionLibraryPage QLabel[metricIcon="true"] {
border: 1px solid #DCE3FF; border-radius: 11px; background: #EEF1FF;
}
#PrescriptionLibraryPage QLabel[metricIcon="true"][kind="info"] {
border-color: #E5DFFF; background: #F2EEFF;
}
#PrescriptionLibraryPage QLabel[metricIcon="true"][kind="success"] {
border-color: #CDEFE3; background: #E8F8F2;
}
#PrescriptionLibraryPage QLabel[metricIcon="true"][kind="warning"] {
border-color: #F8DFC2; background: #FFF3E5;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryFilterBar,
#PrescriptionLibraryPage QFrame#PrescriptionLibraryTableCard {
background: #FFFFFF; border: 1px solid #E2E7F4; border-radius: 13px;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryToolbar {
background: #FFFFFF; border: 0; border-bottom: 1px solid #E7EBF5;
border-top-left-radius: 13px; border-top-right-radius: 13px;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryToolbar QPushButton[toolbarTab="true"] {
min-width: 82px; min-height: 36px; max-height: 36px;
padding: 0 8px; margin: 0 4px 0 0;
color: #59698E; background: transparent; border: 0;
border-bottom: 2px solid transparent; border-radius: 0;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryToolbar QPushButton[toolbarTab="true"]:checked {
color: #5265F6; background: transparent; border-bottom-color: #5265F6;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryFilterBar QLineEdit,
#PrescriptionLibraryPage QFrame#PrescriptionLibraryFilterBar QComboBox {
min-height: 36px; max-height: 36px; padding: 0 11px;
border-radius: 8px; font-size: 12px;
}
#PrescriptionLibraryPage QPushButton {
min-height: 34px; max-height: 34px; padding: 0 14px;
border-radius: 8px; font-size: 12px;
}
#PrescriptionLibraryPage QFrame#PrescriptionLibraryToolbar QPushButton {
min-height: 32px; max-height: 32px; padding: 0 13px;
}
#PrescriptionLibraryPage QPushButton[rowAction="true"] {
min-width: 27px; max-width: 27px; min-height: 27px; max-height: 27px;
padding: 0; border-radius: 7px; background: #FFFFFF;
border: 1px solid #DCE3F5;
}
#PrescriptionLibraryPage QPushButton[rowAction="true"]:hover {
background: #F3F5FF; border-color: #AAB7FF;
}
#PrescriptionLibraryPage QPushButton[rowAction="true"][danger="true"] {
background: #FFF9FA; border-color: #FFD9DE;
}
#PrescriptionLibraryPage QPushButton[rowAction="true"][danger="true"]:hover {
background: #FFF1F3; border-color: #FFACB8;
}
#PrescriptionLibraryPage QTableWidget {
border: 0; border-radius: 0; background: #FFFFFF;
alternate-background-color: #FBFCFF; font-size: 12px;
}
#PrescriptionLibraryPage QTableWidget::item {
padding: 4px 8px; border-bottom: 1px solid #EDF0F7;
}
#PrescriptionLibraryPage QTableWidget::item:selected {
color: #26365F; background: #FCFDFF;
}
#PrescriptionLibraryPage QHeaderView::section {
min-height: 38px; max-height: 38px; padding: 0 8px;
background: #F7F9FE; color: #7481A3;
border: 0; border-bottom: 1px solid #E7EBF5;
font-size: 12px; font-weight: 600;
}
#PrescriptionLibraryPage QWidget#Pager { min-height: 42px; }
#PrescriptionLibraryPage QWidget#Pager QPushButton {
min-width: 34px; max-height: 32px; min-height: 32px; padding: 0 10px;
}
#PrescriptionLibraryPage QWidget#Pager QLabel#PagerActive {
min-width: 48px; min-height: 30px; border-radius: 7px;
background: #5265F6; color: #FFFFFF; font-weight: 700;
}
#PrescriptionLibraryPage QWidget#BusinessPager QPushButton[pagerPage="true"] {
min-width: 34px; max-width: 34px; min-height: 32px; max-height: 32px;
padding: 0; background: #FFFFFF; color: #405074; border-color: #E2E7F4;
}
#PrescriptionLibraryPage QWidget#BusinessPager QPushButton[pagerPage="true"][active="true"] {
background: #5265F6; color: #FFFFFF; border-color: #5265F6;
}
#PrescriptionLibraryPage QWidget#BusinessPager QComboBox {
min-height: 32px; max-height: 32px; min-width: 92px; padding: 0 9px;
}
"""
def _formula_text(value: Any, _row: Any = None) -> str:
@@ -70,6 +198,40 @@ def _herbs_detail(_value: Any, row: Any) -> str:
return "".join(pieces) if pieces else "暂无药材"
def _efficacy_text(_value: Any, row: Any) -> str:
return display_text(
first_value(
row,
"efficacy",
"efficacy_text",
"effect",
"effect_text",
"indications",
default="",
)
)
def _metric_card(title: str, kind: str = "accent") -> MetricCard:
card = MetricCard(title, "0", kind=kind, glyph="")
card.setFixedHeight(80)
card.layout().setContentsMargins(18, 12, 16, 12)
icon = QLabel(card)
icon.setProperty("metricIcon", True)
icon.setProperty("kind", kind)
icon.setAlignment(Qt.AlignmentFlag.AlignCenter)
icon.setFixedSize(42, 42)
colors = {
"accent": "#5365F5",
"info": "#8268E8",
"success": "#23A77D",
"warning": "#E6932C",
}
icon.setPixmap(_painted_icon("document", colors.get(kind, "#5365F5"), 18).pixmap(18, 18))
card.layout().addWidget(icon)
return card
class PrescriptionLibraryPage(QWidget):
"""Filter, inspect, and manage reusable prescriptions."""
@@ -81,6 +243,8 @@ class PrescriptionLibraryPage(QWidget):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.setObjectName("PrescriptionLibraryPage")
self.setStyleSheet(PRESCRIPTION_LIBRARY_PAGE_QSS)
self.repository = repository
self.permissions = permissions
self.current_user = current_user
@@ -91,83 +255,155 @@ class PrescriptionLibraryPage(QWidget):
self._page_size = 15
root = QVBoxLayout(self)
root.setContentsMargins(24, 20, 24, 24)
root.setSpacing(15)
root.setContentsMargins(24, 19, 24, 14)
root.setSpacing(12)
header = PageHeader(
"我的处方库",
"管理可复用药材组合;公开模板可被其他医生导入,禁用修改仅作用于导入后的处方。",
"处方库",
"管理常用处方模板,支持 AI 解析辅助开方。",
)
self.new_button = QPushButton(" 新增处方", header)
header.actions.setSpacing(16)
self.new_button = QPushButton("新增处方", header)
self.new_button.setMinimumWidth(124)
self.new_button.setProperty("variant", "primary")
self.new_button.setIcon(_painted_icon("plus", "#FFFFFF", 15))
self.new_button.setIconSize(QSize(15, 15))
self.new_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.new_button.setVisible(has_permission(permissions, "wcf.prescription/add"))
self.new_button.clicked.connect(self._new_template)
header.add_action(self.new_button)
root.addWidget(header)
metrics = QHBoxLayout()
metrics.setContentsMargins(0, 0, 0, 5)
metrics.setSpacing(24)
self.metric_cards = {
"total": _metric_card("全部处方"),
"private": _metric_card("仅自己", "info"),
"public": _metric_card("公开处方", "success"),
"month": _metric_card("本月新增", "warning"),
}
for metric in self.metric_cards.values():
metrics.addWidget(metric)
root.addLayout(metrics)
self.hint_banner = MessageBanner(parent=self)
self.hint_banner.show_message(
"AI 仅依据药材组合推测可能证候与主治方向,不能替代四诊、辨证和处方审核。",
"warning",
)
self.hint_banner.hide()
filters = QFrame()
filters.setObjectName("FilterBar")
filters.setObjectName("PrescriptionLibraryFilterBar")
grid = QGridLayout(filters)
grid.setContentsMargins(16, 13, 16, 13)
grid.setHorizontalSpacing(10)
grid.setContentsMargins(16, 11, 16, 11)
grid.setHorizontalSpacing(20)
self.name_filter = QLineEdit()
self.name_filter.setPlaceholderText("处方名称")
self.name_filter.setPlaceholderText("搜索处方名称、药材、功效等关键词")
self.name_filter.setClearButtonEnabled(True)
self.name_filter.returnPressed.connect(self._search)
grid.addWidget(self.name_filter, 0, 0, 1, 2)
grid.addWidget(self.name_filter, 0, 0)
self.formula_filter = QComboBox()
self.formula_filter.addItem("全部类型", "")
self.formula_filter.addItem("主方", "主方")
self.formula_filter.addItem("辅方", "辅方")
grid.addWidget(self.formula_filter, 0, 2)
grid.addWidget(self.formula_filter, 0, 1)
self.visibility_filter = QComboBox()
self.visibility_filter.addItem("全部公开范围", "")
self.visibility_filter.addItem("仅自己可见", 0)
self.visibility_filter.addItem("所有人可见", 1)
grid.addWidget(self.visibility_filter, 0, 3)
query = QPushButton("查询")
query.setProperty("variant", "secondary")
query.clicked.connect(self._search)
grid.addWidget(query, 0, 4)
reset = QPushButton("重置")
reset.setProperty("variant", "ghost")
reset.clicked.connect(self._reset_filters)
grid.addWidget(reset, 0, 5)
grid.addWidget(self.visibility_filter, 0, 2)
self.effect_filter = QComboBox()
self.effect_filter.addItem("全部功效类型", "")
self.effect_filter.addItem("益气养阴", "益气养阴")
self.effect_filter.addItem("清热祛湿", "清热祛湿")
self.effect_filter.addItem("滋阴补肾", "滋阴补肾")
grid.addWidget(self.effect_filter, 0, 3)
self.name_filter.setMinimumWidth(500)
self.formula_filter.setFixedWidth(190)
self.visibility_filter.setFixedWidth(174)
self.effect_filter.setFixedWidth(190)
self.query_button = QPushButton("查询")
self.query_button.setFixedWidth(66)
self.query_button.setProperty("variant", "secondary")
self.query_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.query_button.clicked.connect(self._search)
grid.addWidget(self.query_button, 0, 4)
self.reset_button = QPushButton("重置")
self.reset_button.setFixedWidth(66)
self.reset_button.setProperty("variant", "ghost")
self.reset_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.reset_button.clicked.connect(self._reset_filters)
grid.addWidget(self.reset_button, 0, 5)
grid.setColumnStretch(0, 1)
root.addWidget(filters)
self.banner = MessageBanner()
root.addWidget(self.banner)
card = QFrame()
card.setObjectName("Card")
card.setObjectName("PrescriptionLibraryTableCard")
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(16, 14, 16, 14)
card_layout.setSpacing(10)
toolbar = QHBoxLayout()
title = QLabel("处方模板")
title.setProperty("role", "sectionTitle")
toolbar.addWidget(title)
card_layout.setContentsMargins(0, 0, 0, 0)
card_layout.setSpacing(0)
toolbar_host = QFrame(card)
toolbar_host.setObjectName("PrescriptionLibraryToolbar")
toolbar = QHBoxLayout(toolbar_host)
toolbar.setContentsMargins(16, 10, 16, 9)
toolbar.setSpacing(8)
self.all_tab = QPushButton("处方列表", toolbar_host)
self.all_tab.setProperty("toolbarTab", True)
self.all_tab.setCheckable(True)
self.all_tab.setChecked(True)
self.all_tab.clicked.connect(lambda: self._set_library_scope(False))
toolbar.addWidget(self.all_tab)
self.favorite_tab = QPushButton("我的收藏", toolbar_host)
self.favorite_tab.setProperty("toolbarTab", True)
self.favorite_tab.setCheckable(True)
self.favorite_tab.clicked.connect(lambda: self._set_library_scope(True))
toolbar.addWidget(self.favorite_tab)
toolbar.addStretch(1)
self.view_button = QPushButton("查看", card)
self.view_button.setIcon(_painted_icon("eye", "#5265F6", 15))
self.view_button.setIconSize(QSize(15, 15))
self.view_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.view_button.setVisible(has_permission(permissions, "wcf.prescription/read"))
self.view_button.setEnabled(False)
self.view_button.clicked.connect(self._view_selected)
toolbar.addWidget(self.view_button)
self.ai_button = QPushButton("AI解释", card)
self.ai_button.setProperty("variant", "secondary")
self.ai_button.setIcon(_painted_icon("spark", "#5265F6", 15))
self.ai_button.setIconSize(QSize(15, 15))
self.ai_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.ai_button.setVisible(can_open_ai_explain(permissions))
self.ai_button.setEnabled(False)
self.ai_button.clicked.connect(self._explain_selected)
toolbar.addWidget(self.ai_button)
self.edit_button = QPushButton("编辑", card)
self.edit_button.setIcon(_painted_icon("pencil", "#5265F6", 15))
self.edit_button.setIconSize(QSize(15, 15))
self.edit_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.edit_button.setVisible(has_permission(permissions, "wcf.prescription/edit"))
self.edit_button.setEnabled(False)
self.edit_button.clicked.connect(self._edit_selected)
toolbar.addWidget(self.edit_button)
self.delete_button = QPushButton("删除", card)
self.delete_button.setProperty("variant", "danger")
self.delete_button.setIcon(_painted_icon("trash", "#F34E64", 15))
self.delete_button.setIconSize(QSize(15, 15))
self.delete_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.delete_button.setVisible(has_permission(permissions, "wcf.prescription/delete"))
self.delete_button.setEnabled(False)
self.delete_button.clicked.connect(self._delete_selected)
toolbar.addWidget(self.delete_button)
refresh = QPushButton("刷新")
refresh.setProperty("variant", "ghost")
refresh.setIcon(_painted_icon("refresh", "#5D6E96", 15))
refresh.setIconSize(QSize(15, 15))
refresh.setCursor(Qt.CursorShape.PointingHandCursor)
refresh.clicked.connect(self.refresh)
toolbar.addWidget(refresh)
card_layout.addLayout(toolbar)
card_layout.addWidget(toolbar_host)
self.stack = QStackedWidget()
table_host = QWidget()
@@ -175,21 +411,26 @@ class PrescriptionLibraryPage(QWidget):
table_layout.setContentsMargins(0, 0, 0, 0)
self.table = SortableTable(
[
TableColumn("id", "ID", 60),
TableColumn("prescription_name", "处方名称", 180),
TableColumn("formula_type", "处方类型", 90, _formula_text),
TableColumn("herbs", "药材数量", 90, _herb_count),
TableColumn("herbs", "药材明细", 300, _herbs_detail),
TableColumn("is_public", "是否公开", 110, _visibility_text),
TableColumn("disable_edit", "禁用修改", 95, _disable_edit_text),
TableColumn("id", "ID", 62),
TableColumn("prescription_name", "处方名称", 162),
TableColumn("formula_type", "处方类型", 88, _formula_text),
TableColumn("herbs", "药材数量", 84, _herb_count),
TableColumn("herbs", "药材组成(部分)", 296, _herbs_detail),
TableColumn("efficacy", "功效主治", 170, _efficacy_text),
TableColumn("is_public", "公开范围", 135, _visibility_text),
TableColumn("creator_name", "创建人", 100),
TableColumn("create_time", "创建时间", 150),
TableColumn("create_time", "创建时间", 178),
TableColumn("__actions__", "操作", 160, lambda _value, _row: ""),
]
)
self.table.verticalHeader().setDefaultSectionSize(37)
self.table.horizontalHeader().setFixedHeight(41)
self.table.setWordWrap(False)
self.table.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.table.itemSelectionChanged.connect(self._selection_changed)
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
table_layout.addWidget(self.table, 1)
self.pager = Pager(self._page_size)
self.pager = BusinessPager(self._page_size)
self.pager.page_changed.connect(self._change_page)
table_layout.addWidget(self.pager)
self.stack.addWidget(table_host)
@@ -208,10 +449,23 @@ class PrescriptionLibraryPage(QWidget):
self._page = 1
self.refresh()
def _set_library_scope(self, favorites_only: bool) -> None:
"""Switch the visual library tab while retaining server-backed rows.
The current repository contract has no favorites discriminator, so the
tab is a presentation-compatible scope until that capability is exposed.
"""
self.all_tab.setChecked(not favorites_only)
self.favorite_tab.setChecked(favorites_only)
self._page = 1
self.refresh()
def _reset_filters(self) -> None:
self.name_filter.clear()
self.formula_filter.setCurrentIndex(0)
self.visibility_filter.setCurrentIndex(0)
self.effect_filter.setCurrentIndex(0)
self._search()
def _change_page(self, page: int) -> None:
@@ -250,12 +504,165 @@ class PrescriptionLibraryPage(QWidget):
if generation != self._generation:
return
rows = page_items(result)
effect = str(self.effect_filter.currentData() or "").strip()
if effect:
rows = [
row
for row in rows
if effect
in str(
first_value(
row,
"efficacy",
"efficacy_text",
"effect",
"effect_text",
"indications",
default="",
)
)
]
self.table.set_rows(rows)
self.pager.update_state(requested_page, page_total(result, len(rows)))
self._decorate_rows(rows)
total = page_total(result, len(rows))
self.pager.update_state(requested_page, total)
public_count = sum(
1 for row in rows if _truthy(first_value(row, "is_public", default=False))
)
month_prefix = datetime.now().strftime("%Y-%m")
month_count = sum(
1
for row in rows
if str(first_value(row, "create_time", "created_at", default="")).startswith(
month_prefix
)
)
self.metric_cards["total"].set_value(total)
self.metric_cards["private"].set_value(max(0, len(rows) - public_count))
self.metric_cards["public"].set_value(public_count)
self.metric_cards["month"].set_value(month_count)
self.stack.setCurrentIndex(0 if rows else 1)
self.banner.clear()
self._selection_changed()
def _decorate_rows(self, rows: list[Any]) -> None:
"""Install compact tags and permission-aware row actions from the comp."""
for row_index, row in enumerate(rows):
id_item = self.table.item(row_index, 0)
if id_item is not None:
id_item.setTextAlignment(
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
)
name_item = self.table.item(row_index, 1)
if name_item is not None:
name_item.setForeground(QColor("#24355F"))
font = name_item.font()
font.setWeight(QFont.Weight.DemiBold)
name_item.setFont(font)
formula = _formula_text(first_value(row, "formula_type", default="主方"))
formula_kind = "success" if formula == "主方" else "accent"
self.table.setCellWidget(
row_index,
2,
_style_row_host(
_cell_host(
_tag_label(formula, formula_kind, self.table.viewport())
),
row_index,
),
)
visibility_host = QWidget(self.table.viewport())
_style_row_host(visibility_host, row_index)
visibility_layout = QHBoxLayout(visibility_host)
visibility_layout.setContentsMargins(8, 0, 6, 0)
visibility_layout.setSpacing(6)
visibility_icon = QLabel(visibility_host)
visibility_icon.setFixedSize(15, 15)
visibility_icon.setPixmap(_painted_icon("lock", "#60709A", 14).pixmap(14, 14))
visibility_layout.addWidget(visibility_icon)
visibility_label = QLabel(
_visibility_text(first_value(row, "is_public", default=0)),
visibility_host,
)
visibility_label.setStyleSheet("color:#3F4F76;background:transparent;border:0;")
visibility_layout.addWidget(visibility_label)
visibility_layout.addStretch(1)
self.table.setCellWidget(row_index, 6, visibility_host)
actions_host = QWidget(self.table.viewport())
_style_row_host(actions_host, row_index)
actions = QHBoxLayout(actions_host)
actions.setContentsMargins(6, 0, 6, 0)
actions.setSpacing(5)
actions.addStretch(1)
if has_permission(self.permissions, "wcf.prescription/read"):
actions.addWidget(
_row_action_button(
"eye",
"查看处方模板",
lambda _checked=False, target=row: self._run_row_action(
target, self._view_selected
),
actions_host,
)
)
if can_open_ai_explain(self.permissions):
actions.addWidget(
_row_action_button(
"spark",
"AI 解释",
lambda _checked=False, target=row: self._run_row_action(
target, self._explain_selected
),
actions_host,
)
)
manageable = self._can_manage_row(row)
if has_permission(self.permissions, "wcf.prescription/edit"):
actions.addWidget(
_row_action_button(
"pencil",
"编辑处方模板",
lambda _checked=False, target=row: self._run_row_action(
target, self._edit_selected
),
actions_host,
enabled=manageable,
)
)
if has_permission(self.permissions, "wcf.prescription/delete"):
actions.addWidget(
_row_action_button(
"trash",
"删除处方模板",
lambda _checked=False, target=row: self._run_row_action(
target, self._delete_selected
),
actions_host,
danger=True,
enabled=manageable,
)
)
actions.addStretch(1)
self.table.setCellWidget(row_index, 9, actions_host)
def _run_row_action(self, row: Any, callback: Any) -> None:
target_id = first_value(row, "id", "template_id", default=None)
for row_index in range(self.table.rowCount()):
item = self.table.item(row_index, 0)
candidate = item.data(Qt.ItemDataRole.UserRole) if item is not None else None
candidate_id = first_value(candidate, "id", "template_id", default=None)
if candidate is row or (
target_id is not None and str(candidate_id) == str(target_id)
):
self.table.selectRow(row_index)
break
callback()
def _load_error(self, error: Exception, generation: int) -> None:
if generation == self._generation:
self.banner.show_message(friendly_error(error), "danger")
@@ -270,6 +677,7 @@ class PrescriptionLibraryPage(QWidget):
def _selection_changed(self) -> None:
row = self.table.current_data()
self.view_button.setEnabled(row is not None)
self.ai_button.setEnabled(row is not None)
manageable = self._can_manage_row(row)
self.edit_button.setEnabled(row is not None and manageable)
self.delete_button.setEnabled(row is not None and manageable)
@@ -310,6 +718,14 @@ class PrescriptionLibraryPage(QWidget):
parent=self,
).exec()
def _explain_selected(self) -> None:
row = self.table.current_data()
if row is None or not can_open_ai_explain(self.permissions):
return
dialog = PrescriptionAiReportDialog(self.repository, self.permissions, parent=self)
dialog.open_for(row)
dialog.exec()
def _edit_selected(self) -> None:
row = self.table.current_data()
if (
@@ -400,6 +816,7 @@ class PrescriptionLibraryPage(QWidget):
self.new_button.setEnabled(enabled)
if not enabled:
self.view_button.setEnabled(False)
self.ai_button.setEnabled(False)
self.edit_button.setEnabled(False)
self.delete_button.setEnabled(False)
else:
@@ -6,8 +6,10 @@ from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
from typing import Any
from PySide6.QtCore import QDateTime, Qt
from PySide6.QtCore import QDateTime, QRectF, QSize, Qt, Signal
from PySide6.QtGui import QColor, QFont, QIcon, QPainter, QPen, QPixmap
from PySide6.QtWidgets import (
QAbstractItemView,
QComboBox,
QDateTimeEdit,
QDialog,
@@ -39,7 +41,6 @@ from ..widgets import (
EmptyState,
MessageBanner,
PageHeader,
Pager,
SortableTable,
TableColumn,
display_text,
@@ -54,6 +55,308 @@ from ..widgets import (
show_toast,
)
PRESCRIPTIONS_PAGE_QSS = """
#PrescriptionsPage { background: #F8FAFF; }
#PrescriptionsPage QWidget#PageHeader { min-height: 84px; max-height: 84px; }
#PrescriptionsPage QWidget#PageHeader QLabel[role="breadcrumb"],
#PrescriptionsPage QWidget#PageHeader QLabel[role="breadcrumbSeparator"],
#PrescriptionsPage QWidget#PageHeader QLabel[role="breadcrumbCurrent"] {
min-height: 16px; max-height: 16px;
}
#PrescriptionsPage QLabel[role="pageTitle"] {
color: #15224A; font-size: 20px; font-weight: 700;
}
#PrescriptionsPage QWidget#PageHeader QLabel[role="muted"] {
color: #7481A3; font-size: 12px; padding-top: 5px;
}
#PrescriptionsPage QFrame#PrescriptionFilterBar,
#PrescriptionsPage QFrame#PrescriptionTableCard {
background: #FFFFFF;
border: 1px solid #E2E7F4;
border-radius: 13px;
}
#PrescriptionsPage QFrame#PrescriptionToolbar {
background: #FFFFFF;
border: 0;
border-bottom: 1px solid #E7EBF5;
border-top-left-radius: 13px;
border-top-right-radius: 13px;
}
#PrescriptionsPage QFrame#PrescriptionFilterBar QLineEdit,
#PrescriptionsPage QFrame#PrescriptionFilterBar QComboBox,
#PrescriptionsPage QFrame#PrescriptionFilterBar QDateTimeEdit {
min-height: 34px; max-height: 34px; padding: 0 11px;
border-radius: 8px; font-size: 12px;
}
#PrescriptionsPage QPushButton {
min-height: 34px; max-height: 34px; padding: 0 14px;
border-radius: 8px; font-size: 12px;
}
#PrescriptionsPage QFrame#PrescriptionToolbar QPushButton {
min-height: 32px; max-height: 32px; padding: 0 13px;
}
#PrescriptionsPage QLabel#PrescriptionCountBadge {
min-height: 22px; max-height: 22px; padding: 0 9px;
color: #5265F6; background: #F0F2FF; border-radius: 11px;
font-size: 11px; font-weight: 600;
}
#PrescriptionsPage QPushButton[rowAction="true"] {
min-width: 28px; max-width: 28px; min-height: 28px; max-height: 28px;
padding: 0; border-radius: 7px; background: #FFFFFF;
border: 1px solid #DCE3F5;
}
#PrescriptionsPage QPushButton[rowAction="true"]:hover {
background: #F3F5FF; border-color: #AAB7FF;
}
#PrescriptionsPage QPushButton[rowAction="true"][danger="true"] {
background: #FFF9FA; border-color: #FFD9DE;
}
#PrescriptionsPage QPushButton[rowAction="true"][danger="true"]:hover {
background: #FFF1F3; border-color: #FFACB8;
}
#PrescriptionsPage QTableWidget {
border: 0; border-radius: 0; background: #FFFFFF;
alternate-background-color: #FBFCFF; font-size: 12px;
}
#PrescriptionsPage QTableWidget::item {
padding: 4px 8px; border-bottom: 1px solid #EDF0F7;
}
#PrescriptionsPage QTableWidget::item:selected {
color: #26365F; background: #FCFDFF;
}
#PrescriptionsPage QHeaderView::section {
min-height: 38px; max-height: 38px; padding: 0 8px;
background: #F7F9FE; color: #7481A3;
border: 0; border-bottom: 1px solid #E7EBF5;
font-size: 12px; font-weight: 600;
}
#PrescriptionsPage QWidget#Pager { min-height: 42px; }
#PrescriptionsPage QWidget#Pager QPushButton {
min-width: 34px; max-height: 32px; min-height: 32px; padding: 0 10px;
}
#PrescriptionsPage QWidget#Pager QLabel#PagerActive {
min-width: 48px; min-height: 30px; border-radius: 7px;
background: #5265F6; color: #FFFFFF; font-weight: 700;
}
#PrescriptionsPage QWidget#BusinessPager QPushButton[pagerPage="true"] {
min-width: 34px; max-width: 34px; min-height: 32px; max-height: 32px;
padding: 0; background: #FFFFFF; color: #405074; border-color: #E2E7F4;
}
#PrescriptionsPage QWidget#BusinessPager QPushButton[pagerPage="true"][active="true"] {
background: #5265F6; color: #FFFFFF; border-color: #5265F6;
}
#PrescriptionsPage QWidget#BusinessPager QComboBox {
min-height: 32px; max-height: 32px; min-width: 92px; padding: 0 9px;
}
"""
def _painted_icon(kind: str, color: str = "#5265F6", size: int = 16) -> QIcon:
"""Return a crisp page-local icon without relying on emoji or icon fonts."""
pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
pen = QPen(QColor(color), max(1.25, size / 11.5))
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
painter.setPen(pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
scale = size / 16.0
if kind == "eye":
painter.drawEllipse(QRectF(6.0 * scale, 6.0 * scale, 4.0 * scale, 4.0 * scale))
painter.drawArc(QRectF(1.8 * scale, 3.3 * scale, 12.4 * scale, 9.4 * scale), 18 * 16, 144 * 16)
painter.drawArc(QRectF(1.8 * scale, 3.3 * scale, 12.4 * scale, 9.4 * scale), 198 * 16, 144 * 16)
elif kind == "pencil":
painter.drawLine(4.0 * scale, 12.0 * scale, 11.8 * scale, 4.2 * scale)
painter.drawLine(5.7 * scale, 13.0 * scale, 13.0 * scale, 5.7 * scale)
painter.drawLine(4.0 * scale, 12.0 * scale, 3.2 * scale, 14.0 * scale)
painter.drawLine(3.2 * scale, 14.0 * scale, 5.7 * scale, 13.0 * scale)
painter.drawLine(11.8 * scale, 4.2 * scale, 13.0 * scale, 5.7 * scale)
elif kind == "trash":
painter.drawRoundedRect(QRectF(4.4 * scale, 5.2 * scale, 7.2 * scale, 8.2 * scale), 1, 1)
painter.drawLine(3.3 * scale, 4.1 * scale, 12.7 * scale, 4.1 * scale)
painter.drawLine(6.2 * scale, 2.4 * scale, 9.8 * scale, 2.4 * scale)
painter.drawLine(6.7 * scale, 7.2 * scale, 6.7 * scale, 11.2 * scale)
painter.drawLine(9.3 * scale, 7.2 * scale, 9.3 * scale, 11.2 * scale)
elif kind == "refresh":
painter.drawArc(QRectF(2.5 * scale, 2.5 * scale, 11.0 * scale, 11.0 * scale), 35 * 16, 276 * 16)
painter.drawLine(11.5 * scale, 2.7 * scale, 13.5 * scale, 3.0 * scale)
painter.drawLine(11.5 * scale, 2.7 * scale, 12.1 * scale, 4.8 * scale)
elif kind == "spark":
painter.drawLine(8.0 * scale, 1.8 * scale, 8.0 * scale, 5.0 * scale)
painter.drawLine(8.0 * scale, 11.0 * scale, 8.0 * scale, 14.2 * scale)
painter.drawLine(1.8 * scale, 8.0 * scale, 5.0 * scale, 8.0 * scale)
painter.drawLine(11.0 * scale, 8.0 * scale, 14.2 * scale, 8.0 * scale)
painter.drawLine(4.0 * scale, 4.0 * scale, 6.0 * scale, 6.0 * scale)
painter.drawLine(10.0 * scale, 10.0 * scale, 12.0 * scale, 12.0 * scale)
painter.drawLine(12.0 * scale, 4.0 * scale, 10.0 * scale, 6.0 * scale)
painter.drawLine(6.0 * scale, 10.0 * scale, 4.0 * scale, 12.0 * scale)
painter.drawEllipse(QRectF(6.2 * scale, 6.2 * scale, 3.6 * scale, 3.6 * scale))
elif kind == "plus":
painter.drawLine(8.0 * scale, 3.2 * scale, 8.0 * scale, 12.8 * scale)
painter.drawLine(3.2 * scale, 8.0 * scale, 12.8 * scale, 8.0 * scale)
elif kind == "lock":
painter.drawRoundedRect(QRectF(3.4 * scale, 7.0 * scale, 9.2 * scale, 7.0 * scale), 1.4, 1.4)
painter.drawArc(QRectF(5.0 * scale, 2.4 * scale, 6.0 * scale, 8.0 * scale), 0, 180 * 16)
elif kind == "checkbox":
painter.drawRoundedRect(QRectF(3.2 * scale, 3.2 * scale, 9.6 * scale, 9.6 * scale), 1.6, 1.6)
elif kind == "document":
painter.drawRoundedRect(QRectF(3.4 * scale, 2.0 * scale, 9.2 * scale, 12.0 * scale), 1.2, 1.2)
painter.drawLine(5.4 * scale, 6.0 * scale, 10.6 * scale, 6.0 * scale)
painter.drawLine(5.4 * scale, 8.5 * scale, 10.6 * scale, 8.5 * scale)
painter.drawLine(5.4 * scale, 11.0 * scale, 8.8 * scale, 11.0 * scale)
painter.end()
return QIcon(pixmap)
def _tag_label(text: str, kind: str, parent: QWidget) -> QLabel:
palettes = {
"accent": ("#315CF4", "#EDF2FF"),
"success": ("#16966F", "#E7F8F1"),
"warning": ("#D98A16", "#FFF5DF"),
"danger": ("#E84B5F", "#FFF0F2"),
"neutral": ("#657397", "#F1F3F8"),
}
foreground, background = palettes.get(kind, palettes["neutral"])
label = QLabel(text, parent)
label.setAlignment(Qt.AlignmentFlag.AlignCenter)
label.setStyleSheet(
f"color:{foreground};background:{background};border:0;border-radius:4px;"
"padding:2px 7px;font-size:11px;font-weight:600;"
)
label.setSizePolicy(label.sizePolicy().horizontalPolicy(), label.sizePolicy().verticalPolicy())
return label
def _cell_host(widget: QWidget, *, alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignLeft) -> QWidget:
host = QWidget(widget.parentWidget())
layout = QHBoxLayout(host)
layout.setContentsMargins(8, 0, 8, 0)
layout.setSpacing(6)
if alignment & Qt.AlignmentFlag.AlignHCenter:
layout.addStretch(1)
layout.addWidget(widget)
layout.addStretch(1)
return host
def _style_row_host(host: QWidget, row_index: int) -> QWidget:
host.setObjectName("PrescriptionTableCellHost")
host.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
host.setStyleSheet(
"QWidget#PrescriptionTableCellHost {"
f"background:{'#FFFFFF' if row_index % 2 == 0 else '#FBFCFF'};border:0;"
"}"
)
return host
def _row_action_button(
kind: str,
tooltip: str,
callback: Callable[[], None],
parent: QWidget,
*,
danger: bool = False,
enabled: bool = True,
) -> QPushButton:
button = QPushButton(parent)
button.setProperty("rowAction", True)
button.setProperty("danger", danger)
button.setAccessibleName(tooltip)
button.setToolTip(tooltip)
button.setCursor(Qt.CursorShape.PointingHandCursor)
button.setIcon(_painted_icon(kind, "#F34E64" if danger else "#4965F5", 15))
button.setIconSize(QSize(15, 15))
button.setEnabled(enabled)
button.clicked.connect(callback)
return button
class BusinessPager(QWidget):
"""Reference-style numbered pager while retaining the page's fixed size contract."""
page_changed = Signal(int)
def __init__(self, page_size: int = 15, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setObjectName("BusinessPager")
self.page = 1
self.page_size = page_size
self.total = 0
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 6, 0, 0)
layout.setSpacing(7)
self.summary = QLabel("共 0 条", self)
self.summary.setProperty("role", "muted")
layout.addWidget(self.summary)
layout.addStretch(1)
self.previous = QPushButton("", self)
self.previous.setProperty("variant", "ghost")
self.previous.clicked.connect(lambda: self._request(self.page - 1))
layout.addWidget(self.previous)
self.pages_host = QWidget(self)
self.pages_layout = QHBoxLayout(self.pages_host)
self.pages_layout.setContentsMargins(0, 0, 0, 0)
self.pages_layout.setSpacing(5)
layout.addWidget(self.pages_host)
self.next = QPushButton("", self)
self.next.setProperty("variant", "ghost")
self.next.clicked.connect(lambda: self._request(self.page + 1))
layout.addWidget(self.next)
self.size_combo = QComboBox(self)
self.size_combo.addItem(f"{page_size} 条/页", page_size)
layout.addWidget(self.size_combo)
self.page_label: QPushButton | None = None
self.update_state(1, 0)
@property
def page_count(self) -> int:
return max(1, (self.total + self.page_size - 1) // self.page_size)
def update_state(self, page: int, total: int) -> None:
self.page = max(1, page)
self.total = max(0, total)
self.summary.setText(f"{self.total}")
while self.pages_layout.count():
item = self.pages_layout.takeAt(0)
if item.widget() is not None:
item.widget().deleteLater()
count = self.page_count
if count <= 4:
pages: list[int | None] = list(range(1, count + 1))
elif self.page <= 3:
pages = [1, 2, 3, None, count]
elif self.page >= count - 2:
pages = [1, None, count - 2, count - 1, count]
else:
pages = [1, None, self.page, None, count]
self.page_label = None
for number in pages:
if number is None:
ellipsis = QLabel("", self.pages_host)
ellipsis.setAlignment(Qt.AlignmentFlag.AlignCenter)
ellipsis.setFixedWidth(24)
self.pages_layout.addWidget(ellipsis)
continue
button = QPushButton(str(number), self.pages_host)
button.setProperty("pagerPage", True)
button.setProperty("active", number == self.page)
button.setCursor(Qt.CursorShape.PointingHandCursor)
button.clicked.connect(lambda _checked=False, value=number: self._request(value))
self.pages_layout.addWidget(button)
if number == self.page:
self.page_label = button
self.previous.setEnabled(self.page > 1)
self.next.setEnabled(self.page < count)
def _request(self, page: int) -> None:
if 1 <= page <= self.page_count and page != self.page:
self.page_changed.emit(page)
def _int(value: Any, default: int = 0) -> int:
try:
@@ -77,11 +380,7 @@ def _row_mapping(value: Any) -> dict[str, Any]:
if isinstance(raw, Mapping) and raw:
return dict(raw)
fields = getattr(value, "__dataclass_fields__", {})
return {
name: getattr(value, name, None)
for name in fields
if name != "raw"
}
return {name: getattr(value, name, None) for name in fields if name != "raw"}
def merge_prescription_detail(row: Any, detail: Any) -> dict[str, Any]:
@@ -166,18 +465,14 @@ def _order_warnings(row: Any) -> list[str]:
def _sn_cell(_value: Any, row: Any) -> str:
sn = first_value(row, "sn", "prescription_no", "id", default="")
prescription_id = first_value(row, "id", "prescription_id", default="")
warnings = _order_warnings(row)
suffix = "\n" + "\n".join(warnings) if warnings else ""
return f"{sn}\nID: {prescription_id}{suffix}"
return str(first_value(row, "sn", "prescription_no", "id", default=""))
def _patient_cell(_value: Any, row: Any) -> str:
gender = first_value(row, "gender", default=None)
gender_text = "" if gender in (1, "1") else "" if gender in (0, "0") else "未知"
age = first_value(row, "age", default="")
return f"{first_value(row, 'patient_name', default='')}\n{gender_text} · {age}"
return f"{first_value(row, 'patient_name', default='')} · {gender_text} · {age}"
def _source_cell(_value: Any, row: Any) -> str:
@@ -208,10 +503,7 @@ def _void_cell(_value: Any, row: Any) -> str:
def _doctor_cell(_value: Any, row: Any) -> str:
return (
f"{first_value(row, 'doctor_name', 'creator_name', default='')}\n"
f"{first_value(row, 'prescription_date', default='')}"
)
return str(first_value(row, "doctor_name", "creator_name", default=""))
class DoctorMultiSelect(QWidget):
@@ -292,6 +584,8 @@ class PrescriptionsPage(QWidget):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.setObjectName("PrescriptionsPage")
self.setStyleSheet(PRESCRIPTIONS_PAGE_QSS)
self.repository = repository
self.permissions = permissions
self.current_user = current_user
@@ -307,18 +601,27 @@ class PrescriptionsPage(QWidget):
self._mutation_pending = False
root = QVBoxLayout(self)
root.setContentsMargins(24, 20, 24, 24)
root.setSpacing(14)
# Reference business screens place the heading 32 px below the 70 px
# shell bar; keep that baseline shared with the library screen.
root.setContentsMargins(24, 19, 24, 14)
root.setSpacing(12)
header = PageHeader(
"已开处方",
"管理处方审核、患者修正与履约订单;已通过且未作废的处方只允许查看。",
)
header.actions.setSpacing(16)
self.orders_button = QPushButton("业务订单", header)
self.orders_button.setMinimumWidth(86)
self.orders_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.orders_button.setVisible(has_permission(permissions, "tcm.prescriptionOrder/lists"))
self.orders_button.clicked.connect(lambda: self._open_orders())
header.add_action(self.orders_button)
self.add_button = QPushButton(" 新增处方", header)
self.add_button = QPushButton("新增处方", header)
self.add_button.setMinimumWidth(122)
self.add_button.setProperty("variant", "primary")
self.add_button.setIcon(_painted_icon("plus", "#FFFFFF", 15))
self.add_button.setIconSize(QSize(15, 15))
self.add_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.add_button.setVisible(has_permission(permissions, "cf.prescription/add"))
self.add_button.clicked.connect(self._add_prescription)
header.add_action(self.add_button)
@@ -331,11 +634,11 @@ class PrescriptionsPage(QWidget):
def _build_filters(self) -> QWidget:
frame = QFrame()
frame.setObjectName("FilterBar")
frame.setObjectName("PrescriptionFilterBar")
grid = QGridLayout(frame)
grid.setContentsMargins(14, 12, 14, 12)
grid.setHorizontalSpacing(9)
grid.setVerticalSpacing(8)
grid.setContentsMargins(14, 17, 14, 17)
grid.setHorizontalSpacing(18)
grid.setVerticalSpacing(15)
self.quick_date = QComboBox()
self.quick_date.addItem("全部时间", "all")
self.quick_date.addItem("今日", "today")
@@ -366,7 +669,7 @@ class PrescriptionsPage(QWidget):
self.source_filter.addItem("全部来源", "all")
self.source_filter.addItem("手工", "manual")
self.source_filter.addItem("空白处方", "system")
grid.addWidget(self.source_filter, 0, 4)
grid.addWidget(self.source_filter, 0, 4, 1, 2)
self.sn_filter = QLineEdit()
self.sn_filter.setPlaceholderText("处方编号")
self.sn_filter.setClearButtonEnabled(True)
@@ -382,34 +685,47 @@ class PrescriptionsPage(QWidget):
current_name = str(first_value(self.current_user, "name", "real_name", default="")).strip()
if current_id and current_name:
self.doctor_filter.update_options([{"id": current_id, "name": current_name}])
grid.addWidget(self.doctor_filter, 1, 2)
query = QPushButton("查询")
query.setProperty("variant", "secondary")
query.clicked.connect(self._search)
grid.addWidget(query, 1, 3)
reset = QPushButton("重置")
reset.setProperty("variant", "ghost")
reset.clicked.connect(self._reset_filters)
grid.addWidget(reset, 1, 4)
grid.setColumnStretch(1, 1)
grid.setColumnStretch(2, 1)
grid.addWidget(self.doctor_filter, 1, 2, 1, 2)
self.query_button = QPushButton("查询")
self.query_button.setProperty("variant", "secondary")
self.query_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.query_button.clicked.connect(self._search)
grid.addWidget(self.query_button, 1, 4)
self.reset_button = QPushButton("重置")
self.reset_button.setProperty("variant", "ghost")
self.reset_button.setCursor(Qt.CursorShape.PointingHandCursor)
self.reset_button.clicked.connect(self._reset_filters)
grid.addWidget(self.reset_button, 1, 5)
grid.setColumnMinimumWidth(0, 218)
grid.setColumnMinimumWidth(4, 92)
grid.setColumnMinimumWidth(5, 92)
grid.setColumnStretch(1, 383)
grid.setColumnStretch(2, 320)
grid.setColumnStretch(3, 207)
return frame
def _build_table_card(self) -> QWidget:
card = QFrame()
card.setObjectName("Card")
card.setObjectName("PrescriptionTableCard")
layout = QVBoxLayout(card)
layout.setContentsMargins(14, 13, 14, 13)
layout.setSpacing(9)
toolbar = QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
toolbar_host = QFrame(card)
toolbar_host.setObjectName("PrescriptionToolbar")
toolbar = QHBoxLayout(toolbar_host)
toolbar.setContentsMargins(16, 11, 16, 11)
toolbar.setSpacing(8)
title = QLabel("处方列表")
title.setProperty("role", "sectionTitle")
toolbar.addWidget(title)
self.count_badge = QLabel("共 0 条", toolbar_host)
self.count_badge.setObjectName("PrescriptionCountBadge")
toolbar.addWidget(self.count_badge)
toolbar.addStretch(1)
self.view_button = self._action_button("查看", "cf.prescription/read", self._view_selected)
toolbar.addWidget(self.view_button)
self.patch_button = self._action_button(
"患者", "tcm.prescription/patchPatient", self._patch_selected
"患者", "tcm.prescription/patchPatient", self._patch_selected
)
toolbar.addWidget(self.patch_button)
self.create_order_button = self._action_button(
@@ -428,30 +744,42 @@ class PrescriptionsPage(QWidget):
toolbar.addWidget(self.delete_button)
refresh = QPushButton("刷新")
refresh.setProperty("variant", "ghost")
refresh.setCursor(Qt.CursorShape.PointingHandCursor)
refresh.setIcon(_painted_icon("refresh", "#5D6E96", 15))
refresh.setIconSize(QSize(15, 15))
refresh.clicked.connect(self.refresh)
toolbar.addWidget(refresh)
layout.addLayout(toolbar)
layout.addWidget(toolbar_host)
self.stack = QStackedWidget()
table_host = QWidget()
table_layout = QVBoxLayout(table_host)
table_layout.setContentsMargins(0, 0, 0, 0)
self.table = SortableTable(
[
TableColumn("sn", "处方编号", 190, _sn_cell),
TableColumn("prescription_type", "处方类型", 95),
TableColumn("is_system_auto", "来源", 90, _source_cell),
TableColumn("patient_name", "患者信息", 120, _patient_cell),
TableColumn("audit_status", "审核状态", 220, _audit_cell),
TableColumn("void_status", "作废", 70, _void_cell),
TableColumn("doctor_name", "医生信息", 130, _doctor_cell),
TableColumn("assistant_name", "", 90),
TableColumn("create_time", "创建时间", 145),
TableColumn("__selected__", "", 46, lambda _value, _row: ""),
TableColumn("sn", "处方编号", 174, _sn_cell),
TableColumn("prescription_type", "处方类型", 96),
TableColumn("is_system_auto", "来源", 88, _source_cell),
TableColumn("patient_name", "患者信息", 190, _patient_cell),
TableColumn("audit_status", "审核状态", 110, _audit_cell),
TableColumn("void_status", "作废", 72, _void_cell),
TableColumn("doctor_name", "生信息", 180, _doctor_cell),
TableColumn("assistant_name", "医助", 125),
TableColumn("create_time", "创建时间", 180),
TableColumn("__actions__", "操作", 150, lambda _value, _row: ""),
]
)
self.table.verticalHeader().setDefaultSectionSize(36)
self.table.horizontalHeader().setFixedHeight(38)
self.table.setWordWrap(False)
self.table.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.table.horizontalHeaderItem(0).setIcon(_painted_icon("checkbox", "#AEB9D4", 14))
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
self.table.itemSelectionChanged.connect(self._selection_changed)
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
table_layout.addWidget(self.table, 1)
self.pager = Pager(self._page_size)
self.pager = BusinessPager(self._page_size)
self.pager.summary.hide()
self.pager.page_changed.connect(self._change_page)
table_layout.addWidget(self.pager)
self.stack.addWidget(table_host)
@@ -472,6 +800,17 @@ class PrescriptionsPage(QWidget):
# Keep permission-driven initial visibility inside the page hierarchy;
# otherwise Windows briefly exposes the button as its own window.
button = QPushButton(text, self)
button.setCursor(Qt.CursorShape.PointingHandCursor)
icon_kind = {
"查看": "eye",
"编辑": "pencil",
"删除": "trash",
}.get(text)
if icon_kind:
button.setIcon(
_painted_icon(icon_kind, "#F34E64" if danger else "#5265F6", 15)
)
button.setIconSize(QSize(15, 15))
if danger:
button.setProperty("variant", "danger")
button.setVisible(has_permission(self.permissions, permission))
@@ -578,7 +917,10 @@ class PrescriptionsPage(QWidget):
return
rows = page_items(result)
self.table.set_rows(rows)
self.pager.update_state(requested_page, page_total(result, len(rows)))
self._decorate_rows(rows)
total = page_total(result, len(rows))
self.pager.update_state(requested_page, total)
self.count_badge.setText(f"{total}")
self.stack.setCurrentIndex(0 if rows else 1)
doctor_rows = []
for row in rows:
@@ -599,6 +941,130 @@ class PrescriptionsPage(QWidget):
self.table.selectRow(0)
self._selection_changed()
def _decorate_rows(self, rows: list[Any]) -> None:
"""Apply the reference table's tags, checkbox, avatar, and row actions."""
for row_index, row in enumerate(rows):
selector = self.table.item(row_index, 0)
if selector is not None:
selector.setFlags(
selector.flags()
| Qt.ItemFlag.ItemIsUserCheckable
| Qt.ItemFlag.ItemIsEnabled
| Qt.ItemFlag.ItemIsSelectable
)
selector.setCheckState(Qt.CheckState.Unchecked)
selector.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
sn_item = self.table.item(row_index, 1)
if sn_item is not None:
sn_item.setForeground(QColor("#315CF4"))
font = sn_item.font()
font.setWeight(QFont.Weight.DemiBold)
sn_item.setFont(font)
warnings = _order_warnings(row)
if warnings:
sn_item.setToolTip("\n".join(warnings))
prescription_type = display_text(
first_value(row, "prescription_type", default="")
)
self.table.setCellWidget(
row_index,
2,
_style_row_host(
_cell_host(_tag_label(prescription_type, "accent", self.table.viewport())),
row_index,
),
)
audit_text, audit_kind = prescription_status(row)
audit = _tag_label(audit_text, audit_kind, self.table.viewport())
audit_item = self.table.item(row_index, 5)
if audit_item is not None and "\n" in audit_item.text():
audit.setToolTip(audit_item.text())
self.table.setCellWidget(
row_index, 5, _style_row_host(_cell_host(audit), row_index)
)
doctor_name = _doctor_cell(None, row)
doctor_host = QWidget(self.table.viewport())
_style_row_host(doctor_host, row_index)
doctor_layout = QHBoxLayout(doctor_host)
doctor_layout.setContentsMargins(8, 0, 6, 0)
doctor_layout.setSpacing(7)
avatar = QLabel(doctor_name[:1] if doctor_name != "" else "", doctor_host)
avatar.setAlignment(Qt.AlignmentFlag.AlignCenter)
avatar.setFixedSize(20, 20)
avatar.setStyleSheet(
"color:#5365D9;background:#EEF1FF;border:1px solid #DBE1FF;"
"border-radius:10px;font-size:10px;font-weight:700;"
)
doctor_layout.addWidget(avatar)
doctor_label = QLabel(doctor_name, doctor_host)
doctor_label.setStyleSheet("color:#31416A;background:transparent;border:0;")
doctor_layout.addWidget(doctor_label)
doctor_layout.addStretch(1)
self.table.setCellWidget(row_index, 7, doctor_host)
actions_host = QWidget(self.table.viewport())
_style_row_host(actions_host, row_index)
actions = QHBoxLayout(actions_host)
actions.setContentsMargins(7, 0, 7, 0)
actions.setSpacing(7)
actions.addStretch(1)
if has_permission(self.permissions, "cf.prescription/read"):
actions.addWidget(
_row_action_button(
"eye",
"查看处方",
lambda _checked=False, target=row: self._run_row_action(
target, self._view_selected
),
actions_host,
)
)
if has_permission(self.permissions, "cf.prescription/edit"):
actions.addWidget(
_row_action_button(
"pencil",
"编辑处方",
lambda _checked=False, target=row: self._run_row_action(
target, self._edit_selected
),
actions_host,
enabled=can_edit_or_delete(row),
)
)
if has_permission(self.permissions, "cf.prescription/del"):
actions.addWidget(
_row_action_button(
"trash",
"删除处方",
lambda _checked=False, target=row: self._run_row_action(
target, self._delete_selected
),
actions_host,
danger=True,
enabled=can_edit_or_delete(row),
)
)
actions.addStretch(1)
self.table.setCellWidget(row_index, 10, actions_host)
def _run_row_action(self, row: Any, callback: Callable[[], None]) -> None:
target_id = first_value(row, "id", "prescription_id", default=None)
for row_index in range(self.table.rowCount()):
item = self.table.item(row_index, 0)
candidate = item.data(Qt.ItemDataRole.UserRole) if item is not None else None
candidate_id = first_value(candidate, "id", "prescription_id", default=None)
if candidate is row or (
target_id is not None and str(candidate_id) == str(target_id)
):
self.table.selectRow(row_index)
break
callback()
def _load_error(self, error: Exception, generation: int) -> None:
if generation == self._generation:
self.banner.show_message(friendly_error(error), "danger")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+363 -53
View File
@@ -1,55 +1,68 @@
"""Application-wide light desktop theme.
"""Application-wide visual system for the AI consultation workstation.
The default workstation palette uses white data surfaces, cool neutral canvas
tones and a restrained indigo accent. Dense medical content stays opaque and
high-contrast while borders and focus states preserve the desktop hierarchy.
The palette and density follow the supplied product references: a quiet blue
canvas, crisp white data surfaces, luminous indigo actions and compact tables.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from string import Template
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QEvent, QObject, Qt
from PySide6.QtGui import QColor, QFont, QFontDatabase, QPalette
from PySide6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QFileDialog,
QInputDialog,
QMessageBox,
)
# Canonical semantic tokens. The legacy teal/ink aliases remain available to
# callers while the stylesheet itself is generated from this mapping.
COLORS = {
"canvas": "#F5F7FB",
"canvas_mid": "#F8F9FC",
"canvas_glow": "#EEF2FF",
# Sampled from the supplied 1710 x 920 product comps. The window edge is
# the only blue-tinted surface; the application workspace itself is an
# almost-white #FCFDFE field.
"canvas": "#EEF3FD",
"canvas_mid": "#F7F9FE",
"canvas_glow": "#E5ECFD",
"surface": "#FFFFFF",
"surface_alt": "#F7F8FC",
"raised": "#EEF2F8",
"glass": "rgba(255, 255, 255, 248)",
"glass_alt": "rgba(247, 248, 252, 250)",
"line": "#D8DEEA",
"line_soft": "rgba(79, 99, 217, 52)",
"text": "#172033",
"text_soft": "#34415A",
"muted": "#667085",
"disabled_surface": "#ECEFF5",
"disabled_text": "#98A2B3",
"indigo": "#4F63D9",
"indigo_hover": "#4053C7",
"indigo_pressed": "#3446AF",
"indigo_pale": "#E9EDFF",
"focus": "#8795F5",
"selection": "#DCE3FF",
"success": "#16876C",
"success_pale": "#E8F6F1",
"warning": "#9A6813",
"warning_pale": "#FFF4D8",
"danger": "#C43E55",
"danger_pale": "#FDECEF",
"info": "#2F6EDB",
"info_pale": "#EAF2FF",
"surface_alt": "#FAFBFE",
"raised": "#F5F7FC",
"glass": "rgba(255, 255, 255, 252)",
"glass_alt": "rgba(248, 250, 255, 252)",
"line": "#E6EAF5",
"line_soft": "rgba(82, 97, 246, 40)",
"text": "#111F46",
"text_soft": "#3F4E75",
"muted": "#7886AA",
"disabled_surface": "#F0F2F8",
"disabled_text": "#A4ADC3",
"indigo": "#5761F4",
"indigo_hover": "#4C57E9",
"indigo_pressed": "#4451E2",
"indigo_pale": "#F0F2FF",
"focus": "#8D9BFF",
"selection": "#EDF0FF",
"success": "#17A77D",
"success_pale": "#EAF9F3",
"warning": "#D38625",
"warning_pale": "#FFF5E6",
"danger": "#F15B67",
"danger_pale": "#FFF1F3",
"info": "#4D69ED",
"info_pale": "#F0F4FF",
# Backward-compatible names used by older UI code and integrations.
"ink": "#172033",
"ink_soft": "#34415A",
"teal": "#4F63D9",
"teal_dark": "#3446AF",
"teal_pale": "#E9EDFF",
"ink": "#111F46",
"ink_soft": "#3F4E75",
"teal": "#5761F4",
"teal_dark": "#4451E2",
"teal_pale": "#F0F2FF",
}
@@ -65,17 +78,86 @@ QWidget {
QMainWindow, QDialog, QWidget#LoginCanvas {
background-color: $canvas;
}
QWidget#AppCanvas, QWidget#ShellWorkspace, QStackedWidget#ShellPageStack {
QDialog[businessDialog="true"] {
background-color: $canvas_mid;
}
QDialog[businessDialog="true"] QFrame#DialogSurface,
QDialog[businessDialog="true"] QFrame[dialogSurface="true"] {
background-color: $surface;
border: 1px solid $line;
border-radius: 14px;
}
QDialog[businessDialog="true"] QFrame#DialogHeader,
QDialog[businessDialog="true"] QFrame[dialogRole="header"] {
min-height: 58px;
background-color: $surface;
border: 0;
border-bottom: 1px solid $line;
}
QDialog[businessDialog="true"] QLabel[dialogRole="title"] {
color: $text;
font-size: 18px;
font-weight: 700;
}
QDialog[businessDialog="true"] QLabel[dialogRole="subtitle"] {
color: $muted;
font-size: 12px;
}
QDialog[businessDialog="true"] QDialogButtonBox,
QMessageBox QDialogButtonBox,
QInputDialog QDialogButtonBox {
min-height: 58px;
background-color: $surface;
border: 0;
border-top: 1px solid $line;
}
QDialog[businessDialog="true"] QDialogButtonBox QPushButton,
QMessageBox QDialogButtonBox QPushButton,
QInputDialog QDialogButtonBox QPushButton {
min-width: 88px;
min-height: 38px;
}
QMessageBox[businessDialog="true"],
QInputDialog[businessDialog="true"] {
background-color: $canvas_mid;
}
QMessageBox QLabel#qt_msgbox_label {
min-width: 300px;
padding: 8px 2px;
color: $text_soft;
font-size: 14px;
}
QMessageBox QLabel#qt_msgboxex_icon_label {
min-width: 44px;
min-height: 44px;
margin: 4px 10px 4px 0;
}
QMessageBox[messageKind="warning"] QLabel#qt_msgbox_label,
QMessageBox[messageKind="critical"] QLabel#qt_msgbox_label {
color: $text;
}
QInputDialog QLabel {
color: $text_soft;
font-size: 14px;
}
QWidget#AppCanvas {
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 1,
stop: 0 $canvas,
stop: 0.58 $canvas_mid,
stop: 1 $canvas_glow
);
border-radius: 18px;
}
QWidget#ShellWorkspace, QStackedWidget#ShellPageStack {
background-color: #FCFDFE;
}
QLabel[role="muted"] { color: $muted; }
QLabel[role="danger"] { color: $danger; }
QLabel[role="breadcrumb"] { color: $muted; font-size: 12px; }
QLabel[role="breadcrumbSeparator"] { color: #ADB5C9; font-size: 14px; }
QLabel[role="breadcrumbCurrent"] { color: $text_soft; font-size: 12px; font-weight: 600; }
QLabel[role="eyebrow"] {
color: $indigo_hover;
font-size: 11px;
@@ -84,7 +166,7 @@ QLabel[role="eyebrow"] {
}
QLabel[role="pageTitle"] {
color: $text;
font-size: 24px;
font-size: 22px;
font-weight: 700;
}
QLabel[role="sectionTitle"] {
@@ -106,14 +188,52 @@ QLabel[role="metric"] {
QFrame#Card, QFrame#Panel, QFrame#FilterBar, QFrame#DetailPanel,
QFrame[glass="true"] {
background-color: $glass;
border: 1px solid $line_soft;
border-radius: 14px;
border: 1px solid $line;
border-radius: 12px;
}
QFrame#FilterBar { background-color: $surface; }
QFrame#SubtleCard {
background-color: $glass_alt;
border: 1px solid $line;
border-radius: 12px;
}
QFrame#MetricCard {
background-color: $surface;
border: 1px solid $line;
border-radius: 11px;
}
QFrame#MetricCard:hover { border-color: $line_soft; background-color: $surface_alt; }
QFrame#MetricCard QLabel[role="metricTitle"] { color: $muted; font-size: 12px; }
QFrame#MetricCard QLabel[role="metricValue"] { color: $text; font-size: 21px; font-weight: 700; }
QFrame#MetricCard QLabel[role="metricHint"] { color: $muted; font-size: 11px; }
QFrame#ReceptionDetailPanel { background-color: transparent; border: 0; }
QFrame#ReceptionAiCard {
min-height: 132px;
background-color: #F8FAFF;
border: 1px solid $line_soft;
border-radius: 13px;
}
QLabel#ReceptionAiTitle {
color: $indigo_pressed;
font-size: 14px;
font-weight: 700;
}
QFrame#ReceptionAiCard QPushButton[variant="secondary"] {
min-height: 30px;
padding: 0 9px;
font-size: 11px;
}
QLabel#MetricGlyph {
color: $indigo;
background-color: $indigo_pale;
border: 1px solid $line_soft;
border-radius: 11px;
font-size: 16px;
font-weight: 700;
}
QLabel#MetricGlyph[kind="success"] { color: $success; background-color: $success_pale; }
QLabel#MetricGlyph[kind="warning"] { color: $warning; background-color: $warning_pale; }
QLabel#MetricGlyph[kind="info"] { color: $info; background-color: $info_pale; }
QFrame#Divider {
background-color: $line;
min-height: 1px;
@@ -138,8 +258,8 @@ QPushButton {
min-height: 36px;
padding: 0 16px;
border: 1px solid $line;
border-radius: 10px;
background-color: $surface_alt;
border-radius: 9px;
background-color: $surface;
color: $text;
font-weight: 600;
}
@@ -168,7 +288,10 @@ QPushButton:disabled {
QPushButton[variant="primary"] {
color: #FFFFFF;
background-color: $indigo;
background-color: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 $indigo, stop: 1 #7769F7
);
border-color: $indigo;
}
QPushButton[variant="primary"]:hover,
@@ -363,7 +486,7 @@ QTimeEdit, QSpinBox, QDoubleSpinBox, QKeySequenceEdit {
min-height: 36px;
padding: 0 12px;
border: 1px solid $line;
border-radius: 10px;
border-radius: 9px;
background-color: $surface;
color: $text;
selection-background-color: $indigo;
@@ -436,7 +559,7 @@ QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
QAbstractItemView, QTableWidget, QTableView, QListWidget, QListView, QTreeWidget, QTreeView {
color: $text;
background-color: $surface;
alternate-background-color: $surface_alt;
alternate-background-color: #FBFCFF;
border: 0;
border-radius: 12px;
gridline-color: $line;
@@ -446,7 +569,7 @@ QAbstractItemView, QTableWidget, QTableView, QListWidget, QListView, QTreeWidget
}
QAbstractItemView:focus { border: 1px solid $indigo_hover; }
QTableWidget::item, QTableView::item {
padding: 9px 8px;
padding: 8px 8px;
border-bottom: 1px solid $line;
}
QTableWidget::item:hover, QTableView::item:hover { background-color: $surface_alt; }
@@ -455,12 +578,12 @@ QTableWidget::item:selected, QTableView::item:selected {
background-color: $selection;
}
QHeaderView::section {
background-color: $surface_alt;
background-color: #F7F9FE;
color: $muted;
border: 0;
border-right: 1px solid $line;
border-bottom: 1px solid $line;
padding: 10px 8px;
padding: 9px 8px;
font-size: 12px;
font-weight: 700;
}
@@ -506,6 +629,26 @@ QTabBar::tab:selected {
border-color: $line_soft;
}
QTabBar::tab:disabled { color: $disabled_text; }
QTabBar#ReceptionDetailTabs {
background-color: $surface;
border-bottom: 1px solid $line;
}
QTabBar#ReceptionDetailTabs::tab {
min-height: 38px;
padding: 0 14px;
margin: 0 4px 0 0;
color: $muted;
background-color: transparent;
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
}
QTabBar#ReceptionDetailTabs::tab:hover { color: $text; background-color: $surface_alt; }
QTabBar#ReceptionDetailTabs::tab:selected {
color: $indigo_pressed;
background-color: transparent;
border-bottom: 2px solid $indigo;
}
QMenuBar {
color: $text_soft;
@@ -634,6 +777,23 @@ QLabel#StatusBadge[kind="danger"] { color: $danger; background-color: $danger_pa
QLabel#StatusBadge[kind="info"] { color: $info; background-color: $info_pale; border-color: rgba(120, 167, 255, 72); }
QLabel#StatusBadge[kind="accent"] { color: $indigo_hover; background-color: $indigo_pale; border-color: $line_soft; }
QWidget#Pager QLabel#PagerActive {
min-width: 42px;
min-height: 30px;
color: #FFFFFF;
background-color: $indigo;
border: 1px solid $indigo;
border-radius: 8px;
font-size: 12px;
font-weight: 700;
}
QWidget#Pager QPushButton {
min-height: 30px;
padding: 0 10px;
border: 1px solid $line;
background-color: $surface;
}
QWidget#EmptyState { background-color: transparent; }
QLabel#EmptyStateGlyph {
color: $indigo_hover;
@@ -684,7 +844,8 @@ QFrame#BusyOverlay QProgressBar { background-color: $line; }
QWidget#Sidebar {
background-color: rgba(255, 255, 255, 250);
border-right: 1px solid $line;
border: 1px solid $line;
border-radius: 17px;
}
QFrame#TopBar, QFrame#MultipleTabs {
background-color: rgba(255, 255, 255, 248);
@@ -739,10 +900,158 @@ def _apply_group(
palette.setColor(group, role, QColor(value))
def _register_preferred_cjk_fonts() -> str:
"""Make the bundled/offscreen Windows runtime aware of its CJK fonts.
Qt's offscreen platform does not always enumerate the Windows font
collection. Registering the already-installed YaHei collection only
when it is missing prevents Chinese text from degrading to tofu boxes in
packaged captures and headless visual checks. Other platforms continue
to use their native PingFang/Noto fallback.
"""
platform_families = {
"win32": ("Microsoft YaHei UI", "Microsoft YaHei"),
"darwin": ("PingFang SC", "Hiragino Sans GB"),
}
preferred = platform_families.get(sys.platform, ("Noto Sans CJK SC", "Source Han Sans SC"))
available = set(QFontDatabase.families())
if not any(family in available for family in preferred) and sys.platform == "win32":
windows_root = Path(
os.environ.get("SYSTEMROOT") or os.environ.get("WINDIR") or r"C:\Windows"
)
font_dir = windows_root / "Fonts"
for filename in ("msyh.ttc", "msyhbd.ttc", "msyhl.ttc"):
font_path = font_dir / filename
if not font_path.is_file():
continue
font_id = QFontDatabase.addApplicationFont(str(font_path))
if font_id >= 0 and QFontDatabase.applicationFontFamilies(font_id):
available.update(QFontDatabase.applicationFontFamilies(font_id))
for family in (*preferred, "Arial Unicode MS"):
if family in available:
return family
return QApplication.font().family()
_PRIMARY_STANDARD_BUTTONS = frozenset(
{
QDialogButtonBox.StandardButton.Ok,
QDialogButtonBox.StandardButton.Save,
QDialogButtonBox.StandardButton.SaveAll,
QDialogButtonBox.StandardButton.Open,
QDialogButtonBox.StandardButton.Apply,
QDialogButtonBox.StandardButton.Yes,
QDialogButtonBox.StandardButton.YesToAll,
QDialogButtonBox.StandardButton.Retry,
}
)
_DANGER_STANDARD_BUTTONS = frozenset(
{
QDialogButtonBox.StandardButton.Abort,
QDialogButtonBox.StandardButton.Discard,
}
)
def _refresh_widget_style(widget: object) -> None:
style = getattr(widget, "style", lambda: None)()
if style is None:
return
style.unpolish(widget)
style.polish(widget)
def _polish_dialog_buttons(button_box: QDialogButtonBox) -> None:
"""Assign semantic variants to Qt standard buttons without changing behavior."""
for button in button_box.buttons():
if button.property("variant"):
continue
standard = button_box.standardButton(button)
if standard in _PRIMARY_STANDARD_BUTTONS:
variant = "primary"
elif standard in _DANGER_STANDARD_BUTTONS:
variant = "danger"
else:
variant = "secondary"
button.setProperty("variant", variant)
_refresh_widget_style(button)
def mark_business_dialog(dialog: QDialog, object_name: str | None = None) -> None:
"""Opt a business subwindow into the shared visual contract.
The helper intentionally does not change modality, ownership or result
handling. It only supplies stable styling metadata and semantic button
roles, so existing workflows keep their original behavior.
"""
if object_name and not dialog.objectName():
dialog.setObjectName(object_name)
dialog.setProperty("businessDialog", True)
for button_box in dialog.findChildren(QDialogButtonBox):
_polish_dialog_buttons(button_box)
_refresh_widget_style(dialog)
class _BusinessDialogStyleFilter(QObject):
"""Style dynamically-created Qt dialogs just before they become visible."""
@staticmethod
def _is_native_or_overlay(dialog: QDialog) -> bool:
return (
isinstance(dialog, QFileDialog)
or dialog.metaObject().className() == "QPrintDialog"
or dialog.testAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
)
def eventFilter(self, watched: QObject, event: QEvent) -> bool: # noqa: N802
event_type = event.type()
if isinstance(watched, QDialogButtonBox) and event_type in {
QEvent.Type.Polish,
QEvent.Type.Show,
}:
_polish_dialog_buttons(watched)
elif isinstance(watched, QDialog) and event_type == QEvent.Type.Show:
for button_box in watched.findChildren(QDialogButtonBox):
_polish_dialog_buttons(button_box)
if not self._is_native_or_overlay(watched):
mark_business_dialog(watched)
if isinstance(watched, QMessageBox):
icon_kinds = {
QMessageBox.Icon.Information: "info",
QMessageBox.Icon.Warning: "warning",
QMessageBox.Icon.Critical: "critical",
QMessageBox.Icon.Question: "question",
}
watched.setProperty("messageKind", icon_kinds.get(watched.icon(), "plain"))
watched.setMinimumWidth(max(watched.minimumWidth(), 460))
elif isinstance(watched, QInputDialog):
watched.setMinimumWidth(max(watched.minimumWidth(), 480))
return False
def _install_business_dialog_styling(app: QApplication) -> None:
existing = getattr(app, "_doctor_business_dialog_style_filter", None)
if existing is not None:
return
style_filter = _BusinessDialogStyleFilter(app)
app.installEventFilter(style_filter)
app._doctor_business_dialog_style_filter = style_filter
def apply_theme(app: QApplication) -> None:
"""Apply the global Fusion palette and stylesheet to ``app``."""
app.setStyle("Fusion")
cjk_family = _register_preferred_cjk_fonts()
# Pin the Windows CJK face explicitly. A comma-separated QSS fallback
# list can resolve to Qt's generic sans face in offscreen/native title-bar
# captures, which changes glyph width and can even yield tofu boxes.
application_font = QFont(app.font())
application_font.setFamily(cjk_family)
app.setFont(application_font)
palette = QPalette()
active = {
QPalette.ColorRole.WindowText: COLORS["text"],
@@ -795,6 +1104,7 @@ def apply_theme(app: QApplication) -> None:
_apply_group(palette, QPalette.ColorGroup.Disabled, disabled)
app.setPalette(palette)
app.setStyleSheet(GLOBAL_QSS)
_install_business_dialog_styling(app)
__all__ = ["COLORS", "GLOBAL_QSS", "apply_theme"]
__all__ = ["COLORS", "GLOBAL_QSS", "apply_theme", "mark_business_dialog"]
+80 -6
View File
@@ -378,7 +378,7 @@ def friendly_error(error: Any) -> str:
class PageHeader(QWidget):
"""Consistent title, subtitle, and action area for business pages."""
"""Reference-design page heading with breadcrumb, copy and actions."""
def __init__(
self,
@@ -387,9 +387,29 @@ class PageHeader(QWidget):
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
layout = QHBoxLayout(self)
self.setObjectName("PageHeader")
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(16)
layout.setSpacing(8)
breadcrumb = QHBoxLayout()
breadcrumb.setContentsMargins(0, 0, 0, 0)
breadcrumb.setSpacing(7)
home_label = QLabel("首页", self)
home_label.setProperty("role", "breadcrumb")
breadcrumb.addWidget(home_label)
separator = QLabel("", self)
separator.setProperty("role", "breadcrumbSeparator")
breadcrumb.addWidget(separator)
self.breadcrumb_label = QLabel(title, self)
self.breadcrumb_label.setProperty("role", "breadcrumbCurrent")
breadcrumb.addWidget(self.breadcrumb_label)
breadcrumb.addStretch(1)
layout.addLayout(breadcrumb)
heading = QHBoxLayout()
heading.setContentsMargins(0, 0, 0, 0)
heading.setSpacing(16)
text_layout = QVBoxLayout()
text_layout.setSpacing(3)
self.title_label = QLabel(title, self)
@@ -400,10 +420,11 @@ class PageHeader(QWidget):
self.subtitle_label.setWordWrap(True)
self.subtitle_label.setVisible(bool(subtitle))
text_layout.addWidget(self.subtitle_label)
layout.addLayout(text_layout, 1)
heading.addLayout(text_layout, 1)
self.actions = QHBoxLayout()
self.actions.setSpacing(8)
layout.addLayout(self.actions)
heading.addLayout(self.actions)
layout.addLayout(heading)
def add_action(self, widget: QWidget) -> QWidget:
self.actions.addWidget(widget)
@@ -414,6 +435,56 @@ class PageHeader(QWidget):
self.subtitle_label.setVisible(bool(text))
class MetricCard(QFrame):
"""Compact dashboard metric used above dense business tables."""
def __init__(
self,
title: str,
value: str = "0",
*,
hint: str = "",
kind: str = "accent",
glyph: str = "",
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.setObjectName("MetricCard")
self.setProperty("kind", kind)
self.setMinimumHeight(82)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
layout = QHBoxLayout(self)
layout.setContentsMargins(16, 12, 14, 12)
layout.setSpacing(12)
copy = QVBoxLayout()
copy.setSpacing(3)
title_label = QLabel(title, self)
title_label.setProperty("role", "metricTitle")
copy.addWidget(title_label)
self.value_label = QLabel(str(value), self)
self.value_label.setProperty("role", "metricValue")
copy.addWidget(self.value_label)
self.hint_label = QLabel(hint, self)
self.hint_label.setProperty("role", "metricHint")
self.hint_label.setVisible(bool(hint))
copy.addWidget(self.hint_label)
layout.addLayout(copy, 1)
self.glyph_label = QLabel(glyph, self)
self.glyph_label.setObjectName("MetricGlyph")
self.glyph_label.setProperty("kind", kind)
self.glyph_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.glyph_label.setFixedSize(38, 38)
self.glyph_label.setVisible(bool(glyph))
layout.addWidget(self.glyph_label)
def set_value(self, value: Any) -> None:
self.value_label.setText(display_text(value, "0"))
def set_hint(self, text: str) -> None:
self.hint_label.setText(text)
self.hint_label.setVisible(bool(text))
class StatusBadge(QLabel):
def __init__(
self, text: str = "", kind: str = "neutral", parent: QWidget | None = None
@@ -688,20 +759,22 @@ class Pager(QWidget):
def __init__(self, page_size: int = 20, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setObjectName("Pager")
self.page = 1
self.page_size = page_size
self.total = 0
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 4, 0, 0)
layout.addStretch(1)
self.summary = QLabel("共 0 条")
self.summary.setProperty("role", "muted")
layout.addWidget(self.summary)
layout.addStretch(1)
self.previous = QPushButton("上一页")
self.previous.setProperty("variant", "ghost")
self.previous.clicked.connect(lambda: self._request(self.page - 1))
layout.addWidget(self.previous)
self.page_label = QLabel("1 / 1")
self.page_label.setObjectName("PagerActive")
self.page_label.setMinimumWidth(58)
self.page_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.page_label)
@@ -763,6 +836,7 @@ __all__ = [
"BusyOverlay",
"EmptyState",
"MessageBanner",
"MetricCard",
"OverlayHost",
"PageHeader",
"Pager",