road-traffic-viewer 1.0.0

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/README.md ADDED
@@ -0,0 +1,961 @@
1
+ # RoadTrafficViewer — 车道车流可视化组件
2
+
3
+ 基于 **Canvas 2D** 的道路俯视图车流可视化组件,支持 **Vue 3 / Vue 2 / 纯 JavaScript** 三种使用方式。
4
+
5
+ 仿照大屏车流可视化工具类的道路俯视图渲染逻辑,在此基础上扩展了**多车道**、**路段流量染色**、**SVG 图片渲染**、**鼠标拖拽平移**、**桩号灵活输入**、**全配置化颜色**等能力。
6
+
7
+ ---
8
+
9
+ ## 组件介绍
10
+
11
+ RoadTrafficViewer 是一款专为交通态势大屏设计的 Canvas 2D 道路可视化组件,能够在网页中实时渲染道路俯视图,直观展示多车道车辆的行驶状态和路段拥堵程度。
12
+
13
+ ### 核心能力
14
+
15
+ - **多车道渲染**:支持上下行独立配置多车道(1-8车道),车道间白色虚线分隔,中央隔离带配合双黄线
16
+ - **车辆精细绘制**:14层 Canvas 绘制车辆(阴影→车身→车顶→挡风玻璃→车灯→轮廓),支持 SVG 图片替代
17
+ - **路段流量染色**:根据交通状态(畅通/缓行/拥堵/严重拥堵)对道路区间进行颜色渲染
18
+ - **灵活桩号系统**:支持 `K112`、`k112.500`、`K112+500`、`112000` 等多种输入格式
19
+ - **全配置化样式**:道路底色、路肩、隔离带、双黄线、车道线、文字颜色等全部可自定义
20
+ - **鼠标拖拽平移**:当视口小于路线总长时,支持鼠标拖拽平移路线
21
+ - **高性能渲染**:requestAnimationFrame 单帧调度,轻松支撑 1000+ 台车辆实时渲染
22
+
23
+ ### 适用场景
24
+
25
+ - 高速公路/城市道路态势监控大屏
26
+ - 交通指挥中心实时可视化系统
27
+ - 车路协同仿真演示平台
28
+ - 物流运输轨迹监控系统
29
+
30
+ ---
31
+
32
+ ## 目录
33
+
34
+ - [组件介绍](#组件介绍)
35
+ - [安装](#安装)
36
+ - [特性概览](#特性概览)
37
+ - [快速开始](#快速开始)
38
+ - [Vue 3 使用方式](#vue-3-使用方式)
39
+ - [Vue 2 使用方式](#vue-2-使用方式)
40
+ - [纯 JS 使用方式](#纯-js-使用方式)
41
+ - [桩号系统](#桩号系统)
42
+ - [配置项详解](#配置项详解)
43
+ - [数据格式](#数据格式)
44
+ - [API 方法](#api-方法)
45
+ - [使用案例](#使用案例)
46
+ - [性能说明](#性能说明)
47
+ - [文件结构](#文件结构)
48
+
49
+ ---
50
+
51
+ ## 安装
52
+
53
+ 通过 npm、yarn 或 pnpm 安装组件包:
54
+
55
+ ```bash
56
+ # 使用 npm 安装
57
+ npm install road-traffic-viewer
58
+
59
+ # 使用 yarn 安装
60
+ yarn add road-traffic-viewer
61
+
62
+ # 使用 pnpm 安装
63
+ pnpm add road-traffic-viewer
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 特性概览
69
+
70
+ | 特性 | 说明 |
71
+ |------|------|
72
+ | 📍 **灵活桩号** | 支持 `"K112"` / `"k112.500"` / `"K112+500"` / `112000` 等多种输入格式,整公里显示 `K112`,非整公里显示 `K112+500` |
73
+ | 🛣️ **多车道** | 上下行独立配置车道数,车道间白色虚线分隔,车辆 `lane` 字段定位到具体车道 |
74
+ | 🎨 **全配置化颜色** | 道路底色(rgba)、路肩、隔离带、双黄线、车道线、文字等全部可自定义 |
75
+ | 🚦 **路段流量染色** | 根据 TrafficFlow 数据对道路区间按 0:畅通/1:缓行/2:拥堵/3:严重拥堵 染色,颜色可配置 |
76
+ | 🚙 **车辆精细渲染** | 14 层 Canvas 绘制(阴影→车身→车顶→挡风玻璃→车灯→轮廓),支持 SVG 图片替代 |
77
+ | 🖼️ **SVG 图片** | 车辆和道路底图均支持 SVG URL / data URI / 原始 SVG 字符串 |
78
+ | 🖱️ **鼠标拖拽** | 当视口小于路线总长时,鼠标拖拽平移路线(grab/grabbing 光标) |
79
+ | 🔍 **比例尺** | `scale` = 视口可见米数,值越小放得越大,车辆移动越明显 |
80
+ | 🧭 **行驶方向** | 上行 (direction=-1) 桩号小→大 车头朝右,下行 (direction=1) 桩号大→小 车头朝左 |
81
+ | ⚡ **RAF 节流** | requestAnimationFrame 单帧调度,轻松支撑 1000+ 台车辆 |
82
+ | 📐 **灵活尺寸** | 宽高支持 px 数值和百分比字符串 |
83
+ | 🏷️ **标签可控** | `showCarSpeed` / `showCarId` 控制是否显示速度/ID 标签,speed 字段可选 |
84
+ | 🔧 **框架无关** | 核心渲染类可在纯 JS 中直接 `new` 使用,同步提供 Vue 3 / Vue 2 组件 |
85
+
86
+ ---
87
+
88
+ ## 快速开始
89
+
90
+ ### Vue 3 使用方式
91
+
92
+ ```vue
93
+ <template>
94
+ <div style="width: 100%; height: 200px;">
95
+ <RoadTrafficViewer
96
+ ref="viewerRef"
97
+ :config="config"
98
+ :vehicles="vehicles"
99
+ :traffic-flows="trafficFlows"
100
+ />
101
+ </div>
102
+ </template>
103
+
104
+ <script setup lang="ts">
105
+ import { ref, onMounted } from 'vue'
106
+ import { RoadTrafficViewer } from 'road-traffic-viewer'
107
+ import type { RoadTrafficConfig, Vehicle, TrafficFlow, RoadTrafficViewerExposed } from 'road-traffic-viewer'
108
+
109
+ // 1. 配置
110
+ const config: RoadTrafficConfig = {
111
+ startPile: 'K112',
112
+ endPile: 'K122',
113
+ scale: 2000, // 视口显示 2000 米路线
114
+ lanesDown: 2, // 下行 2 车道
115
+ lanesUp: 2, // 上行 2 车道
116
+ segmentIntervalMeters: 1000,
117
+ paddingStartMeters: 50,
118
+ paddingEndMeters: 50,
119
+ }
120
+
121
+ // 2. 车辆数据(direction: 1=下行, -1=上行)
122
+ const vehicles = ref<Vehicle[]>([
123
+ {
124
+ id: 'V001',
125
+ position: 'K115+200', // K115+200 = 115200 米
126
+ speed: 80,
127
+ direction: 1, // 下行(隔离带下方,自右向左行驶)
128
+ carColor: '#00ff88',
129
+ lane: 2,
130
+ },
131
+ {
132
+ id: 'V002',
133
+ position: 116500, // 直接传米数
134
+ speed: 30,
135
+ direction: -1, // 上行(隔离带上方,自左向右行驶)
136
+ carColor: '#ff4444',
137
+ lane: 1,
138
+ },
139
+ ])
140
+
141
+ // 3. 路段流量数据
142
+ const trafficFlows = ref<TrafficFlow[]>([
143
+ { startPile: 'K115', endPile: 'K117', direction: 1, status: 3 }, // 下行严重拥堵
144
+ { startPile: 'K118', endPile: 'K120', direction: -1, status: 1 }, // 上行缓行
145
+ ])
146
+
147
+ const viewerRef = ref<RoadTrafficViewerExposed | null>(null)
148
+
149
+ onMounted(() => {
150
+ console.log('核心实例就绪:', viewerRef.value?.core)
151
+ })
152
+ </script>
153
+ ```
154
+
155
+ ### Vue 2 使用方式
156
+
157
+ ```vue
158
+ <template>
159
+ <div style="width: 100%; height: 200px;">
160
+ <RoadTrafficViewerV2
161
+ ref="viewer"
162
+ :config="config"
163
+ :vehicles="vehicles"
164
+ :traffic-flows="trafficFlows"
165
+ @ready="onReady"
166
+ />
167
+ </div>
168
+ </template>
169
+
170
+ <script>
171
+ import { RoadTrafficViewerV2 } from 'road-traffic-viewer'
172
+
173
+ export default {
174
+ components: { RoadTrafficViewerV2 },
175
+ data() {
176
+ return {
177
+ config: {
178
+ startPile: 'K0',
179
+ endPile: 'K20',
180
+ scale: 5000,
181
+ lanesDown: 2,
182
+ lanesUp: 2,
183
+ },
184
+ vehicles: [
185
+ { id: 1, position: 'K5', speed: 80, direction: 1 },
186
+ { id: 2, position: 'K15', speed: 60, direction: -1, carColor: '#ffaa00' },
187
+ ],
188
+ trafficFlows: [
189
+ { startPile: 'K8', endPile: 'K12', direction: 1, status: 2 },
190
+ ],
191
+ }
192
+ },
193
+ methods: {
194
+ onReady(core) {
195
+ console.log('核心实例就绪:', core)
196
+ },
197
+ },
198
+ }
199
+ </script>
200
+ ```
201
+
202
+ ### 纯 JS 使用方式
203
+
204
+ ```html
205
+ <div id="road-container" style="width: 100%; height: 200px;"></div>
206
+ <script type="module">
207
+ import { RoadTrafficCore } from 'road-traffic-viewer'
208
+
209
+ const container = document.getElementById('road-container')
210
+ const core = new RoadTrafficCore(container, {
211
+ startPile: 'K112',
212
+ endPile: 'K122',
213
+ scale: 5000,
214
+ lanesDown: 2,
215
+ lanesUp: 2,
216
+ })
217
+
218
+ core.render()
219
+
220
+ core.updateVehicles([
221
+ { id: 'V1', position: 'K115', speed: 80, direction: 1, lane: 1 },
222
+ { id: 'V2', position: 'K118', speed: 45, direction: -1, lane: 2 },
223
+ ])
224
+
225
+ core.updateTrafficFlows([
226
+ { startPile: 'K115', endPile: 'K117', direction: 1, status: 3 },
227
+ ])
228
+
229
+ window.addEventListener('resize', () => core.render())
230
+ </script>
231
+ ```
232
+
233
+ ---
234
+
235
+ ## 桩号系统
236
+
237
+ ### 输入格式(自由)
238
+
239
+ | 输入 | 含义 | 解析结果(米) |
240
+ |------|------|---------------|
241
+ | `"K112"` | K112 | 112000 |
242
+ | `"k112.500"` | K112+500 | 112500 |
243
+ | `"K112+500"` | K112+500 | 112500 |
244
+ | `112000` | 纯米数 | 112000 |
245
+ | `"112000"` | 数字字符串 | 112000 |
246
+ | `"k0.123"` | K0+123 | 123 |
247
+
248
+ ### 显示格式
249
+
250
+ 内部统一以**米**为单位存储和计算,展示时整公里省略 `+000`:
251
+
252
+ ```
253
+ formatPile(112000) // => "K112"
254
+ formatPile(112500) // => "K112+500"
255
+ formatPile(123) // => "K0+123"
256
+ ```
257
+
258
+ ### 工具函数
259
+
260
+ ```ts
261
+ import { parsePile, formatPile, formatPileShort } from 'road-traffic-viewer'
262
+
263
+ parsePile('K112+500') // => 112500 (米)
264
+ formatPile(112000) // => "K112"(整公里省略 +000)
265
+ formatPile(112500) // => "K112+500"
266
+ formatPileShort(112500) // => "K112"(始终只显示公里数)
267
+ ```
268
+
269
+ ---
270
+
271
+ ## 配置项详解
272
+
273
+ > 以下列出 `RoadTrafficConfig` 中**全部**可配置属性,含类型、默认值和详细注释。
274
+
275
+ ### 路线与桩号
276
+
277
+ | 配置项 | 类型 | 默认值 | 说明 |
278
+ |--------|------|--------|------|
279
+ | `startPile` | `number \| string` | **必填** | 路线起点桩号。支持 `"K112"` / `"K112+500"` / `112000` 等格式 |
280
+ | `endPile` | `number \| string` | **必填** | 路线终点桩号。格式同 `startPile` |
281
+ | `paddingStartMeters` | `number` | `0` | 起点外侧额外空白米数,防止起点桩号标记被遮挡 |
282
+ | `paddingEndMeters` | `number` | `0` | 终点外侧额外空白米数,防止终点桩号标记被遮挡 |
283
+ | `segmentIntervalMeters` | `number` | `1000` | 桩号标尺分段间隔(米)。默认 1000 米一段 |
284
+
285
+ ### 比例尺与尺寸
286
+
287
+ | 配置项 | 类型 | 默认值 | 说明 |
288
+ |--------|------|--------|------|
289
+ | `scale` | `number` | `1000` | 显示比例尺:画布视口内可见的路线米数。值越小看得越细(放大)。**若 scale < 路线总长,支持鼠标拖拽平移** |
290
+ | `canvasWidth` | `number \| string` | `"100%"` | 画布宽度。支持 px 数值或百分比字符串 |
291
+ | `canvasHeight` | `number \| string` | 自动计算 | 画布高度。不传则根据车道数自动计算。支持 px 数值或百分比字符串 |
292
+
293
+ **比例尺参考(scale = 视口可见米数):**
294
+
295
+ | scale | 视口可见 | 10km 路线能否全览 | 适用场景 |
296
+ |-------|----------|-------------------|----------|
297
+ | `500` | 500 米 | ❌ 需拖拽 | 放大细览,车辆清晰可见 |
298
+ | `2000` | 2 公里 | ❌ 需拖拽 | 中览,适合主视图 |
299
+ | `5000` | 5 公里 | ❌ 需拖拽 | 粗览 |
300
+ | `10000` | 10 公里 | ✅ 全览 | 总览,刚好显示整条路线 |
301
+ | `20000` | 20 公里 | ✅ 全览 | 超粗览 |
302
+
303
+ ### 方向定义
304
+
305
+ | 配置项 | 类型 | 默认值 | 说明 |
306
+ |--------|------|--------|------|
307
+ | `directionDown` | `number \| string` | `1` | 下行(桩号由大变小,隔离带下方)对应的 `direction` 值。可自定义映射 |
308
+ | `directionUp` | `number \| string` | `-1` | 上行(桩号由小变大,隔离带上方)对应的 `direction` 值。可自定义映射 |
309
+
310
+ ### 车道布局
311
+
312
+ | 配置项 | 类型 | 默认值 | 说明 |
313
+ |--------|------|--------|------|
314
+ | `lanesUp` | `number` | `1` | 上行车道数量(隔离带上方,桩号小→大方向)。传 0 则不显示 |
315
+ | `lanesDown` | `number` | `1` | 下行车道数量(隔离带下方,桩号大→小方向)。传 0 则不显示 |
316
+ | `laneHeight` | `number` | `28` | 单条车道高度(像素)。影响整个道路区域的总高度 |
317
+ | `laneLineColor` | `string` | `"#ffffff"` | 车道间间隔虚线颜色。默认白色 |
318
+ | `roadPadding` | `number` | `3` | 路肩高度(像素),车道外侧到道路边缘的间距 |
319
+ | `medianHeight` | `number` | `6` | 中央隔离带高度(像素),上下行车道之间的分隔带高度 |
320
+
321
+ **车道编号规则:**
322
+
323
+ - 车道编号从 **1** 开始,**1 号车道** = 最靠近中央隔离带的车道
324
+ - 编号越大,离中央隔离带越远
325
+ - 车辆的 `lane` 字段默认值为 1
326
+
327
+ ```
328
+ ┌──────────────────────────────────────┐
329
+ │ 路肩 (roadPadding) │
330
+ ├──────────────────────────────────────┤
331
+ │ 上行 车道3 (离隔离带最远) │ lane: 3
332
+ │ - - - - - - - - - - - - - - - - - │ ← 虚线 (laneLineColor)
333
+ │ 上行 车道2 │ lane: 2
334
+ │ - - - - - - - - - - - - - - - - - │ ← 虚线
335
+ │ 上行 车道1 (离隔离带最近) │ lane: 1
336
+ │ direction=-1 桩号小→大 →→→ │ (车头朝右)
337
+ ├══════════════════════════════════════┤
338
+ │ ═══ 隔离带 (medianHeight) ════════════ │ + 双黄线 (medianLineColor)
339
+ ├══════════════════════════════════════┤
340
+ │ 下行 车道1 (离隔离带最近) │ lane: 1
341
+ │ ←←← direction=1 桩号大→小 │ (车头朝左)
342
+ │ - - - - - - - - - - - - - - - - - │ ← 虚线
343
+ │ 下行 车道2 │ lane: 2
344
+ │ - - - - - - - - - - - - - - - - - │ ← 虚线
345
+ │ 下行 车道3 (离隔离带最远) │ lane: 3
346
+ ├──────────────────────────────────────┤
347
+ │ 路肩 (roadPadding) │
348
+ └──────────────────────────────────────┘
349
+ ```
350
+
351
+ ### 道路颜色
352
+
353
+ | 配置项 | 类型 | 默认值 | 说明 |
354
+ |--------|------|--------|------|
355
+ | `roadBg` | `string` | `"#141a28"` | 道路主底色。支持 rgba 透明度,如 `"rgba(20,26,40,0.8)"` |
356
+ | `roadBgImage` | `string` | — | 道路底图 SVG(URL / data URI / 原始 SVG)。传入后替代 `roadBg` |
357
+ | `roadShoulder` | `string` | `"#1e2a3e"` | 路肩区域颜色 |
358
+ | `roadLine` | `string` | `"#2a3a55"` | 路边线颜色(最外侧边界线) |
359
+ | `roadEdge` | `string` | `"#3a5070"` | 道路边缘刻度/标尺线颜色 |
360
+ | `medianBg` | `string` | `"#1a2535"` | 中央隔离带背景填充色 |
361
+ | `medianLineColor` | `string` | `"#ffb020"` | 中央隔离带双黄线颜色。默认金黄色 |
362
+ | `canvasBg` | `string` | `"#0a0f1a"` | 画布整体背景色(道路区域之外的填充色) |
363
+ | `markerColor` | `string` | `"#3a5070"` | 桩号标记文字和刻度线的颜色 |
364
+ | `laneLabelColor` | `string` | `"#2a3a55"` | 车道标签文字("上行"/"下行")的颜色 |
365
+
366
+ ### 车辆渲染
367
+
368
+ | 配置项 | 类型 | 默认值 | 说明 |
369
+ |--------|------|--------|------|
370
+ | `defaultCarColor` | `string` | `"#00ff88"` | 车辆默认颜色(数据中无 `carColor` 时使用) |
371
+ | `carLength` | `number` | `16` | 车辆图标长度(像素) |
372
+ | `carWidth` | `number` | `12` | 车辆图标宽度(像素) |
373
+ | `carSvgUrl` | `string` | — | 车辆 SVG 图片(URL / data URI / 原始 SVG)。传入后替代默认 14 层 Canvas 绘制 |
374
+ | `showCarSpeed` | `boolean` | `true` | 是否在车身上方显示速度标签(km/h) |
375
+ | `showCarId` | `boolean` | `true` | 是否在车身下方显示车辆 ID 标签 |
376
+
377
+ ### 路段流量状态颜色
378
+
379
+ | 配置项 | 类型 | 默认值 | 说明 |
380
+ |--------|------|--------|------|
381
+ | `trafficStatusColors` | `{ key: number; color: string }[]` | 见下方 | 拥堵状态 → 路面颜色映射。`key` 为状态值,`color` 为 CSS 颜色 |
382
+
383
+ **默认映射:**
384
+
385
+ ```ts
386
+ [
387
+ { key: 3, color: '#ff4d6a' }, // 严重拥堵 → 红色
388
+ { key: 2, color: '#ff9500' }, // 拥堵 → 橙色
389
+ { key: 1, color: '#00bfff' }, // 缓行 → 青色
390
+ { key: 0, color: '#00ff88' }, // 畅通 → 绿色
391
+ ]
392
+ ```
393
+
394
+ ---
395
+
396
+ ## 数据格式
397
+
398
+ ### 车辆数据 Vehicle
399
+
400
+ ```ts
401
+ interface Vehicle {
402
+ /** 车辆唯一 ID */
403
+ id: string | number
404
+ /** 车辆所在桩号 */
405
+ position: number | string
406
+ /** 速度 km/h(可选),不传则不显示速度标签 */
407
+ speed?: number
408
+ /** 行驶方向:1=下行, -1=上行 */
409
+ direction: number
410
+ /** 车辆颜色(可选) */
411
+ carColor?: string
412
+ /** 所在车道编号(可选),从 1 开始,默认 1 */
413
+ lane?: number
414
+ /** 车辆 SVG 图片地址(可选),优先级高于全局 carSvgUrl */
415
+ carSvgUrl?: string
416
+ }
417
+ ```
418
+
419
+ ### 路段流量数据 TrafficFlow
420
+
421
+ ```ts
422
+ interface TrafficFlow {
423
+ /** 流量状态起点桩号 */
424
+ startPile: number | string
425
+ /** 流量状态终点桩号 */
426
+ endPile: number | string
427
+ /** 方向:1=下行, -1=上行 */
428
+ direction: number
429
+ /** 拥堵状态:0=畅通 / 1=缓行 / 2=拥堵 / 3=严重拥堵 */
430
+ status: number | string
431
+ }
432
+ ```
433
+
434
+ ---
435
+
436
+ ## API 方法
437
+
438
+ ### RoadTrafficCore(核心类)
439
+
440
+ ```ts
441
+ class RoadTrafficCore {
442
+ constructor(container: HTMLElement, config: RoadTrafficConfig)
443
+
444
+ /** 重绘道路(窗口 resize 后调用) */
445
+ render(): void
446
+
447
+ /** 更新车辆数据(RAF 节流) */
448
+ updateVehicles(vehicles: Vehicle[]): void
449
+
450
+ /** 更新路段流量数据 */
451
+ updateTrafficFlows(flows: TrafficFlow[]): void
452
+
453
+ /** 同时更新车辆和流量 */
454
+ updateAll(vehicles: Vehicle[], flows: TrafficFlow[]): void
455
+
456
+ /** 部分更新配置并重绘 */
457
+ updateConfig(partial: Partial<RoadTrafficConfig>): void
458
+
459
+ /** 获取 Canvas 元素 */
460
+ getCanvas(): HTMLCanvasElement
461
+
462
+ /** 获取渲染度量参数 */
463
+ getMetrics(): RenderMetrics | null
464
+
465
+ /** 销毁实例 */
466
+ destroy(): void
467
+ }
468
+ ```
469
+
470
+ ### RoadTrafficViewer(Vue 3 组件)
471
+
472
+ | Prop | 类型 | 说明 |
473
+ |------|------|------|
474
+ | `config` | `RoadTrafficConfig` | 渲染配置(必填) |
475
+ | `vehicles` | `Vehicle[]` | 车辆数据 |
476
+ | `trafficFlows` | `TrafficFlow[]` | 路段流量数据 |
477
+
478
+ | 事件 | 参数 | 说明 |
479
+ |------|------|------|
480
+ | `ready` | `core: RoadTrafficCore` | 渲染引擎就绪时触发 |
481
+
482
+ | 暴露 | 类型 | 说明 |
483
+ |------|------|------|
484
+ | `core` | `RoadTrafficCore \| null` | 核心渲染实例 |
485
+ | `containerRef` | `HTMLDivElement \| null` | 容器 DOM 引用 |
486
+
487
+ ### RoadTrafficViewerV2(Vue 2 组件)
488
+
489
+ | Prop | 类型 | 说明 |
490
+ |------|------|------|
491
+ | `config` | `Object` | 渲染配置(必填) |
492
+ | `vehicles` | `Array` | 车辆数据 |
493
+ | `trafficFlows` | `Array` | 路段流量数据 |
494
+
495
+ | 事件 | 参数 | 说明 |
496
+ |------|------|------|
497
+ | `ready` | `core` | 渲染引擎就绪时触发 |
498
+
499
+ ---
500
+
501
+ ## 使用案例
502
+
503
+ ### 案例 1:基础双向单车道
504
+
505
+ ```vue
506
+ <template>
507
+ <div style="width: 100%; height: 180px;">
508
+ <RoadTrafficViewer ref="viewerRef" :config="config" :vehicles="vehicles" />
509
+ </div>
510
+ </template>
511
+
512
+ <script setup lang="ts">
513
+ import { ref, onMounted } from 'vue'
514
+ import { RoadTrafficViewer } from 'road-traffic-viewer'
515
+ import type { RoadTrafficConfig, Vehicle, RoadTrafficViewerExposed } from 'road-traffic-viewer'
516
+
517
+ const config: RoadTrafficConfig = {
518
+ startPile: 'K100',
519
+ endPile: 'K120',
520
+ scale: 2000, // 视口显示 2000 米路线
521
+ }
522
+
523
+ // direction: 1=下行(隔离带下方), -1=上行(隔离带上方)
524
+ const vehicles = ref<Vehicle[]>([
525
+ { id: 1, position: 'K105', speed: 80, direction: 1 }, // 下行
526
+ { id: 2, position: 'K108', speed: 60, direction: 1, carColor: '#ffaa00' },
527
+ { id: 3, position: 'K110', speed: 90, direction: -1 }, // 上行
528
+ ])
529
+
530
+ const viewerRef = ref<RoadTrafficViewerExposed | null>(null)
531
+ onMounted(() => { console.log(viewerRef.value?.core) })
532
+ </script>
533
+ ```
534
+
535
+ ### 案例 2:双向 3 车道 + 流量染色
536
+
537
+ ```vue
538
+ <script setup lang="ts">
539
+ const config: RoadTrafficConfig = {
540
+ startPile: 'K50', endPile: 'K70', scale: 3000,
541
+ lanesDown: 3, lanesUp: 3,
542
+ segmentIntervalMeters: 500,
543
+ paddingStartMeters: 100, paddingEndMeters: 100,
544
+ }
545
+
546
+ const vehicles = ref<Vehicle[]>([
547
+ { id: 'A1', position: 'K52', speed: 75, direction: 1, lane: 1 },
548
+ { id: 'A2', position: 'K53', speed: 70, direction: 1, lane: 2 },
549
+ { id: 'B1', position: 'K60', speed: 85, direction: -1, lane: 1 },
550
+ { id: 'B2', position: 'K61', speed: 80, direction: -1, lane: 2 },
551
+ ])
552
+
553
+ const trafficFlows = ref<TrafficFlow[]>([
554
+ { startPile: 'K55', endPile: 'K58', direction: 1, status: 3 },
555
+ { startPile: 'K60', endPile: 'K63', direction: -1, status: 1 },
556
+ ])
557
+ </script>
558
+ ```
559
+
560
+ ### 案例 3:自定义颜色 + SVG 车辆
561
+
562
+ ```vue
563
+ <script setup lang="ts">
564
+ const config: RoadTrafficConfig = {
565
+ startPile: 'K0', endPile: 'K10', scale: 5000,
566
+ // 亮色道路主题 + 透明度
567
+ roadBg: 'rgba(232,232,232,0.9)',
568
+ roadShoulder: '#cccccc',
569
+ medianBg: '#dddddd',
570
+ medianLineColor: '#ffaa00',
571
+ canvasBg: '#ffffff',
572
+ // SVG 车辆图片
573
+ carSvgUrl: '<svg viewBox="0 0 1024 1024" ...>...</svg>',
574
+ carLength: 24, carWidth: 18,
575
+ // 自定义流量颜色
576
+ trafficStatusColors: [
577
+ { key: 3, color: '#cc0000' },
578
+ { key: 2, color: '#ee8800' },
579
+ { key: 1, color: '#eebb00' },
580
+ { key: 0, color: '#00aa44' },
581
+ ],
582
+ }
583
+ </script>
584
+ ```
585
+
586
+ ### 案例 4:纯 JS 环境 + 定时器
587
+
588
+ ```html
589
+ <div id="road" style="width: 1200px; height: 200px;"></div>
590
+ <script type="module">
591
+ import { RoadTrafficCore, parsePile } from 'road-traffic-viewer'
592
+
593
+ const core = new RoadTrafficCore(document.getElementById('road'), {
594
+ startPile: 'K0', endPile: 'K20', scale: 5000,
595
+ lanesDown: 2, lanesUp: 2,
596
+ })
597
+ core.render()
598
+
599
+ setInterval(() => {
600
+ core.updateVehicles(
601
+ Array.from({ length: 200 }, (_, i) => ({
602
+ id: i,
603
+ position: Math.random() * 20000,
604
+ speed: Math.random() * 120,
605
+ direction: Math.random() > 0.5 ? 1 : -1,
606
+ lane: Math.ceil(Math.random() * 2),
607
+ carColor: ['#00ff88', '#ffaa00', '#ff4444'][Math.floor(Math.random() * 3)],
608
+ }))
609
+ )
610
+ }, 1000)
611
+ </script>
612
+ ```
613
+
614
+ ### 案例 5:单车道 + WebSocket 实时推送
615
+
616
+ ```vue
617
+ <script setup lang="ts">
618
+ const config: RoadTrafficConfig = {
619
+ startPile: 'K0', endPile: 'K50', scale: 3000,
620
+ lanesDown: 1, lanesUp: 0, // 仅下行单车道
621
+ segmentIntervalMeters: 2000,
622
+ paddingStartMeters: 80, paddingEndMeters: 80,
623
+ }
624
+
625
+ const vehicles = ref<Vehicle[]>([])
626
+ const trafficFlows = ref<TrafficFlow[]>([])
627
+
628
+ let ws: WebSocket | null = null
629
+
630
+ onMounted(() => {
631
+ ws = new WebSocket('wss://your-server.com/traffic')
632
+ ws.onmessage = (e) => {
633
+ const raw = JSON.parse(e.data)
634
+ vehicles.value = raw.vehicles || []
635
+ if (raw.flows) trafficFlows.value = raw.flows
636
+ }
637
+ })
638
+
639
+ onUnmounted(() => { ws?.close() })
640
+ </script>
641
+ ```
642
+
643
+ ### 案例 6:双向多车道 + WebSocket 实时推送
644
+
645
+ 高速公路场景:上下行各 3 车道,WebSocket 高频推送 500+ 台车辆,含心跳保活和预过滤。
646
+
647
+ ```vue
648
+ <script setup lang="ts">
649
+ const config: RoadTrafficConfig = {
650
+ startPile: 'K100', endPile: 'K160', scale: 5000,
651
+ lanesDown: 3, lanesUp: 3, laneHeight: 24,
652
+ paddingStartMeters: 100, paddingEndMeters: 100,
653
+ }
654
+
655
+ const vehicles = ref<Vehicle[]>([])
656
+ const trafficFlows = ref<TrafficFlow[]>([])
657
+ let ws: WebSocket | null = null
658
+ let heartbeatTimer: ReturnType<typeof setInterval> | null = null
659
+
660
+ onMounted(() => {
661
+ ws = new WebSocket('wss://highway.example.com/ws')
662
+ ws.onopen = () => {
663
+ heartbeatTimer = setInterval(() => ws?.send(JSON.stringify({ type: 'ping' })), 30000)
664
+ }
665
+ ws.onmessage = (e) => {
666
+ const msg = JSON.parse(e.data)
667
+ switch (msg.type) {
668
+ case 'vehicles':
669
+ vehicles.value = msg.data.filter((v: Vehicle) => {
670
+ const pos = typeof v.position === 'number' ? v.position : parsePile(v.position)
671
+ return pos >= 100000 - 200 && pos <= 160000 + 200
672
+ })
673
+ break
674
+ case 'traffic_flow':
675
+ trafficFlows.value = msg.data
676
+ break
677
+ }
678
+ }
679
+ })
680
+
681
+ onUnmounted(() => {
682
+ if (heartbeatTimer) clearInterval(heartbeatTimer)
683
+ ws?.close()
684
+ })
685
+ </script>
686
+ ```
687
+
688
+ ### 案例 7:道路底图 SVG + 车辆 SVG + rgba 透明度
689
+
690
+ ```vue
691
+ <script setup lang="ts">
692
+ const config: RoadTrafficConfig = {
693
+ startPile: 'K0', endPile: 'K30', scale: 5000,
694
+ lanesDown: 2, lanesUp: 2,
695
+ // 道路底图
696
+ roadBgImage: '<svg viewBox="0 0 1024 200" xmlns="...">...</svg>',
697
+ // 半透明底色兜底(SVG 加载前显示)
698
+ roadBg: 'rgba(20, 26, 40, 0.6)',
699
+ // SVG 车辆
700
+ carSvgUrl: 'https://example.com/car-icon.svg',
701
+ carLength: 20, carWidth: 16,
702
+ }
703
+ </script>
704
+ ```
705
+
706
+ ### 案例 8:隐藏标签 + speed 为空
707
+
708
+ 不显示速度和 ID 标签,适合纯图标展示场景。
709
+
710
+ ```vue
711
+ <script setup lang="ts">
712
+ const config: RoadTrafficConfig = {
713
+ startPile: 'K0', endPile: 'K50', scale: 5000,
714
+ lanesDown: 2, lanesUp: 2,
715
+ showCarSpeed: false, // 隐藏速度标签
716
+ showCarId: false, // 隐藏 ID 标签
717
+ }
718
+
719
+ const vehicles = ref<Vehicle[]>([
720
+ { id: 1, position: 'K10', direction: 1, carSvgUrl: '<svg ...>...</svg>' },
721
+ { id: 2, position: 'K20', direction: -1 }, // speed 为空,使用默认绘制
722
+ ])
723
+ </script>
724
+ ```
725
+
726
+ ### 案例 9:定时器模拟行驶 — 全部配置展示(深色主题)
727
+
728
+ 用 `setInterval` 模拟两辆车在上下行车道来回行驶,**展示全部 30+ 配置项**含默认值注释。
729
+
730
+ ```vue
731
+ <template>
732
+ <div style="width: 100%; height: 220px;">
733
+ <RoadTrafficViewer ref="viewer" :config="config" :vehicles="vehicles" :traffic-flows="flows" />
734
+ </div>
735
+ </template>
736
+
737
+ <script setup lang="ts">
738
+ import { ref, onMounted, onUnmounted } from 'vue'
739
+ import { RoadTrafficViewer, parsePile } from 'road-traffic-viewer'
740
+ import type { RoadTrafficConfig, Vehicle, TrafficFlow } from 'road-traffic-viewer'
741
+
742
+ const config: RoadTrafficConfig = {
743
+ // ---- 路线与桩号 ----
744
+ startPile: 'K100', // 起点桩号,K100 = 100000 米
745
+ endPile: 'K120', // 终点桩号,K120 = 120000 米
746
+ paddingStartMeters: 60, // 起点空白米数,默认 0
747
+ paddingEndMeters: 60, // 终点空白米数,默认 0
748
+ segmentIntervalMeters: 1000, // 分段间隔(米),默认 1000
749
+
750
+ // ---- 比例尺与尺寸 ----
751
+ scale: 5000, // 视口可见 5000 米,默认 1000
752
+ canvasWidth: '100%', // 画布宽度,默认 "100%"
753
+ // canvasHeight 不传,自动计算
754
+
755
+ // ---- 方向定义 ----
756
+ directionDown: 1, // 下行方向值,默认 1
757
+ directionUp: -1, // 上行方向值,默认 -1
758
+
759
+ // ---- 车道布局 ----
760
+ lanesDown: 1, // 下行车道数,默认 1
761
+ lanesUp: 1, // 上行车道数,默认 1
762
+ laneHeight: 28, // 单车道高度(px),默认 28
763
+ laneLineColor: '#ffffff', // 车道虚线颜色,默认白色
764
+ roadPadding: 3, // 路肩高度(px),默认 3
765
+ medianHeight: 6, // 隔离带高度(px),默认 6
766
+
767
+ // ---- 道路颜色 ----
768
+ roadBg: '#141a28', // 道路底色(支持 rgba),默认深蓝灰
769
+ roadBgImage: undefined, // 道路底图 SVG(可选)
770
+ roadShoulder: '#1e2a3e', // 路肩颜色
771
+ roadLine: '#2a3a55', // 路边线颜色
772
+ roadEdge: '#3a5070', // 道路边缘颜色
773
+ medianBg: '#1a2535', // 隔离带背景色
774
+ medianLineColor: '#ffb020', // 双黄线颜色
775
+ canvasBg: '#0a0f1a', // 画布背景色
776
+ markerColor: '#3a5070', // 桩号文字颜色
777
+ laneLabelColor: '#2a3a55', // 车道标签颜色
778
+
779
+ // ---- 车辆渲染 ----
780
+ defaultCarColor: '#00ff88', // 默认车辆颜色
781
+ carLength: 16, // 车辆长度(px),默认 16
782
+ carWidth: 12, // 车辆宽度(px),默认 12
783
+ carSvgUrl: undefined, // 全局 SVG 车辆(可选)
784
+
785
+ // ---- 标签显示 ----
786
+ showCarSpeed: true, // 显示速度标签,默认 true
787
+ showCarId: true, // 显示 ID 标签,默认 true
788
+
789
+ // ---- 流量状态颜色 ----
790
+ trafficStatusColors: [
791
+ { key: 3, color: '#ff4d6a' }, // 严重拥堵→红
792
+ { key: 2, color: '#ff9500' }, // 拥堵→橙
793
+ { key: 1, color: '#00bfff' }, // 缓行→青
794
+ { key: 0, color: '#00ff88' }, // 畅通→绿
795
+ ],
796
+ }
797
+
798
+ // 路段流量
799
+ const flows = ref<TrafficFlow[]>([
800
+ { startPile: 'K105', endPile: 'K108', direction: 1, status: 2 },
801
+ { startPile: 'K115', endPile: 'K118', direction: -1, status: 1 },
802
+ ])
803
+
804
+ // 两台车
805
+ const vehicles = ref<Vehicle[]>([
806
+ { id: 'CAR-DOWN', position: 'K102', speed: 65, direction: 1, lane: 1 },
807
+ { id: 'CAR-UP', position: 'K118', speed: 55, direction: -1, lane: 1 },
808
+ ])
809
+
810
+ // 定时模拟移动
811
+ const R_MIN = parsePile(config.startPile), R_MAX = parsePile(config.endPile)
812
+ let timer: ReturnType<typeof setInterval> | null = null
813
+ onMounted(() => {
814
+ timer = setInterval(() => {
815
+ vehicles.value = vehicles.value.map(v => {
816
+ const pos = parsePile(v.position)
817
+ const step = v.speed! / 18
818
+ let newPos = v.direction === -1 ? pos + step : pos - step
819
+ if (newPos >= R_MAX || newPos <= R_MIN) {
820
+ return { ...v, position: newPos >= R_MAX ? R_MAX : R_MIN, direction: -v.direction as (1 | -1) }
821
+ }
822
+ return { ...v, position: newPos }
823
+ })
824
+ }, 200)
825
+ })
826
+ onUnmounted(() => { if (timer) clearInterval(timer) })
827
+ </script>
828
+ ```
829
+
830
+ ### 案例 10:定时器多车 + 亮色主题 — 全部配置展示
831
+
832
+ 双向各 2 车道,4 辆车在不同车道行驶,到达终点后折返+切换车道。
833
+
834
+ ```vue
835
+ <script setup lang="ts">
836
+ const config: RoadTrafficConfig = {
837
+ startPile: 'K30', endPile: 'K48', scale: 4000,
838
+ paddingStartMeters: 80, paddingEndMeters: 80,
839
+ segmentIntervalMeters: 2000,
840
+ directionDown: 1, directionUp: -1,
841
+
842
+ lanesDown: 2, lanesUp: 2,
843
+ laneHeight: 24, laneLineColor: '#cccccc',
844
+ roadPadding: 4, medianHeight: 8,
845
+
846
+ // 亮色主题
847
+ roadBg: '#e0e0e0', roadShoulder: '#bdbdbd',
848
+ roadLine: '#9e9e9e', roadEdge: '#757575',
849
+ medianBg: '#d0d0d0', medianLineColor: '#ff8f00',
850
+ canvasBg: '#f5f5f5', markerColor: '#616161',
851
+ laneLabelColor: '#9e9e9e',
852
+
853
+ defaultCarColor: '#1976d2',
854
+ carLength: 18, carWidth: 14,
855
+ showCarSpeed: true, showCarId: true,
856
+
857
+ trafficStatusColors: [
858
+ { key: 3, color: '#d32f2f' },
859
+ { key: 2, color: '#f57c00' },
860
+ { key: 1, color: '#fbc02d' },
861
+ { key: 0, color: '#388e3c' },
862
+ ],
863
+ }
864
+
865
+ const vehicles = ref<Vehicle[]>([
866
+ { id: 'D1', position: 'K31+200', speed: 80, direction: 1, carColor: '#e91e63', lane: 1 },
867
+ { id: 'D2', position: 'K35+800', speed: 50, direction: 1, carColor: '#2196f3', lane: 2 },
868
+ { id: 'U1', position: 'K46+500', speed: 70, direction: -1, carColor: '#4caf50', lane: 1 },
869
+ { id: 'U2', position: 'K40+100', speed: 45, direction: -1, carColor: '#ff9800', lane: 2 },
870
+ ])
871
+
872
+ const flows = ref<TrafficFlow[]>([
873
+ { startPile: 'K33', endPile: 'K36', direction: 1, status: 3 },
874
+ { startPile: 'K42', endPile: 'K45', direction: -1, status: 2 },
875
+ ])
876
+ </script>
877
+ ```
878
+
879
+ ### 案例 11:Vue 2 使用方式
880
+
881
+ ```vue
882
+ <template>
883
+ <div style="width: 100%; height: 200px;">
884
+ <RoadTrafficViewerV2
885
+ :config="config"
886
+ :vehicles="vehicles"
887
+ :traffic-flows="trafficFlows"
888
+ @ready="onReady"
889
+ />
890
+ </div>
891
+ </template>
892
+
893
+ <script>
894
+ import { RoadTrafficViewerV2 } from 'road-traffic-viewer'
895
+
896
+ export default {
897
+ components: { RoadTrafficViewerV2 },
898
+ data() {
899
+ return {
900
+ config: {
901
+ startPile: 'K0', endPile: 'K20', scale: 5000,
902
+ lanesDown: 2, lanesUp: 2,
903
+ },
904
+ vehicles: [
905
+ { id: 1, position: 'K5', speed: 60, direction: 1 },
906
+ { id: 2, position: 'K15', speed: 80, direction: -1 },
907
+ ],
908
+ trafficFlows: [],
909
+ }
910
+ },
911
+ methods: {
912
+ onReady(core) {
913
+ console.log('核心实例就绪:', core)
914
+ // 可保存 core 引用用于后续手动操作
915
+ this.core = core
916
+ },
917
+ },
918
+ }
919
+ </script>
920
+ ```
921
+
922
+ ---
923
+
924
+ ## 性能说明
925
+
926
+ ### 节流机制
927
+
928
+ - 车辆渲染使用 `requestAnimationFrame` 单帧调度
929
+ - 高频调用 `updateVehicles()` 时数据缓存,仅在下一帧执行一次渲染
930
+ - 一帧内多次调用只触发一次实际绘制
931
+
932
+ ### 车辆数量建议
933
+
934
+ | 车辆数 | 性能 | 建议 |
935
+ |--------|------|------|
936
+ | < 100 | 毫无压力 | 无需特殊处理 |
937
+ | 100–1000 | 流畅 | 默认模式即可 |
938
+ | 1000–3000 | 基本流畅 | 建议预过滤超出视口的车辆 |
939
+ | > 3000 | 可能掉帧 | 强烈建议预过滤 |
940
+
941
+ ### 优化提示
942
+
943
+ 1. **大数组使用不可变替换**:`vehicles.value = [...newData]` 而非 push/splice
944
+ 2. **预过滤**:只保留视口范围内 ± 缓冲的车辆
945
+ 3. **合理设置 scale**:scale 越小视口越窄,需渲染的桩号标记越少
946
+ 4. **SVG 图片**:使用 SVG 渲染车辆时性能优于 14 层 Canvas 绘制
947
+
948
+ ---
949
+
950
+ ## 文件结构
951
+
952
+ ```
953
+ src/
954
+ types.ts — TypeScript 类型定义
955
+ pile-utils.ts — 桩号解析/格式化工具
956
+ road-traffic-core.ts — 核心 Canvas 渲染类(框架无关,1500+ 行)
957
+ RoadTrafficViewer.vue — Vue 3 组件封装
958
+ RoadTrafficViewerV2.vue — Vue 2 组件封装
959
+ index.ts — 统一导出入口
960
+ README.md — 本文档
961
+ ```