feat: 新增扫码打卡流程与展位图画廊功能
- 新增扫码打卡列表页、详情页及展位图画廊页,包含完整样式与配置 - 新增扫码打卡点模拟数据工具函数 - 升级活动详情页以适配扫码打卡类型流程 - 将新页面注册至应用配置列表 - 更新地图活动API接口,新增打卡类型参数 - 修复应用配置及API文件的格式问题 - 新增项目开发指导文档AGENTS.md
Showing
14 changed files
with
1110 additions
and
0 deletions
AGENTS.md
0 → 100644
| 1 | +# AGENTS.md | ||
| 2 | + | ||
| 3 | +本文件为 Codex (Codex.ai/code) 在此代码库中工作时提供指导。 | ||
| 4 | + | ||
| 5 | +## 项目概述 | ||
| 6 | + | ||
| 7 | +**lls_program** 是一个基于 Taro 4 + Vue 3 + NutUI 的微信小程序,名为"老来赛"。这是一个家庭活动和积分奖励管理系统。 | ||
| 8 | + | ||
| 9 | +## 技术栈 | ||
| 10 | + | ||
| 11 | +- **框架**: Taro 4.1.7 - 跨平台小程序框架 | ||
| 12 | +- **UI**: Vue 3.3 + Composition API (`<script setup>`) | ||
| 13 | +- **UI 组件库**: NutUI Taro 4.3.13 (自动导入,无需手动引入) | ||
| 14 | +- **样式**: TailwindCSS 3.4 + Less (组件特定样式) | ||
| 15 | +- **状态管理**: Pinia 3.0 + taro-plugin-pinia | ||
| 16 | +- **HTTP 请求**: axios-miniprogram 2.7.2 | ||
| 17 | +- **构建工具**: Webpack 5 | ||
| 18 | + | ||
| 19 | +## 开发命令 | ||
| 20 | + | ||
| 21 | +```bash | ||
| 22 | +# 安装依赖 | ||
| 23 | +pnpm install | ||
| 24 | + | ||
| 25 | +# 开发(微信小程序) | ||
| 26 | +pnpm run dev:weapp | ||
| 27 | + | ||
| 28 | +# 生产构建 | ||
| 29 | +pnpm run build:weapp | ||
| 30 | + | ||
| 31 | +# 其他平台 | ||
| 32 | +pnpm run dev:h5 # H5 开发 | ||
| 33 | +pnpm run dev:alipay # 支付宝小程序 | ||
| 34 | +pnpm run dev:tt # 抖音小程序 | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +## 架构设计 | ||
| 38 | + | ||
| 39 | +### 核心目录结构 | ||
| 40 | + | ||
| 41 | +``` | ||
| 42 | +src/ | ||
| 43 | +├── api/ # 按业务领域组织的 API 接口 | ||
| 44 | +├── assets/ # 静态资源(图片、样式) | ||
| 45 | +├── components/ # 可复用的 Vue 组件 | ||
| 46 | +├── composables/ # Vue 3 组合式函数 (useXxx) | ||
| 47 | +├── pages/ # Taro 页面(每个页面包含 index.vue + index.config.js) | ||
| 48 | +├── stores/ # Pinia 状态管理 | ||
| 49 | +├── utils/ # 工具函数 | ||
| 50 | +├── app.config.js # Taro 应用配置(页面列表、窗口、权限) | ||
| 51 | +└── app.less # 全局样式 | ||
| 52 | +``` | ||
| 53 | + | ||
| 54 | +### 路径别名 (config/index.js:30-38) | ||
| 55 | + | ||
| 56 | +```javascript | ||
| 57 | +@/utils → src/utils | ||
| 58 | +@/components → src/components | ||
| 59 | +@/images → src/assets/images | ||
| 60 | +@/assets → src/assets | ||
| 61 | +@/composables → src/composables | ||
| 62 | +@/api → src/api | ||
| 63 | +@/stores → src/stores | ||
| 64 | +@/hooks → src/hooks | ||
| 65 | +``` | ||
| 66 | + | ||
| 67 | +### 设计宽度配置 | ||
| 68 | + | ||
| 69 | +- **NutUI 组件**: 375px (自动处理) | ||
| 70 | +- **其他所有内容**: 750px (Taro 标准) | ||
| 71 | +- `config/index.js` 中的 `designWidth` 函数根据文件路径自动切换 | ||
| 72 | + | ||
| 73 | +## 核心 API 模式 | ||
| 74 | + | ||
| 75 | +### API 响应格式 | ||
| 76 | + | ||
| 77 | +所有 API 响应遵循以下结构: | ||
| 78 | +```javascript | ||
| 79 | +{ | ||
| 80 | + code: 1, // 1 = 成功,其他值 = 失败 | ||
| 81 | + data: {...}, // 响应数据 | ||
| 82 | + msg: "message" // 错误/成功消息 | ||
| 83 | +} | ||
| 84 | +``` | ||
| 85 | + | ||
| 86 | +**始终检查** `res.code === 1`(而不是 `res.code`)来判断成功。 | ||
| 87 | + | ||
| 88 | +### 认证机制 (sessionid) | ||
| 89 | + | ||
| 90 | +**关键**: 项目使用 `sessionid` 进行认证(存储在 `wx.storage` 中): | ||
| 91 | + | ||
| 92 | +1. **获取**: `src/utils/request.js:23-30` - `getSessionId()` 从 `wx.getStorageSync("sessionid")` 读取 | ||
| 93 | +2. **设置**: 在 `miniProgramAuthAPI` 或 `loginAPI` 成功后设置 | ||
| 94 | +3. **使用**: 请求拦截器 (`request.js:75-78`) 设置 `config.headers.cookie = sessionid` | ||
| 95 | +4. **清除**: 收到 401 响应或用户登出时 | ||
| 96 | + | ||
| 97 | +⚠️ **重要**: sessionid **不**由前端用于判断登录状态(后端通过 401 响应来判断)。它只是传递给服务器的凭证。 | ||
| 98 | + | ||
| 99 | +### 请求拦截器 (src/utils/request.js:66-80) | ||
| 100 | + | ||
| 101 | +```javascript | ||
| 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 | + | ||
| 112 | +### API 模块模式 (src/api/) | ||
| 113 | + | ||
| 114 | +每个 API 文件导出调用中央 `fn()` 辅助函数的函数: | ||
| 115 | + | ||
| 116 | +```javascript | ||
| 117 | +// src/api/common.js | ||
| 118 | +export const smsAPI = (params) => fn(fetch.post(Api.SMS, params)); | ||
| 119 | +``` | ||
| 120 | + | ||
| 121 | +关键 API 模块: | ||
| 122 | +- `common.js` - 短信验证码、上传凭证 | ||
| 123 | +- `user.js` - 用户认证和个人信息 | ||
| 124 | +- `family.js` - 家庭管理 | ||
| 125 | +- `points.js` - 积分/奖励系统 | ||
| 126 | +- `photo.js` - 照片/媒体处理 | ||
| 127 | +- `organization.js` - 组织管理 | ||
| 128 | + | ||
| 129 | +## Taro 小程序限制 | ||
| 130 | + | ||
| 131 | +### ❌ 禁止使用 Web API | ||
| 132 | + | ||
| 133 | +```javascript | ||
| 134 | +// 禁止 - 在小程序中会崩溃 | ||
| 135 | +window.document.getElementById() | ||
| 136 | +localStorage | ||
| 137 | +window.location.href | ||
| 138 | +fetch() | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +### ✅ 必须使用 Taro API | ||
| 142 | + | ||
| 143 | +```javascript | ||
| 144 | +// 正确 - 使用 Taro 等价 API | ||
| 145 | +Taro.createSelectorQuery() | ||
| 146 | +Taro.getStorage() / Taro.setStorage() | ||
| 147 | +Taro.navigateTo() | ||
| 148 | +Taro.request() | ||
| 149 | +``` | ||
| 150 | + | ||
| 151 | +### 页面生命周期(使用 Taro Hooks) | ||
| 152 | + | ||
| 153 | +```javascript | ||
| 154 | +import { useLoad, useShow, useReady } from '@tarojs/taro' | ||
| 155 | + | ||
| 156 | +useLoad((options) => { | ||
| 157 | + // 页面加载(仅触发一次)- 适合获取路由参数 | ||
| 158 | +}) | ||
| 159 | + | ||
| 160 | +useShow(() => { | ||
| 161 | + // 页面显示(每次显示都触发)- 适合刷新数据 | ||
| 162 | +}) | ||
| 163 | + | ||
| 164 | +useReady(() => { | ||
| 165 | + // 页面首次渲染完成 | ||
| 166 | +}) | ||
| 167 | +``` | ||
| 168 | + | ||
| 169 | +### ❌ 页面中避免使用 Vue 生命周期 | ||
| 170 | + | ||
| 171 | +```javascript | ||
| 172 | +// 不要使用 - 可能无法正常工作 | ||
| 173 | +onMounted(() => { ... }) | ||
| 174 | +onUnmounted(() => { ... }) | ||
| 175 | +``` | ||
| 176 | + | ||
| 177 | +## 组件指南 | ||
| 178 | + | ||
| 179 | +### 页面结构 | ||
| 180 | + | ||
| 181 | +每个页面目录包含: | ||
| 182 | +- `index.vue` - 页面组件(必须使用 `<script setup>`) | ||
| 183 | +- `index.config.js` - 页面特定配置(navigationBarTitleText 等) | ||
| 184 | +- `index.less` - 页面特定样式(scoped) | ||
| 185 | + | ||
| 186 | +### 组件命名规范 | ||
| 187 | + | ||
| 188 | +- **页面**: 目录名(如 `pages/Dashboard/`) | ||
| 189 | +- **组件**: PascalCase 多单词命名(如 `PointsCollector.vue`、`FamilyAlbum.vue`) | ||
| 190 | +- **API 文件**: camelCase(如 `miniProgramAuthAPI`) | ||
| 191 | + | ||
| 192 | +### NutUI 自动导入 | ||
| 193 | + | ||
| 194 | +NutUI 组件通过 `unplugin-vue-components` 自动导入。**不要**手动导入: | ||
| 195 | + | ||
| 196 | +```vue | ||
| 197 | +<!-- ✅ 正确 - 自动导入 --> | ||
| 198 | +<template> | ||
| 199 | + <nut-button type="primary">点击</nut-button> | ||
| 200 | +</template> | ||
| 201 | + | ||
| 202 | +<!-- ❌ 错误 - 不要导入 --> | ||
| 203 | +<script setup> | ||
| 204 | +import { Button } from '@nutui/nutui-taro' | ||
| 205 | +</script> | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +## 样式 | ||
| 209 | + | ||
| 210 | +### TailwindCSS + Less 混合使用 | ||
| 211 | + | ||
| 212 | +- **TailwindCSS**: 用于布局、间距、颜色、排版(80% 的样式) | ||
| 213 | +- **Less**: 用于组件特定样式、动画、深度选择器(20%) | ||
| 214 | + | ||
| 215 | +### Tailwind 配置 | ||
| 216 | + | ||
| 217 | +- **Content**: `./src/**/*.{html,js,ts,jsx,tsx,vue}` (tailwind.config.js:13) | ||
| 218 | +- **Preflight**: 禁用(小程序不需要) | ||
| 219 | +- **rem → rpx**: 由 `weapp-tailwindcss` 插件处理 (rem2rpx: true) | ||
| 220 | + | ||
| 221 | +### 样式指南 | ||
| 222 | + | ||
| 223 | +```vue | ||
| 224 | +<style lang="less" scoped> | ||
| 225 | +/* ✅ 组件必须使用 scoped */ | ||
| 226 | +.page-container { | ||
| 227 | + padding: 30px; | ||
| 228 | +} | ||
| 229 | + | ||
| 230 | +/* ✅ 使用 Less 处理深度选择器 */ | ||
| 231 | +.custom-element :deep(.nut-popup) { | ||
| 232 | + background-color: #fff; | ||
| 233 | +} | ||
| 234 | +</style> | ||
| 235 | +``` | ||
| 236 | + | ||
| 237 | +## 状态管理 (Pinia) | ||
| 238 | + | ||
| 239 | +### Store 模式 | ||
| 240 | + | ||
| 241 | +```javascript | ||
| 242 | +// src/stores/host.js | ||
| 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 | +``` | ||
| 257 | + | ||
| 258 | +### 在组件中使用 | ||
| 259 | + | ||
| 260 | +```vue | ||
| 261 | +<script setup> | ||
| 262 | +import { hostStore } from '@/stores/host' | ||
| 263 | + | ||
| 264 | +const host = hostStore() | ||
| 265 | +host.add('123') | ||
| 266 | +</script> | ||
| 267 | +``` | ||
| 268 | + | ||
| 269 | +## 常用模式 | ||
| 270 | + | ||
| 271 | +### 页面导航 | ||
| 272 | + | ||
| 273 | +```javascript | ||
| 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 | + | ||
| 297 | +### 本地存储 | ||
| 298 | + | ||
| 299 | +```javascript | ||
| 300 | +// 异步(推荐) | ||
| 301 | +await Taro.setStorage({ key: 'user', data: userInfo }) | ||
| 302 | +const { data } = await Taro.getStorage({ key: 'user' }) | ||
| 303 | + | ||
| 304 | +// 同步(谨慎使用) | ||
| 305 | +Taro.setStorageSync('token', 'xxxx') | ||
| 306 | +const token = Taro.getStorageSync('token') | ||
| 307 | +``` | ||
| 308 | + | ||
| 309 | +### 提示/弹窗 | ||
| 310 | + | ||
| 311 | +```javascript | ||
| 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 | + | ||
| 331 | +## 页面注册 | ||
| 332 | + | ||
| 333 | +页面在 `src/app.config.js` 中注册: | ||
| 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 | + | ||
| 346 | +**创建新页面时**: 必须将其添加到此数组中。 | ||
| 347 | + | ||
| 348 | +## 构建输出 | ||
| 349 | + | ||
| 350 | +- **开发环境**: `dist/` 目录 | ||
| 351 | +- **微信开发者工具**: 打开 `dist/` 作为项目根目录 | ||
| 352 | + | ||
| 353 | +## 重要文件说明 | ||
| 354 | + | ||
| 355 | +### `src/utils/request.js` | ||
| 356 | + | ||
| 357 | +核心 HTTP 客户端,包含: | ||
| 358 | +- SessionID 注入 | ||
| 359 | +- 401 响应处理 | ||
| 360 | +- 401 时静默授权重定向 | ||
| 361 | +- 错误处理 | ||
| 362 | + | ||
| 363 | +### `src/utils/authRedirect.js` | ||
| 364 | + | ||
| 365 | +处理小程序登录流程的静默授权。 | ||
| 366 | + | ||
| 367 | +### `src/utils/tools.js` | ||
| 368 | + | ||
| 369 | +通用工具函数: | ||
| 370 | +- `formatDate()` - 使用 moment.js 格式化日期 | ||
| 371 | +- `wxInfo()` - 平台检测(Android/iOS/微信) | ||
| 372 | +- `hasEllipsis()` - 文本溢出检测 | ||
| 373 | + | ||
| 374 | +## 开发注意事项 | ||
| 375 | + | ||
| 376 | +1. **始终使用 Taro API** 而非 Web API | ||
| 377 | +2. **检查 `res.code === 1`** 判断 API 成功(不是 `res.code`) | ||
| 378 | +3. **NutUI 组件已自动导入** - 不要手动导入 | ||
| 379 | +4. **页面中使用 Taro 生命周期钩子**(`useLoad`、`useShow`) | ||
| 380 | +5. **SessionID 动态获取** - 每次请求从存储中读取 | ||
| 381 | +6. **已配置路径别名** - 使用 `@/components` 代替相对路径 | ||
| 382 | +7. **设计宽度双模式**: NutUI 使用 375px,其他使用 750px | ||
| 383 | + | ||
| 384 | +## 平台差异 | ||
| 385 | + | ||
| 386 | +项目通过 Taro 支持多平台: | ||
| 387 | +- **微信 (weapp)**: 主要目标平台 | ||
| 388 | +- **H5**: Web 浏览器版本 | ||
| 389 | +- **支付宝 (alipay)**: 支付宝小程序 | ||
| 390 | +- **抖音 (tt)**: 字节跳动小程序 | ||
| 391 | + | ||
| 392 | +平台特定代码可使用: | ||
| 393 | +```javascript | ||
| 394 | +if (process.env.TARO_ENV === 'weapp') { | ||
| 395 | + // 微信特定代码 | ||
| 396 | +} | ||
| 397 | +``` |
| ... | @@ -35,6 +35,7 @@ export const checkinAPI = params => fn(fetch.post(Api.Checkin, params)) | ... | @@ -35,6 +35,7 @@ export const checkinAPI = params => fn(fetch.post(Api.Checkin, params)) |
| 35 | * data: { | 35 | * data: { |
| 36 | url: string; // 地图网址 | 36 | url: string; // 地图网址 |
| 37 | id: integer; // 活动ID | 37 | id: integer; // 活动ID |
| 38 | + type: string; // 打卡类型,MAP=地图打卡,QR_CODE=扫码打卡 | ||
| 38 | cover: string; // 封面图 | 39 | cover: string; // 封面图 |
| 39 | begin_date: string; // 开始时间 | 40 | begin_date: string; // 开始时间 |
| 40 | end_date: string; // 结束时间 | 41 | end_date: string; // 结束时间 | ... | ... |
| ... | @@ -37,6 +37,9 @@ export default { | ... | @@ -37,6 +37,9 @@ export default { |
| 37 | 'pages/FamilyRank/index', | 37 | 'pages/FamilyRank/index', |
| 38 | 'pages/PosterCheckin/index', | 38 | 'pages/PosterCheckin/index', |
| 39 | 'pages/PosterCheckinDetail/index', | 39 | 'pages/PosterCheckinDetail/index', |
| 40 | + 'pages/ScanCheckinList/index', | ||
| 41 | + 'pages/ScanCheckinDetail/index', | ||
| 42 | + 'pages/BoothMapGallery/index', | ||
| 40 | 'pages/CheckinList/index', | 43 | 'pages/CheckinList/index', |
| 41 | 'pages/CheckinMap/index', | 44 | 'pages/CheckinMap/index', |
| 42 | 'pages/JoinOrganization/index', | 45 | 'pages/JoinOrganization/index', | ... | ... |
| ... | @@ -158,6 +158,11 @@ import { mockMapActivityDetailAPI } from '@/utils/mockData' | ... | @@ -158,6 +158,11 @@ import { mockMapActivityDetailAPI } from '@/utils/mockData' |
| 158 | // const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | 158 | // const USE_MOCK_DATA = process.env.NODE_ENV === 'development' |
| 159 | const USE_MOCK_DATA = false | 159 | const USE_MOCK_DATA = false |
| 160 | 160 | ||
| 161 | +const CHECKIN_TYPES = { | ||
| 162 | + MAP: 'MAP', | ||
| 163 | + QR_CODE: 'QR_CODE', | ||
| 164 | +} | ||
| 165 | + | ||
| 161 | // 默认海报图 | 166 | // 默认海报图 |
| 162 | const defaultPoster = ref( | 167 | const defaultPoster = ref( |
| 163 | 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60' | 168 | 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60' |
| ... | @@ -272,6 +277,7 @@ const activityData = ref({ | ... | @@ -272,6 +277,7 @@ const activityData = ref({ |
| 272 | '有机会获得商户优惠券', | 277 | '有机会获得商户优惠券', |
| 273 | '参与月度积分排行榜', | 278 | '参与月度积分排行榜', |
| 274 | ], | 279 | ], |
| 280 | + checkinType: CHECKIN_TYPES.MAP, | ||
| 275 | }) | 281 | }) |
| 276 | 282 | ||
| 277 | // 分享配置(动态生成,包含当前页面参数) | 283 | // 分享配置(动态生成,包含当前页面参数) |
| ... | @@ -398,6 +404,8 @@ const getUserLocation = async (skipAuthCheck = false) => { | ... | @@ -398,6 +404,8 @@ const getUserLocation = async (skipAuthCheck = false) => { |
| 398 | * 获取按钮显示文本 | 404 | * 获取按钮显示文本 |
| 399 | */ | 405 | */ |
| 400 | const getButtonText = () => { | 406 | const getButtonText = () => { |
| 407 | + const currentCheckinType = getCurrentCheckinType() | ||
| 408 | + | ||
| 401 | // 如果活动已结束,显示"活动已结束" | 409 | // 如果活动已结束,显示"活动已结束" |
| 402 | if (activityStatus.value.is_ended) { | 410 | if (activityStatus.value.is_ended) { |
| 403 | return '活动已结束' | 411 | return '活动已结束' |
| ... | @@ -413,6 +421,10 @@ const getButtonText = () => { | ... | @@ -413,6 +421,10 @@ const getButtonText = () => { |
| 413 | return '立即参加' | 421 | return '立即参加' |
| 414 | } | 422 | } |
| 415 | 423 | ||
| 424 | + if (currentCheckinType === CHECKIN_TYPES.QR_CODE) { | ||
| 425 | + return '进入打卡点' | ||
| 426 | + } | ||
| 427 | + | ||
| 416 | // 如果位置获取失败,显示"重新定位" | 428 | // 如果位置获取失败,显示"重新定位" |
| 417 | if (locationError.value) { | 429 | if (locationError.value) { |
| 418 | return '重新定位' | 430 | return '重新定位' |
| ... | @@ -433,6 +445,33 @@ const getButtonText = () => { | ... | @@ -433,6 +445,33 @@ const getButtonText = () => { |
| 433 | } | 445 | } |
| 434 | 446 | ||
| 435 | /** | 447 | /** |
| 448 | + * 获取当前生效的打卡类型 | ||
| 449 | + * 接口未返回 type 时,默认仍走原来的地图打卡流程。 | ||
| 450 | + */ | ||
| 451 | +const getCurrentCheckinType = () => { | ||
| 452 | + return activityData.value.checkinType || CHECKIN_TYPES.MAP | ||
| 453 | +} | ||
| 454 | + | ||
| 455 | +/** | ||
| 456 | + * 跳转到扫码打卡点列表页 | ||
| 457 | + */ | ||
| 458 | +const navigateToQrCodeCheckin = async () => { | ||
| 459 | + const params = new URLSearchParams() | ||
| 460 | + | ||
| 461 | + if (activityId.value) { | ||
| 462 | + params.append('activityId', activityId.value) | ||
| 463 | + } | ||
| 464 | + | ||
| 465 | + if (activityData.value.title) { | ||
| 466 | + params.append('title', activityData.value.title) | ||
| 467 | + } | ||
| 468 | + | ||
| 469 | + await Taro.navigateTo({ | ||
| 470 | + url: `/pages/ScanCheckinList/index?${params.toString()}`, | ||
| 471 | + }) | ||
| 472 | +} | ||
| 473 | + | ||
| 474 | +/** | ||
| 436 | * 检查用户是否加入家庭并处理参加活动按钮点击 | 475 | * 检查用户是否加入家庭并处理参加活动按钮点击 |
| 437 | */ | 476 | */ |
| 438 | const checkFamilyStatusAndJoinActivity = async () => { | 477 | const checkFamilyStatusAndJoinActivity = async () => { |
| ... | @@ -565,6 +604,11 @@ const handleJoinActivity = async () => { | ... | @@ -565,6 +604,11 @@ const handleJoinActivity = async () => { |
| 565 | isJoining.value = true | 604 | isJoining.value = true |
| 566 | 605 | ||
| 567 | try { | 606 | try { |
| 607 | + if (getCurrentCheckinType() === CHECKIN_TYPES.QR_CODE) { | ||
| 608 | + await navigateToQrCodeCheckin() | ||
| 609 | + return | ||
| 610 | + } | ||
| 611 | + | ||
| 568 | // 检查定位授权状态 | 612 | // 检查定位授权状态 |
| 569 | const authSetting = await Taro.getSetting() | 613 | const authSetting = await Taro.getSetting() |
| 570 | const hasLocationPermission = authSetting.authSetting['scope.userLocation'] | 614 | const hasLocationPermission = authSetting.authSetting['scope.userLocation'] |
| ... | @@ -1005,6 +1049,7 @@ const transformApiDataToActivityData = apiData => { | ... | @@ -1005,6 +1049,7 @@ const transformApiDataToActivityData = apiData => { |
| 1005 | discount_title: apiData.discount_title || '打卡点专属优惠', | 1049 | discount_title: apiData.discount_title || '打卡点专属优惠', |
| 1006 | activityId: apiData.id || '', | 1050 | activityId: apiData.id || '', |
| 1007 | mapUrl: apiData.url || '', // 保留地图 URL | 1051 | mapUrl: apiData.url || '', // 保留地图 URL |
| 1052 | + checkinType: apiData.type || CHECKIN_TYPES.MAP, | ||
| 1008 | } | 1053 | } |
| 1009 | } | 1054 | } |
| 1010 | 1055 | ... | ... |
src/pages/BoothMapGallery/index.config.js
0 → 100644
src/pages/BoothMapGallery/index.less
0 → 100644
| 1 | +.booth-map-gallery-page { | ||
| 2 | + min-height: 100vh; | ||
| 3 | + padding: 24rpx; | ||
| 4 | + background: #f4f6f8; | ||
| 5 | + box-sizing: border-box; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +.booth-map-gallery-grid { | ||
| 9 | + column-count: 2; | ||
| 10 | + column-gap: 20rpx; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +.booth-map-gallery-item { | ||
| 14 | + break-inside: avoid; | ||
| 15 | + margin-bottom: 20rpx; | ||
| 16 | + border-radius: 24rpx; | ||
| 17 | + overflow: hidden; | ||
| 18 | + background: #ffffff; | ||
| 19 | + box-shadow: 0 12rpx 32rpx rgba(15, 23, 42, 0.08); | ||
| 20 | +} | ||
| 21 | + | ||
| 22 | +.booth-map-gallery-image { | ||
| 23 | + width: 100%; | ||
| 24 | + display: block; | ||
| 25 | + background: #e5e7eb; | ||
| 26 | +} |
src/pages/BoothMapGallery/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="booth-map-gallery-page"> | ||
| 3 | + <view class="booth-map-gallery-grid"> | ||
| 4 | + <view | ||
| 5 | + v-for="(item, index) in imageList" | ||
| 6 | + :key="item.id" | ||
| 7 | + class="booth-map-gallery-item" | ||
| 8 | + @click="previewImage(index)" | ||
| 9 | + > | ||
| 10 | + <image class="booth-map-gallery-image" :src="item.url" :mode="item.mode || 'widthFix'" /> | ||
| 11 | + </view> | ||
| 12 | + </view> | ||
| 13 | + </view> | ||
| 14 | +</template> | ||
| 15 | + | ||
| 16 | +<script setup> | ||
| 17 | +import { ref } from 'vue' | ||
| 18 | +import Taro from '@tarojs/taro' | ||
| 19 | +import './index.less' | ||
| 20 | + | ||
| 21 | +const imageList = ref([ | ||
| 22 | + { | ||
| 23 | + id: 'booth-01', | ||
| 24 | + url: 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 25 | + mode: 'widthFix', | ||
| 26 | + }, | ||
| 27 | + { | ||
| 28 | + id: 'booth-02', | ||
| 29 | + url: 'https://cdn.ipadbiz.cn/lls_prog/images/welcome_8.jpg?imageMogr2/strip/quality/60', | ||
| 30 | + mode: 'widthFix', | ||
| 31 | + }, | ||
| 32 | + { | ||
| 33 | + id: 'booth-03', | ||
| 34 | + url: 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 35 | + mode: 'widthFix', | ||
| 36 | + }, | ||
| 37 | +]) | ||
| 38 | + | ||
| 39 | +const previewImage = index => { | ||
| 40 | + Taro.previewImage({ | ||
| 41 | + current: imageList.value[index].url, | ||
| 42 | + urls: imageList.value.map(item => item.url), | ||
| 43 | + }) | ||
| 44 | +} | ||
| 45 | +</script> | ||
| 46 | + | ||
| 47 | +<script> | ||
| 48 | +export default { | ||
| 49 | + name: 'BoothMapGallery', | ||
| 50 | +} | ||
| 51 | +</script> |
src/pages/ScanCheckinDetail/index.config.js
0 → 100644
src/pages/ScanCheckinDetail/index.less
0 → 100644
| 1 | +.scan-checkin-detail-page { | ||
| 2 | + min-height: 100vh; | ||
| 3 | + padding: 0 0 60rpx; | ||
| 4 | + background: #f2f4f7; | ||
| 5 | + box-sizing: border-box; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +.scan-checkin-detail-cover { | ||
| 9 | + width: 100%; | ||
| 10 | + height: 520rpx; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +.scan-checkin-detail-cover-image { | ||
| 14 | + width: 100%; | ||
| 15 | + height: 100%; | ||
| 16 | + display: block; | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +.scan-checkin-detail-card { | ||
| 20 | + margin: 28rpx 24rpx 0; | ||
| 21 | + padding: 36rpx 32rpx 40rpx; | ||
| 22 | + border-radius: 32rpx; | ||
| 23 | + background: #ffffff; | ||
| 24 | + box-shadow: 0 16rpx 50rpx rgba(15, 23, 42, 0.08); | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +.scan-checkin-detail-heading { | ||
| 28 | + display: flex; | ||
| 29 | + align-items: flex-start; | ||
| 30 | + justify-content: space-between; | ||
| 31 | + gap: 24rpx; | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +.scan-checkin-detail-title { | ||
| 35 | + flex: 1; | ||
| 36 | + font-size: 36rpx; | ||
| 37 | + line-height: 1.35; | ||
| 38 | + color: #1f2937; | ||
| 39 | + font-weight: 600; | ||
| 40 | + word-break: break-all; | ||
| 41 | +} | ||
| 42 | + | ||
| 43 | +.scan-checkin-detail-status { | ||
| 44 | + padding: 10rpx 24rpx; | ||
| 45 | + border-radius: 999rpx; | ||
| 46 | + font-size: 24rpx; | ||
| 47 | + line-height: 1; | ||
| 48 | + white-space: nowrap; | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +.scan-checkin-detail-status.pending { | ||
| 52 | + color: #f97316; | ||
| 53 | + border: 1rpx solid rgba(249, 115, 22, 0.5); | ||
| 54 | + background: rgba(255, 247, 237, 0.9); | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +.scan-checkin-detail-status.done { | ||
| 58 | + color: #16a34a; | ||
| 59 | + border: 1rpx solid rgba(22, 163, 74, 0.45); | ||
| 60 | + background: rgba(240, 253, 244, 0.95); | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +.scan-checkin-detail-subtitle { | ||
| 64 | + display: block; | ||
| 65 | + margin-top: 18rpx; | ||
| 66 | + font-size: 28rpx; | ||
| 67 | + line-height: 1.5; | ||
| 68 | + color: #c3cad5; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +.scan-checkin-detail-section { | ||
| 72 | + margin-top: 34rpx; | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +.scan-checkin-detail-section-header { | ||
| 76 | + padding-bottom: 2rpx; | ||
| 77 | + border-bottom: 1rpx solid #e5e7eb; | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +.scan-checkin-detail-section-title { | ||
| 81 | + position: relative; | ||
| 82 | + display: inline-block; | ||
| 83 | + padding-bottom: 18rpx; | ||
| 84 | + font-size: 30rpx; | ||
| 85 | + font-weight: 600; | ||
| 86 | + line-height: 1.2; | ||
| 87 | + color: #df7750; | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +.scan-checkin-detail-section-title::after { | ||
| 91 | + content: ''; | ||
| 92 | + position: absolute; | ||
| 93 | + left: 50%; | ||
| 94 | + bottom: 0; | ||
| 95 | + width: 80rpx; | ||
| 96 | + height: 8rpx; | ||
| 97 | + border-radius: 999rpx; | ||
| 98 | + background: #df7750; | ||
| 99 | + transform: translateX(-50%); | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +.scan-checkin-detail-content { | ||
| 103 | + margin-top: 24rpx; | ||
| 104 | + padding-top: 26rpx; | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +.scan-checkin-detail-rich-text { | ||
| 108 | + display: block; | ||
| 109 | + color: #4b5563; | ||
| 110 | + font-size: 30rpx; | ||
| 111 | + line-height: 1.8; | ||
| 112 | +} | ||
| 113 | + | ||
| 114 | +.scan-checkin-detail-button-wrap { | ||
| 115 | + display: flex; | ||
| 116 | + justify-content: center; | ||
| 117 | + margin-top: 48rpx; | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +.scan-checkin-detail-button { | ||
| 121 | + width: 420rpx; | ||
| 122 | + height: 96rpx; | ||
| 123 | + border-radius: 24rpx; | ||
| 124 | + font-size: 38rpx; | ||
| 125 | + font-weight: 600; | ||
| 126 | + box-shadow: 0 18rpx 36rpx rgba(239, 123, 69, 0.28); | ||
| 127 | +} |
src/pages/ScanCheckinDetail/index.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="scan-checkin-detail-page"> | ||
| 3 | + <view class="scan-checkin-detail-cover"> | ||
| 4 | + <image class="scan-checkin-detail-cover-image" :src="detail.cover" mode="aspectFill" /> | ||
| 5 | + </view> | ||
| 6 | + | ||
| 7 | + <view class="scan-checkin-detail-card"> | ||
| 8 | + <view class="scan-checkin-detail-heading"> | ||
| 9 | + <text class="scan-checkin-detail-title">{{ detail.code }} {{ detail.title }}</text> | ||
| 10 | + <view class="scan-checkin-detail-status" :class="detail.isChecked ? 'done' : 'pending'"> | ||
| 11 | + {{ detail.isChecked ? '已打卡' : '未打卡' }} | ||
| 12 | + </view> | ||
| 13 | + </view> | ||
| 14 | + | ||
| 15 | + <text class="scan-checkin-detail-subtitle">{{ detail.guideText }}</text> | ||
| 16 | + | ||
| 17 | + <view class="scan-checkin-detail-section"> | ||
| 18 | + <view class="scan-checkin-detail-section-header"> | ||
| 19 | + <text class="scan-checkin-detail-section-title">{{ detail.discountTitle }}</text> | ||
| 20 | + </view> | ||
| 21 | + <view class="scan-checkin-detail-content"> | ||
| 22 | + <rich-text class="scan-checkin-detail-rich-text" :nodes="formattedDiscountContent" /> | ||
| 23 | + </view> | ||
| 24 | + </view> | ||
| 25 | + </view> | ||
| 26 | + | ||
| 27 | + <view class="scan-checkin-detail-button-wrap"> | ||
| 28 | + <nut-button | ||
| 29 | + type="primary" | ||
| 30 | + class="scan-checkin-detail-button" | ||
| 31 | + color="#DF7750" | ||
| 32 | + :loading="scanSubmitting" | ||
| 33 | + @click="handleScanCheckin" | ||
| 34 | + > | ||
| 35 | + 扫码打卡 | ||
| 36 | + </nut-button> | ||
| 37 | + </view> | ||
| 38 | + </view> | ||
| 39 | +</template> | ||
| 40 | + | ||
| 41 | +<script setup> | ||
| 42 | +import { reactive, computed } from 'vue' | ||
| 43 | +import Taro, { useLoad } from '@tarojs/taro' | ||
| 44 | +import './index.less' | ||
| 45 | +import { getMockScanCheckinDetail } from '@/utils/mockQrCheckin' | ||
| 46 | + | ||
| 47 | +const detail = reactive({ | ||
| 48 | + id: '', | ||
| 49 | + code: 'W2D01', | ||
| 50 | + title: '泰康之家经营管理有限公司上海分公司', | ||
| 51 | + guideText: '在点位现场扫码打卡并推荐好物', | ||
| 52 | + discountTitle: '打卡点专属优惠', | ||
| 53 | + discountContentRaw: '', | ||
| 54 | + cover: 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 55 | + isChecked: false, | ||
| 56 | + lastScanCode: '', | ||
| 57 | + scanSubmitting: false, | ||
| 58 | +}) | ||
| 59 | + | ||
| 60 | +const scanSubmitting = computed(() => detail.scanSubmitting === true) | ||
| 61 | + | ||
| 62 | +const formattedDiscountContent = computed(() => { | ||
| 63 | + const content = detail.discountContentRaw | ||
| 64 | + | ||
| 65 | + if (!content) { | ||
| 66 | + return '' | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + if (Array.isArray(content)) { | ||
| 70 | + return content | ||
| 71 | + .map( | ||
| 72 | + item => | ||
| 73 | + `<p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">${item}</p>` | ||
| 74 | + ) | ||
| 75 | + .join('') | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + let formattedContent = content | ||
| 79 | + | ||
| 80 | + formattedContent = formattedContent.replace(/\n/g, '<br>') | ||
| 81 | + formattedContent = formattedContent.replace( | ||
| 82 | + /<img/g, | ||
| 83 | + '<img style="max-width:100%;height:auto;display:block;border-radius:16rpx;margin:24rpx 0;"' | ||
| 84 | + ) | ||
| 85 | + | ||
| 86 | + return formattedContent | ||
| 87 | +}) | ||
| 88 | + | ||
| 89 | +const mockSubmitScanCode = async code => { | ||
| 90 | + await new Promise(resolve => { | ||
| 91 | + setTimeout(resolve, 500) | ||
| 92 | + }) | ||
| 93 | + | ||
| 94 | + return { | ||
| 95 | + code: 1, | ||
| 96 | + msg: '打卡成功', | ||
| 97 | + data: { | ||
| 98 | + scan_code: code, | ||
| 99 | + }, | ||
| 100 | + } | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +const handleScanCheckin = async () => { | ||
| 104 | + detail.scanSubmitting = true | ||
| 105 | + | ||
| 106 | + try { | ||
| 107 | + const scanResult = await Taro.scanCode({ | ||
| 108 | + onlyFromCamera: false, | ||
| 109 | + scanType: ['qrCode', 'barCode'], | ||
| 110 | + }) | ||
| 111 | + | ||
| 112 | + const scannedCode = scanResult.result || '' | ||
| 113 | + | ||
| 114 | + if (!scannedCode) { | ||
| 115 | + Taro.showToast({ | ||
| 116 | + title: '未识别到扫码结果', | ||
| 117 | + icon: 'none', | ||
| 118 | + }) | ||
| 119 | + return | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + const submitResult = await mockSubmitScanCode(scannedCode) | ||
| 123 | + | ||
| 124 | + if (submitResult.code === 1) { | ||
| 125 | + detail.isChecked = true | ||
| 126 | + detail.lastScanCode = scannedCode | ||
| 127 | + | ||
| 128 | + await Taro.showModal({ | ||
| 129 | + title: '模拟提交成功', | ||
| 130 | + content: `扫码结果:${scannedCode}`, | ||
| 131 | + showCancel: false, | ||
| 132 | + confirmText: '知道了', | ||
| 133 | + }) | ||
| 134 | + return | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + Taro.showToast({ | ||
| 138 | + title: submitResult.msg || '提交失败', | ||
| 139 | + icon: 'none', | ||
| 140 | + }) | ||
| 141 | + } catch (error) { | ||
| 142 | + if (error?.errMsg && error.errMsg.includes('cancel')) { | ||
| 143 | + return | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + console.error('扫码打卡失败:', error) | ||
| 147 | + Taro.showToast({ | ||
| 148 | + title: '扫码失败,请重试', | ||
| 149 | + icon: 'none', | ||
| 150 | + }) | ||
| 151 | + } finally { | ||
| 152 | + detail.scanSubmitting = false | ||
| 153 | + } | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +const handleMockDataLoaded = mockDetail => { | ||
| 157 | + detail.scanSubmitting = false | ||
| 158 | + | ||
| 159 | + Object.assign(detail, { | ||
| 160 | + ...mockDetail, | ||
| 161 | + discountContentRaw: mockDetail.discountContent, | ||
| 162 | + isChecked: mockDetail.status === '已打卡', | ||
| 163 | + }) | ||
| 164 | +} | ||
| 165 | + | ||
| 166 | +useLoad(options => { | ||
| 167 | + const detailId = options.detailId || options.id || '' | ||
| 168 | + const mockDetail = getMockScanCheckinDetail(detailId) | ||
| 169 | + | ||
| 170 | + if (!mockDetail) { | ||
| 171 | + Taro.showToast({ | ||
| 172 | + title: '未找到打卡点', | ||
| 173 | + icon: 'none', | ||
| 174 | + }) | ||
| 175 | + return | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + handleMockDataLoaded(mockDetail) | ||
| 179 | +}) | ||
| 180 | +</script> | ||
| 181 | + | ||
| 182 | +<script> | ||
| 183 | +export default { | ||
| 184 | + name: 'ScanCheckinDetail', | ||
| 185 | +} | ||
| 186 | +</script> |
src/pages/ScanCheckinList/index.config.js
0 → 100644
src/pages/ScanCheckinList/index.less
0 → 100644
| 1 | +.scan-checkin-list-page { | ||
| 2 | + min-height: 100vh; | ||
| 3 | + padding: 32rpx 24rpx 40rpx; | ||
| 4 | + background: linear-gradient(180deg, #f6f8fb 0%, #eef2f5 100%); | ||
| 5 | + box-sizing: border-box; | ||
| 6 | + position: relative; | ||
| 7 | +} | ||
| 8 | + | ||
| 9 | +.scan-checkin-list-header { | ||
| 10 | + margin-bottom: 24rpx; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +.scan-checkin-list-title { | ||
| 14 | + display: block; | ||
| 15 | + font-size: 40rpx; | ||
| 16 | + font-weight: 600; | ||
| 17 | + color: #1f2937; | ||
| 18 | + line-height: 1.4; | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +.scan-checkin-list-subtitle { | ||
| 22 | + display: block; | ||
| 23 | + margin-top: 8rpx; | ||
| 24 | + font-size: 26rpx; | ||
| 25 | + color: #7b8794; | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +.scan-checkin-list-card { | ||
| 29 | + display: flex; | ||
| 30 | + align-items: center; | ||
| 31 | + gap: 20rpx; | ||
| 32 | + padding: 24rpx 28rpx; | ||
| 33 | + margin-bottom: 20rpx; | ||
| 34 | + border-radius: 32rpx; | ||
| 35 | + background: #ffffff; | ||
| 36 | + box-shadow: 0 12rpx 40rpx rgba(15, 23, 42, 0.08); | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +.scan-checkin-list-leading { | ||
| 40 | + width: 68rpx; | ||
| 41 | + height: 68rpx; | ||
| 42 | + border-radius: 50%; | ||
| 43 | + background: rgba(84, 171, 174, 0.12); | ||
| 44 | + display: flex; | ||
| 45 | + align-items: center; | ||
| 46 | + justify-content: center; | ||
| 47 | + color: #3aa9ad; | ||
| 48 | + flex-shrink: 0; | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +.scan-checkin-list-content { | ||
| 52 | + flex: 1; | ||
| 53 | + min-width: 0; | ||
| 54 | +} | ||
| 55 | + | ||
| 56 | +.scan-checkin-list-name { | ||
| 57 | + display: block; | ||
| 58 | + font-size: 32rpx; | ||
| 59 | + line-height: 1.45; | ||
| 60 | + color: #2f3a4a; | ||
| 61 | + word-break: break-all; | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +.scan-checkin-list-note { | ||
| 65 | + display: block; | ||
| 66 | + margin-top: 6rpx; | ||
| 67 | + font-size: 24rpx; | ||
| 68 | + color: #95a0ad; | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +.scan-checkin-list-action { | ||
| 72 | + width: 72rpx; | ||
| 73 | + height: 72rpx; | ||
| 74 | + border-radius: 50%; | ||
| 75 | + background: #36b5bb; | ||
| 76 | + display: flex; | ||
| 77 | + align-items: center; | ||
| 78 | + justify-content: center; | ||
| 79 | + color: #ffffff; | ||
| 80 | + flex-shrink: 0; | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +.scan-checkin-list-floating-button { | ||
| 84 | + position: fixed; | ||
| 85 | + right: 24rpx; | ||
| 86 | + bottom: 120rpx; | ||
| 87 | + width: 120rpx; | ||
| 88 | + height: 120rpx; | ||
| 89 | + border-radius: 50%; | ||
| 90 | + background: #88c055; | ||
| 91 | + display: flex; | ||
| 92 | + flex-direction: column; | ||
| 93 | + align-items: center; | ||
| 94 | + justify-content: center; | ||
| 95 | + box-shadow: 0 12rpx 30rpx rgba(136, 192, 85, 0.35); | ||
| 96 | + z-index: 20; | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +.scan-checkin-list-floating-icon { | ||
| 100 | + width: 40rpx; | ||
| 101 | + height: 40rpx; | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +.scan-checkin-list-floating-text { | ||
| 105 | + margin-top: 10rpx; | ||
| 106 | + font-size: 24rpx; | ||
| 107 | + line-height: 1; | ||
| 108 | + color: #ffffff; | ||
| 109 | +} |
src/pages/ScanCheckinList/index.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2026-05-19 14:40:21 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-05-19 15:03:23 | ||
| 5 | + * @FilePath: /lls_program/src/pages/ScanCheckinList/index.vue | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | +--> | ||
| 8 | +<template> | ||
| 9 | + <view class="scan-checkin-list-page"> | ||
| 10 | + <view class="scan-checkin-list-header"> | ||
| 11 | + <text class="scan-checkin-list-subtitle">请选择一个打卡点,进入详情后完成扫码打卡</text> | ||
| 12 | + </view> | ||
| 13 | + | ||
| 14 | + <view v-for="point in pointList" :key="point.id" class="scan-checkin-list-card"> | ||
| 15 | + <view class="scan-checkin-list-leading"> | ||
| 16 | + <IconFont size="30" name="https://cdn.ipadbiz.cn/lls_prog/icon/check_list_logo.png" /> | ||
| 17 | + </view> | ||
| 18 | + | ||
| 19 | + <view class="scan-checkin-list-content"> | ||
| 20 | + <text class="scan-checkin-list-name">{{ point.code }} {{ point.title }}</text> | ||
| 21 | + </view> | ||
| 22 | + | ||
| 23 | + <view class="scan-checkin-list-action" @click="goToDetail(point)"> | ||
| 24 | + <Scan2 size="20" /> | ||
| 25 | + </view> | ||
| 26 | + </view> | ||
| 27 | + | ||
| 28 | + <view class="scan-checkin-list-floating-button" @click="handleShowBoothMap"> | ||
| 29 | + <IconFont | ||
| 30 | + class="scan-checkin-list-floating-icon" | ||
| 31 | + size="20" | ||
| 32 | + name="https://cdn.ipadbiz.cn/lls_prog/icon/%E5%B1%95%E4%BD%8D%E5%9B%BE@2x.png" | ||
| 33 | + /> | ||
| 34 | + <text class="scan-checkin-list-floating-text">展位图</text> | ||
| 35 | + </view> | ||
| 36 | + </view> | ||
| 37 | +</template> | ||
| 38 | + | ||
| 39 | +<script setup> | ||
| 40 | +import { ref } from 'vue' | ||
| 41 | +import Taro, { useLoad } from '@tarojs/taro' | ||
| 42 | +import { IconFont, Scan2 } from '@nutui/icons-vue-taro' | ||
| 43 | +import './index.less' | ||
| 44 | +import { getMockScanCheckinPoints } from '@/utils/mockQrCheckin' | ||
| 45 | + | ||
| 46 | +const pointList = ref([]) | ||
| 47 | +const activityId = ref('') | ||
| 48 | + | ||
| 49 | +const goToDetail = point => { | ||
| 50 | + const params = new URLSearchParams({ | ||
| 51 | + activityId: activityId.value, | ||
| 52 | + detailId: point.id, | ||
| 53 | + title: point.title, | ||
| 54 | + }) | ||
| 55 | + | ||
| 56 | + Taro.navigateTo({ | ||
| 57 | + url: `/pages/ScanCheckinDetail/index?${params.toString()}`, | ||
| 58 | + }) | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +const handleShowBoothMap = () => { | ||
| 62 | + const params = new URLSearchParams({ | ||
| 63 | + activityId: activityId.value, | ||
| 64 | + }) | ||
| 65 | + | ||
| 66 | + Taro.navigateTo({ | ||
| 67 | + url: `/pages/BoothMapGallery/index?${params.toString()}`, | ||
| 68 | + }) | ||
| 69 | +} | ||
| 70 | + | ||
| 71 | +useLoad(options => { | ||
| 72 | + activityId.value = options.activityId || options.id || '' | ||
| 73 | + pointList.value = getMockScanCheckinPoints(activityId.value) | ||
| 74 | +}) | ||
| 75 | +</script> | ||
| 76 | + | ||
| 77 | +<script> | ||
| 78 | +export default { | ||
| 79 | + name: 'ScanCheckinList', | ||
| 80 | +} | ||
| 81 | +</script> |
src/utils/mockQrCheckin.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-05-19 14:40:21 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-05-19 15:25:07 | ||
| 5 | + * @FilePath: /lls_program/src/utils/mockQrCheckin.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +const mockScanCheckinPoints = [ | ||
| 9 | + { | ||
| 10 | + id: 'point-01', | ||
| 11 | + code: 'W2D01', | ||
| 12 | + title: '泰康之家经营管理有限公司上海分公司', | ||
| 13 | + description: '在点位现场扫码,完成拍照打卡并推荐好物。', | ||
| 14 | + status: '未打卡', | ||
| 15 | + cover: | ||
| 16 | + 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 17 | + discountTitle: '打卡点专属优惠', | ||
| 18 | + discountContent: | ||
| 19 | + '<p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">朋友圈发圈即可获得申园种子纸,10个赞即送扇子,30个赞送冰箱贴。</p><p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">到店或线上购买产品可送优惠券(满100减20,满200减50)。</p>', | ||
| 20 | + }, | ||
| 21 | + { | ||
| 22 | + id: 'point-02', | ||
| 23 | + code: 'W2D02', | ||
| 24 | + title: '泰康之家申园体验区', | ||
| 25 | + description: '完成现场扫码后可查看展区亮点,并领取到店权益。', | ||
| 26 | + status: '未打卡', | ||
| 27 | + cover: | ||
| 28 | + 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 29 | + discountTitle: '打卡点专属优惠', | ||
| 30 | + discountContent: | ||
| 31 | + '<p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">完成打卡后可领取体验区纪念贴纸 1 份。</p><p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">现场咨询指定产品可额外领取优惠券包。</p>', | ||
| 32 | + }, | ||
| 33 | + { | ||
| 34 | + id: 'point-03', | ||
| 35 | + code: 'W2D03', | ||
| 36 | + title: '泰康之家乐龄生活馆', | ||
| 37 | + description: '扫码进入生活馆详情页,完成互动任务后即可点亮本点位。', | ||
| 38 | + status: '已打卡', | ||
| 39 | + cover: | ||
| 40 | + 'https://cdn.ipadbiz.cn/lls_prog/images/check_detail_img.png?imageMogr2/strip/quality/60', | ||
| 41 | + discountTitle: '打卡点专属优惠', | ||
| 42 | + discountContent: | ||
| 43 | + '<p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">现场互动完成后可领取康养手册 1 份。</p><p style="margin: 0 0 20rpx; line-height: 1.8; color: #4b5563; font-size: 30rpx;">指定商品现场下单可享专属立减优惠。</p>', | ||
| 44 | + }, | ||
| 45 | +] | ||
| 46 | + | ||
| 47 | +/** | ||
| 48 | + * 获取扫码打卡点列表 mock 数据 | ||
| 49 | + * @param {string} activityId | ||
| 50 | + * @returns {Array} | ||
| 51 | + */ | ||
| 52 | +export const getMockScanCheckinPoints = activityId => { | ||
| 53 | + return mockScanCheckinPoints.map(item => ({ | ||
| 54 | + ...item, | ||
| 55 | + activityId: activityId || '', | ||
| 56 | + })) | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +/** | ||
| 60 | + * 获取扫码打卡点详情 mock 数据 | ||
| 61 | + * @param {string} detailId | ||
| 62 | + * @returns {Object|null} | ||
| 63 | + */ | ||
| 64 | +export const getMockScanCheckinDetail = detailId => { | ||
| 65 | + const detail = mockScanCheckinPoints.find(item => item.id === detailId) | ||
| 66 | + | ||
| 67 | + if (!detail) { | ||
| 68 | + return null | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + return { | ||
| 72 | + ...detail, | ||
| 73 | + guideText: detail.description, | ||
| 74 | + } | ||
| 75 | +} |
-
Please register or login to post a comment