@363045841yyt/klinechart 0.7.5-alpha.2 → 0.7.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/dist/components/DrawingStyleToolbar.vue.d.ts +1 -15
- package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
- 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 -6
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/KLineTooltip.vue.d.ts +1 -1
- package/dist/components/KLineTooltip.vue.d.ts.map +1 -1
- package/dist/components/MarkerTooltip.vue.d.ts +1 -12
- package/dist/components/MarkerTooltip.vue.d.ts.map +1 -1
- package/dist/composables/useFullscreenTeleportTarget.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/{klinechart.css → index.css} +1 -2
- package/dist/index.d.cts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +792 -841
- package/dist/version.d.ts +1 -1
- package/dist/web-component.d.ts +18 -0
- package/dist/web-component.d.ts.map +1 -0
- package/package.json +10 -2
- package/src/__tests__/_mockController.ts +11 -1
- package/src/components/DrawingStyleToolbar.vue +1 -14
- package/src/components/IndicatorParams.vue +2 -1
- package/src/components/IndicatorSelector.vue +63 -63
- package/src/components/KLineChart.vue +248 -546
- package/src/components/KLineTooltip.vue +2 -2
- package/src/components/MarkerTooltip.vue +1 -12
- package/src/composables/useFullscreenTeleportTarget.ts +0 -2
- package/src/index.ts +72 -0
- package/src/web-component.ts +14 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="chart-wrapper" :data-theme="chartTheme">
|
|
2
|
+
<div ref="chartWrapperRef" class="chart-wrapper" :data-theme="chartTheme">
|
|
3
3
|
<div
|
|
4
4
|
class="chart-stage"
|
|
5
5
|
:class="{
|
|
@@ -118,31 +118,25 @@ import KLineTooltip from './KLineTooltip.vue'
|
|
|
118
118
|
import MarkerTooltip from './MarkerTooltip.vue'
|
|
119
119
|
import IndicatorSelector from './IndicatorSelector.vue'
|
|
120
120
|
import DrawingStyleToolbar from './DrawingStyleToolbar.vue'
|
|
121
|
-
import {
|
|
122
|
-
import type { KLineData } from '@363045841yyt/klinechart-core/types/price'
|
|
123
|
-
import {
|
|
124
|
-
createChartStore,
|
|
125
|
-
TRAILING_DRAWING_SLOTS,
|
|
126
|
-
type ChartStore,
|
|
127
|
-
} from '@363045841yyt/klinechart-core/engine/chart-store'
|
|
128
|
-
import { zoomLevelToKWidth, kGapFromKWidth } from '@363045841yyt/klinechart-core/engine/utils/zoom'
|
|
129
|
-
import { getPhysicalKLineConfig } from '@363045841yyt/klinechart-core/engine/utils/klineConfig'
|
|
130
|
-
import { type SubIndicatorType } from '@363045841yyt/klinechart-core/engine/renderers/Indicator'
|
|
121
|
+
import { provideFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
131
122
|
import {
|
|
123
|
+
createChartController,
|
|
124
|
+
type ChartController,
|
|
125
|
+
type PaneSpec,
|
|
126
|
+
type IndicatorInstance,
|
|
127
|
+
type SubIndicatorType,
|
|
128
|
+
type InteractionSnapshot,
|
|
129
|
+
type DrawingToolId,
|
|
130
|
+
type KLineData,
|
|
131
|
+
zoomLevelToKWidth,
|
|
132
|
+
kGapFromKWidth,
|
|
133
|
+
getPhysicalKLineConfig,
|
|
132
134
|
SUB_PANE_INDICATOR_CONFIGS,
|
|
133
135
|
SUB_PANE_INDICATORS,
|
|
134
|
-
} from '@363045841yyt/klinechart-core/engine/renderers/Indicator/subPaneConfig'
|
|
135
|
-
import {
|
|
136
|
-
createPaneTitleRendererPlugin,
|
|
137
|
-
type TitleInfo,
|
|
138
|
-
} from '@363045841yyt/klinechart-core/engine/renderers/paneTitle'
|
|
139
|
-
import type { InteractionSnapshot } from '@363045841yyt/klinechart-core/engine/controller/interaction'
|
|
140
|
-
import type { DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
|
|
141
|
-
import LeftToolbar from './LeftToolbar.vue'
|
|
142
|
-
import {
|
|
143
136
|
DrawingInteractionController,
|
|
144
|
-
|
|
145
|
-
} from '@363045841yyt/klinechart-core/
|
|
137
|
+
} from '@363045841yyt/klinechart-core/controllers'
|
|
138
|
+
import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
|
|
139
|
+
import LeftToolbar from './LeftToolbar.vue'
|
|
146
140
|
|
|
147
141
|
const props = withDefaults(
|
|
148
142
|
defineProps<{
|
|
@@ -187,82 +181,60 @@ const emit = defineEmits<{
|
|
|
187
181
|
(e: 'toggleFullscreen'): void
|
|
188
182
|
}>()
|
|
189
183
|
|
|
190
|
-
const xAxisCanvasRef = ref<HTMLCanvasElement | null>(null)
|
|
191
|
-
const canvasLayerRef = ref<HTMLDivElement | null>(null)
|
|
192
|
-
const rightAxisLayerRef = ref<HTMLDivElement | null>(null)
|
|
193
184
|
const containerRef = ref<HTMLDivElement | null>(null)
|
|
194
185
|
const chartMainRef = ref<HTMLDivElement | null>(null)
|
|
186
|
+
const chartWrapperRef = ref<HTMLDivElement | null>(null)
|
|
195
187
|
const tooltipLayerRef = ref<HTMLDivElement | null>(null)
|
|
196
188
|
const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
|
|
189
|
+
provideFullscreenTeleportTarget(chartWrapperRef)
|
|
197
190
|
|
|
198
|
-
/* ==========
|
|
199
|
-
const
|
|
191
|
+
/* ========== 图表控制器 ========== */
|
|
192
|
+
const controller = shallowRef<ChartController | null>(null)
|
|
200
193
|
|
|
201
194
|
/* ========== 语义化控制器 ========== */
|
|
202
195
|
const semanticController = shallowRef<SemanticChartController | null>(null)
|
|
203
196
|
|
|
204
|
-
/* ========== ChartStore
|
|
205
|
-
const
|
|
206
|
-
|
|
197
|
+
/* ========== 本地响应式状态(信号驱动,取代 ChartStore) ========== */
|
|
198
|
+
const dataLength = ref(0)
|
|
199
|
+
const dataVersion = ref(0)
|
|
200
|
+
const viewportDpr = ref(1)
|
|
201
|
+
const zoomLevel = ref(props.initialZoomLevel ?? 1)
|
|
202
|
+
const kWidth = ref(0)
|
|
203
|
+
const kGap = ref(1)
|
|
204
|
+
const viewWidth = ref(0)
|
|
205
|
+
const paneRatios = ref<Record<string, number>>({})
|
|
206
|
+
const selectedDrawingId = ref<string | null>(null)
|
|
207
|
+
const drawings = ref<DrawingObject[]>([])
|
|
208
|
+
|
|
209
|
+
// 初始化 kWidth / kGap(与 Chart 引擎 zoom→物理值 转换一致)
|
|
210
|
+
const initZoom = zoomLevel.value
|
|
211
|
+
kWidth.value = zoomLevelToKWidth(initZoom, {
|
|
207
212
|
minKWidth: props.minKWidth,
|
|
208
213
|
maxKWidth: props.maxKWidth,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
priceLabelWidth: props.priceLabelWidth,
|
|
214
|
+
zoomLevelCount: props.zoomLevels,
|
|
215
|
+
dpr: viewportDpr.value,
|
|
212
216
|
})
|
|
217
|
+
kGap.value = kGapFromKWidth(kWidth.value, viewportDpr.value)
|
|
213
218
|
|
|
214
219
|
/* ========== 主题状态 ========== */
|
|
215
220
|
const chartTheme = ref<'light' | 'dark'>('light')
|
|
216
221
|
|
|
217
|
-
// 初始化 kWidth / kGap
|
|
218
|
-
store.actions.setZoomState(
|
|
219
|
-
store.state.zoomLevel,
|
|
220
|
-
zoomLevelToKWidth(store.state.zoomLevel, {
|
|
221
|
-
minKWidth: props.minKWidth,
|
|
222
|
-
maxKWidth: props.maxKWidth,
|
|
223
|
-
zoomLevelCount: props.zoomLevels,
|
|
224
|
-
dpr: store.state.viewportDpr,
|
|
225
|
-
}),
|
|
226
|
-
kGapFromKWidth(
|
|
227
|
-
zoomLevelToKWidth(store.state.zoomLevel, {
|
|
228
|
-
minKWidth: props.minKWidth,
|
|
229
|
-
maxKWidth: props.maxKWidth,
|
|
230
|
-
zoomLevelCount: props.zoomLevels,
|
|
231
|
-
dpr: store.state.viewportDpr,
|
|
232
|
-
}),
|
|
233
|
-
store.state.viewportDpr,
|
|
234
|
-
),
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
// 为逐步迁移保留的局部别名
|
|
238
|
-
const dataLength = computed(() => store.state.dataLength)
|
|
239
|
-
const viewportDpr = computed(() => store.state.viewportDpr)
|
|
240
|
-
const zoomLevel = computed(() => store.state.zoomLevel)
|
|
241
|
-
const kWidth = computed(() => store.state.kWidth)
|
|
242
|
-
const kGap = computed(() => store.state.kGap)
|
|
243
|
-
const paneRatios = computed(() => store.state.paneRatios)
|
|
244
|
-
const selectedDrawingId = computed(() => store.state.selectedDrawingId)
|
|
245
|
-
const dataVersion = computed(() => store.state.dataVersion)
|
|
246
|
-
|
|
247
222
|
function scheduleRender() {
|
|
248
|
-
|
|
223
|
+
/* Controller auto-renders on state changes */
|
|
249
224
|
}
|
|
250
225
|
|
|
251
226
|
function handleSettingsChange(settings: Record<string, boolean | string>) {
|
|
252
|
-
|
|
227
|
+
controller.value?.updateSettingsFacade(settings)
|
|
253
228
|
|
|
254
|
-
// 万条K线性能测试
|
|
255
229
|
if (settings.performanceTest10kKlines) {
|
|
256
230
|
const testData = generate10kKLineData()
|
|
257
231
|
console.time('updateData-10k')
|
|
258
|
-
|
|
232
|
+
controller.value?.updateData(testData)
|
|
259
233
|
console.timeEnd('updateData-10k')
|
|
260
|
-
|
|
261
|
-
|
|
234
|
+
dataLength.value = testData.length
|
|
235
|
+
dataVersion.value++
|
|
262
236
|
} else {
|
|
263
|
-
|
|
264
|
-
// 通过重新应用语义化配置来恢复
|
|
265
|
-
if (semanticController.value && chartRef.value?.getData()?.length === 10000) {
|
|
237
|
+
if (semanticController.value && controller.value?.getData()?.length === 10000) {
|
|
266
238
|
semanticController.value.applyConfig(props.semanticConfig)
|
|
267
239
|
}
|
|
268
240
|
}
|
|
@@ -318,7 +290,7 @@ function setTooltipEl(el: HTMLDivElement | null) {
|
|
|
318
290
|
nextTick(() => {
|
|
319
291
|
if (!el.isConnected) return
|
|
320
292
|
const size = measureTooltipSize(el, 180, 80)
|
|
321
|
-
|
|
293
|
+
controller.value?.setTooltipSize(size)
|
|
322
294
|
})
|
|
323
295
|
}
|
|
324
296
|
|
|
@@ -368,7 +340,7 @@ const drawingController = shallowRef<DrawingInteractionController | null>(null)
|
|
|
368
340
|
const selectedDrawing = computed(() => {
|
|
369
341
|
const id = selectedDrawingId.value
|
|
370
342
|
if (!id) return null
|
|
371
|
-
return
|
|
343
|
+
return drawings.value.find((d) => d.id === id) ?? null
|
|
372
344
|
})
|
|
373
345
|
const paneSeparatorLines = ref<Array<{ id: string; top: number }>>([])
|
|
374
346
|
const markerTooltipSize = ref({ width: 220, height: 120 })
|
|
@@ -403,8 +375,8 @@ const containerCursor = computed(() => {
|
|
|
403
375
|
const hovered = computed(() => {
|
|
404
376
|
const idx = interactionState.value.hoveredIndex
|
|
405
377
|
if (typeof idx !== 'number') return null
|
|
406
|
-
void dataVersion.value
|
|
407
|
-
const data =
|
|
378
|
+
void dataVersion.value
|
|
379
|
+
const data = controller.value?.getData()
|
|
408
380
|
if (data && idx >= 0 && idx < data.length) {
|
|
409
381
|
return data[idx]
|
|
410
382
|
}
|
|
@@ -430,8 +402,8 @@ const markerTooltipAnchorStyle = computed(() => ({
|
|
|
430
402
|
}))
|
|
431
403
|
const tooltipAnchorPlacement = computed(() => interactionState.value.tooltipAnchorPlacement)
|
|
432
404
|
const markerTooltipAnchorPlacement = computed<'right-bottom' | 'left-bottom'>(() => {
|
|
433
|
-
const
|
|
434
|
-
const viewport =
|
|
405
|
+
const c = controller.value
|
|
406
|
+
const viewport = c?.viewport.peek()
|
|
435
407
|
const container = containerRef.value
|
|
436
408
|
const plotWidth = viewport?.plotWidth ?? (container ? container.clientWidth : 0)
|
|
437
409
|
const padding = 12
|
|
@@ -441,10 +413,9 @@ const markerTooltipAnchorPlacement = computed<'right-bottom' | 'left-bottom'>(()
|
|
|
441
413
|
return wouldOverflowRight ? 'left-bottom' : 'right-bottom'
|
|
442
414
|
})
|
|
443
415
|
|
|
444
|
-
// 获取当前图表数据
|
|
445
416
|
const chartData = computed(() => {
|
|
446
|
-
void dataVersion.value
|
|
447
|
-
return
|
|
417
|
+
void dataVersion.value
|
|
418
|
+
return controller.value?.getData() ?? []
|
|
448
419
|
})
|
|
449
420
|
|
|
450
421
|
// 通知数据变化(在数据更新后调用)
|
|
@@ -456,24 +427,21 @@ function onUpdateDrawingStyle(style: Partial<DrawingStyle>) {
|
|
|
456
427
|
const d = selectedDrawing.value
|
|
457
428
|
if (!d || !drawingController.value) return
|
|
458
429
|
drawingController.value.updateDrawingStyle(d.id, style)
|
|
459
|
-
store.actions.bumpDrawingVersion()
|
|
460
430
|
}
|
|
461
431
|
|
|
462
432
|
function onDeleteDrawing() {
|
|
463
433
|
const d = selectedDrawing.value
|
|
464
434
|
if (!d || !drawingController.value) return
|
|
465
435
|
drawingController.value.removeDrawing(d.id)
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
store.actions.setDrawings(drawingController.value.getDrawings())
|
|
436
|
+
selectedDrawingId.value = null
|
|
437
|
+
drawings.value = drawingController.value.getDrawings()
|
|
469
438
|
}
|
|
470
439
|
|
|
471
440
|
function onPointerDown(e: PointerEvent) {
|
|
472
|
-
|
|
441
|
+
controller.value?.handlePointerEvent(e, {
|
|
473
442
|
onPointerDown: (event, container) => {
|
|
474
443
|
if (drawingController.value?.onPointerDown(event, container)) {
|
|
475
|
-
|
|
476
|
-
store.actions.bumpDrawingVersion()
|
|
444
|
+
drawings.value = drawingController.value.getDrawings()
|
|
477
445
|
return true
|
|
478
446
|
}
|
|
479
447
|
return false
|
|
@@ -490,10 +458,10 @@ function onPointerMove(e: PointerEvent) {
|
|
|
490
458
|
y: e.clientY - rect.top,
|
|
491
459
|
}
|
|
492
460
|
}
|
|
493
|
-
|
|
461
|
+
controller.value?.handlePointerEvent(e, {
|
|
494
462
|
onPointerMove: (event, container) => {
|
|
495
463
|
if (drawingController.value?.onPointerMove(event, container)) {
|
|
496
|
-
|
|
464
|
+
drawings.value = drawingController.value.getDrawings()
|
|
497
465
|
return true
|
|
498
466
|
}
|
|
499
467
|
return false
|
|
@@ -502,10 +470,10 @@ function onPointerMove(e: PointerEvent) {
|
|
|
502
470
|
}
|
|
503
471
|
|
|
504
472
|
function onPointerUp(e: PointerEvent) {
|
|
505
|
-
|
|
473
|
+
controller.value?.handlePointerEvent(e, {
|
|
506
474
|
onPointerUp: (event, container) => {
|
|
507
475
|
if (drawingController.value?.onPointerUp(event, container)) {
|
|
508
|
-
|
|
476
|
+
drawings.value = drawingController.value.getDrawings()
|
|
509
477
|
return true
|
|
510
478
|
}
|
|
511
479
|
return false
|
|
@@ -514,28 +482,27 @@ function onPointerUp(e: PointerEvent) {
|
|
|
514
482
|
}
|
|
515
483
|
|
|
516
484
|
function onPointerLeave(e: PointerEvent) {
|
|
517
|
-
|
|
518
|
-
chartRef.value?.handlePointerEvent(e)
|
|
485
|
+
controller.value?.handlePointerEvent(e)
|
|
519
486
|
}
|
|
520
487
|
|
|
521
488
|
function onRightAxisPointerDown(e: PointerEvent) {
|
|
522
|
-
|
|
489
|
+
controller.value?.handlePointerEvent(e)
|
|
523
490
|
}
|
|
524
491
|
|
|
525
492
|
function onRightAxisPointerMove(e: PointerEvent) {
|
|
526
|
-
|
|
493
|
+
controller.value?.handlePointerEvent(e)
|
|
527
494
|
}
|
|
528
495
|
|
|
529
496
|
function onRightAxisPointerUp(e: PointerEvent) {
|
|
530
|
-
|
|
497
|
+
controller.value?.handlePointerEvent(e)
|
|
531
498
|
}
|
|
532
499
|
|
|
533
500
|
function onRightAxisPointerLeave(e: PointerEvent) {
|
|
534
|
-
|
|
501
|
+
controller.value?.handlePointerEvent(e)
|
|
535
502
|
}
|
|
536
503
|
|
|
537
504
|
function onScroll() {
|
|
538
|
-
|
|
505
|
+
controller.value?.handleScrollEvent()
|
|
539
506
|
}
|
|
540
507
|
|
|
541
508
|
// 主图指标显式状态(副图指标从 subPanes 派生)
|
|
@@ -607,27 +574,6 @@ function generatePaneId(indicatorId: SubIndicatorType): string {
|
|
|
607
574
|
return `${indicatorId}_${count}`
|
|
608
575
|
}
|
|
609
576
|
|
|
610
|
-
// paneTitle 渲染器名称映射(paneId -> rendererName)
|
|
611
|
-
const paneTitleRendererNames = new Map<string, string>()
|
|
612
|
-
|
|
613
|
-
function mountSubPaneTitle(paneId: string, indicatorId: SubIndicatorType): void {
|
|
614
|
-
const paneTitleRenderer = createPaneTitleRendererPlugin({
|
|
615
|
-
paneId,
|
|
616
|
-
title: indicatorId,
|
|
617
|
-
getTitleInfo: () => getSubPaneTitleInfo(paneId),
|
|
618
|
-
})
|
|
619
|
-
chartRef.value?.useRenderer(paneTitleRenderer)
|
|
620
|
-
paneTitleRendererNames.set(paneId, paneTitleRenderer.name)
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function unmountSubPaneTitle(paneId: string): void {
|
|
624
|
-
const rendererName = paneTitleRendererNames.get(paneId)
|
|
625
|
-
if (rendererName) {
|
|
626
|
-
chartRef.value?.removeRenderer(rendererName)
|
|
627
|
-
paneTitleRendererNames.delete(paneId)
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
577
|
// 添加副图(使用 Chart API)
|
|
632
578
|
function addSubPane(
|
|
633
579
|
indicatorId: SubIndicatorType = 'VOLUME',
|
|
@@ -639,134 +585,41 @@ function addSubPane(
|
|
|
639
585
|
|
|
640
586
|
const mergedParams = params ?? getDefaultParams(indicatorId)
|
|
641
587
|
|
|
642
|
-
|
|
643
|
-
const paneId = chartRef.value?.addIndicator(indicatorId, 'sub', mergedParams)
|
|
588
|
+
const paneId = controller.value?.addIndicator(indicatorId, 'sub', mergedParams)
|
|
644
589
|
if (!paneId) return false
|
|
645
|
-
|
|
646
|
-
// 创建 paneTitle 渲染器(UI 层职责)
|
|
647
|
-
mountSubPaneTitle(paneId, indicatorId)
|
|
648
|
-
|
|
649
|
-
// 更新本地状态
|
|
650
|
-
subPanes.value.push({
|
|
651
|
-
id: paneId,
|
|
652
|
-
indicatorId,
|
|
653
|
-
params: mergedParams,
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
scheduleRender()
|
|
657
590
|
return true
|
|
658
591
|
}
|
|
659
592
|
|
|
660
|
-
// 移除副图(使用高层 Facade API)
|
|
661
593
|
function removeSubPane(paneId: string): void {
|
|
662
|
-
|
|
663
|
-
if (index === -1) return
|
|
664
|
-
|
|
665
|
-
const pane = subPanes.value[index]
|
|
666
|
-
if (!pane) return
|
|
667
|
-
|
|
668
|
-
// 移除 paneTitle 渲染器
|
|
669
|
-
unmountSubPaneTitle(paneId)
|
|
670
|
-
|
|
671
|
-
// 使用高层 Facade API 移除指标
|
|
672
|
-
chartRef.value?.removeIndicator(paneId)
|
|
673
|
-
|
|
674
|
-
// 更新本地状态
|
|
675
|
-
subPanes.value.splice(index, 1)
|
|
594
|
+
controller.value?.removeIndicator(paneId)
|
|
676
595
|
}
|
|
677
596
|
|
|
678
|
-
// 清除所有副图(使用高层 Facade API)
|
|
679
597
|
function clearAllSubPanes(): void {
|
|
680
|
-
// 使用高层 Facade API 逐个移除
|
|
681
598
|
for (const pane of subPanes.value) {
|
|
682
|
-
|
|
683
|
-
unmountSubPaneTitle(pane.id)
|
|
599
|
+
controller.value?.removeIndicator(pane.id)
|
|
684
600
|
}
|
|
685
|
-
|
|
686
|
-
// 清空本地状态
|
|
687
|
-
subPanes.value = []
|
|
688
601
|
subPaneCounters.clear()
|
|
689
|
-
paneTitleRendererNames.clear()
|
|
690
602
|
}
|
|
691
603
|
|
|
692
|
-
// 从语义化配置初始化指标状态(单向数据流:config → chart)
|
|
693
604
|
function initIndicatorsFromConfig(): void {
|
|
694
605
|
const config = props.semanticConfig
|
|
695
|
-
const
|
|
696
|
-
if (!
|
|
606
|
+
const c = controller.value
|
|
607
|
+
if (!c) return
|
|
697
608
|
|
|
698
|
-
// 初始化主图指标 - 直接调用Chart API
|
|
699
609
|
const mainIndicators = config.indicators?.main
|
|
700
610
|
if (mainIndicators) {
|
|
701
611
|
for (const indicator of mainIndicators) {
|
|
702
612
|
if (indicator.enabled) {
|
|
703
|
-
|
|
704
|
-
if (!mainActiveIndicators.value.includes(indicator.type)) {
|
|
705
|
-
mainActiveIndicators.value.push(indicator.type)
|
|
706
|
-
}
|
|
707
|
-
// 保存参数
|
|
708
|
-
if (indicator.params) {
|
|
709
|
-
indicatorParams.value[indicator.type] = indicator.params as Record<string, unknown>
|
|
710
|
-
}
|
|
711
|
-
// 启用指标(Chart内部管理渲染器)
|
|
712
|
-
chart.enableMainIndicator(
|
|
713
|
-
indicator.type,
|
|
714
|
-
indicator.params as Record<string, number | boolean | string>,
|
|
715
|
-
)
|
|
613
|
+
c.addIndicator(indicator.type, 'main', indicator.params as Record<string, number | boolean | string>)
|
|
716
614
|
}
|
|
717
615
|
}
|
|
718
616
|
}
|
|
719
|
-
|
|
720
|
-
// 副图指标参数由 syncSubPanesFromChart 处理
|
|
721
617
|
}
|
|
722
618
|
|
|
723
|
-
// 监听主图指标参数变化,同步到Chart(状态由Chart管理,Vue只同步参数)
|
|
724
|
-
watch(
|
|
725
|
-
[activeIndicators, indicatorParams],
|
|
726
|
-
([indicators]) => {
|
|
727
|
-
const chart = chartRef.value
|
|
728
|
-
if (!chart) return
|
|
729
|
-
|
|
730
|
-
// 只更新mainIndicatorLegend的配置(用于图例显示)
|
|
731
|
-
// 渲染器的启用/禁用由Chart内部管理
|
|
732
|
-
chart.updateRendererConfig('mainIndicatorLegend', {
|
|
733
|
-
indicators: {
|
|
734
|
-
MA: {
|
|
735
|
-
enabled: indicators.includes('MA'),
|
|
736
|
-
params: indicatorParams.value['MA'] || {},
|
|
737
|
-
},
|
|
738
|
-
BOLL: {
|
|
739
|
-
enabled: indicators.includes('BOLL'),
|
|
740
|
-
params: indicatorParams.value['BOLL'] || {},
|
|
741
|
-
},
|
|
742
|
-
EXPMA: {
|
|
743
|
-
enabled: indicators.includes('EXPMA'),
|
|
744
|
-
params: indicatorParams.value['EXPMA'] || {},
|
|
745
|
-
},
|
|
746
|
-
ENE: {
|
|
747
|
-
enabled: indicators.includes('ENE'),
|
|
748
|
-
params: indicatorParams.value['ENE'] || {},
|
|
749
|
-
},
|
|
750
|
-
},
|
|
751
|
-
})
|
|
752
|
-
|
|
753
|
-
scheduleRender()
|
|
754
|
-
},
|
|
755
|
-
{ deep: true },
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
// 从 Chart 同步副图状态到本地(语义化配置后调用)
|
|
759
619
|
function syncSubPanesFromChart(): void {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
// 清空本地状态
|
|
763
|
-
subPanes.value = []
|
|
764
|
-
paneTitleRendererNames.clear()
|
|
765
|
-
|
|
766
|
-
for (const entry of chartSubPaneEntries) {
|
|
620
|
+
const entries = controller.value?.subPanes.peek() ?? []
|
|
621
|
+
for (const entry of entries) {
|
|
767
622
|
const { paneId, indicatorId, params } = entry
|
|
768
|
-
|
|
769
|
-
// 恢复计数器状态
|
|
770
623
|
const match = paneId.match(/^(.+)_(\d+)$/)
|
|
771
624
|
if (match) {
|
|
772
625
|
const [, indicator, countStr] = match
|
|
@@ -776,216 +629,68 @@ function syncSubPanesFromChart(): void {
|
|
|
776
629
|
subPaneCounters.set(indicator as SubIndicatorType, count + 1)
|
|
777
630
|
}
|
|
778
631
|
}
|
|
779
|
-
|
|
780
|
-
// 创建 paneTitle 渲染器
|
|
781
|
-
mountSubPaneTitle(paneId, indicatorId)
|
|
782
|
-
|
|
783
|
-
// 更新本地状态
|
|
784
|
-
subPanes.value.push({
|
|
785
|
-
id: paneId,
|
|
786
|
-
indicatorId,
|
|
787
|
-
params: { ...params },
|
|
788
|
-
})
|
|
789
632
|
}
|
|
790
|
-
|
|
791
|
-
scheduleRender()
|
|
792
633
|
}
|
|
793
634
|
|
|
794
|
-
// 切换副图指标(使用 Chart API)
|
|
795
635
|
function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
|
|
796
|
-
const pane = subPanes.value.find((p) => p.id === paneId)
|
|
797
|
-
if (!pane) return
|
|
798
|
-
|
|
799
636
|
const nextParams = getDefaultParams(newIndicatorId)
|
|
800
|
-
|
|
801
|
-
// 移除旧的 paneTitle 渲染器
|
|
802
|
-
unmountSubPaneTitle(paneId)
|
|
803
|
-
|
|
804
|
-
// 使用 Chart API 替换副图指标(paneId 不变,只换指标类型)
|
|
805
|
-
chartRef.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
806
|
-
|
|
807
|
-
// 创建新的 paneTitle 渲染器
|
|
808
|
-
mountSubPaneTitle(paneId, newIndicatorId)
|
|
809
|
-
|
|
810
|
-
// 更新本地状态(paneId 保持不变)
|
|
811
|
-
const index = subPanes.value.findIndex((p) => p.id === paneId)
|
|
812
|
-
if (index !== -1) {
|
|
813
|
-
subPanes.value[index] = {
|
|
814
|
-
id: paneId,
|
|
815
|
-
indicatorId: newIndicatorId,
|
|
816
|
-
params: nextParams,
|
|
817
|
-
}
|
|
818
|
-
}
|
|
637
|
+
controller.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
819
638
|
}
|
|
820
639
|
|
|
821
|
-
// 获取副图标题信息(带缓存,只在 crosshairIdx 或 data 变化时重算)
|
|
822
|
-
const _titleInfoCache = new Map<
|
|
823
|
-
string,
|
|
824
|
-
{ idx: number | null; dataLen: number; result: TitleInfo | null }
|
|
825
|
-
>()
|
|
826
|
-
|
|
827
|
-
function getSubPaneTitleInfo(paneId: string): TitleInfo | null {
|
|
828
|
-
const pane = subPanes.value.find((p) => p.id === paneId)
|
|
829
|
-
if (!pane) return null
|
|
830
|
-
|
|
831
|
-
const data = chartRef.value?.getData()
|
|
832
|
-
if (!data || data.length === 0) return null
|
|
833
|
-
|
|
834
|
-
const idx = crosshairIdx.value
|
|
835
|
-
const dataLen = data.length
|
|
836
|
-
|
|
837
|
-
// 缓存命中:crosshairIdx 和 dataLen 都没变
|
|
838
|
-
const cached = _titleInfoCache.get(paneId)
|
|
839
|
-
if (cached && cached.idx === idx && cached.dataLen === dataLen) {
|
|
840
|
-
return cached.result
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const config = SUB_PANE_INDICATOR_CONFIGS[pane.indicatorId]
|
|
844
|
-
const params = pane.params as Record<string, number>
|
|
845
|
-
const pluginHost = chartRef.value?.plugin
|
|
846
|
-
const result = pluginHost ? config.getTitleInfo(data, idx, params, pluginHost, paneId) : null
|
|
847
|
-
|
|
848
|
-
_titleInfoCache.set(paneId, { idx, dataLen, result })
|
|
849
|
-
return result
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// 指标切换处理(使用高层 Facade API)
|
|
853
640
|
function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
854
|
-
const
|
|
855
|
-
if (!
|
|
641
|
+
const c = controller.value
|
|
642
|
+
if (!c) return
|
|
856
643
|
|
|
857
|
-
// 主图指标处理
|
|
858
644
|
const mainIndicatorIds = [
|
|
859
|
-
'MA',
|
|
860
|
-
'
|
|
861
|
-
'
|
|
862
|
-
'ENE',
|
|
863
|
-
'WMA',
|
|
864
|
-
'DEMA',
|
|
865
|
-
'TEMA',
|
|
866
|
-
'HMA',
|
|
867
|
-
'KAMA',
|
|
868
|
-
'SAR',
|
|
869
|
-
'SUPERTREND',
|
|
870
|
-
'KELTNER',
|
|
871
|
-
'DONCHIAN',
|
|
872
|
-
'ICHIMOKU',
|
|
873
|
-
'PIVOT',
|
|
874
|
-
'FIB',
|
|
875
|
-
'STRUCTURE',
|
|
876
|
-
'ZONES',
|
|
645
|
+
'MA', 'BOLL', 'EXPMA', 'ENE', 'WMA', 'DEMA', 'TEMA', 'HMA',
|
|
646
|
+
'KAMA', 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
|
|
647
|
+
'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
|
|
877
648
|
]
|
|
878
649
|
if (mainIndicatorIds.includes(indicatorId)) {
|
|
879
650
|
const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
|
|
880
|
-
|
|
881
651
|
if (active && !existingIndicator) {
|
|
882
|
-
|
|
883
|
-
chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
884
|
-
mainActiveIndicators.value.push(indicatorId)
|
|
652
|
+
c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
885
653
|
} else if (!active && existingIndicator) {
|
|
886
|
-
|
|
887
|
-
const instanceId = indicatorId.toUpperCase()
|
|
888
|
-
chart.removeIndicator(instanceId)
|
|
889
|
-
mainActiveIndicators.value = mainActiveIndicators.value.filter((id) => id !== indicatorId)
|
|
654
|
+
c.removeIndicator(indicatorId.toUpperCase())
|
|
890
655
|
}
|
|
891
656
|
return
|
|
892
657
|
}
|
|
893
658
|
|
|
894
|
-
// 副图指标处理
|
|
895
659
|
if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
|
|
896
660
|
if (active) {
|
|
897
|
-
// 如果已存在同类型指标 pane,跳过
|
|
898
661
|
const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
|
|
899
662
|
if (existingPane) return
|
|
900
|
-
|
|
901
|
-
// 副图数量上限检查
|
|
902
663
|
if (subPanes.value.length >= maxSubPanes) return
|
|
903
664
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (paneId) {
|
|
907
|
-
// 创建 paneTitle 渲染器
|
|
908
|
-
mountSubPaneTitle(paneId, indicatorId as SubIndicatorType)
|
|
909
|
-
// 同步本地状态
|
|
910
|
-
subPanes.value.push({
|
|
911
|
-
id: paneId,
|
|
912
|
-
indicatorId: indicatorId as SubIndicatorType,
|
|
913
|
-
params: { ...indicatorParams.value[indicatorId] },
|
|
914
|
-
})
|
|
915
|
-
} else if (subPanes.value.length > 0) {
|
|
916
|
-
// 添加失败(可能达到上限),替换最后一个
|
|
665
|
+
const paneId = c.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
|
|
666
|
+
if (!paneId && subPanes.value.length > 0) {
|
|
917
667
|
const lastPane = subPanes.value[subPanes.value.length - 1]
|
|
918
668
|
switchSubIndicator(lastPane.id, indicatorId as SubIndicatorType)
|
|
919
669
|
}
|
|
920
670
|
} else {
|
|
921
|
-
// 找到并移除该指标的所有 pane
|
|
922
671
|
const panesToRemove = subPanes.value.filter((p) => p.indicatorId === indicatorId)
|
|
923
672
|
panesToRemove.forEach((pane) => {
|
|
924
|
-
|
|
925
|
-
unmountSubPaneTitle(pane.id)
|
|
673
|
+
c.removeIndicator(pane.id)
|
|
926
674
|
})
|
|
927
|
-
subPanes.value = subPanes.value.filter((p) => p.indicatorId !== indicatorId)
|
|
928
675
|
}
|
|
929
|
-
scheduleRender()
|
|
930
676
|
}
|
|
931
677
|
}
|
|
932
678
|
|
|
933
|
-
// 更新主图指标图例配置
|
|
934
|
-
function updateMainIndicatorLegendConfig() {
|
|
935
|
-
chartRef.value?.updateRendererConfig('mainIndicatorLegend', {
|
|
936
|
-
indicators: {
|
|
937
|
-
MA: {
|
|
938
|
-
enabled: activeIndicators.value.includes('MA'),
|
|
939
|
-
params: indicatorParams.value['MA'] || {},
|
|
940
|
-
},
|
|
941
|
-
BOLL: {
|
|
942
|
-
enabled: activeIndicators.value.includes('BOLL'),
|
|
943
|
-
params: indicatorParams.value['BOLL'] || {},
|
|
944
|
-
},
|
|
945
|
-
EXPMA: {
|
|
946
|
-
enabled: activeIndicators.value.includes('EXPMA'),
|
|
947
|
-
params: indicatorParams.value['EXPMA'] || {},
|
|
948
|
-
},
|
|
949
|
-
ENE: {
|
|
950
|
-
enabled: activeIndicators.value.includes('ENE'),
|
|
951
|
-
params: indicatorParams.value['ENE'] || {},
|
|
952
|
-
},
|
|
953
|
-
},
|
|
954
|
-
})
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// 指标参数更新处理
|
|
958
679
|
function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
|
|
959
|
-
// 保存参数配置
|
|
960
|
-
indicatorParams.value[indicatorId] = params
|
|
961
|
-
|
|
962
|
-
// 主图指标参数更新 - 使用Chart API
|
|
963
680
|
if (
|
|
964
|
-
indicatorId === 'MA' ||
|
|
965
|
-
indicatorId === '
|
|
966
|
-
indicatorId === 'EXPMA' ||
|
|
967
|
-
indicatorId === 'ENE'
|
|
681
|
+
indicatorId === 'MA' || indicatorId === 'BOLL' ||
|
|
682
|
+
indicatorId === 'EXPMA' || indicatorId === 'ENE'
|
|
968
683
|
) {
|
|
969
|
-
|
|
970
|
-
indicatorId,
|
|
971
|
-
params as Record<string, number | boolean | string>,
|
|
972
|
-
)
|
|
973
|
-
scheduleRender()
|
|
684
|
+
controller.value?.updateIndicatorParams(indicatorId, params)
|
|
974
685
|
return
|
|
975
686
|
}
|
|
976
|
-
|
|
977
687
|
if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
|
|
978
688
|
subPanes.value
|
|
979
689
|
.filter((p) => p.indicatorId === indicatorId)
|
|
980
690
|
.forEach((pane) => {
|
|
981
|
-
|
|
982
|
-
pane.params = { ...params }
|
|
691
|
+
controller.value?.updateIndicatorParams(pane.id, params)
|
|
983
692
|
})
|
|
984
|
-
scheduleRender()
|
|
985
|
-
return
|
|
986
693
|
}
|
|
987
|
-
|
|
988
|
-
scheduleRender()
|
|
989
694
|
}
|
|
990
695
|
|
|
991
696
|
function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
|
|
@@ -1022,54 +727,40 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
|
|
|
1022
727
|
|
|
1023
728
|
subPanes.value = nextSubPanes
|
|
1024
729
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
if (!chart) return
|
|
1029
|
-
chart.updatePaneLayout(buildPaneLayoutIntent())
|
|
730
|
+
const c = controller.value
|
|
731
|
+
if (!c) return
|
|
732
|
+
c.updatePaneLayout(buildPaneLayoutIntent())
|
|
1030
733
|
}
|
|
1031
734
|
|
|
1032
735
|
/* 计算总宽度:从 Vue 响应式状态读取,zoom 变化时自动重算 */
|
|
1033
736
|
const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
|
|
1034
737
|
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
const totalWidth = store.computed.totalWidth
|
|
1038
|
-
|
|
1039
|
-
// 缩放由 Chart 回调驱动 scrollLeft 与渲染时序。
|
|
738
|
+
const totalWidth = computed(() => controller.value?.getContentWidth() ?? 0)
|
|
1040
739
|
|
|
1041
740
|
function scrollToRight() {
|
|
1042
741
|
const container = containerRef.value
|
|
1043
|
-
const
|
|
1044
|
-
if (!container || !
|
|
742
|
+
const c = controller.value
|
|
743
|
+
if (!container || !c) return
|
|
1045
744
|
|
|
1046
|
-
const dataLength =
|
|
745
|
+
const dataLength = c.getData()?.length ?? 0
|
|
1047
746
|
if (dataLength === 0) return
|
|
1048
747
|
|
|
1049
|
-
const
|
|
748
|
+
const vp = c.viewport.peek()
|
|
749
|
+
const dpr = vp.dpr
|
|
1050
750
|
const { unitPx, startXPx } = getPhysicalKLineConfig(kWidth.value, kGap.value, dpr)
|
|
1051
751
|
|
|
1052
|
-
// 计算最后一根K线的结束位置(不含 TRAILING_DRAWING_SLOTS)
|
|
1053
752
|
const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
|
|
1054
|
-
|
|
1055
|
-
// 计算最大可滚动距离
|
|
1056
753
|
const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth)
|
|
1057
|
-
|
|
1058
|
-
// 计算需要的滚动位置,使最后一根K线紧贴最右侧
|
|
1059
754
|
const targetScrollLeft = Math.min(
|
|
1060
755
|
maxScrollLeft,
|
|
1061
756
|
Math.max(0, lastKLineEndPx - container.clientWidth),
|
|
1062
757
|
)
|
|
1063
758
|
|
|
1064
759
|
container.scrollLeft = Math.round(targetScrollLeft * dpr) / dpr
|
|
1065
|
-
scheduleRender()
|
|
1066
760
|
}
|
|
1067
761
|
|
|
1068
|
-
/* 缩放到指定级别(通过 Chart facade API) */
|
|
1069
762
|
function applyZoomToLevel(targetLevel: number, anchorX?: number) {
|
|
1070
|
-
|
|
1071
|
-
if (!chart) return
|
|
1072
|
-
chart.zoomToLevel(targetLevel, anchorX)
|
|
763
|
+
controller.value?.zoomToLevel(targetLevel, anchorX)
|
|
1073
764
|
}
|
|
1074
765
|
|
|
1075
766
|
defineExpose({
|
|
@@ -1079,30 +770,20 @@ defineExpose({
|
|
|
1079
770
|
removeSubPane,
|
|
1080
771
|
switchSubIndicator,
|
|
1081
772
|
clearAllSubPanes,
|
|
1082
|
-
get plugin() {
|
|
1083
|
-
return chartRef.value?.plugin
|
|
1084
|
-
},
|
|
1085
|
-
|
|
1086
|
-
// Zoom Level API(Vue SSOT)
|
|
1087
773
|
zoomToLevel: applyZoomToLevel,
|
|
1088
774
|
zoomIn: (anchorX?: number) => applyZoomToLevel(zoomLevel.value + 1, anchorX),
|
|
1089
775
|
zoomOut: (anchorX?: number) => applyZoomToLevel(zoomLevel.value - 1, anchorX),
|
|
1090
776
|
getZoomLevel: () => zoomLevel.value,
|
|
1091
|
-
getZoomLevelCount: () =>
|
|
777
|
+
getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
|
|
1092
778
|
})
|
|
1093
779
|
|
|
1094
780
|
// ==================== onMounted 拆分函数 ====================
|
|
1095
781
|
|
|
1096
|
-
function setupWheelHandler(
|
|
782
|
+
function setupWheelHandler(): (e: WheelEvent) => void {
|
|
1097
783
|
const onWheelHandler = (e: WheelEvent) => {
|
|
1098
784
|
e.preventDefault()
|
|
1099
|
-
|
|
1100
|
-
if (!chart) return
|
|
1101
|
-
|
|
1102
|
-
// 使用 Chart facade API 处理滚轮事件
|
|
1103
|
-
chart.handleWheelEvent(e)
|
|
785
|
+
controller.value?.handleWheelEvent(e)
|
|
1104
786
|
}
|
|
1105
|
-
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
1106
787
|
return onWheelHandler
|
|
1107
788
|
}
|
|
1108
789
|
|
|
@@ -1111,74 +792,65 @@ function initChart(
|
|
|
1111
792
|
canvasLayer: HTMLDivElement,
|
|
1112
793
|
rightAxisLayer: HTMLDivElement,
|
|
1113
794
|
xAxisCanvas: HTMLCanvasElement,
|
|
1114
|
-
):
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
)
|
|
1130
|
-
return
|
|
795
|
+
): ChartController {
|
|
796
|
+
const ctrl = createChartController({
|
|
797
|
+
container,
|
|
798
|
+
data: [],
|
|
799
|
+
canvasLayer,
|
|
800
|
+
rightAxisLayer,
|
|
801
|
+
xAxisCanvas,
|
|
802
|
+
initialZoomLevel: props.initialZoomLevel,
|
|
803
|
+
zoomLevels: props.zoomLevels,
|
|
804
|
+
yPaddingPx: props.yPaddingPx,
|
|
805
|
+
rightAxisWidth: props.rightAxisWidth,
|
|
806
|
+
bottomAxisHeight: props.bottomAxisHeight,
|
|
807
|
+
priceLabelWidth: props.priceLabelWidth,
|
|
808
|
+
minKWidth: props.minKWidth,
|
|
809
|
+
maxKWidth: props.maxKWidth,
|
|
810
|
+
})
|
|
811
|
+
return ctrl
|
|
1131
812
|
}
|
|
1132
813
|
|
|
1133
|
-
function setupChartCallbacks(
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
chart.setOnPaneLayoutChange(() => {
|
|
1137
|
-
// 分隔线位置计算(需要实际像素位置,保留在回调中)
|
|
814
|
+
function setupChartCallbacks(ctrl: ChartController): void {
|
|
815
|
+
const unsubscribePaneLayout = ctrl.paneLayout.subscribe(() => {
|
|
1138
816
|
invalidateContainerRectCache()
|
|
1139
|
-
const renderers = chart.getPaneRenderers()
|
|
1140
817
|
const borderTop = containerRef.value
|
|
1141
818
|
? parseInt(getComputedStyle(containerRef.value).borderTopWidth) || 0
|
|
1142
819
|
: 0
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
820
|
+
const panes = ctrl.paneLayout.peek()
|
|
821
|
+
// 使用 pane 的实际渲染位置计算分隔线位置,确保与鼠标检测一致
|
|
822
|
+
paneSeparatorLines.value = panes.slice(0, -1).map((pane) => {
|
|
823
|
+
const paneInfo = ctrl.getPaneInfo(pane.id)
|
|
824
|
+
// 分隔线位置 = pane 顶部位置 + pane 实际高度
|
|
825
|
+
const separatorTop = (paneInfo?.top ?? 0) + (paneInfo?.height ?? 0)
|
|
826
|
+
return { id: pane.id, top: separatorTop + borderTop }
|
|
1149
827
|
})
|
|
1150
828
|
})
|
|
1151
829
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
store.actions.setPaneRatios({ ...ratios })
|
|
830
|
+
const unsubscribePaneRatios = ctrl.paneRatios.subscribe(() => {
|
|
831
|
+
const ratios = ctrl.paneRatios.peek()
|
|
832
|
+
paneRatios.value = { ...ratios }
|
|
1156
833
|
})
|
|
1157
834
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const vp = chart.viewport.peek()
|
|
835
|
+
const unsubscribeViewport = ctrl.viewport.subscribe(() => {
|
|
836
|
+
const vp = ctrl.viewport.peek()
|
|
1161
837
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
store.actions.setViewportDpr(vp.dpr)
|
|
838
|
+
if (viewportDpr.value !== vp.dpr) {
|
|
839
|
+
viewportDpr.value = vp.dpr
|
|
1165
840
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (store.state.viewWidth !== vp.plotWidth) {
|
|
1169
|
-
store.actions.setViewWidth(vp.plotWidth)
|
|
841
|
+
if (viewWidth.value !== vp.plotWidth) {
|
|
842
|
+
viewWidth.value = vp.plotWidth
|
|
1170
843
|
}
|
|
1171
|
-
|
|
1172
|
-
// 完整同步 zoom state 到 Vue store(Chart 是 SSOT)
|
|
1173
844
|
if (
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
845
|
+
zoomLevel.value !== vp.zoomLevel ||
|
|
846
|
+
kWidth.value !== vp.kWidth ||
|
|
847
|
+
kGap.value !== vp.kGap
|
|
1177
848
|
) {
|
|
1178
|
-
|
|
849
|
+
zoomLevel.value = vp.zoomLevel
|
|
850
|
+
kWidth.value = vp.kWidth
|
|
851
|
+
kGap.value = vp.kGap
|
|
1179
852
|
}
|
|
1180
853
|
|
|
1181
|
-
// 在 nextTick 中应用 desiredScrollLeft
|
|
1182
854
|
const desiredLeft = vp.desiredScrollLeft
|
|
1183
855
|
if (desiredLeft !== undefined && desiredLeft !== containerRef.value?.scrollLeft) {
|
|
1184
856
|
invalidateContainerRectCache()
|
|
@@ -1187,85 +859,125 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1187
859
|
if (!c) return
|
|
1188
860
|
const maxScrollLeft = Math.max(0, c.scrollWidth - c.clientWidth)
|
|
1189
861
|
const clampedScrollLeft = Math.min(Math.max(0, desiredLeft), maxScrollLeft)
|
|
1190
|
-
const dpr =
|
|
862
|
+
const dpr = vp.dpr
|
|
1191
863
|
c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
|
|
1192
864
|
})
|
|
1193
865
|
}
|
|
1194
866
|
})
|
|
1195
867
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
store.actions.bumpDataVersion()
|
|
868
|
+
const unsubscribeData = ctrl.data.subscribe(() => {
|
|
869
|
+
const data = ctrl.data.peek()
|
|
870
|
+
dataLength.value = data.length
|
|
871
|
+
dataVersion.value++
|
|
1201
872
|
})
|
|
1202
873
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
874
|
+
const unsubscribeTheme = ctrl.theme.subscribe(() => {
|
|
875
|
+
chartTheme.value = ctrl.theme.peek()
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
const unsubscribeIndicators = ctrl.indicators.subscribe(() => {
|
|
879
|
+
const instances = ctrl.indicators.peek()
|
|
880
|
+
|
|
881
|
+
const mains = instances
|
|
882
|
+
.filter((i): i is IndicatorInstance & { role: 'main' } => i.role === 'main')
|
|
883
|
+
.map((i) => i.definitionId)
|
|
884
|
+
mainActiveIndicators.value = mains
|
|
885
|
+
|
|
886
|
+
const nextParams = { ...indicatorParams.value }
|
|
887
|
+
for (const inst of instances) {
|
|
888
|
+
if (inst.role === 'main' && inst.params && Object.keys(inst.params).length > 0) {
|
|
889
|
+
nextParams[inst.definitionId] = { ...inst.params }
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
ctrl.updateRendererConfig('mainIndicatorLegend', {
|
|
894
|
+
indicators: {
|
|
895
|
+
MA: { enabled: mains.includes('MA'), params: nextParams['MA'] || {} },
|
|
896
|
+
BOLL: { enabled: mains.includes('BOLL'), params: nextParams['BOLL'] || {} },
|
|
897
|
+
EXPMA: { enabled: mains.includes('EXPMA'), params: nextParams['EXPMA'] || {} },
|
|
898
|
+
ENE: { enabled: mains.includes('ENE'), params: nextParams['ENE'] || {} },
|
|
899
|
+
},
|
|
900
|
+
})
|
|
901
|
+
|
|
902
|
+
indicatorParams.value = nextParams
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
const unsubscribeSubPanes = ctrl.subPanes.subscribe(() => {
|
|
906
|
+
const subPaneInfos = ctrl.subPanes.peek()
|
|
907
|
+
const signalIds = new Set(subPaneInfos.map((sp) => sp.paneId))
|
|
908
|
+
|
|
909
|
+
const merged = subPanes.value.filter((p) => signalIds.has(p.id))
|
|
910
|
+
const existingIds = new Set(merged.map((p) => p.id))
|
|
911
|
+
for (const sp of subPaneInfos) {
|
|
912
|
+
if (!existingIds.has(sp.paneId)) {
|
|
913
|
+
merged.push({
|
|
914
|
+
id: sp.paneId,
|
|
915
|
+
indicatorId: sp.indicatorId as SubIndicatorType,
|
|
916
|
+
params: sp.params,
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
subPanes.value = merged
|
|
921
|
+
|
|
922
|
+
const nextParams = { ...indicatorParams.value }
|
|
923
|
+
for (const sp of subPaneInfos) {
|
|
924
|
+
if (sp.params && Object.keys(sp.params).length > 0) {
|
|
925
|
+
nextParams[sp.indicatorId] = { ...sp.params }
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
indicatorParams.value = nextParams
|
|
1207
929
|
})
|
|
1208
930
|
|
|
1209
|
-
// 保存 unsubscribe 函数以便清理
|
|
1210
931
|
onUnmounted(() => {
|
|
1211
932
|
unsubscribeViewport()
|
|
1212
933
|
unsubscribeData()
|
|
1213
934
|
unsubscribePaneRatios()
|
|
935
|
+
unsubscribePaneLayout()
|
|
1214
936
|
unsubscribeTheme()
|
|
937
|
+
unsubscribeIndicators()
|
|
938
|
+
unsubscribeSubPanes()
|
|
1215
939
|
})
|
|
1216
940
|
}
|
|
1217
941
|
|
|
1218
|
-
function applyInitialSettings(
|
|
942
|
+
function applyInitialSettings(ctrl: ChartController): void {
|
|
1219
943
|
const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
|
|
1220
|
-
|
|
944
|
+
ctrl.updateSettingsFacade(initialSettings)
|
|
1221
945
|
|
|
1222
946
|
if (initialSettings.performanceTest10kKlines) {
|
|
1223
947
|
const testData = generate10kKLineData()
|
|
1224
948
|
console.time('updateData-10k')
|
|
1225
|
-
|
|
949
|
+
ctrl.updateData(testData)
|
|
1226
950
|
console.timeEnd('updateData-10k')
|
|
1227
951
|
}
|
|
1228
952
|
}
|
|
1229
953
|
|
|
1230
|
-
function setupDrawingController(
|
|
1231
|
-
drawingController.value = new DrawingInteractionController(
|
|
954
|
+
function setupDrawingController(ctrl: ChartController): void {
|
|
955
|
+
drawingController.value = new DrawingInteractionController(ctrl)
|
|
1232
956
|
drawingController.value.setCallbacks({
|
|
1233
957
|
onDrawingCreated: (drawing) => {
|
|
1234
|
-
|
|
1235
|
-
|
|
958
|
+
drawings.value = [...drawings.value, drawing]
|
|
959
|
+
selectedDrawingId.value = drawing.id
|
|
1236
960
|
},
|
|
1237
961
|
onToolChange: () => {},
|
|
1238
962
|
onDrawingSelected: (drawing) => {
|
|
1239
|
-
|
|
963
|
+
selectedDrawingId.value = drawing?.id ?? null
|
|
1240
964
|
},
|
|
1241
965
|
})
|
|
1242
966
|
}
|
|
1243
967
|
|
|
1244
|
-
function setupInteractionCallbacks(
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
interactionState.value =
|
|
1248
|
-
})
|
|
1249
|
-
|
|
1250
|
-
chart.interaction.setOnPinchZoom((delta, centerClientX) => {
|
|
1251
|
-
if (!chart) return
|
|
1252
|
-
const container = containerRef.value
|
|
1253
|
-
if (!container) return
|
|
1254
|
-
// centerClientX 是 clientX,需要转换为视口局部坐标
|
|
1255
|
-
const rect = container.getBoundingClientRect()
|
|
1256
|
-
const centerX = centerClientX - rect.left
|
|
1257
|
-
chart.handlePinchZoom(delta, centerX)
|
|
968
|
+
function setupInteractionCallbacks(ctrl: ChartController): void {
|
|
969
|
+
ctrl.setTooltipAnchorPositioning(useAnchorPositioning.value)
|
|
970
|
+
ctrl.interactionState.subscribe(() => {
|
|
971
|
+
interactionState.value = ctrl.interactionState.peek()
|
|
1258
972
|
})
|
|
1259
973
|
|
|
1260
|
-
interactionState.value =
|
|
1261
|
-
|
|
1262
|
-
chart.resize()
|
|
974
|
+
interactionState.value = ctrl.interactionState.peek()
|
|
975
|
+
viewportDpr.value = ctrl.viewport.peek().dpr
|
|
1263
976
|
}
|
|
1264
977
|
|
|
1265
|
-
|
|
1266
|
-
function setupSemanticController(chart: Chart): void {
|
|
978
|
+
function setupSemanticController(ctrl: ChartController): void {
|
|
1267
979
|
__setDataFetcher(props.dataFetcher)
|
|
1268
|
-
semanticController.value = new SemanticChartController(
|
|
980
|
+
semanticController.value = new SemanticChartController(ctrl)
|
|
1269
981
|
|
|
1270
982
|
semanticController.value.on('config:error', (error) => {
|
|
1271
983
|
console.error('Semantic config error:', error)
|
|
@@ -1289,62 +1001,52 @@ onMounted(() => {
|
|
|
1289
1001
|
useAnchorPositioning.value = false
|
|
1290
1002
|
|
|
1291
1003
|
const container = containerRef.value
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
const xAxisCanvas = xAxisCanvasRef.value
|
|
1295
|
-
if (!container || !canvasLayer || !rightAxisLayer || !xAxisCanvas) return
|
|
1296
|
-
|
|
1297
|
-
// 1) 滚轮缩放:passive:false 以阻止页面滚动
|
|
1298
|
-
const onWheelHandler = setupWheelHandler(container)
|
|
1299
|
-
|
|
1300
|
-
// 2) 创建 Chart 实例并注册全部渲染器
|
|
1301
|
-
const chart = initChart(container, canvasLayer, rightAxisLayer, xAxisCanvas)
|
|
1302
|
-
chartRef.value = chart
|
|
1004
|
+
const chartMain = chartMainRef.value
|
|
1005
|
+
if (!container || !chartMain) return
|
|
1303
1006
|
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1007
|
+
// 1) 滚轮缩放处理
|
|
1008
|
+
const onWheelHandler = setupWheelHandler()
|
|
1009
|
+
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
1306
1010
|
|
|
1307
|
-
//
|
|
1308
|
-
|
|
1011
|
+
// 2) 创建 Chart 控制器(使用模板 DOM 元素)
|
|
1012
|
+
const canvasLayer = container.querySelector<HTMLDivElement>('.canvas-layer')
|
|
1013
|
+
const xAxisCanvas = container.querySelector<HTMLCanvasElement>('.x-axis-canvas')
|
|
1014
|
+
const rightAxisLayer = chartMain.querySelector<HTMLDivElement>('.right-axis-host')
|
|
1015
|
+
const ctrl = initChart(container, canvasLayer!, rightAxisLayer!, xAxisCanvas!)
|
|
1016
|
+
controller.value = ctrl
|
|
1309
1017
|
|
|
1310
|
-
//
|
|
1311
|
-
|
|
1018
|
+
// 3) 信号回调
|
|
1019
|
+
setupChartCallbacks(ctrl)
|
|
1312
1020
|
|
|
1313
|
-
//
|
|
1314
|
-
|
|
1021
|
+
// 4) 工具栏初始设置
|
|
1022
|
+
applyInitialSettings(ctrl)
|
|
1315
1023
|
|
|
1316
|
-
//
|
|
1317
|
-
|
|
1024
|
+
// 5) 绘图交互控制器
|
|
1025
|
+
setupDrawingController(ctrl)
|
|
1318
1026
|
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1027
|
+
// 6) 交互信号桥接
|
|
1028
|
+
setupInteractionCallbacks(ctrl)
|
|
1321
1029
|
|
|
1322
|
-
//
|
|
1323
|
-
|
|
1030
|
+
// 7) 语义化配置
|
|
1031
|
+
setupSemanticController(ctrl)
|
|
1324
1032
|
})
|
|
1325
1033
|
|
|
1326
1034
|
onUnmounted(() => {
|
|
1327
|
-
const
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
| undefined
|
|
1332
|
-
const container = containerRef.value
|
|
1333
|
-
if (onWheel && container) container.removeEventListener('wheel', onWheel)
|
|
1334
|
-
chart.destroy()
|
|
1035
|
+
const ctrl = controller.value
|
|
1036
|
+
if (ctrl) {
|
|
1037
|
+
controller.value = null
|
|
1038
|
+
ctrl.dispose()
|
|
1335
1039
|
}
|
|
1336
|
-
chartRef.value = null
|
|
1337
1040
|
drawingController.value = null
|
|
1338
1041
|
})
|
|
1339
1042
|
|
|
1340
1043
|
// kWidth/kGap 由 zoomLevel 派生,不再通过 props 直接修改
|
|
1341
1044
|
// 如需程序化控制缩放,请使用 expose 的 zoomToLevel/zoomIn/zoomOut 方法
|
|
1342
1045
|
|
|
1343
|
-
// 监听 yPaddingPx 变化
|
|
1344
1046
|
watch(
|
|
1345
1047
|
() => props.yPaddingPx,
|
|
1346
1048
|
(newVal) => {
|
|
1347
|
-
|
|
1049
|
+
controller.value?.updateOptionsFacade({ yPaddingPx: newVal })
|
|
1348
1050
|
},
|
|
1349
1051
|
)
|
|
1350
1052
|
|