gis-view-webgl2 0.1.2 → 0.1.4

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 (66) hide show
  1. package/README.md +21 -14
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +421 -76
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +151 -0
  7. package/docs/GisView.md +4 -4
  8. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +137 -0
  9. package/docs/api/index.html +23 -9
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +14 -12
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +3 -4
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/ExtensionDemo.vue +69 -12
  16. package/docs/demos/GisViewDemo.vue +64 -8809
  17. package/docs/demos/SearchFilterDemo.vue +13 -11
  18. package/docs/demos/api-index.js +58 -47
  19. package/docs/demos/demos.js +34 -17
  20. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +29 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  25. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  26. package/docs/demos/gis-view-demo/demo.css +1230 -0
  27. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  28. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  29. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  30. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  31. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  32. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  33. package/docs/format/07-/344/272/213/344/273/266/350/275/275/350/215/267/344/270/216/347/244/272/344/276/213/346/225/260/346/215/256.md +3 -2
  34. package/docs/index.html +41 -4
  35. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +1 -1
  36. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +3 -2
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +104 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +15 -14
  39. package/docs/manual/07-/346/214/207/351/222/210/344/270/216/350/246/201/347/264/240/347/274/226/350/276/221.md +40 -3
  40. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +50 -3
  41. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +6 -2
  42. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +39 -7
  43. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +13 -4
  44. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  45. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +121 -49
  46. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +37 -3
  47. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +17 -8
  48. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +45 -25
  49. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +91 -20
  50. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +3 -3
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +25 -1
  52. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +11 -8
  53. package/docs/steps/README.md +17 -11
  54. package/docs/steps/selftest-actions.html +3 -5
  55. package/docs/steps/selftest-demo.html +27 -18
  56. package/docs/steps/selftest-demos.html +409 -32
  57. package/docs/steps/selftest-docs.html +33 -4
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +603 -21
  60. package/docs/steps/selftest-labels.html +74 -0
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +150 -29
  63. package/docs/steps/selftest-select.html +90 -0
  64. package/docs/steps/step-10-full.js +1 -1
  65. package/docs/steps/step-12-events.js +1 -1
  66. package/package.json +1 -1
@@ -25,6 +25,7 @@
25
25
  import { createApp } from 'vue'
26
26
  import App from '/src/App.vue'
27
27
  import { channelDraftArrowPlacement } from '/src/gis-view/engine.js'
28
+ import { createMinimap } from '/src/gis-view/minimap.js'
28
29
  const results = []
29
30
  const flush = () => { document.getElementById('out').textContent = results.join('\n') }
30
31
  const check = (name, ok, extra = '') => {
@@ -90,8 +91,8 @@
90
91
  // 1. 外壳:默认综合示例,页签齐全
91
92
  const loaded = await canvasReady()
92
93
  check('演示外壳把默认演示(综合示例)挂起来了', Boolean(loaded), loaded ? 'canvas 就绪' : '没等到 canvas')
93
- check('页签列出全部演示(20 个:19 个功能 + 综合)', tabs().length === 20, `${tabs().length} 个页签:${tabs().map(tab => tab.textContent.trim()).join(' / ')}`)
94
- check('页头的演示数量直接跟随注册表(20 个)', (document.querySelector('.page-header__count')?.textContent ?? '').includes('20 个'), document.querySelector('.page-header__count')?.textContent?.trim() ?? '没有页头计数')
94
+ check('页签列出全部演示(22 个:21 个功能 + 综合)', tabs().length === 22, `${tabs().length} 个页签:${tabs().map(tab => tab.textContent.trim()).join(' / ')}`)
95
+ check('页头的演示数量直接跟随注册表(22 个)', (document.querySelector('.page-header__count')?.textContent ?? '').includes('22 个'), document.querySelector('.page-header__count')?.textContent?.trim() ?? '没有页头计数')
95
96
  check('默认页签是综合示例', (document.querySelector('.demo-shell__tab--active')?.textContent ?? '').includes('综合'), document.querySelector('.demo-shell__tab--active')?.textContent?.trim() ?? '')
96
97
  check('默认页签写进地址栏(刷新还停在同一个演示)', location.search.includes('demo=full'), location.search)
97
98
  check('综合示例的面板照旧在(拆分没有动它)', document.querySelectorAll('.demo-panel-buttons button').length >= 6, `${document.querySelectorAll('.demo-panel-buttons button').length} 个面板按钮`)
@@ -121,7 +122,7 @@
121
122
  const afterBack = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
122
123
  check('键盘 [ 往回翻(基础视图回到综合示例)', afterBack.includes('综合'), afterBack)
123
124
  const eyebrow = document.querySelector('.demo-shell__eyebrow')?.textContent.trim() ?? ''
124
- check('当前演示显示分组与序号(综合 · 20 / 20)', eyebrow.includes('综合') && eyebrow.includes('20 / 20'), eyebrow)
125
+ check('当前演示显示分组与序号(综合 · 22 / 22)', eyebrow.includes('综合') && eyebrow.includes('22 / 22'), eyebrow)
125
126
  document.querySelector('[data-demo=demo-next]')?.click()
126
127
  await wait(60)
127
128
  const afterStepNext = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
@@ -133,10 +134,93 @@
133
134
  document.body.appendChild(keyGuard)
134
135
  keyGuard.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true }))
135
136
  await wait(60)
