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::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 {}
}
}
}

View File

@ -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}",
}
}

View File

@ -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}" }
}
}
}

View File

@ -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"
}
}
}

View File

@ -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。

View File

@ -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见文件末尾

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。
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" }
}
}
}