惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
O
OpenAI News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
Webroot Blog
Webroot Blog
GbyAI
GbyAI
S
SegmentFault 最新的问题
Cyberwarzone
Cyberwarzone
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
J
Java Code Geeks
Google DeepMind News
Google DeepMind News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 【当耐特】
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
AWS News Blog
AWS News Blog
Engineering at Meta
Engineering at Meta
S
Security Affairs
H
Help Net Security
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
云风的 BLOG
云风的 BLOG
Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
Spread Privacy
Spread Privacy
T
Threatpost
Forbes - Security
Forbes - Security
C
Cisco Blogs
Scott Helme
Scott Helme
Attack and Defense Labs
Attack and Defense Labs
Simon Willison's Weblog
Simon Willison's Weblog
腾讯CDC
The Last Watchdog
The Last Watchdog
Cloudbric
Cloudbric
Last Week in AI
Last Week in AI
Recorded Future
Recorded Future
小众软件
小众软件
V
Vulnerabilities – Threatpost
美团技术团队
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
Hacker News - Newest:
Hacker News - Newest: "LLM"
I
Intezer
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园 - 司徒正美
C
Cybersecurity and Infrastructure Security Agency CISA
Martin Fowler
Martin Fowler
博客园 - 聂微东

Hyde Blog

Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog 关于我 关于我 Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog Hyde Blog
Hyde Blog
Hyde · 2026-05-19 · via Hyde Blog

项目页面 ​

前言 ​

Firefly 主题移植了一个专业的项目展示(Projects)页面,用于展示您参与或开发的项目作品。这个页面可以帮助访客了解您的技术能力和项目经验。

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

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


📁 文件结构 ​

public/
├── js/
│   └── filter-tabs-handler.js   ✨ 新增筛选标签处理器
src/
├── components/
│   ├── atoms/
│   │   └── FilterTabs.astro          ✨ 新增 - 筛选标签组件
│   │   └── index.ts                  ✨ 新增 - 组件导出
│   └── features/
│       └── page-header/
│           ├── index.ts               ✨ 新增 - 组件导出
│           ├── types.ts               ✨ 新增 - 类型定义
│           └── PageHeader.astro       ✨ 新增 - 页面头部组件
│       └── projects/
│           ├── index.ts               ✨ 新增 - 组件导出
│           ├── types.ts               ✨ 新增 - 类型定义
│           └── ProjectCard.astro       ✨ 新增 - 项目卡片组件
├── config/
│   └── siteConfig.ts                  ✏️ 修改 - 添加页面开关配置
│   └── navBarConfig.ts               ✏️ 修改 - 导航栏配置
├── constants/
│   └── link-presets.ts               ✏️ 修改 - 添加导航链接
├── data/
│   └── projects.ts                      ✨ 新增 - 项目数据管理
├── i18n/
│   ├── i18nKey.ts                    ✏️ 修改 - 添加翻译键
│   └── languages/
│       ├── en.ts                     ✏️ 修改 - 英文翻译
│       ├── zh_CN.ts                  ✏️ 修改 - 中文翻译
│       ├── zh_TW.ts                  ✏️ 修改 - 繁体翻译
│       ├── ja.ts                     ✏️ 修改 - 日文翻译
│       └── ru.ts                     ✏️ 修改 - 俄文翻译
├── pages/
│   └── projects.astro                   ✨ 新增 - 项目页面
├── types/
│   └── config.ts                      ✏️ 修改 - 添加类型定义

创建选标签处理器 ​

文件路径public/js/filter-tabs-handler.js

js

// Shared filter handler for FilterTabs atom component
// Works with Swup page transitions
// FilterTabs renders data-filter-attr and data-filter-value on each button
// Cards/entries should have a matching data attribute (e.g. data-category, data-type)

