feat(projects): simplify categories and add cover-ready card layout

This commit is contained in:
zguiyang
2026-03-16 22:30:26 +08:00
parent d873c3b063
commit f76a00c0a6
4 changed files with 148 additions and 188 deletions

View File

@@ -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: '#',
},

View File

@@ -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>

View File

@@ -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>

View File

@@ -61,6 +61,8 @@ export interface Project {
tech: string[];
link: string;
featured?: boolean;
coverImage?: string;
coverImageAlt?: string;
links?: {
github?: string;
demo?: string;