最初版本

This commit is contained in:
sa19840531
2026-08-15 14:51:51 +08:00
parent 4e01df4dce
commit ea47ea4449
176 changed files with 21009 additions and 0 deletions
@@ -0,0 +1,453 @@
{% if chatwoot_config.enabled %}
<script>
(function () {
"use strict";
/*
* 會員身份由 Flask Session 注入,identifier 直接使用 username_clean。
* 不再接受 ?identifier=... 等網址參數,避免使用者冒充其他會員。
*/
const CHATWOOT_CONFIG = {{ chatwoot_config | tojson }};
const CHATWOOT_USER = {{ chatwoot_user | tojson }};
window.chatwootSettings = {
hideMessageBubble: false,
showUnreadMessagesDialog: false,
position: "right",
locale: "zh_TW",
useBrowserLanguage: false,
type: "standard",
darkMode: "auto",
launcherTitle: ""
};
function debugLog() {
if (!CHATWOOT_CONFIG.debug) {
return;
}
console.log.apply(console, arguments);
}
function configureChatwootIdentity() {
if (!window.$chatwoot) {
return;
}
if (!CHATWOOT_USER.is_authenticated) {
if (typeof window.$chatwoot.setCustomAttributes === "function") {
window.$chatwoot.setCustomAttributes({
visitor_type: "guest"
});
}
debugLog("Chatwoot anonymous guest ready");
return;
}
if (
typeof window.$chatwoot.setUser !== "function" ||
!CHATWOOT_USER.identifier
) {
return;
}
const userPayload = {
name: CHATWOOT_USER.name || CHATWOOT_USER.username
};
if (CHATWOOT_USER.identifier_hash) {
userPayload.identifier_hash = CHATWOOT_USER.identifier_hash;
}
window.$chatwoot.setUser(
CHATWOOT_USER.identifier,
userPayload
);
if (typeof window.$chatwoot.setCustomAttributes === "function") {
window.$chatwoot.setCustomAttributes({
visitor_type: "member",
username: CHATWOOT_USER.username || "",
member_uid: CHATWOOT_USER.uid == null
? ""
: String(CHATWOOT_USER.uid),
account_status: CHATWOOT_USER.account_status || "",
locked_reason: CHATWOOT_USER.locked_reason || "",
member_type: CHATWOOT_USER.member_type || "",
vip_level: CHATWOOT_USER.vip_level || "",
tier: CHATWOOT_USER.tier || ""
});
}
}
/*
* Flask 登出前清除 Chatwoot Session,避免下一個登入會員
* 沿用上一位會員的 Chatwoot Cookie 與對話。
*/
window.resetChatwootBeforeLogout = function (form) {
if (!form) {
return true;
}
/*
* 防止使用者重複點擊登出,造成多次提交。
*/
if (form.dataset.submitting === "true") {
return false;
}
form.dataset.submitting = "true";
const submitLogout = function () {
form.submit();
};
if (
window.$chatwoot &&
typeof window.$chatwoot.reset === "function"
) {
try {
window.$chatwoot.reset();
} catch (error) {
console.error(
"Chatwoot reset before logout failed:",
error
);
}
/*
* Chatwoot reset 沒有 Promise 回傳,因此提供短暫緩衝時間。
* 即使前端清理未完成,後端的一次性 guest reset
* 仍會在登出後首次載入匿名頁面時再次執行。
*/
window.setTimeout(
submitLogout,
300
);
return false;
}
submitLogout();
return false;
};
window.openGuestChatNewConversation = function () {
createGuestConversationAndShow({ openAfter: true });
};
window.exitGuestChatAndStartNew = function () {
// 先關掉視窗,再換成新對話並重新打開
closeGuestChat();
createGuestConversationAndShow({ openAfter: true, delayMs: 350 });
};
function createGuestConversationAndShow(options) {
var opts = options || {};
var openAfter = opts.openAfter !== false;
var delayMs = typeof opts.delayMs === "number" ? opts.delayMs : 700;
if (!CHATWOOT_CONFIG.guest_new_path) {
return;
}
fetch(CHATWOOT_CONFIG.guest_new_path, {
method: "POST",
headers: {
Accept: "application/json",
"X-CSRF-Token": CHATWOOT_CONFIG.csrf_token || ""
}
})
.then(function (response) {
return response.json().then(function (data) {
if (!response.ok) {
throw new Error((data && data.message) || "建立游客对话失败");
}
return data;
});
})
.then(function (data) {
if (!data || !data.widget_token) {
throw new Error("Chatwoot 未回傳 widget_token");
}
document.querySelectorAll("iframe[src*='widget']").forEach(
function (iframe) {
const url = new URL(iframe.src, window.location.origin);
url.searchParams.set("cw_conversation", data.widget_token);
url.hash = "/messages";
iframe.src = url.toString();
}
);
if (!openAfter) {
window.__guestWidgetOpen = false;
return;
}
window.__guestWidgetOpen = true;
window.setTimeout(function () {
if (
window.$chatwoot &&
typeof window.$chatwoot.toggle === "function"
) {
window.$chatwoot.toggle("open");
}
}, delayMs);
})
.catch(function (error) {
console.error("Chatwoot guest open failed:", error);
window.alert("客服暂时无法开启,请稍后再试。");
});
};
function isGuestChatOpen() {
var holder = document.querySelector(".woot-widget-holder");
if (holder && !holder.classList.contains("woot--hide")) {
var style = window.getComputedStyle(holder);
var rect = holder.getBoundingClientRect();
if (
style.display !== "none" &&
style.visibility !== "hidden" &&
Number(style.opacity || "1") > 0 &&
rect.width > 0 &&
rect.height > 0
) {
return true;
}
}
return !!window.__guestWidgetOpen;
}
function openGuestChatResume() {
window.__guestWidgetOpen = true;
if (window.$chatwoot && typeof window.$chatwoot.toggle === "function") {
try {
window.$chatwoot.toggle("open");
return;
} catch (error) {}
}
var holder = document.querySelector(".woot-widget-holder");
if (holder) {
holder.classList.remove("woot--hide");
}
}
function closeGuestChat() {
window.__guestWidgetOpen = false;
if (window.$chatwoot && typeof window.$chatwoot.toggle === "function") {
try {
window.$chatwoot.toggle("close");
return;
} catch (error) {}
}
var holder = document.querySelector(".woot-widget-holder");
if (holder) {
holder.classList.add("woot--hide");
}
}
function bindGuestBubbleToggle() {
if (!CHATWOOT_CONFIG.guest_new_path || window.__guestBubbleBound) {
return;
}
window.__guestBubbleBound = true;
window.addEventListener("chatwoot:opened", function () {
window.__guestWidgetOpen = true;
});
window.addEventListener("chatwoot:closed", function () {
window.__guestWidgetOpen = false;
});
document.addEventListener(
"click",
function (event) {
var target = event.target;
if (!target || typeof target.closest !== "function") {
return;
}
var bubble = target.closest(
".woot-widget-bubble, .woot--bubble-holder, button.woot-widget-bubble"
);
if (!bubble) {
return;
}
// 右下角氣泡:只收起/重開同一個對話,绝不開新單。
event.preventDefault();
event.stopPropagation();
if (isGuestChatOpen()) {
closeGuestChat();
} else {
openGuestChatResume();
}
},
true
);
}
window.addEventListener("message", function (event) {
var raw = event && event.data;
if (typeof raw !== "string" || raw.indexOf("chatwoot-widget:") !== 0) {
return;
}
try {
var payload = JSON.parse(raw.replace("chatwoot-widget:", ""));
if (payload && payload.event === "guestExitNewChat") {
if (typeof window.exitGuestChatAndStartNew === "function") {
window.exitGuestChatAndStartNew();
} else if (typeof window.openGuestChatNewConversation === "function") {
window.openGuestChatNewConversation();
}
}
} catch (error) {}
});
window.addEventListener(
"chatwoot:ready",
function () {
debugLog("chatwoot:ready", window.$chatwoot);
const guestResetRequired =
{{ chatwoot_guest_reset_required | tojson }};
/*
* 只有「剛完成會員登出」且目前未登入時,才清除舊會員身份。
* 一般遊客頁面不會每次都 reset,因此不會破壞既有匿名對話。
*/
if (
!CHATWOOT_USER.is_authenticated &&
guestResetRequired &&
window.$chatwoot &&
typeof window.$chatwoot.reset === "function"
) {
try {
window.$chatwoot.reset();
} catch (error) {
console.error(
"Chatwoot guest reset failed:",
error
);
}
/*
* reset 後稍候再設定 visitor_type=guest。
* 不執行 setUser(),因此不會向 Chatwoot 傳遞用戶名。
*/
window.setTimeout(
configureChatwootIdentity,
300
);
bindGuestBubbleToggle();
return;
}
configureChatwootIdentity();
bindGuestBubbleToggle();
}
);
window.addEventListener(
"chatwoot:error",
function (event) {
console.error(
"chatwoot:error",
event && event.detail ? event.detail : event
);
}
);
if (CHATWOOT_CONFIG.debug) {
window.addEventListener(
"chatwoot:on-message",
function (event) {
debugLog("chatwoot:on-message", event.detail);
}
);
window.addEventListener(
"chatwoot:postback",
function (event) {
debugLog("chatwoot:postback", event.detail);
}
);
window.addEventListener(
"chatwoot:opened",
function () {
debugLog("chatwoot:opened");
}
);
window.addEventListener(
"chatwoot:closed",
function () {
debugLog("chatwoot:closed");
}
);
window.addEventListener(
"chatwoot:on-start-conversation",
function (event) {
debugLog(
"chatwoot:on-start-conversation",
event.detail
);
}
);
}
const script = document.createElement("script");
const firstScript = document.getElementsByTagName("script")[0];
script.src = (
CHATWOOT_CONFIG.base_url +
"/packs/js/sdk.js"
);
script.async = true;
script.onload = function () {
if (!window.chatwootSDK) {
console.error("Chatwoot SDK object not found");
return;
}
window.chatwootSDK.run({
websiteToken: CHATWOOT_CONFIG.website_token,
baseUrl: CHATWOOT_CONFIG.base_url
});
// 父頁強制觸發 iframe 內快捷入口/宣傳圖掛載(同源 /cw)
function boostGuestUi() {
try {
var iframes = document.querySelectorAll("iframe");
for (var i = 0; i < iframes.length; i += 1) {
var iframe = iframes[i];
var src = iframe.getAttribute("src") || "";
if (src.indexOf("widget") === -1 && src.indexOf("/cw") === -1) {
continue;
}
var win = iframe.contentWindow;
if (win && typeof win.__ydEnsureGuestUI === "function") {
win.__ydEnsureGuestUI();
}
}
} catch (err) {}
}
window.setInterval(boostGuestUi, 1000);
window.setTimeout(boostGuestUi, 1500);
window.setTimeout(boostGuestUi, 3500);
};
script.onerror = function () {
console.error(
"Chatwoot SDK load failed:",
script.src
);
};
firstScript.parentNode.insertBefore(
script,
firstScript
);
})();
</script>
{% endif %}
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ app_name }}{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="{% block body_class %}{% endblock %}">
{% block body %}{% endblock %}
{% block integrations %}{% endblock %}
</body>
</html>
@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}游客客服|{{ app_name }}{% endblock %}
{% block body_class %}guest-page{% endblock %}
{% block body %}
<header class="topbar">
<div>
<div class="site-title">游客客服中心</div>
<div class="site-subtitle">Guest Customer Service</div>
</div>
<a class="back-link" href="{{ url_for('login') }}">返回登录页</a>
</header>
<main class="page narrow">
<section class="support-panel guest-support-panel">
<div class="support-avatar">🎧</div>
<span class="guest-badge">游客模式</span>
<h1>欢迎使用在线客服</h1>
<p>您目前以游客身份进入,可咨询注册、登录、活动与平台操作问题。</p>
<div class="alert alert-warning">
游客模式不会识别会员身份,也不能查询充值、提款、余额或其他会员资料。
</div>
<button
type="button"
class="primary-button guest-open-chat"
onclick="if (window.openGuestChatNewConversation) { window.openGuestChatNewConversation(); }"
>
开启在线客服
</button>
</section>
</main>
{% endblock %}
{% block integrations %}
{% include "_chatwoot.html" %}
{% endblock %}
@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}遊戲首頁|{{ app_name }}{% endblock %}
{% block body %}
<header class="topbar">
<div>
<div class="site-title">YD GAME DEMO</div>
<div class="site-subtitle">Python 唯讀 API 測試站點</div>
</div>
<div class="member-area">
<span>用戶:<strong>{{ account.raw_username or username }}</strong></span>
<span class="status-badge {{ account.status }}">
{{ "已凍結" if account.status == "locked" else "正常" }}
</span>
<form
method="post"
action="{{ url_for('logout') }}"
onsubmit="return window.resetChatwootBeforeLogout ? window.resetChatwootBeforeLogout(this) : true;"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="logout-button" type="submit">登出</button>
</form>
</div>
</header>
<main class="page">
{% if warning %}
<div class="alert alert-error">帳戶資料讀取失敗:{{ warning }}</div>
{% endif %}
{% if account.status == "locked" %}
<div class="alert alert-warning">
<strong>帳戶已凍結</strong>
{% if account.locked_reason %}|凍結原因:{{ account.locked_reason }}{% endif %}
</div>
{% endif %}
<section class="hero">
<div>
<span class="eyebrow">PYTHON TEST ENVIRONMENT</span>
<h1>歡迎進入遊戲站點模擬首頁</h1>
<p>登入會員:{{ account.username_clean or username }}。本頁只用於驗證會員資料、充值、提款與客服 API。</p>
</div>
<a class="service-entry" href="{{ url_for('customer_service') }}">
<span class="service-icon">🎧</span>
<span><strong>在線客服</strong><small>查詢充值與提款資料</small></span>
</a>
</section>
<section class="account-grid">
<div class="metric-card"><span>UID</span><strong>{{ account.uid or "NA" }}</strong></div>
<div class="metric-card"><span>VIP 等級</span><strong>{{ account.vip_level or "NA" }}</strong></div>
<div class="metric-card"><span>會員餘額</span><strong>{{ account.balance or "0.0000" }}</strong></div>
<div class="metric-card"><span>凍結餘額</span><strong>{{ account.frozen_balance or "0.0000" }}</strong></div>
</section>
<section>
<div class="section-heading"><h2>熱門遊戲</h2><span>畫面模擬,不執行真實投注</span></div>
<div class="game-grid">
{% for game in games %}
<article class="game-card">
<div class="game-icon">{{ game.icon }}</div>
<div><h3>{{ game.name }}</h3><p>{{ game.category }}</p></div>
<button type="button" disabled>模擬遊戲</button>
</article>
{% endfor %}
</div>
</section>
</main>
<a class="floating-service" href="{{ url_for('customer_service') }}">🎧 客服</a>
{% endblock %}
{% block integrations %}
{% include "_chatwoot.html" %}
{% endblock %}
@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}登录|{{ app_name }}{% endblock %}
{% block body_class %}login-body{% endblock %}
{% block body %}
<main class="login-card">
<div class="brand-mark">YD</div>
<h1>游戏站点模拟</h1>
<p class="muted">仅使用 members 表内存在的用户名登录,不需要密码。</p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endwith %}
<form method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label for="username">用户名</label>
<input id="username" name="username" type="text" maxlength="191"
value="{{ username }}" placeholder="输入 username 或 username_clean"
required autofocus>
<button type="submit" class="primary-button">登录测试站点</button>
</form>
<div class="login-divider"><span>或</span></div>
<a
class="guest-service-button"
href="{{ url_for('guest_customer_service') }}"
>
🎧 游客进入客服
</a>
<p class="guest-service-note">
游客只能进行一般咨询,不能查询会员充值、提款或账户资料。
</p>
<p class="security-note">【测试限定】此登录方式不具备正式身份验证能力。</p>
</main>
{% endblock %}