跳至主要内容

iframe 內嵌直聊 Demo

提供一個可直接複製執行的完整 HTML 示例,將獨立訪客聊天頁面嵌入現有網站的 iframe。

Demo 預覽:https://iframe.tgorbit.xyz/

完整 HTML 範例

將下面的完整程式碼儲存為 twt-chat-iframe-demo.html,即可作為網頁 iframe 直聊 Demo 使用。複製前請依照專案實際情況檢查程式碼中的 BASE_URL


<!doctype html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<title>TWT Chat · 有溫度的真人支援</title>
<meta name="renderer" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="theme-color" content="#080a14" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<style>
:root {
--ink: #f4f7ff;
--muted: #9ea7c3;
--muted-strong: #c3cae0;
--surface: rgba(19, 23, 45, 0.72);
--line: rgba(180, 193, 255, 0.15);
--cyan: #6ef5ef;
--blue: #91a4ff;
--violet: #bf9aff;
--lime: #c9f56b;
--shadow: 0 24px 80px rgba(2, 4, 17, 0.52);
--display: 'Space Grotesk', 'Avenir Next', 'Segoe UI', sans-serif;
--mono: 'DM Mono', 'SFMono-Regular', Consolas, monospace;
}

* {
box-sizing: border-box;
}

html {
min-width: 320px;
background: #080a14;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
overflow-x: hidden;
color: var(--ink);
font-family: var(--display);
background:
radial-gradient(circle at 76% 14%, rgba(126, 93, 255, 0.18), transparent 28rem),
radial-gradient(circle at 4% 48%, rgba(44, 221, 220, 0.1), transparent 26rem),
#080a14;
}

body.modal-open {
overflow: hidden;
}

body::before {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
content: '';
opacity: 0.34;
background-image: linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: linear-gradient(to bottom, black, transparent 78%);
}

button {
font: inherit;
}

button:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 4px;
}

.page-shell {
width: min(1180px, calc(100% - 48px));
margin: 0 auto;
}

.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28px 0;
border-bottom: 1px solid rgba(180, 193, 255, 0.11);
}

.brand {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--ink);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.08em;
text-decoration: none;
}

.brand-mark {
display: block;
width: 30px;
height: 30px;
flex: 0 0 auto;
}

.brand-mark img {
width: 100%;
height: 100%;
display: block;
}

.topbar-note {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}

.topbar-actions {
display: flex;
align-items: center;
gap: 18px;
}

.language-switcher {
display: inline-flex;
align-items: center;
gap: 8px;
}

.language-select-wrap {
position: relative;
display: inline-flex;
align-items: center;
}

.language-select-wrap::after {
position: absolute;
right: 20px;
top: 50%;
width: 7px;
height: 7px;
border-right: 1.5px solid var(--cyan);
border-bottom: 1.5px solid var(--cyan);
content: '';
pointer-events: none;
transform: translateY(-65%) rotate(45deg);
transition: border-color 180ms ease, transform 180ms ease;
}

.language-select-wrap:focus-within::after {
border-color: var(--ink);
transform: translateY(-35%) rotate(225deg);
}

.language-switcher__label {
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}

.language-select {
min-width: 116px;
height: 36px;
padding: 0 42px 0 12px;
border: 1px solid rgba(110, 245, 239, 0.28);
border-radius: 999px;
color: var(--ink);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.02em;
cursor: pointer;
outline: none;
appearance: none;
-webkit-appearance: none;
background-color: rgba(18, 24, 50, 0.88);
background-image: none;
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
color-scheme: dark;
}

.language-select:hover,
.language-select:focus-visible {
border-color: rgba(110, 245, 239, 0.74);
background-color: rgba(28, 39, 73, 0.98);
box-shadow: 0 0 0 4px rgba(110, 245, 239, 0.08), 0 0 22px rgba(110, 245, 239, 0.12);
}

.language-select option {
color: var(--ink);
background: #11152b;
}

