Browse Source
refactor(患者详情): 重构患者详情页面的日记记录展示逻辑 fix(图片对比): 修复对比日期选择状态同步问题 style(弹窗): 优化弹窗17的样式和布局 perf(图片裁剪): 实现内置裁剪功能替代页面跳转 docs(接口文档): 更新照片上传接口路径变更说明dev
41 changed files with 1294 additions and 452 deletions
@ -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 @@ |
|||||||
# 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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
.merge-canvas { |
||||||
|
position: fixed; |
||||||
|
left: -9999px; |
||||||
|
top: -9999px; |
||||||
|
width: 1px; |
||||||
|
height: 1px; |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
<canvas type="2d" id="mergeCanvas-{{id}}" class="merge-canvas"></canvas> |
||||||
@ -1,4 +1,7 @@ |
|||||||
{ |
{ |
||||||
"navigationBarTitleText": "照片对比", |
"navigationBarTitleText": "照片对比", |
||||||
"usingComponents": {} |
"usingComponents": { |
||||||
|
"navbar": "/components/navbar/index", |
||||||
|
"van-icon": "@vant/weapp/icon/index" |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,5 +1,6 @@ |
|||||||
{ |
{ |
||||||
"navigationBarTitleText": "xxx的眼突度对比", |
"navigationBarTitleText": "眼突度对比", |
||||||
"navigationStyle": "default", |
"usingComponents": { |
||||||
"usingComponents": {} |
"navbar": "/components/navbar/index" |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,6 +1,8 @@ |
|||||||
{ |
{ |
||||||
"navigationBarTitleText": "对比图编辑", |
"navigationBarTitleText": "对比图编辑", |
||||||
"usingComponents": { |
"usingComponents": { |
||||||
"imageMerge": "/patient/components/image-merge/index" |
"navbar": "/components/navbar/index", |
||||||
|
"imageMerge": "/components/image-merge/index", |
||||||
|
"popup": "/components/popup/index" |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,7 +1,8 @@ |
|||||||
{ |
{ |
||||||
"navigationStyle": "default", |
|
||||||
"usingComponents": { |
"usingComponents": { |
||||||
"van-icon": "@vant/weapp/icon/index" |
"navbar": "/components/navbar/index", |
||||||
|
"van-icon": "@vant/weapp/icon/index", |
||||||
|
"pagination": "/components/pagination/index" |
||||||
}, |
}, |
||||||
"navigationBarTitleText": "记录" |
"navigationBarTitleText": "记录" |
||||||
} |
} |
||||||
|
|||||||
@ -1,8 +1,9 @@ |
|||||||
{ |
{ |
||||||
"navigationStyle": "default", |
|
||||||
"usingComponents": { |
"usingComponents": { |
||||||
|
"navbar": "/components/navbar/index", |
||||||
"van-icon": "@vant/weapp/icon/index", |
"van-icon": "@vant/weapp/icon/index", |
||||||
"imageMerge": "/patient/components/image-merge/index" |
"imageMerge": "/patient/components/image-merge/index", |
||||||
|
"popup": "/components/popup/index" |
||||||
}, |
}, |
||||||
"navigationBarTitleText": "照片对比分析" |
"navigationBarTitleText": "照片对比分析" |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue