feat: 更新样式和功能,优化徽章、登录页面和文章页面的布局,增强可访问性和用户体验
This commit is contained in:
@@ -12,11 +12,16 @@ import CodeCopyButton from '../../components/CodeCopyButton.astro';
|
||||
import Comments from '../../components/Comments.astro';
|
||||
import ParagraphComments from '../../components/ParagraphComments.astro';
|
||||
import QRCode from 'qrcode';
|
||||
import { apiClient, DEFAULT_SITE_SETTINGS } from '../../lib/api/client';
|
||||
import {
|
||||
apiClient,
|
||||
DEFAULT_SITE_SETTINGS,
|
||||
resolvePublicApiBaseUrl,
|
||||
} from '../../lib/api/client';
|
||||
import { formatReadTime, getI18n } from '../../lib/i18n';
|
||||
import {
|
||||
buildArticleFaqs,
|
||||
buildArticleHighlights,
|
||||
buildArticlePreviewParagraphs,
|
||||
buildArticleSynopsis,
|
||||
resolvePostUpdatedAt,
|
||||
} from '../../lib/seo';
|
||||
@@ -38,6 +43,7 @@ const { slug } = Astro.params;
|
||||
|
||||
let post = null;
|
||||
let siteSettings = DEFAULT_SITE_SETTINGS;
|
||||
const analyticsEndpoint = `${resolvePublicApiBaseUrl(Astro.url)}/analytics/content`;
|
||||
let homeData: Awaited<ReturnType<typeof apiClient.getHomePageData>> | null = null;
|
||||
let postLookupFailed = false;
|
||||
|
||||
@@ -192,6 +198,7 @@ const updatedAtLabel = Number.isNaN(new Date(updatedAt).valueOf())
|
||||
day: 'numeric',
|
||||
});
|
||||
const articleSynopsis = buildArticleSynopsis(post, 220);
|
||||
const articlePreviewParagraphs = buildArticlePreviewParagraphs(post, 3, 110);
|
||||
const articleHighlights = buildArticleHighlights(post, 3);
|
||||
const articleFaqs = buildArticleFaqs(post, {
|
||||
locale,
|
||||
@@ -208,8 +215,8 @@ const digestStats = [
|
||||
value: String(articleHighlights.length || 1),
|
||||
},
|
||||
{
|
||||
label: articleCopy.faqCount,
|
||||
value: String(articleFaqs.length),
|
||||
label: articleCopy.keywords,
|
||||
value: String(post.tags?.length || 0),
|
||||
},
|
||||
];
|
||||
const articleDigestClipboardText = [
|
||||
@@ -376,8 +383,8 @@ const breadcrumbJsonLd = {
|
||||
<Lightbox />
|
||||
<CodeCopyButton />
|
||||
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8" data-article-slug={post.slug}>
|
||||
<div class="flex flex-col gap-8 lg:flex-row">
|
||||
<div class="mx-auto max-w-[1660px] px-4 py-8 sm:px-6 lg:px-8" data-article-slug={post.slug}>
|
||||
<div class="grid gap-8 xl:grid-cols-[minmax(0,1fr)_17.5rem] 2xl:grid-cols-[minmax(0,1fr)_18.5rem]">
|
||||
<div class="min-w-0 flex-1">
|
||||
<TerminalWindow title={`~/articles/${post.slug}`} class="w-full">
|
||||
<div class="px-4 pb-2">
|
||||
@@ -430,12 +437,12 @@ const breadcrumbJsonLd = {
|
||||
<p class="max-w-3xl text-base leading-8 text-[var(--text-secondary)]">{post.description}</p>
|
||||
</div>
|
||||
|
||||
<section class="relative overflow-hidden rounded-[28px] border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.12),rgba(var(--secondary-rgb),0.05)_46%,rgba(var(--bg-rgb),0.92))] p-5 sm:p-6">
|
||||
<div class="absolute inset-y-0 left-0 w-1 rounded-full bg-[var(--primary)]/80"></div>
|
||||
<div class="absolute right-0 top-0 h-36 w-36 rounded-full bg-[var(--primary)]/10 blur-3xl"></div>
|
||||
<div class="absolute bottom-0 right-10 h-24 w-24 rounded-full bg-[var(--secondary)]/10 blur-3xl"></div>
|
||||
<div class="relative grid gap-5 xl:grid-cols-[minmax(0,1.4fr)_minmax(19rem,0.95fr)]">
|
||||
<div class="space-y-5">
|
||||
<section class="grid items-start gap-5 xl:grid-cols-[minmax(0,1.62fr)_minmax(16.5rem,0.78fr)] 2xl:grid-cols-[minmax(0,1.8fr)_minmax(17rem,0.82fr)]">
|
||||
<div class="relative overflow-hidden rounded-[28px] border border-[var(--border-color)] bg-[linear-gradient(135deg,rgba(var(--primary-rgb),0.12),rgba(var(--secondary-rgb),0.05)_46%,rgba(var(--bg-rgb),0.92))] p-5 sm:p-6">
|
||||
<div class="absolute inset-y-0 left-0 w-1 rounded-full bg-[var(--primary)]/80"></div>
|
||||
<div class="absolute right-0 top-0 h-36 w-36 rounded-full bg-[var(--primary)]/10 blur-3xl"></div>
|
||||
<div class="absolute bottom-0 right-10 h-24 w-24 rounded-full bg-[var(--secondary)]/10 blur-3xl"></div>
|
||||
<div class="relative space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center gap-2 rounded-full border border-[var(--primary)]/20 bg-[var(--primary)]/10 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.24em] text-[var(--primary)]">
|
||||
<i class="fas fa-sparkles text-[10px]"></i>
|
||||
@@ -453,7 +460,11 @@ const breadcrumbJsonLd = {
|
||||
</div>
|
||||
|
||||
<div class="rounded-[24px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/88 p-5 shadow-[0_20px_55px_rgba(15,23,42,0.08)]">
|
||||
<p class="text-base leading-8 text-[var(--title-color)]">{articleSynopsis}</p>
|
||||
<div class="space-y-3">
|
||||
{articlePreviewParagraphs.map((paragraph) => (
|
||||
<p class="text-[15px] leading-8 text-[var(--title-color)]">{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
@@ -489,121 +500,124 @@ const breadcrumbJsonLd = {
|
||||
></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/72 px-4 py-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{articleCopy.shareChannelsTitle}
|
||||
</p>
|
||||
<p class="text-xs leading-6 text-[var(--text-secondary)]">
|
||||
{articleCopy.shareChannelsDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
href={xShareUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="terminal-action-button"
|
||||
data-article-share-link
|
||||
>
|
||||
<i class="fab fa-twitter"></i>
|
||||
<span>{articleCopy.shareToX}</span>
|
||||
</a>
|
||||
<a
|
||||
href={telegramShareUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="terminal-action-button"
|
||||
data-article-share-link
|
||||
>
|
||||
<i class="fab fa-telegram-plane"></i>
|
||||
<span>{articleCopy.shareToTelegram}</span>
|
||||
</a>
|
||||
{wechatShareQrEnabled && wechatShareQrSvg && (
|
||||
<button
|
||||
type="button"
|
||||
<div class="grid gap-3 xl:grid-cols-[minmax(0,1.18fr)_minmax(13rem,0.82fr)]">
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/72 px-4 py-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{articleCopy.shareChannelsTitle}
|
||||
</p>
|
||||
<p class="text-xs leading-6 text-[var(--text-secondary)]">
|
||||
{articleCopy.shareChannelsDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
href={xShareUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="terminal-action-button"
|
||||
data-article-wechat-qr-open
|
||||
data-article-share-link
|
||||
>
|
||||
<i class="fab fa-weixin"></i>
|
||||
<span>{articleCopy.shareToWeChat}</span>
|
||||
</button>
|
||||
)}
|
||||
<i class="fab fa-twitter"></i>
|
||||
<span>{articleCopy.shareToX}</span>
|
||||
</a>
|
||||
<a
|
||||
href={telegramShareUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="terminal-action-button"
|
||||
data-article-share-link
|
||||
>
|
||||
<i class="fab fa-telegram-plane"></i>
|
||||
<span>{articleCopy.shareToTelegram}</span>
|
||||
</a>
|
||||
{wechatShareQrEnabled && wechatShareQrSvg && (
|
||||
<button
|
||||
type="button"
|
||||
class="terminal-action-button"
|
||||
data-article-wechat-qr-open
|
||||
>
|
||||
<i class="fab fa-weixin"></i>
|
||||
<span>{articleCopy.shareToWeChat}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
{digestStats.map((item) => (
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/78 px-4 py-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-[var(--title-color)]">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
<div class="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
||||
{digestStats.map((item) => (
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/78 px-4 py-3">
|
||||
<div class="text-[11px] uppercase tracking-[0.2em] text-[var(--text-tertiary)]">{item.label}</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-[var(--title-color)]">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-[24px] border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/86 p-5 shadow-[0_18px_42px_rgba(15,23,42,0.06)]">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{articleCopy.sourceTitle}
|
||||
</h3>
|
||||
<dl class="mt-4 space-y-4 text-sm">
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.updated}</dt>
|
||||
<dd class="mt-1 text-[var(--title-color)]">{updatedAtLabel}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.category}</dt>
|
||||
<dd class="mt-1">
|
||||
<a
|
||||
href={buildCategoryUrl(post.category)}
|
||||
class="text-[var(--title-color)] underline decoration-dotted underline-offset-4 hover:text-[var(--primary)]"
|
||||
>
|
||||
{post.category}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.canonical}</dt>
|
||||
<dd class="mt-1 break-all font-mono text-xs leading-6 text-[var(--title-color)]">{canonicalUrl}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.keywords}</dt>
|
||||
<dd class="mt-2 flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<a
|
||||
href={buildTagUrl(tag)}
|
||||
class="terminal-chip terminal-chip--accent px-2.5 py-1 text-xs"
|
||||
style={getAccentVars(getTagTheme(tag))}
|
||||
>
|
||||
<i class="fas fa-hashtag text-[10px]"></i>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{articleHighlights.length > 0 && (
|
||||
<div class="rounded-[24px] border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/86 p-5 shadow-[0_18px_42px_rgba(15,23,42,0.06)]">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{articleCopy.sourceTitle}
|
||||
{articleCopy.highlightsTitle}
|
||||
</h3>
|
||||
<dl class="mt-4 space-y-4 text-sm">
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.updated}</dt>
|
||||
<dd class="mt-1 text-[var(--title-color)]">{updatedAtLabel}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.category}</dt>
|
||||
<dd class="mt-1">
|
||||
<a
|
||||
href={buildCategoryUrl(post.category)}
|
||||
class="text-[var(--title-color)] underline decoration-dotted underline-offset-4 hover:text-[var(--primary)]"
|
||||
>
|
||||
{post.category}
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.canonical}</dt>
|
||||
<dd class="mt-1 break-all font-mono text-xs leading-6 text-[var(--title-color)]">{canonicalUrl}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-[var(--text-tertiary)]">{articleCopy.keywords}</dt>
|
||||
<dd class="mt-2 flex flex-wrap gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<a
|
||||
href={buildTagUrl(tag)}
|
||||
class="terminal-chip terminal-chip--accent px-2.5 py-1 text-xs"
|
||||
style={getAccentVars(getTagTheme(tag))}
|
||||
>
|
||||
<i class="fas fa-hashtag text-[10px]"></i>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{articleFaqs.length > 0 && (
|
||||
<div class="rounded-[24px] border border-[var(--border-color)]/75 bg-[var(--terminal-bg)]/86 p-5 shadow-[0_18px_42px_rgba(15,23,42,0.06)]">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
|
||||
{articleCopy.faqTitle}
|
||||
</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
{articleFaqs.map((item) => (
|
||||
<div class="rounded-2xl border border-[var(--border-color)]/80 bg-[var(--bg)]/58 px-4 py-3">
|
||||
<p class="font-semibold text-[var(--title-color)]">{item.question}</p>
|
||||
<p class="mt-2 text-sm leading-7 text-[var(--text-secondary)]">{item.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-4 space-y-3">
|
||||
{articleHighlights.map((item, index) => (
|
||||
<div class="flex items-start gap-3 rounded-2xl border border-[var(--border-color)]/80 bg-[var(--bg)]/58 px-4 py-3">
|
||||
<span class="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--primary)]/10 text-[11px] font-semibold text-[var(--primary)]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<p class="text-sm leading-7 text-[var(--title-color)]">{item}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -979,6 +993,7 @@ const breadcrumbJsonLd = {
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
analyticsEndpoint,
|
||||
postSlug: post.slug,
|
||||
articleDigestClipboardText,
|
||||
articleDigestShareText,
|
||||
@@ -989,7 +1004,7 @@ const breadcrumbJsonLd = {
|
||||
}}
|
||||
>
|
||||
(() => {
|
||||
const endpoint = '/api/analytics/content';
|
||||
const endpoint = analyticsEndpoint;
|
||||
const sessionStorageKey = `termi:content-session:${postSlug}`;
|
||||
const startedAt = Date.now();
|
||||
let sentPageView = false;
|
||||
|
||||
Reference in New Issue
Block a user