From d35a97f3258e92789c5fb9b9e2fd42b6bd808e28 Mon Sep 17 00:00:00 2001 From: wxf Date: Fri, 26 Jun 2026 14:08:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=B8=B8=E5=88=86=E4=BA=AB=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/life/page.tsx | 2 +- src/components/home-page.tsx | 10 +- src/components/life-article-page.tsx | 132 ++++++------------ src/components/life-page.tsx | 116 ++++++++------- .../life/articles/niko-major-champion.ts | 31 ++-- src/content/life/content.test-d.ts | 30 ++++ src/content/life/content.ts | 15 ++ src/content/life/types.ts | 22 ++- 8 files changed, 192 insertions(+), 166 deletions(-) create mode 100644 src/content/life/content.test-d.ts create mode 100644 src/content/life/content.ts diff --git a/src/app/life/page.tsx b/src/app/life/page.tsx index ac72ded..aaee167 100644 --- a/src/app/life/page.tsx +++ b/src/app/life/page.tsx @@ -4,7 +4,7 @@ import { lifePosts } from "@/content/life/posts"; export const metadata: Metadata = { title: "日常分享 | fly", - description: "fly 的日常分享,记录生活片段、照片和阶段性的想法。", + description: "fly 的日常分享,记录工具使用、阶段复盘、生活观察和亲自走过的经验。", }; export default function LifeIndexPage() { diff --git a/src/components/home-page.tsx b/src/components/home-page.tsx index d2ba221..f0a1689 100644 --- a/src/components/home-page.tsx +++ b/src/components/home-page.tsx @@ -16,7 +16,7 @@ import { const navItems = [ { label: "技术文章", href: "#articles" }, - { label: "生活分享", href: "#life" }, + { label: "日常分享", href: "#life" }, { label: "联系我", href: "mailto:3505891631@qq.com" }, { label: "Gitea", href: "http://124.221.147.173:3000/fly/fly-personal-site" }, ]; @@ -31,10 +31,10 @@ const portals = [ gradient: "from-cyan-300 via-blue-500 to-violet-500", }, { - title: "生活分享", + title: "日常分享", href: "#life", eyebrow: "Life stream", - description: "技术之外的日常片段,呈现更完整的个人侧面。", + description: "记录工具使用、阶段复盘、生活观察和那些亲自走过的经验。", icon: LifeBuoy, gradient: "from-lime-300 via-emerald-400 to-cyan-400", }, @@ -298,10 +298,10 @@ export function HomePage() { life channel - 生活分享 + 日常分享 - 技术之外的日常切面。阅读、音乐、照片、旅行和一些阶段性的想法,让这个网站不只是一份技术档案。 + 记录真实经历里的小路标:工具使用、阶段复盘、生活观察,以及一些值得回头看的想法。 diff --git a/src/components/life-article-page.tsx b/src/components/life-article-page.tsx index 2b04dc1..319489a 100644 --- a/src/components/life-article-page.tsx +++ b/src/components/life-article-page.tsx @@ -1,9 +1,10 @@ "use client"; import Link from "next/link"; +import Image from "next/image"; import { motion, useReducedMotion } from "motion/react"; -import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, ChevronLeft, ChevronRight } from "lucide-react"; -import { useState } from "react"; +import { ArrowLeft, Calendar, Clock, Sparkles } from "lucide-react"; +import { countLifePostParagraphs } from "@/content/life/content"; import type { LifePost } from "@/content/life/types"; import { BackToTopButton } from "@/components/back-to-top-button"; @@ -18,7 +19,7 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro 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 paragraphCount = countLifePostParagraphs(post); const container = { hidden: {}, @@ -40,18 +41,6 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro 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 (
{/* 背景装饰 */} @@ -111,7 +100,7 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro - 约 {Math.ceil(post.paragraphs.length * 0.8)} 分钟阅读 + 约 {Math.ceil(paragraphCount * 0.8)} 分钟阅读 @@ -124,85 +113,42 @@ export function LifeArticlePage({ post, prevPost, nextPost }: LifeArticlePagePro

- {/* 图片画廊 */} - {post.images && post.images.length > 0 && ( - -
- {/* 主图片 */} -
- {post.images[currentImageIndex]?.alt} - {/* 图片渐变遮罩 */} -
- - {/* 图片切换按钮 */} - {post.images.length > 1 && ( - <> - - - - )} - - {/* 图片计数 */} - {post.images.length > 1 && ( -
- {currentImageIndex + 1} / {post.images.length} -
- )} -
- - {/* 图片说明 */} -
-

- {post.images[currentImageIndex]?.caption} + + {post.content.map((block, index) => { + if (block.type === "paragraph") { + return ( +

+ {block.text}

- {post.images.length > 1 && ( -
- {post.images.map((_, index) => ( -
- )} -
-
- - )} + ); + } - {/* 正文内容 */} - - {post.paragraphs.map((paragraph, index) => ( -

- {paragraph} -

- ))} + return ( + +
+ {block.alt} +
+ {block.caption ? ( +
+ {block.caption} +
+ ) : null} +
+ ); + })}
{/* 文章结尾装饰 */} diff --git a/src/components/life-page.tsx b/src/components/life-page.tsx index 92bffdc..e76ba44 100644 --- a/src/components/life-page.tsx +++ b/src/components/life-page.tsx @@ -1,8 +1,10 @@ "use client"; import Link from "next/link"; +import NextImage from "next/image"; import { motion, useReducedMotion } from "motion/react"; -import { ArrowLeft, ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image, Coffee, Music, Camera, Plane, Brain } from "lucide-react"; +import { ArrowUpRight, Calendar, Clock, Sparkles, BookOpen, Image as ImageIcon, Coffee, Music, Camera, Plane, Brain } from "lucide-react"; +import { getLifePostCoverImage, getLifePostImages } from "@/content/life/content"; import type { LifePost } from "@/content/life/types"; const topics = [ @@ -45,6 +47,8 @@ export function LifePage({ posts }: LifePageProps) { }; const latestPost = posts[0]; + const latestPostImages = latestPost ? getLifePostImages(latestPost) : []; + const latestPostCoverImage = latestPost ? getLifePostCoverImage(latestPost) : undefined; return (
@@ -118,7 +122,7 @@ export function LifePage({ posts }: LifePageProps) { - 生活分享 + 日常分享 @@ -145,7 +149,7 @@ export function LifePage({ posts }: LifePageProps) { 日常分享 - 技术之外的日常片段,记录生活中的想法、照片和阶段性的思考。 + 记录真实经历里的小路标:工具使用、阶段复盘、生活观察,以及一些值得回头看的想法。 {/* 主题标签 */} @@ -222,7 +226,11 @@ export function LifePage({ posts }: LifePageProps) { {/* 悬停时的光晕效果 */}
-
+
@@ -230,8 +238,8 @@ export function LifePage({ posts }: LifePageProps) { {latestPost.date} - - {latestPost.images?.length || 0} 张图片 + + {latestPostImages.length} 张图片

@@ -249,21 +257,23 @@ export function LifePage({ posts }: LifePageProps) {

{/* 图片预览区域 */} - {latestPost.images?.[0] && ( + {latestPostCoverImage && (
-
- {latestPost.images[0].alt} + {/* 图片渐变遮罩 */}
{/* 图片装饰角标 */}
- - {latestPost.images.length}P + + {latestPostImages.length}P
)} @@ -295,48 +305,52 @@ export function LifePage({ posts }: LifePageProps) {
- {posts.map((post, index) => ( - - - {/* 左侧装饰条 */} -
+ {posts.map((post, index) => { + const postImages = getLifePostImages(post); - {/* 序号 */} - - {String(index + 1).padStart(2, "0")} - + return ( + + + {/* 左侧装饰条 */} +
- {/* 日期和图片数 */} -
- - - - {post.images?.length || 0} 张 + {/* 序号 */} + + {String(index + 1).padStart(2, "0")} -
- {/* 标题和描述 */} -
-

- {post.title} -

-

- {post.description} -

-
+ {/* 日期和图片数 */} +
+ + + + {postImages.length} 张 + +
- {/* 箭头 */} - - - - - - ))} + {/* 标题和描述 */} +
+

+ {post.title} +

+

+ {post.description} +

+
+ + {/* 箭头 */} + + + + + + ); + })}
{/* 底部装饰 */} diff --git a/src/content/life/articles/niko-major-champion.ts b/src/content/life/articles/niko-major-champion.ts index e807360..e469361 100644 --- a/src/content/life/articles/niko-major-champion.ts +++ b/src/content/life/articles/niko-major-champion.ts @@ -5,27 +5,38 @@ export const nikoMajorChampion = { date: "2026.06.22", title: "NiKo 终于等到了这个冠军", description: "关于 NiKo 与 Falcons 捧起科隆 Major 冠军的一点观赛感想。", - images: [ + content: [ { + type: "paragraph", + text: "看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。", + }, + { + type: "image", src: "/life/niko-major-champion/falcons-trophy.jpg", alt: "Falcons 队员在舞台上举起冠军奖杯", - caption: "Falcons 举起冠军奖杯,舞台上的金色纸屑和火花刚好把这个瞬间托住。", }, { + type: "paragraph", + text: "CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。", + }, + { + type: "image", src: "/life/niko-major-champion/niko-emotion.jpg", alt: "NiKo 站在观众席前情绪激动", - caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。", + // caption: "NiKo 站在观众面前,像是终于从漫长的遗憾里走了出来。", }, { + type: "paragraph", + text: "所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。", + }, + { + type: "image", src: "/life/niko-major-champion/niko-celebration.jpg", alt: "NiKo 在比赛席上握拳庆祝", - caption: "比赛席上的握拳庆祝,比任何解释都更直接。", + }, + { + type: "paragraph", + text: "我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。", }, ], - paragraphs: [ - "看到 NiKo 跟 Falcons 捧起科隆 Major 冠军的时候,第一反应不是“他终于证明自己了”,而是“这个故事总算没有一直卡在遗憾那里”。", - "CS 圈里很少有人会怀疑 NiKo 的个人能力。那些年的精准定位、干拉、残局处理,还有他身上那种很固执的强者气质,早就把他放进了历史级步枪手的讨论里。可越是这样,Major 冠军这个缺口就越刺眼。天赋被看见了,巅峰被看见了,遗憾也被反复看见了。", - "所以这次夺冠最打动我的地方,不只是奖杯本身,而是一个选手在漫长职业生涯里终于把叙事拧回来了。第 17 次 Major,29 岁,换过队友,换过体系,也经历过足够多次“差一点”。这种冠军不是爽文式的轻松圆梦,更像是把很多年没说完的话,终于稳稳地放在了桌面上。", - "我挺喜欢这样的体育瞬间:它提醒人,很多东西不是只属于最年轻、最锋利、最顺风的时候。只要还在场上,还愿意承担,还能把自己交给比赛,故事就没有真正写完。", - ], } satisfies LifePost; diff --git a/src/content/life/content.test-d.ts b/src/content/life/content.test-d.ts new file mode 100644 index 0000000..387a695 --- /dev/null +++ b/src/content/life/content.test-d.ts @@ -0,0 +1,30 @@ +import { nikoMajorChampion } from "./articles/niko-major-champion"; +import { countLifePostParagraphs, getLifePostImages } from "./content"; +import type { LifeContentBlock, LifePostImage } from "./types"; + +const inlineContent: LifeContentBlock[] = [ + { type: "paragraph", text: "first paragraph" }, + { + type: "image", + src: "/life/demo/step.jpg", + alt: "Demo step", + caption: "A demo image inside the article.", + }, + { type: "paragraph", text: "second paragraph" }, +]; + +const images: LifePostImage[] = getLifePostImages(nikoMajorChampion); +const paragraphCount: number = countLifePostParagraphs(nikoMajorChampion); +const inlineImages: LifePostImage[] = getLifePostImages({ + ...nikoMajorChampion, + content: inlineContent, +}); +const inlineParagraphCount: number = countLifePostParagraphs({ + ...nikoMajorChampion, + content: inlineContent, +}); + +void images; +void paragraphCount; +void inlineImages; +void inlineParagraphCount; diff --git a/src/content/life/content.ts b/src/content/life/content.ts new file mode 100644 index 0000000..a0eb5b8 --- /dev/null +++ b/src/content/life/content.ts @@ -0,0 +1,15 @@ +import type { LifePost, LifePostImage } from "./types"; + +export function getLifePostImages(post: LifePost): LifePostImage[] { + return post.content + .filter((block) => block.type === "image") + .map(({ src, alt, caption }) => ({ src, alt, caption })); +} + +export function getLifePostCoverImage(post: LifePost): LifePostImage | undefined { + return getLifePostImages(post)[0]; +} + +export function countLifePostParagraphs(post: LifePost): number { + return post.content.filter((block) => block.type === "paragraph").length; +} diff --git a/src/content/life/types.ts b/src/content/life/types.ts index 19b702c..24588a0 100644 --- a/src/content/life/types.ts +++ b/src/content/life/types.ts @@ -1,12 +1,22 @@ +export type LifePostImage = { + src: string; + alt: string; + caption?: string; +}; + +export type LifeContentBlock = + | { + type: "paragraph"; + text: string; + } + | ({ + type: "image"; + } & LifePostImage); + export type LifePost = { slug: string; date: string; title: string; description: string; - images: { - src: string; - alt: string; - caption: string; - }[]; - paragraphs: string[]; + content: LifeContentBlock[]; };