Browse Source

feat: 新增照片对比功能及相关页面

refactor: 重构患者详情、记录列表和详情页

fix: 修复样式问题和类型定义

docs: 更新AGENTS.md文档说明

style: 统一代码格式和颜色值写法

test: 添加相关接口测试用例

chore: 更新依赖和配置文件
dev
kola-web 3 weeks ago
parent
commit
6aca21f0bf
  1. 240
      .trae/documents/proptosis_api_integration_plan.md
  2. 4
      .vscode/settings.json
  3. 37
      AGENTS.md
  4. 3
      README.md
  5. 6
      eslint.config.js
  6. 2
      project.config.json
  7. 2
      project.private.config.json
  8. 4
      src/app.json
  9. 4
      src/custom-tab-bar/index.scss
  10. 192
      src/pages/d_noteDetail/index.ts
  11. 39
      src/pages/d_noteDetail/index.wxml
  12. 4
      src/pages/d_noteDiff/index.json
  13. 275
      src/pages/d_noteDiff/index.scss
  14. 212
      src/pages/d_noteDiff/index.ts
  15. 81
      src/pages/d_noteDiff/index.wxml
  16. 137
      src/pages/d_noteDiffData/index.ts
  17. 17
      src/pages/d_noteDiffData/index.wxml
  18. 6
      src/pages/d_noteDiffEdit/index.json
  19. 163
      src/pages/d_noteDiffEdit/index.scss
  20. 156
      src/pages/d_noteDiffEdit/index.ts
  21. 32
      src/pages/d_noteDiffEdit/index.wxml
  22. 165
      src/pages/d_noteList/index.ts
  23. 18
      src/pages/d_noteList/index.wxml
  24. 2
      src/pages/d_patient/index.scss
  25. 9
      src/pages/d_patient/index.ts
  26. 2
      src/pages/d_patientDetail/index.scss
  27. 25
      src/pages/d_patientDetail/index.ts
  28. 167
      src/patient/components/camera/index.ts
  29. 6
      src/patient/components/camera/index.wxml
  30. 2
      src/patient/components/image-crop/index.json
  31. 2
      src/patient/components/image-merge/index.json
  32. 11
      src/patient/pages/note/index.scss
  33. 126
      src/patient/pages/note/index.ts
  34. 38
      src/patient/pages/note/index.wxml
  35. 496
      src/patient/pages/noteAdd/index.ts
  36. 76
      src/patient/pages/noteAdd/index.wxml
  37. 183
      src/patient/pages/noteDiff/index.ts
  38. 42
      src/patient/pages/noteDiff/index.wxml
  39. 122
      src/patient/pages/noteDiffEdit/index.scss
  40. 178
      src/patient/pages/noteDiffEdit/index.ts
  41. 38
      src/patient/pages/noteDiffEdit/index.wxml
  42. 4
      src/patient/pages/noteHistory/index.scss
  43. 240
      src/patient/pages/noteHistory/index.ts
  44. 54
      src/patient/pages/noteHistory/index.wxml
  45. 6
      src/sitemap.json
  46. 8
      typings/index.d.ts
  47. 675
      接口文档.md

240
.trae/documents/proptosis_api_integration_plan.md

@ -0,0 +1,240 @@ @@ -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或自家服务器)

4
.vscode/settings.json vendored

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{
"path-autocomplete.pathMappings": {
"@": "${folder}/src",
"/": "${folder}/src",
"/": "${folder}/src"
},
"emmet.preferences": {
"css.intUnit": "rpx",
@ -9,5 +9,5 @@ @@ -9,5 +9,5 @@
},
"css.customData": [
".vscode/css-data.json"
],
]
}

37
AGENTS.md

@ -28,11 +28,14 @@ src/ @@ -28,11 +28,14 @@ src/
## Key Conventions
### Path Aliases
- `@/*``src/*` (configured in tsconfig.json and app.json `resolveAlias`)
- Use `@/utils/request` not relative paths
### User Types & Routing
Login types enforced in `app.ts`:
- `0`: Not logged in
- `1`: Patient (患者) → routes to `/patient/pages/*`
- `2`: Doctor (医生) → routes to `/pages/*`
@ -40,13 +43,17 @@ Login types enforced in `app.ts`: @@ -40,13 +43,17 @@ Login types enforced in `app.ts`:
Use `app.zdWaitLogin()` to guard pages that require login.
### Page/Component Wrappers
`app.ts` overrides global `Page` and `Component` with wrappers from `utils/page.ts` and `utils/component.ts`:
- Auto-sets `imageUrl` and `Timestamp` on page load
- Auto-handles navbar background on scroll
- Provides default share behavior
### Modal Colors (Required)
All `wx.showModal` must use:
```ts
wx.showModal({
confirmColor: '#8c75d0',
@ -54,6 +61,29 @@ wx.showModal({ @@ -54,6 +61,29 @@ wx.showModal({
})
```
### App Instance (Required)
`getApp()` should be called at the top of the file, not inside methods:
````ts
// ✅ Correct
const app = getApp<IAppOption>()
Page({
onLoad() {
// Use app directly
console.log(app.globalData)
}
})
// ❌ Incorrect
Page({
onLoad() {
const app = getApp<IAppOption>()
console.log(app.globalData)
}
})```
## Available Commands
```bash
@ -66,13 +96,14 @@ pnpm install @@ -66,13 +96,14 @@ pnpm install
# Build: Use WeChat Developer Tools
# - Import project with src/ as root
# - project.config.json at repo root
```
````
## NPM Dependencies
**Critical**: After `npm install`, run **Tools → Build npm** in WeChat Developer Tools to generate `miniprogram_npm/`. This is required for Vant and other packages.
Key dependencies:
- `@vant/weapp`: UI components
- `miniprogram-licia`: Utility library (available as `licia`)
- `dayjs`: Date handling
@ -81,12 +112,14 @@ Key dependencies: @@ -81,12 +112,14 @@ Key dependencies:
## Images & Assets
**Images are stored in SVN, not git.**
- SVN URL: `svn://39.106.86.127:28386/projects/xd/proj_src/shop/frontend/web/xd/`
- Local path: `src/images/` (gitignored)
- Excluded from upload via `project.config.json` packOptions
- Only `/images/tabbar/*` is included in uploads
Image URL pattern:
```
{{imageUrl}}/path/to/image.png?t={{Timestamp}}
```
@ -99,6 +132,7 @@ Image URL pattern: @@ -99,6 +132,7 @@ Image URL pattern:
- Types: `miniprogram-api-typings` for WX API
Global types in `typings/index.d.ts`:
- `IAppOption`: Global app instance interface
- `pageType`: 0 | 1 | 2 for user types
- `wx.ajax`: Extended request method
@ -113,6 +147,7 @@ Global types in `typings/index.d.ts`: @@ -113,6 +147,7 @@ Global types in `typings/index.d.ts`:
## Environment Configuration
Selected by App ID in `src/config.ts`:
- `wxf9ce8010f1ad24aa`: Dev/Staging (hbraas.com)
- `wx71ac9c27c3c3e3f4`: Production (hbsaas.com)

3
README.md

