Explorar o código

完善设置面板并整理动画阶段总结

zhangyan hai 1 semana
pai
achega
3b5c4501af

+ 50 - 0
GeminiAnlysis.md

@@ -0,0 +1,50 @@
+# CMR-Mini 项目深度分析报告 (GeminiAnalysis.md)
+
+## 1. 项目定位与核心愿景
+**CMR-Mini** 是一个运行在微信小程序环境中的高性能**定向越野 (Orienteering)** 实时竞赛/练习引擎。其核心竞争力在于通过自研的 **WebGL 地图渲染管线** 提供流畅的地图交互,并结合高精度多传感器融合技术(GPS、罗盘、心率、加速度计等)实现精准的运动反馈。
+
+## 2. 核心系统架构分析
+
+### 2.1 地图渲染引擎 (Map Engine)
+*   **渲染技术**:采用 `Single WebGL Pipeline`。相比微信原生地图组件,具有更高的定制化能力,特别是在“Heading-Up”(朝向朝上)模式下的性能表现。
+*   **瓦片管理**:通过 `TileStore` 实现三级缓存(内存 -> 磁盘 -> 网络),并支持 `tilePersistentCache`。
+*   **投影逻辑**:采用 `WGS84 -> WorldTile -> Camera -> Screen` 的标准 GIS 变换链,能够精准处理地理坐标到屏幕像素的映射。
+
+### 2.2 传感器融合系统 (Sensor System)
+*   **CompassHeadingController**:核心逻辑在于罗盘数据 (`wx.onCompassChange`) 与设备姿态 (`wx.onDeviceMotionChange`) 的协同。
+*   **LocationController**:支持真实 GPS 数据与 Mock 模拟器(通过 WebSocket 连接 `mock-gps-sim` 工具)的无缝切换。
+*   **TelemetryRuntime**:实现了运动参数的实时计算,包括速度、距离目标点距离、心率分区等指标。
+
+### 2.3 游戏逻辑与规则 (Game Logic)
+*   **GameRuntime**:驱动对局状态机,支持“顺序赛 (Classic Sequential)”与“积分赛 (Score-O)”。
+*   **PunchPolicy**:实现了自动进入检查点范围触发、手动打点、跳过点位等业务逻辑。
+
+## 3. 指北针 (Compass) 平滑度瓶颈分析
+根据目前的实现,指北针的卡顿感主要源于以下三个层面:
+
+1.  **采样频率与插值逻辑**:
+    *   目前使用 `interpolateHeadingDeg` 进行线性差值,且 `ABSOLUTE_HEADING_CORRECTION` 为固定系数 (0.44)。这种静态系数在“静态微调”时显得不够敏锐,在“快速旋转”时又显得滞后。
+2.  **Android/iOS 差异化丢帧**:
+    *   Android 传感器回调频率不稳定。
+    *   逻辑中对 `direction` 进行了严格的数值有效性判断,若系统由于硬件抖动返回短时异常值,会导致视觉上的“跳帧”。
+3.  **UI 同步周期限制**:
+    *   `MapEngine` 的 `UI_SYNC_INTERVAL_MS` 设置为 80ms,这意味着视觉反馈的最高帧率仅为 12.5Hz,远低于屏幕刷新率,导致指针转动不够丝滑。
+
+## 4. 优化技术路线建议
+
+### 4.1 引入指数加权移动平均 (EWMA) 的动态系数
+建议根据旋转角速度动态调整平滑系数。当检测到瞬时角位移较大时,降低平滑度以追求响应速度;当位移较小时,增加平滑度以过滤手抖带来的噪声。
+
+### 4.2 视觉平滑:使用 CSS Transform 或 WebGL 帧间补偿
+目前数据是由控制器下发到 UI 的。建议:
+*   **方案 A (推荐)**:在 UI 层(`.wxml`/`.wxss`)利用 `transition: transform 0.1s linear;` 实现视觉层面的自动补帧。
+*   **方案 B**:在 WebGL 渲染循环内进行帧间插值,将数据的 12.5Hz 提升到 渲染循环的 60Hz。
+
+### 4.3 预测与死区 (Dead-zone) 过滤
+在 `CompassHeadingController` 中加入微小位移的死区过滤逻辑,避免由于硬件高频微小抖动导致的视图高频重绘,降低系统功耗的同时提升视觉稳定性。
+
+## 5. 结论
+CMR-Mini 已经建立了一个非常坚实的专业定向越野引擎基础。后续的优化重点应从“功能的实现”转向“交互的极致平滑”,特别是针对指北针这类核心导向组件,需要更精细化的信号处理策略。
+
+---
+*Generated by Gemini CLI Analysis Tool*

+ 0 - 0
MyToDo.md


+ 192 - 0
animation-pipeline-summary.md

