diff --git a/src/pages/admin/dashboard.rs b/src/pages/admin/dashboard.rs index a59341a..7f435ae 100644 --- a/src/pages/admin/dashboard.rs +++ b/src/pages/admin/dashboard.rs @@ -14,7 +14,7 @@ use crate::api::posts::{get_post_stats, list_posts}; use crate::api::posts::{PostListResponse, PostStatsResponse}; use crate::components::empty_state::{EmptyState, EmptyStateAction}; use crate::components::skeletons::atoms::SkeletonBox; -use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS, BTN_SECONDARY}; +use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS, BTN_PRIMARY, BTN_SECONDARY}; use crate::models::post::{PostListItem, PostStats}; use crate::router::Route; @@ -65,7 +65,7 @@ pub fn Admin() -> Element { "管理文章" } Link { - class: "px-6 py-2 rounded-full text-sm font-medium text-[var(--color-paper-theme)] bg-[var(--color-paper-primary)] hover:opacity-90 shadow-sm transition-all cursor-pointer", + class: "{BTN_PRIMARY}", to: Route::Write {}, "发布文章" } diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index e7b949c..1fd3993 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -16,8 +16,8 @@ use crate::components::empty_state::{EmptyState, EmptyStateAction}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::posts_skeleton::PostsSkeleton; use crate::components::ui::{ - Pagination, StatusBadge, Tooltip, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_TEXT_ACCENT, - BTN_TEXT_RED, SPINNER_SVG, + Pagination, StatusBadge, Tooltip, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, BTN_OUTLINE, + BTN_PRIMARY, BTN_TEXT_ACCENT, BTN_TEXT_RED, SPINNER_SVG, }; use crate::hooks::query::use_paginated; use crate::models::post::PostListItem; @@ -88,7 +88,7 @@ pub fn PostsPage(page: i32) -> Element { rebuild_result: rebuild_result, } Link { - class: "px-6 py-2 rounded-full text-sm font-medium text-[var(--color-paper-theme)] bg-[var(--color-paper-primary)] hover:opacity-90 shadow-sm transition-all cursor-pointer", + class: "{BTN_PRIMARY}", to: Route::Write {}, "发布文章" } @@ -244,7 +244,7 @@ fn RebuildCacheBar( tip: "重建 content_html 为空的文章渲染缓存".to_string(), placement: "bottom", button { - class: if rebuilding() { "relative 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-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" }, + class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE }, disabled: rebuilding(), onclick: move |_| do_rebuild(false), span { @@ -263,7 +263,7 @@ fn RebuildCacheBar( tip: "重建所有文章的渲染缓存(含已有内容)".to_string(), placement: "bottom", button { - class: if rebuilding() { "relative 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-paper-primary border border-paper-border hover:border-paper-accent hover:text-paper-accent transition-all" }, + class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE }, disabled: rebuilding(), onclick: move |_| do_rebuild(true), span { diff --git a/src/pages/admin/runner.rs b/src/pages/admin/runner.rs index 40eb339..0386271 100644 --- a/src/pages/admin/runner.rs +++ b/src/pages/admin/runner.rs @@ -9,7 +9,7 @@ use dioxus::prelude::*; use crate::components::code_runner::CodeRunner; -use crate::components::ui::ADMIN_CARD_CLASS; +use crate::components::ui::{ADMIN_CARD_CLASS, BTN_PRIMARY_SM}; use crate::infra::runner_config::ResourceLimits; /// 受支持的语言集合(与 LANGUAGES 注册表 / CODE_RUNNER_LANGUAGES 对齐)。 @@ -72,7 +72,7 @@ pub fn Runner() -> Element { button { key: "{l}", class: (if lang() == *l { - "px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-theme)] bg-[var(--color-paper-accent)] hover:brightness-110 transition shadow-sm cursor-pointer" + BTN_PRIMARY_SM } else { "px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-secondary)] bg-[var(--color-paper-theme)] hover:bg-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)] transition cursor-pointer" }).to_string(), diff --git a/src/pages/admin/system.rs b/src/pages/admin/system.rs index 8712428..5a2d985 100644 --- a/src/pages/admin/system.rs +++ b/src/pages/admin/system.rs @@ -5,7 +5,9 @@ use dioxus::prelude::*; -use crate::components::ui::FilterTabs; +use crate::components::ui::{ + BTN_OUTLINE, BTN_PRIMARY_SM, BTN_TEXT_AMBER, BTN_TEXT_RED, FilterTabs, LoadingButton, +}; /// 系统管理的 5 个功能 tab。 #[derive(Clone, Copy, PartialEq, Debug)] @@ -226,9 +228,10 @@ fn DbStatusTab() -> Element { div { class: "space-y-6", // 工具栏:刷新按钮 + 自动刷新开关 div { class: "flex items-center justify-between", - button { - class: "px-3 py-1.5 text-sm bg-paper-accent text-paper-theme rounded hover:brightness-110 transition disabled:opacity-50", - disabled: loading(), + LoadingButton { + label: "刷新".to_string(), + loading: loading(), + variant: "sm", onclick: move |_| { loading.set(true); #[cfg(target_arch = "wasm32")] @@ -249,7 +252,6 @@ fn DbStatusTab() -> Element { loading.set(false); } }, - if loading() { "加载中..." } else { "刷新" } } div { class: "flex items-center gap-2", span { class: "text-sm text-paper-secondary", "自动刷新" } @@ -535,9 +537,10 @@ fn ServerStatusTab() -> Element { rsx! { div { class: "space-y-6", div { class: "flex items-center justify-between", - button { - class: "px-3 py-1.5 text-sm bg-paper-accent text-paper-theme rounded hover:brightness-110 transition disabled:opacity-50", - disabled: loading(), + LoadingButton { + label: "刷新".to_string(), + loading: loading(), + variant: "sm", onclick: move |_| { loading.set(true); #[cfg(target_arch = "wasm32")] @@ -558,7 +561,6 @@ fn ServerStatusTab() -> Element { loading.set(false); } }, - if loading() { "加载中..." } else { "刷新" } } div { class: "flex items-center gap-2", span { class: "text-sm text-paper-secondary", "自动刷新" } @@ -680,7 +682,7 @@ fn SqlConsoleTab() -> Element { use crate::api::database::sql_console::{execute_sql, ExecuteSqlOpts}; #[cfg(target_arch = "wasm32")] use crate::api::database::schema::get_db_schema; - use crate::components::ui::{ADMIN_TABLE_CLASS, SPINNER_SVG}; + use crate::components::ui::ADMIN_TABLE_CLASS; #[cfg(target_arch = "wasm32")] use crate::codemirror_bridge; // use_resolved_theme 两种构建都用:resolved() 在 wasm 块内消费,非 wasm 仅引用避免警告。 @@ -917,18 +919,10 @@ fn SqlConsoleTab() -> Element { // 工具条:执行按钮 + 普通/危险选项分层 div { class: "flex flex-wrap items-center gap-x-4 gap-y-3", - button { - class: "inline-flex items-center gap-1.5 px-5 py-2 text-sm font-medium rounded-full text-[var(--color-paper-theme)] bg-[var(--color-paper-accent)] hover:brightness-110 active:scale-[0.98] transition disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer", - disabled: running(), + LoadingButton { + label: "执行".to_string(), + loading: running(), onclick: move |_| run_sql(), - if running() { - span { class: "inline-block w-3.5 h-3.5 text-[var(--color-paper-theme)]", - dangerous_inner_html: SPINNER_SVG, - } - "执行中" - } else { - "执行" - } } // 普通选项 label { class: "flex items-center gap-1.5 text-sm text-[var(--color-paper-secondary)] cursor-pointer", @@ -1145,7 +1139,7 @@ fn ExportTab() -> Element { } button { - class: "px-4 py-1.5 text-sm bg-paper-accent text-paper-theme rounded hover:brightness-110 transition", + class: "{BTN_PRIMARY_SM}", onclick: move |_| do_export(), "导出并下载" } @@ -1343,9 +1337,10 @@ fn BackupTab() -> Element { div { class: "space-y-4", // 操作栏 div { class: "flex items-center gap-3", - button { - class: "px-4 py-1.5 text-sm bg-paper-accent text-paper-theme rounded hover:brightness-110 transition disabled:opacity-50", - disabled: is_busy, + LoadingButton { + label: "创建备份".to_string(), + loading: is_busy, + variant: "sm", onclick: move |_| { #[cfg(target_arch = "wasm32")] { @@ -1360,10 +1355,9 @@ fn BackupTab() -> Element { }); } }, - if is_busy { "处理中..." } else { "创建备份" } } button { - class: "px-3 py-1.5 text-sm border border-paper-border text-paper-primary rounded hover:bg-paper-entry transition disabled:opacity-50", + class: "{BTN_OUTLINE}", disabled: loading() || is_busy, onclick: move |_| refresh_list(), "刷新列表" @@ -1502,13 +1496,13 @@ fn BackupRow(props: BackupRowProps) -> Element { "下载" } button { - class: "text-xs text-amber-600 hover:text-amber-800 dark:text-amber-400 mr-3 disabled:opacity-50", + class: "{BTN_TEXT_AMBER} mr-3 disabled:opacity-50", disabled: props.busy, onclick: move |_| on_restore.call(fname_for_restore.clone()), "恢复" } button { - class: "text-xs text-red-600 hover:text-red-800 dark:text-red-400 disabled:opacity-50", + class: "{BTN_TEXT_RED} disabled:opacity-50", disabled: props.busy, onclick: move |_| on_delete.call(fname_for_delete.clone()), "删除" diff --git a/src/pages/admin/trash.rs b/src/pages/admin/trash.rs index 614d4a4..559a38b 100644 --- a/src/pages/admin/trash.rs +++ b/src/pages/admin/trash.rs @@ -21,8 +21,9 @@ use crate::components::empty_state::EmptyState; use crate::components::skeletons::atoms::SkeletonBox; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::ui::{ - 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, + Pagination, StatusBadge, ADMIN_CARD_CLASS, ADMIN_ROW_HOVER, ADMIN_TABLE_CLASS, + BTN_DANGER_OUTLINE, BTN_ICON, BTN_SOLID_GREEN, BTN_SOLID_RED, BTN_TEXT_ACCENT, BTN_TEXT_RED, + CHECKBOX_CLASS, LoadingButton, }; use crate::hooks::query::use_paginated; use crate::models::post::PostListItem; @@ -263,7 +264,7 @@ pub fn TrashPage(page: i32) -> Element { // 底部:清空回收站 + 分页 div { class: "flex items-center justify-between mt-4", button { - class: "px-4 py-2 text-sm font-medium text-red-600 dark:text-red-400 border border-red-300 dark:border-red-900/50 rounded-full hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors cursor-pointer", + class: "{BTN_DANGER_OUTLINE}", onclick: move |_| { #[cfg(target_arch = "wasm32")] { @@ -450,7 +451,7 @@ fn AutoPurgeSettings(settings: Signal) -> Element { 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-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", + class: "{BTN_ICON}", r#type: "button", aria_label: "减少保留天数", onclick: move |_| { @@ -475,7 +476,7 @@ fn AutoPurgeSettings(settings: Signal) -> Element { } // 加号 button { - 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", + class: "{BTN_ICON}", r#type: "button", aria_label: "增加保留天数", onclick: move |_| { @@ -517,10 +518,12 @@ fn AutoPurgeSettings(settings: Signal) -> 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-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-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-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(), + // 保存按钮:主题绿主操作,saving 态显示 spinner,just_saved/无改动禁用 + LoadingButton { + label: "保存设置".to_string(), + loading: saving_settings(), + disabled: just_saved() || !dirty(), + variant: "sm", onclick: move |_| { let days: i32 = settings_draft_days().parse().unwrap_or(30); let enabled = settings_draft_enabled(); @@ -533,11 +536,6 @@ fn AutoPurgeSettings(settings: Signal) -> Element { saving_settings.set(false); }); }, - if saving_settings() { - "保存中…" - } else { - "保存设置" - } } } } diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index 12eef29..e9a9deb 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -17,6 +17,7 @@ use crate::api::posts::{ #[cfg(target_arch = "wasm32")] use crate::tiptap_bridge::{consume_upload_event, upload_image_file, EditorHandle}; // 共享上传状态类型:两端都编译(rsx 在 server SSR 时也要渲染这些结构)。 +use crate::components::ui::{BTN_CLOSE_ICON, BTN_PRIMARY_SM, LoadingButton}; use crate::components::write_skeleton::WriteSkeleton; use crate::models::post::Post; use crate::router::Route; @@ -241,7 +242,7 @@ fn write_editor(post_id: Option) -> Element { }); // 提交表单:校验标题与内容,读取 Tiptap 编辑器 Markdown,调用 create_post 或 update_post。 - let on_submit = move |_| { + let mut on_submit = move |_| { // 上传未完成/失败拦截:有占位符时阻止保存 let in_flight = uploads_in_flight.read(); if in_flight.uploading > 0 || in_flight.error > 0 { @@ -504,7 +505,7 @@ fn write_editor(post_id: Option) -> Element { class: "flex-shrink-0 flex items-center justify-between gap-3 px-4 py-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl text-sm border border-red-100 dark:border-red-900/30 mb-2", span { "图片上传失败: {err.file_name} — {err.message}" } button { - class: "shrink-0 text-red-400 hover:text-red-600 cursor-pointer text-lg leading-none", + class: "{BTN_CLOSE_ICON}", aria_label: "关闭提示", onclick: { // 捕获 owned id,避免借用临时值 @@ -564,11 +565,10 @@ fn write_editor(post_id: Option) -> Element { } } div { class: "w-px h-5 bg-[var(--color-paper-border)]" } - button { - class: if saving() { "px-6 py-2 rounded-full text-sm font-medium bg-[var(--color-paper-tertiary)] text-[var(--color-paper-secondary)] cursor-not-allowed shadow-sm" } else { "px-6 py-2 rounded-full text-sm font-medium bg-[var(--color-paper-primary)] text-[var(--color-paper-theme)] hover:opacity-90 shadow-sm transition-all cursor-pointer" }, - disabled: saving(), - onclick: on_submit, - if saving() { "保存中..." } else if is_edit { "更新文章" } else { "发布文章" } + LoadingButton { + label: if is_edit { "更新文章".to_string() } else { "发布文章".to_string() }, + loading: saving(), + onclick: move |_| on_submit(()), } } } @@ -814,7 +814,7 @@ fn CoverUploader(cover_image: Signal, cover_uploading: Signal) -> }, } button { - class: "shrink-0 px-4 py-1.5 rounded-full text-sm font-medium text-[var(--color-paper-theme)] bg-[var(--color-paper-primary)] hover:opacity-90 shadow-sm transition-all cursor-pointer", + class: "shrink-0 {BTN_PRIMARY_SM}", onclick: move |_| { let v = cover_url_input().trim().to_string(); if !v.is_empty() { @@ -841,7 +841,7 @@ fn CoverUploader(cover_image: Signal, cover_uploading: Signal) -> div { class: "flex items-center justify-between gap-3 px-4 py-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl text-sm border border-red-100 dark:border-red-900/30 mt-2", span { "封面图: {err}" } button { - class: "shrink-0 text-red-400 hover:text-red-600 cursor-pointer text-lg leading-none", + class: "{BTN_CLOSE_ICON}", aria_label: "关闭提示", onclick: move |_| cover_error.set(None), "×"