Browse Source

突眼日记页面开发

dev
kola-web 3 weeks ago
parent
commit
b5f8e6ba78
  1. 134
      AGENTS.md
  2. 2
      project.config.json
  3. 79
      project.private.config.json
  4. 11
      src/app.json
  5. 12
      src/app.ts
  6. 6
      src/components/noteImagePreview/index.json
  7. 75
      src/components/noteImagePreview/index.scss
  8. 56
      src/components/noteImagePreview/index.ts
  9. 12
      src/components/noteImagePreview/index.wxml
  10. 31
      src/components/patient-tab-bar/index.scss
  11. 16
      src/components/patient-tab-bar/index.ts
  12. 7
      src/components/patient-tab-bar/index.wxml
  13. 148
      src/components/popup/index.scss
  14. 24
      src/components/popup/index.wxml
  15. 7
      src/pages/d_noteDetail/index.json
  16. 168
      src/pages/d_noteDetail/index.scss
  17. 88
      src/pages/d_noteDetail/index.ts
  18. 74
      src/pages/d_noteDetail/index.wxml
  19. 5
      src/pages/d_noteDiffData/index.json
  20. 148
      src/pages/d_noteDiffData/index.scss
  21. 25
      src/pages/d_noteDiffData/index.ts
  22. 35
      src/pages/d_noteDiffData/index.wxml
  23. 7
      src/pages/d_noteList/index.json
  24. 106
      src/pages/d_noteList/index.scss
  25. 93
      src/pages/d_noteList/index.ts
  26. 21
      src/pages/d_noteList/index.wxml
  27. 96
      src/pages/d_patientDetail/index.scss
  28. 10
      src/pages/d_patientDetail/index.ts
  29. 26
      src/pages/d_patientDetail/index.wxml
  30. 3
      src/patient/components/camera/index.json
  31. 410
      src/patient/components/camera/index.scss
  32. 223
      src/patient/components/camera/index.ts
  33. 115
      src/patient/components/camera/index.wxml
  34. 57
      src/patient/components/image-merge/index.ts
  35. 4
      src/patient/pages/imageProcessing/index.wxml
  36. 1
      src/patient/pages/index/index.scss
  37. 7
      src/patient/pages/note/index.json
  38. 269
      src/patient/pages/note/index.scss
  39. 35
      src/patient/pages/note/index.ts
  40. 70
      src/patient/pages/note/index.wxml
  41. 10
      src/patient/pages/noteAdd/index.json
  42. 309
      src/patient/pages/noteAdd/index.scss
  43. 132
      src/patient/pages/noteAdd/index.ts
  44. 146
      src/patient/pages/noteAdd/index.wxml
  45. 7
      src/patient/pages/noteDemo/index.json
  46. 55
      src/patient/pages/noteDemo/index.scss
  47. 11
      src/patient/pages/noteDemo/index.ts
  48. 86
      src/patient/pages/noteDemo/index.wxml
  49. 7
      src/patient/pages/noteDiff/index.json
  50. 284
      src/patient/pages/noteDiff/index.scss
  51. 13
      src/patient/pages/noteDiff/index.ts
  52. 85
      src/patient/pages/noteDiff/index.wxml
  53. 8
      src/patient/pages/noteDiffEdit/index.json
  54. 157
      src/patient/pages/noteDiffEdit/index.scss
  55. 19
      src/patient/pages/noteDiffEdit/index.ts
  56. 40
      src/patient/pages/noteDiffEdit/index.wxml
  57. 8
      src/patient/pages/noteHistory/index.json
  58. 183
      src/patient/pages/noteHistory/index.scss
  59. 94
      src/patient/pages/noteHistory/index.ts
  60. 98
      src/patient/pages/noteHistory/index.wxml
  61. 220
      src/utils/captcha.ts

134
AGENTS.md

@ -0,0 +1,134 @@
# 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',
})
```
## 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()`

2
project.config.json

@ -71,5 +71,5 @@
} }
] ]
}, },
"appid": "wx71ac9c27c3c3e3f4" "appid": "wxf9ce8010f1ad24aa"
} }

79
project.private.config.json

@ -23,29 +23,92 @@
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "patient/pages/imageProcessing/index", "name": "医生端-眼突度-对比",
"pathName": "patient/pages/imageProcessing/index", "pathName": "pages/d_noteDiffData/index",
"query": "", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "患者-qol", "name": "医生端-日记列表",
"pathName": "patient/pages/qol/index", "pathName": "pages/d_noteList/index",
"query": "pushId=81", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "医生-患者量表", "name": "医生-突眼日记单条记录",
"pathName": "pages/d_qolDetail/index", "pathName": "pages/d_noteDetail/index",
"query": "id=178", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "医生-患者详情", "name": "医生-患者详情",
"pathName": "pages/d_patientDetail/index", "pathName": "pages/d_patientDetail/index",
"query": "id=319",
"launchMode": "default",
"scene": null
},
{
"name": "突眼日记照片对比编辑",
"pathName": "patient/pages/noteDiffEdit/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "眼突日记照片对比",
"pathName": "patient/pages/noteDiff/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "突眼日记-历史记录",
"pathName": "patient/pages/noteHistory/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "突眼日记拍照示例",
"pathName": "patient/pages/noteDemo/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "突眼日记添加",
"pathName": "patient/pages/noteAdd/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "突眼日记",
"pathName": "patient/pages/note/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "patient/pages/imageProcessing/index",
"pathName": "patient/pages/imageProcessing/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "患者-qol",
"pathName": "patient/pages/qol/index",
"query": "pushId=81",
"launchMode": "default",
"scene": null
},
{
"name": "医生-患者量表",
"pathName": "pages/d_qolDetail/index",
"query": "id=178", "query": "id=178",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null

11
src/app.json

@ -16,7 +16,10 @@
"pages/d_invite/index", "pages/d_invite/index",
"pages/d_patient/index", "pages/d_patient/index",
"pages/d_patientHormones/index", "pages/d_patientHormones/index",
"pages/d_qolDetail/index" "pages/d_qolDetail/index",
"pages/d_noteDetail/index",
"pages/d_noteList/index",
"pages/d_noteDiffData/index"
], ],
"subPackages": [ "subPackages": [
{ {
@ -72,6 +75,12 @@
"pages/hormonesResult/index", "pages/hormonesResult/index",
"pages/medical/index", "pages/medical/index",
"pages/medicalDetail/index", "pages/medicalDetail/index",
"pages/note/index",
"pages/noteAdd/index",
"pages/noteDemo/index",
"pages/noteHistory/index",
"pages/noteDiff/index",
"pages/noteDiffEdit/index",
"pages/imageProcessing/index" "pages/imageProcessing/index"
] ]
}, },

12
src/app.ts

@ -18,15 +18,15 @@ App<IAppOption>({
// 测试号 wx2b0bb13edf717c1d // 测试号 wx2b0bb13edf717c1d
// dev // dev
// appid:wxf9ce8010f1ad24aa // appid:wxf9ce8010f1ad24aa
// url: 'https://m.xd.hbraas.com', url: 'https://m.xd.hbraas.com',
// upFileUrl: 'https://m.xd.hbraas.com/', upFileUrl: 'https://m.xd.hbraas.com/',
// imageUrl: 'https://m.xd.hbraas.com/xd/', imageUrl: 'https://m.xd.hbraas.com/xd/',
// pro // pro
// appid:wx71ac9c27c3c3e3f4 // appid:wx71ac9c27c3c3e3f4
url: 'https://m.xd.hbsaas.com', // url: 'https://m.xd.hbsaas.com',
upFileUrl: 'https://m.xd.hbsaas.com/', // upFileUrl: 'https://m.xd.hbsaas.com/',
imageUrl: 'https://m.xd.hbsaas.com/api/xd/', // imageUrl: 'https://m.xd.hbsaas.com/api/xd/',
loginState: '', loginState: '',
isLogin: 0, isLogin: 0,

6
src/components/noteImagePreview/index.json

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

75
src/components/noteImagePreview/index.scss

@ -0,0 +1,75 @@
.preview-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000;
z-index: 1000;
display: flex;
flex-direction: column;
opacity: 0;
visibility: hidden;
transition:
opacity 0.3s,
visibility 0.3s;
&.show {
opacity: 1;
visibility: visible;
}
}
// 图片展示区域
.image-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.preview-image {
display: block;
width: 100%;
height: 100%;
}
}
// 底部操作栏
.action-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 40rpx;
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
background: #fff;
display: flex;
gap: 32rpx;
.btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
&-delete {
border: 1px solid #b982ff;
color: #b982ff;
background: #fff;
}
&-retake {
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
color: #fff;
}
&:active {
opacity: 0.8;
}
}
}

56
src/components/noteImagePreview/index.ts

@ -0,0 +1,56 @@
Component({
properties: {},
data: {
visible: false,
src: '',
},
methods: {
// 返回/关闭
handleBack() {
this.triggerEvent('close')
},
// 更多操作
handleMore() {
this.triggerEvent('more')
},
// 预览/查看
handlePreview(src) {
if (src) {
this.setData({
visible: true,
src,
})
}
},
handleHidePreview() {
this.setData({
visible: false,
sec: '',
})
},
// 删除
handleDelete() {
wx.showModal({
title: '提示',
content: '确定要删除这张照片吗?',
success: (res) => {
if (res.confirm) {
this.handleHidePreview()
this.triggerEvent('delete')
}
},
})
},
// 重拍
handleRetake() {
this.handleHidePreview()
this.triggerEvent('retake')
},
},
})

12
src/components/noteImagePreview/index.wxml

@ -0,0 +1,12 @@
<view class="preview-container {{visible ? 'show' : ''}}">
<!-- 图片展示区域 -->
<view class="image-wrapper">
<image class="preview-image" src="{{src}}" mode="aspectFit" />
</view>
<!-- 底部操作栏 -->
<view class="action-bar">
<view class="btn btn-delete" bindtap="handleDelete">删除</view>
<view class="btn btn-retake" bindtap="handleRetake">重拍</view>
</view>
</view>

31
src/components/patient-tab-bar/index.scss

