| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160 |
- import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
- import { getEventPlay, getPublicEventPlay, launchEvent, launchPublicEvent, type BackendCourseVariantSummary, type BackendEventPlayResult } from '../../utils/backendApi'
- import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
- import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
- import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
- import { reportBackendClientLog } from '../../utils/backendClientLogs'
- import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
- import { buildPreparePreviewScene, buildPreparePreviewSceneFromBackendPreview, buildPreparePreviewSceneFromVariantControls, type PreparePreviewControl, type PreparePreviewScene, type PreparePreviewTile } from '../../utils/prepareMapPreview'
- 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
- canLaunch: boolean
- launchInFlight: boolean
- launchProgressText: string
- launchProgressPercent: number
- titleText: string
- summaryText: string
- releaseText: string
- actionText: string
- statusText: string
- assignmentMode: string
- variantModeText: string
- variantSummaryText: string
- presentationText: string
- contentBundleText: string
- runtimePlaceText: string
- runtimeMapText: string
- runtimeVariantText: string
- runtimeRouteCodeText: string
- previewVisible: boolean
- previewLoading: boolean
- previewStatusText: string
- previewHintText: string
- previewVariantText: string
- previewTiles: Array<{
- url: string
- styleText: string
- }>
- previewControls: Array<{
- label: string
- styleText: string
- kindClass: string
- }>
- selectedVariantId: string
- selectedVariantText: string
- showVariantSelector: boolean
- variantSelectorEmptyText: 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
- showMockSourceSummary: boolean
- }
- type EventPreparePageContext = WechatMiniprogram.Page.Instance<EventPreparePageData, Record<string, never>> & {
- previewLoadSeq?: number
- lastPlayResult?: BackendEventPlayResult | null
- previewManifestUrl?: string | null
- previewConfigCache?: RemoteMapConfig | null
- previewSceneCache?: Record<string, PreparePreviewScene>
- launchAttemptSeq?: number
- launchTimeoutTimer?: number
- }
- const PREVIEW_WIDTH = 640
- const PREVIEW_HEIGHT = 360
- const PREPARE_LAUNCH_TIMEOUT_MS = 12000
- function toPercent(value: number, total: number): string {
- if (!total) {
- return '0%'
- }
- return `${(value / total) * 100}%`
- }
- function buildPreviewTileView(scene: PreparePreviewScene, tile: PreparePreviewTile) {
- const left = toPercent(tile.leftPx, scene.width)
- const top = toPercent(tile.topPx, scene.height)
- const width = toPercent(tile.sizePx, scene.width)
- const height = toPercent(tile.sizePx, scene.height)
- return {
- url: tile.url,
- styleText: `left:${left};top:${top};width:${width};height:${height};`,
- }
- }
- function buildPreviewControlView(scene: PreparePreviewScene, control: PreparePreviewControl) {
- let kindClass = 'preview-control--normal'
- if (control.kind === 'start') {
- kindClass = 'preview-control--start'
- } else if (control.kind === 'finish') {
- kindClass = 'preview-control--finish'
- }
- return {
- label: control.label,
- kindClass,
- styleText: `left:${toPercent(control.x, scene.width)};top:${toPercent(control.y, scene.height)};`,
- }
- }
- function resolvePreviewManifestUrl(result: BackendEventPlayResult): string {
- if (result.resolvedRelease && result.resolvedRelease.manifestUrl) {
- return result.resolvedRelease.manifestUrl
- }
- if (result.release && result.release.manifestUrl) {
- return result.release.manifestUrl
- }
- return ''
- }
- function canUseBackendPreview(result: BackendEventPlayResult): boolean {
- return !!(
- result.preview
- && result.preview.baseTiles
- && result.preview.baseTiles.tileBaseUrl
- && result.preview.viewport
- && typeof result.preview.viewport.minLon === 'number'
- && typeof result.preview.viewport.minLat === 'number'
- && typeof result.preview.viewport.maxLon === 'number'
- && typeof result.preview.viewport.maxLat === 'number'
- )
- }
- function resolveSelectedPreviewVariant(result: BackendEventPlayResult, selectedVariantId: string) {
- if (!result.preview || !result.preview.variants || !result.preview.variants.length) {
- return null
- }
- const normalizedVariantId = selectedVariantId || (result.preview.selectedVariantId || '')
- const exact = result.preview.variants.find((item) => {
- const candidateId = item.variantId || item.id || ''
- return candidateId === normalizedVariantId
- })
- if (exact) {
- return exact
- }
- return result.preview.variants[0]
- }
- function resolvePreviewHintText(result: BackendEventPlayResult, scene: PreparePreviewScene): string {
- if (detectMultiVariantContext(result)) {
- return scene.overlayAvailable
- ? '当前先展示低级别底图与已知赛道形态;多赛道最终以进入地图后的绑定结果为准。'
- : '当前活动支持多赛道;当前先展示底图与所选赛道信息,赛道点位预览待后端补齐每条赛道的预览数据后联动。'
- }
- return scene.overlayAvailable
- ? '当前先展示低级别底图与当前已知赛道,进入地图后按正式地图继续。'
- : '当前先展示地图范围预览,进入地图后再查看正式赛道。'
- }
- function detectMultiVariantContext(result: BackendEventPlayResult): boolean {
- const assignmentMode = result.play.assignmentMode
- if (assignmentMode === 'manual' || assignmentMode === 'random' || assignmentMode === 'server-assigned') {
- return true
- }
- const variants = result.play.courseVariants || []
- if (variants.length > 0) {
- return true
- }
- const haystacks = [
- result.event.displayName,
- result.event.summary,
- result.release ? result.release.configLabel : '',
- result.resolvedRelease ? result.resolvedRelease.configLabel : '',
- ]
- return haystacks.some((item) => typeof item === 'string' && item.indexOf('多赛道') >= 0)
- }
- 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(' / ')
- const selectableCount = variants.filter((item) => item.selectable !== false).length
- 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}`
- }
- if (selectableCount > 1) {
- return `当前活动支持 ${variants.length} 条赛道。后端当前未明确返回赛道模式,前端先按手动选择兼容显示:${preview}`
- }
- return preview
- }
- function formatPresentationSummary(result: BackendEventPlayResult): string {
- const currentPresentation = result.currentPresentation
- if (!currentPresentation) {
- return '当前发布 release 未绑定展示版本,或当前尚未发布'
- }
- return `${currentPresentation.presentationId || '--'} / ${currentPresentation.templateKey || '--'} / ${currentPresentation.version || '--'}`
- }
- function formatContentBundleSummary(result: BackendEventPlayResult): string {
- const currentContentBundle = result.currentContentBundle
- if (!currentContentBundle) {
- return '当前发布 release 未绑定内容包版本,或当前尚未发布'
- }
- return `${currentContentBundle.bundleId || '--'} / ${currentContentBundle.bundleType || '--'} / ${currentContentBundle.version || '--'}`
- }
- function resolveSelectedVariantId(
- currentVariantId: string,
- assignmentMode?: string | null,
- variants?: BackendCourseVariantSummary[] | null,
- forceVisible?: boolean,
- ): string {
- if (!shouldShowVariantSelector(assignmentMode, variants, forceVisible)) {
- 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,
- forceVisible?: boolean,
- ) {
- if (!shouldShowVariantSelector(assignmentMode, variants, forceVisible) || !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,
- }))
- }
- function shouldShowVariantSelector(
- assignmentMode?: string | null,
- variants?: BackendCourseVariantSummary[] | null,
- forceVisible?: boolean,
- ): boolean {
- if (forceVisible) {
- return true
- }
- const normalizedVariants = variants || []
- if (!normalizedVariants.length) {
- return false
- }
- if (assignmentMode === 'manual') {
- return true
- }
- if (assignmentMode === 'random' || assignmentMode === 'server-assigned') {
- return false
- }
- return normalizedVariants.filter((item) => item.selectable !== false).length > 1
- }
- let prepareHeartRateController: HeartRateController | null = null
- function clearPrepareLaunchTimeout(page: EventPreparePageContext) {
- if (page.launchTimeoutTimer) {
- clearTimeout(page.launchTimeoutTimer)
- page.launchTimeoutTimer = 0
- }
- }
- function resetPrepareLaunchVisualState(page: EventPreparePageContext) {
- clearPrepareLaunchTimeout(page)
- page.launchAttemptSeq = 0
- page.setData({
- launchInFlight: false,
- launchProgressText: '待进入地图',
- launchProgressPercent: 0,
- })
- }
- 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,
- canLaunch: false,
- launchInFlight: false,
- launchProgressText: '待进入地图',
- launchProgressPercent: 0,
- titleText: '开始前准备',
- summaryText: '未加载',
- releaseText: '--',
- actionText: '--',
- statusText: '待加载',
- assignmentMode: '',
- variantModeText: '--',
- variantSummaryText: '--',
- presentationText: '--',
- contentBundleText: '--',
- runtimePlaceText: '进入地图后确认',
- runtimeMapText: '进入地图后确认',
- runtimeVariantText: '进入地图后确认',
- runtimeRouteCodeText: '进入地图后确认',
- previewVisible: false,
- previewLoading: false,
- previewStatusText: '准备加载地图预览',
- previewHintText: '进入地图前先看地图范围与当前已知赛道。',
- previewVariantText: '预览将跟随当前赛道选择联动',
- previewTiles: [],
- previewControls: [],
- selectedVariantId: '',
- selectedVariantText: '当前无需手动指定赛道',
- showVariantSelector: false,
- variantSelectorEmptyText: '当前无需手动指定赛道',
- selectableVariants: [],
- locationStatusText: '待进入地图后校验定位权限与实时精度',
- heartRateStatusText: '局前心率带连接入口待接入,本轮先保留骨架',
- heartRateDeviceText: '--',
- heartRateScanText: '未扫描',
- heartRateConnected: false,
- showHeartRateDevicePicker: false,
- locationPermissionGranted: false,
- locationBackgroundPermissionGranted: false,
- heartRateDiscoveredDevices: [],
- mockSourceStatusText: '模拟源调试仍在地图页调试面板中使用',
- showMockSourceSummary: false,
- } 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() {
- resetPrepareLaunchVisualState(this as unknown as EventPreparePageContext)
- this.refreshPreparationDeviceState()
- },
- onUnload() {
- resetPrepareLaunchVisualState(this as unknown as EventPreparePageContext)
- if (prepareHeartRateController) {
- prepareHeartRateController.destroy()
- prepareHeartRateController = null
- }
- },
- async loadEventPlay(eventId?: string) {
- const targetEventId = eventId || this.data.eventId
- const accessToken = getAccessToken()
- this.setData({
- loading: true,
- statusText: '正在加载局前准备信息',
- })
- try {
- const baseUrl = loadBackendBaseUrl()
- const result = accessToken
- ? await getEventPlay({
- baseUrl,
- eventId: targetEventId,
- accessToken,
- })
- : await getPublicEventPlay({
- baseUrl,
- eventId: targetEventId,
- })
- 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) {
- ;(this as unknown as EventPreparePageContext).lastPlayResult = result
- const page = this as unknown as EventPreparePageContext
- const nextManifestUrl = resolvePreviewManifestUrl(result)
- if (page.previewManifestUrl !== nextManifestUrl) {
- page.previewManifestUrl = nextManifestUrl
- page.previewConfigCache = null
- page.previewSceneCache = {}
- }
- const multiVariantContext = detectMultiVariantContext(result)
- const selectedVariantId = resolveSelectedVariantId(
- this.data.selectedVariantId,
- result.play.assignmentMode,
- result.play.courseVariants,
- multiVariantContext,
- )
- const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
- const showVariantSelector = shouldShowVariantSelector(
- result.play.assignmentMode,
- result.play.courseVariants,
- multiVariantContext,
- )
- const logVariantId = assignmentMode === 'manual' && selectedVariantId ? selectedVariantId : null
- const selectableVariants = buildSelectableVariants(
- selectedVariantId,
- result.play.assignmentMode,
- result.play.courseVariants,
- multiVariantContext,
- )
- const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
- reportBackendClientLog({
- level: 'info',
- category: 'event-prepare',
- message: 'prepare play loaded',
- eventId: result.event.id || this.data.eventId || '',
- releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
- ? result.resolvedRelease.releaseId
- : '',
- manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
- ? result.resolvedRelease.manifestUrl
- : '',
- details: {
- guestMode: !getAccessToken(),
- pageEventId: this.data.eventId || '',
- resultEventId: result.event.id || '',
- selectedVariantId: logVariantId,
- assignmentMode,
- variantCount: result.play.courseVariants ? result.play.courseVariants.length : 0,
- selectableVariantCount: result.play.courseVariants
- ? result.play.courseVariants.filter((item) => item.selectable !== false).length
- : 0,
- showVariantSelector,
- multiVariantContext,
- },
- })
- const variantSelectorEmptyText = multiVariantContext
- ? '当前活动按多赛道处理,但后端暂未返回可选赛道,请稍后刷新或联系后台。'
- : '当前无需手动指定赛道'
- this.setData({
- loading: false,
- canLaunch: result.play.canLaunch,
- titleText: `${result.event.displayName} / 开始前准备`,
- summaryText: result.event.summary || '暂无活动简介',
- releaseText: result.resolvedRelease
- ? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}`
- : '当前无可用 release',
- actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason),
- statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason),
- assignmentMode: result.play.assignmentMode || '',
- variantModeText: result.play.assignmentMode
- ? formatAssignmentMode(result.play.assignmentMode)
- : (showVariantSelector ? '手动选择' : '默认单赛道'),
- variantSummaryText: formatVariantSummary(result),
- presentationText: formatPresentationSummary(result),
- contentBundleText: formatContentBundleSummary(result),
- runtimePlaceText: '进入地图后确认',
- runtimeMapText: '进入地图后确认',
- runtimeVariantText: selectedVariant
- ? selectedVariant.name
- : (result.play.courseVariants && result.play.courseVariants[0]
- ? result.play.courseVariants[0].name
- : '进入地图后确认'),
- runtimeRouteCodeText: selectedVariant
- ? selectedVariant.routeCodeText
- : (result.play.courseVariants && result.play.courseVariants[0] && result.play.courseVariants[0].routeCode
- ? result.play.courseVariants[0].routeCode || '进入地图后确认'
- : '进入地图后确认'),
- previewVisible: true,
- previewLoading: true,
- previewStatusText: '正在生成地图预览',
- previewHintText: '进入地图前先看地图范围与当前已知赛道。',
- previewVariantText: selectedVariant
- ? `当前预览赛道:${selectedVariant.name} / ${selectedVariant.routeCodeText}`
- : (multiVariantContext ? '当前预览赛道:待选择' : '当前预览赛道:默认赛道'),
- previewTiles: [],
- previewControls: [],
- selectedVariantId,
- selectedVariantText: selectedVariant
- ? `${selectedVariant.name} / ${selectedVariant.routeCodeText}`
- : variantSelectorEmptyText,
- showVariantSelector,
- variantSelectorEmptyText,
- selectableVariants,
- })
- this.loadPrepareMapPreview(result)
- },
- async loadPrepareMapPreview(result: BackendEventPlayResult) {
- const page = this as unknown as EventPreparePageContext
- const seq = (page.previewLoadSeq || 0) + 1
- page.previewLoadSeq = seq
- const selectedVariantId = this.data.selectedVariantId || (result.preview && result.preview.selectedVariantId ? result.preview.selectedVariantId : '')
- const manifestUrl = resolvePreviewManifestUrl(result)
- let fallbackConfig: RemoteMapConfig | null = page.previewConfigCache || null
- const multiVariantContext = detectMultiVariantContext(result)
- if (multiVariantContext && canUseBackendPreview(result) && result.preview) {
- const sceneCacheKey = selectedVariantId || '__default__'
- const cachedScene = page.previewSceneCache && page.previewSceneCache[sceneCacheKey]
- if (cachedScene) {
- const previewTiles = cachedScene.tiles.map((item) => buildPreviewTileView(cachedScene, item))
- const previewControls = cachedScene.controls.map((item) => buildPreviewControlView(cachedScene, item))
- this.setData({
- previewVisible: true,
- previewLoading: false,
- previewStatusText: cachedScene.overlayAvailable ? '已加载地图与赛道预览' : '已加载地图范围预览',
- previewHintText: cachedScene.overlayAvailable
- ? '当前预览已按所选赛道联动显示点位,最终绑定以后端 launch 返回结果为准。'
- : '当前预览已切换到所选赛道的底图范围;该赛道暂未返回点位预览数据。',
- previewVariantText: selectedVariantId
- ? `当前预览赛道:${this.data.selectedVariantText}`
- : '当前预览赛道:默认赛道',
- previewTiles,
- previewControls,
- runtimePlaceText: result.event.displayName || this.data.runtimePlaceText,
- })
- return
- }
- if (manifestUrl) {
- if (!fallbackConfig) {
- try {
- fallbackConfig = await loadRemoteMapConfig(manifestUrl)
- page.previewConfigCache = fallbackConfig
- } catch (_error) {
- fallbackConfig = null
- }
- }
- }
- const selectedPreviewVariant = resolveSelectedPreviewVariant(result, selectedVariantId)
- const scene = fallbackConfig && selectedPreviewVariant && selectedPreviewVariant.controls
- ? buildPreparePreviewSceneFromVariantControls(
- fallbackConfig,
- PREVIEW_WIDTH,
- PREVIEW_HEIGHT,
- selectedPreviewVariant.controls,
- )
- : buildPreparePreviewSceneFromBackendPreview(
- result.preview,
- PREVIEW_WIDTH,
- PREVIEW_HEIGHT,
- selectedVariantId,
- fallbackConfig ? fallbackConfig.tileSource : null,
- )
- if (page.previewLoadSeq !== seq) {
- return
- }
- if (scene) {
- if (!page.previewSceneCache) {
- page.previewSceneCache = {}
- }
- page.previewSceneCache[sceneCacheKey] = scene
- const previewTiles = scene.tiles.map((item) => buildPreviewTileView(scene, item))
- const previewControls = scene.controls.map((item) => buildPreviewControlView(scene, item))
- this.setData({
- previewVisible: true,
- previewLoading: false,
- previewStatusText: scene.overlayAvailable ? '已加载地图与赛道预览' : '已加载地图范围预览',
- previewHintText: scene.overlayAvailable
- ? '当前预览已按所选赛道联动显示点位,最终绑定以后端 launch 返回结果为准。'
- : '当前预览已切换到所选赛道的底图范围;该赛道暂未返回点位预览数据。',
- previewVariantText: selectedVariantId
- ? `当前预览赛道:${this.data.selectedVariantText}`
- : '当前预览赛道:默认赛道',
- previewTiles,
- previewControls,
- runtimePlaceText: result.event.displayName || this.data.runtimePlaceText,
- })
- return
- }
- }
- if (!manifestUrl) {
- this.setData({
- previewVisible: true,
- previewLoading: false,
- previewStatusText: '当前发布未返回预览底图来源',
- previewHintText: '当前活动暂无可用地图预览,请稍后刷新或联系后台。',
- previewVariantText: '当前预览赛道:待进入地图后确认',
- previewTiles: [],
- previewControls: [],
- })
- return
- }
- try {
- const config = fallbackConfig || await loadRemoteMapConfig(manifestUrl)
- page.previewConfigCache = config
- if (page.previewLoadSeq !== seq) {
- return
- }
- const overlayEnabled = !multiVariantContext
- const scene = buildPreparePreviewScene(config, PREVIEW_WIDTH, PREVIEW_HEIGHT, overlayEnabled)
- const previewTiles = scene.tiles.map((item) => buildPreviewTileView(scene, item))
- const previewControls = scene.controls.map((item) => buildPreviewControlView(scene, item))
- const runtimeMapText = config.configTitle || '进入地图后确认'
- const runtimePlaceText = result.event.displayName || '进入地图后确认'
- this.setData({
- previewVisible: true,
- previewLoading: false,
- previewStatusText: scene.overlayAvailable ? '已加载地图与赛道预览' : '已加载地图范围预览',
- previewHintText: resolvePreviewHintText(result, scene),
- previewVariantText: this.data.selectedVariantId
- ? `当前预览赛道:${this.data.selectedVariantText}`
- : (result.play.courseVariants && result.play.courseVariants[0]
- ? `当前预览赛道:${result.play.courseVariants[0].name} / ${result.play.courseVariants[0].routeCode || '默认编码'}`
- : '当前预览赛道:默认赛道'),
- previewTiles,
- previewControls,
- runtimePlaceText,
- runtimeMapText,
- })
- } catch (error) {
- if (page.previewLoadSeq !== seq) {
- return
- }
- const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
- this.setData({
- previewVisible: true,
- previewLoading: false,
- previewStatusText: `地图预览加载失败:${message}`,
- previewHintText: '当前先展示文字摘要;预览底图可在刷新后重试。',
- previewVariantText: '当前预览赛道:待进入地图后确认',
- previewTiles: [],
- previewControls: [],
- })
- }
- },
- 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()
- const showMockSourceSummary = autoConnect || channelId !== 'default'
- this.setData({
- mockSourceStatusText: autoConnect
- ? `调试源自动连接已开启 / 通道 ${channelId}`
- : `当前使用调试通道 ${channelId}`,
- showMockSourceSummary,
- })
- },
- 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}`
- : '当前无需手动指定赛道',
- runtimeVariantText: selectedVariant ? selectedVariant.name : '进入地图后确认',
- runtimeRouteCodeText: selectedVariant ? selectedVariant.routeCodeText : '进入地图后确认',
- previewHintText: selectedVariant
- ? (this.data.showVariantSelector
- ? `当前已选 ${selectedVariant.name} / ${selectedVariant.routeCodeText};预览底图会保留不变,最终赛道以 launch 绑定结果为准。`
- : `当前已选 ${selectedVariant.name} / ${selectedVariant.routeCodeText};最终地图以 launch 绑定结果为准。`)
- : this.data.previewHintText,
- previewStatusText: this.data.showVariantSelector ? '已加载地图范围预览' : this.data.previewStatusText,
- previewVariantText: selectedVariant
- ? `当前预览赛道:${selectedVariant.name} / ${selectedVariant.routeCodeText}`
- : '当前预览赛道:待选择',
- selectableVariants,
- })
- const page = this as unknown as EventPreparePageContext
- if (page.lastPlayResult) {
- this.loadPrepareMapPreview(page.lastPlayResult)
- }
- },
- async handleLaunch() {
- const page = this as unknown as EventPreparePageContext
- const accessToken = getAccessToken()
- if (this.data.launchInFlight) {
- wx.showToast({
- title: '正在进入地图,请稍候',
- icon: 'none',
- })
- return
- }
- if (!this.data.canLaunch) {
- this.setData({
- statusText: '当前发布状态不可进入地图',
- })
- wx.showToast({
- title: '当前发布状态不可进入地图',
- icon: 'none',
- })
- return
- }
- if (!this.data.locationPermissionGranted) {
- this.setData({
- statusText: '进入地图前请先完成定位授权',
- })
- wx.showToast({
- title: '请先授权定位',
- icon: 'none',
- })
- return
- }
- this.setData({
- launchInFlight: true,
- launchProgressText: '正在校验并创建本局',
- launchProgressPercent: 24,
- statusText: '正在创建 session 并进入地图',
- })
- const launchSeq = (page.launchAttemptSeq || 0) + 1
- page.launchAttemptSeq = launchSeq
- clearPrepareLaunchTimeout(page)
- page.launchTimeoutTimer = setTimeout(() => {
- if (page.launchAttemptSeq !== launchSeq) {
- return
- }
- this.setData({
- launchInFlight: false,
- launchProgressText: '进入地图超时',
- launchProgressPercent: 0,
- statusText: '进入地图超时,请稍后重试',
- })
- wx.showToast({
- title: '进入地图超时,请重试',
- icon: 'none',
- })
- }, PREPARE_LAUNCH_TIMEOUT_MS) as unknown as number
- try {
- const assignmentMode = this.data.assignmentMode ? this.data.assignmentMode : null
- const selectedVariantId = this.data.showVariantSelector && this.data.selectedVariantId
- ? this.data.selectedVariantId
- : null
- reportBackendClientLog({
- level: 'info',
- category: 'event-prepare',
- message: 'launch requested',
- eventId: this.data.eventId || '',
- details: {
- pageEventId: this.data.eventId || '',
- selectedVariantId,
- assignmentMode,
- phase: 'launch-requested',
- },
- })
- this.setData({
- launchProgressText: '已发起启动请求,正在等待服务器响应',
- launchProgressPercent: 52,
- })
- 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 = accessToken
- ? await launchEvent({
- baseUrl: loadBackendBaseUrl(),
- eventId: this.data.eventId,
- accessToken,
- variantId: this.data.showVariantSelector ? this.data.selectedVariantId : undefined,
- clientType: 'wechat',
- deviceKey: 'mini-dev-device-001',
- })
- : await launchPublicEvent({
- baseUrl: loadBackendBaseUrl(),
- eventId: this.data.eventId,
- variantId: this.data.showVariantSelector ? this.data.selectedVariantId : undefined,
- clientType: 'wechat',
- deviceKey: 'mini-dev-device-001',
- })
- if (page.launchAttemptSeq !== launchSeq) {
- return
- }
- clearPrepareLaunchTimeout(page)
- this.setData({
- launchProgressText: '启动成功,正在载入地图',
- launchProgressPercent: 86,
- })
- reportBackendClientLog({
- level: 'info',
- category: 'event-prepare',
- message: 'launch response received',
- eventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : this.data.eventId || '',
- releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
- sessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
- manifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
- ? result.launch.resolvedRelease.manifestUrl
- : '',
- details: {
- guestMode: !accessToken,
- pageEventId: this.data.eventId || '',
- launchEventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : '',
- launchSessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
- configUrl: result.launch.config && result.launch.config.configUrl ? result.launch.config.configUrl : '',
- releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
- resolvedReleaseId: result.launch.resolvedRelease && result.launch.resolvedRelease.releaseId
- ? result.launch.resolvedRelease.releaseId
- : '',
- resolvedManifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
- ? result.launch.resolvedRelease.manifestUrl
- : '',
- launchVariantId: result.launch.variant && result.launch.variant.id ? result.launch.variant.id : null,
- phase: 'launch-response',
- },
- })
- const envelope = adaptBackendLaunchResultToEnvelope(result)
- wx.navigateTo({
- url: prepareMapPageUrlForLaunch(envelope),
- })
- } catch (error) {
- if (page.launchAttemptSeq !== launchSeq) {
- return
- }
- clearPrepareLaunchTimeout(page)
- const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
- this.setData({
- launchInFlight: false,
- launchProgressText: '进入地图失败',
- launchProgressPercent: 0,
- statusText: `launch 失败:${message}`,
- })
- }
- },
- })
|