From 5321b8cf9733cd3f23a1fdd5b8db589257c3a423 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 5 Jun 2026 09:58:26 +0800 Subject: [PATCH] refactor: reorganize admin write page layout with card-based sections --- Dioxus.toml | 4 +- src/components/write_skeleton.rs | 29 +++-- src/pages/admin/write.rs | 198 ++++++++++++++++--------------- 3 files changed, 122 insertions(+), 109 deletions(-) diff --git a/Dioxus.toml b/Dioxus.toml index 00d96b1..f5ce35b 100644 --- a/Dioxus.toml +++ b/Dioxus.toml @@ -14,5 +14,5 @@ style = ["/style.css", "/highlight.css", "/tiptap/editor.css"] script = ["/tiptap/editor.js"] [web.resource.dev] -style = [] -script = [] +style = ["/tiptap/editor.css"] +script = ["/tiptap/editor.js"] diff --git a/src/components/write_skeleton.rs b/src/components/write_skeleton.rs index 38de377..7b77edd 100644 --- a/src/components/write_skeleton.rs +++ b/src/components/write_skeleton.rs @@ -4,20 +4,26 @@ use crate::components::skeletons::atoms::*; #[component] pub fn WriteSkeleton() -> Element { rsx! { - div { class: "space-y-4", - // 标题输入骨架 - SkeletonBox { class: "w-full h-[52px] mb-4 rounded" } + div { class: "space-y-6 p-1", + div { class: "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 space-y-5", + SkeletonBox { class: "h-9 w-2/3 rounded-lg" } + SkeletonBox { class: "h-16 w-full rounded-lg" } + div { class: "grid grid-cols-1 md:grid-cols-3 gap-3", + for _ in 0..3 { + div { class: "space-y-2", + SkeletonBox { class: "h-3 w-10 rounded" } + SkeletonBox { class: "h-10 w-full rounded-lg" } + } + } + } + } - // 编辑器区域骨架 - div { - class: "w-full h-[600px] border border-gray-200 dark:border-[#333] rounded-lg bg-white dark:bg-[#1e1e1e] p-6 space-y-4", - // 工具栏骨架 + div { class: "w-full h-[500px] rounded-lg border border-gray-200 dark:border-[#333] bg-white dark:bg-[#1e1e1e] p-6 space-y-4", div { class: "flex gap-2 pb-4 border-b border-gray-100 dark:border-[#333]", for _ in 0..8 { SkeletonBox { class: "w-8 h-8 rounded" } } } - // 内容行骨架 div { class: "space-y-3 pt-2", SkeletonBox { class: "h-4 w-[90%] rounded" } SkeletonBox { class: "h-4 w-full rounded" } @@ -34,8 +40,11 @@ pub fn WriteSkeleton() -> Element { } } - // 保存按钮骨架 - SkeletonBox { class: "mt-4 h-10 w-28 rounded-full" } + div { class: "flex justify-end gap-3 pt-2", + SkeletonBox { class: "h-10 w-20 rounded-full" } + SkeletonBox { class: "h-10 w-24 rounded-full" } + SkeletonBox { class: "h-10 w-20 rounded-full" } + } } } } diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index c8f8103..5d2a401 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -37,14 +37,18 @@ pub fn Write() -> Element { return; } if (typeof window.TiptapEditor !== 'undefined' && window.TiptapEditor) { - window.TiptapEditor.create('tiptap-editor', { - content: '', - placeholder: '在此输入内容...', - onUpdate: function(markdown) { - window.__tiptap_content = markdown; - } - }); - window.__tiptap_ready = true; + try { + window.TiptapEditor.create('tiptap-editor', { + content: '', + placeholder: '在此输入内容...', + onUpdate: function(markdown) { + window.__tiptap_content = markdown; + } + }); + window.__tiptap_ready = true; + } catch(e) { + console.error('[tiptap] create error: ' + e.message); + } return; } setTimeout(initEditor, 50); @@ -59,7 +63,7 @@ pub fn Write() -> Element { #[cfg(target_arch = "wasm32")] { wasm_bindgen_futures::spawn_local(async move { - for _ in 0..100 { + for i in 0..100 { if let Ok(promise_val) = js_sys::eval("new Promise(r => setTimeout(r, 100))") { if let Ok(promise) = promise_val.dyn_into::() { let _ = wasm_bindgen_futures::JsFuture::from(promise).await; @@ -168,104 +172,104 @@ pub fn Write() -> Element { }; rsx! { - div { class: "space-y-4 relative", - // 骨架屏覆盖层:编辑器初始化期间显示 + div { class: "relative", if loading() { div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]", WriteSkeleton {} } } - // 标题 - input { - 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()), - } + div { class: "space-y-6", + div { class: "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 space-y-5", + input { + class: "w-full text-2xl font-bold bg-transparent text-gray-900 dark:text-[#dadadb] placeholder-gray-300 dark:placeholder-[#555] 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()), - } + textarea { + class: "w-full text-sm bg-gray-50 dark:bg-[#1d1e20] rounded-lg px-4 py-3 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#555] focus:outline-none resize-none border border-gray-100 dark:border-[#333]", + 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", "发布" } - } - } - - // 封面图 URL - input { - 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", - placeholder: "封面图 URL(可选)", - value: "{cover_image}", - oninput: move |evt| cover_image.set(evt.value()), - } - - // Tiptap 编辑器 - div { - class: "w-full h-[500px] border border-gray-200 dark:border-[#333] rounded-lg overflow-hidden bg-white dark:bg-[#1e1e1e]", - id: "tiptap-editor", - } - - // 错误提示 - 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 { - "保存" + div { class: "grid grid-cols-1 md:grid-cols-3 gap-3", + div { + label { class: "block text-xs text-gray-400 dark:text-[#666] mb-1.5 font-medium", "Slug" } + input { + class: "w-full text-sm bg-gray-50 dark:bg-[#1d1e20] rounded-lg px-3 py-2.5 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#555] focus:outline-none border border-gray-100 dark:border-[#333]", + placeholder: "自动生成", + value: "{slug}", + oninput: move |evt| slug.set(evt.value()), + } + } + div { + label { class: "block text-xs text-gray-400 dark:text-[#666] mb-1.5 font-medium", "标签" } + input { + class: "w-full text-sm bg-gray-50 dark:bg-[#1d1e20] rounded-lg px-3 py-2.5 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#555] focus:outline-none border border-gray-100 dark:border-[#333]", + placeholder: "逗号分隔", + value: "{tags}", + oninput: move |evt| tags.set(evt.value()), + } + } + div { + label { class: "block text-xs text-gray-400 dark:text-[#666] mb-1.5 font-medium", "封面图" } + input { + class: "w-full text-sm bg-gray-50 dark:bg-[#1d1e20] rounded-lg px-3 py-2.5 text-gray-700 dark:text-[#9b9c9d] placeholder-gray-400 dark:placeholder-[#555] focus:outline-none border border-gray-100 dark:border-[#333]", + placeholder: "URL(可选)", + value: "{cover_image}", + oninput: move |evt| cover_image.set(evt.value()), + } + } } } - 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(Route::Admin {}); - }, - "取消" + + div { + class: "w-full h-[500px] border border-gray-200 dark:border-[#333] rounded-lg overflow-hidden bg-white dark:bg-[#1e1e1e]", + id: "tiptap-editor", + } + + if let Some(err) = error() { + div { class: "px-4 py-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl text-sm border border-red-100 dark:border-red-900/30", + "{err}" + } + } + + if success() { + div { class: "px-4 py-3 bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 rounded-xl text-sm border border-green-100 dark:border-green-900/30", + "保存成功" + } + } + + div { class: "flex items-center gap-3 pt-2", + div { class: "flex-1" } + button { + class: "px-5 py-2.5 text-sm bg-gray-200 dark:bg-[#333] text-gray-600 dark:text-[#9b9c9d] rounded-full font-medium hover:opacity-80 transition-opacity cursor-pointer", + onclick: move |_| { + let _ = dioxus::router::navigator().push(Route::Admin {}); + }, + "取消" + } + select { + class: "text-sm bg-gray-50 dark:bg-[#1d1e20] border border-gray-200 dark:border-[#333] rounded-full px-4 py-2.5 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", "发布" } + } + button { + class: if saving() { + "px-6 py-2.5 text-sm bg-gray-400 text-white rounded-full font-medium cursor-not-allowed" + } else { + "px-6 py-2.5 text-sm 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 { "保存" } + } } } }