style: 全项目 rsx! 格式规范化(cargo fmt)
This commit is contained in:
parent
cbb8be7441
commit
886eeca915
@ -95,12 +95,7 @@ pub fn AdminLayout() -> Element {
|
|||||||
"text-[var(--color-paper-secondary)] hover:bg-[var(--color-paper-theme)]/50 hover:text-[var(--color-paper-primary)] border border-transparent"
|
"text-[var(--color-paper-secondary)] hover:bg-[var(--color-paper-theme)]/50 hover:text-[var(--color-paper-primary)] border border-transparent"
|
||||||
};
|
};
|
||||||
rsx! {
|
rsx! {
|
||||||
Link {
|
Link { key: "{label}", class: "{base_class} {text_class}", to: dest, "{label}" }
|
||||||
key: "{label}",
|
|
||||||
class: "{base_class} {text_class}",
|
|
||||||
to: dest,
|
|
||||||
"{label}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,8 +144,12 @@ pub fn AdminLayout() -> Element {
|
|||||||
div { class: "flex-1 min-h-0 flex flex-col animate-pulse",
|
div { class: "flex-1 min-h-0 flex flex-col animate-pulse",
|
||||||
{
|
{
|
||||||
match route {
|
match route {
|
||||||
Route::Write {} => rsx! { WriteSkeleton {} },
|
Route::Write {} => rsx! {
|
||||||
_ => rsx! { AdminDashboardSkeleton {} },
|
WriteSkeleton {}
|
||||||
|
},
|
||||||
|
_ => rsx! {
|
||||||
|
AdminDashboardSkeleton {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -487,7 +487,9 @@ pub fn CodeRunner(
|
|||||||
div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]",
|
div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]",
|
||||||
div { class: "flex items-center gap-3",
|
div { class: "flex items-center gap-3",
|
||||||
span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" }
|
span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" }
|
||||||
span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]", "{language}" }
|
span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]",
|
||||||
|
"{language}"
|
||||||
|
}
|
||||||
button {
|
button {
|
||||||
class: format!(
|
class: format!(
|
||||||
"text-[10px] px-1.5 py-0.5 rounded border transition cursor-pointer {}",
|
"text-[10px] px-1.5 py-0.5 rounded border transition cursor-pointer {}",
|
||||||
@ -495,7 +497,7 @@ pub fn CodeRunner(
|
|||||||
"bg-[var(--color-paper-accent)]/15 text-[var(--color-paper-accent)] border-[var(--color-paper-accent)]/30 font-semibold"
|
"bg-[var(--color-paper-accent)]/15 text-[var(--color-paper-accent)] border-[var(--color-paper-accent)]/30 font-semibold"
|
||||||
} else {
|
} else {
|
||||||
"bg-transparent text-[var(--color-paper-tertiary)] border-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)]"
|
"bg-transparent text-[var(--color-paper-tertiary)] border-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)]"
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
onclick: toggle_vim,
|
onclick: toggle_vim,
|
||||||
"Vim"
|
"Vim"
|
||||||
@ -506,7 +508,8 @@ pub fn CodeRunner(
|
|||||||
disabled: running(),
|
disabled: running(),
|
||||||
onclick: run_code,
|
onclick: run_code,
|
||||||
if running() {
|
if running() {
|
||||||
span { class: "inline-block w-3.5 h-3.5 text-[var(--color-paper-theme)]",
|
span {
|
||||||
|
class: "inline-block w-3.5 h-3.5 text-[var(--color-paper-theme)]",
|
||||||
dangerous_inner_html: SPINNER_SVG,
|
dangerous_inner_html: SPINNER_SVG,
|
||||||
}
|
}
|
||||||
"{stage()}"
|
"{stage()}"
|
||||||
@ -523,14 +526,28 @@ pub fn CodeRunner(
|
|||||||
// editor_ready 由挂载 effect 置 true 后,此处 if 分支消失,骨架屏从 DOM 移除。
|
// editor_ready 由挂载 effect 置 true 后,此处 if 分支消失,骨架屏从 DOM 移除。
|
||||||
// 用绝对定位覆盖在(始终存在的)容器上方,不影响 CodeMirror 的 getElementById 挂载。
|
// 用绝对定位覆盖在(始终存在的)容器上方,不影响 CodeMirror 的 getElementById 挂载。
|
||||||
if !editor_ready() {
|
if !editor_ready() {
|
||||||
div {
|
div { class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
|
||||||
class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
|
|
||||||
// 代码行占位条:递减宽度模拟代码缩进,贴合等宽字体语境。
|
// 代码行占位条:递减宽度模拟代码缩进,贴合等宽字体语境。
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 90%" }
|
div {
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 70%" }
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 55%" }
|
style: "width: 90%",
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 85%" }
|
}
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 40%" }
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 70%",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 55%",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 85%",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 40%",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -548,9 +565,18 @@ pub fn CodeRunner(
|
|||||||
// running 且尚未收到首个 chunk 时,显示骨架屏占位。
|
// running 且尚未收到首个 chunk 时,显示骨架屏占位。
|
||||||
if running() && !has_output() {
|
if running() && !has_output() {
|
||||||
div { class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
|
div { class: "absolute inset-0 flex flex-col justify-center gap-2.5 px-4 py-4 bg-[var(--color-paper-code-block)]",
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 70%" }
|
div {
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 55%" }
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
div { class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse", style: "width: 85%" }
|
style: "width: 70%",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 55%",
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
class: "h-3 rounded bg-[var(--color-paper-tertiary)]/25 dark:bg-gray-600/50 animate-pulse",
|
||||||
|
style: "width: 85%",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,10 +119,9 @@ pub fn CommentSection(post_id: i32) -> Element {
|
|||||||
let has_any = approved_count > 0 || pending_count > 0;
|
let has_any = approved_count > 0 || pending_count > 0;
|
||||||
if !has_any {
|
if !has_any {
|
||||||
rsx! {
|
rsx! {
|
||||||
p { class: "text-paper-tertiary text-center py-8",
|
p { class: "text-paper-tertiary text-center py-8", "暂无评论,成为第一个评论的人吧!" }
|
||||||
"暂无评论,成为第一个评论的人吧!"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rsx! {
|
rsx! {
|
||||||
CommentList {
|
CommentList {
|
||||||
|
|||||||
@ -170,23 +170,25 @@ pub fn PostContent(content_html: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "post-content md-content",
|
div { class: "post-content md-content",
|
||||||
for (i, fragment) in fragments.iter().enumerate() {
|
for (i, fragment) in fragments.iter().enumerate() {
|
||||||
{match fragment {
|
{
|
||||||
ContentFragment::Html(html) => rsx! {
|
match fragment {
|
||||||
div { key: "html-{i}", dangerous_inner_html: "{html}" }
|
ContentFragment::Html(html) => rsx! {
|
||||||
},
|
div { key: "html-{i}", dangerous_inner_html: "{html}" }
|
||||||
ContentFragment::Runnable { lang, source, overrides } => rsx! {
|
},
|
||||||
CodeRunner {
|
ContentFragment::Runnable { lang, source, overrides } => rsx! { // i 是片段序列中的确定性索引(来自纯函数 split_content_fragments
|
||||||
key: "runner-{i}",
|
CodeRunner {
|
||||||
source: source.clone(),
|
key: "runner-{i}",
|
||||||
language: lang.clone(),
|
source: source.clone(),
|
||||||
overrides: overrides.clone(),
|
language: lang.clone(),
|
||||||
// i 是片段序列中的确定性索引(来自纯函数 split_content_fragments
|
overrides: overrides.clone(),
|
||||||
// 对同一 content_html 的解析),SSR 与 hydration 一致,用作容器
|
// i 是片段序列中的确定性索引(来自纯函数 split_content_fragments
|
||||||
// id 后缀保证 hydration 时 CodeMirror 能找到 SSR 渲染的容器。
|
// 对同一 content_html 的解析),SSR 与 hydration 一致,用作容器
|
||||||
instance_id: i,
|
// id 后缀保证 hydration 时 CodeMirror 能找到 SSR 渲染的容器。
|
||||||
}
|
instance_id: i,
|
||||||
},
|
}
|
||||||
}}
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,23 +59,23 @@ pub fn PostCard(post: PostListItem) -> Element {
|
|||||||
}
|
}
|
||||||
div { class: "mt-4 flex flex-wrap items-center gap-3 text-sm font-medium text-[var(--color-paper-tertiary)]",
|
div { class: "mt-4 flex flex-wrap items-center gap-3 text-sm font-medium text-[var(--color-paper-tertiary)]",
|
||||||
span { class: "tracking-wide", "{date_str}" }
|
span { class: "tracking-wide", "{date_str}" }
|
||||||
if !post.tags.is_empty() {
|
if !post.tags.is_empty() {
|
||||||
span { "·" }
|
span { "·" }
|
||||||
for tag in post.tags.clone().into_iter() {
|
for tag in post.tags.clone().into_iter() {
|
||||||
span { key: "{tag}",
|
span { key: "{tag}",
|
||||||
Link {
|
Link {
|
||||||
class: "relative z-10 px-3 py-1 rounded-full border border-[var(--color-paper-border)] hover:bg-[var(--color-paper-accent)] hover:border-[var(--color-paper-accent)] hover:text-white transition-all duration-200",
|
class: "relative z-10 px-3 py-1 rounded-full border border-[var(--color-paper-border)] hover:bg-[var(--color-paper-accent)] hover:border-[var(--color-paper-accent)] hover:text-white transition-all duration-200",
|
||||||
to: Route::TagDetail {
|
to: Route::TagDetail {
|
||||||
tag: tag.clone(),
|
tag: tag.clone(),
|
||||||
},
|
},
|
||||||
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
|
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
|
||||||
"{tag}"
|
"{tag}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 覆盖层链接:铺满卡片,承担整卡跳转。
|
// 覆盖层链接:铺满卡片,承担整卡跳转。
|
||||||
// z-[2] 高于封面完整图 (.blur-img-full z-index:1, input.css:611),
|
// z-[2] 高于封面完整图 (.blur-img-full z-index:1, input.css:611),
|
||||||
// 使鼠标 hover 封面时命中本 <a> 而非 <img>:光标变 pointer 且点击可跳转;
|
// 使鼠标 hover 封面时命中本 <a> 而非 <img>:光标变 pointer 且点击可跳转;
|
||||||
|
|||||||
@ -47,8 +47,7 @@ fn render_cell(value: &serde_json::Value) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Value::String(s) => rsx! {
|
Value::String(s) => rsx! {
|
||||||
span {
|
span { class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
|
||||||
class: "block font-mono text-xs text-[var(--color-paper-secondary)] truncate {TEXT_CELL_MAX_WIDTH_CLASS}",
|
|
||||||
"{s}"
|
"{s}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -79,9 +78,7 @@ fn render_expanded_value(col: &str, value: &serde_json::Value) -> Element {
|
|||||||
span { class: "shrink-0 font-mono text-xs text-[var(--color-paper-tertiary)] min-w-[6rem]",
|
span { class: "shrink-0 font-mono text-xs text-[var(--color-paper-tertiary)] min-w-[6rem]",
|
||||||
"{col}"
|
"{col}"
|
||||||
}
|
}
|
||||||
span { class: "font-mono text-xs text-[var(--color-paper-secondary)]",
|
span { class: "font-mono text-xs text-[var(--color-paper-secondary)]", "{display}" }
|
||||||
"{display}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,8 +111,7 @@ pub fn SqlResultTable(props: SqlResultTableProps) -> Element {
|
|||||||
tr { class: "border-b border-[var(--color-paper-border)] sticky top-0 bg-[var(--color-paper-entry)] z-10",
|
tr { class: "border-b border-[var(--color-paper-border)] sticky top-0 bg-[var(--color-paper-entry)] z-10",
|
||||||
th { class: "w-8 px-2 py-2", "" }
|
th { class: "w-8 px-2 py-2", "" }
|
||||||
for col in props.result.columns.iter() {
|
for col in props.result.columns.iter() {
|
||||||
th {
|
th { class: "px-4 py-2 text-left font-medium whitespace-nowrap text-[var(--color-paper-secondary)]",
|
||||||
class: "px-4 py-2 text-left font-medium whitespace-nowrap text-[var(--color-paper-secondary)]",
|
|
||||||
"{col}"
|
"{col}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,22 +131,23 @@ pub fn SqlResultTable(props: SqlResultTableProps) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
td { class: "px-2 py-2 text-center text-[var(--color-paper-tertiary)] text-xs select-none",
|
td { class: "px-2 py-2 text-center text-[var(--color-paper-tertiary)] text-xs select-none",
|
||||||
if expanded_row() == Some(row_idx) { "▾" } else { "▸" }
|
if expanded_row() == Some(row_idx) {
|
||||||
|
"▾"
|
||||||
|
} else {
|
||||||
|
"▸"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for cell in row.iter() {
|
for cell in row.iter() {
|
||||||
td {
|
td { class: "px-4 py-2 align-top", {render_cell(cell)} }
|
||||||
class: "px-4 py-2 align-top",
|
|
||||||
{render_cell(cell)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 展开详情行(跨列)
|
// 展开详情行(跨列)
|
||||||
if expanded_row() == Some(row_idx) {
|
if expanded_row() == Some(row_idx) {
|
||||||
tr {
|
tr {
|
||||||
td { colspan: "{expand_colspan}",
|
td {
|
||||||
|
colspan: "{expand_colspan}",
|
||||||
class: "px-4 py-3 bg-[var(--color-paper-theme)]",
|
class: "px-4 py-3 bg-[var(--color-paper-theme)]",
|
||||||
div {
|
div { class: "{EXPAND_MAX_HEIGHT_CLASS} overflow-y-auto space-y-0.5 whitespace-pre-wrap break-all",
|
||||||
class: "{EXPAND_MAX_HEIGHT_CLASS} overflow-y-auto space-y-0.5 whitespace-pre-wrap break-all",
|
|
||||||
for (ci, col) in props.result.columns.iter().enumerate() {
|
for (ci, col) in props.result.columns.iter().enumerate() {
|
||||||
{render_expanded_value(col, row.get(ci).unwrap_or(&serde_json::Value::Null))}
|
{render_expanded_value(col, row.get(ci).unwrap_or(&serde_json::Value::Null))}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,13 +169,13 @@ pub fn Pagination(
|
|||||||
nav { class: if is_admin { "flex mt-6 justify-between" } else { "flex mt-10 mb-6 justify-between" },
|
nav { class: if is_admin { "flex mt-6 justify-between" } else { "flex mt-10 mb-6 justify-between" },
|
||||||
if has_prev {
|
if has_prev {
|
||||||
if let Some(on_prev) = on_prev {
|
if let Some(on_prev) = on_prev {
|
||||||
button { class: "{link_class}", onclick: move |_| on_prev.call(()),
|
button {
|
||||||
|
class: "{link_class}",
|
||||||
|
onclick: move |_| on_prev.call(()),
|
||||||
{prev_inner}
|
{prev_inner}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Link { class: "{link_class}", to: prev_route,
|
Link { class: "{link_class}", to: prev_route, {prev_inner} }
|
||||||
{prev_inner}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if is_admin {
|
} else if is_admin {
|
||||||
span { class: "{disabled_class}",
|
span { class: "{disabled_class}",
|
||||||
@ -193,11 +193,15 @@ pub fn Pagination(
|
|||||||
|
|
||||||
if has_next {
|
if has_next {
|
||||||
if let Some(on_next) = on_next {
|
if let Some(on_next) = on_next {
|
||||||
button { class: "{link_class} {link_extra_next}", onclick: move |_| on_next.call(()),
|
button {
|
||||||
|
class: "{link_class} {link_extra_next}",
|
||||||
|
onclick: move |_| on_next.call(()),
|
||||||
{next_inner}
|
{next_inner}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Link { class: "{link_class} {link_extra_next}", to: next_route,
|
Link {
|
||||||
|
class: "{link_class} {link_extra_next}",
|
||||||
|
to: next_route,
|
||||||
{next_inner}
|
{next_inner}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -386,11 +390,7 @@ pub fn Popover(
|
|||||||
onclick: move |_| on_close.call(()),
|
onclick: move |_| on_close.call(()),
|
||||||
}
|
}
|
||||||
// 面板:fixed 定位逃出 overflow-hidden 容器。
|
// 面板:fixed 定位逃出 overflow-hidden 容器。
|
||||||
div {
|
div { class: "{POPOVER_PANEL_CLASS}", style: "{style}", {children} }
|
||||||
class: "{POPOVER_PANEL_CLASS}",
|
|
||||||
style: "{style}",
|
|
||||||
{children}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,9 @@ pub fn About() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "animate-page-enter",
|
div { class: "animate-page-enter",
|
||||||
header { class: "page-header mb-6",
|
header { class: "page-header mb-6",
|
||||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "关于" }
|
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
|
||||||
|
"关于"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
article { class: "prose dark:prose-invert max-w-none text-paper-content leading-relaxed",
|
article { class: "prose dark:prose-invert max-w-none text-paper-content leading-relaxed",
|
||||||
p { "世界……遗忘我……" }
|
p { "世界……遗忘我……" }
|
||||||
|
|||||||
@ -129,8 +129,12 @@ pub fn AdminCommentsPage(page: i32) -> Element {
|
|||||||
div { class: "w-full max-w-7xl mx-auto space-y-6",
|
div { class: "w-full max-w-7xl mx-auto space-y-6",
|
||||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
|
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
|
||||||
div {
|
div {
|
||||||
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]", "评论管理" }
|
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
|
||||||
p { class: "text-base text-[var(--color-paper-secondary)] mt-2", "所有文章评论" }
|
"评论管理"
|
||||||
|
}
|
||||||
|
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
||||||
|
"所有文章评论"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,20 +55,16 @@ pub fn Admin() -> Element {
|
|||||||
// 顶部标题和全局操作栏
|
// 顶部标题和全局操作栏
|
||||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
|
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
|
||||||
div {
|
div {
|
||||||
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]", "仪表盘" }
|
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
|
||||||
p { class: "text-base text-[var(--color-paper-secondary)] mt-2", "数据概览与近期活动" }
|
"仪表盘"
|
||||||
|
}
|
||||||
|
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
||||||
|
"数据概览与近期活动"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "flex items-center gap-3",
|
div { class: "flex items-center gap-3",
|
||||||
Link {
|
Link { class: "{BTN_SECONDARY}", to: Route::Posts {}, "管理文章" }
|
||||||
class: "{BTN_SECONDARY}",
|
Link { class: "{BTN_PRIMARY}", to: Route::Write {}, "发布文章" }
|
||||||
to: Route::Posts {},
|
|
||||||
"管理文章"
|
|
||||||
}
|
|
||||||
Link {
|
|
||||||
class: "{BTN_PRIMARY}",
|
|
||||||
to: Route::Write {},
|
|
||||||
"发布文章"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,9 +73,21 @@ pub fn Admin() -> Element {
|
|||||||
match stats() {
|
match stats() {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
rsx! {
|
rsx! {
|
||||||
StatCard { value: s.total.to_string(), label: "总文章数".to_string(), trend: "+12%".to_string() }
|
StatCard {
|
||||||
StatCard { value: s.published.to_string(), label: "已发布".to_string(), trend: "活跃".to_string() }
|
value: s.total.to_string(),
|
||||||
StatCard { value: s.drafts.to_string(), label: "草稿".to_string(), trend: "待处理".to_string() }
|
label: "总文章数".to_string(),
|
||||||
|
trend: "+12%".to_string(),
|
||||||
|
}
|
||||||
|
StatCard {
|
||||||
|
value: s.published.to_string(),
|
||||||
|
label: "已发布".to_string(),
|
||||||
|
trend: "活跃".to_string(),
|
||||||
|
}
|
||||||
|
StatCard {
|
||||||
|
value: s.drafts.to_string(),
|
||||||
|
label: "草稿".to_string(),
|
||||||
|
trend: "待处理".to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@ -103,13 +111,19 @@ pub fn Admin() -> Element {
|
|||||||
let (color_class, text_class) = if count > 0 {
|
let (color_class, text_class) = if count > 0 {
|
||||||
("text-amber-500", "text-amber-500")
|
("text-amber-500", "text-amber-500")
|
||||||
} else {
|
} else {
|
||||||
("text-[var(--color-paper-secondary)]", "text-[var(--color-paper-primary)]")
|
(
|
||||||
};
|
"text-[var(--color-paper-secondary)]",
|
||||||
|
"text-[var(--color-paper-primary)]",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "text-sm font-medium {color_class}", "待审评论" }
|
div { class: "text-sm font-medium {color_class}", "待审评论" }
|
||||||
div { class: "flex items-baseline justify-between mt-4",
|
div { class: "flex items-baseline justify-between mt-4",
|
||||||
div { class: "text-4xl font-light tracking-tight {text_class}", "{count}" }
|
div { class: "text-4xl font-light tracking-tight {text_class}", "{count}" }
|
||||||
div { class: "text-xs font-medium text-[var(--color-paper-secondary)] group-hover:text-[var(--color-paper-primary)] transition-colors", "去审核 →" }
|
div { class: "text-xs font-medium text-[var(--color-paper-secondary)] group-hover:text-[var(--color-paper-primary)] transition-colors",
|
||||||
|
"去审核 →"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,7 +140,9 @@ pub fn Admin() -> Element {
|
|||||||
// 最近文章列表
|
// 最近文章列表
|
||||||
div { class: "mt-12",
|
div { class: "mt-12",
|
||||||
div { class: "flex items-center justify-between mb-6",
|
div { class: "flex items-center justify-between mb-6",
|
||||||
h2 { class: "text-xl font-bold text-[var(--color-paper-primary)] tracking-tight", "近期文章" }
|
h2 { class: "text-xl font-bold text-[var(--color-paper-primary)] tracking-tight",
|
||||||
|
"近期文章"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match recent_posts() {
|
match recent_posts() {
|
||||||
// 空库 / 无文章:展示空状态占位(与 posts.rs 列表页一致)。
|
// 空库 / 无文章:展示空状态占位(与 posts.rs 列表页一致)。
|
||||||
@ -180,10 +196,16 @@ fn StatCard(value: String, label: String, trend: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col justify-between h-36 relative group hover:-translate-y-1 hover:shadow-md transition-all duration-300",
|
div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col justify-between h-36 relative group hover:-translate-y-1 hover:shadow-md transition-all duration-300",
|
||||||
div { class: "flex justify-between items-start",
|
div { class: "flex justify-between items-start",
|
||||||
div { class: "text-sm font-medium text-[var(--color-paper-secondary)]", "{label}" }
|
div { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
|
||||||
div { class: "text-xs px-2 py-0.5 rounded-full border border-[var(--color-paper-border)] text-[var(--color-paper-tertiary)]", "{trend}" }
|
"{label}"
|
||||||
|
}
|
||||||
|
div { class: "text-xs px-2 py-0.5 rounded-full border border-[var(--color-paper-border)] text-[var(--color-paper-tertiary)]",
|
||||||
|
"{trend}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { class: "text-4xl font-light tracking-tight text-[var(--color-paper-primary)] mt-4",
|
||||||
|
"{value}"
|
||||||
}
|
}
|
||||||
div { class: "text-4xl font-light tracking-tight text-[var(--color-paper-primary)] mt-4", "{value}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,11 +219,19 @@ fn RecentPostItem(post: PostListItem) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "flex flex-col sm:flex-row sm:justify-between sm:items-center px-8 py-5 hover:bg-[var(--color-paper-accent-soft)] transition-colors cursor-pointer group",
|
div { class: "flex flex-col sm:flex-row sm:justify-between sm:items-center px-8 py-5 hover:bg-[var(--color-paper-accent-soft)] transition-colors cursor-pointer group",
|
||||||
div { class: "flex items-center gap-6",
|
div { class: "flex items-center gap-6",
|
||||||
span { class: "text-xs font-mono text-[var(--color-paper-tertiary)] w-12 hidden sm:block", "#{post.id:04}" }
|
span { class: "text-xs font-mono text-[var(--color-paper-tertiary)] w-12 hidden sm:block",
|
||||||
span { class: "text-base font-semibold text-[var(--color-paper-primary)] group-hover:text-[var(--color-paper-accent)] transition-colors", "{post.title}" }
|
"#{post.id:04}"
|
||||||
span { class: "text-xs px-3 py-1 font-medium rounded-full {status_class}", "{status_label}" }
|
}
|
||||||
|
span { class: "text-base font-semibold text-[var(--color-paper-primary)] group-hover:text-[var(--color-paper-accent)] transition-colors",
|
||||||
|
"{post.title}"
|
||||||
|
}
|
||||||
|
span { class: "text-xs px-3 py-1 font-medium rounded-full {status_class}",
|
||||||
|
"{status_label}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span { class: "text-sm text-[var(--color-paper-secondary)] mt-2 sm:mt-0",
|
||||||
|
"{date_str}"
|
||||||
}
|
}
|
||||||
span { class: "text-sm text-[var(--color-paper-secondary)] mt-2 sm:mt-0", "{date_str}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,11 +106,7 @@ pub fn Posts() -> Element {
|
|||||||
if active_tab() == PostsTab::All {
|
if active_tab() == PostsTab::All {
|
||||||
div { class: "flex items-center gap-3",
|
div { class: "flex items-center gap-3",
|
||||||
RebuildCacheBar {}
|
RebuildCacheBar {}
|
||||||
Link {
|
Link { class: "{BTN_PRIMARY}", to: Route::Write {}, "发布文章" }
|
||||||
class: "{BTN_PRIMARY}",
|
|
||||||
to: Route::Write {},
|
|
||||||
"发布文章"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,8 +121,12 @@ pub fn Posts() -> Element {
|
|||||||
// key 化条件渲染:切 tab 完全卸载/重挂,隔离各自 use_paginated 状态。
|
// key 化条件渲染:切 tab 完全卸载/重挂,隔离各自 use_paginated 状态。
|
||||||
div { key: "{active_tab().as_str()}",
|
div { key: "{active_tab().as_str()}",
|
||||||
match active_tab() {
|
match active_tab() {
|
||||||
PostsTab::All => rsx! { AllPostsList {} },
|
PostsTab::All => rsx! {
|
||||||
PostsTab::Trash => rsx! { PostsTrashPanel {} },
|
AllPostsList {}
|
||||||
|
},
|
||||||
|
PostsTab::Trash => rsx! {
|
||||||
|
PostsTrashPanel {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -184,13 +184,9 @@ fn AllPostsList() -> Element {
|
|||||||
thead {
|
thead {
|
||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-3 font-medium", "标题" }
|
th { class: "px-4 py-3 font-medium", "标题" }
|
||||||
th { class: "px-4 py-3 font-medium w-24 text-center",
|
th { class: "px-4 py-3 font-medium w-24 text-center", "状态" }
|
||||||
"状态"
|
|
||||||
}
|
|
||||||
th { class: "px-4 py-3 font-medium w-32", "日期" }
|
th { class: "px-4 py-3 font-medium w-32", "日期" }
|
||||||
th { class: "px-4 py-3 font-medium w-44 text-right",
|
th { class: "px-4 py-3 font-medium w-44 text-right", "操作" }
|
||||||
"操作"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
@ -310,10 +306,7 @@ fn RebuildCacheBar() -> Element {
|
|||||||
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE },
|
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE },
|
||||||
disabled: rebuilding(),
|
disabled: rebuilding(),
|
||||||
onclick: move |_| do_rebuild(false),
|
onclick: move |_| do_rebuild(false),
|
||||||
span {
|
span { class: if rebuilding() { "opacity-40" } else { "" }, "重建内容" }
|
||||||
class: if rebuilding() { "opacity-40" } else { "" },
|
|
||||||
"重建内容"
|
|
||||||
}
|
|
||||||
if rebuilding() {
|
if rebuilding() {
|
||||||
span {
|
span {
|
||||||
class: "absolute inset-0 flex items-center justify-center",
|
class: "absolute inset-0 flex items-center justify-center",
|
||||||
@ -329,10 +322,7 @@ fn RebuildCacheBar() -> Element {
|
|||||||
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE },
|
class: if rebuilding() { "relative px-4 py-2 rounded-full text-sm font-medium cursor-not-allowed text-paper-secondary border border-paper-border" } else { BTN_OUTLINE },
|
||||||
disabled: rebuilding(),
|
disabled: rebuilding(),
|
||||||
onclick: move |_| do_rebuild(true),
|
onclick: move |_| do_rebuild(true),
|
||||||
span {
|
span { class: if rebuilding() { "opacity-40" } else { "" }, "重建全部" }
|
||||||
class: if rebuilding() { "opacity-40" } else { "" },
|
|
||||||
"重建全部"
|
|
||||||
}
|
|
||||||
if rebuilding() {
|
if rebuilding() {
|
||||||
span {
|
span {
|
||||||
class: "absolute inset-0 flex items-center justify-center",
|
class: "absolute inset-0 flex items-center justify-center",
|
||||||
@ -344,9 +334,7 @@ fn RebuildCacheBar() -> Element {
|
|||||||
}
|
}
|
||||||
// 重建结果消息:独立成行,右对齐,与按钮行同属本组件。
|
// 重建结果消息:独立成行,右对齐,与按钮行同属本组件。
|
||||||
if let Some(msg) = rebuild_result() {
|
if let Some(msg) = rebuild_result() {
|
||||||
div { class: "text-xs text-paper-secondary whitespace-pre-line",
|
div { class: "text-xs text-paper-secondary whitespace-pre-line", "{msg}" }
|
||||||
"{msg}"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -388,20 +376,12 @@ fn PostRow(
|
|||||||
to: Route::WriteEdit { id: post.id },
|
to: Route::WriteEdit { id: post.id },
|
||||||
"编辑"
|
"编辑"
|
||||||
}
|
}
|
||||||
Tooltip {
|
Tooltip { tip: "重新渲染这篇文章的 HTML".to_string(),
|
||||||
tip: "重新渲染这篇文章的 HTML".to_string(),
|
|
||||||
button {
|
button {
|
||||||
class: if rebuilding {
|
class: if rebuilding { "relative inline-flex items-center text-xs text-paper-accent cursor-not-allowed" } else { BTN_TEXT_ACCENT },
|
||||||
"relative inline-flex items-center text-xs text-paper-accent cursor-not-allowed"
|
|
||||||
} else {
|
|
||||||
BTN_TEXT_ACCENT
|
|
||||||
},
|
|
||||||
disabled: rebuilding,
|
disabled: rebuilding,
|
||||||
onclick: move |_| on_rebuild.call(post.id),
|
onclick: move |_| on_rebuild.call(post.id),
|
||||||
span {
|
span { class: if rebuilding { "opacity-40" } else { "" }, "重建" }
|
||||||
class: if rebuilding { "opacity-40" } else { "" },
|
|
||||||
"重建"
|
|
||||||
}
|
|
||||||
if rebuilding {
|
if rebuilding {
|
||||||
span {
|
span {
|
||||||
class: "absolute inset-0 flex items-center justify-center",
|
class: "absolute inset-0 flex items-center justify-center",
|
||||||
@ -411,17 +391,10 @@ fn PostRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
class: if deleting {
|
class: if deleting { "relative inline-flex items-center text-xs text-paper-secondary cursor-not-allowed" } else { BTN_TEXT_RED },
|
||||||
"relative inline-flex items-center text-xs text-paper-secondary cursor-not-allowed"
|
|
||||||
} else {
|
|
||||||
BTN_TEXT_RED
|
|
||||||
},
|
|
||||||
disabled: deleting,
|
disabled: deleting,
|
||||||
onclick: move |_| on_delete.call(post.id),
|
onclick: move |_| on_delete.call(post.id),
|
||||||
span {
|
span { class: if deleting { "opacity-40" } else { "" }, "删除" }
|
||||||
class: if deleting { "opacity-40" } else { "" },
|
|
||||||
"删除"
|
|
||||||
}
|
|
||||||
if deleting {
|
if deleting {
|
||||||
span {
|
span {
|
||||||
class: "absolute inset-0 flex items-center justify-center",
|
class: "absolute inset-0 flex items-center justify-center",
|
||||||
@ -498,31 +471,18 @@ pub(super) fn PostsTabs(
|
|||||||
div { class: "relative flex items-center gap-4 border-b border-paper-border",
|
div { class: "relative flex items-center gap-4 border-b border-paper-border",
|
||||||
button {
|
button {
|
||||||
id: "posts-tab-{id_prefix}-all",
|
id: "posts-tab-{id_prefix}-all",
|
||||||
class: if !is_trash {
|
class: if !is_trash { "inline-flex items-center px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer" } else { "inline-flex items-center px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer" },
|
||||||
"inline-flex items-center px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer"
|
|
||||||
} else {
|
|
||||||
"inline-flex items-center px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer"
|
|
||||||
},
|
|
||||||
onclick: move |_| on_change.call(PostsTab::All),
|
onclick: move |_| on_change.call(PostsTab::All),
|
||||||
"全部文章"
|
"全部文章"
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
id: "posts-tab-{id_prefix}-trash",
|
id: "posts-tab-{id_prefix}-trash",
|
||||||
class: if is_trash {
|
class: if is_trash { "inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer" } else { "inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer" },
|
||||||
"inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-primary transition-colors cursor-pointer"
|
|
||||||
} else {
|
|
||||||
"inline-flex items-center gap-1.5 px-2 py-3 text-xs font-mono tracking-widest uppercase text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer"
|
|
||||||
},
|
|
||||||
onclick: move |_| on_change.call(PostsTab::Trash),
|
onclick: move |_| on_change.call(PostsTab::Trash),
|
||||||
"回收站"
|
"回收站"
|
||||||
// 数量角标:有数据才显示。0 显示中性灰,>0 用主题强调色提醒。
|
// 数量角标:有数据才显示。0 显示中性灰,>0 用主题强调色提醒。
|
||||||
if let Some(count) = trash_count() {
|
if let Some(count) = trash_count() {
|
||||||
span {
|
span { class: if count > 0 { "inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-accent-soft text-paper-accent" } else { "inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-tertiary text-paper-secondary" },
|
||||||
class: if count > 0 {
|
|
||||||
"inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-accent-soft text-paper-accent"
|
|
||||||
} else {
|
|
||||||
"inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5 rounded-full text-[0.625rem] font-semibold normal-case tracking-normal bg-paper-tertiary text-paper-secondary"
|
|
||||||
},
|
|
||||||
"{count}"
|
"{count}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,9 @@ pub fn Runner() -> Element {
|
|||||||
// 页头:与 dashboard / posts / system 对齐(h1 text-4xl + 底部分割线)
|
// 页头:与 dashboard / posts / system 对齐(h1 text-4xl + 底部分割线)
|
||||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
|
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-8 border-b border-[var(--color-paper-border)]/50",
|
||||||
div {
|
div {
|
||||||
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]", "代码试运行沙箱" }
|
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
|
||||||
|
"代码试运行沙箱"
|
||||||
|
}
|
||||||
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
||||||
"在此快速试运行代码,验证文章中可运行代码块的预期输出。资源钳制与读者侧一致,速率限制对 admin 放行。"
|
"在此快速试运行代码,验证文章中可运行代码块的预期输出。资源钳制与读者侧一致,速率限制对 admin 放行。"
|
||||||
}
|
}
|
||||||
@ -68,7 +70,9 @@ pub fn Runner() -> Element {
|
|||||||
div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col gap-6",
|
div { class: "{ADMIN_CARD_CLASS} p-8 flex flex-col gap-6",
|
||||||
// 语言切换
|
// 语言切换
|
||||||
div { class: "flex flex-col gap-2",
|
div { class: "flex flex-col gap-2",
|
||||||
label { class: "text-sm font-medium text-[var(--color-paper-secondary)]", "语言" }
|
label { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
|
||||||
|
"语言"
|
||||||
|
}
|
||||||
div { class: "flex gap-2",
|
div { class: "flex gap-2",
|
||||||
for l in SUPPORTED_LANGS {
|
for l in SUPPORTED_LANGS {
|
||||||
button {
|
button {
|
||||||
@ -77,7 +81,8 @@ pub fn Runner() -> Element {
|
|||||||
BTN_PRIMARY_SM
|
BTN_PRIMARY_SM
|
||||||
} else {
|
} else {
|
||||||
"px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-secondary)] bg-[var(--color-paper-theme)] hover:bg-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)] transition cursor-pointer"
|
"px-4 py-1.5 text-sm font-medium rounded-full text-[var(--color-paper-secondary)] bg-[var(--color-paper-theme)] hover:bg-[var(--color-paper-border)] hover:text-[var(--color-paper-primary)] transition cursor-pointer"
|
||||||
}).to_string(),
|
})
|
||||||
|
.to_string(),
|
||||||
onclick: {
|
onclick: {
|
||||||
let ll = (*l).to_string();
|
let ll = (*l).to_string();
|
||||||
move |_| {
|
move |_| {
|
||||||
@ -119,7 +124,7 @@ pub fn Runner() -> Element {
|
|||||||
CodeRunner {
|
CodeRunner {
|
||||||
source: source(),
|
source: source(),
|
||||||
language: lang(),
|
language: lang(),
|
||||||
overrides: overrides,
|
overrides,
|
||||||
instance_id: 0,
|
instance_id: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,8 +65,12 @@ pub fn System() -> Element {
|
|||||||
// 页面标题
|
// 页面标题
|
||||||
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
|
div { class: "flex flex-col md:flex-row md:items-end justify-between gap-6 pb-6 border-b border-[var(--color-paper-border)] mb-6",
|
||||||
div {
|
div {
|
||||||
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]", "系统面板" }
|
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
|
||||||
p { class: "text-base text-[var(--color-paper-secondary)] mt-2", "数据库与服务器诊断" }
|
"系统面板"
|
||||||
|
}
|
||||||
|
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
|
||||||
|
"数据库与服务器诊断"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,11 +96,21 @@ pub fn System() -> Element {
|
|||||||
// 避免 hook slot 复用导致 DelayedSkeleton 的 visible 信号残留为 true。
|
// 避免 hook slot 复用导致 DelayedSkeleton 的 visible 信号残留为 true。
|
||||||
div { key: "{active_tab().as_str()}",
|
div { key: "{active_tab().as_str()}",
|
||||||
match active_tab() {
|
match active_tab() {
|
||||||
SystemTab::DbStatus => rsx! { DbStatusTab {} },
|
SystemTab::DbStatus => rsx! {
|
||||||
SystemTab::ServerStatus => rsx! { ServerStatusTab {} },
|
DbStatusTab {}
|
||||||
SystemTab::SqlConsole => rsx! { SqlConsoleTab {} },
|
},
|
||||||
SystemTab::Export => rsx! { ExportTab {} },
|
SystemTab::ServerStatus => rsx! {
|
||||||
SystemTab::Backup => rsx! { BackupTab {} },
|
ServerStatusTab {}
|
||||||
|
},
|
||||||
|
SystemTab::SqlConsole => rsx! {
|
||||||
|
SqlConsoleTab {}
|
||||||
|
},
|
||||||
|
SystemTab::Export => rsx! {
|
||||||
|
ExportTab {}
|
||||||
|
},
|
||||||
|
SystemTab::Backup => rsx! {
|
||||||
|
BackupTab {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,13 +266,16 @@ fn DbStatusTab() -> Element {
|
|||||||
value: "{refresh_interval().map(|s| s.to_string()).unwrap_or_default()}",
|
value: "{refresh_interval().map(|s| s.to_string()).unwrap_or_default()}",
|
||||||
onchange: move |e| {
|
onchange: move |e| {
|
||||||
let v = e.value();
|
let v = e.value();
|
||||||
refresh_interval.set(match v.as_str() {
|
refresh_interval
|
||||||
"1" => Some(1),
|
.set(
|
||||||
"2" => Some(2),
|
match v.as_str() {
|
||||||
"5" => Some(5),
|
"1" => Some(1),
|
||||||
"30" => Some(30),
|
"2" => Some(2),
|
||||||
_ => None,
|
"5" => Some(5),
|
||||||
});
|
"30" => Some(30),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
option { value: "", "手动" }
|
option { value: "", "手动" }
|
||||||
option { value: "1", "1s" }
|
option { value: "1", "1s" }
|
||||||
@ -278,15 +295,21 @@ fn DbStatusTab() -> Element {
|
|||||||
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "数据库总大小" }
|
p { class: "text-xs text-paper-secondary", "数据库总大小" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{format_bytes(s.db_size_bytes)}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{format_bytes(s.db_size_bytes)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "连接数" }
|
p { class: "text-xs text-paper-secondary", "连接数" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{s.total_connections} / {s.max_connections}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{s.total_connections} / {s.max_connections}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "表数量" }
|
p { class: "text-xs text-paper-secondary", "表数量" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{s.tables.len()}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{s.tables.len()}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "迁移版本" }
|
p { class: "text-xs text-paper-secondary", "迁移版本" }
|
||||||
@ -306,24 +329,48 @@ fn DbStatusTab() -> Element {
|
|||||||
thead {
|
thead {
|
||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-2 font-medium", "表名" }
|
th { class: "px-4 py-2 font-medium", "表名" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "行数" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
th { class: "px-4 py-2 font-medium text-right", "表大小" }
|
"行数"
|
||||||
th { class: "px-4 py-2 font-medium text-right", "索引大小" }
|
}
|
||||||
th { class: "px-4 py-2 font-medium text-right", "总大小" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
th { class: "px-4 py-2 font-medium text-right", "死元组" }
|
"表大小"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"索引大小"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"总大小"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"死元组"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for t in s.tables.iter() {
|
for t in s.tables.iter() {
|
||||||
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
||||||
td { class: "px-4 py-2 font-mono text-paper-primary", "{t.name}" }
|
td { class: "px-4 py-2 font-mono text-paper-primary",
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary",
|
"{t.name}"
|
||||||
if t.row_count_estimated { "~{t.row_count}" } else { "{t.row_count}" }
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
if t.row_count_estimated {
|
||||||
|
"~{t.row_count}"
|
||||||
|
} else {
|
||||||
|
"{t.row_count}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{format_bytes(t.table_size_bytes)}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{format_bytes(t.index_size_bytes)}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-primary font-medium",
|
||||||
|
"{format_bytes(t.total_size_bytes)}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{t.dead_tuples}"
|
||||||
}
|
}
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.table_size_bytes)}" }
|
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(t.index_size_bytes)}" }
|
|
||||||
td { class: "px-4 py-2 text-right text-paper-primary font-medium", "{format_bytes(t.total_size_bytes)}" }
|
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{t.dead_tuples}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,15 +390,23 @@ fn DbStatusTab() -> Element {
|
|||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-2 font-medium", "索引名" }
|
th { class: "px-4 py-2 font-medium", "索引名" }
|
||||||
th { class: "px-4 py-2 font-medium", "所属表" }
|
th { class: "px-4 py-2 font-medium", "所属表" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "大小" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"大小"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for i in s.top_indexes.iter() {
|
for i in s.top_indexes.iter() {
|
||||||
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
||||||
td { class: "px-4 py-2 font-mono text-paper-primary", "{i.name}" }
|
td { class: "px-4 py-2 font-mono text-paper-primary",
|
||||||
td { class: "px-4 py-2 font-mono text-paper-secondary", "{i.table_name}" }
|
"{i.name}"
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{format_bytes(i.size_bytes)}" }
|
}
|
||||||
|
td { class: "px-4 py-2 font-mono text-paper-secondary",
|
||||||
|
"{i.table_name}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{format_bytes(i.size_bytes)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -372,20 +427,30 @@ fn DbStatusTab() -> Element {
|
|||||||
th { class: "px-4 py-2 font-medium", "PID" }
|
th { class: "px-4 py-2 font-medium", "PID" }
|
||||||
th { class: "px-4 py-2 font-medium", "用户" }
|
th { class: "px-4 py-2 font-medium", "用户" }
|
||||||
th { class: "px-4 py-2 font-medium", "状态" }
|
th { class: "px-4 py-2 font-medium", "状态" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "时长(秒)" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"时长(秒)"
|
||||||
|
}
|
||||||
th { class: "px-4 py-2 font-medium", "查询" }
|
th { class: "px-4 py-2 font-medium", "查询" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for c in s.active_connections.iter() {
|
for c in s.active_connections.iter() {
|
||||||
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
||||||
td { class: "px-4 py-2 text-paper-secondary", "{c.pid}" }
|
td { class: "px-4 py-2 text-paper-secondary",
|
||||||
td { class: "px-4 py-2 text-paper-secondary", "{c.user}" }
|
"{c.pid}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-paper-secondary",
|
||||||
|
"{c.user}"
|
||||||
|
}
|
||||||
td { class: "px-4 py-2 text-paper-secondary",
|
td { class: "px-4 py-2 text-paper-secondary",
|
||||||
{c.state.clone().unwrap_or_else(|| "—".to_string())}
|
{c.state.clone().unwrap_or_else(|| "—".to_string())}
|
||||||
}
|
}
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary",
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
{c.query_duration_secs.map(|d| format!("{:.1}", d)).unwrap_or_else(|| "—".to_string())}
|
{
|
||||||
|
c.query_duration_secs
|
||||||
|
.map(|d| format!("{:.1}", d))
|
||||||
|
.unwrap_or_else(|| "—".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
td { class: "px-4 py-2 font-mono text-xs text-paper-secondary max-w-md truncate",
|
td { class: "px-4 py-2 font-mono text-xs text-paper-secondary max-w-md truncate",
|
||||||
{c.query.clone().unwrap_or_else(|| "—".to_string())}
|
{c.query.clone().unwrap_or_else(|| "—".to_string())}
|
||||||
@ -553,7 +618,7 @@ fn ServerStatusTab() -> Element {
|
|||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "space-y-6",
|
div { class: "space-y-6",
|
||||||
div { class: "flex items-center justify-between",
|
div { class: "flex items-center justify-between",
|
||||||
LoadingButton {
|
LoadingButton {
|
||||||
label: "刷新".to_string(),
|
label: "刷新".to_string(),
|
||||||
loading: loading(),
|
loading: loading(),
|
||||||
@ -586,13 +651,16 @@ fn ServerStatusTab() -> Element {
|
|||||||
value: "{refresh_ms().map(|s| s.to_string()).unwrap_or_default()}",
|
value: "{refresh_ms().map(|s| s.to_string()).unwrap_or_default()}",
|
||||||
onchange: move |e| {
|
onchange: move |e| {
|
||||||
let v = e.value();
|
let v = e.value();
|
||||||
refresh_ms.set(match v.as_str() {
|
refresh_ms
|
||||||
"500" => Some(500),
|
.set(
|
||||||
"1000" => Some(1000),
|
match v.as_str() {
|
||||||
"2000" => Some(2000),
|
"500" => Some(500),
|
||||||
"5000" => Some(5000),
|
"1000" => Some(1000),
|
||||||
_ => None,
|
"2000" => Some(2000),
|
||||||
});
|
"5000" => Some(5000),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
option { value: "", "手动" }
|
option { value: "", "手动" }
|
||||||
option { value: "500", "500ms" }
|
option { value: "500", "500ms" }
|
||||||
@ -612,20 +680,30 @@ fn ServerStatusTab() -> Element {
|
|||||||
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "运行时间" }
|
p { class: "text-xs text-paper-secondary", "运行时间" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{format_uptime(s.uptime_secs)}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{format_uptime(s.uptime_secs)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "DB 连接池" }
|
p { class: "text-xs text-paper-secondary", "DB 连接池" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{s.pool_size} / {s.pool_max_size}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
p { class: "text-xs text-paper-secondary", "空闲 {s.pool_available} · 等待 {s.pool_waiting}" }
|
"{s.pool_size} / {s.pool_max_size}"
|
||||||
|
}
|
||||||
|
p { class: "text-xs text-paper-secondary",
|
||||||
|
"空闲 {s.pool_available} · 等待 {s.pool_waiting}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "活跃会话" }
|
p { class: "text-xs text-paper-secondary", "活跃会话" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{s.active_sessions}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{s.active_sessions}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "CPU" }
|
p { class: "text-xs text-paper-secondary", "CPU" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{cpu_pct}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{cpu_pct}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,19 +711,27 @@ fn ServerStatusTab() -> Element {
|
|||||||
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4",
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "内存" }
|
p { class: "text-xs text-paper-secondary", "内存" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{format_bytes(s.host.used_memory as i64)} / {format_bytes(s.host.total_memory as i64)}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{format_bytes(s.host.used_memory as i64)} / {format_bytes(s.host.total_memory as i64)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "磁盘" }
|
p { class: "text-xs text-paper-secondary", "磁盘" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{format_bytes((s.host.disk_total - s.host.disk_available) as i64)} / {format_bytes(s.host.disk_total as i64)}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{format_bytes((s.host.disk_total - s.host.disk_available) as i64)} / {format_bytes(s.host.disk_total as i64)}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "Load (1m)" }
|
p { class: "text-xs text-paper-secondary", "Load (1m)" }
|
||||||
p { class: "mt-1 text-lg font-semibold text-paper-primary", "{load_1}" }
|
p { class: "mt-1 text-lg font-semibold text-paper-primary",
|
||||||
|
"{load_1}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "{ADMIN_CARD_CLASS} p-4",
|
div { class: "{ADMIN_CARD_CLASS} p-4",
|
||||||
p { class: "text-xs text-paper-secondary", "系统" }
|
p { class: "text-xs text-paper-secondary", "系统" }
|
||||||
p { class: "mt-1 text-sm font-medium text-paper-primary truncate", "{s.host.os_name}" }
|
p { class: "mt-1 text-sm font-medium text-paper-primary truncate",
|
||||||
|
"{s.host.os_name}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -659,20 +745,38 @@ fn ServerStatusTab() -> Element {
|
|||||||
thead {
|
thead {
|
||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-2 font-medium", "缓存" }
|
th { class: "px-4 py-2 font-medium", "缓存" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "条目" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
th { class: "px-4 py-2 font-medium text-right", "命中" }
|
"条目"
|
||||||
th { class: "px-4 py-2 font-medium text-right", "未命中" }
|
}
|
||||||
th { class: "px-4 py-2 font-medium text-right", "命中率" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"命中"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"未命中"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"命中率"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for (name, entry_count, hits, misses, rate_pct) in cache_rows.iter() {
|
for (name, entry_count, hits, misses, rate_pct) in cache_rows.iter() {
|
||||||
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
tr { class: "border-b border-paper-border last:border-0 hover:bg-paper-entry transition-colors",
|
||||||
td { class: "px-4 py-2 text-paper-primary", "{name}" }
|
td { class: "px-4 py-2 text-paper-primary",
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{entry_count}" }
|
"{name}"
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{hits}" }
|
}
|
||||||
td { class: "px-4 py-2 text-right text-paper-secondary", "{misses}" }
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
td { class: "px-4 py-2 text-right text-paper-primary font-medium", "{rate_pct}" }
|
"{entry_count}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{hits}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-secondary",
|
||||||
|
"{misses}"
|
||||||
|
}
|
||||||
|
td { class: "px-4 py-2 text-right text-paper-primary font-medium",
|
||||||
|
"{rate_pct}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -968,9 +1072,13 @@ fn SqlConsoleTab() -> Element {
|
|||||||
div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]",
|
div { class: "flex justify-between items-center px-4 py-2.5 border-b border-[var(--color-paper-border)] bg-[var(--color-paper-theme)]",
|
||||||
div { class: "flex items-center gap-2",
|
div { class: "flex items-center gap-2",
|
||||||
span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" }
|
span { class: "w-2 h-2 rounded-full bg-[var(--color-paper-accent)]" }
|
||||||
span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]", "SQL" }
|
span { class: "font-mono text-sm font-semibold text-[var(--color-paper-primary)]",
|
||||||
|
"SQL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
span { class: "text-xs text-[var(--color-paper-tertiary)] font-mono",
|
||||||
|
"⌘↵ 执行"
|
||||||
}
|
}
|
||||||
span { class: "text-xs text-[var(--color-paper-tertiary)] font-mono", "⌘↵ 执行" }
|
|
||||||
}
|
}
|
||||||
// CodeMirror 容器:用 flex 让 .cm-editor(flex:1) 填满整个高度,
|
// CodeMirror 容器:用 flex 让 .cm-editor(flex:1) 填满整个高度,
|
||||||
// 避免编辑器塌缩到内容高度、底部透出容器背景造成上下色差。
|
// 避免编辑器塌缩到内容高度、底部透出容器背景造成上下色差。
|
||||||
@ -1068,7 +1176,9 @@ fn SqlConsoleTab() -> Element {
|
|||||||
div { class: "px-4 py-2 border-b border-paper-border text-sm font-medium text-paper-primary",
|
div { class: "px-4 py-2 border-b border-paper-border text-sm font-medium text-paper-primary",
|
||||||
"执行计划"
|
"执行计划"
|
||||||
}
|
}
|
||||||
pre { class: "p-4 text-xs font-mono text-paper-secondary overflow-x-auto whitespace-pre m-0", "{explain}" }
|
pre { class: "p-4 text-xs font-mono text-paper-secondary overflow-x-auto whitespace-pre m-0",
|
||||||
|
"{explain}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1143,12 +1253,16 @@ fn ExportTab() -> Element {
|
|||||||
value: "{table_name}",
|
value: "{table_name}",
|
||||||
oninput: move |e| table_name.set(e.value()),
|
oninput: move |e| table_name.set(e.value()),
|
||||||
}
|
}
|
||||||
p { class: "text-xs text-paper-secondary mt-1", "仅支持 public schema 下的用户表,表名需为合法标识符" }
|
p { class: "text-xs text-paper-secondary mt-1",
|
||||||
|
"仅支持 public schema 下的用户表,表名需为合法标识符"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 查询输入
|
// 查询输入
|
||||||
div {
|
div {
|
||||||
label { class: "block text-sm text-paper-secondary mb-1", "SELECT 查询(只读)" }
|
label { class: "block text-sm text-paper-secondary mb-1",
|
||||||
|
"SELECT 查询(只读)"
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
class: "w-full px-3 py-2 text-sm border border-paper-border rounded bg-paper-theme text-paper-primary font-mono",
|
class: "w-full px-3 py-2 text-sm border border-paper-border rounded bg-paper-theme text-paper-primary font-mono",
|
||||||
rows: "4",
|
rows: "4",
|
||||||
@ -1182,11 +1296,7 @@ fn ExportTab() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button { class: "{BTN_PRIMARY_SM}", onclick: move |_| do_export(), "导出并下载" }
|
||||||
class: "{BTN_PRIMARY_SM}",
|
|
||||||
onclick: move |_| do_export(),
|
|
||||||
"导出并下载"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p { class: "text-xs text-paper-secondary",
|
p { class: "text-xs text-paper-secondary",
|
||||||
"导出走流式响应,大表不会占满内存。SQL 格式仅含 INSERT 语句(不含 DDL/schema)。"
|
"导出走流式响应,大表不会占满内存。SQL 格式仅含 INSERT 语句(不含 DDL/schema)。"
|
||||||
@ -1357,7 +1467,10 @@ fn BackupTab() -> Element {
|
|||||||
spawn(async move {
|
spawn(async move {
|
||||||
match create_backup().await {
|
match create_backup().await {
|
||||||
Ok(id) => active_task_id.set(Some(id)),
|
Ok(id) => active_task_id.set(Some(id)),
|
||||||
Err(e) => { error.set(Some(e.to_string())); busy.set(false); }
|
Err(e) => {
|
||||||
|
error.set(Some(e.to_string()));
|
||||||
|
busy.set(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1379,13 +1492,18 @@ fn BackupTab() -> Element {
|
|||||||
span { class: "text-sm text-paper-secondary", "{p.percent}%" }
|
span { class: "text-sm text-paper-secondary", "{p.percent}%" }
|
||||||
}
|
}
|
||||||
div { class: "w-full bg-paper-entry rounded-full h-2 overflow-hidden",
|
div { class: "w-full bg-paper-entry rounded-full h-2 overflow-hidden",
|
||||||
div { class: "bg-paper-accent h-full transition-all", style: "width: {p.percent}%" }
|
div {
|
||||||
|
class: "bg-paper-accent h-full transition-all",
|
||||||
|
style: "width: {p.percent}%",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(detail) = p.detail {
|
if let Some(detail) = p.detail {
|
||||||
p { class: "text-xs text-paper-secondary mt-2", "{detail}" }
|
p { class: "text-xs text-paper-secondary mt-2", "{detail}" }
|
||||||
}
|
}
|
||||||
if let Some(err) = p.error {
|
if let Some(err) = p.error {
|
||||||
p { class: "text-xs text-red-600 dark:text-red-400 mt-2", "错误:{err}" }
|
p { class: "text-xs text-red-600 dark:text-red-400 mt-2",
|
||||||
|
"错误:{err}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1406,8 +1524,12 @@ fn BackupTab() -> Element {
|
|||||||
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
tr { class: "border-b border-paper-border text-left text-paper-secondary",
|
||||||
th { class: "px-4 py-2 font-medium", "文件名" }
|
th { class: "px-4 py-2 font-medium", "文件名" }
|
||||||
th { class: "px-4 py-2 font-medium", "模式" }
|
th { class: "px-4 py-2 font-medium", "模式" }
|
||||||
th { class: "px-4 py-2 font-medium text-right", "大小" }
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
th { class: "px-4 py-2 font-medium text-right", "操作" }
|
"大小"
|
||||||
|
}
|
||||||
|
th { class: "px-4 py-2 font-medium text-right",
|
||||||
|
"操作"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
@ -1572,7 +1694,9 @@ fn BackupRow(props: BackupRowProps) -> Element {
|
|||||||
span { class: "font-mono text-xs break-all", "{props.filename}" }
|
span { class: "font-mono text-xs break-all", "{props.filename}" }
|
||||||
"?"
|
"?"
|
||||||
}
|
}
|
||||||
p { class: "text-xs text-paper-secondary", "仅本系统生成的备份可恢复。" }
|
p { class: "text-xs text-paper-secondary",
|
||||||
|
"仅本系统生成的备份可恢复。"
|
||||||
|
}
|
||||||
div { class: "flex justify-end gap-2 pt-1",
|
div { class: "flex justify-end gap-2 pt-1",
|
||||||
button {
|
button {
|
||||||
class: "px-3 py-1.5 text-xs text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer",
|
class: "px-3 py-1.5 text-xs text-paper-secondary hover:text-paper-primary transition-colors cursor-pointer",
|
||||||
|
|||||||
@ -491,7 +491,9 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
div { class: "w-80 flex-shrink-0 min-h-0 overflow-y-auto border-l border-[var(--color-paper-border)] flex flex-col",
|
div { class: "w-80 flex-shrink-0 min-h-0 overflow-y-auto border-l border-[var(--color-paper-border)] flex flex-col",
|
||||||
// Slug 节
|
// Slug 节
|
||||||
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
||||||
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "链接" }
|
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3",
|
||||||
|
"链接"
|
||||||
|
}
|
||||||
input {
|
input {
|
||||||
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all",
|
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all",
|
||||||
placeholder: "自动生成",
|
placeholder: "自动生成",
|
||||||
@ -501,7 +503,9 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
}
|
}
|
||||||
// 标签节
|
// 标签节
|
||||||
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
||||||
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "标签" }
|
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3",
|
||||||
|
"标签"
|
||||||
|
}
|
||||||
input {
|
input {
|
||||||
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all",
|
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all",
|
||||||
placeholder: "逗号分隔...",
|
placeholder: "逗号分隔...",
|
||||||
@ -511,7 +515,9 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
}
|
}
|
||||||
// 摘要节
|
// 摘要节
|
||||||
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
div { class: "p-6 border-b border-[var(--color-paper-border)]",
|
||||||
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "摘要" }
|
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3",
|
||||||
|
"摘要"
|
||||||
|
}
|
||||||
textarea {
|
textarea {
|
||||||
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all resize-none leading-relaxed",
|
class: "w-full text-sm bg-[var(--color-paper-entry)] text-[var(--color-paper-primary)] placeholder-[var(--color-paper-tertiary)] focus:outline-none border border-[var(--color-paper-border)] focus:border-[var(--color-paper-primary)] rounded-2xl px-3 py-2 transition-all resize-none leading-relaxed",
|
||||||
placeholder: "选填...",
|
placeholder: "选填...",
|
||||||
@ -522,7 +528,9 @@ fn write_editor(post_id: Option<i32>) -> Element {
|
|||||||
}
|
}
|
||||||
// 封面图节
|
// 封面图节
|
||||||
div { class: "p-6",
|
div { class: "p-6",
|
||||||
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3", "封面图" }
|
label { class: "block text-xs font-semibold uppercase tracking-wide text-[var(--color-paper-tertiary)] mb-3",
|
||||||
|
"封面图"
|
||||||
|
}
|
||||||
CoverUploader { cover_image, cover_uploading }
|
CoverUploader { cover_image, cover_uploading }
|
||||||
}
|
}
|
||||||
} // 右栏闭合
|
} // 右栏闭合
|
||||||
@ -681,7 +689,9 @@ fn CoverUploader(cover_image: Signal<String>, cover_uploading: Signal<bool>) ->
|
|||||||
// —— 上传中:骨架占位 + 文案 ——
|
// —— 上传中:骨架占位 + 文案 ——
|
||||||
if cover_uploading() {
|
if cover_uploading() {
|
||||||
div { class: "absolute inset-0 flex flex-col items-center justify-center gap-3 bg-[var(--color-paper-tertiary)]/30 animate-pulse",
|
div { class: "absolute inset-0 flex flex-col items-center justify-center gap-3 bg-[var(--color-paper-tertiary)]/30 animate-pulse",
|
||||||
span { class: "text-sm font-medium text-[var(--color-paper-secondary)]", "正在上传..." }
|
span { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
|
||||||
|
"正在上传..."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -735,7 +745,9 @@ fn CoverUploader(cover_image: Signal<String>, cover_uploading: Signal<bool>) ->
|
|||||||
}
|
}
|
||||||
// 底部渐变遮罩 + "更换封面"提示(hover 出现)。
|
// 底部渐变遮罩 + "更换封面"提示(hover 出现)。
|
||||||
div { class: "absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover/cover:opacity-100 transition-opacity flex items-end justify-center pb-2 pointer-events-none",
|
div { class: "absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover/cover:opacity-100 transition-opacity flex items-end justify-center pb-2 pointer-events-none",
|
||||||
span { class: "text-sm font-medium text-white/90 drop-shadow-md", "点击更换封面" }
|
span { class: "text-sm font-medium text-white/90 drop-shadow-md",
|
||||||
|
"点击更换封面"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -103,7 +103,9 @@ pub fn Archives() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "animate-page-enter",
|
div { class: "animate-page-enter",
|
||||||
header { class: "page-header mb-6",
|
header { class: "page-header mb-6",
|
||||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "归档" }
|
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
|
||||||
|
"归档"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ArchivesContent {}
|
ArchivesContent {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,9 +121,7 @@ pub fn PostDetail(slug: String) -> Element {
|
|||||||
div { class: "mt-12 border-t border-gray-200 dark:border-gray-700 pt-8",
|
div { class: "mt-12 border-t border-gray-200 dark:border-gray-700 pt-8",
|
||||||
SuspenseBoundary {
|
SuspenseBoundary {
|
||||||
fallback: move |_| rsx! {
|
fallback: move |_| rsx! {
|
||||||
DelayedSkeleton {
|
DelayedSkeleton { crate::components::skeletons::comment_skeleton::CommentListSkeleton {} }
|
||||||
crate::components::skeletons::comment_skeleton::CommentListSkeleton {}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
crate::components::comments::section::CommentSection { post_id: post.id }
|
crate::components::comments::section::CommentSection { post_id: post.id }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,9 @@ pub fn Search() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "animate-page-enter",
|
div { class: "animate-page-enter",
|
||||||
header { class: "page-header mb-6",
|
header { class: "page-header mb-6",
|
||||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "搜索" }
|
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
|
||||||
|
"搜索"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div { class: "mb-8",
|
div { class: "mb-8",
|
||||||
div { class: "flex gap-2",
|
div { class: "flex gap-2",
|
||||||
|
|||||||
@ -29,7 +29,9 @@ pub fn Tags() -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "animate-page-enter",
|
div { class: "animate-page-enter",
|
||||||
header { class: "page-header mb-6",
|
header { class: "page-header mb-6",
|
||||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "标签" }
|
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
|
||||||
|
"标签"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TagsContent {}
|
TagsContent {}
|
||||||
}
|
}
|
||||||
@ -109,7 +111,9 @@ pub fn TagDetail(tag: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "animate-page-enter",
|
div { class: "animate-page-enter",
|
||||||
header { class: "page-header mb-6",
|
header { class: "page-header mb-6",
|
||||||
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight", "{tag}" }
|
h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
|
||||||
|
"{tag}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TagDetailContent { tag: tag.clone() }
|
TagDetailContent { tag: tag.clone() }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,25 +133,29 @@ fn ErrorLayout() -> Element {
|
|||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
if let Some(captured_error) = err.error() {
|
if let Some(captured_error) = err.error() {
|
||||||
let _ = dioxus::fullstack::FullstackContext::commit_error_status(captured_error);
|
let _ = dioxus::fullstack::FullstackContext::commit_error_status(
|
||||||
|
captured_error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the captured error to detect 404 vs 500
|
|
||||||
let mut is_404 = false;
|
let mut is_404 = false;
|
||||||
if let Some(captured_error) = err.error() {
|
if let Some(captured_error) = err.error() {
|
||||||
if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error.downcast_ref::<ServerFnError>() {
|
if let Some(ServerFnError::ServerError { code, message, .. }) = captured_error
|
||||||
|
.downcast_ref::<ServerFnError>()
|
||||||
|
|
||||||
|
{
|
||||||
if *code == 404 || message == "not_found" {
|
if *code == 404 || message == "not_found" {
|
||||||
is_404 = true;
|
is_404 = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else { // Warning / Alert Icon in a soft red container
|
||||||
let err_str = format!("{:?}", captured_error);
|
let err_str = format!("{:?}", captured_error);
|
||||||
if err_str.contains("NotFound") || err_str.contains("404") || err_str.contains("not found") {
|
if err_str.contains("NotFound") || err_str.contains("404")
|
||||||
|
|| err_str.contains("not found")
|
||||||
|
{
|
||||||
is_404 = true;
|
is_404 = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_404 {
|
if is_404 {
|
||||||
rsx! {
|
rsx! {
|
||||||
NotFound { segments: vec![] }
|
NotFound { segments: vec![] }
|
||||||
@ -174,8 +178,18 @@ fn ErrorLayout() -> Element {
|
|||||||
stroke_linecap: "round",
|
stroke_linecap: "round",
|
||||||
stroke_linejoin: "round",
|
stroke_linejoin: "round",
|
||||||
circle { cx: "12", cy: "12", r: "10" }
|
circle { cx: "12", cy: "12", r: "10" }
|
||||||
line { x1: "12", y1: "8", x2: "12", y2: "12" }
|
line {
|
||||||
line { x1: "12", y1: "16", x2: "12.01", y2: "16" }
|
x1: "12",
|
||||||
|
y1: "8",
|
||||||
|
x2: "12",
|
||||||
|
y2: "12",
|
||||||
|
}
|
||||||
|
line {
|
||||||
|
x1: "12",
|
||||||
|
y1: "16",
|
||||||
|
x2: "12.01",
|
||||||
|
y2: "16",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user