jobs.html
6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
{% extends "base.html" %}
{% block title %}任务监控 - WeChat Viewer{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.jobs-container{max-width:960px;margin:0 auto;padding:20px;}
.jobs-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;}
.jobs-header h1{font-size:20px;color:#2c3e50;}
.jobs-header button{padding:6px 14px;background:#3498db;color:white;border:none;border-radius:6px;cursor:pointer;font-size:13px;}
.job-card{display:flex;align-items:center;gap:14px;background:white;border-radius:8px;padding:14px 18px;margin-bottom:10px;box-shadow:0 1px 2px rgba(0,0,0,0.05);cursor:pointer;text-decoration:none;color:inherit;}
.job-card:hover{background:#f8f9fa;}
.job-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.job-dot.running{background:#27ae60;}
.job-dot.completed{background:#3498db;}
.job-dot.failed{background:#e74c3c;}
.job-info{flex:1;min-width:0;}
.job-info .name{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.job-info .meta{font-size:11px;color:#999;margin-top:3px;}
.job-badge{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;flex-shrink:0;}
.job-badge.daily_report{background:#2c3e50;color:white;}
.job-badge.report_image{background:#e67e22;color:white;}
.job-badge.article_summary{background:#3498db;color:white;}
.job-badge.article_learn{background:#9b59b6;color:white;}
.job-badge.article_image{background:#1abc9c;color:white;}
.job-arrow{color:#ccc;font-size:18px;}
.empty-state{text-align:center;padding:60px 20px;color:#999;}
.empty-state .icon{font-size:48px;margin-bottom:12px;}
.loading{text-align:center;padding:40px;color:#999;}
.spinner{display:inline-block;width:24px;height:24px;border:3px solid #ddd;border-top-color:#3498db;border-radius:50%;animation:spin .6s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
<div class="jobs-container">
<div class="jobs-header">
<h1>🔧 任务监控</h1>
<button onclick="refresh()">🔄 刷新</button>
</div>
<div class="rbar" style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center;">
<input type="text" id="searchInput" placeholder="搜索任务名..." oninput="render()" style="flex:1;min-width:120px;padding:10px;border:1px solid #ddd;border-radius:8px;font-size:14px;">
<select id="typeFilter" onchange="render()" style="padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;">
<option value="">全部类型</option>
<option value="daily_report">日报生成</option>
<option value="report_image">日报图片</option>
<option value="article_summary">文章摘要</option>
<option value="article_learn">文章学习</option>
<option value="article_image">文章图片</option>
</select>
<select id="statusFilter" onchange="render()" style="padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;">
<option value="">全部状态</option>
<option value="running">运行中</option>
<option value="completed">已完成</option>
<option value="failed">失败</option>
</select>
<select id="sortBy" onchange="render()" style="padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;">
<option value="time">按时间↓</option>
<option value="name">按名称</option>
<option value="type">按类型</option>
</select>
</div>
<div id="list"></div>
</div>
<script>
var TYPES = {daily_report:"日报生成",report_image:"日报图片",article_summary:"文章摘要",article_learn:"文章学习",article_image:"文章图片"};
var STATS = {running:"运行中",completed:"已完成",failed:"失败"};
var allJobs = [];
function refresh() {
var el = document.getElementById("list");
el.innerHTML = '<div class="loading"><div class="spinner"></div><p style="margin-top:12px;">加载中...</p></div>';
fetch("/api/jobs").then(function(r){return r.json()}).then(function(j){
if (!j.ok || !j.data.length) {
el.innerHTML = '<div class="empty-state"><div class="icon">📋</div><p>暂无任务</p></div>';
allJobs = [];
render();
return;
}
allJobs = j.data;
render();
}).catch(function(e){
el.innerHTML = '<div class="empty-state"><p>加载失败: ' + e.message + '</p></div>';
});
}
function render() {
var el = document.getElementById("list");
var q = (document.getElementById("searchInput").value || '').trim().toLowerCase();
var tf = document.getElementById("typeFilter").value;
var sf = document.getElementById("statusFilter").value;
var sb = document.getElementById("sortBy").value;
var filtered = [];
for (var i = 0; i < allJobs.length; i++) {
var t = allJobs[i];
if (q) {
var hay = (t.job_name || '').toLowerCase();
if (hay.indexOf(q) < 0) continue;
}
if (tf && t.job_type !== tf) continue;
if (sf && t.status !== sf) continue;
filtered.push(t);
}
if (sb === 'name') filtered.sort(function(a,b){return (a.job_name||'').localeCompare(b.job_name||'')});
else if (sb === 'type') filtered.sort(function(a,b){return (a.job_type||'').localeCompare(b.job_type||'')});
else filtered.sort(function(a,b){return (b.started_at||'').localeCompare(a.started_at||'')});
if (!filtered.length) {
el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>无匹配任务</p></div>';
return;
}
var h = "";
for (var i = 0; i < filtered.length; i++) {
var t = filtered[i];
var tn = TYPES[t.job_type] || t.job_type;
var sn = STATS[t.status] || t.status;
var name = t.job_name || "未命名";
var meta = t.started_at || "";
if (t.completed_at) meta += " → " + t.completed_at;
meta += " · " + sn;
h += "<a href=/job/" + t.task_id + " class=job-card>";
h += "<span class='job-dot " + t.status + "'></span>";
h += "<div class=job-info><div class=name>" + escHtml(name) + "</div>";
h += "<div class=meta>" + escHtml(meta) + "</div></div>";
h += "<span class='job-badge " + t.job_type + "'>" + escHtml(tn) + "</span>";
h += "<span class=job-arrow>›</span></a>";
}
el.innerHTML = h;
}
function escHtml(s) {
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
}
refresh();
setInterval(refresh, 5000);
</script>
{% endblock %}