Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd13dec4f | ||
|
|
c3eef4fa21 | ||
|
|
9d6481bd06 | ||
|
|
e13e4074f1 | ||
|
|
20f3ec1f18 |
@@ -38,6 +38,11 @@ app.register_blueprint(system_bp)
|
|||||||
def index():
|
def index():
|
||||||
return render_template('index.html')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
# 搜索页面
|
||||||
|
@app.route('/search')
|
||||||
|
def search_page():
|
||||||
|
return render_template('search.html')
|
||||||
|
|
||||||
# API首页
|
# API首页
|
||||||
@app.route('/api')
|
@app.route('/api')
|
||||||
def api_index():
|
def api_index():
|
||||||
|
|||||||
+3
-2
@@ -140,17 +140,18 @@ def internet_search():
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
keyword = data.get('keyword', '')
|
keyword = data.get('keyword', '')
|
||||||
max_results = data.get('max_results', 10)
|
max_results = data.get('max_results', 10)
|
||||||
save_to_library = data.get('save_to_library', False)
|
engine = data.get('engine', 'bing_cn') # 默认 Bing 中国
|
||||||
|
|
||||||
if not keyword:
|
if not keyword:
|
||||||
return jsonify({'error': '请提供搜索关键词'}), 400
|
return jsonify({'error': '请提供搜索关键词'}), 400
|
||||||
|
|
||||||
# 执行互联网搜索
|
# 执行互联网搜索
|
||||||
results = search_service.search_internet(keyword, max_results)
|
results = search_service.search_internet(keyword, max_results, engine)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'keyword': keyword,
|
'keyword': keyword,
|
||||||
|
'engine': engine,
|
||||||
'results': results,
|
'results': results,
|
||||||
'count': len(results)
|
'count': len(results)
|
||||||
})
|
})
|
||||||
|
|||||||
+130
-28
@@ -33,18 +33,32 @@ class SearchService:
|
|||||||
)
|
)
|
||||||
return result.stdout, result.stderr, result.returncode
|
return result.stdout, result.stderr, result.returncode
|
||||||
|
|
||||||
def search_internet(self, keyword, max_results=None):
|
def search_internet(self, keyword, max_results=None, engine='bing_cn'):
|
||||||
"""
|
"""
|
||||||
从互联网搜索(使用 agent-browser 浏览器自动化)
|
从互联网搜索(使用 agent-browser 浏览器自动化)
|
||||||
|
|
||||||
|
支持的搜索引擎:
|
||||||
|
- bing_cn: Bing 中国(默认)
|
||||||
|
- bing_global: Bing 国际版
|
||||||
|
- google: Google
|
||||||
|
- baidu: 百度
|
||||||
"""
|
"""
|
||||||
max_results = max_results or self.max_results
|
max_results = max_results or self.max_results
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
try:
|
# 根据搜索引擎选择 URL
|
||||||
# 1. 打开 Bing 搜索
|
encoded_keyword = urllib.parse.quote(keyword)
|
||||||
encoded_keyword = urllib.parse.quote(keyword)
|
search_urls = {
|
||||||
search_url = f"https://www.bing.com/search?q={encoded_keyword}"
|
'bing_cn': f"https://cn.bing.com/search?q={encoded_keyword}",
|
||||||
|
'bing_global': f"https://www.bing.com/search?q={encoded_keyword}",
|
||||||
|
'google': f"https://www.google.com/search?q={encoded_keyword}",
|
||||||
|
'baidu': f"https://www.baidu.com/s?wd={encoded_keyword}"
|
||||||
|
}
|
||||||
|
|
||||||
|
search_url = search_urls.get(engine, search_urls['bing_cn'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 打开搜索引擎
|
||||||
stdout, stderr, code = self._run_browser('open', search_url, '--timeout', '20000')
|
stdout, stderr, code = self._run_browser('open', search_url, '--timeout', '20000')
|
||||||
if code != 0:
|
if code != 0:
|
||||||
print(f"打开搜索页面失败: {stderr}")
|
print(f"打开搜索页面失败: {stderr}")
|
||||||
@@ -66,9 +80,11 @@ class SearchService:
|
|||||||
print(f"解析 JSON 失败: {stdout[:500]}")
|
print(f"解析 JSON 失败: {stdout[:500]}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# 4. 从 accessibility tree 中提取搜索结果
|
# 4. 根据搜索引擎选择解析方法
|
||||||
# Bing 搜索结果在 main[aria-label="搜索结果"] 区域内
|
if engine == 'baidu':
|
||||||
results = self._parse_bing_results(data, max_results)
|
results = self._parse_baidu_results(data, max_results)
|
||||||
|
else:
|
||||||
|
results = self._parse_bing_results(data, max_results)
|
||||||
|
|
||||||
# 5. 关闭浏览器
|
# 5. 关闭浏览器
|
||||||
self._run_browser('close')
|
self._run_browser('close')
|
||||||
@@ -143,6 +159,44 @@ class SearchService:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def _parse_baidu_results(self, snapshot_data, max_results=10):
|
||||||
|
"""从百度搜索结果中解析标题和链接"""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
snapshot = snapshot_data.get('data', {}).get('snapshot', '')
|
||||||
|
if not snapshot:
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 百度搜索结果解析
|
||||||
|
refs = []
|
||||||
|
lines = snapshot.split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# 百度结果通常在 link 标签中
|
||||||
|
if 'link "' in line and '[ref=' in line:
|
||||||
|
match = re.search(r'link "([^"]+)" \[ref=(e\d+)\]', line)
|
||||||
|
if match:
|
||||||
|
title = match.group(1)
|
||||||
|
ref = match.group(2)
|
||||||
|
# 过滤百度内部链接和广告
|
||||||
|
if len(title) > 10 and '百度' not in title[:6]:
|
||||||
|
refs.append((title, ref))
|
||||||
|
|
||||||
|
# 获取每个结果的 URL
|
||||||
|
for title, ref in refs[:max_results]:
|
||||||
|
url = self._get_link_url(ref)
|
||||||
|
if url and 'baidu.com' not in url:
|
||||||
|
results.append({
|
||||||
|
'title': title,
|
||||||
|
'url': url,
|
||||||
|
'snippet': '',
|
||||||
|
'source': 'baidu'
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def _get_link_url(self, ref):
|
def _get_link_url(self, ref):
|
||||||
"""通过 agent-browser 获取链接的 URL"""
|
"""通过 agent-browser 获取链接的 URL"""
|
||||||
try:
|
try:
|
||||||
@@ -155,43 +209,91 @@ class SearchService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def fetch_url_content(self, url):
|
def fetch_url_content(self, url):
|
||||||
"""抓取网页内容"""
|
"""抓取网页内容(使用 agent-browser 浏览器方式,绑过反爬虫)"""
|
||||||
try:
|
try:
|
||||||
headers = {
|
# 使用浏览器方式抓取
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
stdout, stderr, code = self._run_browser('open', url, '--timeout', '20000')
|
||||||
}
|
if code != 0:
|
||||||
response = requests.get(url, headers=headers, timeout=self.timeout)
|
print(f"打开页面失败: {stderr}")
|
||||||
response.raise_for_status()
|
return None
|
||||||
|
|
||||||
soup = BeautifulSoup(response.text, 'lxml')
|
# 等待页面加载
|
||||||
|
self._run_browser('wait', '5000')
|
||||||
|
|
||||||
# 提取标题
|
# 获取页面标题
|
||||||
title = soup.find('title')
|
stdout, stderr, code = self._run_browser('get', 'title', '--timeout', '5000')
|
||||||
title = title.text.strip() if title else ''
|
title = stdout.strip().replace('[agent-browser] ', '').strip() if code == 0 else ''
|
||||||
|
|
||||||
# 提取正文(简单提取,可优化)
|
# 获取页面内容(通过 snapshot 获取 accessibility tree)
|
||||||
# 移除脚本和样式
|
stdout, stderr, code = self._run_browser('snapshot', '--json', '--timeout', '15000')
|
||||||
for script in soup(['script', 'style']):
|
text = ''
|
||||||
script.decompose()
|
if code == 0 and stdout:
|
||||||
|
try:
|
||||||
|
data = json.loads(stdout)
|
||||||
|
snapshot = data.get('data', {}).get('snapshot', '')
|
||||||
|
# 从 snapshot 中提取所有 StaticText
|
||||||
|
text = self._extract_text_from_snapshot(snapshot)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 提取文本
|
# 获取 URL(可能被重定向)
|
||||||
text = soup.get_text(separator='\n', strip=True)
|
stdout, stderr, code = self._run_browser('get', 'url', '--timeout', '5000')
|
||||||
|
actual_url = stdout.strip() if code == 0 else url
|
||||||
|
|
||||||
# 提取元数据
|
# 关闭浏览器
|
||||||
meta_desc = soup.find('meta', attrs={'name': 'description'})
|
self._run_browser('close')
|
||||||
description = meta_desc['content'] if meta_desc else ''
|
|
||||||
|
# 提取描述(从页面内容的前200字符)
|
||||||
|
description = text[:200].strip() if text else ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'content': text,
|
'content': text,
|
||||||
'url': url,
|
'url': actual_url,
|
||||||
'fetch_date': datetime.now().isoformat()
|
'fetch_date': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"抓取URL失败: {url}, 错误: {str(e)}")
|
print(f"抓取URL失败: {url}, 错误: {str(e)}")
|
||||||
|
# 尝试关闭浏览器
|
||||||
|
try:
|
||||||
|
self._run_browser('close')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _extract_text_from_snapshot(self, snapshot):
|
||||||
|
"""从 accessibility tree snapshot 中提取文本内容"""
|
||||||
|
texts = []
|
||||||
|
|
||||||
|
for line in snapshot.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if 'StaticText' in line and 'checkbox' not in line:
|
||||||
|
# 找到 StaticText 后的内容
|
||||||
|
idx = line.find('StaticText')
|
||||||
|
after = line[idx + 10:].strip() # 跳过 'StaticText'
|
||||||
|
|
||||||
|
# 去掉开头的引号
|
||||||
|
if after.startswith('"'):
|
||||||
|
after = after[1:]
|
||||||
|
|
||||||
|
# 如果以 JSON 开头(错误信息),跳过
|
||||||
|
if after.startswith('{'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取文本内容
|
||||||
|
text = after.rstrip('"').strip()
|
||||||
|
if text and len(text) > 1:
|
||||||
|
texts.append(text)
|
||||||
|
|
||||||
|
result = '\n'.join(texts)
|
||||||
|
|
||||||
|
# 检测是否是反爬错误页面
|
||||||
|
if '请求存在异常' in result or '暂时限制本次访问' in result:
|
||||||
|
return '[该网站触发了反爬机制,无法抓取内容]'
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def search_articles(self, keyword, category=None):
|
def search_articles(self, keyword, category=None):
|
||||||
"""从内容库搜索"""
|
"""从内容库搜索"""
|
||||||
return db.search_articles(keyword, category)
|
return db.search_articles(keyword, category)
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
/* 搜索页面专用样式 */
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-select {
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.engine-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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.exists {
|
||||||
|
border-color: #f59e0b;
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -736,3 +736,36 @@ body {
|
|||||||
#internet-search-status i {
|
#internet-search-status i {
|
||||||
font-size: 16px;
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
// 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 engine = document.getElementById('search-engine').value;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 显示进度
|
||||||
|
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, engine })
|
||||||
|
});
|
||||||
|
|
||||||
|
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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查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]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
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.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) {
|
||||||
|
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
@@ -116,23 +116,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 互联网搜索区域 -->
|
<!-- 互联网搜索入口 -->
|
||||||
<div class="panel full-width">
|
<div class="panel full-width">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2><i class="ri-search-line"></i> 互联网搜索</h2>
|
<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>
|
||||||
<div class="panel-body">
|
<div class="panel-body" style="text-align: center; padding: 40px;">
|
||||||
<div id="internet-search-status" style="margin-bottom: 10px; color: #666; font-size: 14px;"></div>
|
<a href="/search" class="search-entrance-link">
|
||||||
<div class="search-results-grid" id="internet-search-results">
|
<i class="ri-search-line"></i>
|
||||||
<div class="empty-text">输入关键词进行互联网搜索</div>
|
<span>点击进入互联网搜索页面</span>
|
||||||
</div>
|
<small>支持一键保存所有搜索结果到内容库</small>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
<!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">
|
||||||
|
<select id="search-engine" class="engine-select" title="选择搜索引擎">
|
||||||
|
<option value="bing_cn" selected>Bing 中国</option>
|
||||||
|
<option value="bing_global">Bing 国际</option>
|
||||||
|
<option value="google">Google</option>
|
||||||
|
<option value="baidu">百度</option>
|
||||||
|
</select>
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user