feat: 添加SEO工具管理页面 - v2.4.1

新增功能:
1. 后台SEO工具管理页面 (/admin/seo-tools)
   - 显示sitemap状态(是否存在、最后更新时间、文件大小)
   - 显示动态sitemap URL并支持一键复制

2. 生成静态sitemap.xml文件 (/api/generate-static-sitemap)
   - 将动态sitemap生成为static/sitemap.xml静态文件
   - 支持手动触发更新
   - 返回URL数量统计信息

3. 通知搜索引擎功能 (/api/notify-search-engines)
   - 支持向Google、Baidu、Bing提交sitemap更新通知
   - 使用各搜索引擎的ping接口
   - 返回每个搜索引擎的提交状态

4. 一键操作
   - 提供"一键生成并通知"功能
   - 自动执行生成sitemap + 通知搜索引擎两个步骤
   - 适合日常SEO维护使用

技术实现:
- 使用Flask路由和@login_required装饰器保护后台接口
- AJAX + fetch API实现前端交互
- Bootstrap 4卡片式UI设计
- 实时显示操作结果,颜色区分成功/失败状态

用户价值:
- 无需手动登录各搜索引擎后台提交sitemap
- 支持批量更新和通知,提升SEO工作效率
- 可视化状态展示,便于监控sitemap更新情况

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jowe
2026-01-03 17:29:14 +08:00
parent 7da0bb6e54
commit c74b115ac0
3 changed files with 596 additions and 0 deletions

View File

@@ -79,6 +79,12 @@
<div class="nav-section">
<div class="nav-section-title">系统</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="{{ url_for('seo_tools') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">search</span>
<span class="nav-text">SEO工具</span>
</a>
</li>
<li class="nav-item">
<a href="{{ url_for('batch_import') }}" class="nav-link">
<span class="material-symbols-outlined nav-icon">upload_file</span>

View File