@ -1,6 +1,37 @@
/* custom-tab-bar/index.wxss */ /* custom-tab-bar/index.wxss */
.tab-custom-item {
flex: 1;
z-index: 1;
.circle {
display: block;
margin: 0 auto;
margin-top: -53rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 3px solid #fff;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
display: flex;
align-items: center;
justify-content: center;
.icon {
width: 60rpx;
height: 60rpx;
}
}
.name {
font-size: 26rpx;
color: rgba(105, 104, 110, 1);
text-align: center;
&.active {
color: rgba(33, 29, 46, 1);
}
}
}
.tab-item { .tab-item {
flex: 1;
.icon { .icon {
width: 48rpx; width: 48rpx;
height: 48rpx; height: 48rpx;

16
src/components/patient-tab-bar/index.ts

@ -1,4 +1,5 @@
import { getCurrentPageUrl } from '@/utils/util' import { getCurrentPageUrl } from '@/utils/util'
const app = getApp() const app = getApp()
Component({ Component({
@ -21,6 +22,11 @@ Component({
iconActive: 'tab-active2', iconActive: 'tab-active2',
}, },
{ {
pagePath: '/patient/pages/note/index',
text: '突眼日记',
custom: true,
},
{
pagePath: '/patient/pages/live/index', pagePath: '/patient/pages/live/index',
text: '大咖说', text: '大咖说',
icon: 'tab5', icon: 'tab5',
@ -48,7 +54,7 @@ Component({
}) })
const pagePath = getCurrentPageUrl() const pagePath = getCurrentPageUrl()
const active = this.data.list.findIndex((item) => item.pagePath === pagePath) const active = this.data.list.findIndex(item => item.pagePath === pagePath)
this.setData({ this.setData({
active, active,
}) })
@ -72,8 +78,14 @@ Component({
handleNav(e) { handleNav(e) {
const { index } = e.currentTarget.dataset const { index } = e.currentTarget.dataset
const { list } = this.data const { list } = this.data
const pagePath = list[index].pagePath const { pagePath, custom } = list[index]
app.globalData.BeginnerCardId = '' app.globalData.BeginnerCardId = ''
if (custom) {
wx.navigateTo({
url: pagePath,
})
return
}
wx.reLaunch({ wx.reLaunch({
url: pagePath, url: pagePath,
}) })

7
src/components/patient-tab-bar/index.wxml

@ -1,6 +1,13 @@
<van-tabbar active="{{ active }}" active-color="#CF5375" bind:change="onChange" inactive-color="#CCCCCC"> <van-tabbar active="{{ active }}" active-color="#CF5375" bind:change="onChange" inactive-color="#CCCCCC">
<block wx:for="{{list}}" wx:key="*this"> <block wx:for="{{list}}" wx:key="*this">
<view class="tab-custom-item" wx:if="{{item.custom}}" bind:tap="handleNav" data-index="{{index}}">
<view class="circle">
<image class="icon" src="{{imageUrl}}icon142.png?t={{Timestamp}}"></image>
</view>
<view class="name">突眼日记</view>
</view>
<van-tabbar-item <van-tabbar-item
wx:else
class="tab-item" class="tab-item"
bind:tap="handleNav" bind:tap="handleNav"
data-index="{{index}}" data-index="{{index}}"

148
src/components/popup/index.scss

@ -393,7 +393,7 @@
margin-top: 32rpx; margin-top: 32rpx;
padding: 0 32rpx; padding: 0 32rpx;
font-size: 32rpx; font-size: 32rpx;
color: #211D2E; color: #211d2e;
line-height: 48rpx; line-height: 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@ -491,6 +491,152 @@
} }
} }
.popup15 {
.badge {
position: relative;
z-index: 1;
display: block;
width: 144rpx;
height: 144rpx;
margin: 0 auto -72rpx;
text-align: center;
}
.popup-container {
width: 670rpx;
box-sizing: border-box;
padding: 118rpx 48rpx 16rpx;
border-radius: 32rpx;
background: #fff;
.title {
font-size: 40rpx;
color: #211d2e;
font-weight: bold;
text-align: center;
}
.content {
margin-top: 20rpx;
font-size: 32rpx;
color: #69686e;
line-height: 48rpx;
text-align: center;
.date {
color: #b982ff;
}
}
.btn {
margin-top: 44rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
font-size: 36rpx;
color: #ffffff;
background: linear-gradient(344deg, #ffbcf9 0%, #b982ff 100%);
border-radius: 100rpx;
}
.cancel {
padding: 32rpx;
font-size: 32rpx;
color: #b982ff;
text-align: center;
}
}
}
.popup16 {
.badge {
position: relative;
z-index: 1;
display: block;
width: 144rpx;
height: 144rpx;
margin: 0 auto -72rpx;
text-align: center;
}
.popup-container {
width: 670rpx;
box-sizing: border-box;
padding: 118rpx 48rpx 44rpx;
border-radius: 32rpx;
background: #fff;
.title {
font-size: 40rpx;
color: #211d2e;
font-weight: bold;
text-align: center;
}
.p-footer {
margin-top: 52rpx;
display: flex;
gap: 30rpx;
.sure,
.cancel {
flex: 1;
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;
}
}
}
}
.popup17 {
.popup-container {
width: 100vw;
box-sizing: border-box;
padding: 0 48rpx 132rpx;
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;
color: #211d2e;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.select {
margin-top: 32rpx;
display: flex;
align-items: center;
gap: 34rpx;
.item {
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;
}
}
}
}
}
.close { .close {
margin: 48rpx auto 0; margin: 48rpx auto 0;
display: block; display: block;

24
src/components/popup/index.wxml

@ -203,6 +203,30 @@
<view class="btn" bind:tap="handleOk">继续</view> <view class="btn" bind:tap="handleOk">继续</view>
</view> </view>
</view> </view>
<view class="popup15" wx:if="{{type==='popup15'}}">
<image class="badge" src="{{imageUrl}}icon155.png?t={{Timestamp}}"></image>
<view class="popup-container">
<view class="title">确认删除记录?</view>
<view class="content">
删除
<text class="date">2026-04-02</text>
记录
<view>此操作不可逆,相关照片将永久删除</view>
</view>
<view class="btn" bind:tap="handleOk">确认删除</view>
<view class="cancel" bind:tap="handleCancel">取消</view>
</view>
</view>
<view class="popup16" wx:if="{{type==='popup16'}}">
<image class="badge" src="{{imageUrl}}icon156.png?t={{Timestamp}}"></image>
<view class="popup-container">
<view class="title">您的记录还未保存</view>
<view class="p-footer">
<view class="cancel" bind:tap="handleCancel">退出</view>
<view class="sure" bind:tap="handleOk">保存记录</view>
</view>
</view>
</view>
<image <image
wx:if="{{params.close}}" wx:if="{{params.close}}"

7
src/pages/d_noteDetail/index.json

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

168
src/pages/d_noteDetail/index.scss

@ -0,0 +1,168 @@
.page {
.container {
position: relative;
padding: 40rpx 40rpx 240rpx;
border-radius: 32rpx 32rpx 0 0;
background-color: #fff;
.benchmark {
position: absolute;
top: 0;
right: 0;
padding-top: 8rpx;
text-align: center;
width: 144rpx;
height: 64rpx;
font-size: 28rpx;
color: #fff;
line-height: 32rpx;
}
.date {
font-size: 40rpx;
color: #211d2e;
font-weight: bold;
}
.total {
margin-top: 32rpx;
padding-top: 48rpx;
font-size: 32rpx;
color: #211d2e;
border-top: 1px solid rgba(173, 172, 178, 0.1);
display: flex;
gap: 22rpx;
align-items: center;
.num {
font-size: 40rpx;
color: #b073ff;
}
&::before {
content: '';
width: 8rpx;
height: 32rpx;
background: #b982ff;
border-radius: 44rpx 44rpx 44rpx 44rpx;
}
}
.banner {
margin-top: 24rpx;
padding: 32rpx;
background: #f6f8f9;
border-radius: 32rpx 32rpx 32rpx 32rpx;
border: 2rpx solid #ffffff;
.title {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.row {
margin-top: 24rpx;
display: flex;
gap: 24rpx;
text-align: center;
.col {
padding: 24rpx;
flex: 1;
background-color: #fff;
border-radius: 16rpx;
.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;
}
}
}
}
}
.card {
margin-top: 48rpx;
.card-title {
display: flex;
align-items: center;
gap: 16rpx;
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
&::before {
content: '';
width: 8rpx;
height: 32rpx;
background: #b982ff;
border-radius: 44rpx 44rpx 44rpx 44rpx;
}
}
.card-container {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(2, 320rpx);
justify-content: space-between;
gap: 30rpx;
.card-item {
.photo {
display: block;
width: 100%;
height: 320rpx;
border-radius: 24rpx;
}
.name {
margin-top: 16rpx;
font-size: 28rpx;
color: #adacb2;
text-align: center;
}
}
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
box-sizing: border-box;
margin-top: 46rpx;
display: flex;
align-items: center;
gap: 22rpx;
background-color: #fff;
box-shadow: 0 -10rpx 10rpx rgba(204, 204, 204, 0.1);
.btn1 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #b982ff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #b982ff;
background-color: #fff;
border-radius: 100rpx 100rpx 100rpx 100rpx;
}
.btn2 {
flex: 1;
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 100rpx 100rpx 100rpx;
}
}
}

88
src/pages/d_noteDetail/index.ts

@ -0,0 +1,88 @@
const _app = getApp<IAppOption>()
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: '',
},
],
},
},
onLoad() {},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
},
})
export {}

74
src/pages/d_noteDetail/index.wxml

@ -0,0 +1,74 @@
<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}}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 class="total">
当前记录对应的替妥尤单抗使用次数:
<text class="num">1</text>
</view>
<view 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="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">眶间距</view>
<view class="content">
<view class="num">12</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="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>
</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>
</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>
</view>
</view>
</view>
<view class="footer">
<view class="btn1">眼突度 对比</view>
<view class="btn2">照片 对比</view>
</view>
</view>

5
src/pages/d_noteDiffData/index.json

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "xxx的眼突度对比",
"navigationStyle": "default",
"usingComponents": {}
}

148
src/pages/d_noteDiffData/index.scss

@ -0,0 +1,148 @@
.container {
min-height: 100vh;
background-color: #f6f8f9;
padding: 16rpx 30rpx;
box-sizing: border-box;
}
.table-wrapper {
background-color: #eee2ff;
border-radius: 13rpx;
border: 1rpx solid #fff;
overflow: hidden;
}
/* 表头样式 */
.table-header {
display: flex;
background-color: #f7f0ff;
&.sticky {
position: sticky;
top: 0;
z-index: 100;
}
}
.th {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #b982ff;
font-size: 28rpx;
font-weight: 500;
box-sizing: border-box;
}
.th-date {
width: 200rpx;
height: 120rpx;
border-right: 1rpx solid #eee4ff;
justify-content: flex-end;
padding-bottom: 20rpx;
}
.th-eye {
flex: 1;
padding: 0;
border-right: 1rpx solid #eee4ff;
}
.th-eye-title {
text-align: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #eee4ff;
width: 100%;
font-size: 28rpx;
height: 60rpx;
box-sizing: border-box;
}
.th-eye-sub {
display: flex;
width: 100%;
height: 60rpx;
}
.th-sub-item {
flex: 1;
text-align: center;
border-right: 1rpx solid #eee4ff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
&:last-child {
border-right: none;
}
}
.th-count {
width: 120rpx;
text-align: center;
line-height: 1.3;
font-size: 28rpx;
height: 120rpx;
justify-content: center;
}
/* 表格内容样式 */
.table-body {
background-color: #eee2ff;
}
.tr {
display: flex;
height: 100rpx;
&:last-child {
.td-date,
.td-eye,
.td-count {
border-bottom: none;
}
}
}
.td {
display: flex;
align-items: center;
justify-content: center;
color: #211d2e;
font-size: 32rpx;
font-weight: 500;
box-sizing: border-box;
height: 100rpx;
}
.td-date {
width: 200rpx;
background-color: #ffffff;
border-right: 1rpx solid #eee4ff;
border-bottom: 1rpx solid #eee4ff;
}
.td-eye {
flex: 1;
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #eee4ff;
}
.td-eye-item {
flex: 1;
text-align: center;
border-right: 1rpx solid #eee4ff;
display: flex;
align-items: center;
justify-content: center;
height: 100rpx;
}
.td-count {
width: 120rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #eee4ff;
}

