feat: 优化 /life 页面样式和图片显示
- 优化 /life 列表页视觉设计,增加主题标签、背景装饰、卡片动效 - 修复列表页精选文章图片显示 - 优化文章详情页图片画廊,支持切换和计数显示 - 保持整体风格一致性(米白背景 + 深灰文字 + 绿色强调)
This commit is contained in:
@@ -2,6 +2,9 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "export",
|
output: "export",
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 588 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 387 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { LifeArticlePage } from "@/components/life-article-page";
|
||||||
|
import { lifePosts, getLifePost } from "@/content/life/posts";
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<{
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return lifePosts.map((post) => ({ slug: post.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getLifePost(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return {
|
||||||
|
title: "文章未找到 | fly",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${post.title} | fly 日常分享`,
|
||||||
|
description: post.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function LifeArticlePageRoute({ params }: PageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const post = getLifePost(slug);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentIndex = lifePosts.findIndex((p) => p.slug === slug);
|
||||||
|
const prevPost = currentIndex > 0 ? lifePosts[currentIndex - 1] : undefined;
|
||||||
|
const nextPost = currentIndex < lifePosts.length - 1 ? lifePosts[currentIndex + 1] : undefined;
|
||||||
|
|
||||||
|
return <LifeArticlePage post={post} prevPost={prevPost} nextPost={nextPost} />;
|
||||||
|
}
|
||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
import { PlaceholderPage } from "@/components/placeholder-page";
|
import type { Metadata } from "next";
|
||||||
|
import { LifePage } from "@/components/life-page";
|
||||||
|
import { lifePosts } from "@/content/life/posts";
|
||||||
|
|
||||||
export default function LifePage() {
|
export const metadata: Metadata = {
|
||||||
return (
|
title: "日常分享 | fly",
|
||||||
<PlaceholderPage
|
description: "fly 的日常分享,记录生活片段、照片和阶段性的想法。",
|
||||||
eyebrow="Life journal"
|
};
|
||||||
title="生活分享"
|
|
||||||
description="技术之外的个人切面:日常、阅读、音乐、照片、旅行,以及阶段性的想法。"
|
export default function LifeIndexPage() {
|
||||||
items={["日常记录", "阅读和音乐", "照片和旅行", "一些想法"]}
|
return <LifePage posts={lifePosts} />;
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
|
import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { LifePost } from "@/content/life/types";
|
||||||
|
import { BackToTopButton } from "@/components/back-to-top-button";
|
||||||
|
|
||||||
|
type LifeArticlePageProps = {
|
||||||
|
post: LifePost;
|
||||||
|
prevPost?: LifePost;
|
||||||
|
nextPost?: LifePost;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePageProps) {
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
const distance = reduceMotion ? 0 : 24;
|
||||||
|
const duration = reduceMotion ? 0 : 0.55;
|
||||||
|
const easeOut = [0.22, 1, 0.36, 1] as const;
|
||||||
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
hidden: {},
|
||||||
|
show: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: reduceMotion ? 0 : 0.08,
|
||||||
|
delayChildren: reduceMotion ? 0 : 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const reveal = {
|
||||||
|
hidden: { opacity: 0, y: distance, filter: reduceMotion ? "none" : "blur(8px)" },
|
||||||
|
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration, ease: easeOut } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const scaleReveal = {
|
||||||
|
hidden: { opacity: 0, y: distance, scale: reduceMotion ? 1 : 0.97 },
|
||||||
|
show: { opacity: 1, y: 0, scale: 1, transition: { duration, ease: easeOut } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextImage = () => {
|
||||||
|
if (post.images.length > 0) {
|
||||||
|
setCurrentImageIndex((prev) => (prev + 1) % post.images.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevImage = () => {
|
||||||
|
if (post.images.length > 0) {
|
||||||
|
setCurrentImageIndex((prev) => (prev - 1 + post.images.length) % post.images.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
||||||
|
{/* 背景装饰 */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 -z-10">
|
||||||
|
<div
|
||||||
|
className="absolute -top-20 left-1/2 h-96 w-[80%] -translate-x-1/2 rounded-full opacity-25 blur-3xl"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, rgba(95, 111, 82, 0.35) 0%, rgba(95, 111, 82, 0.1) 40%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto flex min-h-dvh w-full max-w-4xl flex-col px-5 py-5 sm:px-8 lg:px-10">
|
||||||
|
{/* 顶部导航 */}
|
||||||
|
<motion.header
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
variants={container}
|
||||||
|
className="relative z-10 flex items-center justify-between rounded-full border border-[#151515]/10 bg-white/60 px-4 py-3 shadow-lg shadow-[#5f6f52]/5 backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<motion.div variants={scaleReveal}>
|
||||||
|
<Link
|
||||||
|
href="/life"
|
||||||
|
className="group inline-flex h-11 items-center gap-2 rounded-full border border-[#151515]/10 bg-white/70 px-4 text-sm font-medium text-[#151515] outline-none transition hover:border-[#5f6f52]/30 hover:bg-[#5f6f52]/5 hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 transition group-hover:-translate-x-0.5" />
|
||||||
|
返回列表
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={reveal}>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="group inline-flex h-11 items-center gap-2 rounded-full bg-[#151515] px-4 text-sm font-black text-white outline-none transition hover:bg-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4 transition group-hover:rotate-12" />
|
||||||
|
fly
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
{/* 文章内容 */}
|
||||||
|
<article className="flex-1 py-12 sm:py-16">
|
||||||
|
<motion.div
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
variants={container}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
{/* 文章头部 */}
|
||||||
|
<motion.header variants={reveal} className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#5f6f52]/10 px-3 py-1 font-mono text-xs font-medium text-[#5f6f52]">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
{post.date}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full border border-[#151515]/8 bg-white/50 px-3 py-1 font-mono text-xs text-[#151515]/50">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
约 {Math.ceil(post.paragraphs.length * 0.8)} 分钟阅读
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="font-display text-4xl font-semibold leading-tight tracking-tight text-[#151515] sm:text-5xl lg:text-6xl">
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-lg leading-8 text-[#151515]/60">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
{/* 图片画廊 */}
|
||||||
|
{post.images && post.images.length > 0 && (
|
||||||
|
<motion.div variants={scaleReveal}>
|
||||||
|
<div className="relative overflow-hidden rounded-[1.5rem] border border-[#151515]/8 bg-white shadow-xl shadow-[#5f6f52]/8">
|
||||||
|
{/* 主图片 */}
|
||||||
|
<div className="relative aspect-[16/9] w-full overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={post.images[currentImageIndex]?.src}
|
||||||
|
alt={post.images[currentImageIndex]?.alt}
|
||||||
|
className="h-full w-full object-cover transition-opacity duration-300"
|
||||||
|
key={currentImageIndex}
|
||||||
|
/>
|
||||||
|
{/* 图片渐变遮罩 */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent" />
|
||||||
|
|
||||||
|
{/* 图片切换按钮 */}
|
||||||
|
{post.images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={prevImage}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/80 text-[#151515]/70 shadow-lg backdrop-blur transition hover:bg-white hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
aria-label="上一张图片"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextImage}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/80 text-[#151515]/70 shadow-lg backdrop-blur transition hover:bg-white hover:text-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
aria-label="下一张图片"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图片计数 */}
|
||||||
|
{post.images.length > 1 && (
|
||||||
|
<div className="absolute right-4 top-4 inline-flex items-center gap-1 rounded-full bg-black/40 px-2.5 py-1 font-mono text-xs text-white backdrop-blur">
|
||||||
|
{currentImageIndex + 1} / {post.images.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片说明 */}
|
||||||
|
<div className="p-5 sm:p-6">
|
||||||
|
<p className="text-sm leading-6 text-[#151515]/55">
|
||||||
|
{post.images[currentImageIndex]?.caption}
|
||||||
|
</p>
|
||||||
|
{post.images.length > 1 && (
|
||||||
|
<div className="mt-4 flex items-center justify-center gap-2">
|
||||||
|
{post.images.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setCurrentImageIndex(index)}
|
||||||
|
className={`h-2 rounded-full transition-all ${
|
||||||
|
index === currentImageIndex
|
||||||
|
? "w-6 bg-[#5f6f52]"
|
||||||
|
: "w-2 bg-[#151515]/20 hover:bg-[#151515]/30"
|
||||||
|
}`}
|
||||||
|
aria-label={`查看第 ${index + 1} 张图片`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 正文内容 */}
|
||||||
|
<motion.div variants={reveal} className="space-y-6 pt-4">
|
||||||
|
{post.paragraphs.map((paragraph, index) => (
|
||||||
|
<p
|
||||||
|
key={index}
|
||||||
|
className="text-base leading-8 text-[#151515]/75 sm:text-lg sm:leading-9"
|
||||||
|
>
|
||||||
|
{paragraph}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* 文章结尾装饰 */}
|
||||||
|
<motion.div variants={reveal} className="pt-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[#5f6f52]/30 to-transparent" />
|
||||||
|
<span className="font-mono text-xs uppercase text-[#5f6f52]/60">end</span>
|
||||||
|
<div className="h-px flex-1 bg-gradient-to-r from-transparent via-[#5f6f52]/30 to-transparent" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{/* 上一篇/下一篇导航 */}
|
||||||
|
<motion.section
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, margin: "-80px" }}
|
||||||
|
variants={container}
|
||||||
|
className="border-t border-[#151515]/8 py-10"
|
||||||
|
>
|
||||||
|
<motion.p
|
||||||
|
variants={reveal}
|
||||||
|
className="mb-6 font-mono text-xs uppercase tracking-normal text-[#5f6f52]"
|
||||||
|
>
|
||||||
|
继续阅读
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
{prevPost ? (
|
||||||
|
<motion.div variants={reveal}>
|
||||||
|
<Link
|
||||||
|
href={`/life/${prevPost.slug}`}
|
||||||
|
className="group block rounded-2xl border border-[#151515]/8 bg-white/60 p-5 transition hover:border-[#5f6f52]/25 hover:bg-white hover:shadow-lg hover:shadow-[#5f6f52]/8 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
<p className="mb-2 font-mono text-xs uppercase text-[#151515]/38">
|
||||||
|
← 上一篇
|
||||||
|
</p>
|
||||||
|
<h3 className="text-lg font-medium text-[#151515] transition group-hover:text-[#5f6f52]">
|
||||||
|
{prevPost.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 line-clamp-1 text-sm text-[#151515]/50">
|
||||||
|
{prevPost.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{nextPost ? (
|
||||||
|
<motion.div variants={reveal}>
|
||||||
|
<Link
|
||||||
|
href={`/life/${nextPost.slug}`}
|
||||||
|
className="group block rounded-2xl border border-[#151515]/8 bg-white/60 p-5 text-right transition hover:border-[#5f6f52]/25 hover:bg-white hover:shadow-lg hover:shadow-[#5f6f52]/8 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
<p className="mb-2 font-mono text-xs uppercase text-[#151515]/38">
|
||||||
|
下一篇 →
|
||||||
|
</p>
|
||||||
|
<h3 className="text-lg font-medium text-[#151515] transition group-hover:text-[#5f6f52]">
|
||||||
|
{nextPost.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 line-clamp-1 text-sm text-[#151515]/50">
|
||||||
|
{nextPost.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
{/* 页脚 */}
|
||||||
|
<motion.footer
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: reduceMotion ? 0 : 0.6, delay: reduceMotion ? 0 : 0.5 }}
|
||||||
|
className="flex items-center justify-between border-t border-[#151515]/8 py-6"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs uppercase tracking-normal text-[#151515]/32">
|
||||||
|
fly / life
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs text-[#151515]/32">
|
||||||
|
made with ♥
|
||||||
|
</span>
|
||||||
|
</motion.footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BackToTopButton />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion, useReducedMotion } from "motion/react";
|
||||||
|
import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image, Coffee, Music, Camera, Plane, Brain } from "lucide-react";
|
||||||
|
import type { LifePost } from "@/content/life/types";
|
||||||
|
|
||||||
|
const topics = [
|
||||||
|
{ name: "Daily", icon: Coffee },
|
||||||
|
{ name: "Reading", icon: BookOpen },
|
||||||
|
{ name: "Music", icon: Music },
|
||||||
|
{ name: "Photo", icon: Camera },
|
||||||
|
{ name: "Travel", icon: Plane },
|
||||||
|
{ name: "Thoughts", icon: Brain },
|
||||||
|
];
|
||||||
|
|
||||||
|
type LifePageProps = {
|
||||||
|
posts: readonly LifePost[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LifePage({ posts }: LifePageProps) {
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
const distance = reduceMotion ? 0 : 24;
|
||||||
|
const duration = reduceMotion ? 0 : 0.55;
|
||||||
|
const easeOut = [0.22, 1, 0.36, 1] as const;
|
||||||
|
|
||||||
|
const container = {
|
||||||
|
hidden: {},
|
||||||
|
show: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: reduceMotion ? 0 : 0.08,
|
||||||
|
delayChildren: reduceMotion ? 0 : 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const reveal = {
|
||||||
|
hidden: { opacity: 0, y: distance, filter: reduceMotion ? "none" : "blur(8px)" },
|
||||||
|
show: { opacity: 1, y: 0, filter: "blur(0px)", transition: { duration, ease: easeOut } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const scaleReveal = {
|
||||||
|
hidden: { opacity: 0, y: distance, scale: reduceMotion ? 1 : 0.97 },
|
||||||
|
show: { opacity: 1, y: 0, scale: 1, transition: { duration, ease: easeOut } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const latestPost = posts[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-dvh overflow-x-clip bg-[#f4f5f1] text-[#151515]">
|
||||||
|
{/* 背景装饰 */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 -z-10">
|
||||||
|
{/* 顶部绿色光晕 */}
|
||||||
|
<div
|
||||||
|
className="absolute -top-32 left-1/2 h-[28rem] w-[90%] -translate-x-1/2 rounded-full opacity-40 blur-3xl"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(ellipse at center, rgba(95, 111, 82, 0.25) 0%, rgba(95, 111, 82, 0.08) 45%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 右下角暖绿色光晕 */}
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-20 -right-20 h-96 w-96 rounded-full opacity-30 blur-3xl"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, rgba(163, 177, 138, 0.35) 0%, rgba(163, 177, 138, 0.1) 50%, transparent 70%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 左下角深绿色光晕 */}
|
||||||
|
<div
|
||||||
|
className="absolute bottom-1/4 -left-32 h-72 w-72 rounded-full opacity-20 blur-3xl"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"radial-gradient(circle, rgba(95, 111, 82, 0.3) 0%, transparent 65%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 细微纹理网格 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.015]"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"linear-gradient(rgba(95, 111, 82, 0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(95, 111, 82, 0.5) 1px, transparent 1px)",
|
||||||
|
backgroundSize: "60px 60px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto flex min-h-dvh w-full max-w-6xl flex-col px-5 py-5 sm:px-8 lg:px-10">
|
||||||
|
{/* 顶部导航 */}
|
||||||
|
<motion.header
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
variants={container}
|
||||||
|
className="relative z-10 flex items-center justify-between rounded-full border border-[#151515]/8 bg-white/55 px-4 py-2.5 shadow-lg shadow-[#5f6f52]/6 backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<motion.div variants={scaleReveal}>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="group inline-flex h-10 items-center gap-2 rounded-full bg-[#151515] px-4 text-sm font-black text-white outline-none transition hover:bg-[#5f6f52] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-3.5 w-3.5 transition group-hover:rotate-12" />
|
||||||
|
fly
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={reveal} className="hidden items-center gap-0.5 md:flex">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="rounded-full px-4 py-2 text-sm font-medium text-[#151515]/55 outline-none transition hover:bg-[#151515]/5 hover:text-[#151515] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/30"
|
||||||
|
>
|
||||||
|
首页
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/notes/frontend"
|
||||||
|
className="rounded-full px-4 py-2 text-sm font-medium text-[#151515]/55 outline-none transition hover:bg-[#151515]/5 hover:text-[#151515] focus-visible:ring-2 focus-visible:ring-[#5f6f52]/30"
|
||||||
|
>
|
||||||
|
技术笔记
|
||||||
|
</Link>
|
||||||
|
<span className="relative rounded-full px-4 py-2 text-sm font-semibold text-[#5f6f52]">
|
||||||
|
<span className="absolute inset-0 rounded-full bg-[#5f6f52]/10" />
|
||||||
|
<span className="relative">生活分享</span>
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
{/* Hero 区域 */}
|
||||||
|
<motion.section
|
||||||
|
initial="hidden"
|
||||||
|
animate="show"
|
||||||
|
variants={container}
|
||||||
|
className="grid gap-10 py-14 sm:py-20 lg:grid-cols-[1fr_16rem] lg:items-end"
|
||||||
|
>
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<motion.p
|
||||||
|
variants={reveal}
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border border-[#5f6f52]/20 bg-[#5f6f52]/8 px-4 py-2 font-mono text-xs uppercase tracking-normal text-[#5f6f52]"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
|
daily notes / life stream
|
||||||
|
</motion.p>
|
||||||
|
<motion.h1
|
||||||
|
variants={reveal}
|
||||||
|
className="mt-6 font-display text-6xl font-medium leading-none tracking-tight text-[#151515] sm:text-7xl lg:text-[8.5rem]"
|
||||||
|
>
|
||||||
|
日常分享
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p variants={reveal} className="mt-6 max-w-xl text-lg leading-8 text-[#151515]/58">
|
||||||
|
技术之外的日常片段,记录生活中的想法、照片和阶段性的思考。
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* 主题标签 */}
|
||||||
|
<motion.div variants={reveal} className="mt-8 flex flex-wrap gap-2">
|
||||||
|
{topics.map((topic) => {
|
||||||
|
const Icon = topic.icon;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={topic.name}
|
||||||
|
className="group inline-flex items-center gap-1.5 rounded-full border border-[#151515]/8 bg-white/50 px-3 py-1.5 font-mono text-xs text-[#151515]/45 backdrop-blur transition hover:border-[#5f6f52]/25 hover:bg-[#5f6f52]/8 hover:text-[#5f6f52]"
|
||||||
|
>
|
||||||
|
<Icon className="h-3 w-3" />
|
||||||
|
{topic.name}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={scaleReveal}
|
||||||
|
className="grid grid-cols-2 gap-4 rounded-3xl border border-[#151515]/8 bg-white/70 p-5 shadow-xl shadow-[#5f6f52]/8 backdrop-blur-xl"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-mono text-[0.68rem] uppercase tracking-normal text-[#151515]/38">
|
||||||
|
文章总数
|
||||||
|
</p>
|
||||||
|
<p className="font-display text-4xl font-medium leading-none text-[#151515]">
|
||||||
|
{posts.length.toString().padStart(2, "0")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-mono text-[0.68rem] uppercase tracking-normal text-[#151515]/38">
|
||||||
|
最新更新
|
||||||
|
</p>
|
||||||
|
<p className="font-mono text-sm font-medium text-[#5f6f52]">{latestPost?.date}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 flex items-center gap-2 border-t border-[#151515]/8 pt-4">
|
||||||
|
<Calendar className="h-4 w-4 text-[#5f6f52]" />
|
||||||
|
<span className="text-sm text-[#151515]/50">不定期更新,记录生活点滴</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.section>
|
||||||
|
|
||||||
|
{/* 精选文章(最新一篇) */}
|
||||||
|
{latestPost && (
|
||||||
|
<motion.section
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, margin: "-80px" }}
|
||||||
|
variants={container}
|
||||||
|
className="pb-12"
|
||||||
|
>
|
||||||
|
<motion.p
|
||||||
|
variants={reveal}
|
||||||
|
className="mb-5 flex items-center gap-2 font-mono text-xs uppercase tracking-normal text-[#5f6f52]"
|
||||||
|
>
|
||||||
|
<span className="h-1 w-1 rounded-full bg-[#5f6f52]" />
|
||||||
|
featured / 最新文章
|
||||||
|
</motion.p>
|
||||||
|
<motion.div variants={scaleReveal}>
|
||||||
|
<Link
|
||||||
|
href={`/life/${latestPost.slug}`}
|
||||||
|
className="group relative block overflow-hidden rounded-[2rem] border border-[#151515]/8 bg-white/75 shadow-2xl shadow-[#5f6f52]/10 backdrop-blur-xl transition hover:-translate-y-1.5 hover:border-[#5f6f52]/25 hover:shadow-[#5f6f52]/18 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40"
|
||||||
|
>
|
||||||
|
{/* 渐变装饰 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-0 transition duration-500 group-hover:opacity-100"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(95, 111, 82, 0.05) 0%, rgba(163, 177, 138, 0.03) 50%, rgba(95, 111, 82, 0.04) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 悬停时的光晕效果 */}
|
||||||
|
<div className="absolute -top-1/2 -right-1/2 h-full w-full rounded-full bg-[#5f6f52]/0 blur-3xl transition duration-700 group-hover:bg-[#5f6f52]/8" />
|
||||||
|
|
||||||
|
<div className="relative grid gap-8 p-6 sm:p-8 lg:grid-cols-[1fr_20rem] lg:items-center lg:p-10">
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#5f6f52]/12 px-3 py-1 font-mono text-xs font-medium text-[#5f6f52]">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{latestPost.date}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1 font-mono text-xs uppercase text-[#151515]/30">
|
||||||
|
<Image className="h-3 w-3" />
|
||||||
|
{latestPost.images?.length || 0} 张图片
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="font-display text-3xl font-semibold leading-tight text-[#151515] transition duration-300 group-hover:text-[#5f6f52] sm:text-4xl">
|
||||||
|
{latestPost.title}
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-xl text-base leading-7 text-[#151515]/55">
|
||||||
|
{latestPost.description}
|
||||||
|
</p>
|
||||||
|
<div className="pt-2">
|
||||||
|
<span className="group/btn inline-flex items-center gap-2 text-sm font-semibold text-[#5f6f52]">
|
||||||
|
阅读全文
|
||||||
|
<ArrowUpRight className="h-4 w-4 transition duration-300 group-hover/btn:-translate-y-0.5 group-hover/btn:translate-x-0.5" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 图片预览区域 */}
|
||||||
|
{latestPost.images?.[0] && (
|
||||||
|
<div className="relative overflow-hidden rounded-2xl border border-[#151515]/8 bg-[#f4f5f1]">
|
||||||
|
<div className="aspect-[4/3] w-full">
|
||||||
|
<img
|
||||||
|
src={latestPost.images[0].src}
|
||||||
|
alt={latestPost.images[0].alt}
|
||||||
|
className="h-full w-full object-cover transition duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
{/* 图片渐变遮罩 */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-[#5f6f52]/10 via-transparent to-transparent" />
|
||||||
|
</div>
|
||||||
|
{/* 图片装饰角标 */}
|
||||||
|
<div className="absolute left-3 top-3 inline-flex items-center gap-1 rounded-full bg-white/80 px-2 py-1 font-mono text-[0.65rem] font-medium text-[#5f6f52] backdrop-blur">
|
||||||
|
<Image className="h-3 w-3" />
|
||||||
|
{latestPost.images.length}P
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
</motion.section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文章列表 */}
|
||||||
|
<motion.section
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, margin: "-60px" }}
|
||||||
|
variants={container}
|
||||||
|
className="flex-1 py-8 sm:py-10"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={reveal}
|
||||||
|
className="mb-6 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<p className="flex items-center gap-2 font-mono text-xs uppercase tracking-normal text-[#5f6f52]">
|
||||||
|
<span className="h-1 w-1 rounded-full bg-[#5f6f52]" />
|
||||||
|
all posts / 全部文章
|
||||||
|
</p>
|
||||||
|
<span className="font-mono text-xs text-[#151515]/35">
|
||||||
|
{posts.length} 篇
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{posts.map((post, index) => (
|
||||||
|
<motion.div key={post.slug} variants={reveal}>
|
||||||
|
<Link
|
||||||
|
href={`/life/${post.slug}`}
|
||||||
|
className="group relative flex items-center gap-4 overflow-hidden rounded-2xl border border-[#151515]/8 bg-white/45 p-5 transition-all duration-300 hover:border-[#5f6f52]/20 hover:bg-white/80 hover:shadow-lg hover:shadow-[#5f6f52]/6 focus-visible:ring-2 focus-visible:ring-[#5f6f52]/40 sm:gap-6 sm:p-6"
|
||||||
|
>
|
||||||
|
{/* 左侧装饰条 */}
|
||||||
|
<div className="absolute left-0 top-0 h-full w-1 bg-[#5f6f52]/0 transition-all duration-300 group-hover:bg-[#5f6f52]/40" />
|
||||||
|
|
||||||
|
{/* 序号 */}
|
||||||
|
<span className="hidden font-mono text-sm font-medium text-[#151515]/20 sm:block">
|
||||||
|
{String(index + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* 日期和图片数 */}
|
||||||
|
<div className="flex shrink-0 flex-col gap-1.5">
|
||||||
|
<time className="font-mono text-xs uppercase tracking-normal text-[#151515]/40">
|
||||||
|
{post.date}
|
||||||
|
</time>
|
||||||
|
<span className="hidden items-center gap-1 font-mono text-[0.65rem] text-[#151515]/25 sm:inline-flex">
|
||||||
|
<Image className="h-3 w-3" />
|
||||||
|
{post.images?.length || 0} 张
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 标题和描述 */}
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="truncate text-xl font-medium text-[#151515] transition-colors duration-300 group-hover:text-[#5f6f52] sm:text-2xl">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 truncate text-sm text-[#151515]/48">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 箭头 */}
|
||||||
|
<span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-[#151515]/8 text-[#151515]/30 transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:border-[#5f6f52]/30 group-hover:bg-[#5f6f52]/8 group-hover:text-[#5f6f52]">
|
||||||
|
<ArrowUpRight className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部装饰 */}
|
||||||
|
<motion.div
|
||||||
|
variants={reveal}
|
||||||
|
className="mt-auto flex items-center justify-between border-t border-[#151515]/8 pt-8"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs uppercase tracking-normal text-[#151515]/25">
|
||||||
|
fly / life
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs text-[#151515]/25">
|
||||||
|
made with ♥
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { LifePost } from "../types";
|
||||||
|
|
||||||
|
export const nikoMajorChampion = {
|
||||||
|
slug: "niko-major-champion",
|
||||||
|
date: "2026.06.22",
|
||||||
|
title: "NiKo 终于等到了这个冠军",
|
||||||
|
description: "关于 NiKo 与 Falcons 捧起科隆 Major 冠军的一点观赛感想。",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
src: "/life/niko-major-champion/falcons-trophy.jpg",
|
||||||
|
alt: "Falcons 队员在舞台上举起冠军奖杯",
|
||||||
|
caption: "Falcons 举起冠军奖杯,舞台上的金色纸屑和火花刚好把这个瞬间托住。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/life/niko-major-champion/niko-emotion.jpg",
|
||||||
|
alt: "NiKo 站在观众席前情绪激动",
|
||||||
|
caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "/life/niko-major-champion/niko-celebration.jpg",
|
||||||
|
alt: "NiKo 在比赛席上握拳庆祝",
|
||||||
|
caption: "比赛席上的握拳庆祝,比任何解释都更直接。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
paragraphs: [
|
||||||
|
"看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。",
|
||||||
|
"CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。",
|
||||||
|
"所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。",
|
||||||
|
"我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。",
|
||||||
|
],
|
||||||
|
} satisfies LifePost;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { nikoMajorChampion } from "./articles/niko-major-champion";
|
||||||
|
import type { LifePost } from "./types";
|
||||||
|
|
||||||
|
export const lifePosts = [nikoMajorChampion] as const satisfies readonly LifePost[];
|
||||||
|
|
||||||
|
export function getLifePost(slug: string) {
|
||||||
|
return lifePosts.find((post) => post.slug === slug);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type LifePost = {
|
||||||
|
slug: string;
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
images: {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
caption: string;
|
||||||
|
}[];
|
||||||
|
paragraphs: string[];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user