fix: update date format

This commit is contained in:
JOYCEQL
2026-02-12 14:01:21 +08:00
parent 1aacd57b06
commit b64a62dc47
3 changed files with 134 additions and 125 deletions
+32 -31
View File
@@ -1,9 +1,9 @@
"use client";
import { DateInput } from "@heroui/date-input";
import { HeroUIProvider } from "@heroui/react";
import { CalendarDate, parseDate } from "@internationalized/date";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import { useState } from "react";
interface UnifiedDateInputProps {
value: string;
@@ -18,52 +18,53 @@ export function UnifiedDateInput({
value,
onChange,
label,
placeholder,
isRequired,
className,
}: UnifiedDateInputProps) {
const t = useTranslations("common");
// Parse string "YYYY-MM" to CalendarDate
// We use useMemo to calculate the initial value only once or when key changes
// actually, we want to respect value updates if they are completely different (e.g. item switch)
// but ignore them if they are just caused by our own null update.
// Easiest robust way for now: Uncontrolled with defaultValue.
// Assuming the component is remounted when switching items (verified by AnimatePresence in parents).
const initialDate = useMemo(() => {
if (!value) return null;
const parseValue = (input: string): CalendarDate | null => {
if (!input) return null;
try {
const dateStr = value.length === 7 ? `${value}-01` : value;
return parseDate(dateStr);
} catch (e) {
let normalized = input.replace(/[./]/g, "-");
if (normalized.length === 7) normalized = `${normalized}-01`;
return parseDate(normalized);
} catch {
return null;
}
}, []); // Empty dependency array to treat as mount-time config
};
const [selectedDate, setSelectedDate] = useState<CalendarDate | null>(() =>
parseValue(value)
);
const handleDateChange = (date: CalendarDate | null) => {
setSelectedDate(date);
if (!date) {
onChange("");
return;
}
const month = date.month.toString().padStart(2, "0");
onChange(`${date.year}-${month}`);
onChange(`${date.year}/${month}`);
};
return (
<div className={className}>
<DateInput
label={label}
defaultValue={initialDate}
onChange={handleDateChange}
isRequired={isRequired}
granularity="month"
variant="bordered"
labelPlacement="outside"
classNames={{
label: "text-sm font-medium text-foreground",
inputWrapper: "shadow-sm hover:border-primary/50 focus-within:ring-2 focus-within:ring-primary focus-within:border-primary bg-background",
}}
/>
<HeroUIProvider locale="ja-JP">
<DateInput
label={label}
value={selectedDate}
onChange={handleDateChange}
isRequired={isRequired}
granularity="month"
variant="bordered"
labelPlacement="outside"
shouldForceLeadingZeros
classNames={{
label: "text-sm font-medium text-foreground",
inputWrapper:
"shadow-sm hover:border-primary/50 focus-within:ring-2 focus-within:ring-primary focus-within:border-primary bg-background",
}}
/>
</HeroUIProvider>
</div>
);
}
+101 -93
View File
@@ -1,9 +1,9 @@
"use client";
import { DateInput } from "@heroui/date-input";
import { HeroUIProvider } from "@heroui/react";
import { CalendarDate, parseDate } from "@internationalized/date";
import { useMemo, useState, useEffect } from "react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface UnifiedDateRangeInputProps {
@@ -14,120 +14,128 @@ interface UnifiedDateRangeInputProps {
className?: string;
}
const SEPARATOR = " - ";
const PRESENT_VALUES = new Set(["至今", "Present", "Now"]);
const parsePart = (part: string): CalendarDate | null => {
if (!part) return null;
const cleanPart = part.trim();
if (PRESENT_VALUES.has(cleanPart)) return null;
try {
let isoStr = cleanPart.replace(/[./]/g, "-");
if (isoStr.length === 7) isoStr += "-01";
if (isoStr.length === 4) isoStr += "-01-01";
return parseDate(isoStr);
} catch {
return null;
}
};
const parseRange = (rangeValue: string) => {
if (!rangeValue) return { start: null, end: null };
let startStr = "";
let endStr = "";
if (rangeValue.includes(SEPARATOR)) {
[startStr, endStr] = rangeValue.split(SEPARATOR);
} else {
const match = rangeValue.match(/^([^\s]+)\s*(?:-||—)\s*([^\s]+)$/);
if (match) {
startStr = match[1];
endStr = match[2];
} else {
startStr = rangeValue;
}
}
return { start: parsePart(startStr), end: parsePart(endStr) };
};
export function UnifiedDateRangeInput({
value,
onChange,
label,
className,
}: UnifiedDateRangeInputProps) {
// Parsing logic
// Expected formats: "YYYY.MM - YYYY.MM"
const t = useTranslations("common");
// Helper to normalize separator
const SEPARATOR = " - ";
// Helper to parse a single date string part
const parsePart = (part: string): CalendarDate | null => {
if (!part) return null;
const cleanPart = part.trim();
// Legacy support: if value has "Present", treat as null/empty for end date input?
// Or just let it fail parse and show empty.
if (["至今", "Present", "Now"].includes(cleanPart)) return null;
try {
// Replace dots with dashes for standard parsing
let isoStr = cleanPart.replace(/\./g, "-");
if (isoStr.length === 7) isoStr += "-01"; // YYYY-MM -> YYYY-MM-DD
if (isoStr.length === 4) isoStr += "-01-01"; // YYYY -> YYYY-MM-DD
return parseDate(isoStr);
} catch (e) {
return null;
}
};
// Deconstruct value into state
const { start, end } = useMemo(() => {
if (!value) return { start: null, end: null };
// ... logic ...
const parts = value.split("-").map(s => s.trim());
let startStr = "", endStr = "";
if (value.includes(" - ")) {
[startStr, endStr] = value.split(" - ");
} else {
const match = value.match(/^([^\s]+)\s*(?:-||—)\s*([^\s]+)$/);
if (match) {
startStr = match[1];
endStr = match[2];
} else {
startStr = value;
}
}
const start = parsePart(startStr);
// If endStr was "Present", parsePart returns null, which is what we want (empty input)
const end = parsePart(endStr);
return { start, end };
}, []); // Empty dependency array for uncontrolled initialization
const [range, setRange] = useState<{ start: CalendarDate | null; end: CalendarDate | null }>(
() => parseRange(value)
);
const updateValue = (newStart: CalendarDate | null, newEnd: CalendarDate | null) => {
// Format: YYYY.MM
const format = (d: CalendarDate) => `${d.year}.${d.month.toString().padStart(2, "0")}`;
const format = (d: CalendarDate) =>
`${d.year}/${d.month.toString().padStart(2, "0")}`;
const startStr = newStart ? format(newStart) : "";
const endStr = newEnd ? format(newEnd) : "";
if (!startStr && !endStr) {
onChange("");
return;
onChange("");
return;
}
if (startStr && !endStr) {
// If end is empty, maybe keep separator? "YYYY.MM - "
// Or just "YYYY.MM"? Context usually implies " - Present" if missing?
// But user removed "Present" option.
// Let's keep separator " - " to denote range started.
onChange(`${startStr}${SEPARATOR}`);
return;
onChange(`${startStr}${SEPARATOR}`);
return;
}
onChange(`${startStr}${SEPARATOR}${endStr}`);
};
const handleStartChange = (newStart: CalendarDate | null) => {
setRange((prev) => {
const next = { start: newStart, end: prev.end };
updateValue(next.start, next.end);
return next;
});
};
const handleEndChange = (newEnd: CalendarDate | null) => {
setRange((prev) => {
const next = { start: prev.start, end: newEnd };
updateValue(next.start, next.end);
return next;
});
};
return (
<div className={cn("w-full", className)}>
<HeroUIProvider locale="ja-JP">
<div className="flex items-center gap-2">
<div className="flex-1">
<DateInput
value={start}
onChange={(d) => updateValue(d, end)}
variant="bordered"
granularity="month"
aria-label="Start Date"
classNames={{
inputWrapper: "bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
innerWrapper: "pb-0",
}}
/>
</div>
<span className="text-muted-foreground">-</span>
<div className="flex-1 relative">
<DateInput
value={end}
onChange={(d) => updateValue(start, d)}
variant="bordered"
granularity="month"
aria-label="End Date"
classNames={{
inputWrapper: "bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
innerWrapper: "pb-0",
}}
/>
</div>
<div className="flex-1">
<DateInput
value={range.start}
onChange={handleStartChange}
variant="bordered"
granularity="month"
shouldForceLeadingZeros
aria-label="Start Date"
classNames={{
inputWrapper:
"bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
innerWrapper: "pb-0",
}}
/>
</div>
<span className="text-muted-foreground">-</span>
<div className="flex-1 relative">
<DateInput
value={range.end}
onChange={handleEndChange}
variant="bordered"
granularity="month"
shouldForceLeadingZeros
aria-label="End Date"
classNames={{
inputWrapper:
"bg-background hover:bg-muted/20 h-9 min-h-0 py-0 px-3 shadow-sm ring-1 ring-inset ring-input border-0",
innerWrapper: "pb-0",
}}
/>
</div>
</div>
</HeroUIProvider>
</div>
);
}
+1 -1
View File
@@ -46,7 +46,7 @@ export function formatDateString(dateStr: string | undefined, locale: string = "
try {
if (locale === "zh" || locale === "zh-CN") {
return `${date.getUTCFullYear()}.${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
return `${date.getUTCFullYear()}/${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
}
const formatter = new Intl.DateTimeFormat(locale, {
year: 'numeric',