外链跳转

This commit is contained in:
2026-06-18 18:42:26 +08:00 Unverified
parent b066a94ce3
commit 41854a128d
5 changed files with 346 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { externalLinkWaitSeconds, externalLinkWhitelist } from '@/lib/external-link-whitelist';
const whitelist = externalLinkWhitelist.map((domain) => domain.toLowerCase());
---
<BaseLayout title="外链跳转" description="即将离开本站,请确认访问的外部链接。">
<section class="external-redirect" data-external-redirect>
<div class="external-redirect-panel">
<p class="external-redirect-eyebrow" data-status-label>外部链接</p>
<h1 data-title>正在检查目标链接</h1>
<p class="external-redirect-copy" data-message>请稍候,正在读取跳转地址。</p>
<div class="external-redirect-target" data-target-wrap hidden>
<span>目标地址</span>
<code data-target-url></code>
</div>
<div class="external-redirect-countdown" data-countdown-wrap hidden>
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle cx="60" cy="60" r="52"></circle>
<circle cx="60" cy="60" r="52" data-countdown-ring></circle>
</svg>
<strong data-countdown>{externalLinkWaitSeconds}</strong>
</div>
<div class="external-redirect-actions">
<a class="external-redirect-button is-primary" href="/" data-continue hidden>立即访问</a>
<a class="external-redirect-button" href="/">返回首页</a>
</div>
</div>
</section>
<script
is:inline
define:vars={{
waitSeconds: externalLinkWaitSeconds,
whitelist,
}}
>
(() => {
const params = new URLSearchParams(window.location.search);
const rawUrl = params.get('url') || params.get('target') || params.get('to') || '';
const title = document.querySelector('[data-title]');
const message = document.querySelector('[data-message]');
const statusLabel = document.querySelector('[data-status-label]');
const targetWrap = document.querySelector('[data-target-wrap]');
const targetUrl = document.querySelector('[data-target-url]');
const countdownWrap = document.querySelector('[data-countdown-wrap]');
const countdown = document.querySelector('[data-countdown]');
const ring = document.querySelector('[data-countdown-ring]');
const continueLink = document.querySelector('[data-continue]');
const circumference = 2 * Math.PI * 52;
function setText(node, text) {
if (node) node.textContent = text;
}
function isWhitelisted(hostname) {
const host = hostname.toLowerCase();
return whitelist.some((domain) => {
const normalized = String(domain).replace(/^\*\./, '').toLowerCase();
return host === normalized || host.endsWith(`.${normalized}`);
});
}
function showError(text) {
setText(statusLabel, '无法跳转');
setText(title, '链接无效');
setText(message, text);
}
let destination;
try {
destination = new URL(rawUrl);
} catch {
showError('没有找到有效的目标地址,请返回上一页重新打开链接。');
return;
}
if (!['http:', 'https:'].includes(destination.protocol)) {
showError('仅支持跳转到 HTTP 或 HTTPS 链接。');
return;
}
targetWrap.hidden = false;
continueLink.hidden = false;
continueLink.href = destination.href;
targetUrl.textContent = destination.href;
if (isWhitelisted(destination.hostname)) {
setText(statusLabel, '白名单链接');
setText(title, '即将前往可信站点');
setText(message, '该域名已在白名单中,将立即为你打开。');
window.setTimeout(() => {
window.location.replace(destination.href);
}, 300);
return;
}
let remaining = Number(waitSeconds);
setText(statusLabel, '请确认');
setText(title, '即将离开本站');
setText(message, `目标域名不在白名单中,${remaining} 秒后将自动跳转。`);
countdownWrap.hidden = false;
countdown.textContent = remaining;
if (ring) {
ring.style.strokeDasharray = `${circumference}`;
ring.style.strokeDashoffset = '0';
}
const timer = window.setInterval(() => {
remaining -= 1;
countdown.textContent = remaining;
setText(message, `目标域名不在白名单中,${remaining} 秒后将自动跳转。`);
if (ring) {
ring.style.strokeDashoffset = String(circumference * (1 - remaining / waitSeconds));
}
if (remaining <= 0) {
window.clearInterval(timer);
window.location.replace(destination.href);
}
}, 1000);
})();
</script>
</BaseLayout>