diff --git a/frontend/app/globals.css b/frontend/app/globals.css index cc61445..7c580ad 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -700,6 +700,12 @@ body { .j13-deadline-picker { display: flex; flex-direction: column; + gap: 10px; +} +.j13-deadline-picker-head { + display: flex; + flex-direction: column; + align-items: flex-start; gap: 8px; } .j13-deadline-picker-label { @@ -709,11 +715,12 @@ body { } .j13-deadline-picker-modes { display: inline-flex; - gap: 4px; - padding: 3px; - border-radius: 10px; - background: var(--tint); align-self: flex-start; + gap: 2px; + padding: 3px; + border-radius: 999px; + background: var(--tint); + border: 1px solid color-mix(in srgb, var(--line) 80%, transparent); } .j13-deadline-mode { border: 0; @@ -721,32 +728,179 @@ body { color: var(--ink-2); font-size: 12.5px; font-weight: 600; - padding: 5px 10px; - border-radius: 8px; + padding: 5px 12px; + border-radius: 999px; cursor: pointer; + transition: background 0.12s ease, color 0.12s ease, box-shadow 0.12s ease; } .j13-deadline-mode.is-on { background: var(--panel); color: var(--ink); box-shadow: var(--shadow-sm); } +.j13-deadline-mode:hover:not(:disabled):not(.is-on) { + color: var(--ink); +} .j13-deadline-mode:disabled { opacity: 0.55; cursor: not-allowed; } -.j13-deadline-presets { +.j13-deadline-presets, +.j13-deadline-quick-chips { display: flex; flex-wrap: wrap; - gap: 8px; + gap: 7px; +} +.j13-deadline-chip { + height: 32px; + padding: 0 13px; + border-radius: 999px; + border: 1px solid var(--line); + background: var(--panel); + color: var(--ink-2); + font-size: 13px; + font-weight: 500; + font-variant-numeric: tabular-nums; + transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; +} +.j13-deadline-chip-sm { + height: 28px; + padding: 0 10px; + font-size: 12.5px; +} +.j13-deadline-chip:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); + color: var(--accent); +} +.j13-deadline-chip.is-on { + background: var(--accent-soft); + border-color: color-mix(in srgb, var(--accent) 40%, var(--line)); + color: var(--accent); +} +.j13-deadline-chip:disabled { + opacity: 0.45; + cursor: not-allowed; } .j13-deadline-custom { display: flex; flex-direction: column; - gap: 6px; - max-width: 22rem; + gap: 10px; } -.j13-deadline-datetime { +.j13-deadline-fields { + display: flex; + flex-wrap: wrap; + align-items: stretch; + gap: 8px; +} +.j13-deadline-field { + display: inline-flex; + align-items: center; + gap: 8px; + /* 合适宽度:比半行窄、比内容贴边宽,留一点点击余量 */ + width: 11.75rem; + max-width: 100%; + flex: 0 0 auto; + min-width: 0; + padding: 8px 10px; + border-radius: 12px; + border: 1px solid var(--line); + background: var(--panel); + cursor: pointer; + transition: border-color 0.12s ease, box-shadow 0.12s ease; +} +.j13-deadline-field:last-child { + width: 8.75rem; +} +.j13-deadline-field:focus-within { + border-color: color-mix(in srgb, var(--accent) 45%, var(--line)); + box-shadow: 0 0 0 3px var(--accent-soft); +} +.j13-deadline-field-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + flex-shrink: 0; + color: var(--accent); + background: var(--accent-soft); +} +.j13-deadline-field-meta { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + flex: 1 1 auto; + position: relative; +} +.j13-deadline-field-label { + font-size: 11px; + font-weight: 650; + letter-spacing: 0.04em; + color: var(--ink-3); +} +.j13-deadline-field-input { + display: block; width: 100%; + min-width: 0; + border: 0; + padding: 0; + margin: 0; + background: transparent; + color: var(--ink); + font-size: 13.5px; + font-weight: 600; + font-variant-numeric: tabular-nums; + font-family: var(--font-sans); + outline: none; + color-scheme: light dark; +} +.j13-deadline-field-input:disabled { + opacity: 0.55; + cursor: not-allowed; +} +/* 左侧已有图标:隐藏原生指示器,避免双图标与多余空白 */ +.j13-deadline-field-input::-webkit-calendar-picker-indicator { + opacity: 0; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + cursor: pointer; +} +.j13-deadline-quick { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 10px; +} +.j13-deadline-quick-label { + flex-shrink: 0; + font-size: 12px; + font-weight: 650; + color: var(--ink-3); + min-width: 2em; +} +.j13-deadline-preview { + margin: 0; + padding: 9px 12px; + border-radius: 10px; + background: color-mix(in srgb, var(--accent-soft) 70%, var(--panel)); + border: 1px solid color-mix(in srgb, var(--accent) 18%, var(--line)); + color: var(--ink-2); + font-size: 12.5px; + line-height: 1.45; +} +.j13-deadline-preview strong { + color: var(--ink); + font-weight: 650; +} +.j13-deadline-hint { + margin: 0; + font-size: 12px; + line-height: 1.45; + color: var(--ink-3); } /* 奖品 / 中奖者:横向排布,放不下换行 */ .j13-lottery-prize-row, diff --git a/frontend/components/DeadlinePicker.tsx b/frontend/components/DeadlinePicker.tsx index 97a78b6..3b2ae05 100644 --- a/frontend/components/DeadlinePicker.tsx +++ b/frontend/components/DeadlinePicker.tsx @@ -1,6 +1,8 @@ "use client"; -/** 发帖截止时间:快捷天数 + 自定义时刻 */ +import { CalendarDays, Clock3 } from "lucide-react"; + +/** 发帖截止时间:快捷天数 + 指定时刻(分栏 + 快捷芯片) */ export type DeadlineMode = "preset" | "custom"; @@ -17,7 +19,7 @@ export function toLocalInputValue(d: Date): string { export function defaultCustomLocal(days = 7): string { const d = new Date(); d.setDate(d.getDate() + days); - d.setSeconds(0, 0); + d.setHours(18, 0, 0, 0); return toLocalInputValue(d); } @@ -32,12 +34,95 @@ export function formatLocalPreview(local: string): string { const t = Date.parse(local); if (!Number.isFinite(t)) return ""; try { - return new Date(t).toLocaleString("zh-CN"); + return new Date(t).toLocaleString("zh-CN", { + year: "numeric", + month: "long", + day: "numeric", + weekday: "short", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); } catch { return local; } } +function splitLocal(v: string): { date: string; time: string } { + const [date = "", rawTime = ""] = (v || "").split("T"); + const time = (rawTime || "18:00").slice(0, 5); + return { date, time: /^\d{2}:\d{2}$/.test(time) ? time : "18:00" }; +} + +function joinLocal(date: string, time: string): string { + if (!date) return ""; + const t = /^\d{2}:\d{2}$/.test(time) ? time : "18:00"; + return `${date}T${t}`; +} + +function startOfDay(d: Date): Date { + const x = new Date(d); + x.setHours(0, 0, 0, 0); + return x; +} + +function withTime(base: Date, hours: number, minutes: number): Date { + const d = new Date(base); + d.setHours(hours, minutes, 0, 0); + return d; +} + +/** 至少晚于当前 1 小时;不满足则推到合法时刻 */ +function ensureMinLead(d: Date): Date { + const min = new Date(Date.now() + 60 * 60 * 1000); + min.setSeconds(0, 0); + return d.getTime() < min.getTime() ? min : d; +} + +type QuickSet = { id: string; label: string; build: () => Date }; + +function buildQuickSets(): QuickSet[] { + const now = new Date(); + return [ + { + id: "tonight", + label: "今晚 22:00", + build: () => ensureMinLead(withTime(now, 22, 0)), + }, + { + id: "tomorrow-noon", + label: "明天中午", + build: () => { + const d = new Date(now); + d.setDate(d.getDate() + 1); + return withTime(d, 12, 0); + }, + }, + { + id: "plus3-eve", + label: "3 天后晚", + build: () => { + const d = new Date(now); + d.setDate(d.getDate() + 3); + return withTime(d, 21, 0); + }, + }, + { + id: "sunday-eve", + label: "本周日晚", + build: () => { + const d = startOfDay(now); + const day = d.getDay(); // 0=日 + const add = day === 0 ? 7 : 7 - day; + d.setDate(d.getDate() + add); + return ensureMinLead(withTime(d, 21, 0)); + }, + }, + ]; +} + +const TIME_CHIPS = ["09:00", "12:00", "18:00", "21:00", "22:00", "23:59"] as const; + export default function DeadlinePicker({ label = "截止时间", presets, @@ -61,46 +146,66 @@ export default function DeadlinePicker({ 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)); + const minDate = toLocalInputValue(new Date()).slice(0, 10); + const maxDate = toLocalInputValue(new Date(Date.now() + 90 * 24 * 60 * 60 * 1000)).slice(0, 10); + const { date, time } = splitLocal(customLocal); + const quickSets = buildQuickSets(); + + const applyCustom = (next: Date) => { + onCustomLocalChange(toLocalInputValue(ensureMinLead(next))); + }; + + const patchDate = (nextDate: string) => { + onCustomLocalChange(joinLocal(nextDate, time || "18:00")); + }; + + const patchTime = (nextTime: string) => { + const baseDate = date || toLocalInputValue(new Date()).slice(0, 10); + onCustomLocalChange(joinLocal(baseDate, nextTime)); + }; + + const enterCustom = () => { + onModeChange("custom"); + if (!customLocal) onCustomLocalChange(defaultCustomLocal(days || 7)); + }; return (
-
{label}
-
- - +
+
{label}
+
+ + +
{mode === "preset" ? ( -
+
{presets.map((d) => (
) : (
- onCustomLocalChange(e.target.value)} - aria-label="指定截止时刻" - /> +
+ + +
+ +
+ 快捷 +
+ {quickSets.map((q) => ( + + ))} +
+
+ +
+ 时刻 +
+ {TIME_CHIPS.map((t) => ( + + ))} +
+
+ {customLocal ? ( -

将于 {formatLocalPreview(customLocal)} 截止

+

+ 将于 {formatLocalPreview(customLocal)} 截止 +

) : null}
)} - {hint ?

{hint}

: null} + {hint ?

{hint}

: null}
); }