🧹 清理文件管理日期控件

This commit is contained in:
2026-06-05 15:11:44 +08:00
parent 63ef49195a
commit a66a0b6a77
3 changed files with 104 additions and 55 deletions
+2 -23
View File
@@ -22,17 +22,13 @@
<input
:type="type"
:value="modelValue ?? ''"
:min="inputMin"
:max="inputMax"
class="block w-full rounded-lg border-0 py-2.5 pl-4 pr-10 transition-all duration-200 focus:ring-2 focus:ring-inset sm:text-sm"
:class="[
isNativeDateField ? 'native-date-input min-h-12 cursor-pointer' : '',
isDarkMode
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
]"
:placeholder="placeholder"
@click="openNativePicker"
@input="handleInput"
/>
<div
@@ -45,7 +41,7 @@
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
import { inject } from 'vue'
import { CheckIcon } from 'lucide-vue-next'
const props = withDefaults(
@@ -53,7 +49,7 @@ const props = withDefaults(
label: string
modelValue: string | number | null
placeholder?: string
type?: 'text' | 'number' | 'date' | 'datetime-local'
type?: 'text' | 'number'
}>(),
{
placeholder: '',
@@ -66,23 +62,6 @@ const emit = defineEmits<{
}>()
const isDarkMode = inject('isDarkMode')
const isNativeDateField = computed(() => props.type === 'date' || props.type === 'datetime-local')
const inputMin = computed(() => {
if (props.type === 'date') return '1900-01-01'
if (props.type === 'datetime-local') return '1900-01-01T00:00'
return undefined
})
const inputMax = computed(() => {
if (props.type === 'date') return '2100-12-31'
if (props.type === 'datetime-local') return '2100-12-31T23:59'
return undefined
})
const openNativePicker = (event: MouseEvent) => {
if (!isNativeDateField.value) return
const input = event.currentTarget as HTMLInputElement & { showPicker?: () => void }
input.showPicker?.()
}
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
+49 -7
View File
@@ -1,5 +1,5 @@
<template>
<div class="native-date-strip" :aria-label="ariaLabel">
<div class="native-date-strip" :class="fieldClass" :aria-label="ariaLabel">
<select
:value="parts.year"
class="native-date-part native-date-year"
@@ -30,6 +30,30 @@
{{ day.toString().padStart(2, '0') }}
</option>
</select>
<template v-if="includeTime">
<span class="native-date-time-spacer" />
<select
:value="parts.hour"
class="native-date-part native-date-time"
:aria-label="`${ariaLabel}时`"
@change="updatePart('hour', ($event.target as HTMLSelectElement).value)"
>
<option v-for="hour in hours" :key="hour" :value="hour">
{{ hour.toString().padStart(2, '0') }}
</option>
</select>
<span class="native-date-separator">:</span>
<select
:value="parts.minute"
class="native-date-part native-date-time"
:aria-label="`${ariaLabel}分`"
@change="updatePart('minute', ($event.target as HTMLSelectElement).value)"
>
<option v-for="minute in minutes" :key="minute" :value="minute">
{{ minute.toString().padStart(2, '0') }}
</option>
</select>
</template>
</div>
</template>
@@ -43,12 +67,14 @@ const props = withDefaults(
ariaLabel?: string
minYear?: number
maxYear?: number
includeTime?: boolean
}>(),
{
fieldClass: '',
ariaLabel: '日期',
minYear: 1900,
maxYear: 2100
maxYear: 2100,
includeTime: false
}
)
@@ -57,7 +83,7 @@ const emit = defineEmits<{
change: []
}>()
type DatePart = 'year' | 'month' | 'day'
type DatePart = 'year' | 'month' | 'day' | 'hour' | 'minute'
const pad = (value: number) => String(value).padStart(2, '0')
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value))
@@ -67,9 +93,13 @@ const years = computed(() =>
Array.from({ length: props.maxYear - props.minYear + 1 }, (_, index) => props.minYear + index)
)
const months = Array.from({ length: 12 }, (_, index) => index + 1)
const hours = Array.from({ length: 24 }, (_, index) => index)
const minutes = Array.from({ length: 60 }, (_, index) => index)
const parts = computed(() => {
const [rawYear, rawMonth, rawDay] = props.modelValue.split('-').map(Number)
const [rawDate = '', rawTime = ''] = props.modelValue.split('T')
const [rawYear, rawMonth, rawDay] = rawDate.split('-').map(Number)
const [rawHour, rawMinute] = rawTime.split(':').map(Number)
const year = clamp(Number.isFinite(rawYear) ? rawYear : new Date().getFullYear(), props.minYear, props.maxYear)
const month = clamp(Number.isFinite(rawMonth) ? rawMonth : new Date().getMonth() + 1, 1, 12)
const day = clamp(
@@ -77,7 +107,9 @@ const parts = computed(() => {
1,
getDaysInMonth(year, month)
)
return { year, month, day }
const hour = clamp(Number.isFinite(rawHour) ? rawHour : 0, 0, 23)
const minute = clamp(Number.isFinite(rawMinute) ? rawMinute : 0, 0, 59)
return { year, month, day, hour, minute }
})
const days = computed(() =>
@@ -90,7 +122,11 @@ const days = computed(() =>
const updatePart = (part: DatePart, value: string) => {
const next = { ...parts.value, [part]: Number(value) }
next.day = clamp(next.day, 1, getDaysInMonth(next.year, next.month))
emit('update:modelValue', `${next.year}-${pad(next.month)}-${pad(next.day)}`)
const nextDate = `${next.year}-${pad(next.month)}-${pad(next.day)}`
const nextValue = props.includeTime
? `${nextDate}T${pad(next.hour)}:${pad(next.minute)}`
: nextDate
emit('update:modelValue', nextValue)
emit('change')
}
</script>
@@ -157,10 +193,16 @@ const updatePart = (part: DatePart, value: string) => {
}
.native-date-month,
.native-date-day {
.native-date-day,
.native-date-time {
width: 2.2ch;
}
.native-date-time-spacer {
width: 0.9ch;
flex: 0 0 auto;
}
.native-date-separator {
margin-inline: 0.35ch;
color: #789096;