Files
ai-kefu-jc/static/admin.html
T
2026-09-29 14:15:12 +08:00

490 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VOLT - 管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; color: #333; height: 100vh; overflow: hidden; }
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-box { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); width: 360px; }
.login-box h2 { text-align: center; margin-bottom: 24px; color: #1a73e8; }
.login-box input { width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; outline: none; }
.login-box input:focus { border-color: #1a73e8; }
.login-box button { width: 100%; padding: 10px; background: #1a73e8; color: #fff; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; margin-top: 16px; }
.login-box button:hover { background: #1557b0; }
.login-error { color: #e53935; font-size: 13px; text-align: center; margin-top: 12px; display: none; }
.app { display: none; height: 100vh; flex-direction: column; }
.header { background: #fff; padding: 12px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.header h1 { font-size: 16px; color: #1a73e8; }
.header-right { display: flex; align-items: center; gap: 16px; }
.header .logout { color: #999; cursor: pointer; font-size: 13px; }
.header .logout:hover { color: #e53935; }
.header select { padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; outline: none; }
.header .stat { font-size: 13px; color: #666; }
.main { display: flex; flex: 1; overflow: hidden; }
/* 左侧会话列表 */
.left-panel { width: 340px; border-right: 1px solid #e0e0e0; background: #fff; display: flex; flex-direction: column; flex-shrink: 0; }
.conv-list { flex: 1; overflow-y: auto; }
.conv-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.15s; }
.conv-item:hover { background: #f8faff; }
.conv-item.active { background: #e8f0fe; border-left: 3px solid #1a73e8; }
.conv-item .conv-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
.conv-item .conv-time { font-size: 11px; color: #999; }
.conv-item .conv-ip { font-size: 12px; color: #333; font-weight: 500; }
.conv-item .conv-bottom { display: flex; gap: 6px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
.tag { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; }
.tag-interested { background: #e8f5e9; color: #2e7d32; }
.tag-not_interested { background: #fce4ec; color: #c62828; }
.tag-pending { background: #fff3e0; color: #e65100; }
.tag-active { background: #e3f2fd; color: #1565c0; }
.tag-completed { background: #f5f5f5; color: #666; }
.conv-meta { font-size: 11px; color: #aaa; }
.conv-device { font-size: 11px; color: #bbb; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.conv-preview { font-size: 12px; color: #888; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 右侧聊天区 */
.right-panel { flex: 1; display: flex; flex-direction: column; background: #f0f2f5; }
.chat-header { padding: 12px 20px; background: #fff; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; }
.chat-header-left { display: flex; align-items: center; gap: 12px; }
.chat-header-info { font-size: 13px; color: #666; }
.chat-header-info span { margin-right: 12px; }
.contact-info { font-size: 12px; color: #1a73e8; }
.btn-delete { padding: 4px 12px; border: 1px solid #e53935; color: #e53935; background: #fff; border-radius: 4px; cursor: pointer; font-size: 12px; }
.btn-delete:hover { background: #e53935; color: #fff; }
.chat-messages { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 12px; }
.chat-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: #bbb; font-size: 15px; }
.msg-row { display: flex; gap: 8px; max-width: 70%; }
.msg-row.user { align-self: flex-end; flex-direction: row-reverse; }
.msg-row.assistant { align-self: flex-start; }
.msg-avatar { width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; flex-shrink: 0; }
.msg-avatar.user { background: #1a73e8; color: #fff; }
.msg-avatar.assistant { background: #34a853; color: #fff; }
.msg-content { padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; word-break: break-word; }
.msg-row.user .msg-content { background: #1a73e8; color: #fff; border-bottom-right-radius: 4px; }
.msg-row.assistant .msg-content { background: #fff; color: #333; border-bottom-left-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.msg-time { font-size: 10px; color: #bbb; margin-top: 2px; }
.msg-row.user .msg-time { text-align: right; }
/* 客户信息栏 */
.profile-bar { padding: 12px 20px; background: #fff; border-top: 1px solid #e0e0e0; flex-shrink: 0; }
.profile-title { font-size: 12px; color: #999; margin-bottom: 6px; }
.profile-items { display: flex; gap: 8px; flex-wrap: wrap; }
.profile-chip { font-size: 11px; padding: 3px 8px; background: #f5f5f5; border-radius: 4px; color: #555; }
.profile-chip .pk { color: #999; }
.load-more { text-align: center; padding: 12px; }
.load-more button { padding: 6px 16px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 13px; color: #666; }
.load-more button:hover { border-color: #1a73e8; color: #1a73e8; }
.back-btn { display: none; background: none; border: none; font-size: 18px; cursor: pointer; padding: 4px 8px; color: #666; }
/* 推送日志视图(Task9 配套入口) */
.btn-logs { padding: 6px 14px; border: 1px solid #1a73e8; color: #1a73e8; background: #fff; border-radius: 6px; cursor: pointer; font-size: 13px; }
.btn-logs:hover { background: #e8f0fe; }
.btn-logs.active { background: #1a73e8; color: #fff; }
.logs-view { flex: 1; overflow-y: auto; background: #fff; padding: 20px; }
.logs-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.logs-toolbar select { padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; outline: none; }
.logs-count { font-size: 13px; color: #666; }
.logs-pager { display: flex; align-items: center; gap: 8px; margin-left: auto; font-size: 13px; color: #666; }
.logs-pager button { padding: 4px 12px; border: 1px solid #ddd; border-radius: 6px; background: #fff; cursor: pointer; font-size: 13px; color: #666; }
.logs-pager button:disabled { opacity: .4; cursor: default; }
.logs-wrap { overflow-x: auto; }
.logs-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 900px; }
.logs-table th { text-align: left; padding: 10px 12px; background: #f8f9fa; color: #666; font-weight: 500; border-bottom: 1px solid #e0e0e0; white-space: nowrap; }
.logs-table td { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; vertical-align: top; word-break: break-all; }
.tag-success { background: #e8f5e9; color: #2e7d32; }
.tag-failed { background: #fce4ec; color: #c62828; }
.tag-skipped { background: #f5f5f5; color: #666; }
.logs-err { color: #c62828; font-size: 12px; max-width: 280px; }
.logs-key { color: #999; font-size: 12px; }
@media (max-width: 768px) {
.left-panel { width: 100%; }
.right-panel { display: none; }
.right-panel.mobile-show { display: flex; position: fixed; inset: 0; z-index: 100; }
.back-btn { display: inline-block !important; }
}
</style>
</head>
<body>
<div class="login-wrap" id="loginWrap">
<div class="login-box">
<h2>VOLT 管理后台</h2>
<input type="password" id="loginPwd" placeholder="请输入管理密码" autofocus>
<button onclick="doLogin()">登录</button>
<div class="login-error" id="loginErr">密码错误</div>
</div>
</div>
<div class="app" id="app">
<div class="header">
<h1>VOLT - 沟通记录</h1>
<div class="header-right">
<button class="btn-logs" id="btnLogs" onclick="toggleLogs()">推送日志</button>
<select id="filterIntent" onchange="resetAndLoad()">
<option value="">全部意向</option>
<option value="interested">有意向</option>
<option value="not_interested">无意向</option>
<option value="pending">待定</option>
</select>
<span class="stat" id="statText"></span>
<span class="logout" onclick="doLogout()">退出登录</span>
</div>
</div>
<div class="main">
<div class="logs-view" id="logsView" style="display:none;">
<div class="logs-toolbar">
<select id="filterPushStatus" onchange="pushPage=1;loadPushLogs()">
<option value="">全部状态</option>
<option value="success">成功</option>
<option value="failed">失败</option>
<option value="pending">推送中</option>
<option value="skipped">跳过</option>
</select>
<span class="logs-count" id="pushStat"></span>
<div class="logs-pager">
<button id="pushPrev" onclick="pushPage--;loadPushLogs()">上一页</button>
<span id="pushPageInfo"></span>
<button id="pushNext" onclick="pushPage++;loadPushLogs()">下一页</button>
</div>
</div>
<div class="logs-wrap">
<table class="logs-table">
<thead><tr>
<th>时间</th><th>类型</th><th>状态</th><th>对方结论</th><th>尝试</th><th>HTTP</th><th>失败/跳过原因</th><th>会话ID</th>
</tr></thead>
<tbody id="pushRows"></tbody>
</table>
</div>
</div>
<div class="left-panel">
<div class="conv-list" id="convList"></div>
<div class="load-more" id="loadMore"></div>
</div>
<div class="right-panel" id="rightPanel">
<div class="chat-header" id="chatHeader" style="display:none;">
<div class="chat-header-left">
<button class="back-btn" onclick="closeMobile()">&larr;</button>
<span class="chat-header-info" id="chatInfo"></span>
</div>
<div style="display:flex;align-items:center;gap:8px;">
<span class="contact-info" id="contactInfo"></span>
<button class="btn-delete" id="btnDelete" onclick="deleteConv()">删除会话</button>
</div>
</div>
<div class="chat-messages" id="chatMessages">
<div class="chat-empty">点击左侧会话查看完整对话记录</div>
</div>
<div class="profile-bar" id="profileBar" style="display:none;"></div>
</div>
</div>
</div>
<script>
var currentPage = 1;
var allConvs = [];
var selectedId = null;
var totalConvs = 0;
function doLogin() {
var pwd = document.getElementById('loginPwd').value;
document.getElementById('loginErr').style.display = 'none';
fetch('/api/admin/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
body: JSON.stringify({password: pwd})
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
document.getElementById('loginWrap').style.display = 'none';
document.getElementById('app').style.display = 'flex';
loadData();
} else {
document.getElementById('loginErr').textContent = d.error || '密码错误';
document.getElementById('loginErr').style.display = 'block';
}
});
}
document.getElementById('loginPwd').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doLogin();
});
function doLogout() {
fetch('/api/admin/logout', {method: 'POST', credentials: 'same-origin'})
.finally(function() { location.reload(); });
}
function resetAndLoad() {
currentPage = 1;
allConvs = [];
selectedId = null;
loadData();
}
function loadData(append) {
var intent = document.getElementById('filterIntent').value;
var url = '/api/admin/conversations?page=' + currentPage + '&size=20';
if (intent) url += '&intent=' + intent;
fetch(url, {credentials: 'same-origin'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error === 'unauthorized') { doLogout(); return; }
if (!d.items) return;
totalConvs = d.total || 0;
if (append) {
allConvs = allConvs.concat(d.items);
} else {
allConvs = d.items;
}
renderConvList();
document.getElementById('statText').textContent = '共 ' + totalConvs + ' 个会话';
var hasMore = allConvs.length < totalConvs;
document.getElementById('loadMore').innerHTML = hasMore
? '<button onclick="loadMoreConvs()">加载更多</button>'
: '';
});
}
function loadMoreConvs() {
currentPage++;
loadData(true);
}
function renderConvList() {
var html = '';
allConvs.forEach(function(item) {
var time = item.created_at ? new Date(item.created_at).toLocaleString('zh-CN', {month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit'}) : '-';
var tag = '';
if (item.intent_level) {
tag = '<span class="tag tag-' + item.intent_level + '">' + intentLabel(item.intent_level) + '</span>';
} else {
tag = '<span class="tag tag-' + item.status + '">' + statusLabel(item.status) + '</span>';
}
var preview = item.preview ? escHtml(item.preview) : '';
if (preview.length > 40) preview = preview.substring(0, 40) + '...';
html += '<div class="conv-item' + (selectedId === item.id ? ' active' : '') + '" onclick="selectConv(\'' + item.id + '\')">';
html += '<div class="conv-top">';
html += '<span class="conv-ip">' + escHtml(item.country || item.ip || '未知') + '</span>';
html += '<span class="conv-time">' + time + '</span>';
html += '</div>';
html += '<div class="conv-bottom">';
html += tag;
html += '<span class="conv-meta">' + item.message_count + ' 条消息</span>';
html += '</div>';
if (preview) {
html += '<div class="conv-preview">' + preview + '</div>';
}
html += '</div>';
});
document.getElementById('convList').innerHTML = html || '<div style="padding:40px;text-align:center;color:#bbb;">暂无会话记录</div>';
}
function selectConv(convId) {
selectedId = convId;
renderConvList();
document.getElementById('rightPanel').classList.add('mobile-show');
// Find conv info
var conv = allConvs.find(function(c) { return c.id === convId; });
fetch('/api/admin/conversations/' + convId + '/messages', {credentials: 'same-origin'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) return;
// Header info
document.getElementById('chatHeader').style.display = 'flex';
var info = '';
if (conv) {
info += '<span>' + escHtml(conv.country || conv.ip || '未知') + '</span>';
info += '<span>' + sourceLabel(conv.source) + '</span>';
var time = conv.created_at ? new Date(conv.created_at).toLocaleString('zh-CN') : '';
if (time) info += '<span>' + time + '</span>';
}
document.getElementById('chatInfo').innerHTML = info;
// Contact info
var contactHtml = '';
if (conv && conv.contact_info) {
var ci = conv.contact_info;
if (ci.WhatsApp) contactHtml += 'WhatsApp: ' + escHtml(ci.WhatsApp) + ' ';
if (ci['邮箱'] || ci.email) contactHtml += '邮箱: ' + escHtml(ci['邮箱'] || ci.email) + ' ';
if (ci['手机'] || ci.phone) contactHtml += '电话: ' + escHtml(ci['手机'] || ci.phone);
}
document.getElementById('contactInfo').innerHTML = contactHtml;
// Messages - show ALL messages in this conversation
var html = '';
d.messages.forEach(function(m) {
var cls = m.role === 'user' ? 'user' : 'assistant';
var avatar = m.role === 'user' ? '客' : 'V';
var roleLabel = m.role === 'user' ? '客户' : 'VOLT';
html += '<div class="msg-row ' + cls + '">';
html += '<div class="msg-avatar ' + cls + '">' + avatar + '</div>';
html += '<div>';
html += '<div class="msg-content">' + escHtml(m.content) + '</div>';
if (m.time) {
html += '<div class="msg-time">' + new Date(m.time).toLocaleString('zh-CN', {hour:'2-digit', minute:'2-digit', second:'2-digit'}) + '</div>';
}
html += '</div>';
html += '</div>';
});
document.getElementById('chatMessages').innerHTML = html || '<div class="chat-empty">该会话暂无消息</div>';
// Scroll to bottom
var chatEl = document.getElementById('chatMessages');
chatEl.scrollTop = chatEl.scrollHeight;
// Profile bar
if (d.profile && Object.keys(d.profile).length > 0) {
var ph = '<div class="profile-title">客户信息</div><div class="profile-items">';
Object.keys(d.profile).forEach(function(k) {
var v = d.profile[k];
if (typeof v === 'object') v = JSON.stringify(v);
if (v && v !== '{}' && v !== '') {
ph += '<span class="profile-chip"><span class="pk">' + escHtml(k) + ':</span> ' + escHtml(String(v)) + '</span>';
}
});
ph += '</div>';
document.getElementById('profileBar').innerHTML = ph;
document.getElementById('profileBar').style.display = ph ? 'block' : 'none';
} else {
document.getElementById('profileBar').style.display = 'none';
}
});
}
function closeMobile() {
document.getElementById('rightPanel').classList.remove('mobile-show');
}
function deleteConv() {
if (!selectedId) return;
if (!confirm('确定要删除这条会话记录吗?删除后无法恢复。')) return;
fetch('/api/admin/conversations/' + selectedId, {
method: 'DELETE',
credentials: 'same-origin',
})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.ok) {
selectedId = null;
document.getElementById('chatHeader').style.display = 'none';
document.getElementById('profileBar').style.display = 'none';
document.getElementById('chatMessages').innerHTML = '<div class="chat-empty">点击左侧会话查看完整对话记录</div>';
resetAndLoad();
} else {
alert(d.error || '删除失败');
}
});
}
// ---- 推送日志视图(Task9 配套) ----
var pushPage = 1;
var logsShown = false;
function toggleLogs() {
logsShown = !logsShown;
var btn = document.getElementById('btnLogs');
document.getElementById('logsView').style.display = logsShown ? '' : 'none';
document.querySelector('.left-panel').style.display = logsShown ? 'none' : '';
document.getElementById('rightPanel').style.display = logsShown ? 'none' : '';
if (logsShown) {
btn.textContent = '返回会话';
btn.classList.add('active');
pushPage = 1;
loadPushLogs();
} else {
btn.textContent = '推送日志';
btn.classList.remove('active');
}
}
function loadPushLogs() {
var st = document.getElementById('filterPushStatus').value;
var url = '/api/admin/push-logs?page=' + pushPage + '&size=20';
if (st) url += '&status=' + st;
fetch(url, {credentials: 'same-origin'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error === 'unauthorized') { doLogout(); return; }
if (!d.items) return;
document.getElementById('pushStat').textContent = '共 ' + (d.total || 0) + ' 条';
var html = '';
d.items.forEach(function(it) {
// 后端时间是 naive UTC(无时区标记)——补 Z 避免浏览器误当本地时间(差8小时)
var iso = it.created_at || '';
if (iso && iso.indexOf('Z') === -1 && iso.indexOf('+') === -1) iso += 'Z';
var time = iso ? new Date(iso).toLocaleString('zh-CN') : '-';
html += '<tr>';
html += '<td style="white-space:nowrap;">' + time + '</td>';
html += '<td>' + pushTypeLabel(it.push_type) + '</td>';
html += '<td><span class="tag tag-' + it.status + '">' + pushStatusLabel(it.status) + '</span></td>';
html += '<td>' + (it.outcome ? pushOutcomeLabel(it.outcome) : '-') + '</td>';
html += '<td>' + (it.attempts || 0) + '</td>';
html += '<td>' + (it.response_status || '-') + '</td>';
html += '<td class="logs-err">' + escHtml(it.error_message || '') + '</td>';
html += '<td class="logs-key" title="' + escHtml(it.conversation_id || '') + '">' + escHtml((it.conversation_id || '').substring(0, 8)) + '…</td>';
html += '</tr>';
});
document.getElementById('pushRows').innerHTML = html
|| '<tr><td colspan="8" style="text-align:center;color:#bbb;padding:40px;">暂无推送记录</td></tr>';
var totalPages = Math.max(1, Math.ceil((d.total || 0) / (d.size || 20)));
document.getElementById('pushPageInfo').textContent = pushPage + ' / ' + totalPages;
document.getElementById('pushPrev').disabled = pushPage <= 1;
document.getElementById('pushNext').disabled = pushPage >= totalPages;
});
}
function pushTypeLabel(t) { return {lead:'首询盘', returning:'回访', correction:'更正'}[t] || t; }
function pushStatusLabel(s) { return {success:'成功', failed:'失败', pending:'推送中', skipped:'跳过'}[s] || s; }
function pushOutcomeLabel(o) { return {accepted:'已入库', duplicated:'重复跳过'}[o] || o; }
function intentLabel(l) { return {interested:'有意向', not_interested:'无意向', pending:'待定'}[l] || l; }
function statusLabel(s) { return {active:'进行中', completed:'已结束', abandoned:'已离开'}[s] || s; }
function sourceLabel(s) { return {standalone:'独立页面', embedded:'嵌入', widget:'Widget'}[s] || s; }
function escHtml(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
// Auto-check auth
fetch('/api/admin/conversations?page=1&size=1', {credentials: 'same-origin'})
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.error !== 'unauthorized') {
document.getElementById('loginWrap').style.display = 'none';
document.getElementById('app').style.display = 'flex';
loadData();
}
})
.catch(function() {});
</script>
</body>
</html>