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
This commit is contained in:
parent
973d6f3d57
commit
df339cb084
@ -3,14 +3,22 @@ use dioxus::prelude::*;
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
|
use crate::api::posts::{create_post, CreatePostResponse};
|
||||||
use crate::components::write_skeleton::WriteSkeleton;
|
use crate::components::write_skeleton::WriteSkeleton;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
#[allow(unused_mut, unused_variables)]
|
#[allow(unused_mut, unused_variables)]
|
||||||
pub fn Write() -> Element {
|
pub fn Write() -> Element {
|
||||||
let mut title = use_signal(|| "".to_string());
|
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 content = use_signal(|| "".to_string());
|
||||||
let mut loading = use_signal(|| true);
|
let mut loading = use_signal(|| true);
|
||||||
|
let mut saving = use_signal(|| false);
|
||||||
|
let mut error = use_signal(|| None::<String>);
|
||||||
|
let mut success = use_signal(|| false);
|
||||||
|
|
||||||
// 初始化 Tiptap 编辑器
|
// 初始化 Tiptap 编辑器
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
@ -19,7 +27,6 @@ pub fn Write() -> Element {
|
|||||||
let _ = js_sys::eval(
|
let _ = js_sys::eval(
|
||||||
r#"
|
r#"
|
||||||
(function initEditor() {
|
(function initEditor() {
|
||||||
// 如果已经初始化过,直接标记为就绪
|
|
||||||
if (window.__tiptap_ready) return;
|
if (window.__tiptap_ready) return;
|
||||||
|
|
||||||
var container = document.getElementById('tiptap-editor');
|
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<String> = 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! {
|
rsx! {
|
||||||
div { class: "space-y-4 relative",
|
div { class: "space-y-4 relative",
|
||||||
// 骨架屏覆盖层:编辑器初始化期间显示
|
// 骨架屏覆盖层:编辑器初始化期间显示
|
||||||
@ -80,35 +153,89 @@ pub fn Write() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 真实内容始终渲染,确保 #tiptap-editor 在 DOM 中
|
// 标题
|
||||||
// 初始化期间被骨架屏遮住,就绪后骨架屏消失
|
|
||||||
input {
|
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: "文章标题...",
|
placeholder: "文章标题...",
|
||||||
value: "{title}",
|
value: "{title}",
|
||||||
oninput: move |evt| title.set(evt.value()),
|
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 {
|
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",
|
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",
|
if let Some(err) = error() {
|
||||||
onclick: move |_| {
|
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",
|
||||||
#[cfg(target_arch = "wasm32")]
|
"{err}"
|
||||||
{
|
}
|
||||||
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 || '');
|
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",
|
||||||
"#).ok().and_then(|v| v.as_string()).unwrap_or_default();
|
"保存成功!"
|
||||||
content.set(md.clone());
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存按钮
|
||||||
|
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");
|
||||||
|
},
|
||||||
|
"取消"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user