- 新增 remote_browser.py: CDP screencast截图流 + 鼠标/键盘事件转发 - Flask-SocketIO 实时通信 - 短信登录时弹出远程浏览器窗口,用户直接在Web页面拖滑块 - 自动检测登录成功并保存auth状态
299 lines
12 KiB
HTML
299 lines
12 KiB
HTML
{% extends "base.html" %}
|
||
{% block content %}
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<h4 class="mb-0">账号管理</h4>
|
||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addModal">
|
||
<i class="bi bi-plus-lg"></i> 添加账号
|
||
</button>
|
||
</div>
|
||
<div class="card">
|
||
<div class="table-responsive">
|
||
<table class="table table-hover mb-0">
|
||
<thead><tr>
|
||
<th>ID</th><th>名称</th><th>手机号</th><th>登录状态</th><th>更新时间</th><th>操作</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{% for a in accounts %}
|
||
<tr>
|
||
<td>{{ a.id }}</td>
|
||
<td>{{ a.name }}</td>
|
||
<td>{{ a.phone[:3] }}****{{ a.phone[-4:] if a.phone|length > 4 else '' }}</td>
|
||
<td>
|
||
{% if a.is_logged_in %}
|
||
<span class="badge bg-success">已登录</span>
|
||
{% elif a.login_msg == '登录中...' or a.login_msg == '等待人机交互...' %}
|
||
<span class="badge bg-warning">
|
||
<span class="spinner-border spinner-border-sm" style="width:.7rem;height:.7rem"></span>
|
||
{{ a.login_msg }}
|
||
</span>
|
||
{% else %}
|
||
<span class="badge bg-secondary" title="{{ a.login_msg or '' }}">未登录</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ a.updated_at }}</td>
|
||
<td>
|
||
<button class="btn btn-outline-primary btn-sm" onclick="doLogin({{ a.id }}, this)">
|
||
<i class="bi bi-key"></i> 密码登录
|
||
</button>
|
||
<button class="btn btn-outline-info btn-sm" onclick="doSmsLogin({{ a.id }}, this)">
|
||
<i class="bi bi-chat-dots"></i> 短信登录
|
||
</button>
|
||
<button class="btn btn-outline-danger btn-sm" onclick="deleteAccount({{ a.id }})">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
{% if not accounts %}
|
||
<tr><td colspan="6" class="text-center text-muted py-4">暂无账号,点击右上角添加</td></tr>
|
||
{% endif %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 添加账号弹窗 -->
|
||
<div class="modal fade" id="addModal" tabindex="-1">
|
||
<div class="modal-dialog"><div class="modal-content">
|
||
<div class="modal-header"><h5 class="modal-title">添加微店账号</h5></div>
|
||
<div class="modal-body">
|
||
<form id="addForm">
|
||
<div class="mb-3">
|
||
<label class="form-label">备注名称</label>
|
||
<input type="text" class="form-control" name="name" required placeholder="如:主号、小号1">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">手机号</label>
|
||
<input type="tel" class="form-control" name="phone" required placeholder="微店登录手机号">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">密码</label>
|
||
<input type="password" class="form-control" name="password" required placeholder="微店登录密码">
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||
<button class="btn btn-primary" onclick="addAccount()">添加</button>
|
||
</div>
|
||
</div></div>
|
||
</div>
|
||
|
||
<!-- 远程浏览器弹窗 -->
|
||
<div class="modal fade" id="rbModal" tabindex="-1" data-bs-backdrop="static">
|
||
<div class="modal-dialog modal-dialog-centered" style="max-width:430px">
|
||
<div class="modal-content" style="border-radius:16px;overflow:hidden">
|
||
<div class="modal-header py-2" style="background:#f0f4ff;border:none">
|
||
<span class="fw-bold" style="font-size:.9rem">
|
||
<i class="bi bi-phone"></i> 远程浏览器 — 短信登录
|
||
</span>
|
||
<button type="button" class="btn-close" onclick="closeRB()"></button>
|
||
</div>
|
||
<div class="modal-body p-0 text-center" style="background:#000">
|
||
<div id="rbStatus" style="color:#fff;font-size:.8rem;padding:6px 12px;background:#333">
|
||
正在启动浏览器...
|
||
</div>
|
||
<canvas id="rbCanvas" width="390" height="844"
|
||
style="width:390px;height:844px;cursor:pointer;display:block;margin:0 auto"></canvas>
|
||
</div>
|
||
<div class="modal-footer py-2 justify-content-center" style="background:#f0f4ff;border:none">
|
||
<span style="font-size:.75rem;color:#888">
|
||
直接在上方画面中点击、拖动操作浏览器
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||
<script>
|
||
var rbSocket = null, rbSessionId = null, rbDragging = false;
|
||
var rbCanvas = document.getElementById('rbCanvas');
|
||
var rbCtx = rbCanvas.getContext('2d');
|
||
|
||
function addAccount() {
|
||
var form = document.getElementById('addForm');
|
||
var data = new FormData(form);
|
||
var btn = event.target;
|
||
btn.disabled = true; btn.textContent = '添加中...';
|
||
fetch('/accounts/add', { method: 'POST', body: data })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success) {
|
||
bootstrap.Modal.getInstance(document.getElementById('addModal')).hide();
|
||
form.reset(); location.reload();
|
||
} else { alert(d.msg); btn.disabled = false; btn.textContent = '添加'; }
|
||
}).catch(function() { btn.disabled = false; btn.textContent = '添加'; });
|
||
}
|
||
|
||
function doLogin(id, btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 登录中...';
|
||
fetch('/accounts/login/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function() { pollStatus(id, btn); })
|
||
.catch(function() { btn.disabled = false; btn.innerHTML = '<i class="bi bi-key"></i> 密码登录'; });
|
||
}
|
||
|
||
function doSmsLogin(id, btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 启动中...';
|
||
fetch('/accounts/login_sms/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success && d.session_id) {
|
||
openRB(d.session_id, id, btn);
|
||
} else {
|
||
alert(d.msg || '启动失败');
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-chat-dots"></i> 短信登录';
|
||
}
|
||
}).catch(function() {
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-chat-dots"></i> 短信登录';
|
||
});
|
||
}
|
||
|
||
function openRB(sessionId, accountId, btn) {
|
||
rbSessionId = sessionId;
|
||
document.getElementById('rbStatus').textContent = '正在启动浏览器...';
|
||
rbCtx.fillStyle = '#111';
|
||
rbCtx.fillRect(0, 0, 390, 844);
|
||
rbCtx.fillStyle = '#666';
|
||
rbCtx.font = '14px sans-serif';
|
||
rbCtx.textAlign = 'center';
|
||
rbCtx.fillText('正在加载...', 195, 422);
|
||
|
||
var modal = new bootstrap.Modal(document.getElementById('rbModal'));
|
||
modal.show();
|
||
|
||
// 连接 WebSocket
|
||
if (rbSocket) { rbSocket.disconnect(); }
|
||
rbSocket = io('/rb', { transports: ['websocket', 'polling'] });
|
||
|
||
rbSocket.on('connect', function() {
|
||
console.log('RB WebSocket connected');
|
||
});
|
||
|
||
rbSocket.on('rb_frame', function(data) {
|
||
if (data.session_id !== rbSessionId) return;
|
||
var img = new Image();
|
||
img.onload = function() {
|
||
rbCtx.drawImage(img, 0, 0, 390, 844);
|
||
};
|
||
img.src = 'data:image/jpeg;base64,' + data.data;
|
||
});
|
||
|
||
rbSocket.on('rb_status', function(data) {
|
||
if (data.session_id !== rbSessionId) return;
|
||
document.getElementById('rbStatus').textContent = data.msg || '';
|
||
if (data.status === 'success') {
|
||
document.getElementById('rbStatus').style.background = '#198754';
|
||
document.getElementById('rbStatus').textContent = '✅ 登录成功!3秒后关闭...';
|
||
setTimeout(function() { closeRB(); location.reload(); }, 3000);
|
||
} else if (data.status === 'failed') {
|
||
document.getElementById('rbStatus').style.background = '#dc3545';
|
||
}
|
||
});
|
||
|
||
// Canvas 鼠标/触摸事件
|
||
rbCanvas.onmousedown = function(e) {
|
||
rbDragging = true;
|
||
var r = rbCanvas.getBoundingClientRect();
|
||
var x = e.clientX - r.left, y = e.clientY - r.top;
|
||
rbSocket.emit('rb_mouse', { session_id: rbSessionId, action: 'down', x: x, y: y });
|
||
};
|
||
rbCanvas.onmousemove = function(e) {
|
||
if (!rbDragging) return;
|
||
var r = rbCanvas.getBoundingClientRect();
|
||
var x = e.clientX - r.left, y = e.clientY - r.top;
|
||
rbSocket.emit('rb_mouse', { session_id: rbSessionId, action: 'move', x: x, y: y });
|
||
};
|
||
rbCanvas.onmouseup = function(e) {
|
||
if (!rbDragging) return;
|
||
rbDragging = false;
|
||
var r = rbCanvas.getBoundingClientRect();
|
||
var x = e.clientX - r.left, y = e.clientY - r.top;
|
||
rbSocket.emit('rb_mouse', { session_id: rbSessionId, action: 'up', x: x, y: y });
|
||
};
|
||
rbCanvas.onclick = function(e) {
|
||
if (rbDragging) return;
|
||
var r = rbCanvas.getBoundingClientRect();
|
||
var x = e.clientX - r.left, y = e.clientY - r.top;
|
||
rbSocket.emit('rb_mouse', { session_id: rbSessionId, action: 'click', x: x, y: y });
|
||
};
|
||
|
||
// 键盘事件(当 modal 打开时捕获)
|
||
document.addEventListener('keydown', rbKeyHandler);
|
||
|
||
// 存储 btn 引用用于关闭时恢复
|
||
window._rbBtn = btn;
|
||
window._rbAccountId = accountId;
|
||
}
|
||
|
||
function rbKeyHandler(e) {
|
||
if (!rbSocket || !rbSessionId) return;
|
||
// 不拦截 Escape
|
||
if (e.key === 'Escape') return;
|
||
e.preventDefault();
|
||
if (e.key.length === 1) {
|
||
rbSocket.emit('rb_keyboard', { session_id: rbSessionId, text: e.key });
|
||
} else {
|
||
rbSocket.emit('rb_key', { session_id: rbSessionId, key: e.key });
|
||
}
|
||
}
|
||
|
||
function closeRB() {
|
||
document.removeEventListener('keydown', rbKeyHandler);
|
||
if (rbSocket && rbSessionId) {
|
||
rbSocket.emit('rb_close', { session_id: rbSessionId });
|
||
rbSocket.disconnect();
|
||
rbSocket = null;
|
||
}
|
||
rbSessionId = null;
|
||
try {
|
||
bootstrap.Modal.getInstance(document.getElementById('rbModal')).hide();
|
||
} catch(e) {}
|
||
if (window._rbBtn) {
|
||
window._rbBtn.disabled = false;
|
||
window._rbBtn.innerHTML = '<i class="bi bi-chat-dots"></i> 短信登录';
|
||
}
|
||
}
|
||
|
||
function pollStatus(id, btn) {
|
||
var interval = setInterval(function() {
|
||
fetch('/accounts/status/' + id)
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.done) {
|
||
clearInterval(interval);
|
||
var row = btn ? btn.closest('tr') : null;
|
||
if (row) {
|
||
var badge = row.querySelector('.badge');
|
||
if (d.is_logged_in) {
|
||
badge.className = 'badge bg-success'; badge.textContent = '已登录';
|
||
} else {
|
||
badge.className = 'badge bg-danger'; badge.textContent = '失败';
|
||
badge.title = d.login_msg;
|
||
}
|
||
}
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-key"></i> 密码登录';
|
||
}
|
||
}
|
||
});
|
||
}, 2000);
|
||
}
|
||
|
||
function deleteAccount(id) {
|
||
if (!confirm('确定删除此账号?相关任务也会被删除。')) return;
|
||
fetch('/accounts/delete/' + id, { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) { if (d.success) location.reload(); });
|
||
}
|
||
</script>
|
||
{% endblock %}
|