Files
billai/web/src/lib/components/ui/date-range-picker/date-range-picker.svelte

114 lines
3.5 KiB
Svelte

<script lang="ts">
import { untrack } from 'svelte';
import { CalendarDate, type DateValue } from "@internationalized/date";
import CalendarIcon from "@lucide/svelte/icons/calendar";
import * as Popover from "$lib/components/ui/popover";
import { RangeCalendar } from "$lib/components/ui/range-calendar";
import { Button } from "$lib/components/ui/button";
import { cn } from "$lib/utils";
import type { DateRange } from "bits-ui";
interface Props {
startDate?: string;
endDate?: string;
onchange?: (start: string, end: string) => void;
class?: string;
}
let { startDate = $bindable(), endDate = $bindable(), onchange, class: className }: Props = $props();
// 将 YYYY-MM-DD 字符串转换为 CalendarDate
function parseDate(dateStr?: string): DateValue | undefined {
if (!dateStr) return undefined;
const [year, month, day] = dateStr.split('-').map(Number);
return new CalendarDate(year, month, day);
}
// 将 CalendarDate 转换为 YYYY-MM-DD 字符串
function formatDate(date: DateValue | undefined): string {
if (!date) return '';
return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`;
}
// 内部日期范围状态
let value: DateRange = $state({
start: parseDate(startDate),
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(() => {
if (value.start && value.end) {
return `${formatDate(value.start)} ~ ${formatDate(value.end)}`;
}
if (value.start) {
return `${formatDate(value.start)} ~ `;
}
return "选择日期范围";
});
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
variant="outline"
class={cn(
"w-[260px] justify-start text-left font-normal",
!value.start && "text-muted-foreground",
className
)}
{...props}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{displayText()}
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start">
<RangeCalendar
bind:value
class="rounded-md border"
numberOfMonths={2}
pagedNavigation={true}
fixedWeeks={true}
weekdayFormat="short"
locale="zh-CN"
weekStartsOn={1}
/>
</Popover.Content>
</Popover.Root>