🧹 清理文件管理日期控件
This commit is contained in:
@@ -22,17 +22,13 @@
|
|||||||
<input
|
<input
|
||||||
:type="type"
|
:type="type"
|
||||||
:value="modelValue ?? ''"
|
: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="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="[
|
:class="[
|
||||||
isNativeDateField ? 'native-date-input min-h-12 cursor-pointer' : '',
|
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'bg-gray-700/50 text-white placeholder-gray-400 focus:ring-indigo-500/50'
|
? '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'
|
: 'bg-gray-50 text-gray-900 placeholder-gray-400 focus:ring-indigo-500'
|
||||||
]"
|
]"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@click="openNativePicker"
|
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -45,7 +41,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject } from 'vue'
|
import { inject } from 'vue'
|
||||||
import { CheckIcon } from 'lucide-vue-next'
|
import { CheckIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -53,7 +49,7 @@ const props = withDefaults(
|
|||||||
label: string
|
label: string
|
||||||
modelValue: string | number | null
|
modelValue: string | number | null
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
type?: 'text' | 'number' | 'date' | 'datetime-local'
|
type?: 'text' | 'number'
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
@@ -66,23 +62,6 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isDarkMode = inject('isDarkMode')
|
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 handleInput = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="native-date-strip" :aria-label="ariaLabel">
|
<div class="native-date-strip" :class="fieldClass" :aria-label="ariaLabel">
|
||||||
<select
|
<select
|
||||||
:value="parts.year"
|
:value="parts.year"
|
||||||
class="native-date-part native-date-year"
|
class="native-date-part native-date-year"
|
||||||
@@ -30,6 +30,30 @@
|
|||||||
{{ day.toString().padStart(2, '0') }}
|
{{ day.toString().padStart(2, '0') }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -43,12 +67,14 @@ const props = withDefaults(
|
|||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
minYear?: number
|
minYear?: number
|
||||||
maxYear?: number
|
maxYear?: number
|
||||||
|
includeTime?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
fieldClass: '',
|
fieldClass: '',
|
||||||
ariaLabel: '日期',
|
ariaLabel: '日期',
|
||||||
minYear: 1900,
|
minYear: 1900,
|
||||||
maxYear: 2100
|
maxYear: 2100,
|
||||||
|
includeTime: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,7 +83,7 @@ const emit = defineEmits<{
|
|||||||
change: []
|
change: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
type DatePart = 'year' | 'month' | 'day'
|
type DatePart = 'year' | 'month' | 'day' | 'hour' | 'minute'
|
||||||
|
|
||||||
const pad = (value: number) => String(value).padStart(2, '0')
|
const pad = (value: number) => String(value).padStart(2, '0')
|
||||||
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value))
|
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)
|
Array.from({ length: props.maxYear - props.minYear + 1 }, (_, index) => props.minYear + index)
|
||||||
)
|
)
|
||||||
const months = Array.from({ length: 12 }, (_, index) => index + 1)
|
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 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 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 month = clamp(Number.isFinite(rawMonth) ? rawMonth : new Date().getMonth() + 1, 1, 12)
|
||||||
const day = clamp(
|
const day = clamp(
|
||||||
@@ -77,7 +107,9 @@ const parts = computed(() => {
|
|||||||
1,
|
1,
|
||||||
getDaysInMonth(year, month)
|
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(() =>
|
const days = computed(() =>
|
||||||
@@ -90,7 +122,11 @@ const days = computed(() =>
|
|||||||
const updatePart = (part: DatePart, value: string) => {
|
const updatePart = (part: DatePart, value: string) => {
|
||||||
const next = { ...parts.value, [part]: Number(value) }
|
const next = { ...parts.value, [part]: Number(value) }
|
||||||
next.day = clamp(next.day, 1, getDaysInMonth(next.year, next.month))
|
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')
|
emit('change')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -157,10 +193,16 @@ const updatePart = (part: DatePart, value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.native-date-month,
|
.native-date-month,
|
||||||
.native-date-day {
|
.native-date-day,
|
||||||
|
.native-date-time {
|
||||||
width: 2.2ch;
|
width: 2.2ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.native-date-time-spacer {
|
||||||
|
width: 0.9ch;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.native-date-separator {
|
.native-date-separator {
|
||||||
margin-inline: 0.35ch;
|
margin-inline: 0.35ch;
|
||||||
color: #789096;
|
color: #789096;
|
||||||
|
|||||||
@@ -434,17 +434,9 @@
|
|||||||
<span
|
<span
|
||||||
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
class="inline-flex max-w-full items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
:class="getInsightBadgeClass(file.statusInsightSeverity)"
|
:class="getInsightBadgeClass(file.statusInsightSeverity)"
|
||||||
:title="file.displayHealthAction"
|
|
||||||
>
|
>
|
||||||
{{ file.displayHealthState }}
|
{{ file.displayHealthState }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
|
||||||
class="max-w-[16rem] truncate text-xs"
|
|
||||||
:class="[mutedTextClass]"
|
|
||||||
:title="file.displayHealthAction"
|
|
||||||
>
|
|
||||||
{{ file.displayHealthAction }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -949,10 +941,6 @@
|
|||||||
<h4 class="mt-1 text-base font-semibold" :class="[primaryTextClass]">
|
<h4 class="mt-1 text-base font-semibold" :class="[primaryTextClass]">
|
||||||
{{ getInsightStateLabel(selectedFileDetail.statusInsightState) }}
|
{{ getInsightStateLabel(selectedFileDetail.statusInsightState) }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="mt-1 text-sm" :class="[mutedTextClass]">
|
|
||||||
{{ t('fileManage.nextAction') }}:
|
|
||||||
{{ getInsightActionLabel(selectedFileDetail.statusInsightNextAction) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="inline-flex w-fit items-center rounded-full px-2.5 py-1 text-xs font-medium"
|
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')"
|
:label="t('fileManage.form.suffix')"
|
||||||
:placeholder="t('fileManage.form.suffixPlaceholder')"
|
:placeholder="t('fileManage.form.suffixPlaceholder')"
|
||||||
/>
|
/>
|
||||||
<FileEditField
|
<div class="space-y-2 group">
|
||||||
v-model="editForm.expired_at"
|
<label
|
||||||
type="datetime-local"
|
class="flex items-center space-x-2 text-sm font-medium transition-colors duration-200"
|
||||||
:label="t('send.expiration.label')"
|
: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
|
<FileEditField
|
||||||
v-model="editForm.expired_count"
|
v-model="editForm.expired_count"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -1235,12 +1244,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FileEditField
|
<div v-if="batchEditForm.mode === 'expiresAt'" class="space-y-2 group">
|
||||||
v-if="batchEditForm.mode === 'expiresAt'"
|
<label
|
||||||
v-model="batchEditForm.expired_at"
|
class="flex items-center space-x-2 text-sm font-medium transition-colors duration-200"
|
||||||
type="datetime-local"
|
:class="[
|
||||||
:label="t('fileManage.batchEditExpiresAt')"
|
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]">
|
<div v-else-if="batchEditForm.mode === 'relativeExpire'" class="grid gap-3 sm:grid-cols-[1fr_10rem]">
|
||||||
<FileEditField
|
<FileEditField
|
||||||
v-model="batchEditForm.relative_expire_value"
|
v-model="batchEditForm.relative_expire_value"
|
||||||
@@ -1421,6 +1450,7 @@ import {
|
|||||||
import DataTable from '@/components/common/DataTable.vue'
|
import DataTable from '@/components/common/DataTable.vue'
|
||||||
import DataPagination from '@/components/common/DataPagination.vue'
|
import DataPagination from '@/components/common/DataPagination.vue'
|
||||||
import FileEditField from '@/components/common/FileEditField.vue'
|
import FileEditField from '@/components/common/FileEditField.vue'
|
||||||
|
import NativeDateSelect from '@/components/common/NativeDateSelect.vue'
|
||||||
import BaseModal from '@/components/common/BaseModal.vue'
|
import BaseModal from '@/components/common/BaseModal.vue'
|
||||||
import BaseButton from '@/components/common/BaseButton.vue'
|
import BaseButton from '@/components/common/BaseButton.vue'
|
||||||
import { useAdminFiles, useInjectedDarkMode, useTransferAnalytics } from '@/composables'
|
import { useAdminFiles, useInjectedDarkMode, useTransferAnalytics } from '@/composables'
|
||||||
@@ -1924,8 +1954,6 @@ const getInsightSeverityLabel = (severity: AdminFileInsightSeverity) =>
|
|||||||
|
|
||||||
const getInsightStateLabel = (state: string) => t(`fileManage.insightStates.${state}`)
|
const getInsightStateLabel = (state: string) => t(`fileManage.insightStates.${state}`)
|
||||||
|
|
||||||
const getInsightActionLabel = (action: string) => t(`fileManage.insightActions.${action}`)
|
|
||||||
|
|
||||||
const getInsightPanelClass = (severity: AdminFileInsightSeverity) => {
|
const getInsightPanelClass = (severity: AdminFileInsightSeverity) => {
|
||||||
const darkClasses: Record<AdminFileInsightSeverity, string> = {
|
const darkClasses: Record<AdminFileInsightSeverity, string> = {
|
||||||
success: 'border-emerald-500/20 bg-emerald-500/10',
|
success: 'border-emerald-500/20 bg-emerald-500/10',
|
||||||
|
|||||||
Reference in New Issue
Block a user