whitelist.html 9.6 KB
{% extends "base.html" %}
{% block title %}回复白名单 - WeChat Viewer{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.wl-container{max-width:760px;margin:0 auto;padding:20px;}
.wl-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;}
.wl-header h1{font-size:20px;color:#2c3e50;}
.btn-add{background:#27ae60;color:white;border:none;border-radius:6px;padding:8px 16px;font-size:14px;cursor:pointer;}
.wl-hint{font-size:12px;color:#888;margin:8px 0 16px;line-height:1.5;}
.wl-card{background:white;border-radius:8px;padding:12px 16px;margin-bottom:8px;box-shadow:0 1px 2px rgba(0,0,0,0.05);display:flex;align-items:center;gap:10px;}
.kb{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;flex-shrink:0;}
.kb.contact{background:#d6eaf8;color:#1a5276}.kb.group{background:#fdebd0;color:#b9770e}
.wl-name{font-weight:600;font-size:14px;}
.wl-wxid{font-size:11px;color:#999;word-break:break-all;}
.btn-del{margin-left:auto;background:#e74c3c;color:white;border:none;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;flex-shrink:0;}
.empty-state{text-align:center;padding:50px 20px;color:#999;}
/* modal */
.modal-mask{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:1000;}
.modal-mask.show{display:flex;align-items:center;justify-content:center;}
.modal{background:white;border-radius:12px;width:92%;max-width:560px;max-height:82vh;display:flex;flex-direction:column;overflow:hidden;}
.modal-hd{padding:14px 18px;border-bottom:1px solid #eee;display:flex;align-items:center;gap:10px;}
.modal-hd h3{font-size:16px;flex:1;}
.modal-hd .x{background:none;border:none;font-size:22px;color:#999;cursor:pointer;}
.modal-search{padding:10px 18px;border-bottom:1px solid #f0f0f0;}
.modal-search input{width:100%;box-sizing:border-box;border:1px solid #cfd8e3;border-radius:8px;padding:9px 12px;font-size:14px;}
.modal-body{flex:1;overflow-y:auto;padding:6px 0;}
.pick-row{display:flex;align-items:center;gap:10px;padding:9px 18px;cursor:pointer;}
.pick-row:hover{background:#f5f8fb;}
.pick-row input{width:17px;height:17px;flex-shrink:0;}
.pick-row .nm{font-size:14px;flex:1;}
.pick-row .kb2{font-size:9px;padding:1px 6px;border-radius:8px;flex-shrink:0;}
.kb2.contact{background:#d6eaf8;color:#1a5276}.kb2.group{background:#fdebd0;color:#b9770e}
.modal-ft{padding:12px 18px;border-top:1px solid #eee;display:flex;align-items:center;gap:10px;}
.modal-ft .cnt{font-size:13px;color:#666;flex:1;}
.btn-confirm{background:#27ae60;color:white;border:none;border-radius:8px;padding:9px 20px;font-size:14px;cursor:pointer;}
.btn-confirm:disabled{opacity:.5;cursor:not-allowed;}
.loading{text-align:center;padding:30px;color:#999;}
/* 总开关卡片 */
.as-card{background:white;border-radius:8px;padding:14px 16px;margin-bottom:14px;box-shadow:0 1px 2px rgba(0,0,0,0.05);}
.as-row{display:flex;align-items:center;gap:12px;}
.as-txt{flex:1;}
.as-title{font-weight:600;font-size:15px;color:#2c3e50;}
.as-state{font-size:12px;margin-top:2px;}
.as-state.on{color:#27ae60;font-weight:600;}
.as-state.off{color:#999;}
.switch{position:relative;display:inline-block;width:52px;height:30px;flex-shrink:0;}
.switch input{opacity:0;width:0;height:0;}
.slider{position:absolute;cursor:pointer;inset:0;background:#ccc;border-radius:30px;transition:.2s;}
.slider:before{content:"";position:absolute;height:24px;width:24px;left:3px;bottom:3px;background:white;border-radius:50%;transition:.2s;}
.switch input:checked+.slider{background:#27ae60;}
.switch input:checked+.slider:before{transform:translateX(22px);}
.switch input:disabled+.slider{opacity:.5;cursor:not-allowed;}
.as-warn{font-size:12px;color:#b9770e;background:#fef9e7;border-radius:6px;padding:8px 10px;margin-top:10px;line-height:1.5;}
</style>
<div class="wl-container">
<div class="wl-header"><h1>⭐ 回复白名单</h1><button class="btn-add" onclick="openPicker()">➕ 添加</button></div>

<div class="as-card">
  <div class="as-row">
    <div class="as-txt">
      <div class="as-title">自动发送总开关</div>
      <div class="as-state off" id="asState">加载中…</div>
    </div>
    <label class="switch"><input type="checkbox" id="asToggle" onchange="toggleAuto(this)" disabled><span class="slider"></span></label>
  </div>
  <div class="as-warn">开=白名单里的联系人/群<b>直接自动发</b>(钱/承诺/约时间/隐私等敏感词仍强制转人工,绝不误发)。关=一切都进「审批」等你手动点发。建议审核模式跑几天、草稿质量稳了再开。</div>
</div>

<div class="wl-hint">白名单里的联系人 / 群,消息可被<b>自动回复</b>(仍受敏感词等安全规则拦截,命中转人工)。不在名单里的一律进「自动回复审批」等你手动发。</div>
<div id="list"></div>
</div>

<!-- 选人弹窗 -->
<div class="modal-mask" id="mask">
  <div class="modal">
    <div class="modal-hd"><h3>选择联系人 / 群</h3><button class="x" onclick="closePicker()">×</button></div>
    <div class="modal-search"><input id="q" placeholder="搜索昵称 / 备注 / 群名..." oninput="onSearch()"></div>
    <div class="modal-body" id="picklist"><div class="loading">加载中...</div></div>
    <div class="modal-ft"><span class="cnt" id="cnt">已选 0</span>
      <button class="btn-confirm" id="btnConfirm" onclick="confirmAdd()" disabled>添加所选</button></div>
  </div>
</div>

<script>
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
var selected={};      // wxid -> {wxid,name,kind}
var searchTimer=null;

function load(){
  var el=document.getElementById('list');
  fetch('/api/whitelist').then(function(r){return r.json()}).then(function(j){
    if(!j.ok){el.innerHTML='<div class="empty-state">加载失败</div>';return;}
    if(!j.data.length){el.innerHTML='<div class="empty-state"><p>白名单为空 — 当前所有回复都会进人工审批</p></div>';return;}
    var h='';
    for(var i=0;i<j.data.length;i++){var t=j.data[i];
      h+='<div class="wl-card"><span class="kb '+t.kind+'">'+(t.kind==='group'?'群':'联系人')+'</span>';
      h+='<div><div class="wl-name">'+esc(t.name||'(未命名)')+'</div><div class="wl-wxid">'+esc(t.wxid)+'</div></div>';
      h+='<button class="btn-del" onclick="del('+t.id+')">删除</button></div>';
    }
    el.innerHTML=h;
  });
}
function del(id){fetch('/api/whitelist/'+id+'/delete',{method:'POST'}).then(function(r){return r.json()}).then(function(j){if(j.ok)load();else alert('失败');});}

function renderAuto(on){
  var st=document.getElementById('asState'),tg=document.getElementById('asToggle');
  tg.checked=!!on;tg.disabled=false;
  st.textContent=on?'已开启 · 白名单自动发':'已关闭 · 全部人工审批';
  st.className='as-state '+(on?'on':'off');
}
function loadAuto(){
  fetch('/api/auto_send').then(function(r){return r.json()}).then(function(j){
    if(j.ok)renderAuto(j.enabled);
    else{document.getElementById('asState').textContent='读取失败';}
  }).catch(function(){document.getElementById('asState').textContent='读取失败';});
}
function toggleAuto(cb){
  var want=cb.checked;cb.disabled=true;
  if(want&&!confirm('确定开启自动发送?白名单里的消息将无需你确认直接发出(敏感词仍会拦截转人工)。')){
    cb.checked=false;cb.disabled=false;return;
  }
  fetch('/api/auto_send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:want})})
    .then(function(r){return r.json()}).then(function(j){
      if(j.ok)renderAuto(j.enabled);
      else{alert('失败: '+(j.error||''));cb.checked=!want;cb.disabled=false;}
    }).catch(function(){alert('网络失败');cb.checked=!want;cb.disabled=false;});
}

function openPicker(){
  selected={};updateCnt();
  document.getElementById('q').value='';
  document.getElementById('mask').classList.add('show');
  fetchList('/api/contacts');
}
function closePicker(){document.getElementById('mask').classList.remove('show');}
function onSearch(){
  clearTimeout(searchTimer);
  var q=document.getElementById('q').value.trim();
  searchTimer=setTimeout(function(){
    fetchList(q?('/api/contacts/search?q='+encodeURIComponent(q)):'/api/contacts');
  },250);
}
function fetchList(url){
  var el=document.getElementById('picklist');el.innerHTML='<div class="loading">加载中...</div>';
  fetch(url).then(function(r){return r.json()}).then(function(j){
    if(!j.ok){el.innerHTML='<div class="loading">加载失败: '+esc(j.error)+'</div>';return;}
    if(!j.data.length){el.innerHTML='<div class="loading">无结果</div>';return;}
    var h='';
    for(var i=0;i<j.data.length;i++){var t=j.data[i];
      var ck=selected[t.wxid]?'checked':'';
      h+='<label class="pick-row"><input type="checkbox" '+ck+
         ' onchange="toggle(this,'+esc(JSON.stringify(t)).replace(/"/g,'&quot;')+')">'+
         '<span class="nm">'+esc(t.name)+'</span>'+
         '<span class="kb2 '+t.kind+'">'+(t.kind==='group'?'群':'联系人')+'</span></label>';
    }
    el.innerHTML=h;
  });
}
function toggle(cb,t){if(cb.checked)selected[t.wxid]=t;else delete selected[t.wxid];updateCnt();}
function updateCnt(){
  var n=Object.keys(selected).length;
  document.getElementById('cnt').textContent='已选 '+n;
  document.getElementById('btnConfirm').disabled=(n===0);
}
function confirmAdd(){
  var items=Object.keys(selected).map(function(k){return selected[k];});
  if(!items.length)return;
  fetch('/api/whitelist',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({items:items})})
    .then(function(r){return r.json()}).then(function(j){
      if(j.ok){closePicker();load();}else{alert('失败: '+(j.error||''));}
    });
}
load();
loadAuto();
</script>
{% endblock %}