@363045841yyt/klinechart 0.7.12 → 0.8.1-alpha.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.
@@ -1,5 +1,15 @@
1
1
  <template>
2
2
  <div ref="chartWrapperRef" class="chart-wrapper" :data-theme="chartTheme" :style="themeCssVars">
3
+ <TopToolbar
4
+ :symbol="currentSymbol"
5
+ :k-line-level="kLineLevel"
6
+ :symbol-loading="symbolLoading"
7
+ :symbol-error="symbolError"
8
+ @add-overlay-symbol="$emit('addOverlaySymbol')"
9
+ @k-line-level-change="onKLineLevelChange"
10
+ @toggle-indicator="onToggleIndicator"
11
+ @symbol-change="onSymbolChange"
12
+ />
3
13
  <div
4
14
  class="chart-stage"
5
15
  :class="{
@@ -99,6 +109,7 @@
99
109
  </div>
100
110
  </div>
101
111
  <IndicatorSelector
112
+ ref="indicatorSelectorRef"
102
113
  :active-indicators="activeIndicators"
103
114
  :indicator-params="indicatorParams"
104
115
  @toggle="handleIndicatorToggle"
@@ -112,7 +123,6 @@
112
123
  import { ref, computed, onMounted, onUnmounted, watch, nextTick, shallowRef } from 'vue'
113
124
  import {
114
125
  SemanticChartController,
115
- __setDataFetcher,
116
126
  type SemanticChartConfig,
117
127
  type DataFetcher,
118
128
  } from '@363045841yyt/klinechart-core/semantic'
@@ -133,14 +143,23 @@ import {
133
143
  zoomLevelToKWidth,
134
144
  kGapFromKWidth,
135
145
  getPhysicalKLineConfig,
136
- SUB_PANE_INDICATOR_CONFIGS,
137
- SUB_PANE_INDICATORS,
138
146
  DrawingInteractionController,
139
147
  } from '@363045841yyt/klinechart-core/controllers'
148
+ import {
149
+ getRegisteredIndicatorDefinition,
150
+ getRegisteredIndicatorDefinitions,
151
+ } from '@363045841yyt/klinechart-core/indicators'
140
152
  import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
141
153
  import type { ChartSettings } from '@363045841yyt/klinechart-core/config'
142
- import { resolveThemeColors, themeToCssVars, lightTheme, darkTheme, type ColorPresetSettings } from '@363045841yyt/klinechart-core'
154
+ import {
155
+ resolveThemeColors,
156
+ themeToCssVars,
157
+ lightTheme,
158
+ darkTheme,
159
+ type ColorPresetSettings,
160
+ } from '@363045841yyt/klinechart-core'
143
161
  import LeftToolbar from './LeftToolbar.vue'
162
+ import TopToolbar, { type SymbolItem } from './TopToolbar.vue'
144
163
 
