index.html 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1" />
  6. <title>WebSocket 测试</title>
  7. <style>
  8. :root {
  9. --bg: #0f1419;
  10. --surface: #1a2332;
  11. --border: #2d3a4d;
  12. --text: #e7ecf3;
  13. --muted: #8b9bb4;
  14. --accent: #3d9cf0;
  15. --accent-hover: #5aadf5;
  16. --ok: #3ecf8e;
  17. --err: #f07178;
  18. --warn: #e6c07b;
  19. }
  20. * { box-sizing: border-box; margin: 0; padding: 0; }
  21. body {
  22. font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
  23. background:
  24. radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%),
  25. radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%),
  26. var(--bg);
  27. color: var(--text);
  28. min-height: 100vh;
  29. padding: 2rem 1.25rem 3rem;
  30. line-height: 1.5;
  31. }
  32. main {
  33. max-width: 720px;
  34. margin: 0 auto;
  35. }
  36. h1 {
  37. font-family: "IBM Plex Mono", "Cascadia Code", monospace;
  38. font-size: 1.35rem;
  39. font-weight: 600;
  40. letter-spacing: -0.02em;
  41. margin-bottom: 0.35rem;
  42. }
  43. .sub {
  44. color: var(--muted);
  45. font-size: 0.9rem;
  46. margin-bottom: 1.75rem;
  47. }
  48. .sub code {
  49. font-family: "IBM Plex Mono", monospace;
  50. font-size: 0.85em;
  51. color: var(--accent);
  52. }
  53. .row {
  54. display: flex;
  55. flex-wrap: wrap;
  56. gap: 0.6rem;
  57. align-items: center;
  58. margin-bottom: 1rem;
  59. }
  60. .status {
  61. display: inline-flex;
  62. align-items: center;
  63. gap: 0.45rem;
  64. font-size: 0.85rem;
  65. color: var(--muted);
  66. margin-left: auto;
  67. }
  68. .dot {
  69. width: 8px;
  70. height: 8px;
  71. border-radius: 50%;
  72. background: var(--muted);
  73. }
  74. .dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
  75. .dot.connecting { background: var(--warn); }
  76. .dot.closed, .dot.error { background: var(--err); }
  77. button, input {
  78. font: inherit;
  79. }
  80. button {
  81. background: var(--accent);
  82. color: #0a1220;
  83. border: none;
  84. padding: 0.55rem 1.1rem;
  85. font-weight: 600;
  86. font-size: 0.875rem;
  87. cursor: pointer;
  88. transition: background 0.15s;
  89. }
  90. button:hover:not(:disabled) { background: var(--accent-hover); }
  91. button:disabled {
  92. opacity: 0.4;
  93. cursor: not-allowed;
  94. }
  95. button.ghost {
  96. background: transparent;
  97. color: var(--text);
  98. border: 1px solid var(--border);
  99. }
  100. button.ghost:hover:not(:disabled) {
  101. border-color: var(--muted);
  102. background: var(--surface);
  103. }
  104. .url {
  105. width: 100%;
  106. background: var(--surface);
  107. border: 1px solid var(--border);
  108. color: var(--text);
  109. padding: 0.65rem 0.85rem;
  110. font-family: "IBM Plex Mono", monospace;
  111. font-size: 0.85rem;
  112. margin-bottom: 1rem;
  113. }
  114. .url:focus {
  115. outline: none;
  116. border-color: var(--accent);
  117. }
  118. .send-row {
  119. display: flex;
  120. gap: 0.6rem;
  121. margin-bottom: 1rem;
  122. }
  123. .send-row input {
  124. flex: 1;
  125. background: var(--surface);
  126. border: 1px solid var(--border);
  127. color: var(--text);
  128. padding: 0.65rem 0.85rem;
  129. font-size: 0.9rem;
  130. }
  131. .send-row input:focus {
  132. outline: none;
  133. border-color: var(--accent);
  134. }
  135. .log {
  136. background: var(--surface);
  137. border: 1px solid var(--border);
  138. height: min(52vh, 420px);
  139. overflow-y: auto;
  140. padding: 0.75rem;
  141. font-family: "IBM Plex Mono", monospace;
  142. font-size: 0.8rem;
  143. }
  144. .log .line {
  145. padding: 0.2rem 0;
  146. border-bottom: 1px solid transparent;
  147. word-break: break-all;
  148. white-space: pre-wrap;
  149. }
  150. .log .line .t { color: var(--muted); margin-right: 0.5rem; }
  151. .log .line.sys { color: var(--muted); }
  152. .log .line.out { color: var(--accent); }
  153. .log .line.in { color: var(--ok); }
  154. .log .line.err { color: var(--err); }
  155. </style>
  156. </head>
  157. <body>
  158. <main>
  159. <h1>WebSocket 测试</h1>
  160. <p class="sub">连接到当前服务器 <code id="pathHint">/api/ws.lua</code></p>
  161. <input class="url" id="url" type="text" spellcheck="false" />
  162. <div class="row">
  163. <button id="btnConnect" type="button">连接</button>
  164. <button id="btnDisconnect" class="ghost" type="button" disabled>断开</button>
  165. <button id="btnClear" class="ghost" type="button">清空日志</button>
  166. <span class="status">
  167. <span class="dot" id="dot"></span>
  168. <span id="statusText">未连接</span>
  169. </span>
  170. </div>
  171. <div class="send-row">
  172. <input id="msg" type="text" placeholder="输入要发送的消息…" disabled />
  173. <button id="btnSend" type="button" disabled>发送</button>
  174. </div>
  175. <div class="log" id="log" aria-live="polite"></div>
  176. </main>
  177. <script>
  178. (function () {
  179. var path = "/api/ws.lua";
  180. var proto = location.protocol === "https:" ? "wss:" : "ws:";
  181. var defaultUrl = proto + "//" + location.host + path;
  182. var urlEl = document.getElementById("url");
  183. var msgEl = document.getElementById("msg");
  184. var logEl = document.getElementById("log");
  185. var dotEl = document.getElementById("dot");
  186. var statusEl = document.getElementById("statusText");
  187. var btnConnect = document.getElementById("btnConnect");
  188. var btnDisconnect = document.getElementById("btnDisconnect");
  189. var btnSend = document.getElementById("btnSend");
  190. var btnClear = document.getElementById("btnClear");
  191. urlEl.value = defaultUrl;
  192. var ws = null;
  193. function now() {
  194. var d = new Date();
  195. return (
  196. String(d.getHours()).padStart(2, "0") + ":" +
  197. String(d.getMinutes()).padStart(2, "0") + ":" +
  198. String(d.getSeconds()).padStart(2, "0") + "." +
  199. String(d.getMilliseconds()).padStart(3, "0")
  200. );
  201. }
  202. function log(kind, text) {
  203. var line = document.createElement("div");
  204. line.className = "line " + kind;
  205. var t = document.createElement("span");
  206. t.className = "t";
  207. t.textContent = now();
  208. line.appendChild(t);
  209. line.appendChild(document.createTextNode(text));
  210. logEl.appendChild(line);
  211. logEl.scrollTop = logEl.scrollHeight;
  212. }
  213. function setState(state, label) {
  214. dotEl.className = "dot " + state;
  215. statusEl.textContent = label;
  216. var open = state === "open";
  217. var connecting = state === "connecting";
  218. btnConnect.disabled = open || connecting;
  219. btnDisconnect.disabled = !open && !connecting;
  220. btnSend.disabled = !open;
  221. msgEl.disabled = !open;
  222. urlEl.disabled = open || connecting;
  223. }
  224. function connect() {
  225. var url = urlEl.value.trim();
  226. if (!url) return;
  227. if (ws) {
  228. try { ws.close(); } catch (e) {}
  229. ws = null;
  230. }
  231. setState("connecting", "连接中…");
  232. log("sys", "→ 连接 " + url);
  233. try {
  234. ws = new WebSocket(url);
  235. } catch (e) {
  236. setState("error", "错误");
  237. log("err", "创建失败: " + e.message);
  238. return;
  239. }
  240. ws.onopen = function () {
  241. setState("open", "已连接");
  242. log("sys", "✓ 连接已建立 (readyState=" + ws.readyState + ")");
  243. msgEl.focus();
  244. };
  245. ws.onmessage = function (ev) {
  246. var data = ev.data;
  247. if (data instanceof Blob) {
  248. data.arrayBuffer().then(function (buf) {
  249. log("in", "← binary " + buf.byteLength + " bytes");
  250. });
  251. } else if (data instanceof ArrayBuffer) {
  252. log("in", "← binary " + data.byteLength + " bytes");
  253. } else {
  254. log("in", "← " + data);
  255. }
  256. };
  257. ws.onerror = function () {
  258. log("err", "WebSocket error");
  259. setState("error", "错误");
  260. };
  261. ws.onclose = function (ev) {
  262. var reason = ev.reason ? " — " + ev.reason : "";
  263. log("sys", "× 已关闭 (code=" + ev.code + reason + ", clean=" + ev.wasClean + ")");
  264. setState("closed", "已断开");
  265. ws = null;
  266. };
  267. }
  268. function disconnect() {
  269. if (!ws) return;
  270. log("sys", "→ 主动关闭");
  271. ws.close(1000, "client close");
  272. }
  273. function send() {
  274. if (!ws || ws.readyState !== WebSocket.OPEN) return;
  275. var text = msgEl.value;
  276. ws.send(text);
  277. log("out", "→ " + text);
  278. msgEl.value = "";
  279. msgEl.focus();
  280. }
  281. btnConnect.addEventListener("click", connect);
  282. btnDisconnect.addEventListener("click", disconnect);
  283. btnSend.addEventListener("click", send);
  284. btnClear.addEventListener("click", function () {
  285. logEl.innerHTML = "";
  286. });
  287. msgEl.addEventListener("keydown", function (e) {
  288. if (e.key === "Enter") send();
  289. });
  290. urlEl.addEventListener("keydown", function (e) {
  291. if (e.key === "Enter") connect();
  292. });
  293. setState("closed", "未连接");
  294. })();
  295. </script>
  296. </body>
  297. </html>