@363045841yyt/klinechart 0.8.4 → 0.8.6
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/README.md +6 -1
- package/dist/components/BaseModal.vue.d.ts +54 -0
- package/dist/components/BaseModal.vue.d.ts.map +1 -0
- package/dist/components/BatchStockDialog.vue.d.ts +13 -0
- package/dist/components/BatchStockDialog.vue.d.ts.map +1 -0
- package/dist/components/ChartSettingsDialog.vue.d.ts.map +1 -1
- package/dist/components/ColorPresetPanel.vue.d.ts +4 -1
- package/dist/components/ColorPresetPanel.vue.d.ts.map +1 -1
- package/dist/components/CompareSymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/DrawingStyleToolbar.vue.d.ts.map +1 -1
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/ExportProgressDialog.vue.d.ts +15 -0
- package/dist/components/ExportProgressDialog.vue.d.ts.map +1 -0
- package/dist/components/IndicatorParams.vue.d.ts.map +1 -1
- package/dist/components/IndicatorSelector.vue.d.ts.map +1 -1
- package/dist/components/KLineChart.vue.d.ts +5 -9
- package/dist/components/KLineChart.vue.d.ts.map +1 -1
- package/dist/components/LeftToolbar.vue.d.ts.map +1 -1
- package/dist/components/RangeSelectionExport.vue.d.ts +23 -0
- package/dist/components/RangeSelectionExport.vue.d.ts.map +1 -0
- package/dist/components/SymbolSelector.vue.d.ts.map +1 -1
- package/dist/components/TopToolbar.vue.d.ts.map +1 -1
- package/dist/components/common/CanvasToolbar.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbar.vue.d.ts.map +1 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts +14 -0
- package/dist/components/common/CanvasToolbarStack.vue.d.ts.map +1 -0
- package/dist/composables/chart/useChartTheme.d.ts +329 -0
- package/dist/composables/chart/useChartTheme.d.ts.map +1 -0
- package/dist/composables/chart/useDrawingManager.d.ts +86 -0
- package/dist/composables/chart/useDrawingManager.d.ts.map +1 -0
- package/dist/composables/chart/useIndicatorManager.d.ts +38 -0
- package/dist/composables/chart/useIndicatorManager.d.ts.map +1 -0
- package/dist/composables/chart/useRangeSelection.d.ts +66 -0
- package/dist/composables/chart/useRangeSelection.d.ts.map +1 -0
- package/dist/composables/useTeleportedPopup.d.ts +8 -0
- package/dist/composables/useTeleportedPopup.d.ts.map +1 -0
- package/dist/index.cjs +9 -2
- package/dist/index.css +1 -1
- package/dist/index.js +2149 -1409
- package/dist/tools/calcRangeOverlayPixel.d.ts +15 -0
- package/dist/tools/calcRangeOverlayPixel.d.ts.map +1 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts +4 -0
- package/dist/tools/getKLineIndexByTimestamp.d.ts.map +1 -0
- package/dist/web-component.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BaseModal.vue +292 -0
- package/src/components/BatchStockDialog.vue +128 -0
- package/src/components/ChartSettingsDialog.vue +248 -405
- package/src/components/ColorPresetPanel.vue +58 -106
- package/src/components/CompareSymbolSelector.vue +37 -10
- package/src/components/DrawingStyleToolbar.vue +33 -72
- package/src/components/Dropdown.vue +42 -19
- package/src/components/ExportProgressDialog.vue +118 -0
- package/src/components/IndicatorParams.vue +194 -321
- package/src/components/IndicatorSelector.vue +188 -405
- package/src/components/KLineChart.vue +228 -403
- package/src/components/LeftToolbar.vue +3 -2
- package/src/components/RangeSelectionExport.vue +117 -0
- package/src/components/SymbolSelector.vue +37 -10
- package/src/components/TopToolbar.vue +55 -2
- package/src/components/common/CanvasToolbar.vue +70 -0
- package/src/components/common/CanvasToolbarStack.vue +32 -0
- package/src/composables/chart/useChartTheme.ts +86 -0
- package/src/composables/chart/useDrawingManager.ts +67 -0
- package/src/composables/chart/useIndicatorManager.ts +307 -0
- package/src/composables/chart/useRangeSelection.ts +424 -0
- package/src/composables/useTeleportedPopup.ts +46 -0
- package/src/tools/calcRangeOverlayPixel.ts +28 -0
- package/src/tools/getKLineIndexByTimestamp.ts +40 -0
|
@@ -139,6 +139,7 @@ import IconTablerShape from '~icons/tabler/shape'
|
|
|
139
139
|
import IconTablerChartDots3 from '~icons/tabler/chart-dots-3'
|
|
140
140
|
import IconTablerCaretUpDown from '~icons/tabler/caret-up-down'
|
|
141
141
|
import IconTablerBrackets from '~icons/tabler/brackets'
|
|
142
|
+
import IconTablerArrowsHorizontal from '~icons/tabler/arrows-horizontal'
|
|
142
143
|
import {
|
|
143
144
|
DEFAULT_SETTINGS,
|
|
144
145
|
SETTINGS_STORAGE_KEY,
|
|
@@ -181,6 +182,7 @@ const primaryTools: ToolDef[] = [
|
|
|
181
182
|
{ id: 'disjoint-channel', title: '不相交通道', icon: IconTablerBrackets },
|
|
182
183
|
],
|
|
183
184
|
},
|
|
185
|
+
{ id: 'range-select', title: '导出区间数据', icon: IconTablerArrowsHorizontal },
|
|
184
186
|
]
|
|
185
187
|
|
|
186
188
|
defineProps<{
|
|
@@ -416,7 +418,7 @@ onUnmounted(() => {
|
|
|
416
418
|
gap: 4px;
|
|
417
419
|
padding: 0 5px;
|
|
418
420
|
height: 40px;
|
|
419
|
-
background: var(--klc-color-
|
|
421
|
+
background: var(--klc-color-background);
|
|
420
422
|
backdrop-filter: blur(8px);
|
|
421
423
|
-webkit-backdrop-filter: blur(8px);
|
|
422
424
|
border: 1px solid var(--klc-color-border-chart);
|
|
@@ -482,5 +484,4 @@ onUnmounted(() => {
|
|
|
482
484
|
height: 36px;
|
|
483
485
|
}
|
|
484
486
|
}
|
|
485
|
-
|
|
486
487
|
</style>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<CanvasToolbar>
|
|
3
|
+
<input
|
|
4
|
+
class="range-input"
|
|
5
|
+
:value="startDate"
|
|
6
|
+
@input="$emit('update:startDate', ($event.target as HTMLInputElement).value)"
|
|
7
|
+
:placeholder="startLabel"
|
|
8
|
+
/>
|
|
9
|
+
<span class="range-sep">~</span>
|
|
10
|
+
<input
|
|
11
|
+
class="range-input"
|
|
12
|
+
:value="endDate"
|
|
13
|
+
@input="$emit('update:endDate', ($event.target as HTMLInputElement).value)"
|
|
14
|
+
:placeholder="endLabel"
|
|
15
|
+
/>
|
|
16
|
+
<span class="range-count">共 {{ count }} 条</span>
|
|
17
|
+
<button type="button" class="toolbar-btn" title="批量设置" @click="$emit('batchSetting')">
|
|
18
|
+
批量设置
|
|
19
|
+
</button>
|
|
20
|
+
<button type="button" class="toolbar-btn" title="导出" @click="$emit('export')">导出</button>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="toolbar-btn toolbar-btn--delete"
|
|
24
|
+
title="取消选区"
|
|
25
|
+
@click="$emit('clear')"
|
|
26
|
+
>
|
|
27
|
+
<svg
|
|
28
|
+
class="delete-icon"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
stroke-width="2"
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
stroke-linejoin="round"
|
|
35
|
+
aria-hidden="true"
|
|
36
|
+
>
|
|
37
|
+
<path d="M3 6h18" />
|
|
38
|
+
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
39
|
+
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
</CanvasToolbar>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import CanvasToolbar from './common/CanvasToolbar.vue'
|
|
47
|
+
|
|
48
|
+
defineProps<{
|
|
49
|
+
startDate: string
|
|
50
|
+
endDate: string
|
|
51
|
+
startLabel: string
|
|
52
|
+
endLabel: string
|
|
53
|
+
count: number
|
|
54
|
+
}>()
|
|
55
|
+
|
|
56
|
+
defineEmits<{
|
|
57
|
+
'update:startDate': [value: string]
|
|
58
|
+
'update:endDate': [value: string]
|
|
59
|
+
export: []
|
|
60
|
+
clear: []
|
|
61
|
+
batchSetting: []
|
|
62
|
+
}>()
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.range-input {
|
|
67
|
+
color: var(--klc-color-axis-text);
|
|
68
|
+
font-size: 12px;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
border: none;
|
|
71
|
+
background: transparent;
|
|
72
|
+
outline: none;
|
|
73
|
+
padding: 0 8px;
|
|
74
|
+
width: auto;
|
|
75
|
+
field-sizing: content;
|
|
76
|
+
min-width: 60px;
|
|
77
|
+
height: 26px;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
font-family: inherit;
|
|
80
|
+
border-radius: 4px;
|
|
81
|
+
text-align: center;
|
|
82
|
+
transition:
|
|
83
|
+
background 0.15s ease,
|
|
84
|
+
color 0.15s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.range-input::placeholder {
|
|
88
|
+
color: var(--klc-color-axis-text);
|
|
89
|
+
opacity: 0.6;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.range-input:hover,
|
|
93
|
+
.range-input:focus {
|
|
94
|
+
background: var(--klc-color-grid-minor);
|
|
95
|
+
color: var(--klc-color-foreground);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.range-sep {
|
|
99
|
+
color: var(--klc-color-axis-text);
|
|
100
|
+
font-size: 12px;
|
|
101
|
+
opacity: 0.6;
|
|
102
|
+
user-select: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.range-count {
|
|
106
|
+
color: var(--klc-color-axis-text);
|
|
107
|
+
font-size: 12px;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
user-select: none;
|
|
110
|
+
padding: 0 8px;
|
|
111
|
+
margin-right: 4px;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
height: 18px;
|
|
115
|
+
border-right: 1px solid var(--klc-color-border-button);
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -13,8 +13,16 @@
|
|
|
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>
|
|
16
|
-
<
|
|
17
|
-
<
|
|
16
|
+
<Teleport :to="teleportTarget">
|
|
17
|
+
<Transition name="symbol-popover">
|
|
18
|
+
<div
|
|
19
|
+
v-if="showPopup"
|
|
20
|
+
ref="popupRef"
|
|
21
|
+
class="symbol-popover"
|
|
22
|
+
:style="popupStyle"
|
|
23
|
+
role="dialog"
|
|
24
|
+
aria-label="切换合约"
|
|
25
|
+
>
|
|
18
26
|
<div class="symbol-search">
|
|
19
27
|
<span class="symbol-search__icon" aria-hidden="true">
|
|
20
28
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
@@ -95,14 +103,17 @@
|
|
|
95
103
|
<span class="symbol-list__exchange">{{ item.exchange }}</span>
|
|
96
104
|
</button>
|
|
97
105
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
</div>
|
|
107
|
+
</Transition>
|
|
108
|
+
</Teleport>
|
|
100
109
|
</div>
|
|
101
110
|
</template>
|
|
102
111
|
|
|
103
112
|
<script setup lang="ts">
|
|
104
113
|
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
105
114
|
import IconTablerAlertTriangle from '~icons/tabler/alert-triangle'
|
|
115
|
+
import { useTeleportedPopup } from '../composables/useTeleportedPopup'
|
|
116
|
+
import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
|
|
106
117
|
|
|
107
118
|
export interface SymbolItem {
|
|
108
119
|
code: string
|
|
@@ -126,6 +137,15 @@ const showPopup = ref(false)
|
|
|
126
137
|
const searchQuery = ref('')
|
|
127
138
|
const searchInputRef = ref<HTMLInputElement | null>(null)
|
|
128
139
|
const chipWrapRef = ref<HTMLElement | null>(null)
|
|
140
|
+
const popupRef = ref<HTMLElement | null>(null)
|
|
141
|
+
|
|
142
|
+
const teleportTarget = useFullscreenTeleportTarget()
|
|
143
|
+
|
|
144
|
+
const { popupStyle, startPositionSync, stopPositionSync } = useTeleportedPopup(
|
|
145
|
+
chipWrapRef,
|
|
146
|
+
popupRef,
|
|
147
|
+
8,
|
|
148
|
+
)
|
|
129
149
|
|
|
130
150
|
const currentSymbol = computed<SymbolItem | undefined>(() =>
|
|
131
151
|
props.symbols.find((s) => s.code === props.symbol),
|
|
@@ -155,6 +175,14 @@ function togglePopup() {
|
|
|
155
175
|
}
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
watch(showPopup, (val) => {
|
|
179
|
+
if (val) {
|
|
180
|
+
startPositionSync()
|
|
181
|
+
} else {
|
|
182
|
+
stopPositionSync()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
158
186
|
function clearSearch() {
|
|
159
187
|
searchQuery.value = ''
|
|
160
188
|
searchInputRef.value?.focus()
|
|
@@ -170,7 +198,9 @@ function selectSymbol(item: SymbolItem) {
|
|
|
170
198
|
}
|
|
171
199
|
|
|
172
200
|
function onDocumentClick(e: MouseEvent) {
|
|
173
|
-
|
|
201
|
+
const chip = chipWrapRef.value
|
|
202
|
+
const popup = popupRef.value
|
|
203
|
+
if (chip && !chip.contains(e.target as Node) && !popup?.contains(e.target as Node)) {
|
|
174
204
|
showPopup.value = false
|
|
175
205
|
}
|
|
176
206
|
}
|
|
@@ -238,15 +268,12 @@ watch(() => props.symbol, () => {
|
|
|
238
268
|
}
|
|
239
269
|
|
|
240
270
|
.symbol-popover {
|
|
241
|
-
|
|
242
|
-
top: calc(100% + 8px);
|
|
243
|
-
left: 0;
|
|
244
|
-
z-index: 20;
|
|
271
|
+
z-index: 110;
|
|
245
272
|
width: min(320px, calc(100vw - 24px));
|
|
246
273
|
padding: 14px;
|
|
247
274
|
border: 1px solid var(--klc-color-border-button);
|
|
248
275
|
border-radius: 3px;
|
|
249
|
-
background: var(--klc-color-
|
|
276
|
+
background: var(--klc-color-background);
|
|
250
277
|
color: var(--klc-color-foreground);
|
|
251
278
|
|
|
252
279
|
box-sizing: border-box;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
ref="toolbarRef"
|
|
4
|
+
class="top-toolbar"
|
|
5
|
+
@mousedown="onMouseDown"
|
|
6
|
+
@mousemove="onMouseMove"
|
|
7
|
+
@mouseup="onMouseUp"
|
|
8
|
+
@mouseleave="onMouseUp"
|
|
9
|
+
>
|
|
3
10
|
<SymbolSelector
|
|
4
11
|
v-if="displaySymbol"
|
|
5
12
|
:symbol="displaySymbol"
|
|
@@ -38,7 +45,7 @@
|
|
|
38
45
|
</template>
|
|
39
46
|
|
|
40
47
|
<script setup lang="ts">
|
|
41
|
-
import { computed } from 'vue'
|
|
48
|
+
import { computed, ref } from 'vue'
|
|
42
49
|
import KLineLevelDropdown, { type KLineLevel } from './KLineLevelDropdown.vue'
|
|
43
50
|
import KLineAdjustmentDropdown, { type KLineAdjustment } from './KLineAdjustmentDropdown.vue'
|
|
44
51
|
import SymbolSelector from './SymbolSelector.vue'
|
|
@@ -47,6 +54,41 @@ import type { SymbolItem } from './SymbolSelector.vue'
|
|
|
47
54
|
|
|
48
55
|
export type { SymbolItem }
|
|
49
56
|
|
|
57
|
+
const toolbarRef = ref<HTMLElement | null>(null)
|
|
58
|
+
|
|
59
|
+
let isDown = false
|
|
60
|
+
let startX = 0
|
|
61
|
+
let scrollLeft = 0
|
|
62
|
+
|
|
63
|
+
function onMouseDown(e: MouseEvent) {
|
|
64
|
+
const el = toolbarRef.value
|
|
65
|
+
if (!el) return
|
|
66
|
+
isDown = true
|
|
67
|
+
startX = e.pageX - el.getBoundingClientRect().left
|
|
68
|
+
scrollLeft = el.scrollLeft
|
|
69
|
+
el.style.cursor = 'grabbing'
|
|
70
|
+
el.style.userSelect = 'none'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onMouseMove(e: MouseEvent) {
|
|
74
|
+
if (!isDown) return
|
|
75
|
+
const el = toolbarRef.value
|
|
76
|
+
if (!el) return
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
const x = e.pageX - el.getBoundingClientRect().left
|
|
79
|
+
const walk = x - startX
|
|
80
|
+
el.scrollLeft = scrollLeft - walk
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function onMouseUp() {
|
|
84
|
+
if (!isDown) return
|
|
85
|
+
isDown = false
|
|
86
|
+
const el = toolbarRef.value
|
|
87
|
+
if (!el) return
|
|
88
|
+
el.style.cursor = ''
|
|
89
|
+
el.style.userSelect = ''
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
const props = defineProps<{
|
|
51
93
|
symbol?: string
|
|
52
94
|
kLineLevel?: string
|
|
@@ -115,6 +157,14 @@ function onSymbolSelectorChange(item: SymbolItem) {
|
|
|
115
157
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
116
158
|
box-sizing: border-box;
|
|
117
159
|
user-select: none;
|
|
160
|
+
overflow-x: auto;
|
|
161
|
+
overflow-y: hidden;
|
|
162
|
+
scrollbar-width: none;
|
|
163
|
+
-ms-overflow-style: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.top-toolbar::-webkit-scrollbar {
|
|
167
|
+
display: none;
|
|
118
168
|
}
|
|
119
169
|
|
|
120
170
|
.indicator-button {
|
|
@@ -168,5 +218,8 @@ function onSymbolSelectorChange(item: SymbolItem) {
|
|
|
168
218
|
.indicator-button__text {
|
|
169
219
|
display: none;
|
|
170
220
|
}
|
|
221
|
+
.indicator-button {
|
|
222
|
+
height: 26px;
|
|
223
|
+
}
|
|
171
224
|
}
|
|
172
225
|
</style>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="canvas-toolbar" @pointerdown.stop @pointermove.stop @pointerup.stop>
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<style scoped>
|
|
8
|
+
.canvas-toolbar {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 4px;
|
|
12
|
+
padding: 4px;
|
|
13
|
+
background: color-mix(in srgb, var(--klc-color-background) 92%, transparent);
|
|
14
|
+
backdrop-filter: blur(12px);
|
|
15
|
+
-webkit-backdrop-filter: blur(12px);
|
|
16
|
+
border: 1px solid var(--klc-color-border-button);
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
box-shadow:
|
|
19
|
+
0 2px 8px rgba(0, 0, 0, 0.08),
|
|
20
|
+
0 1px 2px rgba(0, 0, 0, 0.04);
|
|
21
|
+
user-select: none;
|
|
22
|
+
pointer-events: auto;
|
|
23
|
+
overflow-x: auto;
|
|
24
|
+
overflow-y: hidden;
|
|
25
|
+
scrollbar-width: none;
|
|
26
|
+
-ms-overflow-style: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.canvas-toolbar::-webkit-scrollbar {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.canvas-toolbar :deep(.toolbar-btn) {
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
height: 26px;
|
|
38
|
+
padding: 0 10px;
|
|
39
|
+
border: none;
|
|
40
|
+
border-radius: 4px;
|
|
41
|
+
background: transparent;
|
|
42
|
+
color: var(--klc-color-axis-text);
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
transition:
|
|
47
|
+
background 0.15s ease,
|
|
48
|
+
color 0.15s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.canvas-toolbar :deep(.toolbar-btn:hover) {
|
|
52
|
+
background: var(--klc-color-grid-minor);
|
|
53
|
+
color: var(--klc-color-foreground);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.canvas-toolbar :deep(.toolbar-btn--delete) {
|
|
57
|
+
width: 26px;
|
|
58
|
+
padding: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.canvas-toolbar :deep(.toolbar-btn--delete:hover) {
|
|
62
|
+
color: #dc2626;
|
|
63
|
+
background: color-mix(in srgb, #dc2626 10%, transparent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.canvas-toolbar :deep(.delete-icon) {
|
|
67
|
+
width: 14px;
|
|
68
|
+
height: 14px;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="canvas-toolbar-stack"
|
|
4
|
+
@pointerdown.stop
|
|
5
|
+
@pointermove.stop
|
|
6
|
+
@pointerup.stop
|
|
7
|
+
>
|
|
8
|
+
<slot />
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<style scoped>
|
|
13
|
+
.canvas-toolbar-stack {
|
|
14
|
+
position: absolute;
|
|
15
|
+
left: 50%;
|
|
16
|
+
top: 10px;
|
|
17
|
+
transform: translateX(-50%);
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
align-items: stretch;
|
|
21
|
+
gap: 8px;
|
|
22
|
+
z-index: 100;
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
max-width: calc(100% - 20px);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (max-width: 768px) {
|
|
28
|
+
.canvas-toolbar-stack {
|
|
29
|
+
max-width: 90%;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages chart theme state (light/dark), computed CSS vars for theming,
|
|
3
|
+
* tooltip up/down colors, and auto theme detection via prefers-color-scheme.
|
|
4
|
+
* Handles settings persistence through ChartController.updateSettingsFacade.
|
|
5
|
+
*/
|
|
6
|
+
import { ref, computed, onUnmounted } from 'vue'
|
|
7
|
+
import type { Ref } from 'vue'
|
|
8
|
+
import {
|
|
9
|
+
resolveThemeColors,
|
|
10
|
+
themeToCssVars,
|
|
11
|
+
lightTheme,
|
|
12
|
+
darkTheme,
|
|
13
|
+
type ColorPresetSettings,
|
|
14
|
+
} from '@363045841yyt/klinechart-core'
|
|
15
|
+
import type { ChartSettings } from '@363045841yyt/klinechart-core/config'
|
|
16
|
+
import type { ChartController } from '@363045841yyt/klinechart-core/controllers'
|
|
17
|
+
|
|
18
|
+
export function useChartTheme(ctrl: Ref<ChartController | null>) {
|
|
19
|
+
const chartTheme = ref<'light' | 'dark'>('light')
|
|
20
|
+
const chartSettings = ref<ChartSettings>({})
|
|
21
|
+
|
|
22
|
+
const tooltipColors = computed(() => {
|
|
23
|
+
const isAsiaMarket = chartSettings.value.isAsiaMarket ?? false
|
|
24
|
+
const colors = resolveThemeColors(chartTheme.value, isAsiaMarket as boolean | undefined)
|
|
25
|
+
return {
|
|
26
|
+
upColor: colors.candleUpBody,
|
|
27
|
+
downColor: colors.candleDownBody,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const themeCssVars = computed(() => {
|
|
32
|
+
const theme = chartTheme.value === 'dark' ? darkTheme : lightTheme
|
|
33
|
+
const overrides = (chartSettings.value.colorPresetSettings as ColorPresetSettings | undefined)?.[
|
|
34
|
+
chartTheme.value
|
|
35
|
+
]
|
|
36
|
+
if (overrides && Object.keys(overrides).length > 0) {
|
|
37
|
+
return themeToCssVars({ ...theme, colors: { ...theme.colors, ...overrides } })
|
|
38
|
+
}
|
|
39
|
+
return themeToCssVars(theme)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
let autoThemeMediaQuery: MediaQueryList | null = null
|
|
43
|
+
|
|
44
|
+
function onSystemThemeChange(e: MediaQueryListEvent) {
|
|
45
|
+
ctrl.value?.setTheme(e.matches ? 'dark' : 'light')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyThemeFromSettings(themeSetting: string | undefined) {
|
|
49
|
+
const chartCtrl = ctrl.value
|
|
50
|
+
if (!chartCtrl || !themeSetting) return
|
|
51
|
+
|
|
52
|
+
if (themeSetting === 'auto') {
|
|
53
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
54
|
+
chartCtrl.setTheme(mq.matches ? 'dark' : 'light')
|
|
55
|
+
if (autoThemeMediaQuery !== mq) {
|
|
56
|
+
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
57
|
+
autoThemeMediaQuery = mq
|
|
58
|
+
mq.addEventListener('change', onSystemThemeChange)
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
62
|
+
autoThemeMediaQuery = null
|
|
63
|
+
chartCtrl.setTheme(themeSetting as 'light' | 'dark')
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handleSettingsChange(settings: ChartSettings) {
|
|
68
|
+
chartSettings.value = settings
|
|
69
|
+
applyThemeFromSettings(settings.theme as string)
|
|
70
|
+
ctrl.value?.updateSettingsFacade(settings)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onUnmounted(() => {
|
|
74
|
+
autoThemeMediaQuery?.removeEventListener('change', onSystemThemeChange)
|
|
75
|
+
autoThemeMediaQuery = null
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
chartTheme,
|
|
80
|
+
chartSettings,
|
|
81
|
+
tooltipColors,
|
|
82
|
+
themeCssVars,
|
|
83
|
+
handleSettingsChange,
|
|
84
|
+
applyThemeFromSettings,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages drawing interaction state (selected drawing, drawings list),
|
|
3
|
+
* tool activation, style updates, and deletion.
|
|
4
|
+
* Provides setupDrawing() to initialize DrawingInteractionController
|
|
5
|
+
* with lifecycle callbacks that sync back to Vue refs.
|
|
6
|
+
*/
|
|
7
|
+
import { ref, computed, shallowRef, type Ref } from 'vue'
|
|
8
|
+
import {
|
|
9
|
+
DrawingInteractionController,
|
|
10
|
+
type ChartController,
|
|
11
|
+
type DrawingToolId,
|
|
12
|
+
} from '@363045841yyt/klinechart-core/controllers'
|
|
13
|
+
import type { DrawingObject, DrawingStyle } from '@363045841yyt/klinechart-core/plugin'
|
|
14
|
+
|
|
15
|
+
export function useDrawingManager(ctrl: Ref<ChartController | null>) {
|
|
16
|
+
const drawingController = shallowRef<DrawingInteractionController | null>(null)
|
|
17
|
+
const selectedDrawingId = ref<string | null>(null)
|
|
18
|
+
const drawings = ref<DrawingObject[]>([])
|
|
19
|
+
const selectedDrawing = computed(() => {
|
|
20
|
+
const id = selectedDrawingId.value
|
|
21
|
+
if (!id) return null
|
|
22
|
+
return drawings.value.find((d) => d.id === id) ?? null
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function handleSelectTool(toolId: string) {
|
|
26
|
+
drawingController.value?.setTool(toolId as DrawingToolId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function onUpdateDrawingStyle(style: Partial<DrawingStyle>) {
|
|
30
|
+
const d = selectedDrawing.value
|
|
31
|
+
if (!d || !drawingController.value) return
|
|
32
|
+
drawingController.value.updateDrawingStyle(d.id, style)
|
|
33
|
+
drawings.value = drawingController.value.getDrawings()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onDeleteDrawing() {
|
|
37
|
+
const d = selectedDrawing.value
|
|
38
|
+
if (!d || !drawingController.value) return
|
|
39
|
+
drawingController.value.removeDrawing(d.id)
|
|
40
|
+
drawings.value = drawingController.value.getDrawings()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setupDrawing(chartCtrl: ChartController): void {
|
|
44
|
+
drawingController.value = new DrawingInteractionController(chartCtrl)
|
|
45
|
+
drawingController.value.setCallbacks({
|
|
46
|
+
onDrawingCreated: (drawing) => {
|
|
47
|
+
drawings.value = [...drawings.value, drawing]
|
|
48
|
+
selectedDrawingId.value = drawing.id
|
|
49
|
+
},
|
|
50
|
+
onToolChange: () => {},
|
|
51
|
+
onDrawingSelected: (drawing) => {
|
|
52
|
+
selectedDrawingId.value = drawing?.id ?? null
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
drawingController,
|
|
59
|
+
selectedDrawingId,
|
|
60
|
+
selectedDrawing,
|
|
61
|
+
drawings,
|
|
62
|
+
handleSelectTool,
|
|
63
|
+
onUpdateDrawingStyle,
|
|
64
|
+
onDeleteDrawing,
|
|
65
|
+
setupDrawing,
|
|
66
|
+
}
|
|
67
|
+
}
|