@363045841yyt/klinechart 0.7.4 → 0.7.5-alpha.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.
@@ -1,1169 +1,1169 @@
1
- <template>
2
- <div class="indicator-selector">
3
- <div class="indicator-scroll-container">
4
- <div class="indicator-list">
5
- <!-- 已激活的指标 -->
6
- <template v-for="indicator in activeIndicatorsList" :key="indicator.id">
7
- <div
8
- v-if="indicator.id === firstActiveSubIndicatorId"
9
- class="indicator-divider"
10
- aria-hidden="true"
11
- ></div>
12
-
13
- <div
14
- class="indicator-item"
15
- :class="{
16
- draggable: isSubIndicatorId(indicator.id),
17
- 'drag-over': dragOverIndicatorId === indicator.id,
18
- 'is-dragging': draggingIndicatorId === indicator.id,
19
- }"
20
- :draggable="isSubIndicatorId(indicator.id)"
21
- @dragstart="onDragStart($event, indicator.id)"
22
- @dragover.prevent="onDragOver($event, indicator.id)"
23
- @drop.prevent="onDrop($event, indicator.id)"
24
- @dragend="onDragEnd"
25
- >
26
- <div
27
- class="indicator-btn-wrapper"
28
- @mouseenter="hoveredIndicator = indicator.id"
29
- @mouseleave="hoveredIndicator = null"
30
- >
31
- <button
32
- class="indicator-btn"
33
- :class="{ active: true, hovering: hoveredIndicator === indicator.id }"
34
- >
35
- <span class="btn-content">
36
- {{ indicator.label }}
37
- <span v-if="indicator.params?.length" class="param-hint">
38
- ({{ getParamDisplay(indicator) }})
39
- </span>
40
- </span>
41
- <!-- 悬浮操作层 -->
42
- <Transition name="fade">
43
- <div v-if="hoveredIndicator === indicator.id" class="hover-overlay">
44
- <button
45
- v-if="indicator.params?.length"
46
- class="action-btn settings-btn"
47
- @click.stop="showParams(indicator.id)"
48
- title="编辑参数"
49
- >
50
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
51
- <path
52
- d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
53
- />
54
- </svg>
55
- </button>
56
- <span v-if="indicator.params?.length" class="divider"></span>
57
- <button
58
- class="action-btn remove-btn"
59
- @click.stop="removeIndicator(indicator.id)"
60
- title="移除指标"
61
- >
62
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
63
- <path
64
- d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
65
- />
66
- </svg>
67
- </button>
68
- </div>
69
- </Transition>
70
- </button>
71
- </div>
72
- </div>
73
- </template>
74
-
75
- <!-- 添加按钮 -->
76
- <div class="indicator-item">
77
- <button ref="addBtnRef" class="add-btn" @click="toggleAddMenu" title="添加指标">
78
- <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
79
- <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
80
- </svg>
81
- </button>
82
- </div>
83
- </div>
84
- </div>
85
-
86
- <!-- 添加指标弹窗 -->
87
- <Teleport :to="teleportTarget">
88
- <Transition name="overlay">
89
- <div v-if="showAddMenu" class="selector-overlay" @click="closeAddMenu">
90
- <Transition name="modal">
91
- <div v-if="showAddMenu" class="selector-modal" @click.stop>
92
- <!-- 弹窗头部 -->
93
- <div class="modal-header">
94
- <div class="header-title">
95
- <span class="title-text">添加指标</span>
96
- <span class="title-sub">{{ totalIndicatorsCount }} 个可用指标</span>
97
- </div>
98
- <div class="header-actions">
99
- <button
100
- class="view-toggle-btn"
101
- :class="{ active: isCompactView }"
102
- @click="isCompactView = !isCompactView"
103
- title="简洁模式"
104
- >
105
- <svg
106
- v-if="!isCompactView"
107
- viewBox="0 0 24 24"
108
- width="16"
109
- height="16"
110
- fill="currentColor"
111
- >
112
- <path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
113
- </svg>
114
- <svg v-else viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
115
- <path
116
- d="M3 3h18v18H3V3zm16 16V5H5v14h14zM7 7h4v4H7V7zm0 6h4v4H7v-4zm6-6h4v4h-4V7zm0 6h4v4h-4v-4z"
117
- />
118
- </svg>
119
- </button>
120
- <button class="modal-close" @click="closeAddMenu" title="关闭">
121
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
122
- <path
123
- d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
124
- />
125
- </svg>
126
- </button>
127
- </div>
128
- </div>
129
-
130
- <!-- 弹窗主体 -->
131
- <div class="modal-body">
132
- <!-- 搜索框 -->
133
- <div class="search-box">
134
- <svg
135
- class="search-icon"
136
- viewBox="0 0 24 24"
137
- width="16"
138
- height="16"
139
- fill="currentColor"
140
- >
141
- <path
142
- d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
143
- />
144
- </svg>
145
- <input
146
- v-model="searchQuery"
147
- type="text"
148
- class="search-input"
149
- placeholder="搜索指标名称..."
150
- />
151
- </div>
152
- <!-- 主图指标区域 -->
153
- <div v-if="filteredMainIndicators.length > 0" class="indicator-section">
154
- <div class="section-header">
155
- <span class="section-title">主图指标</span>
156
- <span class="section-count">{{ filteredMainIndicators.length }}</span>
157
- </div>
158
- <div class="indicator-grid" :class="{ compact: isCompactView }">
159
- <button
160
- v-for="indicator in filteredMainIndicators"
161
- :key="indicator.id"
162
- class="indicator-card"
163
- :class="{ active: isActive(indicator.id), compact: isCompactView }"
164
- @click="
165
- isActive(indicator.id)
166
- ? removeIndicator(indicator.id)
167
- : addIndicator(indicator.id)
168
- "
169
- >
170
- <template v-if="isCompactView">
171
- <span class="card-label">{{ indicator.label }}</span>
172
- <span class="card-tooltip">{{ indicator.name }}</span>
173
- </template>
174
- <template v-else>
175
- <div class="card-header">
176
- <span class="card-label">{{ indicator.label }}</span>
177
- <div class="card-header-actions">
178
- <button
179
- v-if="indicator.params?.length"
180
- class="card-settings-btn"
181
- @click.stop="showParams(indicator.id)"
182
- title="编辑参数"
183
- >
184
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
185
- <path
186
- d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
187
- />
188
- </svg>
189
- </button>
190
- </div>
191
- </div>
192
- <div class="card-name">{{ indicator.name }}</div>
193
- </template>
194
- </button>
195
- </div>
196
- </div>
197
-
198
- <!-- 分隔线 -->
199
- <div
200
- v-if="filteredMainIndicators.length > 0 && filteredSubIndicators.length > 0"
201
- class="section-divider"
202
- ></div>
203
-
204
- <!-- 无匹配结果提示 -->
205
- <div v-if="!hasSearchResults && searchQuery.trim()" class="no-results">
206
- <svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
207
- <path
208
- d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
209
- />
210
- </svg>
211
- <p>未找到匹配的指标</p>
212
- <span class="no-results-hint">请尝试其他关键词</span>
213
- </div>
214
-
215
- <!-- 副图指标区域 -->
216
- <div v-if="filteredSubIndicators.length > 0" class="indicator-section">
217
- <div class="section-header">
218
- <span class="section-title">副图指标</span>
219
- <span class="section-count">{{ filteredSubIndicators.length }}</span>
220
- </div>
221
- <div class="indicator-grid" :class="{ compact: isCompactView }">
222
- <button
223
- v-for="indicator in filteredSubIndicators"
224
- :key="indicator.id"
225
- class="indicator-card"
226
- :class="{ active: isActive(indicator.id), compact: isCompactView }"
227
- @click="
228
- isActive(indicator.id)
229
- ? removeIndicator(indicator.id)
230
- : addIndicator(indicator.id)
231
- "
232
- >
233
- <template v-if="isCompactView">
234
- <span class="card-label">{{ indicator.label }}</span>
235
- <span class="card-tooltip">{{ indicator.name }}</span>
236
- </template>
237
- <template v-else>
238
- <div class="card-header">
239
- <span class="card-label">{{ indicator.label }}</span>
240
- <div class="card-header-actions">
241
- <button
242
- v-if="indicator.params?.length"
243
- class="card-settings-btn"
244
- @click.stop="showParams(indicator.id)"
245
- title="编辑参数"
246
- >
247
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
248
- <path
249
- d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
250
- />
251
- </svg>
252
- </button>
253
- </div>
254
- </div>
255
- <div class="card-name">{{ indicator.name }}</div>
256
- </template>
257
- </button>
258
- </div>
259
- </div>
260
- </div>
261
-
262
- <!-- 弹窗底部 -->
263
- <div class="modal-footer">
264
- <div class="footer-info">
265
- <span class="info-text">已激活 {{ activeCount }} 个指标</span>
266
- </div>
267
- <button class="btn btn-confirm" @click="closeAddMenu">确认</button>
268
- </div>
269
- </div>
270
- </Transition>
271
- </div>
272
- </Transition>
273
- </Teleport>
274
-
275
- <!-- 参数编辑弹窗 -->
276
- <IndicatorParams
277
- v-if="currentIndicator"
278
- :visible="paramsVisible"
279
- :indicator-id="currentIndicator.id"
280
- :indicator-name="currentIndicator.name"
281
- :indicator-description="currentIndicator.description"
282
- :params="currentIndicator.params || []"
283
- :values="getParamValues(currentIndicator.id)"
284
- @close="paramsVisible = false"
285
- @confirm="onParamsConfirm"
286
- />
287
- </div>
288
- </template>
289
-
290
- <script setup lang="ts">
291
- import { ref, computed, onMounted, onUnmounted } from 'vue'
292
- import IndicatorParams from './IndicatorParams.vue'
293
- import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
294
- import {
295
- mainIndicators,
296
- subIndicators,
297
- findIndicator,
298
- isSubIndicatorId,
299
- } from '@363045841yyt/klinechart-core/engine/renderers/Indicator/indicatorData'
300
- import type { Indicator } from '@363045841yyt/klinechart-core/engine/renderers/Indicator/indicatorData'
301
-
302
- const props = defineProps<{
303
- activeIndicators?: string[]
304
- indicatorParams?: Record<string, Record<string, unknown>>
305
- }>()
306
-
307
- const emit = defineEmits<{
308
- toggle: [indicatorId: string, active: boolean]
309
- updateParams: [indicatorId: string, params: Record<string, number>]
310
- reorderSubIndicators: [orderedIndicatorIds: string[]]
311
- }>()
312
-
313
- const addBtnRef = ref<HTMLButtonElement | null>(null)
314
- const paramsVisible = ref(false)
315
- const currentIndicatorId = ref<string | null>(null)
316
- const hoveredIndicator = ref<string | null>(null)
317
- const showAddMenu = ref(false)
318
- const dragOverIndicatorId = ref<string | null>(null)
319
- const draggingIndicatorId = ref<string | null>(null)
320
- const isCompactView = ref(false)
321
- const searchQuery = ref('')
322
-
323
- // Teleport target for fullscreen modal visibility
324
- const teleportTarget = useFullscreenTeleportTarget()
325
-
326
- const activeIndicatorsList = computed(() => {
327
- if (!props.activeIndicators?.length) return []
328
- return props.activeIndicators
329
- .map((id) => findIndicator(id))
330
- .filter((i): i is Indicator => i !== undefined)
331
- .sort((a, b) => {
332
- if (a.pane === b.pane) return 0
333
- return a.pane === 'main' ? -1 : 1
334
- })
335
- })
336
-
337
- const firstActiveSubIndicatorId = computed(() => {
338
- const hasMain = activeIndicatorsList.value.some((indicator) => indicator.pane === 'main')
339
- if (!hasMain) return null
340
- const firstSub = activeIndicatorsList.value.find((indicator) => indicator.pane === 'sub')
341
- return firstSub?.id ?? null
342
- })
343
-
344
- const currentIndicator = computed(() => {
345
- if (!currentIndicatorId.value) return null
346
- return findIndicator(currentIndicatorId.value)
347
- })
348
-
349
- const totalIndicatorsCount = computed(() => mainIndicators.length + subIndicators.length)
350
-
351
- const activeCount = computed(() => props.activeIndicators?.length ?? 0)
352
-
353
- // 过滤后的主图指标
354
- const filteredMainIndicators = computed(() => {
355
- if (!searchQuery.value.trim()) return mainIndicators
356
- const query = searchQuery.value.toLowerCase().trim()
357
- return mainIndicators.filter(
358
- (i) =>
359
- i.label.toLowerCase().includes(query) ||
360
- i.name.toLowerCase().includes(query) ||
361
- i.id.toLowerCase().includes(query),
362
- )
363
- })
364
-
365
- // 过滤后的副图指标
366
- const filteredSubIndicators = computed(() => {
367
- if (!searchQuery.value.trim()) return subIndicators
368
- const query = searchQuery.value.toLowerCase().trim()
369
- return subIndicators.filter(
370
- (i) =>
371
- i.label.toLowerCase().includes(query) ||
372
- i.name.toLowerCase().includes(query) ||
373
- i.id.toLowerCase().includes(query),
374
- )
375
- })
376
-
377
- // 是否有搜索结果
378
- const hasSearchResults = computed(
379
- () => filteredMainIndicators.value.length > 0 || filteredSubIndicators.value.length > 0,
380
- )
381
-
382
- function isActive(indicatorId: string): boolean {
383
- return props.activeIndicators?.includes(indicatorId) ?? false
384
- }
385
-
386
- function addIndicator(indicatorId: string) {
387
- if (isActive(indicatorId)) return
388
-
389
- const indicator = findIndicator(indicatorId)
390
- if (!indicator) return
391
-
392
- if (indicator.pane === 'main') {
393
- mainIndicators
394
- .filter((i) => i.id !== indicatorId && isActive(i.id))
395
- .forEach((i) => emit('toggle', i.id, false))
396
- }
397
-
398
- emit('toggle', indicatorId, true)
399
- }
400
-
401
- function removeIndicator(indicatorId: string) {
402
- emit('toggle', indicatorId, false)
403
- }
404
-
405
- function showParams(indicatorId: string) {
406
- currentIndicatorId.value = indicatorId
407
- paramsVisible.value = true
408
- }
409
-
410
- function closeAddMenu() {
411
- showAddMenu.value = false
412
- }
413
-
414
- function getParamValues(indicatorId: string): Record<string, number> {
415
- const indicator = findIndicator(indicatorId)
416
- if (!indicator?.params) return {}
417
-
418
- const defaultParams: Record<string, number> = {}
419
- for (const p of indicator.params) {
420
- defaultParams[p.key] = p.default ?? p.min ?? 1
421
- }
422
-
423
- const userParams = props.indicatorParams?.[indicatorId] || {}
424
- const result: Record<string, number> = { ...defaultParams }
425
-
426
- for (const [key, value] of Object.entries(userParams)) {
427
- if (typeof value === 'number') {
428
- result[key] = value
429
- }
430
- }
431
-
432
- return result
433
- }
434
-
435
- function getParamDisplay(indicator: Indicator): string {
436
- const values = getParamValues(indicator.id)
437
- if (!indicator.params) return ''
438
- return indicator.params.map((p) => values[p.key] ?? '').join(',')
439
- }
440
-
441
- function onParamsConfirm(values: Record<string, number>) {
442
- if (currentIndicatorId.value) {
443
- emit('updateParams', currentIndicatorId.value, values)
444
- }
445
- paramsVisible.value = false
446
- }
447
-
448
- function onDragStart(event: DragEvent, indicatorId: string) {
449
- if (!isSubIndicatorId(indicatorId)) {
450
- event.preventDefault()
451
- return
452
- }
453
- draggingIndicatorId.value = indicatorId
454
- dragOverIndicatorId.value = null
455
- event.dataTransfer?.setData('text/plain', indicatorId)
456
- if (event.dataTransfer) {
457
- event.dataTransfer.effectAllowed = 'move'
458
- }
459
- }
460
-
461
- function onDragOver(event: DragEvent, indicatorId: string) {
462
- if (
463
- !draggingIndicatorId.value ||
464
- !isSubIndicatorId(indicatorId) ||
465
- draggingIndicatorId.value === indicatorId
466
- )
467
- return
468
- dragOverIndicatorId.value = indicatorId
469
- if (event.dataTransfer) {
470
- event.dataTransfer.dropEffect = 'move'
471
- }
472
- }
473
-
474
- function onDrop(event: DragEvent, targetIndicatorId: string) {
475
- const sourceIndicatorId =
476
- draggingIndicatorId.value || event.dataTransfer?.getData('text/plain') || ''
477
- if (!sourceIndicatorId || sourceIndicatorId === targetIndicatorId) {
478
- onDragEnd()
479
- return
480
- }
481
- if (!isSubIndicatorId(sourceIndicatorId) || !isSubIndicatorId(targetIndicatorId)) {
482
- onDragEnd()
483
- return
484
- }
485
-
486
- const sourceIndex = activeIndicatorsList.value.findIndex((i) => i.id === sourceIndicatorId)
487
- const targetIndex = activeIndicatorsList.value.findIndex((i) => i.id === targetIndicatorId)
488
- if (sourceIndex < 0 || targetIndex < 0) {
489
- onDragEnd()
490
- return
491
- }
492
-
493
- const next = [...activeIndicatorsList.value.map((i) => i.id)]
494
- const [moved] = next.splice(sourceIndex, 1)
495
- if (!moved) {
496
- onDragEnd()
497
- return
498
- }
499
- next.splice(targetIndex, 0, moved)
500
-
501
- emit(
502
- 'reorderSubIndicators',
503
- next.filter((id) => isSubIndicatorId(id)),
504
- )
505
- onDragEnd()
506
- }
507
-
508
- function onDragEnd() {
509
- dragOverIndicatorId.value = null
510
- draggingIndicatorId.value = null
511
- }
512
-
513
- function toggleAddMenu() {
514
- showAddMenu.value = !showAddMenu.value
515
- }
516
-
517
- function handleKeydown(event: KeyboardEvent) {
518
- if (event.key === 'Escape' && showAddMenu.value) {
519
- showAddMenu.value = false
520
- }
521
- }
522
-
523
- onMounted(() => {
524
- document.addEventListener('keydown', handleKeydown)
525
- })
526
-
527
- onUnmounted(() => {
528
- document.removeEventListener('keydown', handleKeydown)
529
- })
530
- </script>
531
-
532
- <style scoped>
533
- .indicator-selector {
534
- margin: 20px;
535
- width: 80%;
536
- position: relative;
537
- }
538
-
539
- .indicator-scroll-container {
540
- width: 100%;
541
- overflow-x: auto;
542
- overflow-y: hidden;
543
- scrollbar-width: none;
544
- -webkit-overflow-scrolling: touch;
545
- text-align: center;
546
- }
547
-
548
- .indicator-scroll-container::-webkit-scrollbar {
549
- display: none;
550
- }
551
-
552
- .indicator-list {
553
- display: inline-flex;
554
- gap: 8px;
555
- padding: 2px;
556
- margin: 0 auto;
557
- }
558
-
559
- .indicator-divider {
560
- width: 1px;
561
- height: 20px;
562
- align-self: center;
563
- background: #d9d9d9;
564
- }
565
-
566
- .indicator-item {
567
- display: flex;
568
- align-items: center;
569
- gap: 4px;
570
- }
571
-
572
- .indicator-item.draggable,
573
- .indicator-item.draggable .indicator-btn,
574
- .indicator-item.draggable:hover,
575
- .indicator-item.draggable:hover .indicator-btn {
576
- cursor: move;
577
- }
578
-
579
- .indicator-item.is-dragging {
580
- opacity: 0.6;
581
- }
582
-
583
- .indicator-item.drag-over .indicator-btn {
584
- border-color: #1a1a1a;
585
- box-shadow: 0 0 0 2px rgba(26, 26, 26, 0.12);
586
- }
587
-
588
- .indicator-btn-wrapper {
589
- position: relative;
590
- }
591
-
592
- .indicator-btn {
593
- position: relative;
594
- flex-shrink: 0;
595
- padding: 6px 16px;
596
- border: 1px solid #e0e0e0;
597
- border-radius: 16px;
598
- background: #ffffff;
599
- color: #666;
600
- font-size: 13px;
601
- font-weight: 500;
602
- cursor: pointer;
603
- transition: all 0.3s ease;
604
- white-space: nowrap;
605
- display: flex;
606
- align-items: center;
607
- justify-content: center;
608
- gap: 4px;
609
- overflow: hidden;
610
- }
611
-
612
- .indicator-btn:hover:not(.hovering) {
613
- background: #f8f8f8;
614
- border-color: #ccc;
615
- color: #333;
616
- }
617
-
618
- .indicator-btn.active {
619
- background: #f8f8f8;
620
- border-color: #1a1a1a;
621
- color: #1a1a1a;
622
- }
623
-
624
- .indicator-btn.active:hover:not(.hovering) {
625
- background: #f0f0f0;
626
- border-color: #333;
627
- }
628
-
629
- .btn-content {
630
- position: relative;
631
- z-index: 1;
632
- }
633
-
634
- .param-hint {
635
- font-size: 11px;
636
- opacity: 0.85;
637
- }
638
-
639
- /* 悬浮操作层 */
640
- .hover-overlay {
641
- position: absolute;
642
- top: 0;
643
- left: 0;
644
- right: 0;
645
- bottom: 0;
646
- display: flex;
647
- align-items: center;
648
- justify-content: center;
649
- gap: 4px;
650
- background: rgba(255, 255, 255, 0.85);
651
- backdrop-filter: blur(4px);
652
- border-radius: 16px;
653
- z-index: 2;
654
- }
655
-
656
- .action-btn {
657
- width: 24px;
658
- height: 24px;
659
- padding: 0;
660
- border: none;
661
- border-radius: 50%;
662
- background: transparent;
663
- color: #666;
664
- cursor: pointer;
665
- display: flex;
666
- align-items: center;
667
- justify-content: center;
668
- transition: all 0.2s;
669
- }
670
-
671
- .action-btn:hover {
672
- background: rgba(0, 0, 0, 0.06);
673
- color: #333;
674
- }
675
-
676
- .settings-btn:hover {
677
- color: #1a1a1a;
678
- }
679
-
680
- .remove-btn:hover {
681
- color: #ff4d4f;
682
- }
683
-
684
- .divider {
685
- width: 1px;
686
- height: 14px;
687
- background: #e0e0e0;
688
- }
689
-
690
- /* 添加按钮 */
691
- .add-btn {
692
- flex-shrink: 0;
693
- width: 32px;
694
- height: 32px;
695
- padding: 0;
696
- border: 1px dashed #d9d9d9;
697
- border-radius: 50%;
698
- background: transparent;
699
- color: #999;
700
- cursor: pointer;
701
- display: flex;
702
- align-items: center;
703
- justify-content: center;
704
- transition: all 0.3s ease;
705
- }
706
-
707
- .add-btn:hover {
708
- border-color: #1a1a1a;
709
- color: #1a1a1a;
710
- background: rgba(26, 26, 26, 0.04);
711
- }
712
-
713
- /* ─────────────────────────────────────────────────────────────────
714
- 弹窗样式 - 与其他弹窗保持一致
715
- ───────────────────────────────────────────────────────────────── */
716
-
717
- /* 遮罩层 */
718
- .selector-overlay {
719
- position: fixed;
720
- inset: 0;
721
- background: rgba(0, 0, 0, 0.3);
722
- backdrop-filter: blur(4px);
723
- display: flex;
724
- align-items: center;
725
- justify-content: center;
726
- z-index: 1000;
727
- }
728
-
729
- /* 弹窗容器 */
730
- .selector-modal {
731
- background: #ffffff;
732
- border: 1px solid #e0e0e0;
733
- border-radius: 12px;
734
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
735
- width: 90vw;
736
- max-width: 860px;
737
- max-height: 85vh;
738
- overflow: hidden;
739
- display: flex;
740
- flex-direction: column;
741
- }
742
-
743
- /* 弹窗头部 */
744
- .modal-header {
745
- display: flex;
746
- justify-content: space-between;
747
- align-items: center;
748
- padding: 16px 20px;
749
- background: #f8f8f8;
750
- border-bottom: 1px solid #e8e8e8;
751
- flex-shrink: 0;
752
- }
753
-
754
- .header-title {
755
- display: flex;
756
- flex-direction: column;
757
- gap: 2px;
758
- }
759
-
760
- .title-text {
761
- font-size: 14px;
762
- font-weight: 600;
763
- color: #1a1a1a;
764
- letter-spacing: 0.2px;
765
- }
766
-
767
- .title-sub {
768
- font-size: 11px;
769
- color: #999;
770
- }
771
-
772
- .modal-close {
773
- background: #fff;
774
- border: 1px solid #e0e0e0;
775
- border-radius: 6px;
776
- width: 28px;
777
- height: 28px;
778
- display: flex;
779
- align-items: center;
780
- justify-content: center;
781
- cursor: pointer;
782
- color: #888;
783
- transition: all 0.15s;
784
- padding: 0;
785
- }
786
-
787
- .modal-close:hover {
788
- background: #f0f0f0;
789
- color: #333;
790
- border-color: #ccc;
791
- }
792
-
793
- .modal-close svg {
794
- width: 14px;
795
- height: 14px;
796
- }
797
-
798
- .header-actions {
799
- display: flex;
800
- align-items: center;
801
- gap: 8px;
802
- }
803
-
804
- .view-toggle-btn {
805
- background: #fff;
806
- border: 1px solid #e0e0e0;
807
- border-radius: 6px;
808
- width: 28px;
809
- height: 28px;
810
- display: flex;
811
- align-items: center;
812
- justify-content: center;
813
- cursor: pointer;
814
- color: #888;
815
- transition: all 0.15s;
816
- padding: 0;
817
- }
818
-
819
- .view-toggle-btn:hover {
820
- background: #f0f0f0;
821
- color: #333;
822
- border-color: #ccc;
823
- }
824
-
825
- .view-toggle-btn.active {
826
- background: #1a1a1a;
827
- border-color: #1a1a1a;
828
- color: #fff;
829
- }
830
-
831
- /* 弹窗主体 */
832
- .modal-body {
833
- padding: 20px;
834
- overflow-y: auto;
835
- flex: 1;
836
- display: flex;
837
- flex-direction: column;
838
- gap: 20px;
839
- }
840
-
841
- /* 搜索框 */
842
- .search-box {
843
- display: flex;
844
- align-items: center;
845
- gap: 10px;
846
- padding: 10px 14px;
847
- border: 1px solid #e0e0e0;
848
- border-radius: 8px;
849
- transition: all 0.2s ease;
850
- }
851
-
852
- .search-box:focus-within {
853
- background: #ffffff;
854
- border-color: #1a1a1a;
855
- box-shadow: 0 0 0 2px rgba(26, 26, 26, 0.08);
856
- }
857
-
858
- .search-icon {
859
- flex-shrink: 0;
860
- color: #999;
861
- }
862
-
863
- .search-input {
864
- flex: 1;
865
- border: none;
866
- background: transparent;
867
- font-size: 13px;
868
- color: #333;
869
- outline: none;
870
- }
871
-
872
- .search-input::placeholder {
873
- color: #aaa;
874
- }
875
-
876
- /* 无结果提示 */
877
- .no-results {
878
- display: flex;
879
- flex-direction: column;
880
- align-items: center;
881
- justify-content: center;
882
- padding: 48px 20px;
883
- color: #ccc;
884
- gap: 12px;
885
- }
886
-
887
- .no-results svg {
888
- opacity: 0.5;
889
- }
890
-
891
- .no-results p {
892
- margin: 0;
893
- font-size: 14px;
894
- color: #999;
895
- font-weight: 500;
896
- }
897
-
898
- .no-results-hint {
899
- font-size: 12px;
900
- color: #bbb;
901
- }
902
-
903
- /* 指标区域 */
904
- .indicator-section {
905
- display: flex;
906
- flex-direction: column;
907
- gap: 12px;
908
- }
909
-
910
- .section-header {
911
- display: flex;
912
- align-items: center;
913
- gap: 8px;
914
- }
915
-
916
- .section-title {
917
- font-size: 13px;
918
- font-weight: 600;
919
- color: #1a1a1a;
920
- }
921
-
922
- .section-count {
923
- font-size: 11px;
924
- color: #999;
925
- background: #f0f0f0;
926
- padding: 2px 8px;
927
- border-radius: 10px;
928
- }
929
-
930
- /* 自适应列数网格 */
931
- .indicator-grid {
932
- display: grid;
933
- grid-template-columns: repeat(auto-fill, minmax(195px, 1fr));
934
- gap: 10px;
935
- }
936
-
937
- /* 紧凑模式 - 标签形式 */
938
- .indicator-grid.compact {
939
- display: flex;
940
- flex-wrap: wrap;
941
- gap: 8px;
942
- }
943
-
944
- .indicator-grid.compact .indicator-card {
945
- display: inline-flex;
946
- align-items: center;
947
- justify-content: center;
948
- padding: 6px 14px;
949
- border-radius: 16px;
950
- min-height: 32px;
951
- white-space: nowrap;
952
- position: relative;
953
- }
954
-
955
- .indicator-grid.compact .indicator-card .card-tooltip {
956
- position: absolute;
957
- bottom: calc(100% + 6px);
958
- left: 50%;
959
- transform: translateX(-50%);
960
- padding: 4px 10px;
961
- border-radius: 6px;
962
- background: #333;
963
- color: #fff;
964
- font-size: 12px;
965
- white-space: nowrap;
966
- pointer-events: none;
967
- opacity: 0;
968
- transition: opacity 0.15s ease;
969
- z-index: 10;
970
- }
971
-
972
- .indicator-grid.compact .indicator-card:hover .card-tooltip {
973
- opacity: 1;
974
- }
975
-
976
- .indicator-grid.compact .indicator-card .card-label {
977
- font-size: 12px;
978
- font-weight: 500;
979
- }
980
-
981
- /* 指标卡片 */
982
- .indicator-card {
983
- display: flex;
984
- flex-direction: column;
985
- gap: 4px;
986
- padding: 12px 14px;
987
- border: 1px solid #e8e8e8;
988
- border-radius: 8px;
989
- background: #ffffff;
990
- cursor: pointer;
991
- transition: all 0.15s;
992
- text-align: left;
993
- }
994
-
995
- .indicator-card:hover:not(.disabled) {
996
- border-color: #1a1a1a;
997
- background: #fafafa;
998
- transform: translateY(-1px);
999
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1000
- }
1001
-
1002
- .indicator-card.active {
1003
- border-color: #1a1a1a;
1004
- background: #f8f8f8;
1005
- }
1006
-
1007
- .card-header {
1008
- display: flex;
1009
- align-items: center;
1010
- justify-content: space-between;
1011
- gap: 8px;
1012
- }
1013
-
1014
- .card-label {
1015
- font-size: 13px;
1016
- font-weight: 600;
1017
- color: #1a1a1a;
1018
- }
1019
-
1020
- .card-header-actions {
1021
- display: flex;
1022
- align-items: center;
1023
- gap: 4px;
1024
- }
1025
-
1026
- .card-settings-btn {
1027
- display: flex;
1028
- align-items: center;
1029
- justify-content: center;
1030
- width: 20px;
1031
- height: 20px;
1032
- padding: 0;
1033
- border: none;
1034
- border-radius: 4px;
1035
- background: transparent;
1036
- color: #bbb;
1037
- cursor: pointer;
1038
- transition: all 0.15s;
1039
- }
1040
-
1041
- .card-settings-btn:hover {
1042
- background: #f0f0f0;
1043
- color: #555;
1044
- }
1045
-
1046
- .card-name {
1047
- font-size: 11px;
1048
- color: #666;
1049
- line-height: 1.4;
1050
- }
1051
-
1052
- .card-params {
1053
- font-size: 10px;
1054
- color: #999;
1055
- margin-top: 2px;
1056
- }
1057
-
1058
- /* 区域分隔线 */
1059
- .section-divider {
1060
- height: 1px;
1061
- background: linear-gradient(90deg, transparent, #e0e0e0, transparent);
1062
- margin: 4px 0;
1063
- }
1064
-
1065
- /* 弹窗底部 */
1066
- .modal-footer {
1067
- display: flex;
1068
- align-items: center;
1069
- justify-content: space-between;
1070
- padding: 12px 20px;
1071
- background: #f8f8f8;
1072
- border-top: 1px solid #e8e8e8;
1073
- flex-shrink: 0;
1074
- }
1075
-
1076
- .footer-info {
1077
- font-size: 12px;
1078
- color: #666;
1079
- }
1080
-
1081
- .info-text {
1082
- color: #999;
1083
- }
1084
-
1085
- /* 按钮样式 */
1086
- .btn {
1087
- display: flex;
1088
- align-items: center;
1089
- gap: 5px;
1090
- padding: 6px 16px;
1091
- border-radius: 7px;
1092
- font-size: 13px;
1093
- font-weight: 500;
1094
- cursor: pointer;
1095
- border: 1px solid transparent;
1096
- transition: all 0.15s;
1097
- line-height: 1.4;
1098
- }
1099
-
1100
- .btn-confirm {
1101
- background: #1a1a1a;
1102
- border-color: #1a1a1a;
1103
- color: #fff;
1104
- }
1105
-
1106
- .btn-confirm:hover {
1107
- background: #333;
1108
- border-color: #333;
1109
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
1110
- transform: translateY(-1px);
1111
- }
1112
-
1113
- /* 过渡动画 */
1114
- .fade-enter-active,
1115
- .fade-leave-active {
1116
- transition: opacity 0.2s ease;
1117
- }
1118
-
1119
- .fade-enter-from,
1120
- .fade-leave-to {
1121
- opacity: 0;
1122
- }
1123
-
1124
- /* 遮罩层动画 */
1125
- .overlay-enter-active,
1126
- .overlay-leave-active {
1127
- transition: opacity 0.2s ease;
1128
- }
1129
-
1130
- .overlay-enter-from,
1131
- .overlay-leave-to {
1132
- opacity: 0;
1133
- }
1134
-
1135
- /* 弹窗动画 */
1136
- .modal-enter-active {
1137
- transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
1138
- }
1139
-
1140
- .modal-leave-active {
1141
- transition: all 0.16s ease-in;
1142
- }
1143
-
1144
- .modal-enter-from {
1145
- opacity: 0;
1146
- transform: scale(0.88) translateY(-16px);
1147
- }
1148
-
1149
- .modal-leave-to {
1150
- opacity: 0;
1151
- transform: scale(0.94) translateY(8px);
1152
- }
1153
-
1154
- /* 响应式适配 */
1155
- @media (max-width: 640px) {
1156
- .selector-modal {
1157
- width: 95vw;
1158
- max-height: 90vh;
1159
- }
1160
-
1161
- .indicator-grid {
1162
- grid-template-columns: 1fr;
1163
- }
1164
-
1165
- .modal-body {
1166
- padding: 16px;
1167
- }
1168
- }
1169
- </style>
1
+ <template>
2
+ <div class="indicator-selector">
3
+ <div class="indicator-scroll-container">
4
+ <div class="indicator-list">
5
+ <!-- 已激活的指标 -->
6
+ <template v-for="indicator in activeIndicatorsList" :key="indicator.id">
7
+ <div
8
+ v-if="indicator.id === firstActiveSubIndicatorId"
9
+ class="indicator-divider"
10
+ aria-hidden="true"
11
+ ></div>
12
+
13
+ <div
14
+ class="indicator-item"
15
+ :class="{
16
+ draggable: isSubIndicatorId(indicator.id),
17
+ 'drag-over': dragOverIndicatorId === indicator.id,
18
+ 'is-dragging': draggingIndicatorId === indicator.id,
19
+ }"
20
+ :draggable="isSubIndicatorId(indicator.id)"
21
+ @dragstart="onDragStart($event, indicator.id)"
22
+ @dragover.prevent="onDragOver($event, indicator.id)"
23
+ @drop.prevent="onDrop($event, indicator.id)"
24
+ @dragend="onDragEnd"
25
+ >
26
+ <div
27
+ class="indicator-btn-wrapper"
28
+ @mouseenter="hoveredIndicator = indicator.id"
29
+ @mouseleave="hoveredIndicator = null"
30
+ >
31
+ <button
32
+ class="indicator-btn"
33
+ :class="{ active: true, hovering: hoveredIndicator === indicator.id }"
34
+ >
35
+ <span class="btn-content">
36
+ {{ indicator.label }}
37
+ <span v-if="indicator.params?.length" class="param-hint">
38
+ ({{ getParamDisplay(indicator) }})
39
+ </span>
40
+ </span>
41
+ <!-- 悬浮操作层 -->
42
+ <Transition name="fade">
43
+ <div v-if="hoveredIndicator === indicator.id" class="hover-overlay">
44
+ <button
45
+ v-if="indicator.params?.length"
46
+ class="action-btn settings-btn"
47
+ @click.stop="showParams(indicator.id)"
48
+ title="编辑参数"
49
+ >
50
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
51
+ <path
52
+ d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
53
+ />
54
+ </svg>
55
+ </button>
56
+ <span v-if="indicator.params?.length" class="divider"></span>
57
+ <button
58
+ class="action-btn remove-btn"
59
+ @click.stop="removeIndicator(indicator.id)"
60
+ title="移除指标"
61
+ >
62
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
63
+ <path
64
+ d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
65
+ />
66
+ </svg>
67
+ </button>
68
+ </div>
69
+ </Transition>
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </template>
74
+
75
+ <!-- 添加按钮 -->
76
+ <div class="indicator-item">
77
+ <button ref="addBtnRef" class="add-btn" @click="toggleAddMenu" title="添加指标">
78
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
79
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
80
+ </svg>
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- 添加指标弹窗 -->
87
+ <Teleport :to="teleportTarget">
88
+ <Transition name="overlay">
89
+ <div v-if="showAddMenu" class="selector-overlay" @click="closeAddMenu">
90
+ <Transition name="modal">
91
+ <div v-if="showAddMenu" class="selector-modal" @click.stop>
92
+ <!-- 弹窗头部 -->
93
+ <div class="modal-header">
94
+ <div class="header-title">
95
+ <span class="title-text">添加指标</span>
96
+ <span class="title-sub">{{ totalIndicatorsCount }} 个可用指标</span>
97
+ </div>
98
+ <div class="header-actions">
99
+ <button
100
+ class="view-toggle-btn"
101
+ :class="{ active: isCompactView }"
102
+ @click="isCompactView = !isCompactView"
103
+ title="简洁模式"
104
+ >
105
+ <svg
106
+ v-if="!isCompactView"
107
+ viewBox="0 0 24 24"
108
+ width="16"
109
+ height="16"
110
+ fill="currentColor"
111
+ >
112
+ <path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z" />
113
+ </svg>
114
+ <svg v-else viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
115
+ <path
116
+ d="M3 3h18v18H3V3zm16 16V5H5v14h14zM7 7h4v4H7V7zm0 6h4v4H7v-4zm6-6h4v4h-4V7zm0 6h4v4h-4v-4z"
117
+ />
118
+ </svg>
119
+ </button>
120
+ <button class="modal-close" @click="closeAddMenu" title="关闭">
121
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
122
+ <path
123
+ d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
124
+ />
125
+ </svg>
126
+ </button>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- 弹窗主体 -->
131
+ <div class="modal-body">
132
+ <!-- 搜索框 -->
133
+ <div class="search-box">
134
+ <svg
135
+ class="search-icon"
136
+ viewBox="0 0 24 24"
137
+ width="16"
138
+ height="16"
139
+ fill="currentColor"
140
+ >
141
+ <path
142
+ d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
143
+ />
144
+ </svg>
145
+ <input
146
+ v-model="searchQuery"
147
+ type="text"
148
+ class="search-input"
149
+ placeholder="搜索指标名称..."
150
+ />
151
+ </div>
152
+ <!-- 主图指标区域 -->
153
+ <div v-if="filteredMainIndicators.length > 0" class="indicator-section">
154
+ <div class="section-header">
155
+ <span class="section-title">主图指标</span>
156
+ <span class="section-count">{{ filteredMainIndicators.length }}</span>
157
+ </div>
158
+ <div class="indicator-grid" :class="{ compact: isCompactView }">
159
+ <button
160
+ v-for="indicator in filteredMainIndicators"
161
+ :key="indicator.id"
162
+ class="indicator-card"
163
+ :class="{ active: isActive(indicator.id), compact: isCompactView }"
164
+ @click="
165
+ isActive(indicator.id)
166
+ ? removeIndicator(indicator.id)
167
+ : addIndicator(indicator.id)
168
+ "
169
+ >
170
+ <template v-if="isCompactView">
171
+ <span class="card-label">{{ indicator.label }}</span>
172
+ <span class="card-tooltip">{{ indicator.name }}</span>
173
+ </template>
174
+ <template v-else>
175
+ <div class="card-header">
176
+ <span class="card-label">{{ indicator.label }}</span>
177
+ <div class="card-header-actions">
178
+ <button
179
+ v-if="indicator.params?.length"
180
+ class="card-settings-btn"
181
+ @click.stop="showParams(indicator.id)"
182
+ title="编辑参数"
183
+ >
184
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
185
+ <path
186
+ d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
187
+ />
188
+ </svg>
189
+ </button>
190
+ </div>
191
+ </div>
192
+ <div class="card-name">{{ indicator.name }}</div>
193
+ </template>
194
+ </button>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- 分隔线 -->
199
+ <div
200
+ v-if="filteredMainIndicators.length > 0 && filteredSubIndicators.length > 0"
201
+ class="section-divider"
202
+ ></div>
203
+
204
+ <!-- 无匹配结果提示 -->
205
+ <div v-if="!hasSearchResults && searchQuery.trim()" class="no-results">
206
+ <svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor">
207
+ <path
208
+ d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
209
+ />
210
+ </svg>
211
+ <p>未找到匹配的指标</p>
212
+ <span class="no-results-hint">请尝试其他关键词</span>
213
+ </div>
214
+
215
+ <!-- 副图指标区域 -->
216
+ <div v-if="filteredSubIndicators.length > 0" class="indicator-section">
217
+ <div class="section-header">
218
+ <span class="section-title">副图指标</span>
219
+ <span class="section-count">{{ filteredSubIndicators.length }}</span>
220
+ </div>
221
+ <div class="indicator-grid" :class="{ compact: isCompactView }">
222
+ <button
223
+ v-for="indicator in filteredSubIndicators"
224
+ :key="indicator.id"
225
+ class="indicator-card"
226
+ :class="{ active: isActive(indicator.id), compact: isCompactView }"
227
+ @click="
228
+ isActive(indicator.id)
229
+ ? removeIndicator(indicator.id)
230
+ : addIndicator(indicator.id)
231
+ "
232
+ >
233
+ <template v-if="isCompactView">
234
+ <span class="card-label">{{ indicator.label }}</span>
235
+ <span class="card-tooltip">{{ indicator.name }}</span>
236
+ </template>
237
+ <template v-else>
238
+ <div class="card-header">
239
+ <span class="card-label">{{ indicator.label }}</span>
240
+ <div class="card-header-actions">
241
+ <button
242
+ v-if="indicator.params?.length"
243
+ class="card-settings-btn"
244
+ @click.stop="showParams(indicator.id)"
245
+ title="编辑参数"
246
+ >
247
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
248
+ <path
249
+ d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
250
+ />
251
+ </svg>
252
+ </button>
253
+ </div>
254
+ </div>
255
+ <div class="card-name">{{ indicator.name }}</div>
256
+ </template>
257
+ </button>
258
+ </div>
259
+ </div>
260
+ </div>
261
+
262
+ <!-- 弹窗底部 -->
263
+ <div class="modal-footer">
264
+ <div class="footer-info">
265
+ <span class="info-text">已激活 {{ activeCount }} 个指标</span>
266
+ </div>
267
+ <button class="btn btn-confirm" @click="closeAddMenu">确认</button>
268
+ </div>
269
+ </div>
270
+ </Transition>
271
+ </div>
272
+ </Transition>
273
+ </Teleport>
274
+
275
+ <!-- 参数编辑弹窗 -->
276
+ <IndicatorParams
277
+ v-if="currentIndicator"
278
+ :visible="paramsVisible"
279
+ :indicator-id="currentIndicator.id"
280
+ :indicator-name="currentIndicator.name"
281
+ :indicator-description="currentIndicator.description"
282
+ :params="currentIndicator.params || []"
283
+ :values="getParamValues(currentIndicator.id)"
284
+ @close="paramsVisible = false"
285
+ @confirm="onParamsConfirm"
286
+ />
287
+ </div>
288
+ </template>
289
+
290
+ <script setup lang="ts">
291
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
292
+ import IndicatorParams from './IndicatorParams.vue'
293
+ import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
294
+ import {
295
+ mainIndicators,
296
+ subIndicators,
297
+ findIndicator,
298
+ isSubIndicatorId,
299
+ } from '@363045841yyt/klinechart-core/engine/renderers/Indicator/indicatorData'
300
+ import type { Indicator } from '@363045841yyt/klinechart-core/engine/renderers/Indicator/indicatorData'
301
+
302
+ const props = defineProps<{
303
+ activeIndicators?: string[]
304
+ indicatorParams?: Record<string, Record<string, unknown>>
305
+ }>()
306
+
307
+ const emit = defineEmits<{
308
+ toggle: [indicatorId: string, active: boolean]
309
+ updateParams: [indicatorId: string, params: Record<string, number>]
310
+ reorderSubIndicators: [orderedIndicatorIds: string[]]
311
+ }>()
312
+
313
+ const addBtnRef = ref<HTMLButtonElement | null>(null)
314
+ const paramsVisible = ref(false)
315
+ const currentIndicatorId = ref<string | null>(null)
316
+ const hoveredIndicator = ref<string | null>(null)
317
+ const showAddMenu = ref(false)
318
+ const dragOverIndicatorId = ref<string | null>(null)
319
+ const draggingIndicatorId = ref<string | null>(null)
320
+ const isCompactView = ref(false)
321
+ const searchQuery = ref('')
322
+
323
+ // Teleport target for fullscreen modal visibility
324
+ const teleportTarget = useFullscreenTeleportTarget()
325
+
326
+ const activeIndicatorsList = computed(() => {
327
+ if (!props.activeIndicators?.length) return []
328
+ return props.activeIndicators
329
+ .map((id) => findIndicator(id))
330
+ .filter((i): i is Indicator => i !== undefined)
331
+ .sort((a, b) => {
332
+ if (a.pane === b.pane) return 0
333
+ return a.pane === 'main' ? -1 : 1
334
+ })
335
+ })
336
+
337
+ const firstActiveSubIndicatorId = computed(() => {
338
+ const hasMain = activeIndicatorsList.value.some((indicator) => indicator.pane === 'main')
339
+ if (!hasMain) return null
340
+ const firstSub = activeIndicatorsList.value.find((indicator) => indicator.pane === 'sub')
341
+ return firstSub?.id ?? null
342
+ })
343
+
344
+ const currentIndicator = computed(() => {
345
+ if (!currentIndicatorId.value) return null
346
+ return findIndicator(currentIndicatorId.value)
347
+ })
348
+
349
+ const totalIndicatorsCount = computed(() => mainIndicators.length + subIndicators.length)
350
+
351
+ const activeCount = computed(() => props.activeIndicators?.length ?? 0)
352
+
353
+ // 过滤后的主图指标
354
+ const filteredMainIndicators = computed(() => {
355
+ if (!searchQuery.value.trim()) return mainIndicators
356
+ const query = searchQuery.value.toLowerCase().trim()
357
+ return mainIndicators.filter(
358
+ (i) =>
359
+ i.label.toLowerCase().includes(query) ||
360
+ i.name.toLowerCase().includes(query) ||
361
+ i.id.toLowerCase().includes(query),
362
+ )
363
+ })
364
+
365
+ // 过滤后的副图指标
366
+ const filteredSubIndicators = computed(() => {
367
+ if (!searchQuery.value.trim()) return subIndicators
368
+ const query = searchQuery.value.toLowerCase().trim()
369
+ return subIndicators.filter(
370
+ (i) =>
371
+ i.label.toLowerCase().includes(query) ||
372
+ i.name.toLowerCase().includes(query) ||
373
+ i.id.toLowerCase().includes(query),
374
+ )
375
+ })
376
+
377
+ // 是否有搜索结果
378
+ const hasSearchResults = computed(
379
+ () => filteredMainIndicators.value.length > 0 || filteredSubIndicators.value.length > 0,
380
+ )
381
+
382
+ function isActive(indicatorId: string): boolean {
383
+ return props.activeIndicators?.includes(indicatorId) ?? false
384
+ }
385
+
386
+ function addIndicator(indicatorId: string) {
387
+ if (isActive(indicatorId)) return
388
+
389
+ const indicator = findIndicator(indicatorId)
390
+ if (!indicator) return
391
+
392
+ if (indicator.pane === 'main') {
393
+ mainIndicators
394
+ .filter((i) => i.id !== indicatorId && isActive(i.id))
395
+ .forEach((i) => emit('toggle', i.id, false))
396
+ }
397
+
398
+ emit('toggle', indicatorId, true)
399
+ }
400
+
401
+ function removeIndicator(indicatorId: string) {
402
+ emit('toggle', indicatorId, false)
403
+ }
404
+
405
+ function showParams(indicatorId: string) {
406
+ currentIndicatorId.value = indicatorId
407
+ paramsVisible.value = true
408
+ }
409
+
410
+ function closeAddMenu() {
411
+ showAddMenu.value = false
412
+ }
413
+
414
+ function getParamValues(indicatorId: string): Record<string, number> {
415
+ const indicator = findIndicator(indicatorId)
416
+ if (!indicator?.params) return {}
417
+
418
+ const defaultParams: Record<string, number> = {}
419
+ for (const p of indicator.params) {
420
+ defaultParams[p.key] = p.default ?? p.min ?? 1
421
+ }
422
+
423
+ const userParams = props.indicatorParams?.[indicatorId] || {}
424
+ const result: Record<string, number> = { ...defaultParams }
425
+
426
+ for (const [key, value] of Object.entries(userParams)) {
427
+ if (typeof value === 'number') {
428
+ result[key] = value
429
+ }
430
+ }
431
+
432
+ return result
433
+ }
434
+
435
+ function getParamDisplay(indicator: Indicator): string {
436
+ const values = getParamValues(indicator.id)
437
+ if (!indicator.params) return ''
438
+ return indicator.params.map((p) => values[p.key] ?? '').join(',')
439
+ }
440
+
441
+ function onParamsConfirm(values: Record<string, number>) {
442
+ if (currentIndicatorId.value) {
443
+ emit('updateParams', currentIndicatorId.value, values)
444
+ }
445
+ paramsVisible.value = false
446
+ }
447
+
448
+ function onDragStart(event: DragEvent, indicatorId: string) {
449
+ if (!isSubIndicatorId(indicatorId)) {
450
+ event.preventDefault()
451
+ return
452
+ }
453
+ draggingIndicatorId.value = indicatorId
454
+ dragOverIndicatorId.value = null
455
+ event.dataTransfer?.setData('text/plain', indicatorId)
456
+ if (event.dataTransfer) {
457
+ event.dataTransfer.effectAllowed = 'move'
458
+ }
459
+ }
460
+
461
+ function onDragOver(event: DragEvent, indicatorId: string) {
462
+ if (
463
+ !draggingIndicatorId.value ||
464
+ !isSubIndicatorId(indicatorId) ||
465
+ draggingIndicatorId.value === indicatorId
466
+ )
467
+ return
468
+ dragOverIndicatorId.value = indicatorId
469
+ if (event.dataTransfer) {
470
+ event.dataTransfer.dropEffect = 'move'
471
+ }
472
+ }
473
+
474
+ function onDrop(event: DragEvent, targetIndicatorId: string) {
475
+ const sourceIndicatorId =
476
+ draggingIndicatorId.value || event.dataTransfer?.getData('text/plain') || ''
477
+ if (!sourceIndicatorId || sourceIndicatorId === targetIndicatorId) {
478
+ onDragEnd()
479
+ return
480
+ }
481
+ if (!isSubIndicatorId(sourceIndicatorId) || !isSubIndicatorId(targetIndicatorId)) {
482
+ onDragEnd()
483
+ return
484
+ }
485
+
486
+ const sourceIndex = activeIndicatorsList.value.findIndex((i) => i.id === sourceIndicatorId)
487
+ const targetIndex = activeIndicatorsList.value.findIndex((i) => i.id === targetIndicatorId)
488
+ if (sourceIndex < 0 || targetIndex < 0) {
489
+ onDragEnd()
490
+ return
491
+ }
492
+
493
+ const next = [...activeIndicatorsList.value.map((i) => i.id)]
494
+ const [moved] = next.splice(sourceIndex, 1)
495
+ if (!moved) {
496
+ onDragEnd()
497
+ return
498
+ }
499
+ next.splice(targetIndex, 0, moved)
500
+
501
+ emit(
502
+ 'reorderSubIndicators',
503
+ next.filter((id) => isSubIndicatorId(id)),
504
+ )
505
+ onDragEnd()
506
+ }
507
+
508
+ function onDragEnd() {
509
+ dragOverIndicatorId.value = null
510
+ draggingIndicatorId.value = null
511
+ }
512
+
513
+ function toggleAddMenu() {
514
+ showAddMenu.value = !showAddMenu.value
515
+ }
516
+
517
+ function handleKeydown(event: KeyboardEvent) {
518
+ if (event.key === 'Escape' && showAddMenu.value) {
519
+ showAddMenu.value = false
520
+ }
521
+ }
522
+
523
+ onMounted(() => {
524
+ document.addEventListener('keydown', handleKeydown)
525
+ })
526
+
527
+ onUnmounted(() => {
528
+ document.removeEventListener('keydown', handleKeydown)
529
+ })
530
+ </script>
531
+
532
+ <style scoped>
533
+ .indicator-selector {
534
+ margin: 20px;
535
+ width: 80%;
536
+ position: relative;
537
+ }
538
+
539
+ .indicator-scroll-container {
540
+ width: 100%;
541
+ overflow-x: auto;
542
+ overflow-y: hidden;
543
+ scrollbar-width: none;
544
+ -webkit-overflow-scrolling: touch;
545
+ text-align: center;
546
+ }
547
+
548
+ .indicator-scroll-container::-webkit-scrollbar {
549
+ display: none;
550
+ }
551
+
552
+ .indicator-list {
553
+ display: inline-flex;
554
+ gap: 8px;
555
+ padding: 2px;
556
+ margin: 0 auto;
557
+ }
558
+
559
+ .indicator-divider {
560
+ width: 1px;
561
+ height: 20px;
562
+ align-self: center;
563
+ background: #d9d9d9;
564
+ }
565
+
566
+ .indicator-item {
567
+ display: flex;
568
+ align-items: center;
569
+ gap: 4px;
570
+ }
571
+
572
+ .indicator-item.draggable,
573
+ .indicator-item.draggable .indicator-btn,
574
+ .indicator-item.draggable:hover,
575
+ .indicator-item.draggable:hover .indicator-btn {
576
+ cursor: move;
577
+ }
578
+
579
+ .indicator-item.is-dragging {
580
+ opacity: 0.6;
581
+ }
582
+
583
+ .indicator-item.drag-over .indicator-btn {
584
+ border-color: #1a1a1a;
585
+ box-shadow: 0 0 0 2px rgba(26, 26, 26, 0.12);
586
+ }
587
+
588
+ .indicator-btn-wrapper {
589
+ position: relative;
590
+ }
591
+
592
+ .indicator-btn {
593
+ position: relative;
594
+ flex-shrink: 0;
595
+ padding: 6px 16px;
596
+ border: 1px solid #e0e0e0;
597
+ border-radius: 16px;
598
+ background: #ffffff;
599
+ color: #666;
600
+ font-size: 13px;
601
+ font-weight: 500;
602
+ cursor: pointer;
603
+ transition: all 0.3s ease;
604
+ white-space: nowrap;
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: center;
608
+ gap: 4px;
609
+ overflow: hidden;
610
+ }
611
+
612
+ .indicator-btn:hover:not(.hovering) {
613
+ background: #f8f8f8;
614
+ border-color: #ccc;
615
+ color: #333;
616
+ }
617
+
618
+ .indicator-btn.active {
619
+ background: #f8f8f8;
620
+ border-color: #1a1a1a;
621
+ color: #1a1a1a;
622
+ }
623
+
624
+ .indicator-btn.active:hover:not(.hovering) {
625
+ background: #f0f0f0;
626
+ border-color: #333;
627
+ }
628
+
629
+ .btn-content {
630
+ position: relative;
631
+ z-index: 1;
632
+ }
633
+
634
+ .param-hint {
635
+ font-size: 11px;
636
+ opacity: 0.85;
637
+ }
638
+
639
+ /* 悬浮操作层 */
640
+ .hover-overlay {
641
+ position: absolute;
642
+ top: 0;
643
+ left: 0;
644
+ right: 0;
645
+ bottom: 0;
646
+ display: flex;
647
+ align-items: center;
648
+ justify-content: center;
649
+ gap: 4px;
650
+ background: rgba(255, 255, 255, 0.85);
651
+ backdrop-filter: blur(4px);
652
+ border-radius: 16px;
653
+ z-index: 2;
654
+ }
655
+
656
+ .action-btn {
657
+ width: 24px;
658
+ height: 24px;
659
+ padding: 0;
660
+ border: none;
661
+ border-radius: 50%;
662
+ background: transparent;
663
+ color: #666;
664
+ cursor: pointer;
665
+ display: flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ transition: all 0.2s;
669
+ }
670
+
671
+ .action-btn:hover {
672
+ background: rgba(0, 0, 0, 0.06);
673
+ color: #333;
674
+ }
675
+
676
+ .settings-btn:hover {
677
+ color: #1a1a1a;
678
+ }
679
+
680
+ .remove-btn:hover {
681
+ color: #ff4d4f;
682
+ }
683
+
684
+ .divider {
685
+ width: 1px;
686
+ height: 14px;
687
+ background: #e0e0e0;
688
+ }
689
+
690
+ /* 添加按钮 */
691
+ .add-btn {
692
+ flex-shrink: 0;
693
+ width: 32px;
694
+ height: 32px;
695
+ padding: 0;
696
+ border: 1px dashed #d9d9d9;
697
+ border-radius: 50%;
698
+ background: transparent;
699
+ color: #999;
700
+ cursor: pointer;
701
+ display: flex;
702
+ align-items: center;
703
+ justify-content: center;
704
+ transition: all 0.3s ease;
705
+ }
706
+
707
+ .add-btn:hover {
708
+ border-color: #1a1a1a;
709
+ color: #1a1a1a;
710
+ background: rgba(26, 26, 26, 0.04);
711
+ }
712
+
713
+ /* ─────────────────────────────────────────────────────────────────
714
+ 弹窗样式 - 与其他弹窗保持一致
715
+ ───────────────────────────────────────────────────────────────── */
716
+
717
+ /* 遮罩层 */
718
+ .selector-overlay {
719
+ position: fixed;
720
+ inset: 0;
721
+ background: rgba(0, 0, 0, 0.3);
722
+ backdrop-filter: blur(4px);
723
+ display: flex;
724
+ align-items: center;
725
+ justify-content: center;
726
+ z-index: 1000;
727
+ }
728
+
729
+ /* 弹窗容器 */
730
+ .selector-modal {
731
+ background: #ffffff;
732
+ border: 1px solid #e0e0e0;
733
+ border-radius: 12px;
734
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
735
+ width: 90vw;
736
+ max-width: 860px;
737
+ max-height: 85vh;
738
+ overflow: hidden;
739
+ display: flex;
740
+ flex-direction: column;
741
+ }
742
+
743
+ /* 弹窗头部 */
744
+ .modal-header {
745
+ display: flex;
746
+ justify-content: space-between;
747
+ align-items: center;
748
+ padding: 16px 20px;
749
+ background: #f8f8f8;
750
+ border-bottom: 1px solid #e8e8e8;
751
+ flex-shrink: 0;
752
+ }
753
+
754
+ .header-title {
755
+ display: flex;
756
+ flex-direction: column;
757
+ gap: 2px;
758
+ }
759
+
760
+ .title-text {
761
+ font-size: 14px;
762
+ font-weight: 600;
763
+ color: #1a1a1a;
764
+ letter-spacing: 0.2px;
765
+ }
766
+
767
+ .title-sub {
768
+ font-size: 11px;
769
+ color: #999;
770
+ }
771
+
772
+ .modal-close {
773
+ background: #fff;
774
+ border: 1px solid #e0e0e0;
775
+ border-radius: 6px;
776
+ width: 28px;
777
+ height: 28px;
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ cursor: pointer;
782
+ color: #888;
783
+ transition: all 0.15s;
784
+ padding: 0;
785
+ }
786
+
787
+ .modal-close:hover {
788
+ background: #f0f0f0;
789
+ color: #333;
790
+ border-color: #ccc;
791
+ }
792
+
793
+ .modal-close svg {
794
+ width: 14px;
795
+ height: 14px;
796
+ }
797
+
798
+ .header-actions {
799
+ display: flex;
800
+ align-items: center;
801
+ gap: 8px;
802
+ }
803
+
804
+ .view-toggle-btn {
805
+ background: #fff;
806
+ border: 1px solid #e0e0e0;
807
+ border-radius: 6px;
808
+ width: 28px;
809
+ height: 28px;
810
+ display: flex;
811
+ align-items: center;
812
+ justify-content: center;
813
+ cursor: pointer;
814
+ color: #888;
815
+ transition: all 0.15s;
816
+ padding: 0;
817
+ }
818
+
819
+ .view-toggle-btn:hover {
820
+ background: #f0f0f0;
821
+ color: #333;
822
+ border-color: #ccc;
823
+ }
824
+
825
+ .view-toggle-btn.active {
826
+ background: #1a1a1a;
827
+ border-color: #1a1a1a;
828
+ color: #fff;
829
+ }
830
+
831
+ /* 弹窗主体 */
832
+ .modal-body {
833
+ padding: 20px;
834
+ overflow-y: auto;
835
+ flex: 1;
836
+ display: flex;
837
+ flex-direction: column;
838
+ gap: 20px;
839
+ }
840
+
841
+ /* 搜索框 */
842
+ .search-box {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 10px;
846
+ padding: 10px 14px;
847
+ border: 1px solid #e0e0e0;
848
+ border-radius: 8px;
849
+ transition: all 0.2s ease;
850
+ }
851
+
852
+ .search-box:focus-within {
853
+ background: #ffffff;
854
+ border-color: #1a1a1a;
855
+ box-shadow: 0 0 0 2px rgba(26, 26, 26, 0.08);
856
+ }
857
+
858
+ .search-icon {
859
+ flex-shrink: 0;
860
+ color: #999;
861
+ }
862
+
863
+ .search-input {
864
+ flex: 1;
865
+ border: none;
866
+ background: transparent;
867
+ font-size: 13px;
868
+ color: #333;
869
+ outline: none;
870
+ }
871
+
872
+ .search-input::placeholder {
873
+ color: #aaa;
874
+ }
875
+
876
+ /* 无结果提示 */
877
+ .no-results {
878
+ display: flex;
879
+ flex-direction: column;
880
+ align-items: center;
881
+ justify-content: center;
882
+ padding: 48px 20px;
883
+ color: #ccc;
884
+ gap: 12px;
885
+ }
886
+
887
+ .no-results svg {
888
+ opacity: 0.5;
889
+ }
890
+
891
+ .no-results p {
892
+ margin: 0;
893
+ font-size: 14px;
894
+ color: #999;
895
+ font-weight: 500;
896
+ }
897
+
898
+ .no-results-hint {
899
+ font-size: 12px;
900
+ color: #bbb;
901
+ }
902
+
903
+ /* 指标区域 */
904
+ .indicator-section {
905
+ display: flex;
906
+ flex-direction: column;
907
+ gap: 12px;
908
+ }
909
+
910
+ .section-header {
911
+ display: flex;
912
+ align-items: center;
913
+ gap: 8px;
914
+ }
915
+
916
+ .section-title {
917
+ font-size: 13px;
918
+ font-weight: 600;
919
+ color: #1a1a1a;
920
+ }
921
+
922
+ .section-count {
923
+ font-size: 11px;
924
+ color: #999;
925
+ background: #f0f0f0;
926
+ padding: 2px 8px;
927
+ border-radius: 10px;
928
+ }
929
+
930
+ /* 自适应列数网格 */
931
+ .indicator-grid {
932
+ display: grid;
933
+ grid-template-columns: repeat(auto-fill, minmax(195px, 1fr));
934
+ gap: 10px;
935
+ }
936
+
937
+ /* 紧凑模式 - 标签形式 */
938
+ .indicator-grid.compact {
939
+ display: flex;
940
+ flex-wrap: wrap;
941
+ gap: 8px;
942
+ }
943
+
944
+ .indicator-grid.compact .indicator-card {
945
+ display: inline-flex;
946
+ align-items: center;
947
+ justify-content: center;
948
+ padding: 6px 14px;
949
+ border-radius: 16px;
950
+ min-height: 32px;
951
+ white-space: nowrap;
952
+ position: relative;
953
+ }
954
+
955
+ .indicator-grid.compact .indicator-card .card-tooltip {
956
+ position: absolute;
957
+ bottom: calc(100% + 6px);
958
+ left: 50%;
959
+ transform: translateX(-50%);
960
+ padding: 4px 10px;
961
+ border-radius: 6px;
962
+ background: #333;
963
+ color: #fff;
964
+ font-size: 12px;
965
+ white-space: nowrap;
966
+ pointer-events: none;
967
+ opacity: 0;
968
+ transition: opacity 0.15s ease;
969
+ z-index: 10;
970
+ }
971
+
972
+ .indicator-grid.compact .indicator-card:hover .card-tooltip {
973
+ opacity: 1;
974
+ }
975
+
976
+ .indicator-grid.compact .indicator-card .card-label {
977
+ font-size: 12px;
978
+ font-weight: 500;
979
+ }
980
+
981
+ /* 指标卡片 */
982
+ .indicator-card {
983
+ display: flex;
984
+ flex-direction: column;
985
+ gap: 4px;
986
+ padding: 12px 14px;
987
+ border: 1px solid #e8e8e8;
988
+ border-radius: 8px;
989
+ background: #ffffff;
990
+ cursor: pointer;
991
+ transition: all 0.15s;
992
+ text-align: left;
993
+ }
994
+
995
+ .indicator-card:hover:not(.disabled) {
996
+ border-color: #1a1a1a;
997
+ background: #fafafa;
998
+ transform: translateY(-1px);
999
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1000
+ }
1001
+
1002
+ .indicator-card.active {
1003
+ border-color: #1a1a1a;
1004
+ background: #f8f8f8;
1005
+ }
1006
+
1007
+ .card-header {
1008
+ display: flex;
1009
+ align-items: center;
1010
+ justify-content: space-between;
1011
+ gap: 8px;
1012
+ }
1013
+
1014
+ .card-label {
1015
+ font-size: 13px;
1016
+ font-weight: 600;
1017
+ color: #1a1a1a;
1018
+ }
1019
+
1020
+ .card-header-actions {
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 4px;
1024
+ }
1025
+
1026
+ .card-settings-btn {
1027
+ display: flex;
1028
+ align-items: center;
1029
+ justify-content: center;
1030
+ width: 20px;
1031
+ height: 20px;
1032
+ padding: 0;
1033
+ border: none;
1034
+ border-radius: 4px;
1035
+ background: transparent;
1036
+ color: #bbb;
1037
+ cursor: pointer;
1038
+ transition: all 0.15s;
1039
+ }
1040
+
1041
+ .card-settings-btn:hover {
1042
+ background: #f0f0f0;
1043
+ color: #555;
1044
+ }
1045
+
1046
+ .card-name {
1047
+ font-size: 11px;
1048
+ color: #666;
1049
+ line-height: 1.4;
1050
+ }
1051
+
1052
+ .card-params {
1053
+ font-size: 10px;
1054
+ color: #999;
1055
+ margin-top: 2px;
1056
+ }
1057
+
1058
+ /* 区域分隔线 */
1059
+ .section-divider {
1060
+ height: 1px;
1061
+ background: linear-gradient(90deg, transparent, #e0e0e0, transparent);
1062
+ margin: 4px 0;
1063
+ }
1064
+
1065
+ /* 弹窗底部 */
1066
+ .modal-footer {
1067
+ display: flex;
1068
+ align-items: center;
1069
+ justify-content: space-between;
1070
+ padding: 12px 20px;
1071
+ background: #f8f8f8;
1072
+ border-top: 1px solid #e8e8e8;
1073
+ flex-shrink: 0;
1074
+ }
1075
+
1076
+ .footer-info {
1077
+ font-size: 12px;
1078
+ color: #666;
1079
+ }
1080
+
1081
+ .info-text {
1082
+ color: #999;
1083
+ }
1084
+
1085
+ /* 按钮样式 */
1086
+ .btn {
1087
+ display: flex;
1088
+ align-items: center;
1089
+ gap: 5px;
1090
+ padding: 6px 16px;
1091
+ border-radius: 7px;
1092
+ font-size: 13px;
1093
+ font-weight: 500;
1094
+ cursor: pointer;
1095
+ border: 1px solid transparent;
1096
+ transition: all 0.15s;
1097
+ line-height: 1.4;
1098
+ }
1099
+
1100
+ .btn-confirm {
1101
+ background: #1a1a1a;
1102
+ border-color: #1a1a1a;
1103
+ color: #fff;
1104
+ }
1105
+
1106
+ .btn-confirm:hover {
1107
+ background: #333;
1108
+ border-color: #333;
1109
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
1110
+ transform: translateY(-1px);
1111
+ }
1112
+
1113
+ /* 过渡动画 */
1114
+ .fade-enter-active,
1115
+ .fade-leave-active {
1116
+ transition: opacity 0.2s ease;
1117
+ }
1118
+
1119
+ .fade-enter-from,
1120
+ .fade-leave-to {
1121
+ opacity: 0;
1122
+ }
1123
+
1124
+ /* 遮罩层动画 */
1125
+ .overlay-enter-active,
1126
+ .overlay-leave-active {
1127
+ transition: opacity 0.2s ease;
1128
+ }
1129
+
1130
+ .overlay-enter-from,
1131
+ .overlay-leave-to {
1132
+ opacity: 0;
1133
+ }
1134
+
1135
+ /* 弹窗动画 */
1136
+ .modal-enter-active {
1137
+ transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
1138
+ }
1139
+
1140
+ .modal-leave-active {
1141
+ transition: all 0.16s ease-in;
1142
+ }
1143
+
1144
+ .modal-enter-from {
1145
+ opacity: 0;
1146
+ transform: scale(0.88) translateY(-16px);
1147
+ }
1148
+
1149
+ .modal-leave-to {
1150
+ opacity: 0;
1151
+ transform: scale(0.94) translateY(8px);
1152
+ }
1153
+
1154
+ /* 响应式适配 */
1155
+ @media (max-width: 640px) {
1156
+ .selector-modal {
1157
+ width: 95vw;
1158
+ max-height: 90vh;
1159
+ }
1160
+
1161
+ .indicator-grid {
1162
+ grid-template-columns: 1fr;
1163
+ }
1164
+
1165
+ .modal-body {
1166
+ padding: 16px;
1167
+ }
1168
+ }
1169
+ </style>