Files
weidian/templates/tasks.html
Jeason 822a4636c0 feat: Web管理系统 + Docker支持
- 多账号管理(异步登录、状态轮询)
- 购物车预售商品同步(倒计时/定时开售)
- 定时抢购(自动刷新、SKU选择、重试机制)
- 账号隔离调度(同账号顺序、跨账号并行)
- Web面板(任务分组、实时倒计时、批量操作)
- Dockerfile + docker-compose
2026-03-18 13:38:17 +08:00

273 lines
14 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>
<div>
<button class="btn btn-outline-success btn-sm me-1 rounded-pill" data-bs-toggle="modal" data-bs-target="#syncCartModal">
<i class="bi bi-cart-check"></i> 同步购物车
</button>
<button class="btn btn-outline-warning btn-sm me-1 rounded-pill" onclick="startAll()">
<i class="bi bi-play-circle"></i> 全部启动
</button>
<button class="btn btn-outline-primary btn-sm rounded-pill" data-bs-toggle="modal" data-bs-target="#addTaskModal">
<i class="bi bi-plus-lg"></i> 手动新建
</button>
</div>
</div>
{% if not grouped %}
<div class="card p-5 text-center text-muted">
<i class="bi bi-inbox" style="font-size:2.5rem"></i>
<p class="mt-2 mb-0">暂无任务,点击"同步购物车"自动添加预售商品</p>
</div>
{% else %}
{% for account_id, group in grouped.items() %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center"
style="background:linear-gradient(135deg,#f8f9ff,#eef1fb);border-radius:14px 14px 0 0;border:none">
<div>
<i class="bi bi-person-circle text-primary"></i>
<span class="fw-bold">{{ group.name }}</span>
<span class="badge bg-light text-muted ms-1">{{ group.tasks|length }} 个任务</span>
</div>
<button class="btn btn-success btn-sm rounded-pill"
onclick="startAccount({{ account_id }})">
<i class="bi bi-play-fill"></i> 启动全部
</button>
</div>
<div class="card-body p-2">
{% for t in group.tasks %}
<div class="task-row d-flex align-items-start gap-3 p-2 {% if not loop.last %}border-bottom{% endif %}">
<div class="countdown-box text-center flex-shrink-0"
data-snatch-time="{{ t.snatch_time }}" data-status="{{ t.status }}">
<div class="cd-time font-monospace">--:--:--</div>
<div class="cd-label text-muted" style="font-size:.7rem">距开售</div>
</div>
<div class="flex-grow-1 min-w-0">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="status-badge status-{{ t.status }}">{{ t.status }}</span>
{% if t.price %}
<span class="text-danger fw-bold" style="font-size:.85rem">¥{{ t.price }}</span>
{% endif %}
</div>
<div class="item-name mb-1">
{% if t.item_name %}
<a href="{{ t.target_url }}" target="_blank" class="text-decoration-none text-dark"
title="{{ t.target_url }}">{{ t.item_name[:50] }}{% if t.item_name|length > 50 %}...{% endif %}</a>
{% elif t.target_url %}
<a href="{{ t.target_url }}" target="_blank" class="text-decoration-none text-muted"
style="font-size:.85rem">{{ t.target_url[:60] }}...</a>
{% else %}<span class="text-muted">-</span>{% endif %}
</div>
<div style="font-size:.78rem" class="text-muted">
<i class="bi bi-clock"></i> {{ t.snatch_time }}
{% if t.result %}
<span class="ms-2 text-truncate d-inline-block" style="max-width:250px;vertical-align:bottom"
title="{{ t.result }}">· {{ t.result[:50] }}</span>
{% endif %}
</div>
</div>
<div class="flex-shrink-0 d-flex gap-1">
{% if t.status == 'pending' %}
<button class="btn btn-success btn-sm rounded-pill" onclick="startTask({{ t.id }})"><i class="bi bi-play-fill"></i></button>
{% elif t.id in running_ids %}
<button class="btn btn-warning btn-sm rounded-pill" onclick="stopTask({{ t.id }})"><i class="bi bi-stop-fill"></i></button>
{% endif %}
<button class="btn btn-outline-danger btn-sm rounded-pill" onclick="deleteTask({{ t.id }})"><i class="bi bi-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endif %}
<!-- 同步购物车弹窗 -->
<div class="modal fade" id="syncCartModal" tabindex="-1">
<div class="modal-dialog"><div class="modal-content rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title">同步购物车预售商品</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">获取购物车中的预售商品并自动创建抢购任务</p>
<div class="mb-3">
<label class="form-label">选择账号</label>
<select class="form-select rounded-3" id="syncAccountId">
<option value="">请选择</option>
{% for a in accounts %}
{% if a.is_logged_in %}
<option value="{{ a.id }}">{{ a.name }}</option>
{% else %}
<option value="{{ a.id }}" disabled>{{ a.name }}(未登录)</option>
{% endif %}
{% endfor %}
</select>
</div>
<div id="syncResult" class="d-none">
<div class="alert rounded-3" role="alert"></div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button class="btn btn-outline-secondary btn-sm rounded-pill" data-bs-dismiss="modal">取消</button>
<button class="btn btn-success btn-sm rounded-pill" id="syncBtn" onclick="syncCart()">
<i class="bi bi-cart-check"></i> 同步选中
</button>
<button class="btn btn-primary btn-sm rounded-pill" id="syncAllBtn" onclick="syncAllCarts()">
<i class="bi bi-arrow-repeat"></i> 同步全部
</button>
</div>
</div></div>
</div>
<!-- 手动新建任务弹窗 -->
<div class="modal fade" id="addTaskModal" tabindex="-1">
<div class="modal-dialog"><div class="modal-content rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title">手动新建抢购任务</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="taskForm">
<div class="mb-3">
<label class="form-label">选择账号</label>
<select class="form-select rounded-3" name="account_id" required>
<option value="">请选择</option>
{% for a in accounts %}
<option value="{{ a.id }}">{{ a.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">商品名称(可选)</label>
<input type="text" class="form-control rounded-3" name="item_name" placeholder="备注商品名">
</div>
<div class="mb-3">
<label class="form-label">商品链接</label>
<input type="url" class="form-control rounded-3" name="target_url" required placeholder="https://weidian.com/...">
</div>
<div class="mb-3">
<label class="form-label">抢购时间</label>
<input type="text" class="form-control rounded-3" name="snatch_time" required placeholder="2026-03-20 10:00:00">
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">商品ID可选</label>
<input type="text" class="form-control rounded-3" name="item_id">
</div>
<div class="col-6 mb-3">
<label class="form-label">SKU ID可选</label>
<input type="text" class="form-control rounded-3" name="sku_id">
</div>
</div>
<div class="mb-3">
<label class="form-label">价格(可选)</label>
<input type="text" class="form-control rounded-3" name="price">
</div>
</form>
</div>
<div class="modal-footer border-0 pt-0">
<button class="btn btn-outline-secondary btn-sm rounded-pill" data-bs-dismiss="modal">取消</button>
<button class="btn btn-primary btn-sm rounded-pill" onclick="addTask()">创建</button>
</div>
</div></div>
</div>
{% endblock %}
{% block scripts %}
<style>
.task-row { transition: background .15s; }
.task-row:hover { background: #f8f9ff; }
.countdown-box {
width: 84px; padding: 8px 4px;
background: linear-gradient(135deg, #f0f4ff, #e8ecf8);
border-radius: 10px;
}
.countdown-box.cd-active { background: linear-gradient(135deg, #fff3e0, #ffe0b2); }
.countdown-box.cd-passed { background: linear-gradient(135deg, #e8f5e9, #c8e6c9); }
.countdown-box.cd-done { background: #f5f5f5; }
.cd-time { font-size: .95rem; font-weight: 700; color: #333; line-height: 1.2; }
.cd-active .cd-time { color: #e65100; }
.cd-passed .cd-time { color: #2e7d32; font-size: .82rem; }
.cd-done .cd-time { color: #999; font-size: .82rem; }
.cd-label { line-height: 1; }
.status-badge {
display: inline-block; padding: 2px 10px; border-radius: 20px;
font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .3px;
}
.status-pending { background: #e3e8f0; color: #5a6a85; }
.status-running { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-failed { background: #f8d7da; color: #721c24; }
.status-cancelled { background: #e2e3e5; color: #383d41; }
.item-name { font-size: .92rem; font-weight: 500; }
.card-header { font-size: .9rem; }
</style>
<script>
function updateCountdowns() {
var now = new Date();
document.querySelectorAll('.countdown-box').forEach(function(box) {
var ts = box.dataset.snatchTime, st = box.dataset.status;
var ct = box.querySelector('.cd-time'), cl = box.querySelector('.cd-label');
if (['completed','cancelled'].includes(st)) {
box.className = 'countdown-box text-center flex-shrink-0 cd-done';
ct.textContent = st === 'completed' ? '已完成' : '已取消'; cl.textContent = ''; return;
}
if (st === 'failed') {
box.className = 'countdown-box text-center flex-shrink-0 cd-done';
ct.textContent = '失败'; cl.textContent = ''; return;
}
var target = new Date(ts.replace(/-/g, '/'));
if (isNaN(target.getTime())) { ct.textContent = ts || '--'; cl.textContent = ''; return; }
var diff = target - now;
if (diff <= 0) {
box.className = 'countdown-box text-center flex-shrink-0 cd-passed';
ct.textContent = '已开售'; cl.textContent = st === 'running' ? '抢购中' : ''; return;
}
box.className = 'countdown-box text-center flex-shrink-0 cd-active';
var h = Math.floor(diff/3600000), m = Math.floor((diff%3600000)/60000), s = Math.floor((diff%60000)/1000);
if (h > 24) { var d = Math.floor(h/24); ct.textContent = d + '天' + (h%24) + '时'; }
else { ct.textContent = pad(h)+':'+pad(m)+':'+pad(s); }
cl.textContent = '距开售';
});
}
function pad(n) { return n < 10 ? '0'+n : ''+n; }
updateCountdowns(); setInterval(updateCountdowns, 1000);
function syncCart() {
var id = document.getElementById('syncAccountId').value;
if (!id) { alert('请选择账号'); return; }
var btn = document.getElementById('syncBtn');
btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 同步中...';
var rd = document.getElementById('syncResult'); rd.classList.add('d-none');
fetch('/tasks/sync_cart/' + id, { method: 'POST' }).then(r=>r.json()).then(d => {
rd.classList.remove('d-none');
var a = rd.querySelector('.alert');
a.className = 'alert rounded-3 ' + (d.success ? 'alert-success' : 'alert-danger');
a.textContent = d.msg; btn.disabled = false; btn.innerHTML = '<i class="bi bi-cart-check"></i> 同步选中';
if (d.success && d.count > 0) setTimeout(() => location.reload(), 1500);
}).catch(() => { btn.disabled = false; btn.innerHTML = '<i class="bi bi-cart-check"></i> 同步选中'; });
}
function syncAllCarts() {
var btn = document.getElementById('syncAllBtn');
btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 同步中...';
var rd = document.getElementById('syncResult'); rd.classList.add('d-none');
fetch('/tasks/sync_all', { method: 'POST' }).then(r=>r.json()).then(d => {
rd.classList.remove('d-none');
var a = rd.querySelector('.alert');
a.className = 'alert rounded-3 ' + (d.success ? 'alert-success' : 'alert-danger');
a.textContent = d.msg; btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> 同步全部';
if (d.success && d.count > 0) setTimeout(() => location.reload(), 1500);
}).catch(() => { btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> 同步全部'; });
}
function addTask() {
var data = new FormData(document.getElementById('taskForm'));
fetch('/tasks/add', { method: 'POST', body: data }).then(r=>r.json()).then(d => { if (d.success) location.reload(); else alert(d.msg); });
}
function startTask(id) { fetch('/tasks/start/'+id, { method:'POST' }).then(()=>location.reload()); }
function stopTask(id) { fetch('/tasks/stop/'+id, { method:'POST' }).then(()=>location.reload()); }
function deleteTask(id) { if (!confirm('确定删除?')) return; fetch('/tasks/delete/'+id, { method:'POST' }).then(r=>r.json()).then(d => { if (d.success) location.reload(); }); }
function startAll() { if (!confirm('启动所有账号的待执行任务?')) return; fetch('/tasks/start_all', { method:'POST' }).then(r=>r.json()).then(d => { alert(d.msg); location.reload(); }); }
function startAccount(aid) { fetch('/tasks/start_account/'+aid, { method:'POST' }).then(r=>r.json()).then(d => { alert(d.msg); location.reload(); }); }
</script>
{% endblock %}