吉安感知网项目-前端
张含笑
2026-01-12 30c1b138d4c9c2daa3f3e67b62550eabaa9e6ca9
feat:模板
3 files modified
1 files added
503 ■■■■■ changed files
applications/mobile-web-view/src/appComponents/LeafletMap/index.vue 363 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appDataSource/leafletMapIcon/addressSearch.svg 5 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/Map/index.vue 11 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appPages/Map/searchBar.vue 124 ●●●●● patch | view | raw | blame | history
applications/mobile-web-view/src/appComponents/LeafletMap/index.vue
@@ -1,18 +1,6 @@
<template>
    <div class="page-container">
        <div class="map" id="map"></div>
        <div class="weather-box" v-show="weatherShow">
            <div class="icon">
                <van-image :src="WEATHER?.[weather] || ''" width="100%" height="100%"></van-image>
            </div>
            <div class="content">
                <span class="tq">{{ weather }} {{ temperature }}</span>
                <span class="tq">风速:{{ windVelocity }}</span>
            </div>
        </div>
        <div class="layer-btn" @click="layerChangePopupShow = true" v-show="layerShow">
            <van-image width="20" height="20" :src="layerIcon" />
            <div class="label">图层</div>
@@ -22,10 +10,7 @@
            <van-image width="20" height="20" :src="locationIcon" />
            <div class="label">定位</div>
        </div>
        <slot name="searchBar"></slot>
        <slot name="eventNav"></slot>
        <van-popup v-model:show="layerChangePopupShow" position="bottom" round>
            <div class="popup-container">
                <div class="category-content">
@@ -48,33 +33,6 @@
                        </div>
                    </div>
                </div>
                <div class="category-content">
                    <div class="header base-header">
                        <div class="title">基础图层</div>
                    </div>
                    <div class="content">
                        <BaseLayerManage v-if="isMapReady" :getCurMap="getCurMap" />
                        <!--                        <el-tree-->
                        <!--                            :props="defaultProps"-->
                        <!--                            :data="treeData"-->
                        <!--                            show-checkbox-->
                        <!--                            node-key="id"-->
                        <!--                            :expand-on-click-node="false"-->
                        <!--                            :check-on-click-node="false"-->
                        <!--                            :check-on-click-leaf="false"-->
                        <!--                            @check="handleCheckChange"-->
                        <!--                        />-->
                    </div>
                </div>
                <div class="category-content">
                    <MapLayerManage
                        v-for="item in customizeLayer"
                        :options="item"
                        :key="item.id"
                        v-if="isMapReady"
                        :getCurMap="getCurMap"
                    />
                </div>
            </div>
        </van-popup>
    </div>
