@363045841yyt/klinechart 0.7.4 → 0.7.5

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,570 +1,570 @@
1
- <template>
2
- <Teleport :to="teleportTarget">
3
- <Transition name="overlay">
4
- <div v-if="visible" class="params-overlay" @click="$emit('close')">
5
- <Transition name="modal">
6
- <div class="indicator-params" @click.stop>
7
- <!-- 头部 -->
8
- <div class="params-header">
9
- <div class="header-left">
10
- <span class="params-title">{{ indicatorName }}</span>
11
- <span class="params-subtitle">参数设置</span>
12
- </div>
13
- <div class="header-right">
14
- <button
15
- class="toggle-desc-btn"
16
- :class="{ active: showDescription }"
17
- @click="showDescription = !showDescription"
18
- title="显示/隐藏说明"
19
- >
20
- <svg viewBox="0 0 1024 1024">
21
- <path d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m36.571429 268.190476v292.571428h-73.142858V438.857143h73.142858z m0-121.904762v73.142857h-73.142858v-73.142857h73.142858z" fill="currentColor" />
22
- </svg>
23
- </button>
24
- <button class="params-close" @click="$emit('close')">
25
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
26
- <path d="M18 6L6 18M6 6l12 12" />
27
- </svg>
28
- </button>
29
- </div>
30
- </div>
31
-
32
- <!-- 指标描述 -->
33
- <Transition name="slide">
34
- <div v-if="showDescription && indicatorDescription" class="indicator-description">
35
- <p>{{ indicatorDescription }}</p>
36
- </div>
37
- </Transition>
38
-
39
- <!-- 体部 -->
40
- <div class="params-body">
41
- <div
42
- v-for="param in params"
43
- :key="param.key"
44
- class="param-item"
45
- :class="{ 'has-desc': showDescription && param.description }"
46
- >
47
- <div class="param-header">
48
- <label class="param-label">
49
- <span class="param-label-text">{{ param.label }}</span>
50
- <span
51
- v-if="param.min !== undefined || param.max !== undefined"
52
- class="param-range"
53
- >
54
- {{ param.min ?? '-∞' }} ~ {{ param.max ?? '+∞' }}
55
- </span>
56
- </label>
57
- <div class="input-wrapper">
58
- <button
59
- class="stepper-btn"
60
- :disabled="param.min !== undefined && (localValues[param.key] ?? 0) <= param.min"
61
- @click="step(param, -1)"
62
- >
63
-
64
- </button>
65
- <input
66
- v-if="param.type === 'number'"
67
- type="number"
68
- class="param-input"
69
- :value="localValues[param.key]"
70
- :min="param.min"
71
- :max="param.max"
72
- :step="param.step || 1"
73
- @input="onInput(param.key, $event)"
74
- />
75
- <button
76
- class="stepper-btn"
77
- :disabled="param.max !== undefined && (localValues[param.key] ?? 0) >= param.max"
78
- @click="step(param, 1)"
79
- >
80
- +
81
- </button>
82
- </div>
83
- </div>
84
- <Transition name="slide">
85
- <div v-if="showDescription && param.description" class="param-description">
86
- {{ param.description }}
87
- </div>
88
- </Transition>
89
- </div>
90
- </div>
91
-
92
- <!-- 底部 -->
93
- <div class="params-footer">
94
- <button class="params-btn reset" @click="onReset">
95
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
96
- <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
97
- <path d="M3 3v5h5" />
98
- </svg>
99
- 重置
100
- </button>
101
- <div class="footer-right">
102
- <button class="params-btn cancel" @click="$emit('close')">取消</button>
103
- <button class="params-btn confirm" @click="onConfirm">
104
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
105
- <path d="M20 6L9 17l-5-5" />
106
- </svg>
107
- 确定
108
- </button>
109
- </div>
110
- </div>
111
- </div>
112
- </Transition>
113
- </div>
114
- </Transition>
115
- </Teleport>
116
- </template>
117
-
118
- <script setup lang="ts">
119
- import { ref, watch, computed } from 'vue'
120
-
121
- export interface ParamConfig {
122
- key: string
123
- label: string
124
- type: 'number'
125
- min?: number
126
- max?: number
127
- step?: number
128
- default?: number
129
- description?: string
130
- }
131
-
132
- const props = defineProps<{
133
- visible: boolean
134
- indicatorId: string
135
- indicatorName: string
136
- indicatorDescription?: string
137
- params: ParamConfig[]
138
- values: Record<string, number>
139
- }>()
140
-
141
- const emit = defineEmits<{
142
- close: []
143
- confirm: [values: Record<string, number>]
144
- }>()
145
-
146
- const localValues = ref<Record<string, number>>({ ...props.values })
147
- const showDescription = ref(true)
148
-
149
- const teleportTarget = computed(() => 'body')
150
-
151
- watch(
152
- () => props.values,
153
- (newValues) => {
154
- localValues.value = { ...newValues }
155
- },
156
- { deep: true, immediate: true }
157
- )
158
-
159
- watch(
160
- () => props.visible,
161
- (visible) => {
162
- if (visible) localValues.value = { ...props.values }
163
- }
164
- )
165
-
166
- function onInput(key: string, event: Event) {
167
- const target = event.target as HTMLInputElement
168
- const value = parseFloat(target.value)
169
- if (!isNaN(value)) localValues.value[key] = value
170
- }
171
-
172
- function step(param: ParamConfig, direction: 1 | -1) {
173
- const s = param.step || 1
174
- let next = (localValues.value[param.key] || 0) + direction * s
175
- if (param.min !== undefined) next = Math.max(param.min, next)
176
- if (param.max !== undefined) next = Math.min(param.max, next)
177
- localValues.value[param.key] = parseFloat(next.toFixed(10))
178
- }
179
-
180
- function onReset() {
181
- const defaults: Record<string, number> = {}
182
- props.params.forEach((p) => {
183
- defaults[p.key] = p.default ?? props.values[p.key] ?? 0
184
- })
185
- localValues.value = defaults
186
- }
187
-
188
- function onConfirm() {
189
- emit('confirm', { ...localValues.value })
190
- }
191
- </script>
192
-
193
- <style scoped>
194
- /* ── 遮罩 ── */
195
- .params-overlay {
196
- position: fixed;
197
- inset: 0;
198
- background: rgba(0, 0, 0, 0.3);
199
- backdrop-filter: blur(4px);
200
- display: flex;
201
- align-items: center;
202
- justify-content: center;
203
- z-index: 1000;
204
- }
205
-
206
- /* ── 弹窗 ── */
207
- .indicator-params {
208
- background: #ffffff;
209
- border: 1px solid #e0e0e0;
210
- border-radius: 12px;
211
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
212
- min-width: 340px;
213
- max-width: 420px;
214
- width: 90vw;
215
- overflow: hidden;
216
- }
217
-
218
- /* ── 头部 ── */
219
- .params-header {
220
- display: flex;
221
- justify-content: space-between;
222
- align-items: center;
223
- padding: 16px 20px;
224
- background: #f8f8f8;
225
- border-bottom: 1px solid #e8e8e8;
226
- }
227
-
228
- .header-left {
229
- display: flex;
230
- align-items: baseline;
231
- gap: 8px;
232
- }
233
-
234
- .header-right {
235
- display: flex;
236
- align-items: center;
237
- gap: 8px;
238
- }
239
-
240
- .params-title {
241
- font-size: 14px;
242
- font-weight: 600;
243
- color: #1a1a1a;
244
- letter-spacing: 0.2px;
245
- }
246
-
247
- .params-subtitle {
248
- font-size: 11px;
249
- color: #999;
250
- }
251
-
252
- .toggle-desc-btn {
253
- background: #fff;
254
- border: 1px solid #e0e0e0;
255
- border-radius: 6px;
256
- width: 28px;
257
- height: 28px;
258
- display: flex;
259
- align-items: center;
260
- justify-content: center;
261
- cursor: pointer;
262
- color: #888;
263
- transition: all 0.2s;
264
- padding: 0;
265
- }
266
-
267
- .toggle-desc-btn:hover {
268
- background: #f0f0f0;
269
- color: #555;
270
- border-color: #ccc;
271
- }
272
-
273
- .toggle-desc-btn.active {
274
- background: #1a1a1a;
275
- border-color: #1a1a1a;
276
- color: #fff;
277
- }
278
-
279
- .toggle-desc-btn svg {
280
- width: 14px;
281
- height: 14px;
282
- }
283
-
284
- .params-close {
285
- background: #fff;
286
- border: 1px solid #e0e0e0;
287
- border-radius: 6px;
288
- width: 28px;
289
- height: 28px;
290
- display: flex;
291
- align-items: center;
292
- justify-content: center;
293
- cursor: pointer;
294
- color: #888;
295
- transition: background 0.15s, color 0.15s, border-color 0.15s;
296
- padding: 0;
297
- }
298
-
299
- .params-close:hover {
300
- background: #f0f0f0;
301
- color: #333;
302
- border-color: #ccc;
303
- }
304
-
305
- .params-close svg {
306
- width: 14px;
307
- height: 14px;
308
- }
309
-
310
- /* ── 指标描述 ── */
311
- .indicator-description {
312
- padding: 12px 20px;
313
- background: #f0f7ff;
314
- border-bottom: 1px solid #d6e8f5;
315
- }
316
-
317
- .indicator-description p {
318
- margin: 0;
319
- font-size: 12px;
320
- line-height: 1.6;
321
- color: #2c5282;
322
- }
323
-
324
- /* ── 体部 ── */
325
- .params-body {
326
- padding: 16px 20px;
327
- display: flex;
328
- flex-direction: column;
329
- gap: 10px;
330
- }
331
-
332
- .param-item {
333
- padding: 10px 14px;
334
- border-radius: 8px;
335
- background: #f8f8f8;
336
- border: 1px solid #e8e8e8;
337
- transition: border-color 0.2s;
338
- }
339
-
340
- .param-item:has(.param-input:focus) {
341
- border-color: #bbb;
342
- }
343
-
344
- .param-item.has-desc {
345
- padding: 10px 14px 8px;
346
- }
347
-
348
- .param-header {
349
- display: flex;
350
- align-items: center;
351
- justify-content: space-between;
352
- gap: 16px;
353
- }
354
-
355
- .param-label {
356
- display: flex;
357
- flex-direction: column;
358
- gap: 3px;
359
- }
360
-
361
- .param-label-text {
362
- font-size: 13px;
363
- font-weight: 500;
364
- color: #333;
365
- }
366
-
367
- .param-range {
368
- font-size: 11px;
369
- color: #999;
370
- }
371
-
372
- /* ── 参数描述 ── */
373
- .param-description {
374
- margin-top: 8px;
375
- padding-top: 8px;
376
- border-top: 1px dashed #e0e0e0;
377
- font-size: 11px;
378
- line-height: 1.5;
379
- color: #666;
380
- }
381
-
382
- /* ── 步进输入框 ── */
383
- .input-wrapper {
384
- display: flex;
385
- align-items: stretch;
386
- height: 32px;
387
- border: 1px solid #d0d0d0;
388
- border-radius: 7px;
389
- overflow: hidden;
390
- background: #fff;
391
- transition: border-color 0.2s;
392
- }
393
-
394
- .input-wrapper:focus-within {
395
- border-color: #999;
396
- }
397
-
398
- .stepper-btn {
399
- width: 28px;
400
- background: #f0f0f0;
401
- border: none;
402
- cursor: pointer;
403
- font-size: 15px;
404
- font-weight: 400;
405
- color: #666;
406
- display: flex;
407
- align-items: center;
408
- justify-content: center;
409
- transition: background 0.15s, color 0.15s;
410
- flex-shrink: 0;
411
- line-height: 1;
412
- }
413
-
414
- .stepper-btn:hover:not(:disabled) {
415
- background: #e0e0e0;
416
- color: #333;
417
- }
418
-
419
- .stepper-btn:disabled {
420
- color: #ccc;
421
- cursor: not-allowed;
422
- }
423
-
424
- .param-input {
425
- width: 60px;
426
- border: none;
427
- border-left: 1px solid #e8e8e8;
428
- border-right: 1px solid #e8e8e8;
429
- font-size: 13px;
430
- font-weight: 600;
431
- text-align: center;
432
- color: #1a1a1a;
433
- background: transparent;
434
- -moz-appearance: textfield;
435
- appearance: textfield;
436
- }
437
-
438
- .param-input::-webkit-inner-spin-button,
439
- .param-input::-webkit-outer-spin-button {
440
- -webkit-appearance: none;
441
- }
442
-
443
- .param-input:focus {
444
- outline: none;
445
- }
446
-
447
- /* ── 底部 ── */
448
- .params-footer {
449
- display: flex;
450
- align-items: center;
451
- justify-content: space-between;
452
- padding: 12px 20px;
453
- background: #f8f8f8;
454
- border-top: 1px solid #e8e8e8;
455
- }
456
-
457
- .footer-right {
458
- display: flex;
459
- gap: 8px;
460
- }
461
-
462
- .params-btn {
463
- display: flex;
464
- align-items: center;
465
- gap: 5px;
466
- padding: 6px 14px;
467
- border-radius: 7px;
468
- font-size: 13px;
469
- font-weight: 500;
470
- cursor: pointer;
471
- border: 1px solid transparent;
472
- transition: all 0.15s;
473
- line-height: 1.4;
474
- }
475
-
476
- .params-btn svg {
477
- width: 12px;
478
- height: 12px;
479
- flex-shrink: 0;
480
- }
481
-
482
- /* 重置 */
483
- .params-btn.reset {
484
- background: transparent;
485
- border-color: #d0d0d0;
486
- color: #666;
487
- }
488
-
489
- .params-btn.reset:hover {
490
- border-color: #c0392b;
491
- color: #e74c3c;
492
- background: rgba(231, 76, 60, 0.08);
493
- }
494
-
495
- /* 取消 */
496
- .params-btn.cancel {
497
- background: transparent;
498
- border-color: #d0d0d0;
499
- color: #666;
500
- }
501
-
502
- .params-btn.cancel:hover {
503
- background: #f0f0f0;
504
- color: #333;
505
- border-color: #bbb;
506
- }
507
-
508
- /* 确定 */
509
- .params-btn.confirm {
510
- background: #1a1a1a;
511
- border-color: #1a1a1a;
512
- color: #fff;
513
- }
514
-
515
- .params-btn.confirm:hover {
516
- background: #333;
517
- border-color: #333;
518
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
519
- transform: translateY(-1px);
520
- }
521
-
522
- .params-btn.confirm:active {
523
- transform: translateY(0);
524
- box-shadow: none;
525
- }
526
-
527
- /* ── 动画 ── */
528
- .overlay-enter-active,
529
- .overlay-leave-active {
530
- transition: opacity 0.2s ease;
531
- }
532
-
533
- .overlay-enter-from,
534
- .overlay-leave-to {
535
- opacity: 0;
536
- }
537
-
538
- .modal-enter-active {
539
- transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
540
- }
541
-
542
- .modal-leave-active {
543
- transition: all 0.16s ease-in;
544
- }
545
-
546
- .modal-enter-from {
547
- opacity: 0;
548
- transform: scale(0.88) translateY(-16px);
549
- }
550
-
551
- .modal-leave-to {
552
- opacity: 0;
553
- transform: scale(0.94) translateY(8px);
554
- }
555
-
556
- .slide-enter-active,
557
- .slide-leave-active {
558
- transition: all 0.2s ease;
559
- overflow: hidden;
560
- }
561
-
562
- .slide-enter-from,
563
- .slide-leave-to {
564
- opacity: 0;
565
- max-height: 0;
566
- padding-top: 0;
567
- padding-bottom: 0;
568
- margin-top: 0;
569
- }
570
- </style>
1
+ <template>
2
+ <Teleport :to="teleportTarget">
3
+ <Transition name="overlay">
4
+ <div v-if="visible" class="params-overlay" @click="$emit('close')">
5
+ <Transition name="modal">
6
+ <div class="indicator-params" @click.stop>
7
+ <!-- 头部 -->
8
+ <div class="params-header">
9
+ <div class="header-left">
10
+ <span class="params-title">{{ indicatorName }}</span>
11
+ <span class="params-subtitle">参数设置</span>
12
+ </div>
13
+ <div class="header-right">
14
+ <button
15
+ class="toggle-desc-btn"
16
+ :class="{ active: showDescription }"
17
+ @click="showDescription = !showDescription"
18
+ title="显示/隐藏说明"
19
+ >
20
+ <svg viewBox="0 0 1024 1024">
21
+ <path d="M512 97.52381c228.912762 0 414.47619 185.563429 414.47619 414.47619s-185.563429 414.47619-414.47619 414.47619S97.52381 740.912762 97.52381 512 283.087238 97.52381 512 97.52381z m0 73.142857C323.486476 170.666667 170.666667 323.486476 170.666667 512s152.81981 341.333333 341.333333 341.333333 341.333333-152.81981 341.333333-341.333333S700.513524 170.666667 512 170.666667z m36.571429 268.190476v292.571428h-73.142858V438.857143h73.142858z m0-121.904762v73.142857h-73.142858v-73.142857h73.142858z" fill="currentColor" />
22
+ </svg>
23
+ </button>
24
+ <button class="params-close" @click="$emit('close')">
25
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
26
+ <path d="M18 6L6 18M6 6l12 12" />
27
+ </svg>
28
+ </button>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- 指标描述 -->
33
+ <Transition name="slide">
34
+ <div v-if="showDescription && indicatorDescription" class="indicator-description">
35
+ <p>{{ indicatorDescription }}</p>
36
+ </div>
37
+ </Transition>
38
+
39
+ <!-- 体部 -->
40
+ <div class="params-body">
41
+ <div
42
+ v-for="param in params"
43
+ :key="param.key"
44
+ class="param-item"
45
+ :class="{ 'has-desc': showDescription && param.description }"
46
+ >
47
+ <div class="param-header">
48
+ <label class="param-label">
49
+ <span class="param-label-text">{{ param.label }}</span>
50
+ <span
51
+ v-if="param.min !== undefined || param.max !== undefined"
52
+ class="param-range"
53
+ >
54
+ {{ param.min ?? '-∞' }} ~ {{ param.max ?? '+∞' }}
55
+ </span>
56
+ </label>
57
+ <div class="input-wrapper">
58
+ <button
59
+ class="stepper-btn"
60
+ :disabled="param.min !== undefined && (localValues[param.key] ?? 0) <= param.min"
61
+ @click="step(param, -1)"
62
+ >
63
+
64
+ </button>
65
+ <input
66
+ v-if="param.type === 'number'"
67
+ type="number"
68
+ class="param-input"
69
+ :value="localValues[param.key]"
70
+ :min="param.min"
71
+ :max="param.max"
72
+ :step="param.step || 1"
73
+ @input="onInput(param.key, $event)"
74
+ />
75
+ <button
76
+ class="stepper-btn"
77
+ :disabled="param.max !== undefined && (localValues[param.key] ?? 0) >= param.max"
78
+ @click="step(param, 1)"
79
+ >
80
+ +
81
+ </button>
82
+ </div>
83
+ </div>
84
+ <Transition name="slide">
85
+ <div v-if="showDescription && param.description" class="param-description">
86
+ {{ param.description }}
87
+ </div>
88
+ </Transition>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- 底部 -->
93
+ <div class="params-footer">
94
+ <button class="params-btn reset" @click="onReset">
95
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
96
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
97
+ <path d="M3 3v5h5" />
98
+ </svg>
99
+ 重置
100
+ </button>
101
+ <div class="footer-right">
102
+ <button class="params-btn cancel" @click="$emit('close')">取消</button>
103
+ <button class="params-btn confirm" @click="onConfirm">
104
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
105
+ <path d="M20 6L9 17l-5-5" />
106
+ </svg>
107
+ 确定
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </Transition>
113
+ </div>
114
+ </Transition>
115
+ </Teleport>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ import { ref, watch, computed } from 'vue'
120
+
121
+ export interface ParamConfig {
122
+ key: string
123
+ label: string
124
+ type: 'number'
125
+ min?: number
126
+ max?: number
127
+ step?: number
128
+ default?: number
129
+ description?: string
130
+ }
131
+
132
+ const props = defineProps<{
133
+ visible: boolean
134
+ indicatorId: string
135
+ indicatorName: string
136
+ indicatorDescription?: string
137
+ params: ParamConfig[]
138
+ values: Record<string, number>
139
+ }>()
140
+
141
+ const emit = defineEmits<{
142
+ close: []
143
+ confirm: [values: Record<string, number>]
144
+ }>()
145
+
146
+ const localValues = ref<Record<string, number>>({ ...props.values })
147
+ const showDescription = ref(true)
148
+
149
+ const teleportTarget = computed(() => 'body')
150
+
151
+ watch(
152
+ () => props.values,
153
+ (newValues) => {
154
+ localValues.value = { ...newValues }
155
+ },
156
+ { deep: true, immediate: true }
157
+ )
158
+
159
+ watch(
160
+ () => props.visible,
161
+ (visible) => {
162
+ if (visible) localValues.value = { ...props.values }
163
+ }
164
+ )
165
+
166
+ function onInput(key: string, event: Event) {
167
+ const target = event.target as HTMLInputElement
168
+ const value = parseFloat(target.value)
169
+ if (!isNaN(value)) localValues.value[key] = value
170
+ }
171
+
172
+ function step(param: ParamConfig, direction: 1 | -1) {
173
+ const s = param.step || 1
174
+ let next = (localValues.value[param.key] || 0) + direction * s
175
+ if (param.min !== undefined) next = Math.max(param.min, next)
176
+ if (param.max !== undefined) next = Math.min(param.max, next)
177
+ localValues.value[param.key] = parseFloat(next.toFixed(10))
178
+ }
179
+
180
+ function onReset() {
181
+ const defaults: Record<string, number> = {}
182
+ props.params.forEach((p) => {
183
+ defaults[p.key] = p.default ?? props.values[p.key] ?? 0
184
+ })
185
+ localValues.value = defaults
186
+ }
187
+
188
+ function onConfirm() {
189
+ emit('confirm', { ...localValues.value })
190
+ }
191
+ </script>
192
+
193
+ <style scoped>
194
+ /* ── 遮罩 ── */
195
+ .params-overlay {
196
+ position: fixed;
197
+ inset: 0;
198
+ background: rgba(0, 0, 0, 0.3);
199
+ backdrop-filter: blur(4px);
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ z-index: 1000;
204
+ }
205
+
206
+ /* ── 弹窗 ── */
207
+ .indicator-params {
208
+ background: #ffffff;
209
+ border: 1px solid #e0e0e0;
210
+ border-radius: 12px;
211
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
212
+ min-width: 340px;
213
+ max-width: 420px;
214
+ width: 90vw;
215
+ overflow: hidden;
216
+ }
217
+
218
+ /* ── 头部 ── */
219
+ .params-header {
220
+ display: flex;
221
+ justify-content: space-between;
222
+ align-items: center;
223
+ padding: 16px 20px;
224
+ background: #f8f8f8;
225
+ border-bottom: 1px solid #e8e8e8;
226
+ }
227
+
228
+ .header-left {
229
+ display: flex;
230
+ align-items: baseline;
231
+ gap: 8px;
232
+ }
233
+
234
+ .header-right {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 8px;
238
+ }
239
+
240
+ .params-title {
241
+ font-size: 14px;
242
+ font-weight: 600;
243
+ color: #1a1a1a;
244
+ letter-spacing: 0.2px;
245
+ }
246
+
247
+ .params-subtitle {
248
+ font-size: 11px;
249
+ color: #999;
250
+ }
251
+
252
+ .toggle-desc-btn {
253
+ background: #fff;
254
+ border: 1px solid #e0e0e0;
255
+ border-radius: 6px;
256
+ width: 28px;
257
+ height: 28px;
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: center;
261
+ cursor: pointer;
262
+ color: #888;
263
+ transition: all 0.2s;
264
+ padding: 0;
265
+ }
266
+
267
+ .toggle-desc-btn:hover {
268
+ background: #f0f0f0;
269
+ color: #555;
270
+ border-color: #ccc;
271
+ }
272
+
273
+ .toggle-desc-btn.active {
274
+ background: #1a1a1a;
275
+ border-color: #1a1a1a;
276
+ color: #fff;
277
+ }
278
+
279
+ .toggle-desc-btn svg {
280
+ width: 14px;
281
+ height: 14px;
282
+ }
283
+
284
+ .params-close {
285
+ background: #fff;
286
+ border: 1px solid #e0e0e0;
287
+ border-radius: 6px;
288
+ width: 28px;
289
+ height: 28px;
290
+ display: flex;
291
+ align-items: center;
292
+ justify-content: center;
293
+ cursor: pointer;
294
+ color: #888;
295
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
296
+ padding: 0;
297
+ }
298
+
299
+ .params-close:hover {
300
+ background: #f0f0f0;
301
+ color: #333;
302
+ border-color: #ccc;
303
+ }
304
+
305
+ .params-close svg {
306
+ width: 14px;
307
+ height: 14px;
308
+ }
309
+
310
+ /* ── 指标描述 ── */
311
+ .indicator-description {
312
+ padding: 12px 20px;
313
+ background: #f0f7ff;
314
+ border-bottom: 1px solid #d6e8f5;
315
+ }
316
+
317
+ .indicator-description p {
318
+ margin: 0;
319
+ font-size: 12px;
320
+ line-height: 1.6;
321
+ color: #2c5282;
322
+ }
323
+
324
+ /* ── 体部 ── */
325
+ .params-body {
326
+ padding: 16px 20px;
327
+ display: flex;
328
+ flex-direction: column;
329
+ gap: 10px;
330
+ }
331
+
332
+ .param-item {
333
+ padding: 10px 14px;
334
+ border-radius: 8px;
335
+ background: #f8f8f8;
336
+ border: 1px solid #e8e8e8;
337
+ transition: border-color 0.2s;
338
+ }
339
+
340
+ .param-item:has(.param-input:focus) {
341
+ border-color: #bbb;
342
+ }
343
+
344
+ .param-item.has-desc {
345
+ padding: 10px 14px 8px;
346
+ }
347
+
348
+ .param-header {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: space-between;
352
+ gap: 16px;
353
+ }
354
+
355
+ .param-label {
356
+ display: flex;
357
+ flex-direction: column;
358
+ gap: 3px;
359
+ }
360
+
361
+ .param-label-text {
362
+ font-size: 13px;
363
+ font-weight: 500;
364
+ color: #333;
365
+ }
366
+
367
+ .param-range {
368
+ font-size: 11px;
369
+ color: #999;
370
+ }
371
+
372
+ /* ── 参数描述 ── */
373
+ .param-description {
374
+ margin-top: 8px;
375
+ padding-top: 8px;
376
+ border-top: 1px dashed #e0e0e0;
377
+ font-size: 11px;
378
+ line-height: 1.5;
379
+ color: #666;
380
+ }
381
+
382
+ /* ── 步进输入框 ── */
383
+ .input-wrapper {
384
+ display: flex;
385
+ align-items: stretch;
386
+ height: 32px;
387
+ border: 1px solid #d0d0d0;
388
+ border-radius: 7px;
389
+ overflow: hidden;
390
+ background: #fff;
391
+ transition: border-color 0.2s;
392
+ }
393
+
394
+ .input-wrapper:focus-within {
395
+ border-color: #999;
396
+ }
397
+
398
+ .stepper-btn {
399
+ width: 28px;
400
+ background: #f0f0f0;
401
+ border: none;
402
+ cursor: pointer;
403
+ font-size: 15px;
404
+ font-weight: 400;
405
+ color: #666;
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: center;
409
+ transition: background 0.15s, color 0.15s;
410
+ flex-shrink: 0;
411
+ line-height: 1;
412
+ }
413
+
414
+ .stepper-btn:hover:not(:disabled) {
415
+ background: #e0e0e0;
416
+ color: #333;
417
+ }
418
+
419
+ .stepper-btn:disabled {
420
+ color: #ccc;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ .param-input {
425
+ width: 60px;
426
+ border: none;
427
+ border-left: 1px solid #e8e8e8;
428
+ border-right: 1px solid #e8e8e8;
429
+ font-size: 13px;
430
+ font-weight: 600;
431
+ text-align: center;
432
+ color: #1a1a1a;
433
+ background: transparent;
434
+ -moz-appearance: textfield;
435
+ appearance: textfield;
436
+ }
437
+
438
+ .param-input::-webkit-inner-spin-button,
439
+ .param-input::-webkit-outer-spin-button {
440
+ -webkit-appearance: none;
441
+ }
442
+
443
+ .param-input:focus {
444
+ outline: none;
445
+ }
446
+
447
+ /* ── 底部 ── */
448
+ .params-footer {
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: space-between;
452
+ padding: 12px 20px;
453
+ background: #f8f8f8;
454
+ border-top: 1px solid #e8e8e8;
455
+ }
456
+
457
+ .footer-right {
458
+ display: flex;
459
+ gap: 8px;
460
+ }
461
+
462
+ .params-btn {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 5px;
466
+ padding: 6px 14px;
467
+ border-radius: 7px;
468
+ font-size: 13px;
469
+ font-weight: 500;
470
+ cursor: pointer;
471
+ border: 1px solid transparent;
472
+ transition: all 0.15s;
473
+ line-height: 1.4;
474
+ }
475
+
476
+ .params-btn svg {
477
+ width: 12px;
478
+ height: 12px;
479
+ flex-shrink: 0;
480
+ }
481
+
482
+ /* 重置 */
483
+ .params-btn.reset {
484
+ background: transparent;
485
+ border-color: #d0d0d0;
486
+ color: #666;
487
+ }
488
+
489
+ .params-btn.reset:hover {
490
+ border-color: #c0392b;
491
+ color: #e74c3c;
492
+ background: rgba(231, 76, 60, 0.08);
493
+ }
494
+
495
+ /* 取消 */
496
+ .params-btn.cancel {
497
+ background: transparent;
498
+ border-color: #d0d0d0;
499
+ color: #666;
500
+ }
501
+
502
+ .params-btn.cancel:hover {
503
+ background: #f0f0f0;
504
+ color: #333;
505
+ border-color: #bbb;
506
+ }
507
+
508
+ /* 确定 */
509
+ .params-btn.confirm {
510
+ background: #1a1a1a;
511
+ border-color: #1a1a1a;
512
+ color: #fff;
513
+ }
514
+
515
+ .params-btn.confirm:hover {
516
+ background: #333;
517
+ border-color: #333;
518
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
519
+ transform: translateY(-1px);
520
+ }
521
+
522
+ .params-btn.confirm:active {
523
+ transform: translateY(0);
524
+ box-shadow: none;
525
+ }
526
+
527
+ /* ── 动画 ── */
528
+ .overlay-enter-active,
529
+ .overlay-leave-active {
530
+ transition: opacity 0.2s ease;
531
+ }
532
+
533
+ .overlay-enter-from,
534
+ .overlay-leave-to {
535
+ opacity: 0;
536
+ }
537
+
538
+ .modal-enter-active {
539
+ transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
540
+ }
541
+
542
+ .modal-leave-active {
543
+ transition: all 0.16s ease-in;
544
+ }
545
+
546
+ .modal-enter-from {
547
+ opacity: 0;
548
+ transform: scale(0.88) translateY(-16px);
549
+ }
550
+
551
+ .modal-leave-to {
552
+ opacity: 0;
553
+ transform: scale(0.94) translateY(8px);
554
+ }
555
+
556
+ .slide-enter-active,
557
+ .slide-leave-active {
558
+ transition: all 0.2s ease;
559
+ overflow: hidden;
560
+ }
561
+
562
+ .slide-enter-from,
563
+ .slide-leave-to {
564
+ opacity: 0;
565
+ max-height: 0;
566
+ padding-top: 0;
567
+ padding-bottom: 0;
568
+ margin-top: 0;
569
+ }
570
+ </style>