Browse Source

feat(图片合并): 新增图片合并组件及功能优化

refactor(患者详情): 重构患者详情页面的日记记录展示逻辑
fix(图片对比): 修复对比日期选择状态同步问题
style(弹窗): 优化弹窗17的样式和布局
perf(图片裁剪): 实现内置裁剪功能替代页面跳转
docs(接口文档): 更新照片上传接口路径变更说明
dev
kola-web 1 week ago
parent
commit
6d53a8b081
  1. 419
      .trae/rules/project_rules.md
  2. 169
      AGENTS.md
  3. 8
      src/components/image-merge/index.json
  4. 7
      src/components/image-merge/index.scss
  5. 186
      src/components/image-merge/index.ts
  6. 1
      src/components/image-merge/index.wxml
  7. 14
      src/components/noteImagePreview/index.ts
  8. 64
      src/components/popup/index.scss
  9. 14
      src/components/popup/index.wxml
  10. 36
      src/pages/d_noteDetail/index.ts
  11. 27
      src/pages/d_noteDetail/index.wxml
  12. 5
      src/pages/d_noteDiff/index.json
  13. 23
      src/pages/d_noteDiff/index.scss
  14. 29
      src/pages/d_noteDiff/index.ts
  15. 14
      src/pages/d_noteDiff/index.wxml
  16. 7
      src/pages/d_noteDiffData/index.json
  17. 71
      src/pages/d_noteDiffData/index.ts
  18. 15
      src/pages/d_noteDiffData/index.wxml
  19. 4
      src/pages/d_noteDiffEdit/index.json
  20. 72
      src/pages/d_noteDiffEdit/index.scss
  21. 112
      src/pages/d_noteDiffEdit/index.ts
  22. 33
      src/pages/d_noteDiffEdit/index.wxml
  23. 5
      src/pages/d_noteList/index.json
  24. 21
      src/pages/d_noteList/index.ts
  25. 15
      src/pages/d_noteList/index.wxml
  26. 7
      src/pages/d_patientDetail/index.ts
  27. 15
      src/pages/d_patientDetail/index.wxml
  28. 2
      src/patient/components/camera/index.ts
  29. 2
      src/patient/pages/note/index.wxml
  30. 48
      src/patient/pages/noteAdd/index.ts
  31. 21
      src/patient/pages/noteAdd/index.wxml
  32. 4
      src/patient/pages/noteDemo/index.wxml
  33. 2
      src/patient/pages/noteDiff/index.json
  34. 23
      src/patient/pages/noteDiff/index.ts
  35. 12
      src/patient/pages/noteDiff/index.wxml
  36. 5
      src/patient/pages/noteDiffEdit/index.json
  37. 60
      src/patient/pages/noteDiffEdit/index.ts
  38. 10
      src/patient/pages/noteDiffEdit/index.wxml
  39. 72
      src/patient/pages/noteHistory/index.ts
  40. 23
      src/patient/pages/noteHistory/index.wxml
  41. 69
      接口文档.md

419
.trae/rules/project_rules.md

@ -0,0 +1,419 @@ @@ -0,0 +1,419 @@
# 项目规则 - 信达小程序 (Xinda Mini Program)
WeChat Mini Program for thyroid eye disease patient management. Dual-mode app serving both patients (患者端) and doctors (医生端).
## Project Type
- **Framework**: WeChat Mini Program v3.7.7
- **Language**: TypeScript (strict mode)
- **Styling**: SCSS/Sass via `useCompilerPlugins`
- **UI Library**: Vant Weapp (@vant/weapp)
- **Renderer**: Skyline enabled
- **App ID**: `wxf9ce8010f1ad24aa` (dev), `wx71ac9c27c3c3e3f4` (prod)
## Directory Structure
```
src/
├── pages/ # Doctor-side main pages (医生端)
├── patient/pages/ # Patient-side pages (subpackage)
├── gift/pages/ # DTP pharmacy pages (subpackage)
├── doc/pages/ # Privacy/terms documentation (subpackage)
├── components/ # Shared components
├── utils/ # Request, page/component wrappers, utilities
├── app.ts # App entry with global data
└── config.ts # Environment configs per appId
```
## 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/*`
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',
cancelColor: '#141515',
})
```
### 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)
}
})
```
## WXML 开发规范
### 1. 不支持在 WXML 中直接调用方法
**错误示例:**
```xml
<!-- ❌ 错误:WXML 中不能直接调用方法 -->
<view>{{getUploadedCount()}}</view>
<view wx:if="{{hasPhotos()}}">内容</view>
```
**正确做法:**
```xml
<!-- ✅ 正确:使用数据绑定 -->
<view>{{uploadedCount}}</view>
<view wx:if="{{hasPhotos}}">内容</view>
```
```typescript
// 在 TS 中计算好数据
this.setData({
uploadedCount: photos.length,
hasPhotos: photos.length > 0
})
```
### 2. 列表渲染时使用 wx:key
```xml
<view wx:for="{{list}}" wx:key="id">{{item.name}}</view>
```
### 3. 条件渲染
```xml
<view wx:if="{{condition}}">显示</view>
<view wx:else>隐藏</view>
```
### 4. WXML 表达式限制
WXML 中的双大括号 `{{}}` 只能使用简单的表达式,**不支持**以下语法:
**❌ 不支持的语法:**
```xml
<!-- 方法调用 -->
<view>{{arr.indexOf(item) > -1}}</view>
<!-- 复杂表达式 -->
<view>{{obj.method().property}}</view>
<!-- 正则表达式 -->
<view>{{str.match(/regex/)}}</view>
<!-- new 操作符 -->
<view>{{new Date().getTime()}}</view>
```
**✅ 支持的语法:**
```xml
<!-- 简单属性访问 -->
<view>{{item.name}}</view>
<!-- 三元表达式 -->
<view class="{{item.isActive ? 'active' : ''}}">内容</view>
<!-- 简单比较 -->
<view wx:if="{{count > 0}}">有数据</view>
<!-- 逻辑运算 -->
<view wx:if="{{isLogin && isVip}}">VIP用户</view>
```
**解决方案:**
```typescript
// 在 TS 中预处理数据,将复杂计算转换为简单布尔值
this.setData({
// ❌ 不要在 WXML 中使用 indexOf
// isSelected: selectedDates.indexOf(item.recordId) > -1
// ✅ 在数据中直接提供 isSelected 字段
list: rawList.map(item => ({
...item,
isSelected: selectedDates.includes(item.recordId)
}))
})
```
## Available Commands
```bash
# Lint and auto-fix (only command available)
npm run lint:fix
# Install dependencies (pnpm preferred based on lockfile)
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
- `mp-html`: Rich HTML rendering
## 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}}
```
## TypeScript Configuration
- Strict mode enabled
- `noImplicitAny: false` (allows implicit any)
- Paths: `@/*` mapped to `src/*`
- 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
## ESLint & Formatting
- Config: `@antfu/eslint-config` (flat config in `eslint.config.js`)
- Prettier: 2-space tabs, no semis, single quotes, trailing commas
- WXML files parsed as HTML, WXSS as CSS
- Globals defined: `wx`, `App`, `Page`, `Component`, `getCurrentPages`, etc.
## Environment Configuration
Selected by App ID in `src/config.ts`:
- `wxf9ce8010f1ad24aa`: Dev/Staging (hbraas.com)
- `wx71ac9c27c3c3e3f4`: Production (hbsaas.com)
App reads `wx.getAccountInfoSync().miniProgram.appId` on launch to select config.
## Testing & Development
- No unit test framework configured
- Manual testing via WeChat Developer Tools
- `project.private.config.json` has hot reload enabled (`compileHotReLoad: true`)
- Predefined test pages in `project.private.config.json` condition list
## Common Gotchas
1. **NPM packages**: Must run "Build npm" in WeChat tools after install
2. **Images**: Will 404 if SVN images not checked out to `src/images/`
3. **Subpackages**: Patient pages are in `patient/` subpackage, not main package
4. **Skyline**: Enabled in rendererOptions, some components may behave differently
5. **Login flow**: App automatically calls `startLogin()` on launch; pages must wait via `app.zdWaitLogin()`
## 自定义导航栏规范
### 统一使用 navbar 组件
所有需要自定义导航的页面统一使用 `/components/navbar/index`。`/components/zd-navBar/navBar` 是历史遗留组件,新页面**不要使用**。
**JSON 配置:**
```json
{
"navigationStyle": "custom",
"usingComponents": {
"navbar": "/components/navbar/index"
}
}
```
**WXML 用法:**
```xml
<navbar fixed title="页面标题" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top:{{pageTop+20}}px;">
```
**TS 实现:**
```typescript
handleBack() {
wx.navigateBack()
}
```
### 关键参数
| 属性 | 说明 | 常用值 |
| -------------- | ------------ | --------------------------- |
| `fixed` | 固定导航栏 | `fixed` |
| `title` | 导航栏标题 | 字符串 |
| `custom-style` | 自定义样式 | `background:{{background}}` |
| `leftArrow` | 显示返回箭头 | Boolean |
| `slot="left"` | 左侧插槽 | van-icon 返回按钮 |
| `bind:tap` | 返回按钮点击 | `handleBack` |
### pageTop 变量
`pageTop``utils/page.ts` wrapper 自动注入,表示导航栏高度。页面内容区使用 `padding-top:{{pageTop+20}}px;` 避开导航栏。**不要手动计算 navBarHeight**。
## 返回拦截与弹窗规范
### 使用自定义弹窗替代系统弹窗
**禁止使用** `wx.showModal` 进行操作确认,统一使用项目内的 popup 组件:
```json
{
"usingComponents": {
"popup": "/components/popup/index"
}
}
```
```xml
<popup show="{{popupShow}}" type="{{popupType}}" params="{{popupParams}}" bind:ok="handlePopupOk" bind:cancel="handlePopupCancel" />
```
### 常用 popup 类型
| 类型 | 场景 | 标题 | 确认按钮 | 取消按钮 |
| --------- | -------------- | ------------------ | -------- | -------- |
| `popup15` | 删除确认 | "确认删除记录?" | 确认删除 | 取消 |
| `popup16` | 未保存数据退出 | "您的记录还未保存" | 保存记录 | 退出 |
| `popup17` | 裁剪未保存退出 | "您有裁剪的照片" | 退出 | 取消 |
### popup 数据定义
```typescript
data: {
popupShow: false,
popupType: 'popup16',
popupParams: {
close: false,
position: 'center',
} as any,
}
```
### 返回拦截模式
当页面有未保存数据时,使用自定义导航 + `bind:back` 拦截返回:
```typescript
handleBack() {
if (this.hasUnsavedData()) {
this.setData({ popupShow: true, popupType: 'popup16' })
} else {
wx.navigateBack()
}
}
handlePopupOk() {
this.setData({ popupShow: false })
this.handleSave()
}
handlePopupCancel() {
this.setData({ popupShow: false })
wx.navigateBack()
}
```
**注意**:`navigationStyle: "custom"` 后系统返回手势无法拦截,只有 navBar 的返回按钮可被拦截。如需拦截系统返回手势,需要额外使用 `wx.enableAlertBeforeUnload`
## wx.cropImage 规范
`wx.cropImage``src` 参数**只接受本地文件路径**,不支持网络 URL。裁剪网络图片时必须先用 `wx.getImageInfo` 下载到本地:
```typescript
handleCrop(e: any) {
const index = e.currentTarget.dataset.index
const photo = this.data.photos[index]
wx.showLoading({ title: '加载中...' })
wx.getImageInfo({
src: photo.photoUrl,
success: (imgRes) => {
wx.hideLoading()
wx.cropImage({
src: imgRes.path,
cropScale: '9:16',
success: (cropRes) => {
photos[index].croppedUrl = cropRes.tempFilePath
this.setData({ photos })
},
fail: (err) => {
if (err.errMsg?.includes('cancel')) return
wx.showToast({ title: '裁剪取消', icon: 'none' })
},
})
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '图片加载失败', icon: 'none' })
},
})
}
```
## 子包组件引用规范
主包页面(`src/pages/`)**不能引用**子包组件(`src/patient/components/`)。如果主包和子包都需要使用某个组件,需要将组件复制到 `src/components/` 目录下。
示例:`image-merge` 组件在 `patient/components/``components/` 各有一份,分别供子包和主包页面使用。

