外链跳转

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
+1
View File
@@ -143,6 +143,7 @@ const currentYear = new Date().getFullYear();
<script is:inline src="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script>
<script is:inline src="/js/random.js"></script>
<script is:inline src="/js/search/typesense-search.js"></script>
<script is:inline src="/js/external-links.js"></script>
<script is:inline>
(() => {
const storageKey = 'site-theme';
+11
View File
@@ -0,0 +1,11 @@
export const externalLinkWhitelist = [
'blog.biss.click',
'biss.click',
'github.com',
'astro.build',
'edgeone.ai',
'localhost',
'127.0.0.1',
];
export const externalLinkWaitSeconds = 10;
+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>
+159
View File
@@ -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;