25
src/pages/d_noteDiffData/index.ts

@ -0,0 +1,25 @@
const _app = getApp<IAppOption>();
Page({
data: {
dataList: [
{
date: '2026.1.5',
left: '40',
spacing: '40',
right: '40',
count: '30',
},
{
date: '2025.12.30',
left: '30',
spacing: '30',
right: '30',
count: '20',
},
],
},
onLoad() {},
});
export {};

35
src/pages/d_noteDiffData/index.wxml

@ -0,0 +1,35 @@
<view class="container">
<!-- 表格区域 -->
<view class="table-wrapper">
<!-- 吸顶表头 -->
<view class="table-header sticky">
<view class="th th-date">日期</view>
<view class="th th-eye">
<view class="th-eye-title">眼突度(mm)</view>
<view class="th-eye-sub">
<view class="th-sub-item">左侧</view>
<view class="th-sub-item">眶间距</view>
<view class="th-sub-item">右侧</view>
</view>
</view>
<view class="th th-count">
<view>替妥尤</view>
<view>输注</view>
<view>次数</view>
</view>
</view>
<!-- 表格内容 -->
<view class="table-body">
<view class="tr" wx:for="{{dataList}}" wx:key="date">
<view class="td td-date">{{item.date}}</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>
<view class="td td-count">{{item.count}}</view>
</view>
</view>
</view>
</view>

7
src/pages/d_noteList/index.json

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

106
src/pages/d_noteList/index.scss

@ -0,0 +1,106 @@
page {
background-color: #f6f8f9;
}
.page {
.total {
padding: 32rpx 40rpx;
font-size: 32rpx;
color: #211d2e;
}
.history-list {
padding: 0 40rpx 240rpx;
.list-item {
margin-bottom: 24rpx;
position: relative;
border-radius: 32rpx;
background-color: #fff;
padding: 32rpx;
display: flex;
justify-content: space-between;
gap: 24rpx;
.benchmark {
position: absolute;
top: 0;
right: 0;
padding-top: 8rpx;
text-align: center;
width: 124rpx;
height: 64rpx;
font-size: 28rpx;
color: #ffa300;
line-height: 32rpx;
}
.photo {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
}
.wrap {
flex: 1;
.date {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.tag {
margin-top: 16rpx;
padding: 4rpx 12rpx;
display: inline-block;
font-size: 24rpx;
color: #b073ff;
background: rgba(176, 115, 255, 0.16);
border-radius: 6rpx 6rpx 6rpx 6rpx;
}
.rotate {
margin-top: 28rpx;
font-size: 28rpx;
color: #adacb2;
line-height: 1;
}
}
.more {
align-self: center;
width: 44rpx;
height: 44rpx;
}
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
box-sizing: border-box;
margin-top: 46rpx;
display: flex;
align-items: center;
gap: 22rpx;
background-color: #fff;
box-shadow: 0 -10rpx 10rpx rgba(204, 204, 204, 0.1);
.btn1 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #b982ff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #b982ff;
background-color: #fff;
border-radius: 100rpx 100rpx 100rpx 100rpx;
}
.btn2 {
flex: 1;
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 100rpx 100rpx 100rpx;
}
}
}

93
src/pages/d_noteList/index.ts

@ -0,0 +1,93 @@
const _app = getApp<IAppOption>()
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: '',
},
],
},
},
onLoad() {},
handleDiffData() {
wx.navigateTo({
url: '/pages/d_noteDiffData/index',
})
},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
},
})
export {}

21
src/pages/d_noteList/index.wxml

@ -0,0 +1,21 @@
<view class="page">
<view class="total">共X条日记记录</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 class="wrap">
<view class="date">2026-04-01</view>
<view class="tag">替妥尤单抗:2</view>
<view class="rotate">已上传1个角度</view>
</view>
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image>
</view>
</view>
<view class="footer">
<view class="btn1" bind:tap="handleDiffData">眼突度 对比</view>
<view class="btn2">照片 对比</view>
</view>
</view>

96
src/pages/d_patientDetail/index.scss

@ -142,6 +142,102 @@ page {
font-weight: bold; font-weight: bold;
} }
} }
.note {
margin: 48rpx 30rpx 0;
.n-title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 36rpx;
color: #211d2e;
font-weight: bold;
&::before {
content: '';
width: 8rpx;
height: 36rpx;
background: #b982ff;
border-radius: 2rpx;
}
.sub {
align-self: flex-end;
font-size: 32rpx;
color: #adacb2;
font-weight: normal;
}
}
.n-container {
margin-top: 24rpx;
padding: 32rpx;
background: #ffffff;
border-radius: 32rpx 32rpx 32rpx 32rpx;
.n-card {
position: relative;
margin-top: 24rpx;
border-radius: 32rpx;
background-color: #f6f8f9;
padding: 24rpx;
display: flex;
justify-content: space-between;
gap: 24rpx;
.benchmark {
position: absolute;
top: 0;
right: 0;
padding-top: 8rpx;
text-align: center;
width: 124rpx;
height: 64rpx;
font-size: 28rpx;
color: #fff;
line-height: 28rpx;
}
.photo {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
}
.wrap {
flex: 1;
.date {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.tag {
margin-top: 16rpx;
padding: 4rpx 12rpx;
display: inline-block;
font-size: 24rpx;
color: #b073ff;
background: rgba(176, 115, 255, 0.16);
border-radius: 6rpx 6rpx 6rpx 6rpx;
}
.rotate {
margin-top: 28rpx;
font-size: 28rpx;
color: #adacb2;
line-height: 1;
}
}
.more {
align-self: center;
width: 44rpx;
height: 44rpx;
}
}
.btn {
margin-top: 24rpx;
height: 76rpx;
font-size: 32rpx;
color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(270deg, #e98ff8 0%, #b073ff 100%);
border-radius: 72rpx 72rpx 72rpx 72rpx;
}
}
}
.kkd { .kkd {
margin: 48rpx 30rpx 0; margin: 48rpx 30rpx 0;
.k-title { .k-title {

10
src/pages/d_patientDetail/index.ts

@ -892,4 +892,14 @@ Page({
url: `/pages/d_qolDetail/index?id=${this.data.detail.PatientId}`, url: `/pages/d_qolDetail/index?id=${this.data.detail.PatientId}`,
}) })
}, },
handleNoteDetail() {
wx.navigateTo({
url: '/pages/d_noteDetail/index',
})
},
handleNoteList() {
wx.navigateTo({
url: '/pages/d_noteList/index',
})
},
}) })

26
src/pages/d_patientDetail/index.wxml

@ -72,9 +72,33 @@
<image wx:else class="icon" src="{{imageUrl}}icon66.png?t={{Timestamp}}"></image> <image wx:else class="icon" src="{{imageUrl}}icon66.png?t={{Timestamp}}"></image>
<view class="content">标识为EDC患者</view> <view class="content">标识为EDC患者</view>
</view> </view>
<view class="note">
<view class="n-title">
突眼日记
<view class="sub">(可查看患者突眼、眼突度)</view>
</view>
<view class="n-container">
<view class="n-card" bind:tap="handleNoteDetail">
<view
class="benchmark"
style="background: url('/images/bg56.png') no-repeat top center/100%"
>
基准照
</view>
<image class="photo" src="{{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>
<image class="more" src="/images/icon168.png"></image>
</view>
<view class="btn" bind:tap="handleNoteList">共3条日记记录,点击查看全部</view>
</view>
</view>
<view class="kkd"> <view class="kkd">
<view class="k-title"> <view class="k-title">
患者健康图表 健康量
<view class="sub">最近数值</view> <view class="sub">最近数值</view>
</view> </view>
<view class="k-container"> <view class="k-container">

3
src/patient/components/camera/index.json

@ -2,6 +2,7 @@
"component": true, "component": true,
"usingComponents": { "usingComponents": {
"van-icon": "@vant/weapp/icon/index", "van-icon": "@vant/weapp/icon/index",
"van-button": "@vant/weapp/button/index" "van-button": "@vant/weapp/button/index",
"van-popup": "@vant/weapp/popup/index"
} }
} }

410
src/patient/components/camera/index.scss

