Browse Source
refactor(患者详情): 重构患者详情页面的日记记录展示逻辑 fix(图片对比): 修复对比日期选择状态同步问题 style(弹窗): 优化弹窗17的样式和布局 perf(图片裁剪): 实现内置裁剪功能替代页面跳转 docs(接口文档): 更新照片上传接口路径变更说明dev
41 changed files with 1294 additions and 452 deletions
@ -0,0 +1,419 @@
@@ -0,0 +1,419 @@
|
||||
# 项目规则 - 信达小程序 (Xinda Mini Program) |
||||
|
||||
WeChat Mini Program for thyroid eye disease patient management. Dual-mode app serving both patients (患者端) and doctors (医生端). |
||||
|
||||
## Project Type |
||||
|
||||
- **Framework**: WeChat Mini Program v3.7.7 |
||||
- **Language**: TypeScript (strict mode) |
||||
- **Styling**: SCSS/Sass via `useCompilerPlugins` |
||||
- **UI Library**: Vant Weapp (@vant/weapp) |
||||
- **Renderer**: Skyline enabled |
||||
- **App ID**: `wxf9ce8010f1ad24aa` (dev), `wx71ac9c27c3c3e3f4` (prod) |
||||
|
||||
## Directory Structure |
||||
|
||||
``` |
||||
src/ |
||||
├── pages/ # Doctor-side main pages (医生端) |
||||
├── patient/pages/ # Patient-side pages (subpackage) |
||||
├── gift/pages/ # DTP pharmacy pages (subpackage) |
||||
├── doc/pages/ # Privacy/terms documentation (subpackage) |
||||
├── components/ # Shared components |
||||
├── utils/ # Request, page/component wrappers, utilities |
||||
├── app.ts # App entry with global data |
||||
└── config.ts # Environment configs per appId |
||||
``` |
||||
|
||||
## Key Conventions |
||||
|
||||
### Path Aliases |
||||
|
||||
- `@/*` → `src/*` (configured in tsconfig.json and app.json `resolveAlias`) |
||||
- Use `@/utils/request` not relative paths |
||||
|
||||
### User Types & Routing |
||||
|
||||
Login types enforced in `app.ts`: |
||||
|
||||
- `0`: Not logged in |
||||
- `1`: Patient (患者) → routes to `/patient/pages/*` |
||||
- `2`: Doctor (医生) → routes to `/pages/*` |
||||
|
||||
Use `app.zdWaitLogin()` to guard pages that require login. |
||||
|
||||
### Page/Component Wrappers |
||||
|
||||
`app.ts` overrides global `Page` and `Component` with wrappers from `utils/page.ts` and `utils/component.ts`: |
||||
|
||||
- Auto-sets `imageUrl` and `Timestamp` on page load |
||||
- Auto-handles navbar background on scroll |
||||
- Provides default share behavior |
||||
|
||||
### Modal Colors (Required) |
||||
|
||||
All `wx.showModal` must use: |
||||
|
||||
```ts |
||||
wx.showModal({ |
||||
confirmColor: '#8c75d0', |
||||
cancelColor: '#141515', |
||||
}) |
||||
``` |
||||
|
||||
### App Instance (Required) |
||||
|
||||
`getApp()` should be called at the top of the file, not inside methods: |
||||
|
||||
```ts |
||||
// ✅ Correct |
||||
const app = getApp<IAppOption>() |
||||
|
||||
Page({ |
||||
onLoad() { |
||||
// Use app directly |
||||
console.log(app.globalData) |
||||
} |
||||
}) |
||||
|
||||
// ❌ Incorrect |
||||
Page({ |
||||
onLoad() { |
||||
const app = getApp<IAppOption>() |
||||
console.log(app.globalData) |
||||
} |
||||
}) |
||||
``` |
||||
|
||||
## WXML 开发规范 |
||||
|
||||
### 1. 不支持在 WXML 中直接调用方法 |
||||
|
||||
**错误示例:** |
||||
|
||||
```xml |
||||
<!-- ❌ 错误:WXML 中不能直接调用方法 --> |
||||
<view>{{getUploadedCount()}}</view> |
||||
<view wx:if="{{hasPhotos()}}">内容</view> |
||||
``` |
||||
|
||||
**正确做法:** |
||||
|
||||
```xml |
||||
<!-- ✅ 正确:使用数据绑定 --> |
||||
<view>{{uploadedCount}}</view> |
||||
<view wx:if="{{hasPhotos}}">内容</view> |
||||
``` |
||||
|
||||
```typescript |
||||
// 在 TS 中计算好数据 |
||||
this.setData({ |
||||
uploadedCount: photos.length, |
||||
hasPhotos: photos.length > 0 |
||||
}) |
||||
``` |
||||
|
||||
### 2. 列表渲染时使用 wx:key |
||||
|
||||
```xml |
||||
<view wx:for="{{list}}" wx:key="id">{{item.name}}</view> |
||||
``` |
||||
|
||||
### 3. 条件渲染 |
||||
|
||||
```xml |
||||
<view wx:if="{{condition}}">显示</view> |
||||
<view wx:else>隐藏</view> |
||||
``` |
||||
|
||||
### 4. WXML 表达式限制 |
||||
|
||||
WXML 中的双大括号 `{{}}` 只能使用简单的表达式,**不支持**以下语法: |
||||
|
||||
**❌ 不支持的语法:** |
||||
|
||||
```xml |
||||
<!-- 方法调用 --> |
||||
<view>{{arr.indexOf(item) > -1}}</view> |
||||
|
||||
<!-- 复杂表达式 --> |
||||
<view>{{obj.method().property}}</view> |
||||
|
||||
<!-- 正则表达式 --> |
||||
<view>{{str.match(/regex/)}}</view> |
||||
|
||||
<!-- new 操作符 --> |
||||
<view>{{new Date().getTime()}}</view> |
||||
``` |
||||
|
||||
**✅ 支持的语法:** |
||||
|
||||
```xml |
||||
<!-- 简单属性访问 --> |
||||
<view>{{item.name}}</view> |
||||
|
||||
<!-- 三元表达式 --> |
||||
<view class="{{item.isActive ? 'active' : ''}}">内容</view> |
||||
|
||||
<!-- 简单比较 --> |
||||
<view wx:if="{{count > 0}}">有数据</view> |
||||
|
||||
<!-- 逻辑运算 --> |
||||
<view wx:if="{{isLogin && isVip}}">VIP用户</view> |
||||
``` |
||||
|
||||
**解决方案:** |
||||
|
||||
```typescript |
||||
// 在 TS 中预处理数据,将复杂计算转换为简单布尔值 |
||||
this.setData({ |
||||
// ❌ 不要在 WXML 中使用 indexOf |
||||
// isSelected: selectedDates.indexOf(item.recordId) > -1 |
||||
|
||||
// ✅ 在数据中直接提供 isSelected 字段 |
||||
list: rawList.map(item => ({ |
||||
...item, |
||||
isSelected: selectedDates.includes(item.recordId) |
||||
})) |
||||
}) |
||||
``` |
||||
|
||||
## Available Commands |
||||
|
||||
```bash |
||||
# Lint and auto-fix (only command available) |
||||
npm run lint:fix |
||||
|
||||
# Install dependencies (pnpm preferred based on lockfile) |
||||
pnpm install |
||||
|
||||
# Build: Use WeChat Developer Tools |
||||
# - Import project with src/ as root |
||||
# - project.config.json at repo root |
||||
``` |
||||
|
||||
## NPM Dependencies |
||||
|
||||
**Critical**: After `npm install`, run **Tools → Build npm** in WeChat Developer Tools to generate `miniprogram_npm/`. This is required for Vant and other packages. |
||||
|
||||
Key dependencies: |
||||
|
||||
- `@vant/weapp`: UI components |
||||
- `miniprogram-licia`: Utility library (available as `licia`) |
||||
- `dayjs`: Date handling |
||||
- `mp-html`: Rich HTML rendering |
||||
|
||||
## Images & Assets |
||||
|
||||
**Images are stored in SVN, not git.** |
||||
|
||||
- SVN URL: `svn://39.106.86.127:28386/projects/xd/proj_src/shop/frontend/web/xd/` |
||||
- Local path: `src/images/` (gitignored) |
||||
- Excluded from upload via `project.config.json` packOptions |
||||
- Only `/images/tabbar/*` is included in uploads |
||||
|
||||
Image URL pattern: |
||||
|
||||
``` |
||||
{{imageUrl}}/path/to/image.png?t={{Timestamp}} |
||||
``` |
||||
|
||||
## TypeScript Configuration |
||||
|
||||
- Strict mode enabled |
||||
- `noImplicitAny: false` (allows implicit any) |
||||
- Paths: `@/*` mapped to `src/*` |
||||
- Types: `miniprogram-api-typings` for WX API |
||||
|
||||
Global types in `typings/index.d.ts`: |
||||
|
||||
- `IAppOption`: Global app instance interface |
||||
- `pageType`: 0 | 1 | 2 for user types |
||||
- `wx.ajax`: Extended request method |
||||
|
||||
## ESLint & Formatting |
||||
|
||||
- Config: `@antfu/eslint-config` (flat config in `eslint.config.js`) |
||||
- Prettier: 2-space tabs, no semis, single quotes, trailing commas |
||||
- WXML files parsed as HTML, WXSS as CSS |
||||
- Globals defined: `wx`, `App`, `Page`, `Component`, `getCurrentPages`, etc. |
||||
|
||||
## Environment Configuration |
||||
|
||||
Selected by App ID in `src/config.ts`: |
||||
|
||||
- `wxf9ce8010f1ad24aa`: Dev/Staging (hbraas.com) |
||||
- `wx71ac9c27c3c3e3f4`: Production (hbsaas.com) |
||||
|
||||
App reads `wx.getAccountInfoSync().miniProgram.appId` on launch to select config. |
||||
|
||||
## Testing & Development |
||||
|
||||
- No unit test framework configured |
||||
- Manual testing via WeChat Developer Tools |
||||
- `project.private.config.json` has hot reload enabled (`compileHotReLoad: true`) |
||||
- Predefined test pages in `project.private.config.json` condition list |
||||
|
||||
## Common Gotchas |
||||
|
||||
1. **NPM packages**: Must run "Build npm" in WeChat tools after install |
||||
2. **Images**: Will 404 if SVN images not checked out to `src/images/` |
||||
3. **Subpackages**: Patient pages are in `patient/` subpackage, not main package |
||||
4. **Skyline**: Enabled in rendererOptions, some components may behave differently |
||||
5. **Login flow**: App automatically calls `startLogin()` on launch; pages must wait via `app.zdWaitLogin()` |
||||
|
||||
## 自定义导航栏规范 |
||||
|
||||
### 统一使用 navbar 组件 |
||||
|
||||
所有需要自定义导航的页面统一使用 `/components/navbar/index`。`/components/zd-navBar/navBar` 是历史遗留组件,新页面**不要使用**。 |
||||
|
||||
**JSON 配置:** |
||||
|
||||
```json |
||||
{ |
||||
"navigationStyle": "custom", |
||||
"usingComponents": { |
||||
"navbar": "/components/navbar/index" |
||||
} |
||||
} |
||||
``` |
||||
|
||||
**WXML 用法:** |
||||
|
||||
```xml |
||||
<navbar fixed title="页面标题" custom-style="background:{{background}}"> |
||||
<van-icon name="arrow-left" slot="left" size="18px" color="#000" bind:tap="handleBack" /> |
||||
</navbar> |
||||
|
||||
<view class="page" style="padding-top:{{pageTop+20}}px;"> |
||||
``` |
||||
|
||||
**TS 实现:** |
||||
|
||||
```typescript |
||||
handleBack() { |
||||
wx.navigateBack() |
||||
} |
||||
``` |
||||
|
||||
### 关键参数 |
||||
|
||||
| 属性 | 说明 | 常用值 | |
||||
| -------------- | ------------ | --------------------------- | |
||||
| `fixed` | 固定导航栏 | `fixed` | |
||||
| `title` | 导航栏标题 | 字符串 | |
||||
| `custom-style` | 自定义样式 | `background:{{background}}` | |
||||
| `leftArrow` | 显示返回箭头 | Boolean | |
||||
| `slot="left"` | 左侧插槽 | van-icon 返回按钮 | |
||||
| `bind:tap` | 返回按钮点击 | `handleBack` | |
||||
|
||||
### pageTop 变量 |
||||
|
||||
`pageTop` 由 `utils/page.ts` wrapper 自动注入,表示导航栏高度。页面内容区使用 `padding-top:{{pageTop+20}}px;` 避开导航栏。**不要手动计算 navBarHeight**。 |
||||
|
||||
## 返回拦截与弹窗规范 |
||||
|
||||
### 使用自定义弹窗替代系统弹窗 |
||||
|
||||
**禁止使用** `wx.showModal` 进行操作确认,统一使用项目内的 popup 组件: |
||||
|
||||
```json |
||||
{ |
||||
"usingComponents": { |
||||
"popup": "/components/popup/index" |
||||
} |
||||
} |
||||
``` |
||||
|
||||
```xml |
||||
<popup show="{{popupShow}}" type="{{popupType}}" params="{{popupParams}}" bind:ok="handlePopupOk" bind:cancel="handlePopupCancel" /> |
||||
``` |
||||
|
||||
### 常用 popup 类型 |
||||
|
||||
| 类型 | 场景 | 标题 | 确认按钮 | 取消按钮 | |
||||
| --------- | -------------- | ------------------ | -------- | -------- | |
||||
| `popup15` | 删除确认 | "确认删除记录?" | 确认删除 | 取消 | |
||||
| `popup16` | 未保存数据退出 | "您的记录还未保存" | 保存记录 | 退出 | |
||||
| `popup17` | 裁剪未保存退出 | "您有裁剪的照片" | 退出 | 取消 | |
||||
|
||||
### popup 数据定义 |
||||
|
||||
```typescript |
||||
data: { |
||||
popupShow: false, |
||||
popupType: 'popup16', |
||||
popupParams: { |
||||
close: false, |
||||
position: 'center', |
||||
} as any, |
||||
} |
||||
``` |
||||
|
||||
### 返回拦截模式 |
||||
|
||||
当页面有未保存数据时,使用自定义导航 + `bind:back` 拦截返回: |
||||
|
||||
```typescript |
||||
handleBack() { |
||||
if (this.hasUnsavedData()) { |
||||
this.setData({ popupShow: true, popupType: 'popup16' }) |
||||
} else { |
||||
wx.navigateBack() |
||||
} |
||||
} |
||||
|
||||
handlePopupOk() { |
||||
this.setData({ popupShow: false }) |
||||
this.handleSave() |
||||
} |
||||
|
||||
handlePopupCancel() { |
||||
this.setData({ popupShow: false }) |
||||
wx.navigateBack() |
||||
} |
||||
``` |
||||
|
||||
**注意**:`navigationStyle: "custom"` 后系统返回手势无法拦截,只有 navBar 的返回按钮可被拦截。如需拦截系统返回手势,需要额外使用 `wx.enableAlertBeforeUnload`。 |
||||
|
||||
## wx.cropImage 规范 |
||||
|
||||
`wx.cropImage` 的 `src` 参数**只接受本地文件路径**,不支持网络 URL。裁剪网络图片时必须先用 `wx.getImageInfo` 下载到本地: |
||||
|
||||
```typescript |
||||
handleCrop(e: any) { |
||||
const index = e.currentTarget.dataset.index |
||||
const photo = this.data.photos[index] |
||||
|
||||
wx.showLoading({ title: '加载中...' }) |
||||
wx.getImageInfo({ |
||||
src: photo.photoUrl, |
||||
success: (imgRes) => { |
||||
wx.hideLoading() |
||||
wx.cropImage({ |
||||
src: imgRes.path, |
||||
cropScale: '9:16', |
||||
success: (cropRes) => { |
||||
photos[index].croppedUrl = cropRes.tempFilePath |
||||
this.setData({ photos }) |
||||
}, |
||||
fail: (err) => { |
||||
if (err.errMsg?.includes('cancel')) return |
||||
wx.showToast({ title: '裁剪取消', icon: 'none' }) |
||||
}, |
||||
}) |
||||
}, |
||||
fail: () => { |
||||
wx.hideLoading() |
||||
wx.showToast({ title: '图片加载失败', icon: 'none' }) |
||||
}, |
||||
}) |
||||
} |
||||
``` |
||||
|
||||
## 子包组件引用规范 |
||||
|
||||
主包页面(`src/pages/`)**不能引用**子包组件(`src/patient/components/`)。如果主包和子包都需要使用某个组件,需要将组件复制到 `src/components/` 目录下。 |
||||
|
||||
示例:`image-merge` 组件在 `patient/components/` 和 `components/` 各有一份,分别供子包和主包页面使用。 |
||||
@ -1,169 +0,0 @@
@@ -1,169 +0,0 @@
|
||||
# AGENTS.md - 信达小程序 (Xinda Mini Program) |
||||
|
||||
WeChat Mini Program for thyroid eye disease patient management. Dual-mode app serving both patients (患者端) and doctors (医生端). |
||||
|
||||
## Project Type |
||||
|
||||
- **Framework**: WeChat Mini Program v3.7.7 |
||||
- **Language**: TypeScript (strict mode) |
||||
- **Styling**: SCSS/Sass via `useCompilerPlugins` |
||||
- **UI Library**: Vant Weapp (@vant/weapp) |
||||
- **Renderer**: Skyline enabled |
||||
- **App ID**: `wxf9ce8010f1ad24aa` (dev), `wx71ac9c27c3c3e3f4` (prod) |
||||
|
||||
## Directory Structure |
||||
|
||||
``` |
||||
src/ |
||||
├── pages/ # Doctor-side main pages (医生端) |
||||
├── patient/pages/ # Patient-side pages (subpackage) |
||||
├── gift/pages/ # DTP pharmacy pages (subpackage) |
||||
├── doc/pages/ # Privacy/terms documentation (subpackage) |
||||
├── components/ # Shared components |
||||
├── utils/ # Request, page/component wrappers, utilities |
||||
├── app.ts # App entry with global data |
||||
└── config.ts # Environment configs per appId |
||||
``` |
||||
|
||||
## Key Conventions |
||||
|
||||
### Path Aliases |
||||
|
||||
- `@/*` → `src/*` (configured in tsconfig.json and app.json `resolveAlias`) |
||||
- Use `@/utils/request` not relative paths |
||||
|
||||
### User Types & Routing |
||||
|
||||
Login types enforced in `app.ts`: |
||||
|
||||
- `0`: Not logged in |
||||
- `1`: Patient (患者) → routes to `/patient/pages/*` |
||||
- `2`: Doctor (医生) → routes to `/pages/*` |
||||
|
||||
Use `app.zdWaitLogin()` to guard pages that require login. |
||||
|
||||
### Page/Component Wrappers |
||||
|
||||
`app.ts` overrides global `Page` and `Component` with wrappers from `utils/page.ts` and `utils/component.ts`: |
||||
|
||||
- Auto-sets `imageUrl` and `Timestamp` on page load |
||||
- Auto-handles navbar background on scroll |
||||
- Provides default share behavior |
||||
|
||||
### Modal Colors (Required) |
||||
|
||||
All `wx.showModal` must use: |
||||
|
||||
```ts |
||||
wx.showModal({ |
||||
confirmColor: '#8c75d0', |
||||
cancelColor: '#141515', |
||||
}) |
||||
``` |
||||
|
||||
### App Instance (Required) |
||||
|
||||
`getApp()` should be called at the top of the file, not inside methods: |
||||
|
||||
````ts |
||||
// ✅ Correct |
||||
const app = getApp<IAppOption>() |
||||
|
||||
Page({ |
||||
onLoad() { |
||||
// Use app directly |
||||
console.log(app.globalData) |
||||
} |
||||
}) |
||||
|
||||
// ❌ Incorrect |
||||
Page({ |
||||
onLoad() { |
||||
const app = getApp<IAppOption>() |
||||
console.log(app.globalData) |
||||
} |
||||
})``` |
||||
|
||||
## Available Commands |
||||
|
||||
```bash |
||||
# Lint and auto-fix (only command available) |
||||
npm run lint:fix |
||||
|
||||
# Install dependencies (pnpm preferred based on lockfile) |
||||
pnpm install |
||||
|
||||
# Build: Use WeChat Developer Tools |
||||
# - Import project with src/ as root |
||||
# - project.config.json at repo root |
||||
```` |
||||
|
||||
## NPM Dependencies |
||||
|
||||
**Critical**: After `npm install`, run **Tools → Build npm** in WeChat Developer Tools to generate `miniprogram_npm/`. This is required for Vant and other packages. |
||||
|
||||
Key dependencies: |
||||
|
||||
- `@vant/weapp`: UI components |
||||
- `miniprogram-licia`: Utility library (available as `licia`) |
||||
- `dayjs`: Date handling |
||||
- `mp-html`: Rich HTML rendering |
||||
|
||||
## Images & Assets |
||||
|
||||
**Images are stored in SVN, not git.** |
||||
|
||||
- SVN URL: `svn://39.106.86.127:28386/projects/xd/proj_src/shop/frontend/web/xd/` |
||||
- Local path: `src/images/` (gitignored) |
||||
- Excluded from upload via `project.config.json` packOptions |
||||
- Only `/images/tabbar/*` is included in uploads |
||||
|
||||
Image URL pattern: |
||||
|
||||
``` |
||||
{{imageUrl}}/path/to/image.png?t={{Timestamp}} |
||||
``` |
||||
|
||||
## TypeScript Configuration |
||||
|
||||
- Strict mode enabled |
||||
- `noImplicitAny: false` (allows implicit any) |
||||
- Paths: `@/*` mapped to `src/*` |
||||
- Types: `miniprogram-api-typings` for WX API |
||||
|
||||
Global types in `typings/index.d.ts`: |
||||
|
||||
- `IAppOption`: Global app instance interface |
||||
- `pageType`: 0 | 1 | 2 for user types |
||||
- `wx.ajax`: Extended request method |
||||
|
||||
## ESLint & Formatting |
||||
|
||||
- Config: `@antfu/eslint-config` (flat config in `eslint.config.js`) |
||||
- Prettier: 2-space tabs, no semis, single quotes, trailing commas |
||||
- WXML files parsed as HTML, WXSS as CSS |
||||
- Globals defined: `wx`, `App`, `Page`, `Component`, `getCurrentPages`, etc. |
||||
|
||||
## Environment Configuration |
||||
|
||||
Selected by App ID in `src/config.ts`: |
||||
|
||||
- `wxf9ce8010f1ad24aa`: Dev/Staging (hbraas.com) |
||||
- `wx71ac9c27c3c3e3f4`: Production (hbsaas.com) |
||||
|
||||
App reads `wx.getAccountInfoSync().miniProgram.appId` on launch to select config. |
||||
|
||||
## Testing & Development |
||||
|
||||
- No unit test framework configured |
||||
- Manual testing via WeChat Developer Tools |
||||
- `project.private.config.json` has hot reload enabled (`compileHotReLoad: true`) |
||||
- Predefined test pages in `project.private.config.json` condition list |
||||
|
||||
## Common Gotchas |
||||
|
||||
1. **NPM packages**: Must run "Build npm" in WeChat tools after install |
||||
2. **Images**: Will 404 if SVN images not checked out to `src/images/` |
||||
3. **Subpackages**: Patient pages are in `patient/` subpackage, not main package |
||||
4. **Skyline**: Enabled in rendererOptions, some components may behave differently |
||||
5. **Login flow**: App automatically calls `startLogin()` on launch; pages must wait via `app.zdWaitLogin()` |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
{ |
||||
"component": true, |
||||
"usingComponents": { |
||||
"van-button": "@vant/weapp/button/index", |
||||
"van-icon": "@vant/weapp/icon/index", |
||||
"van-toast": "@vant/weapp/toast/index" |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
.merge-canvas { |
||||
position: fixed; |
||||
left: -9999px; |
||||
top: -9999px; |
||||
width: 1px; |
||||
height: 1px; |
||||
} |
||||
@ -0,0 +1,186 @@
@@ -0,0 +1,186 @@
|
||||
interface ImageItem { |
||||
src: string |
||||
time?: string |
||||
} |
||||
|
||||
Component({ |
||||
properties: { |
||||
id: { |
||||
type: String, |
||||
value: 'default', |
||||
}, |
||||
}, |
||||
|
||||
data: { |
||||
imageList: [] as ImageItem[], |
||||
mergedImage: '', |
||||
isLoading: false, |
||||
}, |
||||
|
||||
methods: { |
||||
mergeImages(imageList: ImageItem[]) { |
||||
if (this.data.isLoading) { |
||||
return |
||||
} |
||||
|
||||
if (!imageList || imageList.length < 2) { |
||||
wx.showToast({ title: '至少需要2张图片才能拼接', icon: 'none' }) |
||||
return |
||||
} |
||||
|
||||
wx.showLoading({ title: '拼接中...', mask: true }) |
||||
this.setData({ isLoading: true, imageList }) |
||||
|
||||
this.validateImages(imageList) |
||||
.then(() => this.drawImagesOnCanvas()) |
||||
.then((mergedImage) => { |
||||
wx.hideLoading() |
||||
this.setData({ |
||||
mergedImage, |
||||
isLoading: false, |
||||
}) |
||||
this.triggerEvent('save', { tempFilePath: mergedImage }) |
||||
wx.previewImage({ |
||||
urls: [mergedImage], |
||||
current: mergedImage, |
||||
showmenu: true, |
||||
}) |
||||
}) |
||||
.catch((error) => { |
||||
wx.hideLoading() |
||||
console.error('拼接失败:', error) |
||||
this.setData({ isLoading: false }) |
||||
wx.showToast({ title: error.message || '拼接失败,请重试', icon: 'none' }) |
||||
this.triggerEvent('error', { reason: error.message }) |
||||
}) |
||||
}, |
||||
|
||||
validateImages(imageList: ImageItem[]): Promise<void> { |
||||
return Promise.all( |
||||
imageList.map( |
||||
(item, index) => |
||||
new Promise<void>((resolve, reject) => { |
||||
wx.getImageInfo({ |
||||
src: item.src, |
||||
success: () => resolve(), |
||||
fail: () => reject(new Error(`第${index + 1}张图片加载失败`)), |
||||
}) |
||||
}), |
||||
), |
||||
) |
||||
}, |
||||
|
||||
formatTime(date: Date): string { |
||||
const year = date.getFullYear() |
||||
const month = String(date.getMonth() + 1).padStart(2, '0') |
||||
const day = String(date.getDate()).padStart(2, '0') |
||||
const hours = String(date.getHours()).padStart(2, '0') |
||||
const minutes = String(date.getMinutes()).padStart(2, '0') |
||||
return `${year}-${month}-${day} ${hours}:${minutes}` |
||||
}, |
||||
|
||||
drawImagesOnCanvas(): Promise<string> { |
||||
return new Promise((resolve, reject) => { |
||||
const { imageList, id } = this.data |
||||
const query = this.createSelectorQuery() |
||||
query |
||||
.in(this) |
||||
.select(`#mergeCanvas-${id}`) |
||||
.fields({ node: true, size: true }) |
||||
.exec((res) => { |
||||
if (!res[0]) { |
||||
reject(new Error('Canvas not found')) |
||||
return |
||||
} |
||||
|
||||
const canvas = res[0].node |
||||
const ctx = canvas.getContext('2d') |
||||
|
||||
Promise.all(imageList.map(item => this.getImageInfo(item.src))) |
||||
.then((imageInfos) => { |
||||
const targetWidth = 750 |
||||
const pixelRatio = wx.getWindowInfo().pixelRatio |
||||
const canvasWidth = Math.floor((targetWidth * pixelRatio) / 2) |
||||
|
||||
let totalHeight = 0 |
||||
const scaledHeights: number[] = [] |
||||
|
||||
imageInfos.forEach((info) => { |
||||
const scale = canvasWidth / info.width |
||||
const scaledHeight = Math.floor(info.height * scale) |
||||
scaledHeights.push(scaledHeight) |
||||
totalHeight += scaledHeight |
||||
}) |
||||
|
||||
canvas.width = canvasWidth |
||||
canvas.height = totalHeight |
||||
|
||||
let currentY = 0 |
||||
let loadedCount = 0 |
||||
|
||||
imageInfos.forEach((info, index) => { |
||||
const img = canvas.createImage() |
||||
img.src = info.path |
||||
img.onload = () => { |
||||
ctx.drawImage(img, 0, currentY, canvasWidth, scaledHeights[index]) |
||||
|
||||
const timeText = imageList[index].time || this.formatTime(new Date()) |
||||
const padding = 20 |
||||
const textY = currentY + 40 |
||||
|
||||
ctx.fillStyle = '#ffffff' |
||||
ctx.font = 'bold 28px sans-serif' |
||||
ctx.textAlign = 'left' |
||||
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)' |
||||
ctx.shadowBlur = 4 |
||||
ctx.shadowOffsetX = 1 |
||||
ctx.shadowOffsetY = 1 |
||||
ctx.fillText(timeText, padding, textY) |
||||
|
||||
ctx.shadowColor = 'transparent' |
||||
ctx.shadowBlur = 0 |
||||
ctx.shadowOffsetX = 0 |
||||
ctx.shadowOffsetY = 0 |
||||
|
||||
if (index === imageInfos.length - 1) { |
||||
const lastImageBottom = currentY + scaledHeights[index] |
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)' |
||||
ctx.font = '24px sans-serif' |
||||
ctx.textAlign = 'right' |
||||
ctx.fillText('由-TED关爱小助手-小程序生成', canvasWidth - 20, lastImageBottom - 20) |
||||
} |
||||
|
||||
currentY += scaledHeights[index] |
||||
loadedCount++ |
||||
|
||||
if (loadedCount === imageInfos.length) { |
||||
wx.canvasToTempFilePath({ |
||||
canvas, |
||||
success: (result) => { |
||||
resolve(result.tempFilePath) |
||||
}, |
||||
fail: reject, |
||||
}) |
||||
} |
||||
} |
||||
img.onerror = () => { |
||||
reject(new Error(`Failed to load image: ${info.path}`)) |
||||
} |
||||
}) |
||||
}) |
||||
.catch(reject) |
||||
}) |
||||
}) |
||||
}, |
||||
|
||||
getImageInfo(src: string): Promise<WechatMiniprogram.GetImageInfoSuccessCallbackResult> { |
||||
return new Promise((resolve, reject) => { |
||||
wx.getImageInfo({ |
||||
src, |
||||
success: resolve, |
||||
fail: reject, |
||||
}) |
||||
}) |
||||
}, |
||||
}, |
||||
}) |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
<canvas type="2d" id="mergeCanvas-{{id}}" class="merge-canvas"></canvas> |
||||
@ -1,4 +1,7 @@
@@ -1,4 +1,7 @@
|
||||
{ |
||||
"navigationBarTitleText": "照片对比", |
||||
"usingComponents": {} |
||||
"usingComponents": { |
||||
"navbar": "/components/navbar/index", |
||||
"van-icon": "@vant/weapp/icon/index" |
||||
} |
||||
} |
||||
|
||||
@ -1,5 +1,6 @@
@@ -1,5 +1,6 @@
|
||||
{ |
||||
"navigationBarTitleText": "xxx的眼突度对比", |
||||
"navigationStyle": "default", |
||||
"usingComponents": {} |
||||
"navigationBarTitleText": "眼突度对比", |
||||
"usingComponents": { |
||||
"navbar": "/components/navbar/index" |
||||
} |
||||
} |
||||
|
||||
@ -1,6 +1,8 @@
@@ -1,6 +1,8 @@
|
||||
{ |
||||
"navigationBarTitleText": "对比图编辑", |
||||
"usingComponents": { |
||||
"imageMerge": "/patient/components/image-merge/index" |
||||
"navbar": "/components/navbar/index", |
||||
"imageMerge": "/components/image-merge/index", |
||||
"popup": "/components/popup/index" |
||||
} |
||||
} |
||||
|
||||
@ -1,7 +1,8 @@
@@ -1,7 +1,8 @@
|
||||
{ |
||||
"navigationStyle": "default", |
||||
"usingComponents": { |
||||
"van-icon": "@vant/weapp/icon/index" |
||||
"navbar": "/components/navbar/index", |
||||
"van-icon": "@vant/weapp/icon/index", |
||||
"pagination": "/components/pagination/index" |
||||
}, |
||||
"navigationBarTitleText": "记录" |
||||
} |
||||
|
||||
@ -1,8 +1,9 @@
@@ -1,8 +1,9 @@
|
||||
{ |
||||
"navigationStyle": "default", |
||||
"usingComponents": { |
||||
"navbar": "/components/navbar/index", |
||||
"van-icon": "@vant/weapp/icon/index", |
||||
"imageMerge": "/patient/components/image-merge/index" |
||||
"imageMerge": "/patient/components/image-merge/index", |
||||
"popup": "/components/popup/index" |
||||
}, |
||||
"navigationBarTitleText": "照片对比分析" |
||||
} |
||||
|
||||
Loading…
Reference in new issue