Initial release: cloud browser with auth and one-click deploy on port 32450
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
const AUTH = {
|
||||
async me() {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
},
|
||||
|
||||
async login(username, password) {
|
||||
const res = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.detail || "登录失败");
|
||||
return data;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
|
||||
},
|
||||
|
||||
async changeCredentials(payload) {
|
||||
const res = await fetch("/api/auth/change-credentials", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.detail || "修改失败");
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>云端浏览器</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<header class="header">
|
||||
<h1>云端浏览器</h1>
|
||||
<p class="subtitle">输入网址,在境外服务器上打开并远程操作</p>
|
||||
</header>
|
||||
|
||||
<!-- 登录 -->
|
||||
<section id="login-section" class="panel hidden">
|
||||
<h2>登录</h2>
|
||||
<form id="login-form" class="stack-form">
|
||||
<label for="login-user">用户名</label>
|
||||
<input id="login-user" type="text" autocomplete="username" required>
|
||||
<label for="login-pass">密码</label>
|
||||
<input id="login-pass" type="password" autocomplete="current-password" required>
|
||||
<button type="submit">登录</button>
|
||||
<p id="login-error" class="error hidden"></p>
|
||||
</form>
|
||||
<p class="hint">默认账号:admin / admin,登录后请尽快修改</p>
|
||||
</section>
|
||||
|
||||
<!-- 主界面 -->
|
||||
<section id="main-section" class="hidden">
|
||||
<div class="top-bar">
|
||||
<span id="welcome-user" class="welcome"></span>
|
||||
<button id="btn-settings" type="button" class="btn-secondary">账号设置</button>
|
||||
<button id="btn-logout" type="button" class="btn-secondary">退出</button>
|
||||
</div>
|
||||
|
||||
<form id="start-form" class="start-form">
|
||||
<label for="url-input">目标网址</label>
|
||||
<div class="input-row">
|
||||
<input
|
||||
id="url-input"
|
||||
type="text"
|
||||
placeholder="https://example.com"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
<button type="submit" id="start-btn">进入</button>
|
||||
</div>
|
||||
<p id="error-msg" class="error hidden"></p>
|
||||
</form>
|
||||
|
||||
<section class="tips">
|
||||
<h2>使用说明</h2>
|
||||
<ul>
|
||||
<li>页面将在云服务器 Chromium 中加载,画面实时回传</li>
|
||||
<li>可同时登录账号、搜索和浏览你的数据</li>
|
||||
<li>会话空闲 30 分钟后自动关闭</li>
|
||||
</ul>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- 修改账号 -->
|
||||
<section id="settings-section" class="panel hidden">
|
||||
<h2>修改用户名和密码</h2>
|
||||
<form id="settings-form" class="stack-form">
|
||||
<label for="cur-user">当前用户名</label>
|
||||
<input id="cur-user" type="text" required>
|
||||
<label for="cur-pass">当前密码</label>
|
||||
<input id="cur-pass" type="password" required>
|
||||
<label for="new-user">新用户名</label>
|
||||
<input id="new-user" type="text" required>
|
||||
<label for="new-pass">新密码</label>
|
||||
<input id="new-pass" type="password" required>
|
||||
<div class="btn-row">
|
||||
<button type="submit">保存</button>
|
||||
<button id="btn-settings-cancel" type="button" class="btn-secondary">取消</button>
|
||||
</div>
|
||||
<p id="settings-error" class="error hidden"></p>
|
||||
<p id="settings-success" class="success hidden"></p>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/static/auth.js"></script>
|
||||
<script src="/static/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
const loginSection = document.getElementById("login-section");
|
||||
const mainSection = document.getElementById("main-section");
|
||||
const settingsSection = document.getElementById("settings-section");
|
||||
const loginForm = document.getElementById("login-form");
|
||||
const loginError = document.getElementById("login-error");
|
||||
const welcomeUser = document.getElementById("welcome-user");
|
||||
const settingsForm = document.getElementById("settings-form");
|
||||
const settingsError = document.getElementById("settings-error");
|
||||
const settingsSuccess = document.getElementById("settings-success");
|
||||
const form = document.getElementById("start-form");
|
||||
const urlInput = document.getElementById("url-input");
|
||||
const startBtn = document.getElementById("start-btn");
|
||||
const errorMsg = document.getElementById("error-msg");
|
||||
|
||||
function show(el) {
|
||||
el.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hide(el) {
|
||||
el.classList.add("hidden");
|
||||
}
|
||||
|
||||
function showPanelError(el, message) {
|
||||
el.textContent = message;
|
||||
show(el);
|
||||
}
|
||||
|
||||
function hidePanelError(el) {
|
||||
hide(el);
|
||||
}
|
||||
|
||||
function showMain(username) {
|
||||
hide(loginSection);
|
||||
hide(settingsSection);
|
||||
show(mainSection);
|
||||
welcomeUser.textContent = `当前用户:${username}`;
|
||||
document.getElementById("cur-user").value = username;
|
||||
urlInput.focus();
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
hide(mainSection);
|
||||
hide(settingsSection);
|
||||
show(loginSection);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const user = await AUTH.me();
|
||||
if (user) {
|
||||
showMain(user.username);
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
}
|
||||
|
||||
loginForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
hidePanelError(loginError);
|
||||
const username = document.getElementById("login-user").value.trim();
|
||||
const password = document.getElementById("login-pass").value;
|
||||
try {
|
||||
const data = await AUTH.login(username, password);
|
||||
showMain(data.username);
|
||||
} catch (err) {
|
||||
showPanelError(loginError, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btn-logout").addEventListener("click", async () => {
|
||||
await AUTH.logout();
|
||||
showLogin();
|
||||
});
|
||||
|
||||
document.getElementById("btn-settings").addEventListener("click", () => {
|
||||
hide(mainSection);
|
||||
hidePanelError(settingsError);
|
||||
hide(settingsSuccess);
|
||||
show(settingsSection);
|
||||
});
|
||||
|
||||
document.getElementById("btn-settings-cancel").addEventListener("click", () => {
|
||||
const user = welcomeUser.textContent.replace("当前用户:", "");
|
||||
showMain(user);
|
||||
});
|
||||
|
||||
settingsForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
hidePanelError(settingsError);
|
||||
hide(settingsSuccess);
|
||||
try {
|
||||
const data = await AUTH.changeCredentials({
|
||||
current_username: document.getElementById("cur-user").value.trim(),
|
||||
current_password: document.getElementById("cur-pass").value,
|
||||
new_username: document.getElementById("new-user").value.trim(),
|
||||
new_password: document.getElementById("new-pass").value,
|
||||
});
|
||||
document.getElementById("cur-pass").value = "";
|
||||
document.getElementById("new-pass").value = "";
|
||||
settingsSuccess.textContent = "账号已更新,请使用新凭据登录";
|
||||
show(settingsSuccess);
|
||||
setTimeout(async () => {
|
||||
await AUTH.logout();
|
||||
showLogin();
|
||||
}, 1500);
|
||||
welcomeUser.textContent = `当前用户:${data.username}`;
|
||||
} catch (err) {
|
||||
showPanelError(settingsError, err.message);
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
errorMsg.textContent = message;
|
||||
show(errorMsg);
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
hide(errorMsg);
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
hideError();
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
showError("请输入网址");
|
||||
return;
|
||||
}
|
||||
|
||||
startBtn.disabled = true;
|
||||
startBtn.textContent = "启动中...";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/session", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
if (response.status === 401) {
|
||||
showLogin();
|
||||
showPanelError(loginError, "登录已过期,请重新登录");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
showError(data.detail || "创建会话失败");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `/view/${data.session_id}`;
|
||||
} catch (err) {
|
||||
showError("网络错误,请稍后重试");
|
||||
} finally {
|
||||
startBtn.disabled = false;
|
||||
startBtn.textContent = "进入";
|
||||
}
|
||||
});
|
||||
|
||||
init();
|
||||
@@ -0,0 +1,269 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #0f1419;
|
||||
color: #e7e9ea;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #8b98a5;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.start-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-row input {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #38444d;
|
||||
border-radius: 8px;
|
||||
background: #192734;
|
||||
color: #e7e9ea;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.input-row input:focus {
|
||||
outline: none;
|
||||
border-color: #1d9bf0;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #1d9bf0;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1a8cd8;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f4212e;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin-top: 48px;
|
||||
padding: 24px;
|
||||
background: #192734;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #38444d;
|
||||
}
|
||||
|
||||
.tips h2 {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tips ul {
|
||||
padding-left: 20px;
|
||||
color: #8b98a5;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Viewer */
|
||||
.viewer-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #192734;
|
||||
border-bottom: 1px solid #38444d;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
padding: 8px 12px;
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.toolbar #address-bar {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #38444d;
|
||||
border-radius: 6px;
|
||||
background: #0f1419;
|
||||
color: #e7e9ea;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.toolbar #address-bar:focus {
|
||||
outline: none;
|
||||
border-color: #1d9bf0;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.8rem;
|
||||
color: #8b98a5;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f4212e;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc1d28;
|
||||
}
|
||||
|
||||
.viewport-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#screen {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
cursor: default;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.overlay a {
|
||||
color: #1d9bf0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.overlay a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 24px;
|
||||
padding: 24px;
|
||||
background: #192734;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #38444d;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stack-form label {
|
||||
display: block;
|
||||
margin: 12px 0 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stack-form input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #38444d;
|
||||
border-radius: 8px;
|
||||
background: #0f1419;
|
||||
color: #e7e9ea;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.stack-form input:focus {
|
||||
outline: none;
|
||||
border-color: #1d9bf0;
|
||||
}
|
||||
|
||||
.stack-form button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #38444d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4a5560;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
flex: 1;
|
||||
color: #8b98a5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 12px;
|
||||
color: #8b98a5;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #00ba7c;
|
||||
margin-top: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>浏览中 - 云端浏览器</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="viewer-body">
|
||||
<div class="toolbar">
|
||||
<button id="btn-back" title="后退" type="button">←</button>
|
||||
<button id="btn-forward" title="前进" type="button">→</button>
|
||||
<button id="btn-reload" title="刷新" type="button">↻</button>
|
||||
<input id="address-bar" type="text" placeholder="输入网址..." autocomplete="off">
|
||||
<button id="btn-go" type="button">前往</button>
|
||||
<span id="status" class="status">连接中...</span>
|
||||
<button id="btn-close" class="btn-danger" type="button">关闭会话</button>
|
||||
</div>
|
||||
|
||||
<div id="viewport-wrap" class="viewport-wrap">
|
||||
<canvas id="screen" tabindex="0"></canvas>
|
||||
<div id="overlay" class="overlay hidden">
|
||||
<p id="overlay-msg">会话已结束</p>
|
||||
<a href="/">返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/viewer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,217 @@
|
||||
(function () {
|
||||
const sessionId = window.location.pathname.split("/").pop();
|
||||
const canvas = document.getElementById("screen");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const addressBar = document.getElementById("address-bar");
|
||||
const statusEl = document.getElementById("status");
|
||||
const overlay = document.getElementById("overlay");
|
||||
const overlayMsg = document.getElementById("overlay-msg");
|
||||
|
||||
let ws = null;
|
||||
let viewportWidth = 1280;
|
||||
let viewportHeight = 720;
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
let pingTimer = null;
|
||||
|
||||
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws/${sessionId}`;
|
||||
|
||||
function setStatus(text) {
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function showOverlay(message) {
|
||||
overlayMsg.textContent = message;
|
||||
overlay.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function mapCoords(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = (clientX - rect.left) / scaleX;
|
||||
const y = (clientY - rect.top) / scaleY;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function send(payload) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(payload));
|
||||
}
|
||||
}
|
||||
|
||||
function drawFrame(blob) {
|
||||
const img = new Image();
|
||||
const url = URL.createObjectURL(blob);
|
||||
img.onload = () => {
|
||||
if (canvas.width !== img.width || canvas.height !== img.height) {
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
viewportWidth = img.width;
|
||||
viewportHeight = img.height;
|
||||
updateScale();
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
function updateScale() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
scaleX = rect.width / viewportWidth;
|
||||
scaleY = rect.height / viewportHeight;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(wsUrl);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
ws.onopen = () => {
|
||||
setStatus("已连接");
|
||||
pingTimer = setInterval(() => send({ type: "ping" }), 60000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data instanceof ArrayBuffer) {
|
||||
drawFrame(new Blob([event.data], { type: "image/jpeg" }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "init") {
|
||||
viewportWidth = msg.width;
|
||||
viewportHeight = msg.height;
|
||||
addressBar.value = msg.url || "";
|
||||
updateScale();
|
||||
} else if (msg.type === "url" || msg.type === "url_update") {
|
||||
addressBar.value = msg.url || "";
|
||||
} else if (msg.type === "closed") {
|
||||
showOverlay("会话已结束");
|
||||
ws.close();
|
||||
} else if (msg.type === "error") {
|
||||
setStatus(msg.message);
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setStatus("已断开");
|
||||
clearInterval(pingTimer);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setStatus("连接错误");
|
||||
};
|
||||
}
|
||||
|
||||
canvas.addEventListener("click", (e) => {
|
||||
canvas.focus();
|
||||
const { x, y } = mapCoords(e.clientX, e.clientY);
|
||||
send({ action: "click", x, y, button: "left" });
|
||||
});
|
||||
|
||||
canvas.addEventListener("dblclick", (e) => {
|
||||
e.preventDefault();
|
||||
const { x, y } = mapCoords(e.clientX, e.clientY);
|
||||
send({ action: "dblclick", x, y });
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
send({ action: "wheel", deltaX: e.deltaX, deltaY: e.deltaY });
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener("mousemove", (e) => {
|
||||
const { x, y } = mapCoords(e.clientX, e.clientY);
|
||||
send({ action: "mousemove", x, y });
|
||||
});
|
||||
|
||||
const specialKeys = new Set([
|
||||
"Enter", "Backspace", "Delete", "Tab", "Escape",
|
||||
"ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight",
|
||||
"Home", "End", "PageUp", "PageDown",
|
||||
]);
|
||||
|
||||
canvas.addEventListener("keydown", (e) => {
|
||||
e.preventDefault();
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
send({ action: "type", text: e.key });
|
||||
} else if (specialKeys.has(e.key)) {
|
||||
send({ action: "press", key: e.key });
|
||||
} else {
|
||||
send({ action: "keydown", key: e.key });
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("keyup", (e) => {
|
||||
e.preventDefault();
|
||||
if (e.key.length > 1 || e.ctrlKey || e.metaKey || e.altKey) {
|
||||
send({ action: "keyup", key: e.key });
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", updateScale);
|
||||
|
||||
async function apiPost(path) {
|
||||
const res = await fetch(path, { method: "POST", credentials: "include" });
|
||||
if (res.status === 401) {
|
||||
window.location.href = "/";
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function ensureAuth() {
|
||||
const res = await fetch("/api/auth/me", { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
window.location.href = "/";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
document.getElementById("btn-back").addEventListener("click", async () => {
|
||||
const data = await apiPost(`/api/session/${sessionId}/back`);
|
||||
if (data) addressBar.value = data.url;
|
||||
});
|
||||
|
||||
document.getElementById("btn-forward").addEventListener("click", async () => {
|
||||
const data = await apiPost(`/api/session/${sessionId}/forward`);
|
||||
if (data) addressBar.value = data.url;
|
||||
});
|
||||
|
||||
document.getElementById("btn-reload").addEventListener("click", async () => {
|
||||
const data = await apiPost(`/api/session/${sessionId}/reload`);
|
||||
if (data) addressBar.value = data.url;
|
||||
});
|
||||
|
||||
function navigateTo(url) {
|
||||
send({ type: "navigate", url });
|
||||
}
|
||||
|
||||
document.getElementById("btn-go").addEventListener("click", () => {
|
||||
navigateTo(addressBar.value.trim());
|
||||
});
|
||||
|
||||
addressBar.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
navigateTo(addressBar.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("btn-close").addEventListener("click", async () => {
|
||||
await fetch(`/api/session/${sessionId}`, { method: "DELETE", credentials: "include" });
|
||||
showOverlay("会话已关闭");
|
||||
if (ws) ws.close();
|
||||
});
|
||||
|
||||
ensureAuth().then((ok) => {
|
||||
if (ok) {
|
||||
connect();
|
||||
canvas.focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user