Files
weidian/templates/accounts.html

299 lines
12 KiB
HTML
Raw Normal View History

{% 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 %}