gis-view-webgl2 0.1.8 → 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 (48) hide show
  1. package/README.md +7 -5
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +214 -14
  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 -547
  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 -526
  10. package/docs/demos/BasicDemo.vue +142 -25
  11. package/docs/demos/BatchAddDemo.vue +127 -1
  12. package/docs/demos/ChannelDemo.vue +61 -0
  13. package/docs/demos/CopyMergeDemo.vue +125 -36
  14. package/docs/demos/EventDemo.vue +12 -4
  15. package/docs/demos/api-index.js +26 -16
  16. package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
  17. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
  18. package/docs/index.html +136 -13
  19. 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
  20. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +82 -14
  21. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +2 -1
  22. 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
  23. 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 +48 -11
  24. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +8 -4
  25. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +57 -0
  26. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +26 -3
  27. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +34 -1
  28. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
  29. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +13 -10
  30. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +14 -8
  31. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +43 -3
  32. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +3 -1
  33. 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 +5 -2
  34. package/docs/steps/README.md +200 -121
  35. package/docs/steps/index.html +216 -0
  36. package/docs/steps/selftest-demos.html +209 -31
  37. package/docs/steps/selftest-docs.html +17 -6
  38. package/docs/steps/selftest-features.html +311 -3
  39. package/docs/steps/selftest-labels.html +81 -0
  40. package/docs/steps/selftest-perf.html +31 -3
  41. package/docs/steps/selftest-select.html +51 -1
  42. 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
  43. package/package.json +1 -1
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
@@ -25,7 +25,7 @@ const copyItemsText = ref('—')
25
25
  const copyLineChannelsText = ref('—')
26
26
  const manualText = ref('—')
27
27
  const mergeBackendText = ref('合并变更 → 后端事务:—')
28
- const splitText = ref('解除合并:先选中一个节点(不选就取第一个),再点按钮')
28
+ const splitText = ref('自动解除合并:先选中一个合并后的节点(不选就找第一个带 mergeInfo 的节点)')
29
29
  const lastSplitResult = shallowRef(null)
30
30
  const copyBackendText = ref('复制落库:—')
31
31
  const mergeSyncText = ref('合并落库:—')
@@ -45,8 +45,12 @@ const mergedVoltage = ref('525')
45
45
  const mergedColor = ref('#ff5d8f')
46
46
  const mergedSize = ref('16')
47
47
  const mergedSymbol = ref('square')
48
- const mergedStrokeColor = ref('#ffffff')
49
- const mergeTargetId = ref('')
48
+ const mergedStrokeColor = ref('#ffffff')
49
+ /** 合并时取双方最高值的字段;空字符串表示不取任何键 */
50
+ const mergeMaxKeys = ref('')
51
+ /** 拆分 / 自动解除合并后是否选中结果节点 */
52
+ const splitSelect = ref(false)
53
+ const mergeTargetId = ref('')
50
54
  const mergeKeep = ref('target')
51
55
  const rewriteOnConfirm = ref(true)
52
56
  const pointOptions = ref([])
@@ -73,6 +77,7 @@ let disposed = false
73
77
 
74
78
  const pointLabel = item => String(item?.name ?? item?.label ?? item?.id ?? '节点')
75
79
  const pointKey = item => String(item?.id ?? item?.label ?? item?.name ?? '—')
80
+ const parsedMergeMaxKeys = () => mergeMaxKeys.value.split(',').map(key => key.trim()).filter(Boolean)
76
81
  const itemId = item => (item?.id ?? item?.line_id ?? item?.channel_id ?? '无 id')
77
82
  /** 复制载荷的 lines 与 lineChannels 一一对齐:直接列出每条回线复制后属于哪条通道 / 哪一组 */