145
164
  const props = withDefaults(
146
165
  defineProps<{
@@ -184,13 +203,43 @@ const emit = defineEmits<{
184
203
  (e: 'zoomLevelChange', level: number, kWidth: number): void
185
204
  (e: 'toggleFullscreen'): void
186
205
  (e: 'themeChange', theme: 'light' | 'dark'): void
206
+ (e: 'addOverlaySymbol'): void
207
+ (e: 'kLineLevelChange', level: string): void
187
208
  }>()
188
209
 
210
+ const kLineLevel = ref(props.semanticConfig.data.period)
211
+ const currentSymbol = ref('选择商品')
212
+ const symbolLoading = ref(false)
213
+ const symbolError = ref(false)
214
+
215
+ function onKLineLevelChange(level: string) {
216
+ kLineLevel.value = level as typeof kLineLevel.value
217
+ emit('kLineLevelChange', level)
218
+ }
219
+
220
+ function onSymbolChange(item: SymbolItem) {
221
+ symbolLoading.value = true
222
+ symbolError.value = false
223
+ currentSymbol.value = item.code
224
+ controller.value?.setSymbols([
225
+ {
226
+ symbol: item.code,
227
+ exchange: item.exchange,
228
+ period: kLineLevel.value,
229
+ source: item.source,
230
+ startDate: props.semanticConfig.data.startDate,
231
+ endDate: props.semanticConfig.data.endDate,
232
+ adjust: props.semanticConfig.data.adjust,
233
+ },
234
+ ])
235
+ }
236
+
189
237
  const containerRef = ref<HTMLDivElement | null>(null)
190
238
  const chartMainRef = ref<HTMLDivElement | null>(null)
191
239
  const chartWrapperRef = ref<HTMLDivElement | null>(null)
192
240
  const tooltipLayerRef = ref<HTMLDivElement | null>(null)
193
241
  const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
242
+ const indicatorSelectorRef = ref<InstanceType<typeof IndicatorSelector> | null>(null)
194
243
  provideFullscreenTeleportTarget(chartWrapperRef)
195
244
 
196
245
  /* ========== 图表控制器 ========== */
@@ -237,7 +286,9 @@ const tooltipColors = computed(() => {
237
286
 
238
287
  const themeCssVars = computed(() => {
239
288
  const theme = chartTheme.value === 'dark' ? darkTheme : lightTheme
240
- const overrides = (chartSettings.value.colorPresetSettings as ColorPresetSettings | undefined)?.[chartTheme.value]
289
+ const overrides = (chartSettings.value.colorPresetSettings as ColorPresetSettings | undefined)?.[
290
+ chartTheme.value
291
+ ]
241
292
  if (overrides && Object.keys(overrides).length > 0) {
242
293
  return themeToCssVars({ ...theme, colors: { ...theme.colors, ...overrides } })
243
294
  }
@@ -278,57 +329,12 @@ function handleSettingsChange(settings: ChartSettings) {
278
329
  applyThemeFromSettings(controller.value, settings.theme as string)
279
330
  controller.value?.updateSettingsFacade(settings)
280
331
 
281
- if (settings.performanceTest10kKlines) {
282
- const testData = generate10kKLineData()
283
- console.time('updateData-10k')
284
- controller.value?.updateData(testData)
285
- console.timeEnd('updateData-10k')
286
- dataLength.value = testData.length
287
- dataVersion.value++
288
- } else {
289
- if (semanticController.value && controller.value?.getData()?.length === 10000) {
290
- semanticController.value.applyConfig(props.semanticConfig)
291
- }
332
+ controller.value?.setDataFetcher(props.dataFetcher)
333
+ if (semanticController.value && props.semanticConfig) {
334
+ semanticController.value.applyConfig(props.semanticConfig)
292
335
  }
293
336
  }
294
337
 
295
- // 生成1万条K线测试数据
296
- function generate10kKLineData() {
297
- const data: KLineData[] = []
298
- const startTime = new Date('2020-01-01').getTime()
299
- const dayMs = 24 * 60 * 60 * 1000
300
-
301
- let lastClose = 3000 // 起始价格
302
-
303
- for (let i = 0; i < 10000; i++) {
304
- const timestamp = startTime + i * dayMs
305
-
306
- // 生成随机波动
307
- const volatility = 0.02 // 2%日波动率
308
- const trend = 0.0001 // 轻微上涨趋势
309
- const change = (Math.random() - 0.5) * 2 * volatility + trend
310
-
311
- const open = lastClose
312
- const close = open * (1 + change)
313
- const high = Math.max(open, close) * (1 + Math.random() * 0.01)
314
- const low = Math.min(open, close) * (1 - Math.random() * 0.01)
315
- const volume = Math.floor(1000000 + Math.random() * 5000000)
316
-
317
- data.push({
318
- timestamp,
319
- open: parseFloat(open.toFixed(2)),
320
- high: parseFloat(high.toFixed(2)),
321
- low: parseFloat(low.toFixed(2)),
322
- close: parseFloat(close.toFixed(2)),
323
- volume,
324
- })
325
-
326
- lastClose = close
327
- }
328
-
329
- return data
330
- }
331
-
332
338
  function measureTooltipSize(el: HTMLDivElement, minWidth: number, minHeight: number) {
333
339
  const r = el.getBoundingClientRect()
334
340
  return {
@@ -475,10 +481,15 @@ function handleSelectTool(toolId: string) {
475
481
  drawingController.value?.setTool(toolId as DrawingToolId)
476
482
  }
477
483
 
484
+ function onToggleIndicator() {
485
+ indicatorSelectorRef.value?.toggleMenu()
486
+ }
487
+
478
488
  function onUpdateDrawingStyle(style: Partial<DrawingStyle>) {
479
489
  const d = selectedDrawing.value
480
490
  if (!d || !drawingController.value) return
481
491
  drawingController.value.updateDrawingStyle(d.id, style)
492
+ drawings.value = drawingController.value.getDrawings()
482
493
  }
483
494
 
484
495
  function onDeleteDrawing() {
@@ -614,7 +625,19 @@ function buildPaneLayoutIntent(): PaneSpec[] {
614
625
  function getDefaultParams(
615
626
  indicatorId: SubIndicatorType,
616
627
  ): Record<string, number | boolean | string> {
617
- return { ...SUB_PANE_INDICATOR_CONFIGS[indicatorId].defaultParams }
628
+ if (indicatorId === 'VOLUME') return {}
629
+ const meta = getRegisteredIndicatorDefinition(indicatorId)
630
+ if (meta?.runtime?.defaultConfig) {
631
+ return { ...meta.runtime.defaultConfig } as Record<string, number | boolean | string>
632
+ }
633
+ return {}
634
+ }
635
+
636
+ // 副图指标判定(基于 registry category + VOLUME 特例)
637
+ function isSubPaneIndicator(id: string): boolean {
638
+ if (id === 'VOLUME') return true
639
+ const def = getRegisteredIndicatorDefinition(id)
640
+ return !!def && def.category !== 'main'
618
641
  }
619
642
 
620
643
  // 副图实例计数器:用于生成 'RSI_0', 'MACD_0' 这样的 paneId
@@ -698,9 +721,24 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
698
721
  if (!c) return
699
722
 
700
723
  const mainIndicatorIds = [
701
- 'MA', 'BOLL', 'EXPMA', 'ENE', 'WMA', 'DEMA', 'TEMA', 'HMA',
702
- 'KAMA', 'SAR', 'SUPERTREND', 'KELTNER', 'DONCHIAN', 'ICHIMOKU',
703
- 'PIVOT', 'FIB', 'STRUCTURE', 'ZONES',
724
+ 'MA',
725
+ 'BOLL',
726
+ 'EXPMA',
727
+ 'ENE',
728
+ 'WMA',
729
+ 'DEMA',
730
+ 'TEMA',
731
+ 'HMA',
732
+ 'KAMA',
733
+ 'SAR',
734
+ 'SUPERTREND',
735
+ 'KELTNER',
736
+ 'DONCHIAN',
737
+ 'ICHIMOKU',
738
+ 'PIVOT',
739
+ 'FIB',
740
+ 'STRUCTURE',
741
+ 'ZONES',
704
742
  ]
705
743
  if (mainIndicatorIds.includes(indicatorId)) {
706
744
  const existingIndicator = mainActiveIndicators.value.find((id) => id === indicatorId)
@@ -712,7 +750,7 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
712
750
  return
713
751
  }
714
752
 
715
- if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
753
+ if (isSubPaneIndicator(indicatorId)) {
716
754
  if (active) {
717
755
  const existingPane = subPanes.value.find((p) => p.indicatorId === indicatorId)
718
756
  if (existingPane) return
@@ -734,13 +772,15 @@ function handleIndicatorToggle(indicatorId: string, active: boolean) {
734
772
 
735
773
  function handleUpdateParams(indicatorId: string, params: Record<string, unknown>) {
736
774
  if (
737
- indicatorId === 'MA' || indicatorId === 'BOLL' ||
738
- indicatorId === 'EXPMA' || indicatorId === 'ENE'
775
+ indicatorId === 'MA' ||
776
+ indicatorId === 'BOLL' ||
777
+ indicatorId === 'EXPMA' ||
778
+ indicatorId === 'ENE'
739
779
  ) {
740
780
  controller.value?.updateIndicatorParams(indicatorId, params)
741
781
  return
742
782
  }
743
- if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
783
+ if (isSubPaneIndicator(indicatorId)) {
744
784
  subPanes.value
745
785
  .filter((p) => p.indicatorId === indicatorId)
746
786
  .forEach((pane) => {
@@ -753,7 +793,7 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
753
793
  if (!orderedIndicatorIds.length || subPanes.value.length <= 1) return
754
794
 
755
795
  const validOrder = orderedIndicatorIds.filter((id): id is SubIndicatorType =>
756
- SUB_PANE_INDICATORS.includes(id as SubIndicatorType),
796
+ isSubPaneIndicator(id),
757
797
  )
758
798
  if (!validOrder.length) return
759
799
 
@@ -904,11 +944,7 @@ function setupChartCallbacks(ctrl: ChartController): void {
904
944
  if (viewWidth.value !== vp.plotWidth) {
905
945
  viewWidth.value = vp.plotWidth
906
946
  }
907
- if (
908
- zoomLevel.value !== vp.zoomLevel ||
909
- kWidth.value !== vp.kWidth ||
910
- kGap.value !== vp.kGap
911
- ) {
947
+ if (zoomLevel.value !== vp.zoomLevel || kWidth.value !== vp.kWidth || kGap.value !== vp.kGap) {
912
948
  zoomLevel.value = vp.zoomLevel
913
949
  kWidth.value = vp.kWidth
914
950
  kGap.value = vp.kGap
@@ -932,6 +968,8 @@ function setupChartCallbacks(ctrl: ChartController): void {
932
968
  const data = ctrl.data.peek()
933
969
  dataLength.value = data.length
934
970
  dataVersion.value++
971
+ symbolLoading.value = false
972
+ symbolError.value = data.length === 0
935
973
  })
936
974
 
937
975
  const unsubscribeTheme = ctrl.theme.subscribe(() => {
@@ -1010,13 +1048,6 @@ function applyInitialSettings(ctrl: ChartController): void {
1010
1048
  chartSettings.value = initialSettings
1011
1049
  applyThemeFromSettings(ctrl, initialSettings.theme as string)
1012
1050
  ctrl.updateSettingsFacade(initialSettings)
1013
-
1014
- if (initialSettings.performanceTest10kKlines) {
1015
- const testData = generate10kKLineData()
1016
- console.time('updateData-10k')
1017
- ctrl.updateData(testData)
1018
- console.timeEnd('updateData-10k')
1019
- }
1020
1051
  }
1021
1052
 
1022
1053
  function setupDrawingController(ctrl: ChartController): void {
@@ -1044,7 +1075,7 @@ function setupInteractionCallbacks(ctrl: ChartController): void {
1044
1075
  }
1045
1076
 
1046
1077
  function setupSemanticController(ctrl: ChartController): void {
1047
- __setDataFetcher(props.dataFetcher)
1078
+ ctrl.setDataFetcher(props.dataFetcher)
1048
1079
  semanticController.value = new SemanticChartController(ctrl)
1049
1080
 
1050
1081
  semanticController.value.on('config:error', (error) => {
@@ -1057,21 +1088,12 @@ function setupSemanticController(ctrl: ChartController): void {
1057
1088
  syncSubPanesFromChart()
1058
1089
  nextTick(() => scrollToRight())
1059
1090
  })
1060
- // 应用副图、主图配置
1061
- if (chartSettings.value.performanceTest10kKlines) {
1062
- // 10k 性能测试模式:数据由外部(applyInitialSettings)提供,
1063
- // 语义控制器只注册指标和标记,不 fetch/updateData,避免数据跳变
1064
- const result = semanticController.value.applyIndicatorsOnly(props.semanticConfig)
1065
- if (result && !result.success) {
1066
- console.error('Semantic config apply failed:', result.errors)
1067
- }
1068
- } else {
1069
- semanticController.value.applyConfig(props.semanticConfig).then((result) => {
1070
- if (result && !result.success) {
1071
- console.error('Semantic config apply failed:', result.errors)
1072
- }
1073
- })
1074
- }
1091
+ // 暂时断开语义化配置加载,由搜索结果驱动
1092
+ // semanticController.value.applyConfig(props.semanticConfig).then((result) => {
1093
+ // if (result && !result.success) {
1094
+ // console.error('Semantic config apply failed:', result.errors)
1095
+ // }
1096
+ // })
1075
1097
  }
1076
1098
 
1077
1099
  onMounted(() => {
@@ -1161,20 +1183,23 @@ watch(
1161
1183
 
1162
1184
  display: flex;
1163
1185
  align-items: center;
1164
- justify-content: center;
1165
1186
  width: var(--kmap-width);
1166
- height: var(--kmap-height);
1187
+ height: calc(var(--kmap-height) - 32px);
1167
1188
  min-height: 300px;
1168
1189
  flex-direction: column;
1190
+ margin: 16px 0;
1191
+ padding: 0;
1192
+ box-sizing: border-box;
1193
+ gap: 4px;
1169
1194
  }
1170
1195
 
1171
1196
  .chart-stage {
1172
1197
  width: 95%;
1173
- height: 85%;
1198
+ flex: 1;
1174
1199
  min-height: 255px;
1175
1200
  display: flex;
1176
1201
  align-items: stretch;
1177
- gap: 8px;
1202
+ gap: 4px;
1178
1203
  }
1179
1204
 
1180
1205
  .chart-main {
@@ -1243,7 +1268,7 @@ watch(
1243
1268
  -ms-overflow-style: none;
1244
1269
  border: 1px solid var(--chart-border);
1245
1270
  border-right: 0;
1246
- border-radius: 6px 0 0 6px;
1271
+ border-radius: 3px 0 0 3px;
1247
1272
  box-sizing: border-box;
1248
1273
  background: var(--chart-bg);
1249
1274
 
@@ -1266,8 +1291,8 @@ watch(
1266
1291
  background: var(--chart-bg);
1267
1292
  overflow: visible;
1268
1293
  border: 1px solid var(--chart-border);
1269
- border-top-right-radius: 6px;
1270
- border-bottom-right-radius: 6px;
1294
+ border-top-right-radius: 3px;
1295
+ border-bottom-right-radius: 3px;
1271
1296
 
1272
1297
  -webkit-touch-callout: none;
1273
1298
  -webkit-user-select: none;
@@ -1311,8 +1336,12 @@ watch(
1311
1336
  }
1312
1337
 
1313
1338
  @media (max-width: 768px), (max-height: 640px) {
1339
+ .chart-wrapper {
1340
+ gap: 4px;
1341
+ }
1342
+
1314
1343
  .chart-stage {
1315
- gap: 6px;
1344
+ gap: 4px;
1316
1345
  }
1317
1346
  }
1318
1347
  </style>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <Dropdown
3
+ :model-value="modelValue"
4
+ :options="kLineLevelOptions"
5
+ label="级别"
6
+ title="K线级别"
7
+ size="md"
8
+ @update:model-value="emit('update:modelValue', $event as KLineLevel)"
9
+ />
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import Dropdown from './Dropdown.vue'
14
+
15
+ export type KLineLevel =
16
+ | '1min'
17
+ | '5min'
18
+ | '15min'
19
+ | '30min'
20
+ | '60min'
21
+ | 'weekly'
22
+ | 'monthly'
23
+ | 'quarterly'
24
+ | 'yearly'
25
+
26
+ const kLineLevelOptions: Array<{ label: string; value: KLineLevel }> = [
27
+ { label: '1min', value: '1min' },
28
+ { label: '5min', value: '5min' },
29
+ { label: '15min', value: '15min' },
30
+ { label: '30min', value: '30min' },
31
+ { label: '1小时', value: '60min' },
32
+ { label: '1周', value: 'weekly' },
33
+ { label: '1月', value: 'monthly' },
34
+ { label: '3月', value: 'quarterly' },
35
+ { label: '12月', value: 'yearly' },
36
+ ]
37
+
38
+ defineProps<{
39
+ modelValue?: string
40
+ }>()
41
+
42
+ const emit = defineEmits<{
43
+ (e: 'update:modelValue', level: KLineLevel): void
44
+ }>()
45
+ </script>
@@ -307,7 +307,7 @@ onUnmounted(() => {
307
307
  gap: 6px;
308
308
  padding: 8px 5px;
309
309
  border: 1px solid var(--klc-color-border-chart);
310
- border-radius: 6px;
310
+ border-radius: 3px;
311
311
  background: var(--klc-color-background);
312
312
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
313
313
  box-sizing: border-box;
@@ -333,7 +333,7 @@ onUnmounted(() => {
333
333
  height: 28px;
334
334
  padding: 0;
335
335
  border: 1px solid transparent;
336
- border-radius: 4px;
336
+ border-radius: 3px;
337
337
  background: transparent;
338
338
  color: var(--klc-color-axis-text);
339
339
  cursor: pointer;
@@ -420,7 +420,7 @@ onUnmounted(() => {
420
420
  backdrop-filter: blur(8px);
421
421
  -webkit-backdrop-filter: blur(8px);
422
422
  border: 1px solid var(--klc-color-border-chart);
423
- border-radius: 6px;
423
+ border-radius: 3px;
424
424
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
425
425
  box-sizing: border-box;
426
426
  z-index: 100;
@@ -451,7 +451,7 @@ onUnmounted(() => {
451
451
  flex-basis: 36px;
452
452
  padding: 6px 4px;
453
453
  gap: 5px;
454
- border-radius: 5px;
454
+ border-radius: 3px;
455
455
  }
456
456
 
457
457
  .left-toolbar__group {