@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.
@@ -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 } 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,134 +585,41 @@ function addSubPane(
639
585
 
640
586
  const mergedParams = params ?? getDefaultParams(indicatorId)
641
587
 
642
- // 使用高层 Facade API 创建副图指标
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
- const index = subPanes.value.findIndex((p) => p.id === paneId)
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
- chartRef.value?.removeIndicator(pane.id)
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 chart = chartRef.value
696
- if (!chart) return
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
- // 同步Vue状态(用于UI展示)
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 chartSubPaneEntries = chartRef.value?.getSubPaneEntries() ?? []
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 chart = chartRef.value
855
- if (!chart) return
641
+ const c = controller.value
642
+ if (!c) return
856
643
 
857
- // 主图指标处理
858
644
  const mainIndicatorIds = [
859
- 'MA',
860
- 'BOLL',
861
- 'EXPMA',
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
- // 使用高层 API 添加副图指标
905
- const paneId = chart.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
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
- chart.removeIndicator(pane.id)
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 === 'BOLL' ||
966
- indicatorId === 'EXPMA' ||
967
- indicatorId === 'ENE'
681
+ indicatorId === 'MA' || indicatorId === 'BOLL' ||
682
+ indicatorId === 'EXPMA' || indicatorId === 'ENE'
968
683
  ) {
969
- chartRef.value?.updateMainIndicatorParams(
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
- chartRef.value?.updateSubPaneParams(pane.id, params)
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
- // activeIndicators computed 自动派生,无需手动同步
1026
-
1027
- const chart = chartRef.value
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 TRAILING_DRAWING_SLOTS_VAL = TRAILING_DRAWING_SLOTS
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 chart = chartRef.value
1044
- if (!container || !chart) return
742
+ const c = controller.value
743
+ if (!container || !c) return
1045
744
 
1046
- const dataLength = chart.getData()?.length ?? 0
745
+ const dataLength = c.getData()?.length ?? 0
1047
746
  if (dataLength === 0) return
1048
747
 
1049
- const dpr = chart.getCurrentDpr()
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
- const chart = chartRef.value
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: () => chartRef.value?.getZoomLevelCount() ?? 10,
777
+ getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
1092
778
  })
1093
779
 
1094
780
  // ==================== onMounted 拆分函数 ====================
1095
781
 
1096
- function setupWheelHandler(container: HTMLDivElement): (e: WheelEvent) => void {
782
+ function setupWheelHandler(): (e: WheelEvent) => void {
1097
783
  const onWheelHandler = (e: WheelEvent) => {
1098
784
  e.preventDefault()
1099
- const chart = chartRef.value
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
- ): Chart {
1115
- const chart = new Chart(
1116
- { container, canvasLayer, rightAxisLayer, xAxisCanvas },
1117
- {
1118
- yPaddingPx: props.yPaddingPx,
1119
- rightAxisWidth: props.rightAxisWidth,
1120
- bottomAxisHeight: props.bottomAxisHeight,
1121
- priceLabelWidth: props.priceLabelWidth,
1122
- minKWidth: props.minKWidth,
1123
- maxKWidth: props.maxKWidth,
1124
- panes: [{ id: 'main', ratio: 1 }],
1125
- paneGap: 0,
1126
- zoomLevels: props.zoomLevels,
1127
- initialZoomLevel: props.initialZoomLevel,
1128
- },
1129
- )
1130
- 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
1131
812
  }
1132
813
 
1133
- function setupChartCallbacks(chart: Chart): void {
1134
- // 注意:setOnViewportChange 已合并到 viewport signal 订阅者中
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
- paneSeparatorLines.value = renderers.slice(0, -1).map((renderer) => {
1144
- const pane = renderer.getPane()
1145
- return {
1146
- id: pane.id,
1147
- top: pane.top + pane.height + borderTop,
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
- // 订阅 paneRatios signal,同步到 Vue store
1153
- const unsubscribePaneRatios = chart.paneRatios.subscribe(() => {
1154
- const ratios = chart.paneRatios.peek()
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
- // 订阅 viewport signal,处理缩放、DPR、width 变化和 scrollLeft 更新
1159
- const unsubscribeViewport = chart.viewport.subscribe(() => {
1160
- const vp = chart.viewport.peek()
835
+ const unsubscribeViewport = ctrl.viewport.subscribe(() => {
836
+ const vp = ctrl.viewport.peek()
1161
837
 
1162
- // DPR 变化时同步到 store
1163
- if (store.state.viewportDpr !== vp.dpr) {
1164
- store.actions.setViewportDpr(vp.dpr)
838
+ if (viewportDpr.value !== vp.dpr) {
839
+ viewportDpr.value = vp.dpr
1165
840
  }
1166
-
1167
- // ViewWidth 变化时同步到 store
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
- store.state.zoomLevel !== vp.zoomLevel ||
1175
- store.state.kWidth !== vp.kWidth ||
1176
- store.state.kGap !== vp.kGap
845
+ zoomLevel.value !== vp.zoomLevel ||
846
+ kWidth.value !== vp.kWidth ||
847
+ kGap.value !== vp.kGap
1177
848
  ) {
1178
- store.actions.setZoomState(vp.zoomLevel, vp.kWidth, vp.kGap)
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 = chart.getCurrentDpr()
862
+ const dpr = vp.dpr
1191
863
  c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
1192
864
  })
1193
865
  }
1194
866
  })
1195
867
 
1196
- // 订阅 data signal,替换 onDataChange 回调
1197
- const unsubscribeData = chart.data.subscribe(() => {
1198
- const data = chart.data.peek()
1199
- store.actions.setDataLength(data.length)
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
- // 订阅 theme signal,同步到 CSS data-theme
1204
- const unsubscribeTheme = chart.theme.subscribe(() => {
1205
- const theme = chart.theme.peek()
1206
- chartTheme.value = theme
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(chart: Chart): void {
942
+ function applyInitialSettings(ctrl: ChartController): void {
1219
943
  const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
1220
- chart.updateSettings(initialSettings)
944
+ ctrl.updateSettingsFacade(initialSettings)
1221
945
 
1222
946
  if (initialSettings.performanceTest10kKlines) {
1223
947
  const testData = generate10kKLineData()
1224
948
  console.time('updateData-10k')
1225
- chart.updateData(testData)
949
+ ctrl.updateData(testData)
1226
950
  console.timeEnd('updateData-10k')
1227
951
  }
1228
952
  }
1229
953
 
1230
- function setupDrawingController(chart: Chart): void {
1231
- drawingController.value = new DrawingInteractionController(chart)
954
+ function setupDrawingController(ctrl: ChartController): void {
955
+ drawingController.value = new DrawingInteractionController(ctrl)
1232
956
  drawingController.value.setCallbacks({
1233
957
  onDrawingCreated: (drawing) => {
1234
- store.actions.setDrawings([...store.state.drawings, drawing])
1235
- store.actions.setSelectedDrawingId(drawing.id)
958
+ drawings.value = [...drawings.value, drawing]
959
+ selectedDrawingId.value = drawing.id
1236
960
  },
1237
961
  onToolChange: () => {},
1238
962
  onDrawingSelected: (drawing) => {
1239
- store.actions.setSelectedDrawingId(drawing?.id ?? null)
963
+ selectedDrawingId.value = drawing?.id ?? null
1240
964
  },
1241
965
  })
1242
966
  }
1243
967
 
1244
- function setupInteractionCallbacks(chart: Chart): void {
1245
- chart.interaction.setTooltipAnchorPositioning(useAnchorPositioning.value)
1246
- chart.interaction.setOnInteractionChange((snapshot) => {
1247
- interactionState.value = snapshot
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 = chart.interaction.getInteractionSnapshot()
1261
- store.actions.setViewportDpr(chart.getCurrentDpr())
1262
- chart.resize()
974
+ interactionState.value = ctrl.interactionState.peek()
975
+ viewportDpr.value = ctrl.viewport.peek().dpr
1263
976
  }
1264
977
 
1265
- /** 语义化控制器:外部配置 Chart API 的桥梁 */
1266
- function setupSemanticController(chart: Chart): void {
978
+ function setupSemanticController(ctrl: ChartController): void {
1267
979
  __setDataFetcher(props.dataFetcher)
1268
- semanticController.value = new SemanticChartController(chart)
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 canvasLayer = canvasLayerRef.value
1293
- const rightAxisLayer = rightAxisLayerRef.value
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
- // 3) 视口 / 面板布局 / 数据变更回调
1305
- setupChartCallbacks(chart)
1007
+ // 1) 滚轮缩放处理
1008
+ const onWheelHandler = setupWheelHandler()
1009
+ container.addEventListener('wheel', onWheelHandler, { passive: false })
1306
1010
 
1307
- // 4) 同步 zoom 状态(Vue SSOT → Chart)
1308
- 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
1309
1017
 
1310
- // 5) 工具栏初始设置(含性能测试数据)
1311
- applyInitialSettings(chart)
1018
+ // 3) 信号回调
1019
+ setupChartCallbacks(ctrl)
1312
1020
 
1313
- // 6) 绘图交互控制器(线段/箭头等)
1314
- setupDrawingController(chart)
1021
+ // 4) 工具栏初始设置
1022
+ applyInitialSettings(ctrl)
1315
1023
 
1316
- // 7) 十字线、捏合缩放、初始交互快照
1317
- setupInteractionCallbacks(chart)
1024
+ // 5) 绘图交互控制器
1025
+ setupDrawingController(ctrl)
1318
1026
 
1319
- // 8) 语义化配置控制器(最终驱动数据加载)
1320
- setupSemanticController(chart)
1027
+ // 6) 交互信号桥接
1028
+ setupInteractionCallbacks(ctrl)
1321
1029
 
1322
- // onUnmounted 移除 wheel 监听
1323
- ;(chart as any).__onWheel = onWheelHandler
1030
+ // 7) 语义化配置
1031
+ setupSemanticController(ctrl)
1324
1032
  })
1325
1033
 
1326
1034
  onUnmounted(() => {
1327
- const chart = chartRef.value
1328
- if (chart) {
1329
- const onWheel = (chart as any).__onWheel as
1330
- | ((this: HTMLElement, ev: WheelEvent) => any)
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
- chartRef.value?.updateOptions({ yPaddingPx: newVal })
1049
+ controller.value?.updateOptionsFacade({ yPaddingPx: newVal })
1348
1050
  },
1349
1051
  )
1350
1052