gis-view-webgl2 0.1.7 → 0.1.9

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 (70) hide show
  1. package/README.md +14 -11
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +445 -241
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -498
  7. package/docs/GisView.md +1 -1
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +1 -1
  9. 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 +114 -478
  10. package/docs/demos/BasicDemo.vue +144 -27
  11. package/docs/demos/BatchAddDemo.vue +133 -7
  12. package/docs/demos/ChannelDemo.vue +61 -0
  13. package/docs/demos/ConnectDemo.vue +164 -33
  14. package/docs/demos/CopyMergeDemo.vue +144 -42
  15. package/docs/demos/EventDemo.vue +29 -24
  16. package/docs/demos/LabelDemo.vue +5 -5
  17. package/docs/demos/api-index.js +53 -41
  18. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
  19. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
  20. package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
  21. package/docs/demos/gis-view-demo/useGisViewDemo.js +31 -33
  22. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
  23. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
  24. 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 +8 -7
  25. package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
  26. package/docs/from0/07-/351/200/232/351/201/223/345/210/206/347/273/204/344/270/216/345/244/232/345/233/236/347/272/277.md +26 -10
  27. package/docs/index.html +136 -13
  28. package/docs/internals/15-04-/351/200/220/350/275/256/350/256/260/345/275/225-/347/203/255/345/212/233/345/233/276/344/270/216/346/200/247/350/203/275.md +10 -0
  29. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
  30. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
  31. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +23 -17
  32. package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +65 -0
  33. package/docs/manual/06-/346/225/260/346/215/256/344/270/216/345/234/272/346/231/257/346/233/264/346/226/260.md +9 -10
  34. 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 +100 -40
  35. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +11 -7
  36. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +66 -6
  37. 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 +226 -493
  38. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +28 -5
  39. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +46 -14
  40. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
  41. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +34 -29
  42. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +30 -22
  43. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +168 -81
  44. 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 +1 -1
  45. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +5 -3
  46. 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 +9 -6
  47. package/docs/steps/README.md +201 -120
  48. package/docs/steps/bench-scale.html +1 -1
  49. package/docs/steps/index.html +222 -6
  50. package/docs/steps/selftest-actions.html +26 -25
  51. package/docs/steps/selftest-data.html +1 -1
  52. package/docs/steps/selftest-demos.html +290 -16
  53. package/docs/steps/selftest-docs.html +17 -6
  54. package/docs/steps/selftest-features.html +594 -54
  55. package/docs/steps/selftest-full.html +3 -3
  56. package/docs/steps/selftest-labels.html +82 -1
  57. package/docs/steps/selftest-perf.html +62 -7
  58. package/docs/steps/selftest-select.html +71 -6
  59. package/docs/steps/step-05-pick.js +4 -8
  60. package/docs/steps/step-07-channels.js +3 -5
  61. package/docs/steps/step-08-labels.js +3 -3
  62. package/docs/steps/step-10-full.js +3 -3
  63. package/docs/steps/step-12-events.js +41 -24
  64. package/docs//344/273/2160/345/210/260/345/256/214/346/225/264/347/232/204/345/256/236/347/216/260.md +1 -0
  65. package/package.json +1 -1
  66. package/docs/ai/01-/350/265/267/346/272/220/344/270/216/350/277/231/344/273/275/346/261/207/346/200/273/347/232/204/347/224/261/346/235/245.md +0 -29
  67. package/docs/ai/02-/346/214/211/345/206/205/345/256/271/345/210/206/347/261/273-/350/277/231/344/270/200/350/267/257/351/203/275/345/234/250/346/224/271/344/273/200/344/271/210.md +0 -66
  68. package/docs/ai/03-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/200/357/274/211/357/274/232/351/200/211/345/236/213/351/200/232/351/201/223/344/270/216/346/240/267/345/274/217.md +0 -178
  69. package/docs/ai/04-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/272/214/357/274/211/357/274/232/344/272/244/344/272/222/346/200/247/350/203/275/344/270/216/346/225/260/346/215/256.md +0 -140
  70. package/docs/ai/05-/351/207/215/350/246/201/351/227/256/347/255/224/351/200/211/357/274/210/344/270/211/357/274/211/357/274/232/345/256/277/344/270/273/346/227/201/346/224/257/344/270/216/346/224/266/345/217/243.md +0 -43
@@ -268,6 +268,92 @@
268
268
  check(`「${spec.tab}」把演示 id 写进地址栏(?demo=${spec.id})`, location.search.includes(`demo=${spec.id}`), location.search)
269
269
  }
270
270
 
