Files
tech-blog-dev/app/templates/admin/backup.html
T
hz4th_coder df2f0860b5 feat: 新增备份恢复功能 & 改进图片上传体验
- 新增备份管理页面(备份/恢复/下载/删除)
- 新增 backup_service 服务层
- 文章编辑器增加工具栏图片上传按钮
- 优化图片上传交互提示
2026-07-16 16:38:38 +08:00

275 lines
6.4 KiB
HTML

{% extends 'admin/base.html' %}
{% block title %}数据备份{% endblock %}
{% block page_title %}数据备份管理{% endblock %}
{% block extra_css %}
<style>
.backup-section {
background: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.backup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.backup-header h2 {
margin: 0;
font-size: 1.2em;
}
.backup-stats {
display: flex;
gap: 30px;
margin-bottom: 20px;
}
.stat-item {
background: #f8f9fa;
padding: 15px 25px;
border-radius: 8px;
text-align: center;
}
.stat-item .value {
font-size: 1.8em;
font-weight: 600;
color: var(--admin-primary);
}
.stat-item .label {
font-size: 0.85em;
color: #666;
margin-top: 5px;
}
.backup-actions {
background: #e8f4fc;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.backup-actions .action-info {
flex: 1;
}
.backup-actions h3 {
margin: 0 0 8px 0;
font-size: 1.1em;
}
.backup-actions p {
margin: 0;
color: #666;
font-size: 0.9em;
}
.backup-list {
margin-top: 20px;
}
.backup-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 10px;
transition: all 0.2s;
}
.backup-item:hover {
border-color: var(--admin-primary);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.backup-info {
display: flex;
gap: 20px;
align-items: center;
}
.backup-info .icon {
font-size: 2em;
}
.backup-info .details {
display: flex;
flex-direction: column;
gap: 5px;
}
.backup-info .filename {
font-weight: 500;
color: #333;
}
.backup-info .meta {
font-size: 0.85em;
color: #666;
display: flex;
gap: 15px;
}
.backup-actions-buttons {
display: flex;
gap: 10px;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
background: #f8f9fa;
border-radius: 8px;
}
.empty-state .icon {
font-size: 3em;
margin-bottom: 15px;
}
/* 备份中状态 */
.backup-item.processing {
opacity: 0.6;
pointer-events: none;
}
</style>
{% endblock %}
{% block extra_js %}
<script>
function createBackup() {
const btn = document.getElementById('create-backup-btn');
const originalText = btn.textContent;
btn.textContent = '⏳ 备份中...';
btn.disabled = true;
fetch('{{ url_for("admin.backup_create") }}', {
method: 'POST'
})
.then(res => res.json())
.then(data => {
btn.textContent = originalText;
btn.disabled = false;
if (data.success) {
alert('✅ 备份成功!\n\n文件: ' + data.backup.filename + '\n大小: ' + data.backup.size);
location.reload();
} else {
alert('❌ 备份失败: ' + data.message);
}
})
.catch(err => {
btn.textContent = originalText;
btn.disabled = false;
alert('备份失败: ' + err.message);
});
}
function deleteBackup(filename) {
if (!confirm('确定要删除备份文件 ' + filename + ' 吗?')) {
return;
}
fetch('/admin/backup/delete/' + filename, {
method: 'POST'
})
.then(res => res.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('删除失败: ' + data.message);
}
});
}
</script>
{% endblock %}
{% block content %}
<div class="backup-section">
<div class="backup-header">
<h2>📊 备份概览</h2>
</div>
<div class="backup-stats">
<div class="stat-item">
<div class="value">{{ stats.total_backups }}</div>
<div class="label">备份文件数</div>
</div>
<div class="stat-item">
<div class="value">{{ (stats.total_size / 1024 / 1024) | round(2) }} MB</div>
<div class="label">备份总大小</div>
</div>
</div>
<div class="backup-actions">
<div class="action-info">
<h3>💾 创建新备份</h3>
<p>一键备份网站数据库和所有上传的图片附件资源,打包成 ZIP 文件保存。</p>
</div>
<button id="create-backup-btn" onclick="createBackup()" class="btn btn-primary">
📦 立即备份
</button>
</div>
<div class="backup-list">
<h3 style="margin-bottom: 15px;">备份文件列表</h3>
{% if backups %}
{% for backup in backups %}
<div class="backup-item">
<div class="backup-info">
<div class="icon">📦</div>
<div class="details">
<div class="filename">{{ backup.filename }}</div>
<div class="meta">
<span>📅 {{ backup.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
<span>💾 {{ (backup.size / 1024) | round(1) }} KB</span>
</div>
</div>
</div>
<div class="backup-actions-buttons">
<a href="{{ url_for('admin.backup_download', filename=backup.filename) }}"
class="btn btn-sm" target="_blank">📥 下载</a>
<button onclick="deleteBackup('{{ backup.filename }}')"
class="btn btn-sm btn-danger">🗑️ 删除</button>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<div class="icon">📭</div>
<p>暂无备份文件</p>
<p style="margin-top: 10px; font-size: 0.9em;">点击上方"立即备份"按钮创建第一个备份</p>
</div>
{% endif %}
</div>
</div>
<div style="background: #fff3cd; border-radius: 8px; padding: 15px; margin-top: 20px;">
<h4 style="margin: 0 0 10px 0;">⚠️ 注意事项</h4>
<ul style="margin: 0; padding-left: 20px; font-size: 0.9em; color: #856404;">
<li>备份包含数据库文件和上传的图片附件</li>
<li>备份文件保存在服务器的 <code>backups/</code> 目录</li>
<li>建议定期备份并下载到本地保存</li>
<li>恢复数据需要手动解压并替换相应文件</li>
</ul>
</div>
{% endblock %}