Skip to content
0

时间线页面

前言

Firefly 主题移植了一个专业的时间线(Timeline)页面,用于按时间顺序展示您的人生重要事件、工作经历、教育背景和项目成就等。这个页面可以帮助访客了解您的成长历程和专业发展。

  • 本文档详细介绍如何将技能页面功能移植到 Firefly-Hyde 主题中。

  • 在开始之前,请确保你已经按照文件结构创建好对应的目录和文件。


📁 文件结构

src/
├── components/
│   └── features/
│       └── timeline/
│           ├── index.ts               ✨ 新增 - 组件导出
│           ├── types.ts               ✨ 新增 - 类型定义
│           └── TimelineCard.astro       ✨ 新增 - 时间线卡片组件
├── config/
│   └── siteConfig.ts                  ✏️ 修改 - 添加页面开关配置
│   └── navBarConfig.ts               ✏️ 修改 - 导航栏配置
├── constants/
│   └── link-presets.ts               ✏️ 修改 - 添加导航链接
├── data/
│   └── timeline.ts                      ✨ 新增 - 时间线数据管理
├── i18n/
│   ├── i18nKey.ts                    ✏️ 修改 - 添加翻译键
│   └── languages/
│       ├── en.ts                     ✏️ 修改 - 英文翻译
│       ├── zh_CN.ts                  ✏️ 修改 - 中文翻译
│       ├── zh_TW.ts                  ✏️ 修改 - 繁体翻译
│       ├── ja.ts                     ✏️ 修改 - 日文翻译
│       └── ru.ts                     ✏️ 修改 - 俄文翻译
├── pages/
│   └── timeline.astro                   ✨ 新增 - 时间线页面
├── types/
│   └── config.ts                      ✏️ 修改 - 添加类型定义

创建时间线卡片组件

  • 文件路径src/components/features/timeline/index.ts 组件导出

  • 文件路径src/components/features/timeline/types.ts 类型定义

  • 文件路径src/components/features/timeline/TimelineCard.astro 时间线卡片组件

ts
export { default as SkillCard } from "./SkillCard.astro";
export * from "./types";
ts
import type { Skill } from "../../../data/skills";

export interface SkillCardProps {
	skill: Skill;
}
astro
---
import I18nKey from "../../../i18n/i18nKey";
import { i18n } from "../../../i18n/translation";
import Icon from "../../misc/Icon.astro";
import type { SkillCardProps } from "./types";

const { skill } = Astro.props as SkillCardProps;

const skillColor = skill.color || "#3B82F6";

const getLevelText = (level: string) => {
	switch (level) {
		case "expert":
			return i18n(I18nKey.skillsExpert);
		case "advanced":
			return i18n(I18nKey.skillsAdvanced);
		case "intermediate":
			return i18n(I18nKey.skillsIntermediate);
		case "beginner":
			return i18n(I18nKey.skillsBeginner);
		default:
			return level;
	}
};

const getLevelWidth = (level: string) => {
	switch (level) {
		case "expert":
			return "100%";
		case "advanced":
			return "80%";
		case "intermediate":
			return "60%";
		case "beginner":
			return "40%";
		default:
			return "20%";
	}
};

const formatExperience = (exp: { years: number; months: number }) => {
	const parts: string[] = [];
	if (exp.years > 0) {
		parts.push(`${exp.years} ${i18n(I18nKey.skillYears)}`);
	}
	if (exp.months > 0) {
		parts.push(`${exp.months} ${i18n(I18nKey.skillMonths)}`);
	}
	return parts.join(" ");
};
---

<div
	class="skill-card group relative bg-transparent rounded-xl border border-black/10 dark:border-white/10 overflow-hidden transition-all duration-300 hover:shadow-xl hover:-translate-y-1"
	data-category={skill.category}
