Initial commit

This commit is contained in:
2026-02-02 22:47:52 +03:00
committed by GitHub
commit f53016aeda
239 changed files with 84360 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
---
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
export interface Props {
project: {
id: string;
title: string;
description: string;
image?: string;
category: string;
techStack: string[];
status: "completed" | "in-progress" | "planned";
demoUrl?: string;
sourceUrl?: string;
startDate: string;
endDate?: string;
featured?: boolean;
tags?: string[];
};
size?: "small" | "medium" | "large";
showImage?: boolean;
maxTechStack?: number;
}
const {
project,
size = "medium",
showImage = true,
maxTechStack = 4,
} = Astro.props;
// 状态样式映射
const getStatusStyle = (status: string) => {
switch (status) {
case "completed":
return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400";
case "in-progress":
return "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400";
case "planned":
return "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400";
default:
return "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400";
}
};
// 状态文本映射
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;
}
};
// 尺寸样式映射
const getSizeClasses = (size: string) => {
switch (size) {
case "small":
return {
container: "p-4",
title: "text-lg",
description: "text-sm line-clamp-2",
tech: "text-xs",
links: "text-sm",
};
case "large":
return {
container: "p-6",
title: "text-xl",
description: "text-base line-clamp-3",
tech: "text-sm",
links: "text-base",
};
default: // medium
return {
container: "p-5",
title: "text-lg",
description: "text-sm line-clamp-2",
tech: "text-xs",
links: "text-sm",
};
}
};
const sizeClasses = getSizeClasses(size);
---
<div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 overflow-hidden hover:shadow-lg transition-all duration-300 group">
<!-- 项目图片 -->
{showImage && project.image && (
<div class={`overflow-hidden ${size === 'large' ? 'aspect-video' : 'aspect-4/3'}`}>
<img
src={project.image}
alt={project.title}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
loading="lazy"
/>
</div>
)}
<!-- 项目内容 -->
<div class={sizeClasses.container}>
<!-- 标题和状态 -->
<div class="flex items-start justify-between mb-3">
<h3 class={`font-semibold text-black/90 dark:text-white/90 ${sizeClasses.title} ${size === 'small' ? 'line-clamp-1' : ''}`}>
{project.title}
</h3>
<span class={`px-2 py-1 text-xs rounded-full shrink-0 ml-2 ${getStatusStyle(project.status)}`}>
{getStatusText(project.status)}
</span>
</div>
<!-- 分类标签 -->
{project.category && (
<div class="mb-2">
<span class="px-2 py-1 text-xs bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400 rounded-sm">
{project.category}
</span>
</div>
)}
<!-- 项目描述 -->
<p class={`text-black/60 dark:text-white/60 mb-4 ${sizeClasses.description}`}>
{project.description}
</p>
<!-- 技术栈 -->
{project.techStack && project.techStack.length > 0 && (
<div class="flex flex-wrap gap-1 mb-4">
{project.techStack.slice(0, maxTechStack).map((tech) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.tech}`}>
{tech}
</span>
))}
{project.techStack.length > maxTechStack && (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm ${sizeClasses.tech}`}>
+{project.techStack.length - maxTechStack}
</span>
)}
</div>
)}
<!-- 标签 -->
{project.tags && project.tags.length > 0 && (
<div class="flex flex-wrap gap-1 mb-4">
{project.tags.map((tag) => (
<span class={`px-2 py-1 bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400 rounded-sm ${sizeClasses.tech}`}>
#{tag}
</span>
))}
</div>
)}
<!-- 链接 -->
<div class="flex gap-3">
{project.demoUrl && (
<a
href={project.demoUrl}
target="_blank"
rel="noopener noreferrer"
class={`inline-flex items-center px-3 py-1.5 rounded-md border border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-300 bg-blue-50/60 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/40 font-medium transition-colors ${sizeClasses.links}`}
>
{i18n(I18nKey.projectsDemo)}
</a>
)}
{project.sourceUrl && (
<a
href={project.sourceUrl}
target="_blank"
rel="noopener noreferrer"
class={`inline-flex items-center px-3 py-1.5 rounded-md border border-gray-400 text-gray-700 dark:border-gray-500 dark:text-gray-200 bg-gray-50/80 dark:bg-gray-800/60 hover:bg-gray-100 dark:hover:bg-gray-700 font-medium transition-colors ${sizeClasses.links}`}
>
{i18n(I18nKey.projectsSource)}
</a>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,219 @@
---
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import Icon from "@components/common/icon.astro";
export interface Props {
skill: {
id: string;
name: string;
description: string;
icon?: string;
category: string;
level: "beginner" | "intermediate" | "advanced" | "expert";
experience: string | { years: number; months: number };
relatedProjects?: string[];
certifications?: string[];
color?: string;
};
size?: "small" | "medium" | "large";
showProgress?: boolean;
showIcon?: boolean;
}
const {
skill,
size = "medium",
showProgress = true,
showIcon = true,
} = Astro.props;
// 技能等级颜色映射
const getLevelColor = (level: string) => {
switch (level) {
case "expert":
return "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400";
case "advanced":
return "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400";
case "intermediate":
return "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400";
case "beginner":
return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400";
default:
return "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400";
}
};
// 技能等级文本映射
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 getSizeClasses = (size: string) => {
switch (size) {
case "small":
return {
container: "p-4",
icon: "w-8 h-8",
iconText: "text-lg",
title: "text-base",
description: "text-xs line-clamp-2",
badge: "text-xs",
progress: "h-1.5",
};
case "large":
return {
container: "p-6",
icon: "w-14 h-14",
iconText: "text-3xl",
title: "text-xl",
description: "text-sm line-clamp-3",
badge: "text-sm",
progress: "h-3",
};
default: // medium
return {
container: "p-5",
icon: "w-10 h-10",
iconText: "text-xl",
title: "text-lg",
description: "text-sm line-clamp-2",
badge: "text-xs",
progress: "h-2",
};
}
};
const sizeClasses = getSizeClasses(size);
const skillColor = skill.color || "#3B82F6";
// 经验展示文本
const getExperienceText = (experience: Props["skill"]["experience"]) => {
if (typeof experience === "string") return experience;
const yearsText = `${experience.years}${i18n(I18nKey.skillYears)}`;
const monthsText =
experience.months > 0
? ` ${experience.months}${i18n(I18nKey.skillMonths)}`
: "";
return `${yearsText}${monthsText}`;
};
---
<div class="bg-white dark:bg-slate-800/50 rounded-lg border border-black/10 dark:border-white/10 overflow-hidden hover:shadow-lg transition-all duration-300 group">
<div class={sizeClasses.container}>
<div class="flex items-start gap-4">
<!-- 技能图标 -->
{showIcon && skill.icon && (
<div class={`rounded-lg flex items-center justify-center shrink-0 ${sizeClasses.icon}`} style={`background-color: ${skillColor}20`}>
<Icon
icon={skill.icon}
class={sizeClasses.iconText}
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-2">
<h3 class={`font-semibold text-black/90 dark:text-white/90 ${sizeClasses.title} ${size === 'small' ? 'truncate' : ''}`}>
{skill.name}
</h3>
<span class={`px-2 py-1 rounded-full shrink-0 ml-2 ${sizeClasses.badge} ${getLevelColor(skill.level)}`}>
{getLevelText(skill.level)}
</span>
</div>
<!-- 分类标签 -->
{skill.category && (
<div class="mb-2">
<span class={`px-2 py-1 bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400 rounded-sm ${sizeClasses.badge}`}>
{skill.category}
</span>
</div>
)}
<!-- 技能描述 -->
<p class={`text-black/60 dark:text-white/60 mb-3 ${sizeClasses.description}`}>
{skill.description}
</p>
<!-- 经验和进度条 -->
{showProgress && (
<div class="mb-3">
<div class="flex justify-between text-sm mb-1">
<span class="text-black/60 dark:text-white/60">{i18n(I18nKey.skillExperience)}</span>
<span class="text-black/80 dark:text-white/80">{getExperienceText(skill.experience)}</span>
</div>
<div class={`w-full bg-gray-200 dark:bg-gray-700 rounded-full ${sizeClasses.progress}`}>
<div
class={`rounded-full transition-all duration-500 ${sizeClasses.progress}`}
style={`width: ${getLevelWidth(skill.level)}; background-color: ${skillColor}`}
></div>
</div>
</div>
)}
<!-- 认证信息 -->
{skill.certifications && skill.certifications.length > 0 && (
<div class="mb-3">
<div class="flex flex-wrap gap-1">
{skill.certifications.map((cert) => (
<span class={`px-2 py-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 rounded-sm ${sizeClasses.badge}`}>
🏆 {cert}
</span>
))}
</div>
</div>
)}
<!-- 相关项目 -->
{skill.relatedProjects && skill.relatedProjects.length > 0 && (
<div class="text-sm text-black/60 dark:text-white/60">
{i18n(I18nKey.skillsProjects)}: {skill.relatedProjects.length}
</div>
)}
</div>
</div>
</div>
</div>
<script>
// 监听图标加载完成事件
document.addEventListener('DOMContentLoaded', () => {
const skillCard = document.currentScript?.parentElement;
if (skillCard) {
skillCard.classList.add('skill-card');
// 监听图标准备就绪事件
skillCard.addEventListener('iconify-ready', () => {
// 图标加载完成,可以执行额外的初始化逻辑
skillCard.classList.add('icons-loaded');
});
}
});
</script>

View File

@@ -0,0 +1,372 @@
---
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import Icon from "@components/common/icon.astro";
export interface Props {
item: {
id: string;
title: string;
description: string;
type: "education" | "work" | "project" | "achievement";
startDate: string;
endDate?: string;
location?: string;
organization?: string;
position?: string;
skills?: string[];
achievements?: string[];
links?: {
name: string;
url: string;
type: "certificate" | "project" | "other";
}[];
icon?: string;
color?: string;
featured?: boolean;
};
showTimeline?: boolean;
size?: "small" | "medium" | "large";
layout?: "card" | "timeline";
}
const {
item,
showTimeline = true,
size = "medium",
layout = "timeline",
} = Astro.props;
// 类型图标映射
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:event";
}
};
// 类型颜色映射
const getTypeColor = (type: string) => {
switch (type) {
case "education":
return "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400";
case "work":
return "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400";
case "project":
return "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400";
case "achievement":
return "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400";
default:
return "bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400";
}
};
// 类型文本映射
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 getLinkIcon = (type: string) => {
switch (type) {
case "certificate":
return "🏆";
case "project":
return "📑";
case "other":
return "🔗";
default:
return "🔗";
}
};
// 格式化日期
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("zh-CN", { year: "numeric", month: "long" });
};
// 计算持续时间
const getDuration = (startDate: string, endDate?: string) => {
const start = new Date(startDate);
const end = endDate ? new Date(endDate) : new Date();
const diffTime = Math.abs(end.getTime() - start.getTime());
const diffMonths = Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 30));
if (diffMonths < 12) {
return `${diffMonths} ${i18n(I18nKey.timelineMonths)}`;
} else {
const years = Math.floor(diffMonths / 12);
const months = diffMonths % 12;
if (months === 0) {
return `${years} ${i18n(I18nKey.timelineYears)}`;
} else {
return `${years} ${i18n(I18nKey.timelineYears)} ${months} ${i18n(I18nKey.timelineMonths)}`;
}
}
};
// 尺寸样式映射
const getSizeClasses = (size: string) => {
switch (size) {
case "small":
return {
container: "p-4",
node: "w-8 h-8",
nodeIcon: "text-lg",
title: "text-lg",
meta: "text-xs",
description: "text-sm",
badge: "text-xs",
};
case "large":
return {
container: "p-8",
node: "w-16 h-16",
nodeIcon: "text-2xl",
title: "text-2xl",
meta: "text-base",
description: "text-base",
badge: "text-sm",
};
default: // medium
return {
container: "p-6",
node: "w-12 h-12",
nodeIcon: "text-xl",
title: "text-xl",
meta: "text-sm",
description: "text-sm",
badge: "text-xs",
};
}
};
const sizeClasses = getSizeClasses(size);
const itemColor = item.color || "#3B82F6";
---
{layout === 'timeline' ? (
<!-- 时间线布局 -->
<div class="relative flex items-start gap-6">
<!-- 时间线节点 -->
{showTimeline && (
<div class={`relative z-10 rounded-full flex items-center justify-center shrink-0 ${sizeClasses.node}`} style={`background-color: ${itemColor}`}>
<Icon
icon={item.icon || getTypeIcon(item.type)}
class={`text-white ${sizeClasses.nodeIcon}`}
color="white"
fallback={item.title.charAt(0)}
loading="eager"
/>
</div>
)}
<!-- 内容卡片 -->
<div class="flex-1 bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 hover:shadow-lg transition-shadow duration-300">
<div class={sizeClasses.container}>
<!-- 标题和类型 -->
<div class="flex items-start justify-between mb-3">
<div>
<h3 class={`font-semibold text-black/90 dark:text-white/90 mb-1 ${sizeClasses.title}`}>
{item.title}
{item.featured && (
<span class="ml-2 text-yellow-500">⭐</span>
)}
</h3>
{item.organization && (
<div class={`text-black/70 dark:text-white/70 ${sizeClasses.meta}`}>
{item.organization} {item.position && `• ${item.position}`}
</div>
)}
</div>
<span class={`px-2 py-1 rounded-full shrink-0 ml-4 ${sizeClasses.badge} ${getTypeColor(item.type)}`}>
{getTypeText(item.type)}
</span>
</div>
<!-- 时间和地点信息 -->
<div class={`flex items-center gap-4 mb-3 text-black/60 dark:text-white/60 ${sizeClasses.meta}`}>
<div>
{formatDate(item.startDate)} - {item.endDate ? formatDate(item.endDate) : i18n(I18nKey.timelinePresent)}
</div>
<div>•</div>
<div>{getDuration(item.startDate, item.endDate)}</div>
{item.location && (
<>
<div>•</div>
<div>📍 {item.location}</div>
</>
)}
</div>
<!-- 描述 -->
<p class={`text-black/70 dark:text-white/70 mb-4 ${sizeClasses.description}`}>
{item.description}
</p>
<!-- 成就 -->
{item.achievements && item.achievements.length > 0 && (
<div class="mb-4">
<h4 class={`font-semibold text-black/80 dark:text-white/80 mb-2 ${sizeClasses.meta}`}>
{i18n(I18nKey.timelineAchievements)}
</h4>
<ul class="space-y-1">
{item.achievements.map((achievement) => (
<li class={`text-black/70 dark:text-white/70 flex items-start gap-2 ${sizeClasses.description}`}>
<span class="text-green-500 mt-1">•</span>
<span>{achievement}</span>
</li>
))}
</ul>
</div>
)}
<!-- 技能 -->
{item.skills && item.skills.length > 0 && (
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{item.skills.map((skill) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge}`}>
{skill}
</span>
))}
</div>
</div>
)}
<!-- 链接 -->
{item.links && item.links.length > 0 && (
<div class="flex gap-4">
{item.links.map((link) => (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
class={`inline-flex items-center px-3 py-1.5 rounded-md border border-(--primary) text-(--primary) bg-[color-mix(in_oklch,var(--primary)_8%,transparent)] hover:bg-[color-mix(in_oklch,var(--primary)_14%,transparent)] active:bg-[color-mix(in_oklch,var(--primary)_20%,transparent)] text-sm font-medium transition-colors gap-1 ${sizeClasses.meta}`}
>
{getLinkIcon(link.type)}
{link.name}
</a>
))}
</div>
)}
</div>
</div>
</div>
) : (
<!-- 卡片布局 -->
<div class="bg-white dark:bg-gray-800 rounded-lg border border-black/10 dark:border-white/10 hover:shadow-lg transition-shadow duration-300">
<div class={sizeClasses.container}>
<!-- 图标和标题 -->
<div class="flex items-start gap-4 mb-3">
<div class={`rounded-lg flex items-center justify-center shrink-0 ${sizeClasses.node}`} style={`background-color: ${itemColor}20`}>
<Icon
icon={item.icon || getTypeIcon(item.type)}
class={sizeClasses.nodeIcon}
color={itemColor}
fallback={item.title.charAt(0)}
loading="eager"
/>
</div>
<div class="flex-1">
<div class="flex items-start justify-between mb-2">
<h3 class={`font-semibold text-black/90 dark:text-white/90 ${sizeClasses.title}`}>
{item.title}
{item.featured && (
<span class="ml-2 text-yellow-500">⭐</span>
)}
</h3>
<span class={`px-2 py-1 rounded-full shrink-0 ml-2 ${sizeClasses.badge} ${getTypeColor(item.type)}`}>
{getTypeText(item.type)}
</span>
</div>
{item.organization && (
<div class={`text-black/70 dark:text-white/70 mb-1 ${sizeClasses.meta}`}>
{item.organization} {item.position && `• ${item.position}`}
</div>
)}
{item.location && (
<div class={`text-black/60 dark:text-white/60 mb-2 ${sizeClasses.meta}`}>
📍 {item.location}
</div>
)}
</div>
</div>
<!-- 时间信息 -->
<div class={`text-black/70 dark:text-white/70 mb-3 ${sizeClasses.meta}`}>
{formatDate(item.startDate)} - {item.endDate ? formatDate(item.endDate) : i18n(I18nKey.timelinePresent)} ({getDuration(item.startDate, item.endDate)})
</div>
<!-- 描述 -->
<p class={`text-black/60 dark:text-white/60 mb-4 ${sizeClasses.description}`}>
{item.description}
</p>
<!-- 成就 -->
{item.achievements && item.achievements.length > 0 && (
<div class="mb-4">
<h4 class={`font-semibold text-black/80 dark:text-white/80 mb-2 ${sizeClasses.meta}`}>
{i18n(I18nKey.timelineAchievements)}
</h4>
<ul class="space-y-1">
{item.achievements.slice(0, 3).map((achievement) => (
<li class={`text-black/70 dark:text-white/70 flex items-start gap-2 ${sizeClasses.description}`}>
<span class="text-green-500 mt-1">•</span>
<span>{achievement}</span>
</li>
))}
{item.achievements.length > 3 && (
<li class={`text-black/60 dark:text-white/60 ${sizeClasses.description}`}>
... 还有 {item.achievements.length - 3} 项成就
</li>
)}
</ul>
</div>
)}
<!-- 技能和链接 -->
<div class="flex items-center justify-between">
{item.skills && item.skills.length > 0 && (
<div class="flex flex-wrap gap-1">
{item.skills.slice(0, 3).map((skill) => (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 rounded-sm ${sizeClasses.badge}`}>
{skill}
</span>
))}
{item.skills.length > 3 && (
<span class={`px-2 py-1 bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400 rounded-sm ${sizeClasses.badge}`}>
+{item.skills.length - 3}
</span>
)}
</div>
)}
{item.links && item.links.length > 0 && (
<div class="flex gap-3">
{item.links.slice(0, 2).map((link) => (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
class={`inline-flex items-center px-3 py-1.5 rounded-md border border-(--primary) text-(--primary) bg-[color-mix(in_oklch,var(--primary)_8%,transparent)] hover:bg-[color-mix(in_oklch,var(--primary)_14%,transparent)] active:bg-[color-mix(in_oklch,var(--primary)_20%,transparent)] text-sm font-medium transition-colors ${sizeClasses.meta}`}
>
{getLinkIcon(link.type)} {link.name}
</a>
))}
</div>
)}
</div>
</div>
</div>
)}