Files
weibo_signin/frontend/templates/dashboard.html
Jeason 8f4e0a2411 前后端数据流对齐:
后端改动:

GET /api/v1/accounts 现在返回分页格式 {items, total, page, size, total_pages, status_counts},默认每页 12 个
批量操作用 size=500 一次拉全部
前端改动:

base.html — 加了移动端汉堡菜单、全局响应式样式、pagination disabled 状态
dashboard.html — 账号卡片分页,统计数据从 API 的 status_counts 取,移动端单列布局
account_detail.html — 签到记录改成上下两行布局(topic+状态 / 消息+时间),分页用统一的上一页/下一页样式,移动端适配
分页逻辑统一:前后端都用 page/total_pages 字段,pagination 组件显示当前页 ±2 页码。
2026-03-18 09:45:55 +08:00

212 lines
9.6 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 title %}控制台 - 微博超话签到{% endblock %}
{% block extra_css %}
<style>
.dash-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.dash-header h1 { font-size: 22px; font-weight: 700; color: #1e293b; }
.stats-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 20px; }
.stat-card {
background: rgba(255,255,255,0.9); backdrop-filter: blur(8px);
border-radius: 16px; padding: 16px; border: 1px solid rgba(255,255,255,0.6);
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
}
.stat-card .stat-icon { font-size: 22px; margin-bottom: 4px; }
.stat-card .stat-value { font-size: 24px; font-weight: 700; color: #1e293b; }
.stat-card .stat-label { font-size: 12px; color: #94a3b8; font-weight: 500; }
.account-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
.account-card {
background: rgba(255,255,255,0.92); backdrop-filter: blur(8px);
border-radius: 16px; padding: 18px; border: 1px solid rgba(255,255,255,0.6);
box-shadow: 0 1px 3px rgba(0,0,0,0.04); cursor: pointer;
transition: all 0.2s; position: relative; overflow: hidden;
}
.account-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; border-radius: 16px 16px 0 0; }
.account-card.status-active::before { background: linear-gradient(90deg, #10b981, #34d399); }
.account-card.status-pending::before { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.account-card.status-invalid_cookie::before { background: linear-gradient(90deg, #ef4444, #f87171); }
.account-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(99,102,241,0.1); }
.account-card-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.account-avatar {
width: 40px; height: 40px; border-radius: 12px;
background: linear-gradient(135deg, #6366f1, #a855f7);
display: flex; align-items: center; justify-content: center;
color: white; font-size: 16px; font-weight: 700; flex-shrink: 0;
}
.account-name { font-size: 15px; font-weight: 700; color: #1e293b; word-break: break-all; }
.account-remark { font-size: 12px; color: #94a3b8; margin-top: 2px; }
.account-meta {
display: flex; justify-content: space-between; align-items: center;
padding-top: 10px; border-top: 1px solid #f1f5f9;
}
.account-date { font-size: 11px; color: #cbd5e1; }
.account-del-btn {
width: 28px; height: 28px; border-radius: 8px; border: 1.5px solid #fecaca;
background: #fff; color: #ef4444; font-size: 12px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.account-del-btn:hover { background: #fef2f2; }
.batch-bar {
background: rgba(255,255,255,0.9); border-radius: 14px; padding: 12px 16px;
margin-bottom: 14px; border: 1px solid rgba(255,255,255,0.6);
display: flex; justify-content: space-between; align-items: center; gap: 10px;
flex-wrap: wrap;
}
.batch-bar .info { font-size: 13px; color: #64748b; }
.empty-state {
text-align: center; padding: 60px 20px; background: rgba(255,255,255,0.7);
border-radius: 20px; border: 2px dashed #e2e8f0;
}
.empty-state .empty-icon { font-size: 48px; margin-bottom: 12px; }
.empty-state p { color: #94a3b8; margin-bottom: 20px; font-size: 15px; }
@media (max-width: 768px) {
.stats-row { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.stat-card { padding: 12px; }
.stat-card .stat-value { font-size: 20px; }
.stat-card .stat-label { font-size: 11px; }
.account-grid { grid-template-columns: 1fr; }
.batch-bar { flex-direction: column; align-items: stretch; text-align: center; }
.batch-bar .info { margin-bottom: 8px; }
.dash-header { flex-direction: column; gap: 10px; align-items: flex-start; }
}
</style>
{% endblock %}
{% block content %}
<div class="dash-header">
<h1>👋 控制台</h1>
<a href="{{ url_for('add_account') }}" class="btn btn-primary">+ 添加账号</a>
</div>
{% set sc = pagination.get('status_counts', {}) %}
{% set total_accounts = pagination.get('total', 0) %}
{% set active_count = sc.get('active', 0) %}
{% set need_attention = sc.get('pending', 0) + sc.get('invalid_cookie', 0) %}
{% if total_accounts > 0 %}
<div class="stats-row">
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-value">{{ total_accounts }}</div>
<div class="stat-label">账号总数</div>
</div>
<div class="stat-card">
<div class="stat-icon"></div>
<div class="stat-value">{{ active_count }}</div>
<div class="stat-label">正常运行</div>
</div>
<div class="stat-card">
<div class="stat-icon">⚠️</div>
<div class="stat-value">{{ need_attention }}</div>
<div class="stat-label">需要关注</div>
</div>
</div>
<div class="batch-bar">
<div class="info">💡 可手动触发批量操作</div>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<button class="btn btn-secondary" id="batch-verify-btn" onclick="batchVerify()">🔍 批量验证</button>
<button class="btn btn-primary" id="batch-signin-btn" onclick="batchSignin()">🚀 全部签到</button>
</div>
</div>
<div class="account-grid">
{% for account in accounts %}
<div class="account-card status-{{ account.status }}" onclick="window.location.href='{{ url_for('account_detail', account_id=account.id) }}'">
<div class="account-card-top">
<div style="flex:1; min-width:0;">
<div class="account-name">{{ account.remark or account.weibo_user_id }}</div>
<div class="account-remark">UID: {{ account.weibo_user_id }}</div>
</div>
<div class="account-avatar">{{ (account.remark or account.weibo_user_id)[:1] }}</div>
</div>
<div class="account-meta">
<div>
{% if account.status == 'active' %}<span class="badge badge-success">正常</span>
{% elif account.status == 'pending' %}<span class="badge badge-warning">待验证</span>
{% elif account.status == 'invalid_cookie' %}<span class="badge badge-danger">Cookie 失效</span>
{% elif account.status == 'banned' %}<span class="badge badge-danger">已封禁</span>
{% endif %}
</div>
<div style="display:flex; align-items:center; gap:8px;">
<span class="account-date">{{ account.created_at[:10] }}</span>
<button class="account-del-btn" title="删除" onclick="event.stopPropagation(); deleteAccount('{{ account.id }}', '{{ account.remark or account.weibo_user_id }}');">🗑</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% if pagination.get('total_pages', 0) > 1 %}
<div class="pagination">
{% set p = pagination.page %}
{% set tp = pagination.total_pages %}
{% if p > 1 %}
<a href="?page={{ p - 1 }}"> 上一页</a>
{% else %}
<span class="disabled"> 上一页</span>
{% endif %}
{% for i in range(max(1, p - 2), min(tp, p + 2) + 1) %}
{% if i == p %}<span class="active">{{ i }}</span>
{% else %}<a href="?page={{ i }}">{{ i }}</a>{% endif %}
{% endfor %}
{% if p < tp %}
<a href="?page={{ p + 1 }}">下一页 </a>
{% else %}
<span class="disabled">下一页 </span>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="empty-state">
<div class="empty-icon">📱</div>
<p>暂无账号,扫码添加你的微博账号开始自动签到</p>
<a href="{{ url_for('add_account') }}" class="btn btn-primary" style="padding:12px 28px;">添加第一个账号</a>
</div>
{% endif %}
<script>
async function deleteAccount(id, name) {
if (!confirm(`确定要删除账号「${name}」吗?`)) return;
const form = document.createElement('form');
form.method = 'POST';
form.action = `/accounts/${id}/delete`;
document.body.appendChild(form);
form.submit();
}
async function batchVerify() {
const btn = document.getElementById('batch-verify-btn');
btn.disabled = true; btn.textContent = '⏳ 验证中...';
try {
const resp = await fetch('/api/batch/verify', {method: 'POST'});
const data = await resp.json();
if (data.success) {
const r = data.data;
alert(`验证完成:${r.valid} 有效,${r.invalid} 失效,${r.errors} 出错`);
} else { alert('验证失败: ' + (data.message || '未知错误')); }
} catch(e) { alert('请求失败: ' + e.message); }
btn.disabled = false; btn.textContent = '🔍 批量验证';
location.reload();
}
async function batchSignin() {
const btn = document.getElementById('batch-signin-btn');
if (!confirm('确定要对所有正常账号执行签到吗?')) return;
btn.disabled = true; btn.textContent = '⏳ 签到中...';
try {
const resp = await fetch('/api/batch/signin', {method: 'POST'});
const data = await resp.json();
if (data.success) {
const r = data.data;
alert(`签到完成:${r.total_accounts} 个账号,${r.total_signed} 成功,${r.total_already} 已签,${r.total_failed} 失败`);
} else { alert('签到失败: ' + (data.message || '未知错误')); }
} catch(e) { alert('请求失败: ' + e.message); }
btn.disabled = false; btn.textContent = '🚀 全部签到';
location.reload();
}
</script>
{% endblock %}