@yxhl/specter-pui-vtk 1.0.76 → 1.0.78

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,1370 @@
1
+ <template>
2
+ <div class="vtk-date-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="date-time-input">
16
+ <VTextField
17
+ :model-value="displayValue"
18
+ :placeholder="placeholder"
19
+ :disabled="disabled"
20
+ density="compact"
21
+ variant="outlined"
22
+ hide-details
23
+ readonly
24
+ v-bind="menuProps"
25
+ :clearable="clearable && !!displayValue && !disabled"
26
+ clear-icon="mdi-close-circle"
27
+ persistent-clear
28
+ class="date-time-input-field"
29
+ @click:clear.stop="clearValue"
30
+ >
31
+ <template #append-inner>
32
+ <VIcon size="16" class="date-time-icon">mdi-clock-outline</VIcon>
33
+ </template>
34
+ </VTextField>
35
+ </div>
36
+ </template>
37
+
38
+ <div class="date-time-picker-dropdown" :class="{ 'range-mode': isRange }" @click.stop @mousedown.stop>
39
+ <template v-if="isRange">
40
+ <div class="range-input-row">
41
+ <input class="range-date-input" :value="formatDatePart(tempRange[0])" readonly placeholder="开始日期" />
42
+ <button
43
+ class="range-time-input"
44
+ type="button"
45
+ :class="{ active: activeTimeSide === 'start' }"
46
+ @click="openRangeTime('start')"
47
+ >
48
+ {{ formatTimePart(tempRange[0]) || defaultStartTime }}
49
+ </button>
50
+ <span class="range-arrow">›</span>
51
+ <input class="range-date-input" :value="formatDatePart(tempRange[1])" readonly placeholder="结束日期" />
52
+ <button
53
+ class="range-time-input"
54
+ type="button"
55
+ :class="{ active: activeTimeSide === 'end' }"
56
+ @click="openRangeTime('end')"
57
+ >
58
+ {{ formatTimePart(tempRange[1]) || defaultEndTime }}
59
+ </button>
60
+
61
+ <div v-if="activeTimeSide" :class="['range-time-popover', activeTimeSide]">
62
+ <div class="time-columns compact">
63
+ <div class="time-column">
64
+ <button
65
+ v-for="hour in hourOptions"
66
+ :key="hour"
67
+ type="button"
68
+ :class="['time-item', { selected: hour === activeRangeHour }]"
69
+ @click="selectRangeTime('hour', hour)"
70
+ >
71
+ {{ padTime(hour) }}
72
+ </button>
73
+ </div>
74
+ <div class="time-column">
75
+ <button
76
+ v-for="minute in minuteOptions"
77
+ :key="minute"
78
+ type="button"
79
+ :class="['time-item', { selected: minute === activeRangeMinute }]"
80
+ @click="selectRangeTime('minute', minute)"
81
+ >
82
+ {{ padTime(minute) }}
83
+ </button>
84
+ </div>
85
+ <div v-if="showSeconds" class="time-column">
86
+ <button
87
+ v-for="second in secondOptions"
88
+ :key="second"
89
+ type="button"
90
+ :class="['time-item', { selected: second === activeRangeSecond }]"
91
+ @click="selectRangeTime('second', second)"
92
+ >
93
+ {{ padTime(second) }}
94
+ </button>
95
+ </div>
96
+ </div>
97
+ <div class="time-popover-footer">
98
+ <button class="btn btn-text small" type="button" @click="activeTimeSide = null">取消</button>
99
+ <button class="btn btn-primary small" type="button" @click="activeTimeSide = null">确定</button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="range-calendar-shell">
105
+ <div class="calendar-panel">
106
+ <div class="calendar-header">
107
+ <button class="nav-btn-small" type="button" @click="prevLeftMonth">‹</button>
108
+ <span class="month-title">{{ leftYear }} 年 {{ leftMonth + 1 }} 月</span>
109
+ <button class="nav-btn-small invisible" type="button">›</button>
110
+ </div>
111
+ <div class="week-header">
112
+ <div v-for="day in weekDays" :key="day" class="week-day">{{ day }}</div>
113
+ </div>
114
+ <div class="day-grid">
115
+ <button
116
+ v-for="day in leftDayList"
117
+ :key="day.date"
118
+ type="button"
119
+ :class="rangeDayClass(day)"
120
+ @click="selectRangeDay(day)"
121
+ >
122
+ {{ day.day }}
123
+ </button>
124
+ </div>
125
+ </div>
126
+
127
+ <div class="calendar-divider"></div>
128
+
129
+ <div class="calendar-panel">
130
+ <div class="calendar-header">
131
+ <button class="nav-btn-small invisible" type="button">‹</button>
132
+ <span class="month-title">{{ rightYear }} 年 {{ rightMonth + 1 }} 月</span>
133
+ <button class="nav-btn-small" type="button" @click="nextRightMonth">›</button>
134
+ </div>
135
+ <div class="week-header">
136
+ <div v-for="day in weekDays" :key="day" class="week-day">{{ day }}</div>
137
+ </div>
138
+ <div class="day-grid">
139
+ <button
140
+ v-for="day in rightDayList"
141
+ :key="day.date"
142
+ type="button"
143
+ :class="rangeDayClass(day)"
144
+ @click="selectRangeDay(day)"
145
+ >
146
+ {{ day.day }}
147
+ </button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </template>
152
+
153
+ <template v-else>
154
+ <div class="single-input-row">
155
+ <input class="single-date-input" :value="formatDatePart(tempDate)" readonly placeholder="请选择日期" />
156
+ <button
157
+ class="single-time-input"
158
+ type="button"
159
+ :class="{ active: activeSingleTime }"
160
+ @click="activeSingleTime = true"
161
+ >
162
+ {{ singleTimeValue }}
163
+ </button>
164
+
165
+ <div v-if="activeSingleTime" class="single-time-popover">
166
+ <div class="time-columns compact">
167
+ <div class="time-column">
168
+ <button
169
+ v-for="hour in hourOptions"
170
+ :key="hour"
171
+ type="button"
172
+ :class="['time-item', { selected: hour === selectedHour, disabled: isTimePartDisabled(hour, selectedMinute, selectedSecond) }]"
173
+ @click="selectHour(hour)"
174
+ >
175
+ {{ padTime(hour) }}
176
+ </button>
177
+ </div>
178
+ <div class="time-column">
179
+ <button
180
+ v-for="minute in minuteOptions"
181
+ :key="minute"
182
+ type="button"
183
+ :class="['time-item', { selected: minute === selectedMinute, disabled: isTimePartDisabled(selectedHour, minute, selectedSecond) }]"
184
+ @click="selectMinute(minute)"
185
+ >
186
+ {{ padTime(minute) }}
187
+ </button>
188
+ </div>
189
+ <div v-if="showSeconds" class="time-column">
190
+ <button
191
+ v-for="second in secondOptions"
192
+ :key="second"
193
+ type="button"
194
+ :class="['time-item', { selected: second === selectedSecond, disabled: isTimePartDisabled(selectedHour, selectedMinute, second) }]"
195
+ @click="selectSecond(second)"
196
+ >
197
+ {{ padTime(second) }}
198
+ </button>
199
+ </div>
200
+ </div>
201
+ <div class="time-popover-footer">
202
+ <button class="btn btn-text small" type="button" @click="activeSingleTime = false">取消</button>
203
+ <button class="btn btn-primary small" type="button" @click="activeSingleTime = false">确定</button>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <div class="single-calendar-panel">
209
+ <div class="picker-header single-header">
210
+ <button class="nav-btn" type="button" @click="prevPeriod">‹</button>
211
+ <div class="header-title">
212
+ <span class="clickable" @click="changeViewMode('year')">{{ currentYear }} 年</span>
213
+ <span v-if="viewMode !== 'year'" class="clickable" @click="changeViewMode('month')">
214
+ {{ currentMonth + 1 }} 月
215
+ </span>
216
+ </div>
217
+ <button class="nav-btn" type="button" @click="nextPeriod">›</button>
218
+ </div>
219
+
220
+ <div v-if="viewMode === 'year'" class="picker-body">
221
+ <div class="year-grid">
222
+ <button
223
+ v-for="year in yearList"
224
+ :key="year"
225
+ type="button"
226
+ :class="['year-item', { selected: year === currentYear, disabled: isYearDisabled(year) }]"
227
+ @click="selectYear(year)"
228
+ >
229
+ {{ year }}
230
+ </button>
231
+ </div>
232
+ </div>
233
+
234
+ <div v-else-if="viewMode === 'month'" class="picker-body">
235
+ <div class="month-grid">
236
+ <button
237
+ v-for="(month, index) in monthList"
238
+ :key="month"
239
+ type="button"
240
+ :class="['month-item', { selected: index === currentMonth, disabled: isMonthDisabled(index) }]"
241
+ @click="selectMonth(index)"
242
+ >
243
+ {{ month }}
244
+ </button>
245
+ </div>
246
+ </div>
247
+
248
+ <div v-else class="picker-body">
249
+ <div class="week-header">
250
+ <div v-for="day in weekDays" :key="day" class="week-day">{{ day }}</div>
251
+ </div>
252
+ <div class="day-grid">
253
+ <button
254
+ v-for="day in dayList"
255
+ :key="day.date"
256
+ type="button"
257
+ :class="['day-item', {
258
+ 'other-month': day.otherMonth,
259
+ selected: isDaySelected(day),
260
+ today: day.isToday,
261
+ disabled: isDayDisabled(day)
262
+ }]"
263
+ @click="selectDay(day)"
264
+ >
265
+ {{ day.day }}
266
+ </button>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </template>
271
+
272
+ <div class="picker-footer">
273
+ <button v-if="!isRange" class="btn btn-text" type="button" @click="handleNow">此刻</button>
274
+ <button v-if="isRange" class="btn btn-text" type="button" @click="clearTempRange">清空</button>
275
+ <span class="footer-spacer"></span>
276
+ <button class="btn btn-text" type="button" @click="handleCancel">取消</button>
277
+ <button class="btn btn-primary" type="button" :disabled="!canConfirm" @click="handleConfirm">确定</button>
278
+ </div>
279
+ </div>
280
+ </VMenu>
281
+
282
+ <div v-if="inline" class="date-time-picker-dropdown inline-mode" :class="{ 'range-mode': isRange }" @click.stop @mousedown.stop>
283
+ <div class="inline-message">内联模式请使用默认单选面板。</div>
284
+ </div>
285
+ </div>
286
+ </template>
287
+
288
+ <script setup>
289
+ import { computed, ref, watch } from 'vue';
290
+
291
+ defineOptions({
292
+ name: 'VtkDateTimePicker',
293
+ });
294
+
295
+ const props = defineProps({
296
+ modelValue: {
297
+ type: [String, Date, Number, Array],
298
+ default: null
299
+ },
300
+ mode: {
301
+ type: String,
302
+ default: 'single',
303
+ validator: (value) => ['single', 'range'].includes(value)
304
+ },
305
+ placeholder: {
306
+ type: String,
307
+ default: '请选择日期时间'
308
+ },
309
+ disabled: {
310
+ type: Boolean,
311
+ default: false
312
+ },
313
+ clearable: {
314
+ type: Boolean,
315
+ default: true
316
+ },
317
+ format: {
318
+ type: String,
319
+ default: 'YYYY-MM-DD HH:mm:ss'
320
+ },
321
+ valueFormat: {
322
+ type: String,
323
+ default: 'YYYY-MM-DD HH:mm:ss'
324
+ },
325
+ separator: {
326
+ type: String,
327
+ default: ' 至 '
328
+ },
329
+ min: {
330
+ type: [String, Date, Number],
331
+ default: null
332
+ },
333
+ max: {
334
+ type: [String, Date, Number],
335
+ default: null
336
+ },
337
+ disableFuture: {
338
+ type: Boolean,
339
+ default: false
340
+ },
341
+ minuteStep: {
342
+ type: Number,
343
+ default: 1
344
+ },
345
+ secondStep: {
346
+ type: Number,
347
+ default: 1
348
+ },
349
+ showSeconds: {
350
+ type: Boolean,
351
+ default: true
352
+ },
353
+ inline: {
354
+ type: Boolean,
355
+ default: false
356
+ },
357
+ placement: {
358
+ type: String,
359
+ default: 'left',
360
+ validator: (value) => ['left', 'right'].includes(value)
361
+ },
362
+ noTeleport: {
363
+ type: Boolean,
364
+ default: false
365
+ },
366
+ onCancel: {
367
+ type: Function,
368
+ default: null
369
+ },
370
+ onConfirm: {
371
+ type: Function,
372
+ default: null
373
+ }
374
+ });
375
+
376
+ const emit = defineEmits(['update:modelValue', 'change', 'cancel', 'confirm']);
377
+
378
+ const showPicker = ref(false);
379
+ const selectedDateTime = ref(null);
380
+ const selectedRange = ref([]);
381
+ const tempDate = ref(null);
382
+ const tempRange = ref([null, null]);
383
+ const currentYear = ref(new Date().getFullYear());
384
+ const currentMonth = ref(new Date().getMonth());
385
+ const leftYear = ref(new Date().getFullYear());
386
+ const leftMonth = ref(new Date().getMonth());
387
+ const rightYear = ref(new Date().getMonth() === 11 ? new Date().getFullYear() + 1 : new Date().getFullYear());
388
+ const rightMonth = ref(new Date().getMonth() === 11 ? 0 : new Date().getMonth() + 1);
389
+ const viewMode = ref('day');
390
+ const selectedHour = ref(0);
391
+ const selectedMinute = ref(0);
392
+ const selectedSecond = ref(0);
393
+ const activeRangeSide = ref('start');
394
+ const activeTimeSide = ref(null);
395
+ const activeSingleTime = ref(false);
396
+
397
+ const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
398
+ const monthList = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
399
+ const defaultStartTime = '00:00:00';
400
+ const defaultEndTime = '23:59:59';
401
+
402
+ const isRange = computed(() => props.mode === 'range');
403
+ const menuAttach = computed(() => (props.noTeleport ? true : false));
404
+ const menuLocation = computed(() => props.placement === 'right' ? 'bottom end' : 'bottom start');
405
+ const menuOrigin = computed(() => props.placement === 'right' ? 'top end' : 'top start');
406
+ const menuWidth = computed(() => isRange.value ? 646 : 360);
407
+ const minDateTime = computed(() => parseValue(props.min));
408
+ const maxDateTime = computed(() => parseValue(props.max));
409
+ const hourOptions = computed(() => Array.from({ length: 24 }, (_, index) => index));
410
+ const minuteOptions = computed(() => buildStepOptions(props.minuteStep));
411
+ const secondOptions = computed(() => buildStepOptions(props.secondStep));
412
+
413
+ const yearList = computed(() => {
414
+ const startYear = Math.floor(currentYear.value / 12) * 12;
415
+ return Array.from({ length: 12 }, (_, index) => startYear + index);
416
+ });
417
+
418
+ const dayList = computed(() => getDayList(currentYear.value, currentMonth.value));
419
+ const leftDayList = computed(() => getDayList(leftYear.value, leftMonth.value));
420
+ const rightDayList = computed(() => getDayList(rightYear.value, rightMonth.value));
421
+
422
+ const displayValue = computed(() => {
423
+ if (isRange.value) {
424
+ if (!selectedRange.value.length) return '';
425
+ const [start, end] = selectedRange.value;
426
+ return [start, end].filter(Boolean).map(date => formatByPattern(date, props.format)).join(props.separator);
427
+ }
428
+ return selectedDateTime.value ? formatByPattern(selectedDateTime.value, props.format) : '';
429
+ });
430
+
431
+ const previewValue = computed(() => tempDate.value ? formatByPattern(buildTempDateTime(), props.format) : '');
432
+ const singleTimeValue = computed(() => {
433
+ const pattern = props.showSeconds ? 'HH:mm:ss' : 'HH:mm';
434
+ const date = buildTempDateTime() || new Date(2000, 0, 1, selectedHour.value, selectedMinute.value, selectedSecond.value);
435
+ return formatByPattern(date, pattern);
436
+ });
437
+
438
+ const canConfirm = computed(() => {
439
+ if (isRange.value) {
440
+ const [start, end] = tempRange.value;
441
+ return !!start && !!end && start <= end && !isDateTimeDisabled(start) && !isDateTimeDisabled(end);
442
+ }
443
+ if (!tempDate.value) return false;
444
+ return !isDateTimeDisabled(buildTempDateTime());
445
+ });
446
+
447
+ const activeRangeDate = computed(() => {
448
+ return activeTimeSide.value === 'end' ? tempRange.value[1] : tempRange.value[0];
449
+ });
450
+
451
+ const activeRangeHour = computed(() => activeRangeDate.value ? activeRangeDate.value.getHours() : 0);
452
+ const activeRangeMinute = computed(() => activeRangeDate.value ? activeRangeDate.value.getMinutes() : 0);
453
+ const activeRangeSecond = computed(() => activeRangeDate.value ? activeRangeDate.value.getSeconds() : 0);
454
+
455
+ const buildStepOptions = (step) => {
456
+ const normalizedStep = Number.isFinite(step) && step > 0 ? Math.max(1, Math.floor(step)) : 1;
457
+ const options = [];
458
+ for (let value = 0; value < 60; value += normalizedStep) {
459
+ options.push(value);
460
+ }
461
+ return options;
462
+ };
463
+
464
+ const getDayList = (year, month) => {
465
+ const firstDay = new Date(year, month, 1);
466
+ const startDay = firstDay.getDay();
467
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
468
+ const daysInPrevMonth = new Date(year, month, 0).getDate();
469
+ const today = new Date();
470
+ const days = [];
471
+
472
+ for (let index = startDay - 1; index >= 0; index -= 1) {
473
+ days.push(createDay(new Date(year, month - 1, daysInPrevMonth - index), true, today));
474
+ }
475
+ for (let day = 1; day <= daysInMonth; day += 1) {
476
+ days.push(createDay(new Date(year, month, day), false, today));
477
+ }
478
+ for (let day = 1; days.length < 42; day += 1) {
479
+ days.push(createDay(new Date(year, month + 1, day), true, today));
480
+ }
481
+
482
+ return days;
483
+ };
484
+
485
+ const createDay = (date, otherMonth, today) => ({
486
+ date: formatByPattern(date, 'YYYY-MM-DD'),
487
+ day: date.getDate(),
488
+ year: date.getFullYear(),
489
+ month: date.getMonth(),
490
+ otherMonth,
491
+ isToday: isSameDate(date, today)
492
+ });
493
+
494
+ const padTime = (value) => String(value).padStart(2, '0');
495
+
496
+ const formatByPattern = (date, pattern) => {
497
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
498
+ const replacements = {
499
+ YYYY: date.getFullYear(),
500
+ MM: padTime(date.getMonth() + 1),
501
+ DD: padTime(date.getDate()),
502
+ HH: padTime(date.getHours()),
503
+ mm: padTime(date.getMinutes()),
504
+ ss: padTime(date.getSeconds())
505
+ };
506
+ return Object.keys(replacements).reduce((result, token) => {
507
+ return result.replace(new RegExp(token, 'g'), replacements[token]);
508
+ }, pattern);
509
+ };
510
+
511
+ const formatDatePart = (date) => date ? formatByPattern(date, 'YYYY-MM-DD') : '';
512
+ const formatTimePart = (date) => date ? formatByPattern(date, props.showSeconds ? 'HH:mm:ss' : 'HH:mm') : '';
513
+
514
+ const parseValue = (value) => {
515
+ if (!value && value !== 0) return null;
516
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
517
+ if (typeof value === 'number') {
518
+ const date = new Date(value);
519
+ return Number.isNaN(date.getTime()) ? null : date;
520
+ }
521
+ if (typeof value !== 'string') return null;
522
+
523
+ const normalized = value.trim().replace(/\//g, '-');
524
+ const match = normalized.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?/);
525
+ if (!match) {
526
+ const fallback = new Date(value);
527
+ return Number.isNaN(fallback.getTime()) ? null : fallback;
528
+ }
529
+
530
+ const [, year, month, day, hour = '0', minute = '0', second = '0'] = match;
531
+ const date = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
532
+ return Number.isNaN(date.getTime()) ? null : date;
533
+ };
534
+
535
+ const parseRangeValue = (value) => {
536
+ if (Array.isArray(value)) return value.map(parseValue).filter(Boolean).slice(0, 2);
537
+ if (typeof value === 'string' && value.includes(props.separator)) {
538
+ return value.split(props.separator).map(item => parseValue(item.trim())).filter(Boolean).slice(0, 2);
539
+ }
540
+ return [];
541
+ };
542
+
543
+ const toModelValue = (date) => {
544
+ if (!date) return null;
545
+ if (props.valueFormat === 'timestamp') return date.getTime();
546
+ if (props.valueFormat === 'Date') return new Date(date.getTime());
547
+ return formatByPattern(date, props.valueFormat);
548
+ };
549
+
550
+ const toModelRange = (range) => range.map(date => toModelValue(date));
551
+
552
+ const isSameDate = (left, right) => {
553
+ return left.getFullYear() === right.getFullYear()
554
+ && left.getMonth() === right.getMonth()
555
+ && left.getDate() === right.getDate();
556
+ };
557
+
558
+ const buildTempDateTime = (hour = selectedHour.value, minute = selectedMinute.value, second = selectedSecond.value) => {
559
+ if (!tempDate.value) return null;
560
+ return new Date(tempDate.value.getFullYear(), tempDate.value.getMonth(), tempDate.value.getDate(), hour, minute, props.showSeconds ? second : 0);
561
+ };
562
+
563
+ const isDateTimeDisabled = (date) => {
564
+ if (!date) return true;
565
+ if (minDateTime.value && date < minDateTime.value) return true;
566
+ if (maxDateTime.value && date > maxDateTime.value) return true;
567
+ if (props.disableFuture && date > new Date()) return true;
568
+ return false;
569
+ };
570
+
571
+ const isYearDisabled = (year) => {
572
+ const start = new Date(year, 0, 1, 0, 0, 0);
573
+ const end = new Date(year, 11, 31, 23, 59, 59);
574
+ if (minDateTime.value && end < minDateTime.value) return true;
575
+ if (maxDateTime.value && start > maxDateTime.value) return true;
576
+ if (props.disableFuture && start > new Date()) return true;
577
+ return false;
578
+ };
579
+
580
+ const isMonthDisabled = (month) => {
581
+ const start = new Date(currentYear.value, month, 1, 0, 0, 0);
582
+ const end = new Date(currentYear.value, month + 1, 0, 23, 59, 59);
583
+ if (minDateTime.value && end < minDateTime.value) return true;
584
+ if (maxDateTime.value && start > maxDateTime.value) return true;
585
+ if (props.disableFuture && start > new Date()) return true;
586
+ return false;
587
+ };
588
+
589
+ const isDayDisabled = (day) => {
590
+ const start = new Date(day.year, day.month, day.day, 0, 0, 0);
591
+ const end = new Date(day.year, day.month, day.day, 23, 59, 59);
592
+ if (minDateTime.value && end < minDateTime.value) return true;
593
+ if (maxDateTime.value && start > maxDateTime.value) return true;
594
+ if (props.disableFuture && start > new Date()) return true;
595
+ return false;
596
+ };
597
+
598
+ const isTimePartDisabled = (hour, minute, second) => {
599
+ if (!tempDate.value) return false;
600
+ return isDateTimeDisabled(buildTempDateTime(hour, minute, second));
601
+ };
602
+
603
+ const isDaySelected = (day) => {
604
+ if (!tempDate.value) return false;
605
+ return tempDate.value.getFullYear() === day.year
606
+ && tempDate.value.getMonth() === day.month
607
+ && tempDate.value.getDate() === day.day;
608
+ };
609
+
610
+ const isRangeDaySelected = (day, side) => {
611
+ const date = tempRange.value[side === 'start' ? 0 : 1];
612
+ return !!date && date.getFullYear() === day.year && date.getMonth() === day.month && date.getDate() === day.day;
613
+ };
614
+
615
+ const isRangeDayInRange = (day) => {
616
+ const [start, end] = tempRange.value;
617
+ if (!start || !end) return false;
618
+ const date = new Date(day.year, day.month, day.day, 12, 0, 0);
619
+ return date > start && date < end;
620
+ };
621
+
622
+ const rangeDayClass = (day) => ['day-item', {
623
+ 'other-month': day.otherMonth,
624
+ today: day.isToday,
625
+ disabled: isDayDisabled(day),
626
+ selected: isRangeDaySelected(day, 'start') || isRangeDaySelected(day, 'end'),
627
+ 'range-start': isRangeDaySelected(day, 'start'),
628
+ 'range-end': isRangeDaySelected(day, 'end'),
629
+ 'in-range': isRangeDayInRange(day)
630
+ }];
631
+
632
+ const setSinglePanelFromDate = (date) => {
633
+ if (!date) return;
634
+ currentYear.value = date.getFullYear();
635
+ currentMonth.value = date.getMonth();
636
+ tempDate.value = new Date(date.getFullYear(), date.getMonth(), date.getDate());
637
+ selectedHour.value = date.getHours();
638
+ selectedMinute.value = date.getMinutes();
639
+ selectedSecond.value = date.getSeconds();
640
+ };
641
+
642
+ const setRangePanelsFromDate = (date) => {
643
+ const base = date || new Date();
644
+ leftYear.value = base.getFullYear();
645
+ leftMonth.value = base.getMonth();
646
+ const right = new Date(base.getFullYear(), base.getMonth() + 1, 1);
647
+ rightYear.value = right.getFullYear();
648
+ rightMonth.value = right.getMonth();
649
+ };
650
+
651
+ const initializePicker = () => {
652
+ if (isRange.value) {
653
+ tempRange.value = selectedRange.value.length ? selectedRange.value.map(date => new Date(date.getTime())) : [null, null];
654
+ setRangePanelsFromDate(tempRange.value[0] || new Date());
655
+ activeRangeSide.value = tempRange.value[0] && !tempRange.value[1] ? 'end' : 'start';
656
+ activeTimeSide.value = null;
657
+ return;
658
+ }
659
+ setSinglePanelFromDate(selectedDateTime.value || new Date());
660
+ viewMode.value = 'day';
661
+ activeSingleTime.value = false;
662
+ };
663
+
664
+ const prevPeriod = () => {
665
+ if (viewMode.value === 'year') currentYear.value -= 12;
666
+ else if (viewMode.value === 'month') currentYear.value -= 1;
667
+ else if (currentMonth.value === 0) {
668
+ currentMonth.value = 11;
669
+ currentYear.value -= 1;
670
+ } else currentMonth.value -= 1;
671
+ };
672
+
673
+ const nextPeriod = () => {
674
+ if (viewMode.value === 'year') currentYear.value += 12;
675
+ else if (viewMode.value === 'month') currentYear.value += 1;
676
+ else if (currentMonth.value === 11) {
677
+ currentMonth.value = 0;
678
+ currentYear.value += 1;
679
+ } else currentMonth.value += 1;
680
+ };
681
+
682
+ const prevLeftMonth = () => {
683
+ const left = new Date(leftYear.value, leftMonth.value - 1, 1);
684
+ leftYear.value = left.getFullYear();
685
+ leftMonth.value = left.getMonth();
686
+ const right = new Date(leftYear.value, leftMonth.value + 1, 1);
687
+ rightYear.value = right.getFullYear();
688
+ rightMonth.value = right.getMonth();
689
+ };
690
+
691
+ const nextRightMonth = () => {
692
+ const right = new Date(rightYear.value, rightMonth.value + 1, 1);
693
+ rightYear.value = right.getFullYear();
694
+ rightMonth.value = right.getMonth();
695
+ const left = new Date(rightYear.value, rightMonth.value - 1, 1);
696
+ leftYear.value = left.getFullYear();
697
+ leftMonth.value = left.getMonth();
698
+ };
699
+
700
+ const changeViewMode = (mode) => {
701
+ viewMode.value = mode;
702
+ };
703
+
704
+ const selectYear = (year) => {
705
+ if (isYearDisabled(year)) return;
706
+ currentYear.value = year;
707
+ viewMode.value = 'month';
708
+ };
709
+
710
+ const selectMonth = (month) => {
711
+ if (isMonthDisabled(month)) return;
712
+ currentMonth.value = month;
713
+ viewMode.value = 'day';
714
+ };
715
+
716
+ const selectDay = (day) => {
717
+ if (isDayDisabled(day)) return;
718
+ tempDate.value = new Date(day.year, day.month, day.day);
719
+ };
720
+
721
+ const selectRangeDay = (day) => {
722
+ if (isDayDisabled(day)) return;
723
+ const selected = new Date(day.year, day.month, day.day);
724
+ if (activeRangeSide.value === 'start' || !tempRange.value[0] || (tempRange.value[0] && tempRange.value[1])) {
725
+ const currentStart = tempRange.value[0];
726
+ selected.setHours(currentStart?.getHours() ?? 0, currentStart?.getMinutes() ?? 0, props.showSeconds ? currentStart?.getSeconds() ?? 0 : 0, 0);
727
+ tempRange.value = [selected, null];
728
+ activeRangeSide.value = 'end';
729
+ } else {
730
+ const currentEnd = tempRange.value[1];
731
+ selected.setHours(currentEnd?.getHours() ?? 23, currentEnd?.getMinutes() ?? 59, props.showSeconds ? currentEnd?.getSeconds() ?? 59 : 0, 0);
732
+ const start = tempRange.value[0];
733
+ tempRange.value = start <= selected ? [start, selected] : [selected, start];
734
+ activeRangeSide.value = 'start';
735
+ }
736
+ };
737
+
738
+ const selectHour = (hour) => {
739
+ if (isTimePartDisabled(hour, selectedMinute.value, selectedSecond.value)) return;
740
+ selectedHour.value = hour;
741
+ };
742
+
743
+ const selectMinute = (minute) => {
744
+ if (isTimePartDisabled(selectedHour.value, minute, selectedSecond.value)) return;
745
+ selectedMinute.value = minute;
746
+ };
747
+
748
+ const selectSecond = (second) => {
749
+ if (isTimePartDisabled(selectedHour.value, selectedMinute.value, second)) return;
750
+ selectedSecond.value = second;
751
+ };
752
+
753
+ const openRangeTime = (side) => {
754
+ activeTimeSide.value = side;
755
+ activeRangeSide.value = side;
756
+ const index = side === 'start' ? 0 : 1;
757
+ if (!tempRange.value[index]) {
758
+ const base = new Date();
759
+ base.setHours(side === 'start' ? 0 : 23, side === 'start' ? 0 : 59, props.showSeconds ? side === 'start' ? 0 : 59 : 0, 0);
760
+ tempRange.value[index] = base;
761
+ }
762
+ };
763
+
764
+ const selectRangeTime = (part, value) => {
765
+ const index = activeTimeSide.value === 'end' ? 1 : 0;
766
+ const fallback = new Date();
767
+ const date = tempRange.value[index] ? new Date(tempRange.value[index].getTime()) : fallback;
768
+ if (part === 'hour') date.setHours(value);
769
+ if (part === 'minute') date.setMinutes(value);
770
+ if (part === 'second') date.setSeconds(value);
771
+ if (!props.showSeconds) date.setSeconds(0);
772
+ date.setMilliseconds(0);
773
+ tempRange.value[index] = date;
774
+ };
775
+
776
+ const closePicker = () => {
777
+ showPicker.value = false;
778
+ activeSingleTime.value = false;
779
+ };
780
+
781
+ const clearTempRange = () => {
782
+ tempRange.value = [null, null];
783
+ activeRangeSide.value = 'start';
784
+ activeTimeSide.value = null;
785
+ };
786
+
787
+ const clearValue = () => {
788
+ if (isRange.value) {
789
+ selectedRange.value = [];
790
+ tempRange.value = [null, null];
791
+ emit('update:modelValue', []);
792
+ emit('change', []);
793
+ } else {
794
+ selectedDateTime.value = null;
795
+ tempDate.value = null;
796
+ emit('update:modelValue', null);
797
+ emit('change', null);
798
+ }
799
+ };
800
+
801
+ const confirm = () => {
802
+ if (!canConfirm.value) return;
803
+ if (isRange.value) {
804
+ selectedRange.value = tempRange.value.map(date => new Date(date.getTime()));
805
+ const modelValue = toModelRange(selectedRange.value);
806
+ emit('update:modelValue', modelValue);
807
+ emit('change', modelValue);
808
+ closePicker();
809
+ return;
810
+ }
811
+ const nextValue = buildTempDateTime();
812
+ selectedDateTime.value = nextValue;
813
+ const modelValue = toModelValue(nextValue);
814
+ emit('update:modelValue', modelValue);
815
+ emit('change', modelValue);
816
+ closePicker();
817
+ };
818
+
819
+ const handleCancel = () => {
820
+ if (props.onCancel) {
821
+ props.onCancel();
822
+ } else {
823
+ emit('cancel');
824
+ closePicker();
825
+ }
826
+ };
827
+
828
+ const handleConfirm = () => {
829
+ const value = isRange.value ? toModelRange(tempRange.value) : toModelValue(buildTempDateTime());
830
+ if (props.onConfirm) {
831
+ props.onConfirm(value);
832
+ } else {
833
+ emit('confirm', value);
834
+ confirm();
835
+ }
836
+ };
837
+
838
+ const handleNow = () => {
839
+ const now = new Date();
840
+ if (isDateTimeDisabled(now)) return;
841
+ setSinglePanelFromDate(now);
842
+ };
843
+
844
+ watch(showPicker, (isOpen) => {
845
+ if (isOpen) initializePicker();
846
+ });
847
+
848
+ watch(() => props.modelValue, (newValue) => {
849
+ if (isRange.value) {
850
+ selectedRange.value = parseRangeValue(newValue);
851
+ } else {
852
+ selectedDateTime.value = parseValue(newValue);
853
+ }
854
+ }, { immediate: true });
855
+ </script>
856
+
857
+ <style scoped>
858
+ .vtk-date-time-picker {
859
+ position: relative;
860
+ width: 100%;
861
+ }
862
+
863
+ .date-time-input {
864
+ width: 100%;
865
+ }
866
+
867
+ .date-time-input-field {
868
+ cursor: pointer;
869
+ }
870
+
871
+ :deep(.date-time-input-field .v-field),
872
+ :deep(.date-time-input-field input) {
873
+ cursor: pointer;
874
+ }
875
+
876
+ .date-time-icon {
877
+ margin-left: 8px;
878
+ color: rgb(var(--v-theme-on-surface), 0.6);
879
+ }
880
+
881
+ .date-time-picker-dropdown {
882
+ background: rgb(var(--v-theme-surface));
883
+ border-radius: 4px;
884
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
885
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
886
+ overflow: visible;
887
+ }
888
+
889
+ .date-time-picker-dropdown.range-mode {
890
+ width: 646px;
891
+ min-width: 646px;
892
+ max-width: 646px;
893
+ }
894
+
895
+ .date-time-picker-dropdown:not(.range-mode) {
896
+ width: 360px;
897
+ min-width: 360px;
898
+ max-width: none;
899
+ }
900
+
901
+ .date-time-picker-dropdown.inline-mode {
902
+ box-shadow: none;
903
+ width: fit-content;
904
+ border: none;
905
+ }
906
+
907
+ .picker-shell,
908
+ .range-calendar-shell {
909
+ display: flex;
910
+ align-items: stretch;
911
+ flex-wrap: nowrap;
912
+ }
913
+
914
+ .date-panel {
915
+ width: 288px;
916
+ border-right: 1px solid rgb(var(--v-theme-on-surface), 0.12);
917
+ }
918
+
919
+ .calendar-panel {
920
+ flex: 0 0 322px;
921
+ width: 322px;
922
+ min-width: 322px;
923
+ max-width: 322px;
924
+ padding: 12px 16px 16px;
925
+ box-sizing: border-box;
926
+ }
927
+
928
+ .calendar-divider {
929
+ flex: 0 0 1px;
930
+ width: 1px;
931
+ background: rgb(var(--v-theme-on-surface), 0.12);
932
+ }
933
+
934
+ .picker-header,
935
+ .calendar-header {
936
+ display: flex;
937
+ align-items: center;
938
+ justify-content: space-between;
939
+ }
940
+
941
+ .picker-header {
942
+ padding: 12px 16px;
943
+ border-bottom: 1px solid rgb(var(--v-theme-on-surface), 0.12);
944
+ }
945
+
946
+ .picker-header.single-header {
947
+ border-bottom: none;
948
+ padding: 14px 16px 8px;
949
+ }
950
+
951
+ .calendar-header {
952
+ height: 36px;
953
+ margin-bottom: 8px;
954
+ }
955
+
956
+ .header-title,
957
+ .month-title {
958
+ font-size: 16px;
959
+ font-weight: 500;
960
+ color: rgb(var(--v-theme-on-surface));
961
+ }
962
+
963
+ .clickable {
964
+ cursor: pointer;
965
+ padding: 4px 8px;
966
+ border-radius: 4px;
967
+ transition: background 0.2s;
968
+ }
969
+
970
+ .clickable:hover {
971
+ background: rgb(var(--v-theme-on-surface), 0.08);
972
+ }
973
+
974
+ .nav-btn,
975
+ .nav-btn-small {
976
+ border: none;
977
+ background: none;
978
+ color: rgb(var(--v-theme-on-surface), 0.6);
979
+ cursor: pointer;
980
+ border-radius: 4px;
981
+ transition: background 0.2s;
982
+ }
983
+
984
+ .nav-btn {
985
+ font-size: 20px;
986
+ padding: 4px 12px;
987
+ }
988
+
989
+ .nav-btn-small {
990
+ font-size: 18px;
991
+ padding: 4px 8px;
992
+ }
993
+
994
+ .nav-btn:hover,
995
+ .nav-btn-small:hover {
996
+ background: rgb(var(--v-theme-on-surface), 0.08);
997
+ }
998
+
999
+ .nav-btn-small.invisible {
1000
+ visibility: hidden;
1001
+ }
1002
+
1003
+ .picker-body {
1004
+ padding: 10px 16px 16px;
1005
+ min-height: 250px;
1006
+ }
1007
+
1008
+ .year-grid,
1009
+ .month-grid {
1010
+ display: grid;
1011
+ grid-template-columns: repeat(3, 1fr);
1012
+ gap: 8px;
1013
+ }
1014
+
1015
+ .year-item,
1016
+ .month-item {
1017
+ height: 42px;
1018
+ border: 1px solid transparent;
1019
+ background: transparent;
1020
+ text-align: center;
1021
+ border-radius: 4px;
1022
+ cursor: pointer;
1023
+ transition: all 0.2s;
1024
+ color: rgb(var(--v-theme-on-surface));
1025
+ font-size: 13px;
1026
+ }
1027
+
1028
+ .year-item:hover,
1029
+ .month-item:hover {
1030
+ background: rgb(var(--v-theme-on-surface), 0.08);
1031
+ }
1032
+
1033
+ .year-item.selected,
1034
+ .month-item.selected {
1035
+ background: rgb(var(--v-theme-primary));
1036
+ color: rgb(var(--v-theme-on-primary));
1037
+ }
1038
+
1039
+ .week-header {
1040
+ display: grid;
1041
+ grid-template-columns: repeat(7, 1fr);
1042
+ gap: 2px;
1043
+ margin-bottom: 6px;
1044
+ }
1045
+
1046
+ .week-day {
1047
+ text-align: center;
1048
+ padding: 6px 0;
1049
+ font-size: 12px;
1050
+ color: rgb(var(--v-theme-on-surface), 0.6);
1051
+ font-weight: 500;
1052
+ }
1053
+
1054
+ .day-grid {
1055
+ display: grid;
1056
+ grid-template-columns: repeat(7, 1fr);
1057
+ gap: 2px;
1058
+ }
1059
+
1060
+ .day-item {
1061
+ aspect-ratio: 1;
1062
+ border: 1px solid transparent;
1063
+ background: transparent;
1064
+ display: flex;
1065
+ align-items: center;
1066
+ justify-content: center;
1067
+ border-radius: 4px;
1068
+ cursor: pointer;
1069
+ transition: all 0.2s;
1070
+ font-size: 13px;
1071
+ color: rgb(var(--v-theme-on-surface));
1072
+ }
1073
+
1074
+ .day-item:hover {
1075
+ background: rgb(var(--v-theme-on-surface), 0.08);
1076
+ }
1077
+
1078
+ .day-item.other-month {
1079
+ color: rgb(var(--v-theme-on-surface), 0.38);
1080
+ }
1081
+
1082
+ .day-item.today {
1083
+ color: rgb(var(--v-theme-primary));
1084
+ font-weight: 600;
1085
+ }
1086
+
1087
+ .day-item.selected,
1088
+ .day-item.range-start,
1089
+ .day-item.range-end {
1090
+ background: rgb(var(--v-theme-primary));
1091
+ color: rgb(var(--v-theme-on-primary));
1092
+ }
1093
+
1094
+ .day-item.in-range {
1095
+ background: rgb(var(--v-theme-primary), 0.1);
1096
+ }
1097
+
1098
+ .year-item.disabled,
1099
+ .month-item.disabled,
1100
+ .day-item.disabled,
1101
+ .time-item.disabled {
1102
+ color: rgb(var(--v-theme-on-surface), 0.26) !important;
1103
+ background: rgb(var(--v-theme-on-surface), 0.04) !important;
1104
+ cursor: not-allowed !important;
1105
+ pointer-events: none;
1106
+ opacity: 0.5;
1107
+ }
1108
+
1109
+ .time-panel {
1110
+ width: 156px;
1111
+ }
1112
+
1113
+ .time-header {
1114
+ height: 49px;
1115
+ display: flex;
1116
+ align-items: center;
1117
+ justify-content: center;
1118
+ font-size: 14px;
1119
+ font-weight: 500;
1120
+ color: rgb(var(--v-theme-on-surface));
1121
+ border-bottom: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1122
+ }
1123
+
1124
+ .time-columns {
1125
+ display: flex;
1126
+ height: 250px;
1127
+ }
1128
+
1129
+ .time-columns.compact {
1130
+ height: 192px;
1131
+ }
1132
+
1133
+ .time-column {
1134
+ flex: 1;
1135
+ overflow-y: auto;
1136
+ padding: 6px 0;
1137
+ border-right: 1px solid rgb(var(--v-theme-on-surface), 0.08);
1138
+ scrollbar-width: none;
1139
+ -ms-overflow-style: none;
1140
+ }
1141
+
1142
+ .time-column::-webkit-scrollbar {
1143
+ width: 0;
1144
+ height: 0;
1145
+ }
1146
+
1147
+ .time-column:last-child {
1148
+ border-right: none;
1149
+ }
1150
+
1151
+ .time-item {
1152
+ width: 100%;
1153
+ height: 28px;
1154
+ border: none;
1155
+ background: transparent;
1156
+ color: rgb(var(--v-theme-on-surface));
1157
+ cursor: pointer;
1158
+ font-size: 13px;
1159
+ transition: all 0.2s;
1160
+ }
1161
+
1162
+ .time-item:hover {
1163
+ background: rgb(var(--v-theme-on-surface), 0.08);
1164
+ }
1165
+
1166
+ .time-item.selected {
1167
+ color: rgb(var(--v-theme-primary));
1168
+ font-weight: 600;
1169
+ background: rgb(var(--v-theme-primary), 0.1);
1170
+ }
1171
+
1172
+ .range-input-row {
1173
+ position: relative;
1174
+ display: flex;
1175
+ align-items: center;
1176
+ justify-content: center;
1177
+ flex-wrap: nowrap;
1178
+ gap: 8px;
1179
+ padding: 8px 12px;
1180
+ border-bottom: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1181
+ box-sizing: border-box;
1182
+ }
1183
+
1184
+ .single-input-row {
1185
+ position: relative;
1186
+ display: flex;
1187
+ align-items: center;
1188
+ justify-content: center;
1189
+ gap: 8px;
1190
+ padding: 9px 12px;
1191
+ border-bottom: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1192
+ box-sizing: border-box;
1193
+ }
1194
+
1195
+ .single-date-input,
1196
+ .single-time-input {
1197
+ flex: 0 0 auto;
1198
+ width: 150px;
1199
+ height: 32px;
1200
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.16);
1201
+ border-radius: 4px;
1202
+ background: rgb(var(--v-theme-surface));
1203
+ color: rgb(var(--v-theme-on-surface));
1204
+ font-size: 13px;
1205
+ padding: 0 12px;
1206
+ outline: none;
1207
+ }
1208
+
1209
+ .single-time-input {
1210
+ text-align: left;
1211
+ cursor: pointer;
1212
+ }
1213
+
1214
+ .single-time-input.active,
1215
+ .single-time-input:focus,
1216
+ .single-date-input:focus {
1217
+ border-color: rgb(var(--v-theme-primary));
1218
+ }
1219
+
1220
+ .single-calendar-panel {
1221
+ width: 360px;
1222
+ }
1223
+
1224
+ .range-date-input,
1225
+ .range-time-input {
1226
+ flex: 0 0 auto;
1227
+ height: 32px;
1228
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.16);
1229
+ border-radius: 4px;
1230
+ background: rgb(var(--v-theme-surface));
1231
+ color: rgb(var(--v-theme-on-surface));
1232
+ font-size: 13px;
1233
+ padding: 0 12px;
1234
+ outline: none;
1235
+ }
1236
+
1237
+ .range-date-input {
1238
+ width: 144px;
1239
+ }
1240
+
1241
+ .range-time-input {
1242
+ width: 132px;
1243
+ text-align: left;
1244
+ cursor: pointer;
1245
+ }
1246
+
1247
+ .range-time-input.active,
1248
+ .range-time-input:focus,
1249
+ .range-date-input:focus {
1250
+ border-color: rgb(var(--v-theme-primary));
1251
+ }
1252
+
1253
+ .range-arrow {
1254
+ flex: 0 0 16px;
1255
+ width: 16px;
1256
+ text-align: center;
1257
+ color: rgb(var(--v-theme-on-surface), 0.6);
1258
+ font-size: 22px;
1259
+ }
1260
+
1261
+ .range-time-popover {
1262
+ position: absolute;
1263
+ top: 45px;
1264
+ width: 180px;
1265
+ z-index: 2;
1266
+ background: rgb(var(--v-theme-surface));
1267
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1268
+ border-radius: 4px;
1269
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1270
+ }
1271
+
1272
+ .single-time-popover {
1273
+ position: absolute;
1274
+ top: 45px;
1275
+ left: 156px;
1276
+ width: 180px;
1277
+ z-index: 2;
1278
+ background: rgb(var(--v-theme-surface));
1279
+ border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1280
+ border-radius: 4px;
1281
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1282
+ }
1283
+
1284
+ .range-time-popover.start {
1285
+ left: 162px;
1286
+ }
1287
+
1288
+ .range-time-popover.end {
1289
+ left: 430px;
1290
+ }
1291
+
1292
+ .time-popover-footer {
1293
+ display: flex;
1294
+ justify-content: flex-end;
1295
+ gap: 8px;
1296
+ padding: 8px 10px;
1297
+ border-top: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1298
+ }
1299
+
1300
+ .selected-display {
1301
+ padding: 10px 16px;
1302
+ border-top: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1303
+ text-align: center;
1304
+ }
1305
+
1306
+ .selected-text {
1307
+ font-size: 14px;
1308
+ color: rgb(var(--v-theme-on-surface));
1309
+ }
1310
+
1311
+ .placeholder-text,
1312
+ .inline-message {
1313
+ font-size: 14px;
1314
+ color: rgb(var(--v-theme-on-surface), 0.38);
1315
+ }
1316
+
1317
+ .inline-message {
1318
+ padding: 16px;
1319
+ }
1320
+
1321
+ .picker-footer {
1322
+ display: flex;
1323
+ align-items: center;
1324
+ gap: 8px;
1325
+ padding: 8px 10px;
1326
+ border-top: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1327
+ }
1328
+
1329
+ .footer-spacer {
1330
+ flex: 1;
1331
+ }
1332
+
1333
+ .btn {
1334
+ padding: 8px 16px;
1335
+ border-radius: 4px;
1336
+ font-size: 14px;
1337
+ cursor: pointer;
1338
+ transition: all 0.2s;
1339
+ border: none;
1340
+ outline: none;
1341
+ }
1342
+
1343
+ .btn.small {
1344
+ padding: 4px 8px;
1345
+ font-size: 12px;
1346
+ }
1347
+
1348
+ .btn:disabled {
1349
+ cursor: not-allowed;
1350
+ opacity: 0.5;
1351
+ }
1352
+
1353
+ .btn-text {
1354
+ background: none;
1355
+ color: rgb(var(--v-theme-primary));
1356
+ }
1357
+
1358
+ .btn-text:hover:not(:disabled) {
1359
+ background: rgb(var(--v-theme-on-surface), 0.08);
1360
+ }
1361
+
1362
+ .btn-primary {
1363
+ background: rgb(var(--v-theme-primary));
1364
+ color: rgb(var(--v-theme-on-primary));
1365
+ }
1366
+
1367
+ .btn-primary:hover:not(:disabled) {
1368
+ opacity: 0.9;
1369
+ }
1370
+ </style>