@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.
@@ -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 { Chart, type PaneSpec, type IndicatorInstance, type SubPaneInfo } from '@363045841yyt/klinechart-core/engine/chart'
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
- type DrawingToolId,
145
- } from '@363045841yyt/klinechart-core/engine/drawing'
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 chartRef = shallowRef<Chart | null>(null)
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 store = createChartStore({
206
- initialZoomLevel: props.initialZoomLevel ?? 1,
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
- zoomLevels: props.zoomLevels,
210
- rightAxisWidth: props.rightAxisWidth,
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
- chartRef.value?.scheduleDraw()
223
+ /* Controller auto-renders on state changes */
249
224
  }
250
225
 
251
226
  function handleSettingsChange(settings: Record<string, boolean | string>) {
252
- chartRef.value?.updateSettings(settings)
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
- chartRef.value?.updateData(testData)
232
+ controller.value?.updateData(testData)
259
233
  console.timeEnd('updateData-10k')
260
- store.actions.setDataLength(testData.length)
261
- store.actions.bumpDataVersion()
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
- chartRef.value?.interaction.setTooltipSize(size)
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 store.state.drawings.find((d) => d.id === id) ?? null
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 = chartRef.value?.getData()
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 chart = chartRef.value
434
- const viewport = chart?.getViewport()
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 chartRef.value?.getData() ?? []
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
- store.actions.setSelectedDrawingId(null)
467
- store.actions.bumpDrawingVersion()
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
- chartRef.value?.handlePointerEvent(e, {
441
+ controller.value?.handlePointerEvent(e, {
473
442
  onPointerDown: (event, container) => {
474
443
  if (drawingController.value?.onPointerDown(event, container)) {
475
- store.actions.setDrawings(drawingController.value.getDrawings())
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
- chartRef.value?.handlePointerEvent(e, {
461
+ controller.value?.handlePointerEvent(e, {
494
462
  onPointerMove: (event, container) => {
495
463
  if (drawingController.value?.onPointerMove(event, container)) {
496
- store.actions.setDrawings(drawingController.value.getDrawings())
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
- chartRef.value?.handlePointerEvent(e, {
473
+ controller.value?.handlePointerEvent(e, {
506
474
  onPointerUp: (event, container) => {
507
475
  if (drawingController.value?.onPointerUp(event, container)) {
508
- store.actions.setDrawings(drawingController.value.getDrawings())
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
- // pointerleave 不需要绘图控制器路由,直接调用
518
- chartRef.value?.handlePointerEvent(e)
485
+ controller.value?.handlePointerEvent(e)
519
486
  }
520
487
 
521
488
  function onRightAxisPointerDown(e: PointerEvent) {
522
- chartRef.value?.handlePointerEvent(e)
489
+ controller.value?.handlePointerEvent(e)
523
490
  }
524
491
 
525
492
  function onRightAxisPointerMove(e: PointerEvent) {
526
- chartRef.value?.handlePointerEvent(e)
493
+ controller.value?.handlePointerEvent(e)
527
494
  }
528
495
 
529
496
  function onRightAxisPointerUp(e: PointerEvent) {
530
- chartRef.value?.handlePointerEvent(e)
497
+ controller.value?.handlePointerEvent(e)
531
498
  }
532
499
 
533
500
  function onRightAxisPointerLeave(e: PointerEvent) {
534
- chartRef.value?.handlePointerEvent(e)
501
+ controller.value?.handlePointerEvent(e)
535
502
  }
536
503
 
537
504
  function onScroll() {
538
- chartRef.value?.handleScrollEvent()
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
- // 使用高层 Facade API 创建副图指标(Signal 订阅自动同步本地状态和 scheduleDraw)
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
- // 移除 paneTitle 渲染器
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
- chartRef.value?.removeIndicator(pane.id)
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 chart = chartRef.value
679
- if (!chart) return
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
- chart.enableMainIndicator(
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 chartSubPaneEntries = chartRef.value?.getSubPaneEntries() ?? []
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 chart = chartRef.value
767
- if (!chart) return
641
+ const c = controller.value
642
+ if (!c) return
768
643
 
769
- // 主图指标处理
770
644
  const mainIndicatorIds = [
771
- 'MA',
772
- 'BOLL',
773
- 'EXPMA',
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
- // 添加主图指标(Signal 订阅自动同步本地状态)
795
- chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
652
+ c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
796
653
  } else if (!active && existingIndicator) {
797
- // 移除主图指标(Signal 订阅自动同步本地状态)
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
- // 使用高层 API 添加副图指标(Signal 订阅自动同步本地状态)
815
- const paneId = chart.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
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
- chart.removeIndicator(pane.id)
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 === 'BOLL' ||
840
- indicatorId === 'EXPMA' ||
841
- indicatorId === 'ENE'
681
+ indicatorId === 'MA' || indicatorId === 'BOLL' ||
682
+ indicatorId === 'EXPMA' || indicatorId === 'ENE'
842
683
  ) {
843
- chartRef.value?.updateMainIndicatorParams(
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
- chartRef.value?.updateSubPaneParams(pane.id, params)
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
- // activeIndicators computed 自动派生,无需手动同步
895
-
896
- const chart = chartRef.value
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 TRAILING_DRAWING_SLOTS_VAL = TRAILING_DRAWING_SLOTS
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 chart = chartRef.value
913
- if (!container || !chart) return
742
+ const c = controller.value
743
+ if (!container || !c) return
914
744
 
915
- const dataLength = chart.getData()?.length ?? 0
745
+ const dataLength = c.getData()?.length ?? 0
916
746
  if (dataLength === 0) return
917
747
 
918
- const dpr = chart.getCurrentDpr()
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
- const chart = chartRef.value
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: () => chartRef.value?.getZoomLevelCount() ?? 10,
777
+ getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
961
778
  })
962
779
 
963
780
  // ==================== onMounted 拆分函数 ====================
964
781
 
965
- function setupWheelHandler(container: HTMLDivElement): (e: WheelEvent) => void {
782
+ function setupWheelHandler(): (e: WheelEvent) => void {
966
783
  const onWheelHandler = (e: WheelEvent) => {
967
784
  e.preventDefault()
968
- const chart = chartRef.value
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
- ): Chart {
984
- const chart = new Chart(
985
- { container, canvasLayer, rightAxisLayer, xAxisCanvas },
986
- {
987
- yPaddingPx: props.yPaddingPx,
988
- rightAxisWidth: props.rightAxisWidth,
989
- bottomAxisHeight: props.bottomAxisHeight,
990
- priceLabelWidth: props.priceLabelWidth,
991
- minKWidth: props.minKWidth,
992
- maxKWidth: props.maxKWidth,
993
- panes: [{ id: 'main', ratio: 1 }],
994
- paneGap: 0,
995
- zoomLevels: props.zoomLevels,
996
- initialZoomLevel: props.initialZoomLevel,
997
- },
998
- )
999
- return chart
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(chart: Chart): void {
1003
- // 注意:setOnViewportChange 已合并到 viewport signal 订阅者中
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
- paneSeparatorLines.value = renderers.slice(0, -1).map((renderer) => {
1013
- const pane = renderer.getPane()
1014
- return {
1015
- id: pane.id,
1016
- top: pane.top + pane.height + borderTop,
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
- // 订阅 paneRatios signal,同步到 Vue store
1022
- const unsubscribePaneRatios = chart.paneRatios.subscribe(() => {
1023
- const ratios = chart.paneRatios.peek()
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
- // 订阅 viewport signal,处理缩放、DPR、width 变化和 scrollLeft 更新
1028
- const unsubscribeViewport = chart.viewport.subscribe(() => {
1029
- const vp = chart.viewport.peek()
835
+ const unsubscribeViewport = ctrl.viewport.subscribe(() => {
836
+ const vp = ctrl.viewport.peek()
1030
837
 
1031
- // DPR 变化时同步到 store
1032
- if (store.state.viewportDpr !== vp.dpr) {
1033
- store.actions.setViewportDpr(vp.dpr)
838
+ if (viewportDpr.value !== vp.dpr) {
839
+ viewportDpr.value = vp.dpr
1034
840
  }
1035
-
1036
- // ViewWidth 变化时同步到 store
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
- store.state.zoomLevel !== vp.zoomLevel ||
1044
- store.state.kWidth !== vp.kWidth ||
1045
- store.state.kGap !== vp.kGap
845
+ zoomLevel.value !== vp.zoomLevel ||
846
+ kWidth.value !== vp.kWidth ||
847
+ kGap.value !== vp.kGap
1046
848
  ) {
1047
- store.actions.setZoomState(vp.zoomLevel, vp.kWidth, vp.kGap)
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 = chart.getCurrentDpr()
862
+ const dpr = vp.dpr
1060
863
  c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
1061
864
  })
1062
865
  }
1063
866
  })
1064
867
 
1065
- // 订阅 data signal,替换 onDataChange 回调
1066
- const unsubscribeData = chart.data.subscribe(() => {
1067
- const data = chart.data.peek()
1068
- store.actions.setDataLength(data.length)
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
- // 订阅 theme signal,同步到 CSS data-theme
1073
- const unsubscribeTheme = chart.theme.subscribe(() => {
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
- // 订阅 indicators signal,派生 Vue 本地状态(SSOT: Chart 引擎)
1079
- const unsubscribeIndicators = chart.indicators.subscribe(() => {
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
- // 订阅 subPanes signal,派生 Vue 本地状态
1110
- // 注意:保持当前显示顺序(reorder UI 层私有状态),仅同步新增/删除
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(chart: Chart): void {
942
+ function applyInitialSettings(ctrl: ChartController): void {
1151
943
  const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
1152
- chart.updateSettings(initialSettings)
944
+ ctrl.updateSettingsFacade(initialSettings)
1153
945
 
1154
946
  if (initialSettings.performanceTest10kKlines) {
1155
947
  const testData = generate10kKLineData()
1156
948
  console.time('updateData-10k')
1157
- chart.updateData(testData)
949
+ ctrl.updateData(testData)
1158
950
  console.timeEnd('updateData-10k')
1159
951
  }
1160
952
  }
1161
953
 
1162
- function setupDrawingController(chart: Chart): void {
1163
- drawingController.value = new DrawingInteractionController(chart)
954
+ function setupDrawingController(ctrl: ChartController): void {
955
+ drawingController.value = new DrawingInteractionController(ctrl)
1164
956
  drawingController.value.setCallbacks({
1165
957
  onDrawingCreated: (drawing) => {
1166
- store.actions.setDrawings([...store.state.drawings, drawing])
1167
- store.actions.setSelectedDrawingId(drawing.id)
958
+ drawings.value = [...drawings.value, drawing]
959
+ selectedDrawingId.value = drawing.id
1168
960
  },
1169
961
  onToolChange: () => {},
1170
962
  onDrawingSelected: (drawing) => {
1171
- store.actions.setSelectedDrawingId(drawing?.id ?? null)
963
+ selectedDrawingId.value = drawing?.id ?? null
1172
964
  },
1173
965
  })
1174
966
  }
1175
967
 
1176
- function setupInteractionCallbacks(chart: Chart): void {
1177
- chart.interaction.setTooltipAnchorPositioning(useAnchorPositioning.value)
1178
- chart.interaction.setOnInteractionChange((snapshot) => {
1179
- interactionState.value = snapshot
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 = chart.interaction.getInteractionSnapshot()
1193
- store.actions.setViewportDpr(chart.getCurrentDpr())
1194
- chart.resize()
974
+ interactionState.value = ctrl.interactionState.peek()
975
+ viewportDpr.value = ctrl.viewport.peek().dpr
1195
976
  }
1196
977
 
1197
- /** 语义化控制器:外部配置 Chart API 的桥梁 */
1198
- function setupSemanticController(chart: Chart): void {
978
+ function setupSemanticController(ctrl: ChartController): void {
1199
979
  __setDataFetcher(props.dataFetcher)
1200
- semanticController.value = new SemanticChartController(chart)
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 canvasLayer = canvasLayerRef.value
1225
- const rightAxisLayer = rightAxisLayerRef.value
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
- // 3) 视口 / 面板布局 / 数据变更回调
1237
- setupChartCallbacks(chart)
1007
+ // 1) 滚轮缩放处理
1008
+ const onWheelHandler = setupWheelHandler()
1009
+ container.addEventListener('wheel', onWheelHandler, { passive: false })
1238
1010
 
1239
- // 4) 同步 zoom 状态(Vue SSOT → Chart)
1240
- chart.applyRenderState(store.state.kWidth, store.state.kGap, store.state.zoomLevel)
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
- // 5) 工具栏初始设置(含性能测试数据)
1243
- applyInitialSettings(chart)
1018
+ // 3) 信号回调
1019
+ setupChartCallbacks(ctrl)
1244
1020
 
1245
- // 6) 绘图交互控制器(线段/箭头等)
1246
- setupDrawingController(chart)
1021
+ // 4) 工具栏初始设置
1022
+ applyInitialSettings(ctrl)
1247
1023
 
1248
- // 7) 十字线、捏合缩放、初始交互快照
1249
- setupInteractionCallbacks(chart)
1024
+ // 5) 绘图交互控制器
1025
+ setupDrawingController(ctrl)
1250
1026
 
1251
- // 8) 语义化配置控制器(最终驱动数据加载)
1252
- setupSemanticController(chart)
1027
+ // 6) 交互信号桥接
1028
+ setupInteractionCallbacks(ctrl)
1253
1029
 
1254
- // onUnmounted 移除 wheel 监听
1255
- ;(chart as any).__onWheel = onWheelHandler
1030
+ // 7) 语义化配置
1031
+ setupSemanticController(ctrl)
1256
1032
  })
1257
1033
 
1258
1034
  onUnmounted(() => {
1259
- const chart = chartRef.value
1260
- if (chart) {
1261
- const onWheel = (chart as any).__onWheel as
1262
- | ((this: HTMLElement, ev: WheelEvent) => any)
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
- chartRef.value?.updateOptions({ yPaddingPx: newVal })
1049
+ controller.value?.updateOptionsFacade({ yPaddingPx: newVal })
1280
1050
  },
1281
1051
  )
1282
1052