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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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({ @@ -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; }