@363045841yyt/klinechart 0.8.1-alpha.3 → 0.8.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@363045841yyt/klinechart",
3
- "version": "0.8.1-alpha.3",
3
+ "version": "0.8.1",
4
4
  "description": "Vue 3 bindings for @363045841yyt/klinechart-core. Idiomatic composables, SFC components.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@
11
11
  >
12
12
  <span class="compare-chip__icon" aria-hidden="true">+</span>
13
13
  <span class="compare-chip__text">比较商品</span>
14
+ <span v-if="comparisonLoading" class="compare-chip__spinner" />
14
15
  <span v-if="selected.length > 0" class="compare-chip__badge">{{ selected.length }}</span>
15
16
  </button>
16
17
  <Transition name="symbol-popover">
@@ -65,6 +66,10 @@
65
66
  :key="item.code"
66
67
  class="compare-selected__item"
67
68
  >
69
+ <span
70
+ class="compare-selected__color"
71
+ :style="{ background: comparisonColors?.get(item.code) ?? '#888' }"
72
+ />
68
73
  <span class="compare-selected__code">{{ item.code }}</span>
69
74
  <span class="compare-selected__desc">{{ item.description }}</span>
70
75
  <button
@@ -140,6 +145,8 @@ import type { SymbolItem } from './SymbolSelector.vue'
140
145
  const props = withDefaults(defineProps<{
141
146
  symbols: SymbolItem[]
142
147
  selected?: string[]
148
+ comparisonColors?: Map<string, string>
149
+ comparisonLoading?: boolean
143
150
  }>(), {
144
151
  selected: () => [],
145
152
  })
@@ -280,6 +287,20 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentClick)
280
287
  line-height: 1;
281
288
  }
282
289
 
290
+ .compare-chip__spinner {
291
+ display: inline-block;
292
+ width: 12px;
293
+ height: 12px;
294
+ border: 2px solid var(--klc-color-axis-text);
295
+ border-top-color: transparent;
296
+ border-radius: 50%;
297
+ animation: compare-spin 0.6s linear infinite;
298
+ }
299
+
300
+ @keyframes compare-spin {
301
+ to { transform: rotate(360deg); }
302
+ }
303
+
283
304
  .compare-popover {
284
305
  position: absolute;
285
306
  top: calc(100% + 8px);
@@ -406,6 +427,14 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocumentClick)
406
427
  line-height: 1.3;
407
428
  }
408
429
 
