活动专区页面 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 悦养豆兑换专区 交给 <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)
				}

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' 时券包相关入参直接返回 {}

参数读取:

		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')) {

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()
	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')

4. 三种形态的详细逻辑

4.1 悦养豆兑换专区(zoneType = 6)

页面只做「取数 + 转发」,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)
  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,「更多」下拉展开):
	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:
	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 防抖,图片较多的专区首屏联动可能有一帧偏差。