@@ -0,0 +1,416 @@
{% extends 'admin/master.html' %}
{% block head_css %}
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h1 class="mb-4">
<i class="fa fa-search"></i> SEO工具管理
</h1>
<!-- Sitemap状态卡片 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fa fa-sitemap"></i> Sitemap状态
</h5>
</div>
<div class="card-body">
{% if sitemap_info.exists %}
<div class="alert alert-success">
<i class="fa fa-check-circle"></i> 静态sitemap.xml已生成
</div>
<table class="table table-sm">
<tr>
<th width="40%">最后更新时间:</th>
<td>{{ sitemap_info.last_updated }}</td>
</tr>
<tr>
<th>文件大小:</th>
<td>{{ (sitemap_info.size / 1024) | round(2) }} KB</td>
</tr>
<tr>
<th>文件路径:</th>
<td><code>static/sitemap.xml</code></td>
</tr>
</table>
{% else %}
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i> 静态sitemap.xml尚未生成
</div>
<p class="text-muted mb-0">点击下方按钮生成静态sitemap文件</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fa fa-globe"></i> 动态Sitemap
</h5>
</div>
<div class="card-body">
<p>动态sitemap路由始终可用</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="{{ request.url_root }}sitemap.xml" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('{{ request.url_root }}sitemap.xml')">
<i class="fa fa-copy"></i> 复制
</button>
</div>
<p class="text-muted small mb-0">
<i class="fa fa-info-circle"></i> 动态sitemap实时反映数据库内容无需手动生成
</p>
</div>
</div>
</div>
</div>
<!-- 操作按钮区 -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-header bg-dark text-white">
<h5 class="mb-0">
<i class="fa fa-tools"></i> 快速操作
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<h6><i class="fa fa-file-code"></i> 生成静态Sitemap</h6>
<p class="text-muted small">生成static/sitemap.xml文件可用于静态服务或下载</p>
<button id="generateSitemapBtn" class="btn btn-success btn-lg w-100">
<i class="fa fa-cog"></i> 生成静态sitemap.xml
</button>
</div>
<div class="col-md-6 mb-3">
<h6><i class="fa fa-bell"></i> 通知搜索引擎</h6>
<p class="text-muted small">向Google、Baidu、Bing提交sitemap更新通知</p>
<button id="notifyEnginesBtn" class="btn btn-primary btn-lg w-100">
<i class="fa fa-paper-plane"></i> 通知搜索引擎更新
</button>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<button id="doAllBtn" class="btn btn-warning btn-lg w-100">
<i class="fa fa-rocket"></i> 一键生成并通知(推荐)
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作结果显示区 -->
<div class="row">
<div class="col-md-12">
<div id="resultArea" class="card" style="display: none;">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">
<i class="fa fa-history"></i> 操作结果
</h5>
</div>
<div class="card-body" id="resultContent">
<!-- 动态填充内容 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.card {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.btn-lg {
padding: 12px 24px;
font-size: 16px;
}
#resultArea {
animation: slideDown 0.3s ease-out;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-item {
padding: 12px;
border-left: 4px solid #ddd;
margin-bottom: 10px;
background: #f8f9fa;
}
.result-item.success {
border-left-color: #28a745;
background: #d4edda;
}
.result-item.error {
border-left-color: #dc3545;
background: #f8d7da;
}
.result-item.warning {
border-left-color: #ffc107;
background: #fff3cd;
}
</style>
<script>
// 复制到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
alert('已复制到剪贴板!');
});
}
// 禁用按钮
function disableButtons() {
document.querySelectorAll('#generateSitemapBtn, #notifyEnginesBtn, #doAllBtn').forEach(btn => {
btn.disabled = true;
});
}
// 启用按钮
function enableButtons() {
document.querySelectorAll('#generateSitemapBtn, #notifyEnginesBtn, #doAllBtn').forEach(btn => {
btn.disabled = false;
});
}
// 显示结果
function showResult(html) {
const resultArea = document.getElementById('resultArea');
const resultContent = document.getElementById('resultContent');
resultContent.innerHTML = html;
resultArea.style.display = 'block';
resultArea.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// 生成静态sitemap
document.getElementById('generateSitemapBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 生成中...';
try {
const response = await fetch('/api/generate-static-sitemap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showResult(`
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 生成成功</h5>
<p>${data.message}</p>
<table class="table table-sm mt-2">
<tr><th>URL数量</th><td>${data.total_urls}</td></tr>
<tr><th>文件路径:</th><td><code>${data.file_path}</code></td></tr>
<tr><th>生成时间:</th><td>${data.timestamp}</td></tr>
</table>
</div>
`);
// 刷新页面以更新sitemap状态
setTimeout(() => location.reload(), 2000);
} else {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 生成失败</h5>
<p>${data.message}</p>
</div>
`);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 请求失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
// 通知搜索引擎
document.getElementById('notifyEnginesBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 通知中...';
try {
const response = await fetch('/api/notify-search-engines', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (data.success) {
let resultsHtml = `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> ${data.message}</h5>
<p>Sitemap URL: <code>${data.sitemap_url}</code></p>
</div>
`;
data.results.forEach(result => {
const statusClass = result.status === 'success' ? 'success' : 'error';
const icon = result.status === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
resultsHtml += `
<div class="result-item ${statusClass}">
<h6><i class="fa ${icon}"></i> ${result.engine}</h6>
<p>${result.message}</p>
</div>
`;
});
showResult(resultsHtml);
} else {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 通知失败</h5>
<p>${data.message}</p>
</div>
`);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 请求失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
// 一键生成并通知
document.getElementById('doAllBtn').addEventListener('click', async function() {
disableButtons();
const originalText = this.innerHTML;
this.innerHTML = '<i class="fa fa-spinner fa-spin"></i> 处理中...';
let allResultsHtml = '';
try {
// Step 1: 生成sitemap
const sitemapResponse = await fetch('/api/generate-static-sitemap', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const sitemapData = await sitemapResponse.json();
if (sitemapData.success) {
allResultsHtml += `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 步骤1生成sitemap成功</h5>
<p>${sitemapData.message}</p>
</div>
`;
// Step 2: 通知搜索引擎
const notifyResponse = await fetch('/api/notify-search-engines', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const notifyData = await notifyResponse.json();
if (notifyData.success) {
allResultsHtml += `
<div class="result-item success">
<h5><i class="fa fa-check-circle"></i> 步骤2${notifyData.message}</h5>
</div>
`;
notifyData.results.forEach(result => {
const statusClass = result.status === 'success' ? 'success' : 'warning';
const icon = result.status === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle';
allResultsHtml += `
<div class="result-item ${statusClass}">
<h6><i class="fa ${icon}"></i> ${result.engine}</h6>
<p>${result.message}</p>
</div>
`;
});
} else {
allResultsHtml += `
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 步骤2通知搜索引擎失败</h5>
<p>${notifyData.message}</p>
</div>
`;
}
} else {
allResultsHtml += `
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 步骤1生成sitemap失败</h5>
<p>${sitemapData.message}</p>
</div>
`;
}
showResult(allResultsHtml);
// 如果全部成功2秒后刷新页面
if (sitemapData.success) {
setTimeout(() => location.reload(), 3000);
}
} catch (error) {
showResult(`
<div class="result-item error">
<h5><i class="fa fa-times-circle"></i> 操作失败</h5>
<p>网络错误: ${error.message}</p>
</div>
`);
} finally {
this.innerHTML = originalText;
enableButtons();
}
});
</script>
{% endblock %}