114 lines
3.5 KiB
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>
|