136
- const afterGuard = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
137
- keyGuard.remove()
138
- check('焦点在输入框里时键盘 [ / ] 不抢键', afterGuard.includes('综合'), afterGuard)
139
- // 2. 每个功能演示都切一遍:画布起得来、说明与读数有东西
137
+ const afterGuard = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
138
+ keyGuard.remove()
139
+ check('焦点在输入框里时键盘 [ / ] 不抢键', afterGuard.includes('综合'), afterGuard)
140
+ // 1.2 鹰眼图线路层:同一份数据补画命中缓存,固定范围外的线路不投影 / 不建路径
141
+ const miniHost = document.createElement('div')
142
+ miniHost.style.cssText = 'position:absolute;left:-9999px;width:180px;height:120px'
143
+ document.body.append(miniHost)
144
+ const miniData = {
145
+ points: [{ x: 50, y: 50 }],
146
+ lines: [
147
+ { path: [[0, 0], [100, 0]], minX: 0, minY: 0, maxX: 100, maxY: 0 },
148
+ { path: [[1000, 1000], [1100, 1000]], minX: 1000, minY: 1000, maxX: 1100, maxY: 1000 }
149
+ ]
150
+ }
151
+ const mini = createMinimap(miniHost, {
152
+ enabled: true,
153
+ width: 180,
154
+ height: 120,
155
+ background: null,
156
+ extent: { minX: 0, minY: 0, maxX: 100, maxY: 100 },
157
+ maxPixelRatio: 1
158
+ }, {
159
+ getData: () => miniData,
160
+ getCamera: () => ({ center: [50, 50], scale: 1 }),
161
+ getViewport: () => ({ width: 180, height: 120, pixelRatio: 1, displayScale: 1 }),
162
+ getFallbackBounds: () => ({ minX: 0, minY: 0, maxX: 100, maxY: 100 }),
163
+ getZoomBaseScale: () => 0,
164
+ getBackground: () => null
165
+ })
166
+ await wait(220)
167
+ const miniFirst = mini.getPaintStats()
168
+ check('鹰眼图线路层:首帧只铺固定范围里的线路(范围外那条被跳过)',
169
+ miniFirst.lineRasterDraws > 0 && miniFirst.linePathsDrawn === miniFirst.lineRasterDraws,
170
+ JSON.stringify(miniFirst))
171
+ mini.refresh()
172
+ await wait(100)
173
+ const miniRepeat = mini.getPaintStats()
174
+ check('鹰眼图线路层:同一份数据补画命中缓存(不再重复投影 / stroke)',
175
+ miniRepeat.lineRasterHits > miniFirst.lineRasterHits && miniRepeat.lineRasterDraws === miniFirst.lineRasterDraws,
176
+ JSON.stringify({ first: miniFirst, repeat: miniRepeat }))
177
+ mini.setZoom(2)
178
+ await wait(100)
179
+ const miniZoomed = mini.getPaintStats()
180
+ check('鹰眼图线路层:小图自身缩放变化时只重铺线路层一次(范围外线路仍被裁剪)',
181
+ miniZoomed.lineRasterDraws === miniRepeat.lineRasterDraws + 1 && miniZoomed.linePathsDrawn === miniRepeat.linePathsDrawn,
182
+ JSON.stringify({ repeat: miniRepeat, zoomed: miniZoomed }))
183
+ mini.destroy()
184
+ miniHost.remove()
185
+ // 节点抗锯齿:单独一个无底图 / 无线路的小图,边缘应出现 0 < alpha < 255 的覆盖率像素。
186
+ const aaHost = document.createElement('div')
187
+ aaHost.style.cssText = 'position:absolute;left:-9999px;width:180px;height:120px'
188
+ document.body.append(aaHost)
189
+ const aaData = { points: [{ x: 50, y: 50, size: 5.5, color: '#ff0000' }], lines: [] }
190
+ const aa = createMinimap(aaHost, {
191
+ enabled: true,
192
+ width: 180,
193
+ height: 120,
194
+ background: null,
195
+ extent: { minX: 0, minY: 0, maxX: 100, maxY: 100 },
196
+ maxPixelRatio: 1
197
+ }, {
198
+ getData: () => aaData,
199
+ getCamera: () => ({ center: [50, 50], scale: 1 }),
200
+ getViewport: () => ({ width: 180, height: 120, pixelRatio: 1, displayScale: 1 }),
201
+ getFallbackBounds: () => ({ minX: 0, minY: 0, maxX: 100, maxY: 100 }),
202
+ getZoomBaseScale: () => 0,
203
+ getBackground: () => null
204
+ })
205
+ await wait(180)
206
+ const aaCanvas = aaHost.querySelector('canvas')
207
+ const aaPixels = aaCanvas.getContext('2d').getImageData(0, 0, aaCanvas.width, aaCanvas.height).data
208
+ let aaSolid = 0
209
+ let aaSoft = 0
210
+ for (let offset = 3; offset < aaPixels.length; offset += 4) {
211
+ if (aaPixels[offset] === 255) {
212
+ aaSolid += 1
213
+ }
214
+ else if (aaPixels[offset] > 0) {
215
+ aaSoft += 1
216
+ }
217
+ }
218
+ check('鹰眼图:节点边缘按像素覆盖率抗锯齿(既有实心像素也有半透明边缘)',
219
+ aaSolid > 0 && aaSoft > 0,
220
+ `实心 ${aaSolid} px / 半透明 ${aaSoft} px`)
221
+ aa.destroy()
222
+ aaHost.remove()
223
+ // 2. 每个功能演示都切一遍:画布起得来、说明与读数有东西
140
224
  const demoSpecs = [
141
225
  { tab: '基础视图', id: 'basic' },
142
226
  { tab: '标签交互', id: 'labels' },
@@ -153,6 +237,7 @@
153
237
  { tab: '扩展能力', id: 'extension' },
154
238
  { tab: '编辑与保存', id: 'backend' },
155
239
  { tab: '性能与质量', id: 'perf' },
240
+ { tab: '点聚合', id: 'cluster' },
156
241
  { tab: '动画效果', id: 'effects' },
157
242
  { tab: '扩展图形', id: 'graphic' }
158
243
  ]
@@ -162,10 +247,82 @@
162
247
  const panel = document.querySelector('.dv-demo .dv-panel')
163
248
  const active = document.querySelector('.demo-shell__tab--active')?.textContent.trim() ?? ''
164
249
  check(`切到「${spec.tab}」:画布与读数面板都起来了`, Boolean(ready) && Boolean(panel) && active === spec.tab, `${ready ? 'canvas 就绪' : '没有 canvas'} / ${panel ? `${panel.querySelectorAll('.dv-group').length} 组读数` : '没有面板'} / 当前页签 ${active}`)
165
- await wait(260)
250
+ await wait(260)
166
251
  check(`「${spec.tab}」把演示 id 写进地址栏(?demo=${spec.id})`, location.search.includes(`demo=${spec.id}`), location.search)
167
252
  }
168
253
 
