xsh_1997 1 miesiąc temu
rodzic
commit
31ea8410c3

Plik diff jest za duży
+ 2399 - 0
doc/大屏/GIS接口文档.docx


Plik diff jest za duży
+ 4376 - 0
doc/大屏/集中化GIS系统JSAPI接口文档.docx


+ 3 - 3
ruoyi-screen/.env.development

@@ -1,5 +1,5 @@
1 1
 # 页面标题(对应 ruoyi-ui 的 VUE_APP_TITLE)
2
-VITE_APP_TITLE=数字畜牧一张图
2
+VITE_APP_TITLE=牧业大脑-数字畜牧一张图
3 3
 
4 4
 # 开发环境
5 5
 ENV=development
@@ -25,6 +25,6 @@ VITE_GIS_DEFAULT_CENTER=94.046223,31.920382
25 25
 VITE_GIS_DEFAULT_ZOOM=14
26 26
 VITE_GIS_MIN_ZOOM=7
27 27
 # 地图最大缩放;建议与瓦片最高级别一致,避免放到顶过采样缺图
28
-VITE_GIS_MAX_ZOOM=17
28
+VITE_GIS_MAX_ZOOM=20
29 29
 # 瓦片请求最高 TileMatrix;与 MAX_ZOOM 相同则每级都请求新瓦片,低 1 级则顶级用放大显示
30
-VITE_GIS_TILE_MAX_ZOOM=16
30
+VITE_GIS_TILE_MAX_ZOOM=19

+ 3 - 3
ruoyi-screen/.env.production

@@ -1,5 +1,5 @@
1 1
 # 页面标题(对应 ruoyi-ui 的 VUE_APP_TITLE)
2
-VITE_APP_TITLE=数字畜牧一张图
2
+VITE_APP_TITLE=牧业大脑-数字畜牧一张图
3 3
 
4 4
 # 生产环境
5 5
 ENV=production
@@ -23,8 +23,8 @@ VITE_GIS_TILE_SIZE=256
23 23
 VITE_GIS_DEFAULT_CENTER=94.046223,31.920382
24 24
 VITE_GIS_DEFAULT_ZOOM=14
25 25
 VITE_GIS_MIN_ZOOM=7
26
-VITE_GIS_MAX_ZOOM=17
27
-VITE_GIS_TILE_MAX_ZOOM=16
26
+VITE_GIS_MAX_ZOOM=20
27
+VITE_GIS_TILE_MAX_ZOOM=19
28 28
 
29 29
 # 大屏免密登录(生产需后端 bigscreen.login.enabled=true 且配置 RSA 密钥)
30 30
 VITE_SCREEN_AUTO_LOGIN=true

+ 162 - 0
ruoyi-screen/src/components/GisMap.vue

@@ -11,6 +11,7 @@ import {
11 11
   resolveGisMapStyle,
12 12
   createGisTransformRequest
13 13
 } from '@/utils/gisLoader'
14
+import { parsePoiLngLat, searchNearbyPois } from '@/utils/gisPoiSearch'
14 15
 
