Просмотр исходного кода

Revamp map page layout and compass while removing GPS demo

zhangyan 2 недель назад
Родитель
Сommit
277121fd51

+ 4 - 0
miniprogram/assets/compass-north-arrow.svg

@@ -0,0 +1,4 @@
+<svg width="200" height="400" viewBox="0 0 100 200" xmlns="http://www.w3.org/2000/svg">
+  <path d="M50 10 L50 140 L20 190 Z" fill="#FF7F00" stroke="#4A3419" stroke-width="1.5" stroke-linejoin="round"/>
+  <path d="M50 10 L80 190 L50 140 Z" fill="#FDF5E6" stroke="#4A3419" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>

+ 7 - 0
miniprogram/assets/compass-north-arrow2.svg

@@ -0,0 +1,7 @@
+<svg width="200" height="400" viewBox="0 0 100 200" xmlns="http://www.w3.org/2000/svg">
+  <!-- 左半部分:橙色 -->
+  <path d="M50 10 L50 140 L20 190 Z" fill="#FF7F00" stroke="#4A3419" stroke-width="1.5" stroke-linejoin="round"/>
+  
+  <!-- 右半部分:浅米色 -->
+  <path d="M50 10 L80 190 L50 140 Z" fill="#FDF5E6" stroke="#4A3419" stroke-width="1.5" stroke-linejoin="round"/>
+</svg>

+ 9 - 1
miniprogram/engine/layer/gpsLayer.ts

@@ -16,7 +16,11 @@ function buildVectorCamera(scene: MapScene): CameraState {
 }
 }
 
 
 export class GpsLayer implements MapLayer {
 export class GpsLayer implements MapLayer {
-  projectPoint(scene: MapScene): ScreenPoint {
+  projectPoint(scene: MapScene): ScreenPoint | null {
+    if (!scene.gpsPoint) {
+      return null
+    }
+
     const camera = buildVectorCamera(scene)
     const camera = buildVectorCamera(scene)
     const worldPoint = calibratedLonLatToWorldTile(scene.gpsPoint, scene.zoom, scene.gpsCalibration, scene.gpsCalibrationOrigin)
     const worldPoint = calibratedLonLatToWorldTile(scene.gpsPoint, scene.zoom, scene.gpsCalibration, scene.gpsCalibrationOrigin)
     return worldToScreen(camera, worldPoint, false)
     return worldToScreen(camera, worldPoint, false)
@@ -29,6 +33,10 @@ export class GpsLayer implements MapLayer {
   draw(context: LayerRenderContext): void {
   draw(context: LayerRenderContext): void {
     const { ctx, scene, pulseFrame } = context
     const { ctx, scene, pulseFrame } = context
     const gpsScreenPoint = this.projectPoint(scene)
     const gpsScreenPoint = this.projectPoint(scene)
+    if (!gpsScreenPoint) {
+      return
+    }
+
     const pulse = this.getPulseRadius(pulseFrame)
     const pulse = this.getPulseRadius(pulseFrame)
 
 
     ctx.save()
     ctx.save()

+ 3 - 0
miniprogram/engine/layer/trackLayer.ts

@@ -32,6 +32,9 @@ export class TrackLayer implements MapLayer {
   draw(context: LayerRenderContext): void {
   draw(context: LayerRenderContext): void {
     const { ctx, scene } = context
     const { ctx, scene } = context
     const points = this.projectPoints(scene)
     const points = this.projectPoints(scene)
+    if (!points.length) {
+      return
+    }
 
 
     ctx.save()
     ctx.save()
     ctx.lineCap = 'round'
     ctx.lineCap = 'round'

+ 7 - 17
miniprogram/engine/map/mapEngine.ts

@@ -47,17 +47,6 @@ const COMPASS_NEEDLE_SMOOTHING = 0.12
 const GPS_TRACK_MAX_POINTS = 200
 const GPS_TRACK_MAX_POINTS = 200
 const GPS_TRACK_MIN_STEP_METERS = 3
 const GPS_TRACK_MIN_STEP_METERS = 3
 
 
-const SAMPLE_TRACK_WGS84: LonLatPoint[] = [
-  worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.72, y: DEFAULT_CENTER_TILE_Y + 0.44 }, DEFAULT_ZOOM),
-  worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X - 0.18, y: DEFAULT_CENTER_TILE_Y + 0.08 }, DEFAULT_ZOOM),
-  worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X + 0.22, y: DEFAULT_CENTER_TILE_Y - 0.16 }, DEFAULT_ZOOM),
-  worldTileToLonLat({ x: DEFAULT_CENTER_TILE_X + 0.64, y: DEFAULT_CENTER_TILE_Y - 0.52 }, DEFAULT_ZOOM),
-]
-const SAMPLE_GPS_WGS84: LonLatPoint = worldTileToLonLat(
-  { x: DEFAULT_CENTER_TILE_X + 0.12, y: DEFAULT_CENTER_TILE_Y - 0.06 },
-  DEFAULT_ZOOM,
-)
-
 type TouchPoint = WechatMiniprogram.TouchDetail
 type TouchPoint = WechatMiniprogram.TouchDetail
 
 
 type GestureMode = 'idle' | 'pan' | 'pinch'
 type GestureMode = 'idle' | 'pan' | 'pinch'
@@ -435,7 +424,7 @@ export class MapEngine {
   defaultCenterTileX: number
   defaultCenterTileX: number
   defaultCenterTileY: number
   defaultCenterTileY: number
   tileBoundsByZoom: Record<number, TileZoomBounds> | null
   tileBoundsByZoom: Record<number, TileZoomBounds> | null
-  currentGpsPoint: LonLatPoint
+  currentGpsPoint: LonLatPoint | null
   currentGpsTrack: LonLatPoint[]
   currentGpsTrack: LonLatPoint[]
   currentGpsAccuracyMeters: number | null
   currentGpsAccuracyMeters: number | null
   hasGpsCenteredOnce: boolean
   hasGpsCenteredOnce: boolean
@@ -485,7 +474,7 @@ export class MapEngine {
     this.defaultCenterTileX = DEFAULT_CENTER_TILE_X
     this.defaultCenterTileX = DEFAULT_CENTER_TILE_X
     this.defaultCenterTileY = DEFAULT_CENTER_TILE_Y
     this.defaultCenterTileY = DEFAULT_CENTER_TILE_Y
     this.tileBoundsByZoom = null
     this.tileBoundsByZoom = null
-    this.currentGpsPoint = SAMPLE_GPS_WGS84
+    this.currentGpsPoint = null
     this.currentGpsTrack = []
     this.currentGpsTrack = []
     this.currentGpsAccuracyMeters = null
     this.currentGpsAccuracyMeters = null
     this.hasGpsCenteredOnce = false
     this.hasGpsCenteredOnce = false
@@ -1302,13 +1291,12 @@ export class MapEngine {
     const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
     const exactCenter = this.getExactCenterFromTranslate(this.state.tileTranslateX, this.state.tileTranslateY)
     const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, nextRotationDeg)
     const resolvedViewport = this.resolveViewportForExactCenter(exactCenter.x, exactCenter.y, nextRotationDeg)
 
 
-    this.state = {
-      ...this.state,
+    this.setState({
       ...resolvedViewport,
       ...resolvedViewport,
       rotationDeg: nextRotationDeg,
       rotationDeg: nextRotationDeg,
       rotationText: formatRotationText(nextRotationDeg),
       rotationText: formatRotationText(nextRotationDeg),
       centerText: buildCenterText(this.state.zoom, resolvedViewport.centerTileX, resolvedViewport.centerTileY),
       centerText: buildCenterText(this.state.zoom, resolvedViewport.centerTileX, resolvedViewport.centerTileY),
-    }
+    })
     this.syncRenderer()
     this.syncRenderer()
   }
   }
 
 
@@ -1408,7 +1396,7 @@ export class MapEngine {
       previewScale: this.previewScale || 1,
       previewScale: this.previewScale || 1,
       previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
       previewOriginX: this.previewOriginX || this.state.stageWidth / 2,
       previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
       previewOriginY: this.previewOriginY || this.state.stageHeight / 2,
-      track: this.currentGpsTrack.length ? this.currentGpsTrack : SAMPLE_TRACK_WGS84,
+      track: this.currentGpsTrack,
       gpsPoint: this.currentGpsPoint,
       gpsPoint: this.currentGpsPoint,
       gpsCalibration: GPS_MAP_CALIBRATION,
       gpsCalibration: GPS_MAP_CALIBRATION,
       gpsCalibrationOrigin: worldTileToLonLat({ x: this.defaultCenterTileX, y: this.defaultCenterTileY }, this.defaultZoom),
       gpsCalibrationOrigin: worldTileToLonLat({ x: this.defaultCenterTileX, y: this.defaultCenterTileY }, this.defaultZoom),
@@ -1807,6 +1795,8 @@ export class MapEngine {
 
 
 
 
 
 
+
+
 
 
 
 
 
 

+ 1 - 1
miniprogram/engine/renderer/mapRenderer.ts

@@ -23,7 +23,7 @@ export interface MapScene {
   previewOriginX: number
   previewOriginX: number
   previewOriginY: number
   previewOriginY: number
   track: LonLatPoint[]
   track: LonLatPoint[]
-  gpsPoint: LonLatPoint
+  gpsPoint: LonLatPoint | null
   gpsCalibration: MapCalibration
   gpsCalibration: MapCalibration
   gpsCalibrationOrigin: LonLatPoint
   gpsCalibrationOrigin: LonLatPoint
   osmReferenceEnabled: boolean
   osmReferenceEnabled: boolean

+ 9 - 4
miniprogram/engine/renderer/webglVectorRenderer.ts

@@ -137,9 +137,15 @@ export class WebGLVectorRenderer {
       this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene)
       this.pushCircle(positions, colors, point.x, point.y, 6.5, [0.97, 0.98, 0.95, 1], scene)
     }
     }
 
 
-    this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene)
-    this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene)
-    this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene)
+    if (gpsPoint) {
+      this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, this.gpsLayer.getPulseRadius(pulseFrame), [0.13, 0.62, 0.74, 0.22], scene)
+      this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 13, [1, 1, 1, 0.95], scene)
+      this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], scene)
+    }
+
+    if (!positions.length) {
+      return
+    }
 
 
     gl.viewport(0, 0, this.canvas.width, this.canvas.height)
     gl.viewport(0, 0, this.canvas.width, this.canvas.height)
     gl.useProgram(this.program)
     gl.useProgram(this.program)