271
+ // 2.0.1 基础视图:比例读写与空闲检测都能从面板操作并回读
272
+ if (await clickTab('基础视图')) {
273
+ const ready = await canvasReady()
274
+ await wait(300)
275
+ const demo = document.querySelector('.dv-demo')
276
+ const setup = demo?.__vueParentComponent?.setupState
277
+ const view = await until(() => setup?.view, 8000)
278
+ const canvas = demo?.querySelector('canvas.gis-view__canvas') ?? demo?.querySelector('canvas')
279
+ const scaleInput = document.querySelector('[data-demo=basic-scale-input]')
280
+ const scaleSet = document.querySelector('[data-demo=basic-scale-set]')
281
+ const scaleGet = document.querySelector('[data-demo=basic-scale-get]')
282
+ const scaleRead = document.querySelector('[data-demo=basic-scale-read]')
283
+ const idleToggle = document.querySelector('[data-demo=basic-idle-toggle]')
284
+ const idleRead = document.querySelector('[data-demo=basic-idle-read]')
285
+ check('基础视图:比例与空闲面板控件齐全',
286
+ Boolean(ready && view && canvas && scaleInput && scaleSet && scaleGet && scaleRead && idleToggle && idleRead),
287
+ `${ready ? 'canvas 就绪' : '没有 canvas'} / view=${Boolean(view)} / 比例 ${[scaleInput, scaleSet, scaleGet, scaleRead].filter(Boolean).length}/4 / 空闲 ${[idleToggle, idleRead].filter(Boolean).length}/2`)
288
+ if (ready && view && canvas && scaleInput && scaleSet && scaleGet && scaleRead && idleToggle && idleRead) {
289
+ const targetScale = 96
290
+ scaleInput.value = String(targetScale)
291
+ scaleInput.dispatchEvent(new Event('input', { bubbles: true }))
292
+ scaleSet.click()
293
+ await wait(120)
294
+ check('基础视图:setScale() 改到输入值,getScale() 与面板读数同步',
295
+ Math.abs(view.getScale() - targetScale) < 0.01 && scaleRead.textContent.includes(targetScale.toFixed(2)),
296
+ `getScale=${view.getScale()} / 面板 ${scaleRead.textContent.trim()}`)
297
+ scaleGet.click()
298
+ await wait(80)
299
+ check('基础视图:getScale() 按钮把读到的值写回输入与事件读数',
300
+ Math.abs(Number(scaleInput.value) - view.getScale()) < 0.01 && setup.readouts.event.includes('getScale()'),
301
+ `input=${scaleInput.value} / 事件 ${setup.readouts.event}`)
302
+
303
+ const pointerMove = () => {
304
+ const rect = canvas.getBoundingClientRect()
305
+ canvas.dispatchEvent(new PointerEvent('pointermove', {
306
+ bubbles: true,
307
+ cancelable: true,
308
+ composed: true,
309
+ pointerId: 1,
310
+ pointerType: 'mouse',
311
+ isPrimary: true,
312
+ clientX: rect.left + 12,
313
+ clientY: rect.top + 12
314
+ }))
315
+ }
316
+ pointerMove()
317
+ await wait(280)
318
+ const idleBeforeScale = view.getIdleTime()
319
+ const scaleBeforeProgrammatic = view.getScale()
320
+ view.setScale(scaleBeforeProgrammatic * 1.1)
321
+ await wait(100)
322
+ check('基础视图:程序化 setScale() 不重置空闲计时',
323
+ idleBeforeScale > 200 && view.getIdleTime() >= idleBeforeScale,
324
+ `${idleBeforeScale.toFixed(0)} ms → ${view.getIdleTime().toFixed(0)} ms`)
325
+
326
+ pointerMove()
327
+ await wait(40)
328
+ const idleAfterActivity = view.getIdleTime()
329
+ check('基础视图:真实 pointermove 后 getIdleTime() 归到阈值以内',
330
+ idleAfterActivity < 300,
331
+ `${idleAfterActivity.toFixed(0)} ms`)
332
+
333
+ const idleCallbacksBefore = setup.idleCallbackCount
334
+ const idleEventsBefore = setup.idleEventCount
335
+ await wait(1500)
336
+ await wait(250)
337
+ check('基础视图:停手超过 idleTimeout 后 onIdle 与 idle 各触发一次,读数同步刷新',
338
+ setup.idleCallbackCount === idleCallbacksBefore + 1 &&
339
+ setup.idleEventCount === idleEventsBefore + 1 &&
340
+ view.getIdleTime() > 1200 &&
341
+ idleRead.textContent.includes(`onIdle:${setup.idleCallbackCount} 次`) &&
342
+ idleRead.textContent.includes(`idle 事件:${setup.idleEventCount} 次`),
343
+ `onIdle ${idleCallbacksBefore}→${setup.idleCallbackCount} / idle ${idleEventsBefore}→${setup.idleEventCount} / idleTime ${view.getIdleTime().toFixed(0)} ms`)
344
+ await wait(400)
345
+ check('基础视图:同一空闲段不会重复触发 onIdle / idle',
346
+ setup.idleCallbackCount === idleCallbacksBefore + 1 && setup.idleEventCount === idleEventsBefore + 1,
347
+ `onIdle ${setup.idleCallbackCount} 次 / idle ${setup.idleEventCount} 次`)
348
+ if (!idleToggle.checked) {
349
+ idleToggle.click()
350
+ }
351
+ }
352
+ }
353
+ else {
354
+ check('基础视图:页签在', false, '没找到「基础视图」页签')
355
+ }
356
+
271
357
  // 2.0 点聚合:控件齐全、聚合圆点击只发 clusterClick,maxZoom 展开后又回来