>
	<div class="p-5">
		<div class="flex items-start gap-4 mb-3">
			<div
				class="w-12 h-12 flex-shrink-0 rounded-lg flex items-center justify-center"
				style={`background-color: ${skillColor}20`}
			>
				<Icon
					icon={skill.icon}
					class="text-xl"
					color={skillColor}
					fallback={skill.name.charAt(0)}
					loading="eager"
				/>
			</div>

			<div class="flex-1 min-w-0">
				<div class="flex items-center justify-between mb-1">
					<h3
						class="text-lg font-bold text-black/90 dark:text-white/90 truncate group-hover:text-[var(--primary)] transition-colors duration-200"
					>
						{skill.name}
					</h3>
					<span
						class="shrink-0 ml-2 px-2 py-0.5 text-xs rounded-md bg-[var(--primary)]/10 text-[var(--primary)] font-medium"
					>
						{getLevelText(skill.level)}
					</span>
				</div>
				<p class="text-xs text-black/50 dark:text-white/50">
					{formatExperience(skill.experience)}
				</p>
			</div>
		</div>

		<p
			class="text-sm text-black/60 dark:text-white/60 line-clamp-2 min-h-[2.5rem]"
		>
			{skill.description}
		</p>

		<div class="mt-3 w-full bg-[var(--btn-regular-bg)] rounded-full h-1.5">
			<div
				class="h-1.5 rounded-full transition-all duration-500"
				style={`width: ${getLevelWidth(skill.level)}; background-color: ${skillColor}`}
			>
			</div>
		</div>
	</div>

	<div
		class="absolute inset-0 bg-gradient-to-br from-[var(--primary)]/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
	>
	</div>
</div>

<style>
	.skill-card {
		animation: fadeInUp 0.5s ease-out forwards;
		opacity: 0;
	}

	.skill-card.filtered-out {
		display: none;
	}

	@keyframes fadeInUp {
		from {
			opacity: 0;
			transform: translateY(20px);
		}
		to {
			opacity: 1;
			transform: translateY(0);
		}
	}

	.skill-card:nth-child(1) {
		animation-delay: 0.03s;
	}
	.skill-card:nth-child(2) {
		animation-delay: 0.06s;
	}
	.skill-card:nth-child(3) {
		animation-delay: 0.09s;
	}
	.skill-card:nth-child(4) {
		animation-delay: 0.12s;
	}
	.skill-card:nth-child(5) {
		animation-delay: 0.15s;
	}
	.skill-card:nth-child(6) {
		animation-delay: 0.18s;
	}
	.skill-card:nth-child(7) {
		animation-delay: 0.21s;
	}
	.skill-card:nth-child(8) {
		animation-delay: 0.24s;
	}
	.skill-card:nth-child(9) {
		animation-delay: 0.27s;
	}
	.skill-card:nth-child(10) {
		animation-delay: 0.3s;
	}
	.skill-card:nth-child(11) {
		animation-delay: 0.33s;
	}
	.skill-card:nth-child(12) {
		animation-delay: 0.36s;
	}

	.line-clamp-2 {
		display: -webkit-box;
		-webkit-line-clamp: 2;
		-webkit-box-orient: vertical;
		overflow: hidden;
	}
</style>

页面开关导航栏配置

  • SiteConfig 类型中添加页面开关配置,文件路径src/config/siteConfig.ts

  • navBarConfig 类型中更新导航栏配置,文件路径src/config/navBarConfig.ts

ts
// 页面开关配置
pages: {
    // ... 其他配置
    // 时间线页面开关
		timeline: true,
},
ts
// 我的及其子菜单
	links.push({
		name: "我的",
		url: "/my/",
		icon: "material-symbols:person",
		children: [
      // 根据配置决定是否添加时间线,在siteConfig关闭pages.timeline时导航栏不显示时间线
			...(siteConfig.pages.timeline ? [LinkPreset.Timeline] : []),
		],
	});

更新类型配置链接

  • types 类型的 config 中添加类型定义,文件路径src/types/config.ts

  • constants 类型的 link-presets 添加到导航链接,文件路径src/constants/link-presets.ts

