xsh_1997 vor 1 Monat
Ursprung
Commit
77bf07bc94

+ 126 - 0
doc/大屏/集中化GIS地图对接技术方案.md

@@ -0,0 +1,126 @@
1
+# 大屏 — 集中化 GIS 地图对接技术方案
2
+
3
+> 依据《中国移动集中化 GIS 平台 JSAPI 接口协议 v1.0》(`集中化GIS系统JSAPI接口文档.docx`)。  
4
+> 前端引擎为 **cmmap GL JS**(API 形态与 Mapbox GL JS 接近),全局对象 **`cmmapgl`**。
5
+
6
+---
7
+
8
+## 1. 对接前提(向 GIS 平台申请)
9
+
10
+| 项 | 说明 |
11
+| --- | --- |
12
+| **GIS 服务根地址** | 文档中的「GIS 服务地址」,如 `https://gis.example.com` |
13
+| **地图密钥 accessToken** | 文档 §2.2;初始化前赋值 `cmmapgl.accessToken` |
14
+| **底图样式 style** | Mapbox Style 规范 JSON 或样式 URL(`MapOptions.style`) |
15
+| **跨域** | 生产由 GIS/Nginx 放行;开发可用 Vite 代理 `/gis-dev-proxy` |
16
+
17
+---
18
+
19
+## 2. 静态资源引入
20
+
21
+文档 §2.1 要求在页面引入(将 `GIS服务地址` 替换为实际根地址):
22
+
23
+```html
24
+<script src="{GIS_BASE}/agis/resources/v1/tile-server/dist/cmmap-gl/1.0/cmmap-gl.js"></script>
25
+<link href="{GIS_BASE}/agis/resources/v1/tile-server/dist/cmmap-gl/1.0/cmmap-gl.css" rel="stylesheet" />
26
+```
27
+
28
+`ruoyi-screen` 通过 `src/utils/gisLoader.js` **按需动态加载**,避免未配置 GIS 时阻塞整站。
29
+
30
+---
31
+
32
+## 3. 初始化流程
33
+
34
+```text
35
+loadGisSdk() → cmmapgl.accessToken = token → new cmmapgl.Map({ container, center, zoom, style })
36
+```
37
+
38
+| 参数 | 说明 |
39
+| --- | --- |
40
+| `container` | 地图容器 DOM 或 id |
41
+| `center` | **`[经度, 纬度]`**(与 GeoJSON 一致,勿写反) |
42
+| `zoom` | 0–24,默认约 2–9 |
43
+| `style` | 底图样式 URL 或 JSON |
44
+| `accessToken` | 也可在 `MapOptions` 中传 `accessToken`,会覆盖全局 `cmmapgl.accessToken` |
45
+
46
+巴青县默认中心(可在页面覆盖):`[94.05, 31.92]`,缩放 `9`。
47
+
48
+---
49
+
50
+## 4. 常用能力(按大屏场景)
51
+
52
+| 文档章节 | 能力 | 大屏典型用途 |
53
+| --- | --- | --- |
54
+| §3.1 | `cmmapgl.Map` | 底图展示 |
55
+| §3.3 | `NavigationControl` / `ScaleControl` | 导航、比例尺(大屏可关闭交互) |
56
+| §4 | `addSource` + `addLayer` | 矢量/栅格/GeoJSON 业务图层 |
57
+| §5.1 | `cmmapgl.Marker` | 牧场、检疫站、交易市场点位 |
58
+| §5.2 | `Popup` | 点位详情气泡 |
59
+| §7 | 热力图、散点、轨迹等 | 疫病风险、资源分布 |
60
+| §8 | `agis.turf` | 空间计算(点线面关系) |
61
+| §10–11 | POI / 路线 | 后续扩展 |
62
+
63
+---
64
+
65
+## 5. ruoyi-screen 工程约定
66
+
67
+### 5.1 环境变量(`.env.development` / `.env.production`)
68
+
69
+```bash
70
+# GIS 服务根地址(勿带末尾 /)
71
+VITE_GIS_BASE_URL=/gis-dev-proxy
72
+# 地图密钥(§2.2,由 GIS 平台发放)
73
+VITE_GIS_ACCESS_TOKEN=
74
+# 底图样式 URL 或 JSON 路径
75
+VITE_GIS_MAP_STYLE=
76
+# 默认中心:经度,纬度
77
+VITE_GIS_DEFAULT_CENTER=94.05,31.92
78
+VITE_GIS_DEFAULT_ZOOM=9
79
+```
80
+
81
+开发环境在 `vite.config.js` 将 `/gis-dev-proxy` 代理到真实 GIS 服务(`VITE_GIS_PROXY_TARGET`)。
82
+
83
+### 5.2 代码入口
84
+
85
+| 文件 | 职责 |
86
+| --- | --- |
87
+| `src/utils/gisLoader.js` | 动态加载 cmmap-gl.js/css、读取 env |
88
+| `src/components/GisMap.vue` | 通用地图容器,`@ready` 抛出 `map` 实例 |
89
+
90
+### 5.3 页面使用示例
91
+
92
+```vue
93
+<GisMap
94
+  class="my-map"
95
+  :interactive="false"
96
+  @ready="onMapReady"
97
+  @error="onMapError"
98
+/>
99
+```
100
+
101
+```javascript
102
+function onMapReady(map) {
103
+  // map 为 cmmapgl.Map 实例
104
+  // map.addSource / addLayer / Marker 等见文档 §4–§5
105
+}
106
+```
107
+
108
+---
109
+
110
+## 6. 注意事项
111
+
112
+1. **坐标顺序**:一律 `[lng, lat]`。
113
+2. **容器尺寸**:父级须有明确宽高(大屏组件内 `width/height: 100%`)。
114
+3. **销毁**:路由离开须 `map.remove()`(`GisMap.vue` 已在 `onUnmounted` 处理)。
115
+4. **resize**:大屏 `screenFit` 缩放后若地图空白,在布局稳定后调用 `map.resize()`。
116
+5. **token 缺失**:未配置 `VITE_GIS_ACCESS_TOKEN` 时组件展示占位提示,不抛白屏。
117
+
118
+---
119
+
120
+## 7. 交付清单
121
+
122
+- [x] `gisLoader.js` 动态加载 SDK
123
+- [x] `GisMap.vue` 基础地图组件
124
+- [x] Vite 开发代理占位
125
+- [x] 业务页嵌入地图(`ScreenLayout` 中间栏共用底图,各子页左右面板叠在两侧)
126
+- [ ] 业务点位/GeoJSON 与后端接口联调

