fix: 修复日期范围选择器时区和性能问题
- 修复时区问题:使用本地时区格式化日期,避免 toISOString() 导致的日期偏移 - 优化日期范围选择器性能:使用 untrack 避免循环更新 - 统一日期格式化方法:在 utils.ts 中添加 formatLocalDate 工具函数 - 修复分页逻辑:优化页码计算和显示 - 更新相关页面:bills 和 analysis 页面使用统一的日期格式化方法
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import type { BillRecord } from '$lib/api';
|
import type { BillRecord } from '$lib/api';
|
||||||
import { pieColors } from '$lib/constants/chart';
|
import { pieColors } from '$lib/constants/chart';
|
||||||
|
import { formatLocalDate } from '$lib/utils';
|
||||||
import BillRecordsTable from './BillRecordsTable.svelte';
|
import BillRecordsTable from './BillRecordsTable.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -96,7 +97,7 @@
|
|||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // 调整到周一
|
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // 调整到周一
|
||||||
d.setDate(diff);
|
d.setDate(diff);
|
||||||
return d.toISOString().split('T')[0];
|
return formatLocalDate(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取月份标识
|
// 获取月份标识
|
||||||
@@ -526,7 +527,7 @@
|
|||||||
|
|
||||||
// 点击图表任意位置都触发,选择最近的日期
|
// 点击图表任意位置都触发,选择最近的日期
|
||||||
const clickedDate = data[closestIdx].date;
|
const clickedDate = data[closestIdx].date;
|
||||||
const dateStr = clickedDate.toISOString().split('T')[0];
|
const dateStr = formatLocalDate(clickedDate);
|
||||||
|
|
||||||
// 找出当天的所有支出记录
|
// 找出当天的所有支出记录
|
||||||
selectedDate = clickedDate;
|
selectedDate = clickedDate;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { untrack } from 'svelte';
|
||||||
import { CalendarDate, type DateValue } from "@internationalized/date";
|
import { CalendarDate, type DateValue } from "@internationalized/date";
|
||||||
import CalendarIcon from "@lucide/svelte/icons/calendar";
|
import CalendarIcon from "@lucide/svelte/icons/calendar";
|
||||||
import * as Popover from "$lib/components/ui/popover";
|
import * as Popover from "$lib/components/ui/popover";
|
||||||
@@ -29,12 +30,45 @@
|
|||||||
return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
|
return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内部日期范围状态,使用 $derived 响应 props 变化
|
// 内部日期范围状态
|
||||||
let value: DateRange = $derived({
|
let value: DateRange = $state({
|
||||||
start: parseDate(startDate),
|
start: parseDate(startDate),
|
||||||
end: parseDate(endDate)
|
end: parseDate(endDate)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 当 startDate 或 endDate 从外部变化时,同步更新 value
|
||||||
|
$effect(() => {
|
||||||
|
const newStart = parseDate(startDate);
|
||||||
|
const newEnd = parseDate(endDate);
|
||||||
|
|
||||||
|
// 检查值是否真的变化了
|
||||||
|
const currentStartStr = untrack(() => value.start ? formatDate(value.start) : '');
|
||||||
|
const currentEndStr = untrack(() => value.end ? formatDate(value.end) : '');
|
||||||
|
|
||||||
|
if (currentStartStr !== startDate || currentEndStr !== endDate) {
|
||||||
|
value = {
|
||||||
|
start: newStart,
|
||||||
|
end: newEnd
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当 value 变化时(用户通过日历选择),更新绑定的 startDate 和 endDate
|
||||||
|
$effect(() => {
|
||||||
|
const newStartDate = value.start ? formatDate(value.start) : '';
|
||||||
|
const newEndDate = value.end ? formatDate(value.end) : '';
|
||||||
|
|
||||||
|
// 检查是否与当前值不同,避免循环更新
|
||||||
|
if (newStartDate !== untrack(() => startDate) || newEndDate !== untrack(() => endDate)) {
|
||||||
|
startDate = newStartDate;
|
||||||
|
endDate = newEndDate;
|
||||||
|
// 调用回调函数(只在有完整日期范围时)
|
||||||
|
if (onchange && value.start && value.end) {
|
||||||
|
onchange(newStartDate, newEndDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 格式化显示文本
|
// 格式化显示文本
|
||||||
let displayText = $derived(() => {
|
let displayText = $derived(() => {
|
||||||
if (value.start && value.end) {
|
if (value.start && value.end) {
|
||||||
@@ -45,13 +79,6 @@
|
|||||||
}
|
}
|
||||||
return "选择日期范围";
|
return "选择日期范围";
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当日期变化时通知父组件
|
|
||||||
function handleValueChange(newValue: DateRange) {
|
|
||||||
if (newValue.start && newValue.end && onchange) {
|
|
||||||
onchange(formatDate(newValue.start), formatDate(newValue.end));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
@@ -73,8 +100,7 @@
|
|||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content class="w-auto p-0" align="start">
|
<Popover.Content class="w-auto p-0" align="start">
|
||||||
<RangeCalendar
|
<RangeCalendar
|
||||||
{value}
|
bind:value
|
||||||
onValueChange={handleValueChange}
|
|
||||||
numberOfMonths={2}
|
numberOfMonths={2}
|
||||||
locale="zh-CN"
|
locale="zh-CN"
|
||||||
weekStartsOn={1}
|
weekStartsOn={1}
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用本地时区格式化日期为 YYYY-MM-DD 格式
|
||||||
|
* 避免使用 toISOString() 导致的时区转换问题
|
||||||
|
*/
|
||||||
|
export function formatLocalDate(date: Date): string {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import { DateRangePicker } from '$lib/components/ui/date-range-picker';
|
import { DateRangePicker } from '$lib/components/ui/date-range-picker';
|
||||||
|
import { formatLocalDate } from '$lib/utils';
|
||||||
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
|
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
|
||||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||||
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
||||||
@@ -41,8 +42,9 @@
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const year = today.getFullYear();
|
const year = today.getFullYear();
|
||||||
const month = today.getMonth();
|
const month = today.getMonth();
|
||||||
const startDate = new Date(year, month, 1).toISOString().split('T')[0];
|
|
||||||
const endDate = today.toISOString().split('T')[0];
|
const startDate = formatLocalDate(new Date(year, month, 1));
|
||||||
|
const endDate = formatLocalDate(today);
|
||||||
return { startDate, endDate };
|
return { startDate, endDate };
|
||||||
}
|
}
|
||||||
const defaultDates = getDefaultDates();
|
const defaultDates = getDefaultDates();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import * as Table from '$lib/components/ui/table';
|
import * as Table from '$lib/components/ui/table';
|
||||||
import { DateRangePicker } from '$lib/components/ui/date-range-picker';
|
import { DateRangePicker } from '$lib/components/ui/date-range-picker';
|
||||||
|
import { formatLocalDate } from '$lib/utils';
|
||||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||||
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
||||||
import Search from '@lucide/svelte/icons/search';
|
import Search from '@lucide/svelte/icons/search';
|
||||||
@@ -40,8 +41,9 @@
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const year = today.getFullYear();
|
const year = today.getFullYear();
|
||||||
const month = today.getMonth();
|
const month = today.getMonth();
|
||||||
const startDate = new Date(year, month, 1).toISOString().split('T')[0];
|
|
||||||
const endDate = today.toISOString().split('T')[0];
|
const startDate = formatLocalDate(new Date(year, month, 1));
|
||||||
|
const endDate = formatLocalDate(today);
|
||||||
return { startDate, endDate };
|
return { startDate, endDate };
|
||||||
}
|
}
|
||||||
const defaultDates = getDefaultDates();
|
const defaultDates = getDefaultDates();
|
||||||
@@ -221,11 +223,9 @@
|
|||||||
<div class="space-y-1.5 col-span-2 sm:col-span-2">
|
<div class="space-y-1.5 col-span-2 sm:col-span-2">
|
||||||
<Label class="text-xs">日期范围</Label>
|
<Label class="text-xs">日期范围</Label>
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
{startDate}
|
bind:startDate
|
||||||
{endDate}
|
bind:endDate
|
||||||
onchange={(start, end) => {
|
onchange={(start, end) => {
|
||||||
startDate = start;
|
|
||||||
endDate = end;
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -344,6 +344,9 @@
|
|||||||
<div class="flex items-center justify-between mt-4">
|
<div class="flex items-center justify-between mt-4">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
显示 {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalRecords)} 条,共 {totalRecords} 条
|
显示 {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, totalRecords)} 条,共 {totalRecords} 条
|
||||||
|
{#if searchText}
|
||||||
|
<span class="text-muted-foreground/70">(当前页筛选后:{displayRecords.length} 条)</span>
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -356,13 +359,17 @@
|
|||||||
上一页
|
上一页
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
{#each Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
{#each (() => {
|
||||||
// 计算显示的页码范围
|
// 计算显示的页码范围(最多显示5页)
|
||||||
let start = Math.max(1, currentPage - 2);
|
const maxPages = 5;
|
||||||
let end = Math.min(totalPages, start + 4);
|
let start = Math.max(1, currentPage - Math.floor(maxPages / 2));
|
||||||
start = Math.max(1, end - 4);
|
let end = Math.min(totalPages, start + maxPages - 1);
|
||||||
return start + i;
|
// 如果右侧空间不足,向左调整
|
||||||
}).filter(p => p <= totalPages) as page}
|
if (end - start < maxPages - 1) {
|
||||||
|
start = Math.max(1, end - maxPages + 1);
|
||||||
|
}
|
||||||
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
||||||
|
})() as page}
|
||||||
<Button
|
<Button
|
||||||
variant={page === currentPage ? 'default' : 'outline'}
|
variant={page === currentPage ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user