gis-view-webgl2 0.1.3 → 0.1.5

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 (44) hide show
  1. package/README.md +17 -14
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +252 -37
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +79 -0
  6. package/docs/GisView.md +53 -53
  7. 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 +66 -0
  8. package/docs/api/index.html +22 -8
  9. package/docs/demos/ApiCoverageDemo.vue +1 -1
  10. package/docs/demos/ConnectDemo.vue +114 -3
  11. package/docs/demos/ExtensionDemo.vue +69 -12
  12. package/docs/demos/MinimapDemo.vue +62 -16
  13. package/docs/demos/SearchFilterDemo.vue +13 -11
  14. package/docs/demos/api-index.js +26 -21
  15. package/docs/demos/demos.js +1 -1
  16. package/docs/demos/gis-view-demo/GisViewDemoCardHeader.vue +1 -0
  17. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +25 -16
  18. package/docs/demos/gis-view-demo/panels/OpsPanels.vue +2 -2
  19. package/docs/demos/gis-view-demo/useGisViewDemo.js +36 -25
  20. package/docs/index.html +41 -4
  21. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +6 -5
  22. package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +5 -3
  23. package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +12 -0
  24. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +29 -6
  25. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +7 -6
  26. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +24 -3
  27. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +1 -1
  28. 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 +423 -3
  29. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +169 -62
  30. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +32 -3
  31. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -8
  32. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +13 -9
  33. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +156 -12
  34. 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 +3 -3
  35. 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 +11 -5
  36. package/docs/steps/README.md +8 -8
  37. package/docs/steps/selftest-demos.html +173 -12
  38. package/docs/steps/selftest-docs.html +33 -4
  39. package/docs/steps/selftest-features.html +91 -10
  40. package/docs/steps/selftest-full.html +315 -12
  41. package/docs/steps/selftest-perf.html +39 -1
  42. package/docs/steps/selftest-scale.html +98 -0
  43. package/docs/steps/selftest-select.html +90 -0
  44. package/package.json +1 -1
@@ -28,6 +28,13 @@ const arrow = ref(true)
28
28
  const arrowSize = ref(12)
29
29
  const arrowPositionsText = ref('0.5')
30
30
  const circuitSpacing = ref(12)
31
+ const startPointId = ref('')
32
+ const dotsVisible = ref(true)
33
+ const dotSize = ref(5)
34
+ const startDotColor = ref('#2f6fd0')
35
+ const endDotColor = ref('#d9544d')
36
+ const dotShape = ref('circle')
37
+ const draftLayer = ref('above')
31
38
  const draft = ref(null)
32
39
  const actionText = ref('配置好线条之后点「开始连线」,依次点两个节点')
33
40
  const dataText = ref('正在创建视图…')
