@363045841yyt/klinechart 0.7.5 → 0.7.7

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.
Files changed (43) hide show
  1. package/README.md +4 -4
  2. package/dist/components/ChartSettingsDialog.vue.d.ts +14 -0
  3. package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -0
  4. package/dist/components/ColorPresetPanel.vue.d.ts +12 -0
  5. package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -0
  6. package/dist/components/DrawingStyleToolbar.vue.d.ts +1 -15
  7. package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
  8. package/dist/components/IndicatorParams.vue.d.ts.map +1 -1
  9. package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
  10. package/dist/components/KLineChart.vue.d.ts +7 -6
  11. package/dist/components/KLineChart.vue.d.ts.map +1 -1
  12. package/dist/components/KLineTooltip.vue.d.ts +9 -2
  13. package/dist/components/KLineTooltip.vue.d.ts.map +1 -1
  14. package/dist/components/LeftToolbar.vue.d.ts +4 -3
  15. package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
  16. package/dist/components/MarkerTooltip.vue.d.ts +1 -12
  17. package/dist/components/MarkerTooltip.vue.d.ts.map +1 -1
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/composables/useFullscreenTeleportTarget.d.ts.map +1 -1
  21. package/dist/index.cjs +2 -2
  22. package/dist/index.css +1 -0
  23. package/dist/index.d.cts +5 -5
  24. package/dist/index.d.ts +5 -5
  25. package/dist/index.js +1009 -905
  26. package/dist/version.d.ts +1 -1
  27. package/dist/web-component.d.ts +18 -0
  28. package/dist/web-component.d.ts.map +1 -0
  29. package/package.json +10 -2
  30. package/src/__tests__/_mockController.ts +11 -1
  31. package/src/components/ChartSettingsDialog.vue +624 -0
  32. package/src/components/ColorPresetPanel.vue +289 -0
  33. package/src/components/DrawingStyleToolbar.vue +12 -25
  34. package/src/components/IndicatorParams.vue +58 -57
  35. package/src/components/IndicatorSelector.vue +91 -88
  36. package/src/components/KLineChart.vue +267 -442
  37. package/src/components/KLineTooltip.vue +19 -13
  38. package/src/components/LeftToolbar.vue +35 -393
  39. package/src/components/MarkerTooltip.vue +5 -16
  40. package/src/components/index.ts +1 -0
  41. package/src/composables/useFullscreenTeleportTarget.ts +0 -2
  42. package/src/web-component.ts +14 -0
  43. package/dist/klinechart.css +0 -2
@@ -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" :style="themeCssVars">
3
3
  <div
4
4
  class="chart-stage"
5
5
  :class="{
@@ -75,6 +75,8 @@
75
75
  :set-el="setTooltipEl"
76
76
  :use-anchor="useAnchorPositioning"
77
77
  :anchor-placement="tooltipAnchorPlacement"
78
+ :up-color="tooltipColors.upColor"
79
+ :down-color="tooltipColors.downColor"
78
80
  />
79
81
  <MarkerTooltip
80
82
  v-if="hoveredMarker || hoveredCustomMarker"
@@ -118,31 +120,27 @@ import KLineTooltip from './KLineTooltip.vue'
118
120
  import MarkerTooltip from './MarkerTooltip.vue'
119
121
  import IndicatorSelector from './IndicatorSelector.vue'
120
122
  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'
123
+ import { provideFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
131
124
  import {
125
+ createChartController,
126
+ type ChartController,
127
+ type PaneSpec,
128
+ type IndicatorInstance,
129
+ type SubIndicatorType,
130
+ type InteractionSnapshot,
131
+ type DrawingToolId,
132
+ type KLineData,
133
+ zoomLevelToKWidth,
134
+ kGapFromKWidth,
135
+ getPhysicalKLineConfig,
132
136
  SUB_PANE_INDICATOR_CONFIGS,
133
137
  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
138
  DrawingInteractionController,
144
- type DrawingToolId,
145
- } from '@363045841yyt/klinechart-core/engine/drawing'
139
+ } from '@363045841yyt/klinechart-core/controllers'
140
+ import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
141
+ import type { ChartSettings } from '@363045841yyt/klinechart-core/config'
142
+ import { resolveThemeColors, themeToCssVars, lightTheme, darkTheme, type ColorPresetSettings } from '@363045841yyt/klinechart-core'
143
+ import LeftToolbar from './LeftToolbar.vue'
146
144
 
