第八章:表单处理与用户输入

博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)

第八章:表单处理与用户输入

1. 受控组件

在 Dioxus 中,表单控件的值与 Signal 绑定,通过事件同步更新:

#[component]
fn LoginForm() -> Element {
    // Signal 是唯一数据源
    let username = use_signal(String::new);
    let password = use_signal(String::new);

    rsx! {
        form { class: "space-y-4",
            div { class: "mb-4",
                label { class: "block text-sm mb-1", "用户名" }
                input {
                    class: "w-full border rounded-lg px-3 py-2",
                    value: "{username}",          // 显示 Signal 的值
                    oninput: move |e| username.set(e.value()), // 用户输入时更新
                    placeholder: "请输入用户名",
                }
            }
            div { class: "mb-4",
                label { class: "block text-sm mb-1", "密码" }
                input {
                    r#type: "password",
                    class: "w-full border rounded-lg px-3 py-2",
                    value: "{password}",
                    oninput: move |e| password.set(e.value()),
                }
            }
        }
    }
}

2. 输入事件

2.1 常见事件

// oninput —— 输入时触发(推荐)
input {
    oninput: move |e| println!("输入: {}", e.value()),
}

// onchange —— 失焦且值变化时触发
input {
    onchange: move |e| println!("最终值: {}", e.value()),
}

// onkeydown —— 按键
input {
    onkeydown: move |e| {
        if e.key() == Key::Enter {
            println!("按下了回车");
        }
        if e.key() == Key::Escape {
            println!("按下了 Escape");
        }
    },
}

2.2 不同类型输入

#[component]
fn AllInputs() -> Element {
    let text = use_signal(String::new);
    let number = use_signal(|| 0u32);
    let checked = use_signal(|| false);
    let selected = use_signal(|| "rust".to_string());

    rsx! {
        // 文本
        input { value: "{text}", oninput: move |e| text.set(e.value()) }

        // 数字
        input {
            r#type: "number",
            value: "{number}",
            oninput: move |e| {
                if let Ok(n) = e.value().parse::<u32>() {
                    number.set(n);
                }
            },
        }

        // 复选框
        label {
            input {
                r#type: "checkbox",
                checked: "{checked}",
                oninput: move |e| checked.set(e.checked()),
            }
            "同意条款"
        }

        // 下拉选择
        select {
            value: "{selected}",
            oninput: move |e| selected.set(e.value()),
            option { value: "rust", "Rust" }
            option { value: "python", "Python" }
            option { value: "js", "JavaScript" }
        }

        // 文本域
        textarea {
            value: "{text}",
            oninput: move |e| text.set(e.value()),
        }
    }
}

3. 表单提交

3.1 基础提交

#[component]
fn RegisterForm() -> Element {
    let username = use_signal(String::new);
    let email = use_signal(String::new);
    let submitting = use_signal(|| false);

    let handle_submit = move |_| {
        if submitting() { return; }
        submitting.set(true);

        // 异步提交
        spawn(async move {
            let payload = serde_json::json!({
                "username": username(),
                "email": email(),
            });
            // 调用 API...
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            submitting.set(false);
        });
    };

    rsx! {
        form {
            class: "max-w-md mx-auto",
            onsubmit: handle_submit,

            // 表单字段...
            button {
                class: "w-full py-2 rounded-lg text-white font-medium",
                style: "background: var(--primary);",
                disabled: submitting(),
                if submitting() { "提交中..." } else { "注册" }
            }
        }
    }
}

3.2 阻止默认提交

rsx! {
    form {
        onsubmit: move |e| {
            e.prevent_default();  // 阻止浏览器默认提交
            // 处理表单数据...
        },
        // ...
    }
}

4. 表单验证

4.1 实时验证

#[component]
fn ValidatedInput() -> Element {
    let email = use_signal(String::new);

    // 派生验证状态
    let email_valid = use_memo(move || {
        let val = email();
        if val.is_empty() {
            None  // 未输入
        } else if val.contains('@') && val.contains('.') {
            Some(true)
        } else {
            Some(false)
        }
    });

    rsx! {
        div { class: "mb-4",
            input {
                class: "w-full border rounded-lg px-3 py-2
                        {match email_valid() {
                            Some(false) => \"border-red-500\",
                            _ => \"border-gray-300\",
                        }}",
                value: "{email}",
                oninput: move |e| email.set(e.value()),
            }
            match email_valid() {
                Some(false) => rsx! {
                    p { class: "text-red-500 text-xs mt-1", "邮箱格式不正确" }
                },
                Some(true) => rsx! {
                    p { class: "text-green-500 text-xs mt-1", "✓ 邮箱格式正确" }
                },
                None => rsx! { div {} },
            }
        }
    }
}