@ -49,9 +49,8 @@ wx.showModal({ @@ -49,9 +49,8 @@ wx.showModal({
正整数 2
正整数 3
已修改未上线内容
1. 医生端首页 统计数据改为实时更新
2. 就诊地图重新定位,在非北京地区重新定位会重新定位到北京,再次切换才会定位到当地
3. 患者端注册页导航栏增加返回上一页按钮

6
eslint.config.js

@ -4,6 +4,10 @@ export default antfu({ @@ -4,6 +4,10 @@ export default antfu({
env: {
es6: true,
},
ignores: [
'**/miniprogram_npm/**',
'src/miniprogram_npm/**',
],
parserOptions: { project: ['./tsconfig.json'] },
globals: {
wx: true,
@ -18,7 +22,7 @@ export default antfu({ @@ -18,7 +22,7 @@ export default antfu({
rules: {
'eslint-comments/no-unlimited-disable': 'off',
'ts/no-require-imports': 'off',
eqeqeq: 'off',
'eqeqeq': 'off',
},
formatters: {
css: 'prettier',

2
project.config.json

@ -72,4 +72,4 @@ @@ -72,4 +72,4 @@
]
},
"appid": "wxf9ce8010f1ad24aa"
}
}

2
project.private.config.json

@ -320,4 +320,4 @@ @@ -320,4 +320,4 @@
}
},
"libVersion": "3.8.9"
}
}

4
src/app.json

@ -19,7 +19,9 @@ @@ -19,7 +19,9 @@
"pages/d_qolDetail/index",
"pages/d_noteDetail/index",
"pages/d_noteList/index",
"pages/d_noteDiffData/index"
"pages/d_noteDiffData/index",
"pages/d_noteDiff/index",
"pages/d_noteDiffEdit/index"
],
"subPackages": [
{

4
src/custom-tab-bar/index.scss

@ -40,7 +40,7 @@ @@ -40,7 +40,7 @@
.name {
margin-top: 8rpx;
font-size: 22rpx;
color: #69686E;
color: #69686e;
line-height: 21rpx;
}
&.active {
@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
}
}
.name {
color: #211D2E;
color: #211d2e;
}
}
}

192
src/pages/d_noteDetail/index.ts

@ -1,86 +1,130 @@ @@ -1,86 +1,130 @@
const _app = getApp<IAppOption>()
const app = getApp<IAppOption>()
interface Photo {
photoId: string
photoAngle: string
photoAngleName: string
photoUrl: string
uploadTime: string
}
interface RecordDetail {
recordId: string
userId: string
patientId: string
recordDate: string
treatmentCount: number
isBaseline: number
leftEye: number
rightEye: number
interorbitalDistance: number
uploadCompleted: number
photos: Photo[]
}
Page({
data: {
history: {
frontend: [
{
title: '正面睁眼照',
content: '平视,目光看向镜头方向,自然睁眼,不眯眼、不瞪眼。',
},
{
title: '正面闭眼照',
content: '正对镜头,面部居中,自然放松,双眼轻轻闭合,不皱眉、不挤眼。',
},
{
title: '正面仰头照',
content: '拍摄时,正对镜头,面部居中,头部自然向上仰至约 45°,双眼同步平视,保持自然睁眼、不眯眼。',
},
],
backend: [
{
title: '左侧-90°',
content: '身体与头部完全转向右侧,呈标准 90° 侧面,仅可见左侧眼睛。',
},
{
title: '右侧-90°',
content: '身体与头部完全转向左侧,呈标准 90° 侧面,仅可见右侧眼睛。',
},
{
title: '左侧-45°',
content: '身体与头部转向右前方 45°。',
},
{
title: '右侧-45°',
content: '身体与头部转向左前方 45°',
},
],
other: [
{
title: '正面眼睛上看',
content: '',
},
{
title: '正面眼睛下看',
content: '',
},
{
title: '正面眼睛左看',
content: '',
},
{
title: '正面眼睛右看',
content: '',
},
{
title: '正面眼睛左上看',
content: '',
},
{
title: '正面眼睛右上看',
content: '',
},
{
title: '正面眼睛左下看',
content: '',
},
{
title: '正面眼睛右下看',
content: '',
},
],
patientId: '',
recordId: '',
recordDetail: {} as RecordDetail,
// 角度分组
angleGroups: {
frontend: ['front_open', 'front_closed', 'front_looking_up'],
backend: ['side_left_90', 'side_right_90', 'side_left_45', 'side_right_45'],
other: ['eye_up_left', 'eye_up', 'eye_up_right', 'eye_left', 'eye_right', 'eye_down_left', 'eye_down', 'eye_down_right'],
},
// 角度名称映射
angleNameMap: {
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: '正面眼睛右下',
} as Record<string, string>,
},
onLoad() {},
handlePopupOk() {
onLoad(option: any) {
this.setData({
popupShow: false,
patientId: option.patientId || '',
recordId: option.recordId || '',
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
onShow() {
app.waitLogin({ type: [2] }).then(() => {
if (this.data.recordId) {
this.getRecordDetail()
}
})
},
// 获取记录详情
getRecordDetail() {
wx.showLoading({ title: '加载中...' })
wx.ajax({
method: 'GET',
url: '?r=xd/doctor/proptosis/detail',
data: {
recordId: this.data.recordId,
},
}).then((res: any) => {
wx.hideLoading()
this.setData({
recordDetail: res,
})
}).catch((err) => {
wx.hideLoading()
console.error('获取记录详情失败:', err)
wx.showToast({ title: '加载失败', icon: 'none' })
})
},
// 获取指定角度的照片
getPhotoByAngle(angle: string): Photo | undefined {
const photos = this.data.recordDetail.photos || []
return photos.find((p: Photo) => p.photoAngle === angle)
},
// 返回上一页
handleBack() {
wx.navigateBack()
},
// 预览图片
handlePreview(e: any) {
const { angle } = e.currentTarget.dataset
const photo = this.getPhotoByAngle(angle)
if (photo) {
wx.previewImage({
urls: [photo.photoUrl],
current: photo.photoUrl,
})
}
},
// 眼突度对比
handleDiffData() {
wx.navigateTo({
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}`,
})
},
// 照片对比
handlePhotoCompare() {
wx.navigateTo({
url: `/pages/d_noteDiff/index?patientId=${this.data.patientId}`,
})
},
})

39
src/pages/d_noteDetail/index.wxml

@ -7,33 +7,33 @@ @@ -7,33 +7,33 @@
style="background: url('{{imageUrl}}bg10.png?t={{Timestamp}}') no-repeat top center/100% 610rpx;padding-top:{{pageTop+16}}px;"
>
<view class="container">
<view class="benchmark" style="background: url('/images/bg56.png') no-repeat top center/100%">基准照</view>
<view class="date">2026-04-02</view>
<view wx:if="{{recordDetail.isBaseline === 1}}" class="benchmark" style="background: url('/images/bg56.png') no-repeat top center/100%">基准照</view>
<view class="date">{{recordDetail.recordDate}}</view>
<view class="total">
当前记录对应的替妥尤单抗使用次数:
<text class="num">1</text>
<text class="num">{{recordDetail.treatmentCount}}</text>
</view>
<view class="banner">
<view class="banner" wx:if="{{recordDetail.leftEye || recordDetail.rightEye || recordDetail.interorbitalDistance}}">
<view class="title">眼球突出度测量记录</view>
<view class="row">
<view class="col">
<view class="name">右眼</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.rightEye || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">眶间距</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.interorbitalDistance || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">左眼</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.leftEye || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
@ -42,33 +42,36 @@ @@ -42,33 +42,36 @@
<view class="card">
<view class="card-title">正面</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.frontend}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.frontend}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<view class="card">
<view class="card-title">侧面</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.backend}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.backend}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<view class="card">
<view class="card-title">眼球运动八个方向</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.other}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.other}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
</view>
<view class="footer">
<view class="btn1">眼突度 对比</view>
<view class="btn2">照片 对比</view>
<view class="btn1" bind:tap="handleDiffData">眼突度 对比</view>
<view class="btn2" bind:tap="handlePhotoCompare">照片 对比</view>
</view>
</view>

4
src/pages/d_noteDiff/index.json

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
{
"navigationBarTitleText": "照片对比",
"usingComponents": {}
}

275
src/pages/d_noteDiff/index.scss

@ -0,0 +1,275 @@ @@ -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;
}
}
}
}

212
src/pages/d_noteDiff/index.ts

@ -0,0 +1,212 @@ @@ -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 {}

81
src/pages/d_noteDiff/index.wxml

@ -0,0 +1,81 @@ @@ -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>

137
src/pages/d_noteDiffData/index.ts

@ -1,25 +1,126 @@ @@ -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({
data: {
dataList: [
{
date: '2026.1.5',
left: '40',
spacing: '40',
right: '40',
count: '30',
patientId: '',
dataList: [] as CompareItem[],
loading: false,
},
onLoad(option: any) {
this.setData({
patientId: option.patientId || '',
})
},
onShow() {
app.waitLogin({ type: [2] }).then(() => {
if (this.data.patientId) {
this.getCompareData()
}
})
},
// 获取对比数据
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' })
}
},
{
date: '2025.12.30',
left: '30',
spacing: '30',
right: '30',
count: '20',
fail: (err) => {
wx.hideLoading()
console.error('下载失败:', err)
wx.showToast({ title: '导出失败', icon: 'none' })
},
],
})
},
onLoad() {},
});
})
export {};
export {}

17
src/pages/d_noteDiffData/index.wxml

@ -21,15 +21,20 @@ @@ -21,15 +21,20 @@
<!-- 表格内容 -->
<view class="table-body">
<view class="tr" wx:for="{{dataList}}" wx:key="date">
<view class="td td-date">{{item.date}}</view>
<view class="tr" wx:for="{{dataList}}" wx:key="recordId">
<view class="td td-date">{{item.recordDate}}</view>
<view class="td td-eye">
<view class="td-eye-item">{{item.left}}</view>
<view class="td-eye-item">{{item.spacing}}</view>
<view class="td-eye-item">{{item.right}}</view>
<view class="td-eye-item">{{item.leftEye || '--'}}</view>
<view class="td-eye-item">{{item.interorbitalDistance || '--'}}</view>
<view class="td-eye-item">{{item.rightEye || '--'}}</view>
</view>
<view class="td td-count">{{item.count}}</view>
<view class="td td-count">{{item.treatmentCount}}</view>
</view>
</view>
</view>
<!-- 导出按钮 -->
<view class="footer">
<view class="export-btn" bind:tap="handleExport">导出Excel</view>
</view>
</view>

6
src/pages/d_noteDiffEdit/index.json

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
{
"navigationBarTitleText": "对比图编辑",
"usingComponents": {
"imageMerge": "/patient/components/image-merge/index"
}
}

163
src/pages/d_noteDiffEdit/index.scss

@ -0,0 +1,163 @@ @@ -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;
}
}
}
}

156
src/pages/d_noteDiffEdit/index.ts

@ -0,0 +1,156 @@ @@ -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 {}

32
src/pages/d_noteDiffEdit/index.wxml

@ -0,0 +1,32 @@ @@ -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" />

165
src/pages/d_noteList/index.ts

@ -1,91 +1,100 @@ @@ -1,91 +1,100 @@
const _app = getApp<IAppOption>()
const app = getApp<IAppOption>()
interface RecordItem {
recordId: string
recordDate: string
treatmentCount: number
leftEye: number
rightEye: number
interorbitalDistance: number
isBaseline: number
photoCount: number
firstPhotoUrl: string
uploadCompleted: number
}
Page({
data: {
history: {
frontend: [
{
title: '正面睁眼照',
content: '平视,目光看向镜头方向,自然睁眼,不眯眼、不瞪眼。',
},
{
title: '正面闭眼照',
content: '正对镜头,面部居中,自然放松,双眼轻轻闭合,不皱眉、不挤眼。',
},
{
title: '正面仰头照',
content: '拍摄时,正对镜头,面部居中,头部自然向上仰至约 45°,双眼同步平视,保持自然睁眼、不眯眼。',
},
],
backend: [
{
title: '左侧-90°',
content: '身体与头部完全转向右侧,呈标准 90° 侧面,仅可见左侧眼睛。',
},
{
title: '右侧-90°',
content: '身体与头部完全转向左侧,呈标准 90° 侧面,仅可见右侧眼睛。',
},
{
title: '左侧-45°',
content: '身体与头部转向右前方 45°。',
},
{
title: '右侧-45°',
content: '身体与头部转向左前方 45°',
},
],
other: [
{
title: '正面眼睛上看',
content: '',
},
{
title: '正面眼睛下看',
content: '',
},
{
title: '正面眼睛左看',
content: '',
},
{
title: '正面眼睛右看',
content: '',
},
{
title: '正面眼睛左上看',
content: '',
},
{
title: '正面眼睛右上看',
content: '',
},
{
title: '正面眼睛左下看',
content: '',
},
{
title: '正面眼睛右下看',
content: '',
},
],
},
patientId: '',
recordList: [] as RecordItem[],
total: 0,
page: 1,
pageSize: 10,
loading: false,
hasMore: true,
},
onLoad() {},
handleDiffData() {
onLoad(option: any) {
this.setData({
patientId: option.patientId || '',
})
},
onShow() {
app.waitLogin({ type: [2] }).then(() => {
this.getRecordList(true)
})
},
// 获取患者突眼记录列表
getRecordList(reset = false) {
if (this.data.loading)
return
if (!reset && !this.data.hasMore)
return
const page = reset ? 1 : this.data.page
this.setData({ loading: true })
wx.ajax({
method: 'GET',
url: '?r=xd/doctor/proptosis/list',
data: {
patientId: this.data.patientId,
page,
pageSize: this.data.pageSize,
},
}).then((res: any) => {
const list = res.list || []
const total = res.pagination?.total || 0
this.setData({
recordList: reset ? list : [...this.data.recordList, ...list],
total,
page: page + 1,
hasMore: list.length >= this.data.pageSize,
loading: false,
})
}).catch((err) => {
console.error('获取记录列表失败:', err)
this.setData({ loading: false })
})
},
// 加载更多
loadMore() {
this.getRecordList()
},
// 查看记录详情
handleHistory(e: any) {
const { recordId } = e.currentTarget.dataset
wx.navigateTo({
url: '/pages/d_noteDiffData/index',
url: `/pages/d_noteDetail/index?recordId=${recordId}&patientId=${this.data.patientId}`,
})
},
handlePopupOk() {
this.setData({
popupShow: false,
// 眼突度对比
handleDiffData() {
wx.navigateTo({
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}`,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
// 照片对比
handlePhotoCompare() {
wx.navigateTo({
url: `/pages/d_noteDiff/index?patientId=${this.data.patientId}`,
})
},
})

18
src/pages/d_noteList/index.wxml

@ -1,21 +1,23 @@ @@ -1,21 +1,23 @@
<view class="page">
<view class="total">共X条日记记录</view>
<view class="total">共{{total}}条日记记录</view>
<view class="history-list">
<view class="list-item" bind:tap="handleHistory">
<view class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%">
<view class="list-item" wx:for="{{recordList}}" wx:key="recordId" data-record-id="{{item.recordId}}" bind:tap="handleHistory">
<view wx:if="{{item.isBaseline === 1}}" class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%">
基准照
</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="date">2026-04-01</view>
<view class="tag">替妥尤单抗:2</view>
<view class="rotate">已上传1个角度</view>
<view class="date">{{item.recordDate}}</view>
<view class="tag">替妥尤单抗:{{item.treatmentCount}}</view>
<view class="rotate">已上传{{item.photoCount}}个角度</view>
</view>
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image>
</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="btn1" bind:tap="handleDiffData">眼突度 对比</view>
<view class="btn2">照片 对比</view>
<view class="btn2" bind:tap="handlePhotoCompare">照片 对比</view>
</view>
</view>

2
src/pages/d_patient/index.scss

@ -180,7 +180,7 @@ page { @@ -180,7 +180,7 @@ page {
.send {
padding: 6rpx 24rpx;
font-size: 28rpx;
color: #B982FF;
color: #b982ff;
line-height: 36rpx;
border-radius: 32rpx 32rpx 32rpx 32rpx;
border: 2rpx solid rgba(185, 130, 255, 1);

9
src/pages/d_patient/index.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import dayjs from 'dayjs'
const filterColumns = {}
const app = getApp<IAppOption>()
@ -59,7 +60,7 @@ Page({ @@ -59,7 +60,7 @@ Page({
},
onShow() {
app.waitLogin().then((_res) => {
app.mpBehavior({ doctor:true, PageName: 'PG_DoctorPatientList' })
app.mpBehavior({ doctor: true, PageName: 'PG_DoctorPatientList' })
this.getList()
app.getUserInfo(2).then((userInfo) => {
this.setData({
@ -99,7 +100,7 @@ Page({ @@ -99,7 +100,7 @@ Page({
this.getList()
},
handleTapSearch() {
app.mpBehavior({ doctor:true, PageName: 'BTN_DoctorPatientListSearch' })
app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorPatientListSearch' })
},
handleNav(e) {
const { active } = e.currentTarget.dataset
@ -185,7 +186,7 @@ Page({ @@ -185,7 +186,7 @@ Page({
})
},
handleDetail(e: any) {
app.mpBehavior({ doctor:true, PageName: 'BTN_DoctorPatientList' })
app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorPatientList' })
const { index } = e.currentTarget.dataset
const item = this.data.list[index]
@ -194,7 +195,7 @@ Page({ @@ -194,7 +195,7 @@ Page({
})
},
handleSend(e) {
app.mpBehavior({ doctor:true, PageName: 'BTN_DoctorPatientListSendMessage' })
app.mpBehavior({ doctor: true, PageName: 'BTN_DoctorPatientListSendMessage' })
const { index } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/d_interactiveDoctor/index?patientId=${this.data.list[index].PatientId}`,

2
src/pages/d_patientDetail/index.scss

@ -229,7 +229,7 @@ page { @@ -229,7 +229,7 @@ page {
margin-top: 24rpx;
height: 76rpx;
font-size: 32rpx;
color: #FFFFFF;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;

25
src/pages/d_patientDetail/index.ts

@ -11,6 +11,7 @@ Page({ @@ -11,6 +11,7 @@ Page({
hospital: {},
qolDetail: {},
hormoneDetail: {},
proptosisDetail: {}, // 突眼记录详情
id: '',
remark: '',
@ -201,6 +202,7 @@ Page({ @@ -201,6 +202,7 @@ Page({
this.handleHormoneTypeChange()
this.getQolDetail()
this.getHormoneDetail()
this.getProptosisDetail()
await this.getDetail()
app.getUserInfo(2).then((userInfo) => {
this.setData({
@ -249,6 +251,25 @@ Page({ @@ -249,6 +251,25 @@ Page({
})
})
},
// 获取患者最近的突眼记录
getProptosisDetail() {
wx.ajax({
method: 'GET',
url: '?r=xd/doctor/proptosis/latest',
data: {
patientId: this.data.id,
},
}).then((res: any) => {
this.setData({
proptosisDetail: {
...res,
recordDate: res.recordDate ? dayjs(res.recordDate).format('YYYY-MM-DD') : '',
},
})
}).catch((err) => {
console.error('获取突眼记录失败:', err)
})
},
getDoctorDetail() {
wx.ajax({
method: 'GET',
@ -894,12 +915,12 @@ Page({ @@ -894,12 +915,12 @@ Page({
},
handleNoteDetail() {
wx.navigateTo({
url: '/pages/d_noteDetail/index',
url: `/pages/d_noteDetail/index?patientId=${this.data.detail.PatientId}`,
})
},
handleNoteList() {
wx.navigateTo({
url: '/pages/d_noteList/index',
url: `/pages/d_noteList/index?patientId=${this.data.detail.PatientId}`,
})
},
})

167
src/patient/components/camera/index.ts

@ -1,9 +1,26 @@ @@ -1,9 +1,26 @@
const app = getApp<IAppOption>()
Component({
properties: {
defaultPosition: {
type: String,
value: 'back',
},
// 照片角度,用于调用 photo-upload 接口
photoAngle: {
type: String,
value: '',
},
// 记录ID(编辑/补拍时用于直接关联照片)
recordId: {
type: String,
value: '',
},
// 是否只使用相机拍摄(不显示相册选项)
onlyCamera: {
type: Boolean,
value: false,
},
},
data: {
@ -11,11 +28,30 @@ Component({ @@ -11,11 +28,30 @@ Component({
devicePosition: 'back',
flash: 'off' as 'off' | 'auto' | 'on',
previewImage: '',
sessionIdCache: '',
selectShow: false,
exampleShow: false,
type: 1,
// 拍摄位置名称映射
typeNameMap: {
1: { name: '正面睁眼', group: '正面', index: 1, total: 3 },
2: { name: '正面闭眼', group: '正面', index: 2, total: 3 },
3: { name: '正面仰头', group: '正面', index: 3, total: 3 },
4: { name: '左侧-90°', group: '侧面', index: 1, total: 4 },
5: { name: '右侧-90°', group: '侧面', index: 2, total: 4 },
6: { name: '左侧-45°', group: '侧面', index: 3, total: 4 },
7: { name: '右侧-45°', group: '侧面', index: 4, total: 4 },
8: { name: '左上', group: '眼球运动', index: 1, total: 8 },
9: { name: '向上', group: '眼球运动', index: 2, total: 8 },
10: { name: '右上', group: '眼球运动', index: 3, total: 8 },
11: { name: '向左', group: '眼球运动', index: 4, total: 8 },
12: { name: '向右', group: '眼球运动', index: 5, total: 8 },
13: { name: '左下', group: '眼球运动', index: 6, total: 8 },
14: { name: '向下', group: '眼球运动', index: 7, total: 8 },
15: { name: '右下', group: '眼球运动', index: 8, total: 8 },
},
frame: {
1: {
src: 'note-camera1',
@ -91,9 +127,19 @@ Component({ @@ -91,9 +127,19 @@ Component({
methods: {
handleSelect(type: number) {
this.setData({
selectShow: true,
type,
})
// 如果设置了只使用相机,直接打开相机
if (this.properties.onlyCamera) {
this.setData({
visible: true,
})
}
else {
this.setData({
selectShow: true,
})
}
},
handleCancel() {
this.setData({
@ -236,27 +282,25 @@ Component({ @@ -236,27 +282,25 @@ Component({
if (!isValid)
return
const app = getApp<IAppOption>()
wx.showLoading({
title: '正在上传',
})
// 第一步:上传图片到文件服务
wx.uploadFile({
filePath: tempFilePath,
name: 'file',
url: `${app.globalData.upFileUrl}?r=file-service/upload-img`,
success: (res) => {
wx.hideLoading()
try {
const data = JSON.parse(res.data)
if (data.code === 0 && data.data && data.data.Url) {
this.triggerEvent('uploadsuccess', {
url: data.data.Url,
fileUrl: data.data.Url,
tempFilePath,
})
const photoUrl = data.data.Url
// 第二步:调用 photo-upload 接口保存照片信息
this.savePhotoInfo(photoUrl)
}
else {
wx.hideLoading()
wx.showToast({
title: '上传失败',
icon: 'none',
@ -265,6 +309,7 @@ Component({ @@ -265,6 +309,7 @@ Component({
}
}
catch (e) {
wx.hideLoading()
wx.showToast({
title: '解析响应失败',
icon: 'none',
@ -283,6 +328,110 @@ Component({ @@ -283,6 +328,110 @@ Component({
})
})
},
// 获取 sessionId
getSessionId(): Promise<string> {
return new Promise((resolve, reject) => {
if (this.data.sessionIdCache) {
resolve(this.data.sessionIdCache)
return
}
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/get-session-id',
data: {},
}).then((res: any) => {
if (res.sessionId) {
this.setData({ sessionIdCache: res.sessionId })
resolve(res.sessionId)
}
else {
reject(new Error('sessionId not found'))
}
}).catch((err) => {
reject(err)
})
})
},
// 保存照片信息到后端
savePhotoInfo(photoUrl: string) {
const photoAngle = this.properties.photoAngle
if (!photoAngle) {
wx.hideLoading()
wx.showToast({
title: '照片角度未设置',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'photo_angle_not_set' })
return
}
// 先获取 sessionId,再上传照片
this.getSessionId().then((sessionId) => {
const recordId = this.properties.recordId
wx.ajax({
method: 'POST',
url: '?r=xd/proptosis/photo-upload',
data: {
...(recordId ? { recordId } : {}),
sessionId,
photoAngle,
photoUrl,
},
}).then((res: any) => {
wx.hideLoading()
const { photoId, checkStatus, isContinue, message } = res
// 触发上传成功事件,返回完整的照片信息
this.triggerEvent('uploadsuccess', {
photoId,
photoAngle,
photoUrl,
checkStatus,
isContinue,
message,
})
// 关闭相机
this.setData({
visible: false,
})
// 如果机审不通过且不允许继续,提示用户
if (checkStatus === 2 && !isContinue) {
wx.showModal({
title: '提示',
content: message || '图片不合规,请重新上传',
showCancel: false,
confirmColor: '#8c75d0',
cancelColor: '#141515',
})
}
else if (checkStatus === 2 && isContinue) {
// 机审不通过但允许继续,提示用户
wx.showToast({
title: message || '图片可能不合规',
icon: 'none',
})
}
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: '保存照片信息失败',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'save_photo_failed', error: err })
})
}).catch((err) => {
wx.hideLoading()
wx.showToast({
title: '获取会话ID失败',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'get_session_failed', error: err })
})
},
handleExample() {
this.setData({
exampleShow: !this.data.exampleShow,
@ -290,3 +439,5 @@ Component({ @@ -290,3 +439,5 @@ Component({
},
},
})
export {}

6
src/patient/components/camera/index.wxml

@ -39,9 +39,9 @@ @@ -39,9 +39,9 @@
</view>
<view>
<view class="order">
正面睁眼
<view class="num">1</view>
<view class="m-num">/3</view>
{{typeNameMap[type].name}}
<view class="num">{{typeNameMap[type].index}}</view>
<view class="m-num">/{{typeNameMap[type].total}}</view>
</view>
</view>
<view class="controls">

2
src/patient/components/image-crop/index.json

@ -5,4 +5,4 @@ @@ -5,4 +5,4 @@
"van-icon": "@vant/weapp/icon/index",
"van-toast": "@vant/weapp/toast/index"
}
}
}

2
src/patient/components/image-merge/index.json

@ -5,4 +5,4 @@ @@ -5,4 +5,4 @@
"van-icon": "@vant/weapp/icon/index",
"van-toast": "@vant/weapp/toast/index"
}
}
}

11
src/patient/pages/note/index.scss

@ -266,4 +266,15 @@ page { @@ -266,4 +266,15 @@ page {
}
}
}
.list-footer {
padding: 24rpx 0 12rpx;
display: flex;
justify-content: center;
.footer-text {
font-size: 26rpx;
color: #adacb2;
}
}
}

126
src/patient/pages/note/index.ts

@ -1,14 +1,120 @@ @@ -1,14 +1,120 @@
const _app = getApp<IAppOption>()
const app = getApp<IAppOption>()
interface RecordItem {
recordId: string
recordDate: string
isBaseline: number
photoCount: number
firstPhotoUrl: string
treatmentCount: number
leftEye: number
rightEye: number
interorbitalDistance: number
uploadCompleted: number
}
Page({
data: {
background: '#fff',
hasBaseline: false,
baselineRecordId: '',
baselineDate: '',
baselinePhotoUrl: '',
recordList: [] as RecordItem[],
loading: false,
refreshing: false,
page: 1,
pageSize: 10,
total: 0,
hasMore: true,
},
onLoad() {
// 页面加载时的初始化
},
onShow() {
app.waitLogin({ type: [1] }).then(() => {
this.getBaselineStatus()
this.getRecordList(true)
})
},
// 获取基准照设置状态
getBaselineStatus() {
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/baseline-status',
data: {},
}).then((res: any) => {
this.setData({
hasBaseline: res.hasBaseline,
baselineRecordId: res.baselineRecordId || '',
})
}).catch((err) => {
console.error('获取基准照状态失败:', err)
})
},
// 获取突眼记录列表
getRecordList(reset = false) {
if (this.data.loading)
return
if (!reset && !this.data.hasMore)
return
const page = reset ? 1 : this.data.page
this.setData({
loading: true,
...(reset ? { refreshing: true } : {}),
})
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/record-list',
data: {
page,
pageSize: this.data.pageSize,
},
}).then((res: any) => {
const list = res.list || []
const total = res.pagination?.total || 0
const nextList = reset ? list : [...this.data.recordList, ...list]
// 从已加载数据中找到基准照信息(避免后续分页把基准照“翻走”)
const baselineItem = nextList.find((item: RecordItem) => item.isBaseline === 1)
if (baselineItem) {
this.setData({
baselineDate: baselineItem.recordDate,
baselinePhotoUrl: baselineItem.firstPhotoUrl,
})
}
this.setData({
recordList: nextList,
loading: false,
refreshing: false,
page: page + 1,
total,
hasMore: nextList.length < total && list.length >= this.data.pageSize,
})
wx.stopPullDownRefresh()
}).catch((err) => {
console.error('获取记录列表失败:', err)
this.setData({ loading: false, refreshing: false })
wx.stopPullDownRefresh()
})
},
onPullDownRefresh() {
this.getBaselineStatus()
this.getRecordList(true)
},
onReachBottom() {
this.getRecordList()
},
// 返回上一页
handleBack() {
wx.navigateBack()
@ -20,16 +126,30 @@ Page({ @@ -20,16 +126,30 @@ Page({
url: '/patient/pages/noteAdd/index',
})
},
handleHistory() {
// 查看历史记录详情
handleHistory(e: any) {
const { recordId } = e.currentTarget.dataset
wx.navigateTo({
url: '/patient/pages/noteHistory/index',
url: `/patient/pages/noteHistory/index?recordId=${recordId}`,
})
},
// 照片对比
comparePhotos() {
wx.navigateTo({
url: '/patient/pages/noteDiff/index',
})
},
// 查看基准照详情
viewBaselineDetail() {
if (this.data.baselineRecordId) {
wx.navigateTo({
url: `/patient/pages/noteHistory/index?recordId=${this.data.baselineRecordId}`,
})
}
},
})
export {}

38
src/patient/pages/note/index.wxml

@ -3,8 +3,8 @@ @@ -3,8 +3,8 @@
</navbar>
<view class="container" style="padding-top:{{pageTop+20}}px">
<!-- 基准照片设置卡片 -->
<view class="setting-card-empty" bindtap="addRecord">
<!-- 基准照片设置卡片 - 未设置状态 -->
<view wx:if="{{!hasBaseline}}" class="setting-card-empty" bindtap="addRecord">
<view class="setting-header">
<view class="setting-title">
<image class="icon" src="{{imageUrl}}icon144.png?t={{Timestamp}}"></image>
@ -14,7 +14,9 @@ @@ -14,7 +14,9 @@
</view>
<view class="setting-desc">请先上传一组完整的照片作为基准,后续记录将以此为参照进行对比。</view>
</view>
<view class="setting-card">
<!-- 基准照片设置卡片 - 已设置状态 -->
<view wx:if="{{hasBaseline}}" class="setting-card" bindtap="viewBaselineDetail">
<view class="setting-header">
<view class="setting-title">
<image class="icon" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
@ -22,10 +24,10 @@ @@ -22,10 +24,10 @@
</view>
</view>
<view class="setting-body">
<image class="photo" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
<image class="photo" src="{{baselinePhotoUrl || imageUrl + 'icon143.png?t=' + Timestamp}}"></image>
<view class="wrap">
<view class="name">已设置基准照</view>
<view class="date">记录日期:2026-04-01</view>
<view class="date">记录日期:{{baselineDate}}</view>
</view>
<view class="more">查看</view>
</view>
@ -50,21 +52,31 @@ @@ -50,21 +52,31 @@
<view class="title-bar"></view>
<text>历史记录</text>
</view>
<view class="history-empty">
<!-- 空状态 -->
<view wx:if="{{recordList.length === 0 && !loading}}" class="history-empty">
<image class="icon" src="{{imageUrl}}icon146.png?t={{Timestamp}}"></image>
<text class="empty-text">暂无记录,点击上方按钮开始</text>
</view>
<view class="history-list">
<view class="list-item" bind:tap="handleHistory">
<view class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%">基准照</view>
<image class="photo" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
<!-- 记录列表 -->
<view wx:if="{{recordList.length > 0}}" class="history-list">
<view class="list-item" wx:for="{{recordList}}" wx:key="recordId" data-record-id="{{item.recordId}}" bind:tap="handleHistory">
<view wx:if="{{item.isBaseline === 1}}" class="benchmark" style="background: url('{{imageUrl}}bg50.png?t={{Timestamp}}') no-repeat top center/100%">基准照</view>
<image class="photo" src="{{item.firstPhotoUrl || imageUrl + 'icon143.png?t=' + Timestamp}}"></image>
<view class="wrap">
<view class="date">2026-04-01</view>
<view class="tag">替妥尤单抗:2</view>
<view class="rotate">已上传1个角度</view>
<view class="date">{{item.recordDate}}</view>
<view class="tag">替妥尤单抗:{{item.treatmentCount}}</view>
<view class="rotate">已上传{{item.photoCount}}个角度</view>
</view>
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image>
</view>
</view>
<!-- 分页状态 -->
<view wx:if="{{recordList.length > 0}}" class="list-footer">
<view wx:if="{{loading}}" class="footer-text">加载中...</view>
<view wx:elif="{{!hasMore}}" class="footer-text">没有更多了</view>
</view>
</view>
</view>

496
src/patient/pages/noteAdd/index.ts

@ -1,129 +1,465 @@ @@ -1,129 +1,465 @@
const _app = getApp<IAppOption>()
const app = getApp<IAppOption>()
interface PhotoItem {
photoId: string
photoAngle: string
photoUrl: string
checkStatus: number
isContinue: boolean
message: string
}
interface RecordPhoto {
photoId: string
photoAngle: string
photoAngleName: string
photoUrl: string
uploadTime: string
}
interface RecordDetail {
recordId: string
recordDate: string
treatmentCount: number
isBaseline: number
leftEye: number
rightEye: number
interorbitalDistance: number
uploadCompleted: number
photos: RecordPhoto[]
}
Page({
data: {
popupShow: false,
popupType: 'popup16', // 提示保存弹窗
popupType: 'popup16',
popupParams: {
close: false,
position: 'bottom',
} as any,
imagePreview: false,
previewImageSrc: '',
currentPhotoAngle: '',
recordId: '',
// 表单数据
recordDate: '',
treatmentCount: 0,
isBaseline: 0,
leftEye: '',
rightEye: '',
interorbitalDistance: '',
// 照片数据
photoMap: {} as Record<string, PhotoItem>,
// 顺序拍摄模式
sequentialShootMode: false,
sequentialShootList: [] as { name: string, type: string, angle: string }[],
sequentialShootIndex: 0,
imageSrc: '',
// 替妥尤单抗使用次数选项 (0-9, 9表示大于8)
treatmentCountRange: ['0次', '1次', '2次', '3次', '4次', '5次', '6次', '7次', '8次', '大于8次'],
cameraList: {
frontend: [
{
name: '睁眼',
type: 1,
},
{
name: '闭眼',
type: 2,
},
{
name: '仰头',
type: 3,
},
{ name: '睁眼', type: 'front_open', angle: 'front_open' },
{ name: '闭眼', type: 'front_closed', angle: 'front_closed' },
{ name: '仰头', type: 'front_looking_up', angle: 'front_looking_up' },
],
backend: [
{
name: '左侧-90°',
type: 4,
},
{
name: '右侧-90°',
type: 5,
},
{
name: '左侧-45°',
type: 6,
},
{
name: '右侧-45°',
type: 7,
},
{ name: '左侧-90°', type: 'side_left_90', angle: 'side_left_90' },
{ name: '右侧-90°', type: 'side_right_90', angle: 'side_right_90' },
{ name: '左侧-45°', type: 'side_left_45', angle: 'side_left_45' },
{ name: '右侧-45°', type: 'side_right_45', angle: 'side_right_45' },
],
other: [
{
name: '左上',
type: 8,
},
{
name: '向上',
type: 9,
},
{
name: '左下',
type: 10,
},
{
name: '向左',
type: 11,
},
{
name: '向右',
type: 12,
},
{
name: '右上',
type: 13,
},
{
name: '向下',
type: 14,
},
{
name: '右下',
type: 15,
},
{ name: '左上', type: 'eye_up_left', angle: 'eye_up_left' },
{ name: '向上', type: 'eye_up', angle: 'eye_up' },
{ name: '右上', type: 'eye_up_right', angle: 'eye_up_right' },
{ name: '向左', type: 'eye_left', angle: 'eye_left' },
{ name: '向右', type: 'eye_right', angle: 'eye_right' },
{ name: '左下', type: 'eye_down_left', angle: 'eye_down_left' },
{ name: '向下', type: 'eye_down', angle: 'eye_down' },
{ name: '右下', type: 'eye_down_right', angle: 'eye_down_right' },
],
},
},
onLoad() {},
handleDemo() {
wx.navigateTo({
url: '/patient/pages/noteDemo/index',
onLoad(option: any) {
const recordId = option?.recordId || ''
// 新增时默认日期为今天;编辑时以接口返回为准
const today = new Date().toISOString().split('T')[0]
this.setData({
recordId,
recordDate: recordId ? '' : today,
})
},
handleImageRetake() {
onShow() {
app.waitLogin({ type: [1] }).then(() => {
if (this.data.recordId) {
this.getRecordDetail()
}
})
},
// 获取记录详情(用于编辑/补拍回显)
getRecordDetail() {
wx.showLoading({ title: '加载中...' })
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/record-detail',
data: {
recordId: this.data.recordId,
},
}).then((res: RecordDetail) => {
wx.hideLoading()
const photoMap: Record<string, PhotoItem> = {}
;(res.photos || []).forEach((p) => {
photoMap[p.photoAngle] = {
photoId: p.photoId,
photoAngle: p.photoAngle,
photoUrl: p.photoUrl,
// 已在后台入库的历史照片,默认视为可继续
checkStatus: 1,
isContinue: true,
message: '',
}
})
this.setData({
recordDate: res.recordDate || '',
treatmentCount: typeof res.treatmentCount === 'number' ? res.treatmentCount : 0,
isBaseline: res.isBaseline || 0,
leftEye: res.leftEye != null ? String(res.leftEye) : '',
rightEye: res.rightEye != null ? String(res.rightEye) : '',
interorbitalDistance: res.interorbitalDistance != null ? String(res.interorbitalDistance) : '',
photoMap,
})
}).catch((err) => {
wx.hideLoading()
console.error('获取记录详情失败:', err)
wx.showToast({ title: '加载失败', icon: 'none' })
})
},
// 日期选择
onDateChange(e: any) {
this.setData({
imagePreview: false,
recordDate: e.detail.value,
})
},
handleImageDel() {
// 替妥尤单抗使用次数选择
onTreatmentCountChange(e: any) {
const index = e.detail.value
this.setData({
imagePreview: false,
treatmentCount: index,
})
},
// 是否设置为基准照
onBaselineChange(e: any) {
const values: string[] = e?.detail?.value || []
this.setData({
isBaseline: values.length > 0 ? 1 : 0,
})
},
// 左眼度数输入
onLeftEyeInput(e: any) {
this.setData({
leftEye: e.detail.value,
})
},
// 右眼度数输入
onRightEyeInput(e: any) {
this.setData({
rightEye: e.detail.value,
})
},
// 眶间距输入
onInterorbitalDistanceInput(e: any) {
this.setData({
interorbitalDistance: e.detail.value,
})
},
// 打开相机选择照片
handleCamera(e: any) {
const { angle } = e.currentTarget.dataset
this.setData({
currentPhotoAngle: angle,
})
// 调用 camera 组件的 handleSelect 方法,传入类型
const cameraComponent = this.selectComponent('#camera-component')
if (cameraComponent) {
// 根据 angle 映射到对应的 type
const type = this.getCameraType(angle)
cameraComponent.handleSelect(type)
}
},
// 一键顺序拍摄
handleSequentialShoot() {
// 定义拍摄顺序:正面 -> 侧面 -> 眼球运动
const shootOrder = [
...this.data.cameraList.frontend,
...this.data.cameraList.backend,
...this.data.cameraList.other,
]
this.setData({
sequentialShootMode: true,
sequentialShootList: shootOrder,
sequentialShootIndex: 0,
})
// 开始第一个拍摄
this.startSequentialShoot()
},
handleCamera(e) {
const { type } = e.currentTarget.dataset
// 开始顺序拍摄
startSequentialShoot() {
const { sequentialShootList, sequentialShootIndex } = this.data
if (sequentialShootIndex >= sequentialShootList.length) {
// 全部拍摄完成
this.setData({
sequentialShootMode: false,
sequentialShootList: [],
sequentialShootIndex: 0,
})
wx.showToast({
title: '全部拍摄完成',
icon: 'success',
})
return
}
const currentItem = sequentialShootList[sequentialShootIndex]
this.setData({
currentPhotoAngle: currentItem.angle,
})
// 调用 camera 组件,设置 onlyCamera 为 true
const cameraComponent = this.selectComponent('#camera-component')
cameraComponent.handleSelect(type)
if (cameraComponent) {
const type = this.getCameraType(currentItem.angle)
cameraComponent.setData({ onlyCamera: true })
cameraComponent.handleSelect(type)
}
},
// 顺序拍摄模式下的上传成功回调
onSequentialUploadSuccess(e: any) {
const { photoId, photoAngle, photoUrl, checkStatus, isContinue, message } = e.detail
// 保存照片信息到页面数据
const photoMap = this.data.photoMap
photoMap[photoAngle] = {
photoId,
photoAngle,
photoUrl,
checkStatus,
isContinue,
message,
}
// 检查是否是顺序拍摄模式
if (this.data.sequentialShootMode) {
// 继续下一个拍摄
const nextIndex = this.data.sequentialShootIndex + 1
this.setData({
photoMap,
sequentialShootIndex: nextIndex,
})
// 延迟一下再打开下一个相机,给用户反馈时间
setTimeout(() => {
this.startSequentialShoot()
}, 500)
}
else {
this.setData({ photoMap })
}
},
// 将 angle 映射到 camera 组件的 type
getCameraType(angle: string): number {
const typeMap: Record<string, number> = {
front_open: 1,
front_closed: 2,
front_looking_up: 3,
side_left_90: 4,
side_right_90: 5,
side_left_45: 6,
side_right_45: 7,
eye_up_left: 8,
eye_up: 9,
eye_up_right: 10,
eye_left: 11,
eye_right: 12,
eye_down_left: 13,
eye_down: 14,
eye_down_right: 15,
}
return typeMap[angle] || 1
},
// camera 组件上传成功回调
onUploadSuccess(e: any) {
const { photoId, photoAngle, photoUrl, checkStatus, isContinue, message } = e.detail
// 不允许继续的情况:不写入 photoMap,提示重新上传;顺序拍摄不推进 index
if (checkStatus === 2 && !isContinue) {
wx.showToast({ title: message || '图片不合规,请重新上传', icon: 'none' })
if (this.data.sequentialShootMode) {
setTimeout(() => {
this.startSequentialShoot()
}, 500)
}
return
}
// 保存照片信息到页面数据
const photoMap = this.data.photoMap
photoMap[photoAngle] = {
photoId,
photoAngle,
photoUrl,
checkStatus,
isContinue,
message,
}
// 检查是否是顺序拍摄模式
if (this.data.sequentialShootMode) {
// 继续下一个拍摄
const nextIndex = this.data.sequentialShootIndex + 1
this.setData({
photoMap,
sequentialShootIndex: nextIndex,
})
// 延迟一下再打开下一个相机,给用户反馈时间
setTimeout(() => {
this.startSequentialShoot()
}, 500)
}
else {
this.setData({ photoMap })
}
},
// camera 组件上传失败回调
onUploadError(e: any) {
const { errMsg } = e.detail
wx.showToast({ title: errMsg || '上传失败', icon: 'none' })
},
onUploadSuccess(e) {
// 关闭相机
onCloseCamera() {
// camera 组件内部处理关闭逻辑
},
// 预览图片
handlePreview(e: any) {
const { angle } = e.currentTarget.dataset
const photo = this.data.photoMap[angle]
if (photo) {
this.setData({
imagePreview: true,
previewImageSrc: photo.photoUrl,
currentPhotoAngle: angle,
})
}
},
// 重新拍摄
handleImageRetake() {
this.setData({
imageSrc: e.detail.url,
imagePreview: false,
})
this.handleCamera({ currentTarget: { dataset: { angle: this.data.currentPhotoAngle } } })
},
onUploadError(e) {
console.log('DEBUGPRINT[221]: index.ts:34: e=', e)
// 删除图片
handleImageDel() {
const photoMap = this.data.photoMap
delete photoMap[this.data.currentPhotoAngle]
this.setData({
imagePreview: false,
photoMap,
})
},
handlePreview() {
this.selectComponent('#note-image-preview').handlePreview(this.data.imageSrc)
// 保存记录
handleSave() {
const { recordId, recordDate, treatmentCount, isBaseline, leftEye, rightEye, interorbitalDistance, photoMap } = this.data
// 表单验证
if (!recordDate) {
wx.showToast({ title: '请选择记录日期', icon: 'none' })
return
}
// 获取所有已上传的照片ID
const photoIds = Object.values(photoMap).map(photo => photo.photoId).join(',')
const data: any = {
...(recordId ? { recordId } : {}),
recordDate,
treatmentCount,
isBaseline,
photoIds,
}
// 可选字段
if (leftEye)
data.leftEye = leftEye
if (rightEye)
data.rightEye = rightEye
if (interorbitalDistance)
data.interorbitalDistance = interorbitalDistance
wx.showLoading({ title: '保存中...' })
wx.ajax({
method: 'POST',
url: '?r=xd/proptosis/record-save',
data,
}).then(() => {
wx.hideLoading()
wx.showToast({
title: '保存成功',
icon: 'success',
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}).catch((err: any) => {
wx.hideLoading()
const msg = err?.data?.msg || '保存失败'
wx.showToast({ title: msg, icon: 'none' })
})
},
handleDemo() {
wx.navigateTo({
url: '/patient/pages/noteDemo/index',
})
},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
},
handleBack() {
wx.navigateBack()
},

76
src/patient/pages/noteAdd/index.wxml

@ -6,45 +6,47 @@ @@ -6,45 +6,47 @@
<view class="page" style="padding-top: {{pageTop+20}}px;">
<view class="benchmark">
<checkbox class="checkbox" color="#fff">设置为基准记录,用于对比</checkbox>
<checkbox-group bindchange="onBaselineChange">
<checkbox class="checkbox" color="#fff" value="1" checked="{{isBaseline === 1}}">设置为基准记录,用于对比</checkbox>
</checkbox-group>
</view>
<view class="form">
<view class="form-title">记录日期</view>
<picker mode="date">
<picker mode="date" value="{{recordDate}}" bindchange="onDateChange">
<view class="select">
<view class="content" data-place="请选择记录日期"></view>
<view class="content {{recordDate ? '' : 'placeholder'}}">{{recordDate || '请选择记录日期'}}</view>
<van-icon class="more" name="arrow-down" />
</view>
</picker>
<view class="form-title form-title-gap">当前记录对应的替妥尤单抗使用次数</view>
<picker mode="selector">
<picker mode="selector" range="{{treatmentCountRange}}" value="{{treatmentCount}}" bindchange="onTreatmentCountChange">
<view class="select">
<view class="content" data-place="请选择使用次数"></view>
<view class="content {{treatmentCount !== null ? '' : 'placeholder'}}">{{treatmentCountRange[treatmentCount] || '请选择使用次数'}}</view>
<van-icon class="more" name="arrow-down" />
</view>
</picker>
</view>
<view class="form">
<view class="form-title">记录本次突眼度</view>
<view class="form-title">记录本次突眼度(选填)</view>
<view class="dobule">
<view class="item">
<view class="name">右眼</view>
<view class="i-content">
<input type="digit" class="num" placeholder-class="place" placeholder="填写" />
<input type="digit" class="num" placeholder-class="place" placeholder="填写" value="{{rightEye}}" bindinput="onRightEyeInput" />
MM
</view>
</view>
<view class="item">
<view class="name">眶间距</view>
<view class="i-content">
<input type="digit" class="num" placeholder-class="place" placeholder="填写" />
<input type="digit" class="num" placeholder-class="place" placeholder="填写" value="{{interorbitalDistance}}" bindinput="onInterorbitalDistanceInput" />
MM
</view>
</view>
<view class="item">
<view class="name">左眼</view>
<view class="i-content">
<input type="digit" class="num" placeholder-class="place" placeholder="填写" />
<input type="digit" class="num" placeholder-class="place" placeholder="填写" value="{{leftEye}}" bindinput="onLeftEyeInput" />
MM
</view>
</view>
@ -57,7 +59,7 @@ @@ -57,7 +59,7 @@
<image class="icon" src="{{imageUrl}}icon149.png?t={{Timestamp}}"></image>
标准拍摄示范
</view>
<view class="btn btn2">
<view class="btn btn2" bind:tap="handleSequentialShoot">
<image class="icon" src="{{imageUrl}}icon150.png?t={{Timestamp}}"></image>
一键顺序拍摄
</view>
@ -68,16 +70,16 @@ @@ -68,16 +70,16 @@
正面
</view>
<view class="upload-container">
<view class="upload-item" wx:for="{{cameraList.frontend}}" wx:key="index">
<view class="upload-preview" wx:if="{{imageSrc}}" bind:tap="handlePreview">
<image class="photo" src="{{imageSrc}}"></image>
<view class="status">
<view class="upload-item" wx:for="{{cameraList.frontend}}" wx:key="angle" data-angle="{{item.angle}}">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview">
<image class="photo" src="{{photoMap[item.angle].photoUrl}}"></image>
<view class="status" wx:if="{{photoMap[item.angle].checkStatus === 2}}">
<image class="icon" src="/images/icon164.png"></image>
<view class="content">不符合规范</view>
<view class="guide">重新上传</view>
<view class="guide" catchtap="handleCamera">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">{{item.name}}</view>
</view>
@ -90,11 +92,16 @@ @@ -90,11 +92,16 @@
侧面
</view>
<view class="upload-container">
<view class="upload-item" wx:for="{{cameraList.backend}}" wx:key="index">
<view class="upload-preview" wx:if="{{imageSrc}}" bind:tap="handlePreview">
<image class="photo" src="{{imageSrc}}"></image>
<view class="upload-item" wx:for="{{cameraList.backend}}" wx:key="angle" data-angle="{{item.angle}}">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview">
<image class="photo" src="{{photoMap[item.angle].photoUrl}}"></image>
<view class="status" wx:if="{{photoMap[item.angle].checkStatus === 2}}">
<image class="icon" src="/images/icon164.png"></image>
<view class="content">不符合规范</view>
<view class="guide" catchtap="handleCamera">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">{{item.name}}</view>
</view>
@ -107,11 +114,16 @@ @@ -107,11 +114,16 @@
眼球运动八个方向
</view>
<view class="upload-container">
<view class="upload-item" wx:for="{{cameraList.other}}" wx:key="index">
<view class="upload-preview" wx:if="{{imageSrc}}" bind:tap="handlePreview">
<image class="photo" src="{{imageSrc}}"></image>
<view class="upload-item" wx:for="{{cameraList.other}}" wx:key="angle" data-angle="{{item.angle}}">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview">
<image class="photo" src="{{photoMap[item.angle].photoUrl}}"></image>
<view class="status" wx:if="{{photoMap[item.angle].checkStatus === 2}}">
<image class="icon" src="/images/icon164.png"></image>
<view class="content">不符合规范</view>
<view class="guide" catchtap="handleCamera">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">
<image class="arrow" src="/images/icon167.png"></image>
@ -122,17 +134,15 @@ @@ -122,17 +134,15 @@
</view>
</view>
<view class="c-footer">
<view class="btn">诺和益托管视频</view>
<view class="btn" bind:tap="handleSave">保存记录</view>
</view>
</view>
</view>
<camera id="camera-component" bind:uploadsuccess="onUploadSuccess" bind:uploaderror="onUploadError" />
<noteImagePreview
id="note-image-preview"
visible="{{imagePreview}}"
src="{{imageUrl}}note-demo10.png?t={{Timestamp}}"
src="{{previewImageSrc}}"
bind:delete="handleImageDel"
bind:retake="handleImageRetake"
></noteImagePreview>
@ -144,3 +154,13 @@ @@ -144,3 +154,13 @@
bind:ok="handlePopupOk"
bind:cancel="handlePopupCancel"
></popup>
<!-- 相机组件 -->
<camera
id="camera-component"
photo-angle="{{currentPhotoAngle}}"
record-id="{{recordId}}"
bind:uploadsuccess="onUploadSuccess"
bind:uploaderror="onUploadError"
bind:close="onCloseCamera"
></camera>

183
src/patient/pages/noteDiff/index.ts

@ -1,11 +1,186 @@ @@ -1,11 +1,186 @@
const _app = getApp<IAppOption>()
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
}
Page({
data: {},
onLoad() {},
data: {
// 对比角度
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[],
// 空状态
hasBaseline: true,
},
onLoad() {
this.getCompareAngle()
},
onShow() {
app.waitLogin({ type: [1] }).then(() => {
this.getBaselineStatus()
})
},
// 获取对比角度列表
getCompareAngle() {
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/get-compare-angle',
data: {},
}).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)
})
},
// 获取基准照状态
getBaselineStatus() {
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/baseline-status',
data: {},
}).then((res: any) => {
this.setData({
hasBaseline: res.hasBaseline,
})
}).catch((err) => {
console.error('获取基准照状态失败:', err)
})
},
// 获取对比可用日期
getCompareDates() {
if (!this.data.photoAngle)
return
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/compare-dates',
data: {
photoAngle: this.data.photoAngle,
},
}).then((res: any) => {
this.setData({
baseline: res.baseline || null,
nonBaselineList: res.nonBaselineList || [],
})
}).catch((err) => {
console.error('获取对比日期失败:', err)
})
},
// 选择对比角度
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/proptosis/compare-photos',
data: {
photoAngle,
recordIds: recordIds.join(','),
},
}).then((res: any) => {
this.setData({
comparePhotos: res.photos || [],
})
}).catch((err) => {
console.error('获取对比照片失败:', err)
})
},
// 去设置基准照
goSetBaseline() {
wx.navigateTo({
url: '/patient/pages/noteAdd/index',
})
},
// 生成对比图
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: '/patient/pages/noteDiffEdit/index',
url: `/patient/pages/noteDiffEdit/index?photoAngle=${this.data.photoAngle}&recordIds=${recordIds.join(',')}`,
})
},
})

42
src/patient/pages/noteDiff/index.wxml

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<view class="page-none" wx:if="{{false}}">
<view class="page-none" wx:if="{{!hasBaseline}}">
<image class="icon" src="{{imageUrl}}none3.png?t={{Timestamp}}"></image>
<view class="title">未设置基准照片</view>
<view class="content">
@ -6,9 +6,9 @@ @@ -6,9 +6,9 @@
<view></view>
以便进行对比分析
</view>
<view class="btn">去设置</view>
<view class="btn" bind:tap="goSetBaseline">去设置</view>
</view>
<view class="page">
<view class="page" wx:if="{{hasBaseline}}">
<view class="page-tip">
<image class="icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image>
<view class="content">
@ -18,9 +18,9 @@ @@ -18,9 +18,9 @@
<view class="form">
<view class="form-item">
<view class="title">选择对比角度</view>
<picker mode="selector">
<picker mode="selector" range="{{angleList}}" range-key="name" value="{{0}}" bindchange="onAngleChange">
<view class="select">
<view class="content" data-place="请选择对比角度"></view>
<view class="content {{photoAngleName ? '' : 'placeholder'}}">{{photoAngleName || '请选择对比角度'}}</view>
<van-icon class="more" name="arrow-down" />
</view>
</picker>
@ -28,49 +28,55 @@ @@ -28,49 +28,55 @@
<view class="form-item">
<view class="title">选择对比日期(可多选)</view>
<view class="multiple">
<view class="item {{index==0&&'active'}}" wx:for="{{8}}" wx:key="index">2026-04-02</view>
<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">
<view class="container" wx:if="{{comparePhotos.length > 0}}">
<view class="title">
正面睁眼时间线对比
<view class="date">生成日期:2026-04-02</view>
{{photoAngleName}}时间线对比
<view class="date">生成日期:{{comparePhotos[0].recordDate}}</view>
</view>
<view class="card">
<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">基准照片 2026-04-02</view>
<view class="date">{{item.isBaseline === 1 ? '基准照片' : '对比照片'}} {{item.recordDate}}</view>
</view>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</view>
<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="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="row">
<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">12</view>
<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">12</view>
<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">12</view>
<view class="num">{{item.leftEye || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>

122
src/patient/pages/noteDiffEdit/index.scss

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
page {
background-color: #f6f8f9;
}
.page {
padding-bottom: 80rpx;
padding-bottom: 160rpx;
.container {
margin: 40rpx 40rpx 0;
.title {
padding-bottom: 40rpx;
font-size: 32rpx;
@ -13,41 +16,43 @@ page { @@ -13,41 +16,43 @@ page {
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;
border-radius: 44rpx;
}
}
.card {
display: flex;
.aside {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
.line-bottom {
border: 1px dashed #b982ff;
}
.line-bottom {
flex: 1;
}
width: 32rpx;
.circle {
position: relative;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(185, 130, 255, 0.29);
flex-shrink: 0;
&::after {
position: relative;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@ -59,65 +64,84 @@ page { @@ -59,65 +64,84 @@ page {
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;
color: #ffa300;
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;
position: relative;
.photo {
border-radius: 32rpx 32rpx 0 0;
border-radius: 32rpx;
height: 352rpx;
display: block;
width: 100%;
}
.mask {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 85rpx;
right: 0;
height: 88rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 0 0 32rpx 32rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
background: rgba(0, 0, 0, 0.3);
font-size: 32rpx;
color: #ffffff;
font-size: 28rpx;
color: #fff;
.icon {
width: 40rpx;
height: 40rpx;
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
}
}
@ -125,33 +149,73 @@ page { @@ -125,33 +149,73 @@ page {
}
.footer {
margin-top: 46rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 40rpx 48rpx;
background: #fff;
display: flex;
align-items: center;
gap: 22rpx;
gap: 24rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
.btn1 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #b982ff;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #b982ff;
background-color: #fff;
border-radius: 100rpx 100rpx 100rpx 100rpx;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
border-radius: 100rpx;
}
.btn2 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #ffffff;
color: #b073ff;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
border-radius: 100rpx 100rpx 100rpx 100rpx;
background: #fff;
border: 2rpx solid #b073ff;
border-radius: 100rpx;
}
}
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 40rpx;
.loading-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 40rpx;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 20rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}

178
src/patient/pages/noteDiffEdit/index.ts

@ -1,18 +1,178 @@ @@ -1,18 +1,178 @@
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({
data: {},
onLoad() {},
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: {},
}).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() {
const { photos } = this.data
if (photos.length === 0) {
wx.showToast({ title: '暂无照片', icon: 'none' })
return
}
const mergeComponent = this.selectComponent('#merge')
console.log("DEBUGPRINT[224]: index.ts:7: mergeComponent=", mergeComponent)
if (mergeComponent) {
mergeComponent.mergeImages([
{ src: 'https://picsum.photos/400/300', time: '2024-01-01 10:00' },
{ src: 'https://picsum.photos/400/301', time: '2024-01-01 12:00' },
{ src: 'https://picsum.photos/400/302' },
])
const imageList = photos.map((item) => {
const label = item.isBaseline === 1
? `基准照片 ${item.recordDate}`
: `对比照片 ${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' })
},
})

38
src/patient/pages/noteDiffEdit/index.wxml

@ -1,30 +1,30 @@ @@ -1,30 +1,30 @@
<view class="page">
<view class="container">
<view class="container" wx:if="{{!loading && photos.length > 0}}">
<view class="title">
正面睁眼时间线对比
<view class="date">生成日期:2026-04-02</view>
{{photoAngleName}}时间线对比
<view class="date">生成日期:{{photos[0].recordDate}}</view>
</view>
<view class="card">
<view class="card" wx:for="{{photos}}" wx:key="photoId" data-index="{{index}}">
<view class="aside">
<view class="circle"></view>
<view class="line-bottom"></view>
</view>
<view class="c-container">
<view class="c-header">
<view class="date">基准照片 2026-04-02</view>
<view class="date">{{item.isBaseline === 1 ? '基准照片' : '对比照片'}} {{item.recordDate}}</view>
</view>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</view>
<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="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="mask" wx:if="{{false}}">
<image class="icon" src="/images/icon165.png"></image>
<image class="photo" src="{{item.photoUrl}}" mode="aspectFill"></image>
<view class="mask" wx:if="{{!item.isCropped}}" bind:tap="handleCrop" data-index="{{index}}">
<image class="icon" src="{{imageUrl}}icon165.png?t={{Timestamp}}"></image>
点击裁剪
</view>
<view class="mask" wx:else>
<image class="icon" src="/images/icon166.png"></image>
<view class="mask" wx:else bind:tap="handleRestore" data-index="{{index}}">
<image class="icon" src="{{imageUrl}}icon166.png?t={{Timestamp}}"></image>
点击还原
</view>
</view>
@ -32,9 +32,21 @@ @@ -32,9 +32,21 @@
</view>
<view class="footer">
<view class="btn1" bind:tap="handleMergePreview">对比图预览</view>
<view class="btn2">保存到相册</view>
<view class="btn2" bind:tap="handleSaveAlbum">保存到相册</view>
</view>
</view>
<!-- 加载中 -->
<view class="loading" wx:if="{{loading}}">
<van-loading type="spinner" color="#8c75d0"></van-loading>
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{!loading && photos.length === 0}}">
<image class="empty-icon" src="{{imageUrl}}none3.png?t={{Timestamp}}"></image>
<text class="empty-text">暂无对比照片</text>
</view>
</view>
<imageMerge id="merge" bindsave="onMergeSave" binderror="onMergeError" />

4
src/patient/pages/noteHistory/index.scss

@ -170,10 +170,10 @@ @@ -170,10 +170,10 @@
height: 320rpx;
border-radius: 24rpx;
}
.name{
.name {
margin-top: 16rpx;
font-size: 28rpx;
color: #ADACB2;
color: #adacb2;
text-align: center;
}
}

240
src/patient/pages/noteHistory/index.ts

@ -1,89 +1,187 @@ @@ -1,89 +1,187 @@
const _app = getApp<IAppOption>()
const app = getApp<IAppOption>()
interface Photo {
photoId: string
photoAngle: string
photoAngleName: string
photoUrl: string
uploadTime: string
}
interface RecordDetail {
recordId: string
recordDate: string
treatmentCount: number
isBaseline: number
leftEye: number
rightEye: number
interorbitalDistance: number
uploadCompleted: number
photos: Photo[]
}
Page({
data: {
popupShow: true,
popupType: 'popup15', // 确认删除弹窗
popupShow: false,
popupType: 'popup15',
popupParams: {
close: false,
} as any,
history: {
frontend: [
{
title: '正面睁眼照',
content: '平视,目光看向镜头方向,自然睁眼,不眯眼、不瞪眼。',
},
{
title: '正面闭眼照',
content: '正对镜头,面部居中,自然放松,双眼轻轻闭合,不皱眉、不挤眼。',
},
{
title: '正面仰头照',
content: '拍摄时,正对镜头,面部居中,头部自然向上仰至约 45°,双眼同步平视,保持自然睁眼、不眯眼。',
},
],
backend: [
{
title: '左侧-90°',
content: '身体与头部完全转向右侧,呈标准 90° 侧面,仅可见左侧眼睛。',
},
{
title: '右侧-90°',
content: '身体与头部完全转向左侧,呈标准 90° 侧面,仅可见右侧眼睛。',
},
{
title: '左侧-45°',
content: '身体与头部转向右前方 45°。',
},
{
title: '右侧-45°',
content: '身体与头部转向左前方 45°',
},
],
other: [
{
title: '正面眼睛上看',
content: '',
},
{
title: '正面眼睛下看',
content: '',
},
{
title: '正面眼睛左看',
content: '',
},
{
title: '正面眼睛右看',
content: '',
},
{
title: '正面眼睛左上看',
content: '',
},
{
title: '正面眼睛右上看',
content: '',
},
{
title: '正面眼睛左下看',
content: '',
},
{
title: '正面眼睛右下看',
content: '',
},
],
recordId: '',
recordDetail: {} as RecordDetail,
// 角度分组
angleGroups: {
frontend: ['front_open', 'front_closed', 'front_looking_up'],
backend: ['side_left_90', 'side_right_90', 'side_left_45', 'side_right_45'],
other: ['eye_up_left', 'eye_up', 'eye_up_right', 'eye_left', 'eye_right', 'eye_down_left', 'eye_down', 'eye_down_right'],
},
// 角度名称映射
angleNameMap: {
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: '正面眼睛右下',
} as Record<string, string>,
},
onLoad(option: any) {
this.setData({
recordId: option.recordId || '',
})
},
onShow() {
app.waitLogin({ type: [1] }).then(() => {
if (this.data.recordId) {
this.getRecordDetail()
}
})
},
// 获取记录详情
getRecordDetail() {
wx.showLoading({ title: '加载中...' })
wx.ajax({
method: 'GET',
url: '?r=xd/proptosis/record-detail',
data: {
recordId: this.data.recordId,
},
}).then((res: any) => {
wx.hideLoading()
this.setData({
recordDetail: res,
})
}).catch((err) => {
wx.hideLoading()
console.error('获取记录详情失败:', err)
wx.showToast({ title: '加载失败', icon: 'none' })
})
},
// 获取指定角度的照片
getPhotoByAngle(angle: string): Photo | undefined {
const photos = this.data.recordDetail.photos || []
return photos.find((p: Photo) => p.photoAngle === angle)
},
// 获取已上传角度数量
getUploadedCount(): number {
const photos = this.data.recordDetail.photos || []
return photos.length
},
// 返回上一页
handleBack() {
wx.navigateBack()
},
// 编辑记录
handleEdit() {
wx.navigateTo({
url: `/patient/pages/noteAdd/index?recordId=${this.data.recordId}`,
})
},
// 删除记录
handleDelete() {
wx.showModal({
title: '确认删除',
content: '确定要删除这条记录吗?删除后无法恢复。',
confirmColor: '#8c75d0',
cancelColor: '#141515',
success: (res) => {
if (res.confirm) {
this.doDelete()
}
},
})
},
// 执行删除
doDelete() {
wx.showLoading({ title: '删除中...' })
wx.ajax({
method: 'POST',
url: '?r=xd/proptosis/record-delete',
data: {
recordId: this.data.recordId,
},
}).then(() => {
wx.hideLoading()
wx.showToast({
title: '删除成功',
icon: 'success',
})
setTimeout(() => {
wx.navigateBack()
}, 1500)
}).catch((err) => {
wx.hideLoading()
console.error('删除失败:', err)
wx.showToast({ title: '删除失败', icon: 'none' })
})
},
// 预览图片
handlePreview(e: any) {
const { angle } = e.currentTarget.dataset
const photo = this.getPhotoByAngle(angle)
if (photo) {
wx.previewImage({
urls: [photo.photoUrl],
current: photo.photoUrl,
})
}
},
// 去补充照片
handleSupplement() {
wx.navigateTo({
url: `/patient/pages/noteAdd/index?recordId=${this.data.recordId}`,
})
},
onLoad() {},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,

54
src/patient/pages/noteHistory/index.wxml

@ -8,81 +8,93 @@ @@ -8,81 +8,93 @@
>
<view class="header">
<view class="wrap">
<view class="date">2026-04-02</view>
<view class="date">{{recordDetail.recordDate}}</view>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</view>
<view wx:if="{{recordDetail.isBaseline === 1}}" class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:{{recordDetail.treatmentCount}}</view>
</view>
</view>
<view class="opt">
<view class="item">
<view class="item" bind:tap="handleEdit">
<image class="icon" src="{{imageUrl}}icon152.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="item" bind:tap="handleDelete">
<image class="icon" src="{{imageUrl}}icon153.png?t={{Timestamp}}"></image>
</view>
</view>
</view>
<view class="container">
<view class="tip-card">
<!-- 未录入完全提示 -->
<view wx:if="{{recordDetail.uploadCompleted === 0}}" class="tip-card">
<image class="tip-icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image>
<view class="wrap">
<view class="title">记录未录入完全</view>
<view class="content">当前已录入1/15个角度。您可以点击右上角的编辑按钮继续补充照片。</view>
<view class="content">当前已录入{{getUploadedCount()}}/15个角度。您可以点击右上角的编辑按钮继续补充照片。</view>
</view>
<view class="btn">去补充</view>
<view class="btn" bind:tap="handleSupplement">去补充</view>
</view>
<view class="banner">
<!-- 眼球突出度测量记录 -->
<view wx:if="{{recordDetail.leftEye || recordDetail.rightEye || recordDetail.interorbitalDistance}}" class="banner">
<view class="title">眼球突出度测量记录</view>
<view class="row">
<view class="col">
<view class="name">右眼</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.rightEye || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">眶间距</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.interorbitalDistance || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">左眼</view>
<view class="content">
<view class="num">12</view>
<view class="num">{{recordDetail.leftEye || '--'}}</view>
<view class="sub">MM</view>
</view>
</view>
</view>
</view>
<!-- 正面照片 -->
<view class="card">
<view class="card-title">正面</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.frontend}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.frontend}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<!-- 侧面照片 -->
<view class="card">
<view class="card-title">侧面</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.backend}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.backend}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<!-- 眼球运动八个方向 -->
<view class="card">
<view class="card-title">眼球运动八个方向</view>
<view class="card-container">
<view class="card-item" wx:for="{{history.other}}" wx:key="index">
<image class="photo" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{item.title}}</view>
<view class="card-item" wx:for="{{angleGroups.other}}" wx:key="*this" data-angle="{{item}}" bind:tap="handlePreview">
<image wx:if="{{getPhotoByAngle(item)}}" class="photo" mode="aspectFill" src="{{getPhotoByAngle(item).photoUrl}}"></image>
<image wx:else class="photo photo-empty" mode="aspectFill" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>

6
src/sitemap.json

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
"action": "allow",
"page": "*"
}]
}
}

8
typings/index.d.ts vendored

@ -41,11 +41,11 @@ interface IAppOption { @@ -41,11 +41,11 @@ interface IAppOption {
checkLoginType: (type: pageType[] | 'any', backPath?: string) => boolean
getMenuInfo: (arg0: WechatMiniprogram.Page.Instance<any, any>) => void
getUserInfo: (type: 0 | 1 | 2) => Promise<never>
mpBehavior: (data: { doctor?: boolean; PageName: string; detailId?: string; promotionMethodId?: string }) => void
oldMpBehavior: (data: { PositionId: string; OperateType: string; OperateId: string }) => void
zdMpBehavior: (data: { PageName: string; doctor?: boolean }) => void
mpBehavior: (data: { doctor?: boolean, PageName: string, detailId?: string, promotionMethodId?: string }) => void
oldMpBehavior: (data: { PositionId: string, OperateType: string, OperateId: string }) => void
zdMpBehavior: (data: { PageName: string, doctor?: boolean }) => void
zdGetTheme: () => Promise<'PATIENT'>
zdWaitLogin: (obj?: { isReg?: boolean; loginPage?: boolean; pub?: boolean }) => Promise<any>
zdWaitLogin: (obj?: { isReg?: boolean, loginPage?: boolean, pub?: boolean }) => Promise<any>
[propName: string]: any
}

675
接口文档.md

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save