mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-04 23:17:51 +08:00
219 lines
6.6 KiB
HTML
219 lines
6.6 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block nav_key %}logs{% endblock %}
|
|
{% block title %}运行日志 | 抖音多账号续火花控制台{% endblock %}
|
|
{% block page_title %}运行日志{% endblock %}
|
|
{% block page_subtitle %}登录同步 · 发送过程 · 容器状态{% endblock %}
|
|
|
|
{% block topbar_actions %}
|
|
<a class="ghost-button" href="/">⌂ 返回首页</a>
|
|
<a class="ghost-button" href="/ops/send-console">✦ 发送控制台</a>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
.logs-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 16px;
|
|
}
|
|
.logs-toolbar__left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.logs-meta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: var(--text-soft);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.logs-meta .dot {
|
|
width: 9px;
|
|
height: 9px;
|
|
border-radius: 50%;
|
|
background: var(--fire-grad);
|
|
box-shadow: 0 0 0 4px rgba(255, 122, 61, 0.14);
|
|
}
|
|
.logs-meta .dot.live {
|
|
background: linear-gradient(135deg, #18a558, #4fd08a);
|
|
box-shadow: 0 0 0 4px rgba(24, 165, 88, 0.16);
|
|
animation: livePulse 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes livePulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.45; }
|
|
}
|
|
.toggle-switch {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
.toggle-track {
|
|
width: 42px;
|
|
height: 24px;
|
|
border-radius: 999px;
|
|
background: var(--border-strong);
|
|
position: relative;
|
|
transition: background 0.18s ease;
|
|
flex: 0 0 42px;
|
|
}
|
|
.toggle-track::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
box-shadow: 0 2px 5px rgba(20, 35, 70, 0.18);
|
|
transition: transform 0.18s ease;
|
|
}
|
|
.toggle-switch input { display: none; }
|
|
.toggle-switch input:checked + .toggle-track {
|
|
background: linear-gradient(180deg, var(--primary), var(--primary-strong));
|
|
}
|
|
.toggle-switch input:checked + .toggle-track::after {
|
|
transform: translateX(18px);
|
|
}
|
|
.logs-code {
|
|
margin: 0;
|
|
border-radius: var(--radius-md);
|
|
background: #0f1728;
|
|
color: #d8e2fb;
|
|
padding: 18px 20px;
|
|
overflow: auto;
|
|
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
font-size: 12.5px;
|
|
line-height: 1.75;
|
|
min-height: 420px;
|
|
max-height: 70vh;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
tab-size: 2;
|
|
}
|
|
.logs-code:empty::before {
|
|
content: "暂无日志输出";
|
|
color: #5f6f8c;
|
|
}
|
|
.logs-empty {
|
|
padding: 60px 24px;
|
|
text-align: center;
|
|
border-radius: var(--radius-md);
|
|
border: 1px dashed var(--border-strong);
|
|
background: var(--empty-bg);
|
|
color: var(--text-soft);
|
|
}
|
|
</style>
|
|
|
|
<section class="panel">
|
|
<div class="panel-header">
|
|
<div>
|
|
<h2>详细日志</h2>
|
|
<p class="muted compact">展示最近的任务输出,便于检查登录同步、发送过程和容器状态。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logs-toolbar">
|
|
<div class="logs-toolbar__left">
|
|
<span class="logs-meta"><span class="dot" id="logs-live-dot"></span><span id="logs-status-text">最近更新:刚刚</span></span>
|
|
<span class="logs-meta"><span class="dot"></span><span id="logs-line-count">{{ (log_tail|trim).splitlines()|length }} 行</span></span>
|
|
</div>
|
|
<div class="logs-toolbar__left">
|
|
<label class="toggle-switch" title="每 5 秒自动刷新日志">
|
|
<input type="checkbox" id="logs-autorefresh">
|
|
<span class="toggle-track"></span>
|
|
<span>自动刷新</span>
|
|
</label>
|
|
<button type="button" class="ghost-button" id="logs-refresh-btn">↻ 刷新</button>
|
|
<a class="ghost-button" id="logs-download-btn" href="data:text/plain;charset=utf-8,{{ log_tail|urlencode }}">⬇ 下载日志</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if log_tail and log_tail.strip() %}
|
|
<pre class="logs-code" id="logs-code">{{ log_tail }}</pre>
|
|
{% else %}
|
|
<div class="logs-empty" id="logs-empty">当前没有日志输出。任务运行后这里会实时显示。</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<script>
|
|
(() => {
|
|
const codeEl = document.getElementById("logs-code");
|
|
const emptyEl = document.getElementById("logs-empty");
|
|
const statusEl = document.getElementById("logs-status-text");
|
|
const lineEl = document.getElementById("logs-line-count");
|
|
const liveDot = document.getElementById("logs-live-dot");
|
|
const toggle = document.getElementById("logs-autorefresh");
|
|
const refreshBtn = document.getElementById("logs-refresh-btn");
|
|
let timer = null;
|
|
|
|
const fmtTime = () => {
|
|
const d = new Date();
|
|
const p = (n) => String(n).padStart(2, "0");
|
|
return `最近更新:${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
};
|
|
|
|
const renderBody = (text) => {
|
|
const has = !!(text && text.trim());
|
|
if (has) {
|
|
if (emptyEl) emptyEl.style.display = "none";
|
|
if (codeEl) {
|
|
codeEl.style.display = "";
|
|
codeEl.textContent = text;
|
|
}
|
|
if (lineEl) lineEl.textContent = `${text.trim().split(/\r?\n/).length} 行`;
|
|
} else {
|
|
if (codeEl) codeEl.style.display = "none";
|
|
if (emptyEl) emptyEl.style.display = "";
|
|
if (lineEl) lineEl.textContent = "0 行";
|
|
}
|
|
};
|
|
|
|
const refresh = async () => {
|
|
try {
|
|
const res = await fetch("/ops/logs", { credentials: "same-origin", headers: { "Accept": "text/html" } });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const html = await res.text();
|
|
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
const fetched = doc.getElementById("logs-code");
|
|
const fetchedEmpty = doc.getElementById("logs-empty");
|
|
if (fetched) {
|
|
renderBody(fetched.textContent);
|
|
} else if (fetchedEmpty) {
|
|
renderBody("");
|
|
}
|
|
if (statusEl) statusEl.textContent = fmtTime();
|
|
} catch (err) {
|
|
if (statusEl) statusEl.textContent = `刷新失败:${err.message}`;
|
|
}
|
|
};
|
|
|
|
const setLive = (on) => {
|
|
if (liveDot) liveDot.classList.toggle("live", on);
|
|
if (timer) { clearInterval(timer); timer = null; }
|
|
if (on) {
|
|
timer = setInterval(refresh, 5000);
|
|
refresh();
|
|
}
|
|
};
|
|
|
|
if (toggle) toggle.addEventListener("change", () => setLive(toggle.checked));
|
|
if (refreshBtn) refreshBtn.addEventListener("click", refresh);
|
|
if (statusEl) statusEl.textContent = fmtTime();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|