完善多赛道联调与全局产品架构

This commit is contained in:
2026-04-02 18:11:43 +08:00
parent 6964e26ec9
commit 0e28f70bad
45 changed files with 4819 additions and 282 deletions

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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;