af-mobile-client-vue3 1.0.94 → 1.0.96

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 (113) hide show
  1. package/.env +6 -6
  2. package/.env.development +4 -4
  3. package/.env.envoiceShow +6 -6
  4. package/.env.production +6 -6
  5. package/.husky/commit-msg +1 -1
  6. package/.husky/pre-commit +1 -1
  7. package/.vscode/settings.json +61 -61
  8. package/build/vite/index.ts +91 -91
  9. package/eslint.config.js +9 -1
  10. package/mock/modules/user.mock.ts +152 -152
  11. package/package.json +1 -1
  12. package/public/favicon.svg +4 -4
  13. package/public/safari-pinned-tab.svg +32 -32
  14. package/scripts/verifyCommit.js +19 -19
  15. package/src/App.vue +43 -43
  16. package/src/api/user/index.ts +40 -40
  17. package/src/bootstrap.ts +18 -18
  18. package/src/components/core/App/MicroAppView.vue +3 -3
  19. package/src/components/core/NavBar/index.vue +12 -12
  20. package/src/components/core/SvgIcon/index.vue +1 -1
  21. package/src/components/core/Tabbar/index.vue +38 -38
  22. package/src/components/core/Uploader/index.vue +1 -1
  23. package/src/components/core/XGridDropOption/index.vue +151 -151
  24. package/src/components/core/XMultiSelect/index.vue +183 -183
  25. package/src/components/data/XCellDetail/index.vue +106 -106
  26. package/src/components/data/XCellList/XCellList.md +28 -28
  27. package/src/components/data/XCellList/index.vue +50 -11
  28. package/src/components/data/XCellListFilter/QrScanner/index.vue +1 -1
  29. package/src/components/data/XCellListFilter/VpnRecognition/index.vue +3 -3
  30. package/src/components/data/XCellListFilter/index.vue +160 -62
  31. package/src/components/data/XForm/index.vue +2 -2
  32. package/src/components/data/XFormGroup/index.vue +3 -3
  33. package/src/components/data/XFormItem/index.vue +25 -26
  34. package/src/components/data/XOlMap/demo.vue +209 -0
  35. package/src/components/data/XOlMap/index.vue +644 -0
  36. package/src/components/data/XReportForm/XReportFormJsonRender.vue +220 -220
  37. package/src/components/data/XReportForm/index.vue +1079 -1079
  38. package/src/components/data/XReportGrid/XAddReport/XAddReport.vue +4 -7
  39. package/src/components/data/XReportGrid/XAddReport/index.md +3 -7
  40. package/src/components/data/XReportGrid/XAddReport/index.ts +2 -2
  41. package/src/components/data/XReportGrid/XReport.vue +25 -38
  42. package/src/components/data/XReportGrid/XReportDesign.vue +46 -46
  43. package/src/components/data/XReportGrid/XReportDrawer/XReportDrawer.vue +3 -3
  44. package/src/components/data/XReportGrid/XReportDrawer/index.ts +2 -2
  45. package/src/components/data/XReportGrid/XReportJsonRender.vue +20 -7
  46. package/src/components/data/XReportGrid/XReportTrGroup.vue +4 -4
  47. package/src/components/data/XReportGrid/index.md +4 -6
  48. package/src/components/data/XSignature/index.vue +285 -285
  49. package/src/components/data/XTag/index.vue +10 -10
  50. package/src/components/layout/NormalDataLayout/index.vue +70 -70
  51. package/src/components/layout/TabBarLayout/index.vue +40 -40
  52. package/src/components.d.ts +53 -53
  53. package/src/env.d.ts +16 -16
  54. package/src/font-style/font.css +3 -3
  55. package/src/hooks/useCommon.ts +9 -9
  56. package/src/layout/GridView/index.vue +1 -1
  57. package/src/layout/PageLayout.vue +5 -5
  58. package/src/layout/SingleLayout.vue +3 -3
  59. package/src/locales/en-US.json +25 -25
  60. package/src/locales/zh-CN.json +25 -25
  61. package/src/plugins/AppData.ts +38 -38
  62. package/src/plugins/index.ts +1 -1
  63. package/src/router/guards.ts +59 -59
  64. package/src/router/index.ts +61 -60
  65. package/src/router/invoiceRoutes.ts +33 -33
  66. package/src/router/routes.ts +20 -14
  67. package/src/services/api/common.ts +109 -109
  68. package/src/services/api/manage.ts +8 -8
  69. package/src/services/restTools.ts +52 -52
  70. package/src/services/v3Api.ts +46 -35
  71. package/src/stores/modules/cachedView.ts +1 -1
  72. package/src/stores/modules/setting.ts +52 -52
  73. package/src/stores/modules/user.ts +5 -5
  74. package/src/stores/mutation-type.ts +7 -7
  75. package/src/styles/app.less +6 -1
  76. package/src/utils/Storage.ts +1 -1
  77. package/src/utils/authority-utils.ts +84 -84
  78. package/src/utils/crypto.ts +39 -39
  79. package/src/utils/http/index.ts +6 -6
  80. package/src/utils/i18n.ts +41 -41
  81. package/src/utils/indexedDB.ts +180 -180
  82. package/src/utils/mobileUtil.ts +26 -26
  83. package/src/utils/routerUtil.ts +271 -271
  84. package/src/utils/runEvalFunction.ts +13 -13
  85. package/src/utils/validate.ts +1 -1
  86. package/src/utils/wechatUtil.ts +9 -9
  87. package/src/views/chat/index.vue +1 -1
  88. package/src/views/common/LoadError.vue +64 -64
  89. package/src/views/common/NotFound.vue +68 -68
  90. package/src/views/component/EvaluateRecordView/index.vue +40 -40
  91. package/src/views/component/XCellDetailView/index.vue +217 -216
  92. package/src/views/component/XCellListView/index.vue +1 -1
  93. package/src/views/component/XFormAppraiseView/index.vue +4 -4
  94. package/src/views/component/XFormGroupView/index.vue +1 -1
  95. package/src/views/component/XFormView/index.vue +6 -7
  96. package/src/views/component/XReportFormIframeView/index.vue +47 -47
  97. package/src/views/component/XReportFormView/index.vue +13 -13
  98. package/src/views/component/XReportGridView/index.vue +2 -3
  99. package/src/views/component/XSignatureView/index.vue +50 -50
  100. package/src/views/component/index.vue +4 -4
  101. package/src/views/component/menu.vue +117 -117
  102. package/src/views/component/notice.vue +46 -46
  103. package/src/views/component/topNav.vue +36 -36
  104. package/src/views/invoiceShow/index.vue +61 -62
  105. package/src/views/user/login/ForgetPasswordForm.vue +94 -93
  106. package/src/views/user/login/LoginForm.vue +8 -7
  107. package/src/views/user/login/LoginTitle.vue +68 -68
  108. package/src/views/user/login/index.vue +22 -22
  109. package/src/views/user/my/index.vue +230 -230
  110. package/src/vue-router.d.ts +9 -9
  111. package/tsconfig.json +43 -43
  112. package/uno.config.ts +1 -1
  113. package/vite.config.ts +123 -123
