diff --git a/src/components.d.ts b/src/components.d.ts
index 18b953c..f648498 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -12,6 +12,10 @@ declare module 'vue' {
DeviceCard: typeof import('./components/deviceCard.vue')['default']
DeviceInfo: typeof import('./components/deviceInfo.vue')['default']
FullscreenNav: typeof import('./components/FullscreenNav.vue')['default']
+ HeatmapCanvas: typeof import('./components/heatmap/canvas.vue')['default']
+ HeatmapLayerPanel: typeof import('./components/heatmap/layerPanel.vue')['default']
+ HeatmapLegend: typeof import('./components/heatmap/legend.vue')['default']
+ HeatmapPointDetail: typeof import('./components/heatmap/pointDetail.vue')['default']
HomeConstruction: typeof import('./components/home/construction.vue')['default']
HomeHomeNav: typeof import('./components/home/homeNav.vue')['default']
HomeList: typeof import('./components/home/list.vue')['default']
diff --git a/src/components/heatmap/canvas.vue b/src/components/heatmap/canvas.vue
new file mode 100644
index 0000000..628451c
--- /dev/null
+++ b/src/components/heatmap/canvas.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/heatmap/heatUtils.ts b/src/components/heatmap/heatUtils.ts
new file mode 100644
index 0000000..a1a3497
--- /dev/null
+++ b/src/components/heatmap/heatUtils.ts
@@ -0,0 +1,74 @@
+/** 热力图图层与色阶工具 */
+
+export type HeatmapLayerKey = 'compaction' | 'speed' | 'temperature' | 'passCount'
+
+export const HEATMAP_LAYERS: { key: HeatmapLayerKey, label: string }[] = [
+ { key: 'compaction', label: '压实度' },
+ { key: 'speed', label: '压实速度' },
+ { key: 'temperature', label: '温度' },
+ { key: 'passCount', label: '碾压遍数' },
+]
+
+/** 色阶阈值:0–90 绿,90–95 黄,95–100 红 */
+export const HEAT_THRESHOLD_YELLOW = 90
+export const HEAT_THRESHOLD_RED = 95
+
+export function colorByScore(score: number): string {
+ if (score >= HEAT_THRESHOLD_RED)
+ return '#ef4444'
+ if (score >= HEAT_THRESHOLD_YELLOW)
+ return '#eab308'
+ return '#22c55e'
+}
+
+/** 各图层原始值映射到 0–100 分,统一用 90/95 分色 */
+export function layerScore(
+ layer: HeatmapLayerKey,
+ point: { compaction: number, speed: number, temperature: number, passCount: number },
+): number {
+ switch (layer) {
+ case 'compaction':
+ return point.compaction
+ case 'speed': {
+ // 目标约 4~5 km/h 偏优,偏快偏慢降分
+ const ideal = 4.2
+ const diff = Math.abs(point.speed - ideal)
+ return Math.max(0, Math.min(100, 100 - diff * 18))
+ }
+ case 'temperature': {
+ // 目标约 150℃
+ const ideal = 150
+ const diff = Math.abs(point.temperature - ideal)
+ return Math.max(0, Math.min(100, 100 - diff * 2.5))
+ }
+ case 'passCount':
+ // 目标遍数 6
+ return Math.max(0, Math.min(100, (point.passCount / 6) * 100))
+ default:
+ return 0
+ }
+}
+
+export function layerRawText(
+ layer: HeatmapLayerKey,
+ point: { compaction: number, speed: number, temperature: number, passCount: number },
+): string {
+ switch (layer) {
+ case 'compaction':
+ return `${point.compaction}%`
+ case 'speed':
+ return `${point.speed}km/h`
+ case 'temperature':
+ return `${point.temperature}℃`
+ case 'passCount':
+ return `${point.passCount}遍`
+ default:
+ return '-'
+ }
+}
+
+export function headingText(heading: number): string {
+ const dirs = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
+ const idx = Math.round(heading / 45) % 8
+ return `${dirs[idx]} ${heading.toFixed(1)}°`
+}
diff --git a/src/components/heatmap/layerPanel.vue b/src/components/heatmap/layerPanel.vue
new file mode 100644
index 0000000..66cab42
--- /dev/null
+++ b/src/components/heatmap/layerPanel.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+ 图层切换
+
+
+
+
+
+ 图层切换
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
diff --git a/src/components/heatmap/legend.vue b/src/components/heatmap/legend.vue
new file mode 100644
index 0000000..98555b1
--- /dev/null
+++ b/src/components/heatmap/legend.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ 90
+
+
+ 95
+
+
+ 100
+
+
+
+
+
+
diff --git a/src/components/heatmap/pointDetail.vue b/src/components/heatmap/pointDetail.vue
new file mode 100644
index 0000000..546048a
--- /dev/null
+++ b/src/components/heatmap/pointDetail.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+ 点位详情
+
+
+ 点击地图任意位置查看
+
+
+
+
+
+
+ 点位坐标
+
+
+ {{ point.longitude.toFixed(4) }}°E, {{ point.latitude.toFixed(4) }}°N
+
+
+
+
+ 航向角
+
+
+ {{ headingText(point.heading) }}
+
+
+
+
+ 压实速度
+
+
+ {{ point.speed }}km/h
+
+
+
+
+ 压实度
+
+
+ {{ point.compaction }}%
+
+
+
+
+ 温度
+
+
+ {{ point.temperature }}℃
+
+
+
+
+ 碾压遍数
+
+
+ {{ point.passCount }}遍
+
+
+
+
+ 点击空白处关闭
+
+
+
+
+
+
diff --git a/src/components/work/rtkCanvas.vue b/src/components/work/rtkCanvas.vue
index fc3d082..9f35151 100644
--- a/src/components/work/rtkCanvas.vue
+++ b/src/components/work/rtkCanvas.vue
@@ -7,10 +7,20 @@ import { getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from
import { useCurrentProject } from '@/composables/useCurrentProject'
import { createMockRtkStream } from '@/mock/rtkTrack'
+const props = withDefaults(defineProps<{
+ /** canvas-id,同页多实例时需唯一 */
+ canvasId?: string
+ /** 是否点击跳转全屏 RTK 页(全屏页内应关闭) */
+ navigateOnClick?: boolean
+}>(), {
+ canvasId: 'rtkTrackCanvas',
+ navigateOnClick: true,
+})
+
const { hasCurrentProject } = useCurrentProject()
const instance = getCurrentInstance()?.proxy
-const CANVAS_ID = 'rtkTrackCanvas'
+const CANVAS_ID = props.canvasId
const PADDING = 28
const ROLLER_WIDTH = 14
const MIN_H = 160
@@ -31,6 +41,8 @@ let fixedMaxLat = 0 // 北端固定(起点一侧)
let pxPerMeter = 1
function goToRtk() {
+ if (!props.navigateOnClick)
+ return
uni.navigateTo({ url: '/pages/rtk/index' })
}
diff --git a/src/mock/rtkTrack.ts b/src/mock/rtkTrack.ts
index 50a04be..ea1405c 100644
--- a/src/mock/rtkTrack.ts
+++ b/src/mock/rtkTrack.ts
@@ -1,13 +1,16 @@
/**
* RTK 轨迹假数据:蛇形碾压路径 + 持续推送
- * 字段:经纬度、航向角(度,0=北)、压实度
+ * 字段:经纬度、航向、压实度、速度、温度、碾压遍数
*/
export interface MockRtkPoint {
- latitude: number
- longitude: number
- heading: number
- compaction: number
+ latitude: number // 纬度
+ longitude: number // 经度
+ heading: number // 航向角(度,0=北,顺时针)
+ compaction: number // 压实度 %
+ speed: number // 压实速度 km/h
+ temperature: number // 温度 ℃
+ passCount: number // 碾压遍数
}
export interface MockRtkStreamOptions {
@@ -40,6 +43,35 @@ function makeCompaction(seed: number) {
return Number(Math.min(98, Math.max(78, 88 + wave + (Math.random() - 0.5) * 3)).toFixed(1))
}
+function makeSpeed(seed: number) {
+ return Number((3.2 + Math.sin(seed / 5) * 1.2 + Math.random() * 0.8).toFixed(1))
+}
+
+function makeTemperature(seed: number) {
+ return Number((148 + Math.sin(seed / 7) * 8 + (Math.random() - 0.5) * 6).toFixed(0))
+}
+
+function makePassCount(seed: number) {
+ return Math.max(1, Math.min(8, Math.round(2 + (seed % 9) / 2 + Math.random())))
+}
+
+function buildPoint(
+ lat: number,
+ lon: number,
+ heading: number,
+ seed: number,
+): MockRtkPoint {
+ return {
+ latitude: Number(lat.toFixed(7)),
+ longitude: Number(lon.toFixed(7)),
+ heading,
+ compaction: makeCompaction(seed),
+ speed: makeSpeed(seed),
+ temperature: makeTemperature(seed),
+ passCount: makePassCount(seed),
+ }
+}
+
/**
* 行末三角掉头:行末 → 外侧顶点 → 下行起点
* 航向先转 60°(到顶点),再转 120°(合计 180° 衔接下行)
@@ -55,30 +87,25 @@ function pushUTurnTriangle(
) {
const nextLat = startLat + latStep
const midLat = startLat + latStep / 2
- // 外侧突出量 ≈ 半个行距(换算到经度),形成尖角
const tipLatOffset = Math.abs(latStep) / 2
const metersPerDegLat = 111320
const metersPerDegLon = 111320 * Math.cos((startLat * Math.PI) / 180)
const tipLonOffset = (tipLatOffset * metersPerDegLat) / metersPerDegLon
const tipLon = startLon + (eastward ? tipLonOffset : -tipLonOffset)
-
const turnSign = eastward ? 1 : -1
- // 1) 外侧顶点:航向 +60° / -60°
- points.push({
- latitude: Number(midLat.toFixed(7)),
- longitude: Number(tipLon.toFixed(7)),
- heading: normalizeHeading(fromHeading + 60 * turnSign),
- compaction: makeCompaction(seedRef.value++),
- })
-
- // 2) 下行起点:航向转满 180°
- points.push({
- latitude: Number(nextLat.toFixed(7)),
- longitude: Number(startLon.toFixed(7)),
- heading: normalizeHeading(fromHeading + 180 * turnSign),
- compaction: makeCompaction(seedRef.value++),
- })
+ points.push(buildPoint(
+ midLat,
+ tipLon,
+ normalizeHeading(fromHeading + 60 * turnSign),
+ seedRef.value++,
+ ))
+ points.push(buildPoint(
+ nextLat,
+ startLon,
+ normalizeHeading(fromHeading + 180 * turnSign),
+ seedRef.value++,
+ ))
}
/** 生成类似示意图的左右蛇形碾压路径 */
@@ -86,11 +113,10 @@ export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): Moc
const rows = options.rows ?? 80
const pointsPerRow = options.pointsPerRow ?? 40
- // 以雄安附近坐标为基准,量级约几十米行程
const originLat = 39.0425
const originLon = 115.8620
- const lonStep = 0.00012 // ~10m 东向
- const latStep = -0.000045 // ~5m 南向(行间距)
+ const lonStep = 0.00012
+ const latStep = -0.000045
const points: MockRtkPoint[] = []
const seedRef = { value: 0 }
@@ -103,15 +129,9 @@ export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): Moc
for (let col = 0; col < pointsPerRow; col++) {
const t = eastward ? col : (pointsPerRow - 1 - col)
const lon = originLon + t * lonStep
- points.push({
- latitude: Number(lat.toFixed(7)),
- longitude: Number(lon.toFixed(7)),
- heading,
- compaction: makeCompaction(seedRef.value++),
- })
+ points.push(buildPoint(lat, lon, heading, seedRef.value++))
}
- // 行末三角掉头(外凸尖角)
if (row < rows - 1) {
const endT = eastward ? (pointsPerRow - 1) : 0
const endLon = originLon + endT * lonStep
diff --git a/src/pages/heatmap/index.vue b/src/pages/heatmap/index.vue
index 1f497f8..d1dd350 100644
--- a/src/pages/heatmap/index.vue
+++ b/src/pages/heatmap/index.vue
@@ -1,23 +1,66 @@
-
-
+
-
-
-
- 压实热力图
-
-
+
+
+
+
+ 点击地图查看点位详情
+
+
+
+
+
+
+
+
+
+
+
@@ -25,19 +68,38 @@ definePage({
.page {
min-height: 100vh;
background-color: #EAEFF9;
+ display: flex;
+ flex-direction: column;
}
.content {
- padding: 16px;
+ flex: 1;
+ height: 0;
+ padding: 12px 16px;
padding-top: 140px;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: calc(100vh - 120px);
+ box-sizing: border-box;
}
-.placeholder-text {
- font-size: 18px;
- color: #999;
+.card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 12px;
+}
+
+.toolbar {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.tip {
+ flex: 1;
+ font-size: 13px;
+ color: #666;
+ /* background: #f8fafc; */
+ border-radius: 8px;
+ padding: 10px 12px;
}
diff --git a/src/pages/rtk/index.vue b/src/pages/rtk/index.vue
index 42ce355..158ae94 100644
--- a/src/pages/rtk/index.vue
+++ b/src/pages/rtk/index.vue
@@ -2,7 +2,6 @@
definePage({
layout: false,
style: {
- // navigationBarTitleText: 'RTK轨迹',
navigationStyle: 'custom',
},
})
@@ -10,15 +9,17 @@ definePage({
-
-
-
-
- RTK轨迹
-
-
+
+
+
+
+
+
@@ -26,19 +27,24 @@ definePage({
.page {
min-height: 100vh;
background-color: #EAEFF9;
+ display: flex;
+ flex-direction: column;
}
.content {
- padding: 16px;
+ flex: 1;
+ height: 0;
+ padding: 12px 16px;
padding-top: 140px;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: calc(100vh - 120px);
+ box-sizing: border-box;
}
-.placeholder-text {
- font-size: 18px;
- color: #999;
+.canvas-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 12px;
+ /* 全屏宽度绘制 → 比例尺更大,视觉上放大 */
+ width: 100%;
+ box-sizing: border-box;
}