Files
search-hub/static/js/admin.js

224 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Search Hub — 管理面板 */
(function () {
'use strict';
let currentEditSource = null;
loadSources();
loadStatus();
loadApiDocs();
loadUsage();
// ===== 搜索源 =====
function loadSources() {
fetch('/api/sources')
.then(function (r) { return r.json(); })
.then(function (data) {
var sources = data.sources || [];
var html = '';
sources.forEach(function (s) {
var badge = s.available
? '<span class="source-badge badge-ok">可用</span>'
: '<span class="source-badge badge-fail">不可用</span>';
var keyInfo = s.needs_api_key
? (s.available ? ' | 密钥已配置' : ' | 需配置密钥')
: ' | 无需密钥';
var editBtn = s.needs_api_key
? '<button class="btn-ghost" onclick="openEditModal(\'' + s.name + '\')">编辑</button>'
: '';
html += '<div class="source-card">';
html += ' <div class="d-flex justify-content-between align-items-center">';
html += ' <div><span class="source-name">' + escapeHtml(s.display_name || s.name) + '</span></div>';
html += ' <div class="d-flex gap-3 align-items-center">' + badge + editBtn + '</div>';
html += ' </div>';
html += ' <div class="small text-muted mt-1">' + s.name + keyInfo + '</div>';
html += '</div>';
});
document.getElementById('sources-list').innerHTML = html;
});
}
// ===== 服务状态 =====
function loadStatus() {
fetch('/api/status')
.then(function (r) { return r.json(); })
.then(function (data) {
document.getElementById('hub-status').textContent = data.status === 'ok' ? '运行中' : '异常';
document.getElementById('hub-version').textContent = data.version || '-';
document.getElementById('hub-history').textContent = data.history_count || 0;
});
}
// ===== API 文档 =====
function loadApiDocs() {
fetch('/api/docs')
.then(function (r) { return r.json(); })
.then(function (data) {
var endpoints = data.endpoints || [];
var html = '';
endpoints.forEach(function (ep) {
var methodClass = ep.method === 'GET' ? 'method-get' : 'method-post';
html += '<div class="api-endpoint">';
html += ' <span class="method ' + methodClass + '">' + ep.method + '</span>';
html += ' <span class="path">' + ep.path + '</span>';
html += ' <div class="desc">' + escapeHtml(ep.desc) + '</div>';
if (ep.example) {
html += ' <button class="show-example-btn" onclick="toggleExample(this)">显示示例</button>';
html += ' <div class="example">' + escapeHtml(ep.example) + '</div>';
}
html += '</div>';
});
document.getElementById('api-docs-list').innerHTML = html;
});
}
// ===== 服务用量(异步加载,不阻塞页面) =====
function loadUsage() {
var el = document.getElementById('usage-list');
fetch('/api/admin/usage')
.then(function (r) { return r.json(); })
.then(function (data) {
var html = '';
// Tavily优先显示 account 级别用量)
var t = data.tavily || {};
if (t.available) {
var usage = t.key && t.key.limit > 0 ? t.key.usage : (t.account ? t.account.search_usage : 0);
var limit = t.key && t.key.limit > 0 ? t.key.limit : (t.account ? t.account.plan_limit : 0);
var remaining = t.key && t.key.limit > 0 ? t.key.remaining : (limit - usage);
var pct = limit > 0 ? Math.round(usage / limit * 100) : 0;
html += '<div class="info-chip">';
html += ' <strong>Tavily</strong>';
html += ' <div style="margin-top:4px">已用: ' + usage + ' / ' + limit;
html += ' <span style="margin-left:8px;color:' + (pct > 80 ? '#ef4444' : '#10b981') + '">剩余: ' + remaining + '</span></div>';
if (t.account) {
html += ' <div class="small text-muted" style="margin-top:2px">账户搜索: ' + t.account.search_usage + ' / ' + t.account.plan_limit;
if (t.account.paygo_usage > 0) html += ' | 按量: ' + t.account.paygo_usage;
html += '</div>';
}
html += '</div>';
} else {
html += '<div class="info-chip"><strong>Tavily</strong><br><span class="text-muted">' + (t.error || '未配置') + '</span></div>';
}
// 百度 VDB
var b = data.baidu_vdb || {};
if (b.available) {
html += '<div class="info-chip" style="margin-top:6px">';
html += ' <strong>百度 VDB</strong>';
html += ' <div style="margin-top:4px">免费额度: ' + b.freeQuota + '</div>';
html += '</div>';
} else {
html += '<div class="info-chip" style="margin-top:6px"><strong>百度 VDB</strong><br><span class="text-muted">' + (b.error || '未配置') + '</span></div>';
}
el.innerHTML = html;
})
.catch(function () {
el.innerHTML = '<div class="info-chip text-muted">用量查询暂不可用</div>';
});
}
window.toggleExample = function (btn) {
var example = btn.nextElementSibling;
if (example) {
example.classList.toggle('visible');
btn.textContent = example.classList.contains('visible') ? '收起' : '显示示例';
}
};
// ===== 编辑弹窗 =====
window.openEditModal = function (name) {
currentEditSource = name;
var modal = document.getElementById('edit-modal');
var title = document.getElementById('edit-modal-title');
var body = document.getElementById('edit-modal-body');
title.textContent = '加载中...';
body.innerHTML = '<div class="form-hint">加载中...</div>';
modal.style.display = 'flex';
fetch('/api/admin/sources/' + name)
.then(function (r) { return r.json(); })
.then(function (data) {
title.textContent = '编辑: ' + (data.display_name || name);
var html = '';
if (data.fields.length === 0) {
html = '<div class="form-hint">该搜索源无需配置</div>';
} else {
data.fields.forEach(function (f) {
html += '<div class="form-group">';
html += ' <label>' + escapeHtml(f.label) + (f.required ? ' <span class="text-danger">*</span>' : '') + '</label>';
if (f.type === 'password') {
html += ' <input type="password" class="form-control" id="cfg-' + f.key + '"';
html += ' placeholder="' + (f.has_value ? '已配置,输入新值覆盖' : '请输入') + '">';
if (f.has_value) {
html += ' <div class="form-hint">当前: ' + escapeHtml(f.value) + '</div>';
}
} else {
html += ' <input type="text" class="form-control" id="cfg-' + f.key + '"';
html += ' value="' + escapeHtml(f.value || '') + '">';
}
html += '</div>';
});
}
body.innerHTML = html;
})
.catch(function (err) {
body.innerHTML = '<div class="text-danger">加载失败: ' + err.message + '</div>';
});
};
window.closeEditModal = function () {
document.getElementById('edit-modal').style.display = 'none';
currentEditSource = null;
};
window.saveConfig = function () {
if (!currentEditSource) return;
var body = document.getElementById('edit-modal-body');
var inputs = body.querySelectorAll('input[id^="cfg-"]');
var data = {};
inputs.forEach(function (input) {
var key = input.id.replace('cfg-', '');
var val = input.value.trim();
if (val) data[key] = val;
});
if (Object.keys(data).length === 0) {
alert('请至少填写一个字段');
return;
}
var saveBtn = document.getElementById('save-btn');
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
fetch('/api/admin/sources/' + currentEditSource, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
.then(function (r) { return r.json(); })
.then(function (result) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
if (result.success) {
closeEditModal();
loadSources();
loadStatus();
} else {
alert('保存失败: ' + (result.error || '未知错误'));
}
})
.catch(function (err) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
alert('请求失败: ' + err.message);
});
};
// ===== 辅助 =====
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
}
})();