gis-view-webgl2 0.1.6 → 0.1.8
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.
- package/README.md +9 -8
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +316 -233
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +106 -0
- package/docs/GisView.md +1 -1
- 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 +110 -0
- package/docs/demos/BasicDemo.vue +2 -2
- package/docs/demos/BatchAddDemo.vue +6 -6
- package/docs/demos/ConnectDemo.vue +164 -33
- package/docs/demos/CopyMergeDemo.vue +27 -14
- package/docs/demos/EventDemo.vue +17 -20
- package/docs/demos/LabelDemo.vue +7 -7
- package/docs/demos/StyleDemo.vue +1 -1
- package/docs/demos/api-index.js +34 -31
- package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -1
- package/docs/demos/gis-view-demo/GisViewDemoStage.vue +1 -1
- package/docs/demos/gis-view-demo/panels/DataPanels.vue +1 -1
- package/docs/demos/gis-view-demo/useGisViewDemo.js +33 -35
- 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
- package/docs/from0/05-/347/233/270/346/234/272/344/270/216/344/272/244/344/272/222.md +1 -1
- 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
- package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +1 -1
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +49 -16
- 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
- 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 +60 -36
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +60 -4
- package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +17 -1
- 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
- package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +2 -2
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +12 -13
- package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +26 -23
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +23 -20
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +132 -79
- 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
- package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +2 -1
- 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 +4 -4
- package/docs/steps/README.md +9 -7
- package/docs/steps/bench-scale.html +1 -1
- package/docs/steps/index.html +6 -6
- package/docs/steps/selftest-actions.html +26 -25
- package/docs/steps/selftest-data.html +1 -1
- package/docs/steps/selftest-demos.html +102 -6
- package/docs/steps/selftest-docs.html +3 -3
- package/docs/steps/selftest-features.html +284 -52
- package/docs/steps/selftest-full.html +3 -3
- package/docs/steps/selftest-labels.html +1 -1
- package/docs/steps/selftest-perf.html +31 -4
- package/docs/steps/selftest-select.html +183 -1
- package/docs/steps/step-05-pick.js +4 -8
- package/docs/steps/step-07-channels.js +3 -5
- package/docs/steps/step-08-labels.js +3 -3
- package/docs/steps/step-10-full.js +3 -3
- package/docs/steps/step-12-events.js +41 -24
- package/package.json +1 -1
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* 可视化连线:只靠 points + lines 也能独立成图,再把「新增一条线」的几种常见配置摆在一起。
|
|
4
4
|
*
|
|
5
5
|
* 这一页的初始数据明确是 channels: [] —— 没有任何通道,节点与线路照样能渲染、选中与新增。
|
|
6
|
-
*
|
|
6
|
+
* 「开始连线」按类型分流:独立线路走 beginAddLine,通道走 beginAddChannel,两个节点选完先出草稿:
|
|
7
7
|
* - routing: 'orthogonal' 让预览自动补一个先横后纵的 90° 拐点;
|
|
8
|
-
* - channelDraftStyle 直接交给 beginAddChannel,草稿期间用 updateAddChannelStyle 原地改箭头位置 / 间距 / 配色;
|
|
8
|
+
* - channelDraftStyle 直接交给 beginAddLine / beginAddChannel,草稿期间用 updateAddChannelStyle 原地改箭头位置 / 间距 / 配色;
|
|
9
|
+
* - 预置一条已有通道(两组)后,可演示 singleChannel 阻断、existingChannel 新分组或并入指定分组;
|
|
10
|
+
* - beginAddLine 显式写 existingChannel 时会把新回线挂进已有通道,不写则始终是独立线路;
|
|
9
11
|
* - 取消只收草稿,数据一个字节不动;
|
|
10
12
|
* - 选好起点后点节点以外的位置也会直接取消,事件里的 reason 是 click-outside-node;
|
|
11
13
|
* - 默认添加走 commitAddChannel;
|
|
@@ -29,6 +31,10 @@ const arrowSize = ref(12)
|
|
|
29
31
|
const arrowPositionsText = ref('0.5')
|
|
30
32
|
const circuitSpacing = ref(12)
|
|
31
33
|
const startPointId = ref('')
|
|
34
|
+
const singleChannel = ref(true)
|
|
35
|
+
const existingChannelPolicy = ref('none')
|
|
36
|
+
const groupTarget = ref('')
|
|
37
|
+
const existingGroups = ref([])
|
|
32
38
|
const dotsVisible = ref(true)
|
|
33
39
|
const dotSize = ref(5)
|
|
34
40
|
const startDotColor = ref('#2f6fd0')
|
|
@@ -38,6 +44,7 @@ const draftLayer = ref('above')
|
|
|
38
44
|
const draft = ref(null)
|
|
39
45
|
const actionText = ref('配置好线条之后点「开始连线」,依次点两个节点')
|
|
40
46
|
const dataText = ref('正在创建视图…')
|
|
47
|
+
const eventText = ref('channelDraft:尚未触发')
|
|
41
48
|
const cancelText = ref('尚未取消:选好起点后点一下节点以外的空白处,应收到 reason = click-outside-node')
|
|
42
49
|
const logLines = ref([])
|
|
43
50
|
const logText = computed(() => logLines.value.length
|
|
@@ -78,6 +85,15 @@ const createInitialLines = () => {
|
|
|
78
85
|
|
|
79
86
|
const isChannel = computed(() => linkKind.value !== 'line')
|
|
80
87
|
const circuits = computed(() => linkKind.value === 'channel-3' ? 3 : linkKind.value === 'channel-2' ? 2 : 1)
|
|
88
|
+
const addMethod = computed(() => isChannel.value ? 'beginAddChannel' : 'beginAddLine')
|
|
89
|
+
const groupLineCount = group => Number(group?.lineCount ?? group?.lineIndexes?.length ?? group?.lineIds?.length ?? 0)
|
|
90
|
+
const groupOptions = computed(() => existingGroups.value.map((group, index) => ({
|
|
91
|
+
value: String(group.id),
|
|
92
|
+
label: `第 ${index + 1} 组 · ${String(group.id)} · ${groupLineCount(group)} 回`
|
|
93
|
+
})))
|
|
94
|
+
const existingText = computed(() => existingGroups.value.length
|
|
95
|
+
? `已有通道 connect-existing-channel:${existingGroups.value.map((group, index) => `第 ${index + 1} 组 ${groupLineCount(group)} 回`).join(' / ')}`
|
|
96
|
+
: '已有通道:未预置(先点上方按钮创建一条两组的通道)')
|
|
81
97
|
const arrowPositions = computed(() => arrowPositionsText.value.split(',').map(value => Number(value.trim())).filter(value => Number.isFinite(value)))
|
|
82
98
|
const startPointLabel = computed(() => {
|
|
83
99
|
const point = points.find(item => String(item.id) === String(startPointId.value))
|
|
@@ -100,7 +116,12 @@ const draftText = computed(() => {
|
|
|
100
116
|
}
|
|
101
117
|
const to = current.to.item?.name ?? current.to.id
|
|
102
118
|
const bends = current.path?.length ?? 0
|
|
103
|
-
|
|
119
|
+
const policy = current.existingChannel
|
|
120
|
+
? ` · existingChannel=${current.existingChannel}${current.targetGroupId !== null ? ` → ${String(current.targetGroupId)}` : ''}`
|
|
121
|
+
: ''
|
|
122
|
+
const single = current.singleChannel ? ' · singleChannel' : ''
|
|
123
|
+
const blocked = current.blocked ? ` · 已阻断:${current.reason ?? '未知原因'}` : ''
|
|
124
|
+
return `${kind}${single}${policy} · ${from} → ${to} · ${current.routing === 'orthogonal' ? '正交折线' : '直线'} · 中间拐点 ${bends} · 箭头 ${current.style?.arrow ? current.style.arrowPositions.map(value => `${Math.round(value * 100)}%`).join('/') : '关'} · 回线间距 ${current.style?.circuitSpacing ?? 0}px${blocked}`
|
|
104
125
|
})
|
|
105
126
|
|
|
106
127
|
const pushLog = (name, text) => {
|
|
@@ -143,24 +164,91 @@ const currentLineProps = () => ({
|
|
|
143
164
|
direction: arrow.value ? { count: 1, size: arrowSize.value } : false
|
|
144
165
|
})
|
|
145
166
|
|
|
167
|
+
/** 读取预置通道的分组,供 joinGroup 下拉直接选择;预置前后 / 草稿 ready 时都刷新一次 */
|
|
168
|
+
const refreshExistingGroups = () => {
|
|
169
|
+
if (!view) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const between = view.getChannelBetween('node-0', 'node-1')
|
|
173
|
+
existingGroups.value = between?.hasChannel ? [...(between.groups ?? [])] : []
|
|
174
|
+
if (!existingGroups.value.some(group => String(group.id) === String(groupTarget.value))) {
|
|
175
|
+
groupTarget.value = existingGroups.value[0]?.id ?? ''
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 预置一条 node-0 ↔ node-1 的通道,带两个分组,专门演示单一通道和指定分组 */
|
|
180
|
+
const seedExistingChannel = () => {
|
|
181
|
+
if (!view) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
view.endAddChannel()
|
|
185
|
+
draft.value = null
|
|
186
|
+
const between = view.getChannelBetween('node-0', 'node-1')
|
|
187
|
+
if (between?.hasChannel) {
|
|
188
|
+
refreshExistingGroups()
|
|
189
|
+
actionText.value = `已有通道 ${String(between.channelId)} 已存在,共 ${existingGroups.value.length} 组;可以直接开始连线`
|
|
190
|
+
pushLog('seed-existing', actionText.value)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
const lineA = 'connect-existing-line-a'
|
|
194
|
+
const lineB = 'connect-existing-line-b'
|
|
195
|
+
const result = view.addChannel({
|
|
196
|
+
id: 'connect-existing-channel',
|
|
197
|
+
name: '演示已有通道(两组)',
|
|
198
|
+
from: 'node-0',
|
|
199
|
+
to: 'node-1'
|
|
200
|
+
}, [
|
|
201
|
+
{ id: lineA, name: '已有通道 · A 组回线', color: '#2f6fd0', width: 2.2 },
|
|
202
|
+
{ id: lineB, name: '已有通道 · B 组回线', color: '#d9544d', width: 2.2 }
|
|
203
|
+
], [
|
|
204
|
+
{ id: 'connect-existing-group-a', name: 'A 组', lineIds: [lineA] },
|
|
205
|
+
{ id: 'connect-existing-group-b', name: 'B 组', lineIds: [lineB] }
|
|
206
|
+
], { select: true })
|
|
207
|
+
refreshExistingGroups()
|
|
208
|
+
refreshState()
|
|
209
|
+
if (!result.ok) {
|
|
210
|
+
actionText.value = `预置已有通道失败:${result.reason ?? '未知原因'}`
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
actionText.value = `已预置已有通道 ${String(result.channelId)}:node-0 ↔ node-1,共 ${existingGroups.value.length} 组;现在可用 singleChannel / newGroup / joinGroup 演示`
|
|
214
|
+
}
|
|
215
|
+
pushLog('addChannel(预置已有通道)', actionText.value)
|
|
216
|
+
emit('notice', actionText.value)
|
|
217
|
+
}
|
|
218
|
+
|
|
146
219
|
const startDraft = () => {
|
|
147
220
|
if (!view) {
|
|
148
221
|
return
|
|
149
222
|
}
|
|
150
223
|
view.endAddChannel()
|
|
151
|
-
|
|
224
|
+
const options = {
|
|
152
225
|
...(startPointId.value ? { from: startPointId.value } : {}),
|
|
153
|
-
kind: isChannel.value ? 'channel' : 'line',
|
|
154
226
|
circuits: circuits.value,
|
|
155
227
|
routing: routing.value,
|
|
156
228
|
select: true,
|
|
157
229
|
channelDraftStyle: currentDraftStyle(),
|
|
158
230
|
lineProps: currentLineProps()
|
|
159
|
-
}
|
|
231
|
+
}
|
|
232
|
+
if (existingChannelPolicy.value !== 'none') {
|
|
233
|
+
options.existingChannel = existingChannelPolicy.value
|
|
234
|
+
}
|
|
235
|
+
if (existingChannelPolicy.value === 'joinGroup' && groupTarget.value !== '') {
|
|
236
|
+
options.groupId = groupTarget.value
|
|
237
|
+
}
|
|
238
|
+
if (isChannel.value) {
|
|
239
|
+
// beginAddChannel 的单一通道模式:已有通道时按 existingChannel 处理,不写时默认 block
|
|
240
|
+
options.singleChannel = singleChannel.value
|
|
241
|
+
view.beginAddChannel(options)
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// beginAddLine 不认 singleChannel;要挂进已有通道必须显式写 existingChannel
|
|
245
|
+
view.beginAddLine(options)
|
|
246
|
+
}
|
|
247
|
+
const modeText = existingChannelPolicy.value === 'none' ? '未指定已有通道策略' : `existingChannel=${existingChannelPolicy.value}`
|
|
160
248
|
actionText.value = startPointId.value
|
|
161
|
-
?
|
|
162
|
-
:
|
|
163
|
-
pushLog(
|
|
249
|
+
? `${addMethod.value}:起点已指定为 ${startPointLabel.value},下一次点节点只选终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${modeText})`
|
|
250
|
+
: `${addMethod.value}:先点起点,再点终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${modeText})`
|
|
251
|
+
pushLog(addMethod.value, actionText.value)
|
|
164
252
|
}
|
|
165
253
|
|
|
166
254
|
const cancelDraft = () => {
|
|
@@ -184,17 +272,15 @@ const commitDraft = () => {
|
|
|
184
272
|
if (!view || !canFinish.value) {
|
|
185
273
|
return
|
|
186
274
|
}
|
|
187
|
-
const result = view.commitAddChannel()
|
|
275
|
+
const result = view.commitAddChannel(undefined, { select: true })
|
|
188
276
|
refreshState()
|
|
189
277
|
if (!result.ok) {
|
|
190
|
-
actionText.value = `commitAddChannel
|
|
278
|
+
actionText.value = `commitAddChannel → blocked:${result.reason ?? '未知原因'}`
|
|
191
279
|
}
|
|
192
280
|
else {
|
|
193
281
|
const lines = result.lines?.length ?? (result.line ? 1 : 0)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
view.selectFeatures(result.lineIds)
|
|
197
|
-
}
|
|
282
|
+
const group = result.groupId !== undefined && result.groupId !== null ? ` · 分组 ${String(result.groupId)}` : ''
|
|
283
|
+
actionText.value = `commitAddChannel → ${result.kind === 'line' ? '独立线路' : '通道'} ${String(result.channelId ?? result.lineId ?? '')} · ${lines} 回线${group}`
|
|
198
284
|
}
|
|
199
285
|
pushLog('commitAddChannel', actionText.value)
|
|
200
286
|
emit('notice', actionText.value)
|
|
@@ -238,7 +324,7 @@ const commitWithObject = () => {
|
|
|
238
324
|
groups: [{ id: channelId, lineIds, ...(middle ? { path: middle } : {}) }]
|
|
239
325
|
}
|
|
240
326
|
}
|
|
241
|
-
const result = view.commitAddChannel(payload)
|
|
327
|
+
const result = view.commitAddChannel(current.kind === 'line' ? { line: payload } : { channel: payload }, { select: true })
|
|
242
328
|
refreshState()
|
|
243
329
|
if (!result.ok) {
|
|
244
330
|
actionText.value = `commitAddChannel(对象) 没有落地:${result.reason ?? '未知原因'}`
|
|
@@ -246,9 +332,6 @@ const commitWithObject = () => {
|
|
|
246
332
|
else {
|
|
247
333
|
const lines = result.lines?.length ?? (result.line ? 1 : 0)
|
|
248
334
|
actionText.value = `commitAddChannel(对象) → ${result.kind === 'line' ? '线路对象' : '通道对象'} ${String(result.channelId ?? result.lineId ?? '')} · ${lines} 回线`
|
|
249
|
-
if (result.lineIds?.length) {
|
|
250
|
-
view.selectFeatures(result.lineIds)
|
|
251
|
-
}
|
|
252
335
|
}
|
|
253
336
|
pushLog('commitAddChannel(对象)', actionText.value)
|
|
254
337
|
emit('notice', actionText.value)
|
|
@@ -303,23 +386,27 @@ const addWithChannels = () => {
|
|
|
303
386
|
sequence += 1
|
|
304
387
|
const middle = middlePathOf(current)
|
|
305
388
|
const kind = current.kind === 'line' ? 'line' : 'channel'
|
|
306
|
-
const result =
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
389
|
+
const result = kind === 'line'
|
|
390
|
+
? view.addLine({
|
|
391
|
+
id: `connect-line-${sequence}`,
|
|
392
|
+
name: `独立线路 ${sequence}`,
|
|
393
|
+
from: current.from.id,
|
|
394
|
+
to: current.to.id,
|
|
395
|
+
...(middle ? { path: middle } : {}),
|
|
396
|
+
lineProps: currentLineProps()
|
|
397
|
+
}, undefined, { select: true })
|
|
398
|
+
: view.addChannels([{
|
|
399
|
+
id: `connect-channel-${sequence}`,
|
|
400
|
+
name: `可视化通道 ${sequence}`,
|
|
401
|
+
from: current.from.id,
|
|
402
|
+
to: current.to.id,
|
|
403
|
+
...(middle ? { path: middle } : {}),
|
|
404
|
+
lineData: Array.from({ length: Math.max(Number(current.circuits) || 1, 1) }, () => ({ ...currentLineProps() }))
|
|
405
|
+
}], { select: true })
|
|
316
406
|
refreshState()
|
|
317
407
|
if (result.ok) {
|
|
318
408
|
view.endAddChannel()
|
|
319
409
|
draft.value = null
|
|
320
|
-
if (result.lineIds.length) {
|
|
321
|
-
view.selectFeatures(result.lineIds)
|
|
322
|
-
}
|
|
323
410
|
actionText.value = `addChannels → ${result.count} 条 · ${result.lineIds.length} 回线${result.channelIds.length ? ` · 通道 ${result.channelIds.join(' / ')}` : ' · 未新建通道'}${middle ? ` · path ${middle.length} 拐点` : ''}`
|
|
324
411
|
}
|
|
325
412
|
else {
|
|
@@ -338,6 +425,11 @@ const resetScene = () => {
|
|
|
338
425
|
view.clearHistory()
|
|
339
426
|
view.fitView(56)
|
|
340
427
|
draft.value = null
|
|
428
|
+
existingGroups.value = []
|
|
429
|
+
groupTarget.value = ''
|
|
430
|
+
existingChannelPolicy.value = 'none'
|
|
431
|
+
singleChannel.value = true
|
|
432
|
+
eventText.value = 'channelDraft:尚未触发'
|
|
341
433
|
sequence = 0
|
|
342
434
|
actionText.value = '已恢复为 channels: [] 的纯点 + 线场景'
|
|
343
435
|
refreshState()
|
|
@@ -366,6 +458,15 @@ onMounted(() => {
|
|
|
366
458
|
},
|
|
367
459
|
onChannelDraft: payload => {
|
|
368
460
|
draft.value = payload
|
|
461
|
+
const target = payload.targetGroupId !== undefined && payload.targetGroupId !== null ? ` · targetGroup=${String(payload.targetGroupId)}` : ''
|
|
462
|
+
eventText.value = `channelDraft:phase=${payload.phase} · kind=${payload.kind ?? '—'} · blocked=${payload.blocked === true} · singleChannel=${payload.singleChannel === true} · existingChannel=${payload.existingChannel ?? 'null'}${target}${payload.reason ? ` · reason=${payload.reason}` : ''}`
|
|
463
|
+
if (payload.phase === 'ready' && payload.hasChannel) {
|
|
464
|
+
refreshExistingGroups()
|
|
465
|
+
}
|
|
466
|
+
if (payload.phase === 'blocked') {
|
|
467
|
+
actionText.value = `失败事件(phase=blocked):${payload.reason ?? '当前草稿不允许新增'}`
|
|
468
|
+
emit('notice', actionText.value)
|
|
469
|
+
}
|
|
369
470
|
if (payload.phase === 'cancel') {
|
|
370
471
|
cancelText.value = `取消:phase = ${payload.phase} · reason = ${payload.reason ?? '未提供'} · 起点 ${payload.from?.item?.name ?? payload.from?.id ?? '—'}`
|
|
371
472
|
actionText.value = payload.reason === 'click-outside-node'
|
|
@@ -438,7 +539,36 @@ onBeforeUnmount(() => {
|
|
|
438
539
|
</label>
|
|
439
540
|
<label>回线间距 <input v-model.number="circuitSpacing" type="range" min="6" max="28" step="1" data-demo="connect-spacing" /> {{ circuitSpacing }}px</label>
|
|
440
541
|
</div>
|
|
441
|
-
<p class="dv-hint">草稿样式直接传给 <code>
|
|
542
|
+
<p class="dv-hint">草稿样式直接传给 <code>beginAddLine</code> / <code>beginAddChannel</code>;连线期间改颜色 / 线宽 / 箭头位置 / 回线间距会走 <code>updateAddChannelStyle()</code>,预览立即更新。正交折线会自动补一个 90 度拐点;落地线路用完整 path,通道只写中间拐点。</p>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div class="dv-group">
|
|
546
|
+
<h3>单一通道与已有分组</h3>
|
|
547
|
+
<div class="dv-row">
|
|
548
|
+
<button type="button" class="dv-button" data-demo="connect-seed-existing" :disabled="isDraftLive" @click="seedExistingChannel">预置已有通道(两组)</button>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="dv-row">
|
|
551
|
+
<label><input v-model="singleChannel" type="checkbox" data-demo="connect-single-channel" :disabled="!isChannel || isDraftLive" /> singleChannel</label>
|
|
552
|
+
<label>已有通道时
|
|
553
|
+
<select v-model="existingChannelPolicy" data-demo="connect-existing-mode" :disabled="isDraftLive">
|
|
554
|
+
<option value="none">不指定(线路默认独立)</option>
|
|
555
|
+
<option value="auto">auto · 新分组优先</option>
|
|
556
|
+
<option value="newGroup">newGroup · 新分组</option>
|
|
557
|
+
<option value="joinGroup">joinGroup · 指定分组</option>
|
|
558
|
+
<option value="block">block · 直接失败</option>
|
|
559
|
+
</select>
|
|
560
|
+
</label>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="dv-row">
|
|
563
|
+
<label>目标分组
|
|
564
|
+
<select v-model="groupTarget" data-demo="connect-group-target" :disabled="existingChannelPolicy !== 'joinGroup' || isDraftLive">
|
|
565
|
+
<option v-if="!groupOptions.length" value="">先预置已有通道</option>
|
|
566
|
+
<option v-for="group in groupOptions" :key="group.value" :value="group.value">{{ group.label }}</option>
|
|
567
|
+
</select>
|
|
568
|
+
</label>
|
|
569
|
+
</div>
|
|
570
|
+
<p class="dv-readout" data-demo="connect-existing-readout">{{ existingText }}</p>
|
|
571
|
+
<p class="dv-hint"><code>beginAddChannel({ singleChannel: true })</code> 在两端已有通道时默认阻断;显式 <code>existingChannel</code> 可改为新分组 / 指定分组 / 自动。<code>beginAddLine</code> 不读 singleChannel,但显式写 <code>existingChannel: 'newGroup' / 'joinGroup'</code> 时,会把新回线加进已有通道。</p>
|
|
442
572
|
</div>
|
|
443
573
|
|
|
444
574
|
<div class="dv-group">
|
|
@@ -481,6 +611,7 @@ onBeforeUnmount(() => {
|
|
|
481
611
|
<p class="dv-readout" data-demo="connect-draft-readout">{{ draftText }}</p>
|
|
482
612
|
<p class="dv-readout" data-demo="connect-data-readout">{{ dataText }}</p>
|
|
483
613
|
<p class="dv-readout" data-demo="connect-action-readout">{{ actionText }}</p>
|
|
614
|
+
<p class="dv-readout" data-demo="connect-event-readout">{{ eventText }}</p>
|
|
484
615
|
<p class="dv-readout" data-demo="connect-cancel-readout">{{ cancelText }}</p>
|
|
485
616
|
</div>
|
|
486
617
|
|
|
@@ -25,7 +25,8 @@ 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('解除合并:先选中一个节点(不选就取第一个),再点按钮')
|
|
29
|
+
const lastSplitResult = shallowRef(null)
|
|
29
30
|
const copyBackendText = ref('复制落库:—')
|
|
30
31
|
const mergeSyncText = ref('合并落库:—')
|
|
31
32
|
const backendMode = ref('success')
|
|
@@ -748,15 +749,11 @@ const prepareMerge = () => {
|
|
|
748
749
|
return actionText.value
|
|
749
750
|
}
|
|
750
751
|
|
|
751
|
-
/**
|
|
752
|
+
/** 解除合并:splitPoint 的 autoLines=alternate 按通道成组、独立线路单条轮流分配,不会把同通道成员拆散 */
|
|
752
753
|
const splitSelectedPoint = () => {
|
|
753
754
|
const data = view?.getData()
|
|
754
755
|
if (!data?.points?.length) return '没有可拆分的节点'
|
|
755
756
|
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
757
|
splitRound += 1
|
|
761
758
|
const offset = 0.42 + splitRound * 0.04
|
|
762
759
|
const result = view.splitPoint(source.id, [
|
|
@@ -764,23 +761,38 @@ const splitSelectedPoint = () => {
|
|
|
764
761
|
x: Number(source.x) - offset,
|
|
765
762
|
y: Number(source.y) - offset * 0.55,
|
|
766
763
|
name: `${pointLabel(source)} · A`,
|
|
767
|
-
zone_name: '
|
|
764
|
+
zone_name: '解除合并后 · A'
|
|
768
765
|
},
|
|
769
766
|
{
|
|
770
767
|
x: Number(source.x) + offset,
|
|
771
768
|
y: Number(source.y) + offset * 0.55,
|
|
772
769
|
name: `${pointLabel(source)} · B`,
|
|
773
|
-
zone_name: '
|
|
774
|
-
lines: movedLines
|
|
770
|
+
zone_name: '解除合并后 · B'
|
|
775
771
|
}
|
|
776
|
-
])
|
|
772
|
+
], { autoLines: 'alternate', label: '解除合并' })
|
|
773
|
+
lastSplitResult.value = result
|
|
777
774
|
if (!result.ok) {
|
|
778
|
-
splitText.value =
|
|
775
|
+
splitText.value = `解除合并失败:${result.reason}`
|
|
779
776
|
actionText.value = splitText.value
|
|
780
777
|
pushLog('splitPoint', splitText.value)
|
|
781
778
|
return splitText.value
|
|
782
779
|
}
|
|
783
|
-
|
|
780
|
+
const lineSlot = new Map()
|
|
781
|
+
for (const assignment of result.lineAssignments ?? []) {
|
|
782
|
+
for (const lineId of assignment.lineIds ?? []) {
|
|
783
|
+
lineSlot.set(String(lineId), assignment.index)
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const channelsKept = (data.channels ?? []).every((channel) => {
|
|
787
|
+
const slots = new Set()
|
|
788
|
+
for (const lineId of channel.lineIds ?? []) {
|
|
789
|
+
const slot = lineSlot.get(String(lineId))
|
|
790
|
+
if (slot !== undefined) slots.add(slot)
|
|
791
|
+
}
|
|
792
|
+
return slots.size <= 1
|
|
793
|
+
})
|
|
794
|
+
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 ? '保持完整' : '发生拆分'}`
|
|
784
796
|
actionText.value = splitText.value
|
|
785
797
|
pushLog('splitPoint', splitText.value)
|
|
786
798
|
view.selectFeatures(result.pointIds)
|
|
@@ -1177,11 +1189,12 @@ onBeforeUnmount(() => {
|
|
|
1177
1189
|
<p class="dv-hint">mergeSelectedPoints 只并选中集;target 可以指定保留谁,item 只改最终节点的字段。拖点相撞走 pending / confirm 两站,确认前也能改写。合并落地后的 changes 可直接作为后端事务:删除来源节点、更新保留节点、更新 / 删除线路与通道。</p>
|
|
1178
1190
|
</div>
|
|
1179
1191
|
<div class="dv-group">
|
|
1180
|
-
<h3>4 ·
|
|
1192
|
+
<h3>4 · 解除合并</h3>
|
|
1181
1193
|
<div class="dv-row">
|
|
1182
|
-
<button type="button" class="dv-button dv-button--primary" data-demo="split-selected" @click="splitSelectedPoint"
|
|
1194
|
+
<button type="button" class="dv-button dv-button--primary" data-demo="split-selected" @click="splitSelectedPoint">解除合并(拆分节点)</button>
|
|
1183
1195
|
</div>
|
|
1184
1196
|
<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>
|
|
1185
1198
|
</div>
|
|
1186
1199
|
<div class="dv-group">
|
|
1187
1200
|
<h3>5 · 待确认合并</h3>
|
package/docs/demos/EventDemo.vue
CHANGED
|
@@ -58,23 +58,22 @@ const describePayload = (payload) => {
|
|
|
58
58
|
if (payload.index !== undefined) parts.push(`index=${payload.index}`)
|
|
59
59
|
if (payload.labelId !== undefined) parts.push(`labelId=${payload.labelId}`)
|
|
60
60
|
if (payload.count !== undefined) parts.push(`count=${payload.count}`)
|
|
61
|
-
if (payload.
|
|
62
|
-
if (payload.channelId !== undefined) parts.push(`channel=${payload.channelId}`)
|
|
61
|
+
if (payload.channelId !== undefined) parts.push(`channel=${payload.channelId}`)
|
|
63
62
|
if (payload.camera !== undefined) parts.push('camera=…')
|
|
64
63
|
if (payload.screen !== undefined) parts.push(`screen=[${payload.screen.map(value => Number(value).toFixed(0)).join(', ')}]`)
|
|
65
64
|
if (payload.action) {
|
|
66
65
|
const action = payload.action
|
|
67
|
-
parts.push(`action=${action.type}/${action.target}${action.moved ? '+moved' : ''}${action.double ? '+double' : ''}@${
|
|
68
|
-
parts.push(`data=${dataLabel(
|
|
66
|
+
parts.push(`action=${action.type}/${action.target}${action.moved ? '+moved' : ''}${action.double ? '+double' : ''}@${payload.originalEvent?.button ?? '-'}`)
|
|
67
|
+
parts.push(`data=${dataLabel(hitDataOf(payload))}`)
|
|
69
68
|
}
|
|
70
69
|
if (!parts.length) parts.push(`keys=[${Object.keys(payload).slice(0, 6).join(', ')}]`)
|
|
71
70
|
return parts.join(' · ')
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*/
|
|
74
|
+
* 命中的数据在载荷顶层:节点 / 线路看 item,通道看 channel,选中集报 items 条数,
|
|
75
|
+
* 标签(labelClick / labelDrag / labelContextMenu / labelDblClick)看 label / parent。
|
|
76
|
+
*/
|
|
78
77
|
const dataLabel = (value) => {
|
|
79
78
|
if (Array.isArray(value)) return `${value.length} 项`
|
|
80
79
|
if (!value || typeof value !== 'object') return String(value)
|
|
@@ -88,12 +87,10 @@ const dataLabel = (value) => {
|
|
|
88
87
|
return value.id ?? value.channelId ?? value.name ?? `对象 keys=[${Object.keys(value).slice(0, 4).join(', ')}]`
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const EVENT_PAYLOAD_EVENTS = new Set(['pointerDown', 'pointerUp', 'contextMenu'])
|
|
96
|
-
const unwrapPayload = (raw, name) => (EVENT_PAYLOAD_EVENTS.has(name) && raw && typeof raw === 'object' && 'payload' in raw ? raw.payload : raw)
|
|
90
|
+
/** 一次事件里可读的那份顶层数据:不把通道 / 分组上下文再打成一个 action.data */
|
|
91
|
+
const hitDataOf = payload => payload.item ?? payload.channel ?? payload.label ?? payload.line
|
|
92
|
+
?? (payload.type === 'cluster' ? { id: payload.id, count: payload.count } : null)
|
|
93
|
+
?? (Array.isArray(payload.items) ? payload.items : null)
|
|
97
94
|
|
|
98
95
|
/** 会带 action 的事件名:* 订阅里从载荷上认出「这一次是什么操作」 */
|
|
99
96
|
const ACTION_EVENTS = new Set(['click', 'dblClick', 'pointerDown', 'pointerUp', 'contextMenu', 'labelClick', 'labelDrag', 'labelContextMenu', 'labelDblClick', 'selectChange', 'waypointChange'])
|
|
@@ -257,10 +254,8 @@ const dismissCopy = () => {
|
|
|
257
254
|
|
|
258
255
|
onMounted(() => {
|
|
259
256
|
view = createGisView(container.value, buildOptions())
|
|
260
|
-
// 一条 '*'
|
|
261
|
-
offAll = view.on('*', (
|
|
262
|
-
// 四个要 DOM 事件的事件先拆包,后面按「载荷就是回调里那一份」统一读
|
|
263
|
-
const payload = unwrapPayload(raw, name)
|
|
257
|
+
// 一条 '*' 订阅全部事件:回调拿到的第二参就是事件名;载荷与 options 里的 onXxx 完全同一份
|
|
258
|
+
offAll = view.on('*', (payload, name) => {
|
|
264
259
|
counts.value = { ...counts.value, [name]: (counts.value[name] ?? 0) + 1 }
|
|
265
260
|
// 合并 / 复制:同一条 * 订阅里收中间态,确认条与日志读的是同一份载荷
|
|
266
261
|
if (name === 'pointMerge') {
|
|
@@ -275,8 +270,10 @@ onMounted(() => {
|
|
|
275
270
|
if (ACTION_EVENTS.has(name) && payload?.action) {
|
|
276
271
|
// 一次操作的判读:什么操作 / 点到了什么 / 拿到了什么数据 —— 左键与右键都走这一套
|
|
277
272
|
const action = payload.action
|
|
278
|
-
const
|
|
279
|
-
|
|
273
|
+
const event = payload.originalEvent
|
|
274
|
+
const held = [event?.ctrlKey && 'ctrl', event?.metaKey && 'meta', event?.shiftKey && 'shift', event?.altKey && 'alt'].filter(Boolean).join('+')
|
|
275
|
+
const data = hitDataOf(payload)
|
|
276
|
+
lastAction.value = `${name}:${action.type} → ${action.target}${data === null || data === undefined ? '' : ` · ${dataLabel(data)}`}${event ? ` · button ${event.button}` : ''}${held ? ` · ${held}` : ''}${action.moved ? ' · 拖动过' : ''}${action.double ? ' · 双击第二下' : ''}`
|
|
280
277
|
}
|
|
281
278
|
if (!showHighFrequency.value && HIGH_FREQUENCY.has(name)) {
|
|
282
279
|
return
|
|
@@ -350,7 +347,7 @@ onBeforeUnmount(() => {
|
|
|
350
347
|
<div class="dv-group">
|
|
351
348
|
<h3>动作判读(action)</h3>
|
|
352
349
|
<p class="dv-readout">{{ lastAction }}</p>
|
|
353
|
-
<p class="dv-hint">在画布上点一下 / 右键 / 拖一段 /
|
|
350
|
+
<p class="dv-hint">在画布上点一下 / 右键 / 拖一段 / 框选一下:action.type 说清「是什么操作」、action.target 说清「点到了什么」,命中的数据直接读载荷顶层的 item / channel / label 等字段;按键与修饰键读 originalEvent。</p>
|
|
354
351
|
</div>
|
|
355
352
|
<div class="dv-group">
|
|
356
353
|
<h3>事件计数(* 订阅)</h3>
|
package/docs/demos/LabelDemo.vue
CHANGED
|
@@ -338,11 +338,11 @@ const onLabelDrag = (payload) => {
|
|
|
338
338
|
const onLabelContextMenu = (payload) => {
|
|
339
339
|
hit.value = payload
|
|
340
340
|
syncFormFromHit()
|
|
341
|
-
//
|
|
342
|
-
menu.open = true
|
|
343
|
-
menu.x = payload.clientX
|
|
344
|
-
menu.y = payload.clientY
|
|
345
|
-
pushLog('labelContextMenu', `右键 ${describeLabel(payload)} @ ${
|
|
341
|
+
// 用原始指针事件的 clientX / clientY 把自绘菜单贴在指针位置(系统菜单已被 preventContextMenu 收掉)
|
|
342
|
+
menu.open = true
|
|
343
|
+
menu.x = payload.originalEvent?.clientX ?? 0
|
|
344
|
+
menu.y = payload.originalEvent?.clientY ?? 0
|
|
345
|
+
pushLog('labelContextMenu', `右键 ${describeLabel(payload)} @ ${menu.x}, ${menu.y}`)
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
const onLabelDblClick = (payload) => {
|
|
@@ -477,8 +477,8 @@ onMounted(() => {
|
|
|
477
477
|
pushLog('labelChange', describeChange(payload))
|
|
478
478
|
syncLabelStates()
|
|
479
479
|
}],
|
|
480
|
-
//
|
|
481
|
-
['selectChange', () => syncSelection()]
|
|
480
|
+
// 选中镜像统一走 selectChange:覆盖点击与接口来源,且重复设同一份选中集不会重复通知
|
|
481
|
+
['selectChange', () => syncSelection()]
|
|
482
482
|
].map(([name, handler]) => view.on(name, handler))
|
|
483
483
|
window.addEventListener('keydown', onKeyDown)
|
|
484
484
|
window.addEventListener('pointerdown', onGlobalPointerDown)
|
package/docs/demos/StyleDemo.vue
CHANGED
|
@@ -86,7 +86,7 @@ const buildOptions = () => ({
|
|
|
86
86
|
syncRules()
|
|
87
87
|
},
|
|
88
88
|
/**
|
|
89
|
-
* 读数接 onSelectChange 而不是 onSelect:前者对「点选、框选、API
|
|
89
|
+
* 读数接 onSelectChange 而不是 onSelect:前者对「点选、框选、API selectFeatures()」都会发,
|
|
90
90
|
* 后者只代表画布上的那一次点击 —— 用接口选中时读数也不会停在「未选中」。
|
|
91
91
|
* 选中高亮的颜色 / 尺寸写在 pointSelectStyle / lineSelectStyle 里,这里只让面板知道选中集变了。
|
|
92
92
|
*/
|