新增功能: - 用户管理列表页面(搜索、分页) - 用户详情页面(基本信息、收藏统计) - 管理员重置用户密码功能 - 管理员修改用户昵称功能 - 管理后台首页添加用户统计卡片 优化改进: - 统一后台菜单结构,创建可复用的 sidebar 组件 - 所有后台页面使用统一菜单,避免硬编码 - 优化权限配置文件,清理冗余规则 技术文档: - 添加任务分解规则文档 - 添加后台菜单统一规则文档 - 添加数据库字段修复脚本 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
482 lines
18 KiB
HTML
482 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>SEO工具管理 - ZJPB - 自己品吧</title>
|
||
|
||
<!-- Google Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||
|
||
<!-- Google Material Symbols -->
|
||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||
|
||
<!-- Font Awesome for icons -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
|
||
<!-- Bootstrap CSS -->
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
|
||
<!-- Custom Admin Theme -->
|
||
<link href="{{ url_for('static', filename='css/admin-sidebar.css') }}" rel="stylesheet">
|
||
<link href="{{ url_for('static', filename='css/admin-actions.css') }}" rel="stylesheet">
|
||
</head>
|
||
<body class="admin-sidebar-layout">
|
||
{% set active_page = 'seo' %}
|
||
{% include 'admin/components/sidebar.html' %}
|
||
|
||
<!-- 右侧主内容区 -->
|
||
<div class="admin-main">
|
||
<!-- 顶部导航栏 -->
|
||
<header class="admin-header">
|
||
<div class="header-breadcrumb">
|
||
<a href="{{ url_for('admin.index') }}" class="breadcrumb-link">控制台</a>
|
||
<span class="breadcrumb-separator">/</span>
|
||
<span class="breadcrumb-current">SEO工具管理</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<div class="search-box">
|
||
<span class="material-symbols-outlined search-icon">search</span>
|
||
<input type="text" placeholder="全局搜索..." class="search-input">
|
||
</div>
|
||
<button class="header-btn">
|
||
<span class="material-symbols-outlined">notifications</span>
|
||
</button>
|
||
<button class="header-btn">
|
||
<span class="material-symbols-outlined">settings</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 页面内容 -->
|
||
<main class="admin-content">
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">
|
||
<i class="fa fa-search"></i> SEO工具管理
|
||
</h1>
|
||
<p class="page-description">管理sitemap、通知搜索引擎更新</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Bootstrap JS -->
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
<style>
|
||
.page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.page-description {
|
||
color: #666;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.card {
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
margin-bottom: 20px;
|
||
border-radius: 8px;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.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>
|
||
</body>
|
||
</html>
|