Make project cards easier to scan in dense three-column grids
Constraint: Keep projects page in a 3-column grid while reducing line-wrap pressure in card metadata/action areas Rejected: Keeping metadata and actions split into side-by-side columns | caused unstable wrapping and noisy visual rhythm Confidence: high Scope-risk: narrow Directive: Keep project-card internals on project-specific classes and preserve one metadata row per semantic purpose Tested: npm run build; ./scripts/deploy-homepage.sh Not-tested: Manual cross-browser visual checks beyond current deployed environment
This commit is contained in:
parent
7a091a9b44
commit
84159db417
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
import { site } from '../config';
|
||||
import type { ProjectDownload } from '../data/loaders';
|
||||
import { formatDateTime } from '../utils/datetime';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
|
|
@ -30,40 +29,55 @@ const {
|
|||
downloadLink = '',
|
||||
downloads = [],
|
||||
updatedAt = '',
|
||||
source,
|
||||
source: _source,
|
||||
} = Astro.props;
|
||||
|
||||
const fallbackRepoUrl = repo && site.gitea.url ? `${site.gitea.url}/${repo}` : '';
|
||||
const resolvedRepoUrl = repoUrl || fallbackRepoUrl;
|
||||
const visibleDownloads = downloads.filter((item) => item.name || item.url);
|
||||
const sourceLabels = {
|
||||
seed: '本地种子',
|
||||
gitea: 'Gitea',
|
||||
'gitea+seafile': 'Gitea + Seafile',
|
||||
} as const;
|
||||
const updatedLabel = formatDateTime(updatedAt);
|
||||
const updatedLabel = formatProjectUpdateLabel(updatedAt);
|
||||
|
||||
function formatProjectUpdateLabel(value: string) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parsed = Date.parse(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const date = new Date(parsed);
|
||||
const now = new Date();
|
||||
|
||||
if (date.getFullYear() < now.getFullYear()) {
|
||||
return '很久之前';
|
||||
}
|
||||
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
}
|
||||
---
|
||||
|
||||
<article class="card">
|
||||
<article class={`card project-card${featured ? ' project-card--featured' : ''}`}>
|
||||
<div class="project-cover">
|
||||
<img src={coverImage} alt={`${name} 项目封面`} loading="lazy" />
|
||||
{featured ? <span class="project-featured-badge" aria-label="Featured 项目">★</span> : null}
|
||||
</div>
|
||||
|
||||
<div class="project-meta">
|
||||
<span class="mono">{repo}</span>
|
||||
{featured ? <span class="tag">featured</span> : null}
|
||||
{source ? <span class="tag">{sourceLabels[source]}</span> : null}
|
||||
{updatedAt ? <span class="mono">更新于 {updatedLabel}</span> : null}
|
||||
<div class="project-card__content">
|
||||
<h3 class="project-card__title">{name}</h3>
|
||||
<p class="project-card__description">{description}</p>
|
||||
</div>
|
||||
|
||||
<h3 class="project-title">{name}</h3>
|
||||
<p class="project-description">{description}</p>
|
||||
|
||||
<div class="tag-list" style="margin: 1rem 0 0.95rem;">
|
||||
<div class="project-card__meta">
|
||||
<div class="project-card__tags tag-list">
|
||||
{tags.map((tag) => <span class="tag tag--tech">{tag}</span>)}
|
||||
</div>
|
||||
|
||||
<div class="project-links">
|
||||
<div class="project-card__footer">
|
||||
{updatedLabel ? <span class="project-card__time mono">更新时间 {updatedLabel}</span> : null}
|
||||
|
||||
<div class="project-links project-card__actions">
|
||||
{resolvedRepoUrl ? (
|
||||
<a class="section-link" href={resolvedRepoUrl} target="_blank" rel="noreferrer">
|
||||
仓库
|
||||
|
|
@ -79,11 +93,6 @@ const updatedLabel = formatDateTime(updatedAt);
|
|||
下载
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{
|
||||
visibleDownloads.length > 0 ? (
|
||||
<div class="project-links" style="margin-top: 0.75rem; flex-wrap: wrap; gap: 0.6rem 0.8rem;">
|
||||
{visibleDownloads.map((item) =>
|
||||
item.url ? (
|
||||
<a class="section-link" href={item.url} target="_blank" rel="noreferrer">
|
||||
|
|
@ -94,6 +103,6 @@ const updatedLabel = formatDateTime(updatedAt);
|
|||
),
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ import Layout from '../../layouts/Layout.astro';
|
|||
import { getLatestDate } from '../../utils/datetime';
|
||||
|
||||
const projects = await loadProjects();
|
||||
const sortedProjects = [...projects].sort((a, b) => {
|
||||
if (a.featured !== b.featured) {
|
||||
return Number(b.featured) - Number(a.featured);
|
||||
}
|
||||
|
||||
const leftUpdated = Date.parse(a.updated_at || '');
|
||||
const rightUpdated = Date.parse(b.updated_at || '');
|
||||
const leftTime = Number.isNaN(leftUpdated) ? 0 : leftUpdated;
|
||||
const rightTime = Number.isNaN(rightUpdated) ? 0 : rightUpdated;
|
||||
return rightTime - leftTime;
|
||||
});
|
||||
const projectSources = [...new Set(projects.map((project) => project.source).filter(Boolean))];
|
||||
const projectsUpdatedAt = getLatestDate(projects.map((project) => project.updated_at).filter(Boolean));
|
||||
---
|
||||
|
|
@ -33,7 +44,7 @@ const projectsUpdatedAt = getLatestDate(projects.map((project) => project.update
|
|||
|
||||
<div class="card-grid">
|
||||
{
|
||||
projects.map((project) => (
|
||||
sortedProjects.map((project) => (
|
||||
<ProjectCard
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
|
|
|
|||
|
|
@ -612,6 +612,59 @@ main {
|
|||
margin-left: auto;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-card--featured {
|
||||
border-color: rgba(245, 158, 11, 0.45);
|
||||
box-shadow: 0 12px 28px rgba(245, 158, 11, 0.12);
|
||||
}
|
||||
|
||||
.project-card__content {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.project-card__title {
|
||||
margin: 0;
|
||||
font-family: 'Archivo', sans-serif;
|
||||
font-size: 1.26rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.project-card__description {
|
||||
margin: 0;
|
||||
color: var(--muted-strong);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.project-card__meta {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.project-card__tags {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.7rem 1rem;
|
||||
}
|
||||
|
||||
.project-card__time {
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.project-card__actions {
|
||||
gap: 0.6rem 0.8rem;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus-within {
|
||||
transform: translateY(-2px);
|
||||
|
|
@ -682,6 +735,7 @@ main {
|
|||
}
|
||||
|
||||
.project-cover {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
|
|
@ -690,6 +744,23 @@ main {
|
|||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.project-featured-badge {
|
||||
position: absolute;
|
||||
top: 0.55rem;
|
||||
right: 0.55rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.65rem;
|
||||
height: 1.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(245, 158, 11, 0.95);
|
||||
color: #ffffff;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1;
|
||||
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
.activity-panel {
|
||||
padding: 1.5rem;
|
||||
color: #f8fafc;
|
||||
|
|
@ -963,6 +1034,16 @@ main {
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
.project-card__footer {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.project-card__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.section-heading,
|
||||
.footer-card {
|
||||
align-items: flex-start;
|
||||
|
|
|
|||
Loading…
Reference in New Issue