(function () {
	function initFilterTabs(reset) {
		var containers = document.querySelectorAll(".filter-tabs");

		containers.forEach(function (container) {
			if (!reset && container.dataset.initialized) return;
			container.dataset.initialized = "true";

			var tabs = container.querySelectorAll(".filter-tabs-item");
			var filterAttr = tabs[0] ? tabs[0].dataset.filterAttr : null;
			if (!filterAttr) return;

			var dataSelector = "[data-" + filterAttr + "]";
			var parent = container.closest(".card-base") || document;
			var items = parent.querySelectorAll(dataSelector);
			var noResults = parent.querySelector("#no-results");

			if (items.length === 0) return;

			tabs.forEach(function (tab) {
				tab.addEventListener("click", function () {
					tabs.forEach(function (t) {
						t.classList.remove("active");
					});
					tab.classList.add("active");

					var activeValue = tab.dataset.filterValue || "all";
					var visibleCount = 0;

					items.forEach(function (item) {
						var itemValue = item.dataset[filterAttr];
						var match =
							activeValue === "all" || (itemValue && itemValue.split(",").indexOf(activeValue) !== -1);

						if (match) {
							item.classList.remove("filtered-out");
							visibleCount++;
						} else {
							item.classList.add("filtered-out");
						}
					});

					if (noResults) {
						noResults.classList.toggle("hidden", visibleCount > 0);
					}
				});
			});
		});
	}

	// Expose for dynamic tab rebuild (e.g. Memos API fetch)
	window.__initFilterTabs = function () {
		initFilterTabs(true);
	};

	function onInit() {
		if (document.querySelector(".filter-tabs")) {
			initFilterTabs(false);
		}
	}

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", onInit);
	} else {
		onInit();
	}

	document.addEventListener("astro:page-load", onInit);
})();

创建筛选标签组件 ​

文件路径src/components/atoms/FilterTabs.astrosrc/components/atoms/index.ts

FilterTabs.astroindex.ts

astro

---
import { Icon } from "astro-icon/components";

interface FilterTab {
	value: string;
	label: string;
	icon?: string;
	count?: number;
}

interface Props {
	tabs: FilterTab[];
	dataAttr: string;
	activeValue?: string;
	class?: string;
}

const {
	tabs,
	dataAttr,
	activeValue = "all",
	class: className = "",
} = Astro.props;
---

<div class:list={["filter-tabs", className]}>
	{
		tabs.map((tab) => (
			<button
				class:list={[
					"filter-tabs-item",
					{ active: tab.value === activeValue },
				]}
				data-filter-value={tab.value}
				data-filter-attr={dataAttr}
			>
				{tab.icon && <Icon name={tab.icon} class="text-base w-4 h-4" />}
				<span>{tab.label}</span>
				{tab.count !== undefined && (
					<span class="filter-tabs-count">({tab.count})</span>
				)}
			</button>
		))
	}
</div>

<style>
	.filter-tabs {
		display: flex;
		flex-wrap: wrap;
		gap: 0.5rem;
	}

	.filter-tabs-item {
		display: inline-flex;
		align-items: center;
		gap: 0.375rem;
		padding: 0.5rem 1rem;
		border: 1px solid var(--line-divider);
		border-radius: var(--radius-large);
		background: var(--btn-regular-bg);
		color: var(--btn-content);
		font-size: 0.875rem;
		font-weight: 500;
		cursor: pointer;
		transition: all 0.2s ease;
		white-space: nowrap;
	}

	.filter-tabs-item iconify-icon {
		flex-shrink: 0;
		opacity: 0.7;
		transition: opacity 0.2s ease;
	}

	.filter-tabs-item:hover:not(.active) {
		background: var(--btn-regular-bg-hover);
		border-color: var(--primary);
	}

	.filter-tabs-item:hover:not(.active) iconify-icon {
		opacity: 1;
	}

	.filter-tabs-item.active {
		background: var(--primary);
		color: white;
		border-color: var(--primary);
	}

	.filter-tabs-item.active iconify-icon {
		opacity: 1;
	}

	.filter-tabs-count {
		opacity: 0.6;
		font-size: 0.8rem;
	}

	@media (max-width: 768px) {
		.filter-tabs {
			gap: 0.375rem;
		}

		.filter-tabs-item {
			padding: 0.4rem 0.8rem;
			font-size: 0.8rem;
		}

		.filter-tabs-count {
			display: none;
		}
	}
