feat 对齐数据

This commit is contained in:
Ironsp
2026-09-17 16:26:30 +08:00
parent c1d2958095
commit faf4ea1155
5 changed files with 82 additions and 60 deletions

View File

@@ -1,12 +1,14 @@
/** 热力图图层与色阶工具 */ /** 热力图图层与色阶工具(字段与 RtkTrack / MockRtkPoint 对齐) */
export type HeatmapLayerKey = 'compaction' | 'speed' | 'temperature' | 'passCount' import type { MockRtkPoint } from '@/mock/rtkTrack'
export type HeatmapLayerKey = 'compaction_value' | 'speed' | 'steel_wheel_temp' | 'pass_count'
export const HEATMAP_LAYERS: { key: HeatmapLayerKey, label: string }[] = [ export const HEATMAP_LAYERS: { key: HeatmapLayerKey, label: string }[] = [
{ key: 'compaction', label: '压实度' }, { key: 'compaction_value', label: '压实度' },
{ key: 'speed', label: '压实速度' }, { key: 'speed', label: '压实速度' },
{ key: 'temperature', label: '温度' }, { key: 'steel_wheel_temp', label: '温度' },
{ key: 'passCount', label: '碾压遍数' }, { key: 'pass_count', label: '碾压遍数' },
] ]
/** 色阶阈值:0–90 绿,90–95 黄,95–100 红 */ /** 色阶阈值:0–90 绿,90–95 黄,95–100 红 */
@@ -22,46 +24,37 @@ export function colorByScore(score: number): string {
} }
/** 各图层原始值映射到 0–100 分,统一用 90/95 分色 */ /** 各图层原始值映射到 0–100 分,统一用 90/95 分色 */
export function layerScore( export function layerScore(layer: HeatmapLayerKey, point: MockRtkPoint): number {
layer: HeatmapLayerKey,
point: { compaction: number, speed: number, temperature: number, passCount: number },
): number {
switch (layer) { switch (layer) {
case 'compaction': case 'compaction_value':
return point.compaction return point.compaction_value
case 'speed': { case 'speed': {
// 目标约 4~5 km/h 偏优,偏快偏慢降分
const ideal = 4.2 const ideal = 4.2
const diff = Math.abs(point.speed - ideal) const diff = Math.abs(point.speed - ideal)
return Math.max(0, Math.min(100, 100 - diff * 18)) return Math.max(0, Math.min(100, 100 - diff * 18))
} }
case 'temperature': { case 'steel_wheel_temp': {
// 目标约 150℃
const ideal = 150 const ideal = 150
const diff = Math.abs(point.temperature - ideal) const diff = Math.abs(point.steel_wheel_temp - ideal)
return Math.max(0, Math.min(100, 100 - diff * 2.5)) return Math.max(0, Math.min(100, 100 - diff * 2.5))
} }
case 'passCount': case 'pass_count':
// 目标遍数 6 return Math.max(0, Math.min(100, (point.pass_count / 6) * 100))
return Math.max(0, Math.min(100, (point.passCount / 6) * 100))
default: default:
return 0 return 0
} }
} }
export function layerRawText( export function layerRawText(layer: HeatmapLayerKey, point: MockRtkPoint): string {
layer: HeatmapLayerKey,
point: { compaction: number, speed: number, temperature: number, passCount: number },
): string {
switch (layer) { switch (layer) {
case 'compaction': case 'compaction_value':
return `${point.compaction}%` return `${point.compaction_value}%`
case 'speed': case 'speed':
return `${point.speed}km/h` return `${point.speed}km/h`
case 'temperature': case 'steel_wheel_temp':
return `${point.temperature}℃` return `${point.steel_wheel_temp}℃`
case 'passCount': case 'pass_count':
return `${point.passCount}遍` return `${point.pass_count}遍`
default: default:
return '-' return '-'
} }

View File

@@ -56,7 +56,7 @@ const emit = defineEmits<{
压实度 压实度
</text> </text>
<text class="value"> <text class="value">
{{ point.compaction }}% {{ point.compaction_value }}%
</text> </text>
</view> </view>
<view class="row"> <view class="row">
@@ -64,7 +64,7 @@ const emit = defineEmits<{
温度 温度
</text> </text>
<text class="value"> <text class="value">
{{ point.temperature }}℃ {{ point.steel_wheel_temp }}℃
</text> </text>
</view> </view>
<view class="row"> <view class="row">
@@ -72,7 +72,15 @@ const emit = defineEmits<{
碾压遍数 碾压遍数
</text> </text>
<text class="value"> <text class="value">
{{ point.passCount }}遍 {{ point.pass_count }}遍
</text>
</view>
<view class="row">
<text class="label">
合格率
</text>
<text class="value">
{{ point.qualified_rate }}%
</text> </text>
</view> </view>

View File

@@ -142,7 +142,7 @@ function draw() {
ctx.setLineWidth(ROLLER_WIDTH) ctx.setLineWidth(ROLLER_WIDTH)
for (let i = 1; i < coords.length; i++) { for (let i = 1; i < coords.length; i++) {
ctx.beginPath() ctx.beginPath()
ctx.setStrokeStyle(compactionColor(path[i].compaction)) ctx.setStrokeStyle(compactionColor(path[i].compaction_value))
ctx.moveTo(coords[i - 1].x, coords[i - 1].y) ctx.moveTo(coords[i - 1].x, coords[i - 1].y)
ctx.lineTo(coords[i].x, coords[i].y) ctx.lineTo(coords[i].x, coords[i].y)
ctx.stroke() ctx.stroke()

View File

@@ -1,16 +1,25 @@
/** /**
* RTK 轨迹假数据:蛇形碾压路径 + 持续推送 * RTK 轨迹假数据:蛇形碾压路径 + 持续推送
* 字段:经纬度、航向、压实度、速度、温度、碾压遍数 * 字段与 `@/service/rtkTrack` 的 RtkTrack 对齐
*/ */
import type { RtkTrack } from '@/service/rtkTrack'
export interface MockRtkPoint { /** 假数据点:与 rtk_track 表结构一致(坐标类字段必填便于绘制) */
latitude: number // 纬度 export type MockRtkPoint = Required<
longitude: number // 经度 Pick<
heading: number // 航向角(度,0=北,顺时针) RtkTrack,
compaction: number // 压实度 % | 'latitude'
speed: number // 压实速度 km/h | 'longitude'
temperature: number // 温度 ℃ | 'speed'
passCount: number // 碾压遍数 | 'heading'
| 'steel_wheel_temp'
| 'compaction_value'
| 'pass_count'
| 'qualified_rate'
>
> & {
work_record_id?: number
created_at?: string
} }
export interface MockRtkStreamOptions { export interface MockRtkStreamOptions {
@@ -18,14 +27,15 @@ export interface MockRtkStreamOptions {
intervalMs?: number intervalMs?: number
/** 结束后是否循环,默认 true */ /** 结束后是否循环,默认 true */
loop?: boolean loop?: boolean
/** 横向行数,默认 80(约 10 倍于早期 8 行) */ /** 横向行数,默认 80 */
rows?: number rows?: number
/** 每行采样点数,默认 40 */ /** 每行采样点数,默认 40 */
pointsPerRow?: number pointsPerRow?: number
/** 关联作业记录 id,默认 1 */
workRecordId?: number
} }
export interface MockRtkStream { export interface MockRtkStream {
/** 完整路径(用于画布定界) */
readonly points: MockRtkPoint[] readonly points: MockRtkPoint[]
start: (onPoint: (point: MockRtkPoint, index: number) => void) => void start: (onPoint: (point: MockRtkPoint, index: number) => void) => void
stop: () => void stop: () => void
@@ -33,12 +43,11 @@ export interface MockRtkStream {
getIndex: () => number getIndex: () => number
} }
/** 航向归一化到 [0, 360) */
function normalizeHeading(deg: number) { function normalizeHeading(deg: number) {
return ((deg % 360) + 360) % 360 return ((deg % 360) + 360) % 360
} }
function makeCompaction(seed: number) { function makeCompactionValue(seed: number) {
const wave = Math.sin(seed / 4) * 4 const wave = Math.sin(seed / 4) * 4
return Number(Math.min(98, Math.max(78, 88 + wave + (Math.random() - 0.5) * 3)).toFixed(1)) return Number(Math.min(98, Math.max(78, 88 + wave + (Math.random() - 0.5) * 3)).toFixed(1))
} }
@@ -47,7 +56,7 @@ function makeSpeed(seed: number) {
return Number((3.2 + Math.sin(seed / 5) * 1.2 + Math.random() * 0.8).toFixed(1)) return Number((3.2 + Math.sin(seed / 5) * 1.2 + Math.random() * 0.8).toFixed(1))
} }
function makeTemperature(seed: number) { function makeSteelWheelTemp(seed: number) {
return Number((148 + Math.sin(seed / 7) * 8 + (Math.random() - 0.5) * 6).toFixed(0)) return Number((148 + Math.sin(seed / 7) * 8 + (Math.random() - 0.5) * 6).toFixed(0))
} }
@@ -55,27 +64,36 @@ function makePassCount(seed: number) {
return Math.max(1, Math.min(8, Math.round(2 + (seed % 9) / 2 + Math.random()))) return Math.max(1, Math.min(8, Math.round(2 + (seed % 9) / 2 + Math.random())))
} }
function makeQualifiedRate(compaction: number) {
return Number(Math.min(100, Math.max(70, compaction - 2 + (Math.random() - 0.5) * 4)).toFixed(1))
}
function nowDateTime() {
return new Date().toISOString().replace('T', ' ').slice(0, 19)
}
function buildPoint( function buildPoint(
lat: number, lat: number,
lon: number, lon: number,
heading: number, heading: number,
seed: number, seed: number,
workRecordId: number,
): MockRtkPoint { ): MockRtkPoint {
const compaction_value = makeCompactionValue(seed)
return { return {
work_record_id: workRecordId,
latitude: Number(lat.toFixed(7)), latitude: Number(lat.toFixed(7)),
longitude: Number(lon.toFixed(7)), longitude: Number(lon.toFixed(7)),
heading,
compaction: makeCompaction(seed),
speed: makeSpeed(seed), speed: makeSpeed(seed),
temperature: makeTemperature(seed), heading,
passCount: makePassCount(seed), steel_wheel_temp: makeSteelWheelTemp(seed),
compaction_value,
pass_count: makePassCount(seed),
qualified_rate: makeQualifiedRate(compaction_value),
created_at: nowDateTime(),
} }
} }
/**
* 行末三角掉头:行末 → 外侧顶点 → 下行起点
* 航向先转 60°(到顶点),再转 120°(合计 180° 衔接下行)
*/
function pushUTurnTriangle( function pushUTurnTriangle(
points: MockRtkPoint[], points: MockRtkPoint[],
startLat: number, startLat: number,
@@ -84,6 +102,7 @@ function pushUTurnTriangle(
latStep: number, latStep: number,
eastward: boolean, eastward: boolean,
seedRef: { value: number }, seedRef: { value: number },
workRecordId: number,
) { ) {
const nextLat = startLat + latStep const nextLat = startLat + latStep
const midLat = startLat + latStep / 2 const midLat = startLat + latStep / 2
@@ -99,19 +118,22 @@ function pushUTurnTriangle(
tipLon, tipLon,
normalizeHeading(fromHeading + 60 * turnSign), normalizeHeading(fromHeading + 60 * turnSign),
seedRef.value++, seedRef.value++,
workRecordId,
)) ))
points.push(buildPoint( points.push(buildPoint(
nextLat, nextLat,
startLon, startLon,
normalizeHeading(fromHeading + 180 * turnSign), normalizeHeading(fromHeading + 180 * turnSign),
seedRef.value++, seedRef.value++,
workRecordId,
)) ))
} }
/** 生成类似示意图的左右蛇形碾压路径 */ /** 生成蛇形碾压路径假数据 */
export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): MockRtkPoint[] { export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): MockRtkPoint[] {
const rows = options.rows ?? 80 const rows = options.rows ?? 80
const pointsPerRow = options.pointsPerRow ?? 40 const pointsPerRow = options.pointsPerRow ?? 40
const workRecordId = options.workRecordId ?? 1
const originLat = 39.0425 const originLat = 39.0425
const originLon = 115.8620 const originLon = 115.8620
@@ -129,13 +151,13 @@ export function generateSerpentineTrack(options: MockRtkStreamOptions = {}): Moc
for (let col = 0; col < pointsPerRow; col++) { for (let col = 0; col < pointsPerRow; col++) {
const t = eastward ? col : (pointsPerRow - 1 - col) const t = eastward ? col : (pointsPerRow - 1 - col)
const lon = originLon + t * lonStep const lon = originLon + t * lonStep
points.push(buildPoint(lat, lon, heading, seedRef.value++)) points.push(buildPoint(lat, lon, heading, seedRef.value++, workRecordId))
} }
if (row < rows - 1) { if (row < rows - 1) {
const endT = eastward ? (pointsPerRow - 1) : 0 const endT = eastward ? (pointsPerRow - 1) : 0
const endLon = originLon + endT * lonStep const endLon = originLon + endT * lonStep
pushUTurnTriangle(points, lat, endLon, heading, latStep, eastward, seedRef) pushUTurnTriangle(points, lat, endLon, heading, latStep, eastward, seedRef, workRecordId)
} }
} }
@@ -175,8 +197,7 @@ export function createMockRtkStream(options: MockRtkStreamOptions = {}): MockRtk
return return
} }
} }
const point = points[index] onPoint(points[index], index)
onPoint(point, index)
index += 1 index += 1
}, intervalMs) }, intervalMs)
} }

View File

@@ -10,7 +10,7 @@ definePage({
}, },
}) })
const layer = ref<HeatmapLayerKey>('compaction') const layer = ref<HeatmapLayerKey>('compaction_value')
const layerVisible = ref(false) const layerVisible = ref(false)
const detailVisible = ref(false) const detailVisible = ref(false)
const selectedPoint = ref<MockRtkPoint | null>(null) const selectedPoint = ref<MockRtkPoint | null>(null)