vite-plugin-sw-offline 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/sw.js +48 -20
- package/src/index.js +20 -0
- package/templates/default/offline-i18n.json +145 -0
- package/templates/default/offline.html +157 -6
package/package.json
CHANGED
package/runtime/sw.js
CHANGED
|
@@ -96,6 +96,9 @@ const NETWORK_PROBE_URL = '__NETWORK_PROBE_URL__';
|
|
|
96
96
|
/** 网络探测超时时间(毫秒,构建时注入) */
|
|
97
97
|
const NETWORK_PROBE_TIMEOUT = __SW_RT_NETWORK_PROBE_TIMEOUT__;
|
|
98
98
|
|
|
99
|
+
/** 离线页文案(构建时注入 JSON,与 offline.html 同源) */
|
|
100
|
+
const OFFLINE_I18N = __OFFLINE_I18N_INJECT__;
|
|
101
|
+
|
|
99
102
|
// ============================================
|
|
100
103
|
// 二、工具函数
|
|
101
104
|
// ============================================
|
|
@@ -216,23 +219,26 @@ async function purgeStaleApiEntries() {
|
|
|
216
219
|
}
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
function stringifyOfflineI18nForInlineScript() {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.stringify(OFFLINE_I18N).replace(/</g, '\\u003c');
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return '{}';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
219
230
|
/**
|
|
220
|
-
*
|
|
221
|
-
* 优先从缓存读取完整的 offline.html,
|
|
222
|
-
* 若缓存中也没有(极端情况),则返回一个内联兜底页面(文案与 offline.html 保持一致)
|
|
231
|
+
* SW 内联兜底离线页 HTML(无 search-bar;语言逻辑与 offline.html 一致)
|
|
223
232
|
*/
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// 注意:文案和按钮需与 offline.html 保持一致,避免用户体验割裂
|
|
232
|
-
console.warn('[SW] offline.html not in cache, using inline fallback');
|
|
233
|
-
return new Response(
|
|
233
|
+
function buildInlineOfflineFallbackHtml() {
|
|
234
|
+
const messagesJson = stringifyOfflineI18nForInlineScript();
|
|
235
|
+
const script =
|
|
236
|
+
'(function(){var M=' +
|
|
237
|
+
messagesJson +
|
|
238
|
+
';var SHORT={en:"en_US",zh:"zh_CN",ja:"ja_JP",ko:"ko_KR",ar:"ar_SA",hi:"hi_IN",pt:"pt_BR",ru:"ru_RU",th:"th_TH",tr:"tr_TR",vi:"vi_VN",es:"es_MX"};function norm(s){if(!s||typeof s!=="string")return "";s=s.trim().replace(/-/g,"_");if(s.indexOf("_")===-1)return SHORT[s.toLowerCase()]||"";var i=s.indexOf("_");return s.slice(0,i).toLowerCase()+"_"+s.slice(i+1).toUpperCase();}function resolveKey(){var u="";try{u=new URLSearchParams(location.search).get("locale")||"";}catch(e){}var st="";try{var raw=localStorage.getItem("common");if(raw){var o=JSON.parse(raw);if(o&&typeof o.locale==="string")st=o.locale;}}catch(e){}return norm(u)||norm(st)||"zh_CN";}var key=resolveKey();if(!M[key])key="zh_CN";var t=M[key]||M.zh_CN;if(!t)return;document.documentElement.setAttribute("lang",key.replace("_","-"));if(key.indexOf("ar_")===0)document.documentElement.setAttribute("dir","rtl");document.title=t.title;var el=document.getElementById("oh");if(el)el.textContent=t.heading;el=document.getElementById("ob");if(el)el.textContent=t.body;el=document.getElementById("oc");if(el)el.textContent=t.contact;el=document.getElementById("or");if(el)el.textContent=t.reload;window.__offlineContactHint=t.contactOfflineHint;})();';
|
|
239
|
+
return (
|
|
234
240
|
'<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">' +
|
|
235
|
-
'<title
|
|
241
|
+
'<title></title>' +
|
|
236
242
|
'<style>*{margin:0;padding:0;box-sizing:border-box}' +
|
|
237
243
|
'body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#131529;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 20px;color:#fff}' +
|
|
238
244
|
'.c{text-align:center;max-width:400px;width:100%}' +
|
|
@@ -245,18 +251,40 @@ async function getOfflineResponse() {
|
|
|
245
251
|
'.p{background:linear-gradient(180deg,#6591FD 0%,#3D75FF 100%);color:#fff;box-shadow:0 4px 16px rgba(74,124,255,0.3);border-radius:100px}' +
|
|
246
252
|
'</style></head>' +
|
|
247
253
|
'<body><div class="c">' +
|
|
248
|
-
'<h1
|
|
249
|
-
'<p
|
|
254
|
+
'<h1 id="oh"></h1>' +
|
|
255
|
+
'<p id="ob"></p>' +
|
|
250
256
|
'<div class="btns">' +
|
|
251
|
-
'<a class="btn s" href="javascript:void(0)" onclick="var u=localStorage.getItem(\'customerServiceUrl\');u?window.open(u,\'_blank\'):alert(\'
|
|
252
|
-
'<button class="btn p" onclick="location.reload()"
|
|
257
|
+
'<a class="btn s" href="javascript:void(0)" id="oc" onclick="var u=localStorage.getItem(\'customerServiceUrl\');u?window.open(u,\'_blank\'):alert(window.__offlineContactHint||\'\')"></a>' +
|
|
258
|
+
'<button class="btn p" type="button" id="or" onclick="location.reload()"></button>' +
|
|
253
259
|
'</div></div>' +
|
|
260
|
+
'<script>' +
|
|
261
|
+
script +
|
|
262
|
+
'</script>' +
|
|
254
263
|
'<script>window.addEventListener("online",function(){location.reload()});</script>' +
|
|
255
|
-
'</body></html>'
|
|
256
|
-
{ status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
|
264
|
+
'</body></html>'
|
|
257
265
|
);
|
|
258
266
|
}
|
|
259
267
|
|
|
268
|
+
/**
|
|
269
|
+
* 返回离线页面
|
|
270
|
+
* 优先从缓存读取完整的 offline.html,
|
|
271
|
+
* 若缓存中也没有(极端情况),则返回一个内联兜底页面(文案与 offline.html 保持一致)
|
|
272
|
+
*/
|
|
273
|
+
async function getOfflineResponse() {
|
|
274
|
+
const cache = await caches.open(CACHE_NAMES.STATIC);
|
|
275
|
+
const offlineResponse = await cache.match(OFFLINE_PAGE);
|
|
276
|
+
if (offlineResponse) {
|
|
277
|
+
return offlineResponse;
|
|
278
|
+
}
|
|
279
|
+
// 内联兜底:仅在 offline.html 完全无法从缓存获取时才使用
|
|
280
|
+
// 注意:文案和按钮需与 offline.html 保持一致,避免用户体验割裂
|
|
281
|
+
console.warn('[SW] offline.html not in cache, using inline fallback');
|
|
282
|
+
return new Response(buildInlineOfflineFallbackHtml(), {
|
|
283
|
+
status: 503,
|
|
284
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
260
288
|
/**
|
|
261
289
|
* 预缓存离线页 HTML(STATIC)+ 背景图(IMAGE),与 fetch 分发策略一致
|
|
262
290
|
*/
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
9
9
|
const RUNTIME_DIR = path.join(PKG_ROOT, 'runtime');
|
|
10
10
|
const DEFAULT_OFFLINE_HTML = path.join(PKG_ROOT, 'templates', 'default', 'offline.html');
|
|
11
|
+
const DEFAULT_OFFLINE_I18N_JSON = path.join(PKG_ROOT, 'templates', 'default', 'offline-i18n.json');
|
|
11
12
|
/** 默认离线页背景(与模板、runtime/sw.js 中 /static/offline-bg.jpg 一致) */
|
|
12
13
|
const DEFAULT_OFFLINE_BG_JPG = path.join(PKG_ROOT, 'assets', 'offline-bg.jpg');
|
|
13
14
|
|
|
@@ -43,7 +44,25 @@ function normalizeOfflineLogoPath(raw) {
|
|
|
43
44
|
return withSlash + query;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function loadOfflineI18nMessages() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(DEFAULT_OFFLINE_I18N_JSON, 'utf-8');
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.warn(LOG, 'offline-i18n.json missing or invalid:', e.message);
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function injectOfflineI18nPlaceholder(content) {
|
|
58
|
+
const messages = loadOfflineI18nMessages();
|
|
59
|
+
const raw = JSON.stringify(messages);
|
|
60
|
+
const safe = raw.replace(/</g, '\\u003c');
|
|
61
|
+
return content.replace(/__OFFLINE_I18N_INJECT__/g, safe);
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function injectOfflineHtml(content, swConfig) {
|
|
65
|
+
content = injectOfflineI18nPlaceholder(content);
|
|
47
66
|
const logo = normalizeOfflineLogoPath((swConfig && swConfig.offlineLogoPath) || '');
|
|
48
67
|
if (logo) {
|
|
49
68
|
content = content.replace(/__OFFLINE_LOGO__/g, logo);
|
|
@@ -116,6 +135,7 @@ function injectServiceWorkerPlaceholders(content, swConfig) {
|
|
|
116
135
|
}
|
|
117
136
|
|
|
118
137
|
function applySwJsPlaceholders(content, swConfig) {
|
|
138
|
+
content = injectOfflineI18nPlaceholder(content);
|
|
119
139
|
content = injectServiceWorkerPlaceholders(content, swConfig);
|
|
120
140
|
const apiPathsJson = JSON.stringify(resolveCacheableApiPaths(swConfig));
|
|
121
141
|
content = content.replace(
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ar_SA": {
|
|
3
|
+
"title": "فشل الاتصال بالشبكة",
|
|
4
|
+
"heading": "فشل الاتصال بالشبكة",
|
|
5
|
+
"body": "يرجى التحقق من إعدادات الشبكة ثم المحاولة مرة أخرى، أو الاتصال بخدمة العملاء للمساعدة. إذا تعذّر إعادة الاتصال، يرجى الانتظار قليلاً والمحاولة لاحقًا.",
|
|
6
|
+
"contact": "خدمة العملاء",
|
|
7
|
+
"reload": "إعادة التحميل",
|
|
8
|
+
"copyAria": "نسخ اسم النطاق",
|
|
9
|
+
"copySuccess": "تم النسخ!",
|
|
10
|
+
"copyFail": "فشل النسخ، يرجى النسخ يدويًا.",
|
|
11
|
+
"contactOfflineHint": "يرجى التواصل مع خدمة العملاء بعد عودة الاتصال."
|
|
12
|
+
},
|
|
13
|
+
"en_US": {
|
|
14
|
+
"title": "Network connection failed",
|
|
15
|
+
"heading": "Network connection failed",
|
|
16
|
+
"body": "Please check your network settings and try again, or contact customer support for help. If the connection cannot be restored, please wait and try again later.",
|
|
17
|
+
"contact": "Contact support",
|
|
18
|
+
"reload": "Reload",
|
|
19
|
+
"copyAria": "Copy domain",
|
|
20
|
+
"copySuccess": "Copied!",
|
|
21
|
+
"copyFail": "Copy failed. Please copy manually.",
|
|
22
|
+
"contactOfflineHint": "Please contact customer support after your connection is restored."
|
|
23
|
+
},
|
|
24
|
+
"hi_IN": {
|
|
25
|
+
"title": "नेटवर्क कनेक्शन विफल",
|
|
26
|
+
"heading": "नेटवर्क कनेक्शन विफल",
|
|
27
|
+
"body": "कृपया अपनी नेटवर्क सेटिंग जाँचकर पुनः प्रयास करें, या सहायता के लिए ग्राहक सेवा से संपर्क करें। यदि कनेक्शन बहाल नहीं हो पाता है, कृपया प्रतीक्षा करें और बाद में फिर कोशिश करें।",
|
|
28
|
+
"contact": "ग्राहक सेवा",
|
|
29
|
+
"reload": "पुनः लोड करें",
|
|
30
|
+
"copyAria": "डोमेन कॉपी करें",
|
|
31
|
+
"copySuccess": "कॉपी हो गया!",
|
|
32
|
+
"copyFail": "कॉपी विफल। कृपया मैन्युअली कॉपी करें।",
|
|
33
|
+
"contactOfflineHint": "कनेक्शन बहाल होने के बाद कृपया ग्राहक सेवा से संपर्क करें।"
|
|
34
|
+
},
|
|
35
|
+
"ja_JP": {
|
|
36
|
+
"title": "ネットワーク接続に失敗しました",
|
|
37
|
+
"heading": "ネットワーク接続に失敗しました",
|
|
38
|
+
"body": "ネットワーク設定を確認してから再度お試しいただくか、カスタマーサポートまでお問い合わせください。接続が復旧しない場合は、しばらく時間をおいてから再度お試しください。",
|
|
39
|
+
"contact": "サポートに連絡",
|
|
40
|
+
"reload": "再読み込み",
|
|
41
|
+
"copyAria": "ドメインをコピー",
|
|
42
|
+
"copySuccess": "コピーしました!",
|
|
43
|
+
"copyFail": "コピーに失敗しました。手動でコピーしてください。",
|
|
44
|
+
"contactOfflineHint": "接続が復旧しましたら、カスタマーサポートにお問い合わせください。"
|
|
45
|
+
},
|
|
46
|
+
"ko_KR": {
|
|
47
|
+
"title": "네트워크 연결 실패",
|
|
48
|
+
"heading": "네트워크 연결 실패",
|
|
49
|
+
"body": "네트워크 설정을 확인한 뒤 다시 시도하거나 고객 지원에 문의해 주세요. 연결이 복구되지 않으면 잠시 후 다시 시도해 주세요.",
|
|
50
|
+
"contact": "고객 지원",
|
|
51
|
+
"reload": "새로고침",
|
|
52
|
+
"copyAria": "도메인 복사",
|
|
53
|
+
"copySuccess": "복사되었습니다!",
|
|
54
|
+
"copyFail": "복사에 실패했습니다. 직접 복사해 주세요.",
|
|
55
|
+
"contactOfflineHint": "연결이 복구된 후 고객 지원에 문의해 주세요."
|
|
56
|
+
},
|
|
57
|
+
"pt_BR": {
|
|
58
|
+
"title": "Falha na conexão de rede",
|
|
59
|
+
"heading": "Falha na conexão de rede",
|
|
60
|
+
"body": "Verifique as configurações de rede e tente novamente ou entre em contato com o suporte ao cliente para obter ajuda. Se a conexão não for restaurada, aguarde e tente mais tarde.",
|
|
61
|
+
"contact": "Suporte",
|
|
62
|
+
"reload": "Recarregar",
|
|
63
|
+
"copyAria": "Copiar domínio",
|
|
64
|
+
"copySuccess": "Copiado!",
|
|
65
|
+
"copyFail": "Falha ao copiar. Copie manualmente.",
|
|
66
|
+
"contactOfflineHint": "Entre em contato com o suporte após a conexão ser restaurada."
|
|
67
|
+
},
|
|
68
|
+
"ru_RU": {
|
|
69
|
+
"title": "Ошибка сетевого подключения",
|
|
70
|
+
"heading": "Ошибка сетевого подключения",
|
|
71
|
+
"body": "Проверьте настройки сети и повторите попытку или обратитесь в службу поддержки. Если подключение не восстанавливается, подождите немного и попробуйте снова позже.",
|
|
72
|
+
"contact": "Поддержка",
|
|
73
|
+
"reload": "Обновить",
|
|
74
|
+
"copyAria": "Копировать домен",
|
|
75
|
+
"copySuccess": "Скопировано!",
|
|
76
|
+
"copyFail": "Не удалось скопировать. Скопируйте вручную.",
|
|
77
|
+
"contactOfflineHint": "Обратитесь в поддержку после восстановления соединения."
|
|
78
|
+
},
|
|
79
|
+
"th_TH": {
|
|
80
|
+
"title": "การเชื่อมต่อเครือข่ายล้มเหลว",
|
|
81
|
+
"heading": "การเชื่อมต่อเครือข่ายล้มเหลว",
|
|
82
|
+
"body": "โปรดตรวจสอบการตั้งค่าเครือข่ายแล้วลองอีกครั้ง หรือติดต่อฝ่ายบริการลูกค้าเพื่อขอความช่วยเหลือ หากยังเชื่อมต่อไม่ได้ โปรดรอสักครู่แล้วลองใหม่ภายหลัง",
|
|
83
|
+
"contact": "ติดต่อฝ่ายบริการ",
|
|
84
|
+
"reload": "โหลดใหม่",
|
|
85
|
+
"copyAria": "คัดลอกโดเมน",
|
|
86
|
+
"copySuccess": "คัดลอกแล้ว!",
|
|
87
|
+
"copyFail": "คัดลอกไม่สำเร็จ โปรดคัดลอกด้วยตนเอง",
|
|
88
|
+
"contactOfflineHint": "โปรดติดต่อฝ่ายบริการหลังจากเชื่อมต่อกลับมาได้"
|
|
89
|
+
},
|
|
90
|
+
"tr_TR": {
|
|
91
|
+
"title": "Ağ bağlantısı başarısız",
|
|
92
|
+
"heading": "Ağ bağlantısı başarısız",
|
|
93
|
+
"body": "Lütfen ağ ayarlarınızı kontrol edip yeniden deneyin veya yardım için müşteri desteğiyle iletişime geçin. Bağlantı kurulamazsa lütfen bekleyip daha sonra tekrar deneyin.",
|
|
94
|
+
"contact": "Destek",
|
|
95
|
+
"reload": "Yeniden yükle",
|
|
96
|
+
"copyAria": "Alan adını kopyala",
|
|
97
|
+
"copySuccess": "Kopyalandı!",
|
|
98
|
+
"copyFail": "Kopyalanamadı. Lütfen elle kopyalayın.",
|
|
99
|
+
"contactOfflineHint": "Bağlantı düzeldiğinde müşteri desteğiyle iletişime geçin."
|
|
100
|
+
},
|
|
101
|
+
"vi_VN": {
|
|
102
|
+
"title": "Kết nối mạng thất bại",
|
|
103
|
+
"heading": "Kết nối mạng thất bại",
|
|
104
|
+
"body": "Vui lòng kiểm tra cài đặt mạng và thử lại, hoặc liên hệ bộ phận hỗ trợ khách hàng để được giúp đỡ. Nếu không khôi phục được kết nối, hãy đợi một lúc rồi thử lại sau.",
|
|
105
|
+
"contact": "Liên hệ hỗ trợ",
|
|
106
|
+
"reload": "Tải lại",
|
|
107
|
+
"copyAria": "Sao chép tên miền",
|
|
108
|
+
"copySuccess": "Đã sao chép!",
|
|
109
|
+
"copyFail": "Sao chép thất bại. Vui lòng sao chép thủ công.",
|
|
110
|
+
"contactOfflineHint": "Vui lòng liên hệ hỗ trợ sau khi mạng đã khôi phục."
|
|
111
|
+
},
|
|
112
|
+
"zh_CN": {
|
|
113
|
+
"title": "网络连接失败",
|
|
114
|
+
"heading": "网络连接失败",
|
|
115
|
+
"body": "请检查网络设置后重试,或联系客服获取帮助,若无法重启,请您耐心等待并稍后再进行尝试。",
|
|
116
|
+
"contact": "联系客服",
|
|
117
|
+
"reload": "重新加载",
|
|
118
|
+
"copyAria": "复制域名",
|
|
119
|
+
"copySuccess": "复制成功!",
|
|
120
|
+
"copyFail": "复制失败,请手动复制",
|
|
121
|
+
"contactOfflineHint": "请在网络恢复后联系客服"
|
|
122
|
+
},
|
|
123
|
+
"zh_TW": {
|
|
124
|
+
"title": "網路連線失敗",
|
|
125
|
+
"heading": "網路連線失敗",
|
|
126
|
+
"body": "請檢查網路設定後重試,或聯絡客服取得協助;若無法重新連線,請耐心等候並稍後再試。",
|
|
127
|
+
"contact": "聯絡客服",
|
|
128
|
+
"reload": "重新載入",
|
|
129
|
+
"copyAria": "複製網域",
|
|
130
|
+
"copySuccess": "複製成功!",
|
|
131
|
+
"copyFail": "複製失敗,請手動複製",
|
|
132
|
+
"contactOfflineHint": "請在網路恢復後聯絡客服"
|
|
133
|
+
},
|
|
134
|
+
"es_MX": {
|
|
135
|
+
"title": "Error de conexión de red",
|
|
136
|
+
"heading": "Error de conexión de red",
|
|
137
|
+
"body": "Verifica tu configuración de red e inténtalo de nuevo o contacta a soporte para obtener ayuda. Si la conexión no se restablece, espera un momento y vuelve a intentarlo más tarde.",
|
|
138
|
+
"contact": "Contactar soporte",
|
|
139
|
+
"reload": "Volver a cargar",
|
|
140
|
+
"copyAria": "Copiar dominio",
|
|
141
|
+
"copySuccess": "¡Copiado!",
|
|
142
|
+
"copyFail": "No se pudo copiar. Cópialo manualmente.",
|
|
143
|
+
"contactOfflineHint": "Contacta a soporte cuando se restaure la conexión."
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -86,6 +86,8 @@
|
|
|
86
86
|
display: flex;
|
|
87
87
|
align-items: center;
|
|
88
88
|
justify-content: center;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
-webkit-tap-highlight-color: transparent;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
.search-bar-btn svg {
|
|
@@ -164,6 +166,27 @@
|
|
|
164
166
|
color: #ffffff;
|
|
165
167
|
box-shadow: 0 4px 16px rgba(74, 124, 255, 0.3);
|
|
166
168
|
}
|
|
169
|
+
|
|
170
|
+
.copy-toast {
|
|
171
|
+
position: fixed;
|
|
172
|
+
left: 50%;
|
|
173
|
+
bottom: 48px;
|
|
174
|
+
transform: translateX(-50%);
|
|
175
|
+
padding: 10px 20px;
|
|
176
|
+
border-radius: 8px;
|
|
177
|
+
background: rgba(0, 0, 0, 0.78);
|
|
178
|
+
color: #ffffff;
|
|
179
|
+
font-size: 14px;
|
|
180
|
+
line-height: 1.4;
|
|
181
|
+
opacity: 0;
|
|
182
|
+
pointer-events: none;
|
|
183
|
+
transition: opacity 0.2s ease;
|
|
184
|
+
z-index: 1000;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.copy-toast.show {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
}
|
|
167
190
|
</style>
|
|
168
191
|
</head>
|
|
169
192
|
<body>
|
|
@@ -178,7 +201,7 @@
|
|
|
178
201
|
<div class="search-bar-input">
|
|
179
202
|
<span id="typingText"></span><span class="typing-cursor">|</span>
|
|
180
203
|
</div>
|
|
181
|
-
<div class="search-bar-btn">
|
|
204
|
+
<div class="search-bar-btn" id="copyDomainBtn" role="button" tabindex="0" aria-label="">
|
|
182
205
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
183
206
|
<circle cx="11" cy="11" r="7"></circle>
|
|
184
207
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
@@ -186,19 +209,88 @@
|
|
|
186
209
|
</div>
|
|
187
210
|
</div>
|
|
188
211
|
|
|
189
|
-
<h1>网络连接失败</h1>
|
|
190
|
-
<p>请检查网络设置后重试,或联系客服获取帮助,若无法重启,请您耐心等待并稍后再进行尝试。</p>
|
|
212
|
+
<h1 id="offline-heading">网络连接失败</h1>
|
|
213
|
+
<p id="offline-body">请检查网络设置后重试,或联系客服获取帮助,若无法重启,请您耐心等待并稍后再进行尝试。</p>
|
|
191
214
|
|
|
192
215
|
<div class="buttons">
|
|
193
216
|
<a class="btn btn-secondary" href="javascript:void(0)" id="contactBtn">联系客服</a>
|
|
194
|
-
<button class="btn btn-primary" onclick="location.reload()">重新加载</button>
|
|
217
|
+
<button type="button" class="btn btn-primary" id="reloadBtn" onclick="location.reload()">重新加载</button>
|
|
195
218
|
</div>
|
|
196
219
|
</div>
|
|
197
220
|
|
|
221
|
+
<div class="copy-toast" id="copyToast" aria-live="polite"></div>
|
|
222
|
+
|
|
198
223
|
<script>
|
|
224
|
+
window.__OFFLINE_I18N__ = __OFFLINE_I18N_INJECT__;
|
|
225
|
+
var offlineDomainText = '__OFFLINE_DOMAIN__';
|
|
226
|
+
|
|
227
|
+
(function applyOfflineLocale() {
|
|
228
|
+
var M = window.__OFFLINE_I18N__ || {};
|
|
229
|
+
var SHORT_TO_FULL = {
|
|
230
|
+
en: 'en_US',
|
|
231
|
+
zh: 'zh_CN',
|
|
232
|
+
ja: 'ja_JP',
|
|
233
|
+
ko: 'ko_KR',
|
|
234
|
+
ar: 'ar_SA',
|
|
235
|
+
hi: 'hi_IN',
|
|
236
|
+
pt: 'pt_BR',
|
|
237
|
+
ru: 'ru_RU',
|
|
238
|
+
th: 'th_TH',
|
|
239
|
+
tr: 'tr_TR',
|
|
240
|
+
vi: 'vi_VN',
|
|
241
|
+
es: 'es_MX'
|
|
242
|
+
};
|
|
243
|
+
function normalizeLocaleKey(s) {
|
|
244
|
+
if (!s || typeof s !== 'string') return '';
|
|
245
|
+
s = s.trim().replace(/-/g, '_');
|
|
246
|
+
if (s.indexOf('_') === -1) return SHORT_TO_FULL[s.toLowerCase()] || '';
|
|
247
|
+
var i = s.indexOf('_');
|
|
248
|
+
return s.slice(0, i).toLowerCase() + '_' + s.slice(i + 1).toUpperCase();
|
|
249
|
+
}
|
|
250
|
+
function resolveOfflineLocaleKey() {
|
|
251
|
+
var fromUrl = '';
|
|
252
|
+
try {
|
|
253
|
+
fromUrl = new URLSearchParams(window.location.search).get('locale') || '';
|
|
254
|
+
} catch (e) {}
|
|
255
|
+
var fromStorage = '';
|
|
256
|
+
try {
|
|
257
|
+
var rawCommon = localStorage.getItem('common');
|
|
258
|
+
if (rawCommon) {
|
|
259
|
+
var parsed = JSON.parse(rawCommon);
|
|
260
|
+
if (parsed && typeof parsed.locale === 'string') fromStorage = parsed.locale;
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {}
|
|
263
|
+
return normalizeLocaleKey(fromUrl) || normalizeLocaleKey(fromStorage) || 'zh_CN';
|
|
264
|
+
}
|
|
265
|
+
var loc = resolveOfflineLocaleKey();
|
|
266
|
+
if (!M[loc]) loc = 'zh_CN';
|
|
267
|
+
var t = M[loc] || M.zh_CN;
|
|
268
|
+
if (!t) return;
|
|
269
|
+
document.documentElement.setAttribute('lang', loc.replace('_', '-'));
|
|
270
|
+
if (loc.indexOf('ar_') === 0) document.documentElement.setAttribute('dir', 'rtl');
|
|
271
|
+
document.title = t.title;
|
|
272
|
+
var el = document.getElementById('offline-heading');
|
|
273
|
+
if (el) el.textContent = t.heading;
|
|
274
|
+
el = document.getElementById('offline-body');
|
|
275
|
+
if (el) el.textContent = t.body;
|
|
276
|
+
el = document.getElementById('contactBtn');
|
|
277
|
+
if (el) el.textContent = t.contact;
|
|
278
|
+
el = document.getElementById('reloadBtn');
|
|
279
|
+
if (el) el.textContent = t.reload;
|
|
280
|
+
el = document.getElementById('copyDomainBtn');
|
|
281
|
+
if (el) el.setAttribute('aria-label', t.copyAria);
|
|
282
|
+
el = document.getElementById('copyToast');
|
|
283
|
+
if (el) el.textContent = t.copySuccess;
|
|
284
|
+
window.__offlineUiStrings = {
|
|
285
|
+
copySuccess: t.copySuccess,
|
|
286
|
+
copyFail: t.copyFail,
|
|
287
|
+
contactOfflineHint: t.contactOfflineHint
|
|
288
|
+
};
|
|
289
|
+
})();
|
|
290
|
+
|
|
199
291
|
// 打字机动画
|
|
200
292
|
(function() {
|
|
201
|
-
var text =
|
|
293
|
+
var text = offlineDomainText;
|
|
202
294
|
var el = document.getElementById('typingText');
|
|
203
295
|
var i = 0;
|
|
204
296
|
var isDeleting = false;
|
|
@@ -232,13 +324,72 @@
|
|
|
232
324
|
setTimeout(tick, 600);
|
|
233
325
|
})();
|
|
234
326
|
|
|
327
|
+
// 搜索按钮:复制域名到剪贴板
|
|
328
|
+
(function() {
|
|
329
|
+
var copyBtn = document.getElementById('copyDomainBtn');
|
|
330
|
+
var toast = document.getElementById('copyToast');
|
|
331
|
+
var toastTimer = null;
|
|
332
|
+
|
|
333
|
+
function showCopyToast() {
|
|
334
|
+
if (window.__offlineUiStrings && window.__offlineUiStrings.copySuccess) {
|
|
335
|
+
toast.textContent = window.__offlineUiStrings.copySuccess;
|
|
336
|
+
}
|
|
337
|
+
toast.classList.add('show');
|
|
338
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
339
|
+
toastTimer = setTimeout(function() {
|
|
340
|
+
toast.classList.remove('show');
|
|
341
|
+
}, 2000);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function copyToClipboard(text) {
|
|
345
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
346
|
+
return navigator.clipboard.writeText(text);
|
|
347
|
+
}
|
|
348
|
+
return new Promise(function(resolve, reject) {
|
|
349
|
+
var ta = document.createElement('textarea');
|
|
350
|
+
ta.value = text;
|
|
351
|
+
ta.setAttribute('readonly', '');
|
|
352
|
+
ta.style.position = 'fixed';
|
|
353
|
+
ta.style.left = '-9999px';
|
|
354
|
+
document.body.appendChild(ta);
|
|
355
|
+
ta.select();
|
|
356
|
+
try {
|
|
357
|
+
var ok = document.execCommand('copy');
|
|
358
|
+
document.body.removeChild(ta);
|
|
359
|
+
ok ? resolve() : reject(new Error('copy failed'));
|
|
360
|
+
} catch (err) {
|
|
361
|
+
document.body.removeChild(ta);
|
|
362
|
+
reject(err);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function handleCopy() {
|
|
368
|
+
var text = (offlineDomainText || '').trim();
|
|
369
|
+
if (!text) return;
|
|
370
|
+
copyToClipboard(text).then(showCopyToast).catch(function() {
|
|
371
|
+
var msg = (window.__offlineUiStrings && window.__offlineUiStrings.copyFail) || '';
|
|
372
|
+
alert(msg);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
copyBtn.addEventListener('click', handleCopy);
|
|
377
|
+
copyBtn.addEventListener('keydown', function(e) {
|
|
378
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
handleCopy();
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
})();
|
|
384
|
+
|
|
235
385
|
// 尝试从 localStorage 获取客服链接
|
|
236
386
|
document.getElementById('contactBtn').addEventListener('click', function() {
|
|
237
387
|
var customerServiceUrl = localStorage.getItem('customerServiceUrl');
|
|
238
388
|
if (customerServiceUrl) {
|
|
239
389
|
window.open(customerServiceUrl, '_blank');
|
|
240
390
|
} else {
|
|
241
|
-
|
|
391
|
+
var msg = (window.__offlineUiStrings && window.__offlineUiStrings.contactOfflineHint) || '';
|
|
392
|
+
alert(msg);
|
|
242
393
|
}
|
|
243
394
|
});
|
|
244
395
|
|