@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.
- package/README.md +4 -4
- package/dist/components/ChartSettingsDialog.vue.d.ts +14 -0
- package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -0
- package/dist/components/ColorPresetPanel.vue.d.ts +12 -0
- package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -0
- package/dist/components/DrawingStyleToolbar.vue.d.ts +1 -15
- package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
- package/dist/components/IndicatorParams.vue.d.ts.map +1 -1
- package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
- package/dist/components/KLineChart.vue.d.ts +7 -6
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/KLineTooltip.vue.d.ts +9 -2
- package/dist/components/KLineTooltip.vue.d.ts.map +1 -1
- package/dist/components/LeftToolbar.vue.d.ts +4 -3
- package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
- package/dist/components/MarkerTooltip.vue.d.ts +1 -12
- package/dist/components/MarkerTooltip.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/composables/useFullscreenTeleportTarget.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.css +1 -0
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1009 -905
- package/dist/version.d.ts +1 -1
- package/dist/web-component.d.ts +18 -0
- package/dist/web-component.d.ts.map +1 -0
- package/package.json +10 -2
- package/src/__tests__/_mockController.ts +11 -1
- package/src/components/ChartSettingsDialog.vue +624 -0
- package/src/components/ColorPresetPanel.vue +289 -0
- package/src/components/DrawingStyleToolbar.vue +12 -25
- package/src/components/IndicatorParams.vue +58 -57
- package/src/components/IndicatorSelector.vue +91 -88
- package/src/components/KLineChart.vue +267 -442
- package/src/components/KLineTooltip.vue +19 -13
- package/src/components/LeftToolbar.vue +35 -393
- package/src/components/MarkerTooltip.vue +5 -16
- package/src/components/index.ts +1 -0
- package/src/composables/useFullscreenTeleportTarget.ts +0 -2
- package/src/web-component.ts +14 -0
- 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 {
|
|
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
|
-
|
|
145
|
-
} from '@363045841yyt/klinechart-core/
|
|
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
|
|
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
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
})
|
|
233
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
273
|
+
/* Controller auto-renders on state changes */
|
|
249
274
|
}
|
|
250
275
|
|
|
251
|
-
function handleSettingsChange(settings:
|
|
252
|
-
|
|
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
|
-
|
|
284
|
+
controller.value?.updateData(testData)
|
|
259
285
|
console.timeEnd('updateData-10k')
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
434
|
-
const viewport =
|
|
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
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
493
|
+
controller.value?.handlePointerEvent(e, {
|
|
473
494
|
onPointerDown: (event, container) => {
|
|
474
495
|
if (drawingController.value?.onPointerDown(event, container)) {
|
|
475
|
-
|
|
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
|
-
|
|
513
|
+
controller.value?.handlePointerEvent(e, {
|
|
494
514
|
onPointerMove: (event, container) => {
|
|
495
515
|
if (drawingController.value?.onPointerMove(event, container)) {
|
|
496
|
-
|
|
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
|
-
|
|
525
|
+
controller.value?.handlePointerEvent(e, {
|
|
506
526
|
onPointerUp: (event, container) => {
|
|
507
527
|
if (drawingController.value?.onPointerUp(event, container)) {
|
|
508
|
-
|
|
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
|
-
|
|
518
|
-
chartRef.value?.handlePointerEvent(e)
|
|
537
|
+
controller.value?.handlePointerEvent(e)
|
|
519
538
|
}
|
|
520
539
|
|
|
521
540
|
function onRightAxisPointerDown(e: PointerEvent) {
|
|
522
|
-
|
|
541
|
+
controller.value?.handlePointerEvent(e)
|
|
523
542
|
}
|
|
524
543
|
|
|
525
544
|
function onRightAxisPointerMove(e: PointerEvent) {
|
|
526
|
-
|
|
545
|
+
controller.value?.handlePointerEvent(e)
|
|
527
546
|
}
|
|
528
547
|
|
|
529
548
|
function onRightAxisPointerUp(e: PointerEvent) {
|
|
530
|
-
|
|
549
|
+
controller.value?.handlePointerEvent(e)
|
|
531
550
|
}
|
|
532
551
|
|
|
533
552
|
function onRightAxisPointerLeave(e: PointerEvent) {
|
|
534
|
-
|
|
553
|
+
controller.value?.handlePointerEvent(e)
|
|
535
554
|
}
|
|
536
555
|
|
|
537
556
|
function onScroll() {
|
|
538
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
679
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
|
767
|
-
if (!
|
|
693
|
+
const c = controller.value
|
|
694
|
+
if (!c) return
|
|
768
695
|
|
|
769
|
-
// 主图指标处理
|
|
770
696
|
const mainIndicatorIds = [
|
|
771
|
-
'MA',
|
|
772
|
-
'
|
|
773
|
-
'
|
|
774
|
-
'ENE',
|
|
775
|
-
'WMA',
|
|
776
|
-
'DEMA',
|
|
777
|
-
'TEMA',
|
|
778
|
-
'HMA',
|
|
779
|
-
'KAMA',
|
|
780
|
-
'SAR',
|
|
781
|
-
'SUPERTREND',
|
|
782
|
-
'KELTNER',
|
|
783
|
-
'DONCHIAN',
|
|
784
|
-
'ICHIMOKU',
|
|
785
|
-
'PIVOT',
|
|
786
|
-
'FIB',
|
|
787
|
-
'STRUCTURE',
|
|
788
|
-
'ZONES',
|
|
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
|
-
|
|
795
|
-
chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
704
|
+
c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
796
705
|
} else if (!active && existingIndicator) {
|
|
797
|
-
|
|
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
|
-
|
|
815
|
-
|
|
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
|
-
|
|
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 === '
|
|
840
|
-
indicatorId === 'EXPMA' ||
|
|
841
|
-
indicatorId === 'ENE'
|
|
733
|
+
indicatorId === 'MA' || indicatorId === 'BOLL' ||
|
|
734
|
+
indicatorId === 'EXPMA' || indicatorId === 'ENE'
|
|
842
735
|
) {
|
|
843
|
-
|
|
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
|
-
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
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
|
|
913
|
-
if (!container || !
|
|
801
|
+
const c = controller.value
|
|
802
|
+
if (!container || !c) return
|
|
914
803
|
|
|
915
|
-
const dataLength =
|
|
804
|
+
const dataLength = c.getData()?.length ?? 0
|
|
916
805
|
if (dataLength === 0) return
|
|
917
806
|
|
|
918
|
-
const
|
|
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
|
-
|
|
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: () =>
|
|
836
|
+
getZoomLevelCount: () => controller.value?.getZoomLevelCount() ?? 10,
|
|
961
837
|
})
|
|
962
838
|
|
|
963
839
|
// ==================== onMounted 拆分函数 ====================
|
|
964
840
|
|
|
965
|
-
function setupWheelHandler(
|
|
841
|
+
function setupWheelHandler(): (e: WheelEvent) => void {
|
|
966
842
|
const onWheelHandler = (e: WheelEvent) => {
|
|
967
843
|
e.preventDefault()
|
|
968
|
-
|
|
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
|
-
):
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
)
|
|
999
|
-
return
|
|
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(
|
|
1003
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
const vp = chart.viewport.peek()
|
|
894
|
+
const unsubscribeViewport = ctrl.viewport.subscribe(() => {
|
|
895
|
+
const vp = ctrl.viewport.peek()
|
|
1030
896
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
store.actions.setViewportDpr(vp.dpr)
|
|
897
|
+
if (viewportDpr.value !== vp.dpr) {
|
|
898
|
+
viewportDpr.value = vp.dpr
|
|
1034
899
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
904
|
+
zoomLevel.value !== vp.zoomLevel ||
|
|
905
|
+
kWidth.value !== vp.kWidth ||
|
|
906
|
+
kGap.value !== vp.kGap
|
|
1046
907
|
) {
|
|
1047
|
-
|
|
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 =
|
|
921
|
+
const dpr = vp.dpr
|
|
1060
922
|
c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
|
|
1061
923
|
})
|
|
1062
924
|
}
|
|
1063
925
|
})
|
|
1064
926
|
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
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(
|
|
1004
|
+
function applyInitialSettings(ctrl: ChartController): void {
|
|
1151
1005
|
const initialSettings = toolbarRef.value?.getSettings() ?? { showVolumePriceMarkers: true }
|
|
1152
|
-
|
|
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
|
-
|
|
1013
|
+
ctrl.updateData(testData)
|
|
1158
1014
|
console.timeEnd('updateData-10k')
|
|
1159
1015
|
}
|
|
1160
1016
|
}
|
|
1161
1017
|
|
|
1162
|
-
function setupDrawingController(
|
|
1163
|
-
drawingController.value = new DrawingInteractionController(
|
|
1018
|
+
function setupDrawingController(ctrl: ChartController): void {
|
|
1019
|
+
drawingController.value = new DrawingInteractionController(ctrl)
|
|
1164
1020
|
drawingController.value.setCallbacks({
|
|
1165
1021
|
onDrawingCreated: (drawing) => {
|
|
1166
|
-
|
|
1167
|
-
|
|
1022
|
+
drawings.value = [...drawings.value, drawing]
|
|
1023
|
+
selectedDrawingId.value = drawing.id
|
|
1168
1024
|
},
|
|
1169
1025
|
onToolChange: () => {},
|
|
1170
1026
|
onDrawingSelected: (drawing) => {
|
|
1171
|
-
|
|
1027
|
+
selectedDrawingId.value = drawing?.id ?? null
|
|
1172
1028
|
},
|
|
1173
1029
|
})
|
|
1174
1030
|
}
|
|
1175
1031
|
|
|
1176
|
-
function setupInteractionCallbacks(
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
interactionState.value =
|
|
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
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
1198
|
-
function setupSemanticController(chart: Chart): void {
|
|
1042
|
+
function setupSemanticController(ctrl: ChartController): void {
|
|
1199
1043
|
__setDataFetcher(props.dataFetcher)
|
|
1200
|
-
semanticController.value = new SemanticChartController(
|
|
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
|
|
1225
|
-
|
|
1226
|
-
const xAxisCanvas = xAxisCanvasRef.value
|
|
1227
|
-
if (!container || !canvasLayer || !rightAxisLayer || !xAxisCanvas) return
|
|
1228
|
-
|
|
1229
|
-
// 1) 滚轮缩放:passive:false 以阻止页面滚动
|
|
1230
|
-
const onWheelHandler = setupWheelHandler(container)
|
|
1231
|
-
|
|
1232
|
-
// 2) 创建 Chart 实例并注册全部渲染器
|
|
1233
|
-
const chart = initChart(container, canvasLayer, rightAxisLayer, xAxisCanvas)
|
|
1234
|
-
chartRef.value = chart
|
|
1068
|
+
const chartMain = chartMainRef.value
|
|
1069
|
+
if (!container || !chartMain) return
|
|
1235
1070
|
|
|
1236
|
-
//
|
|
1237
|
-
|
|
1071
|
+
// 1) 滚轮缩放处理
|
|
1072
|
+
const onWheelHandler = setupWheelHandler()
|
|
1073
|
+
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
1238
1074
|
|
|
1239
|
-
//
|
|
1240
|
-
|
|
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
|
-
//
|
|
1243
|
-
|
|
1082
|
+
// 3) 信号回调
|
|
1083
|
+
setupChartCallbacks(ctrl)
|
|
1244
1084
|
|
|
1245
|
-
//
|
|
1246
|
-
|
|
1085
|
+
// 4) 工具栏初始设置
|
|
1086
|
+
applyInitialSettings(ctrl)
|
|
1247
1087
|
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1088
|
+
// 5) 绘图交互控制器
|
|
1089
|
+
setupDrawingController(ctrl)
|
|
1250
1090
|
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1091
|
+
// 6) 交互信号桥接
|
|
1092
|
+
setupInteractionCallbacks(ctrl)
|
|
1253
1093
|
|
|
1254
|
-
//
|
|
1255
|
-
|
|
1094
|
+
// 7) 语义化配置
|
|
1095
|
+
setupSemanticController(ctrl)
|
|
1256
1096
|
})
|
|
1257
1097
|
|
|
1258
1098
|
onUnmounted(() => {
|
|
1259
|
-
const
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
| undefined
|
|
1264
|
-
const container = containerRef.value
|
|
1265
|
-
if (onWheel && container) container.removeEventListener('wheel', onWheel)
|
|
1266
|
-
chart.destroy()
|
|
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
|
-
|
|
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:
|
|
1304
|
-
--chart-bg-secondary:
|
|
1305
|
-
--chart-border:
|
|
1306
|
-
--chart-border-active: #
|
|
1307
|
-
--chart-text:
|
|
1308
|
-
--chart-text-secondary:
|
|
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%;
|