-
-
-
-
+
+
setOpen(false)} />
+
+
+ {/* 移动端顶部条 */}
+
+
+
+ sundynix{' '}
+ admin
+
+
+
+
+
+
+
)
}
diff --git a/admin/src/components/admin-sidebar.tsx b/admin/src/components/admin-sidebar.tsx
new file mode 100644
index 0000000..dc5777d
--- /dev/null
+++ b/admin/src/components/admin-sidebar.tsx
@@ -0,0 +1,109 @@
+import { Link, NavLink, useNavigate } from 'react-router-dom'
+import { FileText, LayoutDashboard, LogOut, Moon, Sun, X } from 'lucide-react'
+import { logout } from '@/api/auth'
+import { useTheme } from '@/components/theme-provider'
+import { LogoMark } from '@/components/logo'
+import { cn } from '@/lib/utils'
+
+const NAV = [
+ { to: '/', label: '仪表盘', icon: LayoutDashboard, end: true },
+ { to: '/posts', label: '文章', icon: FileText, end: false },
+]
+
+interface Props {
+ open: boolean
+ onClose: () => void
+}
+
+export function AdminSidebar({ open, onClose }: Props) {
+ const navigate = useNavigate()
+ const { resolved, setTheme } = useTheme()
+
+ const itemCls = ({ isActive }: { isActive: boolean }) =>
+ cn(
+ 'flex items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] transition-colors',
+ isActive
+ ? 'bg-accent-soft font-medium text-accent-ink'
+ : 'text-ink-2 hover:bg-ground hover:text-ink',
+ )
+
+ const bottomBtnCls =
+ 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] text-ink-2 transition-colors hover:bg-ground hover:text-ink'
+
+ return (
+ <>
+ {open && (
+
+ )}
+
+ >
+ )
+}
diff --git a/admin/src/index.css b/admin/src/index.css
index 5ddc07e..de39a39 100644
--- a/admin/src/index.css
+++ b/admin/src/index.css
@@ -71,3 +71,14 @@ body {
background: var(--accent-soft);
color: var(--accent-ink);
}
+
+/* 全局隐藏滚动条(保留滚动功能) */
+* {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+*::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ display: none;
+}
diff --git a/admin/src/pages/dashboard.tsx b/admin/src/pages/dashboard.tsx
new file mode 100644
index 0000000..c95a663
--- /dev/null
+++ b/admin/src/pages/dashboard.tsx
@@ -0,0 +1,114 @@
+import { useEffect, useState } from 'react'
+import { Link } from 'react-router-dom'
+import { CheckCircle2, FileEdit, FileText, Plus } from 'lucide-react'
+import { fetchStats, type Stats } from '@/api/posts'
+
+export default function DashboardPage() {
+ const [stats, setStats] = useState
(null)
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ let cancelled = false
+ fetchStats()
+ .then((s) => {
+ if (!cancelled) setStats(s)
+ })
+ .catch((e: Error) => {
+ if (!cancelled) setError(e.message)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [])
+
+ const cards = [
+ { label: '文章总数', value: stats?.total, icon: FileText },
+ { label: '已发布', value: stats?.published, icon: CheckCircle2 },
+ { label: '草稿', value: stats?.draft, icon: FileEdit },
+ ]
+
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {cards.map((c) => (
+
+
+ {c.label}
+
+
+
+ {c.value ?? '—'}
+
+
+ ))}
+
+
+
+
+
最近更新
+
+ 全部文章 →
+
+
+
+ {stats && stats.recent.length > 0 ? (
+ stats.recent.map((post) => (
+
+
+ {post.title}
+
+
+ {post.published_at ? (
+
+ 已发布
+
+ ) : (
+
+ 草稿
+
+ )}
+
+ {post.updated_at.slice(0, 10)}
+
+
+
+ ))
+ ) : (
+
+ {stats ? '还没有文章,点右上角新建' : '加载中…'}
+
+ )}
+
+
+
+ )
+}
diff --git a/server/internal/handler/admin_post.go b/server/internal/handler/admin_post.go
index bdc68ac..77f0cef 100644
--- a/server/internal/handler/admin_post.go
+++ b/server/internal/handler/admin_post.go
@@ -156,3 +156,23 @@ func (h *AdminPostHandler) Delete(c *gin.Context) {
}
resp.OK(c, nil)
}
+
+// Stats GET /api/admin/stats — 概览统计(总数 / 已发布 / 草稿 / 最近文章)
+func (h *AdminPostHandler) Stats(c *gin.Context) {
+ var total, published int64
+ if err := h.db.Model(&model.Post{}).Count(&total).Error; err != nil {
+ resp.ServerError(c, "查询失败")
+ return
+ }
+ h.db.Model(&model.Post{}).Where("published_at IS NOT NULL").Count(&published)
+
+ var recent []model.PostListItem
+ h.db.Model(&model.Post{}).Order("updated_at DESC").Limit(5).Find(&recent)
+
+ resp.OK(c, gin.H{
+ "total": total,
+ "published": published,
+ "draft": total - published,
+ "recent": recent,
+ })
+}
diff --git a/server/internal/router/router.go b/server/internal/router/router.go
index e39d4ac..736fdd6 100644
--- a/server/internal/router/router.go
+++ b/server/internal/router/router.go
@@ -42,6 +42,7 @@ func New(db *gorm.DB) (*gin.Engine, error) {
adminAPI := api.Group("/admin", middleware.Auth())
{
posts := handler.NewAdminPostHandler(db)
+ adminAPI.GET("/stats", posts.Stats)
adminAPI.GET("/posts", posts.List)
adminAPI.POST("/posts", posts.Create)
adminAPI.GET("/posts/:id", posts.Get)
diff --git a/web/src/index.css b/web/src/index.css
index a4b5caa..5728aef 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -82,6 +82,17 @@ body {
color: var(--accent-ink);
}
+/* 全局隐藏滚动条(保留滚动功能) */
+* {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+*::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ display: none;
+}
+
/* 品牌渐变文字(logo 同源:青→蓝→紫),克制使用 */
.text-brand-gradient {
background: linear-gradient(