gis-view-webgl2 0.1.1 → 0.1.3

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 +23 -7
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +563 -61
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +182 -0
  7. package/docs/GisView.md +2 -2
  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 +165 -0
  9. package/docs/api/index.html +1 -1
  10. package/docs/demos/ApiCoverageDemo.vue +644 -0
  11. package/docs/demos/ChannelDemo.vue +580 -18
  12. package/docs/demos/ClusterDemo.vue +251 -0
  13. package/docs/demos/CopyMergeDemo.vue +60 -10
  14. package/docs/demos/EventDemo.vue +3 -3
  15. package/docs/demos/GisViewDemo.vue +64 -8809
  16. package/docs/demos/LabelDemo.vue +67 -11
  17. package/docs/demos/api-index.js +58 -41
  18. package/docs/demos/demos.js +33 -16
  19. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +28 -0
  20. package/docs/demos/gis-view-demo/GisViewDemoDialogs.vue +82 -0
  21. package/docs/demos/gis-view-demo/GisViewDemoFooter.vue +16 -0
  22. package/docs/demos/gis-view-demo/GisViewDemoHeading.vue +16 -0
  23. package/docs/demos/gis-view-demo/GisViewDemoPanelStack.vue +71 -0
  24. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +219 -0
  25. package/docs/demos/gis-view-demo/demo.css +1230 -0
  26. package/docs/demos/gis-view-demo/demoContext.js +12 -0
  27. package/docs/demos/gis-view-demo/panels/DataPanels.vue +303 -0
  28. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +174 -0
  29. package/docs/demos/gis-view-demo/panels/StylePanels.vue +561 -0
  30. package/docs/demos/gis-view-demo/panels/ViewPanels.vue +396 -0
  31. package/docs/demos/gis-view-demo/useGisViewDemo.js +6188 -0
  32. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +4 -2
  33. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  34. 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
  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 +10 -4
  37. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +105 -16
  38. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +8 -8
  39. 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 +100 -0
  40. 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 +78 -3
  41. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +31 -5
  42. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +64 -10
  43. 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 +65 -8
  44. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +54 -1
  45. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +3 -0
  46. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +35 -19
  47. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -0
  48. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +18 -6
  49. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +47 -22
  50. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +80 -13
  51. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +27 -0
  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 +5 -5
  53. package/docs/steps/README.md +18 -10
  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 +578 -53
  57. package/docs/steps/selftest-docs.html +7 -6
  58. package/docs/steps/selftest-effects.html +200 -0
  59. package/docs/steps/selftest-features.html +725 -12
  60. package/docs/steps/selftest-labels.html +323 -4
  61. package/docs/steps/selftest-perf.html +293 -20
  62. package/docs/steps/selftest-scale.html +52 -29
  63. package/docs/steps/step-10-full.js +1 -1
  64. package/docs/steps/step-12-events.js +1 -1
  65. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +17 -11
  66. package/package.json +2 -2
