当网站后端暂时无法访问时,可以让 Nginx 自动显示一个静态备用页面,提示用户检查网络,并提供售后信息。
一、准备文件
将以下两个文件上传到宝塔网站根目录:
text
/www/wwwroot/test.zhangsan.cool/offline.html
/www/wwwroot/test.zhangsan.cool/sw.js
文件说明(供参考,具体自行修改):
offline.html:网站无法访问时显示的备用页面。
bash
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#f5f7fb">
<meta name="description" content="网站备用入口和联系方式。">
<title>网站暂时无法访问|备用入口</title>
<style>
:root { color-scheme: light; --ink:#182235; --muted:#687386; --blue:#2563eb; --line:#e2e7f0; --surface:#fff; }
* { box-sizing:border-box; }
html, body { margin:0; min-height:100%; }
body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",Arial,sans-serif; color:var(--ink); background:#f5f7fb; }
.shell { min-height:100vh; padding:24px 16px calc(28px + env(safe-area-inset-bottom)); display:flex; align-items:center; justify-content:center; }
.page { width:min(100%,620px); }
.card { background:var(--surface); border:1px solid var(--line); border-radius:18px; padding:28px 20px 22px; box-shadow:0 12px 35px rgba(30,45,75,.07); }
h1 { margin:0; font-size:clamp(28px,7vw,42px); line-height:1.2; letter-spacing:-.03em; }
.status { display:inline-flex; align-items:center; gap:7px; margin-bottom:14px; color:#a16207; font-size:13px; font-weight:700; }
.status-dot { width:8px; height:8px; border-radius:50%; background:#eab308; box-shadow:0 0 0 4px #fef3c7; }
.intro { margin:10px 0 22px; color:var(--muted); font-size:15px; line-height:1.65; }
.nav-link { display:flex; align-items:center; justify-content:center; gap:9px; min-height:54px; padding:14px 18px; border-radius:12px; color:#fff; background:var(--blue); text-decoration:none; font-size:17px; font-weight:700; box-shadow:0 8px 16px rgba(37,99,235,.2); }
.nav-link:hover, .nav-link:focus-visible { background:#1d4ed8; }
.nav-link svg { width:19px; height:19px; }
.section-title { margin:26px 0 10px; font-size:17px; }
.contacts { display:grid; gap:10px; }
.contact { display:flex; align-items:center; gap:12px; min-width:0; padding:14px; border:1px solid var(--line); border-radius:12px; }
.contact-icon { flex:0 0 36px; width:36px; height:36px; display:grid; place-items:center; border-radius:10px; color:var(--blue); background:#eef4ff; }
.contact-icon svg { width:20px; height:20px; }
.contact-copy { flex:1; min-width:0; }
.label { margin:0 0 3px; color:var(--muted); font-size:13px; }
.value { margin:0; font-size:19px; font-weight:700; overflow-wrap:anywhere; }
.copy { flex:0 0 auto; min-height:40px; padding:0 14px; border:1px solid #bfd2f6; border-radius:9px; color:var(--blue); background:#fff; font:inherit; font-size:14px; font-weight:700; cursor:pointer; }
.copy:hover, .copy:focus-visible { background:#eef4ff; }
.copy.copied { border-color:#b7dfc9; color:#147a4b; background:#edf9f2; }
.contact--featured { border-color:#bfd2f6; background:#f8fbff; }
:is(.nav-link, .copy):focus-visible { outline:3px solid rgba(37,99,235,.28); outline-offset:3px; }
.note { margin:18px 0 0; color:#8a94a5; font-size:12px; line-height:1.6; text-align:center; }
.toast { position:fixed; left:50%; bottom:calc(22px + env(safe-area-inset-bottom)); transform:translate(-50%,16px); opacity:0; pointer-events:none; padding:9px 13px; border-radius:9px; color:#fff; background:#1d293d; font-size:13px; transition:opacity .18s,transform .18s; }
.toast.show { opacity:1; transform:translate(-50%,0); }
@media (max-width:599px) { .shell { padding:14px 12px calc(20px + env(safe-area-inset-bottom)); } .card { padding:18px 14px 22px; border-radius:15px; } .intro { margin-bottom:18px; } .section-title { margin-top:22px; } }
@media (min-width:760px) { .card { padding:40px 42px 30px; } .contacts { grid-template-columns:1fr 1fr; } }
@media (prefers-reduced-motion: reduce) { .toast { transition:none; } }
</style>
</head>
<body>
<main class="shell">
<section class="page card" aria-labelledby="title">
<div class="status"><span class="status-dot" aria-hidden="true"></span>当前连接可能不稳定</div>
<h1 id="title">网站暂时无法访问</h1>
<p class="intro">请先检查网络连接并刷新页面;如果仍无法访问,可通过下方联系方式获取帮助。</p>
<a class="nav-link" href="https://vlink.cc/uu5wok0k" target="_blank" rel="noopener noreferrer">打开最新网址<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M14 5h5v5M19 5l-8 8"/><path d="M18 13v5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg></a>
<h2 class="section-title">备用联系方式</h2>
<div class="contacts">
<div class="contact contact--featured"><span class="contact-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 7h10v8H7z"/><path d="M7 10H4v5h3M17 10h3v5h-3M9 18h6M12 15v3"/></svg></span><div class="contact-copy"><p class="label">售后QQ群 · 进群获取最新资讯</p><p class="value">815440815</p></div><button class="copy" type="button" data-copy="815440815" data-default-label="复制" aria-label="复制QQ群号 815440815">复制</button></div>
<div class="contact"><span class="contact-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 11.5a7.5 7.5 0 0 1-11.8 6.1L4 19l1.5-3.9A7.5 7.5 0 1 1 20 11.5Z"/><path d="M9 10.5c.6 1.3 1.7 2.4 3 3l1-.8 1.5.7-.3 1.3c-.2.6-.8.8-1.4.6a8.3 8.3 0 0 1-5.1-5.1c-.2-.6 0-1.2.6-1.4l1.3-.3.7 1.5-.8 1Z"/></svg></span><div class="contact-copy"><p class="label">售后微信</p><p class="value">15682448658</p></div><button class="copy" type="button" data-copy="15682448658" data-default-label="复制" aria-label="复制售后微信号 15682448658">复制</button></div>
</div>
<p class="note">建议收藏本页,方便下次快速找到入口。</p>
</section>
</main>
<div class="toast" role="status" aria-live="polite"></div>
<script>
(() => {
const toast = document.querySelector('.toast');
let timer;
const show = (message) => { toast.textContent = message; toast.classList.add('show'); clearTimeout(timer); timer = setTimeout(() => toast.classList.remove('show'), 1800); };
const fallbackCopy = (text) => { const area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', ''); area.style.position = 'fixed'; area.style.opacity = '0'; document.body.appendChild(area); area.select(); const ok = document.execCommand('copy'); area.remove(); return ok; };
document.querySelectorAll('[data-copy]').forEach((button) => button.addEventListener('click', async () => { const text = button.dataset.copy; try { if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text); else if (!fallbackCopy(text)) throw new Error('copy failed'); button.textContent = '已复制 ✓'; button.classList.add('copied'); clearTimeout(button._resetTimer); button._resetTimer = setTimeout(() => { button.textContent = button.dataset.defaultLabel; button.classList.remove('copied'); }, 1800); show('已复制:' + text); } catch { show('复制失败,请手动复制'); } }));
})();
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((registration) => {
console.log('Service Worker 注册成功:', registration.scope);
return registration.update();
})
.catch((error) => console.error('Service Worker 注册失败:', error));
});
} else {
console.warn('当前浏览器不支持 Service Worker');
}
</script>
</body>
</html>
sw.js:负责缓存备用页面,并在断网时返回缓存内容。
bash
const CACHE_NAME = 'offline-contact-v5';
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.add(OFFLINE_URL))
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
))
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (
event.request.method !== 'GET' ||
event.request.mode !== 'navigate' ||
url.origin !== self.location.origin ||
url.pathname.startsWith('/service-api/')
) return;
event.respondWith(
fetch(event.request)
.then((response) => {
if (![502, 503, 504].includes(response.status)) return response;
return caches.match(OFFLINE_URL).then((cached) => cached || response);
})
.catch(() => caches.match(OFFLINE_URL).then((cached) => cached || Response.error()))
);
});
二、配置 HTTPS
Service Worker 必须运行在 HTTPS 环境下。
在宝塔中进入:
text
网站 → 你的域名 → SSL
申请并启用 SSL,之后使用下面的地址访问网站:
text
https://你的域名/
不要使用普通 HTTP 或服务器 IP 地址测试。
三、添加宝塔自定义配置
进入:
text
网站 → 你的域名 → 配置文件 → 自定义配置文件
不要修改"主配置文件",将下面完整内容粘贴到"自定义配置文件"中:
nginx
location = /sw.js {
root /www/wwwroot/test.zhangsan.cool;
default_type application/javascript;
add_header Cache-Control "no-cache" always;
add_header Service-Worker-Allowed "/" always;
try_files $uri =404;
}
location = /offline.html {
root /www/wwwroot/test.zhangsan.cool;
default_type text/html;
add_header Cache-Control "no-cache" always;
try_files $uri =404;
}
error_page 502 503 504 =200 /offline.html;
proxy_set_header Accept-Encoding "";
sub_filter_types text/html;
sub_filter_once on;
sub_filter '</body>' '<script>if("serviceWorker" in navigator){window.addEventListener("load",function(){navigator.serviceWorker.register("/sw.js",{scope:"/"}).then(function(r){console.log("Service Worker 注册成功",r.scope);return r.update();}).catch(function(e){console.error("Service Worker 注册失败",e);});});}</script></body>';
其中反向代理后端使用本机地址:
text
127.0.0.1:8080
如果宝塔已经自动生成了反向代理配置,不要重复添加第二个 location ^~ /,只添加上面的自定义内容即可。
四、修改页面后更新缓存
编辑修改 offline.html 或 sw.js 后,需要修改 sw.js 中的缓存版本号,例如:
js
const CACHE_NAME = 'offline-contact-v4';
然后重新上传文件,联网刷新页面,再测试断网。