abf78cbbb5
Add LunarBirthPicker for bazi and combined forms, converting lunar input to solar for chart calculation. Co-authored-by: Cursor <cursoragent@cursor.com>
140 lines
4.0 KiB
TypeScript
140 lines
4.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import {
|
||
formatLunarLabel,
|
||
formatSolarLabel,
|
||
lunarToSolarYmd,
|
||
solarYmdToLunarParts,
|
||
todayLunarParts,
|
||
todaySolarYmd,
|
||
type LunarDateParts,
|
||
} from "@/lib/calc/lunar-date";
|
||
|
||
interface LunarBirthPickerProps {
|
||
solarDate: string;
|
||
time: string;
|
||
onSolarDateChange: (solarYmd: string) => void;
|
||
onTimeChange: (time: string) => void;
|
||
label?: string;
|
||
timeDisabled?: boolean;
|
||
}
|
||
|
||
const inputClass =
|
||
"rounded-md border bg-background px-3 py-2 text-sm disabled:opacity-50";
|
||
|
||
export default function LunarBirthPicker({
|
||
solarDate,
|
||
time,
|
||
onSolarDateChange,
|
||
onTimeChange,
|
||
label = "出生日期 / 时间",
|
||
timeDisabled = false,
|
||
}: LunarBirthPickerProps) {
|
||
const [lunar, setLunar] = useState<LunarDateParts>(() => {
|
||
return solarYmdToLunarParts(solarDate) ?? todayLunarParts();
|
||
});
|
||
|
||
useEffect(() => {
|
||
const parsed = solarYmdToLunarParts(solarDate);
|
||
if (parsed) {
|
||
setLunar(parsed);
|
||
}
|
||
}, [solarDate]);
|
||
|
||
const solarYmd = useMemo(() => lunarToSolarYmd(lunar), [lunar]);
|
||
const lunarLabel = useMemo(() => formatLunarLabel(lunar), [lunar]);
|
||
const solarLabel = useMemo(
|
||
() => (solarYmd ? formatSolarLabel(solarYmd) : null),
|
||
[solarYmd],
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (solarYmd && solarYmd !== solarDate) {
|
||
onSolarDateChange(solarYmd);
|
||
}
|
||
}, [solarYmd, solarDate, onSolarDateChange]);
|
||
|
||
function updateLunar(patch: Partial<LunarDateParts>) {
|
||
setLunar((prev) => ({ ...prev, ...patch }));
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
<label className="text-sm font-medium">{label}</label>
|
||
<p className="text-xs text-muted-foreground">请按农历(阴历)填写出生日期</p>
|
||
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<div className="space-y-1">
|
||
<span className="text-xs text-muted-foreground">农历年</span>
|
||
<input
|
||
type="number"
|
||
className={inputClass + " w-full"}
|
||
min={1900}
|
||
max={2100}
|
||
value={lunar.year}
|
||
onChange={(e) => updateLunar({ year: Number(e.target.value) })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<span className="text-xs text-muted-foreground">农历月</span>
|
||
<input
|
||
type="number"
|
||
className={inputClass + " w-full"}
|
||
min={1}
|
||
max={12}
|
||
value={lunar.month}
|
||
onChange={(e) => updateLunar({ month: Number(e.target.value) })}
|
||
/>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<span className="text-xs text-muted-foreground">农历日</span>
|
||
<input
|
||
type="number"
|
||
className={inputClass + " w-full"}
|
||
min={1}
|
||
max={30}
|
||
value={lunar.day}
|
||
onChange={(e) => updateLunar({ day: Number(e.target.value) })}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
||
<input
|
||
type="checkbox"
|
||
checked={lunar.isLeapMonth}
|
||
onChange={(e) => updateLunar({ isLeapMonth: e.target.checked })}
|
||
/>
|
||
闰月
|
||
</label>
|
||
|
||
{lunarLabel && (
|
||
<p className="text-xs text-muted-foreground">农历:{lunarLabel}</p>
|
||
)}
|
||
|
||
{solarLabel ? (
|
||
<p className="rounded-md border border-primary/20 bg-primary/5 px-3 py-2 text-sm">
|
||
对应阳历:<span className="font-medium text-foreground">{solarLabel}</span>
|
||
<span className="ml-2 text-xs text-muted-foreground">({solarYmd})</span>
|
||
</p>
|
||
) : (
|
||
<p className="text-sm text-destructive">农历日期无效,请检查年月日或闰月</p>
|
||
)}
|
||
|
||
<div className="space-y-1">
|
||
<span className="text-xs text-muted-foreground">出生时辰</span>
|
||
<input
|
||
type="time"
|
||
className={inputClass + " w-full sm:w-auto"}
|
||
value={time}
|
||
disabled={timeDisabled}
|
||
onChange={(e) => onTimeChange(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export { todaySolarYmd };
|