feat(projects): simplify categories and add cover-ready card layout
This commit is contained in:
@@ -181,6 +181,8 @@ const sharedCases: Project[] = [
|
||||
'An open AI workspace for builders.',
|
||||
'Used as an R&D track, not the primary professional narrative.',
|
||||
],
|
||||
coverImage: '',
|
||||
coverImageAlt: 'Elynd project cover',
|
||||
tech: ['TypeScript', 'React', 'AI Workflow', 'Open Source'],
|
||||
link: '#',
|
||||
},
|
||||
|
||||
@@ -9,82 +9,96 @@ import { defaultLang } from '@/i18n/ui';
|
||||
|
||||
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
||||
const isZh = lang === 'zh';
|
||||
const pageProjects = projects[lang];
|
||||
|
||||
const filters = [
|
||||
{ key: 'all', label: isZh ? '全部' : 'All' },
|
||||
{ key: 'product', label: isZh ? '产品系统' : 'Product Systems' },
|
||||
{ key: 'client', label: isZh ? '企业项目' : 'Client Systems' },
|
||||
{ key: 'experiment', label: isZh ? '实验项目' : 'Experiments' },
|
||||
];
|
||||
const pageProjects = projects[lang].map((project) => ({
|
||||
...project,
|
||||
category: project.id === 'elynd' ? 'indie' : 'enterprise',
|
||||
}));
|
||||
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
||||
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
||||
---
|
||||
|
||||
<Layout title={isZh ? '项目' : 'Projects'}>
|
||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||
|
||||
<main class="min-h-screen pt-24 pb-20" data-projects-root>
|
||||
<main class="min-h-screen pt-24 pb-20">
|
||||
<Container>
|
||||
<section class="page-content-main">
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '工程案例' : 'Engineering Case Studies'}</h1>
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
||||
<p class="mt-4 text-lg leading-relaxed text-muted-foreground">
|
||||
{isZh
|
||||
? '围绕复杂系统建设的真实项目经验,重点展示技术挑战、职责边界和结果。'
|
||||
: 'Real projects focused on complex system delivery, highlighting technical challenges, responsibilities, and outcomes.'}
|
||||
? '按两类展示:企业项目与独立开发项目。采用卡片布局,聚焦项目背景、职责和结果。'
|
||||
: 'Organized into two categories: enterprise projects and independent projects, shown in a cover-style card grid.'}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="page-content-main mt-8">
|
||||
<div class="page-surface mb-8 flex flex-wrap gap-2 p-2">
|
||||
{filters.map((filter, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-filter={filter.key}
|
||||
aria-pressed={index === 0 ? 'true' : 'false'}
|
||||
class={`rounded-md px-4 py-2 text-sm font-semibold ${index === 0 ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground'}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
<div class="mb-5 flex items-end justify-between gap-3">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '企业项目' : 'Enterprise Projects'}</h2>
|
||||
<span class="text-sm text-muted-foreground">{enterpriseProjects.length} {isZh ? '个项目' : 'projects'}</span>
|
||||
</div>
|
||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{enterpriseProjects.map((project) => (
|
||||
<article class="page-surface overflow-hidden">
|
||||
<div class={`bg-gradient-to-br ${project.image.bg} p-5`}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-foreground/80">{isZh ? '企业项目' : 'Enterprise'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="mt-2 text-sm text-foreground/80">{project.systemType}</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{project.tech.slice(0, 5).map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
||||
<p class="mt-3 text-xs text-muted-foreground">{isZh ? '受保密协议限制,暂不提供在线预览。' : 'Online preview is not available due to confidentiality restrictions.'}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="space-y-6">
|
||||
{pageProjects.map((project) => (
|
||||
<article data-project-card data-type={project.type} class="page-surface p-6 md:p-8">
|
||||
<div class="grid gap-6 lg:grid-cols-[1.2fr_1fr]">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary">{project.systemType}</p>
|
||||
<h2 class="mt-2 text-2xl font-bold tracking-tight">{project.title}</h2>
|
||||
<p class="mt-3 text-muted-foreground">{project.context}</p>
|
||||
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
{project.tech.map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '技术挑战' : 'Technical Challenges'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-muted-foreground">
|
||||
{project.challenges?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '职责范围' : 'Responsibilities'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-muted-foreground">
|
||||
{project.responsibilities?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '结果' : 'Outcomes'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-foreground/90">
|
||||
{project.outcomes?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
<section class="page-content-main mt-12">
|
||||
<div class="mb-5 flex items-end justify-between gap-3">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '独立项目' : 'Independent Projects'}</h2>
|
||||
<span class="text-sm text-muted-foreground">{indieProjects.length} {isZh ? '个项目' : 'projects'}</span>
|
||||
</div>
|
||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{indieProjects.map((project) => (
|
||||
<article class="page-surface overflow-hidden">
|
||||
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
||||
{project.coverImage ? (
|
||||
<img
|
||||
src={project.coverImage}
|
||||
alt={project.coverImageAlt || project.title}
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<div class={`flex h-full w-full flex-col justify-end bg-gradient-to-br ${project.image.bg} p-5`}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-foreground/80">{isZh ? '封面待补充' : 'Cover Coming Soon'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{project.tech.slice(0, 5).map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
||||
{project.link !== '#' && (
|
||||
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
|
||||
{isZh ? '查看项目' : 'Open Project'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -95,39 +109,3 @@ const filters = [
|
||||
|
||||
<Footer lang={lang} client:load />
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
const root = document.querySelector('[data-projects-root]');
|
||||
if (!root) {
|
||||
// no-op
|
||||
} else {
|
||||
const buttons = Array.from(root.querySelectorAll('[data-filter]'));
|
||||
const cards = Array.from(root.querySelectorAll('[data-project-card]'));
|
||||
|
||||
const apply = (activeFilter) => {
|
||||
cards.forEach((card) => {
|
||||
const cardType = card.getAttribute('data-type');
|
||||
const visible = activeFilter === 'all' || activeFilter === cardType;
|
||||
card.classList.toggle('hidden', !visible);
|
||||
});
|
||||
|
||||
buttons.forEach((button) => {
|
||||
const isActive = button.getAttribute('data-filter') === activeFilter;
|
||||
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
button.classList.toggle('bg-primary', isActive);
|
||||
button.classList.toggle('text-primary-foreground', isActive);
|
||||
button.classList.toggle('text-muted-foreground', !isActive);
|
||||
button.classList.toggle('hover:bg-muted', !isActive);
|
||||
button.classList.toggle('hover:text-foreground', !isActive);
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
apply(button.getAttribute('data-filter') ?? 'all');
|
||||
});
|
||||
});
|
||||
|
||||
apply('all');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,82 +9,96 @@ import { defaultLang } from '@/i18n/ui';
|
||||
|
||||
const lang = (Astro.currentLocale as Lang) || defaultLang;
|
||||
const isZh = lang === 'zh';
|
||||
const pageProjects = projects[lang];
|
||||
|
||||
const filters = [
|
||||
{ key: 'all', label: isZh ? '全部' : 'All' },
|
||||
{ key: 'product', label: isZh ? '产品系统' : 'Product Systems' },
|
||||
{ key: 'client', label: isZh ? '企业项目' : 'Client Systems' },
|
||||
{ key: 'experiment', label: isZh ? '实验项目' : 'Experiments' },
|
||||
];
|
||||
const pageProjects = projects[lang].map((project) => ({
|
||||
...project,
|
||||
category: project.id === 'elynd' ? 'indie' : 'enterprise',
|
||||
}));
|
||||
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
|
||||
const indieProjects = pageProjects.filter((project) => project.category === 'indie');
|
||||
---
|
||||
|
||||
<Layout title={isZh ? '项目' : 'Projects'}>
|
||||
<GlassHeader lang={lang} client:load transition:persist="header" />
|
||||
|
||||
<main class="min-h-screen pt-24 pb-20" data-projects-root>
|
||||
<main class="min-h-screen pt-24 pb-20">
|
||||
<Container>
|
||||
<section class="page-content-main">
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '工程案例' : 'Engineering Case Studies'}</h1>
|
||||
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>
|
||||
<p class="mt-4 text-lg leading-relaxed text-muted-foreground">
|
||||
{isZh
|
||||
? '围绕复杂系统建设的真实项目经验,重点展示技术挑战、职责边界和结果。'
|
||||
: 'Real projects focused on complex system delivery, highlighting technical challenges, responsibilities, and outcomes.'}
|
||||
? '按两类展示:企业项目与独立开发项目。采用卡片布局,聚焦项目背景、职责和结果。'
|
||||
: 'Organized into two categories: enterprise projects and independent projects, shown in a cover-style card grid.'}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="page-content-main mt-8">
|
||||
<div class="page-surface mb-8 flex flex-wrap gap-2 p-2">
|
||||
{filters.map((filter, index) => (
|
||||
<button
|
||||
type="button"
|
||||
data-filter={filter.key}
|
||||
aria-pressed={index === 0 ? 'true' : 'false'}
|
||||
class={`rounded-md px-4 py-2 text-sm font-semibold ${index === 0 ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground'}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
<div class="mb-5 flex items-end justify-between gap-3">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '企业项目' : 'Enterprise Projects'}</h2>
|
||||
<span class="text-sm text-muted-foreground">{enterpriseProjects.length} {isZh ? '个项目' : 'projects'}</span>
|
||||
</div>
|
||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{enterpriseProjects.map((project) => (
|
||||
<article class="page-surface overflow-hidden">
|
||||
<div class={`bg-gradient-to-br ${project.image.bg} p-5`}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-foreground/80">{isZh ? '企业项目' : 'Enterprise'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="mt-2 text-sm text-foreground/80">{project.systemType}</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{project.tech.slice(0, 5).map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
||||
<p class="mt-3 text-xs text-muted-foreground">{isZh ? '受保密协议限制,暂不提供在线预览。' : 'Online preview is not available due to confidentiality restrictions.'}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="space-y-6">
|
||||
{pageProjects.map((project) => (
|
||||
<article data-project-card data-type={project.type} class="page-surface p-6 md:p-8">
|
||||
<div class="grid gap-6 lg:grid-cols-[1.2fr_1fr]">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary">{project.systemType}</p>
|
||||
<h2 class="mt-2 text-2xl font-bold tracking-tight">{project.title}</h2>
|
||||
<p class="mt-3 text-muted-foreground">{project.context}</p>
|
||||
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
{project.tech.map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '技术挑战' : 'Technical Challenges'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-muted-foreground">
|
||||
{project.challenges?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '职责范围' : 'Responsibilities'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-muted-foreground">
|
||||
{project.responsibilities?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-bold uppercase tracking-wider text-foreground/80">{isZh ? '结果' : 'Outcomes'}</h3>
|
||||
<ul class="mt-2 space-y-1.5 text-sm text-foreground/90">
|
||||
{project.outcomes?.map((item) => <li>• {item}</li>)}
|
||||
</ul>
|
||||
<section class="page-content-main mt-12">
|
||||
<div class="mb-5 flex items-end justify-between gap-3">
|
||||
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '独立项目' : 'Independent Projects'}</h2>
|
||||
<span class="text-sm text-muted-foreground">{indieProjects.length} {isZh ? '个项目' : 'projects'}</span>
|
||||
</div>
|
||||
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{indieProjects.map((project) => (
|
||||
<article class="page-surface overflow-hidden">
|
||||
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
|
||||
{project.coverImage ? (
|
||||
<img
|
||||
src={project.coverImage}
|
||||
alt={project.coverImageAlt || project.title}
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<div class={`flex h-full w-full flex-col justify-end bg-gradient-to-br ${project.image.bg} p-5`}>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-foreground/80">{isZh ? '封面待补充' : 'Cover Coming Soon'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p>
|
||||
<h3 class="mt-2 text-lg font-bold">{project.title}</h3>
|
||||
<p class="text-sm text-muted-foreground">{project.context}</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{project.tech.slice(0, 5).map((tech) => (
|
||||
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
|
||||
{project.link !== '#' && (
|
||||
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
|
||||
{isZh ? '查看项目' : 'Open Project'}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -95,39 +109,3 @@ const filters = [
|
||||
|
||||
<Footer lang={lang} client:load />
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
const root = document.querySelector('[data-projects-root]');
|
||||
if (!root) {
|
||||
// no-op
|
||||
} else {
|
||||
const buttons = Array.from(root.querySelectorAll('[data-filter]'));
|
||||
const cards = Array.from(root.querySelectorAll('[data-project-card]'));
|
||||
|
||||
const apply = (activeFilter) => {
|
||||
cards.forEach((card) => {
|
||||
const cardType = card.getAttribute('data-type');
|
||||
const visible = activeFilter === 'all' || activeFilter === cardType;
|
||||
card.classList.toggle('hidden', !visible);
|
||||
});
|
||||
|
||||
buttons.forEach((button) => {
|
||||
const isActive = button.getAttribute('data-filter') === activeFilter;
|
||||
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
button.classList.toggle('bg-primary', isActive);
|
||||
button.classList.toggle('text-primary-foreground', isActive);
|
||||
button.classList.toggle('text-muted-foreground', !isActive);
|
||||
button.classList.toggle('hover:bg-muted', !isActive);
|
||||
button.classList.toggle('hover:text-foreground', !isActive);
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
apply(button.getAttribute('data-filter') ?? 'all');
|
||||
});
|
||||
});
|
||||
|
||||
apply('all');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -61,6 +61,8 @@ export interface Project {
|
||||
tech: string[];
|
||||
link: string;
|
||||
featured?: boolean;
|
||||
coverImage?: string;
|
||||
coverImageAlt?: string;
|
||||
links?: {
|
||||
github?: string;
|
||||
demo?: string;
|
||||
|
||||
Reference in New Issue
Block a user