147
145
  const props = withDefaults(
148
146
  defineProps<{
@@ -185,84 +183,110 @@ const props = withDefaults(
185
183
  const emit = defineEmits<{
186
184
  (e: 'zoomLevelChange', level: number, kWidth: number): void
187
185
  (e: 'toggleFullscreen'): void
186
+ (e: 'themeChange', theme: 'light' | 'dark'): void
188
187
  }>()
189
188
 
190
- const xAxisCanvasRef = ref<HTMLCanvasElement | null>(null)
191
- const canvasLayerRef = ref<HTMLDivElement | null>(null)
192
- const rightAxisLayerRef = ref<HTMLDivElement | null>(null)
193
189
  const containerRef = ref<HTMLDivElement | null>(null)
194
190
  const chartMainRef = ref<HTMLDivElement | null>(null)
191
+ const chartWrapperRef = ref<HTMLDivElement | null>(null)
195
192
  const tooltipLayerRef = ref<HTMLDivElement | null>(null)
196
193
  const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
194
+ provideFullscreenTeleportTarget(chartWrapperRef)
197
195
 
198
- /* ========== 十字线(鼠标悬停位置) ========== */
199
- const chartRef = shallowRef<Chart | null>(null)
196
+ /* ========== 图表控制器 ========== */
197
+ const controller = shallowRef<ChartController | null>(null)
200
198
 
201
199
  /* ========== 语义化控制器 ========== */
202
200
  const semanticController = shallowRef<SemanticChartController | null>(null)
203
201
 
204
- /* ========== ChartStore(响应式状态中心) ========== */
205
- const store = createChartStore({
206
- initialZoomLevel: props.initialZoomLevel ?? 1,
202
+ /* ========== 本地响应式状态(信号驱动,取代 ChartStore ========== */
203
+ const dataLength = ref(0)
204
+ const dataVersion = ref(0)
205
+ const viewportDpr = ref(1)
206
+ const zoomLevel = ref(props.initialZoomLevel ?? 1)
207
+ const kWidth = ref(0)
208
+ const kGap = ref(1)
209
+ const viewWidth = ref(0)
210
+ const paneRatios = ref<Record<string, number>>({})
211
+ const selectedDrawingId = ref<string | null>(null)
212
+ const drawings = ref<DrawingObject[]>([])
213
+
214
+ // 初始化 kWidth / kGap(与 Chart 引擎 zoom→物理值 转换一致)
215
+ const initZoom = zoomLevel.value
216
+ kWidth.value = zoomLevelToKWidth(initZoom, {
207
217
  minKWidth: props.minKWidth,
208
218
  maxKWidth: props.maxKWidth,
209
- zoomLevels: props.zoomLevels,
210
- rightAxisWidth: props.rightAxisWidth,
211
- priceLabelWidth: props.priceLabelWidth,
219
+ zoomLevelCount: props.zoomLevels,
220
+ dpr: viewportDpr.value,
212
221
  })
222
+ kGap.value = kGapFromKWidth(kWidth.value, viewportDpr.value)
213
223
 
214
224
  /* ========== 主题状态 ========== */
215
225
  const chartTheme = ref<'light' | 'dark'>('light')
216
226
 
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
- )
227
+ const chartSettings = ref<ChartSettings>({})
228
+
229
+ const tooltipColors = computed(() => {
230
+ const isAsiaMarket = chartSettings.value.isAsiaMarket ?? false
231
+ const colors = resolveThemeColors(chartTheme.value, isAsiaMarket as boolean | undefined)
232
+ return {
233
+ upColor: colors.candleUpBody,
234
+ downColor: colors.candleDownBody,
235
+ }
236
+ })
237
+
238
+ const themeCssVars = computed(() => {
239
+ const theme = chartTheme.value === 'dark' ? darkTheme : lightTheme
240
+ const overrides = (chartSettings.value.colorPresetSettings as ColorPresetSettings | undefined)?.[chartTheme.value]
241
+ if (overrides && Object.keys(overrides).length > 0) {
242
+ return themeToCssVars({ ...theme, colors: { ...theme.colors, ...overrides } })
243
+ }
244
+ return themeToCssVars(theme)
245
+ })
246
+
247
+ /* ========== 主题切换(支持 light / dark / auto 跟随系统) ========== */
248
+ let autoThemeMediaQuery: MediaQueryList | null = null
249
+
250
+ function onSystemThemeChange(e: MediaQueryListEvent) {
251
+ controller.value?.setTheme(e.matches ? 'dark' : 'light')
252
+ }
253
+
254
+ function applyThemeFromSettings(ctrl: ChartController | null, themeSetting: string | undefined) {
255
+ if (!ctrl || !themeSetting) return
236
256
 
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)
257
+ if (themeSetting === 'auto') {
258
+ const mq = window.matchMedia('(prefers-color-scheme: dark)')
259
+ ctrl.setTheme(mq.matches ? 'dark' : 'light')
260
+ if (autoThemeMediaQuery !== mq) {
261
+ autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
262
+ autoThemeMediaQuery = mq
263
+ mq.addEventListener('change', onSystemThemeChange)
264
+ }
265
+ } else {
266
+ autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
267
+ autoThemeMediaQuery = null
268
+ ctrl.setTheme(themeSetting as 'light' | 'dark')
269
+ }
270
+ }
246
271
 
247
272
  function scheduleRender() {
248
- chartRef.value?.scheduleDraw()
273
+ /* Controller auto-renders on state changes */
249
274
  }
250
275
 
251
- function handleSettingsChange(settings: Record<string, boolean | string>) {
252
- chartRef.value?.updateSettings(settings)
276
+ function handleSettingsChange(settings: ChartSettings) {
277
+ chartSettings.value = settings
278
+ applyThemeFromSettings(controller.value, settings.theme as string)
279
+ controller.value?.updateSettingsFacade(settings)
253
280
 
254
- // 万条K线性能测试
255
281
  if (settings.performanceTest10kKlines) {
256
282
  const testData = generate10kKLineData()
257
283
  console.time('updateData-10k')
258
- chartRef.value?.updateData(testData)
284
+ controller.value?.updateData(testData)
259
285
  console.timeEnd('updateData-10k')
260
- store.actions.setDataLength(testData.length)
261
- store.actions.bumpDataVersion()
286
+ dataLength.value = testData.length
287
+ dataVersion.value++
262
288
  } else {
263
- // 如果关闭性能测试,恢复原始数据
264
- // 通过重新应用语义化配置来恢复
265
- if (semanticController.value && chartRef.value?.getData()?.length === 10000) {
289
+ if (semanticController.value && controller.value?.getData()?.length === 10000) {
266
290
  semanticController.value.applyConfig(props.semanticConfig)
267
291
  }
268
292
  }
@@ -318,7 +342,7 @@ function setTooltipEl(el: HTMLDivElement | null) {
318
342
  nextTick(() => {
319
343
  if (!el.isConnected) return
320
344
  const size = measureTooltipSize(el, 180, 80)
321
- chartRef.value?.interaction.setTooltipSize(size)
345
+ controller.value?.setTooltipSize(size)
322
346
  })
323
347
  }
324
348
 
@@ -368,7 +392,7 @@ const drawingController = shallowRef<DrawingInteractionController | null>(null)
368
392
  const selectedDrawing = computed(() => {
369
393
  const id = selectedDrawingId.value
370
394
  if (!id) return null
371
- return store.state.drawings.find((d) => d.id === id) ?? null
395
+ return drawings.value.find((d) => d.id === id) ?? null
372
396
  })
373
397
  const paneSeparatorLines = ref<Array<{ id: string; top: number }>>([])
374
398
  const markerTooltipSize = ref({ width: 220, height: 120 })
@@ -403,8 +427,8 @@ const containerCursor = computed(() => {
403
427
  const hovered = computed(() => {
404
428
  const idx = interactionState.value.hoveredIndex
405
429
  if (typeof idx !== 'number') return null
406
- void dataVersion.value // 建立响应式依赖
407
- const data = chartRef.value?.getData()
430
+ void dataVersion.value
431
+ const data = controller.value?.getData()
408
432
  if (data && idx >= 0 && idx < data.length) {
409
433
  return data[idx]
410
434
  }
@@ -430,8 +454,8 @@ const markerTooltipAnchorStyle = computed(() => ({
430
454
  }))
431
455
  const tooltipAnchorPlacement = computed(() => interactionState.value.tooltipAnchorPlacement)
432
456
  const markerTooltipAnchorPlacement = computed<'right-bottom' | 'left-bottom'>(() => {
433
- const chart = chartRef.value
434
- const viewport = chart?.getViewport()
457
+ const c = controller.value
458
+ const viewport = c?.viewport.peek()
435
459
  const container = containerRef.value
436
460
  const plotWidth = viewport?.plotWidth ?? (container ? container.clientWidth : 0)
437
461
  const padding = 12
@@ -441,10 +465,9 @@ const markerTooltipAnchorPlacement = computed<'right-bottom' | 'left-bottom'>(()
441
465
  return wouldOverflowRight ? 'left-bottom' : 'right-bottom'
442
466
  })
443
467
 
444
- // 获取当前图表数据
445
468
  const chartData = computed(() => {
446
- void dataVersion.value // 建立响应式依赖,确保数据变化时重新求值
447
- return chartRef.value?.getData() ?? []
469
+ void dataVersion.value
470
+ return controller.value?.getData() ?? []
448
471
  })
449
472
 
450
473
  // 通知数据变化(在数据更新后调用)
@@ -456,24 +479,21 @@ function onUpdateDrawingStyle(style: Partial<DrawingStyle>) {
456
479
  const d = selectedDrawing.value
457
480
  if (!d || !drawingController.value) return
458
481
  drawingController.value.updateDrawingStyle(d.id, style)
459
- store.actions.bumpDrawingVersion()
460
482
  }
461
483
 
462
484
  function onDeleteDrawing() {
463
485
  const d = selectedDrawing.value
464
486
  if (!d || !drawingController.value) return
465
487
  drawingController.value.removeDrawing(d.id)
466
- store.actions.setSelectedDrawingId(null)
467
- store.actions.bumpDrawingVersion()
468
- store.actions.setDrawings(drawingController.value.getDrawings())
488
+ selectedDrawingId.value = null
489
+ drawings.value = drawingController.value.getDrawings()
469
490
  }
470
491
 
471
492
  function onPointerDown(e: PointerEvent) {
472
- chartRef.value?.handlePointerEvent(e, {
493
+ controller.value?.handlePointerEvent(e, {
473
494
  onPointerDown: (event, container) => {
474
495
  if (drawingController.value?.onPointerDown(event, container)) {
475
- store.actions.setDrawings(drawingController.value.getDrawings())
476
- store.actions.bumpDrawingVersion()
496
+ drawings.value = drawingController.value.getDrawings()
477
497
  return true
478
498
  }
479
499
  return false
@@ -490,10 +510,10 @@ function onPointerMove(e: PointerEvent) {
490
510
  y: e.clientY - rect.top,
491
511
  }
492
512
  }
493
- chartRef.value?.handlePointerEvent(e, {
513
+ controller.value?.handlePointerEvent(e, {
494
514
  onPointerMove: (event, container) => {
495
515
  if (drawingController.value?.onPointerMove(event, container)) {
496
- store.actions.setDrawings(drawingController.value.getDrawings())
516
+ drawings.value = drawingController.value.getDrawings()
497
517
  return true
498
518
  }
499
519
  return false
@@ -502,10 +522,10 @@ function onPointerMove(e: PointerEvent) {
502
522
  }
503
523
 
504
524
  function onPointerUp(e: PointerEvent) {
505
- chartRef.value?.handlePointerEvent(e, {
525
+ controller.value?.handlePointerEvent(e, {
506
526
  onPointerUp: (event, container) => {
507
527
  if (drawingController.value?.onPointerUp(event, container)) {
508
- store.actions.setDrawings(drawingController.value.getDrawings())
528
+ drawings.value = drawingController.value.getDrawings()
509
529
  return true
510
530
  }
511
531
  return false
@@ -514,28 +534,27 @@ function onPointerUp(e: PointerEvent) {
514
534
  }
515
535
 
516
536
  function onPointerLeave(e: PointerEvent) {
517
- // pointerleave 不需要绘图控制器路由,直接调用
518
- chartRef.value?.handlePointerEvent(e)
537
+ controller.value?.handlePointerEvent(e)
519
538
  }
520
539
 
521
540
  function onRightAxisPointerDown(e: PointerEvent) {
522
- chartRef.value?.handlePointerEvent(e)
541
+ controller.value?.handlePointerEvent(e)
523
542
  }
524
543
 
525
544
  function onRightAxisPointerMove(e: PointerEvent) {
526
- chartRef.value?.handlePointerEvent(e)
545
+ controller.value?.handlePointerEvent(e)
527
546
  }
528
547
 
529
548
  function onRightAxisPointerUp(e: PointerEvent) {
530
- chartRef.value?.handlePointerEvent(e)
549
+ controller.value?.handlePointerEvent(e)
531
550
  }
532
551
 
533
552
  function onRightAxisPointerLeave(e: PointerEvent) {
534
- chartRef.value?.handlePointerEvent(e)
553
+ controller.value?.handlePointerEvent(e)
535
554
  }
536
555
 
537
556
  function onScroll() {
538
- chartRef.value?.handleScrollEvent()
557
+ controller.value?.handleScrollEvent()
539
558
  }
540
559
 
541
560
  // 主图指标显式状态(副图指标从 subPanes 派生)
@@ -607,27 +626,6 @@ function generatePaneId(indicatorId: SubIndicatorType): string {
607
626
  return `${indicatorId}_${count}`
608
627
  }
609
628
 
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
629
  // 添加副图(使用 Chart API)
632
630
  function addSubPane(
633
631
  indicatorId: SubIndicatorType = 'VOLUME',
@@ -639,68 +637,41 @@ function addSubPane(
639
637
 
640
638
  const mergedParams = params ?? getDefaultParams(indicatorId)
641
639
 
642
- // 使用高层 Facade API 创建副图指标(Signal 订阅自动同步本地状态和 scheduleDraw)
643
- const paneId = chartRef.value?.addIndicator(indicatorId, 'sub', mergedParams)
640
+ const paneId = controller.value?.addIndicator(indicatorId, 'sub', mergedParams)
644
641
  if (!paneId) return false
645
-
646
- // 创建 paneTitle 渲染器(UI 层职责)
647
- mountSubPaneTitle(paneId, indicatorId)
648
-
649
642
  return true
650
643
  }
651
644
 
652
- // 移除副图(使用高层 Facade API)
653
645
  function removeSubPane(paneId: string): void {
654
- // 移除 paneTitle 渲染器
655
- unmountSubPaneTitle(paneId)
656
-
657
- // 使用高层 Facade API 移除指标(Signal 订阅自动同步本地状态)
658
- chartRef.value?.removeIndicator(paneId)
646
+ controller.value?.removeIndicator(paneId)
659
647
  }
660
648
 
661
- // 清除所有副图(使用高层 Facade API)
662
649
  function clearAllSubPanes(): void {
663
- // 使用高层 Facade API 逐个移除
664
650
  for (const pane of subPanes.value) {
665
- chartRef.value?.removeIndicator(pane.id)
666
- unmountSubPaneTitle(pane.id)
651
+ controller.value?.removeIndicator(pane.id)
667
652
  }
668
-
669
- // 清空本地状态(Signal 订阅自动同步 subPanes,只需要清理 UI 层状态)
670
653
  subPaneCounters.clear()
671
- paneTitleRendererNames.clear()
672
654
  }
673
655
 
674
- // 从语义化配置初始化指标状态(单向数据流:config → chart)
675
- // Signal 订阅会自动同步本地状态,此处只需调用 Chart API
676
656
  function initIndicatorsFromConfig(): void {
677
657
  const config = props.semanticConfig
678
- const chart = chartRef.value
679
- if (!chart) return
658
+ const c = controller.value
659
+ if (!c) return
680
660
 
681
661
  const mainIndicators = config.indicators?.main
682
662
  if (mainIndicators) {
683
663
  for (const indicator of mainIndicators) {
684
664
  if (indicator.enabled) {
685
- chart.enableMainIndicator(
686
- indicator.type,
687
- indicator.params as Record<string, number | boolean | string>,
688
- )
665
+ c.addIndicator(indicator.type, 'main', indicator.params as Record<string, number | boolean | string>)
689
666
  }
690
667
  }
691
668
  }
692
669
  }
693
670
 
694
- // 从 Chart 同步副图状态到本地(语义化配置后调用)
695
671
  function syncSubPanesFromChart(): void {
696
- const chartSubPaneEntries = chartRef.value?.getSubPaneEntries() ?? []
697
-
698
- paneTitleRendererNames.clear()
699
-
700
- for (const entry of chartSubPaneEntries) {
672
+ const entries = controller.value?.subPanes.peek() ?? []
673
+ for (const entry of entries) {
701
674
  const { paneId, indicatorId, params } = entry
702
-
703
- // 恢复计数器状态
704
675
  const match = paneId.match(/^(.+)_(\d+)$/)
705
676
  if (match) {
706
677
  const [, indicator, countStr] = match
@@ -710,150 +681,67 @@ function syncSubPanesFromChart(): void {
710
681
  subPaneCounters.set(indicator as SubIndicatorType, count + 1)
711
682
  }
712
683
  }
713
-
714
- // 创建 paneTitle 渲染器
715
- mountSubPaneTitle(paneId, indicatorId)
716
684
  }
717
685
  }
718
686
 
719
- // 切换副图指标(使用 Chart API)
720
687
  function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
721
688
  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)
689
+ controller.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
731
690
  }
732
691
 
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
692
  function handleIndicatorToggle(indicatorId: string, active: boolean) {
766
- const chart = chartRef.value
767
- if (!chart) return
693
+ const c = controller.value
694
+ if (!c) return
768
695
 
769
- // 主图指标处理
770
696
  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',
697
+ 'MA', 'BOLL', 'EXPMA', 'ENE', 'WMA', 'DEMA', 'TEMA', 'HMA',
698
+ 'KAMA', 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
699
+ 'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
789
700
  ]
790
701
  if (mainIndicatorIds.includes(indicatorId)) {
791
702
  const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
792
-
793
703
  if (active && !existingIndicator) {
794
- // 添加主图指标(Signal 订阅自动同步本地状态)
795
- chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
704
+ c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
796
705
  } else if (!active && existingIndicator) {
797
- // 移除主图指标(Signal 订阅自动同步本地状态)
798
- const instanceId = indicatorId.toUpperCase()
799
- chart.removeIndicator(instanceId)
706
+ c.removeIndicator(indicatorId.toUpperCase())
800
707
  }
801
708
  return
802
709
  }
803
710
 
804
- // 副图指标处理
805
711
  if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
806
712
  if (active) {
807
- // 如果已存在同类型指标 pane,跳过
808
713
  const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
809
714
  if (existingPane) return
810
-
811
- // 副图数量上限检查
812
715
  if (subPanes.value.length >= maxSubPanes) return
813
716
 
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
- // 添加失败(可能达到上限),替换最后一个
717
+ const paneId = c.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
718
+ if (!paneId && subPanes.value.length > 0) {
820
719
  const lastPane = subPanes.value[subPanes.value.length - 1]
821
720
  switchSubIndicator(lastPane.id, indicatorId as SubIndicatorType)
822
721
  }
823
722
  } else {
824
- // 找到并移除该指标的所有 pane
825
723
  const panesToRemove = subPanes.value.filter((p) => p.indicatorId === indicatorId)
826
724
  panesToRemove.forEach((pane) => {
827
- chart.removeIndicator(pane.id)
828
- unmountSubPaneTitle(pane.id)
725
+ c.removeIndicator(pane.id)
829
726
  })
830
727
  }
831
728
  }
832
729
  }
833
730
 
834
- // 指标参数更新处理
835
731
  function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
836
- // 主图指标参数更新 - 使用Chart API(Signal 订阅自动同步本地状态和 scheduleDraw)
837
732
  if (
838
- indicatorId === 'MA' ||
839
- indicatorId === 'BOLL' ||
840
- indicatorId === 'EXPMA' ||
841
- indicatorId === 'ENE'
733
+ indicatorId === 'MA' || indicatorId === 'BOLL' ||
734
+ indicatorId === 'EXPMA' || indicatorId === 'ENE'
842
735
  ) {
843
- chartRef.value?.updateMainIndicatorParams(
844
- indicatorId,
845
- params as Record<string, number | boolean | string>,
846
- )
736
+ controller.value?.updateIndicatorParams(indicatorId, params)
847
737
  return
848
738
  }
849
-
850
739
  if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
851
740
  subPanes.value
852
741
  .filter((p) => p.indicatorId === indicatorId)
853
742
  .forEach((pane) => {
854
- chartRef.value?.updateSubPaneParams(pane.id, params)
743
+ controller.value?.updateIndicatorParams(pane.id, params)
855
744
  })
856
- return
857
745
  }
858
746
  }
859
747
 
@@ -891,54 +779,47 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
891
779
 
892
780
  subPanes.value = nextSubPanes
893
781
 
894
- // activeIndicators computed 自动派生,无需手动同步
895
-
896
- const chart = chartRef.value
897
- if (!chart) return
898
- chart.updatePaneLayout(buildPaneLayoutIntent())
782
+ const c = controller.value
783
+ if (!c) return
784
+ c.updatePaneLayout(buildPaneLayoutIntent())
899
785
  }
900
786
 
901
787
  /* 计算总宽度:从 Vue 响应式状态读取,zoom 变化时自动重算 */
902
788
  const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
903
789
 
904
- const TRAILING_DRAWING_SLOTS_VAL = TRAILING_DRAWING_SLOTS
905
-
906
- const totalWidth = store.computed.totalWidth
907
-
908
- // 缩放由 Chart 回调驱动 scrollLeft 与渲染时序。
790
+ const totalWidth = computed(() => {
791
+ void dataVersion.value
792
+ void viewWidth.value
793
+ void kWidth.value
794
+ void kGap.value
795
+ void viewportDpr.value
796
+ return controller.value?.getContentWidth() ?? 0
797
+ })
909
798
 
910
799
  function scrollToRight() {
911
800
  const container = containerRef.value
912
- const chart = chartRef.value
913
- if (!container || !chart) return
801
+ const c = controller.value
802
+ if (!container || !c) return
914
803
 
915
- const dataLength = chart.getData()?.length ?? 0
804
+ const dataLength = c.getData()?.length ?? 0
916
805
  if (dataLength === 0) return
917
806
 
918
- const dpr = chart.getCurrentDpr()
807
+ const vp = c.viewport.peek()
808
+ const dpr = vp.dpr
919
809
  const { unitPx, startXPx } = getPhysicalKLineConfig(kWidth.value, kGap.value, dpr)
920
810
 
921
- // 计算最后一根K线的结束位置(不含 TRAILING_DRAWING_SLOTS)
922
811
  const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
923
-
924
- // 计算最大可滚动距离
925
812
  const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth)
926
-
927
- // 计算需要的滚动位置,使最后一根K线紧贴最右侧
928
813
  const targetScrollLeft = Math.min(
929
814
  maxScrollLeft,
930
815
  Math.max(0, lastKLineEndPx - container.clientWidth),
931
816
  )
932
817
 
933
818
  container.scrollLeft = Math.round(targetScrollLeft * dpr) / dpr
934
- scheduleRender()
935
819
  }
936
820
 
937
- /* 缩放到指定级别(通过 Chart facade API) */
938
821
  function applyZoomToLevel(targetLevel: number, anchorX?: number) {
939
- const chart = chartRef.value
940
- if (!chart) return
941
- chart.zoomToLevel(targetLevel, anchorX)
822
+ controller.value?.zoomToLevel(targetLevel, anchorX)
942
823
  }
943
824
 
944
825
  defineExpose({
@@ -948,30 +829,20 @@ defineExpose({
948
829
  removeSubPane,
949
830
  switchSubIndicator,
950
831
  clearAllSubPanes,
951
- get plugin() {
952
- return chartRef.value?.plugin
953
- },
954
-
955
- // Zoom Level API(Vue SSOT)
956
832
  zoomToLevel: applyZoomToLevel,
957
833
  zoomIn: (anchorX?: number) => applyZoomToLevel(zoomLevel.value + 1, anchorX),
958
834
  zoomOut: (anchorX?: number) => applyZoomToLevel(zoomLevel.value - 1, anchorX),
959
835
  getZoomLevel: () => zoomLevel.value,
960
- getZoomLevelCount: () => chartRef.value?.getZoomLevelCount() ?? 10,
836
+ getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
961
837
  })
962
838
 
963
839
  // ==================== onMounted 拆分函数 ====================
964
840
 
965
- function setupWheelHandler(container: HTMLDivElement): (e: WheelEvent) => void {
841
+ function setupWheelHandler(): (e: WheelEvent) => void {
966
842
  const onWheelHandler = (e: WheelEvent) => {
967
843
  e.preventDefault()
968
- const chart = chartRef.value
969
- if (!chart) return
970
-
971
- // 使用 Chart facade API 处理滚轮事件
972
- chart.handleWheelEvent(e)
844
+ controller.value?.handleWheelEvent(e)
973
845
  }
974
- container.addEventListener('wheel', onWheelHandler, { passive: false })
975
846
  return onWheelHandler
976
847
  }
977
848
 
@@ -980,74 +851,65 @@ function initChart(
980
851
  canvasLayer: HTMLDivElement,
981
852
  rightAxisLayer: HTMLDivElement,
982
853
  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
854
+ ): ChartController {
855
+ const ctrl = createChartController({
856
+ container,
857
+ data: [],
858
+ canvasLayer,
859
+ rightAxisLayer,
860
+ xAxisCanvas,
861
+ initialZoomLevel: props.initialZoomLevel,
862
+ zoomLevels: props.zoomLevels,
863
+ yPaddingPx: props.yPaddingPx,
864
+ rightAxisWidth: props.rightAxisWidth,
865
+ bottomAxisHeight: props.bottomAxisHeight,
866
+ priceLabelWidth: props.priceLabelWidth,
867
+ minKWidth: props.minKWidth,
868
+ maxKWidth: props.maxKWidth,
869
+ })
870
+ return ctrl
1000
871
  }
1001
872
 
1002
- function setupChartCallbacks(chart: Chart): void {
1003
- // 注意:setOnViewportChange 已合并到 viewport signal 订阅者中
1004
-
1005
- chart.setOnPaneLayoutChange(() => {
1006
- // 分隔线位置计算(需要实际像素位置,保留在回调中)
873
+ function setupChartCallbacks(ctrl: ChartController): void {
874
+ const unsubscribePaneLayout = ctrl.paneLayout.subscribe(() => {
1007
875
  invalidateContainerRectCache()
1008
- const renderers = chart.getPaneRenderers()
1009
876
  const borderTop = containerRef.value
1010
877
  ? parseInt(getComputedStyle(containerRef.value).borderTopWidth) || 0
1011
878
  : 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
- }
879
+ const panes = ctrl.paneLayout.peek()
880
+ // 使用 pane 的实际渲染位置计算分隔线位置,确保与鼠标检测一致
881
+ paneSeparatorLines.value = panes.slice(0, -1).map((pane) => {
882
+ const paneInfo = ctrl.getPaneInfo(pane.id)
883
+ // 分隔线位置 = pane 顶部位置 + pane 实际高度
884
+ const separatorTop = (paneInfo?.top ?? 0) + (paneInfo?.height ?? 0)
885
+ return { id: pane.id, top: separatorTop + borderTop }
1018
886
  })
1019
887
  })
1020
888
 
1021
- // 订阅 paneRatios signal,同步到 Vue store
1022
- const unsubscribePaneRatios = chart.paneRatios.subscribe(() => {
1023
- const ratios = chart.paneRatios.peek()
1024
- store.actions.setPaneRatios({ ...ratios })
889
+ const unsubscribePaneRatios = ctrl.paneRatios.subscribe(() => {
890
+ const ratios = ctrl.paneRatios.peek()
891
+ paneRatios.value = { ...ratios }
1025
892
  })
1026
893
 
1027
- // 订阅 viewport signal,处理缩放、DPR、width 变化和 scrollLeft 更新
1028
- const unsubscribeViewport = chart.viewport.subscribe(() => {
1029
- const vp = chart.viewport.peek()
894
+ const unsubscribeViewport = ctrl.viewport.subscribe(() => {
895
+ const vp = ctrl.viewport.peek()
1030
896
 
1031
- // DPR 变化时同步到 store
1032
- if (store.state.viewportDpr !== vp.dpr) {
1033
- store.actions.setViewportDpr(vp.dpr)
897
+ if (viewportDpr.value !== vp.dpr) {
898
+ viewportDpr.value = vp.dpr
1034
899
  }
1035
-
1036
- // ViewWidth 变化时同步到 store
1037
- if (store.state.viewWidth !== vp.plotWidth) {
1038
- store.actions.setViewWidth(vp.plotWidth)
900
+ if (viewWidth.value !== vp.plotWidth) {
901
+ viewWidth.value = vp.plotWidth
1039
902
  }
1040
-
1041
- // 完整同步 zoom state 到 Vue store(Chart 是 SSOT)
1042
903
  if (
1043
- store.state.zoomLevel !== vp.zoomLevel ||
1044
- store.state.kWidth !== vp.kWidth ||
1045
- store.state.kGap !== vp.kGap
904
+ zoomLevel.value !== vp.zoomLevel ||
905
+ kWidth.value !== vp.kWidth ||
906
+ kGap.value !== vp.kGap
1046
907
  ) {
1047
- store.actions.setZoomState(vp.zoomLevel, vp.kWidth, vp.kGap)
908
+ zoomLevel.value = vp.zoomLevel
909
+ kWidth.value = vp.kWidth
910
+ kGap.value = vp.kGap
1048
911
  }
1049
912
 
1050
- // 在 nextTick 中应用 desiredScrollLeft
1051
913
  const desiredLeft = vp.desiredScrollLeft
1052
914
  if (desiredLeft !== undefined && desiredLeft !== containerRef.value?.scrollLeft) {
1053
915
  invalidateContainerRectCache()
@@ -1056,36 +918,32 @@ function setupChartCallbacks(chart: Chart): void {
1056
918
  if (!c) return
1057
919
  const maxScrollLeft = Math.max(0, c.scrollWidth - c.clientWidth)
1058
920
  const clampedScrollLeft = Math.min(Math.max(0, desiredLeft), maxScrollLeft)
1059
- const dpr = chart.getCurrentDpr()
921
+ const dpr = vp.dpr
1060
922
  c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
1061
923
  })
1062
924
  }
1063
925
  })
1064
926
 
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()
927
+ const unsubscribeData = ctrl.data.subscribe(() => {
928
+ const data = ctrl.data.peek()
929
+ dataLength.value = data.length
930
+ dataVersion.value++
1070
931
  })
1071
932
 
1072
- // 订阅 theme signal,同步到 CSS data-theme
1073
- const unsubscribeTheme = chart.theme.subscribe(() => {
1074
- const theme = chart.theme.peek()
1075
- chartTheme.value = theme
933
+ const unsubscribeTheme = ctrl.theme.subscribe(() => {
934
+ const newTheme = ctrl.theme.peek()
935
+ chartTheme.value = newTheme
936
+ emit('themeChange', newTheme)
1076
937
  })
1077
938
 
1078
- // 订阅 indicators signal,派生 Vue 本地状态(SSOT: Chart 引擎)
1079
- const unsubscribeIndicators = chart.indicators.subscribe(() => {
1080
- const instances = chart.indicators.peek()
939
+ const unsubscribeIndicators = ctrl.indicators.subscribe(() => {
940
+ const instances = ctrl.indicators.peek()
1081
941
 
1082
- // 同步主图指标列表
1083
942
  const mains = instances
1084
943
  .filter((i): i is IndicatorInstance & { role: 'main' } => i.role === 'main')
1085
944
  .map((i) => i.definitionId)
1086
945
  mainActiveIndicators.value = mains
1087
946
 
1088
- // 合并主图指标参数(不覆盖副图参数)
1089
947
  const nextParams = { ...indicatorParams.value }
1090
948
  for (const inst of instances) {
1091
949
  if (inst.role === 'main' && inst.params && Object.keys(inst.params).length > 0) {
@@ -1093,8 +951,7 @@ function setupChartCallbacks(chart: Chart): void {
1093
951
  }
1094
952
  }
1095
953
 
1096
- // 更新主图指标图例配置
1097
- chart.updateRendererConfig('mainIndicatorLegend', {
954
+ ctrl.updateRendererConfig('mainIndicatorLegend', {
1098
955
  indicators: {
1099
956
  MA: { enabled: mains.includes('MA'), params: nextParams['MA'] || {} },
1100
957
  BOLL: { enabled: mains.includes('BOLL'), params: nextParams['BOLL'] || {} },
@@ -1106,13 +963,10 @@ function setupChartCallbacks(chart: Chart): void {
1106
963
  indicatorParams.value = nextParams
1107
964
  })
1108
965
 
1109
- // 订阅 subPanes signal,派生 Vue 本地状态
1110
- // 注意:保持当前显示顺序(reorder UI 层私有状态),仅同步新增/删除
1111
- const unsubscribeSubPanes = chart.subPanes.subscribe(() => {
1112
- const subPaneInfos = chart.subPanes.peek()
966
+ const unsubscribeSubPanes = ctrl.subPanes.subscribe(() => {
967
+ const subPaneInfos = ctrl.subPanes.peek()
1113
968
  const signalIds = new Set(subPaneInfos.map((sp) => sp.paneId))
1114
969
 
1115
- // 保留 display order,移除已删除的 pane,追加新增的
1116
970
  const merged = subPanes.value.filter((p) => signalIds.has(p.id))
1117
971
  const existingIds = new Set(merged.map((p) => p.id))
1118
972
  for (const sp of subPaneInfos) {
@@ -1126,7 +980,6 @@ function setupChartCallbacks(chart: Chart): void {
1126
980
  }
1127
981
  subPanes.value = merged
1128
982
 
1129
- // 合并副图指标参数(不覆盖主图参数)
1130
983
  const nextParams = { ...indicatorParams.value }
1131
984
  for (const sp of subPaneInfos) {
1132
985
  if (sp.params && Object.keys(sp.params).length > 0) {
@@ -1136,68 +989,59 @@ function setupChartCallbacks(chart: Chart): void {
1136
989
  indicatorParams.value = nextParams
1137
990
  })
1138
991
 
1139
- // 保存 unsubscribe 函数以便清理
1140
992
  onUnmounted(() => {
1141
993
  unsubscribeViewport()
1142
994
  unsubscribeData()
1143
995
  unsubscribePaneRatios()
996
+ unsubscribePaneLayout()
1144
997
  unsubscribeTheme()
1145
998
  unsubscribeIndicators()
1146
999
  unsubscribeSubPanes()
1000
+ autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
1147
1001
  })
1148
1002
  }
1149
1003
 
1150
- function applyInitialSettings(chart: Chart): void {
1004
+ function applyInitialSettings(ctrl: ChartController): void {
1151
1005
  const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
1152
- chart.updateSettings(initialSettings)
1006
+ chartSettings.value = initialSettings
1007
+ applyThemeFromSettings(ctrl, initialSettings.theme as string)
1008
+ ctrl.updateSettingsFacade(initialSettings)
1153
1009
 
1154
1010
  if (initialSettings.performanceTest10kKlines) {
1155
1011
  const testData = generate10kKLineData()
1156
1012
  console.time('updateData-10k')
1157
- chart.updateData(testData)
1013
+ ctrl.updateData(testData)
1158
1014
  console.timeEnd('updateData-10k')
1159
1015
  }
1160
1016
  }
1161
1017
 
1162
- function setupDrawingController(chart: Chart): void {
1163
- drawingController.value = new DrawingInteractionController(chart)
1018
+ function setupDrawingController(ctrl: ChartController): void {
1019
+ drawingController.value = new DrawingInteractionController(ctrl)
1164
1020
  drawingController.value.setCallbacks({
1165
1021
  onDrawingCreated: (drawing) => {
1166
- store.actions.setDrawings([...store.state.drawings, drawing])
1167
- store.actions.setSelectedDrawingId(drawing.id)
1022
+ drawings.value = [...drawings.value, drawing]
1023
+ selectedDrawingId.value = drawing.id
1168
1024
  },
1169
1025
  onToolChange: () => {},
1170
1026
  onDrawingSelected: (drawing) => {
1171
- store.actions.setSelectedDrawingId(drawing?.id ?? null)
1027
+ selectedDrawingId.value = drawing?.id ?? null
1172
1028
  },
1173
1029
  })
1174
1030
  }
1175
1031
 
1176
- function setupInteractionCallbacks(chart: Chart): void {
1177
- chart.interaction.setTooltipAnchorPositioning(useAnchorPositioning.value)
1178
- chart.interaction.setOnInteractionChange((snapshot) => {
1179
- interactionState.value = snapshot
1032
+ function setupInteractionCallbacks(ctrl: ChartController): void {
1033
+ ctrl.setTooltipAnchorPositioning(useAnchorPositioning.value)
1034
+ ctrl.interactionState.subscribe(() => {
1035
+ interactionState.value = ctrl.interactionState.peek()
1180
1036
  })
1181
1037
 
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)
1190
- })
1191
-
1192
- interactionState.value = chart.interaction.getInteractionSnapshot()
1193
- store.actions.setViewportDpr(chart.getCurrentDpr())
1194
- chart.resize()
1038
+ interactionState.value = ctrl.interactionState.peek()
1039
+ viewportDpr.value = ctrl.viewport.peek().dpr
1195
1040
  }
1196
1041
 
1197
- /** 语义化控制器:外部配置 Chart API 的桥梁 */
1198
- function setupSemanticController(chart: Chart): void {
1042
+ function setupSemanticController(ctrl: ChartController): void {
1199
1043
  __setDataFetcher(props.dataFetcher)
1200
- semanticController.value = new SemanticChartController(chart)
1044
+ semanticController.value = new SemanticChartController(ctrl)
1201
1045
 
1202
1046
  semanticController.value.on('config:error', (error) => {
1203
1047
  console.error('Semantic config error:', error)
@@ -1221,62 +1065,52 @@ onMounted(() => {
1221
1065
  useAnchorPositioning.value = false
1222
1066
 
1223
1067
  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
1068
+ const chartMain = chartMainRef.value
1069
+ if (!container || !chartMain) return
1235
1070
 
1236
- // 3) 视口 / 面板布局 / 数据变更回调
1237
- setupChartCallbacks(chart)
1071
+ // 1) 滚轮缩放处理
1072
+ const onWheelHandler = setupWheelHandler()
1073
+ container.addEventListener('wheel', onWheelHandler, { passive: false })
1238
1074
 
1239
- // 4) 同步 zoom 状态(Vue SSOT → Chart)
1240
- chart.applyRenderState(store.state.kWidth, store.state.kGap, store.state.zoomLevel)
1075
+ // 2) 创建 Chart 控制器(使用模板 DOM 元素)
1076
+ const canvasLayer = container.querySelector<HTMLDivElement>('.canvas-layer')
1077
+ const xAxisCanvas = container.querySelector<HTMLCanvasElement>('.x-axis-canvas')
1078
+ const rightAxisLayer = chartMain.querySelector<HTMLDivElement>('.right-axis-host')
1079
+ const ctrl = initChart(container, canvasLayer!, rightAxisLayer!, xAxisCanvas!)
1080
+ controller.value = ctrl
1241
1081
 
1242
- // 5) 工具栏初始设置(含性能测试数据)
1243
- applyInitialSettings(chart)
1082
+ // 3) 信号回调
1083
+ setupChartCallbacks(ctrl)
1244
1084
 
1245
- // 6) 绘图交互控制器(线段/箭头等)
1246
- setupDrawingController(chart)
1085
+ // 4) 工具栏初始设置
1086
+ applyInitialSettings(ctrl)
1247
1087
 
1248
- // 7) 十字线、捏合缩放、初始交互快照
1249
- setupInteractionCallbacks(chart)
1088
+ // 5) 绘图交互控制器
1089
+ setupDrawingController(ctrl)
1250
1090
 
1251
- // 8) 语义化配置控制器(最终驱动数据加载)
1252
- setupSemanticController(chart)
1091
+ // 6) 交互信号桥接
1092
+ setupInteractionCallbacks(ctrl)
1253
1093
 
1254
- // onUnmounted 移除 wheel 监听
1255
- ;(chart as any).__onWheel = onWheelHandler
1094
+ // 7) 语义化配置
1095
+ setupSemanticController(ctrl)
1256
1096
  })
1257
1097
 
1258
1098
  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()
1099
+ const ctrl = controller.value
1100
+ if (ctrl) {
1101
+ controller.value = null
1102
+ ctrl.dispose()
1267
1103
  }
1268
- chartRef.value = null
1269
1104
  drawingController.value = null
1270
1105
  })
1271
1106
 
1272
1107
  // kWidth/kGap 由 zoomLevel 派生,不再通过 props 直接修改
1273
1108
  // 如需程序化控制缩放,请使用 expose 的 zoomToLevel/zoomIn/zoomOut 方法
1274
1109
 
1275
- // 监听 yPaddingPx 变化
1276
1110
  watch(
1277
1111
  () => props.yPaddingPx,
1278
1112
  (newVal) => {
1279
- chartRef.value?.updateOptions({ yPaddingPx: newVal })
1113
+ controller.value?.updateOptionsFacade({ yPaddingPx: newVal })
1280
1114
  },
1281
1115
  )
1282
1116
 
@@ -1300,12 +1134,12 @@ watch(
1300
1134
  --kmap-height: var(--kmap-chart-height, 100%);
1301
1135
  --kmap-width: var(--kmap-chart-width, 100%);
1302
1136
 
1303
- --chart-bg: #ffffff;
1304
- --chart-bg-secondary: #f8f9fa;
1305
- --chart-border: #e5e7eb;
1306
- --chart-border-active: #3b82f6;
1307
- --chart-text: #374151;
1308
- --chart-text-secondary: #6b7280;
1137
+ --chart-bg: var(--klc-color-chart-background);
1138
+ --chart-bg-secondary: var(--klc-color-chart-background);
1139
+ --chart-border: var(--klc-color-border-chart);
1140
+ --chart-border-active: #1890ff;
1141
+ --chart-text: var(--klc-color-foreground);
1142
+ --chart-text-secondary: var(--klc-color-axis-text);
1309
1143
 
1310
1144
  display: flex;
1311
1145
  align-items: center;
@@ -1316,15 +1150,6 @@ watch(
1316
1150
  flex-direction: column;
1317
1151
  }
1318
1152
 
1319
- .chart-wrapper[data-theme='dark'] {
1320
- --chart-bg: #1a1a2e;
1321
- --chart-bg-secondary: #16162a;
1322
- --chart-border: #2d2d44;
1323
- --chart-border-active: #60a5fa;
1324
- --chart-text: #e5e7eb;
1325
- --chart-text-secondary: #9ca3af;
1326
- }
1327
-
1328
1153
  .chart-stage {
1329
1154
  width: 95%;
1330
1155
  height: 85%;