Files
software-workspace/src/pages/ProjectsPage.tsx
T
shenjianZ b6f15f82d8 feat: 新增 QuantaNote 项目展示,重构项目卡片与截图浏览组件
- 新增 QuantaNote 完整项目数据(特性描述、截图、Logo)
  - 为所有项目添加 logo 和 websiteUrl 字段支持
  - 移除 stars/forks 相关展示与排序逻辑
  - ScreenshotCarousel 增加左右切换箭头和 Lightbox 全屏预览(支持键盘导航)
  - 更新项目创建文档,补充 logo 和 installGuide 配置说明
2026-05-22 16:07:30 +08:00

105 lines
3.2 KiB
TypeScript

import { useSearchParams } from 'react-router-dom';
import { useI18n } from '../hooks/useI18n';
import { useProjectFilters } from '../hooks/useProjectFilters';
import { siteData } from '../data/siteData';
import ProjectCard from '../components/ProjectCard';
import SelectControl from '../components/SelectControl';
export default function ProjectsPage() {
const { t, bi } = useI18n();
const [searchParams] = useSearchParams();
const catParam = searchParams.get('cat');
const {
search,
setSearch,
tech,
setTech,
platform,
setPlatform,
status,
setStatus,
sort,
setSort,
allTech,
allPlatforms,
allStatuses,
filteredProjects,
} = useProjectFilters();
const displayedProjects = catParam
? filteredProjects.filter((p) => p.type.includes(catParam))
: filteredProjects;
const techOptions = [
{ value: '', label: t('projects.filter.all') },
...allTech.map((item) => ({ value: item, label: item })),
];
const platformOptions = [
{ value: '', label: t('projects.filter.all') },
...allPlatforms.map((item) => ({ value: item, label: item })),
];
const statusOptions = [
{ value: '', label: t('projects.filter.all') },
...allStatuses.map((item) => ({ value: item, label: bi(siteData.statuses[item]?.label) })),
];
const sortOptions = [
{ value: 'updated', label: t('projects.sort.updated') },
{ value: 'name', label: t('projects.sort.name') },
];
return (
<div className="container fade-in">
<div className="page-header">
<h1 className="page-title">{t('projects.title')}</h1>
<p className="page-subtitle">{t('projects.subtitle')}</p>
</div>
<div className="filter-bar">
<input
type="text"
className="search-input"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('projects.search')}
/>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.tech')}</span>
<SelectControl
value={tech}
options={techOptions}
onChange={setTech}
searchable={techOptions.length > 8}
searchPlaceholder={t('projects.filter.tech')}
emptyLabel={t('projects.noResults')}
/>
</div>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.platform')}</span>
<SelectControl value={platform} options={platformOptions} onChange={setPlatform} />
</div>
<div className="filter-group">
<span className="filter-group-label">{t('projects.filter.status')}</span>
<SelectControl value={status} options={statusOptions} onChange={setStatus} />
</div>
<div className="filter-group filter-group-sort">
<span className="filter-group-label">Sort</span>
<SelectControl value={sort} options={sortOptions} onChange={setSort} />
</div>
</div>
<div className="card-grid">
{displayedProjects.length === 0 ? (
<div className="empty-state">{t('projects.noResults')}</div>
) : (
displayedProjects.map((p) => <ProjectCard key={p.id} project={p} />)
)}
</div>
</div>
);
}