From d11e6df9a89bc1c19318262906443e56e824e3a9 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 11:27:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui):=20=E6=97=A5=E5=8E=86=E7=BF=BB?= =?UTF-8?q?=E9=A1=B5=E8=BF=87=E6=B8=A1=E5=8A=A8=E7=94=BB=EF=BC=88=E6=96=B9?= =?UTF-8?q?=E5=90=91=E8=B7=9F=E9=9A=8F=E6=89=8B=E5=8A=BF/=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「日历滑动页面为什么没有切换动画?」。 ## 做法 `shift()` 里记下方向(手势与"上一页/下一页"按钮**都走这一个函数** ⇒ 方向只有一个来源), 容器用 **key + 声明的动画类**:
## 为什么不是"命令式加类" 第一版用 `el.classList.add()`(与视图切换动画同一套写法),**类根本没进 DOM**。 原因:切月时 `loading` 会把网格整块换成加载态、再换回来,命令式加上的类会被 React 的重渲染与那次重挂载抹掉。key 变化 ⇒ 新元素自带动画类出现 ⇒ 必然重放。 ## 顺带修掉一个守卫 bug `firstRender` 守卫原先只在 `el` 存在时才消费。挂载时 `loading=true` ⇒ 容器还没渲染 ⇒ 守卫一直留着 ⇒ **用户第一次真正翻页的动画被吞掉**(实测:点第一下不动、第二下才动)。 现在无条件消费。 ## 判据 `test/manual/calendar-swipe-verify.mjs` 增加三条(与滑动手势同一条线索): 反向对照"刚进页面不跑动画"(animationName=none)、翻页时 `cal-slide-next` + `cal-in-next` 且时长 >0、反向翻页是 `cal-slide-prev`(方向正确性)。 **实测**:静止 none → 点下一页 `cal-slide-next`/`cal-in-next` 0.2s → 上一页 `cal-slide-prev`/`cal-in-prev`。 --- .../electron/src/components/CalendarView.tsx | 49 ++++++++++++++++--- client/electron/src/index.css | 44 +++++++++++++++++ .../test/manual/calendar-swipe-verify.mjs | 40 ++++++++++++++- 3 files changed, 124 insertions(+), 9 deletions(-) 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} 个`);