mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-04 12:50:49 +04:00
Initial commit
This commit is contained in:
179
src/components/data/projectCard.astro
Normal file
179
src/components/data/projectCard.astro
Normal 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>
|
||||
219
src/components/data/skillCard.astro
Normal file
219
src/components/data/skillCard.astro
Normal 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>
|
||||
372
src/components/data/timelineItem.astro
Normal file
372
src/components/data/timelineItem.astro
Normal 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>
|
||||
)}
|
||||
Reference in New Issue
Block a user