完善多赛道联调与全局产品架构
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
"pages/login/login",
|
||||
"pages/home/home",
|
||||
"pages/event/event",
|
||||
"pages/event-prepare/event-prepare",
|
||||
"pages/result/result",
|
||||
"pages/results/results",
|
||||
"pages/map/map",
|
||||
"pages/experience-webview/experience-webview",
|
||||
"pages/webview-test/webview-test",
|
||||
|
||||
@@ -6,6 +6,8 @@ App<IAppOption>({
|
||||
telemetryPlayerProfile: null,
|
||||
backendBaseUrl: null,
|
||||
backendAuthTokens: null,
|
||||
pendingResultSnapshot: null,
|
||||
pendingHeartRateAutoConnect: null,
|
||||
},
|
||||
onLaunch() {
|
||||
this.globalData.backendBaseUrl = loadBackendBaseUrl()
|
||||
|
||||
575
miniprogram/pages/event-prepare/event-prepare.ts
Normal file
575
miniprogram/pages/event-prepare/event-prepare.ts
Normal file
@@ -0,0 +1,575 @@
|
||||
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||
import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type BackendEventPlayResult } from '../../utils/backendApi'
|
||||
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
|
||||
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
|
||||
import { HeartRateController } from '../../engine/sensor/heartRateController'
|
||||
|
||||
const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
|
||||
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
|
||||
const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
|
||||
|
||||
type EventPreparePageData = {
|
||||
eventId: string
|
||||
loading: boolean
|
||||
titleText: string
|
||||
summaryText: string
|
||||
releaseText: string
|
||||
actionText: string
|
||||
statusText: string
|
||||
assignmentMode: string
|
||||
variantModeText: string
|
||||
variantSummaryText: string
|
||||
selectedVariantId: string
|
||||
selectedVariantText: string
|
||||
selectableVariants: Array<{
|
||||
id: string
|
||||
name: string
|
||||
routeCodeText: string
|
||||
descriptionText: string
|
||||
selected: boolean
|
||||
}>
|
||||
locationStatusText: string
|
||||
heartRateStatusText: string
|
||||
heartRateDeviceText: string
|
||||
heartRateScanText: string
|
||||
heartRateConnected: boolean
|
||||
showHeartRateDevicePicker: boolean
|
||||
locationPermissionGranted: boolean
|
||||
locationBackgroundPermissionGranted: boolean
|
||||
heartRateDiscoveredDevices: Array<{
|
||||
deviceId: string
|
||||
name: string
|
||||
rssiText: string
|
||||
preferred: boolean
|
||||
connected: boolean
|
||||
}>
|
||||
mockSourceStatusText: string
|
||||
}
|
||||
|
||||
function formatAssignmentMode(mode?: string | null): string {
|
||||
if (mode === 'manual') {
|
||||
return '手动选择'
|
||||
}
|
||||
if (mode === 'random') {
|
||||
return '随机分配'
|
||||
}
|
||||
if (mode === 'server-assigned') {
|
||||
return '后台指定'
|
||||
}
|
||||
return '默认单赛道'
|
||||
}
|
||||
|
||||
function formatVariantSummary(result: BackendEventPlayResult): string {
|
||||
const variants = result.play.courseVariants || []
|
||||
if (!variants.length) {
|
||||
return '当前未声明额外赛道版本,启动时按默认赛道进入。'
|
||||
}
|
||||
|
||||
const preview = variants.map((item) => {
|
||||
const title = item.routeCode || item.name
|
||||
return item.selectable === false ? `${title}(固定)` : title
|
||||
}).join(' / ')
|
||||
|
||||
if (result.play.assignmentMode === 'manual') {
|
||||
return `当前活动支持 ${variants.length} 条赛道。本阶段前端先展示赛道信息,最终绑定以后端 launch 返回为准:${preview}`
|
||||
}
|
||||
|
||||
if (result.play.assignmentMode === 'random') {
|
||||
return `当前活动支持 ${variants.length} 条赛道,进入地图前由后端随机绑定:${preview}`
|
||||
}
|
||||
|
||||
if (result.play.assignmentMode === 'server-assigned') {
|
||||
return `当前活动赛道由后台预先指定:${preview}`
|
||||
}
|
||||
|
||||
return preview
|
||||
}
|
||||
|
||||
function resolveSelectedVariantId(
|
||||
currentVariantId: string,
|
||||
assignmentMode?: string | null,
|
||||
variants?: BackendCourseVariantSummary[] | null,
|
||||
): string {
|
||||
if (assignmentMode !== 'manual' || !variants || !variants.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const selectable = variants.filter((item) => item.selectable !== false)
|
||||
if (!selectable.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const currentStillExists = selectable.some((item) => item.id === currentVariantId)
|
||||
if (currentVariantId && currentStillExists) {
|
||||
return currentVariantId
|
||||
}
|
||||
|
||||
return selectable[0].id
|
||||
}
|
||||
|
||||
function buildSelectableVariants(
|
||||
selectedVariantId: string,
|
||||
assignmentMode?: string | null,
|
||||
variants?: BackendCourseVariantSummary[] | null,
|
||||
) {
|
||||
if (assignmentMode !== 'manual' || !variants || !variants.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return variants
|
||||
.filter((item) => item.selectable !== false)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
routeCodeText: item.routeCode || '默认编码',
|
||||
descriptionText: item.description || '暂无赛道说明',
|
||||
selected: item.id === selectedVariantId,
|
||||
}))
|
||||
}
|
||||
|
||||
let prepareHeartRateController: HeartRateController | null = null
|
||||
|
||||
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 loadPreferredHeartRateDeviceName(): string | null {
|
||||
try {
|
||||
const stored = wx.getStorageSync(PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY)
|
||||
if (!stored || typeof stored !== 'object') {
|
||||
return null
|
||||
}
|
||||
const normalized = stored as { name?: unknown }
|
||||
return typeof normalized.name === 'string' && normalized.name.trim().length > 0
|
||||
? normalized.name.trim()
|
||||
: '心率带'
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function loadStoredMockChannelId(): string {
|
||||
try {
|
||||
const stored = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
|
||||
if (typeof stored === 'string' && stored.trim().length > 0) {
|
||||
return stored.trim()
|
||||
}
|
||||
} catch (_error) {
|
||||
return 'default'
|
||||
}
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function loadMockAutoConnectEnabled(): boolean {
|
||||
try {
|
||||
return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
eventId: '',
|
||||
loading: false,
|
||||
titleText: '开始前准备',
|
||||
summaryText: '未加载',
|
||||
releaseText: '--',
|
||||
actionText: '--',
|
||||
statusText: '待加载',
|
||||
assignmentMode: '',
|
||||
variantModeText: '--',
|
||||
variantSummaryText: '--',
|
||||
selectedVariantId: '',
|
||||
selectedVariantText: '当前无需手动指定赛道',
|
||||
selectableVariants: [],
|
||||
locationStatusText: '待进入地图后校验定位权限与实时精度',
|
||||
heartRateStatusText: '局前心率带连接入口待接入,本轮先保留骨架',
|
||||
heartRateDeviceText: '--',
|
||||
heartRateScanText: '未扫描',
|
||||
heartRateConnected: false,
|
||||
showHeartRateDevicePicker: false,
|
||||
locationPermissionGranted: false,
|
||||
locationBackgroundPermissionGranted: false,
|
||||
heartRateDiscoveredDevices: [],
|
||||
mockSourceStatusText: '模拟源调试仍在地图页调试面板中使用',
|
||||
} as EventPreparePageData,
|
||||
|
||||
onLoad(query: { eventId?: string }) {
|
||||
const eventId = query && query.eventId ? decodeURIComponent(query.eventId) : ''
|
||||
if (!eventId) {
|
||||
this.setData({
|
||||
statusText: '缺少 eventId',
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setData({ eventId })
|
||||
this.ensurePrepareHeartRateController()
|
||||
this.refreshPreparationDeviceState()
|
||||
this.loadEventPlay(eventId)
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.refreshPreparationDeviceState()
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
if (prepareHeartRateController) {
|
||||
prepareHeartRateController.destroy()
|
||||
prepareHeartRateController = null
|
||||
}
|
||||
},
|
||||
|
||||
async loadEventPlay(eventId?: string) {
|
||||
const targetEventId = eventId || this.data.eventId
|
||||
const accessToken = getAccessToken()
|
||||
if (!accessToken) {
|
||||
wx.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
loading: true,
|
||||
statusText: '正在加载局前准备信息',
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await getEventPlay({
|
||||
baseUrl: loadBackendBaseUrl(),
|
||||
eventId: targetEventId,
|
||||
accessToken,
|
||||
})
|
||||
this.applyEventPlay(result)
|
||||
} catch (error) {
|
||||
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
|
||||
this.setData({
|
||||
loading: false,
|
||||
statusText: `局前准备加载失败:${message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
applyEventPlay(result: BackendEventPlayResult) {
|
||||
const selectedVariantId = resolveSelectedVariantId(
|
||||
this.data.selectedVariantId,
|
||||
result.play.assignmentMode,
|
||||
result.play.courseVariants,
|
||||
)
|
||||
const selectableVariants = buildSelectableVariants(
|
||||
selectedVariantId,
|
||||
result.play.assignmentMode,
|
||||
result.play.courseVariants,
|
||||
)
|
||||
const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
|
||||
this.setData({
|
||||
loading: false,
|
||||
titleText: `${result.event.displayName} / 开始前准备`,
|
||||
summaryText: result.event.summary || '暂无活动简介',
|
||||
releaseText: result.resolvedRelease
|
||||
? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}`
|
||||
: '当前无可用 release',
|
||||
actionText: `${result.play.primaryAction} / ${result.play.reason}`,
|
||||
statusText: result.play.canLaunch ? '准备完成,可进入地图' : '当前不可启动',
|
||||
assignmentMode: result.play.assignmentMode || '',
|
||||
variantModeText: formatAssignmentMode(result.play.assignmentMode),
|
||||
variantSummaryText: formatVariantSummary(result),
|
||||
selectedVariantId,
|
||||
selectedVariantText: selectedVariant
|
||||
? `${selectedVariant.name} / ${selectedVariant.routeCodeText}`
|
||||
: '当前无需手动指定赛道',
|
||||
selectableVariants,
|
||||
})
|
||||
},
|
||||
|
||||
refreshPreparationDeviceState() {
|
||||
this.refreshLocationPermissionStatus()
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
this.refreshMockSourcePreparationStatus()
|
||||
},
|
||||
|
||||
ensurePrepareHeartRateController() {
|
||||
if (prepareHeartRateController) {
|
||||
return prepareHeartRateController
|
||||
}
|
||||
|
||||
prepareHeartRateController = new HeartRateController({
|
||||
onHeartRate: () => {},
|
||||
onStatus: (message) => {
|
||||
this.setData({
|
||||
heartRateStatusText: message,
|
||||
})
|
||||
},
|
||||
onError: (message) => {
|
||||
this.setData({
|
||||
heartRateStatusText: message,
|
||||
})
|
||||
},
|
||||
onConnectionChange: (connected, deviceName) => {
|
||||
this.setData({
|
||||
heartRateConnected: connected,
|
||||
heartRateDeviceText: connected ? (deviceName || '心率带') : (deviceName || '--'),
|
||||
})
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
onDeviceListChange: (devices) => {
|
||||
this.setData({
|
||||
heartRateScanText: devices.length ? `已发现 ${devices.length} 个设备` : '未扫描',
|
||||
heartRateDiscoveredDevices: devices.map((device) => ({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
rssiText: typeof device.rssi === 'number' ? `${device.rssi} dBm` : 'RSSI --',
|
||||
preferred: !!device.isPreferred,
|
||||
connected: !!prepareHeartRateController
|
||||
&& !!prepareHeartRateController.currentDeviceId
|
||||
&& prepareHeartRateController.currentDeviceId === device.deviceId
|
||||
&& prepareHeartRateController.connected,
|
||||
})),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return prepareHeartRateController
|
||||
},
|
||||
|
||||
refreshLocationPermissionStatus() {
|
||||
wx.getSetting({
|
||||
success: (result) => {
|
||||
const authSetting = result && result.authSetting
|
||||
? result.authSetting as Record<string, boolean | undefined>
|
||||
: {}
|
||||
const hasForeground = authSetting['scope.userLocation'] === true
|
||||
const hasBackground = authSetting['scope.userLocationBackground'] === true
|
||||
let locationStatusText = '未请求定位权限'
|
||||
if (hasForeground && hasBackground) {
|
||||
locationStatusText = '已授权前后台定位'
|
||||
} else if (hasForeground) {
|
||||
locationStatusText = '已授权前台定位'
|
||||
} else if (authSetting['scope.userLocation'] === false) {
|
||||
locationStatusText = '定位权限被拒绝'
|
||||
}
|
||||
this.setData({
|
||||
locationStatusText,
|
||||
locationPermissionGranted: hasForeground,
|
||||
locationBackgroundPermissionGranted: hasBackground,
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
this.setData({
|
||||
locationStatusText: '无法读取定位权限状态',
|
||||
locationPermissionGranted: false,
|
||||
locationBackgroundPermissionGranted: false,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
handleRequestLocationPermission() {
|
||||
wx.authorize({
|
||||
scope: 'scope.userLocation',
|
||||
success: () => {
|
||||
this.refreshLocationPermissionStatus()
|
||||
wx.showToast({
|
||||
title: '前台定位已授权',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
this.refreshLocationPermissionStatus()
|
||||
wx.showToast({
|
||||
title: '请在设置中开启定位权限',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
handleOpenLocationSettings() {
|
||||
wx.openSetting({
|
||||
success: () => {
|
||||
this.refreshLocationPermissionStatus()
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({
|
||||
title: '无法打开设置面板',
|
||||
icon: 'none',
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
refreshHeartRatePreparationStatus() {
|
||||
const controller = this.ensurePrepareHeartRateController()
|
||||
const preferredDeviceName = loadPreferredHeartRateDeviceName()
|
||||
this.setData({
|
||||
heartRateStatusText: controller.connected
|
||||
? '局前心率带已连接'
|
||||
: preferredDeviceName
|
||||
? `已记住首选设备:${preferredDeviceName}`
|
||||
: '未设置首选设备,可在此连接或进入地图后连接',
|
||||
heartRateDeviceText: controller.currentDeviceName || preferredDeviceName || '--',
|
||||
heartRateScanText: controller.scanning
|
||||
? '扫描中'
|
||||
: (controller.discoveredDevices.length ? `已发现 ${controller.discoveredDevices.length} 个设备` : '未扫描'),
|
||||
heartRateConnected: controller.connected,
|
||||
heartRateDiscoveredDevices: controller.discoveredDevices.map((device) => ({
|
||||
deviceId: device.deviceId,
|
||||
name: device.name,
|
||||
rssiText: typeof device.rssi === 'number' ? `${device.rssi} dBm` : 'RSSI --',
|
||||
preferred: !!device.isPreferred,
|
||||
connected: !!controller.currentDeviceId && controller.currentDeviceId === device.deviceId && controller.connected,
|
||||
})),
|
||||
})
|
||||
},
|
||||
|
||||
refreshMockSourcePreparationStatus() {
|
||||
const channelId = loadStoredMockChannelId()
|
||||
const autoConnect = loadMockAutoConnectEnabled()
|
||||
this.setData({
|
||||
mockSourceStatusText: autoConnect
|
||||
? `自动连接已开启 / 通道 ${channelId}`
|
||||
: `自动连接未开启 / 通道 ${channelId}`,
|
||||
})
|
||||
},
|
||||
|
||||
handleRefresh() {
|
||||
this.loadEventPlay()
|
||||
},
|
||||
|
||||
handleBack() {
|
||||
wx.navigateBack()
|
||||
},
|
||||
|
||||
handlePrepareHeartRateConnect() {
|
||||
const controller = this.ensurePrepareHeartRateController()
|
||||
controller.startScanAndConnect()
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
|
||||
handleOpenHeartRateDevicePicker() {
|
||||
const controller = this.ensurePrepareHeartRateController()
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: true,
|
||||
})
|
||||
if (!controller.scanning) {
|
||||
controller.startScanAndConnect()
|
||||
}
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
|
||||
handleCloseHeartRateDevicePicker() {
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: false,
|
||||
})
|
||||
},
|
||||
|
||||
handlePrepareHeartRateDeviceConnect(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
|
||||
const deviceId = event.currentTarget.dataset.deviceId
|
||||
if (!deviceId) {
|
||||
return
|
||||
}
|
||||
const controller = this.ensurePrepareHeartRateController()
|
||||
controller.connectToDiscoveredDevice(deviceId)
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: false,
|
||||
})
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
|
||||
handlePrepareHeartRateDisconnect() {
|
||||
if (!prepareHeartRateController) {
|
||||
return
|
||||
}
|
||||
prepareHeartRateController.disconnect()
|
||||
this.setData({
|
||||
heartRateConnected: false,
|
||||
})
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
|
||||
handlePrepareHeartRateClearPreferred() {
|
||||
const controller = this.ensurePrepareHeartRateController()
|
||||
controller.clearPreferredDevice()
|
||||
this.refreshHeartRatePreparationStatus()
|
||||
},
|
||||
|
||||
handleSelectVariant(event: WechatMiniprogram.BaseEvent<{ variantId?: string }>) {
|
||||
const variantId = event.currentTarget.dataset.variantId
|
||||
if (!variantId) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectableVariants = this.data.selectableVariants.map((item) => ({
|
||||
...item,
|
||||
selected: item.id === variantId,
|
||||
}))
|
||||
const selectedVariant = selectableVariants.find((item) => item.id === variantId) || null
|
||||
this.setData({
|
||||
selectedVariantId: variantId,
|
||||
selectedVariantText: selectedVariant
|
||||
? `${selectedVariant.name} / ${selectedVariant.routeCodeText}`
|
||||
: '当前无需手动指定赛道',
|
||||
selectableVariants,
|
||||
})
|
||||
},
|
||||
|
||||
async handleLaunch() {
|
||||
const accessToken = getAccessToken()
|
||||
if (!accessToken) {
|
||||
wx.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.data.locationPermissionGranted) {
|
||||
this.setData({
|
||||
statusText: '进入地图前请先完成定位授权',
|
||||
})
|
||||
wx.showToast({
|
||||
title: '请先授权定位',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
statusText: '正在创建 session 并进入地图',
|
||||
})
|
||||
|
||||
try {
|
||||
const app = getApp<IAppOption>()
|
||||
if (app.globalData) {
|
||||
const pendingDeviceName = prepareHeartRateController && prepareHeartRateController.currentDeviceName
|
||||
? prepareHeartRateController.currentDeviceName
|
||||
: loadPreferredHeartRateDeviceName()
|
||||
app.globalData.pendingHeartRateAutoConnect = {
|
||||
enabled: !!pendingDeviceName,
|
||||
deviceName: pendingDeviceName || null,
|
||||
}
|
||||
}
|
||||
if (prepareHeartRateController) {
|
||||
prepareHeartRateController.destroy()
|
||||
prepareHeartRateController = null
|
||||
}
|
||||
const result = await launchEvent({
|
||||
baseUrl: loadBackendBaseUrl(),
|
||||
eventId: this.data.eventId,
|
||||
accessToken,
|
||||
variantId: this.data.assignmentMode === 'manual' ? this.data.selectedVariantId : undefined,
|
||||
clientType: 'wechat',
|
||||
deviceKey: 'mini-dev-device-001',
|
||||
})
|
||||
const envelope = adaptBackendLaunchResultToEnvelope(result)
|
||||
wx.navigateTo({
|
||||
url: prepareMapPageUrlForLaunch(envelope),
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
|
||||
this.setData({
|
||||
statusText: `launch 失败:${message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
105
miniprogram/pages/event-prepare/event-prepare.wxml
Normal file
105
miniprogram/pages/event-prepare/event-prepare.wxml
Normal file
@@ -0,0 +1,105 @@
|
||||
<scroll-view class="page" scroll-y>
|
||||
<view class="shell">
|
||||
<view class="hero">
|
||||
<view class="hero__eyebrow">Prepare</view>
|
||||
<view class="hero__title">{{titleText}}</view>
|
||||
<view class="hero__desc">{{summaryText}}</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel__title">活动与发布</view>
|
||||
<view class="summary">Release:{{releaseText}}</view>
|
||||
<view class="summary">主动作:{{actionText}}</view>
|
||||
<view class="summary">状态:{{statusText}}</view>
|
||||
<view class="summary">赛道模式:{{variantModeText}}</view>
|
||||
<view class="summary">赛道摘要:{{variantSummaryText}}</view>
|
||||
<view class="summary">当前选择:{{selectedVariantText}}</view>
|
||||
</view>
|
||||
|
||||
<view class="panel" wx:if="{{assignmentMode === 'manual' && selectableVariants.length}}">
|
||||
<view class="panel__title">赛道选择</view>
|
||||
<view class="summary">当前活动要求手动指定赛道。这里的选择会随 launch 一起带给后端,最终绑定以后端返回为准。</view>
|
||||
<view class="variant-list">
|
||||
<view wx:for="{{selectableVariants}}" wx:key="id" class="variant-card {{item.selected ? 'variant-card--active' : ''}}" data-variant-id="{{item.id}}" bindtap="handleSelectVariant">
|
||||
<view class="variant-card__main">
|
||||
<view class="variant-card__title-row">
|
||||
<text class="variant-card__name">{{item.name}}</text>
|
||||
<text class="variant-card__badge" wx:if="{{item.selected}}">已选中</text>
|
||||
</view>
|
||||
<text class="variant-card__meta">{{item.routeCodeText}}</text>
|
||||
<text class="variant-card__meta">{{item.descriptionText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel__title">设备准备</view>
|
||||
<view class="summary">这一页现在负责局前设备准备。定位权限先在这里确认,心率带支持先连后进图,地图内仍保留局中快速重连入口。</view>
|
||||
<view class="row">
|
||||
<view class="row__label">定位状态</view>
|
||||
<view class="row__value">{{locationStatusText}}</view>
|
||||
</view>
|
||||
<view class="summary" wx:if="{{locationPermissionGranted && !locationBackgroundPermissionGranted}}">已完成前台定位授权;如果后续需要后台持续定位,请在系统设置中补齐后台权限。</view>
|
||||
<view class="actions">
|
||||
<button class="btn btn--secondary" bindtap="handleRequestLocationPermission">申请定位权限</button>
|
||||
<button class="btn btn--ghost" bindtap="handleOpenLocationSettings">打开系统设置</button>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="row__label">心率带</view>
|
||||
<view class="row__value">{{heartRateStatusText}}</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="row__label">当前设备</view>
|
||||
<view class="row__value">{{heartRateDeviceText}}</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="row__label">扫描状态</view>
|
||||
<view class="row__value">{{heartRateScanText}}</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<view class="row__label">模拟源</view>
|
||||
<view class="row__value">{{mockSourceStatusText}}</view>
|
||||
</view>
|
||||
<view class="actions">
|
||||
<button class="btn btn--secondary" bindtap="handleOpenHeartRateDevicePicker">选择设备</button>
|
||||
<button class="btn btn--ghost" bindtap="handlePrepareHeartRateConnect">重新扫描</button>
|
||||
<button class="btn btn--ghost" bindtap="handlePrepareHeartRateDisconnect">断开连接</button>
|
||||
<button class="btn btn--ghost" bindtap="handlePrepareHeartRateClearPreferred">清除首选</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel__title">开始比赛</view>
|
||||
<view class="summary">这一页先承担局前准备壳子,后面会继续接定位权限、心率带局前连接和设备检查。</view>
|
||||
<view class="actions">
|
||||
<button class="btn btn--secondary" bindtap="handleBack">返回活动页</button>
|
||||
<button class="btn btn--ghost" bindtap="handleRefresh">刷新</button>
|
||||
<button class="btn btn--primary" bindtap="handleLaunch">进入地图</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showHeartRateDevicePicker}}" class="picker-mask" bindtap="handleCloseHeartRateDevicePicker"></view>
|
||||
<view wx:if="{{showHeartRateDevicePicker}}" class="picker-sheet">
|
||||
<view class="picker-sheet__header">
|
||||
<view class="picker-sheet__title">选择心率带设备</view>
|
||||
<button class="picker-sheet__close" bindtap="handleCloseHeartRateDevicePicker">关闭</button>
|
||||
</view>
|
||||
<view class="summary">扫描状态:{{heartRateScanText}}</view>
|
||||
<view wx:if="{{!heartRateDiscoveredDevices.length}}" class="summary">当前还没有发现设备,可先点“重新扫描”。</view>
|
||||
<view wx:if="{{heartRateDiscoveredDevices.length}}" class="device-list">
|
||||
<view wx:for="{{heartRateDiscoveredDevices}}" wx:key="deviceId" class="device-card">
|
||||
<view class="device-card__main">
|
||||
<view class="device-card__title-row">
|
||||
<text class="device-card__name">{{item.name}}</text>
|
||||
<text class="device-card__badge" wx:if="{{item.preferred}}">首选</text>
|
||||
<text class="device-card__badge device-card__badge--active" wx:if="{{item.connected}}">已连接</text>
|
||||
</view>
|
||||
<text class="device-card__meta">{{item.rssiText}}</text>
|
||||
</view>
|
||||
<button class="btn {{item.connected ? 'btn--ghost' : 'btn--secondary'}} device-card__action" data-device-id="{{item.deviceId}}" bindtap="handlePrepareHeartRateDeviceConnect">{{item.connected ? '已连接' : '连接'}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
276
miniprogram/pages/event-prepare/event-prepare.wxss
Normal file
276
miniprogram/pages/event-prepare/event-prepare.wxss
Normal file
@@ -0,0 +1,276 @@
|
||||
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);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.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,
|
||||
.row__label,
|
||||
.row__value {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
color: #30465f;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 10rpx 0;
|
||||
border-bottom: 2rpx solid #edf2f7;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.row__value {
|
||||
max-width: 70%;
|
||||
text-align: right;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: grid;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.variant-list {
|
||||
display: grid;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.variant-card {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f6f9fc;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.variant-card--active {
|
||||
background: #edf5ff;
|
||||
border-color: #4f86c9;
|
||||
}
|
||||
|
||||
.variant-card__main {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.variant-card__title-row {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.variant-card__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.variant-card__badge {
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #dff3e8;
|
||||
color: #1f6a45;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.variant-card__meta {
|
||||
font-size: 22rpx;
|
||||
color: #5c7288;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.picker-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(10, 22, 38, 0.42);
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.picker-sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 31;
|
||||
display: grid;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 24rpx 36rpx;
|
||||
border-top-left-radius: 28rpx;
|
||||
border-top-right-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 -14rpx 36rpx rgba(22, 43, 71, 0.18);
|
||||
}
|
||||
|
||||
.picker-sheet__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-sheet__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.picker-sheet__close {
|
||||
margin: 0;
|
||||
min-height: 60rpx;
|
||||
padding: 0 18rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
background: #eef3f8;
|
||||
color: #455a72;
|
||||
}
|
||||
|
||||
.picker-sheet__close::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f6f9fc;
|
||||
}
|
||||
|
||||
.device-card__main {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.device-card__title-row {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.device-card__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.device-card__badge {
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e1ecfa;
|
||||
color: #35567d;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.device-card__badge--active {
|
||||
background: #dff3e8;
|
||||
color: #1f6a45;
|
||||
}
|
||||
|
||||
.device-card__meta {
|
||||
font-size: 22rpx;
|
||||
color: #5c7288;
|
||||
}
|
||||
|
||||
.device-card__action {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
min-height: 76rpx;
|
||||
padding: 0 24rpx;
|
||||
line-height: 76rpx;
|
||||
border-radius: 18rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.btn::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: #173d73;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn--secondary {
|
||||
background: #dfeaf8;
|
||||
color: #173d73;
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: #ffffff;
|
||||
color: #52657d;
|
||||
border: 2rpx solid #d8e2ec;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||
import { getEventPlay, launchEvent, type BackendEventPlayResult } from '../../utils/backendApi'
|
||||
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
|
||||
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
|
||||
import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi'
|
||||
|
||||
type EventPageData = {
|
||||
eventId: string
|
||||
@@ -11,6 +9,33 @@ type EventPageData = {
|
||||
releaseText: string
|
||||
actionText: string
|
||||
statusText: string
|
||||
variantModeText: string
|
||||
variantSummaryText: string
|
||||
}
|
||||
|
||||
function formatAssignmentMode(mode?: string | null): string {
|
||||
if (mode === 'manual') {
|
||||
return '手动选择'
|
||||
}
|
||||
if (mode === 'random') {
|
||||
return '随机分配'
|
||||
}
|
||||
if (mode === 'server-assigned') {
|
||||
return '后台指定'
|
||||
}
|
||||
return '默认单赛道'
|
||||
}
|
||||
|
||||
function formatVariantSummary(result: BackendEventPlayResult): string {
|
||||
const variants = result.play.courseVariants || []
|
||||
if (!variants.length) {
|
||||
return '当前未声明额外赛道版本'
|
||||
}
|
||||
|
||||
const selectable = variants.filter((item) => item.selectable !== false)
|
||||
const preview = variants.slice(0, 3).map((item) => item.routeCode || item.name).join(' / ')
|
||||
const suffix = variants.length > 3 ? ' / ...' : ''
|
||||
return `${variants.length} 条赛道,可选 ${selectable.length} 条:${preview}${suffix}`
|
||||
}
|
||||
|
||||
function getAccessToken(): string | null {
|
||||
@@ -30,6 +55,8 @@ Page({
|
||||
releaseText: '--',
|
||||
actionText: '--',
|
||||
statusText: '待加载',
|
||||
variantModeText: '--',
|
||||
variantSummaryText: '--',
|
||||
} as EventPageData,
|
||||
|
||||
onLoad(query: { eventId?: string }) {
|
||||
@@ -83,6 +110,8 @@ Page({
|
||||
: '当前无可用 release',
|
||||
actionText: `${result.play.primaryAction} / ${result.play.reason}`,
|
||||
statusText: result.play.canLaunch ? '可启动' : '当前不可启动',
|
||||
variantModeText: formatAssignmentMode(result.play.assignmentMode),
|
||||
variantSummaryText: formatVariantSummary(result),
|
||||
})
|
||||
},
|
||||
|
||||
@@ -91,33 +120,8 @@ Page({
|
||||
},
|
||||
|
||||
async handleLaunch() {
|
||||
const accessToken = getAccessToken()
|
||||
if (!accessToken) {
|
||||
wx.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
statusText: '正在创建 session 并进入地图',
|
||||
wx.navigateTo({
|
||||
url: `/pages/event-prepare/event-prepare?eventId=${encodeURIComponent(this.data.eventId)}`,
|
||||
})
|
||||
|
||||
try {
|
||||
const result = await launchEvent({
|
||||
baseUrl: loadBackendBaseUrl(),
|
||||
eventId: this.data.eventId,
|
||||
accessToken,
|
||||
clientType: 'wechat',
|
||||
deviceKey: 'mini-dev-device-001',
|
||||
})
|
||||
const envelope = adaptBackendLaunchResultToEnvelope(result)
|
||||
wx.navigateTo({
|
||||
url: prepareMapPageUrlForLaunch(envelope),
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
|
||||
this.setData({
|
||||
statusText: `launch 失败:${message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
<view class="summary">Release:{{releaseText}}</view>
|
||||
<view class="summary">主动作:{{actionText}}</view>
|
||||
<view class="summary">状态:{{statusText}}</view>
|
||||
<view class="summary">赛道模式:{{variantModeText}}</view>
|
||||
<view class="summary">赛道摘要:{{variantSummaryText}}</view>
|
||||
<view class="actions">
|
||||
<button class="btn btn--secondary" bindtap="handleRefresh">刷新</button>
|
||||
<button class="btn btn--primary" bindtap="handleLaunch">开始比赛</button>
|
||||
<button class="btn btn--primary" bindtap="handleLaunch">前往准备页</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -15,6 +15,17 @@ type HomePageData = {
|
||||
cards: BackendCardResult[]
|
||||
}
|
||||
|
||||
function formatSessionSummary(session?: BackendEntryHomeResult['ongoingSession'] | null): string {
|
||||
if (!session) {
|
||||
return '无'
|
||||
}
|
||||
|
||||
const title = session.eventName || session.eventDisplayName || session.eventId || session.id || session.sessionId
|
||||
const status = session.status || session.sessionStatus || '--'
|
||||
const route = session.routeCode || session.variantName || '默认赛道'
|
||||
return `${title} / ${status} / ${route}`
|
||||
}
|
||||
|
||||
function requireAuthToken(): string | null {
|
||||
const app = getApp<IAppOption>()
|
||||
const tokens = app.globalData && app.globalData.backendAuthTokens
|
||||
@@ -79,12 +90,8 @@ Page({
|
||||
userNameText: result.user.nickname || result.user.publicId || result.user.id,
|
||||
tenantText: `${result.tenant.name} (${result.tenant.code})`,
|
||||
channelText: `${result.channel.displayName} / ${result.channel.code}`,
|
||||
ongoingSessionText: result.ongoingSession
|
||||
? `${result.ongoingSession.eventName || result.ongoingSession.eventDisplayName || result.ongoingSession.eventId || result.ongoingSession.id || result.ongoingSession.sessionId} / ${result.ongoingSession.status || result.ongoingSession.sessionStatus}`
|
||||
: '无',
|
||||
recentSessionText: result.recentSession
|
||||
? `${result.recentSession.eventName || result.recentSession.eventDisplayName || result.recentSession.eventId || result.recentSession.id || result.recentSession.sessionId} / ${result.recentSession.status || result.recentSession.sessionStatus}`
|
||||
: '无',
|
||||
ongoingSessionText: formatSessionSummary(result.ongoingSession),
|
||||
recentSessionText: formatSessionSummary(result.recentSession),
|
||||
cards: result.cards || [],
|
||||
})
|
||||
},
|
||||
@@ -110,7 +117,7 @@ Page({
|
||||
|
||||
handleOpenRecentResult() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/result/result',
|
||||
url: '/pages/results/results',
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ type MapPageData = MapEngineViewState & {
|
||||
showGameInfoPanel: boolean
|
||||
showResultScene: boolean
|
||||
showSystemSettingsPanel: boolean
|
||||
showHeartRateDevicePicker: boolean
|
||||
showCenterScaleRuler: boolean
|
||||
showPunchHintBanner: boolean
|
||||
punchHintFxClass: string
|
||||
@@ -92,6 +93,7 @@ type MapPageData = MapEngineViewState & {
|
||||
resultSceneHeroLabel: string
|
||||
resultSceneHeroValue: string
|
||||
resultSceneRows: MapEngineGameInfoRow[]
|
||||
resultSceneCountdownText: string
|
||||
panelTimerText: string
|
||||
panelTimerMode: 'elapsed' | 'countdown'
|
||||
panelMileageText: string
|
||||
@@ -157,6 +159,7 @@ const PUNCH_HINT_AUTO_HIDE_MS = 30000
|
||||
const PUNCH_HINT_FX_DURATION_MS = 420
|
||||
const PUNCH_HINT_HAPTIC_GAP_MS = 2400
|
||||
const SESSION_RECOVERY_PERSIST_INTERVAL_MS = 5000
|
||||
const RESULT_EXIT_REDIRECT_DELAY_MS = 3000
|
||||
let currentGameLaunchEnvelope: GameLaunchEnvelope = getDemoGameLaunchEnvelope()
|
||||
let mapEngine: MapEngine | null = null
|
||||
let stageCanvasAttached = false
|
||||
@@ -172,6 +175,8 @@ let panelMileageFxTimer = 0
|
||||
let panelSpeedFxTimer = 0
|
||||
let panelHeartRateFxTimer = 0
|
||||
let sessionRecoveryPersistTimer = 0
|
||||
let resultExitRedirectTimer = 0
|
||||
let resultExitCountdownTimer = 0
|
||||
let lastPunchHintHapticAt = 0
|
||||
let currentSystemSettingsConfig: SystemSettingsConfig | undefined
|
||||
let currentRemoteMapConfig: RemoteMapConfig | undefined
|
||||
@@ -179,6 +184,8 @@ let systemSettingsLockLifetimeActive = false
|
||||
let syncedBackendSessionStartId = ''
|
||||
let syncedBackendSessionFinishId = ''
|
||||
let shouldAutoRestoreRecoverySnapshot = false
|
||||
let redirectedToResultPage = false
|
||||
let pendingHeartRateSwitchDeviceName: string | null = null
|
||||
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
|
||||
const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
|
||||
let lastCenterScaleRulerStablePatch: Pick<
|
||||
@@ -469,6 +476,34 @@ function clearSessionRecoveryPersistTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearResultExitRedirectTimer() {
|
||||
if (resultExitRedirectTimer) {
|
||||
clearTimeout(resultExitRedirectTimer)
|
||||
resultExitRedirectTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function clearResultExitCountdownTimer() {
|
||||
if (resultExitCountdownTimer) {
|
||||
clearInterval(resultExitCountdownTimer)
|
||||
resultExitCountdownTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
function navigateAwayFromMapAfterCancel() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack({
|
||||
delta: 1,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.redirectTo({
|
||||
url: '/pages/home/home',
|
||||
})
|
||||
}
|
||||
|
||||
function hasExplicitLaunchOptions(options?: MapPageLaunchOptions | null): boolean {
|
||||
if (!options) {
|
||||
return false
|
||||
@@ -776,11 +811,12 @@ function buildEmptyResultSceneSnapshot(): MapEngineResultSnapshot {
|
||||
|
||||
Page({
|
||||
data: {
|
||||
showDebugPanel: false,
|
||||
showGameInfoPanel: false,
|
||||
showResultScene: false,
|
||||
showSystemSettingsPanel: false,
|
||||
showCenterScaleRuler: false,
|
||||
showDebugPanel: false,
|
||||
showGameInfoPanel: false,
|
||||
showResultScene: false,
|
||||
showSystemSettingsPanel: false,
|
||||
showHeartRateDevicePicker: false,
|
||||
showCenterScaleRuler: false,
|
||||
statusBarHeight: 0,
|
||||
topInsetHeight: 12,
|
||||
hudPanelIndex: 0,
|
||||
@@ -798,6 +834,7 @@ Page({
|
||||
resultSceneHeroLabel: '本局用时',
|
||||
resultSceneHeroValue: '--',
|
||||
resultSceneRows: buildEmptyResultSceneSnapshot().rows,
|
||||
resultSceneCountdownText: '',
|
||||
panelTimerText: '00:00:00',
|
||||
panelTimerMode: 'elapsed',
|
||||
panelMileageText: '0m',
|
||||
@@ -927,8 +964,11 @@ Page({
|
||||
|
||||
onLoad(options: MapPageLaunchOptions) {
|
||||
clearSessionRecoveryPersistTimer()
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
syncedBackendSessionStartId = ''
|
||||
syncedBackendSessionFinishId = ''
|
||||
redirectedToResultPage = false
|
||||
shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
|
||||
currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
|
||||
if (!hasExplicitLaunchOptions(options)) {
|
||||
@@ -959,6 +999,7 @@ Page({
|
||||
const includeRulerFields = this.data.showCenterScaleRuler
|
||||
let shouldSyncRuntimeSystemSettings = false
|
||||
let nextLockLifetimeActive = isSystemSettingsLockLifetimeActive()
|
||||
let heartRateSwitchToastText = ''
|
||||
const nextData: Partial<MapPageData> = filterDebugOnlyPatch({
|
||||
...nextPatch,
|
||||
}, includeDebugFields, includeRulerFields)
|
||||
@@ -1054,6 +1095,8 @@ Page({
|
||||
: this.data.animationLevel
|
||||
let shouldSyncBackendSessionStart = false
|
||||
let backendSessionFinishStatus: 'finished' | 'failed' | null = null
|
||||
let shouldOpenResultExitPrompt = false
|
||||
let resultPageSnapshot: MapEngineResultSnapshot | null = null
|
||||
|
||||
if (nextAnimationLevel === 'lite') {
|
||||
clearHudFxTimer('timer')
|
||||
@@ -1112,13 +1155,24 @@ Page({
|
||||
shouldSyncRuntimeSystemSettings = true
|
||||
clearSessionRecoverySnapshot()
|
||||
clearSessionRecoveryPersistTimer()
|
||||
this.syncResultSceneSnapshot()
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
resultPageSnapshot = mapEngine ? mapEngine.getResultSceneSnapshot() : null
|
||||
nextData.showResultScene = true
|
||||
nextData.showDebugPanel = false
|
||||
nextData.showGameInfoPanel = false
|
||||
nextData.showSystemSettingsPanel = false
|
||||
clearGameInfoPanelSyncTimer()
|
||||
backendSessionFinishStatus = nextPatch.gameSessionStatus === 'finished' ? 'finished' : 'failed'
|
||||
shouldOpenResultExitPrompt = true
|
||||
if (resultPageSnapshot) {
|
||||
nextData.resultSceneTitle = resultPageSnapshot.title
|
||||
nextData.resultSceneSubtitle = resultPageSnapshot.subtitle
|
||||
nextData.resultSceneHeroLabel = resultPageSnapshot.heroLabel
|
||||
nextData.resultSceneHeroValue = resultPageSnapshot.heroValue
|
||||
nextData.resultSceneRows = resultPageSnapshot.rows
|
||||
}
|
||||
nextData.resultSceneCountdownText = '3 秒后自动进入成绩页'
|
||||
} else if (
|
||||
nextPatch.gameSessionStatus !== this.data.gameSessionStatus
|
||||
&& nextPatch.gameSessionStatus === 'idle'
|
||||
@@ -1128,6 +1182,8 @@ Page({
|
||||
shouldSyncRuntimeSystemSettings = true
|
||||
clearSessionRecoverySnapshot()
|
||||
clearSessionRecoveryPersistTimer()
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
} else if (
|
||||
nextPatch.gameSessionStatus !== this.data.gameSessionStatus
|
||||
&& nextPatch.gameSessionStatus === 'running'
|
||||
@@ -1138,6 +1194,19 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
pendingHeartRateSwitchDeviceName
|
||||
&& nextPatch.heartRateConnected === true
|
||||
&& typeof nextPatch.heartRateDeviceText === 'string'
|
||||
) {
|
||||
const connectedDeviceName = nextPatch.heartRateDeviceText.trim()
|
||||
if (connectedDeviceName && connectedDeviceName === pendingHeartRateSwitchDeviceName) {
|
||||
heartRateSwitchToastText = `已切换到 ${connectedDeviceName}`
|
||||
nextData.statusText = `已切换心率带:${connectedDeviceName}`
|
||||
pendingHeartRateSwitchDeviceName = null
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(nextData).length || Object.keys(derivedPatch).length) {
|
||||
this.setData({
|
||||
...nextData,
|
||||
@@ -1152,9 +1221,20 @@ Page({
|
||||
if (backendSessionFinishStatus) {
|
||||
this.syncBackendSessionFinish(backendSessionFinishStatus)
|
||||
}
|
||||
if (shouldSyncRuntimeSystemSettings) {
|
||||
this.applyRuntimeSystemSettings(nextLockLifetimeActive)
|
||||
}
|
||||
if (shouldOpenResultExitPrompt && resultPageSnapshot) {
|
||||
this.stashPendingResultSnapshot(resultPageSnapshot)
|
||||
this.presentResultExitPrompt()
|
||||
}
|
||||
if (heartRateSwitchToastText) {
|
||||
wx.showToast({
|
||||
title: `${heartRateSwitchToastText},并设为首选设备`,
|
||||
icon: 'none',
|
||||
duration: 1800,
|
||||
})
|
||||
}
|
||||
if (shouldSyncRuntimeSystemSettings) {
|
||||
this.applyRuntimeSystemSettings(nextLockLifetimeActive)
|
||||
}
|
||||
if (this.data.showGameInfoPanel) {
|
||||
this.scheduleGameInfoPanelSnapshotSync()
|
||||
}
|
||||
@@ -1169,6 +1249,10 @@ Page({
|
||||
if (backendSessionFinishStatus) {
|
||||
this.syncBackendSessionFinish(backendSessionFinishStatus)
|
||||
}
|
||||
if (shouldOpenResultExitPrompt && resultPageSnapshot) {
|
||||
this.stashPendingResultSnapshot(resultPageSnapshot)
|
||||
this.presentResultExitPrompt()
|
||||
}
|
||||
if (shouldSyncRuntimeSystemSettings) {
|
||||
this.applyRuntimeSystemSettings(nextLockLifetimeActive)
|
||||
}
|
||||
@@ -1209,6 +1293,7 @@ Page({
|
||||
...buildResolvedSystemSettingsPatch(systemSettingsState),
|
||||
showDebugPanel: false,
|
||||
showGameInfoPanel: false,
|
||||
showResultScene: false,
|
||||
showSystemSettingsPanel: false,
|
||||
statusBarHeight,
|
||||
topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
|
||||
@@ -1218,6 +1303,12 @@ Page({
|
||||
gameInfoSubtitle: '未开始',
|
||||
gameInfoLocalRows: [],
|
||||
gameInfoGlobalRows: buildEmptyGameInfoSnapshot().globalRows,
|
||||
resultSceneTitle: '本局结果',
|
||||
resultSceneSubtitle: '未开始',
|
||||
resultSceneHeroLabel: '本局用时',
|
||||
resultSceneHeroValue: '--',
|
||||
resultSceneRows: buildEmptyResultSceneSnapshot().rows,
|
||||
resultSceneCountdownText: '',
|
||||
panelTimerText: '00:00:00',
|
||||
panelTimerMode: 'elapsed',
|
||||
panelTimerFxClass: '',
|
||||
@@ -1349,6 +1440,18 @@ Page({
|
||||
stageCanvasAttached = false
|
||||
this.measureStageAndCanvas()
|
||||
this.loadGameLaunchEnvelope(currentGameLaunchEnvelope)
|
||||
const app = getApp<IAppOption>()
|
||||
const pendingHeartRateAutoConnect = app.globalData ? app.globalData.pendingHeartRateAutoConnect : null
|
||||
if (pendingHeartRateAutoConnect && pendingHeartRateAutoConnect.enabled && mapEngine) {
|
||||
const pendingDeviceName = pendingHeartRateAutoConnect.deviceName || '心率带'
|
||||
app.globalData.pendingHeartRateAutoConnect = null
|
||||
mapEngine.handleConnectHeartRate()
|
||||
this.setData({
|
||||
statusText: `正在自动连接局前设备:${pendingDeviceName}`,
|
||||
heartRateStatusText: `正在自动连接 ${pendingDeviceName}`,
|
||||
heartRateDeviceText: pendingDeviceName,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -1360,6 +1463,8 @@ Page({
|
||||
|
||||
onHide() {
|
||||
this.persistSessionRecoverySnapshot()
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
if (mapEngine) {
|
||||
mapEngine.handleAppHide()
|
||||
}
|
||||
@@ -1368,6 +1473,8 @@ Page({
|
||||
onUnload() {
|
||||
this.persistSessionRecoverySnapshot()
|
||||
clearSessionRecoveryPersistTimer()
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
syncedBackendSessionStartId = ''
|
||||
syncedBackendSessionFinishId = ''
|
||||
clearGameInfoPanelSyncTimer()
|
||||
@@ -1388,6 +1495,7 @@ Page({
|
||||
systemSettingsLockLifetimeActive = false
|
||||
currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
|
||||
shouldAutoRestoreRecoverySnapshot = false
|
||||
redirectedToResultPage = false
|
||||
stageCanvasAttached = false
|
||||
},
|
||||
|
||||
@@ -1528,6 +1636,57 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
stashPendingResultSnapshot(snapshot: MapEngineResultSnapshot) {
|
||||
const app = getApp<IAppOption>()
|
||||
if (app.globalData) {
|
||||
app.globalData.pendingResultSnapshot = snapshot
|
||||
}
|
||||
},
|
||||
|
||||
redirectToResultPage() {
|
||||
if (redirectedToResultPage) {
|
||||
return
|
||||
}
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
redirectedToResultPage = true
|
||||
const sessionContext = getCurrentBackendSessionContext()
|
||||
const resultUrl = sessionContext
|
||||
? `/pages/result/result?sessionId=${encodeURIComponent(sessionContext.sessionId)}`
|
||||
: '/pages/result/result'
|
||||
wx.redirectTo({
|
||||
url: resultUrl,
|
||||
})
|
||||
},
|
||||
|
||||
presentResultExitPrompt() {
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
|
||||
let remainingSeconds = Math.ceil(RESULT_EXIT_REDIRECT_DELAY_MS / 1000)
|
||||
this.setData({
|
||||
showResultScene: true,
|
||||
resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
|
||||
})
|
||||
|
||||
resultExitCountdownTimer = setInterval(() => {
|
||||
remainingSeconds -= 1
|
||||
if (remainingSeconds <= 0) {
|
||||
clearResultExitCountdownTimer()
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
resultSceneCountdownText: `${remainingSeconds} 秒后自动进入成绩页`,
|
||||
})
|
||||
}, 1000) as unknown as number
|
||||
|
||||
resultExitRedirectTimer = setTimeout(() => {
|
||||
resultExitRedirectTimer = 0
|
||||
this.redirectToResultPage()
|
||||
}, RESULT_EXIT_REDIRECT_DELAY_MS) as unknown as number
|
||||
},
|
||||
|
||||
restoreRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
|
||||
systemSettingsLockLifetimeActive = true
|
||||
this.applyRuntimeSystemSettings(true)
|
||||
@@ -2052,20 +2211,53 @@ Page({
|
||||
},
|
||||
|
||||
handleConnectHeartRate() {
|
||||
if (mapEngine) {
|
||||
mapEngine.handleConnectHeartRate()
|
||||
}
|
||||
},
|
||||
|
||||
handleDisconnectHeartRate() {
|
||||
if (mapEngine) {
|
||||
mapEngine.handleDisconnectHeartRate()
|
||||
if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
if (mapEngine) {
|
||||
mapEngine.handleConnectHeartRate()
|
||||
}
|
||||
},
|
||||
|
||||
handleOpenHeartRateDevicePicker() {
|
||||
if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: true,
|
||||
})
|
||||
if (mapEngine) {
|
||||
mapEngine.handleConnectHeartRate()
|
||||
}
|
||||
},
|
||||
|
||||
handleCloseHeartRateDevicePicker() {
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: false,
|
||||
})
|
||||
},
|
||||
|
||||
handleDisconnectHeartRate() {
|
||||
if (this.data.lockHeartRateDevice || this.data.heartRateSourceMode !== 'real') {
|
||||
return
|
||||
}
|
||||
if (mapEngine) {
|
||||
mapEngine.handleDisconnectHeartRate()
|
||||
}
|
||||
},
|
||||
|
||||
handleConnectHeartRateDevice(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
|
||||
if (mapEngine && event.currentTarget && event.currentTarget.dataset && event.currentTarget.dataset.deviceId) {
|
||||
mapEngine.handleConnectHeartRateDevice(event.currentTarget.dataset.deviceId)
|
||||
const targetDeviceId = event.currentTarget.dataset.deviceId
|
||||
const targetDevice = this.data.heartRateDiscoveredDevices.find((item) => item.deviceId === targetDeviceId)
|
||||
pendingHeartRateSwitchDeviceName = targetDevice ? targetDevice.name : null
|
||||
mapEngine.handleConnectHeartRateDevice(targetDeviceId)
|
||||
this.setData({
|
||||
showHeartRateDevicePicker: false,
|
||||
statusText: targetDevice
|
||||
? `正在切换到 ${targetDevice.name}`
|
||||
: '正在切换心率带设备',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2174,9 +2366,21 @@ Page({
|
||||
cancelText: '取消',
|
||||
success: (result) => {
|
||||
if (result.confirm && mapEngine) {
|
||||
clearResultExitRedirectTimer()
|
||||
clearResultExitCountdownTimer()
|
||||
this.syncBackendSessionFinish('cancelled')
|
||||
clearSessionRecoverySnapshot()
|
||||
clearSessionRecoveryPersistTimer()
|
||||
systemSettingsLockLifetimeActive = false
|
||||
mapEngine.handleForceExitGame()
|
||||
wx.showToast({
|
||||
title: '已退出当前对局',
|
||||
icon: 'none',
|
||||
duration: 1000,
|
||||
})
|
||||
setTimeout(() => {
|
||||
navigateAwayFromMapAfterCancel()
|
||||
}, 180)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -2312,24 +2516,11 @@ Page({
|
||||
handleResultSceneTap() {},
|
||||
|
||||
handleCloseResultScene() {
|
||||
this.setData({
|
||||
showResultScene: false,
|
||||
})
|
||||
this.redirectToResultPage()
|
||||
},
|
||||
|
||||
handleRestartFromResult() {
|
||||
if (!mapEngine) {
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showResultScene: false,
|
||||
}, () => {
|
||||
if (mapEngine) {
|
||||
systemSettingsLockLifetimeActive = true
|
||||
this.applyRuntimeSystemSettings(true)
|
||||
mapEngine.handleStartGame()
|
||||
}
|
||||
})
|
||||
this.redirectToResultPage()
|
||||
},
|
||||
|
||||
handleOpenSystemSettingsPanel() {
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
|
||||
<view class="result-scene-modal" wx:if="{{showResultScene}}" bindtap="handleCloseResultScene">
|
||||
<view class="result-scene-modal__dialog" catchtap="handleResultSceneTap">
|
||||
<view class="result-scene-modal__eyebrow">RESULT</view>
|
||||
<view class="result-scene-modal__eyebrow">FINISH</view>
|
||||
<view class="result-scene-modal__title">{{resultSceneTitle}}</view>
|
||||
<view class="result-scene-modal__subtitle">{{resultSceneSubtitle}}</view>
|
||||
|
||||
@@ -340,9 +340,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-scene-modal__countdown">{{resultSceneCountdownText}}</view>
|
||||
|
||||
<view class="result-scene-modal__actions">
|
||||
<view class="result-scene-modal__action result-scene-modal__action--secondary" bindtap="handleCloseResultScene">返回地图</view>
|
||||
<view class="result-scene-modal__action result-scene-modal__action--primary" bindtap="handleRestartFromResult">再来一局</view>
|
||||
<view class="result-scene-modal__action result-scene-modal__action--primary" bindtap="handleRestartFromResult">查看成绩</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -726,13 +727,31 @@
|
||||
<view class="debug-section__header-row">
|
||||
<view class="debug-section__header-main">
|
||||
<view class="debug-section__title">16. 心率设备</view>
|
||||
<view class="debug-section__desc">清除已记住的首选心率带设备,下次重新选择</view>
|
||||
<view class="debug-section__desc">局内正式入口,可快速更换、重连或断开当前心率带</view>
|
||||
</view>
|
||||
<view class="debug-section__lock {{lockHeartRateDevice ? 'debug-section__lock--active' : ''}}">
|
||||
<text class="debug-section__lock-text">{{lockHeartRateDevice ? '配置锁定' : '允许调整'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-panel__row">
|
||||
<text class="info-panel__label">当前状态</text>
|
||||
<text class="info-panel__value">{{heartRateStatusText}}{{heartRateSourceMode !== 'real' ? ' · 当前为模拟模式' : ''}}</text>
|
||||
</view>
|
||||
<view class="info-panel__row info-panel__row--stack">
|
||||
<text class="info-panel__label">当前设备</text>
|
||||
<text class="info-panel__value">{{heartRateDeviceText}}</text>
|
||||
</view>
|
||||
<view class="info-panel__row" wx:if="{{heartRateSourceMode === 'real'}}">
|
||||
<text class="info-panel__label">扫描状态</text>
|
||||
<text class="info-panel__value">{{heartRateScanText}}</text>
|
||||
</view>
|
||||
<view class="summary" wx:if="{{heartRateSourceMode !== 'real'}}">当前为模拟心率模式,如需连接真实心率带,请先在调试面板切回“真实心率”。</view>
|
||||
<view class="control-row" wx:if="{{heartRateSourceMode === 'real'}}">
|
||||
<view class="control-chip control-chip--secondary {{lockHeartRateDevice ? 'control-chip--disabled' : ''}}" bindtap="handleOpenHeartRateDevicePicker">更换心率带</view>
|
||||
<view class="control-chip {{heartRateConnected ? 'control-chip--active' : 'control-chip--secondary'}} {{lockHeartRateDevice ? 'control-chip--disabled' : ''}}" bindtap="handleConnectHeartRate">{{heartRateConnected ? '重新扫描' : '连接心率带'}}</view>
|
||||
<view class="control-chip control-chip--secondary {{lockHeartRateDevice ? 'control-chip--disabled' : ''}}" bindtap="handleDisconnectHeartRate">断开心率带</view>
|
||||
</view>
|
||||
<view class="control-row">
|
||||
<view class="control-chip control-chip--secondary {{lockHeartRateDevice ? 'control-chip--disabled' : ''}}" bindtap="handleClearPreferredHeartRateDevice">清除首选设备</view>
|
||||
</view>
|
||||
@@ -897,25 +916,10 @@
|
||||
<text class="info-panel__label">HR Scan</text>
|
||||
<text class="info-panel__value">{{heartRateScanText}}</text>
|
||||
</view>
|
||||
<view class="debug-device-list" wx:if="{{heartRateSourceMode === 'real' && heartRateDiscoveredDevices.length}}">
|
||||
<view class="debug-device-card" wx:for="{{heartRateDiscoveredDevices}}" wx:key="deviceId">
|
||||
<view class="debug-device-card__main">
|
||||
<view class="debug-device-card__title-row">
|
||||
<text class="debug-device-card__name">{{item.name}}</text>
|
||||
<text class="debug-device-card__badge" wx:if="{{item.preferred}}">首选</text>
|
||||
</view>
|
||||
<text class="debug-device-card__meta">{{item.rssiText}}</text>
|
||||
</view>
|
||||
<view class="debug-device-card__action {{item.connected ? 'debug-device-card__action--active' : ''}}" data-device-id="{{item.deviceId}}" bindtap="handleConnectHeartRateDevice">{{item.connected ? '已连接' : '连接'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="control-row" wx:if="{{heartRateSourceMode === 'real'}}">
|
||||
<view class="control-chip {{heartRateConnected ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleConnectHeartRate">{{heartRateConnected ? '心率带已连接' : '连接心率带'}}</view>
|
||||
<view class="control-chip control-chip--secondary" bindtap="handleDisconnectHeartRate">断开心率带</view>
|
||||
</view>
|
||||
<view class="control-row" wx:if="{{heartRateSourceMode === 'real'}}">
|
||||
<view class="control-chip control-chip--secondary" bindtap="handleClearPreferredHeartRateDevice">清除首选</view>
|
||||
<view class="control-chip {{heartRateConnected ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleConnectHeartRate">{{heartRateConnected ? '重新扫描' : '连接心率带'}}</view>
|
||||
</view>
|
||||
<view class="summary" wx:if="{{heartRateSourceMode === 'real'}}">正式用户入口已放到系统设置;这里仅保留心率源切换与开发调试能力。</view>
|
||||
<view class="info-panel__row info-panel__row--stack" wx:if="{{heartRateSourceMode === 'mock'}}">
|
||||
<text class="info-panel__label">心率模拟状态</text>
|
||||
<text class="info-panel__value">{{mockHeartRateBridgeStatusText}}</text>
|
||||
@@ -1169,9 +1173,32 @@
|
||||
<text class="info-panel__value">{{networkFetchCount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showHeartRateDevicePicker}}" class="picker-mask" bindtap="handleCloseHeartRateDevicePicker"></view>
|
||||
<view wx:if="{{showHeartRateDevicePicker}}" class="picker-sheet">
|
||||
<view class="picker-sheet__header">
|
||||
<view class="picker-sheet__title">选择心率带设备</view>
|
||||
<button class="picker-sheet__close" bindtap="handleCloseHeartRateDevicePicker">关闭</button>
|
||||
</view>
|
||||
<view class="summary">扫描状态:{{heartRateScanText}}</view>
|
||||
<view wx:if="{{!heartRateDiscoveredDevices.length}}" class="summary">当前还没有发现设备,可先点“重新扫描”。</view>
|
||||
<view wx:if="{{heartRateDiscoveredDevices.length}}" class="device-list">
|
||||
<view wx:for="{{heartRateDiscoveredDevices}}" wx:key="deviceId" class="device-card">
|
||||
<view class="device-card__main">
|
||||
<view class="device-card__title-row">
|
||||
<text class="device-card__name">{{item.name}}</text>
|
||||
<text class="device-card__badge" wx:if="{{item.preferred}}">首选</text>
|
||||
<text class="device-card__badge device-card__badge--active" wx:if="{{item.connected}}">已连接</text>
|
||||
</view>
|
||||
<text class="device-card__meta">{{item.rssiText}}</text>
|
||||
</view>
|
||||
<button class="btn {{item.connected ? 'btn--ghost' : 'btn--secondary'}} device-card__action" data-device-id="{{item.deviceId}}" bindtap="handleConnectHeartRateDevice">{{item.connected ? '已连接' : '连接'}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
@@ -1458,6 +1458,14 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.result-scene-modal__countdown {
|
||||
margin-top: 18rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
color: #6a826f;
|
||||
}
|
||||
|
||||
.result-scene-modal__actions {
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
@@ -1781,6 +1789,143 @@
|
||||
color: #f7fbf2;
|
||||
}
|
||||
|
||||
.picker-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 22, 38, 0.42);
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
.picker-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 91;
|
||||
display: grid;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx 24rpx 36rpx;
|
||||
border-top-left-radius: 28rpx;
|
||||
border-top-right-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 -14rpx 36rpx rgba(22, 43, 71, 0.18);
|
||||
}
|
||||
|
||||
.picker-sheet__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.picker-sheet__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.picker-sheet__close {
|
||||
margin: 0;
|
||||
min-height: 60rpx;
|
||||
padding: 0 18rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
background: #eef3f8;
|
||||
color: #455a72;
|
||||
}
|
||||
|
||||
.picker-sheet__close::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
color: #30465f;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
display: grid;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f6f9fc;
|
||||
}
|
||||
|
||||
.device-card__main {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.device-card__title-row {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.device-card__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
|
||||
.device-card__badge {
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e1ecfa;
|
||||
color: #35567d;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.device-card__badge--active {
|
||||
background: #dff3e8;
|
||||
color: #1f6a45;
|
||||
}
|
||||
|
||||
.device-card__meta {
|
||||
font-size: 22rpx;
|
||||
color: #5c7288;
|
||||
}
|
||||
|
||||
.device-card__action {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: #ffffff;
|
||||
color: #52657d;
|
||||
border: 2rpx solid #d8e2ec;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||
import { getMyResults, getSessionResult, type BackendSessionResultView } from '../../utils/backendApi'
|
||||
import { getSessionResult } from '../../utils/backendApi'
|
||||
import type { MapEngineResultSnapshot } from '../../engine/map/mapEngine'
|
||||
|
||||
type ResultPageData = {
|
||||
sessionId: string
|
||||
@@ -7,7 +8,6 @@ type ResultPageData = {
|
||||
sessionTitleText: string
|
||||
sessionSubtitleText: string
|
||||
rows: Array<{ label: string; value: string }>
|
||||
recentResults: BackendSessionResultView[]
|
||||
}
|
||||
|
||||
function getAccessToken(): string | null {
|
||||
@@ -25,6 +25,22 @@ function formatValue(value: unknown): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function formatRouteSummary(input: {
|
||||
variantName?: string | null
|
||||
routeCode?: string | null
|
||||
}): string {
|
||||
if (input.variantName && input.routeCode) {
|
||||
return `${input.variantName} / ${input.routeCode}`
|
||||
}
|
||||
if (input.variantName) {
|
||||
return input.variantName
|
||||
}
|
||||
if (input.routeCode) {
|
||||
return input.routeCode
|
||||
}
|
||||
return '默认赛道'
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
sessionId: '',
|
||||
@@ -32,17 +48,49 @@ Page({
|
||||
sessionTitleText: '结果页',
|
||||
sessionSubtitleText: '未加载',
|
||||
rows: [],
|
||||
recentResults: [],
|
||||
} as ResultPageData,
|
||||
|
||||
onLoad(query: { sessionId?: string }) {
|
||||
const sessionId = query && query.sessionId ? decodeURIComponent(query.sessionId) : ''
|
||||
this.setData({ sessionId })
|
||||
this.applyPendingResultSnapshot()
|
||||
if (sessionId) {
|
||||
this.loadSingleResult(sessionId)
|
||||
return
|
||||
}
|
||||
this.loadRecentResults()
|
||||
this.setData({
|
||||
statusText: '未提供单局会话,已跳转历史结果',
|
||||
})
|
||||
wx.redirectTo({
|
||||
url: '/pages/results/results',
|
||||
})
|
||||
},
|
||||
|
||||
applyPendingResultSnapshot() {
|
||||
const app = getApp<IAppOption>()
|
||||
const snapshot = app.globalData && app.globalData.pendingResultSnapshot
|
||||
? app.globalData.pendingResultSnapshot as MapEngineResultSnapshot
|
||||
: null
|
||||
if (!snapshot) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
statusText: '正在加载结果',
|
||||
sessionTitleText: snapshot.title,
|
||||
sessionSubtitleText: snapshot.subtitle,
|
||||
rows: [
|
||||
{ label: snapshot.heroLabel, value: snapshot.heroValue },
|
||||
...snapshot.rows.map((row) => ({
|
||||
label: row.label,
|
||||
value: row.value,
|
||||
})),
|
||||
],
|
||||
})
|
||||
|
||||
if (app.globalData) {
|
||||
app.globalData.pendingResultSnapshot = null
|
||||
}
|
||||
},
|
||||
|
||||
async loadSingleResult(sessionId: string) {
|
||||
@@ -65,8 +113,9 @@ Page({
|
||||
this.setData({
|
||||
statusText: '单局结果加载完成',
|
||||
sessionTitleText: result.session.eventName || result.session.eventDisplayName || result.session.eventId || result.session.id || result.session.sessionId,
|
||||
sessionSubtitleText: `${result.session.status || result.session.sessionStatus} / ${result.result.status}`,
|
||||
sessionSubtitleText: `${result.session.status || result.session.sessionStatus} / ${result.result.status} / ${formatRouteSummary(result.session)}`,
|
||||
rows: [
|
||||
{ label: '赛道版本', value: formatRouteSummary(result.session) },
|
||||
{ label: '最终得分', value: formatValue(result.result.finalScore) },
|
||||
{ label: '最终用时(秒)', value: formatValue(result.result.finalDurationSec) },
|
||||
{ label: '完成点数', value: formatValue(result.result.completedControls) },
|
||||
@@ -84,51 +133,9 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async loadRecentResults() {
|
||||
const accessToken = getAccessToken()
|
||||
if (!accessToken) {
|
||||
wx.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
statusText: '正在加载最近结果',
|
||||
})
|
||||
|
||||
try {
|
||||
const results = await getMyResults({
|
||||
baseUrl: loadBackendBaseUrl(),
|
||||
accessToken,
|
||||
limit: 20,
|
||||
})
|
||||
this.setData({
|
||||
statusText: '最近结果加载完成',
|
||||
sessionSubtitleText: '最近结果列表',
|
||||
recentResults: results,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
|
||||
this.setData({
|
||||
statusText: `结果加载失败:${message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleOpenResult(event: WechatMiniprogram.TouchEvent) {
|
||||
const sessionId = event.currentTarget.dataset.sessionId as string | undefined
|
||||
if (!sessionId) {
|
||||
return
|
||||
}
|
||||
wx.redirectTo({
|
||||
url: `/pages/result/result?sessionId=${encodeURIComponent(sessionId)}`,
|
||||
})
|
||||
},
|
||||
|
||||
handleBackToList() {
|
||||
this.setData({
|
||||
sessionId: '',
|
||||
rows: [],
|
||||
wx.redirectTo({
|
||||
url: '/pages/results/results',
|
||||
})
|
||||
this.loadRecentResults()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<view class="panel">
|
||||
<view class="panel__title">当前状态</view>
|
||||
<view class="summary">{{statusText}}</view>
|
||||
<button wx:if="{{sessionId}}" class="btn btn--ghost" bindtap="handleBackToList">返回最近结果</button>
|
||||
<button class="btn btn--ghost" bindtap="handleBackToList">查看历史结果</button>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{rows.length}}" class="panel">
|
||||
@@ -19,15 +19,5 @@
|
||||
<view class="row__value">{{item.value}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{!sessionId}}" class="panel">
|
||||
<view class="panel__title">最近结果</view>
|
||||
<view wx:if="{{!recentResults.length}}" class="summary">当前没有结果记录</view>
|
||||
<view wx:for="{{recentResults}}" wx:key="session.id" class="result-card" bindtap="handleOpenResult" data-session-id="{{item.session.id}}">
|
||||
<view class="result-card__title">{{item.session.eventName || item.session.id}}</view>
|
||||
<view class="result-card__meta">{{item.result.status}} / {{item.session.status}}</view>
|
||||
<view class="result-card__meta">得分 {{item.result.finalScore || '--'}} / 用时 {{item.result.finalDurationSec || '--'}}s</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
104
miniprogram/pages/results/results.ts
Normal file
104
miniprogram/pages/results/results.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||
import { getMyResults, type BackendSessionResultView } from '../../utils/backendApi'
|
||||
|
||||
type ResultsPageData = {
|
||||
loading: boolean
|
||||
statusText: string
|
||||
results: Array<{
|
||||
sessionId: string
|
||||
titleText: string
|
||||
statusText: string
|
||||
scoreText: string
|
||||
routeText: string
|
||||
}>
|
||||
}
|
||||
|
||||
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 formatRouteSummary(result: BackendSessionResultView): string {
|
||||
const session = result.session
|
||||
if (session.variantName && session.routeCode) {
|
||||
return `${session.variantName} / ${session.routeCode}`
|
||||
}
|
||||
if (session.variantName) {
|
||||
return session.variantName
|
||||
}
|
||||
if (session.routeCode) {
|
||||
return session.routeCode
|
||||
}
|
||||
return '默认赛道'
|
||||
}
|
||||
|
||||
function buildResultCardView(result: BackendSessionResultView) {
|
||||
return {
|
||||
sessionId: result.session.id,
|
||||
titleText: result.session.eventName || result.session.id,
|
||||
statusText: `${result.result.status} / ${result.session.status}`,
|
||||
scoreText: `得分 ${result.result.finalScore || '--'} / 用时 ${result.result.finalDurationSec || '--'}s`,
|
||||
routeText: `赛道 ${formatRouteSummary(result)}`,
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
statusText: '准备加载历史结果',
|
||||
results: [],
|
||||
} as ResultsPageData,
|
||||
|
||||
onLoad() {
|
||||
this.loadResults()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadResults()
|
||||
},
|
||||
|
||||
async loadResults() {
|
||||
const accessToken = getAccessToken()
|
||||
if (!accessToken) {
|
||||
wx.redirectTo({ url: '/pages/login/login' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({
|
||||
loading: true,
|
||||
statusText: '正在加载历史结果',
|
||||
})
|
||||
|
||||
try {
|
||||
const results = await getMyResults({
|
||||
baseUrl: loadBackendBaseUrl(),
|
||||
accessToken,
|
||||
limit: 20,
|
||||
})
|
||||
this.setData({
|
||||
loading: false,
|
||||
statusText: `历史结果加载完成,共 ${results.length} 条`,
|
||||
results: results.map(buildResultCardView),
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
|
||||
this.setData({
|
||||
loading: false,
|
||||
statusText: `历史结果加载失败:${message}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleOpenResult(event: WechatMiniprogram.TouchEvent) {
|
||||
const sessionId = event.currentTarget.dataset.sessionId as string | undefined
|
||||
if (!sessionId) {
|
||||
return
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: `/pages/result/result?sessionId=${encodeURIComponent(sessionId)}`,
|
||||
})
|
||||
},
|
||||
})
|
||||
25
miniprogram/pages/results/results.wxml
Normal file
25
miniprogram/pages/results/results.wxml
Normal file
@@ -0,0 +1,25 @@
|
||||
<scroll-view class="page" scroll-y>
|
||||
<view class="shell">
|
||||
<view class="hero">
|
||||
<view class="hero__eyebrow">Results</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>
|
||||
|
||||
<view class="panel">
|
||||
<view class="panel__title">结果列表</view>
|
||||
<view wx:if="{{!results.length}}" class="summary">当前没有结果记录</view>
|
||||
<view wx:for="{{results}}" wx:key="sessionId" class="result-card" bindtap="handleOpenResult" data-session-id="{{item.sessionId}}">
|
||||
<view class="result-card__title">{{item.titleText}}</view>
|
||||
<view class="result-card__meta">{{item.statusText}}</view>
|
||||
<view class="result-card__meta">{{item.scoreText}}</view>
|
||||
<view class="result-card__meta">{{item.routeText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
76
miniprogram/pages/results/results.wxss
Normal file
76
miniprogram/pages/results/results.wxss
Normal file
@@ -0,0 +1,76 @@
|
||||
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,
|
||||
.result-card__meta {
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
color: #30465f;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
display: grid;
|
||||
gap: 8rpx;
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f6f9fc;
|
||||
}
|
||||
|
||||
.result-card__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #17345a;
|
||||
}
|
||||
@@ -30,6 +30,21 @@ export interface BackendResolvedRelease {
|
||||
routeCode?: string | null
|
||||
}
|
||||
|
||||
export interface BackendCourseVariantSummary {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
routeCode?: string | null
|
||||
selectable?: boolean
|
||||
}
|
||||
|
||||
export interface BackendLaunchVariantSummary {
|
||||
id: string
|
||||
name: string
|
||||
routeCode?: string | null
|
||||
assignmentMode?: string | null
|
||||
}
|
||||
|
||||
export interface BackendEntrySessionSummary {
|
||||
id: string
|
||||
status: string
|
||||
@@ -38,6 +53,8 @@ export interface BackendEntrySessionSummary {
|
||||
releaseId?: string | null
|
||||
configLabel?: string | null
|
||||
routeCode?: string | null
|
||||
variantId?: string | null
|
||||
variantName?: string | null
|
||||
launchedAt?: string | null
|
||||
startedAt?: string | null
|
||||
endedAt?: string | null
|
||||
@@ -111,6 +128,8 @@ export interface BackendEventPlayResult {
|
||||
primaryAction: string
|
||||
reason: string
|
||||
launchSource?: string
|
||||
assignmentMode?: string | null
|
||||
courseVariants?: BackendCourseVariantSummary[] | null
|
||||
ongoingSession?: BackendEntrySessionSummary | null
|
||||
recentSession?: BackendEntrySessionSummary | null
|
||||
}
|
||||
@@ -139,6 +158,7 @@ export interface BackendLaunchResult {
|
||||
sessionTokenExpiresAt: string
|
||||
routeCode?: string | null
|
||||
}
|
||||
variant?: BackendLaunchVariantSummary | null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +314,7 @@ export function launchEvent(input: {
|
||||
eventId: string
|
||||
accessToken: string
|
||||
releaseId?: string
|
||||
variantId?: string
|
||||
clientType: string
|
||||
deviceKey: string
|
||||
}): Promise<BackendLaunchResult> {
|
||||
@@ -304,6 +325,9 @@ export function launchEvent(input: {
|
||||
if (input.releaseId) {
|
||||
body.releaseId = input.releaseId
|
||||
}
|
||||
if (input.variantId) {
|
||||
body.variantId = input.variantId
|
||||
}
|
||||
return requestBackend<BackendLaunchResult>({
|
||||
method: 'POST',
|
||||
baseUrl: input.baseUrl,
|
||||
|
||||
@@ -17,5 +17,17 @@ export function adaptBackendLaunchResultToEnvelope(result: BackendLaunchResult):
|
||||
sessionToken: result.launch.business.sessionToken,
|
||||
sessionTokenExpiresAt: result.launch.business.sessionTokenExpiresAt,
|
||||
},
|
||||
variant: result.launch.variant
|
||||
? {
|
||||
variantId: result.launch.variant.id,
|
||||
variantName: result.launch.variant.name,
|
||||
routeCode: result.launch.variant.routeCode || result.launch.config.routeCode || result.launch.business.routeCode || null,
|
||||
assignmentMode: result.launch.variant.assignmentMode || null,
|
||||
}
|
||||
: (result.launch.config.routeCode || result.launch.business.routeCode)
|
||||
? {
|
||||
routeCode: result.launch.config.routeCode || result.launch.business.routeCode || null,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,17 @@ export interface BusinessLaunchContext {
|
||||
realtimeToken?: string | null
|
||||
}
|
||||
|
||||
export interface GameVariantLaunchContext {
|
||||
variantId?: string | null
|
||||
variantName?: string | null
|
||||
routeCode?: string | null
|
||||
assignmentMode?: string | null
|
||||
}
|
||||
|
||||
export interface GameLaunchEnvelope {
|
||||
config: GameConfigLaunchRequest
|
||||
business: BusinessLaunchContext | null
|
||||
variant?: GameVariantLaunchContext | null
|
||||
}
|
||||
|
||||
export interface MapPageLaunchOptions {
|
||||
@@ -46,6 +54,9 @@ export interface MapPageLaunchOptions {
|
||||
sessionTokenExpiresAt?: string
|
||||
realtimeEndpoint?: string
|
||||
realtimeToken?: string
|
||||
variantId?: string
|
||||
variantName?: string
|
||||
assignmentMode?: string
|
||||
}
|
||||
|
||||
type PendingGameLaunchStore = Record<string, GameLaunchEnvelope>
|
||||
@@ -121,6 +132,28 @@ function buildBusinessLaunchContext(options?: MapPageLaunchOptions | null): Busi
|
||||
}
|
||||
}
|
||||
|
||||
function buildVariantLaunchContext(options?: MapPageLaunchOptions | null): GameVariantLaunchContext | null {
|
||||
if (!options) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variantId = normalizeOptionalString(options.variantId)
|
||||
const variantName = normalizeOptionalString(options.variantName)
|
||||
const routeCode = normalizeOptionalString(options.routeCode)
|
||||
const assignmentMode = normalizeOptionalString(options.assignmentMode)
|
||||
|
||||
if (!variantId && !variantName && !routeCode && !assignmentMode) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
variantId,
|
||||
variantName,
|
||||
routeCode,
|
||||
assignmentMode,
|
||||
}
|
||||
}
|
||||
|
||||
function loadPendingGameLaunchStore(): PendingGameLaunchStore {
|
||||
try {
|
||||
const stored = wx.getStorageSync(PENDING_GAME_LAUNCH_STORAGE_KEY)
|
||||
@@ -146,6 +179,7 @@ export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): G
|
||||
business: {
|
||||
source: 'demo',
|
||||
},
|
||||
variant: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +251,7 @@ export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null)
|
||||
routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
|
||||
},
|
||||
business: buildBusinessLaunchContext(options),
|
||||
variant: buildVariantLaunchContext(options),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user