统一投票/悬赏/抽奖截止与状态展示,抽奖支持多奖品行;发帖页悬赏积分对齐表格输入,并在余额不足时于底部就地提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
"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>
|
||
);
|
||
}
|