@ -9,130 +9,380 @@
flex-direction: column; flex-direction: column;
background-color: #000; background-color: #000;
z-index: 1000; z-index: 1000;
.camera-container {
width: 100%;
height: 100%;
} }
.camera { .camera {
flex: 1; flex: 1;
width: 100%; width: 100%;
position: relative; position: relative;
text-align: center;
.example {
position: relative;
margin: 120rpx 32rpx 0;
width: 202rpx;
height: 56rpx;
font-size: 32rpx;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.33);
border-radius: 124rpx 124rpx 124rpx 124rpx;
.icon {
width: 36rpx;
height: 36rpx;
} }
&.active {
.camera-frame { background: linear-gradient(180deg, #e98ff8 0%, #b073ff 100%);
}
.example-popup {
padding: 32rpx;
position: absolute; position: absolute;
top: 50%; left: 8rpx;
left: 50%; bottom: -32rpx;
transform: translate(-50%, -50%); width: 670rpx;
width: 80%; transform: translateY(100%);
aspect-ratio: 3 / 4; background-color: #fff;
pointer-events: none; border-radius: 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.3); box-sizing: border-box;
.content {
text-align: left;
font-size: 36rpx;
color: #69686e;
line-height: 48rpx;
}
.photo-wrap {
position: relative;
.label {
position: absolute;
top: 20rpx;
right: -20rpx;
width: 110rpx;
height: 78rpx;
} }
.photo {
margin-top: 32rpx;
width: 606rpx;
max-height: 304rpx;
}
}
&::before {
content: '';
position: absolute;
top: -20rpx;
left: 62rpx;
width: 0;
height: 0;
border-style: solid;
border-width: 0 29rpx 50rpx 29rpx;
border-color: transparent transparent #fff transparent;
}
}
}
.camera-frame {
position: relative;
display: block;
margin: 38rpx auto 0;
height: 50vh;
aspect-ratio: 4/5;
pointer-events: none;
.frame-corner { // 公共 icon 基础样式
%icon-base {
position: absolute; position: absolute;
width: 40rpx; width: 40rpx;
height: 40rpx; height: 105rpx;
border: 4rpx solid #fff;
} }
.left-top { .icon8 {
top: 0; @extend %icon-base;
left: 0; top: 11.5vh;
border-right: none; left: 4vh;
border-bottom: none; transform: rotate(-45deg);
animation: 1s ease-in-out infinite slidein8;
} }
.right-top { .icon9-1 {
top: 0; @extend %icon-base;
right: 0; top: 9vh;
border-left: none; left: 10.7vh;
border-bottom: none; transform: rotate(0);
animation: 1s ease-in-out infinite slidein9;
} }
.left-bottom { .icon9-2 {
bottom: 0; @extend %icon-base;
left: 0; top: 9vh;
border-right: none; right: 10.7vh;
border-top: none; transform: rotate(0);
animation: 1s ease-in-out infinite slidein9;
} }
.right-bottom { .icon10 {
bottom: 0; @extend %icon-base;
right: 0; top: 22.5vh;
border-left: none; left: 4vh;
border-top: none; transform: rotate(-135deg);
animation: 1s ease-in-out infinite slidein10;
} }
.close-btn { .icon11 {
position: fixed; @extend %icon-base;
top: 40rpx; top: 17vh;
right: 40rpx; left: 4vh;
width: 60rpx; transform: rotate(-90deg);
height: 60rpx; animation: 1s ease-in-out infinite slidein11;
display: flex; }
align-items: center;
justify-content: center; .icon12 {
background-color: rgba(0, 0, 0, 0.5); @extend %icon-base;
border-radius: 50%; top: 17vh;
z-index: 10; right: 4vh;
transform: rotate(90deg);
animation: 1s ease-in-out infinite slidein12;
}
.icon13 {
@extend %icon-base;
top: 12vh;
right: 4vh;
transform: rotate(45deg);
animation: 1s ease-in-out infinite slidein13;
}
.icon14-1 {
@extend %icon-base;
top: 25vh;
left: 10.7vh;
transform: rotate(180deg);
animation: 1s ease-in-out infinite slidein14;
}
.icon14-2 {
@extend %icon-base;
top: 25vh;
right: 10.7vh;
transform: rotate(180deg);
animation: 1s ease-in-out infinite slidein14;
}
.icon15 {
@extend %icon-base;
top: 22vh;
right: 4vh;
transform: rotate(135deg);
animation: 1s ease-in-out infinite slidein15;
}
}
.tip {
margin: 28rpx auto 0;
padding: 0 32rpx;
font-size: 32rpx;
color: #ffffff;
display: inline-block;
line-height: 56rpx;
background: rgba(0, 0, 0, 0.33);
border-radius: 124rpx 124rpx 124rpx 124rpx;
}
.order {
margin: 32rpx auto 0;
padding: 18rpx 48rpx;
font-size: 40rpx;
font-weight: bold;
text-align: center;
color: #211d2e;
border-radius: 94rpx;
display: inline-flex;
align-items: baseline;
background-color: #fff;
.num {
margin-left: 20rpx;
}
.m-num {
font-size: 28rpx;
}
}
} }
.controls { .controls {
position: fixed; position: fixed;
bottom: 80rpx; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
gap: 80rpx; gap: 80rpx;
padding: 30rpx 0; padding: 32rpx 114rpx calc(env(safe-area-inset-bottom) + 20rpx);
background: rgba(255, 255, 255, 0.88);
border-radius: 32rpx 32rpx 0 0;
.switch-btn {
width: 120rpx;
.icon {
width: 68rpx;
height: 68rpx;
}
.name {
font-size: 32rpx;
color: #adacb2;
}
} }
.control-btn { .control-btn {
flex-shrink: 0;
width: 160rpx;
height: 160rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 80rpx; background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
height: 80rpx; box-shadow: 0rpx 8rpx 47rpx 0rpx rgba(69, 0, 157, 0.15);
border-radius: 50%; border-radius: 96rpx 96rpx 96rpx 96rpx;
background-color: rgba(255, 255, 255, 0.2); .icon {
width: 48rpx;
height: 48rpx;
}
}
.block {
width: 120rpx;
}
}
} }
.capture-btn { .popup-container {
width: 140rpx; width: 100vw;
height: 140rpx; box-sizing: border-box;
background-color: rgba(255, 255, 255, 0.3); padding: 0 48rpx 132rpx;
border: 6rpx solid #fff; 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;
color: #211d2e;
font-weight: bold;
display: flex;
justify-content: space-between;
align-items: center;
}
.select {
margin-top: 32rpx;
display: flex;
align-items: center;
gap: 34rpx;
.item {
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;
}
}
}
}
.capture-inner { @keyframes slidein8 {
width: 110rpx; 0% {
height: 110rpx; transform: translate(0, 0) rotate(-45deg);
border-radius: 50%; }
background-color: #fff; 50% {
transform: translate(-20rpx, -20rpx) rotate(-45deg);
}
100% {
transform: translate(0, 0) rotate(-45deg);
} }
} }
.preview-section { @keyframes slidein9 {
position: fixed; 0% {
top: 0; transform: translate(0, 0) rotate(0);
left: 0; }
right: 0; 50% {
bottom: 0; transform: translate(0, -20rpx) rotate(0);
background-color: #000; }
display: flex; 100% {
flex-direction: column; transform: translate(0, 0) rotate(0);
z-index: 10; }
}
.preview-image { @keyframes slidein10 {
flex: 1; 0% {
width: 100%; transform: translate(0, 0) rotate(-135deg);
}
50% {
transform: translate(20rpx, -20rpx) rotate(-135deg);
}
100% {
transform: translate(0, 0) rotate(-135deg);
}
} }
.preview-actions { @keyframes slidein11 {
display: flex; 0% {
justify-content: center; transform: translate(0, 0) rotate(-90deg);
padding: 40rpx; }
background-color: #000; 50% {
transform: translate(-20rpx, 0rpx) rotate(-90deg);
}
100% {
transform: translate(0, 0) rotate(-90deg);
}
}
@keyframes slidein12 {
0% {
transform: translate(0, 0) rotate(90deg);
}
50% {
transform: translate(20rpx, 0rpx) rotate(90deg);
}
100% {
transform: translate(0, 0) rotate(90deg);
}
}
@keyframes slidein13 {
0% {
transform: translate(0, 0) rotate(45deg);
}
50% {
transform: translate(20rpx, -20rpx) rotate(45deg);
}
100% {
transform: translate(0, 0) rotate(45deg);
}
}
@keyframes slidein14 {
0% {
transform: translate(0, 0) rotate(180deg);
}
50% {
transform: translate(0, 20rpx) rotate(180deg);
}
100% {
transform: translate(0, 0) rotate(180deg);
}
}
@keyframes slidein15 {
0% {
transform: translate(0, 0) rotate(135deg);
}
50% {
transform: translate(20rpx, 20rpx) rotate(135deg);
}
100% {
transform: translate(0, 0) rotate(135deg);
} }
} }

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

@ -11,6 +11,73 @@ Component({
devicePosition: 'back', devicePosition: 'back',
flash: 'off' as 'off' | 'auto' | 'on', flash: 'off' as 'off' | 'auto' | 'on',
previewImage: '', previewImage: '',
selectShow: false,
exampleShow: false,
type: 1,
frame: {
1: {
src: 'note-camera1',
exampleSrc: 'note-demo1',
},
2: {
src: 'note-camera1',
exampleSrc: 'note-demo2',
},
3: {
src: 'note-camera3',
exampleSrc: 'note-demo3',
},
4: {
src: 'note-camera4',
exampleSrc: 'note-demo4',
},
5: {
src: 'note-camera5',
exampleSrc: 'note-demo5',
},
6: {
src: 'note-camera6',
exampleSrc: 'note-demo6',
},
7: {
src: 'note-camera7',
exampleSrc: 'note-demo7',
},
8: {
src: 'note-camera1',
exampleSrc: 'note-demo8',
},
9: {
src: 'note-camera1',
exampleSrc: 'note-demo9',
},
10: {
src: 'note-camera1',
exampleSrc: 'note-demo10',
},
11: {
src: 'note-camera1',
exampleSrc: 'note-demo11',
},
12: {
src: 'note-camera1',
exampleSrc: 'note-demo12',
},
13: {
src: 'note-camera1',
exampleSrc: 'note-demo13',
},
14: {
src: 'note-camera1',
exampleSrc: 'note-demo14',
},
15: {
src: 'note-camera1',
exampleSrc: 'note-demo15',
},
},
}, },
lifetimes: { lifetimes: {
@ -22,6 +89,54 @@ Component({
}, },
methods: { methods: {
handleSelect(type: number) {
this.setData({
selectShow: true,
type,
})
},
handleCancel() {
this.setData({
selectShow: false,
})
},
handleCamera() {
this.setData({
selectShow: false,
visible: true,
})
},
handleHideCamera() {
this.setData({
visible: false,
})
},
handlePicture() {
this.handleCancel()
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album'],
success: (res) => {
if (res.tempFiles && res.tempFiles.length > 0) {
const tempFile = res.tempFiles[0]
const maxSize = 10 * 1024 * 1024 // 10M
if (tempFile.size > maxSize) {
wx.showToast({
title: '图片大小不能超过10M',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'file_too_large', size: tempFile.size, maxSize })
return
}
this.uploadImage(tempFile.tempFilePath)
}
},
fail: (err) => {
console.error('选择图片失败:', err)
},
})
},
openCamera() { openCamera() {
this.setData({ this.setData({
visible: true, visible: true,
@ -29,11 +144,6 @@ Component({
}) })
}, },
closeCamera() {
this.setData({ visible: false })
this.triggerEvent('cancel')
},
switchCamera() { switchCamera() {
const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back' const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back'
this.setData({ this.setData({
@ -60,7 +170,19 @@ Component({
this.setData({ this.setData({
previewImage: res.tempImagePath, previewImage: res.tempImagePath,
}) })
this.triggerEvent('capture', { tempFilePath: res.tempImagePath }) wx.cropImage({
src: res.tempImagePath,
cropScale: '9:16',
success: (cropRes) => {
// 裁剪成功后上传图片
this.uploadImage(cropRes.tempFilePath || res.tempImagePath)
},
fail: () => {
// 裁剪失败直接上传原图
this.uploadImage(res.tempImagePath)
},
})
// this.triggerEvent('capture', { tempFilePath: res.tempImagePath })
}, },
fail: (err) => { fail: (err) => {
console.error('拍照失败:', err) console.error('拍照失败:', err)
@ -81,17 +203,90 @@ Component({
}) })
this.triggerEvent('error', { reason: 'permission_denied' }) this.triggerEvent('error', { reason: 'permission_denied' })
}, },
checkImageSize(tempFilePath: string): Promise<boolean> {
retake() { return new Promise((resolve) => {
this.setData({ const fs = wx.getFileSystemManager()
previewImage: '', fs.stat({
path: tempFilePath,
success: (res) => {
const maxSize = 10 * 1024 * 1024 // 10M
const stats = Array.isArray(res.stats) ? res.stats[0] : res.stats
const fileSize = (stats as WechatMiniprogram.Stats).size
if (fileSize > maxSize) {
wx.showToast({
title: '图片大小不能超过10M',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'file_too_large', size: fileSize, maxSize })
resolve(false)
}
else {
resolve(true)
}
},
fail: () => {
resolve(true)
},
})
}) })
this.triggerEvent('retake')
}, },
usePhoto() { uploadImage(tempFilePath: string) {
this.triggerEvent('use', { tempFilePath: this.data.previewImage }) this.checkImageSize(tempFilePath).then((isValid) => {
this.setData({ visible: false }) 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,
})
}
else {
wx.showToast({
title: '上传失败',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'upload_failed', data })
}
}
catch (e) {
wx.showToast({
title: '解析响应失败',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'parse_error', error: e })
}
},
fail: (err) => {
wx.hideLoading()
wx.showToast({
title: '上传失败',
icon: 'none',
})
this.triggerEvent('uploaderror', { reason: 'upload_failed', error: err })
},
})
})
},
handleExample() {
this.setData({
exampleShow: !this.data.exampleShow,
})
}, },
}, },
}) })

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

