docs: 重写 AGENTS.md 和 README.md 项目文档
- AGENTS.md 精简 Codex 协作说明,聚焦扫码打卡链路等既有约定 - README.md 补充核心功能、目录结构、开发命令等项目描述 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
2 changed files
with
415 additions
and
317 deletions
| 1 | # AGENTS.md | 1 | # AGENTS.md |
| 2 | 2 | ||
| 3 | -本文件为 Codex (Codex.ai/code) 在此代码库中工作时提供指导。 | 3 | +本文件为 Codex 在 `lls_program` 仓库中的协作说明。目标不是介绍 Taro 通用知识,而是帮助后续修改尽量贴合这个项目当前的真实实现。 |
| 4 | 4 | ||
| 5 | ## 项目概述 | 5 | ## 项目概述 |
| 6 | 6 | ||
| 7 | -**lls_program** 是一个基于 Taro 4 + Vue 3 + NutUI 的微信小程序,名为"老来赛"。这是一个家庭活动和积分奖励管理系统。 | 7 | +`lls_program` 是一个基于 Taro 4 + Vue 3 + NutUI 的微信小程序,当前核心业务包括: |
| 8 | + | ||
| 9 | +- 家庭创建、加入、成员资料维护 | ||
| 10 | +- 活动页、广告页、海报打卡、扫码打卡 | ||
| 11 | +- 积分、奖励、优惠券相关页面 | ||
| 12 | +- 基于微信授权和 `sessionid` 的登录态流转 | ||
| 13 | + | ||
| 14 | +最近一轮实现里,扫码打卡链路已经接入真实接口,并补上了注册来源归因、回跳续扫、地理围栏判断等逻辑。后续改动请优先遵守这些既有约定,不要在页面里重新长出一套平行逻辑。 | ||
| 8 | 15 | ||
| 9 | ## 技术栈 | 16 | ## 技术栈 |
| 10 | 17 | ||
| 11 | -- **框架**: Taro 4.1.7 - 跨平台小程序框架 | 18 | +- 框架:Taro `4.1.7` |
| 12 | -- **UI**: Vue 3.3 + Composition API (`<script setup>`) | 19 | +- UI:Vue `3.3`,统一使用 `<script setup>` |
| 13 | -- **UI 组件库**: NutUI Taro 4.3.13 (自动导入,无需手动引入) | 20 | +- UI 组件:NutUI Taro `4.3.13` |
| 14 | -- **样式**: TailwindCSS 3.4 + Less (组件特定样式) | 21 | +- 样式:TailwindCSS `3.4` + Less |
| 15 | -- **状态管理**: Pinia 3.0 + taro-plugin-pinia | 22 | +- 状态管理:Pinia `3.0` + `taro-plugin-pinia` |
| 16 | -- **HTTP 请求**: axios-miniprogram 2.7.2 | 23 | +- 请求库:`axios-miniprogram` |
| 17 | -- **构建工具**: Webpack 5 | 24 | +- 构建:Webpack 5 |
| 25 | +- 测试:Vitest | ||
| 26 | + | ||
| 27 | +## 常用命令 | ||
| 18 | 28 | ||
| 19 | -## 开发命令 | 29 | +项目当前 `package.json` 里是标准 npm scripts,使用 `npm` 或 `pnpm` 都可以,但文档和命令示例优先按仓库现状写 `npm`。 |
| 20 | 30 | ||
| 21 | ```bash | 31 | ```bash |
| 22 | # 安装依赖 | 32 | # 安装依赖 |
| 23 | -pnpm install | 33 | +npm install |
| 24 | 34 | ||
| 25 | -# 开发(微信小程序) | 35 | +# 微信小程序开发 |
| 26 | -pnpm run dev:weapp | 36 | +npm run dev:weapp |
| 27 | 37 | ||
| 28 | -# 生产构建 | 38 | +# 微信小程序打包 |
| 29 | -pnpm run build:weapp | 39 | +npm run build:weapp |
| 30 | 40 | ||
| 31 | -# 其他平台 | 41 | +# H5 / 支付宝 / 抖音 |
| 32 | -pnpm run dev:h5 # H5 开发 | 42 | +npm run dev:h5 |
| 33 | -pnpm run dev:alipay # 支付宝小程序 | 43 | +npm run dev:alipay |
| 34 | -pnpm run dev:tt # 抖音小程序 | 44 | +npm run dev:tt |
| 35 | -``` | ||
| 36 | - | ||
| 37 | -## 架构设计 | ||
| 38 | 45 | ||
| 39 | -### 核心目录结构 | 46 | +# 代码质量 |
| 47 | +npm run lint | ||
| 48 | +npm run format | ||
| 40 | 49 | ||
| 50 | +# 测试 | ||
| 51 | +npm run test | ||
| 52 | +npm run test:run | ||
| 53 | +npm run test:coverage | ||
| 41 | ``` | 54 | ``` |
| 55 | + | ||
| 56 | +## 目录结构 | ||
| 57 | + | ||
| 58 | +```text | ||
| 42 | src/ | 59 | src/ |
| 43 | -├── api/ # 按业务领域组织的 API 接口 | 60 | +├── api/ # 只放接口调用 |
| 44 | -├── assets/ # 静态资源(图片、样式) | 61 | +├── assets/ # 静态资源 |
| 45 | -├── components/ # 可复用的 Vue 组件 | 62 | +├── components/ # 通用组件 |
| 46 | -├── composables/ # Vue 3 组合式函数 (useXxx) | 63 | +├── pages/ # 页面 |
| 47 | -├── pages/ # Taro 页面(每个页面包含 index.vue + index.config.js) | 64 | +├── stores/ # Pinia 状态 |
| 48 | -├── stores/ # Pinia 状态管理 | 65 | +├── utils/ # 纯工具逻辑、流程辅助、请求封装 |
| 49 | -├── utils/ # 工具函数 | 66 | +├── app.config.js # 页面注册、权限声明 |
| 50 | -├── app.config.js # Taro 应用配置(页面列表、窗口、权限) | ||
| 51 | └── app.less # 全局样式 | 67 | └── app.less # 全局样式 |
| 52 | ``` | 68 | ``` |
| 53 | 69 | ||
| 54 | -### 路径别名 (config/index.js:30-38) | 70 | +当前和扫码打卡强相关的目录: |
| 71 | + | ||
| 72 | +- `src/api/map.js` | ||
| 73 | +- `src/pages/ScanCheckinList/` | ||
| 74 | +- `src/pages/ScanCheckinDetail/` | ||
| 75 | +- `src/pages/Welcome/` | ||
| 76 | +- `src/pages/AddProfile/` | ||
| 77 | +- `src/pages/CreateFamily/` | ||
| 78 | +- `src/pages/JoinFamily/` | ||
| 79 | +- `src/utils/checkinLocation.js` | ||
| 80 | +- `src/utils/scanCheckin.js` | ||
| 81 | +- `src/utils/returnUrl.js` | ||
| 82 | +- `src/utils/userProfile.js` | ||
| 83 | +- `src/components/RichTextRenderer.vue` | ||
| 84 | + | ||
| 85 | +## 路径别名 | ||
| 86 | + | ||
| 87 | +项目已经配置好以下别名,新增代码优先使用别名,不要堆相对路径: | ||
| 55 | 88 | ||
| 56 | ```javascript | 89 | ```javascript |
| 57 | -@/utils → src/utils | 90 | +@/utils |
| 58 | -@/components → src/components | 91 | +@/components |
| 59 | -@/images → src/assets/images | 92 | +@/assets |
| 60 | -@/assets → src/assets | 93 | +@/api |
| 61 | -@/composables → src/composables | 94 | +@/stores |
| 62 | -@/api → src/api | 95 | +@/composables |
| 63 | -@/stores → src/stores | 96 | +@/hooks |
| 64 | -@/hooks → src/hooks | ||
| 65 | ``` | 97 | ``` |
| 66 | 98 | ||
| 67 | -### 设计宽度配置 | 99 | +## API 与请求约定 |
| 68 | - | ||
| 69 | -- **NutUI 组件**: 375px (自动处理) | ||
| 70 | -- **其他所有内容**: 750px (Taro 标准) | ||
| 71 | -- `config/index.js` 中的 `designWidth` 函数根据文件路径自动切换 | ||
| 72 | 100 | ||
| 73 | -## 核心 API 模式 | 101 | +### 响应判断 |
| 74 | 102 | ||
| 75 | -### API 响应格式 | 103 | +所有接口统一按下面结构处理: |
| 76 | 104 | ||
| 77 | -所有 API 响应遵循以下结构: | ||
| 78 | ```javascript | 105 | ```javascript |
| 79 | { | 106 | { |
| 80 | - code: 1, // 1 = 成功,其他值 = 失败 | 107 | + code: 1, |
| 81 | - data: {...}, // 响应数据 | 108 | + data: {}, |
| 82 | - msg: "message" // 错误/成功消息 | 109 | + msg: '...' |
| 83 | } | 110 | } |
| 84 | ``` | 111 | ``` |
| 85 | 112 | ||
| 86 | -**始终检查** `res.code === 1`(而不是 `res.code`)来判断成功。 | 113 | +必须显式判断 `res.code === 1`,不要写成 `if (res.code)`。 |
| 87 | 114 | ||
| 88 | -### 认证机制 (sessionid) | 115 | +### sessionid 机制 |
| 89 | 116 | ||
| 90 | -**关键**: 项目使用 `sessionid` 进行认证(存储在 `wx.storage` 中): | 117 | +项目通过 `sessionid` 做服务端认证: |
| 91 | 118 | ||
| 92 | -1. **获取**: `src/utils/request.js:23-30` - `getSessionId()` 从 `wx.getStorageSync("sessionid")` 读取 | 119 | +1. `src/utils/request.js` 每次请求前动态从 storage 读取 `sessionid` |
| 93 | -2. **设置**: 在 `miniProgramAuthAPI` 或 `loginAPI` 成功后设置 | 120 | +2. 请求头通过 `config.headers.cookie = sessionid` 透传 |
| 94 | -3. **使用**: 请求拦截器 (`request.js:75-78`) 设置 `config.headers.cookie = sessionid` | 121 | +3. 401 由现有登录/静默授权流程接管 |
| 95 | -4. **清除**: 收到 401 响应或用户登出时 | ||
| 96 | 122 | ||
| 97 | -⚠️ **重要**: sessionid **不**由前端用于判断登录状态(后端通过 401 响应来判断)。它只是传递给服务器的凭证。 | 123 | +重要约定: |
| 98 | 124 | ||
| 99 | -### 请求拦截器 (src/utils/request.js:66-80) | 125 | +- 前端不要把 `sessionid` 当成“能否继续业务流程”的唯一判断条件 |
| 126 | +- 是否能继续业务流程,应看接口结果和页面自己的业务条件 | ||
| 100 | 127 | ||
| 101 | -```javascript | 128 | +### API 文件职责边界 |
| 102 | -service.interceptors.request.use(config => { | ||
| 103 | - // 动态获取 sessionid 并设置到请求头 | ||
| 104 | - const sessionid = getSessionId(); | ||
| 105 | - if (sessionid) { | ||
| 106 | - config.headers.cookie = sessionid; | ||
| 107 | - } | ||
| 108 | - return config; | ||
| 109 | -}) | ||
| 110 | -``` | ||
| 111 | 129 | ||
| 112 | -### API 模块模式 (src/api/) | 130 | +`src/api/*.js` 只放接口定义和请求函数,不要把参数拼装、流程判断、字段归一化 helper 塞进去。 |
| 113 | 131 | ||
| 114 | -每个 API 文件导出调用中央 `fn()` 辅助函数的函数: | 132 | +正确做法: |
| 115 | 133 | ||
| 116 | -```javascript | 134 | +- API 请求函数放 `src/api/` |
| 117 | -// src/api/common.js | 135 | +- payload 构建、字段解析、return_url 处理、地理围栏计算等 helper 放 `src/utils/` |
| 118 | -export const smsAPI = (params) => fn(fetch.post(Api.SMS, params)); | ||
| 119 | -``` | ||
| 120 | 136 | ||
| 121 | -关键 API 模块: | 137 | +当前已经落地的例子: |
| 122 | -- `common.js` - 短信验证码、上传凭证 | ||
| 123 | -- `user.js` - 用户认证和个人信息 | ||
| 124 | -- `family.js` - 家庭管理 | ||
| 125 | -- `points.js` - 积分/奖励系统 | ||
| 126 | -- `photo.js` - 照片/媒体处理 | ||
| 127 | -- `organization.js` - 组织管理 | ||
| 128 | 138 | ||
| 129 | -## Taro 小程序限制 | 139 | +- `src/utils/userProfile.js` |
| 140 | + - `buildUpdateUserProfilePayload` | ||
| 141 | + - `isUserProfileComplete` | ||
| 142 | +- `src/utils/scanCheckin.js` | ||
| 143 | + - 负责从二维码内容里解析 `activity_id` / `detail_id` | ||
| 144 | +- `src/utils/returnUrl.js` | ||
| 145 | + - 负责 `return_url` 解码和拼接 | ||
| 146 | +- `src/utils/checkinLocation.js` | ||
| 147 | + - 负责地理围栏判断 | ||
| 130 | 148 | ||
| 131 | -### ❌ 禁止使用 Web API | 149 | +## 当前扫码打卡链路 |
| 132 | 150 | ||
| 133 | -```javascript | 151 | +这是现在最需要保持稳定的一条业务链路,后续改动请先看清楚,不要只盯某一个页面。 |
| 134 | -// 禁止 - 在小程序中会崩溃 | ||
| 135 | -window.document.getElementById() | ||
| 136 | -localStorage | ||
| 137 | -window.location.href | ||
| 138 | -fetch() | ||
| 139 | -``` | ||
| 140 | 152 | ||
| 141 | -### ✅ 必须使用 Taro API | 153 | +### 页面与接口 |
| 142 | 154 | ||
| 143 | -```javascript | 155 | +- 列表页:`src/pages/ScanCheckinList/index.vue` |
| 144 | -// 正确 - 使用 Taro 等价 API | 156 | + - 使用 `getScanStageListAPI` |
| 145 | -Taro.createSelectorQuery() | 157 | +- 详情页:`src/pages/ScanCheckinDetail/index.vue` |
| 146 | -Taro.getStorage() / Taro.setStorage() | 158 | + - 使用 `getScanStageDetailAPI` |
| 147 | -Taro.navigateTo() | 159 | + - 使用 `submitScanCheckinAPI` |
| 148 | -Taro.request() | 160 | +- 接口定义集中在 `src/api/map.js` |
| 149 | -``` | ||
| 150 | 161 | ||
| 151 | -### 页面生命周期(使用 Taro Hooks) | 162 | +### 当前流程 |
| 152 | 163 | ||
| 153 | -```javascript | 164 | +1. 用户从活动或二维码入口进入扫码打卡相关页面 |
| 154 | -import { useLoad, useShow, useReady } from '@tarojs/taro' | 165 | +2. 扫码链接会带 `activityId`,详情页自身还可能带 `reg_source`、`reg_stage_id` |
| 166 | +3. `ScanCheckinDetail` 点击“扫码打卡”时,先按现有路线检查“是否已有家庭” | ||
| 167 | +4. 没有家庭时,不直接扫码,先弹提示,再跳 `Welcome` | ||
| 168 | +5. `Welcome` 再决定是否先补资料、再创建家庭或加入家庭 | ||
| 169 | +6. 完成资料和家庭链路后,通过 `return_url` 回到原扫码详情页 | ||
| 170 | +7. 用户再次点击“扫码打卡”时: | ||
| 171 | + - 重新静默获取当前位置 | ||
| 172 | + - 如果该关卡开启地理围栏,则先判断是否在范围内 | ||
| 173 | + - 调起微信扫码 | ||
| 174 | + - 从二维码结果里解析真实的 `activity_id` / `detail_id` | ||
| 175 | + - 调用打卡接口 | ||
| 176 | +8. 打卡成功后跳转到 `ScanCheckinList` | ||
| 155 | 177 | ||
| 156 | -useLoad((options) => { | 178 | +### 关键业务约束 |
| 157 | - // 页面加载(仅触发一次)- 适合获取路由参数 | ||
| 158 | -}) | ||
| 159 | 179 | ||
| 160 | -useShow(() => { | 180 | +- 打卡提交参数来自二维码内容,不来自详情页当前路由参数 |
| 161 | - // 页面显示(每次显示都触发)- 适合刷新数据 | 181 | +- 详情页路由参数主要用于展示、回跳和列表跳转 |
| 162 | -}) | 182 | +- 注册来源归因字段当前只保留: |
| 183 | + - `reg_source` | ||
| 184 | + - `reg_stage_id` | ||
| 185 | +- `reg_activity_id` 已经不再需要,不要再传 | ||
| 186 | +- “是否补全资料”的判断交给 `Welcome` 链路,不要在 `ScanCheckinDetail` 再复制一套资料完整性分支 | ||
| 163 | 187 | ||
| 164 | -useReady(() => { | 188 | +## return_url 回跳约定 |
| 165 | - // 页面首次渲染完成 | ||
| 166 | -}) | ||
| 167 | -``` | ||
| 168 | 189 | ||
| 169 | -### ❌ 页面中避免使用 Vue 生命周期 | 190 | +扫码打卡目前依赖多页串联回跳,任何一个页面处理不对,都会出现跳错页、路径双重编码、甚至 `redirectTo` 找不到页面。 |
| 170 | 191 | ||
| 171 | -```javascript | 192 | +当前约定: |
| 172 | -// 不要使用 - 可能无法正常工作 | ||
| 173 | -onMounted(() => { ... }) | ||
| 174 | -onUnmounted(() => { ... }) | ||
| 175 | -``` | ||
| 176 | 193 | ||
| 177 | -## 组件指南 | 194 | +- 统一使用 `src/utils/returnUrl.js` |
| 195 | +- 页面收到 `options.return_url` 后,先走 `normalizeReturnUrl` | ||
| 196 | +- 页面拼接下一个页面地址时,优先走 `appendReturnUrlParam` | ||
| 197 | +- 不要在页面里到处手写 `decodeURIComponent(options.return_url || '')` | ||
| 178 | 198 | ||
| 179 | -### 页面结构 | 199 | +当前受这个约束影响较大的页面: |
| 180 | 200 | ||
| 181 | -每个页面目录包含: | 201 | +- `src/pages/Welcome/index.vue` |
| 182 | -- `index.vue` - 页面组件(必须使用 `<script setup>`) | 202 | +- `src/pages/AddProfile/index.vue` |
| 183 | -- `index.config.js` - 页面特定配置(navigationBarTitleText 等) | 203 | +- `src/pages/CreateFamily/index.vue` |
| 184 | -- `index.less` - 页面特定样式(scoped) | 204 | +- `src/pages/JoinFamily/index.vue` |
| 185 | 205 | ||
| 186 | -### 组件命名规范 | 206 | +## 注册资料与来源归因 |
| 187 | 207 | ||
| 188 | -- **页面**: 目录名(如 `pages/Dashboard/`) | 208 | +`buildUpdateUserProfilePayload` 目前已经抽到 `src/utils/userProfile.js`,专门负责把页面表单转成接口 payload。 |
| 189 | -- **组件**: PascalCase 多单词命名(如 `PointsCollector.vue`、`FamilyAlbum.vue`) | ||
| 190 | -- **API 文件**: camelCase(如 `miniProgramAuthAPI`) | ||
| 191 | 209 | ||
| 192 | -### NutUI 自动导入 | 210 | +当前约定: |
| 193 | 211 | ||
| 194 | -NutUI 组件通过 `unplugin-vue-components` 自动导入。**不要**手动导入: | 212 | +- `user.js` 里只保留接口调用 |
| 213 | +- `buildUpdateUserProfilePayload` 不回迁到 API 文件 | ||
| 214 | +- 注册来源字段在补资料接口里继续透传: | ||
| 215 | + - `reg_source` | ||
| 216 | + - `reg_stage_id` | ||
| 217 | +- `reg_stage_id` 需要按数值类型传给后端 | ||
| 195 | 218 | ||
| 196 | -```vue | 219 | +## 地理围栏约定 |
| 197 | -<!-- ✅ 正确 - 自动导入 --> | ||
| 198 | -<template> | ||
| 199 | - <nut-button type="primary">点击</nut-button> | ||
| 200 | -</template> | ||
| 201 | 220 | ||
| 202 | -<!-- ❌ 错误 - 不要导入 --> | 221 | +扫码详情页如果 `geo_enabled === true`,必须做范围判断。 |
| 203 | -<script setup> | ||
| 204 | -import { Button } from '@nutui/nutui-taro' | ||
| 205 | -</script> | ||
| 206 | -``` | ||
| 207 | 222 | ||
| 208 | -## 样式 | 223 | +当前实现约定: |
| 209 | 224 | ||
| 210 | -### TailwindCSS + Less 混合使用 | 225 | +- 判断逻辑统一放 `src/utils/checkinLocation.js` |
| 226 | +- 页面层只负责调用,不要在页面里自己算经纬度距离 | ||
| 227 | +- 点击“扫码打卡”时重新调用 `Taro.getLocation()`,不依赖旧缓存 | ||
| 228 | +- 关卡详情接口字段: | ||
| 229 | + - `geo_enabled` | ||
| 230 | + - `center_lng` | ||
| 231 | + - `center_lat` | ||
| 232 | + - `radius_meters` | ||
| 211 | 233 | ||
| 212 | -- **TailwindCSS**: 用于布局、间距、颜色、排版(80% 的样式) | 234 | +## 富文本与轮播图约定 |
| 213 | -- **Less**: 用于组件特定样式、动画、深度选择器(20%) | ||
| 214 | 235 | ||
| 215 | -### Tailwind 配置 | 236 | +扫码详情页当前已经不是单图和原生富文本直出: |
| 216 | 237 | ||
| 217 | -- **Content**: `./src/**/*.{html,js,ts,jsx,tsx,vue}` (tailwind.config.js:13) | 238 | +- 顶部 banner 使用 NutUI `nut-swiper` |
| 218 | -- **Preflight**: 禁用(小程序不需要) | 239 | +- 富文本显示统一走 `src/components/RichTextRenderer.vue` |
| 219 | -- **rem → rpx**: 由 `weapp-tailwindcss` 插件处理 (rem2rpx: true) | ||
| 220 | 240 | ||
| 221 | -### 样式指南 | 241 | +后续如果再改扫码详情页: |
| 222 | 242 | ||
| 223 | -```vue | 243 | +- 不要再改回单张 `<image>` |
| 224 | -<style lang="less" scoped> | 244 | +- 不要再临时拼一个简化版富文本组件替代现有 `RichTextRenderer` |
| 225 | -/* ✅ 组件必须使用 scoped */ | ||
| 226 | -.page-container { | ||
| 227 | - padding: 30px; | ||
| 228 | -} | ||
| 229 | 245 | ||
| 230 | -/* ✅ 使用 Less 处理深度选择器 */ | 246 | +## 页面开发约定 |
| 231 | -.custom-element :deep(.nut-popup) { | ||
| 232 | - background-color: #fff; | ||
| 233 | -} | ||
| 234 | -</style> | ||
| 235 | -``` | ||
| 236 | 247 | ||
| 237 | -## 状态管理 (Pinia) | 248 | +### 生命周期 |
| 238 | 249 | ||
| 239 | -### Store 模式 | 250 | +页面优先使用 Taro 生命周期: |
| 240 | 251 | ||
| 241 | ```javascript | 252 | ```javascript |
| 242 | -// src/stores/host.js | 253 | +import { useLoad, useShow, useDidShow, useReady } from '@tarojs/taro' |
| 243 | -import { defineStore } from 'pinia' | ||
| 244 | - | ||
| 245 | -export const hostStore = defineStore('host', { | ||
| 246 | - state: () => ({ | ||
| 247 | - id: '', | ||
| 248 | - join_id: '' | ||
| 249 | - }), | ||
| 250 | - actions: { | ||
| 251 | - add(id) { | ||
| 252 | - this.id = id | ||
| 253 | - } | ||
| 254 | - } | ||
| 255 | -}) | ||
| 256 | ``` | 254 | ``` |
| 257 | 255 | ||
| 258 | -### 在组件中使用 | 256 | +尽量避免把页面主流程写到 Vue 的 `onMounted` / `onUnmounted` 里。 |
| 259 | 257 | ||
| 260 | -```vue | 258 | +### 页面结构 |
| 261 | -<script setup> | ||
| 262 | -import { hostStore } from '@/stores/host' | ||
| 263 | 259 | ||
| 264 | -const host = hostStore() | 260 | +每个页面目录通常包含: |
| 265 | -host.add('123') | ||
| 266 | -</script> | ||
| 267 | -``` | ||
| 268 | 261 | ||
| 269 | -## 常用模式 | 262 | +- `index.vue` |
| 263 | +- `index.config.js` | ||
| 264 | +- `index.less` | ||
| 270 | 265 | ||
| 271 | -### 页面导航 | 266 | +新增页面后必须同步注册到 `src/app.config.js`。 |
| 272 | 267 | ||
| 273 | -```javascript | 268 | +### 小程序 API 约束 |
| 274 | -import Taro from '@tarojs/taro' | ||
| 275 | - | ||
| 276 | -// 跳转到页面 | ||
| 277 | -Taro.navigateTo({ | ||
| 278 | - url: '/pages/Detail/index?id=123' | ||
| 279 | -}) | ||
| 280 | - | ||
| 281 | -// 重定向(无返回) | ||
| 282 | -Taro.redirectTo({ | ||
| 283 | - url: '/pages/Login/index' | ||
| 284 | -}) | ||
| 285 | - | ||
| 286 | -// 切换 Tab | ||
| 287 | -Taro.switchTab({ | ||
| 288 | - url: '/pages/Dashboard/index' | ||
| 289 | -}) | ||
| 290 | - | ||
| 291 | -// 获取路由参数 | ||
| 292 | -useLoad((options) => { | ||
| 293 | - const { id } = options | ||
| 294 | -}) | ||
| 295 | -``` | ||
| 296 | 269 | ||
| 297 | -### 本地存储 | 270 | +不要在页面或工具函数里使用浏览器 API,例如: |
| 298 | 271 | ||
| 299 | -```javascript | 272 | +- `window` |
| 300 | -// 异步(推荐) | 273 | +- `document` |
| 301 | -await Taro.setStorage({ key: 'user', data: userInfo }) | 274 | +- `localStorage` |
| 302 | -const { data } = await Taro.getStorage({ key: 'user' }) | 275 | +- 原生 `fetch` |
| 303 | 276 | ||
| 304 | -// 同步(谨慎使用) | 277 | +统一使用 Taro API,例如: |
| 305 | -Taro.setStorageSync('token', 'xxxx') | ||
| 306 | -const token = Taro.getStorageSync('token') | ||
| 307 | -``` | ||
| 308 | 278 | ||
| 309 | -### 提示/弹窗 | 279 | +- `Taro.navigateTo` |
| 280 | +- `Taro.redirectTo` | ||
| 281 | +- `Taro.switchTab` | ||
| 282 | +- `Taro.getStorage` | ||
| 283 | +- `Taro.scanCode` | ||
| 284 | +- `Taro.getLocation` | ||
| 310 | 285 | ||
| 311 | -```javascript | 286 | +## 样式约定 |
| 312 | -// Toast 提示 | ||
| 313 | -Taro.showToast({ | ||
| 314 | - title: '操作成功', | ||
| 315 | - icon: 'success', | ||
| 316 | - duration: 2000 | ||
| 317 | -}) | ||
| 318 | - | ||
| 319 | -// Modal 弹窗 | ||
| 320 | -Taro.showModal({ | ||
| 321 | - title: '提示', | ||
| 322 | - content: '确定删除吗?', | ||
| 323 | - success: (res) => { | ||
| 324 | - if (res.confirm) { | ||
| 325 | - // 用户点击了确定 | ||
| 326 | - } | ||
| 327 | - } | ||
| 328 | -}) | ||
| 329 | -``` | ||
| 330 | 287 | ||
| 331 | -## 页面注册 | 288 | +- NutUI 组件自动导入,不要手动 import |
| 289 | +- 页面样式继续保持 Tailwind + Less 混合方式 | ||
| 290 | +- Less 样式默认 `scoped` | ||
| 291 | +- 深层覆盖 NutUI 时使用 `:deep(...)` | ||
| 332 | 292 | ||
| 333 | -页面在 `src/app.config.js` 中注册: | 293 | +这个仓库已经有一个和扫码页相关的稳定样式经验: |
| 334 | - | ||
| 335 | -```javascript | ||
| 336 | -export default { | ||
| 337 | - pages: [ | ||
| 338 | - 'pages/Dashboard/index', | ||
| 339 | - 'pages/MyFamily/index', | ||
| 340 | - 'pages/Activities/index', | ||
| 341 | - // ... 更多页面 | ||
| 342 | - ] | ||
| 343 | -} | ||
| 344 | -``` | ||
| 345 | 294 | ||
| 346 | -**创建新页面时**: 必须将其添加到此数组中。 | 295 | +- `ScanCheckinDetail` 底部按钮采用固定定位和安全区留白 |
| 296 | +- 如果只是调整按钮视觉位置,优先做 CSS 修改,不要顺手改业务逻辑 | ||
| 347 | 297 | ||
| 348 | -## 构建输出 | 298 | +## 权限与配置 |
| 349 | 299 | ||
| 350 | -- **开发环境**: `dist/` 目录 | 300 | +`src/app.config.js` 当前已经声明了位置权限: |
| 351 | -- **微信开发者工具**: 打开 `dist/` 作为项目根目录 | ||
| 352 | 301 | ||
| 353 | -## 重要文件说明 | 302 | +- `requiredPrivateInfos: ['getLocation']` |
| 303 | +- `permission['scope.userLocation']` | ||
| 354 | 304 | ||
| 355 | -### `src/utils/request.js` | 305 | +所以如果你在扫码打卡里继续用定位能力,优先复用现有授权前提,不要再设计一套重复授权流程。 |
| 356 | 306 | ||
| 357 | -核心 HTTP 客户端,包含: | 307 | +## 修改建议 |
| 358 | -- SessionID 注入 | ||
| 359 | -- 401 响应处理 | ||
| 360 | -- 401 时静默授权重定向 | ||
| 361 | -- 错误处理 | ||
| 362 | 308 | ||
| 363 | -### `src/utils/authRedirect.js` | 309 | +### 适合抽到 utils 的逻辑 |
| 364 | 310 | ||
| 365 | -处理小程序登录流程的静默授权。 | 311 | +- 二维码参数解析 |
| 312 | +- 距离计算和范围判断 | ||
| 313 | +- return_url 编解码 | ||
| 314 | +- 用户资料 payload 构建 | ||
| 315 | +- 纯字段映射或判空逻辑 | ||
| 366 | 316 | ||
| 367 | -### `src/utils/tools.js` | 317 | +### 不适合放进 API 文件的逻辑 |
| 368 | 318 | ||
| 369 | -通用工具函数: | 319 | +- 表单转 payload |
| 370 | -- `formatDate()` - 使用 moment.js 格式化日期 | 320 | +- 页面跳转流程判断 |
| 371 | -- `wxInfo()` - 平台检测(Android/iOS/微信) | 321 | +- 二维码内容解析 |
| 372 | -- `hasEllipsis()` - 文本溢出检测 | 322 | +- 路由参数兼容处理 |
| 323 | +- 页面专用字段映射 | ||
| 373 | 324 | ||
| 374 | -## 开发注意事项 | 325 | +### 做改动前先检查 |
| 375 | 326 | ||
| 376 | -1. **始终使用 Taro API** 而非 Web API | 327 | +- 这是页面职责,还是工具职责 |
| 377 | -2. **检查 `res.code === 1`** 判断 API 成功(不是 `res.code`) | 328 | +- 这是接口请求定义,还是接口参数拼装 |
| 378 | -3. **NutUI 组件已自动导入** - 不要手动导入 | 329 | +- 这是详情页展示参数,还是打卡提交真实参数 |
| 379 | -4. **页面中使用 Taro 生命周期钩子**(`useLoad`、`useShow`) | 330 | +- 这是当前页下一跳,还是完整链路里的回跳页面 |
| 380 | -5. **SessionID 动态获取** - 每次请求从存储中读取 | ||
| 381 | -6. **已配置路径别名** - 使用 `@/components` 代替相对路径 | ||
| 382 | -7. **设计宽度双模式**: NutUI 使用 375px,其他使用 750px | ||
| 383 | 331 | ||
| 384 | -## 平台差异 | 332 | +## 交付标准 |
| 385 | 333 | ||
| 386 | -项目通过 Taro 支持多平台: | 334 | +在这个仓库里完成修改时,优先做到: |
| 387 | -- **微信 (weapp)**: 主要目标平台 | ||
| 388 | -- **H5**: Web 浏览器版本 | ||
| 389 | -- **支付宝 (alipay)**: 支付宝小程序 | ||
| 390 | -- **抖音 (tt)**: 字节跳动小程序 | ||
| 391 | 335 | ||
| 392 | -平台特定代码可使用: | 336 | +- 与当前真实业务链路一致 |
| 393 | -```javascript | 337 | +- 不在页面里复制已有 helper |
| 394 | -if (process.env.TARO_ENV === 'weapp') { | 338 | +- 不在 API 文件里塞 helper |
| 395 | - // 微信特定代码 | 339 | +- 不破坏 `return_url` 回跳 |
| 396 | -} | 340 | +- 不破坏扫码打卡的地理围栏与注册来源归因 |
| 397 | -``` | 341 | +- 不把 Taro 小程序页面写成浏览器页面 | ... | ... |
| 1 | -## 项目介绍 | 1 | +# lls_program |
| 2 | 2 | ||
| 3 | -基于Taro4的微信小程序模版,集成了常用的功能,如登录、注册、列表、详情、购物车等。 | 3 | +`lls_program` 是一个基于 Taro 4 + Vue 3 + NutUI 的微信小程序项目,当前业务围绕家庭管理、活动参与、积分奖励,以及海报/扫码打卡展开。 |
| 4 | + | ||
| 5 | +这不是一个通用模板仓库。当前代码里已经有比较明确的业务结构,尤其是扫码打卡链路、资料补全链路、家庭创建/加入链路,后续开发建议直接沿用现有实现方式,而不是重新搭一套平行流程。 | ||
| 4 | 6 | ||
| 5 | ## 技术栈 | 7 | ## 技术栈 |
| 6 | 8 | ||
| 7 | -- Taro4 | 9 | +- Taro `4.1.7` |
| 8 | -- Vue3 | 10 | +- Vue `3.3` |
| 9 | -- TypeScript | 11 | +- NutUI Taro `4.3.13` |
| 10 | -- Pinia | 12 | +- Pinia `3.0` |
| 13 | +- TailwindCSS `3.4` | ||
| 11 | - Less | 14 | - Less |
| 15 | +- axios-miniprogram | ||
| 16 | +- Vitest | ||
| 17 | + | ||
| 18 | +## 核心功能 | ||
| 19 | + | ||
| 20 | +- 微信小程序登录与静默授权 | ||
| 21 | +- 家庭创建、加入、家庭资料维护 | ||
| 22 | +- 用户资料补全与注册来源归因 | ||
| 23 | +- 活动详情、海报打卡、扫码打卡 | ||
| 24 | +- 积分、奖励、优惠券相关页面 | ||
| 25 | +- 地理位置相关能力与位置权限接入 | ||
| 26 | + | ||
| 27 | +## 当前重点业务链路 | ||
| 28 | + | ||
| 29 | +### 扫码打卡 | ||
| 30 | + | ||
| 31 | +当前扫码打卡已接入真实接口,主要页面和工具如下: | ||
| 32 | + | ||
| 33 | +- 列表页:`src/pages/ScanCheckinList/index.vue` | ||
| 34 | +- 详情页:`src/pages/ScanCheckinDetail/index.vue` | ||
| 35 | +- API:`src/api/map.js` | ||
| 36 | +- 地理围栏:`src/utils/checkinLocation.js` | ||
| 37 | +- 扫码参数解析:`src/utils/scanCheckin.js` | ||
| 38 | +- 回跳参数处理:`src/utils/returnUrl.js` | ||
| 39 | +- 富文本渲染:`src/components/RichTextRenderer.vue` | ||
| 40 | + | ||
| 41 | +当前流程: | ||
| 42 | + | ||
| 43 | +1. 用户进入扫码打卡详情页 | ||
| 44 | +2. 点击“扫码打卡”时先检查是否已有家庭 | ||
| 45 | +3. 没有家庭则提示后跳转 `Welcome` | ||
| 46 | +4. `Welcome -> AddProfile -> Welcome -> CreateFamily/JoinFamily -> 原详情页` | ||
| 47 | +5. 返回详情页后再次扫码 | ||
| 48 | +6. 若关卡启用地理围栏,先重新静默获取当前位置并判断范围 | ||
| 49 | +7. 调起微信扫码 | ||
| 50 | +8. 从二维码结果中解析真实的 `activity_id` / `detail_id` | ||
| 51 | +9. 调用真实打卡接口 | ||
| 52 | +10. 成功后跳到扫码关卡列表页 | ||
| 53 | + | ||
| 54 | +补充约定: | ||
| 55 | + | ||
| 56 | +- 打卡参数来自二维码内容,不来自当前页面路由参数 | ||
| 57 | +- 注册来源归因目前通过 `reg_source`、`reg_stage_id` 透传 | ||
| 58 | +- `reg_activity_id` 已经不再使用 | ||
| 59 | +- `return_url` 回跳统一通过 `src/utils/returnUrl.js` 处理 | ||
| 12 | 60 | ||
| 13 | ## 项目结构 | 61 | ## 项目结构 |
| 14 | 62 | ||
| 15 | -- src | 63 | +```text |
| 16 | - - api:请求接口 | 64 | +src/ |
| 17 | - - assets:静态资源 | 65 | +├── api/ # 接口定义,只放请求相关代码 |
| 18 | - - components:全局组件 | 66 | +├── assets/ # 静态资源 |
| 19 | - - config:项目配置 | 67 | +├── components/ # 通用组件 |
| 20 | - - pages:页面 | 68 | +├── pages/ # 页面 |
| 21 | - - stores:状态管理 | 69 | +├── stores/ # Pinia 状态管理 |
| 22 | - - utils:工具函数 | 70 | +├── utils/ # 工具函数、流程辅助 |
| 23 | - - app.config.js:项目配置 | 71 | +├── app.config.js # 页面注册、权限声明 |
| 24 | - - app.js:应用入口 | 72 | +└── app.less # 全局样式 |
| 25 | - - app.less:全局样式 | 73 | +``` |
| 26 | -- taro.config.js:Taro配置 | 74 | + |
| 27 | -- tsconfig.json:TypeScript配置 | 75 | +几个常用目录说明: |
| 28 | -- package.json:依赖配置 | 76 | + |
| 29 | - | 77 | +- `src/api/user.js`:用户认证、资料接口 |
| 30 | -## 项目运行 | 78 | +- `src/api/family.js`:家庭相关接口 |
| 31 | - | 79 | +- `src/api/map.js`:地图、扫码关卡、扫码打卡相关接口 |
| 32 | -1. 安装依赖 | 80 | +- `src/utils/request.js`:请求拦截器、`sessionid` 注入 |
| 81 | +- `src/utils/userProfile.js`:资料相关 helper | ||
| 82 | +- `src/components/RichTextRenderer.vue`:富文本渲染组件 | ||
| 83 | + | ||
| 84 | +## 开发约定 | ||
| 85 | + | ||
| 86 | +### API 与 helper 分层 | ||
| 87 | + | ||
| 88 | +- `src/api/*.js` 只放接口调用 | ||
| 89 | +- 参数拼装、字段解析、距离计算、回跳处理等逻辑放 `src/utils/` | ||
| 90 | + | ||
| 91 | +例如: | ||
| 92 | + | ||
| 93 | +- `buildUpdateUserProfilePayload` 在 `src/utils/userProfile.js` | ||
| 94 | +- 扫码结果解析在 `src/utils/scanCheckin.js` | ||
| 95 | +- 地理围栏判断在 `src/utils/checkinLocation.js` | ||
| 96 | + | ||
| 97 | +### 请求返回判断 | ||
| 98 | + | ||
| 99 | +所有接口都要显式判断: | ||
| 100 | + | ||
| 101 | +```javascript | ||
| 102 | +if (res.code === 1) { | ||
| 103 | + // success | ||
| 104 | +} | ||
| 105 | +``` | ||
| 106 | + | ||
| 107 | +不要写成: | ||
| 108 | + | ||
| 109 | +```javascript | ||
| 110 | +if (res.code) { | ||
| 111 | + // 不推荐 | ||
| 112 | +} | ||
| 113 | +``` | ||
| 114 | + | ||
| 115 | +### 小程序环境约束 | ||
| 116 | + | ||
| 117 | +页面里不要使用浏览器 API: | ||
| 118 | + | ||
| 119 | +- `window` | ||
| 120 | +- `document` | ||
| 121 | +- `localStorage` | ||
| 122 | +- 原生 `fetch` | ||
| 123 | + | ||
| 124 | +统一使用 Taro API: | ||
| 125 | + | ||
| 126 | +- `Taro.navigateTo` | ||
| 127 | +- `Taro.redirectTo` | ||
| 128 | +- `Taro.switchTab` | ||
| 129 | +- `Taro.scanCode` | ||
| 130 | +- `Taro.getLocation` | ||
| 131 | + | ||
| 132 | +### 页面生命周期 | ||
| 133 | + | ||
| 134 | +页面优先使用 Taro 提供的生命周期: | ||
| 135 | + | ||
| 136 | +- `useLoad` | ||
| 137 | +- `useShow` | ||
| 138 | +- `useDidShow` | ||
| 139 | +- `useReady` | ||
| 140 | + | ||
| 141 | +## 登录与认证 | ||
| 142 | + | ||
| 143 | +项目当前通过 `sessionid` 维持服务端登录态: | ||
| 144 | + | ||
| 145 | +- 从 storage 读取 `sessionid` | ||
| 146 | +- 在请求拦截器中注入到 `cookie` | ||
| 147 | +- 401 交给现有授权/跳转逻辑处理 | ||
| 148 | + | ||
| 149 | +需要注意: | ||
| 150 | + | ||
| 151 | +- `sessionid` 只是认证凭证 | ||
| 152 | +- 它不等于“用户一定能继续后续业务流程” | ||
| 153 | +- 业务上是否可继续,仍要结合资料、家庭、活动规则等条件判断 | ||
| 154 | + | ||
| 155 | +## 本地开发 | ||
| 156 | + | ||
| 157 | +### 安装依赖 | ||
| 33 | 158 | ||
| 34 | ```bash | 159 | ```bash |
| 35 | npm install | 160 | npm install |
| 36 | ``` | 161 | ``` |
| 37 | 162 | ||
| 38 | -2. 运行项目 | 163 | +### 微信小程序开发 |
| 39 | 164 | ||
| 40 | ```bash | 165 | ```bash |
| 41 | npm run dev:weapp | 166 | npm run dev:weapp |
| 42 | ``` | 167 | ``` |
| 43 | 168 | ||
| 44 | -3. 打包项目 | 169 | +### 微信小程序构建 |
| 45 | 170 | ||
| 46 | ```bash | 171 | ```bash |
| 47 | npm run build:weapp | 172 | npm run build:weapp |
| 48 | ``` | 173 | ``` |
| 174 | + | ||
| 175 | +### 其他常用命令 | ||
| 176 | + | ||
| 177 | +```bash | ||
| 178 | +npm run dev:h5 | ||
| 179 | +npm run dev:alipay | ||
| 180 | +npm run dev:tt | ||
| 181 | +npm run lint | ||
| 182 | +npm run format | ||
| 183 | +npm run test:run | ||
| 184 | +``` | ||
| 185 | + | ||
| 186 | +## 页面注册与权限 | ||
| 187 | + | ||
| 188 | +页面统一在 `src/app.config.js` 中注册。 | ||
| 189 | + | ||
| 190 | +当前已经声明位置相关能力: | ||
| 191 | + | ||
| 192 | +- `requiredPrivateInfos: ['getLocation']` | ||
| 193 | +- `permission.scope.userLocation` | ||
| 194 | + | ||
| 195 | +这也是扫码打卡地理围栏能力能直接接上的基础配置。 | ||
| 196 | + | ||
| 197 | +## 备注 | ||
| 198 | + | ||
| 199 | +- NutUI 组件是自动导入的,不需要手动 import | ||
| 200 | +- `ScanCheckinDetail` 顶部 banner 现在使用 NutUI `nut-swiper` | ||
| 201 | +- `ScanCheckinDetail` 富文本展示统一使用 `RichTextRenderer` | ||
| 202 | +- 如果只是调整扫码详情页底部按钮视觉位置,优先改样式,不要顺手改业务逻辑 | ... | ... |
-
Please register or login to post a comment