diff --git a/.trae/documents/igg4-tracking-implementation-plan.md b/.trae/documents/igg4-tracking-implementation-plan.md new file mode 100644 index 0000000..c2139d1 --- /dev/null +++ b/.trae/documents/igg4-tracking-implementation-plan.md @@ -0,0 +1,310 @@ +# IGG4 小程序埋点实施计划 + +## 概要 + +基于 `doc/track.md` 文档和信达项目的成熟埋点模式,为 IGG4 项目实施页面级 PV 埋点和点击级事件埋点。采用信达项目已验证的 `PG_` / `BTN_` 双层命名体系,复用现有 `app.mpBehavior()` 基础设施。 + +## 现状分析 + +| 维度 | 现状 | +|------|------| +| 埋点函数 | `app.mpBehavior({ PageName, doctor? })` — 仅 1 处调用 (`MINI_PROGRAM_VISIT`) | +| API 接口 | 患者端 `?r=igg4/mp-behavior/add`,医生端 `?r=igg4/doctor/mp-behavior/add` | +| 类型签名 | `{ PageName: string; doctor?: boolean }` — 需扩展以支持 `detailId` 等可选参数 | +| 页面级埋点 | 完全未实现(track.md 中 25+ 页面均未添加调用) | +| 点击级埋点 | 完全未实现 | +| 命名规范 | 无(信达项目使用 `PG_`/`BTN_` 前缀命名体系) | + +## 命名规范(参照信达项目) + +| 类别 | 前缀 | 格式 | 示例 | +|------|------|------|------| +| 页面浏览(患者端) | `PG_` | `PG_Patient{功能名}` | `PG_PatientHome`、`PG_PatientRepository` | +| 页面浏览(医生端) | `PG_` | `PG_Doctor{功能名}` | `PG_DoctorHome`、`PG_DoctorPatient` | +| 点击事件(患者端) | `BTN_` | `BTN_Patient{功能名}{操作}` | `BTN_PatientHomeDoctorCard`、`BTN_PatientMyVip` | +| 点击事件(医生端) | `BTN_` | `BTN_Doctor{功能名}{操作}` | `BTN_DoctorHomeInvite`、`BTN_DoctorPatientDetail` | + +### PageName 与 track.md 页面对照表 + +**患者端页面级埋点(PG_):** + +| track.md 页面路径 | PageName | +|-------------------|----------| +| pages/index/index | `PG_PatientHome` | +| gift/pages/recordList/index | `PG_PatientRecordList` | +| gift/pages/record/index | `PG_PatientRecordAdd` | +| gift/pages/recordDetail/index | `PG_PatientRecordDetail` | +| pages/interactivePatient/index | `PG_PatientInteractive` | +| pages/doctorDetail/index | `PG_PatientDoctorDetail` | +| pages/repository/index | `PG_PatientRepository` | +| pages/repositoryDetail/index | `PG_PatientRepositoryDetail` | +| pages/my/index | `PG_PatientMy` | +| pages/qaFormDetail/index | `PG_PatientQaFormDetail` | +| pages/qaForm/index | `PG_PatientQaForm` | +| pages/caseReport/index | `PG_PatientCaseReport` | +| pages/personalInformation/index | `PG_PatientPersonalInfo` | +| pages/cancellation/index | `PG_PatientCancellation` | +| pages/mySave/index | `PG_PatientMySave` | +| pages/feedback/index | `PG_PatientFeedback` | + +**医生端页面级埋点(PG_):** + +| track.md 页面路径 | PageName | +|-------------------|----------| +| doctor/pages/d_home/index | `PG_DoctorHome` | +| doctor/pages/d_invite/index | `PG_DoctorInvite` | +| doctor/pages/d_patient/index | `PG_DoctorPatient` | +| doctor/pages/d_patientDetail/index | `PG_DoctorPatientDetail` | +| doctor/pages/d_taskList/index | `PG_DoctorTaskList` | +| doctor/pages/d_taskDetail/index | `PG_DoctorTaskDetail` | +| doctor/pages/d_createTask/index | `PG_DoctorCreateTask` | +| doctor/pages/d_interactive/index | `PG_DoctorInteractive` | +| doctor/pages/d_interactiveDoctor/index | `PG_DoctorInteractiveDoctor` | +| doctor/pages/d_my/index | `PG_DoctorMy` | +| doctor/pages/d_userInfo/index | `PG_DoctorUserInfo` | +| doctor/pages/d_changeDoctor/index | `PG_DoctorChangeDoctor` | + +## 实施步骤 + +### 第 1 步:扩展 mpBehavior 类型签名 + +**文件**: `src/app.ts` + `typings/index.d.ts` + +扩展 `mpBehavior` 方法签名,支持可选的 `detailId` 参数(用于详情页等场景): + +```ts +// typings/index.d.ts +mpBehavior: (data: { PageName: string; doctor?: boolean; detailId?: string }) => void + +// src/app.ts — 实现签名同步更新 +mpBehavior(data: { PageName: string; doctor?: boolean; detailId?: string }) { + // ... 现有逻辑不变,detailId 会随 data 对象一起 POST 到后端 +} +``` + +> 说明:与信达项目一致,`detailId` 等额外字段作为 POST data 的一部分直接发送给后端,无需在 `mpBehavior` 函数内做特殊处理。 + +### 第 2 步:实施患者端页面级埋点(16 个页面) + +在每个页面的 `onShow` 中(有 `waitLogin` 的在 `waitLogin().then()` 内),调用 `app.mpBehavior()`。 + +**通用模式:** + +```ts +// 患者端页面(无 waitLogin) +onShow() { + app.mpBehavior({ PageName: 'PG_PatientXxx' }) +} + +// 患者端页面(有 waitLogin) +onShow() { + app.waitLogin().then(() => { + app.mpBehavior({ PageName: 'PG_PatientXxx' }) + // ... 原有业务逻辑 + }) +} +``` + +**修改文件清单:** + +| # | 文件路径 | PageName | 嵌入位置 | +|---|---------|----------|---------| +| 1 | `src/pages/index/index.ts` | `PG_PatientHome` | `onShow` → `waitLogin().then()` 内首行 | +| 2 | `src/gift/pages/recordList/index.ts` | `PG_PatientRecordList` | `onShow` → `waitLogin().then()` 内首行 | +| 3 | `src/gift/pages/record/index.ts` | `PG_PatientRecordAdd` | `onShow` → `waitLogin().then()` 内首行 | +| 4 | `src/gift/pages/recordDetail/index.ts` | `PG_PatientRecordDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 5 | `src/pages/interactivePatient/index.ts` | `PG_PatientInteractive` | `onLoad` → `waitLogin().then()` 内首行 | +| 6 | `src/pages/doctorDetail/index.ts` | `PG_PatientDoctorDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 7 | `src/pages/repository/index.ts` | `PG_PatientRepository` | `onShow` → `waitLogin().then()` 内首行 | +| 8 | `src/pages/repositoryDetail/index.ts` | `PG_PatientRepositoryDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 9 | `src/pages/my/index.ts` | `PG_PatientMy` | `onShow` → `waitLogin().then()` 内首行 | +| 10 | `src/pages/qaFormDetail/index.ts` | `PG_PatientQaFormDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 11 | `src/pages/qaForm/index.ts` | `PG_PatientQaForm` | `onShow` → `waitLogin().then()` 内首行 | +| 12 | `src/pages/caseReport/index.ts` | `PG_PatientCaseReport` | `onShow` → `waitLogin().then()` 内首行 | +| 13 | `src/pages/personalInformation/index.ts` | `PG_PatientPersonalInfo` | `onShow` → `waitLogin().then()` 内首行 | +| 14 | `src/pages/cancellation/index.ts` | `PG_PatientCancellation` | `onShow` → `waitLogin().then()` 内首行 | +| 15 | `src/pages/mySave/index.ts` | `PG_PatientMySave` | `onShow` → `waitLogin().then()` 内首行 | +| 16 | `src/pages/feedback/index.ts` | `PG_PatientFeedback` | `onShow` → `waitLogin().then()` 内首行 | + +### 第 3 步:实施医生端页面级埋点(12 个页面) + +医生端埋点需传入 `doctor: true`,API 路径切换为 `?r=igg4/doctor/mp-behavior/add`。 + +**通用模式:** + +```ts +// 医生端页面 +onShow() { + app.waitLogin().then(() => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorXxx' }) + // ... 原有业务逻辑 + }) +} +``` + +**修改文件清单:** + +| # | 文件路径 | PageName | 嵌入位置 | +|---|---------|----------|---------| +| 1 | `src/doctor/pages/d_home/index.ts` | `PG_DoctorHome` | `onLoad` → `waitLogin().then()` 内首行 | +| 2 | `src/doctor/pages/d_invite/index.ts` | `PG_DoctorInvite` | `onShow` → `waitLogin().then()` 内首行 | +| 3 | `src/doctor/pages/d_patient/index.ts` | `PG_DoctorPatient` | `onShow` → `waitLogin().then()` 内首行 | +| 4 | `src/doctor/pages/d_patientDetail/index.ts` | `PG_DoctorPatientDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 5 | `src/doctor/pages/d_taskList/index.ts` | `PG_DoctorTaskList` | `onShow` → `waitLogin().then()` 内首行 | +| 6 | `src/doctor/pages/d_taskDetail/index.ts` | `PG_DoctorTaskDetail` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 7 | `src/doctor/pages/d_createTask/index.ts` | `PG_DoctorCreateTask` | `onShow` → `waitLogin().then()` 内首行 | +| 8 | `src/doctor/pages/d_interactive/index.ts` | `PG_DoctorInteractive` | `onShow` → `waitLogin().then()` 内首行 | +| 9 | `src/doctor/pages/d_interactiveDoctor/index.ts` | `PG_DoctorInteractiveDoctor` | `onShow` → `waitLogin().then()` 内首行,带 `detailId` | +| 10 | `src/doctor/pages/d_my/index.ts` | `PG_DoctorMy` | `onShow` → `waitLogin().then()` 内首行 | +| 11 | `src/doctor/pages/d_userInfo/index.ts` | `PG_DoctorUserInfo` | `onShow` → `waitLogin().then()` 内首行 | +| 12 | `src/doctor/pages/d_changeDoctor/index.ts` | `PG_DoctorChangeDoctor` | `onShow` → `waitLogin().then()` 内首行 | + +### 第 4 步:实施点击级埋点(BTN_) + +在关键交互的事件处理函数首行,插入 `app.mpBehavior()` 调用。 + +#### 患者端点击埋点 + +**首页 — `src/pages/index/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| `routerTo(e)` 中 doctor 类卡片 | `BTN_PatientHomeDoctorCard` | 迷你医生卡片点击 | +| `routerTo(e)` 中功能入口 | `BTN_PatientHomeFunctionCard` | 功能卡片跳转 | +| `handleNextVisit()` | `BTN_PatientHomeNextVisit` | 下次复诊入口 | +| `handleArticleView(e)` | `BTN_PatientHomeArticle` | 文章点击 | +| `handleArticleSave(e)` | `BTN_PatientHomeArticleSave` | 文章收藏 | +| `handleToastOk()`(绑定医生确认) | `BTN_PatientHomeBindDoctor` | 绑定医生弹窗确认 | +| `handlePublic()` | `BTN_PatientHomeFollowPublic` | 关注公众号 | + +**我的页面 — `src/pages/my/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| `routerVipTo()` | `BTN_PatientMyVip` | VIP 认证入口 | +| `handleMyHealthRecord(e)` | `BTN_PatientMyHealthRecord` | 健康档案 | +| `handleQaForm()` | `BTN_PatientMyQaForm` | 健康问卷 | +| `routerTo(e)` 中 mySave | `BTN_PatientMySave` | 我的收藏 | +| `routerTo(e)` 中 personalInfo | `BTN_PatientMyPersonalInfo` | 个人信息 | +| `routerTo(e)` 中 feedback | `BTN_PatientMyFeedback` | 问题反馈 | +| `handleMiniDoctor()` | `BTN_PatientMyMiniDoctor` | 迷你医生 | + +**医患互动 — `src/pages/interactivePatient/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleHot(e)` | `BTN_PatientInteractiveHotQuestion` | 热门问题点击 | +| `handleQuestion(e)` | `BTN_PatientInteractiveQuestion` | 快捷问题点击 | +| `handleConfirm()` | `BTN_PatientInteractiveSend` | 发送消息 | +| `handleAdl()` | `BTN_PatientInteractiveAdl` | ADL 评估入口 | +| `handleQol()` | `BTN_PatientInteractiveQol` | QoL 入口 | +| `handleDoctorDetail()` | `BTN_PatientInteractiveDoctorDetail` | 查看医生详情 | + +**知识库 — `src/pages/repository/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| `tabChange(e)` | `BTN_PatientRepositoryTab` | Tab 切换 | +| `handleSearch()` | `BTN_PatientRepositorySearch` | 搜索 | +| `handleCate(e)` | `BTN_PatientRepositoryCategory` | 分类筛选 | +| `handleDetail(e)` | `BTN_PatientRepositoryArticle` | 文章详情 | +| `handleStar(e)` | `BTN_PatientRepositoryStar` | 点赞 | +| `handleToggleSave(e)` | `BTN_PatientRepositorySave` | 收藏 | + +**就诊记录列表 — `src/gift/pages/recordList/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 新增就诊记录入口 | `BTN_PatientRecordListAdd` | 新增记录按钮 | +| 点击记录项 | `BTN_PatientRecordListItem` | 记录列表项点击 | + +**就诊记录新增 — `src/gift/pages/record/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 提交/保存按钮 | `BTN_PatientRecordAddSubmit` | 保存记录 | + +**医生详情 — `src/pages/doctorDetail/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 查看互动 | `BTN_PatientDoctorDetailInteractive` | 进入互动页 | + +#### 医生端点击埋点 + +**医生首页 — `src/doctor/pages/d_home/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handlePatient(e)` | `BTN_DoctorHomePatient` | 患者列表入口 | +| `handleInvite()` | `BTN_DoctorHomeInvite` | 邀约按钮 | +| `handleSelectYear1()` | `BTN_DoctorHomeSelectYear` | 年份筛选 | + +**邀约患者 — `src/doctor/pages/d_invite/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 发送邀约按钮 | `BTN_DoctorInviteSend` | 发送邀约 | + +**患者列表 — `src/doctor/pages/d_patient/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 搜索 | `BTN_DoctorPatientSearch` | 搜索患者 | +| 点击患者项 | `BTN_DoctorPatientItem` | 患者列表项 | + +**互动列表 — `src/doctor/pages/d_interactive/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 搜索 | `BTN_DoctorInteractiveSearch` | 搜索互动 | +| 点击互动项 | `BTN_DoctorInteractiveItem` | 互动列表项 | + +**互动页 — `src/doctor/pages/d_interactiveDoctor/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 发送消息 | `BTN_DoctorInteractiveSend` | 发送消息 | + +**群发列表 — `src/doctor/pages/d_taskList/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 新增群发 | `BTN_DoctorTaskListAdd` | 新增群发入口 | +| 点击群发项 | `BTN_DoctorTaskListItem` | 群发列表项 | + +**群发新增 — `src/doctor/pages/d_createTask/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 提交群发 | `BTN_DoctorCreateTaskSubmit` | 创建群发 | + +**切换医生 — `src/doctor/pages/d_changeDoctor/index.ts`** + +| 方法 | PageName | 说明 | +|------|----------|------| +| 选择医生 | `BTN_DoctorChangeDoctorSelect` | 切换医生选择 | + +> **注意**:以上点击埋点 PageName 是基于代码探索的预定义清单。实施时需逐页确认实际的方法名和交互行为,如有差异需现场调整。 + +### 第 5 步:更新 doc/track.md 文档 + +将完整的 PageName 对照表(页面级 + 点击级)更新到 `doc/track.md`,作为埋点实施的参考文档。 + +## 假设与决策 + +1. **命名规范**:采用信达项目的 `PG_/BTN_` 前缀体系,患者端用 `Patient` 前缀,医生端用 `Doctor` 前缀。这样命名清晰且便于后端区分数据来源。 +2. **detailId 参数**:扩展 `mpBehavior` 类型签名以支持可选 `detailId`,用于详情页场景。实际在 JS 层该字段会随 data 对象一起 POST,无需函数内额外处理。 +3. **页面级埋点位置**:有 `waitLogin` 的页面在 `.then()` 内首行调用;无 `waitLogin` 的在 `onShow` 首行调用。确保用户登录后才上报,避免无效数据。 +4. **点击级埋点位置**:在事件处理函数首行调用 `mpBehavior`,先上报再执行业务逻辑(与信达项目一致)。 +5. **医生端 `doctor: true`**:所有医生端埋点均传入 `doctor: true`,确保 API 路径正确切换。 +6. **MINI_PROGRAM_VISIT 保留**:`app.ts` 中现有的 `MINI_PROGRAM_VISIT` 埋点保留不变,它记录小程序整体打开事件,与页面级 PV 埋点互不冲突。 +7. **无集中常量文件**:PageName 采用硬编码字符串(与信达项目一致),不创建额外的常量/枚举文件,避免增加维护成本。 + +## 验证步骤 + +1. 编译检查:确保 `pnpm run lint:fix` 无新增错误 +2. 逐页验证:在微信开发者工具中打开每个页面,检查 console 中 mpBehavior POST 请求是否正常发出 +3. 参数验证:确认患者端请求路径为 `?r=igg4/mp-behavior/add`,医生端为 `?r=igg4/doctor/mp-behavior/add` +4. doctor 参数验证:确认 `doctor` 字段在发送前被正确删除(不出现在 POST body 中) +5. detailId 验证:确认详情页的 `detailId` 正确传入 POST body diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..6d52f27 --- /dev/null +++ b/doc/api.md @@ -0,0 +1,1419 @@ +# 就诊记录接口文档 + +## 通用说明 + +### 响应格式 + +所有接口返回 JSON,严格遵循以下格式: + +```json +{ + "code": 0, + "message": "success", + "data": {} +} +``` + +- `code`:0 表示成功,非 0 表示失败 +- `message`:成功时为 "success",失败时为错误描述 +- `data`:业务数据,失败时为 null + +### 数据命名规则 + +`data` 中所有字段使用**小驼峰**命名法(如 `visitDate`、`igG4Value`)。 + +### 登录校验 + +所有接口需通过请求头 `loginState` 或请求参数 `loginState` 传递登录态,未登录返回 code=403。 + +### 错误码 + +| code | 说明 | +|------|------| +| 0 | 成功 | +| 1 | 参数缺失 | +| 2 | 记录不存在 | +| 3 | 操作失败 | +| 403 | 需要登录 | + +--- + +## 一、患者端接口 + +模块路由前缀:`igg4/medical-visit` + +--- + +### 1.1 保存草稿 + +- **路由**:`?r=igg4/medical-visit/save-draft` +- **请求方式**:POST +- **说明**:创建或更新草稿记录。传入 `id` 时更新已有草稿,不传时新建草稿。图片列表支持新增、更新、逻辑删除的 diff 处理。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 否 | 已有草稿 ID,更新时必传 | +| visitDate | string | 是 | 本次就诊时间,格式 YYYY-MM-DD | +| nextVisitDate | string | 否 | 下次就诊时间,格式 YYYY-MM-DD | +| oralHormoneDosage | float | 否 | 口服激素每日用量(片) | +| manualIgG4Value | float | 否 | 患者手动录入的IgG4数值(冗余字段,单位mg/dL) | +| medicationsUsed | array | 否 | 本次就诊已使用的药物(多选),值为常量ID数组,如 [1,2,3],见附录G | +| immunosuppressantName | array | 否 | 正在使用的免疫抑制剂名称(多选),值为常量ID数组,如 [1,5],见附录G | +| medicationsUsedOtherText | string | 否 | 本次就诊已使用药物选择"其他"(99)时的补充文本 | +| immunosuppressantNameOtherText | string | 否 | 正在使用的免疫抑制剂名称选择"其他"(99)时的补充文本 | +| images | array | 否 | 图片列表 | + +#### images 每项参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| imageUrl | string | 是 | 图片 URL | +| imageType | string | 是 | 图片类型标识,见枚举表 | + +#### imageType 枚举 + +| 值 | 说明 | +|------|------| +| outpatientRecord | 门诊病历及处方 | +| labReport | 检验报告 | +| imagingExam | 影像学检查 | +| pathology | 病理诊断及活检 | +| immuneFunction | 外周血免疫功能评估 | + +#### 请求示例 + +```json +{ + "id": 0, + "visitDate": "2026-06-29", + "nextVisitDate": "2026-09-29", + "oralHormoneDosage": 4, + "manualIgG4Value": 135.0, + "medicationsUsed": [2, 3], + "immunosuppressantName": [1, 7], + "medicationsUsedOtherText": null, + "immunosuppressantNameOtherText": null, + "images": [ + { + "imageUrl": "https://oss.example.com/img001.jpg", + "imageType": "labReport" + }, + { + "imageUrl": "https://oss.example.com/img002.jpg", + "imageType": "outpatientRecord" + } + ] +} +``` + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "recordStatus": 1 + } +} +``` + +#### 业务逻辑说明 + +1. 若传入 `id`,校验记录存在、属于当前患者、为草稿状态(recordStatus=1),否则报错 +2. 若未传 `id`,新建主记录,设置 recordStatus=1(草稿)、status=1(有效) +3. 保存 CreateWorkerId/CreateTime/UpdateWorkerId/UpdateTime +4. 图片 diff 处理:通过 RecordId + ImageUrl 匹配已有图片记录 + - 已存在 → 更新 ImageType,重置 OcrStatus 为待解析(0),清空 OcrText/OcrParsedData/指标字段 + - 不存在 → 插入新图片记录(OcrStatus 默认为 0) + - 已有但本次未传入 → 逻辑删除(status=100) +5. 最终将所有检验报告图片的指标值和门诊病历图片的病历文本聚合到主表(后出现的值覆盖先出现的值) +6. `manualIgG4Value` 参数写入主表 `ManualIgG4Value` 字段,作为冗余存储,不影响 OCR 解析产生的主数据 `IgG4Value` + +--- + +### 1.1.0 保存图片记录 + +- **路由**:`?r=igg4/medical-visit/save-image` +- **请求方式**:POST +- **说明**:仅保存单张图片记录到就诊记录图片表,不更新主记录字段,不触发指标聚合。前端需先创建草稿获取 RecordId 后再调用此接口。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| recordId | int | 是 | 已有草稿记录 ID | +| imageUrl | string | 是 | 图片 URL | +| imageType | string | 是 | 图片类型标识,见枚举表 | + +#### imageType 枚举 + +同 [1.1 images.imageType](#imagetype-枚举) + +#### 请求示例 + +```json +{ + "recordId": 1, + "imageUrl": "https://oss.example.com/img001.jpg", + "imageType": "labReport" +} +``` + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "imageId": 102, + "recordId": 1, + "imageUrl": "https://oss.example.com/img001.jpg", + "imageType": "labReport", + "ocrStatus": 0 + } +} +``` + +#### 业务逻辑说明 + +1. 校验 recordId 对应的记录存在且属于当前患者,否则报错"就诊记录不存在"或"就诊记录不属于当前患者" +2. 校验记录状态为草稿(recordStatus=1),否则报错"仅草稿状态的记录可添加图片" +3. 将 imageType 字符串转为数据库整型值,无效则报错"无效的图片类型" +4. 根据 RecordId + ImageUrl 判断图片是否已存在: + - 已存在:更新 ImageType,重置 OcrStatus 为待解析(0),清空 OcrText、OcrParsedData 及对应指标/文本字段 + - 不存在:新建图片记录(含 CreateWorkerId/CreateTime),OcrStatus 默认为待解析(0) +5. **不触发主表指标聚合**(与 saveDraft 不同),聚合在调用 saveDraft 或 submit 时发生 +6. 小程序端仅存储原始图片,OCR 解析由后台管理员涂抹图片后触发,返回 ocrStatus 表示当前解析状态 + +--- + +### 1.1.1 删除单个图片 + +- **路由**:`?r=igg4/medical-visit/delete-image` +- **请求方式**:POST +- **说明**:逻辑删除指定图片记录,设置 Status=100,记录操作人和操作时间。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| imageId | int | 是 | 图片记录 ID | + +#### 请求示例 + +```json +{ + "imageId": 102 +} +``` + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": true +} +``` + +#### 业务逻辑说明 + +1. 校验 imageId 必传,否则报错"图片记录ID不能为空" +2. 校验管理员已登录,获取当前操作人 workerId +3. 校验图片记录存在且有效(Status=1),否则报错"图片记录不存在" +4. 逻辑删除:设置 Status=100(已删除),记录 UpdateWorkerId/UpdateTime +5. 返回 true + +--- + +### 1.2 提交就诊记录 + +- **路由**:`?r=igg4/medical-visit/submit` +- **请求方式**:POST +- **说明**:将草稿状态的记录提交,提交后不可再编辑。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 草稿记录 ID | + +#### 请求示例 + +```json +{ + "id": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": true +} +``` + +#### 业务逻辑说明 + +1. 校验记录存在、属于当前患者 +2. 校验记录状态为草稿(recordStatus=1),否则报错"仅草稿状态的记录可提交" +3. 更新 recordStatus=2(已提交),记录 UpdateWorkerId/UpdateTime + +--- + +### 1.3 获取就诊记录详情 + +- **路由**:`?r=igg4/medical-visit/detail` +- **请求方式**:GET +- **说明**:获取单条就诊记录详情,包含主记录所有指标数据和按类型分组的图片列表。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 就诊记录 ID | + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "visitDate": "2026-06-29", + "nextVisitDate": "2026-09-29", + "recordStatus": 2, + "igG4Value": 135.0, + "manualIgG4Value": 135.0, + "displayIgG4Value": 135.0, + "totalIgGValue": 1200.0, + "igEValue": 50.0, + "eosCount": 0.3, + "esrValue": 20.0, + "crpValue": 5.0, + "oralHormoneDosage": 4.0, + "medicationsUsed": [2, 3], + "immunosuppressantName": [1, 7], + "medicationsUsedOtherText": null, + "immunosuppressantNameOtherText": null, + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "reportTime": "2026-06-28 14:20:00", + "chiefComplaint": "反复腹痛3月", + "medicalHistory": "现病史:患者3月前无明显诱因出现腹痛...", + "diagnosis": "IgG4相关性疾病", + "treatmentPlan": "激素治疗,定期复查", + "prescription": "甲泼尼龙片 4mg 每日1次 口服 30片", + "clinicalDiagnosis": "IgG4相关性疾病", + "images": { + "outpatientRecord": [ + { + "id": 101, + "imageUrl": "https://...", + "maskedImageUrl": "https://...masked.jpg", + "ocrStatus": 2, + "ocrParsedData": "门诊病历文本...", + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "reportTime": null + } + ], + "labReport": [ + { + "id": 102, + "imageUrl": "https://...", + "maskedImageUrl": "https://...masked.jpg", + "ocrStatus": 2, + "ocrParsedData": { + "reportTime": "2026-06-28", + "indicators": { + "igG4": {"value": 135.0, "unit": "mg/dL"}, + "totalIgG": {"value": 1200.0, "unit": "mg/dL"}, + "igE": {"value": 50.0, "unit": "IU/mL"}, + "eos": {"value": 0.3, "unit": "10^9/L"}, + "esr": {"value": 20.0, "unit": "mm/h"}, + "crp": {"value": 5.0, "unit": "mg/L"} + } + }, + "medicalRecordTime": null, + "prescriptionTime": null, + "reportTime": "2026-06-28 14:20:00" + } + ], + "imagingExam": [ + { + "id": 103, + "imageUrl": "https://...", + "maskedImageUrl": null, + "ocrStatus": 0, + "ocrParsedData": null + } + ], + "pathology": [], + "immuneFunction": [] + } + } +} +``` + +#### images 字段说明 + +- 固定返回 5 种图片类型的分组,无数据时为空数组 +- 每张图片包含 `id`、`imageUrl`、`maskedImageUrl`、`ocrStatus`、`ocrParsedData`、`medicalRecordTime`、`prescriptionTime`、`reportTime` +- `maskedImageUrl`:涂抹后图片 URL,后台管理员未涂抹时为 null +- `ocrStatus`:OCR 解析状态,0-待解析,1-解析中,2-解析完成,3-解析失败 +- `ocrParsedData`:检验报告类型为包含 `reportTime` 和 `indicators` 的结构化 JSON 对象,门诊病历类型为包含 `medicalRecordTime` 和 `prescriptionTime` 的结构化 JSON 对象,其他类型为拼接文本字符串,无 OCR 数据时为 null +- `medicalRecordTime`:病历时间(图片类型=门诊病历且识别为门诊病历时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss +- `prescriptionTime`:处方时间(图片类型=门诊病历且识别为处方时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss +- `reportTime`:检查报告时间(图片类型=检验报告时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss + +#### 主记录字段说明 + +- `displayIgG4Value`:展示用 IgG4 数值,优先取 manualIgG4Value,为空则取 igG4Value;均为空时为 null +- `medicalRecordTime`:病历时间(从门诊病历图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null +- `prescriptionTime`:处方时间(从处方图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null +- `reportTime`:检查报告时间(从检验报告图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null + +--- + +### 1.4 就诊记录列表 + +- **路由**:`?r=igg4/medical-visit/list` +- **请求方式**:GET +- **说明**:获取当前患者的就诊记录列表,支持分页和年份/次数筛选。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| page | int | 否 | 页码,默认 1 | +| pageSize | int | 否 | 每页条数,默认 10 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "page": 1, + "pages": 5, + "totalCount": 50, + "list": [ + { + "id": 1, + "visitDate": "2026-06-29", + "nextVisitDate": "2026-09-29", + "recordStatus": 2, + "igG4Value": 135.0, + "manualIgG4Value": 135.0, + "displayIgG4Value": 135.0, + "medicationsUsed": [2, 3], + "immunosuppressantName": [1, 7], + "medicationsUsedOtherText": null, + "immunosuppressantNameOtherText": null, + "lastIgG4Value": 120.0, + "change": 15.0, + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "reportTime": "2026-06-28 14:20:00" + } + ] + } +} +``` + +#### 说明 + +- 列表按 VisitDate 降序排列 +- 同时传入 `year` 和 `count` 时,先按年份筛选再取最近 N 条 +- `lastIgG4Value`:上一次就诊的 IgG4 值,float 类型;无上一次记录时为 null +- `change`:本次与上一次 IgG4 值的差值(本次 - 上次),float 类型;任一值为 null 时为 null +- `manualIgG4Value`:患者手动录入的 IgG4 数值(冗余字段),未录入时为 null +- `displayIgG4Value`:展示用 IgG4 数值,优先取 manualIgG4Value,为空则取 igG4Value;均为空时为 null +- `medicalRecordTime`:病历时间(从门诊病历图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null +- `prescriptionTime`:处方时间(从处方图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null +- `reportTime`:检查报告时间(从检验报告图片聚合),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss,无数据时为 null +- `medicationsUsed`:本次就诊已使用的药物(多选),值为常量ID数组,见附录G;未选择时为 null +- `immunosuppressantName`:正在使用的免疫抑制剂名称(多选),值为常量ID数组,见附录G;未选择时为 null +- `medicationsUsedOtherText`:本次就诊已使用药物选择"其他"(99)时的补充文本,未选择"其他"时为 null +- `immunosuppressantNameOtherText`:正在使用的免疫抑制剂名称选择"其他"(99)时的补充文本,未选择"其他"时为 null + +- 年份筛选优先级:传入 `yearStart`+`yearEnd` 时优先按范围筛选,忽略 `year` 参数;仅传 `yearStart` 取 >= 起始年份;仅传 `yearEnd` 取 <= 结束年份;仅传 `year` 则按单年份筛选 + +--- + +### 1.5 删除就诊记录 + +- **路由**:`?r=igg4/medical-visit/delete` +- **请求方式**:POST +- **说明**:逻辑删除就诊记录及其所有图片。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 就诊记录 ID | + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": true +} +``` + +#### 业务逻辑说明 + +1. 校验记录存在、属于当前患者 +2. 将主记录 status 设为 100(删除),记录 UpdateWorkerId/UpdateTime +3. 将该记录下所有图片 status 设为 100(删除) + +--- + +### 1.6 图片字段历史列表 + +- **路由**:`?r=igg4/medical-visit/image-history` +- **请求方式**:GET +- **说明**:查询指定图片类型的历史列表,返回每次就诊中该类型图片的 URL,按就诊日期降序排列。仅返回已提交的记录。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| imageField | string | 是 | 图片类型标识 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | +| page | int | 否 | 页码,默认 1 | +| pageSize | int | 否 | 每页条数,默认 10 | + +#### imageField 枚举 + +| 值 | 说明 | +|------|------| +| outpatientRecord | 门诊病历及处方 | +| labReport | 检验报告 | +| imagingExam | 影像学检查 | +| pathology | 病理诊断及活检 | +| immuneFunction | 外周血免疫功能评估 | + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "page": 1, + "pages": 5, + "totalCount": 50, + "list": [ + { + "visitDate": "2026-06-29", + "imageUrls": [ + "https://picsissiok-10049618.cos.ap-shanghai.myqcloud.com/0e3a32a7612090359a9ca36117d0f2db_17835667161658.jpg" + ], + "ocrStatus": 2 + }, + { + "visitDate": "2026-03-15", + "imageUrls": [ + "https://...", + ], + "ocrStatus": 0 + } + ] + } +} +``` + +- 分页按就诊记录维度,每页包含对应就诊记录中该类型的所有图片 URL 数组 + +-> imageUrls :图片 URL 数组 + +--- + +### 1.7 指标字段历史列表 + +- **路由**:`?r=igg4/medical-visit/indicator-history` +- **请求方式**:GET +- **说明**:查询指定指标字段的历史数据列表,返回每次就诊中该指标的值,按就诊日期降序排列。仅返回已提交的记录。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| indicatorField | string | 是 | 指标字段标识 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | + +#### indicatorField 枚举 + +| 值 | 说明 | 单位 | +|------|------|------| +| igG4 | IgG4(免疫球蛋白G4亚类) | mg/dL | +| totalIgG | 总IgG(免疫球蛋白G) | mg/dL | +| igE | IgE(免疫球蛋白E) | IU/mL | +| eos | EOS#(嗜酸细胞绝对计数) | 10^9/L | +| esr | ESR(红细胞沉降率/血沉) | mm/h | +| crp | CRP(C-反应蛋白) | mg/L | + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "unit": "mg/dL", + "list": [ + { + "visitDate": "2026-06-29", + "value": 135.0 + }, + { + "visitDate": "2026-03-15", + "value": 260.0 + } + ] + } +} +``` + +--- + +### 1.8 OCR 图片解析 + +- **路由**:`?r=igg4/medical-visit/ocr-parse` +- **请求方式**:POST +- **说明**:对图片进行阿里云 OCR 文字识别。检验报告类型自动提取指标数值并做单位换算,其他类型仅返回拼接文本。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| imageUrl | string | 是 | 图片 URL(需为阿里云上海 OSS 地址) | +| imageType | string | 是 | 图片类型标识 | + +#### imageType 枚举 + +同 [1.1 images.imageType](#imagetype-枚举) + +#### 响应示例 — 检验报告类型(imageType=labReport) + +```json +{ + "code": 0, + "message": "success", + "data": { + "ocrText": "IgG4 1.35 g/L\n总IgG 12.0 g/L\nIgE 50 IU/mL\nESR 20 mm/h\nCRP 5 mg/L", + "ocrParsedData": { + "reportTime": "2026-06-28 14:20:00", + "indicators": { + "igG4": {"value": 135.0, "unit": "mg/dL"}, + "totalIgG": {"value": 1200.0, "unit": "mg/dL"}, + "igE": {"value": 50.0, "unit": "IU/mL"}, + "eos": {"value": null, "unit": "10^9/L"}, + "esr": {"value": 20.0, "unit": "mm/h"}, + "crp": {"value": 5.0, "unit": "mg/L"} + } + } + } +} +``` + +#### 响应示例 — 门诊病历类型(imageType=outpatientRecord) + +```json +{ + "code": 0, + "message": "success", + "data": { + "ocrText": "患者XXX,男,45岁\n主诉:反复腹痛3月\n现病史:患者3月前无明显诱因出现腹痛\n诊断:IgG4相关性疾病\n处理意见:激素治疗,定期复查", + "ocrParsedData": { + "chiefComplaint": "反复腹痛3月", + "medicalHistory": "患者3月前无明显诱因出现腹痛", + "diagnosis": "IgG4相关性疾病", + "treatmentPlan": "激素治疗,定期复查", + "prescription": "甲泼尼龙片 4mg 每日1次 口服 30片", + "clinicalDiagnosis": "IgG4相关性疾病" + } + } +} +``` + +#### 响应示例 — 其他类型(如 imageType=imagingExam) + +```json +{ + "code": 0, + "message": "success", + "data": { + "ocrText": "影像学检查报告文本...", + "ocrParsedData": "影像学检查报告文本..." + } +} +``` + +#### OCR 指标识别规则 + +| 指标 | 关键词匹配 | 单位换算 | +|------|-----------|---------| +| IgG4 | `IgG4`、`免疫球蛋白G4`、`免疫球蛋白G4亚类` | 若为 g/L 则 ×100 → mg/dL | +| 总IgG | `IgG`(独立,非IgG1-5/IgG4/IgE)、`免疫球蛋白G` | 若为 g/L 则 ×100 → mg/dL | +| IgE | `IgE`、`免疫球蛋白E` | 不换算,保持 IU/mL | +| EOS# | `EOS#`、`嗜酸细胞绝对计数`、`嗜酸性粒细胞绝对值` | 不换算,保持 10^9/L | +| ESR | `ESR`、`红细胞沉降率`、`血沉` | 不换算,保持 mm/h | +| CRP | `CRP`、`C-反应蛋白`、`C反应蛋白` | 不换算,保持 mg/L | + +#### 门诊病历 OCR 识别规则 + +| 字段 | 关键词匹配 | +|------|-----------| +| 主诉 | `主诉` | +| 病史 | `现病史`、`既往史`、`病史` | +| 诊断 | `诊断`、`Imp`、`印象` | +| 处理意见 | `处理意见`、`医嘱`、`处理`、`治疗方案` | + +--- + +### 1.9 保存复诊提醒配置 + +- **路由**:`?r=igg4/medical-visit/save-reminder-config` +- **请求方式**:POST +- **说明**:患者配置是否开启复诊提醒。开启后,定时任务将在复诊日期前7天发送微信Push通知。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| reminderEnabled | int | 是 | 是否开启复诊提醒:1-开启,2-关闭 | + +#### 请求示例 + +```json +{ + "reminderEnabled": 1 +} +``` + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": true +} +``` + +#### 业务逻辑说明 + +1. 校验 reminderEnabled 参数必填,值只能为1(开启)或2(关闭),否则报错"reminderEnabled参数无效" +2. 查询当前患者是否已有配置记录 +3. 已有配置:更新 ReminderEnabled,记录 UpdateWorkerId/UpdateTime +4. 无配置:新建配置记录,设置 PatientId、ReminderEnabled、Status=1,记录 CreateWorkerId/CreateTime/UpdateWorkerId/UpdateTime +5. 每个患者只保留一条有效配置记录(通过 PatientId 唯一约束保证) + +--- + +### 1.10 获取复诊提醒配置 + +- **路由**:`?r=igg4/medical-visit/get-reminder-config` +- **请求方式**:GET +- **说明**:获取当前患者的复诊提醒配置。未配置过时返回默认值(关闭)。 + +#### 请求参数 + +无 + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "reminderEnabled": 2 + } +} +``` + +#### 说明 + +- `reminderEnabled`:1-开启,2-关闭。患者未配置过时默认返回2(关闭) +- 配置数据来源于 `t_medical_visit_reminder_config` 表 + +--- + +### 1.11 获取最近就诊记录 + +- **路由**:`?r=igg4/medical-visit/get-latest-record` +- **请求方式**:GET +- **说明**:获取当前患者按就诊日期最近的一条就诊记录详情,包含草稿和已提交记录。无记录时 data 为 null。 + +#### 请求参数 + +无 + +#### 响应示例 — 有记录时 + +同 [1.3 获取就诊记录详情](#13-获取就诊记录详情) 的 data 格式。 + +#### 响应示例 — 无记录时 + +```json +{ + "code": 0, + "message": "success", + "data": null +} +``` + +#### 说明 + +- 返回当前患者按就诊日期(VisitDate)降序最近的一条就诊记录详情 +- 不限制记录状态,草稿(recordStatus=1)和已提交(recordStatus=2)均可能返回 +- 同一天多条记录时,返回 ID 最大的那条 +- 返回格式与详情接口完全一致,包含图片分组等完整信息 + +--- + +## 二、医生端接口 + +模块路由前缀:`igg4/doctor/medical-visit` + +医生端所有接口均为**只读**,且只能查看**已提交**(recordStatus=2)的记录,草稿对医生不可见。所有接口需额外传入 `patientId` 参数指定查看的患者。 + +--- + +### 2.1 就诊记录列表 + +- **路由**:`?r=igg4/doctor/medical-visit/list` +- **请求方式**:GET +- **说明**:医生查看指定患者的已提交就诊记录列表。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| patientId | int | 是 | 患者 ID | +| page | int | 否 | 页码,默认 1 | +| pageSize | int | 否 | 每页条数,默认 10 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | + +#### 响应示例 + +同 [1.4 就诊记录列表](#14-就诊记录列表),但仅返回已提交的记录。 + +--- + +### 2.2 就诊记录详情 + +- **路由**:`?r=igg4/doctor/medical-visit/detail` +- **请求方式**:GET +- **说明**:医生查看指定患者的某条就诊记录详情。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| id | int | 是 | 就诊记录 ID | +| patientId | int | 是 | 患者 ID | + +#### 响应示例 + +同 [1.3 获取就诊记录详情](#13-获取就诊记录详情)。 + +--- + +### 2.3 图片字段历史列表 + +- **路由**:`?r=igg4/doctor/medical-visit/image-history` +- **请求方式**:GET +- **说明**:医生查看指定患者的某种图片类型历史列表。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| patientId | int | 是 | 患者 ID | +| imageField | string | 是 | 图片类型标识 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | + +#### imageField 枚举 + +同 [1.6 imageField 枚举](#imagefield-枚举) + +#### 响应示例 + +同 [1.6 图片字段历史列表](#16-图片字段历史列表),仅返回已提交记录的数据。 + +--- + +### 2.4 指标字段历史列表 + +- **路由**:`?r=igg4/doctor/medical-visit/indicator-history` +- **请求方式**:GET +- **说明**:医生查看指定患者的某个指标字段历史数据趋势。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| patientId | int | 是 | 患者 ID | +| indicatorField | string | 是 | 指标字段标识 | +| year | int | 否 | 按就诊年份筛选(快捷方式,等同于yearStart=year&yearEnd=year) | +| yearStart | int | 否 | 按就诊年份范围筛选-起始年份,优先级高于year | +| yearEnd | int | 否 | 按就诊年份范围筛选-结束年份,优先级高于year | + +#### indicatorField 枚举 + +同 [1.7 indicatorField 枚举](#indicatorfield-枚举) + +#### 响应示例 + +同 [1.7 指标字段历史列表](#17-指标字段历史列表),仅返回已提交记录的数据。 + +--- + +### 2.5 导出就诊记录PPT + +- **路由**:`?r=igg4/doctor/medical-visit/export-ppt` +- **请求方式**:POST +- **说明**:医生导出指定患者的就诊记录PPT文件,包含5个模块:患者基本信息、主诉与诊断、检验报告指标趋势、就诊记录处方、激素用量。仅包含已提交的记录。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| patientId | int | 是 | 患者 ID | + +#### 响应 + +直接返回PPT文件流(Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation),浏览器触发下载。文件名格式:`就诊记录_{patientId}_{timestamp}.pptx`。 + +#### PPT内容结构 + +| 页码 | 模块 | 说明 | +|------|------|------| +| 1 | 01. 患者基本信息 | 姓名(脱敏)、性别、年龄段、首次发病时间、首次发病部位、是否确诊IgG4 | +| 2+ | 02. 就诊记录病历:主诉与诊断 | 每页5条,展示就诊时间/主诉/病史/诊断/处理意见 | +| N+ | 03. 检验报告(实验室核心血清学指标趋势追踪) | 每页10条,表格展示6项指标 | +| N+ | 04. 就诊记录处方 | 每页3条,展示处方信息 | +| N+ | 05. 激素用量 | 每页10条,表格展示日期+用量 | + +#### 数据脱敏规则 + +- 患者姓名:2字显示"张*",3字及以上显示"张*丰"(保留首尾字符) +- 年龄:转换为年龄段(18-44岁/45-59岁/60-74岁/75岁以上) + +#### 业务逻辑说明 + +1. 校验 patientId 必传 +2. 查询患者基本信息(TZdPatientInfo + 健康问卷最新答案) +3. 查询已提交就诊记录列表(RecordStatus=2, Status=1) +4. 查询用药记录(TZdPatientMedicationRecord) +5. 生成PPT临时文件并返回文件流 +6. 返回后自动删除临时文件 + +--- + +## 三、后台管理员接口 + +模块路由前缀:`medical-visit-record` + +后台管理员负责对小程序用户上传的图片进行涂抹处理,涂抹后上传涂抹图片并同步触发 OCR 解析。所有 POST 接口使用数据库事务。 + +--- + +### 3.1 就诊记录列表 + +- **路由**:`?r=medical-visit-record/list` +- **请求方式**:GET +- **说明**:后台管理就诊记录列表,以图片为主表(一张图一条数据),支持按提交人姓名和图片类型筛选,仅展示已提交的记录。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| submitterName | string | 否 | 提交人姓名(患者姓名模糊搜索) | +| imageType | string | 否 | 图片类型标识,不传则返回全部,见枚举表 | +| page | int | 否 | 页码,默认 1 | +| pageSize | int | 否 | 每页条数,默认 10 | + +#### imageType 枚举 + +同 [1.1 images.imageType](#imagetype-枚举) + +#### 响应示例 + +```json +{ + "code": 0, + "message": "success", + "data": { + "page": 1, + "pages": 5, + "totalCount": 50, + "list": [ + { + "id": 102, + "submitterName": "张三", + "recordTime": "2026-06-29 14:20:00", + "visitDate": "2026-06-29", + "nextVisitDate": "2026-09-29", + "imageType": "labReport", + "imageUrl": "https://oss.example.com/img001.jpg", + "maskedImageUrl": "https://oss.example.com/img001_masked.jpg", + "igG4Value": 135.0, + "manualIgG4Value": 135.0, + "displayIgG4Value": 135.0, + "chiefComplaint": null, + "medicalHistory": null, + "diagnosis": null, + "treatmentPlan": null, + "medicalRecordTime": null, + "prescriptionTime": null, + "reportTime": "2026-06-28 14:20:00", + "prescription": null, + "clinicalDiagnosis": null, + "oralHormoneDosage": 4.0, + "medicationsUsed": [2, 3], + "immunosuppressantName": [1, 7], + "medicationsUsedOtherText": null, + "immunosuppressantNameOtherText": null + }, + { + "id": 103, + "submitTime": "2026-06-29 10:30:00", + "submitterName": "张三", + "recordTime": "2026-06-29 15:00:00", + "visitDate": "2026-06-29", + "nextVisitDate": "2026-09-29", + "imageType": "outpatientRecord", + "imageUrl": "https://oss.example.com/img002.jpg", + "maskedImageUrl": "https://oss.example.com/img002_masked.jpg", + "igG4Value": null, + "manualIgG4Value": 135.0, + "displayIgG4Value": 135.0, + "chiefComplaint": "反复腹痛3月", + "medicalHistory": "患者3月前无明显诱因出现腹痛", + "diagnosis": "IgG4相关性疾病", + "treatmentPlan": "激素治疗,定期复查", + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "reportTime": null, + "prescription": "甲泼尼龙片 4mg 每日1次 口服 30片", + "clinicalDiagnosis": "IgG4相关性疾病", + "oralHormoneDosage": 4.0, + "medicationsUsed": [2, 3], + "immunosuppressantName": [1, 7], + "medicationsUsedOtherText": null, + "immunosuppressantNameOtherText": null + } + ] + } +} +``` + +#### 字段说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 图片记录 ID | +| submitterName | string | 提交人姓名(患者姓名) | +| recordTime | string | 记录时间(图片创建时间) | +| visitDate | string | 就诊时间 | +| nextVisitDate | string | 复诊时间 | +| imageType | string | 图片类型标识(camelCase) | +| imageUrl | string | 患者提交的图片 URL | +| maskedImageUrl | string | 后台处理后的图片 URL,未涂抹时为 null | +| igG4Value | float | IgG4 数值(mg/dL),仅检验报告类型有值 | +| manualIgG4Value | float | 患者手动录入的 IgG4 数值(冗余字段,mg/dL),未录入时为 null | +| displayIgG4Value | float | 展示用 IgG4 数值,优先取 manualIgG4Value,为空则取 igG4Value;均为空时为 null | +| chiefComplaint | string | 主诉内容,仅门诊病历类型有值 | +| medicalHistory | string | 现病史/既往史/病史,仅门诊病历类型有值 | +| diagnosis | string | 诊断,仅门诊病历类型有值 | +| treatmentPlan | string | 处理意见/医嘱/治疗方案,仅门诊病历类型有值 | +| medicalRecordTime | string | 病历时间(图片类型=门诊病历时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss | +| prescriptionTime | string | 处方时间(图片类型=门诊病历时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss | +| reportTime | string | 检查报告时间(图片类型=检验报告时有值),格式 YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss | +| prescription | string | 处方内容,仅门诊病历类型有值 | +| clinicalDiagnosis | string | 临床诊断,仅处方类型图片有值;未解析到时为 null | +| oralHormoneDosage | float | 激素用量(片/天) | +| medicationsUsed | array | 本次就诊已使用的药物(多选),值为常量ID数组,见附录G;未选择时为 null | +| immunosuppressantName | array | 正在使用的免疫抑制剂名称(多选),值为常量ID数组,见附录G;未选择时为 null | +| medicationsUsedOtherText | string | 本次就诊已使用药物选择"其他"(99)时的补充文本,未选择"其他"时为 null | +| immunosuppressantNameOtherText | string | 正在使用的免疫抑制剂名称选择"其他"(99)时的补充文本,未选择"其他"时为 null | + +#### 业务逻辑说明 + +1. 以图片表(t_iggfour_medical_visit_record_image)为主表,一张图一条数据 +2. JOIN 就诊记录表获取 VisitDate、NextVisitDate、OralHormoneDosage,且约束 RecordStatus=2(已提交) +3. JOIN 患者表获取患者姓名(submitterName) +4. 提交人姓名筛选:患者姓名模糊匹配(LIKE) +5. 图片类型筛选:imageType 参数传入 camelCase 标识,内部转为整型后 WHERE 匹配 +6. 仅展示已提交记录(RecordStatus=2),草稿不在列表中显示 +7. 按图片创建时间(CreateTime)降序排列 + +--- + +### 3.2 上传涂抹图片并解析 + +- **路由**:`?r=medical-visit-record/upload-masked-image` +- **请求方式**:POST +- **说明**:管理员上传涂抹后的图片 URL,保存后同步触发 OCR 解析。涂抹后立即执行 OCR,无需异步任务。使用数据库事务。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| imageId | int | 是 | 图片记录 ID | +| maskedImageUrl | string | 是 | 涂抹后图片 URL | + +#### 请求示例 + +```json +{ + "imageId": 102, + "maskedImageUrl": "https://oss.example.com/img001_masked.jpg" +} +``` + +#### 响应示例 — 检验报告类型(imageType=labReport) + +```json +{ + "code": 0, + "message": "success", + "data": { + "imageId": 102, + "maskedImageUrl": "https://oss.example.com/img001_masked.jpg", + "ocrStatus": 2, + "ocrText": "IgG4 1.35 g/L\n总IgG 12.0 g/L\nIgE 50 IU/mL\nESR 20 mm/h\nCRP 5 mg/L", + "ocrParsedData": { + "reportTime": "2026-06-28 14:20:00", + "indicators": { + "igG4": {"value": 135.0, "unit": "mg/dL"}, + "totalIgG": {"value": 1200.0, "unit": "mg/dL"}, + "igE": {"value": 50.0, "unit": "IU/mL"}, + "eos": {"value": null, "unit": "10^9/L"}, + "esr": {"value": 20.0, "unit": "mm/h"}, + "crp": {"value": 5.0, "unit": "mg/L"} + } + }, + "igG4Value": 322, + "totalIgGValue": 1370, + "igEValue": null, + "eosCount": null, + "esrValue": null, + "crpValue": null, + "chiefComplaint": null, + "medicalHistory": null, + "diagnosis": null, + "treatmentPlan": null, + "prescription": null, + "clinicalDiagnosis": null, + "medicalRecordTime": null, + "prescriptionTime": null, + "reportTime": "2026-06-28 14:20:00" + } +} +``` + +#### 响应示例 — 门诊病历类型(imageType=outpatientRecord) + +```json +{ + "code": 0, + "message": "success", + "data": { + "imageId": 103, + "maskedImageUrl": "https://oss.example.com/img002_masked.jpg", + "ocrStatus": 2, + "ocrText": "患者XXX,男,45岁\n主诉:反复腹痛3月\n诊断:IgG4相关性疾病\n处理意见:激素治疗,定期复查", + "ocrParsedData": { + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "chiefComplaint": "反复腹痛3月", + "medicalHistory": "患者3月前无明显诱因出现腹痛", + "diagnosis": "IgG4相关性疾病", + "treatmentPlan": "激素治疗,定期复查", + "prescription": "甲泼尼龙片 4mg 每日1次 口服 30片", + "clinicalDiagnosis": "IgG4相关性疾病" + }, + "igG4Value": null, + "totalIgGValue": null, + "igEValue": null, + "eosCount": null, + "esrValue": null, + "crpValue": null, + "chiefComplaint": "反复腹痛3月", + "medicalHistory": "患者3月前无明显诱因出现腹痛", + "diagnosis": "IgG4相关性疾病", + "treatmentPlan": "激素治疗,定期复查", + "prescription": "甲泼尼龙片 4mg 每日1次 口服 30片", + "clinicalDiagnosis": "IgG4相关性疾病", + "medicalRecordTime": "2026-06-29 09:30:00", + "prescriptionTime": "2026-06-29 10:15:00", + "reportTime": null + } +} +``` + +#### 响应示例 — OCR 解析失败 + +```json +{ + "code": 0, + "message": "success", + "data": { + "imageId": 104, + "maskedImageUrl": "https://oss.example.com/img003_masked.jpg", + "ocrStatus": 3, + "ocrText": null, + "ocrParsedData": null, + "igG4Value": null, + "totalIgGValue": null, + "igEValue": null, + "eosCount": null, + "esrValue": null, + "crpValue": null, + "chiefComplaint": null, + "medicalHistory": null, + "diagnosis": null, + "treatmentPlan": null, + "prescription": null, + "clinicalDiagnosis": null, + "medicalRecordTime": null, + "prescriptionTime": null, + "reportTime": null + } +} +``` + +#### 业务逻辑说明 + +1. 校验 imageId 对应的图片记录存在,否则报错"图片记录不存在" +2. 校验管理员已登录,获取当前操作人 workerId +3. **事务1**:保存 MaskedImageUrl,设置 OcrStatus=1(解析中),清空旧的 OcrText/OcrParsedData/指标字段/时间字段(MedicalRecordTime/PrescriptionTime/ReportTime),记录 UpdateWorkerId/UpdateTime +4. 事务1提交后,同步调用 OCR 服务:`LabReportOcrService::recognizeByType($maskedImageUrl, $imageTypeKey)` +5. 根据 imageType 类型处理 OCR 结果: + - 检验报告(labReport):解析指标值写入图片记录的独立指标字段(IgG4Value/TotalIgGValue 等),时间字段写入 ReportTime + - 门诊病历(outpatientRecord):解析文本写入图片记录的病历文本字段(ChiefComplaint/MedicalHistory 等),时间字段写入 MedicalRecordTime/PrescriptionTime + - 其他类型:原始文本写入 OcrText,OcrParsedData 存原文 +6. **事务2**:写入 OCR 解析结果,设置 OcrStatus=2(解析完成),记录 UpdateWorkerId/UpdateTime +7. 若 OCR 调用抛异常,**事务2回退**:设置 OcrStatus=3(解析失败),记录 UpdateWorkerId/UpdateTime +8. 返回包含 maskedImageUrl、ocrStatus、ocrText、ocrParsedData、指标字段 的结果数据 +9. igG4Value - IgG4 指标值(单位:mg/dL) +10. totalIgGValue - 总 IgG 指标值(单位:mg/dL) +11. igEValue - IgE 指标值(单位:IU/mL) +12. eosCount - eos 指标值(单位:10^9/L) +13. esrValue - ESR 指标值(单位:mm/h) +14. crpValue - CRP 指标值(单位:mg/L) +15. chiefComplaint - 主诉 +16. medicalHistory - 病史 +17. diagnosis - 诊断 +18. treatmentPlan - 处理意见 +19. prescription - 处方 +20. medicalRecordTime - 病历时间(门诊病历图片识别为门诊病历时,从OCR解析获取) +21. prescriptionTime - 处方时间(门诊病历图片识别为处方时,从OCR解析获取) +22. reportTime - 检查报告时间(检验报告图片,从OCR解析获取) +23. ocrParsedData 结构说明:检验报告返回 `{reportTime, indicators: {...}}`,门诊病历返回 `{medicalRecordTime, prescriptionTime, chiefComplaint, ...}` + + + +--- + +#### ocrStatus 枚举 + +| 值 | 说明 | +|------|------| +| 0 | 待解析(小程序用户已上传原图,后台尚未涂抹) | +| 1 | 解析中(后台已上传涂抹图片,OCR 正在执行) | +| 2 | 解析完成 | +| 3 | 解析失败 | + +--- + +## 附录 + +### A. 记录状态枚举 + +| 值 | 常量名 | 说明 | +|------|------|------| +| 1 | RECORD_STATUS_DRAFT | 草稿 | +| 2 | RECORD_STATUS_SUBMITTED | 已提交 | + +### B. 数据状态枚举 + +| 值 | 常量名 | 说明 | +|------|------|------| +| 1 | STATUS_ACTIVE | 有效 | +| 100 | STATUS_DELETED | 已删除 | + +### C. 口服激素用量预设选项(片/天) + +| 值 | 说明 | +|------|------| +| 1 | 1片/天 | +| 2 | 2片/天 | +| 4 | 4片/天 | +| 6 | 6片/天 | +| 8 | 8片/天 | + +### D. 检验指标单位换算 + +| 指标 | 原始单位 | 目标单位 | 换算公式 | +|------|---------|---------|---------| +| IgG4 | g/L | mg/dL | 值 × 100 | +| 总IgG | g/L | mg/dL | 值 × 100 | +| IgE | — | IU/mL | 不换算 | +| EOS# | — | 10^9/L | 不换算 | +| ESR | — | mm/h | 不换算 | +| CRP | — | mg/L | 不换算 | + +### E. 数据库表结构 + +#### t_iggfour_medical_visit_record 就诊记录主表 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| Id | int AUTO_INCREMENT PK | 主键 | +| UserId | varchar(40) NOT NULL | 用户 ID | +| PatientId | int NOT NULL | 患者 ID | +| VisitDate | date NOT NULL | 就诊日期 | +| NextVisitDate | date DEFAULT NULL | 下次复诊日期 | +| IgG4Value | decimal(10,2) DEFAULT NULL | IgG4 数值(OCR 解析) | +| ManualIgG4Value | decimal(10,2) DEFAULT NULL | IgG4 数值(患者手动录入,冗余字段) | +| TotalIgGValue | decimal(10,2) DEFAULT NULL | 总 IgG 数值 | +| IgEValue | decimal(10,2) DEFAULT NULL | IgE 数值 | +| EosCount | decimal(10,2) DEFAULT NULL | EOS# 计数 | +| ESRValue | decimal(10,2) DEFAULT NULL | ESR 数值 | +| CRPValue | decimal(10,2) DEFAULT NULL | CRP 数值 | +| OralHormoneDosage | decimal(10,2) DEFAULT NULL | 口服激素每日用量(片) | +| MedicationsUsed | varchar(255) DEFAULT NULL | 本次就诊已使用的药物(JSON数组,多选),见附录G | +| ImmunosuppressantName | varchar(255) DEFAULT NULL | 正在使用的免疫抑制剂名称(JSON数组,多选),见附录G | +| MedicationsUsedOtherText | varchar(500) DEFAULT NULL | 本次就诊已使用药物-其他-补充文本 | +| ImmunosuppressantNameOtherText | varchar(500) DEFAULT NULL | 正在使用的免疫抑制剂名称-其他-补充文本 | +| MedicalRecordTime | varchar(30) DEFAULT NULL | 门诊病历时间 | +| PrescriptionTime | varchar(30) DEFAULT NULL | 处方时间 | +| ReportTime | varchar(30) DEFAULT NULL | 检查报告时间 | +| ChiefComplaint | text DEFAULT NULL | 主诉 | +| MedicalHistory | text DEFAULT NULL | 病史 | +| Diagnosis | text DEFAULT NULL | 诊断 | +| TreatmentPlan | text DEFAULT NULL | 处理意见 | +| Prescription | text DEFAULT NULL | 处方内容 | +| ClinicalDiagnosis | varchar(500) DEFAULT NULL | 临床诊断(从处方图片解析) | +| RecordStatus | tinyint NOT NULL DEFAULT 1 | 记录状态:1-草稿,2-已提交 | +| LastRecordId | int DEFAULT NULL | 上一条记录 ID | +| Status | tinyint NOT NULL DEFAULT 1 | 数据状态:1-有效,100-删除 | +| CreateWorkerId | int NOT NULL DEFAULT 0 | 创建人 | +| CreateTime | datetime NOT NULL | 创建时间 | +| UpdateWorkerId | int NOT NULL DEFAULT 0 | 更新人 | +| UpdateTime | datetime NOT NULL | 更新时间 | + +#### t_iggfour_medical_visit_record_image 就诊记录图片表 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| Id | int AUTO_INCREMENT PK | 主键 | +| RecordId | int NOT NULL | 关联就诊记录 ID | +| ImageType | tinyint NOT NULL | 图片类型:1-门诊病历,2-检验报告,3-影像检查,4-其他 | +| ImageUrl | varchar(500) NOT NULL | 原始图片 URL | +| MaskedImageUrl | varchar(500) DEFAULT NULL | 涂抹后图片 URL | +| OcrStatus | tinyint NOT NULL DEFAULT 0 | OCR 状态:0-未解析,1-解析中,2-解析完成,3-解析失败 | +| OcrText | text DEFAULT NULL | OCR 原始文本 | +| OcrParsedData | text DEFAULT NULL | OCR 解析结果(JSON) | +| IgG4Value | decimal(10,2) DEFAULT NULL | IgG4 数值 | +| TotalIgGValue | decimal(10,2) DEFAULT NULL | 总 IgG 数值 | +| IgEValue | decimal(10,2) DEFAULT NULL | IgE 数值 | +| EosCount | decimal(10,2) DEFAULT NULL | EOS# 计数 | +| ESRValue | decimal(10,2) DEFAULT NULL | ESR 数值 | +| CRPValue | decimal(10,2) DEFAULT NULL | CRP 数值 | +| ChiefComplaint | text DEFAULT NULL | 主诉(从门诊病历图片解析) | +| MedicalHistory | text DEFAULT NULL | 病史(从门诊病历图片解析) | +| Diagnosis | text DEFAULT NULL | 诊断(从门诊病历图片解析) | +| TreatmentPlan | text DEFAULT NULL | 处理意见(从门诊病历图片解析) | +| Prescription | text DEFAULT NULL | 从本图OCR解析的处方内容,仅图片类型=1时有值 | +| ClinicalDiagnosis | varchar(500) DEFAULT NULL | 临床诊断(从处方图片解析) | +| MedicalRecordTime | varchar(30) DEFAULT NULL | 门诊病历时间 | +| PrescriptionTime | varchar(30) DEFAULT NULL | 处方时间 | +| ReportTime | varchar(30) DEFAULT NULL | 检查报告时间 | +| SortOrder | int NOT NULL DEFAULT 0 | 排序序号 | +| Status | tinyint NOT NULL DEFAULT 1 | 数据状态:1-有效,100-删除 | +| CreateWorkerId | int NOT NULL DEFAULT 0 | 创建人 | +| CreateTime | datetime NOT NULL | 创建时间 | +| UpdateWorkerId | int NOT NULL DEFAULT 0 | 更新人 | +| UpdateTime | datetime NOT NULL | 更新时间 | + +#### t_medical_visit_reminder_config 复诊提醒配置表 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| Id | int AUTO_INCREMENT PK | 主键 | +| PatientId | int NOT NULL | 患者 ID(唯一索引) | +| ReminderEnabled | tinyint NOT NULL DEFAULT 2 | 是否开启复诊提醒:1-开启,2-关闭 | +| Status | tinyint NOT NULL DEFAULT 1 | 数据状态:1-有效,100-删除 | +| CreateWorkerId | int DEFAULT NULL | 创建人 | +| CreateTime | datetime DEFAULT NULL | 创建时间 | +| UpdateWorkerId | int DEFAULT NULL | 更新人 | +| UpdateTime | datetime DEFAULT NULL | 更新时间 | + + +### G. 药物选项枚举 + +#### MedicationsUsed 本次就诊已使用的药物(多选) + +| 值 | 常量名 | 说明 | +|------|------|------| +| 1 | MED_USED_NO_HORMONE | 没有使用激素 | +| 2 | MED_USED_HORMONE | 激素 | +| 3 | MED_USED_IMMUNOSUPPRESSANT | 免疫抑制剂 | +| 4 | MED_USED_CD19 | 靶向CD19生物制剂(伊奈利珠单抗) | +| 5 | MED_USED_CD20 | 靶向CD20生物制剂(利妥昔单抗等) | +| 99 | MED_USED_OTHER | 其他 | + +#### ImmunosuppressantName 正在使用的免疫抑制剂名称(多选) + +| 值 | 常量名 | 说明 | +|------|------|------| +| 1 | IMMUNOSUPPRESSANT_MMF | 吗替麦考酚酯(骁悉、麦考芬) | +| 2 | IMMUNOSUPPRESSANT_AZA | 替唑嘌呤 | +| 3 | IMMUNOSUPPRESSANT_CYC | 环磷酰胺 | +| 4 | IMMUNOSUPPRESSANT_LEF | 来氟米特 | +| 5 | IMMUNOSUPPRESSANT_MTX | 甲氨蝶呤 | +| 6 | IMMUNOSUPPRESSANT_CYC_A | 环孢素 A | +| 7 | IMMUNOSUPPRESSANT_TAC | 他克莫司 | +| 8 | IMMUNOSUPPRESSANT_IGU | 艾拉莫德 | +| 99 | IMMUNOSUPPRESSANT_OTHER | 其他 | + +#### 复诊提醒配置枚举 + +| 值 | 常量名 | 说明 | +|------|------|------| +| 1 | REMINDER_ENABLED | 开启复诊提醒 | +| 2 | REMINDER_DISABLED | 关闭复诊提醒 | +| 7 | REMINDER_DAYS_BEFORE | 提前提醒天数(天) | diff --git a/doc/api_doctor_msg_task.md b/doc/api_doctor_msg_task.md new file mode 100644 index 0000000..607cff9 --- /dev/null +++ b/doc/api_doctor_msg_task.md @@ -0,0 +1,572 @@ +# 医生消息任务接口文档 + +## 通用说明 + +### 响应格式 + +所有接口返回 JSON,严格遵循以下格式: + +```json +{ + "code": 0, + "message": "success", + "data": {} +} +``` + +- `code`:0 表示成功,非 0 表示失败 +- `message`:成功时为 "success",失败时为错误描述 +- `data`:业务数据,失败时为 null + +### 数据命名规则 + +`data` 中所有字段使用**小驼峰**命名法(如 `patientNum`、`sendTime`)。 + +### 登录校验 + +所有接口需通过请求头 `loginState` 或请求参数 `loginState` 传递登录态,未登录返回 code=403。本模块所有接口需医生身份登录。 + +### 错误码 + +| code | 说明 | +|------|------| +| 0 | 成功 | +| 1 | 参数缺失或业务异常 | +| 403 | 需要登录 | + +--- + +## 一、筛选患者人数 + +- **路由**:`?r=igg4/doctor/task/filter-patient-num` +- **请求方式**:GET +- **说明**:根据筛选条件获取符合条件的患者人数 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| SelectAll | int | 否 | 全选:0-否,1-是;为1时跳过其余筛选条件 | +| BindStartDate | string | 否 | 绑定日期开始,格式 YYYY-MM-DD | +| BindEndDate | string | 否 | 绑定日期结束,格式 YYYY-MM-DD | +| ConfirmedIgg4 | int | 否 | 确诊IGG4-RD:0-全部,1-是,2-否 | +| MedicationType | string | 否 | 正在使用的药物,逗号分隔选项值(如"2,3"),空或0表示全部。可选值见下面枚举表 | + +#### ConfirmedIgg4 枚举 + +| 值 | 说明 | +|----|------| +| 0 | 全部 | +| 1 | 是(确诊患者) | +| 2 | 否(非确诊患者,含未填写和疑似) | + +#### MedicationType 枚举 + +| 值 | 说明 | +|----|------| +| 0 | 全部 | +| 2 | 激素 | +| 3 | 免疫抑制剂 | +| 4 | 靶向CD19生物制剂(伊奈利珠单抗) | +| 5 | 靶向CD20生物制剂(利妥昔单抗等) | + +### 请求示例 + +``` +?r=igg4/doctor/task/filter-patient-num&SelectAll=0&BindStartDate=2026-01-01&BindEndDate=2026-06-30&ConfirmedIgg4=1&MedicationType=2,3 +``` + +### 响应示例 + +```json +{ + "code": 0, + "data": { + "PatientNum": "1" + } +} +``` + +### 业务逻辑说明 + +1. 基础条件:患者状态有效、属于当前医生、有绑定用户 +2. SelectAll=1 时跳过所有筛选直接返回 +3. 绑定时间范围:按 `InviteTime` 字段筛选,使用 BETWEEN 逻辑 +4. 确诊IGG4-RD:选"是"时匹配 `IsSuspectedPatient=2`,选"否"时排除 `IsSuspectedPatient=2`(保留未填写和疑似患者) +5. 药物筛选:关联 `t_patient_health_questionnaire_latest` 表(QuestionNo=4),使用 `FIND_IN_SET` 匹配多选答案,多个选项值之间为 OR 关系 + +--- + +## 二、获取任务模板 + +- **路由**:`?r=igg4/doctor/task/get-template` +- **请求方式**:GET +- **说明**:获取所有消息推送模板,按模板类型分组 + +### 请求参数 + +无 + +### 响应示例 + +```json +{ + "code": 0, + "data": [ + { + "templateType": 1, + "templateTypeText": "复诊提醒", + "templateList": [ + { + "id": 1, + "templateType": 1, + "interactMsgContent": "互动消息内容", + "templateIndex": "模板index", + "templateContent": "模板内容JSON" + } + ] + } + ] +} +``` + +#### TemplateType 枚举 + +| 值 | 说明 | +|----|------| +| 1 | 复诊提醒 | +| 2 | 激素风险提醒 | + +### 业务逻辑说明 + +1. 查询所有模板记录,按 `TemplateType` 升序、`Id` 升序排列 +2. 按 `TemplateType` 分组返回,每组包含类型编号、类型文本和模板列表 + +--- + +## 三、创建任务 + +- **路由**:`?r=igg4/doctor/task/create` +- **请求方式**:POST +- **说明**:创建消息推送任务,使用数据库事务 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| FilterCondition | object | 否 | 筛选条件JSON对象,结构同"筛选患者人数"接口参数 | +| PushTemplateId | int | 是 | 推送模板ID | +| IsSendNow | int | 是 | 是否立即发送:0-否,1-是 | +| SendTime | string | 条件必填 | 发送时间(IsSendNow=0时必填),格式 YYYY-MM-DD HH:mm:ss | +| PatientNum | int | 是 | 患者人数 | + +#### FilterCondition 结构 + +| 字段 | 类型 | 说明 | +|------|------|------| +| SelectAll | int | 全选:0-否,1-是 | +| BindStartDate | string | 绑定日期开始 | +| BindEndDate | string | 绑定日期结束 | +| ConfirmedIgg4 | int | 确诊IGG4-RD:0-全部,1-是,2-否 | +| MedicationType | string | 正在使用的药物,逗号分隔 | + +### 请求示例 + +```json +{ + "FilterCondition": { + "SelectAll": 0, + "BindStartDate": "2026-01-01", + "BindEndDate": "2026-06-30", + "ConfirmedIgg4": 1, + "MedicationType": "2" + }, + "PushTemplateId": 2, + "IsSendNow": 1, + "SendTime": "", + "PatientNum": 25 +} +``` + +### 响应示例(成功) + +```json +{ + "code": 0, + "data": "创建成功" +} +``` + +### 响应示例(失败) + +```json +{ + "code": 1, + "data": "每个医生每天只能创建一次任务" +} +``` + +### 业务逻辑说明 + +1. 校验医生存在(逻辑删除标记 `DeleteTime` 为空) +2. 校验模板存在 +3. 每个医生每天只能创建一次任务(当天已有状态为待发送/发送中/暂停/已发送/已停止的任务则不可再创建) +4. IsSendNow=1 时,SendTime 自动设为当前时间 +5. 自动生成 IdNumber(`YmdHis` + 3位随机数) +6. IsDoctorCreate=1(医生端创建),CreateWorkerId 为空 +7. 生成模板消息内容(调用 `transformTemplateContent` 转化模板变量) +8. 保存任务后,自动调用 `generateUserRecord` 生成用户发送记录 +9. 更新任务的 ActualPatientNum 和 IsInsertUser 字段 + +--- + +## 四、获取任务列表 + +- **路由**:`?r=igg4/doctor/task/list` +- **请求方式**:GET +- **说明**:获取当前医生的任务列表,支持分页,按创建时间降序排列 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| count | int | 否 | 每页条数,默认10 | +| page | int | 否 | 页码,从1开始 | + +### 请求示例 + +``` +?r=igg4/doctor/task/list&count=10&page=1 +``` + +### 响应示例 + +```json +{ + "code": 0, + "data": { + "list": [ + { + "Id": 1, + "IdNumber": "20260702143025123", + "DoctorId": 5, + "FilterCondition": { + "SelectAll": 0, + "BindStartDate": "2026-01-01", + "BindEndDate": "2026-06-30", + "ConfirmedIgg4": 1, + "MedicationType": "2" + }, + "TemplateType": 1, + "CreateTime": "2026-07-02 14:30:25", + "IsSendNow": 1, + "SendTime": "2026-07-02 14:30:25", + "Status": 4, + "PatientNum": 25, + "ActualPatientNum": 23 + } + ], + "pages": 3, + "count": 28, + "page": 1 + } +} +``` + +### 响应字段说明 +| 字段 | 类型 | 说明 | +|------|------|------| +| Id | int | 任务ID | +| IdNumber | string | 任务编号 | +| DoctorId | int | 医生ID | +| FilterCondition | object | 筛选条件JSON对象 | +| TemplateType | int | 模板类型 | +| CreateTime | string | 创建时间 | +| IsSendNow | int | 是否立即发送:0-否,1-是 | +| SendTime | string | 发送时间 | +| Status | int | 任务状态 | +| PatientNum | int | 患者人数 | +| ActualPatientNum | int | 实际发送患者人数 | + +#### FilterCondition 结构说明 + +| 字段 | 类型 | 说明 | +|------|------|------| +| SelectAll | int | 全选:0-否,1-是 | +| BindStartDate | string | 绑定日期开始 | +| BindEndDate | string | 绑定日期结束 | +| ConfirmedIgg4 | int | 确诊IGG4-RD:0-全部,1-是,2-否 | +| MedicationType | string | 正在使用的药物,逗号分隔 | + +#### TemplateType 枚举 + +| 值 | 说明 | +|----|------| +| 1 | 复诊提醒 | +| 2 | 激素风险提醒 | + +#### Status 枚举 + +| 值 | 说明 | +|----|------| +| 1 | 待发送 | +| 2 | 发送中 | +| 3 | 暂停 | +| 4 | 已发送 | +| 5 | 已停止 | +| 6 | 已取消 | + +### 业务逻辑说明 + +1. 仅查询 `DeleteTime` 为空的任务 +2. FilterCondition 字段从 JSON 字符串解码为数组返回 +3. 返回字段包含:Id, IdNumber, DoctorId, FilterCondition, TemplateType, CreateTime, IsSendNow, SendTime, Status, PatientNum, ActualPatientNum +4. 分页参数:pages 为总页数,count 为总记录数,page 为当前页码(从1开始) + +--- + +## 五、获取任务详情 + +- **路由**:`?r=igg4/doctor/task/detail` +- **请求方式**:GET +- **说明**:获取单个任务的详细信息,包含发送统计 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| TaskId | int | 是 | 任务ID | + +### 请求示例 + +``` +?r=igg4/doctor/task/detail&TaskId=1 +``` + +### 响应示例 + +```json +{ + "code": 0, + "data": { + "Id": 1, + "IdNumber": "20260702143025123", + "DoctorId": 5, + "FilterCondition": { + "SelectAll": 0, + "BindStartDate": "2026-01-01", + "BindEndDate": "2026-06-30", + "ConfirmedIgg4": 1, + "MedicationType": "2" + }, + "TemplateType": 1, + "PushTemplateId": 2, + "TemplateMsgContent": "{...}", + "InteractMsgContent": "互动消息内容", + "IsSendNow": 1, + "SendTime": "2026-07-02 14:30:25", + "PatientNum": 25, + "ActualPatientNum": 23, + "IsInsertUser": 1, + "Status": 4, + "IsDoctorCreate": 1, + "WorkerId": "", + "DeleteTime": null, + "CreateTime": "2026-07-02 14:30:25", + "UpdateTime": "2026-07-02 14:30:30", + "SendDetail": { + "TaskId": 1, + "SumSendCount": 23, + "SendCount": 20, + "UnSendCount": 3, + "InteractSendSuccessCount": 18, + "InteractSendFailCount": 2, + "PushSendSuccessCount": 16, + "PushSendFailCount": 4, + "PushClickCount": 10, + "InteractReadCount": 15, + "InteractUnReadCount": 5, + "PushUnClickCount": 10 + } + } +} +``` + +#### SendDetail 字段说明 + +| 字段 | 说明 | +|------|------| +| sumSendCount | 总人数(去重) | +| sendCount | 已发送人数 | +| unSendCount | 未发送人数 | +| InteractSendSuccessCount | 互动消息发送成功人数 | +| InteractSendFailCount | 互动消息发送失败人数(派生值:SendCount - InteractSendSuccessCount) | +| PushSendSuccessCount | Push消息发送成功人数 | +| PushSendFailCount | Push消息发送失败人数 | +| PushClickCount | Push消息点击人数 | +| InteractReadCount | 互动消息已读人数 | +| InteractUnReadCount | 互动消息未读人数(派生值:sendCount - InteractReadCount) | +| PushUnClickCount | Push消息未点击人数(派生值:sendCount - PushClickCount) | + +### 业务逻辑说明 + +1. 校验任务存在且 `DeleteTime` 为空,否则抛出"任务不存在" +2. FilterCondition 从 JSON 字符串解码为数组 +3. 根据 PushTemplateId 查询模板,补充 TemplateType 和 InteractMsgContent 字段 +4. 通过 `TZdDoctorMsgTaskUserMessage::taskDetail` 获取发送统计数据,包含派生的失败/未读/未点击人数 + +--- + +## 六、删除任务 + +- **路由**:`?r=igg4/doctor/task/delete` +- **请求方式**:POST +- **说明**:逻辑删除已取消的任务,使用数据库事务 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| TaskId | int | 是 | 任务ID | + +### 请求示例 + +```json +{ + "TaskId": 1 +} +``` + +### 响应示例(成功) + +```json +{ + "code": 0, + "data": "删除成功" +} +``` + +### 响应示例(失败) + +```json +{ + "code": 1, + "msg": "当前任务状态不能删除", +} +``` + +### 业务逻辑说明 + +1. 校验任务存在、属于当前医生且 `DeleteTime` 为空,否则抛出"任务不存在" +2. 只有状态为"已取消"(Status=6)的任务才能删除 +3. 设置 `DeleteTime` 为当前时间(软删除) + +--- + +## 七、停止任务 + +- **路由**:`?r=igg4/doctor/task/stop` +- **请求方式**:POST +- **说明**:停止正在发送中或暂停的任务,使用数据库事务 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| TaskId | int | 是 | 任务ID | + +### 请求示例 + +```json +{ + "TaskId": 1 +} +``` + +### 响应示例(成功) + +```json +{ + "code": 0, + "data": "停止成功" +} +``` + +### 响应示例(失败) + +```json +{ + "code": 1, + "msg": "当前任务状态不能停止", +} +``` + +### 业务逻辑说明 + +1. 校验任务存在、属于当前医生且 `DeleteTime` 为空,否则抛出"任务不存在" +2. 只有状态为"发送中"(Status=2)或"暂停"(Status=3)的任务才能停止 +3. 将状态更新为"已停止"(Status=5) + +--- + +## 八、取消任务 + +- **路由**:`?r=igg4/doctor/task/cancel` +- **请求方式**:POST +- **说明**:取消待发送的任务,使用数据库事务 + +### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| TaskId | int | 是 | 任务ID | + +### 请求示例 + +```json +{ + "TaskId": 1 +} +``` + +### 响应示例(成功) + +```json +{ + "code": 0, + "data": "取消成功" +} +``` + +### 响应示例(失败) + +```json +{ + "code": 1, + "msg": "当前任务状态不能取消", +} +``` + +### 业务逻辑说明 + +1. 校验任务存在、属于当前医生且 `DeleteTime` 为空,否则抛出"任务不存在" +2. 只有状态为"待发送"(Status=1)的任务才能取消 +3. 将状态更新为"已取消"(Status=6) + +--- + +## 附录 + +### 附录A:任务状态流转 + +``` +待发送(1) ──发送调度──> 发送中(2) ──发送完成──> 已发送(4) + │ │ ↑ + │取消 │ │暂停 + ↓ ↓ │ +已取消(6) <────────── 暂停(3)─┘ + │ │ + │删除(软删) │停止 + ↓ ↓ +(DeleteTime非空) 已停止(5) +``` diff --git a/doc/track.md b/doc/track.md new file mode 100644 index 0000000..ecb44aa --- /dev/null +++ b/doc/track.md @@ -0,0 +1,216 @@ +# 埋点文档 + +## 命名规范 + +| 类别 | 前缀 | 格式 | 示例 | +|------|------|------|------| +| 页面浏览(患者端) | `PG_` | `PG_Patient{功能名}` | `PG_PatientHome` | +| 页面浏览(医生端) | `PG_` | `PG_Doctor{功能名}` | `PG_DoctorHome` | +| 点击事件(患者端) | `BTN_` | `BTN_Patient{功能名}{操作}` | `BTN_PatientHomeNextVisit` | +| 点击事件(医生端) | `BTN_` | `BTN_Doctor{功能名}{操作}` | `BTN_DoctorHomeInvite` | + +## 埋点示例 + +### 患者端页面访问埋点 + +```ts +// 页面级 PV +app.waitLogin().then(() => { + app.mpBehavior({ PageName: 'PG_PatientHome' }) +}) + +// 详情页带 detailId +app.waitLogin().then(() => { + app.mpBehavior({ PageName: 'PG_PatientDoctorDetail', detailId: options.id }) +}) +``` + +### 医生端页面访问埋点 + +```ts +app.waitLogin().then(() => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorHome' }) +}) +``` + +### 患者端点击埋点 + +```ts +handleNextVisit() { + app.mpBehavior({ PageName: 'BTN_PatientHomeNextVisit' }) + // ... 业务逻辑 +} +``` + +### 医生端点击埋点 + +```ts +handleInvite() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorHomeInvite' }) + // ... 业务逻辑 +} +``` + +### 全局访问埋点 + +```ts +// app.ts onShow 中,记录小程序整体打开事件 +app.mpBehavior({ PageName: 'MINI_PROGRAM_VISIT' }) +``` + +## 页面级埋点(PG_) + +### 患者端 + +| 页面路径 | 页面名称 | PageName | detailId | +|---------|---------|----------|----------| +| pages/index/index | 首页 | `PG_PatientHome` | - | +| gift/pages/recordList/index | 就诊记录列表页 | `PG_PatientRecordList` | - | +| gift/pages/record/index | 就诊记录新增页 | `PG_PatientRecordAdd` | - | +| gift/pages/recordDetail/index | 就诊记录详情页 | `PG_PatientRecordDetail` | `options.id` | +| pages/interactivePatient/index | 医患互动页 | `PG_PatientInteractive` | - | +| pages/doctorDetail/index | 医生详情页 | `PG_PatientDoctorDetail` | `options.id` | +| pages/repository/index | 知识库 | `PG_PatientRepository` | - | +| pages/repositoryDetail/index | 知识库详情页 | `PG_PatientRepositoryDetail` | `id` | +| pages/my/index | 我的 | `PG_PatientMy` | - | +| pages/qaFormDetail/index | 健康档案页 | `PG_PatientQaFormDetail` | - | +| pages/qaForm/index | 健康档案编辑页 | `PG_PatientQaForm` | - | +| pages/caseReport/index | 病例报告页 | `PG_PatientCaseReport` | - | +| pages/personalInformation/index | 个人信息页 | `PG_PatientPersonalInfo` | - | +| pages/cancellation/index | 注销账号页 | `PG_PatientCancellation` | - | +| pages/mySave/index | 我的收藏页 | `PG_PatientMySave` | - | +| pages/feedback/index | 问题反馈页 | `PG_PatientFeedback` | - | + +### 医生端 + +| 页面路径 | 页面名称 | PageName | detailId | +|---------|---------|----------|----------| +| doctor/pages/d_home/index | 首页 | `PG_DoctorHome` | - | +| doctor/pages/d_invite/index | 邀约患者页 | `PG_DoctorInvite` | - | +| doctor/pages/d_patient/index | 患者列表页 | `PG_DoctorPatient` | - | +| doctor/pages/d_patientDetail/index | 患者详情页 | `PG_DoctorPatientDetail` | `this.data.id` | +| doctor/pages/d_taskList/index | 群发列表页 | `PG_DoctorTaskList` | - | +| doctor/pages/d_taskDetail/index | 群发详情页 | `PG_DoctorTaskDetail` | `this.data.id` | +| doctor/pages/d_createTask/index | 群发新增页 | `PG_DoctorCreateTask` | - | +| doctor/pages/d_interactive/index | 互动列表页 | `PG_DoctorInteractive` | - | +| doctor/pages/d_interactiveDoctor/index | 互动页 | `PG_DoctorInteractiveDoctor` | `this.data.patientId` | +| doctor/pages/d_my/index | 我的 | `PG_DoctorMy` | - | +| doctor/pages/d_userInfo/index | 个人信息页 | `PG_DoctorUserInfo` | - | +| doctor/pages/d_changeDoctor/index | 切换医生页 | `PG_DoctorChangeDoctor` | - | + +## 点击级埋点(BTN_) + +### 患者端 + +#### 首页(pages/index/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `routerTo(e)` | `BTN_PatientHomeFunctionCard` | 功能卡片跳转 | +| `handleNextVisit()` | `BTN_PatientHomeNextVisit` | 下次复诊入口 | +| `handleArticleView(e)` | `BTN_PatientHomeArticle` | 文章点击 | +| `handleArticleSave(e)` | `BTN_PatientHomeArticleSave` | 文章收藏 | +| `handleToastOk()` | `BTN_PatientHomeBindDoctor` | 绑定医生弹窗确认 | +| `handlePublic()` | `BTN_PatientHomeFollowPublic` | 关注公众号 | + +#### 我的(pages/my/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `routerVipTo()` | `BTN_PatientMyVip` | VIP 认证入口 | +| `handleMyHealthRecord(e)` | `BTN_PatientMyHealthRecord` | 健康档案 | +| `handleQaForm()` | `BTN_PatientMyQaForm` | 健康问卷 | +| `routerTo(e)` → mySave | `BTN_PatientMySave` | 我的收藏 | +| `routerTo(e)` → personalInfo | `BTN_PatientMyPersonalInfo` | 个人信息 | +| `handleMiniDoctor()` | `BTN_PatientMyMiniDoctor` | 迷你医生 | +| `handleFeedback()` | `BTN_PatientMyFeedback` | 问题反馈 | + +#### 医患互动(pages/interactivePatient/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleHot(e)` | `BTN_PatientInteractiveHotQuestion` | 热门问题点击 | +| `handleQuestion(e)` | `BTN_PatientInteractiveQuestion` | 快捷问题点击 | +| `handleConfirm()` | `BTN_PatientInteractiveSend` | 发送消息 | +| `handleAdl()` | `BTN_PatientInteractiveAdl` | ADL 评估入口 | +| `handleQol()` | `BTN_PatientInteractiveQol` | QoL 入口 | +| `handleDoctorDetail()` | `BTN_PatientInteractiveDoctorDetail` | 查看医生详情 | + +#### 知识库(pages/repository/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `tabChange(e)` | `BTN_PatientRepositoryTab` | Tab 切换 | +| `handleSearch()` | `BTN_PatientRepositorySearch` | 搜索 | +| `handleCate(e)` | `BTN_PatientRepositoryCategory` | 分类筛选 | +| `handleDetail(e)` | `BTN_PatientRepositoryArticle` | 文章详情 | +| `handleStar(e)` | `BTN_PatientRepositoryStar` | 点赞 | +| `handleToggleSave(e)` | `BTN_PatientRepositorySave` | 收藏 | + +#### 就诊记录列表(gift/pages/recordList/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleAdd()` | `BTN_PatientRecordListAdd` | 新增记录 | +| `handleDetail(e)` | `BTN_PatientRecordListItem` | 记录列表项 | + +#### 就诊记录新增(gift/pages/record/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleSubmit()` | `BTN_PatientRecordAddSubmit` | 保存记录 | + +### 医生端 + +#### 医生首页(doctor/pages/d_home/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handlePatient(e)` | `BTN_DoctorHomePatient` | 患者列表入口 | +| `handleInvite()` | `BTN_DoctorHomeInvite` | 邀约按钮 | +| `handleSelectYear1()` | `BTN_DoctorHomeSelectYear` | 年份筛选 | + +#### 邀约患者(doctor/pages/d_invite/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleDownload()` | `BTN_DoctorInviteSend` | 发送邀约(海报保存/分享) | + +#### 患者列表(doctor/pages/d_patient/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleSearch()` | `BTN_DoctorPatientSearch` | 搜索患者 | +| `handleDetail(e)` | `BTN_DoctorPatientItem` | 患者列表项 | + +#### 互动列表(doctor/pages/d_interactive/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleSearch()` | `BTN_DoctorInteractiveSearch` | 搜索互动 | +| `handleDetail(e)` | `BTN_DoctorInteractiveItem` | 互动列表项 | + +#### 互动页(doctor/pages/d_interactiveDoctor/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleConfirm()` | `BTN_DoctorInteractiveSend` | 发送消息 | + +#### 群发列表(doctor/pages/d_taskList/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleCreate()` | `BTN_DoctorTaskListAdd` | 新增群发 | +| `handleDetail(e)` | `BTN_DoctorTaskListItem` | 群发列表项 | + +#### 群发新增(doctor/pages/d_createTask/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleSubmit()` | `BTN_DoctorCreateTaskSubmit` | 创建群发 | + +#### 切换医生(doctor/pages/d_changeDoctor/index) + +| 方法 | PageName | 说明 | +|------|----------|------| +| `handleSubmit()` | `BTN_DoctorChangeDoctorSelect` | 切换医生选择 | diff --git a/project.private.config.json b/project.private.config.json index 5e21393..40cb7a4 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -22,11 +22,60 @@ "miniprogram": { "list": [ { + "name": "就诊记录", + "pathName": "gift/pages/record/index", + "query": "id=15", + "scene": null, + "launchMode": "default" + }, + { + "name": "医生-患者详情", + "pathName": "doctor/pages/d_patientDetail/index", + "query": "id=67", + "launchMode": "default", + "scene": null + }, + { + "name": "病例报告", + "pathName": "pages/caseReport/index", + "query": "name=门诊病历及处方", + "launchMode": "default", + "scene": null + }, + { + "name": "就诊记录详情", + "pathName": "gift/pages/recordDetail/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "就诊记录列表页", + "pathName": "gift/pages/recordList/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "医生-新建消息", + "pathName": "doctor/pages/d_createTask/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "医生-任务列表", + "pathName": "doctor/pages/d_taskList/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { "name": "患者-问题反馈", "pathName": "pages/feedback/index", "query": "", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "患者-更改手机号", @@ -134,13 +183,6 @@ "scene": null }, { - "name": "医生-患者详情", - "pathName": "doctor/pages/d_patientDetail/index", - "query": "id=1313", - "launchMode": "default", - "scene": null - }, - { "name": "医生-患者列表", "pathName": "doctor/pages/d_patient/index", "query": "", diff --git a/src/api/request.ts b/src/api/request.ts index aa310a1..fe5db74 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -20,7 +20,7 @@ export const request = function ( ...header, }, url: gUrl + url, - method: method, + method, data: { loginState: getApp().globalData.loginState, ...(data as object), diff --git a/src/app.json b/src/app.json index d80ceaf..1ca41f9 100644 --- a/src/app.json +++ b/src/app.json @@ -47,9 +47,9 @@ "pages/webview/index", "pages/infusionCenter/index", "pages/interactivePatient/index", - "pages/referral/index", "pages/qaForm/index", "pages/qaFormDetail/index", + "pages/caseReport/index", "pages/feedback/index" ], "subpackages": [ @@ -64,7 +64,10 @@ "pages/myHealthRecordChart/index", "pages/cutaneous/index", "pages/cutaneousDetail/index", - "pages/cutaneousVideo/index" + "pages/cutaneousVideo/index", + "pages/record/index", + "pages/recordList/index", + "pages/recordDetail/index" ] }, { @@ -91,7 +94,8 @@ "pages/d_patientList/index", "pages/d_transfer/index", "pages/d_transferLog/index", - "pages/d_transferDetail/index" + "pages/d_transferDetail/index", + "pages/d_caseReport/index" ] }, { @@ -120,10 +124,6 @@ "text": "知识" }, { - "pagePath": "pages/live/index", - "text": "活动" - }, - { "pagePath": "pages/my/index", "text": "我的" } diff --git a/src/app.ts b/src/app.ts index a91c519..8426bc9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -411,7 +411,7 @@ App({ callback(res) }) }, - mpBehavior(data: { PageName: string; doctor?: boolean }) { + mpBehavior(data: { PageName: string; doctor?: boolean; detailId?: string }) { let url = '?r=igg4/mp-behavior/add' if (data.doctor) { url = '?r=igg4/doctor/mp-behavior/add' diff --git a/src/components/toast/index.scss b/src/components/toast/index.scss index cee76ff..9205f32 100644 --- a/src/components/toast/index.scss +++ b/src/components/toast/index.scss @@ -2451,7 +2451,7 @@ width: 670rpx; box-sizing: border-box; border-radius: 24rpx; - background: linear-gradient(180deg, #E6F4FF 0%, #FFFFFF 22.89%, #FFFFFF 100%); + background: linear-gradient(180deg, #e6f4ff 0%, #ffffff 22.89%, #ffffff 100%); .content { font-size: 36rpx; color: rgba(34, 34, 34, 1); @@ -2477,21 +2477,29 @@ } .popup-public-toast { .popup-container { - position: relative; - padding: 360rpx 146rpx 0; - width: 590rpx; - height: 880rpx; + padding: 226rpx 0; + width: 630rpx; + height: 864rpx; box-sizing: border-box; + .title { + font-size: 40rpx; + color: rgba(34, 34, 34, 1); + text-align: center; + } .code { - width: 286rpx; - height: 286rpx; + display: block; + width: 310rpx; + height: 310rpx; + margin: 20rpx auto 0; + border: 1px solid rgba(0, 0, 0, 0.03); + border-radius: 22rpx; } } - .close { - margin: 68rpx auto 0; + .clsoe{ display: block; - width: 64rpx; - height: 64rpx; + margin: 52rpx auto 0; + width: 84rpx; + height: 84rpx; } } @@ -3621,6 +3629,102 @@ } } +.popup-no-write { + .badge { + position: relative; + z-index: 1; + margin: 0 auto -90rpx; + display: block; + width: 204rpx; + height: 160rpx; + } + .popup-container { + padding: 144rpx 0 0; + width: 540rpx; + box-sizing: border-box; + background-color: #fff; + border-radius: 24rpx; + box-shadow: inset 0rpx -1rpx 0rpx 0rpx #e5e6eb; + .title { + padding-bottom: 48rpx; + font-size: 36rpx; + color: #222222; + font-weight: bold; + text-align: center; + } + .footer { + border-top: 1px solid #eaebef; + display: flex; + .btn1, + .btn2 { + flex: 1; + padding: 22rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + } + .btn1 { + border-right: 1px solid #eaebef; + color: #212120; + } + .btn2 { + color: #0eb66d; + } + } + } + &.drug { + .footer { + .btn2 { + color: rgba(89, 86, 233, 1); + } + } + } +} + +.popup-one-write { + .badge { + position: relative; + z-index: 1; + margin: 0 auto -90rpx; + display: block; + width: 204rpx; + height: 160rpx; + } + .popup-container { + padding: 116rpx 0 32rpx; + width: 550rpx; + box-sizing: border-box; + background-color: #fff; + border-radius: 24rpx; + box-shadow: inset 0rpx -1rpx 0rpx 0rpx #e5e6eb; + .title { + padding-bottom: 48rpx; + font-size: 36rpx; + color: #222222; + font-weight: bold; + text-align: center; + } + .btn { + margin: 0 auto; + width: 486rpx; + height: 96rpx; + background: linear-gradient(86deg, #0eb66d 0%, #00d277 100%); + border-radius: 16rpx 16rpx 16rpx 16rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + color: #ffffff; + } + } + &.drug { + .btn { + background: linear-gradient(274deg, #5956e9 0%, #b384f4 100%); + } + } +} + .close { margin: 48rpx auto 0; display: block; diff --git a/src/components/toast/index.ts b/src/components/toast/index.ts index 48ed459..0b6d815 100644 --- a/src/components/toast/index.ts +++ b/src/components/toast/index.ts @@ -42,7 +42,7 @@ Component({ if (['storyLead', 'storyStar', 'storyShare'].includes(this.data.type)) { this.getSettingInfo() } - if (this.data.type === 'feedback' || this.data.type === 'doctor-feedback') { + if (this.data.type === 'feedback' || this.data.type === 'doctor-feedback' || this.data.type === 'public-toast') { this.getFeedbackCode() } if (this.data.type === 'follow') { diff --git a/src/components/toast/index.wxml b/src/components/toast/index.wxml index 0a9312a..aad9ad2 100644 --- a/src/components/toast/index.wxml +++ b/src/components/toast/index.wxml @@ -718,16 +718,20 @@ bind:tap="handleCancel" > - + - + + 关注「IgG4愈见昕生」公众号 + + 重要提醒不丢失 + + @@ -892,7 +896,9 @@ 《用户协议》 《隐私政策》 - 《个人信息共享知情同意书》 + + 《个人信息共享知情同意书》 + @@ -1092,6 +1098,23 @@ + + + + 您有填写信息还未提交 + + 退出 + 提交 + + + + + + + 请至少完成一项填写 + 继续 + + + - + - + - 用药提醒 + 就诊记录 = { imageField: this.data.imageField, page, pageSize: 10 } + data.patientId = this.data.patientId + wx.ajax({ + method: 'GET', + url: '?r=igg4/doctor/medical-visit/image-history', + data, + }).then((res: any) => { + if (!res || !res.list) return + const newList = res.list + this.setData({ + list: reset ? newList : [...this.data.list, ...newList], + pagination: { + count: res.totalCount || 0, + page: res.page || 1, + pages: res.pages || 1, + }, + }) + }) + }, + + handlePreviewImage(e: WechatMiniprogram.CustomEvent) { + const { url, urls } = e.currentTarget.dataset + wx.previewImage({ + current: url, + urls, + }) + }, +}) + +export {} diff --git a/src/doctor/pages/d_caseReport/index.wxml b/src/doctor/pages/d_caseReport/index.wxml new file mode 100644 index 0000000..6f97843 --- /dev/null +++ b/src/doctor/pages/d_caseReport/index.wxml @@ -0,0 +1,31 @@ + + + + + + + + + + {{item.visitDate}} + + + + + + + + 暂无记录 + + diff --git a/src/doctor/pages/d_changeDoctor/index.ts b/src/doctor/pages/d_changeDoctor/index.ts index b1e4ffc..6c210f6 100644 --- a/src/doctor/pages/d_changeDoctor/index.ts +++ b/src/doctor/pages/d_changeDoctor/index.ts @@ -14,6 +14,9 @@ Page({ timer = null this.setData({ codeText: '发送验证码' }) } + app.waitLogin().then(() => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorChangeDoctor' }) + }) }, onUnload() { if (timer) { @@ -67,6 +70,7 @@ Page({ }) }, handleSubmit() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorChangeDoctorSelect' }) if (!this.data.mobile) { wx.showToast({ icon: 'none', diff --git a/src/doctor/pages/d_createTask/index.scss b/src/doctor/pages/d_createTask/index.scss index 5ac22ae..a1fb6cf 100644 --- a/src/doctor/pages/d_createTask/index.scss +++ b/src/doctor/pages/d_createTask/index.scss @@ -18,11 +18,13 @@ page { height: 48rpx; text-align: center; font-size: 32rpx; - color: rgba(103, 186, 202, 1); + color: rgba(39, 66, 164, 1); line-height: 48rpx; - border: 8rpx solid #85d4d5; border-radius: 50%; - background-color: #85d4d5; + background-color: rgba(255, 255, 255, 1); + border: 8rpx solid rgba(255, 255, 255, 0); + background-clip: content-box; + opacity: 0.8; } .order-end { width: 48rpx; @@ -36,6 +38,8 @@ page { &.active { .order { background-color: #fff; + border: 8rpx solid rgba(255, 255, 255, 0.19); + opacity: 1; } .name { color: rgba(255, 255, 255, 1); @@ -45,9 +49,9 @@ page { .line { margin: 30rpx -10rpx 0; flex: 1; - border-top: 1px dashed #a3d6d7; + border-top: 1px dashed rgba(255, 255, 255, 1); &.active { - border-color: #fff; + border-color: rgba(255, 255, 255, 1); } } } @@ -55,37 +59,68 @@ page { margin-top: 24rpx; text-align: center; font-size: 28rpx; - color: #ffffff; + color: rgba(255, 255, 255, 0.8); line-height: 40rpx; } .step1 { margin: 32rpx 30rpx; - padding: 30rpx; - border-radius: 24rpx; - background-color: #fff; .form { + padding: 30rpx; + margin-bottom: 24rpx; + border-radius: 24rpx; + background-color: #fff; .form-item { margin-bottom: 48rpx; + &:last-of-type { + margin-bottom: 0; + } + &.vertical { + display: flex; + align-items: center; + justify-content: space-between; + } .f-title { font-size: 36rpx; color: rgba(40, 48, 49, 1); font-weight: bold; } + .checkbox { + .wx-checkbox-input { + width: 40rpx; + height: 40rpx; + &.wx-checkbox-input-checked { + background: rgba(22, 121, 203, 1); + border-color: rgba(22, 121, 203, 1); + &::before { + color: #fff; + } + } + } + } .select { margin-top: 24rpx; - display: grid; - grid-template-columns: repeat(3, 1fr); + display: flex; + flex-wrap: wrap; gap: 16rpx; .s-item { - padding: 18rpx 0; + flex: 0 0 auto; + min-width: calc((100% - 44rpx) / 3); + padding: 22rpx 12rpx; text-align: center; border-radius: 12rpx; - font-size: 28rpx; - color: rgba(103, 113, 114, 1); - background: #f2f4f5; + font-size: 32rpx; + color: rgba(34, 34, 34, 0.6); + background: rgba(248, 248, 248, 1); + border: 1px solid rgba(248, 248, 248, 1); + box-sizing: border-box; &.active { - color: #fff; - background-color: rgba(103, 186, 202, 1); + color: rgba(22, 121, 203, 1); + background-color: rgba(22, 121, 203, 0.1); + border-color: rgba(22, 121, 203, 1); + } + &:nth-of-type(4), + &:nth-of-type(5) { + width: 100%; } } } @@ -113,12 +148,10 @@ page { .date { margin-top: 24rpx; - display: flex; - align-items: center; - justify-content: space-between; .picker { flex: 1; + margin-top: 32rpx; .d-item { flex: 1; @@ -126,17 +159,17 @@ page { align-items: center; justify-content: space-between; padding: 18rpx 32rpx; - background: #f7f8f9; + background: rgba(248, 248, 248, 1); border-radius: 12rpx 12rpx 12rpx 12rpx; .icon { - width: 32rpx; - height: 32rpx; + width: 40rpx; + height: 40rpx; } .content { font-size: 32rpx; - color: #677172; + color: rgba(34, 34, 34, 1); &:empty::after { content: attr(data-place); @@ -169,7 +202,7 @@ page { align-items: baseline; .title { font-size: 36rpx; - color: #283031; + color: rgba(34, 34, 34, 1); font-weight: bold; } .tip { @@ -188,7 +221,7 @@ page { align-items: center; gap: 16rpx; font-size: 32rpx; - color: #283031; + color: rgba(34, 34, 34, 1); line-height: 1; .circle { flex-shrink: 0; @@ -196,10 +229,10 @@ page { height: 32rpx; background: #ffffff; border-radius: 50%; - border: 2rpx solid #d4d9da; + border: 2rpx solid rgba(212, 217, 218, 1); &.active { position: relative; - border-color: #62bed0; + border-color: rgba(22, 121, 203, 1); &::after { content: ''; position: absolute; @@ -209,7 +242,7 @@ page { width: 18rpx; height: 18rpx; border-radius: 50%; - background-color: #62bed0; + background-color: rgba(22, 121, 203, 1); } } } @@ -224,16 +257,16 @@ page { flex: 1; padding: 18rpx; font-size: 32rpx; - color: #677172; + color: rgba(34, 34, 34, 0.6); text-align: center; - background: #f7f8f9; + background: rgba(248, 248, 248, 1); border-radius: 12rpx 12rpx 12rpx 12rpx; - border: 2rpx solid rgba(207, 212, 213, 0.56); + border: 2rpx solid rgba(248, 248, 248, 1); font-weight: bold; &.active { - color: #67baca; - border-color: #67baca; - background: rgba(103, 186, 202, 0.1); + color: rgba(22, 121, 203, 1); + border-color: rgba(22, 121, 203, 1); + background: rgba(22, 121, 203, 0.1); } } } @@ -251,9 +284,10 @@ page { padding: 22rpx 0; font-size: 32rpx; text-align: center; + color: rgba(34, 34, 34, 1); &:empty::after { content: attr(data-place); - color: #c2c9ca; + color: rgba(34, 34, 34, 0.2); } } } @@ -262,8 +296,8 @@ page { color: #677172; } .icon { - width: 32rpx; - height: 32rpx; + width: 40rpx; + height: 40rpx; } } } @@ -284,13 +318,14 @@ page { display: none; } .switch-item { + flex: 1; position: relative; text-align: center; font-size: 32rpx; color: rgba(103, 113, 114, 1); white-space: nowrap; &.active { - color: rgba(98, 190, 208, 1); + color: rgba(22, 121, 203, 1); &::after { position: absolute; bottom: -14rpx; @@ -299,8 +334,8 @@ page { content: ''; width: 54rpx; height: 8rpx; - border-radius: 12rpx 12rpx 0 0; - background: #62bed0; + border-radius: 8rpx; + background: rgba(22, 121, 203, 1); } } } @@ -315,14 +350,16 @@ page { transform: scale(0.8); } .wrap { + flex: 1; margin-left: 14rpx; padding: 30rpx; - box-shadow: 0rpx 8rpx 32rpx 0rpx rgba(40, 48, 49, 0.08); - border-radius: 24rpx 24rpx 24rpx 24rpx; + box-shadow: 0rpx 8rpx 32rpx 0rpx rgba(40, 48, 49, 0.04); + border-radius: 24rpx; border: 2rpx solid #f8f9f9; .content { + flex: 1; font-size: 32rpx; - color: rgba(40, 48, 49, 1); + color: rgba(34, 34, 34, 1); } .rows { .row { @@ -362,17 +399,17 @@ page { display: flex; align-items: center; font-size: 32rpx; - color: rgba(40, 48, 49, 1); + color: rgba(34, 34, 34, 1); .resource { margin-left: 26rpx; - color: rgba(103, 186, 202, 1); + color: rgba(22, 121, 203, 1); } } .clear { display: flex; align-items: center; font-size: 32rpx; - color: rgba(103, 186, 202, 1); + color: rgba(22, 121, 203, 1); gap: 12rpx; .icon { width: 36rpx; @@ -388,27 +425,27 @@ page { gap: 26rpx; .cancel { flex: 1; - height: 84rpx; + height: 92rpx; font-size: 36rpx; - color: rgba(103, 186, 202, 1); + color: rgba(22, 121, 203, 1); display: flex; align-items: center; justify-content: center; background: #ffffff; - border-radius: 98rpx 98rpx 98rpx 98rpx; - border: 2rpx solid #67baca; + border-radius: 12rpx; + border: 2rpx solid rgba(22, 121, 203, 1); } .submit { flex: 1; - height: 84rpx; + height: 92rpx; font-size: 36rpx; color: #fff; display: flex; align-items: center; justify-content: center; - background: rgba(103, 186, 202, 1); - border-radius: 98rpx 98rpx 98rpx 98rpx; - border: 2rpx solid #67baca; + background: linear-gradient(96deg, #293b9f 0%, #1679cb 100%); + border-radius: 12rpx; + border: 2rpx solid transparent; } } } diff --git a/src/doctor/pages/d_createTask/index.ts b/src/doctor/pages/d_createTask/index.ts index c446b33..3b351d9 100644 --- a/src/doctor/pages/d_createTask/index.ts +++ b/src/doctor/pages/d_createTask/index.ts @@ -1,10 +1,11 @@ import dayjs from 'dayjs' -import { filterColumns } from '@/utils/doctorPatientTask' const licia = require('miniprogram-licia') const app = getApp() Page({ data: { + background: 'transparent', + step: 1, TemplateType: 1, @@ -23,28 +24,24 @@ Page({ PatientNum: 0, //患者人数 // form - Search: '', - HasAdl: 0, - DoubleStandard: 0, - AdlTime: 0, - AdlScore: [] as any, - AdlScoreChange: [], - AdlScoreChangeType: 0, - AdlScoreChangeMax: '', - AdlScoreChangeMin: '', - SteroidDailyDose: [] as any, - SteroidDailyDoseChange: [] as any, - IsThisWeekAdl: 0, - PharyngealSymptoms: 0, - AntibodyType: 0, - ThyroidAbnormal: 0, - PreviousConvulsion: 0, - TraditionalImmunosuppressant: 0, - MedicalInsuranceType: 0, + SelectAll: 0, + ConfirmedIgg4: 0, + MedicationType: [] as number[], BindStartDate: '', BindEndDate: '', - ...filterColumns, + ConfirmedIgg4Column: [ + { id: 0, value: '全部' }, + { id: 1, value: '是' }, + { id: 2, value: '否' }, + ], + MedicationTypeColumn: [ + { id: 0, value: '全部' }, + { id: 2, value: '激素' }, + { id: 3, value: '免疫抑制剂' }, + { id: 4, value: '靶向CD19生物制剂(伊奈利珠单抗)' }, + { id: 5, value: '靶向CD20生物制剂(利妥昔单抗等)' }, + ], }, onLoad() { const SystemInfo = app.globalSystemInfo @@ -65,6 +62,7 @@ Page({ }) app.waitLogin().then(() => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorCreateTask' }) if (this.data.step === 2) { this.getTaskNum() } @@ -104,34 +102,17 @@ Page({ if (typeof e === 'object') { btn = e?.currentTarget?.dataset?.btn } - if (btn == 1) { - } return wx .ajax({ method: 'GET', url: '?r=igg4/doctor/task/filter-patient-num', data: { - Search: this.data.Search, - HasAdl: this.data.HasAdl, - DoubleStandard: this.data.DoubleStandard, - AdlTime: this.data.AdlTime, - AdlScore: this.data.AdlScore.join('-'), - AdlScoreChange: this.data.AdlScoreChange.join('-'), - AdlScoreChangeType: this.data.AdlScoreChangeType, - AdlScoreChangeMax: this.data.AdlScoreChangeMax, - AdlScoreChangeMin: this.data.AdlScoreChangeMin, - SteroidDailyDose: this.data.SteroidDailyDose.join('-'), - SteroidDailyDoseChange: this.data.SteroidDailyDoseChange.join('-'), - IsThisWeekAdl: this.data.IsThisWeekAdl, - PharyngealSymptoms: this.data.PharyngealSymptoms, - AntibodyType: this.data.AntibodyType, - ThyroidAbnormal: this.data.ThyroidAbnormal, - PreviousConvulsion: this.data.PreviousConvulsion, - TraditionalImmunosuppressant: this.data.TraditionalImmunosuppressant, - MedicalInsuranceType: this.data.MedicalInsuranceType, + SelectAll: this.data.SelectAll, BindStartDate: this.data.BindStartDate, BindEndDate: this.data.BindEndDate, + ConfirmedIgg4: this.data.ConfirmedIgg4, + MedicationType: this.data.MedicationType.join(','), }, loading: true, }) @@ -148,24 +129,9 @@ Page({ success: (res) => { if (res.confirm) { this.setData({ - Search: '', - HasAdl: 0, - DoubleStandard: 0, - AdlTime: 0, - AdlScore: [] as any, - AdlScoreChange: [], - AdlScoreChangeType: 0, - AdlScoreChangeMax: '', - AdlScoreChangeMin: '', - SteroidDailyDose: [] as any, - SteroidDailyDoseChange: [] as any, - IsThisWeekAdl: 0, - PharyngealSymptoms: 0, - AntibodyType: 0, - ThyroidAbnormal: 0, - PreviousConvulsion: 0, - TraditionalImmunosuppressant: 0, - MedicalInsuranceType: 0, + SelectAll: 0, + ConfirmedIgg4: 0, + MedicationType: [], BindStartDate: '', BindEndDate: '', }) @@ -184,14 +150,24 @@ Page({ handleCheckbox(e: any) { const { index, key } = e.currentTarget.dataset const data = this.data[`${key}Column`] + const clickedId = data[index].id const oldValue = this.data[`${key}`] - const value = oldValue.includes(data[index].id) - ? oldValue.filter((item: any) => item !== data[index].id) - : [...oldValue, data[index].id] - this.setData({ - [`${key}`]: data[index].id ? value : [], - }) + if (!clickedId) { + // 点击"全部":清空其他选择 + this.setData({ [`${key}`]: [] }) + return + } + + const value = oldValue.includes(clickedId) + ? oldValue.filter((item: any) => item !== clickedId) + : [...oldValue.filter((item: any) => item !== 0), clickedId] // 选具体选项时移除"全部" + + this.setData({ [`${key}`]: value }) + }, + handleSelectAll() { + this.setData({ SelectAll: this.data.SelectAll === 1 ? 0 : 1 }) + this.getTaskNum() }, handleDay(e) { const { value } = e.currentTarget.dataset @@ -234,33 +210,12 @@ Page({ wx.navigateBack() }, async handleNext() { - const anyValueKeys = [ - 'Search', - 'HasAdl', - 'DoubleStandard', - 'AdlTime', - 'AdlScore', - 'AdlScoreChange', - 'AdlScoreChangeType', - 'AdlScoreChangeMax', - 'AdlScoreChangeMin', - 'SteroidDailyDose', - 'SteroidDailyDoseChange', - 'IsThisWeekAdl', - 'PharyngealSymptoms', - 'AntibodyType', - 'ThyroidAbnormal', - 'PreviousConvulsion', - 'TraditionalImmunosuppressant', - 'MedicalInsuranceType', - 'BindStartDate', - 'BindEndDate', - ] + const anyValueKeys = ['SelectAll', 'ConfirmedIgg4', 'MedicationType', 'BindStartDate', 'BindEndDate'] const empty = anyValueKeys.every((item) => Array.isArray(this.data[item]) ? licia.isEmpty(this.data[item]) : !this.data[item], ) - - if (empty) { + const hasSelectAll = this.data.SelectAll === 1 + if (empty && !hasSelectAll) { wx.showToast({ icon: 'none', title: '请至少选择一个筛选条件', @@ -291,25 +246,11 @@ Page({ this.handleViewMp() }, handleSubmit() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorCreateTaskSubmit' }) const { - Search, - HasAdl, - DoubleStandard, - AdlTime, - AdlScore, - AdlScoreChange, - AdlScoreChangeType, - AdlScoreChangeMax, - AdlScoreChangeMin, - SteroidDailyDose, - SteroidDailyDoseChange, - IsThisWeekAdl, - PharyngealSymptoms, - AntibodyType, - ThyroidAbnormal, - PreviousConvulsion, - TraditionalImmunosuppressant, - MedicalInsuranceType, + SelectAll, + ConfirmedIgg4, + MedicationType, BindStartDate, BindEndDate, @@ -348,30 +289,17 @@ Page({ title: '确认创建群发任务', content: '将会按您创建的规则给所有符合条件患者发送消息', confirmColor: 'rgba(22, 121, 203, 1)', + confirmText: '确认', + cancelText: '取消', success: (res) => { if (res.confirm) { const params = { FilterCondition: { - Search, - HasAdl, - DoubleStandard, - AdlTime, - AdlScore: AdlScore.join('-'), - AdlScoreChange: AdlScoreChange.join('-'), - AdlScoreChangeType, - AdlScoreChangeMax, - AdlScoreChangeMin, - SteroidDailyDose: SteroidDailyDose.join('-'), - SteroidDailyDoseChange: SteroidDailyDoseChange.join('-'), - IsThisWeekAdl, - PharyngealSymptoms, - AntibodyType, - ThyroidAbnormal, - PreviousConvulsion, - TraditionalImmunosuppressant, - MedicalInsuranceType, + SelectAll, BindStartDate, BindEndDate, + ConfirmedIgg4, + MedicationType: MedicationType.join(','), }, PushTemplateId, IsSendNow, @@ -401,6 +329,5 @@ Page({ handleBack() { wx.navigateBack() }, - handleViewMp() { - }, + handleViewMp() {}, }) diff --git a/src/doctor/pages/d_createTask/index.wxml b/src/doctor/pages/d_createTask/index.wxml index 356fb05..6920904 100644 --- a/src/doctor/pages/d_createTask/index.wxml +++ b/src/doctor/pages/d_createTask/index.wxml @@ -1,7 +1,7 @@ 您配置的内容会通过对话列表及公众号提醒通知患者 + + 选择全员 + + + + 绑定时间 {{BindStartDate}} - + - - {{BindEndDate}} - + + + - 本周ADL测评 - - - {{item.value}} - - - - - 是否有ADL测评 - - - {{item.value}} - - - - - ADL测评间隔时长 - - - {{item.value}} - - - - - 是否双达标 - - - {{item.value}} - - - - - ADL评分(多选) - - - {{item.value}} - - - - - ADL分数变化(多选) + 确诊IgG4-RD - {{item.value}} - - - - {{item.value}} - - - - 分 - - - - - - 分 - - - - - 激素用量(多选) - - - {{item.value}} - - + + - 激素变化(多选) + 正在使用的药物 - - 延髓症状 - - - {{item.value}} - - - - - 抗体类型 - - - {{item.value}} - - - - - 胸腺异常 - - - {{item.value}} - - - - - 既往发生危象 - - - {{item.value}} - - - - - 传统免疫抑制剂 - - - {{item.value}} - - - - - 医保类型 - - - {{item.value}} - - - @@ -351,7 +142,7 @@ data-id="{{item.Id}}" > - + {{item.InteractMsgContent}} @@ -369,7 +160,7 @@ 刷新 - + 清空选项 diff --git a/src/doctor/pages/d_customExport/index.ts b/src/doctor/pages/d_customExport/index.ts index c13ec5b..2464a63 100644 --- a/src/doctor/pages/d_customExport/index.ts +++ b/src/doctor/pages/d_customExport/index.ts @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' const app = getApp() Page({ diff --git a/src/doctor/pages/d_home/index.ts b/src/doctor/pages/d_home/index.ts index 05e1c81..44fc583 100644 --- a/src/doctor/pages/d_home/index.ts +++ b/src/doctor/pages/d_home/index.ts @@ -35,6 +35,7 @@ Page({ } echarts = await require.async('../../../resource/components/echart/echarts.js') app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorHome' }) this.getDoctorInfo() this.getDetail() this.getList1_1() @@ -65,6 +66,7 @@ Page({ }) }, handleSelectYear1() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorHomeSelectYear' }) this.getList1_1() }, getList1_1() { @@ -203,6 +205,7 @@ Page({ } as any) }, handlePatient(e) { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorHomePatient' }) const { type, name } = e.currentTarget.dataset wx.navigateTo({ @@ -210,6 +213,7 @@ Page({ }) }, handleInvite() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorHomeInvite' }) wx.navigateTo({ url: '/doctor/pages/d_invite/index', }) diff --git a/src/doctor/pages/d_interactive/index.scss b/src/doctor/pages/d_interactive/index.scss index 16ef107..b5d7171 100644 --- a/src/doctor/pages/d_interactive/index.scss +++ b/src/doctor/pages/d_interactive/index.scss @@ -13,9 +13,12 @@ page { .wrap { display: flex; align-items: center; + gap: 16rpx; .search { flex: 1; padding: 16rpx 32rpx; + height: 76rpx; + box-sizing: border-box; display: flex; align-items: center; gap: 16rpx; @@ -34,6 +37,23 @@ page { color: rgba(173, 179, 180, 1); } } + .send { + flex-shrink: 0; + padding: 16rpx 28rpx; + background: linear-gradient(96deg, #293b9f 0%, #1679cb 100%); + border-radius: 48rpx; + font-size: 32rpx; + color: #ffffff; + display: flex; + align-items: center; + gap: 12rpx; + height: 76rpx; + box-sizing: border-box; + .icon { + width: 40rpx; + height: 40rpx; + } + } .clear { margin-left: 32rpx; width: 56rpx; diff --git a/src/doctor/pages/d_interactive/index.ts b/src/doctor/pages/d_interactive/index.ts index 02f2e0c..c550d91 100644 --- a/src/doctor/pages/d_interactive/index.ts +++ b/src/doctor/pages/d_interactive/index.ts @@ -14,6 +14,7 @@ Page({ }, onShow() { app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorInteractive' }) this.getList() app.setTabbarNoticeMessage() }) @@ -21,6 +22,7 @@ Page({ handleTapSearch() { }, handleSearch() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorInteractiveSearch' }) this.setData({ list: [], pagination: { @@ -67,6 +69,7 @@ Page({ } }, handleDetail(e) { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorInteractiveItem' }) const { index } = e.currentTarget.dataset const { list } = this.data const listItem: any = list[index] @@ -107,4 +110,9 @@ Page({ }, }) }, + handleSendGroup() { + wx.navigateTo({ + url: '/doctor/pages/d_taskList/index', + }) + }, }) diff --git a/src/doctor/pages/d_interactive/index.wxml b/src/doctor/pages/d_interactive/index.wxml index 9b578ec..6c387f5 100644 --- a/src/doctor/pages/d_interactive/index.wxml +++ b/src/doctor/pages/d_interactive/index.wxml @@ -7,26 +7,19 @@ type="text" class="input" placeholder-class="input-place" - placeholder="可根据姓名、手机号搜索患者" + placeholder="搜索姓名、手机号" model:value="{{search}}" confirm-type="search" bindconfirm="handleSearch" bind:tap="handleTapSearch" /> + + + 群发 + - - - - - - - - - - - diff --git a/src/doctor/pages/d_interactiveDoctor/index.ts b/src/doctor/pages/d_interactiveDoctor/index.ts index 239e911..bb1df87 100644 --- a/src/doctor/pages/d_interactiveDoctor/index.ts +++ b/src/doctor/pages/d_interactiveDoctor/index.ts @@ -66,6 +66,7 @@ Page({ }, onShow() { app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorInteractiveDoctor', detailId: this.data.patientId }) this.getDoctorDetail() this.getPatientDetail() this.getMessageList() @@ -332,6 +333,7 @@ Page({ } }, handleConfirm() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorInteractiveSend' }) const { customMessage } = this.data this.handleHideKeyboard() this.handleSendCustomMessage(customMessage) diff --git a/src/doctor/pages/d_invite/index.ts b/src/doctor/pages/d_invite/index.ts index 5efba41..a4b9dae 100644 --- a/src/doctor/pages/d_invite/index.ts +++ b/src/doctor/pages/d_invite/index.ts @@ -18,6 +18,7 @@ Page({ }) } app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorInvite' }) wx.showLoading({ title: '加载中', }) @@ -57,6 +58,7 @@ Page({ }) }, handleDownload() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorInviteSend' }) wx.showToast({ title: '请长按海报图片进行保存', icon: 'none', diff --git a/src/doctor/pages/d_my/index.ts b/src/doctor/pages/d_my/index.ts index 998767a..5e7a161 100644 --- a/src/doctor/pages/d_my/index.ts +++ b/src/doctor/pages/d_my/index.ts @@ -20,6 +20,7 @@ Page({ }, onShow() { app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorMy' }) this.getDetail() }) }, diff --git a/src/doctor/pages/d_patient/index.scss b/src/doctor/pages/d_patient/index.scss index 76fe0cf..16ced0c 100644 --- a/src/doctor/pages/d_patient/index.scss +++ b/src/doctor/pages/d_patient/index.scss @@ -14,26 +14,51 @@ page { z-index: 10; background-color: #fff; - .search { - padding: 16rpx 32rpx; + .search-wrap { display: flex; align-items: center; gap: 16rpx; - background: rgba(243, 243, 243, 1); - border-radius: 140rpx 140rpx 140rpx 140rpx; + .search { + flex: 1; + padding: 16rpx 32rpx; + display: flex; + align-items: center; + gap: 16rpx; + background: rgba(243, 243, 243, 1); + border-radius: 140rpx 140rpx 140rpx 140rpx; + height: 76rpx; + box-sizing: border-box; - .icon { - width: 40rpx; - height: 40rpx; - } + .icon { + width: 40rpx; + height: 40rpx; + } - .input { - flex: 1; - font-size: 32rpx; - } + .input { + flex: 1; + font-size: 32rpx; + } - .input-place { - color: rgba(33, 33, 32, 0.3); + .input-place { + color: rgba(33, 33, 32, 0.3); + } + } + .send { + flex-shrink: 0; + padding: 16rpx 28rpx; + background: linear-gradient(96deg, #293b9f 0%, #1679cb 100%); + border-radius: 48rpx; + font-size: 32rpx; + color: #ffffff; + display: flex; + align-items: center; + gap: 12rpx; + height: 76rpx; + box-sizing: border-box; + .icon { + width: 40rpx; + height: 40rpx; + } } } .navbar { diff --git a/src/doctor/pages/d_patient/index.ts b/src/doctor/pages/d_patient/index.ts index bfb149f..423e6e2 100644 --- a/src/doctor/pages/d_patient/index.ts +++ b/src/doctor/pages/d_patient/index.ts @@ -57,6 +57,7 @@ Page({ IsThisWeekAdl: Number(options.IsThisWeekAdl || ''), }) app.waitLogin().then((_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorPatient' }) this.getList() }) }, @@ -105,6 +106,7 @@ Page({ this.handleSearch() }, handleSearch() { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorPatientSearch' }) this.getList() }, getList(newPage = 1) { @@ -180,6 +182,7 @@ Page({ }) }, handleDetail(e: any) { + app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorPatientItem' }) const { index } = e.currentTarget.dataset const item = this.data.list[index] diff --git a/src/doctor/pages/d_patient/index.wxml b/src/doctor/pages/d_patient/index.wxml index c0e06e3..47b4a6c 100644 --- a/src/doctor/pages/d_patient/index.wxml +++ b/src/doctor/pages/d_patient/index.wxml @@ -1,18 +1,24 @@ - - - + + + + + + + + 群发 + 全部 @@ -27,7 +33,10 @@ {{item.Name}} - 病程:{{item.DiagnosisTimeRange == 1 ? '6个月以内' : item.DiagnosisTimeRange == 2 ? '6个月-3年' : '3年以上'}} + + 病程:{{item.DiagnosisTimeRange == 1 ? '6个月以内' : item.DiagnosisTimeRange == 2 ? '6个月-3年' : + '3年以上'}} + 邀约时间:{{tools.formatDate(item.InviteTime)}} 备注:{{item.DoctorRemark || '无'}} diff --git a/src/doctor/pages/d_patientDetail/index.json b/src/doctor/pages/d_patientDetail/index.json index 1d9f7fc..e30ea72 100644 --- a/src/doctor/pages/d_patientDetail/index.json +++ b/src/doctor/pages/d_patientDetail/index.json @@ -3,6 +3,8 @@ "usingComponents": { "van-icon": "@vant/weapp/icon/index", "van-popup": "@vant/weapp/popup/index", - "toast": "/components/toast/index" + "toast": "/components/toast/index", + "van-sticky": "@vant/weapp/sticky/index", + "ec-canvas": "/components/ec-canvas/ec-canvas" } } diff --git a/src/doctor/pages/d_patientDetail/index.scss b/src/doctor/pages/d_patientDetail/index.scss index d339dc1..0116721 100644 --- a/src/doctor/pages/d_patientDetail/index.scss +++ b/src/doctor/pages/d_patientDetail/index.scss @@ -1,4 +1,5 @@ page { + background-color: #fcfcfc; padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); } .page { @@ -6,6 +7,7 @@ page { overflow-x: hidden; padding-bottom: 200rpx; .banner { + position: relative; margin: 40rpx 40rpx 0; padding: 48rpx 32rpx; display: flex; @@ -13,6 +15,25 @@ page { border-radius: 12rpx 12rpx 12rpx 12rpx; border: 1px solid #f4f3f3; box-shadow: 0 4rpx 32rpx 0 rgba(0, 0, 0, 0.08); + .generate-pdf { + position: absolute; + right: 0; + top: 0; + padding: 0 24rpx; + height: 56rpx; + background: linear-gradient(96deg, #293b9f 0%, #1679cb 100%); + font-size: 28rpx; + color: rgba(255, 255, 255, 1); + border-radius: 0 12rpx 0 24rpx; + line-height: 56rpx; + display: flex; + align-items: center; + gap: 12rpx; + .icon { + width: 28rpx; + height: 28rpx; + } + } .avatar { flex-shrink: 0; .avatar-img { @@ -30,23 +51,48 @@ page { flex-wrap: wrap; font-size: 44rpx; color: rgba(33, 33, 32, 1); + } + .content { + margin-top: 16rpx; + font-size: 32rpx; + color: rgba(33, 33, 32, 1); + line-height: 52rpx; .tel-icon { - flex-shrink: 0; + display: inline-block; margin-left: 10rpx; + vertical-align: middle; width: 40rpx; height: 40rpx; } } - .content { - margin-top: 16rpx; + } + } + .nav { + padding: 32rpx 40rpx; + background-color: #fcfcfc; + .nav-wrap { + background-color: rgba(243, 243, 243, 1); + padding: 8rpx; + border-radius: 12rpx; + display: flex; + align-items: center; + .nav-item { + flex: 1; + padding: 10rpx; font-size: 32rpx; - color: rgba(33, 33, 32, 1); - line-height: 52rpx; + color: rgba(33, 33, 32, 0.4); + line-height: 48rpx; + text-align: center; + &.active { + background-color: #fff; + border-radius: 8rpx; + color: rgba(22, 121, 203, 1); + } } } } .container { - margin: 32rpx 40rpx; + margin: 0 40rpx 32rpx; padding: 0 32rpx; background: #ffffff; box-shadow: 0rpx 4rpx 32rpx 0rpx rgba(0, 0, 0, 0.08); @@ -57,6 +103,7 @@ page { justify-content: space-between; padding: 30rpx 0; border-bottom: 1px solid rgba(244, 243, 243, 1); + gap: 32rpx; &.row-vetical { display: block; .content { @@ -73,22 +120,273 @@ page { font-weight: bold; } .content { + flex: 1; text-align: right; - font-size: 36rpx; + font-size: 32rpx; color: rgba(33, 33, 32, 1); } } } + .chart { + margin: 32rpx 40rpx; + .chart-header { + margin-top: 48rpx; + display: flex; + gap: 24rpx; + .type { + padding: 8rpx 32rpx; + font-size: 32rpx; + color: rgba(34, 34, 34, 0.6); + background-color: rgba(246, 246, 246, 1); + border-radius: 58rpx; + display: flex; + align-items: center; + gap: 14rpx; + .icon { + font-size: 24rpx; + color: rgba(34, 34, 34, 0.6); + transform: rotate(90deg); + } + } + .date { + padding: 8rpx 32rpx; + font-size: 32rpx; + color: rgba(34, 34, 34, 0.6); + background-color: rgba(246, 246, 246, 1); + border-radius: 58rpx; + display: flex; + align-items: center; + gap: 14rpx; + .line { + margin: 0 10rpx; + } + .icon { + transform: rotate(90deg); + color: rgba(34, 34, 34, 0.6); + font-size: 24rpx; + } + } + } + .chart-container { + margin-top: 32rpx; + padding: 32rpx; + background-color: #fff; + border-radius: 24rpx 24rpx 24rpx 24rpx; + border: 2rpx solid #ffffff; + box-shadow: 0rpx 4rpx 32rpx 0rpx rgba(0, 0, 0, 0.08); + .c-header { + display: flex; + justify-content: space-between; + .title { + font-size: 36rpx; + color: rgba(0, 0, 0, 1); + font-weight: bold; + display: flex; + align-items: center; + gap: 24rpx; + &::before { + content: ''; + width: 8rpx; + height: 36rpx; + background: rgba(22, 121, 203, 1); + border-radius: 32rpx 32rpx 32rpx 32rpx; + } + } + .sub { + font-size: 32rpx; + color: rgba(34, 34, 34, 0.4); + } + } + .w-chart { + height: 460rpx; + } + } + } + .case-report { + margin: 64rpx 40rpx 0; + .c-header { + display: flex; + align-items: center; + gap: 32rpx; + .name { + flex-shrink: 0; + font-size: 36rpx; + color: rgba(0, 0, 0, 1); + font-weight: bold; + text-align: center; + position: relative; + z-index: 1; + &::after { + margin-top: -20rpx; + display: block; + content: ''; + height: 20rpx; + background: linear-gradient(90deg, #1679cb 0%, rgba(22, 121, 203, 0) 100%); + border-radius: 0rpx 0rpx 0rpx 0rpx; + opacity: 0.2; + } + } + } + .list { + .row { + margin-top: 32rpx; + padding: 34rpx; + background-color: #fff; + border-radius: 24rpx; + display: flex; + gap: 24rpx; + box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(0, 0, 0, 0.08); + .icon { + flex-shrink: 0; + width: 96rpx; + height: 96rpx; + } + .col { + .name { + font-size: 36rpx; + color: rgba(34, 34, 34, 1); + } + .content { + margin-top: 8rpx; + font-size: 28rpx; + color: rgba(34, 34, 34, 0.5); + } + } + } + } + } + + .record-list { + margin: 32rpx 40rpx 0; + .card { + display: flex; + gap: 20rpx; + .aside { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; + .line-top, + .line-bottom { + border-right: 1px dashed rgba(34, 34, 34, 0.2); + } + .line-top { + flex-shrink: 0; + } + .line-bottom { + flex: 1; + } + .circle { + flex-shrink: 0; + width: 20rpx; + height: 20rpx; + border-radius: 50%; + background-color: rgba(22, 121, 203, 1); + background-clip: content-box; + border: 6rpx solid rgba(22, 121, 203, 0.1); + } + } + .wrap { + flex: 1; + min-width: 0; + padding-bottom: 48rpx; + .date { + font-size: 36rpx; + color: rgba(34, 34, 34, 1); + line-height: 1; + } + .w-container { + position: relative; + margin-top: 24rpx; + padding: 32rpx; + border-radius: 24rpx; + background-color: #fff; + box-shadow: 0rpx 4rpx 32rpx 0rpx rgba(0, 0, 0, 0.08); + .del { + position: absolute; + top: 1px; + right: 1px; + padding: 8rpx 16rpx; + background-color: rgba(246, 246, 246, 1); + font-size: 28rpx; + font-weight: bold; + color: rgba(34, 34, 34, 1); + border-radius: 0 24rpx 0 24rpx; + } + .name { + font-size: 32rpx; + color: rgba(34, 34, 34, 1); + } + .content { + margin-top: 16rpx; + display: flex; + align-items: center; + justify-content: space-between; + .c-left { + display: flex; + gap: 8rpx; + align-items: baseline; + line-height: 84rpx; + .num { + font-size: 72rpx; + color: rgba(34, 34, 34, 1); + font-weight: bold; + } + .sub { + font-size: 36rpx; + color: rgba(34, 34, 34, 1); + } + .icon { + width: 44rpx; + height: 51rpx; + } + } + .status { + font-size: 32rpx; + color: rgba(14, 182, 109, 1); + padding: 0 12rpx; + background-color: rgba(14, 182, 109, 0.08); + border-radius: 8rpx; + } + } + .row { + margin-top: 32rpx; + padding: 32rpx; + background-color: rgba(246, 246, 246, 1); + border-radius: 16rpx; + display: flex; + align-items: center; + gap: 8rpx; + .col { + flex: 1; + .c-name { + font-size: 32rpx; + color: rgba(34, 34, 34, 0.5); + } + .c-content { + margin-top: 16rpx; + font-size: 40rpx; + color: rgba(34, 34, 34, 1); + } + } + } + } + } + } + } .footer { - padding: 0 40rpx; + padding: 32rpx 40rpx calc(env(safe-area-inset-bottom) + 32rpx); position: fixed; - bottom: calc(env(safe-area-inset-bottom) + 32rpx); + bottom: 0; left: 0; + z-index: 10; width: 100%; box-sizing: border-box; display: flex; align-items: center; gap: 30rpx; + background-color: #fff; + box-shadow: 0rpx -12rpx 44rpx 0rpx rgba(0, 0, 0, 0.05); .remark { flex: 1; height: 96rpx; diff --git a/src/doctor/pages/d_patientDetail/index.ts b/src/doctor/pages/d_patientDetail/index.ts index 668988d..c829be6 100644 --- a/src/doctor/pages/d_patientDetail/index.ts +++ b/src/doctor/pages/d_patientDetail/index.ts @@ -2,6 +2,29 @@ import dayjs from 'dayjs' const app = getApp() +let echarts: any = null + +const MEDICATIONS_MAP: Record = { + 1: '没有使用激素', + 2: '激素', + 3: '免疫抑制剂', + 4: '靶向CD19生物制剂', + 5: '靶向CD20生物制剂', + 99: '其他', +} + +const IMMUNOSUPPRESSANT_MAP: Record = { + 1: '吗替麦考酚酯', + 2: '替唑嘌呤', + 3: '环磷酰胺', + 4: '来氟米特', + 5: '甲氨蝶呤', + 6: '环孢素A', + 7: '他克莫司', + 8: '艾拉莫德', + 99: '其他', +} + Page({ data: { toastShow: false, @@ -106,16 +129,306 @@ Page({ 2: '6个月-3年', 3: '3年以上', }, + + // 图表筛选 + yearOptions: [ + { label: '按年', value: 1 }, + { label: '按次', value: 2 }, + ] as { label: string; value: number }[], + selected: 1, + selectedLabel: '按年', + BeginMonth: dayjs().subtract(1, 'year').add(1, 'month').format('YYYY-MM'), + EndMonth: dayjs().format('YYYY-MM'), + chartUnit: '', + chartList: [] as any[], + + // 病历报告 + caseReportList: [ + { + icon: 'icon142.png', + name: '门诊病历及处方', + content: '医生手写或电脑打印的病历、处方', + imageField: 'outpatientRecord', + }, + { icon: 'icon127.png', name: '检验报告', content: '抽血检测的IgG4、IgG、ESR等', imageField: 'labReport' }, + { + icon: 'icon128.png', + name: '影像学检查', + content: '超声/CT/MRI的描述结论、电子胶片', + imageField: 'imagingExam', + }, + { + icon: 'icon129.png', + name: '病理诊断及活检', + content: '受累组织穿刺或切除免疫组化报告', + imageField: 'pathology', + }, + { + icon: 'icon130.png', + name: '外周血免疫功能评估', + content: '免疫细胞亚群、淋巴细胞比例测定', + imageField: 'immuneFunction', + }, + ], + + // Tab 切换 + currentTab: 0, + + // 就诊记录列表 + recordList: [] as any[], + recordPagination: { count: 0, page: 1, pages: 1 }, + + // 导出PPT + pptGenerating: false, }, - onLoad(option) { + ecDataTrendComponent: null as any, + async onLoad(option) { this.setData({ id: option.id, }) + echarts = await require.async('../../../resource/components/echart/echarts.js') + this.ecDataTrendComponent = this.selectComponent('#chart1') app.waitLogin().then(async (_res) => { + app.mpBehavior({ doctor: true, PageName: 'PG_DoctorPatientDetail', detailId: this.data.id }) this.getDoctorDetail() await this.getDetail() + this.getIndicatorHistory() + this.getRecordList(true) }) }, + + // ========== Tab 切换 ========== + + handleTabChange(e: any) { + const { index } = e.currentTarget.dataset + const tab = Number(index) + if (tab === this.data.currentTab) return + this.setData({ currentTab: tab }) + wx.pageScrollTo({ scrollTop: 0, duration: 200 }) + // 切换到就诊记录时如果列表为空则请求 + if (tab === 1 && !this.data.recordList.length) { + this.getRecordList(true) + } + // 切换到基本信息时重新获取图表组件并渲染 + if (tab === 0 && this.data.chartList.length) { + setTimeout(() => { + this.ecDataTrendComponent = this.selectComponent('#chart1') + this.initChart(this.data.chartList) + }, 100) + } + }, + + // ========== 图表筛选 ========== + + handleChange(e: any) { + const idx = Number(e.detail.value) + const selected = this.data.yearOptions[idx] + if (!selected) return + const isByYear = selected.value === 1 + this.setData({ + selected: selected.value, + selectedLabel: selected.label, + BeginMonth: isByYear ? dayjs().subtract(1, 'year').add(1, 'month').format('YYYY-MM') : '', + EndMonth: isByYear ? dayjs().format('YYYY-MM') : '', + }) + this.getIndicatorHistory() + }, + + handleBeginDateChange(e: any) { + const BeginMonth = e.detail.value + this.setData({ BeginMonth }) + if (this.data.EndMonth) { + this.getIndicatorHistory() + } + }, + + handleEndDateChange(e: any) { + const EndMonth = e.detail.value + this.setData({ EndMonth }) + if (this.data.BeginMonth) { + this.getIndicatorHistory() + } + }, + + // ========== 指标趋势图表 ========== + + initChart(defaultList: any[] = []) { + if (!defaultList.length) return + if (!echarts || !this.ecDataTrendComponent) { + this.ecDataTrendComponent = this.selectComponent('#chart1') + if (!echarts || !this.ecDataTrendComponent) return + } + + const list = defaultList + + this.ecDataTrendComponent.init((canvas: any, width: number, height: number, dpr: number) => { + const chart = echarts.init(canvas, null, { + width, + height, + devicePixelRatio: dpr, + }) + canvas.setChart(chart) + + const color = 'rgba(22, 121, 203, 1)' + + const option: any = { + tooltip: { + show: false, + trigger: 'axis', + axisPointer: { type: 'shadow' }, + confine: true, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { color: '#fff', fontSize: 10 }, + order: 'seriesDesc', + }, + grid: { + top: '30', + left: '0', + right: '0', + bottom: '10', + containLabel: true, + }, + xAxis: [ + { + type: 'category', + axisTick: { alignWithLabel: true }, + axisLine: { + lineStyle: { color: 'rgba(34, 34, 34, 0.20)', type: 'dashed' }, + }, + data: list.map((item: any) => dayjs(item.visitDate).format('MM-DD')), + }, + ], + yAxis: [ + { + type: 'value', + minInterval: 1, + axisLabel: { color: 'rgba(34, 34, 34, 0.40)' }, + splitLine: { + lineStyle: { color: 'rgba(34, 34, 34, 0.20)', type: 'dashed' }, + }, + }, + ], + series: [ + { + name: 'IgG4', + data: list.length ? list.map((item: any) => item.value) : [0], + barWidth: '16', + label: { + show: true, + position: 'top', + color, + }, + type: 'line', + symbol: 'circle', + symbolSize: 8, + showSymbol: list.length >= 1, + connectNulls: true, + z: 10, + itemStyle: { + color, + }, + markLine: { + symbol: ['none', 'none'], + data: [ + { + name: '135 安全区', + yAxis: 135, + label: { + formatter: '{b}', + position: 'insideMiddle', + color, + fontSize: '10', + }, + lineStyle: { + cap: '', + color, + type: 'dashed', + }, + }, + ], + }, + markArea: { + itemStyle: { + color: 'rgba(22, 121, 203, 0.16)', + }, + data: [[{ yAxis: 0 }, { yAxis: 135 }]], + }, + }, + ], + dataZoom: { + type: 'inside', + startValue: 0, + endValue: 4, + filterMode: 'none', + }, + } + + chart.setOption(option) + return chart + }) + }, + + getIndicatorHistory() { + const data: Record = { + patientId: this.data.id, + indicatorField: 'igG4', + } + const { BeginMonth, EndMonth, selected } = this.data + + if (BeginMonth && EndMonth && selected === 1) { + data.yearStart = Number(BeginMonth.substring(0, 4)) + data.yearEnd = Number(EndMonth.substring(0, 4)) + } + + wx.ajax({ + method: 'GET', + url: '?r=igg4/doctor/medical-visit/indicator-history', + data, + }).then((res: any) => { + if (!res) return + this.setData({ + chartUnit: res.unit || 'mg/dL', + chartList: res.list || [], + }) + this.initChart(res.list || []) + }) + }, + + // ========== 就诊记录列表 ========== + + getRecordList(reset = false) { + const page = reset ? 1 : this.data.recordPagination.page + wx.ajax({ + method: 'GET', + url: '?r=igg4/doctor/medical-visit/list', + data: { + patientId: this.data.id, + page, + pageSize: 10, + }, + }).then((res: any) => { + if (!res) return + const list = (res.list || []).map((item: any) => ({ + ...item, + medicationLabels: (item.medicationsUsed || []).map((id: number) => MEDICATIONS_MAP[id]).filter(Boolean), + immunosuppressantLabels: (item.immunosuppressantName || []) + .map((id: number) => IMMUNOSUPPRESSANT_MAP[id]) + .filter(Boolean), + })) + + this.setData({ + recordList: reset ? list : [...this.data.recordList, ...list], + recordPagination: { + count: res.totalCount || 0, + page: res.page || 1, + pages: res.pages || 1, + }, + }) + }) + }, + + // ========== 患者详情 ========== + getDoctorDetail() { wx.ajax({ method: 'GET', @@ -157,7 +470,7 @@ Page({ res.HealthQA.questions.forEach((q: any) => { q.isAnswerArray = Array.isArray(q.AnswerText) if (q.isAnswerArray) { - q.AnswerTextArr = q.AnswerText.filter((a: any) => a !== null && a !== undefined) + q.AnswerTextArr = q.AnswerText.filter((a: any) => a !== null && a !== undefined && a !== '其他') q.AnswerTextStr = q.AnswerTextArr.join('、') + (q.OtherText ? `(${q.OtherText})` : '') } else { q.AnswerTextStr = q.AnswerText || '' @@ -167,10 +480,12 @@ Page({ } }) } + wx.setNavigationBarTitle({ + title: res.Name ? `${res.Name}的患者详情` : '患者详情', + }) this.setData({ detail: { ...res, - hideName: res.Name.length <= 2 ? res.Name.charAt(0) + '*' : res.Name.charAt(0) + '*' + res.Name.charAt(res.Name.length - 1), hideTelephone: res.Telephone ? res.Telephone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '', }, }) @@ -275,4 +590,58 @@ Page({ handleToastCancel() { wx.navigateBack() }, + + // ========== 病历报告导航 ========== + + handleDetail(e: any) { + const { name, imagefield } = e.currentTarget.dataset + wx.navigateTo({ + url: `/doctor/pages/d_caseReport/index?name=${name}&imageField=${imagefield}&patientId=${this.data.id}`, + }) + }, + + // ========== 导出PPT ========== + + handleGeneratePdf() { + if (this.data.pptGenerating) return + this.setData({ pptGenerating: true }) + const patientId = this.data.id + const downloadUrl = `${app.globalData.url}?r=igg4/doctor/medical-visit/export-ppt&loginState=${app.globalData.loginState}&patientId=${patientId}` + wx.downloadFile({ + url: downloadUrl, + success: (res) => { + if (res.statusCode === 200) { + wx.shareFileMessage({ + filePath: res.tempFilePath, + fileName: 'medical-visit.pptx', + success: () => { + wx.showToast({ title: '分享成功', icon: 'success' }) + this.setData({ pptGenerating: false }) + }, + fail: () => { + wx.showToast({ title: '分享失败', icon: 'none' }) + this.setData({ pptGenerating: false }) + }, + }) + } else { + wx.showToast({ title: '下载失败', icon: 'none' }) + this.setData({ pptGenerating: false }) + } + }, + fail: () => { + wx.showToast({ title: '下载失败', icon: 'none' }) + this.setData({ pptGenerating: false }) + }, + }) + }, + + // ========== 分页加载 ========== + + onReachBottom() { + const { page, pages } = this.data.recordPagination + if (page < pages) { + this.setData({ 'recordPagination.page': page + 1 }) + this.getRecordList() + } + }, }) diff --git a/src/doctor/pages/d_patientDetail/index.wxml b/src/doctor/pages/d_patientDetail/index.wxml index aef6b86..f6fc465 100644 --- a/src/doctor/pages/d_patientDetail/index.wxml +++ b/src/doctor/pages/d_patientDetail/index.wxml @@ -1,39 +1,166 @@