@ -1,41 +1,102 @@
<view class="camera-container" wx:if="{{visible}}"> <view class="camera-container" wx:if="{{visible}}">
<camera <camera class="camera" device-position="{{devicePosition}}" flash="{{flash}}" binderror="onCameraError">
class="camera" <view
device-position="{{devicePosition}}" class="camera-container"
flash="{{flash}}" style="background: url('{{imageUrl}}camera-bg.png?t={{Timestamp}}') no-repeat center/100%"
binderror="onCameraError"
> >
<view class="camera-frame"> <view class="example {{exampleShow && 'active'}}" bind:tap="handleExample">
<view class="frame-corner left-top"></view> <image class="icon" src="{{imageUrl}}icon159.png?t={{Timestamp}}"></image>
<view class="frame-corner right-top"></view> 查看示范
<view class="frame-corner left-bottom"></view> <view class="example-popup" hidden="{{!exampleShow}}">
<view class="frame-corner right-bottom"></view> <view class="content">平视,目光看向镜头方向,自然睁眼,不眯眼、不瞪眼。</view>
<view class="photo-wrap">
<image class="photo" src="{{imageUrl}}{{frame[type].exampleSrc}}.png?t={{Timestamp}}"></image>
<image class="label" src="{{imageUrl}}icon162.png?t={{Timestamp}}"></image>
</view>
</view>
</view>
<view
class="camera-frame"
style="background: url('{{imageUrl}}{{frame[type].src}}.png?t={{Timestamp}}') no-repeat top center/100%"
>
<image wx:if="{{type==8}}" class="icon8" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<block wx:elif="{{type==9}}">
<image class="icon9-1" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<image class="icon9-2" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
</block>
<image wx:elif="{{type==10}}" class="icon10" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<image wx:elif="{{type==11}}" class="icon11" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<image wx:elif="{{type==12}}" class="icon12" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<image wx:elif="{{type==13}}" class="icon13" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<block wx:elif="{{type==14}}">
<image class="icon14-1" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
<image class="icon14-2" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
</block>
<image wx:elif="{{type==15}}" class="icon15" src="{{imageUrl}}icon163.png?t={{Timestamp}}"></image>
</view>
<view>
<view class="tip">请将眼睛置于虚线框中心位置</view>
</view>
<view>
<view class="order">
正面睁眼
<view class="num">1</view>
<view class="m-num">/3</view>
</view> </view>
</camera>
<view class="close-btn" bindtap="closeCamera">
<van-icon name="cross" size="20px" color="#fff" />
</view> </view>
<view class="controls"> <view class="controls">
<view class="control-btn switch-btn" bindtap="switchCamera"> <view class="switch-btn" bindtap="switchCamera">
<van-icon name="reload" size="24px" color="#fff" /> <image class="icon" src="{{imageUrl}}icon160.png?t={{Timestamp}}"></image>
<view class="name">翻转</view>
</view>
<view class="control-btn" bindtap="takePhoto">
<image class="icon" src="{{imageUrl}}icon161.png?t={{Timestamp}}"></image>
</view>
<view class="block"></view>
</view>
</view> </view>
</camera>
<view class="control-btn capture-btn" bindtap="takePhoto"> <!-- <view class="controls"> -->
<view class="capture-inner"></view> <!-- <view class="control-btn switch-btn" bindtap="switchCamera"> -->
<!-- <van-icon name="reload" size="24px" color="#fff" /> -->
<!-- </view> -->
<!---->
<!-- <view class="control-btn capture-btn" bindtap="takePhoto"> -->
<!-- <view class="capture-inner"></view> -->
<!-- </view> -->
<!---->
<!-- <view class="control-btn flash-btn" bindtap="toggleFlash"> -->
<!-- <van-icon name="{{flash === 'off' ? 'closed-eye' : 'eye'}}" size="24px" color="#fff" /> -->
<!-- </view> -->
<!-- </view> -->
</view> </view>
<view class="control-btn flash-btn" bindtap="toggleFlash"> <van-popup
<van-icon name="{{flash === 'off' ? 'closed-eye' : 'eye'}}" size="24px" color="#fff" /> wx:if="{{selectShow}}"
bind:click-overlay="handleCancel"
round
z-index="{{100000}}"
show="{{ true }}"
custom-style="background:transparent;"
position="bottom"
safe-area-inset-bottom="{{false}}"
root-portal
>
<view class="popup-container">
<view class="title">
选择上传方式
<van-icon name="cross" />
</view> </view>
<view class="select">
<view class="item" bind:tap="handleCamera">
<image class="icon" src="{{imageUrl}}icon157.png?t={{Timestamp}}"></image>
<view class="name">拍摄</view>
</view> </view>
<view class="item" bind:tap="handlePicture">
<view class="preview-section" wx:if="{{previewImage}}"> <image class="icon" src="{{imageUrl}}icon158.png?t={{Timestamp}}"></image>
<image class="preview-image" src="{{previewImage}}" mode="aspectFit" /> <view class="name">从相册选择</view>
<view class="preview-actions">
<van-button type="default" round bindtap="retake">重拍</van-button>
<van-button type="primary" round custom-style="margin-left: 30rpx;" bindtap="usePhoto">使用照片</van-button>
</view> </view>
</view> </view>
</view> </view>
</van-popup>

57
src/patient/components/image-merge/index.ts

