diff --git a/client/electron/src/components/CalendarView.tsx b/client/electron/src/components/CalendarView.tsx index b70ca54..e188a60 100644 --- a/client/electron/src/components/CalendarView.tsx +++ b/client/electron/src/components/CalendarView.tsx @@ -112,7 +112,29 @@ export default function CalendarView() { const byDay = useMemo(() => bucketByDay(events), [events]); + /* + * 翻页过渡(2026-09-14 用户:「日历滑动页面为什么没有切换动画?」)。 + * + * 用**声明式** key + 类,而不是"命令式加类":切月时 loading 会把网格整块换成 + * 加载态再换回来,命令式加上的类会被 React 的重渲染与那次重挂载抹掉 + * (实测:类根本没进 DOM)。key 变化 ⇒ 新元素带着动画类出现 ⇒ 一定会重放。 + * + * 首次进入不动画(`navigated` 为 false),否则一打开日历就滑一下很怪。 + */ + const bodyRef = useRef(null); + const slideDir = useRef<1 | -1>(1); + const navigated = useRef(false); + const slideClass = navigated.current + ? slideDir.current === 1 + ? 'cal-slide-next' + : 'cal-slide-prev' + : ''; + function shift(dir: 1 | -1) { + // 记下方向:过渡动画要往正确的方向推(下一段从右边进、上一段从左边进)。 + // 手势和"上一页/下一页"按钮都走这里 ⇒ 方向只有一个来源。 + slideDir.current = dir; + navigated.current = true; if (scale === 'month') setAnchor(a => addMonths(a, dir)); else if (scale === 'week') setAnchor(a => addDays(a, dir * 7)); else setAnchor(a => addDays(a, dir)); @@ -322,7 +344,16 @@ export default function CalendarView() {
- ) : scale === 'month' ? ( + ) : ( + // 翻页动画的挂载点:三种刻度共用同一个容器,动画只有一处实现。 + // key 里带 anchor/scale:一变就是新元素 ⇒ 动画必然重放(见上面的说明)。 +
+ {scale === 'month' ? ( - ) : ( - + ) : ( + + )} +
)} ); diff --git a/client/electron/src/index.css b/client/electron/src/index.css index 5262d69..d823d2c 100644 --- a/client/electron/src/index.css +++ b/client/electron/src/index.css @@ -1087,3 +1087,47 @@ html.view-switch .glass-control, backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.2); -webkit-backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.2); } + + +/* + * 日历翻页过渡(2026-09-14 用户:「日历滑动页面为什么没有切换动画?」)。 + * + * 方向由 shift() 记录:下一段从右边推进来、上一段从左边推进来 —— 与手势方向一致。 + * 位移比整屏小(12% / 上限 56px):日历是密排网格,整屏平移会显得晃。 + */ +@keyframes cal-in-next { + from { + opacity: 0; + transform: translateX(12%); + } + to { + opacity: 1; + transform: none; + } +} + +@keyframes cal-in-prev { + from { + opacity: 0; + transform: translateX(-12%); + } + to { + opacity: 1; + transform: none; + } +} + +.cal-slide-next { + animation: cal-in-next 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +.cal-slide-prev { + animation: cal-in-prev 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + .cal-slide-next, + .cal-slide-prev { + animation: none; + } +} diff --git a/client/electron/test/manual/calendar-swipe-verify.mjs b/client/electron/test/manual/calendar-swipe-verify.mjs index 05339dd..4accced 100644 --- a/client/electron/test/manual/calendar-swipe-verify.mjs +++ b/client/electron/test/manual/calendar-swipe-verify.mjs @@ -1,7 +1,7 @@ /** * 日历左右滑动手势验收(2026-09-14 用户:「日历页面还不支持左右滑动手势」)。 * - * 三条判据必须一起看,否则"滑一下就翻页"也能算过: + * 判据必须一起看,否则"滑一下就翻页"也能算过: * ① 左滑 → 下一段(月/周/日三种刻度都走同一个 shift()) * ② 右滑 → 回到上一段(可逆) * ③ **反向对照**:纵向拖动不翻页 —— 移动端最容易犯的手势错误就是把滚动当翻页 @@ -77,6 +77,44 @@ try { const afterVertical = await title(); record('③ 反向对照:纵向拖动不翻页', afterVertical === afterRight, `仍为 ${afterVertical}`); + // ⑤ 翻页过渡动画(用户后续追问:「日历滑动页面为什么没有切换动画?」) + const slide = () => + page.evaluate(() => { + const el = document.querySelector('[data-slide]'); + if (!el) return null; + return { + slide: el.dataset.slide, + anim: getComputedStyle(el).animationName, + dur: getComputedStyle(el).animationDuration + }; + }); + const rest = await slide(); + record( + '⑤ 反向对照:刚进页面时不跑翻页动画', + !!rest && rest.anim === 'none', + `animationName=${rest?.anim}` + ); + await page.click('button[aria-label="下一页"]'); + await page.waitForTimeout(700); + await page.click('button[aria-label="下一页"]'); + await page.waitForTimeout(80); + const during = await slide(); + record( + '⑤ 翻页时有方向正确的过渡动画', + during?.slide === 'cal-slide-next' && during?.anim === 'cal-in-next' && parseFloat(during.dur) > 0, + `class=${during?.slide} 动画=${during?.anim} ${during?.dur}` + ); + // 反方向:上一页应当是 cal-slide-prev(方向只有一个来源 = shift()) + await page.waitForTimeout(900); + await page.click('button[aria-label="上一页"]'); + await page.waitForTimeout(80); + const backDuring = await slide(); + record( + '⑤ 反向翻页的方向也是对的(prev)', + backDuring?.slide === 'cal-slide-prev' && backDuring?.anim === 'cal-in-prev', + `class=${backDuring?.slide}` + ); + // ④ 回到今天不该被手势判断干扰(按钮仍在) const todayBtn = await page.locator('button:has-text("今天")').count(); record('④ 手势没有吃掉「今天」按钮', todayBtn > 0, `找到 ${todayBtn} 个`);