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.
Files changed (284) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.cursorrules +60 -60
  3. package/.editorconfig +9 -9
  4. package/.env +10 -10
  5. package/.env.development +1 -1
  6. package/.env.production +1 -1
  7. package/.node-version +1 -1
  8. package/.vscode/extensions.json +12 -12
  9. package/.vscode/settings.json +68 -66
  10. package/CLAUDE.md +218 -189
  11. package/README.md +182 -182
  12. package/af-example-mobile-vue-web.iml +9 -9
  13. package/build/vite/index.ts +98 -98
  14. package/build/vite/optimize.ts +34 -34
  15. package/build/vite/vconsole.ts +47 -47
  16. package/commitlint.config.ts +32 -32
  17. package/compress.js +36 -36
  18. package/eslint.config.ts +31 -30
  19. package/index.html +23 -23
  20. package/mock/data.ts +20 -20
  21. package/mock/index.ts +7 -7
  22. package/mock/modules/prose.mock.ts +13 -13
  23. package/mock/modules/user.mock.ts +95 -152
  24. package/mock/util.ts +19 -19
  25. package/netlify.toml +12 -12
  26. package/package.json +135 -114
  27. package/postcss.config.ts +27 -27
  28. package/public/favicon.svg +4 -4
  29. package/public/safari-pinned-tab.svg +4 -4
  30. package/scripts/verifyCommit.js +19 -19
  31. package/src/App.vue +79 -79
  32. package/src/api/auth/index.ts +77 -0
  33. package/src/api/auth/types.ts +200 -0
  34. package/src/api/mock/index.ts +30 -30
  35. package/src/api/user/index.ts +40 -40
  36. package/src/assets/img/user/login/background-shadow-1.svg +20 -20
  37. package/src/assets/img/user/login/logo-background.svg +20 -20
  38. package/src/bootstrap.ts +26 -26
  39. package/src/components/core/BeautifulLoading/index.vue +52 -52
  40. package/src/components/core/ImageUploader/index.vue +251 -251
  41. package/src/components/core/NavBar/index.vue +53 -53
  42. package/src/components/core/Tabbar/index.vue +32 -32
  43. package/src/components/core/Uploader/index.vue +124 -124
  44. package/src/components/core/XGridDropOption/index.vue +154 -154
  45. package/src/components/core/XMultiSelect/index.vue +183 -183
  46. package/src/components/core/XSelect/index.vue +149 -149
  47. package/src/components/data/CardContainer/CardContainer.vue +118 -118
  48. package/src/components/data/CardContainer/CardHeader.vue +99 -99
  49. package/src/components/data/InfoDisplay/index.vue +132 -132
  50. package/src/components/data/UserDetail/api.ts +24 -24
  51. package/src/components/data/UserDetail/index.vue +620 -620
  52. package/src/components/data/UserDetail/recordEntries.ts +159 -159
  53. package/src/components/data/UserDetail/types.ts +26 -26
  54. package/src/components/data/XBadge/index.vue +82 -82
  55. package/src/components/data/XCellDetail/index.vue +105 -105
  56. package/src/components/data/XCellList/XCellList.md +432 -432
  57. package/src/components/data/XCellList/index.vue +1436 -1436
  58. package/src/components/data/XCellListFilter/QrScanner/index.vue +207 -207
  59. package/src/components/data/XCellListFilter/QrScanner/startScanAnimation.ts +53 -53
  60. package/src/components/data/XCellListFilter/VpnRecognition/index.vue +119 -119
  61. package/src/components/data/XCellListFilter/index.vue +705 -705
  62. package/src/components/data/XForm/index.vue +659 -659
  63. package/src/components/data/XFormGroup/doc/DeviceForm.vue +122 -122
  64. package/src/components/data/XFormGroup/doc/FormGroupDemo.vue +56 -56
  65. package/src/components/data/XFormGroup/doc/README.md +286 -286
  66. package/src/components/data/XFormGroup/doc/UserForm.vue +102 -102
  67. package/src/components/data/XFormGroup/index.vue +240 -240
  68. package/src/components/data/XFormItem/index.vue +1310 -1310
  69. package/src/components/data/XOlMap/README.md +227 -227
  70. package/src/components/data/XOlMap/XLocationPicker/index.vue +226 -226
  71. package/src/components/data/XOlMap/index.vue +1490 -1490
  72. package/src/components/data/XOlMap/types.ts +149 -149
  73. package/src/components/data/XOlMap/utils/{wgs84ToGcj02.js → wgs84ToGcj02.ts} +171 -154
  74. package/src/components/data/XReportForm/DateTimeSecondsPicker.vue +208 -208
  75. package/src/components/data/XReportForm/XReportFormJsonRender.vue +220 -220
  76. package/src/components/data/XReportForm/index.vue +1393 -1393
  77. package/src/components/data/XReportGrid/XAddReport/XAddReport.vue +198 -198
  78. package/src/components/data/XReportGrid/XAddReport/index.js +3 -3
  79. package/src/components/data/XReportGrid/XAddReport/index.md +53 -53
  80. package/src/components/data/XReportGrid/XAddReport/index.ts +10 -10
  81. package/src/components/data/XReportGrid/XReport.vue +960 -960
  82. package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
  83. package/src/components/data/XReportGrid/XReportDesign.vue +597 -597
  84. package/src/components/data/XReportGrid/XReportDrawer/XReportDrawer.vue +148 -148
  85. package/src/components/data/XReportGrid/XReportDrawer/index.js +3 -3
  86. package/src/components/data/XReportGrid/XReportDrawer/index.ts +10 -10
  87. package/src/components/data/XReportGrid/XReportJsonRender.vue +399 -399
  88. package/src/components/data/XReportGrid/XReportTrGroup.vue +592 -592
  89. package/src/components/data/XReportGrid/index.md +46 -46
  90. package/src/components/data/XReportGrid/print.js +184 -184
  91. package/src/components/data/XSignature/index.vue +284 -284
  92. package/src/components/data/XTag/index.vue +10 -10
  93. package/src/components/layout/NormalDataLayout/index.vue +69 -69
  94. package/src/components/layout/TabBarLayout/index.vue +40 -40
  95. package/src/composables/dark.ts +5 -5
  96. package/src/config/routes.ts +9 -9
  97. package/src/constants/index.ts +2 -2
  98. package/src/enums/requestEnum.ts +25 -25
  99. package/src/expression/ExpressionRunner.ts +28 -28
  100. package/src/expression/TestExpression.ts +510 -510
  101. package/src/expression/core/Delegate.ts +116 -116
  102. package/src/expression/core/Expression.ts +1359 -1359
  103. package/src/expression/core/Program.ts +985 -985
  104. package/src/expression/core/Token.ts +29 -29
  105. package/src/expression/enums/ExpressionType.ts +81 -81
  106. package/src/expression/enums/TokenType.ts +11 -11
  107. package/src/expression/exception/BreakWayException.ts +2 -2
  108. package/src/expression/exception/ContinueWayException.ts +2 -2
  109. package/src/expression/exception/ExpressionException.ts +29 -29
  110. package/src/expression/exception/ReturnWayException.ts +14 -14
  111. package/src/expression/exception/ServiceException.ts +22 -22
  112. package/src/expression/instances/JSONArray.ts +52 -52
  113. package/src/expression/instances/JSONObject.ts +118 -118
  114. package/src/expression/instances/LogicConsole.ts +31 -31
  115. package/src/font-style/font.css +4 -4
  116. package/src/hooks/useBoolean.ts +26 -26
  117. package/src/hooks/useCommon.ts +9 -9
  118. package/src/hooks/useLoading.ts +16 -16
  119. package/src/hooks/useLogin.ts +97 -97
  120. package/src/icons/svg/check-in.svg +32 -32
  121. package/src/icons/svg/dark.svg +4 -4
  122. package/src/icons/svg/github.svg +4 -4
  123. package/src/icons/svg/light.svg +4 -4
  124. package/src/icons/svg/link.svg +4 -4
  125. package/src/icons/svgo.yml +22 -22
  126. package/src/index.ts +4 -0
  127. package/src/layout/GridView/index.vue +16 -16
  128. package/src/layout/PageLayout.vue +9 -9
  129. package/src/layout/SingleLayout.vue +9 -9
  130. package/src/locales/en-US.json +128 -128
  131. package/src/locales/zh-CN.json +128 -128
  132. package/src/logic/LogicRunner.ts +67 -67
  133. package/src/logic/TestLogic.ts +13 -13
  134. package/src/logic/plugins/common/DateTools.ts +35 -35
  135. package/src/logic/plugins/common/VueTools.ts +30 -30
  136. package/src/logic/plugins/index.ts +7 -7
  137. package/src/main.ts +44 -44
  138. package/src/plugins/AppData.ts +38 -38
  139. package/src/plugins/GetLoginInfoService.ts +10 -10
  140. package/src/plugins/collectIcons.ts +10 -10
  141. package/src/plugins/index.ts +11 -11
  142. package/src/router/README.md +8 -8
  143. package/src/router/external-routes.ts +60 -0
  144. package/src/router/guards.ts +131 -59
  145. package/src/router/index.ts +35 -35
  146. package/src/router/invoiceRoutes.ts +33 -33
  147. package/src/router/routes.ts +426 -347
  148. package/src/services/api/Login.ts +6 -6
  149. package/src/services/api/common.ts +109 -109
  150. package/src/services/api/index.ts +7 -7
  151. package/src/services/api/manage.ts +8 -8
  152. package/src/services/api/search.ts +16 -16
  153. package/src/services/api/user.ts +17 -17
  154. package/src/services/restTools.ts +56 -56
  155. package/src/services/v3Api.ts +147 -147
  156. package/src/stores/index.ts +13 -13
  157. package/src/stores/modules/counter.ts +19 -19
  158. package/src/stores/modules/homeApp.ts +55 -55
  159. package/src/stores/modules/routeCache.ts +22 -23
  160. package/src/stores/modules/setting.ts +87 -87
  161. package/src/stores/modules/user.ts +326 -235
  162. package/src/stores/mutation-type.ts +12 -7
  163. package/src/styles/app.less +36 -36
  164. package/src/styles/login.less +109 -109
  165. package/src/styles/var.less +25 -25
  166. package/src/types/auth.ts +85 -0
  167. package/src/types/env.d.ts +16 -16
  168. package/src/types/platform.ts +194 -0
  169. package/src/types/settings.ts +1 -1
  170. package/src/types/vue-router.d.ts +13 -9
  171. package/src/utils/Storage.ts +124 -124
  172. package/src/utils/authority-utils.ts +84 -84
  173. package/src/utils/common.ts +41 -41
  174. package/src/utils/crypto.ts +39 -39
  175. package/src/utils/dataUtil.ts +42 -42
  176. package/src/utils/dictUtil.ts +52 -52
  177. package/src/utils/http/index.ts +201 -199
  178. package/src/utils/i18n.ts +72 -72
  179. package/src/utils/indexedDB.ts +195 -195
  180. package/src/utils/inline-px-to-vw.ts +28 -28
  181. package/src/utils/mobileUtil.ts +33 -34
  182. package/src/utils/platform-auth.ts +134 -0
  183. package/src/utils/progress.ts +19 -19
  184. package/src/utils/routerUtil.ts +271 -271
  185. package/src/utils/runEvalFunction.ts +13 -13
  186. package/src/utils/secureStorage.ts +70 -71
  187. package/src/utils/set-page-title.ts +5 -5
  188. package/src/utils/validate.ts +6 -6
  189. package/src/views/chat/index.vue +153 -153
  190. package/src/views/common/Forbidden.vue +97 -0
  191. package/src/views/common/LoadError.vue +63 -63
  192. package/src/views/common/NotFound.vue +67 -67
  193. package/src/views/component/EvaluateRecordView/index.vue +40 -40
  194. package/src/views/component/IconifyView/index.vue +504 -504
  195. package/src/views/component/UserDetailView/UserDetailPage.vue +77 -77
  196. package/src/views/component/UserDetailView/index.vue +234 -234
  197. package/src/views/component/XCellDetailView/index.vue +217 -217
  198. package/src/views/component/XCellListView/index.vue +108 -129
  199. package/src/views/component/XFormAppraiseView/index.vue +174 -174
  200. package/src/views/component/XFormGroupView/index.vue +78 -82
  201. package/src/views/component/XFormView/index.vue +27 -27
  202. package/src/views/component/XOlMapView/XLocationPicker/index.vue +118 -118
  203. package/src/views/component/XOlMapView/index.vue +434 -434
  204. package/src/views/component/XOlMapView/testData.ts +64 -64
  205. package/src/views/component/XReportFormIframeView/index.vue +47 -47
  206. package/src/views/component/XReportFormView/index.vue +13 -13
  207. package/src/views/component/XReportGridView/index.vue +17 -17
  208. package/src/views/component/XRequestView/index.vue +234 -234
  209. package/src/views/component/XSignatureView/index.vue +50 -50
  210. package/src/views/component/index.vue +181 -181
  211. package/src/views/component/menu.vue +117 -117
  212. package/src/views/component/notice.vue +46 -46
  213. package/src/views/component/topNav.vue +36 -36
  214. package/src/views/external/index.vue +152 -0
  215. package/src/views/invoiceShow/index.vue +61 -61
  216. package/src/views/loading/AuthLoading.vue +345 -0
  217. package/src/views/user/login/ForgetPasswordForm.vue +94 -94
  218. package/src/views/user/login/LoginForm.vue +350 -347
  219. package/src/views/user/login/LoginTitle.vue +76 -76
  220. package/src/views/user/login/LoginWave.vue +109 -109
  221. package/src/views/user/login/index.vue +22 -22
  222. package/src/views/user/my/comm/ModifyPassword.vue +346 -346
  223. package/src/views/user/my/index.vue +507 -507
  224. package/src/views/user/register/index.vue +952 -952
  225. package/src/views/userRecords/AbnormalAlarmRecords.vue +21 -21
  226. package/src/views/userRecords/CardReplacementRecords.vue +21 -21
  227. package/src/views/userRecords/ChangeRecords.vue +19 -19
  228. package/src/views/userRecords/CommandViewRecords.vue +20 -20
  229. package/src/views/userRecords/GasCompensationRecords.vue +20 -20
  230. package/src/views/userRecords/InstrumentCollectionRecords.vue +21 -21
  231. package/src/views/userRecords/MeterRecords.vue +20 -20
  232. package/src/views/userRecords/OperateRecords.vue +51 -51
  233. package/src/views/userRecords/OtherChargeRecords.vue +19 -19
  234. package/src/views/userRecords/PaymentRecords.vue +28 -28
  235. package/src/views/userRecords/PriceAdjustmentRecords.vue +19 -19
  236. package/src/views/userRecords/ReplacementRecords.vue +19 -19
  237. package/src/views/userRecords/SafetyRecords.vue +19 -19
  238. package/src/views/userRecords/TransactionRecords.vue +21 -21
  239. package/src/views/userRecords/TransferRecords.vue +19 -19
  240. package/src/views/userRecords/operateRecordDetail/index.vue +316 -316
  241. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AddUserDetail.vue +124 -124
  242. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AdvanceDeliveryDetail.vue +88 -88
  243. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsCancelDetail.vue +205 -205
  244. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsDetail.vue +192 -192
  245. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankDkDetail.vue +192 -192
  246. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankPayDetail.vue +192 -192
  247. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BlacklistDetail.vue +153 -153
  248. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CancellationDetail.vue +101 -101
  249. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterCancelDetail.vue +127 -127
  250. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterDetail.vue +153 -153
  251. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardOverUserDetail.vue +153 -153
  252. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterCancelDetail.vue +166 -166
  253. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterDetail.vue +205 -205
  254. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/DisableManageDetail.vue +127 -127
  255. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/EnableManageDetail.vue +114 -114
  256. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FaZheChangeDetail.vue +124 -124
  257. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FeeDeductionDetail.vue +153 -153
  258. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/GasPriceChangeDetail.vue +126 -126
  259. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/InputtorChangeDetail.vue +126 -126
  260. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterCancelDetail.vue +114 -114
  261. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterDetail.vue +127 -127
  262. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotOpenDetail.vue +88 -88
  263. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineCardDetail.vue +101 -101
  264. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterCancelDetail.vue +218 -218
  265. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterDetail.vue +153 -153
  266. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OffGasAddGasDetail.vue +140 -140
  267. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeCancelDetail.vue +127 -127
  268. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeDetail.vue +114 -114
  269. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OverUserChangeDetail.vue +127 -127
  270. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReBillDetail.vue +127 -127
  271. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/RefundDetail.vue +114 -114
  272. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageCancelDetail.vue +127 -127
  273. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageDetail.vue +114 -114
  274. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/SaleCardGasDetail.vue +140 -140
  275. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageCancelDetail.vue +152 -152
  276. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageDetail.vue +178 -178
  277. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/UserChangeDetail.vue +123 -123
  278. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/WechatPayDetail.vue +192 -192
  279. package/src/views/userRecords/types.ts +66 -66
  280. package/tsconfig.json +39 -39
  281. package/uno.config.ts +82 -82
  282. package/vite.config.ts +119 -118
  283. package/src/router/types.ts +0 -7
  284. 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>