一、项目概述
1.1 项目背景
智慧公路管理是交通数字化转型的核心场景。传统公路管理系统往往功能分散、可视化手段单一,难以满足"一图统揽、全域感知"的管理需求。本项目旨在构建一个集二维/三维地图、数据大屏、业务管理于一体的综合GIS平台,为公路管理者提供全方位的决策支持。
1.2 项目预览
系统包含以下核心页面:
模块 技术栈 核心能力
综合态势 ECharts + OpenLayers KPI统计、路网监控、趋势分析
二维地图 OpenLayers 矢量/卫星双模式、7图层管理、实时路况
三维地图 Three.js 俯视视角、卫星影像底图、事件标注
智慧大屏 ECharts 全屏可视化、实时预警、路网态势
资产管理 OpenLayers 路线/桥梁/隧道可视化
应急指挥 OpenLayers 应急事件、资源调度
二、技术栈选型
2.1 核心技术栈
{
"框架": "Vue 3.5 + TypeScript 5.4",
"构建工具": "Vite 5.4",
"UI组件库": "Element Plus 2.14",
"状态管理": "Pinia 3.0",
"路由": "Vue Router 4.4 (Hash模式)",
"2D地图": "OpenLayers 10.9",
"3D地图": "Three.js 0.185",
"图表库": "ECharts 5.5",
"HTTP": "Axios 1.18"
}
2.2 选型理由
为什么选Vue3 + TypeScript?
Vue3的Composition API让逻辑复用更加灵活,TypeScript提供类型安全,特别适合大型GIS项目中海量数据结构的管理。比如地图要素的属性定义:
TypeScript
// 交通事件数据结构
interface TrafficEvent {
id: string
type: 'accident' | 'congestion' | 'construction' | 'breakdown'
name: string
lon: number
lat: number
level: 'danger' | 'warning' | 'info'
}
// 道路数据结构
interface Road {
id: string
name: string
status: 'smooth' | 'slow' | 'congested' | 'blocked'
coords: [number, number][]
}
为什么同时用OpenLayers和Three.js?
两者各有优势:
OpenLayers:专业2D地图库,瓦片加载、投影转换、矢量要素渲染成熟稳定
Three.js:WebGL 3D引擎,适合三维场景渲染、动画特效
双引擎互补,2D地图走OpenLayers,3D可视化走Three.js
Vite构建优化
TypeScript
// vite.config.ts 分包策略
build: {
rollupOptions: {
output: {
manualChunks: {
'vue': ['vue', 'vue-router'],
'echarts': ['echarts'],
'ol': ['ol'],
}
}
}
}
三、系统架构设计
3.1 目录结构
src/
├── components/ # 通用组件
│ └── MapContainer.vue # 地图容器
├── composables/ # 组合式函数
│ └── useMap3D.ts # 3D地图逻辑封装
├── config/ # 配置文件
│ └── map.ts # 地图配置(高德瓦片URL等)
├── data/ # 模拟数据
│ └── mockData.ts # 路线/桥梁/隧道等业务数据
├── layout/ # 布局组件
│ └── MainLayout.vue # 主布局(侧边栏+顶栏)
├── router/ # 路由配置
│ └── index.ts # 路由表+登录守卫
├── stores/ # Pinia状态
│ └── user.ts # 用户登录态
├── utils/ # 工具函数
│ └── mapUtils.ts # 坐标转换/瓦片URL生成
├── views/ # 14个页面组件
│ ├── Login.vue # 登录
│ ├── Dashboard.vue # 综合态势
│ ├── Map2D.vue # 二维地图
│ ├── Map3D.vue # 三维地图
│ ├── BigScreen.vue # 智慧大屏
│ ├── Emergency.vue # 应急指挥
│ └── ... # 其他业务页面
├── App.vue
├── main.ts
└── style.css # 全局主题
3.2 路由设计
路由采用Hash模式,分为三组:
TypeScript
const routes = [
// 1. 独立页面(无需登录)
{ path: '/login', component: Login },
{ path: '/bigscreen', component: BigScreen, meta: { public: true } },
// 2. 主布局子路由(需登录)
{
path: '/',
component: MainLayout,
redirect: '/dashboard',
children: [
{ path: 'dashboard', component: () => import('../views/Dashboard.vue') },
{ path: 'map-2d', component: () => import('../views/Map2D.vue') },
{ path: 'map-3d', component: () => import('../views/Map3D.vue') },
// ... 12个业务子页面,全部懒加载
]
}
]
// 3. 全局登录守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.public || userStore.isLogin) {
next()
} else {
next('/login')
}
})
3.3 主题设计
采用科技感深色主题,通过CSS变量统一管理:
CSS
:root {
/* 主色调 */
--primary-color: #00d4ff; /* 青色 */
--secondary-color: #7b68ee; /* 紫色 */
/* 状态色 */
--success-color: #00ff88;
--warning-color: #ffaa00;
--danger-color: #ff5566;
/* 背景 */
--bg-dark: #0a0e1a;
--bg-card: rgba(10, 20, 40, 0.95);
/* 边框 */
--border-color: rgba(0, 212, 255, 0.2);
/* 发光阴影 */
--shadow-glow: 0 0 20px rgba(0, 212, 255, 0.3);
}
配合毛玻璃效果(backdrop-filter: blur(16px))和发光动画,打造大屏科技感。
四、核心功能实现
4.1 二维地图 —— OpenLayers
二维地图是系统的核心页面,基于OpenLayers实现,支持矢量/卫星双模式切换、7种图层管理、实时路况、事件标注等。
瓦片底图加载
TypeScript
const createTrafficLayer = () => {
const source = new VectorSource()
const trafficColor: Record<string, string> = {
smooth: '#00ff88',
slow: '#ffaa00',
congested: '#ff5566',
blocked: '#990000'
}
trafficRoads.forEach(road => {
const feature = new Feature()
feature.setGeometry(new LineString(road.coords.map(c => fromLonLat(c))))
feature.setProperties({
featureType: 'road',
name: road.name,
status: road.status,
speed: getSpeedByStatus(road.status),
congestionIndex: getIndexByStatus(road.status)
})
feature.setStyle(new Style({
stroke: new Stroke({
color: trafficColor[road.status],
width: road.status === 'blocked' ? 8 : 5
}),
text: new Text({
text: road.name,
fill: new Fill({ color: '#fff' }),
stroke: new Stroke({ color: '#000', width: 3 }),
font: '12px Microsoft YaHei',
placement: 'line'
})
}))
source.addFeature(feature)
})
return new VectorLayer({ source, zIndex: 10 })
}
图层切换控制
TypeScript
1
2
3
4
5
6
7
8
9
const handleLayerChange = () => {
if (trafficLayer) trafficLayer.setVisible(layerChecked.value.includes('traffic'))
if (signalLayer) signalLayer.setVisible(layerChecked.value.includes('signal'))
if (cameraLayer) cameraLayer.setVisible(layerChecked.value.includes('camera'))
if (eventLayer) eventLayer.setVisible(layerChecked.value.includes('event'))
if (vehicleLayer) vehicleLayer.setVisible(layerChecked.value.includes('vehicle'))
if (hotspotLayer) hotspotLayer.setVisible(layerChecked.value.includes('hotspot'))
if (labelLayer) labelLayer.setVisible(layerChecked.value.includes('label'))
}
点击交互详情面板
TypeScript
map.on('singleclick', (evt) => {
const feature = map.forEachFeatureAtPixel(evt.pixel, (f) => f)
if (feature) {
const type = feature.get('featureType') || 'unknown'
detailType.value = type
detailData.value = feature.getProperties()
showDetailPanel.value = true
}
})
4.2 三维地图 —— Three.js
三维地图基于Three.js实现,采用俯视视角展示,支持矢量/卫星图模式切换,事件直接标注在地图上。
场景初始化
TypeScript
const initScene = () => {
if (!mapRef.value || !labelRef.value) return
scene = new THREE.Scene()
scene.background = new THREE.Color(0x030508)
scene.fog = new THREE.FogExp2(0x030508, 0.003)
// 俯视相机
const width = mapRef.value.clientWidth
const height = mapRef.value.clientHeight
camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000)
camera.position.set(0, 250, 0.1) // 正上方俯视
camera.lookAt(0, 0, 0)
// WebGL渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.toneMapping = THREE.ACESFilmicToneMapping
mapRef.value.appendChild(renderer.domElement)
// CSS2D标签渲染器
labelRenderer = new CSS2DRenderer()
labelRenderer.setSize(width, height)
labelRenderer.domElement.style.position = 'absolute'
labelRenderer.domElement.style.pointerEvents = 'none'
labelRef.value.appendChild(labelRenderer.domElement)
// 轨道控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.maxPolarAngle = Math.PI / 2 // 限制俯视角度
controls.target.set(0, 0, 0)
}
高德瓦片加载到Three.js
三维模式下加载高德卫星瓦片,拼贴为纹理贴到地面:
TypeScript
import { MAP_CONFIG, AMAP_TILE_URLS } from '../config/map'
import { getTileXY, getTileUrl } from '../utils/mapUtils'
const loadAmapTiles = async () => {
if (!realGround) return
const zoom = 12
const centerTile = getTileXY(MAP_CONFIG.CENTER_LON, MAP_CONFIG.CENTER_LAT, zoom)
const tileCount = 2
const tileSize = 256
// 创建拼贴画布
const tileCanvas = document.createElement('canvas')
tileCanvas.width = tileSize * tileCount
tileCanvas.height = tileSize * tileCount
const tileCtx = tileCanvas.getContext('2d')!
// 逐个加载瓦片
for (let dy = 0; dy < tileCount; dy++) {
for (let dx = 0; dx < tileCount; dx++) {
const tileX = centerTile.x + dx - 1
const tileY = centerTile.y + dy - 1
const url = getTileUrl(AMAP_TILE_URLS.satellite, tileX, tileY, zoom)
await new Promise<void>((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
tileCtx.drawImage(img, dx * tileSize, dy * tileSize, tileSize, tileSize)
resolve()
}
img.onerror = () => resolve()
img.src = url
})
}
}
// 转为Three.js纹理
const realTex = new THREE.CanvasTexture(tileCanvas)
realTex.wrapS = THREE.ClampToEdgeWrapping
realTex.wrapT = THREE.ClampToEdgeWrapping
;(realGround.material as THREE.MeshStandardMaterial).map = realTex
;(realGround.material as THREE.MeshStandardMaterial).needsUpdate = true
}
坐标转换工具
TypeScript
// src/utils/mapUtils.ts
// 经纬度转瓦片坐标
export function getTileXY(lon: number, lat: number, zoom: number) {
const n = Math.pow(2, zoom)
const x = Math.floor(((lon + 180) / 360) * n)
const y = Math.floor(
((1 - Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) / Math.PI) / 2) * n
)
return { x, y }
}
// 生成瓦片URL(支持子域名轮询)
export function getTileUrl(url: string, x: number, y: number, z: number): string {
const subdomain = ['1', '2', '3', '4'][Math.abs(x + y) % 4]
return url
.replace('{x}', x.toString())
.replace('{y}', y.toString())
.replace('{z}', z.toString())
.replace('{s}', subdomain)
}
事件平面标注
事件标注采用平面圆形图标,适合俯视视角:
TypeScript
const createEvents = () => {
const group = new THREE.Group()
const colorMap: Record<string, number> = {
danger: 0xff3333,
warning: 0xffaa00,
info: 0x00d4ff,
}
events.forEach(evt => {
// 内核圆
const innerGeo = new THREE.CircleGeometry(4, 32)
const innerMat = new THREE.MeshBasicMaterial({
color: colorMap[evt.level],
transparent: true,
opacity: 0.9
})
const inner = new THREE.Mesh(innerGeo, innerMat)
inner.rotation.x = -Math.PI / 2
inner.position.set(evt.x, 0.1, evt.z)
// 外圈脉冲环
const outerGeo = new THREE.RingGeometry(4, 6, 32)
const outerMat = new THREE.MeshBasicMaterial({
color: colorMap[evt.level],
transparent: true,
opacity: 0.5,
side: THREE.DoubleSide
})
const outer = new THREE.Mesh(outerGeo, outerMat)
outer.rotation.x = -Math.PI / 2
outer.position.set(evt.x, 0.15, evt.z)
pulseRings.push(outer)
// 中心白点
const dotGeo = new THREE.CircleGeometry(1.5, 16)
const dotMat = new THREE.MeshBasicMaterial({ color: 0xffffff })
const dot = new THREE.Mesh(dotGeo, dotMat)
dot.rotation.x = -Math.PI / 2
dot.position.set(evt.x, 0.2, evt.z)
// CSS2D标签
const labelDiv = document.createElement('div')
labelDiv.textContent = evt.name
labelDiv.style.cssText = `
color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap;
padding: 4px 12px; background: rgba(0, 0, 0, 0.7);
border: 1px solid ${evt.level === 'danger' ? '#ff3333' : '#ffaa00'};
border-radius: 6px;
`
const label = new CSS2DObject(labelDiv)
label.position.set(evt.x, 0, evt.z)
group.add(inner, outer, dot, label)
})
return group
}
动画循环
TypeScript
const animate = () => {
animationId = requestAnimationFrame(animate)
time += 0.016
controls?.update()
// 事件标注脉冲动画
pulseRings.forEach((ring, i) => {
const scale = 1 + Math.sin(time * 3 + i) * 0.15
ring.scale.set(scale, scale, 1)
const mat = ring.material as THREE.MeshBasicMaterial
mat.opacity = 0.4 + Math.sin(time * 3 + i) * 0.2
})
renderer?.render(scene!, camera!)
labelRenderer?.render(scene!, camera!)
}
4.3 智慧大屏 —— ECharts
大屏页面整合ECharts图表和OpenLayers地图,支持全屏展示:
TypeScript
// 路网运行态势 - 多系列柱状图
const trendChart = echarts.init(trendRef.value)
trendChart.setOption({
tooltip: { trigger: 'axis' },
legend: {
data: ['今日车流量', '昨日车流量', '上周同期'],
textStyle: { color: 'rgba(255,255,255,0.8)' }
},
xAxis: {
type: 'category',
data: ['00时', '04时', '08时', '12时', '16时', '20时', '24时'],
axisLabel: { color: 'rgba(255,255,255,0.6)' }
},
yAxis: { type: 'value', axisLabel: { color: 'rgba(255,255,255,0.6)' } },
series: [
{ name: '今日车流量', type: 'bar', data: [320, 180, 850, 920, 780, 650, 420] },
{ name: '昨日车流量', type: 'bar', data: [350, 200, 800, 880, 750, 680, 450] },
{ name: '上周同期', type: 'line', data: [380, 220, 820, 900, 760, 700, 480] }
]
})
4.4 全局主题深度定制
对Element Plus组件进行深度样式覆盖,使整体UI风格统一:
CSS
/* 深色科技主题覆盖Element Plus */
.el-menu {
background: transparent;
border-right: none;
}
.el-menu-item {
color: rgba(255, 255, 255, 0.65);
border-left: 2px solid transparent;
transition: all 0.3s;
}
.el-menu-item.is-active {
color: #00d4ff;
background: linear-gradient(90deg, rgba(0, 212, 255, 0.15), transparent);
border-left: 2px solid #00d4ff;
}
.el-button--primary {
background: linear-gradient(135deg, #00d4ff, #0099cc);
border: none;
}
/* 毛玻璃面板 */
.el-card {
background: rgba(10, 20, 40, 0.95);
backdrop-filter: blur(16px);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 12px;
}
五、性能优化实践
5.1 构建分包策略
TypeScript
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue': ['vue', 'vue-router'],
'echarts': ['echarts'],
'ol': ['ol'],
}
}
}
}
})
分包效果:
Chunk 大小 gzip后
vue 106 KB 41 KB
echarts 1036 KB 343 KB
ol 273 KB 79 KB
Map3D 555 KB 141 KB
index 1086 KB 344 KB
5.2 路由懒加载
所有业务页面均采用动态import实现按需加载:
TypeScript
{
path: 'map-2d',
name: 'Map2D',
component: () => import('../views/Map2D.vue')
}
5.3 3D地图性能优化
按需加载瓦片:仅在切换到卫星模式时才加载高德瓦片
纹理尺寸控制:瓦片拼贴画布使用合理尺寸避免内存溢出
动画精简:仅保留必要的脉冲动画,移除冗余特效
资源清理:组件卸载时释放WebGL资源
TypeScript
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
if (animationId) cancelAnimationFrame(animationId)
controls?.dispose()
renderer?.dispose()
scene?.clear()
})
5.4 Element Plus按需导入
TypeScript
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
AutoImport({ resolvers: [ElementPlusResolver()] }),
Components({ resolvers: [ElementPlusResolver()] }),
vue()
]
})
六、关键问题与解决方案
6.1 内存溢出问题
问题:3D地图构建时出现 JavaScript heap out of memory
原因:纹理尺寸过大(4096x4096)、3D建筑模型过多导致文件体积过大
解决方案:
Bash
# 1. 增加Node内存限制
set NODE_OPTIONS=--max-old-space-size=4096
TypeScript
// 2. 减小纹理尺寸
const tileCanvas = document.createElement('canvas')
tileCanvas.width = 512 // 原4096
tileCanvas.height = 512
// 3. 按需加载(仅切换到真实模式时才加载)
if (mode === 'real' && !amapTilesLoaded) {
loadAmapTiles()
}
// 4. 代码分割,提取到composable
// src/composables/useMap3D.ts
export function useMap3D(containerRef, labelRef) {
// 封装3D地图初始化逻辑
}
6.2 瓦片跨域问题
问题:高德瓦片加载时canvas被"污染",无法导出纹理
解决方案:设置 crossOrigin = 'anonymous'
TypeScript
const img = new Image()
img.crossOrigin = 'anonymous' // 关键
img.onload = () => {
tileCtx.drawImage(img, dx * tileSize, dy * tileSize, tileSize, tileSize)
resolve()
}
img.src = url
6.3 TypeScript类型错误
问题:img.onerror = resolve 类型不匹配
解决方案:
TypeScript
// 错误写法
img.onerror = resolve
// 正确写法
img.onerror = () => resolve()
七、项目亮点总结
7.1 三引擎协同
系统同时集成三种可视化引擎,各司其职:
Plain Text
┌─────────────────────────────────────────┐
│ 智慧公路GIS平台 │
├──────────┬──────────┬───────────────────┤
│ OpenLayers│ Three.js │ ECharts │
│ 2D地图 │ 3D地图 │ 数据大屏 │
│ │ │ │
│·瓦片底图 │·俯视渲染 │·统计图表 │
│·矢量要素 │·事件标注 │·趋势分析 │
│·交互选点 │·脉冲动画 │·预警展示 │
└──────────┴──────────┴───────────────────┘
7.2 双地图模式
2D和3D地图均支持矢量/卫星双模式切换:
矢量模式:自绘路网底图,深色科技风格
卫星模式:高德卫星影像瓦片,真实地理场景
7.3 完整业务闭环
覆盖公路管理全流程:
Plain Text
综合态势 → 路网监测 → 资产管理 → 养护管理
↓ ↓
应急指挥 ← 交通规划 ← 运维管理 ← 数据管理
7.4 科技感UI设计
深色背景 + 青色主色调,契合大屏场景
毛玻璃面板 + 发光边框,科技感十足
CSS变量统一管理,主题切换便捷
响应式动画效果,交互流畅自然
八、总结
本项目通过Vue3 + TypeScript + OpenLayers + Three.js + ECharts的技术组合,实现了一个功能完整、可视化丰富的智慧公路GIS综合管理平台。核心经验:
技术选型要匹配场景:2D地图用OpenLayers,3D用Three.js,图表用ECharts,各取所长
性能优化要前置:分包策略、懒加载、按需渲染从一开始就要规划好
主题设计要统一:CSS变量 + 深度覆盖,保证UI一致性
类型安全很重要:TypeScript在大型项目中能有效减少运行时错误
写作声明:本文为原创技术文章,基于实际项目开发经验撰写。如需转载请注明出处。