346 lines
9.0 KiB
HTML
346 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>WebSocket 测试</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1419;
|
||
--surface: #1a2332;
|
||
--border: #2d3a4d;
|
||
--text: #e7ecf3;
|
||
--muted: #8b9bb4;
|
||
--accent: #3d9cf0;
|
||
--accent-hover: #5aadf5;
|
||
--ok: #3ecf8e;
|
||
--err: #f07178;
|
||
--warn: #e6c07b;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||
background:
|
||
radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%),
|
||
radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%),
|
||
var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
padding: 2rem 1.25rem 3rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
main {
|
||
max-width: 720px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
h1 {
|
||
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
||
font-size: 1.35rem;
|
||
font-weight: 600;
|
||
letter-spacing: -0.02em;
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
|
||
.sub {
|
||
color: var(--muted);
|
||
font-size: 0.9rem;
|
||
margin-bottom: 1.75rem;
|
||
}
|
||
|
||
.sub code {
|
||
font-family: "IBM Plex Mono", monospace;
|
||
font-size: 0.85em;
|
||
color: var(--accent);
|
||
}
|
||
|
||
.row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.6rem;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.status {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.45rem;
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
margin-left: auto;
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--muted);
|
||
}
|
||
|
||
.dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
|
||
.dot.connecting { background: var(--warn); }
|
||
.dot.closed, .dot.error { background: var(--err); }
|
||
|
||
button, input {
|
||
font: inherit;
|
||
}
|
||
|
||
button {
|
||
background: var(--accent);
|
||
color: #0a1220;
|
||
border: none;
|
||
padding: 0.55rem 1.1rem;
|
||
font-weight: 600;
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
button:hover:not(:disabled) { background: var(--accent-hover); }
|
||
button:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
button.ghost {
|
||
background: transparent;
|
||
color: var(--text);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
button.ghost:hover:not(:disabled) {
|
||
border-color: var(--muted);
|
||
background: var(--surface);
|
||
}
|
||
|
||
.url {
|
||
width: 100%;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
padding: 0.65rem 0.85rem;
|
||
font-family: "IBM Plex Mono", monospace;
|
||
font-size: 0.85rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.url:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.send-row {
|
||
display: flex;
|
||
gap: 0.6rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.send-row input {
|
||
flex: 1;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
padding: 0.65rem 0.85rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.send-row input:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.log {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
height: min(52vh, 420px);
|
||
overflow-y: auto;
|
||
padding: 0.75rem;
|
||
font-family: "IBM Plex Mono", monospace;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.log .line {
|
||
padding: 0.2rem 0;
|
||
border-bottom: 1px solid transparent;
|
||
word-break: break-all;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.log .line .t { color: var(--muted); margin-right: 0.5rem; }
|
||
.log .line.sys { color: var(--muted); }
|
||
.log .line.out { color: var(--accent); }
|
||
.log .line.in { color: var(--ok); }
|
||
.log .line.err { color: var(--err); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main>
|
||
<h1>WebSocket 测试</h1>
|
||
<p class="sub">连接到当前服务器 <code id="pathHint">/api/ws.lua</code></p>
|
||
|
||
<input class="url" id="url" type="text" spellcheck="false" />
|
||
|
||
<div class="row">
|
||
<button id="btnConnect" type="button">连接</button>
|
||
<button id="btnDisconnect" class="ghost" type="button" disabled>断开</button>
|
||
<button id="btnClear" class="ghost" type="button">清空日志</button>
|
||
<span class="status">
|
||
<span class="dot" id="dot"></span>
|
||
<span id="statusText">未连接</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="send-row">
|
||
<input id="msg" type="text" placeholder="输入要发送的消息…" disabled />
|
||
<button id="btnSend" type="button" disabled>发送</button>
|
||
</div>
|
||
|
||
<div class="log" id="log" aria-live="polite"></div>
|
||
</main>
|
||
|
||
<script>
|
||
(function () {
|
||
var path = "/api/ws.lua";
|
||
var proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||
var defaultUrl = proto + "//" + location.host + path;
|
||
|
||
var urlEl = document.getElementById("url");
|
||
var msgEl = document.getElementById("msg");
|
||
var logEl = document.getElementById("log");
|
||
var dotEl = document.getElementById("dot");
|
||
var statusEl = document.getElementById("statusText");
|
||
var btnConnect = document.getElementById("btnConnect");
|
||
var btnDisconnect = document.getElementById("btnDisconnect");
|
||
var btnSend = document.getElementById("btnSend");
|
||
var btnClear = document.getElementById("btnClear");
|
||
|
||
urlEl.value = defaultUrl;
|
||
|
||
var ws = null;
|
||
|
||
function now() {
|
||
var d = new Date();
|
||
return (
|
||
String(d.getHours()).padStart(2, "0") + ":" +
|
||
String(d.getMinutes()).padStart(2, "0") + ":" +
|
||
String(d.getSeconds()).padStart(2, "0") + "." +
|
||
String(d.getMilliseconds()).padStart(3, "0")
|
||
);
|
||
}
|
||
|
||
function log(kind, text) {
|
||
var line = document.createElement("div");
|
||
line.className = "line " + kind;
|
||
var t = document.createElement("span");
|
||
t.className = "t";
|
||
t.textContent = now();
|
||
line.appendChild(t);
|
||
line.appendChild(document.createTextNode(text));
|
||
logEl.appendChild(line);
|
||
logEl.scrollTop = logEl.scrollHeight;
|
||
}
|
||
|
||
function setState(state, label) {
|
||
dotEl.className = "dot " + state;
|
||
statusEl.textContent = label;
|
||
var open = state === "open";
|
||
var connecting = state === "connecting";
|
||
btnConnect.disabled = open || connecting;
|
||
btnDisconnect.disabled = !open && !connecting;
|
||
btnSend.disabled = !open;
|
||
msgEl.disabled = !open;
|
||
urlEl.disabled = open || connecting;
|
||
}
|
||
|
||
function connect() {
|
||
var url = urlEl.value.trim();
|
||
if (!url) return;
|
||
|
||
if (ws) {
|
||
try { ws.close(); } catch (e) {}
|
||
ws = null;
|
||
}
|
||
|
||
setState("connecting", "连接中…");
|
||
log("sys", "→ 连接 " + url);
|
||
|
||
try {
|
||
ws = new WebSocket(url);
|
||
} catch (e) {
|
||
setState("error", "错误");
|
||
log("err", "创建失败: " + e.message);
|
||
return;
|
||
}
|
||
|
||
ws.onopen = function () {
|
||
setState("open", "已连接");
|
||
log("sys", "✓ 连接已建立 (readyState=" + ws.readyState + ")");
|
||
msgEl.focus();
|
||
};
|
||
|
||
ws.onmessage = function (ev) {
|
||
var data = ev.data;
|
||
if (data instanceof Blob) {
|
||
data.arrayBuffer().then(function (buf) {
|
||
log("in", "← binary " + buf.byteLength + " bytes");
|
||
});
|
||
} else if (data instanceof ArrayBuffer) {
|
||
log("in", "← binary " + data.byteLength + " bytes");
|
||
} else {
|
||
log("in", "← " + data);
|
||
}
|
||
};
|
||
|
||
ws.onerror = function () {
|
||
log("err", "WebSocket error");
|
||
setState("error", "错误");
|
||
};
|
||
|
||
ws.onclose = function (ev) {
|
||
var reason = ev.reason ? " — " + ev.reason : "";
|
||
log("sys", "× 已关闭 (code=" + ev.code + reason + ", clean=" + ev.wasClean + ")");
|
||
setState("closed", "已断开");
|
||
ws = null;
|
||
};
|
||
}
|
||
|
||
function disconnect() {
|
||
if (!ws) return;
|
||
log("sys", "→ 主动关闭");
|
||
ws.close(1000, "client close");
|
||
}
|
||
|
||
function send() {
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||
var text = msgEl.value;
|
||
ws.send(text);
|
||
log("out", "→ " + text);
|
||
msgEl.value = "";
|
||
msgEl.focus();
|
||
}
|
||
|
||
btnConnect.addEventListener("click", connect);
|
||
btnDisconnect.addEventListener("click", disconnect);
|
||
btnSend.addEventListener("click", send);
|
||
btnClear.addEventListener("click", function () {
|
||
logEl.innerHTML = "";
|
||
});
|
||
|
||
msgEl.addEventListener("keydown", function (e) {
|
||
if (e.key === "Enter") send();
|
||
});
|
||
|
||
urlEl.addEventListener("keydown", function (e) {
|
||
if (e.key === "Enter") connect();
|
||
});
|
||
|
||
setState("closed", "未连接");
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|