@@ -72,6 +79,10 @@ const createInitialLines = () => {
72
79
  const isChannel = computed(() => linkKind.value !== 'line')
73
80
  const circuits = computed(() => linkKind.value === 'channel-3' ? 3 : linkKind.value === 'channel-2' ? 2 : 1)
74
81
  const arrowPositions = computed(() => arrowPositionsText.value.split(',').map(value => Number(value.trim())).filter(value => Number.isFinite(value)))
82
+ const startPointLabel = computed(() => {
83
+ const point = points.find(item => String(item.id) === String(startPointId.value))
84
+ return point ? point.name ?? String(point.id) : ''
85
+ })
75
86
  const isDraftLive = computed(() => Boolean(draft.value) && ['start', 'from', 'ready', 'restart', 'pick'].includes(draft.value.phase))
76
87
  const canFinish = computed(() => draft.value?.phase === 'ready')
77
88
  const draftText = computed(() => {
@@ -110,10 +121,19 @@ const currentDraftStyle = () => ({
110
121
  color: lineColor.value,
111
122
  width: lineWidth.value,
112
123
  dot: 4,
124
+ dots: dotsVisible.value
125
+ ? {
126
+ size: dotSize.value,
127
+ shape: dotShape.value,
128
+ from: { color: startDotColor.value },
129
+ to: { color: endDotColor.value }
130
+ }
131
+ : false,
113
132
  arrow: arrow.value,
114
133
  arrowSize: arrowSize.value,
115
134
  arrowPositions: arrowPositions.value,
116
- circuitSpacing: circuitSpacing.value
135
+ circuitSpacing: circuitSpacing.value,
136
+ layer: draftLayer.value
117
137
  })
118
138
 
119
139
  /** 预览与真实落地共用同一份线条字段:颜色 / 线宽 / 方向箭头都从这里出发 */
@@ -129,6 +149,7 @@ const startDraft = () => {
129
149
  }
130
150
  view.endAddChannel()
131
151
  view.beginAddChannel({
152
+ ...(startPointId.value ? { from: startPointId.value } : {}),
132
153
  kind: isChannel.value ? 'channel' : 'line',
133
154
  circuits: circuits.value,
134
155
  routing: routing.value,
@@ -136,7 +157,9 @@ const startDraft = () => {
136
157
  channelDraftStyle: currentDraftStyle(),
137
158
  lineProps: currentLineProps()
138
159
  })
139
- actionText.value = `开始连线:先点起点,再点终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
160
+ actionText.value = startPointId.value
161
+ ? `开始连线:起点已指定为 ${startPointLabel.value},下一次点节点只选终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
162
+ : `开始连线:先点起点,再点终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
140
163
  pushLog('beginAddChannel', actionText.value)
141
164
  }
142
165
 
@@ -177,6 +200,60 @@ const commitDraft = () => {
177
200
  emit('notice', actionText.value)
178
201
  }
179
202
 
203
+ /** 直接把本次新增的线路 / 通道对象交给 commitAddChannel:不再先 addLine / addChannel */
204
+ const commitWithObject = () => {
205
+ if (!view || !canFinish.value) {
206
+ return
207
+ }
208
+ const current = view.getAddChannelDraft()
209
+ if (!current?.from || !current?.to) {
210
+ return
211
+ }
212
+ sequence += 1
213
+ const from = current.from.id
214
+ const to = current.to.id
215
+ const middle = middlePathOf(current)
216
+ const fullPath = fullPathOf(current)
217
+ let payload
218
+ if (current.kind === 'line') {
219
+ payload = {
220
+ id: `connect-object-line-${sequence}`,
221
+ name: `对象线路 ${sequence}`,
222
+ from,
223
+ to,
224
+ ...(fullPath ? { path: fullPath } : {}),
225
+ ...currentLineProps()
226
+ }
227
+ }
228
+ else {
229
+ const lineIds = Array.from({ length: current.circuits }, (_item, index) => `connect-object-line-${sequence}-${index + 1}`)
230
+ const channelId = `connect-object-channel-${sequence}`
231
+ payload = {
232
+ id: channelId,
233
+ name: `对象通道 ${sequence}`,
234
+ from,
235
+ to,
236
+ lineIds,
237
+ ...(middle ? { path: middle } : {}),
238
+ groups: [{ id: channelId, lineIds, ...(middle ? { path: middle } : {}) }]
239
+ }
240
+ }
241
+ const result = view.commitAddChannel(payload)
242
+ refreshState()
243
+ if (!result.ok) {
244
+ actionText.value = `commitAddChannel(对象) 没有落地:${result.reason ?? '未知原因'}`
245
+ }
246
+ else {
247
+ const lines = result.lines?.length ?? (result.line ? 1 : 0)
248
+ actionText.value = `commitAddChannel(对象) → ${result.kind === 'line' ? '线路对象' : '通道对象'} ${String(result.channelId ?? result.lineId ?? '')} · ${lines} 回线`
249
+ if (result.lineIds?.length) {
250
+ view.selectFeatures(result.lineIds)
251
+ }
252
+ }
253
+ pushLog('commitAddChannel(对象)', actionText.value)
254
+ emit('notice', actionText.value)
255
+ }
256
+
180
257
  const middlePathOf = current => (current.path?.length ? current.path.map(point => [point[0], point[1]]) : null)
181
258
  const fullPathOf = current => {
182
259
  const middle = middlePathOf(current)
@@ -267,7 +344,7 @@ const resetScene = () => {
267
344
  pushLog('reset', actionText.value)
268
345
  }
269
346
 
270
- watch([lineColor, lineWidth, arrow, arrowSize, arrowPositionsText, circuitSpacing], () => {
347
+ watch([lineColor, lineWidth, arrow, arrowSize, arrowPositionsText, circuitSpacing, dotsVisible, dotSize, startDotColor, endDotColor, dotShape, draftLayer], () => {
271
348
  if (view && isDraftLive.value) {
272
349
  view.updateAddChannelStyle(currentDraftStyle())
273
350
  draft.value = view.getAddChannelDraft()
@@ -333,6 +410,14 @@ onBeforeUnmount(() => {
333
410
  </select>
334
411
  </label>
335
412
  </div>
413
+ <div class="dv-row">
414
+ <label>指定起点
415
+ <select v-model="startPointId" data-demo="connect-start-point" :disabled="isDraftLive">
416
+ <option value="">点击时再选</option>
417
+ <option v-for="point in points" :key="String(point.id)" :value="String(point.id)">{{ point.name ?? point.id }}</option>
418
+ </select>
419
+ </label>
420
+ </div>
336
421
  <div class="dv-row">
337
422
  <label>线色 <input v-model="lineColor" type="color" data-demo="connect-color" /></label>
338
423
  <label>线宽 <input v-model.number="lineWidth" type="range" min="1" max="6" step="0.2" data-demo="connect-width" /> {{ lineWidth.toFixed(1) }}</label>
@@ -356,6 +441,31 @@ onBeforeUnmount(() => {
356
441
  <p class="dv-hint">草稿样式直接传给 <code>beginAddChannel({ channelDraftStyle })</code>;连线期间改颜色 / 线宽 / 箭头位置 / 回线间距会走 <code>updateAddChannelStyle()</code>,预览立即更新。正交折线会自动补一个 90 度拐点;落地线路用完整 path,通道只写中间拐点。</p>
357
442
  </div>
358
443
 
444
+ <div class="dv-group">
445
+ <h3>示例点与层级</h3>
446
+ <div class="dv-row">
447
+ <label><input v-model="dotsVisible" type="checkbox" data-demo="connect-dots-visible" /> 显示两端示例点</label>
448
+ <label>形状
449
+ <select v-model="dotShape" data-demo="connect-dot-shape">
450
+ <option value="circle">圆形</option>
451
+ <option value="square">方形</option>
452
+ </select>
453
+ </label>
454
+ <label>尺寸 <input v-model.number="dotSize" type="range" min="2" max="12" step="1" data-demo="connect-dot-size" /> {{ dotSize }}px</label>
455
+ </div>
456
+ <div class="dv-row">
457
+ <label>起点色 <input v-model="startDotColor" type="color" data-demo="connect-dot-from-color" :disabled="!dotsVisible" /></label>
458
+ <label>终点色 <input v-model="endDotColor" type="color" data-demo="connect-dot-to-color" :disabled="!dotsVisible" /></label>
459
+ <label>示例线路层级
460
+ <select v-model="draftLayer" data-demo="connect-draft-layer">
461
+ <option value="above">真实节点之上</option>
462
+ <option value="below-points">真实节点之下</option>
463
+ </select>
464
+ </label>
465
+ </div>
466
+ <p class="dv-hint">示例点始终落在被点击节点的正中心(多回线偏移不会带走它),可通过 <code>dots: false</code> 关闭,或用 <code>dots.from / dots.to</code> 分别配颜色、尺寸与形状。层级只作用于示例线 / 箭头 / <code>customDraw</code>;示例点仍画在真实节点之后,保证看得见。</p>
467
+ </div>
468
+
359
469
  <div class="dv-group">
360
470
  <h3>草稿与落地</h3>
361
471
  <div class="dv-row">
@@ -364,6 +474,7 @@ onBeforeUnmount(() => {
364
474
  </div>
365
475
  <div class="dv-row">
366
476
  <button type="button" class="dv-button" data-demo="connect-default" :disabled="!canFinish" @click="commitDraft">默认添加(commitAddChannel)</button>
477
+ <button type="button" class="dv-button" data-demo="connect-commit-object" :disabled="!canFinish" @click="commitWithObject">对象提交(通道 / 线路对象)</button>
367
478
  <button type="button" class="dv-button" data-demo="connect-add-lines" :disabled="!canFinish" @click="addWithLines">用 addLines 添加</button>
368
479
  <button type="button" class="dv-button" data-demo="connect-add-channels" :disabled="!canFinish" @click="addWithChannels">用 addChannels 添加</button>
369
480
  </div>
@@ -1,8 +1,8 @@
1
1
  <script setup>
2
2
  /**
3
- * 扩展能力:用同一个视图把五个扩展点串起来 ——
3
+ * 扩展能力:用同一个视图把六个扩展点串起来 ——
4
4
  * 运行时插件(view.use)、自定义图元(registerSymbol)、自定义条件运算符(registerOperator)
5
- * 自定义导出格式(defineExportFormat)与 WebGL 自定义层(addWebGLLayer)。
5
+ * 自定义导出格式(defineExportFormat)、WebGL 自定义层(addWebGLLayer)与实例命令(registerCommand)。
6
6
  * 每一块都可单独开关,读数里能直接看到注册表与可见数据。
7
7
  */
8
8
  import { onBeforeUnmount, onMounted, ref } from 'vue'
@@ -20,6 +20,8 @@ const webglMounted = ref(false)
20
20
  const webglEnabled = ref(false)
21
21
  const webglOrder = ref(20)
22
22
  const webglDrawCount = ref(0)
23
+ const commandRegistered = ref(false)
24
+ const commandReadout = ref('实例命令尚未注册。注册后可以执行 focusHighestLoad。')
23
25
  const readout = ref('正在创建视图…')
24
26
  const exportText = ref('尚未导出。点上面的按钮,会走 defineExportFormat 注册的同步适配器。')
25
27
 
@@ -33,6 +35,7 @@ let disposeSymbol = null
33
35
  let disposePlugin = null
34
36
  let disposeExport = null
35
37
  let disposeWebgl = null
38
+ let disposeCommand = null
36
39
 
37
40
  /** WebGL 自定义层:按 gl 缓存程序 / 缓冲,draw 里只更新 uniform 与画一笔 */
38
41
  const webglResources = new WeakMap()
@@ -172,18 +175,21 @@ const paintSparkDiamond = (context, info) => {
172
175
  context.fill()
173
176
  }
174
177
 
175
- const visiblePointCount = () => view?.getVisibleData()?.points?.length ?? points.length
176
- const refreshReadout = () => {
178
+ const visiblePointCount = () => view?.getVisibleData()?.points?.length ?? points.length
179
+ const COMMAND_NAME = 'focusHighestLoad'
180
+ const refreshReadout = () => {
177
181
  if (!view) {
178
182
  return
179
183
  }
180
184
  const operators = view.listOperators()
181
185
  const symbols = view.listSymbols()
186
+ const commands = view.listCommands()
182
187
  readout.value = [
183
188
  `自定义运算符 ${operators.includes('evenIndex') ? 'evenIndex 已注册' : '未注册'}`,
184
189
  `筛选 ${operatorOn.value ? '开' : '关'}(可见 ${visiblePointCount()} / ${points.length} 个)`,
185
190
  `自定义图元 ${symbols.includes('spark') ? `spark 已注册(${symbolVariant.value === 'diamond' ? '菱形' : '十字'})` : '未注册'}${symbolOn.value ? ' / 使用中' : ''}`,
186
191
  `运行时插件 ${pluginOn.value ? '已挂载' : '未挂载'}`,
192
+ `实例命令 ${commands.includes(COMMAND_NAME) ? `${COMMAND_NAME} 已注册(${commands.length} 个)` : '未注册'}`,
187
193
  `WebGL 上下文 ${view.gl ? '可直接取' : '不可用'}`,
188
194
  `WebGL 层 ${webglMounted.value ? `已挂载 · ${webglEnabled.value ? '绘制中' : '已隐藏'} · order ${webglOrder.value} · draw ${webglDrawCount.value} 次` : '未挂载'}`
189
195
  ].join(' · ')
@@ -199,7 +205,7 @@ const buildOptions = () => ({
199
205
  fitPadding: 40,
200
206
  features: 'view',
201
207
  label: { enabled: true, content: '{name}', position: 'top', gap: 7, maxCount: 20, style: { color: '#27405f', fontSize: '11px' } },
202
- onReady: () => emit('notice', '扩展能力:已挂载 5 个扩展点')
208
+ onReady: () => emit('notice', '扩展能力:已挂载 6 个扩展点')
203
209
  })
204
210
 
205
211
  const toggleOperator = () => {
@@ -249,10 +255,47 @@ const togglePlugin = () => {
249
255
  disposePlugin?.()
250
256
  disposePlugin = null
251
257
  }
252
- refreshReadout()
258
+ refreshReadout()
253
259
  emit('notice', pluginOn.value ? '插件已挂载:左下角出现徽标' : '插件已卸载')
254
260
  }
255
261
 
262
+ /** 实例命令:业务侧把「选中最重载节点并定位」注册进视图;handler 里通过 context.view 继续调实例方法 */
263
+ const toggleCommand = () => {
264
+ if (!view) {
265
+ return
266
+ }
267
+ if (commandRegistered.value) {
268
+ disposeCommand?.()
269
+ disposeCommand = null
270
+ commandRegistered.value = false
271
+ commandReadout.value = `已卸载 ${COMMAND_NAME};listCommands() 里不再有它。`
272
+ refreshReadout()
273
+ emit('notice', `实例命令 ${COMMAND_NAME} 已卸载`)
274
+ return
275
+ }
276
+ disposeCommand = view.registerCommand(COMMAND_NAME, (payload, context) => {
277
+ const threshold = Number(payload?.threshold ?? 1)
278
+ const target = points.find(point => Number(point.max_loading_rate) >= threshold) ?? points[0]
279
+ context.view.focusOn(target.id, { select: true, zoom: true, maxZoom: 8 })
280
+ return { id: target.id, name: target.name, threshold, selected: context.view.getSelectionData().count }
281
+ })
282
+ commandRegistered.value = view.listCommands().includes(COMMAND_NAME)
283
+ commandReadout.value = `${COMMAND_NAME} 已注册,可执行;同名再注册会覆盖。`
284
+ refreshReadout()
285
+ emit('notice', `实例命令 ${COMMAND_NAME} 已注册`)
286
+ }
287
+ const runCommand = () => {
288
+ if (!view) {
289
+ return
290
+ }
291
+ const result = view.executeCommand(COMMAND_NAME, { threshold: 1 })
292
+ commandReadout.value = result.ok
293
+ ? `executeCommand -> ok: true · value: ${JSON.stringify(result.value)}`
294
+ : `executeCommand -> ok: false · ${result.reason ?? '未知原因'}`
295
+ refreshReadout()
296
+ emit('notice', result.ok ? `实例命令已执行:${result.value?.name ?? COMMAND_NAME}` : `实例命令执行失败:${result.reason ?? '未知原因'}`)
297
+ }
298
+
256
299
  /** 挂载 / 移除 WebGL 自定义层:层本身不改数据,只在每帧最后叠加一圈光环 */
257
300
  const toggleWebgl = () => {
258
301
  if (!view) {
@@ -350,6 +393,8 @@ onBeforeUnmount(() => {
350
393
  disposeExport = null
351
394
  disposeWebgl?.()
352
395
  disposeWebgl = null
396
+ disposeCommand?.()
397
+ disposeCommand = null
353
398
  view?.destroy()
354
399
  view = null
355
400
  })
@@ -365,7 +410,7 @@ onBeforeUnmount(() => {
365
410
  </div>
366
411
  <aside class="dv-panel">
367
412
  <div class="dv-group">
368
- <h3>五个扩展点</h3>
413
+ <h3>六个扩展点</h3>
369
414
  <div class="dv-row">
370
415
  <button
371
416
  type="button"
@@ -400,6 +445,17 @@ onBeforeUnmount(() => {
400
445
  <div class="dv-row">
401
446
  <button type="button" class="dv-button dv-button--primary" data-demo="extension-export" @click="exportSummary">自定义格式导出</button>
402
447
  </div>
448
+ <div class="dv-row">
449
+ <button
450
+ type="button"
451
+ class="dv-button"
452
+ :class="{ 'dv-button--primary': commandRegistered }"
453
+ :aria-pressed="commandRegistered"
454
+ data-demo="extension-command-register"
455
+ @click="toggleCommand"
456
+ >{{ commandRegistered ? '卸载实例命令' : '注册实例命令' }}</button>
457
+ <button type="button" class="dv-button" :disabled="!commandRegistered" data-demo="extension-command-run" @click="runCommand">执行命令</button>
458
+ </div>
403
459
  <div class="dv-row">
404
460
  <button
405
461
  type="button"
@@ -415,12 +471,13 @@ onBeforeUnmount(() => {
415
471
  <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-order" @click="cycleWebglOrder">order {{ webglOrder }}(切换)</button>
416
472
  <button type="button" class="dv-button" :disabled="!webglMounted" data-demo="extension-webgl-request" @click="requestWebglFrame">requestRender()</button>
417
473
  </div>
418
- <p class="dv-hint">运算符注册进条件语言,图元注册进符号表,插件挂在实例上,导出格式挂在模块注册表,WebGL 层直接拿 <code>gl</code> 在内置内容之后画自己的东西;五者都不需要改组件源码。图元切换走一次 <code>patchPoints</code> 批量补丁,WebGL 层可用 <code>enabled</code> / <code>order</code> 原地维护。</p>
419
- </div>
420
- <div class="dv-group">
421
- <h3>注册表读数</h3>
422
- <p class="dv-readout" data-demo="extension-readout" aria-live="polite">{{ readout }}</p>
474
+ <p class="dv-hint">运算符注册进条件语言,图元注册进符号表,插件挂在实例上,导出格式挂在模块注册表,WebGL 层直接拿 <code>gl</code> 在内置内容之后画自己的东西,实例命令把业务动作注册进当前视图;六者都不需要改组件源码。图元切换走一次 <code>patchPoints</code> 批量补丁,WebGL 层可用 <code>enabled</code> / <code>order</code> 原地维护,命令通过 <code>executeCommand</code> 同步执行并返回 handler 的结果。</p>
423
475
  </div>
476
+ <div class="dv-group">
477
+ <h3>注册表读数</h3>
478
+ <p class="dv-readout" data-demo="extension-readout" aria-live="polite">{{ readout }}</p>
479
+ <p class="dv-readout" data-demo="extension-command-readout" aria-live="polite">{{ commandReadout }}</p>
480
+ </div>
424
481
  <div class="dv-group">
425
482
  <h3>导出结果</h3>
426
483
  <pre class="dv-readout demo-extension-export" data-demo="extension-export-text">{{ exportText }}</pre>
@@ -17,12 +17,17 @@ const enabled = ref(true)
17
17
  const size = ref('medium')
18
18
  const position = ref('bottom-right')
19
19
  const extent = ref('auto')
20
- const ruleMode = ref('voltage')
20
+ const ruleMode = ref('inherit')
21
21
  const levelMode = ref('all')
22
- const linkedSizes = ref(true)
22
+ const pointSizeScale = ref(1)
23
+ const lineWidthScale = ref(1)
24
+ const pointZoomScale = ref(0)
25
+ const lineZoomScale = ref(0)
23
26
  const channelCircuits = ref(true)
24
- const zoomable = ref(true)
27
+ const zoomMode = ref('manual')
28
+ const followZoomScale = ref(1)
25
29
  const dragAction = ref('pan')
30
+ const extentFollowCamera = ref(false)
26
31
  const maxPixelRatio = ref('2')
27
32
  const gridPlacement = ref('below-data')
28
33
  const customLayers = ref(true)
@@ -125,6 +130,7 @@ const minimapOptions = () => {
125
130
  margin: 12,
126
131
  padding: 0.05,
127
132
  extent: extent.value,
133
+ extentFollowCamera: extentFollowCamera.value,
128
134
  background: 'inherit',
129
135
  backgroundStyle: 'rgba(255, 255, 255, 0.94)',
130
136
  borderColor: '#c7d8eb',
@@ -140,16 +146,19 @@ const minimapOptions = () => {
140
146
  levels: levelMode.value === 'top' ? [2] : undefined,
141
147
  cascade: 'both',
142
148
  layered: true,
143
- pointSizeScale: 1,
144
- lineWidthScale: 1,
145
- pointMapZoomScale: linkedSizes.value ? 0.5 : 0,
146
- lineMapZoomScale: linkedSizes.value ? 0.5 : 0,
149
+ // 自己规则 / 沿用主图都乘这一份;与主图相机倍数、小图自身倍数都无关
150
+ pointSizeScale: Number(pointSizeScale.value),
151
+ lineWidthScale: Number(lineWidthScale.value),
152
+ // 只有小图自身放大时才按这两个指数联动;沿用主图的节点 / 线路也一样
153
+ pointZoomScale: Number(pointZoomScale.value),
154
+ lineZoomScale: Number(lineZoomScale.value),
147
155
  boxStyle: { fill: '#3678bf', fillOpacity: 0.16, stroke: '#3678bf', strokeWidth: 1, strokeStyle: 'dashed', radius: 3 },
148
156
  selectStyle: { fill: '#f0a43c', stroke: '#f0a43c' },
149
157
  interactive: true,
150
158
  onInteraction: handleMinimapInteraction,
151
159
  longPressDelay: 180,
152
- zoomable: zoomable.value,
160
+ zoomable: zoomMode.value === 'manual' ? true : zoomMode.value === 'follow' ? 'follow' : false,
161
+ followZoomScale: Number(followZoomScale.value),
153
162
  minZoom: 0.7,
154
163
  maxZoom: 6,
155
164
  zoomStep: 1.35,
@@ -168,10 +177,14 @@ const refreshReadout = () => {
168
177
  const sizeText = root ? `${Math.round(root.getBoundingClientRect().width)}×${Math.round(root.getBoundingClientRect().height)}` : '未挂载'
169
178
  readout.value = `${sizeText} · 备份 ${canvas?.width ?? 0}×${canvas?.height ?? 0} · 像素比 ${ratio ? ratio.toFixed(2) : '—'} · 范围 ${extent.value} · 自定义层 ${options.layers.length}`
170
179
  if (view) {
171
- zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomable.value ? '滚轮缩放小图' : '滚轮缩放主图'}`
180
+ zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomModeLabel.value}`
172
181
  }
173
182
  }
174
183
 
184
+ const zoomModeLabel = computed(() => zoomMode.value === 'manual'
185
+ ? '手动缩放小图'
186
+ : zoomMode.value === 'follow' ? `跟随主图(指数 ${Number(followZoomScale.value).toFixed(1)})` : '不缩放小图,滚轮缩放主图')
187
+
175
188
  const applyMinimap = () => {
176
189
  if (!view) {
177
190
  return
@@ -193,10 +206,20 @@ const zoomMinimap = (factor) => {
193
206
 
194
207
  const refreshZoom = () => {
195
208
  if (view) {
196
- zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomable.value ? '滚轮缩放小图' : '滚轮缩放主图'}`
209
+ zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomModeLabel.value}`
197
210
  }
198
211
  }
199
212
 
213
+ /** 拖拽演示:框选不依赖 zoomable;平移才需要手动缩放后内容有可移动余量 */
214
+ const showDragDemo = (action) => {
215
+ dragAction.value = action
216
+ zoomMode.value = action === 'box' ? 'off' : 'manual'
217
+ applyMinimap()
218
+ eventText.value = action === 'box'
219
+ ? '拖拽演示:缩放模式已关闭,在小图空白处直接拖动仍能拉出框选矩形'
220
+ : '拖拽演示:已切到手动缩放,在小图空白处拖动平移小图内容'
221
+ }
222
+
200
223
  const refreshLayerReadout = () => {
201
224
  layerReadout.value = `网格层 ${layerPaints.grid} 次(${layerPaints.gridStage}) · 指北针层 ${layerPaints.compass} 次`
202
225
  refreshReadout()
@@ -289,6 +312,9 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
289
312
  </select>
290
313
  </label>
291
314
  </div>
315
+ <div class="dv-row">
316
+ <label><input v-model="extentFollowCamera" type="checkbox" data-demo="minimap-extent-follow" @change="applyMinimap" /> 自动范围随相机收窄(默认关)</label>
317
+ </div>
292
318
  <p class="dv-hint"><code>maxPixelRatio</code> 越大越清晰,也越吃填充率;高分屏按机器档位调到 2 通常就够。</p>
293
319
  </div>
294
320
  <div class="dv-group">
@@ -309,11 +335,24 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
309
335
  </label>
310
336
  </div>
311
337
  <div class="dv-row">
312
- <label><input v-model="linkedSizes" type="checkbox" data-demo="minimap-linked" @change="applyMinimap" /> 尺寸跟随主图缩放</label>
338
+ <label>节点整体倍数 <input v-model.number="pointSizeScale" type="range" min="0" max="3" step="0.1" data-demo="minimap-point-size-scale" @change="applyMinimap" /> {{ Number(pointSizeScale).toFixed(1) }}</label>
339
+ <label>线路整体倍数 <input v-model.number="lineWidthScale" type="range" min="0" max="3" step="0.1" data-demo="minimap-line-width-scale" @change="applyMinimap" /> {{ Number(lineWidthScale).toFixed(1) }}</label>
340
+ </div>
341
+ <div class="dv-row">
342
+ <label>小图放大节点 <input v-model.number="pointZoomScale" type="range" min="0" max="1" step="0.1" data-demo="minimap-point-zoom-scale" @change="applyMinimap" /> {{ Number(pointZoomScale).toFixed(1) }}</label>
343
+ <label>小图放大线路 <input v-model.number="lineZoomScale" type="range" min="0" max="1" step="0.1" data-demo="minimap-line-zoom-scale" @change="applyMinimap" /> {{ Number(lineZoomScale).toFixed(1) }}</label>
344
+ </div>
345
+ <div class="dv-row">
313
346
  <label><input v-model="channelCircuits" type="checkbox" data-demo="minimap-circuits" @change="applyMinimap" /> 画全部回线</label>
314
347
  </div>
315
348
  <div class="dv-row">
316
- <label><input v-model="zoomable" type="checkbox" data-demo="minimap-zoomable" @change="applyMinimap" /> 小图自身可缩放</label>
349
+ <label>缩放模式
350
+ <select v-model="zoomMode" data-demo="minimap-zoomable" @change="applyMinimap">
351
+ <option value="off">关闭</option>
352
+ <option value="manual">手动缩放小图</option>
353
+ <option value="follow">跟随主图</option>
354
+ </select>
355
+ </label>
317
356
  <label>空白拖动
318
357
  <select v-model="dragAction" data-demo="minimap-drag-action" @change="applyMinimap">
319
358
  <option value="pan">平移小图</option>
@@ -321,7 +360,14 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
321
360
  </select>
322
361
  </label>
323
362
  </div>
324
- <p class="dv-hint">打开「小图自身可缩放」后,滚轮缩放小图;按住 Ctrl / ⌘ + 滚轮仍然缩放主图。视口框可以拖,空白处长按跟随。</p>
363
+ <div class="dv-row">
364
+ <label>跟随指数 <input v-model.number="followZoomScale" type="range" min="0" max="2" step="0.1" data-demo="minimap-follow-scale" @change="applyMinimap" /> {{ Number(followZoomScale).toFixed(1) }}</label>
365
+ </div>
366
+ <div class="dv-row">
367
+ <button type="button" class="dv-button" data-demo="minimap-drag-box" @click="showDragDemo('box')">拖拽演示:框选</button>
368
+ <button type="button" class="dv-button" data-demo="minimap-drag-pan" @click="showDragDemo('pan')">拖拽演示:平移</button>
369
+ </div>
370
+ <p class="dv-hint">沿用主图规则时,节点 / 线路先按「小图视口 ÷ 主图视口」缩放,主图放大缩小不会改变它们的大小;滚轮或 + / - 放大鹰眼图时,再按 <code>pointZoomScale</code> / <code>lineZoomScale</code> 的指数变化。两个整体倍数对自有规则与沿用主图的要素都生效。<code>zoomable: true</code> 是小图手动缩放,<code>'follow'</code> 是整体范围跟随主图,两者互斥;<code>followZoomScale</code> 是主图倍数的指数。框选不依赖缩放开关:选「关闭」+ <code>dragAction: 'box'</code>,空白处拖动仍会直接框选。视口框可以拖,空白处长按跟随。</p>
325
371
  </div>
326
372
  <div class="dv-group">
327
373
  <h3>扩展层</h3>
@@ -341,9 +387,9 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
341
387
  <div class="dv-group">
342
388
  <h3>小图倍数</h3>
343
389
  <div class="dv-row">
344
- <button type="button" class="dv-button" data-demo="minimap-zoom-out" @click="zoomMinimap(1 / 1.35)">-</button>
345
- <button type="button" class="dv-button dv-button--primary" data-demo="minimap-zoom-reset" @click="resetZoom">1×</button>
346
- <button type="button" class="dv-button" data-demo="minimap-zoom-in" @click="zoomMinimap(1.35)">+</button>
390
+ <button type="button" class="dv-button" data-demo="minimap-zoom-out" :disabled="zoomMode === 'follow'" @click="zoomMinimap(1 / 1.35)">-</button>
391
+ <button type="button" class="dv-button dv-button--primary" data-demo="minimap-zoom-reset" :disabled="zoomMode === 'follow'" @click="resetZoom">1×</button>
392
+ <button type="button" class="dv-button" data-demo="minimap-zoom-in" :disabled="zoomMode === 'follow'" @click="zoomMinimap(1.35)">+</button>
347
393
  </div>
348
394
  <p class="dv-readout" data-demo="minimap-zoom-readout">{{ zoomReadout }}</p>
349
395
  </div>
@@ -48,10 +48,11 @@ let view = null
48
48
  let requestSeq = 0
49
49
 
50
50
  const SCOPES = [
51
- { id: 'all', label: '全部三类' },
51
+ { id: 'all', label: '全部四类' },
52
52
  { id: 'points', label: '只看节点' },
53
53
  { id: 'lines', label: '只看线路' },
54
- { id: 'channels', label: '只看通道' }
54
+ { id: 'channels', label: '只看通道' },
55
+ { id: 'groups', label: '只看分组' }
55
56
  ]
56
57
  const FIELD_MODES = [
57
58
  { id: 'all', label: '全部字段', fields: null },
@@ -64,6 +65,7 @@ const PRESETS = [
64
65
  { id: 'name', demo: 'search-filter-preset-name', label: '变电站(limit 3)', keyword: '变电站', scope: 'points', field: 'all', limit: 3 },
65
66
  { id: 'owner', demo: 'search-filter-preset-owner', label: '北城运检(props.owner)', keyword: '北城运检', scope: 'points', field: 'owner', limit: 10 },
66
67
  { id: 'channel', demo: 'search-filter-preset-channel', label: '甲通道(scope=channels)', keyword: '甲通道', scope: 'channels', field: 'all', limit: 10 },
68
+ { id: 'group', demo: 'search-filter-preset-group', label: 'channel-a(scope=groups)', keyword: 'channel-a', scope: 'groups', field: 'all', limit: 10 },
67
69
  { id: 'symbol', demo: 'search-filter-preset-symbol', label: 'square(显式 symbol)', keyword: 'square', scope: 'points', field: 'symbol', limit: 10 }
68
70
  ]
69
71
  const FILTERS = [
@@ -72,15 +74,15 @@ const FILTERS = [
72
74
  { id: 'zone', demo: 'search-filter-filter-zone', label: '分区 2 / 4', condition: { points: { zone_name: { in: ['分区 2', '分区 4'] } }, cascade: 'binding' } },
73
75
  { id: 'loading', demo: 'search-filter-filter-loading', label: '线路负载 ≥ 1', condition: { lines: { max_loading_rate: { gte: 1 } } } }
74
76
  ]
75
- const KIND_LABELS = { point: '节点', line: '线路', channel: '通道' }
77
+ const KIND_LABELS = { point: '节点', line: '线路', channel: '通道', group: '分组' }
76
78
 
77
- const scopeLabel = computed(() => SCOPES.find(item => item.id === scope.value)?.label ?? '全部三类')
79
+ const scopeLabel = computed(() => SCOPES.find(item => item.id === scope.value)?.label ?? '全部四类')
78
80
  const searchSummary = computed(() => {
79
81
  const result = searchResult.value
80
82
  if (!result) {
81
83
  return '还没有搜索'
82
84
  }
83
- return `命中 ${result.total} 条:节点 ${result.points.length} / 线路 ${result.lines.length} / 通道 ${result.channels.length}`
85
+ return `命中 ${result.total} 条:节点 ${result.points.length} / 线路 ${result.lines.length} / 通道 ${result.channels.length} / 分组 ${result.groups?.length ?? 0}`
84
86
  })
85
87
  const searchMeta = computed(() => {
86
88
  const result = searchResult.value
@@ -172,8 +174,8 @@ const applyWorker = () => {
172
174
  : 'searchWorker=false:searchAsync 退回主线程同步计算'
173
175
  }
174
176
  const focusHit = (hit) => {
175
- view?.focusOn(hit, { select: true })
176
- actionText.value = `focusOn(${KIND_LABELS[hit.kind] ?? hit.kind} #${hit.index}, { select: true })`
177
+ view?.focusOn(hit, { select: true, zoom: true })
178
+ actionText.value = `focusOn(${KIND_LABELS[hit.kind] ?? hit.kind} #${hit.index}, { select: true, zoom: true })`
177
179
  }
178
180
  const focusAllHits = () => {
179
181
  const hits = searchResult.value?.hits ?? []
@@ -181,8 +183,8 @@ const focusAllHits = () => {
181
183
  actionText.value = '当前没有可定位的命中'
182
184
  return
183
185
  }
184
- view?.focusOn(hits, { select: true })
185
- actionText.value = `focusOn(hits, { select: true }):${hits.length} 条命中一起进包围盒并选中`
186
+ view?.focusOn(hits, { select: true, zoom: true })
187
+ actionText.value = `focusOn(hits, { select: true, zoom: true }):${hits.length} 条命中一起进包围盒并选中`
186
188
  }
187
189
  const readVisibleData = () => {
188
190
  if (!view) {
@@ -296,7 +298,7 @@ onBeforeUnmount(() => {
296
298
  <div class='dv-chips'>
297
299
  <button v-for='preset in PRESETS' :key='preset.id' type='button' class='dv-chip' :data-demo='preset.demo' @click='choosePreset(preset)'>{{ preset.label }}</button>
298
300
  </div>
299
- <p class='dv-hint'>显式 fields 能搜默认会跳过的 symbol;搜索基于全量数据,不隐藏、也不自动移动视角。</p>
301
+ <p class='dv-hint'>scope 可写 points / lines / channels / groups,也认 point / line / channel / group;显式 fields 能搜默认会跳过的 symbol。搜索基于全量数据,不隐藏、也不自动移动视角。</p>
300
302
  <div class='dv-row'>
301
303
  <button type='button' class='dv-button' data-demo='search-filter-focus-all' @click='focusAllHits'>聚焦并选中全部命中</button>
302
304
  </div>
@@ -335,7 +337,7 @@ onBeforeUnmount(() => {
335
337
  </div>
336
338
  <div class='dv-group'>
337
339
  <h3>两件事别混用</h3>
338
- <p class='dv-hint'>search 只把要素找出来,结果带 bounds;要居中就再调 focusOn。setFilter 才会隐藏要素,并从 getVisibleData / listPoints 的可见集合里消失。</p>
340
+ <p class='dv-hint'>search 只把要素找出来,结果带 bounds;要居中就再调 focusOn。setFilter 只隐藏显示、不删数据,也不清空选中集:条件放宽后,原本选中的要素仍保持选中。</p>
339
341
  </div>
340
342
  </aside>
341
343
  </section>