mirror of
https://github.com/teest114514/chatlog_alpha.git
synced 2026-04-11 12:39:35 +08:00
784 lines
30 KiB
HTML
784 lines
30 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>Chatlog 管理控制台</title>
|
||
<style>
|
||
:root {
|
||
--primary: #2563eb;
|
||
--primary-hover: #1d4ed8;
|
||
--bg-body: #f3f4f6;
|
||
--bg-card: #ffffff;
|
||
--text-main: #1f2937;
|
||
--text-secondary: #6b7280;
|
||
--border: #e5e7eb;
|
||
--danger: #ef4444;
|
||
--success: #10b981;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
background-color: var(--bg-body);
|
||
color: var(--text-main);
|
||
margin: 0;
|
||
padding: 20px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* Header */
|
||
header {
|
||
background: var(--bg-card);
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
h1 {
|
||
margin: 0;
|
||
font-size: 1.5rem;
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
/* Tabs */
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
overflow-x: auto;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
background: var(--bg-card);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
transition: all 0.2s;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
color: var(--primary);
|
||
background: #eff6ff;
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
/* Content Area */
|
||
.tab-content {
|
||
display: none;
|
||
background: var(--bg-card);
|
||
padding: 25px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(5px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Forms */
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
label {
|
||
font-weight: 500;
|
||
font-size: 0.9rem;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
input, select {
|
||
padding: 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
font-size: 0.95rem;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
input:focus, select:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
border-top: 1px solid var(--border);
|
||
padding-top: 20px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 24px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--danger);
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f3f4f6;
|
||
color: var(--text-main);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e5e7eb;
|
||
}
|
||
|
||
/* Results */
|
||
.result-area {
|
||
margin-top: 20px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
padding: 15px;
|
||
position: relative;
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px dashed var(--border);
|
||
}
|
||
|
||
.url-display {
|
||
font-family: monospace;
|
||
background: #e2e8f0;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 0.85rem;
|
||
color: var(--text-secondary);
|
||
word-break: break-all;
|
||
}
|
||
|
||
pre {
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
font-family: 'Consolas', monospace;
|
||
font-size: 0.9rem;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* Database List Styles */
|
||
.db-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.db-group-title {
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
color: var(--text-main);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.db-list {
|
||
display: grid;
|
||
gap: 8px;
|
||
}
|
||
|
||
.db-item {
|
||
background: white;
|
||
border: 1px solid var(--border);
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-family: monospace;
|
||
font-size: 0.9rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.db-item:hover {
|
||
border-color: var(--primary);
|
||
background: #eff6ff;
|
||
}
|
||
|
||
.badge {
|
||
background: #e0f2fe;
|
||
color: #0369a1;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; justify-content: center; align-items: center; }
|
||
.modal.hidden { display: none; }
|
||
.modal-content { background: white; width: 90%; max-width: 1200px; height: 90%; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.2); }
|
||
.modal-header { padding: 15px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: #f9fafb; border-radius: 8px 8px 0 0; }
|
||
.modal-header h3 { margin: 0; color: var(--text-main); font-size: 1.2rem; }
|
||
.modal-body { flex: 1; overflow: hidden; padding: 20px; display: flex; flex-direction: column; }
|
||
.close-btn { background: none; border: none; font-size: 2rem; line-height: 1; cursor: pointer; color: var(--text-secondary); }
|
||
.close-btn:hover { color: var(--danger); }
|
||
|
||
.db-table-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; overflow-y: auto; padding: 5px; }
|
||
.db-table-item { padding: 15px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; text-align: center; background: white; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
||
.db-table-item:hover { background: #eff6ff; border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
||
|
||
.data-view-container { display: flex; flex-direction: column; height: 100%; }
|
||
.data-controls { display: flex; gap: 15px; align-items: center; margin-bottom: 15px; flex-shrink: 0; }
|
||
.table-scroll { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
|
||
|
||
#data-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; }
|
||
#data-table th, #data-table td { padding: 10px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
|
||
#data-table th { background: #f3f4f6; position: sticky; top: 0; z-index: 10; font-weight: 600; text-align: left; }
|
||
#data-table tr:hover { background-color: #f9fafb; }
|
||
|
||
.pagination { display: flex; justify-content: center; gap: 15px; margin-top: 15px; align-items: center; flex-shrink: 0; }
|
||
|
||
/* Helper Classes */
|
||
.hidden { display: none; }
|
||
.text-sm { font-size: 0.875rem; }
|
||
.text-gray { color: var(--text-secondary); }
|
||
.text-danger { color: var(--danger); }
|
||
.text-success { color: var(--success); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||
</svg>
|
||
Chatlog 控制台
|
||
</h1>
|
||
<div>
|
||
<button id="clearCacheBtn" class="btn btn-danger text-sm">清除媒体缓存</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="switchTab('dashboard')">仪表盘 & 数据库</button>
|
||
<button class="tab-btn" onclick="switchTab('session')">最近会话</button>
|
||
<button class="tab-btn" onclick="switchTab('chatroom')">群聊管理</button>
|
||
<button class="tab-btn" onclick="switchTab('contact')">联系人</button>
|
||
<button class="tab-btn" onclick="switchTab('chatlog')">消息检索</button>
|
||
</div>
|
||
|
||
<!-- Dashboard / DB List -->
|
||
<div id="dashboard" class="tab-content active">
|
||
<h2>当前已解密数据库</h2>
|
||
<p class="text-gray" style="margin-bottom: 20px;">点击下方的数据库文件可直接浏览其表结构和数据内容。</p>
|
||
<div id="db-list-container">
|
||
<div class="loading">加载中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Session Tab -->
|
||
<div id="session" class="tab-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label>关键词搜索 (可选)</label>
|
||
<input type="text" id="session-keyword" placeholder="昵称 / 备注 / wxid">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>输出格式</label>
|
||
<select id="session-format">
|
||
<option value="json">JSON (预览)</option>
|
||
<option value="csv">CSV 导出</option>
|
||
<option value="xlsx">Excel 导出</option>
|
||
<option value="text">纯文本</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" onclick="queryAPI('session')">查询 / 导出</button>
|
||
</div>
|
||
<div id="session-result" class="result-area hidden"></div>
|
||
</div>
|
||
|
||
<!-- Chatroom Tab -->
|
||
<div id="chatroom" class="tab-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label>搜索群聊 (可选)</label>
|
||
<input type="text" id="chatroom-keyword" placeholder="群名称 / 群ID">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>输出格式</label>
|
||
<select id="chatroom-format">
|
||
<option value="json">JSON (预览)</option>
|
||
<option value="csv">CSV 导出</option>
|
||
<option value="xlsx">Excel 导出</option>
|
||
<option value="text">纯文本</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" onclick="queryAPI('chatroom')">查询 / 导出</button>
|
||
</div>
|
||
<div id="chatroom-result" class="result-area hidden"></div>
|
||
</div>
|
||
|
||
<!-- Contact Tab -->
|
||
<div id="contact" class="tab-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label>搜索联系人 (可选)</label>
|
||
<input type="text" id="contact-keyword" placeholder="昵称 / 备注 / wxid">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>输出格式</label>
|
||
<select id="contact-format">
|
||
<option value="json">JSON (预览)</option>
|
||
<option value="csv">CSV 导出</option>
|
||
<option value="xlsx">Excel 导出</option>
|
||
<option value="text">纯文本</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" onclick="queryAPI('contact')">查询 / 导出</button>
|
||
</div>
|
||
<div id="contact-result" class="result-area hidden"></div>
|
||
</div>
|
||
|
||
<!-- Chatlog Tab -->
|
||
<div id="chatlog" class="tab-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label>时间范围 <span style="color:red">*</span></label>
|
||
<input type="text" id="chatlog-time" placeholder="例如: 2023-01-01~2023-12-31">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>聊天对象 (Talker) <span style="color:red">*</span></label>
|
||
<input type="text" id="chatlog-talker" placeholder="wxid / 群ID / 备注">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>发送者 (Sender) (可选)</label>
|
||
<input type="text" id="chatlog-sender" placeholder="仅群聊有效,指定发送人">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>内容关键词 (可选)</label>
|
||
<input type="text" id="chatlog-keyword" placeholder="正则匹配消息内容">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>限制数量 (Limit)</label>
|
||
<input type="number" id="chatlog-limit" value="100">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>输出格式</label>
|
||
<select id="chatlog-format">
|
||
<option value="json">JSON (预览)</option>
|
||
<option value="csv">CSV 导出</option>
|
||
<option value="xlsx">Excel 导出</option>
|
||
<option value="text">纯文本</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="actions">
|
||
<button class="btn btn-primary" onclick="queryAPI('chatlog')">查询 / 导出</button>
|
||
</div>
|
||
<div id="chatlog-result" class="result-area hidden"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- DB Viewer Modal -->
|
||
<div id="db-viewer-modal" class="modal hidden">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3 id="modal-title">数据库查看器</h3>
|
||
<button class="close-btn" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<!-- Table List View -->
|
||
<div id="table-list-view">
|
||
<p class="text-gray" style="margin-bottom: 15px;">请选择要查看的表:</p>
|
||
<div id="table-list" class="db-table-list"></div>
|
||
</div>
|
||
|
||
<!-- Table Data View -->
|
||
<div id="table-data-view" class="data-view-container hidden">
|
||
<div class="data-controls">
|
||
<button class="btn btn-secondary text-sm" onclick="backToTableList()">← 返回表列表</button>
|
||
<strong id="current-table-name" style="font-size: 1.1rem;"></strong>
|
||
<span id="loading-indicator" class="text-gray hidden">加载中...</span>
|
||
</div>
|
||
<div class="table-scroll">
|
||
<table id="data-table">
|
||
<thead></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="pagination">
|
||
<button class="btn btn-secondary text-sm" onclick="prevPage()">上一页</button>
|
||
<span id="page-info" class="text-sm font-bold">Page 1</span>
|
||
<button class="btn btn-secondary text-sm" onclick="nextPage()">下一页</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Init
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadDBList();
|
||
});
|
||
|
||
// Tab Switching
|
||
function switchTab(tabId) {
|
||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||
|
||
event.target.classList.add('active');
|
||
document.getElementById(tabId).classList.add('active');
|
||
|
||
if(tabId === 'dashboard') {
|
||
loadDBList();
|
||
}
|
||
}
|
||
|
||
// --- Dashboard / DB Viewer Logic ---
|
||
|
||
let currentDB = { group: null, file: null };
|
||
let currentTable = null;
|
||
let currentPage = 0;
|
||
const pageSize = 50;
|
||
|
||
// Load DB List
|
||
async function loadDBList() {
|
||
const container = document.getElementById('db-list-container');
|
||
try {
|
||
const res = await fetch('/api/v1/db');
|
||
if(!res.ok) throw new Error('Failed to load DB list');
|
||
const data = await res.json();
|
||
|
||
let html = '';
|
||
for (const [group, files] of Object.entries(data)) {
|
||
html += `
|
||
<div class="db-group">
|
||
<div class="db-group-title">
|
||
<span class="badge">${group}</span>
|
||
<span class="text-sm text-gray">(${files ? files.length : 0} files)</span>
|
||
</div>
|
||
<div class="db-list">
|
||
`;
|
||
|
||
if (files && files.length > 0) {
|
||
files.forEach(file => {
|
||
// Escape backslashes for JS string
|
||
const escapedFile = file.replace(/\\/g, '\\\\');
|
||
html += `<div class="db-item" onclick="openDBViewer('${group}', '${escapedFile}')">
|
||
<span style="font-weight:bold;">📄 ${file.split(/[\\/]/).pop()}</span>
|
||
<span class="text-gray text-sm" style="margin-left:auto;">${file}</span>
|
||
</div>`;
|
||
});
|
||
} else {
|
||
html += `<div class="db-item text-gray" style="cursor:default;">暂无文件</div>`;
|
||
}
|
||
|
||
html += `</div></div>`;
|
||
}
|
||
container.innerHTML = html;
|
||
} catch (e) {
|
||
container.innerHTML = `<div class="text-danger">加载失败: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function openDBViewer(group, file) {
|
||
currentDB = { group, file };
|
||
document.getElementById('modal-title').textContent = `正在查看: ${file.split(/[\/]/).pop()}`;
|
||
document.getElementById('db-viewer-modal').classList.remove('hidden');
|
||
|
||
// Load tables
|
||
const listContainer = document.getElementById('table-list');
|
||
listContainer.innerHTML = '<div class="loading">加载表列表中...</div>';
|
||
|
||
backToTableList(); // Ensure we are in list view
|
||
|
||
try {
|
||
const res = await fetch(`/api/v1/db/tables?group=${group}&file=${encodeURIComponent(file)}`);
|
||
if(!res.ok) throw new Error('Failed to load tables');
|
||
const tables = await res.json();
|
||
|
||
let html = '';
|
||
if(tables && tables.length > 0) {
|
||
tables.forEach(t => {
|
||
html += `<div class="db-table-item" onclick="loadTableData('${t}')">${t}</div>`;
|
||
});
|
||
} else {
|
||
html = '<div class="text-gray">此数据库中没有表。</div>';
|
||
}
|
||
listContainer.innerHTML = html;
|
||
} catch(e) {
|
||
listContainer.innerHTML = `<div class="text-danger">加载表失败: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('db-viewer-modal').classList.add('hidden');
|
||
currentDB = { group: null, file: null };
|
||
currentTable = null;
|
||
}
|
||
|
||
function backToTableList() {
|
||
document.getElementById('table-list-view').classList.remove('hidden');
|
||
document.getElementById('table-data-view').classList.add('hidden');
|
||
currentTable = null;
|
||
}
|
||
|
||
async function loadTableData(table) {
|
||
currentTable = table;
|
||
currentPage = 0;
|
||
|
||
document.getElementById('table-list-view').classList.add('hidden');
|
||
document.getElementById('table-data-view').classList.remove('hidden');
|
||
document.getElementById('current-table-name').textContent = table;
|
||
|
||
await fetchTableData();
|
||
}
|
||
|
||
async function fetchTableData() {
|
||
if(!currentTable || !currentDB.file) return;
|
||
|
||
const tbody = document.querySelector('#data-table tbody');
|
||
const thead = document.querySelector('#data-table thead');
|
||
const indicator = document.getElementById('loading-indicator');
|
||
|
||
indicator.classList.remove('hidden');
|
||
tbody.innerHTML = ''; // Clear current data
|
||
// Keep header if we are just changing pages? No, schema might ensure safety but refreshing is better.
|
||
|
||
const offset = currentPage * pageSize;
|
||
|
||
try {
|
||
const url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&limit=${pageSize}&offset=${offset}`;
|
||
const res = await fetch(url);
|
||
if(!res.ok) throw new Error('Failed to load data');
|
||
const data = await res.json();
|
||
|
||
// Render Header
|
||
thead.innerHTML = '';
|
||
if(data && data.length > 0) {
|
||
const headers = Object.keys(data[0]);
|
||
let headerHtml = '<tr>';
|
||
headers.forEach(h => headerHtml += `<th>${h}</th>`);
|
||
headerHtml += '</tr>';
|
||
thead.innerHTML = headerHtml;
|
||
|
||
// Render Body
|
||
let bodyHtml = '';
|
||
data.forEach(row => {
|
||
bodyHtml += '<tr>';
|
||
headers.forEach(h => {
|
||
let val = row[h];
|
||
if(val === null) val = '<span class="text-gray">null</span>';
|
||
// Basic escaping
|
||
if(typeof val === 'string') {
|
||
if(val.length > 100) val = val.substring(0, 100) + '...';
|
||
val = escapeHtml(val);
|
||
}
|
||
bodyHtml += `<td>${val}</td>`;
|
||
});
|
||
bodyHtml += '</tr>';
|
||
});
|
||
tbody.innerHTML = bodyHtml;
|
||
} else {
|
||
if(currentPage === 0) {
|
||
thead.innerHTML = '<tr><td>暂无数据</td></tr>';
|
||
} else {
|
||
// Keep header, show empty row
|
||
const prevHeader = thead.innerHTML;
|
||
if(!prevHeader) thead.innerHTML = '<tr><td>End of data</td></tr>';
|
||
else tbody.innerHTML = '<tr><td colspan="100%" style="text-align:center; padding: 20px;">没有更多数据了</td></tr>';
|
||
}
|
||
}
|
||
|
||
document.getElementById('page-info').textContent = `Page ${currentPage + 1}`;
|
||
|
||
} catch(e) {
|
||
tbody.innerHTML = `<tr><td class="text-danger">Error: ${e.message}</td></tr>`;
|
||
} finally {
|
||
indicator.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
function prevPage() {
|
||
if(currentPage > 0) {
|
||
currentPage--;
|
||
fetchTableData();
|
||
}
|
||
}
|
||
|
||
function nextPage() {
|
||
currentPage++;
|
||
fetchTableData();
|
||
}
|
||
|
||
// --- Generic API Query ---
|
||
async function queryAPI(type) {
|
||
const resultArea = document.getElementById(`${type}-result`);
|
||
resultArea.classList.remove('hidden');
|
||
resultArea.innerHTML = '<div class="loading">正在处理...</div>';
|
||
|
||
try {
|
||
let params = new URLSearchParams();
|
||
let format = 'json';
|
||
|
||
if (type === 'session') {
|
||
const kw = document.getElementById('session-keyword').value;
|
||
format = document.getElementById('session-format').value;
|
||
if(kw) params.append('keyword', kw);
|
||
} else if (type === 'chatroom') {
|
||
const kw = document.getElementById('chatroom-keyword').value;
|
||
format = document.getElementById('chatroom-format').value;
|
||
if(kw) params.append('keyword', kw);
|
||
} else if (type === 'contact') {
|
||
const kw = document.getElementById('contact-keyword').value;
|
||
format = document.getElementById('contact-format').value;
|
||
if(kw) params.append('keyword', kw);
|
||
} else if (type === 'chatlog') {
|
||
const time = document.getElementById('chatlog-time').value;
|
||
const talker = document.getElementById('chatlog-talker').value;
|
||
if(!time || !talker) {
|
||
alert('时间和聊天对象是必填项');
|
||
resultArea.classList.add('hidden');
|
||
return;
|
||
}
|
||
params.append('time', time);
|
||
params.append('talker', talker);
|
||
|
||
const sender = document.getElementById('chatlog-sender').value;
|
||
if(sender) params.append('sender', sender);
|
||
|
||
const kw = document.getElementById('chatlog-keyword').value;
|
||
if(kw) params.append('keyword', kw);
|
||
|
||
const limit = document.getElementById('chatlog-limit').value;
|
||
if(limit) params.append('limit', limit);
|
||
|
||
format = document.getElementById('chatlog-format').value;
|
||
}
|
||
|
||
params.append('format', format);
|
||
const url = `/api/v1/${type}?${params.toString()}`;
|
||
|
||
if (format === 'csv' || format === 'xlsx') {
|
||
// Trigger download
|
||
window.location.href = url;
|
||
resultArea.innerHTML = `<div class="text-success">已触发 ${format.toUpperCase()} 下载。<br>请求URL: <span class="url-display">${url}</span></div>`;
|
||
} else {
|
||
const res = await fetch(url);
|
||
if (!res.ok) throw new Error(res.statusText);
|
||
|
||
let text;
|
||
if(format === 'json') {
|
||
const json = await res.json();
|
||
text = JSON.stringify(json, null, 2);
|
||
} else {
|
||
text = await res.text();
|
||
}
|
||
|
||
resultArea.innerHTML = `
|
||
<div class="result-header">
|
||
<span class="url-display">${url}</span>
|
||
<button class="btn btn-secondary text-sm" onclick="copyResult('${type}')">复制</button>
|
||
</div>
|
||
<pre id="${type}-pre">${escapeHtml(text)}</pre>
|
||
`;
|
||
}
|
||
|
||
} catch (e) {
|
||
resultArea.innerHTML = `<div style="color:var(--danger)">请求错误: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return text;
|
||
return String(text)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
function copyResult(type) {
|
||
const text = document.getElementById(`${type}-pre`).innerText;
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
alert('已复制到剪贴板');
|
||
});
|
||
}
|
||
|
||
// Clear Cache
|
||
document.getElementById('clearCacheBtn').addEventListener('click', async () => {
|
||
if(!confirm('确定要清除所有解密的媒体文件缓存吗?')) return;
|
||
try {
|
||
const res = await fetch('/api/v1/cache/clear', { method: 'POST' });
|
||
const data = await res.json();
|
||
alert(`缓存已清除,共删除 ${data.deletedCount} 个文件`);
|
||
} catch (e) {
|
||
alert('清除失败: ' + e.message);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |