jobs.html 6.29 KB
{% 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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}

refresh();
setInterval(refresh, 5000);
</script>
{% endblock %}