167 lines
6.0 KiB
Plaintext
167 lines
6.0 KiB
Plaintext
---
|
|
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>
|
|
<button class="external-redirect-button" type="button" data-cancel hidden>取消跳转</button>
|
|
<a class="external-redirect-button" href="/" data-back>返回上一页</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 rawFrom = params.get('from') || '';
|
|
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 backLink = document.querySelector('[data-back]');
|
|
const cancelButton = document.querySelector('[data-cancel]');
|
|
const circumference = 2 * Math.PI * 52;
|
|
let timer = 0;
|
|
let hasCanceled = false;
|
|
|
|
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 returnUrl = '/';
|
|
try {
|
|
const fromUrl = new URL(rawFrom, window.location.origin);
|
|
if (fromUrl.origin === window.location.origin && fromUrl.pathname !== window.location.pathname) {
|
|
returnUrl = `${fromUrl.pathname}${fromUrl.search}${fromUrl.hash}`;
|
|
}
|
|
} catch {
|
|
returnUrl = '/';
|
|
}
|
|
|
|
if (backLink) backLink.href = returnUrl;
|
|
|
|
backLink?.addEventListener('click', (event) => {
|
|
if (window.history.length <= 1) return;
|
|
event.preventDefault();
|
|
window.history.back();
|
|
});
|
|
|
|
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;
|
|
cancelButton.hidden = false;
|
|
countdown.textContent = remaining;
|
|
|
|
if (ring) {
|
|
ring.style.strokeDasharray = `${circumference}`;
|
|
ring.style.strokeDashoffset = '0';
|
|
}
|
|
|
|
cancelButton?.addEventListener('click', () => {
|
|
hasCanceled = true;
|
|
window.clearInterval(timer);
|
|
cancelButton.hidden = true;
|
|
countdownWrap.hidden = true;
|
|
setText(statusLabel, '已取消');
|
|
setText(title, '已取消自动跳转');
|
|
setText(message, '自动跳转已停止,你可以继续访问目标地址,或返回上一页。');
|
|
});
|
|
|
|
timer = window.setInterval(() => {
|
|
if (hasCanceled) return;
|
|
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>
|