@363045841yyt/klinechart 0.7.12 → 0.8.0

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,87 +1,5 @@
1
1
  <template>
2
2
  <div class="indicator-selector">
3
- <div class="indicator-scroll-container">
4
- <div class="indicator-list">
5
- <!-- 已激活的指标 -->
6
- <template v-for="indicator in activeIndicatorsList" :key="indicator.id">
7
- <div
8
- v-if="indicator.id === firstActiveSubIndicatorId"
9
- class="indicator-divider"
10
- aria-hidden="true"
11
- ></div>
12
-
13
- <div
14
- class="indicator-item"
15
- :class="{
16
- draggable: isSubIndicatorId(indicator.id),
17
- 'drag-over': dragOverIndicatorId === indicator.id,
18
- 'is-dragging': draggingIndicatorId === indicator.id,
19
- }"
20
- :draggable="isSubIndicatorId(indicator.id)"
21
- @dragstart="onDragStart($event, indicator.id)"
22
- @dragover.prevent="onDragOver($event, indicator.id)"
23
- @drop.prevent="onDrop($event, indicator.id)"
24
- @dragend="onDragEnd"
25
- >
26
- <div
27
- class="indicator-btn-wrapper"
28
- @mouseenter="hoveredIndicator = indicator.id"
29
- @mouseleave="hoveredIndicator = null"
30
- >
31
- <button
32
- class="indicator-btn"
33
- :class="{ active: true, hovering: hoveredIndicator === indicator.id }"
34
- >
35
- <span class="btn-content">
36
- {{ indicator.label }}
37
- <span v-if="indicator.params?.length" class="param-hint">
38
- ({{ getParamDisplay(indicator) }})
39
- </span>
40
- </span>
41
- <!-- 悬浮操作层 -->
42
- <Transition name="fade">
43
- <div v-if="hoveredIndicator === indicator.id" class="hover-overlay">
44
- <button
45
- v-if="indicator.params?.length"
46
- class="action-btn settings-btn"
47
- @click.stop="showParams(indicator.id)"
48
- title="编辑参数"
49
- >
50
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
51
- <path
52
- d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
53
- />
54
- </svg>
55
- </button>
56
- <span v-if="indicator.params?.length" class="divider"></span>
57
- <button
58
- class="action-btn remove-btn"
59
- @click.stop="removeIndicator(indicator.id)"
60
- title="移除指标"
61
- >
62
- <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
63
- <path
64
- d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
65
- />
66
- </svg>
67
- </button>
68
- </div>
69
- </Transition>
70
- </button>
71
- </div>
72
- </div>
73
- </template>
74
-
75
- <!-- 添加按钮 -->
76
- <div class="indicator-item">
77
- <button ref="addBtnRef" class="add-btn" @click.stop="controller.toggleMenu()" title="添加指标">
78
- <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
79
- <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
80
- </svg>
81
- </button>
82
- </div>
83
- </div>
84
- </div>
85
3
 
86
4
  <!-- 添加指标弹窗 -->
87
5
  <Teleport :to="teleportTarget">
@@ -127,9 +45,8 @@
127
45
  </div>
128
46
  </div>
129
47
 
130
- <!-- 弹窗主体 -->
131
- <div class="modal-body">
132
- <!-- 搜索框 -->
48
+ <!-- 搜索区域 -->
49
+ <div class="modal-search-area">
133
50
  <div class="search-box">
134
51
  <svg
135
52
  class="search-icon"
@@ -149,6 +66,10 @@
149
66
  placeholder="搜索指标名称..."
150
67
  />
151
68
  </div>
69
+ </div>
70
+
71
+ <!-- 弹窗主体 -->
72
+ <div class="modal-body">
152
73
  <!-- 主图指标区域 -->
153
74
  <div v-if="filteredMain.length > 0" class="indicator-section">
154
75
  <div class="section-header">
@@ -297,7 +218,6 @@ import {
297
218
  type IndicatorDefinition,
298
219
  allIndicators,
299
220
  findIndicator,
300
- isSubIndicatorId,
301
221
  type Indicator,
302
222
  } from '@363045841yyt/klinechart-core/controllers'
303
223
 