254
+ // 2.0 点聚合:控件齐全、聚合圆点击只发 clusterClick,maxZoom 展开后又回来
255
+ if (await clickTab('点聚合')) {
256
+ const ready = await canvasReady()
257
+ await wait(500)
258
+ const demo = document.querySelector('.dv-demo')
259
+ const setup = demo?.__vueParentComponent?.setupState
260
+ const view = await until(() => setup?.view, 8000)
261
+ const text = key => document.querySelector(`[data-demo=${key}]`)?.textContent.trim() ?? ''
262
+ const canvas = demo?.querySelector('canvas.gis-view__canvas') ?? demo?.querySelector('canvas')
263
+ const toggle = document.querySelector('[data-demo=cluster-toggle]')
264
+ const radiusInput = document.querySelector('[data-demo=cluster-radius]')
265
+ const minPointsSelect = document.querySelector('[data-demo=cluster-min-points]')
266
+ const maxZoomSelect = document.querySelector('[data-demo=cluster-max-zoom]')
267
+ const countSelect = document.querySelector('[data-demo=cluster-count]')
268
+ const anchor = await until(() => {
269
+ const node = document.querySelector('[data-demo=cluster-first-client]')
270
+ return node?.dataset.clientX !== '' && node?.dataset.clientY !== '' ? node : null
271
+ }, 8000)
272
+ if (ready && view && canvas && toggle && radiusInput && minPointsSelect && maxZoomSelect && countSelect) {
273
+ const initialClusters = view.getClusters().filter(item => item.count > 1)
274
+ check('点聚合:配置控件齐全,初始数据已聚合并给出读数',
275
+ initialClusters.length > 0 && text('cluster-stats').includes('原始点') && text('cluster-stats').includes('个聚合圆'),
276
+ `${initialClusters.length} 个多成员圆 / ${text('cluster-stats')}`)
277
+ const selectedBefore = view.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
278
+ const clientX = Number(anchor?.dataset.clientX)
279
+ const clientY = Number(anchor?.dataset.clientY)
280
+ if (Number.isFinite(clientX) && Number.isFinite(clientY)) {
281
+ tap(canvas, { clientX, clientY })
282
+ await wait(180)
283
+ }
284
+ const selectedAfter = view.getSelections().map(entry => entry.item?.id ?? entry.index).join(',')
285
+ check('点聚合:点中聚合圆只增加 clusterClick,普通 click 为 0、选中集不变',
286
+ text('cluster-click-count').includes('0 次普通点击 / 1 次聚合点击') && text('cluster-event').includes('clusterClick') && selectedBefore === selectedAfter,
287
+ `${text('cluster-click-count')} / ${text('cluster-event')} / 选中 ${selectedBefore || '空'} → ${selectedAfter || '空'}`)
288
+ maxZoomSelect.value = '2'
289
+ maxZoomSelect.dispatchEvent(new Event('change', { bubbles: true }))
290
+ await wait(220)
291
+ view.zoomBy(3)
292
+ await wait(380)
293
+ const zoomed = view.getPointCluster()
294
+ check('点聚合:maxZoom 控件生效,放大超过阈值后展开原始点',
295
+ zoomed?.active === false && view.getStats().clusterCount === 0,
296
+ `active=${zoomed?.active} clusterCount=${view.getStats().clusterCount} maxZoom=${zoomed?.maxZoom}`)
297
+ document.querySelector('[data-demo=cluster-fit]')?.click()
298
+ await wait(420)
299
+ check('点聚合:适应全部后回到聚合档,聚合圆重新出现',
300
+ view.getPointCluster()?.active === true && view.getStats().clusterCount > 0,
301
+ `active=${view.getPointCluster()?.active} clusterCount=${view.getStats().clusterCount}`)
302
+ radiusInput.value = '24'
303
+ radiusInput.dispatchEvent(new Event('input', { bubbles: true }))
304
+ radiusInput.dispatchEvent(new Event('change', { bubbles: true }))
305
+ await wait(220)
306
+ countSelect.value = '2000'
307
+ countSelect.dispatchEvent(new Event('change', { bubbles: true }))
308
+ document.querySelector('[data-demo=cluster-rebuild]')?.click()
309
+ const rebuilt = await until(() => view.getData().points.length === 2000, 8000)
310
+ await wait(260)
311
+ check('点聚合:半径可调、重建到 2000 点后统计跟着换',
312
+ Boolean(rebuilt) && view.getPointCluster()?.radius === 24 && view.getStats().pointCount === 2000 && view.getStats().clusterCount > 0,
313
+ `radius=${view.getPointCluster()?.radius} pointCount=${view.getStats().pointCount} clusterCount=${view.getStats().clusterCount}`)
314
+ toggle.checked = false
315
+ toggle.dispatchEvent(new Event('change', { bubbles: true }))
316
+ await wait(300)
317
+ check('点聚合:关掉开关后原始点恢复、聚合统计归零',
318
+ view.getPointCluster()?.enabled === false && view.getStats().clusterCount === 0 && view.getStats().clusterPointCount === 0,
319
+ JSON.stringify({ cluster: view.getPointCluster(), stats: { count: view.getStats().clusterCount, points: view.getStats().clusterPointCount } }))
320
+ }
321
+ else {
322
+ check('点聚合:演示页、实例与六个控件都起来了', false, ready ? '缺少 demo / setupState.view / 控件' : '没有 canvas')
323
+ }
324
+ }
325
+
169
326
  // 2.0 动态装饰器:吸附方式与位置来源正交,两种吸附方式都能选三种位置
170
327
  if (await clickTab('动态装饰器')) {
171
328
  await canvasReady()
@@ -494,10 +651,16 @@
494
651
  click('search-filter-preset-owner')
495
652
  const ownerReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
496
653
  check('搜索与筛选:fields=[props.owner] 能命中嵌套业务字段', Boolean(ownerReady) && readout('search-filter-meta').includes('truncated=否'), `${readout('search-filter-summary')} / ${readout('search-filter-meta')}`)
497
- click('search-filter-preset-channel')
498
- const channelReady = await until(() => readout('search-filter-summary').includes('通道 1'), 4000)
499
- check('搜索与筛选:scope=channels 只把通道放进命中', Boolean(channelReady) && readout('search-filter-summary').includes('节点 0') && readout('search-filter-summary').includes('线路 0'), readout('search-filter-summary'))
500
- click('search-filter-preset-symbol')
654
+ click('search-filter-preset-channel')
655
+ const channelReady = await until(() => readout('search-filter-summary').includes('通道 1'), 4000)
656
+ check('搜索与筛选:scope=channels 只把通道放进命中', Boolean(channelReady) && readout('search-filter-summary').includes('节点 0') && readout('search-filter-summary').includes('线路 0'), readout('search-filter-summary'))
657
+ click('search-filter-preset-group')
658
+ const groupReady = await until(() => readout('search-filter-summary').includes('分组 1'), 4000)
659
+ const groupHit = view.search({ keyword: 'channel-a', scope: 'groups', limit: 10 }).groups[0]
660
+ view.focusOn(groupHit)
661
+ view.selectFeatures(groupHit)
662
+ check('搜索与筛选:scope=groups 返回通道分组,并可直接聚焦 / 选中分组命中', Boolean(groupReady) && readout('search-filter-summary').includes('节点 0') && readout('search-filter-summary').includes('通道 0') && groupHit?.kind === 'group' && view.getSelections().length > 0, `${readout('search-filter-summary')} / ${JSON.stringify(groupHit)}`)
663
+ click('search-filter-preset-symbol')
501
664
  const symbolReady = await until(() => readout('search-filter-summary').includes('节点 8'), 4000)
502
665
  check('搜索与筛选:显式 fields=[symbol] 能搜默认跳过的符号字段', Boolean(symbolReady) && readout('search-filter-hit-0').includes('symbol = square'), `${readout('search-filter-summary')} / ${readout('search-filter-hit-0')}`)
503
666
  document.querySelector('[data-demo=search-filter-worker]')?.click()
@@ -829,7 +992,7 @@
829
992
  }
830
993
  }
831
994
 
