feat(theme): 切换按钮点击立即换图标并播放进入动画
将 theme.set 从 450ms 延迟改为立即执行——图标在点击瞬间切换,不再 等待圆形 VT 动画结束。原作者担心重渲染打断 VT 实属多余:VT 伪元素 快照在 __startThemeTransition 内已同步拍好,后续真实 DOM 变化不影响。 - 移除 click_gen 防抖机制与 spawn_local 延迟回调(不再需要)。 - SVG 加 key 属性,theme 变化时 Dioxus 重新挂载 SVG,触发 CSS 进入动画。 - input.css 新增 @keyframes theme-icon-enter(缩放 0.5→1 + 旋转 -60°→0° + 淡入,0.3s ease-out),仅在 .theme-toggle svg 上生效。 - prefers-reduced-motion 下禁用该动画,遵循项目既有约定。
This commit is contained in:
parent
9a4e6c3e6e
commit
a10003c90d
23
input.css
23
input.css
@ -634,6 +634,25 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 主题切换图标进入动画 ==========
|
||||||
|
点击切换主题时 theme.set 立即换 SVG(配合 RSX 的 key 强制重挂载),
|
||||||
|
该动画为新挂载的图标播放一次缩放+淡入,给用户即时反馈。
|
||||||
|
仅作用于 .theme-toggle 内的 svg,不影响页面其它 svg。 */
|
||||||
|
.theme-toggle svg {
|
||||||
|
animation: theme-icon-enter 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes theme-icon-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.5) rotate(-60deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
@ -704,6 +723,10 @@
|
|||||||
.page-enter {
|
.page-enter {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-toggle svg {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== Blur-up 渐进图片加载(已移入 @layer components) ========== */
|
/* ========== Blur-up 渐进图片加载(已移入 @layer components) ========== */
|
||||||
|
|||||||
56
src/theme.rs
56
src/theme.rs
@ -284,26 +284,23 @@ pub fn ThemePreload() -> Element {
|
|||||||
|
|
||||||
/// 主题切换按钮组件(三态循环:Light → Dark → System → Light)。
|
/// 主题切换按钮组件(三态循环:Light → Dark → System → Light)。
|
||||||
///
|
///
|
||||||
/// 点击根据当前模式取下一个 `Theme`。仅当**实际生效明暗**(ResolvedTheme)
|
/// 点击后**立即**切换图标(theme.set 不再延迟),让用户即时获得反馈;
|
||||||
/// 因切换而翻转时才触发圆形展开动画;若切换前后明暗不变(如 Light → System
|
/// 图标自身的进入动画(缩放+淡入)由 CSS `.theme-toggle svg` 的 keyframe
|
||||||
/// 且系统当前为浅色),直接更新 Signal,无动画无延迟。
|
/// 驱动,配合 SVG 元素的 `key` 属性——theme 变化时 Dioxus 重新挂载 SVG,
|
||||||
|
/// 进入动画随之重新触发。
|
||||||
|
///
|
||||||
|
/// 仅当**实际生效明暗**(ResolvedTheme)因切换而翻转时,才额外触发圆形展开
|
||||||
|
/// VT 动画(颜色过渡);明暗不变时(如 Light → System 且系统浅色)只换图标。
|
||||||
///
|
///
|
||||||
// evt 仅在 wasm32 用于取点击坐标,服务端构建剥离,故允许非 wasm 的 unused_variables。
|
// evt 仅在 wasm32 用于取点击坐标,服务端构建剥离,故允许非 wasm 的 unused_variables。
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))]
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_variables))]
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ThemeToggle() -> Element {
|
pub fn ThemeToggle() -> Element {
|
||||||
// theme 仅在非 wasm(server)构建的 theme.set(next) 需要 mut;wasm 端该分支
|
// theme 在 wasm 与 server 两侧都需要 mut(onclick 内 theme.set)。
|
||||||
// 被 cfg 剥离,写操作转移到副本 theme_clone,故 wasm 下标注 unused_mut。
|
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
|
||||||
let mut theme = use_theme();
|
|
||||||
// generation:每次点击递增。延迟回调检查自己的 gen 是否最新,过期则跳过 set。
|
|
||||||
// 解决连续点击时多个 spawn_local 堆积导致的状态错乱。
|
|
||||||
// click_gen 仅在 wasm 分支被 set(连续点击防抖);server 构建剥离该分支,
|
|
||||||
// 故非 wasm 端标注 unused_mut。
|
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
|
#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))]
|
||||||
let mut click_gen = use_signal(|| 0u32);
|
let mut theme = use_theme();
|
||||||
// resolved / system_dark 用于判断切换前后实际明暗是否翻转。
|
// resolved / system_dark 用于判断切换前后实际明暗是否翻转(决定是否触发 VT)。
|
||||||
// resolved 仅在 wasm 分支读取,system_dark 同理;非 wasm 端用 _ 引用避免警告。
|
// 仅在 wasm 分支读取;非 wasm 端用占位值避免 unused 警告。
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let resolved = use_resolved_theme();
|
let resolved = use_resolved_theme();
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -324,7 +321,7 @@ pub fn ThemeToggle() -> Element {
|
|||||||
let prev_resolved = resolved();
|
let prev_resolved = resolved();
|
||||||
let new_resolved = next.resolve(system_dark());
|
let new_resolved = next.resolve(system_dark());
|
||||||
if prev_resolved != new_resolved {
|
if prev_resolved != new_resolved {
|
||||||
// 实际明暗翻转 → 触发圆形展开动画。
|
// 实际明暗翻转 → 触发圆形展开 VT 动画(颜色过渡)。
|
||||||
// JS 从 DOM 现状推导目标主题(不传 isDark),避免与 Signal 状态不同步。
|
// JS 从 DOM 现状推导目标主题(不传 isDark),避免与 Signal 状态不同步。
|
||||||
let coords = evt.client_coordinates();
|
let coords = evt.client_coordinates();
|
||||||
let x = coords.x;
|
let x = coords.x;
|
||||||
@ -332,25 +329,16 @@ pub fn ThemeToggle() -> Element {
|
|||||||
let _ = js_sys::eval(
|
let _ = js_sys::eval(
|
||||||
&format!(
|
&format!(
|
||||||
"if (window.__startThemeTransition) \
|
"if (window.__startThemeTransition) \
|
||||||
window.__startThemeTransition({x}, {y});", // theme.set 推迟到动画结束:其触发的 Dioxus 微任务重渲染会打断 VT。
|
window.__startThemeTransition({x}, {y});",
|
||||||
x = x,
|
x = x,
|
||||||
y = y,
|
y = y,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
let gen = click_gen() + 1;
|
|
||||||
click_gen.set(gen);
|
|
||||||
let mut theme_clone = theme;
|
|
||||||
let gen_clone = click_gen;
|
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
|
||||||
crate::utils::time::sleep_ms(450).await;
|
|
||||||
if gen_clone() == gen {
|
|
||||||
theme_clone.set(next);
|
|
||||||
}
|
}
|
||||||
});
|
// 立即切换图标:theme.set 触发重渲染换 SVG,配合 key 触发 CSS 进入动画。
|
||||||
} else {
|
// VT 的伪元素快照在 __startThemeTransition 内已同步拍好,不受后续真实
|
||||||
// 切换前后明暗不变(如 Light → System 且系统浅色):直接更新,无动画。
|
// DOM 变化影响,故无需推迟 theme.set。
|
||||||
theme.set(next);
|
theme.set(next);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@ -359,9 +347,18 @@ pub fn ThemeToggle() -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 图标按当前主题意图(而非 resolved)选择,用户能看出自己在哪种模式。
|
// 图标按当前主题意图(而非 resolved)选择,用户能看出自己在哪种模式。
|
||||||
|
// key 强制 theme 变化时 Dioxus 重新挂载 SVG,从而重新触发 CSS 进入动画。
|
||||||
|
// (Dioxus 要求 key 为格式化字符串,故用 {icon_key} 占位。)
|
||||||
|
{
|
||||||
|
let icon_key: &str = match theme() {
|
||||||
|
Theme::Dark => "dark",
|
||||||
|
Theme::Light => "light",
|
||||||
|
Theme::System => "system",
|
||||||
|
};
|
||||||
match theme() {
|
match theme() {
|
||||||
Theme::Dark => rsx! {
|
Theme::Dark => rsx! {
|
||||||
svg {
|
svg {
|
||||||
|
key: "icon-{icon_key}",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
height: "24px",
|
height: "24px",
|
||||||
view_box: "0 -960 960 960",
|
view_box: "0 -960 960 960",
|
||||||
@ -372,6 +369,7 @@ pub fn ThemeToggle() -> Element {
|
|||||||
},
|
},
|
||||||
Theme::Light => rsx! {
|
Theme::Light => rsx! {
|
||||||
svg {
|
svg {
|
||||||
|
key: "icon-{icon_key}",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
height: "24px",
|
height: "24px",
|
||||||
view_box: "0 -960 960 960",
|
view_box: "0 -960 960 960",
|
||||||
@ -382,6 +380,7 @@ pub fn ThemeToggle() -> Element {
|
|||||||
},
|
},
|
||||||
Theme::System => rsx! {
|
Theme::System => rsx! {
|
||||||
svg {
|
svg {
|
||||||
|
key: "icon-{icon_key}",
|
||||||
xmlns: "http://www.w3.org/2000/svg",
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
height: "24px",
|
height: "24px",
|
||||||
view_box: "0 -960 960 960",
|
view_box: "0 -960 960 960",
|
||||||
@ -393,6 +392,7 @@ pub fn ThemeToggle() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 主题意图的中文标签,用于 aria-label / title。
|
/// 主题意图的中文标签,用于 aria-label / title。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user