4.2 验证函数

fn validate_username(name: &str) -> Result<(), &'static str> {
    if name.len() < 3 { return Err("用户名至少3个字符"); }
    if name.len() > 20 { return Err("用户名不超过20个字符"); }
    if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
        return Err("只能包含字母、数字和下划线");
    }
    Ok(())
}

fn validate_email(email: &str) -> Result<(), &'static str> {
    if !email.contains('@') { return Err("邮箱必须包含 @"); }
    if !email.contains('.') { return Err("邮箱格式不正确"); }
    Ok(())
}

5. 表单组组件

#[component]
fn FormField(
    label: String,
    error: Option<String>,
    children: Element,
) -> Element {
    rsx! {
        div { class: "mb-4",
            label { class: "block text-sm font-medium mb-1",
                style: "color: var(--text);",
                "{label}"
            }
            {children}
            if let Some(ref msg) = error {
                p { class: "text-red-500 text-xs mt-1",
                    role: "alert",
                    "{msg}"
                }
            }
        }
    }
}

// 使用
FormField {
    label: "邮箱".to_string(),
    error: email_error(),
    input {
        class: "w-full border rounded-lg px-3 py-2",
        value: "{email}",
        oninput: move |e| email.set(e.value()),
    }
}

6. 搜索输入框

#[component]
fn SearchInput(on_search: EventHandler<String>) -> Element {
    let value = use_signal(String::new);

    rsx! {
        div { class: "relative",
            input {
                class: "w-full border rounded-lg pl-10 pr-4 py-2",
                value: "{value}",
                oninput: move |e| value.set(e.value()),
                onkeydown: move |e| {
                    if e.key() == Key::Enter {
                        on_search.call(value());
                    }
                },
                placeholder: "搜索文章...",
            }
            // 搜索图标
            svg { class: "absolute left-3 top-2.5 w-4 h-4 text-gray-400", ... }
            // 清除按钮
            if !value().is_empty() {
                button {
                    class: "absolute right-3 top-2.5 text-gray-400",
                    onclick: move |_| value.set(String::new()),
                    "×"
                }
            }
        }
    }
}

7. 实战:评论表单

#[component]
fn CommentForm(article_slug: String) -> Element {
    let content = use_signal(String::new);
    let submitting = use_signal(|| false);
    let error = use_signal(|| Option::<String>::None);
    let success = use_signal(|| false);

    let submit = move |_| {
        if content().trim().is_empty() {
            error.set(Some("评论内容不能为空".to_string()));
            return;
        }
        error.set(None);
        submitting.set(true);

        let slug = article_slug.clone();
        let body = content();
        spawn(async move {
            match post_comment(&slug, &body).await {
                Ok(_) => {
                    content.set(String::new());
                    success.set(true);
                }
                Err(e) => error.set(Some(e)),
            }
            submitting.set(false);
        });
    };

    rsx! {
        form { class: "space-y-3", onsubmit: submit,
            h3 { class: "font-semibold", "发表评论" }

            if success() {
                div { class: "bg-green-100 text-green-700 px-4 py-2 rounded text-sm",
                    "评论发表成功!"
                }
            }

            if let Some(ref msg) = error() {
                div { class: "bg-red-100 text-red-700 px-4 py-2 rounded text-sm",
                    "{msg}"
                }
            }

            textarea {
                class: "w-full border rounded-lg p-3 h-24 resize-none",
                value: "{content}",
                oninput: move |e| content.set(e.value()),
                placeholder: "写下你的评论...",
            }

            div { class: "flex items-center justify-between",
                span { class: "text-xs",
                    style: "color: var(--tertiary);",
                    "{content().len()} / 500"
                }
                button {
                    class: "px-4 py-2 rounded-lg text-white text-sm",
                    style: "background: var(--primary);",
                    disabled: submitting() || content().len() > 500,
                    if submitting() { "提交中..." } else { "发表评论" }
                }
            }
        }
    }
}

8. 小结

  • 使用 value + oninput 实现受控组件模式
  • 不同类型输入(text, number, checkbox, select, textarea)各有对应事件
  • onsubmit 处理表单提交,prevent_default() 阻止浏览器默认行为
  • use_memo 派生验证状态,实时显示错误提示
  • 封装 FormField 组件复用标签+错误提示结构
  • 提交时禁用按钮、显示加载状态,提升用户体验
  • 下一章将学习异步编程与数据获取
dioxusforminputeventvalidation