af-mobile-client-vue3 1.3.16 → 1.3.18

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