@@ -0,0 +1,192 @@
+# 动画体系阶段性小结
+
+## 1. 当前定位
+
+目前动画体系已经从“页面里临时加 class”的阶段,进入了**有主链、有分层、有性能分级**的阶段。
+
+当前主链可以概括为:
+
+- 事件触发
+- `feedbackConfig`
+- `UiEffectDirector`
+- `FeedbackDirector`
+- `MapEngine`
+- 页面层 / 渲染层消费
+
+也就是:
+
+**事件 -> 效果配置 -> 宿主提交 -> 页面 / Renderer 落地**
+
+这说明动画已经不再是零散实现,而开始进入架构化管理。
+
+---
+
+## 2. 已经完成的内容
+
+### 2.1 HUD 动效
+
+已经完成:
+
+- 打点成功后的 `进度` 动效
+- 打点成功后的 `点距` 动效
+- HUD 数字轻量过渡:
+  - 计时
+  - 里程
+  - 速度
+  - 心率
+
+这些动效已经接入正式链路,不是页面单独临时处理。
+
+### 2.2 地图空间动画
+
+已经完成:
+
+- 当前目标点状态强调
+- 可打点状态强调
+- 已完成点状态过渡
+- 已跳过点灰态与标记
+- 开始点 / 终点完成后的 settle 外环
+- 轻量地图 pulse
+
+### 2.3 局部 UI / Stage 动效
+
+已经完成:
+
+- 轻量 stage flash
+- 顶部提示和局部反馈的基础动画承载链
+
+### 2.4 动画性能分级
+
+已经完成 2 级动画分级:
+
+- `standard`
+- `lite`
+
+当前 `lite` 的主要策略包括:
+
+- 减少 pulse 层数
+- 降低几何分段
+- 降低渲染动画频率
+- 关闭部分 HUD 动画
+- 关闭或减弱某些 stage/UI 动效
+
+这意味着动画体系已经开始考虑**低端机表现**,不是只追求效果。
+
+---
+
+## 3. 当前架构上的价值
+
+动画体系现在已经带来了几个明确收益:
+
+- 动效不再散落在多个页面细节里
+- 高频状态变化有了统一反馈语言
+- 地图状态和 HUD 状态开始形成一致体验
+- 性能分级已经进入体系,可服务低端机
+
+从架构角度看,这意味着:
+
+**动画已经成为正式能力层,而不是临时视觉补丁。**
+
+---
+
+## 4. 当前还不够完整的地方
+
+虽然主链已经成型,但当前还没有完全形成“动画字典”和完整 profile 体系。
+
+目前仍然存在这些不足:
+
+- 哪些事件触发哪些动画,还没有整理成统一字典
+- 部分高频状态切换还不够连续
+- `跳点` 已有逻辑和状态,但动画语言还不完整
+- 危险/高压状态动画还没有正式开始
+- 动画 profile 还没有真正配置化
+
+所以当前阶段可以定义为:
+
+**第一阶段后半段:主链已成型,但还需要把高频体验打磨完整。**
+
+---
+
+## 5. 下一阶段最值得做的事情
+
+### 5.1 先整理动画字典
+
+建议先把动画按事件梳理出来,例如:
+
+- `session_started`
+- `control_ready`
+- `control_completed:start`
+- `control_completed:control`
+- `control_completed:finish`
+- `control_skipped`
+- `gps_lock_changed`
+- `guidance_state_changed`
+- `heart_rate_zone_changed`
+
+并明确每个事件对应:
+
+- 地图动画
+- HUD 动画
+- UI 动画
+- `lite` 下是否保留
+
+这一步是当前最值得优先完成的工作。
+
+### 5.2 补完整“目标状态切换连续感”
+
+继续打磨:
+
+- 当前目标
+- 进入可打点
+- 打点成功
+- 切到下一个目标
+
+让这一整段切换更连贯、更有节奏。
+
+### 5.3 补齐“跳点”动画
+
+建议下一步把跳点也正式纳入动画体系:
+
+- 跳点确认后
+- 当前点灰化
+- 下一个目标接管强调
+- HUD 给出轻量反馈
+
+### 5.4 再做危险 / 高压反馈
+
+这部分适合进入下一阶段:
+
+- 高心率反馈
+- 危险区反馈
+- 幽灵追逐反馈
+- 边缘呼吸 / 紧张感动效
+
+这条线很适合后续玩法扩展。
+
+---
+
+## 6. 建议的实施顺序
+
+推荐继续推进的顺序:
+
+1. 动画字典整理
+2. 目标切换连续感补齐
+3. 跳点动画补齐
+4. 危险 / 高压状态动画
+5. 更进一步的配置化 profile
+
+---
+
+## 7. 结论
+
+当前动画体系已经是一个明确的阶段性成果:
+
+- 有主链
+- 有分层
+- 有高频核心动画
+- 有性能分级
+
+接下来最该做的不是“继续零散加动画”,而是:
+
+**把现有能力收成动画字典,并优先打磨目标切换与跳点这两条高频体验链。**
+

+ 38 - 0
miniprogram/engine/map/mapEngine.ts

@@ -59,6 +59,7 @@ const AUTO_ROTATE_MAX_STEP_DEG = 0.75
 const AUTO_ROTATE_HEADING_SMOOTHING = 0.46
 const COMPASS_NEEDLE_FRAME_MS = 16
 const COMPASS_NEEDLE_SNAP_DEG = 0.08
