信达小程序
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

# 项目规则 - 信达小程序 (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/` 各有一份,分别供子包和主包页面使用。