Compare commits

..

No commits in common. "373d4f09ed7290fcf228f62e5c013e505395a558" and "b1a5fdcb6e5d0bf25f42aca73b6a5ad2b6cb88be" have entirely different histories.

5 changed files with 142 additions and 195 deletions

View File

@ -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.css", "/highlight.css", "/tiptap/editor.css"] style = ["/tiptap/editor.css"]
script = ["/tiptap/editor.js"] script = ["/tiptap/editor.js"]

1
public/tiptap/editor.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -73,26 +73,12 @@ pub fn AdminLayout() -> Element {
} }
}; };
let is_write_route = matches!(route, Route::Write {}) || matches!(route, Route::WriteEdit { .. });
let main_class = if is_write_route {
"flex-1 w-full max-w-5xl mx-auto px-6 flex flex-col overflow-hidden"
} else {
"flex-1 w-full max-w-5xl mx-auto px-6 py-8"
};
// Write 路由:页面固定高度,不滚动,由编辑器内部处理滚动
let root_class = if is_write_route {
"h-dvh flex flex-col overflow-hidden bg-white dark:bg-[#1d1e20]"
} else {
"min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]"
};
match ((ctx.checked)(), (ctx.user)()) { match ((ctx.checked)(), (ctx.user)()) {
(true, Some(_)) => { (true, Some(_)) => {
rsx! { rsx! {
div { class: "{root_class}", div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]",
Header { nav_items: admin_nav_items, right_content: right_content } Header { nav_items: admin_nav_items, right_content: right_content }
main { class: "{main_class}", main { class: "flex-1 w-full max-w-5xl mx-auto px-6 py-8",
Outlet::<Route> {} Outlet::<Route> {}
} }
Footer {} Footer {}
@ -101,19 +87,17 @@ pub fn AdminLayout() -> Element {
} }
(true, None) => { (true, None) => {
rsx! { rsx! {
div { class: "{root_class}", div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]",
div { class: "flex-1 flex items-center justify-center", p { class: "text-gray-600 dark:text-[#9b9c9d]", "未登录,正在跳转..." }
p { class: "text-gray-600 dark:text-[#9b9c9d]", "未登录,正在跳转..." }
}
} }
} }
} }
(false, _) => { (false, _) => {
// 使用与真实布局完全相同的结构包裹内容骨架,避免 checked 变化时的布局闪烁 // 使用与真实布局完全相同的结构包裹内容骨架,避免 checked 变化时的布局闪烁
rsx! { rsx! {
div { class: "{root_class}", div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]",
Header { nav_items: admin_nav_items, right_content: right_content } Header { nav_items: admin_nav_items, right_content: right_content }
main { class: "{main_class}", main { class: "flex-1 w-full max-w-5xl mx-auto px-6 py-8",
div { class: if show_skeleton() { "" } else { "opacity-0" }, div { class: if show_skeleton() { "" } else { "opacity-0" },
{match route { {match route {
Route::Write {} => rsx! { WriteSkeleton {} }, Route::Write {} => rsx! { WriteSkeleton {} },

View File

@ -4,61 +4,47 @@ use crate::components::skeletons::atoms::*;
#[component] #[component]
pub fn WriteSkeleton() -> Element { pub fn WriteSkeleton() -> Element {
rsx! { rsx! {
div { class: "relative flex flex-col flex-1 min-h-0 overflow-hidden", 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",
div { class: "flex-shrink-0 space-y-5 pt-8", SkeletonBox { class: "h-9 w-2/3 rounded-lg" }
// 标题骨架 SkeletonBox { class: "h-16 w-full rounded-lg" }
SkeletonBox { class: "h-12 w-2/3 rounded-lg" } div { class: "grid grid-cols-1 md:grid-cols-3 gap-3",
// 摘要骨架
SkeletonBox { class: "h-14 w-full rounded-lg" }
// 元数据行骨架
div { class: "flex flex-wrap items-end gap-x-8 gap-y-4",
for _ in 0..3 { for _ in 0..3 {
div { class: "flex-1 min-w-[140px] space-y-2", div { class: "space-y-2",
SkeletonBox { class: "h-3 w-12 rounded" } SkeletonBox { class: "h-3 w-10 rounded" }
SkeletonBox { class: "h-8 w-full rounded-lg" } SkeletonBox { class: "h-10 w-full rounded-lg" }
} }
} }
} }
// 分隔线
div { class: "h-px bg-[var(--color-paper-tertiary)]" }
}
// 编辑器骨架 - 沾满剩余高度
div { class: "flex-1 min-h-0 flex flex-col my-4",
div { class: "flex-1 min-h-0 w-full rounded-xl border border-[var(--color-paper-border)] bg-[var(--color-paper-entry)] space-y-4 p-4",
// 编辑器工具栏骨架
div { class: "flex gap-2 pb-3 border-b border-[var(--color-paper-border)]",
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" }
SkeletonBox { class: "h-4 w-[85%] rounded" }
SkeletonBox { class: "h-4 w-[95%] rounded" }
SkeletonBox { class: "h-4 w-[60%] rounded" }
SkeletonBox { class: "h-4 w-full rounded" }
SkeletonBox { class: "h-4 w-[75%] rounded" }
SkeletonBox { class: "h-4 w-[80%] rounded" }
div { class: "h-4" }
SkeletonBox { class: "h-4 w-[70%] rounded" }
SkeletonBox { class: "h-4 w-full rounded" }
SkeletonBox { class: "h-4 w-[90%] rounded" }
}
}
} }
// 按钮行骨架 div { class: "w-full h-[500px] rounded-lg border border-gray-200 dark:border-[#333] bg-white dark:bg-[#1e1e1e] space-y-4",
div { class: "flex-shrink-0 flex items-center gap-2 pt-2 pb-4", div { class: "flex gap-2 p-4 border-b border-gray-100 dark:border-[#333]",
SkeletonBox { class: "h-9 w-[60px] rounded-full" } for _ in 0..8 {
SkeletonBox { class: "h-9 w-[80px] rounded-full" } SkeletonBox { class: "w-8 h-8 rounded" }
SkeletonBox { class: "h-9 w-[60px] rounded-full" } }
}
div { class: "space-y-3 pt-2",
SkeletonBox { class: "h-4 w-[90%] rounded" }
SkeletonBox { class: "h-4 w-full rounded" }
SkeletonBox { class: "h-4 w-[85%] rounded" }
SkeletonBox { class: "h-4 w-[95%] rounded" }
SkeletonBox { class: "h-4 w-[60%] rounded" }
SkeletonBox { class: "h-4 w-full rounded" }
SkeletonBox { class: "h-4 w-[75%] rounded" }
SkeletonBox { class: "h-4 w-[80%] rounded" }
div { class: "h-4" }
SkeletonBox { class: "h-4 w-[70%] rounded" }
SkeletonBox { class: "h-4 w-full rounded" }
SkeletonBox { class: "h-4 w-[90%] rounded" }
}
}
div { class: "flex items-center gap-3 pt-2",
div { class: "flex-1" }
SkeletonBox { class: "h-10 w-[72px] rounded-full" }
SkeletonBox { class: "h-10 w-[100px] rounded-full" }
SkeletonBox { class: "h-10 w-[72px] rounded-full" }
} }
} }
} }

View File

@ -3,9 +3,9 @@ use dioxus::prelude::*;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use crate::api::posts::{get_post_by_id, SinglePostResponse};
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
use crate::api::posts::{create_post, update_post, CreatePostResponse}; use crate::api::posts::{create_post, update_post, CreatePostResponse};
use crate::api::posts::{get_post_by_id, SinglePostResponse};
use crate::components::write_skeleton::WriteSkeleton; use crate::components::write_skeleton::WriteSkeleton;
use crate::router::Route; use crate::router::Route;
@ -276,11 +276,7 @@ fn write_editor(post_id: Option<i32>) -> Element {
success.set(true); success.set(true);
let _ = dioxus::router::navigator().push(Route::Posts {}); let _ = dioxus::router::navigator().push(Route::Posts {});
} }
Ok(CreatePostResponse { Ok(CreatePostResponse { success: false, message, .. }) => {
success: false,
message,
..
}) => {
saving.set(false); saving.set(false);
error.set(Some(message)); error.set(Some(message));
} }
@ -309,11 +305,7 @@ fn write_editor(post_id: Option<i32>) -> Element {
success.set(true); success.set(true);
let _ = dioxus::router::navigator().push(Route::Admin {}); let _ = dioxus::router::navigator().push(Route::Admin {});
} }
Ok(CreatePostResponse { Ok(CreatePostResponse { success: false, message, .. }) => {
success: false,
message,
..
}) => {
saving.set(false); saving.set(false);
error.set(Some(message)); error.set(Some(message));
} }
@ -336,144 +328,128 @@ fn write_editor(post_id: Option<i32>) -> Element {
}; };
rsx! { rsx! {
div { class: "relative flex flex-col flex-1 min-h-0 overflow-hidden", div { class: "relative",
if loading() { if loading() {
div { class: "absolute inset-0 z-10 bg-[var(--color-paper-theme)]", div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]",
WriteSkeleton {} WriteSkeleton {}
} }
} }
// 顶部元信息区域 - 固定高度,不滚动 div { class: "space-y-6",
div { class: "flex-shrink-0 space-y-5 pt-8", div { class: "rounded-xl bg-white dark:bg-[#2e2e33] border border-gray-200 dark:border-[#333] p-6 space-y-5",
// 标题区域 - 大字号无框输入
div {
input { input {
class: "w-full text-3xl md:text-4xl font-bold bg-transparent text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none tracking-tight leading-tight", 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-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]",
class: "w-full text-base bg-transparent text-[var(--color-paper-secondary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none resize-none leading-relaxed", placeholder: "摘要(留空则自动生成)",
placeholder: "摘要(留空则自动生成)", rows: "2",
rows: "2", value: "{summary}",
value: "{summary}", oninput: move |evt| summary.set(evt.value()),
oninput: move |evt| summary.set(evt.value()), }
}
// 元数据行 - 紧凑精致 div { class: "grid grid-cols-1 md:grid-cols-3 gap-3",
div { class: "flex flex-wrap items-end gap-x-8 gap-y-4 text-sm", div {
div { class: "flex-1 min-w-[140px]", label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] mb-1.5 font-medium", "Slug" }
label { class: "block text-[11px] font-medium text-[var(--color-paper-secondary)] tracking-wider mb-2", input {
"Slug" 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()),
}
} }
input { div {
class: "w-full text-sm bg-transparent text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border-b border-[var(--color-paper-tertiary)] focus:border-[var(--color-paper-primary)] transition-colors pb-1.5", label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] mb-1.5 font-medium", "标签" }
placeholder: "自动生成", input {
value: "{slug}", 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]",
oninput: move |evt| slug.set(evt.value()), placeholder: "逗号分隔",
value: "{tags}",
oninput: move |evt| tags.set(evt.value()),
}
} }
} div {
div { class: "flex-1 min-w-[140px]", label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] mb-1.5 font-medium", "封面图" }
label { class: "block text-[11px] font-medium text-[var(--color-paper-secondary)] tracking-wider mb-2", 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可选",
input { value: "{cover_image}",
class: "w-full text-sm bg-transparent text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border-b border-[var(--color-paper-tertiary)] focus:border-[var(--color-paper-primary)] transition-colors pb-1.5", oninput: move |evt| cover_image.set(evt.value()),
placeholder: "逗号分隔", }
value: "{tags}",
oninput: move |evt| tags.set(evt.value()),
}
}
div { class: "flex-1 min-w-[140px]",
label { class: "block text-[11px] font-medium text-[var(--color-paper-secondary)] tracking-wider mb-2",
"封面图"
}
input {
class: "w-full text-sm bg-transparent text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border-b border-[var(--color-paper-tertiary)] focus:border-[var(--color-paper-primary)] transition-colors pb-1.5",
placeholder: "URL可选",
value: "{cover_image}",
oninput: move |evt| cover_image.set(evt.value()),
} }
} }
} }
}
// 编辑器区域 - 沾满剩余高度
div { class: "flex-1 min-h-0 flex flex-col my-4",
div { div {
class: "flex-1 min-h-0 w-full border border-[var(--color-paper-border)] rounded-xl overflow-hidden bg-[var(--color-paper-entry)] shadow-[0_2px_8px_rgba(0,0,0,0.04)] dark:shadow-none", 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) = load_error() {
if let Some(err) = load_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",
div { class: "flex-shrink-0 px-4 py-2 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 mb-2", "{err}"
"{err}"
}
}
if let Some(err) = error() {
div { class: "flex-shrink-0 px-4 py-2 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 mb-2",
"{err}"
}
}
if success() {
div { class: "flex-shrink-0 px-4 py-2 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 mb-2",
"保存成功"
}
}
// 底部操作栏 - 在编辑器下方,左对齐
div { class: "flex-shrink-0 flex items-center gap-2 pt-2 pb-4",
button {
class: "px-4 py-1.5 text-sm text-[var(--color-paper-secondary)] hover:text-[var(--color-paper-primary)] transition-colors cursor-pointer",
onclick: move |_| {
let _ = dioxus::router::navigator().push(Route::Posts {});
},
"取消"
}
div { class: "w-px h-5 bg-[var(--color-paper-border)]" }
div {
class: "relative inline-flex items-center px-3 py-1.5 text-sm text-[var(--color-paper-secondary)] cursor-pointer",
select {
class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
style: "appearance: none; -webkit-appearance: none;",
value: "{status}",
onchange: move |evt| status.set(evt.value()),
option { value: "draft", "草稿" }
option { value: "published", "发布" }
} }
span { class: "pr-1.5 text-[var(--color-paper-primary)] font-medium", }
if status() == "draft" { "草稿" } else { "发布" }
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}"
} }
svg { }
class: "h-3.5 w-3.5 text-[var(--color-paper-tertiary)] pointer-events-none",
xmlns: "http://www.w3.org/2000/svg", if success() {
view_box: "0 0 20 20", 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",
fill: "currentColor", "保存成功"
path { }
fill_rule: "evenodd", }
d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z",
clip_rule: "evenodd" 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-700 dark:text-[#dadadb] rounded-full font-medium hover:opacity-80 transition-opacity cursor-pointer",
onclick: move |_| {
let _ = dioxus::router::navigator().push(Route::Posts {});
},
"取消"
}
div {
class: "relative inline-flex items-center px-5 py-2.5 text-sm bg-gray-50 dark:bg-[#1d1e20] border border-gray-200 dark:border-[#333] rounded-full text-gray-700 dark:text-[#9b9c9d] cursor-pointer min-w-[80px]",
select {
class: "absolute inset-0 w-full h-full opacity-0 cursor-pointer",
style: "appearance: none; -webkit-appearance: none;",
value: "{status}",
onchange: move |evt| status.set(evt.value()),
option { value: "draft", "草稿" }
option { value: "published", "发布" }
}
span { class: "pr-2",
if status() == "draft" { "草稿" } else { "发布" }
}
svg {
class: "h-4 w-4 text-gray-500 dark:text-[#666] pointer-events-none",
xmlns: "http://www.w3.org/2000/svg",
view_box: "0 0 20 20",
fill: "currentColor",
path {
fill_rule: "evenodd",
d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z",
clip_rule: "evenodd"
}
} }
} }
} button {
div { class: "w-px h-5 bg-[var(--color-paper-border)]" } class: if saving() {
button { "px-6 py-2.5 text-sm bg-gray-400 text-white rounded-full font-medium cursor-not-allowed"
class: if saving() { } else {
"px-5 py-1.5 text-sm bg-[var(--color-paper-tertiary)] text-[var(--color-paper-secondary)] rounded-xl font-medium cursor-not-allowed" "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"
} else { },
"px-5 py-1.5 text-sm bg-[var(--color-paper-primary)] text-[var(--color-paper-theme)] rounded-xl font-medium hover:opacity-90 transition-opacity cursor-pointer" disabled: saving(),
}, onclick: on_submit,
disabled: saving(), "{save_button_text}"
onclick: on_submit, }
"{save_button_text}"
} }
} }
} }