yggdrasil/pages/
friends.rs1use dioxus::prelude::*;
9
10use crate::api::friends::list_friend_links;
11use crate::components::empty_state::EmptyState;
12use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
13use crate::components::skeletons::friends_skeleton::FriendsSkeleton;
14use crate::models::friend_link::FriendLink;
15
16#[component]
20pub fn Friends() -> Element {
21 rsx! {
22 div { class: "animate-page-enter",
23 header { class: "page-header mb-6",
24 h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
25 "友链"
26 }
27 p { class: "text-paper-secondary mt-2", "交换过链接的伙伴们" }
28 }
29 FriendsContent {}
30 }
31 }
32}
33
34#[component]
38fn FriendsContent() -> Element {
39 let links_res = use_server_future(list_friend_links)?;
40
41 let links_data = links_res.read();
42 match &*links_data {
43 Some(Ok(links)) => {
44 if links.is_empty() {
45 rsx! {
46 EmptyState {
47 title: "还没有友链",
48 description: "在后台「友链」中添加第一位伙伴吧。",
49 }
50 }
51 } else {
52 rsx! {
53 div { class: "mt-2 text-base text-paper-secondary",
54 "共 "
55 span { class: "font-medium text-paper-primary", "{links.len()}" }
56 " 位伙伴"
57 }
58 div { class: "grid grid-cols-1 sm:grid-cols-2 gap-6 mt-6",
59 for link in links.iter() {
60 FriendCard { key: "{link.id}", link: link.clone() }
61 }
62 }
63 }
64 }
65 }
66 Some(Err(e)) => {
67 rsx! {
68 div { class: "text-center text-red-500 dark:text-red-400 py-20", "加载失败: {e}" }
69 }
70 }
71 None => {
72 rsx! {
73 DelayedSkeleton { FriendsSkeleton {} }
74 }
75 }
76 }
77}
78
79#[component]
84fn FriendCard(link: FriendLink) -> Element {
85 let mut img_failed = use_signal(|| false);
87 let initial: String = link
88 .name
89 .chars()
90 .next()
91 .map(|c| c.to_uppercase().collect())
92 .unwrap_or_else(|| "?".to_string());
93
94 rsx! {
95 div { class: "group relative bg-paper-entry rounded-card p-8 shadow-sm border border-transparent hover:border-paper-border hover:-translate-y-0.5 hover:shadow-md transition-all duration-300",
96 div { class: "flex items-start gap-5",
97 div { class: "relative h-14 w-14 shrink-0 rounded-2xl bg-paper-code-bg flex items-center justify-center overflow-hidden",
98 span { class: "text-xl font-semibold text-paper-primary select-none",
99 "{initial}"
100 }
101 if let Some(avatar_url) = &link.avatar_url {
102 if !img_failed() {
103 img {
104 class: "absolute inset-0 w-full h-full object-cover rounded-2xl",
105 src: "{avatar_url}",
106 alt: "{link.name} 的头像",
107 onerror: move |_| img_failed.set(true),
108 }
109 }
110 }
111 }
112 div { class: "flex-1 min-w-0",
113 h3 { class: "text-lg font-semibold text-paper-primary group-hover:text-paper-accent transition-colors truncate",
114 "{link.name}"
115 }
116 if !link.description.is_empty() {
117 p { class: "text-sm text-paper-secondary mt-1.5 leading-relaxed line-clamp-2",
118 "{link.description}"
119 }
120 }
121 }
122 }
123 div { class: "mt-6 flex items-center gap-1",
124 span { class: "text-sm text-paper-tertiary group-hover:text-paper-secondary transition-colors",
125 "访问站点"
126 }
127 span { class: "text-sm text-paper-tertiary group-hover:text-paper-secondary group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform inline-block",
128 "↗"
129 }
130 }
131 a {
132 class: "absolute inset-0 z-10",
133 href: "{link.url}",
134 target: "_blank",
135 rel: "noopener noreferrer",
136 aria_label: "访问 {link.name}",
137 }
138 }
139 }
140}