event-prepare.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778
  1. import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
  2. import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type BackendEventPlayResult } from '../../utils/backendApi'
  3. import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
  4. import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
  5. import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
  6. import { reportBackendClientLog } from '../../utils/backendClientLogs'
  7. import { HeartRateController } from '../../engine/sensor/heartRateController'
  8. const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
  9. const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
  10. const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
  11. type EventPreparePageData = {
  12. eventId: string
  13. loading: boolean
  14. canLaunch: boolean
  15. titleText: string
  16. summaryText: string
  17. releaseText: string
  18. actionText: string
  19. statusText: string
  20. assignmentMode: string
  21. variantModeText: string
  22. variantSummaryText: string
  23. presentationText: string
  24. contentBundleText: string
  25. runtimePlaceText: string
  26. runtimeMapText: string
  27. runtimeVariantText: string
  28. runtimeRouteCodeText: string
  29. selectedVariantId: string
  30. selectedVariantText: string
  31. showVariantSelector: boolean
  32. variantSelectorEmptyText: string
  33. selectableVariants: Array<{
  34. id: string
  35. name: string
  36. routeCodeText: string
  37. descriptionText: string
  38. selected: boolean
  39. }>
  40. locationStatusText: string
  41. heartRateStatusText: string
  42. heartRateDeviceText: string
  43. heartRateScanText: string
  44. heartRateConnected: boolean
  45. showHeartRateDevicePicker: boolean
  46. locationPermissionGranted: boolean
  47. locationBackgroundPermissionGranted: boolean
  48. heartRateDiscoveredDevices: Array<{
  49. deviceId: string
  50. name: string
  51. rssiText: string
  52. preferred: boolean
  53. connected: boolean
  54. }>
  55. mockSourceStatusText: string
  56. }
  57. function detectMultiVariantContext(result: BackendEventPlayResult): boolean {
  58. const assignmentMode = result.play.assignmentMode
  59. if (assignmentMode === 'manual' || assignmentMode === 'random' || assignmentMode === 'server-assigned') {
  60. return true
  61. }
  62. const variants = result.play.courseVariants || []
  63. if (variants.length > 0) {
  64. return true
  65. }
  66. const haystacks = [
  67. result.event.displayName,
  68. result.event.summary,
  69. result.release ? result.release.configLabel : '',
  70. result.resolvedRelease ? result.resolvedRelease.configLabel : '',
  71. ]
  72. return haystacks.some((item) => typeof item === 'string' && item.indexOf('多赛道') >= 0)
  73. }
  74. function formatAssignmentMode(mode?: string | null): string {
  75. if (mode === 'manual') {
  76. return '手动选择'
  77. }
  78. if (mode === 'random') {
  79. return '随机分配'
  80. }
  81. if (mode === 'server-assigned') {
  82. return '后台指定'
  83. }
  84. return '默认单赛道'
  85. }
  86. function formatVariantSummary(result: BackendEventPlayResult): string {
  87. const variants = result.play.courseVariants || []
  88. if (!variants.length) {
  89. return '当前未声明额外赛道版本,启动时按默认赛道进入。'
  90. }
  91. const preview = variants.map((item) => {
  92. const title = item.routeCode || item.name
  93. return item.selectable === false ? `${title}(固定)` : title
  94. }).join(' / ')
  95. const selectableCount = variants.filter((item) => item.selectable !== false).length
  96. if (result.play.assignmentMode === 'manual') {
  97. return `当前活动支持 ${variants.length} 条赛道。本阶段前端先展示赛道信息,最终绑定以后端 launch 返回为准:${preview}`
  98. }
  99. if (result.play.assignmentMode === 'random') {
  100. return `当前活动支持 ${variants.length} 条赛道,进入地图前由后端随机绑定:${preview}`
  101. }
  102. if (result.play.assignmentMode === 'server-assigned') {
  103. return `当前活动赛道由后台预先指定:${preview}`
  104. }
  105. if (selectableCount > 1) {
  106. return `当前活动支持 ${variants.length} 条赛道。后端当前未明确返回赛道模式,前端先按手动选择兼容显示:${preview}`
  107. }
  108. return preview
  109. }
  110. function formatPresentationSummary(result: BackendEventPlayResult): string {
  111. const currentPresentation = result.currentPresentation
  112. if (!currentPresentation) {
  113. return '当前发布 release 未绑定展示版本,或当前尚未发布'
  114. }
  115. return `${currentPresentation.presentationId || '--'} / ${currentPresentation.templateKey || '--'} / ${currentPresentation.version || '--'}`
  116. }
  117. function formatContentBundleSummary(result: BackendEventPlayResult): string {
  118. const currentContentBundle = result.currentContentBundle
  119. if (!currentContentBundle) {
  120. return '当前发布 release 未绑定内容包版本,或当前尚未发布'
  121. }
  122. return `${currentContentBundle.bundleId || '--'} / ${currentContentBundle.bundleType || '--'} / ${currentContentBundle.version || '--'}`
  123. }
  124. function resolveSelectedVariantId(
  125. currentVariantId: string,
  126. assignmentMode?: string | null,
  127. variants?: BackendCourseVariantSummary[] | null,
  128. forceVisible?: boolean,
  129. ): string {
  130. if (!shouldShowVariantSelector(assignmentMode, variants, forceVisible)) {
  131. return ''
  132. }
  133. const selectable = (variants || []).filter((item) => item.selectable !== false)
  134. if (!selectable.length) {
  135. return ''
  136. }
  137. const currentStillExists = selectable.some((item) => item.id === currentVariantId)
  138. if (currentVariantId && currentStillExists) {
  139. return currentVariantId
  140. }
  141. return selectable[0].id
  142. }
  143. function buildSelectableVariants(
  144. selectedVariantId: string,
  145. assignmentMode?: string | null,
  146. variants?: BackendCourseVariantSummary[] | null,
  147. forceVisible?: boolean,
  148. ) {
  149. if (!shouldShowVariantSelector(assignmentMode, variants, forceVisible) || !variants || !variants.length) {
  150. return []
  151. }
  152. return variants
  153. .filter((item) => item.selectable !== false)
  154. .map((item) => ({
  155. id: item.id,
  156. name: item.name,
  157. routeCodeText: item.routeCode || '默认编码',
  158. descriptionText: item.description || '暂无赛道说明',
  159. selected: item.id === selectedVariantId,
  160. }))
  161. }
  162. function shouldShowVariantSelector(
  163. assignmentMode?: string | null,
  164. variants?: BackendCourseVariantSummary[] | null,
  165. forceVisible?: boolean,
  166. ): boolean {
  167. if (forceVisible) {
  168. return true
  169. }
  170. const normalizedVariants = variants || []
  171. if (!normalizedVariants.length) {
  172. return false
  173. }
  174. if (assignmentMode === 'manual') {
  175. return true
  176. }
  177. if (assignmentMode === 'random' || assignmentMode === 'server-assigned') {
  178. return false
  179. }
  180. return normalizedVariants.filter((item) => item.selectable !== false).length > 1
  181. }
  182. let prepareHeartRateController: HeartRateController | null = null
  183. function getAccessToken(): string | null {
  184. const app = getApp<IAppOption>()
  185. const tokens = app.globalData && app.globalData.backendAuthTokens
  186. ? app.globalData.backendAuthTokens
  187. : loadBackendAuthTokens()
  188. return tokens && tokens.accessToken ? tokens.accessToken : null
  189. }
  190. function loadPreferredHeartRateDeviceName(): string | null {
  191. try {
  192. const stored = wx.getStorageSync(PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY)
  193. if (!stored || typeof stored !== 'object') {
  194. return null
  195. }
  196. const normalized = stored as { name?: unknown }
  197. return typeof normalized.name === 'string' && normalized.name.trim().length > 0
  198. ? normalized.name.trim()
  199. : '心率带'
  200. } catch (_error) {
  201. return null
  202. }
  203. }
  204. function loadStoredMockChannelId(): string {
  205. try {
  206. const stored = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
  207. if (typeof stored === 'string' && stored.trim().length > 0) {
  208. return stored.trim()
  209. }
  210. } catch (_error) {
  211. return 'default'
  212. }
  213. return 'default'
  214. }
  215. function loadMockAutoConnectEnabled(): boolean {
  216. try {
  217. return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
  218. } catch (_error) {
  219. return false
  220. }
  221. }
  222. Page({
  223. data: {
  224. eventId: '',
  225. loading: false,
  226. canLaunch: false,
  227. titleText: '开始前准备',
  228. summaryText: '未加载',
  229. releaseText: '--',
  230. actionText: '--',
  231. statusText: '待加载',
  232. assignmentMode: '',
  233. variantModeText: '--',
  234. variantSummaryText: '--',
  235. presentationText: '--',
  236. contentBundleText: '--',
  237. runtimePlaceText: '待 launch 确认',
  238. runtimeMapText: '待 launch 确认',
  239. runtimeVariantText: '待 launch 确认',
  240. runtimeRouteCodeText: '待 launch 确认',
  241. selectedVariantId: '',
  242. selectedVariantText: '当前无需手动指定赛道',
  243. showVariantSelector: false,
  244. variantSelectorEmptyText: '当前无需手动指定赛道',
  245. selectableVariants: [],
  246. locationStatusText: '待进入地图后校验定位权限与实时精度',
  247. heartRateStatusText: '局前心率带连接入口待接入,本轮先保留骨架',
  248. heartRateDeviceText: '--',
  249. heartRateScanText: '未扫描',
  250. heartRateConnected: false,
  251. showHeartRateDevicePicker: false,
  252. locationPermissionGranted: false,
  253. locationBackgroundPermissionGranted: false,
  254. heartRateDiscoveredDevices: [],
  255. mockSourceStatusText: '模拟源调试仍在地图页调试面板中使用',
  256. } as EventPreparePageData,
  257. onLoad(query: { eventId?: string }) {
  258. const eventId = query && query.eventId ? decodeURIComponent(query.eventId) : ''
  259. if (!eventId) {
  260. this.setData({
  261. statusText: '缺少 eventId',
  262. })
  263. return
  264. }
  265. this.setData({ eventId })
  266. this.ensurePrepareHeartRateController()
  267. this.refreshPreparationDeviceState()
  268. this.loadEventPlay(eventId)
  269. },
  270. onShow() {
  271. this.refreshPreparationDeviceState()
  272. },
  273. onUnload() {
  274. if (prepareHeartRateController) {
  275. prepareHeartRateController.destroy()
  276. prepareHeartRateController = null
  277. }
  278. },
  279. async loadEventPlay(eventId?: string) {
  280. const targetEventId = eventId || this.data.eventId
  281. const accessToken = getAccessToken()
  282. if (!accessToken) {
  283. wx.redirectTo({ url: '/pages/login/login' })
  284. return
  285. }
  286. this.setData({
  287. loading: true,
  288. statusText: '正在加载局前准备信息',
  289. })
  290. try {
  291. const result = await getEventPlay({
  292. baseUrl: loadBackendBaseUrl(),
  293. eventId: targetEventId,
  294. accessToken,
  295. })
  296. this.applyEventPlay(result)
  297. } catch (error) {
  298. const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
  299. this.setData({
  300. loading: false,
  301. statusText: `局前准备加载失败:${message}`,
  302. })
  303. }
  304. },
  305. applyEventPlay(result: BackendEventPlayResult) {
  306. const multiVariantContext = detectMultiVariantContext(result)
  307. const selectedVariantId = resolveSelectedVariantId(
  308. this.data.selectedVariantId,
  309. result.play.assignmentMode,
  310. result.play.courseVariants,
  311. multiVariantContext,
  312. )
  313. const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
  314. const showVariantSelector = shouldShowVariantSelector(
  315. result.play.assignmentMode,
  316. result.play.courseVariants,
  317. multiVariantContext,
  318. )
  319. const logVariantId = assignmentMode === 'manual' && selectedVariantId ? selectedVariantId : null
  320. const selectableVariants = buildSelectableVariants(
  321. selectedVariantId,
  322. result.play.assignmentMode,
  323. result.play.courseVariants,
  324. multiVariantContext,
  325. )
  326. const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
  327. reportBackendClientLog({
  328. level: 'info',
  329. category: 'event-prepare',
  330. message: 'prepare play loaded',
  331. eventId: result.event.id || this.data.eventId || '',
  332. releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
  333. ? result.resolvedRelease.releaseId
  334. : '',
  335. manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
  336. ? result.resolvedRelease.manifestUrl
  337. : '',
  338. details: {
  339. pageEventId: this.data.eventId || '',
  340. resultEventId: result.event.id || '',
  341. selectedVariantId: logVariantId,
  342. assignmentMode,
  343. variantCount: result.play.courseVariants ? result.play.courseVariants.length : 0,
  344. selectableVariantCount: result.play.courseVariants
  345. ? result.play.courseVariants.filter((item) => item.selectable !== false).length
  346. : 0,
  347. showVariantSelector,
  348. multiVariantContext,
  349. },
  350. })
  351. const variantSelectorEmptyText = multiVariantContext
  352. ? '当前活动按多赛道处理,但后端暂未返回可选赛道,请稍后刷新或联系后台。'
  353. : '当前无需手动指定赛道'
  354. this.setData({
  355. loading: false,
  356. canLaunch: result.play.canLaunch,
  357. titleText: `${result.event.displayName} / 开始前准备`,
  358. summaryText: result.event.summary || '暂无活动简介',
  359. releaseText: result.resolvedRelease
  360. ? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}`
  361. : '当前无可用 release',
  362. actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason),
  363. statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason),
  364. assignmentMode: result.play.assignmentMode || '',
  365. variantModeText: result.play.assignmentMode
  366. ? formatAssignmentMode(result.play.assignmentMode)
  367. : (showVariantSelector ? '手动选择' : '默认单赛道'),
  368. variantSummaryText: formatVariantSummary(result),
  369. presentationText: formatPresentationSummary(result),
  370. contentBundleText: formatContentBundleSummary(result),
  371. runtimePlaceText: '待 launch.runtime 确认',
  372. runtimeMapText: '待 launch.runtime 确认',
  373. runtimeVariantText: selectedVariant
  374. ? selectedVariant.name
  375. : (result.play.courseVariants && result.play.courseVariants[0]
  376. ? result.play.courseVariants[0].name
  377. : '待 launch 确认'),
  378. runtimeRouteCodeText: selectedVariant
  379. ? selectedVariant.routeCodeText
  380. : (result.play.courseVariants && result.play.courseVariants[0] && result.play.courseVariants[0].routeCode
  381. ? result.play.courseVariants[0].routeCode || '待 launch 确认'
  382. : '待 launch 确认'),
  383. selectedVariantId,
  384. selectedVariantText: selectedVariant
  385. ? `${selectedVariant.name} / ${selectedVariant.routeCodeText}`
  386. : variantSelectorEmptyText,
  387. showVariantSelector,
  388. variantSelectorEmptyText,
  389. selectableVariants,
  390. })
  391. },
  392. refreshPreparationDeviceState() {
  393. this.refreshLocationPermissionStatus()
  394. this.refreshHeartRatePreparationStatus()
  395. this.refreshMockSourcePreparationStatus()
  396. },
  397. ensurePrepareHeartRateController() {
  398. if (prepareHeartRateController) {
  399. return prepareHeartRateController
  400. }
  401. prepareHeartRateController = new HeartRateController({
  402. onHeartRate: () => {},
  403. onStatus: (message) => {
  404. this.setData({
  405. heartRateStatusText: message,
  406. })
  407. },
  408. onError: (message) => {
  409. this.setData({
  410. heartRateStatusText: message,
  411. })
  412. },
  413. onConnectionChange: (connected, deviceName) => {
  414. this.setData({
  415. heartRateConnected: connected,
  416. heartRateDeviceText: connected ? (deviceName || '心率带') : (deviceName || '--'),
  417. })
  418. this.refreshHeartRatePreparationStatus()
  419. },
  420. onDeviceListChange: (devices) => {
  421. this.setData({
  422. heartRateScanText: devices.length ? `已发现 ${devices.length} 个设备` : '未扫描',
  423. heartRateDiscoveredDevices: devices.map((device) => ({
  424. deviceId: device.deviceId,
  425. name: device.name,
  426. rssiText: typeof device.rssi === 'number' ? `${device.rssi} dBm` : 'RSSI --',
  427. preferred: !!device.isPreferred,
  428. connected: !!prepareHeartRateController
  429. && !!prepareHeartRateController.currentDeviceId
  430. && prepareHeartRateController.currentDeviceId === device.deviceId
  431. && prepareHeartRateController.connected,
  432. })),
  433. })
  434. },
  435. })
  436. return prepareHeartRateController
  437. },
  438. refreshLocationPermissionStatus() {
  439. wx.getSetting({
  440. success: (result) => {
  441. const authSetting = result && result.authSetting
  442. ? result.authSetting as Record<string, boolean | undefined>
  443. : {}
  444. const hasForeground = authSetting['scope.userLocation'] === true
  445. const hasBackground = authSetting['scope.userLocationBackground'] === true
  446. let locationStatusText = '未请求定位权限'
  447. if (hasForeground && hasBackground) {
  448. locationStatusText = '已授权前后台定位'
  449. } else if (hasForeground) {
  450. locationStatusText = '已授权前台定位'
  451. } else if (authSetting['scope.userLocation'] === false) {
  452. locationStatusText = '定位权限被拒绝'
  453. }
  454. this.setData({
  455. locationStatusText,
  456. locationPermissionGranted: hasForeground,
  457. locationBackgroundPermissionGranted: hasBackground,
  458. })
  459. },
  460. fail: () => {
  461. this.setData({
  462. locationStatusText: '无法读取定位权限状态',
  463. locationPermissionGranted: false,
  464. locationBackgroundPermissionGranted: false,
  465. })
  466. },
  467. })
  468. },
  469. handleRequestLocationPermission() {
  470. wx.authorize({
  471. scope: 'scope.userLocation',
  472. success: () => {
  473. this.refreshLocationPermissionStatus()
  474. wx.showToast({
  475. title: '前台定位已授权',
  476. icon: 'none',
  477. })
  478. },
  479. fail: () => {
  480. this.refreshLocationPermissionStatus()
  481. wx.showToast({
  482. title: '请在设置中开启定位权限',
  483. icon: 'none',
  484. })
  485. },
  486. })
  487. },
  488. handleOpenLocationSettings() {
  489. wx.openSetting({
  490. success: () => {
  491. this.refreshLocationPermissionStatus()
  492. },
  493. fail: () => {
  494. wx.showToast({
  495. title: '无法打开设置面板',
  496. icon: 'none',
  497. })
  498. },
  499. })
  500. },
  501. refreshHeartRatePreparationStatus() {
  502. const controller = this.ensurePrepareHeartRateController()
  503. const preferredDeviceName = loadPreferredHeartRateDeviceName()
  504. this.setData({
  505. heartRateStatusText: controller.connected
  506. ? '局前心率带已连接'
  507. : preferredDeviceName
  508. ? `已记住首选设备:${preferredDeviceName}`
  509. : '未设置首选设备,可在此连接或进入地图后连接',
  510. heartRateDeviceText: controller.currentDeviceName || preferredDeviceName || '--',
  511. heartRateScanText: controller.scanning
  512. ? '扫描中'
  513. : (controller.discoveredDevices.length ? `已发现 ${controller.discoveredDevices.length} 个设备` : '未扫描'),
  514. heartRateConnected: controller.connected,
  515. heartRateDiscoveredDevices: controller.discoveredDevices.map((device) => ({
  516. deviceId: device.deviceId,
  517. name: device.name,
  518. rssiText: typeof device.rssi === 'number' ? `${device.rssi} dBm` : 'RSSI --',
  519. preferred: !!device.isPreferred,
  520. connected: !!controller.currentDeviceId && controller.currentDeviceId === device.deviceId && controller.connected,
  521. })),
  522. })
  523. },
  524. refreshMockSourcePreparationStatus() {
  525. const channelId = loadStoredMockChannelId()
  526. const autoConnect = loadMockAutoConnectEnabled()
  527. this.setData({
  528. mockSourceStatusText: autoConnect
  529. ? `自动连接已开启 / 通道 ${channelId}`
  530. : `自动连接未开启 / 通道 ${channelId}`,
  531. })
  532. },
  533. handleRefresh() {
  534. this.loadEventPlay()
  535. },
  536. handleBack() {
  537. wx.navigateBack()
  538. },
  539. handlePrepareHeartRateConnect() {
  540. const controller = this.ensurePrepareHeartRateController()
  541. controller.startScanAndConnect()
  542. this.refreshHeartRatePreparationStatus()
  543. },
  544. handleOpenHeartRateDevicePicker() {
  545. const controller = this.ensurePrepareHeartRateController()
  546. this.setData({
  547. showHeartRateDevicePicker: true,
  548. })
  549. if (!controller.scanning) {
  550. controller.startScanAndConnect()
  551. }
  552. this.refreshHeartRatePreparationStatus()
  553. },
  554. handleCloseHeartRateDevicePicker() {
  555. this.setData({
  556. showHeartRateDevicePicker: false,
  557. })
  558. },
  559. handlePrepareHeartRateDeviceConnect(event: WechatMiniprogram.BaseEvent<{ deviceId?: string }>) {
  560. const deviceId = event.currentTarget.dataset.deviceId
  561. if (!deviceId) {
  562. return
  563. }
  564. const controller = this.ensurePrepareHeartRateController()
  565. controller.connectToDiscoveredDevice(deviceId)
  566. this.setData({
  567. showHeartRateDevicePicker: false,
  568. })
  569. this.refreshHeartRatePreparationStatus()
  570. },
  571. handlePrepareHeartRateDisconnect() {
  572. if (!prepareHeartRateController) {
  573. return
  574. }
  575. prepareHeartRateController.disconnect()
  576. this.setData({
  577. heartRateConnected: false,
  578. })
  579. this.refreshHeartRatePreparationStatus()
  580. },
  581. handlePrepareHeartRateClearPreferred() {
  582. const controller = this.ensurePrepareHeartRateController()
  583. controller.clearPreferredDevice()
  584. this.refreshHeartRatePreparationStatus()
  585. },
  586. handleSelectVariant(event: WechatMiniprogram.BaseEvent<{ variantId?: string }>) {
  587. const variantId = event.currentTarget.dataset.variantId
  588. if (!variantId) {
  589. return
  590. }
  591. const selectableVariants = this.data.selectableVariants.map((item) => ({
  592. ...item,
  593. selected: item.id === variantId,
  594. }))
  595. const selectedVariant = selectableVariants.find((item) => item.id === variantId) || null
  596. this.setData({
  597. selectedVariantId: variantId,
  598. selectedVariantText: selectedVariant
  599. ? `${selectedVariant.name} / ${selectedVariant.routeCodeText}`
  600. : '当前无需手动指定赛道',
  601. runtimeVariantText: selectedVariant ? selectedVariant.name : '待 launch 确认',
  602. runtimeRouteCodeText: selectedVariant ? selectedVariant.routeCodeText : '待 launch 确认',
  603. selectableVariants,
  604. })
  605. },
  606. async handleLaunch() {
  607. const accessToken = getAccessToken()
  608. if (!accessToken) {
  609. wx.redirectTo({ url: '/pages/login/login' })
  610. return
  611. }
  612. if (!this.data.canLaunch) {
  613. this.setData({
  614. statusText: '当前发布状态不可进入地图',
  615. })
  616. wx.showToast({
  617. title: '当前发布状态不可进入地图',
  618. icon: 'none',
  619. })
  620. return
  621. }
  622. if (!this.data.locationPermissionGranted) {
  623. this.setData({
  624. statusText: '进入地图前请先完成定位授权',
  625. })
  626. wx.showToast({
  627. title: '请先授权定位',
  628. icon: 'none',
  629. })
  630. return
  631. }
  632. this.setData({
  633. statusText: '正在创建 session 并进入地图',
  634. })
  635. try {
  636. const assignmentMode = this.data.assignmentMode ? this.data.assignmentMode : null
  637. const selectedVariantId = this.data.showVariantSelector && this.data.selectedVariantId
  638. ? this.data.selectedVariantId
  639. : null
  640. reportBackendClientLog({
  641. level: 'info',
  642. category: 'event-prepare',
  643. message: 'launch requested',
  644. eventId: this.data.eventId || '',
  645. details: {
  646. pageEventId: this.data.eventId || '',
  647. selectedVariantId,
  648. assignmentMode,
  649. phase: 'launch-requested',
  650. },
  651. })
  652. const app = getApp<IAppOption>()
  653. if (app.globalData) {
  654. const pendingDeviceName = prepareHeartRateController && prepareHeartRateController.currentDeviceName
  655. ? prepareHeartRateController.currentDeviceName
  656. : loadPreferredHeartRateDeviceName()
  657. app.globalData.pendingHeartRateAutoConnect = {
  658. enabled: !!pendingDeviceName,
  659. deviceName: pendingDeviceName || null,
  660. }
  661. }
  662. if (prepareHeartRateController) {
  663. prepareHeartRateController.destroy()
  664. prepareHeartRateController = null
  665. }
  666. const result = await launchEvent({
  667. baseUrl: loadBackendBaseUrl(),
  668. eventId: this.data.eventId,
  669. accessToken,
  670. variantId: this.data.showVariantSelector ? this.data.selectedVariantId : undefined,
  671. clientType: 'wechat',
  672. deviceKey: 'mini-dev-device-001',
  673. })
  674. reportBackendClientLog({
  675. level: 'info',
  676. category: 'event-prepare',
  677. message: 'launch response received',
  678. eventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : this.data.eventId || '',
  679. releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
  680. sessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
  681. manifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
  682. ? result.launch.resolvedRelease.manifestUrl
  683. : '',
  684. details: {
  685. pageEventId: this.data.eventId || '',
  686. launchEventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : '',
  687. launchSessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
  688. configUrl: result.launch.config && result.launch.config.configUrl ? result.launch.config.configUrl : '',
  689. releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
  690. resolvedReleaseId: result.launch.resolvedRelease && result.launch.resolvedRelease.releaseId
  691. ? result.launch.resolvedRelease.releaseId
  692. : '',
  693. resolvedManifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
  694. ? result.launch.resolvedRelease.manifestUrl
  695. : '',
  696. launchVariantId: result.launch.variant && result.launch.variant.id ? result.launch.variant.id : null,
  697. phase: 'launch-response',
  698. },
  699. })
  700. const envelope = adaptBackendLaunchResultToEnvelope(result)
  701. wx.navigateTo({
  702. url: prepareMapPageUrlForLaunch(envelope),
  703. })
  704. } catch (error) {
  705. const message = error && (error as { message?: string }).message ? (error as { message: string }).message : '未知错误'
  706. this.setData({
  707. statusText: `launch 失败:${message}`,
  708. })
  709. }
  710. },
  711. })