.hero {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
gap: clamp(36px, 8vw, 112px);
align-items: center;
min-height: 620px;
padding: 84px 0 76px;
}

.eyebrow {
display: inline-flex;
align-items: center;
gap: 9px;
margin-bottom: 25px;
color: var(--cyan);
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
}

.eyebrow-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--lime);
box-shadow: 0 0 0 5px rgba(201, 245, 107, 0.1), 0 0 16px var(--lime);
}

h1 {
max-width: 680px;
margin: 0;
font-size: clamp(50px, 7vw, 88px);
font-weight: 600;
letter-spacing: -0.065em;
line-height: 0.98;
}

h1 em {
display: block;
color: transparent;
font-style: normal;
background: linear-gradient(100deg, var(--cyan) 3%, var(--violet) 68%, #ff9bcf 100%);
-webkit-background-clip: text;
background-clip: text;
}

.hero-copy {
max-width: 495px;
margin: 28px 0 34px;
color: var(--muted-strong);
font-size: 17px;
line-height: 1.75;
}

.hero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 17px;
}

.primary-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
min-height: 54px;
padding: 0 22px 0 25px;
border: 1px solid rgba(110, 245, 239, 0.62);
border-radius: 999px;
color: #071016;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
background: linear-gradient(110deg, var(--cyan), #a1ddff 54%, var(--violet));
box-shadow: 0 12px 32px rgba(74, 223, 230, 0.19), inset 0 1px 0 rgba(255, 255, 255, 0.56);
transition: transform 180ms ease, box-shadow 180ms ease, filter 180ms ease;
}

.primary-button:hover {
filter: saturate(1.15) brightness(1.06);
transform: translateY(-3px);
box-shadow: 0 17px 36px rgba(74, 223, 230, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.56);
}

.primary-button:active {
transform: translateY(-1px) scale(0.99);
}

.button-arrow {
display: grid;
width: 25px;
height: 25px;
place-items: center;
border-radius: 50%;
color: var(--ink);
background: rgba(7, 16, 22, 0.76);
}

.hero-hint {
color: var(--muted);
font-family: var(--mono);
font-size: 11px;
}

.hero-visual {
position: relative;
min-height: 455px;
}

.hero-visual::before,
.hero-visual::after {
position: absolute;
content: '';
border-radius: 50%;
pointer-events: none;
}

.hero-visual::before {
top: 7%;
right: 7%;
width: 300px;
height: 300px;
border: 1px solid rgba(145, 164, 255, 0.19);
box-shadow: 0 0 0 28px rgba(145, 164, 255, 0.035), 0 0 0 56px rgba(145, 164, 255, 0.022);
}

.hero-visual::after {
top: 26%;
right: 20%;
width: 11px;
height: 11px;
background: var(--lime);
box-shadow: 0 0 20px var(--lime);
}

.signal-card {
position: absolute;
top: 13%;
right: 1%;
z-index: 1;
width: min(100%, 380px);
padding: 24px;
border: 1px solid var(--line);
border-radius: 26px;
background: linear-gradient(145deg, rgba(28, 34, 67, 0.89), rgba(14, 17, 36, 0.75));
box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.06);
backdrop-filter: blur(18px);
transform: rotate(3.5deg);
}

.signal-topline {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 42px;
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
}

.signal-live {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--lime);
}

.signal-live::before {
width: 6px;
height: 6px;
border-radius: 50%;
content: '';
background: currentColor;
box-shadow: 0 0 13px currentColor;
}

.signal-card h2 {
max-width: 260px;
margin: 0;
font-size: 27px;
font-weight: 500;
letter-spacing: -0.045em;
line-height: 1.14;
}

.signal-card h2 .signal-accent {
color: var(--cyan);
}

.signal-message {
display: flex;
gap: 11px;
align-items: flex-start;
margin-top: 33px;
padding-top: 18px;
border-top: 1px solid var(--line);
color: var(--muted-strong);
font-size: 13px;
line-height: 1.5;
}

