hyapp-server/tools/baishun-game-test.html
2026-05-28 15:11:10 +08:00

388 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>百顺游戏测试页</title>
<style>
:root {
color-scheme: light;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f4f7fb;
color: #172033;
}
* { box-sizing: border-box; }
body { margin: 0; padding: 24px; }
main { max-width: 1120px; margin: 0 auto; display: grid; gap: 16px; }
h1 { margin: 0; font-size: 24px; }
h2 { margin: 0 0 12px; font-size: 16px; }
section {
border: 1px solid #d9e2ef;
border-radius: 8px;
background: #fff;
padding: 16px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
}
label { display: grid; gap: 6px; color: #53627a; font-size: 13px; font-weight: 700; }
input, select, textarea {
width: 100%;
border: 1px solid #cad6e6;
border-radius: 8px;
padding: 10px 12px;
font: inherit;
color: #172033;
background: #fbfdff;
}
textarea { min-height: 88px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
button {
border: 1px solid #2563eb;
border-radius: 8px;
padding: 10px 14px;
font: inherit;
font-weight: 800;
color: #fff;
background: #2563eb;
cursor: pointer;
}
button.secondary { color: #2563eb; background: #fff; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.muted { color: #697891; font-size: 13px; }
.status { min-height: 22px; color: #334155; font-size: 13px; }
.status.error { color: #dc2626; }
.status.ok { color: #16a34a; }
.games { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
.game {
display: grid;
gap: 8px;
align-content: start;
border: 1px solid #d9e2ef;
border-radius: 8px;
padding: 10px;
background: #fbfdff;
}
.game.selected { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); }
.game img { width: 64px; height: 64px; border-radius: 8px; object-fit: cover; background: #eef3fa; }
.game-title { font-weight: 850; overflow-wrap: anywhere; }
.pill { display: inline-flex; width: fit-content; border-radius: 999px; padding: 2px 8px; background: #eef3ff; color: #2563eb; font-size: 12px; font-weight: 800; }
iframe { width: 100%; min-height: 720px; border: 1px solid #d9e2ef; border-radius: 8px; background: #fff; }
@media (max-width: 760px) {
body { padding: 12px; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main>
<header>
<h1>百顺游戏测试页</h1>
<div class="muted">测试服:<code>https://api-test.global-interaction.com</code>;本地代理打开时 API Base URL 留空即可</div>
</header>
<section>
<h2>1. API 与账号</h2>
<div class="grid">
<label>
API Base URL
<input id="apiBase" value="https://api-test.global-interaction.com" placeholder="本地代理模式留空,直接用 /api/v1/..." />
</label>
<label>
X-App-Code
<input id="appCode" value="lalu" />
</label>
<label>
用户名 / UID
<input id="account" placeholder="用于账号密码登录" />
</label>
<label>
密码
<input id="password" type="password" placeholder="用于登录或快捷创建" />
</label>
</div>
<label style="margin-top:12px">
Access Token
<textarea id="token" placeholder="可以直接粘贴 App 登录 access_token也可以点击下方登录/快捷创建自动填充"></textarea>
</label>
<div class="actions" style="margin-top:12px">
<button id="loginBtn" type="button">用户名密码登录</button>
<button id="quickCreateBtn" class="secondary" type="button">快捷创建测试账号</button>
<span id="authStatus" class="status"></span>
</div>
</section>
<section>
<h2>2. 获取游戏列表</h2>
<div class="grid">
<label>
scene
<input id="scene" value="voice_room" />
</label>
<label>
room_id
<input id="roomId" value="test_room_1" />
</label>
</div>
<div class="actions" style="margin-top:12px">
<button id="loadGamesBtn" type="button">获取 /api/v1/games</button>
<span id="gamesStatus" class="status"></span>
</div>
<div id="games" class="games" style="margin-top:12px"></div>
</section>
<section>
<h2>3. 启动游戏</h2>
<div class="actions">
<button id="launchBtn" type="button" disabled>启动选中游戏</button>
<button id="openBtn" class="secondary" type="button" disabled>新窗口打开</button>
<span id="launchStatus" class="status"></span>
</div>
<label style="margin-top:12px">
launch_url
<textarea id="launchUrl" readonly></textarea>
</label>
<iframe id="gameFrame" title="game preview"></iframe>
</section>
</main>
<script>
const state = {
games: [],
selectedGameId: "",
launchUrl: "",
};
const $ = (id) => document.getElementById(id);
const defaultApiBase = "https://api-test.global-interaction.com";
const isLocalProxy = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
const bundledProxyPorts = new Set(["8787", "8788"]);
const isBundledProxy = isLocalProxy && bundledProxyPorts.has(window.location.port);
const localDirectApiBasePattern = /^https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/;
$("token").value = localStorage.getItem("baishun_test_access_token") || "";
$("account").value = localStorage.getItem("baishun_test_account") || "";
const savedApiBase = localStorage.getItem("baishun_test_api_base") || "";
$("apiBase").value = initialApiBase(savedApiBase);
$("appCode").value = localStorage.getItem("baishun_test_app_code") || $("appCode").value;
function initialApiBase(savedValue) {
const saved = savedValue.trim().replace(/\/+$/, "");
// 仓库自带 Node 代理默认监听 8787本地占用时可切到 8788空 API Base 会让请求走同源 /api 转发。Python 等纯静态服务
// 没有这个代理能力,因此必须保留用户手动填写的本地 gateway 地址,交给 gateway CORS 白名单处理。
if (isLocalProxy && (!saved || saved === defaultApiBase || (isBundledProxy && localDirectApiBasePattern.test(saved)))) {
localStorage.setItem("baishun_test_api_base", "");
return "";
}
return saved || defaultApiBase;
}
function apiBase() {
return $("apiBase").value.trim().replace(/\/+$/, "");
}
function appCode() {
return $("appCode").value.trim() || "lalu";
}
function token() {
return $("token").value.trim();
}
function setStatus(id, message, type = "") {
const el = $(id);
el.textContent = message || "";
el.className = ["status", type].filter(Boolean).join(" ");
}
async function request(path, options = {}) {
const headers = {
"Content-Type": "application/json",
"X-App-Code": appCode(),
"X-App-Platform": "web",
"X-App-Language": "en",
...(options.headers || {}),
};
if (options.auth !== false && token()) {
headers.Authorization = `Bearer ${token()}`;
}
const response = await fetch(`${apiBase()}${path}`, {
method: options.method || "GET",
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const text = await response.text();
let payload = {};
try {
payload = text ? JSON.parse(text) : {};
} catch {
payload = { message: text };
}
if (!response.ok || !isSuccessCode(payload.code)) {
throw new Error(payload.message || response.statusText || `HTTP ${response.status}`);
}
return payload.data;
}
function isSuccessCode(code) {
return code === undefined || code === null || code === 0 || code === "0" || code === 200 || code === "200" || code === "OK";
}
function saveToken(data) {
const accessToken = data?.access_token || data?.token?.access_token || "";
if (!accessToken) {
throw new Error("登录成功但响应里没有 access_token");
}
$("token").value = accessToken;
localStorage.setItem("baishun_test_access_token", accessToken);
if (data.account || data.uid) {
$("account").value = data.account || data.uid;
localStorage.setItem("baishun_test_account", $("account").value);
}
localStorage.setItem("baishun_test_api_base", apiBase());
localStorage.setItem("baishun_test_app_code", appCode());
}
async function login() {
setStatus("authStatus", "登录中...");
const account = $("account").value.trim();
const password = $("password").value;
if (!account || !password) {
setStatus("authStatus", "请填写用户名/UID 和密码", "error");
return;
}
try {
const data = await request("/api/v1/auth/account/login", {
method: "POST",
auth: false,
body: {
account,
password,
device_id: "baishun-html-test",
platform: "web",
language: "en",
timezone: "Asia/Shanghai",
},
});
saveToken(data);
setStatus("authStatus", "登录成功token 已填入", "ok");
} catch (err) {
setStatus("authStatus", err.message, "error");
}
}
async function quickCreate() {
setStatus("authStatus", "创建中...");
const password = $("password").value || "Test@123456";
try {
const data = await request("/api/v1/auth/account/quick-create", {
method: "POST",
auth: false,
body: {
password,
device_id: "baishun-html-test",
platform: "web",
language: "en",
timezone: "Asia/Shanghai",
},
});
$("password").value = password;
saveToken(data);
setStatus("authStatus", `创建成功,账号/UID${data.account || data.uid || ""}`, "ok");
} catch (err) {
setStatus("authStatus", err.message, "error");
}
}
async function loadGames() {
setStatus("gamesStatus", "加载中...");
state.selectedGameId = "";
state.launchUrl = "";
$("launchBtn").disabled = true;
$("openBtn").disabled = true;
$("launchUrl").value = "";
$("gameFrame").src = "about:blank";
try {
const query = new URLSearchParams();
if ($("scene").value.trim()) query.set("scene", $("scene").value.trim());
if ($("roomId").value.trim()) query.set("room_id", $("roomId").value.trim());
const data = await request(`/api/v1/games?${query.toString()}`);
state.games = Array.isArray(data.games) ? data.games : [];
renderGames();
setStatus("gamesStatus", `已获取 ${state.games.length} 个游戏`, "ok");
} catch (err) {
$("games").innerHTML = "";
setStatus("gamesStatus", err.message, "error");
}
}
function renderGames() {
$("games").innerHTML = "";
for (const game of state.games) {
const card = document.createElement("button");
card.type = "button";
card.className = `game ${state.selectedGameId === game.game_id ? "selected" : ""}`;
card.innerHTML = `
${game.icon_url ? `<img src="${escapeHtml(game.icon_url)}" alt="">` : ""}
<span class="game-title">${escapeHtml(game.name || game.game_id)}</span>
<span class="muted">${escapeHtml(game.game_id || "")}</span>
<span class="pill">${escapeHtml(game.platform_code || "")}</span>
<span class="muted">${escapeHtml(game.enabled ? "可启动" : game.maintenance ? "维护" : "不可用")}</span>
`;
card.onclick = () => {
state.selectedGameId = game.game_id;
$("launchBtn").disabled = false;
renderGames();
};
$("games").appendChild(card);
}
}
async function launchSelected() {
const gameId = state.selectedGameId;
if (!gameId) {
setStatus("launchStatus", "请先选择游戏", "error");
return;
}
setStatus("launchStatus", "启动中...");
try {
const data = await request(`/api/v1/games/${encodeURIComponent(gameId)}/launch`, {
method: "POST",
body: {
scene: $("scene").value.trim() || "voice_room",
room_id: $("roomId").value.trim() || "test_room_1",
},
});
state.launchUrl = data.launch_url || "";
$("launchUrl").value = state.launchUrl;
$("openBtn").disabled = !state.launchUrl;
if (state.launchUrl) {
$("gameFrame").src = state.launchUrl;
setStatus("launchStatus", "启动成功", "ok");
} else {
setStatus("launchStatus", "接口成功,但 launch_url 为空", "error");
}
} catch (err) {
setStatus("launchStatus", err.message, "error");
}
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
$("loginBtn").onclick = login;
$("quickCreateBtn").onclick = quickCreate;
$("loadGamesBtn").onclick = loadGames;
$("launchBtn").onclick = launchSelected;
$("openBtn").onclick = () => {
if (state.launchUrl) window.open(state.launchUrl, "_blank", "noopener,noreferrer");
};
</script>
</body>
</html>