yggdrasil/src/pages/admin/dashboard.rs
xfy 27ed605223 fix(comments): 修复 section.rs / dashboard.rs 括号不匹配导致编译失败
两处同一类错误:match arm 内 `if {} else {}` 表达式后多了一个 `}`,
导致 arm 被提前关闭,触发 "unexpected closing delimiter" 编译错误。

- section.rs:125 — `if !has_any {…}` 块后多余的 `}`,删除使 if-else 合一
- dashboard.rs:119 — 同类问题,另需补 `;` 终止 `let` 语句
2026-07-15 18:12:00 +08:00

237 lines
10 KiB
Rust

//! 管理后台仪表盘页面。
//!
//! 采用高密度工业风设计的管理面板,突出核心数据指标与最新的工作流状态。
//! 数据仅在 WASM 前端通过 Dioxus server functions 异步加载。
use dioxus::prelude::*;
use dioxus::router::components::Link;
#[cfg(target_arch = "wasm32")]
use crate::api::comments::get_pending_count;
#[cfg(target_arch = "wasm32")]
use crate::api::posts::{get_post_stats, list_posts};
#[cfg(target_arch = "wasm32")]
use crate::api::posts::{PostListResponse, PostStatsResponse};
use crate::components::empty_state::{EmptyState, EmptyStateAction};
use crate::components::skeletons::atoms::SkeletonBox;
use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS, BTN_PRIMARY, BTN_SECONDARY};
use crate::models::post::{PostListItem, PostStats};
use crate::router::Route;
#[component]
#[allow(unused_mut)]
pub fn Admin() -> Element {
let mut stats = use_signal(|| None::<PostStats>);
let mut recent_posts = use_signal(|| None::<Vec<PostListItem>>);
let mut pending_count = use_signal(|| None::<i64>);
let mut loaded = use_signal(|| false);
use_effect(move || {
if !loaded() {
loaded.set(true);
#[cfg(target_arch = "wasm32")]
{
spawn(async move {
if let Ok(PostStatsResponse { stats: s }) = get_post_stats().await {
stats.set(Some(s));
}
});
spawn(async move {
if let Ok(PostListResponse { posts, total: _ }) = list_posts(1, 5).await {
recent_posts.set(Some(posts));
}
});
spawn(async move {
if let Ok(resp) = get_pending_count().await {
pending_count.set(Some(resp.count));
}
});
}
}
});
rsx! {
div { class: "w-full max-w-7xl mx-auto space-y-8",
// 顶部标题和全局操作栏
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 {
h1 { class: "text-4xl font-extrabold tracking-tight text-[var(--color-paper-primary)]",
"仪表盘"
}
p { class: "text-base text-[var(--color-paper-secondary)] mt-2",
"数据概览与近期活动"
}
}
div { class: "flex items-center gap-3",
Link { class: "{BTN_SECONDARY}", to: Route::Posts {}, "管理文章" }
Link { class: "{BTN_PRIMARY}", to: Route::Write {}, "发布文章" }
}
}
// 数据指标 Bento Grid
div { class: "grid grid-cols-1 md:grid-cols-4 gap-4",
match stats() {
Some(s) => {
rsx! {
StatCard {
value: s.total.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 => {
rsx! {
for _ in 0..3 {
div { class: "{ADMIN_CARD_CLASS} p-6 flex flex-col justify-between h-32 animate-pulse",
SkeletonBox { class: "h-3 w-20 rounded" }
SkeletonBox { class: "h-10 w-16 rounded mt-4" }
}
}
}
}
}
// 评论待办卡片 (独立色块突出)
Link {
class: "block {ADMIN_CARD_CLASS} p-8 bg-[var(--color-paper-entry)] hover:bg-[var(--color-paper-border)]/20 transition-all h-36 flex flex-col justify-between group hover:-translate-y-1 hover:shadow-md duration-300",
to: Route::AdminComments {},
match pending_count() {
Some(count) => {
let (color_class, text_class) = if count > 0 {
("text-amber-500", "text-amber-500")
} else {
(
"text-[var(--color-paper-secondary)]",
"text-[var(--color-paper-primary)]",
)
};
rsx! {
div { class: "text-sm font-medium {color_class}", "待审评论" }
div { class: "flex items-baseline justify-between mt-4",
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",
"去审核 →"
}
}
}
}
None => {
rsx! {
SkeletonBox { class: "h-3 w-24 rounded" }
SkeletonBox { class: "h-10 w-16 rounded mt-4" }
}
}
}
}
}
// 最近文章列表
div { class: "mt-12",
div { class: "flex items-center justify-between mb-6",
h2 { class: "text-xl font-bold text-[var(--color-paper-primary)] tracking-tight",
"近期文章"
}
}
match recent_posts() {
// 空库 / 无文章:展示空状态占位(与 posts.rs 列表页一致)。
// 放在 ADMIN_TABLE_CLASS 容器之外,避免 overflow-hidden 裁掉插画的 py-20 内边距。
Some(posts) if posts.is_empty() => {
rsx! {
EmptyState {
title: "暂无文章",
description: "还没有创建任何文章,开始写下你的第一篇文字吧。",
action: Some(EmptyStateAction {
label: "写文章".to_string(),
to: Route::Write {},
}),
}
}
}
Some(posts) => {
rsx! {
div { class: "{ADMIN_TABLE_CLASS}",
div { class: "divide-y divide-paper-border",
for post in posts.iter().take(5) {
RecentPostItem { key: "{post.id}", post: post.clone() }
}
}
}
}
}
// 加载中:骨架屏。
None => {
rsx! {
div { class: "{ADMIN_TABLE_CLASS}",
div { class: "divide-y divide-paper-border animate-pulse",
for _ in 0..5 {
div { class: "flex justify-between items-center px-6 py-4",
SkeletonBox { class: "h-4 w-[40%] rounded" }
SkeletonBox { class: "h-3 w-24 rounded" }
}
}
}
}
}
}
}
}
}
}
}
#[component]
fn StatCard(value: String, label: String, trend: String) -> Element {
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: "flex justify-between items-start",
div { class: "text-sm font-medium text-[var(--color-paper-secondary)]",
"{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}"
}
}
}
}
#[component]
fn RecentPostItem(post: PostListItem) -> Element {
let date_str = post.formatted_date();
let status_label = post.status_label();
let status_class = post.status_class();
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 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-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}"
}
}
}
}