78
83
  const describeLineChannels = (payload) => {
@@ -102,6 +107,14 @@ const describeMergeChanges = changes => describeMergeChangeSets(changes ? [chang
102
107
  const describeMergeImpact = impact => (impact
103
108
  ? `影响 ${impact.lineCount} 条线路(改线 ${impact.linesChanged} / 删线 ${impact.removedLines})· 通道 改 ${impact.channelsChanged} / 删 ${impact.removedChannels} / 并 ${impact.mergedChannels}`
104
109
  : '影响读数:当前数据对不上')
110
+ const voltageOf = item => item?.vn_kv ?? item?.voltage ?? item?.voltageKv ?? item?.voltage_level ?? item?.voltageLevel ?? '—'
111
+ /** 合并追溯读数:来源、保留前电压与快照条数,演示解除合并需要的字段都在这里 */
112
+ const describeMergeInfo = (item) => {
113
+ const info = item?.mergeInfo
114
+ if (!info || typeof info !== 'object') return 'mergeInfo —'
115
+ const sourceIds = (info.sourceIds ?? info.points?.map(snapshot => snapshot.id) ?? []).map(id => String(id ?? '无 id')).join(' / ') || '—'
116
+ return `mergeInfo v${info.version} · 保留前 ${info.targetPoint?.name ?? '—'} / ${voltageOf(info.targetPoint)} kV · 来源 [${sourceIds}] · 快照 线 ${info.lines?.length ?? 0} / 通道 ${info.channels?.length ?? 0}`
117
+ }
105
118
  const backendBusy = computed(() => backendBusyCount.value > 0)
106
119
  const backendStateText = computed(() => `假后端 v${backendSnapshot.value.version} · 已应用 ${backendSnapshot.value.applied} 条 · op 缓存 ${backendSnapshot.value.cached} · 幂等回放 ${backendSnapshot.value.replays ?? 0} 次 · 模拟延迟 ${backendSnapshot.value.latency}ms`)
107
120
  const backendModeHint = computed(() => FAKE_BACKEND_MODES.find(item => item.value === backendMode.value)?.hint ?? '')
@@ -749,27 +762,31 @@ const prepareMerge = () => {
749
762
  return actionText.value
750
763
  }
751
764
 
752
- /** 解除合并:splitPoint 的 autoLines=alternate 按通道成组、独立线路单条轮流分配,不会把同通道成员拆散 */
765
+ /** 解除合并:优先用 mergeInfo 自动恢复全部来源;没有合并记录时才退回手工两份拆分 */
753
766
  const splitSelectedPoint = () => {
754
767
  const data = view?.getData()
755
768
  if (!data?.points?.length) return '没有可拆分的节点'
756
- const source = selectedPoints.value[0] ?? data.points[0]
769
+ const mergedPoint = selectedPoints.value.find(item => item?.mergeInfo) ?? data.points.find(item => item?.mergeInfo)
770
+ const source = mergedPoint ?? selectedPoints.value[0] ?? data.points[0]
771
+ const auto = Boolean(mergedPoint?.mergeInfo)
757
772
  splitRound += 1
758
773
  const offset = 0.42 + splitRound * 0.04
759
- const result = view.splitPoint(source.id, [
760
- {
761
- x: Number(source.x) - offset,
762
- y: Number(source.y) - offset * 0.55,
763
- name: `${pointLabel(source)} · A`,
764
- zone_name: '解除合并后 · A'
765
- },
766
- {
767
- x: Number(source.x) + offset,
768
- y: Number(source.y) + offset * 0.55,
769
- name: `${pointLabel(source)} · B`,
770
- zone_name: '解除合并后 · B'
771
- }
772
- ], { autoLines: 'alternate', label: '解除合并' })
774
+ const result = auto
775
+ ? view.splitPoint(source.id, null, { label: '解除合并(自动恢复)', select: splitSelect.value })
776
+ : view.splitPoint(source.id, [
777
+ {
778
+ x: Number(source.x) - offset,
779
+ y: Number(source.y) - offset * 0.55,
780
+ name: `${pointLabel(source)} · A`,
781
+ zone_name: '解除合并后 · A'
782
+ },
783
+ {
784
+ x: Number(source.x) + offset,
785
+ y: Number(source.y) + offset * 0.55,
786
+ name: `${pointLabel(source)} · B`,
787
+ zone_name: '解除合并后 · B'
788
+ }
789
+ ], { autoLines: 'alternate', label: '解除合并(手工拆分)', select: splitSelect.value })
773
790
  lastSplitResult.value = result
774
791
  if (!result.ok) {
775
792
  splitText.value = `解除合并失败:${result.reason}`
@@ -777,6 +794,14 @@ const splitSelectedPoint = () => {
777
794
  pushLog('splitPoint', splitText.value)
778
795
  return splitText.value
779
796
  }
797
+ if (result.autoRestored) {
798
+ const restoredNames = (result.items ?? []).map(item => `${pointLabel(item)}(${String(voltageOf(item))} kV)`).join(' / ')
799
+ splitText.value = `splitPoint(自动解除合并):${pointLabel(source)} → ${restoredNames} · 恢复来源 ${(result.mergeSourceIds ?? []).map(id => String(id)).join(' / ') || '—'} · 改线 ${result.linesChanged} / 改通道 ${result.channelsChanged} · ${splitSelect.value ? '已选中拆分结果' : '未选中拆分结果'} · mergeInfo 已从保留节点移除`
800
+ actionText.value = splitText.value
801
+ pushLog('splitPoint/auto', splitText.value)
802
+ refreshState()
803
+ return splitText.value
804
+ }
780
805
  const lineSlot = new Map()
781
806
  for (const assignment of result.lineAssignments ?? []) {
782
807
  for (const lineId of assignment.lineIds ?? []) {
@@ -792,10 +817,9 @@ const splitSelectedPoint = () => {
792
817
  return slots.size <= 1
793
818
  })
794
819
  const counts = result.lineAssignments.map(assignment => `${assignment.index === 0 ? 'A' : assignment.index === 1 ? 'B' : String(assignment.index + 1)} ${assignment.lineCount}`).join(' / ')
795
- splitText.value = `splitPoint(解除合并):${pointLabel(source)} → ${result.pointIds.join(' / ')} · autoLines=${result.autoLines} · 自动分线 ${result.autoAssignedLines}(${counts})· 改线 ${result.linesChanged} / 删线 ${result.removedLines} · 改通道 ${result.channelsChanged} / 删通道 ${result.removedChannels} · 通道成组${channelsKept ? '保持完整' : '发生拆分'}`
820
+ splitText.value = `splitPoint(解除合并):${pointLabel(source)} → ${result.pointIds.join(' / ')} · autoLines=${result.autoLines} · 自动分线 ${result.autoAssignedLines}(${counts})· 改线 ${result.linesChanged} / 删线 ${result.removedLines} · 改通道 ${result.channelsChanged} / 删通道 ${result.removedChannels} · 通道成组${channelsKept ? '保持完整' : '发生拆分'} · ${splitSelect.value ? '已选中拆分结果' : '未选中拆分结果'}`
796
821
  actionText.value = splitText.value
797
822
  pushLog('splitPoint', splitText.value)
798
- view.selectFeatures(result.pointIds)
799
823
  refreshState()
800
824
  return splitText.value
801
825
  }
@@ -816,7 +840,7 @@ const mergePatch = () => {
816
840
  return patch
817
841
  }
818
842
 
819
- const mergeSelectedByApi = () => {
843
+ const mergeSelectedByApi = () => {
820
844
  const points = selectedPoints.value
821
845
  if (points.length < 2) return 'mergeSelectedPoints:至少选中两个节点'
822
846
  const targetId = mergeTargetOptions.value.some(item => item.value === mergeTargetId.value)
@@ -841,8 +865,59 @@ const mergeSelectedByApi = () => {
841
865
  actionText.value = mergeText.value
842
866
  pushLog('mergeSelectedPoints', mergeText.value)
843
867
  refreshState()
844
- return mergeText.value
845
- }
868
+ return mergeText.value
869
+ }
870
+
871
+ /** 把面板上的字段清单写进 merge.maxKeys;空清单表示所有业务字段都取保留节点 */
872
+ const applyMergeMaxKeys = () => {
873
+ if (!view) return '画布还没准备好'
874
+ const maxKeys = parsedMergeMaxKeys()
875
+ const merge = view.getOptions().merge ?? {}
876
+ view.updateOptions({ merge: { ...merge, enabled: true, maxKeys } })
877
+ actionText.value = maxKeys.length
878
+ ? `merge.maxKeys = [${maxKeys.join(', ')}]:合并时这些数值字段取双方最高值`
879
+ : 'merge.maxKeys = []:合并时所有业务字段保留目标节点(组件不再默认取电压最高)'
880
+ pushLog('merge.maxKeys', actionText.value)
881
+ return actionText.value
882
+ }
883
+
884
+ /** 缺省合并:不传 item,按 merge.maxKeys 决定哪些数值字段取双方最高值 */
885
+ const mergeSelectedByDefault = () => {
886
+ const points = selectedPoints.value
887
+ if (points.length < 2) return 'mergeSelectedPoints:至少选中两个节点'
888
+ const targetId = mergeTargetOptions.value.some(item => item.value === mergeTargetId.value)
889
+ ? mergeTargetId.value
890
+ : mergeTargetOptions.value[0].value
891
+ const target = points.find(item => pointKey(item) === targetId)
892
+ const sources = points.filter(item => pointKey(item) !== targetId).map(item => item.id)
893
+ const maxKeys = parsedMergeMaxKeys()
894
+ const maxReadout = maxKeys.length
895
+ ? maxKeys.map((key) => {
896
+ const values = points.map(item => Number(item[key])).filter(Number.isFinite)
897
+ return `${key}=${values.length ? Math.max(...values) : '无数字'}`
898
+ }).join(' / ')
899
+ : '未配置(全部字段取保留节点)'
900
+ const result = view.mergeSelectedPoints({
901
+ target: { id: target?.id ?? targetId },
902
+ sources,
903
+ label: 'demo 缺省合并(maxKeys)'
904
+ })
905
+ if (result.ok) {
906
+ const item = result.target?.item
907
+ view.selectFeatures(target?.id ?? targetId)
908
+ mergeText.value = `mergeSelectedPoints(缺省规则)· 保留 ${pointLabel(item)} · maxKeys ${maxReadout} · 结果 ${String(voltageOf(item))} kV · 名称 ${item?.name ?? '—'} / 图标 ${String(item?.symbol ?? '—')} · ${describeMergeInfo(item)}`
909
+ pendingText.value = maxKeys.length
910
+ ? `这次没有 item 改写:maxKeys 列出的字段取参与场站最高值,其余字段取保留节点`
911
+ : '这次没有 item 改写:maxKeys 为空,全部业务字段(含电压)取保留节点'
912
+ }
913
+ else {
914
+ mergeText.value = `mergeSelectedPoints 失败:${result.reason}`
915
+ }
916
+ actionText.value = mergeText.value
917
+ pushLog('mergeSelectedPoints/default', mergeText.value)
918
+ refreshState()
919
+ return mergeText.value
920
+ }
846
921
 
847
922
  const handlePointMerge = (payload) => {
848
923
  if (payload.phase === 'pending') {
@@ -880,7 +955,7 @@ const handlePointMerge = (payload) => {
880
955
  if (payload.phase === 'merge') {
881
956
  mergeBackendText.value = payload.ok ? describeMergeChanges(payload.changes) : '合并变更 → 后端事务:合并失败,没有变更集'
882
957
  mergeText.value = payload.ok
883
- ? `merge · 被并 ${pointLabel(payload.source?.item)} → 保留 ${pointLabel(payload.target?.item)} · 最终 ${pointLabel(payload.item)} · 色 ${payload.item?.color ?? '—'} / 尺寸 ${payload.item?.size ?? '—'} / 图标 ${String(payload.item?.symbol ?? '—')} · 改线 ${payload.linesChanged} / 删线 ${payload.removedLines}`
958
+ ? `merge · 被并 ${pointLabel(payload.source?.item)} → 保留 ${pointLabel(payload.target?.item)} · 最终 ${pointLabel(payload.item)} · 电压 ${String(voltageOf(payload.item))} kV · 色 ${payload.item?.color ?? '—'} / 尺寸 ${payload.item?.size ?? '—'} / 图标 ${String(payload.item?.symbol ?? '—')} · 改线 ${payload.linesChanged} / 删线 ${payload.removedLines} · ${describeMergeInfo(payload.item)}`
884
959
  : `merge 失败:${payload.reason}`
885
960
  pendingMerge.value = null
886
961
  pendingText.value = payload.ok ? '合并已落地,拖动来源节点已经收进保留目标' : `合并失败:${payload.reason}`
@@ -933,13 +1008,16 @@ const resetScene = () => {
933
1008
  copyText.value = '复制:—'
934
1009
  copyLineChannelsText.value = '—'
935
1010
  mergeBackendText.value = '合并变更 → 后端事务:—'
936
- splitText.value = '拆分:先选中一个节点(不选就取第一个),再点拆分'
1011
+ splitText.value = '自动解除合并:先选中一个合并后的节点(不选就找第一个带 mergeInfo 的节点)'
937
1012
  clipboardText.value = '剪贴板:—'
938
1013
  copyEventText.value = '还没有 pointCopy 事件'
939
1014
  copySourcesText.value = '—'
940
1015
  copyItemsText.value = '—'
941
1016
  manualText.value = '—'
942
1017
  mergeKeep.value = 'target'
1018
+ mergeMaxKeys.value = ''
1019
+ splitSelect.value = false
1020
+ view.updateOptions({ merge: { enabled: true, distance: 12, channelGroups: 'keep', maxKeys: [] } })
943
1021
  resetBackendDemo()
944
1022
  copyRound = 0
945
1023
  pasteRound = 0
@@ -1012,7 +1090,7 @@ onMounted(() => {
1012
1090
  },
1013
1091
  // Ctrl+C / Ctrl+V 监听的三件套:copy 打开 + keyboard 打开 + 快捷键映射(不写也是这两个缺省值)
1014
1092
  shortcuts: { copy: 'ctrl+c', paste: 'ctrl+v' },
1015
- merge: { enabled: true, distance: 12, channelGroups: 'keep' },
1093
+ merge: { enabled: true, distance: 12, channelGroups: 'keep', maxKeys: [] },
1016
1094
  onReady: () => {
1017
1095
  nextTick(() => {
1018
1096
  view?.fitView(50)
@@ -1178,23 +1256,34 @@ onBeforeUnmount(() => {
1178
1256
  <input v-model="mergedStrokeColor" data-demo="merge-stroke-color" type="text" />
1179
1257
  </label>
1180
1258
  </div>
1181
- <div class="dv-row">
1182
- <label><input v-model="rewriteOnConfirm" type="checkbox" /> 手动拖拽合并确认时改写 payload.item</label>
1183
- </div>
1184
- <div class="dv-row">
1185
- <button type="button" class="dv-button dv-button--primary" data-demo="merge-selected" :disabled="!canMerge" @click="mergeSelectedByApi">合并选中(指定保留节点)</button>
1186
- </div>
1259
+ <div class="dv-row">
1260
+ <label><input v-model="rewriteOnConfirm" type="checkbox" /> 手动拖拽合并确认时改写 payload.item</label>
1261
+ </div>
1262
+ <div class="dv-row cm-form">
1263
+ <label>
1264
+ maxKeys(逗号分隔)
1265
+ <input v-model="mergeMaxKeys" data-demo="merge-max-keys" type="text" placeholder="例如 vn_kv,value;留空则都不取最高" />
1266
+ </label>
1267
+ <button type="button" class="dv-button" data-demo="merge-apply-max-keys" @click="applyMergeMaxKeys">应用 maxKeys</button>
1268
+ </div>
1269
+ <div class="dv-row">
1270
+ <button type="button" class="dv-button dv-button--primary" data-demo="merge-default" :disabled="!canMerge" @click="mergeSelectedByDefault">按缺省规则合并(按 maxKeys 取最高)</button>
1271
+ <button type="button" class="dv-button dv-button--primary" data-demo="merge-selected" :disabled="!canMerge" @click="mergeSelectedByApi">合并选中(指定保留节点)</button>
1272
+ </div>
1187
1273
  <p class="dv-readout" data-demo="merge-readout">{{ mergeText }}</p>
1188
1274
  <p class="dv-readout" data-demo="merge-backend-readout">{{ mergeBackendText }}</p>
1189
- <p class="dv-hint">mergeSelectedPoints 只并选中集;target 可以指定保留谁,item 只改最终节点的字段。拖点相撞走 pending / confirm 两站,确认前也能改写。合并落地后的 changes 可直接作为后端事务:删除来源节点、更新保留节点、更新 / 删除线路与通道。</p>
1275
+ <p class="dv-hint">「按缺省规则合并」不传 item:只有 <code>merge.maxKeys</code> 列出的数值字段取双方最高值(缺省为空,不取任何键),名称 / 图标等其余业务字段取保留节点;合并后的节点带 <code>mergeInfo</code>(来源快照、线路 / 通道快照)。「指定保留节点」仍可用 item 覆盖字段。拖点相撞走 pending / confirm 两站,确认前也能改写。合并落地后的 changes 可直接作为后端事务。</p>
1190
1276
  </div>
1191
1277
  <div class="dv-group">
1192
1278
  <h3>4 · 解除合并</h3>
1193
1279
  <div class="dv-row">
1194
- <button type="button" class="dv-button dv-button--primary" data-demo="split-selected" @click="splitSelectedPoint">解除合并(拆分节点)</button>
1280
+ <label><input v-model="splitSelect" data-demo="split-select" type="checkbox" /> 解除合并后选中恢复出的节点</label>
1281
+ </div>
1282
+ <div class="dv-row">
1283
+ <button type="button" class="dv-button dv-button--primary" data-demo="split-selected" @click="splitSelectedPoint">自动解除合并(恢复来源)</button>
1195
1284
  </div>
1196
1285
  <p class="dv-readout" data-demo="split-readout">{{ splitText }}</p>
1197
- <p class="dv-hint"><code>splitPoint(source, [A, B], { autoLines: 'alternate' })</code> 把没显式分配的线路按“同一条通道的成员成组、独立线路单条”轮流分给 A / B;返回的 <code>autoAssignedLines</code> 与 <code>lineAssignments</code> 直接显示每一份接到哪些回线。</p>
1286
+ <p class="dv-hint"><code>splitPoint(source)</code> 省略 parts 时读取节点上的 <code>mergeInfo</code>,恢复合并前的保留站和全部来源节点,并按快照恢复线路端点、通道端点、分组与回线归属。默认 <code>select: false</code>,拆分后不选中任何恢复节点;勾选这一项才传 <code>select: true</code>。没有合并记录时自动退回 <code>splitPoint(source, [A, B], { autoLines: 'alternate' })</code> 的手工拆分;自动路径返回 <code>autoRestored: true</code> 与 <code>mergeSourceIds</code>。</p>
1198
1287
  </div>
1199
1288
  <div class="dv-group">
1200
1289
  <h3>5 · 待确认合并</h3>
@@ -59,7 +59,9 @@ const describePayload = (payload) => {
59
59
  if (payload.labelId !== undefined) parts.push(`labelId=${payload.labelId}`)
60
60
  if (payload.count !== undefined) parts.push(`count=${payload.count}`)
61
61
  if (payload.channelId !== undefined) parts.push(`channel=${payload.channelId}`)
62
- if (payload.camera !== undefined) parts.push('camera=…')
62
+ if (payload.camera !== undefined) parts.push('camera=…')
63
+ if (payload.idleTime !== undefined) parts.push(`idleTime=${Math.round(payload.idleTime)}ms`)
64
+ if (payload.timeout !== undefined) parts.push(`timeout=${payload.timeout}ms`)
63
65
  if (payload.screen !== undefined) parts.push(`screen=[${payload.screen.map(value => Number(value).toFixed(0)).join(', ')}]`)
64
66
  if (payload.action) {
65
67
  const action = payload.action
@@ -114,8 +116,11 @@ const buildOptions = () => ({
114
116
  channels: dataset.channels,
115
117
  pointStyle: { size: 7, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.5 },
116
118
  lineStyle: { width: 1.5, color: 'rgba(120, 168, 226, 0.55)' },
117
- showGrid: true,
118
- fitPadding: 60,
119
+ showGrid: true,
120
+ fitPadding: 60,
121
+ // 停手 3 秒发一次 idle,事件日志与计数里都能看到;
122
+ // onCameraChange / cameraChange 也在这条链路上,高频开关打开后能看到逐帧载荷。
123
+ idleTimeout: 3000,
119
124
  label: { content: '{name}', position: 'top', gap: 8, maxCount: 30, draggable: true, editable: true, style: { color: '#27405f', fontSize: '12px' } },
120
125
  // 标签按优化档显示(限数 + 避让),见 docs/GisView.md 的「标签显示优化开关」
121
126
  labelOptimize: true,
@@ -168,7 +173,10 @@ const methods = [
168
173
  { name: 'focusOn(前三个节点, { select: true })', run: () => { const ids = (view?.getData().points ?? []).slice(0, 3).map(point => point.id ?? 0); return ids.length > 0 ? view?.focusOn(ids, { select: true }) : null } },
169
174
  { name: 'getStats()', run: () => view?.getStats() },
170
175
  { name: 'getQuality()', run: () => view?.getQuality() },
171
- { name: 'getCamera()', run: () => view?.getCamera() },
176
+ { name: 'getCamera()', run: () => view?.getCamera() },
177
+ { name: 'getScale()', run: () => view?.getScale() },
178
+ { name: 'setScale(当前 × 1.25)', run: () => view?.setScale(view.getScale() * 1.25) },
179
+ { name: 'getIdleTime()', run: () => view?.getIdleTime() },
172
180
  { name: 'getBounds()', run: () => view?.getBounds() },
173
181
  { name: 'getFilterResult()', run: () => view?.getFilterResult() },
174
182
  { name: 'getLabelStates()', run: () => view?.getLabelStates() },