From df339cb0845adc0715f8e9c13952eff984ce2608 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 1 Jun 2026 17:18:10 +0800 Subject: [PATCH] feat: add post creation form to write.rs - Add summary, slug, tags, status fields to editor - Integrate create_post API with validation - Add loading and error states - Keep Tiptap editor initialization intact --- src/pages/admin/write.rs | 165 ++++++++++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 19 deletions(-) diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index 75758c2..bf4684f 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -3,14 +3,22 @@ use dioxus::prelude::*; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; +use crate::api::posts::{create_post, CreatePostResponse}; use crate::components::write_skeleton::WriteSkeleton; #[component] #[allow(unused_mut, unused_variables)] pub fn Write() -> Element { let mut title = use_signal(|| "".to_string()); + let mut summary = use_signal(|| "".to_string()); + let mut slug = use_signal(|| "".to_string()); + let mut tags = use_signal(|| "".to_string()); + let mut status = use_signal(|| "draft".to_string()); let mut content = use_signal(|| "".to_string()); let mut loading = use_signal(|| true); + let mut saving = use_signal(|| false); + let mut error = use_signal(|| None::); + let mut success = use_signal(|| false); // 初始化 Tiptap 编辑器 use_effect(move || { @@ -19,7 +27,6 @@ pub fn Write() -> Element { let _ = js_sys::eval( r#" (function initEditor() { - // 如果已经初始化过,直接标记为就绪 if (window.__tiptap_ready) return; var container = document.getElementById('tiptap-editor'); @@ -71,6 +78,72 @@ pub fn Write() -> Element { } }); + let on_submit = move |_| { + if title().trim().is_empty() { + error.set(Some("标题不能为空".to_string())); + return; + } + + #[cfg(target_arch = "wasm32")] + { + let md = js_sys::eval(r#" + (function() { + var editor = window.TiptapEditor && window.TiptapEditor._instances && window.TiptapEditor._instances.get('tiptap-editor'); + return editor ? editor.getMarkdown() : (window.__tiptap_content || ''); + })() + "#).ok().and_then(|v| v.as_string()).unwrap_or_default(); + + if md.trim().is_empty() { + error.set(Some("内容不能为空".to_string())); + return; + } + + let tags_list: Vec = tags() + .split(',') + .map(|t| t.trim().to_string()) + .filter(|t| !t.is_empty()) + .collect(); + + let slug_opt = if slug().trim().is_empty() { + None + } else { + Some(slug().trim().to_string()) + }; + + let summary_opt = if summary().trim().is_empty() { + None + } else { + Some(summary().trim().to_string()) + }; + + saving.set(true); + error.set(None); + + spawn(async move { + match create_post(title().trim().to_string(), slug_opt, summary_opt, md, status(), tags_list).await { + Ok(CreatePostResponse { success: true, .. }) => { + saving.set(false); + success.set(true); + // Delay navigation slightly so user sees success message + #[cfg(target_arch = "wasm32")] + { + let _ = js_sys::eval("new Promise(r => setTimeout(r, 800))"); + } + let _ = dioxus::router::navigator().push("/admin"); + } + Ok(CreatePostResponse { success: false, message, .. }) => { + saving.set(false); + error.set(Some(message)); + } + Err(e) => { + saving.set(false); + error.set(Some(format!("保存失败: {}", e))); + } + } + }); + } + }; + rsx! { div { class: "space-y-4 relative", // 骨架屏覆盖层:编辑器初始化期间显示 @@ -80,35 +153,89 @@ pub fn Write() -> Element { } } - // 真实内容始终渲染,确保 #tiptap-editor 在 DOM 中 - // 初始化期间被骨架屏遮住,就绪后骨架屏消失 + // 标题 input { - class: "w-full text-2xl font-bold bg-transparent border-b border-gray-200 dark:border-[#333] py-2 mb-4 text-gray-900 dark:text-[#dadadb] placeholder-gray-400 dark:placeholder-[#9b9c9d] focus:outline-none", + class: "w-full text-2xl font-bold bg-transparent border-b border-gray-200 dark:border-[#333] py-2 mb-2 text-gray-900 dark:text-[#dadadb] placeholder-gray-400 dark:placeholder-[#9b9c9d] focus:outline-none", placeholder: "文章标题...", value: "{title}", oninput: move |evt| title.set(evt.value()), } + // 摘要 + textarea { + class: "w-full text-sm bg-transparent border-b border-gray-200 dark:border-[#333] py-2 mb-2 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#9b9c9d] focus:outline-none resize-none", + placeholder: "文章摘要(留空自动生成)", + rows: "2", + value: "{summary}", + oninput: move |evt| summary.set(evt.value()), + } + + // Slug + Tags + Status 行 + div { class: "flex flex-col md:flex-row gap-3 mb-2", + input { + class: "flex-1 text-sm bg-transparent border-b border-gray-200 dark:border-[#333] py-2 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#9b9c9d] focus:outline-none", + placeholder: "URL 标识(留空自动生成)", + value: "{slug}", + oninput: move |evt| slug.set(evt.value()), + } + input { + class: "flex-1 text-sm bg-transparent border-b border-gray-200 dark:border-[#333] py-2 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#9b9c9d] focus:outline-none", + placeholder: "标签,用逗号分隔", + value: "{tags}", + oninput: move |evt| tags.set(evt.value()), + } + select { + class: "text-sm bg-transparent border-b border-gray-200 dark:border-[#333] py-2 text-gray-700 dark:text-[#9b9c9d] focus:outline-none cursor-pointer", + value: "{status}", + onchange: move |evt| status.set(evt.value()), + option { value: "draft", "草稿" } + option { value: "published", "发布" } + } + } + + // Tiptap 编辑器 div { - class: "w-full h-[600px] border border-gray-200 dark:border-[#333] rounded-lg overflow-hidden bg-white dark:bg-[#1e1e1e]", + class: "w-full h-[500px] border border-gray-200 dark:border-[#333] rounded-lg overflow-hidden bg-white dark:bg-[#1e1e1e]", id: "tiptap-editor", } - button { - class: "mt-4 px-6 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full font-medium hover:opacity-80 transition-opacity", - onclick: move |_| { - #[cfg(target_arch = "wasm32")] - { - let md = js_sys::eval(r#" - (function() { - var editor = window.TiptapEditor && window.TiptapEditor._instances && window.TiptapEditor._instances.get('tiptap-editor'); - return editor ? editor.getMarkdown() : (window.__tiptap_content || ''); - })() - "#).ok().and_then(|v| v.as_string()).unwrap_or_default(); - content.set(md.clone()); + // 错误提示 + if let Some(err) = error() { + div { class: "mt-4 p-3 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-lg text-sm", + "{err}" + } + } + + // 成功提示 + if success() { + div { class: "mt-4 p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg text-sm", + "保存成功!" + } + } + + // 保存按钮 + div { class: "flex gap-3 mt-4", + button { + class: if saving() { + "px-6 py-2 bg-gray-400 text-white rounded-full font-medium cursor-not-allowed" + } else { + "px-6 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full font-medium hover:opacity-80 transition-opacity cursor-pointer" + }, + disabled: saving(), + onclick: on_submit, + if saving() { + "保存中..." + } else { + "保存" } - }, - "保存草稿" + } + button { + class: "px-6 py-2 bg-gray-200 dark:bg-[#333] text-gray-700 dark:text-[#dadadb] rounded-full font-medium hover:opacity-80 transition-opacity cursor-pointer", + onclick: move |_| { + let _ = dioxus::router::navigator().push("/admin"); + }, + "取消" + } } } }