832
- // 2.0c 扩展能力:五个扩展点各开一次,读数 / 徽标 / 导出结果与 WebGL 层都要跟着变
995
+ // 2.0c 扩展能力:六个扩展点各开一次,读数 / 徽标 / 导出结果 / WebGL 层与命令表都要跟着变
833
996
  if (await clickTab('扩展能力')) {
834
997
  const ready = await canvasReady()
835
998
  await wait(220)
@@ -856,6 +1019,17 @@
856
1019
  click('extension-plugin')
857
1020
  const badgeRemoved = await until(() => !document.querySelector('.demo-extension-badge'), 4000)
858
1021
  check('扩展能力:卸载函数执行 destroy,徽标与读数一起撤回', Boolean(badgeRemoved) && readout().includes('运行时插件 未挂载'), readout())
1022
+ click('extension-command-register')
1023
+ const commandReady = await until(() => setup.commandRegistered === true && view?.listCommands().includes('focusHighestLoad') && readout().includes('focusHighestLoad 已注册'), 4000)
1024
+ check('扩展能力:registerCommand 把业务动作注册进当前实例的命令表', Boolean(commandReady), readout())
1025
+ click('extension-command-run')
1026
+ const commandText = () => document.querySelector('[data-demo=extension-command-readout]')?.textContent.trim() ?? ''
1027
+ const commandRan = await until(() => commandText().includes('ok: true') && commandText().includes('focusHighestLoad') === false && view?.getSelectionData().count === 1, 4000)
1028
+ check('扩展能力:executeCommand 执行 handler、返回业务值并顺带选中定位目标', Boolean(commandRan) && commandText().includes('selected'), commandText())
1029
+ click('extension-command-register')
1030
+ const commandRemoved = await until(() => setup.commandRegistered === false && !view?.listCommands().includes('focusHighestLoad') && readout().includes('实例命令 未注册'), 4000)
1031
+ const missingCommand = view?.executeCommand('focusHighestLoad', {})
1032
+ check('扩展能力:命令卸载后从 listCommands 消失,再执行返回 ok: false', Boolean(commandRemoved) && missingCommand?.ok === false, commandText())
859
1033
  click('extension-export')
860
1034
  const exported = await until(() => exportText().includes('gis-view.extension-summary.v1') && exportText().includes('highLoadStations'), 4000)
861
1035
  let summary = null
@@ -1546,7 +1720,7 @@
1546
1720
  }
1547
1721
  }
1548
1722
  const channelMenuNode = channelDemo.querySelector('.dv-channel-menu')
1549
- check('通道与数据:右键通道回线弹出交互框(features.channelMenu)', Boolean(menuPayload) && Boolean(channelMenuNode) && channelView.getOptions().features?.channelMenu === true, menuPayload ? '通道 ' + menuPayload.channelId + ' · 命中回线 ' + menuPayload.lineId + ' · 共 ' + menuPayload.groupCount + ' 组' : '没弹出')
1723
+ check('通道与数据:右键通道回线弹出交互框(onContextMenu.channelContext + features.contextMenu)', Boolean(menuPayload) && Boolean(channelMenuNode) && channelView.getFeatures().contextMenu === true, menuPayload ? '通道 ' + menuPayload.channelId + ' · 命中回线 ' + menuPayload.lineId + ' · 共 ' + menuPayload.groupCount + ' 组' : '没弹出')
1550
1724
  const groupsBeforeSplit = channelView.getChannelGroups(menuPayload?.channelId).length
1551
1725
  const channelAction = text => [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].find(node => node.textContent.trim() === text)
1552
1726
  check('通道与数据:交互框列出 6 个 runChannelAction 动作(含取消该分组选中),拐点编辑已开(waypointEditable)', channelDemo.querySelectorAll('.dv-channel-menu__actions button').length === 6 && Boolean(channelAction('加拐点')) && !channelAction('加拐点').disabled && Boolean(channelAction('取消该分组选中')), [...channelDemo.querySelectorAll('.dv-channel-menu__actions button')].map(node => node.textContent.trim() + (node.disabled ? '(灰)' : '')).join(' / '))
@@ -1590,10 +1764,10 @@
1590
1764
  // getChannelGroups 每一项只给 lineIds(lineCount 是 getChannelContext().group 的字段),回线数按 lineIds.length 算
1591
1765
  const targetLineCount = targetGroup?.lineIds?.length ?? 0
1592
1766
  check('通道与数据:点分组 = setCurrentChannelGroup + 选中它的回线', Boolean(targetGroup) && switchedContext?.groupId === targetGroup.id && channelView.getSelections().length === targetLineCount, switchedContext ? '当前「' + switchedContext.groupId + '」/ 选中 ' + channelView.getSelections().length + ' 回线(目标「' + (targetGroup?.id ?? '—') + '」' + targetLineCount + ' 回)' : '没有上下文')
1593
- // 右键落到节点上:通道回调收到 null,框自己收掉
1594
- channelRightClick(fromScreen)
1595
- await wait(200)
1596
- check('通道与数据:右键落到节点上时回调收到 null,框收掉', !channelDemo.querySelector('.dv-channel-menu') && channelSetup.channelMenu === null, '')
1767
+ // 右键落到节点上:onContextMenu 不带 channelContext,框自己收掉
1768
+ channelRightClick(fromScreen)
1769
+ await wait(200)
1770
+ check('通道与数据:右键落到节点上时 channelContext 为空,框收掉', !channelDemo.querySelector('.dv-channel-menu') && channelSetup.channelMenu === null, '')
1597
1771
  // 再右键回回线上、按 Esc 也能收
1598
1772
  channelRightClick(menuScreen)
1599
1773
  const reopenedMenu = await until(() => channelDemo.querySelector('.dv-channel-menu'), 5000)
@@ -2009,19 +2183,80 @@
2009
2183
  else {
2010
2184
  check('编辑与保存:画布与视图都起来了', false, '没等到 canvas 或 view')
2011
2185
  }
