更新
This commit is contained in:
@@ -16,9 +16,20 @@ from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QDate, QIODevice, QPoint, Qt, QTimer, Signal
|
||||
from PySide6.QtCore import (
|
||||
QBuffer,
|
||||
QByteArray,
|
||||
QDate,
|
||||
QIODevice,
|
||||
QPoint,
|
||||
QRectF,
|
||||
Qt,
|
||||
QTimer,
|
||||
Signal,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QImage,
|
||||
QMouseEvent,
|
||||
QPageSize,
|
||||
@@ -2437,7 +2448,14 @@ def _status_text(value: Any) -> str:
|
||||
|
||||
|
||||
def render_case_record_html(prescription: Any) -> str:
|
||||
"""Render the immutable diagnosis snapshot stored with a prescription."""
|
||||
"""Render the immutable diagnosis snapshot as the admin A3 case sheet.
|
||||
|
||||
``QTextDocument`` only implements a deliberately small HTML/CSS subset. In
|
||||
particular, CSS grid and ``display: table-row`` silently degrade into a
|
||||
vertical text stream. The admin preview uses a three-column grid, so this
|
||||
version expresses that layout with real HTML tables which Qt renders
|
||||
consistently on screen, printers and PDF devices.
|
||||
"""
|
||||
|
||||
source = _mapping(prescription)
|
||||
snapshot = _mapping(source.get("case_record"))
|
||||
@@ -2452,11 +2470,35 @@ def render_case_record_html(prescription: Any) -> str:
|
||||
value = json.dumps(value, ensure_ascii=False, default=str)
|
||||
return html.escape(display_text(value, default))
|
||||
|
||||
def value(*keys: str, default: Any = None) -> Any:
|
||||
for key in keys:
|
||||
translated = case.get(f"{key}_text")
|
||||
if translated not in (None, ""):
|
||||
return translated
|
||||
candidate = case.get(key)
|
||||
if candidate not in (None, ""):
|
||||
return candidate
|
||||
return default
|
||||
|
||||
def present_date(raw: Any) -> Any:
|
||||
return None if raw in (None, "", 0, "0", "0000-00-00") else raw
|
||||
|
||||
gender = case.get("gender", source.get("gender"))
|
||||
gender_label = "男" if gender in (1, "1") else "女" if gender in (0, "0", 2, "2") else "—"
|
||||
marital = {0: "未婚", 1: "已婚", 2: "离异"}.get(_int(case.get("marital_status"), -1), "—")
|
||||
gender_label = (
|
||||
case.get("gender_text")
|
||||
or case.get("gender_desc")
|
||||
or ("男" if gender in (1, "1") else "女" if gender in (0, "0", 2, "2") else "—")
|
||||
)
|
||||
marital = case.get("marital_status_text") or {0: "未婚", 1: "已婚", 2: "离异"}.get(
|
||||
_int(case.get("marital_status"), -1), "—"
|
||||
)
|
||||
age = case.get("age") or source.get("age")
|
||||
age_text = f"{age}岁" if age not in (None, "") and not str(age).endswith("岁") else age
|
||||
diabetes_history = value("diabetes_discovery_year")
|
||||
if diabetes_history not in (None, "") and str(diabetes_history).strip().isdigit():
|
||||
diabetes_history = f"{str(diabetes_history).strip()}年"
|
||||
blood_pressure = "—"
|
||||
if case.get("systolic_pressure") not in (None, "") or case.get("diastolic_pressure") not in (
|
||||
if case.get("systolic_pressure") not in (None, "") and case.get("diastolic_pressure") not in (
|
||||
None,
|
||||
"",
|
||||
):
|
||||
@@ -2465,102 +2507,173 @@ def render_case_record_html(prescription: Any) -> str:
|
||||
f"{display_text(case.get('diastolic_pressure'))} mmHg"
|
||||
)
|
||||
|
||||
def section(title: str, fields: Sequence[tuple[str, Any]]) -> str:
|
||||
rows = "".join(
|
||||
f'<div class="item"><span class="label">{html.escape(label)}</span>'
|
||||
f'<span class="value">{esc(value)}</span></div>'
|
||||
for label, value in fields
|
||||
diagnosis_type = value("diagnosis_type_desc", "diagnosis_type")
|
||||
diagnosis_type = _DIAGNOSIS_TYPE_LABELS.get(str(diagnosis_type or "").strip(), diagnosis_type)
|
||||
|
||||
def field_cell(label: str, field_value: Any, *, colspan: int = 1) -> str:
|
||||
return (
|
||||
f'<td class="cr-item" colspan="{colspan}">'
|
||||
'<table class="cr-kv" width="100%" cellspacing="0" cellpadding="0"><tr>'
|
||||
f'<td class="cr-label">{html.escape(label)}</td>'
|
||||
f'<td class="cr-value">{esc(field_value)}</td>'
|
||||
"</tr></table></td>"
|
||||
)
|
||||
|
||||
def section(
|
||||
title: str,
|
||||
fields: Sequence[tuple[str, Any, bool]],
|
||||
) -> str:
|
||||
rows: list[str] = []
|
||||
pending: list[str] = []
|
||||
for label, field_value, full_width in fields:
|
||||
if full_width:
|
||||
if pending:
|
||||
pending.extend('<td class="cr-item empty"></td>' for _ in range(3 - len(pending)))
|
||||
rows.append(f'<tr>{"".join(pending)}</tr>')
|
||||
pending = []
|
||||
rows.append(f'<tr>{field_cell(label, field_value, colspan=3)}</tr>')
|
||||
continue
|
||||
pending.append(field_cell(label, field_value))
|
||||
if len(pending) == 3:
|
||||
rows.append(f'<tr>{"".join(pending)}</tr>')
|
||||
pending = []
|
||||
if pending:
|
||||
pending.extend('<td class="cr-item empty"></td>' for _ in range(3 - len(pending)))
|
||||
rows.append(f'<tr>{"".join(pending)}</tr>')
|
||||
return (
|
||||
'<table class="cr-section" width="100%" cellspacing="0" cellpadding="0">'
|
||||
f'<tr><td class="cr-section-title">{html.escape(title)}</td></tr>'
|
||||
'<tr><td><table class="cr-grid" width="100%" cellspacing="0" cellpadding="0">'
|
||||
f'{"".join(rows)}</table></td></tr>'
|
||||
'<tr><td class="cr-rule"></td></tr></table>'
|
||||
)
|
||||
return f'<section><h2>{html.escape(title)}</h2><div class="grid">{rows}</div></section>'
|
||||
|
||||
sections = [
|
||||
section(
|
||||
"基本信息",
|
||||
(
|
||||
("诊单ID", case.get("id") or case.get("diagnosis_id") or source.get("diagnosis_id")),
|
||||
("姓名", case.get("patient_name") or source.get("patient_name")),
|
||||
("身份证号", case.get("id_card")),
|
||||
("手机号", case.get("phone") or source.get("phone")),
|
||||
("性别", gender_label),
|
||||
("年龄", case.get("age") or source.get("age")),
|
||||
("婚姻状态", marital),
|
||||
("身高", f"{case.get('height')} cm" if case.get("height") not in (None, "") else None),
|
||||
("体重", f"{case.get('weight')} kg" if case.get("weight") not in (None, "") else None),
|
||||
("地区", case.get("region")),
|
||||
(
|
||||
"诊单ID",
|
||||
case.get("id") or case.get("diagnosis_id") or source.get("diagnosis_id"),
|
||||
False,
|
||||
),
|
||||
("姓名", case.get("patient_name") or source.get("patient_name"), False),
|
||||
("身份证号", case.get("id_card"), False),
|
||||
("手机号", case.get("phone") or source.get("phone"), False),
|
||||
("性别", gender_label, False),
|
||||
("年龄", age_text, False),
|
||||
("婚姻状态", marital, False),
|
||||
(
|
||||
"身高",
|
||||
f"{case.get('height')} cm" if case.get("height") not in (None, "") else None,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"体重",
|
||||
f"{case.get('weight')} kg" if case.get("weight") not in (None, "") else None,
|
||||
False,
|
||||
),
|
||||
("地区", case.get("region"), False),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"生命体征",
|
||||
(
|
||||
("血压", blood_pressure),
|
||||
("血压", blood_pressure, False),
|
||||
(
|
||||
"空腹血糖",
|
||||
f"{case.get('fasting_blood_sugar')} mmol/L"
|
||||
if case.get("fasting_blood_sugar") not in (None, "")
|
||||
else None,
|
||||
False,
|
||||
),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"主诉与诊断",
|
||||
"主诉",
|
||||
(
|
||||
("诊断日期", case.get("diagnosis_date")),
|
||||
("诊断类型", case.get("diagnosis_type_desc") or case.get("diagnosis_type")),
|
||||
("证型", case.get("syndrome_type_desc") or case.get("syndrome_type")),
|
||||
("糖尿病期数", case.get("diabetes_type_desc") or case.get("diabetes_type")),
|
||||
("糖尿病病史", case.get("diabetes_discovery_year")),
|
||||
("当地医院诊断", case.get("local_hospital_diagnosis")),
|
||||
("当地就诊医院", case.get("local_hospital_name")),
|
||||
("症状补充", case.get("symptoms") or case.get("clinical_diagnosis")),
|
||||
("诊断日期", present_date(value("diagnosis_date")), False),
|
||||
("诊断类型", diagnosis_type, False),
|
||||
("证型", value("syndrome_type_desc", "syndrome_type"), False),
|
||||
("糖尿病期数", value("diabetes_type_desc", "diabetes_type"), False),
|
||||
("发现糖尿病患病史", diabetes_history, False),
|
||||
("当地医院诊断", value("local_hospital_diagnosis"), True),
|
||||
("当地就诊医院", value("local_hospital_name"), True),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"现病史",
|
||||
(
|
||||
("口腔感觉", case.get("appetite")),
|
||||
("每日饮水量", case.get("water_intake")),
|
||||
("体重变化", case.get("weight_change")),
|
||||
("脂肪肝程度", case.get("fatty_liver_degree")),
|
||||
("饮食情况", case.get("diet_condition")),
|
||||
("肢体感觉", case.get("body_feeling")),
|
||||
("睡眠情况", case.get("sleep_condition")),
|
||||
("眼睛情况", case.get("eye_condition")),
|
||||
("头部感觉", case.get("head_feeling")),
|
||||
("出汗情况", case.get("sweat_condition")),
|
||||
("皮肤情况", case.get("skin_condition")),
|
||||
("小便情况", case.get("urine_condition")),
|
||||
("大便情况", case.get("stool_condition")),
|
||||
("腰肾情况", case.get("kidney_condition")),
|
||||
("口腔感觉", value("appetite"), True),
|
||||
("每日饮水量", value("water_intake"), True),
|
||||
("体重变化", value("weight_change"), True),
|
||||
("脂肪肝程度", value("fatty_liver_degree"), True),
|
||||
("饮食情况", value("diet_condition"), True),
|
||||
("肢体感觉", value("body_feeling"), True),
|
||||
("睡眠情况", value("sleep_condition"), True),
|
||||
("眼睛情况", value("eye_condition"), True),
|
||||
("头部感觉", value("head_feeling"), True),
|
||||
("出汗情况", value("sweat_condition"), True),
|
||||
("皮肤情况", value("skin_condition"), True),
|
||||
("小便情况", value("urine_condition"), True),
|
||||
("大便情况", value("stool_condition"), True),
|
||||
("腰肾情况", value("kidney_condition"), True),
|
||||
("其他补充", case.get("symptoms") or case.get("clinical_diagnosis"), True),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"病史与中医诊断",
|
||||
"既往史",
|
||||
(
|
||||
("既往史", case.get("past_history")),
|
||||
("外伤史", "有" if _bool(case.get("trauma_history")) else "无"),
|
||||
("手术史", "有" if _bool(case.get("surgery_history")) else "无"),
|
||||
("过敏史", "有" if _bool(case.get("allergy_history")) else "无"),
|
||||
("家族病史", "有" if _bool(case.get("family_history")) else "无"),
|
||||
("妊娠哺乳史", "有" if _bool(case.get("pregnancy_history")) else "无"),
|
||||
("舌象", case.get("tongue_coating") or case.get("tongue")),
|
||||
("脉象", case.get("pulse")),
|
||||
("治则", case.get("treatment_principle")),
|
||||
("医嘱", case.get("doctor_advice")),
|
||||
("", value("past_history"), True),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"其他病史",
|
||||
(
|
||||
("外伤史", "有" if _bool(case.get("trauma_history")) else "无", False),
|
||||
("手术史", "有" if _bool(case.get("surgery_history")) else "无", False),
|
||||
("过敏史", "有" if _bool(case.get("allergy_history")) else "无", False),
|
||||
("家族病史", "有" if _bool(case.get("family_history")) else "无", False),
|
||||
(
|
||||
"妊娠哺乳史",
|
||||
"有" if _bool(case.get("pregnancy_history")) else "无",
|
||||
False,
|
||||
),
|
||||
),
|
||||
),
|
||||
section(
|
||||
"诊断信息",
|
||||
(
|
||||
("舌象", case.get("tongue_coating") or case.get("tongue"), True),
|
||||
("脉象", case.get("pulse"), True),
|
||||
("治则", case.get("treatment_principle"), True),
|
||||
("医嘱", case.get("doctor_advice"), True),
|
||||
),
|
||||
),
|
||||
]
|
||||
return f"""
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
body {{ font-family:"Microsoft YaHei","PingFang SC",sans-serif; color:#17231d; margin:18px; }}
|
||||
h1 {{ text-align:center; font-size:22px; margin:0 0 18px; }}
|
||||
section {{ margin:0 0 16px; border:1px solid #bdc7c2; }}
|
||||
h2 {{ font-size:15px; margin:0; padding:8px 10px; background:#edf2ef; }}
|
||||
.grid {{ display:table; width:100%; }}
|
||||
.item {{ display:table-row; }}
|
||||
.label,.value {{ display:table-cell; padding:7px 10px; border-top:1px solid #d7dfdb; }}
|
||||
.label {{ width:120px; color:#65736c; font-weight:600; }}
|
||||
.value {{ line-height:1.6; }}
|
||||
</style></head><body><h1>详细病历</h1>{''.join(sections)}</body></html>
|
||||
body {{ margin:0; padding:12px; background:#eef1f5; color:#303133;
|
||||
font-family:"Microsoft YaHei","PingFang SC","Segoe UI",sans-serif;
|
||||
font-size:13px; }}
|
||||
.paper {{ width:100%; margin:0 auto; background:#ffffff; border:1px solid #dcdfe6; }}
|
||||
.paper-content {{ padding:26px 30px 34px; vertical-align:top; }}
|
||||
h1 {{ text-align:center; font-size:20px; font-weight:600; margin:0;
|
||||
padding:0 0 14px; border-bottom:2px solid #303133; }}
|
||||
.cr-section {{ margin-top:14px; border-collapse:collapse; }}
|
||||
.cr-section-title {{ padding:0 0 8px; color:#303133; font-size:15px; font-weight:600; }}
|
||||
.cr-grid {{ table-layout:fixed; border-collapse:collapse; }}
|
||||
.cr-item {{ width:33.33%; padding:4px 10px 5px 0; vertical-align:top; }}
|
||||
.cr-item.empty {{ padding:0; }}
|
||||
.cr-kv {{ border-collapse:collapse; table-layout:fixed; }}
|
||||
.cr-label {{ width:88px; padding:0 7px 0 0; color:#909399; white-space:nowrap;
|
||||
vertical-align:top; line-height:1.65; }}
|
||||
.cr-value {{ padding:0; color:#303133; vertical-align:top; line-height:1.65; }}
|
||||
.cr-rule {{ height:1px; border-bottom:1px solid #eeeeee; }}
|
||||
</style></head><body>
|
||||
<table class="paper" width="100%" cellspacing="0" cellpadding="0"><tr>
|
||||
<td class="paper-content"><h1>甄养堂 详细病历</h1>{''.join(sections)}</td>
|
||||
</tr></table></body></html>
|
||||
"""
|
||||
|
||||
|
||||
@@ -2568,18 +2681,20 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
"""Build the pharmacy-copy A4 slip used by preview, print and PDF export."""
|
||||
|
||||
source = _mapping(prescription)
|
||||
paper_width = "100%" if print_layout else "820px"
|
||||
paper_dimensions = 'width="100%"' if print_layout else 'width="820" height="1050"'
|
||||
body_padding = "0" if print_layout else "10px"
|
||||
body_background = "#ffffff" if print_layout else "#eef1f5"
|
||||
# consumer/prescription/index.vue uses a strict 210 x 297 mm sheet. At
|
||||
# Qt's 96 logical DPI this is 794 x 1123 px; keeping the HTML width fixed
|
||||
# prevents QTextDocument from stretching the medicine columns with the
|
||||
# containing dialog.
|
||||
paper_width = "100%" if print_layout else "794px"
|
||||
paper_dimensions = 'width="100%"' if print_layout else 'width="794" height="1123"'
|
||||
body_padding = "0" if print_layout else "12px"
|
||||
body_background = "#ffffff" if print_layout else "#f5f6f8"
|
||||
paper_border = "0" if print_layout else "1px solid #d6d6d6"
|
||||
base_font_size = "10px" if print_layout else "13px"
|
||||
notice_font_size = "8.5px" if print_layout else "11px"
|
||||
notice_font_size = "8.5px" if print_layout else "12px"
|
||||
rp_font_size = "12px" if print_layout else "16px"
|
||||
section_font_size = "9px" if print_layout else "12px"
|
||||
watermark_height = "58px" if print_layout else "76px"
|
||||
watermark_font_size = "41px" if print_layout else "54px"
|
||||
bottom_height = "44px" if print_layout else "58px"
|
||||
bottom_height = "44px" if print_layout else "70px"
|
||||
herbs = _herb_rows(prescription)
|
||||
dose_count = max(1, _int(source.get("dose_count"), 1))
|
||||
main = [row for row in herbs if _formula(row.get("formula_type")) == "主方"]
|
||||
@@ -2662,13 +2777,15 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
if not rows:
|
||||
return ""
|
||||
output = [
|
||||
f'<tr><td class="rp-indent"></td><td class="section {kind}" colspan="4">'
|
||||
f'<tr><td class="rp-indent"></td><td class="section {kind}" colspan="5">'
|
||||
f"{html.escape(title)}</td></tr>"
|
||||
]
|
||||
for index in range(0, len(rows), 2):
|
||||
pair = rows[index : index + 2]
|
||||
cells: list[str] = []
|
||||
for row in pair:
|
||||
for pair_index, row in enumerate(pair):
|
||||
if pair_index == 1:
|
||||
cells.append('<td class="rp-gap"></td>')
|
||||
dosage = number_text(row.get("dosage"))
|
||||
total = number_text(_float(row.get("dosage")) * dose_count)
|
||||
cells.extend(
|
||||
@@ -2678,7 +2795,13 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
)
|
||||
)
|
||||
if len(pair) == 1:
|
||||
cells.extend(('<td class="herb-name"> </td>', '<td class="herb-total"></td>'))
|
||||
cells.extend(
|
||||
(
|
||||
'<td class="rp-gap"></td>',
|
||||
'<td class="herb-name"> </td>',
|
||||
'<td class="herb-total"></td>',
|
||||
)
|
||||
)
|
||||
output.append(f'<tr><td class="rp-indent"></td>{"".join(cells)}</tr>')
|
||||
return "".join(output)
|
||||
|
||||
@@ -2740,7 +2863,6 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
pharmacy_remark = (
|
||||
source.get("pharmacy_remark")
|
||||
or source.get("pharmacy_note")
|
||||
or source.get("remark_extra")
|
||||
)
|
||||
|
||||
explicit_out = (
|
||||
@@ -2783,7 +2905,7 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
herb_html = herb_group(main, "主方", "main") + herb_group(aux, "辅方", "aux")
|
||||
if not herb_html:
|
||||
herb_html = (
|
||||
'<tr><td class="rp-indent"></td><td class="empty-herbs" colspan="4">'
|
||||
'<tr><td class="rp-indent"></td><td class="empty-herbs" colspan="5">'
|
||||
"暂无药材明细</td></tr>"
|
||||
)
|
||||
text_rows = [f"<p>主方服法:{esc(main_usage_text)}</p>"]
|
||||
@@ -2815,55 +2937,58 @@ def render_prescription_html(prescription: Any, *, print_layout: bool = False) -
|
||||
return f"""
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8"><style>
|
||||
body {{ margin:0; padding:{body_padding}; background:{body_background}; color:#151515;
|
||||
font-family:"Microsoft YaHei","PingFang SC","Segoe UI",sans-serif;
|
||||
font-size:{base_font_size}; }}
|
||||
body {{ margin:0; padding:{body_padding}; background:{body_background}; color:#1f1f1f;
|
||||
font-family:"Microsoft YaHei","PingFang SC",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||
font-size:{base_font_size}; line-height:1.5; }}
|
||||
.paper {{ width:{paper_width}; margin:0 auto; background:#ffffff;
|
||||
border:{paper_border}; border-collapse:separate; border-spacing:0; }}
|
||||
border:{paper_border}; border-collapse:separate; border-spacing:0;
|
||||
box-shadow:{"none" if print_layout else "0 0 0 1px #d6d6d6"}; }}
|
||||
.paper-content {{ padding:8mm 10mm; vertical-align:top; }}
|
||||
.notice {{ width:100%; border:1px solid #e5e7eb; border-collapse:collapse;
|
||||
table-layout:fixed; background:#f3f4f6; margin:0 0 6px;
|
||||
font-size:{notice_font_size}; }}
|
||||
font-size:{notice_font_size}; color:#1f1f1f; }}
|
||||
.notice td {{ border:0; padding:6px 10px; }}
|
||||
.notice-text {{ width:49%; }}
|
||||
.notice-meta {{ width:51%; text-align:right; }}
|
||||
.notice-meta {{ width:51%; text-align:right; white-space:nowrap; }}
|
||||
.info {{ width:100%; border-collapse:collapse; table-layout:fixed; }}
|
||||
.info td {{ border:1px solid #c8c8c8; padding:6px 10px; vertical-align:middle; }}
|
||||
.info td {{ border:1px solid #c8c8c8; padding:6px 10px; vertical-align:middle; font-size:13px; }}
|
||||
.info .full {{ border-top:0; }}
|
||||
.key {{ white-space:nowrap; }}
|
||||
.rp {{ width:100%; border-collapse:collapse; table-layout:fixed;
|
||||
border-left:1px solid #c8c8c8; border-right:1px solid #c8c8c8; }}
|
||||
.rp td {{ border:0; padding:3px 6px; vertical-align:middle; }}
|
||||
.rp .rp-head td {{ border-bottom:1px solid #d4d4d4; padding-top:9px; padding-bottom:5px; }}
|
||||
.rp-indent {{ width:34px; }}
|
||||
.rp-mark {{ width:34px; font-size:{rp_font_size}; font-weight:700; }}
|
||||
.key {{ white-space:nowrap; color:#1f1f1f; }}
|
||||
.rp-frame {{ width:100%; border-collapse:collapse; table-layout:fixed;
|
||||
border-left:1px solid #c8c8c8; border-right:1px solid #c8c8c8; }}
|
||||
.rp-padding {{ padding:8px 10px 16px; }}
|
||||
.rp {{ width:100%; border-collapse:collapse; table-layout:fixed; }}
|
||||
.rp td {{ border:0; padding:3px 0; vertical-align:middle; }}
|
||||
.rp .rp-head td {{ border-bottom:1px solid #d4d4d4; padding-top:4px; padding-bottom:4px; }}
|
||||
.rp-indent {{ width:44px; }}
|
||||
.rp-mark {{ width:44px; padding-right:8px !important;
|
||||
font-size:{rp_font_size}; font-weight:700; color:#1f1f1f; }}
|
||||
.drug-head {{ color:#1f1f1f; }}
|
||||
.total-head, .herb-total {{ width:62px; text-align:right; white-space:nowrap; }}
|
||||
.total-head, .herb-total {{ width:64px; text-align:right; white-space:nowrap;
|
||||
font-variant-numeric:tabular-nums; }}
|
||||
.rp-gap {{ width:24px; padding:0 !important; }}
|
||||
.section {{ padding-top:7px !important; padding-bottom:1px !important;
|
||||
font-size:{section_font_size}; font-weight:600; }}
|
||||
.section.main {{ color:#409eff; }}
|
||||
.section.aux {{ color:#e6a23c; }}
|
||||
.herb-name {{ line-height:1.65; white-space:nowrap; }}
|
||||
.herb-name {{ line-height:1.85; white-space:nowrap; color:#1f1f1f; }}
|
||||
.empty-herbs {{ color:#8a8f98; padding:18px 6px !important; text-align:center; }}
|
||||
.watermark {{ height:{watermark_height}; color:#eeeeee; text-align:center;
|
||||
font-size:{watermark_font_size};
|
||||
font-weight:700; letter-spacing:18px; vertical-align:middle !important; }}
|
||||
.rx-text {{ border:1px solid #c8c8c8; border-top:0; padding:9px 12px; line-height:1.65; }}
|
||||
.rx-text p {{ margin:1px 0; padding:0; }}
|
||||
.rx-text {{ border:1px solid #c8c8c8; border-top:0; padding:10px 12px; line-height:1.85; font-size:13px; }}
|
||||
.rx-text p {{ margin:0; padding:0; }}
|
||||
.warning {{ color:#d72424; font-weight:600; }}
|
||||
.bottom {{ width:100%; border-collapse:collapse; table-layout:fixed; }}
|
||||
.bottom td {{ border:1px solid #c8c8c8; border-top:0; padding:7px 10px;
|
||||
height:{bottom_height}; vertical-align:middle; }}
|
||||
.bottom .doctor {{ width:22%; vertical-align:top; }}
|
||||
.doctor-title {{ display:block; margin-bottom:4px; }}
|
||||
.doctor-name {{ display:block; margin-top:8px; }}
|
||||
.signature {{ max-width:130px; max-height:42px; vertical-align:middle; }}
|
||||
.meta-key {{ white-space:nowrap; }}
|
||||
.audit {{ width:{paper_width}; margin:8px auto 0; padding:8px 4px; color:#7b8492;
|
||||
border-top:1px dashed #cfd5dd; font-size:12px; line-height:1.7; }}
|
||||
.bottom td {{ border:1px solid #c8c8c8; border-top:0; padding:6px 10px;
|
||||
height:{bottom_height}; vertical-align:middle; font-size:12px; color:#1f1f1f; }}
|
||||
.bottom .doctor {{ width:28%; vertical-align:top; }}
|
||||
.doctor-title {{ display:block; margin-bottom:4px; font-size:13px; }}
|
||||
.doctor-name {{ display:block; margin-top:8px; font-size:13px; }}
|
||||
.signature {{ max-width:110px; max-height:40px; vertical-align:middle; object-fit:contain; }}
|
||||
.meta-key {{ white-space:nowrap; margin-right:6px; }}
|
||||
.audit {{ width:{paper_width}; margin:12px auto 0; padding:8px 12px; color:#6b7280;
|
||||
border:1px dashed #d4d4d4; background:#fafafa; border-radius:4px;
|
||||
font-size:12px; line-height:1.6; }}
|
||||
</style></head><body>
|
||||
<table class="paper" {paper_dimensions}><tr><td class="paper-content" valign="top">
|
||||
<table width="100%"><tr><td height="14"></td></tr></table>
|
||||
<table align="center" class="paper" {paper_dimensions}><tr><td class="paper-content" valign="top">
|
||||
<table class="notice" width="100%"><tr>
|
||||
<td class="notice-text">服药前请核对姓名、电话、医生等信息以及服法、医嘱等要点</td>
|
||||
<td class="notice-meta">日期:{esc(date_text())} 编号:{esc(serial)}</td>
|
||||
@@ -2876,13 +3001,15 @@ body {{ margin:0; padding:{body_padding}; background:{body_background}; color:#1
|
||||
<tr><td class="full" colspan="4"><span class="key">收件信息</span> {esc(recipient_text)}</td></tr>
|
||||
<tr><td class="full" colspan="4"><span class="key">临床诊断</span> {esc(source.get("clinical_diagnosis"))}</td></tr>
|
||||
</table>
|
||||
<table class="rp-frame" title="药房联" width="100%"><tr><td class="rp-padding">
|
||||
<table class="rp" width="100%">
|
||||
<col width="34"/><col/><col width="62"/><col/><col width="62"/>
|
||||
<col width="44"/><col/><col width="64"/><col width="24"/><col/><col width="64"/>
|
||||
<tr class="rp-head"><td class="rp-mark">Rp.</td><td class="drug-head">用药 (单剂)</td>
|
||||
<td class="total-head">总量</td><td class="drug-head">用药 (单剂)</td><td class="total-head">总量</td></tr>
|
||||
<td class="total-head">总量</td><td class="rp-gap"></td>
|
||||
<td class="drug-head">用药 (单剂)</td><td class="total-head">总量</td></tr>
|
||||
{herb_html}
|
||||
<tr><td class="rp-indent"></td><td class="watermark" colspan="4">药房联</td></tr>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
<div class="rx-text">{"".join(text_rows)}</div>
|
||||
<table class="bottom" width="100%"><tr>
|
||||
<td class="doctor"><span class="doctor-title">医师</span>{signature_html}</td>
|
||||
@@ -2896,6 +3023,57 @@ body {{ margin:0; padding:{body_padding}; background:{body_background}; color:#1
|
||||
"""
|
||||
|
||||
|
||||
class _PrescriptionPaperPreview(QTextBrowser):
|
||||
"""A4 prescription preview with the consumer page's floating watermark.
|
||||
|
||||
Qt rich text cannot paint CSS absolute-positioned or transformed elements,
|
||||
so placing the watermark in the HTML makes it consume a table row and
|
||||
deforms the paper. Paint it over the viewport at the corresponding
|
||||
document position instead.
|
||||
"""
|
||||
|
||||
_PAPER_WIDTH = 794
|
||||
|
||||
def __init__(self, prescription: Any, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
herbs = _herb_rows(prescription)
|
||||
main_count = sum(
|
||||
1 for row in herbs if _formula(row.get("formula_type")) == "主方"
|
||||
)
|
||||
aux_count = len(herbs) - main_count
|
||||
medicine_rows = (main_count + 1) // 2 + (aux_count + 1) // 2
|
||||
section_count = int(main_count > 0) + int(aux_count > 0)
|
||||
rp_height = 65 + medicine_rows * 30 + section_count * 22
|
||||
self._watermark_document_y = 150 + rp_height // 2
|
||||
|
||||
def paintEvent(self, event: Any) -> None: # noqa: N802 - Qt override
|
||||
super().paintEvent(event)
|
||||
viewport = self.viewport()
|
||||
center_y = self._watermark_document_y - self.verticalScrollBar().value()
|
||||
if center_y < -90 or center_y > viewport.height() + 90:
|
||||
return
|
||||
paper_width = min(self._PAPER_WIDTH, viewport.width())
|
||||
paper_left = max(0.0, (viewport.width() - paper_width) / 2)
|
||||
painter = QPainter(viewport)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.translate(paper_left + paper_width / 2, center_y)
|
||||
painter.rotate(-22)
|
||||
painter.setPen(QColor(31, 31, 31, 15))
|
||||
font = QFont(
|
||||
"Microsoft YaHei UI",
|
||||
-1,
|
||||
QFont.Weight.Bold,
|
||||
)
|
||||
font.setPixelSize(84)
|
||||
painter.setFont(font)
|
||||
painter.drawText(
|
||||
QRectF(-230, -70, 460, 140),
|
||||
Qt.AlignmentFlag.AlignCenter,
|
||||
"药房联",
|
||||
)
|
||||
painter.end()
|
||||
|
||||
|
||||
class PrescriptionDetailDialog(QDialog):
|
||||
diagnosis_requested = Signal(int)
|
||||
orders_requested = Signal(int)
|
||||
@@ -2937,12 +3115,12 @@ class PrescriptionDetailDialog(QDialog):
|
||||
pdf_button.clicked.connect(self.choose_pdf_path)
|
||||
actions.addWidget(pdf_button)
|
||||
root.addLayout(actions)
|
||||
self.preview = QTextBrowser()
|
||||
self.preview = _PrescriptionPaperPreview(prescription)
|
||||
self.preview.setObjectName("PrescriptionPaperPreview")
|
||||
self.preview.setOpenExternalLinks(False)
|
||||
self.preview.setStyleSheet(
|
||||
"QTextBrowser#PrescriptionPaperPreview {"
|
||||
"background-color:#EEF1F5; border:1px solid #D8DEE8; padding:0;}"
|
||||
"background-color:#F5F6F8; border:1px solid #D8DEE8; padding:0;}"
|
||||
)
|
||||
self.preview.setDocument(self.document)
|
||||
self.tabs = QTabWidget()
|
||||
@@ -2952,8 +3130,15 @@ class PrescriptionDetailDialog(QDialog):
|
||||
self.case_preview: QTextBrowser | None = None
|
||||
if case_record:
|
||||
self.case_document = QTextDocument(self)
|
||||
self.case_document.setDocumentMargin(0)
|
||||
self.case_document.setHtml(render_case_record_html(prescription))
|
||||
self.case_preview = QTextBrowser()
|
||||
self.case_preview.setObjectName("CaseRecordPaperPreview")
|
||||
self.case_preview.setOpenExternalLinks(False)
|
||||
self.case_preview.setStyleSheet(
|
||||
"QTextBrowser#CaseRecordPaperPreview {"
|
||||
"background-color:#EEF1F5; border:1px solid #D8DEE8; padding:0;}"
|
||||
)
|
||||
self.case_preview.setDocument(self.case_document)
|
||||
case_index = self.tabs.addTab(self.case_preview, "详细病历")
|
||||
if initial_tab == "case":
|
||||
|
||||
@@ -8,8 +8,8 @@ from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import Qt, QTimer, QUrl, Signal
|
||||
from PySide6.QtGui import QDesktopServices, QPixmap, QTextCursor
|
||||
from PySide6.QtCore import QSize, Qt, QTimer, QUrl, Signal
|
||||
from PySide6.QtGui import QDesktopServices, QIcon, QPixmap, QTextCursor
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
@@ -139,6 +139,73 @@ def _is_local_material_reference(value: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
class _NoteAttachmentPreview(QPushButton):
|
||||
"""Responsive inline thumbnail that remains clickable for a full preview."""
|
||||
|
||||
_THUMBNAIL_HEIGHT = 132
|
||||
|
||||
def __init__(self, caption: str, parent: QWidget | None = None) -> None:
|
||||
super().__init__("正在加载图片…", parent)
|
||||
self._source_pixmap = QPixmap()
|
||||
self.setObjectName("NoteAttachmentPreview")
|
||||
self.setProperty("variant", "secondary")
|
||||
self.setProperty("loadState", "loading")
|
||||
self.setAccessibleName(f"查看{caption}大图")
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.setMinimumWidth(168)
|
||||
self.setFixedHeight(self._THUMBNAIL_HEIGHT)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
def _refresh_preview(self) -> None:
|
||||
if self._source_pixmap.isNull():
|
||||
return
|
||||
available = QSize(max(120, self.width() - 12), self._THUMBNAIL_HEIGHT - 12)
|
||||
rendered = self._source_pixmap.scaled(
|
||||
available,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation,
|
||||
)
|
||||
self.setIcon(QIcon(rendered))
|
||||
self.setIconSize(rendered.size())
|
||||
|
||||
def show_loading(self) -> None:
|
||||
self._source_pixmap = QPixmap()
|
||||
self.setIcon(QIcon())
|
||||
self.setText("正在加载图片…")
|
||||
self.setProperty("loadState", "loading")
|
||||
self.setEnabled(False)
|
||||
|
||||
def show_preview(self, pixmap: QPixmap) -> None:
|
||||
"""Render the decoded image directly in the note card."""
|
||||
|
||||
self._source_pixmap = pixmap.scaled(
|
||||
QSize(960, 720),
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation,
|
||||
)
|
||||
self.setText("")
|
||||
self._refresh_preview()
|
||||
self.setProperty("loadState", "ready")
|
||||
self.setEnabled(True)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
def show_failure(self) -> None:
|
||||
"""Keep a failed attachment actionable so the user can retry it."""
|
||||
|
||||
self._source_pixmap = QPixmap()
|
||||
self.setIcon(QIcon())
|
||||
self.setText("图片加载失败\n点击重试")
|
||||
self.setProperty("loadState", "failed")
|
||||
self.setEnabled(True)
|
||||
self.style().unpolish(self)
|
||||
self.style().polish(self)
|
||||
|
||||
def resizeEvent(self, event: Any) -> None: # noqa: N802 - Qt override
|
||||
super().resizeEvent(event)
|
||||
self._refresh_preview()
|
||||
|
||||
|
||||
class QueueRow(QWidget):
|
||||
"""Compact appointment summary rendered inside the queue list."""
|
||||
|
||||
@@ -232,6 +299,7 @@ class ReceptionPage(QWidget):
|
||||
self._pending_report_files: list[str] = []
|
||||
self._note_busy = False
|
||||
self._attachment_preview_generation = 0
|
||||
self._attachment_render_generation = 0
|
||||
|
||||
self._can_complete = has_permission(self.permissions, "doctor.appointment/complete")
|
||||
self._can_note = has_permission(self.permissions, "doctor.appointment/addDoctorNote")
|
||||
@@ -1238,6 +1306,8 @@ class ReceptionPage(QWidget):
|
||||
self.daily_text.setText("\n".join(lines) if lines else "近 30 日暂无日常记录。")
|
||||
|
||||
def _render_notes(self, notes: list[Any]) -> None:
|
||||
self._attachment_render_generation += 1
|
||||
render_generation = self._attachment_render_generation
|
||||
clear_layout(self.notes_layout)
|
||||
self.notes_count.setText(f"{len(notes)} 条")
|
||||
if not notes:
|
||||
@@ -1262,44 +1332,159 @@ class ReceptionPage(QWidget):
|
||||
meta.setProperty("role", "muted")
|
||||
layout.addWidget(meta)
|
||||
note_id = _as_int(first_value(note, "id", "note_id", default=None))
|
||||
image_attachments: list[tuple[str, str, str]] = []
|
||||
file_attachments: list[tuple[str, str, str]] = []
|
||||
for image_type, caption in (
|
||||
("tongue_images", "舌苔图"),
|
||||
("report_files", "检查报告"),
|
||||
):
|
||||
for path in _sequence(first_value(note, image_type, default=[])):
|
||||
path_text = str(path).strip()
|
||||
attachment = QWidget()
|
||||
attachment_layout = QHBoxLayout(attachment)
|
||||
attachment_layout.setContentsMargins(0, 0, 0, 0)
|
||||
label = QLabel(f"{caption}:{_attachment_name(path_text)}")
|
||||
label.setToolTip(path_text)
|
||||
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
attachment_layout.addWidget(label, 1)
|
||||
preview_button = QPushButton(
|
||||
"预览" if _is_image_attachment(path_text) else "打开"
|
||||
)
|
||||
preview_button.setProperty("variant", "secondary")
|
||||
preview_button.setEnabled(bool(path_text))
|
||||
preview_button.clicked.connect(
|
||||
lambda _checked=False, current_path=path_text, button=preview_button: (
|
||||
attachment = (image_type, caption, path_text)
|
||||
if _is_image_attachment(path_text):
|
||||
image_attachments.append(attachment)
|
||||
else:
|
||||
file_attachments.append(attachment)
|
||||
|
||||
if image_attachments:
|
||||
image_title = QLabel("图片附件 · 点击图片查看大图")
|
||||
image_title.setProperty("role", "muted")
|
||||
layout.addWidget(image_title)
|
||||
image_host = QWidget()
|
||||
image_grid = QGridLayout(image_host)
|
||||
image_grid.setContentsMargins(0, 0, 0, 0)
|
||||
image_grid.setHorizontalSpacing(10)
|
||||
image_grid.setVerticalSpacing(10)
|
||||
for index, (image_type, caption, path_text) in enumerate(
|
||||
image_attachments
|
||||
):
|
||||
tile = QFrame()
|
||||
tile.setObjectName("NoteAttachmentTile")
|
||||
tile_layout = QVBoxLayout(tile)
|
||||
tile_layout.setContentsMargins(8, 8, 8, 8)
|
||||
tile_layout.setSpacing(6)
|
||||
preview = _NoteAttachmentPreview(caption)
|
||||
preview.setToolTip(f"{caption}:{path_text}\n点击查看大图")
|
||||
preview.clicked.connect(
|
||||
lambda _checked=False, current_path=path_text, button=preview: (
|
||||
self._preview_note_attachment(current_path, button)
|
||||
)
|
||||
)
|
||||
attachment_layout.addWidget(preview_button)
|
||||
tile_layout.addWidget(preview)
|
||||
footer = QHBoxLayout()
|
||||
footer.setSpacing(6)
|
||||
label = QLabel(f"{caption} · {_attachment_name(path_text)}")
|
||||
label.setObjectName("NoteAttachmentName")
|
||||
label.setToolTip(path_text)
|
||||
label.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
label.setMinimumWidth(0)
|
||||
label.setSizePolicy(
|
||||
QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred
|
||||
)
|
||||
footer.addWidget(label, 1)
|
||||
if self._can_note and note_id is not None:
|
||||
delete_button = QPushButton("删除")
|
||||
delete_button.setProperty("variant", "secondary")
|
||||
delete_button.clicked.connect(
|
||||
lambda _checked=False, current_note_id=note_id, current_type=image_type, current_path=str(path), button=delete_button: (
|
||||
lambda _checked=False, current_note_id=note_id, current_type=image_type, current_path=path_text, button=delete_button: (
|
||||
self._delete_note_attachment(
|
||||
current_note_id, current_type, current_path, button
|
||||
)
|
||||
)
|
||||
)
|
||||
attachment_layout.addWidget(delete_button)
|
||||
layout.addWidget(attachment)
|
||||
footer.addWidget(delete_button)
|
||||
tile_layout.addLayout(footer)
|
||||
row, column = divmod(index, 3)
|
||||
image_grid.addWidget(tile, row, column)
|
||||
self._load_note_thumbnail(path_text, preview, render_generation)
|
||||
for column in range(3):
|
||||
image_grid.setColumnStretch(column, 1)
|
||||
layout.addWidget(image_host)
|
||||
|
||||
for image_type, caption, path_text in file_attachments:
|
||||
attachment = QWidget()
|
||||
attachment_layout = QHBoxLayout(attachment)
|
||||
attachment_layout.setContentsMargins(0, 0, 0, 0)
|
||||
label = QLabel(f"{caption}:{_attachment_name(path_text)}")
|
||||
label.setToolTip(path_text)
|
||||
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
attachment_layout.addWidget(label, 1)
|
||||
open_button = QPushButton("打开")
|
||||
open_button.setProperty("variant", "secondary")
|
||||
open_button.setEnabled(bool(path_text))
|
||||
open_button.clicked.connect(
|
||||
lambda _checked=False, current_path=path_text, button=open_button: (
|
||||
self._preview_note_attachment(current_path, button)
|
||||
)
|
||||
)
|
||||
attachment_layout.addWidget(open_button)
|
||||
if self._can_note and note_id is not None:
|
||||
delete_button = QPushButton("删除")
|
||||
delete_button.setProperty("variant", "secondary")
|
||||
delete_button.clicked.connect(
|
||||
lambda _checked=False, current_note_id=note_id, current_type=image_type, current_path=path_text, button=delete_button: (
|
||||
self._delete_note_attachment(
|
||||
current_note_id, current_type, current_path, button
|
||||
)
|
||||
)
|
||||
)
|
||||
attachment_layout.addWidget(delete_button)
|
||||
layout.addWidget(attachment)
|
||||
self.notes_layout.addWidget(card)
|
||||
|
||||
def _load_note_thumbnail(
|
||||
self,
|
||||
path: str,
|
||||
preview: _NoteAttachmentPreview,
|
||||
generation: int,
|
||||
) -> None:
|
||||
"""Download and paint a note image without opening another window."""
|
||||
|
||||
download = getattr(self.repository, "download_public_image", None)
|
||||
if not callable(download):
|
||||
preview.show_failure()
|
||||
return
|
||||
run_async(
|
||||
lambda: invoke(self.repository, "download_public_image", url=path),
|
||||
on_success=lambda payload: self._apply_note_thumbnail(
|
||||
payload, preview, generation
|
||||
),
|
||||
on_error=lambda _error: self._fail_note_thumbnail(preview, generation),
|
||||
)
|
||||
|
||||
def _apply_note_thumbnail(
|
||||
self,
|
||||
payload: Any,
|
||||
preview: _NoteAttachmentPreview,
|
||||
generation: int,
|
||||
) -> None:
|
||||
if generation != self._attachment_render_generation:
|
||||
return
|
||||
content = bytes(payload or b"")
|
||||
pixmap = QPixmap()
|
||||
if (
|
||||
not content
|
||||
or len(content) > 10 * 1024 * 1024
|
||||
or not pixmap.loadFromData(content)
|
||||
or pixmap.isNull()
|
||||
):
|
||||
self._fail_note_thumbnail(preview, generation)
|
||||
return
|
||||
with suppress(RuntimeError):
|
||||
preview.show_preview(pixmap)
|
||||
|
||||
def _fail_note_thumbnail(
|
||||
self,
|
||||
preview: _NoteAttachmentPreview,
|
||||
generation: int,
|
||||
) -> None:
|
||||
if generation != self._attachment_render_generation:
|
||||
return
|
||||
with suppress(RuntimeError):
|
||||
preview.show_failure()
|
||||
|
||||
def _preview_note_attachment(self, path: str, button: QPushButton) -> None:
|
||||
"""Preview note images in-app and open non-image reports safely."""
|
||||
|
||||
@@ -1325,8 +1510,11 @@ class ReceptionPage(QWidget):
|
||||
return
|
||||
self._attachment_preview_generation += 1
|
||||
generation = self._attachment_preview_generation
|
||||
button.setEnabled(False)
|
||||
button.setText("加载中…")
|
||||
if isinstance(button, _NoteAttachmentPreview):
|
||||
button.show_loading()
|
||||
else:
|
||||
button.setEnabled(False)
|
||||
button.setText("加载中…")
|
||||
run_async(
|
||||
lambda: invoke(self.repository, "download_public_image", url=target),
|
||||
on_success=lambda payload: self._show_note_image_preview(
|
||||
@@ -1346,13 +1534,21 @@ class ReceptionPage(QWidget):
|
||||
) -> None:
|
||||
if generation != self._attachment_preview_generation:
|
||||
return
|
||||
button.setEnabled(True)
|
||||
button.setText("预览")
|
||||
content = bytes(payload or b"")
|
||||
pixmap = QPixmap()
|
||||
if not content or len(content) > 10 * 1024 * 1024 or not pixmap.loadFromData(content):
|
||||
if isinstance(button, _NoteAttachmentPreview):
|
||||
button.show_failure()
|
||||
else:
|
||||
button.setEnabled(True)
|
||||
button.setText("预览")
|
||||
show_toast(self, "服务器返回的图片无法预览。", "danger", 4600)
|
||||
return
|
||||
if isinstance(button, _NoteAttachmentPreview):
|
||||
button.show_preview(pixmap)
|
||||
else:
|
||||
button.setEnabled(True)
|
||||
button.setText("预览")
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle(f"预览 · {_attachment_name(path)}")
|
||||
dialog.setModal(True)
|
||||
@@ -1387,8 +1583,11 @@ class ReceptionPage(QWidget):
|
||||
) -> None:
|
||||
if generation != self._attachment_preview_generation:
|
||||
return
|
||||
button.setEnabled(True)
|
||||
button.setText("预览")
|
||||
if isinstance(button, _NoteAttachmentPreview):
|
||||
button.show_failure()
|
||||
else:
|
||||
button.setEnabled(True)
|
||||
button.setText("预览")
|
||||
show_toast(self, f"图片预览失败:{friendly_error(error)}", "danger", 5200)
|
||||
|
||||
def _reset_detail_content(self, seed: Any = None) -> None:
|
||||
|
||||
@@ -205,6 +205,33 @@ QPushButton[variant="secondary"]:checked {
|
||||
border-color: $indigo_pressed;
|
||||
}
|
||||
|
||||
QFrame#NoteAttachmentTile {
|
||||
background-color: $surface;
|
||||
border: 1px solid $line;
|
||||
border-radius: 10px;
|
||||
}
|
||||
QPushButton#NoteAttachmentPreview {
|
||||
min-height: 132px;
|
||||
padding: 6px;
|
||||
color: $muted;
|
||||
background-color: $surface_alt;
|
||||
border: 1px solid $line;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton#NoteAttachmentPreview:hover {
|
||||
background-color: $indigo_pale;
|
||||
border-color: $indigo_hover;
|
||||
}
|
||||
QPushButton#NoteAttachmentPreview[loadState="failed"] {
|
||||
color: $danger;
|
||||
background-color: $danger_pale;
|
||||
}
|
||||
QLabel#NoteAttachmentName {
|
||||
color: $text_soft;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
QPushButton[variant="danger"] {
|
||||
color: $danger;
|
||||
background-color: $danger_pale;
|
||||
|
||||
@@ -423,6 +423,8 @@ def test_order_payload_and_a4_print_document(
|
||||
assert "黄芪" in rendered
|
||||
assert "酸枣仁" in rendered
|
||||
assert "服药前请核对姓名、电话、医生等信息" in rendered
|
||||
assert '<table align="center" class="paper" width="794" height="1123"' in rendered
|
||||
assert '<col width="44"/><col/><col width="64"/><col width="24"/>' in rendered
|
||||
assert "Rp." in rendered
|
||||
assert "药房联" in rendered
|
||||
assert "主方" in rendered
|
||||
@@ -433,8 +435,9 @@ def test_order_payload_and_a4_print_document(
|
||||
assert "每天2次, 一次2袋, 每袋5g, 温水送服, 饭后" in rendered
|
||||
viewer = PrescriptionDetailDialog(prescription)
|
||||
document_html = viewer.document.toHtml()
|
||||
assert "药房联" in document_html
|
||||
assert "用药 (单剂)" in document_html
|
||||
assert viewer.document.idealWidth() == pytest.approx(794.0)
|
||||
assert viewer.preview.__class__.__name__ == "_PrescriptionPaperPreview"
|
||||
viewer.close()
|
||||
order.close()
|
||||
application.processEvents()
|
||||
@@ -450,6 +453,7 @@ def test_prescription_detail_can_open_immutable_case_record_tab(
|
||||
"case_record": {
|
||||
"patient_name": "林晓岚",
|
||||
"phone": "13800138000",
|
||||
"diagnosis_type": "follow_up",
|
||||
"local_hospital_name": "市中医院",
|
||||
"symptoms": "口渴乏力",
|
||||
"tongue_coating": "舌红少苔",
|
||||
@@ -461,8 +465,15 @@ def test_prescription_detail_can_open_immutable_case_record_tab(
|
||||
assert viewer.tabs.tabText(viewer.tabs.currentIndex()) == "详细病历"
|
||||
assert viewer.case_document is not None
|
||||
case_html = viewer.case_document.toHtml()
|
||||
source_html = dialog_module.render_case_record_html(prescription)
|
||||
assert "甄养堂 详细病历" in case_html
|
||||
assert '<table class="cr-grid"' in source_html
|
||||
assert "follow_up" not in case_html
|
||||
assert "复诊" in case_html
|
||||
assert "市中医院" in case_html
|
||||
assert "口渴乏力" in case_html
|
||||
assert viewer.case_preview is not None
|
||||
assert viewer.case_preview.objectName() == "CaseRecordPaperPreview"
|
||||
viewer.close()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
@@ -69,8 +69,9 @@ def test_note_attachment_preview_type_is_extension_aware(path: str, expected: bo
|
||||
assert _is_image_attachment(path) is expected
|
||||
|
||||
|
||||
def test_note_attachments_offer_image_preview_and_file_open(
|
||||
def test_note_attachments_expand_images_and_keep_file_open(
|
||||
application: QApplication,
|
||||
immediate_async: None,
|
||||
) -> None:
|
||||
page = ReceptionPage(DemoDoctorRepository(), PermissionSet([]))
|
||||
page._render_notes(
|
||||
@@ -87,8 +88,13 @@ def test_note_attachments_offer_image_preview_and_file_open(
|
||||
]
|
||||
)
|
||||
|
||||
previews = page.notes_container.findChildren(
|
||||
QPushButton, "NoteAttachmentPreview"
|
||||
)
|
||||
assert len(previews) == 2
|
||||
assert all(button.text() == "" for button in previews)
|
||||
assert all(not button.icon().isNull() for button in previews)
|
||||
labels = [button.text() for button in page.notes_container.findChildren(QPushButton)]
|
||||
assert labels.count("预览") == 2
|
||||
assert labels.count("打开") == 1
|
||||
page.close()
|
||||
application.processEvents()
|
||||
|
||||
Reference in New Issue
Block a user