推进活动系统最小成品闭环与游客体验

This commit is contained in:
2026-04-07 19:05:18 +08:00
parent 1a6008449e
commit 6cd16f08dd
102 changed files with 16087 additions and 3556 deletions

View File

@@ -0,0 +1,131 @@
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
import { getExperienceMaps, getPublicExperienceMaps, type BackendExperienceMapSummary } from '../../utils/backendApi'
import { reportBackendClientLog } from '../../utils/backendClientLogs'
type ExperienceMapCardView = {
mapId: string
placeText: string
mapText: string
summaryText: string
coverUrl: string
defaultExperienceText: string
disabled: boolean
}
type ExperienceMapsPageData = {
loading: boolean
statusText: string
cards: ExperienceMapCardView[]
}
function getAccessToken(): 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 buildCardView(item: BackendExperienceMapSummary): ExperienceMapCardView {
const mapId = item.mapId || ''
const defaultExperienceCount = typeof item.defaultExperienceCount === 'number' ? item.defaultExperienceCount : 0
return {
mapId,
placeText: item.placeName || item.placeId || '地点待确认',
mapText: item.mapName || item.mapId || '地图待确认',
summaryText: item.summary || '当前暂无地图摘要',
coverUrl: item.coverUrl || '',
defaultExperienceText: defaultExperienceCount > 0 ? `默认体验 ${defaultExperienceCount}` : '当前暂无默认体验活动',
disabled: !mapId,
}
}
Page({
data: {
loading: false,
statusText: '准备加载地图体验列表',
cards: [],
} as ExperienceMapsPageData,
onLoad() {
this.loadMaps()
},
onShow() {
this.loadMaps()
},
async loadMaps() {
const accessToken = getAccessToken()
this.setData({
loading: true,
statusText: '正在加载地图体验列表',
})
try {
const baseUrl = loadBackendBaseUrl()
const result = accessToken
? await getExperienceMaps({
baseUrl,
accessToken,
})
: await getPublicExperienceMaps({
baseUrl,
})
reportBackendClientLog({
level: 'info',
category: 'experience-maps',
message: 'experience maps loaded',
details: {
guestMode: !accessToken,
mapCount: result.length,
mapIds: result.map((item) => item.mapId || ''),
mapsWithDefaultExperience: result.filter((item) => {
return typeof item.defaultExperienceCount === 'number' && item.defaultExperienceCount > 0
}).length,
},
})
const cards = result.map(buildCardView)
this.setData({
loading: false,
statusText: cards.length ? '地图体验列表加载完成' : '当前没有可体验地图',
cards,
})
} catch (error) {
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
this.setData({
loading: false,
statusText: `地图体验列表加载失败:${message}`,
cards: [],
})
}
},
handleRefresh() {
this.loadMaps()
},
handleOpenMap(event: WechatMiniprogram.TouchEvent) {
const mapId = event.currentTarget.dataset.mapId as string | undefined
reportBackendClientLog({
level: 'info',
category: 'experience-maps',
message: 'experience map clicked',
details: {
clickedMapId: mapId || '',
},
})
if (!mapId) {
wx.showToast({
title: '该地图暂无详情入口',
icon: 'none',
})
return
}
wx.navigateTo({
url: `/pages/experience-map/experience-map?mapId=${encodeURIComponent(mapId)}`,
})
},
})

View File

@@ -0,0 +1,29 @@
<scroll-view class="page" scroll-y>
<view class="shell">
<view class="hero">
<view class="hero__eyebrow">Map Experience</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="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="mapId" class="card {{item.disabled ? 'card--disabled' : ''}}" bindtap="handleOpenMap" data-map-id="{{item.mapId}}">
<image wx:if="{{item.coverUrl}}" class="card__cover" src="{{item.coverUrl}}" mode="aspectFill"></image>
<view class="card__title">{{item.mapText}}</view>
<view class="card__subtitle">{{item.placeText}}</view>
<view class="card__summary">{{item.summaryText}}</view>
<view class="card__meta">{{item.defaultExperienceText}}</view>
</view>
</view>
</view>
</scroll-view>

View File

@@ -0,0 +1,129 @@
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;
}
.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__title {
font-size: 30rpx;
font-weight: 700;
color: #17345a;
}
.card__subtitle,
.card__summary,
.card__meta {
font-size: 24rpx;
line-height: 1.6;
}
.card__subtitle {
color: #4f627a;
}
.card__summary {
color: #30465f;
}
.card__meta {
color: #64748b;
}