Files
jiang13-bbs/frontend/components/DeadlinePicker.tsx
freefire b2d3d6dd26 feat(posts): 类型帖截止时间、抽奖奖品表与悬赏发帖体验
统一投票/悬赏/抽奖截止与状态展示,抽奖支持多奖品行;发帖页悬赏积分对齐表格输入,并在余额不足时于底部就地提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 10:21:05 +08:00

131 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/** 发帖截止时间:快捷天数 + 自定义时刻 */
export type DeadlineMode = "preset" | "custom";
function pad2(n: number) {
return String(n).padStart(2, "0");
}
/** datetime-local 值(本地时区) */
export function toLocalInputValue(d: Date): string {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
}
/** 默认自定义:从现在起 +N 天 */
export function defaultCustomLocal(days = 7): string {
const d = new Date();
d.setDate(d.getDate() + days);
d.setSeconds(0, 0);
return toLocalInputValue(d);
}
/** 本地 datetime-local → RFC3339(UTC) */
export function localInputToRFC3339(local: string): string | null {
const t = Date.parse(local);
if (!Number.isFinite(t)) return null;
return new Date(t).toISOString().replace(/\.\d{3}Z$/, "Z");
}
export function formatLocalPreview(local: string): string {
const t = Date.parse(local);
if (!Number.isFinite(t)) return "";
try {
return new Date(t).toLocaleString("zh-CN");
} catch {
return local;
}
}
export default function DeadlinePicker({
label = "截止时间",
presets,
mode,
onModeChange,
days,
onDaysChange,
customLocal,
onCustomLocalChange,
disabled,
hint,
}: {
label?: string;
presets: readonly number[];
mode: DeadlineMode;
onModeChange: (m: DeadlineMode) => void;
days: number;
onDaysChange: (d: number) => void;
customLocal: string;
onCustomLocalChange: (v: string) => void;
disabled?: boolean;
hint?: string;
}) {
const minLocal = toLocalInputValue(new Date(Date.now() + 60 * 60 * 1000));
const maxLocal = toLocalInputValue(new Date(Date.now() + 90 * 24 * 60 * 60 * 1000));
return (
<div className="j13-deadline-picker">
<div className="j13-deadline-picker-label">{label}</div>
<div className="j13-deadline-picker-modes" role="tablist" aria-label={label}>
<button
type="button"
role="tab"
aria-selected={mode === "preset"}
className={`j13-deadline-mode${mode === "preset" ? " is-on" : ""}`}
disabled={disabled}
onClick={() => onModeChange("preset")}
>
快捷时长
</button>
<button
type="button"
role="tab"
aria-selected={mode === "custom"}
className={`j13-deadline-mode${mode === "custom" ? " is-on" : ""}`}
disabled={disabled}
onClick={() => {
onModeChange("custom");
if (!customLocal) onCustomLocalChange(defaultCustomLocal(days || 7));
}}
>
指定时刻
</button>
</div>
{mode === "preset" ? (
<div className="j13-deadline-presets">
{presets.map((d) => (
<button
key={d}
type="button"
className={`btn btn-sm ${days === d ? "btn-primary" : "btn-line"}`}
disabled={disabled}
onClick={() => onDaysChange(d)}
>
{d} 天
</button>
))}
</div>
) : (
<div className="j13-deadline-custom">
<input
type="datetime-local"
className="j13-compose-field j13-deadline-datetime"
value={customLocal}
min={minLocal}
max={maxLocal}
disabled={disabled}
onChange={(e) => onCustomLocalChange(e.target.value)}
aria-label="指定截止时刻"
/>
{customLocal ? (
<p className="meta text-[12.5px] !mb-0">将于 {formatLocalPreview(customLocal)} 截止</p>
) : null}
</div>
)}
{hint ? <p className="meta text-[12px] !mb-0">{hint}</p> : null}
</div>
);
}