feat(posts): 类型帖截止时间、抽奖奖品表与悬赏发帖体验
统一投票/悬赏/抽奖截止与状态展示,抽奖支持多奖品行;发帖页悬赏积分对齐表格输入,并在余额不足时于底部就地提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
130
frontend/components/DeadlinePicker.tsx
Normal file
130
frontend/components/DeadlinePicker.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user