ts
export type SiteConfig = {
	// 页面开关配置
	pages: {
		timeline?: boolean; // 时间线页面开关
	};
}

export enum LinkPreset {
		Timeline = 11, // ✨ 新增
}
ts
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
	[LinkPreset.Timeline]: {
		name: i18n(I18nKey.timeline),
		url: "/timeline/",
		icon: "material-symbols:timeline",
	},
}

时间线数据管理

文件路径src/data/timeline.ts

ts
import type { TimelineItem } from "../components/features/timeline/types";

export const timelineData: TimelineItem[] = [
	{
		id: "current-study",
		title: "Studying Computer Science and Technology",
		description:
			"Currently studying Computer Science and Technology, focusing on web development and software engineering.",
		type: "education",
		startDate: "2022-09-01",
		location: "Beijing",
		organization: "Beijing Institute of Technology",
		skills: ["Java", "Python", "JavaScript", "HTML/CSS", "MySQL"],
		achievements: [
			"Current GPA: 3.6/4.0",
			"Completed data structures and algorithms course project",
			"Participated in multiple course project developments",
		],
		icon: "material-symbols:school",
		color: "#059669",
		featured: true,
	},
	{
		id: "mizuki-blog-project",
		title: "Mizuki Personal Blog Project",
		description:
			"A personal blog website developed using the Astro framework as a practical project for learning frontend technologies.",
		type: "project",
		startDate: "2024-06-01",
		endDate: "2024-08-01",
		skills: ["Astro", "TypeScript", "Tailwind CSS", "Git"],
		achievements: [
			"Mastered modern frontend development tech stack",
			"Learned responsive design and user experience optimization",
			"Completed the full process from design to deployment",
		],
		links: [
			{
				name: "GitHub Repository",
				url: "https://github.com/example/mizuki-blog",
				type: "project",
			},
			{
				name: "Live Demo",
				url: "https://mizuki-demo.example.com",
				type: "website",
			},
		],
		icon: "material-symbols:code",
		color: "#7C3AED",
		featured: true,
	},
	{
		id: "summer-internship-2024",
		title: "Frontend Development Intern",
		description:
			"Summer internship at an internet company, participating in frontend development of web applications.",
		type: "work",
		startDate: "2024-07-01",
		endDate: "2024-08-31",
		location: "Beijing",
		organization: "TechStart Internet Company",
		position: "Frontend Development Intern",
		skills: ["React", "JavaScript", "CSS3", "Git", "Figma"],
		achievements: [
			"Completed user interface component development",
			"Learned team collaboration and code standards",
			"Received outstanding internship performance certificate",
		],
		icon: "material-symbols:work",
		color: "#DC2626",
		featured: true,
	},
	{
		id: "web-development-course",
		title: "Completed Web Development Online Course",
		description:
			"Completed a full-stack web development online course, systematically learning frontend and backend development technologies.",
		type: "achievement",
		startDate: "2024-01-15",
		endDate: "2024-05-30",
		organization: "Mooc Website",
		skills: ["HTML", "CSS", "JavaScript", "Node.js", "Express"],
		achievements: [
			"Received course completion certificate",
			"Completed 5 practical projects",
			"Mastered full-stack development fundamentals",
		],
		links: [
			{
				name: "Course Certificate",
				url: "https://certificates.example.com/web-dev",
				type: "certificate",
			},
		],
		icon: "material-symbols:verified",
		color: "#059669",
	},
	{
		id: "student-management-system",
		title: "Student Management System Course Project",
		description:
			"Final project for the database course, developed a complete student information management system.",
		type: "project",
		startDate: "2023-11-01",
		endDate: "2023-12-15",
		skills: ["Java", "MySQL", "Swing", "JDBC"],
		achievements: [
			"Received excellent course project grade",
			"Implemented complete CRUD functionality",
			"Learned database design and optimization",
		],
		icon: "material-symbols:database",
		color: "#EA580C",
	},
	{
		id: "programming-contest",
		title: "University Programming Contest",
		description:
			"Participated in a programming contest held by the university, improving algorithm and programming skills.",
		type: "achievement",
		startDate: "2023-10-20",
		location: "Beijing Institute of Technology",
		organization: "School of Computer Science",
		skills: ["C++", "Algorithms", "Data Structures"],
		achievements: [
			"Won third prize in university contest",
			"Improved algorithmic thinking ability",
			"Strengthened programming fundamentals",
		],
		icon: "material-symbols:emoji-events",
		color: "#7C3AED",
	},
	{
		id: "part-time-tutor",
		title: "Part-time Programming Tutor",
		description:
			"Provided programming tutoring for high school students, helping them learn Python basics.",
		type: "work",
		startDate: "2023-09-01",
		endDate: "2024-01-31",
		position: "Programming Tutor",
		skills: ["Python", "Teaching", "Communication"],
		achievements: [
			"Helped 3 students master Python basics",
			"Improved expression and communication skills",
			"Gained teaching experience",
		],
		icon: "material-symbols:school",
		color: "#059669",
	},
	{
		id: "high-school-graduation",
		title: "High School Graduation",
		description:
			"Graduated from high school with excellent grades and was admitted to the Computer Science and Technology program at Beijing Institute of Technology.",
		type: "education",
		startDate: "2019-09-01",
		endDate: "2022-06-30",
		location: "Jinan, Shandong",
		organization: "No.1 High School of Jinan",
		achievements: [
			"College entrance exam score: 620",
			"Received municipal model student award",
			"Won provincial second prize in math competition",
		],
		icon: "material-symbols:school",
		color: "#2563EB",
	},
	{
		id: "first-programming-experience",
		title: "First Programming Experience",
		description:
			"First encountered programming in high school IT class, started learning Python basic syntax.",
		type: "education",
		startDate: "2021-03-01",
		skills: ["Python", "Basic Programming Concepts"],
		achievements: [
			'Completed first "Hello World" program',
			"Learned basic loops and conditional statements",
			"Developed interest in programming",
		],
		icon: "material-symbols:code",
		color: "#7C3AED",
	},
];

