Files
weidian/templates/tasks.html

273 lines
14 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>
<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 %}