diff --git a/client/electron/src/components/ComposePage.tsx b/client/electron/src/components/ComposePage.tsx index 3840a61..fbb72b9 100644 --- a/client/electron/src/components/ComposePage.tsx +++ b/client/electron/src/components/ComposePage.tsx @@ -58,6 +58,8 @@ export default function ComposePage() { const el = rootRef.current; const from = takeComposeOrigin(); if (!el || !from || fromRect !== from) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; // 关动画的人不该被位移动到 + const to = el.getBoundingClientRect(); if (!to.width || !from.width) return; const scale = Math.max(0.04, from.width / to.width); diff --git a/client/electron/src/components/MailList.tsx b/client/electron/src/components/MailList.tsx index c21343d..d47f668 100644 --- a/client/electron/src/components/MailList.tsx +++ b/client/electron/src/components/MailList.tsx @@ -70,7 +70,7 @@ export default function MailList() { }; return ( -
+

{isSent ? '发件箱' : '收件箱'}

diff --git a/client/electron/src/components/MailView.tsx b/client/electron/src/components/MailView.tsx index f67ea9b..f363d52 100644 --- a/client/electron/src/components/MailView.tsx +++ b/client/electron/src/components/MailView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useLayoutEffect, useRef } from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useMailStore } from '../stores/mailStore'; @@ -17,6 +17,7 @@ import type { Mail } from '../types'; import { MailIcon, ShieldIcon, PersonIcon, BotIcon, ForwardIcon, TreeIcon, TagIcon, GaugeIcon, ChevronRightIcon, ChatBubbleIcon } from './icons'; import { Composer, ComposerChip } from './Composer'; import { useIsNarrow } from '../hooks/useIsNarrow'; +import { rememberOrigin, takeComposeOrigin } from '../lib/composeOrigin'; import { useUIStore } from '../stores/uiStore'; import AddressInput from './AddressInput'; import { AttachmentList, AttachmentPicker, type PendingAttachment } from './Attachments'; @@ -902,8 +903,42 @@ function ReplyBar({ // 默认收起(只留悬浮球),宽窄同一套 —— 理由见 CollapsibleHeader 的说明。 // 跨断点回到默认,否则“自动隐藏”只在首次生效。 const [open, setOpen] = useState(false); + const boxRef = useRef(null); useEffect(() => setOpen(false), [narrow]); + /* + * ★ 2026-09-15 用户:「邮件页面那个蓝色的圆形是聊天图标点击没有任何动画」。 + * + * 上一版这里只有一个 150ms 的 opacity 淡入 —— 观感上等于没有。用户要的形态与 + * 写信页一致:**这个球自己长成回复框**(容器变形)。 + * + * 用 useLayoutEffect 而不是 useEffect:要在浏览器**首次绘制之前**把 CSS 那个 + * 挂载淡入关掉并接上 morph,否则会先闪一下淡入再跳一次。 + * 只动 transform / opacity / border-radius(合成器可做,不触发布局)。 + */ + useLayoutEffect(() => { + const el = boxRef.current; + if (!open || !el) return; + const from = takeComposeOrigin(); + if (!from || !from.width) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; // 关动画的人不该被位移动到 + + const to = el.getBoundingClientRect(); + if (!to.width) 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); + el.style.animation = 'none'; // 让位给 morph:两条动画都动 transform/opacity + const anim = el.animate( + [ + { transform: `translate(${dx}px, ${dy}px) scale(${scale})`, opacity: 0.3, borderRadius: '28px' }, + { transform: 'none', opacity: 1, borderRadius: '0px' } + ], + { duration: 220, easing: 'cubic-bezier(0.22, 0.61, 0.36, 1)' } + ); + return () => anim.cancel(); + }, [open]); + if (!replyTo) return null; // 判据是「当前登录用户名」而不是字面量 'human':后者是单用户时代的遗留, @@ -925,7 +960,12 @@ function ReplyBar({