项目搭建
- 创建项目
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
| |
| const mockjs = require('mockjs') |
| |
| |
| |
| const wuhan_buildings = require('./Wuhan_Buildings.json') |
| |
| const wuhan_roads = require('./Wuhan_roads.json') |
| |
| 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
| |
| import axios from '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
| |
| 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
| |
| 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' |
| |
| |
| |
| |
| export default { |
| install(app) { |
| |
| |
| const token = import.meta.env.VITE_TOKEN |
| mapboxgl.accessToken = token |
| |
| |
| 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', { 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> |
| |
| import { inject, onMounted } from 'vue' |
| |
| onMounted(() => { |
| |
| 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
| |
| import { getCityBuildings } from '@/api/smart_city.js' |
| |
| import { CityBuildingLayer } from '@antv/l7' |
| |
| |
| export default async () => { |
| |
| const buildings_data = await getCityBuildings() |
| |
| |
| |
| const building_layer = new CityBuildingLayer({ |
| name: '武汉市', |
| }) |
| |
| |
| building_layer |
| .source(buildings_data) |
| .size('Elevation', (h) => h) |
| .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 () => { |
| |
| const { scene } = inject('$scene_map') |
| |
| |
| const building_layer = await useBuildings() |
| |
| scene.addLayer(building_layer) |
| }) |
| </script> |
| |
| <style></style> |
渲染城市道路
- 创建文件
SmartCity/hooks/useRoads.js
| |
| import { getRoads } from '@/api/smart_city.js' |
| |
| |
| import { LineLayer } from '@antv/l7' |
| |
| |
| 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 () => { |
| |
| 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(() => { |
| |
| 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; |
| |
| 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: { |
| |
| position: 'top', |
| |
| 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' |
| |
| 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' |
| |
| 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> |
| |
| @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> |
| |
| @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; |
| } |
| |
| |
| 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' |
| |
| 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' |
| |
| 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 |
| |
| 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(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 |
| |
| |
| 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(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.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 |
| |
| |
| 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) { |
| |
| markLayer && scene.removeLayer(markLayer) |
| |
| 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) |
| |
| |
| 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> |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步