@@ -231,4 +237,3 @@ export class WebGLVectorRenderer {
     }
     }
   }
   }
 }
 }
-

+ 1 - 1
miniprogram/pages/map/map.json

@@ -1,4 +1,4 @@
 {
 {
-  "navigationBarTitleText": "地图",
+  "navigationStyle": "custom",
   "disableScroll": true
   "disableScroll": true
 }
 }

+ 90 - 40
miniprogram/pages/map/map.ts

@@ -1,39 +1,107 @@
 import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
 import { MapEngine, type MapEngineStageRect, type MapEngineViewState } from '../../engine/map/mapEngine'
 import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
 import { loadRemoteMapConfig } from '../../utils/remoteMapConfig'
-
+type CompassTickData = {
+  angle: number
+  long: boolean
+  major: boolean
+}
+type CompassLabelData = {
+  text: string
+  angle: number
+  rotateBack: number
+  radius: number
+  className: string
+}
 type MapPageData = MapEngineViewState & {
 type MapPageData = MapEngineViewState & {
   showDebugPanel: boolean
   showDebugPanel: boolean
+  statusBarHeight: number
+  topInsetHeight: number
+  panelTimerText: string
+  panelMileageText: string
+  panelDistanceValueText: string
+  panelProgressText: string
+  panelSpeedValueText: string
+  compassTicks: CompassTickData[]
+  compassLabels: CompassLabelData[]
 }
 }
-
-const INTERNAL_BUILD_VERSION = 'map-build-99'
+const INTERNAL_BUILD_VERSION = 'map-build-106'
 const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json'
 const REMOTE_GAME_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json'
-
 let mapEngine: MapEngine | null = null
 let mapEngine: MapEngine | null = null
