@363045841yyt/klinechart 0.7.5-alpha.2 → 0.7.5

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.
@@ -118,7 +118,7 @@ import KLineTooltip from './KLineTooltip.vue'
118
118
  import MarkerTooltip from './MarkerTooltip.vue'
119
119
  import IndicatorSelector from './IndicatorSelector.vue'
120
120
  import DrawingStyleToolbar from './DrawingStyleToolbar.vue'
121
- import { Chart, type PaneSpec } from '@363045841yyt/klinechart-core/engine/chart'
121
+ import { Chart, type PaneSpec, type IndicatorInstance, type SubPaneInfo } from '@363045841yyt/klinechart-core/engine/chart'
122
122
  import type { KLineData } from '@363045841yyt/klinechart-core/types/price'
123
123
  import {
124
124
  createChartStore,
@@ -639,40 +639,23 @@ function addSubPane(
639
639
 
640
640
  const mergedParams = params ?? getDefaultParams(indicatorId)
641
641
 
642
- // 使用高层 Facade API 创建副图指标
642
+ // 使用高层 Facade API 创建副图指标(Signal 订阅自动同步本地状态和 scheduleDraw)
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
- // 更新本地状态
650
- subPanes.value.push({
651
- id: paneId,
652
- indicatorId,
653
- params: mergedParams,
654
- })
655
-
656
- scheduleRender()
657
649
  return true
658
650
  }
659
651
 
660
652
  // 移除副图(使用高层 Facade API)
661
653
  function removeSubPane(paneId: string): void {
662
- const index = subPanes.value.findIndex((p) => p.id === paneId)
663
- if (index === -1) return
664
-
665
- const pane = subPanes.value[index]
666
- if (!pane) return
667
-
668
654
  // 移除 paneTitle 渲染器
669
655
  unmountSubPaneTitle(paneId)
670
656
 
671
- // 使用高层 Facade API 移除指标
657
+ // 使用高层 Facade API 移除指标(Signal 订阅自动同步本地状态)
672
658
  chartRef.value?.removeIndicator(paneId)
673
-
674
- // 更新本地状态
675
- subPanes.value.splice(index, 1)
676
659
  }
677
660
 
678
661
  // 清除所有副图(使用高层 Facade API)
@@ -683,32 +666,22 @@ function clearAllSubPanes(): void {
683
666
  unmountSubPaneTitle(pane.id)
684
667
  }
685
668
 
686
- // 清空本地状态
687
- subPanes.value = []
669
+ // 清空本地状态(Signal 订阅自动同步 subPanes,只需要清理 UI 层状态)
688
670
  subPaneCounters.clear()
689
671
  paneTitleRendererNames.clear()
690
672
  }
691
673
 
692
674
  // 从语义化配置初始化指标状态(单向数据流:config → chart)
