@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/dist/components/CompareSymbolSelector.vue.d.ts +2 -0
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/KLineChart.vue.d.ts +0 -2
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts +2 -0
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.css +1 -1
- package/dist/index.js +574 -600
- package/package.json +1 -1
- package/src/components/CompareSymbolSelector.vue +29 -0
- package/src/components/KLineChart.vue +24 -91
- package/src/components/SymbolSelector.vue +12 -3
- package/src/components/TopToolbar.vue +23 -18
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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="
|
|
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">{{
|
|
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
|
-
|
|
62
|
-
{ code: '
|
|
63
|
-
{ code: '
|
|
64
|
-
{ code: '
|
|
65
|
-
{ code: '
|
|
66
|
-
{ code: '
|
|
67
|
-
{ code: '
|
|
68
|
-
{ code: '
|
|
69
|
-
{ code: '
|
|
70
|
-
{ code: '
|
|
71
|
-
{ code: '
|
|
72
|
-
|
|
73
|
-
{ code: 'sh.
|
|
74
|
-
{ code: 'sh.
|
|
75
|
-
{ code: '000858',
|
|
76
|
-
{ code: '000001',
|
|
77
|
-
|
|
78
|
-
{ code: 'MOCK-
|
|
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() ?? '')
|