yggdrasil/components/
admin_layout.rs1use dioxus::prelude::*;
7use dioxus::router::components::Link;
8
9use crate::api::auth::{get_current_user, logout};
10use crate::components::admin_skeleton::AdminDashboardSkeleton;
11
12use crate::components::write_skeleton::WriteSkeleton;
13use crate::context::UserContext;
14use crate::router::Route;
15use crate::theme::ThemeToggle;
16
17#[component]
18pub fn AdminLayout() -> Element {
19 let mut ctx: UserContext = use_context();
20 let navigator = dioxus::router::navigator();
21 let route = use_route::<Route>();
22
23 use_effect(move || {
24 if !(ctx.checked)() {
25 (ctx.checked).set(true);
26 spawn(async move {
27 match get_current_user().await {
28 Ok(response) => {
29 if let Some(user) = response.user {
30 ctx.user.set(Some(std::sync::Arc::new(user)));
31 } else {
32 let _ = navigator.push(Route::Login {});
33 }
34 }
35 Err(_) => {
36 let _ = navigator.push(Route::Login {});
37 }
38 }
39 });
40 }
41 });
42
43 let admin_nav_items = vec![
44 (Route::Admin {}, "仪表盘"),
45 (Route::Write {}, "写文章"),
46 (Route::Posts {}, "管理文章"),
47 (Route::Assets {}, "素材"),
48 (Route::FriendsAdmin {}, "友链"),
49 (Route::Runner {}, "试运行"),
50 (Route::Mcp {}, "MCP"),
51 (Route::System {}, "系统"),
52 ];
53
54 let is_write_route =
55 matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
56 let is_posts_route = matches!(route, Route::Posts {});
58
59 let card_overflow = if is_write_route {
64 "overflow-hidden"
65 } else {
66 "overflow-y-auto"
67 };
68 let main_class = if is_write_route {
69 "flex-1 w-full max-w-7xl mx-auto flex flex-col min-h-0"
70 } else {
71 "flex-1 w-full max-w-7xl mx-auto px-6 py-12"
72 };
73
74 let root_class =
75 "min-h-dvh flex bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] font-sans";
76
77 let nav_content = rsx! {
78 aside { class: "w-64 flex-shrink-0 hidden md:flex flex-col h-screen sticky top-0 p-4 bg-[var(--color-paper-entry)]",
79 div { class: "mb-10 px-4",
81 Link {
82 class: "font-extrabold text-2xl tracking-tight text-[var(--color-paper-primary)] hover:text-[var(--color-paper-accent)] transition-colors",
83 to: Route::Home {},
84 "Yggdrasil."
85 }
86 }
87 nav { class: "flex-1 flex flex-col gap-2",
89 for (dest, label) in admin_nav_items {
90 {
91 let is_active = route == dest
92 || (label == "写文章" && is_write_route)
93 || (label == "管理文章" && is_posts_route);
94 let base_class = "flex items-center px-4 py-3 rounded-2xl text-sm font-medium transition-all";
95 let text_class = if is_active {
96 "bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] shadow-sm border border-[var(--color-paper-border)]"
97 } else {
98 "text-[var(--color-paper-secondary)] hover:bg-[var(--color-paper-theme)]/50 hover:text-[var(--color-paper-primary)] border border-transparent"
99 };
100 rsx! {
101 Link { key: "{label}", class: "{base_class} {text_class}", to: dest, "{label}" }
102 }
103 }
104 }
105 }
106 div { class: "mt-auto pt-6 border-t border-[var(--color-paper-border)] flex items-center justify-between px-4",
108 ThemeToggle {}
109 button {
110 class: "text-sm font-medium px-4 py-2 rounded-2xl bg-[var(--color-paper-theme)] border border-[var(--color-paper-border)] shadow-sm hover:shadow-md transition-all text-[var(--color-paper-secondary)] hover:text-red-500 cursor-pointer",
111 onclick: move |_| {
112 spawn(async move {
113 let _ = logout().await;
114 ctx.user.set(None);
115 ctx.checked.set(false);
116 let _ = navigator.push(Route::Login {});
117 });
118 },
119 "退出"
120 }
121 }
122 }
123 };
124
125 match ((ctx.checked)(), (ctx.user)()) {
126 (true, Some(_)) => {
127 rsx! {
128 div { class: "{root_class}",
129 {nav_content}
130 div { class: "flex-1 flex flex-col min-w-0 h-screen p-2 md:p-4",
131 div { class: "flex-1 bg-[var(--color-paper-theme)] rounded-[2rem] shadow-sm border border-[var(--color-paper-border)] {card_overflow} relative flex flex-col",
132 main { class: "{main_class}", Outlet::<Route> {} }
133 }
134 }
135 }
136 }
137 }
138 _ => {
139 rsx! {
140 div { class: "{root_class}",
141 {nav_content}
142 div { class: "flex-1 flex flex-col min-w-0 h-screen p-2 md:p-4",
143 div { class: "flex-1 bg-[var(--color-paper-theme)] rounded-[2rem] shadow-sm border border-[var(--color-paper-border)] overflow-hidden relative flex flex-col",
144 main { class: "{main_class}",
145 div { class: "flex-1 min-h-0 flex flex-col animate-pulse",
148 {
149 match route {
150 Route::Write {} | Route::WriteEdit { .. } => rsx! {
151 WriteSkeleton {}
152 },
153 _ => rsx! {
154 AdminDashboardSkeleton {}
155 },
156 }
157 }
158 }
159 }
160 }
161 }
162 }
163 }
164 }
165 }
166}