272
358
  if (await clickTab('点聚合')) {
273
359
  const ready = await canvasReady()
@@ -559,6 +645,85 @@
559
645
  const afterObjectChannel = view.getData()
560
646
  const objectChannel = channelList().at(-1)
561
647
  check('可视化连线:commitAddChannel 直接传通道对象即可按 lineIds / groups 落地', Boolean(objectChannelDraft) && channelList().length === 2 && afterObjectChannel.lines.length === beforeObjectChannel.lines.length + 2 && String(objectChannel?.id).startsWith('connect-object-channel-') && objectChannel?.lineIds?.length === 2 && objectChannel.lineIds.every(id => String(id).startsWith('connect-object-line-')), `${channelList().length} 通道 / ${beforeObjectChannel.lines.length} -> ${afterObjectChannel.lines.length} 线 / ${objectChannel?.id ?? '无'} / ${JSON.stringify(objectChannel?.lineIds ?? [])}`)
648
+
649
+ // 单一通道 / 已有分组:预置一条 node-0 ↔ node-1 的通道,再分别验证阻断、新分组、指定分组与 beginAddLine 挂回线
650
+ document.querySelector('[data-demo=connect-seed-existing]').click()
651
+ const seededExisting = await until(() => view.getChannelGroups('connect-existing-channel').length === 2, 3000)
652
+ check('可视化连线:预置已有通道带两个分组,面板读数同步', Boolean(seededExisting) && channelList().length === 3 && text('connect-existing-readout').includes('已有通道'), `${channelList().length} 通道 / ${text('connect-existing-readout')}`)
653
+ const singleChannelInput = document.querySelector('[data-demo=connect-single-channel]')
654
+ const existingModeSelect = document.querySelector('[data-demo=connect-existing-mode]')
655
+ const groupTargetSelect = document.querySelector('[data-demo=connect-group-target]')
656
+ const setExistingMode = async value => {
657
+ existingModeSelect.value = value
658
+ existingModeSelect.dispatchEvent(new Event('change', { bubbles: true }))
659
+ await wait(60)
660
+ }
661
+ const beginPresetPair = async () => {
662
+ startPointSelect.value = 'node-0'
663
+ startPointSelect.dispatchEvent(new Event('change', { bubbles: true }))
664
+ await wait(50)
665
+ document.querySelector('[data-demo=connect-start]').click()
666
+ await wait(70)
667
+ await clickNode('node-1')
668
+ return until(() => {
669
+ const current = view.getAddChannelDraft()
670
+ return current?.phase === 'ready' ? current : null
671
+ }, 2500)
672
+ }
673
+ await setExistingMode('none')
674
+ if (!singleChannelInput.checked) singleChannelInput.click()
675
+ await wait(50)
676
+ const blockedLinesBefore = view.getData().lines.length
677
+ const blockedDraft = await beginPresetPair()
678
+ document.querySelector('[data-demo=connect-default]').click()
679
+ const blockedEventReady = await until(() => text('connect-event-readout').includes('phase=blocked'), 2500)
680
+ check('可视化连线:singleChannel + 已有通道触发 ready blocked 与提交失败事件', Boolean(blockedDraft) && blockedDraft.blocked === true && blockedDraft.existingChannel === 'block' && Boolean(blockedEventReady) && text('connect-event-readout').includes('singleChannel=true') && text('connect-action-readout').includes('blocked'), `${text('connect-event-readout')} / ${view.getData().lines.length} 线`)
681
+ check('可视化连线:阻断失败不改线路 / 通道数据', view.getData().lines.length === blockedLinesBefore && channelList().length === 3, `${blockedLinesBefore} → ${view.getData().lines.length} 线 / ${channelList().length} 通道`)
682
+
683
+ await setExistingMode('newGroup')
684
+ const groupsBeforeNew = view.getChannelGroups('connect-existing-channel').length
685
+ const newGroupLinesBefore = view.getData().lines.length
686
+ const newGroupDraft = await beginPresetPair()
687
+ const newGroupCommitReady = newGroupDraft?.preferred === 'newGroup' && newGroupDraft?.blocked === false
688
+ document.querySelector('[data-demo=connect-default]').click()
689
+ await wait(160)
690
+ check('可视化连线:existingChannel: newGroup 把 2 回新线作为新分组加入已有通道', Boolean(newGroupCommitReady) && view.getChannelGroups('connect-existing-channel').length === groupsBeforeNew + 1 && view.getData().lines.length === newGroupLinesBefore + 2 && channelList().length === 3, `${groupsBeforeNew} → ${view.getChannelGroups('connect-existing-channel').length} 组 / ${newGroupLinesBefore} → ${view.getData().lines.length} 线 / ${text('connect-action-readout')}`)
691
+
692
+ await setExistingMode('joinGroup')
693
+ groupTargetSelect.value = 'connect-existing-group-b'
694
+ groupTargetSelect.dispatchEvent(new Event('change', { bubbles: true }))
695
+ await wait(50)
696
+ const joinGroupBefore = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-b')?.lineIds.length ?? 0
697
+ const joinGroupChannelsBefore = channelList().length
698
+ const joinGroupDraft = await beginPresetPair()
699
+ const joinGroupCommitReady = joinGroupDraft?.targetGroupId === 'connect-existing-group-b'
700
+ document.querySelector('[data-demo=connect-default]').click()
701
+ await wait(160)
702
+ const joinGroupAfter = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-b')?.lineIds.length ?? 0
703
+ check('可视化连线:existingChannel: joinGroup + groupId 把 2 回新线并入指定分组', Boolean(joinGroupCommitReady) && joinGroupAfter === joinGroupBefore + 2 && channelList().length === joinGroupChannelsBefore, `group-b ${joinGroupBefore} → ${joinGroupAfter} 回 / ${channelList().length} 通道 / ${text('connect-action-readout')}`)
704
+
705
+ kindSelect.value = 'line'
706
+ kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
707
+ await wait(60)
708
+ groupTargetSelect.value = 'connect-existing-group-a'
709
+ groupTargetSelect.dispatchEvent(new Event('change', { bubbles: true }))
710
+ await wait(50)
711
+ const attachGroupBefore = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-a')?.lineIds.length ?? 0
712
+ const attachChannelsBefore = channelList().length
713
+ const attachDraft = await beginPresetPair()
714
+ const attachDraftReady = attachDraft?.kind === 'line' && attachDraft?.existingChannel === 'joinGroup' && attachDraft?.targetGroupId === 'connect-existing-group-a'
715
+ document.querySelector('[data-demo=connect-default]').click()
716
+ await wait(160)
717
+ const attachGroupAfter = view.getChannelGroups('connect-existing-channel').find(group => group.id === 'connect-existing-group-a')?.lineIds.length ?? 0
718
+ check('可视化连线:beginAddLine + existingChannel: joinGroup 也能把新回线挂进指定分组,不新建通道', Boolean(attachDraftReady) && attachGroupAfter === attachGroupBefore + 1 && channelList().length === attachChannelsBefore && text('connect-log').includes('beginAddLine'), `${text('connect-log')} / group-a ${attachGroupBefore} → ${attachGroupAfter} 回`)
719
+
720
+ kindSelect.value = 'channel-2'
721
+ kindSelect.dispatchEvent(new Event('change', { bubbles: true }))
722
+ await setExistingMode('auto')
723
+ const autoDraft = await beginPresetPair()
724
+ check('可视化连线:existingChannel: auto 在分组有余量时推荐新分组', autoDraft?.blocked === false && autoDraft?.preferred === 'newGroup' && autoDraft?.existingChannel === 'auto', JSON.stringify({ preferred: autoDraft?.preferred, blocked: autoDraft?.blocked }))
725
+ view.cancelAddChannel()
726
+ await wait(60)
562
727
  check('可视化连线:读数面板同步报出纯点 + 线 / 通道数量', text('connect-data-readout').includes('节点') && text('connect-data-readout').includes('通道'), text('connect-data-readout'))
563
728
  }