添加国际化翻译

  • 添加翻译键,文件路径src/i18n/i18nKey.ts

  • 添加英文翻译,文件路径src/i18n/languages/en.ts

  • 添加中文翻译,文件路径src/i18n/languages/zh_CN.ts

  • 添加繁体中文翻译,文件路径src/i18n/languages/zh_TW.ts

  • 添加日文翻译,文件路径src/i18n/languages/ja.ts

  • 添加俄文翻译,文件路径src/i18n/languages/ru.ts

ts
export enum I18nKey {
	// 时间线页面
	timeline = "timeline",
	timelineSubtitle = "timelineSubtitle",
	timelineEducation = "timelineEducation",
	timelineWork = "timelineWork",
	timelineProject = "timelineProject",
	timelineAchievement = "timelineAchievement",
	timelinePresent = "timelinePresent",
	timelineLocation = "timelineLocation",
	timelineDescription = "timelineDescription",
	timelineMonths = "timelineMonths",
	timelineYears = "timelineYears",
	timelineTotal = "timelineTotal",
	timelineProjects = "timelineProjects",
	timelineExperience = "timelineExperience",
	timelineCurrent = "timelineCurrent",
	timelineHistory = "timelineHistory",
	timelineAchievements = "timelineAchievements",
	timelineStartDate = "timelineStartDate",
	timelineDuration = "timelineDuration",
}
ts
	// Timeline Page
	[Key.timeline]: "Timeline",
	[Key.timelineSubtitle]: "My growth journey and important milestones",
	[Key.timelineEducation]: "Education",
	[Key.timelineWork]: "Work Experience",
	[Key.timelineProject]: "Project Experience",
	[Key.timelineAchievement]: "Achievements",
	[Key.timelinePresent]: "Present",
	[Key.timelineLocation]: "Location",
	[Key.timelineDescription]: "Detailed Description",
	[Key.timelineMonths]: "months",
	[Key.timelineYears]: "years",
	[Key.timelineTotal]: "Total",
	[Key.timelineProjects]: "Projects",
	[Key.timelineExperience]: "Work Experience",
	[Key.timelineCurrent]: "Current Status",
	[Key.timelineHistory]: "History",
	[Key.timelineAchievements]: "Achievements",
	[Key.timelineStartDate]: "Start Date",
	[Key.timelineDuration]: "Duration",