2012
- // 2.9 综合示例的「实例方法速查」面板:整份清单、搜索、点一条填表达式、求值、options 清单
2013
- if (await clickTab('综合')) {
2014
- const ready = await canvasReady()
2015
- // 面板在弹层里(v-show):先切到「开关 · 历史」页签,再点出「实例方法速查」那一块
2016
- ;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('开关'))?.click()
2186
+ // 2.9 综合示例的「实例方法速查」面板:整份清单、搜索、点一条填表达式、求值、options 清单
2187
+ if (await clickTab('综合')) {
2188
+ const ready = await canvasReady()
2189
+ // 级联高亮与合并样式:综合示例的「交互」面板里现场开关、点画布看读数,并确认合并样式不是额外 API
2190
+ const cascadeDemo = document.querySelector('.demo-view')
2191
+ const cascadeSetup = cascadeDemo?.__vueParentComponent?.setupState
2192
+ const cascadeView = await until(() => cascadeSetup?.viewRef, 8000)
2193
+ const cascadeCanvas = cascadeDemo?.querySelector('canvas.gis-view__canvas') ?? cascadeDemo?.querySelector('canvas')
2194
+ if (ready && cascadeView && cascadeCanvas) {
2195
+ ;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('视图 · 交互'))?.click()
2196
+ await wait(80)
2197
+ ;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('交互开关与快捷键'))?.click()
2198
+ await wait(100)
2199
+ const cascadeToggle = document.querySelector('[data-demo=cascade-highlight-enabled]')
2200
+ const cascadeScope = document.querySelector('[data-demo=cascade-highlight-scope]')
2201
+ const cascadeReadoutNode = document.querySelector('[data-demo=cascade-highlight-readout]')
2202
+ check('级联高亮 demo:交互面板有开关、范围下拉与读数,默认开启且范围为节点 + 线路',
2203
+ Boolean(cascadeToggle && cascadeScope && cascadeReadoutNode) && cascadeToggle.checked && cascadeScope.value === 'both' && cascadeReadoutNode.textContent.includes('级联高亮'),
2204
+ `${cascadeToggle?.checked} / ${cascadeScope?.value} / ${cascadeReadoutNode?.textContent.trim() ?? '没有读数'}`)
2205
+ const cascadeData = cascadeView.getData()
2206
+ const cascadeLine = cascadeData.lines.find(line => line.from !== undefined && line.to !== undefined)
2207
+ let cascadeTarget = cascadeData.points.find(point => String(point.id) === String(cascadeLine?.from))
2208
+ if (!cascadeTarget && Array.isArray(cascadeLine?.path) && cascadeLine.path.length >= 2) {
2209
+ const [x, y] = cascadeLine.path[0]
2210
+ cascadeTarget = cascadeData.points.find(point => Math.abs(Number(point.x) - Number(x)) < 1e-9 && Math.abs(Number(point.y) - Number(y)) < 1e-9)
2211
+ }
2212
+ const cascadePoint = cascadeTarget ? cascadeView.getPoint(cascadeTarget.id) : null
2213
+ if (cascadePoint?.screen) {
2214
+ const cascadeRect = cascadeCanvas.getBoundingClientRect()
2215
+ tap(cascadeCanvas, { clientX: cascadeRect.left + cascadePoint.screen[0], clientY: cascadeRect.top + cascadePoint.screen[1] })
2216
+ await wait(220)
2217
+ }
2218
+ const cascadeInfo = cascadeView.getCascadeHighlight()
2219
+ check('级联高亮 demo:点节点后画布进入级联态,面板读数给出节点 / 线路条数',
2220
+ cascadeInfo?.origin?.target === 'point' && cascadeInfo.pointIndexes.length >= 1 && cascadeInfo.lineIndexes.length >= 1 && cascadeReadoutNode.textContent.includes('不改选中集'),
2221
+ JSON.stringify({ origin: cascadeInfo?.origin?.item?.id, points: cascadeInfo?.pointIndexes, lines: cascadeInfo?.lineIndexes, readout: cascadeReadoutNode.textContent.trim() }))
2222
+ cascadeScope.value = 'points'
2223
+ cascadeScope.dispatchEvent(new Event('change', { bubbles: true }))
2224
+ await wait(140)
2225
+ const cascadePointsOnly = cascadeView.getCascadeHighlight()
2226
+ check('级联高亮 demo:范围切到 points 后只亮节点,面板与 getOptions 同步',
2227
+ cascadeView.getOptions().cascadeHighlight?.scope === 'points' && cascadePointsOnly?.scope === 'points' && cascadePointsOnly.lineIndexes.length === 0,
2228
+ JSON.stringify({ scope: cascadePointsOnly?.scope, lines: cascadePointsOnly?.lineIndexes, option: cascadeView.getOptions().cascadeHighlight?.scope }))
2229
+ cascadeToggle.click()
2230
+ await wait(140)
2231
+ check('级联高亮 demo:关掉开关后 getCascadeHighlight() 归零,读数写明已关闭',
2232
+ cascadeView.getOptions().cascadeHighlight === false && cascadeView.getCascadeHighlight() === null && cascadeReadoutNode.textContent.includes('关闭'),
2233
+ `${cascadeView.getOptions().cascadeHighlight} / ${cascadeView.getCascadeHighlight()} / ${cascadeReadoutNode.textContent.trim()}`)
2234
+ // 还原成默认开 + both,别把后面的综合示例断言带进一个关掉的状态
2235
+ cascadeScope.value = 'both'
2236
+ cascadeScope.dispatchEvent(new Event('change', { bubbles: true }))
2237
+ cascadeToggle.click()
2238
+ await wait(100)
2239
+
2240
+ ;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('复制与合并'))?.click()
2241
+ await wait(100)
2242
+ const mergeHint = document.querySelector('[data-demo=merge-style-hint]')
2243
+ check('合并样式 demo:读数明确说明它是最终 size / symbol,并给出规则与改写入口',
2244
+ Boolean(mergeHint) && mergeHint.textContent.includes('合并样式') && mergeHint.textContent.includes('不是一项额外配置') && mergeHint.textContent.includes('尺寸取双方较大值') && mergeHint.textContent.includes('merge.createItem'),
2245
+ mergeHint?.textContent.trim() ?? '没有合并样式读数')
2246
+ }
2247
+ else {
2248
+ check('级联高亮 demo:画布与视图都起来了', false, ready ? '没有 setupState.viewRef' : '没有 canvas')
2249
+ }
2250
+ // 面板在弹层里(v-show):先切到「开关 · 历史」页签,再点出「实例方法速查」那一块
2251
+ ;[...document.querySelectorAll('.demo-tabs button')].find(node => node.textContent.includes('开关'))?.click()
2017
2252
  await wait(80)
2018
2253
  ;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.includes('实例方法速查'))?.click()
2019
2254
  await wait(150)
2020
2255
  const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
2021
2256
  const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
