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.
- package/README.md +14 -11
- package/dist/gis-view.css +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +445 -241
- package/dist/index.js +1 -1
- package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +65 -498
- package/docs/GisView.md +1 -1
- package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.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 +114 -478
- package/docs/demos/BasicDemo.vue +144 -27
- package/docs/demos/BatchAddDemo.vue +133 -7
- package/docs/demos/ChannelDemo.vue +61 -0
- package/docs/demos/ConnectDemo.vue +164 -33
- package/docs/demos/CopyMergeDemo.vue +144 -42
- package/docs/demos/EventDemo.vue +29 -24
- package/docs/demos/LabelDemo.vue +5 -5
- package/docs/demos/api-index.js +53 -41
- 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 +31 -33
- package/docs/format/02-/350/212/202/347/202/271/344/270/216/347/272/277/350/267/257.md +1 -0
- package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +3 -0
- 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/index.html +136 -13
- 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
- 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/03-/351/205/215/347/275/256/351/241/271.md +82 -14
- package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +23 -17
- 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
- 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 +100 -40
- package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +11 -7
- 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
- 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 +28 -5
- package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +46 -14
- package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +5 -2
- 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
- package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +30 -22
- package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +168 -81
- 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 +5 -3
- 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
- package/docs/steps/README.md +201 -120
- package/docs/steps/bench-scale.html +1 -1
- package/docs/steps/index.html +222 -6
- package/docs/steps/selftest-actions.html +26 -25
- package/docs/steps/selftest-data.html +1 -1
- package/docs/steps/selftest-demos.html +290 -16
- package/docs/steps/selftest-docs.html +17 -6
- package/docs/steps/selftest-features.html +594 -54
- package/docs/steps/selftest-full.html +3 -3
- package/docs/steps/selftest-labels.html +82 -1
- package/docs/steps/selftest-perf.html +62 -7
- package/docs/steps/selftest-select.html +71 -6
- 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/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
- package/package.json +1 -1
- 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
- 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
- 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
- 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
- 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
package/docs/demos/BasicDemo.vue
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
/**
|
|
3
|
-
* 基础视图:一份数据 +
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
3
|
+
* 基础视图:一份数据 + 最少的配置,重点看相机、比例、空闲与拾取读数怎么拿。
|
|
4
|
+
*
|
|
5
|
+
* 首屏用顶层 center / scale 落位;面板能读写比例、观察 idleTimeout 的回调与事件,
|
|
6
|
+
* 数据区四个开关(数据量 / 网格 / 节点标签 / 通道)仍只改最小配置。读数栏把
|
|
7
|
+
* onCameraChange / onClick / onSelect / onStats / onIdle 的值直接摊开 ——
|
|
8
|
+
* 事件载荷长什么样、什么时候发,一眼能对上。
|
|
9
|
+
*/
|
|
9
10
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
10
11
|
import { createGisView } from '../../src/index.js'
|
|
11
|
-
import { buildDemoData } from './shared/demoData.js'
|
|
12
|
+
import { buildDemoData, DEMO_CENTER } from './shared/demoData.js'
|
|
12
13
|
import './shared/demoStyle.css'
|
|
13
14
|
|
|
14
15
|
const emit = defineEmits(['notice'])
|
|
@@ -16,7 +17,15 @@ const container = ref(null)
|
|
|
16
17
|
const pointCount = ref(28)
|
|
17
18
|
const gridOn = ref(true)
|
|
18
19
|
const labelOn = ref(true)
|
|
19
|
-
const channelOn = ref(true)
|
|
20
|
+
const channelOn = ref(true)
|
|
21
|
+
const scaleDraft = ref('72')
|
|
22
|
+
const currentScale = ref(72)
|
|
23
|
+
const idleElapsedMs = ref(0)
|
|
24
|
+
const idleCallbackCount = ref(0)
|
|
25
|
+
const idleEventCount = ref(0)
|
|
26
|
+
const idleEnabled = ref(true)
|
|
27
|
+
const IDLE_TIMEOUT_MS = 1200
|
|
28
|
+
let idleTicker = null
|
|
20
29
|
const readouts = ref({ camera: '—', pick: '未选中', stats: '—', event: '—' })
|
|
21
30
|
let dataset = buildDemoData({ count: 28 })
|
|
22
31
|
let view = null
|
|
@@ -25,14 +34,14 @@ let statsDrawnAt = Number.NEGATIVE_INFINITY
|
|
|
25
34
|
|
|
26
35
|
/**
|
|
27
36
|
* 事件载荷压成一行:`action.target` 说清点到了哪一类(point / line / channel / empty),
|
|
28
|
-
*
|
|
37
|
+
* 命中的数据在载荷顶层(点 / 线读 item,通道读 channel,标签读 label)—— 指针事件的载荷结构见 docs/数据格式.md。
|
|
29
38
|
*/
|
|
30
39
|
const describeTarget = (payload) => {
|
|
31
40
|
if (!payload) {
|
|
32
41
|
return '空白(清除选中)'
|
|
33
42
|
}
|
|
34
43
|
const target = payload.action?.target ?? payload.kind ?? payload.type ?? '—'
|
|
35
|
-
const data = payload.
|
|
44
|
+
const data = payload.item ?? payload.channel ?? payload.label ?? payload.line ?? null
|
|
36
45
|
const id = Array.isArray(data)
|
|
37
46
|
? `${data.length} 项`
|
|
38
47
|
: data?.id ?? data?.name ?? payload.id ?? payload.index ?? '—'
|
|
@@ -66,9 +75,21 @@ const buildOptions = () => ({
|
|
|
66
75
|
showGrid: gridOn.value,
|
|
67
76
|
gridColor: 'rgba(126, 168, 214, 0.22)',
|
|
68
77
|
fitPadding: 40,
|
|
78
|
+
// 首屏相机用顶层简写:等价于 initialView: { center, scale };显式 initialView 整份优先。
|
|
79
|
+
center: [...DEMO_CENTER],
|
|
80
|
+
scale: 72,
|
|
69
81
|
// 功能开关缺省全关:这一页要能平移缩放、能悬停选中,显式打开查看类(见 docs/GisView.md 的「功能开关」)
|
|
70
82
|
features: 'view',
|
|
71
83
|
label: labelOptions(),
|
|
84
|
+
// 停手 1.2 秒触发一次 onIdle / idle;每个空闲段只触发一次。
|
|
85
|
+
idleTimeout: IDLE_TIMEOUT_MS,
|
|
86
|
+
onIdle: ({ idleTime, timeout }) => {
|
|
87
|
+
idleCallbackCount.value += 1
|
|
88
|
+
readouts.value = {
|
|
89
|
+
...readouts.value,
|
|
90
|
+
event: `onIdle:停手 ${Math.round(idleTime)} ms(阈值 ${timeout} ms)`
|
|
91
|
+
}
|
|
92
|
+
},
|
|
72
93
|
onReady: () => {
|
|
73
94
|
emit('notice', `已加载 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路`)
|
|
74
95
|
readouts.value = { ...readouts.value, event: 'ready:WebGL2 上下文就绪' }
|
|
@@ -113,9 +134,10 @@ const applyData = () => {
|
|
|
113
134
|
}
|
|
114
135
|
dataset = buildDemoData({ count: pointCount.value })
|
|
115
136
|
view.updateData(dataset.points, channelOn.value ? dataset.lines : dataset.linesWithoutChannel)
|
|
116
|
-
view.updateOptions({ channels: channelOn.value ? dataset.channels : [] })
|
|
117
|
-
view.fitView(40)
|
|
118
|
-
|
|
137
|
+
view.updateOptions({ channels: channelOn.value ? dataset.channels : [] })
|
|
138
|
+
view.fitView(40)
|
|
139
|
+
syncScaleFromView()
|
|
140
|
+
emit('notice', `已换成 ${dataset.points.length} 个节点 / ${dataset.lines.length} 条线路`)
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
/**
|
|
@@ -134,16 +156,70 @@ const applyChannel = () => {
|
|
|
134
156
|
})
|
|
135
157
|
}
|
|
136
158
|
|
|
137
|
-
const resetCamera = () => {
|
|
138
|
-
view?.fitView(40)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
const resetCamera = () => {
|
|
160
|
+
view?.fitView(40)
|
|
161
|
+
syncScaleFromView()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const zoomBy = (factor) => {
|
|
165
|
+
view?.zoomBy(factor)
|
|
166
|
+
syncScaleFromView()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const syncScaleFromView = () => {
|
|
170
|
+
if (!view) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
const scale = view.getScale()
|
|
174
|
+
currentScale.value = scale
|
|
175
|
+
scaleDraft.value = Number(scale.toFixed(2)).toString()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const readScale = () => {
|
|
179
|
+
if (!view) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const scale = view.getScale()
|
|
183
|
+
currentScale.value = scale
|
|
184
|
+
scaleDraft.value = Number(scale.toFixed(2)).toString()
|
|
185
|
+
readouts.value = { ...readouts.value, event: `getScale() → ${scale.toFixed(3)} px/单位` }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const applyScale = () => {
|
|
189
|
+
if (!view) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const scale = Number(scaleDraft.value)
|
|
193
|
+
if (!Number.isFinite(scale) || scale <= 0) {
|
|
194
|
+
readouts.value = { ...readouts.value, event: 'setScale() 只接受正数' }
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
view.setScale(scale)
|
|
198
|
+
const applied = view.getScale()
|
|
199
|
+
currentScale.value = applied
|
|
200
|
+
scaleDraft.value = Number(applied.toFixed(2)).toString()
|
|
201
|
+
readouts.value = { ...readouts.value, event: `setScale(${scale}) → ${applied.toFixed(3)} px/单位` }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const toggleIdle = () => {
|
|
205
|
+
view?.updateOptions({ idleTimeout: idleEnabled.value ? IDLE_TIMEOUT_MS : 0 })
|
|
206
|
+
idleElapsedMs.value = view?.getIdleTime() ?? 0
|
|
207
|
+
readouts.value = {
|
|
208
|
+
...readouts.value,
|
|
209
|
+
event: idleEnabled.value ? `idleTimeout 已开启(${IDLE_TIMEOUT_MS} ms)` : 'idleTimeout 已关闭(0)'
|
|
210
|
+
}
|
|
211
|
+
}
|
|
144
212
|
|
|
145
213
|
onMounted(() => {
|
|
146
214
|
view = createGisView(container.value, buildOptions())
|
|
215
|
+
syncScaleFromView()
|
|
216
|
+
view.on('idle', (payload) => {
|
|
217
|
+
idleEventCount.value += 1
|
|
218
|
+
readouts.value = {
|
|
219
|
+
...readouts.value,
|
|
220
|
+
event: `idle 事件:停手 ${Math.round(payload.idleTime)} ms(阈值 ${payload.timeout} ms)`
|
|
221
|
+
}
|
|
222
|
+
})
|
|
147
223
|
// 标签拖动:面板上给一行「偏移记在状态里」的读数,松手后偏移一直留在 getLabelStates() 里
|
|
148
224
|
view.on('labelDrag', (payload) => {
|
|
149
225
|
if (payload.phase === 'move') {
|
|
@@ -151,10 +227,17 @@ onMounted(() => {
|
|
|
151
227
|
}
|
|
152
228
|
readouts.value = { ...readouts.value, event: `labelDrag:${payload.phase} · ${payload.id ?? payload.index} 偏移 [${payload.offset.map(value => Math.round(value)).join(', ')}]` }
|
|
153
229
|
})
|
|
230
|
+
idleTicker = window.setInterval(() => {
|
|
231
|
+
if (view) {
|
|
232
|
+
idleElapsedMs.value = view.getIdleTime()
|
|
233
|
+
}
|
|
234
|
+
}, 200)
|
|
154
235
|
})
|
|
155
|
-
|
|
156
|
-
onBeforeUnmount(() => {
|
|
157
|
-
|
|
236
|
+
|
|
237
|
+
onBeforeUnmount(() => {
|
|
238
|
+
window.clearInterval(idleTicker)
|
|
239
|
+
idleTicker = null
|
|
240
|
+
view?.destroy()
|
|
158
241
|
view = null
|
|
159
242
|
})
|
|
160
243
|
</script><template>
|
|
@@ -167,9 +250,31 @@ onBeforeUnmount(() => {
|
|
|
167
250
|
<button type="button" @click="zoomBy(1 / 1.4)">缩小</button>
|
|
168
251
|
</div>
|
|
169
252
|
</div>
|
|
170
|
-
<aside class="dv-panel">
|
|
171
|
-
<div class="dv-group">
|
|
172
|
-
<h3
|
|
253
|
+
<aside class="dv-panel">
|
|
254
|
+
<div class="dv-group">
|
|
255
|
+
<h3>比例读写<span class="dv-chip">center / scale 首屏</span></h3>
|
|
256
|
+
<div class="dv-row">
|
|
257
|
+
<label>目标比例
|
|
258
|
+
<input v-model="scaleDraft" class="dv-number" data-demo="basic-scale-input" type="number" min="0.01" step="1" />
|
|
259
|
+
</label>
|
|
260
|
+
<button type="button" class="dv-button dv-button--primary" data-demo="basic-scale-set" @click="applyScale">setScale()</button>
|
|
261
|
+
<button type="button" class="dv-button" data-demo="basic-scale-get" @click="readScale">getScale()</button>
|
|
262
|
+
</div>
|
|
263
|
+
<p class="dv-readout" data-demo="basic-scale-read">当前比例:{{ currentScale.toFixed(2) }} px/单位</p>
|
|
264
|
+
<p class="dv-hint">滚轮、拖动或按钮改完相机后,onCameraChange 会把新比例与中心写回下面「读数」;setScale() 省略锚点时以视口中心缩放。</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="dv-group">
|
|
267
|
+
<h3>空闲检测<span class="dv-chip">{{ idleEnabled ? `${IDLE_TIMEOUT_MS} ms` : '已关闭' }}</span></h3>
|
|
268
|
+
<div class="dv-row">
|
|
269
|
+
<label><input v-model="idleEnabled" data-demo="basic-idle-toggle" type="checkbox" @change="toggleIdle" /> 开启 idleTimeout</label>
|
|
270
|
+
</div>
|
|
271
|
+
<p class="dv-readout" data-demo="basic-idle-read">
|
|
272
|
+
getIdleTime():{{ idleElapsedMs.toFixed(0) }} ms · onIdle:{{ idleCallbackCount }} 次 · idle 事件:{{ idleEventCount }} 次
|
|
273
|
+
</p>
|
|
274
|
+
<p class="dv-hint">在画布上移动、滚轮或按键会把计时清零;程序化调用(例如上面的 setScale)不会。停手到阈值后每条空闲段只触发一次。</p>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="dv-group">
|
|
277
|
+
<h3>数据</h3>
|
|
173
278
|
<div class="dv-row">
|
|
174
279
|
<label>节点数
|
|
175
280
|
<select v-model.number="pointCount" @change="applyData">
|
|
@@ -200,4 +305,16 @@ onBeforeUnmount(() => {
|
|
|
200
305
|
</div>
|
|
201
306
|
</aside>
|
|
202
307
|
</section>
|
|
203
|
-
</template>
|
|
308
|
+
</template>
|
|
309
|
+
|
|
310
|
+
<style scoped>
|
|
311
|
+
.dv-number {
|
|
312
|
+
width: 82px;
|
|
313
|
+
padding: 4px 6px;
|
|
314
|
+
border: 1px solid #d6e0ec;
|
|
315
|
+
border-radius: 6px;
|
|
316
|
+
background: #fff;
|
|
317
|
+
color: #1f2a44;
|
|
318
|
+
font-size: 12px;
|
|
319
|
+
}
|
|
320
|
+
</style>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 批量增删改:把「一次调用处理多条」的入口摆在一起。
|
|
4
4
|
*
|
|
5
5
|
* addPoints / addLines / addChannels 走现成数据或通道声明批量追加;addWaypoints 一次给
|
|
6
6
|
* 同一条线路插多个拐点;addChannelGroups 在一条既有通道里一次加多组回线。
|
|
7
|
+
* patchPoints / patchLines 是批量修改;removePoints / removeLines 与 batch 里的 removeChannel
|
|
8
|
+
* 是批量删除;splitChannelGroups / removeChannelGroup 在一条通道上整份重排分组。
|
|
7
9
|
* 每次操作只记一步历史、只发一条数据变更,读数里同时给出方法返回与当前总数。
|
|
8
10
|
*/
|
|
9
11
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
|
@@ -119,21 +121,21 @@ const addChannelBatch = () => runAction('addChannels', () => {
|
|
|
119
121
|
name: `批量通道 ${currentRound}-A`,
|
|
120
122
|
from: picked[0].id,
|
|
121
123
|
to: picked[2].id,
|
|
122
|
-
circuits: 2,
|
|
123
124
|
path: [[Number((middle[0] + normal[0]).toFixed(5)), Number((middle[1] + normal[1]).toFixed(5))]],
|
|
124
|
-
|
|
125
|
+
lineData: [
|
|
126
|
+
{ vn_kv: 525, max_loading_rate: 0.82 },
|
|
127
|
+
{ vn_kv: 525, max_loading_rate: 0.82 }
|
|
128
|
+
]
|
|
125
129
|
},
|
|
126
130
|
{
|
|
127
131
|
id: `batch-channel-${currentRound}-b`,
|
|
128
132
|
name: `批量通道 ${currentRound}-B`,
|
|
129
133
|
from: picked[1].id,
|
|
130
134
|
to: picked[3].id,
|
|
131
|
-
circuits: 1,
|
|
132
135
|
path: [[Number((middle[0] - normal[0]).toFixed(5)), Number((middle[1] - normal[1]).toFixed(5))]],
|
|
133
|
-
|
|
136
|
+
lineData: [{ vn_kv: 230, max_loading_rate: 0.61 }]
|
|
134
137
|
}
|
|
135
|
-
])
|
|
136
|
-
view.selectFeatures(result.lineIds)
|
|
138
|
+
], { select: true })
|
|
137
139
|
return `addChannels → ${result.count} 条通道 · ${result.channelIds.join(' / ')} · ${result.lineIds.length} 条回线`
|
|
138
140
|
})
|
|
139
141
|
|
|
@@ -211,6 +213,118 @@ const addChannelGroupBatch = () => runAction('addChannelGroups', () => {
|
|
|
211
213
|
: `addChannelGroups 没有落地:${result.reason}`
|
|
212
214
|
})
|
|
213
215
|
|
|
216
|
+
/** 一次改场站、线路、通道:三种 patch 包在同一个 batch 里,只记一步历史 */
|
|
217
|
+
const batchUpdateDemo = () => runAction('batch', () => {
|
|
218
|
+
const currentRound = nextRound()
|
|
219
|
+
const data = view.getData()
|
|
220
|
+
const points = data.points.slice(-4)
|
|
221
|
+
const batchLines = data.lines.filter(item => String(item.id).startsWith('batch-line-'))
|
|
222
|
+
const lines = (batchLines.length ? batchLines : data.lines).slice(-4)
|
|
223
|
+
const channels = view.getOptions().channels ?? []
|
|
224
|
+
let pointResult = null
|
|
225
|
+
let lineResult = null
|
|
226
|
+
view.batch(() => {
|
|
227
|
+
if (points.length) {
|
|
228
|
+
pointResult = view.patchPoints(points.map((item, index) => ({
|
|
229
|
+
id: item.id,
|
|
230
|
+
name: `${item.name ?? item.id} · 批量修改 ${currentRound}`,
|
|
231
|
+
zone_name: `批量修改 #${currentRound}`,
|
|
232
|
+
batch_updated: currentRound,
|
|
233
|
+
...(index === 0 ? { vn_kv: 1050 } : {})
|
|
234
|
+
})))
|
|
235
|
+
}
|
|
236
|
+
if (lines.length) {
|
|
237
|
+
lineResult = view.patchLines(lines.map(item => ({ id: item.id })), {
|
|
238
|
+
color: '#2f6fd0',
|
|
239
|
+
width: 2.2,
|
|
240
|
+
batch_updated: currentRound
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
for (const channel of channels) {
|
|
244
|
+
view.updateChannel(channel.id, {
|
|
245
|
+
name: `${channel.name ?? channel.id} · 批量修改 ${currentRound}`,
|
|
246
|
+
width: Math.max(Number(channel.width) || 2.2, 2.4),
|
|
247
|
+
batch_updated: currentRound
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}, { label: '批量修改场站 / 线路 / 通道' })
|
|
251
|
+
return `batch(update) → 节点 ${pointResult?.count ?? 0} / 线路 ${lineResult?.count ?? 0} / 通道 ${channels.length} · 三种 patch 合并为一步历史`
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
/** 一次删场站、线路、通道:逐条接口放进 batch,只发一条数据变更 */
|
|
255
|
+
const batchDeleteDemo = () => runAction('batch', () => {
|
|
256
|
+
const data = view.getData()
|
|
257
|
+
const pointRefs = data.points.filter(item => String(item.id).startsWith('batch-point-')).slice(-2).map(item => item.id)
|
|
258
|
+
const lineRefs = data.lines.filter(item => String(item.id).startsWith('batch-line-')).slice(-2).map(item => item.id)
|
|
259
|
+
const channelIds = (view.getOptions().channels ?? []).filter(item => String(item.id).startsWith('batch-channel-')).slice(-1).map(item => item.id)
|
|
260
|
+
if (!pointRefs.length && !lineRefs.length && !channelIds.length) {
|
|
261
|
+
return 'batch(delete):当前没有本页批量新增的数据;先点上面的 addPoints / addLines / addChannels'
|
|
262
|
+
}
|
|
263
|
+
const before = { points: data.points.length, lines: data.lines.length, channels: view.getOptions().channels?.length ?? 0 }
|
|
264
|
+
view.batch(() => {
|
|
265
|
+
for (const id of channelIds) {
|
|
266
|
+
view.removeChannel(id)
|
|
267
|
+
}
|
|
268
|
+
if (pointRefs.length) {
|
|
269
|
+
view.removePoints(pointRefs)
|
|
270
|
+
}
|
|
271
|
+
const alive = new Set(view.getData().lines.map(item => String(item.id)))
|
|
272
|
+
const remainingLines = lineRefs.filter(id => alive.has(String(id)))
|
|
273
|
+
if (remainingLines.length) {
|
|
274
|
+
view.removeLines(remainingLines)
|
|
275
|
+
}
|
|
276
|
+
}, { label: '批量删除场站 / 线路 / 通道' })
|
|
277
|
+
const after = view.getData()
|
|
278
|
+
return `batch(delete) → 节点 ${before.points}→${after.points.length} / 线路 ${before.lines}→${after.lines.length} / 通道 ${before.channels}→${view.getOptions().channels?.length ?? 0} · 整批只记一步`
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
/** 多条通道一次重排分组:每条通道都只调用一次 splitChannelGroups */
|
|
282
|
+
const batchGroupSplitDemo = () => runAction('batch', () => {
|
|
283
|
+
const channels = view.getOptions().channels ?? []
|
|
284
|
+
const before = view.getChannels().length
|
|
285
|
+
let changed = 0
|
|
286
|
+
view.batch(() => {
|
|
287
|
+
for (const channel of channels) {
|
|
288
|
+
const groups = view.getChannelGroups(channel.id)
|
|
289
|
+
if (groups.length !== 1) {
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
const lineIds = [...new Set(groups[0]?.lineIds ?? [])]
|
|
293
|
+
if (lineIds.length < 2) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
const cut = Math.max(1, Math.floor(lineIds.length / 2))
|
|
297
|
+
view.splitChannelGroups(channel.id, [lineIds.slice(0, cut), lineIds.slice(cut)])
|
|
298
|
+
changed += 1
|
|
299
|
+
}
|
|
300
|
+
}, { label: '批量重排通道分组' })
|
|
301
|
+
return changed
|
|
302
|
+
? `batch(splitChannelGroups) → ${changed} 条通道一次拆成两组 · 分组 ${before}→${view.getChannels().length} · 整批一步历史`
|
|
303
|
+
: 'batch(splitChannelGroups):没有可拆的单组通道(每条至少两条回线)'
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
/** 多条通道一次删掉最后一组:removeChannelGroup 会并回相邻组,回线本身不丢 */
|
|
307
|
+
const batchGroupRemoveDemo = () => runAction('batch', () => {
|
|
308
|
+
const channels = view.getOptions().channels ?? []
|
|
309
|
+
const before = view.getChannels().length
|
|
310
|
+
let changed = 0
|
|
311
|
+
view.batch(() => {
|
|
312
|
+
for (const channel of channels) {
|
|
313
|
+
const groups = view.getChannelGroups(channel.id)
|
|
314
|
+
if (groups.length < 2) {
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
const result = view.removeChannelGroup(channel.id, groups[groups.length - 1].id)
|
|
318
|
+
if (result.ok) {
|
|
319
|
+
changed += 1
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}, { label: '批量删除通道分组' })
|
|
323
|
+
return changed
|
|
324
|
+
? `batch(removeChannelGroup) → ${changed} 条通道各删一组并回相邻组 · 分组 ${before}→${view.getChannels().length} · 回线数据未丢`
|
|
325
|
+
: 'batch(removeChannelGroup):当前没有多组通道'
|
|
326
|
+
})
|
|
327
|
+
|
|
214
328
|
const undoLast = () => runAction('undo', () => {
|
|
215
329
|
const entry = view.undo()
|
|
216
330
|
return entry ? `undo → ${entry.label ?? '已撤销一步'}` : 'undo → 没有可撤销的步骤'
|
|
@@ -289,6 +403,18 @@ onBeforeUnmount(() => {
|
|
|
289
403
|
<p class="dv-readout" data-demo="batch-add-history">{{ historyText }}</p>
|
|
290
404
|
<p class="dv-hint">每个按钮只调一个批量方法;一次调用里的多条数据只记一步历史、只发一条 dataChange。addWaypoints 返回的是真正插入成功的坐标,addChannelGroups 返回逐组结果。</p>
|
|
291
405
|
</div>
|
|
406
|
+
<div class="dv-group">
|
|
407
|
+
<h3>批量修改 / 删除 / 分组</h3>
|
|
408
|
+
<div class="dv-row">
|
|
409
|
+
<button type="button" class="dv-button dv-button--primary" data-demo="batch-update-all" @click="batchUpdateDemo">patchPoints + patchLines + updateChannel</button>
|
|
410
|
+
<button type="button" class="dv-button dv-button--danger" data-demo="batch-delete-all" @click="batchDeleteDemo">batch(removePoints / removeLines / removeChannel)</button>
|
|
411
|
+
</div>
|
|
412
|
+
<div class="dv-row">
|
|
413
|
+
<button type="button" class="dv-button" data-demo="batch-group-split" @click="batchGroupSplitDemo">batch(splitChannelGroups ×N)</button>
|
|
414
|
+
<button type="button" class="dv-button" data-demo="batch-group-remove" @click="batchGroupRemoveDemo">batch(removeChannelGroup ×N)</button>
|
|
415
|
+
</div>
|
|
416
|
+
<p class="dv-hint">节点 / 线路本身支持一次传多条:<code>patchPoints([{ id, name }, …])</code>、<code>patchLines(ids, patch)</code>、<code>removePoints(refs)</code>、<code>removeLines(ids)</code>。通道只有单条 <code>updateChannel</code> / <code>removeChannel</code>,用 <code>view.batch(() => ids.forEach(id => view.removeChannel(id)), { label })</code> 包起来仍是一步历史、一条 <code>batch</code> 数据变更。分组侧 <code>splitChannelGroups</code> 一次换整份 groups,<code>addChannelGroups</code> 一次加多组,删组走 <code>removeChannelGroup</code>,摘回线走 <code>removeChannelLines</code>。</p>
|
|
417
|
+
</div>
|
|
292
418
|
<div class="dv-group">
|
|
293
419
|
<h3>历史</h3>
|
|
294
420
|
<div class="dv-row">
|
|
@@ -36,6 +36,13 @@ const channelSelectMode = ref('line')
|
|
|
36
36
|
/** 分组拐点拖动模式:'parallel' 共用骨架 / 'vertex' 只移同组同序号拐点 */
|
|
37
37
|
const channelWaypointDragMode = ref('parallel')
|
|
38
38
|
const waypointDragText = ref('分组拐点拖动:先点「准备两分组拐点」,再拖中间拐点比较两种模式')
|
|
39
|
+
/** 通道简略:开启后分组回线数超过 threshold 只画一条代表线 */
|
|
40
|
+
const channelSimplify = ref(false)
|
|
41
|
+
const simplifyThreshold = ref(2)
|
|
42
|
+
const simplifyColor = ref('#ff6b35')
|
|
43
|
+
const simplifyWidth = ref(4)
|
|
44
|
+
const simplifyDash = ref('dashed')
|
|
45
|
+
const simplifyText = ref('通道简略:关(reopen 后甲 / 丙通道会各折叠成一条代表线)')
|
|
39
46
|
/** 待确认的删除请求('remove' 事件的 pending 载荷):确认框照着它渲染 */
|
|
40
47
|
const removeRequest = ref(null)
|
|
41
48
|
const removeText = ref('删除:按 Delete 或点上面的按钮,先出载荷、确认之后才动数据')
|
|
@@ -309,6 +316,7 @@ const buildOptions = () => ({
|
|
|
309
316
|
channelSelectMode: channelSelectMode.value,
|
|
310
317
|
// 分组拐点拖动:共用骨架 / 只移同组同序号拐点
|
|
311
318
|
channelWaypointDragMode: channelWaypointDragMode.value,
|
|
319
|
+
channelSimplify: simplifyOption(),
|
|
312
320
|
// 删除配置:confirm 缺省(null)表示组件不弹界面、停在待确认 —— 这一页订阅 'remove' 事件自己画确认框
|
|
313
321
|
remove: { scope: 'lines' },
|
|
314
322
|
// 交互框里的「加拐点 / 删拐点」动的是分组骨架,要有这一项才不是灰的(Ctrl + 点击加点、双击删点也一起开了)
|
|
@@ -337,6 +345,37 @@ const buildOptions = () => ({
|
|
|
337
345
|
}
|
|
338
346
|
})
|
|
339
347
|
|
|
348
|
+
const simplifyOption = () => channelSimplify.value
|
|
349
|
+
? {
|
|
350
|
+
enabled: true,
|
|
351
|
+
threshold: Math.max(1, Math.floor(Number(simplifyThreshold.value) || 1)),
|
|
352
|
+
style: {
|
|
353
|
+
color: simplifyColor.value,
|
|
354
|
+
width: Math.max(0.5, Number(simplifyWidth.value) || 0.5),
|
|
355
|
+
dash: simplifyDash.value,
|
|
356
|
+
lineType: simplifyDash.value
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
: false
|
|
360
|
+
|
|
361
|
+
/** 重读简略读数:分组仍按原始成员计数,只有画布上的可见回线会折叠 */
|
|
362
|
+
const syncSimplifyReadout = () => {
|
|
363
|
+
const groups = view?.getChannelGroups() ?? []
|
|
364
|
+
const enabled = view?.getOptions().channelSimplify?.enabled === true
|
|
365
|
+
const threshold = Math.max(1, Math.floor(Number(view?.getOptions().channelSimplify?.threshold) || 1))
|
|
366
|
+
const lineCount = group => group.lineIds?.length ?? group.lineIndexes?.length ?? 0
|
|
367
|
+
const folded = enabled ? groups.filter(group => lineCount(group) > threshold) : []
|
|
368
|
+
const total = groups.reduce((sum, group) => sum + lineCount(group), 0)
|
|
369
|
+
const visible = groups.reduce((sum, group) => sum + (lineCount(group) > threshold && enabled ? 1 : lineCount(group)), 0)
|
|
370
|
+
simplifyText.value = `通道简略:${enabled ? '开' : '关'} · threshold=${threshold} · ${groups.length} 组 / ${total} 回线 → 画布 ${visible} 条${folded.length ? `(折叠 ${folded.map(group => group.id).join('、')})` : ''} · 数据与分组读数仍为原始 ${total} 条`
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const applyChannelSimplify = () => {
|
|
374
|
+
view?.updateOptions({ channelSimplify: simplifyOption() })
|
|
375
|
+
syncSimplifyReadout()
|
|
376
|
+
pushLog(simplifyText.value)
|
|
377
|
+
}
|
|
378
|
+
|
|
340
379
|
/**
|
|
341
380
|
* 通道读数:几条通道、各自几回线、每组分了几组。
|
|
342
381
|
* getChannels() 给的是**分组**那一档(一条通道拆成几组就返回几条,按 channelId 归并);
|
|
@@ -361,6 +400,7 @@ const syncReadouts = () => {
|
|
|
361
400
|
// 面板上改过分组之后,开着的右键框也要跟着对上(对不上就收掉)
|
|
362
401
|
syncMenu()
|
|
363
402
|
syncChangedChannels()
|
|
403
|
+
syncSimplifyReadout()
|
|
364
404
|
}
|
|
365
405
|
|
|
366
406
|
/** 最近变化通道:分组 / 成员 / 拐点 / 端点 / 可见性 / 其余声明字段 */
|
|
@@ -1151,6 +1191,27 @@ onBeforeUnmount(() => {
|
|
|
1151
1191
|
<p class="dv-hint">内收比例只在 inset 模式下生效;切到 tangent 时通道按切线方向整体让开。</p>
|
|
1152
1192
|
</div>
|
|
1153
1193
|
|
|
1194
|
+
<div class="dv-group">
|
|
1195
|
+
<h3>通道简略<span class="dv-chip">{{ channelSimplify ? 'on' : 'off' }}</span></h3>
|
|
1196
|
+
<div class="dv-row">
|
|
1197
|
+
<label><input v-model="channelSimplify" data-demo="channel-simplify-enabled" type="checkbox" @change="applyChannelSimplify" /> 开启简略</label>
|
|
1198
|
+
<label>阈值 <input v-model.number="simplifyThreshold" data-demo="channel-simplify-threshold" type="number" min="1" step="1" @change="applyChannelSimplify" /></label>
|
|
1199
|
+
</div>
|
|
1200
|
+
<div class="dv-row">
|
|
1201
|
+
<label>代表线颜色 <input v-model="simplifyColor" data-demo="channel-simplify-color" type="color" @change="applyChannelSimplify" /></label>
|
|
1202
|
+
<label>线宽 <input v-model.number="simplifyWidth" data-demo="channel-simplify-width" type="number" min="0.5" max="12" step="0.5" @change="applyChannelSimplify" /></label>
|
|
1203
|
+
<label>线型
|
|
1204
|
+
<select v-model="simplifyDash" data-demo="channel-simplify-dash" @change="applyChannelSimplify">
|
|
1205
|
+
<option value="solid">实线</option>
|
|
1206
|
+
<option value="dashed">虚线</option>
|
|
1207
|
+
<option value="dotted">点线</option>
|
|
1208
|
+
</select>
|
|
1209
|
+
</label>
|
|
1210
|
+
</div>
|
|
1211
|
+
<p class="dv-readout" data-demo="channel-simplify-readout">{{ simplifyText }}</p>
|
|
1212
|
+
<p class="dv-hint">分组回线数大于阈值时只画第一条可见回线,并套用这里的颜色 / 线宽 / 线型;没有 groups 的通道按一整组处理。关闭后全部恢复,getData() / 分组成员 / 导出始终是原始数据。</p>
|
|
1213
|
+
</div>
|
|
1214
|
+
|
|
1154
1215
|
<div class="dv-group">
|
|
1155
1216
|
<h3>分组</h3>
|
|
1156
1217
|
<div class="dv-row">
|