@ -43,6 +43,7 @@ Component({
wx.previewImage({ wx.previewImage({
urls: [mergedImage], urls: [mergedImage],
current: mergedImage, current: mergedImage,
showmenu: true,
}) })
}) })
.catch((error) => { .catch((error) => {
@ -56,15 +57,16 @@ Component({
validateImages(imageList: ImageItem[]): Promise<void> { validateImages(imageList: ImageItem[]): Promise<void> {
return Promise.all( return Promise.all(
imageList.map((item, index) => imageList.map(
(item, index) =>
new Promise<void>((resolve, reject) => { new Promise<void>((resolve, reject) => {
wx.getImageInfo({ wx.getImageInfo({
src: item.src, src: item.src,
success: () => resolve(), success: () => resolve(),
fail: () => reject(new Error(`${index + 1}张图片加载失败`)), fail: () => reject(new Error(`${index + 1}张图片加载失败`)),
}) })
}) }),
) ),
) )
}, },
@ -93,15 +95,12 @@ Component({
const canvas = res[0].node const canvas = res[0].node
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
ctx.fillStyle = '#ffffff'
Promise.all(imageList.map(item => this.getImageInfo(item.src))) Promise.all(imageList.map(item => this.getImageInfo(item.src)))
.then((imageInfos) => { .then((imageInfos) => {
const targetWidth = 750 const targetWidth = 750
const pixelRatio = wx.getWindowInfo().pixelRatio const pixelRatio = wx.getWindowInfo().pixelRatio
const canvasWidth = Math.floor((targetWidth * pixelRatio) / 2) const canvasWidth = Math.floor((targetWidth * pixelRatio) / 2)
const timeAreaHeight = 60
const watermarkHeight = 80
let totalHeight = 0 let totalHeight = 0
const scaledHeights: number[] = [] const scaledHeights: number[] = []
@ -110,16 +109,12 @@ Component({
const scale = canvasWidth / info.width const scale = canvasWidth / info.width
const scaledHeight = Math.floor(info.height * scale) const scaledHeight = Math.floor(info.height * scale)
scaledHeights.push(scaledHeight) scaledHeights.push(scaledHeight)
totalHeight += scaledHeight + timeAreaHeight totalHeight += scaledHeight
}) })
totalHeight += watermarkHeight
canvas.width = canvasWidth canvas.width = canvasWidth
canvas.height = totalHeight canvas.height = totalHeight
ctx.fillRect(0, 0, canvasWidth, totalHeight)
let currentY = 0 let currentY = 0
let loadedCount = 0 let loadedCount = 0
@ -129,22 +124,40 @@ Component({
img.onload = () => { img.onload = () => {
ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index]) ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index])
const timeY = currentY + scaledHeights[index] + 40 // 在每张图片左上角绘制时间,白色字体
ctx.fillStyle = '#666666'
ctx.font = '24px sans-serif'
ctx.textAlign = 'center'
const timeText = imageList[index].time || this.formatTime(new Date()) const timeText = imageList[index].time || this.formatTime(new Date())
ctx.fillText(timeText, canvasWidth / 2, timeY) const padding = 20
const textY = currentY + 40
currentY += scaledHeights[index] + timeAreaHeight // 设置白色字体和阴影以增强可读性
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++ loadedCount++
if (loadedCount === imageInfos.length) { if (loadedCount === imageInfos.length) {
ctx.fillStyle = '#999999'
ctx.font = '28px sans-serif'
ctx.textAlign = 'right'
ctx.fillText('TED关爱小助手', canvasWidth - 30, totalHeight - 30)
wx.canvasToTempFilePath({ wx.canvasToTempFilePath({
canvas, canvas,
success: (result) => { success: (result) => {

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

@ -43,7 +43,9 @@
<view class="demo-content"> <view class="demo-content">
<view wx:if="{{showMerge}}" class="merge-demo"> <view wx:if="{{showMerge}}" class="merge-demo">
<view class="demo-tip">点击下方按钮使用示例图片演示拼接功能</view> <view class="demo-tip">点击下方按钮使用示例图片演示拼接功能</view>
<van-button type="primary" block round loading="{{isMerging}}" disabled="{{isMerging}}" bindtap="demoMerge">演示图片拼接</van-button> <van-button type="primary" block round loading="{{isMerging}}" disabled="{{isMerging}}" bindtap="demoMerge">
演示图片拼接
</van-button>
<image-merge id="merge" bindsave="onMergeSave" binderror="onMergeError" /> <image-merge id="merge" bindsave="onMergeSave" binderror="onMergeError" />
</view> </view>
</view> </view>

1
src/patient/pages/index/index.scss

@ -725,6 +725,7 @@ page {
} }
.reg { .reg {
position: fixed; position: fixed;
z-index: 10;
left: 64rpx; left: 64rpx;
bottom: calc(env(safe-area-inset-bottom) + 120rpx); bottom: calc(env(safe-area-inset-bottom) + 120rpx);
width: 622rpx; width: 622rpx;

7
src/patient/pages/note/index.json

@ -0,0 +1,7 @@
{
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index"
},
"navigationBarTitleText": "突眼日记"
}

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

@ -0,0 +1,269 @@
page {
background-color: #f6f8f9;
}
.container {
padding: 20rpx 30rpx;
min-height: 100vh;
}
// 基准照片设置卡片
.setting-card-empty {
background: #fff;
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0rpx 4rpx 32rpx 0rpx rgba(0, 0, 0, 0.04);
.setting-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.setting-title {
display: flex;
align-items: center;
gap: 12rpx;
.icon {
width: 36rpx;
height: 36rpx;
}
text {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
}
.setting-status {
background: linear-gradient(90deg, #ffd650 0%, #f8a61a 100%);
color: #fff;
font-size: 24rpx;
padding: 6rpx 16rpx;
border-radius: 6rpx;
}
.setting-desc {
font-size: 32rpx;
color: #69686e;
line-height: 48rpx;
background: #f6f8f9;
padding: 32rpx;
border-radius: 24rpx;
}
}
.setting-card {
padding: 32rpx;
background-color: #fff;
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0rpx 4rpx 32rpx 0rpx rgba(0, 0, 0, 0.04);
.setting-header {
.setting-title {
display: flex;
align-items: center;
gap: 12rpx;
.icon {
width: 36rpx;
height: 36rpx;
}
text {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
}
}
.setting-body {
margin-top: 32rpx;
display: flex;
justify-content: space-between;
gap: 24rpx;
.photo {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
}
.wrap {
flex: 1;
.name {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.date {
margin-top: 24rpx;
font-size: 28rpx;
color: #69686e;
}
}
.more {
font-size: 32rpx;
color: #b073ff;
}
}
}
// 功能按钮区
.action-buttons {
display: flex;
gap: 30rpx;
margin-bottom: 56rpx;
}
.action-btn {
flex: 1;
height: 320rpx;
border-radius: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20rpx;
position: relative;
overflow: hidden;
&.primary {
background: linear-gradient(180deg, #e98ff8 0%, #b073ff 100%);
.btn-text {
color: #fff;
}
.corner-fold {
position: absolute;
top: 0;
right: 0;
width: 60rpx;
height: 60rpx;
background: linear-gradient(45deg, #e5d2ff 0%, #f6f8f9 50%, #f6f8f9 100%);
border-radius: 0 0 0 18rpx;
}
}
&.secondary {
background: #fff;
box-shadow: 0 2rpx 12rpx rgba(14, 0, 32, 0.19);
.btn-text {
color: #a855f7;
}
}
}
.btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 108rpx;
height: 108rpx;
}
.btn-text {
font-size: 32rpx;
font-weight: 500;
}
.history-section {
.section-title {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 30rpx;
.title-bar {
width: 8rpx;
height: 32rpx;
background: #a855f7;
border-radius: 4rpx;
}
text {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
}
.history-empty {
background: #fff;
border-radius: 32rpx;
padding: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
gap: 24rpx;
.icon {
width: 64rpx;
height: 64rpx;
}
.empty-text {
font-size: 32rpx;
color: #adacb2;
}
}
.history-list {
.list-item {
position: relative;
margin-top: 24rpx;
border-radius: 32rpx;
background-color: #fff;
padding: 32rpx;
display: flex;
justify-content: space-between;
gap: 24rpx;
.benchmark {
position: absolute;
top: 0;
right: 0;
padding-top: 8rpx;
text-align: center;
width: 124rpx;
height: 64rpx;
font-size: 28rpx;
color: #ffa300;
line-height: 32rpx;
}
.photo {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
}
.wrap {
flex: 1;
.date {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.tag {
margin-top: 16rpx;
padding: 4rpx 12rpx;
display: inline-block;
font-size: 24rpx;
color: #b073ff;
background: rgba(176, 115, 255, 0.16);
border-radius: 6rpx 6rpx 6rpx 6rpx;
}
.rotate {
margin-top: 28rpx;
font-size: 28rpx;
color: #adacb2;
line-height: 1;
}
}
.more {
align-self: center;
width: 44rpx;
height: 44rpx;
}
}
}
}

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

@ -0,0 +1,35 @@
const _app = getApp<IAppOption>()
Page({
data: {
background: '#fff',
},
onLoad() {
// 页面加载时的初始化
},
// 返回上一页
handleBack() {
wx.navigateBack()
},
// 新增记录
addRecord() {
wx.navigateTo({
url: '/patient/pages/noteAdd/index',
})
},
handleHistory() {
wx.navigateTo({
url: '/patient/pages/noteHistory/index',
})
},
comparePhotos() {
wx.navigateTo({
url: '/patient/pages/noteDiff/index',
})
},
})
export {}

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

@ -0,0 +1,70 @@
<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="setting-card-empty" bindtap="addRecord">
<view class="setting-header">
<view class="setting-title">
<image class="icon" src="{{imageUrl}}icon144.png?t={{Timestamp}}"></image>
<text>基准照片设置</text>
</view>
<view class="setting-status">待设置</view>
</view>
<view class="setting-desc">请先上传一组完整的照片作为基准,后续记录将以此为参照进行对比。</view>
</view>
<view class="setting-card">
<view class="setting-header">
<view class="setting-title">
<image class="icon" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
<text>基准照片设置</text>
</view>
</view>
<view class="setting-body">
<image class="photo" src="{{imageUrl}}icon143.png?t={{Timestamp}}"></image>
<view class="wrap">
<view class="name">已设置基准照</view>
<view class="date">记录日期:2026-04-01</view>
</view>
<view class="more">查看</view>
</view>
</view>
<!-- 功能按钮区 -->
<view class="action-buttons">
<view class="action-btn primary" bindtap="addRecord">
<view class="corner-fold"></view>
<image class="btn-icon" src="{{imageUrl}}icon147.png?t={{Timestamp}}"></image>
<text class="btn-text">新增记录</text>
</view>
<view class="action-btn secondary" bindtap="comparePhotos">
<image class="btn-icon" src="{{imageUrl}}icon145.png?t={{Timestamp}}"></image>
<text class="btn-text">照片对比</text>
</view>
</view>
<!-- 历史记录 -->
<view class="history-section">
<view class="section-title">
<view class="title-bar"></view>
<text>历史记录</text>
</view>
<view 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 class="wrap">
<view class="date">2026-04-01</view>
<view class="tag">替妥尤单抗:2</view>
<view class="rotate">已上传1个角度</view>
</view>
<image class="more" src="{{imageUrl}}icon148.png?t={{Timestamp}}"></image>
</view>
</view>
</view>
</view>

10
src/patient/pages/noteAdd/index.json

@ -0,0 +1,10 @@
{
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index",
"popup": "/components/popup/index",
"noteImagePreview": "/components/noteImagePreview/index",
"camera": "/patient/components/camera/index"
},
"navigationBarTitleText": "记录新照片"
}

309
src/patient/pages/noteAdd/index.scss

@ -0,0 +1,309 @@
page {
background-color: #f6f8f9;
}
.page {
.benchmark {
margin-top: 32rpx;
padding: 0 40rpx 28rpx;
.checkbox {
font-size: 28rpx;
color: #211d2e;
.wx-checkbox-input {
background: rgba(173, 172, 178, 0.36);
border-color: transparent;
width: 32rpx;
height: 32rpx;
}
}
}
.form {
margin: 0 40rpx 24rpx;
padding: 32rpx;
background: #ffffff;
border-radius: 32rpx 32rpx 32rpx 32rpx;
.form-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 44rpx 44rpx 44rpx;
}
}
.form-title-gap {
margin-top: 48rpx;
}
.select {
margin-top: 24rpx;
display: flex;
justify-content: space-between;
padding: 26rpx 32rpx;
background: #f6f8f9;
border-radius: 24rpx 24rpx 24rpx 24rpx;
.content {
font-size: 32rpx;
color: #211d2e;
&:empty::after {
content: attr(data-place);
color: rgba(173, 172, 178, 0.6);
}
}
}
.dobule {
margin-top: 44rpx;
border-radius: 24rpx 24rpx 24rpx 24rpx;
display: flex;
gap: 26rpx;
.item {
.name {
font-size: 32rpx;
color: #211d2e;
}
.i-content {
margin-top: 24rpx;
padding: 26rpx 32rpx;
display: flex;
align-items: center;
background: #f6f8f9;
border-radius: 24rpx 24rpx 24rpx 24rpx;
font-size: 32rpx;
color: #211d2e;
.num {
font-size: 32rpx;
color: #211d2e;
}
}
}
}
}
.container {
margin-top: 72rpx;
padding: 32rpx 40rpx 0;
background-color: #fff;
border-radius: 32rpx 32rpx 0 0;
.c-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 44rpx 44rpx 44rpx;
}
}
.options {
margin-top: 30rpx;
display: flex;
align-items: center;
gap: 22rpx;
.btn {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
padding: 26rpx;
border-radius: 18rpx;
.icon {
width: 36rpx;
height: 36rpx;
}
font-size: 32rpx;
&.btn1 {
color: #b982ff;
background: rgba(185, 130, 255, 0.12);
}
&.btn2 {
color: #ffffff;
background: linear-gradient(180deg, #e98ff8 0%, #b073ff 100%);
}
}
}
.card {
margin-top: 32rpx;
padding: 32rpx;
background: #f6f8f9;
border-radius: 24rpx 24rpx 24rpx 24rpx;
.card-title {
display: flex;
align-items: center;
gap: 16rpx;
.order {
width: 40rpx;
height: 40rpx;
font-size: 28rpx;
color: #b982ff;
display: flex;
align-items: center;
justify-content: center;
background: rgba(185, 130, 255, 0.1);
border-radius: 8rpx 8rpx 8rpx 8rpx;
}
}
.upload-container {
margin-top: 32rpx;
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-content: space-between;
gap: 24rpx;
.upload-item {
$degs: (
1: -45deg,
2: 0deg,
3: 45deg,
4: -90deg,
5: 90deg,
6: -135deg,
7: -180deg,
8: 135deg,
);
@each $index, $deg in $degs {
&:nth-of-type(#{$index}) {
.arrow {
transform: rotate(#{$deg});
}
}
}
&:nth-of-type(3),
&:nth-of-type(5),
&:nth-of-type(8) {
.name {
flex-direction: row-reverse;
}
}
.upload-place {
border: 1px dashed #b982ff;
border-radius: 16rpx;
width: 186rpx;
height: 186rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24rpx;
box-sizing: border-box;
.icon {
width: 52rpx;
height: 52rpx;
}
.name {
font-size: 24rpx;
display: flex;
align-items: flex-end;
gap: 8rpx;
color: #211d2e;
line-height: 1;
.arrow {
width: 24rpx;
height: 24rpx;
}
}
}
.upload-preview {
position: relative;
width: 186rpx;
height: 186rpx;
.photo {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.status {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 16rpx;
background: rgba(9, 9, 9, 0.45);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
.icon {
margin-top: 32rpx;
width: 40rpx;
height: 40rpx;
}
.content {
margin-top: 8rpx;
font-size: 24rpx;
color: #ffffff;
}
.guide {
margin-top: 26rpx;
width: 100%;
height: 40rpx;
text-align: center;
line-height: 40rpx;
font-size: 28rpx;
color: #ffffff;
border-radius: 0 0 16rpx 16rpx;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
}
}
}
}
}
&.card3 .upload-container {
.upload-item:nth-child(1) {
grid-area: 1 / 1;
} /* 左上 */
.upload-item:nth-child(2) {
grid-area: 1 / 2;
} /* 上 */
.upload-item:nth-child(3) {
grid-area: 1 / 3;
} /* 右上 */
.upload-item:nth-child(4) {
grid-area: 2 / 1;
} /* 左 */
.upload-item:nth-child(5) {
grid-area: 2 / 3;
} /* 右 */
.upload-item:nth-child(6) {
grid-area: 3 / 1;
} /* 左下 */
.upload-item:nth-child(7) {
grid-area: 3 / 2;
} /* 下 */
.upload-item:nth-child(8) {
grid-area: 3 / 3;
} /* 右下 */
}
}
.c-footer {
position: sticky;
bottom: 0;
left: 0;
background-color: #fff;
padding: 32rpx 0 60rpx;
.btn {
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #ffffff;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
box-shadow: 0rpx 16rpx 32rpx 0rpx rgba(185, 130, 255, 0.25);
border-radius: 100rpx 100rpx 100rpx 100rpx;
}
}
}
}
.place {
color: rgba(173, 172, 178, 0.6);
}

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

@ -0,0 +1,132 @@
const _app = getApp<IAppOption>()
Page({
data: {
popupShow: false,
popupType: 'popup16', // 提示保存弹窗
popupParams: {
close: false,
position: 'bottom',
} as any,
imagePreview: false,
imageSrc: '',
cameraList: {
frontend: [
{
name: '睁眼',
type: 1,
},
{
name: '闭眼',
type: 2,
},
{
name: '仰头',
type: 3,
},
],
backend: [
{
name: '左侧-90°',
type: 4,
},
{
name: '右侧-90°',
type: 5,
},
{
name: '左侧-45°',
type: 6,
},
{
name: '右侧-45°',
type: 7,
},
],
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,
},
],
},
},
onLoad() {},
handleDemo() {
wx.navigateTo({
url: '/patient/pages/noteDemo/index',
})
},
handleImageRetake() {
this.setData({
imagePreview: false,
})
},
handleImageDel() {
this.setData({
imagePreview: false,
})
},
handleCamera(e) {
const { type } = e.currentTarget.dataset
const cameraComponent = this.selectComponent('#camera-component')
cameraComponent.handleSelect(type)
},
onUploadSuccess(e) {
this.setData({
imageSrc: e.detail.url,
})
},
onUploadError(e) {
console.log('DEBUGPRINT[221]: index.ts:34: e=', e)
},
handlePreview() {
this.selectComponent('#note-image-preview').handlePreview(this.data.imageSrc)
},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
},
handleBack() {
wx.navigateBack()
},
})
export {}

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

@ -0,0 +1,146 @@
<page-meta page-style="{{ popupShow || visible ? 'overflow: hidden;' : '' }}" />
<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="benchmark">
<checkbox class="checkbox" color="#fff">设置为基准记录,用于对比</checkbox>
</view>
<view class="form">
<view class="form-title">记录日期</view>
<picker mode="date">
<view class="select">
<view class="content" data-place="请选择记录日期"></view>
<van-icon class="more" name="arrow-down" />
</view>
</picker>
<view class="form-title form-title-gap">当前记录对应的替妥尤单抗使用次数</view>
<picker mode="selector">
<view class="select">
<view class="content" data-place="请选择使用次数"></view>
<van-icon class="more" name="arrow-down" />
</view>
</picker>
</view>
<view class="form">
<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="填写" />
MM
</view>
</view>
<view class="item">
<view class="name">眶间距</view>
<view class="i-content">
<input type="digit" class="num" placeholder-class="place" placeholder="填写" />
MM
</view>
</view>
<view class="item">
<view class="name">左眼</view>
<view class="i-content">
<input type="digit" class="num" placeholder-class="place" placeholder="填写" />
MM
</view>
</view>
</view>
</view>
<view class="container">
<view class="c-title">持续追踪、固定位置拍摄,以便对比评估变化</view>
<view class="options">
<view class="btn btn1" bind:tap="handleDemo">
<image class="icon" src="{{imageUrl}}icon149.png?t={{Timestamp}}"></image>
标准拍摄示范
</view>
<view class="btn btn2">
<image class="icon" src="{{imageUrl}}icon150.png?t={{Timestamp}}"></image>
一键顺序拍摄
</view>
</view>
<view class="card">
<view class="card-title">
<view class="order">1</view>
正面
</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">
<image class="icon" src="/images/icon164.png"></image>
<view class="content">不符合规范</view>
<view class="guide">重新上传</view>
</view>
</view>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">{{item.name}}</view>
</view>
</view>
</view>
</view>
<view class="card">
<view class="card-title">
<view class="order">2</view>
侧面
</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>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">{{item.name}}</view>
</view>
</view>
</view>
</view>
<view class="card card3">
<view class="card-title">
<view class="order">3</view>
眼球运动八个方向
</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>
<view class="upload-place" wx:else bind:tap="handleCamera" data-type="{{item.type}}">
<image class="icon" src="{{imageUrl}}icon151.png?t={{Timestamp}}"></image>
<view class="name">
<image class="arrow" src="/images/icon167.png"></image>
{{item.name}}
</view>
</view>
</view>
</view>
</view>
<view class="c-footer">
<view class="btn">诺和益托管视频</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}}"
bind:delete="handleImageDel"
bind:retake="handleImageRetake"
></noteImagePreview>
<popup
show="{{popupShow}}"
type="{{popupType}}"
params="{{popupParams}}"
bind:ok="handlePopupOk"
bind:cancel="handlePopupCancel"
></popup>

7
src/patient/pages/noteDemo/index.json

@ -0,0 +1,7 @@
{
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index"
},
"navigationBarTitleText": "标准拍照示范"
}

55
src/patient/pages/noteDemo/index.scss

@ -0,0 +1,55 @@
page {
background-color: #f6f8f9;
}
.page {
padding: 0 40rpx 40rpx;
.card {
margin-bottom: 32rpx;
padding: 116rpx 32rpx 32rpx;
border-radius: 32rpx;
background-color: #fff;
.tip {
background: #f7f1ff;
padding: 32rpx;
border-radius: 24rpx 24rpx 24rpx 24rpx;
border: 2rpx solid #b982ff;
}
.item {
margin-top: 36rpx;
.title {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
&::before {
content: '';
width: 6rpx;
height: 32rpx;
background: linear-gradient(344deg, #ffbcf9 0%, #b982ff 100%);
border-radius: 6rpx 6rpx 6rpx 6rpx;
}
}
.content {
margin-top: 22rpx;
font-size: 32rpx;
color: #69686e;
line-height: 48rpx;
}
.photo {
margin-top: 32rpx;
width: 100%;
display: block;
}
}
&.card-dobule {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 48rpx 30rpx;
.item {
margin: 0;
}
}
}
}

11
src/patient/pages/noteDemo/index.ts

@ -0,0 +1,11 @@
const _app = getApp<IAppOption>()
Page({
data: {},
onLoad() {},
handleBack() {
wx.navigateBack()
},
})
export {}

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

@ -0,0 +1,86 @@
<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;"
>
<view class="card" style="background: #fff url('{{imageUrl}}bg52.png?t={{Timestamp}}') no-repeat top center/100% 210rpx">
<view class="tip">拍摄时,正对镜头,面部居中,露出完整双眼及眼眶;光线均匀无阴影。</view>
<view class="item">
<view class="title">正面睁眼照</view>
<view class="content">平视,目光看向镜头方向,自然睁眼,不眯眼、不瞪眼。</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面闭眼照</view>
<view class="content">正对镜头,面部居中,自然放松,双眼轻轻闭合,不皱眉、不挤眼。</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo2.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面仰头照</view>
<view class="content">
拍摄时,正对镜头,面部居中,头部自然向上仰至约 45°,双眼同步平视,保持自然睁眼、不眯眼。
</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo3.png?t={{Timestamp}}"></image>
</view>
</view>
<view class="card" style="background: #fff url('{{imageUrl}}bg53.png?t={{Timestamp}}') no-repeat top center/100% 210rpx">
<view class="tip">拍摄时,头部保持水平,不仰头、不低头,双眼平视前方;光线均匀无阴影。</view>
<view class="item">
<view class="title">左侧-90°</view>
<view class="content">身体与头部完全转向右侧,呈标准 90° 侧面,仅可见左侧眼睛。</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo4.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">右侧-90°</view>
<view class="content">身体与头部完全转向左侧,呈标准 90° 侧面,仅可见右侧眼睛。</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo5.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">左侧-45°</view>
<view class="content">身体与头部转向右前方 45°。</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo6.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">右侧-45°</view>
<view class="content">身体与头部转向左前方 45°</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo7.png?t={{Timestamp}}"></image>
</view>
</view>
<view class="card card-dobule" style="background: #fff url('{{imageUrl}}bg54.png?t={{Timestamp}}') no-repeat top center/100% 210rpx">
<view class="item">
<view class="title">正面眼睛上看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo8.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛下看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo9.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛左看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo10.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛右看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo11.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛左上看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo12.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛右上看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo13.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛左下看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo14.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<view class="title">正面眼睛右下看</view>
<image class="photo" mode="widthFix" src="{{imageUrl}}note-demo15.png?t={{Timestamp}}"></image>
</view>
</view>
</view>

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

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

284
src/patient/pages/noteDiff/index.scss

@ -0,0 +1,284 @@
page {
background-color: #f6f8f9;
}
.page-none {
padding: 290rpx 76rpx 0;
.icon {
display: block;
margin: 0 auto;
width: 458rpx;
height: 218rpx;
}
.title {
margin-top: 26rpx;
font-size: 38rpx;
color: #211d2e;
font-weight: bold;
text-align: center;
}
.content {
margin-top: 24rpx;
font-size: 32rpx;
color: #69686e;
line-height: 48rpx;
text-align: center;
}
.btn {
margin: 88rpx 0 0;
font-size: 32rpx;
color: #ffffff;
height: 88rpx;
text-align: center;
line-height: 88rpx;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
box-shadow: 0rpx 16rpx 32rpx 0rpx rgba(185, 130, 255, 0.25);
border-radius: 100rpx 100rpx 100rpx 100rpx;
}
}
.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 32rpx 32rpx 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 44rpx 44rpx 44rpx;
}
}
.select {
margin-top: 24rpx;
display: flex;
justify-content: space-between;
padding: 26rpx 32rpx;
background: #f6f8f9;
border-radius: 24rpx 24rpx 24rpx 24rpx;
.content {
font-size: 32rpx;
color: #211d2e;
&:empty::after {
content: attr(data-place);
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 24rpx 24rpx 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 44rpx 44rpx 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;
}
.circle {
position: relative;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(185, 130, 255, 0.29);
&::after {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
content: '';
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background-color: #b982ff;
}
}
}
.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;
}
.btn {
width: 164rpx;
height: 56rpx;
background: linear-gradient(0deg, #e98ff8 0%, #b073ff 100%);
border-radius: 124rpx 124rpx 124rpx 124rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 32rpx;
color: #ffffff;
.icon {
width: 40rpx;
height: 40rpx;
}
}
}
.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;
.photo {
border-radius: 32rpx 32rpx 0 0;
height: 352rpx;
display: block;
}
.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 100rpx 100rpx 100rpx;
}
}
}
}

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

@ -0,0 +1,13 @@
const _app = getApp<IAppOption>()
Page({
data: {},
onLoad() {},
handleEdit() {
wx.navigateTo({
url: '/patient/pages/noteDiffEdit/index',
})
},
})
export {}

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

@ -0,0 +1,85 @@
<view class="page-none" wx:if="{{false}}">
<image class="icon" src="{{imageUrl}}none3.png?t={{Timestamp}}"></image>
<view class="title">未设置基准照片</view>
<view class="content">
请先上传一组照片并将其设为基准
<view></view>
以便进行对比分析
</view>
<view class="btn">去设置</view>
</view>
<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">
<view class="select">
<view class="content" data-place="请选择对比角度"></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 {{index==0&&'active'}}" wx:for="{{8}}" wx:key="index">2026-04-02</view>
</view>
</view>
</view>
<view class="container">
<view class="title">
正面睁眼时间线对比
<view class="date">生成日期:2026-04-02</view>
</view>
<view class="card">
<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>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</view>
</view>
<view class="photo-card">
<image class="photo" src="{{imageUrl}}note-demo1.png?t={{Timestamp}}"></image>
<view class="row">
<view class="col">
<view class="name">右眼</view>
<view class="content">
<view class="num">12</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="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">左眼</view>
<view class="content">
<view class="num">12</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>

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

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

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

@ -0,0 +1,157 @@
page {
background-color: #f6f8f9;
}
.page {
padding-bottom: 80rpx;
.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;
.line-bottom {
border: 1px dashed #b982ff;
}
.line-bottom {
flex: 1;
}
.circle {
position: relative;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: rgba(185, 130, 255, 0.29);
&::after {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: block;
content: '';
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background-color: #b982ff;
}
}
}
.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;
height: 352rpx;
display: block;
}
.mask {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 85rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
background: rgba(0, 0, 0, 0.3);
font-size: 32rpx;
color: #ffffff;
.icon {
width: 40rpx;
height: 40rpx;
}
}
}
}
}
.footer {
margin-top: 46rpx;
display: flex;
align-items: center;
gap: 22rpx;
.btn1 {
flex: 1;
height: 88rpx;
font-size: 36rpx;
color: #b982ff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #b982ff;
background-color: #fff;
border-radius: 100rpx 100rpx 100rpx 100rpx;
}
.btn2 {
flex: 1;
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 100rpx 100rpx 100rpx;
}
}
}
}

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

@ -0,0 +1,19 @@
const _app = getApp<IAppOption>()
Page({
data: {},
onLoad() {},
handleMergePreview() {
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' },
])
}
},
})
export {}

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

@ -0,0 +1,40 @@
<view class="page">
<view class="container">
<view class="title">
正面睁眼时间线对比
<view class="date">生成日期:2026-04-02</view>
</view>
<view class="card">
<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>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</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>
点击裁剪
</view>
<view class="mask" wx:else>
<image class="icon" src="/images/icon166.png"></image>
点击还原
</view>
</view>
</view>
</view>
<view class="footer">
<view class="btn1" bind:tap="handleMergePreview">对比图预览</view>
<view class="btn2">保存到相册</view>
</view>
</view>
</view>
<imageMerge id="merge" bindsave="onMergeSave" binderror="onMergeError" />

8
src/patient/pages/noteHistory/index.json

@ -0,0 +1,8 @@
{
"usingComponents": {
"navbar": "/components/navbar/index",
"van-icon": "@vant/weapp/icon/index",
"popup": "/components/popup/index"
},
"navigationBarTitleText": "记录"
}

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

@ -0,0 +1,183 @@
.page {
.header {
padding: 52rpx 32rpx 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
.wrap {
.date {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.tags {
margin-top: 16rpx;
.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;
}
}
}
}
.opt {
display: flex;
align-items: center;
gap: 24rpx;
.item {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
border-radius: 50%;
.icon {
width: 40rpx;
height: 40rpx;
}
}
}
}
.container {
padding: 40rpx;
border-radius: 32rpx 32rpx 0 0;
background-color: #fff;
.tip-card {
padding: 24rpx 32rpx;
border-radius: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
background: #fff7e9;
gap: 16rpx;
.tip-icon {
margin-top: 6rpx;
align-self: flex-start;
flex-shrink: 0;
width: 32rpx;
height: 32rpx;
}
.wrap {
flex: 1;
line-height: 44rpx;
.title {
font-size: 28rpx;
color: #ffa300;
font-weight: bold;
}
.content {
font-size: 24rpx;
color: #ffa300;
}
}
.btn {
flex-shrink: 0;
font-size: 28rpx;
color: #ffffff;
width: 148rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: #ffa300;
border-radius: 92rpx 92rpx 92rpx 92rpx;
}
}
.banner {
margin-top: 24rpx;
padding: 32rpx;
background: #f6f8f9;
border-radius: 32rpx 32rpx 32rpx 32rpx;
border: 2rpx solid #ffffff;
.title {
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
}
.row {
margin-top: 24rpx;
display: flex;
gap: 24rpx;
text-align: center;
.col {
padding: 24rpx;
flex: 1;
background-color: #fff;
border-radius: 16rpx;
.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;
}
}
}
}
}
.card {
margin-top: 48rpx;
.card-title {
display: flex;
align-items: center;
gap: 16rpx;
font-size: 32rpx;
color: #211d2e;
font-weight: bold;
&::before {
content: '';
width: 8rpx;
height: 32rpx;
background: #b982ff;
border-radius: 44rpx 44rpx 44rpx 44rpx;
}
}
.card-container {
margin-top: 24rpx;
display: grid;
grid-template-columns: repeat(2, 320rpx);
justify-content: space-between;
gap: 30rpx;
.card-item {
.photo {
display: block;
width: 100%;
height: 320rpx;
border-radius: 24rpx;
}
.name{
margin-top: 16rpx;
font-size: 28rpx;
color: #ADACB2;
text-align: center;
}
}
}
}
}
}

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

