af-mobile-client-vue3 1.5.69 → 1.5.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "af-mobile-client-vue3",
3
3
  "type": "module",
4
- "version": "1.5.69",
4
+ "version": "1.5.70",
5
5
  "packageManager": "pnpm@10.13.1",
6
6
  "description": "Vue + Vite component lib",
7
7
  "engines": {
@@ -0,0 +1,94 @@
1
+ import { getConfigByNameAsync } from '@af-mobile-client-vue3/services/api/common'
2
+ import { post } from '@af-mobile-client-vue3/services/restTools'
3
+ import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
4
+ import { trajectoryThinning } from '@af-mobile-client-vue3/utils/map/track'
5
+ import { trajectoryGrouping } from '@af-mobile-client-vue3/utils/map/trackUtils'
6
+ import dayjs from 'dayjs/esm/index'
7
+
8
+ /**
9
+ * trajectoryThinning 是否抽稀 默认 false
10
+ * trajectoryCorrection 是否高德纠偏 默认 false
11
+ * group 是否分组 默认 false 要进行高德纠偏必须分组
12
+ * AMAP_KEY 高德key 用来纠偏
13
+ * gapMs 分组时间间隔 毫秒 默认 30分钟
14
+ * rdpEpsilon RDP 精度(度) 默认 0.000001
15
+ * dbscanEpsilon DBSCAN聚类半径(km) 默认 0.01
16
+ * minPuts DBSCAN聚类 最小开始聚类点数 默认 20
17
+ * minDistance 分组数据进行高德纠偏起始纠偏数 默认 20 不能低于20 否则就会纠偏失败,失败也算次数
18
+ */
19
+ export interface TrajectoryParams {
20
+ trajectoryThinning?: boolean
21
+ trajectoryCorrection?: boolean
22
+ group?: boolean
23
+ AMAP_KEY?: string
24
+ gapMs?: number
25
+ rdpEpsilon?: number
26
+ dbscanEpsilon?: number
27
+ minPuts?: number
28
+ minDistance?: number
29
+ orderTime?: string
30
+ serviceName?: string
31
+ }
32
+ export const defaultTrajectoryParams: Required<TrajectoryParams> = {
33
+ trajectoryThinning: false,
34
+ trajectoryCorrection: false,
35
+ group: false,
36
+ AMAP_KEY: '',
37
+ gapMs: 1800000,
38
+ rdpEpsilon: 0.000001,
39
+ dbscanEpsilon: 0.01,
40
+ minPuts: 20,
41
+ minDistance: 20,
42
+ orderTime: 'f_realtime asc',
43
+ serviceName: 'af-linepatrol',
44
+ }
45
+ /**
46
+ *
47
+ * @param userId 查询的用户id
48
+ * @param dateStr 查询的轨迹日期
49
+ * @param trajectoryParams 后台轨迹纠偏参数
50
+ */
51
+ export async function getDeviceState(
52
+ userId: string = useUserStore().getLogin().id,
53
+ dateStr: string = dayjs().format('YYYY-MM-DD'),
54
+ trajectoryParams: TrajectoryParams = defaultTrajectoryParams,
55
+ ) {
56
+ const condition = `1 = 1 and f_user_id = ${userId} and (f_realtime between '${dateStr} 00:00:00' and '${dateStr} 23:59:59'`
57
+ const param = {
58
+ condition,
59
+ dateStr,
60
+ userId,
61
+ ...trajectoryParams,
62
+ }
63
+ // 获取轨迹
64
+ const trackPoint = await post(`/api/${trajectoryParams.serviceName}/logic/getDeviceStateLogic`, param)
65
+ // 轨迹数据未处理,需要对原始数据进行 处理
66
+ if (trackPoint.state === 'error') {
67
+ // 轨迹抽稀
68
+ if (trackPoint.points.length >= 30) {
69
+ // 轨迹抽稀
70
+ let path = trajectoryThinning(trackPoint.points)
71
+ // 存储 path 数据供表格使用
72
+ const webConfig = await getConfigByNameAsync('webConfig')
73
+ if (webConfig?.setting?.lesseeName === 'jinbin') {
74
+ path = path.filter(item => item.sp !== '0.0')
75
+ }
76
+ // 轨迹分组
77
+ const trajectoryGroupingResult = trajectoryGrouping(path, {})
78
+ console.warn('>>>> trajectoryGroupingResult', trajectoryGroupingResult)
79
+ return trajectoryGroupingResult
80
+
81
+ // const graspRoadCorrectRes = await graspRoadCorrect(trajectoryGroupingResult)
82
+ // console.warn('>>>> graspRoadCorrect', graspRoadCorrectRes)
83
+ // console.warn('>>>> graspRoadCorrect', graspRoadCorrectRes.value)
84
+ // 返回数据
85
+ // return JSON.parse(graspRoadCorrectRes.value)
86
+ }
87
+ else {
88
+ console.warn('当日使用app时间较短或行程较少,不足够生成轨迹')
89
+ }
90
+ }
91
+ if (trackPoint.state === 'success') {
92
+ return trackPoint.points
93
+ }
94
+ }
Binary file
Binary file
@@ -15,11 +15,14 @@ import type {
15
15
  PolygonData,
16
16
  PolygonLayerConfig,
17
17
  TrackData,
18
+ TrackGroupItem,
18
19
  WebGLPointOptions,
19
20
  WMSLayerConfig,
20
21
  WMSOptions,
21
22
  } from './types'
22
23
  import locationIcon from '@af-mobile-client-vue3/assets/img/component/positioning.png'
24
+ import startIcon from '@af-mobile-client-vue3/assets/img/map/start.png'
25
+ import terminusIcon from '@af-mobile-client-vue3/assets/img/map/terminus.png'
23
26
  import { getConfigByName } from '@af-mobile-client-vue3/services/api/common'
24
27
  import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
25
28
  import { Map, View } from 'ol'
@@ -1192,7 +1195,6 @@ function addTrackLayer(trackData: TrackData): void {
1192
1195
  // 添加到地图
1193
1196
  map.addLayer(vectorLayer)
1194
1197
  trackLayers[trackData.id] = vectorLayer
1195
-
1196
1198
  // 更新图层状态,确保 show 属性被正确设置
1197
1199
  const trackDataWithShow = {
1198
1200
  ...trackData,
@@ -1201,6 +1203,119 @@ function addTrackLayer(trackData: TrackData): void {
1201
1203
  trackLayerStatus.value.push(trackDataWithShow)
1202
1204
  }
1203
1205
 
1206
+ /**
1207
+ * 添加分段轨迹组图层
1208
+ * @param trackGroups - 轨迹组数组,每组包含唯一 key 和 path
1209
+ * @param options - 轨迹图层配置
1210
+ * @param options.id - 图层ID
1211
+ * @param options.name - 图层名称
1212
+ * @param options.color - 统一颜色,控制所有轨迹段
1213
+ * @param options.show - 图层是否默认显示
1214
+ */
1215
+ function addTrackGroupLayer(
1216
+ trackGroups: TrackGroupItem[],
1217
+ options: {
1218
+ id?: number
1219
+ name?: string
1220
+ color?: string
1221
+ show?: boolean
1222
+ } = {},
1223
+ ): void {
1224
+ if (!map || !Array.isArray(trackGroups) || trackGroups.length === 0)
1225
+ return
1226
+
1227
+ const layerId = options.id ?? 0
1228
+ const layerName = options.name ?? (trackGroups.length === 1 ? trackGroups[0].key : '轨迹组')
1229
+ const layerColor = options.color || '#ff0000'
1230
+ const visible = options.show ?? true
1231
+
1232
+ const vectorSource = new VectorSource()
1233
+ const vectorLayer = new VectorLayer({
1234
+ source: vectorSource,
1235
+ visible,
1236
+ zIndex: 2,
1237
+ })
1238
+
1239
+ trackGroups.forEach((group) => {
1240
+ if (!group.path || group.path.length === 0)
1241
+ return
1242
+
1243
+ const coordinates = group.path.map(coord => fromLonLat([coord.x, coord.y]))
1244
+ if (coordinates.length > 1) {
1245
+ const lineString = new Feature({
1246
+ geometry: new LineString(coordinates),
1247
+ })
1248
+
1249
+ const lineStyle = new Style({
1250
+ stroke: new Stroke({
1251
+ color: layerColor,
1252
+ width: 4,
1253
+ }),
1254
+ })
1255
+
1256
+ lineString.setStyle(lineStyle)
1257
+ vectorSource.addFeature(lineString)
1258
+ }
1259
+
1260
+ const startPoint = new Feature({
1261
+ geometry: new Point(coordinates[0]),
1262
+ properties: { segmentKey: group.key, type: 'start' },
1263
+ })
1264
+ const endPoint = new Feature({
1265
+ geometry: new Point(coordinates[coordinates.length - 1]),
1266
+ properties: { segmentKey: group.key, type: 'end' },
1267
+ })
1268
+
1269
+ const pointStyle = (src: string, text: string) =>
1270
+ new Style({
1271
+ image: new Icon({
1272
+ src,
1273
+ anchor: [0.5, 1],
1274
+ scale: 0.15,
1275
+ crossOrigin: 'anonymous',
1276
+ }),
1277
+ text: new Text({
1278
+ text,
1279
+ offsetY: 10,
1280
+ font: '12px sans-serif',
1281
+ fill: new Fill({
1282
+ color: '#333',
1283
+ }),
1284
+ stroke: new Stroke({
1285
+ color: '#fff',
1286
+ width: 2,
1287
+ }),
1288
+ }),
1289
+ })
1290
+
1291
+ // 解析时间:key格式为 "开始时间-结束时间"
1292
+ const [startTime, endTime] = group.key.split('-')
1293
+
1294
+ startPoint.setStyle(pointStyle(startIcon, startTime || '起点'))
1295
+ endPoint.setStyle(pointStyle(terminusIcon, endTime || '终点'))
1296
+
1297
+ vectorSource.addFeatures([startPoint, endPoint])
1298
+ })
1299
+
1300
+ map.addLayer(vectorLayer)
1301
+ trackLayers[layerId] = vectorLayer
1302
+
1303
+ const flatTrackData = trackGroups.reduce<[number, number][]>((acc, group) => {
1304
+ if (group.path && group.path.length > 0) {
1305
+ acc.push(...group.path.map(p => [p.x, p.y] as [number, number]))
1306
+ }
1307
+ return acc
1308
+ }, [])
1309
+
1310
+ trackLayerStatus.value.push({
1311
+ id: layerId,
1312
+ name: layerName,
1313
+ trackData: flatTrackData,
1314
+ color: layerColor,
1315
+ show: visible,
1316
+ })
1317
+ }
1318
+
1204
1319
  /**
1205
1320
  * 控制轨迹图层显示/隐藏
1206
1321
  * @param trackId - 轨迹ID
@@ -1272,6 +1387,7 @@ defineExpose({
1272
1387
  startNavigation,
1273
1388
  stopNavigation,
1274
1389
  addTrackLayer,
1390
+ addTrackGroupLayer,
1275
1391
  setTrackLayerVisible,
1276
1392
  handleToggleTrackLayer,
1277
1393
  addPolygonLayer,
@@ -150,6 +150,32 @@ export interface TrackData {
150
150
  show?: boolean // 是否显示
151
151
  }
152
152
 
153
+ /**
154
+ * 轨迹点数据接口
155
+ */
156
+ export interface TrackPoint {
157
+ /** 经度 */
158
+ x: number
159
+ /** 纬度 */
160
+ y: number
161
+ /** 角度 */
162
+ ag?: string
163
+ /** 时间戳 */
164
+ tm?: number
165
+ /** 速度 */
166
+ sp?: string
167
+ }
168
+
169
+ /**
170
+ * 分段轨迹数据项
171
+ */
172
+ export interface TrackGroupItem {
173
+ key: string
174
+ path: TrackPoint[]
175
+ color?: string
176
+ show?: boolean
177
+ }
178
+
153
179
  /**
154
180
  * 多边形数据接口
155
181
  */
@@ -0,0 +1,294 @@
1
+ /**
2
+ * ==========================================
3
+ * 轨迹抽稀工具
4
+ * RDP(几何抽稀) + DBSCAN(停留点识别)
5
+ * 坐标统一使用:
6
+ * x -> 经度 (lon)
7
+ * y -> 纬度 (lat)
8
+ * 返回字段仅包含:x、y、ag、tm、sp
9
+ * ==========================================
10
+ */
11
+
12
+ interface Point {
13
+ x: number
14
+ y: number
15
+ ag: any
16
+ tm: any
17
+ sp: any
18
+ }
19
+
20
+ /* ================= DBSCAN ================= */
21
+
22
+ /**
23
+ * DBSCAN 聚类算法(基于经纬度球面距离)
24
+ */
25
+ class DBSCAN {
26
+ private epsilon: number
27
+ private minPts: number
28
+ private data: Point[]
29
+ private labels: number[]
30
+ private cellSize: number
31
+ private grid: Map<string, number[]>
32
+
33
+ constructor(epsilon: number, minPts: number, data: Point[]) {
34
+ this.epsilon = epsilon // km
35
+ this.minPts = minPts
36
+ this.data = data
37
+ this.labels = Array.from({ length: data.length }, () => 0)
38
+
39
+ // 空间网格索引(加速邻域查询)
40
+ this.cellSize = this.epsilon / 110.574
41
+ this.grid = new Map()
42
+
43
+ for (let i = 0; i < data.length; i++) {
44
+ const { x, y } = data[i]
45
+ const xIndex = Math.floor(x / this.cellSize)
46
+ const yIndex = Math.floor(y / this.cellSize)
47
+ const key = `${xIndex},${yIndex}`
48
+
49
+ if (!this.grid.has(key))
50
+ this.grid.set(key, [])
51
+ this.grid.get(key)!.push(i)
52
+ }
53
+ }
54
+
55
+ // 球面距离(Haversine)
56
+ distance(a, b) {
57
+ const lat1 = a.y
58
+ const lon1 = a.x
59
+ const lat2 = b.y
60
+ const lon2 = b.x
61
+
62
+ const dLat = (lat2 - lat1) * Math.PI / 180
63
+ const dLon = (lon2 - lon1) * Math.PI / 180
64
+
65
+ const s
66
+ = Math.sin(dLat / 2) ** 2
67
+ + Math.cos(lat1 * Math.PI / 180)
68
+ * Math.cos(lat2 * Math.PI / 180)
69
+ * Math.sin(dLon / 2) ** 2
70
+
71
+ return 6371 * 2 * Math.atan2(Math.sqrt(s), Math.sqrt(1 - s))
72
+ }
73
+
74
+ regionQuery(index) {
75
+ const neighbors = []
76
+ const { x, y } = this.data[index]
77
+ const xIndex = Math.floor(x / this.cellSize)
78
+ const yIndex = Math.floor(y / this.cellSize)
79
+
80
+ for (let i = -1; i <= 1; i++) {
81
+ for (let j = -1; j <= 1; j++) {
82
+ const key = `${xIndex + i},${yIndex + j}`
83
+ const cell = this.grid.get(key)
84
+ if (!cell)
85
+ continue
86
+
87
+ for (const idx of cell) {
88
+ if (this.distance(this.data[index], this.data[idx]) < this.epsilon) {
89
+ neighbors.push(idx)
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return neighbors
95
+ }
96
+
97
+ dbscan() {
98
+ let clusterId = 0
99
+
100
+ for (let i = 0; i < this.data.length; i++) {
101
+ if (this.labels[i] !== 0)
102
+ continue
103
+
104
+ const neighbors = this.regionQuery(i)
105
+ if (neighbors.length < this.minPts) {
106
+ this.labels[i] = -1
107
+ continue
108
+ }
109
+
110
+ clusterId++
111
+ this.expandCluster(i, neighbors, clusterId)
112
+ }
113
+
114
+ return this.labels
115
+ }
116
+
117
+ expandCluster(index, neighbors, clusterId) {
118
+ this.labels[index] = clusterId
119
+ let i = 0
120
+
121
+ while (i < neighbors.length) {
122
+ const n = neighbors[i]
123
+
124
+ if (this.labels[n] === -1) {
125
+ this.labels[n] = clusterId
126
+ }
127
+ else if (this.labels[n] === 0) {
128
+ this.labels[n] = clusterId
129
+ const nn = this.regionQuery(n)
130
+ if (nn.length >= this.minPts) {
131
+ neighbors.push(...nn)
132
+ }
133
+ }
134
+ i++
135
+ }
136
+ }
137
+ }
138
+
139
+ /* ================= RDP ================= */
140
+
141
+ function getDecimalDigits(num) {
142
+ const str = num.toString()
143
+ const match = str.match(/\.(\d+)/)
144
+ return match ? match[1].length : 0
145
+ }
146
+
147
+ function perpendicularDistance(p, s, e) {
148
+ const x0 = p.x
149
+ const y0 = p.y
150
+ const x1 = s.x
151
+ const y1 = s.y
152
+ const x2 = e.x
153
+ const y2 = e.y
154
+
155
+ const num = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
156
+ const den = Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2)
157
+
158
+ return den === 0
159
+ ? Math.hypot(x0 - x1, y0 - y1)
160
+ : num / den
161
+ }
162
+
163
+ function rdp(points, epsilon) {
164
+ if (points.length < 3)
165
+ return points
166
+
167
+ let maxDist = 0
168
+ let index = 0
169
+ const start = points[0]
170
+ const end = points[points.length - 1]
171
+
172
+ for (let i = 1; i < points.length - 1; i++) {
173
+ const d = perpendicularDistance(points[i], start, end)
174
+ if (d > maxDist) {
175
+ maxDist = d
176
+ index = i
177
+ }
178
+ }
179
+
180
+ if (maxDist > epsilon) {
181
+ const left = rdp(points.slice(0, index + 1), epsilon)
182
+ const right = rdp(points.slice(index), epsilon)
183
+ return left.slice(0, -1).concat(right)
184
+ }
185
+ else {
186
+ return [start, end]
187
+ }
188
+ }
189
+
190
+ /* ================= 主函数 ================= */
191
+
192
+ /**
193
+ * 轨迹抽稀主入口
194
+ * @param {Array} data 原始轨迹
195
+ * @param {number} rdpEpsilon RDP 精度(度)
196
+ * @param {number} dbscanEpsilon DBSCAN 半径(km)
197
+ * @param {number} minPts DBSCAN 最小点数
198
+ */
199
+ export function trajectoryThinning(
200
+ data: any[],
201
+ rdpEpsilon = 0.000001,
202
+ dbscanEpsilon = 0.01,
203
+ minPts = 20,
204
+ ): Point[] {
205
+ if (!Array.isArray(data) || data.length === 0)
206
+ return []
207
+
208
+ // 1️⃣ 数据清洗
209
+ const cleaned: Point[] = data
210
+ .filter(p =>
211
+ p.x && p.y
212
+ && p.ag !== undefined
213
+ && p.tm !== undefined
214
+ && p.sp !== undefined
215
+ && getDecimalDigits(p.x) > 3
216
+ && getDecimalDigits(p.y) > 3,
217
+ )
218
+ .map(p => ({
219
+ x: +p.x,
220
+ y: +p.y,
221
+ ag: p.ag,
222
+ tm: p.tm,
223
+ sp: p.sp,
224
+ }))
225
+ .sort((a, b) => a.tm - b.tm)
226
+
227
+ if (!cleaned.length)
228
+ return []
229
+
230
+ // 2️⃣ 按 tm 去重
231
+ const dedup: Point[] = []
232
+ for (let i = 0; i < cleaned.length; i++) {
233
+ if (!cleaned[i + 1] || cleaned[i].tm !== cleaned[i + 1].tm) {
234
+ dedup.push(cleaned[i])
235
+ }
236
+ }
237
+
238
+ // 3️⃣ RDP 抽稀
239
+ const rdpPoints: Point[] = rdp(dedup, rdpEpsilon)
240
+ if (!rdpPoints.length)
241
+ return []
242
+
243
+ // 4️⃣ DBSCAN 聚类
244
+ const labels = runDbscan(dbscanEpsilon, minPts, rdpPoints)
245
+
246
+ // 5️⃣ 结果整理
247
+ return modifyArray(labels, rdpPoints)
248
+ .filter(Boolean)
249
+ .map(p => ({
250
+ x: +p.x.toFixed(6),
251
+ y: +p.y.toFixed(6),
252
+ ag: p.ag,
253
+ tm: p.tm,
254
+ sp: p.sp,
255
+ }))
256
+ }
257
+
258
+ /* ================= 工具函数 ================= */
259
+
260
+ function runDbscan(epsilon: number, minPts: number, data: Point[]): number[] {
261
+ if (data.length < minPts) {
262
+ return Array.from({ length: data.length }, () => -1)
263
+ }
264
+ return new DBSCAN(epsilon, minPts, data).dbscan()
265
+ }
266
+
267
+ function modifyArray(labels: number[], points: Point[]): Point[] {
268
+ const groups: Record<number, { index: number, tm: number }[]> = {}
269
+ const result: (Point | 0)[] = Array.from({ length: points.length }, () => 0)
270
+
271
+ for (let i = 0; i < labels.length; i++) {
272
+ const id = labels[i]
273
+ if (id === -1)
274
+ continue
275
+ if (!groups[id])
276
+ groups[id] = []
277
+ groups[id].push({ index: i, tm: points[i].tm })
278
+ }
279
+
280
+ for (const group of Object.values(groups)) {
281
+ group.sort((a, b) => a.tm - b.tm)
282
+ result[group[0].index] = points[group[0].index]
283
+ result[group[group.length - 1].index] = points[group[group.length - 1].index]
284
+ }
285
+
286
+ // 噪声点全部保留
287
+ for (let i = 0; i < labels.length; i++) {
288
+ if (labels[i] === -1) {
289
+ result[i] = points[i]
290
+ }
291
+ }
292
+
293
+ return result.filter(Boolean) as Point[]
294
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * 获取两个时间之间的时间间隔
3
+ * @param time1
4
+ * @param time2
5
+ * @returns {number}
6
+ */
7
+ export function getTimeInterval(time1, time2) {
8
+ if (time1 == null || time2 == null)
9
+ return 0
10
+
11
+ let t1 = Number(time1)
12
+ let t2 = Number(time2)
13
+
14
+ // 秒级 → 转毫秒
15
+ if (t1 < 1e12)
16
+ t1 *= 1000
17
+ if (t2 < 1e12)
18
+ t2 *= 1000
19
+
20
+ return Math.abs(t2 - t1)
21
+ }
22
+
23
+ /**
24
+ * 格式化时间为 HH:mm:ss
25
+ */
26
+ export function formatTime(time) {
27
+ if (time == null)
28
+ return ''
29
+
30
+ let timestamp = Number(time)
31
+ if (timestamp < 1e12)
32
+ timestamp *= 1000
33
+
34
+ const date = new Date(timestamp)
35
+ const hh = String(date.getUTCHours()).padStart(2, '0')
36
+ const mm = String(date.getUTCMinutes()).padStart(2, '0')
37
+ const ss = String(date.getUTCSeconds()).padStart(2, '0')
38
+
39
+ return `${hh}:${mm}:${ss}`
40
+ }
41
+ /**
42
+ * 轨迹分组
43
+ * @param trajectory
44
+ * @param {object} options
45
+ * @returns {*}
46
+ */
47
+ export function trajectoryGrouping(trajectory, options) {
48
+ if (Array.isArray(trajectory) && trajectory.length === 0) {
49
+ console.warn('分组失败:无有效轨迹数据')
50
+ return []
51
+ }
52
+ // 排序轨迹
53
+ const sortedPath = trajectory.sort((a, b) =>
54
+ new Date(a.tm).getTime() - new Date(b.tm).getTime(),
55
+ )
56
+ const result = []
57
+ let currentGroup = {
58
+ startTime: null,
59
+ endTime: null,
60
+ path: [],
61
+ }
62
+
63
+ for (let i = 0; i < sortedPath.length; i++) {
64
+ const point = sortedPath[i]
65
+
66
+ if (currentGroup.path.length === 0) {
67
+ // 初始化分组
68
+ currentGroup.startTime = point.tm
69
+ currentGroup.endTime = point.tm
70
+ currentGroup.path.push(point)
71
+ continue
72
+ }
73
+
74
+ const prevPoint = sortedPath[i - 1]
75
+ const interval = getTimeInterval(prevPoint.tm, point.tm)
76
+ const gapMs = options?.gapMs || 1800000
77
+ if (interval > gapMs) {
78
+ // 2️⃣ 时间间隔超过阈值,结束当前分组
79
+ result.push(formatGroup(currentGroup))
80
+
81
+ // 新建分组
82
+ currentGroup = {
83
+ startTime: point.tm,
84
+ endTime: point.tm,
85
+ path: [point],
86
+ }
87
+ }
88
+ else {
89
+ // 继续当前分组
90
+ currentGroup.endTime = point.tm
91
+ currentGroup.path.push(point)
92
+ }
93
+ }
94
+
95
+ // 3️⃣ 推入最后一组
96
+ if (currentGroup.path.length > 0) {
97
+ result.push(formatGroup(currentGroup))
98
+ }
99
+
100
+ return result
101
+ }
102
+ function formatGroup(group) {
103
+ return {
104
+ key: `${formatTime(group.startTime)}-${formatTime(group.endTime)}`,
105
+ path: group.path,
106
+ }
107
+ }
@@ -2,6 +2,7 @@
2
2
  import NormalDataLayout from '@af-mobile-client-vue3/components/layout/NormalDataLayout/index.vue'
3
3
  import { showNotify } from 'vant'
4
4
  import { onMounted, ref } from 'vue'
5
+ import { getDeviceState } from '~root/src/api/map/trackApi'
5
6
  import XOlMap from '../../../components/data/XOlMap/index.vue'
6
7
  import { polygonTestData, trackTestData } from './testData'
7
8
  import 'vant/lib/index.css'
@@ -273,6 +274,12 @@ function handleAddTrack() {
273
274
  })
274
275
  }
275
276
 
277
+ // 添加轨迹数组测试
278
+ async function handleAddTrackGroup() {
279
+ const trackGroup = await getDeviceState('419879409793761282', '2026-04-13')
280
+ mapRef.value.addTrackGroupLayer(trackGroup, { id: 10, name: '轨迹', color: '#fff' })
281
+ }
282
+
276
283
  // 多边形处理函数
277
284
  function handlePolygonClick(polygon: any) {
278
285
  console.log('>>>> polygon: ', JSON.stringify(polygon))
@@ -364,6 +371,9 @@ function stopNavigation() {
364
371
  <van-button type="primary" size="small" @click="handleAddTrack">
365
372
  添加测试轨迹
366
373
  </van-button>
374
+ <van-button type="primary" size="small" @click="handleAddTrackGroup">
375
+ 添加测试轨迹数组
376
+ </van-button>
367
377
  </div>
368
378
 
369
379
  <div class="control-group">