feat(calendar): 年月拨盘(iOS 闹钟式滚轮)替换工具条的月份文字
- 新增 components/MonthYearWheel.jsx:年、月两列滚轮 · scroll-snap 吸附 + 指针按住拖动 + 鼠标滚轮/触摸板,停稳 140ms 后落值 · 滚动过程中实时算居中行,中间行放大加粗高亮、相邻行缩小淡出(像苹果拨盘) · 点某一行直接跳到该行;「回到今天」一键回当月 · 年份范围默认前后 12/20 年,两侧夹在视口内,下方放不下自动翻到上方 - 两个入口: · 工具条上的「2026年9月」由 <strong> 换成按钮,点击弹出拨盘,再点一次收起 · 日期跳转浮层里的标题同样可点:点击后浮层收起、同位弹出拨盘 - 拨盘选完年月,主日历与跳转浮层同步过去 修掉一个竞态:两列在同一帧内先后停稳时,后一次会读到渲染期闭包里的旧值, 把前一次的结果覆盖(滚到 2028年12月 会退回 2026年12月)。 改为组件内用 ref 保存最新年月、每次落值先更新 ref 再回调。 顺带修正跳转浮层标题多出来的一个右花括号。
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年月拨盘(iOS 闹钟式滚轮)
|
||||||
|
* - 两列各自滚动吸附,停稳后把最终值抛给父级(onChange)
|
||||||
|
* - 支持鼠标滚轮 / 触摸板 / 按住拖动;点某一行直接跳到该行
|
||||||
|
* - 外部值变化时反向同步滚动位置(且不会打回环)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ITEM_HEIGHT = 32 // 与 luxury.css 的 --wheel-item 保持一致
|
||||||
|
const SETTLE_DELAY = 140
|
||||||
|
|
||||||
|
function WheelColumn({ values, value, onSettle, format, ariaLabel }) {
|
||||||
|
const listRef = useRef(null)
|
||||||
|
const timerRef = useRef(null)
|
||||||
|
const dragRef = useRef(null)
|
||||||
|
const frameRef = useRef(0)
|
||||||
|
const muteRef = useRef(false)
|
||||||
|
const [dragging, setDragging] = useState(false)
|
||||||
|
const index = Math.max(0, values.indexOf(value))
|
||||||
|
const [centered, setCentered] = useState(index)
|
||||||
|
|
||||||
|
// 滚动过程中实时算出居中那一行,用来做 iOS 拨盘那样的放大/淡出
|
||||||
|
const syncCentered = useCallback(() => {
|
||||||
|
window.cancelAnimationFrame(frameRef.current)
|
||||||
|
frameRef.current = window.requestAnimationFrame(() => {
|
||||||
|
const node = listRef.current
|
||||||
|
if (!node) return
|
||||||
|
setCentered(Math.min(values.length - 1, Math.max(0, Math.round(node.scrollTop / ITEM_HEIGHT))))
|
||||||
|
})
|
||||||
|
}, [values.length])
|
||||||
|
|
||||||
|
const snapTo = useCallback((target, behavior) => {
|
||||||
|
const node = listRef.current
|
||||||
|
if (!node) return
|
||||||
|
if (Math.abs(node.scrollTop - target) < 1) return
|
||||||
|
muteRef.current = true
|
||||||
|
node.scrollTo({ top: target, behavior })
|
||||||
|
window.setTimeout(() => { muteRef.current = false }, behavior === 'smooth' ? 340 : 60)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 外部值变了(含父级纠正)就把滚轮挪到对应行
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
snapTo(index * ITEM_HEIGHT, 'auto')
|
||||||
|
}, [index, snapTo])
|
||||||
|
|
||||||
|
// 值变化(含外部纠正)后,让居中高亮跟过来
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setCentered(index)
|
||||||
|
}, [index])
|
||||||
|
|
||||||
|
const settle = useCallback(() => {
|
||||||
|
const node = listRef.current
|
||||||
|
if (!node) return
|
||||||
|
const next = Math.min(values.length - 1, Math.max(0, Math.round(node.scrollTop / ITEM_HEIGHT)))
|
||||||
|
if (values[next] !== value) onSettle(values[next])
|
||||||
|
snapTo(next * ITEM_HEIGHT, 'smooth')
|
||||||
|
}, [onSettle, snapTo, value, values])
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (muteRef.current || dragRef.current) return
|
||||||
|
syncCentered()
|
||||||
|
window.clearTimeout(timerRef.current)
|
||||||
|
timerRef.current = window.setTimeout(settle, SETTLE_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
window.clearTimeout(timerRef.current)
|
||||||
|
window.cancelAnimationFrame(frameRef.current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onPointerDown = (event) => {
|
||||||
|
const node = listRef.current
|
||||||
|
if (!node || event.button > 0) return
|
||||||
|
window.clearTimeout(timerRef.current)
|
||||||
|
dragRef.current = { startY: event.clientY, startTop: node.scrollTop, moved: false }
|
||||||
|
setDragging(true)
|
||||||
|
try { node.setPointerCapture(event.pointerId) } catch { /* 忽略不支持捕获的浏览器 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = (event) => {
|
||||||
|
if (!dragRef.current) return
|
||||||
|
const node = listRef.current
|
||||||
|
if (!node) return
|
||||||
|
const delta = event.clientY - dragRef.current.startY
|
||||||
|
if (Math.abs(delta) > 3) dragRef.current.moved = true
|
||||||
|
node.scrollTop = dragRef.current.startTop - delta
|
||||||
|
syncCentered()
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDrag = () => {
|
||||||
|
if (!dragRef.current) return
|
||||||
|
const moved = dragRef.current.moved
|
||||||
|
dragRef.current = null
|
||||||
|
setDragging(false)
|
||||||
|
if (moved) settle()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ymw-col" role="group" aria-label={ariaLabel}>
|
||||||
|
<div
|
||||||
|
className={`ymw-list${dragging ? ' is-dragging' : ''}`}
|
||||||
|
ref={listRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={endDrag}
|
||||||
|
onPointerCancel={endDrag}
|
||||||
|
>
|
||||||
|
<div className="ymw-pad" aria-hidden="true" />
|
||||||
|
{values.map((item, position) => {
|
||||||
|
const distance = Math.abs(position - centered)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={item}
|
||||||
|
className={`ymw-cell${distance === 0 ? ' is-active' : ''}`}
|
||||||
|
style={{
|
||||||
|
transform: `scale(${distance === 0 ? 1 : distance === 1 ? 0.92 : 0.84})`,
|
||||||
|
opacity: distance === 0 ? 1 : distance === 1 ? 0.7 : 0.38
|
||||||
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
onClick={() => onSettle(item)}
|
||||||
|
>
|
||||||
|
{format(item)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div className="ymw-pad" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MonthYearWheel({ year, month, onChange, onToday, minYear, maxYear }) {
|
||||||
|
const years = useMemo(() => {
|
||||||
|
const list = []
|
||||||
|
for (let y = minYear; y <= maxYear; y += 1) list.push(y)
|
||||||
|
return list
|
||||||
|
}, [minYear, maxYear])
|
||||||
|
const months = useMemo(() => Array.from({ length: 12 }, (_, index) => index + 1), [])
|
||||||
|
|
||||||
|
// 两列可能在同一帧内先后停稳,各自只认自己那一列时不能读渲染期闭包,
|
||||||
|
// 否则后一次会用旧值覆盖前一次(滚到 2028年12月 会退回 2026年12月)。
|
||||||
|
const latest = useRef({ year, month })
|
||||||
|
latest.current = { year, month }
|
||||||
|
|
||||||
|
const pickYear = useCallback((nextYear) => {
|
||||||
|
latest.current = { ...latest.current, year: nextYear }
|
||||||
|
onChange(latest.current.year, latest.current.month)
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
|
const pickMonth = useCallback((nextMonth) => {
|
||||||
|
latest.current = { ...latest.current, month: nextMonth }
|
||||||
|
onChange(latest.current.year, latest.current.month)
|
||||||
|
}, [onChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ymw-body">
|
||||||
|
<div className="ymw-drum">
|
||||||
|
<span className="ymw-highlight" aria-hidden="true" />
|
||||||
|
<WheelColumn values={years} value={year} onSettle={pickYear} format={item => `${item}`} ariaLabel="年份" />
|
||||||
|
<span className="ymw-unit">年</span>
|
||||||
|
<WheelColumn values={months} value={month} onSettle={pickMonth} format={item => `${item}`} ariaLabel="月份" />
|
||||||
|
<span className="ymw-unit">月</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="ymw-today" onClick={onToday}>回到今天</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
TrashIcon,
|
TrashIcon,
|
||||||
UsersIcon
|
UsersIcon
|
||||||
} from '../components/Icons'
|
} from '../components/Icons'
|
||||||
|
import MonthYearWheel from '../components/MonthYearWheel'
|
||||||
|
|
||||||
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||||
const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
const WEEKDAY_SHORT = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
@@ -30,6 +31,9 @@ const COMPOSER_WIDTH = 306
|
|||||||
const TIP_WIDTH = 268
|
const TIP_WIDTH = 268
|
||||||
const JUMP_WIDTH = 262
|
const JUMP_WIDTH = 262
|
||||||
const JUMP_HEIGHT = 292
|
const JUMP_HEIGHT = 292
|
||||||
|
const WHEEL_WIDTH = 219
|
||||||
|
const WHEEL_HEIGHT = 168
|
||||||
|
const WHEEL_YEAR_SPAN = { back: 12, forward: 20 }
|
||||||
const MAX_CELL_EVENTS = 2
|
const MAX_CELL_EVENTS = 2
|
||||||
|
|
||||||
// 春节(农历正月初一)公历日期,逐年查表;2025–2035 与天文台公布数据一致。
|
// 春节(农历正月初一)公历日期,逐年查表;2025–2035 与天文台公布数据一致。
|
||||||
@@ -303,11 +307,17 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
const [jumpOpen, setJumpOpen] = useState(false)
|
const [jumpOpen, setJumpOpen] = useState(false)
|
||||||
const [jumpPos, setJumpPos] = useState(null)
|
const [jumpPos, setJumpPos] = useState(null)
|
||||||
const [jumpMonth, setJumpMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1))
|
const [jumpMonth, setJumpMonth] = useState(() => new Date(today.getFullYear(), today.getMonth(), 1))
|
||||||
|
// 年月拨盘:工具条月份文字与跳转浮层标题共用
|
||||||
|
const [wheelOpen, setWheelOpen] = useState(false)
|
||||||
|
const [wheelPos, setWheelPos] = useState(null)
|
||||||
|
const [wheelPeriod, setWheelPeriod] = useState(() => ({ year: today.getFullYear(), month: today.getMonth() + 1 }))
|
||||||
|
|
||||||
const composerRef = useRef(null)
|
const composerRef = useRef(null)
|
||||||
const tipRef = useRef(null)
|
const tipRef = useRef(null)
|
||||||
const jumpRef = useRef(null)
|
const jumpRef = useRef(null)
|
||||||
const jumpBtnRef = useRef(null)
|
const jumpBtnRef = useRef(null)
|
||||||
|
const wheelRef = useRef(null)
|
||||||
|
const wheelAnchorRef = useRef(null)
|
||||||
const anchorElRef = useRef(null)
|
const anchorElRef = useRef(null)
|
||||||
const pendingSelectRef = useRef(null)
|
const pendingSelectRef = useRef(null)
|
||||||
|
|
||||||
@@ -460,6 +470,8 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
if (event.key !== 'Escape') return
|
if (event.key !== 'Escape') return
|
||||||
closeComposer()
|
closeComposer()
|
||||||
setTip(null)
|
setTip(null)
|
||||||
|
setJumpOpen(false)
|
||||||
|
setWheelOpen(false)
|
||||||
}
|
}
|
||||||
window.addEventListener('keydown', onKeyDown)
|
window.addEventListener('keydown', onKeyDown)
|
||||||
return () => window.removeEventListener('keydown', onKeyDown)
|
return () => window.removeEventListener('keydown', onKeyDown)
|
||||||
@@ -528,6 +540,43 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
setJumpOpen(false)
|
setJumpOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 年月拨盘:以触发文字为锚点,水平居中于它,下方放不下就翻到上方 */
|
||||||
|
const positionWheel = useCallback((element) => {
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
const centered = rect.left + rect.width / 2 - WHEEL_WIDTH / 2
|
||||||
|
const left = Math.min(Math.max(12, centered), Math.max(12, window.innerWidth - WHEEL_WIDTH - 12))
|
||||||
|
const flipUp = rect.bottom + 9 + WHEEL_HEIGHT > window.innerHeight && rect.top - 9 - WHEEL_HEIGHT > 0
|
||||||
|
setWheelPos({ left, top: flipUp ? rect.top - 9 : rect.bottom + 9, placement: flipUp ? 'above' : 'below' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const openWheel = (element) => {
|
||||||
|
if (!element) return
|
||||||
|
wheelAnchorRef.current = element
|
||||||
|
setWheelPeriod({ year: month.getFullYear(), month: month.getMonth() + 1 })
|
||||||
|
positionWheel(element)
|
||||||
|
setWheelOpen(true)
|
||||||
|
// 从跳转浮层里点进来时,把浮层收掉,只留拨盘
|
||||||
|
setJumpOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeWheel = () => setWheelOpen(false)
|
||||||
|
|
||||||
|
/* 工具条上的月份文字:再点一次即收起 */
|
||||||
|
const toggleWheelFromBar = (element) => {
|
||||||
|
if (wheelOpen && wheelAnchorRef.current === element) { closeWheel(); return }
|
||||||
|
openWheel(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拨盘选定年月:主日历与跳转浮层同步过去 */
|
||||||
|
const applyWheelPeriod = (nextYear, nextMonth) => {
|
||||||
|
setWheelPeriod({ year: nextYear, month: nextMonth })
|
||||||
|
const next = new Date(nextYear, nextMonth - 1, 1)
|
||||||
|
setMonth(next)
|
||||||
|
setJumpMonth(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const wheelGoToday = () => applyWheelPeriod(today.getFullYear(), today.getMonth() + 1)
|
||||||
|
|
||||||
// 点空白处收起跳转浮层(点按钮本身交给 toggleJump 处理)
|
// 点空白处收起跳转浮层(点按钮本身交给 toggleJump 处理)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!jumpOpen) return undefined
|
if (!jumpOpen) return undefined
|
||||||
@@ -552,6 +601,34 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
}
|
}
|
||||||
}, [jumpOpen, positionJump])
|
}, [jumpOpen, positionJump])
|
||||||
|
|
||||||
|
// 点空白处收起拨盘(点触发文字本身交给 openWheel 处理)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wheelOpen) return undefined
|
||||||
|
const onPointerDown = (event) => {
|
||||||
|
if (wheelRef.current?.contains(event.target)) return
|
||||||
|
if (event.target.closest?.('.calendar-month-btn')) return
|
||||||
|
closeWheel()
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onPointerDown)
|
||||||
|
return () => document.removeEventListener('mousedown', onPointerDown)
|
||||||
|
}, [wheelOpen])
|
||||||
|
|
||||||
|
// 页面滚动 / 缩放时跟着锚点走;锚点已从 DOM 摘掉(例如跳转浮层关了)就直接收起
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wheelOpen) return undefined
|
||||||
|
const follow = () => {
|
||||||
|
const anchor = wheelAnchorRef.current
|
||||||
|
if (!anchor || !anchor.isConnected || !anchor.getBoundingClientRect().width) { closeWheel(); return }
|
||||||
|
positionWheel(anchor)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', follow, true)
|
||||||
|
window.addEventListener('resize', follow)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', follow, true)
|
||||||
|
window.removeEventListener('resize', follow)
|
||||||
|
}
|
||||||
|
}, [wheelOpen, positionWheel])
|
||||||
|
|
||||||
const submitDraft = async (event) => {
|
const submitDraft = async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const title = draft.title.trim()
|
const title = draft.title.trim()
|
||||||
@@ -586,7 +663,7 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
<h2><CalendarIcon size={18} /> 工作日历</h2>
|
<h2><CalendarIcon size={18} /> 工作日历</h2>
|
||||||
<p className="calendar-hint">单击日期选中,再次单击添加日程 · 悬浮日程或节日查看详情</p>
|
<p className="calendar-hint">单击日期选中,再次单击添加日程 · 悬浮日程或节日查看详情</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="calendar-toolbar"><button className="calendar-today-btn" onClick={resetMonth}>今天</button><button onClick={() => shiftMonth(-1)} aria-label="上个月">‹</button><strong>{monthLabel}</strong><button onClick={() => shiftMonth(1)} aria-label="下个月">›</button><button type="button" ref={jumpBtnRef} className={`calendar-jump-btn${jumpOpen ? ' is-open' : ''}`} onClick={toggleJump} aria-label="快速跳转日期" aria-expanded={jumpOpen} title="快速跳转日期"><CalendarIcon size={16} /></button></div>
|
<div className="calendar-toolbar"><button className="calendar-today-btn" onClick={resetMonth}>今天</button><button onClick={() => shiftMonth(-1)} aria-label="上个月">‹</button><button type="button" className={`calendar-month-btn${wheelOpen ? ' is-open' : ''}`} onClick={(event) => toggleWheelFromBar(event.currentTarget)} aria-haspopup="dialog" aria-expanded={wheelOpen} title="滚轮选择年月">{monthLabel}</button><button onClick={() => shiftMonth(1)} aria-label="下个月">›</button><button type="button" ref={jumpBtnRef} className={`calendar-jump-btn${jumpOpen ? ' is-open' : ''}`} onClick={toggleJump} aria-label="快速跳转日期" aria-expanded={jumpOpen} title="快速跳转日期"><CalendarIcon size={16} /></button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="calendar-layout">
|
<div className="calendar-layout">
|
||||||
@@ -801,7 +878,7 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
>
|
>
|
||||||
<div className="jump-head">
|
<div className="jump-head">
|
||||||
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() - 1, 1))} aria-label="上个月">‹</button>
|
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() - 1, 1))} aria-label="上个月">‹</button>
|
||||||
<strong>{jumpMonth.getFullYear()}年{jumpMonth.getMonth() + 1}月</strong>
|
<button type="button" className="jump-month-btn calendar-month-btn" onClick={() => openWheel(jumpBtnRef.current)} aria-haspopup="dialog" title="滚轮选择年月">{jumpMonth.getFullYear()}年{jumpMonth.getMonth() + 1}月</button>
|
||||||
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() + 1, 1))} aria-label="下个月">›</button>
|
<button type="button" onClick={() => setJumpMonth(current => new Date(current.getFullYear(), current.getMonth() + 1, 1))} aria-label="下个月">›</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="jump-week">{WEEKDAYS.map(day => <span key={day}>{day.slice(1)}</span>)}</div>
|
<div className="jump-week">{WEEKDAYS.map(day => <span key={day}>{day.slice(1)}</span>)}</div>
|
||||||
@@ -822,6 +899,29 @@ function CalendarBoard({ todos: seedTodos, onTodoCreated }) {
|
|||||||
<button type="button" className="jump-today" onClick={() => pickJumpDay(todayKey)}>回到今天</button>
|
<button type="button" className="jump-today" onClick={() => pickJumpDay(todayKey)}>回到今天</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{wheelOpen && wheelPos && (
|
||||||
|
<div
|
||||||
|
className="calendar-popover calendar-wheel-pop"
|
||||||
|
ref={wheelRef}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="选择年月"
|
||||||
|
style={{
|
||||||
|
left: wheelPos.left,
|
||||||
|
top: wheelPos.top,
|
||||||
|
transform: wheelPos.placement === 'above' ? 'translateY(-100%)' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MonthYearWheel
|
||||||
|
year={wheelPeriod.year}
|
||||||
|
month={wheelPeriod.month}
|
||||||
|
onChange={applyWheelPeriod}
|
||||||
|
onToday={wheelGoToday}
|
||||||
|
minYear={Math.min(today.getFullYear() - WHEEL_YEAR_SPAN.back, month.getFullYear())}
|
||||||
|
maxYear={Math.max(today.getFullYear() + WHEEL_YEAR_SPAN.forward, month.getFullYear())}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,7 +288,9 @@ input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px
|
|||||||
.calendar-toolbar { display: flex; align-items: center; gap: 7px; color: var(--text-secondary); }
|
.calendar-toolbar { display: flex; align-items: center; gap: 7px; color: var(--text-secondary); }
|
||||||
.calendar-toolbar button { display: inline-grid; place-items: center; min-width: 28px; height: 28px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); }
|
.calendar-toolbar button { display: inline-grid; place-items: center; min-width: 28px; height: 28px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); }
|
||||||
.calendar-toolbar button:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
|
.calendar-toolbar button:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
|
||||||
.calendar-toolbar strong { min-width: 88px; color: var(--text-primary); text-align: center; font-size: 12.5px; }
|
.calendar-toolbar .calendar-month-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 88px; height: 28px; padding: 0 9px; color: var(--text-primary); font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
||||||
|
.calendar-toolbar .calendar-month-btn:hover { border-color: var(--primary-border); background: var(--primary-bg); color: var(--primary-light); }
|
||||||
|
.calendar-toolbar .calendar-month-btn.is-open { border-color: var(--primary); background: var(--primary-bg); color: var(--primary-light); }
|
||||||
.calendar-toolbar .calendar-today-btn { padding: 0 9px; border-color: var(--border-light); background: var(--surface-deep); font-size: 11px; }
|
.calendar-toolbar .calendar-today-btn { padding: 0 9px; border-color: var(--border-light); background: var(--surface-deep); font-size: 11px; }
|
||||||
/* 快速跳转日期:平时是低调的日历图标,悬浮/展开变蓝 */
|
/* 快速跳转日期:平时是低调的日历图标,悬浮/展开变蓝 */
|
||||||
.calendar-toolbar .calendar-jump-btn { border-color: var(--border-light); background: var(--surface-deep); }
|
.calendar-toolbar .calendar-jump-btn { border-color: var(--border-light); background: var(--surface-deep); }
|
||||||
@@ -436,6 +438,25 @@ 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-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 { 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-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); }
|
||||||
|
/* 年月拨盘(iOS 闹钟式滚轮) */
|
||||||
|
.calendar-wheel-pop { --wheel-item: 32px; padding: 12px 14px 10px; }
|
||||||
|
.ymw-body { display: grid; gap: 9px; }
|
||||||
|
.ymw-drum { position: relative; display: flex; align-items: center; justify-content: center; gap: 7px; }
|
||||||
|
.ymw-highlight { position: absolute; left: 2px; right: 2px; top: 50%; height: var(--wheel-item); transform: translateY(-50%); border: 1px solid var(--primary-border); border-radius: 9px; background: var(--primary-bg); pointer-events: none; }
|
||||||
|
.ymw-col { position: relative; width: 72px; height: calc(var(--wheel-item) * 3); }
|
||||||
|
.ymw-list { height: 100%; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; touch-action: none; cursor: grab; }
|
||||||
|
.ymw-list::-webkit-scrollbar { display: none; }
|
||||||
|
.ymw-list.is-dragging { scroll-snap-type: none; cursor: grabbing; }
|
||||||
|
.ymw-cell { display: block; width: 100%; height: var(--wheel-item); border: 0; background: transparent; color: var(--text-tertiary); font: 15px var(--font-mono); font-variant-numeric: tabular-nums; text-align: center; scroll-snap-align: center; cursor: pointer; transform-origin: center; transition: color var(--transition-fast); }
|
||||||
|
.ymw-cell:hover { color: var(--text-secondary); }
|
||||||
|
.ymw-cell.is-active { color: var(--primary-light); font-weight: 800; }
|
||||||
|
.ymw-pad { height: var(--wheel-item); }
|
||||||
|
.ymw-unit { color: var(--text-tertiary); font-size: 12px; }
|
||||||
|
.ymw-today { width: 100%; height: 27px; border: 1px solid var(--border-light); border-radius: 7px; background: var(--surface-deep); color: var(--text-secondary); font-size: 11px; cursor: pointer; transition: border-color var(--transition-fast), color var(--transition-fast); }
|
||||||
|
.ymw-today:hover { border-color: var(--primary); color: var(--primary-light); }
|
||||||
.dashboard-content-grid { display: grid; grid-template-columns: minmax(0, 1.52fr) minmax(280px, .72fr); gap: 18px; align-items: start; }
|
.dashboard-content-grid { display: grid; grid-template-columns: minmax(0, 1.52fr) minmax(280px, .72fr); gap: 18px; align-items: start; }
|
||||||
.dashboard-primary-column, .dashboard-secondary-column { min-width: 0; }
|
.dashboard-primary-column, .dashboard-secondary-column { min-width: 0; }
|
||||||
.ai-digest-panel { background: linear-gradient(135deg, color-mix(in srgb, var(--primary-bg) 82%, var(--surface)), var(--surface)); }
|
.ai-digest-panel { background: linear-gradient(135deg, color-mix(in srgb, var(--primary-bg) 82%, var(--surface)), var(--surface)); }
|
||||||
|
|||||||
Reference in New Issue
Block a user