Vue-智慧城市
项目搭建
- 创建项目
npm create vite
- 安装依赖
package.json
:
{
"name": "smartcity_wuhan",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"mock": "json-server -w mock/index.cjs -p 8080"
},
"dependencies": {
"@antv/g2": "^4.2.10",
"@antv/g2plot": "^2.4.31",
"@antv/l7": "^2.15.2",
"@antv/l7-draw": "^3.0.25",
"@antv/l7-maps": "^2.15.2",
"@babel/runtime": "^7.21.5",
"@opd/g2plot-vue": "^3.6.6",
"@turf/turf": "^6.5.0",
"axios": "^1.3.6",
"element-plus": "^2.3.5",
"json-server": "^0.17.3",
"mapbox-gl": "^2.14.1",
"mockjs": "^1.1.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.0.1",
"vite-plugin-vue-devtools": "^7.6.5"
}
}
npm install
- 项目清理
- 清理
App.vue
- 删除
assets
和components
下所有文件 - 在
main.js
中删除引用main.css
的代码
- 清理
项目初始化
搭建mock服务
Mock.js 是一个用于生成模拟数据的 JS库,它的主要作用是:
- 生成模拟数据:可以生成随机的文本、数字、布尔值、日期、颜色、图片等,也可以生成符合特定格式的数据,如邮箱、身份证号等。
- 模拟接口响应:可以拦截 AJAX 请求,并返回模拟的数据,从而无需依赖后端接口即可进行前端开发。
- 简化测试:在单元测试或集成测试中,使用 Mock.js 可以轻松模拟复杂的接口响应,使得测试更加集中和高效。
- 导入geojson数据
mock/Wuhan_Buildings.json
, mock/Wuhan_events.json
, mock/Wuhan_roads.json
- 创建启动文件
mock/index.cjs
// 导入mockjs(加载模拟数据)
const mockjs = require('mockjs')
// 加载数据
// 1. 武汉城市数据
const wuhan_buildings = require('./Wuhan_Buildings.json')
// 2. 武汉道路数据
const wuhan_roads = require('./Wuhan_roads.json')
// 3. 武汉交通事件数据
const wuhan_events = require('./Wuhan_events.json')
// 导出函数
module.exports = () => {
return mockjs.mock({
wuhan_buildings,
wuhan_roads,
wuhan_events,
})
}
- 启动mock服务
npm run mock
使用json-server
启动了一个RestFul
服务, 提供如下接口
Resources
http://localhost:8080/wuhan_buildings
http://localhost:8080/wuhan_roads
http://localhost:8080/wuhan_events
Home
http://localhost:8080
axios封装
启动mock服务后, 我们通过axios
发送请求mock数据
对axios进行再次封装
- 创建
src/api/request.js
// 导入axios
import axios from 'axios'
// 创建axios实例
const instance = axios.create({
baseURL: 'http://localhost:8080',
timeout: 5000,
})
// 配置响应拦截器
instance.interceptors.response.use(
(res) => {
if (res.status === 200) {
return res.data
} else {
console.error('请求失败')
return Promise.reject('请求失败')
}
},
(err) => {
return Promise.reject(err)
}
)
// 导出实例
export default instance
- 编写接口请求
smart_city.js
// 导入request实例
import request from './request'
// 编写接口
export const getCityBuildings = () => {
return request({
url: 'wuhan_buildings',
method: 'GET',
})
}
export const getRoads = () => {
return request({
url: 'wuhan_roads',
method: 'GET',
})
}
export const getEvents = () => {
return request({
url: 'wuhan_events',
method: 'GET',
})
}
初始化样式
- 创建样式
src/assets/styles/reset.css
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
border: none;
}
table {
border-collapse: collapse; /* 合并边框 */
}
html,
body {
overflow: hidden; /* 隐藏滚动条 */
height: 100%;
}
- 导入样式
main.js
import { createApp } from 'vue'
import App from './App.vue'
// 导入初始化样式
import './assets/styles/reset.css'
createApp(App).mount('#app')
初始化UI
在main.js
中引用ElementPlus
// 配置ElementUI
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
createApp(App).use(ElementPlus).mount('#app')
地图初始化
- 创建插件文件
使用Mapbox
+antv-l7
开发.
首先, 需要创建map
对象和scene
对象, 这里可以考虑做一个Vue插件
创建src/plugins/mapbox.js
- 配置token
在项目根目录, 创建.env
配置文件
VITE_TOKEN = 'pk.eyJ1IjoiY2hlbmdjaGFvY2hhbyIsImEiOiJjbGU1aDZ2eWUwMXp4M29udmFnNnNyZjBhIn0.2Kd0ZX06ReEdBnZ9XU4XUA'
-
编写插件
- 导入相关库
- 导出插件对象
- 创建地图容器, 挂载到
body
下 - 创建
map
对象, 并开启雾化 - 创建
scene
对象 - 使用
provide
, 添加到app
应用实例上, 全局共享
- 创建地图容器, 挂载到
// 导入相关库
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { Scene } from '@antv/l7'
import { Mapbox } from '@antv/l7-maps'
// vue的插件: 导出一个对象
// 1. 在该对象必须包含一个install方法
// 2. 当执行app.use(插件对象)时, 会自动执行
export default {
install(app) {
// 创建scene和map对象
// 1. 实例化mapbox中的map对象
const token = import.meta.env.VITE_TOKEN
mapboxgl.accessToken = token
// 创建地图容器 <div id="map" style="width: 100%; height:100%">
const container = document.createElement('div')
container.id = 'map'
container.setAttribute('style', 'width: 100%; height: 100%')
document.body.appendChild(container)
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v11',
center: [114.3, 30.5],
zoom: 1,
projection: 'globe',
})
map.on('style.load', () => {
map.setFog({})
})
const scene = new Scene({
id: 'map',
map: new Mapbox({
mapInstance: map,
}),
logoVisible: false,
})
// 在app中通过provide提供scene和map对象
app.provide('$scene_map', { scene, map })
},
}
- 注册插件
// 注册插件
import Mapbox from './plugins/mapbox.js'
createApp(App).use(ElementPlus).use(Mapbox).mount('#app')
城市场景
基本实现
- 创建组件
src/components/SmartCity/index.vue
<template></template>
<script setup>
// 导入vue
import { inject, onMounted } from 'vue'
onMounted(() => {
// 通过inject, 获取地图场景对象
const { scene, map } = inject('$scene_map')
console.log(scene)
})
</script>
<style></style>
- 在
App.vue
中引用组件
<template>
<!-- 引用组件 -->
<SmartCity />
</template>
<script setup>
// 导入组件
import SmartCity from './components/SmartCity/index.vue'
</script>
<style></style>
渲染城市建筑
使用antv-l7
渲染城市建筑图层, 封装一个hooks
将这个功能集成
- 创建文件
SmartCity/hooks/useBuildings.js
// 导入smart_city接口函数
import { getCityBuildings } from '@/api/smart_city.js'
import { CityBuildingLayer } from '@antv/l7'
// 导出hooks函数
export default async () => {
// 获取城市建筑数据
const buildings_data = await getCityBuildings()
// console.log(buildings_data)
// 创建城市建筑图层
const building_layer = new CityBuildingLayer({
name: '武汉市',
})
// 配置图层
building_layer
.source(buildings_data) // 加载数据源
.size('Elevation', (h) => h) // 使用Elevation来设置高度
.animate(true) // 开启动画
.active({
color: '#0ff',
mix: 0.5,
}) // 设置鼠标悬停的高度效果
.style({
opacity: 0.7,
baseColor: 'rgb(16, 16, 16)',
windowColor: 'rgb(30, 60, 89)',
brightColor: 'rgb(255, 176, 38)',
sweep: {
enable: true,
sweepRadius: 2,
sweepColor: '#1990FF',
sweepSpeed: 0.3,
sweepCenter: [114.3, 30.5],
},
}) // 设置楼房样式
.filter('Elevation', (h) => h > 40)
return building_layer
}
- 在
SmartCity/index.vue
中使用hooks
<template></template>
<script setup>
import { inject, onMounted } from 'vue'
import useBuildings from './hooks/useBuildings'
onMounted(async () => {
// 通过inject注入$scene_map对象, 并解构
const { scene } = inject('$scene_map')
// 创建城市建筑图层
const building_layer = await useBuildings()
// 在场景中添加城市建筑图层
scene.addLayer(building_layer)
})
</script>
<style></style>
渲染城市道路
- 创建文件
SmartCity/hooks/useRoads.js
// 导入smart_city中的道路接口
import { getRoads } from '@/api/smart_city.js'
// 从L7中导入线图层
import { LineLayer } from '@antv/l7'
// 导出hooks函数
export default async () => {
// 获取道路数据
const roads_data = await getRoads()
// 创建线图层, 模拟道路
const roads_layer = new LineLayer({
name: '武汉市道路',
zIndex: 0,
depth: true,
})
// 配置图层
roads_layer
.source(roads_data) // 加载数据源
.size(1) // 设置大小 (线宽)
.shape('line') // 设置形状
.color('#1990FF') // 设置颜色
.animate({
trailLength: 2, // 流线长度
duration: 2, // 持续时间
interval: 1, // 间隔周期
}) // 设置动画
.filter('coordinates', (evt) => evt.length > 20)
// 返回图层对象
return roads_layer
}
- 在
SmartCity/index.vue
中使用hooks
<template></template>
<script setup>
import { inject, onMounted } from 'vue'
import useBuildings from './hooks/useBuildings'
import useRoads from './hooks/useRoads'
onMounted(async () => {
// 通过inject注入$scene_map对象, 并解构
const { scene } = inject('$scene_map')
// 创建城市建筑图层
const building_layer = await useBuildings()
// 在场景中添加城市建筑图层
scene.addLayer(building_layer)
// 创建城市道路图层
const roads_layer = await useRoads()
scene.addLayer(roads_layer)
})
</script>
<style></style>
地图控件
创建组件
创建src/components/MapControls.vue
编写组件
<template></template>
<script setup>
import { inject, onMounted } from 'vue'
import { Logo, Zoom, Fullscreen, MouseLocation, MapTheme } from '@antv/l7'
onMounted(() => {
// 通过inject注入scene, 并解构
const { scene } = inject('$scene_map')
// 注册控件
const logo = new Logo({
img: 'https://img.gejiba.com/images/dfdb6db1623eb881e724f58d9a366af8.png',
href: 'http://www.x-zd.com',
})
scene.addControl(logo)
// 鼠标位置控件
const mouseLocation = new MouseLocation({
transform: (position) => position,
position: 'bottomright',
})
scene.addControl(mouseLocation)
// 添加放大缩小控件
const zoom = new Zoom({
zoomInTitle: '放大',
zoomOutTitle: '缩小',
position: 'bottomright',
})
scene.addControl(zoom)
// 添加全屏控件
const fullscreen = new Fullscreen({
btnText: '全屏',
exitBtnText: '退出全屏',
})
scene.addControl(fullscreen)
// 主题切控件
const mapTheme = new MapTheme()
scene.addControl(mapTheme)
})
</script>
使用组件
在App.vue
导入并使用
<template>
<SmartCity />
<MapControls />
</template>
<script setup>
// 导入组件
import SmartCity from './components/SmartCity/index.vue'
import MapControls from './components/MapControls.vue'
</script>
<style></style>
可视化面板
头部组件
- 创建组件
src/components/Header.vue
<template>
<header class="header">智慧城市-武汉</header>
</template>
<script setup></script>
<style>
.header {
position: fixed;
top: 0;
left: 0;
/* 相对于viewport(视口)的宽度 */
width: 100%;
height: 50px;
background: url('../assets/images/header.png') center no-repeat;
background-size: cover;
color: #fff;
text-align: center;
line-height: 50px;
z-index: 1;
}
</style>
- 引用组件
<template>
<Header />
<SmartCity />
<MapControls />
</template>
<script setup>
// 导入组件
import Header from './components/Header.vue'
import SmartCity from './components/SmartCity/index.vue'
import MapControls from './components/MapControls.vue'
</script>
<style></style>
图表组件
使用antv
的G2Plot来实现图表
目录结构: src/components/G2Charts
-
hooks目录
-
- useLeftBottom.js: 左下图表
- useLeftTop.js: 左上图表
- useRightTop.js: 右上图表
-
index.vue
- 创建组件
index.vue
<template>
<div>
<div class="left-container">
<div class="g2-chart">
<div class="title">出行人口统计</div>
<!-- 柱状图 -->
</div>
<div class="g2-chart">
<div class="title">实时公交在线表</div>
<!-- 玫瑰图 -->
</div>
</div>
<div class="right-conatiner">
<div class="g2-chart">
<div class="title">武汉市人口统计</div>
<!-- 饼状图 -->
</div>
<div class="g2-chart">
<div class="title">武汉市三甲医院</div>
<div class="list">
<div>
<h4>医院 <span>30家</span></h4>
<img src="../../assets/icons/hospital.png" />
</div>
<div>
<h4>门诊部 <span>300个</span></h4>
<img src="../../assets/icons/building.png" />
</div>
<div>
<h4>病床 <span>3000张</span></h4>
<img src="../../assets/icons/bed.png" />
</div>
</div>
</div>
<div class="g2-chart">
<div class="title">高校学生统计</div>
<div class="list">
<div>
<h4>高校 <span>130所</span></h4>
<img src="../../assets/icons/school.png" alt="" />
</div>
<div>
<h4>在校大学生<span>100万</span></h4>
<img src="../../assets/icons/student.png" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup></script>
<style>
.left-container {
position: fixed;
top: 50px;
left: 20px;
z-index: 1;
}
.right-container {
position: fixed;
top: 50px;
right: 20px;
z-index: 1;
}
.g2-chart {
position: relative;
margin: 20px 0;
padding: 20px;
background: linear-gradient(to bottom, #292e4968, #5369766a);
border-radius: 20px;
}
.g2-chart::before {
display: block;
content: '';
position: absolute;
top: -5px;
left: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
transform: rotate(180deg);
}
.g2-chart::after {
display: block;
content: '';
position: absolute;
bottom: -5px;
right: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
}
.g2-chart .title {
padding-left: 64px;
margin-bottom: 20px;
color: #fff;
line-height: 46px;
background: url('../../assets/images/chart-item.png') no-repeat;
}
.g2-chart .list {
display: flex;
justify-content: space-evenly;
font-size: 12px;
color: #fff;
text-align: center;
}
.g2-chart .list img {
width: 40px;
}
</style>
- 使用组件
<template>
<Header />
<SmartCity />
<MapControls />
<G2Charts />
</template>
<script setup>
// 导入组件
import Header from './components/Header.vue'
import SmartCity from './components/SmartCity/index.vue'
import MapControls from './components/MapControls.vue'
import G2Charts from './components/G2Charts/index.vue'
</script>
<style></style>
- 左上图表-柱状图
import { ref } from 'vue'
export const useLeftTop = () => {
const data = ref([
{ type: '汉阳区', value: 10000 },
{ type: '武昌区', value: 20000 },
{ type: '洪山区', value: 50000 },
{ type: '江夏区', value: 30000 },
{ type: '江岸区', value: 35000 },
])
// 模拟动态增长
setInterval(() => {
let res = data.value.map((item) => {
let { value } = item
value += Math.floor(Math.random() * 100)
return { ...item, value: value }
})
data.value = res
}, 1200)
const green = '#00B96B'
const yellow = '#fd7e14'
const red = '#dc3545'
const config = {
xField: 'type',
yField: 'value',
seriesField: 'value',
label: {
// 可手动配置 label 数据标签位置
position: 'top', // 'top', 'bottom', 'middle',
// 配置样式
style: {
fill: '#FFFFFF',
opacity: 0.6,
},
},
color: ({ value }) => {
if (value > 40000) {
return red
} else if (value > 20000 && value < 40000) {
return yellow
} else {
return green
}
},
legend: false,
height: 200,
}
return {
config,
data,
}
}
- 左下图表-玫瑰图
export const useLeftBottom = () => {
const data = [
{ type: '汉阳区', value: 27 },
{ type: '武昌区', value: 25 },
{ type: '硚口区', value: 18 },
{ type: '江夏区', value: 15 },
{ type: '洪山区', value: 10 },
{ type: '其他', value: 10 },
]
const config = {
appendPadding: 10,
xField: 'type',
yField: 'value',
seriesField: 'type',
radius: 0.9,
label: {
offset: -15,
},
interactions: [{ type: 'element-active' }],
height: 150,
}
return {
data,
config,
}
}
- 右上图表-饼状图
export const useRightTop = () => {
const data = [
{ type: '武昌', value: 27 },
{ type: '汉口', value: 25 },
{ type: '汉阳', value: 18 },
{ type: '其他', value: 18 },
]
const config = {
appendPadding: 10,
angleField: 'value',
colorField: 'type',
radius: 0.9,
label: {
type: 'spider',
labelHeight: 28,
content: '{name}\n{percentage}',
style: {
/* 设置标注的颜色 */
fill: '#fff',
stroke: 'black',
shadowColor: '#652e80',
shadowBlur: 20,
cursor: 'pointer',
},
},
interactions: [{ type: 'element-active' }],
data,
height: 150,
legend: {
position: 'top',
itemName: {
style: {
fill: '#fff',
},
},
},
}
return {
config,
}
}
- 在组件中使用hooks函数
<template>
<div>
<div class="left-container">
<div class="g2-chart">
<div class="title">出行人口统计</div>
<!-- 柱状图 -->
<ColumnChart v-bind="lt_config" :data="lt_data" />
</div>
<div class="g2-chart">
<div class="title">实时公交在线表</div>
<!-- 玫瑰图 -->
<RoseChart v-bind="lb_config" :data="lb_data" />
</div>
</div>
<div class="right-container">
<div class="g2-chart">
<div class="title">武汉市人口统计</div>
<!-- 饼状图 -->
<PieChart v-bind="rt_config" />
</div>
<div class="g2-chart">
<div class="title">武汉市三甲医院</div>
<div class="list">
<div>
<h4>医院 <span>30家</span></h4>
<img src="../../assets/icons/hospital.png" />
</div>
<div>
<h4>门诊部 <span>300个</span></h4>
<img src="../../assets/icons/building.png" />
</div>
<div>
<h4>病床 <span>3000张</span></h4>
<img src="../../assets/icons/bed.png" />
</div>
</div>
</div>
<div class="g2-chart">
<div class="title">高校学生统计</div>
<div class="list">
<div>
<h4>高校 <span>130所</span></h4>
<img src="../../assets/icons/school.png" alt="" />
</div>
<div>
<h4>在校大学生<span>100万</span></h4>
<img src="../../assets/icons/student.png" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ColumnChart, RoseChart, PieChart } from '@opd/g2plot-vue'
// 从自定义hooks中获取数据
import { useLeftTop } from './hooks/useLeftTop'
import { useLeftBottom } from './hooks/useLeftBottom'
import { useRightTop } from './hooks/useRightTop'
const { config: lt_config, data: lt_data } = useLeftTop()
const { config: lb_config, data: lb_data } = useLeftBottom()
const { config: rt_config } = useRightTop()
</script>
<style>
.left-container {
position: fixed;
top: 50px;
left: 20px;
z-index: 1;
}
.right-container {
position: fixed;
top: 50px;
right: 20px;
z-index: 1;
}
.g2-chart {
position: relative;
margin: 20px 0;
padding: 20px;
background: linear-gradient(to bottom, #292e4968, #5369766a);
border-radius: 20px;
}
.g2-chart::before {
display: block;
content: '';
position: absolute;
top: -5px;
left: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
transform: rotate(180deg);
}
.g2-chart::after {
display: block;
content: '';
position: absolute;
bottom: -5px;
right: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
}
.g2-chart .title {
padding-left: 64px;
margin-bottom: 20px;
color: #fff;
line-height: 46px;
background: url('../../assets/images/chart-item.png') no-repeat;
}
.g2-chart .list {
display: flex;
justify-content: space-evenly;
font-size: 12px;
color: #fff;
text-align: center;
}
.g2-chart .list img {
width: 40px;
}
</style>
- 样式优化
<template>
<div class="g2-container">
<div class="left-container">
<div class="g2-chart">
<div class="title">出行人口统计</div>
<!-- 柱状图 -->
<ColumnChart v-bind="lt_config" :data="lt_data" />
</div>
<div class="g2-chart">
<div class="title">实时公交在线表</div>
<!-- 玫瑰图 -->
<RoseChart v-bind="lb_config" :data="lb_data" />
</div>
</div>
<div class="right-container">
<div class="g2-chart">
<div class="title">武汉市人口统计</div>
<!-- 饼状图 -->
<PieChart v-bind="rt_config" />
</div>
<div class="g2-chart static">
<div class="title">武汉市三甲医院</div>
<div class="list">
<div>
<h4>医院 <span>30家</span></h4>
<img src="../../assets/icons/hospital.png" />
</div>
<div>
<h4>门诊部 <span>300个</span></h4>
<img src="../../assets/icons/building.png" />
</div>
<div>
<h4>病床 <span>3000张</span></h4>
<img src="../../assets/icons/bed.png" />
</div>
</div>
</div>
<div class="g2-chart static">
<div class="title">高校学生统计</div>
<div class="list">
<div>
<h4>高校 <span>130所</span></h4>
<img src="../../assets/icons/school.png" alt="" />
</div>
<div>
<h4>在校大学生<span>100万</span></h4>
<img src="../../assets/icons/student.png" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ColumnChart, RoseChart, PieChart } from '@opd/g2plot-vue'
// 从自定义hooks中获取数据
import { useLeftTop } from './hooks/useLeftTop'
import { useLeftBottom } from './hooks/useLeftBottom'
import { useRightTop } from './hooks/useRightTop'
const { config: lt_config, data: lt_data } = useLeftTop()
const { config: lb_config, data: lb_data } = useLeftBottom()
const { config: rt_config } = useRightTop()
</script>
<style>
.left-container {
position: fixed;
top: 50px;
left: 20px;
z-index: 1;
}
.right-container {
position: fixed;
top: 50px;
right: 20px;
z-index: 1;
}
.g2-chart {
position: relative;
margin: 20px 0;
padding: 20px;
background: linear-gradient(to bottom, #292e4968, #5369766a);
border-radius: 20px;
}
.g2-chart::before {
display: block;
content: '';
position: absolute;
top: -5px;
left: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
transform: rotate(180deg);
}
.g2-chart::after {
display: block;
content: '';
position: absolute;
bottom: -5px;
right: -2px;
width: 111px;
height: 35px;
background-image: url('../../assets/images/border.png');
}
.g2-chart .title {
padding-left: 64px;
margin-bottom: 20px;
color: #fff;
line-height: 46px;
background: url('../../assets/images/chart-item.png') no-repeat;
}
.g2-chart .list {
display: flex;
justify-content: space-evenly;
font-size: 12px;
color: #fff;
text-align: center;
}
.g2-chart .list img {
width: 40px;
}
/* 为了调用右边的宽高 */
.right-container .g2-chart {
min-width: 300px;
}
.g2-chart.static {
padding: 10px;
height: 120px;
box-sizing: border-box;
}
.g2-chart.static .title {
transform: scale(0.8);
margin-bottom: 0;
}
@media screen and (max-width: 800px) {
.g2-container {
display: none;
}
}
</style>
底部组件
- 创建组件
src/components/Footer/index.vue
<template>
<footer class="footer"></footer>
</template>
<script setup></script>
<style scoped>
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80px;
z-index: 9;
background: url('../../assets/images/footer.png') center no-repeat;
background-size: cover;
}
</style>
- 引用组件
<template>
<Header />
<SmartCity />
<MapControls />
<G2Charts />
<Footer />
</template>
<script setup>
// 导入组件
import Header from './components/Header.vue'
import SmartCity from './components/SmartCity/index.vue'
import MapControls from './components/MapControls.vue'
import G2Charts from './components/G2Charts/index.vue'
import Footer from './components/Footer/index.vue'
</script>
<style></style>
功能模块
地球自转
制作一个按钮, 当点击按钮时, 启动/停止旋转
- 底部组件结构样式
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>停止自转</p>
</div>
</div>
</footer>
</template>
<script setup></script>
<style scoped>
/* 引入iconfont字体图标 */
@import 'https://at.alicdn.com/t/c/font_4072822_j5r3vfaxh8h.css';
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80px;
z-index: 9;
background: url('../../assets/images/footer.png') center no-repeat;
background-size: cover;
}
.btn-groups {
display: flex;
justify-content: center;
font-size: 12px;
color: #fff;
}
.btn-groups .item {
margin-left: 20px;
text-align: center;
}
.btn-groups button {
margin-bottom: 4px;
width: 40px;
height: 40px;
border: none;
outline: none;
color: #fff;
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 0.377),
rgba(0, 128, 255, 0.281)
);
border-radius: 50%;
box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.3);
}
.btn-groups button:hover {
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 1),
rgba(0, 128, 255, 0.281)
);
cursor: pointer;
}
</style>
- 逻辑
Footer/hooks/useRotation.js
import { computed, inject, ref } from 'vue'
export default () => {
const moving = ref(true)
const { map } = inject('$scene_map')
// 自转动画
function rotation() {
const zoom = map.getZoom()
if (zoom < 5) {
let center = map.getCenter()
center.lng += 10
map.easeTo({
center,
duration: 1000,
easing: (n) => n,
})
}
}
rotation()
map.on('moveend', () => {
// 只有当状态为真时, 才执行自转
moving.value && rotation()
})
// 定义控制 自转或者停止的方法
function handleRotation() {
moving.value = !moving.value
if (!moving.value) {
map.stop()
} else {
rotation()
}
}
// 定义计算属性
const mark = computed(() => {
return moving.value ? '停止自转' : '开启自转'
})
return {
mark,
handleRotation,
}
}
- 使用hooks
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn" @click="handleRotation">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>{{ mark }}</p>
</div>
</div>
</footer>
</template>
<script setup>
import useRotation from './hooks/useRotation'
const { mark, handleRotation } = useRotation()
</script>
<style scoped>
/* 引入iconfont字体图标 */
@import 'https://at.alicdn.com/t/c/font_4072822_j5r3vfaxh8h.css';
.footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80px;
z-index: 9;
background: url('../../assets/images/footer.png') center no-repeat;
background-size: cover;
}
.btn-groups {
display: flex;
justify-content: center;
font-size: 12px;
color: #fff;
}
.btn-groups .item {
margin-left: 20px;
text-align: center;
}
.btn-groups button {
margin-bottom: 4px;
width: 40px;
height: 40px;
border: none;
outline: none;
color: #fff;
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 0.377),
rgba(0, 128, 255, 0.281)
);
border-radius: 50%;
box-shadow: 0 0 5px 3px rgba(0, 0, 0, 0.3);
}
.btn-groups button:hover {
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 1),
rgba(0, 128, 255, 0.281)
);
cursor: pointer;
}
</style>
控制中心
index.vue
结构样式
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn" @click="handleRotation">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>{{ mark }}</p>
</div>
<div class="item">
<button class="toggle-btn" @click="toggleCharts">
<i class="iconfont icon-supervision-full"></i>
</button>
<p>控制中心</p>
</div>
</div>
</footer>
</template>
- 逻辑
在index.vue
中触发自定义事件, 将数据传递给父组件
<script setup>
import useRotation from './hooks/useRotation'
const { mark, handleRotation } = useRotation()
let isShow = true
const emits = defineEmits(['toggleCharts'])
function toggleCharts() {
isShow = !isShow
emits('toggleCharts', isShow)
}
</script>
在App.vue
中监听自定义事件, 接收数组, 并控制显示/隐藏
<template>
<!-- 引用组件 -->
<Header />
<SmartCity />
<MapControls />
<G2Charts v-show="showCharts" />
<Footer @toggleCharts="handleCharts"> </Footer>
</template>
<script setup>
// 导入组件
import Header from './components/Header.vue'
import SmartCity from './components/SmartCity/index.vue'
import MapControls from './components/MapControls.vue'
import G2Charts from './components/G2Charts/index.vue'
import Footer from './components/Footer/index.vue'
import { ref } from 'vue'
const showCharts = ref(true)
function handleCharts(value) {
showCharts.value = value
}
</script>
<style></style>
地图复位
index.vue
结构样式
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn" @click="handleRotation">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>{{ mark }}</p>
</div>
<div class="item">
<button class="toggle-btn" @click="toggleCharts">
<i class="iconfont icon-supervision-full"></i>
</button>
<p>控制中心</p>
</div>
<div class="item">
<button class="toggle-btn" @click="flyTo">
<i class="iconfont icon-icon-test"></i>
</button>
<p>{{ flyMsg }}</p>
</div>
</div>
</footer>
</template>
- 逻辑
useFly.js
import { computed, inject, ref } from 'vue'
export default () => {
const { map } = inject('$scene_map')
const isCityView = ref(false)
// 定义计算属性
const flyMsg = computed(() => {
return isCityView.value ? '地球视角' : '城市视角'
})
// 定义复位函数
function flyTo() {
isCityView.value = !isCityView.value
if (isCityView.value) {
map.flyTo({
center: [114.3, 30.5],
zoom: 14,
pitch: 70,
})
} else {
map.flyTo({
center: [114.3, 30.5],
zoom: 1,
pitch: 0,
})
}
}
return {
flyMsg,
flyTo,
}
}
- 使用hooks
<script setup>
import useRotation from './hooks/useRotation'
import useFly from './hooks/useFly'
const { mark, handleRotation } = useRotation()
const { flyTo, flyMsg } = useFly()
let isShow = true
const emits = defineEmits(['toggleCharts'])
function toggleCharts() {
isShow = !isShow
emits('toggleCharts', isShow)
}
</script>
事故查询
查询功能分为如下几个步骤
- 数据准备,通过mockjs, 模拟事故点的数据
- 拉框查询,使用L7的绘制工具,在地图上进行图形绘制
- 结果处理,使用turf工具库,判断事故数据点是否在框选范围内
- 详细信息处理,点击结果面板,展示该条数据的详细信息
- 数据准备
使用L7的geoJSON可视化编辑工具获取数据
写一个函数, 生成带事故信息的GeoJSON
function generatePhoneNumber() {
let prefix = [130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 145, 147, 150, 151, 152, 153, 155, 156, 157, 158, 159, 186, 187, 188];
let prefixIndex = Math.floor(Math.random() * prefix.length);
let phone = prefix[prefixIndex].toString();
for (let i = 0; i < 8; i++) {
phone += Math.floor(Math.random() * 10).toString();
}
return phone;
}
// 生成mock数据
const generateData = async () => {
const { data } = await axios('http://localhost:8080/wuhan_events')
let features = data.features
features = features.map((item, index) => {
let randomNum = (Math.random().toFixed(6) * 1000000).toString()
const car_num = `鄂A${randomNum}`
const phone = generatePhoneNumber()
const id = (10000 + index).toString()
const event_num = `SJ${100000 + index}`
const area = '区域1'
const propertiesInfo = {
name: '碰撞',
level: 2,
car_num, phone, id, event_num, area
}
return {
...item,
properties: propertiesInfo
}
})
const resData = {
type: 'FeatureCollection',
features: features
}
console.log(JSON.stringify(resData));
}
- 拉框查询
绘制组件Footer/DrawTools.vue
<template>
<el-popover
placement="top"
trigger="click"
popper-style="background-color:#53697670;color:#fff"
:width="100"
>
<template #reference>
<slot></slot>
</template>
<div class="popover-w">
<i v-for="item in tools" :class="computedClass(item)"></i>
</div>
</el-popover>
</template>
<script setup>
import { computed, ref } from 'vue'
// 定义tools
const tools = ref([
'drawPolygonTool',
'drawRectTool',
'drawCircleTool',
'delete',
])
// 定义计算属性
const computedClass = computed(() => {
return (item) => {
const res = {
iconfont: true,
'query-item': true,
}
res[`icon-${item}`] = true
return res
}
})
</script>
<style scoped>
.el-button + .el-button {
margin-left: 8px;
}
.popover-w {
display: flex;
align-items: center;
justify-content: space-around;
}
.query-item:hover {
cursor: pointer;
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 0.6),
rgba(0, 128, 255, 0.281)
);
}
</style>
在index.vue
中导入并引用
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn" @click="handleRotation">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>{{ mark }}</p>
</div>
<div class="item">
<button class="toggle-btn" @click="toggleCharts">
<i class="iconfont icon-supervision-full"></i>
</button>
<p>控制中心</p>
</div>
<div class="item">
<button class="toggle-btn" @click="flyTo">
<i class="iconfont icon-icon-test"></i>
</button>
<p>{{ flyMsg }}</p>
</div>
<div class="item">
<DrawTools>
<button class="toggle-btn">
<i class="iconfont icon-paint"></i>
</button>
</DrawTools>
<p>事故查询</p>
</div>
</div>
</footer>
</template>
<script setup>
import DrawTools from './DrawTools.vue'
import useRotation from './hooks/useRotation'
import useFly from './hooks/useFly'
const { mark, handleRotation } = useRotation()
const { flyMsg, flyTo } = useFly()
// 定义状态
let isShow = true
const emits = defineEmits(['toggleCharts'])
function toggleCharts() {
isShow = !isShow
emits('toggleCharts', isShow)
}
</script>
实现拉框绘制
<template>
<el-popover
placement="top"
trigger="click"
popper-style="background-color:#53697670;color:#fff"
:width="100"
>
<template #reference>
<slot></slot>
</template>
<div class="popover-w">
<i
v-for="item in tools"
:class="computedClass(item)"
@click="queryEvents(item)"
></i>
</div>
</el-popover>
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import { DrawEvent, DrawPolygon, DrawCircle, DrawRect } from '@antv/l7-draw'
// 定义tools
const tools = ref([
'drawPolygonTool',
'drawRectTool',
'drawCircleTool',
'delete',
])
// 定义计算属性
const computedClass = computed(() => {
return (item) => {
const res = {
iconfont: true,
'query-item': true,
}
res[`icon-${item}`] = true
return res
}
})
// 定义查询绘制函数
let draw = null
const { scene } = inject('$scene_map')
function queryEvents(type) {
if (draw) {
draw.disable()
draw.clear()
}
switch (type) {
case 'drawPolygonTool':
draw = new DrawPolygon(scene, {})
break
case 'drawRectTool':
draw = new DrawRect(scene, {})
break
case 'drawCircleTool':
draw = new DrawCircle(scene, {})
break
default:
draw = null
return
}
draw.enable()
draw.on(DrawEvent.Change, (allFeatures) => {
// 只保留最后绘制的图形
allFeatures.forEach((item, index) => {
if (index !== allFeatures.length - 1) {
draw.removeFeature(item)
}
})
})
}
</script>
- 结果处理
通过拉框可以得到一个多边形
, 接下来需要使用turf
库来计算落在多边形
中的点
<template>
<el-popover
placement="top"
trigger="click"
popper-style="background-color:#53697670;color:#fff"
:width="100"
>
<template #reference>
<slot></slot>
</template>
<div class="popover-w">
<i
v-for="item in tools"
:class="computedClass(item)"
@click="queryEvents(item)"
></i>
</div>
</el-popover>
</template>
<script setup>
import { computed, inject, onMounted, ref } from 'vue'
import { DrawEvent, DrawPolygon, DrawCircle, DrawRect } from '@antv/l7-draw'
import { point, polygon, booleanPointInPolygon } from '@turf/turf'
import { getEvents } from '@/api/smart_city.js'
// 定义普通数据
let eventsData = null
// 定义tools
const tools = ref([
'drawPolygonTool',
'drawRectTool',
'drawCircleTool',
'delete',
])
// 定义计算属性
const computedClass = computed(() => {
return (item) => {
const res = {
iconfont: true,
'query-item': true,
}
res[`icon-${item}`] = true
return res
}
})
// 在onMounted中获取事故数据
onMounted(async () => {
const res = await getEvents()
eventsData = res.features
})
// 定义查询绘制函数
let draw = null
const { scene } = inject('$scene_map')
function queryEvents(type) {
if (draw) {
draw.disable()
draw.clear()
}
switch (type) {
case 'drawPolygonTool':
draw = new DrawPolygon(scene, {})
break
case 'drawRectTool':
draw = new DrawRect(scene, {})
break
case 'drawCircleTool':
draw = new DrawCircle(scene, {})
break
default:
draw = null
return
}
draw.enable()
draw.on(DrawEvent.Change, (allFeatures) => {
// 找出最后绘制的图形(多边形)
const activeFeature = allFeatures[allFeatures.length - 1]
// 只保留最后绘制的图形
allFeatures.forEach((item, index) => {
if (index !== allFeatures.length - 1) {
draw.removeFeature(item)
}
})
if (eventsData.length && activeFeature) {
const {
geometry: { coordinates: coordinatesActive },
} = activeFeature
// 使用turf判断哪些事故点落在绘制的拉框中
const resData = eventsData.filter((item) => {
const { geometry } = item
if (geometry.type === 'Point') {
const pt = point(geometry.coordinates)
const poly = polygon(coordinatesActive)
const isInArea = booleanPointInPolygon(pt, poly)
return isInArea
}
})
console.log(resData)
}
})
}
</script>
- 数据渲染
将查询到的结果渲染到表格中
创建Footer/DisplayCard.vue
<template>
<div class="display-card">
<el-table :data="computedData" size="small" :max-height="400">
<el-table-column prop="event_num" label="事件编号"></el-table-column>
<el-table-column prop="name" label="类型"></el-table-column>
<el-table-column label="操作" fixed="right">
<el-button size="small" type="primary" link>详情</el-button>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
tableData: {
type: Array,
},
})
const computedData = computed(() => {
return props.tableData.map((row) => {
const {
properties: { event_num, name },
} = row
return {
event_num,
name,
}
})
})
</script>
<style scoped>
.display-card {
position: fixed;
bottom: 80px;
background: #53697670;
border-radius: 4px;
box-shadow: 0 0 5px 3px #333;
}
.eleCeil {
background: transparent;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-table) {
background-color: transparent;
}
:deep(.el-table tr) {
background-color: transparent;
color: #fff;
cursor: pointer;
}
:deep(.el-table tr:hover) {
background-color: rgba(0, 0, 0, 0.5);
}
:deep(.el-table--enable-row-transition .el-table__body td.el-table__cell) {
background-color: transparent;
}
:deep(.el-table th.el-table__cell) {
background-color: transparent;
}
:deep(.el-table td.el-table__cell) {
border-bottom: none;
}
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
:deep(.el-table.is-scrolling-right th.el-table-fixed-column--right) {
background-color: transparent;
}
</style>
在DrawTools.vue
中引用组件
<template>
<el-popover
placement="top"
trigger="click"
popper-style="background-color:#53697670;color:#fff"
:width="100"
>
<template #reference>
<slot></slot>
</template>
<div class="popover-w">
<i
v-for="item in tools"
:class="computedClass(item)"
@click="queryEvents(item)"
></i>
</div>
</el-popover>
<DisplayCard v-if="showTable" :table-data="dataSource"></DisplayCard>
</template>
<script setup>
import DisplayCard from './DisplayCard.vue'
import { computed, inject, onMounted, ref } from 'vue'
import { DrawEvent, DrawPolygon, DrawCircle, DrawRect } from '@antv/l7-draw'
import { point, polygon, booleanPointInPolygon } from '@turf/turf'
import { getEvents } from '@/api/smart_city.js'
// 定义普通数据
let eventsData = null
// 定义响应式数据
const tools = ref([
'drawPolygonTool',
'drawRectTool',
'drawCircleTool',
'delete',
])
const dataSource = ref([])
// 定义计算属性
const computedClass = computed(() => {
return (item) => {
const res = {
iconfont: true,
'query-item': true,
}
res[`icon-${item}`] = true
return res
}
})
const showTable = computed(() => {
return dataSource.value.length > 0
})
// 在onMounted中获取事故数据
onMounted(async () => {
const res = await getEvents()
eventsData = res.features
})
// 定义查询绘制函数
let draw = null
const { scene } = inject('$scene_map')
function queryEvents(type) {
if (draw) {
draw.disable()
draw.clear()
}
switch (type) {
case 'drawPolygonTool':
draw = new DrawPolygon(scene, {})
break
case 'drawRectTool':
draw = new DrawRect(scene, {})
break
case 'drawCircleTool':
draw = new DrawCircle(scene, {})
break
default:
// 清空dataSource
dataSource.value = []
return
}
draw.enable()
draw.on(DrawEvent.Change, (allFeatures) => {
// 找出最后绘制的图形(多边形)
const activeFeature = allFeatures[allFeatures.length - 1]
// 只保留最后绘制的图形
allFeatures.forEach((item, index) => {
if (index !== allFeatures.length - 1) {
draw.removeFeature(item)
}
})
if (eventsData.length && activeFeature) {
const {
geometry: { coordinates: coordinatesActive },
} = activeFeature
// 使用turf判断哪些事故点落在绘制的拉框中
const resData = eventsData.filter((item) => {
const { geometry } = item
if (geometry.type === 'Point') {
const pt = point(geometry.coordinates)
const poly = polygon(coordinatesActive)
const isInArea = booleanPointInPolygon(pt, poly)
return isInArea
}
})
dataSource.value = resData
}
})
}
</script>
DisplayCard.vue添加点击跳转功能
<template>
<div class="display-card">
<el-table
:data="computedData"
size="small"
:max-height="400"
@row-click="rowClick"
>
<el-table-column prop="event_num" label="事件编号"></el-table-column>
<el-table-column prop="name" label="类型"></el-table-column>
<el-table-column label="操作" fixed="right">
<el-button size="small" type="primary" link>详情</el-button>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { computed, inject, onBeforeUnmount } from 'vue'
import { PointLayer } from '@antv/l7'
const { scene, map } = inject('$scene_map')
let markLayer = null
const props = defineProps({
tableData: {
type: Array,
},
})
const computedData = computed(() => {
return props.tableData.map((row) => {
const {
geometry,
properties: { event_num, name },
} = row
return {
geometry,
event_num,
name,
}
})
})
function rowClick(row) {
// console.log(row)
markLayer && scene.removeLayer(markLayer)
// 1. 根据坐标绘制雷达点
const data = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: row.geometry.coordinates,
},
},
],
}
markLayer = new PointLayer()
.source(data)
.shape('radar')
.size(20)
.color('#f00')
.animate(true)
scene.addLayer(markLayer)
// 2. 根据坐标飞行(切换视角)
map.flyTo({
center: row.geometry.coordinates,
zoom: 15,
speed: 1,
pitch: 30,
})
}
onBeforeUnmount(() => {
markLayer && scene.removeLayer(markLayer)
})
</script>
<style scoped>
.display-card {
position: fixed;
bottom: 80px;
background: #53697670;
border-radius: 4px;
box-shadow: 0 0 5px 3px #333;
}
.eleCeil {
background: transparent;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-table) {
background-color: transparent;
}
:deep(.el-table tr) {
background-color: transparent;
color: #fff;
cursor: pointer;
}
:deep(.el-table tr:hover) {
background-color: rgba(0, 0, 0, 0.5);
}
:deep(.el-table--enable-row-transition .el-table__body td.el-table__cell) {
background-color: transparent;
}
:deep(.el-table th.el-table__cell) {
background-color: transparent;
}
:deep(.el-table td.el-table__cell) {
border-bottom: none;
}
:deep(.el-table__inner-wrapper::before) {
height: 0;
}
:deep(.el-table.is-scrolling-right th.el-table-fixed-column--right) {
background-color: transparent;
}
:deep(.el-table.is-scrolling-none th.el-table-fixed-column--right) {
background-color: transparent;
}
</style>
实现功能: 点击详情
, 显示事故详情
el-dialog
组件(对话框)el-table
组件(表格)
编号 | 坐标经度 | 坐标纬度 | 事故类型 | 事故区域 | 车牌号 | 事故人 | 事故等级 | 手机号 |
---|---|---|---|---|---|---|---|---|
地图测量
- 创建组件
MeasureTools.vue
<template>
<el-popover
placement="top"
trigger="click"
popper-style="background-color:#53697670;color:#fff"
:width="100"
>
<template #reference>
<slot></slot>
</template>
<div class="popover-w">
<i
v-for="item in tools"
:class="computedClass(item)"
@click="measure(item)"
></i>
</div>
</el-popover>
</template>
<script setup>
import { computed, inject, ref } from 'vue'
import { DrawPolygon, DrawCircle, DrawRect, DrawLine } from '@antv/l7-draw'
// 定义响应式数据
const tools = ref([
'drawPolygonTool',
'drawRectTool',
'drawCircleTool',
'line',
'delete',
])
// 定义计算属性
const computedClass = computed(() => {
return (item) => {
const res = {
iconfont: true,
'query-item': true,
}
res[`icon-${item}`] = true
return res
}
})
// 定义查询绘制函数
let draw = null
const { scene } = inject('$scene_map')
function measure(type) {
if (draw) {
draw.disable()
draw.clear()
}
switch (type) {
case 'drawPolygonTool':
draw = new DrawPolygon(scene, {
//展示面积
areaOptions: {},
})
break
case 'drawRectTool':
draw = new DrawRect(scene, {
//展示面积
areaOptions: {},
})
break
case 'drawCircleTool':
draw = new DrawCircle(scene, {
//展示面积
areaOptions: {},
})
break
case 'line':
draw = new DrawLine(scene, {
distanceOptions: {
// 是否展示总距离
showTotalDistance: false,
// 是否展示一段的距离
showDashDistance: true,
// 展示的格式
format: (meters) => {
if (meters >= 1000) {
return +(meters / 1000).toFixed(2) + 'km'
} else {
return +meters.toFixed(2) + 'm'
}
},
},
})
break
default:
return
}
draw.enable()
}
</script>
<style scoped>
.popover-w {
display: flex;
align-items: center;
justify-content: space-around;
}
.query-item:hover {
cursor: pointer;
background: linear-gradient(
to bottom,
rgba(0, 128, 255, 0.6),
rgba(0, 128, 255, 0.281)
);
}
</style>
- 引用组件
<template>
<footer class="footer">
<div class="btn-groups">
<div class="item">
<button class="toggle-btn" @click="handleRotation">
<i class="iconfont icon-fuwudiqiu"></i>
</button>
<p>{{ mark }}</p>
</div>
<div class="item">
<button class="toggle-btn" @click="toggleCharts">
<i class="iconfont icon-supervision-full"></i>
</button>
<p>控制中心</p>
</div>
<div class="item">
<button class="toggle-btn" @click="flyTo">
<i class="iconfont icon-icon-test"></i>
</button>
<p>{{ flyMsg }}</p>
</div>
<div class="item">
<DrawTools>
<button class="toggle-btn">
<i class="iconfont icon-paint"></i>
</button>
</DrawTools>
<p>事故查询</p>
</div>
<div class="item">
<MeasureTools>
<button class="toggle-btn">
<i class="iconfont icon-ruler"></i>
</button>
</MeasureTools>
<p>地图测量</p>
</div>
</div>
</footer>
</template>
<script setup>
import DrawTools from './DrawTools.vue'
import MeasureTools from './MeasureTools.vue'
import useRotation from './hooks/useRotation'
import useFly from './hooks/useFly'
const { mark, handleRotation } = useRotation()
const { flyMsg, flyTo } = useFly()
// 定义状态
let isShow = true
const emits = defineEmits(['toggleCharts'])
function toggleCharts() {
isShow = !isShow
emits('toggleCharts', isShow)
}
</script>