</style>

ts

export { default as FilterTabs } from "./FilterTabs.astro";

创建页面头部组件 ​

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

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

  • 文件路径src/components/features/diary/PageHeader.astro 页面头部组件

index.tstypes.tsPageHeader.astro

ts

export { default as PageHeader } from "./PageHeader.astro";
export * from "./types";

ts

export interface PageHeaderProps {
	title: string;
	subtitle?: string;
	class?: string;
}

astro

---
import type { PageHeaderProps } from "./types";

const {
	title,
	subtitle,
	class: className = "",
} = Astro.props as PageHeaderProps;
---

<div class={`flex flex-col items-start justify-center mb-8 ${className}`}>
	<h1
		class="text-4xl font-bold text-black/90 dark:text-white/90 mb-2 relative
			before:w-1 before:h-8 before:rounded-md before:bg-[var(--primary)]
			before:absolute before:top-1/2 before:-translate-y-1/2 before:-left-4"
	>
		{title}
	</h1>
	{
		subtitle && (
			<p class="text-lg text-black/60 dark:text-white/60">{subtitle}</p>
		)
	}
</div>

创建项目卡片组件 ​

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

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

  • 文件路径src/components/features/diary/ProjectCard.astro 项目卡片组件

index.tstypes.tsProjectCard.astro

ts

export { default as ProjectCard } from "./ProjectCard.astro";
export * from "./types";

ts

export interface Project {
	id: string;
	title: string;
	description: string;
	image?: string;
	category: string;
	techStack: string[];
	status: "completed" | "in-progress" | "planned";
	demoUrl?: string;
	sourceUrl?: string;
	liveDemo?: string;
	sourceCode?: string;
	visitUrl?: string;
	startDate: string;
	endDate?: string;
	featured?: boolean;
	tags?: string[];
	showImage?: boolean;
}

export interface ProjectCardProps {
	project: Project;
	size?: "small" | "medium" | "large";
	showImage?: boolean;
	maxTechStack?: number;
}

ts

---
import { Icon } from "astro-icon/components";

import I18nKey from "../../../i18n/i18nKey";
import { i18n } from "../../../i18n/translation";
import type { ProjectCardProps } from "./types";

const { project, maxTechStack = 4 } = Astro.props as ProjectCardProps;

const hasImage = !!project.image;
const showImageArea = project.showImage !== false && hasImage;
const hasVisitUrl = !!project.visitUrl;
const hasSourceCode = !!project.sourceCode;

const getStatusText = (status: string) => {
	switch (status) {
		case "completed":
			return i18n(I18nKey.projectsCompleted);
		case "in-progress":
			return i18n(I18nKey.projectsInProgress);
		case "planned":
			return i18n(I18nKey.projectsPlanned);
		default:
			return status;
	}
};
---

<div
	class="project-card group relative 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={project.category}
