first commit

This commit is contained in:
Your Name
2026-09-08 11:40:15 +08:00
commit a5353f7eb5
9568 changed files with 1646214 additions and 0 deletions
+820
View File
@@ -0,0 +1,820 @@
# admin 医生端源码审计
审计日期:2026-08-10
参考项目:D:\web\zyt\admin
审计方式:只读检查 Vue/TypeScript 源码、API 封装、路由守卫、Pinia store、业务组件和权限判断;未修改 admin 项目,也未把 README 当作结论来源。
## 1. 结论摘要
1. admin 是 Vue 3 + TypeScript + Vite + Element Plus + Pinia 项目。医生端并不是一套独立的静态路由:除登录、H5 诊单和只读诊单外,页面路径、标题、组件和菜单权限都由登录后 GET /adminapi/auth.admin/mySelf 返回的 menu 动态注入。
2. 医生相关页面的全局状态很少。Pinia 只持有认证用户、权限、动态菜单、全局站点配置、布局与多标签;接诊台、处方、患者、问诊列表的查询条件和业务状态均保留在各页面的 ref/reactive 中,分页统一使用 usePaging。
3. “患者列表”有两个不同实现:
- 医生/一诊工作台的“我的患者”:src/views/first_visit/my_patients/index.vue,带患者、订单、面诊进度三个工作区,服务端按当前角色和部门数据范围收窄。
- 平台注册用户列表:src/views/consumer/lists/index.vue,仅展示头像、昵称、账号、手机号、渠道和注册时间,不是医生业务患者工作台。
4. “问诊列表”也有两个相关实现:
- 挂号/问诊执行列表:src/views/tcm/appointment/list.vue,默认“今天 + 待接诊”,支持通话、视频二维码、完成、开方、取消。
- 诊单/患者业务列表:src/views/tcm/diagnosis/index.vue,围绕诊单、挂号、确认、开方、医助指派、二维码和视频旁观。
最终菜单叫什么、URL 是什么取决于后端 menu 配置,不应仅根据文件名硬编码。
5. 实际视频问诊主链是 src/components/chat-dialog/index.vue:腾讯云 Chat UIKit 单聊 + TUICallKit 音视频;通话前后还串联后端通话记录、TRTC 房间绑定、云端混流录制、可选浏览器本地录制、截屏写医生备注。src/components/video-call/index.vue 是另一套旧/独立实现,目前源码中没有被任何页面引用。
6. 处方领域要区分三类对象:
- 处方库模板:tcm.prescriptionLibrary,供医生复用药材组合。
- 已开处方:tcm.prescription,处方笺、患者、医师签名、主辅方、审核与作废。
- 处方业务订单:tcm.prescriptionOrder,收货、费用、双审、支付单、药房和物流履约;它不是支付单 zyt_order。
## 2. 关键源码与路由
### 2.1 页面定位
| 业务 | 关键源文件(绝对路径) | 路由结论 |
|---|---|---|
| 登录 | D:\web\zyt\admin\src\views\account\login.vue | 静态精确路由 /login |
| 接诊台 | D:\web\zyt\admin\src\views\patient\reception\index.vue | 动态菜单组件键应指向 patient/reception/index;实际 URL 取 menu[].paths |
| 我的处方库 | D:\web\zyt\admin\src\views\consumer\prescription\list.vue | 动态菜单组件键应指向 consumer/prescription/list;实际 URL 取 menu[].paths |
| 药品库(不是处方库) | D:\web\zyt\admin\src\views\doctor\medicine.vue | 动态菜单组件键应指向 doctor/medicine |
| 已开处方/处方管理 | D:\web\zyt\admin\src\views\consumer\prescription\index.vue | 动态菜单组件键应指向 consumer/prescription/index |
| 处方业务订单 | D:\web\zyt\admin\src\views\consumer\prescription\order_list.vue | 动态菜单组件键应指向 consumer/prescription/order_list |
| 我的患者 | D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue | 动态菜单组件键应指向 first_visit/my_patients/index |
| 平台用户列表 | D:\web\zyt\admin\src\views\consumer\lists\index.vue | 动态菜单组件键应指向 consumer/lists/index |
| 问诊/挂号列表 | D:\web\zyt\admin\src\views\tcm\appointment\list.vue | 动态菜单组件键应指向 tcm/appointment/list |
| 诊单列表 | D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue | 动态菜单组件键应指向 tcm/diagnosis/index;另有静态 H5 路由 /tcm/diagnosis/h5 |
| 诊单编辑/只读抽屉 | D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue | 被多个页面异步复用,不一定是独立菜单 |
| 患者只读详情 | D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue | 静态精确路由 /tcm/diagnosis-readonly?id=诊单ID |
| 预约视频问诊 | D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue | 抽屉组件,由诊单列表/我的患者调用 |
| 诊间开方 | D:\web\zyt\admin\src\components\tcm-prescription\index.vue | 复用组件,由问诊列表和诊单编辑调用 |
| 聊天与视频问诊 | D:\web\zyt\admin\src\components\chat-dialog\index.vue | 浮动组件,由接诊台及问诊列表调用 |
| 医助视频旁观 | D:\web\zyt\admin\src\views\tcm\diagnosis\components\AssistantWatchCallDialog.vue | 诊单列表内异步组件 |
### 2.2 动态路由机制
关键文件:
- D:\web\zyt\admin\src\router\routes.ts
- D:\web\zyt\admin\src\router\index.ts
- D:\web\zyt\admin\src\permission.ts
- D:\web\zyt\admin\src\stores\modules\user.ts
流程:
1. 常量路由只注册 /login、/403、/change-password、/bind-work-wechat、/user/setting、/doctor/progress、/tcm/diagnosis/h5 和 /tcm/diagnosis-readonly 等少数页面。
2. 登录成功后 GET /auth.admin/mySelf。
3. user store 保存 data.user、data.permissions,并把 data.menu 交给 filterAsyncRoutes。
4. 每个后端菜单项使用以下字段转为 Vue Router:
- paths:路由路径;
- componentsrc/views 下的组件键;
- name:菜单标题;
- perms:写入 route.meta.perms
- is_show:控制 meta.hidden
- is_cache:控制 keepAlive
- params:默认 query
- selectedactiveMenu
- type:目录或菜单。
5. permission.ts 把转换后的路由动态挂到根布局;第一个可见菜单成为 / 的重定向目标。
因此,本仓库源码能确定组件和静态路由,但不能单独确定接诊台、处方库、已开处方、我的患者、问诊列表的生产 URL 与菜单标题。要得到精确值,必须取得当前环境 /auth.admin/mySelf 的 menu 响应或检查服务端菜单表。
## 3. 登录、认证、权限和状态管理
### 3.1 登录链路
关键文件:
- D:\web\zyt\admin\src\views\account\login.vue
- D:\web\zyt\admin\src\api\user.ts
- D:\web\zyt\admin\src\stores\modules\user.ts
- D:\web\zyt\admin\src\utils\request\index.ts
- D:\web\zyt\admin\src\utils\auth.ts
账号密码:
- POST /login/account
- 请求:account、password、terminal=1。
- 响应被页面使用的字段:token、is_paw、need_bind_work_wechat。
- token 写入本地缓存键 token,后续请求通过请求拦截器放到 HTTP 头 token。
- “记住账号”只缓存 account,不缓存密码,缓存键为 account。
企业微信:
- GET /login/workWechatConfig,使用 enabled、corp_id、agent_id。
- 企业微信内置浏览器走 OAuthscope=snsapi_privateinfo、state=admin_login。
- 普通浏览器动态加载 https://wwcdn.weixin.qq.com/node/wework/wwopen/js/wwLogin-1.2.7.js 显示扫码登录。
- 回调 code 通过 POST /login/workWechatLogin,参数 code、terminal=1。
- 另有 POST /auth.admin/bindWorkWechat、POST /auth.admin/unbindWorkWechat。
守卫:
- is_paw=0 强制跳转 /change-password,并通过 POST /login/changeFirstPassword 修改。
- need_bind_work_wechat=true 强制进入 /bind-work-wechat。
- 没有 token 的非白名单路由跳 /login?redirect=原地址。
- /auth.admin/mySelf 没有任何有效菜单时清认证并跳 /403。
- 响应码约定:1 成功、0 失败、-1 登录失效、10 需要绑定企微、2 打开新页面、-2 未安装。
### 3.2 Pinia 与页面状态
| Store/Hook | 文件 | 职责 |
|---|---|---|
| user | D:\web\zyt\admin\src\stores\modules\user.ts | token、userInfo、routes、perms、isPaw;登录、退出、企微登录、加载个人信息 |
| app | D:\web\zyt\admin\src\stores\modules\app.ts | 网站配置、OSS 图片地址、移动端/侧栏状态、视图刷新 |
| tabs | D:\web\zyt\admin\src\stores\modules\multipleTabs.ts | 多标签与 keep-alive 缓存 |
| setting | D:\web\zyt\admin\src\stores\modules\setting.ts | 本地布局、主题配置 |
| usePaging | D:\web\zyt\admin\src\hooks\usePaging.ts | 页码、page_size、loading、count、lists、extend;支持 silent 静默刷新 |
列表接口统一期待服务端 data 为:
- lists:当前页数组;
- count:总数;
- extend:额外统计、日期、权限范围等扩展数据。
页面内筛选和弹窗状态不进入 Pinia。这一约定适合桌面端复用:认证/权限做全局 store,业务工作台保持页面级 store 或 view-model。
### 3.3 权限判断语义
关键文件:
- D:\web\zyt\admin\src\install\directives\perms.ts
- D:\web\zyt\admin\src\utils\perm.ts
需要特别注意两套语义不同:
- v-perms 数组是“任一权限命中即可显示”(OR)。
- hasPermission 数组是“数组内每个权限都必须存在”(AND)。
- permissions 含星号时视为全部权限。
多数业务调用只传单个权限,因此差异暂时不明显;复用时不要把多权限数组在两处互换。
## 4. 业务页面审计
### 4.1 接诊台
源码:
- D:\web\zyt\admin\src\views\patient\reception\index.vue
- D:\web\zyt\admin\src\api\patient.ts
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientInfoCard.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientCaseCard.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\DailyMatrix.vue
- D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue
页面行为:
- 默认显示当天 status=1 待接诊;可切到 status=4 已过号。
- 搜索字段 patient_name;分页 page_no/page_size,固定每页 15。
- 每 5 秒静默刷新队列和已选患者详情;页面隐藏时暂停,恢复可见后立即刷新。
- 队列使用无限滚动,并按 id 去重。
- 队列行主要字段:id、patient_id、patient_name、patient_phone、diagnosis_id、doctor_id/name、assistant_id/name、appointment_date/time、gender、age、status/status_desc、has_prescription、remark。
- 详情结构按源码使用为:
- appointment:挂号;
- diagnosis:诊单/病例;
- doctor_notes:医生备注、舌苔和报告。
- 日常记录不依赖 reception 响应完整下发,而由 DailyMatrix 继续按 diagnosis_id 调 trackingWindow/trackingNotes。
操作:
- 通知医助:POST /doctor.appointment/notifyAssistant,参数 id=挂号ID。
- 发起通话:先 POST /tcm.diagnosis/getCallSignature,参数 patient_id、diagnosis_id,再打开 ChatDialog。
- 备注:POST /doctor.appointment/addDoctorNote,参数 diagnosis_id、content,可追加 tongue_images、report_files。
- 编辑病历:复用 tcm/diagnosis/edit.vue。
- 完成接诊:POST /doctor.appointment/complete,参数 id=挂号ID;页面允许 status=1 或 4。
权限:
- doctor.appointment/addDoctorNote
- tcm.diagnosis/edit
- doctor.appointment/complete
源码中的“通知医助”和“发起通话”按钮没有 v-perms;只能依赖页面菜单权限和后端接口鉴权,桌面端若拆成独立入口应补显式能力判断。
### 4.2 我的处方库
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\list.vue
- D:\web\zyt\admin\src\api\tcm.ts
- D:\web\zyt\admin\src\components\medicine-name-select\index.vue
模型与筛选:
- 查询:prescription_name、formula_type(主方/辅方)、is_public(0 仅自己、1 所有人)。
- 列表:id、prescription_name、formula_type、herbs、is_public、disable_edit、creator_id/name、create_time。
- herbs 项:medicine_id(可选)、name、dosage。
- 编辑:id、prescription_name、formula_type、herbs、is_public、disable_edit。
- disable_edit=1 表示导入模板后锁定整张处方的药材,不可增删改,只能再次导入覆盖。
接口:
- GET /tcm.prescriptionLibrary/lists
- POST /tcm.prescriptionLibrary/add
- POST /tcm.prescriptionLibrary/edit
- POST /tcm.prescriptionLibrary/delete,参数 id
- GET /tcm.prescriptionLibrary/detail,参数 id(API 已封装,但当前列表弹窗直接使用行数据)
权限:
- wcf.prescription/add
- wcf.prescription/read
- wcf.prescription/edit
- wcf.prescription/delete
所有权:
- 普通用户只可编辑/删除 creator_id 等于当前 userInfo.id 的模板。
- root=1 或 role_ids 包含 0、3 可管理全部模板。
- 诊间/已开处方导入模板时会额外传 prescribing_creator_id,通常取处方 creator_id,新增时取当前登录用户 id。
### 4.3 药品库(容易和处方库混淆)
源码:D:\web\zyt\admin\src\views\doctor\medicine.vue
接口:
- GET /doctor.medicine/lists
- POST /doctor.medicine/add
- POST /doctor.medicine/edit
- POST /doctor.medicine/delete
- GET /doctor.medicine/detail
模型:
- id、name、supplier、unit、settlement_price、retail_price、stock、image、status、remark。
- 图片上传直接 POST 到 VITE_APP_BASE_URL + /api/upload/image,并携带 token 头。
当前页面的增删改按钮没有 v-perms。它是药材主数据管理,不应直接当作“我的处方库”复刻。
### 4.4 已开处方/处方管理
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\index.vue
- D:\web\zyt\admin\src\components\tcm-prescription\index.vue
- D:\web\zyt\admin\src\api\tcm.ts
列表筛选:
- sn:处方编号模糊查;
- patient_name
- creator_ids:开方医师多选;
- audit_filterall、pending、passed、not_passed、rejected
- source_filterall、manual、system
- start_time、end_time(按创建时间)。
列表核心字段:
- id、sn、prescription_type
- is_system_auto0 手工、1 空白处方/系统代开;
- patient_name、gender、age、phone
- audit_status、audit_remark、business_prescription_audit_rejected、business_prescription_audit_remark
- void_status、void_by_name、void_time
- doctor_name、creator_id、assistant_name、prescription_date、create_time
- has_prescription_order。
处方编辑/详情模型:
- 关联:id、diagnosis_id、creator_id。
- 患者:patient_name、gender、age、visit_no、prescription_date。
- 诊断:tongue、tongue_image、pulse、pulse_condition、clinical_diagnosis。
- 药材:herbs,每项 medicine_id、name、dosage、formula_type(主方/辅方)、locked。
- 剂型/用法:prescription_type、dosage_amount、dosage_unit、dosage_bag_count、need_decoction、bags_per_dose、dose_count、dose_unit、usage_days、times_per_day、usage_instruction、usage_time、usage_way、dietary_taboo、usage_notes。
- 辅方用法 aux_usagedosage_amount、dosage_bag_count、need_decoction、bags_per_dose、times_per_day、usage_days、prescription_name(部分页面保留模板名)。
- 医师:doctor_name、doctor_signaturePNG data URL,保存前必填)。
- 可见性/审核:is_shared、visible_role_ids、audit_status、audit_time、audit_by_name、audit_remark。
状态规则:
- audit_status0 待审核、1 已通过、2 已驳回。
- 驳回处方会同时作废。
- “已通过且未作废”的有效处方不能普通编辑/删除。
- 新增时前端强制 audit_status=0;编辑保存后提示重新进入待审核。
- 诊间开方组件会保存 case_record 病历快照,并在已有 appointment_id 处方时直接进入只读查看。
- 已存在业务订单时,诊间组件禁止作废处方。
主要接口:
- GET /tcm.prescription/lists
- GET /tcm.prescription/detail,参数 id
- POST /tcm.prescription/add
- POST /tcm.prescription/edit
- POST /tcm.prescription/delete,参数 id
- POST /tcm.prescription/patchPatient,参数 id、patient_name、phone、gender
- POST /tcm.prescription/audit,参数 id、action=approve|reject、remark
- POST /tcm.prescription/void,参数 id
- GET /tcm.prescription/listByDiagnosis,参数 diagnosis_id
- GET /tcm.prescription/getByAppointment,参数 appointment_id
权限:
- cf.prescription/add、read、edit、audit、del
- tcm.prescription/patchPatient
- tcm.prescriptionLibrary/lists
- tcm.prescriptionOrder/create、lists、setShipMode
- finance.account_log/lists
- tcm.prescriptionOrder/editRemarkExtra
角色补充:消费者处方页将 root 或 role_ids 0、3 视为可审核角色;仍应以后端和 cf.prescription/audit 为最终判定。
### 4.5 处方业务订单
源码:
- D:\web\zyt\admin\src\views\consumer\prescription\order_list.vue
- D:\web\zyt\admin\src\views\consumer\prescription\components\PrescriptionOrderDetailDrawer.vue
- D:\web\zyt\admin\src\views\consumer\prescription\components\prescription-order-utils.ts
此页面是已开处方的相邻履约域。核心字段:
- id、order_no、prescription_id、diagnosis_id
- recipient_name、recipient_phone、region、shipping_address
- fee_type、amount、internal_cost
- prescription_audit_status、payment_slip_audit_status
- fulfillment_status
- linked_pay_order_count、linked_pay_order_id、linked_pay_paid_total
- medication_days、service_channel、service_package
- express_company、tracking_number、ship_mode
- doctor_name、creator_id/name、assistant_id
- remark_extra、remark_assistant
- 药房提交号和状态。
审核状态统一为 0 待审核、1 已通过、2 已驳回。履约状态:
- 1 待双审通过
- 2 待发货
- 3 已完成
- 4 已取消
- 5 已发货
- 6 已签收
- 7 进行中
- 8 暂不制药
- 9 拒收
- 10 退款
- 11 保留药方
- 12 制药缓发
核心接口:
- GET /tcm.prescriptionOrder/lists、detail、paidPayOrders、logs、logisticsTrace、export
- POST /tcm.prescriptionOrder/create、edit、withdraw、ddcode
- POST /tcm.prescriptionOrder/auditPrescription、auditPayment、revokeRxAudit、revokePayAudit
- POST /tcm.prescriptionOrder/ship、complete、refund、requestCompletion
- POST /tcm.prescriptionOrder/addPayOrder、linkPayOrder
- POST /tcm.prescriptionOrder/patchPrescriptionPatient、patchPrescriptionUsage、updateAmount
- POST /tcm.prescriptionOrder/setShipMode、uploadToPharmacy
- POST /tcm.prescriptionOrder/submitGancaoRecipel、previewGancaoRecipel、confirmGancaoSubmission
- POST /tcm.prescriptionOrder/batchAssignAssistant、addLog
主要权限:
- tcm.prescriptionOrder/detail、edit、export、ddcode、ship、addPayOrder、complete、refund、withdraw
- tcm.prescriptionOrder/auditPrescription、auditPayment
- tcm.prescriptionOrder/setShipMode、uploadToPharmacy、editRemarkExtra
- finance.account_log/lists、prescription.order/finance
前端还存在角色级显示规则:
- role 2:医助;
- role 6:下单角色;
- role 3、8:下单筛选豁免;
- role 0、3、6:财务字段;
- role 0、3:可绕过双审后的创建人编辑锁、可批量改派;
- 业务订单处方审核角色在共享工具中为 0、3、6。
这些数字与服务端配置耦合,不宜在新客户端再次散落硬编码。
### 4.6 我的患者
首选医生端实现:
- D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue
- D:\web\zyt\admin\src\api\first_visit.ts
页面结构:
- “患者列表”“订单管理”“面诊进度”三个工作区。
- 筛选 keyword、status_filter、start_date、end_date。
- status_filterunbooked 未预约、pending_interview 待面诊、completed 已完成、missed 已过号。
- 日期快捷:今天、明天、后天、近 7 天、近 30 天、自定义。
- extend.summary 返回 today/tomorrow/day_after 计数。
- extend.dates 返回对应日期。
- extend.scope.label 直接展示后端判定的数据范围。
列表使用字段:
- diagnosis_id 或 id、source_patient_id
- patient_name、gender_desc、age、phone_masked、has_id_card
- assistant_id/name
- appointment_id、appointment_doctor_id/name、appointment_status、appointment_status_text、appointment_time_text
- revisit_count、confirmed、confirmation_text、diagnosis_date_text。
操作与接口:
- GET /firstvisit.myPatient/lists
- GET /firstvisit.myPatient/assistants
- POST /firstvisit.myPatient/assignid、assistant_id、is_inherit=0|1
- POST /firstvisit.myPatient/fillIdCardid、id_card
- POST /firstvisit.myPatient/createAppointment:预约完整参数
- POST /firstvisit.myPatient/cancelAppointmentid=挂号ID
- 订单工作区另使用 /firstvisit.myPatient/orders、orderDetail、orderEdit 及双审/发货/退款等受限代理接口。
- 面诊进度使用 GET /firstvisit.myPatient/progress。
权限:
- tcm.diagnosis/edit
- tcm.diagnosis/readonlyDetail
- tcm.diagnosis/guahao
- tcm.diagnosis/assign
服务端按当前角色与部门范围裁剪数据;前端不自行拼接 doctor_id 或 department_id 来模拟数据权限。
平台用户列表 D:\web\zyt\admin\src\views\consumer\lists\index.vue 使用 GET /user.user/lists,字段是 avatar、nickname、account、mobile、channel、create_time,只适合账号管理,不适合医生患者列表。
### 4.7 问诊/挂号列表
源码:
- D:\web\zyt\admin\src\views\tcm\appointment\list.vue
- D:\web\zyt\admin\src\api\doctor.ts
默认条件:
- status=1 待接诊;
- start_date=end_date=今天;
- date_preset=today
- 20 秒静默轮询;
- include_status_counts=1 时从 extend.status_count 一次返回各状态角标。
状态:
- 1 待接诊/已预约
- 2 已取消
- 3 已完成
- 4 已过号
筛选:
- patient_name、doctor_name
- status
- start_date、end_date、date_preset
- diagnosis_confirmed
- assistant_dept_id(选父部门含子级)。
行字段:
- id、patient_id、diagnosis_id
- patient_name、patient_phone、gender、age、height、weight
- doctor_id/name、assistant_name
- appointment_date、appointment_time、period
- diagnosis_confirmed
- has_prescription、prescription_is_system_auto、prescription_audit_status、prescription_void_status
- status/status_desc、remark。
操作:
- 编辑患者:复用诊单编辑抽屉。
- 视频二维码:生成小程序码。
- 通话:获取签名并打开 ChatDialog。
- 完成:POST /doctor.appointment/complete,可同时 POST addDoctorNote。
- 开方/查看:复用 TcmPrescription;有效已审核处方显示“查看”。
- 取消:POST /doctor.appointment/cancel。
权限:
- tcm.diagnosis/edit
- tcm.diagnosis/videoQr
- doctor.appointment/prescription
- doctor.appointment/complete
- tcm.diagnosis/kaifang
- doctor.appointment/cancel
- doctor.appointment/addDoctorNote(完成时备注能力)
角色判断:role_id 为 1 医生、2 医助;两者之外才显示医生姓名筛选。此处同时兼容 role_id 单值或数组,但其他页面多使用 role_ids,说明 user 模型尚未完全统一。
### 4.8 诊单列表
源码:
- D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue
列表筛选字段:
- keyword、diagnosis_type、syndrome_type、assistant_id
- diagnosis_confirmed、appointment_date、has_appointment
- latest_appointment_start_date/end_date/channel_source
- latest_assign_start_date/end_date
- pending_booking、completed_appointment、pending_assign
- pending_assign_order_month、pending_assign_keyword
- sort_unserved_days。
列表使用字段:
- id、patient_name、gender_desc、age
- assistant_id/assistant、assign_read_at
- appointments 或聚合的 appointment_doctor_name、appointment_time_text、appointment_status
- has_appointment、diagnosis_confirmed
- has_prescription、followup_time_text、followup_doctor_name、followup_rx_voided
- unserved_days、last_blood_record_at
- video_call_hint。
video_call_hint
- statenone、pending_room、live 等;
- label
- start_time、end_time。
操作权限:
- tcm.diagnosis/add、edit、delete、readonlyDetail
- tcm.diagnosis/assign
- tcm.diagnosis/kaifang
- tcm.diagnosis/guahao、guahaoLogList
- tcm.diagnosis/videoQr
- tcm.diagnosis/order
- tcm.diagnosis/watchCall
诊单编辑模型的核心字段:
- 标识:id、patient_id。
- 患者:patient_name、id_card、phone、gender、age、marital_status、height、weight、region。
- 诊断:diagnosis_date、diagnosis_type、syndrome_type、diabetes_type、diabetes_discovery_year、local_hospital_diagnosis、local_hospital_name。
- 指标:systolic_pressure、diastolic_pressure、fasting_blood_sugar。
- 现病史多选:appetite、water_intake、diet_condition、weight_change、body_feeling、sleep_condition、eye_condition、head_feeling、sweat_condition、skin_condition、urine_condition、stool_condition、kidney_condition、fatty_liver_degree。
- 既往史:past_history、trauma_history、surgery_history、allergy_history、family_history、pregnancy_history。
- 医疗内容:symptoms、tongue_coating、pulse、treatment_principle、prescription、doctor_advice、remark、current_medications。
- 归属与来源:assistant_id、status、create_source、show_card、external_userid。
详情响应还使用 patient_basic_locked、can_edit_patient_basic、latest_prescription_order。手机号/身份证是否显示明文由 tcm.diagnosis/phonePlain 控制;已有身份证通常只有明文权限才能修改。
诊单详情 Tab 权限:
- tcm.diagnosis/chufang:处方
- tcm.diagnosis/patientOrders:业务订单
- tcm.diagnosis/huifang:视频回放
- tcm.diagnosis/chat:聊天记录
- tcm.diagnosis/assign 或 detail:指派记录
- doctor.appointment/lists:挂号记录
- tcm.diagnosis/dailyRecord:日常记录
## 5. 视频问诊完整链路
### 5.1 预约
源码:D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue
目前预约类型只有 video。请求字段:
- patient_id
- doctor_id
- appointment_date
- period=all
- appointment_time
- appointment_type=video
- remark
- channel_source
- channel_source_detail
普通入口 POST /doctor.appointment/create;“我的患者”入口 POST /firstvisit.myPatient/createAppointment。可用时段来自 GET /doctor.appointment/availableSlots,字段 doctor_id、appointment_date、period=all,响应使用 slots[].time、slots[].available。页面还读取医生排班并限制不能重复预约当天 status=1/4 的号。
### 5.2 医生发起通话
生产主组件:D:\web\zyt\admin\src\components\chat-dialog\index.vue
1. 页面调用 open({ patientId, patientName, diagnosisId })。
2. POST /tcm.diagnosis/getCallSignature,请求 patient_id、diagnosis_id。
3. 响应实际使用:
- sdkAppId
- userId(医生 IM/TRTC user ID
- userSig
- patientUserId,缺省回退 patient_加患者ID
- assistant_id(群视频邀请)
- isLochostVod(是否启用浏览器本地录制)
4. Chat UIKit 登录并创建与 patientUserId 的 C2C 会话。
5. TUICallKitServer.init 初始化通话能力;只有成功后才显示 AudioCallPicker、VideoCallPicker 和群视频按钮。
6. beforeCalling 时 POST /tcm.diagnosis/startCall
- diagnosis_id
- patient_id
- call_type=2(视频)
7. 呼叫状态进入 calling/connected 后从 TUIStore 或 TUICallEngine 捕获 roomID/strRoomID,再 POST /tcm.diagnosis/bindCallRoom
- diagnosis_id
- room_id(字符串)
8. bindCallRoom 的响应可带 cloud_recording.started、task_id、message;源码注释说明后端在这里触发腾讯云 CreateCloudRecording,混流模式由后端负责。
9. 接通后,若 isLochostVod=true,前端从 TUICallKit 视频元素启动 MediaRecorder/Canvas 本地录制。
10. 通话结束、挂断、IM 自定义挂断消息或用户关闭窗口时先 POST /tcm.diagnosis/endCall,再完成本地视频上传并 POST attachLocalCallRecording。
群视频调用 TUICallKitServer.callsuserIDList=[patientUserId, assistant_id]type=VIDEO_CALL。
### 5.3 截屏、录制与回放
关键文件:
- D:\web\zyt\admin\src\utils\call-local-recorder.ts
- D:\web\zyt\admin\src\utils\call-video-screenshot.ts
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\CallRecordPanel.vue
- D:\web\zyt\admin\src\views\tcm\diagnosis\components\RecordingPlaybackBlock.vue
视频浮窗“截屏”会:
1. 抓取当前 video frame
2. 上传图片;
3. POST /doctor.appointment/addDoctorNote,把路径追加到 tongue_images。
通话记录字段:
- id、call_type1 语音、其他视为视频)
- room_id
- status:1 进行中、2 已结束、3 未接听、4 已取消
- recording_status_text
- recording_urls_list
- start_time_text、end_time_text、duration_text
接口:
- GET /tcm.diagnosis/getCallRecordsdiagnosis_id
- POST /tcm.diagnosis/attachLocalCallRecordingdiagnosis_id、file_url、可选 call_record_id
- POST /tcm.diagnosis/createManualCallRecorddiagnosis_id
- POST /tcm.diagnosis/startCloudRecordingdiagnosis_idAPI 有封装,主组件当前通过 bindCallRoom 的后端联动启动)
### 5.4 医助旁观
入口在诊单列表。只有同时满足:
- 当前 userInfo.id 等于该诊单 assistant_id
- 拥有 tcm.diagnosis/watchCall
- video_call_hint.state=live
才可真正进入。
GET /tcm.diagnosis/watchCall,参数 diagnosis_id,响应使用:
- sdkAppId
- userId
- userSig
- roomId 或 strRoomId
- patientName
旁观组件直接使用 trtc-sdk-v5 进入房间,只调用 startRemoteVideo,不开启本地摄像头或麦克风。pending_room 时显示入口提示但点击会阻止进入,等待房间号同步。
### 5.5 视频二维码
页面通过 POST /tcm.diagnosis/generateMiniProgramQrcode 生成 qrcode_url,常用字段:
- diagnosis_id
- patient_id
- doctor_id(部分入口)
- share_user_id
- mini_program_path=pages/login/login(问诊列表入口)
调用前先 GET 小程序配置并校验 app_id。
## 6. API 总表
所有 URL 会被请求层加上 baseUrl 和 adminapi 前缀;下表写的是 API 封装中的业务路径。
### 6.1 认证
| 方法 | 路径 | 关键请求/响应 |
|---|---|---|
| POST | /login/account | account、password、terminal;返回 token、is_paw、need_bind_work_wechat |
| POST | /login/workWechatLogin | code、terminal;返回同登录结果 |
| GET | /login/workWechatConfig | enabled、corp_id、agent_id |
| GET | /auth.admin/mySelf | 返回 user、permissions、menu |
| POST | /login/logout | 退出 |
| POST | /login/changeFirstPassword | password、password_confirm |
| POST | /auth.admin/bindWorkWechat | code |
### 6.2 挂号与接诊
| 方法 | 路径 | 关键字段 |
|---|---|---|
| GET | /doctor.appointment/lists | status、start_date、end_date、patient_name、doctor_name、diagnosis_confirmed、assistant_dept_id、page_no、page_size |
| GET | /doctor.appointment/reception | id=挂号ID;返回 appointment、diagnosis、doctor_notes |
| GET | /doctor.appointment/detail | id |
| GET | /doctor.appointment/availableSlots | doctor_id、appointment_date、period |
| POST | /doctor.appointment/create | patient_id、doctor_id、appointment_date/time、appointment_type、渠道等 |
| POST | /doctor.appointment/cancel | id |
| POST | /doctor.appointment/complete | id |
| POST | /doctor.appointment/notifyAssistant | id |
| POST | /doctor.appointment/addDoctorNote | diagnosis_id、content、tongue_images、report_files |
| GET | /doctor.appointment/doctorNotes | diagnosis_id |
| POST | /doctor.appointment/deleteDoctorNoteImage | note_id、image_type、image_path |
### 6.3 诊单
| 方法 | 路径 | 关键字段 |
|---|---|---|
| GET | /tcm.diagnosis/lists | 诊单列表全部筛选 + page_no/page_size;返回 lists/count/extend |
| GET | /tcm.diagnosis/detail | id |
| GET | /tcm.diagnosis/readonlyDetail | id;返回 appointment、diagnosis、unserved_days、last_blood_record_at、doctor_notes |
| POST | /tcm.diagnosis/add | 完整诊单模型 |
| POST | /tcm.diagnosis/edit | 完整诊单模型 |
| POST | /tcm.diagnosis/delete | id |
| POST | /tcm.diagnosis/assign | id、assistant_id、可选 is_inherit;批量场景由前端逐条调用 |
| GET | /tcm.diagnosis/getAssistants | 医助选项 |
| GET | /tcm.diagnosis/getDoctors | 医生选项 |
| POST | /tcm.diagnosis/checkPhone | phone 及排除 id |
| POST | /tcm.diagnosis/checkIdCard | id_card 及排除 id |
| POST | /tcm.diagnosis/fillIdCard | id、id_card |
| GET | /tcm.diagnosis/trackingWindow | id、start_date、end_date |
| GET | /tcm.diagnosis/trackingNotes | diagnosis_id |
| POST | /tcm.diagnosis/addTrackingNote | diagnosis_id、tracking_content |
### 6.4 处方与业务订单
处方、处方库、业务订单接口已在 4.2、4.4、4.5 分节完整列出。实现时必须保留三个资源命名空间,不要把 prescription、prescriptionLibrary、prescriptionOrder 合并成一个“处方”接口。
### 6.5 视频
| 方法 | 路径 | 关键字段 |
|---|---|---|
| POST | /tcm.diagnosis/getCallSignature | patient_id、diagnosis_id;返回 sdkAppId、userId、userSig、patientUserId、assistant_id、isLochostVod |
| POST | /tcm.diagnosis/startCall | diagnosis_id、patient_id、call_type |
| POST | /tcm.diagnosis/bindCallRoom | diagnosis_id、room_id |
| POST | /tcm.diagnosis/startCloudRecording | diagnosis_id |
| POST | /tcm.diagnosis/endCall | diagnosis_id |
| GET | /tcm.diagnosis/getCallRecords | diagnosis_id |
| POST | /tcm.diagnosis/attachLocalCallRecording | diagnosis_id、file_url、可选 call_record_id |
| POST | /tcm.diagnosis/createManualCallRecord | diagnosis_id |
| GET | /tcm.diagnosis/watchCall | diagnosis_id |
| POST | /tcm.diagnosis/generateMiniProgramQrcode | diagnosis_id、patient_id、doctor_id、share_user_id 等 |
## 7. 可复用约定
1. 请求协议
- baseURL 来自 VITE_APP_BASE_URL,统一 URL 前缀 adminapi。
- token 放在名为 token 的请求头,不是 Bearer Authorization。
- POST 默认把 params 转为 bodyGET 使用 params。
- 标准成功响应是 code=1,业务数据自动解包为 data。
- GET 网络失败默认最多重试 2 次,POST 不自动重试。
2. 列表协议
- 请求 page_no、page_size。
- 响应 lists、count、extend。
- 定时刷新使用 getLists({ silent: true }),避免表格白屏闪烁。
3. 标识约定
- diagnosis_id 是诊单主键。
- appointment.id 是挂号主键。
- prescription.id 是处方主键。
- prescriptionOrder.id 是处方业务订单主键。
- patientUserId 是腾讯云 IM/TRTC 用户名,通常 patient_加患者标识。
4. 隐私
- 默认手机号 3-4-4 脱敏,身份证保留前 6 后 4。
- tcm.diagnosis/phonePlain 控制诊单编辑中的明文能力。
- 数据范围由后端按角色、部门、归属医助裁剪,前端只做 UI 能力门控。
5. 复用组件
- 患者摘要/病例:PatientInfoCard、PatientCaseCard。
- 日常记录:DailyMatrix。
- 医生备注:NoteTimeline。
- 诊单编辑和只读:tcm/diagnosis/edit.vue 的 open、openViewOnly。
- 处方开立/查看:TcmPrescription 的 open、openById。
- 视频通讯:ChatDialog 的 open。
- 业务订单详情:PrescriptionOrderDetailDrawer。
6. 状态文本
- 不建议在新客户端重复定义状态映射。优先抽取 D:\web\zyt\admin\src\views\consumer\prescription\components\prescription-order-utils.ts 中的审核、履约、支付、供货和物流格式化逻辑为共享领域模块。
## 8. 未知点、歧义与风险
1. 动态菜单缺口:admin 前端仓库没有生产环境 /auth.admin/mySelf 的 menu 数据,因此接诊台、处方库、患者、问诊列表的精确 URL、菜单标题和页面级 route.meta.perms 仍未知。
2. API 类型不足:tcm.ts、doctor.ts 多数参数和返回值是 any;本文列出的响应字段来自实际页面读取,不等于完整服务端 schema。后续实现应抓取真实响应或检查服务端 DTO。
3. patient_id 语义有重载:
- 挂号行中 patient_id 常被当作诊单/患者标识;
- 我的患者 openAppointment 又把 diagnosis_id 或 id 同时写进 id 和 patient_id
- ChatDialog 则把它转换为 patient_前缀的腾讯云用户。
新客户端必须先确认数据库实体关系,不能只按字段名推断。
4. 接诊台发起通话使用 diagnosis_id || row.idrow.id 本身是挂号 ID。若后端要求真正诊单 ID,这个回退可能只在特定历史数据下成立。
5. 视频二维码参数疑点:tcm/appointment/list.vue 的一个入口把 diagnosis_id 赋为 row.doctor_id,而其他入口使用真正诊单 ID;这很可能是历史兼容或缺陷,应向后端核实后再复用。
6. 权限命名不统一:
- 处方库用 wcf.prescription/*
- 已开处方用 cf.prescription/*
- 新增能力又混用 tcm.prescription/* 与 tcm.prescriptionOrder/*。
不能按字符串前缀自动推导资源。
7. 角色配置存在差异:
- 消费者处方审核页面写死 0、3
- 业务订单共享工具写死 0、3、6
- 多处注释都声称与服务端配置一致。
最终角色应由服务端下发 capability,避免继续硬编码。
8. 权限 UI 不是安全边界:部分接诊、药品和通话按钮没有 v-perms;所有写接口必须继续依赖服务端鉴权。
9. 双 SDK 并存:
- 实际 ChatDialog 使用 @tencentcloud/call-uikit-vue
- 未引用的 video-call/index.vue 使用 @trtc/calls-uikit-vue
- 医助旁观直接使用 trtc-sdk-v5。
新项目应明确只保留一套主叫/被叫 UI SDK,并将纯 TRTC 旁观作为独立只拉流能力。
10. src/components/video-call/index.vue 当前没有被任何 Vue/TS 源码引用,不应误认为生产主链。
11. PatientCaseCard 的 caseTypeLabel 无论 consultation_type 都返回“复诊”,属于明显展示逻辑疑点。
12. 录制启动/停止部分依赖 TUICallKit 内部 store、引擎属性和方法包装,升级腾讯云 SDK 时风险较高,必须用真实双端通话、拒接、对端挂断、网络中断和房间号延迟场景回归。
13. 处方业务订单大量前端角色规则与 server/config/project.php 注释耦合;当前审计范围只有 admin 前端,无法验证服务端配置是否已同步。
## 9. 面向新医生端的建议映射
若新项目要复刻医生工作流,建议按领域而不是按现有目录命名:
- /login:复用认证协议和企业微信登录。
- /reception:复用接诊台队列、详情、5 秒静默刷新与 ChatDialog。
- /prescription-library:复用 prescriptionLibrary 模板及所有权规则。
- /prescriptions:复用 tcm.prescription 列表、审核、作废、打印/下载。
- /patients:优先复用 firstvisit.myPatient,而不是 user.user/lists。
- /consultations:复用 doctor.appointment/lists 的今天待接诊视图。
- /diagnoses:复用 tcm.diagnosis/lists 的完整诊单工作台。
- /video-consultation:主叫链路复用 ChatDialog/TUICallKit;医助旁观保持独立 TRTC 只拉流组件。
这些建议 URL 是新端的信息架构建议,不是对 admin 当前动态 URL 的断言。
+461
View File
@@ -0,0 +1,461 @@
# 医生桌面端工程架构与打包方案(Windows / macOS
> 结论先行:采用 **Python 3.12 + PySide6 Qt Widgets** 构建原生业务界面,以分层的 `httpx` API client 连接现有后端;会话、权限、离线队列和本地安全存储统一放在 core 层。视频不是整套应用的实现基础,而是独立的可选集成:只有当现有腾讯 TRTC Web 方案无法由原生 SDK 替代时,才在受限的 `QWebEngineView` 中承载单一视频页面。发布使用 **PyInstaller onedir**Windows 与 macOS 必须在各自原生 CI runner 上分别构建、签名和验收,不能交叉编译。
## 1. 已知上下文、边界与待确认项
本结论只对 `admin/package.json` 和环境配置做了最小只读核对,没有审计管理端实现。
- 管理端以 `VITE_APP_BASE_URL` 注入后端根地址;示例文件故意留空,开发示例注释仅以 `http://127.0.0.1:8080` 举例。现有环境文件还出现了 `https://css.zhenyangtang.com.cn/``https://admin.zhenyangtang.com.cn/` 和 60 秒请求超时,但这些地址可能是网关或前端站点,**不能据此认定为稳定的桌面 API 地址**。
- 管理端依赖包含 Axios、腾讯 TRTC/Call/Chat UI、`hls.js` 和 COS JS SDK。可以据此判断视频、聊天、流媒体和对象存储是潜在集成面,但不能推断接口路径、认证协议、权限码或 RTC 凭证格式。
- 桌面端不得读取或复用 Vite 环境变量,不得硬编码管理端 URL,也不得在客户端持有 COS Secret、TRTC SecretKey 或任何服务端签名密钥。
编码前必须由后端确认以下契约,并固化为 OpenAPI 或最小接口文档:
1. API 的正式 base URL、版本前缀、响应 envelope、错误码、分页和时间格式。
2. 登录协议(账号密码、短信、SSO/OIDC 或 Cookie)、access/refresh 生命周期、登出和吊销语义。
3. `/me` 等当前用户接口返回的医生身份、机构/租户、角色与细粒度权限码。
4. 预约、患者、病历、处方等写操作的幂等键、乐观锁版本号及审计要求。
5. 聊天的拉取/推送协议、断线续传游标;COS 上传应由后端提供短期预签名 URL 或临时凭证。
6. TRTC 房间、`userSig` 等凭证必须由后端短时签发;确认现有 Web 页面能否作为受支持的嵌入入口。
7. 桌面端的 CORS、代理、私有 CA、设备绑定、强制升级和最低版本策略。
在这些问题确认前可以完成壳层、接口抽象和模拟服务器,但不应猜测生产 endpoint。
## 2. 目标平台与技术选择
### 2.1 建议支持矩阵
| 项目 | 首发建议 | 说明 |
| --- | --- | --- |
| Python | CPython 3.12,固定 patch 版本 | 生命周期长,第三方包成熟;每个平台使用相同 minor |
| Windows | Windows 10 22H2 / Windows 11x86-64 | ARM64 可作为后续独立制品,不与 x64 混装 |
| macOS | macOS 13+,先 arm64,再按客户量增加 x86-64 | 当前 PySide6 wheel 的最低系统版本必须在锁版本时再次核对 |
| UI | PySide6 Qt Widgets | 医疗表单、表格、快捷键、打印和可访问性更稳定 |
| 视频 | 可选 PySide6 Addons / QtWebEngineWidgets | 只隔离承载视频页,不用 WebEngine 包住整个应用 |
| 打包 | PyInstaller onedir | 对 QtWebEngine helper、资源、签名和启动性能最稳妥 |
macOS 推荐分别产出 `arm64``x86_64` 制品。`universal2` 只有在 Python、PySide6 和所有二进制依赖均提供 universal2 slice,且真实验证签名/视频后再启用;两个单架构制品更易排障且体积更小。
### 2.2 依赖分档
建立一份代码、两种构建 profile:
- `core``PySide6-Essentials``httpx``pydantic``pydantic-settings``platformdirs``keyring``cryptography`。包含 QtCore/Gui/Widgets/Network/Sql/Svg/PrintSupport,不包含 WebEngine。
- `video`:在 core 上增加与 Essentials **完全相同版本**的 `PySide6-Addons`,从而获得 QtWebEngineWidgets、WebChannel、Multimedia 等模块。
- 开发/测试:`pytest``pytest-qt``respx``coverage``ruff``mypy``pip-audit`
- 构建:锁定 `PyInstaller` 及其 hooks 版本。以 2026-08-10 可验证组合为基线,可先验证 Python 3.12 + PySide6 Essentials/Addons 6.11.1 + PyInstaller 6.21.0;只有在两端打包 smoke test 通过后才更新锁。
不要同时安装 PyQt、PySide2 或系统级 PySide6;必须从干净虚拟环境构建。使用平台专属、带 hash 的锁文件(例如 `requirements-win-x64.lock``requirements-macos-arm64.lock`),而不是在发布任务中直接安装“最新版”。
如果所有医生都需要视频,可只发布 `video` 制品;仍保留 profile 边界,以便定位 WebEngine 问题。若视频是少数场景,可以发布 core 制品并在系统浏览器打开受支持的视频页,避免让每个安装包承担 Chromium 的体积和攻击面。
## 3. 分层架构
依赖方向固定为:`ui -> application -> domain``infrastructure` 在 composition root 中实现 domain/application 定义的 port。View 不允许直接调用 `httpx`、SQLite 或 keyring。
```text
app/
├─ pyproject.toml # 项目元数据、依赖分组、工具配置
├─ requirements/ # 各 OS/架构的发布锁及 hash
├─ src/
│ └─ zyt_doctor/
│ ├─ __main__.py # 极薄入口,只调用 bootstrap.main()
│ ├─ bootstrap.py # QApplication、配置、DI、异常钩子、主窗口
│ ├─ build_info.py # 版本、commit、channel;构建时生成
│ ├─ config/
│ │ ├─ models.py # 强类型配置及校验
│ │ └─ loader.py # defaults -> 受管配置 -> 开发环境变量
│ ├─ domain/
│ │ ├─ identity.py # Principal、Tenant、Permission
│ │ ├─ errors.py # 与 UI/HTTP 无关的错误类型
│ │ └─ ports.py # Repository、Clock、SecretStore 等协议
│ ├─ application/
│ │ ├─ commands.py # 写用例、幂等键和确认规则
│ │ ├─ queries.py # 读用例和缓存策略
│ │ └─ result.py # Result / Page / OperationState
│ ├─ core/
│ │ ├─ api/
│ │ │ ├─ client.py # httpx client、header、超时、重试
│ │ │ ├─ auth.py # token/cookie adapter 与 refresh single-flight
│ │ │ ├─ errors.py # HTTP/业务错误归一化
│ │ │ ├─ models.py # 公共 DTO
│ │ │ └─ generated/ # 若有 OpenAPI,生成代码仅放此处
│ │ ├─ session/
│ │ │ ├─ manager.py # 会话状态机、锁屏、租户切换、登出清理
│ │ │ └─ permissions.py # 权限快照与 guard
│ │ ├─ storage/
│ │ │ ├─ database.py # SQLite、migration、单写线程
│ │ │ ├─ secure_store.py # Windows Credential Manager / macOS Keychain
│ │ │ └─ cache.py # 加密缓存、TTL、容量控制
│ │ ├─ offline/
│ │ │ ├─ connectivity.py # 网络状态提示,不作为唯一真相
│ │ │ ├─ outbox.py # 离线写队列状态机
│ │ │ └─ sync.py # 重放、冲突和人工处理
│ │ ├─ jobs.py # QThreadPool 任务、取消、signal 适配
│ │ ├─ events.py # 进程内 typed event bus
│ │ ├─ logging.py # 脱敏日志和诊断包
│ │ └─ paths.py # QStandardPaths/platformdirs;绝不写 bundle
│ ├─ modules/
│ │ ├─ auth/
│ │ ├─ dashboard/
│ │ ├─ patients/
│ │ ├─ appointments/
│ │ ├─ consultations/
│ │ ├─ medical_records/
│ │ ├─ prescriptions/
│ │ ├─ chat/
│ │ ├─ followups/
│ │ └─ settings/
│ │ # 每个模块内含 domain.py、service.py、viewmodel.py、views.py、permissions.py
│ ├─ integrations/
│ │ ├─ realtime/ # WebSocket/轮询 adapter,不侵入模块
│ │ ├─ object_storage/ # 仅消费后端预签名 URL/临时凭证
│ │ └─ video/
│ │ ├─ port.py # join/leave/mute 等抽象
│ │ ├─ external_browser.py
│ │ └─ webengine.py # 唯一允许 import QtWebEngine 的文件
│ ├─ ui/
│ │ ├─ shell/ # 导航、标题栏、全局离线/会话提示
│ │ ├─ widgets/ # Loading、Empty、Error、PermissionDenied
│ │ ├─ dialogs/
│ │ └─ theme/
│ └─ resources/ # qrc、图标、字体许可、翻译、默认配置
├─ tests/
│ ├─ unit/
│ ├─ contract/
│ ├─ integration/
│ ├─ ui/
│ ├─ packaging/
│ └─ fixtures/ # 全部为合成数据,禁止生产病患数据
├─ packaging/
│ ├─ windows/doctor-core.spec
│ ├─ windows/doctor-video.spec
│ ├─ macos/doctor-core.spec
│ ├─ macos/doctor-video.spec
│ ├─ macos/entitlements.plist
│ └─ hooks/
├─ scripts/ # build、self-check、sign、notarize
└─ docs/ # API 映射、权限矩阵、发布 runbook
```
每个业务模块只公开一个 facade 和路由描述,例如 `ModuleDescriptor(id, title, permissions, view_factory)`。主壳根据权限注册菜单,模块内再对按钮和 command 做 guard;这样既不会形成一个巨型主窗口,也不会把权限判断散落在控件代码中。
## 4. API client、并发和会话
### 4.1 API client
建议使用一个长生命周期 `httpx.Client`,由 composition root 创建并注入 service。所有同步请求放入 `QThreadPool/QRunnable`,结果通过 Qt signal 回到主线程;严禁 UI 线程阻塞网络。窗口关闭或查询条件变化时取消尚未开始的任务,并忽略带旧 generation id 的晚到响应。
Client 的固定行为:
- production base URL 只允许 HTTPSHTTP 仅在 debug profile 且 host 为 localhost 时允许。
- production 允许的 API、视频和上传 host 必须来自签名/受管配置或内置 allowlist,避免篡改本地配置后窃取 token。
- 超时拆分为 connect/read/write/pool,不只设一个总数。可从 connect 10 秒、read 60 秒起步,上传/导出另设长超时。
- 每次请求加入 `Authorization`(若契约采用 bearer)、`X-Request-ID`、客户端版本、平台、时区和租户信息;不得写入日志的 header 列表默认包含 Authorization、Cookie 和所有临时凭证。
- GET/HEAD 和带服务端认可幂等键的写操作,才可对连接错误、超时、429、502、503、504 做指数退避 + jitter;遵守 `Retry-After`。验证错误、普通 4xx 和未知写请求不自动重试。
- 所有关键 mutation 生成并持久化 `Idempotency-Key`,直到收到确定结果;请求超时后的状态为“结果未知”,先按键查询/重放,不能直接再创建一条。
- 业务错误映射为稳定类型:`ValidationError``Unauthenticated``Forbidden``Conflict``RateLimited``Maintenance``TransportError``UnknownServerError`。UI 不解析后端文案。
- 支持 ETag/版本字段做乐观锁。收到 409/412 时进入冲突页,展示服务器版本与本地草稿,不做静默覆盖。
- 下载/上传流式处理并限制文件大小、MIME 和保存目录。COS 只使用后端签发的预签名 URL或短期临时凭证,绝不打包永久密钥。
若后端有 OpenAPI,生成的 models/client 放入 `core/api/generated`,外面再包一层业务 adapter;模块不得直接依赖生成器的数据结构。若无 OpenAPI,先写小而明确的 typed endpoint,不做一个接受任意 path/dict 的“万能客户端”。
实时聊天使用独立 adapter:WebSocket 可运行在一个后台 asyncio loop/thread 中,通过 signal 投递事件;实现心跳、指数重连、服务器 sequence/cursor 补拉、重复消息去重和应用休眠恢复。首版若后端没有可靠续传契约,应采用短轮询而不是假装 WebSocket 永不丢消息。
### 4.2 Session 状态机
`SessionManager` 是唯一会话真相,显式状态为:
```text
SIGNED_OUT -> AUTHENTICATING -> AUTHENTICATED
AUTHENTICATED -> REFRESHING -> AUTHENTICATED
AUTHENTICATED/REFRESHING -> LOCKED | EXPIRED | SIGNED_OUT
```
- access token 只保存在内存;需要“保持登录”时,refresh token 或可续期凭证保存在 OS keychain,不能放在 QSettings、SQLite 明文或日志。
- `keyring` 启动时必须检查实际 backend。没有 Windows Credential Manager/macOS Keychain 等安全 backend 时禁用持久登录,而不是退化到明文文件。
- 多请求同时遇到 401 时只能有一个 refresh 在飞行,其他请求等待同一个 future;refresh 失败统一切到 `EXPIRED`,避免 401 风暴。
- 登出、切换医生或切换租户时:取消网络任务、停止实时连接、退出视频、清内存 token、清空 WebEngine profile、关闭并按用户/租户清理本地缓存密钥。
- 支持工作站空闲自动锁定。解锁方式由后端安全策略决定;锁定界面不得继续显示患者姓名、通知正文或缩略图。
-`QLocalServer/QLocalSocket` 实现单实例,第二次启动只唤醒现有窗口,避免同一用户同时运行两个 outbox。
### 4.3 权限模型
权限码由后端返回并作为服务端授权的镜像,例如 `patient.read``record.write``prescription.sign`;具体字符串必须以真实契约为准。
客户端执行三层防误操作:
1. 路由层:无模块权限时不注册菜单/路由。
2. ViewModel/command 层:按钮显示与执行前都检查 `PermissionGuard.require(...)`
3. API 层:403 统一转为 `Forbidden`,刷新权限快照并提示“权限已变更”。
这些仅改善体验,真正的 RBAC/ABAC、租户隔离和审计必须由后端再次校验。不能因为客户端隐藏了按钮就省略服务端授权。对开方、签名、删除等高风险操作增加 step-up authentication 或明确二次确认,并把 request id/idempotency key 传给服务端审计。
## 5. 本地数据、离线与错误态
### 5.1 数据目录与加密
使用 `QStandardPaths``platformdirs` 获取每用户目录:Windows 通常位于 `%LOCALAPPDATA%`macOS 位于 `~/Library/Application Support`。安装目录和 `.app` bundle 始终只读;业务代码不要访问 `sys._MEIPASS`,资源通过 `importlib.resources`/Qt resource system 读取。
SQLite 使用 WAL、schema migration 和单写入 worker(或每线程独立 connection),不在线程间共享 `sqlite3.Connection`。默认只缓存必要元数据;如果确需缓存患者/病历或离线草稿:
- 使用 `cryptography` 的 AES-GCM 做版本化记录加密,随机 nonce,密钥由 OS keychain 保存;AAD 包含 tenant/user/table/record id,防止记录调包。
- outbox、cache 和密钥按机构 + 用户分区;退出账号做 crypto-erasure(删除密钥)并清理索引。SSD 上不能承诺可靠覆盖删除,因此不能用“反复覆盖文件”作为安全保证。
- 设置缓存 TTL、容量上限和最少字段;搜索索引不放诊断正文等敏感内容。
- QSettings 只保存主题、窗口大小等无敏感偏好。
- 日志不记录患者姓名、手机号、证件号、病历正文、处方内容、token、Cookie 或 URL query;提供用户确认后的脱敏诊断包。
### 5.2 离线策略
不要只依赖系统“在线/离线”事件;真正状态以最近请求结果和轻量 health check 综合判定。主壳常驻显示 `在线 / 网络不稳定 / 离线 / 服务维护`,且标明数据最后更新时间。
写操作按风险分类:
| 类别 | 离线行为 | 恢复后 |
| --- | --- | --- |
| 只读列表/详情 | 展示有时间戳的加密缓存,明显标记“可能已过期” | 后台重新验证并原子替换 |
| 普通草稿、低风险备注 | 可进入 outbox,保存幂等键、base version 和依赖 | 自动重放;冲突转人工处理 |
| 病历最终提交、开方/签方、医嘱、删除等高风险操作 | 只允许保存为本地草稿,禁止假显示“已提交” | 恢复网络后重新拉取服务端版本,由医生确认再提交 |
| 视频/实时聊天 | 显示不可用或重连,不能伪造发送成功 | 按 cursor 补拉并去重 |
Outbox 状态至少包含 `QUEUED -> SENDING -> SUCCEEDED`,以及 `NEEDS_ATTENTION``DEAD_LETTER`。保存 payload schema version、创建人/租户、幂等键、重试次数、next attempt、最后错误和服务端 base version。只自动重放白名单 action;切换用户时绝不重放前一用户队列。
### 5.3 统一错误体验
所有页面复用下列状态组件,而不是把异常 traceback 或后端原文弹给用户:
- 首次加载 skeleton;刷新时保留旧数据并显示非阻塞进度。
- 真空数据(业务上没有记录)与加载失败严格区分。
- 离线且有缓存、离线且无缓存、权限不足、登录过期、字段校验、版本冲突、维护中、未知错误各有独立文案和可行动按钮。
- 未知错误显示 request id、发生时间、“重试/复制诊断编号”,详细堆栈只进入脱敏日志。
- 全局未捕获异常写入 rotating log 并打开安全错误页;不要自动上传包含医疗数据的 crash dump。
## 6. 视频与 QtWebEngine 的可执行边界
### 6.1 首选集成顺序
1. 先确认腾讯或现有供应商是否提供受支持的 Windows/macOS 原生桌面 SDK 以及 Python 可调用层。如果维护成本可接受,原生 adapter 最可控。
2. 若现有成熟能力是 TRTC Web 页面,后端提供一个专用、窄功能、HTTPS 的 `/desktop-call` 类入口(实际路径待定),由 `QWebEngineView` 嵌入。
3. 若 SDK/UA/DRM/屏幕共享在 Qt Chromium 中不受支持,可靠回退是系统默认浏览器,不通过修改 UA 或关闭浏览器安全策略强行兼容。
不要把整套 admin 嵌入桌面壳。JS SDK 的版本和构建产物留在专用 Web 页面侧,Python 只持有 `VideoPort(join, leave, mute, device_changed)` 抽象,这样管理端升级 TRTC SDK 不要求桌面二进制同步发版。
### 6.2 安全桥接
- 桌面从后端申请一次性、短有效期的 call ticketWeb 页面再用 ticket 换房间凭证。禁止把 access token、`userSig` 长期放在 URL、日志或 localStorage。
- 使用专用 `QWebEngineProfile`。优先 off-the-record;若必须持久化设备选择,也只能保存无认证数据。通话结束时清 Cookie、HTTP cache、permissions 和页面内容。
- 导航只允许精确的 HTTPS origin/path allowlist;拦截新窗口、任意下载、`file://`、未知 scheme、跨域跳转和证书错误。证书错误 fail closed。
- 相机/麦克风权限只对当前 allowlisted 通话 origin、活跃通话和明确用户操作放行,结束即撤销。屏幕共享另做显式确认。
- 若使用 Qt WebChannelbridge 只暴露少量 typed method/signal,不暴露文件系统、shell、通用 HTTP client、token getter 或任意 Python 调用。每次调用再次校验当前 page origin 和 session/call id。
- release 不启用 remote debugging,不设置 `QTWEBENGINE_DISABLE_SANDBOX=1`,不使用 `--no-sandbox`。应用也不以管理员/root 身份运行。
### 6.3 兼容性与体积现实
`PySide6` 顶层 wheel 会同时拉入 Essentials 和 Addonscore profile 应直接依赖 `PySide6-Essentials`。WebEngine 位于 Addonswheel 和最终制品都会显著增大,不能把它当成“小插件”。最终体积以两端产物为准,不承诺一个固定数字。
QtWebEngine 使用多进程 Chromium,发布物必须保留:
- `QtWebEngineProcess` helper
- QtWebEngineCore/Widgets 库与所需平台插件;
- `qtwebengine_resources*.pak``icudtl.dat`、V8 snapshot
- `qtwebengine_locales`(至少完整验证 `zh-CN``en-US` 后才可裁剪);
- macOS framework/helper 的 bundle 结构和 entitlements。
H.264/AAC/MP3 等专有 codec 是否可用取决于 QtWebEngine 构建与许可,不能因为 `hls.js` 存在就假设一定能播放。首发验收必须覆盖真实 TRTC/WebRTC、摄像头、麦克风、扬声器切换、屏幕共享(若需求存在)、HLS/录播格式和弱网;若要自行构建启用 proprietary codecs,先完成专利/分发许可评审。
## 7. PyInstaller 构建与发布
### 7.1 为什么固定 onedir
虽然 PyInstaller 支持 onefile,但本项目默认 `onedir`
- onefile 每次启动需解压大体积 Chromium,冷启动慢,易触发杀软且临时目录空间不可控;
- WebEngine 是多进程,helper、framework、资源和 macOS 签名/沙箱都依赖正确目录结构;
- onedir 更容易做增量诊断、签名验证和 installer 管理。
用户最终仍拿到一个 `.exe` 安装程序或 `.dmg/.pkg`,无需手动管理 onedir 目录。
### 7.2 spec 设计原则
维护四个薄 spec,公共配置放 `packaging/common.py`。入口始终为 `src/zyt_doctor/__main__.py``pathex=["src"]`;只收集应用 resources 和必要 metadata。WebEngine profile 因 `integrations/video/webengine.py` 中有显式 import,触发 PyInstaller 官方 PySide6 hook;如通过 feature registry 动态加载,再显式加入这些 hidden imports
```python
VIDEO_HIDDEN_IMPORTS = [
"zyt_doctor.integrations.video.webengine",
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebChannel",
]
# core spec 中排除,且 core 构建环境根本不安装 Addons
CORE_EXCLUDES = [
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebEngineQuick",
]
```
不要手工把整个 `site-packages/PySide6` 复制进 datas,也不要用 `collect_all("PySide6")`;这会拉入无关 Qt 模块并可能破坏 hook 期望的目录。优先依赖当前锁定 PyInstaller 的 Qt hooks,仅对 self-check 证实遗漏的自有动态模块写自定义 hook。
macOS `BUNDLE` 至少设置稳定的 bundle id、版本、图标,并在 `Info.plist` 中写清:
```python
info_plist = {
"CFBundleIdentifier": "com.zhenyangtang.doctor",
"NSCameraUsageDescription": "用于医生视频问诊",
"NSMicrophoneUsageDescription": "用于医生视频问诊",
"NSHighResolutionCapable": True,
}
```
仅当有实际功能时再加入其他 TCC 权限说明。不得加入放宽 ATS 的全局例外。Windows spec 使用有版本信息的 manifest、`.ico` 和 GUI subsystem,同时保留内部异常日志;开发 smoke build 可临时打开 console。
### 7.3 构建命令骨架
Windows x64 runner
```powershell
py -3.12 -m venv .venv-build
.venv-build\Scripts\python -m pip install --require-hashes -r requirements\video-win-x64.lock
.venv-build\Scripts\python -m PyInstaller --noconfirm --clean packaging\windows\doctor-video.spec
dist\DoctorDesktop\DoctorDesktop.exe --self-check
```
macOS arm64 runner
```bash
python3.12 -m venv .venv-build
.venv-build/bin/python -m pip install --require-hashes -r requirements/video-macos-arm64.lock
.venv-build/bin/python -m PyInstaller --noconfirm --clean packaging/macos/doctor-video.spec
dist/DoctorDesktop.app/Contents/MacOS/DoctorDesktop --self-check
```
PyInstaller 不能从 Windows 生成 macOS `.app`,反之亦然。每次构建从干净环境执行 `--clean`,记录 Python/PySide6/PyInstaller/OS SDK 版本、锁文件 hash、git commit 和产物 SHA-256,保证可追溯。
### 7.4 WebEngine 自检
应用提供 `--self-check`,不访问患者数据,检查:
- build info、只读 resources、可写 data/log/cache 路径;
- keyring backend 是否安全、SQLite migration 是否可运行;
- TLS CA、production 配置和 host allowlist
- video profile 中通过 `QLibraryInfo` 定位 helper/resources/locales,禁止依赖写死的 `_internal` 路径;
- release 环境没有 `QTWEBENGINE_DISABLE_SANDBOX`/`--no-sandbox`
- GUI smoke 模式实际创建 `QWebEngineView`,加载本地无网络测试页,然后退出;真实视频另由端到端测试覆盖。
任何 helper、`.pak`、ICU、snapshot、platform plugin 或 locale 缺失都应让发布流水线失败,不在运行时静默降级。
### 7.5 Windows 发布
1. 使用固定、受控的 Windows x64 runner 构建;不要在装有多套 Qt/Anaconda 的个人机上发正式包。
2. QtWebEngine 运行依赖合适的 MSVC runtime。由安装器包含/检查 Microsoft Visual C++ RedistributableQt 官方要求的版本下限需按锁定 Qt 再核对),并在干净 VM 验证。
3. 用组织的 Authenticode 证书和 RFC 3161 时间戳签名主程序及最终 MSI/EXE 安装器;验证 `signtool verify /pa /all`
4. 安装到 Program Files,用户数据仍进 LocalAppData;普通用户可运行/升级。可用 WiX Toolset/MSIX 或 Inno Setup,选择后固定 UpgradeCode/AppUserModelID 和回滚策略。
5. 在 Windows 10/11 干净 VM 上验证安装、升级、卸载后保留/清除用户数据的明确策略、SmartScreen、企业代理、中文路径和非管理员账户。
### 7.6 macOS 发布
1. 在目标架构的 macOS runner 构建。PyInstaller 修改 Mach-O 后必须重新签名;使用 Developer ID Application 身份,不用 ad-hoc 签名发布。
2. QtWebEngine helper 是嵌套 app/process。必须保留 framework bundle 结构,并确认 helper 使用 Qt 自带的 `QtWebEngineProcess.entitlements` 所需权限签名;主 app 使用项目的 camera/microphone 权限说明和最小 entitlements。签名顺序由内向外,避免用 `codesign --deep` 掩盖错误。
3. 执行 `codesign --verify --deep --strict --verbose=2 DoctorDesktop.app``spctl --assess --type execute`;随后用 `xcrun notarytool submit ... --wait` 公证,staple ticket,再在离线干净 Mac 验证 Gatekeeper。
4. 用 DMG/PKG 或能保留 symlink 的方式分发。PyInstaller 6+ 的 POSIX bundle 广泛使用 symlink,普通 zip 若不保留 symlink 可能膨胀或破坏运行。
5. 在 Intel(若支持)与 Apple Silicon 真机上分别验证 keychain 升级连续性、摄像头/麦克风 TCC、休眠唤醒、Retina、多显示器和 WebEngine helper 签名。
### 7.7 常见打包坑清单
- **Qt binding 混装**:同环境存在 PyQt/PySide2 或系统 Qthook 收到冲突库。解决:干净 venv、只安装一种 binding、锁版本。
- **误用 onefile**:启动慢、helper/沙箱/签名问题更难复现。解决:正式版固定 onedir。
- **动态 import 未分析**:业务模块或 WebEngine 在 registry 中字符串加载。解决:显式 import 或最小 hiddenimports,并用 frozen smoke test 覆盖。
- **资源路径错误**:开发机相对路径可用,安装后 CWD 变化。解决:`importlib.resources`/qrc;用户数据用 QStandardPaths。
- **过度裁剪 Qt**:删除 `.pak`、ICU、snapshot、locale、platform plugin 后只在某些机器崩。解决:先保留 hook 输出,按真实清单与测试有证据地裁剪。
- **macOS 签名次序/entitlements 错**QtWebEngineProcess 启动即退出或 TCC 不弹窗。解决:嵌套 helper 真机测试、由内到外签、notarize/staple。
- **归档破坏 symlink**`.app` 体积暴涨或 framework 无法加载。解决:DMG/ditto 或明确保留 symlink 的归档工具。
- **GPU/远程桌面差异**WebEngine 黑屏。不要默认全局 `--disable-gpu`;收集诊断后提供经验证的软件渲染或外部浏览器 fallback。
- **codec 误判**:开发机能播 H.264,发布 wheel 不能播。解决:把真实媒体矩阵列入 artifact 验收和许可评审。
- **杀软与信誉**:大量 DLL/helper 或未签名 nightly 被拦截。解决:正式证书、时间戳、稳定 installer identity、干净 VM/主流安全软件测试。
- **升级破坏 keychain/数据**bundle id、签名 identity 或 schema 不稳定。解决:这些值从首版固定,migration 支持备份与回滚。
## 8. 安全与隐私基线
- TLS 校验永远开启。企业私有 CA 应通过受管安装进入系统 trust store;不得用 `verify=False`。如需兼容企业代理,可用 `truststore` 接入 OS 证书库并做专项测试。
- 本地配置不能提供任意 production host 重定向;敏感 token 永远不进入 URL query、clipboard、日志、analytics 或 crash report。
- HTML 病历优先用受限 `QTextBrowser`/原生富文本展示并在服务端净化;不要因为“要展示 HTML”就引入 WebEngine。任何外链由用户确认后交给系统浏览器。
- 限制剪贴板和通知中的患者信息;自动锁定后遮蔽窗口内容。截图阻止在跨平台上不可靠,不能作为合规控制。
- 所有高风险业务写操作由服务端保留不可抵赖审计;客户端日志只记录事件名、耗时、状态、request id 和脱敏技术上下文。
- 发布前完成依赖 SBOM、许可证清单和漏洞扫描。PySide6 采用 LGPLv3/GPLv3 或商业许可,QtWebEngine/Chromium/codec 还包含额外 notices;由法务确认采用的 Qt 许可与分发义务,安装包附第三方 notices。
- 自动升级若后续实现,更新 manifest 必须签名,制品必须校验 SHA-256 与平台签名,并支持回滚;首版宁可用已签名安装器提示升级,也不要执行未签名下载内容。
## 9. 测试与发布门槛
### 9.1 自动化测试分层
- **unit**:权限 guard、会话状态机、单飞 refresh、重试白名单、错误映射、缓存 TTL、加解密、outbox 状态和冲突决策。用 fake clock/random/secret store,保证确定性。
- **API contract**:以 OpenAPI schema 或后端 mock 验证字段、错误码、分页、时间和幂等语义;`respx` 模拟超时、断流、401 并发、429/Retry-After、5xx 和结果未知。
- **integration**:临时 SQLite + fake keyring + staging API,覆盖 migration、损坏缓存、磁盘满、退出清理、租户切换和代理/私有 CA。
- **UI**`pytest-qt` 验证路由权限、loading/empty/error/offline、键盘导航、取消和 late response;不要用脆弱的像素级截图替代行为断言。
- **frozen artifact**Windows/macOS 各自安装后运行 `--self-check`,启动主窗口、登录 mock/staging、访问资源、写用户目录、升级 migration、卸载。
- **视频真机**:摄像头/麦克风授权与拒绝、无设备、设备热插拔、回声设备、弱网/断网重连、休眠唤醒、屏幕共享、录播 codec、结束后权限与 Cookie 清理。
重点故障用例包括:十个请求同时 401、提交后响应丢失、服务器版本冲突、系统时钟偏差、刷新时退出、缓存被截断、keychain 被锁、两实例竞争、磁盘只读、API 维护、WebEngine helper 被杀、证书过期。测试数据必须是合成数据。
### 9.2 CI 矩阵与质量门槛
PR 阶段可并行运行 Windows x64 和 macOS arm64 的 lint/type/unit/UI headless 测试。release tag 阶段在原生 runner 生成制品并执行:
1. `ruff``mypy`、unit/contract/integration 测试全绿,覆盖率阈值重点约束 core 状态机而非 UI 行数。
2. 依赖 lock、SBOM、license、`pip-audit` 无未批准的高危项。
3. 两端 frozen self-check 与安装/升级 smoke 通过。
4. 签名、公证、hash、版本资源和 update channel 验证通过。
5. video profile 在目标硬件的人工/自动验收清单签字;core profile 证明不会意外收集 QtWebEngine。
6. staging 完成登录、权限变更、患者查询、一个低风险写操作、一个高风险确认、登出清理和离线恢复闭环。
## 10. 实施顺序与验收里程碑
### M0:契约和风险封板(约 3–5 天)
- 获取 OpenAPI/认证/权限/RTC/对象存储契约;形成 endpoint 与权限矩阵。
- 在 Windows/macOS 原型中用 QtWebEngine 打开专用测试页,验证 TRTC/WebRTC、设备权限和目标 codec。
- 决定首发是 core、video 还是“core + 外部浏览器”。
**退出条件**API base 和 auth 不再是假设;视频路线有真实 PoC,而非只证明网页能打开。
### M1:可发布骨架
- 完成 bootstrap、配置校验、日志/路径、API client、SessionManager、PermissionGuard、shell 和统一状态组件。
- mock server 下完成登录、`/me`、权限菜单、401 single-flight refresh、登出清理。
- 两端 onedir unsigned nightly 可安装并通过 self-check。
### M2:业务纵切
- 先选一个完整纵切(例如预约 -> 患者概要 -> 诊间记录草稿),按 module 结构贯穿 UI、service、API、权限和测试。
- 再并行扩展患者、病历、处方、随访、聊天;高风险操作必须有后端幂等/审计。
### M3:离线与视频
- 实现加密 cache/outbox、冲突页、断网/恢复和数据清理。
- video adapter、受限 profile、一次性 ticket、权限撤销和外部浏览器 fallback 完成。
### M4:生产发布
- 锁依赖和 runner image,完成 Windows Authenticode、macOS Developer ID/notarization、SBOM/许可证。
- 干净 VM/真机、企业代理、非管理员、升级/回滚、视频设备矩阵全部通过。
## 11. 最终架构决策摘要
1. 业务 UI 用原生 Qt WidgetsQtWebEngine 是隔离的视频实现细节,不是应用架构。
2. 后端契约、服务端授权和服务端审计是权威;桌面端只做强类型 adapter、体验 guard 和安全状态管理。
3. access token 仅在内存,长期凭证进 OS keychain;敏感离线数据加密且按用户/租户隔离。
4. 高风险医疗写操作离线时只能保存草稿,恢复后由医生确认;普通 outbox 依赖幂等键和乐观锁。
5. 发布固定 PyInstaller onedir、平台原生构建、签名和 artifact 级测试;不交叉编译,不依赖开发机“能跑”。
6. production URL、SDK secret、对象存储密钥和 RTC 签名密钥都不能硬编码进客户端。
## 参考资料
- [Qt for Python package details](https://doc.qt.io/qtforpython-6.10/package_details.html)Essentials/Addons 拆分和 wheel 内容。
- [Qt for Python 与 PyInstaller](https://doc.qt.io/qtforpython-6.10/deployment/deployment-pyinstaller.html):官方 PyInstaller 基础部署说明。
- [Qt WebEngine 部署](https://doc.qt.io/qt-6/qtwebengine-deploying.html)helper、resources、locales、macOS entitlements 等必需项。
- [Qt WebEngine features](https://doc.qt.io/qt-6/qtwebengine-features.html)WebRTC/媒体能力及专有 codec 许可提醒。
- [PyInstaller macOS multi-arch 与签名](https://pyinstaller.org/en/stable/feature-notes.html):架构 slice 和 codesign 行为。
- [PyInstaller symlink/common pitfalls](https://pyinstaller.org/en/stable/common-issues-and-pitfalls.html)PyInstaller 6+ POSIX bundle 的 symlink 分发要求。
@@ -0,0 +1,565 @@
# 诊单详情 / 编辑 / 预约视觉规格(后台源码基准)
## 1. 范围与判读原则
本规格只以 `D:/web/zyt/admin` 当前源码为事实源,覆盖:
- 三个主视图:`src/views/tcm/diagnosis/edit.vue``readonly.vue``appointment.vue`
- 三个主视图直接渲染的诊单组件:患者摘要、病例、日常记录、医生备注、处方/病例记录、业务订单、视频、聊天、医助指派历史、挂号历史、待办。
- 备注视图直接引用的 `src/views/patient/reception/components/NoteTimeline.vue`
- 直接生效的全局样式入口、Element Plus 变量和 Tailwind 尺寸映射。
颜色、尺寸若由当前文件明确声明,标为“显式”;若控件仅沿用 Element Plus,则标为“继承”。项目使用 Element Plus `^2.9.4` 与 Tailwind CSS `^3.4.17``package.json:27,64`)。全局样式入口为 `main.ts:1-3`,依次加载 `element.scss``dark.css``var.css``tailwind.css``public.scss``src/styles/index.scss:1-5`)。因此不能把浏览器/Element 默认值误写成某个业务组件的局部规格。
> 关键结论:系统实际上有两种“只读详情”。`readonly.vue` 是独立、卡片化、纵向详情页;`edit.vue` 在 `viewOnly` 模式下则仍是右侧抽屉和 Tab 结构,只禁用编辑控件。两者不能用同一个线框替代。
---
## 2. 全局视觉基础
### 2.1 字体与字号
字体栈由 Tailwind 配置明确为 `PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei, sans-serif``tailwind.config.js:79-80`),并赋给 `--el-font-family``src/styles/var.css:1-3`)。基础字号如下(`src/styles/var.css:11-18`):
| 语义 | 实际字号 |
|---|---:|
| extra large | 18 px |
| large | 16 px |
| medium | 15 px |
| base | 14 px |
| small | 13 px |
| extra small | 12 px |
业务层进一步压缩:编辑抽屉根节点和表单文字为 12 px(`edit.vue:1476-1504,1532-1556`);只读病例键值为 12.5 px`components/PatientCaseCard.vue:307-341`);聊天气泡为 14 px`components/ImChatRecordPanel.vue:260-286`)。
### 2.2 全局颜色与表面
以下均为项目显式变量(`src/styles/var.css:20-40`):
| 用途 | 色值 |
|---|---|
| 页面底 | `#f6f6f6` |
| 普通/浮层底 | `#ffffff` |
| 主文字 | `#333333` |
| 常规文字 | `#666666` |
| 次要文字 | `#999999` |
| placeholder | `#a8abb2` |
| disabled | `#c0c4cc` |
| border / light / lighter | `#dcdfe6` / `#e4e7ed` / `#ebeef5` |
| extra-light border | `#f2f2f2` |
| fill / light / lighter | `#f0f2f5` / `#f8f8f8` / `#fafafa` |
| extra-light fill | `#fafcff` |
Element 语义主色未在这些诊单文件中改写,局部引用 `var(--el-color-primary)` 或显式 `#409eff`。继承的 Element Plus 2.9.4 常规控件高度为 32 pxsmall 24 px、large 40 px);但编辑抽屉底部按钮被局部覆盖为最小 40 px,移动端最小 44 px(`edit.vue:1732-1740,1905-1913`)。
### 2.3 间距基线和全局行为
Tailwind 间距映射是 4 px 基线:`1=4``2=8``2.5=10``3=12``3.5=14``4=16``5=20``6=24``tailwind.config.js:103-125`)。业务文件里的 8/10/12/14/16/18/20/24 px 与该节奏一致。
直接影响这些页面的全局规则:
- Dialog 居中,最大宽 `calc(100vw - 30px)`,圆角 5 px`src/styles/element.scss:11-27`)。
- 全局 Drawer 主内边距变量为 16 pxHeader 内边距 `13px 16px`、下边框 1 px lighter;预约 Drawer 没有重写这些值,编辑 Drawer 则只额外重写背景与边框(`src/styles/element.scss:46-56`)。
- 全局表格字号 14 px、表头主文字色、表头字重 400、表头底 `#f8f8f8``src/styles/element.scss:58-68`; `src/styles/var.css:10`)。因此编辑根节点的 12 px 不应错误地传成所有表格单元字号。
- Input、Select、Textarea 聚焦时出现 2 px primary-light 外环,radio/checkbox active 也有 2 px 外环;校验错误时切换 danger-light 外环(`src/styles/element.scss:140-175`)。
- Tabs 底线全局压至 1 px`src/styles/element.scss:131-133`)。
- 消息与通知层级为 9999`src/styles/element.scss:1-9`)。
- 小于等于 768 px 时分页隐藏页码跳转与 page-size 选择器(`src/styles/element.scss:177-183`)。
- Tailwind 会导入 base/components/utilities`src/styles/tailwind.css:1-3`),但 Element 按钮背景、focus、hover 被重新还原为 Element 变量,避免被 Tailwind reset 覆盖(`src/styles/element.scss:186-200`)。
- `body` 为 14 px 主文字、`overflow:hidden`、最小宽 375 px;页面/Drawer 自己承担滚动(`src/styles/public.scss:1-3`)。全局 `.form-tips` 为 12 px secondary、行高 24 px、上间距 4 px`src/styles/public.scss:4-6`)。
暗色主题另有一套直接全局变量:页面/表面为 `#0a0a0a / #1d2124 / #1d1e1f`,主/常规/次要文字为 `#e5eaf3 / #cfd3dc / #a3a6ad`,边框从 `#636466``#2b2b2c`,遮罩为 `rgba(0,0,0,.8)``src/styles/dark.css:1-31`)。不过三个诊单主视图及卡片大量显式写入白底、浅蓝渐变和 slate 色,不能完整随暗色变量切换;本规格后续均按源码默认浅色态记录,暗色不能视为已完整适配。
---
## 3. 独立只读详情页 `readonly.vue`
### 3.1 页面骨架
源码结构是单列滚动页,不使用 Tabs(`readonly.vue:1-149`):
```text
┌─────────────────────────────────────────────────────────────────────┐
│ ← 返回 诊单只读详情 张三 男 · 42岁 [未服务 3天] │
├─────────────────────────────────────────────────────────────────────┤
│ [患者信息摘要 PatientInfoCard] │
├─────────────────────────────────────────────────────────────────────┤
│ [患者病例 PatientCaseCard] │
├─────────────────────────────────────────────────────────────────────┤
│ [日常记录 DailyMatrix] 按权限可见 │
├─────────────────────────────────────────────────────────────────────┤
│ [医生备注/舌象/报告 NoteTimeline] │
├─────────────────────────────────────────────────────────────────────┤
│ [业务订单 PatientOrderList] 按权限可见 │
├─────────────────────────────────────────────────────────────────────┤
│ [视频录制回放 CallRecordPanel] 按权限可见 │
├─────────────────────────────────────────────────────────────────────┤
│ [聊天 ImChatRecordPanel] 按权限可见 │
├─────────────────────────────────────────────────────────────────────┤
│ [指派医助记录 AssignLogPanel] 按权限可见 │
├─────────────────────────────────────────────────────────────────────┤
│ [挂号记录 AppointmentRecordPanel] 按权限可见 │
└─────────────────────────────────────────────────────────────────────┘
```
渲染顺序严格来自 `readonly.vue:39-146`;异常时在 hero 下方显示错误空态(`readonly.vue:31-36`)。页面根容器垂直间距 16 px、内边距 16 px(`readonly.vue:269-273`)。
### 3.2 顶部 Hero
Hero 是可换行的左右布局:`display:flex; justify-content:space-between; flex-wrap:wrap; gap:12px`,内边距 `12px 16px`,圆角 12 px,边框 `#dde7ff`,背景为 `#f5f8ff → #eef3ff` 的浅蓝渐变(`readonly.vue:275-285`)。
- 页面标题:16 px / 700`readonly.vue:286-303`)。
- 患者姓名:18 px / 700;性别年龄 13 px`readonly.vue:305-320`)。
- 未服务状态胶囊:12 px / 600,内边距 `4px 10px`999 px 全圆角(`readonly.vue:323-330`)。
- 状态色:正常绿 `#16a34a` / 边框 `#bbf7d0`;提醒橙 `#ea580c` / `#fed7aa`;严重红 `#dc2626` / `#fecaca``readonly.vue:332-355`)。从未记录时使用 Element placeholder 灰。
### 3.3 卡片规格
通用详情卡为白底、1 px `#e6ebf2` 边框、14 px 圆角、18 px 内边距、14 px 内部纵向间距和轻阴影(`readonly.vue:364-373`)。卡片标题 15 px / 700,标题前为 `3 × 16 px` 蓝色标记(`readonly.vue:375-396`)。DailyMatrix 外层额外 16 px 内边距以对齐矩阵(`readonly.vue:398-405`)。错误空态卡保持 14 px 圆角并有 40 px 纵向内边距(`readonly.vue:357-362`)。
患者摘要内部是浅蓝 Hero 卡:圆角 12 px,内边距 `16px 18px`,行距 6 px;患者名 22 px / 700,其余信息 13 px、行高 1.7(`components/PatientInfoCard.vue:78-123`)。字段顺序是:
1. 姓名。
2. 脱敏手机 · 性别 · 年龄。
3. 身高 / 体重 · 地区。
4. 预约日期时间 · 时段。
5. 医生 / 医助。
6. 预约状态 · 是否已开方。
7. 备注(有值才显示)。
顺序由 `components/PatientInfoCard.vue:6-25` 确定。
### 3.4 病例字段顺序与网格
病例卡以分组标题 + 网格键值展示;组间 `gap:10px`,除首组外顶部 12 px 内边距并有 dashed 分隔线(`components/PatientCaseCard.vue:258-269`)。分组标题 13 px / 600、蓝色,前有 4 px 圆点(`components/PatientCaseCard.vue:271-287`)。网格横向间距 16 px、纵向 8 px,并提供 1/2/3/4 等分列(`components/PatientCaseCard.vue:289-305`)。键值字号 12.5 px、行高 1.55label 最小宽 58 pxlabel 为 `#6b7280`value 为 `#1f2937` / 500,空值为 `#c0c4cc``components/PatientCaseCard.vue:307-341`)。
只读病例的真实字段顺序(`components/PatientCaseCard.vue:8-124`):
| 分组 | 列数 | 顺序 |
|---|---:|---|
| 基本信息 | 4 | 诊单ID、姓名、身份证、手机、性别、年龄、婚姻、地区 |
| 生命体征 | 4 | 身高、体重、高压、低压、诊断类型、空腹血糖、在用药物(多行) |
| 主诉 | 4 | 当地诊断日期、糖尿病病史、当地医院、当地医院诊断结果 |
| 现病史 | 3 | 口腔、饮水、体重变化、脂肪肝、饮食(跨2列)、肢体、睡眠、眼、头、出汗、皮肤、小便、大便、腰肾、其他补充(跨3列、多行) |
| 既往史 | 1 | 既往史(整行) |
| 其他病史 | 混合 | 外伤、手术、过敏、家族、妊娠 |
| 补充与意见 | 1 | 病史补充(有值才显示)、处方意见 |
超标指标采用 `#dc2626`、700 字重并带上箭头(`components/PatientCaseCard.vue:343-358`)。处方意见为 13 px、`12px 14px` 内边距、8 px 圆角、左侧 3 px 蓝条(`components/PatientCaseCard.vue:360-368`)。
### 3.5 响应式事实
`readonly.vue` 自身没有 media queryHero 仅通过 `flex-wrap` 避免左右信息互相挤压。`PatientCaseCard.vue` 也没有窄屏降列规则,所以 4 列病例网格不会自动变成 1 列。实现 PySide 时不能把“可换行 Hero”误判为整页已经完整响应式。
---
## 4. 编辑 / 抽屉只读 `edit.vue`
### 4.1 框架与模式
右侧 Drawer 宽度固定为视口的 60%,方向 RTL;只读时 z-index 4000,编辑/新增时 1500`edit.vue:2-10`)。抽屉有三种标识:只读 `info`、新建 `success`、编辑 `warning`,标题下显示“患者名 · 诊单ID”(`edit.vue:11-54,859-863`)。
```text
桌面(右侧 60% Drawer
┌───────────────────────────────────────────────────────────────┐
│ 编辑诊单 [编辑] 患者名 · #123 │ Header
├───────────────────────────────────────────────────────────────┤
│ 病历 │ 医生备注 │ 日常记录 │ 处方 │ 业务订单 │ 视频 │ ... → │ Tabs
├───────────────────────────────────────────────────────────────┤
│ │
│ [锁定提示 / 隐私提示,按条件] │
│ 诊单ID [_____________________________] │
│ 姓名 [____________] 身份证 [____________] │
│ │
│ ──────────────── 生命体征 ──────────────── │
│ 婚姻 [____] 身高 [____] 体重 [____] │ Scroll body
│ ... │
│ ──────────────── 现病史 ───────────────── │
│ [可换行 radio / checkbox 组] │
│ │
├───────────────────────────────────────────────────────────────┤
│ [取消/关闭] [保存] │ Footer slot
└───────────────────────────────────────────────────────────────┘
```
`viewOnly` 模式没有切换到独立详情卡,而是给病历 `fieldset` 禁用输入,并隐藏保存;锁定患者信息时显示 warning alert`edit.vue:57-84,766-781`)。
### 4.2 Header、Tabs、Footer
- Header 背景 `#fff → #f8fafc`,底边 1 px `#e2e8f0``edit.vue:1776-1785`)。标题 18 px / 600 / `#0f172a`,行高 1.3;副标题 13 px`edit.vue:1670-1717`)。
- Body 背景 `#f8fafc``edit.vue:1787-1790`)。
- Tabs 横向滚动,滚动条 4 px;nav 使用 `max-content` 且最小宽 100%,tab 水平内边距 14 px,常态 `#64748b` / 500,激活态为 Element primary / 600,激活条 3 px`edit.vue:1798-1858`)。
- Footer 最终生效样式为白底、上边框 1 px `#e2e8f0`,内容右对齐、gap 12 px、内边距 `14px 20px 18px``edit.vue:1719-1730,1860-1864`)。按钮最小高 40 px、水平内边距 20 px、600 字重、圆角 10 px`edit.vue:1732-1740`)。
这里没有 CSS `position: sticky`。底部操作之所以视觉上固定,是因为它使用 Element Drawer 的独立 footer slot,位于可滚动 body 之外(模板 `edit.vue:766-781`;结构样式 `edit.vue:1792-1796`)。PySide 应以固定底栏实现,而不是在滚动内容末尾放按钮。
### 4.3 Tab 顺序、权限与内容
Tab 的准确顺序和门槛(`edit.vue:55-759`):
| 序号 | Tab | 条件 | 内容组件 |
|---:|---|---|---|
| 1 | 病历 | 始终 | 内联表单 |
| 2 | 医生备注 | 始终 | `NoteTimeline` |
| — | 跟踪备注 | **已注释,不渲染** | 原拟 `TrackingNoteTimeline``edit.vue:644-654` |
| 3 | 日常记录 | `tcm.diagnosis/dailyRecord` | `DailyMatrix` |
| 4 | 处方 | `tcm.diagnosis/chufang` | `CaseRecordList` |
| 5 | 业务订单 | `tcm.diagnosis/patientOrders` | `PatientOrderList` |
| 6 | 视频录制回放 | `tcm.diagnosis/huifang` | `CallRecordPanel` |
| 7 | 聊天 | `tcm.diagnosis/chat` | `ImChatRecordPanel` |
| 8 | 指派医助记录 | `tcm.diagnosis/assign``tcm.diagnosis/detail` | `AssignLogPanel` |
| 9 | 挂号记录 | `doctor.appointment/lists` | `AppointmentRecordPanel` |
权限变化后若当前 Tab 失权,代码会主动回到 `basic``edit.vue:883-913`)。除病历外的大部分 Tab 为 lazy 内容,首次选中才加载。
### 4.4 病历表单布局、标签和控件
桌面表单 label width 为 160 px`edit.vue:61-63`)。根字号、label、输入/textarea/radio/checkbox/button 均显式为 12 pxlabel 为 500 / `#606266``edit.vue:1476-1504,1532-1556`)。表单项底部间距 18 px`edit.vue:1528-1530`)。输入、选择、日期、数字框圆角 10 px(`edit.vue:1645-1667`);普通输入高度沿用 Element 32 px。
分组标题的上/下外边距为 28/22 px;中间文字 14 px / 600 / `#0f172a``7px 16px` 内边距、999 px 胶囊圆角,两侧 1 px `#e2e8f0` 横线(`edit.vue:1506-1525`)。单选紧凑组换行、gap 12 px;复选组 gap `12px 16px`;普通 radio/checkbox 右、下间距分别 16/8 px`edit.vue:1558-1604`)。帮助文本 12 px、`#909399`、行高 1.5、顶部 4 px`edit.vue:1606-1611`)。
### 4.5 编辑表单的真实字段顺序
以下只列实际渲染字段,不把 `formData` 中未渲染的属性当作 UI(模板 `edit.vue:57-632`):
| 区段 | 栅格 | 字段顺序与控件 |
|---|---|---|
| 顶部 | 24 | 诊单ID(只读) |
| 基本身份 | 12 + 12 | 姓名;身份证 |
| 联系信息 | 12 + 12 | 手机;性别 |
| 年龄 | 12 | 年龄 |
| **生命体征** | 8 + 8 + 8 | 婚姻;身高;体重 |
| | 8 + 8 + 8 | 地区;高压;低压 |
| | 8 | 空腹血糖 |
| | 12 | 诊断类型 |
| | 12 + 12 | 状态;渠道 |
| | 12 | 统计端就诊卡 |
| | 12 | 在用药物,textarea3 行,maxlength 2000 |
| **主诉** | 12 + 12 | 当地诊断日期;糖尿病病史 |
| | 24 | 当地医院诊断结果(check buttons |
| | 24 | 当地医院名称 |
| **现病史** | 24 | 口腔感觉(check buttons |
| | 24 | 每日饮水量(radio buttons |
| | 24 | 近月体重变化(radio buttons |
| | 24 | 脂肪肝(radio buttons |
| | 24 | 饮食(check buttons |
| | 24 × 9 | 肢体、睡眠、眼睛、头部、出汗、皮肤、小便、大便、腰肾 |
| | 24 | 其他补充,textarea3 行 |
| **既往史** | 24 | 既往史(check buttons |
| **其他病史** | 8 + 8 + 8 | 外伤;手术;过敏 |
| | 12 + 12 | 家族;妊娠 |
| **诊断信息** | 24 | 病史补充,textarea2 行 |
具体模板行:顶部/身份 `edit.vue:86-194`;生命体征 `edit.vue:199-368`;主诉 `edit.vue:369-412`;现病史 `edit.vue:414-549`;既往史 `edit.vue:552-564`;其他病史 `edit.vue:567-613`;诊断信息 `edit.vue:616-629`
当前模板没有显示 `syndrome_type``diabetes_type`、舌象、脉象、治则、医嘱等字段,即便脚本模型中存在,也不应擅自加入视觉复刻。
### 4.6 移动端
小于等于 768 px 时(`edit.vue:1876-2004`):
- Drawer 变为 100vwheader `12px 14px`body `10px 12px`
- Footer 改为纵向、按钮全宽,gap 10 px,内边距 `12px 14px + safe-area`,按钮最小 44 px。
- 标题 17 pxTab 高/行高 44 px、字号 13 px、水平内边距 12 px。
- label 改到控件上方:100% 宽、左对齐、下间距 6 px;表单项底部 14 px。
- 所有 `el-col` 强制 100%,数字框 100%radio gap `8px 16px`checkbox `8px 10px`
- 分组标题外边距 18/14 px,文字 13 px、水平内边距 10 px。
```text
移动端(<=768
┌──────────────────────┐
│ 标题 [状态] × │
│ 患者 · 诊单ID │
├──────────────────────┤
│ ← 可横向滚动 Tabs → │
├──────────────────────┤
│ 姓名 │
│ [__________________] │
│ 身份证 │
│ [__________________] │
│ ... 全部单列 ... │
├──────────────────────┤
│ [取消/关闭 全宽] │
│ [保存 全宽,44px+] │
└──────────────────────┘
```
---
## 5. 预约抽屉 `appointment.vue`
### 5.1 骨架与字段顺序
预约同样是 RTL 右侧 Drawer,宽 60%z-index 2000,且点击遮罩不能关闭(`appointment.vue:2-9`)。表单 label width 为 100 px`appointment.vue:19`)。
```text
┌───────────────────────────────────────────────────────────────┐
│ 预约面诊 × │
├───────────────────────────────────────────────────────────────┤
│ [今日重复预约警告 / 其他日期提示,条件显示] │
│ 上次面诊 2026-08-01 10:30 │
│ 预约方式 (●) 按时间预约 │
│ 预约类型 (●) 视频面诊 │
│ 预约患者 (●) 当前患者 │
│ 渠道来源 [请选择 ▼] │
│ 自媒体详情 [请选择 ▼] ← 仅自媒体渠道 │
│ 预约医生 ( ) 医生A ( ) 医生B ... │
│ 预约时间 [08月10日 周一] [08月11日 周二] ... │
│ ┌───────────────────────────────────────────┐ │
│ │ 可预约时段 [刷新] │ │
│ │ [09:00 可约] [09:30 已满] [10:00 可约] ...│ │
│ └───────────────────────────────────────────┘ │
│ 备注 [_________________________________________] │
├───────────────────────────────────────────────────────────────┤
│ [取消] [确认预约] │
└───────────────────────────────────────────────────────────────┘
```
字段顺序严格为(`appointment.vue:21-181`):
1. 上次面诊(文本)。
2. 预约方式(当前只呈现按时间预约)。
3. 预约类型(视频)。
4. 预约患者(当前患者)。
5. 渠道来源(必填 select)。
6. 自媒体详情(渠道命中自媒体白名单时必填)。
7. 预约医生(radio 列表)。
8. 预约时间:先日期按钮,后可预约时段矩阵。
9. 备注(textarea2 行)。
10. Footer:取消、确认预约。
### 5.2 尺寸、颜色与交互状态
- 渠道控件宽 100%、最大宽 360 px(`appointment.vue:741-750`)。
- 医生/单选组可换行,gap 12 px(`appointment.vue:752-765,911-921`)。
- 日期按钮 flex-wrap、gap 10 px、最小宽 130 px、高 40 px、字号 14 px、圆角 8 pxhover 上移 2 px并加阴影(`appointment.vue:771-789`)。
- 时段容器底 `#f8f9fa`、圆角 8 px、内边距 16 px;标题 15 px / 600 / `#303133``appointment.vue:791-808`)。
- 时段网格 `auto-fill minmax(110px,1fr)`、gap 10 px、最大高 450 px并滚动,滚动条宽 6 px(`appointment.vue:810-830`)。
- 时段卡白底,2 px `#e4e7ed` 边框、8 px 圆角、最小高 70 px、水平内边距 8 px;时间 15 px / 600 / `#303133`,状态 12 px / `#909399``appointment.vue:831-862`)。
- 可约 hover`#409eff` 边框、`#ecf5ff` 底、上移并加阴影(`appointment.vue:864-873`)。
- 不可约:`#f5f7fa` 底、opacity 0.6、文字 `#c0c4cc``appointment.vue:875-894`)。
- 选中:`#409eff → #66b1ff` 渐变、白字和阴影(`appointment.vue:896-909`)。
- Footer 右对齐、gap 12 px、上下内边距 12 px`appointment.vue:923-928`)。
状态逻辑是视觉的一部分:未选医生时显示“请先选择医生”,无排班显示空态;日期只来自当前及未来有效排班(`appointment.vue:299-326`);当天过去时段禁用(`appointment.vue:328-357`);当天已有预约显示 warning,其他日期重复信息为 info(`appointment.vue:359-375`)。提交按钮只有医生、日期、时间、渠道以及条件性渠道详情齐全,且不存在当天冲突时才可用(`appointment.vue:377-386`)。
### 5.3 响应式事实
`appointment.vue` 没有移动端 media query。内部医生、日期和 slot 网格会换行,但 Drawer 在窄屏仍保持 60% 视口宽。这是后台源码的真实缺口;PySide 若要求窗口缩小时可用,应保留桌面 60% 的视觉比例,同时设置合理最小内容宽并在不足时切到全宽/单列,而不声称这是 Vue 原实现已有行为。
---
## 6. 备注、日常记录、处方/订单与历史关联区
### 6.1 医生备注 / 舌象 / 报告
编辑 Drawer 和独立只读页均使用 `NoteTimeline``edit.vue:633-643`; `readonly.vue:56-72`)。编辑态顶部按权限显示“新增备注、选择舌象、选择报告”;只读态隐藏这些操作(`src/views/patient/reception/components/NoteTimeline.vue:1-123`)。
视觉规格(`NoteTimeline.vue:272-436`):
- 顶部动作行 gap 12 px、底部分隔线。
- 时间轴左内边距 22 px,轴线 2 px `#ebeef5`;节点 10 px、`#409eff`,外圈 `#ecf5ff`
- 日期 13 px / 600 / `#303133`,等宽数字;正文 12.5 px / `#606266` / 1.6 行高。
- 舌象缩略图 `64 × 64 px`、圆角 6 px、带边框;删除为红色动作。
- 报告文件 chip 为 `6px 10px`、圆角 6 px、12 px primary 字,最大宽 180 px。
- 上传格 `90 × 90 px`、1 px dashed `#dcdfe6`、圆角 6 px、12 pxhover 转 primary。
- 新增备注 Dialog 宽 480 pxtextarea 4 行、maxlength 500。
`TrackingNoteTimeline` 当前并未进入可见 Tab`edit.vue:644-654` 将整段注释。不能在复刻中把它当作现存“跟踪备注”页签。
### 6.2 日常记录、趋势与待办
DailyMatrix 顶部提供最近 7 天 / 30 天 / 自定义日期和日期范围;右侧有本人记录图例、编辑动作与血糖血压/饮食/运动/备注/刷新按钮,readonly 仅保留刷新(`components/DailyMatrix.vue:1-129`)。矩阵的指标顺序为:空腹、餐后2小时、其他血糖、血压、西药、胰岛素、早餐、午餐、晚餐、运动、跟踪备注(`components/DailyMatrix.vue:410-485`)。固定指标列 120 px,日期列最小 100 px。
- 根内边距 16 pxtoolbar 换行、gap 12 px、下间距 12 px`DailyMatrix.vue:1090-1105`)。
- 趋势卡上间距 16 px、内边距 `16px 18px`、圆角 10 px,图表高 280 px`DailyMatrix.vue:1124-1158`)。
- 超标红 `#dc2626` / 700;本人记录用紫色浅底,徽章 `#6d28d9 / #ede9fe / #ddd6fe``DailyMatrix.vue:1182-1225`)。
- <=768 px 时根内边距 12 px,图表内边距 12 px、高 240 px`DailyMatrix.vue:1240-1260`)。
待办区域位于 DailyMatrix 下方,标题 14 px / 600。表格字段依次为提醒时间、内容、状态、创建人、推送时间、错误信息、操作;toolbar 可筛全部/待推送/已推送/失败/已取消(`components/DiagnosisTodoList.vue:1-112`)。新增 Dialog 宽 520 px、label 100 px,字段顺序为提醒时间、内容(4 行/500 字)、提醒人;toolbar 换行、gap 8 px、下间距 12 px,分页右对齐(`DiagnosisTodoList.vue:291-319`)。
### 6.3 处方 / 病例历史
`CaseRecordList` 顶部编辑态显示小号 primary“开方”,只读时隐藏。表格字段顺序:就诊日期(120)、就诊编号(120)、诊断(min 160)、处方摘要(min 180)、医生(90)、状态(140)、操作(120 固定右侧);根内边距 20 px(`components/CaseRecordList.vue:1-39,103-106`)。
点击“开方”进入直接依赖的全局 `@/components/tcm-prescription/index.vue`,不是在诊单 Tab 内原地编辑:它另开 1200 px Drawer、禁止点遮罩关闭,表单 label 100 px`src/components/tcm-prescription/index.vue:1-10`)。编辑态顺序为:
1. 患者信息:姓名 / 性别 / 年龄三等分,电话 / 门诊号两等分。
2. 诊断信息:面象 / 舌象两等分,临床诊断整行 2 行 textarea;舌象详情和脉象详情在模板中被注释(`index.vue:11-76`)。
3. 中药处方 RP:药材总数与锁定提示;添加主方、添加辅方、处方库导入、粘贴导入;处方价格;主方卡片网格;辅方卡片网格(`index.vue:78-216`)。
4. 用法信息:处方类型、主方用法、条件性辅方用法、剂数、剂量单位、用法、服用时间、服用方式、忌口、其他说明、医师、是否共享、医师签名(`index.vue:217-531`)。
信息分区间距 20 px、内边距 16 px、底 `#f5f7fa`、圆角 6 px;标题 15 px / 600 / `#303133``index.vue:2500-2516`)。RP toolbar 可换行,gap `12px 20px`、内边距 `14px 16px`、圆角 10 px、lighter 边框;标题 16 px、说明/锁定文字 12 px。药材编辑区桌面固定 4 列、gap 12 px,卡片白底、10 px 内边距、6 px 圆角;仅在 <=960 px 时让操作按钮行占满,**没有**把 4 列药材网格降列(`index.vue:2522-2690`)。签名区最大宽 600 px、1 px `#dcdfe6`、圆角 4 px`index.vue:2694-2711`)。
保存后同一 Drawer 切换为处方预览:Tab 为“药房联 / 处方联”,上方工具条与作废/审核驳回状态,主体是 A4 `210 × 297 mm` 白纸,`8mm 10mm` 内边距、13 px 字号、28 px / 700 标题(`index.vue:536-754,2714-2782`)。患者信息是 4 列表格,药材是 2 列,底部医师/类型/天数/剂量为网格(`index.vue:2807-2966,2990-3035`)。未保存编辑态才提供 Drawer footer`index.vue:899-905`)。
### 6.4 业务订单
订单 Tab/卡片顶部先显示“全局就诊序号偏移”工具条:label、tooltip、020 数字框(宽 120 px)、保存按钮(`components/PatientOrderList.vue:7-42`)。工具条换行、`12px 14px` 内边距、1 px lighter 边框、8 px 圆角、light fill`PatientOrderList.vue:252-276`)。
订单表为 bordered + striped,字段顺序和列宽来自 `PatientOrderList.vue:44-106`
1. 订单号,min 200。
2. 全局就诊序号,96。
3. 数量统计,96Tag。
4. 金额,120,右对齐红字。
5. 医生,110。
6. 医助,110。
7. 履约状态,110Tag。
8. 创建时间,min 170。
9. 详情,100,固定右侧。
默认 page size 为 10,底部带分页,点击详情打开只读订单详情(`PatientOrderList.vue:108-148`)。
订单详情复用 `PrescriptionOrderDetailDrawer` 的 readonly 受限版,而不是订单页内展开(`PatientOrderList.vue:113-124`)。该共享 Drawer 宽 80%Header `16px 24px`,标题 18 px / 600,订单号与药房单号用圆角 Tag;加载态为 12 行 skeletonbody 水平内边距 24 px并独立纵向滚动(`src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue:13-50,1752-1757`)。内容顺序为:
1. 金额概览:总金额、已付总额、退款金额、需代收、已付笔数;响应式 2 / 3 / 5 列(`PrescriptionOrderDetailDrawer.vue:154-197`)。
2. 两栏主区:左侧处方详情,右侧未关联收款与关联收款;收款表顺序为 ID、单号、类型、金额、状态、方式(readonly 隐藏)、创建人、创建时间(`PrescriptionOrderDetailDrawer.vue:200-545`)。
3. 履约与收货信息:3 列 descriptionslabel 宽 120 px`PrescriptionOrderDetailDrawer.vue:551-680,1782-1785`)。
4. 物流轨迹与操作日志(有数据/开启附加加载时)(`PrescriptionOrderDetailDrawer.vue:683-824`)。
readonly 明确隐藏顶部告警、流程步骤、挂号关联、内部成本、药材明细、收款方式和外部物流更新等完整版内容,但金额概览、主辅方用量/服法和基础详情仍共享(`PrescriptionOrderDetailDrawer.vue:2-11`)。面板统一 8 px 圆角,Header `14px 16px`、Body 16 px`PrescriptionOrderDetailDrawer.vue:1759-1780`)。
### 6.5 视频、聊天与历史表
**视频录制回放。** CallRecordPanel 编辑态有上传 toolbar;字段为播放(min 320)、开始时间 170、结束时间 170、通话类型 100、房间 180、时长 110、状态 90、录制 100、上传 180(仅编辑)(`components/CallRecordPanel.vue:1-74`)。空态内边距 `28px 12px`,标题 14 px、说明 12 px / 1.6`CallRecordPanel.vue:188-214`)。内嵌视频最大高 180 px、圆角 4 px、黑底;播放覆盖层为 `rgba(0,0,0,.62)`、白色 12 px`components/RecordingVideoPlayer.vue:322-359`)。
**聊天。** 顶部 info alert + 同步/刷新 toolbar,消息区最大高 `min(60vh,520px)`、gap 16 px。单条最大宽 88%;患者消息靠左、灰底,医生消息靠右、primary 浅底;meta 12 px,气泡 14 px、1.5 行高、`10px 12px` 内边距、8 px 圆角;图片最大 `240 × 200 px``components/ImChatRecordPanel.vue:1-70,200-293`)。
**医助指派历史。** 字段依次为时间 175、原医助 min 120、新医助 min 120、继承 72 Tag、快照订单创建人 min 130、快照创建时间 190、操作人 110、账号 120、IP 130`components/AssignLogPanel.vue:1-43`)。
**挂号历史。** 字段依次为 ID 72、状态 100 Tag、患者 min 150(姓名+手机堆叠)、医生 110、医助 110、预约时间 min 130(日期+时段堆叠)、预约类型 100、渠道 110、确认 92、处方 80、备注 min 100、创建时间 165(`components/AppointmentRecordPanel.vue:1-75`)。堆叠单元垂直 gap 2 px、行高 1.35`AppointmentRecordPanel.vue:172-178`)。
### 6.6 `components/` 全目录的可达性边界
三主视图的实际 import 清单见 `edit.vue:787-805``readonly.vue:151-167`。为避免把目录中保留组件误认成当前详情功能,本次也核了其余文件:
| 组件 | 三主视图当前可达性 | 结论 |
|---|---|---|
| `PatientInfoCard``PatientCaseCard` | readonly 直接使用 | 当前只读摘要/病例 |
| `DailyMatrix``CaseRecordList``PatientOrderList``CallRecordPanel``ImChatRecordPanel``AssignLogPanel``AppointmentRecordPanel` | edit/readonly 直接使用(依页面和权限而异) | 当前主链 |
| `RecordingPlaybackBlock``RecordingVideoPlayer` | CallRecordPanel 间接使用 | 当前视频主链(`CallRecordPanel.vue:32,78`; `RecordingPlaybackBlock.vue:5,35` |
| `DiagnosisTodoList` | DailyMatrix 间接使用 | 当前日常记录主链 |
| `TrackingNoteTimeline` | edit 有 import,但模板区整段注释 | 当前不可见(`edit.vue:644-654,803` |
| `BloodRecordList` | 未被三主视图 import | 旧/独立颗粒列表;其能力已由 DailyMatrix 内置 600 px 血糖血压 Dialog 承接(`BloodRecordList.vue:1-170`; `DailyMatrix.vue:131-185` |
| `DietRecordList` | 未被三主视图 import | 旧/独立列表;当前 DailyMatrix 内置 800 px 饮食 Dialog`DietRecordList.vue:1-112`; `DailyMatrix.vue:187-227` |
| `ExerciseRecordList` | 未被三主视图 import | 旧/独立列表;当前 DailyMatrix 内置 600 px 运动 Dialog`ExerciseRecordList.vue:1-86`; `DailyMatrix.vue:229-260` |
| `TrackingMatrix` | 未被三主视图 import | 非当前矩阵;当前入口明确 import `DailyMatrix``edit.vue:796`; `readonly.vue:161` |
| `AssistantWatchCallDialog` | 未被三主视图 import | 旁路观看 Dialog,不属于诊单详情/编辑/预约;自身是 auto-fill、最小 280 px 的观看网格,gap 12 px`AssistantWatchCallDialog.vue:1-19,242-282` |
因此 PySide 对齐应以“当前可达主链”为实现范围,不能因为同目录存在旧颗粒列表而重复增加血糖、饮食、运动 Tab,也不能把观看 Dialog 或 TrackingMatrix 插入诊单详情。
---
## 7. 加载、空态、错误和权限状态
| 场景 | 后台表现 | 源码 |
|---|---|---|
| 独立详情加载 | 页面级 loading | `readonly.vue:1-2` |
| 独立详情失败 | Hero 下方错误空态卡 | `readonly.vue:31-36` |
| 无未服务记录 | Hero 灰色“从未记录”状态 | `readonly.vue:323-355` |
| 抽屉只读 | 状态 badge + disabled fieldset + 无保存 | `edit.vue:11-54,57-84,766-781` |
| 患者隐私锁定 | warning Alert,身份字段不可改 | `edit.vue:71-84` |
| Tab 失权 | 隐藏 Tab;当前失权时回 basic | `edit.vue:655-759,883-913` |
| 预约未选医生 | 时间区提示先选医生 | `appointment.vue:100-171` |
| 无排班/无时段 | 时间区空态 | `appointment.vue:100-171` |
| 当天已有预约 | warning 且禁止确认 | `appointment.vue:10-18,359-386` |
| 其他日期已有预约 | info 提示,不等同当天阻断 | `appointment.vue:359-375` |
| 表格无数据 | 各组件 Element empty/自定义空态 | `PatientOrderList.vue:1-6`; `CallRecordPanel.vue:55-74` |
操作权限应控制“是否渲染/是否可编辑”,而不是只把无权按钮做成 disabled;这正是 Tab 通过 `v-if` 实现的模式。
---
## 8. PySide6 组件映射
### 8.1 结构映射
| Vue / Element 结构 | PySide6 建议 | 必须保留的视觉行为 |
|---|---|---|
| `el-drawer` 右侧抽屉 | 模态 `QDialog` + 右侧面板,或主窗口上的遮罩 `QFrame` + 固定宽面板 | 桌面约 60% 宽;edit 在窄屏切全宽;Header/Body/Footer 三段分离 |
| Drawer body | `QScrollArea` + body widget | 只让 body 滚动,footer 不随滚动 |
| Drawer footer slot | 独立 `QFrame` + `QHBoxLayout` | 上边框、白底、固定在底部;不要放进 ScrollArea |
| `el-tabs` | `QTabWidget` / 自定义 `QTabBar + QStackedWidget` | 可横向滚动、3 px active indicator、按 canonical permission 移除页签 |
| `el-form` + `el-row/el-col` | `QFormLayout``QGridLayout` | 桌面 160 px 编辑标签 / 100 px 预约标签;窄屏改上标签单列 |
| input/select/date/input-number | `QLineEdit``QComboBox``QDateEdit``QSpinBox/QDoubleSpinBox` | 常规高 32 px、圆角 10 pxfooter 按钮 40/44 px |
| radio/checkbox button groups | `QButtonGroup` + 可换行 FlowLayout | 12 px 字号,组间 1216 px;小窗必须换行 |
| textarea | `QTextEdit/QPlainTextEdit` | 2/3/4 行按源码用途设置 minimumHeight,而非无限增高 |
| alert / empty | `QFrame + QLabel` / 统一 EmptyState | warning/info/error 色语义与文案位置一致 |
| card | `QFrame` | 白底、1 px `#e6ebf2`、14 px radius、18 px padding |
| table | `QTableView + QAbstractTableModel` | stripe、固定/最小列宽、状态 `QStyledItemDelegate` tag、右侧动作列 |
| timeline | `QListView` 或纵向 QWidget 列表 + paintEvent 轴线 | 22 px 左缩进、2 px 轴、10 px node,图片缩略图 64 px |
| appointment slot grid | `QScrollArea + FlowLayout` / `QGridLayout` 动态列 | cell min 110 × 70;选中/禁用/可用状态显式 |
| loading | overlay `QFrame` + spinner/文字 | 遮住当前内容但保留布局尺寸,异步返回后解除 |
### 8.2 推荐常量(来自源码,不是重新设计)
```text
FONT_FAMILY = "PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei"
FONT_BASE = 14
FONT_FORM = 12
CONTROL_HEIGHT = 32
FOOTER_BUTTON_HEIGHT = 40 # <=768 对应布局时 44
CARD_RADIUS = 14
CARD_PADDING = 18
CONTROL_RADIUS = 10
PANEL_GAP = 16
BORDER = #e6ebf2
BODY_BG = #f8fafc
PAGE_BG = #f6f6f6
TEXT_PRIMARY = #333333
TEXT_REGULAR = #666666
TEXT_MUTED = #999999
PRIMARY = #409eff # 继承 Element 语义主色
READONLY_ACCENT = #2563eb
READONLY_ACCENT_STRONG = #1d4ed8
```
### 8.3 PySide 字段装配顺序
为避免实现时按数据模型字母序排字段,应按以下 UI 顺序装配:
```text
编辑病历:
诊单ID
→ 姓名 / 身份证
→ 手机 / 性别
→ 年龄
→ [生命体征] 婚姻 / 身高 / 体重
→ 地区 / 高压 / 低压
→ 空腹血糖
→ 诊断类型
→ 状态 / 渠道
→ 统计端就诊卡
→ 在用药物
→ [主诉] 当地诊断日期 / 糖尿病病史
→ 当地医院诊断结果
→ 当地医院名称
→ [现病史] 口腔 → 饮水 → 体重变化 → 脂肪肝 → 饮食
→ 肢体 → 睡眠 → 眼睛 → 头部 → 出汗 → 皮肤 → 小便 → 大便 → 腰肾
→ 其他补充
→ [既往史] 既往史
→ [其他病史] 外伤 / 手术 / 过敏 → 家族 / 妊娠
→ [诊断信息] 病史补充
预约:
上次面诊 → 预约方式 → 预约类型 → 预约患者 → 渠道来源
→ [条件] 自媒体详情 → 预约医生 → 日期 → 时段 → 备注
```
---
## 9. 验收清单
1. 独立 readonly 是纵向卡片流,不出现 Tabs;抽屉 viewOnly 才是 Tabs + disabled 表单。
2. 编辑 Drawer 桌面 60%<=768 等效状态下全宽;预约 Drawer 源码没有该响应式覆盖,需明确属于适配增强。
3. 编辑桌面 label 160 px,预约 label 100 px;编辑移动端 label 在控件上方。
4. 编辑的 body 独立滚动,footer 始终可见;实现中没有把按钮放到滚动区末尾。
5. Tab 顺序与 canonical permission 一致;“跟踪备注”不渲染。
6. 编辑病历字段严格按 §4.5;不把脚本中未渲染字段自行加入。
7. 只读病例按 4/3 列网格及 §3.4 顺序;小窗若做降列,需视为桌面端增强而非源码既有行为。
8. 预约状态至少覆盖:未选医生、无排班、可约、不可约、选中、当天冲突、其他日期提示、loading。
9. 订单、备注、视频、聊天、指派历史、挂号历史的列顺序/动作显隐与 §6 一致。
10. 所有表格提供 loading、empty、error;权限失去时移除入口并将当前页回退到可见页。
以上规格记录的是当前后台源码的可见事实;PySide 可为桌面小窗增加降列和全宽适配,但不得改变字段顺序、权限门槛、状态语义或主操作位置。
@@ -0,0 +1,31 @@
# Diagnosis 详情二次终验报告
日期:2026-08-10
## CLOSED
- 统计端就诊卡:使用原生自绘二态 `DiagnosisSwitch`,保存值严格为 `0/1`
- 日常记录:提供 `+ 血糖 / + 饮食 / + 运动 / + 备注`,以及待办新增、仅待执行且 `can_cancel` 的精确取消;Remote/Demo/Protocol 均接真实方法,包含权限、DTO 校验和 generation 失效保护。
- 诊疗笔记:接入新增/当日追加、舌象与报告上传、单附件删除;所有动作均调用现有真实 endpoint。
- 处方与订单:处方按钮进入完整 `PrescriptionEditorDialog` 并调用 `tcm.prescription/add`;诊次偏移写入 `setRevisitSlotStartOffset`;订单详情调用真实详情方法。
- 视频回放:列表、视频上传、手工通话记录创建、附件绑定均接真实 endpoint;播放仅允许合法 `HTTP(S)` 外链。
- IM:固定 `only_archived=1`,支持异步同步与重新加载,并显示文本、图片、文件消息。
- 视觉:tab 使用 4px 横向滚动条并隐藏原生左右箭头框;footer 有向上阴影;保存按钮自绘 loading spinner、成功勾、失败叉;readonly 摘要禁止拆字;表格状态改为 pill/tag。
- Shell owner:每次 `open_for` / `showEvent``parentWidget().window()` 重新绑定,按 Shell 宽度 60% 计算抽屉,move/resize 立即同步,关闭后全部请求 generation 失效。
- 列表补充合同:QR、挂号日志、通用订单与 exact `doctor.appointment/cancel` 均有 Protocol/Remote/Demo 方法。
## OPEN(服务端合同不存在,fail-closed
- 诊疗笔记“任意改写既有正文”与“删除整条笔记”没有后端 endpoint,因此桌面端没有伪造动作。现有合同只允许“当天追加”和“删除单个附件”:
- `server/app/adminapi/controller/doctor/AppointmentController.php:168-192` 仅公开 `addDoctorNote``doctorNotes``deleteDoctorNoteImage`
- `server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-67``addOrAppend``diagnosis_id + 当天` 追加正文/附件,不是任意正文覆盖。
- `server/app/adminapi/logic/doctor/DoctorNoteLogic.php:114-141` 仅实现单个附件删除。
- `admin/src/api/patient.ts:20-40` 同样只有上述三个客户端 API。
## 验证
- 定向详情与合同:`28 passed`
- 相关 repository/reception/video`57 passed`
- 全量:`226 passed`
- `ruff check .`:exit 0(仅工作区不可访问缓存目录的扫描 warning)。
- 视觉产物:`artifacts/diagnosis_visual/` 中 6 张模式图与 14 张详情状态图,共 20 张本轮详情 PNG。
+155
View File
@@ -0,0 +1,155 @@
# Diagnosis 最终视觉发布门禁
审计日期:2026-08-11
审计性质:第四次独立、只读、最终视觉发布门禁;除更新本报告外,未修改业务源码、测试或 PNG。
基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/` 及其主链直接依赖组件。
当前截图:`D:/web/zyt/app/artifacts/diagnosis_visual/`
Trellis`D:/web/zyt/app/.trellis` 不存在(`Test-Path``False`),因此无可补读的 Trellis workflow/spec。
审计口径:只有与基准达到同型才记 PASS;Qt 与浏览器字体栅格化造成的轻微像素差不算结构问题。
## 最终结论
**PASS。**
- 共逐张按原始像素尺寸检查 **57 张 PNG**
- 逐图判定:**57 PASS / 0 PARTIAL / 0 不可读 / 0 硬裁切到窗口外**。
- 严重度:**P0 = 0P1 = 0P2 = 0**。
- 旧审计中的 Notes 真实缩略图、Chat 图片缩略图、Daily 局部工具栏、1200 px 连续处方 Drawer、订单 80% 详情 Drawer、订单 offset、Shell 与预约均已闭合或保持通过。
- 上轮唯一 P1——视频表内播放器被压成 16 px 黑条——已关闭。`diagnosis_video_inline_player_1200x560.png``diagnosis_state_video_replay_1024x640.png` 均显示完整加载遮罩、播放按钮、进度条、时间、外部打开、独立窗口和备用地址;播放器本体运行时高度为 158 px,未超过基准 180 px 上限。
尺寸分布(按 PNG 实际像素而非文件名):`1024x640` 24 张、`1024x768` 1 张、`1200x560` 1 张、`1200x900` 4 张、`1280x800` 11 张、`1440x900` 13 张、`650x620` 1 张、`820x560` 1 张、`920x780` 1 张。
第四次增量完整性:目录仍为 57 张;相对上一门禁仅 3 张视频 PNG 更新(`diagnosis_video_inline_player_1200x560.png``diagnosis_state_video_replay_1024x640.png``diagnosis_state_video_upload_action_1024x640.png`)。本轮已对这 3 张全部按原始尺寸复核,并原尺寸抽查 Notes、Chat、Daily、处方四段连续态、订单 80% Drawer/offset、Shell、预约及 1024/1440 主列表;其余非视频 PNG 时间戳未变化,沿用上一轮全量逐图结论。
## 此前 OPEN 逐项复核
| 旧 OPEN | 终态 | 视觉与结构证据 |
|---|---|---|
| Notes 舌象真实缩略图 | **CLOSED** | `diagnosis_state_notes_actions_1024x640.png` 已显示实际 64x64 舌象位图,并有右上删除入口;当前 `_RemoteImageButton` 以 cover 方式加载到 `QSize(64, 64)``src/doctor_workstation/ui/diagnosis_drawer.py:2364-2404`。基准为真实 `el-image` 64x64 cover`D:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:52-69`。 |
| Chat 图片消息缩略图 | **CLOSED** | `diagnosis_state_chat_archive_1024x640.png` 的患者图片消息直接显示实际图像缩略图,不再是“查看图片”文字链接;当前最大 `240x200``diagnosis_drawer.py:2553-2573`。基准:`ImChatRecordPanel.vue:39-50`。 |
| Daily 待办局部工具栏 | **CLOSED** | `diagnosis_state_daily_lower_1024x640.png` 同时显示 `+ 新增待办`、全部/待执行/已发送/失败/已取消及局部“刷新”,无遮挡;当前:`diagnosis_drawer.py:1725-1761`,基准:`DiagnosisTodoList.vue:3-20`。顶部动作行也继续在 `diagnosis_state_daily_1024x640.png` 完整换行。 |
| 处方 1200 px 连续 Drawer | **CLOSED** | `diagnosis_state_prescription_editor_1200x900.png``..._herbs_1200x900.png``..._usage_1200x900.png``..._signature_1200x900.png` 连续证明患者、诊断、RP 药材、用法、签名处于同一纵向滚动流,固定 footer 稳定;1024/920 窄宽度采用可用宽度而非截断。当前 `DRAWER_WIDTH = 1200` 且五段连续加入同一 `QScrollArea``src/doctor_workstation/ui/dialogs/prescription.py:1467,1491-1556,1902-1914`。基准:`D:/web/zyt/admin/src/components/tcm-prescription/index.vue:2-216,217-531`。旧 4-tab modal 结构已消失。 |
| 订单 80% 详情 Drawer | **CLOSED** | `diagnosis_order_detail_drawer_1440x900.png` 的 panel 从 x=288 开始、宽 115280%);`diagnosis_state_order_detail_640x540.png``diagnosis_state_order_detail_drawer_1024x640.png` 的实际像素均为 1024x640panel 从 x=205 开始、宽 819(约 80%)。金额概览、处方详情、关联收款在首屏可见;当前连续构建收款、履约与收货、物流轨迹、操作日志:`src/doctor_workstation/ui/dialogs/diagnosis.py:669-710,3623-4065`。基准共享 readonly Drawer`PrescriptionOrderDetailDrawer.vue:13-20,154-220,433-545,550-824`。旧 `640x540` 仅是遗留文件名,不是当前截图尺寸。 |
| 订单 offset 文案与解释层级 | **CLOSED** | `diagnosis_state_order_offset_1024x640.png` 已为“复诊统计起始偏移”+ 问号帮助 +“第 1 笔实单计为三诊”+“保存”,与基准同型;当前:`diagnosis.py:1384-1416,3001-3058`,基准:`PatientOrderList.vue:7-41`。 |
| 视频多地址与逐条上传 | **CLOSED(能力/动作)** | `diagnosis_state_video_upload_action_1024x640.png` 的行级“追加回放”可见;当前每行绑定精确 `call_record_id``diagnosis.py:3304-3342`。备用地址结构存在于 `diagnosis_media.py:358-442`。 |
| 视频表内播放器 | **CLOSED** | 两张终态图均显示完整可操作的表内播放器与备用地址;第四次运行时复核在 1200x560、1024x640 宿主中均测得 row 243 px、cell 242 px、player 158 pxcontrols 全部可见。详见下节。 |
| Shell | **CLOSED / 无回归** | `diagnosis_shell_1024x640.png``diagnosis_shell_1440x900.png` 均保持 183 px sidebar、50 px topbar、40 px tabs,操作区及固定列无错位。 |
| 预约 Drawer | **CLOSED / 无回归** | 8 张预约 PNG 的 60% Drawer、固定 footer、时段网格、empty/error/loading/refreshing/focus 状态均完整。 |
## P0 / P1 / P2
### P0
**0 项。** 未发现窗口级硬裁切、核心信息完全不可见、Drawer 比例失效、footer 滚走、二维码不可辨识、Shell 壳层错位或预约主流程不可操作的发布阻断。
### P1
**0 项。** 上轮唯一 P1“视频表内播放器被裁成 16 px 黑条”已关闭。
#### 视频表内播放器关闭证据
精确截图:
- `D:/web/zyt/app/artifacts/diagnosis_visual/diagnosis_video_inline_player_1200x560.png`
- `D:/web/zyt/app/artifacts/diagnosis_visual/diagnosis_state_video_replay_1024x640.png`
两张图在原始尺寸中均呈现完整播放器:黑色视频 surface 内有“预览待加载 / 点击播放开始加载”,下方可见“播放”、进度条、`00:00 / 00:00`、“外部打开”和“独立窗口”;播放器下方继续显示“备用地址”及 `COS HLS 1``链接 2`,第二条无回放记录也保持独立普通行。1200 与 1024 宿主均无裁切、重叠或 controls 遗失。
第四次运行时几何复核(使用与最终截图相同的 `RenderRepository` 与两行 fixture,未写 PNG)在 1200x560 和 1024x640 两种宿主下得到一致结果:
- table row 0243 px;可视 cell rect242 px`RecordingPlaybackCell`242 px。
- `InlineRecordingPlayer`158 px,满足实现的 158–180 px 约束,也满足基准 `max-height: 180px`
- 播放按钮、slider、时间标签及整个 player 均 `visible=True`
- 备用地址使外层 cell 高于 180 px 是正确行为;180 px 上限只约束播放器本体,不约束播放器 + 备用地址的整行总高。
当前源码的闭环点:
- `src/doctor_workstation/ui/diagnosis_media.py:179-278` 构建 158180 px 内嵌 player、视频 surface 与完整控制栏。
- `diagnosis_media.py:359-457` 将 player、备用地址组成定高 cell,并由 `required_table_row_height()` 返回真实所需高度。
- `src/doctor_workstation/ui/dialogs/diagnosis.py:3304-3367` 在 Qt 的 `resizeRowsToContents()` 后,以 cell 实际 hint 恢复 row 高度并写入 item size hint,避免另一列固定高按钮再次压扁整行。
基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/RecordingPlaybackBlock.vue:1-30` 在“录制回放”单元格内直接放置 `RecordingVideoPlayer``RecordingVideoPlayer.vue:2-13,320-329` 显示 controls、16:9、最大高 180 px 的完整播放器。当前终态在结构、信息层级和可操作性上已同型,因此关闭 P1。
### P2
**0 项。** 上一轮两项 P2 已闭合:Daily 待办已具精确 `+ 新增待办` 与局部刷新;订单 offset 已具精确标题、帮助入口、动态解释与“保存”文案。
## 57 张 PNG 逐张判定
### 预约 Drawer8/8 PASS
| PNG | 判定 | 原始尺寸下目检结论 |
|---|---|---|
| `appointment_drawer_1024x640.png` | PASS | 60% Drawer、body 滚动、时段状态与固定 footer 完整。 |
| `appointment_drawer_1440x900.png` | PASS | 医生、日期、时段网格、备注与 footer 对齐。 |
| `appointment_drawer_empty_doctors_1440x900.png` | PASS | warning、空医生状态与禁用确认完整。 |
| `appointment_drawer_empty_roster_1440x900.png` | PASS | 无排班 warning、医生选择与空态完整。 |
| `appointment_drawer_error_1440x900.png` | PASS | 号源错误 banner、刷新入口、空态与禁用确认完整。 |
| `appointment_drawer_keyboard_focus_1440x900.png` | PASS | 日期按钮 focus ring 清晰,不挤压布局。 |
| `appointment_drawer_loading_1440x900.png` | PASS | 初始 loading overlay、spinner、文案与 footer 完整。 |
| `appointment_drawer_refreshing_1440x900.png` | PASS | 刷新 overlay、spinner、文案与 footer 完整。 |
### 诊单列表、壳层与主 Drawer23/23 PASS
| PNG | 判定 | 原始尺寸下目检结论 |
|---|---|---|
| `diagnosis_1024x640.png` | PASS | 窄屏筛选换行、横向滚动、右 460 px 固定区与行语义完整。 |
| `diagnosis_1440x900.png` | PASS | 宽屏列密度、复合挂号格、业务色与固定操作区成立。 |
| `diagnosis_advanced_filters_1280x800.png` | PASS | 高级筛选全量展开,无控件裁切。 |
| `diagnosis_double_appointment_cancel_1280x800.png` | PASS | 同诊单双挂号各有取消入口。 |
| `diagnosis_edit_1024x640.png` | PASS | 60% Drawer、tab、独立滚动与固定 footer 成立。 |
| `diagnosis_edit_1440x900.png` | PASS | 864 px Drawer 与宽屏字段比例稳定。 |
| `diagnosis_empty_1280x800.png` | PASS | 表头、空态、横向滚动与 0 条分页保留。 |
| `diagnosis_error_1280x800.png` | PASS | 错误态清晰,无旧数据穿透。 |
| `diagnosis_focus_1280x800.png` | PASS | 搜索 focus ring 清晰且不挤压布局。 |
| `diagnosis_full_menu_1280x800.png` | PASS | 8 项菜单、图标、danger 删除与“二维码”均完整。 |
| `diagnosis_horizontal_scroll_1024x640.png` | PASS | 主表横移时右 460 px 固定区不动。 |
| `diagnosis_hover_warning_1280x800.png` | PASS | warning 行 hover 与左侧强调条连续。 |
| `diagnosis_loading_1280x800.png` | PASS | 表内 spinner 与淡化内容完整。 |
| `diagnosis_order_detail_drawer_1440x900.png` | PASS | 80% Drawer、金额概览、处方详情、收款记录与固定 footer 成立。 |
| `diagnosis_order_qrcode_1280x800.png` | PASS | QR 可辨、订单号、状态、只读 URL 与动作完整。 |
| `diagnosis_pending_assign_1280x800.png` | PASS | 待分配医助、月份与搜索处于同一流式区域。 |
| `diagnosis_permissions_cropped_1280x800.png` | PASS | 权限收口后只保留查看,无敏感动作残留。 |
| `diagnosis_readonly_1024x640.png` | PASS | 独立只读页 hero、病例卡与窄屏字段无拆字。 |
| `diagnosis_readonly_1440x900.png` | PASS | 4 列病例密度、异常指标与长页滚动成立。 |
| `diagnosis_shell_1024x640.png` | PASS | 183/50/40 壳层、tabs、固定列及分页成立。 |
| `diagnosis_shell_1440x900.png` | PASS | 宽屏壳层比例及顶栏图标/中文稳定。 |
| `diagnosis_viewonly_1024x640.png` | PASS | 60% 只读 Drawer、警告、disabled 表单与仅关闭 footer 成立。 |
| `diagnosis_viewonly_1440x900.png` | PASS | 宽屏只读栅格、全 tabs 与固定 footer 成立。 |
### 详情状态与子流程(26/26 PASS)
| PNG | 判定 | 原始尺寸下目检结论 |
|---|---|---|
| `diagnosis_state_chat_archive_1024x640.png` | PASS | info、归档同步、左右气泡与真实图片缩略图成立。 |
| `diagnosis_state_daily_1024x640.png` | PASS | Daily 动作完整换行,无裁切。 |
| `diagnosis_state_daily_blood_edit_650x620.png` | PASS | 血糖/血压字段、单位、必填说明与固定动作完整。 |
| `diagnosis_state_daily_lower_1024x640.png` | PASS | `+ 新增待办`、filters、局部刷新、状态与横向滚动完整。 |
| `diagnosis_state_empty_1024x640.png` | PASS | 处方空态与 primary“开方”成立。 |
| `diagnosis_state_error_1024x640.png` | PASS | danger banner、重试、空表单与禁用保存完整。 |
| `diagnosis_state_focus_1024x640.png` | PASS | 姓名 focus ring、close 与固定 footer 完整。 |
| `diagnosis_state_loading_1024x640.png` | PASS | loading overlay、spinner 与文案完整。 |
| `diagnosis_state_notes_actions_1024x640.png` | PASS | 新增/上传动作、实际舌象缩略图、报告 chip 与单附件删除完整。 |
| `diagnosis_state_order_detail_640x540.png` | PASS | 实际为 1024x640 的 80% Drawer;金额与处方首屏、滚动与 footer 成立。 |
| `diagnosis_state_order_detail_drawer_1024x640.png` | PASS | 80% Drawer 窄屏终态与上一张一致且无结构裁切。 |
| `diagnosis_state_order_offset_1024x640.png` | PASS | 精确标题、帮助、动态解释、“保存”、列表与分页成立。 |
| `diagnosis_state_permission_1024x640.png` | PASS | 获权 tabs、敏感字段掩码与仅关闭 footer 成立。 |
| `diagnosis_state_prescription_editor_1024x768.png` | PASS | 窄屏连续 Drawer 首段、RP 工具栏、滚动与固定 footer 成立。 |
| `diagnosis_state_prescription_editor_1200x900.png` | PASS | 1200 px 连续 Drawer 患者/诊断/RP 起始结构成立。 |
| `diagnosis_state_prescription_editor_920x780.png` | PASS | 窄宿主按可用宽度适配,旧 tabbed modal 已消失。 |
| `diagnosis_state_prescription_editor_herbs_1200x900.png` | PASS | 主方/辅方网格、剂量与删除入口处于同一滚动流。 |
| `diagnosis_state_prescription_editor_signature_1200x900.png` | PASS | 辅方用法、医生签名板及固定 footer 成立。 |
| `diagnosis_state_prescription_editor_usage_1200x900.png` | PASS | 主/辅方用法层级、说明字段与连续滚动成立。 |
| `diagnosis_state_save_failure_1024x640.png` | PASS | 红色失败态与文案完整。 |
| `diagnosis_state_save_loading_1024x640.png` | PASS | 按钮内 spinner 与“正在保存”完整。 |
| `diagnosis_state_save_success_1024x640.png` | PASS | 绿色成功态与文案完整。 |
| `diagnosis_state_video_player_820x560.png` | PASS | 独立播放器作为兼容兜底本身完整;不替代表内播放器要求。 |
| `diagnosis_state_video_replay_1024x640.png` | PASS | 158 px 表内 player、加载态、完整 controls 与备用地址均可见,第二条普通行未受高行影响。 |
| `diagnosis_state_video_upload_action_1024x640.png` | PASS | 横移后 243 px 高视频行仍保持状态、录制与“追加回放”垂直居中,第二条普通行未被错误拉高。 |
| `diagnosis_video_inline_player_1200x560.png` | PASS | 宽屏完整显示 158 px 表内 player、controls、备用地址及相邻列,无裁切。 |
## 发布判定
- 旧审计的 Notes、Chat、Daily、处方、订单、视频、Shell、预约问题已经全部闭合或保持通过。
- 两张重新录制的视频原始尺寸 PNG 与运行时实际控件几何相互印证,旧唯一 P1 已关闭。
- 当前最终门禁:**PASS57 PASS / 0 PARTIAL,共检查 57 张 PNGP0=0P1=0P2=0)。**
+373
View File
@@ -0,0 +1,373 @@
# 管理端“诊单列表”视觉规格(源码基准)
> 审计日期:2026-08-10
> 唯一业务页面基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue`
> 方法:只读检查 Vue 模板、同文件 scoped SCSS、直接挂载组件、全局样式、主题生成逻辑和当前锁定的 Element Plus 源码;未启动浏览器、未改业务源码。
## 1. 结论摘要
1. 该页面**没有页面内标题、说明文案或独立标题栏**。路由内容从一张无边框、无阴影的筛选卡直接开始;“新增诊单”等主操作位于第二张列表卡顶部,而不是页面标题旁。证据:`index.vue:2-154`
2. 页面是“浅灰页面底 + 两张白卡”的高密度后台列表。外层路由容器提供左右 `8px`、上下 `16px` 的留白;筛选卡和列表卡之间为 `12px`。证据:`D:/web/zyt/admin/src/layout/default/components/main.vue:2-10``index.vue:154``D:/web/zyt/admin/tailwind.config.js:103-113`
3. 顶部不是常规 tabs,而是可换行的圆角文字 chip:6 个日期入口,再接“待预约 / 已完成 / 待分配医助”,计数是文字后面的普通半透明数字,不是独立 badge。当天默认选中。证据:`index.vue:6-70,964-976,2174-2182,2222-2317`
4. 表格声明列的最小总宽约为 **1403px**;右侧固定“视频旁观” `120px` 与“操作” `340px`,合计 `460px`。窄于该宽度时由 Element Table 横向滚动,右侧两列保持 fixed。证据:`index.vue:191-342`
5. 表格使用斑马纹;普通偶数行是 `#FAFAFA`,普通悬停是 `#F8F8F8`。未确认、未挂号行还声明了左侧语义色和横向渐变,但静态渐变写在 `tr`,斑马纹及 fixed cell 的 `td` 背景可能遮住它;两类行的 hover 渐变直接写在 `td`,表现更稳定。证据:`index.vue:2723-2735`、Element Plus `table.scss:560-588,665-668`
6. 空态不是 `el-empty` 插画,而是表格默认的 **60px 高“暂无数据”文本区**;加载态是覆盖表格的半透明蒙层和 `42px` 主色环。证据:Element Plus `table.scss:52-70``loading.scss:19-73`、中文 locale `zh-cn.mjs:137`
## 2. 源码证据层级
| 层级 | 文件 | 本规格使用内容 |
|---|---|---|
| 页面模板与局部样式 | `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:1-2766` | DOM 次序、列宽、状态类、局部尺寸和颜色 |
| 路由内容容器 | `D:/web/zyt/admin/src/layout/default/components/main.vue:1-12` | 页面底色、外层滚动、`px-2 py-4` |
| 共享组件 | `D:/web/zyt/admin/src/components/pagination/index.vue:1-50``D:/web/zyt/admin/src/components/daterange-picker/index.vue:1-49` | 分页布局、页容量、日期范围控件行为 |
| 项目全局样式 | `D:/web/zyt/admin/src/styles/index.scss:1-5``var.css:1-49``element.scss:58-68,106-115,140-151,177-201``public.scss:1-3` | 全局字体、颜色、表格、输入焦点、移动分页 |
| 项目主题 | `D:/web/zyt/admin/src/config/setting.ts:1-15``src/utils/theme.ts:3-75``src/App.vue:25-34` | 默认主色、明暗主题色派生、运行时覆盖 |
| Tailwind token | `D:/web/zyt/admin/tailwind.config.js:5-155` | 字体族、颜色映射、间距值 |
| UI 框架 | `D:/web/zyt/admin/pnpm-lock.yaml:47-49,2412-2415``node_modules/element-plus/theme-chalk/src/**` | 当前锁定 Element Plus 2.13.7 的默认组件尺寸 |
`package.json:27` 写的是兼容范围 `^2.9.4`,当前 lock 与已安装源码均为 **2.13.7**;本规格的框架默认尺寸以锁文件和当前安装源码为准。
## 3. 页面骨架与 ASCII wireframe
### 3.1 宽屏主态
```text
主内容滚动区,#F6F6F6;左右 8,上下 16
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ 筛选卡 #FFF / r4 / 无边框无阴影;内边距 12V × 16H │
│ [昨天挂号 n] [前天挂号 n] [当天挂号 n] [明天挂号 n] [后天挂号 n] [全部 n] │
│ [待预约 n] [已完成 n] [待分配医助 n] [患者姓名 / 手机号 160] [查询 32h] │
│ ──────────────────────────────────────────────────────────────────────────────────────── │
│ 挂号:[全部] [已挂号] [未挂号] │ 确认:[全部] [已确认] [未确认] 更多筛选⌄ │
│ · · · 展开时:诊断类型 / 证型 / 医助 / 最近挂号日期 / 最近指派日期 / 渠道 / 重置 · · · │
└────────────────────────────────────────────────────────────────────────────────────────────┘
12px
┌────────────────────────────────────────────────────────────────────────────────────────────┐
│ [新增诊单] [批量指派医助] [批量取消指派] 已选 n 条(按需显示) │
├────┬──────┬──────┬───────────┬──────────────┬──────┬────────┬──────┬──────┬──────────┬──────┬────────────────────┤
│ □ │ ID │ 患者 │ 性别/年龄 │ 挂号 │ 确认 │ 复诊 │ 助理 │ 开方 │未服务天数│旁观* │ 操作* │
├────┼──────┼──────┼───────────┼──────────────┼──────┼────────┼──────┼──────┼──────────┼──────┼────────────────────┤
│ □ │123 NEW│张三 │ 男 · 42岁 │[待问诊] 医生 │未确认│日期/医生│医助 │已开方│ 8 │进入 │查看 诊单 开方 … 更多│
│ │ │ │ │时间 / 渠道 │ │ │ │ │ │ │ │
├────┴──────┴──────┴───────────┴──────────────┴──────┴────────┴──────┴──────┴──────────┴──────┴────────────────────┤
│ 共 n 条 [15条/页] [<] 1 2 … [>] 前往 [ ] 页 │
└────────────────────────────────────────────────────────────────────────────────────────────┘
* 右侧固定列,共 460px
```
### 3.2 结构说明
- 外层 `main` 是整页垂直滚动容器;诊单表没有本页固定高度或纵向虚拟滚动,15 条数据会把页面自然撑高。证据:`layout/default/components/main.vue:2-11``index.vue:179-190`
- 两张卡都使用 `shadow="never"``!border-none`;保留 Element Card 的白底、裁切和 `4px` 圆角。框架默认:Element Plus `card.scss:9-17``common/var.scss:1226-1235`
- 页面根节点只设 `min-height:100%`、页面底色和水平居中;不设自身宽度、最大宽度或 padding。证据:`index.vue:2200-2206`
- 顶部全局 header、面包屑和多页签属于应用壳,不在 `index.vue` 中;它们是否出现受全局 setting 控制,不能当作本页标题复刻。
## 4. 可复刻的基础 token
### 4.1 字体与基础尺寸
| 项 | 源码确定值 | 证据 |
|---|---:|---|
| 字体族 | `PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei, sans-serif` | `tailwind.config.js:79-81``var.css:2` |
| body / Element base | `14px` | `public.scss:1-3``var.css:16` |
| small / extra-small | `13px / 12px` | `var.css:17-18` |
| 默认控件高度 | `32px` | Element Plus `common/var.scss:233-241` |
| small 控件高度 | `24px` | 同上 |
| 默认圆角 | `4px` | Element Plus `base.css:1` |
| 卡片圆角 | `4px` | Element Plus `common/var.scss:1227-1234` |
字体栅格的真实像素高度仍受 Windows 缩放、浏览器 DPR 与首个可用字体影响;页面 chip 没有固定 `height`/`line-height`,只能精确复刻 padding,不能仅凭源码承诺最终外框高度。
### 4.2 亮色主题
项目在 `App.vue` mount 时用 setting 重写 Element 语义色。默认主色不是 Element 原生蓝,而是 `#4A5DFF`。主题派生算法见 `src/utils/theme.ts:3-18,29-37,55-74`
| token | 默认运行值 | 使用处 |
|---|---|---|
| `--el-color-primary` | `#4A5DFF` | active chip、主按钮、链接、loading |
| primary light-3 / 5 / 7 / 8 / 9 | `#808EFF / #A5AEFF / #C9CEFF / #DBDFFF / #EDEFFF` | hover、focus、轻底色 |
| primary dark-2 | `#3B4ACC` | active 状态;页面另有硬编码近似值 `#3A4ACC` |
| success | `#67C23A` | 已完成 chip、已确认、成功按钮 |
| warning | `#E6A23C` | 待分配、未确认、警示行 |
| danger | `#F56C6C` | NEW、删除/取消 |
| info | `#909399` | 待预约、完成预约、次要态 |
| page / card | `#F6F6F6 / #FFFFFF` | 页面底 / 卡片与普通行 |
| primary / regular / secondary text | `#333333 / #666666 / #999999` | 标题信息 / 常规 / 辅助 |
| placeholder / disabled | `#A8ABB2 / #C0C4CC` | 空值、禁用 |
| border / light / lighter | `#DCDFE6 / #E4E7ED / #EBEEF5` | 控件、卡线、行分隔 |
| fill / light / lighter | `#F0F2F5 / #F8F8F8 / #FAFAFA` | hover、表头、斑马纹 |
| loading mask | `rgba(255,255,255,.5)` | 表格加载蒙层 |
证据:`src/config/setting.ts:9-14``src/styles/var.css:9-43`。主色的派生结果使用项目同一 `css-color-function` 算法计算;不是页面硬编码。warning/info 的页面渐变使用 `--el-color-*-rgb`,其当前框架默认分别为 `230,162,60``144,147,153`Element Plus `base.css:1`)。
### 4.3 暗色与运行时主题边界
- `.dark` 下页面底为 `#0A0A0A`、普通背景 `#1D2124`、overlay `#1D1E1F`,主/常规/次要文字为 `#E5EAF3 / #CFD3DC / #A3A6AD`lighter border 为 `#363637`。证据:`src/styles/dark.css:1-31`
- `App.vue` 根据 `useDark()` 调用主题生成器,主色仍可保持 `#4A5DFF`,但 light-* 在暗色下改用 shade 算法。证据:`src/App.vue:29-34``src/utils/theme.ts:12-18,55-77`
- setting 会从本地缓存覆盖默认颜色,因此**某个真实账号的主色/语义色可能不同**。证据:`src/stores/modules/setting.ts:12-50,65-77`。上表是无用户自定义时的确定基线。
- 页面内硬编码的 `#4A5DFF``#3A4ACC``rgba(74,93,255,...)` 不会随自定义主题变化,因而自定义主题下会出现两套蓝色。证据:`index.vue:2522-2529,2556-2559,2596-2599`
## 5. 顶部筛选卡
### 5.1 卡与第一行
筛选卡 body padding 为 `12px 16px`。第一行 `.filter-main` 是单行 flex:垂直居中、两端对齐、组间 `16px`、下边距 `8px`**自身没有 `flex-wrap`**。左侧 `.date-chips` 才可换行,chip 间距 `6px`。证据:`index.vue:3-76,2208-2225`
| 元素 | 几何/排版 | 默认/交互颜色 | 证据 |
|---|---|---|---|
| 普通日期 chip | padding `6px 12px`14px/500r6transition `.2s` | 常规 `#666` + `#F8F8F8`hover `#F0F2F5`active 白字 + 主色 | `index.vue:2227-2250` |
| chip 计数 | 与标题相隔 `4px`normal styleopacity `.8` | 跟随父 chip | `index.vue:23-24,2237-2241` |
| 待预约 | 普通 chip 外再有左 margin `8px` | info / info-light-9active info 实色 | `index.vue:26-32,2253-2265` |
| 已完成 | 左 margin `8px` | success / success-light-9active success 实色 | `index.vue:33-39,2268-2280` |
| 待分配医助 | 权限控制;外容器可换行、gap `8px`、左 margin `8px` | warning / warning-light-9active warning 实色 | `index.vue:40-70,2283-2308` |
| 待分配月份 | default 月选择器;宽 `128px` | Element 默认输入态 | `index.vue:51-60,2310-2312` |
| 待分配宽搜索 | default 输入;宽 `220px`,最大 `42vw` | Element 默认输入态 | `index.vue:61-69,2314-2317` |
| 主搜索 | default 输入宽 `160px` | placeholder“患者姓名 / 手机号” | `index.vue:72-75,2320-2326` |
| 查询 | primary default button;高 `32px`14pxr4 | 白字/主色底 | `index.vue:74`、Element Plus `button.scss:22-43,76-82` |
日期 chip 的实际顺序是:**昨天、前天、当天、明天、后天、全部**,不是按自然日期升序;随后才是 3 个业务 chip。证据:`index.vue:964-976`。页面挂载时直接选中当天并请求列表;各 chip 计数延后到 idle(不支持时延后约 `120ms`)获取,所以首帧可能只有标签、稍后才出现数字。证据:`index.vue:1046-1095,2174-2182`
### 5.2 第二行快捷筛选
- 第二行上方有 `1px solid #EBEEF5`;自身 `padding-top:8px``margin-top:8px`、水平 gap `6px`、垂直居中。证据:`index.vue:78-102,2329-2335`
- “挂号:”“确认:”为 `13px #999`label 后额外 `2px`。每组都是“全部 / 已… / 未…”。证据:`index.vue:79-97,2337-2341`
- 小 chip 最终 padding 是 `.chip-sm``4px 10px`13px、r4、透明底;hover/active 用主色文字与 primary-light-9active 权重 500。证据:`index.vue:2343-2366`
- 两组之间为 `1×14px` 的 lighter 分隔线,左右 margin `4px`。证据:`index.vue:2368-2373`
- “更多筛选”是 primary/link/small 按钮,左 margin `8px`;展开时 ArrowDown 旋转 `180°`,文字变“收起”。源码没有为该旋转指定独立 transition。证据:`index.vue:98-101,2375-2380`
### 5.3 展开筛选区
展开区使用 `v-show`,所以是即时显隐而非折叠动画。它为可换行 flex,gap `10px`,上 margin/padding 各 `8px`,顶部是 `1px dashed #EBEEF5`。证据:`index.vue:104-151,2384-2391`
控件从左到右:
1. 诊断类型 selectsmall`24px` 高);
2. 证型 selectsmall);
3. 医助 selectsmall,可搜索);
4. 最近挂号日期范围(共享 `DaterangePicker``260px` 宽、最大 100%,未传 size,故 default `32px` 高);
5. 最近指派日期范围(同上);
6. 最近挂号渠道(small,可搜索,宽 `170px`);
7. 重置(small button`24px` 高)。
证据:`index.vue:104-150,2393-2405`;共享日期组件只是透传一个 `el-date-picker`,见 `components/daterange-picker/index.vue:1-11,15-31`。因此展开区客观上混有 `24px``32px` 两种控件高度。前三个 select 没有本页固定宽度;Element Select 自身是 `width:100%` 的可收缩 flex item,最终每行宽度依赖可用空间与浏览器 flex 计算,源码不能给出一个稳定像素值。
## 6. 列表卡操作栏
- 列表卡距筛选卡 `12px`body padding 清零。证据:`index.vue:154,2408-2412`、Tailwind spacing `tailwind.config.js:103-113`
- 操作栏为左右两端布局,padding `10px 16px`,底边 `1px #EBEEF5`;未固定高度。default button 高 `32px`,故无换行时栏高至少约 `52px`。证据:`index.vue:155-177,2414-2429`
- 左侧依权限显示:primary“新增诊单”、success“批量指派医助”、warning/plain“批量取消指派”。后两者按选中状态禁用。右侧仅在选中数量大于 0 时显示 `13px #999` 的“已选 n 条”。证据:`index.vue:157-176`
- `.list-actions` 声明 `gap:8px`,但 Element 默认还有 `.el-button + .el-button { margin-left:12px }`,此处没有清零。因此相邻按钮的实际起点间空白通常是 **20px8+12**,而不是设计者表面写下的 8px。证据:`index.vue:2421-2424`、Element Plus `button.scss:72-74`
## 7. 表格规格
### 7.1 外框与表头
- 表格外围 padding `10px 16px 0`;表格宽度 100%,默认 fixed layout、无竖边框;行之间由 `1px #EBEEF5` 底边分隔。证据:`index.vue:178-190,2432-2434`、Element Plus `table.scss:11-20,188-195,217-235`
- 表头底 `#F8F8F8`、13px/600、文字 `#666`。默认 cell 水平 padding `12px`,行高 `23px`,纵向 padding `8px`,所以单行表头的理论最小高度约 **39px**(23+16,不含亚像素/边线差异)。证据:`index.vue:2710-2716`、Element Plus `table.scss:156-195``common/var.scss:1014-1031`
- 表头“未服务天数”有双态排序箭头,顺序只允许 descending → ascending;激活箭头使用主色。证据:`index.vue:297-304`、Element Plus `table.scss:548-552`
### 7.2 列宽与内容
| 次序 | 列 | 声明宽度 | 对齐/固定 | 主要视觉内容 |
|---:|---|---:|---|---|
| 1 | 选择 | `48px` | center | header 全选 + 行 checkbox |
| 2 | ID | `min 70px` | left | ID 为 14px/600;未读指派时附 small/dark/danger `NEW` |
| 3 | 患者 | `min 60px` | left | 患者名 14px/600;头像模板已注释,不显示 |
| 4 | 性别 / 年龄 | `100px` | left | `男 · 42岁`,继承表格 14px |
| 5 | 挂号 | `min 175px` | left | 预约状态 badge、医生、时间、可取消链接、最近渠道;无则“未挂号” |
| 6 | 确认 | `88px` | center | 13px 已确认/未确认文字 |
| 7 | 复诊 | `min 120px` | left | 时间、医生、可选“处方已作废” tag;无则“无” |
| 8 | 助理 | `100px` | left | 13px 次要文字,溢出 tooltip |
| 9 | 开方 | `72px` | center | 13px 已开方/未开方 |
| 10 | 未服务天数 | `110px` | center | 13px/600 tabular number,按天数绿/橙/红;可有健康打卡 tooltip |
| 11 | 视频旁观 | `120px` | center / right fixed | small link 或 12px 次要状态 |
| 12 | 操作 | `340px` | left / right fixed | 5 个 small/link 主动作 + click dropdown |
证据:`index.vue:191-406`。声明宽度/最小宽度合计约 `1403px``min-width` 列在更宽窗口可继续分配余量。右固定区域为 `120+340=460px`Element 会在滚动边缘加 fixed-column 阴影;阴影值来自框架 `common/var.scss:981-1000`
### 7.3 行高与密度
- 本页再次把 body cell 设为 `padding:8px 0`cell 内层保留 Element 的水平 `12px``23px` line-height。纯单行内容的最小行高约 **39px**。证据:`index.vue:2720-2722`、Element Plus `table.scss:188-195`
- 挂号 cell 有 `min-height:36px`、内 padding `4px 8px`、r6,所以含该列的常规数据行通常至少约 **52px**cell 36 + table 上下 16);实际由内容较高者决定。证据:`index.vue:2485-2489`
- 同一诊单多条预约时,每一后续预约额外带 `8px` top margin、`8px` top padding 和 `1px dashed #EBEEF5`,行高会显著增加,不是固定行高。证据:`index.vue:227-247,2490-2494`
- 操作区允许换行;链接数量受权限与记录状态影响,所以操作列也可能增高整行。证据:`index.vue:344-403,2683-2688`
### 7.4 斑马、hover 与业务高亮
| 状态 | 静态 | hover | 判定优先级 |
|---|---|---|---|
| 普通奇数行 | 白 `#FFF` | `#F8F8F8` | 无业务类 |
| 普通偶数行 | `#FAFAFA` | `#F8F8F8` | `stripe` |
| 未确认 | 预期左侧 `3px #E6A23C`;从左到右 warning RGB `.06 → transparent` | warning RGB `.10 → transparent`,直接覆盖 `td` | 第一优先 |
| 已确认但未挂号 | 预期左侧 `3px #909399`info RGB `.04 → transparent` | info RGB `.08 → transparent`,直接覆盖 `td` | 第二优先 |
判定函数先检查“是否确认”,只有已确认才检查“是否挂号”,所以一行不会同时得到两个业务类。证据:`index.vue:1313-1337`。页面声明见 `index.vue:2723-2735`;斑马和 hover 框架规则见 Element Plus `table.scss:560-588,665-668`
实现时必须区分“源码意图”和“浏览器可见结果”:
- 静态业务渐变在 `tr`,而偶数斑马行和 fixed cell 的背景落在 `td`;不透明 `td` 会遮住下面的 `tr` 渐变。
- 业务 hover 选择器直接命中 `td` 且带 `!important`,因此 hover 更可靠。
- `border-left` 施加在 `tr`,在 `border-collapse:separate` 的 table 中具体像素绘制还受浏览器表格格式化上下文影响。
PySide 复刻应按“意图”直接给每个可见 cell/row delegate 绘背景和 3px 左条,避免复现 CSS 遮挡缺陷;若目标是截图逐像素一致,则需先用指定 Chromium、固定数据测一张运行态截图。
### 7.5 单元格内部视觉
#### 患者与 ID
- `.patient-name` 为 14px/600、`#333`、line-height `1.3`。ID 与患者列都复用该样式。证据:`index.vue:194-212,2461-2470`
- 32×32、r8、主色渐变的头像 CSS 存在,但患者模板已注释,不参与实际页面。证据:`index.vue:207,2447-2459`
- `NEW` 是 small (`20px` 高)、danger、dark tag:白字、danger 实色底;字体 12px、r4。证据:`index.vue:198`、Element Plus `tag.scss:66-87,142-165``common/var.scss:1110-1138`
#### 挂号块
- 每个预约 badge11px/600、padding `2px 6px`、r4、下 margin `2px`;医生 14px/600 `#333`;时间 12px、上 margin `2px`。证据:`index.vue:2496-2519`
- 活跃预约 badge 为 `#3A4ACC` / `rgba(74,93,255,.15)`,时间硬编码 `#4A5DFF`;整块为 135° 蓝色渐变 `.12 → .06`,边 `rgba(74,93,255,.3)`。证据:`index.vue:2522-2531,2556-2559`
- 已完成预约用 info-dark-2 / info-light-7;爽约用 warning-dark-2 / warning-light-7。整块也分别换成 info 或 warning 透明渐变。证据:`index.vue:2533-2569`
- 最近渠道为上 margin `6px`、12px、line-height `1.35``#999`;未挂号为 13px `#A8ABB2`。证据:`index.vue:2571-2581`
#### 语义文字与 tag
- 已确认:13px/500 success;未确认:13px/600 warning。证据:`index.vue:2584-2594`
- 已开方:13px/500、硬编码 `#4A5DFF`;未开方:13px `#A8ABB2`。证据:`index.vue:2596-2605`
- 复诊时间 13px/500 `#333`,医生 12px `#999`,纵向 gap `4px`、line-height `1.35`;“处方已作废”是 small/plain/info tag。证据:`index.vue:268-285,2651-2676`
- 未服务天数:最小 `28px`、13px/600、等宽数字;空值灰 `#A8ABB2`/400`<=2` 绿 `#16A34A``3-6``#EA580C``>=7``#DC2626`。证据:`index.vue:305-316,1319-1329,2607-2632`
- 视频旁观为 12px、line-height `1.4`,左右 padding `2px`;不可进入时是 `#999`。证据:`index.vue:318-341,2690-2704`
## 8. 按钮、dropdown、tooltip
### 8.1 操作列链接
直接动作依次为:查看(primary)、诊单(primary)、开方/查看处方(primary)、预约(success)、补全身份证(warning),最后是 info“更多”。权限会移除部分按钮。证据:`index.vue:345-403`
- 均为 `size="small"``link`12px,透明边/底,padding `2px`,高度 autohover 使用各语义色的 light-3。证据:Element Plus `button.scss:223-248``element.scss:113-127`
- `.action-cell` 设 flex-wrap + `gap:4px`;但相邻 `.el-button + .el-button` 仍有 `12px` 左 margin,因此连续按钮间通常是 **16px4+12**。dropdown 外壳与前一按钮之间只有 flex gap `4px`。证据:`index.vue:2683-2688`、Element Plus `button.scss:72-74`
- 340px 操作列不足时允许链接换行;没有强制单行或省略号。
### 8.2 “更多”菜单
- click 触发、`bottom-end` 对齐。菜单项按权限/状态可包含:指派、取消指派、视频二维码、二维码、取消挂号、挂号日志、创建订单、删除。证据:`index.vue:379-403`
- Element 默认 dropdown item 为 14px、line-height `22px`、padding `5px 16px`,故普通项约 `32px` 高;菜单上下净 padding 约 `5px`r4、overlay 白底,popper 有 light border 与 shadow。hover 为 fill-light + primary。证据:Element Plus `dropdown.scss:7-24,42-70,137-176`
- 删除项有 divided 顶线,文字由 `.text-danger` 强制为 danger。证据:`index.vue:400,2706-2708`
- tooltip 使用 Element 默认 popper12px、line-height `20px`、padding `5px 11px`;实际位置由视口碰撞计算,不能从源码固定。
## 9. 分页、空态与加载态
### 9.1 分页
- 列表默认 `page=1, size=15`;共享组件页容量选项为 `[15,20,30,40]`。证据:`src/hooks/usePaging.ts:14-34``components/pagination/index.vue:18-28`
- 布局固定为 `total, sizes, prev, pager, next, jumper`,最多显示 5 个 pager,单页也不隐藏。证据:`components/pagination/index.vue:3-14,24-28`
- 容器右对齐,padding `10px 16px 16px`。默认分页按钮 `32×32px`(最小宽),14pxr2,项组间 gap token `16px`active 仅主色文字 + bold,并未启用 background 模式。证据:`index.vue:408-410,2436-2440`、Element Plus `common/var.scss:1034-1057``pagination.scss:5-67`
- page-size select 框架宽 `128px`jumper 输入 `56px`。证据:Element Plus `pagination.scss:73-75,118-150`
- 视口 `<=768px` 时,全局 CSS 隐藏 size selector 与 jumper,仍保留 total、prev、pager、next。证据:`src/styles/element.scss:177-184`
### 9.2 空态
页面没有提供 table `#empty` slot,也没有在主列表使用 `el-empty`。因此空列表为:
- sticky empty blockmin-height `60px`
- 居中纯文本“暂无数据”;
- 文字 `#999`line-height `60px`,宽 50%
- 表头、底线和分页仍存在。
证据:`index.vue:179-190`、Element Plus `table.scss:52-70``src/App.vue:5,25-28`、Element 中文 locale `zh-cn.mjs:137`
### 9.3 加载
- `v-loading="pager.loading"` 直接挂表格;普通请求开始时 loading=true,完成后 false。证据:`index.vue:179-184``src/hooks/usePaging.ts:35-72`
- 蒙层绝对覆盖 tablez-index 2000,亮色为 `rgba(255,255,255,.5)`;中央环 `42px`,主色描边 `2px`,2 秒旋转、1.5 秒 dash 动画。证据:`src/styles/var.css:41-43`、Element Plus `loading.scss:19-73,85-103``common/var.scss:1340-1349`
- 页面每 20 秒静默刷新列表和计数;标签页不可见时跳过,静默刷新不打开 loading,因此不会周期性白闪。证据:`index.vue:1098-1105,2182-2195``src/hooks/usePaging.ts:36-42,64-71`
- 首次 `pager.loading` 初值是 false,但 mount 后立即调用 `getLists()`,正常网络下会很快进入蒙层;能否肉眼看到取决于响应时长。
## 10. 响应式与滚动行为
1. `index.vue` 的 scoped 样式**没有 media query**。页面级响应式主要来自 flex-wrap、宽度上限、Element Table 横向滚动及全局分页规则。
2. body 被全局设为 `min-width:375px``overflow:hidden`;应用主内容通过 `el-scrollbar` 承担页面滚动。证据:`src/styles/public.scss:1-3``layout/default/components/main.vue:2-11`
3. 第一筛选行本身不换行。左侧日期 chip 可换行,但右侧 160px 搜索 + 32px 查询按钮仍与左侧同一 flex 行;在极窄内容宽度下可能压缩、增高左侧或溢出,源码没有把搜索行堆到下一行的断点。
4. 待分配容器可换行,宽搜索最多 `42vw`;展开筛选区可换行,两个日期范围各为 `260px``max-width:100%`。证据:`index.vue:2298-2317,2384-2405`
5. 表格 min 总宽约 1403px。Element Table 内部提供横向 scrollbar;右侧 460px fixed 区保持可见,导致很窄时可见的左侧业务列空间进一步减少。
6. 外层应用在窄屏会折叠侧栏,但那是壳层行为,不改变本页筛选/表格 DOM。证据:`src/App.vue:36-53`
7. dark mode 有完整全局变量,但页面的硬编码蓝/绿/橙/红不会自适应;其亮度对比应另做暗色运行态验收。
## 11. 直接挂载的覆盖层(不占主页面静态布局)
主列表下方常驻挂载多个异步组件或 dialog/drawer;关闭时不占主页面空间,操作后覆盖在页面之上:
| 入口 | 覆盖层 | 关键尺寸 | 证据 |
|---|---|---:|---|
| 新增/诊单 | `edit.vue` 诊单 drawer | `60%` | `index.vue:414``edit.vue:1-10` |
| 详情组件 | `detail.vue` popup | `800px` | `index.vue:415``detail.vue:1-10` |
| 开方/处方 | `tcm-prescription` drawer | `1200px` | `index.vue:416``components/tcm-prescription/index.vue:1-8` |
| 预约 | `appointment.vue` drawer | `60%` | `index.vue:417``appointment.vue:1-10` |
| 视频旁观 | AssistantWatchCall dialog | `760px`;视频 grid min `220px` | `index.vue:418-422``AssistantWatchCallDialog.vue:1-19,242-284` |
| 挂号日志 | drawer | `460px` | `index.vue:424-447` |
| 小程序二维码 | dialog | `400px`;二维码 `256×256` | `index.vue:449-481` |
| 指派医助 | dialog | `500px` | `index.vue:483-531` |
| 创建订单 | dialog | `600px` | `index.vue:533-587` |
| 补全身份证 | dialog | `400px` | `index.vue:589-615` |
| 企微记录 | drawer | `600px` | `index.vue:617-722` |
这些覆盖层说明“操作按钮点击后”的页面层级,但其完整内部视觉不应混入诊单 index 主页面的静态复刻范围。
## 12. PySide 映射建议
### 12.1 页面与筛选
| Vue/CSS 意图 | PySide 建议 | 复刻要点 |
|---|---|---|
| 外层 `el-scrollbar` + 两张 card | `QScrollArea` → 单一 `QWidget/QVBoxLayout`;卡用 `QFrame` | viewport `#F6F6F6`;外 margin `16V/8H`;卡 r4、白底、无 border/shadow;卡间 12 |
| date chips | 自定义 `FlowLayout` + checkable `QToolButton` | padding 6×12、r6、gap6;计数拼接为同一 label,保持源码顺序 |
| 业务色 chip | 同一 button class 加 dynamic property | `pending_booking/info``completed/success``pending/warning`active 白字实底 |
| 第一行搜索 | `QHBoxLayout` 右侧固定 `QLineEdit(160)` + `QPushButton` | 高 32、gap8;若追求源码一致,不自行添加窄屏 stack |
| 第二行筛选 | `QHBoxLayout`/FlowLayout + `QToolButton` | top line、上下 813pxactive primary-light-9 |
| 更多筛选 | 可显隐 `QWidget` + FlowLayout | 无动画;top dashed bordergap10small 控件 24,日期范围 32 |
Qt 没有内建通用 FlowLayout,建议采用 Qt 示例式 `FlowLayout`,并把 chip 的 `sizeHint()` 包含字体宽度、左右 12/10px padding;不要用固定总宽,否则计数从一位变三位时会截断。
### 12.2 表格
- 使用 `QTableView + QAbstractTableModel`,不要堆 15 行 QWidget。列的 initial/min widths 按本规格表设置;表头高建议从 `39px` 起,常规 row 从 `52px` 起,并由 delegate `sizeHint()` 为多预约/操作换行增高。
- 选择列用 check-state delegateID/患者、确认、复诊、开方、未服务天数分别用专用 delegate 绘文字和 tag,避免嵌套控件带来的滚动性能问题。
- 挂号列用一个复合 delegater6 的状态背景、11px badge、14px/600 医生、12px 时间、多预约虚线分隔。可点击“取消挂号”若必须嵌入,可在命中区域处理 mouse event,而不是每格常驻 QPushButton。
- 两个右固定列可用**同步滚动的双 `QTableView`**:主表隐藏最后两列,右表只显示 120/340 两列,共宽 460;共享 model、selection model、vertical scrollbar 与 row height。单一 QTableView 无 Element 的 sticky fixed-column 等价物。
- 行背景应在 delegate 对每个 cell 绘制:普通白/`#FAFAFA`hover `#F8F8F8`;业务行覆盖渐变并在第一可见 cell 绘 3px 语义条。不要把渐变只画在 viewport 底层,否则会重现 HTML `td` 遮挡问题。
- 操作列优先使用 delegate + 命中区域或常驻 `QWidget` 行容器;若用按钮,链接样式为 12px、2px padding、无背景/边框,并预留源码实际的较大按钮间距。
- 横向滚动时保持右表固定;表头与 body 必须共用列宽。多预约行要同步两张表的 row height。
### 12.3 dropdown、分页、空态、加载
- “更多”使用 `QMenu`item 高约 32、左右 padding16、14px;按权限/状态动态构建,删除项前 `addSeparator()` 且 danger 文本。
- 分页建议自定义 `PaginationBar`total、15/20/30/40 combo、prev、最多 5 页、next、jump;高度32,容器右对齐并保留 `10T/16H/16B`。窗口内容宽度 <=768 时隐藏 combo 与 jump。
- 空态在 `QTableView.viewport()` 中央放一个透明 `QLabel("暂无数据")`,有效高度 60;不要用大插画。仍显示 header 和分页。
- 加载态用覆盖 table rect 的半透明 widget:亮色 `rgba(255,255,255,.5)`,中央约 42px 主色 spinner。新请求显示;20 秒静默刷新不显示。
- 用 generation/request id 丢弃过期异步响应,并只在当前请求结束时关闭 overlay;这不是 Vue 当前 `latestOnly:false` 的视觉保证,但能避免 PySide 快速筛选时旧响应造成闪回。
### 12.4 主题与字体
- 定义集中 token,而不是把 `#4A5DFF` 散落在 QSS;但为了忠实,挂号块与“已开方”目前应保留硬编码蓝,或在设计确认后统一为 theme primary。
- Windows 上首选“Microsoft YaHei UI”还是源码的“Microsoft YaHei”会改变字宽;逐像素复刻应显式用 `Microsoft YaHei`14px CSS 大致映射为 Qt pixelSize 14,而不是 pointSize 14。
- Qt 的 devicePixelRatio 与 CSS px 不同。截图验收需固定 Windows 缩放(建议 100%)、字体、窗口内容宽度和主题。
## 13. 能从源码确定 / 不能从源码确定
### 可以确定
- DOM 顺序、无页面内标题、两张卡、每个筛选和操作入口;
- 页面局部 padding/gap/宽度、列声明宽度、固定列、颜色、字体大小/字重;
- 日期 chip 的顺序与当天默认态;
- 表格 stripe、业务类判定优先级、语义文本与预约块状态;
- 共享分页的页容量、布局、空态文案、loading 结构;
- 默认亮/暗 token 及运行时主题派生方式;
- 权限或数据条件控制哪些按钮/tag 出现。
### 仅运行态可最终确定
1. **最终换行和行高**:取决于 viewport、数据长度、按钮权限、预约条数、字体回退、DPR;chip 和多数 row 都没有固定 height。
2. **实际主题色**:setting 存在本地缓存,账号可改主色及语义色;dark 状态也来自浏览器存储/系统偏好。
3. **业务行静态渐变是否完整可见**:受 `tr` 背景、striped/fixed `td` 背景与 Chromium table paint 顺序影响;源码只能确定规则叠加关系。
4. **fixed-column 阴影和 scrollbar 尺寸**:由 Chromium/Element 的滚动状态、平台 scrollbar 策略决定。
5. **tooltip/dropdown 的最终坐标**popper 会做视口碰撞与翻转。
6. **加载态停留时间和计数出现时间**:由 API 延迟、idle callback 调度决定;静默轮询通常无蒙层。
7. **应用壳标题/面包屑/多页签**:由路由菜单、setting 和权限生成,不是 `index.vue` 的页面内视觉。
若后续要求“像素级截图一致”,应在固定 `Element Plus 2.13.7 + Chromium 版本 + 100% Windows 缩放 + 默认亮色主题 + 固定权限/样本数据` 下补一轮运行态测量;本文件已经给出源码能够保证的全部静态约束与明确边界。
+153
View File
@@ -0,0 +1,153 @@
# 诊单视觉重构最终业务回归审计(三次终验)
- 终验日期:2026-08-10
- 范围:生产视频/IM、医生备注与日常记录 mutation、处方/订单、多挂号精确取消、诊单 Shell owner、列表 QR/日志/订单;并复核上一版已关闭的权限、DTO、隐私与 generation 项。
- 基准:当前最新 Python 源码与测试,以及 `D:/web/zyt/admin``D:/web/zyt/server` 的实际 admin 行为和服务端控制器/Logic。
- 约束:只读业务终验;未安装依赖、未打包、未访问线上接口。除更新本报告外,未修改业务源码或测试。
## 最终结论
第三次终验后的剩余项为:
- **P00,明确无 P0。**
- **P11 项 OPEN。** 诊单列表“创建订单”已调用真实 `order.order/create`,但没有消费返回的 `order_no` 并调用现成的 `generate_order_qrcode`,admin 的付款二维码闭环仍中断。
- **P22 项 OPEN。** 一是日常矩阵不能编辑已有血糖/饮食/运动记录;二是视频回放只显示每条记录的第一个 URL,且没有 admin 的“向具体通话记录追加回放”入口。
- **服务端不可实现限制:不计入 P0/P1/P2。** 医生备注与跟踪备注的服务端契约只支持按当天追加;服务端没有任意旧正文覆盖或整条删除 endpoint,桌面端无法在不扩展后端的前提下实现这些动作。
- 其余本轮复核项均 **CLOSED**:生产视频/IM 取数与同步、备注追加/附件、日常新增与待办、处方动作、订单详情/偏移、精确挂号取消、列表视频/确认 QR 与日志、真实 Shell 60%/move-resize/close generation。
- 定向离线回归:**128/128 passed**。
| 复核项 | 三次终验状态 | 最终等级 | 摘要 |
|---|---|---:|---|
| 生产视频/IM `only_archived`、sync、上传、回放 | **OPEN(主体 CLOSED** | **P2** | 生产接口、同步、工具栏上传和首个回放可用;多 URL 与指定 call-record 追加入口缺失 |
| 医生备注 mutation | **CLOSED** | — | 追加正文、上传舌象/报告、删除单附件均走真实合同;正文覆盖/整条删除属后端无 endpoint 限制 |
| 日常记录 mutation | **OPEN(新增 CLOSED** | **P2** | 新增血糖/饮食/运动/跟踪备注及待办新增/取消可用;已有三类记录不能编辑 |
| 处方/详情订单动作 | **CLOSED** | — | 开方、处方查看、业务订单列表/详情与诊次偏移均真实落库/取数 |
| 多挂号精确取消 | **CLOSED** | — | 每个挂号卡携带精确 ID,最终调用 `doctor.appointment/cancel` |
| 诊单 owner、60%、move-resize、generation | **CLOSED** | — | 每次打开重绑真实 Shell,宽度为 Shell 60%,移动/缩放/关闭同步且所有异步代次失效 |
| 列表 QR / 日志 / 创建订单 | **OPENQR/日志/创建主体 CLOSED** | **P1** | 视频/确认 QR、日志、创建订单均真实调用;创建成功后的支付 QR 未接入 UI |
上一版已关闭的 `phonePlain`、canonical `diagnosis_type``viewOnly` endpoint 分流、detail 失败禁存、身份证 15/18 位校验、权限 Tab 裁剪与隐私脱敏,本轮定向回归均未发现反弹。
## 逐项证据
### 1. 生产视频/IM`[OPEN / P2,主体 CLOSED]`
已关闭部分:
- `RemoteDoctorRepository` 已实现生产 `getCallRecords`、手工 call record、附件关联及“上传 → 创建记录 → 关联”顺序:`src/doctor_workstation/services/repository.py:1875-1943`。这与 admin `CallRecordPanel` 的真实流程一致:`D:/web/zyt/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue:109-180`
- IM 首次读取明确发 `only_archived=1`,同步走 `triggerImChatSync``src/doctor_workstation/services/repository.py:1945-1965`dialog 的 lazy 查询也固定 `only_archived=True``src/doctor_workstation/ui/dialogs/diagnosis.py:2016-2038`。admin 对照为 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/ImChatRecordPanel.vue:148-185`
- edit 态视频工具栏调用真实上传,成功后按 media generation 重新加载;聊天同步成功后延时重新读取归档:`src/doctor_workstation/ui/dialogs/diagnosis.py:2541-2611`。聊天图片/文件只能通过 HTTP(S) 安全打开:`:2652-2658`
- production/server 合同存在:`D:/web/zyt/server/app/adminapi/controller/tcm/DiagnosisController.php:397-438``:505-538`;归档快路径在 `D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisLogic.php:1015-1045`
仍 OPEN 的 P2
- 服务端返回 `recording_urls_list` 数组:`D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisLogic.php:1810-1833`;admin 把完整数组交给播放器,并在每个具体 call row 提供上传入口:`D:/web/zyt/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue:30-72`
- Python `_fill_video` 只取 `raw_urls[0]`,其余回放不可见,且视频表没有指定 `call_record_id` 的上传操作列:`src/doctor_workstation/ui/dialogs/diagnosis.py:1126-1135``:2844-2881`。虽然 repository 已支持 `call_record_id``src/doctor_workstation/services/repository.py:1894-1913`),UI 没有把它接出来。
该问题不造成错误写入,且工具栏上传与一个回放可用,故按 **P2** 而非 P1 计。
### 2. 医生备注:`[CLOSED;后端限制已明确]`
- NotesTimeline 已暴露新增、上传、删除单附件与打开附件信号:`src/doctor_workstation/ui/diagnosis_drawer.py:1804-1810``:1820-1851``:1887-1961`dialog 绑定真实方法:`src/doctor_workstation/ui/dialogs/diagnosis.py:989-997``:2376-2487`
- production DTO 分别走 `doctor.appointment/addDoctorNote``doctor.appointment/doctorNotes``doctor.appointment/deleteDoctorNoteImage``src/doctor_workstation/services/repository.py:935-985`。权限分别按新增与删除附件 endpoint 裁剪:`src/doctor_workstation/ui/dialogs/diagnosis.py:478-483``:1233-1241`
- admin 自身也只有追加、上传、删除单附件动作:`D:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:195-268`API 定义仅三条:`D:/web/zyt/admin/src/api/patient.ts:19-40`
服务端不可实现边界:
- controller 仅暴露 `addDoctorNote``doctorNotes``deleteDoctorNoteImage`,随后类即结束:`D:/web/zyt/server/app/adminapi/controller/doctor/AppointmentController.php:168-198`
- `DoctorNoteLogic::addOrAppend` 对当天正文使用换行追加,不支持任意旧正文覆盖:`D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:12-76`;唯一删除逻辑只删除附件数组中的单项:`:111-136`
因此“任意旧笔记正文覆盖/整条删除”不是桌面端漏接已有合同,而是服务端没有 endpoint;本报告将其列为**后端限制,不计客户端 P0/P1/P2**。
### 3. 日常记录:`[OPEN / P2,新增 CLOSED]`
已关闭部分:
- edit 态已提供血糖、饮食、运动、跟踪备注新增按钮和待办新增/取消;只读态/无权限态隐藏:`src/doctor_workstation/ui/diagnosis_drawer.py:1363-1371``:1431-1450``:1481-1511``:1760-1791`
- dialog 将 signal 接到真实 add/todo repository,并为每次 mutation 使用 diagnosis-id + generation 校验后强制刷新:`src/doctor_workstation/ui/dialogs/diagnosis.py:1089-1101``:2175-2358`
- production 合同为 `tcm.bloodRecord/add``tcm.dietRecord/add``tcm.exerciseRecord/add``tcm.diagnosis/addTrackingNote`、todo add/cancel`src/doctor_workstation/services/repository.py:1745-1765``:1778-1805``:1847-1873`
仍 OPEN 的 P2
- admin 点击已有单元格时会把记录(含 `id`)放入表单,保存时分别调用 blood/diet/exercise edit`D:/web/zyt/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:921-1013`
- Python matrix 明确 `NoEditTriggers``NoSelection`,只有 `daily_add_requested`,没有已有记录 edit signal`src/doctor_workstation/ui/diagnosis_drawer.py:1366-1371``:1455-1463`。dialog 也只选择 `add_*``src/doctor_workstation/ui/dialogs/diagnosis.py:2260-2284`
- 这不是后端限制:production repository 已实现三类 edit`src/doctor_workstation/services/repository.py:1767-1776``:1787-1796``:1807-1818`
跟踪备注例外:server 设计就是“仅给今天追加”。controller 仅有 add/list`D:/web/zyt/server/app/adminapi/controller/tcm/DiagnosisController.php:180-214``TrackingNoteLogic` 只有 `addOrAppend/getByDiagnosis`,类在第 95 行结束:`D:/web/zyt/server/app/adminapi/logic/tcm/TrackingNoteLogic.php:8-95`。其任意旧正文覆盖/整条删除同样属于后端无 endpoint 限制,不计客户端问题。
### 4. 处方与详情订单动作:`[CLOSED]`
- “开具处方”打开真实 `PrescriptionEditorDialog`accepted 后调用 `create_prescription`,并携带 diagnosis/appointment/case snapshot`src/doctor_workstation/ui/dialogs/diagnosis.py:2489-2539`
- 历史处方的“查看”打开真实 `PrescriptionDetailDialog``:2791-2842`
- 业务订单 lazy 列表、详情权限/真实 repository 门禁、异步 order generation 及详情 dialog 均已接通:`:1998-2000``:2069-2074``:2883-2991`
- 全局诊次偏移调用真实 `setRevisitSlotStartOffset`,并有独立 generation`:2613-2650`
未发现处方或详情订单的假动作/越权入口,本项 **CLOSED**
### 5. 多挂号精确取消:`[CLOSED]`
- 每个可取消挂号卡按精确 primary key 生成按钮,signal 携带原 record 与该 appointment-id`src/doctor_workstation/ui/diagnosis_index_widgets.py:150-163``:1121-1148`。歧义的行级“取消挂号”仍只在唯一挂号时出现:`:1403-1410`
- handler 选择当前诊单后再次按 ID 和状态校验,确认前后都防 stale,最终仅调用 exact capability`src/doctor_workstation/ui/pages/consultations.py:1868-1921`
- production 方法明确走 `doctor.appointment/cancel`,不再误用患者工作区 endpoint:`src/doctor_workstation/services/repository.py:1545-1555`
- 新测试覆盖两个挂号、超 32 位 appointment-id、错误诊单、状态刷新、权限/能力缺失与 canonical endpoint`tests/test_diagnosis_index_visual.py:340-379``tests/test_consultations_parity_ui.py:610-774``tests/test_diagnosis_detail_contract.py:90-105`
本项 **CLOSED**
### 6. 真实 Shell owner / 60% / move-resize / generation`[CLOSED]`
- `_owner` 不再在 page 构造期缓存;每次 `open_for/showEvent` 都从当前 `parent.window()` 重绑真实 Shell`src/doctor_workstation/ui/dialogs/diagnosis.py:526-527``:1314-1349``:1398-1408`
- overlay 与 owner 同位置/尺寸,宽屏 drawer 固定为 owner 宽度 60%;监听 owner 的 Resize/Move/Show/Close`:1283-1299``:1351-1371`
- `reject/done` 及 owner Close 会使 detail/save/orders/order-detail/daily/notes/media/offset/所有 lazy-tab generation 全部失效:`:1373-1383``:1681-1700`
真实 `ShellWindow -> QStackedWidget -> ConsultationsPage -> cached DiagnosisDialog` offscreen 探针结果:
```text
OPEN owner_shell=True shell=(1200,760) dialog=(1200,760) panel=720 expected=720 pos=(80,60)
MOVED shell=(1024,650) dialog=(1024,650) panel=614 expected=614 pos=(190,140)
CLOSE visible=False generation=True media=True
```
本项 **CLOSED**。现有 `tests/test_diagnosis_drawer_visual.py:837-861` 仍只用直接顶层 owner;本次额外真实 Shell 探针补足了业务终验,但建议后续把该探针固化成回归测试。
### 7. 列表 QR / 日志 / 订单:`[OPEN / P1,主体 CLOSED]`
已关闭部分:
- 页面只在 canonical 权限与真实 repository 方法同时存在时展示视频 QR、确认 QR、日志、订单入口:`src/doctor_workstation/ui/pages/consultations.py:704-715``:1005-1030`
- QR DTO 校验 patient/doctor/share-user/diagnosis,日志使用 diagnosis-id,所有菜单请求均受 selection generation 保护:`:1972-2091`
- production repository 真实调用 `generateMiniProgramQrcode``guahaoLogList``order.order/create``src/doctor_workstation/services/repository.py:1967-2060`。测试验证 exact DTO、权限裁剪与 stale callback`tests/test_diagnosis_detail_contract.py:85-127``tests/test_consultations_parity_ui.py:777-958`
仍 OPEN 的 P1
- admin 创建订单成功后读取 `order_no`,立即调用 `generateOrderQrcode` 并展示付款 QR`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:1474-1509`
- Python `_create_diagnosis_order` 把真实 create 放进通用 `_run_mutation`;该 helper 丢弃 `_result`,只 toast + refresh,因此永远不会生成付款 QR:`src/doctor_workstation/ui/pages/consultations.py:2093-2128``:2460-2479`
- repository 明明已有 `generate_order_qrcode``src/doctor_workstation/services/repository.py:2062-2076`,但 UI 源码没有任何调用。现有测试也只分别测试“UI 创建订单”和“repository 可生成 QR”,未覆盖两者串联:`tests/test_consultations_parity_ui.py:777-872``tests/test_diagnosis_detail_contract.py:88-89`
订单已实际创建却不展示付款入口,用户容易停在半完成状态或重复建单;这是主流程闭环缺失,按 **P1 OPEN** 计。
## 离线验证记录
使用项目现有 `.venv`,设置 `QT_QPA_PLATFORM=offscreen``PYTHONDONTWRITEBYTECODE=1`,禁用 pytest cache,并把 basetemp 放在系统临时目录;未安装任何依赖:
```text
pytest -q -p no:cacheprovider \
tests/test_diagnosis_detail_contract.py \
tests/test_diagnosis_index_visual.py \
tests/test_diagnosis_drawer_visual.py \
tests/test_appointment_drawer_visual.py \
tests/test_consultations_parity_ui.py \
tests/test_prescription_security_ui.py \
tests/test_patients_ui.py \
tests/test_repository_parity.py \
tests/test_permissions.py
128 passed
```
测试退出码为 0`--collect-only -q` 分文件计数合计 128。另执行了上述真实 Shell owner 探针,退出码为 0。
## 发布判断
当前不存在错误 endpoint、歧义挂号取消、权限泄漏、隐私明文回流或关闭后旧异步回调落地等 P0 风险。生产视频/IM、备注追加、日常新增、处方、订单详情及列表主要动作均已从“假动作/未接入”进展为真实 repository 行为。
但若发布标准是“完整保留 admin 的订单支付闭环与所有已存在的数据编辑/回放能力”,当前仍不满足:**剩余 P0 0 / P1 1 / P2 2**。其中任意旧医生/跟踪笔记正文覆盖或整条删除必须先扩展 server endpoint,不能作为桌面端单独修复项。
+113
View File
@@ -0,0 +1,113 @@
# 诊疗工作台第三次、最终只读视觉同型审计
审计日期:2026-08-10
审计性质:最终只读终验;除更新本报告外,未修改任何业务代码、测试、renderer 或 PNG。
事实源:当前 Python 源码、D:/web/zyt/admin/src/views/tcm/diagnosis/ 下 Vue 基准、三份既有视觉规格,以及 artifacts/diagnosis_visual/ 当前 43 张 PNG。
判定方法:43 张 PNG 均按原始尺寸逐张目检;源码只用于解释可见结果与确认真实能力。测试通过数不作为任何视觉结论的依据,本轮也未以测试替代目检。
## 最终结论
**PARTIAL,不是 PASS。**
- P0**0 OPEN**。语义表单、1024 栅格、右固定列、双尺寸 60% 抽屉、固定 footer、核心 loading/empty/error/focus/permission 状态均未回退。
- 上一版 12 项 P1**8 项 CLOSED / EXACT4 项仍 OPEN / PARTIAL**。
- 43 张当前 PNG**36 张 EXACT(其中两张 edit 仅指入镜区域)、7 张 PARTIAL、0 张乱码或不可读**。
- 列表、详情、预约与 Shell 的所有 loading 图均为真实圆弧 spinner;没有字符型 spinner、方框箭头、中文乱码或缺字。
- 当前不能判 PASS 的原因是:1024 Daily 动作行裁切及按钮语义态被压平;完整菜单仍缺 Vue 图标、danger 删除色和精确文案;备注附件视觉仍未命中预期样式;处方/订单/视频/聊天仍有可见简化或缺少关键截图证据。
- “既有笔记整条正文覆盖 / 删除整条笔记”另列为服务端合同限制,不计作伪造桌面能力,也不用于掩盖上述视觉 P1。
## 上一版 12 项 OPEN 逐项终验
| # | 上一版问题 | 状态 | 同型分类 | 最新精确证据与目检结论 |
|---:|---|---|---|---|
| 1 | 统计端就诊卡仍非 switch | **CLOSED** | **EXACT(组件)** | 当前已使用自绘二态 DiagnosisSwitch44×24 轨道、圆形滑块、0/1 文本合同:src/doctor_workstation/ui/diagnosis_drawer.py:643-675;表单 show_card 分支:src/doctor_workstation/ui/dialogs/diagnosis.py:924-925Vue 基准:D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:341-348。现有 edit PNG 没有滚到该字段,因此本项是源码级确定结论,不引用测试,也不声称有单独的 switch 截图。 |
| 2 | Daily 编辑动作不完整 | **OPEN** | **PARTIAL** | + 血糖 / + 饮食 / + 运动 / + 备注、刷新、新增待办和精确可取消动作均已接入:src/doctor_workstation/ui/diagnosis_drawer.py:1431-1450、:1486-1512、:1764-1784;真实处理:src/doctor_workstation/ui/dialogs/diagnosis.py:2175-2356。可是 diagnosis_state_daily_1024x640.png 中最右“刷新”被抽屉右缘裁掉;原因是单个 QHBoxLayout 不换行(diagnosis_drawer.py:1398-1451),而 Vue 明确 flex-wrapD:/web/zyt/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:1094-1109)。同时“最近7天”的 checked 态、+血糖 与新增待办的 primary 态均被 scoped 基础按钮规则压平;Vue 的主动作在 DailyMatrix.vue:25-35、DiagnosisTodoList.vue:3-19。 |
| 3 | “更多”菜单动作被关闭 | **OPEN** | **PARTIAL** | 能力缺口已补齐:权限/真实方法策略在 src/doctor_workstation/ui/pages/consultations.py:1002-1031handler 在 :1672-1692diagnosis_full_menu_1280x800.png 已出现指派、取消指派、视频二维码、确认二维码、取消挂号、挂号日志、创建订单、删除。仍不同型:Python 只创建纯文字 QActionsrc/doctor_workstation/ui/diagnosis_index_widgets.py:1381-1431),Vue 每项有图标,删除为 divided + danger 红,并使用“二维码”而不是“确认二维码”(D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:379-400)。QMenu 白底、边框、hover 与分隔线样式已生效(diagnosis_index_widgets.py:1371-1380),但未覆盖这些剩余语义。 |
| 4 | Tab 溢出仍用左右箭头 | **CLOSED** | **EXACT** | 原生左右工具按钮宽度归零,额外 4 px 横向滚动条可见并驱动选中 Tabsrc/doctor_workstation/ui/diagnosis_drawer.py:194-242、:749-792Vue 基准:D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1798-1828。所有 1024 详情状态图均无盒状左右箭头,蓝色细滚动条连续可见。 |
| 5 | footer 缺上投影 | **CLOSED** | **EXACT** | footer 使用 blur 18、offset 0/-4、半透明深色 QGraphicsDropShadowEffectsrc/doctor_workstation/ui/dialogs/diagnosis.py:784-803Vue 为 0 -4px 24px / 7%D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1792-1796。edit/viewOnly/daily/save 状态图均可见 footer 上缘阴影,且 footer 始终在滚动体外。 |
| 6 | 保存中无按钮 spinner / 结果态 | **CLOSED** | **EXACT** | SaveStateButton 以 QTimer + QPainter 画 loading 圆弧、成功勾和失败叉:src/doctor_workstation/ui/diagnosis_drawer.py:678-746;只在 editable/basic 显示:src/doctor_workstation/ui/dialogs/diagnosis.py:1652-1661;真实保存状态切换::3044-3163。diagnosis_state_save_loading/success/failure_1024x640.png 分别完整显示“正在保存 / 保存成功 / 保存失败”,无缺字。 |
| 7 | readonly 摘要拆“客服” | **CLOSED** | **EXACT** | 摘要值禁止换行并使用 MinimumExpandingsrc/doctor_workstation/ui/dialogs/diagnosis.py:681-712;“客服:…”赋值::1767-1770。diagnosis_readonly_1024x640.png 与 diagnosis_readonly_1440x900.png 均完整显示“客服:赵医助”,不再拆字。 |
| 8 | Notes 缺新增、上传、删除动作 | **OPEN** | **PARTIAL** | 动作和真实端点已存在:NotesTimeline 信号/工具栏/单附件删除在 src/doctor_workstation/ui/diagnosis_drawer.py:1804-1962;处理在 src/doctor_workstation/ui/dialogs/diagnosis.py:2376-2478;远端合同在 src/doctor_workstation/services/repository.py:935-985。diagnosis_state_notes_actions_1024x640.png 已显示新增/追加、上传舌象、上传报告、打开与单附件删除。但视觉仍不同型:QSS 写的是 QLabel#DiagnosisTongueThumb 与 QLabel#DiagnosisAttachmentChipdiagnosis_drawer.py:486-500),运行时却创建 QPushButton:1897-1900、:1932-1937),所以截图中的“舌象/查看”和报告附件退化为普通白按钮,而非 Vue 的真实缩略图、文件 chip 与叠加 CircleCloseD:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:42-100)。 |
| 9 | 处方、订单、视频、聊天仍为简化实现 | **OPEN** | **PARTIAL / MISSING** | 处方已接完整 PrescriptionEditorDialog 和真实 create_prescriptionsrc/doctor_workstation/ui/dialogs/diagnosis.py:2489-2539),订单偏移及详情入口已接真实方法(:2613-2650、:2883-2941),视频上传/回放和聊天归档同步也已接真实方法(:2541-2611、:2844-2881)。但视觉仍未闭合:处方按钮在 diagnosis_state_empty_1024x640.png 中为普通白色“开具处方”,而 Vue 是 primary 小号“开方”(D:/web/zyt/admin/src/views/tcm/diagnosis/components/CaseRecordList.vue:1-5);当前证据集没有处方编辑器或订单详情抽屉 PNG。diagnosis_state_video_replay_1024x640.png 只有外链“播放回放”,代码通过 QDesktopServices 打开 HTTP(S)diagnosis.py:2652-2658、:2874-2881),而 Vue 是表格内 RecordingPlaybackBlockD:/web/zyt/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue:17-34)。diagnosis_state_chat_archive_1024x640.png 有气泡、文件和同步/重载,但省略 Vue 的完整 info alert 和详细归档说明(D:/web/zyt/admin/src/views/tcm/diagnosis/components/ImChatRecordPanel.vue:1-20)。订单偏移可见部分本身为 EXACT。 |
| 10 | 表格状态仍整格着色而非 pill | **CLOSED** | **EXACT** | set_status_tag 以 QLabel pill 放入 cell widget,不再染整格:src/doctor_workstation/ui/diagnosis_drawer.py:2366-2384;所有状态列经 semanticize 转换:src/doctor_workstation/ui/dialogs/diagnosis.py:1900-1921、:2752-2754、:2822-2825、:2870-2874、:2898-2902。diagnosis_state_daily_lower_1024x640.png 的“待执行”蓝色 pill 是直接视觉证据。 |
| 11 | Shell 仍为 216/68 且无 40 px tabs | **CLOSED** | **EXACT** | 侧栏 183src/doctor_workstation/ui/shell.py:537-540topbar 50:599-603multiple-tabs 40:683-687。diagnosis_shell_1024x640.png 与 diagnosis_shell_1440x900.png 均可直接量出 183/50/40 三段,比例一致。 |
| 12 | Shell 颜色偏离且箭头缺字 | **CLOSED** | **EXACT(本项范围)** | 侧栏为 #1D2124src/doctor_workstation/ui/shell.py:541-569;方向、刷新、全屏、下拉、关闭图形均由 QPainter 自绘,不依赖字体 glyph:257-395topbar/tab QMenu 样式::599-628、:683-723。两张最新 Shell PNG 中“更多筛选”、行“更多”、用户菜单和 tabs 菜单箭头均正常,没有方框或乱码。 |
## 最后增加的三个专项检查
| 专项 | 状态 | 结论 |
|---|---|---|
| scoped panel 按钮 | **OPEN / PARTIAL** | src/doctor_workstation/ui/diagnosis_drawer.py:112-136 的 QDialog#DiagnosisDialogRoot QPushButton 规则确实把详情按钮从全局暖灰绿主题隔离出来;但它的 ID 选择器优先级高于全局 QPushButton[variant="primary/ghost"]src/doctor_workstation/ui/theme.py:87-118),而局部 QSS 没有补 primary、ghost、dangerGhost、diagnosisRange:checked 规则。结果在 daily/处方/视频/待办 PNG 中所有动作都被压成同一白色按钮,选中态也不清楚。这是实图结果,不是测试推断。 |
| QMenu | **OPEN / PARTIAL** | diagnosis_full_menu_1280x800.png 证明局部白底、细边框、32 px 左右行高、分隔线和菜单宽度已生效;但与 Vue 相比仍缺每项图标、danger 删除色,且“确认二维码”文案不精确。证据:src/doctor_workstation/ui/diagnosis_index_widgets.py:1371-1431 对照 D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:379-400、:2706-2708。 |
| 紧凑 close | **CLOSED / EXACT** | Header close 被限定 32×32、透明常态、蓝色 hover/focussrc/doctor_workstation/ui/diagnosis_drawer.py:137-154;创建位置:src/doctor_workstation/ui/dialogs/diagnosis.py:754-782。diagnosis_state_focus_1024x640.png 显示紧凑透明常态,默认 edit/viewOnly 图显示蓝色 focus/hover 外观,没有被全局 40 px footer 按钮规则放大。 |
## 43 张 PNG 原尺寸目检矩阵
下表的 EXACT 只针对该张 PNG 实际入镜内容;不以不可见的滚动下方内容作证明。
| PNG | 分类 | 原尺寸目检结论 |
|---|---|---|
| diagnosis_1024x640.png | **EXACT** | 窄屏筛选换行、表格内部横向溢出、右固定列和阴影稳定,无硬裁切。 |
| diagnosis_1440x900.png | **EXACT** | 宽屏列密度、复合挂号卡、斑马/业务语义色和固定操作区成立。 |
| diagnosis_advanced_filters_1280x800.png | **EXACT** | 高级筛选即时展开,日期范围、控件顺序和重置入口完整。 |
| diagnosis_double_appointment_cancel_1280x800.png | **EXACT** | 同一诊单两条预约均有各自取消入口,虚线层级和固定列正常。 |
| diagnosis_empty_1280x800.png | **EXACT** | 表头、内部空态、横向滚动和 0 条分页均保留。 |
| diagnosis_error_1280x800.png | **EXACT** | 错误文本居中可读,无旧数据穿透。 |
| diagnosis_focus_1280x800.png | **EXACT** | 搜索框蓝色 focus ring 清楚且不挤压布局。 |
| diagnosis_full_menu_1280x800.png | **PARTIAL** | 8 项动作与分隔线齐全;缺 Vue 图标、danger 删除色,二维码文案不同。 |
| diagnosis_horizontal_scroll_1024x640.png | **EXACT** | 主表横移、右 460 px 固定区不动,左缘阴影连续。 |
| diagnosis_hover_warning_1280x800.png | **EXACT** | warning 行 hover 渐变、3 px 左条和固定区同步。 |
| diagnosis_loading_1280x800.png | **EXACT** | 表内真实圆弧 spinner,内容淡化,无字符缺字。 |
| diagnosis_pending_assign_1280x800.png | **EXACT** | 待分配 chip、月份与宽搜索保持同一流式区。 |
| diagnosis_permissions_cropped_1280x800.png | **EXACT** | 权限裁掉 toolbar 与敏感动作,仅保留查看。 |
| diagnosis_edit_1024x640.png | **EXACT(可见区)** | 60% 抽屉、2/3 列语义表单、细 tab 条、固定 footer 无裁切;switch 未入镜。 |
| diagnosis_edit_1440x900.png | **EXACT(可见区)** | 宽屏密度、字段比例和 footer 稳定;switch 未入镜。 |
| diagnosis_readonly_1024x640.png | **EXACT** | 独立只读页、hero、病例卡完整,“客服”不拆字。 |
| diagnosis_readonly_1440x900.png | **EXACT** | 4 列病例密度和异常指标层级完整,无不必要换行。 |
| diagnosis_viewonly_1024x640.png | **EXACT** | 60% 只读抽屉、权限提示、禁用表单与仅关闭 footer 成立。 |
| diagnosis_viewonly_1440x900.png | **EXACT** | 宽屏只读栅格、全 Tab 条和固定 footer 成立。 |
| diagnosis_state_daily_1024x640.png | **PARTIAL** | DailyMatrix、异常/自录内容和新增动作出现;最右“刷新”被裁,range/primary 语义态被压平。 |
| diagnosis_state_daily_lower_1024x640.png | **PARTIAL** | 趋势、待办筛选、表格与 pill 出现;“新增待办”不是 Vue primarytoolbar 语义层级不足。 |
| diagnosis_state_empty_1024x640.png | **PARTIAL** | 处方空态和仅取消 footer 正确;“开具处方”按钮文案/primary 样式不同,且未覆盖真实处方编辑器。 |
| diagnosis_state_error_1024x640.png | **EXACT** | danger banner、重试、空表单、禁用保存完整。 |
| diagnosis_state_focus_1024x640.png | **EXACT** | 姓名 focus ring、紧凑 close 常态和固定 footer 清晰。 |
| diagnosis_state_loading_1024x640.png | **EXACT** | 圆弧 spinner 与“正在加载诊单...”完整,无旧 seed 或缺字。 |
| diagnosis_state_permission_1024x640.png | **EXACT** | 只保留病历/医生备注,手机号与身份证掩码,footer 仅关闭。 |
| diagnosis_state_notes_actions_1024x640.png | **PARTIAL** | 增加/上传/打开/单附件删除均出现;舌象与报告未呈现为 Vue 缩略图/chip。 |
| diagnosis_state_order_offset_1024x640.png | **EXACT(可见区)** | 偏移数值框、保存、订单表、分页与横向滚动完整;订单详情未入镜。 |
| diagnosis_state_save_failure_1024x640.png | **EXACT** | 红色叉与“保存失败”完整。 |
| diagnosis_state_save_loading_1024x640.png | **EXACT** | 白色圆弧 spinner 与“正在保存”完整。 |
| diagnosis_state_save_success_1024x640.png | **EXACT** | 绿色勾与“保存成功”完整。 |
| diagnosis_state_video_replay_1024x640.png | **PARTIAL** | 回放动作可见且文案完整;仍是外链按钮,不是 Vue 表内播放器。 |
| diagnosis_state_chat_archive_1024x640.png | **PARTIAL** | 左右气泡、图片/文件入口、同步/重载可见;完整 info alert 与原文案缺失。 |
| diagnosis_shell_1024x640.png | **EXACT(本项范围)** | 183/50/40,深色侧栏、分页、三角箭头和中文均无缺字。 |
| diagnosis_shell_1440x900.png | **EXACT(本项范围)** | 同一 Shell 比例在宽屏保持,固定列阴影和 horizontal scroll 清楚。 |
| appointment_drawer_1024x640.png | **EXACT** | 60% 右抽屉、日期/时段、body 滚动与固定 footer 无裁切。 |
| appointment_drawer_1440x900.png | **EXACT** | 医生 radio、四个 130×40 日期按钮、时段网格、备注与 footer 对齐。 |
| appointment_drawer_empty_doctors_1440x900.png | **EXACT** | warning、80 px 空态插图、“暂无可预约医生”和禁用确认完整。 |
| appointment_drawer_empty_roster_1440x900.png | **EXACT** | 无排班 warning、空态插图和“该医生暂无排班”可读。 |
| appointment_drawer_error_1440x900.png | **EXACT** | “号源加载失败:号源服务暂时不可用”、刷新入口与空态并存。 |
| appointment_drawer_keyboard_focus_1440x900.png | **EXACT** | 130×40 日期按钮的蓝色键盘 focus 边界清楚。 |
| appointment_drawer_loading_1440x900.png | **EXACT** | “正在加载医生、渠道与挂号状态...”与真实圆弧 spinner 完整,无缺字。 |
| appointment_drawer_refreshing_1440x900.png | **EXACT** | “正在刷新可用号源...”与 overlay spinner 完整,确认禁用。 |
## 服务端合同限制:不归入视觉 P1
当前服务端只提供“当日追加 / 查询 / 删除单个附件”,没有“任意覆盖既有正文”或“删除整条笔记”端点:
- D:/web/zyt/server/app/adminapi/controller/doctor/AppointmentController.php:168-196 只公开 addDoctorNote、doctorNotes、deleteDoctorNoteImage。
- D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-69 按 diagnosis_id + 当天 find-or-create,并把新正文追加为新行,不是任意覆盖。
- D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:114-136 只删除 tongue_images / report_files 中的单个路径。
- D:/web/zyt/admin/src/api/patient.ts:19-40 同样只有上述三个客户端 API。
- Python 远端仓库与该合同一致:src/doctor_workstation/services/repository.py:935-985。
因此桌面端没有伪造“覆盖旧正文 / 删除整条笔记”按钮是正确的 fail-closed 行为。该限制不能被当作视觉 PASS 的理由,也不应混入上面 4 个仍可修的 UI P1。
## 最终判定
- **P00 OPEN。**
- **P14 OPEN / PARTIAL**Daily 动作行与 scoped 按钮语义;完整菜单图标/删除色/文案;Notes 附件视觉;处方/订单/视频/聊天视觉与证据覆盖。
- **MISSING 视觉证据**:当前 diagnosis_visual 没有滚动到 switch 的截图,也没有完整处方编辑器和订单详情抽屉截图;源码能力存在,但不能用测试代替这些像素证据。
- **乱码 / 缺字:0。** 43 张图中的中文、圆弧 loading、箭头、保存勾叉均完整。
- **最终:PARTIAL。** 只有消除上述 4 组可见差异并补齐处方编辑器、订单详情与 switch 的当前截图后,才可给视觉同型 PASS。
+547
View File
@@ -0,0 +1,547 @@
# Python 医生工作站诊单视觉 / 交互差异审计
> 审计日期:2026-08-10
> 结论:**OPEN — 当前实现不具备与管理端诊单主链做截图级验收的条件。** 主要差异不是局部颜色或间距,而是索引页层级、表格固定列、只读/编辑形态和预约选择器的组件架构不同。
> 约束:本轮只读检查业务源码;只新增本审计文档,未修改 Python、Vue、接口、payload、权限、状态门槛或业务流程。
> 环境说明:项目提示的 `D:/web/zyt/app/.trellis/` 当前不存在,因此没有可补读的 Trellis 工作流或包级规格。
## 1. 范围与事实源
当前 Python 审计范围:
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py`
- `D:/web/zyt/app/src/doctor_workstation/ui/widgets.py`
- `D:/web/zyt/app/src/doctor_workstation/ui/shell.py`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py`
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py``_AppointmentDialog` 及其入口
管理端唯一视觉事实源:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/readonly.vue`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue`
- 上述页面直接渲染的 diagnosis components、`NoteTimeline.vue` 以及直接生效的全局样式
研究基线:
- `D:/web/zyt/app/research/diagnosis_index_visual_spec.md:1-373`
- `D:/web/zyt/app/research/diagnosis_detail_visual_spec.md:1-565`
明确排除:其他角色页面、小程序、企微运营、二维码、视频观看弹窗、未被三主视图引用的旧 Blood/Diet/Exercise/Tracking 组件,以及任何业务接口改造。本报告提到权限时,只描述入口或 Tab 的可见状态,不重新设计权限规则。
## 2. 总体判定
| 领域 | 当前状态 | 阻断截图复刻的根因 |
|---|---|---|
| 全局主题与密度 | OPEN | Qt 是暖灰绿、13 px、36 px 控件;管理端诊单基线是冷灰白、14 px、32 px 控件,索引主色为 `#4A5DFF` |
| 诊单索引 | OPEN | 多出页面标题;筛选永久四行;单表无右固定列;行操作、复合单元格、分页、空态和 loading 架构不同 |
| 诊单只读 / 编辑 | OPEN | 独立 readonly 与 drawer viewOnly 被合并成同一个居中 `QDialog`;Tabs、字段布局和内容主链大量不等价 |
| 预约 | OPEN | 60% 右 Drawer 被固定 540×560 Dialog 取代;医生、日期、时段网格全部降级为 ComboBox |
| 1024×640 | OPEN | 880×680 诊单 Dialog 请求高度超过外窗;索引页无页面级滚动且高级筛选常驻,表格可见高度过低 |
| 1440×900 | OPEN | 索引仍缺固定 460 px 右区;详情/预约保持固定宽度,无法随视口保持 60% 比例 |
必须先完成四项结构改造,再做颜色和像素微调:
1. 诊单索引改回“无页面标题 + 两张平卡 + 折叠筛选”。
2. 诊单表使用专用 model/delegate,并实现右侧 `120 + 340 = 460 px` 固定区域。
3. 将独立 readonly 页面与 edit/viewOnly Drawer 拆成两个视觉形态。
4. 将预约恢复为右侧 Drawer,并恢复医生 radio、日期按钮、时段卡片网格及完整状态。
## 3. 基础 token 与壳层差异
### G-01 [阻断] 字体、颜色、控件与圆角是另一套设计系统
管理端诊单索引基线为 `PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei`、基础 14 px、页面 `#F6F6F6`、卡片 `#FFFFFF`、主/常规/次要文字 `#333/#666/#999`、默认控件 32 px、small 24 px、索引主色 `#4A5DFF`、卡片 r4。证据:
- `D:/web/zyt/admin/src/config/setting.ts:1-14`
- `D:/web/zyt/admin/src/styles/var.css:1-43`
- `D:/web/zyt/app/research/diagnosis_index_visual_spec.md:65-101`
当前 Qt 为 Microsoft YaHei UI 优先、基础 13 px、画布/表面 `#F4F3EF/#FCFBF8`、墨绿/青绿 `#17382F/#168579`、按钮和输入最小 36 px/r9、卡片 r16。状态色也全部偏成低饱和绿褐色。证据:
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:13-33`
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:36-84`
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:87-175`
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:233-250`
可见后果:同一页面的字宽、首屏密度、按钮高度、卡片轮廓、语义色和表格行数均不同;仅替换主色不能修复。
建议边界:在 `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:13-275` 增加以 `#DiagnosisIndex``#DiagnosisDrawer``#DiagnosisReadonly``#AppointmentDrawer` 为根的局部 token/QSS。除非另立全工作站换肤任务,不应直接改写共享 `Card``QPushButton``QTableWidget` 的全局规则。
### G-02 [高] 壳层占宽与页面滚动模型不同
当前 Shell 最小 1024×640,侧栏固定 216 px、顶栏固定 68 pxworkspace 的 `QStackedWidget` 本身没有页面滚动容器:
- `D:/web/zyt/app/src/doctor_workstation/ui/shell.py:306-325`
- `D:/web/zyt/app/src/doctor_workstation/ui/shell.py:329-423`
管理端默认侧栏宽 183 px,navbar 50 px,默认还可显示 40 px multiple tabs;路由内容由 `el-scrollbar` 承担整页纵向滚动,页面外留白为 8 px 水平 / 16 px 垂直:
- `D:/web/zyt/admin/src/config/setting.ts:1-6`
- `D:/web/zyt/admin/src/styles/var.css:4-10`
- `D:/web/zyt/admin/src/layout/default/components/header/multiple-tabs.vue:65`
- `D:/web/zyt/admin/src/layout/default/components/main.vue:2-11`
当前诊单页又额外使用 24 px 水平边距,因此两档窗口下的静态内容宽约为:
| 外窗 | 当前 workspace | 当前页面内容宽 | 管理端默认路由内容宽(约) | 差值 |
|---|---:|---:|---:|---:|
| 1024×640 | 808 | 760 | 824 | -64 px |
| 1440×900 | 1224 | 1176 | 1240 | -64 px |
建议边界:索引页自行增加页面级滚动及 8/16 外留白;不要为了修诊单页直接改变全工作站 216/68 壳。若最终验收要求连管理端壳一起复刻,应另行冻结侧栏、navbar、multiple tabs 的 setting 后再改 `ShellWindow`
### G-03 [高] hover、checked、focus、read-only 状态不完整
- 日期和业务入口都是 checkable ghost `QPushButton`,但 QSS 没有 `QPushButton[variant="ghost"]:checked`;active 态没有确定的可见反馈。当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:87-118``D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:495-528,776-799`;基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2227-2308`
- Qt 输入 focus 直接把 1 px 边改为 2 px 青绿实线,存在内部尺寸跳动;管理端是 primary-light 的 2 px 外环。当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:140-154`;基准:`D:/web/zyt/admin/src/styles/element.scss:140-175`
- 按钮、Tab、radio、checkbox 没有统一的键盘 focus ring;表格还显式 `outline:0`。当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:87-138,164-186,208-220`
- read-only `QPlainTextEdit` 没有独立 QSS,仍呈白底可编辑外观;只定义了部分 `:disabled`。当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:140-155``D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:685-689`
- 表格没有显式 item hover 规则;管理端普通 hover 是 `#F8F8F8`,业务行还有语义渐变。当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:177-197`;基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2723-2735`
建议边界:为诊单专用 chip、输入、Tab、表格 delegate 增加状态规则;保留键盘可达性,不必复刻 HTML `span` 不可聚焦的缺陷。focus 不应通过改变边框占位宽度实现。
### G-04 [中] 共享组件的视觉契约与诊单基线不同
- `PageHeader` 固定生成标题/副标题层级:`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:331-365`
- `EmptyState` 是 30 px 圆圈 + 16 px 标题 + 说明 + 44 px 垂直留白:`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:388-420`
- `BusyOverlay` 是文字 + 140 px 横向 progress,非 42 px 环形 spinner`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:489-514``D:/web/zyt/app/src/doctor_workstation/ui/theme.py:230-231,271`
- `SortableTable` 是单选、全列本地排序、纯文本 item、末列 stretch`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:535-575`
- `Pager` 只有 total、上一页、页码分数、下一页:`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:585-627`
建议边界:新增诊单专用组件,不要破坏性修改这些被患者、订单、接诊页共享的类。
## 4. 诊单索引页差异
### I-01 [阻断] 页面层级、留白和卡片形态错误
管理端页面内没有标题或说明;路由内容直接从无边框、无阴影、r4 的筛选白卡开始,第二张列表卡相距 12 px。新增诊单在列表卡工具栏:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2-177`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2200-2211,2408-2434`
当前页面先放 25 px `PageHeader("问诊列表", …)`,并把新增按钮放在标题右侧;外边距 24/20/24、间距 14。两张卡是有边框的暖白 r16,表格自身再加 r12,形成“卡中卡”:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:464-587`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:590-674`
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:54-84,177-197`
建议边界:只在 `ConsultationsPage.__init__` 移除页面内 `PageHeader`,将 add 入口移入列表工具栏;保留 Shell topbar。诊单两卡使用局部 objectName/QSS。
### I-02 [阻断] 日期 / 业务 chip 顺序、文案、换行和语义态不同
管理端顺序严格是“昨天挂号、前天挂号、当天挂号、明天挂号、后天挂号、全部”,随后是待预约/已完成/待分配医助;左侧 chips 可换行,计数是同行半透明文字。三种业务 chip 分别用 info/success/warning
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:6-70`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:964-970`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2222-2317`
当前顺序是“前天、昨天、今天、明天、后天、全部”,缺“挂号”文案;九个按钮放在不可换行的 `QHBoxLayout`,全部是通用 ghost,无业务色和明确 checked 态:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:483-530`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:776-799`
在 1024 档,按钮加入两三位计数后可能在约 760 px 内容宽内被挤压或裁字。
建议边界:增加诊单局部 `FlowLayout` 和带 dynamic property 的 chip;沿用现有 `_choose_*``_update_quick_buttons()`,只换视图装配和文案。
### I-03 [阻断] 筛选的两层结构被永久四行网格取代
管理端:第一行右侧是固定 160 px 搜索 + 32 px primary 查询;第二行是“挂号/确认”小 chips 和“更多筛选”;高级区默认隐藏,点击后即时显隐,使用虚线上边界和可换行布局:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:72-151`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2320-2405`
当前:关键词、挂号、确认和所有高级条件永久铺成四行等宽 `QGridLayout`;查询/重置在最后一行。挂号/确认变成 ComboBox;最近日期范围被拆成四个独立 `QDateEdit`;还常驻一个管理端通过表头表达的“未服务排序”下拉:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:532-586`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:683-721`
当前还缺待分配模式选中后显示的月份 128 px 和宽搜索 220 px / max 42vw;管理端证据为 `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:40-70,2298-2317`
建议边界:重建筛选视图,但保留 `_search()``_filters()`、日期校验和现有请求字段。是否补待分配月份/宽搜索涉及现有请求参数,超出本视觉报告授权;在业务层确认前先预留容器,不自行发明接口字段。
### I-04 [阻断] 列表工具栏与选择模型不等价
管理端列表栏无“诊单记录”标题;左侧是新增、批量指派、批量取消指派,右侧按需显示“已选 n 条”。表格首列为 48 px checkbox 多选:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:154-191`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2414-2429`
当前工具栏是标题 + 总数摘要 + 查看/诊单/开方/作废/视频/删除/刷新等全局按钮;表格无 checkbox,使用整行单选,加载后自动选第一行并出现青绿选择底:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:590-640`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:938-946`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:1085-1112`
- `D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:541-549`
建议边界:视图层改为 checkbox check-state;不要用整行 selection 代替多选。批量动作若当前业务方法不存在,本轮只应建立视觉容器和禁用态,不得伪造 mutation。
### I-05 [阻断] 表格列、固定区、排序和行操作架构不同
管理端列顺序与宽度为:选择 48、ID min70、患者 min60、性别年龄 100、挂号 min175、确认 88、复诊 min120、助理 100、开方 72、未服务 110、视频旁观 120 fixed、操作 340 fixed。声明最小总宽约 1403 px,右固定区共 460 px
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:191-406`
- `D:/web/zyt/app/research/diagnosis_index_visual_spec.md:169-186`
当前列约 1392 px,但无选择/操作列,额外有医生 90、作废 74;挂号 300、未服务 180、视频 90。所有列随单一 `QTableWidget` 一起滚动,末列还会 stretch:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:646-663`
- `D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:535-552`
管理端只允许“未服务天数”走 descending → ascending 的自定义排序;当前全部表头都可做客户端排序,并另设排序 ComboBox:
- 基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:297-304`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:543-549``D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:560-570`
管理端每行右侧有查看、诊单、开方/查看处方、预约、补全身份证和“更多”菜单;当前把少量动作集中在列表栏,行内没有动作命中区:
- 基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:342-403`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:605-639`
建议边界:新增 `DiagnosisTableModel`、专用 delegates 和同步的双 `QTableView`;右表只显示 120/340 两列,共享 model、selection、垂直滚动与 row height。不要把固定列能力硬塞进通用 `SortableTable`
### I-06 [阻断] 复合单元格、语义色、hover 和密度全部降级为纯文本
当前 `SortableTable.set_rows()` 只创建 `QTableWidgetItem(text)`;格式化函数也只拼接字符串:
- `D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:554-575`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:233-322`
由此缺失:
- ID/患者 14 px/600`NEW` 20 px danger tag。
- 挂号块的 11 px badge、14 px/600 医生、12 px 时间、状态渐变、多预约虚线、取消挂号链接和最近渠道。
- 已确认/未确认、已开方/未开方、处方作废 tag。
- 未服务 null/`<=2`/`36`/`>=7` 的灰/绿/橙/红,以及健康打卡 tooltip。
- 普通白/#FAFAFA 斑马、#F8F8F8 hover、未确认/未挂号的 3 px 左条和渐变。
- 行级视频入口和 340 px 动作链接区。
管理端证据:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:193-406`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:2442-2735`
当前表格头是 12 px/700、`#EEF1EE`alternate 为暖灰 `#F6F7F4`;基准表头为 13 px/600、`#F8F8F8`,普通预约行约 52 px。当前只调用 `resizeRowsToContents()`,无 39/52 px 基线:
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:177-197``D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:938-940`
- 基准:`D:/web/zyt/app/research/diagnosis_index_visual_spec.md:163-212`
建议边界:复合绘制与 `sizeHint()` 均放在诊单 delegates;不要给每格常驻 QWidget。行样本必须覆盖多预约和动作换行,且双表 row height 同步。
### I-07 [高] 分页、空态、loading、error 的布局契约不同
分页基线为 total、15/20/30/40 size、prev、最多 5 个数字页、next、jumper,按钮 32 px;当前只有 total、上一页、`1/N`、下一页,按钮最小 36 px
- 基准:`D:/web/zyt/app/research/diagnosis_index_visual_spec.md:254-262`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:585-627`
空态基线保留表头和分页,仅在 table body 显示约 60 px 高的“暂无数据”。当前切换整个 `content_stack` 到大型 `EmptyState`,表头与分页同时消失:
- 基准:`D:/web/zyt/app/research/diagnosis_index_visual_spec.md:264-273`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:642-673``D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:388-420`
loading 基线只覆盖表格,亮色 `rgba(255,255,255,.5)` + 42 px 主色环;20 秒静默刷新不闪。当前普通加载在两卡之间插入 MessageBanner,并禁用刷新,没有 table overlay;共享 `BusyOverlay` 的形态也不匹配:
- 基准:`D:/web/zyt/app/research/diagnosis_index_visual_spec.md:275-280`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:901-956``D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:489-514`
错误当前只显示全宽 Banner,原表/旧数据仍在;管理端主表没有独立 error 插画。建议保留错误消息但不得让 Banner 插入造成卡片整体跳位。
建议边界:新增诊单专用 PaginationBar、table viewport empty label 和 table-only overlay;保持现有 generation、silent refresh 与错误传递逻辑不变。
### I-08 [高] 页面纵向滚动与少量数据高度不同
管理端表格没有本页固定高度;15 行自然撑高并由路由 `el-scrollbar` 滚整页。当前列表卡以 stretch=1 填满余高,表格内部滚动;一行数据时会留下大块空白,15 行时筛选和工具栏固定在窗口而不是随页面滚动:
- 基准:`D:/web/zyt/admin/src/layout/default/components/main.vue:2-11``D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:179-190`
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:642-674`
建议边界:诊单页使用单一 `QScrollArea` 承载两卡,表格按 header + 实际 rows + pagination 给自然高度;不要改其他页面的内部表滚动策略。
## 5. 诊单只读 / 编辑差异
### D-01 [阻断] 两种只读形态被合并为一个居中 Dialog
管理端有两个不可互换的只读形态:
1. `readonly.vue`:独立单列页面,无 Tabs,Hero 后按顺序纵向展示摘要、病例、日常记录、备注、订单、视频、聊天、指派、挂号。
2. `edit.vue``viewOnly`:仍是 60% 右 Drawer + Tabs + disabled 表单,只隐藏保存。
证据:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/readonly.vue:1-149`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1-84,766-781`
- `D:/web/zyt/app/research/diagnosis_detail_visual_spec.md:75-152,156-194`
当前所有查看/编辑都调用同一个 `DiagnosisDialog.open_for()`;只通过 `setReadOnly` 和保存按钮显隐切模式:
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:245-288`
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:426-464`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:1113-1129`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:2142-2151`
建议边界:保留 `open_for()`、saved signal、现有 load bundle 和数据快照;仅拆视图为 `DiagnosisReadonlyView``DiagnosisDrawerView`,由入口决定呈现形态。不得通过给现有 QDialog 换皮来声称两种基线都已覆盖。
### D-02 [阻断] Drawer 几何与 Header/Body/Footer 三段壳缺失
编辑/viewOnly 基线为 RTL 右 Drawer、宽视口 60%、全高;header 为白到 `#F8FAFC` 渐变和底线,body `#F8FAFC`,footer 独立白底、上边线,按钮右对齐、40 px/r10:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:3-10`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1670-1745`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1776-1864`
当前是居中系统 `QDialog`,请求 880×680、最小 680×520,统一暖灰画布,四周 20/18;标题 25 px。按钮栏虽在 Tabs 外,能保持不随首 Tab 滚动,但没有 footer frame、边线和确定按钮顺序:
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:245-288`
- `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:44-46,54-58,87-104`
当前 mode 文案/颜色也不同:后台只读 info、编辑 warning、新建 success;当前只读 neutral、可编辑 success,且标题是“患者信息详情/编辑患者病历”:`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:256-266,441-447`
建议边界:在主窗之上实现遮罩 + 右贴边 panel,按外窗宽度计算 60%;只让 body 滚动,header/footer 固定。保留 QDialog 作为临时承载会继续出现系统标题栏和居中几何差异。
### D-03 [阻断] Tabs 样式、顺序、权限可见性和内容主链不等价
管理端顺序:病历、医生备注、日常记录、处方、业务订单、视频录制回放、聊天、指派、挂号;“跟踪备注”已注释,不渲染。Tabs 为可横滚文本导航,4 px scrollbaractive 3 px 底条:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:55-759`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1798-1858`
当前只有患者与病历、医生备注、挂号记录、指派记录、可选患者订单;挂号先于指派,日常记录/待办、处方、视频、聊天均缺失。除订单外,Tab 没按对应权限移除:
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:271-279`
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:479-525`
当前全局 Tab 是青绿填充 pill、r9、36 px,无 3 px active indicator`D:/web/zyt/app/src/doctor_workstation/ui/theme.py:208-220`
建议边界:用诊单局部 `QTabBar + QStackedWidget` 或可横滚 TabBar;按既有 canonical permissions 移除入口,当前 Tab 失权时回病历。不要加入已注释 TrackingNote,也不要重复增加旧 Blood/Diet/Exercise Tabs。
### D-04 [阻断] 编辑表单被统一成单列大文本框,字段顺序也偏离真实模板
当前 `_DIAGNOSIS_FIELDS` 将全部字段装成单列 `QFormLayout + QPlainTextEdit`,每项 4682 px;没有 160 px label、栅格、分组标题或语义控件:
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:49-102`
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:331-346`
管理端是 12 px 表单、160 px label、32 px input/select/date/number、radio/check-button、24/12/8 栅格、18 px 项间距,以及横线夹胶囊的分组标题:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:57-632`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1476-1667`
当前与后台真实模板的具体偏差:
- 缺少/不等价:诊单ID表单行、状态、渠道、统计端就诊卡、当地诊断日期、糖尿病病史、口腔感觉、其他补充、病史补充。
- “当前用药”被放在末尾;后台“在用药物”位于生命体征区。
- 当前显示后台模板未渲染的证型、糖尿病类型、自由文本主诉/现病史/症状、食欲、舌象、舌苔、脉象、临床诊断、治则、处方、处方意见、医嘱。
- 类别项都变成自由文本,失去 radio/check/select 的密度和状态。
真实后台字段次序见 `D:/web/zyt/app/research/diagnosis_detail_visual_spec.md:221-255`
患者基本信息锁定时,当前只设 read-only,没有后台 warning Alert
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:659-689`
- 基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:71-84`
建议边界:只重建 `DiagnosisDialog` 的表单装配和 scoped QSS,保留字段值转换、校验、保存入口及隐私遮罩。严格按后台实际渲染字段,不把脚本模型中但模板未显示的字段继续呈现。
### D-05 [阻断] 独立 readonly 的 Hero、卡片流和病例网格完全缺失
readonly 基线:16 px 页面边距/卡间距;可换行浅蓝 Hero 带返回、患者名、性别年龄、未服务绿橙红 badge;白卡 r14、18 px padding、标题前 3×16 蓝条;患者摘要按确定顺序,病例按 4/3 列分组键值展示:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/readonly.vue:1-149`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/readonly.vue:257-405`
- `D:/web/zyt/app/research/diagnosis_detail_visual_spec.md:75-152`
当前没有返回、Hero、未服务 badge 或纵向卡片流。摘要是两列九项,额外含身份证/血压/血糖,却缺地区、预约状态、是否开方和备注;病例仍是单列可编辑形态:
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:290-349`
- `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:574-700`
建议边界:单独实现只读卡片 view;不要把 disabled 编辑表单当作独立 readonly。1440 档保留后台 4/3 列;1024 档若主动降列,应在验收中标注为桌面可用性增强,而不是声称 Vue 原实现已有响应式。
### D-06 [高] 备注、订单、历史表和关联内容被降级或缺失
- 医生备注:后台是时间轴、64×64 缩略图、文件 chip、编辑动作;当前仅三列“时间/医生/内容”。基准 `D:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:1-436`;当前 `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:351-364`
- 日常记录/待办:后台有日期筛选、矩阵、趋势、toolbar 和待办;当前整个 Tab 缺失。基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:1-1260``D:/web/zyt/admin/src/views/tcm/diagnosis/components/DiagnosisTodoList.vue:1-319`
- 处方历史:后台七列并有编辑态开方入口;当前缺失。基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/CaseRecordList.vue:1-106`
- 业务订单:后台有就诊序号工具栏、九列、Tag/红金额、固定详情;当前八列为订单号/处方/收货人/手机/金额/发货/状态/创建,无工具栏和详情动作。当前 `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:393-424,764-787`;基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/PatientOrderList.vue:7-148,252-276`
- 指派历史:后台九列;当前六列且继承只是纯文本。当前 `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:378-390,748-762`;基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/AssignLogPanel.vue:1-43`
- 挂号历史:后台十二列和多个状态 Tag;当前七列。当前 `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:370-390,731-746`;基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/AppointmentRecordPanel.vue:1-75`
- 视频与聊天:当前完全缺失。基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue:1-214``D:/web/zyt/admin/src/views/tcm/diagnosis/components/ImChatRecordPanel.vue:1-293`
当前所有表格最终都只创建纯文本 `QTableWidgetItem``D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:711-716`
建议边界:按当前主视图可达组件增加局部 model/delegate;不把不可达旧组件或观看弹窗混入。缺失内容若尚无当前 repository 数据来源,应明确保持 OPEN,不能用静态假数据冒充视觉完成。
### D-07 [高] loading、empty、error、保存与隐私锁定状态不足
- 打开详情时只显示 MessageBanner,种子内容继续可见;没有 readonly 页面级 overlay/skeleton`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:452-464`
- 加载失败只改 Banner,后台独立 readonly 应在 Hero 下显示错误 Empty:当前 `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:570-573`;基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/readonly.vue:31-36`
- 清空表格只是 `setRowCount(0)`,没有“暂无…”状态:`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:702-709`
- 订单翻页仍用全局 Banner,未遮住对应 Tab 内容:`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:789-827`
- 保存只禁用按钮并显示 Banner,没有按钮 loading 动画或字段级 danger`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:829-917`
- 隐私/业务锁定只有只读控件,无 warning Alert,且 read-only 控件视觉仍像可编辑白框:`D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:659-689``D:/web/zyt/app/src/doctor_workstation/ui/theme.py:140-155`
建议边界:为每个内容区域提供局部 overlay/empty/error;保留 seed-first、generation 和异常处理。错误样式不能改变业务成功/失败判定。
## 6. 预约差异
### A-01 [阻断] 预约 Drawer 被固定居中 Dialog 取代
基线为 RTL 右 Drawer、外窗 60% 宽、全高、遮罩点击不能关闭、独立 footer;表单 label 100 px
- `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:2-19,185-198`
当前是居中 540×560 模态 QDialog、20/18 内边距、系统标题栏、默认 QFormLayout label 宽;内容顶部还额外放 25 px 患者名和“患者号仅用于视频”说明:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:219-299`
建议边界:保留 `_AppointmentDialog` 的异步查询、状态字段、`payload()``accept()`;只把承载层替换为右贴边 panel,并将 Header/Body/Footer 分离。不要改 `PatientsPage._book_appointment()` 的业务调用:`D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:2208-2224`
### A-02 [阻断] 字段顺序和控件类型不等价
后台严格顺序是:上次就诊、预约方式、预约类型、当前患者、渠道、条件性自媒体补充、医生 radio、日期按钮、时段矩阵、备注:
- `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:20-181`
当前缺上次就诊、预约方式和当前患者;预约类型是 ComboBox,医生/日期/号源也全是 ComboBox
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:247-285`
渠道基线宽 100%、最大 360 px;当前随 QFormLayout 拉伸,没有最大宽。`channel_source_detail` 创建后默认可见,初始化信号又被 `_setting` 拦截,因此首帧/默认渠道可能错误显示“渠道补充”;只有后续用户 change 才显隐:
- 当前:`D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:260-268,345-402,587-594`
- 基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:62-75,741-750`
建议边界:按真实顺序重建表单控件;沿用当前字段与校验,不改变 payload key。条件控件在初始渲染时就必须正确隐藏。
### A-03 [阻断] 日期和时段的所有可见状态都被下拉框吞掉
后台医生为可换行 radio;日期按钮 min 130×40、r8、14 pxhover 上移 2 px并有阴影;时段容器为 `#F8F9FA`、16 px padding,网格 cell min 110×70,显式展示可约、不可约、hover、selected 和刷新 loading
- `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:77-171`
- `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:752-921`
当前只将医生、日期、可用号源放进 ComboBox;`_apply_slots()` 直接过滤不可约、quota<=0 和当天过去时段,因此不可约卡、状态标签、slot hover、selected 渐变和刷新入口均无法呈现:
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:269-280`
- `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:534-574`
建议边界:以服务端原始 slot 列表驱动局部 Flow/Grid;业务上不可提交的时段保留为 disabled card。当前的未来日期限制、过期判定和 submit gate 保持不变。
### A-04 [高] loading、空态、重复预约、hover/focus 状态不足
- 后台未选医生/无排班在时间区显示 Empty;当前只是 ComboBox placeholder + Banner:基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:100-171`;当前 `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:410-493`
- 后台 `v-loading` 覆盖预约 body;当前所有阶段复用表单下方 MessageBanner,没有区域 overlay:基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:10`;当前 `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:286-299,439-448,521-532`
- 当天冲突基线在顶部持续显示 warning,切其他日期显示 info;当前只计算今日 blocking 并通过提交禁用/accept 文案表达,没有持续的 warning/info 双态:当前 `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:319-402,600-643`;基准 `D:/web/zyt/admin/src/views/tcm/diagnosis/appointment.vue:10-18,359-386`
- 后台 available hover、disabled opacity、selected 蓝渐变和 slot refresh loading 当前均不存在。
- 当前 generic focus 仍是青绿 2 px 实边,radio/date/slot 的 focus-visible 不存在。
建议边界:增加预约 body overlay、时间区 empty、顶部冲突 alert、按钮/卡片 hover/pressed/focus;保留现有 `_today_blocking` 与 submit complete 判定。
### A-05 [高] 预约响应式和滚动安全不足
基线预约 Drawer 自身没有 <=768 media query,但内部医生、日期、slot 可换行,body 是 Drawer 可滚区域。当前固定 540×560 且无 `QScrollArea`;恢复完整 slot 网格后若继续用当前容器,1024×640 必然裁掉内容或 footer。
建议边界:桌面保持 60% 比例;当可用内容宽不足时再做全宽/单列增强,并明确这是 PySide 可用性增强。body 独立滚动,footer 永远留在可视区。
## 7. 两档窗口的确定性差异
### 1024×640
| 视图 | 管理端基线 | 当前源码请求 | 必须验收的风险 |
|---|---|---|---|
| 索引 | 页面内容可整体纵向滚;表宽约1403,右460固定 | 内容约760;高级筛选四行常驻;单表约1392且无固定列 | chip 计数裁字、表格高度仅剩少数行、动作不可见、空/loading移位 |
| edit/viewOnly | 右贴边约614 px、全高、固定 footer | 居中880×680,最小680×520 | 请求高度已超过640;系统窗口可能裁剪/强制缩放,footer不可作为稳定截图基线 |
| appointment | 右贴边约614 px、全高 | 居中540×560 | 宽度偏小且大量留白;恢复slot后无滚动会裁内容 |
| standalone readonly | 页面流、16px边距、无Tabs | 880px模态Tabs | 模式、层级和宽度均不可比较 |
### 1440×900
| 视图 | 管理端基线 | 当前 | 必须验收的风险 |
|---|---|---|---|
| 索引 | 内容宽约1240;仍可能横滚,右460固定 | 内容约1176;横滚约248px且无固定列 | 视频/操作随滚动消失;少量数据时列表卡被拉出大空白 |
| edit/viewOnly | 右贴边约864 px | 居中880×680 | 宽度偶然接近,但方向、全高、三段壳仍错误 |
| appointment | 右贴边约864 px | 居中540×560 | 明显过窄,无法呈现日期/slot网格密度 |
| standalone readonly | 宽页面流,病例4/3列 | 固定880px单列表单 | 信息密度和卡片层级完全不同 |
注:Element Drawer 的 60% 按外窗 viewport 计算,因此上表为约 614/864 px;不按 Python 扣除侧栏后的 workspace 计算。
## 8. 建议改动边界(按文件 / 类)
| 目标 | 允许的视觉改造 | 必须保持不动 |
|---|---|---|
| `D:/web/zyt/app/src/doctor_workstation/ui/theme.py:13-275` | 新增诊单局部 token、scoped QSS、checked/focus/read-only/overlay/delegate 状态 | 不全局换肤其他页面;不改变业务状态含义 |
| `D:/web/zyt/app/src/doctor_workstation/ui/widgets.py:331-627` | 新增 FlowLayout、DiagnosisPaginationBar、局部 loading/empty、专用 model/delegate/固定表辅助 | 不破坏现有 PageHeader、EmptyState、SortableTable、Pager 的调用方 |
| `D:/web/zyt/app/src/doctor_workstation/ui/shell.py:306-423` | 仅当全壳明确纳入验收时调整;可提供 drawer host/overlay host | 不以改壳宽度掩盖诊单页布局问题 |
| `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:464-674` | 重建索引层级、筛选、工具栏、表格绑定、页面滚动 | 保留 repository 调用、过滤语义、权限判断、generation、mutation、视频门槛 |
| `D:/web/zyt/app/src/doctor_workstation/ui/pages/consultations.py:901-956` | 将普通 loading/error 映射到 table-only overlay/状态层 | 20秒 silent refresh 继续不闪;不改请求时序 |
| `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:208-424` | 拆 readonly/drawer 视图,重建 header/tabs/form/cards/tables | 保留 `open_for` 契约、saved signal、隐私可见性和已有数据加载 |
| `D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:426-917` | 把加载/错误/锁定/保存映射到局部视觉状态 | 不改 endpoint、payload、字段校验、generation、防过期响应 |
| `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:219-646` | 把预约改成 Drawer、radio/date/slot grid、overlay/empty/alert | 保留排班来源、日期/过期过滤、冲突门槛、submit gate、`payload()` |
| `D:/web/zyt/app/src/doctor_workstation/ui/pages/patients.py:2208-2224` | 仅替换打开的视觉容器 | 不改预约保存调用与 mutation reconcile |
推荐实施顺序:
1. 建 scoped token 与 drawer/table 基础组件。
2. 重建 index 层级、筛选、固定列和 states。
3. 拆 readonly 与 edit/viewOnly,先完成壳和病历主 Tab。
4. 补当前可达的 notes/orders/histories;数据源不存在的主链保持显式 OPEN。
5. 重建 appointment 卡片式选择器。
6. 最后做两档截图的字号、间距和颜色微调。
## 9. 截图验收前置条件
所有截图固定:Windows 缩放 100%、默认亮色、同一 Qt 字体回退、同一权限集合、相同样本数据、无本地主题覆盖。运行态字体/DPR、tooltip/dropdown 坐标和 scrollbar 像素应记录环境,不应以静态源码推断绝对像素。
索引样本至少包含:`NEW`、双预约、已完成预约、爽约、未确认、已确认未挂号、处方作废、unserved=`null/2/5/8`、可进入/不可进入视频。
详情样本至少包含:患者基本信息可编辑/锁定、全部权限/裁减权限、备注含图片和文件、订单有/无数据、挂号/指派有/无数据、加载失败。
预约样本至少包含:未选医生、无排班、可约、不可约、当天过去时段、选中、今日冲突、其他日期 info、渠道需要/不需要补充、loading/error。
## 10. 截图验收矩阵
| ID | 窗口 | 状态 / 操作 | 必须同时看见 | 关键判定 |
|---|---|---|---|---|
| IDX-01 | 1024×640 | 默认15条、筛选收起、横滚最左 | 无页面内标题;两张平卡;两行筛选;表头、分页 | chips 不裁字;搜索完整;默认当天 active;至少能正常纵向滚整页 |
| IDX-02 | 1024×640 | 横滚到最右 | 左表发生横滚;视频120+操作340仍贴右 | 固定区不移动;双表 header/body/row height 无错位 |
| IDX-03 | 1024×640 | 更多筛选展开 | 虚线分隔、高级控件可换行、表格仍可达 | 显隐无动画;24/32px混合高度按源码;页面整体可滚 |
| IDX-04 | 1024×640 | 四类 chip active + 键盘 focus | 当天/待预约/已完成/待分配各自语义色 | 计数为同行文字;focus清晰且无几何跳动 |
| IDX-05 | 1024×640 | 普通、未确认、未挂号 hover | 斑马、#F8F8F8 hover、3px语义条/渐变 | fixed区背景连续;hover不被 selection 色覆盖 |
| IDX-06 | 1024×640 | 慢请求 loading | 原表尺寸保持、table-only 半透明层、42px spinner | 筛选/卡位置不跳;无全宽 banner 插入 |
| IDX-07 | 1024×640 | 空结果 | 表头、60px“暂无数据”、分页 | 无大圆圈插画;pager不消失 |
| IDX-08 | 1440×900 | 默认15条 | 完整两卡、自然表高、完整分页 | 少量数据不拉出大空白;15条由整页滚动承载 |
| IDX-09 | 1440×900 | 多预约 + 操作换行 | 预约 badge/渐变/虚线、340px操作链接 | 行高由内容增长;左右固定表同步 |
| IDX-10 | 两档 | tooltip / 更多菜单 | 助理溢出、未服务、视频 tooltipbottom-end菜单 | tooltip只在语义命中区;删除前分隔且danger |
| DET-01 | 1024×640 | edit 基本 Tab | 右侧约614px全高 Drawer、header、横滚Tabs、固定footer | body单独滚;footer不裁;label160px;无系统标题栏 |
| DET-02 | 1024×640 | viewOnly + 患者锁定 | info badge、disabled表单、warning Alert、关闭按钮 | 无保存;read-only外观不似可编辑;隐私字段无泄漏 |
| DET-03 | 1440×900 | edit 全权限 | 右侧约864px、Tab正确顺序、24/12/8栅格 | 无TrackingNoteactive底条3px;表单字段顺序与§4.5一致 |
| DET-04 | 两档 | 独立 readonly | 无Tabs/模态;返回+Hero+摘要+病例+权限卡片 | 1024可用;1440病例保持4/3列;未服务三色正确 |
| DET-05 | 两档 | 详情 loading/error/empty | overlayHero下错误Empty;各表局部空态 | 不残留错误seederror不导致footer/tab位移 |
| DET-06 | 1440×900 | 备注/订单/指派/挂号 | 时间轴图片/文件;订单工具栏/详情;完整列/Tag | 表头顺序、状态色、固定动作列与管理端一致 |
| APT-01 | 1024×640 | 初始 loading / 未选医生 | 右侧约614px Drawer、body overlay、时间区Empty、固定footer | label100pxfooter可见;渠道补充默认正确隐藏 |
| APT-02 | 1024×640 | 无排班 | 医生radio、时间区“暂无排班”Empty | 不用ComboBox placeholder冒充空态 |
| APT-03 | 1024×640 | 可约/不可约/hover/selected | 日期130×40slot最小110×70;刷新 | 不可约仍显示但禁用;hover抬升;selected蓝渐变 |
| APT-04 | 两档 | 今日冲突 / 其他日期重复 | 顶部warning切info、确认disabled/available | 状态随所选日期切换;不只在点击提交后提示 |
| APT-05 | 1440×900 | 完整预约表单 | 右侧约864px、医生和日期自然换行、slot多列 | 不保持540px窄窗;body滚动,footer固定 |
| A11Y-01 | 两档 | 仅键盘遍历 | chip/input/select/radio/checkbox/table/button均有focus | 顺序可预测;focus对比足够;无控件尺寸抖动 |
## 11. 发布门槛
以下任一项仍存在即不能标记视觉对齐完成:
- 索引页仍有第二个页面标题,或高级筛选默认常驻。
- 表格仍是单个纯文本 `QTableWidget`,右侧 460 px 不固定。
- 空态仍替换掉表头/分页,或普通加载仍插入全宽 Banner。
- 只读与 viewOnly 仍复用同一个居中 Tabs Dialog。
- 编辑/预约仍不是 60% 右贴边、全高、固定 footer 的 Drawer。
- 病历仍由一串同形态 `QPlainTextEdit` 组成。
- 医生、日期和时段仍以 ComboBox 代替 radio/button/grid。
- 1024×640 下任何 footer、搜索、chip 或表格固定区被裁剪。
- 1440×900 下预约仍固定 540 px,或详情仍只有固定 880 px 居中模态形态。
- hover/focus/loading/empty/error/disabled/selected 任一规定状态没有独立截图证据。
本报告只定义视觉和交互呈现的修复边界。所有 repository、endpoint、payload、权限 code、状态门槛和 mutation reconcile 均应原样保留,并在实现后另走现有业务回归测试。
@@ -0,0 +1,164 @@
# 诊单发布后端合同独立终验
- 终验日期:2026-08-11
- 审计对象:`D:/web/zyt/app` 当前工作区终态
- 对照基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/`、其直接引用的 API,以及 `D:/web/zyt/server` 当前服务端控制器/Logic
- 审计性质:只读业务与后端合同终验;未访问网络、未安装依赖、未调用线上接口;除本报告和 pytest 临时产物外,未修改业务源码或测试
## 发布结论
**PASS(限本报告题定范围)。**
| 等级 | OPEN 数 | 结论 |
|---|---:|---|
| P0 | **0** | 未发现错误对象写入、越权入口、错误 endpoint、旧异步回调落地或主数据破坏风险 |
| P1 | **0** | 题定的所有主流程均已形成真实后端闭环 |
| P2 | **0** | 前两轮发现的 Daily 既有记录不可编辑、视频只显示首条 URL/不能逐通话追加等合同缺口均已关闭 |
**AVAILABLE 后端功能是否全部对齐:是。** 在“admin 诊单页面及其直接组件/API 中、本报告明确列出的可用后端功能”这一范围内,桌面端已经全部对齐:列表查询与分页、权限和菜单、多挂号精确取消、订单创建与付款二维码、三种详情模式、Daily 新增/编辑、真实 Notes 追加与附件、处方/业务订单、视频、归档 IM,以及 Shell owner/异步代次保护均为真实调用,无伪造成功动作。
这里的 AVAILABLE 不扩张为对 `admin` 全仓所有未枚举页面和无关路由的声明。本报告也不把后端从未提供的功能算成桌面端缺口:**任意旧笔记正文覆盖、删除整条医生笔记,以及旧跟踪备注改写/整条删除,在当前 server 快照中没有 endpoint,属于后端合同限制,不计入 P0/P1/P2。**
## 合同终验矩阵
| 题定能力 | 状态 | admin / 后端合同 | 桌面端终态 |
|---|---|---|---|
| 诊单列表筛选、分页、权限、菜单 | **CLOSED** | `tcm.diagnosis/lists`;canonical 权限;服务端动态菜单 | 筛选字段、待分配宽搜、分页总数/末页回退、统计计数请求、菜单裁剪均对齐 |
| 多挂号精确取消 | **CLOSED** | `POST doctor.appointment/cancel {id}` | 每张挂号卡携带精确 appointment id,确认前后重验同一诊单和可取消状态 |
| 创建订单后生成付款二维码 | **CLOSED** | `order.order/create``order_no``tcm.diagnosis/generateOrderQrcode` | 已消费真实 `order_no`,即时请求 QR;缺 order_no / qrcode_url 时 fail-closed,可重试且有 selection/generation 防串单 |
| edit / viewOnly / readonly | **CLOSED** | edit、viewOnly 用 `detail`;独立 readonly 用 `readonlyDetail`;保存用 `edit` | mode dispatcher 与 admin 一致,viewOnly 不误打 readonly endpoint |
| Daily 新增与既有血糖/饮食/运动编辑 | **CLOSED** | 三类 `add` / `edit` endpoint | 已有单元格解析 record id 并走 update;新增走 add;权限、DTO、owner/generation 校验完整 |
| Notes 真实新增与附件 | **CLOSEDAVAILABLE** | `addDoctorNote``doctorNotes``deleteDoctorNoteImage` | 正文真实追加;舌象/报告先上传得到服务端 URI 再新增;单附件真实删除 |
| 处方与业务订单 | **CLOSED** | `tcm.prescription/add``listByDiagnosis``tcm.prescriptionOrder/lists` / `detail` 等 | 真实开方、历史处方查看、诊单上下文订单列表/详情与诊次偏移均接通 |
| 视频全量回放与逐通话上传 | **CLOSED** | `getCallRecords``createManualCallRecord``attachLocalCallRecording` | 完整消费 `recording_urls_list`;工具栏上传可建手工记录;行内上传传精确 `call_record_id` |
| IM 归档读取与同步 | **CLOSED** | `only_archived=1``triggerImChatSync` | 首次及重载均固定归档读取;真实触发同步并按 media generation 刷新 |
| owner resize / generation | **CLOSED** | UI 生命周期合同 | 每次打开重绑真实 Shelloverlay 同步 Move/Resize/Show/Close;宽屏 60%、窄屏全宽;关闭使全部异步代次失效 |
## 逐项证据
### 1. 诊单列表:筛选、分页、权限与菜单
- admin 的筛选模型及待分配宽搜在 `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:773-917`;待分配有关键词时只保留分页、`pending_assign=1``pending_assign_keyword`,没有关键词时带月份。
- 桌面端 `src/doctor_workstation/ui/pages/consultations.py:1566-1602` 发送 canonical 字段:`keyword``has_appointment``diagnosis_confirmed``diagnosis_type``syndrome_type``assistant_id`、最新挂号日期/渠道、最新分配日期、`appointment_date``pending_booking``completed_appointment``pending_assign``sort_unserved_days`。待分配宽搜规则与 admin 一致。
- `consultations.py:1604-1653``page/page_size` 传入 repository,消费真实 `total`,超末页时回退并重查;每页仅接受 15/20/30/40。`:1672-1741` 的日期/待挂号/已完成/待分配计数沿用同一筛选语义。
- production repository 在 `src/doctor_workstation/services/repository.py:1562-1589` 调用 `GET tcm.diagnosis/lists`,不丢弃当前 canonical 查询字段。
- `src/doctor_workstation/ui/shell.py:46-254``tcm.diagnosis/lists` 作为诊单导航权限,识别 `tcm/diagnosis``tcm/diagnosis/index`;生产菜单以服务端 visible/enabled/sort/children 为事实源,同时还必须通过精确 slash 权限。空生产菜单不会静默回退出诊单入口,demo 才允许本地回退。
- 页面动作同样使用 exact slash 权限并同时检查 repository capability;没有把 dot 形式或相似字符串当成授权。
结论:查询 DTO、统计 DTO、分页和动态菜单/动作门禁均与 admin 的实际合同对齐。
### 2. 多挂号精确 `doctor.appointment/cancel`
- admin 从 `D:/web/zyt/admin/src/api/doctor.ts:60` 调用 `/doctor.appointment/cancel`,诊单列表在 `index.vue:1820``:1847` 传具体挂号 `id`
- 桌面端按嵌套挂号数组生成多个操作;歧义的行级“取消挂号”仅在唯一可取消挂号时成立。`consultations.py:2040-2107` 会:
1. 按传入的精确 appointment id 查找状态可取消的挂号;
2. 绑定当前 diagnosis
3. 用户确认后再次从当前模型按同一 id 重验;
4. 最终只把该 id 传给 diagnosis 专用取消方法。
- `repository.py:1550-1555` 明确调用 `POST doctor.appointment/cancel {"id": appointment_id}`,没有误走患者工作区的 `firstvisit.myPatient/cancelAppointment`
结论:同一诊单有多条挂号时不会取消错行,超 32 位 Python 整数也不会因 UI 转换而截断;本项 CLOSED。
### 3. 创建订单 → `order_no` → `generateOrderQrcode`
- admin 的完整链在 `D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:1478-1499``orderCreate` 成功后读取 `res.order_no`,随后请求 `generateOrderQrcode({order_no})`
- 桌面端 `consultations.py:2279-2329` 只有在 `tcm.diagnosis/order` 权限以及 create/QR 两个 capability 都存在时开放入口。
- `:2331-2479` 已完整串联结果:读取顶层或 `data.order_no`,创建 QR 对话框并立即请求真实 QR;缺少 `order_no` 不会伪造二维码,缺少 `qrcode_url` 显示失败;retry、当前 diagnosis/patient、权限与独立 order generation 均受保护。
- `repository.py:2035-2076` 精确调用 `order.order/create`,返回 mapping;再调用 `tcm.diagnosis/generateOrderQrcode {"order_no": ...}`,并强制要求非空 `qrcode_url`
结论:此前“订单已创建但付款 QR 闭环中断”的 P1 已关闭。
### 4. edit、viewOnly、readonly endpoint 分流
- admin edit 与 viewOnly 共用 `tcmDiagnosisDetail``D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1263-1322`;保存调用 `tcmDiagnosisEdit``:1367`)。独立 readonly 页调用 `diagnosisReadonlyDetail``readonly.vue:155``:231`
- API 定义分别是 `GET /tcm.diagnosis/detail``POST /tcm.diagnosis/edit``GET /tcm.diagnosis/readonlyDetail``D:/web/zyt/admin/src/api/tcm.ts:9-11``:50-52``:60-62`
- 桌面端 mode resolver 在 `src/doctor_workstation/ui/dialogs/diagnosis.py:1427-1544`edit 与 viewOnly 均调用普通 detail;仅独立 readonly 调用 readonlyDetail。`repository.py:1591-1626` 实现同一分流和 edit 保存。
结论:viewOnly 不再误用权限感知 readonly endpoint;三种模式 CLOSED。
### 5. Daily:新增及既有血糖/饮食/运动编辑
- admin `DailyMatrix.vue:586-648` 按日期聚合记录并保留可编辑 id`:921-1013` 点击已有单元格时载入记录,提交时按 id 在 add/edit 之间分流。
- admin API 的现存合同为:
- `tcm.bloodRecord/add` / `edit``D:/web/zyt/admin/src/api/tcm.ts:114-120`
- `tcm.dietRecord/add` / `edit``:146-152`
- `tcm.exerciseRecord/add` / `edit``:173-179`
- 桌面端 Daily matrix 已按同样规则合并同日血糖字段,并给已有血糖、饮食、运动单元格保留 record id;`diagnosis.py:2234-2338` 分别打开新增/编辑器,并按 id 调用 add 或 update。
- production repository 在 `repository.py:1758-1818` 精确实现上述六个 endpoint;编辑器保留饮食/运动既有图片,mutation 完成只在 diagnosis/patient 和 daily generation 仍匹配时刷新。
结论:此前“只能新增、不能编辑已有三类记录”的 P2 已关闭。跟踪备注仍按后端设计只允许当天追加,见后端限制章节。
### 6. Notes:真实新增、上传与单附件删除
- admin `D:/web/zyt/admin/src/api/patient.ts:20-40` 只有三条医生笔记 API`addDoctorNote``doctorNotes``deleteDoctorNoteImage`
- 桌面端 `diagnosis.py:2424-2511` 的正文新增真实调用 `add_doctor_note`;舌象/报告先调用 multipart `upload_material`,取得服务端 URI 后再把 URI 交给新增接口;单附件删除携带 note id、附件类别和服务器路径。
- production repository 在 `repository.py:906-985` 实现上传以及 `doctor.appointment/addDoctorNote``doctor.appointment/doctorNotes``doctor.appointment/deleteDoctorNoteImage`,并拒绝把本地路径当成已上传附件提交。
- 所有按钮同时受 exact permission 和 capability 裁剪,viewOnly/readonly 不出现可写入口。
结论:当前后端实际 AVAILABLE 的 Notes 能力全部对齐;没有用本地假数据或“只 toast 不落库”模拟新增/附件操作。
### 7. 处方与诊单上下文订单
- 详情页处方 Tab 会查询 `tcm.prescription/listByDiagnosis`,编辑态“开方”打开真实处方编辑器并调用 `tcm.prescription/add`;历史处方查看使用该真实列表返回的处方数据打开详情对话框。
- 订单 Tab 使用 `tcm.prescriptionOrder/lists` 并带诊单上下文,详情调用 `tcm.prescriptionOrder/detail`;诊次统计偏移调用真实 `setRevisitSlotStartOffset`
- production endpoint 证据:`repository.py:1131-1149``:1220-1265``:1633-1645`UI 调度与 capability 门禁在 `diagnosis.py:2048-2058``:2534-2689``:2791-3186`
结论:题定的处方创建/查看及业务订单列表/详情/偏移均为真实后端行为,未发现伪动作或无权限仍可提交。
### 8. 视频:全量 `recording_urls_list` 与逐 `call_record_id` 上传
- admin `CallRecordPanel.vue:32` 把完整 `row.recording_urls_list` 传给播放组件;`:150-177` 支持“上传后新建手工通话记录再绑定”,也支持向指定现有记录追加。
- 桌面端 `diagnosis.py:2899-2967` 会归一化并去重 direct URL 与完整 `recording_urls_list`,逐一提供播放入口,不再只取数组第一个元素。每个有正 id 的行提供“追加回放”,闭包携带该行精确 `call_record_id`
- `diagnosis.py:2587-2613` 将可选精确 id 交给 repository。`repository.py:1875-1943` 的顺序与 admin 一致:先上传;无 id 时创建 manual call record;最后调用 `attachLocalCallRecording`。已有 id 时不创建新记录,直接绑定该 id。
结论:此前“只展示首条 URL、不能逐通话记录追加”的两个 P2 缺口均已关闭。
### 9. IM`only_archived` 与 sync
- admin `ImChatRecordPanel.vue:148-185` 固定 `only_archived: 1` 并调用 `triggerImChatSync`
- 桌面端 lazy query 在 `diagnosis.py:2084-2097` 固定 `only_archived=True`,同步动作在 `:2615-2627` 调用真实 repository,并在当前 media generation 仍有效时重载。
- `repository.py:1945-1965` 精确发送 `GET tcm.diagnosis/getImChatMessages {diagnosis_id, only_archived: 1}``POST tcm.diagnosis/triggerImChatSync`
结论:未混入 live merge 默认值,归档读取和同步闭环 CLOSED。
### 10. Shell owner、resize 与 generation
- `diagnosis.py:1325-1413` 每次打开/显示都从当前 `parentWidget().window()` 重绑真实 owneroverlay 采用 owner 的全局原点与完整尺寸,drawer 在宽屏取 owner 60%,窄屏取全宽,并响应 owner Move/Resize/Show/Close。
- `diagnosis.py:1731-1760` 在 reject/done/owner close 时统一递增 detail、save、orders、order detail、daily、notes、media、offset 及所有 lazy-tab generation,并关闭相关子对话框/播放器。
结论:移动或缩放真实 Shell 后几何会立即同步;关闭后旧请求结果不能回写到下一次打开的诊单。
## 后端合同限制:旧笔记不能任意覆盖或整条删除
本项进行了 admin API、controller、Logic 和 `D:/web/zyt/server` 全仓关键词交叉检索,当前 server 快照的结论明确:
1. `D:/web/zyt/server/app/adminapi/controller/doctor/AppointmentController.php:168-198` 仅公开 `addDoctorNote``doctorNotes``deleteDoctorNoteImage`,随后该 controller 结束;不存在医生笔记正文 edit/update 或整条 delete action。
2. `D:/web/zyt/server/app/adminapi/logic/doctor/DoctorNoteLogic.php:14-76``addOrAppend``diagnosis_id + 当天` find-or-create,并把新正文追加到当日内容,不允许任意覆盖历史正文。
3. 同一 Logic 的唯一删除逻辑 `deleteImage``:114-136`)只从 `tongue_images``report_files` 数组中删除一个附件,不删除笔记行。
4. admin API `D:/web/zyt/admin/src/api/patient.ts:20-40` 也只有上述三条接口,没有被桌面端漏接的隐藏 edit/delete API。
5. 跟踪备注同样只有 `addTrackingNote``trackingNotes` controller action`DiagnosisController.php:183-214`);`TrackingNoteLogic.php:23-95` 只有按当天追加和按诊单读取,没有历史正文覆盖或整条删除。
因此:
- “任意覆盖既有医生/跟踪笔记正文”与“删除整条笔记”是**后端合同限制**,不是客户端 P0/P1/P2。
- 当前桌面端不展示无法兑现的按钮是正确的 fail-closed 行为。
- 若产品必须新增这些能力,应先在 server 增加独立 authenticated endpoint、权限码、归属校验、审计记录、并发/版本策略和删除语义;在此之前不应由客户端通过追加接口模拟覆盖或删除。
## 离线验证
在当前 `.venv` 下使用 `QT_QPA_PLATFORM=offscreen``PYTHONDONTWRITEBYTECODE=1`,禁用 pytest cache,并把 basetemp 放到 `artifacts/pytest_release_contract_audit/`;未使用网络:
```text
python -m pytest -q -p no:cacheprovider --basetemp artifacts/pytest_release_contract_audit tests/test_diagnosis_detail_contract.py tests/test_consultations_parity_ui.py tests/test_diagnosis_drawer_visual.py tests/test_diagnosis_index_visual.py tests/test_shell_contract.py tests/test_repository_parity.py tests/test_permissions.py
118 passed
```
退出码为 0。该结果覆盖 endpoint/DTO、权限裁剪、多挂号精确取消、订单 QR 串联、三种详情模式、Daily edit、Notes、视频多 URL/逐 call id、IM 和 owner/generation 的定向回归。测试通过是回归信号;最终 AVAILABLE 判定同时依赖上面的 admin/API/server 静态合同核对,不以 mock 测试单独代替生产合同证据。
## 发布门槛判断
- **业务/后端合同门槛:通过。**
- **OPENP0 0 / P1 0 / P2 0。**
- **AVAILABLE:题定范围内全部对齐。**
- **唯一保留项:旧医生/跟踪笔记正文覆盖与整条删除需先扩展后端合同;这是明确的 server limitation,不阻断“当前 AVAILABLE 功能”发布。**
@@ -0,0 +1,173 @@
# Diagnosis 发布视觉终验
审计日期:2026-08-11
审计性质:独立只读终验;除新增本报告外,未修改业务源码、测试、renderer 或 PNG。
基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/` 及其当前主链直接依赖组件。
当前实现:PySide6 + scoped QSS + QPainter,主要位于 `src/doctor_workstation/ui/`
Trellis`D:/web/zyt/app/.trellis` 当前不存在(`Test-Path``False`),因此无可补读的 Trellis workflow/spec。
## 最终结论
**PARTIAL,不是 PASS。**
- 检查了 `artifacts/diagnosis_visual/` 当前全部 **49 张 PNG**,均以原始尺寸逐张目检;尺寸分布为:`1024x640` 22 张、`1280x800` 11 张、`1440x900` 12 张、`640x540` / `650x620` / `820x560` / `920x780` 各 1 张。
- PNG 判定:**40 PASS / 9 PARTIAL / 0 不可读 / 0 硬裁切**。
- **P00 OPEN。** 诊单列表、右固定 460 px 区、付款二维码、edit/viewOnly 的 60% Drawer、独立 readonly、预约 Drawer、固定 footer、loading/error/empty/focus/permission 状态及 Shell 183/50/40 均未发现发布阻断。
- 上一轮明确 OPEN 中,Daily 动作行裁切与按钮语义、完整更多菜单的图标/danger/“二维码”文案已经闭合;视频的多回放、逐条追加与应用内播放器能力也已经存在。
- 仍不能给 PASS 的原因是 4 组 P1 同型差异:Notes 不渲染真实缩略图;处方编辑器仍是另一种 tabbed modal;订单详情仍是精简 640x540 表单而非共享 80% Drawer;视频仍从表格跳到独立播放器且聊天图片仍降级为文字链接。另有 2 组 P2 文案/局部工具栏差异。
严重度口径:P0 = 核心流程不可见/不可操作或窗口级裁切;P1 = 本次明确要求的主链组件结构、信息层级或媒体形态未与基准同型;P2 = 功能成立但文案、局部按钮或说明层级未精确对齐。
## 此前 OPEN 复核
| 项目 | 当前状态 | 精确证据 |
|---|---|---|
| Daily 裁切 | **CLOSED** | `diagnosis_state_daily_1024x640.png` 中“刷新”已换到下一行且完整可见;当前用 `FlowLayout``src/doctor_workstation/ui/diagnosis_drawer.py:1607-1609`,基准允许换行:`D:/web/zyt/admin/src/views/tcm/diagnosis/components/DailyMatrix.vue:1094-1109`。 |
| Daily 按钮语义 | **CLOSED(主问题)** | `+血糖` 为 primary`+饮食/+运动/+备注` 为 secondary,范围 checked 态可见,`新增待办` 为 primary;实现:`diagnosis_drawer.py:1613-1659,1691-1702`;基准:`DailyMatrix.vue:25-35``DiagnosisTodoList.vue:3-19`。仍有下述 P2 文案/局部刷新差异。 |
| 完整更多菜单 | **CLOSED** | `diagnosis_full_menu_1280x800.png` 显示 8 项、每项图标、删除分隔线与 danger 红、精确“二维码”。QPainter 图标与 danger 绘制:`src/doctor_workstation/ui/diagnosis_index_widgets.py:74-175`;动作与文案:`:1478-1556`;基准:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:379-400`。 |
| Notes 动作与附件 | **PARTIAL** | 新增/追加、舌象/报告上传、打开与单附件删除均已出现;但舌象始终是 64x64 的“舌象/查看”文字按钮(`diagnosis_drawer.py:2153-2183`),没有像基准那样用真实 URL 渲染 `el-image` 缩略图(`D:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:52-69,370-379`)。 |
| 处方入口 | **CLOSED** | `diagnosis_state_empty_1024x640.png` 中按钮已经是 primary “开方”;实现:`src/doctor_workstation/ui/dialogs/diagnosis.py:1012-1031`;基准:`CaseRecordList.vue:1-38`。编辑器结构仍有 P1,见下节。 |
| 订单列表/偏移/详情 | **PARTIAL** | 列表、金额色、状态 tag、分页、详情入口均存在(`diagnosis.py:1056-1099,2980-3010`),并补了详情 PNG;但详情结构仍非基准共享 Drawer,偏移文案也未精确。 |
| 视频多回放 | **CLOSED(能力)/ PARTIAL(同型)** | `diagnosis_state_video_replay_1024x640.png` 同一记录显示主回放 + 2 个备用回放;实现去重并逐条建链接:`diagnosis.py:2899-2952`。 |
| 视频逐条上传 | **CLOSED** | `diagnosis_state_video_upload_action_1024x640.png` 显示行级“追加回放”;绑定精确 `call_record_id``diagnosis.py:2953-2967`,上传调用:`:2587-2613`。 |
| 视频播放器 | **CLOSED(能力)/ PARTIAL(同型)** | `diagnosis_state_video_player_820x560.png` 显示应用内播放面、播放/进度/时间/安全外开;实现:`src/doctor_workstation/ui/diagnosis_media.py:38-172`。但基准播放器内嵌在表格单元格且最大高 180 px:`RecordingPlaybackBlock.vue:1-30``RecordingVideoPlayer.vue:1-38,320-329`。 |
| Chat info/归档 | **CLOSED(主体)/ PARTIAL(图片)** | `diagnosis_state_chat_archive_1024x640.png` 已有完整 info、only_archived、同步/重载与左右气泡;实现:`diagnosis_drawer.py:2235-2360`。图片消息仍只创建“查看图片”链接(`:2329-2348`),基准直接渲染图片缩略图(`ImChatRecordPanel.vue:39-50,272-276`)。 |
| Shell | **CLOSED** | 两张 Shell PNG 均为 183/50/40;当前源码:`src/doctor_workstation/ui/shell.py:540,602,686`;基准:`D:/web/zyt/admin/src/config/setting.ts:5``D:/web/zyt/admin/src/styles/var.css:8``D:/web/zyt/admin/src/layout/default/components/header/multiple-tabs.vue:65`。 |
## P0 / P1 / P2
### P0
**0 项。** 49 张图未发现核心信息被窗口边界硬裁、60% Drawer 尺寸失效、footer 随正文滚走、中文/图标缺字、二维码不可辨识或 1024/1440 壳层错位。
### P1-1 Notes 舌象不是实际缩略图
- 实图:`diagnosis_state_notes_actions_1024x640.png` 中舌象为蓝色文字卡“舌象 / 查看”,不是图像;报告文件 chip 和两个删除入口可见。
- 当前源码明确只创建 `QPushButton("舌象\n查看")`,没有加载 URL 到 pixmap`src/doctor_workstation/ui/diagnosis_drawer.py:2153-2183`
- 基准用 `el-image :src="getImageUrl(url)"`、64x64 cover 缩略图与叠加 CircleClose`D:/web/zyt/admin/src/views/patient/reception/components/NoteTimeline.vue:52-69,366-379`
- 建议边界:保留当前安全打开与单附件删除合同,只把可安全下载/解码的图片异步渲染为 64x64 缩略图;失败时再回退为当前文字卡。
### P1-2 处方编辑器不是基准的 1200 px 连续 Drawer
- 实图:`diagnosis_state_prescription_editor_920x780.png` 是 920x780 modal,按“患者与诊断 / 药材配方 / 剂型与用法 / 医师签名”拆为 4 个 tabs。
- 当前结构:`src/doctor_workstation/ui/dialogs/prescription.py:1017-1037`;诊单入口调用该 dialog`src/doctor_workstation/ui/dialogs/diagnosis.py:2533-2585`
- 基准入口直接打开 `tcm-prescription`,其根是 `size="1200px"` Drawer,并在同一滚动流中依次呈现患者信息、诊断信息、RP 药材工具栏/网格、用法和签名:`D:/web/zyt/admin/src/views/tcm/diagnosis/index.vue:416,744``D:/web/zyt/admin/src/components/tcm-prescription/index.vue:2-216,217-531`
- 当前 DTO/真实提交能力存在,本项是视觉结构 P1,不是功能缺失。
### P1-3 订单详情仍是精简 modal
- 实图:`diagnosis_state_order_detail_640x540.png` 只有 hero、状态 tag 和 10 行键值信息。
- 当前源码固定 `640x540`,用单个 `QFormLayout` 罗列患者、金额、医护、地址、物流、备注:`src/doctor_workstation/ui/dialogs/diagnosis.py:3060-3135`
- 基准 `PatientOrderList` 打开共享 readonly `PrescriptionOrderDetailDrawer``D:/web/zyt/admin/src/views/tcm/diagnosis/components/PatientOrderList.vue:96-124`;共享组件宽 `80%`,readonly 仍保留金额概览、处方详情、未关联/关联收款、履约收货、物流轨迹与操作日志:`D:/web/zyt/admin/src/views/consumer/prescription/components/PrescriptionOrderDetailDrawer.vue:13-16,154-197,200-545,551-824`
- 因结构和信息密度均明显不同,新增截图只能证明“有详情”,不能证明“同型”。
### P1-4 视频与聊天媒体形态仍非基准同型
- 视频:多地址、逐条追加和 QtMultimedia 播放能力均真实存在,但表格只显示文字链接,点击后打开 820x560 独立播放器(`diagnosis.py:2934-2978``diagnosis_media.py:38-116`)。基准在“录制回放”单元格内直接嵌入最大高 180 px 的播放器,并把其他地址放在其下:`CallRecordPanel.vue:17-34``RecordingPlaybackBlock.vue:1-30``RecordingVideoPlayer.vue:320-329`
- 聊天:文字/文件气泡和 info alert 已同型;图片仍为“查看图片”文字链接(`diagnosis_drawer.py:2329-2348`),基准为最大 `240x200` 的可预览图片(`ImChatRecordPanel.vue:39-50,272-276`)。
- 两者均可安全使用,但尚未达到这轮明确要求的媒体视觉同型。
### P2-1 Daily 待办局部工具栏文案仍未精确
- `diagnosis_state_daily_lower_1024x640.png` 的 primary 按钮为“新增待办”,基准为“+ 新增待办”;当前待办卡内也没有基准同一工具栏上的局部“刷新”(全 Daily 顶部已有刷新)。
- 当前:`diagnosis_drawer.py:1691-1702`;基准:`DiagnosisTodoList.vue:3-19`
- 这不再造成裁切或语义压平,故降为 P2。
### P2-2 订单偏移栏文案与解释层级未精确
- `diagnosis_state_order_offset_1024x640.png` 显示“全局就诊序号偏移 / 0 / 个序号 / 保存偏移”。
- 基准是“复诊统计起始偏移”+ tooltip +“第 1 笔实单计为…”+“保存”:`PatientOrderList.vue:7-41`
- 当前:`src/doctor_workstation/ui/dialogs/diagnosis.py:1056-1083`。数值范围与保存入口成立,本项为文案/说明层级 P2。
## 尺寸与壳层精确证据
| 核对项 | 1024x640 | 1440x900 | 源码/基准 |
|---|---|---|---|
| edit/viewOnly Drawer 60% | panel 从 x=410 开始,宽 614x=409 仍为 overlay | panel 从 x=576 开始,宽 864 | `diagnosis.py:1336-1340`;基准 `edit.vue:3-9` |
| appointment Drawer 60% | panel 从 x=410 开始,宽 614 | panel 从 x=576 开始,宽 864 | `appointment_drawer.py:1716-1719`;基准 `appointment.vue:2-6` |
| Shell sidebar | x=0..182 为侧栏,x=183 进入内容;抽样 x182=`#25292D`、x183=`#F6F6F6` | 同一 183 px 边界 | `shell.py:540-543`;基准 `setting.ts:5` |
| Shell topbar/tabs | x=200 处 y49 为 topbar 底边,y50 进入 tabsy89 为 tabs 底边,y90 进入内容,即 50 + 40 | 同一分段 | `shell.py:602,686`;基准 `var.css:8``multiple-tabs.vue:65` |
## 49 张 PNG 逐张判定
下列 PASS 只证明该张图实际入镜内容;不会拿截图外的滚动内容代替证据。
### 诊单列表与二维码(14
| PNG | 判定 | 目检结论 |
|---|---|---|
| `diagnosis_1024x640.png` | PASS | 窄屏筛选换行、内部横向溢出、右固定区与行语义完整,无硬裁切。 |
| `diagnosis_1440x900.png` | PASS | 宽屏列密度、挂号复合单元格、斑马/业务色与固定操作区成立。 |
| `diagnosis_advanced_filters_1280x800.png` | PASS | 高级筛选完整展开,日期范围、控件顺序与重置可见。 |
| `diagnosis_double_appointment_cancel_1280x800.png` | PASS | 同诊单两条挂号各有取消入口,虚线层级正常。 |
| `diagnosis_empty_1280x800.png` | PASS | 表头、内部空态、横向滚动与 0 条分页均保留。 |
| `diagnosis_error_1280x800.png` | PASS | 错误态居中可读,无旧数据穿透。 |
| `diagnosis_focus_1280x800.png` | PASS | 搜索框 focus ring 清楚且不挤压布局。 |
| `diagnosis_full_menu_1280x800.png` | PASS | 8 项、8 个图标、删除分隔/danger 与精确“二维码”全部可见。 |
| `diagnosis_horizontal_scroll_1024x640.png` | PASS | 主表横移时右 460 px 固定区不动,阴影连续。 |
| `diagnosis_hover_warning_1280x800.png` | PASS | warning 行 hover 渐变、3 px 左条和固定区同步。 |
| `diagnosis_loading_1280x800.png` | PASS | 表内真实圆弧 spinner,内容淡化,无字符 spinner。 |
| `diagnosis_pending_assign_1280x800.png` | PASS | 待分配 chip、月份和宽搜索在同一流式区。 |
| `diagnosis_permissions_cropped_1280x800.png` | PASS | 权限裁掉批量工具栏和敏感动作,仅留查看。 |
| `diagnosis_order_qrcode_1280x800.png` | PASS | 订单号、可辨识 QR、成功态、只读 URL、打开/重试/关闭均完整。 |
### edit / viewOnly / readonly6
| PNG | 判定 | 目检结论 |
|---|---|---|
| `diagnosis_edit_1024x640.png` | PASS | 60% Drawer、2/3 列语义表单、横向 tabs、独立滚动体与固定 footer 成立。 |
| `diagnosis_edit_1440x900.png` | PASS | 864 px Drawer、宽屏字段比例和 footer 稳定。 |
| `diagnosis_viewonly_1024x640.png` | PASS | 60% 只读 Drawer、待配药警告、disabled 表单与仅关闭 footer 成立。 |
| `diagnosis_viewonly_1440x900.png` | PASS | 宽屏只读栅格、全 tabs 与固定 footer 成立。 |
| `diagnosis_readonly_1024x640.png` | PASS | 独立只读页、hero、病例卡和“客服:赵医助”不拆字。 |
| `diagnosis_readonly_1440x900.png` | PASS | 4 列病例密度、异常指标层级及长页面滚动成立。 |
### 详情状态与子流程(19
| PNG | 判定 | 目检结论 |
|---|---|---|
| `diagnosis_state_loading_1024x640.png` | PASS | 圆弧 spinner 与“正在加载诊单...”完整,无旧 seed 穿透。 |
| `diagnosis_state_error_1024x640.png` | PASS | danger banner、重试、空表单和禁用保存完整。 |
| `diagnosis_state_empty_1024x640.png` | PASS | 处方空态与 primary “开方”已对齐;编辑器另见 PARTIAL。 |
| `diagnosis_state_permission_1024x640.png` | PASS | 仅保留获权 tabs,手机号/身份证掩码,footer 仅关闭。 |
| `diagnosis_state_daily_1024x640.png` | PASS | Daily 动作全部可见;刷新换行而未裁切,range/primary/secondary 层级清楚。 |
| `diagnosis_state_daily_lower_1024x640.png` | PARTIALP2 | 趋势、待办 filters、状态 pill 与 primary 新增成立;缺“+”及待办局部刷新。 |
| `diagnosis_state_daily_blood_edit_650x620.png` | PASS | 血糖/血压编辑字段、单位、必填说明、备注和固定动作完整。 |
| `diagnosis_state_focus_1024x640.png` | PASS | 姓名 focus ring、紧凑 close 和固定 footer 清楚。 |
| `diagnosis_state_save_loading_1024x640.png` | PASS | 按钮内圆弧 spinner 与“正在保存”完整。 |
| `diagnosis_state_save_success_1024x640.png` | PASS | 绿色勾与“保存成功”完整。 |
| `diagnosis_state_save_failure_1024x640.png` | PASS | 红色叉与“保存失败”完整。 |
| `diagnosis_state_notes_actions_1024x640.png` | PARTIALP1 | 动作、chip 和单附件删除存在;舌象仍为文字按钮而非真实缩略图。 |
| `diagnosis_state_prescription_editor_920x780.png` | PARTIALP1 | 完整可用的 4-tab 编辑器已入镜;但不是基准 1200 px 连续 Drawer。 |
| `diagnosis_state_order_offset_1024x640.png` | PARTIAL(P2) | 数值、保存、订单表与分页成立;偏移文案/tooltip/解释层级未精确。 |
| `diagnosis_state_order_detail_640x540.png` | PARTIALP1 | 详情 modal 可用;缺基准 80% Drawer 的金额/支付/履约/物流/日志结构。 |
| `diagnosis_state_video_replay_1024x640.png` | PARTIALP1) | 主回放 + 2 个备用回放成立;仍是链接列表,不是表内播放器。 |
| `diagnosis_state_video_upload_action_1024x640.png` | PARTIAL(P1) | 指定行“追加回放”成立;媒体单元格结构仍未同型。 |
| `diagnosis_state_video_player_820x560.png` | PARTIAL(P1) | 应用内播放器和安全外开成立;形态是独立 dialog,不是 180 px 表内播放器。 |
| `diagnosis_state_chat_archive_1024x640.png` | PARTIALP1 | 完整 info、归档、同步/重载及左右气泡成立;图片消息退化为文字链接。 |
### 预约 Drawer8
| PNG | 判定 | 目检结论 |
|---|---|---|
| `appointment_drawer_1024x640.png` | PASS | 60% Drawer、body 滚动、时段状态和固定 footer 无硬裁切。 |
| `appointment_drawer_1440x900.png` | PASS | 医生 radio、四个日期按钮、时段网格、备注和 footer 对齐。 |
| `appointment_drawer_empty_doctors_1440x900.png` | PASS | warning、空态插图、暂无医生与禁用确认完整。 |
| `appointment_drawer_empty_roster_1440x900.png` | PASS | 无排班 warning、空态和“该医生暂无排班”可读。 |
| `appointment_drawer_error_1440x900.png` | PASS | 号源错误 banner、刷新入口和空态并存。 |
| `appointment_drawer_keyboard_focus_1440x900.png` | PASS | 日期按钮蓝色键盘 focus 边界清楚。 |
| `appointment_drawer_loading_1440x900.png` | PASS | 初始加载 overlay、圆弧 spinner、文案与禁用确认完整。 |
| `appointment_drawer_refreshing_1440x900.png` | PASS | 刷新 overlay、圆弧 spinner、文案与禁用确认完整。 |
### Shell2
| PNG | 判定 | 目检结论 |
|---|---|---|
| `diagnosis_shell_1024x640.png` | PASS | 183/50/40、深色侧栏、tabs、固定列与分页均成立;无 glyph 方框。 |
| `diagnosis_shell_1440x900.png` | PASS | 同一壳层比例在宽屏保持,箭头/刷新/全屏/关闭图形和中文完整。 |
## 发布判定
- **可以确认无 P0,且此前 Daily、更多菜单、Shell 的明确 OPEN 已关闭。**
- **不能给视觉同型 PASS。** 需至少关闭 P1-1 至 P1-4,并补录对应终态 PNG;P2 可在同一轮顺手精确。
- 本轮最终:**PARTIAL40 PASS / 9 PARTIAL,共检查 49 张 PNG**。
+260
View File
@@ -0,0 +1,260 @@
# 医生工作站最终发布 parity 审计
审计日期:2026-08-10
审计对象:`D:\web\zyt\app` 当前源码
唯一 Web 基准:`D:\web\zyt\admin\src\views` 及其直接引用的 `src/api``src/components``src/utils`
判定范围:医生桌面五页面(接诊台、我的患者、我的问诊、我的处方库、已开处方)及其 `core/services/video` 支撑合同。
## 1. 发布结论
**RELEASE CANDIDATE。** 原报告的 3 个 P0 与 10 个 P1 已全部关闭:
- P0`CLOSED 3 / OPEN 0`
- P1`CLOSED 10 / OPEN 0`
- 当前未发现医生桌面范围内的发布阻断项。
- 处方预约上下文、附件上传和预约 ID 语义等高风险路径均有反例测试。
- 全量测试:**140 项通过**。
- 静态检查:`ruff check src tests` 通过;`ruff format --check src tests` 显示 **53 files already formatted**
本结论是客户端源码、管理端基准和纯本地测试合同的发布复审结论;服务端生产权限、数据归属和真实第三方视频可用性仍应由部署环境验收。
## 2. OPEN / CLOSED 总表
| 编号 | 原问题 | 最终状态 | parity 结论 |
|---|---|---:|---:|
| P0-1 | 接诊附件未 multipart 上传并可能泄漏本机路径 | **CLOSED** | **EXACT** |
| P0-2 | appointment 处方查询回退 diagnosis 历史处方,缺 `case_record` | **CLOSED** | **EXACT / fail-closed 加固** |
| P0-3 | 我的患者预约误用真实患者 ID,排班 DTO 不完整 | **CLOSED** | **EXACT** |
| P1-1 | DiagnosisDialog 字段缩水、隐私与唯一性检查缺失 | **CLOSED** | **EXACT** |
| P1-2 | 预约表单缺医生排班、服务端号源和关键字段 | **CLOSED** | **EXACT** |
| P1-3 | 诊单上下文订单缺失,支付/退款字段缩水 | **CLOSED** | **EXACT** |
| P1-4 | 建单 paid-order/定金未加载或串诊单竞态 | **CLOSED** | **EXACT / fail-closed 加固** |
| P1-5 | 患者写操作 latest-wins 吞掉已执行 mutation 回调 | **CLOSED** | **EXACT** |
| P1-6 | QRunnable worker 读取 QWidget | **CLOSED** | **EXACT** |
| P1-7 | permission helper 接受点号/斜杠别名,handler 缺二次门槛 | **CLOSED** | **EXACT** |
| P1-8 | 原生医生直呼复用小程序 `videoQr` 权限 | **CLOSED** | **EXACT(客户端)** |
| P1-9 | 接诊队列固定前 50 条 | **CLOSED** | **EXACT** |
| P1-10 | 已开处方无 `readonlyDetail` 权限仍请求诊单 | **CLOSED** | **EXACT** |
## 3. P0 逐项复核
### P0-1 multipart、两阶段提交与安全路径 — CLOSED
Admin 合同:
- `D:\web\zyt\admin\src\api\file.ts:9-32``POST /upload/image|file`multipart 字段为 `file``cid`,返回 `uri/url`
- `D:\web\zyt\admin\src\components\material\picker.vue:260-283`:业务组件只接收上传后的服务端地址。
- `D:\web\zyt\admin\src\views\patient\reception\components\NoteTimeline.vue:195-225`:最终备注 DTO 为 `diagnosis_id/content/tongue_images/report_files`
- `D:\web\zyt\admin\src\api\patient.ts:20-26`:最终提交 `POST /doctor.appointment/addDoctorNote`
App 证据:
- `D:\web\zyt\app\src\doctor_workstation\services\api_client.py:134-157,172-196,249-270`:真实 multipart;上传请求不继承 JSON `Content-Type`boundary 由 `httpx` 生成。
- `D:\web\zyt\app\src\doctor_workstation\services\repository.py:796-818`:图片走 `upload/image`、文件走 `upload/file`,发送 `file``cid=0`
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:1485-1518,1520-1573`:GUI 线程先快照本地选择;worker 按顺序上传全部材料,全部成功后才提交备注。
- `D:\web\zyt\app\src\doctor_workstation\services\repository.py:825-848,1781-1831`:上传响应和最终 JSON 双层拒绝盘符路径、UNC、`file:` 与反斜杠本机路径;空 `uri/url` 也拒绝。
- `D:\web\zyt\app\tests\test_api_client.py:77-113``D:\web\zyt\app\tests\test_reception_parity_ui.py:462-544`:验证 multipart boundary、服务端 URL-only JSON 和任一上传失败时绝不提交备注。
结论:本地文件只作为上传输入,不进入 `addDoctorNote` JSON;原 P0 已关闭。
### P0-2 appointment 唯一权威与完整 `case_record` — CLOSED
Admin 合同:
- `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:1685-1694`:问诊行明确传入 `diagnosis_id``appointment_id`
- `D:\web\zyt\admin\src\components\tcm-prescription\index.vue:1850-1863`:首先请求 `GET tcm.prescription/getByAppointment {appointment_id}`
- 同文件 `:1865-1895,2251-2267`:无当前挂号处方时从诊单详情生成病例快照,保存 `diagnosis_id/appointment_id/case_record`
App 证据:
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1168-1191`:唯一查询源是 `get_prescription_by_appointment`,不存在 diagnosis 历史回退。
- 同文件 `:1193-1209,1227-1241,1300-1307`:仅空结果进入新建;查询异常直接 fail-closed;病例快照返回的 diagnosis ID 必须匹配当前行。
- 同文件 `:1315-1375`:编辑种子和最终创建 DTO 都强制覆盖当前 `diagnosis_id/appointment_id` 并深拷贝完整 `case_record`
- `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1030-1039,1118-1132`:端点分别为 `POST tcm.prescription/add``GET tcm.prescription/getByAppointment`
- `D:\web\zyt\app\src\doctor_workstation\core\models.py:581-584,653-657,728-734``Prescription` 正式反序列化/序列化 `appointment_id``case_record`
- `D:\web\zyt\app\src\doctor_workstation\services\mock_repository.py:861-870`Demo 同样只按 appointment 精确匹配。
- `D:\web\zyt\app\tests\test_consultations_parity_ui.py:239-348`:覆盖 miss、异常、禁止 diagnosis fallback、ID 与不可变病例快照。
说明:Admin 在预约查询异常处吞错后可能继续新建;App 选择更安全的异常 fail-closed,但没有改变 appointment 作为唯一处方权威的业务合同。
### P0-3 预约 ID、排班和提交 DTO — CLOSED
Admin 合同:
- `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:513-518`:预约组件的 `patient_id` 被明确覆盖为 `diagnosis_id || id`
- `D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:392-402,494-628`:加载医生、未来 7 天有效排班、服务端可用号源。
- 同文件 `:669-720`:提交 `patient_id,doctor_id,appointment_date,period,appointment_time,appointment_type,remark,channel_source,channel_source_detail`
- 端点定义:`D:\web\zyt\admin\src\api\tcm.ts:107-108``D:\web\zyt\admin\src\api\doctor.ts:19-20,50-51``D:\web\zyt\admin\src\api\first_visit.ts:120-121`
App 证据:
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:219-251,319-337`:预约上下文和今日重复检查均使用 diagnosis ID`source_patient_id` 只保留给视频。
- 同文件 `:410-486,495-585`:请求未来 7 天排班和 `availableSlots`,只接受可用、未过期、有配额的时段。
- 同文件 `:600-644`:所有必填门槛与 Admin DTO 对齐,`patient_id == diagnosis_id`;特定自媒体渠道要求补充字段。
- 同文件 `:2208-2223`:确认后通过 `book_patient_appointment` 提交完整快照 DTO。
- `D:\web\zyt\app\src\doctor_workstation\services\repository.py:722-784,1428-1433,1570-1573`:端点为 `getDoctors``doctor.roster/lists``availableSlots``firstvisit.myPatient/createAppointment`
- `D:\web\zyt\app\tests\test_patients_ui.py:276-343`:明确断言 diagnosis ID 与真实 patient ID 不同且 body 使用 diagnosis ID,并核对排班/号源 query 和完整 body。
App 额外保留 `diagnosis_id` 作为显式上下文键;服务端仍收到 Admin 所需全部字段,未再混淆 ID。
## 4. P1 逐项复核
### P1-1 DiagnosisDialog 隐私、字段 DTO 与唯一性 — CLOSED
- Admin`D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue:823-856,916-970,1013-1132,1263-1275,1331-1367`,明文受 `tcm.diagnosis/phonePlain` 控制,基础字段受服务端锁定,并调用 phone/id-card 唯一性检查。
- App`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:42-131,213-241,426-450`,接收/继承 permissions,覆盖患者基本信息、生命体征、病史、四诊和诊断字段。
- App:同文件 `:595-689`,缺权限时 phone/id-card 从 seed 到详情均 fail-closed 脱敏,且不把掩码写回服务端;`patient_basic_locked`/`can_edit_patient_basic=false` 时锁定基础字段。
- App:同文件 `:829-903`,保存前校验格式并调用 `checkPhone {phone,id}``checkIdCard {id_card,id}`,然后 `update_diagnosis`
- Repository`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1474-1509,1575-1583`
- 测试:`D:\web\zyt\app\tests\test_prescription_security_ui.py:132-191`
### P1-2 预约医生、排班、号源与字段 — CLOSED
P0-3 已给出完整证据。补充:Admin 的可提交条件在 `D:\web\zyt\admin\src\views\tcm\diagnosis\appointment.vue:377-386`App 对应 fail-closed 条件在 `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:600-644`。医生、排班日、服务端 slot、渠道缺一不可;状态 `1/4` 的今日挂号只阻止再约今天。
### P1-3 诊单上下文订单、支付与退款 — CLOSED
- Admin`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-88`,订单区只在 `tcm.diagnosis/patientOrders` 下出现。
- Admin`D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:131-178`exact query 为 `context_diagnosis_id`、可用时的 `patient_id``scene='diagnosis_edit'``page_size=10`
- App`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\diagnosis.py:506-549,551-563,764-827`,同权限、同 query、同分页,并以 diagnosis generation/target 拒绝过期响应。
- Repository`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1134-1145`,透传到 `GET tcm.prescriptionOrder/lists`
- 支付/退款:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:647-747`,支付包含 `order_type/pay_amount/pay_remark/completion_request/pay_create_type`;退款包含原因和可选 `refund_amount`
- Admin 对应:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderActionHost.vue:573-621`
- 状态门槛:App `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1687-1757,2415-2420,2520-2578` 与 Admin `D:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:89-110` 对齐。
### P1-4 paid-order 上下文竞态与定金门槛 — CLOSED
- Admin`D:\web\zyt\admin\src\views\consumer\prescription\index.vue:2307-2337,2435-2480`,诊单变化清空支付单并重新请求,创建 body 绑定 prescription/diagnosis/pay-order。
- App`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1800-1847,1945-1950`,已有处方的 diagnosis ID 锁定,初始保存禁用。
- App:同文件 `:1965-2060`,变化立即清空旧支付单/定金;响应必须同时匹配 generation、请求 diagnosis、当前 diagnosis。
- App:同文件 `:2070-2139`,加载中、失败或未就绪均禁止提交;门槛开启时必须选支付单且金额不低于 `deposit_min_amount`
- Repository`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1157-1183``paidPayOrders {diagnosis_id}``prescriptionOrder/create`
- 测试:`D:\web\zyt\app\tests\test_prescription_security_ui.py:227-260`
### P1-5 患者 mutation 不丢回调 — CLOSED
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:2161-2202`:每次写操作有独立 pending token;完成顺序不会使已执行 mutation 的 success/error 回调失效。
- 同文件 `:2183-2206`:每个服务端成功都 reconcile 患者、订单、面诊进度三工作区。
- 同文件 `:1687-1757,2415-2420`handler 提交前重新核对 canonical permission 和行状态。
- `D:\web\zyt\app\tests\test_patients_ui.py:373-398`:两个 mutation 乱序完成仍触发两次 reconcile。
### P1-6 QRunnable 只接收 GUI 快照 — CLOSED
- `D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:260-323``run_async` 的函数运行在 QRunnable。
- 接诊:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:522-607`
- 患者列表/订单:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1293-1318,1587-1615`
- 处方库:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:221-247`
- 模板导入:`D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:759-780`
- 已开处方:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-540`
上述位置均在 GUI 线程先读取 `.text()``.currentData()`、日期/页码等,再向 worker 传纯 query/scalarworker 不再读取 QWidget。`D:\web\zyt\app\tests\test_reception_parity_ui.py:269-310``D:\web\zyt\app\tests\test_patients_ui.py:231-268``D:\web\zyt\app\tests\test_prescription_security_ui.py:269-334` 覆盖快照和 pending refresh。
视频后端写同样不阻塞 Qt`D:\web\zyt\app\src\doctor_workstation\video\lifecycle.py:84-153,156-304` 以每通话 FIFO daemon worker 有序执行 `start -> bind -> end`,提供最长 5 秒的有限等待;`D:\web\zyt\app\src\doctor_workstation\video\window.py:515-548` 的窗口关闭只排队结束,不同步阻塞 GUI。
### P1-7 canonical permission exact 与 handler 二次门槛 — CLOSED
- Admin`D:\web\zyt\admin\src\utils\perm.ts:3-14`,权限字符串精确匹配。
- App`D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-183`,只接受 exact、全局 `*`、资源 `prefix/*`;明确不把 `resource.action` 当成 `resource/action`
- 处方库按钮和 handlers`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:100-164,291-392`
- 已开处方查看、增改删、患者修正、审核、建单、订单列表 handlers:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:642-689,707-907`
- Canonical action 基准:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:36,85,93,102``D:\web\zyt\admin\src\views\consumer\prescription\index.vue:107,112,216,232,242,250,260`
- 测试:`D:\web\zyt\app\tests\test_prescription_security_ui.py:123-129`,仅有点号 alias 时 slash action 被拒绝。
### P1-8 视频 eligibility 与权限语义 — CLOSED(客户端范围)
- Admin`D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:390-391``tcm.diagnosis/videoQr` 只保护小程序二维码;同文件 `:1778-1780` 的问诊视频门槛为 `has_appointment && appointment_status == 1`
- App`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:166-183,1106-1111,1441-1453`,使用相同 eligibility,且 appointment/patient/diagnosis 三 ID 分离、均需有效。
- UI 原生直呼不再受 `videoQr` 门槛;测试见 `D:\web\zyt\app\tests\test_consultations_parity_ui.py:86-92,390-403`
- Remote endpoint/payload`D:\web\zyt\app\src\doctor_workstation\services\repository.py:1677-1717`,分别是签名、`startCall``bindCallRoom``endCall`
服务端仍必须最终验证通话状态和数据归属;这属于服务端安全边界,不是客户端页面缺失。
### P1-9 接诊分页 — CLOSED
- Admin`D:\web\zyt\admin\src\views\patient\reception\index.vue:212-214,296-379``PAGE_SIZE=15`、按 `total/count` 持续加载和 ID 去重。
- App`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:196-208,288-292,513-607,609-696`,同为 15 条;切 tab/搜索重置,加载更多,按 ID 合并,以 generation/query-key 拒绝旧响应。
- Repository`D:\web\zyt\app\src\doctor_workstation\services\repository.py:668-695`,透传 `page_no/page_size``GET doctor.appointment/lists`
- 测试:`D:\web\zyt\app\tests\test_reception_parity_ui.py:179-260`,覆盖跨页追加、筛选重置与 total 边界。
第 51 位之后的今日患者已可达,旧“固定前 50 条”结论失效。
### P1-10 处方进入诊单的权限与只读端点 — CLOSED
- `D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:642-659`:处方详情先要求 `cf.prescription/read`;诊单按钮另要求 `tcm.diagnosis/readonlyDetail`
- 同文件 `:661-698`handler 再次检查 canonical 权限和 diagnosis ID,明确调用 readonly endpoint;无权时不发请求。
- `D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:1668-1696`:详情按钮按同一权限 boolean 隐藏。
- `D:\web\zyt\app\src\doctor_workstation\services\repository.py:1474-1488``GET tcm.diagnosis/readonlyDetail {id}`
- `D:\web\zyt\app\tests\test_prescription_security_ui.py:335-388`:无权限不请求,并拒绝过期 target 响应。
## 5. Endpoint / DTO 最终合同
| 能力 | Endpoint | 关键 query/body | 状态 |
|---|---|---|---:|
| 接诊队列 | `GET doctor.appointment/lists` | `status,start_date,end_date,patient_name,page_no,page_size` | **EXACT** |
| 材料上传 | `POST upload/image|file` | multipart `file,cid` | **EXACT** |
| 医生备注 | `POST doctor.appointment/addDoctorNote` | `diagnosis_id,content,tongue_images,report_files`,仅服务器 URI | **EXACT** |
| 当前挂号处方 | `GET tcm.prescription/getByAppointment` | `appointment_id` | **EXACT** |
| 新建处方 | `POST tcm.prescription/add` | `diagnosis_id,appointment_id,case_record` + 完整处方字段 | **EXACT** |
| 预约医生 | `GET tcm.diagnosis/getDoctors` | 无 | **EXACT** |
| 排班 | `GET doctor.roster/lists` | `doctor_id,start_date,end_date,status,page_no,page_size` | **EXACT** |
| 可用号源 | `GET doctor.appointment/availableSlots` | `doctor_id,appointment_date,period='all'` | **EXACT** |
| 我的患者预约 | `POST firstvisit.myPatient/createAppointment` | Admin 九字段;`patient_id == diagnosis_id` | **EXACT** |
| 诊单详情/编辑 | `GET detail|readonlyDetail`; `POST checkPhone|checkIdCard|edit` | `{id}`、唯一性 DTO、完整医生相关字段 | **EXACT** |
| 诊单上下文订单 | `GET tcm.prescriptionOrder/lists` | `context_diagnosis_id,patient_id?,scene='diagnosis_edit',page_no,page_size` | **EXACT** |
| 建单支付单 | `GET tcm.prescriptionOrder/paidPayOrders` | `diagnosis_id` | **EXACT** |
| 创建业务订单 | `POST tcm.prescriptionOrder/create` | `prescription_id,diagnosis_id,pay_order_ids?` + 收货/服务/金额字段 | **EXACT** |
| 视频生命周期 | `getCallSignature/startCall/bindCallRoom/endCall` | 三 ID 分离;`call_type=2``room_id` | **EXACT(客户端)** |
`D:\web\zyt\app\src\doctor_workstation\services\repository.py`、Protocol、Remote、Demo 和五页面调用名进行复核,当前新增调用均存在;未发现不存在或错误命名的 repository 方法。关键端点合同由 `D:\web\zyt\app\tests\test_repository_parity.py:127-153,157-267,287-353` 覆盖。
## 6. 异步、刷新与目标绑定
- 列表请求均以 generation 拒绝旧响应;接诊还绑定 query-key 和 appointment target。
- 处方库/已开处方在 loading 中收到刷新会记录 pending refresh,而不是静默丢失:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:221-266``D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:499-574`
- 处方详情、诊单详情、订单详情均绑定 generation + target ID`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:608-705``D:\web\zyt\app\src\doctor_workstation\ui\dialogs\prescription.py:2198-2284`
- 问诊切行同时 invalidate prescription generation 并释放 busy`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:1085-1111`
- Demo 的问诊筛选、字典、appointment-authoritative 处方及 mutation 均有真实离线语义:`D:\web\zyt\app\tests\test_mock_repository.py:140-178,300-396`
上述原 P2 风险也已关闭;不影响本次 P0/P1 发布判断。
## 7. 动态菜单与医生桌面范围
动态菜单仍以会话 menu 为权威,并要求菜单节点与当前 canonical permission 同时满足:`D:\web\zyt\app\src\doctor_workstation\ui\shell.py:41-80,195-249`。五个本地页面之外的节点不会被错误映射;非 Demo 且服务端 menu 为空时不会自行放宽页面。
下列功能明确不属于本次医生桌面同型范围,因此不计为 MISSING:
- 患者小程序/H5、消费者端自助下单和二维码页面。
- 医助 watchCall、医助专属批量分配与其他角色工作台。
- 企业微信后台运营、素材运营、客户标签、统计/转化后台。
- 药房、财务、物流、审核员等角色的完整运营路由;医生页只保留其被授权的订单动作。
- 管理端 `videoQr` 小程序二维码能力;桌面原生直呼使用后端 ticket 与服务端最终授权。
## 8. 验证记录
执行命令与结果:
```text
.\.venv\Scripts\python.exe -m pytest -o addopts='' --tb=short
collected 140 items
140 passed in 1.49s
.\.venv\Scripts\python.exe -m pytest --collect-only -q
140 tests collected across 15 test files
.\.venv\Scripts\python.exe -m ruff check src tests
All checks passed!
.\.venv\Scripts\python.exe -m ruff format --check src tests
53 files already formatted
```
测试数量明细:`12+11+12+2+1+16+2+12+4+7+10+11+12+10+18 = 140`
## 9. 最终门槛
- **OPEN P00**
- **OPEN P10**
- **发布阻断:无**
- **最终结论:可作为医生桌面 release candidate 进入打包和部署环境验收。**
+101
View File
@@ -0,0 +1,101 @@
# 医生工作站集成审查
审查时间:2026-08-10。范围为当前工作区中的组合根、登录到主壳链路、Remote/Demo repository、权限门控、退出/视频生命周期及 PyInstaller 入口。审查只读进行;除本报告外未修改项目文件,也未发送网络请求。
## 结论
入口链 `packaging/doctor_workstation.spec -> doctor_workstation/__main__.py -> app.main()``build_repository(..., verify=...)` 的构造签名已经对齐,Remote/Demo 的主要 CRUD 与通话方法也具有兼容签名。当前仍有 4 个高严重度和 6 个中严重度集成问题;其中浏览器视频模式目前不能真正发起通话,退出时也存在通话窗口失管和 GUI 阻塞风险。
## P1(高)
### 1. 浏览器视频模式没有把一次性通话上下文交给伴随页
- `src/doctor_workstation/video/window.py:312-326` 在服务端 `start_call` 后仅执行 `webbrowser.open(location.url)``VideoCallRequest` 中的 diagnosis、目标用户和 UserSig 均未交给浏览器。
- `video_companion/src/main.ts:208-236` 只有显式调用 `window.doctorCall.start(config)` 才会初始化 SDK 并呼叫患者,页面自身不获取票据;`video_companion/src/main.ts:281-289` 只是暴露 API 和发送 ready。
- `README.md:70-77` 又把 browser 声明为正式降级路径,并要求用一次性业务票据传递上下文,和当前实现不一致。
系统浏览器不存在 Qt WebChannel,且静态 URL 连 diagnosis_id 都没有,因此页面会一直停在“等待桌面端发起”,而后端通话记录已经开始。浏览器标签关闭也无法回告 Python,`end_call` 只能等显式退出应用。应使用后端签发、单次消费且短有效期的浏览器 handoff ticketURL 中不能放 UserSig),或受认证的本机 IPC;伴随页确认接收后再调用 `start_call`,并建立可观测的结束回调。
### 2. 通话 start/bind/end 的同步 HTTP 被直接放在 Qt GUI 线程执行
- `src/doctor_workstation/video/window.py:206-290` 直接调用 repository 的 `start_call``bind_call_room``end_call`
- browser 在 `src/doctor_workstation/video/window.py:312-320` 同步 startembedded 在 loadFinished 回调 `src/doctor_workstation/video/window.py:437-456` 同步 start,在 bridge/close 回调 `src/doctor_workstation/video/window.py:485-522` 同步 bind/end。
- Remote 实现最终执行同步 `httpx` POST`src/doctor_workstation/services/repository.py:377-402`),而配置超时可达 120 秒。
- 登出和进程退出又在主线程逐个 `call.close()``src/doctor_workstation/app.py:298-315``src/doctor_workstation/app.py:409-415`)。
弱网时打开、挂断、退出都会冻结整个界面;`end_call` 抛错时 embedded 的 `closeEvent` 甚至到不了 `event.accept()`。应把生命周期写操作放入受控 worker,并使用状态机保证单次执行;退出阶段设置短上限、记录未完成结束动作,同时先关闭媒体/UI,不能让网络请求阻塞 Qt 关闭事件。
### 3. 同一 diagnosis 的重复发起会覆盖通话句柄,导致退出后仍可能保留旧视频窗口
- `src/doctor_workstation/app.py:343-356` 没有 pending/active 去重,用户可在票据请求完成前重复发起。
- `src/doctor_workstation/app.py:390-396` 以 diagnosis_id 为唯一 key 直接覆盖旧句柄;旧窗口的 destroyed 回调还会无条件 `pop` 同一个 key,可能把较新的句柄移除。
- 登出/退出只关闭字典当前仍持有的值(`src/doctor_workstation/app.py:298-305``src/doctor_workstation/app.py:409-412`)。
结果是第一个窗口可能在切回登录页后继续持有摄像头/麦克风;反向关闭旧窗口也会让新窗口失去托管。应对 diagnosis 建立 pending/active 单飞,拒绝或先可靠关闭旧通话;销毁回调必须做对象身份判断后再移除。
### 4. WebEngine 对任意来源自动授予音视频权限,且登出没有隔离/清理 profile
- `src/doctor_workstation/video/window.py:381-390` 使用 `QWebEngineView` 的共享默认 profile,没有为单次通话建立隔离 profile。
- `src/doctor_workstation/video/window.py:405-435` 在授权回调中不校验请求 origin、当前页面 URL 或通话状态,只要 feature 名含 audio/video 就 grant。
- 没有自定义 `acceptNavigationRequest`/origin allowlist;关闭时 `src/doctor_workstation/video/window.py:519-522` 仅挂断,不撤销权限或清理 Cookie、cache、local storage。
初始 URL 虽经过 HTTPS 校验,但页面后续导航不受限制;同一进程内切换账号时 WebEngine 状态也可能复用。应使用每通话或每会话的 off-the-record profile、精确 origin/path allowlist,只在活跃通话且当前主文档来源匹配时授权,并在挂断/登出时撤销权限和清理页面/profile。
## P2(中)
### 5. API code=-1 没有接入应用级会话失效处理
- `src/doctor_workstation/services/api_client.py:277-278` 会抛出 `AuthenticationExpiredError`
- worker 只把异常交给页面自己的通用 on_error(`src/doctor_workstation/ui/widgets.py:262-293`);组合根只有用户主动点击时才执行 `_logout``src/doctor_workstation/app.py:298-315`)。
- 项目中除异常定义/抛出外没有消费 `AuthenticationExpiredError` 的代码。
token 过期后主壳仍展示已加载的患者数据和旧权限快照,各页面只显示错误,用户必须手动退出。应在统一 API/worker 边界发出 session-expired 事件,由 Controller 原子地停止轮询和视频、清 token、销毁 ShellWindow 并回到 LoginWindow。
### 6. 页面和动作权限使用过宽的 OR 兜底,并忽略后端 menu
- `src/doctor_workstation/services/repository.py:111-127` 已解析 `Session.menu`,但 `src/doctor_workstation/ui/shell.py:248-260` 只按硬编码 `NAVIGATION.permissions` 注册页面,menu 从未参与判断;这与管理端“无有效 menu 即 403”的已确认行为(`research/admin_audit.md:103-109`)不一致。
- 接诊页可仅凭 `doctor.appointment/reception` 显示,但页面首先请求 `doctor.appointment/lists``src/doctor_workstation/ui/shell.py:40-47``src/doctor_workstation/ui/pages/reception.py:347-364`)。患者页可仅凭 readonlyDetail 显示,却始终请求 firstvisit lists;问诊页可仅凭 appointment lists 显示,却始终请求 diagnosis lists`src/doctor_workstation/ui/shell.py:62-75`)。
- 通知医助和视频按钮又把基础 lists 当动作权限兜底(`src/doctor_workstation/ui/pages/reception.py:321-332``src/doctor_workstation/ui/pages/consultations.py:88-96`),所以缺少 videoQr/notifyAssistant 的账号仍看到按钮。
服务端仍是最终边界,但当前 UI 会显示必然 403 的页面/动作,也可能绕过管理员对 desktop 页面入口的隐藏意图。页面应同时受后端 menu/capability 和实际列表接口权限约束;动作只接受对应动作码或经过确认的同义码,不能用 lists 兜底。
### 7. 已开处方审核状态筛选在 UI 适配层被改成 Remote 不识别的字段
- UI 传入 status`src/doctor_workstation/ui/widgets.py:205-208` 将其改写为 `audit_status`
- Remote 只在收到 `status` 时转换成后端需要的 `audit_filter=pending|passed|rejected``src/doctor_workstation/services/repository.py:284-303`),因此实际请求会原样携带数值 `audit_status`
- Demo 特意兼容了 `audit_status``src/doctor_workstation/services/mock_repository.py:374-399`),所以演示验收不会暴露生产差异。
结果是生产环境的“待审核/已通过/已驳回”筛选可能无效或被后端拒绝。应只在 repository 内做一次 UI 值到 API DTO 的映射,并给 Remote 增加 request-parameter 合同测试。
### 8. 登录 token 在 Session 校验成功前落盘,失败路径不回滚
- `src/doctor_workstation/services/repository.py:82-90` 在调用 `auth.admin/mySelf` 前就设置并持久化 token。
- mySelf 网络失败、协议错误或 code=10 时 LoginWindow 只显示错误(`src/doctor_workstation/ui/login.py:385-390`),不会调用 repository.logout。
这会在用户从未进入有效 Session 的情况下留下内存和 keyring/JSON token,后续登录还会带着该 token 请求 login/account。应先把 token 暂存在内存,完整构建并校验 Session 后再持久化;任何异常都清理 client/token store。
### 9. “记住账号”未控制 TokenStore 中的账号落盘,且持久 token 没有启动恢复闭环
- `src/doctor_workstation/services/repository.py:87-88` 每次成功账号密码登录都把 account 传给 TokenStore,与复选框无关。
- TokenStore 会把 account 写入 JSON 元数据(`src/doctor_workstation/services/token_store.py:83-108`),而 LoginWindow 的复选框只增删 QSettings`src/doctor_workstation/ui/login.py:374-379`)。
- `ApplicationController.start()` 始终显示登录页(`src/doctor_workstation/app.py:172-176`),未调用已实现的 `restore_session`;普通关闭只 close client,不清 token`src/doctor_workstation/app.py:409-415`)。
因此取消“记住账号”仍会在磁盘留下账号;成功 token 则持续保存但下次启动完全不使用。应让 remember-account 明确控制所有账号元数据,并二选一:启动时安全验证/恢复 token,或不持久化并在关闭时清除。
### 10. 当前冻结产物落后于最新 companion,构建脚本也没有执行入口 smoke test
- 最新 `video_companion/dist/index.html:8` 引用 `index-le5ZH3pL.js`(包含 roomId/bindCallRoom 桥接),现有 `dist/DoctorWorkstation/_internal/video_companion_dist/index.html:8` 仍引用旧的 `index-BrQGJzsD.js`
- 应用已经提供 `--smoke-test``src/doctor_workstation/app.py:436-451`),但 Windows 构建只检查 helper/pak/index 文件存在(`scripts/build_windows.ps1:30-43`),macOS 同样只做静态文件和 codesign 检查(`scripts/build_macos.sh:22-37`),都不启动冻结入口。
所以当前 `dist/DoctorWorkstation` 不包含刚合并的 room binding;未来即使入口 import/bootstrap 失败,构建也可能仍打印成功。交付前应重新执行 PyInstaller,并在隔离用户目录、无真实网络的环境下运行冻结程序 `--smoke-test`,同时校验退出码和日志中无未捕获异常。
## 已核对且未发现签名断点
- `ApiClient(base_url, ..., verify=...)``build_repository(..., verify=...)``app.py` 调用一致。
- Remote/Demo 的 login 均返回 `Session`;列表、接诊、处方库 CRUD、患者/问诊列表及 `get_call_ticket/start_call/bind_call_room/end_call` 的 UI 所需参数基本对齐。
- PyInstaller spec 的入口、`src` pathex、resources 和 `video_companion_dist` 目标路径与 `resources.py``_MEIPASS` 查找规则一致。
- 最新 source companion 已产生 roomId 并调用 `bind_call_room`;本报告未把此前已修复的“问诊页交换 appointment/diagnosis ID”或“未绑定 room”列为当前问题。
## 验证限制
本轮没有重新执行 Python 测试:工作区 `.venv/Scripts/python.exe` 指向当前机器上不存在的 uv Python 基础解释器;为保持只读审查,没有重建虚拟环境。现有 `research/ui_acceptance.md` 记录的最近一次完整测试为 46 passed,但上述多项是未被现有单元测试覆盖的跨层行为。
+229
View File
@@ -0,0 +1,229 @@
# 患者列表、动态菜单与权限一致性审计
审计日期:2026-08-10
管理端唯一事实来源:`D:\web\zyt\admin\src\views\**`
Python 对照范围:`D:\web\zyt\app\src\doctor_workstation\**`
## 1. 结论摘要
| 审计项 | 结论 | 摘要 |
|---|---|---|
| “患者列表”真实业务页面识别 | **EXACT** | 医生工作站语义下的真实页面是 `first_visit/my_patients/index.vue`,不是同样含“患者列表”字样的 `doctor/progress.vue`。 |
| Python 患者列表基础查询 | **PARTIAL** | 使用同一“我的患者”业务语义、相同五种状态和服务端数据范围,但只实现了主列表的子集。 |
| 患者详情与历史关联 | **MISSING** | Python 没有进入诊单编辑/只读详情链,也没有详情请求;右侧内容直接取列表行及其 `raw`。 |
| 患者订单管理、面诊进度 | **MISSING** | 管理端同一路由内的两个完整工作区在 Python 中均不存在。 |
| 五模块主导航 | **PARTIAL** | Python 解析并保存 `mySelf.menu`,登录时只判断菜单非空;Shell 随后忽略菜单树,使用固定五项加扁平权限码。 |
| 按钮权限 | **PARTIAL** | 多数已有写操作有权限门禁,但存在 canonical 权限码偏差、额外别名放行、患者动作大量缺失;处方库所有者判断存在 fail-open。 |
| 医生角色/部门数据范围 | **PARTIAL** | 患者主列表的请求未注入任意医生 ID,保持服务端权威范围;但 Python 丢弃 `extend.scope`、汇总和 ownership 模式,且缺少订单/进度工作区。 |
判定口径:
- **EXACT**:当前证据范围内,业务入口、条件、权限或数据语义一致。
- **PARTIAL**:主链存在,但字段、筛选、动作、菜单约束或范围提示不完整/不一致。
- **MISSING**:管理端已存在的业务能力,在 Python 中未找到入口或调用链。
## 2. 证据边界
1. 本报告没有读取 `D:\web\zyt\admin\src\views` 之外的管理端文件。因此可以确认 Vue 页面实际引用的 API **函数名、参数和交互**,但不能读取 `@/api/**` 的实现来反推真实 HTTP URL。
2. `views/**` 内没有 `auth.admin/mySelf` 调用或该接口返回的真实菜单 JSON。五个主页面的 `paths/component/perms/sort/is_show/is_disable` 精确菜单记录无法从本次允许范围内恢复。
3. 管理端的菜单管理页证明动态菜单记录至少分别保存“路由路径”“组件路径”“权限字符”“是否显示”“菜单状态”:`D:\web\zyt\admin\src\views\permission\menu\edit.vue:41-94,125-150`;其中隐藏菜单仍可访问的语义写在 `D:\web\zyt\admin\src\views\permission\menu\edit.vue:135-137`。因此,不能把单一扁平权限码等同于完整路由记录。
4. 对服务端是否再次执行权限/所有权校验不作推断;本报告的 P0 是**客户端授权门禁一致性**问题。
## 3. “患者列表”真实页面识别
### 3.1 主页面:`first_visit/my_patients/index.vue`
确认依据:
- 页面直接声明“一诊 / 我的患者”,并说明“患者、挂号与诊单信息按当前角色和部门数据范围展示”:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:5-10`
- 同一路由内明确包含“患者列表 / 订单管理 / 面诊进度”三个工作区:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:15-20`
- 主表由 `myPatientLists` 驱动,并异步装载诊单编辑、预约、订单和进度组件:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:330-344,404-409`
结论:Python `PatientsPage` 应对标这一页面及其子工作区,而不应只对标某张通用患者/挂号表。
### 3.2 排除项:`doctor/progress.vue`
该文件虽在表头使用“患者列表”,但它是医生面诊进度看板:
- 页面要求先选择医生,再展示该医生挂号:`D:\web\zyt\admin\src\views\doctor\progress.vue:57-88`
- 组件名为 `doctorProgress`API 为 `doctorLists``appointmentLists``D:\web\zyt\admin\src\views\doctor\progress.vue:176-182`
- 它显式传 `role_id: 1` 拉医生,并用 `doctor_id + progress_board=1` 拉挂号;注释说明不按医生/医助角色收窄:`D:\web\zyt\admin\src\views\doctor\progress.vue:408-448,460`
因此该页面是跨医生进度看板,不是“我的患者”主数据页。把它的数据范围套到患者模块会错误扩大语义范围。
## 4. 管理端患者模块真实合同
### 4.1 主列表、筛选和服务端范围
| 能力 | 管理端证据 |
|---|---|
| 关键词 | 患者姓名、手机号、助理、医生:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:23-43` |
| 状态 | 未预约、待面诊、已完成、已过号:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:346-393` |
| 挂号日期 | 今日、明日、后天、近 7 天、近 30 天、自定义区间:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:46-69,395-401` |
| 汇总 | 今日/明日/后天预约人数来自 `pager.extend.summary/dates``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:72-88,411-416` |
| 范围提示 | 顶部和表格显示 `pager.extend.scope.label``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:90-96,417` |
| 列表字段 | 患者性别年龄/脱敏手机、归属助理、预约医生及状态、预约时间、复诊次数、确认信息、诊单日期:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:105-148` |
主列表 API 符号为 `myPatientLists`,调用参数为 `keyword/status_filter/start_date/end_date/page``D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:330-336,380-408`
### 4.2 行级动作与权限码
| 动作 | 管理端权限/条件 | 证据 |
|---|---|---|
| 编辑诊单 | `tcm.diagnosis/edit` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:152,418,492-498` |
| 只读查看 | `tcm.diagnosis/readonlyDetail` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:153,419,500-510` |
| 预约/取消挂号 | `tcm.diagnosis/guahao`;取消仅状态 1/4 | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:154,179-186,420,681-696` |
| 指派/重新指派医助 | `tcm.diagnosis/assign` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:155-162,421,527-575` |
| 补全身份证 | 复用 `tcm.diagnosis/edit` | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:163-170,422,578-628` |
| 诊单二维码 | 复用挂号权限,且要求有效挂号、状态 1、医生 ID | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:171-178,630-673` |
只读诊单不是列表内的摘要面板。页面先从动态路由中查找 `meta.perms === 'tcm.diagnosis/readonlyDetail'`,再携带诊单 ID 跳转:`D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:500-510`
### 4.3 订单管理子工作区
- 数据口径明确为“当前患者范围内”的处方业务订单,且“订单创建人不参与数据归属判断”:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:3-10`
- 支持处方审核、支付单审核、履约状态、关键词与日期筛选:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:14-79`
- 汇总包含订单数、有效金额、待审核、完成/签收、拒收数和拒收率:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:82-123,289-297`
- 列表显示订单/患者/处方与诊单/金额/双审/履约/支付单/助理/医生/创建人:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:125-188`
- 数据由 `myPatientOrderLists` 分页加载:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\OrderPanel.vue:234-244,282-286`
- 订单按钮以 canonical `tcm.prescriptionOrder/*` 权限和业务状态共同判定;完整矩阵在 `D:\web\zyt\admin\src\views\first_visit\my_patients\components\order-actions.ts:32-124`,包括详情、编辑、双审及撤回、快递、补支付、完成、退款、撤回、上传药房。
### 4.4 面诊进度子工作区
- 服务端通过 `extend.schedule_mode` 决定“按本人归属”或“与排班合并”,而不是客户端按角色 ID 猜测:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:7-26,206-214`
- “本人患者”语义、近七日安排、医生聚合、候诊队列均在同一工作区:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:31-117,120-178`
- API 符号为 `myPatientProgressLists`,数据范围标签来自 `extend.scope.label`,本人患者由 `row.is_self_patient` 标识:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:185-203,263,308-309`
- 每 15 秒自动刷新:`D:\web\zyt\admin\src\views\first_visit\my_patients\components\ProgressPanel.vue:318-325`
### 4.5 详情与历史关联链
管理端患者行会进入诊单编辑或只读详情,而后继续关联:
- 只读页真实标题为“患者信息详情”,无权限/不存在时明确空态:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:4-36`
- 基本信息、完整病历、日常记录、医生备注/舌苔/报告:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:38-72`
- 业务订单、视频回放、聊天、医助指派历史、挂号历史分别按权限展示:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:74-146`
- 详情由 `diagnosisReadonlyDetail({id})` 拉取,备注另用 `getDoctorNotes({diagnosis_id})` 补齐:`D:\web\zyt\admin\src\views\tcm\diagnosis\readonly.vue:151-188,209-234`
- 编辑页还提供处方病历、订单、视频、聊天、指派与挂号记录分页签:`D:\web\zyt\admin\src\views\tcm\diagnosis\edit.vue:655-759`
- 挂号历史不是列表行快照,而是按诊单 ID 单独调用 `appointmentLists`,携带 `diag_scope_relax: 1`、最多 500 条:`D:\web\zyt\admin\src\views\tcm\diagnosis\components\AppointmentRecordPanel.vue:79-89,139-152`
- 指派历史同样按诊单 ID 调用 `tcmDiagnosisAssignLogList``D:\web\zyt\admin\src\views\tcm\diagnosis\components\AssignLogPanel.vue:47-57,102-125`
- 业务订单按 `context_diagnosis_id + patient_id + scene=diagnosis_edit` 独立分页:`D:\web\zyt\admin\src\views\tcm\diagnosis\components\PatientOrderList.vue:118-148,170-179`
## 5. Python 患者模块逐项对照
| 项目 | Python 当前实现 | 判定 | 说明 |
|---|---|---|---|
| 页面身份 | 标题“我的患者”,描述服务端授权范围:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:84-92` | **EXACT** | 对标对象正确。 |
| 状态筛选 | 五种状态一致:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:94-119` | **EXACT** | 状态值与管理端一致。 |
| 关键词 | Python 提示“姓名、手机号或诊单号”:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:99-103` | **PARTIAL** | 管理端还明确支持助理/医生。 |
| 日期与汇总 | 无日期快捷项、区间和三日汇总 | **MISSING** | 管理端证据见 4.1。 |
| 表格 | 患者、性别年龄、电话、进度、医助、最近预约、复诊:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:132-196` | **PARTIAL** | 缺预约医生独立列、确认信息、诊单日期和全部操作。 |
| 列表请求 | `patients(keyword,status,page,page_size)``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:307-326` | **PARTIAL** | 无 `start_date/end_date`;服务层正确映射 `status -> status_filter`。 |
| 服务端范围 | `list_patients` 使用 `firstvisit.myPatient/lists`,不注入医生 ID`D:\web\zyt\app\src\doctor_workstation\services\repository.py:398-412` | **EXACT** | 保持服务端权威数据范围,没有客户端越权扩大证据。 |
| `extend` | `PageResult` 完整保留 `extend``D:\web\zyt\app\src\doctor_workstation\core\models.py:590-598,618-672`;页面只读 items/total`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:328-335` | **PARTIAL** | scope、summary、dates 已被解析却未消费。 |
| 患者模型 | 覆盖主列表核心字段并保留 `raw``D:\web\zyt\app\src\doctor_workstation\core\models.py:241-318` | **EXACT** | 解析层可承载主表数据。 |
| 详情 | 选择行后直接 `_render_detail(patient)``D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:349-415` | **MISSING** | 没有诊单详情请求,不能等价于管理端只读/编辑页。 |
| 历史预约 | 从列表行 `appointments` 取最多 6 条:`D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:417-446` | **MISSING** | 管理端按诊单单独请求最多 500 条并显示更完整字段。 |
| 患者行动作 | 无编辑/只读/预约/指派/补身份证/二维码/取消挂号按钮 | **MISSING** | `D:\web\zyt\app\src\doctor_workstation\ui\pages\patients.py:1-454` 没有动作链。 |
| 订单/进度工作区 | 无 | **MISSING** | Python 只有列表与右侧摘要。 |
补充:`get_value` 会在 dataclass 属性缺失时回退到 `.raw``D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:48-66`。因此右侧摘要“可能”显示列表响应附带的额外字段,但它仍是列表快照,不是独立详情/历史合同。
## 6. 五模块动态路由、API 与按钮权限复核
### 6.1 主路由组件与 Python 导航
| 模块 | 管理端实际组件(views 证据) | Python 固定导航权限 | 判定 |
|---|---|---|---|
| 接诊台 | `D:\web\zyt\admin\src\views\patient\reception\index.vue:1-18` | `doctor.appointment/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:40-47` | **PARTIAL** |
| 处方库 | `D:\web\zyt\admin\src\views\consumer\prescription\list.vue:1-4,234-245` | `tcm.prescriptionLibrary/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:48-54` | **PARTIAL** |
| 已开处方 | `D:\web\zyt\admin\src\views\consumer\prescription\index.vue:1-4,1697-1711` | `tcm.prescription/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:55-61` | **PARTIAL** |
| 患者 | `D:\web\zyt\admin\src\views\first_visit\my_patients\index.vue:5-20` | `firstvisit.myPatient/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:62-68` | **PARTIAL** |
| 问诊/诊单 | `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:726-738` | `tcm.diagnosis/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:69-75` | **PARTIAL** |
这里的 **PARTIAL** 不是说固定权限码必然错误,而是:在 `views/**` 证据范围内无法恢复五条真实 `mySelf.menu` 记录,Python 又没有使用已经解析到的菜单树来做组件/顺序/显示/停用约束。
### 6.2 `mySelf` 菜单链
1. Python 正确请求 `auth.admin/mySelf`,解析用户、权限与 menu`D:\web\zyt\app\src\doctor_workstation\services\repository.py:164-195` —— **EXACT**
2. `Session` 正确保留 `menu``D:\web\zyt\app\src\doctor_workstation\core\session.py:12-22` —— **EXACT**
3. 正式登录只检查 `session.menu` 非空,然后把权限交给 Shell`D:\web\zyt\app\src\doctor_workstation\app.py:413-443` —— **PARTIAL**
4. Shell 注册页面时遍历固定 `NAVIGATION`,只调用扁平 `has_permission`,没有读取 `session.menu``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:248-280` —— **MISSING(菜单绑定)**
实际影响:菜单中任意一项存在即可通过登录守卫;随后五模块的显示、顺序、名称及停用状态均由本地固定表决定。动态路由中的隐藏/停用/组件路径/排序语义没有落地。
### 6.3 按钮权限与功能矩阵
#### 接诊台 — **PARTIAL**
- 管理端备注、编辑病历、完成接诊分别使用 `doctor.appointment/addDoctorNote``tcm.diagnosis/edit``doctor.appointment/complete``D:\web\zyt\admin\src\views\patient\reception\index.vue:144-171`
- 管理端队列内“通知医助/发起通话”按钮本身没有 `v-perms``D:\web\zyt\admin\src\views\patient\reception\index.vue:75-90`
- Python 备注与完成权限一致,但通知增加了 `doctor.appointment/notifyAssistant`,视频增加了 `tcm.diagnosis/videoQr`;缺“编辑病历”:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:322-331`
- Python 的通知、备注、完成调用链存在:`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:558-641`;管理端对应 API 调用见 `D:\web\zyt\admin\src\views\patient\reception\index.vue:460-527`
#### 处方库 — **PARTIAL,含 P0 权限门禁问题**
- 管理端 canonical 按钮码是 `wcf.prescription/add|read|edit|delete``D:\web\zyt\admin\src\views\consumer\prescription\list.vue:35-41,83-105`
- 管理端编辑/删除还要求创建人本人,或 root/角色 0、3:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:278-301`
- Python 新增/编辑/删除同时接受 canonical `wcf.*` 和额外 `tcm.prescriptionLibrary/*``D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:271-279,330-348`
- Python root/角色 0、3 的例外与管理端一致,但当 `user_id``creator_id` 缺失时直接返回可管理:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:460-472`。管理端的 `Number(row.creator_id) === Number(user.id)` 不会对缺失创建人默认放行。
#### 已开处方 — **PARTIAL/MISSING actions**
- 管理端支持业务订单跳转、新增、查看、修正患者、创建订单、编辑、审核、删除,对应权限码见 `D:\web\zyt\admin\src\views\consumer\prescription\index.vue:102-117,214-265`
- 业务订单导航依赖动态路由中的 `meta.perms === 'tcm.prescriptionOrder/lists'`,菜单不存在会拒绝跳转并提示:`D:\web\zyt\admin\src\views\consumer\prescription\index.vue:1862-1871`
- Python 只实现关键词/审核状态的列表和详情请求:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescriptions.py:91-188,284-340`;上述写操作与订单导航均不存在。
#### 患者 — **MISSING actions**
完整动作矩阵见第 4.2 节;Python 患者页没有对应按钮或权限检查。
#### 问诊/诊单 — **PARTIAL/MISSING actions**
- 管理端新增、批量指派、查看、诊单、开方、预约、补身份证、指派/取消指派、视频二维码、确认二维码/取消挂号、挂号日志、订单、删除的权限矩阵见 `D:\web\zyt\admin\src\views\tcm\diagnosis\index.vue:154-175,342-405`
- Python 仅保留列表筛选与 `tcm.diagnosis/videoQr` 视频入口:`D:\web\zyt\app\src\doctor_workstation\ui\pages\consultations.py:101-107,110-236,264-323`
### 6.4 权限匹配语义
- `PermissionSet` 支持精确码、全局 `*``prefix/*``D:\web\zyt\app\src\doctor_workstation\core\permissions.py:24-84`OR 语义见 `D:\web\zyt\app\src\doctor_workstation\core\permissions.py:96-104`
- UI `has_permission` 额外把 `/``.` 互换后尝试匹配:`D:\web\zyt\app\src\doctor_workstation\ui\widgets.py:135-180`
- 管理端视图只声明 canonical 形态,例如 `tcm.diagnosis/edit``wcf.prescription/edit`。由于本次不能读取管理端权限工具实现,不能证明管理端也接受点/斜杠变体;Python 的变体放行应判为 **PARTIAL**,不应当作已证明的兼容要求。
## 7. 医生角色与数据范围
### 7.1 已证明一致的部分
- 患者列表:管理端要求按当前角色/部门展示并把最终范围作为 `extend.scope.label` 返回;Python 调用相同业务域列表,不传任意 `doctor_id`,所以没有客户端扩大数据集的证据。判定 **EXACT(请求边界)**
- 处方库:普通医生只能管理自己创建的模板,root/角色 0、3 才可管理全部。Python 的正常 ID 分支与此一致。判定 **EXACT(正常分支)**
### 7.2 部分或缺失
- Python 不显示 `extend.scope.label`,用户无法验证当前是本人、部门还是其他服务端范围:**PARTIAL**。
- Python 缺失患者订单工作区,因而没有落实“当前患者范围内、订单创建人不参与归属”的口径:**MISSING**。
- Python 缺失面诊进度工作区,因而没有落实服务端 `schedule_mode=ownership``is_self_patient` 和近七日聚合:**MISSING**。
- 除患者模块视图明确写出的范围文字外,接诊、处方、诊单视图没有在允许证据范围内定义服务端角色/部门过滤算法。Python 使用同域列表接口且未见额外越权 `doctor_id` 注入,但无法仅凭 views 宣称五模块的后端数据范围 **EXACT**;应保留 **PARTIAL / 服务端待证**
## 8. P0 / P1 缺口
### P0
1. **处方库所有权校验 fail-open。**
Python 在当前用户 ID 或模板 `creator_id` 任一缺失时允许编辑/删除:`D:\web\zyt\app\src\doctor_workstation\ui\pages\prescription_library.py:460-466`;管理端仅允许严格所有者匹配或 root/角色 0、3:`D:\web\zyt\admin\src\views\consumer\prescription\list.vue:286-301`。应改成缺失即拒绝,并只在明确 root/角色例外时放行。若服务端另有强制校验,实际数据写入风险会降低,但客户端授权偏差仍已成立。
### P1
1. **患者模块只有主表子集。** 缺日期筛选、三日汇总、范围标签、确认/诊单日期列和全部行级动作。
2. **患者详情不是管理端真实详情。** Python 不请求诊单只读/编辑详情,病历摘要与最多 6 条历史预约依赖列表行快照,可能为空或陈旧。
3. **患者订单管理与面诊进度两个工作区完全缺失。** 同时丢失订单数据范围、订单操作权限矩阵、ownership 模式和 15 秒候诊刷新。
4. **Shell 忽略 `mySelf.menu`。** 已解析的动态菜单只用于“非空”登录守卫;五模块由固定表和扁平权限决定,未落实组件路径、显示/停用、排序和真实菜单成员关系。
5. **处方库权限码存在额外别名放行。** Python 接受 `tcm.prescriptionLibrary/add|edit|delete`,而视图仅证明 canonical `wcf.prescription/*`;应在拿到真实 `mySelf.permissions/menu` 样本后收敛。
6. **通用权限帮助器接受 `/`/`.` 互换。** 该兼容未被 views-only 证据证明,可能让非 canonical grant 意外点亮按钮。
7. **接诊台按钮矩阵不一致。** Python 比管理端额外门禁通知/视频,同时缺少 `tcm.diagnosis/edit` 的编辑病历动作。
8. **已开处方和问诊页均被压缩为只读子集。** 管理端已有的新增/编辑/审核/删除/订单/预约/指派等受权操作未落地。
## 9. 建议的验收优先级
1. 先修复处方库所有权 fail-open,并用“缺 `creator_id`、缺当前用户 ID、本人、他人、root、角色 0/3”六组合同测试覆盖。
2. 让 Shell 以 `Session.menu` 为主约束,再叠加权限码;不要仅以硬编码五项替代动态菜单。
3. 将患者页拆成与管理端一致的三个工作区,优先补真实只读详情/历史请求和主表行级动作。
4. 消费 `PageResult.extend.scope/summary/dates/schedule_mode`,把服务端权威范围直接呈现给医生。
5. 获取一份真实医生角色 `mySelf` 响应样本后,补做菜单 `paths/component/perms/is_show/is_disable/sort` 的精确合同测试;这一步无法由 `views/**` 单独完成。
+357
View File
@@ -0,0 +1,357 @@
# “我的处方库 / 已开处方”桌面端对齐审计
审计日期:2026-08-10
## 1. 范围、基准与结论口径
本审计只读检查以下两套实现,没有修改业务源码:
- 管理端唯一功能基准:D:/web/zyt/admin/src/views
- Python 桌面端:D:/web/zyt/app/src/doctor_workstation
管理端 API 包装文件、通用组件和分页 hook 仅用于解析“页面已经发起的调用”的 URL、参数与直接子组件行为,不把相邻页面或未被页面调用的 API 当成处方页功能。
结论标签:
- EXACT:用户可见行为、状态语义和请求合同均与基准一致;不要求布局像素一致。
- PARTIAL:仅覆盖基准的子集,或字段/状态/校验语义有差异。
- MISSING:基准存在,而 Python 没有入口或没有可达实现。
- EXACT(负向):基准明确没有该能力,Python 也没有;不能把它列为待补功能。
## 2. 真实页面定位
| 业务名 | 管理端真实 view 组件 | Python 页面 | 结论 |
|---|---|---|---|
| 我的处方库 | D:/web/zyt/admin/src/views/consumer/prescription/list.vue:1-414script name 为 prescriptionLibrary234 | D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:251-545;固定导航见 D:/web/zyt/app/src/doctor_workstation/ui/shell.py:48-54 | EXACT(组件映射) |
| 已开处方 / 处方管理 | D:/web/zyt/admin/src/views/consumer/prescription/index.vue:1-4892script name 为 prescriptionList1697 | D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:72-436;固定导航见 D:/web/zyt/app/src/doctor_workstation/ui/shell.py:55-61 | EXACT(组件映射),功能不是 EXACT |
管理端采用动态菜单,单凭 src/views 只能确定组件键应分别落到 consumer/prescription/list 与 consumer/prescription/index,不能确定生产环境最终 URL、菜单标题或 route.meta.perms。Python 则固定注册为 prescription_library / prescriptions。审计不伪造管理端生产 URL。
## 3. 直接子组件与边界
### 3.1 MedicineNameSelect
处方库在 D:/web/zyt/admin/src/views/consumer/prescription/list.vue:158-165 使用;已开处方的主方、辅方行分别在 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:695-726、736-767 使用。
真实合同:
- 远程、可过滤选择器,提示支持药材名或拼音首字母:D:/web/zyt/admin/src/components/medicine-name-select/index.vue:3-24。
- 展开或搜索时 GET /doctor.medicine/lists,参数 name、page_no=1、page_size=100、status=1:同文件 69-92URL 包装见 D:/web/zyt/admin/src/api/medicine.ts:6-8。
- 选择后同时回写 name 和 medicine_id;不能把任意自由文本当成一次有效选择:组件 95-110。
Python 处方库的 HerbRow 是两个自由文本 QLineEditmedicine_id 只会保留已有行上的值,新增行没有药材主数据选择:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:76-110。结论:PARTIAL。
### 3.2 DaterangePicker
已开处方在 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:19-23 使用。直接子组件默认 datetimerange,值格式 YYYY-MM-DD HH:mm:ss,并分别回写 start_time/end_timeD:/web/zyt/admin/src/components/daterange-picker/index.vue:1-48。
Python 没有日期范围控件:MISSING。
### 3.3 TcmDiagnosisEditView
已开处方异步加载 D:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue,并在 diagnosis_id>0 时提供“查看患者诊单详情”:D:/web/zyt/admin/src/views/consumer/prescription/index.vue:566-574、1692-1693、1728、2910-2921。
直接调用 openViewOnly(id);子页面以同一套界面只读打开、加载 GET /tcm.diagnosis/detail {id},默认病历 tabD:/web/zyt/admin/src/views/tcm/diagnosis/edit.vue:1263-1264、1310-1322URL 见 D:/web/zyt/admin/src/api/tcm.ts:59-62。
该只读子页仍保留全套 tab 边界:
- 病历字段整体 disableddiagnosis/edit.vue:55-67。
- 医生备注 readonly、日常记录 read-only633-670。
- 处方 tab 受 tcm.diagnosis/chufang 控制且 read-only673-686。
- 业务订单受 tcm.diagnosis/patientOrders 控制:688-702。
- 视频回放、聊天、指派、挂号分别受 tcm.diagnosis/huifang、tcm.diagnosis/chat、tcm.diagnosis/assign 或 detail、doctor.appointment/lists 控制:704-758。
Python 已开处方没有 diagnosis_id 跳转或只读诊单子页:MISSING。
### 3.4 分页
两个管理端页面均通过 usePaging 自动发送 page_no/page_size,默认 page=1、size=15,并消费 count/lists/extendD:/web/zyt/admin/src/hooks/usePaging.ts:13-49。Python 两页均默认 page_size=20,使用 PageResult/Pager
- 处方库:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescription_library.py:263-266、413-442。
- 已开处方:D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:84-89、284-316。
参数和总数分页机制 EXACT,默认每页条数 PARTIAL15 vs 20)。
## 4. 我的处方库:管理端真实合同
### 4.1 筛选、字段与状态
- 筛选:prescription_name、formula_type=主方|辅方、is_public=0|1,查询回第一页,重置为初始值:D:/web/zyt/admin/src/views/consumer/prescription/list.vue:5-31、247-252、281-284。
- 列表字段:id、prescription_name、formula_type、herbs.length、全部 herbs 的 name/dosage(g)、is_public、disable_edit、creator_name、create_timelist.vue:44-107。
- 公开范围:0=仅自己可见,1=所有人可见:list.vue:21-26、67-72、195-205。
- disable_edit:0=可修改,1=已禁用;真实语义是“导入到已开处方后锁定整张处方药材”,不是“禁止维护这条模板自身”:list.vue:74-79、208-220。
- 普通用户只管理本人 creator_idroot=1 或 role_ids 含 0、3 可管理任意模板:list.vue:276-301。
### 4.2 查看、创建、编辑、删除、复制
- 新增:独立按钮,表单字段为 prescription_namemaxlength=100)、formula_type、herbs、is_public、disable_editlist.vue:35-41、116-230、259-267。
- 查看:有单独 read 权限按钮,复用列表行数据打开完全禁用的表单;不调用 detaillist.vue:83-87、123-129、333-343。
- 编辑:仅 owner 或管理角色可见;仍可修改 disable_edit=1 模板本身的药材:list.vue:88-106、345-355。
- 删除:确认后 POST {id}list.vue:403-409。
- 复制:页面没有复制/克隆按钮、函数或端点。导入模板属于“已开处方”的复用流程,不是复制模板 CRUD。
- 导出/打印:本页面没有。
### 4.3 药材行和校验
- 药材 DTO{medicine_id?: number, name: string, dosage: number}list.vue:259-266。
- name 必须由 MedicineNameSelect 选取并同步 IDdosage 是 min=0、precision=1、step=0.5 的数字控件:list.vue:156-190。
- 提交要求:名称必填、至少一味药、每行 name 非空、dosage>0list.vue:269-274、357-379。
### 4.4 API DTO
页面调用来源见 list.vue:234-245URL 包装见 D:/web/zyt/admin/src/api/tcm.ts:591-615。
| 方法与端点 | 页面请求参数 / body | 页面使用情况 |
|---|---|---|
| GET /tcm.prescriptionLibrary/lists | page_no、page_size、prescription_name、formula_type、is_public | 列表与分页 |
| POST /tcm.prescriptionLibrary/add | prescription_name、formula_type、herbs、is_public、disable_edit;页面对象还带重置后的 id=0 | 新增 |
| POST /tcm.prescriptionLibrary/edit | id、prescription_name、formula_type、herbs、is_public、disable_edit | 编辑 |
| POST /tcm.prescriptionLibrary/delete | {id} | 删除 |
| GET /tcm.prescriptionLibrary/detail | {id} | API 已封装,但该 view 没有调用 |
权限码为 wcf.prescription/add、read、edit、deletelist.vue:36、85、93、102。
## 5. 我的处方库:Python 逐项对比
| 项目 | Python 证据 | 结论 |
|---|---|---|
| 页面入口 | shell.py:48-54 以 tcm.prescriptionLibrary/lists 注册;页面标题见 prescription_library.py:251-282 | EXACT |
| 筛选 | Python 有关键词、主/辅方、公开范围;repository 把 keyword 改成 prescription_name,把 main/aux 改为主方/辅方:prescription_library.py:284-315、413-429repository.py:279-298、586-588 | PARTIAL:请求合同基本一致,但“名称或药材”的占位文案(291)是假的,实际只查 prescription_name |
| 分页 | page/page_size 经 invoke 转 page_no/page_sizePager 消费总数:prescription_library.py:409-442widgets.py:221-223 | PARTIAL:默认 20,基准 15 |
| 列表字段 | Python 只显示名称、类型、最多 4 味摘要、公开范围、创建人、时间:prescription_library.py:357-384 | PARTIAL:缺 id、药材数量、完整药材、disable_edit;把 create_time 列标题写成“更新时间” |
| 单独查看 | 没有 read 按钮;双击只会走 _edit_selected,且需 edit 按钮可见及 owner/管理员:prescription_library.py:330-350、385-386、479-485 | MISSING |
| 新增 | _new_template -> save -> repository addprescription_library.py:474-501repository.py:309-323 | PARTIAL:端点可用,但药材和 disable_edit 语义不完整 |
| 编辑 | 使用列表行直接打开,与基准一样不请求 detail;保存到 editprescription_library.py:479-501repository.py:325-347 | PARTIAL |
| 删除 | owner/管理员检查、确认、POST {id}prescription_library.py:454-472、507-537repository.py:349-352 | EXACT |
| 公开范围 | 复选框产生 boolrepository 规范为 0/1prescription_library.py:157-159、218-228repository.py:572-575 | EXACT |
| disable_edit | Python 把它解释成“当前模板药材不可编辑”,隐藏增删并禁用输入;而管理端只在导入已开处方后锁药材。Python 也没有切换开关,只能原样回传:prescription_library.py:117-140、167-170、218-224 | PARTIAL(语义错误) |
| 药材选择 | 自由文本 name/dosage;新行无 medicine_id,已有行才保留:prescription_library.py:76-110 | PARTIAL |
| 药材校验 | 只检查至少一行、有名称时 dosage 非空:prescription_library.py:230-248 | PARTIAL:不校验数字和 >0,且允许“10g”字符串,与基准 number DTO 不同 |
| 所有权 | user id 相同、root、role 0/3prescription_library.py:460-472 | EXACT |
| API CRUD | list/detail/add/edit/delete 均存在且 URL、id 合同正确:repository.py:279-352 | EXACT(仓储层) |
| 模型 | PrescriptionTemplate 保留 id/name/formula/herbs/public/disable/creator/time/rawto_api_dict 正规化中文方型和 0/1core/models.py:426-477 | EXACT(字段合同) |
| 权限 | add/edit/delete 接受基准 wcf.*,同时接受 tcm.prescriptionLibrary/* 别名:prescription_library.py:274-279、332-347 | PARTIAL:动作基本受控,但缺 wcf.prescription/read 的可达只读查看;额外别名不是 view 基准 |
| 复制 | 两边都没有 | EXACT(负向) |
| 导出/打印 | 两边都没有 | EXACT(负向) |
## 6. 已开处方:管理端真实合同
### 6.1 筛选与分页 DTO
页面筛选控件见 D:/web/zyt/admin/src/views/consumer/prescription/index.vue:4-99;状态对象见 1874-1887
- sn:处方编号模糊查询。
- patient_name。
- creator_ids:医生 ID 多选;有值才发数组。
- audit_filterall、pending、passed、not_passed、rejectedall 被正规化为空字符串。
- source_filterall、manual、systemall 被正规化为空字符串。
- start_time/end_time:创建时间,格式 YYYY-MM-DD HH:mm:ss。
- 快捷日期:全部、今日、昨日、前天,切换后立即回第一页查询:index.vue:3013-3048。
最终 GET /tcm.prescription/lists 参数是 page_no、page_size 与上述字段;creator_ids 空数组会被省略:index.vue:2990-3007。默认 page_size=15。
### 6.2 列表字段、状态与可达动作
列表与按钮位于 index.vue:102-270
- 列:sn、id、订单一致性警告、prescription_type、is_system_auto1=空白处方,其他=手工)、patient_name/gender/age、综合审核状态和驳回意见、void_status、doctor_name/prescription_date、assistant_name、create_time。
- audit_status0 待审核、1 已通过、2 已驳回:index.vue:3113-3123。
- business_prescription_audit_rejected=1 时,列表主状态覆盖为“已驳回”,并分别展示业务订单与消费者处方的驳回意见:index.vue:3125-3151。
- void_status=1 单独显示“作废”。
- 已有关联业务订单时,药材为空或重名会在编号下给出警告:index.vue:3192-3216。
动作门槛:
- 查看:cf.prescription/read。
- 修正姓名/性别/手机:未作废且 tcm.prescription/patchPatient。
- 创建订单:has_prescription_order=0、未作废且 tcm.prescriptionOrder/create。
- 编辑、删除:只有“非 audit_status=1 且未作废”才出现,分别需 cf.prescription/edit、cf.prescription/del。
- 审核:audit_status=0、未作废且 cf.prescription/audit。
- 业务订单列表:tcm.prescriptionOrder/lists。
精确条件和权限见 index.vue:214-264;“已通过且未作废”判定见 3154-3157。
### 6.3 查看详情、打印与 PDF
查看先用列表行渲染,再 GET /tcm.prescription/detail {id} 覆盖:index.vue:3796-3817。
A4 处方笺显示:
- 来源、作废、消费者审核、业务订单审核及意见:index.vue:284-343。
- 日期/编号;患者姓名、性别、年龄、电话、收件信息、临床诊断:345-390。
- 主方/辅方药材,单剂 dosage 与 dose_count 计算后的总量:392-433。
- 主辅方用法、医嘱、忌口、备注、药房备注、出丸:435-448。
- 医师手写签名、医生、类型、天数/剂数、单剂量:450-481。
- 审核人、审核时间、审核意见:484-496。
打印调用 window.printindex.vue:2741-2751。PDF 用 html2canvas(scale=2) 渲染 A4,再由 jsPDF 分页并下载:index.vue:2753-2813。页面没有“列表导出 Excel”;tcm.ts:384-387 的业务订单导出 API 没有被此 view 导入或调用。
### 6.4 新增/编辑字段与药材规则
表单位于 index.vue:499-1153reactive DTO 在 2815-2862
- 关联/系统:id、diagnosis_id、creator_id、is_system_auto。
- 患者:patient_name、gender、age、visit_no、prescription_date。
- 四诊/诊断:tongue、tongue_image、pulse、pulse_condition、clinical_diagnosis。
- 药材:herbs[{medicine_id?, name, dosage, formula_type=主方|辅方, locked?}]。
- 剂型:prescription_type(浓缩水丸、饮片、颗粒、丸剂、散剂、膏方、汤剂)、dosage_amount、dosage_unit、dosage_bag_count、need_decoction、bags_per_dose、dose_count、dose_unit。
- 主方用法:usage_days、times_per_day、usage_instruction、usage_time、usage_way、dietary_taboo[]、usage_notes。
- 辅方用法 aux_usagedosage_amount、dosage_bag_count、need_decoction、bags_per_dose、times_per_day、usage_days、prescription_name。
- 医师:doctor_name、doctor_signature(手写 canvas 生成 PNG data URL,必填)。
- 隐藏状态:is_shared、visible_role_ids、audit_status/time/by/remark、业务订单审核字段。
主辅方各自使用 MedicineNameSelect 和数字 dosage;导入 disable_edit=1 模板时给行加 locked 并锁定整张处方药材:index.vue:667-813、3489-3515、3712-3732。
新增/编辑校验:
- patient_name、gender、prescription_date、clinical_diagnosis、dose_count、doctor_name、doctor_signature 必填:index.vue:2955-2988。
- 至少一味药,每行 name 非空、dosage>03920-3947。
- 新增提交前强制 audit_status=0add/edit 都把完整 editForm 展开为 body3951-3961。
- 编辑提示保存后重新进入待审核;已作废/已驳回可经编辑恢复,业务订单侧审核会重置:index.vue:514-565。
公开/共享范围的特殊事实:is_shared、visible_role_ids 虽存在于 editForm、详情回填和展开 payload2846-2847、3889-3890、3952),但模板区没有任何 v-model 控件;formatVisibleRoleNames 也未被模板调用(3348-3364)。因此该真实 view 不允许用户设置共享范围。桌面端不应凭字段存在擅自新增 UI。
### 6.5 处方库导入、粘贴导入与“复制”
- 从处方库导入对话框支持处方名称、主/辅方筛选、replace/append、10/15/20/50 分页,展示公开范围和创建人:index.vue:1539-1633。
- 请求 GET /tcm.prescriptionLibrary/lists,参数 page_no、page_size、prescription_name、formula_type、prescribing_creator_idindex.vue:3444-3468。
- prescribing_creator_id 取当前开方医生;语义是“该医生自己的模板 + 公共模板”。
- 粘贴导入解析 name/dosage,只接受 GET /doctor.medicine/lists 中 name 完全相同且唯一的药材;请求 name、page_no=1、page_size=200、status=1index.vue:3538-3709。
- replace 只替换同方型药材,append 追加;重复名仅警告。
该页面没有“复制某张已开处方”动作或端点。处方库导入与文本导入是新增/编辑中的填充方式,不等于复制已开处方。Python 也没有复制:EXACT(负向)。
### 6.6 审核联动
- POST /tcm.prescription/audit body={id, action:'approve'|'reject', remark}D:/web/zyt/admin/src/api/tcm.ts:365-372;调用见 index.vue:3367-3395。
- reject 必填 remark,成功语义为“驳回并作废处方”:index.vue:1669-1689、3373-3385。
- 返回 wecom_notify_ok=false 且有 wecom_notify_hint 时额外警告:3384-3388。
- 页面定义 root/role_ids 0、3 的审核角色辅助函数,但该函数没有被模板使用;真实按钮门槛仍是 v-perms cf.prescription/audit 加行状态:index.vue:1854-1855、247-253、3104-3110。
### 6.7 患者修正与订单联动
患者修正:
- 对话框只改 patient_name、gender、phone,明确不改审核状态,并记录到业务订单日志:index.vue:1155-1195。
- POST /tcm.prescription/patchPatient body={id,patient_name,phone,gender}index.vue:1948-1973API 类型见 tcm.ts:330-338。
订单读取与一致性:
- 编辑处方时 GET /tcm.prescriptionOrder/lists,参数 page_no=1、page_size=5、prescription_id,显示最近订单的 order_no、medication_days、remark_assistantindex.vue:769-811、2879-2907。
- 业务订单审核驳回可以覆盖处方列表状态和处方笺状态,但保留消费者处方本身的 audit_statusindex.vue:165-190、295-343、3125-3151。
从处方创建业务订单:
- 三步表单:诊单患者与收件信息;服务/发货/支付单;费用与备注:index.vue:1197-1537。
- 诊单患者远程搜索意图为 GET /tcm.diagnosis/searchPatient,参数 keyword、page_no=1、page_size=10index.vue:2384-2401API 定义在 D:/web/zyt/admin/src/api/order.ts:88-91。
- GET /tcm.prescriptionOrder/paidPayOrders {diagnosis_id} 返回可关联支付单和 deposit_min_amountindex.vue:2307-2329API 见 tcm.ts:389-398。
- POST /tcm.prescriptionOrder/create 的 body
prescription_id、diagnosis_id、recipient_name、recipient_phone、shipping_address、is_follow_up、prev_staff、service_channel、service_package(逗号拼接)、express_company、tracking_number、ship_mode、fee_type、amount、remark_extra、remark_assistant;可选 shipping_province/city/district、medication_days、internal_cost、pay_order_ids。证据:index.vue:2435-2485URL 见 tcm.ts:374-382。
- ship_mode 选择需 tcm.prescriptionOrder/setShipModeinternal_cost 需 finance.account_log/listsremark_extra 需 tcm.prescriptionOrder/editRemarkExtraindex.vue:1307-1314、1466-1488、2041-2043。
- 服务套餐来自 GET /config/dict {type:'server_order'}index.vue:2180-2187URL 见 D:/web/zyt/admin/src/api/app.ts:13-16。
## 7. 已开处方:Python 逐项对比
D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:1 已明确把本页定义为 “Read-only list and detail view for issued prescriptions”。这不是完整管理端主链。
| 项目 | Python 证据 | 结论 |
|---|---|---|
| 页面入口 | shell.py:55-61,以 tcm.prescription/lists 控制 | EXACT |
| 筛选 | 只有一个“编号/患者”关键词和 audit 0/1/2prescriptions.py:101-125、284-303 | PARTIAL |
| 请求映射 | repository 根据 RX/CF/纯数字猜 sn,否则 patient_namestatus 转 pending/passed/rejectedrepository.py:354-387 | PARTIAL:已有控件映射正确;缺 creator_ids、日期、source、not_passed;同一关键词不能同时查编号与患者 |
| 分页 | PageResult/Pager 完整 | PARTIAL:默认 20,基准 15 |
| 列表字段 | sn、patient、audit、source、doctor、prescription_dateprescriptions.py:139-180 | PARTIAL:缺 id、prescription_type、gender/age、业务/消费者驳回意见、void 独立列、assistant、create_time、订单警告 |
| 来源 | is_system_auto -> “系统代开/医生开具”:prescriptions.py:67-70 | PARTIAL:基准语义/文案是“空白处方/手工” |
| 状态 | 先作废,再业务订单驳回/消费者驳回,再通过/待审;raw fallback 可读模型未声明字段:prescriptions.py:46-60widgets.py:48-66 | PARTIAL:主标签逻辑接近,但不能分别显示两类驳回意见、审核人/时间和独立作废信息 |
| 详情请求 | 选中后 GET /tcm.prescription/detail {id}prescriptions.py:326-357repository.py:389-396 | EXACT(端点) |
| 详情字段 | 患者/性别/年龄、医生、日期、类型、剂数、药材/剂量/方型、频次/天数/方式/时间/忌口/说明和一条审核备注:prescriptions.py:360-420 | PARTIAL |
| A4 处方笺 | 无 A4 版式;缺电话/收件、临床诊断、签名、单味总量、药房备注/出丸、审核轨迹等 | MISSING |
| 新增 | 页面无按钮、无表单;RemoteRepository 无 prescription add 方法 | MISSING |
| 编辑 | 页面无按钮、无表单;缺主/辅方药材、剂型用法、签名、重新待审完整流程 | MISSING |
| 删除 | 无按钮;RemoteRepository 无 delete | MISSING |
| 患者修正 | 无 patchPatient UI/仓储方法 | MISSING |
| 审核 | 无 approve/reject UI/仓储方法;无驳回必填、作废联动、企微提示 | MISSING |
| 订单联动 | 无订单列表入口、创建订单、支付单关联、处方一致性警告、最近订单提示 | MISSING |
| 处方库导入 | 无新增/编辑,自然也没有 replace/append、锁药材、prescribing_creator_id | MISSING |
| 文本导入 | 无解析及药材完全同名验证 | MISSING |
| 药材编辑行 | 详情表只读;无 medicine_id 选择与 dosage 校验 | MISSING |
| 打印 | 无 window/Qt 打印等价能力 | MISSING |
| PDF | 无处方笺 PDF 导出 | MISSING |
| 列表 Excel | 基准页也没有 | EXACT(负向) |
| 复制已开处方 | 基准页也没有 | EXACT(负向) |
| 共享范围 UI | Python 不展示 is_shared/visible_role_ids;基准 view 也没有可见控件 | EXACT(只读 UI 的负向行为) |
| 共享字段模型 | Prescription 存 is_shared、visible_role_ids,但没有 mutationcore/models.py:519-521、574-576 | PARTIAL(数据保留) |
| 诊单详情联动 | 没有 diagnosis_id “查看诊单详情”和全 tab 只读子页 | MISSING |
| 动作权限 | 构造器保存 permissions 但页面没有任何 has_permission 动作判断:prescriptions.py:72-99 | MISSING(动作本身也缺) |
### 7.1 Python Prescription DTO 覆盖度
Prescription 模型保留绝大多数列表/详情核心字段:id、diagnosis、sn、患者、四诊、药材、剂型/主方用法、aux_usage、签名、creator/assistant、source/share/audit/void/order/create_time,并保留 rawD:/web/zyt/app/src/doctor_workstation/core/models.py:480-587。
结论:PARTIAL。
原因:
- 模型层比 UI 完整,但 UI 只消费很小子集。
- business_prescription_audit_rejected、business_prescription_audit_remark、收件/药房/出丸等不设显式 dataclass 字段,只能从 raw 兜底;get_value 确实支持 raw fallbackwidgets.py:48-66。
- RemoteRepository 只实现 GET lists/detailrepository.py:354-396;没有 add/edit/delete/patch/audit/order 的请求合同。
## 8. 权限码总表
### 8.1 我的处方库
| 能力 | 管理端真实码 | Python |
|---|---|---|
| 页面/列表 | 动态菜单实际值不能由 view 单独确定;接口是 tcm.prescriptionLibrary/lists | shell 固定 tcm.prescriptionLibrary/lists |
| 新增 | wcf.prescription/add | 接受 wcf.prescription/add 或 tcm.prescriptionLibrary/add |
| 查看 | wcf.prescription/read | MISSING |
| 编辑 | wcf.prescription/edit + owner/root/role 0,3 | 同时接受 wcf 或 tcm 别名 + 同一所有权规则 |
| 删除 | wcf.prescription/delete + owner/root/role 0,3 | 同时接受 wcf 或 tcm 别名 + 同一所有权规则 |
### 8.2 已开处方
管理端真实页面使用:
- cf.prescription/add、read、edit、audit、del。
- tcm.prescription/patchPatient。
- tcm.prescriptionLibrary/lists。
- tcm.prescriptionOrder/create、lists、setShipMode、editRemarkExtra。
- finance.account_log/lists。
- 诊单子页另用 tcm.diagnosis/chufang、patientOrders、huifang、chat、assign/detail 和 doctor.appointment/lists。
证据集中在 index.vue:102-117、214-264、667-680、1307-1314、1466-1488、1692-1728。
Python 只有页面入口 tcm.prescription/listsshell.py:55-61);PrescriptionsPage 没有上述动作,也没有动作权限判断。结论:MISSING。
## 9. 基准 view 自身的边界/风险(不计作 Python 差异)
1. 订单患者搜索在 index.vue:2391 调用 searchPatientsAPI,但 index.vue:1698-1714 的显式 import 没有该符号;真正 export 位于 api/order.ts:88-91。若工程没有为这个本地 API 符号做自动导入,远程患者搜索会失败。仅以 views 不能证明是否存在额外自动导入,故结论是“高风险未确认”,不是把不存在的合同补到 Python。
2. is_shared、visible_role_ids、roleAll、formatVisibleRoleNames 存在,但没有表单绑定;不要把死状态误判成可见“公开范围”功能。
3. userCanAudit() 定义了 root/role 0,3,但没有被模板调用;真实前端门槛是 cf.prescription/audit 和行状态,后端仍是最终边界。
4. prescriptionVoid API 虽在 tcm.ts:360-363 存在,但此 route 没有导入/直接调用;该页的可达作废路径是 audit reject 的服务端联动。
5. prescriptionOrderExport API 存在,但“已开处方” route 不调用;不能据此声称本页有 Excel 导出。
6. 两个基准页面均无“复制”能力。
## 10. P0 / P1 缺口
### P0
1. 已开处方主链整体缺失:新增、编辑、删除及其 RemoteRepository POST 合同均不存在。当前 Python 实现是明确的只读页,无法替代管理端真实路由。
2. 医疗安全关键编辑合同缺失:药材必须来自 MedicineNameSelect、主辅方和 locked 模板规则、dosage>0、剂型/用法、临床诊断、手写医生签名、编辑后重入待审,Python 全部不可达。
3. 审核/作废闭环缺失:approve/reject、驳回意见必填、reject 同时作废、两类审核意见、企微通知提示均不存在。
4. 订单联动缺失:患者修正、订单创建、支付单关联/定金门槛、发货模式权限、订单审核驳回覆盖状态、处方/订单药材一致性提示全部不存在。
5. 权限闭环缺失:已开处方除了列表入口没有任何 cf.prescription/*、patchPatient、prescriptionOrder/* 动作校验;在补动作前必须按基准精确落码,不能以 lists 权限兜底。
### P1
1. 已开处方列表筛选不完整:缺医生多选、创建时间/快捷日期、来源、not_passed;关键词还通过内容猜测 sn 或 patient_name。
2. 已开处方列表/详情展示不完整:缺业务订单警告、两类驳回意见、独立作废/审核轨迹、收件/电话、临床诊断、医生签名、药房备注/出丸及 A4 处方笺。
3. 已开处方缺打印和 A4 PDF 下载。
4. 已开处方缺 diagnosis_id 只读诊单子页及其权限化 tabs。
5. 处方库缺真正的 wcf.prescription/read 查看路径;只有有编辑权且可管理该行的用户能通过编辑对话框看完整内容。
6. 处方库药材行应改为药材远程选择与数字 dosage;当前自由文本允许合同无效值。
7. 处方库 disable_edit 语义错误且无切换开关:它应控制“导入后的已开处方药材锁定”,不应锁死模板自身编辑。
8. 处方库表格缺 id、药材数、完整明细、disable_edit,且时间列标题与数据不一致。
9. 两页默认 page_size 与基准不同(20 vs 15);属于低风险但可见的分页差异。
“复制”不是缺口:两个管理端基准页面均没有复制动作,Python 也没有。
@@ -0,0 +1,267 @@
# 接诊台 / 问诊列表:管理后台到医生桌面的逐项一致性审计
审计日期:2026-08-10
基准:`D:\web\zyt\admin\src\views` 中从现行路由可达的 Vue/TS 实现
对比对象:`D:\web\zyt\app\src\doctor_workstation` 当前 Python 实现
约束:本次只读审计;未修改 `src/`,未发网络请求。
## 1. 结论摘要
当前 Python 不是管理后台两个页面的等价移植,而是“接诊队列 + 简化问诊表 + 原生直呼视频”的医生端子集。端点骨架中,挂号列表、接诊聚合详情、通知、添加文字备注、完成接诊、诊单列表以及 `getCallSignature/startCall/bindCallRoom/endCall` 已接通;但今日队列边界、异步选人一致性、挂号状态判定、病例详情/编辑、开方、日常记录、备注媒体、复杂筛选和操作权限仍有显著差异。
发布阻断级结论:
1. **P0 — 接诊台快速切换患者会出现旧患者详情挂在新患者选择下。** 管理后台每次选人递增请求序号并同时校验序号和 `selectedId``D:\web\zyt\admin\src\views\patient\reception\index.vue:398-418`);Python 在任一详情请求进行中时直接拒绝下一次加载(`D:\web\zyt\app\src\doctor_workstation\ui\pages\reception.py:406-418`),旧响应只按旧 generation 接受(同文件 `:437-447`)。这会把显示、备注目标和当前队列行拆成两个患者上下文。
2. **P0 — Python 接诊队列没有“今天”条件。** 基准固定发送 `start_date=end_date=today``patient/reception/index.vue:296-305`);Python 只发送 `status/keyword/page/page_size``reception.py:344-361`)。因此历史或未来的状态 1/4 记录可能进入“今日接诊台”,并可触发通知、备注、视频和完成。
3. **P0 — 问诊列表的视频门槛读取了错误的状态域。** 基准以 `has_appointment && appointment_status===1` 为唯一可视频条件(`tcm/diagnosis/index.vue:1778-1780`H5 同口径 `index_h5.vue:949-950`)。Python 读取诊单模型的 `status`,并允许 1 或 4`consultations.py:310-317`);模型又优先取根 `status`,而不是根 `appointment_status``core/models.py:362-366,406-414`)。结果是已完成/过号记录可能仍可直呼,合法已预约记录也可能被误禁用。
其余 P1 缺口集中在:完整病例核对与只读详情、诊单编辑、从诊单开方、接诊详情字段/日常记录/备注附件、手机号脱敏、动作权限契约,以及问诊列表的筛选/状态/多挂号语义。详见第 8 节。
## 2. 判定口径与路由范围
状态含义:
- **exact**:端点、关键参数、状态门槛和结果语义均一致;不要求 Qt 与 Web 视觉相同。
- **partial**:已有对应能力,但参数、字段、权限、状态或边界行为不完整。
- **missing**:基准可达的医生核心能力在 Python 中没有入口或 repository 合同。
- **intentionally unsupported**:明确属于 H5 布局、小程序二维码或医助/后台运营角色,当前原生医生桌面选择了不同交互或没有承载;这是审计分类,不代表产品已正式批准删除。
路由证据:
- 管理后台主菜单是服务端动态路由:Vite 收集所有 `views/**/*.vue`,菜单组件名交给 `loadRouteView` 匹配(`D:\web\zyt\admin\src\router\index.ts:9-15,28-69`)。因此接诊台主文件是 `views/patient/reception/index.vue`,问诊列表 PC 主文件是 `views/tcm/diagnosis/index.vue`
- `/tcm/diagnosis/h5` 静态指向 `index_h5.vue``D:\web\zyt\admin\src\router\routes.ts:82-85`)。
- PC 问诊列表的“查看”与双击跳转隐藏路由 `/tcm/diagnosis-readonly?id=...``tcm/diagnosis/index.vue:2165-2172`),静态路由指向 `readonly.vue``router/routes.ts:87-102`)。
- `index.vue.bak` 的后缀不是 `.vue`,不会进入 `import.meta.glob('/src/views/**/*.vue')``add.vue` 没有被现行两个入口导入;两者均未作为基准。PC 虽挂载 `detail.vue`,却没有存活的 `handleDetail` 调用;该弹窗仅由 H5 卡片点击实际触发(`index_h5.vue:121,144,264-268,1013-1015`)。
Python 页面级门控与主入口一致:接诊台使用 `doctor.appointment/lists`,问诊列表使用 `tcm.diagnosis/lists``D:\web\zyt\app\src\doctor_workstation\ui\shell.py:40-47,69-75,248-264`),判定为 **exact**
## 3. 实际可达文件与依赖图
以下只列业务 Vue/TS 和决定行为的共享文件,不展开 Element Plus、基础 popup/upload 等纯框架组件。
### 3.1 接诊台
- `views/patient/reception/index.vue`:队列、轮询、详情、通知、完成、快捷文字备注、通话入口(导入证据 `:191-210`)。
- `views/patient/reception/components/NoteTimeline.vue`:备注文本、舌苔图片、检查报告、单附件删除(`:1-121,125-132`)。
- `views/tcm/diagnosis/components/PatientInfoCard.vue`:脱敏手机号、预约与人员摘要(`:1-41`)。
- `views/tcm/diagnosis/components/PatientCaseCard.vue`:病例全字段和生命体征阈值(`:1-124,128-153`)。
- `views/tcm/diagnosis/components/DailyMatrix.vue``DiagnosisTodoList.vue`:7/30/自定义日期窗、血糖血压/饮食/运动/跟踪备注/待办(`DailyMatrix.vue:317-332,342-470`)。
- `components/chat-dialog/index.vue``ChatMessageItem.vue` 及本地录制/截图/IM 工具:实时聊天与通话、通话落库、房间绑定、结束、可选本地录制;API 导入见 `chat-dialog/index.vue:129-175`
- `views/tcm/diagnosis/edit.vue` 及其下述诊单详情依赖:`DailyMatrix.vue``CaseRecordList.vue``CallRecordPanel.vue``RecordingPlaybackBlock.vue``RecordingVideoPlayer.vue``ImChatRecordPanel.vue``AssignLogPanel.vue``AppointmentRecordPanel.vue``NoteTimeline.vue``TrackingNoteTimeline.vue``PatientOrderList.vue``PrescriptionOrderDetailDrawer.vue``components/tcm-prescription/index.vue`(导入证据 `edit.vue:788-806`)。
### 3.2 问诊列表
- `views/tcm/diagnosis/index.vue`:PC 列表、全部筛选/角标、行操作、20 秒静默轮询(导入及异步组件 `:727-746`)。
- `views/tcm/diagnosis/index_h5.vue`:静态 H5 路由;复用同一 edit/detail/prescription/appointment/watch-call 组件(`:588-630`)。原生 Python 不需要复刻 H5 布局,标为 **intentionally unsupported**;但它证明的业务状态和动作边界仍计入对比。
- `views/tcm/diagnosis/readonly.vue`:PC 隐藏详情路由;复用 PatientInfo/Case、DailyMatrix、备注、通话回放、IM、指派、挂号、业务订单组件(`:151-167`)。
- `views/tcm/diagnosis/detail.vue`H5 卡片的简版诊单详情(`:1-165`)。
- `views/tcm/diagnosis/edit.vue`:新增/编辑/只读抽屉及所有扩展 Tab。
- `components/tcm-prescription/index.vue`:按诊单开方、查已有处方、作废、处方库导入和 PDF。
- `views/tcm/diagnosis/appointment.vue`:预约、排班、号源、渠道和今日重复挂号边界。
- `views/tcm/diagnosis/components/AssistantWatchCallDialog.vue`:医助 TRTC 只拉流旁观(`:23-26,120-219`)。
- 决定字段与边界的 TS`hooks/usePaging.ts``:13-58`)、`utils/perm.ts``:3-17`)、`install/directives/perms.ts``:13-32`)、`utils/diag-display.ts``utils/blood-thresholds.ts``utils/diabetes-discovery-display.ts`
## 4. 接诊台逐项差异
| 项目 | 基准证据 | Python 证据 | 判定 | 差异/影响 |
|---|---|---|---|---|
| 页面权限 | 动态菜单页面权限为 `doctor.appointment/lists` | `shell.py:40-47,248-264` | exact | 页面级一致。 |
| 状态队列 | 仅 status 1“待接诊”和 4“已过号”(`index.vue:33-45,235` | 两个 QTab 映射 1/4`reception.py:152-156,334-336`) | exact | 状态集合一致。 |
| 今日范围 | 每次请求带当天 `start_date/end_date``index.vue:296-305` | 未发送日期(`reception.py:344-361` | missing / P0 | 非今日记录污染工作台并暴露写动作。 |
| 搜索 | `patient_name`,清空也自动搜索(`index.vue:20-32,296-305,431-436`) | UI 只在回车/搜索按钮触发;repository 将 `keyword` 改为 `patient_name``reception.py:159-168,352-360`; `repository.py:231-236` | partial | 请求字段一致;清空不自动刷新。 |
| 分页与数量 | page size 15、另一状态独立 count、无限滚动、去重合并(`index.vue:212-213,296-318,359-379,606-620`) | 固定第一页 50 条,仅显示已加载条数(`reception.py:344-386` | partial | 50 条以上不可见;没有 waiting/passed 总数或加载更多。 |
| 轮询 | 5 秒链式轮询、倒计时、队列/详情/双 count、防重入、页面隐藏暂停/恢复立即刷新(`index.vue:254-259,539-592`) | 8 秒 QTimer,只刷新当前队列;页面 QWidget 隐藏时停(`reception.py:138-140,663-672` | partial | 无倒计时/另一状态 count;应用最小化不等价于 `document.hidden`;周期不同。 |
| 异步选人 | 每次选人递增 seq、清空详情、结果校验 seq + selected id`index.vue:398-418`) | 请求进行中拒绝新选人;旧结果仍应用(`reception.py:406-418,437-447` | missing / P0 | 可显示旧患者详情并对新选择行执行其他动作。 |
| 队列行字段 | id、patient/diagnosis/doctor/assistant、日期时间、性别年龄、status、处方、remark`index.vue:215-233`);实际显示姓名、性别年龄、过号、时间、医助(`:57-90` | 显示同一核心子集(`reception.py:50-95`) | exact | 行上显示字段基本一致。 |
| 聚合详情端点 | GET `/doctor.appointment/reception?id``api/patient.ts:9-12` | `get_reception()` 同端点/参数(`repository.py:242-250`) | exact | 传输合同一致。 |
| 患者/病例字段 | PatientInfo 显示脱敏电话、性别年龄、身高体重地区、预约、医生/客服、状态/开方、remark(`PatientInfoCard.vue:3-25`);PatientCase 显示基本信息、生命体征、糖尿病史、现病史、既往/其他病史、处方意见(`PatientCaseCard.vue:8-124`) | 仅预约时间/电话/医生/医助,加主诉、临床诊断、舌脉、治则摘要和开方提示(`reception.py:241-285,471-516` | partial / P1 | 大量临床字段缺失;无法达到基准的接诊前核对深度。 |
| 手机脱敏 | 两张卡都调用 `maskPhone``PatientInfoCard.vue:8`; `PatientCaseCard.vue:15` | 直接显示 `patient_phone/phone/phone_masked` 中第一个非空值(`reception.py:478-480` | missing / P1 | API 若下发明文即展示;未受 `tcm.diagnosis/phonePlain` 控制。 |
| 医生备注读取 | 聚合详情 `doctor_notes`,编辑/只读还会 GET `/doctor.appointment/doctorNotes` 并保留详情兜底(`readonly.vue:177-217`; `api/patient.ts:29-32` | 仅消费聚合详情中的 `doctor_notes/notes``reception.py:518-548`);Remote 无 `doctorNotes` 方法 | partial | 无独立补拉;备注元数据只显示 creator/time,忽略 `note_date` 语义。 |
| 文字备注写入 | `diagnosis_id + trimmed content`,最多 500 字,空值/无诊单拒绝(`index.vue:175-181,514-536` | 同端点和空值/诊单检查(`reception.py:575-601`; `repository.py:257-272`),但 QTextEdit 无 500 字限制 | partial | 端点 exact;客户端长度边界缺失。 |
| 舌苔/报告 | NoteTimeline 支持最多 99 个图片/文件、新增后刷新、单附件确认删除(`NoteTimeline.vue:3-40,52-101,195-227,260-268`; `api/patient.ts:19-40` | repository 可发送 `tongue_images/report_files`,但 UI 不展示附件、无上传/预览/删除入口(`repository.py:257-272`; `reception.py:524-548` | missing / P1 | 核心检查资料不可见、不可维护。 |
| 日常记录 | 只读 DailyMatrix,按诊单/患者加载(`index.vue:129-139`);7/30/自定义窗、trackingWindow/trackingNotes`DailyMatrix.vue:410-458,550-564,838-875` | 无 UI、模型和 repository 合同 | missing / P1 | 接诊时看不到血糖血压、饮食、运动、跟踪备注及阈值。 |
| 编辑病历 | `tcm.diagnosis/edit` 权限后打开 `edit.vue``index.vue:155-161,474-480` | 无按钮、无 `tcm.diagnosis/detail/edit` repository 方法 | missing / P1 | 接诊工作台不能维护病历。 |
| 通知医助 | POST `/doctor.appointment/notifyAssistant {id}`,防重入、成功后刷新队列与详情(`index.vue:460-471`; `api/patient.ts:14-17`) | 端点/防按钮重入一致,但只 toast、不刷新(`reception.py:558-573`; `repository.py:252-255` | partial | 后端动作 exact;页面不会立即反映服务端副作用。 |
| 完成接诊 | 仅 apt.status 1/4,权限 `doctor.appointment/complete`,不可逆确认,POST `{id: apt.id}` 后清选中并重载(`index.vue:162-171,492-511`; `api/doctor.ts:84-87` | 同权限、确认和端点(`reception.py:326-328,610-641`; `repository.py:274-277` | partial | 正常队列内近似 exact;受“非今日队列”和错患者详情竞态放大风险,UI 本身未再校验状态。 |
| 通话标识 | 缺 patient_id 拒绝;`diagnosis_id = row.diagnosis_id || row.id``index.vue:438-453` | app 同时要求 patient/diagnosis idpayload 优先真实 diagnosis_id`reception.py:643-661`; `app.py:496-537` | exact | ID 未互换。 |
| 通话生命周期 | ChatDialog 调 get signature、start、bind、end`chat-dialog/index.vue:641-695,996-1032,1089-1096` | Remote 同四端点(`repository.py:443-483`),FIFO 生命周期按 start→bind→end`video/lifecycle.py:184-209,211-263,265-292` | exact(核心) | Python 的核心通话落库顺序一致。 |
| 通话扩展 | ChatDialog 还有 IM、截图写备注、云/本地录像上传(`chat-dialog/index.vue:129-175,241-265,1276-1301` | 原生 companion 未提供这些管理后台附属入口 | partial | 核心视频可用;聊天、截图备注和本地录像回填不等价。 |
### 接诊台权限差异
- 基准悬浮备注/编辑/完成分别使用 `doctor.appointment/addDoctorNote``tcm.diagnosis/edit``doctor.appointment/complete``patient/reception/index.vue:146-171`)。Python 已实现备注与完成门控,但编辑缺失(`reception.py:322-331`)。
- 基准队列行“通知医助”和“发起通话”没有 `v-perms``index.vue:75-90`)。Python 额外要求 `doctor.appointment/notifyAssistant``tcm.diagnosis/videoQr``reception.py:322-325`)。后者在基准中是“小程序视频二维码”权限,不是直呼权限;这可能把拥有接诊页但没有二维码权限的医生挡在视频之外,判为 **partial / P1**
- NoteTimeline 的“添加文字备注”检查 addDoctorNote 权限,但两个媒体 picker 和删除图标只受 readonly 控制(`NoteTimeline.vue:3-40,64-99,150-151`)。这是基准自身的权限不对称,不能据此推导 Python 应无条件开放媒体写入。
## 5. 问诊列表逐项差异
### 5.1 列表、筛选与分页
| 项目 | 基准 | Python | 判定 |
|---|---|---|---|
| 列表端点 | GET `/tcm.diagnosis/lists``api/tcm.ts:3-6` | 同端点(`repository.py:414-440` | exact |
| 分页 | `usePaging` 默认 page size 15,发送 `page_no/page_size``usePaging.ts:13-21,25-48` | page size 20、PageResult 容错(`consultations.py:93-95,264-300` | partial |
| 默认范围 | PC/H5 均默认 `appointment_date=today``index.vue:2174-2182`; `index_h5.vue:1427-1437`) | 默认日期同为今天;Remote 在起止相等时转换为 `appointment_date``consultations.py:129-141,248-251,264-282`; `repository.py:420-424` | exact(日期) |
| 默认状态 | 基准默认今天的所有挂号语义,不额外传 appointment status | Python status 下拉默认 1,并转成 `appointment_status=1``consultations.py:121-128,276-279`; `repository.py:429-434` | partial / P1 |
| 关键词 | `keyword`,提示姓名/手机号(`index.vue:72-75,773-775` | 同字段(`consultations.py:116-120,276` | exact |
| 日期快捷项 | 前天、昨天、今天、明天、后天、全部;另有待预约、已完成、待分配及 9 路角标(`index.vue:963-1085`) | 单个起止日期控件和“今天”;无角标 | partial |
| 挂号/确认 | `has_appointment` 0/1`diagnosis_confirmed` 0/1`index.vue:78-97,804-833` | 无这两个筛选 | missing |
| 诊断/证型/医助 | 字典筛选(`index.vue:104-113,1158-1188` | 无 | missing |
| 最近挂号 | 起止日期 + 渠道,且与 appointment_date/未挂号互斥(`index.vue:114-149,1197-1228` | 多日 start/end 被隐式映射为 latest appointment 起止,但无渠道、无互斥提示(`repository.py:420-428` | partial |
| 最近指派 | 起止日期(`index.vue:124-133,1203-1216,1230-1232` | 无 | missing |
| 未服务排序 | `sort_unserved_days=asc/desc``index.vue:297-304,1234-1248` | 无 | missing |
| 待分配宽搜 | 有关键词时只保留 page、pending_assign 和 pending_assign_keyword,主动清空其他筛选(`index.vue:836-861,923-954` | 无 | intentionally unsupported(医助分配) |
| 定时刷新 | PC 20 秒且 document.hidden 时跳过(`index.vue:1098-1105`);H5 15 秒(`index_h5.vue:1418-1437` | 20 秒,页面 QWidget 隐藏时停(`consultations.py:238-240,325-334` | exactPC 周期)/ partial(可见性语义) |
| 路由 query id | `?id=` 会在异步 edit ref 就绪后打开编辑(`index.vue:1343-1357,2184-2188` | 无 deep-link | missing |
Python 的“状态下拉”不是基准筛选:它提供待接诊/取消/完成/过号(`consultations.py:121-127`),Remote 再把 3 映射为 `completed_appointment=1`,其他值映射成 `appointment_status``repository.py:429-434`)。基准 PC 没有该下拉;“已完成”定义为至少有一条 appointment.status=3`index.vue:1007-1020`),取消状态也不在展示映射中。因此这里只能判 **partial**,不能视为同一合同。
### 5.2 行字段与状态映射
基准 PC 行字段:
- 诊单 ID/NEW、患者姓名、性别年龄(`index.vue:191-218`)。
- `appointments[]` 多挂号;回退到 appointment_id/status/doctor/time 主字段;最近渠道(`:219-261,1747-1757`)。
- 确认状态来自 `DiagnosisViewRecord.some(is_confirmed == 1)``:262-267,1313-1317`)。
- 复诊时间/医生/处方已作废、助理、是否开方(`:268-296`)。
- `unserved_days` 与 last blood tooltip,阈值 null 灰、>=7 红、3-6 橙、<=2 绿(`:297-317,1319-1329`)。
- `video_call_hint.state` 映射 none/live/pending_room/label`:318-341,1373-1427`)。
Python 只显示预约时间、患者、状态、联系方式、医助、确认、处方、remark(`consultations.py:183-225`),判 **partial / P1**。具体错误为:
1. `Consultation` 模型没有正式字段 `has_appointment``appointment_status``appointments`、followup、unserved、last blood、video hint;虽保留 raw,但 UI 不读取这些字段(`core/models.py:322-350`)。
2. 模型从根 `status` 优先取值,只有根 status 缺失才使用首个 nested appointment.status`:362-366,406-414`)。基准明确把 appointment status 放在 `appointment_status`/`appointments[].status`,并将诊单本身的 status 用作启用状态。这是 P0 视频门槛错误的根因。
3. Python `diagnosis_confirmed` 属性只来自根 `confirmed/diagnosis_confirmed``:413-423`),不计算 `DiagnosisViewRecord`;存在把已确认显示成待确认的风险。
4. Python 联系方式列直接显示 `patient_phone/phone/phone_masked``consultations.py:195-201`),而基准 PC/H5 列表不展示手机号明文;编辑页仅 `tcm.diagnosis/phonePlain` 可保持明文(`edit.vue:822-856,1263-1277`)。判 **missing / P1(隐私)**
挂号状态基准只有:1=已预约、3=已完成、4=已过号(`index.vue:1732-1745`);取消仅允许 status 1/4,status 3 明确拒绝,且多挂号必须按具体 appointment id 操作(`:1782-1857`)。Python 把 2 显示为“已取消”,并把 4 也设为可直呼(`consultations.py:53-58,310-317`),不符合基准。
### 5.3 详情、编辑与操作
| 能力 | 基准行为与证据 | Python | 判定 |
|---|---|---|---|
| 只读病例详情 | PC 查看/双击需 `tcm.diagnosis/readonlyDetail`,路由 query id;加载 `/tcm.diagnosis/readonlyDetail` + doctorNotes`index.vue:345-354,2165-2172`; `readonly.vue:172-240`; `api/tcm.ts:8-11`) | 表格双击直接发起视频;无详情页/端点(`consultations.py:227-228,319-323` | missing / P1 |
| H5 简版详情 | 卡片头/体打开 `detail.vue`GET `/tcm.diagnosis/detail`,显示基础、病史、舌苔/报告、症状舌脉治则处方医嘱状态(`index_h5.vue:121,144`; `detail.vue:10-122,157-165` | 无 | missing |
| 完整诊单编辑 | `tcm.diagnosis/edit`GET detail 后 POST edit;新增用 add;手机号/身份证唯一性检查(`edit.vue:1263-1375`; `api/tcm.ts:44-61,77-90` | 无 repository 方法或 UI | missing / P1 |
| 编辑字段 | patient/id card/phone/gender/age/marital/height/weight/region/BP/glucose/type/status/source/card/current medicine/local diagnosis、全部现病史/既往史/其他史/symptoms/remark`edit.vue:86-624,916-970` | Consultation 只存列表摘要(`core/models.py:322-350` | missing / P1 |
| 编辑验证 | 姓名、手机、性别、年龄、空腹血糖、诊断类型、当地医院必填;手机号正则;身份证 18 位正则;病史发现最多 50(`edit.vue:1026-1175` | 无 | missing |
| 开方/查看 | `tcm.diagnosis/kaifang`;审核通过且未作废显示“查看”,否则“开方”;传 diagnosis id + appointment id`index.vue:354-363,1671-1694`) | 无问诊行开方入口;只有独立已开处方列表/详情 | missing / P1 |
| 处方边界 | 先按 appointment 查已有;临床诊断、至少一味药、药名/正剂量、无重复、手写签名必填;可作废但有业务订单时禁止(`tcm-prescription/index.vue:1829-1922,2219-2328` | Remote 仅 list/detail issued prescription`repository.py:354-397`),无 add/getByAppointment/void | missing / P1 |
| 预约 | `tcm.diagnosis/guahao`;医生排班/未来 7 天/可用时段;今天过去时段禁用;渠道必填;指定自媒体渠道补充必填;今天已有 status1/4 禁止重复约今天(`appointment.vue:239-420,423-448,535-619,669-724` | 无 | intentionally unsupported(后台/医助调度) |
| 取消挂号 | status1/4 可取消,3 拒绝,多 appointment 精确到子记录(`index.vue:1782-1857`; `api/doctor.ts:59-67` | 无 | intentionally unsupported(后台/医助调度) |
| 指派/取消指派 | `tcm.diagnosis/assign`;单条/批量、继承标志、取消传 assistant_id=0`index.vue:1532-1661` | 无 | intentionally unsupported(医助管理) |
| 视频二维码/确认二维码 | 仅 `has_appointment && appointment_status=1`;需 weapp config;权限 `videoQr`/`guahao``index.vue:390-391,1778-1780,1964-2055` | 用同一 `videoQr` 权限启动原生直呼(`consultations.py:102-106` | intentionally unsupported(二维码)+ partial(权限/状态复用错误) |
| 医助旁观 | 仅当前被指派医助 + `tcm.diagnosis/watchCall`pending_room 显示但不能进,live 才进;TRTC 只拉流(`index.vue:1380-1413`; `AssistantWatchCallDialog.vue:120-219`) | 无旁观角色/入口 | intentionally unsupported(医助角色) |
| 挂号日志 | `tcm.diagnosis/guahaoLogList`,空数组容错,显示 action/operator/summary/time`index.vue:1859-1897` | 无 | intentionally unsupported |
| 补身份证 | 15/18 位前端校验,POST fillIdCard 后自动年龄(`index.vue:1918-1961` | 无 | intentionally unsupported(后台资料维护) |
| 企微记录 | records/contact 并行、20 条分页、文字 note 新增/删除(`index.vue:2066-2160` | 无 | intentionally unsupported(后台企微归档) |
| 创建订单 | order_type、amount、remark 后生成订单二维码(`index.vue:1430-1515` | 无 | intentionally unsupported(后台运营) |
### 5.4 只读详情的权限化子区块
基准 `/tcm/diagnosis-readonly` 不是一个简单摘要,它按权限加载:
- `tcm.diagnosis/dailyRecord`DailyMatrix`readonly.vue:42-54,183`)。
- `doctor.appointment/addDoctorNote`NoteTimeline 的可写版本在 editreadonly route 固定只读(`readonly.vue:56-70`)。
- `tcm.diagnosis/patientOrders`:业务订单(`:74-88`)。
- `tcm.diagnosis/huifang`:通话录制回放(`:90-101`)。
- `tcm.diagnosis/chat`IM 记录(`:103-113`)。
- `tcm.diagnosis/assign``tcm.diagnosis/detail`:指派日志(`:115-131`)。
- `doctor.appointment/lists`:挂号记录(`:133-146`)。
Python 问诊列表没有选中详情容器,因此上述全部为 **missing**;其中病例、日常记录、备注、挂号记录属于医生核对链路,列 P1;订单、指派和后台 IM 可按角色继续列 intentionally unsupported。
## 6. API 覆盖矩阵
此表只包含从上述可达 view/component 实际导入的业务 API;端点定义证据集中在 `D:\web\zyt\admin\src\api\patient.ts:3-40``api\doctor.ts:49-87``api\tcm.ts:3-109,202-310,313-372,591-648`
| API 组 | 管理后台可达用途 | Python 状态 |
|---|---|---|
| `doctor.appointment/lists` | 接诊队列、预约/挂号记录、最近就诊/今日重复检查 | **exact 端点 / partial 参数**;接诊台漏 today |
| `doctor.appointment/reception` | 接诊聚合详情 | **exact** |
| `notifyAssistant` | 通知医助 | **exact 端点 / partial 刷新与权限** |
| `addDoctorNote` | 文字、舌苔、报告、通话截图 | **partial**;Remote 参数齐,UI 只有文字 |
| `doctorNotes`, `deleteDoctorNoteImage` | 备注补拉、附件删除 | **missing** |
| `doctor.appointment/complete` | 完成接诊 | **exact 端点 / partial 上下文边界** |
| `tcm.diagnosis/lists` | 问诊列表、全部角标 | **exact 端点 / partial filters/model** |
| `readonlyDetail`, `detail`, `add`, `edit`, `delete`, `checkPhone`, `checkIdCard`, `fillIdCard` | 详情与资料维护 | **missing** |
| `assign`, `assignLogList`, `getAssistants`, `watchCall` | 医助分配与旁观 | **intentionally unsupported** |
| `trackingWindow`, `trackingNotes`, `addTrackingNote` | 日常记录/备注窗 | **missing** |
| blood/diet/exercise add/edit | 编辑页日常记录 | **missing** |
| diagnosisTodo lists/add/cancel | 日常记录待办 | **missing** |
| `getCallSignature`, `startCall`, `bindCallRoom`, `endCall` | 原生实时视频核心 | **exact** |
| `getCallRecords`, `attachLocalCallRecording`, `createManualCallRecord` | 回放与人工补传 | **missing**(可选扩展) |
| `getImChatMessages`, `triggerImChatSync` | IM 归档 | **missing** |
| `prescription/listByDiagnosis`, `getByAppointment`, `add`, `void` | 病例处方与开方 | **missing / P1** |
| `prescription/detail`, `prescription/lists` | 查看已开处方 | **exact**,但只在独立页面 |
| `prescriptionLibrary/lists` | 开方时导入本人模板 | **exact endpoint**,但问诊开方 UI 缺失 |
| availableSlots/create/roster/lists | 预约 | **intentionally unsupported** |
| generateMiniProgramQrcode/generateOrderQrcode + weapp config | 二维码 | **intentionally unsupported**,由原生直呼替代 |
| WeChat records/contact/add/delete | 企微归档 | **intentionally unsupported** |
| prescriptionOrder lists/detail/logs | 只读业务订单 | **intentionally unsupported** |
Remote 的相关实现块只覆盖 appointment/reception/actions`D:\web\zyt\app\src\doctor_workstation\services\repository.py:226-277`)、diagnosis list 与四个 call endpoint`:414-483`);因此上述“missing”不是 UI 隐藏但 repository 已具备,而是端到端合同确实不存在。
## 7. 权限码对照
### 基准中实际出现
- 页面/读取:`doctor.appointment/lists``tcm.diagnosis/lists``tcm.diagnosis/readonlyDetail``tcm.diagnosis/detail``tcm.diagnosis/dailyRecord``tcm.diagnosis/patientOrders``tcm.diagnosis/huifang``tcm.diagnosis/chat`
- 接诊写动作:`doctor.appointment/addDoctorNote``doctor.appointment/complete``tcm.diagnosis/edit`
- 问诊列表动作:`tcm.diagnosis/add``edit``delete``assign``kaifang``guahao``videoQr``watchCall``guahaoLogList``order`
- 编辑扩展:`tcm.diagnosis/phonePlain``tcm.diagnosis/chufang``tcm.diagnosis/setRevisitSlotStartOffset``tcm.prescriptionOrder/detail`
证据:接诊 `patient/reception/index.vue:146-171`;问诊 PC `tcm/diagnosis/index.vue:318-400`readonly `readonly.vue:42-146`edit `edit.vue:674-748,822-856`PatientOrderList `components/PatientOrderList.vue:20-33,99-114`
### Python 实际门控
- 页面:两个页面码 **exact**`shell.py:40-75`)。
- 接诊:`notifyAssistant``videoQr``complete``addDoctorNote``reception.py:322-331`);缺 edit。
- 问诊:只有 `videoQr``consultations.py:102-106`);其余动作根本没有 UI。
- Python PermissionSet 本身支持 `*`、all/any`core/permissions.py:60-104`),问题在页面选择了哪些码,而不是权限容器能力。
特别注意:Web 的 `v-perms` 是数组 OR`install/directives/perms.ts:13-32`),`hasPermission()` 工具是数组 AND`utils/perm.ts:3-17`);当前页面绝大多数调用只有一个码,唯一显式 OR 的 assign/detail 是两个独立调用(`readonly.vue:117-120`),所以本次差异不受二者实现差别影响。
## 8. P0 / P1 缺口清单(按严重度排序)
### P0
1. **RACE-RECEPTION-01:快速切换患者导致详情与选择错配。** 修复验收应覆盖 A 请求未返回时选 B,A/B 任意顺序返回后 UI、备注 diagnosis_id、通知/完成 appointment_id 和视频 payload 必须全部指向 B;详见第 1、4 节证据。
2. **SCOPE-RECEPTION-02:接诊列表漏 `start_date=end_date=today`。** 必须在 UI 或 repository 的专用 reception query 中固定今日,不能让通用 appointment list 隐式决定。
3. **STATE-CALL-03:问诊视频使用 `status` 而非 `has_appointment + appointment_status`,且错误允许 4。** 模型和 UI 都需改;只修 UI 的字段名仍会被当前 dataclass 的根 status 优先级遮蔽。
### P1
1. **DETAIL-04:问诊列表没有 `readonlyDetail`/详情入口,双击反而直呼。** 医生无法先核对完整病例、日常记录、备注和挂号历史。
2. **EDIT-05:接诊台和问诊列表均无诊单详情/edit 合同。** 基准接诊台明确提供“编辑病历”。
3. **RX-06:没有从诊单开方/查看/作废的工作流。** 独立“已开处方”页面不能替代 `diagnosis_id + appointment_id` 上下文开方。
4. **RECEPTION-DETAIL-07:接诊详情只显示摘要,缺完整 PatientCase、日常记录、备注附件和附件删除。**
5. **PRIVACY-08:接诊和问诊列表可能直接显示明文手机号,未遵守基准的 mask/phonePlain 边界。**
6. **PERM-09:用 `tcm.diagnosis/videoQr` 保护原生直呼、用未在基准按钮上出现的 `doctor.appointment/notifyAssistant` 保护通知,可能让合法接诊医生缺动作;同时缺 `tcm.diagnosis/edit` 入口。** 需要后端权限清单确认专用 call permission,而不是继续借用二维码码。
7. **MODEL-10Consultation 未正式解析 appointment_status、has_appointment、多挂号、DiagnosisViewRecord、unserved/video hint 等;当前确认和状态显示不可靠。**
8. **FILTER-11:问诊筛选缺 has_appointment、confirmed、诊断类型、证型、医助、最新挂号渠道、最新指派、未服务排序及顶部 count;Python 自创 status 下拉又改变默认结果集。**
9. **BOUNDARY-12:文字备注没有 500 字客户端限制;通知成功不刷新;完成动作未在 handler 中二次验证 1/4。**
不列 P0/P1 的 intentionally unsupported 项:H5 响应式布局、小程序/订单二维码、批量指派、医助旁观、企微后台归档、后台订单创建和排班预约。若产品决定医生桌面也承担医助/运营职责,应把这些重新分类为 missing 并另立需求。
## 9. 基准自身的可疑点(不可静默照搬)
以下均是当前 `admin/src/views` 的真实行为,本报告仅记录,不用其他资料修正它:
1. 接诊台“编辑病历”把 `diag.patient_id` 传给 `edit.open('edit', id)``patient/reception/index.vue:474-476`),而 edit 将该 id 直接传给 `/tcm.diagnosis/detail``tcm/diagnosis/edit.vue:1263-1265,1288-1302`);问诊列表传的则是 `row.id``index.vue:1667-1669`)。这是明显 ID 口径冲突。
2. PC/H5 的“视频二维码”请求把 `diagnosis_id` 赋成 `row.appointment_doctor_id``index.vue:1987-1993`; `index_h5.vue:1262-1268`),而“诊单二维码”正确使用 `row.id``index.vue:2037-2042`)。Python 原生直呼当前没有复制此错误。
3. `PatientCaseCard` 的 consultation_type 三元两边都返回“复诊”(`PatientCaseCard.vue:148-151`)。
4. 接诊行通知/通话无权限指令、NoteTimeline 媒体写入未复用 addDoctorNote 门控,见第 4 节;应先确认服务端授权模型,再决定桌面行为。
## 10. 建议的最小合同测试
后续实现至少应加入不发网络的合同测试:
1. Reception request 必含 status、today start/end、patient_name、page_no/page_size。
2. A/B 详情乱序返回不会产生跨患者详情或写动作目标。
3. Consultation row 的 `status=1, appointment_status=3` 不可视频;`status=1, has_appointment=1, appointment_status=1` 可视频;status 4 一律不可从问诊列表直呼。
4. `DiagnosisViewRecord=[{is_confirmed:1}]` 显示已确认;多 appointments 保留并正确挑选 active appointment。
5. 无 phonePlain 权限时列表和详情只出现掩码。
6. readonlyDetail、edit 和 prescription 动作分别由准确权限码控制;缺码时不创建可调用控件。
7. 备注 501 字在客户端拒绝;媒体 note payload 保留 tongue/report 数组;删除附件的三字段合同固定。
+408
View File
@@ -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. 同文件队列分页/筛选 fixtures247-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。
+329
View File
@@ -0,0 +1,329 @@
# DoctorWorkstation 发布打包就绪审计
审计日期:2026-08-11Asia/Shanghai
审计主机:Windows 10/11 工作区 `D:\web\zyt\app`
审计边界:只检查入口、脚本、PyInstaller spec、冻结依赖、伴随页、文档和既有产物;未执行完整 PyInstaller,未改业务/打包源码。
## 结论
**当前状态:不可直接发布既有 `dist`,也不应立即从当前 Git 状态制作正式发布包。**
构建流水线的主体是完整的:Windows/macOS 均有根目录中英文入口,能从锁文件准备 Python/Node 依赖,能构建 TRTC Web companionPyInstaller 使用 onedir/.app,构建后会检查 QtWebEngine 与 companion,并在隔离目录中运行冻结入口 `--smoke-test`。Windows 既有 ZIP 的命名和 SHA-256 也正确。
但发布前有三个 P0:macOS 入口在 Git 中没有可执行位;新增 QtMultimedia 业务文件仍未纳入版本控制且工作树存在大量未提交发布输入;当前 Windows 成品早于新增媒体代码,缺少 QtMultimedia Python 模块、Widgets 库及媒体插件。另有 P1:当前冻结烟测会在媒体能力缺失时仍通过,生产级签名/公证也尚未进入一键流水线。
**macOS 特别说明:PyInstaller 不能从 Windows 交叉产出或验收 `.app`。本审计只能检查 macOS 脚本和合同;最终 `.app`、原生 Qt/媒体插件、签名、摄像头/麦克风权限及 TRTC 实通必须在原生 macOS(对应 arm64 或 x86_64)上构建和验证。**
严重度定义:
- P0:在任何正式构建/分发前必须解决。
- P1:可做内部技术构建,但生产发布前必须解决或由发布负责人书面接受风险。
- P2:不阻断本次内部构建,建议纳入发布工程改进。
## 构建前阻断
### P0-1macOS Finder/命令行入口在 Git 索引中全部为 `100644`
`git ls-files --stage -- '*.command' 'scripts/*.sh'` 显示以下文件均为 `100644`,不是 `100755`
- `run_macos.command`
- `package_macos.command`
- `一键运行.command`
- `一键打包.command`
- `scripts/build_macos.sh`
- `scripts/package_macos.sh`
- `scripts/run_macos.sh`
- `scripts/macos_helpers.sh`
- `scripts/check_macos_entrypoints.sh`
影响:新的 macOS clone 中,Finder 双击 `.command` 和 README 中的 `./scripts/build_macos.sh` 不能满足“一键运行/一键打包”;`scripts/check_macos_entrypoints.sh` 自己也要求这些入口 `-x`。Windows/NTFS 下 Git Bash 把文件视为可执行,因此当前合同检查通过并不能覆盖 Git 模式错误。
发布前处理:
```powershell
git update-index --chmod=+x run_macos.command package_macos.command `
"一键运行.command" "一键打包.command" `
scripts/build_macos.sh scripts/package_macos.sh scripts/run_macos.sh `
scripts/macos_helpers.sh scripts/check_macos_entrypoints.sh
git ls-files --stage -- '*.command' 'scripts/*.sh'
```
第二条命令的每一行都必须以 `100755` 开头,并将模式变化提交到发布 commit。
### P0-2:当前发布输入不在可复现的 Git 状态
审计时工作树含多处已修改和大量未跟踪代码;与本次媒体发布直接相关的 `src/doctor_workstation/ui/diagnosis_media.py` 及对应测试仍是未跟踪文件。若从当前目录构建,PyInstaller 会把这些本地文件打入包;若从当前 HEAD/干净 CI clone 构建,则不会包含它们。两者结果不一致。
发布前必须由负责人审阅 `git status --short`,只纳入确认属于本次版本的源码、测试、脚本和文档,并从确定的 commit/tag 构建。不要用 `git add -A` 掩盖当前大量测试产物和临时目录。
### P0-3:既有 Windows ZIP/onedir 早于新增 QtMultimedia 代码,禁止分发
证据:
- `dist/DoctorWorkstation/DoctorWorkstation.exe`2026-08-10 17:07。
- `dist/DoctorWorkstation-Windows-x64-0.1.0.zip`2026-08-10 17:08。
- `src/doctor_workstation/ui/diagnosis_media.py`2026-08-11 01:05。
- 既有成品只有 `Qt6Multimedia.dll`/QML 辅助文件,缺少:
- `PySide6/QtMultimedia.pyd`
- `PySide6/QtMultimediaWidgets.pyd`
- `PySide6/Qt6MultimediaWidgets.dll`
- `PySide6/plugins/multimedia/ffmpegmediaplugin.dll`
- `PySide6/plugins/multimedia/windowsmediaplugin.dll`
既有 ZIP 也缺少同一组文件。它的 SHA-256 文件本身有效,但只证明旧 ZIP 未损坏,不证明它对应当前源码。
此外,根目录“一键运行”优先启动既有 `dist`,所以当前直接双击会运行旧代码,不会验证当前工作树。正式验收前必须完成干净重建,并以新 ZIP 的时间戳、文件清单和哈希替换旧证据。
## P1:生产发布前必须闭环
### P1-1QtMultimedia 依赖可用,但冻结收集没有独立门禁
通过项:
- `pyproject.toml` 依赖 `PySide6>=6.8.2,<7``uv.lock` 当前锁定 PySide6/PySide6 Addons/Essentials 6.11.1Addons 提供 QtMultimedia。
- 当前 `.venv``.venv-build` 均有 `QtMultimedia.pyd``QtMultimediaWidgets.pyd``Qt6MultimediaWidgets.dll` 以及 Windows `plugins/multimedia` 下的 FFmpeg/Windows Media 插件。
- 当前 PyInstaller 6.22 的官方 `hook-PySide6.QtMultimedia.py` 会追加 `PySide6.QtMultimediaWidgets`Qt 模块映射会收集 Qt 6 的 `multimedia` 插件。当前源码中的静态 import 理论上会触发该 hook。
缺口:
- `packaging/doctor_workstation.spec` 的显式 hidden imports 只有 QtWebEngine 相关模块,没有显式列出 `PySide6.QtMultimedia`/`PySide6.QtMultimediaWidgets`
- Windows/macOS 构建脚本只检查 QtWebEngine helper、Chromium `.pak``video_companion_dist/index.html`,不检查 QtMultimedia Python 扩展、Widgets 库或媒体后端插件。
- `diagnosis_media.py` 捕获 `ImportError` 并降级到外部打开,因此媒体模块缺失不会让应用启动失败。
- 实测旧冻结包缺少上述模块/插件,但隔离执行 `DoctorWorkstation.exe --smoke-test` 仍返回 0。这证明现有烟测不是 QtMultimedia 发布门禁。
发布前至少应做到以下二者之一,建议二者都做:
1. 在 spec 中显式纳入 `PySide6.QtMultimedia``PySide6.QtMultimediaWidgets`,继续使用 PyInstaller 官方 hooks 收集平台插件。
2. 在两个构建脚本中增加平台化的成品断言;Windows 检查两个 `.pyd``Qt6MultimediaWidgets.dll``plugins/multimedia`macOS 检查两个 Python 扩展、Qt multimedia framework/dylib 和 `plugins/multimedia`
最终验收还要用一个获准分发的 MP4(以及产品真实需要时的 HLS)实际播放,不能只看文件存在。
### P1-2`--smoke-test` 只证明冻结启动,不证明 WebRTC/媒体可用
当前烟测的隔离、30 秒超时、非零退出码和未捕获异常日志检查设计良好;Windows/macOS 都不会访问真实后端。它会启动 composition root,并在约 1.2 秒后退出。
它不执行以下能力:
- 创建 `QMediaPlayer` 并确认媒体 backend 被发现;
- 播放音视频并检查解码/渲染;
- 打开 `QWebEngineView` 的 companion 主文档;
- 执行 `TRTC.isSupported()`、请求摄像头/麦克风或加入真实测试房间;
- 验证设备插拔、休眠恢复、弱网、屏幕共享或录播 codec。
因此冻结烟测应保留为启动门禁,同时新增“冻结媒体插件合同”和原生机器上的手工/自动媒体准入。README 已提示首发要做真实 TRTC/设备验收,这项不能被 `--smoke-test` 替代。
### P1-3:生产分发签名链未纳入一键打包
Windows:当前流水线生成 ZIP 与 SHA-256,但没有 Authenticode 签名步骤。SHA-256 提供完整性,不提供发布者身份;对外分发前应在压缩前签名并验证最终可执行文件。
macOSspec 接受可选的 `MACOS_CODESIGN_IDENTITY`,构建脚本只运行 `codesign --verify --deep --strict`。未提供 Developer ID 时,PyInstaller 的临时/ad-hoc 签名也可能通过这一完整性检查;脚本不执行 notarization/stapling,也没有验证嵌套 `QtWebEngineProcess.app` 的实际 entitlements/运行能力。当前 README 仅把签名、公证作为人工说明。
若本次只是内部 QA 包,应明确标注“未签名/未公证内部构建”。若是生产或院外分发,应先把 Windows 签名,以及 macOS Developer ID → nested code 检查 → notarization → stapling → 重新生成最终 ZIP/哈希,纳入发布流水线。注意:staple 会改变 `.app`,所以不能继续分发公证前生成的 ZIP。
### P1-4macOS 成品尚无原生验证证据
Windows 上的 `bash -n` 和合同脚本只能证明 shell 文本结构。以下证据必须在 macOS 构建机补齐:
- `.app` 原生架构与最低系统版本;
- `QtWebEngineProcess.app`、Qt framework、QtMultimedia 插件实际存在并签名有效;
- `NSCameraUsageDescription`/`NSMicrophoneUsageDescription` 能触发正确权限流程;
- companion 的本地 `file:`/qrc 资源、WebChannel 和 TRTC 能在打包态工作;
- arm64/x64 输出名与实际二进制架构一致;
- 最终 ZIP 的 Gatekeeper/notary 结果。
这不是 Windows 端可以规避的检查;必须安排一台原生 macOS runner/构建机。
## P2:建议改进
1. 仓库没有 `.gitattributes`,而本机 `core.autocrlf=true`。当前 macOS shell 文件实际是无 BOM 的 LF`bash -n` 通过,但建议用 `.gitattributes` 固定 `*.sh`/`*.command` 为 LF,并固定批处理/PowerShell 的预期行尾。
2. Windows 使用 `uv sync --frozen`macOS 使用 `uv sync --locked`。当前 `uv.lock` 包含所需依赖,但 `--frozen` 不承担 lock 新鲜度检查;发布前可增加 `uv lock --check`,或统一为能拒绝 pyproject/lock 漂移的策略。
3. `packaging/windows/version_info.txt``0.1.0.0``pyproject.toml``0.1.0` 当前一致,但它是手工同步;脚本没有发布前一致性检查。升级版本时可能出现 ZIP 名与 EXE 版本资源不一致。
4. README 顶部的一键入口描述正确,但“测试与打包”段落调用 `build_*`,只产生 onedir/.app,不产生版本化 ZIP/哈希。建议明确区分“构建”与“发布打包”,并把 `package_*`/根入口列为正式发布命令。
5. `.env.example` 对 demo/生产、SSL、TRTC secret 禁止项说明清楚,未包含长期凭据;但发布 ZIP 不包含 README 或 `.env.example`,README 也没有明确冻结包从哪里查找 `.env`。若企业部署依赖环境文件,应说明冻结态放置/注入方式;若只允许登录页保存非敏感配置,也应明确说明无需随包放 `.env`
6. Windows 用固定 `dist/SHA256SUMS.txt`macOS 用相邻的 `<zip>.sha256`;两者都可校验,但发布自动化和用户说明可以统一。`dist` 还可能保留旧版本 ZIP,根打包入口成功后打开整个目录,建议在发布清单中明确唯一应交付的文件。
7. `Build_DoctorWorkstation.bat -ValidateOnly` 会打印 `Package ready in: ...`,即使它只做了入口预检;不影响退出码,但容易被误认作已生成新包。
8. spec 没有设置 Windows EXE/macOS bundle 的原生应用图标;窗口内 SVG 正常,但系统文件/Finder 图标仍是默认值。若品牌发布有要求,应在最终签名前补齐 `.ico`/`.icns`
9. AGENTS.md 指向的 `.trellis/workflow.md``.trellis/spec/` 在本工作区不存在;本次无法执行项目内 Trellis 规范审查。这不影响二进制启动,但属于发布治理缺口。
## 已通过的检查
| 检查项 | 结果 | 备注 |
|---|---:|---|
| Windows 根英文入口 `Build_DoctorWorkstation.bat -ValidateOnly` | PASS | Node/uv/必需文件预检通过 |
| Windows 根英文入口 `Run_DoctorWorkstation.bat -ValidateOnly` | PASS | 当前命中既有冻结 EXE;不代表源码新鲜 |
| Windows 两个中文别名 `-ValidateOnly` | PASS | 正确转发参数和退出码 |
| PowerShell parser | PASS | `build_windows.ps1``package_windows.ps1``run_windows.ps1` 均无解析错误 |
| macOS `bash -n` | PASS | helpers、run/package/build/check 与四个 `.command` 均通过 |
| `scripts/check_macos_entrypoints.sh` | PASS(有限) | Git Bash 下合同通过;不能覆盖 Git `100644` 问题 |
| 入口/冻结异常合同 pytest | PASS | `tests/test_one_click_entrypoints.py` + `tests/test_entrypoint.py`4 passed |
| 当前 source QtMultimedia import | PASS | `QMediaPlayer``QAudioOutput``QVideoWidget` 均可导入;两个 Windows multimedia plugin 可见 |
| TRTC companion 工作树 | PASS | `video_companion` 无 Git 变更;dist 新于源码 |
| companion 相对资源 | PASS | `base: './'`index 引用的 JS/CSS 均存在 |
| companion 打包副本 | PASS(旧包) | source dist 与旧 frozen 副本的 `index.html` SHA-256 相同 |
| spec companion/resources 映射 | PASS | `video_companion/dist -> video_companion_dist``resources -> resources` 与运行时查找一致 |
| QtWebEngine 收集策略 | PASS | 显式 imports 触发官方 hooks,构建脚本检查 helper 与 `.pak` |
| 冻结启动烟测机制 | PASS(范围有限) | 旧 EXE 在隔离 demo/offscreen 环境中 `--smoke-test` 退出 0;同时暴露媒体缺口未被检测 |
| Windows 输出名 | PASS(旧包) | `DoctorWorkstation-Windows-x64-0.1.0.zip` |
| Windows SHA-256 | PASS(旧包) | `SHA256SUMS.txt` 与实际 ZIP hash 一致 |
| 版本号一致性 | PASS | pyproject `0.1.0`Windows resource `0.1.0.0` |
## 入口、spec 和输出合同摘要
### Windows
- 根入口:中文别名 → 英文 `.bat``scripts/package_windows.ps1`/`run_windows.ps1`
- 运行:优先 `dist/DoctorWorkstation/DoctorWorkstation.exe`,否则使用 `.venv``uv sync --frozen` 后运行源码。
- 发布打包:Node 20+ → `uv sync --frozen --extra build``npm ci` → companion build → PyInstaller `--clean` → 静态成品检查 → 隔离冻结烟测 → ZIP → SHA-256。
- 产物:
- onedir`dist/DoctorWorkstation/`
- 启动器:`dist/Start_DoctorWorkstation.bat`
- ZIP`dist/DoctorWorkstation-Windows-x64-<version>.zip`
- 校验:`dist/SHA256SUMS.txt`
### macOS
- 根入口:四个 `.command` 解析自身目录后用 `/bin/bash` 调用 `scripts/run_macos.sh``scripts/package_macos.sh`
- 运行:优先 `/usr/bin/open dist/DoctorWorkstation.app`,否则确保 uv、同步锁定依赖并运行源码。
- 发布打包:原生 Darwin 检查 → uv/Node 20+(必要时下载)→ `npm ci` → companion build → PyInstaller `.app` → QtWebEngine/companion 检查 → codesign verify → 隔离冻结烟测 → `ditto` ZIP → SHA-256。
- 产物:
- bundle`dist/DoctorWorkstation.app`
- ZIP`dist/DoctorWorkstation-macOS-{arm64|x64}-<version>.zip`
- 校验:同路径 `<zip>.sha256`
## 主代理最终构建与验证命令
以下命令应在 P0 修复、发布输入已审阅并固定到 commit/tag 后执行。不要把当前旧 `dist` 当成成功证据。
### 1. 通用发布前门禁
```powershell
git status --short
git diff --check
git ls-files --stage -- '*.command' 'scripts/*.sh'
```
要求:无意外修改/未跟踪发布输入;macOS 操作文件均为 `100755`。研究报告或明确允许的生成物可以存在,但不得混入发布源清单。
### 2. Windows:在 Windows x64 原生主机执行
轻量预检:
```powershell
.\Build_DoctorWorkstation.bat -ValidateOnly
.\.venv\Scripts\python.exe -m pytest -p no:cacheprovider `
tests\test_one_click_entrypoints.py tests\test_entrypoint.py
```
正式构建(该入口内部已经执行 locked dependency preparation、companion build、PyInstaller、冻结烟测、ZIP 和哈希):
```powershell
.\Build_DoctorWorkstation.bat
```
构建后强制文件门禁:
```powershell
$artifact = Resolve-Path 'dist\DoctorWorkstation'
$qt = Join-Path $artifact '_internal\PySide6'
$required = @(
(Join-Path $artifact 'DoctorWorkstation.exe'),
(Join-Path $qt 'QtMultimedia.pyd'),
(Join-Path $qt 'QtMultimediaWidgets.pyd'),
(Join-Path $qt 'Qt6Multimedia.dll'),
(Join-Path $qt 'Qt6MultimediaWidgets.dll'),
(Join-Path $qt 'plugins\multimedia\ffmpegmediaplugin.dll'),
(Join-Path $qt 'plugins\multimedia\windowsmediaplugin.dll'),
(Join-Path $artifact '_internal\video_companion_dist\index.html')
)
$missing = $required | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }
if ($missing) { throw "Missing frozen release files:`n$($missing -join "`n")" }
if (-not (Get-ChildItem -LiteralPath $artifact -Recurse -File `
-Filter 'QtWebEngineProcess.exe' | Select-Object -First 1)) {
throw 'QtWebEngineProcess.exe missing'
}
if (-not (Get-ChildItem -LiteralPath $artifact -Recurse -File `
-Filter 'qtwebengine_resources*.pak' | Select-Object -First 1)) {
throw 'QtWebEngine resources missing'
}
```
版本化 ZIP 与哈希复核:
```powershell
$version = ([regex]::Match(
[IO.File]::ReadAllText((Resolve-Path 'pyproject.toml')),
'(?m)^version\s*=\s*"([^"]+)"'
)).Groups[1].Value
$zip = Resolve-Path "dist\DoctorWorkstation-Windows-x64-$version.zip"
$actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash
$expected = ((Get-Content -Raw -Encoding UTF8 'dist\SHA256SUMS.txt').Trim() -split '\s+')[0]
if ($actual -ne $expected) { throw 'Windows release checksum mismatch' }
Write-Host "Verified: $zip`nSHA-256: $actual"
```
最后在一台无开发环境的 Windows 10/11 x64 机器解压 ZIP,双击 `Start_DoctorWorkstation.bat`,完成登录/demo、内嵌 companion、获准 MP4/HLS 回放、摄像头/麦克风与一次真实测试房间验收。若为生产外发,必须先完成 Authenticode,再重新生成 ZIP 和 SHA-256。
### 3. macOS:只能在原生 macOS 主机执行
模式、语法和入口合同:
```bash
git ls-files --stage -- '*.command' 'scripts/*.sh'
test "$(git ls-files --stage -- '*.command' 'scripts/*.sh' | \
awk '$1 != "100755" { bad++ } END { print bad + 0 }')" = 0
bash -n scripts/macos_helpers.sh scripts/run_macos.sh \
scripts/package_macos.sh scripts/build_macos.sh \
scripts/check_macos_entrypoints.sh \
run_macos.command package_macos.command \
一键运行.command 一键打包.command
CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh
```
内部 QA 构建可直接运行;生产构建应先提供真实 Developer ID
```bash
export MACOS_CODESIGN_IDENTITY='Developer ID Application: <组织名称> (<TEAMID>)'
./一键打包.command
```
构建后检查:
```bash
app='dist/DoctorWorkstation.app'
test -x "$app/Contents/MacOS/DoctorWorkstation"
test -n "$(find "$app" -type f \( -name 'QtMultimedia.so' -o -name 'QtMultimedia.*.so' \) -print -quit)"
test -n "$(find "$app" -type f \( -name 'QtMultimediaWidgets.so' -o -name 'QtMultimediaWidgets.*.so' \) -print -quit)"
test -n "$(find "$app" -type f -path '*/plugins/multimedia/*' -print -quit)"
test -n "$(find "$app" -type f -name 'QtWebEngineProcess' -print -quit)"
test -n "$(find "$app" -type f -name 'qtwebengine_resources*.pak' -print -quit)"
test -n "$(find "$app" -type f -path '*video_companion_dist/index.html' -print -quit)"
codesign --verify --deep --strict --verbose=2 "$app"
codesign -d --entitlements :- "$app/Contents/MacOS/DoctorWorkstation"
file "$app/Contents/MacOS/DoctorWorkstation"
```
版本化 ZIP 与哈希复核:
```bash
version="$(awk -F '"' '/^version = "/ { print $2; exit }' pyproject.toml)"
case "$(uname -m)" in
arm64) arch='arm64' ;;
x86_64) arch='x64' ;;
*) echo 'Unsupported macOS architecture' >&2; exit 1 ;;
esac
zip="DoctorWorkstation-macOS-$arch-$version.zip"
(cd dist && shasum -a 256 -c "$zip.sha256")
```
生产外发还需用 `xcrun notarytool` 提交、等待成功并 `xcrun stapler staple`。staple 后要用 `ditto --keepParent` 重新生成最终 ZIP 并重算 `.sha256`,再以 `spctl --assess --type execute --verbose=4` 和一台干净 macOS 13+ 机器验收。最终必须实测摄像头、麦克风、扬声器切换、QtMultimedia 回放、QtWebEngine companion 和 TRTC 测试房间。
## 发布准入判定
满足以下条件才可把状态改为“可发布”:
1. macOS 操作文件的 Git 模式全部为 `100755`
2. intended release source 已纳入确定 commit/tag,构建工作树无意外输入。
3. Windows 从该 commit 完整重建;新 ZIP 含 QtMultimedia Python/Widgets/插件、QtWebEngine 和当前 companion,哈希通过。
4. 原生 macOS 从同一 commit 完整重建;`.app` 含对应媒体/QtWebEngine 组件,架构、签名、权限、ZIP 哈希通过。
5. 两个平台冻结 `--smoke-test` 通过,且独立媒体/companion/TRTC 验收通过。
6. 若为生产外发,Windows Authenticode 与 macOS Developer ID/notarization/stapling 均在最终归档前完成。
@@ -0,0 +1,230 @@
# 发布打包最终只读复审
日期:2026-08-11Asia/Shanghai
目标:判断是否可以**从当前工作树**重建 DoctorWorkstation 发布包。
边界:未执行完整 PyInstaller,未修改应用/打包源码;仅新增本复审报告。
## 最终结论
**结论:当前工作树已具备启动原生完整重建的条件,未发现构建前 packaging 代码级 P0。**
- Windows 当前工作树可进入 `Build_DoctorWorkstation.bat` 完整重建;根发布入口的 `-ValidateOnly` 已通过。
- macOS 的 9 个操作入口在 Git index 中现已全部为 `100755`,入口合同会同时检查文件系统 `-x` 和 Git index mode,不再被 Windows/NTFS 的可执行位模拟掩盖。
- spec 已显式收集 `PySide6.QtMultimedia`/`QtMultimediaWidgets`,并安装独立 PyInstaller runtime hook。
- Windows/macOS build 均先检查冻结媒体文件,再依次执行 `--media-smoke-test``--smoke-test`package 仅在 build 成功后归档和计算哈希。
- README、packaging README 与 `.env.example` 已说明 build/package 区别、双媒体门禁、macOS mode 以及 `.env` 不进入发布包的部署策略。
本结论只表示“可以开始原生重建”,不表示两个平台产物已经生成或可以立即对外发布。旧 `dist` 仍是修复前成品,必须由新构建替换;macOS `.app` 仍只能在原生 macOS 上产出和验收。
## P0:无
按本轮定义——是否能从**当前本地工作树**发起完整重建——未发现 P0。
特别澄清:`src/doctor_workstation/ui/diagnosis_media.py``packaging/runtime_media_smoke.py``tests/test_packaging_media_gate.py` 等文件目前未跟踪,但它们真实存在于当前工作树,spec/测试/发布入口都能读取,因此**不是本地工作树构建阻断**。它们的未提交状态属于跨机和后续复现风险,列为 P1,而不是本轮 P0。
## P1:完整发布前仍须完成
### P1-1:必须运行新的原生完整构建,旧 `dist` 不得作为验收证据
当前 `dist/DoctorWorkstation` 和版本 ZIP 早于 QtMultimedia 修复,仍缺少新媒体模块/插件。根“一键运行”会优先启动这个旧 EXE,所以完整重建前不要用它判断当前代码。
Windows 必须重新运行:
```powershell
.\Build_DoctorWorkstation.bat
```
只有控制台依次报告以下门禁通过,且随后生成新 ZIP/哈希,才算 Windows 重建成功:
1. QtWebEngine helper/resources 与 TRTC companion 文件检查;
2. `Frozen Qt multimedia file gate passed`
3. `Frozen Qt multimedia smoke gate passed (--media-smoke-test, ...)`
4. `Frozen application entry smoke gate passed (--smoke-test, ...)`
5. `Windows package complete`
本复审按约束未运行完整 PyInstaller,因此不能提前宣称这些冻结态门禁已经在新产物上通过。
### P1-2:macOS 必须在原生目标架构主机完成构建
Windows 只能验证 shell 语法和合同,不能交叉产生 `.app`、macOS framework、媒体插件或有效签名。必须在 arm64 或 x86_64 macOS 构建机执行:
```bash
CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh
./一键打包.command
```
成功证据必须包括媒体文件 gate、两个冻结 smoke gate、`codesign --verify`、实际架构、版本 ZIP 与 `.sha256`。这是一项平台执行门禁,不是当前脚本缺陷。
### P1-3:当前工作树尚未形成可跨机复现的发布 commit/tag
当前本机直接构建会包含未跟踪媒体源码和 runtime hook,因此可以构建;但新的 clone、CI runner 或另一台 macOS 机器不会从当前 HEAD 获得这些文件。当前还有多处修改、staged mode 变化和大量测试产物。
因此:
- 内部本机技术构建可以现在开始;
- 跨机 macOS 构建或正式发布前,发布负责人仍应审阅 intended source,提交必要文件及 9 个 mode 变化,并从确定的 commit/tag 构建;
- 不应使用 `git add -A``.pytest-tmp-*``artifacts/` 等临时产物一并纳入。
这是正式发布的可复现性 P1,但按根任务要求不作为“当前工作树本地重建”P0。
### P1-4:生产签名、公证链仍是发布策略门禁
- Windows package 生成 ZIP 与 SHA-256,但没有集成 Authenticode。
- macOS 接受可选 `MACOS_CODESIGN_IDENTITY` 并运行 `codesign --verify`,但不自动 notarize/staplead-hoc 签名也可能通过完整性验证。
内部 QA 包可以明确标记为内部构建。生产/院外分发则必须在归档前完成 Windows 签名;macOS 必须完成 Developer ID、嵌套 QtWebEngine helper 检查、notarization 和 stapling。staple 会改变 `.app`,之后要重新生成最终 ZIP 与 SHA-256。
### P1-5:媒体 gate 不替代真实播放和 TRTC 准入
`--media-smoke-test` 已真实创建 Qt 媒体对象并检查 decoder backend,但没有加载具体 MP4/HLS,也不请求摄像头/麦克风或加入 TRTC 房间。正式验收仍需在最终冻结包上覆盖:
- 获准分发的 MP4,产品要求时再覆盖 HLS;
- 画面渲染与音频输出;
- QtWebEngine companion/WebChannel
- 摄像头、麦克风、扬声器切换;
- TRTC 测试房间、弱网、设备插拔和休眠恢复。
## P2:建议改进
1. 仓库仍无 `.gitattributes`,本机 `core.autocrlf=true`;当前 shell 文件是 LF 且 `bash -n` 通过,但建议固定 `*.sh`/`*.command` 为 LF。
2. Windows 使用 `uv sync --frozen`macOS 使用 `uv sync --locked`;建议补 `uv lock --check` 或统一 lock 新鲜度策略。
3. Windows `version_info.txt``0.1.0.0` 与 pyproject `0.1.0` 当前一致,但仍是人工同步,建议增加版本一致性门禁。
4. `Build_DoctorWorkstation.bat -ValidateOnly` 仍输出 `Package ready in: ...`,容易让人误以为已经生成新包;退出码和行为本身正确。
5. Windows/macOS 的哈希文件命名分别为 `SHA256SUMS.txt``<zip>.sha256`,可统一以简化发布自动化。
6. spec 尚未设置原生 `.ico`/`.icns`;不影响构建或媒体能力,但正式品牌发布可补齐。
7. `research/release_packaging_audit.md``release_packaging_gate_fixed.md` 是阶段性历史记录,其中关于旧 mode/旧产物的描述不能替代本最终复审。
8. AGENTS.md 引用的 `.trellis/workflow.md``.trellis/spec/` 在当前工作区仍不存在;属于治理缺口,不是本地构建阻断。
## 独立复审结果
### 1. PyInstaller specPASS
`packaging/doctor_workstation.spec` 现在:
- 显式 hidden import
- `PySide6.QtMultimedia`
- `PySide6.QtMultimediaWidgets`
- 继续显式收集 QtWebEngine 模块;
-`video_companion/dist` 放入 `video_companion_dist`
- 要求 `packaging/runtime_media_smoke.py` 必须存在;
-`runtime_hooks=[str(MEDIA_SMOKE_HOOK)]` 安装媒体 gate。
当前 PyInstaller 6.22 的官方 `hook-PySide6.QtMultimedia.py` 会追加 Widgets 模块,Qt6 module mapping 会收集 `plugins/multimedia`,与本 spec 的显式 imports 一致。
### 2. `--media-smoke-test`PASS
`packaging/runtime_media_smoke.py` 作为 runtime hook,在普通启动时无副作用;仅当 argv 含 `--media-smoke-test` 时才在应用入口前执行:
- 导入 `QMediaPlayer``QAudioOutput``QMediaFormat``QVideoWidget`
- 创建并连接 player/audio/video 对象;
- `player.isAvailable()` 必须为真;
- Decode 模式至少暴露一种支持格式;
- 显示 16×16 offscreen video widget,并完成一次 Qt event loop
- 任一错误固定返回 70,成功返回 0。
独立测试验证了三种状态:普通 argv 惰性退出 0;隔离掉 site-packages 后媒体 argv 返回 70;当前 PySide6 环境 offscreen 实例化返回 0。
### 3. Windows build/packagePASS(静态与轻量合同)
`scripts/build_windows.ps1` 在 PyInstaller 后要求以下文件:
- `QtMultimedia.pyd`
- `QtMultimediaWidgets.pyd`
- `Qt6Multimedia.dll`
- `Qt6MultimediaWidgets.dll`
- `plugins/multimedia/ffmpegmediaplugin.dll`
- `plugins/multimedia/windowsmediaplugin.dll`
随后先运行 `--media-smoke-test`,再运行原 `--smoke-test`;两者均沿用隔离配置目录、loopback-only proxy、offscreen、30 秒超时、退出码和未捕获异常日志检查。
`scripts/package_windows.ps1` 把 runtime hook 加入 required files,调用 build 之后才复制 release launcher、压缩 ZIP 和生成 SHA-256。`Build_DoctorWorkstation.bat -ValidateOnly` 本轮通过。
### 4. macOS build/packagePASS(静态与轻量合同)
`scripts/build_macos.sh` 在成功前要求:
- `PySide6.QtMultimedia``QtMultimediaWidgets` Python `.so`
- 对应 framework,或适配 dylib 布局的后备检查;
- `plugins/multimedia` 目录;
- 至少一个 `*mediaplugin*.dylib/.so` backend
- QtWebEngine helper/resources、companion 与主可执行文件。
之后执行 `codesign --verify`,再顺序运行媒体 smoke 和应用 smoke。`scripts/package_macos.sh` 在准备依赖前先运行入口合同,并只在 build 全部成功后用 `ditto` 归档和计算哈希。
这些路径模式与当前 PySide6/PyInstaller 的 macOS framework/plugin 布局相容;最终仍需原生构建确认。
### 5. macOS Git executable modePASS
以下 9 个文件的 index mode 本轮均为 `100755`
1. `package_macos.command`
2. `run_macos.command`
3. `一键打包.command`
4. `一键运行.command`
5. `scripts/build_macos.sh`
6. `scripts/check_macos_entrypoints.sh`
7. `scripts/macos_helpers.sh`
8. `scripts/package_macos.sh`
9. `scripts/run_macos.sh`
`scripts/check_macos_entrypoints.sh` 已将自身纳入 operational files,逐个执行 `bash -n`、检查实际 `-x`,在 Git 工作树中还读取 index mode 并要求 `100755`。本轮 Git Bash 合同执行通过。
### 6. README 与 `.env`PASS
- 根 README 正确区分 `build_*`(只生成/验证 onedir 或 `.app`)和根/package 入口(生成版本 ZIP 与哈希)。
- Windows/macOS 一键打包描述已包含 QtWebEngine/QtMultimedia 文件门禁与两个冻结 smoke。
- README 明确 macOS 操作文件必须以 mode `100755` 跟踪。
- `.env.example` 与 README 明确该文件不进入 ZIP/`.app`;生产配置应由受控 launcher/MDM/进程环境注入。
- 文档明确禁止把密码、token、UserSig、TRTC SecretKey 等凭据放进 `.env` 或发布包。
### 7. 输出合同:PASS(待新构建兑现)
输出命名未被修复破坏:
- Windows onedir`dist/DoctorWorkstation/`
- Windows ZIP`dist/DoctorWorkstation-Windows-x64-<version>.zip`
- Windows checksum`dist/SHA256SUMS.txt`
- macOS bundle`dist/DoctorWorkstation.app`
- macOS ZIP`dist/DoctorWorkstation-macOS-{arm64|x64}-<version>.zip`
- macOS checksum`<zip>.sha256`
两个 package 流程都位于 build/gate 之后,失败不会进入新的归档步骤。
## 本轮实际执行的轻量检查
| 检查 | 结果 |
|---|---:|
| Windows 三份 PowerShell parser | PASS |
| `Build_DoctorWorkstation.bat -ValidateOnly` | PASS |
| 全部 macOS shell/command `bash -n` | PASS |
| `scripts/check_macos_entrypoints.sh`(含 index mode | PASS |
| packaging media gate + one-click + entrypoint pytest | **10 passed** |
| runtime hook/packaging test Ruff | PASS |
| relevant tracked diff whitespace check | PASS |
pytest 首次执行曾因沙箱无权枚举系统 `pytest-of-pc` 临时目录而产生 1 个 setup error;改用工作区内独立临时目录重跑后 10 项全部通过,并已安全清理该目录。该环境错误不是代码失败。
## 主代理下一步
### 当前 Windows 工作树
```powershell
.\Build_DoctorWorkstation.bat -ValidateOnly
.\Build_DoctorWorkstation.bat
```
完成后不要只看退出码;确认新 onedir/ZIP 时间戳、媒体文件、两个 smoke gate 日志及 SHA-256,并在无开发环境机器上做真实媒体/TRTC 验收。
### 原生 macOS
先确保目标机器拿到与当前工作树等价的全部 intended files(正式发布推荐先形成 commit/tag),然后:
```bash
git ls-files --stage -- '*.command' 'scripts/*.sh'
CI=1 DOCTOR_NONINTERACTIVE=1 bash scripts/check_macos_entrypoints.sh
export MACOS_CODESIGN_IDENTITY='Developer ID Application: <组织名称> (<TEAMID>)'
./一键打包.command
```
随后验证 `.app` 架构、媒体 framework/plugin、两个 smoke gate、签名、公证/staple、最终 ZIP 哈希和真实设备/TRTC 流程。
@@ -0,0 +1,76 @@
# 发布打包门禁修复报告
日期:2026-08-11Asia/Shanghai
范围:仅 PyInstaller spec、打包/构建/入口检查脚本、发布说明、环境示例与打包专属测试;未修改 UI、core 或 repository,未执行完整 PyInstaller。
## 结论
`release_packaging_audit.md` 中除 Git 工作树/提交状态和 macOS Git executable mode 本身之外的 QtMultimedia 发布门禁已经落到代码与合同测试中:
- spec 显式列出 `PySide6.QtMultimedia``PySide6.QtMultimediaWidgets`,从而强制触发 PyInstaller 官方 PySide6 hooks,收集对应 Python 扩展、`Qt6Multimedia`/`Qt6MultimediaWidgets` DLL、dylib/framework、`plugins/multimedia` 以及平台媒体后端。
- 新增 PyInstaller runtime hook:冻结程序收到 `--media-smoke-test` 时,在进入正常应用入口前真实导入两个媒体模块,实例化 `QMediaPlayer``QAudioOutput``QVideoWidget`,连接音视频输出,确认媒体 backend 可用且至少暴露一种解码格式,并在 `offscreen` Qt 事件循环中退出。任一导入、链接、backend 或事件循环失败均返回非零(固定失败码 70)。
- Windows 构建在成功前要求 `QtMultimedia.pyd``QtMultimediaWidgets.pyd``Qt6Multimedia.dll``Qt6MultimediaWidgets.dll``plugins/multimedia/ffmpegmediaplugin.dll``windowsmediaplugin.dll`,随后依次运行冻结媒体门禁与原应用 `--smoke-test`
- macOS 构建在成功前要求两个 Python 扩展、两个对应 framework/dylib、`plugins/multimedia` 与至少一个 `*mediaplugin*` backend,随后依次运行相同两项冻结门禁。
- 两个平台的一键 package 入口仍先调用 build;只有 build 的文件门禁和两项冻结 smoke gate 全部通过后才归档 ZIP/生成 SHA-256。
- `.env.example` 和 README 已明确:环境示例不会进入发布包,生产配置应由受控启动器/设备管理/进程环境注入,发布包不得包含密码、token、UserSig、TRTC SecretKey 等凭据。
## 主要文件
- `packaging/doctor_workstation.spec`
- `packaging/runtime_media_smoke.py`
- `scripts/build_windows.ps1`
- `scripts/package_windows.ps1`
- `scripts/build_macos.sh`
- `scripts/package_macos.sh`
- `scripts/check_macos_entrypoints.sh`
- `packaging/README.md`
- `README.md`
- `.env.example`
- `tests/test_packaging_media_gate.py`
## macOS executable mode:必须由根代理完成
本任务遵守约束,没有执行 `git add``git update-index``git commit`。当前 Git index 仍把下列 9 个入口记为 `100644`;根代理必须把每个文件的 index mode 精确改为 `100755`,并把 mode 变化纳入最终发布提交:
1. `run_macos.command``100755`
2. `package_macos.command``100755`
3. `一键运行.command``100755`
4. `一键打包.command``100755`
5. `scripts/build_macos.sh``100755`
6. `scripts/package_macos.sh``100755`
7. `scripts/run_macos.sh``100755`
8. `scripts/macos_helpers.sh``100755`
9. `scripts/check_macos_entrypoints.sh``100755`
根代理可从项目根目录执行以下精确操作(本代理未执行):
```powershell
git update-index --chmod=+x -- run_macos.command package_macos.command `
"一键运行.command" "一键打包.command" `
scripts/build_macos.sh scripts/package_macos.sh scripts/run_macos.sh `
scripts/macos_helpers.sh scripts/check_macos_entrypoints.sh
git ls-files --stage -- '*.command' 'scripts/*.sh'
```
第二条命令对上述 9 个文件必须全部显示 `100755``scripts/check_macos_entrypoints.sh` 现在同时检查实际 `-x` 与 Git index mode,因此 Windows/Git Bash 将文件视为可执行也不能再掩盖 `100644`
## 已执行的轻量验证
- 源环境媒体门禁(`QT_QPA_PLATFORM=offscreen`):PASS;实际播放器、音频输出、VideoWidget 和 decoder backend 均成功创建。
- 缺少 site-packages 的隔离进程:PASS`--media-smoke-test` 返回 70,证明缺组件不会误报成功。
- PowerShell parser`build_windows.ps1``package_windows.ps1``run_windows.ps1` PASS。
- Git Bash `bash -n`:所有 macOS build/package/run/check/helper 与四个 `.command` PASS。
- 打包合同测试:`tests/test_packaging_media_gate.py``tests/test_one_click_entrypoints.py``tests/test_entrypoint.py`10 passed。
- Ruff:新增 runtime hook 与打包测试 PASS。
- `git diff --check`PASS。
## 仍需原生构建机完成的发布验收
按任务约束,本次没有运行完整 PyInstaller。最终发布仍必须:
- 在 Windows x64 原生构建机运行一键打包,确认新 onedir/ZIP 的文件门禁和冻结 `--media-smoke-test` 均通过,替换旧 `dist` 证据;
- 在目标架构 macOS 原生构建机运行一键打包,确认 `.app` framework/plugin 布局、codesign、摄像头/麦克风权限、notarization/stapling
- 使用批准分发的 MP4(产品需要时再加 HLS)完成实际解码、画面渲染和音频输出验收;媒体 smoke gate 是 backend 准入,不替代真实播放;
- 由发布负责人审阅工作树,只从确定的 commit/tag 构建。Git 输入/提交状态属于本任务明确排除项。
补充:仓库指令引用的 `.trellis/workflow.md``.trellis/spec/` 在当前工作区不存在,因此本次无法执行 Trellis 规范审查;这不影响上述脚本合同结果。
+203
View File
@@ -0,0 +1,203 @@
# 诊疗 / 病例 / 订单 / AI 报告子窗口蓝白参考审计
审计日期:2026-08-13
审计性质:只读;未修改业务源码、测试、脚本或既有 PNG。
审计边界:诊疗详情/编辑抽屉、独立病例只读页、诊断上下文中的业务订单详情、诊断 AI 报告。未审计或改动 `theme.py``widgets.py`、处方编辑/患者页/挂号页;AI 部分只看诊断报告模式,不扩展到处方业务。
## 1. 结论先行
1. **用户所指“蓝白参考”最接近的现有实图是 `artifacts/pixel_exact_v3/consultations.png`**1710×9202026-08-13 19:56)。它是当前最新、最完整的蓝白医生工作站视觉:浅蓝壳层、近白内容底、白色卡片、靛蓝主操作、冷灰蓝文字与边框。子窗口应从它取色,不应从旧 Diagnosis 深色图取色。
2. **子窗口结构不能统一成同一种 modal。** 现有正确结构分别是:诊疗编辑/viewOnly 为右侧 60% Drawer;独立病例为无 Tabs 的纵向滚动详情页;订单详情为右侧 80% readonly DrawerAI 报告为 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×7202026-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 1112 px;卡片标题 1415 pxDrawer 标题 1819 pxAI 报告标题 20 px。
- 4 px 间距基线;常用 8/12/16/20/24 px。不要产生 5、13、17、21 等无依据的主布局间距。
- 控件/普通按钮高 34 px;诊疗 Footer 主按钮高 40 px;紧凑 close 为 32×32;诊疗 Tab 高 42 pxAI Tab 高 36 px。
- 内控件/按钮圆角 7–8 px;字段卡/分区 910 pxHero 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 px1440×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 4close 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 的视觉尺度保持 1212.5 pxlabel `#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 px1440 owner 为 1152 px;左侧 20% 为 scrim。
- Header/Body/Footer 三段独立;Body 单独纵向滚动,Footer 始终可见。
- Header 当前内边距 `20px 15px 18px 15px`gap 12;标题栈 gap 4;标题 19 pxmeta 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 18selected 字 `#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%,覆盖 ownerreadonly 属性与各信息区存在。
- 空字段必须显示明确空态,不能伪造 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 间距基线一致;语义色不被“全蓝化”。
+275
View File
@@ -0,0 +1,275 @@
# 腾讯云 TRTC 接入 Python / PySide6 跨平台桌面视频面诊研究
> 调研日期:2026-08-10
> 范围:Windows / macOS 桌面端;Python / PySide6 业务程序;1 对 1 视频面诊,可扩展屏幕共享。
> 来源原则:仅使用腾讯云、Tencent RTC 官方文档与官方 SDK API 文档。文中标为“工程判断/建议”的内容是根据官方能力边界作出的架构推断,并非腾讯云对 Python 或 PySide6 的兼容性承诺。
## 结论摘要
1. **腾讯云没有官方 Python 或 PySide6 TRTC 客户端 SDK。** 用户指定的[产品概述(文档 16788](https://cloud.tencent.com/document/product/647/16788)列出了 Windows、macOS、Web、Electron 等平台,没有 Python/PySide6。官方还提供了 Windows/macOS 的 **C++ Qt** 集成示例,但它不是 Python 绑定,也没有证明 PySide6 / Qt 6 ABI 兼容。
2. **推荐生产架构:PySide6 保留为业务主程序,音视频做成独立 Electron RTC companion(伴随进程/独立面诊窗口),由后端签发进房票据,PySide6 与 companion 通过本机受认证 IPC 交互。** 1 对 1 场景优先使用底层 `trtc-electron-sdk`;若要快速获得完整会议 UI、成员管理和会控,可选官方 TUIRoomKit Electron。官方明确将 TUIRoomKit Electron 用于“医疗问诊”等场景。
3. **最快的业务 MVP 是从 PySide6 打开系统浏览器中的 HTTPS Web SDK 面诊页。** 这是官方浏览器支持路径。把 Web SDK 嵌入 `QWebEngineView` 技术上有机会可行,但 QtWebEngine 不在腾讯的具名支持矩阵中;官方只说理论上支持 Chromium 56+,并要求对 WebView 等未列环境运行能力检测。因此,`QWebEngineView` 只能作为验证分支,不能在未完成双平台全量实测前作为生产承诺。
4. **若“必须在同一 PySide6 窗口内原生渲染”是硬要求,才选择 Native C++ 桥接。** 需要自行为 Windows/macOS 编译 C ABI/CPython 扩展,处理原生渲染句柄、回调线程、Qt 5/Qt 6 兼容、签名与打包;这是可落地但成本和维护风险最高的方案。
5. **生产环境的 SDKSecretKey 只能放在服务端。** 客户端仅获得短期 `UserSig`;需要面诊房间级与媒体权限控制时,开启 Advanced Permission Control 并由服务端签发 `PrivateMapKey`。房间 ID 不能视作访问控制。
## 方案对比
| 方案 | 官方支持边界 | 与 Python/PySide6 的关系 | 摄像头/麦克风/屏幕分享 | 主要风险 | 判断 |
|---|---|---|---|---|---|
| Web SDK + 系统浏览器 | 官方支持 Windows/macOS 主流浏览器;生产要求 HTTPS | PySide6 打开带一次性业务会话的 HTTPS 页面;无 Python SDK 绑定 | 官方均支持;屏幕分享受浏览器/OS 授权和用户选择器约束 | 独立浏览器窗口、业务 UI 一体化较弱 | **最快、风险低的 MVP/兜底** |
| Web SDK + `QWebEngineView` | 官方称理论支持 Chromium 56+,未列环境(含 WebView)需 `TRTC.isSupported()`/能力检测;未明确认证 QtWebEngine | JS 运行在嵌入页,通过 WebChannel/IPC 与 Python 通信(工程自建) | 需验证嵌入引擎的媒体权限、设备切换、屏幕选择与 macOS Screen Recording | 腾讯支持边界、QtWebEngine 版本与权限行为不确定 | **仅作 POC;通过准入测试后才可采用** |
| Electron SDK / TUIRoomKit Electron | 官方支持 Windows/macOSSDK 是 Node 原生模块;官方提供设备权限、打包、屏幕分享和医疗问诊组件指引 | Python 不能直接 `import`;作为独立伴随进程最清晰 | 完整支持,DOM 承载本地/远端画面;可用辅路同时保留摄像头与屏幕 | 包体较大、需维护本机 IPC、两平台分别构建签名 | **推荐生产方案** |
| Native C++ SDK + 自研桥 | 官方全平台 C++ API、Windows/macOS SDK、C++ Qt 示例 | 自行写 C ABI/pybind11/CPython 桥;腾讯不提供 Python/PySide6 绑定 | 能力最完整,包含设备管理、测试、屏幕源枚举、原生渲染与私有加密 API | C++ ABI、Qt 版本、线程、崩溃隔离、双平台构建维护 | **单窗口原生体验的二期方案** |
## 各方案适配判断
### 1. Web SDK
官方事实:
- TRTC Web SDK 是 JavaScript SDK,视频渲染目标是 HTML 元素;通过 `TRTC.create()``enterRoom()``startLocalAudio()``startLocalVideo()``startRemoteVideo()``exitRoom()``destroy()` 完成生命周期,见[Web & H5 快速接入](https://cloud.tencent.com/document/product/647/116544)。
- 官方平台页称理论上支持所有 Chromium 56+ 浏览器;对支持表之外的环境,建议用 `TRTC.isSupported()` 或[能力检测页](https://web.sdk.qcloud.com/trtc/webrtc/demo/detect/index.html)检测。快速 Demo 文档还特别提到 WebView 等环境应先检测,见[Web Demo 准备工作](https://cloud.tencent.com/document/product/647/32398)和[Web API 概览/平台要求](https://intl.cloud.tencent.com/zh/document/product/647/41664)。
- 生产环境推流和屏幕分享要求 HTTPS;HTTP 生产页只能播放,不能上麦/屏幕分享。本地 `localhost` 可用于开发。
- 设备名称和 `deviceId` 在获得摄像头/麦克风授权前可能为空;应先完成授权再展示设备详情。
- 桌面 Web 支持 `startScreenShare()`;用户可能从浏览器系统 UI 停止分享,业务必须监听分享停止事件并恢复 UI 状态,见[Web 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/35163)。
对 PySide6 的工程判断:
- 系统 Chrome/Edge/Safari 是具名支持路径,最适合快速验证服务端、UserSig、房间和媒体链路。
- `QWebEngineView` 虽基于 Chromium,但不是腾讯文档具名认证平台。只有在目标 PySide6 随附的 QtWebEngine 上同时通过能力检测和真实设备测试,才能认为本项目可用。
- 若验证嵌入方案,应把下列项目设为硬门槛:摄像头/麦克风首次授权及拒绝后恢复、设备插拔和切换、远端音频自动播放、窗口/整屏分享、从系统分享条停止、macOS Screen Recording 权限、窗口最小化/休眠恢复、打包后仍可用。
- `file://` 虽在官方表中可用,但生产仍建议加载受控 HTTPS 页面,以便版本发布、CSP、证书与安全响应集中管理。
### 2. Electron SDK / TUIRoomKit Electron
官方事实:
- 当前[Electron 快速接入](https://cloud.tencent.com/document/product/647/116549)要求 Electron 工程安装 `trtc-electron-sdk`,实际加载 `trtc_electron_sdk.node` 原生模块;支持 Windows 和 macOS,并给出了两平台不同的原生模块资源路径、摄像头/麦克风/屏幕权限检查及打包配置。
- `startLocalPreview()` 的渲染目标是 `HTMLElement`,不是 PySide `QWidget``startLocalAudio()` 可选 Speech 模式,官方说明其噪声抑制和弱网抗性更强,适合面诊语音。
- 1 对 1 视频面诊应使用 `TRTCAppSceneVideoCall`,而不是直播场景。官方将 VideoCall 定位为 1 对 1 或 300 人以内实时通话。
- [TUIRoomKit Electron 接入](https://cloud.tencent.com/document/product/647/129879)明确列出“医疗问诊”场景,并内置房间管理、音视频控制、屏幕共享、成员管理和布局。
- Electron 屏幕分享支持主路和辅路。辅路可在摄像头继续上行的同时分享屏幕;一个 TRTC 房间目前只能有一路屏幕分享,见[Electron 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/47619)。
- 官方 Electron API 还提供设备列表、设备切换、摄像头/麦克风/扬声器测试和网络测速;网络测速应在进房前进行,见[Electron SDK API](https://web.sdk.qcloud.com/trtc/electron/doc/en-us/trtc_electron_sdk/TRTCCloud.html)。
对 PySide6 的工程判断:
- Node 原生模块不能当作 Python 模块直接加载。把它塞进 PySide6 进程会引入 Node/Electron 运行时、DOM 渲染和 ABI 问题,不值得。
- 独立 RTC companion 可将故障、升级和媒体权限与 Python 主进程隔离;PySide6 只负责预约、病历、支付等业务 UI,Electron 只持有当次短期票据并负责通话。
- Windows 与 macOS 应各自在目标系统构建、签名并验证产物;不要把包含 `.node`/`.dll`/framework 的包当作纯 JS 跨平台包。
- IPC 采用本机命名管道/Unix domain socket 或 loopback WebSocket,并以每次启动随机 nonce 双向认证;不要把 `UserSig` 放在命令行参数、URL 查询串或长期日志中。此为工程安全建议,不是腾讯 SDK 的内置保证。
### 3. Native C++ SDK 与 Qt 桥接
官方事实:
- [全平台 C++ API 概览](https://cloud.tencent.com/document/product/647/32268)包含实例/回调、进退房、摄像头和远端渲染、音频、设备管理、屏幕分享、网络质量、连接恢复及私有加密等完整能力。
- 腾讯提供[Qt Windows/macOS 集成文档](https://intl.cloud.tencent.com/zh/document/product/647/39665)Windows 使用 C++ SDK 的 `liteav.lib`/DLLmacOS 引用 `TXLiteAVSDK_TRTC_Mac.framework` 的 C++ 接口。示例是 Qt Widgets/C++macOS 文档写的是 Qt 5.10+。
- macOS 需要在 `Info.plist` 声明摄像头和麦克风权限;官方 Electron 指引另外检查 `screen` 权限。
- C++ API 可枚举/切换设备并运行摄像头、麦克风和扬声器测试;屏幕分享可枚举窗口/屏幕、选择目标、设置辅路参数及包含/排除窗口。
对 PySide6 的工程判断:
- PySide6 使用 Qt 6,而官方 Qt 示例仍以 Qt 5 为基线,不能把“支持 C++ Qt”直接等同于“支持 PySide6”。
- 若采用,推荐制作**很薄的 C ABI 桥**,不要让 Python 直接调用 C++ ABI:桥内持有 `ITRTCCloud`、回调对象和 SDK 生命周期,只向 Python 暴露稳定的 opaque handle、进退房、设备、屏幕分享和事件队列。
- 视频尽量由 SDK 绑定原生子窗口/视图句柄渲染;不要默认把每帧 YUV 回调到 Python,这会显著增加跨语言复制、GIL 和掉帧风险。
- SDK 回调可能不在 Qt UI 线程,桥层必须投递到 PySide6 主线程;退出时顺序应固定为停止屏幕分享/本地采集/远端渲染、`exitRoom`、等待退房回调、移除回调、销毁实例。
- 该路线必须先验证 Qt 版本和原生窗口句柄,且建议在正式承诺前向腾讯云提交工单确认目标 SDK 版本的 Qt 6/PySide 嵌入边界。
## 推荐生产架构
```text
┌──────────────────────┐ HTTPS ┌────────────────────────┐
│ PySide6 业务主程序 │ ───────────────────▶ │ 业务后端 / RTC Ticket │
│ 预约、病历、面诊入口 │ │ 鉴权、预约授权、UserSig │
└──────────┬───────────┘ │ PrivateMapKey、结束房间 │
│ 本机受认证 IPC └────────────┬───────────┘
▼ │ TRTC REST / callback
┌──────────────────────┐ ┌─────────────▼──────────┐
│ Electron RTC companion│ ◀──── RTC 媒体 ────▶ │ 腾讯云 TRTC │
│ 设备、视频、屏幕分享 │ └────────────────────────┘
└──────────────────────┘
```
### 组件职责
**PySide6 主程序**
- 只传预约 ID/面诊动作,不生成或持久化 SDKSecretKey。
- 调用业务后端获取本次短期进房票据,启动/聚焦 RTC companion。
- 展示 companion 回传的 `preflight / joining / connected / reconnecting / ended / error` 状态。
- 业务“面诊已结束”不能只依赖窗口关闭事件,应由客户端结果、TRTC 服务端回调和后端结束动作共同收敛。
**业务后端 / RTC Ticket 服务**
- 验证当前登录用户确实是该预约的医生或患者,并验证允许进房的时间窗和预约状态。
- 服务端派生 `userId``roomId`,生成 `UserSig`;启用高级权限控制时同时生成 `PrivateMapKey`
- 返回最小票据:`sdkAppId``userId`、一种且仅一种 room ID、`userSig``expiresAt`、可选 `privateMapKey`、业务角色与 UI 权限。SDKSecretKey 永不返回。
- 提供“结束面诊”接口;需要强制结束时调用官方 `DismissRoom`/`DismissRoomByStrRoomId`,见[解散房间 API](https://cloud.tencent.com/document/api/647/50089)。
- 接收房间/媒体回调,校验签名并幂等处理。
**Electron RTC companion**
- 使用底层 `trtc-electron-sdk` 完成 1 对 1 VideoCall;若需求接近完整会议产品,则用 TUIRoomKit Electron。
- 不保存长期登录态或云密钥;窗口关闭、崩溃和正常退房均向 PySide6/后端报告。
- 所有摄像头、麦克风和屏幕分享都由用户明确操作开启,并在界面持续显示状态。
## UserSig 与房间权限
### UserSig
- [官方用户鉴权文档](https://cloud.tencent.com/document/product/647/17275)明确指出:客户端计算 UserSig 只适合 Demo。客户端代码,尤其 Web,容易被反编译;泄露 SDKSecretKey 会导致腾讯云资源被盗用。
- 正式环境流程必须是:客户端先向业务服务器请求;服务器按 `SDKAppID + UserID` 生成 UserSig;客户端仅把结果交给 SDK。官方提供 Python HMAC-SHA256 服务端示例,因此 Python 后端生成完全可行。
- 建议把 UserSig 有效期控制为“预约可加入窗口 + 最大面诊时长 + 合理重连缓冲”,并在每次重新进入房间前重新授权。腾讯官方云助手的[Web 进房说明](https://cloud.tencent.com/document/product/1715/104507)指出原始 TRTC 的 UserSig 在进房时校验、进房后到期不影响当前通话;若采用包含 IM 登录的 TUIRoomKit,还要监听 `onUserSigExpired` 并从后端续签。
- SecretKey 放在服务端密钥管理系统/受限环境变量中;禁止进入桌面包、前端 JS、崩溃转储、遥测和调试日志。
### PrivateMapKey(高级权限控制)
- UserSig 证明某 UserID 有权使用该 SDKAppID,**不等于有权进入某个面诊房间**。对视频面诊,建议评估开启[高级权限控制](https://intl.cloud.tencent.com/zh/document/product/647/35157)。
- `PrivateMapKey` 绑定 room ID 与权限位,可分别控制创建房间、进房、收发音频、收发主路视频、收发辅路(屏幕分享)。必须由服务端计算。
- 示例权限策略:
- 医生:创建/进入、收发音频、收发视频、发送/接收辅路。
- 患者:进入、收发音频、收发视频、接收辅路;若不允许患者分享,则不授予“发送辅路”。
- 若不能保证医生先进入,需要给患者也授予创建房间,或由业务规则强制医生先创建。
- 启用高级权限控制后,同一 SDKAppID 下所有用户都必须携带 PrivateMapKey;已有线上应用不能直接无迁移开启。建议新建独立 SDKAppID 做灰度验证。
## 房间与用户生命周期
官方[基本概念](https://cloud.tencent.com/document/product/647/46351)给出的关键规则:
- 不存在的房间在首个用户进入时自动创建。
- 通话模式下,所有用户主动退房后房间立即解散;所有人异常掉线时,服务端约 90 秒后清理并解散。异常等待时间仍计入用量。
- 数字 `roomId` 与字符串 `strRoomId` 是两套不同房间,不能混用;全端和服务端必须统一一种类型。
- 同一 UserID 同时进入同一房间会互踢/干扰。UserID 应由后端稳定映射;若允许同一账号多设备同时加入,应给每个设备/会话分配唯一 UserID。
- 原始 TRTC 的远端用户进出回调用于维护成员列表,不代表对方已有视频;显示远端画面必须监听 `onUserVideoAvailable`,屏幕分享监听 `onUserSubStreamAvailable`
建议客户端状态机:
```text
idle
→ device-preflight
→ ticket-issued
→ joining
→ joined(media-off)
→ media-on / screen-sharing
→ exiting
→ idle
joined ↔ reconnecting
joining/connected → kicked | room-dismissed | fatal-error → cleanup → idle
```
实施要点:
- 创建实例后先注册所有错误、进退房、远端媒体、设备变化和连接状态回调,再调用 `enterRoom`
-`onEnterRoom(result > 0)` 作为真正进房成功,不以 `enterRoom()` 函数返回或窗口已打开代替。
- 网络断开时 SDK 会自动重连;监听 `onConnectionLost``onTryToReconnect``onConnectionRecovery`。官方说明远端通常约 90 秒后才收到异常用户离开,因此业务后端不能把短时断网立即当成面诊结束,见[断线重连说明](https://intl.cloud.tencent.com/document/product/647/36057)。
- 正常结束必须成对调用 `exitRoom`;退出后停止本地媒体、停止远端渲染、移除监听并销毁实例。Web 端明确要求 `exitRoom()` 后不再使用时调用 `destroy()`
- 服务端[房间与媒体回调](https://intl.cloud.tencent.com/zh/document/product/647/39558)可能重试,且特殊网络/重进场景可能产生重复事件;回调处理必须幂等,不能只用“最后一条回调”做财务/医疗业务结论。
## 摄像头、麦克风与屏幕分享
### 通话前检查
- 列出并选择摄像头、麦克风和扬声器;运行摄像头预览、麦克风电平和扬声器测试。
- 网络测速只在进房前运行,避免影响通话质量。
- 默认音频质量使用 Speech/语音模式;先以 640×360 的保守视频档位验证弱网和 CPU,再按质量监控数据决定是否提高到 720p。
- 对权限拒绝、设备被占用、设备拔出、默认设备变化给出可恢复 UI,不应直接结束预约。
### 屏幕分享
- 默认采用**辅路**,让医生摄像头与屏幕并存;远端根据辅路可用事件订阅。
- 一个房间只允许一路屏幕分享,第二人发起前应在业务 UI 层仲裁。
- 分享前必须让用户确认目标窗口/屏幕;分享中持续显示醒目标志;无论从应用按钮还是浏览器/系统指示条停止,都要收到事件并清理本地状态。
- 医疗场景应默认不共享整个桌面,优先窗口分享。Native SDK 可用窗口排除/包含 API,避免将病历主窗口、通知或其他患者信息意外共享。
- macOS 打包必须验证 Camera、Microphone 和 Screen Recording 权限的首次授权、拒绝、系统设置中撤销及升级安装后的行为。
## 生产安全与合规边界
1. **应用隔离**:开发、测试、生产使用不同 SDKAppID;正式环境不要复用 Demo 密钥或固定房间号。
2. **最小化标识**`roomId`/`userId` 使用无语义内部 ID,不包含姓名、手机号、身份证号、诊断或预约描述;映射只保存在业务后端。
3. **后端授权**:每次签票都校验预约参与者、角色、状态和时间窗;不能只靠“知道 roomId”。高安全场景使用 PrivateMapKey。
4. **回调真实性**:生产回调使用 HTTPS;按官方算法对原始请求体做 HMAC-SHA256 校验,并校验 SDKAppID;存储事件 ID/组合键以幂等处理重试。
5. **传输与额外加密**:腾讯[信息安全说明](https://cloud.tencent.com/document/product/647/86362)说明其默认传输有私有传输协议/TLS/WSS 保护。若组织政策要求额外媒体私有加密,Native C++ API 提供 `enablePayloadPrivateEncryption`,见[媒体流私有加密](https://cloud.tencent.com/document/product/647/106173);该能力需要相应套餐,并与云端录制、旁路转推等能力存在冲突,应在架构阶段选定。Electron 官方另有 C++ 动态库形式的[自定义媒体加解密插件](https://web.sdk.qcloud.com/trtc/electron/doc/zh-cn/trtc_electron_sdk/tutorial-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E9%9F%B3%E8%A7%86%E9%A2%91%E7%9A%84%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8A%A0%E8%A7%A3%E5%AF%86.html),实施成本应单独评估。
6. **录制默认关闭**:只有在业务、告知同意、保存期限、访问控制和删除策略均明确后才启用。官方说明云录制文件存入客户指定的云存储;开启私有加密会限制云录制等服务。
7. **日志最小化**:不记录 UserSig、PrivateMapKey、完整 IPC 消息、病历内容或屏幕标题;支持包上传前先脱敏。SDK 日志目录应受操作系统用户权限保护并配置留存期。
8. **程序供应链**Windows 代码签名;macOS Developer ID、Hardened Runtime/必要 entitlement、Notarization;固定已验证 SDK 版本,升级先做双平台回归。
9. **网络准入**:医院/机构网络可能限制 UDP。上线前按[防火墙白名单](https://intl.cloud.tencent.com/zh/document/product/647/35164)在真实网络验证 Native/Electron 或 WebRTC 所需端口与动态域名,不要只在家庭网络测试。
10. **合规不是 SDK 自动获得**:腾讯的信息安全说明明确不是国家/行业标准承诺,强制要求建议通过书面 SLA 确认。医疗隐私、录制同意、数据驻留和留存仍需项目方做法务/安全评审。
## 最小可验证集成(推荐:Electron companion
### 验证目标
先在一个 Windows 10/11 x64 和一个受支持 macOS 真机上实现 doctor ↔ patient 互通;进入预生产前再增加同平台终端,覆盖 Windows ↔ Windows、macOS ↔ macOS。最终证明安全签票、跨平台进房、音视频、设备权限、屏幕辅路、重连、正常退房和打包后运行都成立。
### 实施顺序
1. **TRTC 开发应用**
- 新建独立开发 SDKAppID。
- 先关闭自动录制/旁路转推;高级权限控制在基础链路跑通后于同一开发应用或新应用验证。
2. **Python/任意现有后端的 ticket endpoint**
- `POST /api/appointments/{id}/rtc-ticket`
- 从登录态和预约记录派生 `userId`/room ID;使用腾讯官方 Python HMAC-SHA256 示例在服务端生成 UserSig。
- 响应中不包含 SecretKey;票据只允许用于该预约与短时间窗。
3. **最小 Electron companion**
- 安装官方 SDK,创建 `TRTCCloud` 单例并先注册回调。
- 请求相机/麦克风权限,提供摄像头、麦克风、扬声器测试。
- 使用 `TRTCAppSceneVideoCall` 进房;等待 `onEnterRoom > 0`
- 用户点击后调用 `startLocalPreview()``startLocalAudio(TRTCAudioQualitySpeech)`
- 监听远端主路/辅路可用事件并渲染;实现窗口/整屏辅路分享和停止。
- 退出时完整清理并调用 `destroyTRTCShareInstance()`
4. **PySide6 最小联动**
- “开始面诊”请求 ticket,创建带随机 IPC nonce 的 companionticket 通过受认证 IPC 发送,不放命令行。
- companion 将状态和最终错误码回传;PySide6 只展示状态并提供结束入口。
5. **后端回调**
- 配置 HTTPS 房间/媒体回调,启用自定义 callback key。
- 对原始 body 验签并幂等落库;把客户端与回调状态关联到 appointment/session ID。
6. **高级权限控制验证**
- 服务端生成 PrivateMapKey;验证错误房间、过期/错误票据、患者无屏幕上行权限均被服务端拒绝。
### 最小验收清单
- [ ] Windows ↔ Windows、macOS ↔ macOS、Windows ↔ macOS 三组均能进房。
- [ ] SDKSecretKey 不存在于 Python 包、Electron 包、JS source map、命令行和日志。
- [ ] 正确票据进房成功;错误 UserSig、错误 PrivateMapKey、非预约参与者进房失败。
- [ ] 摄像头、麦克风、扬声器可检测/切换;拒绝授权后 UI 可恢复;设备拔插不崩溃。
- [ ] 医生与患者可看到/听到对方;以首帧和首个音频回调确认,不只凭 UI 按钮状态。
- [ ] 辅路屏幕分享与摄像头并存;系统 UI 停止分享后双方状态同步;第二路分享被正确阻止。
- [ ] 网络断开时进入 reconnecting,恢复后继续通话;应用强杀后服务端约 90 秒收敛,客户端有经过验证的重新取票/进房路径。
- [ ] 重复 UserID 的互踢行为有明确提示;正常退出释放摄像头/麦克风并销毁实例。
- [ ] 回调验签失败被拒绝;腾讯回调重试不会重复结算或重复关闭预约。
- [ ] Windows 签名安装包和 macOS 签名/公证包在干净机器可运行并能申请权限。
- [ ] 医院/目标机构网络按官方白名单完成真实音视频与屏幕分享测试。
### 决策门槛
- 若 Electron companion 的包体/双窗口体验可以接受,按该架构进入生产化。
- 若产品坚持单窗口,可并行做 2 个受限 POC:
1. `QWebEngineView`:仅在上述 WebView 准入测试全部通过后采用;失败即回退 Electron。
2. Native C++ bridge:先只实现 SDK version、实例、回调、一个本地/远端原生渲染视图和完整销毁;确认 Qt 6/PySide6 稳定后再扩展设备与屏幕分享。
- 若组织要求媒体私有加密且同时要求云端录制,必须先解决官方能力冲突,不能在开发末期再补。
## 官方资料索引
- [TRTC 产品概述与平台支持(用户指定文档 16788)](https://cloud.tencent.com/document/product/647/16788)
- [TRTC 基本概念:UserID、房间与生命周期](https://cloud.tencent.com/document/product/647/46351)
- [Web & H5 快速接入](https://cloud.tencent.com/document/product/647/116544)
- [Web DemoWebView 检测、HTTPS 与防火墙要求](https://cloud.tencent.com/document/product/647/32398)
- [Web API 概览与平台支持矩阵](https://intl.cloud.tencent.com/zh/document/product/647/41664)
- [Web 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/35163)
- [Electron 快速接入、设备权限与打包](https://cloud.tencent.com/document/product/647/116549)
- [TUIRoomKit Electron:医疗问诊等场景](https://cloud.tencent.com/document/product/647/129879)
- [Electron 屏幕分享](https://intl.cloud.tencent.com/zh/document/product/647/47619)
- [Electron SDK API](https://web.sdk.qcloud.com/trtc/electron/doc/en-us/trtc_electron_sdk/TRTCCloud.html)
- [C++ 全平台 API 概览](https://cloud.tencent.com/document/product/647/32268)
- [C++ Qt Windows/macOS 集成](https://intl.cloud.tencent.com/zh/document/product/647/39665)
- [UserSig 用户鉴权与官方 Python 服务端示例入口](https://cloud.tencent.com/document/product/647/17275)
- [原始 TRTC Web 进房时的 UserSig 校验说明](https://cloud.tencent.com/document/product/1715/104507)
- [PrivateMapKey 高级权限控制](https://intl.cloud.tencent.com/zh/document/product/647/35157)
- [房间与媒体服务端回调、签名和重试](https://intl.cloud.tencent.com/zh/document/product/647/39558)
- [房间解散 REST API](https://cloud.tencent.com/document/api/647/50089)
- [断线与自动重连行为](https://intl.cloud.tencent.com/document/product/647/36057)
- [屏幕分享(macOS/Native](https://cloud.tencent.com/document/product/647/32249)
- [TRTC 信息安全说明](https://cloud.tencent.com/document/product/647/86362)
- [媒体流私有加密](https://cloud.tencent.com/document/product/647/106173)
- [Native/WebRTC 防火墙白名单](https://intl.cloud.tencent.com/zh/document/product/647/35164)
+95
View File
@@ -0,0 +1,95 @@
# 医生工作站 UI 最终离屏回归
- 验收日期:2026-08-10
- 环境:Windows 11、Python 3.12.12、PySide6 6.11.1
- 目标尺寸:1280 × 800
- 最小 Shell 尺寸:1024 × 640
- 最终结论:**PASS**
## 1. 结论摘要
最新原始源码已通过完整离屏回归。验收在全新 Python 进程中执行,没有加载任何方法别名、monkey patch 或兼容垫片,并明确断言 `BusyOverlay` 不存在临时 `set_text` 属性。
`LoginWindow.submit()` 使用空账号和密码触发 Demo 默认凭据,真实执行 `DemoDoctorRepository.login()``get_current_user()`,随后由 `ApplicationController` 创建 `ShellWindow`。登录成功后 loading 正常释放、密码被清空,五个授权页面均能异步加载。
问诊列表到 Controller 再到 `DemoVideoDialog` 的信号链路、性别文本、服务器设置和 1024 × 640 最小 Shell 均通过。上一轮发现的四个问题现已全部关闭。
## 2. 验收方法
1. 使用 `QT_QPA_PLATFORM=offscreen` 创建真实 `QApplication`
2. 创建原始 `LoginWindow`,保持 Demo 模式并将账号密码留空,调用真实 `submit()`
3. 等待 `login_succeeded` 和 Controller 创建 Shell,核对认证 Session、用户、repository、loading 与密码清理状态。
4. 依次进入接诊台、我的处方库、已开处方、我的患者、问诊列表,等待后台 Worker 返回数据和详情。
5. 在问诊列表点击“发起视频问诊”,验证 `ConsultationsPage → ShellWindow.video_requested → ApplicationController._request_video → DemoVideoDialog` 完整链路。
6. 将 Shell 精确调整为 1024 × 640,检查接诊主要按钮的窗口坐标和 splitter 两侧可用宽度。
7. 关闭 Demo 后展开服务器设置,保存 HTTPS 地址与 45 秒超时,验证设置存储和两个配置变更信号。
8. 使用 `QWidget.grab()` 覆盖必要截图并逐张目检。
Qt `offscreen` 平台在本机不提供系统字体列表,因此验收进程临时用 `QFontDatabase.addApplicationFont()` 加载 `C:\Windows\Fonts\msyh.ttc`,使截图反映真实中文排版;未修改产品源码或打包配置。
## 3. 回归结果
| 范围 | 结果 | 验收证据 |
| --- | --- | --- |
| 真实 Demo 登录 | 通过 | 空账号密码成功使用 Demo 默认凭据;发出 `login_succeeded`;Session 已认证;用户为“陈医生(演示)”;loading 释放且密码清空 |
| 服务器设置按钮 | 通过 | Demo 模式下禁用;关闭 Demo 后可展开、保存、收起;地址规范化为 `https://api.example.com/adminapi`,超时为 45 秒 |
| Shell / 权限导航 | 通过 | Controller 创建接诊台、我的处方库、已开处方、我的患者、问诊列表共 5 页 |
| 接诊台 | 通过 | 待接诊 2 条,详情异步加载;性别显示“女”;1280 × 800 与 1024 × 640 均稳定 |
| 我的处方库 | 通过 | Demo 模板 2 条 |
| 已开处方 | 通过 | Demo 处方 2 条;详情显示“赵明远 · 男 · 53岁” |
| 我的患者 | 通过 | Demo 患者 3 条,首条详情正常加载 |
| 问诊列表 | 通过 | 今日待接诊 1 条,视频按钮可用 |
| 问诊 → Controller → DemoVideoDialog | 通过 | payload 为 `appointment_id=101``diagnosis_id=501``patient_id=301`Controller 以 key `501` 创建并回收窗口 |
| Demo 视频窗口 | 通过 | 980 × 660 正常渲染;计时到 `00:01`;麦克风与摄像头均可切换为关闭 |
| 1024 × 640 最小 Shell | 通过 | “完成接诊”边界为 `(883, 243, 84, 38)`,右边界 967、下边界 281,完整位于窗口内;splitter 宽度为 `[322, 430]` |
| 1280 × 800 视觉 | 通过 | 五页、登录页、服务器设置和视频窗无重叠或横向裁切 |
离屏断言摘要:
```text
runtime_adapter = false
login = authenticated / 陈医生(演示) / loading released / password cleared
server_url = https://api.example.com/adminapi
server_timeout = 45
page_rows = reception 2 / library 2 / prescriptions 2 / patients 3 / consultations 1
reception_gender = 女 · 46岁 · 患者编号 301
prescription_gender = 赵明远 · 男 · 53岁
video_ids = appointment 101 / diagnosis 501 / patient 301
video_dialog = key 501 / duration 00:01 / mic off / camera off
compact_shell = 1024 × 640 / complete_button right 967 bottom 281
pytest = 69 passed
```
## 4. 问题关闭情况
| 问题 | 状态 | 本轮证据 |
| --- | --- | --- |
| F-01 问诊列表视频 ID 错置 | 已关闭 | 实际点击后得到 101 / 501 / 301,并由 Controller 创建 `DemoVideoDialog` |
| F-02 紧凑尺寸接诊详情横向裁切 | 已关闭 | Shell 最小尺寸为 1024 × 640;该尺寸下两侧 splitter 有效,“完成接诊”完整可见 |
| F-03 性别显示内部数值 | 已关闭 | 接诊显示“女”,处方详情显示“男” |
| F-04 Demo 登录 loading 调用不存在的方法 | 已关闭 | `LoginWindow` 使用 `BusyOverlay.set_message()`;无垫片真实登录成功进入 Shell |
## 5. 截图索引
1. [登录页 1280 × 800](../artifacts/ui_acceptance/01_login_1280x800.png)
2. [接诊台 1280 × 800](../artifacts/ui_acceptance/02_reception_1280x800.png)
3. [我的处方库 1280 × 800](../artifacts/ui_acceptance/03_prescription_library_1280x800.png)
4. [已开处方 1280 × 800](../artifacts/ui_acceptance/04_prescriptions_1280x800.png)
5. [我的患者 1280 × 800](../artifacts/ui_acceptance/05_patients_1280x800.png)
6. [问诊列表 1280 × 800](../artifacts/ui_acceptance/06_consultations_1280x800.png)
7. [Demo 视频窗口 980 × 660](../artifacts/ui_acceptance/07_demo_video_980x660.png)
8. [最小 Shell 1024 × 640](../artifacts/ui_acceptance/08_shell_compact_1024x640.png)
9. [问诊视频链路成功页 1280 × 800](../artifacts/ui_acceptance/09_consultations_video_success_1280x800.png)
10. [服务器设置 1280 × 800](../artifacts/ui_acceptance/10_login_server_settings_1280x800.png)
旧的 `08_shell_compact_900x600.png``09_consultations_video_error_1280x800.png` 是历史问题证据,不属于本轮最终截图索引。
## 6. 自动化测试记录
```text
uv run --offline pytest
..................................................................... [100%]
69 passed in 0.65s
```
本轮验收没有修改 `src/` 下任何源文件;仅覆盖本报告和 `artifacts/ui_acceptance/` 下的 PNG 截图。