@@ -350,35 +270,13 @@ const hasSearchResults = computed(
350
270
  const catalogLen = controller.catalog.peek().length
351
271
 
352
272
  // ── 本地 UI 状态(非 Controller 管理的纯 UI 状态) ──
353
- const addBtnRef = ref<HTMLButtonElement | null>(null)
354
273
  const paramsVisible = ref(false)
355
274
  const currentIndicatorId = ref<string | null>(null)
356
- const hoveredIndicator = ref<string | null>(null)
357
- const dragOverIndicatorId = ref<string | null>(null)
358
- const draggingIndicatorId = ref<string | null>(null)
359
275
  const isCompactView = ref(false)
360
276
 
361
277
  // Teleport target for fullscreen modal visibility
362
278
  const teleportTarget = useFullscreenTeleportTarget()
363
279
 
364
- const activeIndicatorsList = computed(() => {
365
- if (!props.activeIndicators?.length) return []
366
- return props.activeIndicators
367
- .map((id) => findIndicator(id))
368
- .filter((i): i is Indicator => i !== undefined)
369
- .sort((a, b) => {
370
- if (a.pane === b.pane) return 0
371
- return a.pane === 'main' ? -1 : 1
372
- })
373
- })
374
-
375
- const firstActiveSubIndicatorId = computed(() => {
376
- const hasMain = activeIndicatorsList.value.some((indicator) => indicator.pane === 'main')
377
- if (!hasMain) return null
378
- const firstSub = activeIndicatorsList.value.find((indicator) => indicator.pane === 'sub')
379
- return firstSub?.id ?? null
380
- })
381
-
382
280
  const currentIndicator = computed(() => {
383
281
  if (!currentIndicatorId.value) return null
384
282
  return findIndicator(currentIndicatorId.value)
@@ -396,13 +294,6 @@ function addIndicator(indicatorId: string) {
396
294
  const indicator = findIndicator(indicatorId)
397
295
  if (!indicator) return
398
296
 
399
- if (indicator.pane === 'main') {
400
- const allItems = allIndicators
401
- allItems
402
- .filter((i) => i.id !== indicatorId && isActive(i.id) && i.pane === 'main')
403
- .forEach((i) => emit('toggle', i.id, false))
404
- }
405
-
406
297
  emit('toggle', indicatorId, true)
407
298
  }
408
299
 
@@ -436,12 +327,6 @@ function getParamValues(indicatorId: string): Record<string, number> {
436
327
  return result
437
328
  }
438
329
 
439
- function getParamDisplay(indicator: Indicator): string {
440
- const values = getParamValues(indicator.id)
441
- if (!indicator.params) return ''
442
- return indicator.params.map((p) => values[p.key] ?? '').join(',')
443
- }
444
-
445
330
  function onParamsConfirm(values: Record<string, number>) {
446
331
  if (currentIndicatorId.value) {
447
332
  emit('updateParams', currentIndicatorId.value, values)
@@ -449,71 +334,6 @@ function onParamsConfirm(values: Record<string, number>) {
449
334
  paramsVisible.value = false
450
335
  }
451
336
 
452
- function onDragStart(event: DragEvent, indicatorId: string) {
453
- if (!isSubIndicatorId(indicatorId)) {
454
- event.preventDefault()
455
- return
456
- }
457
- draggingIndicatorId.value = indicatorId
458
- dragOverIndicatorId.value = null
459
- event.dataTransfer?.setData('text/plain', indicatorId)
460
- if (event.dataTransfer) {
461
- event.dataTransfer.effectAllowed = 'move'
462
- }
463
- }
464
-
465
- function onDragOver(event: DragEvent, indicatorId: string) {
466
- if (
467
- !draggingIndicatorId.value ||
468
- !isSubIndicatorId(indicatorId) ||
469
- draggingIndicatorId.value === indicatorId
470
- )
471
- return
472
- dragOverIndicatorId.value = indicatorId
473
- if (event.dataTransfer) {
474
- event.dataTransfer.dropEffect = 'move'
475
- }
476
- }
477
-
478
- function onDrop(event: DragEvent, targetIndicatorId: string) {
479
- const sourceIndicatorId =
480
- draggingIndicatorId.value || event.dataTransfer?.getData('text/plain') || ''
481
- if (!sourceIndicatorId || sourceIndicatorId === targetIndicatorId) {
482
- onDragEnd()
483
- return
484
- }
485
- if (!isSubIndicatorId(sourceIndicatorId) || !isSubIndicatorId(targetIndicatorId)) {
486
- onDragEnd()
487
- return
488
- }
489
-
490
- const sourceIndex = activeIndicatorsList.value.findIndex((i) => i.id === sourceIndicatorId)
491
- const targetIndex = activeIndicatorsList.value.findIndex((i) => i.id === targetIndicatorId)
492
- if (sourceIndex < 0 || targetIndex < 0) {
493
- onDragEnd()
494
- return
495
- }
496
-
497
- const next = [...activeIndicatorsList.value.map((i) => i.id)]
498
- const [moved] = next.splice(sourceIndex, 1)
499
- if (!moved) {
500
- onDragEnd()
501
- return
502
- }
503
- next.splice(targetIndex, 0, moved)
504
-
505
- emit(
506
- 'reorderSubIndicators',
507
- next.filter((id) => isSubIndicatorId(id)),
508
- )
509
- onDragEnd()
510
- }
511
-
512
- function onDragEnd() {
513
- dragOverIndicatorId.value = null
514
- draggingIndicatorId.value = null
515
- }
516
-
517
337
  function handleKeydown(event: KeyboardEvent) {
518
338
  if (event.key === 'Escape' && controller.menuOpen.peek()) {
519
339
  controller.closeMenu()
@@ -527,184 +347,19 @@ onMounted(() => {
527
347
  onUnmounted(() => {
528
348
  document.removeEventListener('keydown', handleKeydown)
529
349
  })
350
+
351
+ defineExpose({
352
+ openMenu: () => controller.openMenu(),
353
+ closeMenu: () => controller.closeMenu(),
354
+ toggleMenu: () => controller.toggleMenu(),
355
+ })
530
356
  </script>
531
357
 
532
358
  <style scoped>
533
359
  .indicator-selector {
534
- margin: 20px;
535
- width: 80%;
536
- position: relative;
537
- }
538
-
539
- .indicator-scroll-container {
540
- width: 100%;
541
- overflow-x: auto;
542
- overflow-y: hidden;
543
- scrollbar-width: none;
544
- -webkit-overflow-scrolling: touch;
545
- text-align: center;
546
- }
547
-
548
- .indicator-scroll-container::-webkit-scrollbar {
549
360
  display: none;
550
361
  }
551
362
 
552
- .indicator-list {
553
- display: inline-flex;
554
- gap: 8px;
555
- padding: 2px;
556
- margin: 0 auto;
557
- }
558
-
559
- .indicator-divider {
560
- width: 1px;
561
- height: 20px;
562
- align-self: center;
563
- background: var(--klc-color-axis-line);
564
- }
565
-
566
- .indicator-item {
567
- display: flex;
568
- align-items: center;
569
- gap: 4px;
570
- }
571
-
572
- .indicator-item.draggable,
573
- .indicator-item.draggable .indicator-btn,
574
- .indicator-item.draggable:hover,
575
- .indicator-item.draggable:hover .indicator-btn {
576
- cursor: move;
577
- }
578
-
579
- .indicator-item.is-dragging {
580
- opacity: 0.6;
581
- }
582
-
583
- .indicator-item.drag-over .indicator-btn {
584
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--klc-color-foreground) 12%, transparent);
585
- }
586
-
587
- .indicator-btn-wrapper {
588
- position: relative;
589
- }
590
-
591
- .indicator-btn {
592
- position: relative;
593
- flex-shrink: 0;
594
- padding: 6px 16px;
595
- border: none;
596
- border-radius: 16px;
597
- background: var(--klc-color-tag-bg-white);
598
- color: var(--klc-color-axis-text);
599
- font-size: 13px;
600
- font-weight: 500;
601
- cursor: pointer;
602
- transition: all 0.3s ease;
603
- white-space: nowrap;
604
- display: flex;
605
- align-items: center;
606
- justify-content: center;
607
- gap: 4px;
608
- overflow: hidden;
609
- }
610
-
611
- .indicator-btn:hover:not(.hovering) {
612
- background: var(--klc-color-tag-bg-hover);
613
- color: var(--klc-color-foreground);
614
- }
615
-
616
- .indicator-btn.active {
617
- background: var(--klc-color-tag-bg-hover);
618
- color: var(--klc-color-foreground);
619
- }
620
-
621
- .indicator-btn.active:hover:not(.hovering) {
622
- background: var(--klc-color-tag-bg-hover);
623
- }
624
-
625
- .btn-content {
626
- position: relative;
627
- z-index: 1;
628
- }
629
-
630
- .param-hint {
631
- font-size: 11px;
632
- opacity: 0.85;
633
- }
634
-
635
- /* 悬浮操作层 */
636
- .hover-overlay {
637
- position: absolute;
638
- top: 0;
639
- left: 0;
640
- right: 0;
641
- bottom: 0;
642
- display: flex;
643
- align-items: center;
644
- justify-content: center;
645
- gap: 4px;
646
- background: color-mix(in srgb, var(--klc-color-background) 85%, transparent);
647
- backdrop-filter: blur(4px);
648
- border-radius: 16px;
649
- z-index: 2;
650
- }
651
-
652
- .action-btn {
653
- width: 24px;
654
- height: 24px;
655
- padding: 0;
656
- border: none;
657
- border-radius: 50%;
658
- background: transparent;
659
- color: var(--klc-color-axis-text);
660
- cursor: pointer;
661
- display: flex;
662
- align-items: center;
663
- justify-content: center;
664
- transition: all 0.2s;
665
- }
666
-
667
- .action-btn:hover {
668
- background: var(--klc-color-tag-bg-hover);
669
- color: var(--klc-color-foreground);
670
- }
671
-
672
- .settings-btn:hover {
673
- color: var(--klc-color-foreground);
674
- }
675
-
676
- .remove-btn:hover {
677
- color: #ff4d4f;
678
- }
679
-
680
- .divider {
681
- width: 1px;
682
- height: 14px;
683
- background: var(--klc-color-border-button);
684
- }
685
-
686
- /* 添加按钮 */
687
- .add-btn {
688
- flex-shrink: 0;
689
- width: 32px;
690
- height: 32px;
691
- padding: 0;
692
- border: none;
693
- border-radius: 50%;
694
- background: transparent;
695
- color: var(--klc-color-axis-text);
696
- cursor: pointer;
697
- display: flex;
698
- align-items: center;
699
- justify-content: center;
700
- transition: all 0.3s ease;
701
- }
702
-
703
- .add-btn:hover {
704
- color: var(--klc-color-foreground);
705
- background: var(--klc-color-tag-bg-hover);
706
- }
707
-
708
363
  /* ─────────────────────────────────────────────────────────────────
709
364
  弹窗样式 - 与其他弹窗保持一致
710
365
  ───────────────────────────────────────────────────────────────── */
@@ -823,6 +478,14 @@ onUnmounted(() => {
823
478
  color: var(--klc-color-foreground);
824
479
  }
825
480
 
481
+ /* 搜索区域 */
482
+ .modal-search-area {
483
+ padding: 16px 20px;
484
+ background: var(--klc-color-tag-bg-white);
485
+ border-bottom: 1px solid var(--klc-color-border-chart);
486
+ flex-shrink: 0;
487
+ }
488
+
826
489
  /* 弹窗主体 */
827
490
  .modal-body {
828
491
  padding: 20px;
@@ -851,6 +514,7 @@ onUnmounted(() => {
851
514
  padding: 10px 14px;
852
515
  border: 1px solid var(--klc-color-border-button);
853
516
  border-radius: 8px;
517
+ background: var(--klc-color-background);
854
518
  transition: all 0.2s ease;
855
519
  }
856
520
 
@@ -1,5 +1,12 @@
1
1
  <template>
2
2
  <div ref="chartWrapperRef" class="chart-wrapper" :data-theme="chartTheme" :style="themeCssVars">
3
+ <TopToolbar
4
+ :symbol="semanticConfig.data.symbol"
5
+ :k-line-level="kLineLevel"
6
+ @add-overlay-symbol="$emit('addOverlaySymbol')"
7
+ @k-line-level-change="onKLineLevelChange"
8
+ @toggle-indicator="onToggleIndicator"
9
+ />
3
10
  <div
4
11
  class="chart-stage"
5
12
  :class="{
@@ -99,6 +106,7 @@
99
106
  </div>
100
107
  </div>
101
108
  <IndicatorSelector
109
+ ref="indicatorSelectorRef"
102
110
  :active-indicators="activeIndicators"
103
111
  :indicator-params="indicatorParams"
104
112
  @toggle="handleIndicatorToggle"
@@ -133,14 +141,17 @@ import {
133
141
  zoomLevelToKWidth,
134
142
  kGapFromKWidth,
135
143
  getPhysicalKLineConfig,
136
- SUB_PANE_INDICATOR_CONFIGS,
137
- SUB_PANE_INDICATORS,
138
144
  DrawingInteractionController,
139
145
  } from '@363045841yyt/klinechart-core/controllers'
146
+ import {
147
+ getRegisteredIndicatorDefinition,
148
+ getRegisteredIndicatorDefinitions,
149
+ } from '@363045841yyt/klinechart-core/indicators'
140
150
  import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
141
151
  import type { ChartSettings } from '@363045841yyt/klinechart-core/config'
142
152
  import { resolveThemeColors, themeToCssVars, lightTheme, darkTheme, type ColorPresetSettings } from '@363045841yyt/klinechart-core'
143
153
  import LeftToolbar from './LeftToolbar.vue'
154
+ import TopToolbar from './TopToolbar.vue'
144
155
 
145
156
  const props = withDefaults(
146
157
  defineProps<{
@@ -184,13 +195,23 @@ const emit = defineEmits<{
184
195
  (e: 'zoomLevelChange', level: number, kWidth: number): void
185
196
  (e: 'toggleFullscreen'): void
186
197
  (e: 'themeChange', theme: 'light' | 'dark'): void
198
+ (e: 'addOverlaySymbol'): void
199
+ (e: 'kLineLevelChange', level: string): void
187
200
  }>()
188
201
 
202
+ const kLineLevel = ref(props.semanticConfig.data.period)
203
+
204
+ function onKLineLevelChange(level: string) {
205
+ kLineLevel.value = level as typeof kLineLevel.value
206
+ emit('kLineLevelChange', level)
207
+ }
208
+
189
209
  const containerRef = ref<HTMLDivElement | null>(null)
190
210
  const chartMainRef = ref<HTMLDivElement | null>(null)
191
211
  const chartWrapperRef = ref<HTMLDivElement | null>(null)
192
212
  const tooltipLayerRef = ref<HTMLDivElement | null>(null)
193
213
  const toolbarRef = ref<InstanceType<typeof LeftToolbar> | null>(null)
214
+ const indicatorSelectorRef = ref<InstanceType<typeof IndicatorSelector> | null>(null)
194
215
  provideFullscreenTeleportTarget(chartWrapperRef)
195
216
 
196
217
  /* ========== 图表控制器 ========== */
@@ -475,10 +496,15 @@ function handleSelectTool(toolId: string) {
475
496
  drawingController.value?.setTool(toolId as DrawingToolId)
476
497
  }
477
498
 
499
+ function onToggleIndicator() {
500
+ indicatorSelectorRef.value?.toggleMenu()
501
+ }
502
+
478
503
  function onUpdateDrawingStyle(style: Partial<DrawingStyle>) {
479
504
  const d = selectedDrawing.value
480
505
  if (!d || !drawingController.value) return
481
506
  drawingController.value.updateDrawingStyle(d.id, style)
507
+ drawings.value = drawingController.value.getDrawings()
482
508
  }
483
509
 
484
510
  function onDeleteDrawing() {
@@ -614,7 +640,19 @@ function buildPaneLayoutIntent(): PaneSpec[] {
614
640
  function getDefaultParams(
615
641
  indicatorId: SubIndicatorType,
616
642
  ): Record<string, number | boolean | string> {
617
- return { ...SUB_PANE_INDICATOR_CONFIGS[indicatorId].defaultParams }
643
+ if (indicatorId === 'VOLUME') return {}
644
+ const meta = getRegisteredIndicatorDefinition(indicatorId)
645
+ if (meta?.runtime?.defaultConfig) {
646
+ return { ...meta.runtime.defaultConfig } as Record<string, number | boolean | string>
647
+ }
648
+ return {}
649
+ }
650
+
651
+ // 副图指标判定(基于 registry category + VOLUME 特例)
652
+ function isSubPaneIndicator(id: string): boolean {
653
+ if (id === 'VOLUME') return true
654
+ const def = getRegisteredIndicatorDefinition(id)
655
+ return !!def && def.category !== 'main'
618
656
  }
619
657
 
620
658
  // 副图实例计数器:用于生成 'RSI_0', 'MACD_0' 这样的 paneId
@@ -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
@@ -740,7 +778,7 @@ function handleUpdateParams(indicatorId: string, params: Record<string, unknown>
740
778
  controller.value?.updateIndicatorParams(indicatorId, params)
741
779
  return
742
780
  }
743
- if (SUB_PANE_INDICATORS.includes(indicatorId as SubIndicatorType)) {
781
+ if (isSubPaneIndicator(indicatorId)) {
744
782
  subPanes.value
745
783
  .filter((p) => p.indicatorId === indicatorId)
746
784
  .forEach((pane) => {
@@ -753,7 +791,7 @@ function handleReorderSubIndicators(orderedIndicatorIds: string[]) {
753
791
  if (!orderedIndicatorIds.length || subPanes.value.length <= 1) return
754
792
 
755
793
  const validOrder = orderedIndicatorIds.filter((id): id is SubIndicatorType =>
756
- SUB_PANE_INDICATORS.includes(id as SubIndicatorType),
794
+ isSubPaneIndicator(id),
757
795
  )
758
796
  if (!validOrder.length) return
759
797
 
@@ -1161,20 +1199,23 @@ watch(
1161
1199
 
1162
1200
  display: flex;
1163
1201
  align-items: center;
1164
- justify-content: center;
1165
1202
  width: var(--kmap-width);
1166
- height: var(--kmap-height);
1203
+ height: calc(var(--kmap-height) - 32px);
1167
1204
  min-height: 300px;
1168
1205
  flex-direction: column;
1206
+ margin: 16px 0;
1207
+ padding: 0;
1208
+ box-sizing: border-box;
1209
+ gap: 4px;
1169
1210
  }
1170
1211
 
1171
1212
  .chart-stage {
1172
1213
  width: 95%;
1173
- height: 85%;
1214
+ flex: 1;
1174
1215
  min-height: 255px;
1175
1216
  display: flex;
1176
1217
  align-items: stretch;
1177
- gap: 8px;
1218
+ gap: 4px;
1178
1219
  }
1179
1220
 
1180
1221
  .chart-main {
@@ -1243,7 +1284,7 @@ watch(
1243
1284
  -ms-overflow-style: none;
1244
1285
  border: 1px solid var(--chart-border);
1245
1286
  border-right: 0;
1246
- border-radius: 6px 0 0 6px;
1287
+ border-radius: 3px 0 0 3px;
1247
1288
  box-sizing: border-box;
1248
1289
  background: var(--chart-bg);
1249
1290
 
@@ -1266,8 +1307,8 @@ watch(
1266
1307
  background: var(--chart-bg);
1267
1308
  overflow: visible;
1268
1309
  border: 1px solid var(--chart-border);
1269
- border-top-right-radius: 6px;
1270
- border-bottom-right-radius: 6px;
1310
+ border-top-right-radius: 3px;
1311
+ border-bottom-right-radius: 3px;
1271
1312
 
1272
1313
  -webkit-touch-callout: none;
1273
1314
  -webkit-user-select: none;
@@ -1311,8 +1352,12 @@ watch(
1311
1352
  }
1312
1353
 
1313
1354
  @media (max-width: 768px), (max-height: 640px) {
1355
+ .chart-wrapper {
1356
+ gap: 4px;
1357
+ }
1358
+
1314
1359
  .chart-stage {
1315
- gap: 6px;
1360
+ gap: 4px;
1316
1361
  }
1317
1362
  }
1318
1363
  </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>