@@ -0,0 +1,251 @@
1
+ <script setup>
2
+ /**
3
+ * 点聚合演示:同一份合成数据在多簇中心附近铺开,面板上调开关 / 半径 / 最少成员数 / 展开缩放。
4
+ *
5
+ * 聚合只改「展示什么、能点到什么」:原始 points、getData()、listPoints() 与业务导出都不变;
6
+ * 聚合圆替代密集成员参与绘制、拾取、快照与 SVG / PNG 导出。点中聚合圆只发 clusterClick,
7
+ * 不会同时发普通 click,也不会改动选中集。
8
+ */
9
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
10
+ import { createGisView } from '../../src/index.js'
11
+ import { DEMO_CENTER, DEMO_RADIUS, createRandom } from './shared/demoData.js'
12
+ import './shared/demoStyle.css'
13
+
14
+ const emit = defineEmits(['notice'])
15
+ const container = ref(null)
16
+ let view = null
17
+ let statsDrawnAt = Number.NEGATIVE_INFINITY
18
+
19
+ const DATA_COUNTS = [2000, 12000, 60000, 100000]
20
+ const pointCountChoice = ref(12000)
21
+ const clusterOn = ref(true)
22
+ const radius = ref(60)
23
+ const minPoints = ref(2)
24
+ const maxZoom = ref(0)
25
+ const statsText = ref('—')
26
+ const clusterText = ref('—')
27
+ const eventText = ref('等待点击聚合圆')
28
+ const normalClicks = ref(0)
29
+ const clusterClicks = ref(0)
30
+ const lastCluster = ref(null)
31
+ const firstClusterClient = ref(null)
32
+ let dataset = { points: [] }
33
+
34
+ /** 多簇数据:每簇一个中心,正态近似(四个随机数求和)让簇内密度自然衰减。 */
35
+ const buildDataset = (count) => {
36
+ const random = createRandom(20261021)
37
+ const points = []
38
+ const centers = [
39
+ [-0.56, 0.34, 0.16],
40
+ [0.08, 0.56, 0.13],
41
+ [0.62, 0.22, 0.18],
42
+ [-0.32, -0.42, 0.15],
43
+ [0.48, -0.5, 0.14],
44
+ [0.02, -0.08, 0.1]
45
+ ]
46
+ for (let index = 0; index < count; index += 1) {
47
+ const center = centers[index % centers.length]
48
+ const common = index < count * 0.08
49
+ const spread = common ? 1.6 : 0.68
50
+ const nx = (random() + random() + random() + random() - 2) * spread
51
+ const ny = (random() + random() + random() + random() - 2) * spread
52
+ points.push({
53
+ id: `cluster-point-${index}`,
54
+ x: Number((DEMO_CENTER[0] + (center[0] + nx * center[2]) * DEMO_RADIUS).toFixed(6)),
55
+ y: Number((DEMO_CENTER[1] + (center[1] + ny * center[2]) * DEMO_RADIUS).toFixed(6)),
56
+ name: `站点 ${index + 1}`,
57
+ zone_name: `分区 ${(index % centers.length) + 1}`,
58
+ vn_kv: [1050, 525, 230, 15.7][index % 4],
59
+ max_loading_rate: Number((0.35 + random() * 1.1).toFixed(3))
60
+ })
61
+ }
62
+ return { points }
63
+ }
64
+
65
+ const clusterOptions = () => clusterOn.value
66
+ ? { enabled: true, radius: radius.value, minPoints: minPoints.value, maxZoom: maxZoom.value }
67
+ : false
68
+
69
+ const buildOptions = () => ({
70
+ points: dataset.points,
71
+ pointStyle: { size: 4, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1 },
72
+ showGrid: true,
73
+ gridColor: 'rgba(126, 168, 214, 0.18)',
74
+ fitPadding: 42,
75
+ denseMode: 'off',
76
+ features: 'view',
77
+ pointCluster: clusterOptions(),
78
+ onReady: () => {
79
+ emit('notice', `点聚合演示已加载 ${dataset.points.length.toLocaleString()} 个点`)
80
+ queueMicrotask(syncClusters)
81
+ },
82
+ onClick: () => {
83
+ normalClicks.value += 1
84
+ eventText.value = '普通 click:聚合圆的点击不会再走到这里'
85
+ },
86
+ onClusterClick: payload => {
87
+ clusterClicks.value += 1
88
+ lastCluster.value = {
89
+ id: payload.id,
90
+ count: payload.count,
91
+ pointIds: [...payload.pointIds],
92
+ bounds: { ...payload.bounds },
93
+ coordinate: [...payload.coordinate]
94
+ }
95
+ const ids = payload.pointIds.slice(0, 6).join('、') + (payload.pointIds.length > 6 ? '…' : '')
96
+ eventText.value = `clusterClick:${payload.count} 点 · ${ids} · 包围盒 ${payload.bounds.minX.toFixed(3)}, ${payload.bounds.minY.toFixed(3)} ~ ${payload.bounds.maxX.toFixed(3)}, ${payload.bounds.maxY.toFixed(3)}`
97
+ },
98
+ onStats: next => {
99
+ const now = performance.now()
100
+ if (now - statsDrawnAt < 250) {
101
+ return
102
+ }
103
+ statsDrawnAt = now
104
+ statsText.value = `${next.pointCount.toLocaleString()} 原始点 · ${next.clusterCount.toLocaleString()} 个聚合圆(${next.clusterPointCount.toLocaleString()} 点)· 展示 ${(next.pointCount - next.clusterPointCount + next.clusterCount).toLocaleString()} 项 · 本帧 ${next.frameMs.toFixed(2)} ms`
105
+ clusterText.value = `聚合耗时 ${next.clusterMs.toFixed(2)} ms · ${view?.getPointCluster()?.active ? '当前缩放仍在聚合' : '当前缩放已展开原始点'}`
106
+ }
107
+ })
108
+
109
+ /** getClusters() 会按当前数据顺序展开单成员;这里只取第一个多成员组,给自测定位点击位置。 */
110
+ const syncClusters = async (options = {}) => {
111
+ if (!view) {
112
+ return
113
+ }
114
+ if (options.render !== false) {
115
+ await view.whenRendered()
116
+ }
117
+ const clusters = view.getClusters().filter(item => item.count > 1)
118
+ const first = clusters[0]
119
+ firstClusterClient.value = first ? view.screenToClient(first.screen[0], first.screen[1]) : null
120
+ }
121
+
122
+ const applyCluster = async () => {
123
+ if (!view) {
124
+ return
125
+ }
126
+ view.updateOptions({ pointCluster: clusterOptions() })
127
+ await syncClusters()
128
+ }
129
+
130
+ const applyData = async () => {
131
+ if (!view) {
132
+ return
133
+ }
134
+ dataset = buildDataset(pointCountChoice.value)
135
+ view.updateData(dataset.points, [])
136
+ view.fitView(42)
137
+ lastCluster.value = null
138
+ await syncClusters()
139
+ emit('notice', `已换成 ${dataset.points.length.toLocaleString()} 个点`)
140
+ }
141
+
142
+ const fitAll = () => {
143
+ view?.fitView(42)
144
+ }
145
+
146
+ const zoomLastCluster = () => {
147
+ if (lastCluster.value?.bounds) {
148
+ view?.zoomToBounds(lastCluster.value.bounds, 54)
149
+ }
150
+ }
151
+
152
+ const countText = computed(() => `${normalClicks.value} 次普通点击 / ${clusterClicks.value} 次聚合点击`)
153
+
154
+ onMounted(() => {
155
+ dataset = buildDataset(pointCountChoice.value)
156
+ view = createGisView(container.value, buildOptions())
157
+ })
158
+
159
+ onBeforeUnmount(() => {
160
+ view?.destroy()
161
+ view = null
162
+ })
163
+ </script>
164
+
165
+ <template>
166
+ <section class='dv-demo'>
167
+ <div class='dv-stage' data-demo='cluster-stage'>
168
+ <div ref='container' class='dv-canvas' data-demo='cluster-canvas' />
169
+ <div class='dv-toolbar'>
170
+ <button type='button' data-demo='cluster-fit' @click='fitAll'>适应全部</button>
171
+ <button type='button' data-demo='cluster-zoom' :disabled='!lastCluster' @click='zoomLastCluster'>放大到最近聚合</button>
172
+ </div>
173
+ <span
174
+ class='cl-anchor'
175
+ data-demo='cluster-first-client'
176
+ :data-client-x='firstClusterClient ? firstClusterClient[0] : undefined'
177
+ :data-client-y='firstClusterClient ? firstClusterClient[1] : undefined'
178
+ />
179
+ </div>
180
+ <aside class='dv-panel'>
181
+ <div class='dv-group'>
182
+ <h3>聚合配置</h3>
183
+ <div class='dv-row'>
184
+ <label><input v-model='clusterOn' data-demo='cluster-toggle' type='checkbox' @change='applyCluster' /> 开启聚合</label>
185
+ </div>
186
+ <div class='dv-row'>
187
+ <label>半径
188
+ <input v-model.number='radius' data-demo='cluster-radius' type='range' min='16' max='180' step='4' @change='applyCluster' />
189
+ <span class='cl-value'>{{ radius }}px</span>
190
+ </label>
191
+ </div>
192
+ <div class='dv-row'>
193
+ <label>最少点数
194
+ <select v-model.number='minPoints' data-demo='cluster-min-points' @change='applyCluster'>
195
+ <option :value='2'>2</option>
196
+ <option :value='3'>3</option>
197
+ <option :value='5'>5</option>
198
+ <option :value='8'>8</option>
199
+ <option :value='12'>12</option>
200
+ </select>
201
+ </label>
202
+ <label>展开缩放
203
+ <select v-model.number='maxZoom' data-demo='cluster-max-zoom' @change='applyCluster'>
204
+ <option :value='0'>不限</option>
205
+ <option :value='1.5'>1.5×</option>
206
+ <option :value='2'>2×</option>
207
+ <option :value='4'>4×</option>
208
+ <option :value='8'>8×</option>
209
+ </select>
210
+ </label>
211
+ </div>
212
+ <p class='dv-hint'>半径是屏幕像素;maxZoom 是相机比例相对基准比例的倍数,超过后展开原始点。聚合期间发散初始化暂停,缩放 / 数据 / 选中变化才重算。</p>
213
+ </div>
214
+ <div class='dv-group'>
215
+ <h3>数据规模</h3>
216
+ <div class='dv-row'>
217
+ <label>点数
218
+ <select v-model.number='pointCountChoice' data-demo='cluster-count'>
219
+ <option v-for='count in DATA_COUNTS' :key='count' :value='count'>{{ count.toLocaleString() }}</option>
220
+ </select>
221
+ </label>
222
+ <button type='button' class='dv-button' data-demo='cluster-rebuild' @click='applyData'>重建</button>
223
+ </div>
224
+ <p class='dv-hint'>十万点仍按整份网格 O(n) 建组;平移不重算,拖动 / 选中中的点强制保持单成员。</p>
225
+ </div>
226
+ <div class='dv-group'>
227
+ <h3>读数</h3>
228
+ <p class='dv-readout' data-demo='cluster-stats'>{{ statsText }}</p>
229
+ <p class='dv-readout' data-demo='cluster-summary'>{{ clusterText }}</p>
230
+ <p class='dv-readout' data-demo='cluster-click-count'>{{ countText }}</p>
231
+ <p class='dv-readout' data-demo='cluster-event'>{{ eventText }}</p>
232
+ <p class='dv-hint'>点击聚合圆应只增加「聚合点击」计数;普通 click 不会被聚合圆触发,选中集也保持不变。</p>
233
+ </div>
234
+ </aside>
235
+ </section>
236
+ </template>
237
+
238
+ <style scoped>
239
+ .cl-anchor {
240
+ position: absolute;
241
+ width: 1px;
242
+ height: 1px;
243
+ overflow: hidden;
244
+ pointer-events: none;
245
+ }
246
+ .cl-value {
247
+ min-width: 38px;
248
+ color: #526b8d;
249
+ font-size: 11px;
250
+ }
251
+ </style>
@@ -25,6 +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
29
  const copyBackendText = ref('复制落库:—')