675
+ // Signal 订阅会自动同步本地状态,此处只需调用 Chart API
693
676
  function initIndicatorsFromConfig(): void {
694
677
  const config = props.semanticConfig
695
678
  const chart = chartRef.value
696
679
  if (!chart) return
697
680
 
698
- // 初始化主图指标 - 直接调用Chart API
699
681
  const mainIndicators = config.indicators?.main
700
682
  if (mainIndicators) {
701
683
  for (const indicator of mainIndicators) {
702
684
  if (indicator.enabled) {
703
- // 同步Vue状态(用于UI展示)
704
- if (!mainActiveIndicators.value.includes(indicator.type)) {
705
- mainActiveIndicators.value.push(indicator.type)
706
- }
707
- // 保存参数
708
- if (indicator.params) {
709
- indicatorParams.value[indicator.type] = indicator.params as Record<string, unknown>
710
- }
711
- // 启用指标(Chart内部管理渲染器)
712
685
  chart.enableMainIndicator(
713
686
  indicator.type,
714
687
  indicator.params as Record<string, number | boolean | string>,
@@ -716,51 +689,12 @@ function initIndicatorsFromConfig(): void {
716
689
  }
717
690
  }
718
691
  }
719
-
720
- // 副图指标参数由 syncSubPanesFromChart 处理
721
692
  }
722
693
 
723
- // 监听主图指标参数变化,同步到Chart(状态由Chart管理,Vue只同步参数)
724
- watch(
725
- [activeIndicators, indicatorParams],
726
- ([indicators]) => {
727
- const chart = chartRef.value
728
- if (!chart) return
729
-
730
- // 只更新mainIndicatorLegend的配置(用于图例显示)
731
- // 渲染器的启用/禁用由Chart内部管理
732
- chart.updateRendererConfig('mainIndicatorLegend', {
733
- indicators: {
734
- MA: {
735
- enabled: indicators.includes('MA'),
736
- params: indicatorParams.value['MA'] || {},
737
- },
738
- BOLL: {
739
- enabled: indicators.includes('BOLL'),
740
- params: indicatorParams.value['BOLL'] || {},
741
- },
742
- EXPMA: {
743
- enabled: indicators.includes('EXPMA'),
744
- params: indicatorParams.value['EXPMA'] || {},
745
- },
746
- ENE: {
747
- enabled: indicators.includes('ENE'),
748
- params: indicatorParams.value['ENE'] || {},
749
- },
750
- },
751
- })
752
-
753
- scheduleRender()
754
- },
755
- { deep: true },
756
- )
757
-
758
694
  // 从 Chart 同步副图状态到本地(语义化配置后调用)
759
695
  function syncSubPanesFromChart(): void {
760
696
  const chartSubPaneEntries = chartRef.value?.getSubPaneEntries() ?? []
761
697
 
762
- // 清空本地状态
763
- subPanes.value = []
764
698
  paneTitleRendererNames.clear()
765
699
 
766
700
  for (const entry of chartSubPaneEntries) {
@@ -779,43 +713,21 @@ function syncSubPanesFromChart(): void {
779
713
 
780
714
  // 创建 paneTitle 渲染器
781
715
  mountSubPaneTitle(paneId, indicatorId)
782
-
783
- // 更新本地状态
784
- subPanes.value.push({
785
- id: paneId,
786
- indicatorId,
787
- params: { ...params },
788
- })
789
716
  }
790
-
791
- scheduleRender()
792
717
  }
793
718
 
794
719
  // 切换副图指标(使用 Chart API)
795
720
  function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
796
- const pane = subPanes.value.find((p) => p.id === paneId)
797
- if (!pane) return
798
-
799
721
  const nextParams = getDefaultParams(newIndicatorId)
800
722
 
801
723
  // 移除旧的 paneTitle 渲染器
802
724
  unmountSubPaneTitle(paneId)
803
725
 
804
- // 使用 Chart API 替换副图指标(paneId 不变,只换指标类型)
726
+ // 使用 Chart API 替换副图指标(paneId 不变,只换指标类型,Signal 订阅自动同步本地状态)
805
727
  chartRef.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
806
728
 
807
729
  // 创建新的 paneTitle 渲染器
808
730
  mountSubPaneTitle(paneId, newIndicatorId)
809
-
810
- // 更新本地状态(paneId 保持不变)
811
- const index = subPanes.value.findIndex((p) => p.id === paneId)
812
- if (index !== -1) {
813
- subPanes.value[index] = {
814
- id: paneId,
815
- indicatorId: newIndicatorId,
816
- params: nextParams,
817
- }
818
- }
819
731
  }
820
732
 
821
733
  // 获取副图标题信息(带缓存,只在 crosshairIdx 或 data 变化时重算)
@@ -879,14 +791,12 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
879
791
  const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
880
792
 
881
793
  if (active && !existingIndicator) {
882
- // 添加主图指标
794
+ // 添加主图指标(Signal 订阅自动同步本地状态)
883
795
  chart.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
884
- mainActiveIndicators.value.push(indicatorId)
885
796
  } else if (!active && existingIndicator) {
886
- // 移除主图指标
797
+ // 移除主图指标(Signal 订阅自动同步本地状态)
887
798
  const instanceId = indicatorId.toUpperCase()
888
799
  chart.removeIndicator(instanceId)
889
- mainActiveIndicators.value = mainActiveIndicators.value.filter((id) => id !== indicatorId)
890
800
  }
891
801
  return
892
802
  }
@@ -901,17 +811,10 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
901
811
  // 副图数量上限检查
902
812
  if (subPanes.value.length >= maxSubPanes) return
903
813
 
904
- // 使用高层 API 添加副图指标
814
+ // 使用高层 API 添加副图指标(Signal 订阅自动同步本地状态)
905
815
  const paneId = chart.addIndicator(indicatorId, 'sub', indicatorParams.value[indicatorId])
906
816
  if (paneId) {
907
- // 创建 paneTitle 渲染器
908
817
  mountSubPaneTitle(paneId, indicatorId as SubIndicatorType)
909
- // 同步本地状态
910
- subPanes.value.push({
911
- id: paneId,
912
- indicatorId: indicatorId as SubIndicatorType,
913
- params: { ...indicatorParams.value[indicatorId] },
914
- })
915
818
  } else if (subPanes.value.length > 0) {
916
819
  // 添加失败(可能达到上限),替换最后一个
917
820
  const lastPane = subPanes.value[subPanes.value.length - 1]
@@ -924,42 +827,13 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
924
827
  chart.removeIndicator(pane.id)
925
828
  unmountSubPaneTitle(pane.id)
926
829
  })
927
- subPanes.value = subPanes.value.filter((p) => p.indicatorId !== indicatorId)
928
830
  }