.avatar {
display: grid;
flex: 0 0 auto;
width: 30px;
height: 30px;
place-items: center;
border-radius: 10px;
color: #111528;
font-family: var(--mono);
font-size: 11px;
background: linear-gradient(135deg, var(--lime), #afffda);
}

.floating-note {
position: absolute;
right: 11%;
bottom: 9%;
display: flex;
align-items: center;
gap: 12px;
padding: 14px 17px;
border: 1px solid rgba(191, 154, 255, 0.25);
border-radius: 15px;
color: var(--muted-strong);
font-family: var(--mono);
font-size: 11px;
background: rgba(27, 24, 52, 0.8);
box-shadow: 0 16px 35px rgba(3, 5, 20, 0.3);
backdrop-filter: blur(12px);
transform: rotate(-4deg);
}

.floating-note strong {
display: block;
margin-bottom: 3px;
color: var(--ink);
font-family: var(--display);
font-size: 15px;
font-weight: 500;
}

.note-icon {
display: grid;
width: 31px;
height: 31px;
place-items: center;
border-radius: 50%;
color: #111528;
font-size: 15px;
background: var(--violet);
}

.insights {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
padding: 0 0 58px;
}

.insight-card {
position: relative;
min-height: 138px;
padding: 22px 22px 19px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 19px;
background: rgba(19, 23, 45, 0.46);
transition: border-color 180ms ease, background 180ms ease, transform 180ms ease;
}

.insight-card:hover {
border-color: rgba(110, 245, 239, 0.35);
background: rgba(26, 31, 59, 0.7);
transform: translateY(-3px);
}

.insight-card::after {
position: absolute;
right: -25px;
bottom: -45px;
width: 110px;
height: 110px;
border: 1px solid rgba(110, 245, 239, 0.2);
border-radius: 50%;
content: '';
}

.insight-index {
display: block;
margin-bottom: 18px;
color: var(--cyan);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.12em;
}

.insight-card h3 {
margin: 0 0 7px;
font-size: 16px;
font-weight: 500;
}

.insight-card p {
max-width: 230px;
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}

.footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22px 0 28px;
border-top: 1px solid rgba(180, 193, 255, 0.11);
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.06em;
}

.footer span:last-child {
color: rgba(195, 202, 224, 0.58);
}

.chat-modal[hidden] {
display: none;
}

.chat-modal {
position: fixed;
inset: 0;
z-index: 20;
display: grid;
place-items: center;
padding: 24px;
}

.chat-backdrop {
position: absolute;
inset: 0;
background: rgba(4, 6, 17, 0.76);
opacity: 0;
transition: opacity 220ms ease;
backdrop-filter: blur(8px);
}

.chat-modal.is-open .chat-backdrop {
opacity: 1;
}

.chat-dialog {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
width: min(600px, calc(100vw - 32px));
height: min(760px, calc(100vh - 48px));
min-height: 460px;
overflow: hidden;
border: 1px solid rgba(182, 195, 255, 0.25);
border-radius: 24px;
background: #11152b;
box-shadow: 0 32px 120px rgba(0, 0, 0, 0.58), 0 0 0 1px rgba(110, 245, 239, 0.07);
opacity: 0;
transform: translateY(18px) scale(0.97);
transition: opacity 220ms ease, transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}

.chat-modal.is-open .chat-dialog {
opacity: 1;
transform: translateY(0) scale(1);
}

.chat-dialog__header {
display: flex;
align-items: center;
justify-content: space-between;
flex: 0 0 auto;
min-height: 68px;
padding: 13px 16px 13px 20px;
border-bottom: 1px solid rgba(180, 193, 255, 0.14);
background: linear-gradient(100deg, rgba(29, 37, 72, 0.94), rgba(17, 21, 43, 0.94));
}

