From 519c0ae3a9ab02337867b3d61d8a132502ba5bb8 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 25 Jun 2026 14:33:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E5=90=8E=E5=8F=B0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E8=87=B3=20paper-*=20=E4=B8=BB=E9=A2=98=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=AF=B9=E9=BD=90=E5=89=8D=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 /admin 路由下硬编码的 Tailwind gray + hex 配色统一迁移到 input.css 定义的 paper-* 主题变量系统,使后台视觉完全对齐前台: - 底色:bg-white → bg-paper-theme(米白,与前台一致) - 卡片/表格:bg-white dark:bg-[#2e2e33] → bg-paper-entry - 主操作按钮(写文章/保存/分页):bg-gray-900 黑 → bg-paper-accent 鼠尾草绿 - 文字/边框/hover:gray-* + dark:hex → text-paper-*/border-paper-* - 清理 trash 页散落的 #5c7a5e/#7da97f/#e8f0e8 等 hex,路由到变量 - 骨架占位统一复用 ADMIN_* 常量与 SkeletonBox 保留 green/amber/red 状态语义色(通过/待审/删除)与 toggle 白色滑块。 统一采用 bg-paper-* 工具类语法,与 header/post_card 主流写法一致。 dx check + clippy + 402 tests 全通过。 --- src/components/admin_layout.rs | 8 +-- src/components/admin_skeleton.rs | 4 +- src/components/skeletons/posts_skeleton.rs | 7 +- src/components/ui.rs | 18 +++--- src/pages/admin/comments.rs | 40 ++++++------ src/pages/admin/dashboard.rs | 37 +++++------ src/pages/admin/posts.rs | 28 ++++---- src/pages/admin/trash.rs | 75 +++++++++++----------- src/pages/admin/write.rs | 2 +- 9 files changed, 112 insertions(+), 107 deletions(-) diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index cd97d74..51552d5 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -79,7 +79,7 @@ pub fn AdminLayout() -> Element { div { class: "flex items-center gap-3", ThemeToggle {} button { - class: "text-sm text-gray-600 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb] transition-colors", + class: "text-sm text-paper-secondary hover:text-paper-primary transition-colors", onclick: move |_| { spawn(async move { let _ = logout().await; @@ -103,9 +103,9 @@ pub fn AdminLayout() -> Element { // Write 路由:页面固定高度,不滚动,由编辑器内部处理滚动 let root_class = if is_write_route { - "h-dvh flex flex-col overflow-hidden bg-white dark:bg-[#1d1e20]" + "h-dvh flex flex-col overflow-hidden bg-paper-theme" } else { - "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]" + "min-h-screen flex flex-col bg-paper-theme" }; // 根据校验状态与用户状态渲染真实布局、跳转提示或骨架屏 @@ -125,7 +125,7 @@ pub fn AdminLayout() -> Element { rsx! { div { class: "{root_class}", div { class: "flex-1 flex items-center justify-center", - p { class: "text-gray-600 dark:text-[#9b9c9d]", "未登录,正在跳转..." } + p { class: "text-paper-secondary", "未登录,正在跳转..." } } } } diff --git a/src/components/admin_skeleton.rs b/src/components/admin_skeleton.rs index 0a13d23..380aa60 100644 --- a/src/components/admin_skeleton.rs +++ b/src/components/admin_skeleton.rs @@ -17,7 +17,7 @@ pub fn AdminDashboardSkeleton() -> Element { // 统计卡片骨架 div { class: "grid grid-cols-1 md:grid-cols-3 gap-6", for _ in 0..3 { - div { class: "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 text-center space-y-3", + div { class: "rounded-xl bg-paper-entry border border-paper-border p-6 text-center space-y-3", SkeletonBox { class: "h-9 w-16 mx-auto rounded" } SkeletonBox { class: "h-4 w-20 mx-auto rounded" } } @@ -35,7 +35,7 @@ pub fn AdminDashboardSkeleton() -> Element { SkeletonBox { class: "h-6 w-24 rounded" } div { class: "space-y-0", for _ in 0..5 { - div { class: "flex justify-between items-center py-3 border-b border-gray-100 dark:border-[#333]", + 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" } } diff --git a/src/components/skeletons/posts_skeleton.rs b/src/components/skeletons/posts_skeleton.rs index d8920b8..75f8028 100644 --- a/src/components/skeletons/posts_skeleton.rs +++ b/src/components/skeletons/posts_skeleton.rs @@ -5,6 +5,7 @@ use dioxus::prelude::*; use crate::components::skeletons::atoms::SkeletonBox; +use crate::components::ui::ADMIN_TABLE_CLASS; /// 后台文章管理列表骨架屏组件。 /// @@ -12,10 +13,10 @@ use crate::components::skeletons::atoms::SkeletonBox; #[component] pub fn PostsSkeleton() -> Element { rsx! { - div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden", + div { class: "{ADMIN_TABLE_CLASS}", table { class: "w-full text-sm", thead { - tr { class: "border-b border-gray-200 dark:border-[#333]", + tr { class: "border-b border-paper-border", th { class: "px-4 py-3", SkeletonBox { class: "h-3 w-10" } } th { class: "px-4 py-3 w-24", SkeletonBox { class: "h-3 w-10 mx-auto" } } th { class: "px-4 py-3 w-32", SkeletonBox { class: "h-3 w-10" } } @@ -24,7 +25,7 @@ pub fn PostsSkeleton() -> Element { } tbody { for _ in 0..10 { - tr { class: "border-b border-gray-100 dark:border-[#333] last:border-0", + tr { class: "border-b border-paper-border last:border-0", td { class: "px-4 py-3", SkeletonBox { class: "h-4 w-1/3" } } td { class: "px-4 py-3", SkeletonBox { class: "h-5 w-14 mx-auto rounded" } } td { class: "px-4 py-3", SkeletonBox { class: "h-4 w-20" } } diff --git a/src/components/ui.rs b/src/components/ui.rs index 3964c58..5a046fc 100644 --- a/src/components/ui.rs +++ b/src/components/ui.rs @@ -17,18 +17,18 @@ use crate::router::Route; /// Admin 卡片容器:白底圆角描边,亮/暗双模式。用于 stat 卡片、面板等。 pub const ADMIN_CARD_CLASS: &str = - "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333]"; + "bg-paper-entry rounded-xl border border-paper-border"; /// Admin 表格容器:在卡片基础上加 `overflow-hidden`,圆角裁剪表格。 pub const ADMIN_TABLE_CLASS: &str = - "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden"; + "bg-paper-entry rounded-xl border border-paper-border overflow-hidden"; /// Admin 表格行 hover 态:底部分割线 + 悬停背景。 pub const ADMIN_ROW_HOVER: &str = - "border-b border-gray-100 dark:border-[#333] last:border-0 hover:bg-gray-50 dark:hover:bg-[#2a2a2a] transition-colors"; + "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors"; /// 列表复选框统一样式(全选表头 + 行内)。 -pub const CHECKBOX_CLASS: &str = "rounded border-gray-300 dark:border-[#555]"; +pub const CHECKBOX_CLASS: &str = "rounded border-paper-border"; /// 状态徽章外层:小号圆角胶囊。 pub const BADGE_BASE: &str = @@ -56,7 +56,7 @@ pub const BTN_TEXT_AMBER: &str = "text-xs text-amber-600 hover:text-amber-800 da pub const BTN_TEXT_RED: &str = "text-xs text-red-500 hover:text-red-700 dark:hover:text-red-300 transition-colors cursor-pointer"; /// 主题绿(鼠尾草)文字小按钮(行内恢复)。 -pub const BTN_TEXT_ACCENT: &str = "text-xs text-[#5c7a5e] hover:text-[#3d5a3f] dark:text-[#7da97f] dark:hover:text-[#9dc79f] transition-colors cursor-pointer"; +pub const BTN_TEXT_ACCENT: &str = "text-xs text-paper-accent hover:text-paper-primary transition-colors cursor-pointer"; // =========================================================================== // 组件 @@ -95,7 +95,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-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", + "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", "", ) } else { @@ -105,7 +105,7 @@ pub fn Pagination( ) }; let disabled_class = - "inline-flex items-center px-4 py-2 text-sm text-gray-400 bg-gray-100 dark:bg-[#2a2a2a] rounded-full cursor-not-allowed"; + "inline-flex items-center px-4 py-2 text-sm text-paper-secondary bg-paper-tertiary rounded-full cursor-not-allowed"; // admin 首尾页渲染禁用态;frontend 首尾页直接不渲染。 rsx! { @@ -126,7 +126,7 @@ pub fn Pagination( // admin 显示页码计数。 if is_admin { - span { class: "text-sm text-gray-500 dark:text-[#9b9c9d] self-center", + span { class: "text-sm text-paper-secondary self-center", "{current_page} / {total_pages} 页 (共 {total} {unit})" } } @@ -177,7 +177,7 @@ pub fn StatusBadge(color_class: &'static str, label: String) -> Element { pub fn EmptyState(message: &'static str, variant: &'static str) -> Element { let class = match variant { "error" => "text-center text-red-500 dark:text-red-400 py-20", - _ => "text-center py-20 text-gray-500 dark:text-[#9b9c9d]", + _ => "text-center py-20 text-paper-secondary", }; rsx! { div { class: "{class}", "{message}" } diff --git a/src/pages/admin/comments.rs b/src/pages/admin/comments.rs index 52eac14..b0bed14 100644 --- a/src/pages/admin/comments.rs +++ b/src/pages/admin/comments.rs @@ -14,10 +14,12 @@ use crate::api::comments::trash_comment; use crate::api::comments::{approve_comment, batch_update_comment_status, spam_comment}; #[cfg(target_arch = "wasm32")] use crate::api::comments::{get_all_comments, AllCommentsResponse}; +use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::ui::{ - EmptyState, Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_AMBER, - BTN_TEXT_GREEN, BTN_TEXT_RED, BTN_SOLID_AMBER, BTN_SOLID_GREEN, BTN_SOLID_RED, CHECKBOX_CLASS, + EmptyState, Pagination, StatusBadge, ADMIN_CARD_CLASS, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, + BTN_TEXT_AMBER, BTN_TEXT_GREEN, BTN_TEXT_RED, BTN_SOLID_AMBER, BTN_SOLID_GREEN, BTN_SOLID_RED, + CHECKBOX_CLASS, }; use crate::models::comment::{AdminComment, CommentStatus}; use crate::router::Route; @@ -122,17 +124,17 @@ pub fn AdminCommentsPage(page: i32) -> Element { rsx! { div { class: "space-y-6", - h1 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb]", + h1 { class: "text-2xl font-bold text-paper-primary", "评论管理" } - div { class: "flex gap-1 border-b border-gray-200 dark:border-[#333]", + div { class: "flex gap-1 border-b border-paper-border", for (status, label) in [("", "全部"), ("pending", "待审核"), ("approved", "已通过"), ("spam", "垃圾箱")] { button { class: if active_filter() == status { - "px-4 py-2 text-sm font-medium border-b-2 border-gray-900 dark:border-[#dadadb] text-gray-900 dark:text-[#dadadb]" + "px-4 py-2 text-sm font-medium border-b-2 border-paper-accent text-paper-primary" } else { - "px-4 py-2 text-sm font-medium text-gray-500 dark:text-[#9b9c9d] hover:text-gray-700 dark:hover:text-[#dadadb] transition-colors" + "px-4 py-2 text-sm font-medium text-paper-secondary hover:text-paper-primary transition-colors" }, onclick: move |_| active_filter.set(status.to_string()), "{label}" @@ -142,8 +144,8 @@ pub fn AdminCommentsPage(page: i32) -> Element { if !selected_ids().is_empty() { { rsx! { - div { class: "flex items-center gap-3 p-3 bg-gray-50 dark:bg-[#2a2a2a] rounded-lg", - span { class: "text-sm text-gray-600 dark:text-[#9b9c9d]", + div { class: "flex items-center gap-3 p-3 bg-paper-theme rounded-lg", + span { class: "text-sm text-paper-secondary", "已选择 {selected_ids().len()} 条" } button { @@ -203,13 +205,13 @@ pub fn AdminCommentsPage(page: i32) -> Element { } else if loading() && comments().is_empty() { rsx! { DelayedSkeleton { - div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] p-6 space-y-4", + div { class: "{ADMIN_CARD_CLASS} p-6 space-y-4", for _ in 0..5 { div { class: "flex items-center gap-4", - div { class: "h-4 w-4 bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-8 w-8 bg-gray-200 dark:bg-[#2a2a2a] rounded-full" } - div { class: "h-4 w-32 bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-4 flex-1 bg-gray-200 dark:bg-[#2a2a2a] rounded" } + SkeletonBox { class: "h-4 w-4 rounded" } + SkeletonBox { class: "h-8 w-8 rounded-full" } + SkeletonBox { class: "h-4 w-32 rounded" } + SkeletonBox { class: "h-4 flex-1 rounded" } } } } @@ -226,7 +228,7 @@ pub fn AdminCommentsPage(page: i32) -> Element { div { class: "overflow-x-auto", table { class: "w-full text-sm", thead { - tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]", + tr { class: "border-b border-paper-border text-left text-paper-secondary", th { class: "px-4 py-3 font-medium w-10", input { r#type: "checkbox", @@ -372,23 +374,23 @@ fn CommentRow( alt: "{comment.author_name}", } div { class: "min-w-0", - div { class: "text-sm font-medium text-gray-900 dark:text-[#dadadb] truncate", + div { class: "text-sm font-medium text-paper-primary truncate", "{comment.author_name}" } - div { class: "text-xs text-gray-400 dark:text-[#666] truncate", + div { class: "text-xs text-paper-secondary truncate", "{comment.author_email}" } } } } td { class: "px-4 py-3 max-w-xs", - p { class: "text-sm text-gray-600 dark:text-[#9b9c9d] truncate", + p { class: "text-sm text-paper-secondary truncate", "{preview}" } } td { class: "px-4 py-3", Link { - class: "text-sm text-gray-700 dark:text-[#dadadb] hover:opacity-80 transition-opacity", + class: "text-sm text-paper-primary hover:text-paper-accent transition-colors", to: Route::PostDetail { slug: comment.post_slug.clone() }, "{comment.post_title}" } @@ -405,7 +407,7 @@ fn CommentRow( label: status_label, } } - td { class: "px-4 py-3 text-sm text-gray-500 dark:text-[#9b9c9d]", + td { class: "px-4 py-3 text-sm text-paper-secondary", "{date_str}" } td { class: "px-4 py-3 text-right", diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index 87f848e..2ac1e47 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -13,6 +13,7 @@ use crate::api::posts::{get_post_stats, list_posts}; #[cfg(target_arch = "wasm32")] use crate::api::posts::{PostListResponse, PostStatsResponse}; use crate::components::ui::ADMIN_CARD_CLASS; +use crate::components::skeletons::atoms::SkeletonBox; use crate::models::post::{PostListItem, PostStats}; use crate::router::Route; @@ -70,9 +71,9 @@ pub fn Admin() -> Element { None => { rsx! { for _ in 0..3 { - div { class: "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 text-center space-y-3 animate-pulse", - div { class: "h-9 w-16 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-4 w-20 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded" } + 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" } } } } @@ -81,7 +82,7 @@ pub fn Admin() -> Element { } Link { - class: "block rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 text-center hover:border-gray-300 dark:hover:border-[#555] transition-colors", + class: "block {ADMIN_CARD_CLASS} p-6 text-center hover:border-paper-accent transition-colors", to: Route::AdminComments {}, match pending_count() { Some(count) => { @@ -89,15 +90,15 @@ pub fn Admin() -> Element { div { class: "text-3xl font-bold text-amber-600 dark:text-amber-400", "{count}" } - div { class: "text-sm text-gray-500 dark:text-[#9b9c9d] mt-2", + div { class: "text-sm text-paper-secondary mt-2", "待审核评论" } } } None => { rsx! { - div { class: "h-9 w-16 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded animate-pulse" } - div { class: "h-4 w-20 mx-auto bg-gray-200 dark:bg-[#2a2a2a] rounded mt-3 animate-pulse" } + SkeletonBox { class: "h-9 w-16 mx-auto rounded" } + SkeletonBox { class: "h-4 w-20 mx-auto rounded mt-3" } } } } @@ -105,19 +106,19 @@ pub fn Admin() -> Element { div { class: "grid grid-cols-1 md:grid-cols-2 gap-4", Link { - class: "bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", + class: "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: "bg-gray-200 dark:bg-[#333] text-gray-700 dark:text-[#dadadb] rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", + class: "bg-paper-tertiary text-paper-primary rounded-full px-6 py-3 text-center font-medium hover:opacity-80 transition-opacity cursor-pointer", to: Route::Posts {}, "管理文章" } } div { class: "mb-8", - h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mb-4", + h2 { class: "text-xl font-bold text-paper-primary mb-4", "最近文章" } match recent_posts() { @@ -134,9 +135,9 @@ pub fn Admin() -> Element { rsx! { div { class: "space-y-4 animate-pulse", for _ in 0..5 { - div { class: "flex justify-between items-center py-3 border-b border-gray-100 dark:border-[#333]", - div { class: "h-4 w-[45%] bg-gray-200 dark:bg-[#2a2a2a] rounded" } - div { class: "h-3 w-20 bg-gray-200 dark:bg-[#2a2a2a] rounded" } + 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" } } } } @@ -153,10 +154,10 @@ pub fn Admin() -> Element { fn StatCard(value: String, label: String) -> Element { rsx! { div { class: "{ADMIN_CARD_CLASS} p-6 text-center", - div { class: "text-3xl font-bold text-gray-900 dark:text-[#dadadb]", + div { class: "text-3xl font-bold text-paper-primary", "{value}" } - div { class: "text-sm text-gray-500 dark:text-[#9b9c9d] mt-2", + div { class: "text-sm text-paper-secondary mt-2", "{label}" } } @@ -171,16 +172,16 @@ fn RecentPostItem(post: PostListItem) -> Element { let status_class = post.status_class(); rsx! { - div { class: "flex justify-between items-center py-3 border-b border-gray-100 dark:border-[#333]", + div { class: "flex justify-between items-center py-3 border-b border-paper-border", div { class: "flex items-center gap-3", - span { class: "text-gray-700 dark:text-[#dadadb]", + span { class: "text-paper-primary", "{post.title}" } span { class: "text-xs {status_class}", "{status_label}" } } - span { class: "text-sm text-gray-400 dark:text-[#9b9c9d]", + span { class: "text-sm text-paper-secondary", "{date_str}" } } diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 8ae0061..ea4eea6 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -101,42 +101,42 @@ pub fn PostsPage(page: i32) -> Element { rsx! { div { class: "space-y-6", div { class: "flex items-center justify-between", - h1 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb]", + h1 { class: "text-2xl font-bold text-paper-primary", "文章管理" } div { class: "flex items-center gap-3", div { class: "group relative", button { class: if rebuilding() { - "px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-gray-400 dark:text-[#666] border border-gray-300 dark:border-[#444]" + "px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { - "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-gray-700 dark:text-[#b0b0b1] border border-gray-300 dark:border-[#444] hover:border-gray-900 dark:hover:border-[#dadadb] hover:text-gray-900 dark:hover:text-[#dadadb] transition-all" + "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" }, disabled: rebuilding(), onclick: move |_| do_rebuild(false), if rebuilding() { "重建中..." } else { "重建内容" } } - div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-[#1a1a1a] shadow-lg z-50", + div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-paper-primary text-paper-theme shadow-lg z-50", "重建 content_html 为空的文章渲染缓存" } } div { class: "group relative", button { class: if rebuilding() { - "px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-gray-400 dark:text-[#666] border border-gray-300 dark:border-[#444]" + "px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { - "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-gray-700 dark:text-[#b0b0b1] border border-gray-300 dark:border-[#444] hover:border-gray-900 dark:hover:border-[#dadadb] hover:text-gray-900 dark:hover:text-[#dadadb] transition-all" + "px-4 py-2 rounded-full text-sm font-medium cursor-pointer text-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" }, disabled: rebuilding(), onclick: move |_| do_rebuild(true), if rebuilding() { "重建中..." } else { "重建全部" } } - div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-[#1a1a1a] shadow-lg z-50", + div { class: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-2 px-3 py-1.5 text-xs font-medium whitespace-nowrap rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-paper-primary text-paper-theme shadow-lg z-50", "重建所有文章的渲染缓存(含已有内容)" } } Link { - class: "px-4 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full text-sm font-medium hover:opacity-80 transition-opacity cursor-pointer", + 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", to: Route::Write {}, "+ 写文章" } @@ -144,7 +144,7 @@ pub fn PostsPage(page: i32) -> Element { } if let Some(msg) = rebuild_result() { - div { class: "text-sm text-gray-600 dark:text-[#9b9c9d] px-1", + div { class: "text-sm text-paper-secondary px-1", "{msg}" } } @@ -157,7 +157,7 @@ pub fn PostsPage(page: i32) -> Element { div { class: "{ADMIN_TABLE_CLASS}", table { class: "w-full text-sm", thead { - tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]", + tr { class: "border-b border-paper-border text-left text-paper-secondary", th { class: "px-4 py-3 font-medium", "标题" } th { class: "px-4 py-3 font-medium w-24 text-center", "状态" } th { class: "px-4 py-3 font-medium w-32", "日期" } @@ -222,7 +222,7 @@ fn PostRow(post: PostListItem, deleting: bool, on_delete: EventHandler) -> tr { class: "{ADMIN_ROW_HOVER}", td { class: "px-4 py-3", Link { - class: "text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity", + class: "text-paper-primary hover:text-paper-accent transition-colors cursor-pointer", to: Route::PostDetail { slug: post.slug.clone() }, "{post.title}" } @@ -233,19 +233,19 @@ fn PostRow(post: PostListItem, deleting: bool, on_delete: EventHandler) -> label: post.status_label().to_string(), } } - td { class: "px-4 py-3 text-gray-500 dark:text-[#9b9c9d]", + td { class: "px-4 py-3 text-paper-secondary", "{date_str}" } td { class: "px-4 py-3 text-right", div { class: "flex justify-end gap-3", Link { - class: "text-xs text-gray-600 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb] transition-colors cursor-pointer", + class: "text-xs text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer", to: Route::WriteEdit { id: post.id }, "编辑" } button { class: if deleting { - "text-xs text-gray-400 cursor-not-allowed" + "text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED }, diff --git a/src/pages/admin/trash.rs b/src/pages/admin/trash.rs index 3d4c075..2fd69b7 100644 --- a/src/pages/admin/trash.rs +++ b/src/pages/admin/trash.rs @@ -18,10 +18,11 @@ use crate::api::posts::{ use crate::api::posts::{list_deleted_posts, PostListResponse}; #[allow(unused_imports)] use crate::api::settings::{get_trash_settings, update_trash_settings}; +use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::ui::{ - EmptyState, Pagination, StatusBadge, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_SOLID_GREEN, - BTN_SOLID_RED, BTN_TEXT_ACCENT, BTN_TEXT_RED, CHECKBOX_CLASS, + EmptyState, Pagination, StatusBadge, ADMIN_CARD_CLASS, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, + BTN_SOLID_GREEN, BTN_SOLID_RED, BTN_TEXT_ACCENT, BTN_TEXT_RED, CHECKBOX_CLASS, }; use crate::models::post::PostListItem; use crate::models::settings::TrashSettings; @@ -124,27 +125,27 @@ pub fn TrashPage(page: i32) -> Element { div { class: "space-y-6", // 页面标题 div { class: "flex items-center gap-3", - h1 { class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb]", "回收站" } - span { class: "text-sm text-gray-500 dark:text-[#9b9c9d]", + h1 { class: "text-2xl font-bold text-paper-primary", "回收站" } + span { class: "text-sm text-paper-secondary", "共 {total()} 篇" } } // 自动清理配置卡片:可折叠的设置面板,顶部始终显示当前状态摘要 div { - class: "rounded-xl border border-gray-200 dark:border-[#333] overflow-hidden bg-white dark:bg-[#2e2e33]", + class: "rounded-xl border border-paper-border overflow-hidden bg-paper-entry", // 顶部可点击摘要条:状态指示灯 + 标题 + 展开箭头 button { - class: "w-full flex items-center gap-3 px-5 py-4 text-left cursor-pointer hover:bg-gray-50 dark:hover:bg-[#34343a] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40", + class: "w-full flex items-center gap-3 px-5 py-4 text-left cursor-pointer hover:bg-paper-theme focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40", onclick: move |_| { settings_panel_open.set(!settings_panel_open()); just_saved.set(false); }, // 状态指示灯 {let dot_class = if settings().auto_purge_enabled { - "w-2 h-2 rounded-full bg-[#5c7a5e] dark:bg-[#7da97f] shadow-[0_0_0_3px_rgba(92,122,94,0.15)]" + "w-2 h-2 rounded-full bg-paper-accent shadow-[0_0_0_3px_rgba(92,122,94,0.15)]" } else { - "w-2 h-2 rounded-full bg-gray-300 dark:bg-[#666]" + "w-2 h-2 rounded-full bg-paper-tertiary" }; rsx! { div { class: "w-2 flex-shrink-0 flex items-center justify-center", @@ -153,10 +154,10 @@ pub fn TrashPage(page: i32) -> Element { }} // 标题 + 当前状态描述 div { class: "flex-1 min-w-0", - div { class: "text-sm font-medium text-gray-900 dark:text-[#dadadb]", + div { class: "text-sm font-medium text-paper-primary", "自动清理" } - div { class: "text-xs text-gray-500 dark:text-[#9b9c9d] mt-0.5 truncate", + div { class: "text-xs text-paper-secondary mt-0.5 truncate", if settings().auto_purge_enabled { "已开启 · 超过 {settings().retention_days} 天的文章将被自动删除" } else { @@ -166,7 +167,7 @@ pub fn TrashPage(page: i32) -> Element { } // 展开箭头(旋转动画) svg { - class: "w-4 h-4 text-gray-400 dark:text-[#9b9c9d] transition-transform duration-200 flex-shrink-0 {chevron_rotate}", + class: "w-4 h-4 text-paper-secondary transition-transform duration-200 flex-shrink-0 {chevron_rotate}", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", @@ -177,15 +178,15 @@ pub fn TrashPage(page: i32) -> Element { // 设置面板(可折叠) if settings_panel_open() { - div { class: "border-t border-gray-100 dark:border-[#3a3a3e] p-5 space-y-6", + div { class: "border-t border-paper-border p-5 space-y-6", // 开关行:启用自动清理 div { class: "flex items-center justify-between gap-4", div { class: "min-w-0", - div { class: "text-sm font-medium text-gray-900 dark:text-[#dadadb]", + div { class: "text-sm font-medium text-paper-primary", "启用自动清理" } - div { class: "text-xs text-gray-500 dark:text-[#9b9c9d] mt-1", + div { class: "text-xs text-paper-secondary mt-1", "后台任务定期彻底删除超过保留期的文章" } } @@ -194,9 +195,9 @@ pub fn TrashPage(page: i32) -> Element { role: "switch", aria_checked: "{settings_draft_enabled()}", class: if settings_draft_enabled() { - "relative w-11 h-6 flex-shrink-0 rounded-full bg-[#5c7a5e] dark:bg-[#7da97f] cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40" + "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-accent cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" } else { - "relative w-11 h-6 flex-shrink-0 rounded-full bg-gray-300 dark:bg-[#555] cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40" + "relative w-11 h-6 flex-shrink-0 rounded-full bg-paper-tertiary cursor-pointer transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" }, onclick: move |_| { settings_draft_enabled.set(!settings_draft_enabled()); @@ -216,19 +217,19 @@ pub fn TrashPage(page: i32) -> Element { // 保留天数行 div { class: "space-y-3", div { class: "min-w-0", - div { class: "text-sm font-medium text-gray-900 dark:text-[#dadadb]", + div { class: "text-sm font-medium text-paper-primary", "保留天数" } - div { class: "text-xs text-gray-500 dark:text-[#9b9c9d] mt-1", + div { class: "text-xs text-paper-secondary mt-1", "文章删除后保留的时长,到期后自动彻底清除(1–365)" } } // 数字输入 + 步进按钮 + 单位后缀 div { class: "flex items-center gap-3", - div { class: "flex items-center rounded-lg border border-gray-300 dark:border-[#444] bg-white dark:bg-[#1d1e20] overflow-hidden", + div { class: "flex items-center rounded-lg border border-paper-border bg-paper-entry overflow-hidden", // 减号 button { - class: "w-9 h-9 flex items-center justify-center text-sm text-gray-500 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb] hover:bg-gray-50 dark:hover:bg-[#2a2a2a] cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40", + class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40", r#type: "button", aria_label: "减少保留天数", onclick: move |_| { @@ -244,7 +245,7 @@ pub fn TrashPage(page: i32) -> Element { r#type: "number", min: "1", max: "365", - class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-gray-900 dark:text-[#dadadb] bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", + class: "w-14 h-9 px-1 text-center text-sm tabular-nums text-paper-primary bg-transparent border-0 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none", value: "{settings_draft_days()}", oninput: move |e| { settings_draft_days.set(e.value()); @@ -253,7 +254,7 @@ pub fn TrashPage(page: i32) -> Element { } // 加号 button { - class: "w-9 h-9 flex items-center justify-center text-sm text-gray-500 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb] hover:bg-gray-50 dark:hover:bg-[#2a2a2a] cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40", + class: "w-9 h-9 flex items-center justify-center text-sm text-paper-secondary hover:text-paper-primary hover:bg-paper-theme cursor-pointer transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40", r#type: "button", aria_label: "增加保留天数", onclick: move |_| { @@ -265,7 +266,7 @@ pub fn TrashPage(page: i32) -> Element { "+" } } - span { class: "text-xs text-gray-400 dark:text-[#666]", "天" } + span { class: "text-xs text-paper-secondary", "天" } } } @@ -273,14 +274,14 @@ pub fn TrashPage(page: i32) -> Element { div { class: "flex items-center justify-between gap-4 pt-1", // 草稿状态提示 if just_saved() { - span { class: "inline-flex items-center gap-1.5 text-xs text-[#5c7a5e] dark:text-[#7da97f]", + span { class: "inline-flex items-center gap-1.5 text-xs text-paper-accent", svg { class: "w-3.5 h-3.5", view_box: "0 0 24 24", fill: "none", stroke: "currentColor", stroke_width: "2.5", path { stroke_linecap: "round", stroke_linejoin: "round", d: "M5 13l4 4L19 7" } } "已保存" } } else if dirty { - span { class: "text-xs text-gray-400 dark:text-[#666]", + span { class: "text-xs text-paper-secondary", "有未保存的更改" } } else { @@ -289,11 +290,11 @@ pub fn TrashPage(page: i32) -> Element { // 保存按钮:启用主题色,禁用/保存中态灰化 button { class: if saving_settings() { - "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-gray-400 dark:text-[#666] bg-gray-100 dark:bg-[#2a2a2a] rounded-full" + "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else if just_saved() { - "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-gray-400 dark:text-[#666] bg-gray-100 dark:bg-[#2a2a2a] rounded-full" + "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium cursor-not-allowed text-paper-secondary bg-paper-tertiary rounded-full" } else { - "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-white bg-[#5c7a5e] dark:bg-[#7da97f] dark:text-[#1d1e20] rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5c7a5e]/40 dark:focus-visible:ring-[#7da97f]/40" + "inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-paper-theme bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-paper-accent/40" }, disabled: saving_settings() || just_saved() || !dirty, onclick: move |_| { @@ -317,8 +318,8 @@ pub fn TrashPage(page: i32) -> Element { // 批量操作栏(选中时显示) if !selected_ids().is_empty() { - div { class: "flex items-center gap-3 p-3 bg-gray-50 dark:bg-[#2a2a2a] rounded-lg", - span { class: "text-sm text-gray-600 dark:text-[#9b9c9d]", + div { class: "flex items-center gap-3 p-3 bg-paper-theme rounded-lg", + span { class: "text-sm text-paper-secondary", "已选择 {selected_ids().len()} 条" } button { @@ -363,9 +364,9 @@ pub fn TrashPage(page: i32) -> Element { } else if loading() && posts().is_empty() { rsx! { DelayedSkeleton { - div { class: "bg-white dark:bg-[#2e2e33] rounded-xl border border-gray-200 dark:border-[#333] p-6 space-y-4", + div { class: "{ADMIN_CARD_CLASS} p-6 space-y-4", for _ in 0..5 { - div { class: "h-10 bg-gray-200 dark:bg-[#2a2a2a] rounded" } + SkeletonBox { class: "h-10 rounded" } } } } @@ -381,7 +382,7 @@ pub fn TrashPage(page: i32) -> Element { div { class: "overflow-x-auto", table { class: "w-full text-sm", thead { - tr { class: "border-b border-gray-200 dark:border-[#333] text-left text-gray-500 dark:text-[#9b9c9d]", + tr { class: "border-b border-paper-border text-left text-paper-secondary", th { class: "px-4 py-3 font-medium w-10", input { r#type: "checkbox", @@ -535,9 +536,9 @@ fn TrashRow( let badge_class = if expired { "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" } else if remaining <= 7 { - "bg-[#e8f0e8] text-[#5c7a5e] dark:bg-[#1e2e1e] dark:text-[#7da97f]" + "bg-paper-accent-soft text-paper-accent" } else { - "bg-gray-100 text-gray-600 dark:bg-[#333] dark:text-[#9b9c9d]" + "bg-paper-tertiary text-paper-secondary" }; let badge_text = if expired { "待清理".to_string() } else { format!("{remaining}天") }; let deleted_str = post @@ -556,7 +557,7 @@ fn TrashRow( } } td { class: "px-4 py-3", - div { class: "text-sm font-medium text-gray-900 dark:text-[#dadadb] truncate max-w-xs", + div { class: "text-sm font-medium text-paper-primary truncate max-w-xs", "{post.title}" } } @@ -566,7 +567,7 @@ fn TrashRow( label: post.status_label().to_string(), } } - td { class: "px-4 py-3 text-sm text-gray-500 dark:text-[#9b9c9d]", + td { class: "px-4 py-3 text-sm text-paper-secondary", "{deleted_str}" } td { class: "px-4 py-3 text-center", diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index 2be2683..b4d38eb 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -423,7 +423,7 @@ fn write_editor(post_id: Option) -> Element { rsx! { div { class: "relative flex flex-col flex-1 min-h-0 overflow-hidden", if loading() { - div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]", + div { class: "absolute inset-0 z-10 bg-paper-theme", WriteSkeleton {} } }