ts
	// 时间线页面
	[Key.timeline]: "时间线",
	[Key.timelineSubtitle]: "我的成长历程和重要里程碑",
	[Key.timelineEducation]: "教育经历",
	[Key.timelineWork]: "工作经历",
	[Key.timelineProject]: "项目经历",
	[Key.timelineAchievement]: "成就荣誉",
	[Key.timelinePresent]: "至今",
	[Key.timelineLocation]: "地点",
	[Key.timelineDescription]: "详细描述",
	[Key.timelineMonths]: "个月",
	[Key.timelineYears]: "年",
	[Key.timelineTotal]: "总计",
	[Key.timelineProjects]: "项目数",
	[Key.timelineExperience]: "工作经验",
	[Key.timelineCurrent]: "当前状态",
	[Key.timelineHistory]: "历史记录",
	[Key.timelineAchievements]: "成就荣誉",
	[Key.timelineStartDate]: "开始日期",
	[Key.timelineDuration]: "持续时间",
ts
	// 時間線頁面
	[Key.timeline]: "時間線",
	[Key.timelineSubtitle]: "我的成長歷程和重要里程碑",
	[Key.timelineEducation]: "教育經歷",
	[Key.timelineWork]: "工作經歷",
	[Key.timelineProject]: "專案經歷",
	[Key.timelineAchievement]: "成就榮譽",
	[Key.timelinePresent]: "至今",
	[Key.timelineLocation]: "地點",
	[Key.timelineDescription]: "詳細描述",
	[Key.timelineMonths]: "個月",
	[Key.timelineYears]: "年",
	[Key.timelineTotal]: "總計",
	[Key.timelineProjects]: "專案數",
	[Key.timelineExperience]: "工作經驗",
	[Key.timelineCurrent]: "當前狀態",
	[Key.timelineHistory]: "歷史記錄",
	[Key.timelineAchievements]: "成就榮譽",
	[Key.timelineStartDate]: "開始日期",
	[Key.timelineDuration]: "持續時間",
ts
	// タイムラインページ
	[Key.timeline]: "タイムライン",
	[Key.timelineSubtitle]: "成長への旅と重要なマイルストーン",
	[Key.timelineEducation]: "教育",
	[Key.timelineWork]: "実務経験",
	[Key.timelineProject]: "プロジェクト経験",
	[Key.timelineAchievement]: "実績",
	[Key.timelinePresent]: "現在",
	[Key.timelineLocation]: "場所",
	[Key.timelineDescription]: "詳細な説明",
	[Key.timelineMonths]: "ヶ月",
	[Key.timelineYears]: "年",
	[Key.timelineTotal]: "合計",
	[Key.timelineProjects]: "プロジェクト",
	[Key.timelineExperience]: "実務経験",
	[Key.timelineCurrent]: "現在のステータス",
	[Key.timelineHistory]: "履歴",
	[Key.timelineAchievements]: "実績",
	[Key.timelineStartDate]: "開始日",
	[Key.timelineDuration]: "期間",
