feat: update tag and timeline share panel copy for clarity and conciseness
Some checks failed
docker-images / resolve-build-targets (push) Successful in 7s
ui-regression / playwright-regression (push) Failing after 13m4s
docker-images / build-and-push (admin) (push) Successful in 1m17s
docker-images / build-and-push (backend) (push) Successful in 28m13s
docker-images / build-and-push (frontend) (push) Successful in 47s
docker-images / submit-indexnow (push) Successful in 13s

style: enhance global CSS for better responsiveness of terminal chips and navigation pills

test: remove inline subscription test and add maintenance mode access code test

feat: implement media library picker dialog for selecting images from the media library

feat: add media URL controls for uploading and managing media assets

feat: add migration for music_enabled and maintenance_mode settings in site settings

feat: implement maintenance mode functionality with access control

feat: create maintenance page with access code input and error handling

chore: add TypeScript declaration for QR code module
This commit is contained in:
2026-04-02 23:05:49 +08:00
parent 6a50dd478c
commit 9665c933b5
94 changed files with 5266 additions and 1612 deletions

View File

@@ -92,20 +92,20 @@ const articleMarkdown = contentText.replace(/^#\s+.+\r?\n+/, '');
const paragraphCommentsEnabled = siteSettings.comments.paragraphsEnabled;
const articleCopy = isEnglish
? {
digestBadge: 'featured digest',
digestKicker: 'ai digest',
digestTitle: 'AI / search summary',
digestBadge: 'quick brief',
digestKicker: 'reading preview',
digestTitle: 'Read this first',
digestDescription:
'This block exposes a compact summary, key takeaways, and canonical follow-up paths for AI search and human skimming.',
'A short overview of the article so readers can quickly grasp the key points before sharing or saving it.',
highlightsTitle: 'Key takeaways',
faqTitle: 'Quick FAQ',
sourceTitle: 'Canonical source signals',
sourceTitle: 'Page details',
readTime: 'Read time',
insightCount: 'Key points',
faqCount: 'FAQ',
updated: 'Updated',
category: 'Category',
canonical: 'Canonical',
canonical: 'Permalink',
keywords: 'Keywords',
copySummary: 'Copy digest',
copySuccess: 'Digest copied',
@@ -114,39 +114,39 @@ const articleCopy = isEnglish
shareSuccess: 'Share panel opened',
shareFallback: 'Share text copied',
shareFailed: 'Share failed',
shareChannelsTitle: 'Quick share',
shareChannelsTitle: 'Share options',
shareChannelsDescription:
'Push this article to social channels with a shorter path, so people and AI search tools can pick up the canonical link faster.',
'Copy the overview or permalink, or continue sharing through the channels below.',
shareToX: 'Share to X',
shareToTelegram: 'Share to Telegram',
shareToWeChat: 'WeChat QR',
qrModalTitle: 'WeChat scan share',
qrModalDescription: 'Scan this local QR code in WeChat to open the canonical article URL on mobile.',
qrModalHint: 'Prefer sharing the canonical link so users and AI engines can fold signals back to one source.',
shareToWeChat: 'WeChat scan',
qrModalTitle: 'Scan with WeChat',
qrModalDescription: 'Scan this QR code in WeChat to continue reading the article on mobile.',
qrModalHint: 'When you want to send the article to someone else, copying the permalink below is usually the easiest option.',
downloadQr: 'Download QR',
downloadQrStarted: 'QR download started',
qrOpened: 'WeChat QR ready',
floatingToolsTitle: 'Digest tools',
floatingToolsTitle: 'Quick actions',
copyPermalinkSuccess: 'Permalink copied',
copyPermalinkFailed: 'Permalink copy failed',
toastSuccessTitle: 'Done',
toastErrorTitle: 'Action failed',
toastInfoTitle: 'Share ready',
toastInfoTitle: 'Ready',
}
: {
digestBadge: '精选摘要',
digestKicker: 'ai digest',
digestTitle: 'AI / 搜索摘要',
digestDescription: '这块内容会把页面结论、重点摘录和规范入口显式写出来,方便 AI 搜索和用户快速理解。',
digestBadge: '文章导读',
digestKicker: '阅读前速览',
digestTitle: '先看重点',
digestDescription: '先用几句话帮你抓住这篇文章的重点,方便快速浏览、收藏或转发。',
highlightsTitle: '关键信息',
faqTitle: '快速问答',
sourceTitle: '规范来源信号',
sourceTitle: '页面信息',
readTime: '阅读时长',
insightCount: '重点条数',
faqCount: '问答条数',
updated: '最近更新',
category: '归档分类',
canonical: '规范地址',
canonical: '固定链接',
keywords: '关键词',
copySummary: '复制摘要',
copySuccess: '摘要已复制',
@@ -155,23 +155,23 @@ const articleCopy = isEnglish
shareSuccess: '已打开分享面板',
shareFallback: '分享文案已复制',
shareFailed: '分享失败',
shareChannelsTitle: '快速分发',
shareChannelsDescription: '用更短路径把这篇内容发到社交渠道,方便二次传播和 AI 引用回链。',
shareChannelsTitle: '分享方式',
shareChannelsDescription: '可以直接复制摘要、固定链接,或通过常用渠道继续转发。',
shareToX: '分享到 X',
shareToTelegram: '分享到 Telegram',
shareToWeChat: '微信扫',
qrModalTitle: '微信扫码分享',
qrModalDescription: '使用本地生成的二维码,在微信扫一扫,就能直接打开这篇文章的规范链接。',
qrModalHint: '尽量分享规范地址,方便用户回访,也方便 AI 搜索把信号聚合回同一篇内容。',
shareToWeChat: '微信扫一扫',
qrModalTitle: '微信扫一扫',
qrModalDescription: '微信扫一扫,就能在手机上继续阅读这篇文章。',
qrModalHint: '发给别人时,优先复制固定链接,对方打开会更方便。',
downloadQr: '下载二维码',
downloadQrStarted: '二维码开始下载',
qrOpened: '微信二维码已打开',
floatingToolsTitle: '摘要工具',
floatingToolsTitle: '快捷操作',
copyPermalinkSuccess: '固定链接已复制',
copyPermalinkFailed: '固定链接复制失败',
toastSuccessTitle: '操作完成',
toastErrorTitle: '操作失败',
toastInfoTitle: '分享渠道已就绪',
toastInfoTitle: '已准备好',
};
const markdownProcessor = await createMarkdownProcessor();
@@ -236,7 +236,7 @@ if (wechatShareQrEnabled) {
wechatShareQrSvg = await QRCode.toString(canonicalUrl, {
type: 'svg',
margin: 1,
width: 220,
width: 240,
color: {
dark: '#111827',
light: '#ffffff',
@@ -244,7 +244,7 @@ if (wechatShareQrEnabled) {
});
wechatShareQrPngDataUrl = await QRCode.toDataURL(canonicalUrl, {
margin: 1,
width: 360,
width: 420,
color: {
dark: '#111827',
light: '#ffffff',
@@ -434,55 +434,7 @@ const breadcrumbJsonLd = {
<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="pointer-events-none absolute right-4 top-4 z-10 hidden xl:block">
<div class="pointer-events-auto rounded-2xl border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/88 px-2 py-2 shadow-[0_10px_28px_rgba(15,23,42,0.08)] backdrop-blur">
<div class="mb-2 px-2 text-[10px] font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
{articleCopy.floatingToolsTitle}
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="digest-copy"
title={articleCopy.copySummary}
aria-label={articleCopy.copySummary}
>
<i class="fas fa-copy text-sm"></i>
</button>
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="digest-share"
title={articleCopy.shareSummary}
aria-label={articleCopy.shareSummary}
>
<i class="fas fa-share-nodes text-sm"></i>
</button>
{wechatShareQrEnabled && wechatShareQrSvg && (
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="wechat-qr"
title={articleCopy.shareToWeChat}
aria-label={articleCopy.shareToWeChat}
>
<i class="fab fa-weixin text-sm"></i>
</button>
)}
<button
type="button"
class="flex h-10 w-10 items-center justify-center rounded-xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="permalink-copy"
title={t('common.copyPermalink')}
aria-label={t('common.copyPermalink')}
>
<i class="fas fa-link text-sm"></i>
</button>
</div>
</div>
</div>
<div class="relative grid gap-5 lg:grid-cols-[minmax(0,1.4fr)_minmax(19rem,0.95fr)]">
<div class="relative grid gap-5 xl:grid-cols-[minmax(0,1.4fr)_minmax(19rem,0.95fr)]">
<div class="space-y-5">
<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)]">
@@ -490,7 +442,7 @@ const breadcrumbJsonLd = {
{articleCopy.digestBadge}
</span>
<span class="terminal-kicker">
<i class="fas fa-robot"></i>
<i class="fas fa-book-open"></i>
{articleCopy.digestKicker}
</span>
</div>
@@ -591,25 +543,6 @@ const breadcrumbJsonLd = {
))}
</div>
{articleHighlights.length > 0 && (
<div class="space-y-3">
<h3 class="text-sm font-semibold uppercase tracking-[0.22em] text-[var(--text-tertiary)]">
{articleCopy.highlightsTitle}
</h3>
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{articleHighlights.map((item, index) => (
<div class="rounded-2xl border border-[var(--border-color)]/80 bg-[var(--terminal-bg)]/80 p-4 shadow-[0_10px_30px_rgba(15,23,42,0.04)]">
<div class="flex items-start gap-3">
<span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-[var(--primary)]/12 text-sm font-semibold text-[var(--primary)]">
{String(index + 1).padStart(2, '0')}
</span>
<p class="text-sm leading-7 text-[var(--text-secondary)]">{item}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
<div class="space-y-4">
@@ -867,7 +800,60 @@ const breadcrumbJsonLd = {
</section>
</div>
<TableOfContents />
<TableOfContents>
<div slot="before-nav" class="terminal-panel-muted space-y-3">
<span class="terminal-kicker">
<i class="fas fa-bolt"></i>
{articleCopy.floatingToolsTitle}
</span>
<div
class:list={[
'grid gap-2',
wechatShareQrEnabled && wechatShareQrSvg ? 'grid-cols-4' : 'grid-cols-3',
]}
>
<button
type="button"
class="flex h-11 w-full items-center justify-center rounded-2xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="digest-copy"
title={articleCopy.copySummary}
aria-label={articleCopy.copySummary}
>
<i class="fas fa-copy text-sm"></i>
</button>
<button
type="button"
class="flex h-11 w-full items-center justify-center rounded-2xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="digest-share"
title={articleCopy.shareSummary}
aria-label={articleCopy.shareSummary}
>
<i class="fas fa-share-nodes text-sm"></i>
</button>
{wechatShareQrEnabled && wechatShareQrSvg && (
<button
type="button"
class="flex h-11 w-full items-center justify-center rounded-2xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="wechat-qr"
title={articleCopy.shareToWeChat}
aria-label={articleCopy.shareToWeChat}
>
<i class="fab fa-weixin text-sm"></i>
</button>
)}
<button
type="button"
class="flex h-11 w-full items-center justify-center rounded-2xl border border-[var(--border-color)]/70 bg-[var(--bg)]/75 text-[var(--text-secondary)] transition hover:border-[var(--primary)] hover:text-[var(--primary)]"
data-article-floating-action="permalink-copy"
title={t('common.copyPermalink')}
aria-label={t('common.copyPermalink')}
>
<i class="fas fa-link text-sm"></i>
</button>
</div>
</div>
</TableOfContents>
</div>
</div>
@@ -878,7 +864,7 @@ const breadcrumbJsonLd = {
aria-hidden="true"
>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="w-full max-w-3xl rounded-[30px] border border-[var(--border-color)] bg-[linear-gradient(180deg,rgba(var(--bg-rgb),0.98),rgba(var(--bg-rgb),0.92))] p-5 shadow-[0_24px_80px_rgba(15,23,42,0.28)] sm:p-6">
<div class="w-full max-w-4xl rounded-[32px] border border-[var(--border-color)]/70 bg-[var(--terminal-bg)] p-5 shadow-[0_30px_90px_rgba(15,23,42,0.36)] sm:p-7">
<div class="flex items-start justify-between gap-4">
<div class="space-y-2">
<span class="terminal-kicker">
@@ -903,24 +889,25 @@ const breadcrumbJsonLd = {
</button>
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-[240px_minmax(0,1fr)]">
<div class="mx-auto w-full max-w-[240px] rounded-[28px] border border-[var(--border-color)]/80 bg-white p-4 shadow-[0_18px_45px_rgba(15,23,42,0.12)]">
<div class="mt-6 grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
<div class="mx-auto w-full max-w-[260px] rounded-[28px] border border-[var(--border-color)]/80 bg-white p-5 shadow-[0_18px_45px_rgba(15,23,42,0.12)]">
<div class="overflow-hidden rounded-2xl" set:html={wechatShareQrSvg}></div>
</div>
<div class="space-y-4">
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/82 p-4">
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--header-bg)] p-5">
<div class="text-[11px] uppercase tracking-[0.2em] text-[var(--text-tertiary)]">
{articleCopy.canonical}
</div>
<p class="mt-2 break-all font-mono text-xs leading-6 text-[var(--title-color)]">{canonicalUrl}</p>
<p class="mt-3 break-all font-mono text-sm leading-7 text-[var(--title-color)]">{canonicalUrl}</p>
</div>
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--terminal-bg)]/72 p-4">
<div class="rounded-2xl border border-[var(--border-color)]/70 bg-[var(--header-bg)] p-5">
<div class="text-[11px] uppercase tracking-[0.2em] text-[var(--text-tertiary)]">
{articleCopy.digestTitle}
</div>
<p class="mt-2 text-sm font-semibold leading-7 text-[var(--title-color)]">{post.title}</p>
<p class="mt-3 text-base font-semibold leading-7 text-[var(--title-color)]">{post.title}</p>
<p class="mt-3 text-sm leading-7 text-[var(--text-secondary)]">{articleSynopsis}</p>
<p class="mt-3 text-sm leading-7 text-[var(--text-secondary)]">{articleCopy.qrModalHint}</p>
</div>