refactor(ui): 抽取 Tooltip 组件,行内重建按钮补 tooltip
新增可复用 Tooltip 组件(group-hover 显现,支持 top/bottom 朝向), 消除顶部重建按钮手写的内联 tooltip div 重复,并为表格行内重建 按钮补上 tooltip(朝上,避免被表格容器的 overflow-hidden 裁掉)。
This commit is contained in:
parent
b6fe0ae082
commit
73b9cad550
@ -192,6 +192,43 @@ pub fn EmptyState(message: &'static str, variant: &'static str) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tooltip 定位样式(胶囊:黑底白字,hover 显现)。
|
||||
const TOOLTIP_STYLE: &str =
|
||||
"pointer-events-none absolute left-1/2 -translate-x-1/2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-paper-primary text-paper-theme shadow-lg z-50";
|
||||
|
||||
/// Tooltip 包裹组件。
|
||||
///
|
||||
/// 将任意触发器(按钮等)包裹后,鼠标 hover 时在上方或下方居中弹出提示。
|
||||
/// 用 CSS `group` + `group-hover:opacity-100` 实现,无 JS 状态,`pointer-events-none`
|
||||
/// 保证不拦截点击。
|
||||
///
|
||||
/// Props:
|
||||
/// - `tip`:提示文案
|
||||
/// - `children`:触发器元素(按钮 / 链接等)
|
||||
/// - `placement`:弹出方向,`"top"`(默认)或 `"bottom"`
|
||||
///
|
||||
/// 注意:父容器若有 `overflow-hidden` 会裁掉 tooltip,此时应选朝外的方向
|
||||
/// (如表格行在 `overflow-hidden` 容器内,朝上的 tooltip 才不会被裁)。
|
||||
#[component]
|
||||
pub fn Tooltip(
|
||||
tip: String,
|
||||
children: Element,
|
||||
#[props(default = "top")] placement: &'static str,
|
||||
) -> Element {
|
||||
// 朝上:tooltip 在触发器上方(bottom-full + mb-2);朝下:在下方(top-full + mt-2)。
|
||||
let position_class = if placement == "bottom" {
|
||||
"top-full mt-2"
|
||||
} else {
|
||||
"bottom-full mb-2"
|
||||
};
|
||||
rsx! {
|
||||
div { class: "group relative",
|
||||
{children}
|
||||
div { class: "{TOOLTIP_STYLE} {position_class}", "{tip}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static TAB_GROUP_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
/// 筛选选项卡组件。
|
||||
|
||||
@ -16,8 +16,8 @@ use crate::components::empty_state::{EmptyState, EmptyStateAction};
|
||||
use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
|
||||
use crate::components::skeletons::posts_skeleton::PostsSkeleton;
|
||||
use crate::components::ui::{
|
||||
Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_ACCENT, BTN_TEXT_RED,
|
||||
SPINNER_SVG,
|
||||
Pagination, StatusBadge, Tooltip, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_ACCENT,
|
||||
BTN_TEXT_RED, SPINNER_SVG,
|
||||
};
|
||||
use crate::hooks::query::use_paginated;
|
||||
use crate::models::post::PostListItem;
|
||||
@ -237,7 +237,9 @@ fn RebuildCacheBar(
|
||||
rsx! {
|
||||
// 仅渲染按钮行本身:结果消息已上提到 PostsPage,作为独立状态行进入文档流。
|
||||
div { class: "flex items-center gap-3",
|
||||
div { class: "group relative",
|
||||
Tooltip {
|
||||
tip: "重建 content_html 为空的文章渲染缓存".to_string(),
|
||||
placement: "bottom",
|
||||
button {
|
||||
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" },
|
||||
disabled: rebuilding(),
|
||||
@ -253,11 +255,10 @@ fn RebuildCacheBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-paper-primary text-paper-theme shadow-lg z-50",
|
||||
"重建 content_html 为空的文章渲染缓存"
|
||||
}
|
||||
}
|
||||
div { class: "group relative",
|
||||
Tooltip {
|
||||
tip: "重建所有文章的渲染缓存(含已有内容)".to_string(),
|
||||
placement: "bottom",
|
||||
button {
|
||||
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" },
|
||||
disabled: rebuilding(),
|
||||
@ -273,9 +274,6 @@ fn RebuildCacheBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-paper-primary text-paper-theme shadow-lg z-50",
|
||||
"重建所有文章的渲染缓存(含已有内容)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -317,22 +315,25 @@ fn PostRow(
|
||||
to: Route::WriteEdit { id: post.id },
|
||||
"编辑"
|
||||
}
|
||||
button {
|
||||
class: if rebuilding {
|
||||
"relative inline-flex items-center text-xs text-paper-accent cursor-not-allowed"
|
||||
} else {
|
||||
BTN_TEXT_ACCENT
|
||||
},
|
||||
disabled: rebuilding,
|
||||
onclick: move |_| on_rebuild.call(post.id),
|
||||
span {
|
||||
class: if rebuilding { "opacity-40" } else { "" },
|
||||
"重建"
|
||||
}
|
||||
if rebuilding {
|
||||
Tooltip {
|
||||
tip: "重新渲染这篇文章的 HTML".to_string(),
|
||||
button {
|
||||
class: if rebuilding {
|
||||
"relative inline-flex items-center text-xs text-paper-accent cursor-not-allowed"
|
||||
} else {
|
||||
BTN_TEXT_ACCENT
|
||||
},
|
||||
disabled: rebuilding,
|
||||
onclick: move |_| on_rebuild.call(post.id),
|
||||
span {
|
||||
class: "absolute inset-0 flex items-center justify-center",
|
||||
dangerous_inner_html: SPINNER_SVG,
|
||||
class: if rebuilding { "opacity-40" } else { "" },
|
||||
"重建"
|
||||
}
|
||||
if rebuilding {
|
||||
span {
|
||||
class: "absolute inset-0 flex items-center justify-center",
|
||||
dangerous_inner_html: SPINNER_SVG,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user