Files
weidian/templates/accounts.html
Jeason 8b64d7e69e feat: 远程浏览器功能 - Web面板内嵌操作滑块验证
- 新增 remote_browser.py: CDP screencast截图流 + 鼠标/键盘事件转发
- Flask-SocketIO 实时通信
- 短信登录时弹出远程浏览器窗口,用户直接在Web页面拖滑块
- 自动检测登录成功并保存auth状态
2026-04-01 13:56:27 +08:00

299 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}