+const COMPASS_BOOTSTRAP_RETRY_DELAY_MS = 700
 const COMPASS_TUNING_PRESETS: Record<CompassTuningProfile, {
   needleMinSmoothing: number
   needleMaxSmoothing: number
@@ -830,6 +831,7 @@ export class MapEngine {
   viewSyncTimer: number
   autoRotateTimer: number
   compassNeedleTimer: number
+  compassBootstrapRetryTimer: number
   pendingViewPatch: Partial<MapEngineViewState>
   mounted: boolean
   diagnosticUiEnabled: boolean
@@ -838,6 +840,7 @@ export class MapEngine {
   smoothedSensorHeadingDeg: number | null
   compassDisplayHeadingDeg: number | null
   targetCompassDisplayHeadingDeg: number | null
+  lastCompassSampleAt: number
   compassSource: 'compass' | 'motion' | null
   compassTuningProfile: CompassTuningProfile
   smoothedMovementHeadingDeg: number | null
@@ -1282,6 +1285,7 @@ export class MapEngine {
     this.viewSyncTimer = 0
     this.autoRotateTimer = 0
     this.compassNeedleTimer = 0
+    this.compassBootstrapRetryTimer = 0
     this.pendingViewPatch = {}
     this.mounted = false
     this.diagnosticUiEnabled = false
@@ -1290,6 +1294,7 @@ export class MapEngine {
     this.smoothedSensorHeadingDeg = null
     this.compassDisplayHeadingDeg = null
     this.targetCompassDisplayHeadingDeg = null
+    this.lastCompassSampleAt = 0
     this.compassSource = null
     this.compassTuningProfile = 'balanced'
     this.smoothedMovementHeadingDeg = null
@@ -1406,6 +1411,7 @@ export class MapEngine {
     this.clearViewSyncTimer()
     this.clearAutoRotateTimer()
     this.clearCompassNeedleTimer()
+    this.clearCompassBootstrapRetryTimer()
     this.clearPunchFeedbackTimer()
     this.clearContentCardTimer()
     this.clearMapPulseTimer()
@@ -1424,6 +1430,11 @@ export class MapEngine {
 
   handleAppShow(): void {
     this.feedbackDirector.setAppAudioMode('foreground')
+    if (this.mounted) {
+      this.lastCompassSampleAt = 0
+      this.compassController.start()
+      this.scheduleCompassBootstrapRetry()
+    }
   }
 
   handleAppHide(): void {
@@ -2351,7 +2362,9 @@ export class MapEngine {
     })
     this.syncRenderer()
     this.accelerometerErrorText = null
+    this.lastCompassSampleAt = 0
     this.compassController.start()
+    this.scheduleCompassBootstrapRetry()
     this.gyroscopeController.start()
     this.deviceMotionController.start()
   }
@@ -2980,6 +2993,8 @@ export class MapEngine {
   }
 
   handleCompassHeading(headingDeg: number): void {
+    this.lastCompassSampleAt = Date.now()
+    this.clearCompassBootstrapRetryTimer()
     this.applyHeadingSample(headingDeg, 'compass')
   }
 
@@ -3584,6 +3599,29 @@ export class MapEngine {
     }
   }
 
+  clearCompassBootstrapRetryTimer(): void {
+    if (this.compassBootstrapRetryTimer) {
+      clearTimeout(this.compassBootstrapRetryTimer)
+      this.compassBootstrapRetryTimer = 0
+    }
+  }
+
+  scheduleCompassBootstrapRetry(): void {
+    this.clearCompassBootstrapRetryTimer()
+    if (!this.mounted) {
+      return
+    }
+
+    this.compassBootstrapRetryTimer = setTimeout(() => {
+      this.compassBootstrapRetryTimer = 0
+      if (!this.mounted || this.lastCompassSampleAt > 0) {
+        return
+      }
+      this.compassController.stop()
+      this.compassController.start()
+    }, COMPASS_BOOTSTRAP_RETRY_DELAY_MS) as unknown as number
+  }
+
   syncCompassDisplayState(): void {
     this.setState({
       compassNeedleDeg: formatCompassNeedleDegForMode(this.northReferenceMode, this.compassDisplayHeadingDeg),

+ 241 - 49
miniprogram/pages/map/map.ts

@@ -29,15 +29,37 @@ type ScaleRulerMajorMarkData = {
   topPx: number
   label: string
 }
-type SideButtonMode = 'all' | 'left' | 'right' | 'hidden'
+type SideButtonMode = 'shown' | 'hidden'
 type SideActionButtonState = 'muted' | 'default' | 'active'
+type SideButtonPlacement = 'left' | 'right'
 type CenterScaleRulerAnchorMode = 'screen-center' | 'compass-center'
 type UserNorthReferenceMode = 'magnetic' | 'true'
+type CompassTuningProfile = 'smooth' | 'balanced' | 'responsive'
+type SettingLockKey =
+  | 'lockAnimationLevel'
+  | 'lockSideButtonPlacement'
+  | 'lockAutoRotate'
+  | 'lockCompassTuning'
+  | 'lockScaleRulerVisible'
+  | 'lockScaleRulerAnchor'
+  | 'lockNorthReference'
+  | 'lockHeartRateDevice'
 type StoredUserSettings = {
   animationLevel?: AnimationLevel
+  autoRotateEnabled?: boolean
+  compassTuningProfile?: CompassTuningProfile
   northReferenceMode?: UserNorthReferenceMode
+  sideButtonPlacement?: SideButtonPlacement
   showCenterScaleRuler?: boolean
   centerScaleRulerAnchorMode?: CenterScaleRulerAnchorMode
+  lockAnimationLevel?: boolean
+  lockSideButtonPlacement?: boolean
+  lockAutoRotate?: boolean
+  lockCompassTuning?: boolean
+  lockScaleRulerVisible?: boolean
+  lockScaleRulerAnchor?: boolean
+  lockNorthReference?: boolean
+  lockHeartRateDevice?: boolean
 }
 type MapPageData = MapEngineViewState & {
   showDebugPanel: boolean
@@ -68,6 +90,16 @@ type MapPageData = MapEngineViewState & {
   compassTicks: CompassTickData[]
   compassLabels: CompassLabelData[]
   sideButtonMode: SideButtonMode
+  sideButtonPlacement: SideButtonPlacement
+  autoRotateEnabled: boolean
+  lockAnimationLevel: boolean
+  lockSideButtonPlacement: boolean
+  lockAutoRotate: boolean
+  lockCompassTuning: boolean
+  lockScaleRulerVisible: boolean
+  lockScaleRulerAnchor: boolean
+  lockNorthReference: boolean
+  lockHeartRateDevice: boolean
   sideToggleIconSrc: string
   sideButton2Class: string
   sideButton4Class: string
@@ -334,12 +366,45 @@ function loadStoredUserSettings(): StoredUserSettings {
     if (normalized.northReferenceMode === 'magnetic' || normalized.northReferenceMode === 'true') {
       settings.northReferenceMode = normalized.northReferenceMode
     }
+    if (typeof normalized.autoRotateEnabled === 'boolean') {
+      settings.autoRotateEnabled = normalized.autoRotateEnabled
+    }
+    if (normalized.compassTuningProfile === 'smooth' || normalized.compassTuningProfile === 'balanced' || normalized.compassTuningProfile === 'responsive') {
+      settings.compassTuningProfile = normalized.compassTuningProfile
+    }
+    if (normalized.sideButtonPlacement === 'left' || normalized.sideButtonPlacement === 'right') {
+      settings.sideButtonPlacement = normalized.sideButtonPlacement
+    }
     if (typeof normalized.showCenterScaleRuler === 'boolean') {
       settings.showCenterScaleRuler = normalized.showCenterScaleRuler
     }
     if (normalized.centerScaleRulerAnchorMode === 'screen-center' || normalized.centerScaleRulerAnchorMode === 'compass-center') {
       settings.centerScaleRulerAnchorMode = normalized.centerScaleRulerAnchorMode
     }
+    if (typeof normalized.lockAnimationLevel === 'boolean') {
+      settings.lockAnimationLevel = normalized.lockAnimationLevel
+    }
+    if (typeof normalized.lockSideButtonPlacement === 'boolean') {
+      settings.lockSideButtonPlacement = normalized.lockSideButtonPlacement
+    }
+    if (typeof normalized.lockAutoRotate === 'boolean') {
+      settings.lockAutoRotate = normalized.lockAutoRotate
+    }
+    if (typeof normalized.lockCompassTuning === 'boolean') {
+      settings.lockCompassTuning = normalized.lockCompassTuning
+    }
+    if (typeof normalized.lockScaleRulerVisible === 'boolean') {
+      settings.lockScaleRulerVisible = normalized.lockScaleRulerVisible
+    }
+    if (typeof normalized.lockScaleRulerAnchor === 'boolean') {
+      settings.lockScaleRulerAnchor = normalized.lockScaleRulerAnchor
+    }
+    if (typeof normalized.lockNorthReference === 'boolean') {
+      settings.lockNorthReference = normalized.lockNorthReference
+    }
+    if (typeof normalized.lockHeartRateDevice === 'boolean') {
+      settings.lockHeartRateDevice = normalized.lockHeartRateDevice
+    }
     return settings
   } catch {
     return {}
@@ -351,26 +416,24 @@ function persistStoredUserSettings(settings: StoredUserSettings) {
     wx.setStorageSync(USER_SETTINGS_STORAGE_KEY, settings)
   } catch {}
 }
+
+function toggleStoredSettingLock(settings: StoredUserSettings, key: SettingLockKey): StoredUserSettings {
+  return {
+    ...settings,
+    [key]: !settings[key],
+  }
+}
 function buildSideButtonVisibility(mode: SideButtonMode) {
   return {
     sideButtonMode: mode,
-    showLeftButtonGroup: mode === 'all' || mode === 'left' || mode === 'right',
-    showRightButtonGroups: mode === 'all' || mode === 'right',
-    showBottomDebugButton: mode !== 'hidden',
+    showLeftButtonGroup: mode === 'shown',
+    showRightButtonGroups: false,
+    showBottomDebugButton: true,
   }
 }
 
 function getNextSideButtonMode(currentMode: SideButtonMode): SideButtonMode {
-  if (currentMode === 'all') {
-    return 'left'
-  }
-  if (currentMode === 'left') {
-    return 'right'
-  }
-  if (currentMode === 'right') {
-    return 'hidden'
-  }
-  return 'left'
+  return currentMode === 'shown' ? 'hidden' : 'shown'
 }
 function buildCompassTicks(): CompassTickData[] {
   const ticks: CompassTickData[] = []
@@ -409,9 +472,6 @@ function getFallbackStageRect(): MapEngineStageRect {
 }
 
 function getSideToggleIconSrc(mode: SideButtonMode): string {
-  if (mode === 'left') {
-    return '../../assets/btn_more2.png'
-  }
   if (mode === 'hidden') {
     return '../../assets/btn_more1.png'
   }
@@ -641,6 +701,15 @@ Page({
     hudPanelIndex: 0,
     configSourceText: '顺序赛配置',
     centerScaleRulerAnchorMode: 'screen-center',
+    autoRotateEnabled: false,
+    lockAnimationLevel: false,
+    lockSideButtonPlacement: false,
+    lockAutoRotate: false,
+    lockCompassTuning: false,
+    lockScaleRulerVisible: false,
+    lockScaleRulerAnchor: false,
+    lockNorthReference: false,
+    lockHeartRateDevice: false,
     gameInfoTitle: '当前游戏',
     gameInfoSubtitle: '未开始',
     gameInfoLocalRows: [],
@@ -653,6 +722,7 @@ Page({
     panelDistanceUnitText: '',
     panelProgressText: '0/0',
     showPunchHintBanner: true,
+    sideButtonPlacement: 'left',
     gameSessionStatus: 'idle',
     gameModeText: '顺序赛',
     gpsLockEnabled: false,
@@ -730,9 +800,9 @@ Page({
     centerScaleRulerMajorMarks: [],
     compassTicks: buildCompassTicks(),
     compassLabels: buildCompassLabels(),
-    ...buildSideButtonVisibility('left'),
+    ...buildSideButtonVisibility('shown'),
     ...buildSideButtonState({
-      sideButtonMode: 'left',
+      sideButtonMode: 'shown',
       showGameInfoPanel: false,
       showSystemSettingsPanel: false,
       showCenterScaleRuler: false,
@@ -787,6 +857,9 @@ Page({
         } as MapPageData
 
         const derivedPatch: Partial<MapPageData> = {}
+        if (typeof nextPatch.orientationMode === 'string') {
+          nextData.autoRotateEnabled = nextPatch.orientationMode === 'heading-up'
+        }
         if (
           this.data.showCenterScaleRuler
           && hasAnyPatchKey(nextPatch as Record<string, unknown>, CENTER_SCALE_RULER_DEP_KEYS)
@@ -886,9 +959,19 @@ Page({
     if (storedUserSettings.animationLevel) {
       mapEngine.handleSetAnimationLevel(storedUserSettings.animationLevel)
     }
+    const initialAutoRotateEnabled = storedUserSettings.autoRotateEnabled !== false
+    if (initialAutoRotateEnabled) {
+      mapEngine.handleSetHeadingUpMode()
+    } else {
+      mapEngine.handleSetManualMode()
+    }
+    if (storedUserSettings.compassTuningProfile) {
+      mapEngine.handleSetCompassTuningProfile(storedUserSettings.compassTuningProfile)
+    }
     if (storedUserSettings.northReferenceMode) {
       mapEngine.handleSetNorthReferenceMode(storedUserSettings.northReferenceMode)
     }
+    const initialSideButtonPlacement = storedUserSettings.sideButtonPlacement || 'left'
 
     mapEngine.setDiagnosticUiEnabled(false)
     centerScaleRulerInputCache = {
@@ -914,6 +997,16 @@ Page({
       hudPanelIndex: 0,
       configSourceText: '顺序赛配置',
       centerScaleRulerAnchorMode: initialCenterScaleRulerAnchorMode,
+      autoRotateEnabled: initialAutoRotateEnabled,
+      lockAnimationLevel: !!storedUserSettings.lockAnimationLevel,
+      lockSideButtonPlacement: !!storedUserSettings.lockSideButtonPlacement,
+      lockAutoRotate: !!storedUserSettings.lockAutoRotate,
+      lockCompassTuning: !!storedUserSettings.lockCompassTuning,
+      lockScaleRulerVisible: !!storedUserSettings.lockScaleRulerVisible,
+      lockScaleRulerAnchor: !!storedUserSettings.lockScaleRulerAnchor,
+      lockNorthReference: !!storedUserSettings.lockNorthReference,
+      lockHeartRateDevice: !!storedUserSettings.lockHeartRateDevice,
+      sideButtonPlacement: initialSideButtonPlacement,
       gameInfoTitle: '当前游戏',
       gameInfoSubtitle: '未开始',
       gameInfoLocalRows: [],
@@ -996,9 +1089,9 @@ Page({
       stageFxClass: '',
       compassTicks: buildCompassTicks(),
       compassLabels: buildCompassLabels(),
-      ...buildSideButtonVisibility('left'),
+      ...buildSideButtonVisibility('shown'),
       ...buildSideButtonState({
-        sideButtonMode: 'left',
+        sideButtonMode: 'shown',
         showGameInfoPanel: false,
         showSystemSettingsPanel: false,
         showCenterScaleRuler: initialShowCenterScaleRuler,
@@ -1218,24 +1311,6 @@ Page({
     }
   },
 
-  handleSetCompassTuningSmooth() {
-    if (mapEngine) {
-      mapEngine.handleSetCompassTuningProfile('smooth')
-    }
-  },
-
-  handleSetCompassTuningBalanced() {
-    if (mapEngine) {
-      mapEngine.handleSetCompassTuningProfile('balanced')
-    }
-  },
-
-  handleSetCompassTuningResponsive() {
-    if (mapEngine) {
-      mapEngine.handleSetCompassTuningProfile('responsive')
-    }
-  },
-
   handleAutoRotateCalibrate() {
     if (mapEngine) {
       mapEngine.handleAutoRotateCalibrate()
@@ -1338,11 +1413,14 @@ Page({
       }
     },
 
-    handleClearPreferredHeartRateDevice() {
-      if (mapEngine) {
-        mapEngine.handleClearPreferredHeartRateDevice()
-      }
-    },
+  handleClearPreferredHeartRateDevice() {
+    if (this.data.lockHeartRateDevice) {
+      return
+    }
+    if (mapEngine) {
+      mapEngine.handleClearPreferredHeartRateDevice()
+    }
+  },
 
   handleDebugHeartRateBlue() {
     if (mapEngine) {
@@ -1462,6 +1540,7 @@ Page({
     const localRows = snapshot.localRows.concat([
       { label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
       { label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
+      { label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
       { label: '比例尺可见', value: this.data.centerScaleRulerVisible ? 'true' : 'false' },
       { label: '比例尺中心X', value: `${this.data.centerScaleRulerCenterXPx}px` },
       { label: '比例尺零点Y', value: `${this.data.centerScaleRulerZeroYPx}px` },
@@ -1575,7 +1654,7 @@ Page({
   handleSystemSettingsPanelTap() {},
 
   handleSetAnimationLevelStandard() {
-    if (!mapEngine) {
+    if (this.data.lockAnimationLevel || !mapEngine) {
       return
     }
     mapEngine.handleSetAnimationLevel('standard')
@@ -1586,7 +1665,7 @@ Page({
   },
 
   handleSetAnimationLevelLite() {
-    if (!mapEngine) {
+    if (this.data.lockAnimationLevel || !mapEngine) {
       return
     }
     mapEngine.handleSetAnimationLevel('lite')
@@ -1596,8 +1675,89 @@ Page({
     })
   },
 
+  handleSetSideButtonPlacementLeft() {
+    if (this.data.lockSideButtonPlacement) {
+      return
+    }
+    this.setData({
+      sideButtonPlacement: 'left',
+    })
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      sideButtonPlacement: 'left',
+    })
+  },
+
+  handleSetSideButtonPlacementRight() {
+    if (this.data.lockSideButtonPlacement) {
+      return
+    }
+    this.setData({
+      sideButtonPlacement: 'right',
+    })
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      sideButtonPlacement: 'right',
+    })
+  },
+
+  handleSetAutoRotateEnabledOn() {
+    if (this.data.lockAutoRotate || !mapEngine) {
+      return
+    }
+    mapEngine.handleSetHeadingUpMode()
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      autoRotateEnabled: true,
+    })
+  },
+
+  handleSetAutoRotateEnabledOff() {
+    if (this.data.lockAutoRotate || !mapEngine) {
+      return
+    }
+    mapEngine.handleSetManualMode()
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      autoRotateEnabled: false,
+    })
+  },
+
+  handleSetCompassTuningSmooth() {
+    if (this.data.lockCompassTuning || !mapEngine) {
+      return
+    }
+    mapEngine.handleSetCompassTuningProfile('smooth')
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      compassTuningProfile: 'smooth',
+    })
+  },
+
+  handleSetCompassTuningBalanced() {
+    if (this.data.lockCompassTuning || !mapEngine) {
+      return
+    }
+    mapEngine.handleSetCompassTuningProfile('balanced')
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      compassTuningProfile: 'balanced',
+    })
+  },
+
+  handleSetCompassTuningResponsive() {
+    if (this.data.lockCompassTuning || !mapEngine) {
+      return
+    }
+    mapEngine.handleSetCompassTuningProfile('responsive')
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      compassTuningProfile: 'responsive',
+    })
+  },
+
   handleSetNorthReferenceMagnetic() {
-    if (!mapEngine) {
+    if (this.data.lockNorthReference || !mapEngine) {
       return
     }
     mapEngine.handleSetNorthReferenceMode('magnetic')
@@ -1608,7 +1768,7 @@ Page({
   },
 
   handleSetNorthReferenceTrue() {
-    if (!mapEngine) {
+    if (this.data.lockNorthReference || !mapEngine) {
       return
     }
     mapEngine.handleSetNorthReferenceMode('true')
@@ -1618,6 +1778,18 @@ Page({
     })
   },
 
+  handleToggleSettingLock(event: WechatMiniprogram.TouchEvent) {
+    const key = event.currentTarget.dataset.key as SettingLockKey | undefined
+    if (!key) {
+      return
+    }
+    const nextValue = !this.data[key]
+    this.setData({
+      [key]: nextValue,
+    } as Record<string, boolean>)
+    persistStoredUserSettings(toggleStoredSettingLock(loadStoredUserSettings(), key))
+  },
+
   handleOverlayTouch() {},
 
   handlePunchAction() {
@@ -1674,16 +1846,24 @@ Page({
     }
   },
   handleToggleMapRotateMode() {
-    if (!mapEngine) {
+    if (!mapEngine || this.data.lockAutoRotate) {
       return
     }
 
     if (this.data.orientationMode === 'heading-up') {
       mapEngine.handleSetManualMode()
+      persistStoredUserSettings({
+        ...loadStoredUserSettings(),
+        autoRotateEnabled: false,
+      })
       return
     }
 
     mapEngine.handleSetHeadingUpMode()
+    persistStoredUserSettings({
+      ...loadStoredUserSettings(),
+      autoRotateEnabled: true,
+    })
   },
   handleToggleDebugPanel() {
     const nextShowDebugPanel = !this.data.showDebugPanel
@@ -1788,6 +1968,9 @@ Page({
   },
 
   handleSetCenterScaleRulerVisibleOn() {
+    if (this.data.lockScaleRulerVisible) {
+      return
+    }
     this.applyCenterScaleRulerSettings(true, this.data.centerScaleRulerAnchorMode)
     persistStoredUserSettings({
       ...loadStoredUserSettings(),
@@ -1797,6 +1980,9 @@ Page({
   },
 
   handleSetCenterScaleRulerVisibleOff() {
+    if (this.data.lockScaleRulerVisible) {
+      return
+    }
     this.applyCenterScaleRulerSettings(false, this.data.centerScaleRulerAnchorMode)
     persistStoredUserSettings({
       ...loadStoredUserSettings(),
@@ -1806,6 +1992,9 @@ Page({
   },
 
   handleSetCenterScaleRulerAnchorScreenCenter() {
+    if (this.data.lockScaleRulerAnchor) {
+      return
+    }
     this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'screen-center')
     persistStoredUserSettings({
       ...loadStoredUserSettings(),
@@ -1815,6 +2004,9 @@ Page({
   },
 
   handleSetCenterScaleRulerAnchorCompassCenter() {
+    if (this.data.lockScaleRulerAnchor) {
+      return
+    }
     this.applyCenterScaleRulerSettings(this.data.showCenterScaleRuler, 'compass-center')
     persistStoredUserSettings({
       ...loadStoredUserSettings(),

+ 181 - 87
miniprogram/pages/map/map.wxml

@@ -85,36 +85,30 @@
     <view class="game-punch-hint__close" catchtouchstart="handlePunchHintTap" catchtouchmove="handlePunchHintTap" catchtouchend="handlePunchHintTap" catchtap="handleClosePunchHint">×</view>
   </view>
 
-  <cover-view class="map-side-toggle" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}" style="top: {{topInsetHeight}}px;" bindtap="handleCycleSideButtons">
+  <cover-view class="map-side-toggle {{sideButtonPlacement === 'right' ? 'map-side-toggle--right' : 'map-side-toggle--left'}}" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}" style="top: {{topInsetHeight}}px;" bindtap="handleCycleSideButtons">
     <cover-view class="map-side-button map-side-button--icon">
       <cover-image class="map-side-button__image" src="{{sideToggleIconSrc}}"></cover-image>
     </cover-view>
   </cover-view>
 
-  <cover-view class="map-side-column map-side-column--left map-side-column--left-group" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showLeftButtonGroup}}" style="top: {{topInsetHeight}}px;">
+  <cover-view class="map-side-column {{sideButtonPlacement === 'right' ? 'map-side-column--right-group' : 'map-side-column--left'}} map-side-column--left-group" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showLeftButtonGroup}}" style="top: {{topInsetHeight}}px;">
     <cover-view class="map-side-button map-side-button--icon" bindtap="handleToggleMapRotateMode"><cover-image class="map-side-button__rotate-image {{orientationMode === 'heading-up' ? 'map-side-button__rotate-image--active' : ''}}" src="../../assets/btn_map_rotate_cropped.png"></cover-image></cover-view>
-    <cover-view class="map-side-button map-side-button--muted"><cover-view class="map-side-button__text">1</cover-view></cover-view>
-    <cover-view class="{{sideButton2Class}}" bindtap="handleToggleGpsLock"><cover-view class="map-side-button__text">2</cover-view></cover-view>
-    <cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">3</cover-view></cover-view>
-    <cover-view class="{{sideButton4Class}}" bindtap="handleForceExitGame"><cover-image class="map-side-button__action-image" src="../../assets/btn_exit.png"></cover-image></cover-view>
-  </cover-view>
-
-  <cover-view class="map-side-column map-side-column--right-main" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
-    <cover-view class="map-side-button"><cover-view class="map-side-button__text">5</cover-view></cover-view>
-    <cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">6</cover-view></cover-view>
-    <cover-view class="map-side-button"><cover-view class="map-side-button__text">7</cover-view></cover-view>
-    <cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">8</cover-view></cover-view>
-    <cover-view class="map-side-button map-side-button--muted"><cover-view class="map-side-button__text">9</cover-view></cover-view>
-    <cover-view class="map-side-button map-side-button--active"><cover-view class="map-side-button__text">10</cover-view></cover-view>
-  </cover-view>
-
-  <cover-view class="map-side-column map-side-column--right-sub" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel && showRightButtonGroups}}" style="top: {{topInsetHeight}}px;">
-    <cover-view class="{{sideButton11Class}}" bindtap="handleOpenGameInfoPanel"><cover-image class="map-side-button__action-image" src="../../assets/btn_info.png"></cover-image></cover-view>
-    <cover-view class="{{sideButton12Class}}" bindtap="handleOpenSystemSettingsPanel"><cover-view class="map-side-button__text">12</cover-view></cover-view>
-    <cover-view class="map-side-button"><cover-view class="map-side-button__text">13</cover-view></cover-view>
-    <cover-view class="map-side-button"><cover-view class="map-side-button__text">14</cover-view></cover-view>
-    <cover-view class="map-side-button"><cover-view class="map-side-button__text">15</cover-view></cover-view>
+    <cover-view class="{{sideButton2Class}}" bindtap="handleToggleGpsLock">
+      <cover-image
+        wx:if="{{gpsLockEnabled}}"
+        class="map-side-button__action-image"
+        src="../../assets/btn_locked.png"
+      ></cover-image>
+      <cover-image
+        wx:else
+        class="map-side-button__action-image"
+        src="../../assets/btn_unlock.png"
+      ></cover-image>
+    </cover-view>
     <cover-view class="{{sideButton16Class}}" bindtap="handleSkipAction"><cover-image class="map-side-button__action-image" src="../../assets/btn_skip_cp.png"></cover-image></cover-view>
+    <cover-view class="{{sideButton11Class}}" bindtap="handleOpenGameInfoPanel"><cover-image class="map-side-button__action-image" src="../../assets/btn_info.png"></cover-image></cover-view>
+    <cover-view class="{{sideButton12Class}}" bindtap="handleOpenSystemSettingsPanel"><cover-image class="map-side-button__action-image" src="../../assets/btn_settings.png"></cover-image></cover-view>
+    <cover-view class="{{sideButton4Class}}" bindtap="handleForceExitGame"><cover-image class="map-side-button__action-image" src="../../assets/btn_exit.png"></cover-image></cover-view>
   </cover-view>
 
   <cover-view class="map-punch-button {{punchButtonEnabled ? 'map-punch-button--active' : ''}} {{punchButtonFxClass}}" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showSystemSettingsPanel}}" bindtap="handlePunchAction">
@@ -295,77 +289,179 @@
         </view>
       </view>
 
-      <scroll-view class="game-info-modal__content" scroll-y enhanced show-scrollbar="true">
-        <view class="debug-section debug-section--info">
-          <view class="debug-section__header">
-            <view class="debug-section__title">01. 动画性能</view>
-            <view class="debug-section__desc">根据设备性能切换动画强度,低端机建议精简</view>
-          </view>
-          <view class="info-panel__row">
-            <text class="info-panel__label">当前级别</text>
-            <text class="info-panel__value">{{animationLevel === 'lite' ? '精简' : '标准'}}</text>
-          </view>
-          <view class="control-row">
-            <view class="control-chip {{animationLevel === 'standard' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetAnimationLevelStandard">标准</view>
-            <view class="control-chip {{animationLevel === 'lite' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetAnimationLevelLite">精简</view>
+        <scroll-view class="game-info-modal__content" scroll-y enhanced show-scrollbar="true">
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">01. 动画性能</view>
+                  <view class="debug-section__desc">根据设备性能切换动画强度,低端机建议精简</view>
+                </view>
+                <view class="debug-section__lock {{lockAnimationLevel ? 'debug-section__lock--active' : ''}}" data-key="lockAnimationLevel" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockAnimationLevel ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前级别</text>
+              <text class="info-panel__value">{{animationLevel === 'lite' ? '精简' : '标准'}}{{lockAnimationLevel ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{animationLevel === 'standard' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockAnimationLevel ? 'control-chip--disabled' : ''}}" bindtap="handleSetAnimationLevelStandard">标准</view>
+              <view class="control-chip {{animationLevel === 'lite' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockAnimationLevel ? 'control-chip--disabled' : ''}}" bindtap="handleSetAnimationLevelLite">精简</view>
+            </view>
           </view>
-        </view>
 
-        <view class="debug-section debug-section--info">
-          <view class="debug-section__header">
-            <view class="debug-section__title">02. 比例尺显示</view>
-            <view class="debug-section__desc">控制比例尺显示与否,默认沿用你的本地偏好</view>
-          </view>
-          <view class="info-panel__row">
-            <text class="info-panel__label">当前状态</text>
-            <text class="info-panel__value">{{showCenterScaleRuler ? '显示' : '隐藏'}}</text>
-          </view>
-          <view class="control-row">
-            <view class="control-chip {{showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerVisibleOn">显示</view>
-            <view class="control-chip {{!showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerVisibleOff">隐藏</view>
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">02. 按钮习惯</view>
+                  <view class="debug-section__desc">切换功能按钮显示在左侧还是右侧,适配左手/右手操作习惯</view>
+                </view>
+                <view class="debug-section__lock {{lockSideButtonPlacement ? 'debug-section__lock--active' : ''}}" data-key="lockSideButtonPlacement" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockSideButtonPlacement ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前习惯</text>
+              <text class="info-panel__value">{{sideButtonPlacement === 'right' ? '右手' : '左手'}}{{lockSideButtonPlacement ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{sideButtonPlacement === 'left' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockSideButtonPlacement ? 'control-chip--disabled' : ''}}" bindtap="handleSetSideButtonPlacementLeft">左手</view>
+              <view class="control-chip {{sideButtonPlacement === 'right' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockSideButtonPlacement ? 'control-chip--disabled' : ''}}" bindtap="handleSetSideButtonPlacementRight">右手</view>
+            </view>
           </view>
-        </view>
 
-        <view class="debug-section debug-section--info">
-          <view class="debug-section__header">
-            <view class="debug-section__title">03. 比例尺基准点</view>
-            <view class="debug-section__desc">设置比例尺零点锚定位置,可跟随屏幕中心或指北针圆心</view>
-          </view>
-          <view class="info-panel__row">
-            <text class="info-panel__label">当前锚点</text>
-            <text class="info-panel__value">{{centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心'}}</text>
-          </view>
-          <view class="control-row">
-            <view class="control-chip {{centerScaleRulerAnchorMode === 'screen-center' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerAnchorScreenCenter">屏幕中心</view>
-            <view class="control-chip {{centerScaleRulerAnchorMode === 'compass-center' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCenterScaleRulerAnchorCompassCenter">指北针圆心</view>
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">03. 自动转图</view>
+                  <view class="debug-section__desc">控制地图是否跟随朝向自动旋转,外部按钮与这里保持同步</view>
+                </view>
+                <view class="debug-section__lock {{lockAutoRotate ? 'debug-section__lock--active' : ''}}" data-key="lockAutoRotate" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockAutoRotate ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前状态</text>
+              <text class="info-panel__value">{{autoRotateEnabled ? '开启' : '关闭'}}{{lockAutoRotate ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{autoRotateEnabled ? 'control-chip--active' : 'control-chip--secondary'}} {{lockAutoRotate ? 'control-chip--disabled' : ''}}" bindtap="handleSetAutoRotateEnabledOn">开启</view>
+              <view class="control-chip {{!autoRotateEnabled ? 'control-chip--active' : 'control-chip--secondary'}} {{lockAutoRotate ? 'control-chip--disabled' : ''}}" bindtap="handleSetAutoRotateEnabledOff">关闭</view>
+            </view>
           </view>
-        </view>
 
-        <view class="debug-section debug-section--info">
-          <view class="debug-section__header">
-            <view class="debug-section__title">04. 北参考</view>
-            <view class="debug-section__desc">切换磁北/真北作为地图与指北针参考</view>
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">04. 指北针响应</view>
+                  <view class="debug-section__desc">切换指针的平滑与跟手程度,影响指北针响应手感</view>
+                </view>
+                <view class="debug-section__lock {{lockCompassTuning ? 'debug-section__lock--active' : ''}}" data-key="lockCompassTuning" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockCompassTuning ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前档位</text>
+              <text class="info-panel__value">{{compassTuningProfileText}}{{lockCompassTuning ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row control-row--triple">
+              <view class="control-chip {{compassTuningProfile === 'smooth' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockCompassTuning ? 'control-chip--disabled' : ''}}" bindtap="handleSetCompassTuningSmooth">顺滑</view>
+              <view class="control-chip {{compassTuningProfile === 'balanced' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockCompassTuning ? 'control-chip--disabled' : ''}}" bindtap="handleSetCompassTuningBalanced">平衡</view>
+              <view class="control-chip {{compassTuningProfile === 'responsive' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockCompassTuning ? 'control-chip--disabled' : ''}}" bindtap="handleSetCompassTuningResponsive">跟手</view>
+            </view>
           </view>
-          <view class="info-panel__row">
-            <text class="info-panel__label">当前参考</text>
-            <text class="info-panel__value">{{northReferenceText}}</text>
+
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">05. 比例尺显示</view>
+                  <view class="debug-section__desc">控制比例尺显示与否,默认沿用你的本地偏好</view>
+                </view>
+                <view class="debug-section__lock {{lockScaleRulerVisible ? 'debug-section__lock--active' : ''}}" data-key="lockScaleRulerVisible" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockScaleRulerVisible ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前状态</text>
+              <text class="info-panel__value">{{showCenterScaleRuler ? '显示' : '隐藏'}}{{lockScaleRulerVisible ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}} {{lockScaleRulerVisible ? 'control-chip--disabled' : ''}}" bindtap="handleSetCenterScaleRulerVisibleOn">显示</view>
+              <view class="control-chip {{!showCenterScaleRuler ? 'control-chip--active' : 'control-chip--secondary'}} {{lockScaleRulerVisible ? 'control-chip--disabled' : ''}}" bindtap="handleSetCenterScaleRulerVisibleOff">隐藏</view>
+            </view>
           </view>
-          <view class="control-row">
-            <view class="control-chip {{northReferenceMode === 'magnetic' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetNorthReferenceMagnetic">磁北</view>
-            <view class="control-chip {{northReferenceMode === 'true' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetNorthReferenceTrue">真北</view>
+
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">06. 比例尺基准点</view>
+                  <view class="debug-section__desc">设置比例尺零点锚定位置,可跟随屏幕中心或指北针圆心</view>
+                </view>
+                <view class="debug-section__lock {{lockScaleRulerAnchor ? 'debug-section__lock--active' : ''}}" data-key="lockScaleRulerAnchor" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockScaleRulerAnchor ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前锚点</text>
+              <text class="info-panel__value">{{centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心'}}{{lockScaleRulerAnchor ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{centerScaleRulerAnchorMode === 'screen-center' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockScaleRulerAnchor ? 'control-chip--disabled' : ''}}" bindtap="handleSetCenterScaleRulerAnchorScreenCenter">屏幕中心</view>
+              <view class="control-chip {{centerScaleRulerAnchorMode === 'compass-center' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockScaleRulerAnchor ? 'control-chip--disabled' : ''}}" bindtap="handleSetCenterScaleRulerAnchorCompassCenter">指北针圆心</view>
+            </view>
           </view>
-        </view>
 
-        <view class="debug-section debug-section--info">
-          <view class="debug-section__header">
-            <view class="debug-section__title">05. 心率设备</view>
-            <view class="debug-section__desc">清除已记住的首选心率带设备,下次重新选择</view>
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">07. 北参考</view>
+                  <view class="debug-section__desc">切换磁北/真北作为地图与指北针参考</view>
+                </view>
+                <view class="debug-section__lock {{lockNorthReference ? 'debug-section__lock--active' : ''}}" data-key="lockNorthReference" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockNorthReference ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="info-panel__row">
+              <text class="info-panel__label">当前参考</text>
+              <text class="info-panel__value">{{northReferenceText}}{{lockNorthReference ? ' · 已锁定' : ' · 可编辑'}}</text>
+            </view>
+            <view class="control-row">
+              <view class="control-chip {{northReferenceMode === 'magnetic' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockNorthReference ? 'control-chip--disabled' : ''}}" bindtap="handleSetNorthReferenceMagnetic">磁北</view>
+              <view class="control-chip {{northReferenceMode === 'true' ? 'control-chip--active' : 'control-chip--secondary'}} {{lockNorthReference ? 'control-chip--disabled' : ''}}" bindtap="handleSetNorthReferenceTrue">真北</view>
+            </view>
           </view>
-          <view class="control-row">
-            <view class="control-chip control-chip--secondary" bindtap="handleClearPreferredHeartRateDevice">清除首选设备</view>
+
+          <view class="debug-section debug-section--info">
+            <view class="debug-section__header">
+              <view class="debug-section__header-row">
+                <view class="debug-section__header-main">
+                  <view class="debug-section__title">08. 心率设备</view>
+                  <view class="debug-section__desc">清除已记住的首选心率带设备,下次重新选择</view>
+                </view>
+                <view class="debug-section__lock {{lockHeartRateDevice ? 'debug-section__lock--active' : ''}}" data-key="lockHeartRateDevice" bindtap="handleToggleSettingLock">
+                  <text class="debug-section__lock-text">{{lockHeartRateDevice ? '已锁' : '可改'}}</text>
+                </view>
+              </view>
+            </view>
+            <view class="control-row">
+              <view class="control-chip control-chip--secondary {{lockHeartRateDevice ? 'control-chip--disabled' : ''}}" bindtap="handleClearPreferredHeartRateDevice">清除首选设备</view>
+            </view>
           </view>
-        </view>
-      </scroll-view>
+        </scroll-view>
     </view>
   </view>
 
@@ -429,6 +525,7 @@
             <view class="debug-section__title">Sensors</view>
             <view class="debug-section__desc">定位、罗盘与心率带连接状态</view>
           </view>
+          <view class="debug-group-title">定位</view>
           <view class="info-panel__row">
             <text class="info-panel__label">GPS</text>
             <text class="info-panel__value">{{gpsTrackingText}}</text>
@@ -469,6 +566,7 @@
             <text class="info-panel__label">Mock Speed</text>
             <text class="info-panel__value">{{mockSpeedText}}</text>
           </view>
+          <view class="debug-group-title">心率</view>
           <view class="info-panel__row">
             <text class="info-panel__label">Heart Rate</text>
             <text class="info-panel__value">{{heartRateStatusText}}</text>
@@ -532,6 +630,7 @@
             <text class="info-panel__label">Mock BPM</text>
             <text class="info-panel__value">{{mockHeartRateText}}</text>
           </view>
+          <view class="debug-group-title">方向</view>
           <view class="info-panel__row">
             <text class="info-panel__label">Heading Mode</text>
             <text class="info-panel__value">{{orientationModeText}}</text>
@@ -560,11 +659,6 @@
             <text class="info-panel__label">Compass Tune</text>
             <text class="info-panel__value">{{compassTuningProfileText}}</text>
           </view>
-          <view class="control-row">
-            <view class="control-chip {{compassTuningProfile === 'smooth' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningSmooth">顺滑</view>
-            <view class="control-chip {{compassTuningProfile === 'balanced' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningBalanced">平衡</view>
-            <view class="control-chip {{compassTuningProfile === 'responsive' ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleSetCompassTuningResponsive">跟手</view>
-          </view>
           <view class="info-panel__row info-panel__row--stack">
             <text class="info-panel__label">Accel</text>
             <text class="info-panel__value">{{accelerometerText}}</text>

+ 68 - 1
miniprogram/pages/map/map.wxss

@@ -305,10 +305,17 @@
 
 .map-side-toggle {
   position: absolute;
-  left: 24rpx;
   z-index: 19;
 }
 
+.map-side-toggle--left {
+  left: 24rpx;
+}
+
+.map-side-toggle--right {
+  right: 24rpx;
+}
+
 .map-side-column {
   position: absolute;
   display: flex;
@@ -322,6 +329,10 @@
   left: 24rpx;
 }
 
+.map-side-column--right-group {
+  right: 24rpx;
+}
+
 .map-side-column--left-group {
   padding-top: 106rpx;
 }
@@ -1417,6 +1428,18 @@
   margin-bottom: 12rpx;
 }
 
+.debug-section__header-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16rpx;
+}
+
+.debug-section__header-main {
+  flex: 1;
+  min-width: 0;
+}
+
 .debug-section__title {
   font-size: 24rpx;
   line-height: 1.2;
@@ -1433,6 +1456,36 @@
   color: #6a826f;
 }
 
+.debug-section__lock {
+  min-width: 76rpx;
+  height: 52rpx;
+  padding: 0 16rpx;
+  flex: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 999rpx;
+  background: rgba(233, 242, 228, 0.92);
+  box-shadow: inset 0 0 0 1rpx rgba(22, 48, 32, 0.08);
+}
+
+.debug-section__lock--active {
+  background: rgba(45, 106, 79, 0.18);
+  box-shadow: inset 0 0 0 2rpx rgba(45, 106, 79, 0.18);
+}
+
+.debug-section__lock-text {
+  font-size: 20rpx;
+  line-height: 1;
+  font-weight: 700;
+  color: #45624b;
+  letter-spacing: 1rpx;
+}
+
+.debug-section__lock--active .debug-section__lock-text {
+  color: #2d6a4f;
+}
+
 .info-panel__row {
   display: flex;
   align-items: flex-start;
@@ -1593,6 +1646,16 @@
 .debug-section .control-row:last-child {
   margin-bottom: 0;
 }
+
+.debug-group-title {
+  margin-top: 18rpx;
+  margin-bottom: 8rpx;
+  font-size: 20rpx;
+  font-weight: 800;
+  letter-spacing: 2rpx;
+  color: #6a826f;
+  text-transform: uppercase;
+}
 .control-row--triple .control-chip {
   font-size: 23rpx;
 }
@@ -1624,6 +1687,10 @@
   color: #f7fbf2;
 }
 
+.control-chip--disabled {
+  opacity: 0.48;
+}
+
 
 
 

+ 1 - 1
project.config.json

@@ -13,7 +13,7 @@
     },
     "coverView": false,
     "postcss": false,
-    "minified": false,
+    "minified": true,
     "enhance": false,
     "showShadowRootInWxmlPanel": false,
     "packNpmRelationList": [],