Compare commits
No commits in common. 'master' and 'lightspot' have entirely different histories.
505 changed files with 23190 additions and 32614 deletions
@ -1,240 +0,0 @@ |
|||||||
# 突眼日记接口联调计划 |
|
||||||
|
|
||||||
## 一、项目现状分析 |
|
||||||
|
|
||||||
### 1.1 已有页面结构 |
|
||||||
|
|
||||||
**医生端 (d\_ 前缀)** |
|
||||||
|
|
||||||
- `d_noteList` - 突眼记录列表页(拍摄示例页) |
|
||||||
|
|
||||||
- `d_noteDetail` - 突眼记录详情页 |
|
||||||
|
|
||||||
- `d_noteDiffData` - 凸眼度对比数据页(表格展示) |
|
||||||
|
|
||||||
_患者端 (patient/pages/note_)\* |
|
||||||
|
|
||||||
- `note` - 突眼日记首页 |
|
||||||
|
|
||||||
- `noteAdd` - 新增记录页 |
|
||||||
|
|
||||||
- `noteHistory` - 历史记录页 |
|
||||||
|
|
||||||
- `noteDiff` - 照片对比页 |
|
||||||
|
|
||||||
- `noteDiffEdit` - 对比编辑页 |
|
||||||
|
|
||||||
- `noteDemo` - 拍摄示例页 |
|
||||||
|
|
||||||
### 1.2 当前状态 |
|
||||||
|
|
||||||
- 所有页面都是静态数据,未接入接口 |
|
||||||
|
|
||||||
- 请求工具已封装在 `utils/request.ts` |
|
||||||
|
|
||||||
- 使用 `wx.ajax` 进行接口调用(在 app.ts 中挂载) |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 二、需要的信息 |
|
||||||
|
|
||||||
在开始联调前,需要你提供以下信息: |
|
||||||
|
|
||||||
### 2.1 环境信息 |
|
||||||
|
|
||||||
| 信息项 | 说明 | 是否必需 | |
|
||||||
| -------------- | --------------------------- | -------- | |
|
||||||
| 后端环境地址 | 开发/测试服务器地址 | 是 | |
|
||||||
| 接口是否已部署 | 后端接口是否已上线可调用 | 是 | |
|
||||||
| 登录态获取方式 | 如何获取 loginState/session | 是 | |
|
||||||
|
|
||||||
### 2.2 接口测试信息 |
|
||||||
|
|
||||||
| 信息项 | 说明 | 是否必需 | |
|
||||||
| ---------- | -------------------------- | -------- | |
|
||||||
| 测试账号 | 医生账号和患者账号 | 是 | |
|
||||||
| 测试患者ID | 用于医生端接口测试 | 是 | |
|
||||||
| 已有数据 | 是否有已创建的突眼记录数据 | 否 | |
|
||||||
|
|
||||||
### 2.3 图片上传相关 |
|
||||||
|
|
||||||
| 信息项 | 说明 | 是否必需 | |
|
||||||
| ------------- | ----------------------------- | -------- | |
|
||||||
| 图片上传方式 | 上传到腾讯云COS还是自家服务器 | 是 | |
|
||||||
| 腾讯云IMS配置 | 是否已配置图片内容安全校验 | 否 | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 三、接口联调清单 |
|
||||||
|
|
||||||
### 3.1 患者端接口 (10个) |
|
||||||
|
|
||||||
| 序号 | 接口 | 用途 | 对应页面 | 优先级 | |
|
||||||
| ---- | ------------------- | ---------------- | ----------------- | ------ | |
|
||||||
| 1 | `baseline-status` | 获取基准照状态 | note | P0 | |
|
||||||
| 2 | `record-list` | 获取记录列表 | note, noteHistory | P0 | |
|
||||||
| 3 | `record-detail` | 获取记录详情 | noteHistory | P0 | |
|
||||||
| 4 | `record-save` | 创建/更新记录 | noteAdd | P0 | |
|
||||||
| 5 | `photo-upload` | 上传照片 | noteAdd | P0 | |
|
||||||
| 6 | `record-delete` | 删除记录 | noteHistory | P1 | |
|
||||||
| 7 | `compare-dates` | 获取对比可用日期 | noteDiff | P1 | |
|
||||||
| 8 | `compare-photos` | 获取对比照片 | noteDiff | P1 | |
|
||||||
| 9 | `get-compare-angle` | 获取对比角度列表 | noteDiffEdit | P1 | |
|
||||||
| 10 | `get-session-id` | 获取唯一标识 | noteAdd | P2 | |
|
||||||
|
|
||||||
### 3.2 医生端接口 (6个) |
|
||||||
|
|
||||||
| 序号 | 接口 | 用途 | 对应页面 | 优先级 | |
|
||||||
| ---- | ------------------------------------ | ---------------- | --------------- | ------ | |
|
||||||
| 1 | `doctor/proptosis/latest` | 获取最近记录 | d_patientDetail | P0 | |
|
||||||
| 2 | `doctor/proptosis/list` | 获取患者记录列表 | d_noteList | P0 | |
|
||||||
| 3 | `doctor/proptosis/detail` | 获取记录详情 | d_noteDetail | P0 | |
|
||||||
| 4 | `doctor/proptosis/export` | Excel导出 | d_noteDiffData | P1 | |
|
||||||
| 5 | `doctor/proptosis/compare` | 获取对比数据 | d_noteDiffData | P1 | |
|
||||||
| 6 | `doctor/proptosis/get-compare-angle` | 获取对比角度 | d_noteDiffData | P1 | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 四、实施步骤 |
|
||||||
|
|
||||||
### Phase 1: 基础联调准备 |
|
||||||
|
|
||||||
1. 确认后端环境可用 |
|
||||||
2. 获取测试账号和患者ID |
|
||||||
3. 验证登录态获取 |
|
||||||
|
|
||||||
### Phase 2: 患者端核心功能 |
|
||||||
|
|
||||||
1. 实现记录列表获取 (`record-list`) |
|
||||||
2. 实现记录详情获取 (`record-detail`) |
|
||||||
3. 实现记录创建/更新 (`record-save`) |
|
||||||
4. 实现照片上传 (`photo-upload`) |
|
||||||
|
|
||||||
### Phase 3: 患者端对比功能 |
|
||||||
|
|
||||||
1. 实现对比日期获取 (`compare-dates`) |
|
||||||
2. 实现对比照片获取 (`compare-photos`) |
|
||||||
3. 实现对比角度获取 (`get-compare-angle`) |
|
||||||
|
|
||||||
### Phase 4: 医生端功能 |
|
||||||
|
|
||||||
1. 实现患者最近记录获取 (`doctor/proptosis/latest`) |
|
||||||
2. 实现患者记录列表 (`doctor/proptosis/list`) |
|
||||||
3. 实现记录详情 (`doctor/proptosis/detail`) |
|
||||||
4. 实现对比数据获取 (`doctor/proptosis/compare`) |
|
||||||
|
|
||||||
### Phase 5: 导出功能 |
|
||||||
|
|
||||||
1. 实现Excel导出 (`doctor/proptosis/export`) |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 五、数据结构映射 |
|
||||||
|
|
||||||
### 5.1 患者端 - 记录列表 |
|
||||||
|
|
||||||
```typescript |
|
||||||
// 接口返回 |
|
||||||
interface RecordItem { |
|
||||||
recordId: string |
|
||||||
recordDate: string |
|
||||||
isBaseline: number |
|
||||||
photoCount: number |
|
||||||
firstPhotoUrl: string |
|
||||||
treatmentCount: number |
|
||||||
leftEye: number |
|
||||||
rightEye: number |
|
||||||
interorbitalDistance: number |
|
||||||
uploadCompleted: number |
|
||||||
} |
|
||||||
|
|
||||||
// 页面使用 |
|
||||||
interface DataListItem { |
|
||||||
date: string // recordDate 格式化 |
|
||||||
left: string // leftEye |
|
||||||
spacing: string // interorbitalDistance |
|
||||||
right: string // rightEye |
|
||||||
count: string // treatmentCount |
|
||||||
isBaseline: boolean |
|
||||||
photoCount: number |
|
||||||
firstPhotoUrl: string |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
### 5.2 医生端 - 凸眼度对比表格 |
|
||||||
|
|
||||||
```typescript |
|
||||||
// 接口返回 (compare 接口) |
|
||||||
interface CompareItem { |
|
||||||
recordId: string |
|
||||||
recordDate: string |
|
||||||
photoUrl: string |
|
||||||
leftEye: number |
|
||||||
rightEye: number |
|
||||||
interorbitalDistance: number |
|
||||||
treatmentCount: number |
|
||||||
} |
|
||||||
|
|
||||||
// 页面使用 (d_noteDiffData) |
|
||||||
interface TableItem { |
|
||||||
date: string // recordDate 格式化 2026.1.5 |
|
||||||
left: string // leftEye |
|
||||||
spacing: string // interorbitalDistance |
|
||||||
right: string // rightEye |
|
||||||
count: string // treatmentCount |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 六、注意事项 |
|
||||||
|
|
||||||
### 6.1 业务规则 |
|
||||||
|
|
||||||
1. 同一天只能创建一条记录 |
|
||||||
2. 必须先上传照片再保存记录(photoIds 关联) |
|
||||||
3. 基准照只能有一张 |
|
||||||
4. 图片需要经过腾讯云IMS安全校验 |
|
||||||
|
|
||||||
### 6.2 技术注意 |
|
||||||
|
|
||||||
1. 使用 `wx.ajax` 进行请求(已挂载在 app 上) |
|
||||||
2. 医生端页面需要等待登录态 `app.waitLogin({ type: [2] })` |
|
||||||
3. 患者端页面需要等待登录态 `app.waitLogin({ type: [1] })` |
|
||||||
4. 图片URL需要拼接 `{{imageUrl}}` 前缀 |
|
||||||
|
|
||||||
### 6.3 角度映射 |
|
||||||
|
|
||||||
接口角度值与页面显示名称映射: |
|
||||||
|
|
||||||
```typescript |
|
||||||
const angleMap = { |
|
||||||
front_open: '正面睁眼', |
|
||||||
front_closed: '正面闭眼', |
|
||||||
front_looking_up: '正面仰头', |
|
||||||
side_left_90: '90°左侧', |
|
||||||
side_right_90: '90°右侧', |
|
||||||
side_left_45: '45°左侧', |
|
||||||
side_right_45: '45°右侧', |
|
||||||
eye_up_left: '左上', |
|
||||||
eye_up: '向上', |
|
||||||
eye_up_right: '右上', |
|
||||||
eye_left: '向左', |
|
||||||
eye_right: '向右', |
|
||||||
eye_down_left: '左下', |
|
||||||
eye_down: '向下', |
|
||||||
eye_down_right: '右下', |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## 七、下一步 |
|
||||||
|
|
||||||
请提供以下信息,我将开始实施联调: |
|
||||||
|
|
||||||
1. **后端环境地址**(如:<https://m.xd.hbraas.com)> |
|
||||||
2. **测试账号**(医生和患者各一个) |
|
||||||
3. **测试患者ID**(用于医生端接口测试) |
|
||||||
4. **接口是否已部署**(确认后端已完成开发) |
|
||||||
5. **图片上传方式**(腾讯云COS或自家服务器) |
|
||||||
@ -1,419 +0,0 @@ |
|||||||
# 项目规则 - 信达小程序 (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,16 +0,0 @@ |
|||||||
// Folder-specific settings |
|
||||||
// |
|
||||||
// For a full list of overridable settings, and general information on folder-specific settings, |
|
||||||
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files |
|
||||||
{ |
|
||||||
"lsp": { |
|
||||||
"emmet-language-server": { |
|
||||||
"initialization_options": { |
|
||||||
"preferences": { |
|
||||||
"css.intUnit": "rpx", |
|
||||||
"css.floatUnitr": "rpx", |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
} |
|
||||||
@ -1,32 +0,0 @@ |
|||||||
import antfu from '@antfu/eslint-config' |
|
||||||
|
|
||||||
export default antfu({ |
|
||||||
env: { |
|
||||||
es6: true, |
|
||||||
}, |
|
||||||
ignores: [ |
|
||||||
'**/miniprogram_npm/**', |
|
||||||
'src/miniprogram_npm/**', |
|
||||||
], |
|
||||||
parserOptions: { project: ['./tsconfig.json'] }, |
|
||||||
globals: { |
|
||||||
wx: true, |
|
||||||
App: true, |
|
||||||
Page: true, |
|
||||||
getCurrentPages: true, |
|
||||||
getApp: true, |
|
||||||
Component: true, |
|
||||||
requirePlugin: true, |
|
||||||
requireMiniProgram: true, |
|
||||||
}, |
|
||||||
rules: { |
|
||||||
'eslint-comments/no-unlimited-disable': 'off', |
|
||||||
'ts/no-require-imports': 'off', |
|
||||||
'eqeqeq': 'off', |
|
||||||
}, |
|
||||||
formatters: { |
|
||||||
css: 'prettier', |
|
||||||
html: 'prettier', |
|
||||||
markdown: 'prettier', |
|
||||||
}, |
|
||||||
}) |
|
||||||
@ -0,0 +1,28 @@ |
|||||||
|
// eslint.config.mjs
|
||||||
|
import antfu from '@antfu/eslint-config' |
||||||
|
import prettierConfig from 'eslint-config-prettier' |
||||||
|
|
||||||
|
export default antfu( |
||||||
|
{ |
||||||
|
env: { |
||||||
|
es6: true, |
||||||
|
}, |
||||||
|
stylistic: false, |
||||||
|
parserOptions: { project: ['./tsconfig.json'] }, |
||||||
|
globals: { |
||||||
|
wx: true, |
||||||
|
App: true, |
||||||
|
Page: true, |
||||||
|
getCurrentPages: true, |
||||||
|
getApp: true, |
||||||
|
Component: true, |
||||||
|
requirePlugin: true, |
||||||
|
requireMiniProgram: true, |
||||||
|
}, |
||||||
|
rules: { |
||||||
|
'eslint-comments/no-unlimited-disable': 'off', |
||||||
|
'ts/no-require-imports': 'off', |
||||||
|
}, |
||||||
|
}, |
||||||
|
prettierConfig, |
||||||
|
) |
||||||
@ -1,3 +1,3 @@ |
|||||||
{ |
{ |
||||||
"component": true |
"component": true |
||||||
} |
} |
||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1 +1 @@ |
|||||||
export {} |
export {}; |
||||||
|
|||||||
@ -1,25 +1,23 @@ |
|||||||
export default class Logger { |
export default class Logger { |
||||||
info(msg) { |
info(msg) { |
||||||
console.log( |
console.log( |
||||||
`%cInfo: %c${msg}`, |
'%cInfo: %c' + msg, |
||||||
'color:#FF0080;font-weight:bold', |
'color:#FF0080;font-weight:bold', |
||||||
'color: #FF509B', |
'color: #FF509B' |
||||||
) |
) |
||||||
} |
} |
||||||
|
|
||||||
warn(msg) { |
warn(msg) { |
||||||
console.log( |
console.log( |
||||||
`%cWarn: %c${msg}`, |
'%cWarn: %c' + msg, |
||||||
'color:#FF6600;font-weight:bold', |
'color:#FF6600;font-weight:bold', |
||||||
'color: #FF9933', |
'color: #FF9933' |
||||||
) |
) |
||||||
} |
} |
||||||
|
|
||||||
tips(msg) { |
tips(msg) { |
||||||
console.log( |
console.log( |
||||||
`%cTips: %c${msg}`, |
'%cTips: %c' + msg, |
||||||
'color:#00B200;font-weight:bold', |
'color:#00B200;font-weight:bold', |
||||||
'color: #00CC33', |
'color: #00CC33' |
||||||
) |
) |
||||||
} |
} |
||||||
} |
} |
||||||
|
|||||||
@ -1,103 +1,101 @@ |
|||||||
import Wxml2Canvas from './wxml2canvas/index.js' // 根据具体路径修改,node_modules会被忽略
|
import Wxml2Canvas from "./wxml2canvas/index.js"; // 根据具体路径修改,node_modules会被忽略
|
||||||
|
|
||||||
Component({ |
Component({ |
||||||
properties: { |
properties: { |
||||||
params: { |
params: { |
||||||
type: Object, |
type: Object, |
||||||
observer(newVal, _olVal) { |
observer(newVal, _olVal) { |
||||||
if (Object.keys(newVal).length > 0) { |
if (Object.keys(newVal).length > 0) { |
||||||
this.paramsFormat(newVal) |
this.paramsFormat(newVal); |
||||||
this.setData({ |
this.setData({ |
||||||
width: newVal.body.width, |
width: newVal.body.width, |
||||||
height: newVal.body.height, |
height: newVal.body.height, |
||||||
bgImg: newVal.body.bgImg, |
bgImg: newVal.body.bgImg, |
||||||
elementsMp: newVal.elements, |
elementsMp: newVal.elements, |
||||||
}) |
}); |
||||||
this.drawImage() |
this.drawImage1(); |
||||||
} |
} |
||||||
}, |
}, |
||||||
}, |
}, |
||||||
}, |
}, |
||||||
data: { |
data: { |
||||||
imgUrl: '', |
imgUrl: "", |
||||||
width: '', |
width: "", |
||||||
height: '', |
height: "", |
||||||
imgheight: '', |
imgheight: "", |
||||||
bgImg: '', |
bgImg: "", |
||||||
elementsMp: [], |
elementsMp: [], |
||||||
}, |
}, |
||||||
lifetimes: { |
lifetimes: { |
||||||
attached() {}, |
attached() {}, |
||||||
}, |
}, |
||||||
methods: { |
methods: { |
||||||
paramsFormat(params) { |
paramsFormat(params) { |
||||||
params.elements.forEach((item) => { |
params.elements.forEach((item) => { |
||||||
if (item.type === 0) { |
if (item.type === 0) { |
||||||
// 图片居中
|
// 图片居中
|
||||||
if (item.halign === 'center') { |
if (item.halign === "center") { |
||||||
item.left = (params.body.width - item.width) / 2 |
item.left = (params.body.width - item.width) / 2; |
||||||
} |
} |
||||||
} |
} else { |
||||||
else { |
item.text = item.text.replace(/[\r\n]/g, ""); |
||||||
item.text = item.text.replace(/[\r\n]/g, '') |
// 文字居中(使文字标签宽度等于画布宽度,文字加上居中的className)
|
||||||
// 文字居中(使文字标签宽度等于画布宽度,文字加上居中的className)
|
if (item.halign === "center") { |
||||||
if (item.halign === 'center') { |
item.width = item.width || params.body.width; |
||||||
item.width = item.width || params.body.width |
item.left = (params.body.width - item.width) / 2; |
||||||
item.left = (params.body.width - item.width) / 2 |
} |
||||||
} |
if (item.halign === "right") { |
||||||
if (item.halign === 'right') { |
item.width = params.body.width; |
||||||
item.width = params.body.width |
} |
||||||
} |
if (item.id === "shareText" && item.text.length > 30) { |
||||||
if (item.id === 'shareText' && item.text.length > 30) { |
item.text = item.text.slice(0, 30) + "..."; |
||||||
item.text = `${item.text.slice(0, 30)}...` |
} |
||||||
} |
if (item.id === "note-title" && item.text.length > 28) { |
||||||
if (item.id === 'note-title' && item.text.length > 28) { |
item.text = item.text.slice(0, 28) + "..."; |
||||||
item.text = `${item.text.slice(0, 28)}...` |
} |
||||||
} |
if (item.id === "note-content" && item.text.length > 90) { |
||||||
if (item.id === 'note-content' && item.text.length > 90) { |
item.text = item.text.slice(0, 90) + "..."; |
||||||
item.text = `${item.text.slice(0, 90)}...` |
} |
||||||
} |
let len = params.elements.some((x) => x.id === "note-title" && x.text.length); |
||||||
const len = params.elements.some(x => x.id === 'note-title' && x.text.length) |
if (len && item.id === "note-summary" && item.text.length > 62) { |
||||||
if (len && item.id === 'note-summary' && item.text.length > 62) { |
item.text = item.text.slice(0, 62) + "..."; |
||||||
item.text = `${item.text.slice(0, 62)}...` |
} else if (item.id === "note-summary" && item.text.length > 76) { |
||||||
} |
item.text = item.text.slice(0, 76) + "..."; |
||||||
else if (item.id === 'note-summary' && item.text.length > 76) { |
} |
||||||
item.text = `${item.text.slice(0, 76)}...` |
} |
||||||
} |
}); |
||||||
} |
}, |
||||||
}) |
drawImage1() { |
||||||
}, |
let self = this; |
||||||
drawImage() { |
this.drawImage1 = new Wxml2Canvas({ |
||||||
const self = this |
obj: self, |
||||||
this.drawImage1 = new Wxml2Canvas({ |
width: this.data.width, // 宽, 以iphone6为基准,传具体数值,其它机型自动适配
|
||||||
obj: self, |
height: this.data.height, // 高
|
||||||
width: this.data.width, // 宽, 以iphone6为基准,传具体数值,其它机型自动适配
|
element: "canvas1", |
||||||
height: this.data.height, // 高
|
background: "transparent", |
||||||
element: 'canvas1', |
progress(percent) {}, |
||||||
background: 'transparent', |
finish(url) { |
||||||
progress(percent) {}, |
self.setData({ |
||||||
finish(url) { |
imgUrl: url, |
||||||
self.setData({ |
}); |
||||||
imgUrl: url, |
self.triggerEvent("finish", url); |
||||||
}) |
}, |
||||||
self.triggerEvent('finish', url) |
error(res) {}, |
||||||
}, |
}); |
||||||
error(res) {}, |
|
||||||
}) |
let data = { |
||||||
|
list: [ |
||||||
const data = { |
{ |
||||||
list: [ |
type: "wxml", |
||||||
{ |
class: "#canvas-bill-body-mp .draw_canvas", |
||||||
type: 'wxml', |
limit: "#canvas-bill-body-mp", |
||||||
class: '#canvas-bill-body-mp .draw_canvas', |
x: 0, |
||||||
limit: '#canvas-bill-body-mp', |
y: 0, |
||||||
x: 0, |
}, |
||||||
y: 0, |
], |
||||||
}, |
}; |
||||||
], |
|
||||||
} |
this.drawImage1.draw(data); |
||||||
|
}, |
||||||
this.drawImage1.draw(data) |
}, |
||||||
}, |
}); |
||||||
}, |
|
||||||
}) |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@ |
|||||||
{ |
{ |
||||||
"component": true, |
"component": true, |
||||||
"usingComponents": {} |
"usingComponents": {} |
||||||
} |
} |
||||||
@ -1,8 +0,0 @@ |
|||||||
{ |
|
||||||
"component": true, |
|
||||||
"usingComponents": { |
|
||||||
"van-button": "@vant/weapp/button/index", |
|
||||||
"van-icon": "@vant/weapp/icon/index", |
|
||||||
"van-toast": "@vant/weapp/toast/index" |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,7 +0,0 @@ |
|||||||
.merge-canvas { |
|
||||||
position: fixed; |
|
||||||
left: -9999px; |
|
||||||
top: -9999px; |
|
||||||
width: 1px; |
|
||||||
height: 1px; |
|
||||||
} |
|
||||||
@ -1,200 +0,0 @@ |
|||||||
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) |
|
||||||
const fontScale = 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 |
|
||||||
|
|
||||||
// 预计算每张图片的 Y 偏移,确保顺序正确
|
|
||||||
const yPositions: number[] = [] |
|
||||||
scaledHeights.forEach((h, i) => { |
|
||||||
yPositions.push(currentY) |
|
||||||
currentY += h |
|
||||||
}) |
|
||||||
|
|
||||||
loadedCount = 0 |
|
||||||
currentY = 0 |
|
||||||
|
|
||||||
imageInfos.forEach((info, index) => { |
|
||||||
const img = canvas.createImage() |
|
||||||
img.src = info.path |
|
||||||
img.onload = () => { |
|
||||||
ctx.drawImage(img, 0, yPositions[index], canvasWidth, scaledHeights[index]) |
|
||||||
|
|
||||||
const timeText = imageList[index].time || this.formatTime(new Date()) |
|
||||||
const padding = 20 * fontScale |
|
||||||
const fontSize = Math.round(28 * fontScale) |
|
||||||
const textY = yPositions[index] + 40 * fontScale |
|
||||||
|
|
||||||
ctx.font = `bold ${fontSize}px sans-serif` |
|
||||||
ctx.textBaseline = 'middle' |
|
||||||
const textMetrics = ctx.measureText(timeText) |
|
||||||
const bgPaddingH = 16 * fontScale |
|
||||||
const bgPaddingV = 10 * fontScale |
|
||||||
const bgH = fontSize + bgPaddingV * 2 |
|
||||||
const bgX = padding - bgPaddingH |
|
||||||
const bgY = textY - bgH / 2 |
|
||||||
const bgW = textMetrics.width + bgPaddingH * 2 |
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)' |
|
||||||
ctx.fillRect(bgX, bgY, bgW, bgH) |
|
||||||
|
|
||||||
ctx.fillStyle = '#ffffff' |
|
||||||
ctx.textAlign = 'left' |
|
||||||
ctx.fillText(timeText, padding, textY) |
|
||||||
|
|
||||||
if (index === imageInfos.length - 1) { |
|
||||||
const lastImageBottom = yPositions[index] + scaledHeights[index] |
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)' |
|
||||||
ctx.font = `${Math.round(24 * fontScale)}px sans-serif` |
|
||||||
ctx.textAlign = 'right' |
|
||||||
ctx.fillText('由-TED关爱小助手-小程序生成', canvasWidth - 20 * fontScale, lastImageBottom - 20 * fontScale) |
|
||||||
} |
|
||||||
|
|
||||||
loadedCount++ |
|
||||||
|
|
||||||
if (loadedCount === imageInfos.length) { |
|
||||||
wx.canvasToTempFilePath({ |
|
||||||
canvas, |
|
||||||
success: (result) => { |
|
||||||
resolve(result.tempFilePath) |
|
||||||
}, |
|
||||||
fail: reject, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
img.onerror = () => { |
|
||||||
reject(new Error(`Failed to load image: ${info.path}`)) |
|
||||||
} |
|
||||||
}) |
|
||||||
}) |
|
||||||
.catch(reject) |
|
||||||
}) |
|
||||||
}) |
|
||||||
}, |
|
||||||
|
|
||||||
getImageInfo(src: string): Promise<WechatMiniprogram.GetImageInfoSuccessCallbackResult> { |
|
||||||
return new Promise((resolve, reject) => { |
|
||||||
wx.getImageInfo({ |
|
||||||
src, |
|
||||||
success: resolve, |
|
||||||
fail: reject, |
|
||||||
}) |
|
||||||
}) |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
@ -1 +0,0 @@ |
|||||||
<canvas type="2d" id="mergeCanvas-{{id}}" class="merge-canvas"></canvas> |
|
||||||
@ -1,7 +0,0 @@ |
|||||||
{ |
|
||||||
"component": true, |
|
||||||
"usingComponents": { |
|
||||||
"van-icon": "@vant/weapp/icon/index", |
|
||||||
"navbar": "/components/navbar/index" |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,157 +0,0 @@ |
|||||||
.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; |
|
||||||
position: relative; |
|
||||||
|
|
||||||
.order { |
|
||||||
position: absolute; |
|
||||||
left: 50%; |
|
||||||
transform: translate(-50%, 0 ); |
|
||||||
padding: 18rpx 32rpx; |
|
||||||
font-size: 40rpx; |
|
||||||
font-weight: bold; |
|
||||||
text-align: center; |
|
||||||
color: #211d2e; |
|
||||||
border-radius: 94rpx; |
|
||||||
display: inline-flex; |
|
||||||
align-items: baseline; |
|
||||||
background-color: #fff; |
|
||||||
z-index: 10; |
|
||||||
white-space: nowrap; |
|
||||||
|
|
||||||
.num { |
|
||||||
margin-left: 20rpx; |
|
||||||
} |
|
||||||
|
|
||||||
.m-num { |
|
||||||
font-size: 28rpx; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
.preview-image { |
|
||||||
display: block; |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
// 左右切换按钮 |
|
||||||
.nav-btn { |
|
||||||
position: absolute; |
|
||||||
top: 50%; |
|
||||||
transform: translateY(-50%); |
|
||||||
width: 72rpx; |
|
||||||
height: 72rpx; |
|
||||||
border-radius: 50%; |
|
||||||
background: rgba(0, 0, 0, 0.4); |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
z-index: 10; |
|
||||||
|
|
||||||
&:active:not(.disabled) { |
|
||||||
background: rgba(0, 0, 0, 0.6); |
|
||||||
} |
|
||||||
|
|
||||||
&.disabled { |
|
||||||
opacity: 0.3; |
|
||||||
} |
|
||||||
|
|
||||||
&.nav-prev { |
|
||||||
left: 24rpx; |
|
||||||
} |
|
||||||
|
|
||||||
&.nav-next { |
|
||||||
right: 24rpx; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 页码指示器 |
|
||||||
.indicator { |
|
||||||
position: absolute; |
|
||||||
left: 0; |
|
||||||
right: 0; |
|
||||||
text-align: center; |
|
||||||
color: #fff; |
|
||||||
font-size: 28rpx; |
|
||||||
z-index: 10; |
|
||||||
bottom: calc(168rpx + env(safe-area-inset-bottom)); |
|
||||||
|
|
||||||
.current { |
|
||||||
font-weight: 600; |
|
||||||
} |
|
||||||
|
|
||||||
.separator { |
|
||||||
margin: 0 8rpx; |
|
||||||
opacity: 0.6; |
|
||||||
} |
|
||||||
|
|
||||||
.total { |
|
||||||
opacity: 0.6; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 底部操作栏 |
|
||||||
.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; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,157 +0,0 @@ |
|||||||
Component({ |
|
||||||
properties: { |
|
||||||
visible: { |
|
||||||
type: Boolean, |
|
||||||
value: false, |
|
||||||
observer(newVal) { |
|
||||||
this.setData({ visible: newVal }) |
|
||||||
}, |
|
||||||
}, |
|
||||||
src: { |
|
||||||
type: String, |
|
||||||
value: '', |
|
||||||
}, |
|
||||||
images: { |
|
||||||
type: Array, |
|
||||||
value: [], |
|
||||||
}, |
|
||||||
currentIndex: { |
|
||||||
type: Number, |
|
||||||
value: 0, |
|
||||||
}, |
|
||||||
photoLabels: { |
|
||||||
type: Array, |
|
||||||
value: [] as { name: string; index: number; total: number }[], |
|
||||||
}, |
|
||||||
showActions: { |
|
||||||
type: Boolean, |
|
||||||
value: true, |
|
||||||
}, |
|
||||||
}, |
|
||||||
|
|
||||||
data: { |
|
||||||
visible: false, |
|
||||||
src: '', |
|
||||||
images: [] as string[], |
|
||||||
currentIndex: 0, |
|
||||||
navHeight: wx.getSystemInfoSync().statusBarHeight + 44, |
|
||||||
startX: 0, |
|
||||||
startY: 0, |
|
||||||
}, |
|
||||||
|
|
||||||
observers: { |
|
||||||
'currentIndex, images': function (currentIndex: number, images: string[]) { |
|
||||||
if (images && images.length > 0 && images[currentIndex]) { |
|
||||||
this.setData({ |
|
||||||
src: images[currentIndex], |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
}, |
|
||||||
|
|
||||||
methods: { |
|
||||||
// 返回/关闭
|
|
||||||
handleBack() { |
|
||||||
this.triggerEvent('close') |
|
||||||
}, |
|
||||||
|
|
||||||
// 更多操作
|
|
||||||
handleMore() { |
|
||||||
this.triggerEvent('more') |
|
||||||
}, |
|
||||||
|
|
||||||
// 预览/查看
|
|
||||||
handlePreview(src) { |
|
||||||
if (src) { |
|
||||||
this.setData({ |
|
||||||
visible: true, |
|
||||||
src, |
|
||||||
}) |
|
||||||
} |
|
||||||
}, |
|
||||||
handleHidePreview() { |
|
||||||
this.setData({ |
|
||||||
visible: false, |
|
||||||
src: '', |
|
||||||
}) |
|
||||||
}, |
|
||||||
|
|
||||||
// 上一张
|
|
||||||
handlePrev() { |
|
||||||
const { currentIndex, images } = this.data |
|
||||||
if (currentIndex > 0) { |
|
||||||
const newIndex = currentIndex - 1 |
|
||||||
this.setData({ |
|
||||||
currentIndex: newIndex, |
|
||||||
}) |
|
||||||
this.triggerEvent('change', { index: newIndex }) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
// 下一张
|
|
||||||
handleNext() { |
|
||||||
const { currentIndex, images } = this.data |
|
||||||
if (currentIndex < images.length - 1) { |
|
||||||
const newIndex = currentIndex + 1 |
|
||||||
this.setData({ |
|
||||||
currentIndex: newIndex, |
|
||||||
}) |
|
||||||
this.triggerEvent('change', { index: newIndex }) |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
// 滑动手势 - 开始
|
|
||||||
handleTouchStart(e: any) { |
|
||||||
this.setData({ |
|
||||||
startX: e.touches[0].clientX, |
|
||||||
startY: e.touches[0].clientY, |
|
||||||
}) |
|
||||||
}, |
|
||||||
|
|
||||||
// 滑动手势 - 结束
|
|
||||||
handleTouchEnd(e: any) { |
|
||||||
const { startX, startY, currentIndex, images } = this.data |
|
||||||
const endX = e.changedTouches[0].clientX |
|
||||||
const endY = e.changedTouches[0].clientY |
|
||||||
const diffX = endX - startX |
|
||||||
const diffY = endY - startY |
|
||||||
|
|
||||||
// 水平滑动距离大于50且大于垂直滑动距离时才触发切换
|
|
||||||
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY)) { |
|
||||||
if (diffX > 0) { |
|
||||||
// 右滑 - 上一张
|
|
||||||
if (currentIndex > 0) { |
|
||||||
this.handlePrev() |
|
||||||
} |
|
||||||
} else { |
|
||||||
// 左滑 - 下一张
|
|
||||||
if (currentIndex < images.length - 1) { |
|
||||||
this.handleNext() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
// 删除
|
|
||||||
handleDelete() { |
|
||||||
wx.showModal({ |
|
||||||
title: '提示', |
|
||||||
content: '确定要删除这张照片吗?', |
|
||||||
confirmColor: '#8c75d0', |
|
||||||
cancelColor: '#141515', |
|
||||||
success: (res) => { |
|
||||||
if (res.confirm) { |
|
||||||
this.handleHidePreview() |
|
||||||
this.triggerEvent('delete') |
|
||||||
} |
|
||||||
}, |
|
||||||
}) |
|
||||||
}, |
|
||||||
|
|
||||||
// 重拍
|
|
||||||
handleRetake() { |
|
||||||
this.handleHidePreview() |
|
||||||
this.triggerEvent('retake') |
|
||||||
}, |
|
||||||
}, |
|
||||||
}) |
|
||||||
@ -1,31 +0,0 @@ |
|||||||
<view class="preview-container {{visible ? 'show' : ''}}"> |
|
||||||
<navbar fixed title="" custom-style="background:transparent" bind:click-left="handleBack"> |
|
||||||
<van-icon name="arrow-left" slot="left" size="18px" color="#fff" bind:tap="handleBack" /> |
|
||||||
</navbar> |
|
||||||
|
|
||||||
<view class="image-wrapper" style="padding-top:{{navHeight}}px;" bindtouchstart="handleTouchStart" bindtouchend="handleTouchEnd"> |
|
||||||
<view class="order" wx:if="{{photoLabels[currentIndex]}}" style="top:{{navHeight * 0.5}}px;"> |
|
||||||
{{photoLabels[currentIndex].name}} |
|
||||||
<!-- <view class="num">{{photoLabels[currentIndex].index}}</view> --> |
|
||||||
<!-- <view class="m-num">/{{photoLabels[currentIndex].total}}</view> --> |
|
||||||
</view> |
|
||||||
<view class="nav-btn nav-prev {{currentIndex <= 0 ? 'disabled' : ''}}" wx:if="{{images.length > 1}}" bindtap="handlePrev"> |
|
||||||
<van-icon name="arrow-left" size="20px" color="{{currentIndex <= 0 ? '#666' : '#fff'}}" /> |
|
||||||
</view> |
|
||||||
<image class="preview-image" src="{{src}}" mode="aspectFit" /> |
|
||||||
<view class="nav-btn nav-next {{currentIndex >= images.length - 1 ? 'disabled' : ''}}" wx:if="{{images.length > 1}}" bindtap="handleNext"> |
|
||||||
<van-icon name="arrow" size="20px" color="{{currentIndex >= images.length - 1 ? '#666' : '#fff'}}" /> |
|
||||||
</view> |
|
||||||
</view> |
|
||||||
|
|
||||||
<view class="indicator" wx:if="{{images.length > 1}}"> |
|
||||||
<text class="current">{{currentIndex + 1}}</text> |
|
||||||
<text class="separator">/</text> |
|
||||||
<text class="total">{{images.length}}</text> |
|
||||||
</view> |
|
||||||
|
|
||||||
<view class="action-bar" wx:if="{{showActions}}"> |
|
||||||
<view class="btn btn-delete" bindtap="handleDelete">删除</view> |
|
||||||
<view class="btn btn-retake" bindtap="handleRetake">重拍</view> |
|
||||||
</view> |
|
||||||
</view> |
|
||||||
@ -1,6 +1,6 @@ |
|||||||
{ |
{ |
||||||
|
"component": true, |
||||||
"usingComponents": { |
"usingComponents": { |
||||||
"navbar": "/components/navbar/index", |
|
||||||
"van-popup": "@vant/weapp/popup/index" |
"van-popup": "@vant/weapp/popup/index" |
||||||
} |
} |
||||||
} |
} |
||||||
@ -0,0 +1,95 @@ |
|||||||
|
.from { |
||||||
|
padding: 48rpx 40rpx; |
||||||
|
width: 650rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
background: linear-gradient(349deg, #ffffff 0%, #e2f1f4 100%); |
||||||
|
border-radius: 24rpx 24rpx 24rpx 24rpx; |
||||||
|
border: 2rpx solid #ffffff; |
||||||
|
.title { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #283031; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
.date { |
||||||
|
margin-top: 24rpx; |
||||||
|
padding: 14rpx 32rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
background-color: #f2f4f5; |
||||||
|
border-radius: 16rpx; |
||||||
|
|
||||||
|
.conetent { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #283031; |
||||||
|
} |
||||||
|
.tril { |
||||||
|
width: 0; |
||||||
|
height: 0; |
||||||
|
border-style: solid; |
||||||
|
border-width: 10rpx 10rpx 0 10rpx; |
||||||
|
border-color: #aeb3b4 transparent transparent transparent; |
||||||
|
} |
||||||
|
} |
||||||
|
.select-title { |
||||||
|
margin-top: 48rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
color: #283031; |
||||||
|
font-weight: bold; |
||||||
|
.sub { |
||||||
|
font-weight: normal; |
||||||
|
} |
||||||
|
} |
||||||
|
.list { |
||||||
|
margin-top: 26rpx; |
||||||
|
max-height: 55vh; |
||||||
|
overflow-y: auto; |
||||||
|
&::-webkit-scrollbar{ |
||||||
|
display: none; |
||||||
|
} |
||||||
|
.item { |
||||||
|
margin-bottom: 16rpx; |
||||||
|
padding: 14rpx 32rpx; |
||||||
|
font-size: 32rpx; |
||||||
|
color: #283031; |
||||||
|
line-height: 48rpx; |
||||||
|
background-color: #f2f4f5; |
||||||
|
border: 1px solid #f2f4f5; |
||||||
|
border-radius: 16rpx; |
||||||
|
&.active { |
||||||
|
border-color: #67baca; |
||||||
|
background-color: #e7f5f8; |
||||||
|
color: #67baca; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.footer { |
||||||
|
margin-top: 32rpx; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
gap: 26rpx; |
||||||
|
text-align: center; |
||||||
|
.cancel { |
||||||
|
flex: 1; |
||||||
|
height: 80rpx; |
||||||
|
font-size: 36rpx; |
||||||
|
color: #67BACA; |
||||||
|
line-height: 80rpx; |
||||||
|
background: #ffffff; |
||||||
|
border-radius: 98rpx 98rpx 98rpx 98rpx; |
||||||
|
border: 2rpx solid #67baca; |
||||||
|
} |
||||||
|
|
||||||
|
.submit { |
||||||
|
flex: 1; |
||||||
|
height: 80rpx; |
||||||
|
font-size: 36rpx; |
||||||
|
color: #FFFFFF; |
||||||
|
line-height: 80rpx; |
||||||
|
background: #67baca; |
||||||
|
border-radius: 98rpx 98rpx 98rpx 98rpx; |
||||||
|
border: 2rpx solid #67baca; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
import dayjs from 'dayjs' |
||||||
|
|
||||||
|
const _app = getApp<IAppOption>() |
||||||
|
|
||||||
|
// pages/story/a.ts
|
||||||
|
Component({ |
||||||
|
/** |
||||||
|
* 组件的属性列表 |
||||||
|
*/ |
||||||
|
properties: { |
||||||
|
show: { |
||||||
|
type: Boolean, |
||||||
|
value: false, |
||||||
|
}, |
||||||
|
params: { |
||||||
|
type: Object, |
||||||
|
value: undefined, |
||||||
|
}, |
||||||
|
}, |
||||||
|
|
||||||
|
observers: { |
||||||
|
show() { |
||||||
|
if (this.data.params) { |
||||||
|
this.setData({ |
||||||
|
...this.data.params, |
||||||
|
}) |
||||||
|
this.handleDateChange() |
||||||
|
} else { |
||||||
|
this.setData({ |
||||||
|
visitDateName: '', |
||||||
|
visitDate: '', |
||||||
|
hormone: 2, |
||||||
|
traditionalInhibitor: 2, |
||||||
|
gammaGlobulin: 2, |
||||||
|
plasmaExchange: 2, |
||||||
|
bCellInhibitor: 2, |
||||||
|
fcRnAntagonists: 2, |
||||||
|
c5ComplementInhibitor: 2, |
||||||
|
chineseMedicine: 2, |
||||||
|
other: 2, |
||||||
|
recordId: '', |
||||||
|
}) |
||||||
|
} |
||||||
|
}, |
||||||
|
}, |
||||||
|
data: { |
||||||
|
currentDate: dayjs().format('YYYY-MM-DD'), |
||||||
|
|
||||||
|
visitDateName: '', |
||||||
|
visitDate: '', |
||||||
|
hormone: 2, |
||||||
|
traditionalInhibitor: 2, |
||||||
|
gammaGlobulin: 2, |
||||||
|
plasmaExchange: 2, |
||||||
|
bCellInhibitor: 2, |
||||||
|
fcRnAntagonists: 2, |
||||||
|
c5ComplementInhibitor: 2, |
||||||
|
chineseMedicine: 2, |
||||||
|
other: 2, |
||||||
|
recordId: '', |
||||||
|
}, |
||||||
|
|
||||||
|
methods: { |
||||||
|
handleDateChange() { |
||||||
|
this.setData({ |
||||||
|
visitDateName: dayjs(this.data.visitDate).format('YYYY年MM月DD日'), |
||||||
|
}) |
||||||
|
}, |
||||||
|
handleSelect(e) { |
||||||
|
const { name } = e.currentTarget.dataset |
||||||
|
const value = this.data[name] |
||||||
|
this.setData({ |
||||||
|
[name]: value === 2 ? 1 : 2, |
||||||
|
}) |
||||||
|
}, |
||||||
|
submit() { |
||||||
|
const { visitDate, recordId } = this.data |
||||||
|
const params = { |
||||||
|
visitDate, |
||||||
|
recordId, |
||||||
|
} |
||||||
|
if(!visitDate){ |
||||||
|
wx.showToast({ |
||||||
|
title: '请选择复诊日期', |
||||||
|
icon: 'none', |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
const selectKeys = [ |
||||||
|
'hormone', |
||||||
|
'traditionalInhibitor', |
||||||
|
'gammaGlobulin', |
||||||
|
'plasmaExchange', |
||||||
|
'bCellInhibitor', |
||||||
|
'fcRnAntagonists', |
||||||
|
'c5ComplementInhibitor', |
||||||
|
'chineseMedicine', |
||||||
|
'other', |
||||||
|
] |
||||||
|
selectKeys.forEach((item) => { |
||||||
|
params[item] = this.data[item] |
||||||
|
}) |
||||||
|
const onlySelect = selectKeys.some((item) => { |
||||||
|
return this.data[item] === 1 |
||||||
|
}) |
||||||
|
if (!onlySelect) { |
||||||
|
wx.showToast({ |
||||||
|
title: '请至少选择一种复诊后的方案', |
||||||
|
icon: 'none', |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
wx.ajax({ |
||||||
|
method: 'POST', |
||||||
|
url: '?r=xd/re-visit/save-record', |
||||||
|
data: params, |
||||||
|
}).then(() => { |
||||||
|
if (recordId) { |
||||||
|
wx.showToast({ |
||||||
|
icon: 'none', |
||||||
|
title: '编辑成功', |
||||||
|
}) |
||||||
|
} else { |
||||||
|
wx.showToast({ |
||||||
|
icon: 'none', |
||||||
|
title: '添加成功', |
||||||
|
}) |
||||||
|
} |
||||||
|
this.handleCancel() |
||||||
|
this.triggerEvent('refresh', params) |
||||||
|
}) |
||||||
|
}, |
||||||
|
handleCancel() { |
||||||
|
this.setData({ |
||||||
|
show: false, |
||||||
|
}) |
||||||
|
}, |
||||||
|
}, |
||||||
|
}) |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
<van-popup custom-style="background: transparent;" round z-index="{{100000}}" show="{{ show }}"> |
||||||
|
<view class="from"> |
||||||
|
<view class="title">您上一次复诊时间?</view> |
||||||
|
<picker mode="date" model:value="{{visitDate}}" end="{{currentDate}}" bind:change="handleDateChange"> |
||||||
|
<view class="date"> |
||||||
|
<view class="content">{{visitDateName||'请选择'}}</view> |
||||||
|
<view class="tril"></view> |
||||||
|
</view> |
||||||
|
</picker> |
||||||
|
<view class="select-title"> |
||||||
|
您复诊后的方案是? |
||||||
|
<text class="sub">(多选)</text> |
||||||
|
</view> |
||||||
|
<view class="list"> |
||||||
|
<view bind:tap="handleSelect" data-name="hormone" class="item {{hormone===1 && 'active'}}">1.激素</view> |
||||||
|
<view |
||||||
|
bind:tap="handleSelect" |
||||||
|
data-name="traditionalInhibitor" |
||||||
|
class="item {{traditionalInhibitor===1 && 'active'}}" |
||||||
|
> |
||||||
|
2.传统免疫抑制剂(如他克莫司、吗 替麦考酚酯等) |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="gammaGlobulin" class="item {{gammaGlobulin===1 && 'active'}}"> |
||||||
|
3.静脉输注丙种球蛋白 |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="plasmaExchange" class="item {{plasmaExchange===1 && 'active'}}"> |
||||||
|
4.血浆置换 |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="bCellInhibitor" class="item {{bCellInhibitor===1 && 'active'}}"> |
||||||
|
5.B细胞抑制剂(如:利妥昔单抗、泰 它西普、伊奈利珠单抗) |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="fcRnAntagonists" class="item {{fcRnAntagonists===1 && 'active'}}"> |
||||||
|
6.FcRn拮抗剂(如:艾加莫德) |
||||||
|
</view> |
||||||
|
<view |
||||||
|
bind:tap="handleSelect" |
||||||
|
data-name="c5ComplementInhibitor" |
||||||
|
class="item {{c5ComplementInhibitor===1 && 'active'}}" |
||||||
|
> |
||||||
|
7.C5补体抑制剂(如:依库珠单抗) |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="chineseMedicine" class="item {{chineseMedicine===1 && 'active'}}"> |
||||||
|
8.中药或中成药 |
||||||
|
</view> |
||||||
|
<view bind:tap="handleSelect" data-name="other" class="item {{other===1 && 'active'}}">9.其他</view> |
||||||
|
</view> |
||||||
|
<view class="footer"> |
||||||
|
<view class="cancel" bind:tap="handleCancel">取消</view> |
||||||
|
<view class="submit" bind:tap="submit">确定</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</van-popup> |
||||||
@ -1,12 +0,0 @@ |
|||||||
export default { |
|
||||||
wxf9ce8010f1ad24aa: { |
|
||||||
url: 'https://m.xd.hbraas.com', |
|
||||||
upFileUrl: 'https://m.xd.hbraas.com/', |
|
||||||
imageUrl: 'https://m.xd.hbraas.com/xd/', |
|
||||||
}, |
|
||||||
wx71ac9c27c3c3e3f4: { |
|
||||||
url: 'https://m.xd.hbsaas.com', |
|
||||||
upFileUrl: 'https://m.xd.hbsaas.com/', |
|
||||||
imageUrl: 'https://m.xd.hbsaas.com/api/xd/', |
|
||||||
}, |
|
||||||
} |
|
||||||
@ -1,3 +1,3 @@ |
|||||||
.page { |
.page{ |
||||||
padding: 0 40rpx; |
padding: 0 40rpx; |
||||||
} |
} |
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,3 +1,3 @@ |
|||||||
.mp-html { |
.mp-html{ |
||||||
padding: 20rpx 40rpx; |
padding: 20rpx 40rpx; |
||||||
} |
} |
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,6 @@ |
|||||||
|
{ |
||||||
|
"navigationBarTitleText": "确认订单", |
||||||
|
"usingComponents": { |
||||||
|
"van-icon": "@vant/weapp/icon/index" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,184 @@ |
|||||||
|
.page { |
||||||
|
padding: 34rpx 40rpx; |
||||||
|
.site { |
||||||
|
padding: 40rpx 32rpx; |
||||||
|
background: #ffffff; |
||||||
|
box-shadow: 0rpx 4rpx 20rpx 0rpx rgba(0, 0, 0, 0.05); |
||||||
|
border-radius: 24rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
.wrap { |
||||||
|
.title { |
||||||
|
.label { |
||||||
|
width: 72rpx; |
||||||
|
height: 36rpx; |
||||||
|
border: 1rpx solid #e04775; |
||||||
|
font-size: 24rpx; |
||||||
|
color: #e04775; |
||||||
|
text-align: center; |
||||||
|
border-radius: 10rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
.name { |
||||||
|
margin-top: -42rpx; |
||||||
|
text-indent: 80rpx; |
||||||
|
font-size: 36rpx; |
||||||
|
line-height: 46rpx; |
||||||
|
color: #3f3f3f; |
||||||
|
font-weight: bold; |
||||||
|
min-width: 0; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
display: -webkit-box; |
||||||
|
-webkit-line-clamp: 2; |
||||||
|
-webkit-box-orient: vertical; |
||||||
|
&.no-indent { |
||||||
|
margin-top: 0; |
||||||
|
text-indent: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.content { |
||||||
|
margin-top: 0; |
||||||
|
margin-top: 12rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #b6b7ba; |
||||||
|
} |
||||||
|
} |
||||||
|
.more { |
||||||
|
flex-shrink: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
.shop { |
||||||
|
margin-top: 16px; |
||||||
|
padding: 40rpx 32rpx; |
||||||
|
background: #ffffff; |
||||||
|
box-shadow: 0rpx 4rpx 20rpx 0rpx rgba(0, 0, 0, 0.05); |
||||||
|
border-radius: 24rpx; |
||||||
|
.shop-header { |
||||||
|
padding-bottom: 10px; |
||||||
|
display: flex; |
||||||
|
.shop-img { |
||||||
|
flex-shrink: 0; |
||||||
|
width: 204rpx; |
||||||
|
height: 204rpx; |
||||||
|
border-radius: 24rpx; |
||||||
|
} |
||||||
|
.wrap { |
||||||
|
padding-top: 8rpx; |
||||||
|
flex: 1; |
||||||
|
padding-left: 24rpx; |
||||||
|
.name { |
||||||
|
font-size: 32rpx; |
||||||
|
font-weight: bold; |
||||||
|
color: #3f3f3f; |
||||||
|
line-height: 44rpx; |
||||||
|
min-width: 0; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
display: -webkit-box; |
||||||
|
-webkit-line-clamp: 2; |
||||||
|
-webkit-box-orient: vertical; |
||||||
|
} |
||||||
|
.specification { |
||||||
|
margin-top: 8rpx; |
||||||
|
font-size: 28rpx; |
||||||
|
color: #b6b7ba; |
||||||
|
} |
||||||
|
.price { |
||||||
|
margin-top: 14rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
.num { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #3f3f3f; |
||||||
|
} |
||||||
|
.sub { |
||||||
|
font-size: 22rpx; |
||||||
|
} |
||||||
|
.val { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #b6b7ba; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.row { |
||||||
|
margin-top: 32rpx; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
.label { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #3f3f3f; |
||||||
|
} |
||||||
|
.content { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #3f3f3f; |
||||||
|
&.yellow { |
||||||
|
color: #e04775; |
||||||
|
} |
||||||
|
.sub { |
||||||
|
font-size: 22rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
.remark { |
||||||
|
margin-top: 16px; |
||||||
|
padding: 40rpx 32rpx; |
||||||
|
background: #ffffff; |
||||||
|
box-shadow: 0rpx 4rpx 20rpx 0rpx rgba(0, 0, 0, 0.05); |
||||||
|
border-radius: 24rpx; |
||||||
|
.title { |
||||||
|
font-size: 32rpx; |
||||||
|
color: #3f3f3f; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
.textarea { |
||||||
|
margin-top: 10rpx; |
||||||
|
padding: 24rpx 32rpx; |
||||||
|
background-color: #fafafa; |
||||||
|
min-height: 196rpx; |
||||||
|
border-radius: 16rpx; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
} |
||||||
|
.footer { |
||||||
|
padding: 24rpx 48rpx 48rpx; |
||||||
|
position: fixed; |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
width: 100vw; |
||||||
|
box-sizing: border-box; |
||||||
|
display: flex; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: center; |
||||||
|
height: 168rpx; |
||||||
|
background: #ffffff; |
||||||
|
box-shadow: 0rpx 8rpx 20rpx 0rpx rgba(0, 0, 0, 0.26); |
||||||
|
.price { |
||||||
|
font-size: 28rpx; |
||||||
|
color: #b6b7ba; |
||||||
|
.num { |
||||||
|
font-size: 40rpx; |
||||||
|
color: #e04775; |
||||||
|
} |
||||||
|
.sub { |
||||||
|
font-size: 24rpx; |
||||||
|
} |
||||||
|
} |
||||||
|
.submit { |
||||||
|
width: 260rpx; |
||||||
|
height: 96rpx; |
||||||
|
background: #e04775; |
||||||
|
border-radius: 48rpx; |
||||||
|
text-align: center; |
||||||
|
line-height: 96rpx; |
||||||
|
color: #fff; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
const app = getApp<IAppOption>(); |
||||||
|
|
||||||
|
Page({ |
||||||
|
data: { |
||||||
|
id: "", |
||||||
|
detail: {}, |
||||||
|
select: false, |
||||||
|
addressDetail: {} as any, |
||||||
|
remark: "", |
||||||
|
}, |
||||||
|
onLoad(options) { |
||||||
|
this.setData({ |
||||||
|
id: options.id, |
||||||
|
}); |
||||||
|
}, |
||||||
|
onShow() { |
||||||
|
app.waitLogin().then(() => { |
||||||
|
this.getDetail(); |
||||||
|
if (!this.data.select) { |
||||||
|
this.getDefaultAddress(); |
||||||
|
} else { |
||||||
|
this.setData({ |
||||||
|
select: false, |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
getDefaultAddress() { |
||||||
|
wx.ajax({ |
||||||
|
method: "GET", |
||||||
|
url: "?r=zd/patient-address/get-default-address", |
||||||
|
data: {}, |
||||||
|
}).then((res) => { |
||||||
|
this.setData({ |
||||||
|
addressDetail: res, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
getDetail() { |
||||||
|
wx.ajax({ |
||||||
|
method: "GET", |
||||||
|
url: "?r=zd/gift-order/get-order-detail", |
||||||
|
data: { |
||||||
|
orderId: this.data.id, |
||||||
|
}, |
||||||
|
}).then((res) => { |
||||||
|
this.setData({ |
||||||
|
detail: res, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
handleSite() { |
||||||
|
if (this.data.addressDetail) { |
||||||
|
wx.navigateTo({ |
||||||
|
url: "/gift/pages/siteList/index", |
||||||
|
}); |
||||||
|
} else { |
||||||
|
wx.navigateTo({ |
||||||
|
url: "/gift/pages/siteEdit/index", |
||||||
|
}); |
||||||
|
} |
||||||
|
}, |
||||||
|
handleSubmit() { |
||||||
|
if (!this.data.addressDetail?.addressId) { |
||||||
|
wx.showToast({ |
||||||
|
icon: "none", |
||||||
|
title: "请选择地址", |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
wx.ajax({ |
||||||
|
method: "POST", |
||||||
|
url: "?r=zd/gift-order/confirm-order", |
||||||
|
data: { |
||||||
|
orderId: this.data.id, |
||||||
|
addressId: this.data.addressDetail.addressId, |
||||||
|
remark: this.data.remark, |
||||||
|
}, |
||||||
|
loading: true, |
||||||
|
}).then(() => { |
||||||
|
wx.reLaunch({ |
||||||
|
url: `/gift/pages/orderEnd/index?id=${this.data.id}`, |
||||||
|
}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,56 @@ |
|||||||
|
<view class="page"> |
||||||
|
<view class="site" bind:tap="handleSite"> |
||||||
|
<view class="wrap"> |
||||||
|
<block wx:if="{{addressDetail}}"> |
||||||
|
<view class="title"> |
||||||
|
<view class="label" wx:if="{{addressDetail.isDefault==1}}">默认</view> |
||||||
|
<view class="name {{addressDetail.isDefault!=1 && 'no-indent'}}"> |
||||||
|
{{addressDetail.provinceName}}{{addressDetail.cityName}}{{addressDetail.countyName}}{{addressDetail.address}} |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="content">{{addressDetail.receiveUserName}} {{addressDetail.receiveTelephone}}</view> |
||||||
|
</block> |
||||||
|
<view class="title" wx:else>请添加收货地址</view> |
||||||
|
</view> |
||||||
|
<van-icon class="more" name="arrow" /> |
||||||
|
</view> |
||||||
|
<view class="shop"> |
||||||
|
<view class="shop-header"> |
||||||
|
<image class="shop-img" src="{{detail.giftBigImg}}"></image> |
||||||
|
<view class="wrap"> |
||||||
|
<view class="name">{{detail.giftName}}</view> |
||||||
|
<view class="specification" wx:if="{{detail.specName}}">规格:{{detail.specName}}</view> |
||||||
|
<view class="price"> |
||||||
|
<view class="num">{{detail.giftScore}}<text class="sub">能量</text></view> |
||||||
|
<view class="val">x{{detail.orderCount}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="row"> |
||||||
|
<view class="label">礼品总价</view> |
||||||
|
<view class="content yellow">{{detail.orderScore}}<text class="sub">能量</text></view> |
||||||
|
</view> |
||||||
|
<view class="row" > |
||||||
|
<view class="label">配送方式</view> |
||||||
|
<view class="content" style="font-weight:normal">快递配送</view> |
||||||
|
</view> |
||||||
|
<view class="row" > |
||||||
|
<view class="label">商家电话</view> |
||||||
|
<view class="content" style="font-weight:normal">{{detail.serviceTel}}</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="remark"> |
||||||
|
<view class="title">留言</view> |
||||||
|
<textarea class="textarea" model:value="{{remark}}" placeholder="请输入留言" auto-height></textarea> |
||||||
|
</view> |
||||||
|
<view class="footer"> |
||||||
|
<view class="price"> |
||||||
|
共{{detail.orderCount}}件 |
||||||
|
<view> |
||||||
|
合计消耗 |
||||||
|
<text class="num">{{detail.orderScore}}<text class="sub">能量</text></text> |
||||||
|
</view> |
||||||
|
</view> |
||||||
|
<view class="submit" bind:tap="handleSubmit">提交订单</view> |
||||||
|
</view> |
||||||
|
</view> |
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue