yggdrasil/components/comments/
form.rs1use dioxus::prelude::*;
6
7use crate::api::comments::create_comment;
8use crate::components::comments::section::CommentContext;
9use crate::components::forms::{AlertBox, INPUT_CLASS};
10use crate::utils::comment_storage::{self, PendingComment};
11
12const 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";
17
18#[component]
30pub fn CommentForm(post_id: i32, parent_id: Option<i64>, parent_indent: Option<i32>) -> Element {
31 let ctx: CommentContext = use_context();
32 let mut active_reply = ctx.active_reply;
33 let mut refresh_trigger = ctx.refresh_trigger;
34 let mut pending_comments = ctx.pending_comments;
35
36 let mut author_name = use_signal(String::new);
37 let mut author_email = use_signal(String::new);
38 let mut author_url = use_signal(String::new);
39 let mut content_md = use_signal(String::new);
40 let mut honeypot = use_signal(String::new);
41 let mut submitting = use_signal(|| false);
42 let mut message = use_signal(|| Option::<(String, &'static str)>::None);
43 let mut loaded = use_signal(|| false);
44
45 use_effect(move || {
47 if loaded() {
48 return;
49 }
50 loaded.set(true);
51 if let Some(info) = comment_storage::load_author() {
52 author_name.set(info.name);
53 author_email.set(info.email);
54 author_url.set(info.url);
55 }
56 });
57
58 if let Some(pid) = parent_id {
60 if active_reply() != Some(pid) {
61 return rsx! {};
62 }
63 }
64
65 let is_reply = parent_id.is_some();
66
67 let id_suffix = match parent_id {
69 Some(pid) => pid.to_string(),
70 None => "root".to_string(),
71 };
72
73 let negative_margin = match (is_reply, parent_indent) {
75 (true, Some(px)) if px > 0 => format!("margin-left: -{px}px;"),
76 _ => String::new(),
77 };
78
79 rsx! {
80 div {
81 class: if is_reply { "mt-3 pt-3 border-t border-gray-100 dark:border-gray-700" } else { "" },
82 style: "{negative_margin}",
83 role: "form",
84 aria_label: if is_reply { "回复评论" } else { "发表评论" },
85
86 if let Some((msg, variant)) = message() {
87 div { aria_live: "polite",
88 AlertBox { message: msg, variant }
89 }
90 }
91
92 div { class: "space-y-3",
93 div { class: "grid grid-cols-1 sm:grid-cols-2 gap-3",
94 div {
95 label {
96 r#for: "comment-name-{id_suffix}",
97 class: "block text-sm font-medium text-paper-secondary mb-1",
98 "昵称 *"
99 }
100 input {
101 id: "comment-name-{id_suffix}",
102 class: INPUT_CLASS,
103 r#type: "text",
104 placeholder: "你的昵称",
105 value: "{author_name}",
106 disabled: submitting(),
107 oninput: move |e| author_name.set(e.value()),
108 }
109 }
110 div {
111 label {
112 r#for: "comment-email-{id_suffix}",
113 class: "block text-sm font-medium text-paper-secondary mb-1",
114 "邮箱 *"
115 }
116 input {
117 id: "comment-email-{id_suffix}",
118 class: INPUT_CLASS,
119 r#type: "email",
120 placeholder: "your@email.com",
121 value: "{author_email}",
122 disabled: submitting(),
123 oninput: move |e| author_email.set(e.value()),
124 }
125 }
126 }
127 div {
128 label {
129 r#for: "comment-url-{id_suffix}",
130 class: "block text-sm font-medium text-paper-secondary mb-1",
131 "网站"
132 }
133 input {
134 id: "comment-url-{id_suffix}",
135 class: INPUT_CLASS,
136 r#type: "url",
137 placeholder: "https://example.com(可选)",
138 value: "{author_url}",
139 disabled: submitting(),
140 oninput: move |e| author_url.set(e.value()),
141 }
142 }
143
144 div {
145 label {
146 r#for: "comment-content-{id_suffix}",
147 class: "block text-sm font-medium text-paper-secondary mb-1",
148 "内容 *"
149 }
150 div { class: "relative bg-paper-entry rounded-lg",
151 textarea {
152 id: "comment-content-{id_suffix}",
153 class: "{INPUT_CLASS} !bg-transparent relative z-10 peer block min-h-[100px] resize-y",
154 value: "{content_md}",
155 disabled: submitting(),
156 oninput: move |e| content_md.set(e.value()),
157 }
158 img {
159 src: "/images/xiantiaoxiaogou_input_bg.webp",
160 alt: "",
161 class: "absolute bottom-1.5 right-1.5 w-24 opacity-10 pointer-events-none z-0",
162 }
163 }
164 }
165
166 p { class: "text-xs text-paper-tertiary", "支持 Markdown 语法" }
167
168 textarea {
170 class: "hidden",
171 aria_hidden: "true",
172 tabindex: "-1",
173 value: "{honeypot}",
174 oninput: move |e| honeypot.set(e.value()),
175 }
176
177 div { class: "flex justify-end",
178 button {
179 class: COMMENT_SUBMIT_CLASS,
180 disabled: submitting(),
181 onclick: move |_| {
182 if submitting() {
183 return;
184 }
185
186 let post_id = post_id;
187 let parent_id = parent_id;
188 let name = author_name();
189 let email = author_email();
190 let url_val = author_url();
191 let content = content_md();
192 let hp = honeypot();
193
194 if !hp.is_empty() {
196 return;
197 }
198
199 if name.trim().is_empty() || email.trim().is_empty() || content.trim().is_empty()
200 {
201 message.set(Some(("请填写所有必填项".to_string(), "error")));
202 return;
203 }
204 submitting.set(true);
205 message.set(None);
206 spawn(async move {
207 let result = create_comment(
208 post_id,
209 parent_id,
210 name.clone(),
211 email.clone(),
212 if url_val.trim().is_empty() { None } else { Some(url_val.clone()) },
213 content.clone(),
214 hp.clone(),
215 )
216 .await;
217 submitting.set(false);
218 match result {
219 Ok(resp) => {
220 if resp.success {
221 comment_storage::save_author(&name, &email, &url_val);
222 if let Some(comment_id) = resp.comment_id {
223 let avatar_url = resp.avatar_url.unwrap_or_default();
224 let depth = resp.depth.unwrap_or(0);
225 let now = chrono::Utc::now().to_rfc3339();
226 let pending = PendingComment {
227 id: comment_id,
228 parent_id,
229 depth,
230 author_name: name.clone(),
231 author_url: if url_val.trim().is_empty() {
232 None
233 } else {
234 Some(url_val)
235 },
236 avatar_url,
237 content_md: content,
238 created_at: now.clone(),
239 stored_at: now,
240 };
241 comment_storage::save_pending_comment(
242 post_id,
243 pending.clone(),
244 );
245 pending_comments.write().push(pending);
246 }
247 content_md.set(String::new());
248 message.set(Some((resp.message, "success")));
249 if parent_id.is_some() {
250 active_reply.set(None);
251 }
252 refresh_trigger.set(!refresh_trigger());
253 } else {
254 message.set(Some((resp.message, "error")));
255 }
256 }
257 Err(_) => {
258 message
259 .set(
260 Some(("提交失败,请稍后重试".to_string(), "error")),
261 );
262 }
263 }
264 });
265 },
266
267 if submitting() {
268 "提交中…"
269 } else if is_reply {
270 "回复"
271 } else {
272 "发表评论"
273 }
274 }
275 }
276 }
277 }
278 }
279}