Browse Source
refactor: 重构患者详情、记录列表和详情页 fix: 修复样式问题和类型定义 docs: 更新AGENTS.md文档说明 style: 统一代码格式和颜色值写法 test: 添加相关接口测试用例 chore: 更新依赖和配置文件dev
47 changed files with 3479 additions and 832 deletions
@ -0,0 +1,240 @@ |
|||||||
|
# 突眼日记接口联调计划 |
||||||
|
|
||||||
|
## 一、项目现状分析 |
||||||
|
|
||||||
|
### 1.1 已有页面结构 |
||||||
|
|
||||||
|
**医生端 (d\_ 前缀)** |
||||||
|
|
||||||
|
- `d_noteList` - 突眼记录列表页(拍摄示例页) |
||||||
|
|
||||||
|
- `d_noteDetail` - 突眼记录详情页 |
||||||
|
|
||||||
|
- `d_noteDiffData` - 凸眼度对比数据页(表格展示) |
||||||
|
|
||||||
|
_患者端 (patient/pages/note_)\* |
||||||
|
|
||||||
|
- `note` - 突眼日记首页 |
||||||
|
|
||||||
|
- `noteAdd` - 新增记录页 |
||||||
|
|
||||||
|
- `noteHistory` - 历史记录页 |
||||||
|
|
||||||
|
- `noteDiff` - 照片对比页 |
||||||
|
|
||||||
|
- `noteDiffEdit` - 对比编辑页 |
||||||
|
|
||||||
|
- `noteDemo` - 拍摄示例页 |
||||||
|
|
||||||
|
### 1.2 当前状态 |
||||||
|
|
||||||
|
- 所有页面都是静态数据,未接入接口 |
||||||
|
|
||||||
|
- 请求工具已封装在 `utils/request.ts` |
||||||
|
|
||||||
|
- 使用 `wx.ajax` 进行接口调用(在 app.ts 中挂载) |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 二、需要的信息 |
||||||
|
|
||||||
|
在开始联调前,需要你提供以下信息: |
||||||
|
|
||||||
|
### 2.1 环境信息 |
||||||
|
|
||||||
|
| 信息项 | 说明 | 是否必需 | |
||||||
|
| -------------- | --------------------------- | -------- | |
||||||
|
| 后端环境地址 | 开发/测试服务器地址 | 是 | |
||||||
|
| 接口是否已部署 | 后端接口是否已上线可调用 | 是 | |
||||||
|
| 登录态获取方式 | 如何获取 loginState/session | 是 | |
||||||
|
|
||||||
|
### 2.2 接口测试信息 |
||||||
|
|
||||||
|
| 信息项 | 说明 | 是否必需 | |
||||||
|
| ---------- | -------------------------- | -------- | |
||||||
|
| 测试账号 | 医生账号和患者账号 | 是 | |
||||||
|
| 测试患者ID | 用于医生端接口测试 | 是 | |
||||||
|
| 已有数据 | 是否有已创建的突眼记录数据 | 否 | |
||||||
|
|
||||||
|
### 2.3 图片上传相关 |
||||||
|
|
||||||
|
| 信息项 | 说明 | 是否必需 | |
||||||
|
| ------------- | ----------------------------- | -------- | |
||||||
|
| 图片上传方式 | 上传到腾讯云COS还是自家服务器 | 是 | |
||||||
|
| 腾讯云IMS配置 | 是否已配置图片内容安全校验 | 否 | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 三、接口联调清单 |
||||||
|
|
||||||
|
### 3.1 患者端接口 (10个) |
||||||
|
|
||||||
|
| 序号 | 接口 | 用途 | 对应页面 | 优先级 | |
||||||
|
| ---- | ------------------- | ---------------- | ----------------- | ------ | |
||||||
|
| 1 | `baseline-status` | 获取基准照状态 | note | P0 | |
||||||
|
| 2 | `record-list` | 获取记录列表 | note, noteHistory | P0 | |
||||||
|
| 3 | `record-detail` | 获取记录详情 | noteHistory | P0 | |
||||||
|
| 4 | `record-save` | 创建/更新记录 | noteAdd | P0 | |
||||||
|
| 5 | `photo-upload` | 上传照片 | noteAdd | P0 | |
||||||
|
| 6 | `record-delete` | 删除记录 | noteHistory | P1 | |
||||||
|
| 7 | `compare-dates` | 获取对比可用日期 | noteDiff | P1 | |
||||||
|
| 8 | `compare-photos` | 获取对比照片 | noteDiff | P1 | |
||||||
|
| 9 | `get-compare-angle` | 获取对比角度列表 | noteDiffEdit | P1 | |
||||||
|
| 10 | `get-session-id` | 获取唯一标识 | noteAdd | P2 | |
||||||
|
|
||||||
|
### 3.2 医生端接口 (6个) |
||||||
|
|
||||||
|
| 序号 | 接口 | 用途 | 对应页面 | 优先级 | |
||||||
|
| ---- | ------------------------------------ | ---------------- | --------------- | ------ | |
||||||
|
| 1 | `doctor/proptosis/latest` | 获取最近记录 | d_patientDetail | P0 | |
||||||
|
| 2 | `doctor/proptosis/list` | 获取患者记录列表 | d_noteList | P0 | |
||||||
|
| 3 | `doctor/proptosis/detail` | 获取记录详情 | d_noteDetail | P0 | |
||||||
|
| 4 | `doctor/proptosis/export` | Excel导出 | d_noteDiffData | P1 | |
||||||
|
| 5 | `doctor/proptosis/compare` | 获取对比数据 | d_noteDiffData | P1 | |
||||||
|
| 6 | `doctor/proptosis/get-compare-angle` | 获取对比角度 | d_noteDiffData | P1 | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 四、实施步骤 |
||||||
|
|
||||||
|
### Phase 1: 基础联调准备 |
||||||
|
|
||||||
|
1. 确认后端环境可用 |
||||||
|
2. 获取测试账号和患者ID |
||||||
|
3. 验证登录态获取 |
||||||
|
|
||||||
|
### Phase 2: 患者端核心功能 |
||||||
|
|
||||||
|
1. 实现记录列表获取 (`record-list`) |
||||||
|
2. 实现记录详情获取 (`record-detail`) |
||||||
|
3. 实现记录创建/更新 (`record-save`) |
||||||
|
4. 实现照片上传 (`photo-upload`) |
||||||
|
|
||||||
|
### Phase 3: 患者端对比功能 |
||||||
|
|
||||||
|
1. 实现对比日期获取 (`compare-dates`) |
||||||
|
2. 实现对比照片获取 (`compare-photos`) |
||||||
|
3. 实现对比角度获取 (`get-compare-angle`) |
||||||
|
|
||||||
|
### Phase 4: 医生端功能 |
||||||
|
|
||||||
|
1. 实现患者最近记录获取 (`doctor/proptosis/latest`) |
||||||
|
2. 实现患者记录列表 (`doctor/proptosis/list`) |
||||||
|
3. 实现记录详情 (`doctor/proptosis/detail`) |
||||||
|
4. 实现对比数据获取 (`doctor/proptosis/compare`) |
||||||
|
|
||||||
|
### Phase 5: 导出功能 |
||||||
|
|
||||||
|
1. 实现Excel导出 (`doctor/proptosis/export`) |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 五、数据结构映射 |
||||||
|
|
||||||
|
### 5.1 患者端 - 记录列表 |
||||||
|
|
||||||
|
```typescript |
||||||
|
// 接口返回 |
||||||
|
interface RecordItem { |
||||||
|
recordId: string |
||||||
|
recordDate: string |
||||||
|
isBaseline: number |
||||||
|
photoCount: number |
||||||
|
firstPhotoUrl: string |
||||||
|
treatmentCount: number |
||||||
|
leftEye: number |
||||||
|
rightEye: number |
||||||
|
interorbitalDistance: number |
||||||
|
uploadCompleted: number |
||||||
|
} |
||||||
|
|
||||||
|
// 页面使用 |
||||||
|
interface DataListItem { |
||||||
|
date: string // recordDate 格式化 |
||||||
|
left: string // leftEye |
||||||
|
spacing: string // interorbitalDistance |
||||||
|
right: string // rightEye |
||||||
|
count: string // treatmentCount |
||||||
|
isBaseline: boolean |
||||||
|
photoCount: number |
||||||
|
firstPhotoUrl: string |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 5.2 医生端 - 凸眼度对比表格 |
||||||
|
|
||||||
|
```typescript |
||||||
|
// 接口返回 (compare 接口) |
||||||
|
interface CompareItem { |
||||||
|
recordId: string |
||||||
|
recordDate: string |
||||||
|
photoUrl: string |
||||||
|
leftEye: number |
||||||
|
rightEye: number |
||||||
|
interorbitalDistance: number |
||||||
|
treatmentCount: number |
||||||
|
} |
||||||
|
|
||||||
|
// 页面使用 (d_noteDiffData) |
||||||
|
interface TableItem { |
||||||
|
date: string // recordDate 格式化 2026.1.5 |
||||||
|
left: string // leftEye |
||||||
|
spacing: string // interorbitalDistance |
||||||
|
right: string // rightEye |
||||||
|
count: string // treatmentCount |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 六、注意事项 |
||||||
|
|
||||||
|
### 6.1 业务规则 |
||||||
|
|
||||||
|
1. 同一天只能创建一条记录 |
||||||
|
2. 必须先上传照片再保存记录(photoIds 关联) |
||||||
|
3. 基准照只能有一张 |
||||||
|
4. 图片需要经过腾讯云IMS安全校验 |
||||||
|
|
||||||
|
### 6.2 技术注意 |
||||||
|
|
||||||
|
1. 使用 `wx.ajax` 进行请求(已挂载在 app 上) |
||||||
|
2. 医生端页面需要等待登录态 `app.waitLogin({ type: [2] })` |
||||||
|
3. 患者端页面需要等待登录态 `app.waitLogin({ type: [1] })` |
||||||
|
4. 图片URL需要拼接 `{{imageUrl}}` 前缀 |
||||||
|
|
||||||
|
### 6.3 角度映射 |
||||||
|
|
||||||
|
接口角度值与页面显示名称映射: |
||||||
|
|
||||||
|
```typescript |
||||||
|
const angleMap = { |
||||||
|
front_open: '正面睁眼', |
||||||
|
front_closed: '正面闭眼', |
||||||
|
front_looking_up: '正面仰头', |
||||||
|
side_left_90: '90°左侧', |
||||||
|
side_right_90: '90°右侧', |
||||||
|
side_left_45: '45°左侧', |
||||||
|
side_right_45: '45°右侧', |
||||||
|
eye_up_left: '左上', |
||||||
|
eye_up: '向上', |
||||||
|
eye_up_right: '右上', |
||||||
|
eye_left: '向左', |
||||||
|
eye_right: '向右', |
||||||
|
eye_down_left: '左下', |
||||||
|
eye_down: '向下', |
||||||
|
eye_down_right: '右下', |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 七、下一步 |
||||||
|
|
||||||
|
请提供以下信息,我将开始实施联调: |
||||||
|
|
||||||
|
1. **后端环境地址**(如:<https://m.xd.hbraas.com)> |
||||||
|
2. **测试账号**(医生和患者各一个) |
||||||
|
3. **测试患者ID**(用于医生端接口测试) |
||||||
|
4. **接口是否已部署**(确认后端已完成开发) |
||||||
|
5. **图片上传方式**(腾讯云COS或自家服务器) |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"navigationBarTitleText": "照片对比", |
||||||
|
"usingComponents": {} |
||||||
|
} |
||||||
@ -0,0 +1,275 @@ |
|||||||
|
page { |
||||||
|
background-color: #f6f8f9; |
||||||
|
} |
||||||
|
|
||||||
|
.page { |
||||||
|
padding-bottom: 80rpx; |
||||||
|
|
||||||
|
.page-tip { |
||||||
|
padding: 24rpx 28rpx; |
||||||
|
background-color: #fff7e9; |
||||||
|
display: flex; |
||||||
|
|
||||||
|
.icon { |
||||||
|
margin-top: 6rpx; |
||||||
|
flex-shrink: 0; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
margin-left: 16rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #ffa300; |
||||||
|
line-height: 44rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.form { |
||||||
|
margin: 32rpx 40rpx 0; |
||||||
|
padding: 38rpx 32rpx 32rpx; |
||||||
|
background: #ffffff; |
||||||
|
border-radius: 32rpx; |
||||||
|
|
||||||
|
.form-item { |
||||||
|
margin-bottom: 48rpx; |
||||||
|
|
||||||
|
&:last-of-type { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.title { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
font-weight: bold; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 16rpx; |
||||||
|
|
||||||
|
&::before { |
||||||
|
content: ''; |
||||||
|
width: 8rpx; |
||||||
|
height: 32rpx; |
||||||
|
background: #b982ff; |
||||||
|
border-radius: 44rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.select { |
||||||
|
margin-top: 24rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
padding: 26rpx 32rpx; |
||||||
|
background: #f6f8f9; |
||||||
|
border-radius: 24rpx; |
||||||
|
|
||||||
|
.content { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
|
||||||
|
&.placeholder { |
||||||
|
color: rgba(173, 172, 178, 0.6); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.multiple { |
||||||
|
margin-top: 24rpx; |
||||||
|
display: grid; |
||||||
|
grid-template-columns: repeat(2, 1fr); |
||||||
|
gap: 24rpx 22rpx; |
||||||
|
|
||||||
|
.item { |
||||||
|
padding: 16rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
background: #f6f8f9; |
||||||
|
border-radius: 24rpx; |
||||||
|
|
||||||
|
&.active { |
||||||
|
color: #fff; |
||||||
|
background: linear-gradient(180deg, #e98ff8 0%, #b073ff 100%); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
margin: 76rpx 40rpx 0; |
||||||
|
|
||||||
|
.title { |
||||||
|
padding-bottom: 40rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
font-weight: bold; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 16rpx; |
||||||
|
|
||||||
|
.date { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #adacb2; |
||||||
|
font-weight: normal; |
||||||
|
align-self: flex-end; |
||||||
|
} |
||||||
|
|
||||||
|
&::before { |
||||||
|
content: ''; |
||||||
|
width: 8rpx; |
||||||
|
height: 32rpx; |
||||||
|
background: #b982ff; |
||||||
|
border-radius: 44rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.card { |
||||||
|
display: flex; |
||||||
|
|
||||||
|
.aside { |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
width: 32rpx; |
||||||
|
|
||||||
|
.circle { |
||||||
|
position: relative; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background: rgba(185, 130, 255, 0.29); |
||||||
|
flex-shrink: 0; |
||||||
|
|
||||||
|
&::after { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
display: block; |
||||||
|
content: ''; |
||||||
|
width: 18rpx; |
||||||
|
height: 18rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: #b982ff; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.line-bottom { |
||||||
|
flex: 1; |
||||||
|
width: 2rpx; |
||||||
|
border-left: 2rpx dashed #b982ff; |
||||||
|
margin-top: 8rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.c-container { |
||||||
|
margin-top: -6rpx; |
||||||
|
margin-left: 14rpx; |
||||||
|
flex: 1; |
||||||
|
padding-bottom: 48rpx; |
||||||
|
|
||||||
|
.c-header { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
|
||||||
|
.date { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.tags { |
||||||
|
margin-top: 12rpx; |
||||||
|
|
||||||
|
.tag { |
||||||
|
display: inline-block; |
||||||
|
margin-right: 16rpx; |
||||||
|
padding: 2rpx 12rpx; |
||||||
|
font-size: 24rpx; |
||||||
|
line-height: 32rpx; |
||||||
|
border-radius: 6rpx; |
||||||
|
|
||||||
|
&.tag1 { |
||||||
|
background: rgba(255, 163, 0, 0.17); |
||||||
|
color: #ffa300; |
||||||
|
} |
||||||
|
|
||||||
|
&.tag2 { |
||||||
|
background: rgba(176, 115, 255, 0.16); |
||||||
|
color: #b073ff; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.photo-card { |
||||||
|
margin-top: 24rpx; |
||||||
|
|
||||||
|
.photo { |
||||||
|
border-radius: 32rpx 32rpx 0 0; |
||||||
|
height: 352rpx; |
||||||
|
display: block; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.row { |
||||||
|
display: flex; |
||||||
|
gap: 24rpx; |
||||||
|
text-align: center; |
||||||
|
border-radius: 0 0 32rpx 32rpx; |
||||||
|
background-color: #fff; |
||||||
|
|
||||||
|
.col { |
||||||
|
padding: 24rpx; |
||||||
|
flex: 1; |
||||||
|
|
||||||
|
.name { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #211d2e; |
||||||
|
line-height: 32rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.content { |
||||||
|
margin-top: 16rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: center; |
||||||
|
align-items: baseline; |
||||||
|
gap: 8rpx; |
||||||
|
|
||||||
|
.num { |
||||||
|
font-size: 56rpx; |
||||||
|
color: #b073ff; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
.sub { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #211d2e; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.footer { |
||||||
|
margin-top: 46rpx; |
||||||
|
|
||||||
|
.btn1 { |
||||||
|
height: 88rpx; |
||||||
|
font-size: 36rpx; |
||||||
|
color: #ffffff; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%); |
||||||
|
border-radius: 100rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,212 @@ |
|||||||
|
const app = getApp<IAppOption>() |
||||||
|
|
||||||
|
interface ComparePhoto { |
||||||
|
recordId: string |
||||||
|
recordDate: string |
||||||
|
isBaseline: number |
||||||
|
photoUrl: string |
||||||
|
leftEye: number |
||||||
|
rightEye: number |
||||||
|
interorbitalDistance: number |
||||||
|
treatmentCount: number |
||||||
|
} |
||||||
|
|
||||||
|
interface CompareDate { |
||||||
|
recordId: string |
||||||
|
recordDate: string |
||||||
|
} |
||||||
|
|
||||||
|
interface ListItem extends CompareDate { |
||||||
|
isBaseline: number |
||||||
|
} |
||||||
|
|
||||||
|
Page({ |
||||||
|
data: { |
||||||
|
patientId: '', |
||||||
|
|
||||||
|
// 对比角度
|
||||||
|
photoAngle: '', |
||||||
|
photoAngleName: '', |
||||||
|
angleList: [] as { key: string, name: string }[], |
||||||
|
angleMap: {} as Record<string, string>, |
||||||
|
|
||||||
|
// 日期选择
|
||||||
|
baseline: null as CompareDate | null, |
||||||
|
nonBaselineList: [] as CompareDate[], |
||||||
|
selectedDates: [] as string[], |
||||||
|
|
||||||
|
// 对比照片
|
||||||
|
comparePhotos: [] as ComparePhoto[], |
||||||
|
}, |
||||||
|
|
||||||
|
onLoad(option: any) { |
||||||
|
this.setData({ |
||||||
|
patientId: option.patientId || '', |
||||||
|
}) |
||||||
|
this.getCompareAngle() |
||||||
|
}, |
||||||
|
|
||||||
|
onShow() { |
||||||
|
app.waitLogin({ type: [2] }).then(() => { |
||||||
|
this.getCompareDates() |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取对比角度列表
|
||||||
|
getCompareAngle() { |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/get-compare-angle', |
||||||
|
// 兼容后端实际需要 patientId(文档可能漏写)
|
||||||
|
data: this.data.patientId ? { patientId: this.data.patientId } : {}, |
||||||
|
}).then((res: any) => { |
||||||
|
const angleMap = res.photoAngle || {} |
||||||
|
const angleList = Object.keys(angleMap).map(key => ({ |
||||||
|
key, |
||||||
|
name: angleMap[key], |
||||||
|
})) |
||||||
|
this.setData({ |
||||||
|
angleMap, |
||||||
|
angleList, |
||||||
|
// 默认选择第一个角度
|
||||||
|
photoAngle: angleList[0]?.key || '', |
||||||
|
photoAngleName: angleList[0]?.name || '', |
||||||
|
}) |
||||||
|
// 获取对比日期
|
||||||
|
if (this.data.photoAngle) { |
||||||
|
this.getCompareDates() |
||||||
|
} |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比角度失败:', err) |
||||||
|
wx.showToast({ title: '获取对比角度失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取对比可用日期
|
||||||
|
getCompareDates() { |
||||||
|
if (!this.data.photoAngle) |
||||||
|
return |
||||||
|
// 医生端接口文档未提供 compare-dates:用记录列表推导 baseline/nonBaseline
|
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/list', |
||||||
|
data: { |
||||||
|
patientId: this.data.patientId, |
||||||
|
page: 1, |
||||||
|
pageSize: 1000, |
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const list: ListItem[] = res.list || [] |
||||||
|
const baselineItem = list.find(i => i.isBaseline === 1) || null |
||||||
|
const nonBaselineList: CompareDate[] = list |
||||||
|
.filter(i => i.isBaseline !== 1) |
||||||
|
.map(i => ({ recordId: i.recordId, recordDate: i.recordDate })) |
||||||
|
|
||||||
|
this.setData({ |
||||||
|
baseline: baselineItem ? { recordId: baselineItem.recordId, recordDate: baselineItem.recordDate } : null, |
||||||
|
nonBaselineList, |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比日期失败:', err) |
||||||
|
wx.showToast({ title: '获取对比日期失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 选择对比角度
|
||||||
|
onAngleChange(e: any) { |
||||||
|
const index = e.detail.value |
||||||
|
const angle = this.data.angleList[index] |
||||||
|
this.setData({ |
||||||
|
photoAngle: angle.key, |
||||||
|
photoAngleName: angle.name, |
||||||
|
selectedDates: [], |
||||||
|
comparePhotos: [], |
||||||
|
}) |
||||||
|
this.getCompareDates() |
||||||
|
}, |
||||||
|
|
||||||
|
// 选择日期
|
||||||
|
onDateSelect(e: any) { |
||||||
|
const { recordId } = e.currentTarget.dataset |
||||||
|
const selectedDates = this.data.selectedDates |
||||||
|
const index = selectedDates.indexOf(recordId) |
||||||
|
if (index > -1) { |
||||||
|
selectedDates.splice(index, 1) |
||||||
|
} |
||||||
|
else { |
||||||
|
selectedDates.push(recordId) |
||||||
|
} |
||||||
|
this.setData({ selectedDates }) |
||||||
|
// 获取对比照片
|
||||||
|
this.getComparePhotos() |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取对比照片
|
||||||
|
getComparePhotos() { |
||||||
|
const { photoAngle, selectedDates, baseline } = this.data |
||||||
|
if (!photoAngle || selectedDates.length === 0) { |
||||||
|
this.setData({ comparePhotos: [] }) |
||||||
|
return |
||||||
|
} |
||||||
|
// 包含基准照ID
|
||||||
|
const recordIds = baseline ? [baseline.recordId, ...selectedDates] : selectedDates |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/compare', |
||||||
|
data: { |
||||||
|
patientId: this.data.patientId, |
||||||
|
photoAngle, |
||||||
|
recordIds: recordIds.join(','), |
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const baselineRes = res.baseline |
||||||
|
const compareList = res.compareList || [] |
||||||
|
const merged: ComparePhoto[] = [] |
||||||
|
if (baselineRes) { |
||||||
|
merged.push({ |
||||||
|
recordId: baselineRes.recordId, |
||||||
|
recordDate: baselineRes.recordDate, |
||||||
|
isBaseline: 1, |
||||||
|
photoUrl: baselineRes.photoUrl, |
||||||
|
leftEye: baselineRes.leftEye, |
||||||
|
rightEye: baselineRes.rightEye, |
||||||
|
interorbitalDistance: baselineRes.interorbitalDistance, |
||||||
|
treatmentCount: baselineRes.treatmentCount, |
||||||
|
}) |
||||||
|
} |
||||||
|
compareList.forEach((item: any) => { |
||||||
|
merged.push({ |
||||||
|
recordId: item.recordId, |
||||||
|
recordDate: item.recordDate, |
||||||
|
isBaseline: 0, |
||||||
|
photoUrl: item.photoUrl, |
||||||
|
leftEye: item.leftEye, |
||||||
|
rightEye: item.rightEye, |
||||||
|
interorbitalDistance: item.interorbitalDistance, |
||||||
|
treatmentCount: item.treatmentCount, |
||||||
|
}) |
||||||
|
}) |
||||||
|
this.setData({ |
||||||
|
comparePhotos: merged, |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比照片失败:', err) |
||||||
|
wx.showToast({ title: '获取对比照片失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 生成对比图
|
||||||
|
handleEdit() { |
||||||
|
if (this.data.selectedDates.length === 0) { |
||||||
|
wx.showToast({ title: '请选择对比日期', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
const { baseline, selectedDates } = this.data |
||||||
|
const recordIds = baseline ? [baseline.recordId, ...selectedDates] : selectedDates |
||||||
|
wx.navigateTo({ |
||||||
|
url: `/pages/d_noteDiffEdit/index?patientId=${this.data.patientId}&photoAngle=${this.data.photoAngle}&recordIds=${recordIds.join(',')}`, |
||||||
|
}) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
export {} |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
<view class="page"> |
||||||
|
<view class="page-tip"> |
||||||
|
<image class="icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image> |
||||||
|
<view class="content"> |
||||||
|
您可以同时选择多个日期,系统将按时间顺序排列照片。点击右上角按钮可预览并保存生成的对比长图。 |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="form"> |
||||||
|
<view class="form-item"> |
||||||
|
<view class="title">选择对比角度</view> |
||||||
|
<picker mode="selector" range="{{angleList}}" range-key="name" value="{{0}}" bindchange="onAngleChange"> |
||||||
|
<view class="select"> |
||||||
|
<view class="content {{photoAngleName ? '' : 'placeholder'}}">{{photoAngleName || '请选择对比角度'}}</view> |
||||||
|
<van-icon class="more" name="arrow-down" /> |
||||||
|
</view> |
||||||
|
</picker> |
||||||
|
</view> |
||||||
|
<view class="form-item"> |
||||||
|
<view class="title">选择对比日期(可多选)</view> |
||||||
|
<view class="multiple"> |
||||||
|
<view |
||||||
|
class="item {{selectedDates.indexOf(item.recordId) > -1 ? 'active' : ''}}" |
||||||
|
wx:for="{{nonBaselineList}}" |
||||||
|
wx:key="recordId" |
||||||
|
data-record-id="{{item.recordId}}" |
||||||
|
bind:tap="onDateSelect" |
||||||
|
>{{item.recordDate}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="container" wx:if="{{comparePhotos.length > 0}}"> |
||||||
|
<view class="title"> |
||||||
|
{{photoAngleName}}时间线对比 |
||||||
|
<view class="date">生成日期:{{comparePhotos[0].recordDate}}</view> |
||||||
|
</view> |
||||||
|
<view class="card" wx:for="{{comparePhotos}}" wx:key="recordId"> |
||||||
|
<view class="aside"> |
||||||
|
<view class="circle"></view> |
||||||
|
<view class="line-bottom"></view> |
||||||
|
</view> |
||||||
|
<view class="c-container"> |
||||||
|
<view class="c-header"> |
||||||
|
<view class="date">{{item.isBaseline === 1 ? '基准照片' : '对比照片'}} {{item.recordDate}}</view> |
||||||
|
</view> |
||||||
|
<view class="tags"> |
||||||
|
<view wx:if="{{item.isBaseline === 1}}" class="tag tag1">基准照片</view> |
||||||
|
<view class="tag tag2">替妥尤单抗:{{item.treatmentCount}}</view> |
||||||
|
</view> |
||||||
|
<view class="photo-card"> |
||||||
|
<image class="photo" src="{{item.photoUrl}}" mode="aspectFill"></image> |
||||||
|
<view class="row" wx:if="{{item.leftEye || item.rightEye || item.interorbitalDistance}}"> |
||||||
|
<view class="col"> |
||||||
|
<view class="name">右眼</view> |
||||||
|
<view class="content"> |
||||||
|
<view class="num">{{item.rightEye || '--'}}</view> |
||||||
|
<view class="sub">MM</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="col"> |
||||||
|
<view class="name">眶间距</view> |
||||||
|
<view class="content"> |
||||||
|
<view class="num">{{item.interorbitalDistance || '--'}}</view> |
||||||
|
<view class="sub">MM</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="col"> |
||||||
|
<view class="name">左眼</view> |
||||||
|
<view class="content"> |
||||||
|
<view class="num">{{item.leftEye || '--'}}</view> |
||||||
|
<view class="sub">MM</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="footer"> |
||||||
|
<view class="btn1" bind:tap="handleEdit">生成对比图</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
@ -1,25 +1,126 @@ |
|||||||
const _app = getApp<IAppOption>(); |
const app = getApp<IAppOption>() |
||||||
|
|
||||||
|
interface CompareItem { |
||||||
|
recordId: string |
||||||
|
recordDate: string |
||||||
|
photoUrl: string |
||||||
|
leftEye: number |
||||||
|
rightEye: number |
||||||
|
interorbitalDistance: number |
||||||
|
treatmentCount: number |
||||||
|
} |
||||||
|
|
||||||
Page({ |
Page({ |
||||||
data: { |
data: { |
||||||
dataList: [ |
patientId: '', |
||||||
{ |
dataList: [] as CompareItem[], |
||||||
date: '2026.1.5', |
loading: false, |
||||||
left: '40', |
}, |
||||||
spacing: '40', |
|
||||||
right: '40', |
onLoad(option: any) { |
||||||
count: '30', |
this.setData({ |
||||||
}, |
patientId: option.patientId || '', |
||||||
{ |
}) |
||||||
date: '2025.12.30', |
}, |
||||||
left: '30', |
|
||||||
spacing: '30', |
onShow() { |
||||||
right: '30', |
app.waitLogin({ type: [2] }).then(() => { |
||||||
count: '20', |
if (this.data.patientId) { |
||||||
}, |
this.getCompareData() |
||||||
], |
} |
||||||
}, |
}) |
||||||
onLoad() {}, |
}, |
||||||
}); |
|
||||||
|
// 获取对比数据
|
||||||
export {}; |
getCompareData() { |
||||||
|
this.setData({ loading: true }) |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/list', |
||||||
|
data: { |
||||||
|
patientId: this.data.patientId, |
||||||
|
page: 1, |
||||||
|
pageSize: 1000, // 尽量拉全,避免导出/对比缺数据
|
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const list: CompareItem[] = (res.list || []) |
||||||
|
.slice() |
||||||
|
.sort((a, b) => String(a.recordDate).localeCompare(String(b.recordDate))) |
||||||
|
this.setData({ |
||||||
|
dataList: list, |
||||||
|
loading: false, |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比数据失败:', err) |
||||||
|
this.setData({ loading: false }) |
||||||
|
wx.showToast({ title: '获取对比数据失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 导出Excel - 直接使用下载链接
|
||||||
|
handleExport() { |
||||||
|
wx.showLoading({ title: '导出中...' }) |
||||||
|
|
||||||
|
// 构建导出URL:该接口为业务接口,优先使用 globalData.url
|
||||||
|
const baseUrl = String(app.globalData.url || '').replace(/\/$/, '') |
||||||
|
const exportUrl = `${baseUrl}/?r=xd/doctor/proptosis/export&patientId=${this.data.patientId}` |
||||||
|
|
||||||
|
wx.downloadFile({ |
||||||
|
url: exportUrl, |
||||||
|
success: (downloadRes) => { |
||||||
|
wx.hideLoading() |
||||||
|
if (downloadRes.statusCode === 200) { |
||||||
|
const filePath = downloadRes.tempFilePath |
||||||
|
// 保存到本地文件系统
|
||||||
|
const fs = wx.getFileSystemManager() |
||||||
|
const savedFilePath = `${wx.env.USER_DATA_PATH}/凸眼度对比_${new Date().toISOString().split('T')[0]}.xlsx` |
||||||
|
|
||||||
|
fs.saveFile({ |
||||||
|
tempFilePath: filePath, |
||||||
|
filePath: savedFilePath, |
||||||
|
success: () => { |
||||||
|
// 打开文档
|
||||||
|
wx.openDocument({ |
||||||
|
filePath: savedFilePath, |
||||||
|
fileType: 'xlsx', |
||||||
|
showMenu: true, |
||||||
|
success: () => { |
||||||
|
wx.showToast({ title: '导出成功', icon: 'success' }) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.error('打开文档失败:', err) |
||||||
|
wx.showToast({ title: '打开文件失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.error('保存文件失败:', err) |
||||||
|
// 直接打开临时文件
|
||||||
|
wx.openDocument({ |
||||||
|
filePath, |
||||||
|
fileType: 'xlsx', |
||||||
|
showMenu: true, |
||||||
|
success: () => { |
||||||
|
wx.showToast({ title: '导出成功', icon: 'success' }) |
||||||
|
}, |
||||||
|
fail: () => { |
||||||
|
wx.showToast({ title: '导出失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
else { |
||||||
|
wx.showToast({ title: '导出失败', icon: 'none' }) |
||||||
|
} |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
wx.hideLoading() |
||||||
|
console.error('下载失败:', err) |
||||||
|
wx.showToast({ title: '导出失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
export {} |
||||||
|
|||||||
@ -0,0 +1,6 @@ |
|||||||
|
{ |
||||||
|
"navigationBarTitleText": "对比图编辑", |
||||||
|
"usingComponents": { |
||||||
|
"imageMerge": "/patient/components/image-merge/index" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,163 @@ |
|||||||
|
page { |
||||||
|
background-color: #f6f8f9; |
||||||
|
} |
||||||
|
|
||||||
|
.page { |
||||||
|
padding-bottom: 160rpx; |
||||||
|
|
||||||
|
.container { |
||||||
|
margin: 40rpx 40rpx 0; |
||||||
|
|
||||||
|
.title { |
||||||
|
padding-bottom: 40rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
font-weight: bold; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
gap: 16rpx; |
||||||
|
|
||||||
|
.date { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #adacb2; |
||||||
|
font-weight: normal; |
||||||
|
align-self: flex-end; |
||||||
|
} |
||||||
|
|
||||||
|
&::before { |
||||||
|
content: ''; |
||||||
|
width: 8rpx; |
||||||
|
height: 32rpx; |
||||||
|
background: #b982ff; |
||||||
|
border-radius: 44rpx 44rpx 44rpx 44rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.card { |
||||||
|
display: flex; |
||||||
|
|
||||||
|
.aside { |
||||||
|
flex-shrink: 0; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
align-items: center; |
||||||
|
width: 32rpx; |
||||||
|
|
||||||
|
.circle { |
||||||
|
position: relative; |
||||||
|
width: 32rpx; |
||||||
|
height: 32rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background: rgba(185, 130, 255, 0.29); |
||||||
|
flex-shrink: 0; |
||||||
|
|
||||||
|
&::after { |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
left: 50%; |
||||||
|
transform: translate(-50%, -50%); |
||||||
|
display: block; |
||||||
|
content: ''; |
||||||
|
width: 18rpx; |
||||||
|
height: 18rpx; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: #b982ff; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.line-bottom { |
||||||
|
flex: 1; |
||||||
|
width: 2rpx; |
||||||
|
border-left: 2rpx dashed #b982ff; |
||||||
|
margin-top: 8rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.c-container { |
||||||
|
margin-top: -6rpx; |
||||||
|
margin-left: 14rpx; |
||||||
|
flex: 1; |
||||||
|
padding-bottom: 48rpx; |
||||||
|
|
||||||
|
.c-header { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
|
||||||
|
.date { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #211d2e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.tags { |
||||||
|
margin-top: 12rpx; |
||||||
|
|
||||||
|
.tag { |
||||||
|
display: inline-block; |
||||||
|
margin-right: 16rpx; |
||||||
|
padding: 2rpx 12rpx; |
||||||
|
font-size: 24rpx; |
||||||
|
line-height: 32rpx; |
||||||
|
border-radius: 6rpx; |
||||||
|
|
||||||
|
&.tag1 { |
||||||
|
background: rgba(255, 163, 0, 0.17); |
||||||
|
color: #ffa300; |
||||||
|
} |
||||||
|
|
||||||
|
&.tag2 { |
||||||
|
background: rgba(176, 115, 255, 0.16); |
||||||
|
color: #b073ff; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.photo-card { |
||||||
|
margin-top: 24rpx; |
||||||
|
|
||||||
|
.photo { |
||||||
|
border-radius: 32rpx; |
||||||
|
height: 352rpx; |
||||||
|
display: block; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.footer { |
||||||
|
position: fixed; |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
padding: 24rpx 40rpx 48rpx; |
||||||
|
background: #fff; |
||||||
|
display: flex; |
||||||
|
gap: 24rpx; |
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); |
||||||
|
|
||||||
|
.btn1, |
||||||
|
.btn2 { |
||||||
|
flex: 1; |
||||||
|
height: 88rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: center; |
||||||
|
border-radius: 100rpx; |
||||||
|
} |
||||||
|
|
||||||
|
.btn1 { |
||||||
|
color: #ffffff; |
||||||
|
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%); |
||||||
|
} |
||||||
|
|
||||||
|
.btn2 { |
||||||
|
color: #b073ff; |
||||||
|
background: rgba(176, 115, 255, 0.1); |
||||||
|
border: 2rpx solid #b073ff; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,156 @@ |
|||||||
|
const app = getApp<IAppOption>() |
||||||
|
|
||||||
|
interface PhotoItem { |
||||||
|
photoId: string |
||||||
|
recordId: string |
||||||
|
photoUrl: string |
||||||
|
recordDate: string |
||||||
|
isBaseline: number |
||||||
|
treatmentCount: number |
||||||
|
} |
||||||
|
|
||||||
|
Page({ |
||||||
|
data: { |
||||||
|
patientId: '', |
||||||
|
photoAngle: '', |
||||||
|
photoAngleName: '', |
||||||
|
recordIds: [] as string[], |
||||||
|
photos: [] as PhotoItem[], |
||||||
|
loading: false, |
||||||
|
}, |
||||||
|
|
||||||
|
onLoad(option: any) { |
||||||
|
this.setData({ |
||||||
|
patientId: option.patientId || '', |
||||||
|
photoAngle: option.photoAngle || '', |
||||||
|
recordIds: option.recordIds ? option.recordIds.split(',') : [], |
||||||
|
}) |
||||||
|
// 获取角度名称
|
||||||
|
this.getAngleName() |
||||||
|
// 获取对比照片
|
||||||
|
this.getComparePhotos() |
||||||
|
}, |
||||||
|
|
||||||
|
onShow() { |
||||||
|
app.waitLogin({ type: [2] }) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取角度名称
|
||||||
|
getAngleName() { |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/get-compare-angle', |
||||||
|
data: { |
||||||
|
patientId: this.data.patientId, |
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const angleMap = res.photoAngle || {} |
||||||
|
this.setData({ |
||||||
|
photoAngleName: angleMap[this.data.photoAngle] || '', |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取角度名称失败:', err) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取对比照片
|
||||||
|
getComparePhotos() { |
||||||
|
const { patientId, photoAngle, recordIds } = this.data |
||||||
|
if (!patientId || !photoAngle || recordIds.length === 0) { |
||||||
|
wx.showToast({ title: '参数错误', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.setData({ loading: true }) |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/doctor/proptosis/compare', |
||||||
|
data: { |
||||||
|
patientId, |
||||||
|
photoAngle, |
||||||
|
recordIds: recordIds.join(','), |
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const baselineRes = res.baseline |
||||||
|
const compareList = res.compareList || [] |
||||||
|
const photos: PhotoItem[] = [] |
||||||
|
if (baselineRes) { |
||||||
|
photos.push({ |
||||||
|
photoId: baselineRes.recordId, |
||||||
|
recordId: baselineRes.recordId, |
||||||
|
photoUrl: baselineRes.photoUrl, |
||||||
|
recordDate: baselineRes.recordDate, |
||||||
|
isBaseline: 1, |
||||||
|
treatmentCount: baselineRes.treatmentCount, |
||||||
|
}) |
||||||
|
} |
||||||
|
compareList.forEach((item: any) => { |
||||||
|
photos.push({ |
||||||
|
photoId: item.recordId, |
||||||
|
recordId: item.recordId, |
||||||
|
photoUrl: item.photoUrl, |
||||||
|
recordDate: item.recordDate, |
||||||
|
isBaseline: 0, |
||||||
|
treatmentCount: item.treatmentCount, |
||||||
|
}) |
||||||
|
}) |
||||||
|
this.setData({ |
||||||
|
photos, |
||||||
|
loading: false, |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比照片失败:', err) |
||||||
|
this.setData({ loading: false }) |
||||||
|
wx.showToast({ title: '获取照片失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 生成对比图预览
|
||||||
|
handleMergePreview() { |
||||||
|
const { photos, photoAngleName } = this.data |
||||||
|
if (photos.length === 0) { |
||||||
|
wx.showToast({ title: '暂无照片', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const mergeComponent = this.selectComponent('#merge') |
||||||
|
if (mergeComponent) { |
||||||
|
const imageList = photos.map((item) => { |
||||||
|
const label = item.isBaseline === 1 |
||||||
|
? `基准照片 ${item.recordDate}` |
||||||
|
: `对比照片 ${item.recordDate} 替妥尤单抗:${item.treatmentCount}次` |
||||||
|
return { |
||||||
|
src: item.photoUrl, |
||||||
|
time: label, |
||||||
|
} |
||||||
|
}) |
||||||
|
mergeComponent.mergeImages(imageList, { |
||||||
|
title: `${photoAngleName}时间线对比`, |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
// 保存成功回调
|
||||||
|
onMergeSave(e: any) { |
||||||
|
const { tempFilePath } = e.detail |
||||||
|
wx.saveImageToPhotosAlbum({ |
||||||
|
filePath: tempFilePath, |
||||||
|
success: () => { |
||||||
|
wx.showToast({ title: '保存成功', icon: 'success' }) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.error('保存失败:', err) |
||||||
|
wx.showToast({ title: '保存失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 合并失败回调
|
||||||
|
onMergeError(e: any) { |
||||||
|
const { error } = e.detail |
||||||
|
console.error('合并图片失败:', error) |
||||||
|
wx.showToast({ title: '生成对比图失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
|
||||||
|
export {} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
<view class="page"> |
||||||
|
<view class="container"> |
||||||
|
<view class="title"> |
||||||
|
{{photoAngleName}}时间线对比 |
||||||
|
<view class="date">生成日期:{{photos[0].recordDate}}</view> |
||||||
|
</view> |
||||||
|
<view class="card" wx:for="{{photos}}" wx:key="recordId"> |
||||||
|
<view class="aside"> |
||||||
|
<view class="circle"></view> |
||||||
|
<view class="line-bottom"></view> |
||||||
|
</view> |
||||||
|
<view class="c-container"> |
||||||
|
<view class="c-header"> |
||||||
|
<view class="date">{{item.isBaseline === 1 ? '基准照片' : '对比照片'}} {{item.recordDate}}</view> |
||||||
|
</view> |
||||||
|
<view class="tags"> |
||||||
|
<view wx:if="{{item.isBaseline === 1}}" class="tag tag1">基准照片</view> |
||||||
|
<view class="tag tag2">替妥尤单抗:{{item.treatmentCount}}</view> |
||||||
|
</view> |
||||||
|
<view class="photo-card"> |
||||||
|
<image class="photo" src="{{item.photoUrl}}" mode="aspectFill"></image> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="footer"> |
||||||
|
<view class="btn1" bind:tap="handleMergePreview">对比图预览</view> |
||||||
|
<view class="btn2" bind:tap="handleMergePreview">保存到相册</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
|
||||||
|
<imageMerge id="merge" bindsave="onMergeSave" binderror="onMergeError" /> |
||||||
@ -1,21 +1,23 @@ |
|||||||
<view class="page"> |
<view class="page"> |
||||||
<view class="total">共X条日记记录</view> |
<view class="total">共{{total}}条日记记录</view> |
||||||
<view class="history-list"> |
<view class="history-list"> |
||||||
<view class="list-item" bind:tap="handleHistory"> |
<view class="list-item" wx:for="{{recordList}}" wx:key="recordId" data-record-id="{{item.recordId}}" bind:tap="handleHistory"> |
||||||
<view class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%"> |
<view wx:if="{{item.isBaseline === 1}}" class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%"> |
||||||
基准照 |
基准照 |
||||||
</view> |
</view> |
||||||
<image class="photo" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image> |
<image class="photo" src="{{item.firstPhotoUrl || imageUrl + 'icon143.png?t=' + Timestamp}}"></image> |
||||||
<view class="wrap"> |
<view class="wrap"> |
||||||
<view class="date">2026-04-01</view> |
<view class="date">{{item.recordDate}}</view> |
||||||
<view class="tag">替妥尤单抗:2</view> |
<view class="tag">替妥尤单抗:{{item.treatmentCount}}</view> |
||||||
<view class="rotate">已上传1个角度</view> |
<view class="rotate">已上传{{item.photoCount}}个角度</view> |
||||||
</view> |
</view> |
||||||
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image> |
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image> |
||||||
</view> |
</view> |
||||||
</view> |
</view> |
||||||
|
<view class="loading" wx:if="{{loading}}">加载中...</view> |
||||||
|
<view class="no-more" wx:if="{{!hasMore && recordList.length > 0}}">没有更多了</view> |
||||||
<view class="footer"> |
<view class="footer"> |
||||||
<view class="btn1" bind:tap="handleDiffData">眼突度 对比</view> |
<view class="btn1" bind:tap="handleDiffData">眼突度 对比</view> |
||||||
<view class="btn2">照片 对比</view> |
<view class="btn2" bind:tap="handlePhotoCompare">照片 对比</view> |
||||||
</view> |
</view> |
||||||
</view> |
</view> |
||||||
|
|||||||
@ -1,19 +1,179 @@ |
|||||||
const _app = getApp<IAppOption>() |
const app = getApp<IAppOption>() |
||||||
|
|
||||||
|
interface PhotoItem { |
||||||
|
photoId: string |
||||||
|
photoUrl: string |
||||||
|
recordDate: string |
||||||
|
isBaseline: number |
||||||
|
treatmentCount: number |
||||||
|
leftEye: number |
||||||
|
rightEye: number |
||||||
|
interorbitalDistance: number |
||||||
|
isCropped?: boolean |
||||||
|
croppedUrl?: string |
||||||
|
} |
||||||
|
|
||||||
Page({ |
Page({ |
||||||
|
data: { |
||||||
|
photoAngle: '', |
||||||
|
photoAngleName: '', |
||||||
|
recordIds: [] as string[], |
||||||
|
photos: [] as PhotoItem[], |
||||||
|
loading: false, |
||||||
|
mergedImage: '', |
||||||
|
}, |
||||||
|
|
||||||
|
onLoad(option: any) { |
||||||
|
this.setData({ |
||||||
|
photoAngle: option.photoAngle || '', |
||||||
|
recordIds: option.recordIds ? option.recordIds.split(',') : [], |
||||||
|
}) |
||||||
|
// 获取角度名称
|
||||||
|
this.getAngleName() |
||||||
|
// 获取对比照片
|
||||||
|
this.getComparePhotos() |
||||||
|
}, |
||||||
|
|
||||||
|
onShow() { |
||||||
|
app.waitLogin({ type: [1] }) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取角度名称
|
||||||
|
getAngleName() { |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/proptosis/get-compare-angle', |
||||||
data: {}, |
data: {}, |
||||||
onLoad() {}, |
}).then((res: any) => { |
||||||
|
const angleMap = res.photoAngle || {} |
||||||
|
this.setData({ |
||||||
|
photoAngleName: angleMap[this.data.photoAngle] || '', |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取角度名称失败:', err) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 获取对比照片
|
||||||
|
getComparePhotos() { |
||||||
|
const { photoAngle, recordIds } = this.data |
||||||
|
if (!photoAngle || recordIds.length === 0) { |
||||||
|
wx.showToast({ title: '参数错误', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.setData({ loading: true }) |
||||||
|
wx.ajax({ |
||||||
|
method: 'GET', |
||||||
|
url: '?r=xd/proptosis/compare-photos', |
||||||
|
data: { |
||||||
|
photoAngle, |
||||||
|
recordIds: recordIds.join(','), |
||||||
|
}, |
||||||
|
}).then((res: any) => { |
||||||
|
const photos = (res.photos || []).map((item: any) => ({ |
||||||
|
photoId: item.photoId || item.recordId, |
||||||
|
photoUrl: item.photoUrl, |
||||||
|
recordDate: item.recordDate, |
||||||
|
isBaseline: item.isBaseline, |
||||||
|
treatmentCount: item.treatmentCount, |
||||||
|
leftEye: item.leftEye, |
||||||
|
rightEye: item.rightEye, |
||||||
|
interorbitalDistance: item.interorbitalDistance, |
||||||
|
isCropped: false, |
||||||
|
croppedUrl: '', |
||||||
|
})) |
||||||
|
this.setData({ |
||||||
|
photos, |
||||||
|
loading: false, |
||||||
|
}) |
||||||
|
}).catch((err) => { |
||||||
|
console.error('获取对比照片失败:', err) |
||||||
|
this.setData({ loading: false }) |
||||||
|
wx.showToast({ title: '获取照片失败', icon: 'none' }) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 点击裁剪
|
||||||
|
handleCrop(e: any) { |
||||||
|
const index = e.currentTarget.dataset.index |
||||||
|
const photos = this.data.photos |
||||||
|
const photo = photos[index] |
||||||
|
|
||||||
|
wx.navigateTo({ |
||||||
|
url: `/patient/pages/noteCrop/index?photoId=${photo.photoId}&photoUrl=${photo.photoUrl}&index=${index}`, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 点击还原
|
||||||
|
handleRestore(e: any) { |
||||||
|
const index = e.currentTarget.dataset.index |
||||||
|
const photos = this.data.photos |
||||||
|
photos[index].isCropped = false |
||||||
|
photos[index].croppedUrl = '' |
||||||
|
this.setData({ photos }) |
||||||
|
}, |
||||||
|
|
||||||
|
// 生成对比图预览
|
||||||
handleMergePreview() { |
handleMergePreview() { |
||||||
|
const { photos } = this.data |
||||||
|
if (photos.length === 0) { |
||||||
|
wx.showToast({ title: '暂无照片', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
const mergeComponent = this.selectComponent('#merge') |
const mergeComponent = this.selectComponent('#merge') |
||||||
console.log("DEBUGPRINT[224]: index.ts:7: mergeComponent=", mergeComponent) |
|
||||||
if (mergeComponent) { |
if (mergeComponent) { |
||||||
mergeComponent.mergeImages([ |
const imageList = photos.map((item) => { |
||||||
{ src: 'https://picsum.photos/400/300', time: '2024-01-01 10:00' }, |
const label = item.isBaseline === 1 |
||||||
{ src: 'https://picsum.photos/400/301', time: '2024-01-01 12:00' }, |
? `基准照片 ${item.recordDate}` |
||||||
{ src: 'https://picsum.photos/400/302' }, |
: `对比照片 ${item.recordDate} 替妥尤单抗:${item.treatmentCount}次` |
||||||
]) |
return { |
||||||
|
src: item.croppedUrl || item.photoUrl, |
||||||
|
time: label, |
||||||
|
} |
||||||
|
}) |
||||||
|
mergeComponent.mergeImages(imageList) |
||||||
} |
} |
||||||
}, |
}, |
||||||
|
|
||||||
|
// 保存到相册
|
||||||
|
handleSaveAlbum() { |
||||||
|
const { mergedImage } = this.data |
||||||
|
if (!mergedImage) { |
||||||
|
wx.showToast({ title: '请先生成对比图', icon: 'none' }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
wx.saveImageToPhotosAlbum({ |
||||||
|
filePath: mergedImage, |
||||||
|
success: () => { |
||||||
|
wx.showToast({ title: '保存成功', icon: 'success' }) |
||||||
|
}, |
||||||
|
fail: (err) => { |
||||||
|
console.error('保存失败:', err) |
||||||
|
wx.showToast({ title: '保存失败', icon: 'none' }) |
||||||
|
}, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 保存成功回调
|
||||||
|
onMergeSave(e: any) { |
||||||
|
const { tempFilePath } = e.detail |
||||||
|
this.setData({ mergedImage: tempFilePath }) |
||||||
|
wx.previewImage({ |
||||||
|
urls: [tempFilePath], |
||||||
|
current: tempFilePath, |
||||||
|
showmenu: true, |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
// 合并失败回调
|
||||||
|
onMergeError(e: any) { |
||||||
|
const { reason } = e.detail |
||||||
|
console.error('合并图片失败:', reason) |
||||||
|
wx.showToast({ title: '生成对比图失败', icon: 'none' }) |
||||||
|
}, |
||||||
}) |
}) |
||||||
|
|
||||||
export {} |
export {} |
||||||
|
|||||||
Loading…
Reference in new issue