>
	{
		showImageArea && (
			<div class="aspect-video overflow-hidden relative bg-gradient-to-br from-[var(--primary)]/5 to-[var(--primary)]/10">
				{hasImage ? (
					<img
						src={project.image!}
						alt={project.title}
						class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
						loading="lazy"
					/>
				) : (
					<div class="w-full h-full flex items-center justify-center">
						<span class="text-4xl font-bold text-[var(--primary)]/15 select-none tracking-wide">
							{project.title}
						</span>
					</div>
				)}
				<div class="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
				{project.featured && (
					<div class="absolute top-3 right-3">
						<Icon
							name="material-symbols:star-rounded"
							class="text-[var(--primary)] text-xl drop-shadow-sm"
							style="filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));"
						/>
					</div>
				)}
			</div>
		)
	}

	<div class="p-5">
		<div class="flex items-center justify-between mb-3">
			<h3
				class="text-lg font-bold text-black/90 dark:text-white/90 truncate group-hover:text-[var(--primary)] transition-colors duration-200"
			>
				{project.title}
			</h3>
			<span
				class="shrink-0 ml-3 px-2 py-0.5 text-xs rounded-md bg-[var(--primary)]/10 text-[var(--primary)] font-medium"
			>
				{getStatusText(project.status)}
			</span>
		</div>

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

		{
			project.techStack && project.techStack.length > 0 && (
				<div class="flex flex-wrap gap-2 mb-4">
					{project.techStack.slice(0, maxTechStack).map((tech) => (
						<span class="px-2 py-1 text-xs rounded-md bg-[var(--primary)]/10 text-[var(--primary)] font-medium">
							{tech}
						</span>
					))}
					{project.techStack.length > maxTechStack && (
						<span class="px-2 py-1 text-xs rounded-md bg-[var(--btn-regular-bg)] text-black/50 dark:text-white/50 font-medium">
							+{project.techStack.length - maxTechStack}
						</span>
					)}
				</div>
			)
		}

		{
			(hasVisitUrl || hasSourceCode) && (
				<div class="flex gap-2">
					{hasVisitUrl && (
						<a
							href={project.visitUrl!}
							target="_blank"
							rel="noopener noreferrer"
							class="btn-regular flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium"
						>
							<Icon
								name="material-symbols:open-in-new"
								class="text-base w-4 h-4"
							/>
							{i18n(I18nKey.projectsVisit)}
						</a>
					)}
					{hasSourceCode && (
						<a
							href={project.sourceCode!}
							target="_blank"
							rel="noopener noreferrer"
							class="btn-regular flex items-center justify-center gap-2 px-4 py-2 rounded-lg text-sm font-medium"
							style={hasVisitUrl ? "" : "flex: 1;"}
						>
							<Icon name="mdi:github" class="text-base w-4 h-4" />
							{!hasVisitUrl && "GitHub"}
						</a>
					)}
				</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>
	.project-card {
		animation: fadeInUp 0.5s ease-out forwards;
		opacity: 0;
	}

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

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

	.project-card:nth-child(1) {
		animation-delay: 0.05s;
	}
	.project-card:nth-child(2) {
		animation-delay: 0.1s;
	}
	.project-card:nth-child(3) {
		animation-delay: 0.15s;
	}
	.project-card:nth-child(4) {
		animation-delay: 0.2s;
	}
	.project-card:nth-child(5) {
		animation-delay: 0.25s;
	}
	.project-card:nth-child(6) {
		animation-delay: 0.3s;
	}
	.project-card:nth-child(7) {
		animation-delay: 0.35s;
	}
	.project-card:nth-child(8) {
		animation-delay: 0.4s;
	}

	.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

siteConfig.tsnavBarConfig.ts

ts

// 页面开关配置
pages: {
    // ... 其他配置
    // 项目页面开关
	projects: true,
},

ts

// 我的及其子菜单
	links.push({
		name: "我的",
		url: "/my/",
		icon: "material-symbols:person",
		children: [
			// 根据配置决定是否添加项目,在siteConfig关闭pages.projects时导航栏不显示项目
			...(siteConfig.pages.projects ? [LinkPreset.Projects] : []),
		],
	});

更新类型配置链接 ​

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

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

config.tslink-presets.ts

ts

export type SiteConfig = {
	// 页面开关配置
	pages: {
		projects?: boolean; // 项目页面开关
	};
}

export enum LinkPreset {
	Projects = 10, // ✨ 新增
}

ts

export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
	[LinkPreset.Projects]: {
		name: i18n(I18nKey.projects),
		url: "/projects/",
		icon: "material-symbols:work",
	},
}

项目数据管理 ​

文件路径src/data/projects.ts

ts

// Project data configuration file
// Used to manage data for the project display page

export interface Project {
	id: string;
	title: string;
	description: string;
	image: string;
	category: "web" | "mobile" | "desktop" | "other";
	techStack: string[];
	status: "completed" | "in-progress" | "planned";
	liveDemo?: string;
	sourceCode?: string;
	visitUrl?: string;
	startDate: string;
	endDate?: string;
	featured?: boolean;
	tags?: string[];
	showImage?: boolean;
}

export const projectsData: Project[] = [
	{
		id: "Firefly", //(必填) 项目的唯一标识符,通常是字符串格式的名称。用于内部引用和过滤。
		title: "Firefly", //(必填) 项目的名称,显示在项目列表中。
		// 项目的详细描述,可以多行文本。
		description:
			"Firefly 是一款基于 Astro 框架和 Fuwari 模板开发的清新美观且现代化个人博客主题模板,专为技术爱好者和内容创作者设计。该主题融合了现代 Web 技术栈,提供了丰富的功能模块和高度可定制的界面,让您能够轻松打造出专业且美观的个人博客网站。",
		// (必填) 项目展示图片的路径,通常放在 public/images/projects/ 目录下。
		image: "https://docs-firefly.cuteleaf.cn/images/1.webp",
		//"web" | "mobile" | "desktop" | "other": (必填) 项目的类型分类,用于筛选。
		category: "web",
		// (必填) 项目使用的技术栈数组,如 ["React", "Node.js"]。
		techStack: ["Astro", "TypeScript", "Tailwind CSS", "Svelte"],
		// "completed" | "in-progress" | "planned": (必填) 项目的当前状态,用于筛选。
		status: "completed",
		//  (可选) 项目源代码仓库的URL地址,通常是GitHub链接。
		sourceCode: "https://github.com/CuteLeaf/Firefly",
		//  (可选) 项目访问链接,可以是演示链接或项目主页。
		visitUrl: "https://firefly.cuteleaf.cn/",
		// (必填) 项目开始日期,格式为 "YYYY-MM-DD"。
		startDate: "2025-10-111",
		// (可选) 项目结束日期,格式为 "YYYY-MM-DD"。对于进行中的项目,此字段可省略。
		endDate: "2024-06-01",
		// (可选) 是否为特色项目,特色项目会优先展示。
		featured: true,
		// (可选) 项目标签数组,用于更细致的分类。
		tags: ["Blog", "Theme", "Open Source"],
	},
	{
		id: "vue-pure-admin",
		title: "vue-pure-admin",
		description:
			"一款开源免费且开箱即用的中后台管理系统模块版本。完全采用ECMAScript模块(ESM)规范来编写和组织代码,使用了最新的Vue3、 Vite、Element-Plus、TypeScript、Pinia、Tailwindcss等主流技术开发",
		image: "https://camo.githubusercontent.com/56acae4e405b42111d2ba2b56a85c3d07a93fe0d2e4865960da81df74386af3c/68747470733a2f2f7869616f7869616e3532312e6769746875622e696f2f68797065726c696e6b2f696d672f7675652d707572652d61646d696e2f312e6a7067",
		category: "mobile",
		techStack: ["Vue3", "Vite", "Element-Plus++", "TypeScript","Pinia","Tailwindcss"],
		status: "in-progress",
		sourceCode: "https://github.com/pure-admin/vue-pure-admin",
		visitUrl: "https://pure-admin.github.io/vue-pure-admin/#/login",
		startDate: "2024-03-01",
		featured: true,
		tags: ["Android", "Root", "Kernel"],
	},
	{
		id: "file-transfer-go",
		title: "文件快传 - P2P文件传输工具",
		description:
			"Go/React开发的端到端webrtc的文件传输/文字传输/桌面共享,安全,隐私,数据不经过服务器。",
		image: "https://raw.githubusercontent.com/MatrixSeven/file-transfer-go/refs/heads/main/img.png",
		category: "desktop",
		techStack: ["Go", "React "],
		status: "completed",
		sourceCode: "https://github.com/MatrixSeven/file-transfer-go",
		visitUrl: "https://transfer.52python.cn/",
		startDate: "2026-02-01",
		endDate: "2026-02-28",
		tags: ["Android", "Tool", "Desktop"],
		showImage: true,
	},
];

// Get project statistics
export const getProjectStats = () => {
	const total = projectsData.length;
	const completed = projectsData.filter(
		(p) => p.status === "completed",
	).length;
	const inProgress = projectsData.filter(
		(p) => p.status === "in-progress",
	).length;
	const planned = projectsData.filter((p) => p.status === "planned").length;

	return {
		total,
		byStatus: {
			completed,
			inProgress,
			planned,
		},
	};
};

// Get projects by category
export const getProjectsByCategory = (category?: string) => {
	if (!category || category === "all") {
		return projectsData;
	}
	return projectsData.filter((p) => p.category === category);
};

// Get featured projects
export const getFeaturedProjects = () => {
	return projectsData.filter((p) => p.featured);
};

// Get all tech stacks
export const getAllTechStack = () => {
	const techSet = new Set<string>();
	projectsData.forEach((project) => {
		project.techStack.forEach((tech) => {
			techSet.add(tech);
		});
	});
	return Array.from(techSet).sort();
};

添加国际化翻译 ​

  • 添加翻译键,文件路径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

i18nKey.tsen.tszh_CN.tszh_TW.tsja.tsru.ts

ts

export enum I18nKey {
    // 项目展示页面
	projects = "projects",
	projectsSubtitle = "projectsSubtitle",
	projectsAll = "projectsAll",
	projectsWeb = "projectsWeb",
	projectsMobile = "projectsMobile",
	projectsDesktop = "projectsDesktop",
	projectsOther = "projectsOther",
	projectTechStack = "projectTechStack",
	projectLiveDemo = "projectLiveDemo",
	projectSourceCode = "projectSourceCode",
	projectDescription = "projectDescription",
	projectStatus = "projectStatus",
	projectStatusCompleted = "projectStatusCompleted",
	projectStatusInProgress = "projectStatusInProgress",
	projectStatusPlanned = "projectStatusPlanned",
	projectsTotal = "projectsTotal",
	projectsCompleted = "projectsCompleted",
	projectsInProgress = "projectsInProgress",
	projectsTechStack = "projectsTechStack",
	projectsFeatured = "projectsFeatured",
	projectsPlanned = "projectsPlanned",
	projectsDemo = "projectsDemo",
	projectsSource = "projectsSource",
	projectsVisit = "projectsVisit",
	projectsGitHub = "projectsGitHub",
}

ts

	// Projects Page
	[Key.projects]: "Projects",
	[Key.projectsSubtitle]: "My development project portfolio",
	[Key.projectsAll]: "All",
	[Key.projectsWeb]: "Web Applications",
	[Key.projectsMobile]: "Mobile Applications",
	[Key.projectsDesktop]: "Desktop Applications",
	[Key.projectsOther]: "Other",
	[Key.projectTechStack]: "Tech Stack",
	[Key.projectLiveDemo]: "Live Demo",
	[Key.projectSourceCode]: "Source Code",
	[Key.projectDescription]: "Project Description",
	[Key.projectStatus]: "Status",
	[Key.projectStatusCompleted]: "Completed",
	[Key.projectStatusInProgress]: "In Progress",
	[Key.projectStatusPlanned]: "Planned",
	[Key.projectsTotal]: "Total Projects",
	[Key.projectsCompleted]: "Completed",
	[Key.projectsInProgress]: "In Progress",
	[Key.projectsTechStack]: "Tech Stack Statistics",
	[Key.projectsFeatured]: "Featured Projects",
	[Key.projectsPlanned]: "Planned",
	[Key.projectsDemo]: "Live Demo",
	[Key.projectsSource]: "Source Code",
	[Key.projectsVisit]: "Visit Project",
	[Key.projectsGitHub]: "GitHub",

ts

	// 项目展示页面
	[Key.projects]: "项目展示",
	[Key.projectsSubtitle]: "我的开发项目作品集",
	[Key.projectsAll]: "全部",
	[Key.projectsWeb]: "网页应用",
	[Key.projectsMobile]: "移动应用",
	[Key.projectsDesktop]: "桌面应用",
	[Key.projectsOther]: "其他",
	[Key.projectTechStack]: "技术栈",
	[Key.projectLiveDemo]: "在线演示",
	[Key.projectSourceCode]: "源代码",
	[Key.projectDescription]: "项目描述",
	[Key.projectStatus]: "项目状态",
	[Key.projectStatusCompleted]: "已完成",
	[Key.projectStatusInProgress]: "进行中",
	[Key.projectStatusPlanned]: "计划中",
	[Key.projectsTotal]: "项目总数",
	[Key.projectsCompleted]: "已完成",
	[Key.projectsInProgress]: "进行中",
	[Key.projectsTechStack]: "技术栈统计",
	[Key.projectsFeatured]: "精选项目",
	[Key.projectsPlanned]: "计划中",
	[Key.projectsDemo]: "在线演示",
	[Key.projectsSource]: "源代码",
	[Key.projectsVisit]: "前往",
	[Key.projectsGitHub]: "GitHub",

ts

	// 專案展示頁面
	[Key.projects]: "專案展示",
	[Key.projectsSubtitle]: "我的開發專案作品集",
	[Key.projectsAll]: "全部",
	[Key.projectsWeb]: "網頁應用",
	[Key.projectsMobile]: "移動應用",
	[Key.projectsDesktop]: "桌面應用",
	[Key.projectsOther]: "其他",
	[Key.projectTechStack]: "技術堆疊",
	[Key.projectLiveDemo]: "線上展示",
	[Key.projectSourceCode]: "原始碼",
	[Key.projectDescription]: "專案描述",
	[Key.projectStatus]: "專案狀態",
	[Key.projectStatusCompleted]: "已完成",
	[Key.projectStatusInProgress]: "進行中",
	[Key.projectStatusPlanned]: "計劃中",
	[Key.projectsTotal]: "專案總數",
	[Key.projectsCompleted]: "已完成",
	[Key.projectsInProgress]: "進行中",
	[Key.projectsTechStack]: "技術堆疊統計",
	[Key.projectsFeatured]: "精選專案",
	[Key.projectsPlanned]: "計劃中",
	[Key.projectsDemo]: "線上展示",
	[Key.projectsSource]: "原始碼",
	[Key.projectsVisit]: "前往專案",
	[Key.projectsGitHub]: "GitHub",

ts

	// プロジェクトページ
	[Key.projects]: "プロジェクト",
	[Key.projectsSubtitle]: "開発プロジェクトのポートフォリオ",
	[Key.projectsAll]: "すべて",
	[Key.projectsWeb]: "ウェブアプリ",
	[Key.projectsMobile]: "モバイルアプリ",
	[Key.projectsDesktop]: "デスクトップアプリ",
	[Key.projectsOther]: "その他",
	[Key.projectTechStack]: "技術スタック",
	[Key.projectLiveDemo]: "ライブデモ",
	[Key.projectSourceCode]: "ソースコード",
	[Key.projectDescription]: "プロジェクトの説明",
	[Key.projectStatus]: "ステータス",
	[Key.projectStatusCompleted]: "完了",
	[Key.projectStatusInProgress]: "進行中",
	[Key.projectStatusPlanned]: "計画中",
	[Key.projectsTotal]: "プロジェクトの合計",
	[Key.projectsCompleted]: "完了",
	[Key.projectsInProgress]: "進行中",
	[Key.projectsTechStack]: "技術スタック",
	[Key.projectsFeatured]: "注目のプロジェクト",
	[Key.projectsPlanned]: "計画中",
	[Key.projectsDemo]: "ライブデモ",
	[Key.projectsSource]: "ソースコード",
	[Key.projectsVisit]: "プロジェクトを開く",
	[Key.projectsGitHub]: "GitHub",

ts

	// Страница проектов
	[Key.projects]: "Проекты",
	[Key.projectsSubtitle]: "Мой портфель проектов",
	[Key.projectsAll]: "Все",
	[Key.projectsWeb]: "Веб-приложения",
	[Key.projectsMobile]: "Мобильные приложения",
	[Key.projectsDesktop]: "Десктопные приложения",
	[Key.projectsOther]: "Другое",
	[Key.projectTechStack]: "Технологический стек",
	[Key.projectLiveDemo]: "Онлайн демо",
	[Key.projectSourceCode]: "Исходный код",
	[Key.projectDescription]: "Описание проекта",
	[Key.projectStatus]: "Статус проекта",
	[Key.projectStatusCompleted]: "Завершено",
	[Key.projectStatusInProgress]: "В процессе",
	[Key.projectStatusPlanned]: "Запланировано",
	[Key.projectsTotal]: "Всего проектов",
	[Key.projectsCompleted]: "Завершено",
	[Key.projectsInProgress]: "В процессе",
	[Key.projectsTechStack]: "Статистика технологий",
	[Key.projectsFeatured]: "Избранные проекты",
	[Key.projectsPlanned]: "Запланировано",
	[Key.projectsDemo]: "Онлайн демо",
	[Key.projectsSource]: "Исходный код",
	[Key.projectsVisit]: "Посетить",
	[Key.projectsGitHub]: "GitHub",

新增项目页面组件 ​

文件路径src/pages/projects.astro

astro

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

import { siteConfig } from "../config";
import { UNCATEGORIZED } from "../constants/constants";
import { projectsData } from "../data/projects";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";

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

const categories = [
	...new Set(projectsData.map((project) => project.category)),
];

const getCategoryText = (category: string) => {
	switch (category) {
		case "web":
			return i18n(I18nKey.projectsWeb);
		case "mobile":
			return i18n(I18nKey.projectsMobile);
		case "desktop":
			return i18n(I18nKey.projectsDesktop);
		case "other":
			return i18n(I18nKey.projectsOther);
		case UNCATEGORIZED:
			return i18n(I18nKey.uncategorized);
		default:
			return category;
	}
};

const getCategoryIcon = (category: string) => {
	switch (category) {
		case "web":
			return "material-symbols:language";
		case "mobile":
			return "material-symbols:smartphone";
		case "desktop":
			return "material-symbols:desktop-windows";
		case "other":
			return "material-symbols:widgets";
		default:
			return "material-symbols:folder";
	}
};

const filterTabs = [
	{
		value: "all",
		label: i18n(I18nKey.friendsFilterAll),
		icon: "material-symbols:apps",
		count: projectsData.length,
	},
	...categories.map((category) => ({
		value: category,
		label: getCategoryText(category),
		icon: getCategoryIcon(category),
		count: projectsData.filter((p) => p.category === category).length,
	})),
];

const title = i18n(I18nKey.projects);
const subtitle = i18n(I18nKey.projectsSubtitle);
---

<MainGridLayout title={title} description={subtitle}>
	<script>
		import { initIconLoader } from "../utils/icon-loader";

		initIconLoader();
	</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="category" />
			</div>

			<div
				id="projects-grid"
				class="grid grid-cols-1 md:grid-cols-2 gap-6 items-start"
			>
				{
					projectsData.map((project) => (
						<ProjectCard project={project} maxTechStack={4} />
					))
				}
			</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 projects
				</p>
			</div>
		</div>
	</div>
</MainGridLayout>

配置教程 ​

有关项目页面配置教程,请参考 项目页面配置教程