polish(compose): 截止时间控件改为分栏与快捷芯片
指定时刻用日期/时刻分栏与常用芯片替代原生 datetime-local,并收紧布局与左对齐模式切换。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<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 className="j13-deadline-picker-head">
|
||||
<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={enterCustom}
|
||||
>
|
||||
指定时刻
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === "preset" ? (
|
||||
<div className="j13-deadline-presets">
|
||||
<div className="j13-deadline-presets" role="group" aria-label="快捷天数">
|
||||
{presets.map((d) => (
|
||||
<button
|
||||
key={d}
|
||||
type="button"
|
||||
className={`btn btn-sm ${days === d ? "btn-primary" : "btn-line"}`}
|
||||
className={`j13-deadline-chip${days === d ? " is-on" : ""}`}
|
||||
disabled={disabled}
|
||||
aria-pressed={days === d}
|
||||
onClick={() => onDaysChange(d)}
|
||||
>
|
||||
{d} 天
|
||||
@@ -109,22 +214,86 @@ export default function DeadlinePicker({
|
||||
</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="指定截止时刻"
|
||||
/>
|
||||
<div className="j13-deadline-fields">
|
||||
<label className="j13-deadline-field">
|
||||
<span className="j13-deadline-field-icon" aria-hidden>
|
||||
<CalendarDays size={15} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="j13-deadline-field-meta">
|
||||
<span className="j13-deadline-field-label">日期</span>
|
||||
<input
|
||||
type="date"
|
||||
className="j13-deadline-field-input"
|
||||
value={date}
|
||||
min={minDate}
|
||||
max={maxDate}
|
||||
disabled={disabled}
|
||||
onChange={(e) => patchDate(e.target.value)}
|
||||
aria-label="截止日期"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<label className="j13-deadline-field">
|
||||
<span className="j13-deadline-field-icon" aria-hidden>
|
||||
<Clock3 size={15} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="j13-deadline-field-meta">
|
||||
<span className="j13-deadline-field-label">时刻</span>
|
||||
<input
|
||||
type="time"
|
||||
className="j13-deadline-field-input"
|
||||
value={time}
|
||||
disabled={disabled}
|
||||
onChange={(e) => patchTime(e.target.value)}
|
||||
aria-label="截止时刻"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="j13-deadline-quick" role="group" aria-label="快捷设到">
|
||||
<span className="j13-deadline-quick-label">快捷</span>
|
||||
<div className="j13-deadline-quick-chips">
|
||||
{quickSets.map((q) => (
|
||||
<button
|
||||
key={q.id}
|
||||
type="button"
|
||||
className="j13-deadline-chip j13-deadline-chip-sm"
|
||||
disabled={disabled}
|
||||
onClick={() => applyCustom(q.build())}
|
||||
>
|
||||
{q.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="j13-deadline-quick" role="group" aria-label="常用时刻">
|
||||
<span className="j13-deadline-quick-label">时刻</span>
|
||||
<div className="j13-deadline-quick-chips">
|
||||
{TIME_CHIPS.map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
className={`j13-deadline-chip j13-deadline-chip-sm${time === t ? " is-on" : ""}`}
|
||||
disabled={disabled || !date}
|
||||
aria-pressed={time === t}
|
||||
onClick={() => patchTime(t)}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{customLocal ? (
|
||||
<p className="meta text-[12.5px] !mb-0">将于 {formatLocalPreview(customLocal)} 截止</p>
|
||||
<p className="j13-deadline-preview">
|
||||
将于 <strong>{formatLocalPreview(customLocal)}</strong> 截止
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{hint ? <p className="meta text-[12px] !mb-0">{hint}</p> : null}
|
||||
{hint ? <p className="j13-deadline-hint">{hint}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user