feat(admin): complete redesign of backend ui with industrial minimalist aesthetic

This commit is contained in:
xfy 2026-07-03 15:32:02 +08:00
parent 45f4e9cde0
commit d1041a84dd
7 changed files with 211 additions and 209 deletions

View File

@ -1,27 +1,20 @@
//! 后台管理布局组件 //! 后台管理布局组件
//! //!
//! 包裹所有后台路由,提供管理员专属导航、登录校验、主题切换与登出入口。 //! 提供全新设计的工业/极简风格的管理员专属后台布局。
//! 在未完成身份校验前显示与真实布局结构一致的骨架屏,避免切换闪烁。 //! 采用 Linear-style / Vercel-style 全宽顶部导航,去除冗余圆角与阴影,
//! 突出数据密度与严谨性(工业/控制台美学)。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link;
use crate::api::auth::{get_current_user, logout}; use crate::api::auth::{get_current_user, logout};
use crate::components::admin_skeleton::AdminDashboardSkeleton; 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::components::write_skeleton::WriteSkeleton;
use crate::context::UserContext; use crate::context::UserContext;
use crate::hooks::delayed_loading::use_delayed_loading; use crate::hooks::delayed_loading::use_delayed_loading;
use crate::router::Route; use crate::router::Route;
use crate::theme::ThemeToggle; use crate::theme::ThemeToggle;
/// 后台管理整体布局组件。
///
/// 负责:
/// - 通过 `get_current_user` 校验登录状态,未登录时跳转登录页
/// - 渲染顶部导航(仪表盘、写文章、管理文章)与主题切换/登出按钮
/// - 根据当前路由切换主区域样式Write 路由固定高度,其他路由可滚动)
/// - 校验完成前使用骨架屏保持布局稳定
#[component] #[component]
pub fn AdminLayout() -> Element { pub fn AdminLayout() -> Element {
let mut ctx: UserContext = use_context(); let mut ctx: UserContext = use_context();
@ -29,7 +22,6 @@ pub fn AdminLayout() -> Element {
let route = use_route::<Route>(); let route = use_route::<Route>();
let show_skeleton = use_delayed_loading(move || !(ctx.checked)()); let show_skeleton = use_delayed_loading(move || !(ctx.checked)());
// 仅在首次挂载时执行一次登录校验
use_effect(move || { use_effect(move || {
if !(ctx.checked)() { if !(ctx.checked)() {
(ctx.checked).set(true); (ctx.checked).set(true);
@ -50,41 +42,58 @@ pub fn AdminLayout() -> Element {
} }
}); });
// 后台导航项,当前路由高亮
let admin_nav_items = vec![ let admin_nav_items = vec![
NavItemConfig { (Route::Admin {}, "仪表盘"),
route: Route::Admin {}, (Route::Write {}, "写文章"),
label: "仪表盘", (Route::Posts {}, "管理文章"),
is_active: matches!(route, Route::Admin {}), (Route::Trash {}, "回收站"),
}, (Route::System {}, "系统"),
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 {}),
},
]; ];
// 右侧操作区:主题切换 + 登出按钮 let is_write_route =
let right_content = rsx! { matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
div { class: "flex items-center gap-3",
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 {} ThemeToggle {}
button { button {
class: "text-sm text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer", 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 |_| { onclick: move |_| {
spawn(async move { spawn(async move {
let _ = logout().await; let _ = logout().await;
@ -93,66 +102,45 @@ pub fn AdminLayout() -> Element {
let _ = navigator.push(Route::Login {}); 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)()) { match ((ctx.checked)(), (ctx.user)()) {
(true, Some(_)) => { (true, Some(_)) => {
rsx! { rsx! {
div { class: "{root_class}", 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> {} } main { class: "{main_class}", Outlet::<Route> {} }
Footer {}
} }
} }
} }
(true, None) => { (true, None) => {
rsx! { rsx! {
div { class: "{root_class}", div { class: "{root_class}",
div { class: "flex-1 flex items-center justify-center", div { class: "flex-1 flex items-center justify-center font-mono text-xs text-paper-secondary tracking-widest",
p { class: "text-paper-secondary", "未登录,正在跳转..." } "AUTHENTICATING..."
} }
} }
} }
} }
(false, _) => { (false, _) => {
// 使用与真实布局完全相同的结构包裹内容骨架,避免 checked 变化时的布局闪烁
rsx! { rsx! {
div { class: "{root_class}", div { class: "{root_class}",
Header { nav_items: admin_nav_items, right_content, max_width: "max-w-5xl" } {nav_content}
main { class: "{main_class}", main { class: "{main_class}",
div { class: if show_skeleton() { "" } else { "opacity-0" }, div { class: if show_skeleton() { "" } else { "opacity-0" },
{ {
match route { match route {
Route::Write {} => rsx! { Route::Write {} => rsx! { WriteSkeleton {} },
WriteSkeleton {} _ => rsx! { AdminDashboardSkeleton {} },
}, }
_ => rsx! { }
AdminDashboardSkeleton {} }
}, }
}
}
}
}
Footer {}
} }
} }
} }

View File

@ -15,15 +15,11 @@ use crate::router::Route;
// 样式常量 // 样式常量
// =========================================================================== // ===========================================================================
/// Admin 卡片容器:白底圆角描边,亮/暗双模式。用于 stat 卡片、面板等 /// Admin 卡片容器:锐利直角/微小圆角,工业控制台风格
pub const ADMIN_CARD_CLASS: &str = "bg-paper-entry rounded-xl border border-paper-border"; pub const ADMIN_CARD_CLASS: &str = "bg-paper-entry border border-paper-border rounded-sm";
/// Admin 表格容器:白底圆角描边,亮/暗双模式。 /// Admin 表格容器:锐利直角/微小圆角,与控制台一致。
/// pub const ADMIN_TABLE_CLASS: &str = "bg-paper-entry border border-paper-border rounded-sm";
/// 不加 `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 表格行 hover 态:底部分割线 + 悬停背景。 /// Admin 表格行 hover 态:底部分割线 + 悬停背景。
pub const ADMIN_ROW_HOVER: &str = pub const ADMIN_ROW_HOVER: &str =
@ -46,13 +42,13 @@ pub const BADGE_BASE: &str =
/// 绿色实心小按钮(批量通过、批量恢复)。 /// 绿色实心小按钮(批量通过、批量恢复)。
pub const BTN_SOLID_GREEN: &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 = 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 = 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 描边风格,从属于主色 Green ---
/// 次要按钮Teal 描边 ghost 风格。 /// 次要按钮:细线勾勒的极简风格,控制台质感。
/// 用于与主操作按钮paper-accent 实心)成对的次操作,如「管理文章」「重建内容」。
/// 亮色文字 #179299 vs mantle 底、暗色 #94e2d5 vs mantle 底,均过 WCAG AA。
pub const BTN_SECONDARY: &str = 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 is_admin = variant == "admin";
let (link_class, link_extra_next) = if is_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 { } else {
@ -122,7 +116,7 @@ pub fn Pagination(
) )
}; };
let disabled_class = 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 首尾页直接不渲染。 // admin 首尾页渲染禁用态frontend 首尾页直接不渲染。
rsx! { rsx! {
@ -296,12 +290,12 @@ pub fn FilterTabs(
}); });
rsx! { 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 { for (value, label) in items {
button { button {
id: "tab-{id_prefix}-{value}", id: "tab-{id_prefix}-{value}",
key: "{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: { onclick: {
let v = value.to_string(); let v = value.to_string();
move |_| { move |_| {
@ -314,7 +308,7 @@ pub fn FilterTabs(
} }
// 绝对定位的滑动颜色条 // 绝对定位的滑动颜色条
div { 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}", style: "{indicator_style}",
} }
} }

View File

@ -1,11 +1,11 @@
//! 管理后台仪表盘页面。 //! 管理后台仪表盘页面。
//! //!
//! 展示文章统计、待审核评论数量以及最近文章列表,仅在 WASM 前端通过 Dioxus server functions 加载数据。 //! 采用高密度工业风设计的管理面板,突出核心数据指标与最新的工作流状态。
//! 数据仅在 WASM 前端通过 Dioxus server functions 异步加载。
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::router::components::Link; use dioxus::router::components::Link;
// 仅在 WASM 前端使用的 server function 导入。
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use crate::api::comments::get_pending_count; use crate::api::comments::get_pending_count;
#[cfg(target_arch = "wasm32")] #[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::models::post::{PostListItem, PostStats};
use crate::router::Route; use crate::router::Route;
/// 后台仪表盘页面组件。
///
/// 展示文章总数、草稿数、已发布数的统计卡片,待审核评论入口,以及最近 5 篇文章列表。
/// 所有数据仅在 WASM 前端通过 server functions 异步加载。
#[component] #[component]
#[allow(unused_mut)] #[allow(unused_mut)]
pub fn Admin() -> Element { pub fn Admin() -> Element {
// 仪表盘状态:统计数据、最近文章、待审核评论数与首次加载标志。
let mut stats = use_signal(|| None::<PostStats>); let mut stats = use_signal(|| None::<PostStats>);
let mut recent_posts = use_signal(|| None::<Vec<PostListItem>>); let mut recent_posts = use_signal(|| None::<Vec<PostListItem>>);
let mut pending_count = use_signal(|| None::<i64>); let mut pending_count = use_signal(|| None::<i64>);
let mut loaded = use_signal(|| false); let mut loaded = use_signal(|| false);
// 组件挂载后触发一次:仅在 WASM 前端异步请求仪表盘数据。
use_effect(move || { use_effect(move || {
if !loaded() { if !loaded() {
loaded.set(true); loaded.set(true);
// 以下请求只在 WASM 前端执行SSR 阶段不会访问浏览器 API。
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
spawn(async move { spawn(async move {
@ -58,67 +50,88 @@ pub fn Admin() -> Element {
}); });
rsx! { rsx! {
div { class: "space-y-8", div { class: "w-full max-w-7xl mx-auto space-y-8",
div { class: "grid grid-cols-1 md:grid-cols-3 gap-6", // 顶部标题和全局操作栏
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() { match stats() {
Some(s) => { Some(s) => {
rsx! { rsx! {
StatCard { value: s.total.to_string(), label: "文章总数" } StatCard { value: s.total.to_string(), label: "TOTAL_POSTS", trend: "+12%" }
StatCard { value: s.drafts.to_string(), label: "草稿数" } StatCard { value: s.published.to_string(), label: "PUBLISHED", trend: "Active" }
StatCard { value: s.published.to_string(), label: "已发布" } StatCard { value: s.drafts.to_string(), label: "DRAFTS", trend: "Pending" }
} }
} }
None => { None => {
rsx! { rsx! {
for _ in 0..3 { for _ in 0..3 {
div { class: "{ADMIN_CARD_CLASS} p-6 text-center space-y-3 animate-pulse", div { class: "{ADMIN_CARD_CLASS} p-6 flex flex-col justify-between h-32 animate-pulse",
SkeletonBox { class: "h-9 w-16 mx-auto rounded" } SkeletonBox { class: "h-3 w-20 rounded" }
SkeletonBox { class: "h-4 w-20 mx-auto rounded" } SkeletonBox { class: "h-10 w-16 rounded mt-4" }
}
} }
} }
} }
} }
} }
// 评论待办卡片 (独立色块突出)
Link { Link {
class: "block {ADMIN_CARD_CLASS} p-6 text-center hover:border-paper-accent transition-colors", 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 {}, to: Route::AdminComments {},
match pending_count() { match pending_count() {
Some(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! { rsx! {
div { class: "text-3xl font-bold text-amber-600 dark:text-amber-400", "{count}" } div { class: "text-[11px] font-mono tracking-widest uppercase {color_class}", "PENDING_COMMENTS" }
div { class: "text-sm text-paper-secondary mt-2", "待审核评论" } 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 => { None => {
rsx! { rsx! {
SkeletonBox { class: "h-9 w-16 mx-auto rounded" } SkeletonBox { class: "h-3 w-24 rounded" }
SkeletonBox { class: "h-4 w-20 mx-auto rounded mt-3" } SkeletonBox { class: "h-10 w-16 rounded mt-4" }
}
} }
} }
} }
} }
div { class: "grid grid-cols-1 md:grid-cols-2 gap-4", // 最近文章列表 (紧凑表格样式)
Link { div { class: "mt-8",
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", div { class: "flex items-center justify-between mb-4",
to: Route::Write {}, h2 { class: "text-sm font-mono tracking-widest text-paper-secondary uppercase", "RECENT_PUBLICATIONS" }
"写文章"
} }
Link { div { class: "{ADMIN_CARD_CLASS} overflow-hidden",
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() { match recent_posts() {
Some(posts) => { Some(posts) => {
rsx! { rsx! {
div { class: "space-y-0", div { class: "divide-y divide-paper-border",
for post in posts.iter().take(5) { for post in posts.iter().take(5) {
RecentPostItem { key: "{post.id}", post: post.clone() } RecentPostItem { key: "{post.id}", post: post.clone() }
} }
@ -127,11 +140,12 @@ pub fn Admin() -> Element {
} }
None => { None => {
rsx! { rsx! {
div { class: "space-y-4 animate-pulse", div { class: "divide-y divide-paper-border animate-pulse",
for _ in 0..5 { for _ in 0..5 {
div { class: "flex justify-between items-center py-3 border-b border-paper-border", div { class: "flex justify-between items-center px-6 py-4",
SkeletonBox { class: "h-4 w-[45%] rounded" } SkeletonBox { class: "h-4 w-[40%] rounded" }
SkeletonBox { class: "h-3 w-20 rounded" } SkeletonBox { class: "h-3 w-24 rounded" }
}
} }
} }
} }
@ -143,31 +157,35 @@ pub fn Admin() -> Element {
} }
} }
/// 统计卡片组件,显示一个数值指标与标签。
#[component] #[component]
fn StatCard(value: String, label: String) -> Element { fn StatCard(value: String, label: String, trend: String) -> Element {
rsx! { rsx! {
div { class: "{ADMIN_CARD_CLASS} p-6 text-center", div { class: "{ADMIN_CARD_CLASS} p-6 flex flex-col justify-between h-32 relative overflow-hidden group",
div { class: "text-3xl font-bold text-paper-primary", "{value}" } div { class: "absolute top-0 left-0 w-1 h-full bg-paper-border group-hover:bg-paper-primary transition-colors" }
div { class: "text-sm text-paper-secondary mt-2", "{label}" } 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] #[component]
fn RecentPostItem(post: PostListItem) -> Element { fn RecentPostItem(post: PostListItem) -> Element {
let date_str = post.formatted_date(); let date_str = post.formatted_date();
let status_label = post.status_label(); 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! { rsx! {
div { class: "flex justify-between items-center py-3 border-b border-paper-border", 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-3", div { class: "flex items-center gap-4",
span { class: "text-paper-primary", "{post.title}" } span { class: "text-[11px] font-mono text-paper-tertiary w-12 hidden sm:block", "#{post.id:04}" }
span { class: "text-xs {status_class}", "{status_label}" } 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}" }
} }
} }
} }

View File

@ -75,9 +75,12 @@ pub fn PostsPage(page: i32) -> Element {
let get_posts = move || -> Vec<PostListItem> { posts() }; let get_posts = move || -> Vec<PostListItem> { posts() };
rsx! { rsx! {
div { class: "space-y-6", div { class: "w-full max-w-7xl mx-auto space-y-6",
div { class: "flex items-center justify-between", div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-paper-border mb-6",
h1 { class: "text-2xl font-bold text-paper-primary", "文章管理" } 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", div { class: "flex items-center gap-3",
// 重建缓存工具条(抽取为子组件 RebuildCacheBar见文件末尾 // 重建缓存工具条(抽取为子组件 RebuildCacheBar见文件末尾
RebuildCacheBar { RebuildCacheBar {
@ -85,9 +88,9 @@ pub fn PostsPage(page: i32) -> Element {
rebuild_result: rebuild_result, rebuild_result: rebuild_result,
} }
Link { 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 {}, to: Route::Write {},
"+ 写文章" "+ NEW_POST"
} }
} }
} }

View File

@ -57,8 +57,13 @@ pub fn System() -> Element {
let mut active_tab = use_signal(|| SystemTab::DbStatus); let mut active_tab = use_signal(|| SystemTab::DbStatus);
rsx! { rsx! {
div { class: "space-y-6", div { class: "w-full max-w-7xl mx-auto space-y-6",
h1 { class: "text-2xl font-bold text-paper-primary", "系统管理" } 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 桥接枚举)。 // 顶部 tab 切换栏:复用公共 FilterTabs 组件(String API,经 as_str/from_str 桥接枚举)。
// 视觉与评论页一致:平滑滑动指示条 + 选中文字 text-paper-primary。 // 视觉与评论页一致:平滑滑动指示条 + 选中文字 text-paper-primary。

View File

@ -78,11 +78,13 @@ pub fn TrashPage(page: i32) -> Element {
}; };
rsx! { 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", div { class: "flex items-end justify-between pb-6 border-b border-paper-border mb-6",
h1 { class: "text-2xl font-bold text-paper-primary", "回收站" } div {
span { class: "text-sm text-paper-secondary", "共 {total()} 篇" } 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见文件末尾 // 自动清理配置卡片(抽取为子组件 AutoPurgeSettings见文件末尾

View File

@ -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。 // 元信息表单复用样式见模块级 META_LABEL_CLASS / META_INPUT_CLASS。
rsx! { 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 { 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 |_| { onclick: move |_| {
let _ = dioxus::router::navigator().push(Route::Posts {}); let _ = dioxus::router::navigator().push(Route::Posts {});
}, },
"取消" "CANCEL"
} }
div { class: "w-px h-5 bg-[var(--color-paper-border)]" } 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 { select {
class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer", class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
style: "appearance: none; -webkit-appearance: none;", style: "appearance: none; -webkit-appearance: none;",
value: "{status}", value: "{status}",
onchange: move |evt| status.set(evt.value()), onchange: move |evt| status.set(evt.value()),
option { value: "draft", "草稿" } option { value: "draft", "DRAFT" }
option { value: "published", "发布" } 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" { if status() == "draft" {
"草稿" "DRAFT"
} else { } else {
"发布" "PUBLISHED"
} }
} }
svg { svg {
@ -558,10 +550,10 @@ fn write_editor(post_id: Option<i32>) -> Element {
} }
div { class: "w-px h-5 bg-[var(--color-paper-border)]" } div { class: "w-px h-5 bg-[var(--color-paper-border)]" }
button { 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(), disabled: saving(),
onclick: on_submit, onclick: on_submit,
"{save_button_text}" if saving() { "SAVING..." } else if is_edit { "UPDATE" } else { "PUBLISH" }
} }
} }
} }