@@ -87,21 +45,12 @@
import layerIcon from '@/appDataSource/leafletMapIcon/layer-icon.svg'
import locationIcon from '@/appDataSource/leafletMapIcon/location-icon.svg'
import { useStore } from 'vuex'
import { getEventPageSimpleApi } from '@/api/home/event'
import { getDroneSuggest, getLayerTreeApi } from '@/api/home/common'
import DroneMarkerPopup from '@/appComponents/DroneMarkerPopup/index.vue'
import EventMarkerPopup from '@/appComponents/EventMarkerPopup/index.vue'
import MapLayerManage from './MapLayerManage.vue'
import BaseLayerManage from '@/appComponents/LeafletMap/BaseLayerManage.vue'
import { getDeviceRegion, getMapEvents } from '@/api/home/aggregation'
import { getIimitationArea } from '@/api/map/dji'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import sl from '@/appDataSource/leafletMapIcon/sl.svg'
import yx from '@/appDataSource/leafletMapIcon/yx.svg'
import EventBus from '@/utils/eventBus'
import { getEventImage } from '@/utils/stateToImageMap/event'
import { WEATHER } from '@/const/weather'
const emit = defineEmits(['location-selected'])
const { weatherShow, layerShow, locationShow, createWorkShow } = defineProps({
    weatherShow: {
@@ -125,7 +74,6 @@
const isProd = import.meta.env.VITE_APP_ENV === 'production'
import { basemapLayer0, basemapLayer1, basemapLayer2, basemapLayer3 } from '@/const/leafletConst'
import { useAreaBoundary } from '@/hooks/useAreaBoundary'
const basemap0 = L.layerGroup([basemapLayer0, basemapLayer1])
const basemap1 = L.layerGroup([basemapLayer2, basemapLayer3])
@@ -155,8 +103,6 @@
let eventMarkersLayer = null
let addressMarkersLayer = null
let boundaryInstance = useAreaBoundary({ initFly: true })
const initMap = () => {
    if (map) return
    map = L.map('map', {
@@ -167,7 +113,6 @@
        doubleClickZoom: false,
        editable: true, //绘制控件
    }).setView([25.992338, 114.823254], 3)
    boundaryInstance.initBoundary(map)
    map.whenReady(() => {
        isMapReady.value = true
@@ -262,7 +207,7 @@
            }
        )
    } else {
        console.log('Geolocation is not supported by this browser.')
        console.log('该浏览器不支持地理定位功能。')
    }
}
let lastLocationMarker = null
@@ -294,9 +239,7 @@
const mapAddMarker = data => {
    if (!map) return
    const { lat, lng, zoom = 16, name = '', customClassName = '' } = data
    mapSetView(data)
    // 创建带有图标和文字的 divIcon
@@ -317,142 +260,15 @@
    L.marker([lat, lng], { icon: droneHtmlIcon }).addTo(addressMarkersLayer)
}
const mapOnlyShowDrone = (data = {}) => {
    const { device_sn = '' } = data
    eventMarkersLayer.clearLayers()
    if (droneAllData.length > 0 && device_sn !== '') {
        mapInitDroneMarkers(device_sn)
    } else {
        initDrone(device_sn)
    }
    if (device_sn !== '') {
        getSignDroneEvents(device_sn)
    } else {
        initEvents()
    }
}
const mapClearMarker = () => {
    addressMarkersLayer.clearLayers()
}
let droneAllData = []
const initDrone = (device_sn = '') => {
    getDeviceRegion({
        hidden_flag: 0,
    }).then(res => {
        const data = res?.data?.data || []
        droneAllData = data.filter(item => item.status !== 'OFFLINE')
        mapInitDroneMarkers(device_sn)
    })
}
const mapInitDroneMarkers = (device_sn = '') => {
    droneMarkersLayer.clearLayers()
    let materialColor = 'rgba(27, 215, 89, 40)'
    let frameColor = 'rgba(27, 215, 89, 255)'
    droneAllData
        .filter(item => item.device_sn === device_sn || device_sn === '')
        .forEach(item => {
            let event_num = item.event_num || 0
            if (event_num > 100) {
                materialColor = 'rgba(255, 37, 9, 40)'
                frameColor = 'rgba(255, 37, 9, 255)'
            } else if (event_num > 50) {
                materialColor = 'rgba(255, 171, 54, 40)'
                frameColor = 'rgba(255, 171, 54, 255)'
            } else {
                materialColor = 'rgba(27, 215, 89, 10)'
                frameColor = 'rgba(27, 215, 89, 255)'
            }
            buildCirclePolygon(item.latitude, item.longitude, materialColor, frameColor).addTo(droneMarkersLayer)
            publicAddMakers({
                ...item,
                makerType: 1,
            })
        })
}
const initEvents = () => {
    getMapEvents({}).then(res => {
        const resData = res?.data?.data?.data || []
        resData.forEach(item => {
            publicAddMakers({
                ...item,
                makerType: 2,
            })
        })
    })
}
const getSignDroneEvents = (device_sn = '') => {
    const params = {
        'device_sn': device_sn,
    }
    getEventPageSimpleApi(params, { current: 1, size: 999999 }).then(res => {
        const resData = res?.data?.data || []
        resData.forEach(item => {
            publicAddMakers({
                ...item,
                makerType: 2,
            })
        })
    })
}
// 天气
const store = useStore()
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode)
const weather = ref('')
// 风速
const windVelocity = ref('')
const temperature = ref('')
// 获取天气建议
const getWeatherSuggest = () => {
    getDroneSuggest({ areaCode: selectedAreaCode.value }).then(res => {
        if (res.data.code !== 0) return
        weather.value = res.data.data.weather
        windVelocity.value = res.data.data.windPower
        temperature.value = res.data.data.temperature
    })
}
// 获取后台自定义图层
const customizeLayer = ref(null)
function getLayerTree() {
    getLayerTreeApi().then(res => {
        const colorList = ['#00ff06', '#ff0000', '#00ffea']
        // const imgList = [dzwlSvg,zdyjfqSvg,gtkjghSvg]
        customizeLayer.value = res.data.data.map((item, index) => ({
            ...item,
            color: colorList[index],
            imgIcon: [], // imgList[index]
        }))
        if (isProd) {
            customizeLayer.value = customizeLayer.value.filter(i => i.id === 1)
        }
    })
}
onMounted(async () => {
    getLayerTree()
    getWeatherSuggest()
    await nextTick()
    initMap()
@@ -461,11 +277,7 @@
    EventBus.on('mapSetView', mapSetView)
    EventBus.on('mapAddMarker', mapAddMarker)
    EventBus.on('mapClearMarker', mapClearMarker)
    EventBus.on('mapOnlyShowDrone', mapOnlyShowDrone)
    initDrone()
    initEvents()
    // initEvents()
    // L.circle([24, 112], {
    //     color: 'red', // 边框颜色
    //     fillColor: '#f03', // 填充颜色
@@ -478,126 +290,8 @@
    EventBus.off('mapSetView', mapSetView)
    EventBus.off('mapAddMarker', mapAddMarker)
    EventBus.off('mapClearMarker', mapClearMarker)
    EventBus.off('mapOnlyShowDrone', mapOnlyShowDrone)
})
function publicAddMakers(data) {
    // makerType 1 = 机巢, 2 = 事件点
    const { makerType, latitude, longitude } = data
    const customIcon = L.icon({
        iconUrl: makerType === 1 ? droneIcon : getEventImage(data.status), // 替换为你的图片路径
        iconSize: makerType === 1 ? [24, 24] : [20, 20], // 图标大小
        iconAnchor: makerType === 1 ? [12, 12] : [10, 10], // 图标的锚点(设置图片的底部中心)
    })
    let marker = null
    if (makerType === 1) {
        // 创建带有图标和文字的 divIcon
        let droneHtmlIcon = L.divIcon({
            html: `
                    <div class="content">
                        <img src="${droneIcon}">
                        <span>
                            ${data.nickname}
                        </span>
                    </div>
                `,
            className: 'ztzf-drone-custom-icon', // 可选,用于添加CSS类
            iconSize: [24, 24], // 设置图标大小
            iconAnchor: [12, 12],
        })
        marker = L.marker([latitude, longitude], { icon: droneHtmlIcon, zIndexOffset: 9 }).addTo(droneMarkersLayer)
    } else {
        marker = L.marker([latitude, longitude], {
            icon: customIcon,
            zIndexOffset: 1,
        }).addTo(eventMarkersLayer)
    }
    marker.on('click', e => {
        // 当前点击要素---关联数据
        marker.unbindPopup()
        const zoomLevel = map.getZoom()
        if (makerType === 1) {
            if (zoomLevel < 11) {
                mapSetView({
                    lat: e.latlng.lat,
                    lng: e.latlng.lng,
                    zoom: 11,
                })
                return
            }
        }
        if (makerType !== 1) {
            if (zoomLevel < 13) {
                mapSetView({
                    lat: e.latlng.lat,
                    lng: e.latlng.lng,
                    zoom: 13,
                })
                return
            }
        }
        const container = document.createElement('div')
        const popupApp = createApp(makerType === 1 ? DroneMarkerPopup : EventMarkerPopup, {
            data: e.target.options.customData,
            onClose: () => {
                marker.closePopup()
                popupApp.unmount() // 卸载 Vue 应用
            },
        })
        popupApp.mount(container)
        marker
            .bindPopup(container, {
                closeButton: false,
                className: 'custom-leaflet-popup',
                offset: L.point(80, 0),
            })
            .openPopup()
    })
    // 使用 marker 的 id 作为标识符
    marker.options.customData = data
}
// 构建圆形多边形
function buildCirclePolygon(lat, lng, materialColor, frameColor) {
    var radius = 5000 / 100460 // 5000米转为大约的纬度差(纬度上的1度大约是111km)
    var parts = []
    for (var i = 0; i < 360; i++) {
        var radians = ((i + 1) * Math.PI) / 180
        // 计算新的点
        var circlePoint = [
            Math.cos(radians) * radius + lat, // 纬度偏移
            Math.sin(radians) * radius + lng, // 经度偏移,需考虑纬度的影响
        ]
        parts[i] = circlePoint
    }
    // 生成多边形,近似圆形
    var polygon = L.polygon(parts, {
        color: frameColor,
        fillColor: materialColor,
        weight: 1,
    })
    return polygon
}
</script>
<style lang="scss" scoped>
@@ -611,61 +305,6 @@
        height: 100%;
    }
    .weather-box {
        padding: 4px 0;
        padding-right: 6px;
        display: flex;
        justify-content: center;
        align-items: center;
        position: absolute;
        top: 12px;
        left: 12px;
        height: 48px;
        background: #fff;
        z-index: 996;
        box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.18);
        border-radius: 6px 6px 6px 6px;
        .icon {
            margin-left: 10px;
            width: 24px;
            height: 24px;
        }
        .content {
            height: 100%;
            margin-left: 4px;
            display: flex;
            flex-direction: column;
            justify-content: space-around;
            font-family: Source Han Sans CN, Source Han Sans CN;
            font-weight: 400;
            font-size: 12px;
            color: #757575;
            text-align: left;
            font-style: normal;
            text-transform: none;
        }
        .tq {
            margin: 0 5px;
        }
        .qk {
            margin-left: 5px;
            color: #04f043;
        }
        .yellowqk {
            margin-left: 5px;
            color: yellow;
        }
        .redqk {
            margin-left: 5px;
            color: red;
        }
    }
    .location-btn,
    .layer-btn {
applications/mobile-web-view/src/appDataSource/leafletMapIcon/addressSearch.svg
New file
@@ -0,0 +1,5 @@
<svg width="7" height="12" viewBox="0 0 7 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector 185">
<path id="Vector 185_2" d="M6 11L1 6L6 1" stroke="#363636" stroke-linecap="round"/>
</g>
</svg>
applications/mobile-web-view/src/appPages/Map/index.vue
@@ -1,13 +1,4 @@
<!--
 * @Author       : yuan
 * @Date         : 2025-10-14 13:57:35
 * @LastEditors  : yuan
 * @LastEditTime : 2025-10-21 11:40:25
 * @FilePath     : \src\appPages\Map\index.vue
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-10-14 13:57:35
-->
<template>
    <div class="wrapper" :style="{ paddingTop: route.query.topMargin + 'px' }">
        <leaflet-map :weatherShow="true" :layerShow="true" :locationShow="true">
applications/mobile-web-view/src/appPages/Map/searchBar.vue
@@ -1,13 +1,3 @@
<!--
 * @Author       : yuan
 * @Date         : 2025-10-13 16:15:19
 * @LastEditors  : yuan
 * @LastEditTime : 2025-12-19 15:32:56
 * @FilePath     : \applications\mobile-web-view\src\appPages\Map\searchBar.vue
 * @Description  :
 * Copyright 2025 OBKoro1, All Rights Reserved.
 * 2025-10-13 16:15:19
-->
<template>
    <van-floating-panel v-model:height="height" :anchors="anchors">
        <template #header>
@@ -24,16 +14,9 @@
                    :border="false"
                >
                    <template #left-icon>
                        <div class="search-left-box" @click="onSelect">
                        <div class="search-left-box">
                            <van-image width="16" height="16" :src="switchoverIcon" />
                            <div class="search-type">{{ curSearchType === 0 ? '机巢' : '地址' }}</div>
                        </div>
                    </template>
                    <template #right-icon v-if="isWeb">
                        <div class="search-right-box" @click="scanCode">
                            <van-image width="20" height="20" :src="qrCodeIcon" />
                            <div class="search-type">地址</div>
                        </div>
                    </template>
                </van-field>
@@ -48,11 +31,7 @@
        </div>
        <van-list v-else :finished="finished" :finished-text="finishedText">
            <van-cell v-show="curSearchType === 0" v-for="item in list" :key="item" @click="flyToDrone(item)">
                <template #title>{{ item.nickname }}</template>
            </van-cell>
            <van-cell v-show="curSearchType === 1" v-for="item in list" :key="item" @click="flyToAddress(item)">
            <van-cell v-for="item in list" :key="item" @click="flyToAddress(item)">
                <template #title>{{ item.name }}</template>
                <template #value>{{ item.district }}</template>
            </van-cell>
@@ -61,16 +40,11 @@
</template>
<script setup>
import switchoverIcon from '@/appDataSource/leafletMapIcon/switchover-icon.svg'
import qrCodeIcon from '@/appDataSource/leafletMapIcon/qr-code.svg'
import switchoverIcon from '@/appDataSource/leafletMapIcon/addressSearch.svg'
import { showToast } from 'vant'
import _ from 'lodash'
import { searchWithPlugin } from '@/utils/util'
import { useStore } from 'vuex'
import { selectDeviceList } from '@/api/home/common'
import { selectDevicePageSimplify } from '@/api/home/machineNest.js'
import EventBus from '@/utils/eventBus'
import { gcj02ToWgs84 } from '@/utils/coordinateTransformation'
import { useRoute } from 'vue-router'
@@ -78,7 +52,6 @@
const isWeb = ref(true)
const store = useStore()
const selectedAreaCode = computed(() => store.state.user.selectedAreaCode)
const searchVal = ref('')
const list = ref([])
const finished = ref(false)
@@ -87,25 +60,7 @@
const anchors = [70, Math.round(0.6 * window.innerHeight)]
const height = ref(anchors[0])
const actions = ref([{ text: '地址' }])
const curSearchType = ref(0) // 0机巢 1地址
const onSelect = () => {
    searchVal.value = ''
    list.value = []
    finished.value = false
    curSearchType.value = curSearchType.value === 0 ? 1 : 0
    actions.value = [{ text: curSearchType.value === 0 ? '地址' : '机巢' }]
    EventBus.emit('mapClearMarker')
    EventBus.emit('mapOnlyShowDrone')
    if (curSearchType.value === 0) {
        getDeviceList() // 切换到机巢时展示全部机巢
    }
}
const curSearchType = ref(1) // 固定为地址搜索
// 获取地址搜索结果
const getAddressList = () => {
@@ -124,37 +79,12 @@
    })
}
// 获取机巢搜索结果
const getDeviceList = async () => {
    const params = {
        nickname: searchVal.value,
        current: 1,
        size: 999,
    }
    const res = await selectDevicePageSimplify(params)
    if (res.data.code !== 0) return
    if (res?.data?.data?.records && res?.data?.data?.records.length > 0) {
        list.value = res?.data?.data?.records.filter(item => item.status !== 'OFFLINE') || []
        finishedText.value = list.value.length > 0 ? '' : '暂无数据'
    } else {
        finishedText.value = '暂无数据'
    }
    setTimeout(() => {
        finished.value = true
        loadingTest.value = false
    }, 800)
}
const tooltipText = computed(() => {
    if (loadingTest.value) {
        return ''
    }
    if (searchVal.value.trim() === '') {
        return curSearchType.value === 0 ? '' : `支持搜索${curSearchType.value === 0 ? '机巢' : '地址'}`
        return '支持搜索地址'
    }
    return finishedText.value || ''
})
@@ -167,24 +97,15 @@
    EventBus.emit('mapClearMarker')
    if (curSearchType.value === 1) {
        getAddressList()
    } else if (curSearchType.value === 0) {
        getDeviceList()
    }
    getAddressList()
}
// 输入框input事件
const handlerInput = _.debounce(() => {
    if (searchVal.value.trim() === '') {
        if (curSearchType.value === 0) {
            // 搜索机巢时,输入框为空展示全部机巢
            inputSelect()
        } else {
            loadingTest.value = false
            list.value = []
            finishedText.value = ''
        }
        loadingTest.value = false
        list.value = []
        finishedText.value = ''
        return
    }
    inputSelect()
@@ -196,26 +117,9 @@
    EventBus.emit('mapOnlyShowDrone')
}
// 飞到机巢位置
const flyToDrone = item => {
    height.value = anchors[0]
    searchVal.value = item.nickname
    inputSelect()
    EventBus.emit('mapOnlyShowDrone', { device_sn: item.device_sn })
    EventBus.emit('mapSetView', {
        lat: item.latitude,
        lng: item.longitude,
        zoom: 11,
    })
}
const flyToAddress = item => {
    if (!item.location) {
        showToast('当前地址无坐标信息')
        return
    }
@@ -252,10 +156,6 @@
)
onMounted(() => {
    if (curSearchType.value === 0) {
        getDeviceList()
    }
    const hash = window.location.hash.slice(1)
    const [hashPath, hashParamsStr] = hash.split('?')
    if (hashParamsStr) {
@@ -307,7 +207,7 @@
                .search-type {
                    margin: 0 4px;
                    color: #1d6fe9;
                    color: #282828;
                }
            }
        }
@@ -347,4 +247,4 @@
        color: #000;
    }
}
</style>
</style>