一、项目背景与核心价值
智慧园区空间管理需要解决区域可视化管控、路径规划、资源定位三大核心问题,本项目基于 Vue3 + Cesium + Turf.js 构建一站式空间管理系统,实现从二维地理计算到三维场景渲染的全链路闭环。相比传统方案,本系统具备以下优势:
- 轻量级前端架构,无后端依赖时可通过本地兜底数据完成演示;
- 基于 Turf.js 实现高精度地理计算(缓冲区、最短路径、最近邻分析);
- 结合 Cesium 完成三维场景可视化,支持交互性的空间操作(坐标拾取、相机聚焦);
- 模块化设计,可快速对接后端接口实现数据持久化。
二、技术栈选型
| 技术 / 库 | 核心用途 |
|---|---|
| Vue3 + Setup 语法 | 组件化开发,响应式状态管理 |
| Cesium | 三维地球场景渲染,坐标拾取与相机控制 |
| Turf.js | 地理空间计算(缓冲区、距离、最短路径) |
| Pinia | 全局状态管理(园区区域、资源、路网数据) |
| Element Plus | 快速构建交互面板(表单、表格、按钮) |
三、核心功能设计与实现
3.1 项目结构
src/
├── components/
│ └── ParkSpaceManagement.vue # 核心业务组件
├── stores/
│ └── park.js # Pinia状态管理(数据与路径算法)
├── router/
│ └── index.js # 路由配置
└── api/
└── client.js # 后端接口封装(可选)
3.2 核心组件:ParkSpaceManagement.vue
该组件是系统的核心交互载体,分为「控制面板」和「Cesium 三维场景」两大部分,通过 Tab 切分「区域管理」「路径规划」「资源定位」三大业务模块。
3.2.1 模板结构(核心片段)
html
<template>
<div class="park-space-management">
<div class="header">
<h2>综合实战:基于Turfjs的智慧园区空间管理系统</h2>
</div>
<div class="container">
<!-- 控制面板:区域管理/路径规划/资源定位 -->
<div class="control-panel">
<el-tabs v-model="tab" type="border-card">
<!-- 区域管理 Tab -->
<el-tab-pane label="区域管理" name="area">
<!-- 新增区域表单 -->
<div class="form-block">
<div class="subtitle">新增区域</div>
<el-input v-model="areaName" placeholder="区域名称" />
<el-input
v-model="areaCoordsStr"
type="textarea"
:rows="4"
placeholder="面坐标数组:[[lon,lat],...],闭合环"
/>
<div class="inline">
<span class="label">颜色</span>
<el-color-picker v-model="areaColor" />
</div>
<div class="inline">
<el-button type="primary" @click="addArea">添加区域</el-button>
<el-button @click="resetAreaInput">清空</el-button>
</div>
</div>
<!-- 区域列表与缓冲区控制 -->
<div class="list-block">
<el-table :data="areas" size="small" style="width: 100%">
<el-table-column prop="id" label="ID" width="90" />
<el-table-column prop="name" label="名称" />
<el-table-column label="颜色" width="110">
<template #default="{ row }">
<el-tag :style="{ background: row.properties?.color || '#2196f3', color: '#fff' }">色块</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" type="primary" @click="focusArea(row)">定位</el-button>
<el-button size="small" type="danger" @click="removeArea(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="subtitle">缓冲区半径 (km)</div>
<el-slider v-model="bufferKm" :min="0" :max="10" :step="0.5" @change="updateBuffer" />
</div>
</el-tab-pane>
<!-- 路径规划 Tab -->
<el-tab-pane label="路径规划" name="route">
<div class="form-block">
<div class="subtitle">起终点</div>
<div class="inline">
<span class="label">起点经纬度</span>
<el-input-number v-model="start[0]" :min="-180" :max="180" :step="0.000001" />
<el-input-number v-model="start[1]" :min="-90" :max="90" :step="0.000001" />
</div>
<div class="inline">
<span class="label">终点经纬度</span>
<el-input-number v-model="end[0]" :min="-180" :max="180" :step="0.000001" />
<el-input-number v-model="end[1]" :min="-90" :max="90" :step="0.000001" />
</div>
<div class="inline">
<el-button type="primary" @click="planRoute">规划路径</el-button>
<el-button @click="clearRoute">清空</el-button>
<el-tag v-if="routeLenKm" type="success">总长 {{ routeLenKm.toFixed(3) }} km</el-tag>
</div>
</div>
</el-tab-pane>
<!-- 资源定位 Tab -->
<el-tab-pane label="资源定位" name="resource">
<div class="form-block">
<div class="subtitle">目标位置</div>
<div class="inline">
<span class="label">经度</span>
<el-input-number v-model="target[0]" :min="-180" :max="180" :step="0.000001" />
<span class="label">纬度</span>
<el-input-number v-model="target[1]" :min="-90" :max="90" :step="0.000001" />
</div>
<div class="inline">
<el-button type="primary" @click="locateNearest">定位最近资源</el-button>
<el-button @click="clearNearest">清空</el-button>
</div>
<!-- 资源列表 -->
<el-table :data="resources" size="small">
<el-table-column prop="id" label="ID" width="90" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="coord" label="坐标">
<template #default="{ row }">
[{{ row.coord[0] }}, {{ row.coord[1] }}]
</template>
</el-table-column>
<el-table-column label="操作" width="260">
<template #default="{ row }">
<el-button size="small" type="primary" @click="flyTo(row.coord)">定位</el-button>
<el-popover placement="bottom" :width="360" trigger="click">
<template #reference>
<el-button size="small">更新坐标</el-button>
</template>
<div class="inline">
<el-input-number v-model="editCoord.lon" :min="-180" :max="180" :step="0.000001" />
<el-input-number v-model="editCoord.lat" :min="-90" :max="90" :step="0.000001" />
<el-button size="small" type="primary" @click="commitResource(row)">保存</el-button>
</div>
</el-popover>
</template>
</el-table-column>
</el-table>
<div v-if="nearestRes" class="result">
最近资源:{{ nearestRes.name }},距离 {{ nearestDistKm.toFixed(3) }} km
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<!-- Cesium 三维场景容器 -->
<div id="cesiumPark" class="cesium-container"></div>
</div>
</div>
</template>
3.2.2 脚本逻辑(核心片段)
javascript
<script setup>
import { onMounted, ref, onBeforeUnmount, computed } from "vue";
import * as Cesium from "cesium";
import "cesium/Build/Cesium/Widgets/widgets.css";
import * as turf from "@turf/turf";
import { useParkStore } from "../stores/park.js";
// 状态初始化
const store = useParkStore();
const tab = ref("area");
const areaName = ref("");
const areaCoordsStr = ref("");
const areaColor = ref("#2196f3");
const bufferKm = ref(1);
const start = ref([116.395, 39.905]);
const end = ref([116.405, 39.915]);
const target = ref([116.4, 39.91]);
const editCoord = ref({ lon: 116.4, lat: 39.91 });
// 响应式数据
const areas = computed(() => store.areas);
const resources = computed(() => store.resources);
const routeLenKm = ref(0);
const nearestRes = ref(null);
const nearestDistKm = ref(0);
// Cesium 核心对象
const viewer = ref(null);
let areaEntities = [];
let bufferEntities = [];
let routeEntity = null;
let resourceEntities = [];
// 初始化 Cesium 场景
function initCesium() {
viewer.value = new Cesium.Viewer("cesiumPark", {
terrainProvider: new Cesium.EllipsoidTerrainProvider(),
animation: false,
timeline: false,
geocoder: false,
homeButton: false,
sceneModePicker: false,
baseLayerPicker: false,
navigationHelpButton: false,
infoBox: false,
selectionIndicator: false,
});
// 隐藏版权信息,开启地形深度测试
viewer.value._cesiumWidget._creditContainer.style.display = "none";
viewer.value.scene.globe.depthTestAgainstTerrain = true;
// 设置初始视角
viewer.value.scene.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(116.405, 39.91, 40000),
});
// 点击场景拾取坐标(快速设置起终点/目标位置)
viewer.value.screenSpaceEventHandler.setInputAction((movement) => {
const pos = viewer.value.scene.pickPosition(movement.position);
if (!pos) return;
const c = Cesium.Cartographic.fromCartesian(pos);
const lon = Cesium.Math.toDegrees(c.longitude);
const lat = Cesium.Math.toDegrees(c.latitude);
// 简化交互:点击直接赋值目标位置(可扩展为起终点切换)
target.value = [Number(lon.toFixed(6)), Number(lat.toFixed(6))];
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
// 渲染园区区域(多边形)
function renderAreas() {
clearEntities(areaEntities);
for (const a of areas.value) {
const coords = a.geometry?.coordinates?.[0] || [];
const hierarchy = coords.map((c) => Cesium.Cartesian3.fromDegrees(c[0], c[1], 0));
const e = viewer.value.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(hierarchy),
material: Cesium.Color.fromCssColorString(a.properties?.color || "#2196f3").withAlpha(0.35),
outline: true,
outlineColor: Cesium.Color.WHITE,
},
});
areaEntities.push(e);
}
}
// 生成并渲染缓冲区
function updateBuffer() {
clearEntities(bufferEntities);
const selected = areas.value[0];
if (!selected) return;
// Turf.js 计算缓冲区
const poly = turf.polygon(selected.geometry.coordinates);
const buf = turf.buffer(poly, bufferKm.value, { units: "kilometers" });
// Cesium 渲染缓冲区
const coords = buf.geometry.coordinates[0];
const hierarchy = coords.map((c) => Cesium.Cartesian3.fromDegrees(c[0], c[1], 0));
const e = viewer.value.entities.add({
polygon: {
hierarchy: new Cesium.PolygonHierarchy(hierarchy),
material: Cesium.Color.fromCssColorString("#26a69a").withAlpha(0.25),
outline: true,
outlineColor: Cesium.Color.fromCssColorString("#26a69a"),
},
});
bufferEntities.push(e);
}
// 路径规划核心逻辑
function planRoute() {
const path = store.planRoute(start.value, end.value);
if (!path.length) {
clearRoute();
return;
}
// 渲染路径
const positions = path.map((c) => Cesium.Cartesian3.fromDegrees(c[0], c[1], 0));
if (routeEntity) viewer.value.entities.remove(routeEntity);
routeEntity = viewer.value.entities.add({
polyline: {
positions,
width: 4,
material: Cesium.Color.CYAN,
},
});
// Turf.js 计算路径长度
let len = 0;
for (let i = 0; i + 1 < path.length; i++) {
len += turf.distance(turf.point(path[i]), turf.point(path[i + 1]), { units: "kilometers" });
}
routeLenKm.value = len;
}
// 最近邻资源定位
function locateNearest() {
let best = null;
let bestD = Infinity;
for (const r of resources.value) {
// Turf.js 计算两点距离
const d = turf.distance(turf.point(r.coord), turf.point(target.value), { units: "kilometers" });
if (d < bestD) {
bestD = d;
best = r;
}
}
nearestRes.value = best;
nearestDistKm.value = bestD === Infinity ? 0 : bestD;
if (best) flyTo(best.coord);
}
// 工具函数:清空实体
function clearEntities(list) {
if (!viewer.value) return;
for (const e of list) viewer.value.entities.remove(e);
list.length = 0;
}
// 生命周期
onMounted(async () => {
initCesium();
await store.init();
renderAreas();
renderResources();
updateBuffer();
});
onBeforeUnmount(() => {
if (viewer.value) viewer.value.destroy();
});
</script>
3.3 状态管理:park.js(Pinia)
该文件封装了数据管理 和核心算法(最短路径 - Dijkstra),是前端地理计算的核心。
javascript
import { defineStore } from "pinia";
import { distance as turfDistance, point as turfPoint } from "@turf/turf";
// 坐标格式化(避免精度问题)
function keyOfCoord(c) {
return `${Number(c[0].toFixed(6))},${Number(c[1].toFixed(6))}`;
}
// 查找最近的路网节点
function nearestNode(nodes, coord) {
let best = null;
let bestD = Infinity;
for (const k of Object.keys(nodes)) {
const c = nodes[k];
const d = turfDistance(turfPoint(c), turfPoint(coord), { units: "kilometers" });
if (d < bestD) {
bestD = d;
best = k;
}
}
return best;
}
// 构建路网图(节点+边)
function buildWalkGraph(walkways) {
const nodes = {};
const edges = {};
const features = walkways?.features || [];
for (const f of features) {
const coords = f?.geometry?.coordinates || [];
for (let i = 0; i < coords.length; i++) {
const a = coords[i];
const ak = keyOfCoord(a);
nodes[ak] = a;
if (i + 1 < coords.length) {
const b = coords[i + 1];
const bk = keyOfCoord(b);
nodes[bk] = b;
// 计算边的权重(距离)
const w = turfDistance(turfPoint(a), turfPoint(b), { units: "kilometers" });
edges[ak] = edges[ak] || {};
edges[bk] = edges[bk] || {};
edges[ak][bk] = Math.min(edges[ak][bk] ?? Infinity, w);
edges[bk][ak] = Math.min(edges[bk][ak] ?? Infinity, w);
}
}
}
return { nodes, edges };
}
// Dijkstra 最短路径算法
function dijkstra(nodes, edges, startK, endK) {
const dist = {};
const prev = {};
const visited = new Set();
// 初始化距离
for (const k of Object.keys(nodes)) dist[k] = Infinity;
dist[startK] = 0;
while (visited.size < Object.keys(nodes).length) {
// 找未访问的最短距离节点
let u = null;
let best = Infinity;
for (const k of Object.keys(nodes)) {
if (!visited.has(k) && dist[k] < best) {
best = dist[k];
u = k;
}
}
if (!u) break;
if (u === endK) break;
visited.add(u);
// 松弛操作
const nbrs = edges[u] || {};
for (const v of Object.keys(nbrs)) {
const alt = dist[u] + nbrs[v];
if (alt < dist[v]) {
dist[v] = alt;
prev[v] = u;
}
}
}
// 回溯路径
const pathKeys = [];
let cur = endK;
if (dist[endK] === Infinity) return [];
while (cur) {
pathKeys.unshift(cur);
cur = prev[cur];
}
return pathKeys.map((k) => nodes[k]);
}
// 定义 Pinia Store
export const useParkStore = defineStore("park", {
state: () => ({
areas: [], // 园区区域
resources: [], // 园区资源(摄像头、无人车等)
walkways: null, // 路网数据
graph: { nodes: {}, edges: {} }, // 路网图
loading: false,
error: null,
}),
actions: {
// 初始化数据(优先接口,兜底本地)
async init() {
this.loading = true;
try {
const [areas, walkways, resources] = await Promise.all([
api.getAreas().catch(() => null),
api.getWalkways().catch(() => null),
api.getResources().catch(() => null),
]);
if (areas) this.areas = areas;
if (walkways) {
this.walkways = walkways;
this.graph = buildWalkGraph(walkways);
}
if (resources) this.resources = resources;
// 兜底数据
if (!areas && !resources && !walkways) {
this.applyDefaults();
}
} finally {
this.loading = false;
}
},
// 兜底默认数据(无后端时演示)
applyDefaults() {
this.areas = [
{
id: "A-001",
name: "研发区",
geometry: {
type: "Polygon",
coordinates: [[[116.39, 39.90], [116.41, 39.90], [116.41, 39.92], [116.39, 39.92], [116.39, 39.90]]],
},
properties: { color: "#1e88e5" },
},
];
this.resources = [
{ id: "R-001", name: "无人车-01", coord: [116.395, 39.905] },
{ id: "R-002", name: "无人车-02", coord: [116.405, 39.915] },
{ id: "R-003", name: "安防-摄像头A", coord: [116.400, 39.910] },
];
// 路网数据
this.walkways = {
type: "FeatureCollection",
features: [/* 省略路网坐标 */],
};
this.graph = buildWalkGraph(this.walkways);
},
// 路径规划入口
planRoute(start, end) {
const g = this.graph;
const sK = nearestNode(g.nodes, start);
const eK = nearestNode(g.nodes, end);
if (!sK || !eK) return [];
const path = dijkstra(g.nodes, g.edges, sK, eK);
return [start, ...path, end]; // 补全起终点
},
// 其他动作:addArea/deleteArea/updateResource 等
async addArea(area) { /* 省略 */ },
async deleteArea(id) { /* 省略 */ },
async updateResource(id, patch) { /* 省略 */ },
},
});
3.4 路由配置
在 router/index.js 中添加系统路由,集成到整体项目:
javascript
import { createRouter, createWebHistory } from "vue-router";
const ParkSpaceManagement = () => import("../components/ParkSpaceManagement.vue");
export const router = createRouter({
history: createWebHistory(),
routes: [
// 其他路由...
{
path: "/park",
name: "ParkSpaceManagement",
component: ParkSpaceManagement,
meta: { title: "综合实战:基于Turfjs的智慧园区空间管理系统" },
},
],
});
router.afterEach((to) => {
if (to?.meta?.title) document.title = to.meta.title;
});
四、核心功能演示
4.1 区域管理
- 输入区域名称、闭合多边形坐标(格式:
[[lon1,lat1],[lon2,lat2],...]),选择颜色后点击「添加区域」; - 区域列表中可点击「定位」聚焦到该区域中心,或「删除」区域;
- 拖动滑块调整缓冲区半径,实时渲染区域的缓冲范围(用于安全 / 管控范围分析)。
4.2 路径规划
- 输入起点、终点经纬度(或点击三维场景快速拾取);
- 点击「规划路径」,系统基于路网数据计算最短路径并渲染,同时显示路径总长;
- 点击「清空」可清除当前路径。
4.3 资源定位
- 输入目标经纬度(或点击场景拾取),点击「定位最近资源」;
- 系统计算并显示最近的资源(如无人车、摄像头)及距离,同时相机聚焦到该资源;
- 资源列表中可「定位」资源、「更新坐标」并保存。

五、扩展与优化方向
- 后端对接 :完善
api/client.js接口封装,实现区域、资源、路网数据的持久化; - 性能优化 :
- 对大规模路网数据做分块加载,避免前端计算压力;
- 缓存 Turf.js 计算结果(如距离、缓冲区),减少重复计算;
- 功能扩展 :
- 支持区域相交 / 包含关系校验;
- 新增资源分类(如安防、巡检、运维),支持筛选;
- 路径规划支持避障(如禁行区域);
- 交互优化 :
- 增加坐标拾取的可视化提示;
- 支持批量导入 / 导出地理数据(GeoJSON 格式);
- 样式优化:完善响应式布局,适配不同屏幕尺寸。
六、项目运行
- 克隆代码仓库:
git clone https://gitee.com/YAY-404/turfjs-vue3-demo.git; - 安装依赖:
npm install; - 配置 Cesium Token:在
.env文件中添加VITE_CESIUM_ION_TOKEN=你的Cesium Token; - 启动项目:
npm run dev; - 访问系统:打开浏览器访问
http://localhost:5173/#/park。