refactor(admin): 统一按钮令牌,消除样式散落

将 7 个 admin 页面中复制粘贴/各自为政的按钮 class 收敛到 ui.rs 令牌:

主操作色系统一为主题绿(bg-paper-accent):
- dashboard/posts「发布文章」、write「发布/更新」+「确认」、runner 语言切换选中态
  改用 BTN_PRIMARY / BTN_PRIMARY_SM(原为深色 bg-paper-primary,现统一为绿)
- system 4 处刷新/执行/创建备份、trash 保存设置、write 发布按钮改用 LoadingButton
  统一 loading 态为 spinner 绝对居中叠加(按钮宽度不变,原 system 用文字切换、
  write 用整体变灰,现全部一致)

描边/图标/行内按钮统一:
- posts 重建、system 刷新列表 → BTN_OUTLINE
- trash 清空回收站 → BTN_DANGER_OUTLINE
- trash 步进 ± 按钮(2 处重复字符串)→ BTN_ICON
- write 关闭 × 按钮(2 处重复)→ BTN_CLOSE_ICON
- system BackupRow 恢复/删除绕过令牌 → BTN_TEXT_AMBER / BTN_TEXT_RED
  (删除色从 red-600 校正为令牌的 red-500,与其他行内删除一致)

消除约 15 处内联 class 字符串重复,system.rs 4 种主操作变体收敛为 1 种。
This commit is contained in:
xfy 2026-07-06 00:17:25 +08:00
parent 545651d304
commit b207babdb6
6 changed files with 53 additions and 61 deletions

View File

@ -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 {},
"发布文章"
}

View File

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

View File

@ -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(),

View File

@ -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()),
"删除"

View File

@ -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<TrashSettings>) -> 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<TrashSettings>) -> 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<TrashSettings>) -> 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 态显示 spinnerjust_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<TrashSettings>) -> Element {
saving_settings.set(false);
});
},
if saving_settings() {
"保存中…"
} else {
"保存设置"
}
}
}
}

View File

@ -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<i32>) -> 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<i32>) -> 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<i32>) -> 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<String>, cover_uploading: Signal<bool>) ->
},
}
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<String>, cover_uploading: Signal<bool>) ->
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),
"×"