approvals.html 11.8 KB
{% extends "base.html" %}
{% block title %}自动回复审批 - WeChat Viewer{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.ap-container{max-width:960px;margin:0 auto;padding:20px;}
.ap-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;}
.ap-header h1{font-size:20px;color:#2c3e50;}
.btn-refresh{background:#95a5a6;color:white;padding:6px 14px;border:none;border-radius:6px;cursor:pointer;font-size:13px;}
.tabs{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;}
.tab{padding:6px 14px;border-radius:6px;cursor:pointer;font-size:13px;border:1px solid #ddd;background:white;}
.tab.active{background:#2c3e50;color:white;border-color:#2c3e50;}
.ap-card{background:white;border-radius:8px;padding:14px 18px;margin-bottom:10px;box-shadow:0 1px 2px rgba(0,0,0,0.05);}
.ap-card.pending{border-left:3px solid #e67e22;}
.ap-card.sent{border-left:3px solid #27ae60;opacity:.75;}
.ap-card.auto_sent{border-left:3px solid #3498db;opacity:.8;}
.ap-card.cancelled,.ap-card.failed{border-left:3px solid #95a5a6;opacity:.65;}
.ap-top{display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap;}
.ap-name{font-weight:600;font-size:14px;}
.kind-badge{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;background:#eef;color:#3a51c6;}
.kind-badge.group_at_me{background:#fdebd0;color:#b9770e}.kind-badge.group_quote_me{background:#e8daef;color:#6c3483}
.status-badge{font-size:10px;padding:2px 8px;border-radius:10px;margin-left:auto;}
.status-badge.sent{background:#d5f5e3;color:#1e8449}.status-badge.auto_sent{background:#d6eaf8;color:#1a5276}
.status-badge.cancelled,.status-badge.failed{background:#f2f3f4;color:#7f8c8d}
.reason{font-size:10px;color:#e67e22;margin-bottom:6px;}
.incoming{background:#f7f7f8;border-radius:6px;padding:8px 10px;font-size:12px;color:#444;margin-bottom:8px;white-space:pre-wrap;word-break:break-word;}
.incoming .lbl{color:#999;font-size:10px;display:block;margin-bottom:2px;}
.draft-box{width:100%;box-sizing:border-box;border:1px solid #cfd8e3;border-radius:6px;padding:8px 10px;font-size:13px;resize:vertical;min-height:44px;font-family:inherit;}
.ap-actions{display:flex;gap:8px;margin-top:8px;}
.ap-actions button{cursor:pointer;padding:7px 16px;border:none;border-radius:6px;font-size:13px;}
.btn-send{background:#27ae60;color:white}.btn-send:hover{background:#219a52}
.btn-cancel{background:#e74c3c;color:white}.btn-cancel:hover{background:#c0392b}
.btn-preview{background:#8e44ad;color:white}.btn-preview:hover{background:#7d3c98}
.ap-check{display:inline-block;margin-right:6px;vertical-align:middle;}
.ap-check input{width:16px;height:16px;cursor:pointer;}
.sent-reply{font-size:13px;color:#1e8449;margin-top:4px;}
.ts{font-size:10px;color:#aaa;}
.empty-state{text-align:center;padding:60px 20px;color:#999;}
.loading{text-align:center;padding:40px;color:#999;}
</style>
<div class="ap-container">
<div class="ap-header">
<h1>✅ 自动回复审批</h1>
<button class="btn-refresh" onclick="load()">🔄 刷新</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;">
  <button class="btn-preview" onclick="toggleBatch()" id="batchToggle" style="padding:10px 14px;border:none;border-radius:8px;font-size:13px;cursor:pointer;">☑ 批量模式</button>
  <button class="btn-send" onclick="batchSend()" id="batchSendBtn" style="display:none;padding:10px 14px;border:none;border-radius:8px;font-size:13px;cursor:pointer;">📤 批量发送</button>
  <button class="btn-cancel" onclick="batchCancel()" id="batchCancelBtn" style="display:none;padding:10px 14px;border:none;border-radius:8px;font-size:13px;cursor:pointer;">🗑 批量取消</button>
  <select id="kindFilter" onchange="render()" style="padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;">
    <option value="">全部类型</option>
    <option value="group_at_me">群@我</option>
    <option value="group_quote_me">群引用我</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="pending">待审批</option>
    <option value="sent">已发送</option>
    <option value="auto_sent">自动已发</option>
    <option value="cancelled">已取消</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="status">按状态</option>
  </select>
</div>
<div class="tabs">
<div class="tab active" data-s="pending" onclick="switchTab('pending',this)">待审批</div>
<div class="tab" data-s="" onclick="switchTab('',this)">全部</div>
<div class="tab" data-s="sent" onclick="switchTab('sent',this)">已发送</div>
<div class="tab" data-s="auto_sent" onclick="switchTab('auto_sent',this)">自动已发</div>
<div class="tab" data-s="cancelled" onclick="switchTab('cancelled',this)">已取消</div>
</div>
<div id="list"></div>
</div>
<script>
var curStatus='pending';
var allReplies = [];
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
var STATUS_CN={pending:'待审批',sent:'已发送',auto_sent:'自动已发',cancelled:'已取消',failed:'发送失败'};
var KIND_CN={private:'私聊',group_at_me:'群@我',group_quote_me:'群引用我'};

function switchTab(s,el){curStatus=s;document.querySelectorAll('.tabs .tab').forEach(function(t){t.classList.remove('active')});el.classList.add('active');load();}

function load(){
  var el=document.getElementById('list');
  el.innerHTML='<div class="loading">加载中...</div>';
  fetch('/api/replies?status='+encodeURIComponent(curStatus)).then(function(r){return r.json()}).then(function(j){
    if(!j.ok){el.innerHTML='<div class="empty-state">加载失败: '+esc(j.error)+'</div>';return;}
    if(!j.data.length){el.innerHTML='<div class="empty-state"><p>暂无</p></div>';return;}
    allReplies = j.data;
    render();
  }).catch(function(e){el.innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>';});
}

function render(){
  var el = document.getElementById('list');
  var q = (document.getElementById('searchInput').value || '').trim().toLowerCase();
  var kf = document.getElementById('kindFilter').value;
  var sf = document.getElementById('statusFilter').value;
  var sb = document.getElementById('sortBy').value;
  var filtered = [];
  for (var i = 0; i < allReplies.length; i++) {
    var t = allReplies[i];
    if (q) {
      var hay = ((t.sender_name || t.sender || t.talker || '') + ' ' + (t.incoming || '') + ' ' + (t.draft || '')).toLowerCase();
      if (hay.indexOf(q) < 0) continue;
    }
    if (kf && t.kind !== kf) continue;
    if (sf && t.status !== sf) continue;
    filtered.push(t);
  }
  if (sb === 'status') filtered.sort(function(a,b){return (a.status||'').localeCompare(b.status||'')});
  else filtered.sort(function(a,b){return (b.created_at||'').localeCompare(a.created_at||'')});

  if (!filtered.length) { el.innerHTML = '<div class="empty-state"><p>无匹配</p></div>'; return; }
  var h='';
  for(var i=0;i<filtered.length;i++){
    var t=filtered[i];
    var pending=t.status==='pending';
    h+='<div class="ap-card '+t.status+'">';
    h+='<div class="ap-top"><span class="ap-name">'+esc(t.sender_name||t.sender||t.talker)+'</span>';
    h+='<span class="kind-badge '+t.kind+'">'+(KIND_CN[t.kind]||t.kind)+'</span>';
    if(!pending)h+='<span class="status-badge '+t.status+'">'+(STATUS_CN[t.status]||t.status)+'</span>';
    h+='</div>';
    if(pending&&t.reason)h+='<div class="reason">判定:'+esc(t.reason)+'</div>';
    h+='<div class="incoming"><span class="lbl">对方说:</span>'+esc(t.incoming)+'</div>';
    if(pending){
      h+='<div class="ap-check" style="display:'+(batchMode?'inline-block':'none')+'"><input type="checkbox" class="sel" value="'+t.id+'"></div>';
      h+='<textarea class="draft-box" id="d'+t.id+'">'+esc(t.draft)+'</textarea>';
      h+='<div class="ap-actions">';
      h+='<button class="btn-preview" onclick="preview('+t.id+')">🔍 预览</button>';
      h+='<button class="btn-send" onclick="send('+t.id+')">✅ 发送</button>';
      h+='<button class="btn-cancel" onclick="cancel('+t.id+')">🗑 取消</button>';
      h+='</div>';
    }else{
      h+='<div class="sent-reply">回复:'+esc(t.draft)+'</div>';
      h+='<div class="ts">'+esc(t.sent_at||t.created_at)+'</div>';
    }
    h+='</div>';
  }
  el.innerHTML=h;
}

function send(id){
  var txt=document.getElementById('d'+id).value;
  if(!txt.trim()){alert('回复内容为空');return;}
  fetch('/api/reply/'+id+'/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:txt})})
    .then(function(r){return r.json()}).then(function(j){
      if(j.ok){load();}else{alert('发送失败: '+(j.error||JSON.stringify(j.data||{})));load();}
    });
}
function cancel(id){
  fetch('/api/reply/'+id+'/cancel',{method:'POST'}).then(function(r){return r.json()}).then(function(j){
    if(j.ok){load();}else{alert('失败: '+(j.error||''));}
  });
}
var batchMode=false;
function toggleBatch(){
  batchMode=!batchMode;
  document.getElementById('batchSendBtn').style.display=batchMode?'inline-block':'none';
  document.getElementById('batchCancelBtn').style.display=batchMode?'inline-block':'none';
  document.getElementById('batchToggle').textContent=batchMode?'✖ 退出批量':'☑ 批量模式';
  render();
}
function selectedIds(){
  var out=[];
  document.querySelectorAll('.sel:checked').forEach(function(c){out.push(parseInt(c.value));});
  return out;
}
function preview(id){
  var txt=document.getElementById('d'+id).value;
  fetch('/api/reply/'+id+'/dry-run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:txt})})
    .then(function(r){return r.json()}).then(function(j){
      if(!j.ok){alert('预览失败: '+(j.error||''));return;}
      var s=j.simulation;
      alert('【模拟发送 · 未真正发送】\n接收: '+(s.to_name||s.to_talker)+' ('+s.to_talker+')\n类型: '+(s.kind||'')+'\n通道: '+s.channel+'\n判定: '+(s.action||'')+(s.reason?' — '+s.reason:'')+'\n\n内容:\n'+s.text);
    });
}
function batchSend(){
  var ids=selectedIds();
  if(!ids.length){alert('未选择任何条目');return;}
  if(!confirm('确认批量发送 '+ids.length+' 条回复?发送后不可撤回。'))return;
  var drafts={};
  ids.forEach(function(id){drafts[id]=document.getElementById('d'+id).value;});
  fetch('/api/replies/batch-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,drafts:drafts})})
    .then(function(r){return r.json()}).then(function(j){
      if(!j.ok){alert('批量发送失败: '+(j.error||''));return;}
      var fails=(j.results||[]).filter(function(r){return !r.ok;});
      if(fails.length){
        alert('成功 '+j.ok_count+' 条,失败 '+fails.length+' 条:\n'+fails.map(function(f){return '#'+f.id+' '+(f.error||JSON.stringify(f.data||{}));}).join('\n'));
      }else{
        alert('批量发送成功 '+j.ok_count+' 条 ✅');
      }
      load();
    });
}
function batchCancel(){
  var ids=selectedIds();
  if(!ids.length){alert('未选择任何条目');return;}
  if(!confirm('确认批量取消 '+ids.length+' 条?'))return;
  fetch('/api/replies/batch-cancel',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids})})
    .then(function(r){return r.json()}).then(function(j){
      if(j.ok){load();}else{alert('批量取消失败: '+(j.error||''));}
    });
}
load();
</script>
{% endblock %}