diff --git a/internal/web/supportchat/assets/chat.css b/internal/web/supportchat/assets/chat.css index ca2f7e0..8397d08 100644 --- a/internal/web/supportchat/assets/chat.css +++ b/internal/web/supportchat/assets/chat.css @@ -174,24 +174,6 @@ svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round .danger-button { border: 1px solid var(--danger); color: #fff; background: var(--danger); } .secondary-button:disabled, .primary-button:disabled, .danger-button:disabled { opacity: .5; cursor: not-allowed; } -.access-overlay { z-index: 60; background: rgba(15,23,42,.58); } -.access-dialog { width: min(100%, 420px); padding: 26px; } -.access-icon { width: 48px; height: 48px; margin-bottom: 16px; display: grid; place-items: center; border-radius: 15px; color: var(--theme); background: rgba(var(--theme-rgb), .1); box-shadow: inset 0 0 0 1px rgba(var(--theme-rgb), .1); } -.access-icon svg { width: 25px; height: 25px; stroke-width: 1.9; } -.access-dialog header { padding-right: 0; } -.access-methods { margin-top: 20px; padding: 4px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; border-radius: 13px; background: var(--muted); } -.access-method { height: 38px; border: 0; border-radius: 10px; color: var(--muted-foreground); background: transparent; cursor: pointer; font-size: 13px; font-weight: 600; } -.access-method.active { color: var(--foreground); background: var(--card); box-shadow: 0 3px 10px rgba(15,23,42,.08); } -.access-panel { margin-top: 18px; } -.access-panel label { margin: 12px 0 7px; display: block; color: var(--foreground); font-size: 12px; font-weight: 600; } -.access-panel input { width: 100%; height: 44px; padding: 0 13px; border: 1px solid var(--border); border-radius: 11px; color: var(--foreground); background: var(--card); outline: 0; } -.access-panel input:focus { border-color: rgba(var(--theme-rgb), .56); box-shadow: 0 0 0 3px rgba(var(--theme-rgb), .12); } -.access-code-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; } -.access-code-row .secondary-button { height: 44px; white-space: nowrap; } -.access-help { margin: 9px 0 0; color: var(--muted-foreground); font-size: 11px; line-height: 18px; } -.access-error { margin: 14px 0 0; padding: 10px 12px; border: 1px solid rgba(220,38,38,.18); border-radius: 10px; color: #b42318; background: rgba(254,226,226,.72); font-size: 12px; line-height: 18px; } -.access-actions { justify-content: stretch; } -.access-actions button { flex: 1; } @keyframes fade-in { from { opacity: 0; } } @keyframes dialog-in { from { transform: scale(.97); opacity: .4; } } diff --git a/internal/web/supportchat/assets/chat.js b/internal/web/supportchat/assets/chat.js index 8c020ef..dc44641 100644 --- a/internal/web/supportchat/assets/chat.js +++ b/internal/web/supportchat/assets/chat.js @@ -41,20 +41,6 @@ closeDialogX: byId("close-dialog-x"), continueButton: byId("continue-button"), confirmClose: byId("confirm-close-button"), - accessOverlay: byId("access-overlay"), - accessTarget: byId("access-target"), - accessPasswordTab: byId("access-password-tab"), - accessSMSTab: byId("access-sms-tab"), - accessPasswordPanel: byId("access-password-panel"), - accessSMSPanel: byId("access-sms-panel"), - accessPassword: byId("access-password"), - accessPhone: byId("access-phone"), - accessCode: byId("access-code"), - accessSendCode: byId("access-send-code"), - accessSMSHelp: byId("access-sms-help"), - accessError: byId("access-error"), - accessCancel: byId("access-cancel"), - accessSubmit: byId("access-submit"), toasts: byId("toast-region"), }; @@ -83,12 +69,10 @@ queueTickTimer: null, queueSyncedAt: 0, initialScrollSettling: false, - accessBusy: false, - accessMethod: "password", chatBinding: accessTarget ? loadChatBinding(accessTarget) : "", }; - class CustomerAccessRequiredError extends Error {} + class CustomerSessionRefreshError extends Error {} function getApiBase() { const explicit = (params.get("api_base_url") || "").trim(); @@ -178,8 +162,8 @@ try { window.sessionStorage.setItem(key, value); } catch (_) { - // Private browsing may disable session storage; verification still works - // for the current navigation and can simply be repeated after reload. + // Private browsing may disable session storage; the chat entry can be + // established again on the next navigation. } } @@ -227,17 +211,17 @@ } if (!response.ok || payload?.ok === false || payload?.success === false) { const value = payload?.msg || payload?.message || payload?.code; - if (accessTarget && isCustomerAccessError(value, response.status)) { + if (accessTarget && isCustomerSessionError(value, response.status)) { clearChatBinding(); - showAccessDialog("客服身份验证已失效,请重新验证。"); - throw new CustomerAccessRequiredError("请重新验证当前卡板或设备"); + window.location.reload(); + throw new CustomerSessionRefreshError("正在重新连接客服"); } throw new Error(chineseError(value)); } return Object.prototype.hasOwnProperty.call(payload || {}, "data") ? payload.data : payload; } - function isCustomerAccessError(value, status) { + function isCustomerSessionError(value, status) { const text = String(value || ""); return status === 401 || /unauthorized|身份验证|二次验证|重新验证|验证已失效/i.test(text); } @@ -272,7 +256,7 @@ try { payload = await response.json(); } catch (_) { - throw new Error("验证服务暂时不可用,请稍后重试"); + throw new Error("客服入口暂时不可用,请稍后重试"); } if (!response.ok || payload?.ok === false || payload?.success === false) { throw new Error(chineseError(payload?.msg || payload?.message || payload?.code)); @@ -280,98 +264,17 @@ return Object.prototype.hasOwnProperty.call(payload || {}, "data") ? payload.data : payload; } - function maskAccessNumber(value) { - const text = String(value || "").trim(); - if (text.length <= 4) return text; - return `${text.slice(0, 2)}${"•".repeat(Math.min(6, Math.max(2, text.length - 6)))}${text.slice(-4)}`; - } - - function setAccessMethod(method) { - state.accessMethod = method === "sms" ? "sms" : "password"; - const password = state.accessMethod === "password"; - dom.accessPasswordTab.classList.toggle("active", password); - dom.accessPasswordTab.setAttribute("aria-selected", String(password)); - dom.accessSMSTab.classList.toggle("active", !password); - dom.accessSMSTab.setAttribute("aria-selected", String(!password)); - dom.accessPasswordPanel.hidden = !password; - dom.accessSMSPanel.hidden = password; - setAccessError(""); - window.setTimeout(() => (password ? dom.accessPassword : dom.accessPhone).focus(), 0); - } - - function showAccessDialog(message = "") { - if (!accessTarget) return; - disconnectSocket(false); - dom.accessTarget.textContent = `${accessTarget.type === "device" ? "设备" : "卡板"} ${maskAccessNumber(accessTarget.number)} 涉及套餐、WiFi 密码或设备操作,请先完成二次验证。`; - dom.accessOverlay.hidden = false; - dom.app.setAttribute("aria-hidden", "true"); - setAccessMethod(state.accessMethod); - setAccessError(message); - } - - function setAccessError(message) { - dom.accessError.textContent = String(message || ""); - dom.accessError.hidden = !String(message || "").trim(); - } - - function setAccessBusy(busy) { - state.accessBusy = busy; - dom.accessSubmit.disabled = busy; - dom.accessSendCode.disabled = busy; - dom.accessPassword.disabled = busy; - dom.accessPhone.disabled = busy; - dom.accessCode.disabled = busy; - dom.accessSubmit.textContent = busy ? "正在验证…" : "验证并进入客服"; - } - - async function sendAccessCode() { - if (!accessTarget || state.accessBusy) return; - setAccessBusy(true); - setAccessError(""); + async function establishCustomerChatSession() { + if (!accessTarget) return false; try { - const result = await h5AccessRequest("/access/send-code", { method: "POST", body: "{}", headers: { "Content-Type": "application/json" } }); - dom.accessSMSHelp.textContent = `验证码已发送到 ${result?.masked_phone || "已绑定手机号"},请输入完整手机号和验证码。`; - toast("验证码已发送"); - dom.accessCode.focus(); - } catch (error) { - setAccessError(error instanceof Error ? error.message : "验证码发送失败"); - } finally { - setAccessBusy(false); - } - } - - async function submitCustomerAccess() { - if (!accessTarget || state.accessBusy) return; - const proof = state.accessMethod === "sms" - ? { phone: dom.accessPhone.value.trim(), code: dom.accessCode.value.trim() } - : { password: dom.accessPassword.value }; - if (state.accessMethod === "sms" && (!/^1\d{10}$/.test(proof.phone) || !proof.code)) { - setAccessError("请输入完整的已绑定手机号和短信验证码。"); - return; - } - if (state.accessMethod === "password" && !String(proof.password || "").trim()) { - setAccessError("请输入卡板或设备密码。"); - return; - } - - setAccessBusy(true); - setAccessError(""); - try { - const authorized = await h5AccessRequest("/access/authorize", { - method: "POST", - body: JSON.stringify(proof), - headers: { "Content-Type": "application/json" }, - }); - const accessToken = String(authorized?.access_token || "").trim(); - if (!accessToken) throw new Error("验证结果无效,请重试"); const entry = await h5AccessRequest("/access/chat-entry", { method: "POST", body: "{}", - headers: { "Content-Type": "application/json", "X-H5-Access-Token": accessToken }, + headers: { "Content-Type": "application/json" }, }); const ticket = String(entry?.ticket || "").trim(); const binding = String(entry?.session_binding || "").trim(); - if (!ticket || !binding) throw new Error("客服入口凭证无效,请重试"); + if (!ticket || !binding) throw new Error("客服入口无效,请重试"); state.chatBinding = binding; writeSessionStorage(chatBindingStorageKey(accessTarget), binding); const destination = new URL(window.location.href); @@ -382,10 +285,11 @@ destination.searchParams.set("access_ticket", ticket); destination.hash = ""; window.location.replace(destination.toString()); + return true; } catch (error) { clearChatBinding(); - setAccessError(error instanceof Error ? error.message : "身份验证失败,请重试"); - setAccessBusy(false); + fatal(error instanceof Error ? error.message : "客服入口建立失败,请重试"); + return false; } } @@ -417,9 +321,10 @@ return; } if (accessTarget && !state.chatBinding) { - dom.loading.hidden = true; - dom.app.setAttribute("aria-busy", "false"); - showAccessDialog(); + state.initializing = true; + updateConnectionStatus("connecting"); + await establishCustomerChatSession(); + state.initializing = false; return; } state.initializing = true; @@ -437,7 +342,7 @@ updateAvailability(); connectSocket(); } catch (error) { - if (error instanceof CustomerAccessRequiredError) { + if (error instanceof CustomerSessionRefreshError) { state.initialized = false; return; } @@ -1382,18 +1287,6 @@ } function bindEvents() { - dom.accessPasswordTab.addEventListener("click", () => setAccessMethod("password")); - dom.accessSMSTab.addEventListener("click", () => setAccessMethod("sms")); - dom.accessSendCode.addEventListener("click", sendAccessCode); - dom.accessSubmit.addEventListener("click", submitCustomerAccess); - dom.accessCancel.addEventListener("click", () => { - clearCustomerAccessState(); - closePage(); - }); - dom.accessOverlay.addEventListener("keydown", (event) => { - if (event.key !== "Enter" || event.shiftKey || state.accessBusy) return; - submitCustomerAccess(); - }); dom.retry.addEventListener("click", retry); dom.close.addEventListener("click", openCloseDialog); dom.loadMore.addEventListener("click", loadOlder); @@ -1443,7 +1336,6 @@ }); document.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; - if (!dom.accessOverlay.hidden) return; if (!dom.quickOverlay.hidden) closeQuickDialog(); else if (!dom.closeOverlay.hidden) closeCloseDialog(); }); diff --git a/internal/web/supportchat/assets/index.html b/internal/web/supportchat/assets/index.html index 66ac95d..54a0f2c 100644 --- a/internal/web/supportchat/assets/index.html +++ b/internal/web/supportchat/assets/index.html @@ -111,42 +111,6 @@ -
-