From 41854a128d7249166c4d3b88620e53890588e5e3 Mon Sep 17 00:00:00 2001 From: biss Date: Thu, 18 Jun 2026 18:42:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E9=93=BE=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/BaseLayout.astro | 1 + src/lib/external-link-whitelist.ts | 11 ++ src/pages/go.astro | 130 +++++++++++++++++++++++ src/styles/site.css | 159 +++++++++++++++++++++++++++++ static/js/external-links.js | 45 ++++++++ 5 files changed, 346 insertions(+) create mode 100644 src/lib/external-link-whitelist.ts create mode 100644 src/pages/go.astro create mode 100644 static/js/external-links.js diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 03a6f07..b9591bf 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -143,6 +143,7 @@ const currentYear = new Date().getFullYear(); + + diff --git a/src/styles/site.css b/src/styles/site.css index 926a1f7..ca4f5e3 100644 --- a/src/styles/site.css +++ b/src/styles/site.css @@ -2243,6 +2243,165 @@ meting-js { border-color: rgba(174, 205, 197, 0.2); } +.external-redirect { + width: min(760px, calc(100% - (var(--page-gutter) * 2))); + min-height: calc(100vh - 240px); + margin: 42px auto 80px; + display: grid; + place-items: center; +} + +.external-redirect-panel { + width: 100%; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.58); + border-radius: 8px; + padding: clamp(26px, 6vw, 48px); + background: + radial-gradient(circle at 15% 0%, rgba(255, 255, 255, 0.74), transparent 36%), + linear-gradient(145deg, rgba(255, 255, 255, 0.7), rgba(226, 248, 241, 0.42)), + rgba(255, 253, 248, 0.78); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.82), + 0 24px 64px rgba(44, 55, 63, 0.16); + backdrop-filter: blur(18px) saturate(165%); + -webkit-backdrop-filter: blur(18px) saturate(165%); +} + +.external-redirect-eyebrow { + margin: 0 0 10px; + color: var(--accent); + font-size: 0.88rem; + font-weight: 800; +} + +.external-redirect h1 { + margin: 0; + color: #20242a; + font-size: clamp(2rem, 6vw, 3.8rem); + line-height: 1.08; +} + +.external-redirect-copy { + max-width: 620px; + margin: 16px 0 0; + color: var(--muted); + font-size: 1rem; +} + +.external-redirect-target { + display: grid; + gap: 8px; + margin-top: 24px; + border: 1px solid rgba(15, 118, 110, 0.16); + border-radius: 8px; + padding: 14px 16px; + background: rgba(236, 253, 245, 0.64); +} + +.external-redirect-target span { + color: var(--accent); + font-size: 0.84rem; + font-weight: 800; +} + +.external-redirect-target code { + overflow-wrap: anywhere; + color: #33433f; + font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; + font-size: 0.92rem; +} + +.external-redirect-countdown { + position: relative; + width: 112px; + height: 112px; + margin-top: 26px; +} + +.external-redirect-countdown svg { + width: 112px; + height: 112px; + transform: rotate(-90deg); +} + +.external-redirect-countdown circle { + fill: none; + stroke: rgba(15, 118, 110, 0.14); + stroke-width: 10; +} + +.external-redirect-countdown circle:last-child { + stroke: #3eb8be; + stroke-linecap: round; + transition: stroke-dashoffset 0.9s linear; +} + +.external-redirect-countdown strong { + position: absolute; + inset: 0; + display: grid; + place-items: center; + color: #2f4358; + font-size: 2rem; + line-height: 1; +} + +.external-redirect-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 28px; +} + +.external-redirect-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + border: 1px solid rgba(15, 118, 110, 0.2); + border-radius: 8px; + padding: 9px 16px; + color: var(--accent); + font-weight: 800; + background: rgba(236, 253, 245, 0.68); + transition: + color 0.2s ease, + background-color 0.2s ease, + transform 0.2s ease; +} + +.external-redirect-button:hover, +.external-redirect-button.is-primary { + color: #fff; + background: #3eb8be; + transform: translateY(-1px); +} + +:root[data-theme='dark'] .external-redirect-panel { + border-color: rgba(174, 205, 197, 0.18); + background: + radial-gradient(circle at 14% 0%, rgba(114, 222, 210, 0.1), transparent 34%), + linear-gradient(145deg, rgba(28, 43, 47, 0.82), rgba(12, 22, 25, 0.72)), + rgba(17, 28, 31, 0.74); + box-shadow: var(--shadow); +} + +:root[data-theme='dark'] .external-redirect h1, +:root[data-theme='dark'] .external-redirect-countdown strong { + color: #edf7f5; +} + +:root[data-theme='dark'] .external-redirect-target, +:root[data-theme='dark'] .external-redirect-button { + border-color: rgba(114, 222, 210, 0.22); + background: rgba(114, 222, 210, 0.1); +} + +:root[data-theme='dark'] .external-redirect-target code { + color: #dce9e6; +} + .site-footer { position: relative; margin-top: 64px; diff --git a/static/js/external-links.js b/static/js/external-links.js new file mode 100644 index 0000000..3068a67 --- /dev/null +++ b/static/js/external-links.js @@ -0,0 +1,45 @@ +(() => { + const redirectPath = '/go/'; + const urlParams = new URLSearchParams(window.location.search); + + if (window.location.pathname === redirectPath || window.location.pathname === '/go') return; + if (urlParams.get('noExternalRedirect') === '1') return; + + function getExternalUrl(anchor) { + const href = anchor.getAttribute('href'); + if (!href || href.startsWith('#')) return null; + + let targetUrl; + try { + targetUrl = new URL(href, window.location.href); + } catch { + return null; + } + + if (!['http:', 'https:'].includes(targetUrl.protocol)) return null; + if (targetUrl.origin === window.location.origin) return null; + + return targetUrl.href; + } + + document.addEventListener('click', (event) => { + const anchor = event.target.closest?.('a[href]'); + if (!anchor) return; + if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + + const externalUrl = getExternalUrl(anchor); + if (!externalUrl) return; + + event.preventDefault(); + + const redirectUrl = `${redirectPath}?url=${encodeURIComponent(externalUrl)}`; + const target = anchor.getAttribute('target'); + + if (target && target !== '_self') { + window.open(redirectUrl, target, 'noopener,noreferrer'); + return; + } + + window.location.href = redirectUrl; + }); +})();