564
729
  else {
@@ -931,7 +1096,7 @@
931
1096
  const click = key => document.querySelector(`[data-demo="${key}"]`)?.click()
932
1097
  const text = key => document.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
933
1098
  if (ready && view) {
934
- const groupsOf = () => view.getChannels().length
1099
+ const groupsOf = () => (view.getOptions().channels ?? []).reduce((sum, channel) => sum + view.getChannelGroups(channel.id).length, 0)
935
1100
  const lineIndex = view.getData().lines.findIndex(line => String(line.id) === 'line-0')
936
1101
  const waypointCount = () => view.getWaypoints(lineIndex).length
937
1102
  const pointsBefore = view.getData().points.length
@@ -950,11 +1115,49 @@
950
1115
  click('batch-add-channel-groups')
951
1116
  const groupsAfter = await until(() => groupsOf() === groupsBefore + 2, 6000)
952
1117
  check('批量新增:addChannelGroups 一次补 2 组、4 条回线', Boolean(groupsAfter) && text('batch-add-readout').includes('2 组 / 4 条回线'), `${groupsBefore} → ${groupsOf()} / ${text('batch-add-readout')}`)
953
- const waypointsBefore = waypointCount()
954
- click('batch-add-waypoints')
955
- const waypointsAfter = await until(() => waypointCount() > waypointsBefore, 6000)
956
- check('批量新增:addWaypoints 一次插多个拐点且不虚报失败坐标', Boolean(waypointsAfter) && text('batch-add-readout').includes('addWaypoints') && !text('batch-add-readout').includes('没有落地'), `${waypointsBefore} → ${waypointCount()} / ${text('batch-add-readout')}`)
957
- }
1118
+ const waypointsBefore = waypointCount()
1119
+ click('batch-add-waypoints')
1120
+ const waypointsAfter = await until(() => waypointCount() > waypointsBefore, 6000)
1121
+ check('批量新增:addWaypoints 一次插多个拐点且不虚报失败坐标', Boolean(waypointsAfter) && text('batch-add-readout').includes('addWaypoints') && !text('batch-add-readout').includes('没有落地'), `${waypointsBefore} → ${waypointCount()} / ${text('batch-add-readout')}`)
1122
+ const updatePointBefore = view.getData().points.find(point => String(point.id).startsWith('batch-point-'))
1123
+ const updateLineBefore = view.getData().lines.find(line => String(line.id).startsWith('batch-line-'))
1124
+ click('batch-update-all')
1125
+ const batchUpdated = await until(() => {
1126
+ const point = view.getData().points.find(item => String(item.id).startsWith('batch-point-') && item.batch_updated !== undefined)
1127
+ const line = view.getData().lines.find(item => String(item.id).startsWith('batch-line-') && item.batch_updated !== undefined)
1128
+ const channel = (view.getOptions().channels ?? []).find(item => item.batch_updated !== undefined)
1129
+ return point && line && channel ? { point, line, channel } : null
1130
+ }, 6000)
1131
+ check('批量新增:batch 包住 patchPoints / patchLines / updateChannel,一次改三类数据', Boolean(batchUpdated) && text('batch-add-readout').includes('batch(update)') && text('batch-add-readout').includes('三种 patch 合并为一步历史') && batchUpdated.point.zone_name?.startsWith('批量修改') && batchUpdated.line.color === '#2f6fd0' && Number(batchUpdated.channel.width) >= 2.4, text('batch-add-readout'))
1132
+ view.undo()
1133
+ const batchUpdateUndone = await until(() => {
1134
+ const point = view.getData().points.find(item => item.id === updatePointBefore?.id)
1135
+ const line = view.getData().lines.find(item => item.id === updateLineBefore?.id)
1136
+ const channel = (view.getOptions().channels ?? []).find(item => item.batch_updated !== undefined)
1137
+ return point && point.batch_updated === undefined && line && line.batch_updated === undefined && !channel
1138
+ }, 5000)
1139
+ check('批量新增:三类 patch 只占一步历史,撤销一次整批恢复', Boolean(batchUpdateUndone), `节点 ${updatePointBefore?.id ?? '—'} / 线路 ${updateLineBefore?.id ?? '—'}`)
1140
+ const deletePoint = view.getData().points.find(point => String(point.id).startsWith('batch-point-'))
1141
+ const deleteChannel = (view.getOptions().channels ?? []).find(channel => String(channel.id).startsWith('batch-channel-'))
1142
+ const deleteBefore = { points: view.getData().points.length, lines: view.getData().lines.length, channels: view.getOptions().channels?.length ?? 0 }
1143
+ click('batch-delete-all')
1144
+ const batchDeleted = await until(() => {
1145
+ const after = view.getData()
1146
+ return after.points.length < deleteBefore.points && after.lines.length < deleteBefore.lines && (view.getOptions().channels?.length ?? 0) < deleteBefore.channels ? after : null
1147
+ }, 6000)
1148
+ check('批量新增:batch 内 removeChannel / removePoints / removeLines 一次删三类数据', Boolean(batchDeleted) && text('batch-add-readout').includes('batch(delete)') && text('batch-add-readout').includes('整批只记一步'), `${deleteBefore.points}/${deleteBefore.lines}/${deleteBefore.channels} → ${view.getData().points.length}/${view.getData().lines.length}/${view.getOptions().channels?.length ?? 0} · ${text('batch-add-readout')}`)
1149
+ view.undo()
1150
+ const batchDeleteUndone = await until(() => view.getData().points.length === deleteBefore.points && view.getData().lines.length === deleteBefore.lines && (view.getOptions().channels?.length ?? 0) === deleteBefore.channels, 5000)
1151
+ check('批量新增:删除批同样只占一步历史,撤销后三类数据一起回来', Boolean(batchDeleteUndone) && view.getData().points.some(point => point.id === deletePoint?.id) && (view.getOptions().channels ?? []).some(channel => channel.id === deleteChannel?.id), `${view.getData().points.length}/${view.getData().lines.length}/${view.getOptions().channels?.length ?? 0}`)
1152
+ const groupsBeforeBatchSplit = view.getChannels().length
1153
+ click('batch-group-split')
1154
+ const batchSplit = await until(() => view.getChannels().length > groupsBeforeBatchSplit && text('batch-add-readout').includes('batch(splitChannelGroups)'), 6000)
1155
+ check('批量新增:batch 内逐条 splitChannelGroups,多条通道分组一次重排', Boolean(batchSplit) && text('batch-add-readout').includes('整批一步历史'), `${groupsBeforeBatchSplit} → ${view.getChannels().length} · ${text('batch-add-readout')}`)
1156
+ const groupsBeforeBatchRemove = view.getChannels().length
1157
+ click('batch-group-remove')
1158
+ const batchGroupRemoved = await until(() => view.getChannels().length < groupsBeforeBatchRemove && text('batch-add-readout').includes('batch(removeChannelGroup)'), 6000)
1159
+ check('批量新增:batch 内逐条 removeChannelGroup,回线并回相邻组且数据不丢', Boolean(batchGroupRemoved) && text('batch-add-readout').includes('回线数据未丢'), `${groupsBeforeBatchRemove} → ${view.getChannels().length} · ${text('batch-add-readout')}`)
1160
+ }
958
1161
  else {
959
1162
  check('批量新增:画布与实例都起来了', false, ready ? '没有 setupState.view' : '没有 canvas')
960
1163
  }