.dialog-title {
display: flex;
align-items: center;
gap: 11px;
}

.dialog-title__icon {
display: block;
width: 34px;
height: 34px;
flex: 0 0 auto;
}

.dialog-title__icon img {
width: 100%;
height: 100%;
display: block;
}

.dialog-title strong {
display: block;
margin-bottom: 3px;
font-size: 14px;
font-weight: 600;
}

.dialog-title small {
display: flex;
align-items: center;
gap: 6px;
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
}

.dialog-title small::before {
width: 6px;
height: 6px;
border-radius: 50%;
content: '';
background: var(--lime);
box-shadow: 0 0 10px var(--lime);
}

.dialog-close {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid rgba(180, 193, 255, 0.18);
border-radius: 11px;
color: var(--muted-strong);
font-size: 20px;
line-height: 1;
cursor: pointer;
background: rgba(255, 255, 255, 0.04);
transition: color 180ms ease, border-color 180ms ease, background 180ms ease, transform 180ms ease;
}

.dialog-close:hover {
border-color: rgba(110, 245, 239, 0.48);
color: var(--cyan);
background: rgba(110, 245, 239, 0.1);
transform: rotate(4deg);
}

.chat-frame {
display: flex;
flex: 1 1 auto;
min-height: 0;
background: #fff;
}

#twt-chat-iframe {
display: block;
width: 100%;
height: 100%;
min-height: 0;
border: 0;
background: #fff;
}

@media (max-width: 800px) {
.page-shell {
width: min(100% - 36px, 580px);
}

.hero {
grid-template-columns: 1fr;
min-height: auto;
padding: 72px 0 62px;
}

.hero-visual {
min-height: 390px;
}

.signal-card {
right: 4%;
}

.floating-note {
right: 5%;
}
}

