af-mobile-client-vue3 1.3.30 → 1.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +10 -0
- package/.cursorrules +60 -60
- package/.editorconfig +9 -9
- package/.env +10 -10
- package/.env.development +1 -1
- package/.env.production +1 -1
- package/.node-version +1 -1
- package/.vscode/extensions.json +12 -12
- package/.vscode/settings.json +68 -66
- package/CLAUDE.md +218 -189
- package/README.md +182 -182
- package/af-example-mobile-vue-web.iml +9 -9
- package/build/vite/index.ts +98 -98
- package/build/vite/optimize.ts +34 -34
- package/build/vite/vconsole.ts +47 -47
- package/commitlint.config.ts +32 -32
- package/compress.js +36 -36
- package/eslint.config.ts +31 -30
- package/index.html +23 -23
- package/mock/data.ts +20 -20
- package/mock/index.ts +7 -7
- package/mock/modules/prose.mock.ts +13 -13
- package/mock/modules/user.mock.ts +95 -152
- package/mock/util.ts +19 -19
- package/netlify.toml +12 -12
- package/package.json +135 -114
- package/postcss.config.ts +27 -27
- package/public/favicon.svg +4 -4
- package/public/safari-pinned-tab.svg +4 -4
- package/scripts/verifyCommit.js +19 -19
- package/src/App.vue +79 -79
- package/src/api/auth/index.ts +77 -0
- package/src/api/auth/types.ts +200 -0
- package/src/api/mock/index.ts +30 -30
- package/src/api/user/index.ts +40 -40
- package/src/assets/img/user/login/background-shadow-1.svg +20 -20
- package/src/assets/img/user/login/logo-background.svg +20 -20
- package/src/bootstrap.ts +26 -26
- package/src/components/core/BeautifulLoading/index.vue +52 -52
- package/src/components/core/ImageUploader/index.vue +251 -251
- package/src/components/core/NavBar/index.vue +53 -53
- package/src/components/core/Tabbar/index.vue +32 -32
- package/src/components/core/Uploader/index.vue +124 -124
- package/src/components/core/XGridDropOption/index.vue +154 -154
- package/src/components/core/XMultiSelect/index.vue +183 -183
- package/src/components/core/XSelect/index.vue +149 -149
- package/src/components/data/CardContainer/CardContainer.vue +118 -118
- package/src/components/data/CardContainer/CardHeader.vue +99 -99
- package/src/components/data/InfoDisplay/index.vue +132 -132
- package/src/components/data/UserDetail/api.ts +24 -24
- package/src/components/data/UserDetail/index.vue +620 -620
- package/src/components/data/UserDetail/recordEntries.ts +159 -159
- package/src/components/data/UserDetail/types.ts +26 -26
- package/src/components/data/XBadge/index.vue +82 -82
- package/src/components/data/XCellDetail/index.vue +105 -105
- package/src/components/data/XCellList/XCellList.md +432 -432
- package/src/components/data/XCellList/index.vue +1436 -1436
- package/src/components/data/XCellListFilter/QrScanner/index.vue +207 -207
- package/src/components/data/XCellListFilter/QrScanner/startScanAnimation.ts +53 -53
- package/src/components/data/XCellListFilter/VpnRecognition/index.vue +119 -119
- package/src/components/data/XCellListFilter/index.vue +705 -705
- package/src/components/data/XForm/index.vue +659 -659
- package/src/components/data/XFormGroup/doc/DeviceForm.vue +122 -122
- package/src/components/data/XFormGroup/doc/FormGroupDemo.vue +56 -56
- package/src/components/data/XFormGroup/doc/README.md +286 -286
- package/src/components/data/XFormGroup/doc/UserForm.vue +102 -102
- package/src/components/data/XFormGroup/index.vue +240 -240
- package/src/components/data/XFormItem/index.vue +1310 -1310
- package/src/components/data/XOlMap/README.md +227 -227
- package/src/components/data/XOlMap/XLocationPicker/index.vue +226 -226
- package/src/components/data/XOlMap/index.vue +1490 -1490
- package/src/components/data/XOlMap/types.ts +149 -149
- package/src/components/data/XOlMap/utils/{wgs84ToGcj02.js → wgs84ToGcj02.ts} +171 -154
- package/src/components/data/XReportForm/DateTimeSecondsPicker.vue +208 -208
- package/src/components/data/XReportForm/XReportFormJsonRender.vue +220 -220
- package/src/components/data/XReportForm/index.vue +1393 -1393
- package/src/components/data/XReportGrid/XAddReport/XAddReport.vue +198 -198
- package/src/components/data/XReportGrid/XAddReport/index.js +3 -3
- package/src/components/data/XReportGrid/XAddReport/index.md +53 -53
- package/src/components/data/XReportGrid/XAddReport/index.ts +10 -10
- package/src/components/data/XReportGrid/XReport.vue +960 -960
- package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
- package/src/components/data/XReportGrid/XReportDesign.vue +597 -597
- package/src/components/data/XReportGrid/XReportDrawer/XReportDrawer.vue +148 -148
- package/src/components/data/XReportGrid/XReportDrawer/index.js +3 -3
- package/src/components/data/XReportGrid/XReportDrawer/index.ts +10 -10
- package/src/components/data/XReportGrid/XReportJsonRender.vue +399 -399
- package/src/components/data/XReportGrid/XReportTrGroup.vue +592 -592
- package/src/components/data/XReportGrid/index.md +46 -46
- package/src/components/data/XReportGrid/print.js +184 -184
- package/src/components/data/XSignature/index.vue +284 -284
- package/src/components/data/XTag/index.vue +10 -10
- package/src/components/layout/NormalDataLayout/index.vue +69 -69
- package/src/components/layout/TabBarLayout/index.vue +40 -40
- package/src/composables/dark.ts +5 -5
- package/src/config/routes.ts +9 -9
- package/src/constants/index.ts +2 -2
- package/src/enums/requestEnum.ts +25 -25
- package/src/expression/ExpressionRunner.ts +28 -28
- package/src/expression/TestExpression.ts +510 -510
- package/src/expression/core/Delegate.ts +116 -116
- package/src/expression/core/Expression.ts +1359 -1359
- package/src/expression/core/Program.ts +985 -985
- package/src/expression/core/Token.ts +29 -29
- package/src/expression/enums/ExpressionType.ts +81 -81
- package/src/expression/enums/TokenType.ts +11 -11
- package/src/expression/exception/BreakWayException.ts +2 -2
- package/src/expression/exception/ContinueWayException.ts +2 -2
- package/src/expression/exception/ExpressionException.ts +29 -29
- package/src/expression/exception/ReturnWayException.ts +14 -14
- package/src/expression/exception/ServiceException.ts +22 -22
- package/src/expression/instances/JSONArray.ts +52 -52
- package/src/expression/instances/JSONObject.ts +118 -118
- package/src/expression/instances/LogicConsole.ts +31 -31
- package/src/font-style/font.css +4 -4
- package/src/hooks/useBoolean.ts +26 -26
- package/src/hooks/useCommon.ts +9 -9
- package/src/hooks/useLoading.ts +16 -16
- package/src/hooks/useLogin.ts +97 -97
- package/src/icons/svg/check-in.svg +32 -32
- package/src/icons/svg/dark.svg +4 -4
- package/src/icons/svg/github.svg +4 -4
- package/src/icons/svg/light.svg +4 -4
- package/src/icons/svg/link.svg +4 -4
- package/src/icons/svgo.yml +22 -22
- package/src/index.ts +4 -0
- package/src/layout/GridView/index.vue +16 -16
- package/src/layout/PageLayout.vue +9 -9
- package/src/layout/SingleLayout.vue +9 -9
- package/src/locales/en-US.json +128 -128
- package/src/locales/zh-CN.json +128 -128
- package/src/logic/LogicRunner.ts +67 -67
- package/src/logic/TestLogic.ts +13 -13
- package/src/logic/plugins/common/DateTools.ts +35 -35
- package/src/logic/plugins/common/VueTools.ts +30 -30
- package/src/logic/plugins/index.ts +7 -7
- package/src/main.ts +44 -44
- package/src/plugins/AppData.ts +38 -38
- package/src/plugins/GetLoginInfoService.ts +10 -10
- package/src/plugins/collectIcons.ts +10 -10
- package/src/plugins/index.ts +11 -11
- package/src/router/README.md +8 -8
- package/src/router/external-routes.ts +60 -0
- package/src/router/guards.ts +131 -59
- package/src/router/index.ts +35 -35
- package/src/router/invoiceRoutes.ts +33 -33
- package/src/router/routes.ts +426 -347
- package/src/services/api/Login.ts +6 -6
- package/src/services/api/common.ts +109 -109
- package/src/services/api/index.ts +7 -7
- package/src/services/api/manage.ts +8 -8
- package/src/services/api/search.ts +16 -16
- package/src/services/api/user.ts +17 -17
- package/src/services/restTools.ts +56 -56
- package/src/services/v3Api.ts +147 -147
- package/src/stores/index.ts +13 -13
- package/src/stores/modules/counter.ts +19 -19
- package/src/stores/modules/homeApp.ts +55 -55
- package/src/stores/modules/routeCache.ts +22 -23
- package/src/stores/modules/setting.ts +87 -87
- package/src/stores/modules/user.ts +326 -235
- package/src/stores/mutation-type.ts +12 -7
- package/src/styles/app.less +36 -36
- package/src/styles/login.less +109 -109
- package/src/styles/var.less +25 -25
- package/src/types/auth.ts +85 -0
- package/src/types/env.d.ts +16 -16
- package/src/types/platform.ts +194 -0
- package/src/types/settings.ts +1 -1
- package/src/types/vue-router.d.ts +13 -9
- package/src/utils/Storage.ts +124 -124
- package/src/utils/authority-utils.ts +84 -84
- package/src/utils/common.ts +41 -41
- package/src/utils/crypto.ts +39 -39
- package/src/utils/dataUtil.ts +42 -42
- package/src/utils/dictUtil.ts +52 -52
- package/src/utils/http/index.ts +201 -199
- package/src/utils/i18n.ts +72 -72
- package/src/utils/indexedDB.ts +195 -195
- package/src/utils/inline-px-to-vw.ts +28 -28
- package/src/utils/mobileUtil.ts +33 -34
- package/src/utils/platform-auth.ts +134 -0
- package/src/utils/progress.ts +19 -19
- package/src/utils/routerUtil.ts +271 -271
- package/src/utils/runEvalFunction.ts +13 -13
- package/src/utils/secureStorage.ts +70 -71
- package/src/utils/set-page-title.ts +5 -5
- package/src/utils/validate.ts +6 -6
- package/src/views/chat/index.vue +153 -153
- package/src/views/common/Forbidden.vue +97 -0
- package/src/views/common/LoadError.vue +63 -63
- package/src/views/common/NotFound.vue +67 -67
- package/src/views/component/EvaluateRecordView/index.vue +40 -40
- package/src/views/component/IconifyView/index.vue +504 -504
- package/src/views/component/UserDetailView/UserDetailPage.vue +77 -77
- package/src/views/component/UserDetailView/index.vue +234 -234
- package/src/views/component/XCellDetailView/index.vue +217 -217
- package/src/views/component/XCellListView/index.vue +108 -129
- package/src/views/component/XFormAppraiseView/index.vue +174 -174
- package/src/views/component/XFormGroupView/index.vue +78 -82
- package/src/views/component/XFormView/index.vue +27 -27
- package/src/views/component/XOlMapView/XLocationPicker/index.vue +118 -118
- package/src/views/component/XOlMapView/index.vue +434 -434
- package/src/views/component/XOlMapView/testData.ts +64 -64
- package/src/views/component/XReportFormIframeView/index.vue +47 -47
- package/src/views/component/XReportFormView/index.vue +13 -13
- package/src/views/component/XReportGridView/index.vue +17 -17
- package/src/views/component/XRequestView/index.vue +234 -234
- package/src/views/component/XSignatureView/index.vue +50 -50
- package/src/views/component/index.vue +181 -181
- package/src/views/component/menu.vue +117 -117
- package/src/views/component/notice.vue +46 -46
- package/src/views/component/topNav.vue +36 -36
- package/src/views/external/index.vue +152 -0
- package/src/views/invoiceShow/index.vue +61 -61
- package/src/views/loading/AuthLoading.vue +345 -0
- package/src/views/user/login/ForgetPasswordForm.vue +94 -94
- package/src/views/user/login/LoginForm.vue +350 -347
- package/src/views/user/login/LoginTitle.vue +76 -76
- package/src/views/user/login/LoginWave.vue +109 -109
- package/src/views/user/login/index.vue +22 -22
- package/src/views/user/my/comm/ModifyPassword.vue +346 -346
- package/src/views/user/my/index.vue +507 -507
- package/src/views/user/register/index.vue +952 -952
- package/src/views/userRecords/AbnormalAlarmRecords.vue +21 -21
- package/src/views/userRecords/CardReplacementRecords.vue +21 -21
- package/src/views/userRecords/ChangeRecords.vue +19 -19
- package/src/views/userRecords/CommandViewRecords.vue +20 -20
- package/src/views/userRecords/GasCompensationRecords.vue +20 -20
- package/src/views/userRecords/InstrumentCollectionRecords.vue +21 -21
- package/src/views/userRecords/MeterRecords.vue +20 -20
- package/src/views/userRecords/OperateRecords.vue +51 -51
- package/src/views/userRecords/OtherChargeRecords.vue +19 -19
- package/src/views/userRecords/PaymentRecords.vue +28 -28
- package/src/views/userRecords/PriceAdjustmentRecords.vue +19 -19
- package/src/views/userRecords/ReplacementRecords.vue +19 -19
- package/src/views/userRecords/SafetyRecords.vue +19 -19
- package/src/views/userRecords/TransactionRecords.vue +21 -21
- package/src/views/userRecords/TransferRecords.vue +19 -19
- package/src/views/userRecords/operateRecordDetail/index.vue +316 -316
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AddUserDetail.vue +124 -124
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AdvanceDeliveryDetail.vue +88 -88
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsCancelDetail.vue +205 -205
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsDetail.vue +192 -192
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankDkDetail.vue +192 -192
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankPayDetail.vue +192 -192
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BlacklistDetail.vue +153 -153
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CancellationDetail.vue +101 -101
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterCancelDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterDetail.vue +153 -153
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardOverUserDetail.vue +153 -153
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterCancelDetail.vue +166 -166
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterDetail.vue +205 -205
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/DisableManageDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/EnableManageDetail.vue +114 -114
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FaZheChangeDetail.vue +124 -124
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FeeDeductionDetail.vue +153 -153
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/GasPriceChangeDetail.vue +126 -126
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/InputtorChangeDetail.vue +126 -126
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterCancelDetail.vue +114 -114
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotOpenDetail.vue +88 -88
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineCardDetail.vue +101 -101
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterCancelDetail.vue +218 -218
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterDetail.vue +153 -153
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OffGasAddGasDetail.vue +140 -140
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeCancelDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeDetail.vue +114 -114
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OverUserChangeDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReBillDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/RefundDetail.vue +114 -114
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageCancelDetail.vue +127 -127
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageDetail.vue +114 -114
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/SaleCardGasDetail.vue +140 -140
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageCancelDetail.vue +152 -152
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageDetail.vue +178 -178
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/UserChangeDetail.vue +123 -123
- package/src/views/userRecords/operateRecordDetail/operateRecordDetails/WechatPayDetail.vue +192 -192
- package/src/views/userRecords/types.ts +66 -66
- package/tsconfig.json +39 -39
- package/uno.config.ts +82 -82
- package/vite.config.ts +119 -118
- package/src/router/types.ts +0 -7
- package/src/utils/wechatUtil.ts +0 -9
|
@@ -1,1490 +1,1490 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* OpenLayers地图组件
|
|
4
|
-
* 支持多种底图切换:
|
|
5
|
-
* - 高德地图(矢量)
|
|
6
|
-
* - 高德卫星图
|
|
7
|
-
* - 天地图(矢量)
|
|
8
|
-
* - 天地图卫星图
|
|
9
|
-
*/
|
|
10
|
-
import type {
|
|
11
|
-
InitParams,
|
|
12
|
-
PhoneLocationStatus,
|
|
13
|
-
PointData,
|
|
14
|
-
PointLayerConfig,
|
|
15
|
-
TrackData,
|
|
16
|
-
WebGLPointOptions,
|
|
17
|
-
WMSLayerConfig,
|
|
18
|
-
WMSOptions,
|
|
19
|
-
} from './types'
|
|
20
|
-
import locationIcon from '@af-mobile-client-vue3/assets/img/component/positioning.png'
|
|
21
|
-
import { getConfigByName } from '@af-mobile-client-vue3/services/api/common'
|
|
22
|
-
import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
|
|
23
|
-
import { Map, View } from 'ol'
|
|
24
|
-
import { defaults as defaultControls, ScaleLine } from 'ol/control'
|
|
25
|
-
import Feature from 'ol/Feature'
|
|
26
|
-
import LineString from 'ol/geom/LineString'
|
|
27
|
-
import Point from 'ol/geom/Point'
|
|
28
|
-
import { defaults as defaultInteractions } from 'ol/interaction'
|
|
29
|
-
import { Image as ImageLayer, Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
|
30
|
-
import { fromLonLat, toLonLat } from 'ol/proj'
|
|
31
|
-
import { ImageWMS, Vector as VectorSource, XYZ } from 'ol/source'
|
|
32
|
-
import { Circle, Fill, Icon, Stroke, Style, Text } from 'ol/style'
|
|
33
|
-
import { Button } from 'vant'
|
|
34
|
-
import { getCurrentInstance, onUnmounted, ref } from 'vue'
|
|
35
|
-
import { wgs84ToGcj02Projection } from './utils/wgs84ToGcj02'
|
|
36
|
-
import 'vant/lib/index.css'
|
|
37
|
-
|
|
38
|
-
// 在 script setup 中添加
|
|
39
|
-
const emit = defineEmits<{
|
|
40
|
-
(e: 'centerChange', center: [number, number]): void
|
|
41
|
-
}>()
|
|
42
|
-
|
|
43
|
-
// 获取当前组件实例
|
|
44
|
-
const instance = getCurrentInstance()
|
|
45
|
-
|
|
46
|
-
// 存储初始化参数
|
|
47
|
-
const mapParams = ref<InitParams>({})
|
|
48
|
-
|
|
49
|
-
/** 地图容器引用 */
|
|
50
|
-
const mapRef = ref<HTMLDivElement>()
|
|
51
|
-
/** 是否为预览模式 */
|
|
52
|
-
const preview = ref(false)
|
|
53
|
-
/** 地图实例 */
|
|
54
|
-
let map: Map | null = null
|
|
55
|
-
/** 当前底图类型 */
|
|
56
|
-
const currentMapType = ref<string>('tianditu')
|
|
57
|
-
/** 控制图层面板显示状态 */
|
|
58
|
-
const showControls = ref<boolean>(false)
|
|
59
|
-
|
|
60
|
-
/** 图层选项配置 */
|
|
61
|
-
const layerOptions = [
|
|
62
|
-
{ text: '高德地图', value: 'gaode' },
|
|
63
|
-
{ text: '高德卫星', value: 'gaodeSatellite' },
|
|
64
|
-
{ text: '天地图', value: 'tianditu' },
|
|
65
|
-
{ text: '天地图卫星', value: 'tianditusatellite' },
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
/** 存储所有底图图层 */
|
|
69
|
-
const baseMaps: Record<string, TileLayer<XYZ>> = {}
|
|
70
|
-
|
|
71
|
-
/** 存储 WMS 图层 */
|
|
72
|
-
const wmsLayers: Record<string, ImageLayer<ImageWMS>> = {}
|
|
73
|
-
|
|
74
|
-
/** WMS 图层状态 */
|
|
75
|
-
const wmsLayerStatus = ref<WMSLayerConfig[]>([])
|
|
76
|
-
|
|
77
|
-
/** 存储点位图层 */
|
|
78
|
-
const vectorLayers: Record<number, VectorLayer<VectorSource>> = {}
|
|
79
|
-
const pointLayerStatus = ref<PointLayerConfig[]>([])
|
|
80
|
-
|
|
81
|
-
const tiandityKey = ref()
|
|
82
|
-
const gaodeKey = ref()
|
|
83
|
-
/** 导航模式 是否正在跟随定位 */
|
|
84
|
-
const isFollowingLocation = ref(false)
|
|
85
|
-
/** 定位定时器 */
|
|
86
|
-
let locationTimer: ReturnType<typeof setInterval> | null = null
|
|
87
|
-
/** 位置图标图层 */
|
|
88
|
-
let locationLayer: VectorLayer<VectorSource> | null = null
|
|
89
|
-
|
|
90
|
-
/** 存储轨迹图层 */
|
|
91
|
-
const trackLayers: Record<number, VectorLayer<VectorSource>> = {}
|
|
92
|
-
const trackLayerStatus = ref<TrackData[]>([])
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 创建位置图标图层
|
|
96
|
-
*/
|
|
97
|
-
function createLocationLayer(): VectorLayer<VectorSource> {
|
|
98
|
-
const source = new VectorSource()
|
|
99
|
-
return new VectorLayer({
|
|
100
|
-
source,
|
|
101
|
-
zIndex: 10, // 确保位置图标在最上层
|
|
102
|
-
})
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 更新位置图标
|
|
107
|
-
*/
|
|
108
|
-
function updateLocationMarker(center: [number, number]): void {
|
|
109
|
-
if (!map) {
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
if (!locationLayer) {
|
|
113
|
-
locationLayer = createLocationLayer()
|
|
114
|
-
map?.addLayer(locationLayer)
|
|
115
|
-
}
|
|
116
|
-
const source = locationLayer.getSource()
|
|
117
|
-
if (!source) {
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
// 清除现有图标
|
|
121
|
-
source.clear()
|
|
122
|
-
|
|
123
|
-
// 创建新的位置图标要素
|
|
124
|
-
const feature = new Feature({
|
|
125
|
-
geometry: new Point(fromLonLat(center)),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// 设置图标样式
|
|
129
|
-
const style = new Style({
|
|
130
|
-
image: new Icon({
|
|
131
|
-
src: locationIcon,
|
|
132
|
-
scale: 0.2,
|
|
133
|
-
anchor: [0.5, 0.5],
|
|
134
|
-
anchorXUnits: 'fraction',
|
|
135
|
-
anchorYUnits: 'fraction',
|
|
136
|
-
crossOrigin: 'anonymous',
|
|
137
|
-
}),
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
feature.setStyle(style)
|
|
141
|
-
source.addFeature(feature)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 切换地图图层
|
|
146
|
-
* @param type - 图层类型
|
|
147
|
-
*/
|
|
148
|
-
function handleMapChange(type: string): void {
|
|
149
|
-
// 隐藏所有图层
|
|
150
|
-
Object.keys(baseMaps).forEach((key) => {
|
|
151
|
-
baseMaps[key].setVisible(false)
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
// 根据选择显示对应图层
|
|
155
|
-
switch (type) {
|
|
156
|
-
case 'gaodeSatellite':
|
|
157
|
-
baseMaps.gaodeSatellite.setVisible(true)
|
|
158
|
-
baseMaps.gaodelabelLayer.setVisible(true)
|
|
159
|
-
break
|
|
160
|
-
case 'tianditu':
|
|
161
|
-
baseMaps.tianditu.setVisible(true)
|
|
162
|
-
baseMaps.tianditulabel.setVisible(true)
|
|
163
|
-
break
|
|
164
|
-
case 'tianditusatellite':
|
|
165
|
-
baseMaps.tianditusatellite.setVisible(true)
|
|
166
|
-
baseMaps.tianditusatlabel.setVisible(true)
|
|
167
|
-
break
|
|
168
|
-
default:
|
|
169
|
-
baseMaps.gaode.setVisible(true)
|
|
170
|
-
}
|
|
171
|
-
currentMapType.value = type
|
|
172
|
-
|
|
173
|
-
// 强制更新地图视图,确保切换后的图层显示正确
|
|
174
|
-
if (map) {
|
|
175
|
-
map.updateSize()
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 初始化底图图层
|
|
181
|
-
* @param tianDiTuKey - 天地图密钥
|
|
182
|
-
*/
|
|
183
|
-
function initializeLayers(tianDiTuKey = ''): void {
|
|
184
|
-
try {
|
|
185
|
-
// 高德地图
|
|
186
|
-
baseMaps.gaode = new TileLayer({
|
|
187
|
-
source: new XYZ({
|
|
188
|
-
url: 'https://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}',
|
|
189
|
-
crossOrigin: 'anonymous',
|
|
190
|
-
projection: 'EPSG:3857',
|
|
191
|
-
}),
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
// 高德卫星图
|
|
195
|
-
baseMaps.gaodeSatellite = new TileLayer({
|
|
196
|
-
source: new XYZ({
|
|
197
|
-
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
|
|
198
|
-
crossOrigin: 'anonymous',
|
|
199
|
-
projection: 'EPSG:3857',
|
|
200
|
-
}),
|
|
201
|
-
visible: false,
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// 高德标注图层
|
|
205
|
-
baseMaps.gaodelabelLayer = new TileLayer({
|
|
206
|
-
source: new XYZ({
|
|
207
|
-
url: 'https://webst02.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
|
|
208
|
-
crossOrigin: 'anonymous',
|
|
209
|
-
projection: 'EPSG:3857',
|
|
210
|
-
}),
|
|
211
|
-
visible: false,
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// 天地图矢量图层
|
|
215
|
-
baseMaps.tianditu = new TileLayer({
|
|
216
|
-
source: new XYZ({
|
|
217
|
-
url: 'https://t0.tianditu.gov.cn/vec_w/wmts?'
|
|
218
|
-
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&'
|
|
219
|
-
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
220
|
-
projection: wgs84ToGcj02Projection,
|
|
221
|
-
}),
|
|
222
|
-
visible: false,
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
// 天地图标注图层
|
|
226
|
-
baseMaps.tianditulabel = new TileLayer({
|
|
227
|
-
source: new XYZ({
|
|
228
|
-
url: 'https://t0.tianditu.gov.cn/cva_w/wmts?'
|
|
229
|
-
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&'
|
|
230
|
-
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
231
|
-
projection: wgs84ToGcj02Projection,
|
|
232
|
-
}),
|
|
233
|
-
visible: false,
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
// 天地图卫星图层
|
|
237
|
-
baseMaps.tianditusatellite = new TileLayer({
|
|
238
|
-
source: new XYZ({
|
|
239
|
-
url: 'https://t0.tianditu.gov.cn/img_w/wmts?'
|
|
240
|
-
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&'
|
|
241
|
-
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
242
|
-
projection: wgs84ToGcj02Projection,
|
|
243
|
-
}),
|
|
244
|
-
visible: false,
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
// 天地图卫星标注
|
|
248
|
-
baseMaps.tianditusatlabel = new TileLayer({
|
|
249
|
-
source: new XYZ({
|
|
250
|
-
url: 'https://t0.tianditu.gov.cn/cia_w/wmts?'
|
|
251
|
-
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&'
|
|
252
|
-
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
253
|
-
projection: wgs84ToGcj02Projection,
|
|
254
|
-
}),
|
|
255
|
-
visible: false,
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
console.error('初始化地图图层失败:', error)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 获取地址信息
|
|
265
|
-
* @param location - 经纬度坐标
|
|
266
|
-
*/
|
|
267
|
-
async function getAddressInfo(location: [number, number]): Promise<string> {
|
|
268
|
-
try {
|
|
269
|
-
const key = gaodeKey.value
|
|
270
|
-
|
|
271
|
-
if (!key) {
|
|
272
|
-
return '获取地址失败: 未配置密钥'
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 高德逆地址编码请求说明 https://amap.apifox.cn/api-14551463
|
|
276
|
-
const response = await fetch(
|
|
277
|
-
`https://restapi.amap.com/v3/geocode/regeo?location=${location[0].toFixed(6)},${location[1].toFixed(6)}&key=${key}&output=JSON`,
|
|
278
|
-
)
|
|
279
|
-
const data = await response.json()
|
|
280
|
-
|
|
281
|
-
if (data.status === '1' && data.regeocode) {
|
|
282
|
-
return data.regeocode.formatted_address
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// 处理错误情况
|
|
286
|
-
if (data.infocode === '10001') {
|
|
287
|
-
return '获取地址失败: key 无效'
|
|
288
|
-
}
|
|
289
|
-
if (data.infocode === '10002') {
|
|
290
|
-
return '获取地址失败: key 未配置平台'
|
|
291
|
-
}
|
|
292
|
-
return `获取地址失败: ${data.info || '未知错误'}`
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
return '获取地址失败: 网络错误'
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* 处理地图移动结束事件
|
|
301
|
-
*/
|
|
302
|
-
async function handleMoveEnd() {
|
|
303
|
-
if (!map)
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
const view = map.getView()
|
|
307
|
-
const center = view.getCenter()
|
|
308
|
-
if (!center)
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
// 转换坐标为经纬度
|
|
312
|
-
const lonLat = toLonLat(center)
|
|
313
|
-
const formattedCenter: [number, number] = [Number(lonLat[0].toFixed(6)), Number(lonLat[1].toFixed(6))]
|
|
314
|
-
|
|
315
|
-
// 直接发送事件,让父组件决定是否处理
|
|
316
|
-
emit('centerChange', formattedCenter)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* 初始化地图
|
|
321
|
-
* @param params - 初始化参数
|
|
322
|
-
*/
|
|
323
|
-
function init(params: InitParams = {}): Promise<void> {
|
|
324
|
-
return new Promise((resolve) => {
|
|
325
|
-
if (!mapRef.value) {
|
|
326
|
-
resolve()
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// 保存初始化参数
|
|
331
|
-
mapParams.value = params
|
|
332
|
-
|
|
333
|
-
// 设置默认参数
|
|
334
|
-
const {
|
|
335
|
-
center = [116.404, 39.915],
|
|
336
|
-
zoom = 10,
|
|
337
|
-
maxZoom = 18,
|
|
338
|
-
minZoom = 4,
|
|
339
|
-
isPreview = false,
|
|
340
|
-
} = params
|
|
341
|
-
// 设置预览模式
|
|
342
|
-
preview.value = isPreview
|
|
343
|
-
try {
|
|
344
|
-
getConfigByName('webConfig', (res) => {
|
|
345
|
-
const tianDiTuKey = res.tianDiTuKey || 'c16876b28898637c0a1a68b3fa410504'
|
|
346
|
-
const amapKey = res.amapKey || '5ebabc4536d4b42e0dd1e20175cca8ab'
|
|
347
|
-
|
|
348
|
-
tiandityKey.value = tianDiTuKey
|
|
349
|
-
gaodeKey.value = amapKey
|
|
350
|
-
// 初始化所有底图图层
|
|
351
|
-
initializeLayers(tianDiTuKey)
|
|
352
|
-
|
|
353
|
-
// 创建地图实例 - 加载所有底图图层,但默认只显示高德地图
|
|
354
|
-
map = new Map({
|
|
355
|
-
target: mapRef.value,
|
|
356
|
-
layers: Object.values(baseMaps), // 加载所有底图图层
|
|
357
|
-
view: new View({
|
|
358
|
-
center: fromLonLat(center),
|
|
359
|
-
zoom,
|
|
360
|
-
projection: 'EPSG:3857',
|
|
361
|
-
maxZoom,
|
|
362
|
-
minZoom,
|
|
363
|
-
}),
|
|
364
|
-
controls: defaultControls({
|
|
365
|
-
zoom: false,
|
|
366
|
-
rotate: false,
|
|
367
|
-
attribution: false,
|
|
368
|
-
}).extend([
|
|
369
|
-
new ScaleLine({
|
|
370
|
-
units: 'metric',
|
|
371
|
-
className: 'ol-scale-line',
|
|
372
|
-
}),
|
|
373
|
-
]),
|
|
374
|
-
interactions: defaultInteractions({
|
|
375
|
-
altShiftDragRotate: false,
|
|
376
|
-
pinchRotate: false,
|
|
377
|
-
}),
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
// 更新地图大小,确保地图正确渲染
|
|
381
|
-
setTimeout(() => {
|
|
382
|
-
if (map) {
|
|
383
|
-
map.updateSize()
|
|
384
|
-
// 确保默认图层正确显示
|
|
385
|
-
handleMapChange('tianditu')
|
|
386
|
-
// 地图初始化完成后解析 Promise
|
|
387
|
-
resolve()
|
|
388
|
-
}
|
|
389
|
-
}, 200)
|
|
390
|
-
|
|
391
|
-
// 监听地图移动结束事件
|
|
392
|
-
map.on('moveend', handleMoveEnd)
|
|
393
|
-
|
|
394
|
-
// 设置鼠标样式
|
|
395
|
-
if (mapRef.value) {
|
|
396
|
-
mapRef.value.style.cursor = 'grab'
|
|
397
|
-
// 监听地图事件
|
|
398
|
-
const mapElement = mapRef.value
|
|
399
|
-
|
|
400
|
-
// 鼠标按下时
|
|
401
|
-
mapElement.addEventListener('mousedown', () => {
|
|
402
|
-
mapElement.style.cursor = 'grabbing'
|
|
403
|
-
// 用户开始拖动地图,取消跟随定位
|
|
404
|
-
if (locationTimer) {
|
|
405
|
-
isFollowingLocation.value = false
|
|
406
|
-
}
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
// 触摸开始时
|
|
410
|
-
mapElement.addEventListener('touchstart', () => {
|
|
411
|
-
// 用户开始拖动地图,取消跟随定位
|
|
412
|
-
if (locationTimer) {
|
|
413
|
-
isFollowingLocation.value = false
|
|
414
|
-
}
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
// 鼠标释放时
|
|
418
|
-
mapElement.addEventListener('mouseup', () => {
|
|
419
|
-
mapElement.style.cursor = 'grab'
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
// 鼠标离开地图时
|
|
423
|
-
mapElement.addEventListener('mouseleave', () => {
|
|
424
|
-
mapElement.style.cursor = 'grab'
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
catch (error) {
|
|
430
|
-
console.error('地图初始化失败:', error)
|
|
431
|
-
resolve()
|
|
432
|
-
}
|
|
433
|
-
})
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* 获取地图实例
|
|
438
|
-
* @returns OpenLayers Map 实例
|
|
439
|
-
*/
|
|
440
|
-
function getMap(): Map | null {
|
|
441
|
-
return map
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* 设置地图中心点
|
|
446
|
-
* @param center - 经纬度坐标 [经度, 纬度]
|
|
447
|
-
* @param animate - 是否使用动画效果,默认true
|
|
448
|
-
*/
|
|
449
|
-
function setCenter(center: [number, number], animate = true): void {
|
|
450
|
-
if (!map)
|
|
451
|
-
return
|
|
452
|
-
|
|
453
|
-
const view = map.getView()
|
|
454
|
-
if (animate) {
|
|
455
|
-
view.animate({
|
|
456
|
-
center: fromLonLat(center),
|
|
457
|
-
duration: 500,
|
|
458
|
-
})
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
view.setCenter(fromLonLat(center))
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* 设置地图缩放级别
|
|
467
|
-
* @param zoom - 缩放级别
|
|
468
|
-
* @param animate - 是否使用动画效果,默认true
|
|
469
|
-
*/
|
|
470
|
-
function setZoom(zoom: number, animate = true): void {
|
|
471
|
-
if (!map)
|
|
472
|
-
return
|
|
473
|
-
|
|
474
|
-
const view = map.getView()
|
|
475
|
-
if (animate) {
|
|
476
|
-
view.animate({
|
|
477
|
-
zoom,
|
|
478
|
-
duration: 500,
|
|
479
|
-
})
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
view.setZoom(zoom)
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* 获取当前地图缩放级别
|
|
488
|
-
* @returns 当前缩放级别
|
|
489
|
-
*/
|
|
490
|
-
function getZoom(): number {
|
|
491
|
-
if (!map)
|
|
492
|
-
return 0
|
|
493
|
-
return map.getView().getZoom() || 0
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* 设置地图中心点和缩放级别
|
|
498
|
-
* @param center - 经纬度坐标 [经度, 纬度]
|
|
499
|
-
* @param zoom - 缩放级别
|
|
500
|
-
* @param animate - 是否使用动画效果,默认true
|
|
501
|
-
*/
|
|
502
|
-
function setCenterAndZoom(center: [number, number], zoom: number, animate = true): void {
|
|
503
|
-
if (!map)
|
|
504
|
-
return
|
|
505
|
-
|
|
506
|
-
const view = map.getView()
|
|
507
|
-
if (animate) {
|
|
508
|
-
view.animate({
|
|
509
|
-
center: fromLonLat(center),
|
|
510
|
-
zoom,
|
|
511
|
-
duration: 500,
|
|
512
|
-
})
|
|
513
|
-
}
|
|
514
|
-
else {
|
|
515
|
-
view.setCenter(fromLonLat(center))
|
|
516
|
-
view.setZoom(zoom)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* 创建点位要素
|
|
522
|
-
* @param point - 点位数据
|
|
523
|
-
* @param icon - 图标URL
|
|
524
|
-
* @param iconAnchor - 图标锚点
|
|
525
|
-
* @returns 返回要素实例
|
|
526
|
-
*/
|
|
527
|
-
function createPointFeature(point: PointData, icon: string, iconAnchor: [number, number] = [0.5, 1], scale: number = 0.5): Feature {
|
|
528
|
-
const feature = new Feature({
|
|
529
|
-
geometry: new Point(fromLonLat([point.longitude, point.latitude])),
|
|
530
|
-
properties: point,
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
const style = new Style({
|
|
534
|
-
image: new Icon({
|
|
535
|
-
src: icon,
|
|
536
|
-
scale,
|
|
537
|
-
anchor: iconAnchor,
|
|
538
|
-
anchorXUnits: 'fraction',
|
|
539
|
-
anchorYUnits: 'fraction',
|
|
540
|
-
crossOrigin: 'anonymous',
|
|
541
|
-
}),
|
|
542
|
-
text: new Text({
|
|
543
|
-
text: point.title || '',
|
|
544
|
-
offsetY: -35,
|
|
545
|
-
font: '12px sans-serif',
|
|
546
|
-
fill: new Fill({
|
|
547
|
-
color: '#333',
|
|
548
|
-
}),
|
|
549
|
-
stroke: new Stroke({
|
|
550
|
-
color: '#fff',
|
|
551
|
-
width: 2,
|
|
552
|
-
}),
|
|
553
|
-
}),
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
feature.setStyle(style)
|
|
557
|
-
return feature
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* 创建点位图层
|
|
562
|
-
* @param config - 图层配置
|
|
563
|
-
* @returns 返回图层实例
|
|
564
|
-
*/
|
|
565
|
-
function createPointLayer(config: PointLayerConfig): VectorLayer<VectorSource> {
|
|
566
|
-
const vectorSource = new VectorSource()
|
|
567
|
-
const vectorLayer = new VectorLayer({
|
|
568
|
-
source: vectorSource,
|
|
569
|
-
visible: config.show,
|
|
570
|
-
zIndex: config.id === undefined ? 1 : 3, // 根据是否有ID决定层级
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
// 添加点位要素
|
|
574
|
-
const addFeatures = (data: PointData[]) => {
|
|
575
|
-
// 清除现有要素
|
|
576
|
-
vectorSource.clear()
|
|
577
|
-
// 添加新的点位要素
|
|
578
|
-
data.forEach((point) => {
|
|
579
|
-
const feature = createPointFeature(point, config.icon, config.iconAnchor, config.scale)
|
|
580
|
-
vectorSource.addFeature(feature)
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// 添加点击事件处理
|
|
585
|
-
if (config.onClick) {
|
|
586
|
-
map?.on('click', (event) => {
|
|
587
|
-
const feature = map.forEachFeatureAtPixel(event.pixel, feature => feature, {
|
|
588
|
-
layerFilter: layer => layer === vectorLayer,
|
|
589
|
-
})
|
|
590
|
-
if (feature) {
|
|
591
|
-
const properties = feature.getProperties()
|
|
592
|
-
const { geometry, ...pointData } = properties
|
|
593
|
-
config.onClick(pointData as PointData, event)
|
|
594
|
-
}
|
|
595
|
-
})
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// 监听图层可见性变化
|
|
599
|
-
vectorLayer.on('change:visible', async () => {
|
|
600
|
-
if (vectorLayer.getVisible() && config.dataProvider) {
|
|
601
|
-
try {
|
|
602
|
-
const data = await config.dataProvider()
|
|
603
|
-
addFeatures(data)
|
|
604
|
-
}
|
|
605
|
-
catch (error) {
|
|
606
|
-
console.error('获取点位数据失败:', error)
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
// 初始加载数据
|
|
612
|
-
if (config.show && config.dataProvider) {
|
|
613
|
-
const result = config.dataProvider()
|
|
614
|
-
if (result instanceof Promise) {
|
|
615
|
-
result.then((data) => {
|
|
616
|
-
addFeatures(data)
|
|
617
|
-
}).catch((error) => {
|
|
618
|
-
console.error('获取初始点位数据失败:', error)
|
|
619
|
-
})
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
addFeatures(result)
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return vectorLayer
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* 添加点位图层
|
|
631
|
-
* @param config - 图层配置
|
|
632
|
-
* @returns 返回图层实例
|
|
633
|
-
*/
|
|
634
|
-
function addPointLayer(config: PointLayerConfig): VectorLayer<VectorSource> | null {
|
|
635
|
-
if (!map)
|
|
636
|
-
return null
|
|
637
|
-
|
|
638
|
-
const vectorLayer = createPointLayer(config)
|
|
639
|
-
map.addLayer(vectorLayer)
|
|
640
|
-
vectorLayers[config.id] = vectorLayer
|
|
641
|
-
|
|
642
|
-
// 更新图层状态
|
|
643
|
-
if (config.showInControl !== false) {
|
|
644
|
-
const existingIndex = pointLayerStatus.value.findIndex(layer => layer.id === config.id)
|
|
645
|
-
if (existingIndex === -1) {
|
|
646
|
-
pointLayerStatus.value.push(config)
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
pointLayerStatus.value[existingIndex] = config
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return vectorLayer
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* 添加海量点图层
|
|
658
|
-
* @param config - 图层配置
|
|
659
|
-
*/
|
|
660
|
-
function addWebGLPoints(config: WebGLPointOptions): void {
|
|
661
|
-
if (!map)
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
const vectorSource = new VectorSource()
|
|
665
|
-
const vectorLayer = new VectorLayer({
|
|
666
|
-
source: vectorSource,
|
|
667
|
-
visible: config.show,
|
|
668
|
-
zIndex: config.id === undefined ? 1 : 3,
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
// 添加点位要素
|
|
672
|
-
const addFeatures = (data: PointData[]) => {
|
|
673
|
-
// 清除现有要素
|
|
674
|
-
vectorSource.clear()
|
|
675
|
-
// 添加新的点位要素
|
|
676
|
-
data.forEach((point) => {
|
|
677
|
-
const feature = createPointFeature(point, config.icon, config.iconAnchor)
|
|
678
|
-
vectorSource.addFeature(feature)
|
|
679
|
-
})
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// 添加点击事件处理
|
|
683
|
-
if (config.onClick) {
|
|
684
|
-
map.on('click', (event) => {
|
|
685
|
-
const feature = map.forEachFeatureAtPixel(event.pixel, feature => feature, {
|
|
686
|
-
layerFilter: layer => layer === vectorLayer,
|
|
687
|
-
})
|
|
688
|
-
if (feature) {
|
|
689
|
-
const properties = feature.getProperties()
|
|
690
|
-
const { geometry, ...pointData } = properties
|
|
691
|
-
config.onClick(pointData as PointData, event)
|
|
692
|
-
}
|
|
693
|
-
})
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// 初始加载数据
|
|
697
|
-
if (config.show && config.dataProvider) {
|
|
698
|
-
const result = config.dataProvider()
|
|
699
|
-
if (result instanceof Promise) {
|
|
700
|
-
result.then((data) => {
|
|
701
|
-
addFeatures(data)
|
|
702
|
-
}).catch((error) => {
|
|
703
|
-
console.error('获取初始点位数据失败:', error)
|
|
704
|
-
})
|
|
705
|
-
}
|
|
706
|
-
else {
|
|
707
|
-
addFeatures(result)
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
map.addLayer(vectorLayer)
|
|
712
|
-
vectorLayers[config.id] = vectorLayer
|
|
713
|
-
|
|
714
|
-
// 更新图层状态
|
|
715
|
-
if (config.showInControl !== false) {
|
|
716
|
-
const existingIndex = pointLayerStatus.value.findIndex(layer => layer.id === config.id)
|
|
717
|
-
if (existingIndex === -1) {
|
|
718
|
-
pointLayerStatus.value.push(config)
|
|
719
|
-
}
|
|
720
|
-
else {
|
|
721
|
-
pointLayerStatus.value[existingIndex] = config
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* 添加 WMS 图层
|
|
728
|
-
* @param options - WMS 配置
|
|
729
|
-
*/
|
|
730
|
-
function addWMSLayers(options: WMSOptions): void {
|
|
731
|
-
if (!map)
|
|
732
|
-
return
|
|
733
|
-
|
|
734
|
-
const { layers, wms } = options
|
|
735
|
-
|
|
736
|
-
// 更新图层状态
|
|
737
|
-
wmsLayerStatus.value = layers.map(layer => ({
|
|
738
|
-
...layer,
|
|
739
|
-
show: layer.show,
|
|
740
|
-
}))
|
|
741
|
-
|
|
742
|
-
// 移除已存在的 WMS 图层
|
|
743
|
-
Object.values(wmsLayers).forEach((layer) => {
|
|
744
|
-
map?.removeLayer(layer)
|
|
745
|
-
})
|
|
746
|
-
|
|
747
|
-
// 清空图层记录
|
|
748
|
-
Object.keys(wmsLayers).forEach((key) => {
|
|
749
|
-
delete wmsLayers[key]
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
// 添加新的 WMS 图层
|
|
753
|
-
layers.forEach((layerConfig) => {
|
|
754
|
-
const wmsSource = new ImageWMS({
|
|
755
|
-
url: wms.url,
|
|
756
|
-
params: {
|
|
757
|
-
LAYERS: layerConfig.layerName,
|
|
758
|
-
FORMAT: wms.format,
|
|
759
|
-
VERSION: wms.version,
|
|
760
|
-
SRS: wms.srs,
|
|
761
|
-
},
|
|
762
|
-
ratio: 1,
|
|
763
|
-
serverType: 'geoserver',
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
const wmsLayer = new ImageLayer({
|
|
767
|
-
source: wmsSource,
|
|
768
|
-
visible: layerConfig.show,
|
|
769
|
-
zIndex: 2,
|
|
770
|
-
})
|
|
771
|
-
|
|
772
|
-
wmsLayers[layerConfig.layerName] = wmsLayer
|
|
773
|
-
map.addLayer(wmsLayer)
|
|
774
|
-
})
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* 控制 WMS 图层显示/隐藏
|
|
779
|
-
* @param layerName - 图层名称
|
|
780
|
-
* @param visible - 是否显示
|
|
781
|
-
*/
|
|
782
|
-
function setWMSLayerVisible(layerName: string, visible: boolean): void {
|
|
783
|
-
const layer = wmsLayers[layerName]
|
|
784
|
-
if (layer) {
|
|
785
|
-
layer.setVisible(visible)
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* 切换 WMS 图层显示状态
|
|
791
|
-
* @param layer - 图层配置
|
|
792
|
-
*/
|
|
793
|
-
function handleToggleWMSLayer(layer: WMSLayerConfig): void {
|
|
794
|
-
layer.show = !layer.show
|
|
795
|
-
setWMSLayerVisible(layer.layerName, layer.show)
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* 控制点位图层显示/隐藏
|
|
800
|
-
* @param layerId - 图层ID
|
|
801
|
-
* @param visible - 是否显示
|
|
802
|
-
*/
|
|
803
|
-
function setPointLayerVisible(layerId: number, visible: boolean): void {
|
|
804
|
-
const layer = vectorLayers[layerId]
|
|
805
|
-
if (layer) {
|
|
806
|
-
layer.setVisible(visible)
|
|
807
|
-
// 更新图层状态
|
|
808
|
-
const layerIndex = pointLayerStatus.value.findIndex(layer => layer.id === layerId)
|
|
809
|
-
if (layerIndex !== -1) {
|
|
810
|
-
pointLayerStatus.value[layerIndex].show = visible
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* 切换点位图层显示状态
|
|
817
|
-
*/
|
|
818
|
-
function handleTogglePointLayer(layer: PointLayerConfig): void {
|
|
819
|
-
layer.show = !layer.show
|
|
820
|
-
setPointLayerVisible(layer.id, layer.show)
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/** 处理定位请求 */
|
|
824
|
-
async function handleLocation() {
|
|
825
|
-
if (!map)
|
|
826
|
-
return
|
|
827
|
-
|
|
828
|
-
// 如果是导航模式,重新开启跟随定位
|
|
829
|
-
if (locationTimer) {
|
|
830
|
-
isFollowingLocation.value = true
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
try {
|
|
834
|
-
mobileUtil.execute({
|
|
835
|
-
param: {},
|
|
836
|
-
funcName: 'getLocationResult',
|
|
837
|
-
callbackFunc: (result) => {
|
|
838
|
-
const res = result as PhoneLocationStatus
|
|
839
|
-
if (res.status === 'success') {
|
|
840
|
-
const locationResult = JSON.parse(res.data.location)
|
|
841
|
-
if (locationResult.longitude && locationResult.latitude) {
|
|
842
|
-
const center: [number, number] = [locationResult.longitude, locationResult.latitude]
|
|
843
|
-
setCenterAndZoom(center, getZoom())
|
|
844
|
-
// 更新位置图标
|
|
845
|
-
if (isFollowingLocation.value) {
|
|
846
|
-
updateLocationMarker(center)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
},
|
|
851
|
-
})
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
// 在 web 端测试时,使用模拟数据
|
|
855
|
-
console.log('获取实时位置失败,使用模拟数据')
|
|
856
|
-
// 生成一个随机偏移量
|
|
857
|
-
const offset = (Math.random() - 0.5) * 0.01
|
|
858
|
-
const center: [number, number] = [
|
|
859
|
-
108.948024 + offset, // 西安经度
|
|
860
|
-
34.263161 + offset, // 西安纬度
|
|
861
|
-
]
|
|
862
|
-
|
|
863
|
-
setCenterAndZoom(center, getZoom())
|
|
864
|
-
// 更新位置图标
|
|
865
|
-
if (isFollowingLocation.value) {
|
|
866
|
-
updateLocationMarker(center)
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// 开启导航
|
|
872
|
-
function startNavigation() {
|
|
873
|
-
isFollowingLocation.value = true
|
|
874
|
-
|
|
875
|
-
// 创建并添加位置图标图层
|
|
876
|
-
if (!locationLayer) {
|
|
877
|
-
locationLayer = createLocationLayer()
|
|
878
|
-
map?.addLayer(locationLayer)
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
locationTimer = setInterval(() => {
|
|
882
|
-
navigationHandleLocation()
|
|
883
|
-
}, 1000)
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function stopNavigation() {
|
|
887
|
-
isFollowingLocation.value = false
|
|
888
|
-
if (locationTimer) {
|
|
889
|
-
clearInterval(locationTimer)
|
|
890
|
-
locationTimer = null
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// 移除位置图标图层
|
|
894
|
-
if (locationLayer && map) {
|
|
895
|
-
map.removeLayer(locationLayer)
|
|
896
|
-
locationLayer = null
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function navigationHandleLocation() {
|
|
901
|
-
// 打开导航定时器, 并且跟随定位
|
|
902
|
-
if (isFollowingLocation.value) {
|
|
903
|
-
handleLocation()
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
/**
|
|
908
|
-
* 添加轨迹图层
|
|
909
|
-
* @param trackData - 轨迹数据
|
|
910
|
-
*/
|
|
911
|
-
function addTrackLayer(trackData: TrackData): void {
|
|
912
|
-
if (!map)
|
|
913
|
-
return
|
|
914
|
-
|
|
915
|
-
const vectorSource = new VectorSource()
|
|
916
|
-
const vectorLayer = new VectorLayer({
|
|
917
|
-
source: vectorSource,
|
|
918
|
-
visible: true,
|
|
919
|
-
zIndex: 2,
|
|
920
|
-
})
|
|
921
|
-
|
|
922
|
-
// 创建轨迹线要素
|
|
923
|
-
const coordinates = trackData.trackData.map(coord => fromLonLat(coord))
|
|
924
|
-
const lineString = new Feature({
|
|
925
|
-
geometry: new LineString(coordinates),
|
|
926
|
-
})
|
|
927
|
-
|
|
928
|
-
// 设置轨迹线样式
|
|
929
|
-
const lineStyle = new Style({
|
|
930
|
-
stroke: new Stroke({
|
|
931
|
-
color: trackData.color,
|
|
932
|
-
width: 3,
|
|
933
|
-
}),
|
|
934
|
-
})
|
|
935
|
-
lineString.setStyle(lineStyle)
|
|
936
|
-
|
|
937
|
-
// 创建起点和终点图标
|
|
938
|
-
const startPoint = new Feature({
|
|
939
|
-
geometry: new Point(coordinates[0]),
|
|
940
|
-
})
|
|
941
|
-
const endPoint = new Feature({
|
|
942
|
-
geometry: new Point(coordinates[coordinates.length - 1]),
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
// 设置起点图标样式 - 使用绿色圆形图标
|
|
946
|
-
const startStyle = new Style({
|
|
947
|
-
image: new Circle({
|
|
948
|
-
radius: 8,
|
|
949
|
-
fill: new Fill({
|
|
950
|
-
color: '#4CAF50',
|
|
951
|
-
}),
|
|
952
|
-
stroke: new Stroke({
|
|
953
|
-
color: '#fff',
|
|
954
|
-
width: 2,
|
|
955
|
-
}),
|
|
956
|
-
}),
|
|
957
|
-
text: new Text({
|
|
958
|
-
text: '起点',
|
|
959
|
-
offsetY: -15,
|
|
960
|
-
font: '12px sans-serif',
|
|
961
|
-
fill: new Fill({
|
|
962
|
-
color: '#333',
|
|
963
|
-
}),
|
|
964
|
-
stroke: new Stroke({
|
|
965
|
-
color: '#fff',
|
|
966
|
-
width: 2,
|
|
967
|
-
}),
|
|
968
|
-
}),
|
|
969
|
-
})
|
|
970
|
-
|
|
971
|
-
// 设置终点图标样式 - 使用红色圆形图标
|
|
972
|
-
const endStyle = new Style({
|
|
973
|
-
image: new Circle({
|
|
974
|
-
radius: 8,
|
|
975
|
-
fill: new Fill({
|
|
976
|
-
color: '#F44336',
|
|
977
|
-
}),
|
|
978
|
-
stroke: new Stroke({
|
|
979
|
-
color: '#fff',
|
|
980
|
-
width: 2,
|
|
981
|
-
}),
|
|
982
|
-
}),
|
|
983
|
-
text: new Text({
|
|
984
|
-
text: '终点',
|
|
985
|
-
offsetY: -15,
|
|
986
|
-
font: '12px sans-serif',
|
|
987
|
-
fill: new Fill({
|
|
988
|
-
color: '#333',
|
|
989
|
-
}),
|
|
990
|
-
stroke: new Stroke({
|
|
991
|
-
color: '#fff',
|
|
992
|
-
width: 2,
|
|
993
|
-
}),
|
|
994
|
-
}),
|
|
995
|
-
})
|
|
996
|
-
|
|
997
|
-
startPoint.setStyle(startStyle)
|
|
998
|
-
endPoint.setStyle(endStyle)
|
|
999
|
-
|
|
1000
|
-
// 添加要素到图层
|
|
1001
|
-
vectorSource.addFeatures([lineString, startPoint, endPoint])
|
|
1002
|
-
|
|
1003
|
-
// 添加到地图
|
|
1004
|
-
map.addLayer(vectorLayer)
|
|
1005
|
-
trackLayers[trackData.id] = vectorLayer
|
|
1006
|
-
|
|
1007
|
-
// 更新图层状态,确保 show 属性被正确设置
|
|
1008
|
-
const trackDataWithShow = {
|
|
1009
|
-
...trackData,
|
|
1010
|
-
show: true, // 默认显示
|
|
1011
|
-
}
|
|
1012
|
-
trackLayerStatus.value.push(trackDataWithShow)
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* 控制轨迹图层显示/隐藏
|
|
1017
|
-
* @param trackId - 轨迹ID
|
|
1018
|
-
* @param visible - 是否显示
|
|
1019
|
-
*/
|
|
1020
|
-
function setTrackLayerVisible(trackId: number, visible: boolean): void {
|
|
1021
|
-
const layer = trackLayers[trackId]
|
|
1022
|
-
if (layer) {
|
|
1023
|
-
layer.setVisible(visible)
|
|
1024
|
-
// 更新图层状态
|
|
1025
|
-
const layerIndex = trackLayerStatus.value.findIndex(layer => layer.id === trackId)
|
|
1026
|
-
if (layerIndex !== -1) {
|
|
1027
|
-
trackLayerStatus.value[layerIndex].show = visible
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
/**
|
|
1033
|
-
* 切换轨迹图层显示状态
|
|
1034
|
-
*/
|
|
1035
|
-
function handleToggleTrackLayer(track: TrackData): void {
|
|
1036
|
-
track.show = !track.show
|
|
1037
|
-
setTrackLayerVisible(track.id, track.show)
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// 暴露方法给父组件
|
|
1041
|
-
defineExpose({
|
|
1042
|
-
updateLocationMarker,
|
|
1043
|
-
init,
|
|
1044
|
-
getMap,
|
|
1045
|
-
setCenter,
|
|
1046
|
-
setZoom,
|
|
1047
|
-
getZoom,
|
|
1048
|
-
setCenterAndZoom,
|
|
1049
|
-
addPointLayer,
|
|
1050
|
-
addWebGLPoints,
|
|
1051
|
-
addWMSLayers,
|
|
1052
|
-
setWMSLayerVisible,
|
|
1053
|
-
handleToggleWMSLayer,
|
|
1054
|
-
setPointLayerVisible,
|
|
1055
|
-
handleTogglePointLayer,
|
|
1056
|
-
getAddressInfo,
|
|
1057
|
-
handleLocation,
|
|
1058
|
-
startNavigation,
|
|
1059
|
-
stopNavigation,
|
|
1060
|
-
addTrackLayer,
|
|
1061
|
-
setTrackLayerVisible,
|
|
1062
|
-
handleToggleTrackLayer,
|
|
1063
|
-
})
|
|
1064
|
-
|
|
1065
|
-
// 组件卸载时清理地图实例
|
|
1066
|
-
onUnmounted(() => {
|
|
1067
|
-
if (map) {
|
|
1068
|
-
stopNavigation()
|
|
1069
|
-
map.setTarget(undefined)
|
|
1070
|
-
map = null
|
|
1071
|
-
}
|
|
1072
|
-
if (locationTimer) {
|
|
1073
|
-
clearInterval(locationTimer)
|
|
1074
|
-
locationTimer = null
|
|
1075
|
-
}
|
|
1076
|
-
locationLayer = null
|
|
1077
|
-
})
|
|
1078
|
-
</script>
|
|
1079
|
-
|
|
1080
|
-
<template>
|
|
1081
|
-
<div class="map-wrapper">
|
|
1082
|
-
<div ref="mapRef" class="ol-map" />
|
|
1083
|
-
|
|
1084
|
-
<div v-if="!preview" class="location-button">
|
|
1085
|
-
<Button size="small" square @click="handleLocation">
|
|
1086
|
-
<img
|
|
1087
|
-
src="@af-mobile-client-vue3/assets/img/component/location.png"
|
|
1088
|
-
class="location-icon"
|
|
1089
|
-
alt="定位"
|
|
1090
|
-
>
|
|
1091
|
-
</Button>
|
|
1092
|
-
</div>
|
|
1093
|
-
<div class="map-controls">
|
|
1094
|
-
<!-- 控制按钮 -->
|
|
1095
|
-
<div class="control-toggle">
|
|
1096
|
-
<Button
|
|
1097
|
-
:type="showControls ? 'primary' : 'default'"
|
|
1098
|
-
size="small"
|
|
1099
|
-
square
|
|
1100
|
-
@click="showControls = !showControls"
|
|
1101
|
-
>
|
|
1102
|
-
<img
|
|
1103
|
-
src="@af-mobile-client-vue3/assets/img/component/mapLayers.png"
|
|
1104
|
-
class="toggle-icon"
|
|
1105
|
-
:class="[{ active: showControls }]"
|
|
1106
|
-
alt="图层"
|
|
1107
|
-
>
|
|
1108
|
-
</Button>
|
|
1109
|
-
</div>
|
|
1110
|
-
|
|
1111
|
-
<!-- 图层控制面板 -->
|
|
1112
|
-
<div v-show="showControls" class="control-panels">
|
|
1113
|
-
<!-- 底图切换 -->
|
|
1114
|
-
<div class="control-panel base-layer-control">
|
|
1115
|
-
<div class="control-title">
|
|
1116
|
-
<i class="van-icon van-icon-map-marked" /> 底图切换
|
|
1117
|
-
</div>
|
|
1118
|
-
<select v-model="currentMapType" @change="handleMapChange(currentMapType)">
|
|
1119
|
-
<option v-for="layer in layerOptions" :key="layer.value" :value="layer.value">
|
|
1120
|
-
{{ layer.text }}
|
|
1121
|
-
</option>
|
|
1122
|
-
</select>
|
|
1123
|
-
</div>
|
|
1124
|
-
|
|
1125
|
-
<!-- 图层控制 -->
|
|
1126
|
-
<div v-if="wmsLayerStatus.length > 0" class="control-panel layer-control">
|
|
1127
|
-
<div class="control-title">
|
|
1128
|
-
<i class="van-icon van-icon-layers" /> 图层控制
|
|
1129
|
-
</div>
|
|
1130
|
-
<div class="layer-list">
|
|
1131
|
-
<div
|
|
1132
|
-
v-for="layer in wmsLayerStatus"
|
|
1133
|
-
:key="layer.id"
|
|
1134
|
-
class="layer-item"
|
|
1135
|
-
:class="{ active: layer.show }"
|
|
1136
|
-
@click="handleToggleWMSLayer(layer)"
|
|
1137
|
-
>
|
|
1138
|
-
<i class="van-icon" :class="layer.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1139
|
-
<span>{{ layer.value }}</span>
|
|
1140
|
-
</div>
|
|
1141
|
-
</div>
|
|
1142
|
-
</div>
|
|
1143
|
-
|
|
1144
|
-
<!-- 点位图层 -->
|
|
1145
|
-
<div v-if="pointLayerStatus.length > 0" class="control-panel layer-control">
|
|
1146
|
-
<div class="control-title">
|
|
1147
|
-
<i class="van-icon van-icon-location" /> 点位图层
|
|
1148
|
-
</div>
|
|
1149
|
-
<div class="layer-list">
|
|
1150
|
-
<div
|
|
1151
|
-
v-for="layer in pointLayerStatus"
|
|
1152
|
-
:key="layer.id"
|
|
1153
|
-
class="layer-item"
|
|
1154
|
-
:class="{ active: layer.show }"
|
|
1155
|
-
@click="handleTogglePointLayer(layer)"
|
|
1156
|
-
>
|
|
1157
|
-
<i class="van-icon" :class="layer.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1158
|
-
<span>{{ layer.value }}</span>
|
|
1159
|
-
</div>
|
|
1160
|
-
</div>
|
|
1161
|
-
</div>
|
|
1162
|
-
|
|
1163
|
-
<!-- 轨迹图层 -->
|
|
1164
|
-
<div v-if="trackLayerStatus.length > 0" class="control-panel layer-control">
|
|
1165
|
-
<div class="control-title">
|
|
1166
|
-
<i class="van-icon van-icon-location-o" /> 轨迹图层
|
|
1167
|
-
</div>
|
|
1168
|
-
<div class="layer-list">
|
|
1169
|
-
<div
|
|
1170
|
-
v-for="track in trackLayerStatus"
|
|
1171
|
-
:key="track.id"
|
|
1172
|
-
class="layer-item"
|
|
1173
|
-
:class="{ active: track.show }"
|
|
1174
|
-
@click="handleToggleTrackLayer(track)"
|
|
1175
|
-
>
|
|
1176
|
-
<i class="van-icon" :class="track.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1177
|
-
<span>{{ track.name }}</span>
|
|
1178
|
-
</div>
|
|
1179
|
-
</div>
|
|
1180
|
-
</div>
|
|
1181
|
-
</div>
|
|
1182
|
-
</div>
|
|
1183
|
-
</div>
|
|
1184
|
-
</template>
|
|
1185
|
-
|
|
1186
|
-
<style lang="less" scoped>
|
|
1187
|
-
.map-wrapper {
|
|
1188
|
-
position: relative;
|
|
1189
|
-
width: 100%;
|
|
1190
|
-
height: 100%;
|
|
1191
|
-
|
|
1192
|
-
.ol-map {
|
|
1193
|
-
width: 100%;
|
|
1194
|
-
height: 100%;
|
|
1195
|
-
min-height: 200px;
|
|
1196
|
-
touch-action: none;
|
|
1197
|
-
|
|
1198
|
-
&:active {
|
|
1199
|
-
cursor: grabbing;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// 定位按钮样式
|
|
1204
|
-
.location-button {
|
|
1205
|
-
position: absolute;
|
|
1206
|
-
left: 0.5em;
|
|
1207
|
-
bottom: calc(0.5em + 30px);
|
|
1208
|
-
z-index: 1000;
|
|
1209
|
-
|
|
1210
|
-
.van-button {
|
|
1211
|
-
width: 32px;
|
|
1212
|
-
height: 32px;
|
|
1213
|
-
padding: 4px;
|
|
1214
|
-
display: flex;
|
|
1215
|
-
align-items: center;
|
|
1216
|
-
justify-content: center;
|
|
1217
|
-
border: 1px solid #ebedf0;
|
|
1218
|
-
background: rgba(255, 255, 255, 0.9);
|
|
1219
|
-
backdrop-filter: blur(4px);
|
|
1220
|
-
box-shadow: none;
|
|
1221
|
-
|
|
1222
|
-
&:hover {
|
|
1223
|
-
border-color: #1989fa;
|
|
1224
|
-
.location-icon {
|
|
1225
|
-
opacity: 1;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
.location-icon {
|
|
1230
|
-
width: 20px;
|
|
1231
|
-
height: 20px;
|
|
1232
|
-
opacity: 0.7;
|
|
1233
|
-
transition: all 0.3s;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.map-controls {
|
|
1239
|
-
position: absolute;
|
|
1240
|
-
right: 10px;
|
|
1241
|
-
top: 10px;
|
|
1242
|
-
z-index: 1000;
|
|
1243
|
-
display: flex;
|
|
1244
|
-
flex-direction: column;
|
|
1245
|
-
gap: 8px;
|
|
1246
|
-
|
|
1247
|
-
.control-toggle {
|
|
1248
|
-
align-self: flex-end;
|
|
1249
|
-
|
|
1250
|
-
.van-button {
|
|
1251
|
-
width: 32px;
|
|
1252
|
-
height: 32px;
|
|
1253
|
-
padding: 4px;
|
|
1254
|
-
display: flex;
|
|
1255
|
-
align-items: center;
|
|
1256
|
-
justify-content: center;
|
|
1257
|
-
border: 1px solid #ebedf0;
|
|
1258
|
-
background: rgba(255, 255, 255, 0.9);
|
|
1259
|
-
backdrop-filter: blur(4px);
|
|
1260
|
-
box-shadow: none;
|
|
1261
|
-
|
|
1262
|
-
&.van-button--primary {
|
|
1263
|
-
background: #1989fa;
|
|
1264
|
-
border-color: #1989fa;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
.toggle-icon {
|
|
1268
|
-
width: 20px;
|
|
1269
|
-
height: 20px;
|
|
1270
|
-
transition: all 0.3s;
|
|
1271
|
-
opacity: 0.7;
|
|
1272
|
-
|
|
1273
|
-
&.active {
|
|
1274
|
-
opacity: 1;
|
|
1275
|
-
filter: brightness(1) invert(1);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
&:hover {
|
|
1280
|
-
border-color: #1989fa;
|
|
1281
|
-
.toggle-icon {
|
|
1282
|
-
opacity: 1;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
.control-panels {
|
|
1289
|
-
background: white;
|
|
1290
|
-
border-radius: 8px;
|
|
1291
|
-
padding: 12px;
|
|
1292
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1293
|
-
min-width: 160px;
|
|
1294
|
-
max-width: 180px;
|
|
1295
|
-
|
|
1296
|
-
.control-panel {
|
|
1297
|
-
& + .control-panel {
|
|
1298
|
-
margin-top: 12px;
|
|
1299
|
-
padding-top: 12px;
|
|
1300
|
-
border-top: 1px solid #f5f5f5;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
.control-title {
|
|
1304
|
-
font-size: 13px;
|
|
1305
|
-
font-weight: 500;
|
|
1306
|
-
color: #323233;
|
|
1307
|
-
margin-bottom: 8px;
|
|
1308
|
-
display: flex;
|
|
1309
|
-
align-items: center;
|
|
1310
|
-
gap: 4px;
|
|
1311
|
-
|
|
1312
|
-
.van-icon {
|
|
1313
|
-
font-size: 16px;
|
|
1314
|
-
color: #1989fa;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
select {
|
|
1319
|
-
width: 100%;
|
|
1320
|
-
padding: 6px 24px 6px 8px;
|
|
1321
|
-
font-size: 13px;
|
|
1322
|
-
border: 1px solid #dcdee0;
|
|
1323
|
-
border-radius: 4px;
|
|
1324
|
-
background-color: #f7f8fa;
|
|
1325
|
-
color: #323233;
|
|
1326
|
-
cursor: pointer;
|
|
1327
|
-
outline: none;
|
|
1328
|
-
appearance: none;
|
|
1329
|
-
-webkit-appearance: none;
|
|
1330
|
-
-moz-appearance: none;
|
|
1331
|
-
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
|
1332
|
-
background-repeat: no-repeat;
|
|
1333
|
-
background-position: right 6px center;
|
|
1334
|
-
background-size: 12px;
|
|
1335
|
-
|
|
1336
|
-
&:hover {
|
|
1337
|
-
border-color: #1989fa;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
option {
|
|
1341
|
-
padding: 6px;
|
|
1342
|
-
background: white;
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
.layer-list {
|
|
1347
|
-
display: flex;
|
|
1348
|
-
flex-direction: column;
|
|
1349
|
-
gap: 4px;
|
|
1350
|
-
|
|
1351
|
-
.layer-item {
|
|
1352
|
-
display: flex;
|
|
1353
|
-
align-items: center;
|
|
1354
|
-
gap: 6px;
|
|
1355
|
-
padding: 6px 8px;
|
|
1356
|
-
border-radius: 4px;
|
|
1357
|
-
cursor: pointer;
|
|
1358
|
-
transition: all 0.3s;
|
|
1359
|
-
background: #f7f8fa;
|
|
1360
|
-
border: 1px solid transparent;
|
|
1361
|
-
|
|
1362
|
-
.van-icon {
|
|
1363
|
-
font-size: 14px;
|
|
1364
|
-
color: #969799;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
span {
|
|
1368
|
-
font-size: 13px;
|
|
1369
|
-
color: #323233;
|
|
1370
|
-
flex: 1;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
&:hover {
|
|
1374
|
-
background: #f0f2f5;
|
|
1375
|
-
border-color: #dcdee0;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
&.active {
|
|
1379
|
-
background: #e6f3ff;
|
|
1380
|
-
border-color: #1989fa;
|
|
1381
|
-
|
|
1382
|
-
.van-icon {
|
|
1383
|
-
color: #1989fa;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
span {
|
|
1387
|
-
color: #1989fa;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// 比例尺样式
|
|
1398
|
-
.ol-scale-line {
|
|
1399
|
-
position: absolute;
|
|
1400
|
-
left: 0.5em;
|
|
1401
|
-
bottom: 0.5em;
|
|
1402
|
-
background: rgba(255, 255, 255, 0.8);
|
|
1403
|
-
padding: 2px;
|
|
1404
|
-
border-radius: 4px;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
.ol-scale-line-inner {
|
|
1408
|
-
border: 2px solid #333;
|
|
1409
|
-
border-top: none;
|
|
1410
|
-
color: #333;
|
|
1411
|
-
font-size: 12px;
|
|
1412
|
-
text-align: center;
|
|
1413
|
-
margin: 1px;
|
|
1414
|
-
will-change: contents, width;
|
|
1415
|
-
transition: width 0.25s;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
// 移动端适配
|
|
1419
|
-
@media screen and (max-width: 768px) {
|
|
1420
|
-
.map-controls {
|
|
1421
|
-
right: 8px;
|
|
1422
|
-
top: 8px;
|
|
1423
|
-
gap: 6px;
|
|
1424
|
-
|
|
1425
|
-
.control-panels {
|
|
1426
|
-
padding: 10px;
|
|
1427
|
-
min-width: 140px;
|
|
1428
|
-
|
|
1429
|
-
.control-panel {
|
|
1430
|
-
& + .control-panel {
|
|
1431
|
-
margin-top: 10px;
|
|
1432
|
-
padding-top: 10px;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
.control-title {
|
|
1436
|
-
font-size: 12px;
|
|
1437
|
-
margin-bottom: 6px;
|
|
1438
|
-
|
|
1439
|
-
.van-icon {
|
|
1440
|
-
font-size: 14px;
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
select {
|
|
1445
|
-
padding: 4px 20px 4px 6px;
|
|
1446
|
-
font-size: 12px;
|
|
1447
|
-
background-size: 10px;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
.layer-list {
|
|
1451
|
-
gap: 3px;
|
|
1452
|
-
|
|
1453
|
-
.layer-item {
|
|
1454
|
-
padding: 4px 6px;
|
|
1455
|
-
|
|
1456
|
-
.van-icon {
|
|
1457
|
-
font-size: 12px;
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
span {
|
|
1461
|
-
font-size: 12px;
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// 比例尺样式
|
|
1471
|
-
:deep(.ol-scale-line) {
|
|
1472
|
-
position: absolute;
|
|
1473
|
-
left: 0.5em;
|
|
1474
|
-
bottom: 0.5em;
|
|
1475
|
-
background: rgba(255, 255, 255, 0.8);
|
|
1476
|
-
padding: 2px;
|
|
1477
|
-
border-radius: 4px;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
:deep(.ol-scale-line-inner) {
|
|
1481
|
-
border: 2px solid #333;
|
|
1482
|
-
border-top: none;
|
|
1483
|
-
color: #333;
|
|
1484
|
-
font-size: 12px;
|
|
1485
|
-
text-align: center;
|
|
1486
|
-
margin: 1px;
|
|
1487
|
-
will-change: contents, width;
|
|
1488
|
-
transition: width 0.25s;
|
|
1489
|
-
}
|
|
1490
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* OpenLayers地图组件
|
|
4
|
+
* 支持多种底图切换:
|
|
5
|
+
* - 高德地图(矢量)
|
|
6
|
+
* - 高德卫星图
|
|
7
|
+
* - 天地图(矢量)
|
|
8
|
+
* - 天地图卫星图
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
InitParams,
|
|
12
|
+
PhoneLocationStatus,
|
|
13
|
+
PointData,
|
|
14
|
+
PointLayerConfig,
|
|
15
|
+
TrackData,
|
|
16
|
+
WebGLPointOptions,
|
|
17
|
+
WMSLayerConfig,
|
|
18
|
+
WMSOptions,
|
|
19
|
+
} from './types'
|
|
20
|
+
import locationIcon from '@af-mobile-client-vue3/assets/img/component/positioning.png'
|
|
21
|
+
import { getConfigByName } from '@af-mobile-client-vue3/services/api/common'
|
|
22
|
+
import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
|
|
23
|
+
import { Map, View } from 'ol'
|
|
24
|
+
import { defaults as defaultControls, ScaleLine } from 'ol/control'
|
|
25
|
+
import Feature from 'ol/Feature'
|
|
26
|
+
import LineString from 'ol/geom/LineString'
|
|
27
|
+
import Point from 'ol/geom/Point'
|
|
28
|
+
import { defaults as defaultInteractions } from 'ol/interaction'
|
|
29
|
+
import { Image as ImageLayer, Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
|
30
|
+
import { fromLonLat, toLonLat } from 'ol/proj'
|
|
31
|
+
import { ImageWMS, Vector as VectorSource, XYZ } from 'ol/source'
|
|
32
|
+
import { Circle, Fill, Icon, Stroke, Style, Text } from 'ol/style'
|
|
33
|
+
import { Button } from 'vant'
|
|
34
|
+
import { getCurrentInstance, onUnmounted, ref } from 'vue'
|
|
35
|
+
import { wgs84ToGcj02Projection } from './utils/wgs84ToGcj02'
|
|
36
|
+
import 'vant/lib/index.css'
|
|
37
|
+
|
|
38
|
+
// 在 script setup 中添加
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
(e: 'centerChange', center: [number, number]): void
|
|
41
|
+
}>()
|
|
42
|
+
|
|
43
|
+
// 获取当前组件实例
|
|
44
|
+
const instance = getCurrentInstance()
|
|
45
|
+
|
|
46
|
+
// 存储初始化参数
|
|
47
|
+
const mapParams = ref<InitParams>({})
|
|
48
|
+
|
|
49
|
+
/** 地图容器引用 */
|
|
50
|
+
const mapRef = ref<HTMLDivElement>()
|
|
51
|
+
/** 是否为预览模式 */
|
|
52
|
+
const preview = ref(false)
|
|
53
|
+
/** 地图实例 */
|
|
54
|
+
let map: Map | null = null
|
|
55
|
+
/** 当前底图类型 */
|
|
56
|
+
const currentMapType = ref<string>('tianditu')
|
|
57
|
+
/** 控制图层面板显示状态 */
|
|
58
|
+
const showControls = ref<boolean>(false)
|
|
59
|
+
|
|
60
|
+
/** 图层选项配置 */
|
|
61
|
+
const layerOptions = [
|
|
62
|
+
{ text: '高德地图', value: 'gaode' },
|
|
63
|
+
{ text: '高德卫星', value: 'gaodeSatellite' },
|
|
64
|
+
{ text: '天地图', value: 'tianditu' },
|
|
65
|
+
{ text: '天地图卫星', value: 'tianditusatellite' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/** 存储所有底图图层 */
|
|
69
|
+
const baseMaps: Record<string, TileLayer<XYZ>> = {}
|
|
70
|
+
|
|
71
|
+
/** 存储 WMS 图层 */
|
|
72
|
+
const wmsLayers: Record<string, ImageLayer<ImageWMS>> = {}
|
|
73
|
+
|
|
74
|
+
/** WMS 图层状态 */
|
|
75
|
+
const wmsLayerStatus = ref<WMSLayerConfig[]>([])
|
|
76
|
+
|
|
77
|
+
/** 存储点位图层 */
|
|
78
|
+
const vectorLayers: Record<number, VectorLayer<VectorSource>> = {}
|
|
79
|
+
const pointLayerStatus = ref<PointLayerConfig[]>([])
|
|
80
|
+
|
|
81
|
+
const tiandityKey = ref()
|
|
82
|
+
const gaodeKey = ref()
|
|
83
|
+
/** 导航模式 是否正在跟随定位 */
|
|
84
|
+
const isFollowingLocation = ref(false)
|
|
85
|
+
/** 定位定时器 */
|
|
86
|
+
let locationTimer: ReturnType<typeof setInterval> | null = null
|
|
87
|
+
/** 位置图标图层 */
|
|
88
|
+
let locationLayer: VectorLayer<VectorSource> | null = null
|
|
89
|
+
|
|
90
|
+
/** 存储轨迹图层 */
|
|
91
|
+
const trackLayers: Record<number, VectorLayer<VectorSource>> = {}
|
|
92
|
+
const trackLayerStatus = ref<TrackData[]>([])
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 创建位置图标图层
|
|
96
|
+
*/
|
|
97
|
+
function createLocationLayer(): VectorLayer<VectorSource> {
|
|
98
|
+
const source = new VectorSource()
|
|
99
|
+
return new VectorLayer({
|
|
100
|
+
source,
|
|
101
|
+
zIndex: 10, // 确保位置图标在最上层
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 更新位置图标
|
|
107
|
+
*/
|
|
108
|
+
function updateLocationMarker(center: [number, number]): void {
|
|
109
|
+
if (!map) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (!locationLayer) {
|
|
113
|
+
locationLayer = createLocationLayer()
|
|
114
|
+
map?.addLayer(locationLayer)
|
|
115
|
+
}
|
|
116
|
+
const source = locationLayer.getSource()
|
|
117
|
+
if (!source) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
// 清除现有图标
|
|
121
|
+
source.clear()
|
|
122
|
+
|
|
123
|
+
// 创建新的位置图标要素
|
|
124
|
+
const feature = new Feature({
|
|
125
|
+
geometry: new Point(fromLonLat(center)),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// 设置图标样式
|
|
129
|
+
const style = new Style({
|
|
130
|
+
image: new Icon({
|
|
131
|
+
src: locationIcon,
|
|
132
|
+
scale: 0.2,
|
|
133
|
+
anchor: [0.5, 0.5],
|
|
134
|
+
anchorXUnits: 'fraction',
|
|
135
|
+
anchorYUnits: 'fraction',
|
|
136
|
+
crossOrigin: 'anonymous',
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
feature.setStyle(style)
|
|
141
|
+
source.addFeature(feature)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 切换地图图层
|
|
146
|
+
* @param type - 图层类型
|
|
147
|
+
*/
|
|
148
|
+
function handleMapChange(type: string): void {
|
|
149
|
+
// 隐藏所有图层
|
|
150
|
+
Object.keys(baseMaps).forEach((key) => {
|
|
151
|
+
baseMaps[key].setVisible(false)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// 根据选择显示对应图层
|
|
155
|
+
switch (type) {
|
|
156
|
+
case 'gaodeSatellite':
|
|
157
|
+
baseMaps.gaodeSatellite.setVisible(true)
|
|
158
|
+
baseMaps.gaodelabelLayer.setVisible(true)
|
|
159
|
+
break
|
|
160
|
+
case 'tianditu':
|
|
161
|
+
baseMaps.tianditu.setVisible(true)
|
|
162
|
+
baseMaps.tianditulabel.setVisible(true)
|
|
163
|
+
break
|
|
164
|
+
case 'tianditusatellite':
|
|
165
|
+
baseMaps.tianditusatellite.setVisible(true)
|
|
166
|
+
baseMaps.tianditusatlabel.setVisible(true)
|
|
167
|
+
break
|
|
168
|
+
default:
|
|
169
|
+
baseMaps.gaode.setVisible(true)
|
|
170
|
+
}
|
|
171
|
+
currentMapType.value = type
|
|
172
|
+
|
|
173
|
+
// 强制更新地图视图,确保切换后的图层显示正确
|
|
174
|
+
if (map) {
|
|
175
|
+
map.updateSize()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 初始化底图图层
|
|
181
|
+
* @param tianDiTuKey - 天地图密钥
|
|
182
|
+
*/
|
|
183
|
+
function initializeLayers(tianDiTuKey = ''): void {
|
|
184
|
+
try {
|
|
185
|
+
// 高德地图
|
|
186
|
+
baseMaps.gaode = new TileLayer({
|
|
187
|
+
source: new XYZ({
|
|
188
|
+
url: 'https://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}',
|
|
189
|
+
crossOrigin: 'anonymous',
|
|
190
|
+
projection: 'EPSG:3857',
|
|
191
|
+
}),
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
// 高德卫星图
|
|
195
|
+
baseMaps.gaodeSatellite = new TileLayer({
|
|
196
|
+
source: new XYZ({
|
|
197
|
+
url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
|
|
198
|
+
crossOrigin: 'anonymous',
|
|
199
|
+
projection: 'EPSG:3857',
|
|
200
|
+
}),
|
|
201
|
+
visible: false,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// 高德标注图层
|
|
205
|
+
baseMaps.gaodelabelLayer = new TileLayer({
|
|
206
|
+
source: new XYZ({
|
|
207
|
+
url: 'https://webst02.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
|
|
208
|
+
crossOrigin: 'anonymous',
|
|
209
|
+
projection: 'EPSG:3857',
|
|
210
|
+
}),
|
|
211
|
+
visible: false,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// 天地图矢量图层
|
|
215
|
+
baseMaps.tianditu = new TileLayer({
|
|
216
|
+
source: new XYZ({
|
|
217
|
+
url: 'https://t0.tianditu.gov.cn/vec_w/wmts?'
|
|
218
|
+
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&'
|
|
219
|
+
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
220
|
+
projection: wgs84ToGcj02Projection,
|
|
221
|
+
}),
|
|
222
|
+
visible: false,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// 天地图标注图层
|
|
226
|
+
baseMaps.tianditulabel = new TileLayer({
|
|
227
|
+
source: new XYZ({
|
|
228
|
+
url: 'https://t0.tianditu.gov.cn/cva_w/wmts?'
|
|
229
|
+
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&'
|
|
230
|
+
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
231
|
+
projection: wgs84ToGcj02Projection,
|
|
232
|
+
}),
|
|
233
|
+
visible: false,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// 天地图卫星图层
|
|
237
|
+
baseMaps.tianditusatellite = new TileLayer({
|
|
238
|
+
source: new XYZ({
|
|
239
|
+
url: 'https://t0.tianditu.gov.cn/img_w/wmts?'
|
|
240
|
+
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&'
|
|
241
|
+
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
242
|
+
projection: wgs84ToGcj02Projection,
|
|
243
|
+
}),
|
|
244
|
+
visible: false,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// 天地图卫星标注
|
|
248
|
+
baseMaps.tianditusatlabel = new TileLayer({
|
|
249
|
+
source: new XYZ({
|
|
250
|
+
url: 'https://t0.tianditu.gov.cn/cia_w/wmts?'
|
|
251
|
+
+ 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&'
|
|
252
|
+
+ `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
|
|
253
|
+
projection: wgs84ToGcj02Projection,
|
|
254
|
+
}),
|
|
255
|
+
visible: false,
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error('初始化地图图层失败:', error)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 获取地址信息
|
|
265
|
+
* @param location - 经纬度坐标
|
|
266
|
+
*/
|
|
267
|
+
async function getAddressInfo(location: [number, number]): Promise<string> {
|
|
268
|
+
try {
|
|
269
|
+
const key = gaodeKey.value
|
|
270
|
+
|
|
271
|
+
if (!key) {
|
|
272
|
+
return '获取地址失败: 未配置密钥'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 高德逆地址编码请求说明 https://amap.apifox.cn/api-14551463
|
|
276
|
+
const response = await fetch(
|
|
277
|
+
`https://restapi.amap.com/v3/geocode/regeo?location=${location[0].toFixed(6)},${location[1].toFixed(6)}&key=${key}&output=JSON`,
|
|
278
|
+
)
|
|
279
|
+
const data = await response.json()
|
|
280
|
+
|
|
281
|
+
if (data.status === '1' && data.regeocode) {
|
|
282
|
+
return data.regeocode.formatted_address
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 处理错误情况
|
|
286
|
+
if (data.infocode === '10001') {
|
|
287
|
+
return '获取地址失败: key 无效'
|
|
288
|
+
}
|
|
289
|
+
if (data.infocode === '10002') {
|
|
290
|
+
return '获取地址失败: key 未配置平台'
|
|
291
|
+
}
|
|
292
|
+
return `获取地址失败: ${data.info || '未知错误'}`
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
return '获取地址失败: 网络错误'
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 处理地图移动结束事件
|
|
301
|
+
*/
|
|
302
|
+
async function handleMoveEnd() {
|
|
303
|
+
if (!map)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
const view = map.getView()
|
|
307
|
+
const center = view.getCenter()
|
|
308
|
+
if (!center)
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
// 转换坐标为经纬度
|
|
312
|
+
const lonLat = toLonLat(center)
|
|
313
|
+
const formattedCenter: [number, number] = [Number(lonLat[0].toFixed(6)), Number(lonLat[1].toFixed(6))]
|
|
314
|
+
|
|
315
|
+
// 直接发送事件,让父组件决定是否处理
|
|
316
|
+
emit('centerChange', formattedCenter)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 初始化地图
|
|
321
|
+
* @param params - 初始化参数
|
|
322
|
+
*/
|
|
323
|
+
function init(params: InitParams = {}): Promise<void> {
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
if (!mapRef.value) {
|
|
326
|
+
resolve()
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 保存初始化参数
|
|
331
|
+
mapParams.value = params
|
|
332
|
+
|
|
333
|
+
// 设置默认参数
|
|
334
|
+
const {
|
|
335
|
+
center = [116.404, 39.915],
|
|
336
|
+
zoom = 10,
|
|
337
|
+
maxZoom = 18,
|
|
338
|
+
minZoom = 4,
|
|
339
|
+
isPreview = false,
|
|
340
|
+
} = params
|
|
341
|
+
// 设置预览模式
|
|
342
|
+
preview.value = isPreview
|
|
343
|
+
try {
|
|
344
|
+
getConfigByName('webConfig', (res) => {
|
|
345
|
+
const tianDiTuKey = res.tianDiTuKey || 'c16876b28898637c0a1a68b3fa410504'
|
|
346
|
+
const amapKey = res.amapKey || '5ebabc4536d4b42e0dd1e20175cca8ab'
|
|
347
|
+
|
|
348
|
+
tiandityKey.value = tianDiTuKey
|
|
349
|
+
gaodeKey.value = amapKey
|
|
350
|
+
// 初始化所有底图图层
|
|
351
|
+
initializeLayers(tianDiTuKey)
|
|
352
|
+
|
|
353
|
+
// 创建地图实例 - 加载所有底图图层,但默认只显示高德地图
|
|
354
|
+
map = new Map({
|
|
355
|
+
target: mapRef.value,
|
|
356
|
+
layers: Object.values(baseMaps), // 加载所有底图图层
|
|
357
|
+
view: new View({
|
|
358
|
+
center: fromLonLat(center),
|
|
359
|
+
zoom,
|
|
360
|
+
projection: 'EPSG:3857',
|
|
361
|
+
maxZoom,
|
|
362
|
+
minZoom,
|
|
363
|
+
}),
|
|
364
|
+
controls: defaultControls({
|
|
365
|
+
zoom: false,
|
|
366
|
+
rotate: false,
|
|
367
|
+
attribution: false,
|
|
368
|
+
}).extend([
|
|
369
|
+
new ScaleLine({
|
|
370
|
+
units: 'metric',
|
|
371
|
+
className: 'ol-scale-line',
|
|
372
|
+
}),
|
|
373
|
+
]),
|
|
374
|
+
interactions: defaultInteractions({
|
|
375
|
+
altShiftDragRotate: false,
|
|
376
|
+
pinchRotate: false,
|
|
377
|
+
}),
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// 更新地图大小,确保地图正确渲染
|
|
381
|
+
setTimeout(() => {
|
|
382
|
+
if (map) {
|
|
383
|
+
map.updateSize()
|
|
384
|
+
// 确保默认图层正确显示
|
|
385
|
+
handleMapChange('tianditu')
|
|
386
|
+
// 地图初始化完成后解析 Promise
|
|
387
|
+
resolve()
|
|
388
|
+
}
|
|
389
|
+
}, 200)
|
|
390
|
+
|
|
391
|
+
// 监听地图移动结束事件
|
|
392
|
+
map.on('moveend', handleMoveEnd)
|
|
393
|
+
|
|
394
|
+
// 设置鼠标样式
|
|
395
|
+
if (mapRef.value) {
|
|
396
|
+
mapRef.value.style.cursor = 'grab'
|
|
397
|
+
// 监听地图事件
|
|
398
|
+
const mapElement = mapRef.value
|
|
399
|
+
|
|
400
|
+
// 鼠标按下时
|
|
401
|
+
mapElement.addEventListener('mousedown', () => {
|
|
402
|
+
mapElement.style.cursor = 'grabbing'
|
|
403
|
+
// 用户开始拖动地图,取消跟随定位
|
|
404
|
+
if (locationTimer) {
|
|
405
|
+
isFollowingLocation.value = false
|
|
406
|
+
}
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
// 触摸开始时
|
|
410
|
+
mapElement.addEventListener('touchstart', () => {
|
|
411
|
+
// 用户开始拖动地图,取消跟随定位
|
|
412
|
+
if (locationTimer) {
|
|
413
|
+
isFollowingLocation.value = false
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// 鼠标释放时
|
|
418
|
+
mapElement.addEventListener('mouseup', () => {
|
|
419
|
+
mapElement.style.cursor = 'grab'
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
// 鼠标离开地图时
|
|
423
|
+
mapElement.addEventListener('mouseleave', () => {
|
|
424
|
+
mapElement.style.cursor = 'grab'
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
console.error('地图初始化失败:', error)
|
|
431
|
+
resolve()
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* 获取地图实例
|
|
438
|
+
* @returns OpenLayers Map 实例
|
|
439
|
+
*/
|
|
440
|
+
function getMap(): Map | null {
|
|
441
|
+
return map
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 设置地图中心点
|
|
446
|
+
* @param center - 经纬度坐标 [经度, 纬度]
|
|
447
|
+
* @param animate - 是否使用动画效果,默认true
|
|
448
|
+
*/
|
|
449
|
+
function setCenter(center: [number, number], animate = true): void {
|
|
450
|
+
if (!map)
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
const view = map.getView()
|
|
454
|
+
if (animate) {
|
|
455
|
+
view.animate({
|
|
456
|
+
center: fromLonLat(center),
|
|
457
|
+
duration: 500,
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
view.setCenter(fromLonLat(center))
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 设置地图缩放级别
|
|
467
|
+
* @param zoom - 缩放级别
|
|
468
|
+
* @param animate - 是否使用动画效果,默认true
|
|
469
|
+
*/
|
|
470
|
+
function setZoom(zoom: number, animate = true): void {
|
|
471
|
+
if (!map)
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
const view = map.getView()
|
|
475
|
+
if (animate) {
|
|
476
|
+
view.animate({
|
|
477
|
+
zoom,
|
|
478
|
+
duration: 500,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
view.setZoom(zoom)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 获取当前地图缩放级别
|
|
488
|
+
* @returns 当前缩放级别
|
|
489
|
+
*/
|
|
490
|
+
function getZoom(): number {
|
|
491
|
+
if (!map)
|
|
492
|
+
return 0
|
|
493
|
+
return map.getView().getZoom() || 0
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 设置地图中心点和缩放级别
|
|
498
|
+
* @param center - 经纬度坐标 [经度, 纬度]
|
|
499
|
+
* @param zoom - 缩放级别
|
|
500
|
+
* @param animate - 是否使用动画效果,默认true
|
|
501
|
+
*/
|
|
502
|
+
function setCenterAndZoom(center: [number, number], zoom: number, animate = true): void {
|
|
503
|
+
if (!map)
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
const view = map.getView()
|
|
507
|
+
if (animate) {
|
|
508
|
+
view.animate({
|
|
509
|
+
center: fromLonLat(center),
|
|
510
|
+
zoom,
|
|
511
|
+
duration: 500,
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
view.setCenter(fromLonLat(center))
|
|
516
|
+
view.setZoom(zoom)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 创建点位要素
|
|
522
|
+
* @param point - 点位数据
|
|
523
|
+
* @param icon - 图标URL
|
|
524
|
+
* @param iconAnchor - 图标锚点
|
|
525
|
+
* @returns 返回要素实例
|
|
526
|
+
*/
|
|
527
|
+
function createPointFeature(point: PointData, icon: string, iconAnchor: [number, number] = [0.5, 1], scale: number = 0.5): Feature {
|
|
528
|
+
const feature = new Feature({
|
|
529
|
+
geometry: new Point(fromLonLat([point.longitude, point.latitude])),
|
|
530
|
+
properties: point,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const style = new Style({
|
|
534
|
+
image: new Icon({
|
|
535
|
+
src: icon,
|
|
536
|
+
scale,
|
|
537
|
+
anchor: iconAnchor,
|
|
538
|
+
anchorXUnits: 'fraction',
|
|
539
|
+
anchorYUnits: 'fraction',
|
|
540
|
+
crossOrigin: 'anonymous',
|
|
541
|
+
}),
|
|
542
|
+
text: new Text({
|
|
543
|
+
text: point.title || '',
|
|
544
|
+
offsetY: -35,
|
|
545
|
+
font: '12px sans-serif',
|
|
546
|
+
fill: new Fill({
|
|
547
|
+
color: '#333',
|
|
548
|
+
}),
|
|
549
|
+
stroke: new Stroke({
|
|
550
|
+
color: '#fff',
|
|
551
|
+
width: 2,
|
|
552
|
+
}),
|
|
553
|
+
}),
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
feature.setStyle(style)
|
|
557
|
+
return feature
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 创建点位图层
|
|
562
|
+
* @param config - 图层配置
|
|
563
|
+
* @returns 返回图层实例
|
|
564
|
+
*/
|
|
565
|
+
function createPointLayer(config: PointLayerConfig): VectorLayer<VectorSource> {
|
|
566
|
+
const vectorSource = new VectorSource()
|
|
567
|
+
const vectorLayer = new VectorLayer({
|
|
568
|
+
source: vectorSource,
|
|
569
|
+
visible: config.show,
|
|
570
|
+
zIndex: config.id === undefined ? 1 : 3, // 根据是否有ID决定层级
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// 添加点位要素
|
|
574
|
+
const addFeatures = (data: PointData[]) => {
|
|
575
|
+
// 清除现有要素
|
|
576
|
+
vectorSource.clear()
|
|
577
|
+
// 添加新的点位要素
|
|
578
|
+
data.forEach((point) => {
|
|
579
|
+
const feature = createPointFeature(point, config.icon, config.iconAnchor, config.scale)
|
|
580
|
+
vectorSource.addFeature(feature)
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 添加点击事件处理
|
|
585
|
+
if (config.onClick) {
|
|
586
|
+
map?.on('click', (event) => {
|
|
587
|
+
const feature = map.forEachFeatureAtPixel(event.pixel, feature => feature, {
|
|
588
|
+
layerFilter: layer => layer === vectorLayer,
|
|
589
|
+
})
|
|
590
|
+
if (feature) {
|
|
591
|
+
const properties = feature.getProperties()
|
|
592
|
+
const { geometry, ...pointData } = properties
|
|
593
|
+
config.onClick(pointData as PointData, event)
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 监听图层可见性变化
|
|
599
|
+
vectorLayer.on('change:visible', async () => {
|
|
600
|
+
if (vectorLayer.getVisible() && config.dataProvider) {
|
|
601
|
+
try {
|
|
602
|
+
const data = await config.dataProvider()
|
|
603
|
+
addFeatures(data)
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
console.error('获取点位数据失败:', error)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// 初始加载数据
|
|
612
|
+
if (config.show && config.dataProvider) {
|
|
613
|
+
const result = config.dataProvider()
|
|
614
|
+
if (result instanceof Promise) {
|
|
615
|
+
result.then((data) => {
|
|
616
|
+
addFeatures(data)
|
|
617
|
+
}).catch((error) => {
|
|
618
|
+
console.error('获取初始点位数据失败:', error)
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
addFeatures(result)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return vectorLayer
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* 添加点位图层
|
|
631
|
+
* @param config - 图层配置
|
|
632
|
+
* @returns 返回图层实例
|
|
633
|
+
*/
|
|
634
|
+
function addPointLayer(config: PointLayerConfig): VectorLayer<VectorSource> | null {
|
|
635
|
+
if (!map)
|
|
636
|
+
return null
|
|
637
|
+
|
|
638
|
+
const vectorLayer = createPointLayer(config)
|
|
639
|
+
map.addLayer(vectorLayer)
|
|
640
|
+
vectorLayers[config.id] = vectorLayer
|
|
641
|
+
|
|
642
|
+
// 更新图层状态
|
|
643
|
+
if (config.showInControl !== false) {
|
|
644
|
+
const existingIndex = pointLayerStatus.value.findIndex(layer => layer.id === config.id)
|
|
645
|
+
if (existingIndex === -1) {
|
|
646
|
+
pointLayerStatus.value.push(config)
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
pointLayerStatus.value[existingIndex] = config
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return vectorLayer
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 添加海量点图层
|
|
658
|
+
* @param config - 图层配置
|
|
659
|
+
*/
|
|
660
|
+
function addWebGLPoints(config: WebGLPointOptions): void {
|
|
661
|
+
if (!map)
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
const vectorSource = new VectorSource()
|
|
665
|
+
const vectorLayer = new VectorLayer({
|
|
666
|
+
source: vectorSource,
|
|
667
|
+
visible: config.show,
|
|
668
|
+
zIndex: config.id === undefined ? 1 : 3,
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// 添加点位要素
|
|
672
|
+
const addFeatures = (data: PointData[]) => {
|
|
673
|
+
// 清除现有要素
|
|
674
|
+
vectorSource.clear()
|
|
675
|
+
// 添加新的点位要素
|
|
676
|
+
data.forEach((point) => {
|
|
677
|
+
const feature = createPointFeature(point, config.icon, config.iconAnchor)
|
|
678
|
+
vectorSource.addFeature(feature)
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 添加点击事件处理
|
|
683
|
+
if (config.onClick) {
|
|
684
|
+
map.on('click', (event) => {
|
|
685
|
+
const feature = map.forEachFeatureAtPixel(event.pixel, feature => feature, {
|
|
686
|
+
layerFilter: layer => layer === vectorLayer,
|
|
687
|
+
})
|
|
688
|
+
if (feature) {
|
|
689
|
+
const properties = feature.getProperties()
|
|
690
|
+
const { geometry, ...pointData } = properties
|
|
691
|
+
config.onClick(pointData as PointData, event)
|
|
692
|
+
}
|
|
693
|
+
})
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 初始加载数据
|
|
697
|
+
if (config.show && config.dataProvider) {
|
|
698
|
+
const result = config.dataProvider()
|
|
699
|
+
if (result instanceof Promise) {
|
|
700
|
+
result.then((data) => {
|
|
701
|
+
addFeatures(data)
|
|
702
|
+
}).catch((error) => {
|
|
703
|
+
console.error('获取初始点位数据失败:', error)
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
addFeatures(result)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
map.addLayer(vectorLayer)
|
|
712
|
+
vectorLayers[config.id] = vectorLayer
|
|
713
|
+
|
|
714
|
+
// 更新图层状态
|
|
715
|
+
if (config.showInControl !== false) {
|
|
716
|
+
const existingIndex = pointLayerStatus.value.findIndex(layer => layer.id === config.id)
|
|
717
|
+
if (existingIndex === -1) {
|
|
718
|
+
pointLayerStatus.value.push(config)
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
pointLayerStatus.value[existingIndex] = config
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 添加 WMS 图层
|
|
728
|
+
* @param options - WMS 配置
|
|
729
|
+
*/
|
|
730
|
+
function addWMSLayers(options: WMSOptions): void {
|
|
731
|
+
if (!map)
|
|
732
|
+
return
|
|
733
|
+
|
|
734
|
+
const { layers, wms } = options
|
|
735
|
+
|
|
736
|
+
// 更新图层状态
|
|
737
|
+
wmsLayerStatus.value = layers.map(layer => ({
|
|
738
|
+
...layer,
|
|
739
|
+
show: layer.show,
|
|
740
|
+
}))
|
|
741
|
+
|
|
742
|
+
// 移除已存在的 WMS 图层
|
|
743
|
+
Object.values(wmsLayers).forEach((layer) => {
|
|
744
|
+
map?.removeLayer(layer)
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
// 清空图层记录
|
|
748
|
+
Object.keys(wmsLayers).forEach((key) => {
|
|
749
|
+
delete wmsLayers[key]
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
// 添加新的 WMS 图层
|
|
753
|
+
layers.forEach((layerConfig) => {
|
|
754
|
+
const wmsSource = new ImageWMS({
|
|
755
|
+
url: wms.url,
|
|
756
|
+
params: {
|
|
757
|
+
LAYERS: layerConfig.layerName,
|
|
758
|
+
FORMAT: wms.format,
|
|
759
|
+
VERSION: wms.version,
|
|
760
|
+
SRS: wms.srs,
|
|
761
|
+
},
|
|
762
|
+
ratio: 1,
|
|
763
|
+
serverType: 'geoserver',
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
const wmsLayer = new ImageLayer({
|
|
767
|
+
source: wmsSource,
|
|
768
|
+
visible: layerConfig.show,
|
|
769
|
+
zIndex: 2,
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
wmsLayers[layerConfig.layerName] = wmsLayer
|
|
773
|
+
map.addLayer(wmsLayer)
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* 控制 WMS 图层显示/隐藏
|
|
779
|
+
* @param layerName - 图层名称
|
|
780
|
+
* @param visible - 是否显示
|
|
781
|
+
*/
|
|
782
|
+
function setWMSLayerVisible(layerName: string, visible: boolean): void {
|
|
783
|
+
const layer = wmsLayers[layerName]
|
|
784
|
+
if (layer) {
|
|
785
|
+
layer.setVisible(visible)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* 切换 WMS 图层显示状态
|
|
791
|
+
* @param layer - 图层配置
|
|
792
|
+
*/
|
|
793
|
+
function handleToggleWMSLayer(layer: WMSLayerConfig): void {
|
|
794
|
+
layer.show = !layer.show
|
|
795
|
+
setWMSLayerVisible(layer.layerName, layer.show)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* 控制点位图层显示/隐藏
|
|
800
|
+
* @param layerId - 图层ID
|
|
801
|
+
* @param visible - 是否显示
|
|
802
|
+
*/
|
|
803
|
+
function setPointLayerVisible(layerId: number, visible: boolean): void {
|
|
804
|
+
const layer = vectorLayers[layerId]
|
|
805
|
+
if (layer) {
|
|
806
|
+
layer.setVisible(visible)
|
|
807
|
+
// 更新图层状态
|
|
808
|
+
const layerIndex = pointLayerStatus.value.findIndex(layer => layer.id === layerId)
|
|
809
|
+
if (layerIndex !== -1) {
|
|
810
|
+
pointLayerStatus.value[layerIndex].show = visible
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* 切换点位图层显示状态
|
|
817
|
+
*/
|
|
818
|
+
function handleTogglePointLayer(layer: PointLayerConfig): void {
|
|
819
|
+
layer.show = !layer.show
|
|
820
|
+
setPointLayerVisible(layer.id, layer.show)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** 处理定位请求 */
|
|
824
|
+
async function handleLocation() {
|
|
825
|
+
if (!map)
|
|
826
|
+
return
|
|
827
|
+
|
|
828
|
+
// 如果是导航模式,重新开启跟随定位
|
|
829
|
+
if (locationTimer) {
|
|
830
|
+
isFollowingLocation.value = true
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
mobileUtil.execute({
|
|
835
|
+
param: {},
|
|
836
|
+
funcName: 'getLocationResult',
|
|
837
|
+
callbackFunc: (result) => {
|
|
838
|
+
const res = result as PhoneLocationStatus
|
|
839
|
+
if (res.status === 'success') {
|
|
840
|
+
const locationResult = JSON.parse(res.data.location)
|
|
841
|
+
if (locationResult.longitude && locationResult.latitude) {
|
|
842
|
+
const center: [number, number] = [locationResult.longitude, locationResult.latitude]
|
|
843
|
+
setCenterAndZoom(center, getZoom())
|
|
844
|
+
// 更新位置图标
|
|
845
|
+
if (isFollowingLocation.value) {
|
|
846
|
+
updateLocationMarker(center)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
// 在 web 端测试时,使用模拟数据
|
|
855
|
+
console.log('获取实时位置失败,使用模拟数据')
|
|
856
|
+
// 生成一个随机偏移量
|
|
857
|
+
const offset = (Math.random() - 0.5) * 0.01
|
|
858
|
+
const center: [number, number] = [
|
|
859
|
+
108.948024 + offset, // 西安经度
|
|
860
|
+
34.263161 + offset, // 西安纬度
|
|
861
|
+
]
|
|
862
|
+
|
|
863
|
+
setCenterAndZoom(center, getZoom())
|
|
864
|
+
// 更新位置图标
|
|
865
|
+
if (isFollowingLocation.value) {
|
|
866
|
+
updateLocationMarker(center)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// 开启导航
|
|
872
|
+
function startNavigation() {
|
|
873
|
+
isFollowingLocation.value = true
|
|
874
|
+
|
|
875
|
+
// 创建并添加位置图标图层
|
|
876
|
+
if (!locationLayer) {
|
|
877
|
+
locationLayer = createLocationLayer()
|
|
878
|
+
map?.addLayer(locationLayer)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
locationTimer = setInterval(() => {
|
|
882
|
+
navigationHandleLocation()
|
|
883
|
+
}, 1000)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function stopNavigation() {
|
|
887
|
+
isFollowingLocation.value = false
|
|
888
|
+
if (locationTimer) {
|
|
889
|
+
clearInterval(locationTimer)
|
|
890
|
+
locationTimer = null
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// 移除位置图标图层
|
|
894
|
+
if (locationLayer && map) {
|
|
895
|
+
map.removeLayer(locationLayer)
|
|
896
|
+
locationLayer = null
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function navigationHandleLocation() {
|
|
901
|
+
// 打开导航定时器, 并且跟随定位
|
|
902
|
+
if (isFollowingLocation.value) {
|
|
903
|
+
handleLocation()
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* 添加轨迹图层
|
|
909
|
+
* @param trackData - 轨迹数据
|
|
910
|
+
*/
|
|
911
|
+
function addTrackLayer(trackData: TrackData): void {
|
|
912
|
+
if (!map)
|
|
913
|
+
return
|
|
914
|
+
|
|
915
|
+
const vectorSource = new VectorSource()
|
|
916
|
+
const vectorLayer = new VectorLayer({
|
|
917
|
+
source: vectorSource,
|
|
918
|
+
visible: true,
|
|
919
|
+
zIndex: 2,
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// 创建轨迹线要素
|
|
923
|
+
const coordinates = trackData.trackData.map(coord => fromLonLat(coord))
|
|
924
|
+
const lineString = new Feature({
|
|
925
|
+
geometry: new LineString(coordinates),
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
// 设置轨迹线样式
|
|
929
|
+
const lineStyle = new Style({
|
|
930
|
+
stroke: new Stroke({
|
|
931
|
+
color: trackData.color,
|
|
932
|
+
width: 3,
|
|
933
|
+
}),
|
|
934
|
+
})
|
|
935
|
+
lineString.setStyle(lineStyle)
|
|
936
|
+
|
|
937
|
+
// 创建起点和终点图标
|
|
938
|
+
const startPoint = new Feature({
|
|
939
|
+
geometry: new Point(coordinates[0]),
|
|
940
|
+
})
|
|
941
|
+
const endPoint = new Feature({
|
|
942
|
+
geometry: new Point(coordinates[coordinates.length - 1]),
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
// 设置起点图标样式 - 使用绿色圆形图标
|
|
946
|
+
const startStyle = new Style({
|
|
947
|
+
image: new Circle({
|
|
948
|
+
radius: 8,
|
|
949
|
+
fill: new Fill({
|
|
950
|
+
color: '#4CAF50',
|
|
951
|
+
}),
|
|
952
|
+
stroke: new Stroke({
|
|
953
|
+
color: '#fff',
|
|
954
|
+
width: 2,
|
|
955
|
+
}),
|
|
956
|
+
}),
|
|
957
|
+
text: new Text({
|
|
958
|
+
text: '起点',
|
|
959
|
+
offsetY: -15,
|
|
960
|
+
font: '12px sans-serif',
|
|
961
|
+
fill: new Fill({
|
|
962
|
+
color: '#333',
|
|
963
|
+
}),
|
|
964
|
+
stroke: new Stroke({
|
|
965
|
+
color: '#fff',
|
|
966
|
+
width: 2,
|
|
967
|
+
}),
|
|
968
|
+
}),
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
// 设置终点图标样式 - 使用红色圆形图标
|
|
972
|
+
const endStyle = new Style({
|
|
973
|
+
image: new Circle({
|
|
974
|
+
radius: 8,
|
|
975
|
+
fill: new Fill({
|
|
976
|
+
color: '#F44336',
|
|
977
|
+
}),
|
|
978
|
+
stroke: new Stroke({
|
|
979
|
+
color: '#fff',
|
|
980
|
+
width: 2,
|
|
981
|
+
}),
|
|
982
|
+
}),
|
|
983
|
+
text: new Text({
|
|
984
|
+
text: '终点',
|
|
985
|
+
offsetY: -15,
|
|
986
|
+
font: '12px sans-serif',
|
|
987
|
+
fill: new Fill({
|
|
988
|
+
color: '#333',
|
|
989
|
+
}),
|
|
990
|
+
stroke: new Stroke({
|
|
991
|
+
color: '#fff',
|
|
992
|
+
width: 2,
|
|
993
|
+
}),
|
|
994
|
+
}),
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
startPoint.setStyle(startStyle)
|
|
998
|
+
endPoint.setStyle(endStyle)
|
|
999
|
+
|
|
1000
|
+
// 添加要素到图层
|
|
1001
|
+
vectorSource.addFeatures([lineString, startPoint, endPoint])
|
|
1002
|
+
|
|
1003
|
+
// 添加到地图
|
|
1004
|
+
map.addLayer(vectorLayer)
|
|
1005
|
+
trackLayers[trackData.id] = vectorLayer
|
|
1006
|
+
|
|
1007
|
+
// 更新图层状态,确保 show 属性被正确设置
|
|
1008
|
+
const trackDataWithShow = {
|
|
1009
|
+
...trackData,
|
|
1010
|
+
show: true, // 默认显示
|
|
1011
|
+
}
|
|
1012
|
+
trackLayerStatus.value.push(trackDataWithShow)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* 控制轨迹图层显示/隐藏
|
|
1017
|
+
* @param trackId - 轨迹ID
|
|
1018
|
+
* @param visible - 是否显示
|
|
1019
|
+
*/
|
|
1020
|
+
function setTrackLayerVisible(trackId: number, visible: boolean): void {
|
|
1021
|
+
const layer = trackLayers[trackId]
|
|
1022
|
+
if (layer) {
|
|
1023
|
+
layer.setVisible(visible)
|
|
1024
|
+
// 更新图层状态
|
|
1025
|
+
const layerIndex = trackLayerStatus.value.findIndex(layer => layer.id === trackId)
|
|
1026
|
+
if (layerIndex !== -1) {
|
|
1027
|
+
trackLayerStatus.value[layerIndex].show = visible
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* 切换轨迹图层显示状态
|
|
1034
|
+
*/
|
|
1035
|
+
function handleToggleTrackLayer(track: TrackData): void {
|
|
1036
|
+
track.show = !track.show
|
|
1037
|
+
setTrackLayerVisible(track.id, track.show)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// 暴露方法给父组件
|
|
1041
|
+
defineExpose({
|
|
1042
|
+
updateLocationMarker,
|
|
1043
|
+
init,
|
|
1044
|
+
getMap,
|
|
1045
|
+
setCenter,
|
|
1046
|
+
setZoom,
|
|
1047
|
+
getZoom,
|
|
1048
|
+
setCenterAndZoom,
|
|
1049
|
+
addPointLayer,
|
|
1050
|
+
addWebGLPoints,
|
|
1051
|
+
addWMSLayers,
|
|
1052
|
+
setWMSLayerVisible,
|
|
1053
|
+
handleToggleWMSLayer,
|
|
1054
|
+
setPointLayerVisible,
|
|
1055
|
+
handleTogglePointLayer,
|
|
1056
|
+
getAddressInfo,
|
|
1057
|
+
handleLocation,
|
|
1058
|
+
startNavigation,
|
|
1059
|
+
stopNavigation,
|
|
1060
|
+
addTrackLayer,
|
|
1061
|
+
setTrackLayerVisible,
|
|
1062
|
+
handleToggleTrackLayer,
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
// 组件卸载时清理地图实例
|
|
1066
|
+
onUnmounted(() => {
|
|
1067
|
+
if (map) {
|
|
1068
|
+
stopNavigation()
|
|
1069
|
+
map.setTarget(undefined)
|
|
1070
|
+
map = null
|
|
1071
|
+
}
|
|
1072
|
+
if (locationTimer) {
|
|
1073
|
+
clearInterval(locationTimer)
|
|
1074
|
+
locationTimer = null
|
|
1075
|
+
}
|
|
1076
|
+
locationLayer = null
|
|
1077
|
+
})
|
|
1078
|
+
</script>
|
|
1079
|
+
|
|
1080
|
+
<template>
|
|
1081
|
+
<div class="map-wrapper">
|
|
1082
|
+
<div ref="mapRef" class="ol-map" />
|
|
1083
|
+
|
|
1084
|
+
<div v-if="!preview" class="location-button">
|
|
1085
|
+
<Button size="small" square @click="handleLocation">
|
|
1086
|
+
<img
|
|
1087
|
+
src="@af-mobile-client-vue3/assets/img/component/location.png"
|
|
1088
|
+
class="location-icon"
|
|
1089
|
+
alt="定位"
|
|
1090
|
+
>
|
|
1091
|
+
</Button>
|
|
1092
|
+
</div>
|
|
1093
|
+
<div class="map-controls">
|
|
1094
|
+
<!-- 控制按钮 -->
|
|
1095
|
+
<div class="control-toggle">
|
|
1096
|
+
<Button
|
|
1097
|
+
:type="showControls ? 'primary' : 'default'"
|
|
1098
|
+
size="small"
|
|
1099
|
+
square
|
|
1100
|
+
@click="showControls = !showControls"
|
|
1101
|
+
>
|
|
1102
|
+
<img
|
|
1103
|
+
src="@af-mobile-client-vue3/assets/img/component/mapLayers.png"
|
|
1104
|
+
class="toggle-icon"
|
|
1105
|
+
:class="[{ active: showControls }]"
|
|
1106
|
+
alt="图层"
|
|
1107
|
+
>
|
|
1108
|
+
</Button>
|
|
1109
|
+
</div>
|
|
1110
|
+
|
|
1111
|
+
<!-- 图层控制面板 -->
|
|
1112
|
+
<div v-show="showControls" class="control-panels">
|
|
1113
|
+
<!-- 底图切换 -->
|
|
1114
|
+
<div class="control-panel base-layer-control">
|
|
1115
|
+
<div class="control-title">
|
|
1116
|
+
<i class="van-icon van-icon-map-marked" /> 底图切换
|
|
1117
|
+
</div>
|
|
1118
|
+
<select v-model="currentMapType" @change="handleMapChange(currentMapType)">
|
|
1119
|
+
<option v-for="layer in layerOptions" :key="layer.value" :value="layer.value">
|
|
1120
|
+
{{ layer.text }}
|
|
1121
|
+
</option>
|
|
1122
|
+
</select>
|
|
1123
|
+
</div>
|
|
1124
|
+
|
|
1125
|
+
<!-- 图层控制 -->
|
|
1126
|
+
<div v-if="wmsLayerStatus.length > 0" class="control-panel layer-control">
|
|
1127
|
+
<div class="control-title">
|
|
1128
|
+
<i class="van-icon van-icon-layers" /> 图层控制
|
|
1129
|
+
</div>
|
|
1130
|
+
<div class="layer-list">
|
|
1131
|
+
<div
|
|
1132
|
+
v-for="layer in wmsLayerStatus"
|
|
1133
|
+
:key="layer.id"
|
|
1134
|
+
class="layer-item"
|
|
1135
|
+
:class="{ active: layer.show }"
|
|
1136
|
+
@click="handleToggleWMSLayer(layer)"
|
|
1137
|
+
>
|
|
1138
|
+
<i class="van-icon" :class="layer.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1139
|
+
<span>{{ layer.value }}</span>
|
|
1140
|
+
</div>
|
|
1141
|
+
</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
|
|
1144
|
+
<!-- 点位图层 -->
|
|
1145
|
+
<div v-if="pointLayerStatus.length > 0" class="control-panel layer-control">
|
|
1146
|
+
<div class="control-title">
|
|
1147
|
+
<i class="van-icon van-icon-location" /> 点位图层
|
|
1148
|
+
</div>
|
|
1149
|
+
<div class="layer-list">
|
|
1150
|
+
<div
|
|
1151
|
+
v-for="layer in pointLayerStatus"
|
|
1152
|
+
:key="layer.id"
|
|
1153
|
+
class="layer-item"
|
|
1154
|
+
:class="{ active: layer.show }"
|
|
1155
|
+
@click="handleTogglePointLayer(layer)"
|
|
1156
|
+
>
|
|
1157
|
+
<i class="van-icon" :class="layer.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1158
|
+
<span>{{ layer.value }}</span>
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
|
|
1163
|
+
<!-- 轨迹图层 -->
|
|
1164
|
+
<div v-if="trackLayerStatus.length > 0" class="control-panel layer-control">
|
|
1165
|
+
<div class="control-title">
|
|
1166
|
+
<i class="van-icon van-icon-location-o" /> 轨迹图层
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="layer-list">
|
|
1169
|
+
<div
|
|
1170
|
+
v-for="track in trackLayerStatus"
|
|
1171
|
+
:key="track.id"
|
|
1172
|
+
class="layer-item"
|
|
1173
|
+
:class="{ active: track.show }"
|
|
1174
|
+
@click="handleToggleTrackLayer(track)"
|
|
1175
|
+
>
|
|
1176
|
+
<i class="van-icon" :class="track.show ? 'van-icon-eye' : 'van-icon-closed-eye'" />
|
|
1177
|
+
<span>{{ track.name }}</span>
|
|
1178
|
+
</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
</div>
|
|
1184
|
+
</template>
|
|
1185
|
+
|
|
1186
|
+
<style lang="less" scoped>
|
|
1187
|
+
.map-wrapper {
|
|
1188
|
+
position: relative;
|
|
1189
|
+
width: 100%;
|
|
1190
|
+
height: 100%;
|
|
1191
|
+
|
|
1192
|
+
.ol-map {
|
|
1193
|
+
width: 100%;
|
|
1194
|
+
height: 100%;
|
|
1195
|
+
min-height: 200px;
|
|
1196
|
+
touch-action: none;
|
|
1197
|
+
|
|
1198
|
+
&:active {
|
|
1199
|
+
cursor: grabbing;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 定位按钮样式
|
|
1204
|
+
.location-button {
|
|
1205
|
+
position: absolute;
|
|
1206
|
+
left: 0.5em;
|
|
1207
|
+
bottom: calc(0.5em + 30px);
|
|
1208
|
+
z-index: 1000;
|
|
1209
|
+
|
|
1210
|
+
.van-button {
|
|
1211
|
+
width: 32px;
|
|
1212
|
+
height: 32px;
|
|
1213
|
+
padding: 4px;
|
|
1214
|
+
display: flex;
|
|
1215
|
+
align-items: center;
|
|
1216
|
+
justify-content: center;
|
|
1217
|
+
border: 1px solid #ebedf0;
|
|
1218
|
+
background: rgba(255, 255, 255, 0.9);
|
|
1219
|
+
backdrop-filter: blur(4px);
|
|
1220
|
+
box-shadow: none;
|
|
1221
|
+
|
|
1222
|
+
&:hover {
|
|
1223
|
+
border-color: #1989fa;
|
|
1224
|
+
.location-icon {
|
|
1225
|
+
opacity: 1;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.location-icon {
|
|
1230
|
+
width: 20px;
|
|
1231
|
+
height: 20px;
|
|
1232
|
+
opacity: 0.7;
|
|
1233
|
+
transition: all 0.3s;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
.map-controls {
|
|
1239
|
+
position: absolute;
|
|
1240
|
+
right: 10px;
|
|
1241
|
+
top: 10px;
|
|
1242
|
+
z-index: 1000;
|
|
1243
|
+
display: flex;
|
|
1244
|
+
flex-direction: column;
|
|
1245
|
+
gap: 8px;
|
|
1246
|
+
|
|
1247
|
+
.control-toggle {
|
|
1248
|
+
align-self: flex-end;
|
|
1249
|
+
|
|
1250
|
+
.van-button {
|
|
1251
|
+
width: 32px;
|
|
1252
|
+
height: 32px;
|
|
1253
|
+
padding: 4px;
|
|
1254
|
+
display: flex;
|
|
1255
|
+
align-items: center;
|
|
1256
|
+
justify-content: center;
|
|
1257
|
+
border: 1px solid #ebedf0;
|
|
1258
|
+
background: rgba(255, 255, 255, 0.9);
|
|
1259
|
+
backdrop-filter: blur(4px);
|
|
1260
|
+
box-shadow: none;
|
|
1261
|
+
|
|
1262
|
+
&.van-button--primary {
|
|
1263
|
+
background: #1989fa;
|
|
1264
|
+
border-color: #1989fa;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.toggle-icon {
|
|
1268
|
+
width: 20px;
|
|
1269
|
+
height: 20px;
|
|
1270
|
+
transition: all 0.3s;
|
|
1271
|
+
opacity: 0.7;
|
|
1272
|
+
|
|
1273
|
+
&.active {
|
|
1274
|
+
opacity: 1;
|
|
1275
|
+
filter: brightness(1) invert(1);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
&:hover {
|
|
1280
|
+
border-color: #1989fa;
|
|
1281
|
+
.toggle-icon {
|
|
1282
|
+
opacity: 1;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.control-panels {
|
|
1289
|
+
background: white;
|
|
1290
|
+
border-radius: 8px;
|
|
1291
|
+
padding: 12px;
|
|
1292
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
1293
|
+
min-width: 160px;
|
|
1294
|
+
max-width: 180px;
|
|
1295
|
+
|
|
1296
|
+
.control-panel {
|
|
1297
|
+
& + .control-panel {
|
|
1298
|
+
margin-top: 12px;
|
|
1299
|
+
padding-top: 12px;
|
|
1300
|
+
border-top: 1px solid #f5f5f5;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.control-title {
|
|
1304
|
+
font-size: 13px;
|
|
1305
|
+
font-weight: 500;
|
|
1306
|
+
color: #323233;
|
|
1307
|
+
margin-bottom: 8px;
|
|
1308
|
+
display: flex;
|
|
1309
|
+
align-items: center;
|
|
1310
|
+
gap: 4px;
|
|
1311
|
+
|
|
1312
|
+
.van-icon {
|
|
1313
|
+
font-size: 16px;
|
|
1314
|
+
color: #1989fa;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
select {
|
|
1319
|
+
width: 100%;
|
|
1320
|
+
padding: 6px 24px 6px 8px;
|
|
1321
|
+
font-size: 13px;
|
|
1322
|
+
border: 1px solid #dcdee0;
|
|
1323
|
+
border-radius: 4px;
|
|
1324
|
+
background-color: #f7f8fa;
|
|
1325
|
+
color: #323233;
|
|
1326
|
+
cursor: pointer;
|
|
1327
|
+
outline: none;
|
|
1328
|
+
appearance: none;
|
|
1329
|
+
-webkit-appearance: none;
|
|
1330
|
+
-moz-appearance: none;
|
|
1331
|
+
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
|
1332
|
+
background-repeat: no-repeat;
|
|
1333
|
+
background-position: right 6px center;
|
|
1334
|
+
background-size: 12px;
|
|
1335
|
+
|
|
1336
|
+
&:hover {
|
|
1337
|
+
border-color: #1989fa;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
option {
|
|
1341
|
+
padding: 6px;
|
|
1342
|
+
background: white;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.layer-list {
|
|
1347
|
+
display: flex;
|
|
1348
|
+
flex-direction: column;
|
|
1349
|
+
gap: 4px;
|
|
1350
|
+
|
|
1351
|
+
.layer-item {
|
|
1352
|
+
display: flex;
|
|
1353
|
+
align-items: center;
|
|
1354
|
+
gap: 6px;
|
|
1355
|
+
padding: 6px 8px;
|
|
1356
|
+
border-radius: 4px;
|
|
1357
|
+
cursor: pointer;
|
|
1358
|
+
transition: all 0.3s;
|
|
1359
|
+
background: #f7f8fa;
|
|
1360
|
+
border: 1px solid transparent;
|
|
1361
|
+
|
|
1362
|
+
.van-icon {
|
|
1363
|
+
font-size: 14px;
|
|
1364
|
+
color: #969799;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
span {
|
|
1368
|
+
font-size: 13px;
|
|
1369
|
+
color: #323233;
|
|
1370
|
+
flex: 1;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
&:hover {
|
|
1374
|
+
background: #f0f2f5;
|
|
1375
|
+
border-color: #dcdee0;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
&.active {
|
|
1379
|
+
background: #e6f3ff;
|
|
1380
|
+
border-color: #1989fa;
|
|
1381
|
+
|
|
1382
|
+
.van-icon {
|
|
1383
|
+
color: #1989fa;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
span {
|
|
1387
|
+
color: #1989fa;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// 比例尺样式
|
|
1398
|
+
.ol-scale-line {
|
|
1399
|
+
position: absolute;
|
|
1400
|
+
left: 0.5em;
|
|
1401
|
+
bottom: 0.5em;
|
|
1402
|
+
background: rgba(255, 255, 255, 0.8);
|
|
1403
|
+
padding: 2px;
|
|
1404
|
+
border-radius: 4px;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
.ol-scale-line-inner {
|
|
1408
|
+
border: 2px solid #333;
|
|
1409
|
+
border-top: none;
|
|
1410
|
+
color: #333;
|
|
1411
|
+
font-size: 12px;
|
|
1412
|
+
text-align: center;
|
|
1413
|
+
margin: 1px;
|
|
1414
|
+
will-change: contents, width;
|
|
1415
|
+
transition: width 0.25s;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// 移动端适配
|
|
1419
|
+
@media screen and (max-width: 768px) {
|
|
1420
|
+
.map-controls {
|
|
1421
|
+
right: 8px;
|
|
1422
|
+
top: 8px;
|
|
1423
|
+
gap: 6px;
|
|
1424
|
+
|
|
1425
|
+
.control-panels {
|
|
1426
|
+
padding: 10px;
|
|
1427
|
+
min-width: 140px;
|
|
1428
|
+
|
|
1429
|
+
.control-panel {
|
|
1430
|
+
& + .control-panel {
|
|
1431
|
+
margin-top: 10px;
|
|
1432
|
+
padding-top: 10px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.control-title {
|
|
1436
|
+
font-size: 12px;
|
|
1437
|
+
margin-bottom: 6px;
|
|
1438
|
+
|
|
1439
|
+
.van-icon {
|
|
1440
|
+
font-size: 14px;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
select {
|
|
1445
|
+
padding: 4px 20px 4px 6px;
|
|
1446
|
+
font-size: 12px;
|
|
1447
|
+
background-size: 10px;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
.layer-list {
|
|
1451
|
+
gap: 3px;
|
|
1452
|
+
|
|
1453
|
+
.layer-item {
|
|
1454
|
+
padding: 4px 6px;
|
|
1455
|
+
|
|
1456
|
+
.van-icon {
|
|
1457
|
+
font-size: 12px;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
span {
|
|
1461
|
+
font-size: 12px;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// 比例尺样式
|
|
1471
|
+
:deep(.ol-scale-line) {
|
|
1472
|
+
position: absolute;
|
|
1473
|
+
left: 0.5em;
|
|
1474
|
+
bottom: 0.5em;
|
|
1475
|
+
background: rgba(255, 255, 255, 0.8);
|
|
1476
|
+
padding: 2px;
|
|
1477
|
+
border-radius: 4px;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
:deep(.ol-scale-line-inner) {
|
|
1481
|
+
border: 2px solid #333;
|
|
1482
|
+
border-top: none;
|
|
1483
|
+
color: #333;
|
|
1484
|
+
font-size: 12px;
|
|
1485
|
+
text-align: center;
|
|
1486
|
+
margin: 1px;
|
|
1487
|
+
will-change: contents, width;
|
|
1488
|
+
transition: width 0.25s;
|
|
1489
|
+
}
|
|
1490
|
+
</style>
|