15 16
 const props = defineProps({
16 17
   center: {
@@ -32,6 +33,11 @@ const props = defineProps({
32 33
   showScale: {
33 34
     type: Boolean,
34 35
     default: false
36
+  },
37
+  /** 是否在地图加载后展示周边 POI Marker */
38
+  showPoiMarkers: {
39
+    type: Boolean,
40
+    default: true
35 41
   }
36 42
 })
37 43
 
@@ -53,6 +59,7 @@ const WHEEL_ZOOM_RATE = 0.0022
53 59
 let wheelDeltaAccum = 0
54 60
 let wheelDebounceTimer = null
55 61
 let lastWheelAround = null
62
+let poiMarkers = []
56 63
 
57 64
 function formatLngLat(lngLat) {
58 65
   if (!lngLat) {
@@ -113,6 +120,105 @@ function bindZoomDebugListeners() {
113 120
   })
114 121
 }
115 122
 
123
+function clearPoiMarkers() {
124
+  poiMarkers.forEach((marker) => {
125
+    try {
126
+      marker.remove()
127
+    } catch {
128
+      /* ignore */
129
+    }
130
+  })
131
+  poiMarkers = []
132
+}
133
+
134
+function escapeHtml(text) {
135
+  return String(text || '')
136
+    .replace(/&/g, '&')
137
+    .replace(/</g, '&lt;')
138
+    .replace(/>/g, '&gt;')
139
+    .replace(/"/g, '&quot;')
140
+}
141
+
142
+function buildPoiPopupHtml(poi) {
143
+  const name = escapeHtml(poi.name || '未知地点')
144
+  const address = poi.address ? escapeHtml(poi.address) : ''
145
+  const distance = poi.distance ? `${escapeHtml(poi.distance)}m` : ''
146
+  const parts = [`<div style="font-weight:600;margin-bottom:4px">${name}</div>`]
147
+  if (address) {
148
+    parts.push(`<div style="font-size:12px;opacity:0.85">${address}</div>`)
149
+  }
150
+  if (distance) {
151
+    parts.push(`<div style="font-size:12px;opacity:0.7;margin-top:4px">距中心 ${distance}</div>`)
152
+  }
153
+  return parts.join('')
154
+}
155
+
156
+/** 自定义 Marker:图标下方常驻显示 POI 名称 */
157
+function createPoiMarkerElement(name) {
158
+  const wrap = document.createElement('div')
159
+  wrap.className = 'gis-poi-marker'
160
+  wrap.style.pointerEvents = 'auto'
161
+
162
+  const label = document.createElement('div')
163
+  label.className = 'gis-poi-marker__label'
164
+  label.textContent = String(name || '未知地点')
165
+  label.title = label.textContent
166
+
167
+  const dot = document.createElement('div')
168
+  dot.className = 'gis-poi-marker__dot'
169
+
170
+  wrap.appendChild(label)
171
+  wrap.appendChild(dot)
172
+  return wrap
173
+}
174
+
175
+async function renderNearbyPoiMarkers() {
176
+  if (!props.showPoiMarkers || !map || !cmmapglRef) {
177
+    return
178
+  }
179
+  clearPoiMarkers()
180
+
181
+  const center = resolveCenter()
182
+  let pois = []
183
+  try {
184
+    pois = await searchNearbyPois({
185
+      center,
186
+      radius: 5000,
187
+      pageSize: 50,
188
+      pageNum: 1
189
+    })
190
+  } catch {
191
+    return
192
+  }
193
+
194
+  const Marker = cmmapglRef.Marker
195
+  const Popup = cmmapglRef.Popup
196
+  if (typeof Marker !== 'function') {
197
+    return
198
+  }
199
+
200
+  pois.forEach((poi) => {
201
+    const lngLat = parsePoiLngLat(poi.location)
202
+    if (!lngLat) {
203
+      return
204
+    }
205
+    const markerOpts = {
206
+      element: createPoiMarkerElement(poi.name),
207
+      anchor: 'bottom'
208
+    }
209
+    const marker = new Marker(markerOpts).setLngLat(lngLat)
210
+    if (typeof Popup === 'function') {
211
+      marker.setPopup(
212
+        new Popup({ offset: 20, closeOnClick: true, maxWidth: '260px' }).setHTML(
213
+          buildPoiPopupHtml(poi)
214
+        )
215
+      )
216
+    }
217
+    marker.addTo(map)
218
+    poiMarkers.push(marker)
219
+  })
220
+}
221
+
116 222
 function destroyMap() {
117 223
   if (zoomLogTimer) {
118 224
     clearTimeout(zoomLogTimer)
@@ -126,6 +232,7 @@ function destroyMap() {
126 232
   lastWheelAround = null
127 233
   lastMouseZoomLogAt = 0
128 234
   clampingZoom = false
235
+  clearPoiMarkers()
129 236
   if (map) {
130 237
     map.remove()
131 238
     map = null
@@ -363,6 +470,7 @@ async function initMap() {
363 470
       configureScrollZoom()
364 471
       snapZoomToLimits()
365 472
       resizeMap()
473
+      renderNearbyPoiMarkers()
366 474
       emit('ready', map)
367 475
     })
368 476
     map.on('click', (e) => emit('click', e))
@@ -381,10 +489,25 @@ watch(
381 489
       center: resolveCenter(),
382 490
       zoom: resolveZoom()
383 491
     })
492
+    if (props.showPoiMarkers) {
493
+      renderNearbyPoiMarkers()
494
+    }
384 495
   },
385 496
   { deep: true }
386 497
 )
387 498
 
499
+watch(
500
+  () => props.showPoiMarkers,
501
+  (show) => {
502
+    if (!map || !cmmapglRef) return
503
+    if (show) {
504
+      renderNearbyPoiMarkers()
505
+    } else {
506
+      clearPoiMarkers()
507
+    }
508
+  }
509
+)
510
+
388 511
 onMounted(() => {
389 512
   initMap()
390 513
 })
@@ -416,3 +539,42 @@ defineExpose({ resizeMap, getMap: () => map })
416 539
   height: 100%;
417 540
 }
418 541
 </style>
542
+
543
+<!-- Marker DOM 由地图 SDK 挂载,需非 scoped 样式 -->
544
+<style>
545
+.gis-poi-marker {
546
+  display: flex;
547
+  flex-direction: column;
548
+  align-items: center;
549
+  cursor: pointer;
550
+  transform: translateY(-2px);
551
+}
552
+
553
+.gis-poi-marker__label {
554
+  max-width: 108px;
555
+  margin-bottom: 3px;
556
+  padding: 2px 6px;
557
+  border-radius: 4px;
558
+  background: rgba(8, 28, 24, 0.82);
559
+  border: 1px solid rgba(240, 180, 41, 0.45);
560
+  color: #eef8f4;
561
+  font-size: 11px;
562
+  line-height: 1.35;
563
+  text-align: center;
564
+  white-space: nowrap;
565
+  overflow: hidden;
566
+  text-overflow: ellipsis;
567
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
568
+  pointer-events: none;
569
+}
570
+
571
+.gis-poi-marker__dot {
572
+  width: 10px;
573
+  height: 10px;
574
+  border-radius: 50%;
575
+  background: #f0b429;
576
+  border: 2px solid #fff;
577
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
578
+  flex-shrink: 0;
579
+}
580
+</style>

+ 61 - 0
ruoyi-screen/src/utils/gisPoiSearch.js

@@ -0,0 +1,61 @@
1
+import { getGisAccessToken, getGisBaseUrl, getGisDefaultCenter } from './gisLoader'
2
+
3
+/** GIS POI 接口成功状态码 */
4
+const POI_STATUS_OK = 2000
5
+
6
+/**
7
+ * 解析 POI location 字段(文档格式:纬度,经度)为地图坐标 [经度, 纬度]
8
+ */
9
+export function parsePoiLngLat(location) {
10
+  if (!location || typeof location !== 'string') {
11
+    return null
12
+  }
13
+  const parts = location.split(',').map((n) => Number(String(n).trim()))
14
+  if (parts.length < 2 || !parts.every((n) => Number.isFinite(n))) {
15
+    return null
16
+  }
17
+  const [lat, lng] = parts
18
+  return [lng, lat]
19
+}
20
+
21
+/**
22
+ * 周边搜索(GIS 接口文档 §2.2 /agis/search/v1/pois/region)
23
+ * @param {{ center?: [number, number], radius?: number, pageNum?: number, pageSize?: number }} [options]
24
+ */
25
+export async function searchNearbyPois(options = {}) {
26
+  const base = getGisBaseUrl()
27
+  const key = getGisAccessToken()
28
+  if (!base || !key) {
29
+    return []
30
+  }
31
+
32
+  const center = options.center || getGisDefaultCenter()
33
+  const [longitude, latitude] = center
34
+  const radius = options.radius ?? 3000
35
+  const pageNum = options.pageNum ?? 1
36
+  const pageSize = options.pageSize ?? 20
37
+
38
+  const params = new URLSearchParams({
39
+    longitude: String(longitude),
40
+    latitude: String(latitude),
41
+    radius: String(radius),
42
+    page_num: String(pageNum),
43
+    page_size: String(pageSize),
44
+    key
45
+  })
46
+
47
+  const url = `${base}/agis/search/v1/pois/region?${params.toString()}`
48
+  const res = await fetch(url)
49
+  if (!res.ok) {
50
+    return []
51
+  }
52
+
53
+  const data = await res.json()
54
+  if (data?.status !== POI_STATUS_OK || !Array.isArray(data?.body?.pois)) {
55
+    return []
56
+  }
57
+
58
+  return data.body.pois
59
+    .map((row) => row?.poi || row)
60
+    .filter((poi) => poi && typeof poi === 'object')
61
+}

+ 1 - 1
ruoyi-ui/.env.development

@@ -1,5 +1,5 @@
1 1
 # 页面标题
2
-VUE_APP_TITLE = 巴青牧业大脑
2
+VUE_APP_TITLE = 行业数字服务链
3 3
 
4 4
 # 开发环境配置
5 5
 ENV = 'development'

+ 1 - 1
ruoyi-ui/.env.production

@@ -1,5 +1,5 @@
1 1
 # 页面标题
2
-VUE_APP_TITLE = 巴青牧业大脑
2
+VUE_APP_TITLE = 行业数字服务链
3 3
 
4 4
 # 生产环境配置
5 5
 ENV = 'production'

+ 1 - 1
ruoyi-ui/vue.config.js

@@ -7,7 +7,7 @@ function resolve(dir) {
7 7
 
8 8
 const CompressionPlugin = require('compression-webpack-plugin')
9 9
 
10
-const name = process.env.VUE_APP_TITLE || '巴青牧业大脑' // 网页标题
10
+const name = process.env.VUE_APP_TITLE || '行业数字服务链' // 网页标题
11 11
 
12 12
 const baseUrl = 'http://192.168.1.6:8010' // 后端接口
13 13
 

+ 36 - 0
scripts/parse-gis-api-doc.mjs

@@ -0,0 +1,36 @@
1
+import fs from 'fs'
2
+import os from 'os'
3
+import path from 'path'
4
+
5
+const xmlPath = path.join(os.tmpdir(), 'gis-doc-raw.xml')
6
+const x = fs.readFileSync(xmlPath, 'utf8')
7
+
8
+function strip(chunk) {
9
+  return chunk
10
+    .replace(/&lt;/g, '<')
11
+    .replace(/&gt;/g, '>')
12
+    .replace(/&quot;/g, '"')
13
+    .replace(/&amp;/g, '&')
14
+    .replace(/<w:tab[^>]*\/>/g, '\t')
15
+    .replace(/<\/w:p>/g, '\n')
16
+    .replace(/<[^>]+>/g, '')
17
+    .replace(/\n{3,}/g, '\n\n')
18
+}
19
+
20
+const text = strip(x)
21
+const keys = ['周边', '2.2', 'longitude', 'latitude', 'radius', 'page_size', 'pageSize', 'POI', '搜索', 'nearby', 'agis']
22
+
23
+for (const k of keys) {
24
+  const i = text.indexOf(k)
25
+  if (i >= 0) {
26
+    console.log(`\n######## ${k} @ ${i} ########`)
27
+    console.log(text.slice(Math.max(0, i - 150), i + 1200))
28
+  }
29
+}
30
+
31
+// dump section around "2.2"
32
+const i22 = text.indexOf('2.2')
33
+if (i22 >= 0) {
34
+  console.log('\n######## FULL 2.2 SECTION ########')
35
+  console.log(text.slice(i22, i22 + 4000))
36
+}