推进活动列表第一刀与联调回归

This commit is contained in:
2026-04-03 19:33:16 +08:00
parent b09c21c814
commit 527b4c78a9
34 changed files with 3094 additions and 224 deletions

View File

@@ -0,0 +1,220 @@
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import {
getEntryHome,
type BackendCardResult,
type BackendContentBundleSummary,
type BackendPresentationSummary,
} from '../../utils/backendApi'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
const DEFAULT_CHANNEL_CODE = 'mini-demo'
const DEFAULT_CHANNEL_TYPE = 'wechat_mini'
type EventListFilter = 'all' | 'experience'
type EventCardView = {
id: string
eventId: string
titleText: string
subtitleText: string
summaryText: string
statusText: string
timeWindowText: string
ctaText: string
badgeText: string
eventTypeText: string
presentationText: string
contentBundleText: string
coverUrl: string
disabled: boolean
}
type EventsPageData = {
loading: boolean
statusText: string
currentFilter: EventListFilter
cards: EventCardView[]
}
function requireAuthToken(): string | null {
const app = getApp<IAppOption>()
const tokens = app.globalData && app.globalData.backendAuthTokens
? app.globalData.backendAuthTokens
: loadBackendAuthTokens()
return tokens && tokens.accessToken ? tokens.accessToken : null
}
function formatPresentationSummary(summary?: BackendPresentationSummary | null): string {
if (!summary) {
return '当前未声明展示版本'
}
return summary.version || summary.templateKey || summary.presentationId || '当前未声明展示版本'
}
function formatContentBundleSummary(summary?: BackendContentBundleSummary | null): string {
if (!summary) {
return '当前未声明内容包版本'
}
return summary.version || summary.bundleType || summary.bundleId || '当前未声明内容包版本'
}
function buildCardView(card: BackendCardResult): EventCardView {
const eventId = card.event && card.event.id ? card.event.id : ''
const statusText = card.status || card.statusCode || '状态待确认'
const badgeText = card.isDefaultExperience ? '体验' : '活动'
const eventTypeText = card.eventType || '类型待确认'
const subtitleText = card.subtitle || (card.event && card.event.displayName ? card.event.displayName : '')
return {
id: card.id,
eventId,
titleText: card.title || '未命名活动',
subtitleText,
summaryText: card.summary || (card.event && card.event.summary ? card.event.summary : '当前暂无活动摘要'),
statusText,
timeWindowText: card.timeWindow || '时间待公布',
ctaText: card.ctaText || '查看详情',
badgeText,
eventTypeText,
presentationText: formatPresentationSummary(card.currentPresentation),
contentBundleText: formatContentBundleSummary(card.currentContentBundle),
coverUrl: card.coverUrl || '',
disabled: !eventId,
}
}
function applyFilter(cards: BackendCardResult[], filter: EventListFilter): EventCardView[] {
const filtered = filter === 'experience'
? cards.filter((item) => item.isDefaultExperience === true)
: cards
return filtered
.slice()
.sort((left, right) => {
const leftPriority = typeof left.displayPriority === 'number' ? left.displayPriority : 0
const rightPriority = typeof right.displayPriority === 'number' ? right.displayPriority : 0
return rightPriority - leftPriority
})
.map(buildCardView)
}
Page({
data: {
loading: false,
statusText: '准备加载活动列表',
currentFilter: 'all',
cards: [],
} as EventsPageData,
onLoad() {
this.loadCards()
},
onShow() {
this.loadCards()
},
async loadCards() {
const accessToken = requireAuthToken()
if (!accessToken) {
wx.redirectTo({ url: '/pages/login/login' })
return
}
this.setData({
loading: true,
statusText: '正在加载活动列表',
})
try {
const result = await getEntryHome({
baseUrl: loadBackendBaseUrl(),
accessToken,
channelCode: DEFAULT_CHANNEL_CODE,
channelType: DEFAULT_CHANNEL_TYPE,
})
this.applyCards(result.cards || [])
} catch (error) {
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
this.setData({
loading: false,
statusText: `活动列表加载失败:${message}`,
cards: [],
})
}
},
applyCards(cards: BackendCardResult[]) {
reportBackendClientLog({
level: 'info',
category: 'event-cards',
message: 'event cards loaded',
details: {
filter: this.data.currentFilter,
cardCount: cards.length,
experienceCount: cards.filter((item) => item.isDefaultExperience === true).length,
cardEventIds: cards.map((item) => (item.event && item.event.id ? item.event.id : '')),
},
})
const filteredCards = applyFilter(cards, this.data.currentFilter)
this.setData({
loading: false,
statusText: filteredCards.length ? '活动列表加载完成' : '当前没有可显示活动',
cards: filteredCards,
})
const pageInstance = this as unknown as WechatMiniprogram.Page.Instance<EventsPageData, Record<string, never>> & {
rawCards?: BackendCardResult[]
}
pageInstance.rawCards = cards
},
handleSwitchFilter(event: WechatMiniprogram.TouchEvent) {
const filter = event.currentTarget.dataset.filter as EventListFilter | undefined
if (!filter || filter === this.data.currentFilter) {
return
}
const pageInstance = this as unknown as WechatMiniprogram.Page.Instance<EventsPageData, Record<string, never>> & {
rawCards?: BackendCardResult[]
}
const rawCards = pageInstance.rawCards || []
const filteredCards = applyFilter(rawCards, filter)
this.setData({
currentFilter: filter,
statusText: filteredCards.length ? '活动列表加载完成' : '当前筛选下没有活动',
cards: filteredCards,
})
},
handleRefresh() {
this.loadCards()
},
handleOpenCard(event: WechatMiniprogram.TouchEvent) {
const eventId = event.currentTarget.dataset.eventId as string | undefined
reportBackendClientLog({
level: 'info',
category: 'event-cards',
message: 'event card clicked',
eventId: eventId || '',
details: {
clickedEventId: eventId || '',
filter: this.data.currentFilter,
},
})
if (!eventId) {
wx.showToast({
title: '该卡片暂无活动入口',
icon: 'none',
})
return
}
wx.navigateTo({
url: `/pages/event/event?eventId=${encodeURIComponent(eventId)}`,
})
},
})

