@363045841yyt/klinechart 0.8.3 → 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/IndicatorSelector.vue.d.ts.map +1 -1
- 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.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1769 -1090
- 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/IndicatorSelector.vue +13 -5
- package/src/components/KLineChart.vue +329 -399
- 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/index.ts +41 -14
- 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) => {
|
|
@@ -894,7 +766,7 @@ function initChart(
|
|
|
894
766
|
canvasLayer: HTMLDivElement,
|
|
895
767
|
rightAxisLayer: HTMLDivElement,
|
|
896
768
|
xAxisCanvas: HTMLCanvasElement,
|
|
897
|
-
): ChartController {
|
|
769
|
+
): Promise<ChartController> {
|
|
898
770
|
const ctrl = createChartController({
|
|
899
771
|
container,
|
|
900
772
|
data: [],
|
|
@@ -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,7 +909,8 @@ function setupSemanticController(ctrl: ChartController): void {
|
|
|
1096
909
|
// })
|
|
1097
910
|
}
|
|
1098
911
|
|
|
1099
|
-
onMounted
|
|
912
|
+
// ── onMounted ──
|
|
913
|
+
onMounted(async () => {
|
|
1100
914
|
useAnchorPositioning.value = false
|
|
1101
915
|
|
|
1102
916
|
const container = containerRef.value
|
|
@@ -1111,7 +925,8 @@ onMounted(() => {
|
|
|
1111
925
|
const canvasLayer = container.querySelector<HTMLDivElement>('.canvas-layer')
|
|
1112
926
|
const xAxisCanvas = container.querySelector<HTMLCanvasElement>('.x-axis-canvas')
|
|
1113
927
|
const rightAxisLayer = chartMain.querySelector<HTMLDivElement>('.right-axis-host')
|
|
1114
|
-
const ctrl = initChart(container, canvasLayer!, rightAxisLayer!, xAxisCanvas!)
|
|
928
|
+
const ctrl = await initChart(container, canvasLayer!, rightAxisLayer!, xAxisCanvas!)
|
|
929
|
+
if (!containerRef.value || !chartMainRef.value) return // 组件已卸载
|
|
1115
930
|
controller.value = ctrl
|
|
1116
931
|
|
|
1117
932
|
// 3) 信号回调
|
|
@@ -1120,13 +935,13 @@ onMounted(() => {
|
|
|
1120
935
|
// 3.5) 在任何 draw 之前注册主图指标(BOLL/MA 等)
|
|
1121
936
|
// initIndicatorsFromConfig 是同步的,读 props.semanticConfig 即可注册,
|
|
1122
937
|
// 确保 scheduler 首次 applyResults 时 BOLL 已在 registry 里
|
|
1123
|
-
initIndicatorsFromConfig()
|
|
938
|
+
initIndicatorsFromConfig(props.semanticConfig)
|
|
1124
939
|
|
|
1125
940
|
// 4) 工具栏初始设置
|
|
1126
941
|
applyInitialSettings(ctrl)
|
|
1127
942
|
|
|
1128
943
|
// 5) 绘图交互控制器
|
|
1129
|
-
|
|
944
|
+
setupDrawing(ctrl)
|
|
1130
945
|
|
|
1131
946
|
// 6) 交互信号桥接
|
|
1132
947
|
setupInteractionCallbacks(ctrl)
|
|
@@ -1135,6 +950,7 @@ onMounted(() => {
|
|
|
1135
950
|
setupSemanticController(ctrl)
|
|
1136
951
|
})
|
|
1137
952
|
|
|
953
|
+
// ── onUnmounted & Watchers ──
|
|
1138
954
|
onUnmounted(() => {
|
|
1139
955
|
const ctrl = controller.value
|
|
1140
956
|
if (ctrl) {
|
|
@@ -1306,10 +1122,124 @@ watch(
|
|
|
1306
1122
|
position: relative;
|
|
1307
1123
|
}
|
|
1308
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
|
+
|
|
1309
1238
|
.canvas-layer {
|
|
1310
1239
|
position: sticky;
|
|
1311
1240
|
left: 0;
|
|
1312
1241
|
top: 0;
|
|
1242
|
+
z-index: 26;
|
|
1313
1243
|
pointer-events: none;
|
|
1314
1244
|
}
|
|
1315
1245
|
|