gis-view-webgl2 0.1.0 → 0.1.2

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 (87) hide show
  1. package/README.md +40 -8
  2. package/dist/gis-view.css +1 -1
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.d.ts +1036 -249
  5. package/dist/index.js +1 -1
  6. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +194 -0
  7. package/docs/GisView.md +9 -8
  8. package/docs/GisView/345/256/236/347/216/260/345/216/237/347/220/206.md +2 -2
  9. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +172 -0
  10. package/docs/api/index.html +545 -59
  11. package/docs/demos/BackendSyncDemo.vue +10 -8
  12. package/docs/demos/BasicDemo.vue +101 -101
  13. package/docs/demos/BatchAddDemo.vue +5 -5
  14. package/docs/demos/ChannelDemo.vue +617 -10
  15. package/docs/demos/ConnectDemo.vue +392 -0
  16. package/docs/demos/CopyMergeDemo.vue +980 -929
  17. package/docs/demos/DecorationDraftDemo.vue +1300 -0
  18. package/docs/demos/DemoShell.vue +569 -240
  19. package/docs/demos/DragDropDemo.vue +11 -11
  20. package/docs/demos/EffectsDemo.vue +6 -6
  21. package/docs/demos/EventDemo.vue +327 -327
  22. package/docs/demos/ExtensionDemo.vue +458 -0
  23. package/docs/demos/GisViewDemo.vue +61 -48
  24. package/docs/demos/GraphicDemo.vue +338 -0
  25. package/docs/demos/LabelDemo.vue +553 -497
  26. package/docs/demos/MinimapDemo.vue +358 -0
  27. package/docs/demos/PerfDemo.vue +29 -0
  28. package/docs/demos/SearchFilterDemo.vue +412 -0
  29. package/docs/demos/StyleDemo.vue +121 -121
  30. package/docs/demos/api-index.js +98 -77
  31. package/docs/demos/demos.js +159 -135
  32. package/docs/demos/shared/demoData.js +107 -107
  33. package/docs/demos/shared/demoStyle.css +249 -249
  34. package/docs/demos/shared/fakeBackend.js +93 -6
  35. package/docs/format/03-/346/213/220/347/202/271/350/243/205/351/245/260/344/270/216/346/240/267/345/274/217.md +28 -8
  36. package/docs/format/04-/351/200/232/351/201/223/344/270/216/346/240/207/347/255/276.md +5 -4
  37. package/docs/format/06-/345/257/274/345/205/245/345/257/274/345/207/272/344/270/216/346/240/207/345/207/206/346/226/207/346/241/243.md +8 -6
  38. 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 +6 -6
  39. package/docs/guide/sample-document.html +4 -4
  40. package/docs/index.html +180 -9
  41. package/docs/internals/11-/350/246/206/347/233/226/345/261/202/346/240/207/347/255/276/344/270/216/346/217/220/347/244/272.md +1 -1
  42. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +3 -2
  43. package/docs/internals/15-03-/351/200/220/350/275/256/350/256/260/345/275/225-/346/200/247/350/203/275/344/270/216/344/277/256/345/244/215-/344/270/213.md +16 -0
  44. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -0
  45. package/docs/internals/17-/350/260/203/350/257/225/344/270/216/346/211/251/345/261/225/345/273/272/350/256/256.md +1 -1
  46. package/docs/manual/02-/345/235/220/346/240/207/344/270/216/346/225/260/346/215/256.md +18 -4
  47. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +110 -25
  48. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +13 -8
  49. 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 +111 -10
  50. 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 +47 -9
  51. package/docs/manual/08-/351/200/211/344/270/255/344/270/216/346/213/220/347/202/271.md +17 -15
  52. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +61 -11
  53. 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 +74 -7
  54. package/docs/manual/11-/346/240/267/345/274/217/344/270/216/350/247/204/345/210/231.md +3 -2
  55. package/docs/manual/12-/346/240/207/347/255/276/344/270/216/350/243/205/351/245/260.md +260 -3
  56. package/docs/manual/13-/345/212/250/346/225/210/344/270/216/345/233/276/345/205/203.md +6 -5
  57. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +27 -14
  58. package/docs/manual/15-/345/257/274/345/207/272/344/270/216/351/233/206/346/210/220.md +142 -49
  59. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +31 -29
  60. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +55 -39
  61. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +189 -64
  62. 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 +4 -4
  63. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +42 -2
  64. 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 +22 -6
  65. package/docs/playground/index.html +5 -5
  66. package/docs/steps/README.md +14 -10
  67. package/docs/steps/check-doc-truth.mjs +1 -1
  68. package/docs/steps/selftest-actions.html +10 -10
  69. package/docs/steps/selftest-data.html +96 -14
  70. package/docs/steps/selftest-decoration.html +577 -0
  71. package/docs/steps/selftest-demo.html +6 -5
  72. package/docs/steps/selftest-demos.html +2201 -1612
  73. package/docs/steps/selftest-docs.html +89 -16
  74. package/docs/steps/selftest-effects.html +5 -5
  75. package/docs/steps/selftest-features.html +757 -86
  76. package/docs/steps/selftest-full.html +583 -583
  77. package/docs/steps/selftest-labels.html +1034 -652
  78. package/docs/steps/selftest-perf.html +1899 -1844
  79. package/docs/steps/selftest-scale.html +117 -7
  80. package/docs/steps/selftest-select.html +24 -17
  81. package/docs/steps/step-10-full.js +1 -1
  82. package/docs//346/216/245/345/217/243/345/215/217/350/256/256.md +167 -17
  83. package/docs//346/225/260/346/215/256/346/240/274/345/274/217.md +1 -1
  84. package/package.json +43 -43
  85. package/docs/demos/HistoryDemo.vue +0 -230
  86. package/docs/demos/InterfaceDemo.vue +0 -309
  87. package/docs/demos/SampleDemo.vue +0 -283