@ -0,0 +1,94 @@
const _app = getApp<IAppOption>()
Page({
data: {
popupShow: true,
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: '',
},
],
},
},
onLoad() {},
handlePopupOk() {
this.setData({
popupShow: false,
})
},
handlePopupCancel() {
this.setData({
popupShow: false,
})
},
})
export {}

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

@ -0,0 +1,98 @@
<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}}bg10.png?t={{Timestamp}}') no-repeat top center/100% 610rpx;padding-top:{{pageTop}}px;"
>
<view class="header">
<view class="wrap">
<view class="date">2026-04-02</view>
<view class="tags">
<view class="tag tag1">基准照片</view>
<view class="tag tag2">替妥尤单抗:2</view>
</view>
</view>
<view class="opt">
<view class="item">
<image class="icon" src="{{imageUrl}}icon152.png?t={{Timestamp}}"></image>
</view>
<view class="item">
<image class="icon" src="{{imageUrl}}icon153.png?t={{Timestamp}}"></image>
</view>
</view>
</view>
<view class="container">
<view 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>
<view class="btn">去补充</view>
</view>
<view 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="sub">MM</view>
</view>
</view>
<view class="col">
<view class="name">眶间距</view>
<view class="content">
<view class="num">12</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="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>
</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>
</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>
</view>
</view>
</view>
</view>
<popup
show="{{popupShow}}"
type="{{popupType}}"
params="{{popupParams}}"
bind:ok="handlePopupOk"
bind:cancel="handlePopupCancel"
></popup>

