2026-01-31 00:00:46 +08:00
|
|
|
<script lang="ts" setup>
|
2026-01-31 02:54:00 +08:00
|
|
|
import type { CalendarRootEmits, CalendarRootProps, DateValue } from "reka-ui";
|
|
|
|
|
import type { HTMLAttributes, Ref } from "vue";
|
|
|
|
|
import type { LayoutTypes } from ".";
|
|
|
|
|
import { getLocalTimeZone, today } from "@internationalized/date";
|
|
|
|
|
import { createReusableTemplate, reactiveOmit, useVModel } from "@vueuse/core";
|
|
|
|
|
import { CalendarRoot, useDateFormatter, useForwardPropsEmits } from "reka-ui";
|
|
|
|
|
import { createYear, createYearRange, toDate } from "reka-ui/date";
|
|
|
|
|
import { computed, toRaw } from "vue";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import {
|
|
|
|
|
NativeSelect,
|
|
|
|
|
NativeSelectOption,
|
|
|
|
|
} from "@/components/ui/native-select";
|
|
|
|
|
import {
|
|
|
|
|
CalendarCell,
|
|
|
|
|
CalendarCellTrigger,
|
|
|
|
|
CalendarGrid,
|
|
|
|
|
CalendarGridBody,
|
|
|
|
|
CalendarGridHead,
|
|
|
|
|
CalendarGridRow,
|
|
|
|
|
CalendarHeadCell,
|
|
|
|
|
CalendarHeader,
|
|
|
|
|
CalendarHeading,
|
|
|
|
|
CalendarNextButton,
|
|
|
|
|
CalendarPrevButton,
|
|
|
|
|
} from ".";
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
const props = withDefaults(
|
|
|
|
|
defineProps<
|
|
|
|
|
CalendarRootProps & {
|
|
|
|
|
class?: HTMLAttributes["class"];
|
|
|
|
|
layout?: LayoutTypes;
|
|
|
|
|
yearRange?: DateValue[];
|
|
|
|
|
}
|
|
|
|
|
>(),
|
|
|
|
|
{
|
|
|
|
|
modelValue: undefined,
|
|
|
|
|
layout: undefined,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
const emits = defineEmits<CalendarRootEmits>();
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder");
|
2026-01-31 00:00:46 +08:00
|
|
|
|
|
|
|
|
const placeholder = useVModel(props, "placeholder", emits, {
|
|
|
|
|
passive: true,
|
|
|
|
|
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
|
2026-01-31 02:54:00 +08:00
|
|
|
}) as Ref<DateValue>;
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
const formatter = useDateFormatter(props.locale ?? "en");
|
2026-01-31 00:00:46 +08:00
|
|
|
|
|
|
|
|
const yearRange = computed(() => {
|
2026-01-31 02:54:00 +08:00
|
|
|
return (
|
|
|
|
|
props.yearRange ??
|
|
|
|
|
createYearRange({
|
|
|
|
|
start:
|
|
|
|
|
props?.minValue ??
|
|
|
|
|
(
|
|
|
|
|
toRaw(props.placeholder) ??
|
|
|
|
|
props.defaultPlaceholder ??
|
|
|
|
|
today(getLocalTimeZone())
|
|
|
|
|
).cycle("year", -100),
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
end:
|
|
|
|
|
props?.maxValue ??
|
|
|
|
|
(
|
|
|
|
|
toRaw(props.placeholder) ??
|
|
|
|
|
props.defaultPlaceholder ??
|
|
|
|
|
today(getLocalTimeZone())
|
|
|
|
|
).cycle("year", 10),
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate<{
|
|
|
|
|
date: DateValue;
|
|
|
|
|
}>();
|
|
|
|
|
const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate<{
|
|
|
|
|
date: DateValue;
|
|
|
|
|
}>();
|
2026-01-31 00:00:46 +08:00
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
2026-01-31 00:00:46 +08:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<DefineMonthTemplate v-slot="{ date }">
|
|
|
|
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
|
|
|
|
<div class="relative">
|
2026-01-31 02:54:00 +08:00
|
|
|
<div
|
|
|
|
|
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
|
|
|
|
|
>
|
|
|
|
|
{{ formatter.custom(toDate(date), { month: "short" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
</div>
|
|
|
|
|
<NativeSelect
|
|
|
|
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
2026-01-31 02:54:00 +08:00
|
|
|
@change="
|
|
|
|
|
(e: Event) => {
|
|
|
|
|
placeholder = placeholder.set({
|
|
|
|
|
month: Number((e?.target as any)?.value),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
"
|
2026-01-31 00:00:46 +08:00
|
|
|
>
|
2026-01-31 02:54:00 +08:00
|
|
|
<NativeSelectOption
|
|
|
|
|
v-for="month in createYear({ dateObj: date })"
|
|
|
|
|
:key="month.toString()"
|
|
|
|
|
:value="month.month"
|
|
|
|
|
:selected="date.month === month.month"
|
|
|
|
|
>
|
|
|
|
|
{{ formatter.custom(toDate(month), { month: "short" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
</NativeSelectOption>
|
|
|
|
|
</NativeSelect>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DefineMonthTemplate>
|
|
|
|
|
|
|
|
|
|
<DefineYearTemplate v-slot="{ date }">
|
|
|
|
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
|
|
|
|
<div class="relative">
|
2026-01-31 02:54:00 +08:00
|
|
|
<div
|
|
|
|
|
class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none"
|
|
|
|
|
>
|
|
|
|
|
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
</div>
|
|
|
|
|
<NativeSelect
|
|
|
|
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
2026-01-31 02:54:00 +08:00
|
|
|
@change="
|
|
|
|
|
(e: Event) => {
|
|
|
|
|
placeholder = placeholder.set({
|
|
|
|
|
year: Number((e?.target as any)?.value),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
"
|
2026-01-31 00:00:46 +08:00
|
|
|
>
|
2026-01-31 02:54:00 +08:00
|
|
|
<NativeSelectOption
|
|
|
|
|
v-for="year in yearRange"
|
|
|
|
|
:key="year.toString()"
|
|
|
|
|
:value="year.year"
|
|
|
|
|
:selected="date.year === year.year"
|
|
|
|
|
>
|
|
|
|
|
{{ formatter.custom(toDate(year), { year: "numeric" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
</NativeSelectOption>
|
|
|
|
|
</NativeSelect>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DefineYearTemplate>
|
|
|
|
|
|
|
|
|
|
<CalendarRoot
|
|
|
|
|
v-slot="{ grid, weekDays, date }"
|
|
|
|
|
v-bind="forwarded"
|
|
|
|
|
v-model:placeholder="placeholder"
|
|
|
|
|
data-slot="calendar"
|
|
|
|
|
:class="cn('p-3', props.class)"
|
|
|
|
|
>
|
|
|
|
|
<CalendarHeader class="pt-0">
|
2026-01-31 02:54:00 +08:00
|
|
|
<nav
|
|
|
|
|
class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between"
|
|
|
|
|
>
|
2026-01-31 00:00:46 +08:00
|
|
|
<CalendarPrevButton>
|
|
|
|
|
<slot name="calendar-prev-icon" />
|
|
|
|
|
</CalendarPrevButton>
|
|
|
|
|
<CalendarNextButton>
|
|
|
|
|
<slot name="calendar-next-icon" />
|
|
|
|
|
</CalendarNextButton>
|
|
|
|
|
</nav>
|
|
|
|
|
|
2026-01-31 02:54:00 +08:00
|
|
|
<slot
|
|
|
|
|
name="calendar-heading"
|
|
|
|
|
:date="date"
|
|
|
|
|
:month="ReuseMonthTemplate"
|
|
|
|
|
:year="ReuseYearTemplate"
|
|
|
|
|
>
|
2026-01-31 00:00:46 +08:00
|
|
|
<template v-if="layout === 'month-and-year'">
|
|
|
|
|
<div class="flex items-center justify-center gap-1">
|
|
|
|
|
<ReuseMonthTemplate :date="date" />
|
|
|
|
|
<ReuseYearTemplate :date="date" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else-if="layout === 'month-only'">
|
|
|
|
|
<div class="flex items-center justify-center gap-1">
|
|
|
|
|
<ReuseMonthTemplate :date="date" />
|
2026-01-31 02:54:00 +08:00
|
|
|
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else-if="layout === 'year-only'">
|
|
|
|
|
<div class="flex items-center justify-center gap-1">
|
2026-01-31 02:54:00 +08:00
|
|
|
{{ formatter.custom(toDate(date), { month: "short" }) }}
|
2026-01-31 00:00:46 +08:00
|
|
|
<ReuseYearTemplate :date="date" />
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<CalendarHeading />
|
|
|
|
|
</template>
|
|
|
|
|
</slot>
|
|
|
|
|
</CalendarHeader>
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
|
|
|
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
|
|
|
|
<CalendarGridHead>
|
|
|
|
|
<CalendarGridRow>
|
2026-01-31 02:54:00 +08:00
|
|
|
<CalendarHeadCell v-for="day in weekDays" :key="day">
|
2026-01-31 00:00:46 +08:00
|
|
|
{{ day }}
|
|
|
|
|
</CalendarHeadCell>
|
|
|
|
|
</CalendarGridRow>
|
|
|
|
|
</CalendarGridHead>
|
|
|
|
|
<CalendarGridBody>
|
2026-01-31 02:54:00 +08:00
|
|
|
<CalendarGridRow
|
|
|
|
|
v-for="(weekDates, index) in month.rows"
|
|
|
|
|
:key="`weekDate-${index}`"
|
|
|
|
|
class="mt-2 w-full"
|
|
|
|
|
>
|
2026-01-31 00:00:46 +08:00
|
|
|
<CalendarCell
|
|
|
|
|
v-for="weekDate in weekDates"
|
|
|
|
|
:key="weekDate.toString()"
|
|
|
|
|
:date="weekDate"
|
|
|
|
|
>
|
2026-01-31 02:54:00 +08:00
|
|
|
<CalendarCellTrigger :day="weekDate" :month="month.value" />
|
2026-01-31 00:00:46 +08:00
|
|
|
</CalendarCell>
|
|
|
|
|
</CalendarGridRow>
|
|
|
|
|
</CalendarGridBody>
|
|
|
|
|
</CalendarGrid>
|
|
|
|
|
</div>
|
|
|
|
|
</CalendarRoot>
|
|
|
|
|
</template>
|