From d3c5653808be33a654520796858a53a9dda15571 Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 10 Jun 2026 17:29:41 +0800 Subject: [PATCH] feat: redesign admin write page with fixed-height editor layout - Redesign write page UI with cleaner, more immersive editing experience - Use CSS variables for theme-aware styling (paper-theme colors) - Make editor fill remaining viewport height with internal scrolling - Move action bar below editor (left-aligned, non-floating) - Fix layout: page no longer scrolls, only editor content scrolls - Update write skeleton to match new layout - Update admin layout to use h-dvh for write routes to prevent page scroll - Fix Dioxus.toml dev resource loading to include style.css Fixes from review: - Add missing relative positioning for loading overlay - Use consistent root_class in unauthenticated state - Use h-dvh instead of h-screen for mobile viewport compatibility - Ensure skeleton matches actual component layout classes --- Dioxus.toml | 2 +- src/components/admin_layout.rs | 28 ++++- src/components/write_skeleton.rs | 76 +++++++----- src/pages/admin/write.rs | 201 +++++++++++++++++-------------- 4 files changed, 178 insertions(+), 129 deletions(-) diff --git a/Dioxus.toml b/Dioxus.toml index f5ce35b..7abe9e8 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 = ["/tiptap/editor.css"] +style = ["/style.css", "/highlight.css", "/tiptap/editor.css"] script = ["/tiptap/editor.js"] diff --git a/src/components/admin_layout.rs b/src/components/admin_layout.rs index b990c4c..d1ea068 100644 --- a/src/components/admin_layout.rs +++ b/src/components/admin_layout.rs @@ -73,12 +73,26 @@ 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)()) { (true, Some(_)) => { rsx! { - div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]", + div { class: "{root_class}", Header { nav_items: admin_nav_items, right_content: right_content } - main { class: "flex-1 w-full max-w-5xl mx-auto px-6 py-8", + main { class: "{main_class}", Outlet:: {} } Footer {} @@ -87,17 +101,19 @@ pub fn AdminLayout() -> Element { } (true, None) => { rsx! { - div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]", - p { class: "text-gray-600 dark:text-[#9b9c9d]", "未登录,正在跳转..." } + div { class: "{root_class}", + div { class: "flex-1 flex items-center justify-center", + p { class: "text-gray-600 dark:text-[#9b9c9d]", "未登录,正在跳转..." } + } } } } (false, _) => { // 使用与真实布局完全相同的结构包裹内容骨架,避免 checked 变化时的布局闪烁 rsx! { - div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]", + div { class: "{root_class}", Header { nav_items: admin_nav_items, right_content: right_content } - main { class: "flex-1 w-full max-w-5xl mx-auto px-6 py-8", + main { class: "{main_class}", div { class: if show_skeleton() { "" } else { "opacity-0" }, {match route { Route::Write {} => rsx! { WriteSkeleton {} }, diff --git a/src/components/write_skeleton.rs b/src/components/write_skeleton.rs index f336d3e..4a4d66f 100644 --- a/src/components/write_skeleton.rs +++ b/src/components/write_skeleton.rs @@ -4,47 +4,61 @@ use crate::components::skeletons::atoms::*; #[component] pub fn WriteSkeleton() -> Element { rsx! { - 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", + div { class: "relative flex flex-col flex-1 min-h-0 overflow-hidden", + // 顶部元信息骨架 - 固定高度 + div { class: "flex-shrink-0 space-y-5 pt-8", + // 标题骨架 + SkeletonBox { class: "h-12 w-2/3 rounded-lg" } + + // 摘要骨架 + 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 { - div { class: "space-y-2", - SkeletonBox { class: "h-3 w-10 rounded" } - SkeletonBox { class: "h-10 w-full rounded-lg" } + div { class: "flex-1 min-w-[140px] space-y-2", + SkeletonBox { class: "h-3 w-12 rounded" } + SkeletonBox { class: "h-8 w-full rounded-lg" } } } } + + // 分隔线 + div { class: "h-px bg-[var(--color-paper-tertiary)]" } } - 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 gap-2 p-4 border-b border-gray-100 dark:border-[#333]", - for _ in 0..8 { - SkeletonBox { class: "w-8 h-8 rounded" } + // 编辑器骨架 - 沾满剩余高度 + 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: "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" } + // 按钮行骨架 + div { class: "flex-shrink-0 flex items-center gap-2 pt-2 pb-4", + SkeletonBox { class: "h-9 w-[60px] rounded-full" } + SkeletonBox { class: "h-9 w-[80px] rounded-full" } + SkeletonBox { class: "h-9 w-[60px] rounded-full" } } } } diff --git a/src/pages/admin/write.rs b/src/pages/admin/write.rs index ce0fdeb..60b55cf 100644 --- a/src/pages/admin/write.rs +++ b/src/pages/admin/write.rs @@ -328,128 +328,147 @@ fn write_editor(post_id: Option) -> Element { }; rsx! { - div { class: "relative", + div { class: "relative flex flex-col flex-1 min-h-0 overflow-hidden", if loading() { - div { class: "absolute inset-0 z-10 bg-white dark:bg-[#1d1e20]", + div { class: "absolute inset-0 z-10 bg-[var(--color-paper-theme)]", 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", + // 顶部元信息区域 - 固定高度,不滚动 + div { class: "flex-shrink-0 space-y-5 pt-8", + // 标题区域 - 大字号无框输入 + div { 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", + 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", placeholder: "文章标题", value: "{title}", oninput: move |evt| title.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()), + // 摘要 + textarea { + 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: "摘要(留空则自动生成)", + rows: "2", + value: "{summary}", + oninput: move |evt| summary.set(evt.value()), + } + + // 元数据行 - 紧凑精致 + div { class: "flex flex-wrap items-end gap-x-8 gap-y-4 text-sm", + div { class: "flex-1 min-w-[140px]", + label { class: "block text-[11px] font-medium text-[var(--color-paper-secondary)] tracking-wider mb-2", + "Slug" + } + 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: "自动生成", + value: "{slug}", + oninput: move |evt| slug.set(evt.value()), + } } - - div { class: "grid grid-cols-1 md:grid-cols-3 gap-3", - div { - label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] 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 { class: "flex-1 min-w-[140px]", + label { class: "block text-[11px] font-medium text-[var(--color-paper-secondary)] tracking-wider mb-2", + "标签" } - div { - label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] 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()), - } + 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: "逗号分隔", + value: "{tags}", + oninput: move |evt| tags.set(evt.value()), } - div { - label { class: "block text-xs text-gray-600 dark:text-[#9b9c9d] 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()), - } + } + 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: "h-px bg-[var(--color-paper-tertiary)]" } + } + + // 编辑器区域 - 沾满剩余高度 + div { class: "flex-1 min-h-0 flex flex-col my-4", div { - class: "w-full h-[500px] border border-gray-200 dark:border-[#333] rounded-lg overflow-hidden bg-white dark:bg-[#1e1e1e]", + 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", id: "tiptap-editor", } + } - 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", - "{err}" - } + // 错误和成功提示 + if let Some(err) = load_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 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 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: "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", - "保存成功" - } + 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 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: "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", "发布" } } - 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" - } + span { class: "pr-1.5 text-[var(--color-paper-primary)] font-medium", + if status() == "draft" { "草稿" } else { "发布" } + } + svg { + class: "h-3.5 w-3.5 text-[var(--color-paper-tertiary)] 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 { - 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, - "{save_button_text}" - } + } + div { class: "w-px h-5 bg-[var(--color-paper-border)]" } + button { + class: if saving() { + "px-5 py-1.5 text-sm bg-[var(--color-paper-tertiary)] text-[var(--color-paper-secondary)] rounded-xl font-medium cursor-not-allowed" + } 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, + "{save_button_text}" } } }