169
AGENTS.md

@ -1,169 +0,0 @@ @@ -1,169 +0,0 @@
# AGENTS.md - 信达小程序 (Xinda Mini Program)
WeChat Mini Program for thyroid eye disease patient management. Dual-mode app serving both patients (患者端) and doctors (医生端).
## Project Type
- **Framework**: WeChat Mini Program v3.7.7
- **Language**: TypeScript (strict mode)
- **Styling**: SCSS/Sass via `useCompilerPlugins`
- **UI Library**: Vant Weapp (@vant/weapp)
- **Renderer**: Skyline enabled
- **App ID**: `wxf9ce8010f1ad24aa` (dev), `wx71ac9c27c3c3e3f4` (prod)
## Directory Structure
```
src/
├── pages/ # Doctor-side main pages (医生端)
├── patient/pages/ # Patient-side pages (subpackage)
├── gift/pages/ # DTP pharmacy pages (subpackage)
├── doc/pages/ # Privacy/terms documentation (subpackage)
├── components/ # Shared components
├── utils/ # Request, page/component wrappers, utilities
├── app.ts # App entry with global data
└── config.ts # Environment configs per appId
```
## 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/*`
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',
cancelColor: '#141515',
})
```
### 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
# Lint and auto-fix (only command available)
npm run lint:fix
# Install dependencies (pnpm preferred based on lockfile)
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
- `mp-html`: Rich HTML rendering
## 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}}
```
## TypeScript Configuration
- Strict mode enabled
- `noImplicitAny: false` (allows implicit any)
- Paths: `@/*` mapped to `src/*`
- 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
## ESLint & Formatting
- Config: `@antfu/eslint-config` (flat config in `eslint.config.js`)
- Prettier: 2-space tabs, no semis, single quotes, trailing commas
- WXML files parsed as HTML, WXSS as CSS
- Globals defined: `wx`, `App`, `Page`, `Component`, `getCurrentPages`, etc.
## Environment Configuration
Selected by App ID in `src/config.ts`:
- `wxf9ce8010f1ad24aa`: Dev/Staging (hbraas.com)
- `wx71ac9c27c3c3e3f4`: Production (hbsaas.com)
App reads `wx.getAccountInfoSync().miniProgram.appId` on launch to select config.
## Testing & Development
- No unit test framework configured
- Manual testing via WeChat Developer Tools
- `project.private.config.json` has hot reload enabled (`compileHotReLoad: true`)
- Predefined test pages in `project.private.config.json` condition list
## Common Gotchas
1. **NPM packages**: Must run "Build npm" in WeChat tools after install
2. **Images**: Will 404 if SVN images not checked out to `src/images/`
3. **Subpackages**: Patient pages are in `patient/` subpackage, not main package
4. **Skyline**: Enabled in rendererOptions, some components may behave differently
5. **Login flow**: App automatically calls `startLogin()` on launch; pages must wait via `app.zdWaitLogin()`

8
src/components/image-merge/index.json

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
{
"component": true,
"usingComponents": {
"van-button": "@vant/weapp/button/index",
"van-icon": "@vant/weapp/icon/index",
"van-toast": "@vant/weapp/toast/index"
}
}

7
src/components/image-merge/index.scss

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
.merge-canvas {
position: fixed;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
}

186
src/components/image-merge/index.ts