929
- scheduleRender()
930
831
  }
931
832
  }
932
833
 
933
- // 更新主图指标图例配置
934
- function updateMainIndicatorLegendConfig() {
935
- chartRef.value?.updateRendererConfig('mainIndicatorLegend', {
936
- indicators: {
937
- MA: {
938
- enabled: activeIndicators.value.includes('MA'),
939
- params: indicatorParams.value['MA'] || {},
940
- },
941
- BOLL: {
942
- enabled: activeIndicators.value.includes('BOLL'),
943
- params: indicatorParams.value['BOLL'] || {},
944
- },
945
- EXPMA: {
946
- enabled: activeIndicators.value.includes('EXPMA'),
947
- params: indicatorParams.value['EXPMA'] || {},
948
- },
949
- ENE: {
950
- enabled: activeIndicators.value.includes('ENE'),
951
- params: indicatorParams.value['ENE'] || {},
952
- },
953
- },
954
- })
955
- }
956
-
957
834
  // 指标参数更新处理
958
835
  function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
959
- // 保存参数配置
960
- indicatorParams.value[indicatorId] = params
961
-
962
- // 主图指标参数更新 - 使用Chart API
836
+ // 主图指标参数更新 - 使用Chart API(Signal 订阅自动同步本地状态和 scheduleDraw)
963
837
  if (
964
838
  indicatorId === 'MA' ||
965
839
  indicatorId === 'BOLL' ||
@@ -970,7 +844,6 @@ function handleUpdateParams(indicatorId: string, params: Record<string, unknown>
970
844
  indicatorId,
971
845
  params as Record<string, number | boolean | string>,
972
846
  )
973
- scheduleRender()
974
847
  return
975
848
  }
976
849
 
@@ -979,13 +852,9 @@ function handleUpdateParams(indicatorId: string, params: Record<string, unknown>
979
852
  .filter((p) => p.indicatorId === indicatorId)
980
853
  .forEach((pane) => {
981
854
  chartRef.value?.updateSubPaneParams(pane.id, params)
982
- pane.params = { ...params }
983
855
  })
984
- scheduleRender()
985
856
  return
986
857
  }
987
-
988
- scheduleRender()
989
858
  }
990
859
 
991
860
  function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
@@ -1206,12 +1075,75 @@ function setupChartCallbacks(chart: Chart): void {
1206
1075
  chartTheme.value = theme
1207
1076
  })
1208
1077
 
1078
+ // 订阅 indicators signal,派生 Vue 本地状态(SSOT: Chart 引擎)
1079
+ const unsubscribeIndicators = chart.indicators.subscribe(() => {
1080
+ const instances = chart.indicators.peek()
1081
+
1082
+ // 同步主图指标列表
1083
+ const mains = instances
1084
+ .filter((i): i is IndicatorInstance & { role: 'main' } => i.role === 'main')
1085
+ .map((i) => i.definitionId)
1086
+ mainActiveIndicators.value = mains
1087
+
1088
+ // 合并主图指标参数(不覆盖副图参数)
1089
+ const nextParams = { ...indicatorParams.value }
1090
+ for (const inst of instances) {
1091
+ if (inst.role === 'main' && inst.params && Object.keys(inst.params).length > 0) {
1092
+ nextParams[inst.definitionId] = { ...inst.params }
1093
+ }
1094
+ }
1095
+
1096
+ // 更新主图指标图例配置
1097
+ chart.updateRendererConfig('mainIndicatorLegend', {
1098
+ indicators: {
1099
+ MA: { enabled: mains.includes('MA'), params: nextParams['MA'] || {} },
1100
+ BOLL: { enabled: mains.includes('BOLL'), params: nextParams['BOLL'] || {} },
1101
+ EXPMA: { enabled: mains.includes('EXPMA'), params: nextParams['EXPMA'] || {} },
1102
+ ENE: { enabled: mains.includes('ENE'), params: nextParams['ENE'] || {} },
1103
+ },
1104
+ })
1105
+
1106
+ indicatorParams.value = nextParams
1107
+ })
1108
+
1109
+ // 订阅 subPanes signal,派生 Vue 本地状态
1110
+ // 注意:保持当前显示顺序(reorder 是 UI 层私有状态),仅同步新增/删除
1111
+ const unsubscribeSubPanes = chart.subPanes.subscribe(() => {
1112
+ const subPaneInfos = chart.subPanes.peek()
1113
+ const signalIds = new Set(subPaneInfos.map((sp) => sp.paneId))
1114
+
1115
+ // 保留 display order,移除已删除的 pane,追加新增的
1116
+ const merged = subPanes.value.filter((p) => signalIds.has(p.id))
1117
+ const existingIds = new Set(merged.map((p) => p.id))
1118
+ for (const sp of subPaneInfos) {
1119
+ if (!existingIds.has(sp.paneId)) {
1120
+ merged.push({
1121
+ id: sp.paneId,
1122
+ indicatorId: sp.indicatorId as SubIndicatorType,
1123
+ params: sp.params,
1124
+ })
1125
+ }
1126
+ }
1127
+ subPanes.value = merged
1128
+
1129
+ // 合并副图指标参数(不覆盖主图参数)
1130
+ const nextParams = { ...indicatorParams.value }
1131
+ for (const sp of subPaneInfos) {
1132
+ if (sp.params && Object.keys(sp.params).length > 0) {
1133
+ nextParams[sp.indicatorId] = { ...sp.params }
1134
+ }
1135
+ }
1136
+ indicatorParams.value = nextParams
1137
+ })
1138
+
1209
1139
  // 保存 unsubscribe 函数以便清理
