You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
420 lines
11 KiB
420 lines
11 KiB
|
1 week ago
|
# 项目规则 - 信达小程序 (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/` 各有一份,分别供子包和主包页面使用。
|