@@ -0,0 +1,644 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * OpenLayers地图组件
4
+ * 支持多种底图切换:
5
+ * - 高德地图(矢量)
6
+ * - 高德卫星图
7
+ * - 天地图(矢量)
8
+ * - 天地图卫星图
9
+ */
10
+ import { Map, View } from 'ol'
11
+ import { defaults as defaultControls, ScaleLine } from 'ol/control'
12
+ import Feature from 'ol/Feature'
13
+ import Point from 'ol/geom/Point'
14
+ import { defaults as defaultInteractions } from 'ol/interaction'
15
+ import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
16
+ import { fromLonLat } from 'ol/proj'
17
+ import { Vector as VectorSource, XYZ } from 'ol/source'
18
+ import { Circle as CircleStyle, Fill, Icon, Stroke, Style } from 'ol/style'
19
+ import { onUnmounted, ref } from 'vue'
20
+ import 'vant/lib/index.css' // 添加 Vant 样式
21
+
22
+ /** 初始化参数接口 */
23
+ interface InitParams {
24
+ /** 地图中心点坐标 [经度, 纬度] */
25
+ center?: [number, number]
26
+ /** 地图缩放级别 */
27
+ zoom?: number
28
+ /** 最大缩放级别 */
29
+ maxZoom?: number
30
+ /** 最小缩放级别 */
31
+ minZoom?: number
32
+ /** 天地图密钥 */
33
+ tianDiTuKey?: string
34
+ }
35
+
36
+ /** 点位数据接口 */
37
+ interface PointData {
38
+ /** 经度 */
39
+ longitude: number
40
+ /** 纬度 */
41
+ latitude: number
42
+ /** 点标题 */
43
+ title?: string
44
+ /** 自定义数据 */
45
+ extData?: Record<string, any>
46
+ }
47
+
48
+ /** 点位图层配置接口 */
49
+ interface PointLayerOptions {
50
+ /** 图标URL地址 */
51
+ icon: string
52
+ /** 图标大小 [宽, 高] */
53
+ iconSize?: [number, number]
54
+ /** 图标锚点位置 [x, y],范围 0-1 */
55
+ iconAnchor?: [number, number]
56
+ /** 点位数据数组 */
57
+ data: PointData[]
58
+ /** 点击事件回调 */
59
+ onClick?: (point: PointData, event: any) => void
60
+ }
61
+
62
+ /** WebGL渲染配置接口 */
63
+ interface WebGLPointOptions extends PointLayerOptions {
64
+ /** 是否开启聚合 */
65
+ enableCluster?: boolean
66
+ /** 聚合距离,单位像素 */
67
+ clusterDistance?: number
68
+ /** 聚合样式配置 */
69
+ clusterStyle?: {
70
+ /** 填充颜色 */
71
+ fillColor?: string
72
+ /** 边框颜色 */
73
+ strokeColor?: string
74
+ /** 文字颜色 */
75
+ textColor?: string
76
+ }
77
+ /** 渲染性能配置 */
78
+ performance?: {
79
+ /** 是否开启分块加载 */
80
+ enableChunk?: boolean
81
+ /** 每块数据量 */
82
+ chunkSize?: number
83
+ /** 是否开启节流 */
84
+ enableThrottle?: boolean
85
+ /** 节流时间间隔(ms) */
86
+ throttleWait?: number
87
+ }
88
+ }
89
+
90
+ /** 地图容器引用 */
91
+ const mapRef = ref<HTMLDivElement>()
92
+ /** 地图实例 */
93
+ let map: Map | null = null
94
+ /** 当前底图类型 */
95
+ const currentMapType = ref<string>('tianditu')
96
+
97
+ /** 图层选项配置 */
98
+ const layerOptions = [
99
+ { text: '高德地图', value: 'gaode' },
100
+ { text: '高德卫星', value: 'gaodeSatellite' },
101
+ { text: '天地图', value: 'tianditu' },
102
+ { text: '天地图卫星', value: 'tianditusatellite' },
103
+ ]
104
+
105
+ /** 存储所有底图图层 */
106
+ const baseMaps: Record<string, TileLayer<XYZ>> = {}
107
+
108
+ /**
109
+ * 切换地图图层
110
+ * @param type - 图层类型
111
+ */
112
+ function handleMapChange(type: string): void {
113
+ // 隐藏所有图层
114
+ Object.keys(baseMaps).forEach((key) => {
115
+ baseMaps[key].setVisible(false)
116
+ })
117
+
118
+ // 根据选择显示对应图层
119
+ switch (type) {
120
+ case 'gaodeSatellite':
121
+ baseMaps.gaodeSatellite.setVisible(true)
122
+ baseMaps.gaodelabelLayer.setVisible(true)
123
+ break
124
+ case 'tianditu':
125
+ baseMaps.tianditu.setVisible(true)
126
+ baseMaps.tianditulabel.setVisible(true)
127
+ break
128
+ case 'tianditusatellite':
129
+ baseMaps.tianditusatellite.setVisible(true)
130
+ baseMaps.tianditusatlabel.setVisible(true)
131
+ break
132
+ default:
133
+ baseMaps.gaode.setVisible(true)
134
+ }
135
+ currentMapType.value = type
136
+ }
137
+
138
+ /**
139
+ * 初始化底图图层
140
+ * @param tianDiTuKey - 天地图密钥
141
+ */
142
+ function initializeLayers(tianDiTuKey = ''): void {
143
+ // 高德地图
144
+ baseMaps.gaode = new TileLayer({
145
+ source: new XYZ({
146
+ url: 'http://wprd04.is.autonavi.com/appmaptile?lang=zh_cn&size=1&style=7&x={x}&y={y}&z={z}',
147
+ crossOrigin: 'anonymous',
148
+ projection: 'EPSG:3857',
149
+ }),
150
+ })
151
+
152
+ // 高德卫星图
153
+ baseMaps.gaodeSatellite = new TileLayer({
154
+ source: new XYZ({
155
+ url: 'https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
156
+ crossOrigin: 'anonymous',
157
+ projection: 'EPSG:3857',
158
+ }),
159
+ visible: false,
160
+ })
161
+
162
+ // 高德标注图层
163
+ baseMaps.gaodelabelLayer = new TileLayer({
164
+ source: new XYZ({
165
+ url: 'http://webst02.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}',
166
+ crossOrigin: 'anonymous',
167
+ projection: 'EPSG:3857',
168
+ }),
169
+ visible: false,
170
+ })
171
+
172
+ // 天地图矢量图层
173
+ baseMaps.tianditu = new TileLayer({
174
+ source: new XYZ({
175
+ url: 'http://t0.tianditu.gov.cn/vec_w/wmts?'
176
+ + 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&'
177
+ + `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
178
+ projection: 'EPSG:3857',
179
+ }),
180
+ visible: false,
181
+ })
182
+
183
+ // 天地图标注图层
184
+ baseMaps.tianditulabel = new TileLayer({
185
+ source: new XYZ({
186
+ url: 'http://t0.tianditu.gov.cn/cva_w/wmts?'
187
+ + 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&'
188
+ + `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
189
+ projection: 'EPSG:3857',
190
+ }),
191
+ visible: false,
192
+ })
193
+
194
+ // 天地图卫星图层
195
+ baseMaps.tianditusatellite = new TileLayer({
196
+ source: new XYZ({
197
+ url: 'http://t0.tianditu.gov.cn/img_w/wmts?'
198
+ + 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&'
199
+ + `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
200
+ projection: 'EPSG:3857',
201
+ }),
202
+ visible: false,
203
+ })
204
+
205
+ // 天地图卫星标注
206
+ baseMaps.tianditusatlabel = new TileLayer({
207
+ source: new XYZ({
208
+ url: 'http://t0.tianditu.gov.cn/cia_w/wmts?'
209
+ + 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&'
210
+ + `FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tianDiTuKey}`,
211
+ projection: 'EPSG:3857',
212
+ }),
213
+ visible: false,
214
+ })
215
+ }
216
+
217
+ /**
218
+ * 初始化地图
219
+ * @param params - 初始化参数
220
+ */
221
+ function init(params: InitParams = {}): void {
222
+ if (!mapRef.value)
223
+ return
224
+
225
+ // 设置默认参数
226
+ const {
227
+ center = [116.404, 39.915], // 默认北京坐标
228
+ zoom = 10,
229
+ maxZoom = 18,
230
+ minZoom = 4,
231
+ tianDiTuKey = '', // 天地图密钥
232
+ } = params
233
+
234
+ // 初始化所有底图图层
235
+ initializeLayers(tianDiTuKey)
236
+
237
+ // 创建地图实例
238
+ map = new Map({
239
+ target: mapRef.value,
240
+ layers: Object.values(baseMaps),
241
+ view: new View({
242
+ center: fromLonLat(center),
243
+ zoom,
244
+ projection: 'EPSG:3857',
245
+ maxZoom,
246
+ minZoom,
247
+ }),
248
+ controls: defaultControls({
249
+ zoom: false,
250
+ rotate: false,
251
+ attribution: false,
252
+ }).extend([
253
+ new ScaleLine({
254
+ units: 'metric',
255
+ className: 'ol-scale-line',
256
+ }),
257
+ ]),
258
+ // 使用默认交互,包括拖动、缩放等
259
+ interactions: defaultInteractions({
260
+ altShiftDragRotate: false, // 禁用 Alt+Shift+拖动 旋转
261
+ pinchRotate: false, // 禁用双指旋转
262
+ }),
263
+ })
264
+
265
+ // 设置鼠标样式
266
+ if (mapRef.value) {
267
+ mapRef.value.style.cursor = 'grab'
268
+
269
+ // 监听地图事件
270
+ const mapElement = mapRef.value
271
+
272
+ // 鼠标按下时
273
+ mapElement.addEventListener('mousedown', () => {
274
+ mapElement.style.cursor = 'grabbing'
275
+ })
276
+
277
+ // 鼠标释放时
278
+ mapElement.addEventListener('mouseup', () => {
279
+ mapElement.style.cursor = 'grab'
280
+ })
281
+
282
+ // 鼠标离开地图时
283
+ mapElement.addEventListener('mouseleave', () => {
284
+ mapElement.style.cursor = 'grab'
285
+ })
286
+ }
287
+ }
288
+
289
+ /**
290
+ * 获取地图实例
291
+ * @returns OpenLayers Map 实例
292
+ */
293
+ function getMap(): Map | null {
294
+ return map
295
+ }
296
+
297
+ /**
298
+ * 设置地图中心点
299
+ * @param center - 经纬度坐标 [经度, 纬度]
300
+ * @param animate - 是否使用动画效果,默认true
301
+ */
302
+ function setCenter(center: [number, number], animate = true): void {
303
+ if (!map)
304
+ return
305
+
306
+ const view = map.getView()
307
+ if (animate) {
308
+ view.animate({
309
+ center: fromLonLat(center),
310
+ duration: 500,
311
+ })
312
+ }
313
+ else {
314
+ view.setCenter(fromLonLat(center))
315
+ }
316
+ }
317
+
318
+ /**
319
+ * 设置地图缩放级别
320
+ * @param zoom - 缩放级别
321
+ * @param animate - 是否使用动画效果,默认true
322
+ */
323
+ function setZoom(zoom: number, animate = true): void {
324
+ if (!map)
325
+ return
326
+
327
+ const view = map.getView()
328
+ if (animate) {
329
+ view.animate({
330
+ zoom,
331
+ duration: 500,
332
+ })
333
+ }
334
+ else {
335
+ view.setZoom(zoom)
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 获取当前地图缩放级别
341
+ * @returns 当前缩放级别
342
+ */
343
+ function getZoom(): number {
344
+ if (!map)
345
+ return 0
346
+ return map.getView().getZoom() || 0
347
+ }
348
+
349
+ /**
350
+ * 设置地图中心点和缩放级别
351
+ * @param center - 经纬度坐标 [经度, 纬度]
352
+ * @param zoom - 缩放级别
353
+ * @param animate - 是否使用动画效果,默认true
354
+ */
355
+ function setCenterAndZoom(center: [number, number], zoom: number, animate = true): void {
356
+ if (!map)
357
+ return
358
+
359
+ const view = map.getView()
360
+ if (animate) {
361
+ view.animate({
362
+ center: fromLonLat(center),
363
+ zoom,
364
+ duration: 500,
365
+ })
366
+ }
367
+ else {
368
+ view.setCenter(fromLonLat(center))
369
+ view.setZoom(zoom)
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 渲染点位
375
+ * @param options - 点位图层配置
376
+ * @returns 返回图层实例
377
+ */
378
+ function addVectorPoints(options: PointLayerOptions): VectorLayer<VectorSource> | null {
379
+ if (!map)
380
+ return null
381
+
382
+ const {
383
+ icon,
384
+ iconAnchor = [0.5, 1],
385
+ data,
386
+ onClick,
387
+ } = options
388
+
389
+ const vectorSource = new VectorSource()
390
+ const vectorLayer = new VectorLayer({
391
+ source: vectorSource,
392
+ zIndex: 1,
393
+ })
394
+
395
+ data.forEach((point) => {
396
+ const feature = new Feature({
397
+ geometry: new Point(fromLonLat([point.longitude, point.latitude])),
398
+ properties: {
399
+ ...point,
400
+ },
401
+ })
402
+
403
+ const style = new Style({
404
+ image: new Icon({
405
+ src: icon,
406
+ scale: 0.5,
407
+ anchor: iconAnchor,
408
+ anchorXUnits: 'fraction',
409
+ anchorYUnits: 'fraction',
410
+ crossOrigin: 'anonymous',
411
+ }),
412
+ })
413
+
414
+ feature.setStyle(style)
415
+ vectorSource.addFeature(feature)
416
+ })
417
+
418
+ map.addLayer(vectorLayer)
419
+
420
+ if (onClick) {
421
+ map.on('click', (event) => {
422
+ map.forEachFeatureAtPixel(event.pixel, (feature) => {
423
+ if (feature) {
424
+ const properties = feature.getProperties()
425
+ onClick(properties as PointData, event)
426
+ return true
427
+ }
428
+ return false
429
+ })
430
+ })
431
+ }
432
+
433
+ return vectorLayer
434
+ }
435
+
436
+ /**
437
+ * 渲染海量点(适用于10万+数据量)
438
+ * @param options - WebGL渲染配置
439
+ * @returns 返回图层实例
440
+ */
441
+ function addWebGLPoints(options: WebGLPointOptions): VectorLayer<VectorSource> | null {
442
+ if (!map)
443
+ return null
444
+
445
+ const {
446
+ iconSize = [16, 16],
447
+ data,
448
+ performance = {
449
+ enableChunk: true,
450
+ chunkSize: 5000,
451
+ enableThrottle: true,
452
+ throttleWait: 100,
453
+ },
454
+ } = options
455
+
456
+ // 创建矢量图层,使用 Canvas 渲染器优化性能
457
+ const vectorSource = new VectorSource()
458
+ const vectorLayer = new VectorLayer({
459
+ source: vectorSource,
460
+ style: new Style({
461
+ image: new CircleStyle({
462
+ radius: iconSize[0] / 2,
463
+ fill: new Fill({
464
+ color: 'rgba(0, 150, 255, 0.8)',
465
+ }),
466
+ stroke: new Stroke({
467
+ color: '#fff',
468
+ width: 1,
469
+ }),
470
+ }),
471
+ }),
472
+ updateWhileAnimating: false, // 动画时不更新以提高性能
473
+ updateWhileInteracting: false, // 交互时不更新以提高性能
474
+ zIndex: 1,
475
+ })
476
+
477
+ // 分块加载数据
478
+ if (performance.enableChunk) {
479
+ const chunkSize = performance.chunkSize || 5000
480
+ for (let i = 0; i < data.length; i += chunkSize) {
481
+ const chunk = data.slice(i, i + chunkSize)
482
+ setTimeout(() => {
483
+ const features = chunk.map(point =>
484
+ new Feature({
485
+ geometry: new Point(fromLonLat([point.longitude, point.latitude])),
486
+ properties: {
487
+ title: point.title,
488
+ ...point.extData,
489
+ },
490
+ }),
491
+ )
492
+ vectorSource.addFeatures(features)
493
+ }, 0)
494
+ }
495
+ }
496
+ else {
497
+ const features = data.map(point =>
498
+ new Feature({
499
+ geometry: new Point(fromLonLat([point.longitude, point.latitude])),
500
+ properties: {
501
+ title: point.title,
502
+ ...point.extData,
503
+ },
504
+ }),
505
+ )
506
+ vectorSource.addFeatures(features)
507
+ }
508
+
509
+ // 添加到地图
510
+ map.addLayer(vectorLayer)
511
+
512
+ return vectorLayer
513
+ }
514
+
515
+ // 暴露方法给父组件
516
+ defineExpose({
517
+ init,
518
+ getMap,
519
+ setCenter,
520
+ setZoom,
521
+ getZoom,
522
+ setCenterAndZoom,
523
+ addVectorPoints,
524
+ addWebGLPoints,
525
+ })
526
+
527
+ // 组件卸载时清理地图实例
528
+ onUnmounted(() => {
529
+ if (map) {
530
+ map.setTarget(undefined)
531
+ map = null
532
+ }
533
+ })
534
+ </script>
535
+
536
+ <template>
537
+ <div class="map-wrapper">
538
+ <div ref="mapRef" class="ol-map" />
539
+
540
+ <!-- 图层切换下拉框 -->
541
+ <div class="layer-selector">
542
+ <select v-model="currentMapType" @change="handleMapChange(currentMapType)">
543
+ <option v-for="layer in layerOptions" :key="layer.value" :value="layer.value">
544
+ {{ layer.text }}
545
+ </option>
546
+ </select>
547
+ </div>
548
+ </div>
549
+ </template>
550
+
551
+ <style scoped lang="less">
552
+ .map-wrapper {
553
+ position: relative;
554
+ width: 100%;
555
+ height: 100%;
556
+ }
557
+
558
+ .ol-map {
559
+ width: 100%;
560
+ height: 100%;
561
+ min-height: 400px;
562
+ touch-action: none;
563
+
564
+ &:active {
565
+ cursor: grabbing;
566
+ }
567
+ }
568
+
569
+ .layer-selector {
570
+ position: absolute;
571
+ right: 10px;
572
+ top: 10px;
573
+ z-index: 1000;
574
+ background: white;
575
+ border-radius: 4px;
576
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
577
+ padding: 2px;
578
+
579
+ select {
580
+ padding: 6px 24px 6px 12px;
581
+ font-size: 14px;
582
+ border: none;
583
+ border-radius: 4px;
584
+ background-color: white;
585
+ cursor: pointer;
586
+ outline: none;
587
+ appearance: none;
588
+ -webkit-appearance: none;
589
+ -moz-appearance: none;
590
+ 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");
591
+ background-repeat: no-repeat;
592
+ background-position: right 6px center;
593
+ background-size: 12px;
594
+
595
+ &:hover {
596
+ background-color: #f5f5f5;
597
+ }
598
+
599
+ option {
600
+ padding: 8px;
601
+ }
602
+ }
603
+ }
604
+
605
+ // 比例尺样式
606
+ :deep(.ol-scale-line) {
607
+ position: absolute;
608
+ left: 0.5em;
609
+ bottom: 0.5em;
610
+ background: rgba(255, 255, 255, 0.8);
611
+ padding: 2px;
612
+ border-radius: 4px;
613
+ }
614
+
615
+ :deep(.ol-scale-line-inner) {
616
+ border: 2px solid #333;
617
+ border-top: none;
618
+ color: #333;
619
+ font-size: 12px;
620
+ text-align: center;
621
+ margin: 1px;
622
+ will-change: contents, width;
623
+ transition: width 0.25s;
624
+ }
625
+
626
+ // 移动端适配
627
+ @media screen and (max-width: 768px) {
628
+ .layer-selector {
629
+ right: 8px;
630
+ top: 8px;
631
+
632
+ select {
633
+ padding: 4px 20px 4px 8px;
634
+ font-size: 12px;
635
+ background-size: 10px;
636
+ }
637
+ }
638
+
639
+ :deep(.ol-scale-line) {
640
+ transform: scale(0.8);
641
+ transform-origin: left bottom;
642
+ }
643
+ }
644
+ </style>