@@ -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[0].src})
+
{/* 图片渐变遮罩 */}
{/* 图片装饰角标 */}
-
- {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[];
};