/** * 公益活动列表页 U31 * * 重构要点(遵循 AGENTS.md 不变量): * - 数据:ActivityStore.fetchActivities / loadMoreActivities(游标分页) * - 列表项:ui-activity-card 组件,"公益免费"为主视觉标识(AGENTS 第 1 条:用户永久免费) * - 跳转:点击卡片 → activity-detail,不在列表页处理报名 * - 单向意向保密:本页不接入互选意向提交(仅在 activity-matches 页由用户主动操作,AGENTS 第 6 条) * - 下拉刷新 + onReachBottom 加载更多 */ import { activityStore, appStore, authStore } from '../../stores'; import { to } from '../../constants'; interface ActivityCategory { code: string; label: string; } /** * 列表项。沿用共享 Activity 类型,并允许服务端附加分类字段用于本地筛选。 * 分类字段在共享类型中尚未收敛,此处显式声明,避免用 any 绕过类型检查。 */ interface ActivityListItem { activityId?: string; id?: string; activityType?: string; category?: string; [key: string]: unknown; } interface ActivityPageData { activities: ActivityListItem[]; loading: boolean; loadingMore: boolean; hasMore: boolean; firstLoaded: boolean; activeCategory: string; categories: ActivityCategory[]; emptyConfig: { title: string; description: string; illustration: string; actions: Array<{ text: string; event: string; variant: string }>; }; } Page({ data: { activities: [] as ActivityListItem[], loading: false, loadingMore: false, hasMore: true, firstLoaded: false, activeCategory: 'all', /** * 分类 Tabs。 * *

Phase 14 修复:code 必须**逐字等于**后端 {@code activities.activity_type} * 的取值(生产约束 {@code chk_activity_type} 白名单)。此前这里写的是 * {@code classroom},而生产白名单里根本没有该值(婚恋课堂是 * {@code relationship_education})—— 点「婚恋课堂」Tab 恒为空白页。 * *

守卫 {@code ActivityConstraintGuardTest#miniappCategoryCodesWithinProductionWhitelist} * 会解析真实 DDL 校验本数组,改错即刻变红。 */ categories: [ { code: 'all', label: '全部' }, { code: 'matchmaking', label: '公益联谊' }, { code: 'interest_social', label: '兴趣社交' }, { code: 'relationship_education', label: '婚恋课堂' }, { code: 'premarital_guidance', label: '婚前辅导' }, { code: 'marriage_custom_culture', label: '婚俗文化' }, { code: 'anti_fraud', label: '反诈宣传' }, ] as ActivityCategory[], /** @type {{title:string, description:string, illustration:string, actions:Array}} */ emptyConfig: { title: '暂无进行中的活动', description: '近期暂无报名中的公益活动,请稍后再来', illustration: 'activity', actions: [ { text: '我的活动', event: 'my', variant: 'secondary' }, ], }, } as ActivityPageData, onLoad() { if (!authStore.state.isLoggedIn) return; (this as any)._unsub = activityStore.bindToPage(this, { activities: 'activities', activitiesHasMore: 'activitiesHasMore', }); this.fetchActivities(true); }, onShow() { if (!authStore.state.isLoggedIn) return; // 从活动详情返回后,若列表为空则刷新(避免详情页报名后列表不同步) if (this.data.firstLoaded && this.data.activities.length === 0) { this.fetchActivities(true); } }, onUnload() { if ((this as any)._unsub) (this as any)._unsub(); }, /** 首次或下拉刷新拉取活动列表 */ async fetchActivities(_first = false) { if (this.data.loading) return; this.setData({ loading: true }); try { await activityStore.fetchActivities(); this.applyActivityFilter(); this.setData({ loading: false, firstLoaded: true, hasMore: activityStore.state.activitiesHasMore, }); } catch (err) { this.setData({ loading: false, firstLoaded: true }); appStore.toastError(err, '加载活动失败'); } }, /** 加载更多(onReachBottom 触发) */ async loadMore() { if (this.data.loadingMore || !this.data.hasMore) return; this.setData({ loadingMore: true }); try { await activityStore.loadMoreActivities(); this.applyActivityFilter(); this.setData({ loadingMore: false, hasMore: activityStore.state.activitiesHasMore, }); } catch (err) { this.setData({ loadingMore: false }); appStore.toastError(err, '加载更多失败'); } }, onPullDownRefresh() { this.fetchActivities(true).then(() => { wx.stopPullDownRefresh(); }); }, onReachBottom() { this.loadMore(); }, onCategoryTap(e: WechatMiniprogram.TouchEvent) { this.setData({ activeCategory: e.currentTarget.dataset.code as string }); this.applyActivityFilter(); }, applyActivityFilter() { const source = (activityStore.state.activities || []) as ActivityListItem[]; const { activeCategory } = this.data; this.setData({ activities: activeCategory === 'all' ? source : source.filter((item) => (item.activityType || item.category) === activeCategory), }); }, /** 点击活动卡片 → 跳转详情 */ onActivityTap(e: any) { const activityId = e.detail.activityId; if (!activityId) return; wx.navigateTo({ url: to.activityDetail(activityId) }); }, /** 空状态:去我的活动 */ onEmptyAction(e: any) { const { event } = e.detail; if (event === 'my') { wx.navigateTo({ url: '/pages/my-activities/my-activities' }); } }, /** 顶部"我的活动"入口 */ onMyActivities() { wx.navigateTo({ url: '/pages/my-activities/my-activities' }); }, });