@363045841yyt/klinechart 0.7.5 → 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 +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +544 -617
- 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 +3 -5
- package/src/components/KLineChart.vue +198 -428
- package/src/components/KLineTooltip.vue +2 -2
- package/src/components/MarkerTooltip.vue +1 -12
- package/src/composables/useFullscreenTeleportTarget.ts +0 -2
- 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,68 +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
590
|
return true
|
|
650
591
|
}
|
|
651
592
|
|
|
652
|
-
// 移除副图(使用高层 Facade API)
|
|
653
593
|
function removeSubPane(paneId: string): void {
|
|
654
|
-
|
|
655
|
-
unmountSubPaneTitle(paneId)
|
|
656
|
-
|
|
657
|
-
// 使用高层 Facade API 移除指标(Signal 订阅自动同步本地状态)
|
|
658
|
-
chartRef.value?.removeIndicator(paneId)
|
|
594
|
+
controller.value?.removeIndicator(paneId)
|
|
659
595
|
}
|
|
660
596
|
|
|
661
|
-
// 清除所有副图(使用高层 Facade API)
|
|
662
597
|
function clearAllSubPanes(): void {
|
|
663
|
-
// 使用高层 Facade API 逐个移除
|
|
664
598
|
for (const pane of subPanes.value) {
|
|
665
|
-
|
|
666
|
-
unmountSubPaneTitle(pane.id)
|
|
599
|
+
controller.value?.removeIndicator(pane.id)
|
|
667
600
|
}
|
|
668
|
-
|
|
669
|
-
// 清空本地状态(Signal 订阅自动同步 subPanes,只需要清理 UI 层状态)
|
|
670
601
|
subPaneCounters.clear()
|
|
671
|
-
paneTitleRendererNames.clear()
|
|
672
602
|
}
|
|
673
603
|
|
|
674
|
-
// 从语义化配置初始化指标状态(单向数据流:config → chart)
|
|
675
|
-
// Signal 订阅会自动同步本地状态,此处只需调用 Chart API
|
|
676
604
|
function initIndicatorsFromConfig(): void {
|
|
677
605
|
const config = props.semanticConfig
|
|
678
|
-
const
|
|
679
|
-
if (!
|
|
606
|
+
const c = controller.value
|
|
607
|
+
if (!c) return
|
|
680
608
|
|
|
681
609
|
const mainIndicators = config.indicators?.main
|
|
682
610
|
if (mainIndicators) {
|
|
683
611
|
for (const indicator of mainIndicators) {
|
|
684
612
|
if (indicator.enabled) {
|
|
685
|
-
|
|
686
|
-
indicator.type,
|
|
687
|
-
indicator.params as Record<string, number | boolean | string>,
|
|
688
|
-
)
|
|
613
|
+
c.addIndicator(indicator.type, 'main', indicator.params as Record<string, number | boolean | string>)
|
|
689
614
|
}
|
|
690
615
|
}
|
|
691
616
|
}
|
|
692
617
|
}
|
|
693
618
|
|
|
694
|
-
// 从 Chart 同步副图状态到本地(语义化配置后调用)
|
|
695
619
|
function syncSubPanesFromChart(): void {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
paneTitleRendererNames.clear()
|
|
699
|
-
|
|
700
|
-
for (const entry of chartSubPaneEntries) {
|
|
620
|
+
const entries = controller.value?.subPanes.peek() ?? []
|
|
621
|
+
for (const entry of entries) {
|
|
701
622
|
const { paneId, indicatorId, params } = entry
|
|
702
|
-
|
|
703
|
-
// 恢复计数器状态
|
|
704
623
|
const match = paneId.match(/^(.+)_(\d+)$/)
|
|
705
624
|
if (match) {
|
|
706
625
|
const [, indicator, countStr] = match
|
|
@@ -710,150 +629,67 @@ function syncSubPanesFromChart(): void {
|
|
|
710
629
|
subPaneCounters.set(indicator as SubIndicatorType, count + 1)
|
|
711
630
|
}
|
|
712
631
|
}
|
|
713
|
-
|
|
714
|
-
// 创建 paneTitle 渲染器
|
|
715
|
-
mountSubPaneTitle(paneId, indicatorId)
|
|
716
632
|
}
|
|
717
633
|
}
|
|
718
634
|
|
|
719
|
-
// 切换副图指标(使用 Chart API)
|
|
720
635
|
function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
|
|
721
636
|
const nextParams = getDefaultParams(newIndicatorId)
|
|
722
|
-
|
|
723
|
-
// 移除旧的 paneTitle 渲染器
|
|
724
|
-
unmountSubPaneTitle(paneId)
|
|
725
|
-
|
|
726
|
-
// 使用 Chart API 替换副图指标(paneId 不变,只换指标类型,Signal 订阅自动同步本地状态)
|
|
727
|
-
chartRef.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
728
|
-
|
|
729
|
-
// 创建新的 paneTitle 渲染器
|
|
730
|
-
mountSubPaneTitle(paneId, newIndicatorId)
|
|
637
|
+
controller.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
731
638
|
}
|
|
732
639
|
|
|
733
|
-
// 获取副图标题信息(带缓存,只在 crosshairIdx 或 data 变化时重算)
|
|
734
|
-
const _titleInfoCache = new Map<
|
|
735
|
-
string,
|
|
736
|
-
{ idx: number | null; dataLen: number; result: TitleInfo | null }
|
|
737
|
-
>()
|
|
738
|
-
|
|
739
|
-
function getSubPaneTitleInfo(paneId: string): TitleInfo | null {
|
|
740
|
-
const pane = subPanes.value.find((p) => p.id === paneId)
|
|
741
|
-
if (!pane) return null
|
|
742
|
-
|
|
743
|
-
const data = chartRef.value?.getData()
|
|
744
|
-
if (!data || data.length === 0) return null
|
|
745
|
-
|
|
746
|
-
const idx = crosshairIdx.value
|
|
747
|
-
const dataLen = data.length
|
|
748
|
-
|
|
749
|
-
// 缓存命中:crosshairIdx 和 dataLen 都没变
|
|
750
|
-
const cached = _titleInfoCache.get(paneId)
|
|
751
|
-
if (cached && cached.idx === idx && cached.dataLen === dataLen) {
|
|
752
|
-
return cached.result
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const config = SUB_PANE_INDICATOR_CONFIGS[pane.indicatorId]
|
|
756
|
-
const params = pane.params as Record<string, number>
|
|
757
|
-
const pluginHost = chartRef.value?.plugin
|
|
758
|
-
const result = pluginHost ? config.getTitleInfo(data, idx, params, pluginHost, paneId) : null
|
|
759
|
-
|
|
760
|
-
_titleInfoCache.set(paneId, { idx, dataLen, result })
|
|
761
|
-
return result
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// 指标切换处理(使用高层 Facade API)
|
|
765
640
|
function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
766
|
-
const
|
|
767
|
-
if (!
|
|
641
|
+
const c = controller.value
|
|
642
|
+
if (!c) return
|
|
768
643
|
|
|
769
|
-
// 主图指标处理
|
|
770
644
|
const mainIndicatorIds = [
|
|
771
|
-
'MA',
|
|
772
|
-
'
|
|
773
|
-
'
|
|
774
|
-
'ENE',
|
|
775
|
-
'WMA',
|
|
776
|
-
'DEMA',
|
|
777
|
-
'TEMA',
|
|
778
|
-
'HMA',
|
|
779
|
-
'KAMA',
|
|
780
|
-
'SAR',
|
|
781
|
-
'SUPERTREND',
|
|
782
|
-
'KELTNER',
|
|
783
|
-
'DONCHIAN',
|
|
784
|
-
'ICHIMOKU',
|
|
785
|
-
'PIVOT',
|
|
786
|
-
'FIB',
|
|
787
|
-
'STRUCTURE',
|
|
788
|
-
'ZONES',
|
|
645
|
+
'MA', 'BOLL', 'EXPMA', 'ENE', 'WMA', 'DEMA', 'TEMA', 'HMA',
|
|
646
|
+
'KAMA', 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
|
|
647
|
+
'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
|
|
789
648
|
]
|
|
790
649
|
if (mainIndicatorIds.includes(indicatorId)) {
|
|
791
650
|
const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
|
|
792
|
-
|
|
793
651
|
if (active && !existingIndicator) {
|
|
794
|
-
|
|
795
|
-
chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
652
|
+
c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
796
653
|
} else if (!active && existingIndicator) {
|
|
797
|
-
|
|
798
|
-
const instanceId = indicatorId.toUpperCase()
|
|
799
|
-
chart.removeIndicator(instanceId)
|
|
654
|
+
c.removeIndicator(indicatorId.toUpperCase())
|
|
800
655
|
}
|
|
801
656
|
return
|
|
802
657
|
}
|
|
803
658
|
|
|
804
|
-
// 副图指标处理
|
|
805
659
|
if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
|
|
806
660
|
if (active) {
|
|
807
|
-
// 如果已存在同类型指标 pane,跳过
|
|
808
661
|
const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
|
|
809
662
|
if (existingPane) return
|
|
810
|
-
|
|
811
|
-
// 副图数量上限检查
|
|
812
663
|
if (subPanes.value.length >= maxSubPanes) return
|
|
813
664
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (paneId) {
|
|
817
|
-
mountSubPaneTitle(paneId, indicatorId as SubIndicatorType)
|
|
818
|
-
} else if (subPanes.value.length > 0) {
|
|
819
|
-
// 添加失败(可能达到上限),替换最后一个
|
|
665
|
+
const paneId = c.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
|
|
666
|
+
if (!paneId && subPanes.value.length > 0) {
|
|
820
667
|
const lastPane = subPanes.value[subPanes.value.length - 1]
|
|
821
668
|
switchSubIndicator(lastPane.id, indicatorId as SubIndicatorType)
|
|
822
669
|
}
|
|
823
670
|
} else {
|
|
824
|
-
// 找到并移除该指标的所有 pane
|
|
825
671
|
const panesToRemove = subPanes.value.filter((p) => p.indicatorId === indicatorId)
|
|
826
672
|
panesToRemove.forEach((pane) => {
|
|
827
|
-
|
|
828
|
-
unmountSubPaneTitle(pane.id)
|
|
673
|
+
c.removeIndicator(pane.id)
|
|
829
674
|
})
|
|
830
675
|
}
|
|
831
676
|
}
|
|
832
677
|
}
|
|
833
678
|
|
|
834
|
-
// 指标参数更新处理
|
|
835
679
|
function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
|
|
836
|
-
// 主图指标参数更新 - 使用Chart API(Signal 订阅自动同步本地状态和 scheduleDraw)
|
|
837
680
|
if (
|
|
838
|
-
indicatorId === 'MA' ||
|
|
839
|
-
indicatorId === '
|
|
840
|
-
indicatorId === 'EXPMA' ||
|
|
841
|
-
indicatorId === 'ENE'
|
|
681
|
+
indicatorId === 'MA' || indicatorId === 'BOLL' ||
|
|
682
|
+
indicatorId === 'EXPMA' || indicatorId === 'ENE'
|
|
842
683
|
) {
|
|
843
|
-
|
|
844
|
-
indicatorId,
|
|
845
|
-
params as Record<string, number | boolean | string>,
|
|
846
|
-
)
|
|
684
|
+
controller.value?.updateIndicatorParams(indicatorId, params)
|
|
847
685
|
return
|
|
848
686
|
}
|
|
849
|
-
|
|
850
687
|
if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
|
|
851
688
|
subPanes.value
|
|
852
689
|
.filter((p) => p.indicatorId === indicatorId)
|
|
853
690
|
.forEach((pane) => {
|
|
854
|
-
|
|
691
|
+
controller.value?.updateIndicatorParams(pane.id, params)
|
|
855
692
|
})
|
|
856
|
-
return
|
|
857
693
|
}
|
|
858
694
|
}
|
|
859
695
|
|
|
@@ -891,54 +727,40 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
|
|
|
891
727
|
|
|
892
728
|
subPanes.value = nextSubPanes
|
|
893
729
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (!chart) return
|
|
898
|
-
chart.updatePaneLayout(buildPaneLayoutIntent())
|
|
730
|
+
const c = controller.value
|
|
731
|
+
if (!c) return
|
|
732
|
+
c.updatePaneLayout(buildPaneLayoutIntent())
|
|
899
733
|
}
|
|
900
734
|
|
|
901
735
|
/* 计算总宽度:从 Vue 响应式状态读取,zoom 变化时自动重算 */
|
|
902
736
|
const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
|
|
903
737
|
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
const totalWidth = store.computed.totalWidth
|
|
907
|
-
|
|
908
|
-
// 缩放由 Chart 回调驱动 scrollLeft 与渲染时序。
|
|
738
|
+
const totalWidth = computed(() => controller.value?.getContentWidth() ?? 0)
|
|
909
739
|
|
|
910
740
|
function scrollToRight() {
|
|
911
741
|
const container = containerRef.value
|
|
912
|
-
const
|
|
913
|
-
if (!container || !
|
|
742
|
+
const c = controller.value
|
|
743
|
+
if (!container || !c) return
|
|
914
744
|
|
|
915
|
-
const dataLength =
|
|
745
|
+
const dataLength = c.getData()?.length ?? 0
|
|
916
746
|
if (dataLength === 0) return
|
|
917
747
|
|
|
918
|
-
const
|
|
748
|
+
const vp = c.viewport.peek()
|
|
749
|
+
const dpr = vp.dpr
|
|
919
750
|
const { unitPx, startXPx } = getPhysicalKLineConfig(kWidth.value, kGap.value, dpr)
|
|
920
751
|
|
|
921
|
-
// 计算最后一根K线的结束位置(不含 TRAILING_DRAWING_SLOTS)
|
|
922
752
|
const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
|
|
923
|
-
|
|
924
|
-
// 计算最大可滚动距离
|
|
925
753
|
const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth)
|
|
926
|
-
|
|
927
|
-
// 计算需要的滚动位置,使最后一根K线紧贴最右侧
|
|
928
754
|
const targetScrollLeft = Math.min(
|
|
929
755
|
maxScrollLeft,
|
|
930
756
|
Math.max(0, lastKLineEndPx - container.clientWidth),
|
|
931
757
|
)
|
|
932
758
|
|
|
933
759
|
container.scrollLeft = Math.round(targetScrollLeft * dpr) / dpr
|
|
934
|
-
scheduleRender()
|
|
935
760
|
}
|
|
936
761
|
|
|
937
|
-
/* 缩放到指定级别(通过 Chart facade API) */
|
|
938
762
|
function applyZoomToLevel(targetLevel: number, anchorX?: number) {
|
|
939
|
-
|
|
940
|
-
if (!chart) return
|
|
941
|
-
chart.zoomToLevel(targetLevel, anchorX)
|
|
763
|
+
controller.value?.zoomToLevel(targetLevel, anchorX)
|
|
942
764
|
}
|
|
943
765
|
|
|
944
766
|
defineExpose({
|
|
@@ -948,30 +770,20 @@ defineExpose({
|
|
|
948
770
|
removeSubPane,
|
|
949
771
|
switchSubIndicator,
|
|
950
772
|
clearAllSubPanes,
|
|
951
|
-
get plugin() {
|
|
952
|
-
return chartRef.value?.plugin
|
|
953
|
-
},
|
|
954
|
-
|
|
955
|
-
// Zoom Level API(Vue SSOT)
|
|
956
773
|
zoomToLevel: applyZoomToLevel,
|
|
957
774
|
zoomIn: (anchorX?: number) => applyZoomToLevel(zoomLevel.value + 1, anchorX),
|
|
958
775
|
zoomOut: (anchorX?: number) => applyZoomToLevel(zoomLevel.value - 1, anchorX),
|
|
959
776
|
getZoomLevel: () => zoomLevel.value,
|
|
960
|
-
getZoomLevelCount: () =>
|
|
777
|
+
getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
|
|
961
778
|
})
|
|
962
779
|
|
|
963
780
|
// ==================== onMounted 拆分函数 ====================
|
|
964
781
|
|
|
965
|
-
function setupWheelHandler(
|
|
782
|
+
function setupWheelHandler(): (e: WheelEvent) => void {
|
|
966
783
|
const onWheelHandler = (e: WheelEvent) => {
|
|
967
784
|
e.preventDefault()
|
|
968
|
-
|
|
969
|
-
if (!chart) return
|
|
970
|
-
|
|
971
|
-
// 使用 Chart facade API 处理滚轮事件
|
|
972
|
-
chart.handleWheelEvent(e)
|
|
785
|
+
controller.value?.handleWheelEvent(e)
|
|
973
786
|
}
|
|
974
|
-
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
975
787
|
return onWheelHandler
|
|
976
788
|
}
|
|
977
789
|
|
|
@@ -980,74 +792,65 @@ function initChart(
|
|
|
980
792
|
canvasLayer: HTMLDivElement,
|
|
981
793
|
rightAxisLayer: HTMLDivElement,
|
|
982
794
|
xAxisCanvas: HTMLCanvasElement,
|
|
983
|
-
):
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
)
|
|
999
|
-
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
|
|
1000
812
|
}
|
|
1001
813
|
|
|
1002
|
-
function setupChartCallbacks(
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
chart.setOnPaneLayoutChange(() => {
|
|
1006
|
-
// 分隔线位置计算(需要实际像素位置,保留在回调中)
|
|
814
|
+
function setupChartCallbacks(ctrl: ChartController): void {
|
|
815
|
+
const unsubscribePaneLayout = ctrl.paneLayout.subscribe(() => {
|
|
1007
816
|
invalidateContainerRectCache()
|
|
1008
|
-
const renderers = chart.getPaneRenderers()
|
|
1009
817
|
const borderTop = containerRef.value
|
|
1010
818
|
? parseInt(getComputedStyle(containerRef.value).borderTopWidth) || 0
|
|
1011
819
|
: 0
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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 }
|
|
1018
827
|
})
|
|
1019
828
|
})
|
|
1020
829
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
store.actions.setPaneRatios({ ...ratios })
|
|
830
|
+
const unsubscribePaneRatios = ctrl.paneRatios.subscribe(() => {
|
|
831
|
+
const ratios = ctrl.paneRatios.peek()
|
|
832
|
+
paneRatios.value = { ...ratios }
|
|
1025
833
|
})
|
|
1026
834
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const vp = chart.viewport.peek()
|
|
835
|
+
const unsubscribeViewport = ctrl.viewport.subscribe(() => {
|
|
836
|
+
const vp = ctrl.viewport.peek()
|
|
1030
837
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
store.actions.setViewportDpr(vp.dpr)
|
|
838
|
+
if (viewportDpr.value !== vp.dpr) {
|
|
839
|
+
viewportDpr.value = vp.dpr
|
|
1034
840
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (store.state.viewWidth !== vp.plotWidth) {
|
|
1038
|
-
store.actions.setViewWidth(vp.plotWidth)
|
|
841
|
+
if (viewWidth.value !== vp.plotWidth) {
|
|
842
|
+
viewWidth.value = vp.plotWidth
|
|
1039
843
|
}
|
|
1040
|
-
|
|
1041
|
-
// 完整同步 zoom state 到 Vue store(Chart 是 SSOT)
|
|
1042
844
|
if (
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
845
|
+
zoomLevel.value !== vp.zoomLevel ||
|
|
846
|
+
kWidth.value !== vp.kWidth ||
|
|
847
|
+
kGap.value !== vp.kGap
|
|
1046
848
|
) {
|
|
1047
|
-
|
|
849
|
+
zoomLevel.value = vp.zoomLevel
|
|
850
|
+
kWidth.value = vp.kWidth
|
|
851
|
+
kGap.value = vp.kGap
|
|
1048
852
|
}
|
|
1049
853
|
|
|
1050
|
-
// 在 nextTick 中应用 desiredScrollLeft
|
|
1051
854
|
const desiredLeft = vp.desiredScrollLeft
|
|
1052
855
|
if (desiredLeft !== undefined && desiredLeft !== containerRef.value?.scrollLeft) {
|
|
1053
856
|
invalidateContainerRectCache()
|
|
@@ -1056,36 +859,30 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1056
859
|
if (!c) return
|
|
1057
860
|
const maxScrollLeft = Math.max(0, c.scrollWidth - c.clientWidth)
|
|
1058
861
|
const clampedScrollLeft = Math.min(Math.max(0, desiredLeft), maxScrollLeft)
|
|
1059
|
-
const dpr =
|
|
862
|
+
const dpr = vp.dpr
|
|
1060
863
|
c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
|
|
1061
864
|
})
|
|
1062
865
|
}
|
|
1063
866
|
})
|
|
1064
867
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
store.actions.bumpDataVersion()
|
|
868
|
+
const unsubscribeData = ctrl.data.subscribe(() => {
|
|
869
|
+
const data = ctrl.data.peek()
|
|
870
|
+
dataLength.value = data.length
|
|
871
|
+
dataVersion.value++
|
|
1070
872
|
})
|
|
1071
873
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const theme = chart.theme.peek()
|
|
1075
|
-
chartTheme.value = theme
|
|
874
|
+
const unsubscribeTheme = ctrl.theme.subscribe(() => {
|
|
875
|
+
chartTheme.value = ctrl.theme.peek()
|
|
1076
876
|
})
|
|
1077
877
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const instances = chart.indicators.peek()
|
|
878
|
+
const unsubscribeIndicators = ctrl.indicators.subscribe(() => {
|
|
879
|
+
const instances = ctrl.indicators.peek()
|
|
1081
880
|
|
|
1082
|
-
// 同步主图指标列表
|
|
1083
881
|
const mains = instances
|
|
1084
882
|
.filter((i): i is IndicatorInstance & { role: 'main' } => i.role === 'main')
|
|
1085
883
|
.map((i) => i.definitionId)
|
|
1086
884
|
mainActiveIndicators.value = mains
|
|
1087
885
|
|
|
1088
|
-
// 合并主图指标参数(不覆盖副图参数)
|
|
1089
886
|
const nextParams = { ...indicatorParams.value }
|
|
1090
887
|
for (const inst of instances) {
|
|
1091
888
|
if (inst.role === 'main' && inst.params && Object.keys(inst.params).length > 0) {
|
|
@@ -1093,8 +890,7 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1093
890
|
}
|
|
1094
891
|
}
|
|
1095
892
|
|
|
1096
|
-
|
|
1097
|
-
chart.updateRendererConfig('mainIndicatorLegend', {
|
|
893
|
+
ctrl.updateRendererConfig('mainIndicatorLegend', {
|
|
1098
894
|
indicators: {
|
|
1099
895
|
MA: { enabled: mains.includes('MA'), params: nextParams['MA'] || {} },
|
|
1100
896
|
BOLL: { enabled: mains.includes('BOLL'), params: nextParams['BOLL'] || {} },
|
|
@@ -1106,13 +902,10 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1106
902
|
indicatorParams.value = nextParams
|
|
1107
903
|
})
|
|
1108
904
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const unsubscribeSubPanes = chart.subPanes.subscribe(() => {
|
|
1112
|
-
const subPaneInfos = chart.subPanes.peek()
|
|
905
|
+
const unsubscribeSubPanes = ctrl.subPanes.subscribe(() => {
|
|
906
|
+
const subPaneInfos = ctrl.subPanes.peek()
|
|
1113
907
|
const signalIds = new Set(subPaneInfos.map((sp) => sp.paneId))
|
|
1114
908
|
|
|
1115
|
-
// 保留 display order,移除已删除的 pane,追加新增的
|
|
1116
909
|
const merged = subPanes.value.filter((p) => signalIds.has(p.id))
|
|
1117
910
|
const existingIds = new Set(merged.map((p) => p.id))
|
|
1118
911
|
for (const sp of subPaneInfos) {
|
|
@@ -1126,7 +919,6 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1126
919
|
}
|
|
1127
920
|
subPanes.value = merged
|
|
1128
921
|
|
|
1129
|
-
// 合并副图指标参数(不覆盖主图参数)
|
|
1130
922
|
const nextParams = { ...indicatorParams.value }
|
|
1131
923
|
for (const sp of subPaneInfos) {
|
|
1132
924
|
if (sp.params && Object.keys(sp.params).length > 0) {
|
|
@@ -1136,68 +928,56 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1136
928
|
indicatorParams.value = nextParams
|
|
1137
929
|
})
|
|
1138
930
|
|
|
1139
|
-
// 保存 unsubscribe 函数以便清理
|
|
1140
931
|
onUnmounted(() => {
|
|
1141
932
|
unsubscribeViewport()
|
|
1142
933
|
unsubscribeData()
|
|
1143
934
|
unsubscribePaneRatios()
|
|
935
|
+
unsubscribePaneLayout()
|
|
1144
936
|
unsubscribeTheme()
|
|
1145
937
|
unsubscribeIndicators()
|
|
1146
938
|
unsubscribeSubPanes()
|
|
1147
939
|
})
|
|
1148
940
|
}
|
|
1149
941
|
|
|
1150
|
-
function applyInitialSettings(
|
|
942
|
+
function applyInitialSettings(ctrl: ChartController): void {
|
|
1151
943
|
const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
|
|
1152
|
-
|
|
944
|
+
ctrl.updateSettingsFacade(initialSettings)
|
|
1153
945
|
|
|
1154
946
|
if (initialSettings.performanceTest10kKlines) {
|
|
1155
947
|
const testData = generate10kKLineData()
|
|
1156
948
|
console.time('updateData-10k')
|
|
1157
|
-
|
|
949
|
+
ctrl.updateData(testData)
|
|
1158
950
|
console.timeEnd('updateData-10k')
|
|
1159
951
|
}
|
|
1160
952
|
}
|
|
1161
953
|
|
|
1162
|
-
function setupDrawingController(
|
|
1163
|
-
drawingController.value = new DrawingInteractionController(
|
|
954
|
+
function setupDrawingController(ctrl: ChartController): void {
|
|
955
|
+
drawingController.value = new DrawingInteractionController(ctrl)
|
|
1164
956
|
drawingController.value.setCallbacks({
|
|
1165
957
|
onDrawingCreated: (drawing) => {
|
|
1166
|
-
|
|
1167
|
-
|
|
958
|
+
drawings.value = [...drawings.value, drawing]
|
|
959
|
+
selectedDrawingId.value = drawing.id
|
|
1168
960
|
},
|
|
1169
961
|
onToolChange: () => {},
|
|
1170
962
|
onDrawingSelected: (drawing) => {
|
|
1171
|
-
|
|
963
|
+
selectedDrawingId.value = drawing?.id ?? null
|
|
1172
964
|
},
|
|
1173
965
|
})
|
|
1174
966
|
}
|
|
1175
967
|
|
|
1176
|
-
function setupInteractionCallbacks(
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
interactionState.value =
|
|
1180
|
-
})
|
|
1181
|
-
|
|
1182
|
-
chart.interaction.setOnPinchZoom((delta, centerClientX) => {
|
|
1183
|
-
if (!chart) return
|
|
1184
|
-
const container = containerRef.value
|
|
1185
|
-
if (!container) return
|
|
1186
|
-
// centerClientX 是 clientX,需要转换为视口局部坐标
|
|
1187
|
-
const rect = container.getBoundingClientRect()
|
|
1188
|
-
const centerX = centerClientX - rect.left
|
|
1189
|
-
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()
|
|
1190
972
|
})
|
|
1191
973
|
|
|
1192
|
-
interactionState.value =
|
|
1193
|
-
|
|
1194
|
-
chart.resize()
|
|
974
|
+
interactionState.value = ctrl.interactionState.peek()
|
|
975
|
+
viewportDpr.value = ctrl.viewport.peek().dpr
|
|
1195
976
|
}
|
|
1196
977
|
|
|
1197
|
-
|
|
1198
|
-
function setupSemanticController(chart: Chart): void {
|
|
978
|
+
function setupSemanticController(ctrl: ChartController): void {
|
|
1199
979
|
__setDataFetcher(props.dataFetcher)
|
|
1200
|
-
semanticController.value = new SemanticChartController(
|
|
980
|
+
semanticController.value = new SemanticChartController(ctrl)
|
|
1201
981
|
|
|
1202
982
|
semanticController.value.on('config:error', (error) => {
|
|
1203
983
|
console.error('Semantic config error:', error)
|
|
@@ -1221,62 +1001,52 @@ onMounted(() => {
|
|
|
1221
1001
|
useAnchorPositioning.value = false
|
|
1222
1002
|
|
|
1223
1003
|
const container = containerRef.value
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1226
|
-
const xAxisCanvas = xAxisCanvasRef.value
|
|
1227
|
-
if (!container || !canvasLayer || !rightAxisLayer || !xAxisCanvas) return
|
|
1228
|
-
|
|
1229
|
-
// 1) 滚轮缩放:passive:false 以阻止页面滚动
|
|
1230
|
-
const onWheelHandler = setupWheelHandler(container)
|
|
1231
|
-
|
|
1232
|
-
// 2) 创建 Chart 实例并注册全部渲染器
|
|
1233
|
-
const chart = initChart(container, canvasLayer, rightAxisLayer, xAxisCanvas)
|
|
1234
|
-
chartRef.value = chart
|
|
1004
|
+
const chartMain = chartMainRef.value
|
|
1005
|
+
if (!container || !chartMain) return
|
|
1235
1006
|
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1007
|
+
// 1) 滚轮缩放处理
|
|
1008
|
+
const onWheelHandler = setupWheelHandler()
|
|
1009
|
+
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
1238
1010
|
|
|
1239
|
-
//
|
|
1240
|
-
|
|
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
|
|
1241
1017
|
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1018
|
+
// 3) 信号回调
|
|
1019
|
+
setupChartCallbacks(ctrl)
|
|
1244
1020
|
|
|
1245
|
-
//
|
|
1246
|
-
|
|
1021
|
+
// 4) 工具栏初始设置
|
|
1022
|
+
applyInitialSettings(ctrl)
|
|
1247
1023
|
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1024
|
+
// 5) 绘图交互控制器
|
|
1025
|
+
setupDrawingController(ctrl)
|
|
1250
1026
|
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1027
|
+
// 6) 交互信号桥接
|
|
1028
|
+
setupInteractionCallbacks(ctrl)
|
|
1253
1029
|
|
|
1254
|
-
//
|
|
1255
|
-
|
|
1030
|
+
// 7) 语义化配置
|
|
1031
|
+
setupSemanticController(ctrl)
|
|
1256
1032
|
})
|
|
1257
1033
|
|
|
1258
1034
|
onUnmounted(() => {
|
|
1259
|
-
const
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
| undefined
|
|
1264
|
-
const container = containerRef.value
|
|
1265
|
-
if (onWheel && container) container.removeEventListener('wheel', onWheel)
|
|
1266
|
-
chart.destroy()
|
|
1035
|
+
const ctrl = controller.value
|
|
1036
|
+
if (ctrl) {
|
|
1037
|
+
controller.value = null
|
|
1038
|
+
ctrl.dispose()
|
|
1267
1039
|
}
|
|
1268
|
-
chartRef.value = null
|
|
1269
1040
|
drawingController.value = null
|
|
1270
1041
|
})
|
|
1271
1042
|
|
|
1272
1043
|
// kWidth/kGap 由 zoomLevel 派生,不再通过 props 直接修改
|
|
1273
1044
|
// 如需程序化控制缩放,请使用 expose 的 zoomToLevel/zoomIn/zoomOut 方法
|
|
1274
1045
|
|
|
1275
|
-
// 监听 yPaddingPx 变化
|
|
1276
1046
|
watch(
|
|
1277
1047
|
() => props.yPaddingPx,
|
|
1278
1048
|
(newVal) => {
|
|
1279
|
-
|
|
1049
|
+
controller.value?.updateOptionsFacade({ yPaddingPx: newVal })
|
|
1280
1050
|
},
|
|
1281
1051
|
)
|
|
1282
1052
|
|