430
+ .compare-selected__color {
431
+ display: inline-block;
432
+ width: 8px;
433
+ height: 8px;
434
+ border-radius: 50%;
435
+ flex-shrink: 0;
436
+ }
437
+
409
438
  .compare-selected__code {
410
439
  font-weight: 600;
411
440
  color: var(--klc-color-foreground);
@@ -6,6 +6,8 @@
6
6
  :symbol-loading="symbolLoading"
7
7
  :symbol-error="symbolError"
8
8
  :overlay-symbols="overlaySymbols"
9
+ :comparison-colors="comparisonColorsMap"
10
+ :comparison-loading="comparisonLoading"
9
11
  @add-overlay-symbol="onAddOverlaySymbol"
10
12
  @remove-overlay-symbol="onRemoveOverlaySymbol"
11
13
  @k-line-level-change="onKLineLevelChange"
@@ -145,7 +147,6 @@ import {
145
147
  type SymbolSpec,
146
148
  zoomLevelToKWidth,
147
149
  kGapFromKWidth,
148
- getPhysicalKLineConfig,
149
150
  DrawingInteractionController,
150
151
  } from '@363045841yyt/klinechart-core/controllers'
151
152
  import {
@@ -153,6 +154,7 @@ import {
153
154
  getRegisteredIndicatorDefinitions,
154
155
  } from '@363045841yyt/klinechart-core/indicators'
155
156
  import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
157
+ import { SETTINGS_STORAGE_KEY } from '@363045841yyt/klinechart-core/config'
156
158
  import type { ChartSettings } from '@363045841yyt/klinechart-core/config'
157
159
  import {
158
160
  resolveThemeColors,
@@ -236,13 +238,13 @@ function onAddOverlaySymbol(item: SymbolItem) {
236
238
  overlaySymbolItems.value = [...overlaySymbolItems.value, item]
237
239
  overlaySymbols.value = overlaySymbolItems.value.map((symbol) => symbol.code)
238
240
  forcePercentAxis()
239
- syncSymbolsToController()
241
+ controller.value?.addComparisonSymbol(toSymbolSpec(item))
240
242
  }
241
243
 
242
244
  function onRemoveOverlaySymbol(code: string) {
243
245
  overlaySymbolItems.value = overlaySymbolItems.value.filter((item) => item.code !== code)
244
246
  overlaySymbols.value = overlaySymbolItems.value.map((symbol) => symbol.code)
245
- syncSymbolsToController()
247
+ controller.value?.removeComparisonSymbol(code)
246
248
  }
247
249
 
248
250
  function toSymbolSpec(item: SymbolItem): SymbolSpec {
@@ -270,6 +272,9 @@ function forcePercentAxis() {
270
272
  const nextSettings = { ...chartSettings.value, axisType: 'percent' as const }
271
273
  chartSettings.value = nextSettings
272
274
  controller.value?.updateSettingsFacade(nextSettings)
275
+ try {
276
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(nextSettings))
277
+ } catch { /* quota exceeded */ }
273
278
  }
274
279
 
275
280
  const containerRef = ref<HTMLDivElement | null>(null)
@@ -297,6 +302,8 @@ const viewWidth = ref(0)
297
302
  const paneRatios = ref<Record<string, number>>({})
298
303
  const selectedDrawingId = ref<string | null>(null)
299
304
  const drawings = ref<DrawingObject[]>([])
305
+ const comparisonColorsMap = ref<Map<string, string>>(new Map())
306
+ const comparisonLoading = ref(false)
300
307
 
301
308
  // 初始化 kWidth / kGap(与 Chart 引擎 zoom→物理值 转换一致)
302
309
  const initZoom = zoomLevel.value
@@ -529,7 +536,6 @@ function onDeleteDrawing() {
529
536
  const d = selectedDrawing.value
530
537
  if (!d || !drawingController.value) return
531
538
  drawingController.value.removeDrawing(d.id)
532
- selectedDrawingId.value = null
533
539
  drawings.value = drawingController.value.getDrawings()
534
540
  }
535
541
 
@@ -537,7 +543,6 @@ function onPointerDown(e: PointerEvent) {
537
543
  controller.value?.handlePointerEvent(e, {
538
544
  onPointerDown: (event, container) => {
539
545
  if (drawingController.value?.onPointerDown(event, container)) {
540
- drawings.value = drawingController.value.getDrawings()
541
546
  return true
542
547
  }
543
548
  return false
@@ -569,7 +574,6 @@ function onPointerUp(e: PointerEvent) {
569
574
  controller.value?.handlePointerEvent(e, {
570
575
  onPointerUp: (event, container) => {
571
576
  if (drawingController.value?.onPointerUp(event, container)) {
572
- drawings.value = drawingController.value.getDrawings()
573
577
  return true
574
578
  }
575
579
  return false
@@ -673,15 +677,6 @@ function isSubPaneIndicator(id: string): boolean {
673
677
  return !!def && def.category !== 'main'
674
678
  }
675
679
 
676
- // 副图实例计数器:用于生成 'RSI_0', 'MACD_0' 这样的 paneId
677
- const subPaneCounters = new Map<SubIndicatorType, number>()
678
-
679
- function generatePaneId(indicatorId: SubIndicatorType): string {
680
- const count = subPaneCounters.get(indicatorId) ?? 0
681
- subPaneCounters.set(indicatorId, count + 1)
682
- return `${indicatorId}_${count}`
683
- }
684
-
685
680
  // 添加副图(使用 Chart API)
686
681
  function addSubPane(
687
682
  indicatorId: SubIndicatorType = 'VOLUME',
@@ -706,7 +701,6 @@ function clearAllSubPanes(): void {
706
701
  for (const pane of subPanes.value) {
707
702
  controller.value?.removeIndicator(pane.id)
708
703
  }
709
- subPaneCounters.clear()
710
704
  }
711
705
 
712
706
  function initIndicatorsFromConfig(): void {
@@ -728,22 +722,6 @@ function initIndicatorsFromConfig(): void {
728
722
  }
729
723
  }
730
724
 
731
- function syncSubPanesFromChart(): void {
732
- const entries = controller.value?.subPanes.peek() ?? []
733
- for (const entry of entries) {
734
- const { paneId, indicatorId, params } = entry
735
- const match = paneId.match(/^(.+)_(\d+)$/)
736
- if (match) {
737
- const [, indicator, countStr] = match
738
- const count = parseInt(countStr!, 10)
739
- const currentCount = subPaneCounters.get(indicator as SubIndicatorType) ?? 0
740
- if (count >= currentCount) {
741
- subPaneCounters.set(indicator as SubIndicatorType, count + 1)
742
- }
743
- }
744
- }
745
- }
746
-
747
725
  function switchSubIndicator(paneId: string, newIndicatorId: SubIndicatorType): void {
748
726
  const nextParams = getDefaultParams(newIndicatorId)
749
727
  controller.value?.replaceSubPaneIndicator(paneId, newIndicatorId, nextParams)
@@ -753,27 +731,9 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
753
731
  const c = controller.value
754
732
  if (!c) return
755
733
 
756
- const mainIndicatorIds = [
757
- 'MA',
758
- 'BOLL',
759
- 'EXPMA',
760
- 'ENE',
761
- 'WMA',
762
- 'DEMA',
763
- 'TEMA',
764
- 'HMA',
765
- 'KAMA',
766
- 'SAR',
767
- 'SUPERTREND',
768
- 'KELTNER',
769
- 'DONCHIAN',
770
- 'ICHIMOKU',
771
- 'PIVOT',
772
- 'FIB',
773
- 'STRUCTURE',
774
- 'ZONES',
775
- ]
776
- if (mainIndicatorIds.includes(indicatorId)) {
734
+ const def = getRegisteredIndicatorDefinition(indicatorId)
735
+ const isMain = def && (def.category === 'main' || def.allowMainPane)
736
+ if (isMain) {
777
737
  const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
778
738
  if (active && !existingIndicator) {
779
739
  c.addIndicator(indicatorId, 'main', indicatorParams.value[indicatorId])
@@ -873,35 +833,12 @@ const totalWidth = computed(() => {
873
833
  return controller.value?.getContentWidth() ?? 0
874
834
  })
875
835
 
876
- function scrollToRight() {
877
- const container = containerRef.value
878
- const c = controller.value
879
- if (!container || !c) return
880
-
881
- const dataLength = c.getData()?.length ?? 0
882
- if (dataLength === 0) return
883
-
884
- const vp = c.viewport.peek()
885
- const dpr = vp.dpr
886
- const { unitPx, startXPx } = getPhysicalKLineConfig(kWidth.value, kGap.value, dpr)
887
-
888
- const lastKLineEndPx = (startXPx + dataLength * unitPx) / dpr
889
- const maxScrollLeft = Math.max(0, container.scrollWidth - container.clientWidth)
890
- const targetScrollLeft = Math.min(
891
- maxScrollLeft,
892
- Math.max(0, lastKLineEndPx - container.clientWidth),
893
- )
894
-
895
- container.scrollLeft = Math.round(targetScrollLeft * dpr) / dpr
896
- }
897
-
898
836
  function applyZoomToLevel(targetLevel: number, anchorX?: number) {
899
837
  controller.value?.zoomToLevel(targetLevel, anchorX)
900
838
  }
901
839
 
902
840
  defineExpose({
903
841
  scheduleRender,
904
- scrollToRight,
905
842
  addSubPane,
906
843
  removeSubPane,
907
844
  switchSubIndicator,
@@ -982,19 +919,6 @@ function setupChartCallbacks(ctrl: ChartController): void {
982
919
  kWidth.value = vp.kWidth
983
920
  kGap.value = vp.kGap
984
921
  }
985
-
986
- const desiredLeft = vp.desiredScrollLeft
987
- if (desiredLeft !== undefined && desiredLeft !== containerRef.value?.scrollLeft) {
988
- invalidateContainerRectCache()
989
- nextTick(() => {
990
- const c = containerRef.value
991
- if (!c) return
992
- const maxScrollLeft = Math.max(0, c.scrollWidth - c.clientWidth)
993
- const clampedScrollLeft = Math.min(Math.max(0, desiredLeft), maxScrollLeft)
994
- const dpr = vp.dpr
995
- c.scrollLeft = Math.round(clampedScrollLeft * dpr) / dpr
996
- })
997
- }
998
922
  })
999
923
 
1000
924
  const unsubscribeData = ctrl.data.subscribe(() => {
@@ -1067,6 +991,14 @@ function setupChartCallbacks(ctrl: ChartController): void {
1067
991
  indicatorParams.value = nextParams
1068
992
  })
1069
993
 
994
+ const unsubscribeComparisonColors = ctrl.comparisonColors.subscribe(() => {
995
+ comparisonColorsMap.value = new Map(ctrl.comparisonColors.peek())
996
+ })
997
+
998
+ const unsubscribeComparisonLoading = ctrl.comparisonLoading.subscribe(() => {
999
+ comparisonLoading.value = ctrl.comparisonLoading.peek()
1000
+ })
1001
+
1070
1002
  onUnmounted(() => {
1071
1003
  unsubscribeViewport()
1072
1004
  unsubscribeData()
@@ -1076,6 +1008,8 @@ function setupChartCallbacks(ctrl: ChartController): void {
1076
1008
  unsubscribeTheme()
1077
1009
  unsubscribeIndicators()
1078
1010
  unsubscribeSubPanes()
1011
+ unsubscribeComparisonColors()
1012
+ unsubscribeComparisonLoading()
1079
1013
  autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
1080
1014
  })
1081
1015
  }
@@ -1122,8 +1056,7 @@ function setupSemanticController(ctrl: ChartController): void {
1122
1056
  // config:ready → Chart 侧已完成创建,Vue 回读状态
1123
1057
  semanticController.value.on('config:ready', () => {
1124
1058
  initIndicatorsFromConfig()
1125
- syncSubPanesFromChart()
1126
- nextTick(() => scrollToRight())
1059
+ nextTick(() => controller.value?.scrollToRight())
1127
1060
  })
1128
1061
  // 暂时断开语义化配置加载,由搜索结果驱动
1129
1062
  // semanticController.value.applyConfig(props.semanticConfig).then((result) => {
@@ -4,12 +4,12 @@
4
4
  type="button"
5
5
  class="symbol-chip"
6
6
  :class="{ 'is-open': showPopup }"
7
- :title="symbol"
7
+ :title="displayText"
8
8
  :aria-expanded="showPopup"
9
9
  aria-haspopup="dialog"
10
10
  @click="togglePopup"
11
11
  >
12
- <span class="symbol-chip__code">{{ symbol }}</span>
12
+ <span class="symbol-chip__code">{{ displayText }}</span>
13
13
  <span v-if="loading" class="symbol-chip__spinner" aria-hidden="true" />
14
14
  <IconTablerAlertTriangle v-else-if="error" class="symbol-chip__warn" aria-hidden="true" />
15
15
  </button>
@@ -127,6 +127,16 @@ const searchQuery = ref('')
127
127
  const searchInputRef = ref<HTMLInputElement | null>(null)
128
128
  const chipWrapRef = ref<HTMLElement | null>(null)
129
129
 
130
+ const currentSymbol = computed<SymbolItem | undefined>(() =>
131
+ props.symbols.find((s) => s.code === props.symbol),
132
+ )
133
+
134
+ const displayText = computed(() => {
135
+ const cur = currentSymbol.value
136
+ if (cur) return `${cur.code} - ${cur.description}`
137
+ return props.symbol
138
+ })
139
+
130
140
  const filteredSymbols = computed<SymbolItem[]>(() => {
131
141
  const q = searchQuery.value.trim().toLowerCase()
132
142
  if (!q) return props.symbols
@@ -186,7 +196,6 @@ watch(() => props.symbol, () => {
186
196
  display: inline-flex;
187
197
  align-items: center;
188
198
  justify-content: center;
189
- max-width: 160px;
190
199
  padding: 0 10px;
191
200
  gap: 5px;
192
201
  border: 1px solid transparent;
@@ -11,6 +11,8 @@
11
11
  <CompareSymbolSelector
12
12
  :symbols="symbolPool"
13
13
  :selected="overlaySymbols"
14
+ :comparison-colors="comparisonColors"
15
+ :comparison-loading="comparisonLoading"
14
16
  @add="emit('addOverlaySymbol', $event)"
15
17
  @remove="emit('removeOverlaySymbol', $event)"
16
18
  />
@@ -47,6 +49,8 @@ const props = defineProps<{
47
49
  symbolLoading?: boolean
48
50
  symbolError?: boolean
49
51
  overlaySymbols?: string[]
52
+ comparisonColors?: Map<string, string>
53
+ comparisonLoading?: boolean
50
54
  }>()
51
55
 
52
56
  const emit = defineEmits<{
@@ -58,24 +62,25 @@ const emit = defineEmits<{
58
62
  }>()
59
63
 
60
64
  const MOCK_SYMBOLS: SymbolItem[] = [
61
- { code: 'AAPL', description: 'Apple Inc.', exchange: 'NASDAQ', source: 'baostock' },
62
- { code: 'TSLA', description: 'Tesla, Inc.', exchange: 'NASDAQ', source: 'baostock' },
63
- { code: 'GOOGL', description: 'Alphabet Inc.', exchange: 'NASDAQ', source: 'baostock' },
64
- { code: 'MSFT', description: 'Microsoft Corporation', exchange: 'NASDAQ', source: 'baostock' },
65
- { code: 'AMZN', description: 'Amazon.com, Inc.', exchange: 'NASDAQ', source: 'baostock' },
66
- { code: 'NVDA', description: 'NVIDIA Corporation', exchange: 'NASDAQ', source: 'baostock' },
67
- { code: 'META', description: 'Meta Platforms, Inc.', exchange: 'NASDAQ', source: 'baostock' },
68
- { code: 'BRK.B', description: 'Berkshire Hathaway Inc.', exchange: 'NYSE', source: 'baostock' },
69
- { code: 'JPM', description: 'JPMorgan Chase & Co.', exchange: 'NYSE', source: 'baostock' },
70
- { code: 'V', description: 'Visa Inc.', exchange: 'NYSE', source: 'baostock' },
71
- { code: 'BTCUSDT', description: 'Bitcoin / Tether', exchange: 'BINANCE', source: 'baostock' },
72
- { code: 'ETHUSDT', description: 'Ethereum / Tether', exchange: 'BINANCE', source: 'baostock' },
73
- { code: 'sh.601360', description: '三六零', exchange: 'SSE', source: 'baostock' },
74
- { code: 'sh.600519', description: '贵州茅台', exchange: 'SSE', source: 'baostock' },
75
- { code: '000858', description: '五 粮 液', exchange: 'SZSE', source: 'baostock' },
76
- { code: '000001', description: '平安银行', exchange: 'SZSE', source: 'baostock' },
77
- { code: 'MOCK-100', description: 'Mock 100 条', exchange: 'MOCK', source: 'mock-100' },
78
- { code: 'MOCK-10000', description: 'Mock 10000 条', exchange: 'MOCK', source: 'mock-10000' },
65
+ // ── TradingView 全球品种 ──
66
+ { code: 'XAUUSD', description: '现货黄金', exchange: 'OANDA', source: 'tradingview' },
67
+ { code: 'BTCUSDT', description: 'Bitcoin / Tether', exchange: 'BINANCE', source: 'tradingview' },
68
+ { code: 'ETHUSDT', description: 'Ethereum / Tether', exchange: 'BINANCE', source: 'tradingview' },
69
+ { code: 'EURUSD', description: '欧元/美元', exchange: 'OANDA', source: 'tradingview' },
70
+ { code: 'SPX', description: '标普 500 指数', exchange: 'SP', source: 'tradingview' },
71
+ { code: 'AAPL', description: 'Apple Inc.', exchange: 'NASDAQ', source: 'tradingview' },
72
+ { code: 'TSLA', description: 'Tesla, Inc.', exchange: 'NASDAQ', source: 'tradingview' },
73
+ { code: '600519', description: '贵州茅台', exchange: 'SSE', source: 'tradingview' },
74
+ { code: '000001', description: '平安银行', exchange: 'SZSE', source: 'tradingview' },
75
+ { code: '1810', description: '小米集团', exchange: 'HKEX', source: 'tradingview' },
76
+ // ── Baostock A ──
77
+ { code: 'sh.600519', description: '贵州茅台', exchange: 'SSE', source: 'baostock' },
78
+ { code: 'sh.601360', description: '三六零', exchange: 'SSE', source: 'baostock' },
79
+ { code: '000858', description: '五 粮 液', exchange: 'SZSE', source: 'baostock' },
80
+ { code: '000001', description: '平安银行', exchange: 'SZSE', source: 'baostock' },
81
+ // ── Mock ──
82
+ { code: 'MOCK-100', description: 'Mock 100 条', exchange: 'MOCK', source: 'mock-100' },
83
+ { code: 'MOCK-10000', description: 'Mock 10000 条', exchange: 'MOCK', source: 'mock-10000' },
79
84
  ]
80
85
 
81
86
  const displaySymbol = computed(() => props.symbol?.trim() ?? '')