yggdrasil/src/components/admin_layout.rs
xfy bbd4553467 feat(admin): add dedicated code-runner sandbox page at /admin/runner
不改动 852 行的 write.rs 编辑器布局,改为独立 admin 页面(与 posts/system 同款):
- runner.rs: Runner 页面(语言切换 join 按钮 + overrides JSON 输入 + CodeRunner 沙箱)
  切换语言自动填充示例源码;overrides JSON 实时校验,畸形时提示并忽略
- mod.rs: 注册 runner 模块并导出 Runner
- router.rs: 新增 #[route("/runner")] Runner {} 嵌套在 /admin 下
- admin_layout.rs: 导航新增「试运行」入口
2026-07-03 18:41:05 +08:00

164 lines
7.0 KiB
Rust

//! 后台管理布局组件
//!
//! 提供全新设计的柔和/软扁平化风格的管理员专属后台布局。
//! 采用圆角矩形、大空间距与友好的交互设计。
use dioxus::prelude::*;
use dioxus::router::components::Link;
use crate::api::auth::{get_current_user, logout};
use crate::components::admin_skeleton::AdminDashboardSkeleton;
use crate::components::write_skeleton::WriteSkeleton;
use crate::context::UserContext;
use crate::hooks::delayed_loading::use_delayed_loading;
use crate::router::Route;
use crate::theme::ThemeToggle;
#[component]
pub fn AdminLayout() -> Element {
let mut ctx: UserContext = use_context();
let navigator = dioxus::router::navigator();
let route = use_route::<Route>();
let show_skeleton = use_delayed_loading(move || !(ctx.checked)());
use_effect(move || {
if !(ctx.checked)() {
(ctx.checked).set(true);
spawn(async move {
match get_current_user().await {
Ok(response) => {
if let Some(user) = response.user {
ctx.user.set(Some(std::sync::Arc::new(user)));
} else {
let _ = navigator.push(Route::Login {});
}
}
Err(_) => {
let _ = navigator.push(Route::Login {});
}
}
});
}
});
let admin_nav_items = vec![
(Route::Admin {}, "仪表盘"),
(Route::Write {}, "写文章"),
(Route::Posts {}, "管理文章"),
(Route::Trash {}, "回收站"),
(Route::Runner {}, "试运行"),
(Route::System {}, "系统"),
];
let is_write_route =
matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
let main_class = if is_write_route {
"flex-1 flex flex-col relative w-full h-full overflow-y-auto px-10 py-12 rounded-[2rem] bg-[var(--color-paper-theme)] shadow-sm border border-[var(--color-paper-border)]"
} else {
"flex-1 w-full max-w-7xl mx-auto px-10 py-12"
};
let root_class = "min-h-dvh flex bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] font-sans";
let nav_content = rsx! {
aside { class: "w-64 flex-shrink-0 hidden md:flex flex-col h-screen sticky top-0 p-6 bg-[var(--color-paper-entry)]",
// Logo
div { class: "mb-10 px-4",
Link {
class: "font-extrabold text-2xl tracking-tight text-[var(--color-paper-primary)] hover:text-[var(--color-paper-accent)] transition-colors",
to: Route::Home {},
"Yggdrasil."
}
}
// Nav Items
nav { class: "flex-1 flex flex-col gap-2",
for (dest, label) in admin_nav_items {
{
let is_active = route == dest || (label == "写文章" && is_write_route) || (label == "回收站" && matches!(route, Route::TrashPage { .. }));
let base_class = "flex items-center px-4 py-3 rounded-2xl text-sm font-medium transition-all";
let text_class = if is_active {
"bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] shadow-sm border border-[var(--color-paper-border)]"
} else {
"text-[var(--color-paper-secondary)] hover:bg-[var(--color-paper-theme)]/50 hover:text-[var(--color-paper-primary)] border border-transparent"
};
rsx! {
Link {
key: "{label}",
class: "{base_class} {text_class}",
to: dest,
"{label}"
}
}
}
}
}
// Bottom Tools
div { class: "mt-auto pt-6 border-t border-[var(--color-paper-border)] flex items-center justify-between px-4",
ThemeToggle {}
button {
class: "text-sm font-medium px-4 py-2 rounded-xl 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",
onclick: move |_| {
spawn(async move {
let _ = logout().await;
ctx.user.set(None);
ctx.checked.set(false);
let _ = navigator.push(Route::Login {});
});
},
"退出"
}
}
}
};
match ((ctx.checked)(), (ctx.user)()) {
(true, Some(_)) => {
rsx! {
div { class: "{root_class}",
{nav_content}
div { class: "flex-1 flex flex-col min-w-0 h-screen p-2 md:p-4",
if !is_write_route {
div { class: "flex-1 bg-[var(--color-paper-theme)] rounded-[2rem] shadow-sm border border-[var(--color-paper-border)] overflow-y-auto relative flex flex-col",
main { class: "{main_class}", Outlet::<Route> {} }
}
} else {
main { class: "{main_class}", Outlet::<Route> {} }
}
}
}
}
}
(true, None) => {
rsx! {
div { class: "{root_class}",
div { class: "flex-1 flex items-center justify-center font-medium text-sm text-[var(--color-paper-secondary)]",
"正在验证身份..."
}
}
}
}
(false, _) => {
rsx! {
div { class: "{root_class}",
{nav_content}
div { class: "flex-1 flex flex-col min-w-0 h-screen p-2 md:p-4",
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",
main { class: "{main_class}",
div { class: if show_skeleton() { "p-10" } else { "opacity-0" },
{
match route {
Route::Write {} => rsx! { WriteSkeleton {} },
_ => rsx! { AdminDashboardSkeleton {} },
}
}
}
}
}
}
}
}
}
}
}