@363045841yyt/klinechart 0.8.4 → 0.8.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.
- package/dist/components/BatchStockDialog.vue.d.ts +13 -0
- package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ExportProgressDialog.vue.d.ts +15 -0
- package/dist/components/ExportProgressDialog.vue.d.ts.map +1 -0
- package/dist/components/KLineChart.vue.d.ts +5 -9
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/composables/chart/useChartTheme.d.ts +329 -0
- package/dist/composables/chart/useChartTheme.d.ts.map +1 -0
- package/dist/composables/chart/useDrawingManager.d.ts +86 -0
- package/dist/composables/chart/useDrawingManager.d.ts.map +1 -0
- package/dist/composables/chart/useIndicatorManager.d.ts +38 -0
- package/dist/composables/chart/useIndicatorManager.d.ts.map +1 -0
- package/dist/composables/chart/useRangeSelection.d.ts +65 -0
- package/dist/composables/chart/useRangeSelection.d.ts.map +1 -0
- package/dist/composables/useTeleportedPopup.d.ts +8 -0
- package/dist/composables/useTeleportedPopup.d.ts.map +1 -0
- package/dist/index.cjs +9 -2
- package/dist/index.css +1 -1
- package/dist/index.js +1722 -1060
- package/dist/tools/calcRangeOverlayPixel.d.ts +15 -0
- package/dist/tools/calcRangeOverlayPixel.d.ts.map +1 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts +4 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts.map +1 -0
- package/dist/web-component.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BatchStockDialog.vue +293 -0
- package/src/components/CompareSymbolSelector.vue +35 -8
- package/src/components/Dropdown.vue +42 -19
- package/src/components/ExportProgressDialog.vue +226 -0
- package/src/components/KLineChart.vue +325 -396
- package/src/components/LeftToolbar.vue +2 -1
- package/src/components/SymbolSelector.vue +35 -8
- package/src/components/TopToolbar.vue +55 -2
- package/src/composables/chart/useChartTheme.ts +86 -0
- package/src/composables/chart/useDrawingManager.ts +67 -0
- package/src/composables/chart/useIndicatorManager.ts +307 -0
- package/src/composables/chart/useRangeSelection.ts +417 -0
- package/src/composables/useTeleportedPopup.ts +33 -0
- package/src/tools/calcRangeOverlayPixel.ts +28 -0
- package/src/tools/getKLineIndexByTimestamp.ts +40 -0
|
@@ -66,6 +66,75 @@
|
|
|
66
66
|
@update-style="onUpdateDrawingStyle"
|
|
67
67
|
@delete="onDeleteDrawing"
|
|
68
68
|
/>
|
|
69
|
+
<div
|
|
70
|
+
v-if="rangeSelectionReady"
|
|
71
|
+
class="range-selection-export"
|
|
72
|
+
@pointerdown.stop
|
|
73
|
+
@pointermove.stop
|
|
74
|
+
@pointerup.stop
|
|
75
|
+
>
|
|
76
|
+
<input
|
|
77
|
+
class="range-selection-export__label"
|
|
78
|
+
v-model="customStartDate"
|
|
79
|
+
:placeholder="rangeSelectionStartLabel"
|
|
80
|
+
/>
|
|
81
|
+
<span class="range-selection-export__sep">~</span>
|
|
82
|
+
<input
|
|
83
|
+
class="range-selection-export__label"
|
|
84
|
+
v-model="customEndDate"
|
|
85
|
+
:placeholder="rangeSelectionEndLabel"
|
|
86
|
+
/>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="toolbar-btn"
|
|
90
|
+
title="批量设置"
|
|
91
|
+
@click.stop="showBatchStockDialog = true"
|
|
92
|
+
>
|
|
93
|
+
批量设置
|
|
94
|
+
</button>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
class="toolbar-btn"
|
|
98
|
+
title="导出"
|
|
99
|
+
@click.stop="exportRangeToCsv"
|
|
100
|
+
>
|
|
101
|
+
导出
|
|
102
|
+
</button>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="toolbar-btn delete-btn"
|
|
106
|
+
title="删除选区"
|
|
107
|
+
@click.stop="clearRangeSelection"
|
|
108
|
+
>
|
|
109
|
+
<svg class="delete-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
110
|
+
<path d="M3 6h18" />
|
|
111
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
112
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
113
|
+
</svg>
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div
|
|
118
|
+
v-if="rangeSelectionOverlayStyle"
|
|
119
|
+
class="range-selection-overlay"
|
|
120
|
+
:class="{ 'is-dragging': rangeSelection.isDragging }"
|
|
121
|
+
:style="rangeSelectionOverlayStyle"
|
|
122
|
+
aria-label="已选择的 K 线区间"
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
v-if="rangeSelectionReady"
|
|
126
|
+
class="range-selection-handle range-selection-handle--left"
|
|
127
|
+
@pointerdown.stop="onEdgePointerDown('left', $event)"
|
|
128
|
+
@pointermove.stop="onEdgePointerMove($event)"
|
|
129
|
+
@pointerup.stop="onEdgePointerUp($event)"
|
|
130
|
+
/>
|
|
131
|
+
<div
|
|
132
|
+
v-if="rangeSelectionReady"
|
|
133
|
+
class="range-selection-handle range-selection-handle--right"
|
|
134
|
+
@pointerdown.stop="onEdgePointerDown('right', $event)"
|
|
135
|
+
@pointermove.stop="onEdgePointerMove($event)"
|
|
136
|
+
@pointerup.stop="onEdgePointerUp($event)"
|
|
137
|
+
/>
|
|
69
138
|
</div>
|
|
70
139
|
</div>
|
|
71
140
|
</div>
|
|
@@ -116,6 +185,12 @@
|
|
|
116
185
|
></div>
|
|
117
186
|
</div>
|
|
118
187
|
</div>
|
|
188
|
+
<ExportProgressDialog :progress="exportingProgress" @close="exportingProgress = null" />
|
|
189
|
+
<BatchStockDialog
|
|
190
|
+
:show="showBatchStockDialog"
|
|
191
|
+
@close="showBatchStockDialog = false"
|
|
192
|
+
@apply="onBatchApply"
|
|
193
|
+
/>
|
|
119
194
|
<IndicatorSelector
|
|
120
195
|
ref="indicatorSelectorRef"
|
|
121
196
|
:active-indicators="activeIndicators"
|
|
@@ -142,34 +217,22 @@ import { provideFullscreenTeleportTarget } from '../composables/useFullscreenTel
|
|
|
142
217
|
import {
|
|
143
218
|
createChartController,
|
|
144
219
|
type ChartController,
|
|
145
|
-
type PaneSpec,
|
|
146
|
-
type IndicatorInstance,
|
|
147
|
-
type SubIndicatorType,
|
|
148
220
|
type InteractionSnapshot,
|
|
149
|
-
type DrawingToolId,
|
|
150
|
-
type KLineData,
|
|
151
221
|
type SymbolSpec,
|
|
152
222
|
zoomLevelToKWidth,
|
|
153
223
|
kGapFromKWidth,
|
|
154
|
-
DrawingInteractionController,
|
|
155
224
|
} from '@363045841yyt/klinechart-core/controllers'
|
|
156
|
-
import {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} from '@363045841yyt/klinechart-core/indicators'
|
|
160
|
-
import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
|
|
225
|
+
import { useChartTheme } from '../composables/chart/useChartTheme'
|
|
226
|
+
import { useIndicatorManager } from '../composables/chart/useIndicatorManager'
|
|
227
|
+
import { useDrawingManager } from '../composables/chart/useDrawingManager'
|
|
161
228
|
import { SETTINGS_STORAGE_KEY } from '@363045841yyt/klinechart-core/config'
|
|
162
|
-
import
|
|
163
|
-
import {
|
|
164
|
-
resolveThemeColors,
|
|
165
|
-
themeToCssVars,
|
|
166
|
-
lightTheme,
|
|
167
|
-
darkTheme,
|
|
168
|
-
type ColorPresetSettings,
|
|
169
|
-
} from '@363045841yyt/klinechart-core'
|
|
229
|
+
import { useRangeSelection } from '../composables/chart/useRangeSelection'
|
|
170
230
|
import LeftToolbar from './LeftToolbar.vue'
|
|
171
231
|
import TopToolbar, { type SymbolItem } from './TopToolbar.vue'
|
|
232
|
+
import BatchStockDialog from './BatchStockDialog.vue'
|
|
233
|
+
import ExportProgressDialog from './ExportProgressDialog.vue'
|
|
172
234
|
|
|
235
|
+
// ── Props & Emits ──
|
|
173
236
|
const props = withDefaults(
|
|
174
237
|
defineProps<{
|
|
175
238
|
/** 语义化配置(可选,唯一控制源) */
|
|
@@ -231,6 +294,7 @@ const emit = defineEmits<{
|
|
|
231
294
|
(e: 'kLineAdjustChange', adjust: 'qfq' | 'hfq' | 'splits' | 'none'): void
|
|
232
295
|
}>()
|
|
233
296
|
|
|
297
|
+
// ── Symbol / Comparison State ──
|
|
234
298
|
const kLineLevel = ref(props.semanticConfig?.data?.period ?? 'daily')
|
|
235
299
|
const kLineAdjust = ref(props.semanticConfig?.data?.adjust ?? 'none')
|
|
236
300
|
const isIntraday = computed(() => kLineLevel.value.includes('min'))
|
|
@@ -302,9 +366,12 @@ function forcePercentAxis() {
|
|
|
302
366
|
controller.value?.updateSettingsFacade(nextSettings)
|
|
303
367
|
try {
|
|
304
368
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(nextSettings))
|
|
305
|
-
} catch {
|
|
369
|
+
} catch {
|
|
370
|
+
/* quota exceeded */
|
|
371
|
+
}
|
|
306
372
|
}
|
|
307
373
|
|
|
374
|
+
// ── DOM Template Refs ──
|
|
308
375
|
const containerRef = ref<HTMLDivElement | null>(null)
|
|
309
376
|
const chartMainRef = ref<HTMLDivElement | null>(null)
|
|
310
377
|
const chartWrapperRef = ref<HTMLDivElement | null>(null)
|
|
@@ -313,26 +380,100 @@ const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
|
|
|
313
380
|
const indicatorSelectorRef = ref<InstanceType<typeof IndicatorSelector> | null>(null)
|
|
314
381
|
provideFullscreenTeleportTarget(chartWrapperRef)
|
|
315
382
|
|
|
316
|
-
|
|
383
|
+
// ── Controller & Composable Wiring ──
|
|
317
384
|
const controller = shallowRef<ChartController | null>(null)
|
|
318
385
|
|
|
319
|
-
|
|
386
|
+
const {
|
|
387
|
+
chartTheme,
|
|
388
|
+
chartSettings,
|
|
389
|
+
tooltipColors,
|
|
390
|
+
themeCssVars,
|
|
391
|
+
handleSettingsChange,
|
|
392
|
+
applyThemeFromSettings,
|
|
393
|
+
} = useChartTheme(controller)
|
|
394
|
+
|
|
320
395
|
const semanticController = shallowRef<SemanticChartController | null>(null)
|
|
321
396
|
|
|
322
|
-
/* ==========
|
|
397
|
+
/* ========== 本地响应式状态 ========== */
|
|
323
398
|
const dataLength = ref(0)
|
|
324
399
|
const dataVersion = ref(0)
|
|
400
|
+
const showBatchStockDialog = ref(false)
|
|
401
|
+
const batchStockCodes = ref<string[]>([])
|
|
402
|
+
const viewportVersion = ref(0)
|
|
325
403
|
const viewportDpr = ref(1)
|
|
326
404
|
const zoomLevel = ref(props.initialZoomLevel ?? 1)
|
|
327
405
|
const kWidth = ref(0)
|
|
328
406
|
const kGap = ref(1)
|
|
329
407
|
const viewWidth = ref(0)
|
|
330
408
|
const paneRatios = ref<Record<string, number>>({})
|
|
331
|
-
const selectedDrawingId = ref<string | null>(null)
|
|
332
|
-
const drawings = ref<DrawingObject[]>([])
|
|
333
409
|
const comparisonColorsMap = ref<Map<string, string>>(new Map())
|
|
334
410
|
const comparisonLoading = ref(false)
|
|
411
|
+
const activeToolId = ref('cursor')
|
|
412
|
+
|
|
413
|
+
const {
|
|
414
|
+
mainActiveIndicators,
|
|
415
|
+
subActiveIndicators,
|
|
416
|
+
activeIndicators,
|
|
417
|
+
indicatorParams,
|
|
418
|
+
subPanes,
|
|
419
|
+
buildPaneLayoutIntent,
|
|
420
|
+
getDefaultParams,
|
|
421
|
+
isSubPaneIndicator,
|
|
422
|
+
addSubPane,
|
|
423
|
+
removeSubPane,
|
|
424
|
+
clearAllSubPanes,
|
|
425
|
+
initIndicatorsFromConfig,
|
|
426
|
+
switchSubIndicator,
|
|
427
|
+
handleIndicatorToggle,
|
|
428
|
+
handleUpdateParams,
|
|
429
|
+
handleReorderSubIndicators,
|
|
430
|
+
setupIndicatorSubscriptions,
|
|
431
|
+
} = useIndicatorManager(controller, paneRatios)
|
|
432
|
+
|
|
433
|
+
const {
|
|
434
|
+
drawingController,
|
|
435
|
+
selectedDrawingId,
|
|
436
|
+
selectedDrawing,
|
|
437
|
+
drawings,
|
|
438
|
+
handleSelectTool: handleDrawingToolSelect,
|
|
439
|
+
onUpdateDrawingStyle,
|
|
440
|
+
onDeleteDrawing,
|
|
441
|
+
setupDrawing,
|
|
442
|
+
} = useDrawingManager(controller)
|
|
443
|
+
|
|
444
|
+
const {
|
|
445
|
+
rangeSelection,
|
|
446
|
+
customStartDate,
|
|
447
|
+
customEndDate,
|
|
448
|
+
containerScrollLeft,
|
|
449
|
+
isRangeSelectActive,
|
|
450
|
+
rangeSelectionReady,
|
|
451
|
+
rangeSelectionBounds,
|
|
452
|
+
rangeSelectionStartLabel,
|
|
453
|
+
rangeSelectionEndLabel,
|
|
454
|
+
rangeSelectionOverlayStyle,
|
|
455
|
+
clearRangeSelection,
|
|
456
|
+
handleRangePointerDown,
|
|
457
|
+
handleRangePointerMove,
|
|
458
|
+
handleRangePointerUp,
|
|
459
|
+
exportRangeToCsv,
|
|
460
|
+
exportingProgress,
|
|
461
|
+
onEdgePointerDown,
|
|
462
|
+
onEdgePointerMove,
|
|
463
|
+
onEdgePointerUp,
|
|
464
|
+
onScroll: onRangeScroll,
|
|
465
|
+
syncScrollLeft: syncRangeScrollLeft,
|
|
466
|
+
} = useRangeSelection({
|
|
467
|
+
controller,
|
|
468
|
+
activeToolId,
|
|
469
|
+
containerRef,
|
|
470
|
+
dataVersion,
|
|
471
|
+
viewportVersion,
|
|
472
|
+
dataFetcher: computed(() => props.dataFetcher),
|
|
473
|
+
batchStockCodes,
|
|
474
|
+
})
|
|
335
475
|
|
|
476
|
+
// ── Viewport Initial Values ──
|
|
336
477
|
// 初始化 kWidth / kGap(与 Chart 引擎 zoom→物理值 转换一致)
|
|
337
478
|
const initZoom = zoomLevel.value
|
|
338
479
|
kWidth.value = zoomLevelToKWidth(initZoom, {
|
|
@@ -343,66 +484,12 @@ kWidth.value = zoomLevelToKWidth(initZoom, {
|
|
|
343
484
|
})
|
|
344
485
|
kGap.value = kGapFromKWidth(kWidth.value, viewportDpr.value)
|
|
345
486
|
|
|
346
|
-
|
|
347
|
-
const chartTheme = ref<'light' | 'dark'>('light')
|
|
348
|
-
|
|
349
|
-
const chartSettings = ref<ChartSettings>({})
|
|
350
|
-
|
|
351
|
-
const tooltipColors = computed(() => {
|
|
352
|
-
const isAsiaMarket = chartSettings.value.isAsiaMarket ?? false
|
|
353
|
-
const colors = resolveThemeColors(chartTheme.value, isAsiaMarket as boolean | undefined)
|
|
354
|
-
return {
|
|
355
|
-
upColor: colors.candleUpBody,
|
|
356
|
-
downColor: colors.candleDownBody,
|
|
357
|
-
}
|
|
358
|
-
})
|
|
359
|
-
|
|
360
|
-
const themeCssVars = computed(() => {
|
|
361
|
-
const theme = chartTheme.value === 'dark' ? darkTheme : lightTheme
|
|
362
|
-
const overrides = (chartSettings.value.colorPresetSettings as ColorPresetSettings | undefined)?.[
|
|
363
|
-
chartTheme.value
|
|
364
|
-
]
|
|
365
|
-
if (overrides && Object.keys(overrides).length > 0) {
|
|
366
|
-
return themeToCssVars({ ...theme, colors: { ...theme.colors, ...overrides } })
|
|
367
|
-
}
|
|
368
|
-
return themeToCssVars(theme)
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
/* ========== 主题切换(支持 light / dark / auto 跟随系统) ========== */
|
|
372
|
-
let autoThemeMediaQuery: MediaQueryList | null = null
|
|
373
|
-
|
|
374
|
-
function onSystemThemeChange(e: MediaQueryListEvent) {
|
|
375
|
-
controller.value?.setTheme(e.matches ? 'dark' : 'light')
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function applyThemeFromSettings(ctrl: ChartController | null, themeSetting: string | undefined) {
|
|
379
|
-
if (!ctrl || !themeSetting) return
|
|
380
|
-
|
|
381
|
-
if (themeSetting === 'auto') {
|
|
382
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
383
|
-
ctrl.setTheme(mq.matches ? 'dark' : 'light')
|
|
384
|
-
if (autoThemeMediaQuery !== mq) {
|
|
385
|
-
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
386
|
-
autoThemeMediaQuery = mq
|
|
387
|
-
mq.addEventListener('change', onSystemThemeChange)
|
|
388
|
-
}
|
|
389
|
-
} else {
|
|
390
|
-
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
391
|
-
autoThemeMediaQuery = null
|
|
392
|
-
ctrl.setTheme(themeSetting as 'light' | 'dark')
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
487
|
+
// ── No-op Render Trigger (exposed) ──
|
|
396
488
|
function scheduleRender() {
|
|
397
489
|
/* Controller auto-renders on state changes */
|
|
398
490
|
}
|
|
399
491
|
|
|
400
|
-
|
|
401
|
-
chartSettings.value = settings
|
|
402
|
-
applyThemeFromSettings(controller.value, settings.theme as string)
|
|
403
|
-
controller.value?.updateSettingsFacade(settings)
|
|
404
|
-
}
|
|
405
|
-
|
|
492
|
+
// ── Tooltip Measurement ──
|
|
406
493
|
function measureTooltipSize(el: HTMLDivElement, minWidth: number, minHeight: number) {
|
|
407
494
|
const r = el.getBoundingClientRect()
|
|
408
495
|
return {
|
|
@@ -428,11 +515,10 @@ function setMarkerTooltipEl(el: HTMLDivElement | null) {
|
|
|
428
515
|
})
|
|
429
516
|
}
|
|
430
517
|
|
|
431
|
-
//
|
|
518
|
+
// ── Marker Tooltip & Container Rect Cache ──
|
|
432
519
|
const mousePos = ref({ x: 0, y: 0 })
|
|
433
520
|
const useAnchorPositioning = ref(false)
|
|
434
521
|
|
|
435
|
-
// 容器 rect 缓存,避免 pointermove 中反复 getBoundingClientRect 强制同步布局
|
|
436
522
|
let _cachedContainerRect: DOMRect | null = null
|
|
437
523
|
function invalidateContainerRectCache(): void {
|
|
438
524
|
_cachedContainerRect = null
|
|
@@ -444,7 +530,7 @@ function getContainerRect(container: HTMLDivElement): DOMRect {
|
|
|
444
530
|
return _cachedContainerRect
|
|
445
531
|
}
|
|
446
532
|
|
|
447
|
-
//
|
|
533
|
+
// ── Interaction State Bridge ──
|
|
448
534
|
const interactionState = shallowRef<InteractionSnapshot>({
|
|
449
535
|
crosshairPos: null,
|
|
450
536
|
crosshairIndex: null,
|
|
@@ -462,12 +548,6 @@ const interactionState = shallowRef<InteractionSnapshot>({
|
|
|
462
548
|
isHoveringRightAxis: false,
|
|
463
549
|
})
|
|
464
550
|
|
|
465
|
-
const drawingController = shallowRef<DrawingInteractionController | null>(null)
|
|
466
|
-
const selectedDrawing = computed(() => {
|
|
467
|
-
const id = selectedDrawingId.value
|
|
468
|
-
if (!id) return null
|
|
469
|
-
return drawings.value.find((d) => d.id === id) ?? null
|
|
470
|
-
})
|
|
471
551
|
const paneSeparatorLines = ref<Array<{ id: string; top: number }>>([])
|
|
472
552
|
const markerTooltipSize = ref({ width: 220, height: 120 })
|
|
473
553
|
const tooltipLayerOffset = computed(() => {
|
|
@@ -490,7 +570,7 @@ const isHoveringRightAxis = computed(() => interactionState.value.isHoveringRigh
|
|
|
490
570
|
const hoveredIdx = computed(() => interactionState.value.hoveredIndex)
|
|
491
571
|
const crosshairIdx = computed(() => interactionState.value.crosshairIndex)
|
|
492
572
|
|
|
493
|
-
//
|
|
573
|
+
// ── Derived Computed (Cursor, Hovered, Tooltip) ──
|
|
494
574
|
const containerCursor = computed(() => {
|
|
495
575
|
if (isDragging.value) return 'grabbing'
|
|
496
576
|
if (isResizingPane.value || isHoveringPaneSeparator.value) return 'ns-resize'
|
|
@@ -544,32 +624,33 @@ const chartData = computed(() => {
|
|
|
544
624
|
return controller.value?.getData() ?? []
|
|
545
625
|
})
|
|
546
626
|
|
|
547
|
-
//
|
|
548
|
-
function handleSelectTool(toolId: string) {
|
|
549
|
-
drawingController.value?.setTool(toolId as DrawingToolId)
|
|
550
|
-
}
|
|
551
|
-
|
|
627
|
+
// ── Pointer Event Handlers ──
|
|
552
628
|
function onToggleIndicator() {
|
|
553
629
|
indicatorSelectorRef.value?.toggleMenu()
|
|
554
630
|
}
|
|
555
631
|
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
if (!d || !drawingController.value) return
|
|
559
|
-
drawingController.value.updateDrawingStyle(d.id, style)
|
|
560
|
-
drawings.value = drawingController.value.getDrawings()
|
|
632
|
+
function onBatchApply(codes: string[]) {
|
|
633
|
+
batchStockCodes.value = codes
|
|
561
634
|
}
|
|
562
635
|
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
636
|
+
function handleSelectTool(toolId: string) {
|
|
637
|
+
activeToolId.value = toolId
|
|
638
|
+
if (toolId === 'range-select') {
|
|
639
|
+
drawingController.value?.setTool('cursor')
|
|
640
|
+
selectedDrawingId.value = null
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
clearRangeSelection()
|
|
645
|
+
handleDrawingToolSelect(toolId)
|
|
568
646
|
}
|
|
569
647
|
|
|
570
648
|
function onPointerDown(e: PointerEvent) {
|
|
571
649
|
controller.value?.handlePointerEvent(e, {
|
|
572
650
|
onPointerDown: (event, container) => {
|
|
651
|
+
if (handleRangePointerDown(event, container)) {
|
|
652
|
+
return true
|
|
653
|
+
}
|
|
573
654
|
if (drawingController.value?.onPointerDown(event, container)) {
|
|
574
655
|
return true
|
|
575
656
|
}
|
|
@@ -589,6 +670,9 @@ function onPointerMove(e: PointerEvent) {
|
|
|
589
670
|
}
|
|
590
671
|
controller.value?.handlePointerEvent(e, {
|
|
591
672
|
onPointerMove: (event, container) => {
|
|
673
|
+
if (handleRangePointerMove(event, container)) {
|
|
674
|
+
return true
|
|
675
|
+
}
|
|
592
676
|
if (drawingController.value?.onPointerMove(event, container)) {
|
|
593
677
|
drawings.value = drawingController.value.getDrawings()
|
|
594
678
|
return true
|
|
@@ -601,6 +685,9 @@ function onPointerMove(e: PointerEvent) {
|
|
|
601
685
|
function onPointerUp(e: PointerEvent) {
|
|
602
686
|
controller.value?.handlePointerEvent(e, {
|
|
603
687
|
onPointerUp: (event, container) => {
|
|
688
|
+
if (handleRangePointerUp(event, container)) {
|
|
689
|
+
return true
|
|
690
|
+
}
|
|
604
691
|
if (drawingController.value?.onPointerUp(event, container)) {
|
|
605
692
|
return true
|
|
606
693
|
}
|
|
@@ -630,226 +717,11 @@ function onRightAxisPointerLeave(e: PointerEvent) {
|
|
|
630
717
|
}
|
|
631
718
|
|
|
632
719
|
function onScroll() {
|
|
720
|
+
onRangeScroll()
|
|
633
721
|
controller.value?.handleScrollEvent()
|
|
634
722
|
}
|
|
635
723
|
|
|
636
|
-
//
|
|
637
|
-
const mainActiveIndicators = ref<string[]>([])
|
|
638
|
-
|
|
639
|
-
// 副图指标列表从 subPanes 自动派生
|
|
640
|
-
const subActiveIndicators = computed(() => {
|
|
641
|
-
const ids: string[] = []
|
|
642
|
-
const seen = new Set<string>()
|
|
643
|
-
for (const pane of subPanes.value) {
|
|
644
|
-
if (!seen.has(pane.indicatorId)) {
|
|
645
|
-
seen.add(pane.indicatorId)
|
|
646
|
-
ids.push(pane.indicatorId)
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return ids
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
// 最终合并列表(主图 + 副图),保持显示顺序
|
|
653
|
-
const activeIndicators = computed(() => [
|
|
654
|
-
...mainActiveIndicators.value,
|
|
655
|
-
...subActiveIndicators.value,
|
|
656
|
-
])
|
|
657
|
-
|
|
658
|
-
// 指标参数配置(MA 的 periods 是数组,需要更宽松的类型)
|
|
659
|
-
const indicatorParams = ref<Record<string, Record<string, unknown>>>({})
|
|
660
|
-
|
|
661
|
-
// 副图槽位状态
|
|
662
|
-
interface SubPaneSlot {
|
|
663
|
-
id: string // pane ID: 'RSI_0', 'MACD_0', ...
|
|
664
|
-
indicatorId: SubIndicatorType
|
|
665
|
-
params: Record<string, unknown>
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// 副图槽位数组(支持多副图)
|
|
669
|
-
const subPanes = ref<SubPaneSlot[]>([])
|
|
670
|
-
|
|
671
|
-
// 最大副图数量
|
|
672
|
-
const maxSubPanes = 4
|
|
673
|
-
|
|
674
|
-
function buildPaneLayoutIntent(): PaneSpec[] {
|
|
675
|
-
const mainRatio = paneRatios.value['main'] ?? 3
|
|
676
|
-
return subPanes.value.length === 0
|
|
677
|
-
? [{ id: 'main', ratio: mainRatio, visible: true, role: 'price' }]
|
|
678
|
-
: [
|
|
679
|
-
{ id: 'main', ratio: mainRatio, visible: true, role: 'price' },
|
|
680
|
-
...subPanes.value.map((pane) => ({
|
|
681
|
-
id: pane.id,
|
|
682
|
-
ratio: paneRatios.value[pane.id] ?? 1,
|
|
683
|
-
visible: true,
|
|
684
|
-
role: 'indicator' as const,
|
|
685
|
-
})),
|
|
686
|
-
]
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// 获取指标默认参数
|
|
690
|
-
function getDefaultParams(
|
|
691
|
-
indicatorId: SubIndicatorType,
|
|
692
|
-
): Record<string, number | boolean | string> {
|
|
693
|
-
if (indicatorId === 'VOLUME') return {}
|
|
694
|
-
const meta = getRegisteredIndicatorDefinition(indicatorId)
|
|
695
|
-
if (meta?.runtime?.defaultConfig) {
|
|
696
|
-
return { ...meta.runtime.defaultConfig } as Record<string, number | boolean | string>
|
|
697
|
-
}
|
|
698
|
-
return {}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// 副图指标判定(基于 registry category + VOLUME 特例)
|
|
702
|
-
function isSubPaneIndicator(id: string): boolean {
|
|
703
|
-
if (id === 'VOLUME') return true
|
|
704
|
-
const def = getRegisteredIndicatorDefinition(id)
|
|
705
|
-
return !!def && def.category !== 'main'
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// 添加副图(使用 Chart API)
|
|
709
|
-
function addSubPane(
|
|
710
|
-
indicatorId: SubIndicatorType = 'VOLUME',
|
|
711
|
-
params?: Record<string, number | boolean | string>,
|
|
712
|
-
): boolean {
|
|
713
|
-
if (subPanes.value.length >= maxSubPanes) {
|
|
714
|
-
return false
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const mergedParams = params ?? getDefaultParams(indicatorId)
|
|
718
|
-
|
|
719
|
-
const paneId = controller.value?.addIndicator(indicatorId, 'sub', mergedParams)
|
|
720
|
-
if (!paneId) return false
|
|
721
|
-
return true
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function removeSubPane(paneId: string): void {
|
|
725
|
-
controller.value?.removeIndicator(paneId)
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function clearAllSubPanes(): void {
|
|
729
|
-
for (const pane of subPanes.value) {
|
|
730
|
-
controller.value?.removeIndicator(pane.id)
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function initIndicatorsFromConfig(): void {
|
|
735
|
-
const config = props.semanticConfig
|
|
736
|
-
const c = controller.value
|
|
737
|
-
if (!config || !c) return
|
|
738
|
-
|
|
739
|
-
const mainIndicators = config.indicators?.main
|
|
740
|
-
if (mainIndicators) {
|
|
741
|
-
for (const indicator of mainIndicators) {
|
|
742
|
-
if (indicator.enabled) {
|
|
743
|
-
const added = c.addIndicator(
|
|
744
|
-
indicator.type,
|
|
745
|
-
'main',
|
|
746
|
-
indicator.params as Record<string, number | boolean | string>,
|
|
747
|
-
)
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
|
|
754
|
-
const nextParams = getDefaultParams(newIndicatorId)
|
|
755
|
-
controller.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
759
|
-
const c = controller.value
|
|
760
|
-
if (!c) return
|
|
761
|
-
|
|
762
|
-
const def = getRegisteredIndicatorDefinition(indicatorId)
|
|
763
|
-
const isMain = def && (def.category === 'main' || def.allowMainPane)
|
|
764
|
-
if (isMain) {
|
|
765
|
-
const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
|
|
766
|
-
if (active && !existingIndicator) {
|
|
767
|
-
c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
768
|
-
} else if (!active && existingIndicator) {
|
|
769
|
-
c.removeIndicator(indicatorId.toUpperCase())
|
|
770
|
-
}
|
|
771
|
-
return
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (isSubPaneIndicator(indicatorId)) {
|
|
775
|
-
if (active) {
|
|
776
|
-
const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
|
|
777
|
-
if (existingPane) return
|
|
778
|
-
if (subPanes.value.length >= maxSubPanes) return
|
|
779
|
-
|
|
780
|
-
const paneId = c.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
|
|
781
|
-
if (!paneId && subPanes.value.length > 0) {
|
|
782
|
-
const lastPane = subPanes.value[subPanes.value.length - 1]
|
|
783
|
-
switchSubIndicator(lastPane.id, indicatorId as SubIndicatorType)
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
const panesToRemove = subPanes.value.filter((p) => p.indicatorId === indicatorId)
|
|
787
|
-
panesToRemove.forEach((pane) => {
|
|
788
|
-
c.removeIndicator(pane.id)
|
|
789
|
-
})
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
|
|
795
|
-
if (
|
|
796
|
-
indicatorId === 'MA' ||
|
|
797
|
-
indicatorId === 'BOLL' ||
|
|
798
|
-
indicatorId === 'EXPMA' ||
|
|
799
|
-
indicatorId === 'ENE'
|
|
800
|
-
) {
|
|
801
|
-
controller.value?.updateIndicatorParams(indicatorId, params)
|
|
802
|
-
return
|
|
803
|
-
}
|
|
804
|
-
if (isSubPaneIndicator(indicatorId)) {
|
|
805
|
-
subPanes.value
|
|
806
|
-
.filter((p) => p.indicatorId === indicatorId)
|
|
807
|
-
.forEach((pane) => {
|
|
808
|
-
controller.value?.updateIndicatorParams(pane.id, params)
|
|
809
|
-
})
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
|
|
814
|
-
if (!orderedIndicatorIds.length || subPanes.value.length <= 1) return
|
|
815
|
-
|
|
816
|
-
const validOrder = orderedIndicatorIds.filter((id): id is SubIndicatorType =>
|
|
817
|
-
isSubPaneIndicator(id),
|
|
818
|
-
)
|
|
819
|
-
if (!validOrder.length) return
|
|
820
|
-
|
|
821
|
-
const paneByIndicator = new Map(subPanes.value.map((pane) => [pane.indicatorId, pane] as const))
|
|
822
|
-
const nextSubPanes: SubPaneSlot[] = []
|
|
823
|
-
|
|
824
|
-
for (const indicatorId of validOrder) {
|
|
825
|
-
const pane = paneByIndicator.get(indicatorId)
|
|
826
|
-
if (pane) {
|
|
827
|
-
nextSubPanes.push(pane)
|
|
828
|
-
paneByIndicator.delete(indicatorId)
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
if (nextSubPanes.length === 0) return
|
|
833
|
-
|
|
834
|
-
for (const pane of subPanes.value) {
|
|
835
|
-
if (paneByIndicator.has(pane.indicatorId)) {
|
|
836
|
-
nextSubPanes.push(pane)
|
|
837
|
-
paneByIndicator.delete(pane.indicatorId)
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const currentSubIds = subPanes.value.map((p) => p.id)
|
|
842
|
-
const nextSubIds = nextSubPanes.map((p) => p.id)
|
|
843
|
-
if (currentSubIds.join('|') === nextSubIds.join('|')) return
|
|
844
|
-
|
|
845
|
-
subPanes.value = nextSubPanes
|
|
846
|
-
|
|
847
|
-
const c = controller.value
|
|
848
|
-
if (!c) return
|
|
849
|
-
c.updatePaneLayout(buildPaneLayoutIntent())
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/* 计算总宽度:从 Vue 响应式状态读取,zoom 变化时自动重算 */
|
|
724
|
+
// ── Width / Zoom / Expose ──
|
|
853
725
|
const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
|
|
854
726
|
|
|
855
727
|
const totalWidth = computed(() => {
|
|
@@ -879,7 +751,7 @@ defineExpose({
|
|
|
879
751
|
getController: () => controller.value,
|
|
880
752
|
})
|
|
881
753
|
|
|
882
|
-
//
|
|
754
|
+
// ── Lifecycle Setup ──
|
|
883
755
|
|
|
884
756
|
function setupWheelHandler(): (e: WheelEvent) => void {
|
|
885
757
|
const onWheelHandler = (e: WheelEvent) => {
|
|
@@ -938,6 +810,8 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
938
810
|
const unsubscribeViewport = ctrl.viewport.subscribe(() => {
|
|
939
811
|
const vp = ctrl.viewport.peek()
|
|
940
812
|
|
|
813
|
+
viewportVersion.value++
|
|
814
|
+
|
|
941
815
|
if (viewportDpr.value !== vp.dpr) {
|
|
942
816
|
viewportDpr.value = vp.dpr
|
|
943
817
|
}
|
|
@@ -949,6 +823,12 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
949
823
|
kWidth.value = vp.kWidth
|
|
950
824
|
kGap.value = vp.kGap
|
|
951
825
|
}
|
|
826
|
+
|
|
827
|
+
nextTick(() => {
|
|
828
|
+
requestAnimationFrame(() => {
|
|
829
|
+
syncRangeScrollLeft()
|
|
830
|
+
})
|
|
831
|
+
})
|
|
952
832
|
})
|
|
953
833
|
|
|
954
834
|
const unsubscribeData = ctrl.data.subscribe(() => {
|
|
@@ -968,58 +848,7 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
968
848
|
emit('themeChange', newTheme)
|
|
969
849
|
})
|
|
970
850
|
|
|
971
|
-
const unsubscribeIndicators = ctrl
|
|
972
|
-
const instances = ctrl.indicators.peek()
|
|
973
|
-
|
|
974
|
-
const mains = instances
|
|
975
|
-
.filter((i): i is IndicatorInstance & { role: 'main' } => i.role === 'main')
|
|
976
|
-
.map((i) => i.definitionId)
|
|
977
|
-
mainActiveIndicators.value = mains
|
|
978
|
-
|
|
979
|
-
const nextParams = { ...indicatorParams.value }
|
|
980
|
-
for (const inst of instances) {
|
|
981
|
-
if (inst.role === 'main' && inst.params && Object.keys(inst.params).length > 0) {
|
|
982
|
-
nextParams[inst.definitionId] = { ...inst.params }
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
ctrl.updateRendererConfig('mainIndicatorLegend', {
|
|
987
|
-
indicators: {
|
|
988
|
-
MA: { enabled: mains.includes('MA'), params: nextParams['MA'] || {} },
|
|
989
|
-
BOLL: { enabled: mains.includes('BOLL'), params: nextParams['BOLL'] || {} },
|
|
990
|
-
EXPMA: { enabled: mains.includes('EXPMA'), params: nextParams['EXPMA'] || {} },
|
|
991
|
-
ENE: { enabled: mains.includes('ENE'), params: nextParams['ENE'] || {} },
|
|
992
|
-
},
|
|
993
|
-
})
|
|
994
|
-
|
|
995
|
-
indicatorParams.value = nextParams
|
|
996
|
-
})
|
|
997
|
-
|
|
998
|
-
const unsubscribeSubPanes = ctrl.subPanes.subscribe(() => {
|
|
999
|
-
const subPaneInfos = ctrl.subPanes.peek()
|
|
1000
|
-
const signalIds = new Set(subPaneInfos.map((sp) => sp.paneId))
|
|
1001
|
-
|
|
1002
|
-
const merged = subPanes.value.filter((p) => signalIds.has(p.id))
|
|
1003
|
-
const existingIds = new Set(merged.map((p) => p.id))
|
|
1004
|
-
for (const sp of subPaneInfos) {
|
|
1005
|
-
if (!existingIds.has(sp.paneId)) {
|
|
1006
|
-
merged.push({
|
|
1007
|
-
id: sp.paneId,
|
|
1008
|
-
indicatorId: sp.indicatorId as SubIndicatorType,
|
|
1009
|
-
params: sp.params,
|
|
1010
|
-
})
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
subPanes.value = merged
|
|
1014
|
-
|
|
1015
|
-
const nextParams = { ...indicatorParams.value }
|
|
1016
|
-
for (const sp of subPaneInfos) {
|
|
1017
|
-
if (sp.params && Object.keys(sp.params).length > 0) {
|
|
1018
|
-
nextParams[sp.indicatorId] = { ...sp.params }
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
indicatorParams.value = nextParams
|
|
1022
|
-
})
|
|
851
|
+
const unsubscribeIndicators = setupIndicatorSubscriptions(ctrl)
|
|
1023
852
|
|
|
1024
853
|
const unsubscribeComparisonColors = ctrl.comparisonColors.subscribe(() => {
|
|
1025
854
|
comparisonColorsMap.value = new Map(ctrl.comparisonColors.peek())
|
|
@@ -1037,34 +866,18 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
1037
866
|
unsubscribePaneLayout()
|
|
1038
867
|
unsubscribeTheme()
|
|
1039
868
|
unsubscribeIndicators()
|
|
1040
|
-
unsubscribeSubPanes()
|
|
1041
869
|
unsubscribeComparisonColors()
|
|
1042
870
|
unsubscribeComparisonLoading()
|
|
1043
|
-
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
1044
871
|
})
|
|
1045
872
|
}
|
|
1046
873
|
|
|
1047
874
|
function applyInitialSettings(ctrl: ChartController): void {
|
|
1048
875
|
const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
|
|
1049
876
|
chartSettings.value = initialSettings
|
|
1050
|
-
applyThemeFromSettings(
|
|
877
|
+
applyThemeFromSettings(initialSettings.theme as string)
|
|
1051
878
|
ctrl.updateSettingsFacade(initialSettings)
|
|
1052
879
|
}
|
|
1053
880
|
|
|
1054
|
-
function setupDrawingController(ctrl: ChartController): void {
|
|
1055
|
-
drawingController.value = new DrawingInteractionController(ctrl)
|
|
1056
|
-
drawingController.value.setCallbacks({
|
|
1057
|
-
onDrawingCreated: (drawing) => {
|
|
1058
|
-
drawings.value = [...drawings.value, drawing]
|
|
1059
|
-
selectedDrawingId.value = drawing.id
|
|
1060
|
-
},
|
|
1061
|
-
onToolChange: () => {},
|
|
1062
|
-
onDrawingSelected: (drawing) => {
|
|
1063
|
-
selectedDrawingId.value = drawing?.id ?? null
|
|
1064
|
-
},
|
|
1065
|
-
})
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
881
|
function setupInteractionCallbacks(ctrl: ChartController): void {
|
|
1069
882
|
ctrl.setTooltipAnchorPositioning(useAnchorPositioning.value)
|
|
1070
883
|
ctrl.interactionState.subscribe(() => {
|
|
@@ -1085,7 +898,7 @@ function setupSemanticController(ctrl: ChartController): void {
|
|
|
1085
898
|
|
|
1086
899
|
// config:ready → Chart 侧已完成创建,Vue 回读状态
|
|
1087
900
|
semanticController.value.on('config:ready', () => {
|
|
1088
|
-
initIndicatorsFromConfig()
|
|
901
|
+
initIndicatorsFromConfig(props.semanticConfig)
|
|
1089
902
|
nextTick(() => controller.value?.scrollToRight())
|
|
1090
903
|
})
|
|
1091
904
|
// 暂时断开语义化配置加载,由搜索结果驱动
|
|
@@ -1096,6 +909,7 @@ function setupSemanticController(ctrl: ChartController): void {
|
|
|
1096
909
|
// })
|
|
1097
910
|
}
|
|
1098
911
|
|
|
912
|
+
// ── onMounted ──
|
|
1099
913
|
onMounted(async () => {
|
|
1100
914
|
useAnchorPositioning.value = false
|
|
1101
915
|
|
|
@@ -1121,13 +935,13 @@ onMounted(async () => {
|
|
|
1121
935
|
// 3.5) 在任何 draw 之前注册主图指标(BOLL/MA 等)
|
|
1122
936
|
// initIndicatorsFromConfig 是同步的,读 props.semanticConfig 即可注册,
|
|
1123
937
|
// 确保 scheduler 首次 applyResults 时 BOLL 已在 registry 里
|
|
1124
|
-
initIndicatorsFromConfig()
|
|
938
|
+
initIndicatorsFromConfig(props.semanticConfig)
|
|
1125
939
|
|
|
1126
940
|
// 4) 工具栏初始设置
|
|
1127
941
|
applyInitialSettings(ctrl)
|
|
1128
942
|
|
|
1129
943
|
// 5) 绘图交互控制器
|
|
1130
|
-
|
|
944
|
+
setupDrawing(ctrl)
|
|
1131
945
|
|
|
1132
946
|
// 6) 交互信号桥接
|
|
1133
947
|
setupInteractionCallbacks(ctrl)
|
|
@@ -1136,6 +950,7 @@ onMounted(async () => {
|
|
|
1136
950
|
setupSemanticController(ctrl)
|
|
1137
951
|
})
|
|
1138
952
|
|
|
953
|
+
// ── onUnmounted & Watchers ──
|
|
1139
954
|
onUnmounted(() => {
|
|
1140
955
|
const ctrl = controller.value
|
|
1141
956
|
if (ctrl) {
|
|
@@ -1307,10 +1122,124 @@ watch(
|
|
|
1307
1122
|
position: relative;
|
|
1308
1123
|
}
|
|
1309
1124
|
|
|
1125
|
+
.range-selection-overlay {
|
|
1126
|
+
position: absolute;
|
|
1127
|
+
top: 0;
|
|
1128
|
+
z-index: 25;
|
|
1129
|
+
box-sizing: border-box;
|
|
1130
|
+
border: 1px solid rgba(24, 144, 255, 0.75);
|
|
1131
|
+
background: rgba(24, 144, 255, 0.14);
|
|
1132
|
+
pointer-events: none;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.range-selection-overlay.is-dragging {
|
|
1136
|
+
background: rgba(24, 144, 255, 0.2);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.range-selection-handle {
|
|
1140
|
+
position: absolute;
|
|
1141
|
+
top: 0;
|
|
1142
|
+
bottom: 0;
|
|
1143
|
+
width: 8px;
|
|
1144
|
+
cursor: ew-resize;
|
|
1145
|
+
pointer-events: auto;
|
|
1146
|
+
z-index: 101;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.range-selection-handle--left { left: -4px; }
|
|
1150
|
+
|
|
1151
|
+
.range-selection-handle--right { right: -4px; }
|
|
1152
|
+
|
|
1153
|
+
.range-selection-export {
|
|
1154
|
+
position: absolute;
|
|
1155
|
+
left: 50%;
|
|
1156
|
+
top: 8px;
|
|
1157
|
+
transform: translateX(-50%);
|
|
1158
|
+
display: flex;
|
|
1159
|
+
align-items: center;
|
|
1160
|
+
gap: 6px;
|
|
1161
|
+
padding: 4px 8px;
|
|
1162
|
+
height: 32px;
|
|
1163
|
+
background: color-mix(in srgb, var(--klc-color-tag-bg-white) 88%, transparent);
|
|
1164
|
+
backdrop-filter: blur(8px);
|
|
1165
|
+
-webkit-backdrop-filter: blur(8px);
|
|
1166
|
+
border: 1px solid var(--klc-color-border-button);
|
|
1167
|
+
border-radius: 6px;
|
|
1168
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
1169
|
+
z-index: 100;
|
|
1170
|
+
user-select: none;
|
|
1171
|
+
pointer-events: auto;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.range-selection-export .toolbar-btn {
|
|
1175
|
+
display: inline-flex;
|
|
1176
|
+
align-items: center;
|
|
1177
|
+
justify-content: center;
|
|
1178
|
+
height: 24px;
|
|
1179
|
+
padding: 0 8px;
|
|
1180
|
+
border: 1px solid var(--klc-color-border-button);
|
|
1181
|
+
border-radius: 4px;
|
|
1182
|
+
background: transparent;
|
|
1183
|
+
color: var(--klc-color-axis-text);
|
|
1184
|
+
font-size: 12px;
|
|
1185
|
+
cursor: pointer;
|
|
1186
|
+
transition:
|
|
1187
|
+
border-color 0.15s ease,
|
|
1188
|
+
background 0.15s ease,
|
|
1189
|
+
color 0.15s ease;
|
|
1190
|
+
white-space: nowrap;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.range-selection-export .toolbar-btn:hover {
|
|
1194
|
+
border-color: var(--klc-color-axis-line);
|
|
1195
|
+
background: var(--klc-color-grid-minor);
|
|
1196
|
+
color: var(--klc-color-foreground);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.range-selection-export .toolbar-btn.delete-btn {
|
|
1200
|
+
padding: 0;
|
|
1201
|
+
width: 24px;
|
|
1202
|
+
border-color: transparent;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.range-selection-export .toolbar-btn.delete-btn:hover {
|
|
1206
|
+
color: #dc2626;
|
|
1207
|
+
border-color: #fca5a5;
|
|
1208
|
+
background: #fef2f2;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
.range-selection-export .delete-icon {
|
|
1212
|
+
width: 14px;
|
|
1213
|
+
height: 14px;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.range-selection-export__label {
|
|
1217
|
+
color: var(--klc-color-axis-text);
|
|
1218
|
+
font-size: 11px;
|
|
1219
|
+
white-space: nowrap;
|
|
1220
|
+
border: 1px solid var(--klc-color-border-button);
|
|
1221
|
+
background: none;
|
|
1222
|
+
outline: none;
|
|
1223
|
+
padding: 1px 4px;
|
|
1224
|
+
width: 80px;
|
|
1225
|
+
height: 24px;
|
|
1226
|
+
box-sizing: border-box;
|
|
1227
|
+
font-family: inherit;
|
|
1228
|
+
border-radius: 3px;
|
|
1229
|
+
text-align: center;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.range-selection-export__sep {
|
|
1233
|
+
color: var(--klc-color-axis-text);
|
|
1234
|
+
font-size: 11px;
|
|
1235
|
+
user-select: none;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1310
1238
|
.canvas-layer {
|
|
1311
1239
|
position: sticky;
|
|
1312
1240
|
left: 0;
|
|
1313
1241
|
top: 0;
|
|
1242
|
+
z-index: 26;
|
|
1314
1243
|
pointer-events: none;
|
|
1315
1244
|
}
|
|
1316
1245
|
|