-
+function buildCompassTicks(): CompassTickData[] {
+  const ticks: CompassTickData[] = []
+  for (let angle = 0; angle < 360; angle += 5) {
+    ticks.push({
+      angle,
+      long: angle % 15 === 0,
+      major: angle % 45 === 0,
+    })
+  }
+  return ticks
+}
+function buildCompassLabels(): CompassLabelData[] {
+  return [
+    { text: '\u5317', angle: 0, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal compass-widget__mark--north' },
+    { text: '\u4e1c\u5317', angle: 45, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northeast' },
+    { text: '\u4e1c', angle: 90, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
+    { text: '\u4e1c\u5357', angle: 135, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
+    { text: '\u5357', angle: 180, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
+    { text: '\u897f\u5357', angle: 225, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate' },
+    { text: '\u897f', angle: 270, rotateBack: 0, radius: 68, className: 'compass-widget__mark--cardinal' },
+    { text: '\u897f\u5317', angle: 315, rotateBack: 0, radius: 58, className: 'compass-widget__mark--intermediate compass-widget__mark--northwest' },
+  ]
+}
 function getFallbackStageRect(): MapEngineStageRect {
 function getFallbackStageRect(): MapEngineStageRect {
   const systemInfo = wx.getSystemInfoSync()
   const systemInfo = wx.getSystemInfoSync()
-  const width = Math.max(320, systemInfo.windowWidth - 20)
-  const height = Math.max(280, Math.floor(systemInfo.windowHeight * 0.66))
+  const width = Math.max(320, systemInfo.windowWidth)
+  const height = Math.max(280, systemInfo.windowHeight)
 
 
   return {
   return {
     width,
     width,
     height,
     height,
-    left: 10,
+    left: 0,
     top: 0,
     top: 0,
   }
   }
 }
 }
 
 
 Page({
 Page({
-  data: { showDebugPanel: false } as MapPageData,
+  data: {
+    showDebugPanel: false,
+    statusBarHeight: 0,
+    topInsetHeight: 12,
+    panelTimerText: '00:00:00',
+    panelMileageText: '0m',
+    panelDistanceValueText: '108',
+    panelProgressText: '0/14',
+    panelSpeedValueText: '0',
+    compassTicks: buildCompassTicks(),
+    compassLabels: buildCompassLabels(),
+  } as MapPageData,
 
 
   onLoad() {
   onLoad() {
+    const systemInfo = wx.getSystemInfoSync()
+    const statusBarHeight = systemInfo.statusBarHeight || 0
+    const menuButtonRect = wx.getMenuButtonBoundingClientRect()
+    const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
+
     mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
     mapEngine = new MapEngine(INTERNAL_BUILD_VERSION, {
       onData: (patch) => {
       onData: (patch) => {
         this.setData(patch)
         this.setData(patch)
       },
       },
     })
     })
 
 
-    this.setData({ ...mapEngine.getInitialData(), showDebugPanel: false })
+    this.setData({
+      ...mapEngine.getInitialData(),
+      showDebugPanel: false,
+      statusBarHeight,
+      topInsetHeight: Math.max(statusBarHeight + 12, menuButtonBottom + 20),
+      panelTimerText: '00:00:00',
+      panelMileageText: '0m',
+      panelDistanceValueText: '108',
+      panelProgressText: '0/14',
+      panelSpeedValueText: '0',
+    compassTicks: buildCompassTicks(),
+    compassLabels: buildCompassLabels(),
+    })
   },
   },
 
 
   onReady() {
   onReady() {
@@ -67,10 +135,10 @@ Page({
           return
           return
         }
         }
 
 
-        const errorMessage = error && error.message ? error.message : '未知错误'
+        const errorMessage = error && error.message ? error.message : '鏈煡閿欒'
         this.setData({
         this.setData({
-          configStatusText: `载入失败: ${errorMessage}`,
-          statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
+          configStatusText: `杞藉叆澶辫触: ${errorMessage}`,
+          statusText: `杩滅▼鍦板浘閰嶇疆杞藉叆澶辫触: ${errorMessage} (${INTERNAL_BUILD_VERSION})`,
         })
         })
       })
       })
   },
   },
@@ -99,7 +167,7 @@ Page({
         const canvasRef = canvasRes[0] as any
         const canvasRef = canvasRes[0] as any
         if (!canvasRef || !canvasRef.node) {
         if (!canvasRef || !canvasRef.node) {
           page.setData({
           page.setData({
-            statusText: `WebGL 引擎初始化失败 (${INTERNAL_BUILD_VERSION})`,
+            statusText: `WebGL 寮曟搸鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
           })
           })
           return
           return
         }
         }
@@ -109,7 +177,7 @@ Page({
           currentEngine.attachCanvas(canvasRef.node, rect.width, rect.height, dpr)
           currentEngine.attachCanvas(canvasRef.node, rect.width, rect.height, dpr)
         } catch (error) {
         } catch (error) {
           page.setData({
           page.setData({
-            statusText: `WebGL 初始化失败 (${INTERNAL_BUILD_VERSION})`,
+            statusText: `WebGL 鍒濆鍖栧け璐?(${INTERNAL_BUILD_VERSION})`,
           })
           })
         }
         }
       })
       })
@@ -212,33 +280,15 @@ Page({
       showDebugPanel: !this.data.showDebugPanel,
       showDebugPanel: !this.data.showDebugPanel,
     })
     })
   },
   },
-})
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 
 
+  handleCloseDebugPanel() {
+    this.setData({
+      showDebugPanel: false,
+    })
+  },
 
 
+  handleDebugPanelTap() {},
+})
 
 
 
 
 
 

+ 228 - 154
miniprogram/pages/map/map.wxml

@@ -1,180 +1,254 @@
 <view class="page">
 <view class="page">
-  <view class="page__header">
-    <view>
-      <view class="page__eyebrow">CMR MINI PROGRAM</view>
-      <view class="page__title">{{mapName}}</view>
+  <view
+    class="map-stage"
+    catchtouchstart="handleTouchStart"
+    catchtouchmove="handleTouchMove"
+    catchtouchend="handleTouchEnd"
+    catchtouchcancel="handleTouchCancel"
+  >
+    <view class="map-content">
+      <canvas
+        id="mapCanvas"
+        type="webgl"
+        canvas-id="mapCanvas"
+        class="map-canvas map-canvas--base"
+      ></canvas>
     </view>
     </view>
-    <view class="page__badge">{{mapReadyText}}</view>
-  </view>
 
 
-  <view class="map-stage-wrap">
-    <view
-      class="map-stage"
-      catchtouchstart="handleTouchStart"
-      catchtouchmove="handleTouchMove"
-      catchtouchend="handleTouchEnd"
-      catchtouchcancel="handleTouchCancel"
-    >
-      <view class="map-content">
-        <canvas
-          id="mapCanvas"
-          type="webgl"
-          canvas-id="mapCanvas"
-          class="map-canvas map-canvas--base"
-        ></canvas>
-      </view>
+    <view class="map-stage__crosshair"></view>
 
 
-      <view class="map-stage__crosshair"></view>
+    <view class="map-stage__overlay">
+      <view class="map-stage__topbar" style="padding-top: {{topInsetHeight}}px;">
+        <view class="map-stage__meta">
+          <view class="map-stage__eyebrow">CMR MINI MAP</view>
+          <view class="map-stage__title">{{mapName}}</view>
+          <view class="map-stage__badge">{{mapReadyText}}</view>
+        </view>
+      </view>
 
 
-      <view class="map-stage__overlay">
-        <view class="overlay-card">
-          <view class="overlay-card__label">WEBGL MAP ENGINE</view>
-          <view class="overlay-card__title">North Up / Heading Up / Manual</view>
-          <view class="overlay-card__desc">
-            地图北已经固定为正上方。现在支持手动旋转、北朝上、朝向朝上三种模式,并提供指北针用于校验朝向。
-          </view>
+      <view class="map-stage__bottom">
+        <view class="map-stage__status">
+          <view class="map-stage__status-label">当前模式</view>
+          <view class="map-stage__status-value">{{orientationModeText}}</view>
+          <view class="map-stage__status-meta">{{gpsTrackingText}} · 缩放 {{zoom}}</view>
         </view>
         </view>
 
 
         <view class="compass-widget">
         <view class="compass-widget">
-          <view class="compass-widget__ring">
-            <view class="compass-widget__north">N</view>
-            <view class="compass-widget__needle" style="transform: translateX(-50%) rotate({{compassNeedleDeg}}deg);"></view>
-            <view class="compass-widget__center"></view>
+          <view class="compass-widget__heading">{{sensorHeadingText}}</view>
+          <view class="compass-widget__dial">
+            <view class="compass-widget__glass"></view>
+            <view class="compass-widget__inner-shadow"></view>
+            <view class="compass-widget__card" style="transform: rotate({{rotationDeg}}deg);">
+              <image class="compass-widget__north-arrow" src="../../assets/compass-north-arrow.svg" mode="aspectFit"></image>
+              <view wx:for="{{compassTicks}}" wx:key="angle" class="compass-widget__tick-anchor" style="transform: translate(-50%, -50%) rotate({{item.angle}}deg);">
+                <view class="compass-widget__tick {{item.long ? 'compass-widget__tick--long' : 'compass-widget__tick--short'}} {{item.major ? 'compass-widget__tick--major' : ''}}"></view>
+              </view>
+              <view wx:for="{{compassLabels}}" wx:key="text" class="compass-widget__mark-anchor" style="transform: translate(-50%, -50%) rotate({{item.angle}}deg);">
+                <view class="compass-widget__mark {{item.className}}" style="transform: translate(-50%, -50%) translateY(-{{item.radius}}rpx) rotate({{item.rotateBack}}deg);">{{item.text}}</view>
+              </view>
+            </view>
+            <view class="compass-widget__needle-anchor" style="transform: translate(-50%, -50%) rotate({{compassNeedleDeg}}deg);">
+              <view class="compass-widget__needle-north"></view>
+              <view class="compass-widget__needle-south"></view>
+            </view>
+            <view class="compass-widget__hub"></view>
+            <view class="compass-widget__hub-core"></view>
           </view>
           </view>
-          <view class="compass-widget__label">{{sensorHeadingText}}</view>
           <view class="compass-widget__hint" wx:if="{{compassDeclinationText}}">{{compassDeclinationText}}</view>
           <view class="compass-widget__hint" wx:if="{{compassDeclinationText}}">{{compassDeclinationText}}</view>
         </view>
         </view>
       </view>
       </view>
     </view>
     </view>
   </view>
   </view>
 
 
-  <scroll-view class="info-panel" scroll-y enhanced show-scrollbar="true">
-    <view class="info-panel__row">
-      <text class="info-panel__label">Build</text>
-      <text class="info-panel__value">{{buildVersion}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <text class="info-panel__label">Config</text>
-      <text class="info-panel__value">{{configStatusText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Heading Mode</text>
-      <text class="info-panel__value">{{orientationModeText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Sensor Heading</text>
-      <text class="info-panel__value">{{sensorHeadingText}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <text class="info-panel__label">North Ref</text>
-      <text class="info-panel__value">{{northReferenceText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Zoom</text>
-      <text class="info-panel__value">{{zoom}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">Rotation</text>
-      <text class="info-panel__value">{{rotationText}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <text class="info-panel__label">Status</text>
-      <text class="info-panel__value">{{statusText}}</text>
-    </view>
-    <view class="info-panel__row">
-      <text class="info-panel__label">GPS</text>
-      <text class="info-panel__value">{{gpsTrackingText}}</text>
-    </view>
-    <view class="info-panel__row info-panel__row--stack">
-      <text class="info-panel__label">GPS Coord</text>
-      <text class="info-panel__value">{{gpsCoordText}}</text>
-    </view>
-    <view class="control-row">
-      <view class="control-chip control-chip--secondary" bindtap="handleToggleDebugPanel">{{showDebugPanel ? '隐藏调试' : '查看调试'}}</view>
-    </view>
+  <cover-view class="screen-button-layer" wx:if="{{!showDebugPanel}}" style="top: {{topInsetHeight}}px;" bindtap="handleToggleDebugPanel">
+    <cover-view class="screen-button-layer__icon">
+      <cover-view class="screen-button-layer__line"></cover-view>
+      <cover-view class="screen-button-layer__stand"></cover-view>
+    </cover-view>
+    <cover-view class="screen-button-layer__text">屏幕</cover-view>
+  </cover-view>
 
 
-    <block wx:if="{{showDebugPanel}}">
-      <view class="info-panel__row">
-        <text class="info-panel__label">Renderer</text>
-        <text class="info-panel__value">{{renderMode}}</text>
-      </view>
-      <view class="info-panel__row info-panel__row--stack">
-        <text class="info-panel__label">Projection</text>
-        <text class="info-panel__value">{{projectionMode}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Auto Source</text>
-        <text class="info-panel__value">{{autoRotateSourceText}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Calibration</text>
-        <text class="info-panel__value">{{autoRotateCalibrationText}}</text>
-      </view>
-      <view class="info-panel__row info-panel__row--stack">
-        <text class="info-panel__label">Tile URL</text>
-        <text class="info-panel__value">{{tileSource}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Center Tile</text>
-        <text class="info-panel__value">{{centerText}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Tile Size</text>
-        <text class="info-panel__value">{{tileSizePx}}px</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Visible Tiles</text>
-        <text class="info-panel__value">{{visibleTileCount}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Ready Tiles</text>
-        <text class="info-panel__value">{{readyTileCount}}</text>
-      </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Memory Tiles</text>
-        <text class="info-panel__value">{{memoryTileCount}}</text>
+  <view class="race-panel">
+    <view class="race-panel__tag race-panel__tag--top-left">目标</view>
+    <view class="race-panel__tag race-panel__tag--top-right">里程</view>
+    <view class="race-panel__tag race-panel__tag--bottom-left">点距</view>
+    <view class="race-panel__tag race-panel__tag--bottom-right">速度</view>
+
+    <view class="race-panel__line race-panel__line--center"></view>
+    <view class="race-panel__line race-panel__line--left-mid"></view>
+    <view class="race-panel__line race-panel__line--right-mid"></view>
+    <view class="race-panel__line race-panel__line--left-top"></view>
+    <view class="race-panel__line race-panel__line--left-bottom"></view>
+    <view class="race-panel__line race-panel__line--right-top"></view>
+    <view class="race-panel__line race-panel__line--right-bottom"></view>
+
+    <view class="race-panel__grid">
+      <view class="race-panel__cell race-panel__cell--action">
+        <view class="race-panel__play"></view>
+      </view>
+      <view class="race-panel__cell race-panel__cell--timer">
+        <text class="race-panel__timer">{{panelTimerText}}</text>
+      </view>
+      <view class="race-panel__cell race-panel__cell--mileage">
+        <view class="race-panel__mileage-wrap">
+          <text class="race-panel__mileage">{{panelMileageText}}</text>
+          <view class="race-panel__chevrons">
+            <view class="race-panel__chevron"></view>
+            <view class="race-panel__chevron race-panel__chevron--offset"></view>
+          </view>
+        </view>
       </view>
       </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Disk Tiles</text>
-        <text class="info-panel__value">{{diskTileCount}}</text>
+      <view class="race-panel__cell race-panel__cell--distance">
+        <view class="race-panel__metric-group race-panel__metric-group--left">
+          <text class="race-panel__metric-value race-panel__metric-value--distance">{{panelDistanceValueText}}</text>
+          <text class="race-panel__metric-unit race-panel__metric-unit--distance">m</text>
+        </view>
       </view>
       </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Cache Hit</text>
-        <text class="info-panel__value">{{cacheHitRateText}}</text>
+      <view class="race-panel__cell race-panel__cell--progress">
+        <text class="race-panel__progress">{{panelProgressText}}</text>
       </view>
       </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Disk Hits</text>
-        <text class="info-panel__value">{{diskHitCount}}</text>
+      <view class="race-panel__cell race-panel__cell--speed">
+        <view class="race-panel__metric-group race-panel__metric-group--right">
+          <text class="race-panel__metric-value race-panel__metric-value--speed">{{panelSpeedValueText}}</text>
+          <text class="race-panel__metric-unit race-panel__metric-unit--speed">km/h</text>
+        </view>
       </view>
       </view>
-      <view class="info-panel__row">
-        <text class="info-panel__label">Net Fetches</text>
-        <text class="info-panel__value">{{networkFetchCount}}</text>
+    </view>
+  </view>
+
+  <view class="debug-modal" wx:if="{{showDebugPanel}}" bindtap="handleCloseDebugPanel">
+    <view class="debug-modal__dialog" catchtap="handleDebugPanelTap">
+      <view class="debug-modal__header">
+        <view>
+          <view class="debug-modal__eyebrow">DEBUG PANEL</view>
+          <view class="debug-modal__title">地图调试信息</view>
+        </view>
+        <view class="debug-modal__close" bindtap="handleCloseDebugPanel">关闭</view>
       </view>
       </view>
-    </block>
 
 
-    <view class="control-row">
-      <view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
-      <view class="control-chip control-chip--secondary" bindtap="handleRotationReset">旋转归零</view>
-    </view>
-    <view class="control-row">
-      <view class="control-chip {{gpsTracking ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleToggleGpsTracking">{{gpsTracking ? '停止定位' : '开启定位'}}</view>
-      <view class="control-chip {{osmReferenceEnabled ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleToggleOsmReference">{{osmReferenceText}}</view>
-    </view>
-    <view class="control-row control-row--triple">
-      <view class="control-chip {{orientationMode === 'manual' ? 'control-chip--active' : ''}}" bindtap="handleSetManualMode">手动</view>
-      <view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view>
-      <view class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
-    </view>
-    <view class="control-row">
-      <view class="control-chip control-chip--secondary" bindtap="handleCycleNorthReferenceMode">{{northReferenceButtonText}}</view>
-    </view>
-    <view class="control-row" wx:if="{{orientationMode === 'heading-up'}}">
-      <view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
-    </view>
-    <view class="control-row" wx:if="{{orientationMode === 'manual'}}">
-      <view class="control-chip" bindtap="handleRotateStep">旋转 +15°</view>
+      <scroll-view class="debug-modal__content" scroll-y enhanced show-scrollbar="true">
+        <view class="info-panel__row">
+          <text class="info-panel__label">Build</text>
+          <text class="info-panel__value">{{buildVersion}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">Config</text>
+          <text class="info-panel__value">{{configStatusText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Heading Mode</text>
+          <text class="info-panel__value">{{orientationModeText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Sensor Heading</text>
+          <text class="info-panel__value">{{sensorHeadingText}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">North Ref</text>
+          <text class="info-panel__value">{{northReferenceText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Zoom</text>
+          <text class="info-panel__value">{{zoom}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Rotation</text>
+          <text class="info-panel__value">{{rotationText}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">Status</text>
+          <text class="info-panel__value">{{statusText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">GPS</text>
+          <text class="info-panel__value">{{gpsTrackingText}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">GPS Coord</text>
+          <text class="info-panel__value">{{gpsCoordText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Renderer</text>
+          <text class="info-panel__value">{{renderMode}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">Projection</text>
+          <text class="info-panel__value">{{projectionMode}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Auto Source</text>
+          <text class="info-panel__value">{{autoRotateSourceText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Calibration</text>
+          <text class="info-panel__value">{{autoRotateCalibrationText}}</text>
+        </view>
+        <view class="info-panel__row info-panel__row--stack">
+          <text class="info-panel__label">Tile URL</text>
+          <text class="info-panel__value">{{tileSource}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Center Tile</text>
+          <text class="info-panel__value">{{centerText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Tile Size</text>
+          <text class="info-panel__value">{{tileSizePx}}px</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Visible Tiles</text>
+          <text class="info-panel__value">{{visibleTileCount}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Ready Tiles</text>
+          <text class="info-panel__value">{{readyTileCount}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Memory Tiles</text>
+          <text class="info-panel__value">{{memoryTileCount}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Disk Tiles</text>
+          <text class="info-panel__value">{{diskTileCount}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Cache Hit</text>
+          <text class="info-panel__value">{{cacheHitRateText}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Disk Hits</text>
+          <text class="info-panel__value">{{diskHitCount}}</text>
+        </view>
+        <view class="info-panel__row">
+          <text class="info-panel__label">Net Fetches</text>
+          <text class="info-panel__value">{{networkFetchCount}}</text>
+        </view>
+
+        <view class="control-row">
+          <view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
+          <view class="control-chip control-chip--secondary" bindtap="handleRotationReset">旋转归零</view>
+        </view>
+        <view class="control-row">
+          <view class="control-chip {{gpsTracking ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleToggleGpsTracking">{{gpsTracking ? '停止定位' : '开启定位'}}</view>
+          <view class="control-chip {{osmReferenceEnabled ? 'control-chip--active' : 'control-chip--secondary'}}" bindtap="handleToggleOsmReference">{{osmReferenceText}}</view>
+        </view>
+        <view class="control-row control-row--triple">
+          <view class="control-chip {{orientationMode === 'manual' ? 'control-chip--active' : ''}}" bindtap="handleSetManualMode">手动</view>
+          <view class="control-chip {{orientationMode === 'north-up' ? 'control-chip--active' : ''}}" bindtap="handleSetNorthUpMode">北朝上</view>
+          <view class="control-chip {{orientationMode === 'heading-up' ? 'control-chip--active' : ''}}" bindtap="handleSetHeadingUpMode">朝向朝上</view>
+        </view>
+        <view class="control-row">
+          <view class="control-chip control-chip--secondary" bindtap="handleCycleNorthReferenceMode">{{northReferenceButtonText}}</view>
+        </view>
+        <view class="control-row" wx:if="{{orientationMode === 'heading-up'}}">
+          <view class="control-chip" bindtap="handleAutoRotateCalibrate">按当前方向校准</view>
+        </view>
+        <view class="control-row" wx:if="{{orientationMode === 'manual'}}">
+          <view class="control-chip" bindtap="handleRotateStep">旋转 +15°</view>
+        </view>
+      </scroll-view>
     </view>
     </view>
-  </scroll-view>
+  </view>
 </view>
 </view>
 
 
 
 

+ 681 - 142
miniprogram/pages/map/map.wxss

@@ -1,63 +1,16 @@
 .page {
 .page {
   height: 100vh;
   height: 100vh;
-  padding: 20rpx 20rpx 24rpx;
-  box-sizing: border-box;
-  display: flex;
-  flex-direction: column;
+  position: relative;
   overflow: hidden;
   overflow: hidden;
-  background:
-    radial-gradient(circle at top left, #d8f3dc 0%, rgba(216, 243, 220, 0) 32%),
-    linear-gradient(180deg, #f7fbf2 0%, #eef6ea 100%);
+  background: #dbeed4;
   color: #163020;
   color: #163020;
 }
 }
 
 
-.page__header {
-  display: flex;
-  align-items: flex-start;
-  justify-content: space-between;
-  margin-bottom: 16rpx;
-  flex-shrink: 0;
-}
-
-.page__eyebrow {
-  font-size: 20rpx;
-  letter-spacing: 4rpx;
-  color: #5f7a65;
-}
-
-.page__title {
-  margin-top: 8rpx;
-  font-size: 44rpx;
-  font-weight: 600;
-}
-
-.page__badge {
-  padding: 10rpx 18rpx;
-  border-radius: 999rpx;
-  background: #163020;
-  color: #f7fbf2;
-  font-size: 22rpx;
-}
-
-.map-stage-wrap {
-  position: relative;
-  width: 100%;
-  height: 66vh;
-  min-height: 520rpx;
-  max-height: 72vh;
-  flex-shrink: 0;
-  margin-bottom: 16rpx;
-}
-
 .map-stage {
 .map-stage {
-  position: relative;
-  width: 100%;
-  height: 100%;
+  position: absolute;
+  inset: 0;
   overflow: hidden;
   overflow: hidden;
-  border: 2rpx solid rgba(22, 48, 32, 0.08);
-  border-radius: 32rpx;
   background: #dbeed4;
   background: #dbeed4;
-  box-shadow: 0 18rpx 40rpx rgba(22, 48, 32, 0.08);
 }
 }
 
 
 .map-content {
 .map-content {
@@ -118,38 +71,140 @@
   position: absolute;
   position: absolute;
   inset: 0;
   inset: 0;
   display: flex;
   display: flex;
-  align-items: flex-start;
+  flex-direction: column;
   justify-content: space-between;
   justify-content: space-between;
-  padding: 24rpx;
+  padding: 0 24rpx 248rpx;
   box-sizing: border-box;
   box-sizing: border-box;
   pointer-events: none;
   pointer-events: none;
   z-index: 4;
   z-index: 4;
 }
 }
 
 
-.overlay-card {
-  width: 68%;
-  padding: 22rpx;
-  border-radius: 24rpx;
-  background: rgba(247, 251, 242, 0.92);
-  box-shadow: 0 12rpx 30rpx rgba(22, 48, 32, 0.08);
+.map-stage__topbar {
+  display: flex;
+  align-items: flex-start;
+  justify-content: flex-start;
+}
+
+.map-stage__meta {
+  max-width: 68%;
+  padding: 18rpx 20rpx 20rpx;
+  border-radius: 28rpx;
+  background: rgba(248, 251, 244, 0.92);
+  box-shadow: 0 14rpx 36rpx rgba(22, 48, 32, 0.12);
+  backdrop-filter: blur(12rpx);
+}
+
+.map-stage__eyebrow {
+  font-size: 20rpx;
+  letter-spacing: 4rpx;
+  color: #5f7a65;
+}
+
+.map-stage__title {
+  margin-top: 8rpx;
+  font-size: 38rpx;
+  font-weight: 600;
+}
+
+.map-stage__badge {
+  display: inline-flex;
+  margin-top: 14rpx;
+  padding: 8rpx 18rpx;
+  border-radius: 999rpx;
+  background: rgba(22, 48, 32, 0.9);
+  color: #f7fbf2;
+  font-size: 22rpx;
+}
+
+.screen-button-layer {
+  position: absolute;
+  right: 24rpx;
+  width: 116rpx;
+  min-height: 116rpx;
+  padding: 18rpx 0 14rpx;
+  border-radius: 30rpx;
+  background: rgba(248, 251, 244, 0.96);
+  box-shadow: 0 14rpx 36rpx rgba(22, 48, 32, 0.14);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  box-sizing: border-box;
+  z-index: 20;
+}
+
+.screen-button-layer__icon {
+  position: relative;
+  width: 54rpx;
+  height: 32rpx;
+  margin: 0 auto;
+  border: 4rpx solid #163020;
+  border-radius: 8rpx;
+  box-sizing: border-box;
+}
+
+.screen-button-layer__line {
+  position: absolute;
+  left: 8rpx;
+  right: 8rpx;
+  bottom: 6rpx;
+  height: 4rpx;
+  border-radius: 999rpx;
+  background: rgba(22, 48, 32, 0.3);
+}
+
+.screen-button-layer__stand {
+  position: absolute;
+  left: 50%;
+  bottom: -12rpx;
+  width: 18rpx;
+  height: 4rpx;
+  margin-left: -9rpx;
+  border-radius: 999rpx;
+  background: #163020;
 }
 }
 
 
-.overlay-card__label {
+.screen-button-layer__text {
+  margin-top: 18rpx;
+  text-align: center;
+  font-size: 22rpx;
+  font-weight: 600;
+  color: #163020;
+  line-height: 1.2;
+}
+
+.map-stage__bottom {
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  gap: 20rpx;
+}
+
+.map-stage__status {
+  max-width: 56%;
+  padding: 18rpx 20rpx;
+  border-radius: 28rpx;
+  background: rgba(248, 251, 244, 0.9);
+  box-shadow: 0 14rpx 30rpx rgba(22, 48, 32, 0.12);
+  backdrop-filter: blur(12rpx);
+}
+
+.map-stage__status-label {
   font-size: 20rpx;
   font-size: 20rpx;
   letter-spacing: 3rpx;
   letter-spacing: 3rpx;
   color: #5f7a65;
   color: #5f7a65;
 }
 }
 
 
-.overlay-card__title {
-  margin-top: 10rpx;
-  font-size: 34rpx;
+.map-stage__status-value {
+  margin-top: 8rpx;
+  font-size: 30rpx;
   font-weight: 600;
   font-weight: 600;
+  color: #163020;
 }
 }
 
 
-.overlay-card__desc {
-  margin-top: 12rpx;
-  font-size: 24rpx;
-  line-height: 1.6;
+.map-stage__status-meta {
+  margin-top: 8rpx;
+  font-size: 22rpx;
   color: #45624b;
   color: #45624b;
 }
 }
 
 
@@ -157,79 +212,546 @@
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
-  gap: 10rpx;
+  gap: 6rpx;
+  flex-shrink: 0;
 }
 }
 
 
-.compass-widget__ring {
+.compass-widget__heading {
+  font-size: 14rpx;
+  line-height: 1;
+  font-weight: 600;
+  color: rgba(32, 42, 34, 0.72);
+  text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.35);
+}
+
+.compass-widget__dial {
   position: relative;
   position: relative;
-  width: 108rpx;
-  height: 108rpx;
+  width: 196rpx;
+  height: 196rpx;
   border-radius: 50%;
   border-radius: 50%;
-  background: rgba(247, 251, 242, 0.94);
-  border: 2rpx solid rgba(22, 48, 32, 0.12);
-  box-shadow: 0 10rpx 24rpx rgba(22, 48, 32, 0.1);
+  background: radial-gradient(circle at 48% 44%, rgba(255, 255, 255, 0.3) 0%, rgba(242, 241, 214, 0.32) 46%, rgba(183, 188, 159, 0.4) 72%, rgba(64, 68, 58, 0.62) 100%);
+  border: 2rpx solid rgba(18, 24, 18, 0.48);
+  box-shadow: 0 6rpx 14rpx rgba(0, 0, 0, 0.14), inset 0 0 0 2rpx rgba(255, 255, 255, 0.24);
+  overflow: hidden;
 }
 }
 
 
-.compass-widget__north {
+.compass-widget__glass,
+.compass-widget__inner-shadow {
+  position: absolute;
+  inset: 0;
+  border-radius: 50%;
+  pointer-events: none;
+}
+
+.compass-widget__glass {
+  background: radial-gradient(circle at 38% 30%, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0.1) 24%, rgba(255, 255, 255, 0) 52%);
+}
+
+.compass-widget__inner-shadow {
+  box-shadow: inset 0 0 0 12rpx rgba(0, 0, 0, 0.07), inset 0 0 18rpx rgba(255, 255, 255, 0.22);
+}
+
+.compass-widget__card {
+  position: absolute;
+  inset: 0;
+  transform-origin: center;
+}
+
+
+.compass-widget__north-arrow {
   position: absolute;
   position: absolute;
   left: 50%;
   left: 50%;
-  top: 10rpx;
-  transform: translateX(-50%);
-  font-size: 20rpx;
+  top: 50%;
+  width: 54rpx;
+  height: 176rpx;
+  transform: translate(-50%, -52%);
+  display: block;
+  pointer-events: none;
+  z-index: 1;
+}
+
+.compass-widget__north-arrow-outline,
+.compass-widget__north-arrow-fill,
+.compass-widget__north-arrow-tail-outline,
+.compass-widget__north-arrow-tail-fill {
+  display: none;
+}
+
+.compass-widget__tick-anchor,
+.compass-widget__mark-anchor,
+.compass-widget__needle-anchor {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+}
+
+
+.compass-widget__tick {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  width: 2rpx;
+  border-radius: 999rpx;
+  transform: translate(-50%, -90rpx);
+  background: rgba(28, 33, 28, 0.72);
+}
+
+.compass-widget__tick--short {
+  height: 8rpx;
+}
+
+.compass-widget__tick--long {
+  height: 12rpx;
+}
+
+.compass-widget__tick--major {
+  width: 3rpx;
+  height: 18rpx;
+  background: rgba(18, 22, 18, 0.88);
+}
+
+.compass-widget__mark {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  line-height: 1;
+  color: rgba(40, 42, 37, 0.88);
+  text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.22);
+  white-space: nowrap;
+  transform-origin: center;
+}
+
+.compass-widget__mark--cardinal {
+  font-size: 26rpx;
   font-weight: 700;
   font-weight: 700;
-  color: #d62828;
 }
 }
 
 
-.compass-widget__needle {
+.compass-widget__mark--intermediate {
+  font-size: 14rpx;
+  font-weight: 600;
+}
+
+.compass-widget__mark--north {
+  color: #d62323;
+}
+
+.compass-widget__mark--northeast,
+.compass-widget__mark--northwest {
+  color: #577347;
+}
+
+.compass-widget__needle-anchor {
+  width: 0;
+  height: 0;
+}
+
+.compass-widget__needle-north,
+.compass-widget__needle-south {
   position: absolute;
   position: absolute;
   left: 50%;
   left: 50%;
-  top: 18rpx;
-  width: 4rpx;
+  top: 50%;
+}
+
+.compass-widget__needle-north {
+  width: 0;
+  height: 0;
+  border-left: 8rpx solid transparent;
+  border-right: 8rpx solid transparent;
+  border-bottom: 64rpx solid #ef2f2f;
+  transform: translate(-50%, -74rpx);
+  filter: drop-shadow(0 2rpx 3rpx rgba(96, 0, 0, 0.24));
+}
+
+.compass-widget__needle-south {
+  width: 7rpx;
   height: 72rpx;
   height: 72rpx;
-  transform-origin: 50% 36rpx;
-  background: linear-gradient(180deg, #d62828 0%, #163020 100%);
   border-radius: 999rpx;
   border-radius: 999rpx;
+  background: linear-gradient(180deg, rgba(236, 238, 232, 0.98) 0%, rgba(146, 151, 143, 0.98) 100%);
+  transform: translate(-50%, 2rpx);
+  box-shadow: 0 1rpx 3rpx rgba(32, 34, 31, 0.18);
 }
 }
 
 
-.compass-widget__center {
+.compass-widget__hub {
   position: absolute;
   position: absolute;
   left: 50%;
   left: 50%;
   top: 50%;
   top: 50%;
-  width: 14rpx;
-  height: 14rpx;
-  transform: translate(-50%, -50%);
+  width: 26rpx;
+  height: 26rpx;
+  transform: translate(-50%, -52%);
   border-radius: 50%;
   border-radius: 50%;
-  background: #163020;
+  background: radial-gradient(circle at 34% 32%, #f4f3e7 0%, #d7d2bd 40%, #928b78 72%, #5a554b 100%);
+  box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.32), 0 2rpx 5rpx rgba(0, 0, 0, 0.16);
 }
 }
 
 
-.compass-widget__label {
-  min-width: 92rpx;
-  padding: 6rpx 10rpx;
-  border-radius: 999rpx;
-  background: rgba(247, 251, 242, 0.94);
-  font-size: 20rpx;
-  text-align: center;
-  color: #163020;
-  box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08);
+.compass-widget__hub-core {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  width: 10rpx;
+  height: 10rpx;
+  transform: translate(-50%, -52%);
+  border-radius: 50%;
+  background: rgba(173, 170, 156, 0.92);
+  box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.2);
 }
 }
 
 
 .compass-widget__hint {
 .compass-widget__hint {
-  margin-top: 8rpx;
-  font-size: 18rpx;
-  line-height: 1.4;
+  max-width: 196rpx;
+  font-size: 14rpx;
+  line-height: 1.3;
   color: #d62828;
   color: #d62828;
   text-align: center;
   text-align: center;
   font-weight: 700;
   font-weight: 700;
+  text-shadow: 0 1rpx 0 rgba(255, 255, 255, 0.24);
+}
+.race-panel {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  height: 216rpx;
+  background: linear-gradient(180deg, #1d97ec 0%, #168ce4 100%);
+  box-shadow: 0 -10rpx 24rpx rgba(10, 75, 125, 0.2);
+  z-index: 15;
+  overflow: hidden;
 }
 }
 
 
-.info-panel {
-  flex: 1;
-  min-height: 0;
-  padding: 22rpx 20rpx 28rpx;
+.race-panel__grid {
+  position: relative;
+  z-index: 2;
+  display: grid;
+  grid-template-columns: 1fr 1fr 1fr;
+  grid-template-rows: 1fr 1fr;
+  width: 100%;
+  height: 100%;
+}
+
+.race-panel__cell {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #ffffff;
+  box-sizing: border-box;
+}
+
+.race-panel__cell--action,
+.race-panel__cell--timer,
+.race-panel__cell--mileage {
+  padding-top: 10rpx;
+}
+
+.race-panel__cell--distance,
+.race-panel__cell--progress,
+.race-panel__cell--speed {
+  padding-top: 2rpx;
+}
+
+.race-panel__cell--action {
+  justify-content: center;
+  padding-left: 0;
+}
+
+.race-panel__cell--timer {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.race-panel__cell--mileage {
+  justify-content: center;
+  padding-right: 0;
+}
+
+.race-panel__cell--distance {
+  justify-content: center;
+  padding-left: 0;
+}
+
+.race-panel__cell--progress {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.race-panel__cell--speed {
+  justify-content: center;
+  padding-right: 0;
+}
+
+.race-panel__play {
+  width: 0;
+  height: 0;
+  margin-left: 2rpx;
+  border-top: 20rpx solid transparent;
+  border-bottom: 20rpx solid transparent;
+  border-left: 30rpx solid #ffffff;
+  filter: drop-shadow(0 2rpx 0 rgba(255, 255, 255, 0.25));
+  transform: translateX(16rpx);
+}
+
+.race-panel__timer {
+  max-width: 100%;
+  box-sizing: border-box;
+  font-size: 50rpx;
+  line-height: 1;
+  letter-spacing: 2rpx;
+  font-family: 'Courier New', monospace;
+  text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.2);
+}
+
+.race-panel__mileage {
+  max-width: 100%;
+  box-sizing: border-box;
+  font-size: 40rpx;
+  line-height: 1;
+  font-weight: 300;
+  text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
+}
+
+.race-panel__mileage-wrap {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10rpx;
+  transform: translateX(-16rpx);
+}
+
+.race-panel__metric-group {
+  max-width: 100%;
+  box-sizing: border-box;
+  display: flex;
+  align-items: baseline;
+  color: #ffffff;
+}
+
+.race-panel__metric-group--left {
+  justify-content: center;
+  transform: translateX(16rpx);
+}
+
+.race-panel__metric-group--right {
+  justify-content: center;
+  transform: translateX(-16rpx);
+}
+
+.race-panel__metric-value {
+  line-height: 1;
+  text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
+}
+
+.race-panel__metric-value--distance {
+  font-size: 54rpx;
+  font-weight: 700;
+}
+
+.race-panel__metric-value--speed {
+  font-size: 48rpx;
+  font-weight: 400;
+}
+
+.race-panel__metric-unit {
+  line-height: 1;
+  margin-left: 6rpx;
+  opacity: 0.95;
+}
+
+.race-panel__metric-unit--distance {
+  font-size: 24rpx;
+  font-weight: 600;
+}
+
+.race-panel__metric-unit--speed {
+  font-size: 18rpx;
+  font-weight: 500;
+}
+
+.race-panel__progress {
+  max-width: 100%;
+  box-sizing: border-box;
+  font-size: 50rpx;
+  line-height: 1;
+  font-weight: 400;
+  text-shadow: 0 2rpx 0 rgba(255, 255, 255, 0.16);
+}
+
+.race-panel__tag {
+  position: absolute;
+  z-index: 3;
+  min-width: 56rpx;
+  height: 32rpx;
+  padding: 0 10rpx;
+  background: #000000;
+  color: #ffffff;
+  font-size: 16rpx;
+  line-height: 32rpx;
+  text-align: center;
+  letter-spacing: 2rpx;
+}
+
+.race-panel__tag--top-left {
+  top: 0;
+  left: 0;
+}
+
+.race-panel__tag--top-right {
+  top: 0;
+  right: 0;
+}
+
+.race-panel__tag--bottom-left {
+  left: 0;
+  bottom: 0;
+}
+
+.race-panel__tag--bottom-right {
+  right: 0;
+  bottom: 0;
+}
+
+.race-panel__line {
+  position: absolute;
+  z-index: 1;
+  height: 2rpx;
+  box-shadow: 0 0 6rpx rgba(255, 255, 255, 0.2);
+}
+
+.race-panel__line--center {
+  left: 33.3333%;
+  right: 33.3333%;
+  top: 50%;
+  transform: translateY(-50%);
+  background: rgba(255, 255, 255, 0.86);
+}
+
+.race-panel__line--left-mid {
+  left: 0;
+  width: 33.3333%;
+  top: 50%;
+  transform: translateY(-50%);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.78) 100%);
+}
+
+.race-panel__line--right-mid {
+  right: 0;
+  width: 33.3333%;
+  top: 50%;
+  transform: translateY(-50%);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 255, 255, 0.08) 100%);
+}
+
+.race-panel__line--left-top,
+.race-panel__line--left-bottom,
+.race-panel__line--right-top,
+.race-panel__line--right-bottom {
+  width: 23%;
+  top: 50%;
+}
+
+.race-panel__line--left-top {
+  right: 66.6667%;
+  transform-origin: right center;
+  transform: translateY(-50%) rotate(70deg);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.82) 100%);
+}
+
+.race-panel__line--left-bottom {
+  right: 66.6667%;
+  transform-origin: right center;
+  transform: translateY(-50%) rotate(-70deg);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.82) 100%);
+}
+
+.race-panel__line--right-top {
+  left: 66.6667%;
+  transform-origin: left center;
+  transform: translateY(-50%) rotate(-70deg);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.08) 100%);
+}
+
+.race-panel__line--right-bottom {
+  left: 66.6667%;
+  transform-origin: left center;
+  transform: translateY(-50%) rotate(70deg);
+  background: linear-gradient(90deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.08) 100%);
+}
+
+.race-panel__chevrons {
+  position: relative;
+  width: 24rpx;
+  height: 24rpx;
+  opacity: 0.5;
+  flex-shrink: 0;
+}
+
+.race-panel__chevron {
+  position: absolute;
+  right: 6rpx;
+  top: 50%;
+  width: 10rpx;
+  height: 10rpx;
+  border-top: 3rpx solid rgba(255, 255, 255, 0.78);
+  border-right: 3rpx solid rgba(255, 255, 255, 0.78);
+  transform: translateY(-50%) rotate(45deg);
+}
+
+.race-panel__chevron--offset {
+  right: 0;
+}
+.debug-modal {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  padding: 0 20rpx 28rpx;
+  box-sizing: border-box;
+  background: rgba(7, 18, 12, 0.34);
+  z-index: 30;
+}
+
+.debug-modal__dialog {
+  width: 100%;
+  max-height: 72vh;
+  border-radius: 36rpx;
+  background: rgba(248, 251, 244, 0.98);
+  box-shadow: 0 20rpx 60rpx rgba(7, 18, 12, 0.24);
+  overflow: hidden;
+}
+
+.debug-modal__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 20rpx;
+  padding: 28rpx 28rpx 20rpx;
+  border-bottom: 1rpx solid rgba(22, 48, 32, 0.08);
+}
+
+.debug-modal__eyebrow {
+  font-size: 20rpx;
+  letter-spacing: 4rpx;
+  color: #5f7a65;
+}
+
+.debug-modal__title {
+  margin-top: 8rpx;
+  font-size: 34rpx;
+  font-weight: 600;
+  color: #163020;
+}
+
+.debug-modal__close {
+  flex-shrink: 0;
+  padding: 14rpx 22rpx;
+  border-radius: 999rpx;
+  background: #163020;
+  color: #f7fbf2;
+  font-size: 24rpx;
+}
+
+.debug-modal__content {
+  max-height: calc(72vh - 108rpx);
+  padding: 12rpx 28rpx 30rpx;
   box-sizing: border-box;
   box-sizing: border-box;
-  border-radius: 28rpx;
-  background: rgba(255, 255, 255, 0.88);
-  box-shadow: 0 12rpx 32rpx rgba(22, 48, 32, 0.08);
 }
 }
 
 
 .info-panel__row {
 .info-panel__row {
@@ -272,40 +794,6 @@
   line-height: 1.5;
   line-height: 1.5;
 }
 }
 
 
-.info-panel__actions {
-  display: flex;
-  gap: 14rpx;
-  margin-top: 18rpx;
-}
-
-.info-panel__actions--triple .info-panel__action {
-  font-size: 23rpx;
-}
-
-.info-panel__action {
-  flex: 1;
-  min-width: 0;
-  border-radius: 999rpx;
-  background: #d7e8da;
-  color: #163020;
-  font-size: 26rpx;
-}
-
-.info-panel__action--primary {
-  background: #2d6a4f;
-  color: #f7fbf2;
-}
-
-.info-panel__action--secondary {
-  background: #eef6ea;
-  color: #45624b;
-}
-
-.info-panel__action--active {
-  background: #2d6a4f;
-  color: #f7fbf2;
-}
-
 .control-row {
 .control-row {
   display: flex;
   display: flex;
   gap: 14rpx;
   gap: 14rpx;
@@ -344,3 +832,54 @@
 }
 }
 
 
 
 
+
+
+
+
+
+.race-panel__cell--action {
+  justify-content: flex-start;
+  padding-left: 44rpx;
+}
+.race-panel__cell--timer {
+  padding-left: 12rpx;
+  padding-right: 12rpx;
+}
+.race-panel__cell--mileage {
+  justify-content: flex-end;
+  padding-right: 56rpx;
+}
+.race-panel__cell--distance {
+  justify-content: flex-start;
+  padding-left: 28rpx;
+}
+.race-panel__cell--progress {
+  padding-left: 8rpx;
+  padding-right: 8rpx;
+}
+.race-panel__cell--speed {
+  justify-content: flex-end;
+  padding-right: 32rpx;
+}
+.race-panel__timer,
+.race-panel__progress,
+.race-panel__mileage,
+.race-panel__metric-group {
+  max-width: 100%;
+  box-sizing: border-box;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+