280 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 评论表单组件
//!
//! 提供发表评论与回复评论的表单,包含昵称、邮箱、网站、内容与反垃圾蜜罐字段。
use dioxus::prelude::*;
use crate::api::comments::create_comment;
use crate::components::comments::section::CommentContext;
use crate::components::forms::{AlertBox, INPUT_CLASS};
use crate::hooks::comment_storage::{self, PendingComment};
/// 评论提交按钮样式:去掉全宽,改为内联宽度并右对齐。
///
/// 与 `BUTTON_PRIMARY_CLASS` 视觉一致,但不含 `w-full`,并把 `px-4` 加宽为 `px-6`
/// 使按钮宽度跟随文字、更适合文章页内联场景。
const COMMENT_SUBMIT_CLASS: &str = "py-2.5 px-6 bg-paper-accent text-white font-medium rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer";
/// 评论表单组件,用于顶层评论或回复评论。
///
/// Props
/// - `post_id`:所属文章 ID
/// - `parent_id`:回复目标评论 ID`None` 表示顶层评论
/// - `parent_indent`:回复时父评论的缩进像素值,用于用负 margin 把表单拉回内容区左边缘
///
/// 关键事件:
/// - 挂载时从本地存储恢复上次填写的作者信息
/// - 提交时校验必填项与蜜罐字段
/// - 提交成功后清空内容、保存作者信息、添加待审核评论并触发列表刷新
#[component]
pub fn CommentForm(post_id: i32, parent_id: Option<i64>, parent_indent: Option<i32>) -> Element {
let ctx: CommentContext = use_context();
let mut active_reply = ctx.active_reply;
let mut refresh_trigger = ctx.refresh_trigger;
let mut pending_comments = ctx.pending_comments;
let mut author_name = use_signal(String::new);
let mut author_email = use_signal(String::new);
let mut author_url = use_signal(String::new);
let mut content_md = use_signal(String::new);
let mut honeypot = use_signal(String::new);
let mut submitting = use_signal(|| false);
let mut message = use_signal(|| Option::<(String, &'static str)>::None);
let mut loaded = use_signal(|| false);
// 首次挂载时从本地存储加载作者信息
use_effect(move || {
if loaded() {
return;
}
loaded.set(true);
if let Some(info) = comment_storage::load_author() {
author_name.set(info.name);
author_email.set(info.email);
author_url.set(info.url);
}
});
// 回复表单:当前未激活回复时隐藏
if let Some(pid) = parent_id {
if active_reply() != Some(pid) {
return rsx! {};
}
}
let is_reply = parent_id.is_some();
// 用于区分顶层表单与多个回复表单的 id 后缀,保证页面内 label/for 关联唯一。
let id_suffix = match parent_id {
Some(pid) => pid.to_string(),
None => "root".to_string(),
};
// 回复表单抵消父评论缩进,让表单回到内容区左边缘,避免深层回复时被越挤越右。
let negative_margin = match (is_reply, parent_indent) {
(true, Some(px)) if px > 0 => format!("margin-left: -{px}px;"),
_ => String::new(),
};
rsx! {
div {
class: if is_reply { "mt-3 pt-3 border-t border-gray-100 dark:border-[#333]" } else { "" },
style: "{negative_margin}",
role: "form",
aria_label: if is_reply { "回复评论" } else { "发表评论" },
if let Some((msg, variant)) = message() {
div { aria_live: "polite",
AlertBox { message: msg, variant }
}
}
div { class: "space-y-3",
div { class: "grid grid-cols-1 sm:grid-cols-2 gap-3",
div {
label {
r#for: "comment-name-{id_suffix}",
class: "block text-sm font-medium text-paper-secondary mb-1",
"昵称 *"
}
input {
id: "comment-name-{id_suffix}",
class: INPUT_CLASS,
r#type: "text",
placeholder: "你的昵称",
value: "{author_name}",
disabled: submitting(),
oninput: move |e| author_name.set(e.value()),
}
}
div {
label {
r#for: "comment-email-{id_suffix}",
class: "block text-sm font-medium text-paper-secondary mb-1",
"邮箱 *"
}
input {
id: "comment-email-{id_suffix}",
class: INPUT_CLASS,
r#type: "email",
placeholder: "your@email.com",
value: "{author_email}",
disabled: submitting(),
oninput: move |e| author_email.set(e.value()),
}
}
}
div {
label {
r#for: "comment-url-{id_suffix}",
class: "block text-sm font-medium text-paper-secondary mb-1",
"网站"
}
input {
id: "comment-url-{id_suffix}",
class: INPUT_CLASS,
r#type: "url",
placeholder: "https://example.com可选",
value: "{author_url}",
disabled: submitting(),
oninput: move |e| author_url.set(e.value()),
}
}
div {
label {
r#for: "comment-content-{id_suffix}",
class: "block text-sm font-medium text-paper-secondary mb-1",
"内容 *"
}
div { class: "relative",
textarea {
id: "comment-content-{id_suffix}",
class: "{INPUT_CLASS} peer min-h-[100px] resize-y",
value: "{content_md}",
disabled: submitting(),
oninput: move |e| content_md.set(e.value()),
}
img {
src: "/images/xiantiaoxiaogou_03.webp",
alt: "",
class: "absolute bottom-2 right-2 w-24 pointer-events-none opacity-60 peer-focus:opacity-10 transition-opacity duration-300",
}
}
}
p { class: "text-xs text-paper-tertiary", "支持 Markdown 语法" }
// 蜜罐字段:对普通用户隐藏,用于拦截简单机器人
textarea {
class: "hidden",
aria_hidden: "true",
tabindex: "-1",
value: "{honeypot}",
oninput: move |e| honeypot.set(e.value()),
}
div { class: "flex justify-end",
button {
class: COMMENT_SUBMIT_CLASS,
disabled: submitting(),
onclick: move |_| {
if submitting() {
return;
}
let post_id = post_id;
let parent_id = parent_id;
let name = author_name();
let email = author_email();
let url_val = author_url();
let content = content_md();
let hp = honeypot();
// 蜜罐被填充则直接丢弃
if !hp.is_empty() {
return;
}
if name.trim().is_empty() || email.trim().is_empty() || content.trim().is_empty()
{
message.set(Some(("请填写所有必填项".to_string(), "error")));
return;
}
submitting.set(true);
message.set(None);
spawn(async move {
let result = create_comment(
post_id,
parent_id,
name.clone(),
email.clone(),
if url_val.trim().is_empty() { None } else { Some(url_val.clone()) },
content.clone(),
hp.clone(),
)
.await;
submitting.set(false);
match result {
Ok(resp) => {
if resp.success {
comment_storage::save_author(&name, &email, &url_val);
if let Some(comment_id) = resp.comment_id {
let avatar_url = resp.avatar_url.unwrap_or_default();
let depth = resp.depth.unwrap_or(0);
let now = chrono::Utc::now().to_rfc3339();
let pending = PendingComment {
id: comment_id,
parent_id,
depth,
author_name: name.clone(),
author_url: if url_val.trim().is_empty() {
None
} else {
Some(url_val)
},
avatar_url,
content_md: content,
created_at: now.clone(),
stored_at: now,
};
comment_storage::save_pending_comment(
post_id,
pending.clone(),
);
pending_comments.write().push(pending);
}
content_md.set(String::new());
message.set(Some((resp.message, "success")));
if parent_id.is_some() {
active_reply.set(None);
}
refresh_trigger.set(!refresh_trigger());
} else {
message.set(Some((resp.message, "error")));
}
}
Err(_) => {
message
.set(
Some(("提交失败,请稍后重试".to_string(), "error")),
);
}
}
});
},
if submitting() {
"提交中…"
} else if is_reply {
"回复"
} else {
"发表评论"
}
}
}
}
}
}
}