Files
param-auto-manager/static/js/search.js
T
hz4th_coder 76e24c4fa3 新增搜索标题和网页标题双字段
- 数据库新增 search_title 字段存储搜索结果标题
- product_names 存储抓取到的网页标题
- 搜索页面显示两个标题(搜索标题 + 网页标题)
- 内容库页面也显示两个标题
2026-07-14 00:00:50 +08:00

624 lines
22 KiB
JavaScript
Raw 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.
// API基础地址
const API_BASE = '';
// 搜索结果存储
let searchResults = [];
let currentResultIndex = -1;
// 自动流程控制
let shouldStop = false;
// 页面加载初始化
document.addEventListener('DOMContentLoaded', () => {
// 回车搜索
document.getElementById('search-keyword').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
doSearch();
}
});
// 加载失败URL
loadFailedUrls();
});
// 执行搜索
async function doSearch() {
const keyword = document.getElementById('search-keyword').value.trim();
const maxResults = parseInt(document.getElementById('search-count').value) || 10;
const engine = document.getElementById('search-engine').value;
const useCache = document.getElementById('use-cache').checked;
const autoFetch = document.getElementById('auto-fetch').checked;
const autoSave = document.getElementById('auto-save').checked;
if (!keyword) {
showToast('请输入搜索关键词', 'error');
return;
}
// 更新状态
const engineNames = {
'bing_cn': 'Bing 中国',
'bing_global': 'Bing 国际',
'google': 'Google',
'baidu': '百度'
};
updateStatus('searching', `正在通过 ${engineNames[engine]} 搜索...`);
document.getElementById('search-btn').disabled = true;
// 显示进度和停止按钮
shouldStop = false;
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
document.getElementById('stop-btn').style.display = 'inline-flex';
document.getElementById('progress-fill').style.width = '0%';
document.getElementById('progress-text').textContent = '正在搜索...';
try {
const response = await fetch(`${API_BASE}/api/articles/internet-search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword, max_results: maxResults, engine, use_cache: useCache })
});
const data = await response.json();
if (data.success) {
searchResults = data.results.map(r => ({
...r,
fetched: false,
saved: false,
content: ''
}));
displayResults();
// 检查是否被停止
if (shouldStop) {
document.getElementById('stop-btn').style.display = 'none';
document.getElementById('progress-text').textContent = '已停止';
updateStatus('warning', '搜索已停止');
return;
}
// 显示缓存状态
if (data.cached) {
updateStatus('success', `找到 ${searchResults.length} 条结果(使用缓存)`);
showToast('使用缓存结果', 'success');
} else {
updateStatus('success', `找到 ${searchResults.length} 条结果`);
}
document.getElementById('results-count').textContent = `${searchResults.length} 条结果`;
document.getElementById('save-all-btn').disabled = searchResults.length === 0;
document.getElementById('progress-fill').style.width = '100%';
document.getElementById('progress-text').textContent = '搜索完成!';
document.getElementById('stop-btn').style.display = 'none'; // 隐藏停止按钮
// 自动抓取和保存
if (autoFetch && searchResults.length > 0) {
setTimeout(() => fetchAllResults(autoSave), 500);
} else {
setTimeout(() => {
progress.style.display = 'none';
}, 1000);
}
} else {
document.getElementById('stop-btn').style.display = 'none';
updateStatus('error', '搜索失败');
showToast('搜索失败: ' + data.error, 'error');
}
} catch (error) {
document.getElementById('stop-btn').style.display = 'none';
updateStatus('error', '搜索出错');
showToast('搜索出错', 'error');
console.error(error);
}
document.getElementById('search-btn').disabled = false;
}
// 显示搜索结果
function displayResults() {
const container = document.getElementById('search-results');
if (searchResults.length === 0) {
container.innerHTML = '<div class="empty-text">未找到相关结果</div>';
return;
}
container.innerHTML = searchResults.map((r, i) => `
<div class="result-item ${r.saved ? 'saved' : ''} ${r.fetched ? 'fetched' : ''}" id="result-${i}">
<div class="result-number">${i + 1}</div>
<div class="result-main">
<div class="result-title" onclick="showResultDetail(${i})">
${r.saved ? '<i class="ri-check-line saved-icon"></i>' : ''}
${escapeHtml(r.title)}
</div>
${r.pageTitle && r.pageTitle !== r.title ? `
<div class="result-page-title">
<i class="ri-article-line"></i> 网页标题: ${escapeHtml(r.pageTitle)}
</div>
` : ''}
<div class="result-url">
<a href="${escapeHtml(r.url)}" target="_blank">
<i class="ri-external-link-line"></i>
${escapeHtml(r.url.substring(0, 70))}${r.url.length > 70 ? '...' : ''}
</a>
</div>
${r.content ? `<div class="result-content-preview">${escapeHtml(r.content.substring(0, 100))}...</div>` : ''}
</div>
<div class="result-actions">
<button onclick="fetchResult(${i})" class="btn btn-sm btn-secondary" id="fetch-btn-${i}" ${r.fetched ? 'disabled' : ''}>
<i class="ri-download-line"></i> ${r.fetched ? '已抓取' : '抓取'}
</button>
<button onclick="saveResult(${i})" class="btn btn-sm btn-success" id="save-btn-${i}" ${r.saved ? 'disabled' : ''}>
<i class="ri-save-line"></i> ${r.saved ? '已保存' : '保存'}
</button>
</div>
</div>
`).join('');
}
// 检查URL是否已在内容库
async function checkUrlExists(url) {
try {
const response = await fetch(`${API_BASE}/api/articles/search?q=${encodeURIComponent(url)}`);
const data = await response.json();
if (data.success && data.articles && data.articles.length > 0) {
// 检查是否有完全匹配的 URL
return data.articles.some(a => a.url === url);
}
} catch (error) {
console.error('检查URL失败:', error);
}
return false;
}
// 抓取单个结果
async function fetchResult(index) {
const result = searchResults[index];
if (!result || result.fetched) return;
const btn = document.getElementById(`fetch-btn-${index}`);
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line"></i> 检查中...';
// 先检查内容库是否已存在
const exists = await checkUrlExists(result.url);
if (exists) {
searchResults[index].fetched = true;
searchResults[index].saved = true;
searchResults[index].content = '[内容库中已存在此链接]';
btn.innerHTML = '<i class="ri-check-line"></i> 已存在';
btn.className = 'btn btn-sm btn-warning';
const saveBtn = document.getElementById(`save-btn-${index}`);
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="ri-check-line"></i> 已存在';
const item = document.getElementById(`result-${index}`);
item.classList.add('saved');
showToast('该链接已在内容库中', 'warning');
return;
}
btn.innerHTML = '<i class="ri-loader-4-line"></i> 抓取中...';
try {
const response = await fetch(`${API_BASE}/api/articles/fetch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: result.url,
product_names: [result.title],
search_title: result.title // 搜索结果的标题
})
});
const data = await response.json();
if (data.success) {
searchResults[index].fetched = true;
searchResults[index].content = data.data.content;
searchResults[index].pageTitle = data.data.title || result.title; // 抓取到的标题
btn.innerHTML = '<i class="ri-check-line"></i> 已抓取';
// 更新结果显示
const item = document.getElementById(`result-${index}`);
item.classList.add('fetched');
showToast('抓取成功', 'success');
displayResults();
} else {
// 记录失败URL
await recordFailedUrl(result.url, result.title, data.error);
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
btn.className = 'btn btn-sm btn-danger';
showToast('抓取失败: ' + data.error, 'error');
}
} catch (error) {
// 记录失败URL
await recordFailedUrl(result.url, result.title, '抓取出错');
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
btn.className = 'btn btn-sm btn-danger';
showToast('抓取出错', 'error');
}
}
// 记录失败的URL
async function recordFailedUrl(url, title, errorMessage) {
try {
await fetch(`${API_BASE}/api/articles/failed-urls`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, title, error_message: errorMessage })
});
} catch (error) {
console.error('记录失败URL出错:', error);
}
}
// 保存单个结果
async function saveResult(index) {
const result = searchResults[index];
if (!result) return;
// 如果已经保存(可能是内容库中已存在),直接返回
if (result.saved) {
return;
}
// 如果未抓取,先抓取
if (!result.fetched) {
await fetchResult(index);
// 抓取后如果已标记为 saved(内容库已存在),不继续保存
if (searchResults[index].saved) return;
}
const btn = document.getElementById(`save-btn-${index}`);
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line"></i> 保存中...';
try {
const response = await fetch(`${API_BASE}/api/articles`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
product_names: [searchResults[index].title],
category: '',
keywords: [],
summary: searchResults[index].content.substring(0, 200),
content: searchResults[index].content,
source: searchResults[index].url,
url: searchResults[index].url
})
});
const data = await response.json();
if (data.success) {
searchResults[index].saved = true;
btn.innerHTML = '<i class="ri-check-line"></i> 已保存';
const item = document.getElementById(`result-${index}`);
item.classList.add('saved');
showToast('保存成功', 'success');
displayResults();
} else {
btn.disabled = false;
btn.innerHTML = '<i class="ri-save-line"></i> 保存';
showToast('保存失败: ' + data.error, 'error');
}
} catch (error) {
btn.disabled = false;
btn.innerHTML = '<i class="ri-save-line"></i> 保存';
showToast('保存出错', 'error');
}
}
// 一键保存全部
async function saveAllResults() {
const unsaved = searchResults.filter(r => !r.saved);
if (unsaved.length === 0) {
showToast('没有需要保存的结果', 'error');
return;
}
const progress = document.getElementById('save-progress');
progress.style.display = 'block';
let saved = 0;
const total = unsaved.length;
for (let i = 0; i < searchResults.length; i++) {
if (searchResults[i].saved) continue;
document.getElementById('save-progress-text').textContent =
`正在保存 ${saved + 1}/${total}...`;
document.getElementById('save-fill').style.width =
`${(saved / total) * 100}%`;
await saveResult(i);
saved++;
}
document.getElementById('save-fill').style.width = '100%';
document.getElementById('save-progress-text').textContent = '保存完成!';
setTimeout(() => {
progress.style.display = 'none';
}, 1500);
showToast(`成功保存 ${saved} 条结果`, 'success');
}
// 抓取所有结果
async function fetchAllResults(autoSave) {
shouldStop = false; // 重置停止标志
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
document.getElementById('stop-btn').style.display = 'inline-flex'; // 显示停止按钮
const total = searchResults.length;
let fetched = 0;
for (let i = 0; i < searchResults.length; i++) {
// 检查是否停止
if (shouldStop) {
document.getElementById('progress-text').textContent = `已停止(已抓取 ${fetched}/${total}`;
document.getElementById('stop-btn').style.display = 'none';
setTimeout(() => {
progress.style.display = 'none';
}, 2000);
showToast('自动抓取已停止', 'warning');
return;
}
if (searchResults[i].fetched) {
fetched++;
continue;
}
document.getElementById('progress-text').textContent =
`正在抓取 ${fetched + 1}/${total}...`;
document.getElementById('progress-fill').style.width =
`${(fetched / total) * 100}%`;
await fetchResult(i);
fetched++;
}
document.getElementById('progress-fill').style.width = '100%';
document.getElementById('progress-text').textContent = '抓取完成!';
document.getElementById('stop-btn').style.display = 'none';
setTimeout(() => {
progress.style.display = 'none';
}, 1000);
// 自动保存
if (autoSave) {
setTimeout(() => saveAllResults(), 500);
}
}
// 停止自动处理
function stopAutoProcess() {
shouldStop = true;
}
// 显示结果详情
function showResultDetail(index) {
currentResultIndex = index;
const result = searchResults[index];
const body = document.getElementById('result-detail-body');
body.innerHTML = `
<div class="detail-meta">
<div class="detail-meta-item">
<label>标题</label>
<span>${escapeHtml(result.title)}</span>
</div>
<div class="detail-meta-item">
<label>URL</label>
<a href="${escapeHtml(result.url)}" target="_blank">${escapeHtml(result.url)}</a>
</div>
<div class="detail-meta-item">
<label>状态</label>
<span>${result.fetched ? '已抓取' : '未抓取'} / ${result.saved ? '已保存' : '未保存'}</span>
</div>
</div>
${result.content ? `
<div class="detail-section">
<h4><i class="ri-file-text-line"></i> 页面内容</h4>
<pre style="white-space: pre-wrap; max-height: 400px; overflow-y: auto; background: #f8f9fa; padding: 15px; border-radius: 8px;">${escapeHtml(result.content.substring(0, 3000))}${result.content.length > 3000 ? '\n...(内容过长,已截断)' : ''}</pre>
</div>
` : '<div class="detail-section"><p>尚未抓取内容,点击"保存到内容库"将自动抓取并保存</p></div>'}
`;
document.getElementById('result-detail-modal').classList.add('active');
}
// 保存当前结果
async function saveCurrentResult() {
if (currentResultIndex >= 0) {
await saveResult(currentResultIndex);
closeModal('result-detail-modal');
}
}
// 清空结果
function clearResults() {
searchResults = [];
displayResults();
document.getElementById('results-count').textContent = '0 条结果';
document.getElementById('save-all-btn').disabled = true;
updateStatus('ready', '就绪');
}
// 更新状态
function updateStatus(status, text) {
const dot = document.getElementById('search-status');
const statusText = document.getElementById('status-text');
dot.className = 'status-dot';
if (status === 'searching') {
dot.style.background = '#f59e0b';
} else if (status === 'success') {
dot.style.background = '#10b981';
} else if (status === 'error') {
dot.style.background = '#ef4444';
} else {
dot.style.background = '#10b981';
}
statusText.textContent = text;
}
// 关闭模态框
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
// 显示提示
function showToast(message, type = '') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast active ${type}`;
setTimeout(() => {
toast.classList.remove('active');
}, 3000);
}
// HTML转义
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ========== 失败URL管理 ==========
// 加载失败URL列表
async function loadFailedUrls() {
try {
const response = await fetch(`${API_BASE}/api/articles/failed-urls`);
const data = await response.json();
if (data.success) {
document.getElementById('failed-count').textContent = `${data.count} 条`;
const container = document.getElementById('failed-urls-list');
if (data.urls.length === 0) {
container.innerHTML = '<div class="empty-text">暂无失败记录</div>';
} else {
container.innerHTML = data.urls.map(url => `
<div class="failed-url-item" id="failed-${url.id}">
<div class="failed-url-info">
<div class="failed-url-title">${escapeHtml(url.title || url.url.substring(0, 50))}</div>
<div class="failed-url-detail">
<a href="${escapeHtml(url.url)}" target="_blank">
<i class="ri-external-link-line"></i> ${escapeHtml(url.url.substring(0, 60))}${url.url.length > 60 ? '...' : ''}
</a>
<span class="failed-error">${escapeHtml(url.error_message || '未知错误')}</span>
</div>
<div class="failed-url-meta">
<span>重试: ${url.retry_count || 0} 次</span>
<span>${url.created_at || ''}</span>
</div>
</div>
<div class="failed-url-actions">
<button onclick="retryFailedUrl(${url.id}, '${escapeHtml(url.url)}')" class="btn btn-sm btn-warning">
<i class="ri-restart-line"></i> 重试
</button>
<button onclick="deleteFailedUrl(${url.id})" class="btn btn-sm btn-danger">
<i class="ri-delete-bin-line"></i>
</button>
</div>
</div>
`).join('');
}
}
} catch (error) {
console.error('加载失败URL出错:', error);
}
}
// 重试单个失败URL
async function retryFailedUrl(urlId, url) {
const item = document.getElementById(`failed-${urlId}`);
item.classList.add('loading');
try {
const response = await fetch(`${API_BASE}/api/articles/failed-urls/retry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (data.success) {
showToast('重试成功', 'success');
loadFailedUrls();
} else {
showToast('重试失败: ' + data.error, 'error');
item.classList.remove('loading');
}
} catch (error) {
showToast('重试出错', 'error');
item.classList.remove('loading');
}
}
// 全部重试
async function retryAllFailed() {
showToast('正在重试所有失败URL...', '');
const response = await fetch(`${API_BASE}/api/articles/failed-urls`);
const data = await response.json();
if (data.success && data.urls.length > 0) {
for (const url of data.urls) {
await retryFailedUrl(url.id, url.url);
await new Promise(r => setTimeout(r, 500)); // 避免太快
}
}
}
// 删除失败URL记录
async function deleteFailedUrl(urlId) {
try {
await fetch(`${API_BASE}/api/articles/failed-urls/${urlId}`, {
method: 'DELETE'
});
loadFailedUrls();
} catch (error) {
showToast('删除失败', 'error');
}
}
// 清空所有失败URL
async function clearFailedUrls() {
if (!confirm('确定要清空所有失败记录吗?')) return;
try {
await fetch(`${API_BASE}/api/articles/failed-urls/clear`, {
method: 'POST'
});
showToast('已清空', 'success');
loadFailedUrls();
} catch (error) {
showToast('清空失败', 'error');
}
}