互联网搜索独立页面 + 一键保存全部功能

- 新增独立搜索页面 /search
- 支持一键保存所有搜索结果到内容库
- 支持自动抓取和自动保存选项
- 显示保存进度条
- 主页添加搜索入口链接
- 搜索结果状态实时显示(已抓取/已保存)
This commit is contained in:
2026-07-13 16:38:14 +08:00
parent 20f3ec1f18
commit e13e4074f1
6 changed files with 879 additions and 13 deletions
+5
View File
@@ -38,6 +38,11 @@ app.register_blueprint(system_bp)
def index():
return render_template('index.html')
# 搜索页面
@app.route('/search')
def search_page():
return render_template('search.html')
# API首页
@app.route('/api')
def api_index():
+334
View File
@@ -0,0 +1,334 @@
/* 搜索页面专用样式 */
.search-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部 */
.search-header {
background: white;
padding: 20px 30px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.back-link {
color: #667eea;
text-decoration: none;
display: flex;
align-items: center;
gap: 5px;
font-size: 14px;
}
.back-link:hover {
text-decoration: underline;
}
.search-header h1 {
color: #333;
font-size: 24px;
display: flex;
align-items: center;
gap: 10px;
}
/* 搜索框 */
.search-box {
background: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
margin-bottom: 20px;
}
.search-input-group {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.search-input-large {
flex: 1;
padding: 15px 20px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 18px;
}
.search-input-large:focus {
outline: none;
border-color: #667eea;
}
.count-input {
width: 80px;
padding: 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
text-align: center;
}
.btn-large {
padding: 15px 30px;
font-size: 16px;
}
.search-options {
display: flex;
gap: 20px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
color: #666;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* 搜索进度 */
.search-progress {
background: white;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.progress-bar {
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.3s;
}
.progress-text {
text-align: center;
color: #666;
font-size: 14px;
}
/* 结果区域 */
.results-section {
background: white;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.results-header {
padding: 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.results-header h2 {
color: #667eea;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.results-actions {
display: flex;
gap: 15px;
align-items: center;
}
.results-count {
color: #666;
font-size: 14px;
}
.results-body {
padding: 20px;
}
/* 搜索结果列表 */
.search-results {
display: flex;
flex-direction: column;
gap: 15px;
}
.result-item {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 20px;
border: 1px solid #e9ecef;
border-radius: 8px;
transition: all 0.2s;
}
.result-item:hover {
border-color: #667eea;
background: #f8f9fa;
}
.result-item.saved {
border-color: #10b981;
background: #f0fdf4;
}
.result-item.fetched {
border-color: #3b82f6;
}
.result-number {
width: 40px;
height: 40px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 16px;
}
.result-item.saved .result-number {
background: #10b981;
}
.result-main {
flex: 1;
}
.result-title {
font-size: 16px;
color: #333;
cursor: pointer;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.result-title:hover {
color: #667eea;
}
.result-title .saved-icon {
color: #10b981;
}
.result-url {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.result-url a {
color: #3b82f6;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.result-url a:hover {
text-decoration: underline;
}
.result-content-preview {
font-size: 13px;
color: #999;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
.result-actions {
display: flex;
gap: 10px;
}
/* 保存进度 */
.save-progress {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 20px 40px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
min-width: 300px;
}
.progress-info {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
font-size: 14px;
color: #333;
}
.progress-info i {
animation: spin 1s linear infinite;
}
/* 响应式 */
@media (max-width: 768px) {
.search-input-group {
flex-direction: column;
}
.search-options {
flex-direction: column;
gap: 10px;
}
.results-header {
flex-direction: column;
gap: 15px;
}
.results-actions {
flex-wrap: wrap;
}
.result-item {
flex-direction: column;
}
.result-actions {
width: 100%;
justify-content: space-between;
}
}
+33
View File
@@ -735,4 +735,37 @@ body {
#internet-search-status i {
font-size: 16px;
}
/* 搜索入口链接 */
.search-entrance-link {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 15px;
padding: 40px 80px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 12px;
transition: all 0.3s;
}
.search-entrance-link:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.search-entrance-link i {
font-size: 48px;
}
.search-entrance-link span {
font-size: 18px;
font-weight: 500;
}
.search-entrance-link small {
font-size: 13px;
opacity: 0.8;
}
+384
View File
@@ -0,0 +1,384 @@
// API基础地址
const API_BASE = '';
// 搜索结果存储
let searchResults = [];
let currentResultIndex = -1;
// 页面加载初始化
document.addEventListener('DOMContentLoaded', () => {
// 回车搜索
document.getElementById('search-keyword').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
doSearch();
}
});
});
// 执行搜索
async function doSearch() {
const keyword = document.getElementById('search-keyword').value.trim();
const maxResults = parseInt(document.getElementById('search-count').value) || 10;
const autoFetch = document.getElementById('auto-fetch').checked;
const autoSave = document.getElementById('auto-save').checked;
if (!keyword) {
showToast('请输入搜索关键词', 'error');
return;
}
// 更新状态
updateStatus('searching', '搜索中...');
document.getElementById('search-btn').disabled = true;
// 显示进度
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
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 })
});
const data = await response.json();
if (data.success) {
searchResults = data.results.map(r => ({
...r,
fetched: false,
saved: false,
content: ''
}));
displayResults();
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 = '搜索完成!';
// 自动抓取和保存
if (autoFetch && searchResults.length > 0) {
setTimeout(() => fetchAllResults(autoSave), 500);
}
} else {
updateStatus('error', '搜索失败');
showToast('搜索失败: ' + data.error, 'error');
}
} catch (error) {
updateStatus('error', '搜索出错');
showToast('搜索出错', 'error');
console.error(error);
}
setTimeout(() => {
progress.style.display = 'none';
}, 1000);
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>
<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('');
}
// 抓取单个结果
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> 抓取中...';
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]
})
});
const data = await response.json();
if (data.success) {
searchResults[index].fetched = true;
searchResults[index].content = data.data.content;
searchResults[index].title = 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 {
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
showToast('抓取失败: ' + data.error, 'error');
}
} catch (error) {
btn.disabled = false;
btn.innerHTML = '<i class="ri-download-line"></i> 抓取';
showToast('抓取出错', 'error');
}
}
// 保存单个结果
async function saveResult(index) {
const result = searchResults[index];
if (!result) return;
// 如果未抓取,先抓取
if (!result.fetched) {
await fetchResult(index);
if (!searchResults[index].fetched) 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) {
const progress = document.getElementById('search-progress');
progress.style.display = 'block';
const total = searchResults.length;
let fetched = 0;
for (let i = 0; i < searchResults.length; i++) {
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 = '抓取完成!';
setTimeout(() => {
progress.style.display = 'none';
}, 1000);
// 自动保存
if (autoSave) {
setTimeout(() => saveAllResults(), 500);
}
}
// 显示结果详情
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;
}
+7 -13
View File
@@ -116,23 +116,17 @@
</div>
</div>
<!-- 互联网搜索区域 -->
<!-- 互联网搜索入口 -->
<div class="panel full-width">
<div class="panel-header">
<h2><i class="ri-search-line"></i> 互联网搜索</h2>
<div class="panel-actions">
<input type="text" id="internet-search-keyword" placeholder="输入关键词搜索..." class="search-input" style="width: 300px;">
<input type="number" id="internet-search-count" value="10" min="1" max="20" style="width: 80px;" title="结果数量">
<button onclick="doInternetSearch()" class="btn btn-primary" id="search-btn">
<i class="ri-search-line"></i> 搜索
</button>
</div>
</div>
<div class="panel-body">
<div id="internet-search-status" style="margin-bottom: 10px; color: #666; font-size: 14px;"></div>
<div class="search-results-grid" id="internet-search-results">
<div class="empty-text">输入关键词进行互联网搜索</div>
</div>
<div class="panel-body" style="text-align: center; padding: 40px;">
<a href="/search" class="search-entrance-link">
<i class="ri-search-line"></i>
<span>点击进入互联网搜索页面</span>
<small>支持一键保存所有搜索结果到内容库</small>
</a>
</div>
</div>
+116
View File
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>互联网搜索 - 参数数据自动化管理系统</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/search.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css">
</head>
<body>
<div class="search-container">
<!-- 头部 -->
<header class="search-header">
<div class="header-left">
<a href="/" class="back-link">
<i class="ri-arrow-left-line"></i> 返回主页
</a>
<h1><i class="ri-search-line"></i> 互联网搜索</h1>
</div>
<div class="header-right">
<div class="status-badge">
<span id="search-status" class="status-dot"></span>
<span id="status-text">就绪</span>
</div>
</div>
</header>
<!-- 搜索区域 -->
<div class="search-box">
<div class="search-input-group">
<input type="text" id="search-keyword" placeholder="输入关键词搜索..." class="search-input-large">
<input type="number" id="search-count" value="10" min="1" max="20" class="count-input" title="结果数量">
<button onclick="doSearch()" class="btn btn-primary btn-large" id="search-btn">
<i class="ri-search-line"></i> 搜索
</button>
</div>
<div class="search-options">
<label class="checkbox-label">
<input type="checkbox" id="auto-fetch" checked>
<span>搜索后自动抓取内容</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="auto-save">
<span>抓取后自动保存到内容库</span>
</label>
</div>
</div>
<!-- 搜索状态 -->
<div id="search-progress" class="search-progress" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">正在搜索...</div>
</div>
<!-- 搜索结果 -->
<div class="results-section">
<div class="results-header">
<h2><i class="ri-list-check"></i> 搜索结果</h2>
<div class="results-actions">
<span id="results-count" class="results-count">0 条结果</span>
<button onclick="saveAllResults()" class="btn btn-success" id="save-all-btn" disabled>
<i class="ri-save-line"></i> 一键保存全部
</button>
<button onclick="clearResults()" class="btn btn-secondary" id="clear-btn">
<i class="ri-delete-bin-line"></i> 清空
</button>
</div>
</div>
<div class="results-body">
<div id="search-results" class="search-results">
<div class="empty-text">输入关键词开始搜索</div>
</div>
</div>
</div>
<!-- 保存进度 -->
<div id="save-progress" class="save-progress" style="display: none;">
<div class="progress-info">
<i class="ri-loader-4-line"></i>
<span id="save-progress-text">正在保存...</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="save-fill"></div>
</div>
</div>
</div>
<!-- 结果详情模态框 -->
<div id="result-detail-modal" class="modal">
<div class="modal-content large">
<div class="modal-header">
<h3><i class="ri-file-text-line"></i> 内容详情</h3>
<button onclick="closeModal('result-detail-modal')" class="close-btn">
<i class="ri-close-line"></i>
</button>
</div>
<div class="modal-body" id="result-detail-body">
</div>
<div class="modal-footer">
<button onclick="closeModal('result-detail-modal')" class="btn btn-secondary">关闭</button>
<button onclick="saveCurrentResult()" class="btn btn-success">
<i class="ri-save-line"></i> 保存到内容库
</button>
</div>
</div>
</div>
<!-- 提示消息 -->
<div id="toast" class="toast"></div>
<script src="/static/js/search.js"></script>
</body>
</html>