@@ -1066,9 +1269,50 @@
1066
1269
  const confirmButton = document.querySelector('[data-demo="merge-confirm"]')
1067
1270
  const cancelButton = document.querySelector('[data-demo="merge-cancel"]')
1068
1271
  check('批量复制与合并:待确认面板给出 resolvePointMerge(true / false) 两个入口', Boolean(confirmButton) && Boolean(cancelButton) && confirmButton.disabled && cancelButton.disabled && confirmButton.textContent.includes('resolvePointMerge(true') && cancelButton.textContent.includes('resolvePointMerge(false'), `${confirmButton?.textContent.trim()} / ${cancelButton?.textContent.trim()}`)
1069
- setup.resetScene()
1070
- await wait(300)
1071
- const dragCanvas = document.querySelector('.gis-view__canvas')
1272
+ setup.resetScene()
1273
+ await wait(300)
1274
+ click('merge-select-sources')
1275
+ await until(() => view.getSelectionData().pointCount === 4, 5000)
1276
+ setup.mergeTargetId = 'node-7'
1277
+ const maxKeysInput = document.querySelector('[data-demo="merge-max-keys"]')
1278
+ maxKeysInput.value = 'vn_kv'
1279
+ maxKeysInput.dispatchEvent(new Event('input', { bubbles: true }))
1280
+ click('merge-apply-max-keys')
1281
+ const maxKeysApplied = await until(() => setup.mergeMaxKeys === 'vn_kv' && view.getOptions().merge?.maxKeys?.join(',') === 'vn_kv', 2000)
1282
+ const maxKeysMergeBefore = view.getData().points.length
1283
+ click('merge-default')
1284
+ const maxKeysMerged = await until(() => view.getData().points.length === maxKeysMergeBefore - 3, 6000)
1285
+ const maxKeysTarget = view.getData().points.find(point => String(point.id) === 'node-7')
1286
+ check('批量复制与合并:maxKeys 输入应用后只把列出的 vn_kv 取参与场站最高', Boolean(maxKeysApplied) && Boolean(maxKeysMerged) && maxKeysTarget?.vn_kv === 1050 && maxKeysTarget?.name === '变电站 8' && text('merge-readout').includes('maxKeys'), JSON.stringify(maxKeysTarget && { name: maxKeysTarget.name, voltage: maxKeysTarget.vn_kv, readout: text('merge-readout') }))
1287
+ setup.resetScene()
1288
+ await wait(300)
1289
+ click('merge-select-sources')
1290
+ await until(() => view.getSelectionData().pointCount === 4, 5000)
1291
+ setup.mergeTargetId = 'node-7'
1292
+ const defaultMergeBefore = view.getData().points.length
1293
+ click('merge-default')
1294
+ const defaultMerged = await until(() => view.getData().points.length === defaultMergeBefore - 3, 6000)
1295
+ const defaultTarget = view.getData().points.find(point => String(point.id) === 'node-7')
1296
+ check('批量复制与合并:maxKeys 缺省时电压等业务字段取保留节点', Boolean(defaultMerged) && defaultTarget?.vn_kv === 15.7 && defaultTarget?.name === '变电站 8' && defaultTarget?.zone_name === '分区 4' && defaultTarget?.demo_merged === undefined, JSON.stringify(defaultTarget && { name: defaultTarget.name, zone: defaultTarget.zone_name, voltage: defaultTarget.vn_kv, demoMerged: defaultTarget.demo_merged }))
1297
+ check('批量复制与合并:缺省合并后的节点带 mergeInfo 来源与快照', defaultTarget?.mergeInfo?.version === 1 && JSON.stringify(defaultTarget.mergeInfo.sourceIds) === '["node-0","node-5","node-12"]' && defaultTarget.mergeInfo.points?.length === 3 && defaultTarget.mergeInfo.lines?.length > 0 && text('merge-readout').includes('mergeInfo'), JSON.stringify({ sources: defaultTarget?.mergeInfo?.sourceIds, points: defaultTarget?.mergeInfo?.points?.length, lines: defaultTarget?.mergeInfo?.lines?.length }))
1298
+ click('split-selected')
1299
+ const defaultSplitLanded = await until(() => view.getData().points.length === defaultMergeBefore && text('split-readout').includes('自动解除合并'), 6000)
1300
+ const defaultSplitResult = unwrap(setup.lastSplitResult)
1301
+ check('批量复制与合并:Demo 自动解除按 mergeInfo 恢复全部来源并回到合并前节点数', Boolean(defaultSplitLanded) && defaultSplitResult?.autoRestored === true && JSON.stringify(defaultSplitResult.mergeSourceIds) === '["node-0","node-5","node-12"]' && view.getData().points.find(point => point.id === 'node-7')?.vn_kv === 15.7, JSON.stringify({ auto: defaultSplitResult?.autoRestored, sources: defaultSplitResult?.mergeSourceIds, points: view.getData().points.length }))
1302
+ check('批量复制与合并:解除合并默认不选中恢复节点,读数明确「未选中拆分结果」', view.getSelectionData().pointCount === 0 && text('split-readout').includes('未选中拆分结果'), JSON.stringify({ selection: view.getSelectionData(), readout: text('split-readout') }))
1303
+ view.undo()
1304
+ const splitUndoForSelect = await until(() => view.getData().points.length === defaultMergeBefore - 3, 5000)
1305
+ const splitSelectInput = document.querySelector('[data-demo="split-select"]')
1306
+ splitSelectInput.checked = true
1307
+ splitSelectInput.dispatchEvent(new Event('change', { bubbles: true }))
1308
+ const splitSelectEnabled = await until(() => setup.splitSelect === true, 2000)
1309
+ click('split-selected')
1310
+ const selectedSplitLanded = await until(() => view.getData().points.length === defaultMergeBefore && text('split-readout').includes('已选中拆分结果'), 6000)
1311
+ const selectedSplitResult = unwrap(setup.lastSplitResult)
1312
+ check('批量复制与合并:勾选后可配置解除合并选中全部恢复节点', Boolean(splitUndoForSelect) && Boolean(splitSelectEnabled) && Boolean(selectedSplitLanded) && view.getSelectionData().pointCount === selectedSplitResult?.items?.length && view.getSelectionData().pointCount === 4, JSON.stringify({ selected: view.getSelectionData().pointCount, items: selectedSplitResult?.items?.length }))
1313
+ setup.resetScene()
1314
+ await wait(300)
1315
+ const dragCanvas = document.querySelector('.gis-view__canvas')
1072
1316
  const dragBox = dragCanvas.getBoundingClientRect()