@@ -0,0 +1,412 @@
1
+ <script setup>
2
+ /**
3
+ * 搜索与筛选演示:
4
+ * 上半区把 keyword / scope / fields / limit 与 search / searchAsync 摆出来,
5
+ * 下半区用 setFilter 对比「隐藏数据」和「只找出来」的差别。
6
+ *
7
+ * 数据里额外塞了 props.owner 与 symbol 两个字段:前者演示业务字段路径,
8
+ * 后者演示默认全字段模式会跳过、显式 fields 点名时又能命中的边界口径。
9
+ */
10
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
11
+ import { createGisView } from '../../src/index.js'
12
+ import { buildDemoData } from './shared/demoData.js'
13
+ import './shared/demoStyle.css'
14
+
15
+ const emit = defineEmits(['notice'])
16
+ const container = ref(null)
17
+
18
+ const OWNERS = ['北城运检', '南城运检', '西城运检']
19
+ const baseData = buildDemoData({ count: 24, channels: 2 })
20
+ const dataset = {
21
+ ...baseData,
22
+ points: baseData.points.map((point, index) => ({
23
+ ...point,
24
+ props: {
25
+ owner: OWNERS[index % OWNERS.length],
26
+ tag: `BJ-${String(index + 1).padStart(3, '0')}`
27
+ },
28
+ symbol: index % 3 === 0 ? 'square' : 'circle'
29
+ })),
30
+ lines: baseData.lines.map((line, index) => ({
31
+ ...line,
32
+ props: { owner: OWNERS[index % OWNERS.length] }
33
+ }))
34
+ }
35
+
36
+ const keyword = ref('变电站')
37
+ const scope = ref('all')
38
+ const fieldMode = ref('all')
39
+ const limit = ref(3)
40
+ const useWorker = ref(false)
41
+ const actionText = ref('输入关键字,点同步或异步搜索')
42
+ const filterAction = ref('当前没有筛选条件')
43
+ const searchResult = ref(null)
44
+ const filterResult = ref(null)
45
+ const visibleDataText = ref('未读取')
46
+ const activeFilter = ref('none')
47
+ let view = null
48
+ let requestSeq = 0
49
+
50
+ const SCOPES = [
51
+ { id: 'all', label: '全部三类' },
52
+ { id: 'points', label: '只看节点' },
53
+ { id: 'lines', label: '只看线路' },
54
+ { id: 'channels', label: '只看通道' }
55
+ ]
56
+ const FIELD_MODES = [
57
+ { id: 'all', label: '全部字段', fields: null },
58
+ { id: 'name', label: 'name', fields: ['name'] },
59
+ { id: 'owner', label: 'props.owner', fields: ['props.owner'] },
60
+ { id: 'symbol', label: '显式 symbol', fields: ['symbol'] }
61
+ ]
62
+ const LIMITS = [3, 10, 20, 0]
63
+ const PRESETS = [
64
+ { id: 'name', demo: 'search-filter-preset-name', label: '变电站(limit 3)', keyword: '变电站', scope: 'points', field: 'all', limit: 3 },
65
+ { id: 'owner', demo: 'search-filter-preset-owner', label: '北城运检(props.owner)', keyword: '北城运检', scope: 'points', field: 'owner', limit: 10 },
66
+ { id: 'channel', demo: 'search-filter-preset-channel', label: '甲通道(scope=channels)', keyword: '甲通道', scope: 'channels', field: 'all', limit: 10 },
67
+ { id: 'symbol', demo: 'search-filter-preset-symbol', label: 'square(显式 symbol)', keyword: 'square', scope: 'points', field: 'symbol', limit: 10 }
68
+ ]
69
+ const FILTERS = [
70
+ { id: 'none', demo: 'search-filter-filter-clear', label: '清空筛选', condition: null },
71
+ { id: 'voltage', demo: 'search-filter-filter-voltage', label: '节点电压 ≥ 525 kV', condition: { points: { vn_kv: { gte: 525 } }, cascade: 'binding' } },
72
+ { id: 'zone', demo: 'search-filter-filter-zone', label: '分区 2 / 4', condition: { points: { zone_name: { in: ['分区 2', '分区 4'] } }, cascade: 'binding' } },
73
+ { id: 'loading', demo: 'search-filter-filter-loading', label: '线路负载 ≥ 1', condition: { lines: { max_loading_rate: { gte: 1 } } } }
74
+ ]
75
+ const KIND_LABELS = { point: '节点', line: '线路', channel: '通道' }
76
+
77
+ const scopeLabel = computed(() => SCOPES.find(item => item.id === scope.value)?.label ?? '全部三类')
78
+ const searchSummary = computed(() => {
79
+ const result = searchResult.value
80
+ if (!result) {
81
+ return '还没有搜索'
82
+ }
83
+ return `命中 ${result.total} 条:节点 ${result.points.length} / 线路 ${result.lines.length} / 通道 ${result.channels.length}`
84
+ })
85
+ const searchMeta = computed(() => {
86
+ const result = searchResult.value
87
+ if (!result) {
88
+ return `limit=${limit.value} · 还没跑搜索`
89
+ }
90
+ const via = result.via === 'async' ? 'searchAsync' : 'search'
91
+ const worker = result.via === 'async' ? ` · worker=${result.worker ? '是' : '否'}` : ''
92
+ const stale = result.stale ? ' · stale=true(这一份已作废)' : ''
93
+ return `${via}${worker} · limit=${limit.value} · truncated=${result.truncated ? '是' : '否'} · ${result.durationMs} ms${stale}`
94
+ })
95
+ const hitRows = computed(() => (searchResult.value?.hits ?? []).slice(0, 6).map((hit, index) => ({
96
+ key: `${hit.kind}-${hit.index}-${index}`,
97
+ demo: `search-filter-hit-${index}`,
98
+ title: `${KIND_LABELS[hit.kind] ?? hit.kind} · ${hit.id ?? `#${hit.index}`}`,
99
+ detail: `${hit.field} = ${hit.value}`,
100
+ hit
101
+ })))
102
+ const filterSummary = computed(() => {
103
+ const result = filterResult.value
104
+ if (!result) {
105
+ return 'getFilterResult:当前没有筛选条件'
106
+ }
107
+ const hiddenPoints = result.hiddenPoints?.length ?? 0
108
+ const hiddenLines = result.hiddenLines?.length ?? 0
109
+ return `getFilterResult:隐藏 ${hiddenPoints} 节点 / ${hiddenLines} 线路 · 可见 ${result.visiblePoints ?? 0} 节点 / ${result.visibleLines ?? 0} 线路`
110
+ })
111
+ const chipClass = active => (active ? 'dv-chip--active' : '')
112
+
113
+ const buildSearchRequest = () => {
114
+ const mode = FIELD_MODES.find(item => item.id === fieldMode.value) ?? FIELD_MODES[0]
115
+ const request = { keyword: keyword.value, limit: limit.value }
116
+ if (scope.value !== 'all') {
117
+ request.scope = scope.value
118
+ }
119
+ if (mode.fields) {
120
+ request.fields = [...mode.fields]
121
+ }
122
+ return request
123
+ }
124
+
125
+ const runSearch = async (mode = 'sync') => {
126
+ if (!view) {
127
+ return
128
+ }
129
+ const seq = (requestSeq += 1)
130
+ const request = buildSearchRequest()
131
+ if (mode === 'async') {
132
+ actionText.value = 'searchAsync 正在计算…'
133
+ try {
134
+ const result = await view.searchAsync(request)
135
+ if (seq !== requestSeq) {
136
+ return
137
+ }
138
+ searchResult.value = { ...result, via: 'async' }
139
+ actionText.value = result.stale ? '异步搜索返回 stale:这一份结果已作废' : '异步搜索完成:数据量大时这段计算在 Worker 里'
140
+ }
141
+ catch (error) {
142
+ if (seq === requestSeq) {
143
+ actionText.value = `searchAsync 失败:${error?.message ?? error}`
144
+ }
145
+ }
146
+ return
147
+ }
148
+ const result = view.search(request)
149
+ if (seq !== requestSeq) {
150
+ return
151
+ }
152
+ searchResult.value = { ...result, via: 'sync' }
153
+ actionText.value = '同步搜索完成:只返回命中,不改筛选与视角'
154
+ }
155
+ const runSync = () => runSearch('sync')
156
+ const runAsync = () => runSearch('async')
157
+
158
+ const choosePreset = (preset) => {
159
+ keyword.value = preset.keyword
160
+ scope.value = preset.scope
161
+ fieldMode.value = preset.field
162
+ limit.value = preset.limit
163
+ runSearch('sync')
164
+ }
165
+ const applyWorker = () => {
166
+ if (!view) {
167
+ return
168
+ }
169
+ view.updateOptions({ searchWorker: useWorker.value ? { threshold: 30 } : false })
170
+ actionText.value = useWorker.value
171
+ ? 'searchWorker.threshold=30:这份小数据也会走 Worker'
172
+ : 'searchWorker=false:searchAsync 退回主线程同步计算'
173
+ }
174
+ const focusHit = (hit) => {
175
+ view?.focusOn(hit, { select: true })
176
+ actionText.value = `focusOn(${KIND_LABELS[hit.kind] ?? hit.kind} #${hit.index}, { select: true })`
177
+ }
178
+ const focusAllHits = () => {
179
+ const hits = searchResult.value?.hits ?? []
180
+ if (!hits.length) {
181
+ actionText.value = '当前没有可定位的命中'
182
+ return
183
+ }
184
+ view?.focusOn(hits, { select: true })
185
+ actionText.value = `focusOn(hits, { select: true }):${hits.length} 条命中一起进包围盒并选中`
186
+ }
187
+ const readVisibleData = () => {
188
+ if (!view) {
189
+ return null
190
+ }
191
+ const data = view.getVisibleData()
192
+ visibleDataText.value = `getVisibleData:${data.points.length} 节点 / ${data.lines.length} 线路`
193
+ return data
194
+ }
195
+ const syncFilterReadout = (result) => {
196
+ filterResult.value = result
197
+ nextTick(readVisibleData)
198
+ }
199
+ const applyFilter = (filter) => {
200
+ if (!view) {
201
+ return
202
+ }
203
+ activeFilter.value = filter.id
204
+ if (filter.condition) {
205
+ view.setFilter(filter.condition)
206
+ filterAction.value = `setFilter:${filter.label}`
207
+ }
208
+ else {
209
+ view.setFilter(null)
210
+ filterAction.value = 'setFilter(null):筛选条件已清空'
211
+ }
212
+ filterResult.value = view.getFilterResult()
213
+ readVisibleData()
214
+ }
215
+
216
+ onMounted(() => {
217
+ view = createGisView(container.value, {
218
+ points: dataset.points,
219
+ lines: dataset.lines,
220
+ channels: dataset.channels,
221
+ channelWidth: 2.2,
222
+ channelInset: 1.6,
223
+ pointStyle: { size: 8, color: '#ffffff', strokeColor: '#4c8dff', strokeWidth: 1.6 },
224
+ lineStyle: { width: 1.2, color: 'rgba(120, 168, 226, 0.43)' },
225
+ showGrid: true,
226
+ gridColor: 'rgba(126, 168, 214, 0.18)',
227
+ fitPadding: 62,
228
+ features: 'view',
229
+ // 默认阈值 20000;这份 demo 只有几十条,勾选后降到 30 才看得到 Worker 路径
230
+ searchWorker: useWorker.value ? { threshold: 30 } : false,
231
+ label: {
232
+ enabled: true,
233
+ content: '{name}',
234
+ position: 'top',
235
+ gap: 6,
236
+ maxCount: 20,
237
+ style: { color: '#24415f', fontSize: '10px', background: 'rgba(255, 255, 255, 0.72)' }
238
+ },
239
+ onReady: () => {
240
+ emit('notice', `${dataset.points.length} 节点 / ${dataset.lines.length} 线路,搜索与筛选都是只读口径`)
241
+ },
242
+ onFilter: result => syncFilterReadout(result),
243
+ onWarning: text => {
244
+ actionText.value = `warning:${text}`
245
+ }
246
+ })
247
+ runSearch('sync')
248
+ readVisibleData()
249
+ })
250
+
251
+ onBeforeUnmount(() => {
252
+ requestSeq += 1
253
+ view?.destroy()
254
+ view = null
255
+ })
256
+ </script>
257
+
258
+ <template>
259
+ <section class='dv-demo'>
260
+ <div class='dv-stage'>
261
+ <div ref='container' class='dv-canvas'></div>
262
+ <div class='dv-toolbar'>
263
+ <button type='button' @click='view?.fitView(62)'>适应全部</button>
264
+ </div>
265
+ </div>
266
+ <aside class='dv-panel'>
267
+ <div class='dv-group'>
268
+ <h3>搜索<span class='dv-chip'>{{ scopeLabel }}</span></h3>
269
+ <div class='dv-row'>
270
+ <label class='sf-keyword'>关键字
271
+ <input v-model='keyword' class='sf-input' data-demo='search-filter-keyword' type='text' @keydown.enter.prevent='runSync' />
272
+ </label>
273
+ </div>
274
+ <div class='dv-row'>
275
+ <label>范围
276
+ <select v-model='scope' data-demo='search-filter-scope'>
277
+ <option v-for='item in SCOPES' :key='item.id' :value='item.id'>{{ item.label }}</option>
278
+ </select>
279
+ </label>
280
+ <label>字段
281
+ <select v-model='fieldMode' data-demo='search-filter-fields'>
282
+ <option v-for='item in FIELD_MODES' :key='item.id' :value='item.id'>{{ item.label }}</option>
283
+ </select>
284
+ </label>
285
+ <label>limit
286
+ <select v-model.number='limit' data-demo='search-filter-limit'>
287
+ <option v-for='value in LIMITS' :key='value' :value='value'>{{ value === 0 ? '0(不限)' : value }}</option>
288
+ </select>
289
+ </label>
290
+ </div>
291
+ <div class='dv-row sf-actions'>
292
+ <button type='button' class='dv-button dv-button--primary' data-demo='search-filter-run' @click='runSync'>同步 search</button>
293
+ <button type='button' class='dv-button' data-demo='search-filter-run-async' @click='runAsync'>异步 searchAsync</button>
294
+ <label><input v-model='useWorker' data-demo='search-filter-worker' type='checkbox' @change='applyWorker' /> 低阈值 Worker(30 条)</label>
295
+ </div>
296
+ <div class='dv-chips'>
297
+ <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
+ </div>
299
+ <p class='dv-hint'>显式 fields 能搜默认会跳过的 symbol;搜索基于全量数据,不隐藏、也不自动移动视角。</p>
300
+ <div class='dv-row'>
301
+ <button type='button' class='dv-button' data-demo='search-filter-focus-all' @click='focusAllHits'>聚焦并选中全部命中</button>
302
+ </div>
303
+ <p class='dv-readout' data-demo='search-filter-summary'>{{ searchSummary }}</p>
304
+ <p class='dv-readout' data-demo='search-filter-meta'>{{ searchMeta }}</p>
305
+ <p class='dv-hint' data-demo='search-filter-action'>{{ actionText }}</p>
306
+ <ul v-if='hitRows.length' class='sf-hits'>
307
+ <li v-for='row in hitRows' :key='row.key'>
308
+ <button type='button' class='sf-hit' :data-demo='row.demo' @click='focusHit(row.hit)'>
309
+ <strong>{{ row.title }}</strong>
310
+ <span>{{ row.detail }}</span>
311
+ </button>
312
+ </li>
313
+ </ul>
314
+ <p v-else class='dv-hint'>当前没有命中,换一个关键字或字段再试。</p>
315
+ </div>
316
+ <div class='dv-group'>
317
+ <h3>筛选<span class='dv-chip'>getFilterResult</span></h3>
318
+ <div class='dv-chips'>
319
+ <button
320
+ v-for='filter in FILTERS'
321
+ :key='filter.id'
322
+ type='button'
323
+ class='dv-chip'
324
+ :class='chipClass(activeFilter === filter.id)'
325
+ :data-demo='filter.demo'
326
+ @click='applyFilter(filter)'
327
+ >{{ filter.label }}</button>
328
+ </div>
329
+ <p class='dv-readout' data-demo='search-filter-filter'>{{ filterSummary }}</p>
330
+ <p class='dv-readout' data-demo='search-filter-visible'>{{ visibleDataText }}</p>
331
+ <div class='dv-row'>
332
+ <button type='button' class='dv-button' data-demo='search-filter-read-visible' @click='readVisibleData'>读取 getVisibleData()</button>
333
+ </div>
334
+ <p class='dv-hint' data-demo='search-filter-filter-action'>{{ filterAction }}</p>
335
+ </div>
336
+ <div class='dv-group'>
337
+ <h3>两件事别混用</h3>
338
+ <p class='dv-hint'>search 只把要素找出来,结果带 bounds;要居中就再调 focusOn。setFilter 才会隐藏要素,并从 getVisibleData / listPoints 的可见集合里消失。</p>
339
+ </div>
340
+ </aside>
341
+ </section>
342
+ </template>
343
+
344
+ <style scoped>
345
+ .sf-keyword {
346
+ flex: 1 1 220px;
347
+ }
348
+
349
+ .sf-keyword .sf-input {
350
+ flex: 1;
351
+ }
352
+
353
+ .sf-input {
354
+ width: 100%;
355
+ min-width: 150px;
356
+ padding: 5px 8px;
357
+ border: 1px solid #d6e0ec;
358
+ border-radius: 6px;
359
+ color: #1f2a44;
360
+ font-size: 12px;
361
+ }
362
+
363
+ .sf-actions {
364
+ align-items: center;
365
+ }
366
+
367
+ .sf-hits {
368
+ max-height: 196px;
369
+ margin: 8px 0 0;
370
+ padding: 0;
371
+ overflow: auto;
372
+ list-style: none;
373
+ }
374
+
375
+ .sf-hits li + li {
376
+ margin-top: 5px;
377
+ }
378
+
379
+ .sf-hit {
380
+ display: flex;
381
+ align-items: baseline;
382
+ gap: 7px;
383
+ width: 100%;
384
+ padding: 6px 8px;
385
+ border: 1px solid #e2e9f2;
386
+ border-radius: 7px;
387
+ background: #f8fbff;
388
+ text-align: left;
389
+ cursor: pointer;
390
+ }
391
+
392
+ .sf-hit:hover {
393
+ border-color: #b7d2ee;
394
+ background: #eef4fc;
395
+ }
396
+
397
+ .sf-hit strong {
398
+ flex: none;
399
+ color: #2f5e94;
400
+ font-size: 11.5px;
401
+ font-weight: 600;
402
+ }
403
+
404
+ .sf-hit span {
405
+ min-width: 0;
406
+ overflow: hidden;
407
+ color: #71809d;
408
+ font-size: 11px;
409
+ text-overflow: ellipsis;
410
+ white-space: nowrap;
411
+ }
412
+ </style>