dashboard.html 11.9 KB
{% extends "base.html" %}
{% block title %}数据看板 - WeChat Viewer{% endblock %}
{% block page_title %}| 数据看板{% endblock %}

{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.dashboard-container { padding: 20px; max-width: 1400px; margin: 0 auto; }
.dashboard-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; flex-wrap: wrap; gap: 12px; }
.dashboard-header h2 { font-size: 22px; color: #2c3e50; }
.dashboard-header .refresh-bar { display: flex; align-items: center; gap: 12px; }
.dashboard-header .refresh-btn { padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
.dashboard-header .refresh-btn:hover { background: #2980b9; }
.dashboard-header .last-update { font-size: 12px; color: #999; }

.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
.dash-card { background: white; border-radius: 10px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); border-top: 4px solid #3498db; transition: transform .15s; }
.dash-card:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.12); }
.dash-card.blue { border-top-color: #3498db; }
.dash-card.green { border-top-color: #27ae60; }
.dash-card.purple { border-top-color: #8e44ad; }
.dash-card.orange { border-top-color: #e67e22; }
.dash-card.gray { border-top-color: #7f8c8d; }
.dash-card.teal { border-top-color: #1abc9c; }
.dash-card.pink { border-top-color: #e84393; }
.dash-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; }
.dash-card-header .icon { font-size: 20px; }
.dash-card-header .title { font-size: 15px; font-weight: 600; color: #2c3e50; }
.dash-card-metrics { display: flex; flex-wrap: wrap; gap: 10px; }
.metric-item { flex: 1; min-width: 60px; text-align: center; padding: 8px 4px; background: #f8f9fa; border-radius: 6px; }
.metric-item .value { font-size: 22px; font-weight: bold; color: #2c3e50; }
.metric-item .value.null { color: #bbb; font-size: 14px; }
.metric-item .label { font-size: 11px; color: #999; margin-top: 2px; }
.metric-item .pct { font-size: 13px; color: #27ae60; font-weight: bold; }
.metric-item .pct.warn { color: #e67e22; }
.metric-item .pct.bad { color: #e74c3c; }

.detail-row { font-size: 12px; color: #666; padding: 3px 0; display: flex; justify-content: space-between; border-top: 1px solid #f0f0f0; margin-top: 8px; padding-top: 8px; }
.detail-row:first-of-type { border-top: none; margin-top: 6px; padding-top: 6px; }

.loading-state { text-align: center; padding: 60px 20px; color: #999; }
.error-state { text-align: center; padding: 40px 20px; color: #e74c3c; background: #fdf0ef; border-radius: 8px; margin-bottom: 20px; display: none; }

@media (max-width: 768px) {
  .dashboard-container { padding: 12px; }
  .card-grid { grid-template-columns: 1fr; }
  .dashboard-header { flex-direction: column; align-items: flex-start; }
}
</style>

<div class="dashboard-container">
  <div class="dashboard-header">
    <h2>📊 数据看板</h2>
    <div class="refresh-bar">
      <span class="last-update" id="lastUpdate">加载中...</span>
      <button class="refresh-btn" id="refreshBtn" onclick="refreshDashboard()">🔄 刷新</button>
    </div>
  </div>

  <div class="error-state" id="errorBox"></div>

  <div class="loading-state" id="loadingBox">
    <div class="spinner"></div>
    <p style="margin-top:12px">加载数据中...</p>
  </div>

  <div class="card-grid" id="cardGrid" style="display:none"></div>
</div>

<script>
var AUTO_REFRESH_SEC = 60;
var refreshTimer = null;

function fmt(n) {
  if (n === null || n === undefined) return '<span class="value null">-</span>';
  return '<span class="value">' + n.toLocaleString() + '</span>';
}

function pctClass(v) {
  if (v >= 80) return 'pct';
  if (v >= 50) return 'pct warn';
  return 'pct bad';
}

function renderCards(data) {
  var html = '';

  // Chats (blue)
  html += '<div class="dash-card blue">' +
    '<div class="dash-card-header"><span class="icon">💬</span><span class="title">聊天数据</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + (data.chats.groups !== null ? data.chats.groups.toLocaleString() : '-') + '</div><div class="label">群聊</div></div>' +
      '<div class="metric-item"><div class="value">' + (data.chats.messages !== null ? data.chats.messages.toLocaleString() : '-') + '</div><div class="label">消息</div></div>' +
      '<div class="metric-item"><div class="value">' + (data.chats.speakers !== null ? data.chats.speakers.toLocaleString() : '-') + '</div><div class="label">发言者</div></div>' +
      '<div class="metric-item"><div class="value">' + data.chats.shares.toLocaleString() + '</div><div class="label">分享</div></div>' +
    '</div></div>';

  // Articles (green)
  html += '<div class="dash-card green">' +
    '<div class="dash-card-header"><span class="icon">📰</span><span class="title">文章数据</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.articles.total.toLocaleString() + '</div><div class="label">总数</div></div>' +
      '<div class="metric-item"><div class="value">' + data.articles.summarized.toLocaleString() + '</div><div class="label">已摘要</div></div>' +
      '<div class="metric-item"><div class="value">' + data.articles.unsummarized.toLocaleString() + '</div><div class="label">未摘要</div></div>' +
      '<div class="metric-item"><div class="value">' + data.articles.learned.toLocaleString() + '</div><div class="label">已学习</div></div>' +
    '</div></div>';

  // Learn (purple)
  var learnHtml = '<div class="dash-card purple">' +
    '<div class="dash-card-header"><span class="icon">🎓</span><span class="title">学习审核</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.learn.pending_review.toLocaleString() + '</div><div class="label">待审核</div></div>' +
      '<div class="metric-item"><div class="value">' + data.learn.applied.toLocaleString() + '</div><div class="label">已吸收</div></div>' +
      '<div class="metric-item"><div class="value">' + data.learn.unclassified.toLocaleString() + '</div><div class="label">待分类</div></div>' +
      '<div class="metric-item"><div class="value">' + data.learn.rejected.toLocaleString() + '</div><div class="label">已拒绝</div></div>' +
    '</div>';
  if (data.learn.by_type) {
    learnHtml += '<div class="detail-row"><span>吸收分布</span><span>';
    var types = [];
    for (var t in data.learn.by_type) {
      if (data.learn.by_type[t] > 0) types.push(t + ': ' + data.learn.by_type[t]);
    }
    learnHtml += (types.length > 0 ? types.join(' / ') : '无');
    learnHtml += '</span></div>';
  }
  learnHtml += '</div>';
  html += learnHtml;

  // Reports (orange)
  html += '<div class="dash-card orange">' +
    '<div class="dash-card-header"><span class="icon">📋</span><span class="title">日报</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.reports.daily.toLocaleString() + '</div><div class="label">日报</div></div>' +
      '<div class="metric-item"><div class="value">' + data.reports.summaries.toLocaleString() + '</div><div class="label">摘要</div></div>' +
      '<div class="metric-item"><div class="value">' + data.reports.learn_records.toLocaleString() + '</div><div class="label">学习记录</div></div>' +
      '<div class="metric-item"><div class="value">' + data.reports.images.toLocaleString() + '</div><div class="label">图片</div></div>' +
    '</div>';
  if (data.reports.last_generated) {
    html += '<div class="detail-row"><span>最近生成</span><span>' + data.reports.last_generated + '</span></div>';
  }
  html += '</div>';

  // Jobs (gray)
  var successHtml = '';
  if (data.jobs.success_rate !== null) {
    var cls = data.jobs.success_rate >= 80 ? 'pct' : (data.jobs.success_rate >= 50 ? 'pct warn' : 'pct bad');
    successHtml = '<div class="metric-item"><div class="' + cls + '">' + data.jobs.success_rate + '%</div><div class="label">成功率</div></div>';
  }
  html += '<div class="dash-card gray">' +
    '<div class="dash-card-header"><span class="icon">🔧</span><span class="title">任务监控</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.jobs.total.toLocaleString() + '</div><div class="label">总数</div></div>' +
      '<div class="metric-item"><div class="value" style="color:#f39c12">' + data.jobs.running.toLocaleString() + '</div><div class="label">运行中</div></div>' +
      '<div class="metric-item"><div class="value" style="color:#27ae60">' + data.jobs.completed.toLocaleString() + '</div><div class="label">已完成</div></div>' +
      '<div class="metric-item"><div class="value" style="color:#e74c3c">' + data.jobs.failed.toLocaleString() + '</div><div class="label">失败</div></div>' +
      successHtml +
    '</div>';
  if (data.jobs.last_completed) {
    html += '<div class="detail-row"><span>最后完成</span><span>' + data.jobs.last_completed + '</span></div>';
  }
  html += '</div>';

  // Auto Reply (teal)
  html += '<div class="dash-card teal">' +
    '<div class="dash-card-header"><span class="icon">🤖</span><span class="title">自动回复</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.auto_reply.whitelist.toLocaleString() + '</div><div class="label">白名单</div></div>' +
      '<div class="metric-item"><div class="value" style="color:#f39c12">' + data.auto_reply.pending.toLocaleString() + '</div><div class="label">待审批</div></div>' +
      '<div class="metric-item"><div class="value" style="color:#27ae60">' + data.auto_reply.sent.toLocaleString() + '</div><div class="label">已发送</div></div>' +
    '</div></div>';

  // AI Circle (pink)
  html += '<div class="dash-card pink">' +
    '<div class="dash-card-header"><span class="icon">💬</span><span class="title">AI 对话</span></div>' +
    '<div class="dash-card-metrics">' +
      '<div class="metric-item"><div class="value">' + data.circle.conversations.toLocaleString() + '</div><div class="label">会话</div></div>' +
      '<div class="metric-item"><div class="value">' + data.circle.messages.toLocaleString() + '</div><div class="label">消息</div></div>' +
    '</div></div>';

  document.getElementById('cardGrid').innerHTML = html;
  document.getElementById('cardGrid').style.display = '';
  document.getElementById('loadingBox').style.display = 'none';
}

async function refreshDashboard() {
  document.getElementById('loadingBox').style.display = '';
  document.getElementById('cardGrid').style.display = 'none';
  document.getElementById('errorBox').style.display = 'none';
  document.getElementById('refreshBtn').disabled = true;

  try {
    var r = await fetch('/api/dashboard');
    var j = await r.json();
    if (j.ok) {
      renderCards(j.data);
      var now = new Date();
      document.getElementById('lastUpdate').textContent = '更新于 ' +
        now.getHours().toString().padStart(2,'0') + ':' +
        now.getMinutes().toString().padStart(2,'0') + ':' +
        now.getSeconds().toString().padStart(2,'0');
    } else {
      throw new Error(j.error || 'Unknown error');
    }
  } catch(e) {
    document.getElementById('loadingBox').style.display = 'none';
    document.getElementById('cardGrid').style.display = 'none';
    document.getElementById('errorBox').style.display = 'block';
    document.getElementById('errorBox').textContent = '⚠️ 加载失败: ' + e.message;
  } finally {
    document.getElementById('refreshBtn').disabled = false;
  }
}

// Auto-refresh
refreshTimer = setInterval(refreshDashboard, AUTO_REFRESH_SEC * 1000);

// Initial load
refreshDashboard();

// Cleanup timer on page unload
window.addEventListener('beforeunload', function() {
  if (refreshTimer) clearInterval(refreshTimer);
});
</script>
{% endblock %}