Files
douyin-sparkflow/DouYinSparkFlow/webui/templates/send_console.html
T

643 lines
19 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 nav_key %}send_console{% endblock %}
{% block title %}发送控制台{% endblock %}
{% block page_title %}发送控制台{% endblock %}
{% block page_subtitle %}今日发送总览{% endblock %}
{% block topbar_actions %}
<a class="ghost-button" href="/">⌂ 返回首页</a>
<form method="post" action="/ops/run-unsent">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="soft-button" type="submit">补发未成功目标</button>
</form>
<form method="post" action="/ops/run-now">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">✈ 补发全部对象</button>
</form>
{% endblock %}
{% block content %}
{% set send_console = ops.send_console %}
<style>
.console-shell {
display: grid;
gap: 18px;
}
.console-overview {
display: grid;
gap: 18px;
}
.console-stats .stat-card {
min-height: 132px;
}
.console-kpi-icon {
font-size: 26px;
}
.console-account-list {
display: grid;
gap: 22px;
}
.account-panel {
padding: 20px 20px 22px;
}
.account-panel__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.account-panel__meta {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.account-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(180deg, #7ea9ff, #5888ff);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 800;
box-shadow: 0 10px 18px rgba(47, 103, 255, 0.22);
}
.account-panel__name {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
}
.account-panel__subline {
margin-top: 6px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-soft);
font-size: 13px;
flex-wrap: wrap;
}
.account-panel__subline code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
color: #7486a4;
background: #f5f8fd;
padding: 2px 6px;
border-radius: 999px;
}
.account-panel__status {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.account-panel__collapse {
color: #98a8c4;
font-size: 18px;
line-height: 1;
}
.account-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.data-card {
border: 1px solid rgba(228, 236, 246, 0.96);
border-radius: 18px;
background: linear-gradient(180deg, #ffffff, #fdfdff);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.data-card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px;
}
.data-card__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 700;
}
.data-card__title i {
font-style: normal;
display: inline-flex;
width: 18px;
justify-content: center;
}
.data-card__title.success {
color: var(--success);
}
.data-card__title.danger {
color: var(--danger);
}
.data-card__title.warning {
color: var(--warning);
}
.data-card__title.info {
color: var(--info);
}
.data-card__table {
border-left: none;
border-right: none;
border-radius: 0;
}
.data-card__table td:first-child,
.data-card__table th:first-child {
min-width: 176px;
}
.data-card__table td:last-child,
.data-card__table th:last-child {
white-space: nowrap;
}
.data-card__footer {
padding: 10px 18px 14px;
display: flex;
justify-content: flex-end;
font-size: 13px;
color: var(--primary);
font-weight: 700;
}
.data-card__footer-button {
border: none;
background: transparent;
color: var(--primary);
padding: 0;
box-shadow: none;
border-radius: 0;
font-size: 13px;
font-weight: 700;
}
.data-card__footer-button:hover {
transform: none;
box-shadow: none;
text-decoration: underline;
}
.data-card__footer-button[hidden] {
display: none;
}
.data-card__extra {
display: none;
padding: 0 18px 16px;
}
.data-card__extra.is-open {
display: block;
}
.data-card__extra .table-shell {
border-radius: 14px;
}
.retry-button {
padding: 8px 12px;
border-radius: 10px;
font-size: 12px;
white-space: nowrap;
}
.console-empty {
padding: 46px 24px;
text-align: center;
color: var(--text-soft);
border-radius: 18px;
border: 1px dashed var(--border-strong);
background: rgba(255, 255, 255, 0.74);
}
.console-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 18px;
padding: 6px 0 0;
color: var(--text-soft);
font-size: 13px;
}
.console-pagination {
display: inline-flex;
align-items: center;
gap: 8px;
}
.console-page {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid var(--border);
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-soft);
font-weight: 700;
}
.console-page.active {
background: linear-gradient(180deg, #3d79ff, #2f67ff);
color: #fff;
border-color: transparent;
box-shadow: 0 10px 18px rgba(47, 103, 255, 0.18);
}
.console-page.ghost {
width: auto;
padding: 0 12px;
}
@media (max-width: 1380px) {
.account-grid {
grid-template-columns: 1fr;
}
}
</style>
<div class="console-shell">
<section class="console-overview">
<div class="overview-time">当前时间:{{ send_console.now|replace('T', ' ') }}</div>
<div class="stats-grid console-stats">
<article class="stat-card">
<div class="stat-meta">
<span class="stat-label">启用账号</span>
<strong class="stat-value">{{ send_console.summary.enabled_accounts }}</strong>
<span class="stat-help">实时统计 · 当前可发送账号</span>
</div>
<div class="stat-icon blue"><span class="console-kpi-icon">👥</span></div>
</article>
<article class="stat-card">
<div class="stat-meta">
<span class="stat-label">今日成功目标</span>
<strong class="stat-value">{{ send_console.summary.today_sent_targets }}</strong>
<span class="stat-help positive">发送成功后立即写回历史</span>
</div>
<div class="stat-icon green"><span class="console-kpi-icon">🎯</span></div>
</article>
<article class="stat-card">
<div class="stat-meta">
<span class="stat-label">失败待补发</span>
<strong class="stat-value">{{ send_console.summary.today_failed_targets }}</strong>
<span class="stat-help negative">按失败分类进入补发与重试</span>
</div>
<div class="stat-icon red"><span class="console-kpi-icon"></span></div>
</article>
<article class="stat-card">
<div class="stat-meta">
<span class="stat-label">待发送 / 未处理</span>
<strong class="stat-value">{{ send_console.summary.today_pending_targets + send_console.summary.today_unprocessed_targets }}</strong>
<span class="stat-help">窗口内按计划发送,窗口外展示剩余目标</span>
</div>
<div class="stat-icon orange"><span class="console-kpi-icon"></span></div>
</article>
</div>
</section>
{% if send_console.accounts %}
<section class="console-account-list">
{% for account in send_console.accounts %}
<article class="panel account-panel">
<div class="account-panel__header">
<div class="account-panel__meta">
<div class="account-avatar">{{ (account.username or 'A')[:1] }}</div>
<div>
<div class="account-panel__name">{{ account.username }}</div>
<div class="account-panel__subline">
<span>unique_id:</span>
<code>{{ account.unique_id }}</code>
</div>
</div>
</div>
<div class="account-panel__status">
<span class="status-chip success">成功 {{ account.sent_targets|length }}</span>
<span class="status-chip danger">失败 {{ account.failed_targets|length }}</span>
<span class="status-chip warning">待发送 {{ account.pending_targets|length }}</span>
<span class="status-chip info">未处理 {{ account.unprocessed_targets|length }}</span>
<span class="account-panel__collapse"></span>
</div>
</div>
<div class="account-grid">
<section class="data-card">
<div class="data-card__head">
<div class="data-card__title success"><i></i><span>A. 今日已成功</span></div>
</div>
<div class="table-shell data-card__table">
<table>
<thead>
<tr>
<th>目标</th>
<th>消息</th>
<th>sentAt</th>
</tr>
</thead>
<tbody>
{% for item in account.sent_targets[:5] %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.message or "-" }}</td>
<td>{{ item.sentAt or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="3">今日暂无成功目标</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="data-card__footer">
<button
type="button"
class="data-card__footer-button"
data-expand-target="sent-{{ loop.index0 }}"
data-total-label="查看全部({{ account.sent_targets|length }}"
{% if account.sent_targets|length <= 5 %}hidden{% endif %}
>查看全部({{ account.sent_targets|length }}</button>
</div>
<div class="data-card__extra" id="sent-{{ loop.index0 }}">
<div class="table-shell scrollable">
<table>
<thead>
<tr>
<th>目标</th>
<th>消息</th>
<th>sentAt</th>
</tr>
</thead>
<tbody>
{% for item in account.sent_targets %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.message or "-" }}</td>
<td>{{ item.sentAt or "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
<section class="data-card">
<div class="data-card__head">
<div class="data-card__title danger"><i></i><span>B. 失败待补发</span></div>
</div>
<div class="table-shell data-card__table scrollable">
<table>
<thead>
<tr>
<th>目标</th>
<th>失败分类</th>
<th>失败原因</th>
<th>次数</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in account.failed_targets[:5] %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.category or "-" }}</td>
<td>{{ item.reason or "-" }}</td>
<td>{{ item.attemptCount or 0 }}</td>
<td>
<form method="post" action="/accounts/{{ account.unique_id }}/retry-target">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="target" value="{{ item.target }}">
<button type="submit" class="soft-button retry-button">重试此目标</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="5">暂无失败待补发目标</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="data-card__footer">
<button
type="button"
class="data-card__footer-button"
data-expand-target="failed-{{ loop.index0 }}"
data-total-label="查看全部({{ account.failed_targets|length }}"
{% if account.failed_targets|length <= 5 %}hidden{% endif %}
>查看全部({{ account.failed_targets|length }}</button>
</div>
<div class="data-card__extra" id="failed-{{ loop.index0 }}">
<div class="table-shell scrollable">
<table>
<thead>
<tr>
<th>目标</th>
<th>失败分类</th>
<th>失败原因</th>
<th>次数</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in account.failed_targets %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.category or "-" }}</td>
<td>{{ item.reason or "-" }}</td>
<td>{{ item.attemptCount or 0 }}</td>
<td>
<form method="post" action="/accounts/{{ account.unique_id }}/retry-target">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="target" value="{{ item.target }}">
<button type="submit" class="soft-button retry-button">重试此目标</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
<section class="data-card">
<div class="data-card__head">
<div class="data-card__title warning"><i></i><span>C. 今日待发送</span></div>
</div>
<div class="table-shell data-card__table">
<table>
<thead>
<tr>
<th>目标</th>
<th>scheduledAt</th>
</tr>
</thead>
<tbody>
{% for item in account.pending_targets[:5] %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.scheduledAt or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="2">今日暂无待发送目标</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="data-card__footer">
<button
type="button"
class="data-card__footer-button"
data-expand-target="pending-{{ loop.index0 }}"
data-total-label="查看全部({{ account.pending_targets|length }}"
{% if account.pending_targets|length <= 5 %}hidden{% endif %}
>查看全部({{ account.pending_targets|length }}</button>
</div>
<div class="data-card__extra" id="pending-{{ loop.index0 }}">
<div class="table-shell scrollable">
<table>
<thead>
<tr>
<th>目标</th>
<th>scheduledAt</th>
</tr>
</thead>
<tbody>
{% for item in account.pending_targets %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.scheduledAt or "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
<section class="data-card">
<div class="data-card__head">
<div class="data-card__title info"><i></i><span>D. 今日未处理</span></div>
</div>
<div class="table-shell data-card__table">
<table>
<thead>
<tr>
<th>目标</th>
<th>scheduledAt</th>
</tr>
</thead>
<tbody>
{% for item in account.unprocessed_targets[:5] %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.scheduledAt or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="2">今日暂无未处理目标</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="data-card__footer">
<button
type="button"
class="data-card__footer-button"
data-expand-target="unprocessed-{{ loop.index0 }}"
data-total-label="查看全部({{ account.unprocessed_targets|length }}"
{% if account.unprocessed_targets|length <= 5 %}hidden{% endif %}
>查看全部({{ account.unprocessed_targets|length }}</button>
</div>
<div class="data-card__extra" id="unprocessed-{{ loop.index0 }}">
<div class="table-shell scrollable">
<table>
<thead>
<tr>
<th>目标</th>
<th>scheduledAt</th>
</tr>
</thead>
<tbody>
{% for item in account.unprocessed_targets %}
<tr>
<td>{{ item.target }}</td>
<td>{{ item.scheduledAt or "-" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
</div>
</article>
{% endfor %}
</section>
<div class="console-footer">
<span>共 {{ send_console.accounts|length }} 个账号</span>
<div class="console-pagination">
<span class="console-page ghost"></span>
<span class="console-page active">1</span>
<span class="console-page">2</span>
<span class="console-page">3</span>
<span class="console-page ghost"></span>
</div>
</div>
{% else %}
<div class="console-empty">当前没有启用账号,暂无可展示的发送控制台数据。</div>
{% endif %}
</div>
<script>
(() => {
document.querySelectorAll(".data-card__footer-button").forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.dataset.expandTarget;
if (!targetId) return;
const panel = document.getElementById(targetId);
if (!panel) return;
const isOpen = panel.classList.toggle("is-open");
button.textContent = isOpen ? "收起" : (button.getAttribute("data-total-label") || "查看全部");
});
});
})();
</script>
{% endblock %}