# 活动专区页面 `pages/zone/index` 逻辑说明 > 代码位置:`pages/zone/index.vue`(约 1670 行) > 主要子组件:`components/yueyangZone/index.vue`、`pages/goods_block/index.vue`、`components/home`、`components/popCoupon`、`components/popVideo`、`components/personalAndShareCoupon`、`components/task`、`components/popNewuser` > 所有接口请求前缀:`{HTTP_REQUEST_URL}/api/front/`(`utils/request.js`,`HTTP_REQUEST_URL` 来自 `config/app`) --- ## 1. 页面定位 活动专区聚合页,根据后端返回的 `zoneType` 渲染三种完全不同的形态: | zoneType | 形态 | 关键特征 | |---|---|---| | `6` | 悦养豆兑换专区 | 交给 `` 组件渲染:悦养豆余额、券兑换卡片、兑换区选项卡 + 商品列表 | | `7` | 分区标签型专区 | 顶部横向标签吸顶 + 分区图/分区商品,标签与页面滚动双向联动 | | 其他(`3`、`8` 等) | 通用活动专区 | 头图 + 富文本/视频(zoneType 8)+ 工具栏(综合/价格/筛选)+ 瀑布流商品列表 + 上拉加载 | 模板分支: ```15:20:pages/zone/index.vue 消耗明细 ``` ```1492:1498:pages/zone/index.vue if (res.data.zoneType == 6) { that.setYueyangZone() } else { if (that.info.zoneType == 7) { that.resetPartitionTabs(that.info.partitionTabList) that.homeTop = that.getHomeTop(0.4) } ``` --- ## 2. 入口参数 `/pages/zone/index?id=专区ID` | 参数 | 取值来源 | 作用 | |---|---|---| | `id` | URL / scene 解析结果 | 专区 ID,`getZoneDetail` 的主参数 | | `cateId` | URL / scene | 仅存入 `this.cateId`,当前未参与请求 | | `relationKey` | URL / scene / 好友券链接 | **仅悦养专区生效**,指定默认选中的兑换区标签 | | `shareToken` | URL / scene | 好友券分享 token,`friendCouponShareToken`,参与券包详情入参 | | `spread` | URL | 分销人 ID | | `scene` | 小程序码 | 走 scene 解析分支(`finishSceneParam` 事件 / `zone_scene` 缓存) | | `couponZone` | URL | 值为 `'false'` 时券包相关入参直接返回 `{}` | 参数读取: ```714:719:pages/zone/index.vue let options = currPage.options; this.relationKey = options.relationKey; this.id = options.id; this.cateId = options.cateId; this.friendCouponShareToken = options.shareToken this.urlOption = options ``` scene 分支(`options.scene`): - 命中缓存 `zone_scene` + `zone_scene_param` → 直接 `JSON.parse` 取 `id / shareToken / cateId / relationKey`; - 未命中 → `uni.$once('finishSceneParam')` 等解析结果,并写缓存。 ```728:772:pages/zone/index.vue if (options.scene) { if (this.$Cache.get('zone_scene') && this.$Cache.get('zone_scene') == options.scene && this.$Cache.get( 'zone_scene_param')) { ``` --- ## 3. 启动时序 ``` onLoad ├─ uni.removeStorageSync('zoneTab') 清空悦养标签缓存 ├─ uni.getWindowInfo / getMenuButtonBoundingClientRect → 计算 marTop(header 高度) └─ 监听 careBeanRate、BELONG_READY 等 onShow ├─ 读取页面参数(id / relationKey / shareToken / cateId / scene) ├─ checkUnionId() → 等 unionId(UPDATE_LOGINSTATUS) │ └─ onGetUnion() → 已登录 onLoadFn();未登录 toLogin() + loginFlSuccess 回调 └─ 等待 careBeanRate(updateCareBeanRate)、belong(BELONG_READY) └─ belongInfoReadyFn → checkLoginFn(埋点 yy_pageView + hideLoading) onLoadFn ├─ handsCoupon() 检查可手动领取券(GET hands/coupon/check) └─ getZoneDetail() 专区详情(核心) ├─ updateZoneShareMenu() 分享菜单显隐 ├─ zoneType==6 → setYueyangZone() ├─ zoneType==7 → resetPartitionTabs(partitionTabList) ├─ 其他 → getGoodList() 分页商品 + validAlert() 倒计时 └─ $nextTick → hasSign() + couponPackVisit() ``` ```1114:1133:pages/zone/index.vue checkUnionId() { let that = this; if (that.$store.getters.unionId) { //初始化数据接口login请求完毕第一次login返回了unionId that.onGetUnion(); } else { uni.$once('UPDATE_LOGINSTATUS', that.onGetUnion); } }, onGetUnion() { let that = this; if (that.$store.getters.isLogin) { that.onLoadFn(); } else { toLogin(that); that.loginBackFn = function () { that.onLoadFn(); } uni.$once('loginFlSuccess', that.registerFn); } }, ``` `onLoad` 会清掉悦养标签缓存: ```658:659:pages/zone/index.vue onLoad(options) { uni.removeStorageSync('zoneTab') ``` --- ## 4. 三种形态的详细逻辑 ### 4.1 悦养豆兑换专区(zoneType = 6) 页面只做「取数 + 转发」,UI 与交互全在 `components/yueyangZone/index.vue`。 **页面侧:** ```1207:1219:pages/zone/index.vue setYueyangZone() { // 悦养专区处理 let that = this this.isYueyangZone = true this.$nextTick(() => { that.getUserBean() // 获取专区优惠券 getZoneCoupon(that.info.id).then(res => { const couponData = res.data that.getCouponCount(couponData, 0) }) }) }, ``` `getCouponCount` 递归逐张券查可用数量,遍历完后调用子组件 `resetCoupons`: ```1190:1206:pages/zone/index.vue getCouponCount(couponData, index) { if (!couponData[index]) { this.$refs.yueyangZoneRef.resetCoupons(couponData) return } let that = this getZoneCouponCount({ couponId: couponData[index].id }).then(res => { let coupons = couponData coupons.splice(index, 1, { ...couponData[index], ...res.data }) that.getCouponCount(coupons, index + 1) }) }, ``` **子组件侧:** - 悦养豆余额:`resetCareBean(res2.data.totalCareBeans)`,来自 `GET user/careBean`; - 券卡片:`couponList`(接口 `zone/coupon/list/{id}`),点击「立即兑换」→ `exchangeCoupon` → `POST zone/coupon/exchange`; - 兑换区选项卡:`couponProductTabs = JSON.parse(couponList[0].couponProductTabs)`,**标签数据来自第一张券的字段**; - 商品:`switchTab` 时按 `tab.productIds` 调 `GET products`(`ids` + `page=1&limit=100&zoneId`)。 **标签默认选中优先级:** ```145:161:components/yueyangZone/index.vue const zoneTab = uni.getStorageSync('zoneTab') let index = 0 if (that.relationKey) { index = couponProductTabs.findIndex(item => item.relationKey == that.relationKey) } else if (zoneTab) { index = couponProductTabs.findIndex(item => item.relationKey == zoneTab) if (index == -1) { index = zoneTab } } else if(that.userInfo.level<3 && couponProductTabs[1]){ // 非臻享会员 index=1 } if (index == -1) { index = 0 } that.switchTab(couponProductTabs[index], index) ``` 1. URL `relationKey` 命中 → 该标签; 2. 本地缓存 `zoneTab`(`uni.setStorageSync('zoneTab', tab.relationKey || index)`)→ 先按 `relationKey` 匹配,匹配不到把缓存值当索引; 3. 非臻享会员(`userInfo.level < 3`)且存在第 2 个标签 → 索引 1; 4. 兜底 → 索引 0。 > 注意:`onLoad` 会 `removeStorageSync('zoneTab')`,所以缓存只在「页面未销毁再次 onShow」时有效。 **可通过地址参数控制选中标签**:`?id=xx&relationKey=yyy`,值必须是 `couponProductTabs[].relationKey`(接口返回,代码无硬编码),匹配失败静默回退到第 0 个标签。 「消耗明细」按钮 → `/pages/consumption/index?zoneId=xx`(该页用 `GET care/bean/consume/record/list`)。 ### 4.2 分区标签型专区(zoneType = 7) - 标签数据:`info.partitionTabList`,由 `resetPartitionTabs` 分成前 4 个(`tagList`)和剩余(`restList`,「更多」下拉展开): ```1392:1398:pages/zone/index.vue resetPartitionTabs(partitionTabs) { let that = this that.partitionList = partitionTabs that.tagList = partitionTabs.slice(0, 4) that.restList = partitionTabs.slice(4) that.scheduleSetAllScrollTop() }, ``` - 商品一次性拉取:把所有分区 `productIds` 合并后请求 `GET products`(`limit=100`),结果按 `id` 存入 `partitionProject` 映射,模板中 `v-if="partitionProject[gitem.id]"` 才渲染; - 标签 ↔ 滚动双向联动: - `setAllScrollTop()` 用 `createSelectorQuery` 量出每个 `#partition-item_i` 的绝对偏移存入 `allTop`; - `changeTag(idx)` → `scrollToTarget()` 滚到锚点,期间 `isScrollingByTag = true` 抑制反向联动; - `onPageScroll` → `checkScrollIndex()`(50ms 节流)→ `updateCurrentIndexByScroll()` 反算 `currentIndex`; - 分区图 `@load="onPartitionImageLoad"` 触发重新量高(图片撑高会改变锚点)。 - `currentIndex === -1` 表示「全部」,滚动到顶部。 ### 4.3 通用活动专区(其他 zoneType) - 工具栏:`toolbarArr = [综合(salesOrder), 价格(priceOrder, asc/desc 可切换), 筛选(filter)]`,`changeToolbar` 重置分页后重新请求; - 筛选弹窗:点「筛选」→ `getAllCategory()` → `categoryList` → 多选 `selectMenu` / 清空 / 确定 `menuSubmit`(`where.cateIds = where.cid = 选中 id 串`); - 商品分页:`where = {page, limit: 20, cateIds, cid}`,`getGoodList()` 追加,`onReachBottom` 触底加载,`loadend` 终止; - 排序入参:`where[value] = sort`(如 `priceOrder: 'desc'`)。 --- ## 5. 接口清单 请求封装:`api/store.js`、`api/user.js`,统一 `request.get/post`,大部分带 `noAuth: true`(游客可访问)。 | 调用方法 | HTTP + 路径(相对 `/api/front/`) | 定义位置 | 触发时机 | 关键用途 | |---|---|---|---|---| | `getZoneDetail(id, params)` | `GET store/zone/detail/{id}` | `api/store.js:213` | `onLoadFn`(核心入口) | 返回 `info`:专区名称/头图/`zoneType`/`shareSwitch`/`shareTitle`/`shareImage`/`expireTime`/`startTime`/`partitionTabList`/`couponPackStatus`/`mdayId`/`ruleTitleText`… | | `getProductslist(where)` | `GET products` | `api/store.js:94` | 通用专区商品分页;zoneType 7 按 ids 批量;悦养专区切标签 | 商品列表(`list` / `page` / `totalPage`) | | `getCategoryList()` | `GET category` | `api/store.js:84` | `onShow`、点「筛选」 | 筛选弹窗分类树 `categoryList` | | `checkSign({zoneId})` | `GET store/zone/check/sign` | `api/store.js:239` | `getZoneDetail` 后 | zoneType 3 判断是否已签到,未签到弹窗拦截 | | `getZoneCoupon(zoneId)` | `GET zone/coupon/list/{id}` | `api/store.js:278` | `setYueyangZone` | 悦养专区可兑换券列表(含 `couponProductTabs`) | | `getZoneCouponCount({couponId})` | `GET zone/coupon/user/count` | `api/store.js:284` | 券列表返回后逐张递归 | 每张券的 `usableCount` | | `couponExchange({couponId, zoneId})` | `POST zone/coupon/exchange` | `api/store.js:296` | 子组件点「立即兑换」→「确定」 | 悦养豆兑换券 | | `couponPackSharelink({activityId})` | `POST coupon-pack/share-link?activityId={id}` | `api/store.js:312` | 好友券分享(`onShareAppMessage` 分支) | 返回 `shareToken` / `path` / `shareCoverImage` | | `getUserBean()` | `GET user/careBean` | `api/user.js:10` | 悦养专区 `setYueyangZone` | `totalCareBeans` 悦养豆余额 | | `handsCoupon()` | `GET hands/coupon/check` | `api/user.js:494` | `onLoadFn` | 检查并自动领取手动导入券 | ### 已导入但当前未使用的接口 | 方法 | 路径 | 说明 | |---|---|---| | `getZoneGoodList` | `GET store/zone/product/list/{id}` | 页面 import 了但从未调用,商品统一走 `getProductslist` | | `couponPackVisit` | `POST coupon-pack/visit` | 被页面同名方法 `couponPackVisit()`(1404 行)遮蔽,实际未发请求;券包状态完全取自信 `getZoneDetail` 的 `info.couponPackStatus` | --- ## 6. 分享逻辑 - 判定:`isZoneShareEnabled()`,券包模式下优先用 `couponPackStatus.zoneShareSwitch`,否则用 `info.shareSwitch`,严格 `=== 1`: ```875:893:pages/zone/index.vue isZoneShareEnabled() { if (this.info.couponPackStatus && this.info.couponPackStatus.issued) { return Number(this.info.couponPackStatus.zoneShareSwitch) === 1 } return Number(this.info.shareSwitch) === 1; }, updateZoneShareMenu() { // #ifdef MP if (this.id && !this.isZoneShareEnabled()) { wx.hideShareMenu({ menus: ['shareAppMessage'] }); ``` - `updateZoneShareMenu` 调用点:`getZoneDetail` 成功、`catch`、`onShow`(`String(info.id) !== String(id)` 时先置 `shareSwitch = 0`)。 - `onShareAppMessage` 三条分支: 1. 好友券分享(`isCouponShare` 或 `couponType == 2 && canShareFriendCoupon()`)→ `buildCouponShareConfig()`(调分享链接接口,标题「好友专享优惠券」); 2. `!isZoneShareEnabled()` → `return {}`(不自定义内容); 3. 普通专区分享 → `path = /pages/zone/index?id=xx&spread=xx&couponZone=false` + 归属参数,标题 `info.shareTitle`,图 `info.shareImage`。 - 埋点:`yy_contentPageShare`、`yy_pageView`、`yy_marketPageClick`(`addTrack` 为全局 mixin)。 --- ## 7. 券包展示(couponPackStatus) `getZoneDetail` 后 `couponPackVisit()` 解析 `info.couponPackStatus`: - `autoIssueOnZoneView == 1`(进专区自动发券): - 设置券图片(`friendCouponShortImage` / `selfCouponShortImage` / …,有默认值兜底)、`stockEmpty`、`couponBannerShow`; - `friendCoupon && selfCoupon` → `couponType = 2`(带分享的双券);`shareUnavailable == false && selfCoupon` → `couponType = 3`(不带分享); - `activityId = selfCoupon.activityId`(好友券分享用); - `productShareSwitch = couponPackStatus.productShareSwitch`; - 弹窗 `personalAndShareCouponRef.show(info, friendCouponShareToken)`。 - `autoIssueOnZoneView == 0`(接口送券展示模式):`couponDisplayShow = displayLayout != 'NONE'`; - `SINGLE_CENTER` → `couponType = 3`,`centerCoupon = displayCoupons[0]`; - `DOUBLE_SIDE` → `couponType = 2`,按 `position` 拆成 `leftCoupon` / `rightCoupon`。 - 券状态文案:`selfStatus`(0 已到账 / 1 已使用 / 2 已失效);`friendStatus` / `displayStatus`(`WAIT_USE` / `USED` / `EXPIRED` / `NOT_STARTED`);8.8 / 1.8 元券各有独立图片。 - 好友券分享按钮 `canShareFriendCoupon()`:`friendStatus` 非空且不在 `['RECEIVED','USED','EXPIRED']`;H5 端提示「请在微信小程序中分享给好友」。 - 弹窗关闭回调 `showOtherPop()` → 只弹一次新人弹窗 `popNewuserRef`。 --- ## 8. 其他能力 | 能力 | 逻辑 | |---|---| | 倒计时 | `validAlert(info)` → `countdownTimer(time, 'start'/'end', token)`;未开始显示「后开启」,已开始显示「后结束」,用 `countdownToken` 作废旧定时器链;`expireTime` 为 `{digits, unit}` 数组 | | 签到校验 | `hasSign()`:zoneType 3 先 `checkSign`,未签到弹窗拦截;否则(且 `isFirst`)弹 `popVideoRef`(活动视频) | | 活动视频 | zoneType 8 且有 `uploadVideo`:`videoControls` 控制海报/播放,`bindZoneVideoPlay` 用 `createVideoContext` 播放 | | 富文本 | zoneType 8 且 `info.info` → `rich-text` | | 抽奖 | `info.isLottery && info.lotteryBanner` → `goLottery()`,跳 webview `${WEB_ACTIVITY_BASE}/acmall_lottery/index.html`(带 token/unionId/mdayId),未登录先登录 | | 规则说明 | `info.ruleTitleText && info.ruleContentText` → 点图打开 `personalAndShareCouponRuleRef` | | 视频弹窗关闭 | `handleClosePopVideo()` → 显示 `popCoupon` 且 `isFirst = false` | | 返回 | `returns()`:`uni.$emit('returnEvent')` + `navigateBack`;`returnShow` 控制返回按钮显隐(页面栈长度 > 1) | | 悦养豆汇率 | `careBeanRate` 来自 `store.getters.careBeanRate`(`updateCareBeanRate` 事件),透传给 `GoodBlock` | --- ## 9. 关键状态字段速查 | 字段 | 含义 | |---|---| | `info` | 专区详情(`getZoneDetail` 全量) | | `isYueyangZone` | 是否悦养专区(zoneType 6) | | `zoneType` | 专区形态:6 悦养 / 7 分区标签 / 3 需签到 / 8 视频+富文本 | | `shareSwitch` / `couponPackStatus.zoneShareSwitch` | 专区分享开关(1 可分享) | | `productShareSwitch` | 传给商品卡的分享开关(先取 `res.data.shareSwitch`,券包模式下被 `couponPackStatus.productShareSwitch` 覆盖) | | `partitionList` / `partitionProject` / `currentIndex` / `allTop` | zoneType 7 的分区数据、商品映射、当前标签、锚点偏移 | | `where` / `goodList` / `loadend` / `activeTabIndex` | 通用专区分页条件、列表、是否加载完、当前排序 | | `couponType` / `couponBannerShow` / `couponDisplayShow` / `stockEmpty` | 券包展示形态控制 | | `friendStatus` / `selfStatus` / `activityId` / `friendCouponShareToken` | 好友券状态与分享凭证 | | `isCouponShare` | 本次分享是否为好友券分享 | --- ## 10. 已知问题与注意点 1. `couponPackVisit` / `getZoneGoodList` 两个 import 未真正使用(前者被同名方法遮蔽),券包状态完全依赖 `getZoneDetail` 返回,若后端改为独立接口需补调用。 2. `productShareSwitch` 会被赋值两次(1505 行 `res.data.shareSwitch` → 1450 行券包模式覆盖),券包开启时以后者为准。 3. 悦养专区商品卡的 `share-switch` 用的是 `zoneInfo.shareSwitch`,而通用专区用的是 `productShareSwitch`,两者来源不同。 4. `activeTab`(子组件)初始值字符串 `'5'`,而 `switchTab` 写入数字索引,模板用 `==` 宽松比较,标签数 ≥ 6 时第 5 个标签会误高亮。 5. `zoneTab` 缓存同时可能存 `relationKey` 或索引两种语义,且为全局 key,跨专区复用时可能选中错误标签(`onLoad` 会清除一次,但未完全隔离)。 6. `relationKey` 为 `0` / `''` 时是 falsy,第一优先级会被跳过;`userInfo` 未就绪时 `userInfo.level < 3` 为 false,非会员兜底失效。 7. `couponProductTabs` 依赖 `couponList[0].couponProductTabs`,解析失败会走 `catch` 置空,整块标签栏与商品列表消失。 8. `cateId` 参数只赋值不使用;`getZoneDetail` 入参里 `where.zoneId` 被写了两次(1636/1637 行),实际以 `info.id` 为准。 9. zoneType 7 的锚点依赖图片加载完成(`onPartitionImageLoad`)与 `scheduleSetAllScrollTop` 的 150ms 防抖,图片较多的专区首屏联动可能有一帧偏差。