From f5d05755b7189905fe3fec5d7d18d532b34a8eb4 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Tue, 15 Sep 2026 08:45:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E7=AA=84=E5=B1=8F=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=20=E2=80=94=E2=80=94=20=E6=94=B6=E8=B5=B7=E8=A1=A5?= =?UTF-8?q?=E5=8F=8D=E5=90=91=20morph=E3=80=81=E7=BC=96=E8=BE=91=E6=80=81?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=9A=90=E8=97=8F=E5=BA=95=E9=83=A8=E5=AF=BC?= =?UTF-8?q?=E8=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户:「收起没有动画,且输入框弹出后,底部导航栏会消失,再次点击界面导航才会出现」 ① 收起没动画:打开有 morph、收起是瞬间卸载(不对称)。补反向 morph: 打开时把球的矩形存进 originRef(球在框打开时不在 DOM 上,收起草稿时取不到), 收起时从当前形状缩回球的位置/大小(160ms),**播完再卸载**(先卸载就没得播)。 finished 被取消时也会 reject,两种情况都落到"收起"。 ② 底部导航消失:测量结果是 nav 的 rect = 0,0,0,0,即 **display:none**,不是被挤出去—— 根因是 `.form-editing .narrow-nav { display: none }` 撞上"回复框展开即自动聚焦 textarea" (form-editing 由 main.tsx 在任何输入框获焦时挂上)⇒ 一弹框导航就消失,点到别处才回来。 而这条隐藏已经没必要:viewport 声明了 `interactive-widget=resizes-content`,键盘弹出时 浏览器会重排布局,导航自然落在键盘上方。已删掉该隐藏。 顺带:详情/列表两个窄屏根节点补 min-h-0(flex 子项默认不可压缩,回复框一撑会把导航顶出可视区)。 判据 +4(编辑态不隐藏导航、viewport 声明 resizes-content、两个根节点带 min-h-0、收起有反向 morph 且先播完再卸载)。变异:抽掉 min-h-0 → 红;收起改回瞬间卸载 → 红;把 display:none 放回去 → 红。 ★ 一处判据自身的坑:判断"还有没有那条隐藏规则"必须用**剥注释版**——我第一版用原文判, 被我刚写的那段解释性注释(里面原样引用了那条规则)判红,与 read.mjs 里记的同一形态。 窄屏(420x820,真实邮件)实测:初始/开信/回复框弹出并聚焦(form-editing)/收起后 —— 底部导航高度都是 51px 且在视口内;收起时框上有 160ms 动画。 --- client/electron/src/components/MailView.tsx | 42 ++++++++++++++++++-- client/electron/src/index.css | 15 +++++-- client/electron/test/narrow-layout.test.mjs | 44 ++++++++++++++++++++- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/client/electron/src/components/MailView.tsx b/client/electron/src/components/MailView.tsx index f363d52..928fe27 100644 --- a/client/electron/src/components/MailView.tsx +++ b/client/electron/src/components/MailView.tsx @@ -73,7 +73,7 @@ export default function MailView() { // relative 是给收起态的回复悬浮球定位用的(见 ReplyBar 的说明): // 宽屏下没有它,球会挂到视口右下角,而不是这块详情栏的右下角。 return ( -
+
} @@ -134,7 +134,7 @@ export default function MailView() { } return ( -
+
markRead(currentMail.mail_id)} @@ -904,6 +904,8 @@ function ReplyBar({ // 跨断点回到默认,否则“自动隐藏”只在首次生效。 const [open, setOpen] = useState(false); const boxRef = useRef(null); + /** 球的矩形:打开时存下来,收起时用它做反向 morph(球那时不在 DOM 上) */ + const originRef = useRef(null); useEffect(() => setOpen(false), [narrow]); /* @@ -939,6 +941,36 @@ function ReplyBar({ return () => anim.cancel(); }, [open]); + /* + * ★ 2026-09-15 用户:「收起没有动画」。打开有 morph、收起是瞬间消失 —— 不对称。 + * 这里补反向:从当前形状缩回球的位置与大小,播完再卸载(先卸载就没得可播)。 + */ + const collapse = () => { + const el = boxRef.current; + const from = originRef.current; + if (!el || !from || window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setOpen(false); + return; + } + const to = el.getBoundingClientRect(); + if (!to.width) { + setOpen(false); + return; + } + const scale = Math.max(0.04, from.width / to.width); + const dx = from.left + from.width / 2 - (to.left + to.width / 2); + const dy = from.top + from.height / 2 - (to.top + to.height / 2); + const anim = el.animate( + [ + { transform: 'none', opacity: 1, borderRadius: '0px' }, + { transform: `translate(${dx}px, ${dy}px) scale(${scale})`, opacity: 0.15, borderRadius: '28px' } + ], + { duration: 160, easing: 'cubic-bezier(0.4, 0, 0.6, 1)' } + ); + // finished 在被取消时会 reject(组件卸载等),两种都得落到"收起" + anim.finished.then(() => setOpen(false)).catch(() => setOpen(false)); + }; + if (!replyTo) return null; // 判据是「当前登录用户名」而不是字面量 'human':后者是单用户时代的遗留, @@ -962,7 +994,9 @@ function ReplyBar({ data-testid="reply-fab" data-morph-trigger onClick={e => { - // 起点就在手边:把这个球自己的矩形记下来,回复框挂载时从它长出来 + // 起点就在手边:把这个球自己的矩形记下来 —— 打开时用它做 morph, + // 收起时还要用它把框"缩回球里"(那时球已经不在 DOM 上,取不到) + originRef.current = e.currentTarget.getBoundingClientRect(); rememberOrigin(e.currentTarget); setOpen(true); }} @@ -1040,7 +1074,7 @@ function ReplyBar({ {ccOpen ? '收起抄送' : '抄送'}