@media (max-width: 580px) {
.page-shell {
width: min(100% - 32px, 440px);
}

.topbar {
padding: 20px 0;
}

.topbar-note {
display: none;
}

.topbar-actions {
gap: 0;
}

.language-switcher__label {
display: none;
}

.language-select {
min-width: 112px;
height: 40px;
}

.hero {
padding: 58px 0 42px;
}

h1 {
font-size: clamp(47px, 14vw, 70px);
}

.hero-copy {
margin: 22px 0 28px;
font-size: 15px;
}

.hero-actions {
align-items: flex-start;
flex-direction: column;
}

.primary-button {
width: 100%;
}

.hero-hint {
padding-left: 3px;
}

.hero-visual {
min-height: 350px;
margin-top: 10px;
}

.hero-visual::before {
top: 6%;
right: 3%;
width: 260px;
height: 260px;
}

.signal-card {
top: 11%;
right: 0;
width: calc(100% - 12px);
padding: 20px;
}

.signal-topline {
margin-bottom: 32px;
}

.floating-note {
right: 2%;
bottom: 4%;
}

.insights {
grid-template-columns: 1fr;
padding-bottom: 44px;
}

.insight-card {
min-height: 118px;
}

.footer {
align-items: flex-start;
flex-direction: column;
gap: 7px;
}

.chat-modal {
padding: 0;
}

.chat-dialog {
width: 100vw;
height: 100vh;
height: 100dvh;
min-height: 0;
border: 0;
border-radius: 0;
}
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
</style>
</head>
<body>
<div class="page-shell">
<header class="topbar">
<a class="brand" href="#top" aria-label="TWT Chat 首页" data-i18n-aria-label="brandLabel">
<span class="brand-mark" aria-hidden="true">
<img src="https://iframe.tgorbit.xyz/twt-logo.svg" alt="TWT Chat"/>
</span>
<span>TWT / CHAT</span>
</a>
<div class="topbar-actions">
<span class="topbar-note" data-i18n="topbarNote">真人优先支持 · 24/7</span>
<label class="language-switcher">
<span class="language-switcher__label" data-i18n="languageLabel">语言</span>
<span class="language-select-wrap">
<select
id="language-select"
class="language-select"
aria-label="切换直聊语言"
data-i18n-aria-label="languageSelectLabel"
>
<option value="en" lang="en">English</option>
<option value="zh-CN" lang="zh-CN">简体中文</option>
<option value="zh-TW" lang="zh-TW">繁體中文</option>
</select>
</span>
</label>
</div>
</header>

<main id="top">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-content">
<div class="eyebrow">
<span class="eyebrow-dot" aria-hidden="true"></span>
<span data-i18n="eyebrow">实时连接入口</span>
</div>
<h1 id="hero-title">
<span data-i18n="heroTitleLine1">让每次对话</span><br />
<em data-i18n="heroTitleLine2">都更有温度。</em>
</h1>
<p class="hero-copy" data-i18n="heroCopy">
一个轻盈、可靠的沟通入口。把问题交给我们,把注意力留给真正重要的事。
</p>
<div class="hero-actions">
<button class="primary-button" id="open-chat" type="button" aria-haspopup="dialog">
<span data-i18n="openChat">开启一段对话</span>
<span class="button-arrow" aria-hidden="true"></span>
</button>
<span class="hero-hint" data-i18n="heroHint">通常在几分钟内回复</span>
</div>
</div>

<div
class="hero-visual"
aria-label="客服对话体验预览"
data-i18n-aria-label="heroVisualLabel"
>
<div class="signal-card">
<div class="signal-topline">
<span data-i18n="signalIndex">twt / 信号 001</span>
<span class="signal-live" data-i18n="onlineNow">当前在线</span>
</div>
<h2>
<span data-i18n="previewTitleLine1">嗨,你好,</span><br />
<span class="signal-accent" data-i18n="previewTitleLine2">有什么能帮你?</span>
</h2>
<div class="signal-message">
<span class="avatar" aria-hidden="true">M</span>
<span data-i18n="signalMessage">不用排队,也不用翻找答案。我们在这里,听你说。</span>
</div>
</div>
<div class="floating-note">
<span class="note-icon" aria-hidden="true"></span>
<span>
<strong data-i18n="channelTitle">你的专属频道</strong>
<span data-i18n="channelMeta">安全 · 私密 · 随时在线</span>
</span>
</div>
</div>
</section>

<section class="insights" aria-label="服务特点" data-i18n-aria-label="featuresLabel">
<article class="insight-card">
<span class="insight-index" data-i18n="directIndex">01 / 直连</span>
<h3 data-i18n="directTitle">直达真人</h3>
<p data-i18n="directCopy">减少层层跳转,把真实的问题交给真实的人。</p>
</article>
<article class="insight-card">
<span class="insight-index" data-i18n="contextIndex">02 / 上下文</span>
<h3 data-i18n="contextTitle">记住上下文</h3>
<p data-i18n="contextCopy">你的每一句话,都能让下一次沟通更顺畅。</p>
</article>
<article class="insight-card">
<span class="insight-index" data-i18n="momentIndex">03 / 此刻</span>
<h3 data-i18n="momentTitle">就在此刻</h3>
<p data-i18n="momentCopy">不必等待合适的时间,想聊的时候就来。</p>
</article>
</section>
</main>

<footer class="footer">
<span>© TWT CHAT EXPERIENCE</span>
<span data-i18n="footerNote">为每一个重要时刻而来</span>
</footer>
</div>

<div class="chat-modal" id="chat-modal" hidden aria-hidden="true">
<div class="chat-backdrop" data-close-modal="true"></div>
<section class="chat-dialog" role="dialog" aria-modal="true" aria-labelledby="chat-dialog-title">
<header class="chat-dialog__header">
<div class="dialog-title">
<span class="dialog-title__icon" aria-hidden="true">
<img src="https://iframe.tgorbit.xyz/twt-logo.svg" alt="TWT Chat"/>
</span>
<div>
<strong id="chat-dialog-title" data-i18n="dialogTitle">TWT 在线客服</strong>
<small data-i18n="dialogStatus">在线,正在等待你的消息</small>
</div>
</div>
<button
class="dialog-close"
id="close-chat"
type="button"
aria-label="关闭对话窗口"
data-i18n-aria-label="closeDialogLabel"
>×</button>
</header>
<div class="chat-frame">
<iframe
id="twt-chat-iframe"
title="TWT 在线客服对话窗口"
data-i18n-title="iframeTitle"
allow="microphone; camera; autoplay; fullscreen"
></iframe>
</div>
</section>
</div>

<script>
(function () {
const STORE_KEY = '__twt_visitor__';
const LANGUAGE_STORE_KEY = '__twt_chat_language__';
const LANGUAGE_OPTIONS = {
'zh-CN': 'zh-cn',
'zh-TW': 'zh-tw',
en: 'en',
};
const TRANSLATIONS = Object.freeze({
en: Object.freeze({
pageTitle: 'TWT Chat · Support that feels human',
brandLabel: 'TWT Chat home',
topbarNote: 'human-first support · 24/7',
languageLabel: 'Language',
languageSelectLabel: 'Switch chat language',
eyebrow: 'Live connection layer',
heroTitleLine1: 'Support that',
heroTitleLine2: 'feels human.',
heroCopy:
'A lightweight, reliable way to connect. Leave the questions with us and save your attention for what matters.',
openChat: 'Start a conversation',
heroHint: 'Usually replies in a few minutes',
heroVisualLabel: 'Support chat experience preview',
signalIndex: 'twt / signal 001',
onlineNow: 'online now',
previewTitleLine1: 'Hi there,',
previewTitleLine2: 'how can we help?',
signalMessage: "No queues and no searching through answers. We're here to listen.",
channelTitle: 'Your private channel',
channelMeta: 'Secure · Private · Always online',
featuresLabel: 'Service highlights',
directIndex: '01 / DIRECT',
directTitle: 'Real human support',
directCopy: 'Skip the handoffs and bring real questions directly to real people.',
contextIndex: '02 / CONTEXT',
contextTitle: 'Context that carries',
contextCopy: 'Every message helps make the next conversation smoother.',
momentIndex: '03 / MOMENT',
momentTitle: 'Here when it matters',
momentCopy: 'No need to wait for the right time. Start a chat whenever you need us.',
footerNote: 'made for meaningful moments',
dialogTitle: 'TWT live support',
dialogStatus: 'Online and waiting for your message',
closeDialogLabel: 'Close chat window',
iframeTitle: 'TWT live support chat',
}),
'zh-CN': Object.freeze({
pageTitle: 'TWT Chat · 有温度的真人支持',
brandLabel: 'TWT Chat 首页',
topbarNote: '真人优先支持 · 24/7',
languageLabel: '语言',
languageSelectLabel: '切换直聊语言',
eyebrow: '实时连接入口',
heroTitleLine1: '让每次对话',
heroTitleLine2: '都更有温度。',
heroCopy: '一个轻盈、可靠的沟通入口。把问题交给我们,把注意力留给真正重要的事。',
openChat: '开启一段对话',
heroHint: '通常在几分钟内回复',
heroVisualLabel: '客服对话体验预览',
signalIndex: 'twt / 信号 001',
onlineNow: '当前在线',
previewTitleLine1: '嗨,你好,',
previewTitleLine2: '有什么能帮你?',
signalMessage: '不用排队,也不用翻找答案。我们在这里,听你说。',
channelTitle: '你的专属频道',
channelMeta: '安全 · 私密 · 随时在线',
featuresLabel: '服务特点',
directIndex: '01 / 直连',
directTitle: '直达真人',
directCopy: '减少层层跳转,把真实的问题交给真实的人。',
contextIndex: '02 / 上下文',
contextTitle: '记住上下文',
contextCopy: '你的每一句话,都能让下一次沟通更顺畅。',
momentIndex: '03 / 此刻',
momentTitle: '就在此刻',
momentCopy: '不必等待合适的时间,想聊的时候就来。',
footerNote: '为每一个重要时刻而来',
dialogTitle: 'TWT 在线客服',
dialogStatus: '在线,正在等待你的消息',
closeDialogLabel: '关闭对话窗口',
iframeTitle: 'TWT 在线客服对话窗口',
}),
'zh-TW': Object.freeze({
pageTitle: 'TWT Chat · 有溫度的真人支援',
brandLabel: 'TWT Chat 首頁',
topbarNote: '真人優先支援 · 24/7',
languageLabel: '語言',
languageSelectLabel: '切換直聊語言',
eyebrow: '即時連線入口',
heroTitleLine1: '讓每次對話',
heroTitleLine2: '都更有溫度。',
heroCopy: '一個輕盈、可靠的溝通入口。把問題交給我們,把注意力留給真正重要的事。',
openChat: '開啟一段對話',
heroHint: '通常在幾分鐘內回覆',
heroVisualLabel: '客服對話體驗預覽',
signalIndex: 'twt / 訊號 001',
onlineNow: '目前在線',
previewTitleLine1: '嗨,你好,',
previewTitleLine2: '有什麼能幫你?',
signalMessage: '不用排隊,也不用翻找答案。我們在這裡,聽你說。',
channelTitle: '你的專屬頻道',
channelMeta: '安全 · 私密 · 隨時在線',
featuresLabel: '服務特色',
directIndex: '01 / 直連',
directTitle: '直達真人',
directCopy: '減少層層跳轉,把真實的問題交給真實的人。',
contextIndex: '02 / 上下文',
contextTitle: '記住上下文',
contextCopy: '你的每一句話,都能讓下一次溝通更順暢。',
momentIndex: '03 / 此刻',
momentTitle: '就在此刻',
momentCopy: '不必等待合適的時間,想聊的時候就來。',
footerNote: '為每一個重要時刻而來',
dialogTitle: 'TWT 線上客服',
dialogStatus: '在線,正在等待你的訊息',
closeDialogLabel: '關閉對話視窗',
iframeTitle: 'TWT 線上客服對話視窗',
}),
});
const BASE_URL = 'https://page.visitor-chat.com/direct/c354839f17e95393397c6b5075a79304';
const CHAT_ORIGIN = new URL(BASE_URL).origin;
const modal = document.getElementById('chat-modal');
const openButton = document.getElementById('open-chat');
const closeButton = document.getElementById('close-chat');
const chatIframe = document.getElementById('twt-chat-iframe');
const languageSelect = document.getElementById('language-select');
let lastFocusedElement = null;
let closeTimer = null;
let iframeLoaded = false;
let stored = {};

try {
stored = JSON.parse(localStorage.getItem(STORE_KEY) || '{}');
} catch (error) {
// localStorage 不可用时静默降级,不阻塞页面加载
}

function appendQueryParam(url, key, value) {
if (!value) return url;
return url + (url.indexOf('?') > -1 ? '&' : '?') + key + '=' + encodeURIComponent(value);
}

function normalizeLanguage(value) {
const normalized = String(value || '').trim().toLowerCase().replace('_', '-');
if (normalized === 'zh-tw' || normalized === 'zh-hant' || normalized === 'zh-hk') return 'zh-TW';
if (normalized === 'en' || normalized.startsWith('en-')) return 'en';
return 'zh-CN';
}

let savedLanguage = '';
try {
savedLanguage = localStorage.getItem(LANGUAGE_STORE_KEY) || '';
} catch (error) {
// localStorage 不可用时使用默认语言
}

const queryLanguage = new URLSearchParams(window.location.search).get('lang');
let currentLanguage = normalizeLanguage(queryLanguage || savedLanguage || document.documentElement.lang);
let src = '';

function buildIframeSrc(language) {
let nextSrc = appendQueryParam(BASE_URL, 'lang', LANGUAGE_OPTIONS[language]);
if (window.location && window.location.origin && window.location.pathname) {
nextSrc = appendQueryParam(nextSrc, 'referer', window.location.origin + window.location.pathname);
}
if (stored.bs) nextSrc = appendQueryParam(nextSrc, 'ext_bs', stored.bs);
if (stored.bs_mm) nextSrc = appendQueryParam(nextSrc, 'ext_bs_mm', stored.bs_mm);
if (stored.fk_id) nextSrc = appendQueryParam(nextSrc, 'ext_fk_id', stored.fk_id);
return nextSrc;
}

function persistLanguage(language) {
try {
localStorage.setItem(LANGUAGE_STORE_KEY, language);
} catch (error) {
// 写入失败时静默处理
}
}

function applyTranslations(language) {
const messages = TRANSLATIONS[language];
document.title = messages.pageTitle;

document.querySelectorAll('[data-i18n]').forEach(function (element) {
const message = messages[element.dataset.i18n];
if (typeof message === 'string') element.textContent = message;
});

document.querySelectorAll('[data-i18n-aria-label]').forEach(function (element) {
const message = messages[element.dataset.i18nAriaLabel];
if (typeof message === 'string') element.setAttribute('aria-label', message);
});

document.querySelectorAll('[data-i18n-title]').forEach(function (element) {
const message = messages[element.dataset.i18nTitle];
if (typeof message === 'string') element.setAttribute('title', message);
});
}

function setLanguage(language) {
const nextLanguage = normalizeLanguage(language);
currentLanguage = nextLanguage;
document.documentElement.lang = nextLanguage;
languageSelect.value = nextLanguage;
applyTranslations(nextLanguage);
persistLanguage(nextLanguage);
src = buildIframeSrc(nextLanguage);
if (iframeLoaded) chatIframe.src = src;
}

setLanguage(currentLanguage);

function openModal() {
if (!modal.hidden) return;
clearTimeout(closeTimer);
if (!iframeLoaded) {
chatIframe.src = src;
iframeLoaded = true;
}
lastFocusedElement = document.activeElement;
modal.hidden = false;
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
requestAnimationFrame(function () {
modal.classList.add('is-open');
});
closeButton.focus();
}

function closeModal() {
if (modal.hidden) return;
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
closeTimer = setTimeout(function () {
modal.hidden = true;
}, 240);
if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
lastFocusedElement.focus();
}
}

function isVisitorIdentityValue(value) {
return (
(typeof value === 'string' && value.trim().length > 0 && value.length <= 256) ||
(typeof value === 'number' && Number.isFinite(value))
);
}

function getOptionalIdentityString(value) {
return typeof value === 'string' && value.length <= 256 ? value : '';
}

languageSelect.addEventListener('change', function (event) {
setLanguage(event.target.value);
});

openButton.addEventListener('click', openModal);
closeButton.addEventListener('click', closeModal);
modal.addEventListener('click', function (event) {
if (event.target && event.target.dataset.closeModal) closeModal();
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && !modal.hidden) closeModal();
});

window.addEventListener('message', function (event) {
if (
event.origin !== CHAT_ORIGIN ||
event.source !== chatIframe.contentWindow ||
!event.data ||
event.data.type !== 'twtVisitorIdentity' ||
!isVisitorIdentityValue(event.data.bs)
) {
return;
}

try {
localStorage.setItem(
STORE_KEY,
JSON.stringify({
bs: event.data.bs,
bs_mm: getOptionalIdentityString(event.data.bs_mm),
fk_id: getOptionalIdentityString(event.data.fk_id),
}),
);
} catch (error) {
// 写入失败时静默处理(可接受降级)
}
});
})();
</script>
</body>
</html>