@yxhl/specter-pui-vtk 1.0.97 → 1.0.98

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.
@@ -0,0 +1,775 @@
1
+ <template>
2
+ <div class="vtk-time-picker">
3
+ <VMenu
4
+ v-if="!inline"
5
+ v-model="showPicker"
6
+ :attach="menuAttach"
7
+ :location="menuLocation"
8
+ :origin="menuOrigin"
9
+ :width="menuWidth"
10
+ :min-width="menuWidth"
11
+ offset="4"
12
+ :close-on-content-click="false"
13
+ >
14
+ <template #activator="{ props: menuProps }">
15
+ <div class="time-input">
16
+ <VTextField
17
+ :model-value="displayValue"
18
+ :label="label || undefined"
19
+ :placeholder="placeholder"
20
+ :disabled="disabled"
21
+ density="compact"
22
+ variant="outlined"
23
+ hide-details
24
+ readonly
25
+ v-bind="menuProps"
26
+ :clearable="clearable && !!displayValue && !disabled"
27
+ clear-icon="mdi-close-circle"
28
+ persistent-clear
29
+ class="time-input-field"
30
+ @click:clear.stop="clearValue"
31
+ >
32
+ <template #append-inner>
33
+ <VIcon size="16" class="time-icon">mdi-clock-outline</VIcon>
34
+ </template>
35
+ </VTextField>
36
+ </div>
37
+ </template>
38
+
39
+ <div class="time-picker-dropdown" :class="{ 'range-mode': isRange }" @click.stop @mousedown.stop>
40
+ <template v-if="isRange">
41
+ <div class="range-input-row">
42
+ <div class="range-time-wrap" :class="{ active: activeTimeSide === 'start' }">
43
+ <button class="range-time-input" type="button" @click="openRangeTime('start')">
44
+ {{ formatTimePart(tempRange[0]) || defaultStartTime }}
45
+ </button>
46
+ <div v-if="activeTimeSide === 'start'" class="range-time-popover">
47
+ <div class="time-columns compact">
48
+ <div class="time-column">
49
+ <button
50
+ v-for="hour in hourOptions"
51
+ :key="hour"
52
+ type="button"
53
+ :class="['time-item', { selected: hour === activeRangeHour, disabled: isRangeTimeDisabled('start', 'hour', hour) }]"
54
+ @click="selectRangeTime('hour', hour)"
55
+ >
56
+ {{ padTime(hour) }}
57
+ </button>
58
+ </div>
59
+ <div class="time-column">
60
+ <button
61
+ v-for="minute in minuteOptions"
62
+ :key="minute"
63
+ type="button"
64
+ :class="['time-item', { selected: minute === activeRangeMinute, disabled: isRangeTimeDisabled('start', 'minute', minute) }]"
65
+ @click="selectRangeTime('minute', minute)"
66
+ >
67
+ {{ padTime(minute) }}
68
+ </button>
69
+ </div>
70
+ <div v-if="showSeconds" class="time-column">
71
+ <button
72
+ v-for="second in secondOptions"
73
+ :key="second"
74
+ type="button"
75
+ :class="['time-item', { selected: second === activeRangeSecond, disabled: isRangeTimeDisabled('start', 'second', second) }]"
76
+ @click="selectRangeTime('second', second)"
77
+ >
78
+ {{ padTime(second) }}
79
+ </button>
80
+ </div>
81
+ </div>
82
+ <div class="time-popover-footer">
83
+ <button class="btn btn-text small" type="button" @click="activeTimeSide = null">取消</button>
84
+ <button class="btn btn-primary small" type="button" @click="activeTimeSide = null">确定</button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <span class="range-arrow">›</span>
90
+
91
+ <div class="range-time-wrap" :class="{ active: activeTimeSide === 'end' }">
92
+ <button class="range-time-input" type="button" @click="openRangeTime('end')">
93
+ {{ formatTimePart(tempRange[1]) || defaultEndTime }}
94
+ </button>
95
+ <div v-if="activeTimeSide === 'end'" class="range-time-popover">
96
+ <div class="time-columns compact">
97
+ <div class="time-column">
98
+ <button
99
+ v-for="hour in hourOptions"
100
+ :key="hour"
101
+ type="button"
102
+ :class="['time-item', { selected: hour === activeRangeHour, disabled: isRangeTimeDisabled('end', 'hour', hour) }]"
103
+ @click="selectRangeTime('hour', hour)"
104
+ >
105
+ {{ padTime(hour) }}
106
+ </button>
107
+ </div>
108
+ <div class="time-column">
109
+ <button
110
+ v-for="minute in minuteOptions"
111
+ :key="minute"
112
+ type="button"
113
+ :class="['time-item', { selected: minute === activeRangeMinute, disabled: isRangeTimeDisabled('end', 'minute', minute) }]"
114
+ @click="selectRangeTime('minute', minute)"
115
+ >
116
+ {{ padTime(minute) }}
117
+ </button>
118
+ </div>
119
+ <div v-if="showSeconds" class="time-column">
120
+ <button
121
+ v-for="second in secondOptions"
122
+ :key="second"
123
+ type="button"
124
+ :class="['time-item', { selected: second === activeRangeSecond, disabled: isRangeTimeDisabled('end', 'second', second) }]"
125
+ @click="selectRangeTime('second', second)"
126
+ >
127
+ {{ padTime(second) }}
128
+ </button>
129
+ </div>
130
+ </div>
131
+ <div class="time-popover-footer">
132
+ <button class="btn btn-text small" type="button" @click="activeTimeSide = null">取消</button>
133
+ <button class="btn btn-primary small" type="button" @click="activeTimeSide = null">确定</button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </template>
139
+
140
+ <template v-else>
141
+ <div class="single-time-panel">
142
+ <div class="time-columns">
143
+ <div class="time-column">
144
+ <button
145
+ v-for="hour in hourOptions"
146
+ :key="hour"
147
+ type="button"
148
+ :class="['time-item', { selected: hour === selectedHour, disabled: isTimePartDisabled(hour, selectedMinute, selectedSecond) }]"
149
+ @click="selectHour(hour)"
150
+ >
151
+ {{ padTime(hour) }}
152
+ </button>
153
+ </div>
154
+ <div class="time-column">
155
+ <button
156
+ v-for="minute in minuteOptions"
157
+ :key="minute"
158
+ type="button"
159
+ :class="['time-item', { selected: minute === selectedMinute, disabled: isTimePartDisabled(selectedHour, minute, selectedSecond) }]"
160
+ @click="selectMinute(minute)"
161
+ >
162
+ {{ padTime(minute) }}
163
+ </button>
164
+ </div>
165
+ <div v-if="showSeconds" class="time-column">
166
+ <button
167
+ v-for="second in secondOptions"
168
+ :key="second"
169
+ type="button"
170
+ :class="['time-item', { selected: second === selectedSecond, disabled: isTimePartDisabled(selectedHour, selectedMinute, second) }]"
171
+ @click="selectSecond(second)"
172
+ >
173
+ {{ padTime(second) }}
174
+ </button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </template>
179
+
180
+ <div class="picker-footer">
181
+ <button v-if="!isRange" class="btn btn-text" type="button" @click="handleNow">此刻</button>
182
+ <button v-if="isRange" class="btn btn-text" type="button" @click="clearTempRange">清空</button>
183
+ <span class="footer-spacer"></span>
184
+ <button class="btn btn-text" type="button" @click="handleCancel">取消</button>
185
+ <button class="btn btn-primary" type="button" :disabled="!canConfirm" @click="handleConfirm">确定</button>
186
+ </div>
187
+ </div>
188
+ </VMenu>
189
+ </div>
190
+ </template>
191
+
192
+ <script setup>
193
+ import { computed, ref, watch } from 'vue';
194
+
195
+ defineOptions({
196
+ name: 'VtkTimePicker',
197
+ });
198
+
199
+ const props = defineProps({
200
+ modelValue: {
201
+ type: [String, Date, Number, Array],
202
+ default: null
203
+ },
204
+ mode: {
205
+ type: String,
206
+ default: 'single',
207
+ validator: (value) => ['single', 'range'].includes(value)
208
+ },
209
+ label: {
210
+ type: String,
211
+ default: ''
212
+ },
213
+ placeholder: {
214
+ type: String,
215
+ default: '请选择时间'
216
+ },
217
+ disabled: {
218
+ type: Boolean,
219
+ default: false
220
+ },
221
+ clearable: {
222
+ type: Boolean,
223
+ default: true
224
+ },
225
+ format: {
226
+ type: String,
227
+ default: 'HH:mm:ss'
228
+ },
229
+ valueFormat: {
230
+ type: String,
231
+ default: 'HH:mm:ss'
232
+ },
233
+ separator: {
234
+ type: String,
235
+ default: ' 至 '
236
+ },
237
+ min: {
238
+ type: String,
239
+ default: null
240
+ },
241
+ max: {
242
+ type: String,
243
+ default: null
244
+ },
245
+ minuteStep: {
246
+ type: Number,
247
+ default: 1
248
+ },
249
+ secondStep: {
250
+ type: Number,
251
+ default: 1
252
+ },
253
+ showSeconds: {
254
+ type: Boolean,
255
+ default: true
256
+ },
257
+ inline: {
258
+ type: Boolean,
259
+ default: false
260
+ },
261
+ placement: {
262
+ type: String,
263
+ default: 'left',
264
+ validator: (value) => ['left', 'right'].includes(value)
265
+ },
266
+ noTeleport: {
267
+ type: Boolean,
268
+ default: false
269
+ },
270
+ onCancel: {
271
+ type: Function,
272
+ default: null
273
+ },
274
+ onConfirm: {
275
+ type: Function,
276
+ default: null
277
+ }
278
+ });
279
+
280
+ const emit = defineEmits(['update:modelValue', 'change', 'cancel', 'confirm']);
281
+
282
+ const showPicker = ref(false);
283
+ const selectedTime = ref(null);
284
+ const selectedRange = ref([]);
285
+ const tempRange = ref([null, null]);
286
+ const selectedHour = ref(0);
287
+ const selectedMinute = ref(0);
288
+ const selectedSecond = ref(0);
289
+ const activeTimeSide = ref(null);
290
+
291
+ const defaultStartTime = '00:00:00';
292
+ const defaultEndTime = '23:59:59';
293
+
294
+ const isRange = computed(() => props.mode === 'range');
295
+ const menuAttach = computed(() => (props.noTeleport ? true : false));
296
+ const menuLocation = computed(() => props.placement === 'right' ? 'bottom end' : 'bottom start');
297
+ const menuOrigin = computed(() => props.placement === 'right' ? 'top end' : 'top start');
298
+ const menuWidth = computed(() => isRange.value ? 320 : (props.showSeconds ? 216 : 150));
299
+ const minTime = computed(() => parseTime(props.min));
300
+ const maxTime = computed(() => parseTime(props.max));
301
+ const hourOptions = computed(() => Array.from({ length: 24 }, (_, index) => index));
302
+ const minuteOptions = computed(() => buildStepOptions(props.minuteStep));
303
+ const secondOptions = computed(() => buildStepOptions(props.secondStep));
304
+
305
+ const displayValue = computed(() => {
306
+ if (isRange.value) {
307
+ if (!selectedRange.value.length) return '';
308
+ const [start, end] = selectedRange.value;
309
+ return [start, end].filter(Boolean).map(date => formatByPattern(date, props.format)).join(props.separator);
310
+ }
311
+ return selectedTime.value ? formatByPattern(selectedTime.value, props.format) : '';
312
+ });
313
+
314
+ const canConfirm = computed(() => {
315
+ if (isRange.value) {
316
+ const [start, end] = tempRange.value;
317
+ if (!start || !end) return false;
318
+ if (start > end) return false;
319
+ return !isTimeDisabled(start.getHours(), start.getMinutes(), start.getSeconds())
320
+ && !isTimeDisabled(end.getHours(), end.getMinutes(), end.getSeconds());
321
+ }
322
+ return !isTimeDisabled(selectedHour.value, selectedMinute.value, props.showSeconds ? selectedSecond.value : 0);
323
+ });
324
+
325
+ const activeRangeDate = computed(() => {
326
+ return activeTimeSide.value === 'end' ? tempRange.value[1] : tempRange.value[0];
327
+ });
328
+
329
+ const activeRangeHour = computed(() => activeRangeDate.value ? activeRangeDate.value.getHours() : 0);
330
+ const activeRangeMinute = computed(() => activeRangeDate.value ? activeRangeDate.value.getMinutes() : 0);
331
+ const activeRangeSecond = computed(() => activeRangeDate.value ? activeRangeDate.value.getSeconds() : 0);
332
+
333
+ const buildStepOptions = (step) => {
334
+ const normalizedStep = Number.isFinite(step) && step > 0 ? Math.max(1, Math.floor(step)) : 1;
335
+ const options = [];
336
+ for (let value = 0; value < 60; value += normalizedStep) {
337
+ options.push(value);
338
+ }
339
+ return options;
340
+ };
341
+
342
+ const padTime = (value) => String(value).padStart(2, '0');
343
+
344
+ const formatByPattern = (date, pattern) => {
345
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
346
+ const replacements = {
347
+ HH: padTime(date.getHours()),
348
+ mm: padTime(date.getMinutes()),
349
+ ss: padTime(date.getSeconds())
350
+ };
351
+ return Object.keys(replacements).reduce((result, token) => {
352
+ return result.replace(new RegExp(token, 'g'), replacements[token]);
353
+ }, pattern);
354
+ };
355
+
356
+ const formatTimePart = (date) => date ? formatByPattern(date, props.showSeconds ? 'HH:mm:ss' : 'HH:mm') : '';
357
+
358
+ const parseTime = (value) => {
359
+ if (!value && value !== 0) return null;
360
+ if (value instanceof Date) {
361
+ if (Number.isNaN(value.getTime())) return null;
362
+ return new Date(2000, 0, 1, value.getHours(), value.getMinutes(), value.getSeconds());
363
+ }
364
+ if (typeof value === 'number') {
365
+ const date = new Date(value);
366
+ if (Number.isNaN(date.getTime())) return null;
367
+ return new Date(2000, 0, 1, date.getHours(), date.getMinutes(), date.getSeconds());
368
+ }
369
+ if (typeof value !== 'string') return null;
370
+
371
+ const match = value.trim().match(/(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/);
372
+ if (!match) return null;
373
+ const [, hour, minute, second = '0'] = match;
374
+ const date = new Date(2000, 0, 1, Math.min(23, Number(hour)), Math.min(59, Number(minute)), Math.min(59, Number(second)));
375
+ return Number.isNaN(date.getTime()) ? null : date;
376
+ };
377
+
378
+ const parseRangeValue = (value) => {
379
+ if (Array.isArray(value)) return value.map(parseTime).filter(Boolean).slice(0, 2);
380
+ if (typeof value === 'string' && value.includes(props.separator)) {
381
+ return value.split(props.separator).map(item => parseTime(item.trim())).filter(Boolean).slice(0, 2);
382
+ }
383
+ return [];
384
+ };
385
+
386
+ const toModelValue = (date) => {
387
+ if (!date) return null;
388
+ if (props.valueFormat === 'timestamp') return date.getTime();
389
+ if (props.valueFormat === 'Date') return new Date(date.getTime());
390
+ return formatByPattern(date, props.valueFormat);
391
+ };
392
+
393
+ const toModelRange = (range) => range.map(date => toModelValue(date));
394
+
395
+ const buildTempTime = (hour = selectedHour.value, minute = selectedMinute.value, second = selectedSecond.value) => {
396
+ return new Date(2000, 0, 1, hour, minute, props.showSeconds ? second : 0);
397
+ };
398
+
399
+ const isTimeDisabled = (hour, minute, second) => {
400
+ const date = new Date(2000, 0, 1, hour, minute, props.showSeconds ? second : 0);
401
+ if (minTime.value && date < minTime.value) return true;
402
+ if (maxTime.value && date > maxTime.value) return true;
403
+ return false;
404
+ };
405
+
406
+ const isTimePartDisabled = (hour, minute, second) => isTimeDisabled(hour, minute, second);
407
+
408
+ const isRangeTimeDisabled = (side, part, value) => {
409
+ const index = side === 'end' ? 1 : 0;
410
+ const otherIndex = 1 - index;
411
+ const current = tempRange.value[index];
412
+ const other = tempRange.value[otherIndex];
413
+ const hour = part === 'hour' ? value : (current ? current.getHours() : 0);
414
+ const minute = part === 'minute' ? value : (current ? current.getMinutes() : 0);
415
+ const second = part === 'second' ? value : (current ? current.getSeconds() : 0);
416
+ const date = new Date(2000, 0, 1, hour, minute, props.showSeconds ? second : 0);
417
+ if (side === 'start') {
418
+ if (other && date > other) return true;
419
+ if (maxTime.value && date > maxTime.value) return true;
420
+ } else {
421
+ if (other && date < other) return true;
422
+ if (minTime.value && date < minTime.value) return true;
423
+ }
424
+ return false;
425
+ };
426
+
427
+ const selectHour = (hour) => {
428
+ if (isTimePartDisabled(hour, selectedMinute.value, selectedSecond.value)) return;
429
+ selectedHour.value = hour;
430
+ };
431
+
432
+ const selectMinute = (minute) => {
433
+ if (isTimePartDisabled(selectedHour.value, minute, selectedSecond.value)) return;
434
+ selectedMinute.value = minute;
435
+ };
436
+
437
+ const selectSecond = (second) => {
438
+ if (isTimePartDisabled(selectedHour.value, selectedMinute.value, second)) return;
439
+ selectedSecond.value = second;
440
+ };
441
+
442
+ const openRangeTime = (side) => {
443
+ activeTimeSide.value = side;
444
+ const index = side === 'start' ? 0 : 1;
445
+ if (!tempRange.value[index]) {
446
+ const base = new Date(2000, 0, 1);
447
+ base.setHours(side === 'start' ? 0 : 23, side === 'start' ? 0 : 59, props.showSeconds ? (side === 'start' ? 0 : 59) : 0, 0);
448
+ tempRange.value[index] = base;
449
+ }
450
+ };
451
+
452
+ const selectRangeTime = (part, value) => {
453
+ const index = activeTimeSide.value === 'end' ? 1 : 0;
454
+ const date = tempRange.value[index] ? new Date(tempRange.value[index].getTime()) : new Date(2000, 0, 1);
455
+ if (part === 'hour') date.setHours(value);
456
+ if (part === 'minute') date.setMinutes(value);
457
+ if (part === 'second') date.setSeconds(value);
458
+ if (!props.showSeconds) date.setSeconds(0);
459
+ date.setMilliseconds(0);
460
+ tempRange.value[index] = date;
461
+ };
462
+
463
+ const setSingleFromTime = (date) => {
464
+ if (!date) return;
465
+ selectedHour.value = date.getHours();
466
+ selectedMinute.value = date.getMinutes();
467
+ selectedSecond.value = date.getSeconds();
468
+ };
469
+
470
+ const initializePicker = () => {
471
+ if (isRange.value) {
472
+ tempRange.value = selectedRange.value.length ? selectedRange.value.map(date => new Date(date.getTime())) : [null, null];
473
+ activeTimeSide.value = null;
474
+ return;
475
+ }
476
+ setSingleFromTime(selectedTime.value || new Date());
477
+ };
478
+
479
+ const closePicker = () => {
480
+ showPicker.value = false;
481
+ activeTimeSide.value = null;
482
+ };
483
+
484
+ const clearTempRange = () => {
485
+ tempRange.value = [null, null];
486
+ activeTimeSide.value = null;
487
+ };
488
+
489
+ const clearValue = () => {
490
+ if (isRange.value) {
491
+ selectedRange.value = [];
492
+ tempRange.value = [null, null];
493
+ emit('update:modelValue', []);
494
+ emit('change', []);
495
+ } else {
496
+ selectedTime.value = null;
497
+ emit('update:modelValue', null);
498
+ emit('change', null);
499
+ }
500
+ };
501
+
502
+ const confirm = () => {
503
+ if (!canConfirm.value) return;
504
+ if (isRange.value) {
505
+ selectedRange.value = tempRange.value.map(date => new Date(date.getTime()));
506
+ const modelValue = toModelRange(selectedRange.value);
507
+ emit('update:modelValue', modelValue);
508
+ emit('change', modelValue);
509
+ closePicker();
510
+ return;
511
+ }
512
+ const nextValue = buildTempTime();
513
+ selectedTime.value = nextValue;
514
+ const modelValue = toModelValue(nextValue);
515
+ emit('update:modelValue', modelValue);
516
+ emit('change', modelValue);
517
+ closePicker();
518
+ };
519
+
520
+ const handleCancel = () => {
521
+ if (props.onCancel) {
522
+ props.onCancel();
523
+ } else {
524
+ emit('cancel');
525
+ closePicker();
526
+ }
527
+ };
528
+
529
+ const handleConfirm = () => {
530
+ const value = isRange.value ? toModelRange(tempRange.value) : toModelValue(buildTempTime());
531
+ if (props.onConfirm) {
532
+ props.onConfirm(value);
533
+ } else {
534
+ emit('confirm', value);
535
+ confirm();
536
+ }
537
+ };
538
+
539
+ const handleNow = () => {
540
+ const now = new Date();
541
+ if (isTimeDisabled(now.getHours(), now.getMinutes(), now.getSeconds())) return;
542
+ setSingleFromTime(now);
543
+ };
544
+
545
+ watch(showPicker, (isOpen) => {
546
+ if (isOpen) initializePicker();
547
+ });
548
+
549
+ watch(() => props.modelValue, (newValue) => {
550
+ if (isRange.value) {
551
+ selectedRange.value = parseRangeValue(newValue);
552
+ } else {
553
+ selectedTime.value = parseTime(newValue);
554
+ }
555
+ }, { immediate: true });
556
+ </script>
557
+
558
+ <style scoped>
559
+ .vtk-time-picker {
560
+ position: relative;
561
+ width: 100%;
562
+ }
563
+
564
+ .time-input {
565
+ width: 100%;
566
+ }
567
+
568
+ .time-input-field {
569
+ cursor: pointer;
570
+ }
571
+
572
+ :deep(.time-input-field .v-field),
573
+ :deep(.time-input-field input) {
574
+ cursor: pointer;
575
+ }
576
+
577
+ .time-icon {
578
+ margin-left: 8px;
579
+ color: rgb(var(--v-theme-on-surface), 0.6);
580
+ }
581
+
582
+ .time-picker-dropdown {
583
+ background: rgb(var(--v-theme-surface));
584
+ border-radius: 4px;
585
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
586
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
587
+ overflow: visible;
588
+ }
589
+
590
+ .time-picker-dropdown.range-mode {
591
+ width: 320px;
592
+ min-width: 320px;
593
+ max-width: 320px;
594
+ }
595
+
596
+ .time-picker-dropdown:not(.range-mode) {
597
+ width: 100%;
598
+ min-width: 100%;
599
+ max-width: none;
600
+ }
601
+
602
+ .time-columns {
603
+ display: flex;
604
+ height: 260px;
605
+ }
606
+
607
+ .time-columns.compact {
608
+ height: 192px;
609
+ }
610
+
611
+ .time-column {
612
+ flex: 1;
613
+ overflow-y: auto;
614
+ padding: 6px 0;
615
+ border-right: 1px solid rgb(var(--v-theme-on-surface), 0.08);
616
+ scrollbar-width: none;
617
+ -ms-overflow-style: none;
618
+ }
619
+
620
+ .time-column::-webkit-scrollbar {
621
+ width: 0;
622
+ height: 0;
623
+ }
624
+
625
+ .time-column:last-child {
626
+ border-right: none;
627
+ }
628
+
629
+ .time-item {
630
+ width: 100%;
631
+ height: 28px;
632
+ border: none;
633
+ background: transparent;
634
+ color: rgb(var(--v-theme-on-surface));
635
+ cursor: pointer;
636
+ font-size: 13px;
637
+ transition: all 0.2s;
638
+ }
639
+
640
+ .time-item:hover {
641
+ background: rgb(var(--v-theme-on-surface), 0.08);
642
+ }
643
+
644
+ .time-item.selected {
645
+ color: rgb(var(--v-theme-primary));
646
+ font-weight: 600;
647
+ background: rgb(var(--v-theme-primary), 0.1);
648
+ }
649
+
650
+ .time-item.disabled {
651
+ color: rgb(var(--v-theme-on-surface), 0.26) !important;
652
+ background: rgb(var(--v-theme-on-surface), 0.04) !important;
653
+ cursor: not-allowed !important;
654
+ pointer-events: none;
655
+ opacity: 0.5;
656
+ }
657
+
658
+ .single-time-panel {
659
+ padding: 8px 0;
660
+ }
661
+
662
+ .range-input-row {
663
+ position: relative;
664
+ display: flex;
665
+ align-items: center;
666
+ justify-content: center;
667
+ flex-wrap: nowrap;
668
+ gap: 10px;
669
+ padding: 10px 12px;
670
+ border-bottom: 1px solid rgb(var(--v-theme-on-surface), 0.12);
671
+ box-sizing: border-box;
672
+ }
673
+
674
+ .range-time-wrap {
675
+ position: relative;
676
+ flex: 0 0 auto;
677
+ }
678
+
679
+ .range-time-input {
680
+ width: 124px;
681
+ height: 32px;
682
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.16);
683
+ border-radius: 4px;
684
+ background: rgb(var(--v-theme-surface));
685
+ color: rgb(var(--v-theme-on-surface));
686
+ font-size: 13px;
687
+ text-align: left;
688
+ padding: 0 12px;
689
+ outline: none;
690
+ cursor: pointer;
691
+ }
692
+
693
+ .range-time-input.active,
694
+ .range-time-input:focus {
695
+ border-color: rgb(var(--v-theme-primary));
696
+ }
697
+
698
+ .range-arrow {
699
+ flex: 0 0 16px;
700
+ width: 16px;
701
+ text-align: center;
702
+ color: rgb(var(--v-theme-on-surface), 0.6);
703
+ font-size: 22px;
704
+ }
705
+
706
+ .range-time-popover {
707
+ position: absolute;
708
+ top: 40px;
709
+ left: 0;
710
+ width: 190px;
711
+ z-index: 2;
712
+ background: rgb(var(--v-theme-surface));
713
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
714
+ border-radius: 4px;
715
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
716
+ }
717
+
718
+ .time-popover-footer {
719
+ display: flex;
720
+ justify-content: flex-end;
721
+ gap: 8px;
722
+ padding: 8px 10px;
723
+ border-top: 1px solid rgb(var(--v-theme-on-surface), 0.12);
724
+ }
725
+
726
+ .picker-footer {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 8px;
730
+ padding: 8px 10px;
731
+ border-top: 1px solid rgb(var(--v-theme-on-surface), 0.12);
732
+ }
733
+
734
+ .footer-spacer {
735
+ flex: 1;
736
+ }
737
+
738
+ .btn {
739
+ padding: 8px 16px;
740
+ border-radius: 4px;
741
+ font-size: 14px;
742
+ cursor: pointer;
743
+ transition: all 0.2s;
744
+ border: none;
745
+ outline: none;
746
+ }
747
+
748
+ .btn.small {
749
+ padding: 4px 8px;
750
+ font-size: 12px;
751
+ }
752
+
753
+ .btn:disabled {
754
+ cursor: not-allowed;
755
+ opacity: 0.5;
756
+ }
757
+
758
+ .btn-text {
759
+ background: none;
760
+ color: rgb(var(--v-theme-primary));
761
+ }
762
+
763
+ .btn-text:hover:not(:disabled) {
764
+ background: rgb(var(--v-theme-on-surface), 0.08);
765
+ }
766
+
767
+ .btn-primary {
768
+ background: rgb(var(--v-theme-primary));
769
+ color: rgb(var(--v-theme-on-primary));
770
+ }
771
+
772
+ .btn-primary:hover:not(:disabled) {
773
+ opacity: 0.9;
774
+ }
775
+ </style>