29
30
  const mergeSyncText = ref('合并落库:—')
30
31
  const backendMode = ref('success')
@@ -64,6 +65,7 @@ let logSeq = 0
64
65
  let copyRound = 0
65
66
  let pasteRound = 0
66
67
  let addPointRound = 0
68
+ let splitRound = 0
67
69
  let mergeSyncTimer = null
68
70
  let mergeSyncQueue = []
69
71
  let disposed = false
@@ -735,7 +737,7 @@ const manualAddPointCopy = () => {
735
737
  return manualText.value
736
738
  }
737
739
 
738
- const prepareMerge = () => {
740
+ const prepareMerge = () => {
739
741
  if (!selectedPoints.value.length) selectBatchSources()
740
742
  if (!mergeTargetOptions.value.some(item => item.value === mergeTargetId.value)) {
741
743
  mergeTargetId.value = mergeTargetOptions.value[0]?.value ?? ''
@@ -743,8 +745,48 @@ const prepareMerge = () => {
743
745
  refreshState()
744
746
  actionText.value = `已准备 ${selectedPoints.value.length} 个选中节点;保留目标 ${mergeTargetId.value || '—'},可在画布继续 Ctrl 点选`
745
747
  pushLog('merge/prepare', actionText.value)
746
- return actionText.value
747
- }
748
+ return actionText.value
749
+ }
750
+
751
+ /** 节点拆分:把连在来源节点上的线路隔一条分给第二份,通道端点默认跟随成员线路 */
752
+ const splitSelectedPoint = () => {
753
+ const data = view?.getData()
754
+ if (!data?.points?.length) return '没有可拆分的节点'
755
+ const source = selectedPoints.value[0] ?? data.points[0]
756
+ const linked = data.lines
757
+ .map((line, index) => ({ line, index }))
758
+ .filter(({ line }) => String(line.from) === String(source.id) || String(line.to) === String(source.id))
759
+ const movedLines = linked.filter((_entry, index) => index % 2 === 1).map(entry => entry.line.id ?? entry.index)
760
+ splitRound += 1
761
+ const offset = 0.42 + splitRound * 0.04
762
+ const result = view.splitPoint(source.id, [
763
+ {
764
+ x: Number(source.x) - offset,
765
+ y: Number(source.y) - offset * 0.55,
766
+ name: `${pointLabel(source)} · A`,
767
+ zone_name: '拆分后 · A'
768
+ },
769
+ {
770
+ x: Number(source.x) + offset,
771
+ y: Number(source.y) + offset * 0.55,
772
+ name: `${pointLabel(source)} · B`,
773
+ zone_name: '拆分后 · B',
774
+ lines: movedLines
775
+ }
776
+ ])
777
+ if (!result.ok) {
778
+ splitText.value = `拆分失败:${result.reason}`
779
+ actionText.value = splitText.value
780
+ pushLog('splitPoint', splitText.value)
781
+ return splitText.value
782
+ }
783
+ splitText.value = `splitPoint:${pointLabel(source)} → ${result.pointIds.join(' / ')} · 改线 ${result.linesChanged} / 删线 ${result.removedLines} · 改通道 ${result.channelsChanged} / 删通道 ${result.removedChannels} · 第 2 份接回线 ${movedLines.length} 条`
784
+ actionText.value = splitText.value
785
+ pushLog('splitPoint', splitText.value)
786
+ view.selectFeatures(result.pointIds)
787
+ refreshState()
788
+ return splitText.value
789
+ }
748
790
 
749
791
  const mergePatch = () => {
750
792
  const patch = {
@@ -879,6 +921,7 @@ const resetScene = () => {
879
921
  copyText.value = '复制:—'
880
922
  copyLineChannelsText.value = '—'
881
923
  mergeBackendText.value = '合并变更 → 后端事务:—'
924
+ splitText.value = '拆分:先选中一个节点(不选就取第一个),再点拆分'
882
925
  clipboardText.value = '剪贴板:—'
883
926
  copyEventText.value = '还没有 pointCopy 事件'
884
927
  copySourcesText.value = '—'
@@ -888,7 +931,8 @@ const resetScene = () => {
888
931
  resetBackendDemo()
889
932
  copyRound = 0
890
933
  pasteRound = 0
891
- addPointRound = 0
934
+ addPointRound = 0
935
+ splitRound = 0
892
936
  refreshState()
893
937
  actionText.value = `已恢复 ${fresh.points.length} 节点 / ${fresh.lines.length} 线路 / ${fresh.channels.length} 通道`
894
938
  return actionText.value
@@ -940,10 +984,9 @@ onMounted(() => {
940
984
  merge: true,
941
985
  keyboard: true,
942
986
  waypoint: false,
943
- remove: false,
944
- channelDraft: false,
945
- channelMenu: false,
946
- contextMenu: false
987
+ remove: false,
988
+ channelDraft: false,
989
+ contextMenu: false
947
990
  },
948
991
  copy: {
949
992
  enabled: true,
@@ -1134,7 +1177,14 @@ onBeforeUnmount(() => {
1134
1177
  <p class="dv-hint">mergeSelectedPoints 只并选中集;target 可以指定保留谁,item 只改最终节点的字段。拖点相撞走 pending / confirm 两站,确认前也能改写。合并落地后的 changes 可直接作为后端事务:删除来源节点、更新保留节点、更新 / 删除线路与通道。</p>
1135
1178
  </div>
1136
1179
  <div class="dv-group">
1137
- <h3>4 · 待确认合并</h3>
1180
+ <h3>4 · 拆分节点</h3>
1181
+ <div class="dv-row">
1182
+ <button type="button" class="dv-button dv-button--primary" data-demo="split-selected" @click="splitSelectedPoint">拆分选中节点</button>
1183
+ </div>
1184
+ <p class="dv-readout" data-demo="split-readout">{{ splitText }}</p>
1185
+ </div>
1186
+ <div class="dv-group">
1187
+ <h3>5 · 待确认合并</h3>
1138
1188
  <p class="dv-readout" data-demo="merge-pending-readout">{{ pendingText }}</p>
1139
1189
  <div class="dv-row cm-form">
1140
1190
  <label>
@@ -1156,7 +1206,7 @@ onBeforeUnmount(() => {
1156
1206
  <p class="dv-hint">清空选中后,把画布上的一个节点拖到另一个节点上松手;选保留拖动来源 / 碰撞目标,再点确认。pending 载荷的 impact 已按「保留碰撞目标」预演影响线路数,confirm 时按最终 keep 重算;名称、分区、电压和样式都随 item 写回最终节点。</p>
1157
1207
  </div>
1158
1208
  <div class="dv-group">
1159
- <h3>5 · 复制 / 合并假接口<span class="dv-chip">{{ backendBusy ? '请求中' : '空闲' }}</span></h3>
1209
+ <h3>6 · 复制 / 合并假接口<span class="dv-chip">{{ backendBusy ? '请求中' : '空闲' }}</span></h3>
1160
1210
  <div class="dv-row cm-form">
1161
1211
  <label>
1162
1212
  下一次响应
@@ -89,14 +89,14 @@ const dataLabel = (value) => {
89
89
  }
90
90
 
91
91
  /**
92
- * 要 DOM 事件的四个事件(pointerDown / pointerUp / contextMenu / channelContextMenu)在总线上
92
+ * 要 DOM 事件的三个事件(pointerDown / pointerUp / contextMenu)在总线上
93
93
  * 载荷是 `{ payload, event }`(与回调的两个参数一一对应),其余事件载荷就是回调里那一份 —— 这里统一拆开。
94
94
  */
95
- const EVENT_PAYLOAD_EVENTS = new Set(['pointerDown', 'pointerUp', 'contextMenu', 'channelContextMenu'])
95
+ const EVENT_PAYLOAD_EVENTS = new Set(['pointerDown', 'pointerUp', 'contextMenu'])
96
96
  const unwrapPayload = (raw, name) => (EVENT_PAYLOAD_EVENTS.has(name) && raw && typeof raw === 'object' && 'payload' in raw ? raw.payload : raw)
97
97
 
98
98
  /** 会带 action 的事件名:* 订阅里从载荷上认出「这一次是什么操作」 */
99
- const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'channelContextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
99
+ const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
100
100
 
101
101
  /** 最近一次操作的判读:面板上的「动作判读」直接读它 */
102
102
  const lastAction = ref('—')