✨ 优化日期选择和提示圆角
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
v-for="alert in alerts"
|
v-for="alert in alerts"
|
||||||
:key="alert.id"
|
:key="alert.id"
|
||||||
:class="[
|
:class="[
|
||||||
'w-full rounded-lg shadow-xl overflow-hidden',
|
'w-full rounded-[30px] shadow-xl overflow-hidden',
|
||||||
'bg-gradient-to-r',
|
'bg-gradient-to-r',
|
||||||
gradientClasses[alert.type]
|
gradientClasses[alert.type]
|
||||||
]"
|
]"
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grid w-full grid-cols-3 gap-2 sm:w-auto">
|
||||||
|
<select
|
||||||
|
:value="parts.year"
|
||||||
|
class="h-12 min-w-0 rounded-[30px] border px-3 text-sm"
|
||||||
|
:class="fieldClass"
|
||||||
|
:aria-label="`${ariaLabel}年`"
|
||||||
|
@change="updatePart('year', ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option v-for="year in years" :key="year" :value="year">{{ year }}年</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
:value="parts.month"
|
||||||
|
class="h-12 min-w-0 rounded-[30px] border px-3 text-sm"
|
||||||
|
:class="fieldClass"
|
||||||
|
:aria-label="`${ariaLabel}月`"
|
||||||
|
@change="updatePart('month', ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option v-for="month in months" :key="month" :value="month">
|
||||||
|
{{ month.toString().padStart(2, '0') }}月
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
:value="parts.day"
|
||||||
|
class="h-12 min-w-0 rounded-[30px] border px-3 text-sm"
|
||||||
|
:class="fieldClass"
|
||||||
|
:aria-label="`${ariaLabel}日`"
|
||||||
|
@change="updatePart('day', ($event.target as HTMLSelectElement).value)"
|
||||||
|
>
|
||||||
|
<option v-for="day in days" :key="day" :value="day">
|
||||||
|
{{ day.toString().padStart(2, '0') }}日
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
fieldClass?: string | string[]
|
||||||
|
ariaLabel?: string
|
||||||
|
minYear?: number
|
||||||
|
maxYear?: number
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
fieldClass: '',
|
||||||
|
ariaLabel: '日期',
|
||||||
|
minYear: 1900,
|
||||||
|
maxYear: 2100
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
change: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type DatePart = 'year' | 'month' | 'day'
|
||||||
|
|
||||||
|
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 getDaysInMonth = (year: number, month: number) => new Date(year, month, 0).getDate()
|
||||||
|
|
||||||
|
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 parts = computed(() => {
|
||||||
|
const [rawYear, rawMonth, rawDay] = props.modelValue.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(
|
||||||
|
Number.isFinite(rawDay) ? rawDay : new Date().getDate(),
|
||||||
|
1,
|
||||||
|
getDaysInMonth(year, month)
|
||||||
|
)
|
||||||
|
return { year, month, day }
|
||||||
|
})
|
||||||
|
|
||||||
|
const days = computed(() =>
|
||||||
|
Array.from(
|
||||||
|
{ length: getDaysInMonth(parts.value.year, parts.value.month) },
|
||||||
|
(_, index) => index + 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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)}`)
|
||||||
|
emit('change')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
class="mb-6 rounded-lg border px-4 py-3 text-sm"
|
class="mb-6 rounded-[30px] border px-4 py-3 text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'border-red-900/50 bg-red-950/30 text-red-200'
|
? 'border-red-900/50 bg-red-950/30 text-red-200'
|
||||||
@@ -246,56 +246,52 @@
|
|||||||
{{ analyticsSummaryText }}
|
{{ analyticsSummaryText }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="w-full max-w-full space-y-3 lg:w-auto">
|
||||||
<input
|
<div class="grid gap-2 md:grid-cols-2">
|
||||||
v-model="analyticsStart"
|
<NativeDateSelect
|
||||||
type="date"
|
v-model="analyticsStart"
|
||||||
min="1900-01-01"
|
:field-class="fieldClass"
|
||||||
max="2100-12-31"
|
aria-label="开始日期"
|
||||||
class="native-date-input h-12 min-w-0 flex-1 cursor-pointer rounded-xl border px-3 text-sm sm:flex-none sm:min-w-40"
|
@change="fetchAnalyticsData"
|
||||||
:class="[fieldClass]"
|
/>
|
||||||
@click="openNativeDatePicker"
|
<NativeDateSelect
|
||||||
@change="fetchAnalyticsData"
|
v-model="analyticsEnd"
|
||||||
/>
|
:field-class="fieldClass"
|
||||||
<input
|
aria-label="结束日期"
|
||||||
v-model="analyticsEnd"
|
@change="fetchAnalyticsData"
|
||||||
type="date"
|
/>
|
||||||
min="1900-01-01"
|
</div>
|
||||||
max="2100-12-31"
|
<div class="grid grid-cols-3 gap-2 sm:flex sm:justify-end">
|
||||||
class="native-date-input h-12 min-w-0 flex-1 cursor-pointer rounded-xl border px-3 text-sm sm:flex-none sm:min-w-40"
|
<button
|
||||||
:class="[fieldClass]"
|
type="button"
|
||||||
@click="openNativeDatePicker"
|
class="h-11 rounded-[30px] border px-4 text-sm font-medium transition-colors"
|
||||||
@change="fetchAnalyticsData"
|
:class="detailActionClass"
|
||||||
/>
|
@click="trendCanvas?.zoom(0.75)"
|
||||||
<button
|
>
|
||||||
type="button"
|
放大
|
||||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
</button>
|
||||||
:class="detailActionClass"
|
<button
|
||||||
@click="trendCanvas?.zoom(0.75)"
|
type="button"
|
||||||
>
|
class="h-11 rounded-[30px] border px-4 text-sm font-medium transition-colors"
|
||||||
放大
|
:class="detailActionClass"
|
||||||
</button>
|
@click="trendCanvas?.zoom(1.33)"
|
||||||
<button
|
>
|
||||||
type="button"
|
缩小
|
||||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
</button>
|
||||||
:class="detailActionClass"
|
<button
|
||||||
@click="trendCanvas?.zoom(1.33)"
|
type="button"
|
||||||
>
|
class="h-11 rounded-[30px] border px-4 text-sm font-medium transition-colors"
|
||||||
缩小
|
:class="detailActionClass"
|
||||||
</button>
|
@click="trendCanvas?.resetWindow()"
|
||||||
<button
|
>
|
||||||
type="button"
|
重置
|
||||||
class="h-11 rounded-lg border px-3 text-sm font-medium transition-colors"
|
</button>
|
||||||
:class="detailActionClass"
|
</div>
|
||||||
@click="trendCanvas?.resetWindow()"
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="analyticsError"
|
v-if="analyticsError"
|
||||||
class="mb-4 rounded-lg border px-4 py-3 text-sm"
|
class="mb-4 rounded-[30px] border px-4 py-3 text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'border-red-900/50 bg-red-950/30 text-red-200'
|
? 'border-red-900/50 bg-red-950/30 text-red-200'
|
||||||
@@ -434,6 +430,7 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import StatCard from '@/components/common/StatCard.vue'
|
import StatCard from '@/components/common/StatCard.vue'
|
||||||
import SmoothTrendCanvas from '@/components/common/SmoothTrendCanvas.vue'
|
import SmoothTrendCanvas from '@/components/common/SmoothTrendCanvas.vue'
|
||||||
|
import NativeDateSelect from '@/components/common/NativeDateSelect.vue'
|
||||||
import { useDashboardStats, useInjectedDarkMode, useTransferAnalytics } from '@/composables'
|
import { useDashboardStats, useInjectedDarkMode, useTransferAnalytics } from '@/composables'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ROUTES } from '@/constants'
|
import { ROUTES } from '@/constants'
|
||||||
@@ -495,10 +492,6 @@ const daysAgo = (days: number) => {
|
|||||||
date.setDate(date.getDate() - days)
|
date.setDate(date.getDate() - days)
|
||||||
return date.toISOString().slice(0, 10)
|
return date.toISOString().slice(0, 10)
|
||||||
}
|
}
|
||||||
const openNativeDatePicker = (event: MouseEvent) => {
|
|
||||||
const input = event.currentTarget as HTMLInputElement & { showPicker?: () => void }
|
|
||||||
input.showPicker?.()
|
|
||||||
}
|
|
||||||
const trendCanvas = ref<InstanceType<typeof SmoothTrendCanvas> | null>(null)
|
const trendCanvas = ref<InstanceType<typeof SmoothTrendCanvas> | null>(null)
|
||||||
const analyticsStart = ref(daysAgo(120))
|
const analyticsStart = ref(daysAgo(120))
|
||||||
const analyticsEnd = ref(today())
|
const analyticsEnd = ref(today())
|
||||||
|
|||||||
@@ -1204,7 +1204,7 @@
|
|||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border px-4 py-3 text-sm"
|
class="rounded-[30px] border px-4 py-3 text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'border-indigo-500/20 bg-indigo-500/10 text-indigo-200'
|
? 'border-indigo-500/20 bg-indigo-500/10 text-indigo-200'
|
||||||
@@ -1249,7 +1249,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="rounded-lg border px-4 py-3 text-sm"
|
class="rounded-[30px] border px-4 py-3 text-sm"
|
||||||
:class="[
|
:class="[
|
||||||
isDarkMode
|
isDarkMode
|
||||||
? 'border-gray-700 bg-gray-700/50 text-gray-300'
|
? 'border-gray-700 bg-gray-700/50 text-gray-300'
|
||||||
|
|||||||
Reference in New Issue
Block a user