更新
This commit is contained in:
@@ -0,0 +1,408 @@
|
||||
# APP 接诊台队列第二行 Python `dict` 泄漏审计
|
||||
|
||||
审计日期:2026-08-14
|
||||
审计范围:`server` 列表 API → Python API client / repository / model normalize → `reception.py` 队列卡片
|
||||
操作边界:只读诊断;未修改任何业务代码或测试代码,仅新增本报告。
|
||||
|
||||
## 0. Trellis 指令检查
|
||||
|
||||
仓库根目录 `D:\web\zyt` 下不存在 `.trellis/`,因此没有可读取的 `.trellis/workflow.md`、`.trellis/spec/` 或任务上下文。本审计已按根目录 `AGENTS.md` 的现有约束执行。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
根因已经确定,不是 Qt 的渲染问题,也不是 JSON 解码问题,而是一个“对象字段被误当成文本别名”的类型边界错误:
|
||||
|
||||
1. 当前服务端 `doctor.appointment/lists` 使用 `->with('diagnosis')`,所以每条挂号记录的 `diagnosis` 字段实际是一个完整的关联诊单对象(JSON object / Python `dict`),缺失关联时则可能为 `null`;它不是诊断名称字符串。
|
||||
2. `RemoteDoctorRepository.list_appointments()` 通过 `PageResult.from_payload(..., Appointment.from_dict)` 做 normalize。`Appointment.from_dict()` 没有诊断摘要字段,只把完整原始行保存在 `Appointment.raw`。
|
||||
3. `get_value()` 对 dataclass 上不存在的字段会回退到 `raw`,因此 `first_value(record, ..., "diagnosis")` 会取出那个 `dict`。
|
||||
4. `QueueRow` 把该值放进 `str(part).strip()`,Python 按字典 `repr` 生成 `"{'id': ..., ...}"`,随后直接传给 `QLabel`。这正是用户看到的第二行文本。
|
||||
|
||||
触发点位于当前工作区尚未提交的接诊台视觉改造:旧版队列第二行只展示预约时间,不读取 `diagnosis`;当前改造在 `QueueRow` 中新增了 `"diagnosis"` 这个兜底别名,从而首次暴露服务端一直存在的关联对象。
|
||||
|
||||
**直接修复不能只是换成 `display_text()`。** `display_text()` 对容器同样执行 `str(value)`,仍会显示 Python 字典。也不应把整个嵌套 `diagnosis` 合并进 appointment,因为两层都有 `id`、`patient_id`、`status` 等不同语义字段,会污染挂号状态和三个 ID 的权威口径。
|
||||
|
||||
## 2. 完整数据链路
|
||||
|
||||
### 2.1 服务端列表实际返回嵌套对象
|
||||
|
||||
入口和响应封装:
|
||||
|
||||
| 文件 / 函数 | 当前行号 | 事实 |
|
||||
|---|---:|---|
|
||||
| `server/app/adminapi/controller/doctor/AppointmentController.php::lists()` | 62-65 | `doctor.appointment/lists` 交给 `AppointmentLists`。 |
|
||||
| `server/app/common/service/JsonService.php::dataLists()` | 120-147 | HTTP envelope 的 `data` 为 `{lists, count, page_no, page_size, extend}`。 |
|
||||
| `server/app/adminapi/lists/doctor/AppointmentLists.php::lists()` | 162-369 | 构造并序列化每一条挂号记录。 |
|
||||
|
||||
决定 `diagnosis` 类型的代码:
|
||||
|
||||
- `AppointmentLists.php:213-218`:查询从 `Appointment::alias('a')->with('diagnosis')` 开始;同时 join `tcm_diagnosis u`,只把患者、医生、医助、`diagnosis_id` 等少数字段平铺到顶层。
|
||||
- `AppointmentLists.php:261-267`:模型 `select()->toArray()`;未限制字段的关联模型随主记录一起转成数组。
|
||||
- `server/app/common/model/doctor/Appointment.php:99-102`:`diagnosis()` 是 `belongsTo(Diagnosis::class, 'patient_id', 'id')`。因此 appointment 表里的 `patient_id` 实际指向诊单 ID,而不是诊单对象中的真实患者 ID。
|
||||
- `server/app/adminapi/logic/doctor/AppointmentLogic.php:623-642` 也明确记录:`appointment.patient_id == tcm_diagnosis.id`。
|
||||
- `server/app/common/model/tcm/Diagnosis.php:26-39`:关联对象对应 `tcm_diagnosis` 模型。
|
||||
- `server/sql/tcm_diagnosis.sql:2-25`:基础 schema 中诊单至少包含 `id`、真实 `patient_id`、`patient_name`、`diagnosis_type`、`syndrome_type`、`symptoms`、`remark` 等字段。不同部署的后续列可能更多,但容器类型不变。
|
||||
|
||||
按照当前代码生成的响应形态如下(字段删减,仅表达类型和 ID 语义):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": {
|
||||
"lists": [
|
||||
{
|
||||
"id": 101,
|
||||
"patient_id": 501,
|
||||
"diagnosis_id": 501,
|
||||
"patient_name": "张三",
|
||||
"appointment_time": "09:00",
|
||||
"status": 1,
|
||||
"diagnosis": {
|
||||
"id": 501,
|
||||
"patient_id": 301,
|
||||
"patient_name": "张三",
|
||||
"diagnosis_type": "follow_up",
|
||||
"syndrome_type": "...",
|
||||
"symptoms": "口干"
|
||||
}
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"page_no": 1,
|
||||
"page_size": 15,
|
||||
"extend": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里的关键合同是:
|
||||
|
||||
- 顶层 `diagnosis_id`:诊单 ID;
|
||||
- 顶层 `patient_id`:历史命名,当前也存诊单 ID;
|
||||
- `diagnosis.id`:诊单 ID;
|
||||
- `diagnosis.patient_id`:真实患者 ID;
|
||||
- `diagnosis`:object 或 null,不应作为字符串渲染。
|
||||
|
||||
本次没有调用线上接口或读取生产数据库;“实际字段形态”依据当前 server 查询、关系定义、模型序列化和 schema 静态确认。容器类型由 `with('diagnosis')` 确定,不依赖具体数据内容。
|
||||
|
||||
### 2.2 API client 解 envelope,但不改变行字段
|
||||
|
||||
- `app/src/doctor_workstation/services/api_client.py::ApiClient._unwrap()`,344-382:校验 envelope,在 `code == 1` 时直接返回 `envelope['data']`。
|
||||
- 所以 repository 收到的是 `{lists, count, ...}`,列表行里的嵌套 `diagnosis` 仍为 Python `dict`。
|
||||
|
||||
### 2.3 Repository / model normalize 保留嵌套对象到 `raw`
|
||||
|
||||
- `app/src/doctor_workstation/services/repository.py::RemoteDoctorRepository.list_appointments()`,879-909:请求 `doctor.appointment/lists`,再调用 `PageResult.from_payload(payload, Appointment.from_dict, ...)`。
|
||||
- `app/src/doctor_workstation/core/models.py::PageResult.from_payload()`,804-865:在 840 行逐条调用 parser。
|
||||
- `app/src/doctor_workstation/core/models.py::Appointment.from_dict()`,213-257:只 normalize 顶层基本字段;没有 `clinical_diagnosis`、`diagnosis_name`、`disease_name` 或 `disease_course` dataclass 字段;256 行执行 `raw=dict(source)`,完整保留嵌套 relation。
|
||||
- `app/src/doctor_workstation/ui/widgets.py::get_value()`,53-71:对象属性不存在时,66-67 行回退到对象的 `raw`。
|
||||
- `app/src/doctor_workstation/ui/widgets.py::first_value()`,74-81:只排除 `None` 和空字符串,不排除 Mapping、Sequence 或其他不可展示容器。
|
||||
|
||||
因此 normalize 后的真实 Python 形态是:
|
||||
|
||||
```python
|
||||
Appointment(
|
||||
id=101,
|
||||
patient_id=501,
|
||||
diagnosis_id=501,
|
||||
# 没有 canonical diagnosis summary 字段
|
||||
raw={
|
||||
# ...
|
||||
"diagnosis": {"id": 501, "patient_id": 301, "symptoms": "口干"}
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 2.4 `QueueRow` 把 Mapping 转成 Python 文本
|
||||
|
||||
- `app/src/doctor_workstation/ui/pages/reception.py::ReceptionPage._apply_queue()`,1473-1519:`page_items()` 取出 `Appointment`,并为每条记录创建 `QueueRow(record)`。
|
||||
- `app/src/doctor_workstation/ui/pages/reception.py::QueueRow.__init__()`,567-574:按 `clinical_diagnosis → diagnosis_name → disease_name → diagnosis` 取第一个非空值。
|
||||
- 前三个字段在当前 server 顶层没有,`diagnosis` 则通过 `get_value()` 的 `raw` 回退命中关联 `dict`。
|
||||
- 同函数 586-590:`str(part).strip()` 对该 dict 生成 Python repr。
|
||||
- 591 行:repr 被送入 `QLabel`,没有任何类型检查。
|
||||
- `app/src/doctor_workstation/ui/widgets.py::display_text()`,84-91:即使改用此函数,91 行仍是 `str(value)`,所以不是修复。
|
||||
|
||||
相邻的 `ReceptionPage._render_identity()` 在 `reception.py:1783-1809` 也有“候选值 → `str(part)`”模式。它当前处理的是详情响应中的 diagnosis mapping 内部字段,不会必然触发本问题,但建议复用同一个 scalar-only helper,避免未来某个详情别名变成 object/list 时再次泄漏容器 repr。
|
||||
|
||||
## 3. 可重复证据
|
||||
|
||||
使用项目现有虚拟环境、`-B` 禁止生成 bytecode,执行了一个无网络、无文件写入的最小复现:
|
||||
|
||||
```python
|
||||
row = Appointment.from_dict({
|
||||
"id": 1,
|
||||
"patient_name": "张三",
|
||||
"appointment_time": "09:00",
|
||||
"diagnosis": {"id": 8, "patient_name": "张三", "symptoms": "口干"},
|
||||
})
|
||||
widget = QueueRow(row)
|
||||
```
|
||||
|
||||
当前代码输出:
|
||||
|
||||
```text
|
||||
normalized_type= Appointment raw_diagnosis_type= dict
|
||||
fallback_value= {'id': 8, 'patient_name': '张三', 'symptoms': '口干'}
|
||||
rendered_subline= {'id': 8, 'patient_name': '张三', 'symptoms': '口干'}
|
||||
```
|
||||
|
||||
这同时证明:
|
||||
|
||||
- API row 到 `Appointment` 的 normalize 已发生;
|
||||
- dict 并未来自 Qt;
|
||||
- 卡片最终文本和 Python dict repr 完全相同。
|
||||
|
||||
## 4. 为什么现有测试没有发现
|
||||
|
||||
1. `app/tests/test_reception_parity_ui.py::test_queue_status_badge_is_not_clipped_in_narrow_panel()`,103-128,只断言状态徽标尺寸和位置;fixture 不含 `diagnosis`,也没有读取 `ReceptionQueueSubline`。
|
||||
2. 同文件队列分页/筛选 fixtures(247-386)只给 `id/patient_name/status` 等平铺字段,未模拟 server 的 `diagnosis: {...}` relation。
|
||||
3. `app/tests/test_mock_repository.py::test_tolerant_page_parsing_accepts_aliases_and_bad_rows()`,298-324,只覆盖简单别名和坏行;没有嵌套关系字段。
|
||||
4. `app/tests/test_repository_parity.py::test_page_result_preserves_outer_and_nested_extend()`,155-174,覆盖的是分页 envelope 嵌套,不是 row 内 relation 嵌套。
|
||||
5. `app/tests/test_repository_parity.py::test_remote_reception_is_forcibly_scoped_to_today()`,227-244,只断言请求 endpoint/参数,不断言返回 DTO 字段类型。
|
||||
6. Demo appointments 在 `app/src/doctor_workstation/services/mock_repository.py:3153-3283` 不包含 `diagnosis` relation,因此视觉截图只会走时间 fallback,无法暴露生产响应问题。
|
||||
|
||||
## 5. 兼容旧 / 新响应的稳健提取规则
|
||||
|
||||
### 5.1 必须先区分“容器”和“可展示标量”
|
||||
|
||||
建议定义一个只接受 JSON scalar 的 helper:
|
||||
|
||||
- 接受:非空 `str`;必要时接受 `int/float` 并转换成字符串;
|
||||
- 拒绝:`Mapping`、list/tuple/set、bool、`None`、空白字符串;
|
||||
- 绝不对未知容器调用 `str()`;
|
||||
- 如果产品以后明确支持多选诊断,应单独定义“纯字符串列表 join”合同,不能把任意 list/dict 通用字符串化。
|
||||
|
||||
### 5.2 诊断摘要优先级
|
||||
|
||||
兼容三类已知/合理响应:
|
||||
|
||||
1. **新/平铺 canonical**:顶层 `clinical_diagnosis`;
|
||||
2. **平铺历史别名**:顶层 `diagnosis_name`、`disease_name`;
|
||||
3. **当前 server relation**:若顶层 `diagnosis` 是 Mapping,只从其内部的 `clinical_diagnosis`、`diagnosis_name`、`disease_name`、标量 `diagnosis` 中选;
|
||||
4. **更老的标量别名**:只有当顶层 `diagnosis` 本身是 scalar 时,才把它作为最后兜底;
|
||||
5. 都没有可展示文本时,返回空字符串,让 UI 回退到预约时间。
|
||||
|
||||
推荐顺序可写成:
|
||||
|
||||
```text
|
||||
top.clinical_diagnosis
|
||||
→ top.diagnosis_name
|
||||
→ top.disease_name
|
||||
→ diagnosis_object.clinical_diagnosis
|
||||
→ diagnosis_object.diagnosis_name
|
||||
→ diagnosis_object.disease_name
|
||||
→ diagnosis_object.diagnosis(仅 scalar)
|
||||
→ top.diagnosis(仅 scalar)
|
||||
→ ""
|
||||
```
|
||||
|
||||
不要把 `diagnosis_type` 直接当临床诊断:它在 server 中是初诊/复诊等类型 code;也不要直接显示未经翻译的 `syndrome_type` code。若产品明确希望第二行显示证型,应由 server 提供 `syndrome_type_text` 或由客户端字典翻译后作为另一个明确字段,不能把整个 relation 当成兜底。
|
||||
|
||||
### 5.3 病程摘要优先级
|
||||
|
||||
同样对顶层和 relation 内部执行 scalar-only 查找:
|
||||
|
||||
```text
|
||||
top.disease_course_text
|
||||
→ top.disease_course
|
||||
→ top.course_text
|
||||
→ top.course
|
||||
→ diagnosis_object.disease_course_text
|
||||
→ diagnosis_object.disease_course
|
||||
→ diagnosis_object.course_text
|
||||
→ diagnosis_object.course
|
||||
→ ""
|
||||
```
|
||||
|
||||
### 5.4 最终渲染规则
|
||||
|
||||
- `diagnosis`、`course` 都有文本:`诊断 · 病程`;
|
||||
- 只有一个:只显示该项;
|
||||
- 两者都没有:显示预约时间;
|
||||
- 时间也没有:显示“时间待确认”;
|
||||
- 无论输入如何,最终字符串都不得包含由容器 repr 产生的 `{...}` / `[...]`。
|
||||
|
||||
## 6. 建议补丁
|
||||
|
||||
### 6.1 首选:model 边界 canonicalize + UI 最后一道类型保护
|
||||
|
||||
#### A. `core/models.py`
|
||||
|
||||
在基础 helper 附近(当前 21-81 行)增加 scalar-only 提取器:
|
||||
|
||||
```python
|
||||
def _first_scalar_text(*values: object) -> str:
|
||||
for value in values:
|
||||
if isinstance(value, str):
|
||||
text = value.strip()
|
||||
if text:
|
||||
return text
|
||||
elif isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
return str(value)
|
||||
return ""
|
||||
```
|
||||
|
||||
给 `Appointment`(当前 179-211 行)增加 canonical 字段:
|
||||
|
||||
```python
|
||||
clinical_diagnosis: str = ""
|
||||
disease_course: str = ""
|
||||
```
|
||||
|
||||
在 `Appointment.from_dict()` 当前 217 行之后只选择性读取 relation,**不要 merge 整个 nested mapping**:
|
||||
|
||||
```python
|
||||
source = _mapping(data)
|
||||
diagnosis_value = source.get("diagnosis")
|
||||
diagnosis = _mapping(diagnosis_value)
|
||||
legacy_diagnosis = None if isinstance(diagnosis_value, Mapping) else diagnosis_value
|
||||
|
||||
clinical_diagnosis = _first_scalar_text(
|
||||
source.get("clinical_diagnosis"),
|
||||
source.get("diagnosis_name"),
|
||||
source.get("disease_name"),
|
||||
diagnosis.get("clinical_diagnosis"),
|
||||
diagnosis.get("diagnosis_name"),
|
||||
diagnosis.get("disease_name"),
|
||||
diagnosis.get("diagnosis"),
|
||||
legacy_diagnosis,
|
||||
)
|
||||
disease_course = _first_scalar_text(
|
||||
source.get("disease_course_text"),
|
||||
source.get("disease_course"),
|
||||
source.get("course_text"),
|
||||
source.get("course"),
|
||||
diagnosis.get("disease_course_text"),
|
||||
diagnosis.get("disease_course"),
|
||||
diagnosis.get("course_text"),
|
||||
diagnosis.get("course"),
|
||||
)
|
||||
```
|
||||
|
||||
随后赋给 dataclass 字段。可顺带在顶层 `diagnosis_id` 缺失时安全回退 `diagnosis.id`,但必须保持 appointment 的 `id/status/patient_id` 仍以顶层为权威。
|
||||
|
||||
#### B. `ui/pages/reception.py`
|
||||
|
||||
即使 model 已 canonicalize,`QueueRow` 仍可能被测试仓库或其他 repository 直接传入 dict,因此 UI 应保留 scalar-only guard。最小安全改法不是简单删除 `"diagnosis"`,而是:
|
||||
|
||||
```python
|
||||
def _display_scalar(value: object) -> str:
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
return str(value)
|
||||
return ""
|
||||
```
|
||||
|
||||
然后在 `QueueRow.__init__()` 当前 567-591 行:
|
||||
|
||||
```python
|
||||
diagnosis = _display_scalar(
|
||||
first_value(
|
||||
record,
|
||||
"clinical_diagnosis",
|
||||
"diagnosis_name",
|
||||
"disease_name",
|
||||
default=None,
|
||||
)
|
||||
) or _display_scalar(get_value(record, "diagnosis", None))
|
||||
|
||||
course = _display_scalar(
|
||||
first_value(
|
||||
record,
|
||||
"disease_course_text",
|
||||
"disease_course",
|
||||
"course_text",
|
||||
"course",
|
||||
default=None,
|
||||
)
|
||||
)
|
||||
|
||||
subline_parts = [part for part in (diagnosis, course) if part]
|
||||
subline = QLabel(" · ".join(subline_parts) or display_text(time or "时间待确认"))
|
||||
```
|
||||
|
||||
如果希望 `QueueRow` 本身也兼容未经 `Appointment.from_dict()` 的嵌套 raw dict,则把 5.2/5.3 的 relation 内部候选一起放进一个纯函数(例如 `_queue_summary_fields(record)`),并由 model/UI 共用或分别调用同一优先级。重点是 relation 容器永远不能进入 `QLabel`。
|
||||
|
||||
建议同样把 `_render_identity()` 当前 1802-1805 行的 `str(part)` 改为这个 scalar-only helper,作为邻接防御。
|
||||
|
||||
### 6.2 不建议的修复
|
||||
|
||||
- **只改 `display_text(diagnosis)`**:仍会 `str(dict)`。
|
||||
- **只删掉 `"diagnosis"` 别名**:能止住当前服务端,但会丢掉历史 scalar `diagnosis` 兼容,也无法读取未来/其他部署的嵌套 canonical 文本。
|
||||
- **`json.dumps(diagnosis)`**:只是把 Python repr 换成 JSON,仍然把内部对象和潜在隐私信息显示给用户。
|
||||
- **把 relation 整体 merge 到 appointment**:会让 diagnosis 的 `id/patient_id/status` 覆盖挂号字段,破坏视频、完成接诊和选中一致性。
|
||||
- **立即删除 server 的 `with('diagnosis')`**:可能影响已有管理端消费者;在没有完整 server contract 回归前不应作为 APP 热修。
|
||||
|
||||
### 6.3 可选的服务端长期收敛
|
||||
|
||||
长期可以让 `AppointmentLists` 明确返回队列所需的 scalar summary,例如 `clinical_diagnosis` / `disease_course_text` / 已翻译的 `syndrome_type_text`,并限制或移除列表里的完整 relation,以减少 payload 和 PII 面。但当前 schema 各部署并不完全一致,直接在 SQL field 中引用未必存在的列会造成查询失败,因此这应作为单独的 API contract 变更,不是本次桌面 APP 热修的前置条件。
|
||||
|
||||
## 7. 必需测试
|
||||
|
||||
### 7.1 Model / repository normalize 测试
|
||||
|
||||
建议放在 `app/tests/test_repository_parity.py`,直接通过 `PageResult.from_payload(..., Appointment.from_dict)` 覆盖真实路径:
|
||||
|
||||
1. relation object 内有 `clinical_diagnosis` 和 `disease_course`,normalize 后得到 canonical 字符串,同时 `raw['diagnosis']` 仍保留原 dict。
|
||||
2. 顶层 flattened canonical 字段优先于嵌套字段。
|
||||
3. 顶层 legacy scalar `diagnosis` 可兼容。
|
||||
4. `diagnosis` 只有无关 mapping 字段时,canonical 诊断为空,不出现 dict repr。
|
||||
5. 顶层 `status=1/id=101/patient_id=501` 与 nested `status=0/id=501/patient_id=301` 同时存在时,appointment 权威字段不得被 nested 覆盖。
|
||||
6. `diagnosis=null`、空 dict、字段为空白、错误的 list/dict 类型均不抛异常。
|
||||
|
||||
建议核心断言示例:
|
||||
|
||||
```python
|
||||
assert appointment.clinical_diagnosis == "消渴"
|
||||
assert appointment.disease_course == "2 年"
|
||||
assert isinstance(appointment.raw["diagnosis"], dict)
|
||||
assert appointment.id == 101
|
||||
assert appointment.status == 1
|
||||
assert appointment.patient_id == 501
|
||||
```
|
||||
|
||||
### 7.2 QueueRow 渲染测试
|
||||
|
||||
建议放在 `app/tests/test_reception_parity_ui.py`,扩展当前 103 行附近的 `QueueRow` 测试;通过 `findChild(QLabel, 'ReceptionQueueSubline')` 直接断言:
|
||||
|
||||
| 输入 | 期望第二行 |
|
||||
|---|---|
|
||||
| flat `clinical_diagnosis='消渴'`, `disease_course_text='2 年'` | `消渴 · 2 年` |
|
||||
| legacy scalar `diagnosis='消渴'`, `course='2 年'` | `消渴 · 2 年` |
|
||||
| nested relation 内含 canonical 文本(经 `Appointment.from_dict`) | `消渴 · 2 年` |
|
||||
| `diagnosis={'id': 8, 'symptoms': '口干'}`,无摘要 | 回退预约时间 |
|
||||
| `clinical_diagnosis={...}` / `course=[...]` | 回退预约时间,且不抛异常 |
|
||||
| 所有字段缺失 | `时间待确认` |
|
||||
|
||||
每个 case 还应有通用安全断言:
|
||||
|
||||
```python
|
||||
assert "{" not in subline.text()
|
||||
assert "}" not in subline.text()
|
||||
assert "[" not in subline.text()
|
||||
assert "]" not in subline.text()
|
||||
```
|
||||
|
||||
如果正常业务文本本身允许这些符号,则更精确地断言“不等于 `repr(payload['diagnosis'])`”并断言预期 fallback;不要只做脆弱的字符黑名单。
|
||||
|
||||
### 7.3 Server contract 测试(若修改 server)
|
||||
|
||||
若后续调整 `AppointmentLists`,PHP 侧应加入 endpoint/列表类 contract:
|
||||
|
||||
- `diagnosis` 明确为 array|null,禁止在 contract 中宣称 string;
|
||||
- 新增的 queue summary 必须是 string|null;
|
||||
- count / scope / 日期过滤不受影响;
|
||||
- relation 字段收窄或移除前,先盘点 admin 其他页面消费者。
|
||||
|
||||
## 8. 修复验收标准
|
||||
|
||||
1. 线上/current server 的 nested `diagnosis` response 不再把 `{...}` 显示在队列第二行。
|
||||
2. 平铺 canonical、历史 scalar 和 nested canonical 三种形态均有确定输出。
|
||||
3. 没有可展示诊断/病程时稳定回退预约时间,而不是空白或容器 repr。
|
||||
4. `Appointment` 的挂号 `id/status/patient_id` 不被 nested diagnosis 覆盖。
|
||||
5. 新增 model 与 QueueRow 测试通过;现有 same-day、分页、切换患者和状态徽标测试保持通过。
|
||||
6. 不需要以修改 server 或数据库 schema 作为 APP 修复前提。
|
||||
|
||||
## 9. 最终判定
|
||||
|
||||
这是一个确定性的 P1 展示与数据边界缺陷:不会直接修改数据,但会把完整关联对象(其中可能包含手机号、身份证、病史等字段,取决于部署 schema)暴露在医生端 UI,并破坏卡片可读性。推荐用“repository/model selective normalize + UI scalar-only guard”双层修复;不要序列化对象,也不要 merge relation。
|
||||
@@ -0,0 +1,203 @@
|
||||
# 诊疗 / 病例 / 订单 / AI 报告子窗口蓝白参考审计
|
||||
|
||||
审计日期:2026-08-13
|
||||
审计性质:只读;未修改业务源码、测试、脚本或既有 PNG。
|
||||
审计边界:诊疗详情/编辑抽屉、独立病例只读页、诊断上下文中的业务订单详情、诊断 AI 报告。未审计或改动 `theme.py`、`widgets.py`、处方编辑/患者页/挂号页;AI 部分只看诊断报告模式,不扩展到处方业务。
|
||||
|
||||
## 1. 结论先行
|
||||
|
||||
1. **用户所指“蓝白参考”最接近的现有实图是 `artifacts/pixel_exact_v3/consultations.png`**(1710×920,2026-08-13 19:56)。它是当前最新、最完整的蓝白医生工作站视觉:浅蓝壳层、近白内容底、白色卡片、靛蓝主操作、冷灰蓝文字与边框。子窗口应从它取色,不应从旧 Diagnosis 深色图取色。
|
||||
2. **子窗口结构不能统一成同一种 modal。** 现有正确结构分别是:诊疗编辑/viewOnly 为右侧 60% Drawer;独立病例为无 Tabs 的纵向滚动详情页;订单详情为右侧 80% readonly Drawer;AI 报告为 920×760 的居中 Dialog。用户要求的“蓝白”应是视觉统一,不是破坏这些已经有测试与研究规格支撑的容器合同。
|
||||
3. **`artifacts/diagnosis_visual/*.png` 的主诊疗图仍是深色旧产物,只能借布局,不能借色。** 例如 `diagnosis_edit_1440x900.png`、`diagnosis_viewonly_1440x900.png`、`diagnosis_readonly_1440x900.png`、`diagnosis_order_detail_drawer_1440x900.png` 均以 `#080B14/#101626/#151D31` 为主。它们与 19:56 的蓝白主壳不属于同一视觉版本。
|
||||
4. **最接近当前浅色订单结构的实图是 `.pytest-tmp-ui-redesign-full/test_drawer_and_inline_player_0/order.png`**(1100×720,2026-08-13 17:53)。它准确显示了 20% 遮罩 + 80% 白色 Drawer、固定 Header/Body/Footer 和卡片层级,但色谱仍是上一轮灰白+青色(`#F5F7FB/#D8DEEA/#CFFAFE/#A5F3FC`),因此仅作订单布局参考。
|
||||
5. **AI Dialog 已有一张新生成的 920×760 蓝白实图:`artifacts/subwindow_exact/prescription_ai_report_920x760.png`。** 它直接证明当前 AI 组件的 Header、snapshot、医疗警示、双模型 Tabs、两列报告、滚动区和 Footer 能按蓝白色板渲染;但画面是“AI 处方解释”模式,不是诊断 `DIAGNOSIS_AI_KIND` 的“AI 报告/完整病历”模式。诊断 AI 使用同一个 Dialog/QSS,故该图可作为外观强参考,仍不能替代诊断模式本身的验收图。`dialogs/prescription_ai.py` 当前在工作树中仍是未跟踪文件,故它是“当前工作树实现”,不是已有发布基线。
|
||||
6. **当前工作树正在变化。** 审计期间 `dialogs/diagnosis.py` 的订单 QSS 已从灰白版本进一步改为蓝白版本(例如遮罩 `rgba(30,64,175,.18)`、面板 `#F6F9FE`、边框 `#DDE7FF`);随后 `diagnosis_drawer.py` 又加入 `_DIAGNOSIS_BLUE_REPLACEMENTS`,在不重写成熟选择器的前提下把旧青色/绿灰字面量映射到靛蓝/冷灰蓝。测试中的选中 chip 断言也已从 `#CFFAFE` 更新为 `#F0F2FF`。本报告以下约束以审计结束时的最新工作树为准,同时明确区分旧 PNG。
|
||||
|
||||
## 2. 证据优先级与可用方式
|
||||
|
||||
| 优先级 | 证据 | 用途 | 不可误用 |
|
||||
|---|---|---|---|
|
||||
| 1 | `artifacts/pixel_exact_v3/consultations.png` | 蓝白总色调、主/次文字、边框、内容底、主按钮 | 不是子窗口几何图,不能据此改变 Drawer 比例 |
|
||||
| 2 | `artifacts/subwindow_exact/prescription_ai_report_920x760.png` | AI Dialog 蓝白实际渲染、阅读密度、滚动与 Footer | 是处方解释模式,不是诊断 AI 报告模式 |
|
||||
| 3 | `src/doctor_workstation/ui/dialogs/prescription_ai.py` 中 `PRESCRIPTION_AI_QSS` | 已落地的蓝白 Dialog 色板、按钮、Tabs、阅读排版 | 文件未跟踪;不能当作已发布基线 |
|
||||
| 4 | `.pytest-tmp-ui-redesign-full/test_drawer_and_inline_player_0/order.png` | 80% 订单 Drawer 的浅色结构和首屏信息密度 | 青色强调与黑色遮罩不是最终蓝白色值 |
|
||||
| 5 | `artifacts/diagnosis_visual/diagnosis_edit_*.png`、`diagnosis_viewonly_*.png` | 60% 诊疗 Drawer、Header/Tabs/Body/Footer、滚动与窄宽布局 | 深色旧主题不能复用 |
|
||||
| 6 | `artifacts/diagnosis_visual/diagnosis_readonly_*.png` | 独立病例纵向流、4/3 列病例密度、异常值层级 | 深色旧主题不能复用 |
|
||||
| 7 | `artifacts/diagnosis_visual/diagnosis_order_detail_drawer_1440x900.png` 与两个 1024×640 状态图 | 80% 比例、金额五卡、处方/收款首屏、固定 Footer | 深色旧主题不能复用;文件名 `640x540` 实际为 1024×640 |
|
||||
| 8 | `research/diagnosis_detail_visual_spec.md`、`diagnosis_final_visual_gate.md` | 后台结构事实、间距、字段顺序、状态、历史验收 | 旧门禁“PASS”只证明当时深色截图结构完整,不代表符合本轮蓝白参考 |
|
||||
|
||||
### 2.1 实图像素色谱
|
||||
|
||||
对 `pixel_exact_v3/consultations.png` 每 2 px 采样得到的主要实色:
|
||||
|
||||
| 角色 | 参考实色 | 说明 |
|
||||
|---|---|---|
|
||||
| 页面/内容底 | `#FCFDFE` | 最大面积;子窗口滚动内容的首选底色 |
|
||||
| 主卡片/浮层 | `#FFFFFF` | 表格、表单、Header、Footer、信息卡 |
|
||||
| 壳层浅蓝 | `#EEF3FD` | 适合 overlay 外的壳层或非常浅的背景层,不宜给所有内卡重复使用 |
|
||||
| 次级填充 | `#F7F9FE`、`#F2F6FE` | 输入只读态、筛选块、提示块、轻卡底 |
|
||||
| 主边框 | `#E2E7F4` | 参考图中卡片与分隔线的高频精确色 |
|
||||
| 主色 | `#5265F6` | 参考图主按钮/选中态的高频精确色 |
|
||||
| 主标题 | `#15224A` | 参考图高频深靛文字 |
|
||||
| 正文 | `#3F4E75` | 正文/表格主内容 |
|
||||
| 次文字 | `#7481A3` | 标签、说明、占位与元信息 |
|
||||
| 危险 | `#F15B67`(实图) | 取消/危险语义;业务详情可继续用更稳的 `#C43E55` 文本 |
|
||||
|
||||
AI 报告现有色板与参考图极近:背景 `#F7F9FE`,主色 `#5761F4`,hover `#6871F6`,pressed/链接 `#4D57D8`,浅主色 `#F0F2FF`,边框 `#DCE3F2`,标题 `#17203F`,正文 `#37415E`,次文字 `#78849D`。两套主色只差 5 个 RGB 量级;**若追求实图像素统一,统一到 `#5265F6`;若追求最小改动,可把 AI 色板整套作为子窗口局部 token,但不得继续混入青色 `#0891B2/#0E7490/#CFFAFE/#A5F3FC`。**
|
||||
|
||||
## 3. 全部子窗口共同约束
|
||||
|
||||
### 3.1 色与表面
|
||||
|
||||
- 外层/滚动区:`#FCFDFE` 或需要轻微分层时 `#F7F9FE`。
|
||||
- Header、Footer、卡片、表格主体:`#FFFFFF`。
|
||||
- 一般边框/分隔:`1px #E2E7F4`;强调边框可用 `#DCE3F2` 或 `#DDE7FF`,但同一控件不要混用三种。
|
||||
- 主操作、选中 Tab、focus:`#5265F6`;hover 可用 `#6871F6`,pressed/深色链接 `#4D57D8`;浅背景 `#F0F2FF`,浅边框 `#D8DCFF`。
|
||||
- 标题/数据主值:`#15224A`(现有 AI 的 `#17203F` 可作为近似);正文 `#3F4E75`;label/meta `#7481A3`。
|
||||
- 成功/提醒/危险仍保留业务语义色,不要全部染成蓝:成功 `#16876C` 或 `#16A34A`;提醒 `#9A6813`;危险 `#C43E55` 或异常指标 `#DC2626`。
|
||||
- 遮罩只负责层级,不变成黑墙:推荐订单当前工作树的 `rgba(30,64,175,.18)`;诊疗 Drawer 可略深但保持蓝灰透明。旧 `rgba(8,11,20,.78)` 明显不符合参考。
|
||||
|
||||
### 3.2 字体、圆角、密度
|
||||
|
||||
- 字体栈:`Microsoft YaHei UI`, `PingFang SC`, `Noto Sans CJK SC`, sans-serif。不要为普通正文引入另一套拉丁字体;病例编号/时间可使用等宽数字。
|
||||
- 正文/控件 13 px;字段 label、提示与 meta 11–12 px;卡片标题 14–15 px;Drawer 标题 18–19 px;AI 报告标题 20 px。
|
||||
- 4 px 间距基线;常用 8/12/16/20/24 px。不要产生 5、13、17、21 等无依据的主布局间距。
|
||||
- 控件/普通按钮高 34 px;诊疗 Footer 主按钮高 40 px;紧凑 close 为 32×32;诊疗 Tab 高 42 px;AI Tab 高 36 px。
|
||||
- 内控件/按钮圆角 7–8 px;字段卡/分区 9–10 px;Hero 12 px;独立只读大卡 14 px。999 px 只用于真正的状态 pill/选择 chip,不要给所有按钮胶囊化。
|
||||
- 表格状态必须使用小型 Tag/pill,不得整格铺色。表头宜 `#F7F9FE/#F8FAFF`,主体白底,行/列分隔 `#E2E7F4`;金额右对齐,状态与操作位置保持现有合同。
|
||||
- hover、pressed、disabled、focus 必须可辨;focus 使用主色边界/外环,不能因改蓝白而删除键盘焦点。
|
||||
|
||||
## 4. 诊疗编辑 / viewOnly Drawer
|
||||
|
||||
### 4.1 必须保持的几何结构
|
||||
|
||||
- `DiagnosisDialog` 外层仍覆盖 owner,右侧 panel RTL 贴边;桌面宽度为 owner 的 **60%**:1024×640 时 614 px,1440×900 时 864 px。
|
||||
- 窗口宽 `<=768` 时 panel 全宽;当前 Dialog 最小 760×520、默认 1024×640。不要改成固定居中 880×680 modal。
|
||||
- 三段分离:Header、可横向滚动 Tabs + 独立滚动 Body、固定 Footer。Footer 不得放到 ScrollArea 尾部。
|
||||
- Header 内边距 `16px 13px`,横向 gap 10;标题行内部 gap 10,副标题与标题垂直 gap 4;close 32×32。
|
||||
- Tabs:单 Tab 最小高 42,水平 padding 14;横向 overflow 用 4 px 细滚动条,隐藏原生盒状左右箭头;激活条使用主色,建议 2–3 px。
|
||||
- Basic Body:`20px 16px 20px 20px`(左/上/右/下)内边距;分区纵向 gap 12;一行两字段时 gap 16;窄模式纵向 gap 12。
|
||||
- Footer:`20px 14px 20px 18px` 内边距,按钮 gap 12,白底、上边 `#E2E7F4`,主按钮 40 px 高。
|
||||
|
||||
### 4.2 应改成的视觉
|
||||
|
||||
- Panel/Header/Footer/Body 从旧深靛或当前青绿色调统一到 §3 色板:白色 Header/Footer、近白蓝 Body,主色 `#5265F6`。
|
||||
- `diagnosis_drawer.py` 的基础 QSS 字面量仍大量是青色 `#0891B2/#0E7490/#22D3EE/#CFFAFE/#A5F3FC` 与绿灰文字 `#134E4A/#2A6B64/#5B7A76`,但当前工作树已通过 `_DIAGNOSIS_BLUE_REPLACEMENTS` 在运行时成组映射为靛蓝/冷灰蓝。这个方向正确;最终检查重点应变为:映射是否覆盖所有 scoped QSS、QPainter 和 inline style,且没有选择器优先级让旧色漏出。
|
||||
- 推荐映射:
|
||||
|
||||
| 当前 Diagnosis 色 | 蓝白目标 |
|
||||
|---|---|
|
||||
| `#0891B2`, `#0E7490` | `#5265F6` / pressed `#4D57D8` |
|
||||
| `#22D3EE` | `#6871F6` 或 focus `#5265F6` |
|
||||
| `#CFFAFE` | `#F0F2FF` |
|
||||
| `#A5F3FC` | `#D8DCFF` |
|
||||
| `#F5F8F7`, `#F6F6F6` | `#FCFDFE` / `#F7F9FE` |
|
||||
| `#D5E5E2`, `#D9DEDA`, `#E2EBE8` | `#E2E7F4` / `#DCE3F2` |
|
||||
| `#134E4A`, `#2A6B64` | `#15224A` / `#3F4E75` |
|
||||
| `#5B7A76`, `#66736D` | `#7481A3` |
|
||||
|
||||
- mode badge、锁定 warning、成功/失败保存状态保留语义色;不要把 warning 也涂成主蓝。
|
||||
- 当前表单代码为中宽两列、字段 label 固定 100 px,`content_w < 520` 才堆叠。研究规格中的后台 label 160 px 与当前 614 px 两列桌面实现存在冲突;**本轮蓝白适配不应贸然把 100 改成 160**,否则 1024×640 会失去已测试的两列无裁切合同。若未来要追后台 160 px,需单独重做栅格,不属于纯视觉换肤。
|
||||
|
||||
## 5. 独立病例 / 病历只读页
|
||||
|
||||
本节“病例”按当前 `DiagnosisDialog` 的 standalone readonly + `CaseGrid` 理解;不进入处方历史详情实现。
|
||||
|
||||
### 5.1 必须保持的布局
|
||||
|
||||
- 独立只读页是纵向 ScrollArea,**没有 Tabs**;页面内容四边 16 px、卡片间 gap 16。
|
||||
- 顶部 Hero 左右可换行;宽度 `<900` 时右侧患者摘要落到下一行。Hero 内边距 `16px 12px`,内部 gap 12,圆角 12。
|
||||
- 患者信息大卡内边距 18、纵向 gap 14;内部患者 Hero 内边距 `18px 16px`、纵向 gap 3。
|
||||
- 病例卡 `CaseGrid` 内边距 18、纵向 gap 14;分组之间 dashed 分隔;网格横向 16、纵向 8。
|
||||
- 宽屏病例分组保持既有列数:基本信息/生命体征/主诉 4 列,现病史与其他病史 3 列,既往史与补充意见整行。异常高压/低压/血糖继续用红色、700 字重和上箭头。
|
||||
- 病例 label/value 的视觉尺度保持 12–12.5 px;label `#7481A3`,value `#15224A/#1F2937`,空值使用更浅的灰蓝。
|
||||
|
||||
### 5.2 蓝白外观
|
||||
|
||||
- 页面底 `#FCFDFE`;Hero 可使用研究规格已有的浅蓝渐变 `#F5F8FF → #EEF3FF`,边框 `#DDE7FF`;不要使用深色整页。
|
||||
- 通用只读卡白底、`1px #E6EBF2`、14 px 圆角;卡片标题 15/700,左侧 3×16 px 主蓝标记。
|
||||
- 当前工作树在患者信息卡标题行新增了“AI 报告”按钮。位置应固定在标题行右侧;使用 secondary 样式(字 `#4D57D8`、底 `#F0F2FF`、边 `#D8DCFF`、34 px 高、7 px 圆角),避免与“保存/生成”级主动作争抢。
|
||||
- `CaseGrid` 已有少量蓝灰 inline 色(subtitle `#7886AA`、divider `#D8DEEE`),方向正确,但应收敛到统一的 `#7481A3/#E2E7F4`,避免同一页出现多套近似边框。
|
||||
|
||||
## 6. 业务订单详情 Drawer
|
||||
|
||||
### 6.1 必须保持的结构与尺寸
|
||||
|
||||
- `OrderDetailDrawer` 覆盖 owner,右侧 panel 固定 **80%**;1024 owner 为约 819 px,1440 owner 为 1152 px;左侧 20% 为 scrim。
|
||||
- Header/Body/Footer 三段独立;Body 单独纵向滚动,Footer 始终可见。
|
||||
- Header 当前内边距 `20px 15px 18px 15px`,gap 12;标题栈 gap 4;标题 19 px,meta 12 px;右侧依次是只读 badge、状态 Tag、关闭。
|
||||
- Body 当前内边距 `18px 16px 18px 22px`,区块 gap 14。区块内边距 `15px 14px 15px 16px`,gap 11;字段/金额卡网格 gap 8。
|
||||
- 金额概览首行 5 等分卡;值 18/700。信息字段为 3 列,物流元信息 2 列。收款表最小高 145,按行数增长但最大 280。
|
||||
- 内容顺序必须保持:金额概览 → 处方详情 → 收款记录 → 履约与收货 → 物流轨迹 → 操作日志。readonly 隐藏收款方式等敏感/可编辑内容,不能为了“清爽”删掉已规定的只读信息层级。
|
||||
- Footer 当前内边距 `18px 10px`;左侧数据来源说明,右侧关闭。
|
||||
|
||||
### 6.2 应匹配的蓝白细节
|
||||
|
||||
- 当前工作树 `_ORDER_DETAIL_QSS` 已基本走在正确方向:scrim `rgba(30,64,175,.18)`、Drawer/Scroll `#F6F9FE`、Header/Footer 白、强边 `#DDE7FF`、section `#FFFFFF/#E6EBF2`、字段底 `#F8FAFF`、空态 dashed `#C9D8F2`。这套可保留。
|
||||
- 仍需确保从 `DIAGNOSIS_QSS` 继承的通用按钮/Tag/表格不会把订单局部重新染成青色。订单局部关闭按钮使用 secondary 蓝白;状态 Tag 保留绿/黄/红业务语义。
|
||||
- 时间轴左线 `#93B4F4` 是合理的浅主蓝;标题 `#1F2937`、meta `#64748B` 可保留,若做全局像素统一再收敛到 `#15224A/#7481A3`。
|
||||
- 旧订单 PNG 中黑色 20% scrim 和深色主画面不能作为目标;浅色测试图的 80% 几何和卡片层级才是目标。
|
||||
|
||||
## 7. AI 报告 Dialog
|
||||
|
||||
### 7.1 当前实现已接近目标
|
||||
|
||||
- 居中 Dialog,默认 920×760,最小 720×560;不要改成右侧 Drawer,除非产品另行决定窗口范式。
|
||||
- Root 内边距 `22px 18px`,纵向 gap 12。
|
||||
- 标题 20/700,副标题 13;右侧可有状态 badge。
|
||||
- 完整病历 snapshot:白底、`1px #DCE3F2`、10 px 圆角,内边距 `14px 12px`,gap 12;左 label 固定 72 px。
|
||||
- 医疗提示使用淡黄语义卡 `#FFF9EE/#F3DFB5`、9 px 圆角;不要为“全蓝白”抹掉警示语义。
|
||||
- 两模型 Tabs 高 36、水平 padding 18;selected 字 `#4D57D8`、底 `#F0F2FF`、2 px 主色下划线;Tab 内容白底、10 px 圆角。
|
||||
- 报告 ScrollArea 白底;host 内边距 `4px 4px 12px 8px`,gap 12。
|
||||
- 核心判断 summary:`#F0F2FF`、左 3 px `#5761F4`,内容内边距 `18px 16px`;结构化报告四格为 2 列,列 gap 28;编辑框最小高 280。
|
||||
- Footer 右对齐;按钮 34 px 高、水平 padding 16、7 px 圆角;生成/保存为 primary,关闭/取消为普通或 secondary。
|
||||
|
||||
### 7.2 已有实图与仍缺的证据
|
||||
|
||||
- `artifacts/subwindow_exact/prescription_ai_report_920x760.png` 已显示一张质量足够的蓝白 AI Dialog:标题区、药材 snapshot、淡黄医疗警示、两模型状态 Tabs、核心判断浅蓝强调、2×2 报告栅格、垂直滚动和底部“关闭/重新生成”均完整,无深色或青色残留。它能证明共享 Dialog 外观已成立。
|
||||
- 该图标题是“AI 处方解释”、snapshot 为“药材组合”,并非诊断模式的“AI 报告/完整病历”。诊断模式虽然复用同一个类和 QSS,仍需自己的实图验证文案高度、完整病历摘要换行和按钮标签。
|
||||
- `tests/test_prescription_ai_ui.py` 只验证权限、文字、双模型数据、生成与编辑,没有截图、尺寸、主色、滚动和窄窗断言。
|
||||
- 当前 `scripts/render_subwindow_exact.py` 的审计结束版本不再包含 AI render 分支,现有 AI PNG 的可重复生成链路不清晰;因此不能仅凭文件存在判可持续门禁。
|
||||
- 后续视觉门禁至少需要:920×760 有报告态、720×560 空态/加载态、编辑态(含 280 px editor)、生成失败/旧报告回退态各一张;同时验证 Tabs、snapshot、warning、Footer 和纵向滚动无裁切。
|
||||
|
||||
## 8. 现有 tests / scripts / research 能保护什么
|
||||
|
||||
### 8.1 已有强结构约束
|
||||
|
||||
- `tests/test_diagnosis_drawer_visual.py`
|
||||
- 诊疗 Drawer 60%、右贴边、全高、Footer 固定;1024/1440 两档。
|
||||
- 独立 readonly 无 Tabs、内容 gap 16、无横向滚动。
|
||||
- Tabs 横向滚动条 4 px,原生工具按钮不可见。
|
||||
- 订单 Tab、病例/Notes/Daily 等真实组件可达,窗口 resize/reopen 与 owner 同步。
|
||||
- `tests/test_diagnosis_order_video_visual.py`
|
||||
- 订单 Drawer 为 80%,覆盖 owner,readonly 属性与各信息区存在。
|
||||
- 空字段必须显示明确空态,不能伪造 0;操作日志受权限保护。
|
||||
- 生成的 order image 只要求 1100×720 且文件大于 10 KB。
|
||||
- `research/diagnosis_detail_visual_spec.md`
|
||||
- 明确三种诊疗视图的结构、4 px 间距基线、Header/Tabs/Footer、病例 4/3 列、订单 80% Drawer 和业务字段顺序。
|
||||
- `research/diagnosis_final_visual_gate.md`
|
||||
- 证明旧版 60%/80% 几何、fixed Footer、窄窗滚动和所有详情区曾完整入镜。
|
||||
|
||||
### 8.2 当前视觉门禁缺口
|
||||
|
||||
1. Diagnosis tests 原先明确断言选中 chip 为青色 `#CFFAFE`;审计结束时当前工作树已把三处断言同步为 `#F0F2FF`。此项已在改动层关闭,但仍需最终测试运行证明未回归。
|
||||
2. 订单的截图测试只看尺寸和文件体积,不校验 80% 边界位置、Header/Footer 色、主色、scrim 或卡片色;可能在视觉回退时继续通过。
|
||||
3. AI 报告完全没有 PNG/像素门禁。
|
||||
4. `scripts/render_diagnosis_detail_visual.py` 和 `render_diagnosis_order_video_visual.py` 能重建诊疗与订单实图;当前工作树还新增了未跟踪的 `scripts/render_subwindow_exact.py`,聚焦生成诊疗 Drawer、订单 Drawer 和日常记录编辑器。`artifacts/subwindow_exact/prescription_ai_report_920x760.png` 虽已存在,但当前脚本版本不再覆盖它,且该图是处方模式;诊断 AI 的可重复 render 门禁仍缺失。`artifacts/diagnosis_visual` 仍是深色旧产物,蓝白实现完成后必须以新图验收,不可用旧图宣布通过。
|
||||
5. 深色 replacement 表仍留在 `diagnosis_drawer.py` 作为未来暗色材料。当前注释说明默认 light 不执行它;后续实现不应删除未来暗色能力,也不能误把该 replacement 再无条件应用到默认模式。
|
||||
|
||||
## 9. 建议的实现优先级(仅供父任务使用)
|
||||
|
||||
1. **先完成并验证 Diagnosis scoped QSS 色板统一**:当前 replacement 表已经落地,下一步应验证青色/绿灰全部映射为蓝白 token,同时保持 60%/80%/窗口结构不动。
|
||||
2. **再清理 inline style 漏点**:病例 subtitle/divider、banner 文本、订单 toolbar/meta 等应使用 objectName + 同一色板,避免局部仍冒出旧深色或青色。
|
||||
3. **保留订单当前工作树的蓝白局部 QSS**,检查它与通用 `DIAGNOSIS_QSS` 的选择器优先级,避免按钮和表格被旧青色覆盖。
|
||||
4. **以 AI 报告色板为一致性校准**,必要时把其主色从 `#5761F4` 微调到参考精确色 `#5265F6`;医疗 warning、成功、错误保留语义色。
|
||||
5. **最后重渲并逐窗比较**:必须同时看 1024×640 与 1440×900 的诊疗/病例/订单,AI 看 920×760 与 720×560。旧深色 PNG 不再作为通过证据。
|
||||
|
||||
## 10. 最终可验收口径
|
||||
|
||||
- 诊疗:60% 右 Drawer,白 Header/Footer、近白蓝 Body、靛蓝 active/focus/primary,无青色残留;Tabs 与 Footer 不裁切。
|
||||
- 病例:无 Tabs 的纵向白卡流,浅蓝 Hero,4/3 列病例仍可读;AI 报告 secondary 按钮位于患者信息卡标题行右侧。
|
||||
- 订单:20% 淡蓝 scrim + 80% 白/浅蓝 Drawer;五金额卡、处方、收款、履约、物流、日志顺序完整;Footer 固定。
|
||||
- AI 报告:920×760/720×560 都能完整显示标题、snapshot、医疗警示、双模型 Tabs、滚动报告与 Footer;主色与 `#5265F6` 同色系,无青色。
|
||||
- 四窗共同:`#FCFDFE/#FFFFFF/#E2E7F4/#5265F6/#15224A/#7481A3` 形成稳定层级;34/40 px 控件节奏、7/10/12/14 px 圆角层级和 4 px 间距基线一致;语义色不被“全蓝化”。
|
||||
Reference in New Issue
Block a user