AGENTS.md 17 KB

zxyj-frontend

uni-app 微信小程序前端,Vue 2 语法,家庭教育任务管理。

WHERE TO LOOK

Task Location Notes
页面组件 pages/*/ login, index, tasks, rewards, profile, guide, games等
页面注册 pages.json TabBar配置和页面路径
应用配置 manifest.json AppID: wx5ba8038ef16fb245
全局样式 uni.scss SCSS变量和全局样式
应用入口 App.vue 全局生命周期和配置
组件库 components/ 地址选择器、任务卡片等可复用组件
API配置 config/ baseUrl和请求封装

STRUCTURE

主包(主包 = pages.jsonpages 数组)

仅含 5 个 TabBar 页面,主包大小已压缩至 ~1MB 以下:

路径 说明
pages/index/index 首页(未登录态引流页 + 角色路由跳转)
pages/mind/index 心维度首页
pages/body/index 身维度首页
pages/wisdom/index 智维度首页
pages/wealth/index 富维度首页

登录用户路由pages/index/indexparent-indexchild-index 已从静态组件改为 redirectTo 跳转到分包页面,不再计入主包体积。

分包(pages.jsonsubPackages 数组,共 45 个)

主要分包(按 root):

Root 内容
pages/teacher 成长规划师中心(7 页)
pages/guide 任务模板、邀请、绑定等(11 页)
pages/parent 家长端市场、孩子详情(3 页)
pages/child 孩子端任务、心愿(2 页)
pages/tasks 任务列表/创建/审核(4 页)
pages/wishes 心愿管理(3 页)
pages/rewards 心愿单/勋章墙(2 页)
pages/games 小游戏(5 页)
pages/assessment 测评申请/报告(5 页)
pages/dan-assessment DAN 测评上传(1 页)
pages/growth 成长记录(6 页)
pages/shop 商城(15 页)
pages/vendor 服务商入驻/管理(5 页)
pages/health 健康功能(18 页)
pages/mind-detail 心智详情(11 页)
pages/body-detail 身体详情(6 页)
pages/wisdom-detail 智慧详情(4 页)
pages/action-detail 行动详情(4 页)
pages/tianpan 家庭天盘(6 页)
pages/article-center 文章中心(4 页)
pages/activity 活动(3 页)
pages/promotion 财富中心/推广(6 页)
pages/membership 会员(3 页)
pages/index 原主包页面(parent-index, child-index, member-home-detail)
pages/login 登录页
pages/share 健康足迹
pages/profile 个人中心(profile + index)
pages/profile-extra 孩子管理/优惠券(6 页)
... 共 45 个分包 详见 pages.json

注意:分包页面在首次访问时异步下载,主包只加载必要的 TabBar 页面,确保主包 < 1.5MB。

TabBar 配置

pages.json 中定义 5 个 TabBar 页面(TabBar 文案,非五维维度名):

  • 首页: pages/index/index - 角色分流入口
  • 身: pages/body/index - 身体健康维度
  • 智: pages/wisdom/index - 智慧维度(心智测评)
  • 心: pages/mind/index - 心理/情感维度
  • 我的: pages/profile/profile - 个人设置和积分

CONVENTIONS

  • Vue 2: 使用 Options API,生命周期 onLoadonShow
  • 页面注册: 新页面必须在 pages.json 中注册
  • 导航: 使用 uni.navigateTouni.redirectTo
  • API 调用: 使用 uni.request,统一在 config.baseUrl
  • 地址选择: 使用 components/address-picker.vue 四级联动组件
  • 角色切换: 页面根据用户角色显示不同内容
  • 页面标题: 所有页面的标题必须在 pages.jsonnavigationBarTitleText 中配置,禁止在 Vue 模板中手动写 <text class="nav-title">。仅动态标题页面(根据数据实时变化)可保留自定义 nav-bar
  • API 调用去重(强制规范 · 违反者会被 CI 拦截): 同一个后端 URL 在同一个页面首次加载时只允许发起一次网络请求。这是硬性规则,不是建议。违规会通过 scripts/audit-duplicate-api-calls.js 被检出(exit code 1)。

为什么是根本性的: 微信小程序生命周期在页面首次进入时 onLoadonShow 都会触发,utils/api.js 的 3 秒去重是兜底,不是免死金牌——去重只能掩盖重复调用,不能消除它带来的延迟、内存和可维护性问题。

规则 1 · 生命周期双发(最常见): onLoadonShow 不得同时无条件调用同一个加载方法。必须使用 _onLoadFired 标志位让首次 onShow 跳过,切 Tab 返回时刷新:

  // 正确
  onLoad(options) {
    this.loadData(options)
    this._onLoadFired = true
  },
  onShow() {
    if (this._onLoadFired) {
      this._onLoadFired = false   // 首次进入:onLoad 已加载,跳过避免双发
    } else {
      this.loadData()              // 切 Tab 返回:刷新数据
    }
  },

如果 onShow 的加载是"从子页面返回后的条件性刷新"(如地址编辑页返回),同样适用该模式——将条件判断放在 else 分支内。

规则 2 · 同接口多封装(utils/api.js): utils/api.js 中,同一个后端 URL 只能有一个包含 request() 调用的导出函数作为规范入口;语义相近的别名必须委托到规范函数,禁止复制 request() 调用:

  // 规范入口
  export const getFamilyMemberList = (params) => request('/api/family/member/list', 'POST', params || {})
  // 别名(委托,不复制 request 调用)
  export const getChildren = () => getFamilyMemberList({ memberType: 'child' })

同 URL 不同 HTTP 方法 / 不同 params 的变体允许共存(如 createPackage POST data vs getMyPackages POST {}),但须在代码注释中标明差异。

规则 3 · 一个方法只打一次接口: 同一个方法内不得发起对同一 URL 的两次调用。常见反模式:loadLoggedInData() 中同时调用 loadFamilyMembers()loadChildren(),二者封装了同一个 URL → 合并为一个请求,从响应中派生不同用途的数据:

  // 错误:同一 URL 打两次
  loadLoggedInData() { this.loadFamilyMembers(); this.loadChildren() }
  // 正确:打一次,派生两类数据
  loadFamilyData() {
    getFamilyMemberList({ visibleOnly: true }).then(res => {
      var arr = res.data
      this.familyMembersVisible = arr
      this.children = arr.filter(m => m.effectiveRole === 'child')
    })
  }

规则 4 · 禁止 N+1 循环请求: 不得在 .map() / .forEach() / for 循环体内发起 API 请求。N 个元素 → N 次请求。应改为:(a) 后端提供批量接口一次返回;(b) 如果确实需要多元素多请求,须用 Promise.all() 并发执行(减少串行等待),同时确认后端能承受并发量;(c) 在循环前做去重/过滤,减少实际请求数。

规则 4a · 循环前过滤成员类型(常见反模式): 调用 getTaskHistory 等按成员 ID 轮询的接口时,必须先过滤 effectiveRole === 'child',不得对家庭成员全量列表(含家长)发起请求。示例:

  // 正确:过滤后轮询
  var children = members.filter(m => m.effectiveRole === 'child')
  Promise.all(children.map(c => getTaskHistory(c.id, 1, 3)))
  // 错误:对全部成员(含家长)发起请求
  Promise.all(members.map(m => getTaskHistory(m.id, 1, 3)))

规则 4b · getChildren 已废弃: getChildren() 内部委托 getFamilyMemberList({}),二者调用同一接口。新代码直接使用 getFamilyMemberList 并在页面内按 effectiveRole === 'child' 过滤,不要同时导入 getChildrengetFamilyMemberList

规则 4c · onLoad/onShow 首次加载只走一次: 小程序 TabBar 页面首次加载时 onLoadonShow 均会触发。所有 API 调用必须确保首次只执行一次:在 onLoad 中设置 _onLoadFired = trueonShow 中检查该标志——首次为 true 则跳过(由 onLoad 负责),后续切 Tab 返回时标志已重置为 false 才刷新。checkMemberStatus 等高频方法同样适用。详见规则 1 代码示例。

CI 门禁: scripts/audit-duplicate-api-calls.js 是 CI 必跑脚本。运行方式:node scripts/audit-duplicate-api-calls.js,退出码 0 = 通过,1 = 存在未解决的 ERROR 级违规。每个新页面提交前必须通过审计。

子组件接口调用: 子组件不得重复调用父页面已调用的接口。父组件应通过 props 传递数据,而非让子组件各自请求。

时间显示规范

页面时间展示必须统一以下两种格式:

使用场景 格式 示例
时间(年月日+时分秒) yyyy-MM-dd HH:mm:ss 2026-08-15 14:30:00
仅日期(不带时间) yyyy-MM-dd 2026-08-15

后端返回 ISO 8601 格式(2026-08-15T14:30:00),前端格式化:

  • 需展示时间:字符串先经 parseDate() 解析(iOS 安全),再用 getFullYear()/getMonth()/getDate()/getHours()/getMinutes()/getSeconds() 拼出 yyyy-MM-dd HH:mm:ss
  • 仅需日期:直接截取字符串前 10 位 → yyyy-MM-dd

禁止 new Date(str).toLocaleString()(iOS 输出格式与渲染结果因人而异)。

ANTI-PATTERNS

  • DO NOT 创建页面后忘记在 pages.json 注册
  • DO NOT 硬编码 API 地址,使用 config.baseUrl
  • DO NOT 在前端存储敏感信息 (密码、密钥)
  • NEVER 跳过 JWT 认证直接调用需登录接口
  • NEVER 在本地存储中保存敏感用户信息
  • NEVER 在 WXML/Vue 模板中使用可选链 ?.,微信小程序不支持 → 使用 && 代替(如 currentWish?.title 改为 currentWish && currentWish.title
  • NEVER:class 绑定中调用方法(如 :class="getStatusClass(item)"),微信小程序模板编译器不支持带参数的方法调用 → 改用内联表达式(如 :class="'status-' + item._cssClass")或计算属性
  • NEVER 在 CSS 类名、选择器中使用中文(如 .val-偏高.badge-低风险),微信小程序 wxss 编译器不支持中文类名 → 使用英文(如 .val-high.badge-low),数据中的中文状态通过 _cssClass 字段映射
  • NEVER:key 中使用表达式(如 :key="item.id || item.circleId"),微信小程序模板编译器不支持带运算符的 :key 绑定 → 改用方法调用(如 :key="getItemKey(item)",在 methods 中定义 getItemKey(item) { return item.id || item.circleId }
  • NEVER 直接用 new Date(string) 解析日期字符串。部分 iOS(JavaScriptCore)的 new Date() 只支持 yyyy/MM/ddyyyy/MM/dd HH:mm:ssyyyy-MM-ddyyyy-MM-ddTHH:mm:ssyyyy-MM-ddTHH:mm:ss+HH:mm 格式,后端常见的 "2026-08-22 14:00"(空格分隔的 MySQL DATETIME 风格)和 Java 的 "Sat Aug 22 14:00:00 CST 2026" 会解析失败返回 Invalid Date → 一律使用 utils/format.jsparseDate(),它会自动归一化上述两种格式并返回 Date|null(如 var d = parseDate(item.startTime); if (!d) return
  • NEVER 新增 uni.getSystemInfoSync() 调用。该 API 已在微信基础库 2.20.1+ 弃用,开发者工具会持续告警(getSystemInfoSync is deprecated)→ 改用 uni.getWindowInfo()(取 windowWidth/pixelRatio/safeArea 等窗口信息)或 uni.getDeviceInfo()(取 system/brand/model 等设备信息)。已有兼容代码(components/mp-html/ 内的 uni.canIUse('getWindowInfo') 兜底)保持不变。参考替换:uni.getSystemInfoSync().pixelRatiouni.getWindowInfo().pixelRatio
  • NEVERprops 传可能为非数组的值而不做兜底。组件接收 Array 类型 prop 时,父组件传值必须保证是数组(|| [] 兜底 + Array.isArray 校验),否则触发 Invalid prop: type check failed for prop "xxx". Expected Array, got Object 告警。典型场景:接口返回结构不确定(res.data.list 可能为 Object)时,用 var arr = (res.data && res.data.list) || (res.data instanceof Array ? res.data : []) 三态兼容

  • NEVER 在页面模板中写 <text class="nav-title"> 或自定义 nav-bar 标题。所有静态标题统一在 pages.jsonnavigationBarTitleText 中配置。需要运行时动态标题时才用自定义 nav-bar

  • NEVER 在同一个页面中多次调用相同的 API 接口。utils/api.jsrequest() 已内置 3 秒窗口去重(同 URL + method + body 自动复用 Promise),但这是安全网不是免死金牌——页面须从源头避免:onLoad 全量初始化 + _initDone 标志跳过 onShow 首次重复、降级路径同步设置所有 state 避免后续方法重复调用、子组件通过 props 接收数据而非各自请求

UNIQUE FEATURES

  • 地址匹配页面: pages/match/street-match.vue - 街道地址匹配功能
  • 地址编辑页面: pages/family/address-edit.vue - 家庭地址管理
  • 四级地址选择: components/address-picker.vue - 省市区街道联动
  • 双角色界面: 根据用户角色(parent/child)显示不同UI

BUILD SYSTEM

打包方式(强制规范)

小程序打包必须用 HBuilderX 完成,Agent 禁止自行执行 npm run build:mp-weixin 等打包命令。

  • 打包工具:HBuilderX(导入 cfc-frontend/ 目录 → 运行 → 运行到小程序模拟器 → 微信开发者工具)
  • 打包产物:cfc-frontend/unpackage/ 目录(HBuilderX 输出位置,dev 产物在 unpackage/dist/dev/mp-weixin/
  • 微信开发者工具导入的目录是 unpackage/dist/dev/mp-weixin/(或 HBuilderX 自动打开),不是 dist/
  • dist/ 目录是 npm CLI 打包的残留产物,不可用——app.js 仅有 89 字节(require 三行),非完整产物
  • Agent 修改代码后:只做语法/结构校验(如 node --check 提取的 script 块),不打包、不运行 build 命令
  • 页面白板排查第一步:确认用户是否在 HBuilderX 中重新打包过(改代码后未重新打包 → 产物过期 → 白板)

依赖约束(2026-07-23 修复后锁定)

版本 原因
@vue/cli-service ~4.5.0 v5 使用 webpack 5,@dcloudio/uni-mp-weixin 依赖 webpack/lib/GraphHelpers(webpack 4 特有)
@vue/cli-plugin-babel ~4.5.0 与 cli-service v4 匹配
postcss (override + dependency) 7.0.39 remove-scoped.js 在 PostCSS 8 下会走 v8 插件路径,但 spaces 属性访问有兼容问题
node-sass (devDependency) @dcloudio/vue-cli-plugin-uni 内置的 sass-loader 需要
copy-webpack-plugin ^5.1.2 与 cli-service v4 的 webpack 4 兼容

postinstall 自动补丁

npm install 后会自动运行 postinstall-fix.js,修补 remove-scoped.js 中的 spaces 崩溃:

// 问题: selector.first 可能为 undefined
// 修改前: selector.first.spaces.before = '';
// 修改后: if (selector.first) { selector.first.spaces.before = ''; }

已知编译错误处理

错误 原因 处理
Cannot read properties of undefined (reading 'spaces') in remove-scoped.js PostCSS 节点没有 spaces 属性 postinstall-fix.js 自动补丁
Cannot find module 'webpack/lib/GraphHelpers' @vue/cli-service v5 的 webpack 5 不支持 保持 @vue/cli-service ~4.5.0
Cannot find module 'node-sass' uni-app sass-loader 需要 npm install node-sass --save-dev
getPreCompileOptions DevTools 内部错误 dist/dev/mp-weixin/ 缺少 project.config.json npm run build:mp-weixin 生成 dist
Unexpected token in .vue JS 语法错误,常见于方法被截断/拼接错误 检查 v-on:click/方法调用是否正确闭合

修改 remove-scoped.js 的风险

该文件在 node_modules/@dcloudio/vue-cli-plugin-uni/ 内,每次 npm install 会通过 postinstall-fix.js 自动恢复补丁。如需手动执行:

node postinstall-fix.js

不要@vue/cli-service 升级到 v5,除非:

  • @dcloudio/uni-mp-weixin 确认移除了 webpack/lib/GraphHelpers 依赖
  • 或者找到替代方法处理独立分包插件

COMMANDS

npm install             # 安装依赖(自动运行 postinstall-fix.js)
# 注意:打包一律用 HBuilderX(产物在 unpackage/),Agent 不要执行 npm run build:mp-weixin

NOTES

  • 小程序AppID: wx5ba8038ef16fb245
  • 后端API地址: 配置在 config/index.jsbaseUrl
  • 地址数据: 使用后端街道API获取四级地址数据
  • 调试工具: 微信开发者工具需配置合法域名
  • 打包流程(HBuilderX):HBuilderX 打开 cfc-frontend/ → 运行到微信开发者工具 → 产物输出至 unpackage/dist/dev/mp-weixin/。改代码后必须在 HBuilderX 中重新运行打包,否则小程序显示旧产物(甚至白板)
  • 禁止在终端执行 npm run build:mp-weixinnpm run dev:mp-weixin 自行打包(产物进 dist/ 而非 unpackage/,与 HBuilderX 工作流冲突)