feat(admin): 后台迁移至 paper-* 主题变量对齐前台

将 /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 全通过。
This commit is contained in:
xfy 2026-06-25 14:33:48 +08:00
parent df85df3d2e
commit 519c0ae3a9
9 changed files with 112 additions and 107 deletions

View File

@ -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", "未登录,正在跳转..." }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<i32>) ->
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<i32>) ->
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
},

View File

@ -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",
"文章删除后保留的时长到期后自动彻底清除1365"
}
}
// 数字输入 + 步进按钮 + 单位后缀
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",

View File

@ -423,7 +423,7 @@ fn write_editor(post_id: Option<i32>) -> 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 {}
}
}