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)
活动专区聚合页,根据后端返回的 zoneType 渲染三种完全不同的形态:
| zoneType | 形态 | 关键特征 |
|---|---|---|
6 |
悦养豆兑换专区 | 交给 <yueyangZone> 组件渲染:悦养豆余额、券兑换卡片、兑换区选项卡 + 商品列表 |
7 |
分区标签型专区 | 顶部横向标签吸顶 + 分区图/分区商品,标签与页面滚动双向联动 |
其他(3、8 等) |
通用活动专区 | 头图 + 富文本/视频(zoneType 8)+ 工具栏(综合/价格/筛选)+ 瀑布流商品列表 + 上拉加载 |
模板分支:
<yueyangZone ref=yueyangZoneRef v-if="isYueyangZone" :zoneInfo="info" :careBeanRate="careBeanRate"
style="position: relative;z-index: 2;" @goLottery="goLottery" :relationKey="relationKey"></yueyangZone>
<view v-if="isYueyangZone" class="detail-btn" @click="goDetail">
<text>消耗明细</text>
</view>
<view v-else>
if (res.data.zoneType == 6) {
that.setYueyangZone()
} else {
if (that.info.zoneType == 7) {
that.resetPartitionTabs(that.info.partitionTabList)
that.homeTop = that.getHomeTop(0.4)
}
/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' 时券包相关入参直接返回 {} |
参数读取:
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') 等解析结果,并写缓存。
if (options.scene) {
if (this.$Cache.get('zone_scene') && this.$Cache.get('zone_scene') == options.scene && this.$Cache.get(
'zone_scene_param')) {
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()
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 会清掉悦养标签缓存:
onLoad(options) {
uni.removeStorageSync('zoneTab')
页面只做「取数 + 转发」,UI 与交互全在 components/yueyangZone/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:
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)。标签默认选中优先级:
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)
relationKey 命中 → 该标签;zoneTab(uni.setStorageSync('zoneTab', tab.relationKey || index))→ 先按 relationKey 匹配,匹配不到把缓存值当索引;userInfo.level < 3)且存在第 2 个标签 → 索引 1;注意:
onLoad会removeStorageSync('zoneTab'),所以缓存只在「页面未销毁再次 onShow」时有效。
可通过地址参数控制选中标签:?id=xx&relationKey=yyy,值必须是 couponProductTabs[].relationKey(接口返回,代码无硬编码),匹配失败静默回退到第 0 个标签。
「消耗明细」按钮 → /pages/consumption/index?zoneId=xx(该页用 GET care/bean/consume/record/list)。
info.partitionTabList,由 resetPartitionTabs 分成前 4 个(tagList)和剩余(restList,「更多」下拉展开): 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 表示「全部」,滚动到顶部。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')。请求封装: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 |
isZoneShareEnabled(),券包模式下优先用 couponPackStatus.zoneShareSwitch,否则用 info.shareSwitch,严格 === 1: 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 三条分支:isCouponShare 或 couponType == 2 && canShareFriendCoupon())→ buildCouponShareConfig()(调分享链接接口,标题「好友专享优惠券」);!isZoneShareEnabled() → return {}(不自定义内容);path = /pages/zone/index?id=xx&spread=xx&couponZone=false + 归属参数,标题 info.shareTitle,图 info.shareImage。yy_contentPageShare、yy_pageView、yy_marketPageClick(addTrack 为全局 mixin)。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。| 能力 | 逻辑 |
|---|---|
| 倒计时 | 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 |
| 字段 | 含义 |
|---|---|
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 |
本次分享是否为好友券分享 |
couponPackVisit / getZoneGoodList 两个 import 未真正使用(前者被同名方法遮蔽),券包状态完全依赖 getZoneDetail 返回,若后端改为独立接口需补调用。productShareSwitch 会被赋值两次(1505 行 res.data.shareSwitch → 1450 行券包模式覆盖),券包开启时以后者为准。share-switch 用的是 zoneInfo.shareSwitch,而通用专区用的是 productShareSwitch,两者来源不同。activeTab(子组件)初始值字符串 '5',而 switchTab 写入数字索引,模板用 == 宽松比较,标签数 ≥ 6 时第 5 个标签会误高亮。zoneTab 缓存同时可能存 relationKey 或索引两种语义,且为全局 key,跨专区复用时可能选中错误标签(onLoad 会清除一次,但未完全隔离)。relationKey 为 0 / '' 时是 falsy,第一优先级会被跳过;userInfo 未就绪时 userInfo.level < 3 为 false,非会员兜底失效。couponProductTabs 依赖 couponList[0].couponProductTabs,解析失败会走 catch 置空,整块标签栏与商品列表消失。cateId 参数只赋值不使用;getZoneDetail 入参里 where.zoneId 被写了两次(1636/1637 行),实际以 info.id 为准。onPartitionImageLoad)与 scheduleSetAllScrollTop 的 150ms 防抖,图片较多的专区首屏联动可能有一帧偏差。