1073
1317
  const dragSend = (type, screen, extra = {}) => {
1074
1318
  dragCanvas.dispatchEvent(new PointerEvent(type, {
@@ -1211,17 +1455,21 @@
1211
1455
  check('批量复制与合并:409 后 rebase 生成新 opId 并成功落库',
1212
1456
  Boolean(copyRebased) && setup.backendSnapshot?.version === conflictVersion + 2 && setup.backendSnapshot?.applied === conflictApplied + 1,
1213
1457
  `v${conflictVersion} → v${setup.backendSnapshot?.version} / applied ${conflictApplied} → ${setup.backendSnapshot?.applied} / ${text('copy-backend-readout')}`)
1214
- // 拆分:先选一组,再点演示按钮;节点 +1、读数落到面板,撤销一步回到拆分前
1458
+ // 解除合并:先选一组,再点演示按钮;有 mergeInfo 时自动恢复全部来源,撤销一步回到合并态
1215
1459
  click('copy-select-sources')
1216
1460
  const splitSelectionReady = await until(() => view.getSelectionData().pointCount === 4, 5000)
1217
1461
  const splitSource = view.getSelectionData().points?.[0] ?? view.getData().points[0]
1218
1462
  const pointsBeforeSplit = view.getData().points.length
1219
1463
  click('split-selected')
1220
- const splitLanded = await until(() => view.getData().points.length === pointsBeforeSplit + 1 && text('split-readout').includes('splitPoint'), 6000)
1464
+ const splitLanded = await until(() => view.getData().points.length > pointsBeforeSplit && text('split-readout').includes('自动解除合并'), 6000)
1221
1465
  const splitReadout = text('split-readout')
1222
- check('批量复制与合并:拆分选中节点走 splitPoint,第二份与改线 / 改通道读数落面板',
1223
- Boolean(splitSelectionReady) && Boolean(splitLanded) && text('copy-action').includes('splitPoint') && splitReadout.includes('改线') && splitReadout.includes('改通道'),
1466
+ const splitResult = unwrap(setup.lastSplitResult)
1467
+ check('批量复制与合并:解除合并优先走 splitPoint 自动恢复,mergeSourceIds / 改线 / 改通道读数落面板',
1468
+ Boolean(splitSelectionReady) && Boolean(splitLanded) && text('copy-action').includes('splitPoint') && splitResult?.autoRestored === true && splitReadout.includes('恢复来源') && splitReadout.includes('改线') && splitReadout.includes('改通道'),
1224
1469
  `${pointsBeforeSplit} → ${view.getData().points.length} · ${splitReadout}`)
1470
+ check('批量复制与合并:自动解除恢复该次合并的来源节点,来源 id 与 mergeInfo 对得上',
1471
+ splitResult?.autoRestored === true && JSON.stringify(splitResult.mergeSourceIds) === '["node-7"]' && splitResult.items.length === 2 && splitResult.mergeSourceIds.every(id => view.getData().points.some(point => String(point.id) === String(id))),
1472
+ JSON.stringify({ sources: splitResult?.mergeSourceIds, items: splitResult?.items?.map(item => item.id) }))
1225
1473
  view.undo()
1226
1474
  const splitUndone = await until(() => view.getData().points.length === pointsBeforeSplit && view.getData().points.some(point => String(point.id) === String(splitSource?.id)), 5000)
1227
1475
  check('批量复制与合并:拆分是一步历史,撤销后节点与连线恢复', Boolean(splitUndone), `${view.getData().points.length} 点 / ${splitSource?.id ?? '—'}`)
@@ -1871,6 +2119,32 @@
1871
2119
  const channelField = key => channelDemo.querySelector(`[data-demo="${key}"]`)?.textContent.trim() ?? ''
1872
2120
  const channelCanvas = channelDemo.querySelector('canvas.gis-view__canvas')
1873
2121
  if (channelView && channelCanvas) {
2122
+ const simplifyToggle = channelDemo.querySelector('[data-demo="channel-simplify-enabled"]')
2123
+ const simplifyThresholdInput = channelDemo.querySelector('[data-demo="channel-simplify-threshold"]')
2124
+ const simplifyGroupsBefore = channelView.getChannelGroups()
2125
+ const simplifyGroupSize = group => group.lineIds?.length ?? group.lineIndexes?.length ?? 0
2126
+ const simplifyLineCountBefore = channelView.getData().lines.length
2127
+ const simplifyGroupLineCountBefore = simplifyGroupsBefore.reduce((sum, group) => sum + simplifyGroupSize(group), 0)
2128
+ simplifyThresholdInput.value = '1'
2129
+ simplifyThresholdInput.dispatchEvent(new Event('input', { bubbles: true }))
2130
+ simplifyThresholdInput.dispatchEvent(new Event('change', { bubbles: true }))
2131
+ await until(() => channelSetup.simplifyThreshold === 1, 2000)
2132
+ simplifyToggle.checked = true
2133
+ simplifyToggle.dispatchEvent(new Event('change', { bubbles: true }))
2134
+ const simplifyApplied = await until(() => channelView.getOptions().channelSimplify?.enabled === true && channelView.getOptions().channelSimplify?.threshold === 1 && channelField('channel-simplify-readout').includes('开 · threshold=1'), 5000)
2135
+ const simplifyGroups = channelView.getChannelGroups()
2136
+ const simplifyFolded = simplifyGroups.filter(group => simplifyGroupSize(group) > 1)
2137
+ const simplifyHiddenIndexes = new Set(simplifyFolded.flatMap(group => group.lineIndexes.slice(1)))
2138
+ const simplifyExpectedIndexes = channelView.getData().lines.map((_line, index) => index).filter(index => !simplifyHiddenIndexes.has(index))
2139
+ const simplifyVisibleIndexes = channelView.getData().lines.map((_line, index) => index).filter(index => channelView.getLineStyle(index) !== null)
2140
+ const simplifyRepresentativeStyle = simplifyFolded.length ? channelView.getLineStyle(simplifyFolded[0].lineIndexes[0]) : null
2141
+ check('通道与数据:开启简略后每个超阈值分组只画代表线,无 groups 的通道按一组处理', Boolean(simplifyApplied) && simplifyFolded.length > 0 && JSON.stringify(simplifyVisibleIndexes) === JSON.stringify(simplifyExpectedIndexes) && simplifyGroups.reduce((sum, group) => sum + simplifyGroupSize(group), 0) === simplifyGroupLineCountBefore && channelView.getData().lines.length === simplifyLineCountBefore && channelField('channel-simplify-readout').includes(`原始 ${simplifyGroupLineCountBefore} 条`), channelField('channel-simplify-readout'))
2142
+ check('通道与数据:简略代表线应用自定义颜色 / 线宽,关闭后全部回线恢复', simplifyRepresentativeStyle?.width === 4 && Math.round(simplifyRepresentativeStyle.color[0] * 255) === 255 && Math.round(simplifyRepresentativeStyle.color[1] * 255) === 107 && Math.round(simplifyRepresentativeStyle.color[2] * 255) === 53, JSON.stringify(simplifyRepresentativeStyle))
2143
+ simplifyToggle.checked = false
2144
+ simplifyToggle.dispatchEvent(new Event('change', { bubbles: true }))
2145
+ const simplifyDisabled = await until(() => channelView.getOptions().channelSimplify === false && channelField('channel-simplify-readout').includes('关'), 5000)
2146
+ const simplifyRestored = channelView.getData().lines.every((_line, index) => channelView.getLineStyle(index) !== null)
2147
+ check('通道与数据:关闭简略后全部回线恢复,原始数据 / 分组条数保持不变', Boolean(simplifyDisabled) && simplifyRestored && channelView.getData().lines.length === simplifyLineCountBefore && channelView.getChannelGroups().length === simplifyGroupsBefore.length, `可见 ${channelView.getData().lines.filter((_line, index) => channelView.getLineStyle(index) !== null).length} / ${simplifyLineCountBefore}`)
1874
2148
  const visibilityChannelId = channelView.getOptions().channels?.[0]?.id
1875
2149
  const groupsBeforeHide = channelView.getChannelGroups(visibilityChannelId).length
1876
2150
  const membersBeforeHide = channelView.getChannel(visibilityChannelId)?.lineIndexes?.length ?? 0
@@ -2493,8 +2767,8 @@
2493
2767
  await wait(150)
2494
2768
  const apiItems = () => [...document.querySelectorAll('.demo-api-list:not(.demo-api-list--options) .demo-api-item')]
2495
2769
  const optionItems = () => [...document.querySelectorAll('.demo-api-list--options .demo-api-item')]
2496
- check('实例方法速查:整份成员清单按分组列全(16 组 / 265 个成员)',
2497
- Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 265,
2770
+ check('实例方法速查:整份成员清单按分组列全(16 组 / 273 个成员)',
2771
+ Boolean(ready) && document.querySelectorAll('.demo-api-group').length === 16 && apiItems().length === 273,
2498
2772
  `${document.querySelectorAll('.demo-api-group').length} 组 / ${apiItems().length} 个成员`)
2499
2773
  const apiNames = apiItems().map(node => node.querySelector('b')?.textContent)
2500
2774
  const roundNames = ['registerSymbol', 'unregisterSymbol', 'listSymbols']
@@ -2513,7 +2787,7 @@
2513
2787
  check('实例方法速查:这一轮拆出来的方法也在清单里(addChannelGroupLines / joinChannelGroupLines / endAddChannel)',
2514
2788
  roundNames1597.every(name => apiNames.includes(name)),
2515
2789
  roundNames1597.filter(name => !apiNames.includes(name)).join(' / ') || '三个都在')
2516
- check('实例方法速查:options 折叠清单列全 96 个键', optionItems().length === 96, `${optionItems().length} 个键`)
2790
+ check('实例方法速查:options 折叠清单列全 100 个键', optionItems().length === 100, `${optionItems().length} 个键`)
2517
2791
  const apiSearch = document.querySelector('.demo-api-search input[type="search"]')
2518
2792
  apiSearch.value = 'focusOn'
2519
2793
  apiSearch.dispatchEvent(new Event('input', { bubbles: true }))
@@ -110,9 +110,11 @@
110
110
  if (check('文档中心:挂上了 __gisDocs 钩子(没钩子多半是 file:// 打开的)', Boolean(api))) {
111
111
  const doc = docsFrame.contentDocument
112
112
  const files = api.docs.map(item => item.file)
113
- check('文档中心:清单 92 份 md(10 个根 + 82 份子文档),含接口协议 / manual/18 / internals/18 / steps/rounds/09', api.docs.length === 92 && files.includes('接口协议.md') && files.includes('manual/18-逐方法示例.md') && files.includes('internals/18-性能实测与WebGPU评估.md') && files.includes('steps/rounds/09-第89-93轮.md'), `${api.docs.length} 份 / ${api.docs.filter(item => item.parent).length} 份挂在父文档下`)
113
+ check('文档中心:清单 87 份 md(10 个根 + 77 份子文档),含接口协议 / manual/18 / internals/18 / steps/rounds/09', api.docs.length === 87 && files.includes('接口协议.md') && files.includes('manual/18-逐方法示例.md') && files.includes('internals/18-性能实测与WebGPU评估.md') && files.includes('steps/rounds/09-第89-93轮.md'), `${api.docs.length} 份 / ${api.docs.filter(item => item.parent).length} 份挂在父文档下`)
114
+ const aiDocs = api.docs.filter(item => item.file === 'AI对话汇总.md' || item.file.startsWith('ai/'))
115
+ check('文档中心:AI 文档精简为「主汇总 + 维护索引」两个入口,旧 01 ~ 05 已合并', aiDocs.length === 2 && aiDocs.some(item => item.file === 'AI对话汇总.md') && aiDocs.some(item => item.file === 'ai/06-会话索引与复查方法.md' && item.parent === 'AI对话汇总.md') && !files.some(file => /^ai\/(0[1-5])-/.test(file)), aiDocs.map(item => item.file).join(' / '))
114
116
  const parents = api.docs.filter(item => item.parent)
115
- check('文档中心:子文档的 parent 都能对上清单里的父文档,分组标题挂在父条目上(6 组)', parents.length === 82 && parents.every(item => files.includes(item.parent) && api.docs.some(candidate => candidate.file === item.parent && candidate.groupLabel)) && new Set(parents.map(item => item.parent)).size === 6, [...new Set(parents.map(item => item.parent))].join(' / '))
117
+ check('文档中心:子文档的 parent 都能对上清单里的父文档,分组标题挂在父条目上(6 组)', parents.length === 77 && parents.every(item => files.includes(item.parent) && api.docs.some(candidate => candidate.file === item.parent && candidate.groupLabel)) && new Set(parents.map(item => item.parent)).size === 6, [...new Set(parents.map(item => item.parent))].join(' / '))
116
118
  check('文档中心:清单 6 个交互页,含 api/index.html / playground/index.html / guide/sample-document.html', api.pages.length === 6 && api.pages.some(item => item.href === 'api/index.html') && api.pages.some(item => item.href === 'playground/index.html') && api.pages.some(item => item.href === 'guide/sample-document.html'), api.pages.map(item => item.href).join(' / '))
117
119
  check('文档中心:侧栏按清单渲染(md 与交互页两条都数得对)', doc.querySelectorAll('#doc-list a').length === api.docs.length && doc.querySelectorAll('#page-list a').length === api.pages.length)
118
120
  const treeGroups = [...doc.querySelectorAll('#doc-list details.doc-children')]
@@ -125,6 +127,15 @@
125
127
  const pathDocs = pathCards.map(card => card.dataset.doc).filter(Boolean)
126
128
  const apiPathCard = pathCards.find(card => card.textContent.includes('查方法与参数'))
127
129
  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(' / '))
130
+ await api.load('manual/08-选中与拐点.md')
131
+ const breadcrumb = doc.querySelector('#content .doc-breadcrumb')
132
+ const crumbParent = breadcrumb?.querySelector('a[data-doc]')
133
+ check('文档中心:正文顶部有当前位置面包屑,父文档可点回索引', Boolean(breadcrumb) && breadcrumb.textContent.includes('文档中心') && breadcrumb.textContent.includes('GisView.md') && breadcrumb.textContent.includes('手册 08') && crumbParent?.dataset.doc === 'GisView.md' && files.includes(crumbParent.dataset.doc), breadcrumb?.textContent.trim() ?? '没有面包屑')
134
+ const pager = doc.querySelector('#content .doc-pager')
135
+ const pagerLinks = [...pager.querySelectorAll('a[data-doc]')]
136
+ check('文档中心:文末有上一份 / 下一份,顺序与侧栏清单一致', pagerLinks.length === 2 && pagerLinks[0].dataset.doc === 'manual/07-指针与要素编辑.md' && pagerLinks[1].dataset.doc === 'manual/09-通道与多回线.md' && pagerLinks.every(link => files.includes(link.dataset.doc)), pagerLinks.map(link => link.dataset.doc).join(' → '))
137
+ const tocCountText = doc.querySelector('#toc-count')?.textContent ?? ''
138
+ check('文档中心:右栏目录标题显示本页小节数,和目录链接数一致', Number(tocCountText) > 0 && Number(tocCountText) === doc.querySelectorAll('#toc nav a[data-id]').length, `${tocCountText} 个小节 / ${doc.querySelectorAll('#toc nav a[data-id]').length} 条目录`)
128
139
  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
140
  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
141
  const tableFixture = api.render('| 字段 | 类型 |\n| --- | --- |\n| `type` | `\'image/png\' \\| \'image/jpeg\' \\| \'svg\'` |\n')
@@ -520,8 +531,8 @@
520
531
  if (check('API 速查:挂上了 __gisApi 钩子(数据来自 api-index.js,没钩子多半是 file:// 打开的)', Boolean(apiHook))) {
521
532
  const apiDoc = apiFrame.contentDocument
522
533
  const summary = apiHook.summary
523
- check('API 速查:清单来自 api-index(265 个成员 / 260 个方法 / 5 个属性 / 96 个 options / 16 个分组)',
524
- summary.members === 265 && summary.methods === 260 && summary.properties === 5 && summary.options === 96 && summary.sections === 16,
534
+ check('API 速查:清单来自 api-index(273 个成员 / 268 个方法 / 5 个属性 / 100 个 options / 16 个分组)',
535
+ summary.members === 273 && summary.methods === 268 && summary.properties === 5 && summary.options === 100 && summary.sections === 16,
525
536
  JSON.stringify(summary))
526
537
  const topJumps = [...apiDoc.querySelectorAll('.doc-jump')]
527
538
  const jumpTargets = topJumps.map(node => `${node.dataset.docJump}:${node.getAttribute('href')}`)
@@ -559,8 +570,8 @@
559
570
  `${apiDoc.querySelectorAll('.api-member').length} 张卡片 / 属性 ${apiDoc.querySelectorAll('.api-member[data-kind="property"]').length} 张`)
560
571
  const noExample = [...apiDoc.querySelectorAll('.api-member')].filter(node => Number(node.dataset.examples) < 1)
561
572
  check('API 速查:每个成员都带至少一段示例(不出现空卡片)', noExample.length === 0,
562
- noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '265 张卡片都有示例')
563
- check('API 速查:96 个 options 都列全(名字 / 类型 / 默认值 / 说明四件套)',
573
+ noExample.length ? noExample.map(node => node.dataset.name).join(' / ') : '273 张卡片都有示例')
574
+ check('API 速查:100 个 options 都列全(名字 / 类型 / 默认值 / 说明四件套)',
564
575
  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')),
565
576
  `${apiDoc.querySelectorAll('.api-option').length} 个`)
566
577
  const statCards = [...apiDoc.querySelectorAll('#stats .api-stat')]