@363045841yyt/klinechart 0.7.3-alpha.1 → 0.7.3-alpha.3
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/package.json +75 -75
- package/src/components/IndicatorSelector.vue +42 -42
- package/src/components/KLineChart.vue +127 -127
- package/src/components/LeftToolbar.vue +44 -44
- package/LICENSE +0 -21
|
@@ -146,27 +146,27 @@ import {
|
|
|
146
146
|
|
|
147
147
|
const props = withDefaults(
|
|
148
148
|
defineProps<{
|
|
149
|
-
/**
|
|
149
|
+
/** 语义化配置(必需,唯一控制源) */
|
|
150
150
|
semanticConfig: SemanticChartConfig
|
|
151
151
|
|
|
152
|
-
/**
|
|
152
|
+
/** 数据获取函数(必需)。框架不绑定数据源,由使用者注入。 */
|
|
153
153
|
dataFetcher: DataFetcher
|
|
154
154
|
|
|
155
155
|
yPaddingPx?: number
|
|
156
156
|
minKWidth?: number
|
|
157
157
|
maxKWidth?: number
|
|
158
|
-
/**
|
|
158
|
+
/** 右侧价格轴宽度 */
|
|
159
159
|
rightAxisWidth?: number
|
|
160
|
-
/**
|
|
160
|
+
/** 底部时间轴高度 */
|
|
161
161
|
bottomAxisHeight?: number
|
|
162
|
-
/**
|
|
162
|
+
/** 价格标签额外宽度(用于显示涨跌幅,默认 60px) */
|
|
163
163
|
priceLabelWidth?: number
|
|
164
164
|
|
|
165
|
-
/**
|
|
165
|
+
/** 缩放级别数量(默认 10) */
|
|
166
166
|
zoomLevels?: number
|
|
167
|
-
/**
|
|
167
|
+
/** 初始缩放级别(1 ~ zoomLevels,默认居中) */
|
|
168
168
|
initialZoomLevel?: number
|
|
169
|
-
/**
|
|
169
|
+
/** 是否全屏 */
|
|
170
170
|
isFullscreen?: boolean
|
|
171
171
|
}>(),
|
|
172
172
|
{
|
|
@@ -195,13 +195,13 @@ const chartMainRef = ref<HTMLDivElement | null>(null)
|
|
|
195
195
|
const tooltipLayerRef = ref<HTMLDivElement | null>(null)
|
|
196
196
|
const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
|
|
197
197
|
|
|
198
|
-
/* ==========
|
|
198
|
+
/* ========== 十字线(鼠标悬停位置) ========== */
|
|
199
199
|
const chartRef = shallowRef<Chart | null>(null)
|
|
200
200
|
|
|
201
|
-
/* ==========
|
|
201
|
+
/* ========== 语义化控制器 ========== */
|
|
202
202
|
const semanticController = shallowRef<SemanticChartController | null>(null)
|
|
203
203
|
|
|
204
|
-
/* ========== ChartStore
|
|
204
|
+
/* ========== ChartStore(响应式状态中心) ========== */
|
|
205
205
|
const store = createChartStore({
|
|
206
206
|
initialZoomLevel: props.initialZoomLevel ?? 1,
|
|
207
207
|
minKWidth: props.minKWidth,
|
|
@@ -211,10 +211,10 @@ const store = createChartStore({
|
|
|
211
211
|
priceLabelWidth: props.priceLabelWidth,
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
-
/* ==========
|
|
214
|
+
/* ========== 主题状态 ========== */
|
|
215
215
|
const chartTheme = ref<'light' | 'dark'>('light')
|
|
216
216
|
|
|
217
|
-
//
|
|
217
|
+
// 初始化 kWidth / kGap
|
|
218
218
|
store.actions.setZoomState(
|
|
219
219
|
store.state.zoomLevel,
|
|
220
220
|
zoomLevelToKWidth(store.state.zoomLevel, {
|
|
@@ -234,7 +234,7 @@ store.actions.setZoomState(
|
|
|
234
234
|
),
|
|
235
235
|
)
|
|
236
236
|
|
|
237
|
-
//
|
|
237
|
+
// 为逐步迁移保留的局部别名
|
|
238
238
|
const dataLength = computed(() => store.state.dataLength)
|
|
239
239
|
const viewportDpr = computed(() => store.state.viewportDpr)
|
|
240
240
|
const zoomLevel = computed(() => store.state.zoomLevel)
|
|
@@ -251,7 +251,7 @@ function scheduleRender() {
|
|
|
251
251
|
function handleSettingsChange(settings: Record<string, boolean | string>) {
|
|
252
252
|
chartRef.value?.updateSettings(settings)
|
|
253
253
|
|
|
254
|
-
//
|
|
254
|
+
// 万条K线性能测试
|
|
255
255
|
if (settings.performanceTest10kKlines) {
|
|
256
256
|
const testData = generate10kKLineData()
|
|
257
257
|
console.time('updateData-10k')
|
|
@@ -260,28 +260,28 @@ function handleSettingsChange(settings: Record<string, boolean | string>) {
|
|
|
260
260
|
store.actions.setDataLength(testData.length)
|
|
261
261
|
store.actions.bumpDataVersion()
|
|
262
262
|
} else {
|
|
263
|
-
//
|
|
264
|
-
//
|
|
263
|
+
// 如果关闭性能测试,恢复原始数据
|
|
264
|
+
// 通过重新应用语义化配置来恢复
|
|
265
265
|
if (semanticController.value && chartRef.value?.getData()?.length === 10000) {
|
|
266
266
|
semanticController.value.applyConfig(props.semanticConfig)
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
//
|
|
271
|
+
// 生成1万条K线测试数据
|
|
272
272
|
function generate10kKLineData() {
|
|
273
273
|
const data: KLineData[] = []
|
|
274
274
|
const startTime = new Date('2020-01-01').getTime()
|
|
275
275
|
const dayMs = 24 * 60 * 60 * 1000
|
|
276
276
|
|
|
277
|
-
let lastClose = 3000 //
|
|
277
|
+
let lastClose = 3000 // 起始价格
|
|
278
278
|
|
|
279
279
|
for (let i = 0; i < 10000; i++) {
|
|
280
280
|
const timestamp = startTime + i * dayMs
|
|
281
281
|
|
|
282
|
-
//
|
|
283
|
-
const volatility = 0.02 // 2
|
|
284
|
-
const trend = 0.0001 //
|
|
282
|
+
// 生成随机波动
|
|
283
|
+
const volatility = 0.02 // 2%日波动率
|
|
284
|
+
const trend = 0.0001 // 轻微上涨趋势
|
|
285
285
|
const change = (Math.random() - 0.5) * 2 * volatility + trend
|
|
286
286
|
|
|
287
287
|
const open = lastClose
|
|
@@ -330,11 +330,11 @@ function setMarkerTooltipEl(el: HTMLDivElement | null) {
|
|
|
330
330
|
})
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
// ===== Marker tooltip
|
|
333
|
+
// ===== Marker tooltip 状态 =====
|
|
334
334
|
const mousePos = ref({ x: 0, y: 0 })
|
|
335
335
|
const useAnchorPositioning = ref(false)
|
|
336
336
|
|
|
337
|
-
//
|
|
337
|
+
// 容器 rect 缓存,避免 pointermove 中反复 getBoundingClientRect 强制同步布局
|
|
338
338
|
let _cachedContainerRect: DOMRect | null = null
|
|
339
339
|
function invalidateContainerRectCache(): void {
|
|
340
340
|
_cachedContainerRect = null
|
|
@@ -346,7 +346,7 @@ function getContainerRect(container: HTMLDivElement): DOMRect {
|
|
|
346
346
|
return _cachedContainerRect
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
// =====
|
|
349
|
+
// ===== 交互状态(单一来源:InteractionController snapshot) =====
|
|
350
350
|
const interactionState = shallowRef<InteractionSnapshot>({
|
|
351
351
|
crosshairPos: null,
|
|
352
352
|
crosshairIndex: null,
|
|
@@ -392,7 +392,7 @@ const isHoveringRightAxis = computed(() => interactionState.value.isHoveringRigh
|
|
|
392
392
|
const hoveredIdx = computed(() => interactionState.value.hoveredIndex)
|
|
393
393
|
const crosshairIdx = computed(() => interactionState.value.crosshairIndex)
|
|
394
394
|
|
|
395
|
-
//
|
|
395
|
+
// 统一光标样式:用内联 style 替代 CSS 类后代选择器,切断级联失效链
|
|
396
396
|
const containerCursor = computed(() => {
|
|
397
397
|
if (isDragging.value) return 'grabbing'
|
|
398
398
|
if (isResizingPane.value || isHoveringPaneSeparator.value) return 'ns-resize'
|
|
@@ -403,7 +403,7 @@ const containerCursor = computed(() => {
|
|
|
403
403
|
const hovered = computed(() => {
|
|
404
404
|
const idx = interactionState.value.hoveredIndex
|
|
405
405
|
if (typeof idx !== 'number') return null
|
|
406
|
-
void dataVersion.value //
|
|
406
|
+
void dataVersion.value // 建立响应式依赖
|
|
407
407
|
const data = chartRef.value?.getData()
|
|
408
408
|
if (data && idx >= 0 && idx < data.length) {
|
|
409
409
|
return data[idx]
|
|
@@ -441,13 +441,13 @@ const markerTooltipAnchorPlacement = computed<'right-bottom' | 'left-bottom'>(()
|
|
|
441
441
|
return wouldOverflowRight ? 'left-bottom' : 'right-bottom'
|
|
442
442
|
})
|
|
443
443
|
|
|
444
|
-
//
|
|
444
|
+
// 获取当前图表数据
|
|
445
445
|
const chartData = computed(() => {
|
|
446
|
-
void dataVersion.value //
|
|
446
|
+
void dataVersion.value // 建立响应式依赖,确保数据变化时重新求值
|
|
447
447
|
return chartRef.value?.getData() ?? []
|
|
448
448
|
})
|
|
449
449
|
|
|
450
|
-
//
|
|
450
|
+
// 通知数据变化(在数据更新后调用)
|
|
451
451
|
function handleSelectTool(toolId: string) {
|
|
452
452
|
drawingController.value?.setTool(toolId as DrawingToolId)
|
|
453
453
|
}
|
|
@@ -514,7 +514,7 @@ function onPointerUp(e: PointerEvent) {
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
function onPointerLeave(e: PointerEvent) {
|
|
517
|
-
// pointerleave
|
|
517
|
+
// pointerleave 不需要绘图控制器路由,直接调用
|
|
518
518
|
chartRef.value?.handlePointerEvent(e)
|
|
519
519
|
}
|
|
520
520
|
|
|
@@ -538,10 +538,10 @@ function onScroll() {
|
|
|
538
538
|
chartRef.value?.handleScrollEvent()
|
|
539
539
|
}
|
|
540
540
|
|
|
541
|
-
//
|
|
541
|
+
// 主图指标显式状态(副图指标从 subPanes 派生)
|
|
542
542
|
const mainActiveIndicators = ref<string[]>([])
|
|
543
543
|
|
|
544
|
-
//
|
|
544
|
+
// 副图指标列表从 subPanes 自动派生
|
|
545
545
|
const subActiveIndicators = computed(() => {
|
|
546
546
|
const ids: string[] = []
|
|
547
547
|
const seen = new Set<string>()
|
|
@@ -554,26 +554,26 @@ const subActiveIndicators = computed(() => {
|
|
|
554
554
|
return ids
|
|
555
555
|
})
|
|
556
556
|
|
|
557
|
-
//
|
|
557
|
+
// 最终合并列表(主图 + 副图),保持显示顺序
|
|
558
558
|
const activeIndicators = computed(() => [
|
|
559
559
|
...mainActiveIndicators.value,
|
|
560
560
|
...subActiveIndicators.value,
|
|
561
561
|
])
|
|
562
562
|
|
|
563
|
-
//
|
|
563
|
+
// 指标参数配置(MA 的 periods 是数组,需要更宽松的类型)
|
|
564
564
|
const indicatorParams = ref<Record<string, Record<string, unknown>>>({})
|
|
565
565
|
|
|
566
|
-
//
|
|
566
|
+
// 副图槽位状态
|
|
567
567
|
interface SubPaneSlot {
|
|
568
568
|
id: string // pane ID: 'RSI_0', 'MACD_0', ...
|
|
569
569
|
indicatorId: SubIndicatorType
|
|
570
570
|
params: Record<string, unknown>
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
-
//
|
|
573
|
+
// 副图槽位数组(支持多副图)
|
|
574
574
|
const subPanes = ref<SubPaneSlot[]>([])
|
|
575
575
|
|
|
576
|
-
//
|
|
576
|
+
// 最大副图数量
|
|
577
577
|
const maxSubPanes = 4
|
|
578
578
|
|
|
579
579
|
function buildPaneLayoutIntent(): PaneSpec[] {
|
|
@@ -591,14 +591,14 @@ function buildPaneLayoutIntent(): PaneSpec[] {
|
|
|
591
591
|
]
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
//
|
|
594
|
+
// 获取指标默认参数
|
|
595
595
|
function getDefaultParams(
|
|
596
596
|
indicatorId: SubIndicatorType,
|
|
597
597
|
): Record<string, number | boolean | string> {
|
|
598
598
|
return { ...SUB_PANE_INDICATOR_CONFIGS[indicatorId].defaultParams }
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
//
|
|
601
|
+
// 副图实例计数器:用于生成 'RSI_0', 'MACD_0' 这样的 paneId
|
|
602
602
|
const subPaneCounters = new Map<SubIndicatorType, number>()
|
|
603
603
|
|
|
604
604
|
function generatePaneId(indicatorId: SubIndicatorType): string {
|
|
@@ -607,7 +607,7 @@ function generatePaneId(indicatorId: SubIndicatorType): string {
|
|
|
607
607
|
return `${indicatorId}_${count}`
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
-
// paneTitle
|
|
610
|
+
// paneTitle 渲染器名称映射(paneId -> rendererName)
|
|
611
611
|
const paneTitleRendererNames = new Map<string, string>()
|
|
612
612
|
|
|
613
613
|
function mountSubPaneTitle(paneId: string, indicatorId: SubIndicatorType): void {
|
|
@@ -628,7 +628,7 @@ function unmountSubPaneTitle(paneId: string): void {
|
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
-
//
|
|
631
|
+
// 添加副图(使用 Chart API)
|
|
632
632
|
function addSubPane(
|
|
633
633
|
indicatorId: SubIndicatorType = 'VOLUME',
|
|
634
634
|
params?: Record<string, number | boolean | string>,
|
|
@@ -639,14 +639,14 @@ function addSubPane(
|
|
|
639
639
|
|
|
640
640
|
const mergedParams = params ?? getDefaultParams(indicatorId)
|
|
641
641
|
|
|
642
|
-
//
|
|
642
|
+
// 使用高层 Facade API 创建副图指标
|
|
643
643
|
const paneId = chartRef.value?.addIndicator(indicatorId, 'sub', mergedParams)
|
|
644
644
|
if (!paneId) return false
|
|
645
645
|
|
|
646
|
-
//
|
|
646
|
+
// 创建 paneTitle 渲染器(UI 层职责)
|
|
647
647
|
mountSubPaneTitle(paneId, indicatorId)
|
|
648
648
|
|
|
649
|
-
//
|
|
649
|
+
// 更新本地状态
|
|
650
650
|
subPanes.value.push({
|
|
651
651
|
id: paneId,
|
|
652
652
|
indicatorId,
|
|
@@ -657,7 +657,7 @@ function addSubPane(
|
|
|
657
657
|
return true
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
-
//
|
|
660
|
+
// 移除副图(使用高层 Facade API)
|
|
661
661
|
function removeSubPane(paneId: string): void {
|
|
662
662
|
const index = subPanes.value.findIndex((p) => p.id === paneId)
|
|
663
663
|
if (index === -1) return
|
|
@@ -665,50 +665,50 @@ function removeSubPane(paneId: string): void {
|
|
|
665
665
|
const pane = subPanes.value[index]
|
|
666
666
|
if (!pane) return
|
|
667
667
|
|
|
668
|
-
//
|
|
668
|
+
// 移除 paneTitle 渲染器
|
|
669
669
|
unmountSubPaneTitle(paneId)
|
|
670
670
|
|
|
671
|
-
//
|
|
671
|
+
// 使用高层 Facade API 移除指标
|
|
672
672
|
chartRef.value?.removeIndicator(paneId)
|
|
673
673
|
|
|
674
|
-
//
|
|
674
|
+
// 更新本地状态
|
|
675
675
|
subPanes.value.splice(index, 1)
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
-
//
|
|
678
|
+
// 清除所有副图(使用高层 Facade API)
|
|
679
679
|
function clearAllSubPanes(): void {
|
|
680
|
-
//
|
|
680
|
+
// 使用高层 Facade API 逐个移除
|
|
681
681
|
for (const pane of subPanes.value) {
|
|
682
682
|
chartRef.value?.removeIndicator(pane.id)
|
|
683
683
|
unmountSubPaneTitle(pane.id)
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
-
//
|
|
686
|
+
// 清空本地状态
|
|
687
687
|
subPanes.value = []
|
|
688
688
|
subPaneCounters.clear()
|
|
689
689
|
paneTitleRendererNames.clear()
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
-
//
|
|
692
|
+
// 从语义化配置初始化指标状态(单向数据流:config → chart)
|
|
693
693
|
function initIndicatorsFromConfig(): void {
|
|
694
694
|
const config = props.semanticConfig
|
|
695
695
|
const chart = chartRef.value
|
|
696
696
|
if (!chart) return
|
|
697
697
|
|
|
698
|
-
//
|
|
698
|
+
// 初始化主图指标 - 直接调用Chart API
|
|
699
699
|
const mainIndicators = config.indicators?.main
|
|
700
700
|
if (mainIndicators) {
|
|
701
701
|
for (const indicator of mainIndicators) {
|
|
702
702
|
if (indicator.enabled) {
|
|
703
|
-
//
|
|
703
|
+
// 同步Vue状态(用于UI展示)
|
|
704
704
|
if (!mainActiveIndicators.value.includes(indicator.type)) {
|
|
705
705
|
mainActiveIndicators.value.push(indicator.type)
|
|
706
706
|
}
|
|
707
|
-
//
|
|
707
|
+
// 保存参数
|
|
708
708
|
if (indicator.params) {
|
|
709
709
|
indicatorParams.value[indicator.type] = indicator.params as Record<string, unknown>
|
|
710
710
|
}
|
|
711
|
-
//
|
|
711
|
+
// 启用指标(Chart内部管理渲染器)
|
|
712
712
|
chart.enableMainIndicator(
|
|
713
713
|
indicator.type,
|
|
714
714
|
indicator.params as Record<string, number | boolean | string>,
|
|
@@ -717,18 +717,18 @@ function initIndicatorsFromConfig(): void {
|
|
|
717
717
|
}
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
-
//
|
|
720
|
+
// 副图指标参数由 syncSubPanesFromChart 处理
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
-
//
|
|
723
|
+
// 监听主图指标参数变化,同步到Chart(状态由Chart管理,Vue只同步参数)
|
|
724
724
|
watch(
|
|
725
725
|
[activeIndicators, indicatorParams],
|
|
726
726
|
([indicators]) => {
|
|
727
727
|
const chart = chartRef.value
|
|
728
728
|
if (!chart) return
|
|
729
729
|
|
|
730
|
-
//
|
|
731
|
-
//
|
|
730
|
+
// 只更新mainIndicatorLegend的配置(用于图例显示)
|
|
731
|
+
// 渲染器的启用/禁用由Chart内部管理
|
|
732
732
|
chart.updateRendererConfig('mainIndicatorLegend', {
|
|
733
733
|
indicators: {
|
|
734
734
|
MA: {
|
|
@@ -755,18 +755,18 @@ watch(
|
|
|
755
755
|
{ deep: true },
|
|
756
756
|
)
|
|
757
757
|
|
|
758
|
-
//
|
|
758
|
+
// 从 Chart 同步副图状态到本地(语义化配置后调用)
|
|
759
759
|
function syncSubPanesFromChart(): void {
|
|
760
760
|
const chartSubPaneEntries = chartRef.value?.getSubPaneEntries() ?? []
|
|
761
761
|
|
|
762
|
-
//
|
|
762
|
+
// 清空本地状态
|
|
763
763
|
subPanes.value = []
|
|
764
764
|
paneTitleRendererNames.clear()
|
|
765
765
|
|
|
766
766
|
for (const entry of chartSubPaneEntries) {
|
|
767
767
|
const { paneId, indicatorId, params } = entry
|
|
768
768
|
|
|
769
|
-
//
|
|
769
|
+
// 恢复计数器状态
|
|
770
770
|
const match = paneId.match(/^(.+)_(\d+)$/)
|
|
771
771
|
if (match) {
|
|
772
772
|
const [, indicator, countStr] = match
|
|
@@ -777,10 +777,10 @@ function syncSubPanesFromChart(): void {
|
|
|
777
777
|
}
|
|
778
778
|
}
|
|
779
779
|
|
|
780
|
-
//
|
|
780
|
+
// 创建 paneTitle 渲染器
|
|
781
781
|
mountSubPaneTitle(paneId, indicatorId)
|
|
782
782
|
|
|
783
|
-
//
|
|
783
|
+
// 更新本地状态
|
|
784
784
|
subPanes.value.push({
|
|
785
785
|
id: paneId,
|
|
786
786
|
indicatorId,
|
|
@@ -791,23 +791,23 @@ function syncSubPanesFromChart(): void {
|
|
|
791
791
|
scheduleRender()
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
//
|
|
794
|
+
// 切换副图指标(使用 Chart API)
|
|
795
795
|
function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
|
|
796
796
|
const pane = subPanes.value.find((p) => p.id === paneId)
|
|
797
797
|
if (!pane) return
|
|
798
798
|
|
|
799
799
|
const nextParams = getDefaultParams(newIndicatorId)
|
|
800
800
|
|
|
801
|
-
//
|
|
801
|
+
// 移除旧的 paneTitle 渲染器
|
|
802
802
|
unmountSubPaneTitle(paneId)
|
|
803
803
|
|
|
804
|
-
//
|
|
804
|
+
// 使用 Chart API 替换副图指标(paneId 不变,只换指标类型)
|
|
805
805
|
chartRef.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
|
|
806
806
|
|
|
807
|
-
//
|
|
807
|
+
// 创建新的 paneTitle 渲染器
|
|
808
808
|
mountSubPaneTitle(paneId, newIndicatorId)
|
|
809
809
|
|
|
810
|
-
//
|
|
810
|
+
// 更新本地状态(paneId 保持不变)
|
|
811
811
|
const index = subPanes.value.findIndex((p) => p.id === paneId)
|
|
812
812
|
if (index !== -1) {
|
|
813
813
|
subPanes.value[index] = {
|
|
@@ -818,7 +818,7 @@ function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): v
|
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
-
//
|
|
821
|
+
// 获取副图标题信息(带缓存,只在 crosshairIdx 或 data 变化时重算)
|
|
822
822
|
const _titleInfoCache = new Map<
|
|
823
823
|
string,
|
|
824
824
|
{ idx: number | null; dataLen: number; result: TitleInfo | null }
|
|
@@ -834,7 +834,7 @@ function getSubPaneTitleInfo(paneId: string): TitleInfo | null {
|
|
|
834
834
|
const idx = crosshairIdx.value
|
|
835
835
|
const dataLen = data.length
|
|
836
836
|
|
|
837
|
-
//
|
|
837
|
+
// 缓存命中:crosshairIdx 和 dataLen 都没变
|
|
838
838
|
const cached = _titleInfoCache.get(paneId)
|
|
839
839
|
if (cached && cached.idx === idx && cached.dataLen === dataLen) {
|
|
840
840
|
return cached.result
|
|
@@ -849,12 +849,12 @@ function getSubPaneTitleInfo(paneId: string): TitleInfo | null {
|
|
|
849
849
|
return result
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
//
|
|
852
|
+
// 指标切换处理(使用高层 Facade API)
|
|
853
853
|
function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
854
854
|
const chart = chartRef.value
|
|
855
855
|
if (!chart) return
|
|
856
856
|
|
|
857
|
-
//
|
|
857
|
+
// 主图指标处理
|
|
858
858
|
const mainIndicatorIds = [
|
|
859
859
|
'MA',
|
|
860
860
|
'BOLL',
|
|
@@ -879,11 +879,11 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
|
879
879
|
const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
|
|
880
880
|
|
|
881
881
|
if (active && !existingIndicator) {
|
|
882
|
-
//
|
|
882
|
+
// 添加主图指标
|
|
883
883
|
chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
|
|
884
884
|
mainActiveIndicators.value.push(indicatorId)
|
|
885
885
|
} else if (!active && existingIndicator) {
|
|
886
|
-
//
|
|
886
|
+
// 移除主图指标
|
|
887
887
|
const instanceId = indicatorId.toUpperCase()
|
|
888
888
|
chart.removeIndicator(instanceId)
|
|
889
889
|
mainActiveIndicators.value = mainActiveIndicators.value.filter((id) => id !== indicatorId)
|
|
@@ -891,34 +891,34 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
|
891
891
|
return
|
|
892
892
|
}
|
|
893
893
|
|
|
894
|
-
//
|
|
894
|
+
// 副图指标处理
|
|
895
895
|
if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
|
|
896
896
|
if (active) {
|
|
897
|
-
//
|
|
897
|
+
// 如果已存在同类型指标 pane,跳过
|
|
898
898
|
const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
|
|
899
899
|
if (existingPane) return
|
|
900
900
|
|
|
901
|
-
//
|
|
901
|
+
// 副图数量上限检查
|
|
902
902
|
if (subPanes.value.length >= maxSubPanes) return
|
|
903
903
|
|
|
904
|
-
//
|
|
904
|
+
// 使用高层 API 添加副图指标
|
|
905
905
|
const paneId = chart.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
|
|
906
906
|
if (paneId) {
|
|
907
|
-
//
|
|
907
|
+
// 创建 paneTitle 渲染器
|
|
908
908
|
mountSubPaneTitle(paneId, indicatorId as SubIndicatorType)
|
|
909
|
-
//
|
|
909
|
+
// 同步本地状态
|
|
910
910
|
subPanes.value.push({
|
|
911
911
|
id: paneId,
|
|
912
912
|
indicatorId: indicatorId as SubIndicatorType,
|
|
913
913
|
params: { ...indicatorParams.value[indicatorId] },
|
|
914
914
|
})
|
|
915
915
|
} else if (subPanes.value.length > 0) {
|
|
916
|
-
//
|
|
916
|
+
// 添加失败(可能达到上限),替换最后一个
|
|
917
917
|
const lastPane = subPanes.value[subPanes.value.length - 1]
|
|
918
918
|
switchSubIndicator(lastPane.id, indicatorId as SubIndicatorType)
|
|
919
919
|
}
|
|
920
920
|
} else {
|
|
921
|
-
//
|
|
921
|
+
// 找到并移除该指标的所有 pane
|
|
922
922
|
const panesToRemove = subPanes.value.filter((p) => p.indicatorId === indicatorId)
|
|
923
923
|
panesToRemove.forEach((pane) => {
|
|
924
924
|
chart.removeIndicator(pane.id)
|
|
@@ -930,7 +930,7 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
|
|
|
930
930
|
}
|
|
931
931
|
}
|
|
932
932
|
|
|
933
|
-
//
|
|
933
|
+
// 更新主图指标图例配置
|
|
934
934
|
function updateMainIndicatorLegendConfig() {
|
|
935
935
|
chartRef.value?.updateRendererConfig('mainIndicatorLegend', {
|
|
936
936
|
indicators: {
|
|
@@ -954,12 +954,12 @@ function updateMainIndicatorLegendConfig() {
|
|
|
954
954
|
})
|
|
955
955
|
}
|
|
956
956
|
|
|
957
|
-
//
|
|
957
|
+
// 指标参数更新处理
|
|
958
958
|
function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
|
|
959
|
-
//
|
|
959
|
+
// 保存参数配置
|
|
960
960
|
indicatorParams.value[indicatorId] = params
|
|
961
961
|
|
|
962
|
-
//
|
|
962
|
+
// 主图指标参数更新 - 使用Chart API
|
|
963
963
|
if (
|
|
964
964
|
indicatorId === 'MA' ||
|
|
965
965
|
indicatorId === 'BOLL' ||
|
|
@@ -1022,21 +1022,21 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
|
|
|
1022
1022
|
|
|
1023
1023
|
subPanes.value = nextSubPanes
|
|
1024
1024
|
|
|
1025
|
-
// activeIndicators
|
|
1025
|
+
// activeIndicators 由 computed 自动派生,无需手动同步
|
|
1026
1026
|
|
|
1027
1027
|
const chart = chartRef.value
|
|
1028
1028
|
if (!chart) return
|
|
1029
1029
|
chart.updatePaneLayout(buildPaneLayoutIntent())
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
|
-
/*
|
|
1032
|
+
/* 计算总宽度:从 Vue 响应式状态读取,zoom 变化时自动重算 */
|
|
1033
1033
|
const axisHostWidth = computed(() => props.rightAxisWidth + props.priceLabelWidth)
|
|
1034
1034
|
|
|
1035
1035
|
const TRAILING_DRAWING_SLOTS_VAL = TRAILING_DRAWING_SLOTS
|
|
1036
1036
|
|
|
1037
1037
|
const totalWidth = store.computed.totalWidth
|
|
1038
1038
|
|
|
1039
|
-
//
|
|
1039
|
+
// 缩放由 Chart 回调驱动 scrollLeft 与渲染时序。
|
|
1040
1040
|
|
|
1041
1041
|
function scrollToRight() {
|
|
1042
1042
|
const container = containerRef.value
|
|
@@ -1049,13 +1049,13 @@ function scrollToRight() {
|
|
|
1049
1049
|
const dpr = chart.getCurrentDpr()
|
|
1050
1050
|
const { unitPx, startXPx } = getPhysicalKLineConfig(kWidth.value, kGap.value, dpr)
|
|
1051
1051
|
|
|
1052
|
-
//
|
|
1052
|
+
// 计算最后一根K线的结束位置(不含 TRAILING_DRAWING_SLOTS)
|
|
1053
1053
|
const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
|
|
1054
1054
|
|
|
1055
|
-
//
|
|
1055
|
+
// 计算最大可滚动距离
|
|
1056
1056
|
const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth)
|
|
1057
1057
|
|
|
1058
|
-
//
|
|
1058
|
+
// 计算需要的滚动位置,使最后一根K线紧贴最右侧
|
|
1059
1059
|
const targetScrollLeft = Math.min(
|
|
1060
1060
|
maxScrollLeft,
|
|
1061
1061
|
Math.max(0, lastKLineEndPx - container.clientWidth),
|
|
@@ -1065,7 +1065,7 @@ function scrollToRight() {
|
|
|
1065
1065
|
scheduleRender()
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
-
/*
|
|
1068
|
+
/* 缩放到指定级别(通过 Chart facade API) */
|
|
1069
1069
|
function applyZoomToLevel(targetLevel: number, anchorX?: number) {
|
|
1070
1070
|
const chart = chartRef.value
|
|
1071
1071
|
if (!chart) return
|
|
@@ -1083,7 +1083,7 @@ defineExpose({
|
|
|
1083
1083
|
return chartRef.value?.plugin
|
|
1084
1084
|
},
|
|
1085
1085
|
|
|
1086
|
-
// Zoom Level API
|
|
1086
|
+
// Zoom Level API(Vue SSOT)
|
|
1087
1087
|
zoomToLevel: applyZoomToLevel,
|
|
1088
1088
|
zoomIn: (anchorX?: number) => applyZoomToLevel(zoomLevel.value + 1, anchorX),
|
|
1089
1089
|
zoomOut: (anchorX?: number) => applyZoomToLevel(zoomLevel.value - 1, anchorX),
|
|
@@ -1091,7 +1091,7 @@ defineExpose({
|
|
|
1091
1091
|
getZoomLevelCount: () => chartRef.value?.getZoomLevelCount() ?? 10,
|
|
1092
1092
|
})
|
|
1093
1093
|
|
|
1094
|
-
// ==================== onMounted
|
|
1094
|
+
// ==================== onMounted 拆分函数 ====================
|
|
1095
1095
|
|
|
1096
1096
|
function setupWheelHandler(container: HTMLDivElement): (e: WheelEvent) => void {
|
|
1097
1097
|
const onWheelHandler = (e: WheelEvent) => {
|
|
@@ -1099,7 +1099,7 @@ function setupWheelHandler(container: HTMLDivElement): (e: WheelEvent) => void {
|
|
|
1099
1099
|
const chart = chartRef.value
|
|
1100
1100
|
if (!chart) return
|
|
1101
1101
|
|
|
1102
|
-
//
|
|
1102
|
+
// 使用 Chart facade API 处理滚轮事件
|
|
1103
1103
|
chart.handleWheelEvent(e)
|
|
1104
1104
|
}
|
|
1105
1105
|
container.addEventListener('wheel', onWheelHandler, { passive: false })
|
|
@@ -1131,10 +1131,10 @@ function initChart(
|
|
|
1131
1131
|
}
|
|
1132
1132
|
|
|
1133
1133
|
function setupChartCallbacks(chart: Chart): void {
|
|
1134
|
-
//
|
|
1134
|
+
// 注意:setOnViewportChange 已合并到 viewport signal 订阅者中
|
|
1135
1135
|
|
|
1136
1136
|
chart.setOnPaneLayoutChange(() => {
|
|
1137
|
-
//
|
|
1137
|
+
// 分隔线位置计算(需要实际像素位置,保留在回调中)
|
|
1138
1138
|
invalidateContainerRectCache()
|
|
1139
1139
|
const renderers = chart.getPaneRenderers()
|
|
1140
1140
|
const borderTop = containerRef.value
|
|
@@ -1149,27 +1149,27 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1149
1149
|
})
|
|
1150
1150
|
})
|
|
1151
1151
|
|
|
1152
|
-
//
|
|
1152
|
+
// 订阅 paneRatios signal,同步到 Vue store
|
|
1153
1153
|
const unsubscribePaneRatios = chart.paneRatios.subscribe(() => {
|
|
1154
1154
|
const ratios = chart.paneRatios.peek()
|
|
1155
1155
|
store.actions.setPaneRatios({ ...ratios })
|
|
1156
1156
|
})
|
|
1157
1157
|
|
|
1158
|
-
//
|
|
1158
|
+
// 订阅 viewport signal,处理缩放、DPR、width 变化和 scrollLeft 更新
|
|
1159
1159
|
const unsubscribeViewport = chart.viewport.subscribe(() => {
|
|
1160
1160
|
const vp = chart.viewport.peek()
|
|
1161
1161
|
|
|
1162
|
-
// DPR
|
|
1162
|
+
// DPR 变化时同步到 store
|
|
1163
1163
|
if (store.state.viewportDpr !== vp.dpr) {
|
|
1164
1164
|
store.actions.setViewportDpr(vp.dpr)
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
-
// ViewWidth
|
|
1167
|
+
// ViewWidth 变化时同步到 store
|
|
1168
1168
|
if (store.state.viewWidth !== vp.plotWidth) {
|
|
1169
1169
|
store.actions.setViewWidth(vp.plotWidth)
|
|
1170
1170
|
}
|
|
1171
1171
|
|
|
1172
|
-
//
|
|
1172
|
+
// 完整同步 zoom state 到 Vue store(Chart 是 SSOT)
|
|
1173
1173
|
if (
|
|
1174
1174
|
store.state.zoomLevel !== vp.zoomLevel ||
|
|
1175
1175
|
store.state.kWidth !== vp.kWidth ||
|
|
@@ -1178,7 +1178,7 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1178
1178
|
store.actions.setZoomState(vp.zoomLevel, vp.kWidth, vp.kGap)
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
-
//
|
|
1181
|
+
// 在 nextTick 中应用 desiredScrollLeft
|
|
1182
1182
|
const desiredLeft = vp.desiredScrollLeft
|
|
1183
1183
|
if (desiredLeft !== undefined && desiredLeft !== containerRef.value?.scrollLeft) {
|
|
1184
1184
|
invalidateContainerRectCache()
|
|
@@ -1193,20 +1193,20 @@ function setupChartCallbacks(chart: Chart): void {
|
|
|
1193
1193
|
}
|
|
1194
1194
|
})
|
|
1195
1195
|
|
|
1196
|
-
//
|
|
1196
|
+
// 订阅 data signal,替换 onDataChange 回调
|
|
1197
1197
|
const unsubscribeData = chart.data.subscribe(() => {
|
|
1198
1198
|
const data = chart.data.peek()
|
|
1199
1199
|
store.actions.setDataLength(data.length)
|
|
1200
1200
|
store.actions.bumpDataVersion()
|
|
1201
1201
|
})
|
|
1202
1202
|
|
|
1203
|
-
//
|
|
1203
|
+
// 订阅 theme signal,同步到 CSS data-theme
|
|
1204
1204
|
const unsubscribeTheme = chart.theme.subscribe(() => {
|
|
1205
1205
|
const theme = chart.theme.peek()
|
|
1206
1206
|
chartTheme.value = theme
|
|
1207
1207
|
})
|
|
1208
1208
|
|
|
1209
|
-
//
|
|
1209
|
+
// 保存 unsubscribe 函数以便清理
|
|
1210
1210
|
onUnmounted(() => {
|
|
1211
1211
|
unsubscribeViewport()
|
|
1212
1212
|
unsubscribeData()
|
|
@@ -1251,7 +1251,7 @@ function setupInteractionCallbacks(chart: Chart): void {
|
|
|
1251
1251
|
if (!chart) return
|
|
1252
1252
|
const container = containerRef.value
|
|
1253
1253
|
if (!container) return
|
|
1254
|
-
// centerClientX
|
|
1254
|
+
// centerClientX 是 clientX,需要转换为视口局部坐标
|
|
1255
1255
|
const rect = container.getBoundingClientRect()
|
|
1256
1256
|
const centerX = centerClientX - rect.left
|
|
1257
1257
|
chart.handlePinchZoom(delta, centerX)
|
|
@@ -1262,7 +1262,7 @@ function setupInteractionCallbacks(chart: Chart): void {
|
|
|
1262
1262
|
chart.resize()
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
1265
|
-
/**
|
|
1265
|
+
/** 语义化控制器:外部配置 → Chart API 的桥梁 */
|
|
1266
1266
|
function setupSemanticController(chart: Chart): void {
|
|
1267
1267
|
__setDataFetcher(props.dataFetcher)
|
|
1268
1268
|
semanticController.value = new SemanticChartController(chart)
|
|
@@ -1271,13 +1271,13 @@ function setupSemanticController(chart: Chart): void {
|
|
|
1271
1271
|
console.error('Semantic config error:', error)
|
|
1272
1272
|
})
|
|
1273
1273
|
|
|
1274
|
-
// config:ready
|
|
1274
|
+
// config:ready → Chart 侧已完成创建,Vue 回读状态
|
|
1275
1275
|
semanticController.value.on('config:ready', () => {
|
|
1276
1276
|
initIndicatorsFromConfig()
|
|
1277
1277
|
syncSubPanesFromChart()
|
|
1278
1278
|
nextTick(() => scrollToRight())
|
|
1279
1279
|
})
|
|
1280
|
-
//
|
|
1280
|
+
// 应用副图、主图配置
|
|
1281
1281
|
semanticController.value.applyConfig(props.semanticConfig).then((result) => {
|
|
1282
1282
|
if (result && !result.success) {
|
|
1283
1283
|
console.error('Semantic config apply failed:', result.errors)
|
|
@@ -1294,32 +1294,32 @@ onMounted(() => {
|
|
|
1294
1294
|
const xAxisCanvas = xAxisCanvasRef.value
|
|
1295
1295
|
if (!container || !canvasLayer || !rightAxisLayer || !xAxisCanvas) return
|
|
1296
1296
|
|
|
1297
|
-
// 1)
|
|
1297
|
+
// 1) 滚轮缩放:passive:false 以阻止页面滚动
|
|
1298
1298
|
const onWheelHandler = setupWheelHandler(container)
|
|
1299
1299
|
|
|
1300
|
-
// 2)
|
|
1300
|
+
// 2) 创建 Chart 实例并注册全部渲染器
|
|
1301
1301
|
const chart = initChart(container, canvasLayer, rightAxisLayer, xAxisCanvas)
|
|
1302
1302
|
chartRef.value = chart
|
|
1303
1303
|
|
|
1304
|
-
// 3)
|
|
1304
|
+
// 3) 视口 / 面板布局 / 数据变更回调
|
|
1305
1305
|
setupChartCallbacks(chart)
|
|
1306
1306
|
|
|
1307
|
-
// 4)
|
|
1307
|
+
// 4) 同步 zoom 状态(Vue SSOT → Chart)
|
|
1308
1308
|
chart.applyRenderState(store.state.kWidth, store.state.kGap, store.state.zoomLevel)
|
|
1309
1309
|
|
|
1310
|
-
// 5)
|
|
1310
|
+
// 5) 工具栏初始设置(含性能测试数据)
|
|
1311
1311
|
applyInitialSettings(chart)
|
|
1312
1312
|
|
|
1313
|
-
// 6)
|
|
1313
|
+
// 6) 绘图交互控制器(线段/箭头等)
|
|
1314
1314
|
setupDrawingController(chart)
|
|
1315
1315
|
|
|
1316
|
-
// 7)
|
|
1316
|
+
// 7) 十字线、捏合缩放、初始交互快照
|
|
1317
1317
|
setupInteractionCallbacks(chart)
|
|
1318
1318
|
|
|
1319
|
-
// 8)
|
|
1319
|
+
// 8) 语义化配置控制器(最终驱动数据加载)
|
|
1320
1320
|
setupSemanticController(chart)
|
|
1321
1321
|
|
|
1322
|
-
//
|
|
1322
|
+
// 供 onUnmounted 移除 wheel 监听
|
|
1323
1323
|
;(chart as any).__onWheel = onWheelHandler
|
|
1324
1324
|
})
|
|
1325
1325
|
|
|
@@ -1337,10 +1337,10 @@ onUnmounted(() => {
|
|
|
1337
1337
|
drawingController.value = null
|
|
1338
1338
|
})
|
|
1339
1339
|
|
|
1340
|
-
// kWidth/kGap
|
|
1341
|
-
//
|
|
1340
|
+
// kWidth/kGap 由 zoomLevel 派生,不再通过 props 直接修改
|
|
1341
|
+
// 如需程序化控制缩放,请使用 expose 的 zoomToLevel/zoomIn/zoomOut 方法
|
|
1342
1342
|
|
|
1343
|
-
//
|
|
1343
|
+
// 监听 yPaddingPx 变化
|
|
1344
1344
|
watch(
|
|
1345
1345
|
() => props.yPaddingPx,
|
|
1346
1346
|
(newVal) => {
|
|
@@ -1348,7 +1348,7 @@ watch(
|
|
|
1348
1348
|
},
|
|
1349
1349
|
)
|
|
1350
1350
|
|
|
1351
|
-
//
|
|
1351
|
+
// 监听 semanticConfig 变化(唯一数据源)
|
|
1352
1352
|
watch(
|
|
1353
1353
|
() => props.semanticConfig,
|
|
1354
1354
|
async (newConfig, oldConfig) => {
|