Vue-智慧城市

项目搭建

  1. 创建项目

npm create vite

  1. 安装依赖

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

  1. 项目清理
    1. 清理App.vue
    2. 删除assetscomponents下所有文件
    3. main.js中删除引用main.css的代码

项目初始化

搭建mock服务

Mock.js 是一个用于生成模拟数据的 JS库,它的主要作用是:

  • 生成模拟数据:可以生成随机的文本、数字、布尔值、日期、颜色、图片等,也可以生成符合特定格式的数据,如邮箱、身份证号等。
  • 模拟接口响应:可以拦截 AJAX 请求,并返回模拟的数据,从而无需依赖后端接口即可进行前端开发。
  • 简化测试:在单元测试或集成测试中,使用 Mock.js 可以轻松模拟复杂的接口响应,使得测试更加集中和高效。
  1. 导入geojson数据

mock/Wuhan_Buildings.json, mock/Wuhan_events.json, mock/Wuhan_roads.json

  1. 创建启动文件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,
  })
}
  1. 启动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进行再次封装

  1. 创建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
  1. 编写接口请求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',
  })
}

初始化样式

  1. 创建样式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%;
}
  1. 导入样式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')

地图初始化

  1. 创建插件文件

使用Mapbox+antv-l7开发.

首先, 需要创建map对象和scene对象, 这里可以考虑做一个Vue插件

创建src/plugins/mapbox.js

  1. 配置token

在项目根目录, 创建.env配置文件

VITE_TOKEN = 'pk.eyJ1IjoiY2hlbmdjaGFvY2hhbyIsImEiOiJjbGU1aDZ2eWUwMXp4M29udmFnNnNyZjBhIn0.2Kd0ZX06ReEdBnZ9XU4XUA'

  1. 编写插件

    1. 导入相关库
    2. 导出插件对象
      1. 创建地图容器, 挂载到body
      2. 创建map对象, 并开启雾化
      3. 创建scene对象
      4. 使用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 })
  },
}
  1. 注册插件
// 注册插件
import Mapbox from './plugins/mapbox.js'

createApp(App).use(ElementPlus).use(Mapbox).mount('#app')

城市场景

基本实现

  1. 创建组件 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>
  1. App.vue中引用组件
<template>
  <!-- 引用组件 -->
  <SmartCity />
</template>

<script setup>
// 导入组件
import SmartCity from './components/SmartCity/index.vue'
</script>

<style></style>

渲染城市建筑

使用antv-l7渲染城市建筑图层, 封装一个hooks将这个功能集成

  1. 创建文件 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
}
  1. 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>

渲染城市道路

  1. 创建文件 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
}
  1. 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>

可视化面板

头部组件

  1. 创建组件 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>
  1. 引用组件
<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>

图表组件

使用antvG2Plot来实现图表

目录结构: src/components/G2Charts

  • hooks目录

    • useLeftBottom.js: 左下图表
    • useLeftTop.js: 左上图表
    • useRightTop.js: 右上图表
  • index.vue

  1. 创建组件 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>
  1. 使用组件
<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>
  1. 左上图表-柱状图
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,
  }
}
  1. 左下图表-玫瑰图
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,
  }
}
  1. 右上图表-饼状图
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,
  }
}
  1. 在组件中使用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>
  1. 样式优化
<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>

底部组件

  1. 创建组件 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>
  1. 引用组件
<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>

功能模块

地球自转

制作一个按钮, 当点击按钮时, 启动/停止旋转

  1. 底部组件结构样式
<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>
  1. 逻辑 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,
  }
}
  1. 使用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>

控制中心

  1. 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>
  1. 逻辑

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>

地图复位

  1. 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>
  1. 逻辑 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,
  }
}
  1. 使用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工具库,判断事故数据点是否在框选范围内
  • 详细信息处理,点击结果面板,展示该条数据的详细信息
  1. 数据准备

使用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));
}
  1. 拉框查询

绘制组件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>
  1. 结果处理

通过拉框可以得到一个多边形, 接下来需要使用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>
  1. 数据渲染
    将查询到的结果渲染到表格中

创建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>

实现功能: 点击详情, 显示事故详情

  1. el-dialog组件(对话框)
  2. el-table组件(表格)
编号 坐标经度 坐标纬度 事故类型 事故区域 车牌号 事故人 事故等级 手机号

地图测量

  1. 创建组件 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>
  1. 引用组件
<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>
posted @ 2024-12-07 23:27  Khru  阅读(23)  评论(0编辑  收藏  举报