| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769 |
- <template>
- <view class="map-view-container">
- <!-- Leaflet WebView 地图 -->
- <leaflet-map
- ref="leafletMapRef"
- :latitude="centerLatitude"
- :longitude="centerLongitude"
- :zoom="props.zoom"
- :markers="markers"
- :layer-type="currentLayerType"
- :tianditu-key="tiandituKey"
- @markerTap="handleMarkerTap"
- @regionChange="handleRegionChange"
- @layerChange="handleLayerChange"
- @ready="handleMapReady"
- @error="handleMapError"
- />
- <!-- 返回按钮 -->
- <view class="back-btn" @click="handleBack">
- <text class="back-icon">←</text>
- </view>
- <!-- 操作按钮 -->
- <view class="map-controls">
- <view class="control-btn" @click="showLayerPopup">
- <image class="control-icon" src="/static/images/map/1.png" mode="aspectFit"></image>
- </view>
- <view class="control-btn" @click="handleZoomIn">
- <image class="control-icon" src="/static/images/map/3.png" mode="aspectFit"></image>
- </view>
- <view class="control-btn" @click="handleZoomOut">
- <image class="control-icon" src="/static/images/map/4.png" mode="aspectFit"></image>
- </view>
- <view class="control-btn" @click="handleLocation">
- <image class="control-icon" src="/static/images/map/2.png" mode="aspectFit"></image>
- </view>
- </view>
- <!-- 图层切换弹窗 - 自定义实现 -->
- <!-- 遮罩层 -->
- <view v-if="layerPopupVisible" class="popup-mask" @click="closeLayerPopup"></view>
- <!-- 弹窗内容 -->
- <view class="layer-popup" :class="{ 'layer-popup-show': layerPopupVisible }">
- <view class="popup-header">
- <text class="popup-title">图层选择</text>
- <view class="popup-close" @click="closeLayerPopup">
- <text class="close-icon">×</text>
- </view>
- </view>
- <scroll-view class="popup-scroll" :scroll-y="true" :show-scrollbar="true">
- <!-- 地图主题 -->
- <view class="layer-section">
- <text class="section-title">{{ baseLayerConfig.name }}</text>
- <view class="layer-list">
- <view v-for="(item, index) in baseLayerConfig.list" :key="index" class="layer-item" :class="{ 'layer-item-active': item.active }" @click="handleBaseLayerChange(index)">
- <image class="layer-image" :src="item.icon" mode="aspectFill"></image>
- <text class="layer-name">{{ item.name }}</text>
- <view v-if="item.active" class="layer-check">
- <text class="check-icon">✓</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 基础数据 -->
- <view class="layer-section">
- <text class="section-title">{{ businessLayerConfig.name }}</text>
- <view class="layer-list">
- <view v-for="(item, index) in businessLayerConfig.list" :key="index" class="layer-item" :class="{ 'layer-item-active': item.active }" @click="handleBusinessLayerChange(index)">
- <image class="layer-image" :src="item.icon" mode="aspectFill"></image>
- <text class="layer-name">{{ item.name }}</text>
- <view v-if="item.active" class="layer-check">
- <text class="check-icon">✓</text>
- </view>
- </view>
- </view>
- </view>
- </scroll-view>
- </view>
- </view>
- </template>
- <script setup lang="uts">
- import { ref, reactive, type ComponentPublicInstance } from 'vue'
- import type { MapMarker, MapLayerType } from '../../types/map'
- import { ARCGIS_CONFIG } from '../../config/map.config'
- // Props
- type Props = {
- latitude?: number
- longitude?: number
- zoom?: number
- markers?: MapMarker[]
- showLocation?: boolean
- enableZoom?: boolean
- layerType?: MapLayerType
- tiandituKey?: string
- }
- const props = withDefaults(defineProps<Props>(), {
- latitude: 32.556211,
- longitude: 111.494811,
- zoom: 13,
- markers: () => [],
- showLocation: true,
- enableZoom: true,
- layerType: 'tianditu-img',
- tiandituKey: '4c9c8a818d3cb25bf5ccbd749e7e67c8'
- })
- // Emits
- const emit = defineEmits<{
- (e: 'markerTap', event: any): void
- (e: 'regionChange', region: any): void
- (e: 'locationUpdate', location: any): void
- (e: 'layerChange', layerType: MapLayerType): void
- (e: 'layerChangeError', error: string): void
- }>()
- // Leaflet 地图引用
- const leafletMapRef = ref<ComponentPublicInstance | null>(null)
- // 地图中心点
- const centerLatitude = ref<number>(props.latitude)
- const centerLongitude = ref<number>(props.longitude)
- const scale = ref<number>(16)
- // 当前图层类型(使用 as string 转换,让 ref 自动推断类型)
- const currentLayerType = ref(props.layerType as string)
- // 地图是否已准备好
- const isMapReady = ref(false)
- // 图层弹窗显示状态
- const layerPopupVisible = ref(false)
- // 当前位置标记点 ID
- const locationMarkerId = ref<number>(999999)
- // 图层配置类型定义
- type LayerItem = {
- code: string
- name: string
- icon: string
- url?: string
- active: boolean
- }
- type LayerConfig = {
- name: string
- list: LayerItem[]
- }
- // 图层配置(使用 ref 包装)
- const baseLayerConfig = ref<LayerConfig>({
- name: '地图主题',
- list: [
- {
- code: 'tianditu-vec',
- name: '天地图-矢量',
- icon: '/static/images/map/map1.jpg',
- active: false
- },
- {
- code: 'tianditu-img',
- name: '天地图-影像',
- icon: '/static/images/map/map2.jpg',
- active: true
- },
- {
- code: 'tianditu-ter',
- name: '天地图-地形',
- icon: '/static/images/map/map3.jpg',
- active: false
- }
- ]
- })
- const businessLayerConfig = ref<LayerConfig>({
- name: '基础数据',
- list: [
- {
- code: 'boundary',
- name: '界桩',
- icon: '/static/images/map/map4.jpg',
- url: '/arcgis/rest/services/水利专题图层/界桩点/MapServer/tile/{z}/{y}/{x}',
- active: false
- },
- {
- code: 'landLine',
- name: '管理线',
- icon: '/static/images/map/map5.jpg',
- url: '/arcgis/rest/services/管理和保护范围/管理范围/MapServer/tile/{z}/{y}/{x}',
- active: true
- },
- {
- code: 'protectLine',
- name: '保护线',
- icon: '/static/images/map/map6.jpg',
- url: '/arcgis/rest/services/管理和保护范围/保护范围/MapServer/tile/{z}/{y}/{x}',
- active: false
- }
- ]
- })
- /**
- * 获取图层显示名称
- */
- const getLayerName = (layerType: MapLayerType): string => {
- if (layerType == 'tianditu-vec') {
- return '天地图矢量'
- } else if (layerType == 'tianditu-img') {
- return '天地图影像'
- } else if (layerType == 'tianditu-ter') {
- return '天地图地形'
- }
- return '未知图层'
- }
- /**
- * 初始化业务图层(加载默认激活的图层)
- * 必须在 handleMapReady 之前定义
- */
- const initBusinessLayers = (): void => {
- console.log('initBusinessLayers 开始执行')
- console.log('leafletMapRef.value:', leafletMapRef.value)
-
- // 获取 token
- const token = uni.getStorageSync('accessToken') as string | null
- // const token = "eyJhbGciOiJIUzI1NiJ9.eyJyZWFsTmFtZSI6IueOi-itpuS9jSIsInN1YiI6IndhbmdqaW5nd2VpIiwiZGVwdElkIjoiMTAwMDA3IiwiZXhwIjoxNzYyOTMwMTAwLCJ1c2VySWQiOjI2OCwiaWF0IjoxNzYyODQzNzAwLCJqdGkiOiI3YzhhNmVmYi0xMDQyLTQ0NjAtOWNlMi1iZGMwMTVjNmZlMDYiLCJ1c2VybmFtZSI6IndhbmdqaW5nd2VpIn0.BlEnejBXkemhZY1nXd5xSAo8t5XoFSTnQVD09CQlVcI"
- console.log('token:', token != null ? 'exists' : 'null')
-
- for (let i = 0; i < businessLayerConfig.value.list.length; i++) {
- const item = businessLayerConfig.value.list[i]
- console.log(`检查图层 ${i}:`, item.name, 'active:', item.active)
-
- if (item.active) {
- const layerUrl = item.url as string | null
- console.log('layerUrl:', layerUrl)
-
- if (layerUrl != null && leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('addBusinessLayer', item.code, layerUrl, ARCGIS_CONFIG.baseUrl, token)
- console.log('✓ 初始化业务图层:', item.name)
- } else {
- console.log('✗ 未能添加图层,layerUrl:', layerUrl, 'leafletMapRef:', leafletMapRef.value)
- }
- }
- }
- }
- /**
- * 获取当前位置
- * 必须在 handleMapReady 之前定义
- *
- * 注意:
- * 1. manifest.json 中已配置定位权限,首次调用时系统会自动请求权限
- * 2. 使用系统定位(system provider),只支持 wgs84 坐标系
- * 3. 如需使用 gcj02 坐标,需配置腾讯定位 provider: 'tencent'
- */
- const handleLocation = (): void => {
- uni.getLocation({
- type: 'wgs84',
- success: (res) => {
- const latitude = res.latitude as number
- const longitude = res.longitude as number
- const accuracy = res.accuracy as number
- centerLatitude.value = latitude
- centerLongitude.value = longitude
- // 等待地图完全准备好再设置中心和添加标记点
- if (isMapReady.value && leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('setCenter', latitude, longitude, null)
- // 先移除旧的位置标记点
- leafletMapRef.value.$callMethod('removeMarker', locationMarkerId.value)
- // 添加位置标记点
- const locationMarker: MapMarker = {
- id: locationMarkerId.value,
- latitude: latitude,
- longitude: longitude,
- title: '我的位置',
- iconPath: '/static/images/map/location.png',
- width: 48,
- height: 48
- }
- leafletMapRef.value.$callMethod('addMarker', locationMarker)
- }
- emit('locationUpdate', {
- latitude: latitude,
- longitude: longitude,
- accuracy: accuracy
- })
- },
- fail: (err) => {
- console.log('定位失败', err)
-
- // #ifdef WEB
- // Web 平台降级方案:使用默认位置(props 中的初始位置)
- const latitude = props.latitude
- const longitude = props.longitude
-
- centerLatitude.value = latitude
- centerLongitude.value = longitude
- // 等待地图完全准备好再设置中心和添加标记点
- if (isMapReady.value && leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('setCenter', latitude, longitude, null)
- // 先移除旧的位置标记点
- leafletMapRef.value.$callMethod('removeMarker', locationMarkerId.value)
- // 添加位置标记点(使用默认位置)
- const locationMarker: MapMarker = {
- id: locationMarkerId.value,
- latitude: latitude,
- longitude: longitude,
- title: '默认位置',
- iconPath: '/static/images/map/location.png',
- width: 48,
- height: 48
- }
- leafletMapRef.value.$callMethod('addMarker', locationMarker)
- }
- emit('locationUpdate', {
- latitude: latitude,
- longitude: longitude,
- accuracy: 0
- })
-
- console.log('Web平台定位失败,使用默认位置')
- // #endif
-
- // #ifndef WEB
- // App 平台直接提示定位失败
- uni.showToast({
- title: '定位失败',
- icon: 'none',
- duration: 2000
- })
- // #endif
- }
- })
- }
- // 地图准备完成
- const handleMapReady = (): void => {
- isMapReady.value = true
- console.log('地图准备完成')
- // 延迟初始化业务图层,确保 JavaScript 完全准备好
- setTimeout(() => {
- console.log('准备初始化业务图层...')
- initBusinessLayers()
- }, 500)
- // 自动定位当前位置
- if (props.showLocation) {
- setTimeout(() => {
- console.log('开始自动定位...')
- handleLocation()
- }, 800)
- }
- }
- // 地图错误
- const handleMapError = (error: string): void => {
- console.error('地图错误:', error)
- }
- // 返回上一页
- const handleBack = (): void => {
- uni.navigateBack()
- }
- // 放大地图
- const handleZoomIn = (): void => {
- if (scale.value < 20) {
- scale.value++
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('setZoom', scale.value)
- }
- }
- }
- // 缩小地图
- const handleZoomOut = (): void => {
- if (scale.value > 5) {
- scale.value--
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('setZoom', scale.value)
- }
- }
- }
- // 标记点点击
- const handleMarkerTap = (e: any): void => {
- emit('markerTap', e)
- }
- // 地图区域变化
- const handleRegionChange = (e: any): void => {
- emit('regionChange', e)
- }
- // 图层变化
- const handleLayerChange = (layerType: MapLayerType): void => {
- currentLayerType.value = layerType as string
- emit('layerChange', layerType)
- }
- /**
- * 切换地图图层(核心方法,必须在 handleSwitchLayer 之前定义)
- */
- const switchLayer = (layerType: MapLayerType): void => {
- try {
- // 验证图层类型
- if (currentLayerType.value == (layerType as string)) {
- return
- }
- // 更新图层
- currentLayerType.value = layerType as string
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('switchLayer', layerType)
- }
- } catch (e: any) {
- const message = e.message as string | null
- const errorMsg: string = message != null ? message : '图层切换失败'
- emit('layerChangeError', errorMsg)
- }
- }
- /**
- * 显示图层选择弹窗
- */
- const showLayerPopup = (): void => {
- layerPopupVisible.value = true
- }
- /**
- * 关闭图层选择弹窗
- */
- const closeLayerPopup = (): void => {
- layerPopupVisible.value = false
- }
- /**
- * 切换基础图层(地图主题)
- */
- const handleBaseLayerChange = (index: number): void => {
- // 取消其他图层的选中状态
- for (let i = 0; i < baseLayerConfig.value.list.length; i++) {
- baseLayerConfig.value.list[i].active = (i == index)
- }
- // 获取选中的图层
- const selectedLayer = baseLayerConfig.value.list[index]
- const layerType = selectedLayer.code as MapLayerType
- // 切换图层
- switchLayer(layerType)
- }
- /**
- * 切换业务图层(基础数据)
- */
- const handleBusinessLayerChange = (index: number): void => {
- // 切换选中状态
- const item = businessLayerConfig.value.list[index]
- item.active = !item.active
- // 获取图层 URL
- const layerUrl = item.url as string | null
- if (layerUrl == null) {
- console.error('业务图层 URL 为空:', item.name)
- return
- }
- // 根据 active 状态添加或移除图层
- if (leafletMapRef.value != null) {
- if (item.active) {
- // 获取 token
- // const token = uni.getStorageSync('accessToken') as string | null
- const token = "eyJhbGciOiJIUzI1NiJ9.eyJyZWFsTmFtZSI6IueOi-itpuS9jSIsInN1YiI6IndhbmdqaW5nd2VpIiwiZGVwdElkIjoiMTAwMDA3IiwiZXhwIjoxNzYyOTMwMTAwLCJ1c2VySWQiOjI2OCwiaWF0IjoxNzYyODQzNzAwLCJqdGkiOiI3YzhhNmVmYi0xMDQyLTQ0NjAtOWNlMi1iZGMwMTVjNmZlMDYiLCJ1c2VybmFtZSI6IndhbmdqaW5nd2VpIn0.BlEnejBXkemhZY1nXd5xSAo8t5XoFSTnQVD09CQlVcI"
-
- // 添加图层
- leafletMapRef.value.$callMethod('addBusinessLayer', item.code, layerUrl, ARCGIS_CONFIG.baseUrl, token)
- console.log('添加业务图层:', item.name, ARCGIS_CONFIG.baseUrl, layerUrl )
- } else {
- // 移除图层
- leafletMapRef.value.$callMethod('removeBusinessLayer', item.code)
- console.log('移除业务图层:', item.name, ARCGIS_CONFIG.baseUrl, layerUrl)
- }
- }
- }
- // 批量添加标记点
- const addMarkers = (markers: MapMarker[]): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('addMarkers', markers)
- }
- }
- // 清除所有标记点
- const clearMarkers = (): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('clearMarkers')
- }
- }
- // 添加路线
- const addPolyline = (points: any[], color: string | null, width: number | null): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('addPolyline', points, color, width)
- }
- }
- // 添加圆形
- const addCircle = (
- latitude: number,
- longitude: number,
- radius: number,
- strokeColor: string | null,
- fillColor: string | null
- ): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('addCircle', latitude, longitude, radius, strokeColor, fillColor)
- }
- }
- // 添加多边形
- const addPolygon = (
- points: any[],
- strokeColor: string | null,
- fillColor: string | null
- ): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('addPolygon', points, strokeColor, fillColor)
- }
- }
- // 清除所有覆盖物
- const clearOverlays = (): void => {
- if (leafletMapRef.value != null) {
- leafletMapRef.value.$callMethod('clearOverlays')
- }
- }
- /**
- * 获取当前图层类型
- */
- const getCurrentLayerType = (): string => {
- return currentLayerType.value
- }
- /**
- * 获取图层配置信息
- */
- const getLayerConfig = (): any => {
- return {
- currentLayerType: currentLayerType.value,
- layerName: getLayerName(currentLayerType.value as MapLayerType),
- isReady: isMapReady.value
- }
- }
- // 暴露方法给父组件
- defineExpose({
- addMarkers,
- clearMarkers,
- addPolyline,
- addCircle,
- addPolygon,
- clearOverlays,
- handleLocation,
- switchLayer,
- getCurrentLayerType,
- getLayerConfig
- })
- </script>
- <style lang="scss">
- .map-view-container {
- position: relative;
- width: 100%;
- height: 100%;
- }
- .back-btn {
- position: absolute;
- left: 20rpx;
- top: 80rpx;
- width: 80rpx;
- height: 80rpx;
- background-color: #ffffff;
- border-radius: 40rpx;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
- justify-content: center;
- align-items: center;
- .back-icon {
- font-size: 36rpx;
- color: #333333;
- font-weight: bold;
- line-height: 80rpx;
- }
- }
- .map-controls {
- position: absolute;
- right: 20rpx;
- top: 80rpx;
- flex-direction: column;
- }
- .control-btn {
- width: 80rpx;
- height: 80rpx;
- margin-bottom: 15rpx;
- background-color: #ffffff;
- border-radius: 8rpx;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
- justify-content: center;
- align-items: center;
- .control-icon {
- width: 48rpx;
- height: 48rpx;
- }
- }
- /* 自定义图层弹窗样式 */
- .popup-mask {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, 0.5);
- z-index: 998;
- }
- .layer-popup {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- width: 70%;
- background-color: #ffffff;
- flex-direction: column;
- z-index: 999;
- transform: translateX(100%);
- transition-property: transform;
- transition-duration: 300ms;
- transition-timing-function: ease;
- .popup-header {
- height: 180rpx;
- padding: 80rpx 30rpx 0 30rpx;
- border-bottom: 1rpx solid #f0f0f0;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- .popup-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333333;
- }
- .popup-close {
- width: 60rpx;
- height: 60rpx;
- justify-content: center;
- align-items: center;
- .close-icon {
- font-size: 48rpx;
- color: #666666;
- line-height: 60rpx;
- }
- }
- }
- .popup-scroll {
- flex: 1;
- padding: 30rpx;
- }
- }
- .layer-popup-show {
- transform: translateX(0) !important;
- }
- .layer-section {
- margin-bottom: 40rpx;
- .section-title {
- font-size: 28rpx;
- color: #666666;
- margin-bottom: 20rpx;
- }
- .layer-list {
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: flex-start;
- .layer-item {
- position: relative;
- width: 200rpx;
- height: 200rpx;
- margin-bottom: 20rpx;
- margin-right: 20rpx;
- border-radius: 8rpx;
- overflow: hidden;
- border: 2rpx solid transparent;
- .layer-image {
- width: 200rpx;
- height: 200rpx;
- }
- .layer-name {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- padding: 10rpx;
- background-color: rgba(0, 0, 0, 0.6);
- font-size: 24rpx;
- color: #ffffff;
- }
- .layer-check {
- position: absolute;
- top: 10rpx;
- right: 10rpx;
- width: 44rpx;
- height: 44rpx;
- background-color: #007aff;
- border-radius: 22rpx;
- justify-content: center;
- align-items: center;
- .check-icon {
- font-size: 28rpx;
- color: #ffffff;
- font-weight: bold;
- }
- }
- }
- .layer-item-active {
- border-color: #007aff;
- }
- }
- }
- </style>
|