更新bug
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 188 KiB |
@@ -43,7 +43,7 @@ from .repository import (
|
||||
ItemT = TypeVar("ItemT")
|
||||
|
||||
|
||||
DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"doctor.appointment/lists",
|
||||
"doctor.appointment/reception",
|
||||
"doctor.appointment/notifyAssistant",
|
||||
@@ -59,12 +59,12 @@ DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"tcm.diagnosis/delete",
|
||||
"tcm.diagnosis/readonlyDetail",
|
||||
"tcm.diagnosis/aiReports",
|
||||
"tcm.diagnosis/generateAiReports",
|
||||
"tcm.diagnosis/editAiReport",
|
||||
"tcm.diagnosis/aiAnalysis",
|
||||
"tcm.diagnosis/aiAssistant",
|
||||
"tcm.diagnosis/patientAiReports",
|
||||
"tcm.diagnosis/generatePatientAiReport",
|
||||
"tcm.diagnosis/generateAiReports",
|
||||
"tcm.diagnosis/editAiReport",
|
||||
"tcm.diagnosis/aiAnalysis",
|
||||
"tcm.diagnosis/aiAssistant",
|
||||
"tcm.diagnosis/patientAiReports",
|
||||
"tcm.diagnosis/generatePatientAiReport",
|
||||
"tcm.diagnosis/dailyRecord",
|
||||
"tcm.diagnosis/chufang",
|
||||
"tcm.diagnosis/huifang",
|
||||
@@ -120,12 +120,12 @@ DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"tcm.diagnosis/guahao",
|
||||
"tcm.diagnosis/fillIdCard",
|
||||
"doctor.medicine/lists",
|
||||
)
|
||||
|
||||
PATIENT_AI_MEDICAL_DISCLAIMER = (
|
||||
"仅供临床辅助参考,不可替代医生诊断,不得直接用于开方、用药调整或其他医疗决策。"
|
||||
"系统未对舌像、报告附件或视频画面进行视觉诊断;仅分析已录入、归档或转写的文字及附件元数据。"
|
||||
)
|
||||
)
|
||||
|
||||
PATIENT_AI_MEDICAL_DISCLAIMER = (
|
||||
"仅供临床辅助参考,不可替代医生诊断,不得直接用于开方、用药调整或其他医疗决策。"
|
||||
"系统未对舌像、报告附件或视频画面进行视觉诊断;仅分析已录入、归档或转写的文字及附件元数据。"
|
||||
)
|
||||
|
||||
|
||||
class DemoDoctorRepository:
|
||||
@@ -155,12 +155,12 @@ class DemoDoctorRepository:
|
||||
self._template_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_template_ai_reports()
|
||||
)
|
||||
self._diagnosis_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_diagnosis_ai_reports()
|
||||
)
|
||||
self._patient_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_patient_ai_reports()
|
||||
)
|
||||
self._diagnosis_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_diagnosis_ai_reports()
|
||||
)
|
||||
self._patient_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_patient_ai_reports()
|
||||
)
|
||||
self._prescriptions = self._build_prescriptions()
|
||||
self._medicines = self._build_medicines()
|
||||
self._patient_orders = self._build_patient_orders()
|
||||
@@ -987,7 +987,7 @@ class DemoDoctorRepository:
|
||||
consultation, generated=True, status="success"
|
||||
)
|
||||
|
||||
def edit_diagnosis_ai_report(
|
||||
def edit_diagnosis_ai_report(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
@@ -1027,155 +1027,155 @@ class DemoDoctorRepository:
|
||||
target["edited_by"] = 1001
|
||||
target["edited_time"] = int(now.timestamp())
|
||||
target["edited_at"] = now.isoformat(sep=" ")
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"report": deepcopy(target),
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
|
||||
def analyze_diagnosis_ai(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
prompt: str,
|
||||
*,
|
||||
task: str = "custom",
|
||||
) -> dict[str, Any]:
|
||||
"""Return a deterministic offline answer matching the server DTO."""
|
||||
|
||||
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")
|
||||
if task not in {
|
||||
"summary",
|
||||
"tcm_pattern",
|
||||
"prescription_review",
|
||||
"medication_review",
|
||||
"exam_review",
|
||||
"complication_risk",
|
||||
"guideline_review",
|
||||
"custom",
|
||||
}:
|
||||
raise ValueError("task is not supported")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
model_key = (
|
||||
"openai"
|
||||
if task in {"exam_review", "complication_risk", "guideline_review"}
|
||||
else "qwen"
|
||||
)
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"answer": (
|
||||
f"已结合{consultation.patient_name}的当前病历分析“{clean_prompt}”。"
|
||||
"请由医生结合四诊与最新检验结果复核。"
|
||||
),
|
||||
"model_key": model_key,
|
||||
"model_label": "千问" if model_key == "qwen" else "OpenAI",
|
||||
"task": task,
|
||||
}
|
||||
|
||||
def get_diagnosis_ai_analysis(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"] = "qwen",
|
||||
) -> dict[str, Any]:
|
||||
"""Return one model's structured offline analysis for the demo case."""
|
||||
|
||||
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")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
diagnosis = str(raw.get("clinical_diagnosis") or "待进一步辨证").strip()
|
||||
complaint = str(raw.get("chief_complaint") or "主诉信息待完善").strip()
|
||||
try:
|
||||
glucose = float(raw.get("fasting_blood_sugar"))
|
||||
except (TypeError, ValueError):
|
||||
glucose = None
|
||||
risk_assessment: list[dict[str, str]] = []
|
||||
if glucose is not None and glucose >= 7.0:
|
||||
risk_assessment.append({"label": "高血糖风险", "level": "high"})
|
||||
elif glucose is not None and glucose >= 6.1:
|
||||
risk_assessment.append({"label": "血糖波动风险", "level": "medium"})
|
||||
if "眠" in complaint or "睡眠" in str(raw.get("remark") or ""):
|
||||
risk_assessment.append({"label": "睡眠质量风险", "level": "low"})
|
||||
if not risk_assessment:
|
||||
risk_assessment.append({"label": "需持续随访", "level": "low"})
|
||||
treatment = str(
|
||||
raw.get("treatment_advice")
|
||||
or raw.get("treatment_principle")
|
||||
or raw.get("prescription_advice")
|
||||
or f"围绕{diagnosis}继续完善四诊信息,并结合最新检验结果调整方案。"
|
||||
).strip()
|
||||
diagnosis_advice = f"{diagnosis};重点复核:{complaint}"
|
||||
if clean_model == "openai":
|
||||
diagnosis_advice = f"{diagnosis};建议同步核对主诉与客观检查:{complaint}"
|
||||
treatment = f"{treatment} 同时复核近期检查趋势与用药安全性。"
|
||||
return {
|
||||
"diagnosis_advice": diagnosis_advice,
|
||||
"risk_assessment": risk_assessment,
|
||||
"treatment_advice": treatment,
|
||||
"model_key": clean_model,
|
||||
"model_label": "千问" if clean_model == "qwen" else "OpenAI",
|
||||
"model_name": (
|
||||
"qwen3.6-35b-demo" if clean_model == "qwen" else "openai-demo"
|
||||
),
|
||||
"generated_at": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
}
|
||||
|
||||
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
|
||||
"""Return every saved patient snapshot without generating a new one."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
return self._patient_ai_reports_payload(patient_id, consultation)
|
||||
|
||||
def generate_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"],
|
||||
) -> dict[str, Any]:
|
||||
"""Append one deterministic demo snapshot while retaining every version."""
|
||||
|
||||
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")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
current = self._patient_ai_reports.setdefault(patient_id, [])
|
||||
version = 1 + sum(
|
||||
str(row.get("model_key") or "").strip().lower() == clean_model
|
||||
for row in current
|
||||
)
|
||||
generated = self._make_patient_ai_report(
|
||||
patient_id,
|
||||
consultation,
|
||||
model=clean_model,
|
||||
generated_at=datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
version=version,
|
||||
)
|
||||
current.append(generated)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"generated_report": deepcopy(generated),
|
||||
"report": deepcopy(generated),
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": deepcopy(generated.get("source_summary", {})),
|
||||
}
|
||||
|
||||
def list_medicines(
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"report": deepcopy(target),
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
|
||||
def analyze_diagnosis_ai(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
prompt: str,
|
||||
*,
|
||||
task: str = "custom",
|
||||
) -> dict[str, Any]:
|
||||
"""Return a deterministic offline answer matching the server DTO."""
|
||||
|
||||
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")
|
||||
if task not in {
|
||||
"summary",
|
||||
"tcm_pattern",
|
||||
"prescription_review",
|
||||
"medication_review",
|
||||
"exam_review",
|
||||
"complication_risk",
|
||||
"guideline_review",
|
||||
"custom",
|
||||
}:
|
||||
raise ValueError("task is not supported")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
model_key = (
|
||||
"openai"
|
||||
if task in {"exam_review", "complication_risk", "guideline_review"}
|
||||
else "qwen"
|
||||
)
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"answer": (
|
||||
f"已结合{consultation.patient_name}的当前病历分析“{clean_prompt}”。"
|
||||
"请由医生结合四诊与最新检验结果复核。"
|
||||
),
|
||||
"model_key": model_key,
|
||||
"model_label": "千问" if model_key == "qwen" else "OpenAI",
|
||||
"task": task,
|
||||
}
|
||||
|
||||
def get_diagnosis_ai_analysis(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"] = "qwen",
|
||||
) -> dict[str, Any]:
|
||||
"""Return one model's structured offline analysis for the demo case."""
|
||||
|
||||
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")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
diagnosis = str(raw.get("clinical_diagnosis") or "待进一步辨证").strip()
|
||||
complaint = str(raw.get("chief_complaint") or "主诉信息待完善").strip()
|
||||
try:
|
||||
glucose = float(raw.get("fasting_blood_sugar"))
|
||||
except (TypeError, ValueError):
|
||||
glucose = None
|
||||
risk_assessment: list[dict[str, str]] = []
|
||||
if glucose is not None and glucose >= 7.0:
|
||||
risk_assessment.append({"label": "高血糖风险", "level": "high"})
|
||||
elif glucose is not None and glucose >= 6.1:
|
||||
risk_assessment.append({"label": "血糖波动风险", "level": "medium"})
|
||||
if "眠" in complaint or "睡眠" in str(raw.get("remark") or ""):
|
||||
risk_assessment.append({"label": "睡眠质量风险", "level": "low"})
|
||||
if not risk_assessment:
|
||||
risk_assessment.append({"label": "需持续随访", "level": "low"})
|
||||
treatment = str(
|
||||
raw.get("treatment_advice")
|
||||
or raw.get("treatment_principle")
|
||||
or raw.get("prescription_advice")
|
||||
or f"围绕{diagnosis}继续完善四诊信息,并结合最新检验结果调整方案。"
|
||||
).strip()
|
||||
diagnosis_advice = f"{diagnosis};重点复核:{complaint}"
|
||||
if clean_model == "openai":
|
||||
diagnosis_advice = f"{diagnosis};建议同步核对主诉与客观检查:{complaint}"
|
||||
treatment = f"{treatment} 同时复核近期检查趋势与用药安全性。"
|
||||
return {
|
||||
"diagnosis_advice": diagnosis_advice,
|
||||
"risk_assessment": risk_assessment,
|
||||
"treatment_advice": treatment,
|
||||
"model_key": clean_model,
|
||||
"model_label": "千问" if clean_model == "qwen" else "OpenAI",
|
||||
"model_name": (
|
||||
"qwen3.6-35b-demo" if clean_model == "qwen" else "openai-demo"
|
||||
),
|
||||
"generated_at": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
}
|
||||
|
||||
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
|
||||
"""Return every saved patient snapshot without generating a new one."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
return self._patient_ai_reports_payload(patient_id, consultation)
|
||||
|
||||
def generate_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"],
|
||||
) -> dict[str, Any]:
|
||||
"""Append one deterministic demo snapshot while retaining every version."""
|
||||
|
||||
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")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
current = self._patient_ai_reports.setdefault(patient_id, [])
|
||||
version = 1 + sum(
|
||||
str(row.get("model_key") or "").strip().lower() == clean_model
|
||||
for row in current
|
||||
)
|
||||
generated = self._make_patient_ai_report(
|
||||
patient_id,
|
||||
consultation,
|
||||
model=clean_model,
|
||||
generated_at=datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
version=version,
|
||||
)
|
||||
current.append(generated)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"generated_report": deepcopy(generated),
|
||||
"report": deepcopy(generated),
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": deepcopy(generated.get("source_summary", {})),
|
||||
}
|
||||
|
||||
def list_medicines(
|
||||
self,
|
||||
*,
|
||||
name: str = "",
|
||||
@@ -3138,23 +3138,23 @@ class DemoDoctorRepository:
|
||||
return patient
|
||||
raise RepositoryNotFoundError(f"patient diagnosis {diagnosis_id} not found")
|
||||
|
||||
def _find_consultation(self, diagnosis_id: int) -> Consultation:
|
||||
for consultation in self._consultations:
|
||||
if consultation.id == diagnosis_id:
|
||||
return consultation
|
||||
raise RepositoryNotFoundError(f"consultation {diagnosis_id} not found")
|
||||
|
||||
def _find_patient_consultation(self, patient_id: int) -> Consultation:
|
||||
"""Return the most recent demo consultation for a source patient id."""
|
||||
|
||||
matches = [
|
||||
consultation
|
||||
for consultation in self._consultations
|
||||
if consultation.patient_id == patient_id
|
||||
]
|
||||
if matches:
|
||||
return max(matches, key=lambda consultation: consultation.id)
|
||||
raise RepositoryNotFoundError(f"patient {patient_id} not found")
|
||||
def _find_consultation(self, diagnosis_id: int) -> Consultation:
|
||||
for consultation in self._consultations:
|
||||
if consultation.id == diagnosis_id:
|
||||
return consultation
|
||||
raise RepositoryNotFoundError(f"consultation {diagnosis_id} not found")
|
||||
|
||||
def _find_patient_consultation(self, patient_id: int) -> Consultation:
|
||||
"""Return the most recent demo consultation for a source patient id."""
|
||||
|
||||
matches = [
|
||||
consultation
|
||||
for consultation in self._consultations
|
||||
if consultation.patient_id == patient_id
|
||||
]
|
||||
if matches:
|
||||
return max(matches, key=lambda consultation: consultation.id)
|
||||
raise RepositoryNotFoundError(f"patient {patient_id} not found")
|
||||
|
||||
def _find_order(self, order_id: int) -> dict[str, Any]:
|
||||
for order in self._patient_orders:
|
||||
@@ -3296,6 +3296,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 1,
|
||||
"status_desc": "待接诊",
|
||||
"clinical_diagnosis": "肝郁脾虚证",
|
||||
"disease_course_text": "病程 3 年",
|
||||
"fasting_blood_sugar": 6.8,
|
||||
"diagnosis_confirmed": 1,
|
||||
"has_prescription": 0,
|
||||
"remark": "复诊,关注睡眠与口干。",
|
||||
@@ -3320,6 +3323,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 4,
|
||||
"status_desc": "已过号",
|
||||
"clinical_diagnosis": "痰湿中阻证",
|
||||
"disease_course_text": "病程 8 年",
|
||||
"fasting_blood_sugar": 8.6,
|
||||
"diagnosis_confirmed": 0,
|
||||
"has_prescription": 1,
|
||||
"prescription_audit_status": 1,
|
||||
@@ -3370,6 +3376,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 3,
|
||||
"status_desc": "已完成",
|
||||
"clinical_diagnosis": "气阴两虚证",
|
||||
"disease_course_text": "病程 5 年",
|
||||
"fasting_blood_sugar": 7.2,
|
||||
"diagnosis_confirmed": 1,
|
||||
"has_prescription": 1,
|
||||
"prescription_audit_status": 1,
|
||||
@@ -3872,10 +3881,10 @@ class DemoDoctorRepository:
|
||||
"is_edited": False,
|
||||
}
|
||||
|
||||
def _build_diagnosis_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next((row for row in self._consultations if row.id == 501), None)
|
||||
if consultation is None:
|
||||
return {}
|
||||
def _build_diagnosis_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next((row for row in self._consultations if row.id == 501), None)
|
||||
if consultation is None:
|
||||
return {}
|
||||
fingerprint = _consultation_fingerprint(consultation)
|
||||
return {
|
||||
501: [
|
||||
@@ -3886,158 +3895,158 @@ class DemoDoctorRepository:
|
||||
name="qwen3.6-35b",
|
||||
fingerprint=fingerprint,
|
||||
generated_at="2026-08-12 09:18:00",
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
def _patient_ai_reports_payload(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
) -> dict[str, Any]:
|
||||
reports = sorted(
|
||||
(deepcopy(row) for row in self._patient_ai_reports.get(patient_id, [])),
|
||||
key=lambda row: (
|
||||
str(row.get("generated_at") or row.get("created_at") or ""),
|
||||
int(row.get("id") or 0),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
latest_by_model: dict[str, dict[str, Any] | None] = {
|
||||
"qwen": None,
|
||||
"openai": None,
|
||||
}
|
||||
for row in reports:
|
||||
model = str(row.get("model_key") or "").strip().lower()
|
||||
if model in latest_by_model and latest_by_model[model] is None:
|
||||
latest_by_model[model] = deepcopy(row)
|
||||
source_summary = (
|
||||
deepcopy(reports[0].get("source_summary"))
|
||||
if reports and isinstance(reports[0].get("source_summary"), Mapping)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"reports": reports,
|
||||
"latest_by_model": latest_by_model,
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": source_summary,
|
||||
}
|
||||
|
||||
def _make_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
*,
|
||||
model: str,
|
||||
generated_at: str,
|
||||
version: int,
|
||||
) -> dict[str, Any]:
|
||||
report_id = self._next_ai_report_id
|
||||
self._next_ai_report_id += 1
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
base_diagnosis = str(
|
||||
raw.get("clinical_diagnosis")
|
||||
or consultation.clinical_diagnosis
|
||||
or "待进一步辨证"
|
||||
).strip()
|
||||
complaint = str(
|
||||
raw.get("chief_complaint")
|
||||
or consultation.chief_complaint
|
||||
or "主诉信息待完善"
|
||||
).strip()
|
||||
is_openai = model == "openai"
|
||||
diagnosis = (
|
||||
f"{base_diagnosis};建议结合客观检查复核:{complaint}"
|
||||
if is_openai
|
||||
else f"{base_diagnosis};重点结合四诊复核:{complaint}"
|
||||
)
|
||||
risks = [
|
||||
{"label": "血糖波动风险", "level": "medium"},
|
||||
{"label": "睡眠质量风险", "level": "low"},
|
||||
]
|
||||
treatment = (
|
||||
"复核近期检查趋势、并用药物与肝肾功能,再由接诊医生决定后续方案。"
|
||||
if is_openai
|
||||
else "继续完善四诊与血糖记录,由接诊医生结合复诊结果确定后续方案。"
|
||||
)
|
||||
disclaimer = PATIENT_AI_MEDICAL_DISCLAIMER
|
||||
source_summary = {
|
||||
"diagnosis_count": 1,
|
||||
"doctor_note_count": 0,
|
||||
"tracking_note_count": 0,
|
||||
"blood_record_count": 0,
|
||||
"diet_record_count": 0,
|
||||
"exercise_record_count": 0,
|
||||
"im_message_count": 0,
|
||||
"wechat_message_count": 0,
|
||||
"call_record_count": 0,
|
||||
"transcript_segment_count": 0,
|
||||
"recording_asset_count": 0,
|
||||
"source_record_count": 1,
|
||||
"snapshot_complete": True,
|
||||
"may_be_truncated": False,
|
||||
"analysis_chunk_count": 1,
|
||||
"analysis_reduction_rounds": 0,
|
||||
"analyzed_source_bytes": 0,
|
||||
"analysis_complete": True,
|
||||
}
|
||||
structured = {
|
||||
"diagnosis": diagnosis,
|
||||
"risk_assessment": risks,
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
}
|
||||
return {
|
||||
"id": report_id,
|
||||
"report_id": report_id,
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": consultation.id,
|
||||
"model_key": model,
|
||||
"model_name": "gpt-5.6-sol-demo" if is_openai else "qwen3.6-35b-demo",
|
||||
"model_label": "OpenAI" if is_openai else "千问",
|
||||
"version": version,
|
||||
"report": structured,
|
||||
"content": json.dumps(structured, ensure_ascii=False),
|
||||
"diagnosis": diagnosis,
|
||||
"diagnosis_advice": diagnosis,
|
||||
"risk_assessment": deepcopy(risks),
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
"source_hash": _consultation_fingerprint(consultation),
|
||||
"source_summary": source_summary,
|
||||
"generated_at": generated_at,
|
||||
"created_at": generated_at,
|
||||
"admin_id": 1001,
|
||||
"department_id": 10,
|
||||
"department_name": "演示中医院",
|
||||
}
|
||||
|
||||
def _build_patient_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next(
|
||||
(row for row in self._consultations if row.patient_id == 301),
|
||||
None,
|
||||
)
|
||||
if consultation is None:
|
||||
return {}
|
||||
reports: list[dict[str, Any]] = []
|
||||
for version, generated_at in (
|
||||
(1, "2026-08-12 09:18:00"),
|
||||
(2, "2026-08-13 15:42:00"),
|
||||
):
|
||||
for model in ("qwen", "openai"):
|
||||
reports.append(
|
||||
self._make_patient_ai_report(
|
||||
301,
|
||||
consultation,
|
||||
model=model,
|
||||
generated_at=generated_at,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
return {301: reports}
|
||||
|
||||
def _build_templates(self) -> list[PrescriptionTemplate]:
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
def _patient_ai_reports_payload(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
) -> dict[str, Any]:
|
||||
reports = sorted(
|
||||
(deepcopy(row) for row in self._patient_ai_reports.get(patient_id, [])),
|
||||
key=lambda row: (
|
||||
str(row.get("generated_at") or row.get("created_at") or ""),
|
||||
int(row.get("id") or 0),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
latest_by_model: dict[str, dict[str, Any] | None] = {
|
||||
"qwen": None,
|
||||
"openai": None,
|
||||
}
|
||||
for row in reports:
|
||||
model = str(row.get("model_key") or "").strip().lower()
|
||||
if model in latest_by_model and latest_by_model[model] is None:
|
||||
latest_by_model[model] = deepcopy(row)
|
||||
source_summary = (
|
||||
deepcopy(reports[0].get("source_summary"))
|
||||
if reports and isinstance(reports[0].get("source_summary"), Mapping)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"reports": reports,
|
||||
"latest_by_model": latest_by_model,
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": source_summary,
|
||||
}
|
||||
|
||||
def _make_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
*,
|
||||
model: str,
|
||||
generated_at: str,
|
||||
version: int,
|
||||
) -> dict[str, Any]:
|
||||
report_id = self._next_ai_report_id
|
||||
self._next_ai_report_id += 1
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
base_diagnosis = str(
|
||||
raw.get("clinical_diagnosis")
|
||||
or consultation.clinical_diagnosis
|
||||
or "待进一步辨证"
|
||||
).strip()
|
||||
complaint = str(
|
||||
raw.get("chief_complaint")
|
||||
or consultation.chief_complaint
|
||||
or "主诉信息待完善"
|
||||
).strip()
|
||||
is_openai = model == "openai"
|
||||
diagnosis = (
|
||||
f"{base_diagnosis};建议结合客观检查复核:{complaint}"
|
||||
if is_openai
|
||||
else f"{base_diagnosis};重点结合四诊复核:{complaint}"
|
||||
)
|
||||
risks = [
|
||||
{"label": "血糖波动风险", "level": "medium"},
|
||||
{"label": "睡眠质量风险", "level": "low"},
|
||||
]
|
||||
treatment = (
|
||||
"复核近期检查趋势、并用药物与肝肾功能,再由接诊医生决定后续方案。"
|
||||
if is_openai
|
||||
else "继续完善四诊与血糖记录,由接诊医生结合复诊结果确定后续方案。"
|
||||
)
|
||||
disclaimer = PATIENT_AI_MEDICAL_DISCLAIMER
|
||||
source_summary = {
|
||||
"diagnosis_count": 1,
|
||||
"doctor_note_count": 0,
|
||||
"tracking_note_count": 0,
|
||||
"blood_record_count": 0,
|
||||
"diet_record_count": 0,
|
||||
"exercise_record_count": 0,
|
||||
"im_message_count": 0,
|
||||
"wechat_message_count": 0,
|
||||
"call_record_count": 0,
|
||||
"transcript_segment_count": 0,
|
||||
"recording_asset_count": 0,
|
||||
"source_record_count": 1,
|
||||
"snapshot_complete": True,
|
||||
"may_be_truncated": False,
|
||||
"analysis_chunk_count": 1,
|
||||
"analysis_reduction_rounds": 0,
|
||||
"analyzed_source_bytes": 0,
|
||||
"analysis_complete": True,
|
||||
}
|
||||
structured = {
|
||||
"diagnosis": diagnosis,
|
||||
"risk_assessment": risks,
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
}
|
||||
return {
|
||||
"id": report_id,
|
||||
"report_id": report_id,
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": consultation.id,
|
||||
"model_key": model,
|
||||
"model_name": "gpt-5.6-sol-demo" if is_openai else "qwen3.6-35b-demo",
|
||||
"model_label": "OpenAI" if is_openai else "千问",
|
||||
"version": version,
|
||||
"report": structured,
|
||||
"content": json.dumps(structured, ensure_ascii=False),
|
||||
"diagnosis": diagnosis,
|
||||
"diagnosis_advice": diagnosis,
|
||||
"risk_assessment": deepcopy(risks),
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
"source_hash": _consultation_fingerprint(consultation),
|
||||
"source_summary": source_summary,
|
||||
"generated_at": generated_at,
|
||||
"created_at": generated_at,
|
||||
"admin_id": 1001,
|
||||
"department_id": 10,
|
||||
"department_name": "演示中医院",
|
||||
}
|
||||
|
||||
def _build_patient_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next(
|
||||
(row for row in self._consultations if row.patient_id == 301),
|
||||
None,
|
||||
)
|
||||
if consultation is None:
|
||||
return {}
|
||||
reports: list[dict[str, Any]] = []
|
||||
for version, generated_at in (
|
||||
(1, "2026-08-12 09:18:00"),
|
||||
(2, "2026-08-13 15:42:00"),
|
||||
):
|
||||
for model in ("qwen", "openai"):
|
||||
reports.append(
|
||||
self._make_patient_ai_report(
|
||||
301,
|
||||
consultation,
|
||||
model=model,
|
||||
generated_at=generated_at,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
return {301: reports}
|
||||
|
||||
def _build_templates(self) -> list[PrescriptionTemplate]:
|
||||
return [
|
||||
PrescriptionTemplate.from_dict(
|
||||
{
|
||||
|
||||
@@ -648,7 +648,7 @@ class AppointmentsPage(QWidget):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(22, 7, 6, 8)
|
||||
root.setSpacing(4)
|
||||
header = PageHeader("患者列表")
|
||||
header = PageHeader("问诊列表")
|
||||
header.title_label.hide()
|
||||
header.subtitle_label.hide()
|
||||
root.addWidget(header)
|
||||
|
||||
@@ -86,7 +86,7 @@ class NavigationItem:
|
||||
NAVIGATION = (
|
||||
NavigationItem(
|
||||
"appointments",
|
||||
"接诊台",
|
||||
"问诊列表",
|
||||
"号",
|
||||
AppointmentsPage,
|
||||
("doctor.appointment/lists",),
|
||||
@@ -301,8 +301,8 @@ def _resolve_navigation(
|
||||
continue
|
||||
if not _canonical_allowed(permissions, item.permissions[0]):
|
||||
continue
|
||||
if item.key in {"appointments", "patients"}:
|
||||
# Keep the two product-facing navigation titles stable even when
|
||||
if item.key in {"appointments", "reception", "patients"}:
|
||||
# Keep the product-facing navigation titles stable even when
|
||||
# the server still carries an older menu label.
|
||||
title = item.title
|
||||
else:
|
||||
@@ -549,7 +549,7 @@ def _painted_navigation_icon(kind: str, size: int = 18) -> QIcon:
|
||||
|
||||
|
||||
class _ShellBrandMark(QWidget):
|
||||
"""Paint the supplied indigo pulse mark without a font glyph dependency."""
|
||||
"""Paint the supplied indigo M mark without a font-glyph asset."""
|
||||
|
||||
def paintEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
|
||||
del event
|
||||
@@ -562,26 +562,12 @@ class _ShellBrandMark(QWidget):
|
||||
painter.setPen(QPen(QColor(117, 130, 255, 150), 1.0))
|
||||
painter.setBrush(gradient)
|
||||
painter.drawRoundedRect(rect, 10, 10)
|
||||
|
||||
pen = QPen(QColor("#FFFFFF"), 1.8)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
y = self.height() / 2
|
||||
painter.drawPolyline(
|
||||
QPolygonF(
|
||||
[
|
||||
QPointF(8, y + 2),
|
||||
QPointF(11, y + 2),
|
||||
QPointF(14, y - 6),
|
||||
QPointF(18, y + 7),
|
||||
QPointF(22, y - 4),
|
||||
QPointF(25, y + 2),
|
||||
QPointF(self.width() - 8, y + 2),
|
||||
]
|
||||
)
|
||||
)
|
||||
painter.setPen(QColor("#FFFFFF"))
|
||||
font = QFont(painter.font())
|
||||
font.setPixelSize(18)
|
||||
font.setWeight(QFont.Weight.Bold)
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "M")
|
||||
|
||||
|
||||
class _AssistantRobot(QWidget):
|
||||
@@ -837,7 +823,7 @@ class ShellWindow(QMainWindow):
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("甄养堂 · AI 问诊助手")
|
||||
self.setWindowTitle("甄养堂 · 问诊中心")
|
||||
self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
|
||||
self.repository = repository
|
||||
@@ -1032,7 +1018,7 @@ class ShellWindow(QMainWindow):
|
||||
brand_copy_layout = QVBoxLayout(self.brand_copy)
|
||||
brand_copy_layout.setContentsMargins(0, 0, 0, 0)
|
||||
brand_copy_layout.setSpacing(1)
|
||||
self.brand_name = QLabel("AI问诊助手", self.brand_copy)
|
||||
self.brand_name = QLabel("问诊中心", self.brand_copy)
|
||||
self.brand_name.setObjectName("ShellBrandName")
|
||||
brand_copy_layout.addWidget(self.brand_name)
|
||||
self.brand_subtitle = QLabel("糖尿病专科版", self.brand_copy)
|
||||
@@ -1083,7 +1069,7 @@ class ShellWindow(QMainWindow):
|
||||
outer_assistant.addWidget(self.assistant_card)
|
||||
layout.addLayout(outer_assistant)
|
||||
|
||||
self.model_label = QLabel("模型:服务端自动匹配 ›", sidebar)
|
||||
self.model_label = QLabel("模型:GPT-4o 医疗版 ›", sidebar)
|
||||
self.model_label.setObjectName("ShellModelLabel")
|
||||
self.model_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.model_label.setFixedHeight(76)
|
||||
@@ -1130,11 +1116,15 @@ class ShellWindow(QMainWindow):
|
||||
font-size: 10px;
|
||||
}
|
||||
QPushButton#ShellAiEntry {
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
min-width: 38px;
|
||||
max-width: 38px;
|
||||
min-height: 38px;
|
||||
max-height: 38px;
|
||||
padding: 0;
|
||||
color: #5265F6;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
QPushButton#ShellAiEntry:hover { background-color: #EEF1FF; }
|
||||
@@ -1220,11 +1210,13 @@ class ShellWindow(QMainWindow):
|
||||
self.context_label.hide()
|
||||
layout.addStretch(1)
|
||||
|
||||
self.ai_top_button = QPushButton("AI 助手", topbar)
|
||||
self.ai_top_button = QPushButton("", topbar)
|
||||
self.ai_top_button.setObjectName("ShellAiEntry")
|
||||
self.ai_top_button.setIcon(_painted_shell_icon("ai", 18))
|
||||
self.ai_top_button.setIconSize(QSize(18, 18))
|
||||
self.ai_top_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.ai_top_button.setToolTip("AI 助手")
|
||||
self.ai_top_button.setAccessibleName("AI 助手")
|
||||
self.ai_top_button.clicked.connect(self._open_ai_assistant)
|
||||
layout.addWidget(self.ai_top_button)
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ def test_saved_history_is_rendered_without_automatic_generation(
|
||||
assert [len(page._ai_analysis_histories[key]) for key in ("qwen", "openai")] == [2, 2]
|
||||
assert "第 2 版" in page.ai_analysis_snapshot_meta.text()
|
||||
assert page.ai_analysis_disclaimer.text() == AI_MEDICAL_DISCLAIMER
|
||||
assert not page.ai_analysis_disclaimer.isVisibleTo(page)
|
||||
assert page.ai_analysis_history_button.objectName() == "ReceptionAiHistoryButton"
|
||||
assert page.ai_analysis_regenerate_button.objectName() == "ReceptionAiRegenerateButton"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from PySide6.QtCore import QPoint, Qt
|
||||
from PySide6.QtCore import QDate, QPoint, Qt
|
||||
from PySide6.QtGui import QPalette
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QPushButton, QScrollArea
|
||||
|
||||
@@ -129,7 +129,7 @@ def test_queue_status_badge_is_not_clipped_in_narrow_panel(
|
||||
{
|
||||
"patient_name": "张蒙",
|
||||
"status": 1,
|
||||
"status_desc": "待接诊",
|
||||
"status_desc": "问诊中",
|
||||
"appointment_time": "12:45:00",
|
||||
"gender": 1,
|
||||
"age": 36,
|
||||
@@ -296,6 +296,33 @@ def test_queue_uses_admin_same_day_contract(
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_queue_date_picker_filters_the_selected_day(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
|
||||
class Repository:
|
||||
def list_appointments(self, **kwargs: Any) -> dict[str, Any]:
|
||||
calls.append(kwargs)
|
||||
return {"lists": [], "count": 0}
|
||||
|
||||
page = ReceptionPage(Repository(), PermissionSet([]))
|
||||
page.show()
|
||||
application.processEvents()
|
||||
page.queue_date_button.click()
|
||||
calendar = page.queue_date_button.calendarWidget()
|
||||
assert calendar.isVisible()
|
||||
page.queue_date_button._pick_date(QDate(2026, 8, 1))
|
||||
assert not calendar.isVisible()
|
||||
assert page.queue_date_button.text() == "2026-08-01"
|
||||
assert page._queue_date == "2026-08-01"
|
||||
assert calls[-1]["start_date"] == "2026-08-01"
|
||||
assert calls[-1]["end_date"] == "2026-08-01"
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_silent_queue_polls_reuse_rows_and_do_not_restart_detail_or_ai(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
@@ -630,14 +657,164 @@ def test_queue_load_more_accumulates_to_total_boundary(
|
||||
page = ReceptionPage(Repository(), PermissionSet([]))
|
||||
page.refresh()
|
||||
assert page.queue_list.count() == 15
|
||||
assert page.load_more_button.isVisibleTo(page)
|
||||
assert page._queue_has_more()
|
||||
assert not hasattr(page, "load_more_button")
|
||||
assert not hasattr(page, "add_patient_button")
|
||||
|
||||
page._load_more()
|
||||
assert [call["page_no"] for call in calls] == [1, 2]
|
||||
assert all(call["page_size"] == 15 for call in calls)
|
||||
assert page.queue_list.count() == 22
|
||||
assert page.queue_summary.text() == "已加载 22 / 共 22 位患者"
|
||||
assert page.load_more_button.isHidden()
|
||||
assert not page._queue_has_more()
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_silent_poll_keeps_already_loaded_queue_pages(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
all_rows = [
|
||||
{
|
||||
"id": index,
|
||||
"patient_id": 1000 + index,
|
||||
"diagnosis_id": 2000 + index,
|
||||
"patient_name": f"患者{index:02d}",
|
||||
"status": 1,
|
||||
}
|
||||
for index in range(1, 28)
|
||||
]
|
||||
|
||||
class Repository:
|
||||
def list_appointments(self, **kwargs: Any) -> dict[str, Any]:
|
||||
calls.append(dict(kwargs))
|
||||
start = (kwargs["page_no"] - 1) * kwargs["page_size"]
|
||||
return {
|
||||
"lists": all_rows[start : start + kwargs["page_size"]],
|
||||
"count": len(all_rows),
|
||||
}
|
||||
|
||||
def get_reception(self, appointment_id: int) -> dict[str, Any]:
|
||||
row = all_rows[appointment_id - 1]
|
||||
return {
|
||||
"appointment": row,
|
||||
"diagnosis": {"id": row["diagnosis_id"], "patient_id": row["patient_id"]},
|
||||
}
|
||||
|
||||
page = ReceptionPage(Repository(), PermissionSet([]))
|
||||
page.refresh()
|
||||
page._load_more()
|
||||
assert page.queue_list.count() == 27
|
||||
assert page._queue_page == 2
|
||||
calls.clear()
|
||||
page.refresh(silent=True)
|
||||
assert page.queue_list.count() == 27
|
||||
assert page._queue_page == 2
|
||||
assert calls[-1]["page_no"] == 1
|
||||
assert calls[-1]["page_size"] >= 27
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_queue_shows_loading_indicator_while_page_request_is_open(
|
||||
application: QApplication,
|
||||
queued_async: list[dict[str, Any]],
|
||||
) -> None:
|
||||
class Repository:
|
||||
def list_appointments(self, **_kwargs: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"lists": [
|
||||
{"id": 1, "patient_name": "加载患者", "status": 1},
|
||||
],
|
||||
"count": 16,
|
||||
}
|
||||
|
||||
def get_reception(self, appointment_id: int) -> dict[str, Any]:
|
||||
return {
|
||||
"appointment": {
|
||||
"id": appointment_id,
|
||||
"patient_id": 1001,
|
||||
"status": 1,
|
||||
},
|
||||
"diagnosis": {"id": 2001, "patient_id": 1001},
|
||||
}
|
||||
|
||||
page = ReceptionPage(Repository(), PermissionSet([]))
|
||||
page.refresh()
|
||||
assert page.queue_loading_indicator.isVisibleTo(page)
|
||||
assert page.queue_loading_indicator.label.text() == "正在加载…"
|
||||
assert page.queue_loading_indicator.spinner._timer.isActive()
|
||||
|
||||
queued_async[0]["on_success"](
|
||||
{
|
||||
"lists": [{"id": 1, "patient_name": "加载患者", "status": 1}],
|
||||
"count": 16,
|
||||
}
|
||||
)
|
||||
queued_async[0]["on_finished"]()
|
||||
assert not page.queue_loading_indicator.isVisibleTo(page)
|
||||
assert not page.queue_loading_indicator.spinner._timer.isActive()
|
||||
|
||||
page._load_more()
|
||||
assert page.queue_loading_indicator.isVisibleTo(page)
|
||||
load_more_job = queued_async[-1]
|
||||
load_more_job["on_success"](
|
||||
{
|
||||
"lists": [{"id": 2, "patient_name": "第二页患者", "status": 1}],
|
||||
"count": 16,
|
||||
}
|
||||
)
|
||||
load_more_job["on_finished"]()
|
||||
assert not page.queue_loading_indicator.isVisibleTo(page)
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_queue_scrolls_to_bottom_loads_next_page(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
) -> None:
|
||||
calls: list[dict[str, Any]] = []
|
||||
all_rows = [
|
||||
{
|
||||
"id": index,
|
||||
"patient_id": 1000 + index,
|
||||
"diagnosis_id": 2000 + index,
|
||||
"patient_name": f"患者{index:02d}",
|
||||
"status": 1,
|
||||
}
|
||||
for index in range(1, 23)
|
||||
]
|
||||
|
||||
class Repository:
|
||||
def list_appointments(self, **kwargs: Any) -> dict[str, Any]:
|
||||
calls.append(kwargs)
|
||||
start = (kwargs["page_no"] - 1) * kwargs["page_size"]
|
||||
return {
|
||||
"lists": all_rows[start : start + kwargs["page_size"]],
|
||||
"count": len(all_rows),
|
||||
}
|
||||
|
||||
def get_reception(self, appointment_id: int) -> dict[str, Any]:
|
||||
row = all_rows[appointment_id - 1]
|
||||
return {
|
||||
"appointment": row,
|
||||
"diagnosis": {"id": row["diagnosis_id"], "patient_id": row["patient_id"]},
|
||||
}
|
||||
|
||||
page = ReceptionPage(Repository(), PermissionSet([]))
|
||||
page.resize(1280, 640)
|
||||
page.show()
|
||||
application.processEvents()
|
||||
assert page.queue_list.count() == 15
|
||||
scrollbar = page.queue_list.verticalScrollBar()
|
||||
assert scrollbar.maximum() > 0
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
application.processEvents()
|
||||
assert [call["page_no"] for call in calls] == [1, 2]
|
||||
assert page.queue_list.count() == 22
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
@@ -871,6 +1048,8 @@ def test_reception_auto_loads_structured_ai_analysis_and_matches_reference_geome
|
||||
] == [("千问", "qwen"), ("OpenAI", "openai")]
|
||||
assert page.ai_analysis_model_selector.currentData() == "qwen"
|
||||
assert page._ai_analysis_state == "success"
|
||||
assert page.ai_analysis_stack.currentWidget() is page.ai_analysis_content_page
|
||||
assert page.ai_summary_label.isVisible()
|
||||
assert page.ai_summary_label.text() == "2 型糖尿病,血糖控制不佳"
|
||||
assert page.ai_treatment_label.text().startswith("建议调整降糖方案")
|
||||
assert page.ai_summary_label.textFormat() == Qt.TextFormat.PlainText
|
||||
@@ -887,7 +1066,7 @@ def test_reception_auto_loads_structured_ai_analysis_and_matches_reference_geome
|
||||
assert [button.text() for button in page.ai_prompt_buttons] == [
|
||||
"基于当前病史,下一步检查建议?",
|
||||
"该患者的用药调整建议?",
|
||||
"糖尿病教育要点?",
|
||||
"糖尿病教育要点有哪些?",
|
||||
"并发症筛查建议?",
|
||||
]
|
||||
assert page.ai_send_button.text() == ""
|
||||
@@ -915,10 +1094,12 @@ def test_reception_auto_loads_structured_ai_analysis_and_matches_reference_geome
|
||||
|
||||
left = page.ai_analysis_card.geometry()
|
||||
right = page.ai_assistant_card.geometry()
|
||||
assert 235 <= left.height() <= 260
|
||||
assert 470 <= left.height() <= 520
|
||||
assert left.height() == right.height()
|
||||
assert 0 <= right.left() - left.right() - 1 <= 2
|
||||
assert abs(left.width() * 5 - right.width() * 4) <= 10
|
||||
assert page.ai_summary_label.width() <= page.ai_analysis_card.contentsRect().width()
|
||||
assert page.ai_summary_label.minimumSizeHint().width() <= 48
|
||||
|
||||
calls_before_switch = list(analysis_calls)
|
||||
page.ai_analysis_model_selector.setCurrentIndex(
|
||||
@@ -931,6 +1112,14 @@ def test_reception_auto_loads_structured_ai_analysis_and_matches_reference_geome
|
||||
)
|
||||
assert page.ai_summary_label.text() == "2 型糖尿病,血糖控制不佳"
|
||||
assert analysis_calls == calls_before_switch
|
||||
assert page.detail_scroll.objectName() == "ReceptionDetailScroll"
|
||||
assert (
|
||||
page.detail_scroll.verticalScrollBarPolicy()
|
||||
== Qt.ScrollBarPolicy.ScrollBarAsNeeded
|
||||
)
|
||||
page.resize(1494, 620)
|
||||
application.processEvents()
|
||||
assert page.detail_scroll.verticalScrollBar().maximum() > 0
|
||||
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
@@ -73,7 +73,7 @@ def test_appointments_navigation_is_named_reception_and_always_first() -> None:
|
||||
)
|
||||
|
||||
assert [(item.key, title) for item, title in resolved] == [
|
||||
("appointments", "接诊台"),
|
||||
("appointments", "问诊列表"),
|
||||
("patients", "我的患者"),
|
||||
]
|
||||
|
||||
@@ -86,7 +86,7 @@ def shell_window(
|
||||
navigation = [
|
||||
NavigationItem(key, title, glyph, _ShellPageDouble, (permission,))
|
||||
for key, title, glyph, permission in (
|
||||
("appointments", "接诊台", "号", "doctor.appointment/lists"),
|
||||
("appointments", "问诊列表", "号", "doctor.appointment/lists"),
|
||||
("reception", "接诊台", "◎", "doctor.appointment/lists"),
|
||||
(
|
||||
"prescription_library",
|
||||
@@ -164,7 +164,7 @@ def test_reference_shell_has_integrated_search_ai_card_and_window_controls(
|
||||
)
|
||||
assert shell_window.assistant_card.isVisible()
|
||||
assert shell_window.assistant_button.text() == "开始对话"
|
||||
assert "服务端自动匹配" in shell_window.model_label.text()
|
||||
assert "GPT-4o 医疗版" in shell_window.model_label.text()
|
||||
assert shell_window.minimize_button.text() == ""
|
||||
assert shell_window.close_button.text() == ""
|
||||
|
||||
|
||||