yggdrasil/components/skeletons/atoms.rs
1//! 骨架屏原子组件
2//!
3//! 提供通用的脉冲动画占位块,供各页面骨架屏组合使用。
4
5use dioxus::prelude::*;
6
7/// 通用骨架占位块。
8///
9/// Props:
10/// - `class`:Tailwind CSS 类,控制尺寸与形状
11/// - `style`:可选的内联样式字符串
12/// - `animate`:是否带 `animate-pulse` 呼吸动画(缺省 `true`)。少数骨架屏在
13/// 切换动画/骨架屏截断场景需要关闭,避免动画被截断显得突兀。
14///
15/// 默认带有 `animate-pulse` 动画与半透明的占位背景。
16#[component]
17pub fn SkeletonBox(
18 class: &'static str,
19 style: Option<&'static str>,
20 #[props(default = true)] animate: bool,
21) -> Element {
22 let pulse = if animate { " animate-pulse" } else { "" };
23 rsx! {
24 div {
25 class: "bg-paper-tertiary/30 dark:bg-gray-600{pulse} {class}",
26 style: style.unwrap_or(""),
27 }
28 }
29}
30
31/// 通用骨架「幽灵卡片」外壳:半透明背景 + 圆角 + 描边,供各页面骨架屏包裹
32/// 内容占位(列表行、表格、日志区等)。收敛此前散落在各骨架屏文件里手打的
33/// 同一段 `bg-[var(--color-paper-entry)]/40 rounded-2xl border ...` 前缀。
34///
35/// Props:
36/// - `class`:追加类(内边距、间距、`overflow-hidden`、`shadow-xs` 等按调用方需要传入)
37/// - `children`:卡片内的占位内容
38#[component]
39pub fn SkeletonCard(#[props(default)] class: Option<&'static str>, children: Element) -> Element {
40 let extra = class.unwrap_or_default();
41 rsx! {
42 div {
43 class: "bg-[var(--color-paper-entry)]/40 rounded-2xl border border-[var(--color-paper-border)]/70 {extra}",
44 {children}
45 }
46 }
47}
48
49/// 表格骨架屏单元格占位形状:不同列的表体内容并非全部是单个占位块,
50/// 部分列(如「用户」列)需要头像+两行文字堆叠,因此用枚举描述形状而非
51/// 单一 `&'static str`。
52#[derive(Clone, Copy, PartialEq)]
53pub enum SkeletonCellShape {
54 /// 单个占位块,`SkeletonBox` 的 `class`。
55 Single(&'static str),
56 /// 两行堆叠占位(如标题 + 副标题),依次为两行 `SkeletonBox` 的 `class`。
57 Stacked(&'static str, &'static str),
58 /// 圆形头像占位 + 两行堆叠占位(如「用户」列:头像 + 昵称/邮箱)。
59 AvatarStacked {
60 avatar: &'static str,
61 line1: &'static str,
62 line2: &'static str,
63 },
64}
65
66/// 表格骨架屏一列的规格:表头单元格类名/占位块类名 + 表体单元格类名/占位形状。
67#[derive(Clone, PartialEq)]
68pub struct SkeletonTableColumn {
69 /// 表头 `th` 的 class(含列宽、对齐、响应式隐藏等)
70 pub header_class: &'static str,
71 /// 表头占位块(`SkeletonBox`)的 class
72 pub header_box_class: &'static str,
73 /// 表体 `td` 的 class
74 pub cell_class: &'static str,
75 /// 表体单元格占位形状
76 pub cell_shape: SkeletonCellShape,
77}
78
79/// 通用表格骨架屏:渲染 `<table>` 本身(表头一行 + N 行占位表体),不含外层
80/// 卡片包裹——各调用方的外层卡片形态并不完全一致(有的独立成卡,有的与标题栏
81/// 共享同一张卡,有的嵌套在已带阴影的父卡片内无需再叠一层阴影),故由调用方
82/// 自行用 [`SkeletonCard`] 或既有容器包裹。收敛此前散落在多个后台列表页骨架屏
83/// 里几乎逐字重复的 `thead`/`tbody` 结构。
84///
85/// Props:
86/// - `columns`:每列的表头/表体占位规格
87/// - `rows`:占位行数
88#[component]
89pub fn SkeletonTable(columns: Vec<SkeletonTableColumn>, rows: usize) -> Element {
90 rsx! {
91 table { class: "w-full text-sm",
92 thead {
93 tr { class: "bg-[var(--color-paper-entry)]/80 border-b border-[var(--color-paper-border)]/70",
94 for (i, col) in columns.iter().enumerate() {
95 th { key: "{i}", class: col.header_class,
96 SkeletonBox { class: col.header_box_class }
97 }
98 }
99 }
100 }
101 tbody {
102 for r in 0..rows {
103 tr { key: "{r}", class: "border-b border-[var(--color-paper-border)]/60 last:border-0",
104 for (i, col) in columns.iter().enumerate() {
105 td { key: "{i}", class: col.cell_class,
106 match col.cell_shape {
107 SkeletonCellShape::Single(w) => rsx! {
108 SkeletonBox { class: w }
109 },
110 SkeletonCellShape::Stacked(w1, w2) => rsx! {
111 div { class: "space-y-1.5",
112 SkeletonBox { class: w1 }
113 SkeletonBox { class: w2 }
114 }
115 },
116 SkeletonCellShape::AvatarStacked { avatar, line1, line2 } => rsx! {
117 div { class: "flex items-center gap-2.5",
118 SkeletonBox { class: avatar }
119 div { class: "space-y-1 min-w-0 flex-1",
120 SkeletonBox { class: line1 }
121 SkeletonBox { class: line2 }
122 }
123 }
124 },
125 }
126 }
127 }
128 }
129 }
130 }
131 }
132 }
133}