diff --git a/src/components/forms.rs b/src/components/forms.rs index 9b6853e..2b0ee9e 100644 --- a/src/components/forms.rs +++ b/src/components/forms.rs @@ -7,6 +7,10 @@ use dioxus::prelude::*; /// 输入框基础 CSS 类,统一文本框、邮箱框、URL 框等样式。 pub const INPUT_CLASS: &str = "w-full px-4 py-2 border border-paper-border rounded-2xl bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors duration-200"; +/// 内联输入框 CSS 类:与 [`INPUT_CLASS`] 同主题,但用 `flex-1 min-w-0` 取代 `w-full`, +/// 用于与按钮并排、需填充剩余宽度的场景(搜索栏、URL 输入栏等)。 +pub const INPUT_INLINE_CLASS: &str = "flex-1 min-w-0 px-4 py-2 border border-paper-border rounded-2xl bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors duration-200"; + /// 主按钮 CSS 类,用于表单提交等主操作按钮。 pub const BUTTON_PRIMARY_CLASS: &str = "w-full py-2.5 px-4 bg-paper-accent text-white font-medium rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer"; @@ -317,6 +321,9 @@ pub fn FormSelect( /// - `disabled`:是否禁用(可选,缺省 `false`) /// - `oninput`:输入事件回调,返回新的字符串值 /// - `onkeydown`:可选的键盘事件回调 +/// - `class`:自定义 class(可选,缺省用 [`INPUT_CLASS`] 全宽表单款)。 +/// 内联/固定宽度场景传 [`INPUT_INLINE_CLASS`] 或自定义类串,覆盖默认样式。 +/// - `mono`:是否使用等宽字体(代码片段、表名、JSON 等,缺省 `false`) #[component] pub fn FormInput( id: Option, @@ -328,16 +335,22 @@ pub fn FormInput( oninput: EventHandler, #[props(default)] onkeydown: Option>, + #[props(default)] + class: Option<&'static str>, + #[props(default)] + mono: bool, ) -> Element { + let base = class.unwrap_or(INPUT_CLASS); + let mono_class = if mono { " font-mono" } else { "" }; let disabled_class = if disabled { - "opacity-60 cursor-not-allowed" + " opacity-60 cursor-not-allowed" } else { "" }; rsx! { input { id: id.unwrap_or_default(), - class: "{INPUT_CLASS} {disabled_class}", + class: "{base}{mono_class}{disabled_class}", r#type: "{r#type}", placeholder: "{placeholder}", value: "{value}", diff --git a/src/pages/admin/asset_picker.rs b/src/pages/admin/asset_picker.rs index 6f14e47..1135ae7 100644 --- a/src/pages/admin/asset_picker.rs +++ b/src/pages/admin/asset_picker.rs @@ -5,6 +5,7 @@ //! 纯 Dioxus 组件,不触碰 Tiptap;数据加载仅在 WASM 前端发生。 use dioxus::prelude::*; +use crate::components::forms::{FormInput, INPUT_INLINE_CLASS}; #[cfg(target_arch = "wasm32")] use crate::api::assets::list_assets; @@ -69,12 +70,12 @@ pub fn AssetPickerModal( h2 { class: "text-lg font-bold text-[var(--color-paper-primary)] shrink-0", "选择封面图" } - input { - class: "flex-1 min-w-0 text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-4 py-2 shadow-sm transition-all", + FormInput { r#type: "search", placeholder: "搜索文件名 / alt", - value: "{query}", - oninput: move |evt| query.set(evt.value()), + value: query(), + class: INPUT_INLINE_CLASS, + oninput: move |v: String| query.set(v), } // 上传新图:成功后直接选中 label { class: "shrink-0 text-sm font-medium px-4 py-2 rounded-full bg-[var(--color-paper-primary)] text-[var(--color-paper-theme)] hover:opacity-80 transition-opacity cursor-pointer", diff --git a/src/pages/admin/assets.rs b/src/pages/admin/assets.rs index 9c42a56..b76f505 100644 --- a/src/pages/admin/assets.rs +++ b/src/pages/admin/assets.rs @@ -22,6 +22,7 @@ use crate::api::assets::AssetListResponse; use crate::api::assets::{BatchDeleteAssetsResponse, PurgeOrphansResponse, RebuildAssetsResponse}; use crate::components::empty_state::EmptyState; use crate::components::ui::{FilterTabs, Pagination}; +use crate::components::forms::FormInput; #[cfg(target_arch = "wasm32")] use crate::utils::js::invoke_optional_global; #[cfg(target_arch = "wasm32")] @@ -247,14 +248,12 @@ pub fn Assets() -> Element { "重建索引" } } - input { - class: "w-56 text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-4 py-2 shadow-sm transition-all", + FormInput { r#type: "search", placeholder: "搜索文件名 / alt", - value: "{query}", - oninput: move |evt| { - query.set(evt.value()); - }, + value: query(), + class: "w-56 text-sm px-4 py-2 border border-paper-border rounded-2xl bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors", + oninput: move |v: String| query.set(v), } button { class: "{sort_created_class}", @@ -576,12 +575,12 @@ pub fn Assets() -> Element { } } else if editing_alt().as_deref() == Some(a.id.as_str()) { div { class: "flex items-center gap-1 mt-2", - input { - class: "flex-1 min-w-0 text-[10px] bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-full px-2 py-1 transition-all", + FormInput { r#type: "text", placeholder: "alt 文本", - value: "{alt_input}", - oninput: move |evt| alt_input.set(evt.value()), + value: alt_input(), + class: "flex-1 min-w-0 text-[10px] px-2 py-1 rounded-full border border-paper-border bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent transition-colors", + oninput: move |v: String| alt_input.set(v), } button { class: "text-[10px] font-medium cursor-pointer px-2 py-1 rounded-full bg-[var(--color-paper-primary)] text-[var(--color-paper-theme)] hover:opacity-80 transition-opacity", diff --git a/src/pages/admin/runner.rs b/src/pages/admin/runner.rs index 963e2ba..1067dd4 100644 --- a/src/pages/admin/runner.rs +++ b/src/pages/admin/runner.rs @@ -9,6 +9,7 @@ use dioxus::prelude::*; use crate::components::code_runner::CodeRunner; +use crate::components::forms::FormInput; use crate::components::ui::{ADMIN_CARD_CLASS, BTN_PRIMARY_SM}; use crate::infra::runner_config::ResourceLimits; @@ -107,12 +108,12 @@ pub fn Runner() -> Element { label { class: "text-sm font-medium text-[var(--color-paper-secondary)]", "资源覆盖 (JSON, 可选)" } - input { - class: "w-full px-3 py-2 text-sm border border-paper-border rounded-lg bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] font-mono focus:outline-none focus:border-[var(--color-paper-accent)] transition-colors", + FormInput { r#type: "text", placeholder: "如 {{\"timeout_secs\":10,\"memory_mb\":512}}", - value: "{overrides_json()}", - oninput: move |e| overrides_json.set(e.value()), + value: overrides_json(), + mono: true, + oninput: move |v: String| overrides_json.set(v), } if !override_error.is_empty() { p { class: "text-xs text-red-500 dark:text-red-400", "{override_error}" } diff --git a/src/pages/admin/system/export.rs b/src/pages/admin/system/export.rs index b1cdb55..da72b54 100644 --- a/src/pages/admin/system/export.rs +++ b/src/pages/admin/system/export.rs @@ -2,7 +2,7 @@ use dioxus::prelude::*; -use crate::components::forms::{FORM_SELECT_COMPACT_CLASS, FormSelect}; +use crate::components::forms::{FormInput, FORM_SELECT_COMPACT_CLASS, FormSelect}; use crate::components::ui::BTN_PRIMARY_SM; /// 数据导出 tab:按表/按查询导出 SQL/CSV,走 Axum 流式下载。 @@ -66,12 +66,12 @@ pub(super) fn ExportTab() -> Element { if mode().as_str() == "table" { div { label { class: "block text-sm text-paper-secondary mb-1", "表名" } - input { + FormInput { r#type: "text", - class: "w-full px-3 py-2 text-sm border border-paper-border rounded bg-paper-theme text-paper-primary font-mono", placeholder: "如 posts", - value: "{table_name}", - oninput: move |e| table_name.set(e.value()), + value: table_name(), + mono: true, + oninput: move |v: String| table_name.set(v), } p { class: "text-xs text-paper-secondary mt-1", "仅支持 public schema 下的用户表,表名需为合法标识符" diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index 5ed493f..7b54e8e 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -17,7 +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::forms::FormSelect; +use crate::components::forms::{FormInput, FormSelect, INPUT_INLINE_CLASS}; use crate::components::ui::{LoadingButton, BTN_CLOSE_ICON, BTN_PRIMARY_SM}; use crate::components::write_skeleton::WriteSkeleton; use crate::models::post::Post; @@ -36,11 +36,6 @@ use dioxus::html::HasFileData; #[cfg(target_arch = "wasm32")] use dioxus::web::{WebEventExt, WebFileExt}; -/// 元信息表单复用的样式常量(label 与 input 各一份,避免多处重复粘贴)。 -/// 提升到模块级以便 CoverUploader 的 URL 输入框复用。 -const META_INPUT_CLASS: &str = - "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-4 py-3 shadow-sm transition-all"; - /// 新建文章页面组件。 /// /// 内部委托给 `write_editor`,以 `None` 表示新建模式。 @@ -408,7 +403,6 @@ fn write_editor(post_id: Option) -> Element { } }; - // 元信息表单复用样式见模块级 META_INPUT_CLASS。 rsx! { // 根容器:flex 分区布局。layout 给 write 的 main 是 flex 容器(无 padding/不滚动), @@ -496,11 +490,11 @@ fn write_editor(post_id: Option) -> Element { label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "链接" } - input { - class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all", + FormInput { + r#type: "text", placeholder: "自动生成", - value: "{slug}", - oninput: move |evt| slug.set(evt.value()), + value: slug(), + oninput: move |v: String| slug.set(v), } } // 标签节 @@ -508,11 +502,11 @@ fn write_editor(post_id: Option) -> Element { label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "标签" } - input { - class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all", + FormInput { + r#type: "text", placeholder: "逗号分隔...", - value: "{tags}", - oninput: move |evt| tags.set(evt.value()), + value: tags(), + oninput: move |v: String| tags.set(v), } } // 摘要节 @@ -809,12 +803,13 @@ fn CoverUploader(cover_image: Signal, cover_uploading: Signal) -> // —— URL 输入模式(内联展开,空态时叠加在容器外,避免与拖拽区争抢点击)—— if cover_url_mode() && cover_image().is_empty() { div { class: "flex items-center gap-2 mt-2", - input { - class: "flex-1 {META_INPUT_CLASS}", + FormInput { + r#type: "url", placeholder: "粘贴图片链接...", - value: "{cover_url_input}", - oninput: move |evt| cover_url_input.set(evt.value()), - onkeydown: move |evt| { + value: cover_url_input(), + class: INPUT_INLINE_CLASS, + oninput: move |v: String| cover_url_input.set(v), + onkeydown: move |evt: KeyboardEvent| { if evt.key() == Key::Enter { let v = cover_url_input().trim().to_string(); if !v.is_empty() {