2022
- check('实例方法速查:整份成员清单按分组列全(16 组 / 249 个成员)',
2023
- Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 249,
2024
- `${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
2257
+ check('实例方法速查:整份成员清单按分组列全(16 组 / 256 个成员)',
2258
+ Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 256,
2259
+ `${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
2025
2260
  const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
2026
2261
  const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
2027
2262
  check('实例方法速查:本轮新增的三个方法都在清单里(registerSymbol / unregisterSymbol / listSymbols)',
@@ -2035,7 +2270,7 @@
2035
2270
  check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
2036
2271
  roundNames1597.every(name => apiNames.includes(name)),
2037
2272
  roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
2038
- check('实例方法速查:options 折叠清单列全 92 个键', optionItems().length === 92, `${optionItems().length} 个键`)
2273
+ check('实例方法速查:options 折叠清单列全 96 个键', optionItems().length === 96, `${optionItems().length} 个键`)
2039
2274
  const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
2040
2275
  apiSearch.value = 'focusOn'
2041
2276
  apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
@@ -2120,7 +2355,7 @@
2120
2355
  const openChannelPanel = async () => {
2121
2356
  ;[...document.querySelectorAll('.demo-panel-buttons button')].find(node => node.textContent.trim() === '通道与拐点')?.click()
2122
2357
  await wait(140)
2123
- return [...document.querySelectorAll('.demo-popover button')].find(node => node.textContent.trim() === '新建通道(点两个节点)') ?? null
2358
+ return [...document.querySelectorAll('.demo-panel-inline button')].find(node => node.textContent.trim() === '新建通道(点两个节点)') ?? null
2124
2359
  }
2125
2360
  let beginDraftButton = await openChannelPanel()
2126
2361
  if (!beginDraftButton) {
@@ -2208,10 +2443,152 @@
2208
2443
  check('新增通道:综合示例的实例拿得到(setupState.viewRef)', false, '没有 setupState.viewRef')
2209
2444
  }
2210
2445
  }
2211
- else {
2212
- check('实例方法速查:综合示例的页签在', false, '没找到「综合」页签')
2213
- }
2214
- // 2.10 假后端契约:头 / body 必须一致,同 opId 换 body 必须报幂等冲突
2446
+ else {
2447
+ check('实例方法速查:综合示例的页签在', false, '没找到「综合」页签')
2448
+ }
2449
+ // 2.9b 接口能力:级联三种触发、通道 / 分组 / 拐点选中、lineFollowsNode 与 focusOn
2450
+ if (await clickTab('接口能力')) {
2451
+ const ready = await canvasReady()
2452
+ const demo = await until(() => [...document.querySelectorAll('.dv-demo')].find(node => node.textContent.includes('级联高亮三种触发')), 8000)
2453
+ const setup = demo?.__vueParentComponent?.setupState
2454
+ const apiView = await until(() => setup?.view?.getSelectionData ? setup.view : null, 8000)
2455
+ if (ready && apiView) {
2456
+ await until(() => apiView.listPoints().length > 0, 5000)
2457
+ const triggerClick = document.querySelector('[data-demo=cascade-trigger-click]')
2458
+ const triggerMulti = document.querySelector('[data-demo=cascade-trigger-multi]')
2459
+ const triggerBox = document.querySelector('[data-demo=cascade-trigger-box]')
2460
+ check('接口能力 demo:三种级联触发都有控件,默认全部打开并写进 getOptions',
2461
+ Boolean(triggerClick && triggerMulti && triggerBox) && triggerClick.checked && triggerMulti.checked && triggerBox.checked &&
2462
+ apiView.getOptions().cascadeHighlight?.triggerOnClick === true &&
2463
+ apiView.getOptions().cascadeHighlight?.triggerOnMultiSelect === true &&
2464
+ apiView.getOptions().cascadeHighlight?.triggerOnBoxSelect === true,
2465
+ `${triggerClick?.checked} / ${triggerMulti?.checked} / ${triggerBox?.checked} · ${JSON.stringify(apiView.getOptions().cascadeHighlight)}`)
2466
+ triggerBox.click()
2467
+ await wait(100)
2468
+ check('接口能力 demo:triggerOnBoxSelect 可以单独关掉,不影响另外两路',
2469
+ apiView.getOptions().cascadeHighlight?.triggerOnBoxSelect === false &&
2470
+ apiView.getOptions().cascadeHighlight?.triggerOnClick === true &&
2471
+ apiView.getOptions().cascadeHighlight?.triggerOnMultiSelect === true,
2472
+ JSON.stringify(apiView.getOptions().cascadeHighlight))
2473
+ triggerBox.click()
2474
+ document.querySelector('[data-demo=select-channel]')?.click()
2475
+ await wait(100)
2476
+ const channelSelection = apiView.getSelectionData()
2477
+ check('接口能力 demo:selectFeatures 显式选中整条通道,getSelectionData 读回通道计数',
2478
+ channelSelection.count > 0 && channelSelection.channelCount > 0 && channelSelection.lineCount > 0,
2479
+ JSON.stringify({ count: channelSelection.count, channels: channelSelection.channelCount, lines: channelSelection.lineCount }))
2480
+ document.querySelector('[data-demo=select-group]')?.click()
2481
+ await wait(100)
2482
+ const groupSelection = apiView.getSelectionData()
2483
+ check('接口能力 demo:selectFeatures 显式选中分组,读回只落在这一组的回线上',
2484
+ groupSelection.count > 0 && groupSelection.channelCount > 0 && groupSelection.lineCount > 0,
2485
+ JSON.stringify({ count: groupSelection.count, channels: groupSelection.channelCount, lines: groupSelection.lineCount }))
2486
+ document.querySelector('[data-demo=select-waypoint]')?.click()
2487
+ await wait(120)
2488
+ const waypointSelection = apiView.getSelectionData()
2489
+ const waypointLine = apiView.listLines().find(line => String(line.item?.id) === 'line-0')
2490
+ check('接口能力 demo:selectWaypoint 补出中间点并同时选中所在线路',
2491
+ Boolean(waypointLine) && apiView.getWaypoints(waypointLine.index).length >= 1 && waypointSelection.lineCount >= 1,
2492
+ JSON.stringify({ waypoints: waypointLine ? apiView.getWaypoints(waypointLine.index).length : -1, lines: waypointSelection.lineCount }))
2493
+ const linesBeforeHide = apiView.listLines({ visibleOnly: true }).length
2494
+ const toggleEndpoint = document.querySelector('[data-demo=toggle-endpoint]')
2495
+ const lineFollows = document.querySelector('[data-demo=line-follows-node]')
2496
+ toggleEndpoint?.click()
2497
+ await wait(260)
2498
+ const linesWhileFollowing = apiView.listLines({ visibleOnly: true }).length
2499
+ lineFollows?.click()
2500
+ await wait(260)
2501
+ const linesWithoutFollowing = apiView.listLines({ visibleOnly: true }).length
2502
+ check('接口能力 demo:lineFollowsNode 开时隐藏端点会带走绑定线路,关掉后线路保留',
2503
+ linesWhileFollowing < linesBeforeHide && linesWithoutFollowing > linesWhileFollowing && apiView.getOptions().lineFollowsNode === false,
2504
+ `线路 ${linesBeforeHide} → 跟随 ${linesWhileFollowing} → 不跟随 ${linesWithoutFollowing}`)
2505
+ lineFollows?.click()
2506
+ toggleEndpoint?.click()
2507
+ await wait(260)
2508
+ const cameraBefore = apiView.getCamera()
2509
+ document.querySelector('[data-demo=focus-channel]')?.click()
2510
+ await wait(120)
2511
+ const cameraAfter = apiView.getCamera()
2512
+ check('接口能力 demo:focusOn({ type: channel }, { zoom: true }) 显式走包围盒适配并更新相机',
2513
+ cameraAfter.scale > 0 && (cameraAfter.scale !== cameraBefore.scale || cameraAfter.center[0] !== cameraBefore.center[0] || cameraAfter.center[1] !== cameraBefore.center[1]),
2514
+ `scale ${cameraBefore.scale} → ${cameraAfter.scale} · center ${JSON.stringify(cameraBefore.center)} → ${JSON.stringify(cameraAfter.center)}`)
2515
+ document.querySelector('[data-demo=style-apis]')?.click()
2516
+ await wait(80)
2517
+ const styledOptions = apiView.getOptions()
2518
+ const styleReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2519
+ check('接口能力 demo:updatePointStyle / updateLineStyle 字段级合并并给出读数',
2520
+ styledOptions.pointStyle?.color === '#fde68a' && styledOptions.lineStyle?.color === '#fbbf24' && styleReadout.includes('updatePointStyle / updateLineStyle'),
2521
+ JSON.stringify({ point: styledOptions.pointStyle, line: styledOptions.lineStyle }))
2522
+ document.querySelector('[data-demo=style-apis]')?.click()
2523
+ await wait(80)
2524
+ document.querySelector('[data-demo=point-decoration-apis]')?.click()
2525
+ document.querySelector('[data-demo=line-decoration-apis]')?.click()
2526
+ await wait(120)
2527
+ const decorationReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2528
+ check('接口能力 demo:节点 / 线路装饰的 add、update、remove 三个入口都能收尾',
2529
+ decorationReadout.includes('线路装饰 CRUD') && decorationReadout.includes('updateLineDecoration size=20') && apiView.getPointDecorations('node-0').length === 0 && apiView.getLineDecorations('line-0').length === 0,
2530
+ decorationReadout.slice(-160))
2531
+ document.querySelector('[data-demo=channel-line-apis]')?.click()
2532
+ await wait(120)
2533
+ const channelApiReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2534
+ check('接口能力 demo:通道线宽、getChannelBetween、线路对账与 removeLines 同屏跑通',
2535
+ channelApiReadout.includes('setChannelLineWidth=3.4→2.6') && channelApiReadout.includes('getChangedLines=1 条') && channelApiReadout.includes('removeLines=ok') && !apiView.getData().lines.some(line => String(line.id) === 'api-temp-line'),
2536
+ channelApiReadout.slice(-180))
2537
+ document.querySelector('[data-demo=geo-background-apis]')?.click()
2538
+ await wait(160)
2539
+ const geoReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2540
+ check('接口能力 demo:updateBackground 后能读 getBackgroundLabels / getGeoProjection / mapDataToGeoJson',
2541
+ Boolean(apiView.getGeoProjection()) && (apiView.getBackgroundLabels()?.length ?? 0) > 0 && geoReadout.includes('mapDataToGeoJson=') && geoReadout.includes('节点'),
2542
+ geoReadout.slice(-180))
2543
+ document.querySelector('[data-demo=geo-background-apis]')?.click()
2544
+ await wait(120)
2545
+ check('接口能力 demo:再次 updateBackground({}) 恢复透明底、投影读数回到 null', apiView.getGeoProjection() === null, String(apiView.getGeoProjection()))
2546
+ const pointsBeforeAdvanced = apiView.getData().points.length
2547
+ document.querySelector('[data-demo=history-apis]')?.click()
2548
+ await wait(120)
2549
+ const historyReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2550
+ check('接口能力 demo:undo / canRedo / redo、合并收尾、落点预览与 isLayoutReady 都有读数',
2551
+ historyReadout.includes('canRedo=true') && historyReadout.includes('isLayoutReady=') && historyReadout.includes('getDropPreview=point') && apiView.getDropPreview() === null,
2552
+ historyReadout.slice(-220))
2553
+ document.querySelector('[data-demo=copy-apis]')?.click()
2554
+ await wait(120)
2555
+ const copyReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2556
+ check('接口能力 demo:beginCopyPoint / updateCopyPoint / commitPointCopy 演示完会清理临时复制品',
2557
+ copyReadout.includes('updateCopyPoint=接口复制预览') && copyReadout.includes('commitPointCopy=ok') && copyReadout.includes('removePoints=ok') && apiView.getData().points.length === pointsBeforeAdvanced,
2558
+ copyReadout.slice(-180))
2559
+ document.querySelector('[data-demo=remove-apis]')?.click()
2560
+ await wait(120)
2561
+ const removeReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2562
+ check('接口能力 demo:requestRemove → getRemoveRequest → cancel,再走 removeSelected',
2563
+ removeReadout.includes('getRemoveRequest=pending') && removeReadout.includes('cancel=cleared') && removeReadout.includes('removeSelected=1 个节点') && !apiView.getData().points.some(point => String(point.id) === 'api-remove-node'),
2564
+ removeReadout.slice(-180))
2565
+ document.querySelector('[data-demo=once-api]')?.click()
2566
+ await wait(80)
2567
+ check('接口能力 demo:once 只触发一次', (document.querySelector('[data-demo=advanced-readout]')?.textContent ?? '').includes('只触发 1 次'), document.querySelector('[data-demo=advanced-readout]')?.textContent ?? '')
2568
+ document.querySelector('[data-demo=webgl-apis]')?.click()
2569
+ await wait(80)
2570
+ check('接口能力 demo:clearWebGLLayers 清空临时层', apiView.getWebGLLayers().length === 0 && (document.querySelector('[data-demo=advanced-readout]')?.textContent ?? '').includes('剩余 0 层'), document.querySelector('[data-demo=advanced-readout]')?.textContent ?? '')
2571
+ document.querySelector('[data-demo=registry-apis]')?.click()
2572
+ await wait(80)
2573
+ const registryReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2574
+ check('接口能力 demo:unregisterSymbol / unregisterOperator 注销临时注册项',
2575
+ registryReadout.includes('unregisterSymbol=true') && registryReadout.includes('unregisterOperator=true') && !apiView.listSymbols().includes('api-ring') && !apiView.listOperators().includes('api-even'),
2576
+ registryReadout.slice(-180))
2577
+ document.querySelector('[data-demo=initial-options]')?.click()
2578
+ await wait(60)
2579
+ const optionsReadout = document.querySelector('[data-demo=advanced-readout]')?.textContent ?? ''
2580
+ check('接口能力 demo:初始化 options 的 ariaLabel / labelRender / gridSize / fitOnInit 等能读回',
2581
+ optionsReadout.includes('ariaLabel=接口能力演示画布') && optionsReadout.includes('labelRender=dom') && optionsReadout.includes('gridSize=96') && optionsReadout.includes('fitOnInit=true') && optionsReadout.includes('progressiveInit=true'),
2582
+ optionsReadout.slice(-220))
2583
+ }
2584
+ else {
2585
+ check('接口能力 demo:画布与 view 都起来了', false, ready ? '没拿到 view' : '没等到 canvas')
2586
+ }
2587
+ }
2588
+ else {
2589
+ check('接口能力 demo:页签在', false, '没找到「接口能力」页签')
2590
+ }
2591
+ // 2.10 假后端契约:头 / body 必须一致,同 opId 换 body 必须报幂等冲突
2215
2592
  {
2216
2593
  const { createFakeBackend } = await import('../demos/shared/fakeBackend.js')
2217
2594
  const backend = createFakeBackend({ initialVersion: 12, latency: 0, prefix: 'contract' })
@@ -127,8 +127,10 @@
127
127
  check('文档中心:README 顶部有 4 条推荐阅读路径,3 篇文档加 API 页都指向真实目标', pathCards.length === 4 && pathDocs.length === 3 && pathDocs.every(file => files.includes(file)) && (apiPathCard?.getAttribute('href') ?? '').includes('api/index.html') && pathCards.every(card => card.querySelector('.doc-path__badge') && card.textContent.trim().length > 12), pathCards.map(card => card.querySelector('b')?.textContent?.trim()).join(' / '))
128
128
  check('文档中心:正文链接的解析(相对「当前那份文档」算,解析不到给 null)', api.resolve('docs/GisView.md', '../README.md') === 'GisView.md' && api.resolve('../从0到完整的实现.md', 'steps/README.md') === '从0到完整的实现.md' && api.resolve('../index.html', 'README.md') === null)
129
129
  const fixture = api.render('# 文档标题\n\n> 一句话摘要\n\n## 标题\n\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\n```js\nconst a = 1\n```\n\n- 一级\n - 二级\n\n> 正文引用\n\n行内 `code` 与 [链接](GisView.md)\n')
130
+ const tableFixture = api.render('| 字段 | 类型 |\n| --- | --- |\n| `type` | `\'image/png\' \\| \'image/jpeg\' \\| \'svg\'` |\n')
130
131
  const leadFixture = api.render('# 只有导语\n\n第一段就是全文导语。\n')
131
132
  check('文档中心:markdown 渲染器(标题 / 表格 / 带语言代码块 / 两级列表 / 引用 / 行内代码 / md 链接 / 目录)', fixture.html.includes('<table>') && fixture.html.includes('data-lang="js"') && (fixture.html.match(/<ul>/g) ?? []).length >= 2 && fixture.html.includes('<blockquote class="doc-summary">') && fixture.html.includes('<blockquote><p>正文引用</p></blockquote>') && fixture.html.includes('<code>code</code>') && fixture.html.includes('data-doc="GisView.md"') && fixture.toc.length === 1, `${fixture.html.length} 字符 / ${fixture.toc.length} 条目录`)
133
+ check('文档中心:表格里的反引号联合类型不被 `|` 拆列(转义竖线留在同一格)', (tableFixture.html.match(/<td>/g) ?? []).length === 2 && tableFixture.html.includes('<code>\'image/png\' | \'image/jpeg\' | \'svg\'</code>'), (tableFixture.html.match(/<td>[\s\S]*?<\/td>/g) ?? []).join(' / ').slice(0, 180))
132
134
  check('文档中心:H1 后的摘要 / 首段分别套导语卡片与导语段落', leadFixture.html.includes('<p class="doc-lead">第一段就是全文导语。</p>'), leadFixture.html.slice(0, 120))
133
135
  await api.load('数据格式.md')
134
136
  const meta2 = doc.querySelector('#content .meta')
@@ -518,10 +520,37 @@
518
520
  if (check('API 速查:挂上了 __gisApi 钩子(数据来自 api-index.js,没钩子多半是 file:// 打开的)', Boolean(apiHook))) {
519
521
  const apiDoc = apiFrame.contentDocument
520
522
  const summary = apiHook.summary
521
- check('API 速查:清单来自 api-index(249 个成员 / 92 个 options / 16 个分组)',
522
- summary.members === 249 && summary.options === 92 && summary.sections === 16,
523
+ check('API 速查:清单来自 api-index(256 个成员 / 253 个方法 / 96 个 options / 16 个分组)',
524
+ summary.members === 256 && summary.methods === 253 && summary.options === 96 && summary.sections === 16,
523
525
  JSON.stringify(summary))
526
+ const topJumps = [...apiDoc.querySelectorAll('.doc-jump')]
527
+ const jumpTargets = topJumps.map(node => `${node.dataset.docJump}:${node.getAttribute('href')}`)
528
+ check('API 速查:顶栏四个文档深链齐全,分别指到文档中心 / 实例方法 / 参数速查 / 逐方法示例',
529
+ jumpTargets.join('|') === 'center:../index.html#GisView.md|methods:../index.html#manual/16-实例方法总表.md|params:../index.html#manual/17-参数速查.md|examples:../index.html#manual/18-逐方法示例.md',
530
+ jumpTargets.join(' | '))
531
+ const groupLinks = [...apiDoc.querySelectorAll('.api-group > h2 .group-manual')]
532
+ check('API 速查:每个分组都有回手册的章节链接(16 组 + options)',
533
+ groupLinks.length === summary.sections + 1 && groupLinks.length === apiDoc.querySelectorAll('.api-group').length &&
534
+ groupLinks.every(node => node.dataset.manualGroup && decodeURIComponent(String(node.getAttribute('href'))).startsWith('../index.html#manual/')),
535
+ groupLinks.map(node => node.dataset.manualGroup).join(' / '))
524
536
  check('API 速查:左侧分组有当前项,滚动 / 点选时会跟着亮', apiHook.activeGroup() !== null, String(apiHook.activeGroup()))
537
+ const apiWindow = apiFrame.contentWindow
538
+ const elementPrototype = apiWindow.Element?.prototype
539
+ const originalScrollIntoView = elementPrototype?.scrollIntoView
540
+ let groupScrollCall = null
541
+ if (elementPrototype) {
542
+ elementPrototype.scrollIntoView = function (options) {
543
+ groupScrollCall = { options, target: this.id }
544
+ }
545
+ }
546
+ const secondGroupButton = apiDoc.querySelector('#side-list .side-item[data-group="1"]')
547
+ click(secondGroupButton)
548
+ if (elementPrototype) {
549
+ elementPrototype.scrollIntoView = originalScrollIntoView
550
+ }
551
+ check('API 速查:左侧分组跳转用即时滚动(behavior: auto),长页不会平滑滚好几秒',
552
+ Boolean(secondGroupButton) && groupScrollCall?.options?.behavior === 'auto' && groupScrollCall?.options?.block === 'start' && groupScrollCall?.target === 'group-1',
553
+ JSON.stringify(groupScrollCall))
525
554
  check('API 速查:分组都渲染出来(16 组 + options 一组)',
526
555
  apiDoc.querySelectorAll('.api-group').length === 17 && apiDoc.querySelectorAll('.api-group > h2 b').length === 17,
527
556
  `${apiDoc.querySelectorAll('.api-group').length} 组`)
@@ -530,8 +559,8 @@
530
559
  `${apiDoc.querySelectorAll('.api-member').length} 张卡片 / 属性 ${apiDoc.querySelectorAll('.api-member[data-kind="property"]').length} 张`)
531
560
  const noExample = [...apiDoc.querySelectorAll('.api-member')].filter(node => Number(node.dataset.examples) < 1)
532
561
  check('API 速查:每个成员都带至少一段示例(不出现空卡片)', noExample.length === 0,
533
- noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '249 张卡片都有示例')
534
- check('API 速查:92 个 options 都列全(名字 / 类型 / 默认值 / 说明四件套)',
562
+ noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '256 张卡片都有示例')
563
+ check('API 速查:96 个 options 都列全(名字 / 类型 / 默认值 / 说明四件套)',
535
564
  apiDoc.querySelectorAll('.api-option').length === summary.options && [...apiDoc.querySelectorAll('.api-option')].every(node => node.querySelector('.name code') && node.querySelector('.type') && node.querySelector('.def') && node.querySelector('.doc')),
536
565
  `${apiDoc.querySelectorAll('.api-option').length} 个`)
537
566
  const statCards = [...apiDoc.querySelectorAll('#stats .api-stat')]