From 1ae0371c18a4221448801902b437e355452420db Mon Sep 17 00:00:00 2001
From: Ironsp <1522278303@qq.com>
Date: Thu, 17 Sep 2026 15:26:32 +0800
Subject: [PATCH] =?UTF-8?q?feat=20RTK=E8=BD=A8=E8=BF=B9=E7=BB=98=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/work/rtkCanvas.vue | 284 +++++++++++++++++++++++++++--
src/components/work/rtkMonitor.vue | 28 ++-
src/mock/index.ts | 3 +
src/mock/rtkTrack.ts | 171 +++++++++++++++++
src/pages/work/index.vue | 10 +-
5 files changed, 467 insertions(+), 29 deletions(-)
create mode 100644 src/mock/rtkTrack.ts
diff --git a/src/components/work/rtkCanvas.vue b/src/components/work/rtkCanvas.vue
index cd3010a..fc3d082 100644
--- a/src/components/work/rtkCanvas.vue
+++ b/src/components/work/rtkCanvas.vue
@@ -1,23 +1,271 @@
-
-
+
+
+
- {{ hasCurrentProject ? '轨迹动画区域' : '--' }}
-
-
- (Canvas 绘制行驶轨迹 + 压实热力图)
+ --
@@ -25,30 +273,30 @@ function goToRtk() {
diff --git a/src/components/work/rtkMonitor.vue b/src/components/work/rtkMonitor.vue
index 308f8b6..d8e7cc4 100644
--- a/src/components/work/rtkMonitor.vue
+++ b/src/components/work/rtkMonitor.vue
@@ -1,24 +1,34 @@
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/mock/index.ts b/src/mock/index.ts
index 0c3342d..052eba6 100644
--- a/src/mock/index.ts
+++ b/src/mock/index.ts
@@ -1,5 +1,8 @@
import formattedDate from './time'
+export { createMockRtkStream, generateSerpentineTrack } from './rtkTrack'
+export type { MockRtkPoint, MockRtkStream, MockRtkStreamOptions } from './rtkTrack'
+
export const mockData = {
time: formattedDate,
message: 'Hello, this is mock data!',
diff --git a/src/mock/rtkTrack.ts b/src/mock/rtkTrack.ts
new file mode 100644
index 0000000..50a04be
--- /dev/null
+++ b/src/mock/rtkTrack.ts
@@ -0,0 +1,171 @@
+/**
+ * RTK 轨迹假数据:蛇形碾压路径 + 持续推送
+ * 字段:经纬度、航向角(度,0=北)、压实度
+ */
+
+export interface MockRtkPoint {
+ latitude: number
+ longitude: number
+ heading: number
+ compaction: number
+}
+
+export interface MockRtkStreamOptions {
+ /** 推送间隔 ms,默认 120 */
+ intervalMs?: number
+ /** 结束后是否循环,默认 true */
+ loop?: boolean
+ /** 横向行数,默认 80(约 10 倍于早期 8 行) */
+ rows?: number
+ /** 每行采样点数,默认 40 */
+ pointsPerRow?: number
+}
+
+export interface MockRtkStream {
+ /** 完整路径(用于画布定界) */
+ readonly points: MockRtkPoint[]
+ start: (onPoint: (point: MockRtkPoint, index: number) => void) => void
+ stop: () => void
+ reset: () => void
+ getIndex: () => number
+}
+
+/** 航向归一化到 [0, 360) */
+function normalizeHeading(deg: number) {
+ return ((deg % 360) + 360) % 360
+}
+
+function makeCompaction(seed: number) {
+ const wave = Math.sin(seed / 4) * 4
+ return Number(Math.min(98, Math.max(78, 88 + wave + (Math.random() - 0.5) * 3)).toFixed(1))
+}
+
+/**
+ * 行末三角掉头:行末 → 外侧顶点 → 下行起点
+ * 航向先转 60°(到顶点),再转 120°(合计 180° 衔接下行)
+ */
+function pushUTurnTriangle(
+ points: MockRtkPoint[],
+ startLat: number,
+ startLon: number,
+ fromHeading: number,
+ latStep: number,
+ eastward: boolean,
+ seedRef: { value: number },
+) {
+ 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++),
+ })
+}
+
+/** 生成类似示意图的左右蛇形碾压路径 */
+export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): MockRtkPoint[] {
+ 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 points: MockRtkPoint[] = []
+ const seedRef = { value: 0 }
+
+ for (let row = 0; row < rows; row++) {
+ const eastward = row % 2 === 0
+ const heading = eastward ? 90 : 270
+ const lat = originLat + row * latStep
+
+ 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++),
+ })
+ }
+
+ // 行末三角掉头(外凸尖角)
+ if (row < rows - 1) {
+ const endT = eastward ? (pointsPerRow - 1) : 0
+ const endLon = originLon + endT * lonStep
+ pushUTurnTriangle(points, lat, endLon, heading, latStep, eastward, seedRef)
+ }
+ }
+
+ return points
+}
+
+/** 持续推送假轨迹点 */
+export function createMockRtkStream(options: MockRtkStreamOptions = {}): MockRtkStream {
+ const points = generateSerpentineTrack(options)
+ const intervalMs = options.intervalMs ?? 120
+ const loop = options.loop ?? true
+
+ let index = 0
+ let timer: ReturnType | null = null
+
+ function stop() {
+ if (timer != null) {
+ clearInterval(timer)
+ timer = null
+ }
+ }
+
+ function reset() {
+ stop()
+ index = 0
+ }
+
+ function start(onPoint: (point: MockRtkPoint, index: number) => void) {
+ stop()
+ timer = setInterval(() => {
+ if (index >= points.length) {
+ if (loop) {
+ index = 0
+ }
+ else {
+ stop()
+ return
+ }
+ }
+ const point = points[index]
+ onPoint(point, index)
+ index += 1
+ }, intervalMs)
+ }
+
+ return {
+ points,
+ start,
+ stop,
+ reset,
+ getIndex: () => index,
+ }
+}
diff --git a/src/pages/work/index.vue b/src/pages/work/index.vue
index 5e9f9eb..71a530b 100644
--- a/src/pages/work/index.vue
+++ b/src/pages/work/index.vue
@@ -40,11 +40,12 @@ definePage({
padding-top: 72px;
}
-/* 双列布局 */
+/* 双列:最小一屏,内容变高时整页跟着撑开可滚动 */
.two-column {
display: flex;
+ align-items: flex-start;
gap: 12px;
- height: calc(100vh - 84px);
+ min-height: calc(100vh - 84px);
}
.column-left {
@@ -52,11 +53,16 @@ definePage({
display: flex;
flex-direction: column;
gap: 12px;
+ position: sticky;
+ top: 72px;
}
.column-right {
flex: 1;
display: flex;
flex-direction: column;
+ /* 右侧白卡随轨迹变高,不锁成一屏高 */
+ height: auto;
+ align-self: stretch;
}