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.jsonresolveAlias)- Use
@/utils/requestnot relative paths
User Types & Routing
Login types enforced in app.ts:
0: Not logged in1: 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
imageUrlandTimestampon page load - Auto-handles navbar background on scroll
- Provides default share behavior
Modal Colors (Required)
All wx.showModal must use:
wx.showModal({
confirmColor: '#8c75d0',
cancelColor: '#141515',
})
App Instance (Required)
getApp() should be called at the top of the file, not inside methods:
// ✅ 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 中直接调用方法
错误示例:
<!-- ❌ 错误:WXML 中不能直接调用方法 -->
<view>{{getUploadedCount()}}</view>
<view wx:if="{{hasPhotos()}}">内容</view>
正确做法:
<!-- ✅ 正确:使用数据绑定 -->
<view>{{uploadedCount}}</view>
<view wx:if="{{hasPhotos}}">内容</view>
// 在 TS 中计算好数据
this.setData({
uploadedCount: photos.length,
hasPhotos: photos.length > 0
})
2. 列表渲染时使用 wx:key
<view wx:for="{{list}}" wx:key="id">{{item.name}}</view>
3. 条件渲染
<view wx:if="{{condition}}">显示</view>
<view wx:else>隐藏</view>
4. WXML 表达式限制
WXML 中的双大括号 {{}} 只能使用简单的表达式,不支持以下语法:
❌ 不支持的语法:
<!-- 方法调用 -->
<view>{{arr.indexOf(item) > -1}}</view>
<!-- 复杂表达式 -->
<view>{{obj.method().property}}</view>
<!-- 正则表达式 -->
<view>{{str.match(/regex/)}}</view>
<!-- new 操作符 -->
<view>{{new Date().getTime()}}</view>
✅ 支持的语法:
<!-- 简单属性访问 -->
<view>{{item.name}}</view>
<!-- 三元表达式 -->
<view class="{{item.isActive ? 'active' : ''}}">内容</view>
<!-- 简单比较 -->
<view wx:if="{{count > 0}}">有数据</view>
<!-- 逻辑运算 -->
<view wx:if="{{isLogin && isVip}}">VIP用户</view>
解决方案:
// 在 TS 中预处理数据,将复杂计算转换为简单布尔值
this.setData({
// ❌ 不要在 WXML 中使用 indexOf
// isSelected: selectedDates.indexOf(item.recordId) > -1
// ✅ 在数据中直接提供 isSelected 字段
list: rawList.map(item => ({
...item,
isSelected: selectedDates.includes(item.recordId)
}))
})
Available Commands
# 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 componentsminiprogram-licia: Utility library (available aslicia)dayjs: Date handlingmp-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.jsonpackOptions - 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 tosrc/* - Types:
miniprogram-api-typingsfor WX API
Global types in typings/index.d.ts:
IAppOption: Global app instance interfacepageType: 0 | 1 | 2 for user typeswx.ajax: Extended request method
ESLint & Formatting
- Config:
@antfu/eslint-config(flat config ineslint.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.jsonhas hot reload enabled (compileHotReLoad: true)- Predefined test pages in
project.private.config.jsoncondition list
Common Gotchas
- NPM packages: Must run "Build npm" in WeChat tools after install
- Images: Will 404 if SVN images not checked out to
src/images/ - Subpackages: Patient pages are in
patient/subpackage, not main package - Skyline: Enabled in rendererOptions, some components may behave differently
- Login flow: App automatically calls
startLogin()on launch; pages must wait viaapp.zdWaitLogin()
自定义导航栏规范
统一使用 navbar 组件
所有需要自定义导航的页面统一使用 /components/navbar/index。/components/zd-navBar/navBar 是历史遗留组件,新页面不要使用。
JSON 配置:
{
"navigationStyle": "custom",
"usingComponents": {
"navbar": "/components/navbar/index"
}
}
WXML 用法:
<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 实现:
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 组件:
{
"usingComponents": {
"popup": "/components/popup/index"
}
}
<popup show="{{popupShow}}" type="{{popupType}}" params="{{popupParams}}" bind:ok="handlePopupOk" bind:cancel="handlePopupCancel" />
常用 popup 类型
| 类型 | 场景 | 标题 | 确认按钮 | 取消按钮 |
|---|---|---|---|---|
popup15 |
删除确认 | "确认删除记录?" | 确认删除 | 取消 |
popup16 |
未保存数据退出 | "您的记录还未保存" | 保存记录 | 退出 |
popup17 |
裁剪未保存退出 | "您有裁剪的照片" | 退出 | 取消 |
popup 数据定义
data: {
popupShow: false,
popupType: 'popup16',
popupParams: {
close: false,
position: 'center',
} as any,
}
返回拦截模式
当页面有未保存数据时,使用自定义导航 + bind:back 拦截返回:
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 下载到本地:
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/ 各有一份,分别供子包和主包页面使用。