388 lines
13 KiB
HTML
388 lines
13 KiB
HTML
<!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("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """)
|
||
.replaceAll("'", "'");
|
||
}
|
||
|
||
$("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>
|