1210
1140
  onUnmounted(() => {
1211
1141
  unsubscribeViewport()
1212
1142
  unsubscribeData()
1213
1143
  unsubscribePaneRatios()
1214
1144
  unsubscribeTheme()
1145
+ unsubscribeIndicators()
1146
+ unsubscribeSubPanes()
1215
1147
  })
1216
1148
  }
1217
1149
 
package/src/index.ts CHANGED
@@ -29,10 +29,14 @@ import type {
29
29
  ChartControllerFactory,
30
30
  ChartMountOptions,
31
31
  ChartViewport,
32
+ IndicatorDefinition,
32
33
  IndicatorInstance,
33
34
  InteractionSnapshot,
34
35
  KLineData,
35
36
  } from '@363045841yyt/klinechart-core'
37
+ import {
38
+ createIndicatorSelectorController,
39
+ } from '@363045841yyt/klinechart-core'
36
40
 
37
41
  export type {
38
42
  ChartController,
@@ -252,6 +256,74 @@ export function useViewport(
252
256
  return vp
253
257
  }
254
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // useIndicatorSelector — composable
261
+ // ---------------------------------------------------------------------------
262
+
263
+ /**
264
+ * Bridge the indicator selector signals into Vue refs.
265
+ *
266
+ * Creates an internal IndicatorSelectorController for menu/search/filter UI
267
+ * state (catalog from `controller.catalog`), and delegates add/remove to the
268
+ * ChartController engine methods.
269
+ */
270
+ export function useIndicatorSelector(controller: ChartController): {
271
+ catalog: ReadonlyArray<IndicatorDefinition>
272
+ filteredMain: Ref<ReadonlyArray<IndicatorDefinition>>
273
+ filteredSub: Ref<ReadonlyArray<IndicatorDefinition>>
274
+ menuOpen: Ref<boolean>
275
+ searchQuery: Ref<string>
276
+ add: (definitionId: string) => string | null
277
+ remove: (instanceId: string) => boolean
278
+ openMenu: () => void
279
+ closeMenu: () => void
280
+ toggleMenu: () => void
281
+ setSearchQuery: (q: string) => void
282
+ isActive: (definitionId: string) => boolean
283
+ } {
284
+ const selector = createIndicatorSelectorController({
285
+ catalog: controller.catalog,
286
+ })
287
+
288
+ onScopeDispose(() => selector.dispose())
289
+
290
+ const filteredMain = coreSignalToVueRef(selector.filteredMain)
291
+ const filteredSub = coreSignalToVueRef(selector.filteredSub)
292
+ const menuOpen = coreSignalToVueRef(selector.menuOpen)
293
+ const searchQuery = coreSignalToVueRef(selector.searchQuery)
294
+
295
+ function add(definitionId: string): string | null {
296
+ const def = controller.catalog.find((d) => d.id === definitionId)
297
+ if (def === undefined) return null
298
+ return controller.addIndicator(definitionId, def.role)
299
+ }
300
+
301
+ function remove(instanceId: string): boolean {
302
+ return controller.removeIndicator(instanceId)
303
+ }
304
+
305
+ function isActive(definitionId: string): boolean {
306
+ return controller.indicators
307
+ .peek()
308
+ .some((i) => i.definitionId === definitionId)
309
+ }
310
+
311
+ return {
312
+ catalog: controller.catalog,
313
+ filteredMain,
314
+ filteredSub,
315
+ menuOpen,
316
+ searchQuery,
317
+ add,
318
+ remove,
319
+ openMenu: () => selector.openMenu(),
320
+ closeMenu: () => selector.closeMenu(),
321
+ toggleMenu: () => selector.toggleMenu(),
322
+ setSearchQuery: (q: string) => selector.setSearchQuery(q),
323
+ isActive,
324
+ }
325
+ }
326
+
255
327
  // ---------------------------------------------------------------------------
256
328
  // <KLineChart /> SFC-equivalent component
257
329
  //