@363045841yyt/klinechart 0.8.4 → 0.8.6
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/README.md +6 -1
- package/dist/components/BaseModal.vue.d.ts +54 -0
- package/dist/components/BaseModal.vue.d.ts.map +1 -0
- package/dist/components/BatchStockDialog.vue.d.ts +13 -0
- package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
- package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -1
- package/dist/components/ColorPresetPanel.vue.d.ts +4 -1
- package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -1
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/DrawingStyleToolbar.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/IndicatorParams.vue.d.ts.map +1 -1
- 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/RangeSelectionExport.vue.d.ts +23 -0
- package/dist/components/RangeSelectionExport.vue.d.ts.map +1 -0
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/components/common/CanvasToolbar.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbar.vue.d.ts.map +1 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts.map +1 -0
- 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 +66 -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 +2149 -1409
- 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/BaseModal.vue +292 -0
- package/src/components/BatchStockDialog.vue +128 -0
- package/src/components/ChartSettingsDialog.vue +248 -405
- package/src/components/ColorPresetPanel.vue +58 -106
- package/src/components/CompareSymbolSelector.vue +37 -10
- package/src/components/DrawingStyleToolbar.vue +33 -72
- package/src/components/Dropdown.vue +42 -19
- package/src/components/ExportProgressDialog.vue +118 -0
- package/src/components/IndicatorParams.vue +194 -321
- package/src/components/IndicatorSelector.vue +188 -405
- package/src/components/KLineChart.vue +228 -403
- package/src/components/LeftToolbar.vue +3 -2
- package/src/components/RangeSelectionExport.vue +117 -0
- package/src/components/SymbolSelector.vue +37 -10
- package/src/components/TopToolbar.vue +55 -2
- package/src/components/common/CanvasToolbar.vue +70 -0
- package/src/components/common/CanvasToolbarStack.vue +32 -0
- 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 +424 -0
- package/src/composables/useTeleportedPopup.ts +46 -0
- package/src/tools/calcRangeOverlayPixel.ts +28 -0
- package/src/tools/getKLineIndexByTimestamp.ts +40 -0
|
@@ -60,18 +60,53 @@
|
|
|
60
60
|
<div class="canvas-layer" ref="canvasLayerRef">
|
|
61
61
|
<canvas class="x-axis-canvas" ref="xAxisCanvasRef"></canvas>
|
|
62
62
|
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
<CanvasToolbarStack>
|
|
64
|
+
<RangeSelectionExport
|
|
65
|
+
v-if="rangeSelectionReady"
|
|
66
|
+
v-model:start-date="customStartDate"
|
|
67
|
+
v-model:end-date="customEndDate"
|
|
68
|
+
:start-label="rangeSelectionStartLabel"
|
|
69
|
+
:end-label="rangeSelectionEndLabel"
|
|
70
|
+
:count="rangeSelectionCount"
|
|
71
|
+
@export="exportRangeToCsv"
|
|
72
|
+
@clear="clearRangeSelection"
|
|
73
|
+
@batch-setting="showBatchStockDialog = true"
|
|
74
|
+
/>
|
|
75
|
+
<DrawingStyleToolbar
|
|
76
|
+
v-if="selectedDrawing"
|
|
77
|
+
:drawing="selectedDrawing"
|
|
78
|
+
@update-style="onUpdateDrawingStyle"
|
|
79
|
+
@delete="onDeleteDrawing"
|
|
80
|
+
/>
|
|
81
|
+
</CanvasToolbarStack>
|
|
82
|
+
</div>
|
|
83
|
+
<div
|
|
84
|
+
v-if="rangeSelectionOverlayStyle"
|
|
85
|
+
class="range-selection-overlay"
|
|
86
|
+
:class="{ 'is-dragging': rangeSelection.isDragging }"
|
|
87
|
+
:style="rangeSelectionOverlayStyle"
|
|
88
|
+
aria-label="已选择的 K 线区间"
|
|
89
|
+
>
|
|
90
|
+
<div
|
|
91
|
+
v-if="rangeSelectionReady"
|
|
92
|
+
class="range-selection-handle range-selection-handle--left"
|
|
93
|
+
@pointerdown.stop="onEdgePointerDown('left', $event)"
|
|
94
|
+
@pointermove.stop="onEdgePointerMove($event)"
|
|
95
|
+
@pointerup.stop="onEdgePointerUp($event)"
|
|
96
|
+
/>
|
|
97
|
+
<div
|
|
98
|
+
v-if="rangeSelectionReady"
|
|
99
|
+
class="range-selection-handle range-selection-handle--right"
|
|
100
|
+
@pointerdown.stop="onEdgePointerDown('right', $event)"
|
|
101
|
+
@pointermove.stop="onEdgePointerMove($event)"
|
|
102
|
+
@pointerup.stop="onEdgePointerUp($event)"
|
|
68
103
|
/>
|
|
69
104
|
</div>
|
|
70
105
|
</div>
|
|
71
106
|
</div>
|
|
72
107
|
<Teleport v-if="tooltipLayerRef" :to="tooltipLayerRef">
|
|
73
108
|
<div
|
|
74
|
-
v-if="hovered"
|
|
109
|
+
v-if="hovered && !isMobile"
|
|
75
110
|
class="tooltip-anchor kline-tooltip-anchor"
|
|
76
111
|
:class="{ 'use-anchor': useAnchorPositioning }"
|
|
77
112
|
:style="klineTooltipAnchorStyle"
|
|
@@ -83,7 +118,7 @@
|
|
|
83
118
|
:style="markerTooltipAnchorStyle"
|
|
84
119
|
></div>
|
|
85
120
|
<KLineTooltip
|
|
86
|
-
v-if="hovered"
|
|
121
|
+
v-if="hovered && !isMobile"
|
|
87
122
|
:k="hovered"
|
|
88
123
|
:index="hoveredIndex"
|
|
89
124
|
:data="chartData"
|
|
@@ -116,6 +151,12 @@
|
|
|
116
151
|
></div>
|
|
117
152
|
</div>
|
|
118
153
|
</div>
|
|
154
|
+
<ExportProgressDialog :progress="exportingProgress" @close="exportingProgress = null" />
|
|
155
|
+
<BatchStockDialog
|
|
156
|
+
:show="showBatchStockDialog"
|
|
157
|
+
@close="showBatchStockDialog = false"
|
|
158
|
+
@apply="onBatchApply"
|
|
159
|
+
/>
|
|
119
160
|
<IndicatorSelector
|
|
120
161
|
ref="indicatorSelectorRef"
|
|
121
162
|
:active-indicators="activeIndicators"
|
|
@@ -138,38 +179,28 @@ import KLineTooltip from './KLineTooltip.vue'
|
|
|
138
179
|
import MarkerTooltip from './MarkerTooltip.vue'
|
|
139
180
|
import IndicatorSelector from './IndicatorSelector.vue'
|
|
140
181
|
import DrawingStyleToolbar from './DrawingStyleToolbar.vue'
|
|
182
|
+
import RangeSelectionExport from './RangeSelectionExport.vue'
|
|
183
|
+
import CanvasToolbarStack from './common/CanvasToolbarStack.vue'
|
|
141
184
|
import { provideFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
142
185
|
import {
|
|
143
186
|
createChartController,
|
|
144
187
|
type ChartController,
|
|
145
|
-
type PaneSpec,
|
|
146
|
-
type IndicatorInstance,
|
|
147
|
-
type SubIndicatorType,
|
|
148
188
|
type InteractionSnapshot,
|
|
149
|
-
type DrawingToolId,
|
|
150
|
-
type KLineData,
|
|
151
189
|
type SymbolSpec,
|
|
152
190
|
zoomLevelToKWidth,
|
|
153
191
|
kGapFromKWidth,
|
|
154
|
-
DrawingInteractionController,
|
|
155
192
|
} 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'
|
|
193
|
+
import { useChartTheme } from '../composables/chart/useChartTheme'
|
|
194
|
+
import { useIndicatorManager } from '../composables/chart/useIndicatorManager'
|
|
195
|
+
import { useDrawingManager } from '../composables/chart/useDrawingManager'
|
|
161
196
|
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'
|
|
197
|
+
import { useRangeSelection } from '../composables/chart/useRangeSelection'
|
|
170
198
|
import LeftToolbar from './LeftToolbar.vue'
|
|
171
199
|
import TopToolbar, { type SymbolItem } from './TopToolbar.vue'
|
|
200
|
+
import BatchStockDialog from './BatchStockDialog.vue'
|
|
201
|
+
import ExportProgressDialog from './ExportProgressDialog.vue'
|
|
172
202
|
|
|
203
|
+
// ── Props & Emits ──
|
|
173
204
|
const props = withDefaults(
|
|
174
205
|
defineProps<{
|
|
175
206
|
/** 语义化配置(可选,唯一控制源) */
|
|
@@ -231,6 +262,7 @@ const emit = defineEmits<{
|
|
|
231
262
|
(e: 'kLineAdjustChange', adjust: 'qfq' | 'hfq' | 'splits' | 'none'): void
|
|
232
263
|
}>()
|
|
233
264
|
|
|
265
|
+
// ── Symbol / Comparison State ──
|
|
234
266
|
const kLineLevel = ref(props.semanticConfig?.data?.period ?? 'daily')
|
|
235
267
|
const kLineAdjust = ref(props.semanticConfig?.data?.adjust ?? 'none')
|
|
236
268
|
const isIntraday = computed(() => kLineLevel.value.includes('min'))
|
|
@@ -302,9 +334,12 @@ function forcePercentAxis() {
|
|
|
302
334
|
controller.value?.updateSettingsFacade(nextSettings)
|
|
303
335
|
try {
|
|
304
336
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(nextSettings))
|
|
305
|
-
} catch {
|
|
337
|
+
} catch {
|
|
338
|
+
/* quota exceeded */
|
|
339
|
+
}
|
|
306
340
|
}
|
|
307
341
|
|
|
342
|
+
// ── DOM Template Refs ──
|
|
308
343
|
const containerRef = ref<HTMLDivElement | null>(null)
|
|
309
344
|
const chartMainRef = ref<HTMLDivElement | null>(null)
|
|
310
345
|
const chartWrapperRef = ref<HTMLDivElement | null>(null)
|
|
@@ -313,26 +348,101 @@ const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
|
|
|
313
348
|
const indicatorSelectorRef = ref<InstanceType<typeof IndicatorSelector> | null>(null)
|
|
314
349
|
provideFullscreenTeleportTarget(chartWrapperRef)
|
|
315
350
|
|
|
316
|
-
|
|
351
|
+
// ── Controller & Composable Wiring ──
|
|
317
352
|
const controller = shallowRef<ChartController | null>(null)
|
|
318
353
|
|
|
319
|
-
|
|
354
|
+
const {
|
|
355
|
+
chartTheme,
|
|
356
|
+
chartSettings,
|
|
357
|
+
tooltipColors,
|
|
358
|
+
themeCssVars,
|
|
359
|
+
handleSettingsChange,
|
|
360
|
+
applyThemeFromSettings,
|
|
361
|
+
} = useChartTheme(controller)
|
|
362
|
+
|
|
320
363
|
const semanticController = shallowRef<SemanticChartController | null>(null)
|
|
321
364
|
|
|
322
|
-
/* ==========
|
|
365
|
+
/* ========== 本地响应式状态 ========== */
|
|
323
366
|
const dataLength = ref(0)
|
|
324
367
|
const dataVersion = ref(0)
|
|
368
|
+
const showBatchStockDialog = ref(false)
|
|
369
|
+
const batchStockCodes = ref<string[]>([])
|
|
370
|
+
const viewportVersion = ref(0)
|
|
325
371
|
const viewportDpr = ref(1)
|
|
326
372
|
const zoomLevel = ref(props.initialZoomLevel ?? 1)
|
|
327
373
|
const kWidth = ref(0)
|
|
328
374
|
const kGap = ref(1)
|
|
329
375
|
const viewWidth = ref(0)
|
|
330
376
|
const paneRatios = ref<Record<string, number>>({})
|
|
331
|
-
const selectedDrawingId = ref<string | null>(null)
|
|
332
|
-
const drawings = ref<DrawingObject[]>([])
|
|
333
377
|
const comparisonColorsMap = ref<Map<string, string>>(new Map())
|
|
334
378
|
const comparisonLoading = ref(false)
|
|
379
|
+
const activeToolId = ref('cursor')
|
|
380
|
+
|
|
381
|
+
const {
|
|
382
|
+
mainActiveIndicators,
|
|
383
|
+
subActiveIndicators,
|
|
384
|
+
activeIndicators,
|
|
385
|
+
indicatorParams,
|
|
386
|
+
subPanes,
|
|
387
|
+
buildPaneLayoutIntent,
|
|
388
|
+
getDefaultParams,
|
|
389
|
+
isSubPaneIndicator,
|
|
390
|
+
addSubPane,
|
|
391
|
+
removeSubPane,
|
|
392
|
+
clearAllSubPanes,
|
|
393
|
+
initIndicatorsFromConfig,
|
|
394
|
+
switchSubIndicator,
|
|
395
|
+
handleIndicatorToggle,
|
|
396
|
+
handleUpdateParams,
|
|
397
|
+
handleReorderSubIndicators,
|
|
398
|
+
setupIndicatorSubscriptions,
|
|
399
|
+
} = useIndicatorManager(controller, paneRatios)
|
|
400
|
+
|
|
401
|
+
const {
|
|
402
|
+
drawingController,
|
|
403
|
+
selectedDrawingId,
|
|
404
|
+
selectedDrawing,
|
|
405
|
+
drawings,
|
|
406
|
+
handleSelectTool: handleDrawingToolSelect,
|
|
407
|
+
onUpdateDrawingStyle,
|
|
408
|
+
onDeleteDrawing,
|
|
409
|
+
setupDrawing,
|
|
410
|
+
} = useDrawingManager(controller)
|
|
411
|
+
|
|
412
|
+
const {
|
|
413
|
+
rangeSelection,
|
|
414
|
+
customStartDate,
|
|
415
|
+
customEndDate,
|
|
416
|
+
containerScrollLeft,
|
|
417
|
+
isRangeSelectActive,
|
|
418
|
+
rangeSelectionReady,
|
|
419
|
+
rangeSelectionBounds,
|
|
420
|
+
rangeSelectionCount,
|
|
421
|
+
rangeSelectionStartLabel,
|
|
422
|
+
rangeSelectionEndLabel,
|
|
423
|
+
rangeSelectionOverlayStyle,
|
|
424
|
+
clearRangeSelection,
|
|
425
|
+
handleRangePointerDown,
|
|
426
|
+
handleRangePointerMove,
|
|
427
|
+
handleRangePointerUp,
|
|
428
|
+
exportRangeToCsv,
|
|
429
|
+
exportingProgress,
|
|
430
|
+
onEdgePointerDown,
|
|
431
|
+
onEdgePointerMove,
|
|
432
|
+
onEdgePointerUp,
|
|
433
|
+
onScroll: onRangeScroll,
|
|
434
|
+
syncScrollLeft: syncRangeScrollLeft,
|
|
435
|
+
} = useRangeSelection({
|
|
436
|
+
controller,
|
|
437
|
+
activeToolId,
|
|
438
|
+
containerRef,
|
|
439
|
+
dataVersion,
|
|
440
|
+
viewportVersion,
|
|
441
|
+
dataFetcher: computed(() => props.dataFetcher),
|
|
442
|
+
batchStockCodes,
|
|
443
|
+
})
|
|
335
444
|
|
|
445
|
+
// ── Viewport Initial Values ──
|
|
336
446
|
// 初始化 kWidth / kGap(与 Chart 引擎 zoom→物理值 转换一致)
|
|
337
447
|
const initZoom = zoomLevel.value
|
|
338
448
|
kWidth.value = zoomLevelToKWidth(initZoom, {
|
|
@@ -343,66 +453,12 @@ kWidth.value = zoomLevelToKWidth(initZoom, {
|
|
|
343
453
|
})
|
|
344
454
|
kGap.value = kGapFromKWidth(kWidth.value, viewportDpr.value)
|
|
345
455
|
|
|
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
|
-
|
|
456
|
+
// ── No-op Render Trigger (exposed) ──
|
|
396
457
|
function scheduleRender() {
|
|
397
458
|
/* Controller auto-renders on state changes */
|
|
398
459
|
}
|
|
399
460
|
|
|
400
|
-
|
|
401
|
-
chartSettings.value = settings
|
|
402
|
-
applyThemeFromSettings(controller.value, settings.theme as string)
|
|
403
|
-
controller.value?.updateSettingsFacade(settings)
|
|
404
|
-
}
|
|
405
|
-
|
|
461
|
+
// ── Tooltip Measurement ──
|
|
406
462
|
function measureTooltipSize(el: HTMLDivElement, minWidth: number, minHeight: number) {
|
|
407
463
|
const r = el.getBoundingClientRect()
|
|
408
464
|
return {
|
|
@@ -428,11 +484,10 @@ function setMarkerTooltipEl(el: HTMLDivElement | null) {
|
|
|
428
484
|
})
|
|
429
485
|
}
|
|
430
486
|
|
|
431
|
-
//
|
|
487
|
+
// ── Marker Tooltip & Container Rect Cache ──
|
|
432
488
|
const mousePos = ref({ x: 0, y: 0 })
|
|
433
489
|
const useAnchorPositioning = ref(false)
|
|
434
490
|
|
|
435
|
-
// 容器 rect 缓存,避免 pointermove 中反复 getBoundingClientRect 强制同步布局
|
|
436
491
|
let _cachedContainerRect: DOMRect | null = null
|
|
437
492
|
function invalidateContainerRectCache(): void {
|
|
438
493
|
_cachedContainerRect = null
|
|
@@ -444,7 +499,7 @@ function getContainerRect(container: HTMLDivElement): DOMRect {
|
|
|
444
499
|
return _cachedContainerRect
|
|
445
500
|
}
|
|
446
501
|
|
|
447
|
-
//
|
|
502
|
+
// ── Interaction State Bridge ──
|
|
448
503
|
const interactionState = shallowRef<InteractionSnapshot>({
|
|
449
504
|
crosshairPos: null,
|
|
450
505
|
crosshairIndex: null,
|
|
@@ -462,12 +517,6 @@ const interactionState = shallowRef<InteractionSnapshot>({
|
|
|
462
517
|
isHoveringRightAxis: false,
|
|
463
518
|
})
|
|
464
519
|
|
|
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
520
|
const paneSeparatorLines = ref<Array<{ id: string; top: number }>>([])
|
|
472
521
|
const markerTooltipSize = ref({ width: 220, height: 120 })
|
|
473
522
|
const tooltipLayerOffset = computed(() => {
|
|
@@ -487,10 +536,11 @@ const isResizingPane = computed(() => interactionState.value.isResizingPaneBound
|
|
|
487
536
|
const isHoveringPaneSeparator = computed(() => interactionState.value.isHoveringPaneBoundary)
|
|
488
537
|
const hoveredPaneBoundaryId = computed(() => interactionState.value.hoveredPaneBoundaryId)
|
|
489
538
|
const isHoveringRightAxis = computed(() => interactionState.value.isHoveringRightAxis)
|
|
539
|
+
const isMobile = window.matchMedia('(pointer: coarse)').matches
|
|
490
540
|
const hoveredIdx = computed(() => interactionState.value.hoveredIndex)
|
|
491
541
|
const crosshairIdx = computed(() => interactionState.value.crosshairIndex)
|
|
492
542
|
|
|
493
|
-
//
|
|
543
|
+
// ── Derived Computed (Cursor, Hovered, Tooltip) ──
|
|
494
544
|
const containerCursor = computed(() => {
|
|
495
545
|
if (isDragging.value) return 'grabbing'
|
|
496
546
|
if (isResizingPane.value || isHoveringPaneSeparator.value) return 'ns-resize'
|
|
@@ -544,32 +594,33 @@ const chartData = computed(() => {
|
|
|
544
594
|
return controller.value?.getData() ?? []
|
|
545
595
|
})
|
|
546
596
|
|
|
547
|
-
//
|
|
548
|
-
function handleSelectTool(toolId: string) {
|
|
549
|
-
drawingController.value?.setTool(toolId as DrawingToolId)
|
|
550
|
-
}
|
|
551
|
-
|
|
597
|
+
// ── Pointer Event Handlers ──
|
|
552
598
|
function onToggleIndicator() {
|
|
553
599
|
indicatorSelectorRef.value?.toggleMenu()
|
|
554
600
|
}
|
|
555
601
|
|
|
556
|
-
function
|
|
557
|
-
|
|
558
|
-
if (!d || !drawingController.value) return
|
|
559
|
-
drawingController.value.updateDrawingStyle(d.id, style)
|
|
560
|
-
drawings.value = drawingController.value.getDrawings()
|
|
602
|
+
function onBatchApply(codes: string[]) {
|
|
603
|
+
batchStockCodes.value = codes
|
|
561
604
|
}
|
|
562
605
|
|
|
563
|
-
function
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
|
|
567
|
-
|
|
606
|
+
function handleSelectTool(toolId: string) {
|
|
607
|
+
activeToolId.value = toolId
|
|
608
|
+
if (toolId === 'range-select') {
|
|
609
|
+
drawingController.value?.setTool('cursor')
|
|
610
|
+
selectedDrawingId.value = null
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
clearRangeSelection()
|
|
615
|
+
handleDrawingToolSelect(toolId)
|
|
568
616
|
}
|
|
569
617
|
|
|
570
618
|
function onPointerDown(e: PointerEvent) {
|
|
571
619
|
controller.value?.handlePointerEvent(e, {
|
|
572
620
|
onPointerDown: (event, container) => {
|
|
621
|
+
if (handleRangePointerDown(event, container)) {
|
|
622
|
+
return true
|
|
623
|
+
}
|
|
573
624
|
if (drawingController.value?.onPointerDown(event, container)) {
|
|
574
625
|
return true
|
|
575
626
|
}
|
|
@@ -589,6 +640,9 @@ function onPointerMove(e: PointerEvent) {
|
|
|
589
640
|
}
|
|
590
641
|
controller.value?.handlePointerEvent(e, {
|
|
591
642
|
onPointerMove: (event, container) => {
|
|
643
|
+
if (handleRangePointerMove(event, container)) {
|
|
644
|
+
return true
|
|
645
|
+
}
|
|
592
646
|
if (drawingController.value?.onPointerMove(event, container)) {
|
|
593
647
|
drawings.value = drawingController.value.getDrawings()
|
|
594
648
|
return true
|
|
@@ -601,6 +655,9 @@ function onPointerMove(e: PointerEvent) {
|
|
|
601
655
|
function onPointerUp(e: PointerEvent) {
|
|
602
656
|
controller.value?.handlePointerEvent(e, {
|
|
603
657
|
onPointerUp: (event, container) => {
|
|
658
|
+
if (handleRangePointerUp(event, container)) {
|
|
659
|
+
return true
|
|
660
|
+
}
|
|
604
661
|
if (drawingController.value?.onPointerUp(event, container)) {
|
|
605
662
|
return true
|
|
606
663
|
}
|
|
@@ -630,226 +687,11 @@ function onRightAxisPointerLeave(e: PointerEvent) {
|
|
|
630
687
|
}
|
|
631
688
|
|
|
632
689
|
function onScroll() {
|
|
690
|
+
onRangeScroll()
|
|
633
691
|
controller.value?.handleScrollEvent()
|
|
634
692
|
}
|
|
635
693
|
|
|
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 变化时自动重算 */
|
|
694
|
+
// ── Width / Zoom / Expose ──
|
|
853
695
|
const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
|
|
854
696
|
|
|
855
697
|
const totalWidth = computed(() => {
|
|
@@ -879,7 +721,7 @@ defineExpose({
|
|
|
879
721
|
getController: () => controller.value,
|
|
880
722
|
})
|
|
881
723
|
|
|
882
|
-
//
|
|
724
|
+
// ── Lifecycle Setup ──
|
|
883
725
|
|
|
884
726
|
function setupWheelHandler(): (e: WheelEvent) => void {
|
|
885
727
|
const onWheelHandler = (e: WheelEvent) => {
|
|
@@ -938,6 +780,8 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
938
780
|
const unsubscribeViewport = ctrl.viewport.subscribe(() => {
|
|
939
781
|
const vp = ctrl.viewport.peek()
|
|
940
782
|
|
|
783
|
+
viewportVersion.value++
|
|
784
|
+
|
|
941
785
|
if (viewportDpr.value !== vp.dpr) {
|
|
942
786
|
viewportDpr.value = vp.dpr
|
|
943
787
|
}
|
|
@@ -949,6 +793,12 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
949
793
|
kWidth.value = vp.kWidth
|
|
950
794
|
kGap.value = vp.kGap
|
|
951
795
|
}
|
|
796
|
+
|
|
797
|
+
nextTick(() => {
|
|
798
|
+
requestAnimationFrame(() => {
|
|
799
|
+
syncRangeScrollLeft()
|
|
800
|
+
})
|
|
801
|
+
})
|
|
952
802
|
})
|
|
953
803
|
|
|
954
804
|
const unsubscribeData = ctrl.data.subscribe(() => {
|
|
@@ -968,58 +818,7 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
968
818
|
emit('themeChange', newTheme)
|
|
969
819
|
})
|
|
970
820
|
|
|
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
|
-
})
|
|
821
|
+
const unsubscribeIndicators = setupIndicatorSubscriptions(ctrl)
|
|
1023
822
|
|
|
1024
823
|
const unsubscribeComparisonColors = ctrl.comparisonColors.subscribe(() => {
|
|
1025
824
|
comparisonColorsMap.value = new Map(ctrl.comparisonColors.peek())
|
|
@@ -1037,34 +836,18 @@ function setupChartCallbacks(ctrl: ChartController): void {
|
|
|
1037
836
|
unsubscribePaneLayout()
|
|
1038
837
|
unsubscribeTheme()
|
|
1039
838
|
unsubscribeIndicators()
|
|
1040
|
-
unsubscribeSubPanes()
|
|
1041
839
|
unsubscribeComparisonColors()
|
|
1042
840
|
unsubscribeComparisonLoading()
|
|
1043
|
-
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
1044
841
|
})
|
|
1045
842
|
}
|
|
1046
843
|
|
|
1047
844
|
function applyInitialSettings(ctrl: ChartController): void {
|
|
1048
845
|
const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
|
|
1049
846
|
chartSettings.value = initialSettings
|
|
1050
|
-
applyThemeFromSettings(
|
|
847
|
+
applyThemeFromSettings(initialSettings.theme as string)
|
|
1051
848
|
ctrl.updateSettingsFacade(initialSettings)
|
|
1052
849
|
}
|
|
1053
850
|
|
|
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
851
|
function setupInteractionCallbacks(ctrl: ChartController): void {
|
|
1069
852
|
ctrl.setTooltipAnchorPositioning(useAnchorPositioning.value)
|
|
1070
853
|
ctrl.interactionState.subscribe(() => {
|
|
@@ -1085,7 +868,7 @@ function setupSemanticController(ctrl: ChartController): void {
|
|
|
1085
868
|
|
|
1086
869
|
// config:ready → Chart 侧已完成创建,Vue 回读状态
|
|
1087
870
|
semanticController.value.on('config:ready', () => {
|
|
1088
|
-
initIndicatorsFromConfig()
|
|
871
|
+
initIndicatorsFromConfig(props.semanticConfig)
|
|
1089
872
|
nextTick(() => controller.value?.scrollToRight())
|
|
1090
873
|
})
|
|
1091
874
|
// 暂时断开语义化配置加载,由搜索结果驱动
|
|
@@ -1096,6 +879,7 @@ function setupSemanticController(ctrl: ChartController): void {
|
|
|
1096
879
|
// })
|
|
1097
880
|
}
|
|
1098
881
|
|
|
882
|
+
// ── onMounted ──
|
|
1099
883
|
onMounted(async () => {
|
|
1100
884
|
useAnchorPositioning.value = false
|
|
1101
885
|
|
|
@@ -1121,13 +905,13 @@ onMounted(async () => {
|
|
|
1121
905
|
// 3.5) 在任何 draw 之前注册主图指标(BOLL/MA 等)
|
|
1122
906
|
// initIndicatorsFromConfig 是同步的,读 props.semanticConfig 即可注册,
|
|
1123
907
|
// 确保 scheduler 首次 applyResults 时 BOLL 已在 registry 里
|
|
1124
|
-
initIndicatorsFromConfig()
|
|
908
|
+
initIndicatorsFromConfig(props.semanticConfig)
|
|
1125
909
|
|
|
1126
910
|
// 4) 工具栏初始设置
|
|
1127
911
|
applyInitialSettings(ctrl)
|
|
1128
912
|
|
|
1129
913
|
// 5) 绘图交互控制器
|
|
1130
|
-
|
|
914
|
+
setupDrawing(ctrl)
|
|
1131
915
|
|
|
1132
916
|
// 6) 交互信号桥接
|
|
1133
917
|
setupInteractionCallbacks(ctrl)
|
|
@@ -1136,6 +920,7 @@ onMounted(async () => {
|
|
|
1136
920
|
setupSemanticController(ctrl)
|
|
1137
921
|
})
|
|
1138
922
|
|
|
923
|
+
// ── onUnmounted & Watchers ──
|
|
1139
924
|
onUnmounted(() => {
|
|
1140
925
|
const ctrl = controller.value
|
|
1141
926
|
if (ctrl) {
|
|
@@ -1307,10 +1092,43 @@ watch(
|
|
|
1307
1092
|
position: relative;
|
|
1308
1093
|
}
|
|
1309
1094
|
|
|
1095
|
+
.range-selection-overlay {
|
|
1096
|
+
position: absolute;
|
|
1097
|
+
top: 0;
|
|
1098
|
+
z-index: 25;
|
|
1099
|
+
box-sizing: border-box;
|
|
1100
|
+
border: 1px solid rgba(24, 144, 255, 0.75);
|
|
1101
|
+
background: rgba(24, 144, 255, 0.14);
|
|
1102
|
+
pointer-events: none;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.range-selection-overlay.is-dragging {
|
|
1106
|
+
background: rgba(24, 144, 255, 0.2);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.range-selection-handle {
|
|
1110
|
+
position: absolute;
|
|
1111
|
+
top: 0;
|
|
1112
|
+
bottom: 0;
|
|
1113
|
+
width: 8px;
|
|
1114
|
+
cursor: ew-resize;
|
|
1115
|
+
pointer-events: auto;
|
|
1116
|
+
z-index: 101;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
.range-selection-handle--left {
|
|
1120
|
+
left: -4px;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.range-selection-handle--right {
|
|
1124
|
+
right: -4px;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1310
1127
|
.canvas-layer {
|
|
1311
1128
|
position: sticky;
|
|
1312
1129
|
left: 0;
|
|
1313
1130
|
top: 0;
|
|
1131
|
+
z-index: 26;
|
|
1314
1132
|
pointer-events: none;
|
|
1315
1133
|
}
|
|
1316
1134
|
|
|
@@ -1345,6 +1163,7 @@ watch(
|
|
|
1345
1163
|
gap: 4px;
|
|
1346
1164
|
}
|
|
1347
1165
|
}
|
|
1166
|
+
|
|
1348
1167
|
</style>
|
|
1349
1168
|
|
|
1350
1169
|
<style>
|
|
@@ -1373,3 +1192,9 @@ watch(
|
|
|
1373
1192
|
z-index: 15;
|
|
1374
1193
|
}
|
|
1375
1194
|
</style>
|
|
1195
|
+
|
|
1196
|
+
<style>
|
|
1197
|
+
* {
|
|
1198
|
+
-webkit-tap-highlight-color: transparent;
|
|
1199
|
+
}
|
|
1200
|
+
</style>
|