@ -0,0 +1,186 @@ @@ -0,0 +1,186 @@
interface ImageItem {
src: string
time?: string
}
Component({
properties: {
id: {
type: String,
value: 'default',
},
},
data: {
imageList: [] as ImageItem[],
mergedImage: '',
isLoading: false,
},
methods: {
mergeImages(imageList: ImageItem[]) {
if (this.data.isLoading) {
return
}
if (!imageList || imageList.length < 2) {
wx.showToast({ title: '至少需要2张图片才能拼接', icon: 'none' })
return
}
wx.showLoading({ title: '拼接中...', mask: true })
this.setData({ isLoading: true, imageList })
this.validateImages(imageList)
.then(() => this.drawImagesOnCanvas())
.then((mergedImage) => {
wx.hideLoading()
this.setData({
mergedImage,
isLoading: false,
})
this.triggerEvent('save', { tempFilePath: mergedImage })
wx.previewImage({
urls: [mergedImage],
current: mergedImage,
showmenu: true,
})
})
.catch((error) => {
wx.hideLoading()
console.error('拼接失败:', error)
this.setData({ isLoading: false })
wx.showToast({ title: error.message || '拼接失败,请重试', icon: 'none' })
this.triggerEvent('error', { reason: error.message })
})
},
validateImages(imageList: ImageItem[]): Promise<void> {
return Promise.all(
imageList.map(
(item, index) =>
new Promise<void>((resolve, reject) => {
wx.getImageInfo({
src: item.src,
success: () => resolve(),
fail: () => reject(new Error(`${index + 1}张图片加载失败`)),
})
}),
),
)
},
formatTime(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
drawImagesOnCanvas(): Promise<string> {
return new Promise((resolve, reject) => {
const { imageList, id } = this.data
const query = this.createSelectorQuery()
query
.in(this)
.select(`#mergeCanvas-${id}`)
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) {
reject(new Error('Canvas not found'))
return
}
const canvas = res[0].node
const ctx = canvas.getContext('2d')
Promise.all(imageList.map(item => this.getImageInfo(item.src)))
.then((imageInfos) => {
const targetWidth = 750
const pixelRatio = wx.getWindowInfo().pixelRatio
const canvasWidth = Math.floor((targetWidth * pixelRatio) / 2)
let totalHeight = 0
const scaledHeights: number[] = []
imageInfos.forEach((info) => {
const scale = canvasWidth / info.width
const scaledHeight = Math.floor(info.height * scale)
scaledHeights.push(scaledHeight)
totalHeight += scaledHeight
})
canvas.width = canvasWidth
canvas.height = totalHeight
let currentY = 0
let loadedCount = 0
imageInfos.forEach((info, index) => {
const img = canvas.createImage()
img.src = info.path
img.onload = () => {
ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index])
const timeText = imageList[index].time || this.formatTime(new Date())
const padding = 20
const textY = currentY + 40
ctx.fillStyle = '#ffffff'
ctx.font = 'bold 28px sans-serif'
ctx.textAlign = 'left'
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
ctx.shadowBlur = 4
ctx.shadowOffsetX = 1
ctx.shadowOffsetY = 1
ctx.fillText(timeText, padding, textY)
ctx.shadowColor = 'transparent'
ctx.shadowBlur = 0
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 0
if (index === imageInfos.length - 1) {
const lastImageBottom = currentY + scaledHeights[index]
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
ctx.font = '24px sans-serif'
ctx.textAlign = 'right'
ctx.fillText('由-TED关爱小助手-小程序生成', canvasWidth - 20, lastImageBottom - 20)
}
currentY += scaledHeights[index]
loadedCount++
if (loadedCount === imageInfos.length) {
wx.canvasToTempFilePath({
canvas,
success: (result) => {
resolve(result.tempFilePath)
},
fail: reject,
})
}
}
img.onerror = () => {
reject(new Error(`Failed to load image: ${info.path}`))
}
})
})
.catch(reject)
})
})
},
getImageInfo(src: string): Promise<WechatMiniprogram.GetImageInfoSuccessCallbackResult> {
return new Promise((resolve, reject) => {
wx.getImageInfo({
src,
success: resolve,
fail: reject,
})
})
},
},
})

1
src/components/image-merge/index.wxml

@ -0,0 +1 @@ @@ -0,0 +1 @@
<canvas type="2d" id="mergeCanvas-{{id}}" class="merge-canvas"></canvas>

14
src/components/noteImagePreview/index.ts

