🧹 清理文件管理日期控件

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;
+53 -25
View File
@@ -434,17 +434,9 @@
<span
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
:class="getInsightBadgeClass(file.statusInsightSeverity)"
:title="file.displayHealthAction"
>
{{ file.displayHealthState }}
</span>
<span
class="max-w-[16rem] truncate text-xs"
:class="[mutedTextClass]"
:title="file.displayHealthAction"
>
{{ file.displayHealthAction }}
</span>
</div>
</div>
</div>
@@ -949,10 +941,6 @@
<h4 class="mt-1 text-base font-semibold" :class="[primaryTextClass]">
{{ getInsightStateLabel(selectedFileDetail.statusInsightState) }}
</h4>
<p class="mt-1 text-sm" :class="[mutedTextClass]">
{{ t('fileManage.nextAction') }}:
{{ getInsightActionLabel(selectedFileDetail.statusInsightNextAction) }}
</p>
</div>
<span
class="inline-flex w-fit items-center rounded-full px-2.5 py-1 text-xs font-medium"
@@ -1159,11 +1147,32 @@
:label="t('fileManage.form.suffix')"
:placeholder="t('fileManage.form.suffixPlaceholder')"
/>
<FileEditField
v-model="editForm.expired_at"
type="datetime-local"
:label="t('send.expiration.label')"
/>
<div class="space-y-2 group">
<label
class="flex items-center space-x-2 text-sm font-medium transition-colors duration-200"
:class="[
isDarkMode
? 'text-gray-300 group-focus-within:text-indigo-400'
: 'text-gray-700 group-focus-within:text-indigo-600'
]"
>
<span>{{ t('send.expiration.label') }}</span>
<div
class="h-px flex-1 transition-colors duration-200"
:class="[
isDarkMode
? 'bg-gray-700 group-focus-within:bg-indigo-500/50'
: 'bg-gray-200 group-focus-within:bg-indigo-500/30'
]"
></div>
</label>
<NativeDateSelect
v-model="editForm.expired_at"
include-time
:field-class="fieldClass"
:aria-label="t('send.expiration.label')"
/>
</div>
<FileEditField
v-model="editForm.expired_count"
type="number"
@@ -1235,12 +1244,32 @@
</div>
</div>
<FileEditField
v-if="batchEditForm.mode === 'expiresAt'"
v-model="batchEditForm.expired_at"
type="datetime-local"
:label="t('fileManage.batchEditExpiresAt')"
/>
<div v-if="batchEditForm.mode === 'expiresAt'" class="space-y-2 group">
<label
class="flex items-center space-x-2 text-sm font-medium transition-colors duration-200"
:class="[
isDarkMode
? 'text-gray-300 group-focus-within:text-indigo-400'
: 'text-gray-700 group-focus-within:text-indigo-600'
]"
>
<span>{{ t('fileManage.batchEditExpiresAt') }}</span>
<div
class="h-px flex-1 transition-colors duration-200"
:class="[
isDarkMode
? 'bg-gray-700 group-focus-within:bg-indigo-500/50'
: 'bg-gray-200 group-focus-within:bg-indigo-500/30'
]"
></div>
</label>
<NativeDateSelect
v-model="batchEditForm.expired_at"
include-time
:field-class="fieldClass"
:aria-label="t('fileManage.batchEditExpiresAt')"
/>
</div>
<div v-else-if="batchEditForm.mode === 'relativeExpire'" class="grid gap-3 sm:grid-cols-[1fr_10rem]">
<FileEditField
v-model="batchEditForm.relative_expire_value"
@@ -1421,6 +1450,7 @@ import {
import DataTable from '@/components/common/DataTable.vue'
import DataPagination from '@/components/common/DataPagination.vue'
import FileEditField from '@/components/common/FileEditField.vue'
import NativeDateSelect from '@/components/common/NativeDateSelect.vue'
import BaseModal from '@/components/common/BaseModal.vue'
import BaseButton from '@/components/common/BaseButton.vue'
import { useAdminFiles, useInjectedDarkMode, useTransferAnalytics } from '@/composables'
@@ -1924,8 +1954,6 @@ const getInsightSeverityLabel = (severity: AdminFileInsightSeverity) =>
const getInsightStateLabel = (state: string) => t(`fileManage.insightStates.${state}`)
const getInsightActionLabel = (action: string) => t(`fileManage.insightActions.${action}`)
const getInsightPanelClass = (severity: AdminFileInsightSeverity) => {
const darkClasses: Record<AdminFileInsightSeverity, string> = {
success: 'border-emerald-500/20 bg-emerald-500/10',