@363045841yyt/klinechart 0.7.4 → 0.7.5-alpha.2

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,844 +1,844 @@
1
- <template>
2
- <nav class="left-toolbar" aria-label="图表工具栏">
3
- <div class="left-toolbar__group">
4
- <div v-for="tool in primaryTools" :key="tool.id" class="tool-item">
5
- <button
6
- type="button"
7
- class="left-toolbar__button"
8
- :class="{ active: isActive(tool) }"
9
- :title="tool.title"
10
- :aria-label="tool.title"
11
- @click="selectTool(tool)"
12
- @pointerdown.stop
13
- @pointermove.stop
14
- @pointerup.stop
15
- >
16
- <component :is="tool.icon" class="tool-icon" aria-hidden="true" />
17
- <span
18
- v-if="tool.children && tool.children.length"
19
- class="corner-indicator"
20
- :class="{ open: openGroupId === tool.id }"
21
- @click.stop="toggleExpand(tool.id)"
22
- aria-label="展开子菜单"
23
- ></span>
24
- </button>
25
-
26
- <Transition name="dropdown">
27
- <div
28
- v-if="openGroupId === tool.id && tool.children && tool.children.length"
29
- class="tool-dropdown"
30
- @pointerdown.stop
31
- @pointermove.stop
32
- @pointerup.stop
33
- >
34
- <button
35
- v-for="child in tool.children"
36
- :key="child.id"
37
- type="button"
38
- class="left-toolbar__button"
39
- :class="{ active: selectedToolId === child.id }"
40
- :title="child.title"
41
- :aria-label="child.title"
42
- @click="selectChild(child)"
43
- >
44
- <component :is="child.icon" class="tool-icon" aria-hidden="true" />
45
- </button>
46
- </div>
47
- </Transition>
48
- </div>
49
- </div>
50
-
51
- <span class="left-toolbar__divider"></span>
52
-
53
- <div class="left-toolbar__group">
54
- <button
55
- type="button"
56
- class="left-toolbar__button"
57
- title="放大"
58
- aria-label="放大"
59
- @click="$emit('zoomIn')"
60
- @pointerdown.stop
61
- @pointermove.stop
62
- @pointerup.stop
63
- >
64
- <IconTablerZoomIn class="tool-icon" aria-hidden="true" />
65
- </button>
66
- <button
67
- type="button"
68
- class="left-toolbar__button"
69
- title="缩小"
70
- aria-label="缩小"
71
- @click="$emit('zoomOut')"
72
- @pointerdown.stop
73
- @pointermove.stop
74
- @pointerup.stop
75
- >
76
- <IconTablerZoomOut class="tool-icon" aria-hidden="true" />
77
- </button>
78
- </div>
79
-
80
- <span class="left-toolbar__divider"></span>
81
-
82
- <div class="left-toolbar__group">
83
- <button
84
- type="button"
85
- class="left-toolbar__button"
86
- :title="isFullscreen ? '退出全屏' : '全屏显示'"
87
- :aria-label="isFullscreen ? '退出全屏' : '全屏显示'"
88
- @click="$emit('toggleFullscreen')"
89
- @pointerdown.stop
90
- @pointermove.stop
91
- @pointerup.stop
92
- >
93
- <IconTablerMinimize v-if="isFullscreen" class="tool-icon" aria-hidden="true" />
94
- <IconTablerMaximize v-else class="tool-icon" aria-hidden="true" />
95
- </button>
96
- </div>
97
-
98
- <span class="left-toolbar__divider"></span>
99
-
100
- <div class="left-toolbar__group">
101
- <button
102
- type="button"
103
- class="left-toolbar__button"
104
- title="设置"
105
- aria-label="设置"
106
- @click="openSettings"
107
- @pointerdown.stop
108
- @pointermove.stop
109
- @pointerup.stop
110
- >
111
- <IconTablerSettings class="tool-icon" aria-hidden="true" />
112
- </button>
113
- </div>
114
- </nav>
115
-
116
- <!-- 设置弹窗 -->
117
- <Teleport :to="teleportTarget">
118
- <Transition name="overlay">
119
- <div v-if="showSettings" class="settings-overlay" @click="closeSettings">
120
- <Transition name="modal">
121
- <div class="settings-modal" @click.stop>
122
- <!-- 头部 -->
123
- <div class="settings-header">
124
- <div class="header-left">
125
- <span class="settings-title">图表设置</span>
126
- <span class="settings-subtitle">个性化配置</span>
127
- </div>
128
- <div class="header-right">
129
- <button class="settings-close" @click="closeSettings">
130
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
131
- <path d="M18 6L6 18M6 6l12 12" />
132
- </svg>
133
- </button>
134
- </div>
135
- </div>
136
-
137
- <!-- 体部 -->
138
- <div class="settings-body">
139
- <!-- 主图设置 -->
140
- <template v-if="mainSettings.length > 0">
141
- <div class="settings-section-divider">
142
- <span class="settings-section-label">主图设置</span>
143
- </div>
144
- <template v-for="item in mainSettings" :key="item.key">
145
- <div class="settings-item">
146
- <label class="settings-label">
147
- <span>{{ item.label }}</span>
148
- <template v-if="item.type === 'boolean'">
149
- <input
150
- type="checkbox"
151
- class="settings-checkbox"
152
- v-model="settings[item.key]"
153
- />
154
- </template>
155
- <template v-else-if="item.type === 'select' && item.options">
156
- <select class="settings-select" v-model="settings[item.key]">
157
- <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
158
- {{ opt.label }}
159
- </option>
160
- </select>
161
- </template>
162
- </label>
163
- </div>
164
- </template>
165
- </template>
166
-
167
- <!-- 实验性设置 -->
168
- <template v-if="experimentalSettings.length > 0">
169
- <div class="settings-section-divider">
170
- <span class="settings-section-label">实验性 / 调试设置</span>
171
- </div>
172
- <template v-for="item in experimentalSettings" :key="item.key">
173
- <div class="settings-item experimental">
174
- <label class="settings-label">
175
- <span>{{ item.label }}</span>
176
- <template v-if="item.type === 'boolean'">
177
- <input
178
- type="checkbox"
179
- class="settings-checkbox"
180
- v-model="settings[item.key]"
181
- />
182
- </template>
183
- <template v-else-if="item.type === 'select' && item.options">
184
- <select class="settings-select" v-model="settings[item.key]">
185
- <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
186
- {{ opt.label }}
187
- </option>
188
- </select>
189
- </template>
190
- </label>
191
- </div>
192
- </template>
193
- </template>
194
- </div>
195
-
196
- <!-- 底部 -->
197
- <div class="settings-footer">
198
- <button class="settings-btn reset" @click="resetSettings">
199
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
200
- <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
201
- <path d="M3 3v5h5" />
202
- </svg>
203
- 重置
204
- </button>
205
- <div class="footer-right">
206
- <button class="settings-btn cancel" @click="closeSettings">取消</button>
207
- <button class="settings-btn confirm" @click="confirmSettings">
208
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
209
- <path d="M20 6L9 17l-5-5" />
210
- </svg>
211
- 确定
212
- </button>
213
- </div>
214
- </div>
215
- </div>
216
- </Transition>
217
- </div>
218
- </Transition>
219
- </Teleport>
220
- </template>
221
-
222
- <script setup lang="ts">
223
- import { ref, computed, onMounted, onUnmounted } from 'vue'
224
- import IconTablerPointer from '~icons/tabler/pointer'
225
- import IconTablerChartLine from '~icons/tabler/chart-line'
226
- import IconTablerArrowUpRight from '~icons/tabler/arrow-up-right'
227
- import IconTablerArrowRight from '~icons/tabler/arrow-right'
228
- import IconTablerMinus from '~icons/tabler/minus'
229
- import IconTablerSeparator from '~icons/tabler/separator'
230
- import IconTablerCrosshair from '~icons/tabler/crosshair'
231
- import IconTablerInfoCircle from '~icons/tabler/info-circle'
232
- import IconTablerZoomIn from '~icons/tabler/zoom-in'
233
- import IconTablerZoomOut from '~icons/tabler/zoom-out'
234
- import IconTablerMaximize from '~icons/tabler/maximize'
235
- import IconTablerMinimize from '~icons/tabler/minimize'
236
- import IconTablerSettings from '~icons/tabler/settings'
237
- import IconTablerShape from '~icons/tabler/shape'
238
- import IconTablerChartDots3 from '~icons/tabler/chart-dots-3'
239
- import IconTablerCaretUpDown from '~icons/tabler/caret-up-down'
240
- import IconTablerBrackets from '~icons/tabler/brackets'
241
- import {
242
- DEFAULT_SETTINGS,
243
- SETTINGS_STORAGE_KEY,
244
- type SettingItem,
245
- } from '@363045841yyt/klinechart-core/config'
246
- import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
247
- import { setCanvasProfilerEnabled } from '../debug/canvasProfiler'
248
-
249
- export interface ToolDef {
250
- id: string
251
- title: string
252
- icon: unknown
253
- children?: ToolDef[]
254
- }
255
-
256
- const primaryTools: ToolDef[] = [
257
- { id: 'cursor', title: '光标', icon: IconTablerPointer },
258
- {
259
- id: 'lines',
260
- title: '线条',
261
- icon: IconTablerChartLine,
262
- children: [
263
- { id: 'trend-line', title: '线段', icon: IconTablerChartLine },
264
- { id: 'ray', title: '射线', icon: IconTablerArrowUpRight },
265
- { id: 'h-line', title: '水平线', icon: IconTablerMinus },
266
- { id: 'h-ray', title: '水平射线', icon: IconTablerArrowRight },
267
- { id: 'v-line', title: '垂直线', icon: IconTablerSeparator },
268
- { id: 'crosshair-line', title: '十字线', icon: IconTablerCrosshair },
269
- { id: 'info-line', title: '信息线', icon: IconTablerInfoCircle },
270
- ],
271
- },
272
- {
273
- id: 'channels',
274
- title: '通道',
275
- icon: IconTablerShape,
276
- children: [
277
- { id: 'parallel-channel', title: '平行通道', icon: IconTablerShape },
278
- { id: 'regression-channel', title: '回归趋势', icon: IconTablerChartDots3 },
279
- { id: 'flat-line', title: '平滑顶底', icon: IconTablerCaretUpDown },
280
- { id: 'disjoint-channel', title: '不相交通道', icon: IconTablerBrackets },
281
- ],
282
- },
283
- ]
284
-
285
- defineProps<{
286
- isFullscreen?: boolean
287
- }>()
288
-
289
- const emit = defineEmits<{
290
- (e: 'selectTool', toolId: string): void
291
- (e: 'toggleFullscreen'): void
292
- (e: 'zoomIn'): void
293
- (e: 'zoomOut'): void
294
- (e: 'settingsChange', settings: Record<string, boolean | string>): void
295
- }>()
296
-
297
- const selectedToolId = ref('cursor')
298
- const openGroupId = ref<string | null>(null)
299
- const showSettings = ref(false)
300
-
301
- const teleportTarget = useFullscreenTeleportTarget()
302
-
303
- const mainSettings = computed(
304
- () => DEFAULT_SETTINGS.filter((s) => s.group === 'main') as unknown as SettingItem[],
305
- )
306
- const experimentalSettings = computed(
307
- () => DEFAULT_SETTINGS.filter((s) => s.group === 'experimental') as unknown as SettingItem[],
308
- )
309
-
310
- function loadSettings(): Record<string, boolean | string> {
311
- try {
312
- const saved = localStorage.getItem(SETTINGS_STORAGE_KEY)
313
- if (saved) {
314
- const parsed = JSON.parse(saved)
315
- const result: Record<string, boolean | string> = {}
316
- DEFAULT_SETTINGS.forEach((item) => {
317
- result[item.key] = parsed[item.key] ?? item.default
318
- })
319
- return result
320
- }
321
- } catch {}
322
- const defaults: Record<string, boolean | string> = {}
323
- DEFAULT_SETTINGS.forEach((item) => {
324
- defaults[item.key] = item.default
325
- })
326
- return defaults
327
- }
328
-
329
- function saveSettings(settings: Record<string, boolean | string>) {
330
- try {
331
- localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings))
332
- } catch {}
333
- }
334
-
335
- const appliedSettings = ref<Record<string, boolean | string>>(loadSettings())
336
- const settings = ref<Record<string, boolean | string>>({ ...appliedSettings.value })
337
-
338
- function isActive(tool: ToolDef): boolean {
339
- if (selectedToolId.value === tool.id) return true
340
- if (tool.children) {
341
- return tool.children.some((c) => c.id === selectedToolId.value)
342
- }
343
- return false
344
- }
345
-
346
- function selectTool(tool: ToolDef) {
347
- if (tool.children?.length) {
348
- const hasActiveChild = tool.children.some((c) => c.id === selectedToolId.value)
349
- if (!hasActiveChild) {
350
- const first = tool.children[0]!
351
- selectedToolId.value = first.id
352
- emit('selectTool', first.id)
353
- }
354
- toggleExpand(tool.id)
355
- return
356
- }
357
- selectedToolId.value = tool.id
358
- emit('selectTool', tool.id)
359
- openGroupId.value = null
360
- }
361
-
362
- function selectChild(child: ToolDef) {
363
- selectedToolId.value = child.id
364
- emit('selectTool', child.id)
365
- openGroupId.value = null
366
- }
367
-
368
- function toggleExpand(groupId: string) {
369
- openGroupId.value = openGroupId.value === groupId ? null : groupId
370
- }
371
-
372
- function openSettings() {
373
- settings.value = { ...appliedSettings.value }
374
- showSettings.value = true
375
- }
376
-
377
- function closeSettings() {
378
- showSettings.value = false
379
- }
380
-
381
- function resetSettings() {
382
- const defaults: Record<string, boolean | string> = {}
383
- DEFAULT_SETTINGS.forEach((item) => {
384
- defaults[item.key] = item.default
385
- })
386
- settings.value = defaults
387
- }
388
-
389
- function confirmSettings() {
390
- appliedSettings.value = { ...settings.value }
391
- saveSettings(appliedSettings.value)
392
- setCanvasProfilerEnabled(!!appliedSettings.value['enableCanvasProfiler'])
393
- emit('settingsChange', { ...appliedSettings.value })
394
- closeSettings()
395
- }
396
-
397
- function getCurrentSettings(): Record<string, boolean | string> {
398
- return { ...appliedSettings.value }
399
- }
400
-
401
- defineExpose({
402
- getSettings: getCurrentSettings,
403
- })
404
-
405
- function handleClickOutside(e: MouseEvent) {
406
- const target = e.target as HTMLElement
407
- if (!target.closest('.tool-item')) {
408
- openGroupId.value = null
409
- }
410
- }
411
-
412
- onMounted(() => {
413
- document.addEventListener('click', handleClickOutside, true)
414
- emit('settingsChange', { ...appliedSettings.value })
415
- setCanvasProfilerEnabled(!!appliedSettings.value['enableCanvasProfiler'])
416
- })
417
-
418
- onUnmounted(() => {
419
- document.removeEventListener('click', handleClickOutside, true)
420
- })
421
- </script>
422
-
423
- <style scoped>
424
- .left-toolbar {
425
- flex: 0 0 40px;
426
- display: flex;
427
- flex-direction: column;
428
- align-items: center;
429
- gap: 6px;
430
- padding: 8px 5px;
431
- border: 1px solid #e5e7eb;
432
- border-radius: 6px;
433
- background: #fafbfc;
434
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
435
- box-sizing: border-box;
436
- user-select: none;
437
- }
438
-
439
- .left-toolbar__group {
440
- display: flex;
441
- flex-direction: column;
442
- gap: 4px;
443
- }
444
-
445
- .left-toolbar__divider {
446
- width: 18px;
447
- height: 1px;
448
- background: #e5e7eb;
449
- }
450
-
451
- /* --- 工具按钮 --- */
452
- .left-toolbar__button {
453
- position: relative;
454
- width: 28px;
455
- height: 28px;
456
- padding: 0;
457
- border: 1px solid transparent;
458
- border-radius: 4px;
459
- background: transparent;
460
- color: #6b7280;
461
- cursor: pointer;
462
- display: inline-flex;
463
- align-items: center;
464
- justify-content: center;
465
- transition:
466
- border-color 0.15s ease,
467
- background 0.15s ease,
468
- color 0.15s ease;
469
- }
470
-
471
- .left-toolbar__button:hover {
472
- border-color: #d1d5db;
473
- background: #f3f4f6;
474
- color: #374151;
475
- }
476
-
477
- .left-toolbar__button.active {
478
- border-color: #9ca3af;
479
- background: #e5e7eb;
480
- color: #1f2937;
481
- }
482
-
483
- .left-toolbar__button:focus-visible {
484
- outline: none;
485
- border-color: #6b7280;
486
- }
487
-
488
- .tool-icon {
489
- width: 16px;
490
- height: 16px;
491
- }
492
-
493
- /* --- 角标三角(TradingView 风格) --- */
494
- .corner-indicator {
495
- position: absolute;
496
- right: 0;
497
- bottom: 0;
498
- width: 8px;
499
- height: 8px;
500
- cursor: pointer;
501
- overflow: hidden;
502
- }
503
-
504
- .corner-indicator::after {
505
- content: '';
506
- position: absolute;
507
- right: 0;
508
- bottom: 0;
509
- width: 0;
510
- height: 0;
511
- border-left: 5px solid transparent;
512
- border-bottom: 5px solid currentColor;
513
- opacity: 0.45;
514
- transition: opacity 0.15s ease;
515
- }
516
-
517
- .left-toolbar__button:hover .corner-indicator::after {
518
- opacity: 0.7;
519
- }
520
-
521
- .left-toolbar__button.active .corner-indicator::after {
522
- opacity: 0.7;
523
- }
524
-
525
- .corner-indicator.open::after {
526
- opacity: 0.8;
527
- }
528
-
529
- /* --- 下拉菜单(与工具栏同配色、同按钮样式,高度对齐工具栏宽度) --- */
530
- .tool-dropdown {
531
- position: absolute;
532
- left: calc(100% + 13px);
533
- top: 50%;
534
- transform: translateY(-50%);
535
- display: flex;
536
- flex-direction: row;
537
- align-items: center;
538
- gap: 4px;
539
- padding: 0 5px;
540
- height: 40px;
541
- background: rgba(250, 251, 252, 0.82);
542
- backdrop-filter: blur(8px);
543
- -webkit-backdrop-filter: blur(8px);
544
- border: 1px solid #e5e7eb;
545
- border-radius: 6px;
546
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
547
- box-sizing: border-box;
548
- z-index: 100;
549
- }
550
-
551
- /* --- 工具项容器 --- */
552
- .tool-item {
553
- position: relative;
554
- }
555
-
556
- /* --- 下拉动画 --- */
557
- .dropdown-enter-active,
558
- .dropdown-leave-active {
559
- transition:
560
- opacity 0.15s ease,
561
- transform 0.15s ease;
562
- }
563
-
564
- .dropdown-enter-from,
565
- .dropdown-leave-to {
566
- opacity: 0;
567
- transform: translateY(-50%) translateX(-6px);
568
- }
569
-
570
- /* --- 响应式 --- */
571
- @media (max-width: 768px), (max-height: 640px) {
572
- .left-toolbar {
573
- flex-basis: 36px;
574
- padding: 6px 4px;
575
- gap: 5px;
576
- border-radius: 5px;
577
- }
578
-
579
- .left-toolbar__group {
580
- gap: 3px;
581
- }
582
-
583
- .left-toolbar__button {
584
- width: 26px;
585
- height: 26px;
586
- border-radius: 3px;
587
- }
588
-
589
- .left-toolbar__divider {
590
- width: 16px;
591
- }
592
-
593
- .corner-indicator {
594
- width: 7px;
595
- height: 7px;
596
- }
597
-
598
- .corner-indicator::after {
599
- border-left-width: 4px;
600
- border-bottom-width: 4px;
601
- }
602
-
603
- .tool-dropdown {
604
- height: 36px;
605
- }
606
- }
607
-
608
- /* ═══ 设置弹窗样式(参考 IndicatorParams.vue)═══ */
609
- .settings-overlay {
610
- position: fixed;
611
- inset: 0;
612
- background: rgba(0, 0, 0, 0.3);
613
- backdrop-filter: blur(4px);
614
- display: flex;
615
- align-items: center;
616
- justify-content: center;
617
- z-index: 1000;
618
- }
619
-
620
- .settings-modal {
621
- background: #ffffff;
622
- border: 1px solid #e0e0e0;
623
- border-radius: 12px;
624
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
625
- min-width: 340px;
626
- max-width: 420px;
627
- width: 90vw;
628
- overflow: hidden;
629
- }
630
-
631
- .settings-header {
632
- display: flex;
633
- justify-content: space-between;
634
- align-items: center;
635
- padding: 16px 20px;
636
- background: #f8f8f8;
637
- border-bottom: 1px solid #e8e8e8;
638
- }
639
-
640
- .header-left {
641
- display: flex;
642
- align-items: baseline;
643
- gap: 8px;
644
- }
645
-
646
- .header-right {
647
- display: flex;
648
- align-items: center;
649
- gap: 8px;
650
- }
651
-
652
- .settings-title {
653
- font-size: 14px;
654
- font-weight: 600;
655
- color: #1a1a1a;
656
- letter-spacing: 0.2px;
657
- }
658
-
659
- .settings-subtitle {
660
- font-size: 11px;
661
- color: #999;
662
- }
663
-
664
- .settings-close {
665
- background: #fff;
666
- border: 1px solid #e0e0e0;
667
- border-radius: 6px;
668
- width: 28px;
669
- height: 28px;
670
- display: flex;
671
- align-items: center;
672
- justify-content: center;
673
- cursor: pointer;
674
- color: #888;
675
- transition:
676
- background 0.15s,
677
- color 0.15s,
678
- border-color 0.15s;
679
- padding: 0;
680
- }
681
-
682
- .settings-close:hover {
683
- background: #f0f0f0;
684
- color: #333;
685
- border-color: #ccc;
686
- }
687
-
688
- .settings-close svg {
689
- width: 14px;
690
- height: 14px;
691
- }
692
-
693
- .settings-body {
694
- padding: 16px 20px;
695
- display: flex;
696
- flex-direction: column;
697
- gap: 10px;
698
- }
699
-
700
- .settings-item {
701
- padding: 8px 12px;
702
- border-radius: 8px;
703
- background: #f8f8f8;
704
- border: 1px solid #e8e8e8;
705
- }
706
-
707
- .settings-label {
708
- display: flex;
709
- align-items: center;
710
- justify-content: space-between;
711
- font-size: 13px;
712
- color: #333;
713
- cursor: pointer;
714
- }
715
-
716
- .settings-checkbox {
717
- width: 16px;
718
- height: 16px;
719
- cursor: pointer;
720
- accent-color: #1a1a1a;
721
- }
722
-
723
- .settings-select {
724
- padding: 4px 8px;
725
- border: 1px solid #d0d0d0;
726
- border-radius: 6px;
727
- background: #fff;
728
- color: #333;
729
- font-size: 12px;
730
- cursor: pointer;
731
- outline: none;
732
- min-width: 140px;
733
- }
734
-
735
- .settings-select:hover {
736
- border-color: #9ca3af;
737
- }
738
-
739
- .settings-select:focus {
740
- border-color: #6b7280;
741
- box-shadow: 0 0 0 2px rgba(107, 114, 128, 0.15);
742
- }
743
-
744
- .settings-section-divider {
745
- display: flex;
746
- align-items: center;
747
- gap: 8px;
748
- margin-top: 4px;
749
- }
750
-
751
- .settings-section-divider::before,
752
- .settings-section-divider::after {
753
- content: '';
754
- flex: 1;
755
- border-top: 1px solid #e0e0e0;
756
- }
757
-
758
- .settings-section-label {
759
- font-size: 11px;
760
- color: #999;
761
- white-space: nowrap;
762
- }
763
-
764
- .settings-item.experimental {
765
- border-color: #f0e0d0;
766
- background: #fdf8f3;
767
- }
768
-
769
- .settings-footer {
770
- display: flex;
771
- align-items: center;
772
- justify-content: space-between;
773
- padding: 12px 20px;
774
- background: #f8f8f8;
775
- border-top: 1px solid #e8e8e8;
776
- }
777
-
778
- .footer-right {
779
- display: flex;
780
- gap: 8px;
781
- }
782
-
783
- .settings-btn {
784
- display: flex;
785
- align-items: center;
786
- gap: 5px;
787
- padding: 6px 14px;
788
- border-radius: 7px;
789
- font-size: 13px;
790
- font-weight: 500;
791
- cursor: pointer;
792
- border: 1px solid transparent;
793
- transition: all 0.15s;
794
- line-height: 1.4;
795
- }
796
-
797
- .settings-btn svg {
798
- width: 12px;
799
- height: 12px;
800
- flex-shrink: 0;
801
- }
802
-
803
- .settings-btn.reset {
804
- background: transparent;
805
- border-color: #d0d0d0;
806
- color: #666;
807
- }
808
-
809
- .settings-btn.reset:hover {
810
- border-color: #c0392b;
811
- color: #e74c3c;
812
- background: rgba(231, 76, 60, 0.08);
813
- }
814
-
815
- .settings-btn.cancel {
816
- background: transparent;
817
- border-color: #d0d0d0;
818
- color: #666;
819
- }
820
-
821
- .settings-btn.cancel:hover {
822
- background: #f0f0f0;
823
- color: #333;
824
- border-color: #bbb;
825
- }
826
-
827
- .settings-btn.confirm {
828
- background: #1a1a1a;
829
- border-color: #1a1a1a;
830
- color: #fff;
831
- }
832
-
833
- .settings-btn.confirm:hover {
834
- background: #333;
835
- border-color: #333;
836
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
837
- transform: translateY(-1px);
838
- }
839
-
840
- .settings-btn.confirm:active {
841
- transform: translateY(0);
842
- box-shadow: none;
843
- }
844
- </style>
1
+ <template>
2
+ <nav class="left-toolbar" aria-label="图表工具栏">
3
+ <div class="left-toolbar__group">
4
+ <div v-for="tool in primaryTools" :key="tool.id" class="tool-item">
5
+ <button
6
+ type="button"
7
+ class="left-toolbar__button"
8
+ :class="{ active: isActive(tool) }"
9
+ :title="tool.title"
10
+ :aria-label="tool.title"
11
+ @click="selectTool(tool)"
12
+ @pointerdown.stop
13
+ @pointermove.stop
14
+ @pointerup.stop
15
+ >
16
+ <component :is="tool.icon" class="tool-icon" aria-hidden="true" />
17
+ <span
18
+ v-if="tool.children && tool.children.length"
19
+ class="corner-indicator"
20
+ :class="{ open: openGroupId === tool.id }"
21
+ @click.stop="toggleExpand(tool.id)"
22
+ aria-label="展开子菜单"
23
+ ></span>
24
+ </button>
25
+
26
+ <Transition name="dropdown">
27
+ <div
28
+ v-if="openGroupId === tool.id && tool.children && tool.children.length"
29
+ class="tool-dropdown"
30
+ @pointerdown.stop
31
+ @pointermove.stop
32
+ @pointerup.stop
33
+ >
34
+ <button
35
+ v-for="child in tool.children"
36
+ :key="child.id"
37
+ type="button"
38
+ class="left-toolbar__button"
39
+ :class="{ active: selectedToolId === child.id }"
40
+ :title="child.title"
41
+ :aria-label="child.title"
42
+ @click="selectChild(child)"
43
+ >
44
+ <component :is="child.icon" class="tool-icon" aria-hidden="true" />
45
+ </button>
46
+ </div>
47
+ </Transition>
48
+ </div>
49
+ </div>
50
+
51
+ <span class="left-toolbar__divider"></span>
52
+
53
+ <div class="left-toolbar__group">
54
+ <button
55
+ type="button"
56
+ class="left-toolbar__button"
57
+ title="放大"
58
+ aria-label="放大"
59
+ @click="$emit('zoomIn')"
60
+ @pointerdown.stop
61
+ @pointermove.stop
62
+ @pointerup.stop
63
+ >
64
+ <IconTablerZoomIn class="tool-icon" aria-hidden="true" />
65
+ </button>
66
+ <button
67
+ type="button"
68
+ class="left-toolbar__button"
69
+ title="缩小"
70
+ aria-label="缩小"
71
+ @click="$emit('zoomOut')"
72
+ @pointerdown.stop
73
+ @pointermove.stop
74
+ @pointerup.stop
75
+ >
76
+ <IconTablerZoomOut class="tool-icon" aria-hidden="true" />
77
+ </button>
78
+ </div>
79
+
80
+ <span class="left-toolbar__divider"></span>
81
+
82
+ <div class="left-toolbar__group">
83
+ <button
84
+ type="button"
85
+ class="left-toolbar__button"
86
+ :title="isFullscreen ? '退出全屏' : '全屏显示'"
87
+ :aria-label="isFullscreen ? '退出全屏' : '全屏显示'"
88
+ @click="$emit('toggleFullscreen')"
89
+ @pointerdown.stop
90
+ @pointermove.stop
91
+ @pointerup.stop
92
+ >
93
+ <IconTablerMinimize v-if="isFullscreen" class="tool-icon" aria-hidden="true" />
94
+ <IconTablerMaximize v-else class="tool-icon" aria-hidden="true" />
95
+ </button>
96
+ </div>
97
+
98
+ <span class="left-toolbar__divider"></span>
99
+
100
+ <div class="left-toolbar__group">
101
+ <button
102
+ type="button"
103
+ class="left-toolbar__button"
104
+ title="设置"
105
+ aria-label="设置"
106
+ @click="openSettings"
107
+ @pointerdown.stop
108
+ @pointermove.stop
109
+ @pointerup.stop
110
+ >
111
+ <IconTablerSettings class="tool-icon" aria-hidden="true" />
112
+ </button>
113
+ </div>
114
+ </nav>
115
+
116
+ <!-- 设置弹窗 -->
117
+ <Teleport :to="teleportTarget">
118
+ <Transition name="overlay">
119
+ <div v-if="showSettings" class="settings-overlay" @click="closeSettings">
120
+ <Transition name="modal">
121
+ <div class="settings-modal" @click.stop>
122
+ <!-- 头部 -->
123
+ <div class="settings-header">
124
+ <div class="header-left">
125
+ <span class="settings-title">图表设置</span>
126
+ <span class="settings-subtitle">个性化配置</span>
127
+ </div>
128
+ <div class="header-right">
129
+ <button class="settings-close" @click="closeSettings">
130
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
131
+ <path d="M18 6L6 18M6 6l12 12" />
132
+ </svg>
133
+ </button>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- 体部 -->
138
+ <div class="settings-body">
139
+ <!-- 主图设置 -->
140
+ <template v-if="mainSettings.length > 0">
141
+ <div class="settings-section-divider">
142
+ <span class="settings-section-label">主图设置</span>
143
+ </div>
144
+ <template v-for="item in mainSettings" :key="item.key">
145
+ <div class="settings-item">
146
+ <label class="settings-label">
147
+ <span>{{ item.label }}</span>
148
+ <template v-if="item.type === 'boolean'">
149
+ <input
150
+ type="checkbox"
151
+ class="settings-checkbox"
152
+ v-model="settings[item.key]"
153
+ />
154
+ </template>
155
+ <template v-else-if="item.type === 'select' && item.options">
156
+ <select class="settings-select" v-model="settings[item.key]">
157
+ <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
158
+ {{ opt.label }}
159
+ </option>
160
+ </select>
161
+ </template>
162
+ </label>
163
+ </div>
164
+ </template>
165
+ </template>
166
+
167
+ <!-- 实验性设置 -->
168
+ <template v-if="experimentalSettings.length > 0">
169
+ <div class="settings-section-divider">
170
+ <span class="settings-section-label">实验性 / 调试设置</span>
171
+ </div>
172
+ <template v-for="item in experimentalSettings" :key="item.key">
173
+ <div class="settings-item experimental">
174
+ <label class="settings-label">
175
+ <span>{{ item.label }}</span>
176
+ <template v-if="item.type === 'boolean'">
177
+ <input
178
+ type="checkbox"
179
+ class="settings-checkbox"
180
+ v-model="settings[item.key]"
181
+ />
182
+ </template>
183
+ <template v-else-if="item.type === 'select' && item.options">
184
+ <select class="settings-select" v-model="settings[item.key]">
185
+ <option v-for="opt in item.options" :key="opt.value" :value="opt.value">
186
+ {{ opt.label }}
187
+ </option>
188
+ </select>
189
+ </template>
190
+ </label>
191
+ </div>
192
+ </template>
193
+ </template>
194
+ </div>
195
+
196
+ <!-- 底部 -->
197
+ <div class="settings-footer">
198
+ <button class="settings-btn reset" @click="resetSettings">
199
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
200
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
201
+ <path d="M3 3v5h5" />
202
+ </svg>
203
+ 重置
204
+ </button>
205
+ <div class="footer-right">
206
+ <button class="settings-btn cancel" @click="closeSettings">取消</button>
207
+ <button class="settings-btn confirm" @click="confirmSettings">
208
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
209
+ <path d="M20 6L9 17l-5-5" />
210
+ </svg>
211
+ 确定
212
+ </button>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </Transition>
217
+ </div>
218
+ </Transition>
219
+ </Teleport>
220
+ </template>
221
+
222
+ <script setup lang="ts">
223
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
224
+ import IconTablerPointer from '~icons/tabler/pointer'
225
+ import IconTablerChartLine from '~icons/tabler/chart-line'
226
+ import IconTablerArrowUpRight from '~icons/tabler/arrow-up-right'
227
+ import IconTablerArrowRight from '~icons/tabler/arrow-right'
228
+ import IconTablerMinus from '~icons/tabler/minus'
229
+ import IconTablerSeparator from '~icons/tabler/separator'
230
+ import IconTablerCrosshair from '~icons/tabler/crosshair'
231
+ import IconTablerInfoCircle from '~icons/tabler/info-circle'
232
+ import IconTablerZoomIn from '~icons/tabler/zoom-in'
233
+ import IconTablerZoomOut from '~icons/tabler/zoom-out'
234
+ import IconTablerMaximize from '~icons/tabler/maximize'
235
+ import IconTablerMinimize from '~icons/tabler/minimize'
236
+ import IconTablerSettings from '~icons/tabler/settings'
237
+ import IconTablerShape from '~icons/tabler/shape'
238
+ import IconTablerChartDots3 from '~icons/tabler/chart-dots-3'
239
+ import IconTablerCaretUpDown from '~icons/tabler/caret-up-down'
240
+ import IconTablerBrackets from '~icons/tabler/brackets'
241
+ import {
242
+ DEFAULT_SETTINGS,
243
+ SETTINGS_STORAGE_KEY,
244
+ type SettingItem,
245
+ } from '@363045841yyt/klinechart-core/config'
246
+ import { useFullscreenTeleportTarget } from '../composables/useFullscreenTeleportTarget'
247
+ import { setCanvasProfilerEnabled } from '../debug/canvasProfiler'
248
+
249
+ export interface ToolDef {
250
+ id: string
251
+ title: string
252
+ icon: unknown
253
+ children?: ToolDef[]
254
+ }
255
+
256
+ const primaryTools: ToolDef[] = [
257
+ { id: 'cursor', title: '光标', icon: IconTablerPointer },
258
+ {
259
+ id: 'lines',
260
+ title: '线条',
261
+ icon: IconTablerChartLine,
262
+ children: [
263
+ { id: 'trend-line', title: '线段', icon: IconTablerChartLine },
264
+ { id: 'ray', title: '射线', icon: IconTablerArrowUpRight },
265
+ { id: 'h-line', title: '水平线', icon: IconTablerMinus },
266
+ { id: 'h-ray', title: '水平射线', icon: IconTablerArrowRight },
267
+ { id: 'v-line', title: '垂直线', icon: IconTablerSeparator },
268
+ { id: 'crosshair-line', title: '十字线', icon: IconTablerCrosshair },
269
+ { id: 'info-line', title: '信息线', icon: IconTablerInfoCircle },
270
+ ],
271
+ },
272
+ {
273
+ id: 'channels',
274
+ title: '通道',
275
+ icon: IconTablerShape,
276
+ children: [
277
+ { id: 'parallel-channel', title: '平行通道', icon: IconTablerShape },
278
+ { id: 'regression-channel', title: '回归趋势', icon: IconTablerChartDots3 },
279
+ { id: 'flat-line', title: '平滑顶底', icon: IconTablerCaretUpDown },
280
+ { id: 'disjoint-channel', title: '不相交通道', icon: IconTablerBrackets },
281
+ ],
282
+ },
283
+ ]
284
+
285
+ defineProps<{
286
+ isFullscreen?: boolean
287
+ }>()
288
+
289
+ const emit = defineEmits<{
290
+ (e: 'selectTool', toolId: string): void
291
+ (e: 'toggleFullscreen'): void
292
+ (e: 'zoomIn'): void
293
+ (e: 'zoomOut'): void
294
+ (e: 'settingsChange', settings: Record<string, boolean | string>): void
295
+ }>()
296
+
297
+ const selectedToolId = ref('cursor')
298
+ const openGroupId = ref<string | null>(null)
299
+ const showSettings = ref(false)
300
+
301
+ const teleportTarget = useFullscreenTeleportTarget()
302
+
303
+ const mainSettings = computed(
304
+ () => DEFAULT_SETTINGS.filter((s) => s.group === 'main') as unknown as SettingItem[],
305
+ )
306
+ const experimentalSettings = computed(
307
+ () => DEFAULT_SETTINGS.filter((s) => s.group === 'experimental') as unknown as SettingItem[],
308
+ )
309
+
310
+ function loadSettings(): Record<string, boolean | string> {
311
+ try {
312
+ const saved = localStorage.getItem(SETTINGS_STORAGE_KEY)
313
+ if (saved) {
314
+ const parsed = JSON.parse(saved)
315
+ const result: Record<string, boolean | string> = {}
316
+ DEFAULT_SETTINGS.forEach((item) => {
317
+ result[item.key] = parsed[item.key] ?? item.default
318
+ })
319
+ return result
320
+ }
321
+ } catch {}
322
+ const defaults: Record<string, boolean | string> = {}
323
+ DEFAULT_SETTINGS.forEach((item) => {
324
+ defaults[item.key] = item.default
325
+ })
326
+ return defaults
327
+ }
328
+
329
+ function saveSettings(settings: Record<string, boolean | string>) {
330
+ try {
331
+ localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings))
332
+ } catch {}
333
+ }
334
+
335
+ const appliedSettings = ref<Record<string, boolean | string>>(loadSettings())
336
+ const settings = ref<Record<string, boolean | string>>({ ...appliedSettings.value })
337
+
338
+ function isActive(tool: ToolDef): boolean {
339
+ if (selectedToolId.value === tool.id) return true
340
+ if (tool.children) {
341
+ return tool.children.some((c) => c.id === selectedToolId.value)
342
+ }
343
+ return false
344
+ }
345
+
346
+ function selectTool(tool: ToolDef) {
347
+ if (tool.children?.length) {
348
+ const hasActiveChild = tool.children.some((c) => c.id === selectedToolId.value)
349
+ if (!hasActiveChild) {
350
+ const first = tool.children[0]!
351
+ selectedToolId.value = first.id
352
+ emit('selectTool', first.id)
353
+ }
354
+ toggleExpand(tool.id)
355
+ return
356
+ }
357
+ selectedToolId.value = tool.id
358
+ emit('selectTool', tool.id)
359
+ openGroupId.value = null
360
+ }
361
+
362
+ function selectChild(child: ToolDef) {
363
+ selectedToolId.value = child.id
364
+ emit('selectTool', child.id)
365
+ openGroupId.value = null
366
+ }
367
+
368
+ function toggleExpand(groupId: string) {
369
+ openGroupId.value = openGroupId.value === groupId ? null : groupId
370
+ }
371
+
372
+ function openSettings() {
373
+ settings.value = { ...appliedSettings.value }
374
+ showSettings.value = true
375
+ }
376
+
377
+ function closeSettings() {
378
+ showSettings.value = false
379
+ }
380
+
381
+ function resetSettings() {
382
+ const defaults: Record<string, boolean | string> = {}
383
+ DEFAULT_SETTINGS.forEach((item) => {
384
+ defaults[item.key] = item.default
385
+ })
386
+ settings.value = defaults
387
+ }
388
+
389
+ function confirmSettings() {
390
+ appliedSettings.value = { ...settings.value }
391
+ saveSettings(appliedSettings.value)
392
+ setCanvasProfilerEnabled(!!appliedSettings.value['enableCanvasProfiler'])
393
+ emit('settingsChange', { ...appliedSettings.value })
394
+ closeSettings()
395
+ }
396
+
397
+ function getCurrentSettings(): Record<string, boolean | string> {
398
+ return { ...appliedSettings.value }
399
+ }
400
+
401
+ defineExpose({
402
+ getSettings: getCurrentSettings,
403
+ })
404
+
405
+ function handleClickOutside(e: MouseEvent) {
406
+ const target = e.target as HTMLElement
407
+ if (!target.closest('.tool-item')) {
408
+ openGroupId.value = null
409
+ }
410
+ }
411
+
412
+ onMounted(() => {
413
+ document.addEventListener('click', handleClickOutside, true)
414
+ emit('settingsChange', { ...appliedSettings.value })
415
+ setCanvasProfilerEnabled(!!appliedSettings.value['enableCanvasProfiler'])
416
+ })
417
+
418
+ onUnmounted(() => {
419
+ document.removeEventListener('click', handleClickOutside, true)
420
+ })
421
+ </script>
422
+
423
+ <style scoped>
424
+ .left-toolbar {
425
+ flex: 0 0 40px;
426
+ display: flex;
427
+ flex-direction: column;
428
+ align-items: center;
429
+ gap: 6px;
430
+ padding: 8px 5px;
431
+ border: 1px solid #e5e7eb;
432
+ border-radius: 6px;
433
+ background: #fafbfc;
434
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
435
+ box-sizing: border-box;
436
+ user-select: none;
437
+ }
438
+
439
+ .left-toolbar__group {
440
+ display: flex;
441
+ flex-direction: column;
442
+ gap: 4px;
443
+ }
444
+
445
+ .left-toolbar__divider {
446
+ width: 18px;
447
+ height: 1px;
448
+ background: #e5e7eb;
449
+ }
450
+
451
+ /* --- 工具按钮 --- */
452
+ .left-toolbar__button {
453
+ position: relative;
454
+ width: 28px;
455
+ height: 28px;
456
+ padding: 0;
457
+ border: 1px solid transparent;
458
+ border-radius: 4px;
459
+ background: transparent;
460
+ color: #6b7280;
461
+ cursor: pointer;
462
+ display: inline-flex;
463
+ align-items: center;
464
+ justify-content: center;
465
+ transition:
466
+ border-color 0.15s ease,
467
+ background 0.15s ease,
468
+ color 0.15s ease;
469
+ }
470
+
471
+ .left-toolbar__button:hover {
472
+ border-color: #d1d5db;
473
+ background: #f3f4f6;
474
+ color: #374151;
475
+ }
476
+
477
+ .left-toolbar__button.active {
478
+ border-color: #9ca3af;
479
+ background: #e5e7eb;
480
+ color: #1f2937;
481
+ }
482
+
483
+ .left-toolbar__button:focus-visible {
484
+ outline: none;
485
+ border-color: #6b7280;
486
+ }
487
+
488
+ .tool-icon {
489
+ width: 16px;
490
+ height: 16px;
491
+ }
492
+
493
+ /* --- 角标三角(TradingView 风格) --- */
494
+ .corner-indicator {
495
+ position: absolute;
496
+ right: 0;
497
+ bottom: 0;
498
+ width: 8px;
499
+ height: 8px;
500
+ cursor: pointer;
501
+ overflow: hidden;
502
+ }
503
+
504
+ .corner-indicator::after {
505
+ content: '';
506
+ position: absolute;
507
+ right: 0;
508
+ bottom: 0;
509
+ width: 0;
510
+ height: 0;
511
+ border-left: 5px solid transparent;
512
+ border-bottom: 5px solid currentColor;
513
+ opacity: 0.45;
514
+ transition: opacity 0.15s ease;
515
+ }
516
+
517
+ .left-toolbar__button:hover .corner-indicator::after {
518
+ opacity: 0.7;
519
+ }
520
+
521
+ .left-toolbar__button.active .corner-indicator::after {
522
+ opacity: 0.7;
523
+ }
524
+
525
+ .corner-indicator.open::after {
526
+ opacity: 0.8;
527
+ }
528
+
529
+ /* --- 下拉菜单(与工具栏同配色、同按钮样式,高度对齐工具栏宽度) --- */
530
+ .tool-dropdown {
531
+ position: absolute;
532
+ left: calc(100% + 13px);
533
+ top: 50%;
534
+ transform: translateY(-50%);
535
+ display: flex;
536
+ flex-direction: row;
537
+ align-items: center;
538
+ gap: 4px;
539
+ padding: 0 5px;
540
+ height: 40px;
541
+ background: rgba(250, 251, 252, 0.82);
542
+ backdrop-filter: blur(8px);
543
+ -webkit-backdrop-filter: blur(8px);
544
+ border: 1px solid #e5e7eb;
545
+ border-radius: 6px;
546
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
547
+ box-sizing: border-box;
548
+ z-index: 100;
549
+ }
550
+
551
+ /* --- 工具项容器 --- */
552
+ .tool-item {
553
+ position: relative;
554
+ }
555
+
556
+ /* --- 下拉动画 --- */
557
+ .dropdown-enter-active,
558
+ .dropdown-leave-active {
559
+ transition:
560
+ opacity 0.15s ease,
561
+ transform 0.15s ease;
562
+ }
563
+
564
+ .dropdown-enter-from,
565
+ .dropdown-leave-to {
566
+ opacity: 0;
567
+ transform: translateY(-50%) translateX(-6px);
568
+ }
569
+
570
+ /* --- 响应式 --- */
571
+ @media (max-width: 768px), (max-height: 640px) {
572
+ .left-toolbar {
573
+ flex-basis: 36px;
574
+ padding: 6px 4px;
575
+ gap: 5px;
576
+ border-radius: 5px;
577
+ }
578
+
579
+ .left-toolbar__group {
580
+ gap: 3px;
581
+ }
582
+
583
+ .left-toolbar__button {
584
+ width: 26px;
585
+ height: 26px;
586
+ border-radius: 3px;
587
+ }
588
+
589
+ .left-toolbar__divider {
590
+ width: 16px;
591
+ }
592
+
593
+ .corner-indicator {
594
+ width: 7px;
595
+ height: 7px;
596
+ }
597
+
598
+ .corner-indicator::after {
599
+ border-left-width: 4px;
600
+ border-bottom-width: 4px;
601
+ }
602
+
603
+ .tool-dropdown {
604
+ height: 36px;
605
+ }
606
+ }
607
+
608
+ /* ═══ 设置弹窗样式(参考 IndicatorParams.vue)═══ */
609
+ .settings-overlay {
610
+ position: fixed;
611
+ inset: 0;
612
+ background: rgba(0, 0, 0, 0.3);
613
+ backdrop-filter: blur(4px);
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ z-index: 1000;
618
+ }
619
+
620
+ .settings-modal {
621
+ background: #ffffff;
622
+ border: 1px solid #e0e0e0;
623
+ border-radius: 12px;
624
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
625
+ min-width: 340px;
626
+ max-width: 420px;
627
+ width: 90vw;
628
+ overflow: hidden;
629
+ }
630
+
631
+ .settings-header {
632
+ display: flex;
633
+ justify-content: space-between;
634
+ align-items: center;
635
+ padding: 16px 20px;
636
+ background: #f8f8f8;
637
+ border-bottom: 1px solid #e8e8e8;
638
+ }
639
+
640
+ .header-left {
641
+ display: flex;
642
+ align-items: baseline;
643
+ gap: 8px;
644
+ }
645
+
646
+ .header-right {
647
+ display: flex;
648
+ align-items: center;
649
+ gap: 8px;
650
+ }
651
+
652
+ .settings-title {
653
+ font-size: 14px;
654
+ font-weight: 600;
655
+ color: #1a1a1a;
656
+ letter-spacing: 0.2px;
657
+ }
658
+
659
+ .settings-subtitle {
660
+ font-size: 11px;
661
+ color: #999;
662
+ }
663
+
664
+ .settings-close {
665
+ background: #fff;
666
+ border: 1px solid #e0e0e0;
667
+ border-radius: 6px;
668
+ width: 28px;
669
+ height: 28px;
670
+ display: flex;
671
+ align-items: center;
672
+ justify-content: center;
673
+ cursor: pointer;
674
+ color: #888;
675
+ transition:
676
+ background 0.15s,
677
+ color 0.15s,
678
+ border-color 0.15s;
679
+ padding: 0;
680
+ }
681
+
682
+ .settings-close:hover {
683
+ background: #f0f0f0;
684
+ color: #333;
685
+ border-color: #ccc;
686
+ }
687
+
688
+ .settings-close svg {
689
+ width: 14px;
690
+ height: 14px;
691
+ }
692
+
693
+ .settings-body {
694
+ padding: 16px 20px;
695
+ display: flex;
696
+ flex-direction: column;
697
+ gap: 10px;
698
+ }
699
+
700
+ .settings-item {
701
+ padding: 8px 12px;
702
+ border-radius: 8px;
703
+ background: #f8f8f8;
704
+ border: 1px solid #e8e8e8;
705
+ }
706
+
707
+ .settings-label {
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: space-between;
711
+ font-size: 13px;
712
+ color: #333;
713
+ cursor: pointer;
714
+ }
715
+
716
+ .settings-checkbox {
717
+ width: 16px;
718
+ height: 16px;
719
+ cursor: pointer;
720
+ accent-color: #1a1a1a;
721
+ }
722
+
723
+ .settings-select {
724
+ padding: 4px 8px;
725
+ border: 1px solid #d0d0d0;
726
+ border-radius: 6px;
727
+ background: #fff;
728
+ color: #333;
729
+ font-size: 12px;
730
+ cursor: pointer;
731
+ outline: none;
732
+ min-width: 140px;
733
+ }
734
+
735
+ .settings-select:hover {
736
+ border-color: #9ca3af;
737
+ }
738
+
739
+ .settings-select:focus {
740
+ border-color: #6b7280;
741
+ box-shadow: 0 0 0 2px rgba(107, 114, 128, 0.15);
742
+ }
743
+
744
+ .settings-section-divider {
745
+ display: flex;
746
+ align-items: center;
747
+ gap: 8px;
748
+ margin-top: 4px;
749
+ }
750
+
751
+ .settings-section-divider::before,
752
+ .settings-section-divider::after {
753
+ content: '';
754
+ flex: 1;
755
+ border-top: 1px solid #e0e0e0;
756
+ }
757
+
758
+ .settings-section-label {
759
+ font-size: 11px;
760
+ color: #999;
761
+ white-space: nowrap;
762
+ }
763
+
764
+ .settings-item.experimental {
765
+ border-color: #f0e0d0;
766
+ background: #fdf8f3;
767
+ }
768
+
769
+ .settings-footer {
770
+ display: flex;
771
+ align-items: center;
772
+ justify-content: space-between;
773
+ padding: 12px 20px;
774
+ background: #f8f8f8;
775
+ border-top: 1px solid #e8e8e8;
776
+ }
777
+
778
+ .footer-right {
779
+ display: flex;
780
+ gap: 8px;
781
+ }
782
+
783
+ .settings-btn {
784
+ display: flex;
785
+ align-items: center;
786
+ gap: 5px;
787
+ padding: 6px 14px;
788
+ border-radius: 7px;
789
+ font-size: 13px;
790
+ font-weight: 500;
791
+ cursor: pointer;
792
+ border: 1px solid transparent;
793
+ transition: all 0.15s;
794
+ line-height: 1.4;
795
+ }
796
+
797
+ .settings-btn svg {
798
+ width: 12px;
799
+ height: 12px;
800
+ flex-shrink: 0;
801
+ }
802
+
803
+ .settings-btn.reset {
804
+ background: transparent;
805
+ border-color: #d0d0d0;
806
+ color: #666;
807
+ }
808
+
809
+ .settings-btn.reset:hover {
810
+ border-color: #c0392b;
811
+ color: #e74c3c;
812
+ background: rgba(231, 76, 60, 0.08);
813
+ }
814
+
815
+ .settings-btn.cancel {
816
+ background: transparent;
817
+ border-color: #d0d0d0;
818
+ color: #666;
819
+ }
820
+
821
+ .settings-btn.cancel:hover {
822
+ background: #f0f0f0;
823
+ color: #333;
824
+ border-color: #bbb;
825
+ }
826
+
827
+ .settings-btn.confirm {
828
+ background: #1a1a1a;
829
+ border-color: #1a1a1a;
830
+ color: #fff;
831
+ }
832
+
833
+ .settings-btn.confirm:hover {
834
+ background: #333;
835
+ border-color: #333;
836
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
837
+ transform: translateY(-1px);
838
+ }
839
+
840
+ .settings-btn.confirm:active {
841
+ transform: translateY(0);
842
+ box-shadow: none;
843
+ }
844
+ </style>