220
src/utils/captcha.ts

@ -0,0 +1,220 @@
/**
* 2.0
*
*/
// 验证码插件实例
let AliyunCaptchaPluginInterface: any = null
// 计时器
let timer: number | null = null
// 页面实例引用
let pageInstance: any = null
// 发送验证码的接口配置
interface SendCodeConfig {
url: string
mobileField?: string
extraData?: Record<string, any>
}
// 验证码配置选项
interface CaptchaOptions {
sceneId: string
sendCodeConfig: SendCodeConfig
onSendSuccess?: () => void
onSendFail?: (err: any) => void
countdown?: number
}
// 当前配置
let currentOptions: CaptchaOptions | null = null
/**
*
*/
function initPlugin() {
if (!AliyunCaptchaPluginInterface) {
AliyunCaptchaPluginInterface = requirePlugin('AliyunCaptcha')
}
}
/**
*
* @param captchaVerifyParam
*/
async function successCallback(captchaVerifyParam: string) {
if (!pageInstance || !currentOptions) return
const { sendCodeConfig, onSendSuccess, onSendFail, countdown = 60 } = currentOptions
const mobileField = sendCodeConfig.mobileField || 'mobile'
const mobile = pageInstance.data[mobileField]
try {
const res = await wx.ajax({
method: 'POST',
url: sendCodeConfig.url,
data: {
[mobileField]: mobile,
captchaVerifyParam,
...sendCodeConfig.extraData,
},
})
wx.showToast({
icon: 'none',
title: '验证码已发送~',
})
// 开始倒计时
startCountdown(countdown)
// 执行成功回调
onSendSuccess?.()
return res
}
catch (err: any) {
wx.showToast({
title: err.data?.msg || '发送失败,请重试',
icon: 'none',
})
onSendFail?.(err)
throw err
}
}
/**
*
* @param error
*/
function failCallback(error: any) {
console.error('阿里云验证码验证失败:', error)
wx.showToast({
title: '验证失败,请重试',
icon: 'none',
})
}
/**
*
* @param seconds
*/
function startCountdown(seconds: number) {
if (timer) {
clearInterval(timer)
timer = null
}
let time = seconds
updateCountdownText(`${time}s后重新发送`)
timer = setInterval(() => {
time--
if (time <= 0) {
clearInterval(timer as number)
timer = null
updateCountdownText('发送验证码')
}
else {
updateCountdownText(`${time}s后重新发送`)
}
}, 1000) as unknown as number
}
/**
*
* @param text
*/
function updateCountdownText(text: string) {
if (pageInstance) {
pageInstance.setData({ codeText: text })
}
}
/**
*
* @param page
* @param options
* @returns
*/
export function initCaptcha(page: any, options: CaptchaOptions) {
initPlugin()
pageInstance = page
currentOptions = options
const pluginProps = {
SceneId: options.sceneId,
mode: 'popup',
success: successCallback.bind(page),
fail: failCallback.bind(page),
slideStyle: {
width: 540,
height: 60,
},
language: 'cn',
region: 'cn',
}
return {
loadCaptcha: true,
pluginProps,
}
}
/**
*
* @returns
*/
export function showCaptcha(): boolean {
if (!AliyunCaptchaPluginInterface) {
initPlugin()
}
if (isCountingDown()) {
return false
}
AliyunCaptchaPluginInterface.show()
return true
}
/**
*
* @returns
*/
export function isCountingDown(): boolean {
return timer !== null
}
/**
*
*/
export function clearCountdown() {
if (timer) {
clearInterval(timer)
timer = null
}
updateCountdownText('发送验证码')
}
/**
*
* @returns
*/
export function getCaptchaPlugin() {
if (!AliyunCaptchaPluginInterface) {
initPlugin()
}
return AliyunCaptchaPluginInterface
}
/**
*
*/
export function destroyCaptcha() {
clearCountdown()
pageInstance = null
currentOptions = null
}
Loading…
Cancel
Save