@ -1,5 +1,17 @@ @@ -1,5 +1,17 @@
Component({
properties: {},
properties: {
visible: {
type: Boolean,
value: false,
observer(newVal) {
this.setData({ visible: newVal })
},
},
src: {
type: String,
value: '',
},
},
data: {
visible: false,

64
src/components/popup/index.scss

@ -596,47 +596,59 @@ @@ -596,47 +596,59 @@
}
.popup17 {
.badge {
position: relative;
z-index: 1;
display: block;
width: 144rpx;
height: 144rpx;
margin: 0 auto -72rpx;
text-align: center;
}
.popup-container {
width: 100vw;
width: 670rpx;
box-sizing: border-box;
padding: 0 48rpx 132rpx;
padding: 118rpx 48rpx 44rpx;
border-radius: 32rpx;
background: #fff;
box-sizing: border-box;
background: linear-gradient(360deg, #f1e6ff 0%, #f6f8f9 49.07%, #f6f8f9 100%);
.title {
padding: 32rpx 0;
font-size: 36rpx;
font-size: 40rpx;
color: #211d2e;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
}
.select {
margin-top: 32rpx;
.p-footer {
margin-top: 52rpx;
display: flex;
align-items: center;
gap: 34rpx;
.item {
gap: 30rpx;
.sure,
.cancel {
flex: 1;
padding: 64rpx;
text-align: center;
background: #ffffff;
border-radius: 24rpx 24rpx 24rpx 24rpx;
.icon {
width: 116rpx;
height: 116rpx;
}
.name {
font-size: 36rpx;
color: #211d2e;
}
font-size: 36rpx;
border-radius: 100rpx;
height: 88rpx;
box-sizing: border-box;
}
.cancel {
color: #b982ff;
border: 1px solid #b982ff;
display: flex;
align-items: center;
justify-content: center;
}
.sure {
padding: 0;
background: linear-gradient(344deg, #ffbcf9 0%, #b982ff 100%);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.close {
margin: 48rpx auto 0;
display: block;

14
src/components/popup/index.wxml

@ -227,6 +227,20 @@ @@ -227,6 +227,20 @@
</view>
</view>
</view>
<view class="popup17" wx:if="{{type==='popup17'}}">
<image class="badge" src="{{imageUrl}}icon156.png?t={{Timestamp}}"></image>
<view class="popup-container">
<view class="title">
您有裁剪的照片
<view></view>
现在退出会被清空
</view>
<view class="p-footer">
<view class="cancel" bind:tap="handleCancel">取消</view>
<view class="sure" bind:tap="handleOk">继续退出</view>
</view>
</view>
</view>
<image
wx:if="{{params.close}}"

36
src/pages/d_noteDetail/index.ts

@ -25,8 +25,15 @@ interface RecordDetail { @@ -25,8 +25,15 @@ interface RecordDetail {
Page({
data: {
patientId: '',
patientName: '',
recordId: '',
recordDetail: {} as RecordDetail,
photoMap: {} as Record<string, Photo>,
// 有照片的角度分组
frontendPhotos: [] as Photo[],
backendPhotos: [] as Photo[],
otherPhotos: [] as Photo[],
// 角度分组
angleGroups: {
@ -58,6 +65,7 @@ Page({ @@ -58,6 +65,7 @@ Page({
onLoad(option: any) {
this.setData({
patientId: option.patientId || '',
patientName: option.patientName || '',
recordId: option.recordId || '',
})
},
@ -81,8 +89,26 @@ Page({ @@ -81,8 +89,26 @@ Page({
},
}).then((res: any) => {
wx.hideLoading()
const photoMap: Record<string, Photo> = {}
const photos = res.photos || []
photos.forEach((p: Photo) => {
if (p.photoUrl) {
photoMap[p.photoAngle] = p
}
})
// 按分组过滤有照片的角度
const { frontend, backend, other } = this.data.angleGroups
const frontendPhotos = frontend.filter(angle => photoMap[angle]).map(angle => photoMap[angle])
const backendPhotos = backend.filter(angle => photoMap[angle]).map(angle => photoMap[angle])
const otherPhotos = other.filter(angle => photoMap[angle]).map(angle => photoMap[angle])
this.setData({
recordDetail: res,
photoMap,
frontendPhotos,
backendPhotos,
otherPhotos,
})
}).catch((err) => {
wx.hideLoading()
@ -91,12 +117,6 @@ Page({ @@ -91,12 +117,6 @@ Page({
})
},
// 获取指定角度的照片
getPhotoByAngle(angle: string): Photo | undefined {
const photos = this.data.recordDetail.photos || []
return photos.find((p: Photo) => p.photoAngle === angle)
},
// 返回上一页
handleBack() {
wx.navigateBack()
@ -105,7 +125,7 @@ Page({ @@ -105,7 +125,7 @@ Page({
// 预览图片
handlePreview(e: any) {
const { angle } = e.currentTarget.dataset
const photo = this.getPhotoByAngle(angle)
const photo = this.data.photoMap[angle]
if (photo) {
wx.previewImage({
urls: [photo.photoUrl],
@ -117,7 +137,7 @@ Page({ @@ -117,7 +137,7 @@ Page({
// 眼突度对比
handleDiffData() {
wx.navigateTo({
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}`,
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}&patientName=${this.data.patientName}`,
})
},

27
src/pages/d_noteDetail/index.wxml

@ -39,33 +39,30 @@ @@ -39,33 +39,30 @@
</view>
</view>
</view>
<view class="card">
<view class="card" wx:if="{{frontendPhotos.length > 0}}">
<view class="card-title">正面</view>
<view class="card-container">
<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 class="card-item" wx:for="{{frontendPhotos}}" wx:key="photoAngle" data-angle="{{item.photoAngle}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{item.photoUrl}}"></image>
<view class="name">{{item.photoAngleName}}</view>
</view>
</view>
</view>
<view class="card">
<view class="card" wx:if="{{backendPhotos.length > 0}}">
<view class="card-title">侧面</view>
<view class="card-container">
<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 class="card-item" wx:for="{{backendPhotos}}" wx:key="photoAngle" data-angle="{{item.photoAngle}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{item.photoUrl}}"></image>
<view class="name">{{item.photoAngleName}}</view>
</view>
</view>
</view>
<view class="card">
<view class="card" wx:if="{{otherPhotos.length > 0}}">
<view class="card-title">眼球运动八个方向</view>
<view class="card-container">
<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 class="card-item" wx:for="{{otherPhotos}}" wx:key="photoAngle" data-angle="{{item.photoAngle}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{item.photoUrl}}"></image>
<view class="name">{{item.photoAngleName}}</view>
</view>
</view>
</view>

5
src/pages/d_noteDiff/index.json

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
{
"navigationBarTitleText": "照片对比",
"usingComponents": {}
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index"
}
}

23
src/pages/d_noteDiff/index.scss

@ -3,7 +3,7 @@ page { @@ -3,7 +3,7 @@ page {
}
.page {
padding-bottom: 80rpx;
padding-bottom: 160rpx;
.page-tip {
padding: 24rpx 28rpx;
@ -272,4 +272,25 @@ page { @@ -272,4 +272,25 @@ page {
}
}
}
.footer-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 40rpx 48rpx;
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
.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;
}
}
}

29
src/pages/d_noteDiff/index.ts

@ -86,25 +86,17 @@ Page({ @@ -86,25 +86,17 @@ Page({
getCompareDates() {
if (!this.data.photoAngle)
return
// 医生端接口文档未提供 compare-dates:用记录列表推导 baseline/nonBaseline
wx.ajax({
method: 'GET',
url: '?r=xd/doctor/proptosis/list',
url: '?r=xd/doctor/proptosis/compare-dates',
data: {
patientId: this.data.patientId,
page: 1,
pageSize: 1000,
photoAngle: this.data.photoAngle,
},
}).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,
baseline: res.baseline || null,
nonBaselineList: res.nonBaselineList || [],
})
}).catch((err) => {
console.error('获取对比日期失败:', err)
@ -136,7 +128,12 @@ Page({ @@ -136,7 +128,12 @@ Page({
else {
selectedDates.push(recordId)
}
this.setData({ selectedDates })
// 更新列表中的选中状态
const nonBaselineList = this.data.nonBaselineList.map((item: CompareDate & { isSelected?: boolean }) => ({
...item,
isSelected: selectedDates.includes(item.recordId),
}))
this.setData({ selectedDates, nonBaselineList })
// 获取对比照片
this.getComparePhotos()
},
@ -187,7 +184,7 @@ Page({ @@ -187,7 +184,7 @@ Page({
})
})
this.setData({
comparePhotos: merged,
comparePhotos: merged.filter(item => item.photoUrl),
})
}).catch((err) => {
console.error('获取对比照片失败:', err)
@ -207,6 +204,10 @@ Page({ @@ -207,6 +204,10 @@ Page({
url: `/pages/d_noteDiffEdit/index?patientId=${this.data.patientId}&photoAngle=${this.data.photoAngle}&recordIds=${recordIds.join(',')}`,
})
},
handleBack() {
wx.navigateBack()
},
})
export {}

14
src/pages/d_noteDiff/index.wxml

@ -1,4 +1,8 @@ @@ -1,4 +1,8 @@
<view class="page">
<navbar fixed title="照片对比" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top:{{pageTop+20}}px;">
<view class="page-tip">
<image class="icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image>
<view class="content">
@ -19,7 +23,7 @@ @@ -19,7 +23,7 @@
<view class="title">选择对比日期(可多选)</view>
<view class="multiple">
<view
class="item {{selectedDates.indexOf(item.recordId) > -1 ? 'active' : ''}}"
class="item {{item.isSelected ? 'active' : ''}}"
wx:for="{{nonBaselineList}}"
wx:key="recordId"
data-record-id="{{item.recordId}}"
@ -74,8 +78,8 @@ @@ -74,8 +78,8 @@
</view>
</view>
</view>
<view class="footer">
<view class="btn1" bind:tap="handleEdit">生成对比图</view>
</view>
</view>
<view class="footer-fixed" wx:if="{{comparePhotos.length > 0}}">
<view class="btn1" bind:tap="handleEdit">生成对比图</view>
</view>
</view>

7
src/pages/d_noteDiffData/index.json

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
{
"navigationBarTitleText": "xxx的眼突度对比",
"navigationStyle": "default",
"usingComponents": {}
"navigationBarTitleText": "眼突度对比",
"usingComponents": {
"navbar": "/components/navbar/index"
}
}

71
src/pages/d_noteDiffData/index.ts

@ -18,9 +18,13 @@ Page({ @@ -18,9 +18,13 @@ Page({
},
onLoad(option: any) {
const patientName = option.patientName || ''
this.setData({
patientId: option.patientId || '',
})
if (patientName) {
wx.setNavigationBarTitle({ title: `${patientName}的眼突度对比` })
}
},
onShow() {
@ -40,7 +44,7 @@ Page({ @@ -40,7 +44,7 @@ Page({
data: {
patientId: this.data.patientId,
page: 1,
pageSize: 1000, // 尽量拉全,避免导出/对比缺数据
pageSize: 1000,
},
}).then((res: any) => {
const list: CompareItem[] = (res.list || [])
@ -57,69 +61,8 @@ Page({ @@ -57,69 +61,8 @@ Page({
})
},
// 导出Excel - 直接使用下载链接
handleExport() {
wx.showLoading({ title: '导出中...' })
// 构建导出URL:该接口为业务接口,优先使用 globalData.url
const baseUrl = String(app.globalData.url || '').replace(/\/$/, '')
const exportUrl = `${baseUrl}/?r=xd/doctor/proptosis/export&patientId=${this.data.patientId}`
wx.downloadFile({
url: exportUrl,
success: (downloadRes) => {
wx.hideLoading()
if (downloadRes.statusCode === 200) {
const filePath = downloadRes.tempFilePath
// 保存到本地文件系统
const fs = wx.getFileSystemManager()
const savedFilePath = `${wx.env.USER_DATA_PATH}/凸眼度对比_${new Date().toISOString().split('T')[0]}.xlsx`
fs.saveFile({
tempFilePath: filePath,
filePath: savedFilePath,
success: () => {
// 打开文档
wx.openDocument({
filePath: savedFilePath,
fileType: 'xlsx',
showMenu: true,
success: () => {
wx.showToast({ title: '导出成功', icon: 'success' })
},
fail: (err) => {
console.error('打开文档失败:', err)
wx.showToast({ title: '打开文件失败', icon: 'none' })
},
})
},
fail: (err) => {
console.error('保存文件失败:', err)
// 直接打开临时文件
wx.openDocument({
filePath,
fileType: 'xlsx',
showMenu: true,
success: () => {
wx.showToast({ title: '导出成功', icon: 'success' })
},
fail: () => {
wx.showToast({ title: '导出失败', icon: 'none' })
},
})
},
})
}
else {
wx.showToast({ title: '导出失败', icon: 'none' })
}
},
fail: (err) => {
wx.hideLoading()
console.error('下载失败:', err)
wx.showToast({ title: '导出失败', icon: 'none' })
},
})
handleBack() {
wx.navigateBack()
},
})

15
src/pages/d_noteDiffData/index.wxml

@ -1,6 +1,10 @@ @@ -1,6 +1,10 @@
<view class="container">
<navbar fixed title="眼突度对比" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="container" style="padding-top:{{pageTop+20}}px;">
<!-- 表格区域 -->
<view class="table-wrapper">
<view class="table-wrapper" wx:if="{{dataList.length > 0}}">
<!-- 吸顶表头 -->
<view class="table-header sticky">
<view class="th th-date">日期</view>
@ -33,8 +37,9 @@ @@ -33,8 +37,9 @@
</view>
</view>
<!-- 导出按钮 -->
<view class="footer">
<view class="export-btn" bind:tap="handleExport">导出Excel</view>
<!-- 空状态 -->
<view class="empty" wx:if="{{!loading && dataList.length === 0}}">
<text>暂无对比数据</text>
</view>
</view>

4
src/pages/d_noteDiffEdit/index.json

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

72
src/pages/d_noteDiffEdit/index.scss

@ -29,7 +29,7 @@ page { @@ -29,7 +29,7 @@ page {
width: 8rpx;
height: 32rpx;
background: #b982ff;
border-radius: 44rpx 44rpx 44rpx 44rpx;
border-radius: 44rpx;
}
}
@ -115,6 +115,7 @@ page { @@ -115,6 +115,7 @@ page {
.photo-card {
margin-top: 24rpx;
position: relative;
.photo {
border-radius: 32rpx;
@ -122,6 +123,27 @@ page { @@ -122,6 +123,27 @@ page {
display: block;
width: 100%;
}
.mask {
position: absolute;
bottom: 0;
left: 0;
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;
font-size: 28rpx;
color: #fff;
.icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
}
}
}
}
@ -137,27 +159,57 @@ page { @@ -137,27 +159,57 @@ page {
gap: 24rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
.btn1,
.btn2 {
.btn1 {
flex: 1;
height: 88rpx;
font-size: 32rpx;
font-size: 36rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100rpx;
}
.btn1 {
color: #ffffff;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
border-radius: 100rpx;
}
.btn2 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #b073ff;
background: rgba(176, 115, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
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-text {
font-size: 28rpx;
color: #999;
}
}
}

112
src/pages/d_noteDiffEdit/index.ts

@ -7,16 +7,23 @@ interface PhotoItem { @@ -7,16 +7,23 @@ interface PhotoItem {
recordDate: string
isBaseline: number
treatmentCount: number
isCropped?: boolean
croppedUrl?: string
}
Page({
data: {
popupShow: false,
popupType: 'popup17',
popupParams: {},
patientId: '',
photoAngle: '',
photoAngleName: '',
recordIds: [] as string[],
photos: [] as PhotoItem[],
loading: false,
mergedImage: '',
},
onLoad(option: any) {
@ -82,6 +89,8 @@ Page({ @@ -82,6 +89,8 @@ Page({
recordDate: baselineRes.recordDate,
isBaseline: 1,
treatmentCount: baselineRes.treatmentCount,
isCropped: false,
croppedUrl: '',
})
}
compareList.forEach((item: any) => {
@ -92,10 +101,12 @@ Page({ @@ -92,10 +101,12 @@ Page({
recordDate: item.recordDate,
isBaseline: 0,
treatmentCount: item.treatmentCount,
isCropped: false,
croppedUrl: '',
})
})
this.setData({
photos,
photos: photos.filter(item => item.photoUrl),
loading: false,
})
}).catch((err) => {
@ -105,9 +116,55 @@ Page({ @@ -105,9 +116,55 @@ Page({
})
},
// 点击裁剪
handleCrop(e: any) {
const index = e.currentTarget.dataset.index
const photos = this.data.photos
const photo = photos[index]
wx.showLoading({ title: '加载中...' })
wx.getImageInfo({
src: photo.photoUrl,
success: (imgRes) => {
wx.hideLoading()
wx.cropImage({
src: imgRes.path,
cropScale: '9:16',
success: (cropRes) => {
photos[index].isCropped = true
photos[index].croppedUrl = cropRes.tempFilePath
this.setData({ photos })
wx.showToast({ title: '裁剪成功', icon: 'success' })
},
fail: (err) => {
console.error('裁剪失败:', err)
if (err.errMsg?.includes('cancel')) {
return
}
wx.showToast({ title: '裁剪取消', icon: 'none' })
},
})
},
fail: (err) => {
wx.hideLoading()
console.error('图片加载失败:', err)
wx.showToast({ title: '图片加载失败', icon: 'none' })
},
})
},
// 点击还原
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, photoAngleName } = this.data
const { photos } = this.data
if (photos.length === 0) {
wx.showToast({ title: '暂无照片', icon: 'none' })
return
@ -120,21 +177,24 @@ Page({ @@ -120,21 +177,24 @@ Page({
? `基准照片 ${item.recordDate}`
: `对比照片 ${item.recordDate} 替妥尤单抗:${item.treatmentCount}`
return {
src: item.photoUrl,
src: item.croppedUrl || item.photoUrl,
time: label,
}
})
mergeComponent.mergeImages(imageList, {
title: `${photoAngleName}时间线对比`,
})
mergeComponent.mergeImages(imageList)
}
},
// 保存成功回调
onMergeSave(e: any) {
const { tempFilePath } = e.detail
// 保存到相册
handleSaveAlbum() {
const { mergedImage } = this.data
if (!mergedImage) {
this.handleMergePreview()
return
}
wx.saveImageToPhotosAlbum({
filePath: tempFilePath,
filePath: mergedImage,
success: () => {
wx.showToast({ title: '保存成功', icon: 'success' })
},
@ -145,12 +205,44 @@ Page({ @@ -145,12 +205,44 @@ Page({
})
},
// 保存成功回调
onMergeSave(e: any) {
const { tempFilePath } = e.detail
this.setData({ mergedImage: tempFilePath })
},
// 合并失败回调
onMergeError(e: any) {
const { error } = e.detail
console.error('合并图片失败:', error)
wx.showToast({ title: '生成对比图失败', icon: 'none' })
},
// 是否有裁剪过的照片
hasCroppedPhoto(): boolean {
return this.data.photos.some(item => item.isCropped)
},
// 返回上一页
handleBack() {
if (this.hasCroppedPhoto()) {
this.setData({ popupShow: true })
}
else {
wx.navigateBack()
}
},
// popup 确认(继续退出)
handlePopupOk() {
this.setData({ popupShow: false })
wx.navigateBack()
},
// popup 取消
handlePopupCancel() {
this.setData({ popupShow: false })
},
})
export {}

33
src/pages/d_noteDiffEdit/index.wxml

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
<view class="page">
<view class="container">
<navbar fixed title="对比图编辑" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top:{{pageTop+20}}px;">
<view class="container" wx:if="{{photos.length > 0}}">
<view class="title">
{{photoAngleName}}时间线对比
<view class="date">生成日期:{{photos[0].recordDate}}</view>
@ -18,15 +22,36 @@ @@ -18,15 +22,36 @@
<view class="tag tag2">替妥尤单抗:{{item.treatmentCount}}</view>
</view>
<view class="photo-card">
<image class="photo" src="{{item.photoUrl}}" mode="aspectFill"></image>
<image class="photo" src="{{item.croppedUrl || 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 bind:tap="handleRestore" data-index="{{index}}">
<image class="icon" src="{{imageUrl}}icon166.png?t={{Timestamp}}"></image>
点击还原
</view>
</view>
</view>
</view>
<view class="footer">
<view class="btn1" bind:tap="handleMergePreview">对比图预览</view>
<view class="btn2" bind:tap="handleMergePreview">保存到相册</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}}">
<text>暂无对比照片</text>
</view>
</view>
<imageMerge id="merge" bindsave="onMergeSave" binderror="onMergeError" />
<popup id="popup" show="{{popupShow}}" type="{{popupType}}" params="{{popupParams}}" bind:ok="handlePopupOk" bind:cancel="handlePopupCancel" />

5
src/pages/d_noteList/index.json

@ -1,7 +1,8 @@ @@ -1,7 +1,8 @@
{
"navigationStyle": "default",
"usingComponents": {
"van-icon": "@vant/weapp/icon/index"
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index",
"pagination": "/components/pagination/index"
},
"navigationBarTitleText": "记录"
}

21
src/pages/d_noteList/index.ts

@ -16,17 +16,24 @@ interface RecordItem { @@ -16,17 +16,24 @@ interface RecordItem {
Page({
data: {
patientId: '',
patientName: '',
recordList: [] as RecordItem[],
total: 0,
page: 1,
pageSize: 10,
loading: false,
hasMore: true,
pagination: {
count: 0,
page: 1,
pages: 1,
},
},
onLoad(option: any) {
this.setData({
patientId: option.patientId || '',
patientName: option.patientName || '',
})
},
@ -57,6 +64,7 @@ Page({ @@ -57,6 +64,7 @@ Page({
}).then((res: any) => {
const list = res.list || []
const total = res.pagination?.total || 0
const pages = Math.ceil(total / this.data.pageSize) || 1
this.setData({
recordList: reset ? list : [...this.data.recordList, ...list],
@ -64,6 +72,11 @@ Page({ @@ -64,6 +72,11 @@ Page({
page: page + 1,
hasMore: list.length >= this.data.pageSize,
loading: false,
pagination: {
count: total,
page,
pages,
},
})
}).catch((err) => {
console.error('获取记录列表失败:', err)
@ -80,14 +93,14 @@ Page({ @@ -80,14 +93,14 @@ Page({
handleHistory(e: any) {
const { recordId } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/d_noteDetail/index?recordId=${recordId}&patientId=${this.data.patientId}`,
url: `/pages/d_noteDetail/index?recordId=${recordId}&patientId=${this.data.patientId}&patientName=${this.data.patientName}`,
})
},
// 眼突度对比
handleDiffData() {
wx.navigateTo({
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}`,
url: `/pages/d_noteDiffData/index?patientId=${this.data.patientId}&patientName=${this.data.patientName}`,
})
},
@ -97,6 +110,10 @@ Page({ @@ -97,6 +110,10 @@ Page({
url: `/pages/d_noteDiff/index?patientId=${this.data.patientId}`,
})
},
handleBack() {
wx.navigateBack()
},
})
export {}

15
src/pages/d_noteList/index.wxml

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
<view class="page">
<view class="total">共{{total}}条日记记录</view>
<navbar fixed title="记录" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top:{{pageTop+20}}px;">
<view class="total" wx:if="{{recordList.length > 0}}">共{{total}}条日记记录</view>
<view 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%">
@ -13,10 +17,11 @@ @@ -13,10 +17,11 @@
</view>
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image>
</view>
<pagination pagination="{{pagination}}" customEmpty>
<view class="empty">暂无日记记录</view>
</pagination>
</view>
<view class="loading" wx:if="{{loading}}">加载中...</view>
<view class="no-more" wx:if="{{!hasMore && recordList.length > 0}}">没有更多了</view>
<view class="footer">
<view class="footer" wx:if="{{recordList.length > 0}}">
<view class="btn1" bind:tap="handleDiffData">眼突度 对比</view>
<view class="btn2" bind:tap="handlePhotoCompare">照片 对比</view>
</view>

7
src/pages/d_patientDetail/index.ts

@ -913,14 +913,15 @@ Page({ @@ -913,14 +913,15 @@ Page({
url: `/pages/d_qolDetail/index?id=${this.data.detail.PatientId}`,
})
},
handleNoteDetail() {
handleNoteDetail(e: any) {
const recordId = e.currentTarget.dataset.recordid
wx.navigateTo({
url: `/pages/d_noteDetail/index?patientId=${this.data.detail.PatientId}`,
url: `/pages/d_noteDetail/index?patientId=${this.data.detail.PatientId}&patientName=${this.data.detail.Name}&recordId=${recordId}`,
})
},
handleNoteList() {
wx.navigateTo({
url: `/pages/d_noteList/index?patientId=${this.data.detail.PatientId}`,
url: `/pages/d_noteList/index?patientId=${this.data.detail.PatientId}&patientName=${this.data.detail.Name}`,
})
},
})

15
src/pages/d_patientDetail/index.wxml

@ -72,28 +72,29 @@ @@ -72,28 +72,29 @@
<image wx:else class="icon" src="{{imageUrl}}icon66.png?t={{Timestamp}}"></image>
<view class="content">标识为EDC患者</view>
</view>
<view class="note">
<view class="note" wx:if="{{proptosisDetail.recordId}}">
<view class="n-title">
突眼日记
<view class="sub">(可查看患者突眼、眼突度)</view>
</view>
<view class="n-container">
<view class="n-card" bind:tap="handleNoteDetail">
<view class="n-card" bind:tap="handleNoteDetail" data-recordid="{{proptosisDetail.recordId}}">
<view
class="benchmark"
wx:if="{{proptosisDetail.isBaseline === 1}}"
style="background: url('/images/bg56.png') no-repeat top center/100%"
>
基准照
</view>
<image class="photo" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
<image class="photo" src="{{proptosisDetail.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">{{proptosisDetail.recordDate}}</view>
<view class="tag">替妥尤单抗:{{proptosisDetail.treatmentCount}}</view>
<view class="rotate">已上传{{proptosisDetail.photoCount}}个角度</view>
</view>
<image class="more" src="/images/icon168.png"></image>
</view>
<view class="btn" bind:tap="handleNoteList">共3条日记记录,点击查看全部</view>
<view class="btn" bind:tap="handleNoteList">共{{proptosisDetail.totalRecords || 0}}条日记记录,点击查看全部</view>
</view>
</view>
<view class="kkd">

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

@ -372,7 +372,7 @@ Component({ @@ -372,7 +372,7 @@ Component({
const recordId = this.properties.recordId
wx.ajax({
method: 'POST',
url: '?r=xd/proptosis/photo-upload',
url: '?r=xd/proptosis/photo-checker',
data: {
...(recordId ? { recordId } : {}),
sessionId,

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

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="container" style="padding-top:{{pageTop+20}}px">
<view class="container" style="padding-top:{{pageTop+20}}px;">
<!-- 基准照片设置卡片 - 未设置状态 -->
<view wx:if="{{!hasBaseline}}" class="setting-card-empty" bindtap="addRecord">
<view class="setting-header">

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

@ -35,7 +35,7 @@ Page({ @@ -35,7 +35,7 @@ Page({
popupType: 'popup16',
popupParams: {
close: false,
position: 'bottom',
position: 'center',
} as any,
imagePreview: false,
@ -62,6 +62,10 @@ Page({ @@ -62,6 +62,10 @@ Page({
// 替妥尤单抗使用次数选项 (0-9, 9表示大于8)
treatmentCountRange: ['0次', '1次', '2次', '3次', '4次', '5次', '6次', '7次', '8次', '大于8次'],
hasUnsavedData: false,
isCapturing: false,
cameraList: {
frontend: [
{ name: '睁眼', type: 'front_open', angle: 'front_open' },
@ -98,6 +102,10 @@ Page({ @@ -98,6 +102,10 @@ Page({
},
onShow() {
if (this.data.isCapturing) {
this.setData({ isCapturing: false })
return
}
app.waitLogin({ type: [1] }).then(() => {
if (this.data.recordId) {
this.getRecordDetail()
@ -162,9 +170,8 @@ Page({ @@ -162,9 +170,8 @@ Page({
// 是否设置为基准照
onBaselineChange(e: any) {
const values: string[] = e?.detail?.value || []
this.setData({
isBaseline: values.length > 0 ? 1 : 0,
isBaseline: e.detail.value ? 1 : 0,
})
},
@ -194,6 +201,7 @@ Page({ @@ -194,6 +201,7 @@ Page({
const { angle } = e.currentTarget.dataset
this.setData({
currentPhotoAngle: angle,
isCapturing: true,
})
// 调用 camera 组件的 handleSelect 方法,传入类型
const cameraComponent = this.selectComponent('#camera-component')
@ -216,6 +224,7 @@ Page({ @@ -216,6 +224,7 @@ Page({
sequentialShootMode: true,
sequentialShootList: shootOrder,
sequentialShootIndex: 0,
isCapturing: true,
})
// 开始第一个拍摄
this.startSequentialShoot()
@ -338,6 +347,7 @@ Page({ @@ -338,6 +347,7 @@ Page({
this.setData({
photoMap,
sequentialShootIndex: nextIndex,
hasUnsavedData: true,
})
// 延迟一下再打开下一个相机,给用户反馈时间
setTimeout(() => {
@ -345,7 +355,7 @@ Page({ @@ -345,7 +355,7 @@ Page({
}, 500)
}
else {
this.setData({ photoMap })
this.setData({ photoMap, hasUnsavedData: true })
}
},
@ -401,8 +411,21 @@ Page({ @@ -401,8 +411,21 @@ Page({
return
}
// 获取所有已上传的照片ID
const photoIds = Object.values(photoMap).map(photo => photo.photoId).join(',')
// 获取所有已上传的照片
const photos = Object.values(photoMap)
if (photos.length === 0) {
wx.showToast({ title: '请至少上传一张照片', icon: 'none' })
return
}
// 所有照片全不合规才不允许保存
const hasCompliantPhoto = photos.some(photo => photo.checkStatus === 1)
if (!hasCompliantPhoto) {
wx.showToast({ title: '所有照片不合规,请重新上传', icon: 'none' })
return
}
const photoIds = photos.map(photo => photo.photoId).join(',')
const data: any = {
...(recordId ? { recordId } : {}),
@ -428,6 +451,7 @@ Page({ @@ -428,6 +451,7 @@ Page({
data,
}).then(() => {
wx.hideLoading()
this.setData({ hasUnsavedData: false })
wx.showToast({
title: '保存成功',
icon: 'success',
@ -452,16 +476,26 @@ Page({ @@ -452,16 +476,26 @@ Page({
this.setData({
popupShow: false,
})
this.handleSave()
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
wx.navigateBack()
},
handleBack() {
wx.navigateBack()
if (this.data.hasUnsavedData) {
this.setData({
popupShow: true,
popupType: 'popup16',
})
}
else {
wx.navigateBack()
}
},
})

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

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top: {{pageTop+20}}px;">
<view class="page" style="padding-top:{{pageTop+20}}px;">
<view class="benchmark">
<checkbox-group bindchange="onBaselineChange">
<checkbox class="checkbox" color="#fff" value="1" checked="{{isBaseline === 1}}">设置为基准记录,用于对比</checkbox>
@ -70,13 +70,13 @@ @@ -70,13 +70,13 @@
正面
</view>
<view class="upload-container">
<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">
<view class="upload-item" wx:for="{{cameraList.frontend}}" wx:key="angle">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview" data-angle="{{item.angle}}">
<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 class="guide" catchtap="handleCamera" data-angle="{{item.angle}}">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
@ -92,13 +92,13 @@ @@ -92,13 +92,13 @@
侧面
</view>
<view class="upload-container">
<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">
<view class="upload-item" wx:for="{{cameraList.backend}}" wx:key="angle">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview" data-angle="{{item.angle}}">
<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 class="guide" catchtap="handleCamera" data-angle="{{item.angle}}">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
@ -114,13 +114,13 @@ @@ -114,13 +114,13 @@
眼球运动八个方向
</view>
<view class="upload-container">
<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">
<view class="upload-item" wx:for="{{cameraList.other}}" wx:key="angle">
<view class="upload-preview" wx:if="{{photoMap[item.angle]}}" bind:tap="handlePreview" data-angle="{{item.angle}}">
<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 class="guide" catchtap="handleCamera" data-angle="{{item.angle}}">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-angle="{{item.angle}}">
@ -143,6 +143,7 @@ @@ -143,6 +143,7 @@
id="note-image-preview"
visible="{{imagePreview}}"
src="{{previewImageSrc}}"
bind:close="handleImagePreviewClose"
bind:delete="handleImageDel"
bind:retake="handleImageRetake"
></noteImagePreview>

4
src/patient/pages/noteDemo/index.wxml

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
<navbar fixed title="记录新照片" custom-style="background:{{background}}">
<navbar fixed title="标准拍照示范" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view
class="page"
style="background: url('{{imageUrl}}bg51.png?t={{Timestamp}}') no-repeat top center/100% 716rpx;padding-top: {{pageTop+120}}px;"
style="background: url('{{imageUrl}}bg51.png?t={{Timestamp}}') no-repeat top center/100% 716rpx;padding-top:{{pageTop+120}}px;"
>
<view class="card" style="background: #fff url('{{imageUrl}}bg52.png?t={{Timestamp}}') no-repeat top center/100% 210rpx">
<view class="tip">拍摄时,正对镜头,面部居中,露出完整双眼及眼眶;光线均匀无阴影。</view>

2
src/patient/pages/noteDiff/index.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"navigationStyle": "default",
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index"
},
"navigationBarTitleText": "照片对比分析"

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

@ -14,6 +14,7 @@ interface ComparePhoto { @@ -14,6 +14,7 @@ interface ComparePhoto {
interface CompareDate {
recordId: string
recordDate: string
isSelected?: boolean
}
Page({
@ -100,9 +101,14 @@ Page({ @@ -100,9 +101,14 @@ Page({
photoAngle: this.data.photoAngle,
},
}).then((res: any) => {
const selectedDates = this.data.selectedDates
const nonBaselineList = (res.nonBaselineList || []).map((item: CompareDate) => ({
...item,
isSelected: selectedDates.includes(item.recordId),
}))
this.setData({
baseline: res.baseline || null,
nonBaselineList: res.nonBaselineList || [],
nonBaselineList,
})
}).catch((err) => {
console.error('获取对比日期失败:', err)
@ -133,7 +139,12 @@ Page({ @@ -133,7 +139,12 @@ Page({
else {
selectedDates.push(recordId)
}
this.setData({ selectedDates })
// 更新列表中的选中状态
const nonBaselineList = this.data.nonBaselineList.map((item: CompareDate) => ({
...item,
isSelected: selectedDates.includes(item.recordId),
}))
this.setData({ selectedDates, nonBaselineList })
// 获取对比照片
this.getComparePhotos()
},
@ -155,8 +166,10 @@ Page({ @@ -155,8 +166,10 @@ Page({
recordIds: recordIds.join(','),
},
}).then((res: any) => {
// 过滤掉 photoUrl 为空的数据
const photos = (res.photos || []).filter((item: any) => item.photoUrl)
this.setData({
comparePhotos: res.photos || [],
comparePhotos: photos,
})
}).catch((err) => {
console.error('获取对比照片失败:', err)
@ -183,6 +196,10 @@ Page({ @@ -183,6 +196,10 @@ Page({
url: `/patient/pages/noteDiffEdit/index?photoAngle=${this.data.photoAngle}&recordIds=${recordIds.join(',')}`,
})
},
handleBack() {
wx.navigateBack()
},
})
export {}

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

@ -1,3 +1,7 @@ @@ -1,3 +1,7 @@
<navbar fixed title="照片对比分析" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page-none" wx:if="{{!hasBaseline}}">
<image class="icon" src="{{imageUrl}}none3.png?t={{Timestamp}}"></image>
<view class="title">未设置基准照片</view>
@ -8,7 +12,7 @@ @@ -8,7 +12,7 @@
</view>
<view class="btn" bind:tap="goSetBaseline">去设置</view>
</view>
<view class="page" wx:if="{{hasBaseline}}">
<view class="page" wx:if="{{hasBaseline}}" style="padding-top:{{pageTop+20}}px;">
<view class="page-tip">
<image class="icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image>
<view class="content">
@ -28,9 +32,9 @@ @@ -28,9 +32,9 @@
<view class="form-item">
<view class="title">选择对比日期(可多选)</view>
<view class="multiple">
<view
class="item {{selectedDates.indexOf(item.recordId) > -1 ? 'active' : ''}}"
wx:for="{{nonBaselineList}}"
<view
class="item {{item.isSelected ? 'active' : ''}}"
wx:for="{{nonBaselineList}}"
wx:key="recordId"
data-record-id="{{item.recordId}}"
bind:tap="onDateSelect"

5
src/patient/pages/noteDiffEdit/index.json

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
{
"navigationStyle": "default",
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index",
"imageMerge": "/patient/components/image-merge/index"
"imageMerge": "/patient/components/image-merge/index",
"popup": "/components/popup/index"
},
"navigationBarTitleText": "照片对比分析"
}

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

@ -15,6 +15,10 @@ interface PhotoItem { @@ -15,6 +15,10 @@ interface PhotoItem {
Page({
data: {
popupShow: false,
popupType: 'popup17',
popupParams: {},
photoAngle: '',
photoAngleName: '',
recordIds: [] as string[],
@ -100,8 +104,34 @@ Page({ @@ -100,8 +104,34 @@ Page({
const photos = this.data.photos
const photo = photos[index]
wx.navigateTo({
url: `/patient/pages/noteCrop/index?photoId=${photo.photoId}&photoUrl=${photo.photoUrl}&index=${index}`,
wx.showLoading({ title: '加载中...' })
wx.getImageInfo({
src: photo.photoUrl,
success: (imgRes) => {
wx.hideLoading()
wx.cropImage({
src: imgRes.path,
cropScale: '9:16',
success: (cropRes) => {
photos[index].isCropped = true
photos[index].croppedUrl = cropRes.tempFilePath
this.setData({ photos })
wx.showToast({ title: '裁剪成功', icon: 'success' })
},
fail: (err) => {
console.error('裁剪失败:', err)
if (err.errMsg?.includes('cancel')) {
return
}
wx.showToast({ title: '裁剪取消', icon: 'none' })
},
})
},
fail: (err) => {
wx.hideLoading()
console.error('图片加载失败:', err)
wx.showToast({ title: '图片加载失败', icon: 'none' })
},
})
},
@ -174,6 +204,32 @@ Page({ @@ -174,6 +204,32 @@ Page({
console.error('合并图片失败:', reason)
wx.showToast({ title: '生成对比图失败', icon: 'none' })
},
// 是否有裁剪过的照片
hasCroppedPhoto(): boolean {
return this.data.photos.some(item => item.isCropped)
},
// 返回上一页
handleBack() {
if (this.hasCroppedPhoto()) {
this.setData({ popupShow: true })
}
else {
wx.navigateBack()
}
},
// popup 确认(继续退出)
handlePopupOk() {
this.setData({ popupShow: false })
wx.navigateBack()
},
// popup 取消
handlePopupCancel() {
this.setData({ popupShow: false })
},
})
export {}

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

@ -1,4 +1,8 @@ @@ -1,4 +1,8 @@
<view class="page">
<navbar fixed title="照片对比分析" custom-style="background:{{background}}">
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" />
</navbar>
<view class="page" style="padding-top:{{pageTop+20}}px;">
<view class="container" wx:if="{{!loading && photos.length > 0}}">
<view class="title">
{{photoAngleName}}时间线对比
@ -18,7 +22,7 @@ @@ -18,7 +22,7 @@
<view class="tag tag2">替妥尤单抗:{{item.treatmentCount}}</view>
</view>
<view class="photo-card">
<image class="photo" src="{{item.photoUrl}}" mode="aspectFill"></image>
<image class="photo" src="{{item.croppedUrl || 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>
点击裁剪
@ -50,3 +54,5 @@ @@ -50,3 +54,5 @@
</view>
<imageMerge id="merge" bindsave="onMergeSave" binderror="onMergeError" />
<popup id="popup" show="{{popupShow}}" type="{{popupType}}" params="{{popupParams}}" bind:ok="handlePopupOk" bind:cancel="handlePopupCancel" />

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

@ -20,6 +20,10 @@ interface RecordDetail { @@ -20,6 +20,10 @@ interface RecordDetail {
photos: Photo[]
}
interface PhotoMap {
[key: string]: Photo
}
Page({
data: {
popupShow: false,
@ -31,6 +35,17 @@ Page({ @@ -31,6 +35,17 @@ Page({
recordId: '',
recordDetail: {} as RecordDetail,
// 照片映射表
photoMap: {} as PhotoMap,
// 各组是否有照片
hasFrontend: false,
hasBackend: false,
hasOther: false,
// 已上传照片数量
uploadedCount: 0,
// 角度分组
angleGroups: {
frontend: ['front_open', 'front_closed', 'front_looking_up'],
@ -83,8 +98,31 @@ Page({ @@ -83,8 +98,31 @@ Page({
},
}).then((res: any) => {
wx.hideLoading()
// 构建照片映射表
const photoMap: PhotoMap = {}
const photos = res.photos || []
photos.forEach((p: Photo) => {
if (p.photoUrl) {
photoMap[p.photoAngle] = p
}
})
// 判断各组是否有照片(只计算有 photoUrl 的照片)
const { frontend, backend, other } = this.data.angleGroups
const hasFrontend = frontend.some(angle => photoMap[angle]?.photoUrl)
const hasBackend = backend.some(angle => photoMap[angle]?.photoUrl)
const hasOther = other.some(angle => photoMap[angle]?.photoUrl)
// 计算有 photoUrl 的照片数量
const uploadedCount = photos.filter((p: Photo) => p.photoUrl).length
this.setData({
recordDetail: res,
photoMap,
hasFrontend,
hasBackend,
hasOther,
uploadedCount,
})
}).catch((err) => {
wx.hideLoading()
@ -105,6 +143,24 @@ Page({ @@ -105,6 +143,24 @@ Page({
return photos.length
},
// 判断正面照片组是否有照片
hasFrontendPhotos(): boolean {
const { frontend } = this.data.angleGroups
return frontend.some(angle => this.getPhotoByAngle(angle))
},
// 判断侧面照片组是否有照片
hasBackendPhotos(): boolean {
const { backend } = this.data.angleGroups
return backend.some(angle => this.getPhotoByAngle(angle))
},
// 判断眼球运动组是否有照片
hasOtherPhotos(): boolean {
const { other } = this.data.angleGroups
return other.some(angle => this.getPhotoByAngle(angle))
},
// 返回上一页
handleBack() {
wx.navigateBack()
@ -119,16 +175,9 @@ Page({ @@ -119,16 +175,9 @@ Page({
// 删除记录
handleDelete() {
wx.showModal({
title: '确认删除',
content: '确定要删除这条记录吗?删除后无法恢复。',
confirmColor: '#8c75d0',
cancelColor: '#141515',
success: (res) => {
if (res.confirm) {
this.doDelete()
}
},
this.setData({
popupShow: true,
popupType: 'popup15',
})
},
@ -160,7 +209,7 @@ Page({ @@ -160,7 +209,7 @@ Page({
// 预览图片
handlePreview(e: any) {
const { angle } = e.currentTarget.dataset
const photo = this.getPhotoByAngle(angle)
const photo = this.data.photoMap[angle]
if (photo) {
wx.previewImage({
urls: [photo.photoUrl],
@ -180,6 +229,7 @@ Page({ @@ -180,6 +229,7 @@ Page({
this.setData({
popupShow: false,
})
this.doDelete()
},
handlePopupCancel() {

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

@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
<image class="tip-icon" src="{{imageUrl}}icon154.png?t={{Timestamp}}"></image>
<view class="wrap">
<view class="title">记录未录入完全</view>
<view class="content">当前已录入{{getUploadedCount()}}/15个角度。您可以点击右上角的编辑按钮继续补充照片。</view>
<view class="content">当前已录入{{uploadedCount}}/15个角度。您可以点击右上角的编辑按钮继续补充照片。</view>
</view>
<view class="btn" bind:tap="handleSupplement">去补充</view>
</view>
@ -64,36 +64,33 @@ @@ -64,36 +64,33 @@
</view>
<!-- 正面照片 -->
<view class="card">
<view class="card" wx:if="{{hasFrontend}}">
<view class="card-title">正面</view>
<view class="card-container">
<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="card-item" wx:for="{{angleGroups.frontend}}" wx:key="*this" wx:if="{{photoMap[item]}}" data-angle="{{item}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{photoMap[item].photoUrl}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<!-- 侧面照片 -->
<view class="card">
<view class="card" wx:if="{{hasBackend}}">
<view class="card-title">侧面</view>
<view class="card-container">
<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="card-item" wx:for="{{angleGroups.backend}}" wx:key="*this" wx:if="{{photoMap[item]}}" data-angle="{{item}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{photoMap[item].photoUrl}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>
</view>
<!-- 眼球运动八个方向 -->
<view class="card">
<view class="card" wx:if="{{hasOther}}">
<view class="card-title">眼球运动八个方向</view>
<view class="card-container">
<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="card-item" wx:for="{{angleGroups.other}}" wx:key="*this" wx:if="{{photoMap[item]}}" data-angle="{{item}}" bind:tap="handlePreview">
<image class="photo" mode="aspectFill" src="{{photoMap[item].photoUrl}}"></image>
<view class="name">{{angleNameMap[item]}}</view>
</view>
</view>

69
接口文档.md

@ -156,7 +156,7 @@ @@ -156,7 +156,7 @@
#### 业务流程(修订后)
1. 前端:点击保存按钮时,先调用 photo-upload 上传照片(此时没有recordId,返回临时photoId)
1. 前端:点击保存按钮时,先调用 photo-checker 上传照片(此时没有recordId,返回临时photoId)
2. 前端:然后调用 record-save 保存基本信息(含photoIds),将照片关联到记录
3. 这样设计是因为只有一个保存按钮,需要先创建记录,再上传照片
@ -170,7 +170,7 @@ @@ -170,7 +170,7 @@
#### 接口信息
- **接口路径**: `/?r=xd/proptosis/photo-upload`
- **接口路径**: `/?r=xd/proptosis/photo-checker`
- **请求方式**: POST
- **代码位置**: `shop/modules/xd_frontend/controllers/ProptosisController.php`
- **认证方式**: 内部获取当前登录用户ID
@ -196,7 +196,7 @@ @@ -196,7 +196,7 @@
#### 业务流程(调整后)
1. 前端:先调用 photo-upload 上传照片,返回 photoId
1. 前端:先调用 photo-checker 上传照片,返回 photoId
2. 前端:再调用 record-save,传入 photoIds 关联照片和记录
#### 同步保存数据
@ -371,40 +371,6 @@ @@ -371,40 +371,6 @@
#### 返回字段
| 字段名 | 类型 | 说明 |
| ------------------------- | ------ | ------- |
| recordId | string | 记录ID |
| photoAngle.side_right_45 | string | 45°右侧 |
| photoAngle.eye_up_left | string | 左上 |
| photoAngle.eye_up | string | 向上 |
| photoAngle.eye_up_right | string | 右上 |
| photoAngle.eye_left | string | 向左 |
| photoAngle.eye_right | string | 向右 |
| photoAngle.eye_down_left | string | 左下 |
| photoAngle.eye_down | string | 向下 |
| photoAngle.eye_down_right | string | 右下 |
---
## 三、医生端接口
### 3.1 获取患者最近的突眼记录
#### 接口信息
- **接口路径**: `/?r=xd/doctor/proptosis/latest`
- **请求方式**: GET
- **代码位置**: `shop/modules/xd_frontend/controllers/doctor/ProptosisController.php`
- **认证方式**: 内部获取当前登录医生ID
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| --------- | ------ | ---- | ------ |
| patientId | string | 是 | 患者ID |
#### 返回字段
| 字段名 | 类型 | 说明 |
| -------------------- | ------- | --------------------- |
| recordId | string | 记录ID |
@ -601,6 +567,35 @@ @@ -601,6 +567,35 @@
---
### 3.7 获取对比照片可用日期
#### 接口信息
- **接口路径**: `/?r=xd/doctor/proptosis/compare-dates`
- **请求方式**: GET
- **代码位置**: `shop/modules/xd_frontend/controllers/doctor/ProptosisController.php`
- **认证方式**: 内部获取当前登录医生ID
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| ---------- | ------ | ---- | -------- |
| patientId | string | 是 | 患者ID |
| photoAngle | string | 是 | 照片角度 |
#### 返回字段
| 字段名 | 类型 | 说明 |
| ---------------------------- | ------ | ---------------- |
| baseline | object | 基准照信息 |
| baseline.recordId | string | 记录ID |
| baseline.recordDate | string | 记录日期 |
| nonBaselineList[] | array | 非基准照日期列表 |
| nonBaselineList[].recordId | string | 记录ID |
| nonBaselineList[].recordDate | string | 记录日期 |
---
## 四、管理后台接口
### 4.1 获取图片列表

Loading…
Cancel
Save