49c0e5559c
Co-Authored-By: Claude Code <noreply@anthropic.com>
490 lines
24 KiB
HTML
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()">←</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>
|