index.html 7.74 KB
{% extends "base.html" %}
{% set request_path = "/" %}

{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.sidebar { height: calc(100vh - 48px); }
@media (min-width: 768px) {
  .sidebar { max-width: none; margin: 0; width: 380px; border-right: 1px solid #e0e0e0; }
}
.sort-row { display:flex; align-items:center; gap:6px; padding:6px 16px; font-size:12px; border-bottom:1px solid #eee; background:#fafafa; }
.sort-row select { padding:2px 6px; font-size:12px; border:1px solid #ddd; border-radius:4px; }
.pagination { display:flex; align-items:center; justify-content:center; gap:4px; padding:8px 16px; border-top:1px solid #eee; background:#fafafa; }
.pagination button { padding:4px 10px; font-size:12px; border:1px solid #ddd; border-radius:4px; background:#fff; cursor:pointer; }
.pagination button:disabled { opacity:.4; cursor:default; }
.pagination span { font-size:12px; color:#666; }
</style>

<div class="sidebar" id="sidebar" style="width:100%;max-width:420px;margin:0 auto;">
  <div class="search-box">
    <input type="text" id="searchInput" placeholder="搜索联系人/群聊..." oninput="onSearchInput()">
  </div>
  <div class="sort-row">
    <span>排序:</span>
    <select id="sortSelect" onchange="onSortChange()">
      <option value="time_desc">最新优先</option>
      <option value="time_asc">最旧优先</option>
      <option value="name">按名称</option>
    </select>
    <span style="flex:1;text-align:right;color:#999;" id="sessionCount">-</span>
  </div>
  <div id="sessionList">
    <div class="loading"><div class="spinner"></div></div>
  </div>
  <div class="pagination" id="pagination"></div>
</div>

<script>
var sessionData = [];
var currentPage = 1;
var totalPages = 1;
var totalCount = 0;
var currentSort = 'time_desc';
var perPage = 10;

async function loadSessions(page, sort) {
  page = page || currentPage;
  sort = sort || currentSort;
  var list = document.getElementById('sessionList');
  list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
  try {
    var url = '/api/sessions?page=' + page + '&per_page=' + perPage + '&sort=' + sort;
    var r = await fetch(url);
    var j = await r.json();
    if (!j.ok) { list.innerHTML = '<div class="empty-state">加载失败: ' + j.error + '</div>'; return; }
    sessionData = j.data;
    currentPage = j.page;
    totalPages = Math.ceil(j.total / perPage);
    totalCount = j.total;
    currentSort = sort;
    renderSessions(sessionData);
    renderPagination();
  } catch(e) {
    list.innerHTML = '<div class="empty-state">网络错误</div>';
  }
}

function renderSessions(data) {
  var list = document.getElementById('sessionList');
  var info = '共 ' + totalCount + ' 个会话,第 ' + currentPage + '/' + totalPages + ' 页';
  document.getElementById('sessionCount').textContent = info;
  if (!data.length) { list.innerHTML = '<div class="empty-state">暂无会话</div>'; return; }
  list.innerHTML = data.map(function(s) {
    var name = s.remark || s.display_name || s.talker;
    var initial = name.charAt(0);
    var cls = s.talker_type === 'group' ? 'group' : s.talker_type === 'official' ? 'official' : '';
    var time = new Date((s.createTime || 0) * 1000);
    var ts = time.toLocaleDateString() + ' ' + time.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
    return '<div class="session-item" style="display:flex;align-items:center;">' +
      '<a href="/chat/' + encodeURIComponent(s.talker) + '" style="text-decoration:none;color:inherit;display:flex;flex:1;min-width:0;">' +
        '<div class="avatar ' + cls + '">' + initial + '</div>' +
        '<div class="info">' +
          '<div class="name">' + esc(name) + '</div>' +
          '<div class="preview">' + esc(s.preview || '') + '</div>' +
          '<div class="time">' + ts + '</div>' +
        '</div>' +
      '</a>' +
      (s.talker_type === 'group' ? '<span class="gen-report-btn" style="background:none;border:1px solid #ddd;border-radius:6px;padding:4px 8px;cursor:pointer;font-size:16px;flex-shrink:0;" title="生成日报">\u{1F4CB}</span>' : '') +
    '</div>';
  }).join('');

  // Bind gen-report buttons
  var btns = list.querySelectorAll('.gen-report-btn');
  var groups = data.filter(function(s) { return s.talker_type === 'group'; });
  btns.forEach(function(btn, i) {
    btn.addEventListener('click', function(e) {
      e.stopPropagation();
      var group = groups[i];
      if (group) {
        var gname = group.remark || group.display_name || group.talker;
        genReport(group.talker, gname);
      }
    });
  });
}

function renderPagination() {
  var pg = document.getElementById('pagination');
  if (totalPages <= 1) { pg.innerHTML = ''; return; }
  var html = '';
  html += '<button onclick="goToPage(1)"' + (currentPage === 1 ? ' disabled' : '') + '>&lt;&lt;</button>';
  html += '<button onclick="goToPage(' + (currentPage - 1) + ')"' + (currentPage === 1 ? ' disabled' : '') + '>&lt;</button>';
  html += '<span>' + currentPage + '/' + totalPages + '</span>';
  html += '<button onclick="goToPage(' + (currentPage + 1) + ')"' + (currentPage === totalPages ? ' disabled' : '') + '>&gt;</button>';
  html += '<button onclick="goToPage(' + totalPages + ')"' + (currentPage === totalPages ? ' disabled' : '') + '>&gt;&gt;</button>';
  pg.innerHTML = html;
}

function goToPage(n) {
  if (n < 1 || n > totalPages || n === currentPage) return;
  loadSessions(n, currentSort);
}

function onSortChange() {
  var sort = document.getElementById('sortSelect').value;
  currentSort = sort;
  currentPage = 1;
  loadSessions(1, sort);
}

async function onSearchInput() {
  var q = document.getElementById('searchInput').value.trim();
  if (!q) {
    // Reload paginated list, reset to page 1
    loadSessions(1, currentSort);
    return;
  }
  var list = document.getElementById('sessionList');
  list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
  document.getElementById('pagination').innerHTML = '';
  document.getElementById('sessionCount').textContent = '搜索中...';
  try {
    var r = await fetch('/api/search?q=' + encodeURIComponent(q));
    var j = await r.json();
    if (!j.ok) { list.innerHTML = '<div class="empty-state">搜索出错</div>'; return; }
    sessionData = j.data;
    document.getElementById('sessionCount').textContent = '找到 ' + j.data.length + ' 个结果';
    renderSessions(j.data);
    document.getElementById('pagination').innerHTML = '';
  } catch(e) { list.innerHTML = '<div class="empty-state">搜索出错</div>'; }
}

async function genReport(chatroomId, chatroomName) {
  var yesterday = new Date(Date.now()-86400000).toISOString().slice(0,10);
  var date = prompt('为「' + chatroomName + '」生成日报\n输入日期 (YYYY-MM-DD):', yesterday);
  if (!date) return;
  try {
    var r = await fetch('/api/reports/generate', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({chatroom_id: chatroomId, date: date})
    });
    var j = await r.json();
    if (!j.ok) { alert('失败: ' + (j.error || '')); return; }
    alert('日报任务已提交,Hermes 子进程正在生成中...\n完成后自动跳转日报列表。');
    await pollForReport(j.data.task_id);
  } catch(e) { alert('错误: ' + e.message); }
}

async function pollForReport(taskId) {
  for (var i = 0; i < 60; i++) {
    await new Promise(function(r) { setTimeout(r, 3000); });
    try {
      var r = await fetch('/api/reports/generate/status');
      var j = await r.json();
      if (j.ok && j.data && j.data.content) {
        window.location.href = '/reports';
        return;
      }
    } catch(e) {}
  }
  alert('生成超时,请稍后刷新日报列表查看。');
}

function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

loadSessions();
</script>
{% endblock %}