不改动 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: 导航新增「试运行」入口
164 lines
7.0 KiB
Rust
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 {} },
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|