View File

@@ -0,0 +1,47 @@
<scroll-view class="page" scroll-y>
<view class="shell">
<view class="hero">
<view class="hero__eyebrow">Activity List</view>
<view class="hero__title">活动列表</view>
<view class="hero__desc">当前阶段先做独立列表页第一刀,不重构首页入口区。</view>
</view>
<view class="panel">
<view class="panel__title">筛选</view>
<view class="summary">{{statusText}}</view>
<view class="filters">
<view class="filter-chip {{currentFilter === 'all' ? 'filter-chip--active' : ''}}" data-filter="all" bindtap="handleSwitchFilter">全部</view>
<view class="filter-chip {{currentFilter === 'experience' ? 'filter-chip--active' : ''}}" data-filter="experience" bindtap="handleSwitchFilter">体验</view>
</view>
<view class="actions">
<button class="btn btn--secondary" bindtap="handleRefresh">刷新列表</button>
</view>
</view>
<view class="panel">
<view class="panel__title">活动卡片</view>
<view wx:if="{{!cards.length}}" class="summary">当前筛选下没有活动卡片</view>
<view wx:for="{{cards}}" wx:key="id" class="card {{item.disabled ? 'card--disabled' : ''}}" bindtap="handleOpenCard" data-event-id="{{item.eventId}}">
<image wx:if="{{item.coverUrl}}" class="card__cover" src="{{item.coverUrl}}" mode="aspectFill"></image>
<view class="card__top">
<text class="card__badge">{{item.badgeText}}</text>
<text class="card__type">{{item.eventTypeText}}</text>
</view>
<view class="card__title">{{item.titleText}}</view>
<view wx:if="{{item.subtitleText}}" class="card__subtitle">{{item.subtitleText}}</view>
<view class="card__summary">{{item.summaryText}}</view>
<view class="card__meta-row">
<text class="card__meta">{{item.statusText}}</text>
<text class="card__meta">{{item.timeWindowText}}</text>
</view>
<view class="card__meta-row">
<text class="card__meta">展示:{{item.presentationText}}</text>
</view>
<view class="card__meta-row">
<text class="card__meta">内容:{{item.contentBundleText}}</text>
</view>
<view class="card__cta">{{item.ctaText}}</view>
</view>
</view>
</view>
</scroll-view>

View File

@@ -0,0 +1,186 @@
page {
min-height: 100vh;
background: linear-gradient(180deg, #eff4fb 0%, #e8eff7 100%);
}
.page {
min-height: 100vh;
}
.shell {
display: grid;
gap: 24rpx;
padding: 28rpx 24rpx 40rpx;
}
.hero,
.panel {
display: grid;
gap: 16rpx;
padding: 24rpx;
border-radius: 24rpx;
}
.hero {
background: linear-gradient(135deg, #163a66 0%, #1f5da1 100%);
color: #ffffff;
}
.hero__eyebrow {
font-size: 22rpx;
letter-spacing: 0.16em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.72);
}
.hero__title {
font-size: 40rpx;
font-weight: 700;
}
.hero__desc {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.84);
}
.panel {
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 14rpx 32rpx rgba(40, 63, 95, 0.08);
}
.panel__title {
font-size: 30rpx;
font-weight: 700;
color: #17345a;
}
.summary {
font-size: 24rpx;
line-height: 1.6;
color: #30465f;
}
.filters {
display: flex;
gap: 16rpx;
flex-wrap: wrap;
}
.filter-chip {
min-width: 120rpx;
padding: 14rpx 20rpx;
border-radius: 999rpx;
background: #eef3f8;
color: #50677f;
font-size: 24rpx;
text-align: center;
}
.filter-chip--active {
background: #173d73;
color: #ffffff;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.btn {
margin: 0;
min-height: 76rpx;
padding: 0 24rpx;
line-height: 76rpx;
border-radius: 18rpx;
font-size: 26rpx;
}
.btn::after {
border: 0;
}
.btn--secondary {
background: #dfeaf8;
color: #173d73;
}
.card {
display: grid;
gap: 12rpx;
padding: 22rpx;
border-radius: 22rpx;
background: #f6f9fc;
}
.card--disabled {
opacity: 0.7;
}
.card__cover {
width: 100%;
height: 220rpx;
border-radius: 18rpx;
background: #d7e4f2;
}
.card__top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16rpx;
}
.card__badge {
display: inline-flex;
align-items: center;
min-height: 40rpx;
padding: 0 14rpx;
border-radius: 999rpx;
background: #dce9fb;
color: #173d73;
font-size: 22rpx;
font-weight: 700;
}
.card__type {
font-size: 22rpx;
color: #64748b;
}
.card__title {
font-size: 30rpx;
font-weight: 700;
color: #17345a;
}
.card__subtitle,
.card__summary,
.card__meta,
.card__cta {
font-size: 24rpx;
line-height: 1.6;
}
.card__subtitle {
color: #4f627a;
}
.card__summary {
color: #30465f;
}
.card__meta-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.card__meta {
color: #64748b;
}
.card__cta {
color: #173d73;
font-weight: 700;
}