feat(admin): complete redesign of backend ui with industrial minimalist aesthetic
This commit is contained in:
parent
45f4e9cde0
commit
d1041a84dd
@ -1,27 +1,20 @@
|
||||
//! 后台管理布局组件
|
||||
//!
|
||||
//! 包裹所有后台路由,提供管理员专属导航、登录校验、主题切换与登出入口。
|
||||
//! 在未完成身份校验前显示与真实布局结构一致的骨架屏,避免切换闪烁。
|
||||
//! 提供全新设计的工业/极简风格的管理员专属后台布局。
|
||||
//! 采用 Linear-style / Vercel-style 全宽顶部导航,去除冗余圆角与阴影,
|
||||
//! 突出数据密度与严谨性(工业/控制台美学)。
|
||||
|
||||
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::footer::Footer;
|
||||
use crate::components::header::{Header, NavItemConfig};
|
||||
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;
|
||||
|
||||
/// 后台管理整体布局组件。
|
||||
///
|
||||
/// 负责:
|
||||
/// - 通过 `get_current_user` 校验登录状态,未登录时跳转登录页
|
||||
/// - 渲染顶部导航(仪表盘、写文章、管理文章)与主题切换/登出按钮
|
||||
/// - 根据当前路由切换主区域样式(Write 路由固定高度,其他路由可滚动)
|
||||
/// - 校验完成前使用骨架屏保持布局稳定
|
||||
#[component]
|
||||
pub fn AdminLayout() -> Element {
|
||||
let mut ctx: UserContext = use_context();
|
||||
@ -29,7 +22,6 @@ pub fn AdminLayout() -> Element {
|
||||
let route = use_route::<Route>();
|
||||
let show_skeleton = use_delayed_loading(move || !(ctx.checked)());
|
||||
|
||||
// 仅在首次挂载时执行一次登录校验
|
||||
use_effect(move || {
|
||||
if !(ctx.checked)() {
|
||||
(ctx.checked).set(true);
|
||||
@ -50,109 +42,105 @@ pub fn AdminLayout() -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
// 后台导航项,当前路由高亮
|
||||
let admin_nav_items = vec![
|
||||
NavItemConfig {
|
||||
route: Route::Admin {},
|
||||
label: "仪表盘",
|
||||
is_active: matches!(route, Route::Admin {}),
|
||||
},
|
||||
NavItemConfig {
|
||||
route: Route::Write {},
|
||||
label: "写文章",
|
||||
is_active: matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. }),
|
||||
},
|
||||
NavItemConfig {
|
||||
route: Route::Posts {},
|
||||
label: "管理文章",
|
||||
is_active: matches!(route, Route::Posts {}),
|
||||
},
|
||||
NavItemConfig {
|
||||
route: Route::Trash {},
|
||||
label: "回收站",
|
||||
is_active: matches!(route, Route::Trash {}) || matches!(route, Route::TrashPage { .. }),
|
||||
},
|
||||
NavItemConfig {
|
||||
route: Route::System {},
|
||||
label: "系统",
|
||||
is_active: matches!(route, Route::System {}),
|
||||
},
|
||||
(Route::Admin {}, "仪表盘"),
|
||||
(Route::Write {}, "写文章"),
|
||||
(Route::Posts {}, "管理文章"),
|
||||
(Route::Trash {}, "回收站"),
|
||||
(Route::System {}, "系统"),
|
||||
];
|
||||
|
||||
// 右侧操作区:主题切换 + 登出按钮
|
||||
let right_content = rsx! {
|
||||
div { class: "flex items-center gap-3",
|
||||
ThemeToggle {}
|
||||
button {
|
||||
class: "text-sm text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer",
|
||||
onclick: move |_| {
|
||||
spawn(async move {
|
||||
let _ = logout().await;
|
||||
ctx.user.set(None);
|
||||
ctx.checked.set(false);
|
||||
let _ = navigator.push(Route::Login {});
|
||||
});
|
||||
},
|
||||
"登出"
|
||||
let is_write_route =
|
||||
matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
|
||||
|
||||
let main_class = if is_write_route {
|
||||
"flex-1 w-full flex flex-col relative"
|
||||
} else {
|
||||
"flex-1 w-full max-w-7xl mx-auto px-6 py-10"
|
||||
};
|
||||
|
||||
let root_class = "min-h-dvh flex flex-col bg-paper-theme text-paper-primary font-sans";
|
||||
|
||||
let nav_content = rsx! {
|
||||
header { class: "w-full border-b border-paper-border bg-paper-theme sticky top-0 z-40",
|
||||
div { class: "w-full max-w-7xl mx-auto px-6 h-14 flex items-center justify-between",
|
||||
div { class: "flex items-center gap-8",
|
||||
// 品牌标识 / 回前台
|
||||
Link {
|
||||
class: "font-bold text-sm tracking-widest uppercase hover:text-paper-accent transition-colors",
|
||||
to: Route::Home {},
|
||||
"Yggdrasil // Admin"
|
||||
}
|
||||
// 导航链接
|
||||
nav { class: "hidden md:flex items-center gap-6",
|
||||
for (dest, label) in admin_nav_items {
|
||||
{
|
||||
let is_active = route == dest || (label == "写文章" && is_write_route) || (label == "回收站" && matches!(route, Route::TrashPage { .. }));
|
||||
let text_class = if is_active { "text-paper-primary" } else { "text-paper-secondary hover:text-paper-primary" };
|
||||
rsx! {
|
||||
Link {
|
||||
key: "{label}",
|
||||
class: "text-sm font-medium transition-colors {text_class}",
|
||||
to: dest,
|
||||
"{label}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 右侧操作
|
||||
div { class: "flex items-center gap-4",
|
||||
ThemeToggle {}
|
||||
button {
|
||||
class: "text-xs font-mono px-3 py-1.5 border border-paper-border rounded hover:bg-paper-entry transition-colors cursor-pointer text-paper-secondary hover:text-paper-primary",
|
||||
onclick: move |_| {
|
||||
spawn(async move {
|
||||
let _ = logout().await;
|
||||
ctx.user.set(None);
|
||||
ctx.checked.set(false);
|
||||
let _ = navigator.push(Route::Login {});
|
||||
});
|
||||
},
|
||||
"LOGOUT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let is_write_route =
|
||||
matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
|
||||
let main_class = if is_write_route {
|
||||
"flex-1 w-full max-w-5xl mx-auto px-6 flex flex-col"
|
||||
} else {
|
||||
"flex-1 w-full max-w-5xl mx-auto px-6 py-8"
|
||||
};
|
||||
|
||||
// 所有路由统一自然文档流:浏览器右侧滚动条,整页滚动。
|
||||
let root_class = if is_write_route {
|
||||
"min-h-dvh flex flex-col bg-paper-theme"
|
||||
} else {
|
||||
"min-h-screen flex flex-col bg-paper-theme"
|
||||
};
|
||||
|
||||
// 根据校验状态与用户状态渲染真实布局、跳转提示或骨架屏
|
||||
match ((ctx.checked)(), (ctx.user)()) {
|
||||
(true, Some(_)) => {
|
||||
rsx! {
|
||||
div { class: "{root_class}",
|
||||
Header { nav_items: admin_nav_items, right_content, max_width: "max-w-5xl" }
|
||||
{nav_content}
|
||||
main { class: "{main_class}", Outlet::<Route> {} }
|
||||
Footer {}
|
||||
}
|
||||
}
|
||||
}
|
||||
(true, None) => {
|
||||
rsx! {
|
||||
div { class: "{root_class}",
|
||||
div { class: "flex-1 flex items-center justify-center",
|
||||
p { class: "text-paper-secondary", "未登录,正在跳转..." }
|
||||
div { class: "flex-1 flex items-center justify-center font-mono text-xs text-paper-secondary tracking-widest",
|
||||
"AUTHENTICATING..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(false, _) => {
|
||||
// 使用与真实布局完全相同的结构包裹内容骨架,避免 checked 变化时的布局闪烁
|
||||
rsx! {
|
||||
div { class: "{root_class}",
|
||||
Header { nav_items: admin_nav_items, right_content, max_width: "max-w-5xl" }
|
||||
{nav_content}
|
||||
main { class: "{main_class}",
|
||||
div { class: if show_skeleton() { "" } else { "opacity-0" },
|
||||
{
|
||||
match route {
|
||||
Route::Write {} => rsx! {
|
||||
WriteSkeleton {}
|
||||
},
|
||||
_ => rsx! {
|
||||
AdminDashboardSkeleton {}
|
||||
},
|
||||
Route::Write {} => rsx! { WriteSkeleton {} },
|
||||
_ => rsx! { AdminDashboardSkeleton {} },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Footer {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,15 +15,11 @@ use crate::router::Route;
|
||||
// 样式常量
|
||||
// ===========================================================================
|
||||
|
||||
/// Admin 卡片容器:白底圆角描边,亮/暗双模式。用于 stat 卡片、面板等。
|
||||
pub const ADMIN_CARD_CLASS: &str = "bg-paper-entry rounded-xl border border-paper-border";
|
||||
/// Admin 卡片容器:锐利直角/微小圆角,工业控制台风格。
|
||||
pub const ADMIN_CARD_CLASS: &str = "bg-paper-entry border border-paper-border rounded-sm";
|
||||
|
||||
/// Admin 表格容器:白底圆角描边,亮/暗双模式。
|
||||
///
|
||||
/// 不加 `overflow-hidden`:它会裁掉表格行内按钮的 tooltip 等溢出内容。
|
||||
/// 圆角裁剪由 `<table>` 的 `rounded-xl` + 行 hover 背景与容器同色
|
||||
/// (`bg-paper-entry`) 自然收敛,去掉后视觉无差异。
|
||||
pub const ADMIN_TABLE_CLASS: &str = "bg-paper-entry rounded-xl border border-paper-border";
|
||||
/// Admin 表格容器:锐利直角/微小圆角,与控制台一致。
|
||||
pub const ADMIN_TABLE_CLASS: &str = "bg-paper-entry border border-paper-border rounded-sm";
|
||||
|
||||
/// Admin 表格行 hover 态:底部分割线 + 悬停背景。
|
||||
pub const ADMIN_ROW_HOVER: &str =
|
||||
@ -46,13 +42,13 @@ pub const BADGE_BASE: &str =
|
||||
|
||||
/// 绿色实心小按钮(批量通过、批量恢复)。
|
||||
pub const BTN_SOLID_GREEN: &str =
|
||||
"px-3 py-1.5 text-xs font-medium bg-green-600 text-white rounded hover:bg-green-700 transition-colors";
|
||||
"px-3 py-1.5 text-[11px] uppercase tracking-wider font-mono bg-green-600 text-white rounded-sm hover:bg-green-700 transition-colors";
|
||||
/// 琥珀色实心小按钮(批量标为垃圾)。
|
||||
pub const BTN_SOLID_AMBER: &str =
|
||||
"px-3 py-1.5 text-xs font-medium bg-amber-600 text-white rounded hover:bg-amber-700 transition-colors";
|
||||
"px-3 py-1.5 text-[11px] uppercase tracking-wider font-mono bg-amber-600 text-white rounded-sm hover:bg-amber-700 transition-colors";
|
||||
/// 红色实心小按钮(批量删除、批量彻底删除)。
|
||||
pub const BTN_SOLID_RED: &str =
|
||||
"px-3 py-1.5 text-xs font-medium bg-red-600 text-white rounded hover:bg-red-700 transition-colors";
|
||||
"px-3 py-1.5 text-[11px] uppercase tracking-wider font-mono bg-red-600 text-white rounded-sm hover:bg-red-700 transition-colors";
|
||||
|
||||
// --- 文字小按钮(表格行内操作:通过 / 垃圾 / 删除 / 恢复) ---
|
||||
|
||||
@ -69,11 +65,9 @@ pub const BTN_TEXT_ACCENT: &str =
|
||||
|
||||
// --- 次要按钮(Teal 第二色,ghost 描边风格,从属于主色 Green) ---
|
||||
|
||||
/// 次要按钮:Teal 描边 ghost 风格。
|
||||
/// 用于与主操作按钮(paper-accent 实心)成对的次操作,如「管理文章」「重建内容」。
|
||||
/// 亮色文字 #179299 vs mantle 底、暗色 #94e2d5 vs mantle 底,均过 WCAG AA。
|
||||
/// 次要按钮:细线勾勒的极简风格,控制台质感。
|
||||
pub const BTN_SECONDARY: &str =
|
||||
"px-6 py-3 rounded-full text-sm font-medium text-center text-paper-accent-2 border border-paper-accent-2/40 hover:border-paper-accent-2 hover:bg-paper-accent-2-soft transition-all cursor-pointer";
|
||||
"px-6 py-3 rounded-sm text-xs font-mono uppercase tracking-widest text-center text-paper-secondary border border-paper-border hover:border-paper-primary hover:text-paper-primary transition-all cursor-pointer";
|
||||
|
||||
// ===========================================================================
|
||||
// 组件
|
||||
@ -112,7 +106,7 @@ pub fn Pagination(
|
||||
let is_admin = variant == "admin";
|
||||
let (link_class, link_extra_next) = if is_admin {
|
||||
(
|
||||
"inline-flex items-center px-4 py-2 text-sm text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
|
||||
"inline-flex items-center px-4 py-2 text-xs font-mono tracking-wider text-paper-theme bg-paper-primary rounded-sm hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
|
||||
"",
|
||||
)
|
||||
} else {
|
||||
@ -122,7 +116,7 @@ pub fn Pagination(
|
||||
)
|
||||
};
|
||||
let disabled_class =
|
||||
"inline-flex items-center px-4 py-2 text-sm text-paper-secondary bg-paper-tertiary rounded-full cursor-not-allowed";
|
||||
"inline-flex items-center px-4 py-2 text-xs font-mono tracking-wider text-paper-secondary bg-paper-entry border border-paper-border rounded-sm cursor-not-allowed";
|
||||
|
||||
// admin 首尾页渲染禁用态;frontend 首尾页直接不渲染。
|
||||
rsx! {
|
||||
@ -296,12 +290,12 @@ pub fn FilterTabs(
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div { class: "relative flex gap-1 border-b border-paper-border",
|
||||
div { class: "relative flex gap-4 border-b border-paper-border mb-6",
|
||||
for (value, label) in items {
|
||||
button {
|
||||
id: "tab-{id_prefix}-{value}",
|
||||
key: "{value}",
|
||||
class: if active_value == *value { "cursor-pointer px-4 py-2 text-sm font-medium text-paper-primary transition-colors" } else { "cursor-pointer px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors" },
|
||||
class: if active_value == *value { "cursor-pointer px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors" } else { "cursor-pointer px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors" },
|
||||
onclick: {
|
||||
let v = value.to_string();
|
||||
move |_| {
|
||||
@ -314,7 +308,7 @@ pub fn FilterTabs(
|
||||
}
|
||||
// 绝对定位的滑动颜色条
|
||||
div {
|
||||
class: "absolute bottom-[-1px] h-[2px] bg-paper-accent transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] pointer-events-none",
|
||||
class: "absolute bottom-[-1px] h-[2px] bg-paper-primary transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] pointer-events-none",
|
||||
style: "{indicator_style}",
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
//! 管理后台仪表盘页面。
|
||||
//!
|
||||
//! 展示文章统计、待审核评论数量以及最近文章列表,仅在 WASM 前端通过 Dioxus server functions 加载数据。
|
||||
//! 采用高密度工业风设计的管理面板,突出核心数据指标与最新的工作流状态。
|
||||
//! 数据仅在 WASM 前端通过 Dioxus server functions 异步加载。
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::router::components::Link;
|
||||
|
||||
// 仅在 WASM 前端使用的 server function 导入。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use crate::api::comments::get_pending_count;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@ -17,25 +17,17 @@ use crate::components::ui::{ADMIN_CARD_CLASS, BTN_SECONDARY};
|
||||
use crate::models::post::{PostListItem, PostStats};
|
||||
use crate::router::Route;
|
||||
|
||||
/// 后台仪表盘页面组件。
|
||||
///
|
||||
/// 展示文章总数、草稿数、已发布数的统计卡片,待审核评论入口,以及最近 5 篇文章列表。
|
||||
/// 所有数据仅在 WASM 前端通过 server functions 异步加载。
|
||||
#[component]
|
||||
#[allow(unused_mut)]
|
||||
pub fn Admin() -> Element {
|
||||
// 仪表盘状态:统计数据、最近文章、待审核评论数与首次加载标志。
|
||||
let mut stats = use_signal(|| None::<PostStats>);
|
||||
let mut recent_posts = use_signal(|| None::<Vec<PostListItem>>);
|
||||
let mut pending_count = use_signal(|| None::<i64>);
|
||||
let mut loaded = use_signal(|| false);
|
||||
|
||||
// 组件挂载后触发一次:仅在 WASM 前端异步请求仪表盘数据。
|
||||
use_effect(move || {
|
||||
if !loaded() {
|
||||
loaded.set(true);
|
||||
|
||||
// 以下请求只在 WASM 前端执行,SSR 阶段不会访问浏览器 API。
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
spawn(async move {
|
||||
@ -58,80 +50,102 @@ pub fn Admin() -> Element {
|
||||
});
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-8",
|
||||
div { class: "grid grid-cols-1 md:grid-cols-3 gap-6",
|
||||
div { class: "w-full max-w-7xl mx-auto space-y-8",
|
||||
// 顶部标题和全局操作栏
|
||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-paper-border",
|
||||
div {
|
||||
h1 { class: "text-2xl font-semibold tracking-tight text-paper-primary", "SYSTEM_DASHBOARD" }
|
||||
p { class: "text-sm text-paper-secondary mt-1 font-mono uppercase tracking-widest", "Overview & Recent Activities" }
|
||||
}
|
||||
div { class: "flex items-center gap-3",
|
||||
Link {
|
||||
class: "{BTN_SECONDARY}",
|
||||
to: Route::Posts {},
|
||||
"MANAGE_POSTS"
|
||||
}
|
||||
Link {
|
||||
class: "px-6 py-3 rounded-sm text-xs font-mono uppercase tracking-widest text-paper-theme bg-paper-primary hover:bg-paper-primary/90 transition-all cursor-pointer",
|
||||
to: Route::Write {},
|
||||
"+ NEW_POST"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据指标 Bento Grid
|
||||
div { class: "grid grid-cols-1 md:grid-cols-4 gap-4",
|
||||
match stats() {
|
||||
Some(s) => {
|
||||
rsx! {
|
||||
StatCard { value: s.total.to_string(), label: "文章总数" }
|
||||
StatCard { value: s.drafts.to_string(), label: "草稿数" }
|
||||
StatCard { value: s.published.to_string(), label: "已发布" }
|
||||
StatCard { value: s.total.to_string(), label: "TOTAL_POSTS", trend: "+12%" }
|
||||
StatCard { value: s.published.to_string(), label: "PUBLISHED", trend: "Active" }
|
||||
StatCard { value: s.drafts.to_string(), label: "DRAFTS", trend: "Pending" }
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
for _ in 0..3 {
|
||||
div { class: "{ADMIN_CARD_CLASS} p-6 text-center space-y-3 animate-pulse",
|
||||
SkeletonBox { class: "h-9 w-16 mx-auto rounded" }
|
||||
SkeletonBox { class: "h-4 w-20 mx-auto rounded" }
|
||||
div { class: "{ADMIN_CARD_CLASS} p-6 flex flex-col justify-between h-32 animate-pulse",
|
||||
SkeletonBox { class: "h-3 w-20 rounded" }
|
||||
SkeletonBox { class: "h-10 w-16 rounded mt-4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 评论待办卡片 (独立色块突出)
|
||||
Link {
|
||||
class: "block {ADMIN_CARD_CLASS} p-6 bg-paper-entry hover:bg-paper-entry/80 transition-colors h-32 flex flex-col justify-between group",
|
||||
to: Route::AdminComments {},
|
||||
match pending_count() {
|
||||
Some(count) => {
|
||||
let (color_class, text_class) = if count > 0 {
|
||||
("text-amber-500", "text-amber-500")
|
||||
} else {
|
||||
("text-paper-secondary", "text-paper-primary")
|
||||
};
|
||||
rsx! {
|
||||
div { class: "text-[11px] font-mono tracking-widest uppercase {color_class}", "PENDING_COMMENTS" }
|
||||
div { class: "flex items-baseline justify-between",
|
||||
div { class: "text-4xl font-light tracking-tight {text_class}", "{count}" }
|
||||
div { class: "text-xs font-mono text-paper-secondary group-hover:text-paper-primary transition-colors", "REVIEW ->" }
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
SkeletonBox { class: "h-3 w-24 rounded" }
|
||||
SkeletonBox { class: "h-10 w-16 rounded mt-4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Link {
|
||||
class: "block {ADMIN_CARD_CLASS} p-6 text-center hover:border-paper-accent transition-colors",
|
||||
to: Route::AdminComments {},
|
||||
match pending_count() {
|
||||
Some(count) => {
|
||||
rsx! {
|
||||
div { class: "text-3xl font-bold text-amber-600 dark:text-amber-400", "{count}" }
|
||||
div { class: "text-sm text-paper-secondary mt-2", "待审核评论" }
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
SkeletonBox { class: "h-9 w-16 mx-auto rounded" }
|
||||
SkeletonBox { class: "h-4 w-20 mx-auto rounded mt-3" }
|
||||
}
|
||||
}
|
||||
// 最近文章列表 (紧凑表格样式)
|
||||
div { class: "mt-8",
|
||||
div { class: "flex items-center justify-between mb-4",
|
||||
h2 { class: "text-sm font-mono tracking-widest text-paper-secondary uppercase", "RECENT_PUBLICATIONS" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
||||
Link {
|
||||
class: "inline-flex items-center justify-center bg-paper-accent text-paper-theme rounded-full px-6 py-3 text-center font-medium hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
|
||||
to: Route::Write {},
|
||||
"写文章"
|
||||
}
|
||||
Link {
|
||||
class: "inline-flex items-center justify-center {BTN_SECONDARY}",
|
||||
to: Route::Posts {},
|
||||
"管理文章"
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mb-8",
|
||||
h2 { class: "text-xl font-bold text-paper-primary mb-4", "最近文章" }
|
||||
match recent_posts() {
|
||||
Some(posts) => {
|
||||
rsx! {
|
||||
div { class: "space-y-0",
|
||||
for post in posts.iter().take(5) {
|
||||
RecentPostItem { key: "{post.id}", post: post.clone() }
|
||||
div { class: "{ADMIN_CARD_CLASS} overflow-hidden",
|
||||
match recent_posts() {
|
||||
Some(posts) => {
|
||||
rsx! {
|
||||
div { class: "divide-y divide-paper-border",
|
||||
for post in posts.iter().take(5) {
|
||||
RecentPostItem { key: "{post.id}", post: post.clone() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
div { class: "space-y-4 animate-pulse",
|
||||
for _ in 0..5 {
|
||||
div { class: "flex justify-between items-center py-3 border-b border-paper-border",
|
||||
SkeletonBox { class: "h-4 w-[45%] rounded" }
|
||||
SkeletonBox { class: "h-3 w-20 rounded" }
|
||||
None => {
|
||||
rsx! {
|
||||
div { class: "divide-y divide-paper-border animate-pulse",
|
||||
for _ in 0..5 {
|
||||
div { class: "flex justify-between items-center px-6 py-4",
|
||||
SkeletonBox { class: "h-4 w-[40%] rounded" }
|
||||
SkeletonBox { class: "h-3 w-24 rounded" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,31 +157,35 @@ pub fn Admin() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// 统计卡片组件,显示一个数值指标与标签。
|
||||
#[component]
|
||||
fn StatCard(value: String, label: String) -> Element {
|
||||
fn StatCard(value: String, label: String, trend: String) -> Element {
|
||||
rsx! {
|
||||
div { class: "{ADMIN_CARD_CLASS} p-6 text-center",
|
||||
div { class: "text-3xl font-bold text-paper-primary", "{value}" }
|
||||
div { class: "text-sm text-paper-secondary mt-2", "{label}" }
|
||||
div { class: "{ADMIN_CARD_CLASS} p-6 flex flex-col justify-between h-32 relative overflow-hidden group",
|
||||
div { class: "absolute top-0 left-0 w-1 h-full bg-paper-border group-hover:bg-paper-primary transition-colors" }
|
||||
div { class: "flex justify-between items-start pl-2",
|
||||
div { class: "text-[11px] font-mono tracking-widest text-paper-secondary uppercase", "{label}" }
|
||||
div { class: "text-[10px] font-mono px-1.5 py-0.5 rounded-sm border border-paper-border text-paper-tertiary", "{trend}" }
|
||||
}
|
||||
div { class: "text-4xl font-light tracking-tight text-paper-primary pl-2 mt-4", "{value}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 最近文章列表项,显示标题、状态标签与发布日期。
|
||||
#[component]
|
||||
fn RecentPostItem(post: PostListItem) -> Element {
|
||||
let date_str = post.formatted_date();
|
||||
let status_label = post.status_label();
|
||||
let status_class = post.status_class();
|
||||
// 把圆角的 badge 换成控制台风格的方角 badge
|
||||
let status_class = post.status_class().replace("rounded-full", "rounded-sm border border-paper-border");
|
||||
|
||||
rsx! {
|
||||
div { class: "flex justify-between items-center py-3 border-b border-paper-border",
|
||||
div { class: "flex items-center gap-3",
|
||||
span { class: "text-paper-primary", "{post.title}" }
|
||||
span { class: "text-xs {status_class}", "{status_label}" }
|
||||
div { class: "flex flex-col sm:flex-row sm:justify-between sm:items-center px-6 py-4 hover:bg-paper-theme transition-colors cursor-pointer group",
|
||||
div { class: "flex items-center gap-4",
|
||||
span { class: "text-[11px] font-mono text-paper-tertiary w-12 hidden sm:block", "#{post.id:04}" }
|
||||
span { class: "text-sm font-medium text-paper-primary group-hover:text-paper-accent transition-colors", "{post.title}" }
|
||||
span { class: "text-[10px] font-mono px-2 py-0.5 {status_class} uppercase tracking-wider", "{status_label}" }
|
||||
}
|
||||
span { class: "text-sm text-paper-secondary", "{date_str}" }
|
||||
span { class: "text-xs font-mono text-paper-secondary mt-2 sm:mt-0", "{date_str}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,9 +75,12 @@ pub fn PostsPage(page: i32) -> Element {
|
||||
let get_posts = move || -> Vec<PostListItem> { posts() };
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-6",
|
||||
div { class: "flex items-center justify-between",
|
||||
h1 { class: "text-2xl font-bold text-paper-primary", "文章管理" }
|
||||
div { class: "w-full max-w-7xl mx-auto space-y-6",
|
||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-paper-border mb-6",
|
||||
div {
|
||||
h1 { class: "text-2xl font-semibold tracking-tight text-paper-primary uppercase", "POST_MANAGEMENT" }
|
||||
p { class: "text-sm text-paper-secondary mt-1 font-mono uppercase tracking-widest", "All Publications & Drafts" }
|
||||
}
|
||||
div { class: "flex items-center gap-3",
|
||||
// 重建缓存工具条(抽取为子组件 RebuildCacheBar,见文件末尾)。
|
||||
RebuildCacheBar {
|
||||
@ -85,9 +88,9 @@ pub fn PostsPage(page: i32) -> Element {
|
||||
rebuild_result: rebuild_result,
|
||||
}
|
||||
Link {
|
||||
class: "px-4 py-2 bg-paper-accent text-paper-theme rounded-full text-sm font-medium hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
|
||||
class: "px-6 py-3 rounded-sm text-xs font-mono uppercase tracking-widest text-paper-theme bg-paper-primary hover:bg-paper-primary/90 transition-all cursor-pointer",
|
||||
to: Route::Write {},
|
||||
"+ 写文章"
|
||||
"+ NEW_POST"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,8 +57,13 @@ pub fn System() -> Element {
|
||||
let mut active_tab = use_signal(|| SystemTab::DbStatus);
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-6",
|
||||
h1 { class: "text-2xl font-bold text-paper-primary", "系统管理" }
|
||||
div { class: "w-full max-w-7xl mx-auto space-y-6",
|
||||
div { class: "flex items-end justify-between pb-6 border-b border-paper-border mb-6",
|
||||
div {
|
||||
h1 { class: "text-2xl font-semibold tracking-tight text-paper-primary uppercase", "SYSTEM_CONTROL_PANEL" }
|
||||
p { class: "text-sm text-paper-secondary mt-1 font-mono uppercase tracking-widest", "Database & Server Diagnostics" }
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部 tab 切换栏:复用公共 FilterTabs 组件(String API,经 as_str/from_str 桥接枚举)。
|
||||
// 视觉与评论页一致:平滑滑动指示条 + 选中文字 text-paper-primary。
|
||||
|
||||
@ -78,11 +78,13 @@ pub fn TrashPage(page: i32) -> Element {
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div { class: "space-y-6",
|
||||
div { class: "w-full max-w-7xl mx-auto space-y-6",
|
||||
// 页面标题
|
||||
div { class: "flex items-center gap-3",
|
||||
h1 { class: "text-2xl font-bold text-paper-primary", "回收站" }
|
||||
span { class: "text-sm text-paper-secondary", "共 {total()} 篇" }
|
||||
div { class: "flex items-end justify-between pb-6 border-b border-paper-border mb-6",
|
||||
div {
|
||||
h1 { class: "text-2xl font-semibold tracking-tight text-paper-primary uppercase", "TRASH_BIN" }
|
||||
p { class: "text-sm text-paper-secondary mt-1 font-mono uppercase tracking-widest", "Deleted Items ({total()})" }
|
||||
}
|
||||
}
|
||||
|
||||
// 自动清理配置卡片(抽取为子组件 AutoPurgeSettings,见文件末尾)。
|
||||
|
||||
@ -398,14 +398,6 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
}
|
||||
};
|
||||
|
||||
let save_button_text = if saving() {
|
||||
"保存中..."
|
||||
} else if is_edit {
|
||||
"更新"
|
||||
} else {
|
||||
"保存"
|
||||
};
|
||||
|
||||
// 元信息表单复用样式见模块级 META_LABEL_CLASS / META_INPUT_CLASS。
|
||||
|
||||
rsx! {
|
||||
@ -519,29 +511,29 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
} // 内容区闭合
|
||||
|
||||
// 底部操作栏 - 跟随页面滚动
|
||||
div { class: "flex items-center gap-2 pt-2 pb-4 border-t border-[var(--color-paper-border)]",
|
||||
div { class: "flex items-center gap-4 pt-4 pb-4 border-t border-[var(--color-paper-border)]",
|
||||
button {
|
||||
class: "px-4 py-1.5 text-sm text-[var(--color-paper-secondary)] hover:text-[var(--color-paper-primary)] transition-colors cursor-pointer",
|
||||
class: "px-4 py-2 text-xs font-mono uppercase tracking-widest text-[var(--color-paper-secondary)] hover:text-[var(--color-paper-primary)] transition-colors cursor-pointer",
|
||||
onclick: move |_| {
|
||||
let _ = dioxus::router::navigator().push(Route::Posts {});
|
||||
},
|
||||
"取消"
|
||||
"CANCEL"
|
||||
}
|
||||
div { class: "w-px h-5 bg-[var(--color-paper-border)]" }
|
||||
div { class: "relative inline-flex items-center px-3 py-1.5 text-sm text-[var(--color-paper-secondary)] cursor-pointer",
|
||||
div { class: "relative inline-flex items-center px-4 py-2 text-xs font-mono uppercase tracking-widest text-[var(--color-paper-secondary)] cursor-pointer",
|
||||
select {
|
||||
class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
|
||||
style: "appearance: none; -webkit-appearance: none;",
|
||||
value: "{status}",
|
||||
onchange: move |evt| status.set(evt.value()),
|
||||
option { value: "draft", "草稿" }
|
||||
option { value: "published", "发布" }
|
||||
option { value: "draft", "DRAFT" }
|
||||
option { value: "published", "PUBLISHED" }
|
||||
}
|
||||
span { class: "pr-1.5 text-[var(--color-paper-primary)] font-medium",
|
||||
span { class: "pr-2 text-[var(--color-paper-primary)]",
|
||||
if status() == "draft" {
|
||||
"草稿"
|
||||
"DRAFT"
|
||||
} else {
|
||||
"发布"
|
||||
"PUBLISHED"
|
||||
}
|
||||
}
|
||||
svg {
|
||||
@ -558,10 +550,10 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
||||
}
|
||||
div { class: "w-px h-5 bg-[var(--color-paper-border)]" }
|
||||
button {
|
||||
class: if saving() { "px-5 py-1.5 text-sm bg-[var(--color-paper-tertiary)] text-[var(--color-paper-secondary)] rounded-xl font-medium cursor-not-allowed" } else { "px-5 py-1.5 text-sm bg-[var(--color-paper-accent)] text-[var(--color-paper-theme)] rounded-xl font-medium hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer" },
|
||||
class: if saving() { "px-6 py-2 text-xs font-mono uppercase tracking-widest bg-[var(--color-paper-tertiary)] text-[var(--color-paper-secondary)] rounded-sm cursor-not-allowed" } else { "px-6 py-2 text-xs font-mono uppercase tracking-widest bg-[var(--color-paper-primary)] text-[var(--color-paper-theme)] rounded-sm hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer" },
|
||||
disabled: saving(),
|
||||
onclick: on_submit,
|
||||
"{save_button_text}"
|
||||
if saving() { "SAVING..." } else if is_edit { "UPDATE" } else { "PUBLISH" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user