feat(calendar): 跳转浮层年月改为直接输入,回车快速跳转

- 点开 🗓 后的浮层标题由「弹出拨盘」改为输入框:前 4 位是年、后 1~2 位是月,
  只填 4 位则沿用当前月;回车跳转并关闭编辑态,Esc 取消且不关浮层
- 非数字自动过滤、上限 6 位;月越界或年份越界标红提示且保留焦点
- 进入编辑态时把「2026年9月」换成「202609」并全选,两种触发(鼠标按下/聚焦)都稳
- 工具条上的年月文字仍保留 iOS 拨盘入口
This commit is contained in:
Drsakura
2026-09-11 11:22:04 +08:00
parent 0453a49e4e
commit 850abe9327
2 changed files with 67 additions and 5 deletions
+61 -2
View File
@@ -307,7 +307,10 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const [jumpOpen, setJumpOpen] = useState(false)
const [jumpPos, setJumpPos] = useState(null)
const [jumpMonth, setJumpMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1))
// 年月拨盘:工具条月份文字与跳转浮层标题共用
// 跳转浮层标题:直接输入年月(前 4 位年 + 后 1~2 位月,回车跳转)
const [jumpDraft, setJumpDraft] = useState(null)
const [jumpInvalid, setJumpInvalid] = useState(false)
// 年月拨盘:仅工具条月份文字使用
const [wheelOpen, setWheelOpen] = useState(false)
const [wheelPos, setWheelPos] = useState(null)
const [wheelPeriod, setWheelPeriod] = useState(() => ({ year: today.getFullYear(), month: today.getMonth() + 1 }))
@@ -316,6 +319,7 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const tipRef = useRef(null)
const jumpRef = useRef(null)
const jumpBtnRef = useRef(null)
const jumpInputRef = useRef(null)
const wheelRef = useRef(null)
const wheelAnchorRef = useRef(null)
const anchorElRef = useRef(null)
@@ -577,6 +581,42 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
const wheelGoToday = () => applyWheelPeriod(today.getFullYear(), today.getMonth() + 1)
/* 年月两种写法:「2026年9月」↔「202609」 */
const compactPeriod = (date) => `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`
/* 进入编辑态:换成紧凑数字串并全选,方便直接改;已在编辑中就不打断输入 */
const primeJumpInput = () => {
if (jumpDraft !== null) return
setJumpInvalid(false)
setJumpDraft(compactPeriod(jumpMonth))
requestAnimationFrame(() => jumpInputRef.current?.select())
}
const changeJumpInput = (event) => {
setJumpDraft(event.target.value.replace(/\D/g, '').slice(0, 6))
if (jumpInvalid) setJumpInvalid(false)
}
/* 前 4 位是年,剩下 1~2 位是月;只写 4 位则沿用当前月 */
const commitJumpInput = () => {
const digits = (jumpDraft ?? '').replace(/\D/g, '')
if (digits.length < 4) { setJumpInvalid(true); return false }
const year = Number(digits.slice(0, 4))
const tail = digits.slice(4)
const monthNum = tail ? Number(tail) : jumpMonth.getMonth() + 1
if (year < 1900 || year > 2999 || monthNum < 1 || monthNum > 12) { setJumpInvalid(true); return false }
setJumpMonth(new Date(year, monthNum - 1, 1))
setJumpInvalid(false)
setJumpDraft(null)
jumpInputRef.current?.blur()
return true
}
const cancelJumpInput = () => {
setJumpDraft(null)
setJumpInvalid(false)
}
// 点空白处收起跳转浮层(点按钮本身交给 toggleJump 处理)
useEffect(() => {
if (!jumpOpen) return undefined
@@ -878,7 +918,26 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
>
<div className="jump-head">
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() - 1, 1))} aria-label="上个月">‹</button>
<button type="button" className="jump-month-btn calendar-month-btn" onClick={() => openWheel(jumpBtnRef.current)} aria-haspopup="dialog" title="滚轮选择年月">{jumpMonth.getFullYear()}年{jumpMonth.getMonth() + 1}月</button>
<input
ref={jumpInputRef}
type="text"
className={`jump-month-input${jumpInvalid ? ' is-invalid' : ''}`}
value={jumpDraft ?? `${jumpMonth.getFullYear()}年${jumpMonth.getMonth() + 1}月`}
onMouseDown={primeJumpInput}
onFocus={primeJumpInput}
onChange={changeJumpInput}
onKeyDown={(event) => {
if (event.key === 'Enter') { event.preventDefault(); commitJumpInput(); return }
if (event.key === 'Escape') { event.stopPropagation(); event.preventDefault(); cancelJumpInput(); event.currentTarget.blur() }
}}
onBlur={cancelJumpInput}
inputMode="numeric"
autoComplete="off"
spellCheck={false}
aria-label="输入年月快速跳转"
aria-invalid={jumpInvalid || undefined}
title="前 4 位是年、后 1~2 位是月,如 202609,回车跳转"
/>
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() + 1, 1))} aria-label="下个月">›</button>
</div>
<div className="jump-week">{WEEKDAYS.map(day => <span key={day}>{day.slice(1)}</span>)}</div>
+6 -3
View File
@@ -438,9 +438,12 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
.jump-day.is-selected { border-color: var(--primary); background: var(--primary); color: var(--surface-deep); font-weight: 800; }
.jump-today { width: 100%; height: 27px; margin-top: 8px; border: 1px solid var(--border-light); border-radius: 7px; background: var(--surface-deep); color: var(--text-secondary); font-size: 11px; cursor: pointer; }
.jump-today:hover { border-color: var(--primary); color: var(--primary-light); }
/* 跳转浮层标题 = 可点的年月(弹出拨盘) */
.jump-head .jump-month-btn { display: inline-flex; align-items: center; justify-content: center; flex: 1; width: auto; min-width: 0; height: 24px; padding: 0 8px; border-radius: 7px; color: var(--text-primary); font-size: 12px; font-variant-numeric: tabular-nums; }
.jump-head .jump-month-btn:hover { border-color: var(--primary-border); background: var(--primary-bg); color: var(--primary-light); }
/* 跳转浮层标题 = 直接输入年月(前 4 位年 + 后 1~2 位月,回车跳转) */
.jump-head .jump-month-input { display: block; flex: 1; width: auto; min-width: 0; min-height: 0; height: 26px; padding: 0 8px; border: 1px solid var(--border-light); border-radius: 7px; background: var(--surface-deep); box-shadow: none; color: var(--text-primary); font: 12px var(--font-body); font-variant-numeric: tabular-nums; text-align: center; outline: none; cursor: text; transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast); }
.jump-head .jump-month-input:hover { border-color: var(--primary-border); background: var(--primary-bg); }
.jump-head .jump-month-input:focus { border-color: var(--primary); background: var(--primary-bg); color: var(--primary-light); font-weight: 700; letter-spacing: .3px; }
.jump-head .jump-month-input.is-invalid { border-color: var(--danger); background: var(--danger-bg); color: var(--danger); }
.jump-head .jump-month-input::selection { background: var(--primary); color: var(--surface-deep); }
/* 年月拨盘(iOS 闹钟式滚轮) */
.calendar-wheel-pop { --wheel-item: 32px; padding: 12px 14px 10px; }
.ymw-body { display: grid; gap: 9px; }