ts
	// Страница временной шкалы
	[Key.timeline]: "Временная шкала",
	[Key.timelineSubtitle]: "Мой путь роста и важные вехи",
	[Key.timelineEducation]: "Образование",
	[Key.timelineWork]: "Опыт работы",
	[Key.timelineProject]: "Проекты",
	[Key.timelineAchievement]: "Достижения",
	[Key.timelinePresent]: "Настоящее время",
	[Key.timelineLocation]: "Местоположение",
	[Key.timelineDescription]: "Подробное описание",
	[Key.timelineMonths]: "месяцев",
	[Key.timelineYears]: "лет",
	[Key.timelineTotal]: "Всего",
	[Key.timelineProjects]: "Количество проектов",
	[Key.timelineExperience]: "Опыт работы",
	[Key.timelineCurrent]: "Текущий статус",
	[Key.timelineHistory]: "История",
	[Key.timelineAchievements]: "Достижения",
	[Key.timelineStartDate]: "Дата начала",
	[Key.timelineDuration]: "Продолжительность",

创建时间线页面

文件路径src/pages/timeline.astro

ts
---
import { FilterTabs } from "@components/atoms/";
import { TimelineCard } from "@components/features/timeline";
import { PageHeader } from "@components/features/page-header";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { Icon } from "astro-icon/components";

import { siteConfig } from "../config";
import { timelineData } from "../data/timeline";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";

if (!siteConfig.pages.timeline) {
	return Astro.redirect("/404/");
}

const types = ["education", "work", "project", "achievement"] as const;

const getTypeIcon = (type: string) => {
	switch (type) {
		case "education":
			return "material-symbols:school";
		case "work":
			return "material-symbols:work";
		case "project":
			return "material-symbols:code";
		case "achievement":
			return "material-symbols:emoji-events";
		default:
			return "material-symbols:apps";
	}
};

const getTypeText = (type: string) => {
	switch (type) {
		case "education":
			return i18n(I18nKey.timelineEducation);
		case "work":
			return i18n(I18nKey.timelineWork);
		case "project":
			return i18n(I18nKey.timelineProject);
		case "achievement":
			return i18n(I18nKey.timelineAchievement);
		default:
			return type;
	}
};

const filterTabs = [
	{
		value: "all",
		label: i18n(I18nKey.friendsFilterAll),
		icon: "material-symbols:apps",
		count: timelineData.length,
	},
	...types.map((type) => ({
		value: type,
		label: getTypeText(type),
		icon: getTypeIcon(type),
		count: timelineData.filter((item) => item.type === type).length,
	})),
];

const title = i18n(I18nKey.timeline);
const subtitle = i18n(I18nKey.timelineSubtitle);
---

<MainGridLayout title={title} description={subtitle}>
	<script>
		// import("../scripts/right-sidebar-layout.js");
		import { loadIconify } from "../utils/icon-loader";

		loadIconify().catch((error) => {
			console.error("Failed to load Iconify:", error);
		});
	</script>

	<script is:inline src="/js/filter-tabs-handler.js"></script>

	<div
		class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-32"
	>
		<div class="card-base z-10 px-6 sm:px-9 py-6 relative w-full">
			<PageHeader title={title} subtitle={subtitle} />

			<div class="mb-8">
				<FilterTabs tabs={filterTabs} dataAttr="type" />
			</div>

			<div id="timeline-list" class="timeline-list">
				{timelineData.map((item) => <TimelineCard item={item} />)}
			</div>

			<div id="no-results" class="hidden text-center py-16">
				<Icon
					name="material-symbols:search-off-rounded"
					class="text-6xl text-black/15 dark:text-white/15 mb-4"
				/>
				<p class="text-black/40 dark:text-white/40 text-lg">
					No matching items
				</p>
			</div>
		</div>
	</div>
</MainGridLayout>

<style>
	.timeline-list {
		position: relative;
	}

	.timeline-list::before {
		content: "";
		position: absolute;
		left: 9px;
		top: 0;
		bottom: 0;
		width: 2px;
		background: var(--line-divider);
		border-radius: 1px;
	}
</style>
最近更新