feat(webui): 日历翻页过渡动画(方向跟随手势/按钮)

用户:「日历滑动页面为什么没有切换动画?」。

## 做法

`shift()` 里记下方向(手势与"上一页/下一页"按钮**都走这一个函数** ⇒ 方向只有一个来源),
容器用 **key + 声明的动画类**:

    <div key={`${anchor.getTime()}-${scale}`} className={`flex-1 min-h-0 flex ${slideClass}`}>

## 为什么不是"命令式加类"

第一版用 `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`。
This commit is contained in:
2026-09-14 11:27:37 +08:00
parent ef68c4950c
commit d11e6df9a8
3 changed files with 124 additions and 9 deletions

View File

@ -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}`);