fix(widget): make connection errors actionable

This commit is contained in:
WangShen007 committed 2026-10-28 20:01:18 +08:00
1 parent 3017753b88
commit bb541bd212
4 files changed
+251 -40

No files matched your search

+59 -26
View File
@@ -1092,11 +1092,35 @@ window.replypierConfig;
font-size: 14px;
line-height: 1.6;
color: #64748b;
margin: 0 0 24px 0;
margin: 0 0 20px 0;
max-width: 280px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.replypier-error-retry {
min-width: 104px;
min-height: 44px;
padding: 10px 24px;
margin: 0 0 22px;
border: 0;
border-radius: 12px;
background: #4f46e5;
color: #fff;
font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
cursor: pointer;
}
.replypier-error-retry:hover:not(:disabled) {
background: #4338ca;
}
.replypier-error-retry:focus-visible {
outline: 3px solid #818cf8;
outline-offset: 3px;
}
.replypier-error-retry:disabled {
opacity: 0.7;
cursor: wait;
}
.replypier-error-footer {
display: flex;
align-items: center;
@@ -1141,7 +1165,6 @@ window.replypierConfig;
}
.replypier-error-message {
font-size: 13px;
max-width: 260px;
}
}
@@ -1575,8 +1598,8 @@ window.replypierConfig;
isDestroyed = true
}
// Create error UI for authentication failures using safe DOM methods
function createErrorUI(message) {
// Keep technical failure details in the console, not in the visitor's UI.
function createErrorUI(message, onRetry) {
const errorDiv = document.createElement('div');
errorDiv.className = 'replypier-error-ui';
@@ -1603,13 +1626,16 @@ window.replypierConfig;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('class', 'replypier-error-icon');
iconSvg.setAttribute('viewBox', '0 0 24 24');
iconSvg.setAttribute('aria-hidden', 'true');
iconSvg.setAttribute('fill', 'none');
iconSvg.setAttribute('stroke', 'currentColor');
iconSvg.setAttribute('stroke-width', '1.5');
iconSvg.setAttribute('stroke-linecap', 'round');
iconSvg.setAttribute('stroke-linejoin', 'round');
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z');
path1.setAttribute('d', 'M5 4h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-8l-4 3v-3H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M9 12l2 2 4-4');
path2.setAttribute('d', 'M8 10h8M8 13h5');
iconSvg.appendChild(path1);
iconSvg.appendChild(path2);
iconWrapper.appendChild(iconSvg);
@@ -1617,13 +1643,23 @@ window.replypierConfig;
// Title
const title = document.createElement('h2');
title.className = 'replypier-error-title';
title.textContent = '客服暂不可用';
title.textContent = '客服暂时无法连接';
// Message
const messageEl = document.createElement('p');
messageEl.className = 'replypier-error-message';
messageEl.textContent = message;
const retryButton = document.createElement('button');
retryButton.type = 'button';
retryButton.className = 'replypier-error-retry';
retryButton.textContent = '重试';
retryButton.addEventListener('click', function () {
retryButton.disabled = true;
retryButton.textContent = '正在重试…';
onRetry();
});
// Footer
const footer = document.createElement('div');
footer.className = 'replypier-error-footer';
@@ -1647,14 +1683,24 @@ window.replypierConfig;
card.appendChild(iconWrapper);
card.appendChild(title);
card.appendChild(messageEl);
card.appendChild(retryButton);
card.appendChild(footer);
errorDiv.appendChild(card);
return errorDiv;
}
function showLoadError(previousError) {
removeNode(previousError)
const errorUI = createErrorUI('请检查网络后重试,或稍后再来。', function () {
prefetchWidget(errorUI)
})
container.appendChild(errorUI)
revealButton()
}
// Start prefetching the widget data
async function prefetchWidget() {
async function prefetchWidget(previousError) {
if (isLoading || iframe) return
// A reload for a new identity bumps loadGeneration, so a response that was
@@ -1690,6 +1736,7 @@ window.replypierConfig;
// error or silently starting over as a stranger.
if (detail && detail.code === 'identity_expired') {
button.classList.remove('loading')
removeNode(previousError)
emitEvent('identityExpired')
recoverIdentityAndLoad()
return null;
@@ -1699,11 +1746,8 @@ window.replypierConfig;
if (errorDetail.includes('generate-token') || errorDetail.includes('API key') || errorDetail.includes('Token must be obtained')) {
button.classList.remove('loading')
removeToken();
const errorUI = createErrorUI('此聊天组件尚未配置完成,请联系网站管理员开通在线客服。');
container.appendChild(errorUI);
// Reveal (with the default color) so this error is reachable —
// it'll never get a real color since the widget never loaded.
revealButton();
console.error('Failed to load widget: visitor authentication required (HTTP 401); check the widget token configuration.')
showLoadError(previousError)
return null;
}
} catch {
@@ -1720,6 +1764,7 @@ window.replypierConfig;
if (html === null) return; // Error was already handled
if (generation !== loadGeneration) return; // superseded by a reload
removeNode(previousError)
iframe = document.createElement('iframe')
iframe.className = 'replypier-iframe'
// Screen readers announce a frame by its title.
@@ -1753,19 +1798,7 @@ window.replypierConfig;
if (generation !== loadGeneration) return; // superseded by a reload
console.error('Failed to load widget:', error)
button.classList.remove('loading')
// A failed fetch here is almost always the browser blocking the request
// because this domain isn't in the organization's allowed domains (CORS).
// Point the site owner at the fix instead of a dead-end "try again later".
const host = window.location.hostname || '当前域名';
const isLocal = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?)$/i.test(host) || /\.local$/i.test(host);
const message = isLocal
? '聊天窗口无法在“' + host + '”上加载。本地测试时,请在 ReplyPier 后台(组织设置)中把组织域名设为“' + host + '”。'
: '聊天窗口无法在“' + host + '”上加载。请在 ReplyPier 后台(组织设置)中将此域名加入组织,即可在此启用聊天。';
const errorUI = createErrorUI(message);
container.appendChild(errorUI);
// Reveal (with the default color) so this error is reachable — it'll
// never get a real color since the widget never loaded.
revealButton();
showLoadError(previousError)
});
} catch (error) {