+ 0 - 0
doc/大屏/集中化GIS系统JSAPI-目录.txt


Datei-Diff unterdrückt, da er zu groß ist
+ 1363 - 0
doc/大屏/集中化GIS系统JSAPI接口文档-提取.txt


+ 10 - 2
ruoyi-screen/.env.development

@@ -15,5 +15,13 @@ VITE_WEATHER_APP_ID=36793262
15 15
 VITE_WEATHER_APP_SECRET=t6a6z0H8
16 16
 VITE_WEATHER_CITY=巴青
17 17
 
18
-# 大屏免密登录(获取公钥 + RSA 加密 UUID)
19
-VITE_SCREEN_AUTO_LOGIN=true
18
+# 集中化 GIS(浏览器直连 GIS 服务)
19
+VITE_GIS_BASE_URL=https://bqxm.alafarms.com/agis-api
20
+VITE_GIS_ACCESS_TOKEN=8XughK9JVapV1pqaE7HnxfHiKGrLy21L9LbsKskL
21
+VITE_GIS_WMTS_LAYER=baqing_wp
22
+# 瓦片边长:512 可向 WMTS 请求 WIDTH/HEIGHT=512 高清图(若服务支持)
23
+VITE_GIS_TILE_SIZE=256
24
+VITE_GIS_DEFAULT_CENTER=94.046223,31.920382
25
+VITE_GIS_DEFAULT_ZOOM=14
26
+VITE_GIS_MIN_ZOOM=6
27
+VITE_GIS_MAX_ZOOM=17

+ 10 - 0
ruoyi-screen/.env.production

@@ -15,6 +15,16 @@ VITE_WEATHER_APP_ID=36793262
15 15
 VITE_WEATHER_APP_SECRET=t6a6z0H8
16 16
 VITE_WEATHER_CITY=巴青
17 17
 
18
+# 集中化 GIS
19
+VITE_GIS_BASE_URL=https://bqxm.alafarms.com/agis-api
20
+VITE_GIS_ACCESS_TOKEN=8XughK9JVapV1pqaE7HnxfHiKGrLy21L9LbsKskL
21
+VITE_GIS_WMTS_LAYER=baqing_wp
22
+VITE_GIS_TILE_SIZE=256
23
+VITE_GIS_DEFAULT_CENTER=94.046223,31.920382
24
+VITE_GIS_DEFAULT_ZOOM=14
25
+VITE_GIS_MIN_ZOOM=6
26
+VITE_GIS_MAX_ZOOM=17
27
+
18 28
 # 大屏免密登录(生产需后端 bigscreen.login.enabled=true 且配置 RSA 密钥)
19 29
 VITE_SCREEN_AUTO_LOGIN=true
20 30
 

BIN
ruoyi-screen/src/assets/header.png


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

@@ -0,0 +1,234 @@
1
+<script setup>
2
+import { onMounted, onUnmounted, ref, watch } from 'vue'
3
+import {
4
+  getGisAccessToken,
5
+  getGisDefaultCenter,
6
+  getGisDefaultZoom,
7
+  getGisMaxZoom,
8
+  getGisMinZoom,
9
+  isGisConfigured,
10
+  loadGisSdk,
11
+  resolveGisMapStyle,
12
+  createGisTransformRequest
13
+} from '@/utils/gisLoader'
14
+import { getMapPixelRatio } from '@/utils/screenFit'
15
+
16
+const props = defineProps({
17
+  center: {
18
+    type: Array,
19
+    default: null
20
+  },
21
+  zoom: {
22
+    type: Number,
23
+    default: null
24
+  },
25
+  interactive: {
26
+    type: Boolean,
27
+    default: true
28
+  },
29
+  showNav: {
30
+    type: Boolean,
31
+    default: false
32
+  },
33
+  showScale: {
34
+    type: Boolean,
35
+    default: false
36
+  }
37
+})
38
+
39
+const emit = defineEmits(['ready', 'click'])
40
+
41
+const containerRef = ref(null)
42
+
43
+let map = null
44
+let cmmapglRef = null
45
+let zoomLogTimer = null
46
+let lastMouseZoomLogAt = 0
47
+
48
+function formatLngLat(lngLat) {
49
+  if (!lngLat) {
50
+    return '—'
51
+  }
52
+  const lng = Number(lngLat.lng ?? lngLat[0])
53
+  const lat = Number(lngLat.lat ?? lngLat[1])
54
+  if (!Number.isFinite(lng) || !Number.isFinite(lat)) {
55
+    return '—'
56
+  }
57
+  return `[${lng.toFixed(6)}, ${lat.toFixed(6)}]`
58
+}
59
+
60
+function logMapState(source, cursorLngLat) {
61
+  if (!map || typeof map.getZoom !== 'function') {
62
+    return
63
+  }
64
+  const z = map.getZoom()
65
+  const zoomText = Number.isFinite(z) ? z.toFixed(2) : String(z)
66
+  const center = typeof map.getCenter === 'function' ? map.getCenter() : null
67
+  const parts = [
68
+    `[GIS] ${source}`,
69
+    `zoom: ${zoomText}`,
70
+    `center: ${formatLngLat(center)}`
71
+  ]
72
+  if (cursorLngLat) {
73
+    parts.push(`point: ${formatLngLat(cursorLngLat)}`)
74
+  }
75
+  console.log(parts.join(' | '))
76
+}
77
+
78
+function bindZoomDebugListeners() {
79
+  if (!map) {
80
+    return
81
+  }
82
+  map.on('load', () => logMapState('load'))
83
+  map.on('zoomend', () => logMapState('zoomend'))
84
+  map.on('moveend', () => logMapState('moveend'))
85
+  map.on('wheel', () => {
86
+    if (zoomLogTimer) {
87
+      clearTimeout(zoomLogTimer)
88
+    }
89
+    zoomLogTimer = setTimeout(() => logMapState('wheel'), 80)
90
+  })
91
+  map.on('mousemove', (e) => {
92
+    const now = Date.now()
93
+    if (now - lastMouseZoomLogAt < 300) {
94
+      return
95
+    }
96
+    lastMouseZoomLogAt = now
97
+    logMapState('mousemove', e.lngLat)
98
+  })
99
+  map.on('click', (e) => {
100
+    logMapState('click', e.lngLat)
101
+  })
102
+}
103
+
104
+function destroyMap() {
105
+  if (zoomLogTimer) {
106
+    clearTimeout(zoomLogTimer)
107
+    zoomLogTimer = null
108
+  }
109
+  lastMouseZoomLogAt = 0
110
+  if (map) {
111
+    map.remove()
112
+    map = null
113
+  }
114
+}
115
+
116
+function resolveCenter() {
117
+  if (Array.isArray(props.center) && props.center.length >= 2) {
118
+    return [Number(props.center[0]), Number(props.center[1])]
119
+  }
120
+  return getGisDefaultCenter()
121
+}
122
+
123
+function resolveZoom() {
124
+  if (props.zoom != null && Number.isFinite(Number(props.zoom))) {
125
+    return Number(props.zoom)
126
+  }
127
+  return getGisDefaultZoom()
128
+}
129
+
130
+function applyMapSharpness() {
131
+  if (!map) {
132
+    return
133
+  }
134
+  const ratio = getMapPixelRatio()
135
+  if (typeof map.setPixelRatio === 'function') {
136
+    map.setPixelRatio(ratio)
137
+  }
138
+  if (typeof map.resize === 'function') {
139
+    map.resize()
140
+  }
141
+}
142
+
143
+async function initMap() {
144
+  destroyMap()
145
+  if (!isGisConfigured()) {
146
+    return
147
+  }
148
+  try {
149
+    const cmmapgl = await loadGisSdk()
150
+    cmmapglRef = cmmapgl
151
+    const token = getGisAccessToken()
152
+    if (token) {
153
+      cmmapgl.accessToken = token
154
+    }
155
+    const style = resolveGisMapStyle()
156
+    map = new cmmapgl.Map({
157
+      container: containerRef.value,
158
+      center: resolveCenter(),
159
+      zoom: resolveZoom(),
160
+      minZoom: getGisMinZoom(),
161
+      maxZoom: getGisMaxZoom(),
162
+      style,
163
+      interactive: props.interactive,
164
+      accessToken: token || undefined,
165
+      collectResourceTiming: false,
166
+      pixelRatio: getMapPixelRatio(),
167
+      transformRequest: createGisTransformRequest(getMapPixelRatio)
168
+    })
169
+    if (props.showNav) {
170
+      map.addControl(new cmmapgl.NavigationControl(), 'top-right')
171
+    }
172
+    if (props.showScale) {
173
+      map.addControl(new cmmapgl.ScaleControl({ unit: 'metric' }), 'bottom-left')
174
+    }
175
+    bindZoomDebugListeners()
176
+    map.on('load', () => {
177
+      applyMapSharpness()
178
+      emit('ready', map)
179
+    })
180
+    map.on('click', (e) => emit('click', e))
181
+    // 瓦片/遥测等非致命错误不向页面报错
182
+    map.on('error', () => {})
183
+  } catch {
184
+    /* 静默失败,不遮挡大屏 */
185
+  }
186
+}
187
+
188
+function resizeMap() {
189
+  applyMapSharpness()
190
+}
191
+
192
+watch(
193
+  () => [props.center, props.zoom],
194
+  () => {
195
+    if (!map || !cmmapglRef) return
196
+    map.jumpTo({
197
+      center: resolveCenter(),
198
+      zoom: resolveZoom()
199
+    })
200
+  },
201
+  { deep: true }
202
+)
203
+
204
+onMounted(() => {
205
+  initMap()
206
+})
207
+
208
+onUnmounted(() => {
209
+  destroyMap()
210
+})
211
+
212
+defineExpose({ resizeMap, getMap: () => map })
213
+</script>
214
+
215
+<template>
216
+  <div class="gis-map">
217
+    <div ref="containerRef" class="gis-map__canvas" />
218
+  </div>
219
+</template>
220
+
221
+<style scoped>
222
+.gis-map {
223
+  position: relative;
224
+  width: 100%;
225
+  height: 100%;
226
+  min-height: 120px;
227
+  overflow: hidden;
228
+}
229
+
230
+.gis-map__canvas {
231
+  width: 100%;
232
+  height: 100%;
233
+}
234
+</style>

