Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
## [2.4.0](https://github.com/one-ea/Monolith/compare/v2.3.2...v2.4.0) (2026-05-03)


### Features
### Features

* **ui:** improve blog navigation and social links ([0851c95](https://github.com/one-ea/Monolith/commit/0851c95db1e2fb9a8c0bdf193326a63fb16af2cf))
144 changes: 89 additions & 55 deletions client/src/components/admin-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Link, useLocation } from "wouter";
import { clearToken } from "@/lib/api";
import {
Expand All @@ -14,16 +14,44 @@ import {
ExternalLink,
Menu,
Search,
X,
} from "lucide-react";
import { ThemeToggle } from "@/components/theme-toggle";

interface AdminLayoutProps {
children: React.ReactNode;
}

const NAV_GROUPS = [
{
title: "内容管理",
items: [
{ href: "/admin", icon: LayoutDashboard, label: "控制台" },
{ href: "/admin/pages", icon: StickyNote, label: "独立页面" },
{ href: "/admin/comments", icon: MessageCircle, label: "评论审核" },
],
},
{
title: "资源与数据",
items: [
{ href: "/admin/media", icon: ImageIcon, label: "媒体库" },
{ href: "/admin/analytics", icon: BarChart3, label: "数据分析" },
{ href: "/admin/seo", icon: Sparkles, label: "SEO 优化" },
{ href: "/admin/backup", icon: HardDrive, label: "安全备份" },
],
},
{
title: "系统配置",
items: [
{ href: "/admin/settings", icon: Settings, label: "站点设置" },
],
},
];

export function AdminLayout({ children }: AdminLayoutProps) {
const [location, setLocation] = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [navQuery, setNavQuery] = useState("");

useEffect(() => {
if (!mobileMenuOpen) return;
Expand All @@ -39,52 +67,33 @@ export function AdminLayout({ children }: AdminLayoutProps) {
setLocation("/admin/login");
};

const navGroups = [
{
title: "内容管理",
items: [
{ href: "/admin", icon: LayoutDashboard, label: "控制台" },
{ href: "/admin/pages", icon: StickyNote, label: "独立页面" },
{ href: "/admin/comments", icon: MessageCircle, label: "评论审核" },
],
},
{
title: "资源与数据",
items: [
{ href: "/admin/media", icon: ImageIcon, label: "媒体库" },
{ href: "/admin/analytics", icon: BarChart3, label: "数据分析" },
{ href: "/admin/seo", icon: Sparkles, label: "SEO 优化" },
{ href: "/admin/backup", icon: HardDrive, label: "安全备份" },
],
},
{
title: "系统配置",
items: [
{ href: "/admin/settings", icon: Settings, label: "站点设置" },
],
},
];

const currentTitle =
navGroups.flatMap((group) => group.items).find((item) =>
NAV_GROUPS.flatMap((group) => group.items).find((item) =>
item.href === "/admin" ? location === "/admin" : location.startsWith(item.href)
)?.label || "管理后台";

const filteredNavGroups = useMemo(() => {
const query = navQuery.trim().toLowerCase();
if (!query) return NAV_GROUPS;

return NAV_GROUPS
.map((group) => ({
...group,
items: group.items.filter((item) =>
item.label.toLowerCase().includes(query)
|| item.href.toLowerCase().includes(query)
|| group.title.toLowerCase().includes(query)
),
}))
.filter((group) => group.items.length > 0);
}, [navQuery]);

const SidebarFooter = () => (
<div className="space-y-[2px] border-t border-border/30 p-[12px]">
<div className="space-y-[2px] border-t border-border/20 p-[12px]">
<div className="flex min-h-[44px] items-center justify-between px-[12px] py-[8px]">
<span className="text-[13px] font-medium text-muted-foreground/60">主题</span>
<span className="text-[13px] font-medium text-muted-foreground/55">主题</span>
<ThemeToggle />
</div>
<a
href="/"
target="_blank"
rel="noopener noreferrer"
className="flex min-h-[44px] items-center gap-[10px] rounded-md px-[12px] py-[8px] text-[13px] font-medium text-muted-foreground/60 transition-colors hover:bg-muted/40 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
<ExternalLink className="w-[14px] h-[14px]" />
查看站点
</a>
<button
onClick={handleLogout}
className="flex min-h-[44px] w-full items-center gap-[10px] rounded-md px-[12px] py-[8px] text-[13px] font-medium text-red-500/70 transition-colors hover:bg-red-500/10 hover:text-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
Expand All @@ -99,22 +108,38 @@ export function AdminLayout({ children }: AdminLayoutProps) {
<div className="flex flex-col h-full">
<div className="border-b border-border/25 p-[16px]">
<Link href="/admin" className="flex items-center gap-[10px] rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring" onClick={() => setMobileMenuOpen(false)}>
<div className="flex h-[34px] w-[24px] items-center justify-center rounded-[4px] bg-gradient-to-b from-foreground to-foreground/45 text-[0] shadow-[0_12px_28px_oklch(0_0_0_/_20%)]">
<div className="flex h-[34px] w-[24px] items-center justify-center rounded-[4px] bg-gradient-to-b from-foreground/90 to-foreground/35 text-[0] shadow-[0_12px_28px_oklch(0_0_0_/_20%)]">
M
</div>
<div>
<span className="block text-[17px] font-semibold tracking-[-0.02em]">Monolith</span>
<span className="block font-heading text-[17px] font-semibold tracking-[-0.02em]">Monolith</span>
<span className="block text-[10px] text-muted-foreground/45">Admin Console</span>
</div>
</Link>
<div className="mt-[14px] flex min-h-[34px] items-center gap-[8px] rounded-md border border-border/20 bg-background/40 px-[10px] text-[12px] text-muted-foreground/45">
<Search className="h-[13px] w-[13px]" />
快速定位模块
</div>
<label className="mt-[14px] flex min-h-[44px] items-center gap-[8px] rounded-md border border-border/25 bg-background/45 px-[12px] text-[13px] text-foreground shadow-[0_8px_24px_oklch(0_0_0_/_10%)] transition-colors focus-within:border-foreground/25 focus-within:bg-background/65 focus-within:ring-1 focus-within:ring-foreground/10">
<Search className="h-[15px] w-[15px] shrink-0 text-muted-foreground/70" />
<span className="sr-only">快速定位模块</span>
<input
value={navQuery}
onChange={(event) => setNavQuery(event.target.value)}
placeholder="搜索模块"
className="min-w-0 flex-1 bg-transparent text-[13px] text-foreground outline-none placeholder:text-muted-foreground/65"
/>
{navQuery && (
<button
type="button"
onClick={() => setNavQuery("")}
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
aria-label="清空模块搜索"
>
<X className="h-[13px] w-[13px]" />
</button>
)}
</label>
</div>

<nav className="flex-1 space-y-[18px] overflow-y-auto px-[12px] py-[16px]">
{navGroups.map((group) => (
{filteredNavGroups.map((group) => (
<div key={group.title}>
<h3 className="mb-[6px] px-[12px] text-[10px] font-semibold tracking-normal text-muted-foreground/35">
{group.title}
Expand All @@ -128,14 +153,17 @@ export function AdminLayout({ children }: AdminLayoutProps) {
<Link
key={item.href}
href={item.href}
onClick={() => setMobileMenuOpen(false)}
onClick={() => {
setMobileMenuOpen(false);
setNavQuery("");
}}
className={`relative flex min-h-[40px] items-center gap-[10px] rounded-md px-[12px] py-[8px] text-[13px] font-medium transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring md:min-h-[36px] ${
isActive
? "bg-foreground text-background shadow-[0_10px_28px_oklch(0_0_0_/_18%)]"
: "text-muted-foreground/55 hover:bg-muted/40 hover:text-foreground"
? "bg-foreground/92 text-background shadow-[0_10px_28px_oklch(0_0_0_/_18%)]"
: "text-muted-foreground/55 hover:bg-muted/35 hover:text-foreground/85"
}`}
>
{isActive && <span className="absolute left-[4px] top-1/2 h-[18px] w-[2px] -translate-y-1/2 rounded-full bg-cyan-300" />}
{isActive && <span className="absolute left-[4px] top-1/2 h-[18px] w-[2px] -translate-y-1/2 rounded-full bg-cyan-300/80" />}
<item.icon className="w-[14px] h-[14px]" />
{item.label}
</Link>
Expand All @@ -144,6 +172,11 @@ export function AdminLayout({ children }: AdminLayoutProps) {
</div>
</div>
))}
{filteredNavGroups.length === 0 && (
<div className="rounded-md border border-border/25 bg-background/35 px-[12px] py-[14px] text-[12px] leading-[1.6] text-muted-foreground/65">
没有匹配的模块
</div>
)}
</nav>

<SidebarFooter />
Expand All @@ -152,7 +185,7 @@ export function AdminLayout({ children }: AdminLayoutProps) {

return (
<div className="h-screen w-full bg-background">
<aside className="fixed inset-y-0 left-0 z-30 hidden w-[248px] flex-col border-r border-border/30 bg-card/20 backdrop-blur-xl md:flex">
<aside className="fixed inset-y-0 left-0 z-30 hidden w-[248px] flex-col border-r border-border/25 bg-background/70 backdrop-blur-xl md:flex">
<SidebarContent />
</aside>

Expand Down Expand Up @@ -202,10 +235,11 @@ export function AdminLayout({ children }: AdminLayoutProps) {
</div>
</header>

<div className="hidden h-[56px] items-center justify-between border-b border-border/25 bg-background/70 px-[24px] backdrop-blur-xl md:flex">
<div>
<p className="text-[12px] text-muted-foreground/45">Monolith 管理后台</p>
<h1 className="text-[18px] font-semibold tracking-[-0.01em]">{currentTitle}</h1>
<div className="hidden h-[56px] items-center justify-between border-b border-border/20 bg-background/72 px-[24px] backdrop-blur-xl md:flex">
<div className="flex items-center gap-[8px]">
<span className="font-heading text-[13px] font-medium text-foreground/90">Monolith 管理后台</span>
<span className="text-[12px] text-muted-foreground/25">/</span>
<span className="font-heading text-[13px] font-semibold text-foreground/75">{currentTitle}</span>
</div>
<div className="flex items-center gap-[8px]">
<a
Expand Down
37 changes: 23 additions & 14 deletions client/src/components/article-card.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link } from "wouter";
import { Badge } from "@/components/ui/badge";
import type { PostMeta } from "@/lib/api";
import { ArrowRight, Pin } from "lucide-react";
import { ArrowRight, CalendarDays, FolderOpen, Pin } from "lucide-react";

function formatDate(dateStr: string): string {
const date = new Date(dateStr);
Expand All @@ -24,11 +24,11 @@ export function ArticleCard({ post }: { post: PostMeta }) {
href={`/posts/${post.slug}`}
className="group block rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring"
>
<article className="relative overflow-hidden rounded-md border border-border/25 bg-background/25 transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-[2px] hover:border-border/60 hover:bg-card/35 hover:shadow-[0_12px_34px_oklch(0_0_0_/_14%)]">
<div className="flex flex-col sm:flex-row">
<article className="relative overflow-hidden rounded-md border border-border/20 bg-background/30 transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-[2px] hover:border-border/55 hover:bg-card/28">
<div className="grid sm:grid-cols-[168px_minmax(0,1fr)] lg:grid-cols-[196px_minmax(0,1fr)]">
{/* 封面区 */}
<div className="relative shrink-0 overflow-hidden border-b border-border/20 sm:w-[156px] sm:border-b-0 sm:border-r lg:w-[176px]">
<div className="aspect-[16/9] sm:h-full sm:aspect-auto sm:min-h-[132px]">
<div className="relative overflow-hidden border-b border-border/16 bg-foreground/[0.03] sm:border-b-0 sm:border-r">
<div className="aspect-[16/9] sm:h-full sm:aspect-auto sm:min-h-[148px]">
{cover ? (
<img
src={cover}
Expand All @@ -38,8 +38,8 @@ export function ArticleCard({ post }: { post: PostMeta }) {
className="h-full w-full object-cover transition-transform duration-500 ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-[1.04]"
/>
) : (
<div className={`flex h-full w-full items-center justify-center bg-gradient-to-br ${gradient}`}>
<span className="select-none text-[44px] font-semibold tracking-[-0.04em] text-foreground/30 lg:text-[52px]">
<div className={`flex h-full w-full items-center justify-center bg-gradient-to-br ${gradient} grayscale`}>
<span className="select-none font-heading text-[48px] font-semibold tracking-[-0.04em] text-foreground/26 lg:text-[58px]">
{getInitial(post.title)}
</span>
</div>
Expand All @@ -49,7 +49,7 @@ export function ArticleCard({ post }: { post: PostMeta }) {

{/* 内容区 */}
<div className="flex min-w-0 flex-1 flex-col p-[16px] sm:p-[18px] lg:p-[20px]">
<div className="mb-[8px] flex flex-wrap items-center gap-[8px]">
<div className="mb-[10px] flex flex-wrap items-center gap-[8px]">
{post.pinned && (
<Badge variant="outline" className="h-[24px] rounded-[4px] border-amber-500/30 bg-amber-500/10 px-[8px] text-[12px] font-normal tracking-normal text-amber-500/90">
<Pin className="h-[12px] w-[12px]" />
Expand All @@ -59,17 +59,26 @@ export function ArticleCard({ post }: { post: PostMeta }) {
{post.tags.slice(0, 2).map((tag) => (
<Badge key={tag} variant="secondary" className="h-[24px] rounded-[4px] px-[8px] text-[12px] font-normal tracking-normal">{tag}</Badge>
))}
<span className="text-[12px] text-muted-foreground/60">{formatDate(post.createdAt)}</span>
<span className="inline-flex items-center gap-[4px] text-[12px] text-muted-foreground/55">
<CalendarDays className="h-[12px] w-[12px]" />
{formatDate(post.createdAt)}
</span>
</div>
<h2 className="text-[19px] font-semibold tracking-[-0.015em] leading-snug text-foreground transition-colors duration-200 group-hover:text-foreground/90 lg:text-[21px]">
<h2 className="font-heading text-[20px] font-semibold tracking-[-0.018em] leading-snug text-foreground transition-colors duration-200 group-hover:text-foreground/90 lg:text-[23px]">
{post.title}
</h2>
<p className="mt-[8px] text-[14px] leading-[1.7] text-muted-foreground line-clamp-2">
<p className="mt-[10px] text-[14px] leading-[1.75] text-muted-foreground line-clamp-2">
{post.excerpt}
</p>
<div className="mt-auto flex min-h-[32px] items-center gap-[6px] pt-[12px] text-[13px] text-muted-foreground/55 transition-colors duration-200 group-hover:text-foreground">
<span>阅读全文</span>
<ArrowRight className="h-[14px] w-[14px] transition-transform duration-200 group-hover:translate-x-[3px]" />
<div className="mt-auto flex min-h-[36px] items-end justify-between gap-[12px] pt-[14px]">
<span className="inline-flex min-w-0 items-center gap-[4px] text-[12px] text-muted-foreground/35">
<FolderOpen className="h-[12px] w-[12px] shrink-0" />
<span className="truncate">{post.category || "未分类"}</span>
</span>
<span className="inline-flex items-center gap-[6px] text-[13px] text-muted-foreground/55 transition-colors duration-200 group-hover:text-foreground">
阅读全文
<ArrowRight className="h-[14px] w-[14px] transition-transform duration-200 group-hover:translate-x-[3px]" />
</span>
</div>
</div>
</div>
Expand Down
10 changes: 8 additions & 2 deletions client/src/components/cookie-consent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ export function CookieConsent() {
return (
<div className="fixed bottom-0 left-0 right-0 z-[9999] p-[12px] animate-fade-in">
<div className="mx-auto flex max-w-[720px] flex-col items-start gap-[12px] rounded-md border border-border/30 bg-card/95 px-[20px] py-[16px] shadow-lg shadow-black/20 backdrop-blur-md sm:flex-row sm:items-center">
<div className="flex-1 text-[13px] text-muted-foreground/80 leading-[1.6]">
本站使用 Cookie 进行访问统计与第三方脚本加载。继续访问即表示您同意我们的{" "}
<div className="min-w-0 flex-1 text-[13px] leading-[1.6] text-muted-foreground/80">
<span className="hidden sm:inline">
本站使用 Cookie 进行访问统计与第三方脚本加载。继续访问即表示您同意我们的{" "}
</span>
<span className="sm:hidden">
<span className="block">本站使用 Cookie 进行访问统计。</span>
<span className="block">继续访问即表示您同意 </span>
</span>
<a href="/privacy" className="text-foreground/70 underline underline-offset-2 hover:text-foreground transition-colors">隐私政策</a>。
</div>
<div className="flex shrink-0 gap-[8px]">
Expand Down
Loading