Revamp map page layout and compass while removing GPS demo

This commit is contained in:
2026-03-23 12:59:51 +08:00
parent 51740761f5
commit 277121fd51
11 changed files with 1053 additions and 373 deletions

View File

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

After

Width:  |  Height:  |  Size: 323 B

View File

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

After

Width:  |  Height:  |  Size: 394 B

View File

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

View File

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

View File

@@ -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.setState({
...this.state,
...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 {

View File

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

View File

@@ -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)
} }
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, 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, 13, [1, 1, 1, 0.95], scene)
this.pushCircle(positions, colors, gpsPoint.x, gpsPoint.y, 9, [0.13, 0.63, 0.74, 1], 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 {
} }
} }
} }

View File

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

View File

@@ -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-106'
const INTERNAL_BUILD_VERSION = 'map-build-99'
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 width = Math.max(320, systemInfo.windowWidth)
const height = Math.max(280, Math.floor(systemInfo.windowHeight * 0.66)) 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}`, configStatusText: `杞藉叆澶辫触: ${errorMessage}`,
statusText: `远程地图配置载入失败: ${errorMessage} (${INTERNAL_BUILD_VERSION})`, 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,6 +280,14 @@ Page({
showDebugPanel: !this.data.showDebugPanel, showDebugPanel: !this.data.showDebugPanel,
}) })
}, },
handleCloseDebugPanel() {
this.setData({
showDebugPanel: false,
})
},
handleDebugPanelTap() {},
}) })
@@ -223,29 +299,3 @@ Page({

View File

@@ -1,13 +1,4 @@
<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>
<view class="page__badge">{{mapReadyText}}</view>
</view>
<view class="map-stage-wrap">
<view <view
class="map-stage" class="map-stage"
catchtouchstart="handleTouchStart" catchtouchstart="handleTouchStart"
@@ -27,28 +18,115 @@
<view class="map-stage__crosshair"></view> <view class="map-stage__crosshair"></view>
<view class="map-stage__overlay"> <view class="map-stage__overlay">
<view class="overlay-card"> <view class="map-stage__topbar" style="padding-top: {{topInsetHeight}}px;">
<view class="overlay-card__label">WEBGL MAP ENGINE</view> <view class="map-stage__meta">
<view class="overlay-card__title">North Up / Heading Up / Manual</view> <view class="map-stage__eyebrow">CMR MINI MAP</view>
<view class="overlay-card__desc"> <view class="map-stage__title">{{mapName}}</view>
地图北已经固定为正上方。现在支持手动旋转、北朝上、朝向朝上三种模式,并提供指北针用于校验朝向。 <view class="map-stage__badge">{{mapReadyText}}</view>
</view> </view>
</view> </view>
<view class="compass-widget"> <view class="map-stage__bottom">
<view class="compass-widget__ring"> <view class="map-stage__status">
<view class="compass-widget__north">N</view> <view class="map-stage__status-label">当前模式</view>
<view class="compass-widget__needle" style="transform: translateX(-50%) rotate({{compassNeedleDeg}}deg);"></view> <view class="map-stage__status-value">{{orientationModeText}}</view>
<view class="compass-widget__center"></view> <view class="map-stage__status-meta">{{gpsTrackingText}} · 缩放 {{zoom}}</view>
</view>
<view class="compass-widget">
<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"> <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>
<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 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 class="race-panel__cell race-panel__cell--progress">
<text class="race-panel__progress">{{panelProgressText}}</text>
</view>
<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>
<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>
<scroll-view class="debug-modal__content" scroll-y enhanced show-scrollbar="true">
<view class="info-panel__row"> <view class="info-panel__row">
<text class="info-panel__label">Build</text> <text class="info-panel__label">Build</text>
<text class="info-panel__value">{{buildVersion}}</text> <text class="info-panel__value">{{buildVersion}}</text>
@@ -89,11 +167,6 @@
<text class="info-panel__label">GPS Coord</text> <text class="info-panel__label">GPS Coord</text>
<text class="info-panel__value">{{gpsCoordText}}</text> <text class="info-panel__value">{{gpsCoordText}}</text>
</view> </view>
<view class="control-row">
<view class="control-chip control-chip--secondary" bindtap="handleToggleDebugPanel">{{showDebugPanel ? '隐藏调试' : '查看调试'}}</view>
</view>
<block wx:if="{{showDebugPanel}}">
<view class="info-panel__row"> <view class="info-panel__row">
<text class="info-panel__label">Renderer</text> <text class="info-panel__label">Renderer</text>
<text class="info-panel__value">{{renderMode}}</text> <text class="info-panel__value">{{renderMode}}</text>
@@ -150,7 +223,6 @@
<text class="info-panel__label">Net Fetches</text> <text class="info-panel__label">Net Fetches</text>
<text class="info-panel__value">{{networkFetchCount}}</text> <text class="info-panel__value">{{networkFetchCount}}</text>
</view> </view>
</block>
<view class="control-row"> <view class="control-row">
<view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view> <view class="control-chip control-chip--primary" bindtap="handleRecenter">回到首屏</view>
@@ -176,6 +248,8 @@
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</view>
</view>

View File

@@ -1,63 +1,16 @@
.page { .page {
height: 100vh; height: 100vh;
padding: 20rpx 20rpx 24rpx; position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
background: background: #dbeed4;
radial-gradient(circle at top left, #d8f3dc 0%, rgba(216, 243, 220, 0) 32%),
linear-gradient(180deg, #f7fbf2 0%, #eef6ea 100%);
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; position: absolute;
width: 100%; inset: 0;
height: 100%;
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 { .map-stage__topbar {
width: 68%; display: flex;
padding: 22rpx; align-items: flex-start;
border-radius: 24rpx; justify-content: flex-start;
background: rgba(247, 251, 242, 0.92);
box-shadow: 0 12rpx 30rpx rgba(22, 48, 32, 0.08);
} }
.overlay-card__label { .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;
}
.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 { .map-stage__status-value {
margin-top: 10rpx; margin-top: 8rpx;
font-size: 34rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #163020;
} }
.overlay-card__desc { .map-stage__status-meta {
margin-top: 12rpx; margin-top: 8rpx;
font-size: 24rpx; font-size: 22rpx;
line-height: 1.6;
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; width: 196rpx;
height: 108rpx; height: 196rpx;
border-radius: 50%; border-radius: 50%;
background: rgba(247, 251, 242, 0.94); 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(22, 48, 32, 0.12); border: 2rpx solid rgba(18, 24, 18, 0.48);
box-shadow: 0 10rpx 24rpx rgba(22, 48, 32, 0.1); 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; position: absolute;
left: 50%; inset: 0;
top: 10rpx; border-radius: 50%;
transform: translateX(-50%); pointer-events: none;
font-size: 20rpx;
font-weight: 700;
color: #d62828;
} }
.compass-widget__needle { .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; position: absolute;
left: 50%; inset: 0;
top: 18rpx; transform-origin: center;
width: 4rpx;
height: 72rpx;
transform-origin: 50% 36rpx;
background: linear-gradient(180deg, #d62828 0%, #163020 100%);
border-radius: 999rpx;
} }
.compass-widget__center {
.compass-widget__north-arrow {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
width: 14rpx; width: 54rpx;
height: 14rpx; height: 176rpx;
transform: translate(-50%, -50%); transform: translate(-50%, -52%);
border-radius: 50%; display: block;
background: #163020; pointer-events: none;
z-index: 1;
} }
.compass-widget__label { .compass-widget__north-arrow-outline,
min-width: 92rpx; .compass-widget__north-arrow-fill,
padding: 6rpx 10rpx; .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; border-radius: 999rpx;
background: rgba(247, 251, 242, 0.94); transform: translate(-50%, -90rpx);
font-size: 20rpx; background: rgba(28, 33, 28, 0.72);
text-align: center;
color: #163020;
box-shadow: 0 8rpx 18rpx rgba(22, 48, 32, 0.08);
} }
.compass-widget__hint { .compass-widget__tick--short {
margin-top: 8rpx; height: 8rpx;
font-size: 18rpx; }
line-height: 1.4;
color: #d62828; .compass-widget__tick--long {
text-align: center; 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;
} }
.info-panel { .compass-widget__mark--intermediate {
flex: 1; font-size: 14rpx;
min-height: 0; font-weight: 600;
padding: 22rpx 20rpx 28rpx; }
.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;
left: 50%;
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;
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__hub {
position: absolute;
left: 50%;
top: 50%;
width: 26rpx;
height: 26rpx;
transform: translate(-50%, -52%);
border-radius: 50%;
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__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 {
max-width: 196rpx;
font-size: 14rpx;
line-height: 1.3;
color: #d62828;
text-align: center;
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;
}
.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;
}