+ 4 - 0
ruoyi-screen/src/components/ScreenNavBar.vue

@@ -114,6 +114,8 @@ function onLogout() {
114 114
   height: 100px;
115 115
   position: relative;
116 116
   font-family: inherit;
117
+  pointer-events: none;
118
+  background: url("../assets/header.png") no-repeat center center;
117 119
 }
118 120
 .screen-nav-left {
119 121
   position: absolute;
@@ -131,6 +133,7 @@ function onLogout() {
131 133
   padding: 5px 10px;
132 134
   cursor: pointer;
133 135
   margin-right: 10px;
136
+  pointer-events: auto;
134 137
 }
135 138
 .screen-nav-left-item:hover {
136 139
   background: url("../assets/nav/btn_active.png") no-repeat center center;
@@ -156,6 +159,7 @@ function onLogout() {
156 159
   padding: 5px 10px;
157 160
   cursor: pointer;
158 161
   margin-right: 10px;
162
+  pointer-events: auto;
159 163
 }
160 164
 .screen-nav-right-item:hover {
161 165
   background: url("../assets/nav/btn_active.png") no-repeat center center;

+ 60 - 16
ruoyi-screen/src/layout/ScreenLayout.vue

@@ -1,17 +1,26 @@
1 1
 <script setup>
2
-import { onMounted, onUnmounted, ref } from "vue"
2
+import { onMounted, onUnmounted, provide, ref } from "vue"
3
+import GisMap from "../components/GisMap.vue"
3 4
 import ScreenNavBar from "../components/ScreenNavBar.vue"
4 5
 import { bindScreenScale, DESIGN_HEIGHT, DESIGN_WIDTH } from "../utils/screenFit.js"
5 6
 
6
-/** 视口根节点,供 screenFit 写入 --screen-scale */
7 7
 const viewportRef = ref(null)
8
+const gisMapRef = ref(null)
9
+provide("screenGisMap", gisMapRef)
10
+
8 11
 let unbindScale = null
9 12
 
13
+function resizeGisMap() {
14
+  gisMapRef.value?.resizeMap?.()
15
+}
16
+
10 17
 onMounted(() => {
11
-  unbindScale = bindScreenScale(viewportRef.value)
18
+  unbindScale = bindScreenScale(viewportRef.value, resizeGisMap)
19
+  window.addEventListener("resize", resizeGisMap)
12 20
 })
13 21
 
14 22
 onUnmounted(() => {
23
+  window.removeEventListener("resize", resizeGisMap)
15 24
   if (typeof unbindScale === "function") {
16 25
     unbindScale()
17 26
   }
@@ -19,15 +28,19 @@ onUnmounted(() => {
19 28
 </script>
20 29
 
21 30
 <template>
22
-  <!-- 外层铺满窗口,内层舞台按 21:9 等比缩放 -->
23 31
   <div ref="viewportRef" class="screen-viewport">
24 32
     <div
25 33
       class="screen-stage"
26 34
       :style="{ width: DESIGN_WIDTH + 'px', height: DESIGN_HEIGHT + 'px' }"
27 35
     >
28
-      <ScreenNavBar />
29
-      <div class="screen-body">
30
-        <router-view />
36
+      <div class="screen-map-layer">
37
+        <GisMap ref="gisMapRef" :interactive="true" />
38
+      </div>
39
+      <div class="screen-chrome">
40
+        <ScreenNavBar />
41
+        <div class="screen-body">
42
+          <router-view />
43
+        </div>
31 44
       </div>
32 45
     </div>
33 46
   </div>
@@ -41,10 +54,7 @@ onUnmounted(() => {
41 54
   display: flex;
42 55
   align-items: center;
43 56
   justify-content: center;
44
-  /* background: var(--screen-bg); */
45 57
   box-sizing: border-box;
46
-  /* background: url('@/assets/bg.png') no-repeat center center;
47
-  background-size: cover; */
48 58
 }
49 59
 
50 60
 .screen-stage {
@@ -52,17 +62,51 @@ onUnmounted(() => {
52 62
   flex-shrink: 0;
53 63
   transform: scale(var(--screen-scale, 1));
54 64
   transform-origin: center center;
55
-  display: flex;
56
-  flex-direction: column;
57 65
   box-sizing: border-box;
58 66
   overflow: hidden;
59
-  /* background: var(--screen-bg-stage); */
60
-  background: url('../assets/bg.png') no-repeat center center;
67
+}
68
+
69
+.screen-map-layer {
70
+  position: absolute;
71
+  inset: 0;
72
+  z-index: 0;
73
+}
74
+
75
+.screen-chrome {
76
+  position: relative;
77
+  z-index: 1;
78
+  display: flex;
79
+  flex-direction: column;
80
+  width: 100%;
81
+  height: 100%;
82
+  pointer-events: none;
83
+}
84
+
85
+.screen-chrome::before {
86
+  content: "";
87
+  position: absolute;
88
+  inset: 0;
89
+  /* background: url("../assets/bg.png") no-repeat center center; */
61 90
   background-size: cover;
91
+  pointer-events: none;
92
+  z-index: 0;
93
+}
94
+
95
+.screen-chrome > * {
96
+  position: relative;
97
+  z-index: 1;
62 98
 }
63 99
 
64 100
 .screen-body {
65
-  width: 100%;
66
-  height: calc(100% - 100px);
101
+  flex: 1;
102
+  min-height: 0;
103
+  position: relative;
104
+  pointer-events: none;
105
+}
106
+
107
+.screen-body > :deep(.screen-page) {
108
+  position: relative;
109
+  z-index: 1;
110
+  height: 100%;
67 111
 }
68 112
 </style>

+ 6 - 0
ruoyi-screen/src/style.css

@@ -51,4 +51,10 @@ body {
51 51
   min-height: 0;
52 52
   display: flex;
53 53
   flex-direction: column;
54
+  /* 中间地图区域无面板,点击穿透到底图 */
55
+  pointer-events: none;
56
+}
57
+
58
+.screen-page > * {
59
+  pointer-events: auto;
54 60
 }

+ 331 - 0
ruoyi-screen/src/utils/gisLoader.js

@@ -0,0 +1,331 @@
1
+const CMMAP_GL_VERSION = '1.0'
2
+
3
+let loadPromise = null
4
+
5
+function trimSlash(url) {
6
+  return String(url || '').replace(/\/$/, '')
7
+}
8
+
9
+/** GIS 服务根地址,浏览器直连,如 http://117.180.211.5:8074 */
10
+export function getGisBaseUrl() {
11
+  return trimSlash(import.meta.env.VITE_GIS_BASE_URL)
12
+}
13
+
14
+export function getGisAssetUrls() {
15
+  const base = getGisBaseUrl()
16
+  const dist = `/agis/resources/v1/tile-server/dist/cmmap-gl/${CMMAP_GL_VERSION}`
17
+  return {
18
+    js: `${base}${dist}/cmmap-gl.js`,
19
+    css: `${base}${dist}/cmmap-gl.css`
20
+  }
21
+}
22
+
23
+export function getGisAccessToken() {
24
+  return (import.meta.env.VITE_GIS_ACCESS_TOKEN || '').trim()
25
+}
26
+
27
+export function getGisMapStyle() {
28
+  const style = (import.meta.env.VITE_GIS_MAP_STYLE || '').trim()
29
+  return style || undefined
30
+}
31
+
32
+/** WMTS 图层名,如 baqing_wp */
33
+export function getGisWmtsLayer() {
34
+  return (import.meta.env.VITE_GIS_WMTS_LAYER || '').trim()
35
+}
36
+
37
+/** 瓦片边长(256 或 512);512 时向 WMTS 追加 WIDTH/HEIGHT 请求高清瓦片 */
38
+export function getGisTileSize() {
39
+  const n = Number(import.meta.env.VITE_GIS_TILE_SIZE)
40
+  return Number.isFinite(n) && n > 0 ? n : 256
41
+}
42
+
43
+function getGisWmtsTileMatrixSet() {
44
+  return (import.meta.env.VITE_GIS_WMTS_TILEMATRIXSET || 'EPSG:3857_base').trim()
45
+}
46
+
47
+/** TileMatrix 模板,{z} 由地图替换,如 {z} 或 EPSG:3857_base:{z} */
48
+function getGisWmtsTileMatrixTemplate() {
49
+  return (import.meta.env.VITE_GIS_WMTS_TILE_MATRIX || '{z}').trim()
50
+}
51
+
52
+function getGisWmtsZoomOffset() {
53
+  const n = Number(import.meta.env.VITE_GIS_WMTS_ZOOM_OFFSET)
54
+  return Number.isFinite(n) ? n : 0
55
+}
56
+
57
+function buildWmtsTileUrl() {
58
+  const base = getGisBaseUrl()
59
+  const layer = getGisWmtsLayer()
60
+  const key = getGisAccessToken()
61
+  const tileSize = getGisTileSize()
62
+  const matrix = getGisWmtsTileMatrixTemplate()
63
+  const matrixSet = encodeURIComponent(getGisWmtsTileMatrixSet())
64
+  let url =
65
+    `${base}/agis/maps/v1/wmts` +
66
+    `?LAYER=${encodeURIComponent(layer)}` +
67
+    '&SERVICE=WMTS&REQUEST=GetTile' +
68
+    `&TileMatrix=${matrix}` +
69
+    '&TileCol={x}&TileRow={y}' +
70
+    '&Version=1.0.0&FORMAT=image/png&style=' +
71
+    `&tilematrixset=${matrixSet}` +
72
+    `&key=${encodeURIComponent(key)}`
73
+  if (tileSize !== 256) {
74
+    url += `&WIDTH=${tileSize}&HEIGHT=${tileSize}`
75
+  }
76
+  return url
77
+}
78
+
79
+/** 高 DPI 时提升一级瓦片并同步 x/y,避免只改 TileMatrix 导致错位 */
80
+function boostWmtsTileUrl(url, boost) {
81
+  if (boost <= 0) {
82
+    return url
83
+  }
84
+  const zm = url.match(/TileMatrix=([^&]+)/)
85
+  const xc = url.match(/TileCol=(\d+)/)
86
+  const yr = url.match(/TileRow=(\d+)/)
87
+  if (!zm || !xc || !yr) {
88
+    return url
89
+  }
90
+  const matrixRaw = decodeURIComponent(zm[1])
91
+  const zMatch = matrixRaw.match(/(\d+)$/)
92
+  if (!zMatch) {
93
+    return url
94
+  }
95
+  let z = parseInt(zMatch[1], 10)
96
+  let x = parseInt(xc[1], 10)
97
+  let y = parseInt(yr[1], 10)
98
+  const maxZ = getGisMaxZoom()
99
+  for (let i = 0; i < boost && z < maxZ; i += 1) {
100
+    z += 1
101
+    x *= 2
102
+    y *= 2
103
+  }
104
+  const nextMatrix = matrixRaw.replace(/\d+$/, String(z))
105
+  return url
106
+    .replace(/TileMatrix=[^&]+/, `TileMatrix=${encodeURIComponent(nextMatrix)}`)
107
+    .replace(/TileCol=\d+/, `TileCol=${x}`)
108
+    .replace(/TileRow=\d+/, `TileRow=${y}`)
109
+}
110
+
111
+/**
112
+ * 瓦片请求改写:zoom 偏移 + 高 DPI 时请求更高级别瓦片(更清晰)
113
+ * @param {() => number} getPixelRatio
114
+ */
115
+export function createGisTransformRequest(getPixelRatio) {
116
+  const zoomOffset = getGisWmtsZoomOffset()
117
+  return (url, resourceType) => {
118
+    if (resourceType !== 'Tile' || !String(url).includes('/wmts')) {
119
+      return { url }
120
+    }
121
+    let next = url
122
+    if (zoomOffset) {
123
+      next = next.replace(/TileMatrix=(\d+)/, (_, z) => `TileMatrix=${parseInt(z, 10) + zoomOffset}`)
124
+    }
125
+    const ratio = typeof getPixelRatio === 'function' ? getPixelRatio() : 1
126
+    const boost = ratio >= 2 ? 1 : 0
127
+    if (boost > 0) {
128
+      next = boostWmtsTileUrl(next, boost)
129
+    }
130
+    return { url: next }
131
+  }
132
+}
133
+
134
+/** 由 WMTS GetTile 模板生成 Mapbox Style(raster 源) */
135
+export function buildWmtsMapStyle() {
136
+  const base = getGisBaseUrl()
137
+  const layer = getGisWmtsLayer()
138
+  const key = getGisAccessToken()
139
+  if (!base || !layer || !key) {
140
+    return null
141
+  }
142
+  const tileSize = getGisTileSize()
143
+  const tileUrl = buildWmtsTileUrl()
144
+  return {
145
+    version: 8,
146
+    sources: {
147
+      'gis-wmts': {
148
+        type: 'raster',
149
+        tiles: [tileUrl],
150
+        tileSize,
151
+        scheme: 'xyz'
152
+      }
153
+    },
154
+    layers: [
155
+      {
156
+        id: 'gis-wmts-layer',
157
+        type: 'raster',
158
+        source: 'gis-wmts',
159
+        minzoom: getGisMinZoom(),
160
+        maxzoom: getGisMaxZoom()
161
+      }
162
+    ]
163
+  }
164
+}
165
+
166
+/** 优先使用 VITE_GIS_MAP_STYLE URL;否则按 WMTS 图层拼装样式对象 */
167
+export function resolveGisMapStyle() {
168
+  const explicit = getGisMapStyle()
169
+  if (explicit) {
170
+    return explicit
171
+  }
172
+  return buildWmtsMapStyle()
173
+}
174
+
175
+export function getGisDefaultCenter() {
176
+  const raw = (import.meta.env.VITE_GIS_DEFAULT_CENTER || '94.05,31.92').trim()
177
+  const parts = raw.split(',').map((n) => Number(n.trim()))
178
+  if (parts.length >= 2 && parts.every((n) => Number.isFinite(n))) {
179
+    return [parts[0], parts[1]]
180
+  }
181
+  return [94.05, 31.92]
182
+}
183
+
184
+export function getGisDefaultZoom() {
185
+  const z = Number(import.meta.env.VITE_GIS_DEFAULT_ZOOM)
186
+  return Number.isFinite(z) ? z : 17
187
+}
188
+
189
+export function getGisMinZoom() {
190
+  const z = Number(import.meta.env.VITE_GIS_MIN_ZOOM)
191
+  return Number.isFinite(z) ? z : 6
192
+}
193
+
194
+export function getGisMaxZoom() {
195
+  const z = Number(import.meta.env.VITE_GIS_MAX_ZOOM)
196
+  return Number.isFinite(z) ? z : 17
197
+}
198
+
199
+export function isGisConfigured() {
200
+  return !!(getGisBaseUrl() && getGisAccessToken() && resolveGisMapStyle())
201
+}
202
+
203
+/** 关闭 cmmap 遥测,避免 events.cmmap.com SSL 报错刷控制台 */
204
+export function disableCmmapTelemetry(cmmapgl) {
205
+  if (!cmmapgl) {
206
+    return
207
+  }
208
+  try {
209
+    if (typeof cmmapgl.setTelemetryEnabled === 'function') {
210
+      cmmapgl.setTelemetryEnabled(false)
211
+    }
212
+  } catch {
213
+    /* ignore */
214
+  }
215
+  try {
216
+    if (cmmapgl.config) {
217
+      cmmapgl.config.EVENTS_URL = ''
218
+      if ('FEEDBACK_URL' in cmmapgl.config) {
219
+        cmmapgl.config.FEEDBACK_URL = ''
220
+      }
221
+    }
222
+  } catch {
223
+    /* ignore */
224
+  }
225
+}
226
+
227
+let telemetryBlocked = false
228
+
229
+function blockCmmapTelemetryRequests() {
230
+  if (telemetryBlocked || typeof window === 'undefined') {
231
+    return
232
+  }
233
+  telemetryBlocked = true
234
+  const shouldBlock = (url) => {
235
+    const s = String(url || '')
236
+    return s.includes('events.cmmap.com')
237
+  }
238
+  const nativeFetch = window.fetch
239
+  if (typeof nativeFetch === 'function') {
240
+    window.fetch = function blockedFetch(input, init) {
241
+      const url = typeof input === 'string' ? input : input?.url
242
+      if (shouldBlock(url)) {
243
+        return Promise.resolve(new Response(null, { status: 204 }))
244
+      }
245
+      return nativeFetch.call(this, input, init)
246
+    }
247
+  }
248
+  const XHR = window.XMLHttpRequest
249
+  if (XHR && XHR.prototype) {
250
+    const open = XHR.prototype.open
251
+    XHR.prototype.open = function blockedOpen(method, url, ...rest) {
252
+      this.__gisBlockedTelemetry = shouldBlock(url)
253
+      return open.call(this, method, url, ...rest)
254
+    }
255
+    const send = XHR.prototype.send
256
+    XHR.prototype.send = function blockedSend(...args) {
257
+      if (this.__gisBlockedTelemetry) {
258
+        return
259
+      }
260
+      return send.apply(this, args)
261
+    }
262
+  }
263
+}
264
+
265
+function loadCss(href) {
266
+  return new Promise((resolve, reject) => {
267
+    if (document.querySelector(`link[data-gis-sdk="cmmap-gl-css"]`)) {
268
+      resolve()
269
+      return
270
+    }
271
+    const link = document.createElement('link')
272
+    link.rel = 'stylesheet'
273
+    link.href = href
274
+    link.dataset.gisSdk = 'cmmap-gl-css'
275
+    link.onload = () => resolve()
276
+    link.onerror = () => reject(new Error('GIS 样式加载失败'))
277
+    document.head.appendChild(link)
278
+  })
279
+}
280
+
281
+function loadScript(src) {
282
+  return new Promise((resolve, reject) => {
283
+    if (window.cmmapgl) {
284
+      resolve(window.cmmapgl)
285
+      return
286
+    }
287
+    const existing = document.querySelector(`script[data-gis-sdk="cmmap-gl-js"]`)
288
+    if (existing) {
289
+      existing.addEventListener('load', () => resolve(window.cmmapgl))
290
+      existing.addEventListener('error', () => reject(new Error('GIS 脚本加载失败')))
291
+      return
292
+    }
293
+    const script = document.createElement('script')
294
+    script.src = src
295
+    script.async = true
296
+    script.dataset.gisSdk = 'cmmap-gl-js'
297
+    script.onload = () => resolve(window.cmmapgl)
298
+    script.onerror = () => reject(new Error('GIS 脚本加载失败'))
299
+    document.head.appendChild(script)
300
+  })
301
+}
302
+
303
+/** 动态加载 cmmap GL JS SDK,返回全局 cmmapgl */
304
+export function loadGisSdk() {
305
+  if (window.cmmapgl) {
306
+    blockCmmapTelemetryRequests()
307
+    disableCmmapTelemetry(window.cmmapgl)
308
+    return Promise.resolve(window.cmmapgl)
309
+  }
310
+  if (!getGisBaseUrl()) {
311
+    return Promise.reject(new Error('未配置 VITE_GIS_BASE_URL'))
312
+  }
313
+  if (!loadPromise) {
314
+    const { js, css } = getGisAssetUrls()
315
+    loadPromise = loadCss(css)
316
+      .then(() => loadScript(js))
317
+      .then((cmmapgl) => {
318
+        if (!cmmapgl) {
319
+          throw new Error('cmmapgl 未挂载到 window')
320
+        }
321
+        blockCmmapTelemetryRequests()
322
+        disableCmmapTelemetry(cmmapgl)
323
+        return cmmapgl
324
+      })
325
+      .catch((err) => {
326
+        loadPromise = null
327
+        throw err
328
+      })
329
+  }
330
+  return loadPromise
331
+}

+ 21 - 1
ruoyi-screen/src/utils/screenFit.js

@@ -14,7 +14,7 @@ export const DESIGN_HEIGHT = (DESIGN_WIDTH * 9) / 21
14 14
  * @param {HTMLElement} root 包住舞台的节点(一般占满视口)
15 15
  * @returns {function} 取消监听用的函数,在组件卸载时调用
16 16
  */
17
-export function bindScreenScale(root) {
17
+export function bindScreenScale(root, onScaleChange) {
18 18
   function update() {
19 19
     if (!root) {
20 20
       return
@@ -23,6 +23,9 @@ export function bindScreenScale(root) {
23 23
     const sy = window.innerHeight / DESIGN_HEIGHT
24 24
     const scale = Math.min(sx, sy)
25 25
     root.style.setProperty("--screen-scale", String(scale))
26
+    if (typeof onScaleChange === "function") {
27
+      requestAnimationFrame(onScaleChange)
28
+    }
26 29
   }
27 30
   update()
28 31
   window.addEventListener("resize", update)
@@ -30,3 +33,20 @@ export function bindScreenScale(root) {
30 33
     window.removeEventListener("resize", update)
31 34
   }
32 35
 }
36
+
37
+/** 读取 screenFit 写入的 --screen-scale(舞台 CSS 缩放比) */
38
+export function getScreenScale(root = document.querySelector(".screen-viewport")) {
39
+  if (!root) {
40
+    return 1
41
+  }
42
+  const raw = getComputedStyle(root).getPropertyValue("--screen-scale").trim()
43
+  const scale = parseFloat(raw)
44
+  return Number.isFinite(scale) && scale > 0 ? scale : 1
45
+}
46
+
47
+/** 地图像素比:补偿舞台 scale 放大,避免 WebGL 画布被拉伸发糊 */
48
+export function getMapPixelRatio(root) {
49
+  const scale = getScreenScale(root)
50
+  const dpr = window.devicePixelRatio || 1
51
+  return Math.min(dpr * scale, 3)
52
+}

+ 73 - 64
ruoyi-screen/vite.config.js

@@ -1,65 +1,74 @@
1
-import { defineConfig, loadEnv } from 'vite'

2
-import vue from '@vitejs/plugin-vue'

3
-import path from 'path'

4
-import { fileURLToPath } from 'url'

5
-

6
-const __dirname = path.dirname(fileURLToPath(import.meta.url))

7
-

8
-/** 与 ruoyi-ui vue.config.js publicPath 一致:生产 /screen,开发 / */

9
-function resolvePublicPath(mode, env) {

10
-  if (mode !== 'production') {

11
-    return '/'

12
-  }

13
-  const raw = String(env.VITE_APP_PUBLIC_PATH || '/screen').trim()

14
-  if (!raw || raw === '/') {

15
-    return '/'

16
-  }

17
-  return `/${raw.replace(/^\/+|\/+$/g, '')}/`

18
-}

19
-

20
-// https://vite.dev/config/

21
-export default defineConfig(({ mode }) => {

22
-  const env = loadEnv(mode, process.cwd(), '')

23
-  const base = resolvePublicPath(mode, env)

24
-

25
-  const baseApi = env.VITE_APP_BASE_API || '/dev-api'

26
-  const proxyTarget = (env.VITE_PROXY_TARGET || 'http://192.168.1.6:8010').replace(/\/$/, '')

27
-

28
-  const proxy = {

29
-    [baseApi]: {

30
-      target: proxyTarget,

31
-      changeOrigin: true,

32
-      rewrite: (path) => path.replace(new RegExp(`^${baseApi}`), '')

33
-    },

34
-    '^/v3/api-docs/(.*)': {

35
-      target: proxyTarget,

36
-      changeOrigin: true

37
-    }

38
-  }

39
-

40
-  const llmTarget = env.LLM_PROXY_TARGET

41
-  if (llmTarget) {

42
-    proxy['^/llm-dev-proxy'] = {

43
-      target: llmTarget.replace(/\/$/, ''),

44
-      changeOrigin: true,

45
-      rewrite: (path) => path.replace(/^\/llm-dev-proxy/, '')

46
-    }

47
-  }

48
-

49
-  return {

50
-    base,

51
-    plugins: [vue()],

52
-    resolve: {

53
-      alias: {

54
-        '@': path.resolve(__dirname, 'src')

55
-      }

56
-    },

57
-    server: {

58
-      host: true,

59
-      port: 5188,

60
-      strictPort: false,

61
-      proxy

62
-    }

63
-  }

64
-})

1
+import { defineConfig, loadEnv } from 'vite'
2
+import vue from '@vitejs/plugin-vue'
3
+import path from 'path'
4
+import { fileURLToPath } from 'url'
5
+
6
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+/** 与 ruoyi-ui vue.config.js publicPath 一致:生产 /screen,开发 / */
9
+function resolvePublicPath(mode, env) {
10
+  if (mode !== 'production') {
11
+    return '/'
12
+  }
13
+  const raw = String(env.VITE_APP_PUBLIC_PATH || '/screen').trim()
14
+  if (!raw || raw === '/') {
15
+    return '/'
16
+  }
17
+  return `/${raw.replace(/^\/+|\/+$/g, '')}/`
18
+}
19
+
20
+// https://vite.dev/config/
21
+export default defineConfig(({ mode }) => {
22
+  const env = loadEnv(mode, process.cwd(), '')
23
+  const base = resolvePublicPath(mode, env)
24
+
25
+  const baseApi = env.VITE_APP_BASE_API || '/dev-api'
26
+  const proxyTarget = (env.VITE_PROXY_TARGET || 'http://192.168.1.6:8010').replace(/\/$/, '')
27
+
28
+  const proxy = {
29
+    [baseApi]: {
30
+      target: proxyTarget,
31
+      changeOrigin: true,
32
+      rewrite: (path) => path.replace(new RegExp(`^${baseApi}`), '')
33
+    },
34
+    '^/v3/api-docs/(.*)': {
35
+      target: proxyTarget,
36
+      changeOrigin: true
37
+    }
38
+  }
39
+
40
+  const llmTarget = env.LLM_PROXY_TARGET
41
+  if (llmTarget) {
42
+    proxy['^/llm-dev-proxy'] = {
43
+      target: llmTarget.replace(/\/$/, ''),
44
+      changeOrigin: true,
45
+      rewrite: (path) => path.replace(/^\/llm-dev-proxy/, '')
46
+    }
47
+  }
48
+
49
+  const gisTarget = env.VITE_GIS_PROXY_TARGET
50
+  if (gisTarget) {
51
+    proxy['^/gis-dev-proxy'] = {
52
+      target: gisTarget.replace(/\/$/, ''),
53
+      changeOrigin: true,
54
+      rewrite: (path) => path.replace(/^\/gis-dev-proxy/, '')
55
+    }
56
+  }
57
+
58
+  return {
59
+    base,
60
+    plugins: [vue()],
61
+    resolve: {
62
+      alias: {
63
+        '@': path.resolve(__dirname, 'src')
64
+      }
65
+    },
66
+    server: {
67
+      host: true,
68
+      port: 5188,
69
+      strictPort: false,
70
+      proxy
71
+    }
72
+  }
73
+})
65 74
 

+ 9 - 0
scripts/extract-docx-text.mjs

@@ -0,0 +1,9 @@
1
+import fs from 'fs'
2
+import path from 'path'
3
+import os from 'os'
4
+
5
+const raw = path.join(os.tmpdir(), 'gis-doc-raw.xml')
6
+let x = fs.readFileSync(raw, 'utf8')
7
+x = x.replace(/<\/w:p>/g, '\n').replace(/<[^>]+>/g, '')
8
+x = x.replace(/\n{2,}/g, '\n')
9
+process.stdout.write(x)

+ 42 - 0
scripts/extract-gis-body.mjs

@@ -0,0 +1,42 @@
1
+import fs from 'fs'
2
+import path from 'path'
3
+import os from 'os'
4
+
5
+const x = fs.readFileSync(path.join(os.tmpdir(), 'gis-doc-raw.xml'), 'utf8')
6
+
7
+function strip(chunk) {
8
+  return chunk
9
+    .replace(/&lt;/g, '<')
10
+    .replace(/&gt;/g, '>')
11
+    .replace(/&quot;/g, '"')
12
+    .replace(/&amp;/g, '&')
13
+    .replace(/<w:tab[^>]*\/>/g, '\t')
14
+    .replace(/<\/w:p>/g, '\n')
15
+    .replace(/<[^>]+>/g, '')
16
+    .replace(/\n{3,}/g, '\n\n')
17
+}
18
+
19
+const anchors = [
20
+  '部署在GIS服务上',
21
+  'cmmapgl.accessToken',
22
+  '地图密钥',
23
+  '2.3 地图组件',
24
+  '构造一个地图对象',
25
+  'new cmmapgl.Map',
26
+  'cmmapgl.Marker',
27
+  'addSource',
28
+  'addLayer',
29
+  'CMMapDrawControl',
30
+  'DistrictCluster',
31
+  '路线规划'
32
+]
33
+
34
+for (const a of anchors) {
35
+  const i = x.indexOf(a)
36
+  if (i < 0) {
37
+    console.log(`\n[MISS] ${a}`)
38
+    continue
39
+  }
40
+  console.log(`\n######## ${a} ########`)
41
+  console.log(strip(x.slice(i, i + 6000)))
42
+}

+ 62 - 0
scripts/extract-gis-paragraphs.mjs

@@ -0,0 +1,62 @@
1
+import fs from 'fs'
2
+import path from 'path'
3
+import os from 'os'
4
+
5
+const x = fs.readFileSync(path.join(os.tmpdir(), 'gis-doc-raw.xml'), 'utf8')
6
+
7
+function decodeEntities(s) {
8
+  return s
9
+    .replace(/&lt;/g, '<')
10
+    .replace(/&gt;/g, '>')
11
+    .replace(/&quot;/g, '"')
12
+    .replace(/&amp;/g, '&')
13
+}
14
+
15
+function stripXml(chunk) {
16
+  return decodeEntities(chunk)
17
+    .replace(/<w:tab[^>]*\/>/g, '\t')
18
+    .replace(/<\/w:p>/g, '\n')
19
+    .replace(/<[^>]+>/g, '')
20
+    .replace(/\n{3,}/g, '\n\n')
21
+    .trim()
22
+}
23
+
24
+// Collect paragraphs that look like doc body (skip TOC hyperlinks)
25
+const paras = []
26
+for (const m of x.matchAll(/<w:p[\s\S]*?<\/w:p>/g)) {
27
+  const t = stripXml(m[0])
28
+  if (!t || t.length < 4) continue
29
+  if (t.includes('PAGEREF') || t.includes('HYPERLINK \\l')) continue
30
+  if (t.startsWith('TOC ')) continue
31
+  paras.push(t)
32
+}
33
+
34
+const out = []
35
+const keywords = /GIS|cmmap|Map|token|密钥|Marker|GeoJSON|图层|地图|script|container|accessToken|NavigationControl|MouseTool|路线|POI/i
36
+let inSection = false
37
+for (let i = 0; i < paras.length; i++) {
38
+  const p = paras[i]
39
+  if (/^2[\.\s]/.test(p) || /^[0-9]+\s/.test(p) || keywords.test(p)) {
40
+    inSection = true
41
+  }
42
+  if (inSection && keywords.test(p)) {
43
+    out.push(p)
44
+    // include next few lines if code-like
45
+    for (let j = 1; j <= 3 && i + j < paras.length; j++) {
46
+      const n = paras[i + j]
47
+      if (n.includes('{') || n.includes('cmmap') || n.includes('map.') || n.includes('<')) {
48
+        out.push(n)
49
+      }
50
+    }
51
+  }
52
+}
53
+
54
+const unique = [...new Set(out)]
55
+const target = path.resolve('doc/大屏/集中化GIS系统JSAPI接口文档-提取.txt')
56
+fs.writeFileSync(target, unique.join('\n\n'), 'utf8')
57
+console.log('written', target, 'lines', unique.length)
58
+
59
+// Also dump section 2.x headings
60
+const headings = paras.filter((p) => /^[0-9]+(\.[0-9]+)*\s/.test(p) && p.length < 80)
61
+fs.writeFileSync(path.resolve('doc/大屏/集中化GIS系统JSAPI-目录.txt'), headings.join('\n'), 'utf8')
62
+console.log('headings', headings.length)

+ 45 - 0
scripts/extract-gis-sections.mjs

@@ -0,0 +1,45 @@
1
+import fs from 'fs'
2
+import path from 'path'
3
+import os from 'os'
4
+
5
+const x = fs.readFileSync(path.join(os.tmpdir(), 'gis-doc-raw.xml'), 'utf8')
6
+
7
+function strip(chunk) {
8
+  return chunk
9
+    .replace(/<w:tab[^>]*\/>/g, '\t')
10
+    .replace(/<\/w:p>/g, '\n')
11
+    .replace(/<[^>]+>/g, '')
12
+}
13
+
14
+const terms = [
15
+  'cmmapgl.accessToken',
16
+  'setAccessToken',
17
+  '地图密钥',
18
+  '2.3 地图组件',
19
+  'new cmmapgl.Map',
20
+  'NavigationControl',
21
+  'GeoJSON',
22
+  '准备步骤',
23
+  '基本准备'
24
+]
25
+
26
+for (const t of terms) {
27
+  let idx = 0
28
+  let n = 0
29
+  while (n < 3) {
30
+    const found = x.indexOf(t, idx)
31
+    if (found < 0) break
32
+    console.log(`\n==== ${t} #${n + 1} ====`)
33
+    console.log(strip(x.slice(found, found + 2000)))
34
+    idx = found + t.length
35
+    n++
36
+  }
37
+}
38
+
39
+const markers = ['2.2 地图密钥', '2.3 地图组件', '准备步骤', '基本准备', 'cmmapgl.Map']
40
+for (const m of markers) {
41
+  const i = x.indexOf(m)
42
+  if (i < 0) continue
43
+  console.log(`\n######## SECTION ${m} ########`)
44
+  console.log(strip(x.slice(i, i + 12000)))
45
+}

+ 34 - 0
scripts/find-gis-snippets.mjs

@@ -0,0 +1,34 @@
1
+import fs from 'fs'
2
+import path from 'path'
3
+import os from 'os'
4
+
5
+const x = fs.readFileSync(path.join(os.tmpdir(), 'gis-doc-raw.xml'), 'utf8')
6
+
7
+function snippet(k, len = 1800) {
8
+  const i = x.indexOf(k)
9
+  if (i < 0) return `${k}: NOT FOUND`
10
+  return x
11
+    .slice(Math.max(0, i - 200), i + len)
12
+    .replace(/&lt;/g, '<')
13
+    .replace(/&gt;/g, '>')
14
+    .replace(/&quot;/g, '"')
15
+    .replace(/<w:tab[^>]*\/>/g, ' ')
16
+    .replace(/<\/w:p>/g, '\n')
17
+    .replace(/<[^>]+>/g, '')
18
+}
19
+
20
+const keys = [
21
+  'cmmap-gl.js',
22
+  'cmmap-gl.css',
23
+  'cmmapgl.accessToken',
24
+  '地图密钥',
25
+  '地图组件',
26
+  'script src',
27
+  'style:',
28
+  'agis/resources'
29
+]
30
+
31
+for (const k of keys) {
32
+  console.log('\n========', k, '========\n')
33
+  console.log(snippet(k))
34
+}