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

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:

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 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 配置:

{
  "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 变量

pageToputils/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.cropImagesrc 参数只接受本地文件路径,不支持网络 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/ 各有一份,分别供子包和主包页面使用。