refactor: reorganize admin write page layout with card-based sections
This commit is contained in:
parent
5b481e801c
commit
5321b8cf97
@ -14,5 +14,5 @@ style = ["/style.css", "/highlight.css", "/tiptap/editor.css"]
|
|||||||
script = ["/tiptap/editor.js"]
|
script = ["/tiptap/editor.js"]
|
||||||
|
|
||||||
[web.resource.dev]
|
[web.resource.dev]
|
||||||
style = []
|
style = ["/tiptap/editor.css"]
|
||||||
script = []
|
script = ["/tiptap/editor.js"]
|
||||||
|
|||||||
@ -4,20 +4,26 @@ use crate::components::skeletons::atoms::*;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn WriteSkeleton() -> Element {
|
pub fn WriteSkeleton() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "space-y-4",
|
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: "w-full h-[52px] mb-4 rounded" }
|
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-[500px] rounded-lg border border-gray-200 dark:border-[#333] bg-white dark:bg-[#1e1e1e] p-6 space-y-4",
|
||||||
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: "flex gap-2 pb-4 border-b border-gray-100 dark:border-[#333]",
|
div { class: "flex gap-2 pb-4 border-b border-gray-100 dark:border-[#333]",
|
||||||
for _ in 0..8 {
|
for _ in 0..8 {
|
||||||
SkeletonBox { class: "w-8 h-8 rounded" }
|
SkeletonBox { class: "w-8 h-8 rounded" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 内容行骨架
|
|
||||||
div { class: "space-y-3 pt-2",
|
div { class: "space-y-3 pt-2",
|
||||||
SkeletonBox { class: "h-4 w-[90%] rounded" }
|
SkeletonBox { class: "h-4 w-[90%] rounded" }
|
||||||
SkeletonBox { class: "h-4 w-full rounded" }
|
SkeletonBox { class: "h-4 w-full rounded" }
|
||||||
@ -34,8 +40,11 @@ pub fn WriteSkeleton() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存按钮骨架
|
div { class: "flex justify-end gap-3 pt-2",
|
||||||
SkeletonBox { class: "mt-4 h-10 w-28 rounded-full" }
|
SkeletonBox { class: "h-10 w-20 rounded-full" }
|
||||||
|
SkeletonBox { class: "h-10 w-24 rounded-full" }
|
||||||
|
SkeletonBox { class: "h-10 w-20 rounded-full" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ pub fn Write() -> Element {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof window.TiptapEditor !== 'undefined' && window.TiptapEditor) {
|
if (typeof window.TiptapEditor !== 'undefined' && window.TiptapEditor) {
|
||||||
|
try {
|
||||||
window.TiptapEditor.create('tiptap-editor', {
|
window.TiptapEditor.create('tiptap-editor', {
|
||||||
content: '',
|
content: '',
|
||||||
placeholder: '在此输入内容...',
|
placeholder: '在此输入内容...',
|
||||||
@ -45,6 +46,9 @@ pub fn Write() -> Element {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.__tiptap_ready = true;
|
window.__tiptap_ready = true;
|
||||||
|
} catch(e) {
|
||||||
|
console.error('[tiptap] create error: ' + e.message);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(initEditor, 50);
|
setTimeout(initEditor, 50);
|
||||||
@ -59,7 +63,7 @@ pub fn Write() -> Element {
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
wasm_bindgen_futures::spawn_local(async move {
|
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_val) = js_sys::eval("new Promise(r => setTimeout(r, 100))") {
|
||||||
if let Ok(promise) = promise_val.dyn_into::<js_sys::Promise>() {
|
if let Ok(promise) = promise_val.dyn_into::<js_sys::Promise>() {
|
||||||
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
@ -168,105 +172,105 @@ pub fn Write() -> Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "space-y-4 relative",
|
div { class: "relative",
|
||||||
// 骨架屏覆盖层:编辑器初始化期间显示
|
|
||||||
if loading() {
|
if loading() {
|
||||||
div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]",
|
div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]",
|
||||||
WriteSkeleton {}
|
WriteSkeleton {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标题
|
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 {
|
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",
|
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: "文章标题...",
|
placeholder: "文章标题",
|
||||||
value: "{title}",
|
value: "{title}",
|
||||||
oninput: move |evt| title.set(evt.value()),
|
oninput: move |evt| title.set(evt.value()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 摘要
|
|
||||||
textarea {
|
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",
|
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: "文章摘要(留空自动生成)",
|
placeholder: "摘要(留空则自动生成)",
|
||||||
rows: "2",
|
rows: "2",
|
||||||
value: "{summary}",
|
value: "{summary}",
|
||||||
oninput: move |evt| summary.set(evt.value()),
|
oninput: move |evt| summary.set(evt.value()),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slug + Tags + Status 行
|
div { class: "grid grid-cols-1 md:grid-cols-3 gap-3",
|
||||||
div { class: "flex flex-col md:flex-row gap-3 mb-2",
|
div {
|
||||||
|
label { class: "block text-xs text-gray-400 dark:text-[#666] mb-1.5 font-medium", "Slug" }
|
||||||
input {
|
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",
|
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 标识(留空自动生成)",
|
placeholder: "自动生成",
|
||||||
value: "{slug}",
|
value: "{slug}",
|
||||||
oninput: move |evt| slug.set(evt.value()),
|
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 {
|
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",
|
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: "标签,用逗号分隔",
|
placeholder: "逗号分隔",
|
||||||
value: "{tags}",
|
value: "{tags}",
|
||||||
oninput: move |evt| tags.set(evt.value()),
|
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", "发布" }
|
|
||||||
}
|
}
|
||||||
}
|
div {
|
||||||
|
label { class: "block text-xs text-gray-400 dark:text-[#666] mb-1.5 font-medium", "封面图" }
|
||||||
// 封面图 URL
|
|
||||||
input {
|
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",
|
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(可选)",
|
placeholder: "URL(可选)",
|
||||||
value: "{cover_image}",
|
value: "{cover_image}",
|
||||||
oninput: move |evt| cover_image.set(evt.value()),
|
oninput: move |evt| cover_image.set(evt.value()),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tiptap 编辑器
|
|
||||||
div {
|
div {
|
||||||
class: "w-full h-[500px] 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",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误提示
|
|
||||||
if let Some(err) = error() {
|
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",
|
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}"
|
"{err}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功提示
|
|
||||||
if success() {
|
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: "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 gap-3 mt-4",
|
div { class: "flex-1" }
|
||||||
button {
|
button {
|
||||||
class: if saving() {
|
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",
|
||||||
"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 |_| {
|
onclick: move |_| {
|
||||||
let _ = dioxus::router::navigator().push(Route::Admin {});
|
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 { "保存" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user