Files
2026-08-28 11:11:25 +08:00

444 lines
22 KiB
HTML

<!-- admin.html -->
{% extends "base.html" %}
{% block content %}
<div class="repo-container repo-container--fluid" style="padding: 16px 24px;">
<!-- 顶部导航 -->
<div class="repo-flex repo-items-center repo-justify-between repo-flex-wrap repo-gap-4 repo-mb-6">
<div class="repo-flex repo-items-center repo-gap-3">
<div class="repo-avatar repo-avatar--md" style="background: var(--repo-color-primary); color: #fff;">
<i class="fas fa-cogs"></i>
</div>
<div>
<h1 class="repo-text-lg repo-font-bold">博客管理系统</h1>
<p class="repo-text-sm repo-text-secondary">v2.0.1 | {{ articles|length }}篇文章</p>
</div>
</div>
<div class="repo-flex repo-gap-2">
<a href="/" target="_blank" class="repo-btn repo-btn--secondary repo-btn--sm">
<i class="fas fa-external-link-alt repo-mr-2"></i>查看博客
</a>
<a href="{{ url_for('admin_logout') }}" class="repo-btn repo-btn--danger repo-btn--sm">
<i class="fas fa-sign-out-alt repo-mr-2"></i>退出登录
</a>
</div>
</div>
<!-- 状态卡片 -->
<div class="repo-row repo-mb-6">
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-flex repo-items-center repo-justify-between">
<div>
<p class="repo-text-sm repo-text-secondary">文章总数</p>
<p class="repo-text-2xl repo-font-bold">{{ articles|length }}</p>
</div>
<i class="fas fa-file-alt" style="font-size: 24px; color: var(--repo-color-primary);"></i>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-flex repo-items-center repo-justify-between">
<div>
<p class="repo-text-sm repo-text-secondary">文件存储</p>
<p class="repo-text-2xl repo-font-bold">∞</p>
</div>
<i class="fas fa-database" style="font-size: 24px; color: var(--repo-color-success);"></i>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body repo-flex repo-items-center repo-justify-between">
<div>
<p class="repo-text-sm repo-text-secondary">系统状态</p>
<p class="repo-text-2xl repo-font-bold">正常</p>
</div>
<i class="fas fa-check-circle" style="font-size: 24px; color: var(--repo-color-warning);"></i>
</div>
</div>
</div>
</div>
<!-- 双栏布局 -->
<div class="repo-row">
<!-- 左侧:发布文章 + 文件上传 -->
<div class="repo-col" style="flex: 0 0 50%; max-width: 50%;">
<div class="repo-card repo-mb-4">
<div class="repo-card__header">
<div class="repo-flex repo-items-center repo-gap-2">
<i class="fas fa-edit" style="color: var(--repo-color-primary);"></i>
<h2 class="repo-card__title">发布新文章</h2>
</div>
</div>
<div class="repo-card__body">
<form id="articleForm">
<div class="repo-mb-4">
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">文章标题</label>
<input type="text" id="title" name="title" required
class="repo-input" placeholder="请输入文章标题">
</div>
<div class="repo-mb-4">
<label class="repo-text-sm repo-font-medium repo-mb-2 repo-block">文章内容 (Markdown)</label>
<textarea id="content" name="content" rows="12" required
class="repo-input repo-textarea" placeholder="在此输入Markdown内容..."
style="font-family: var(--repo-font-family-mono); font-size: 13px;"></textarea>
</div>
<!-- Markdown快捷工具栏 -->
<div class="repo-flex repo-flex-wrap repo-gap-2 repo-mb-4" style="padding: 8px; background: var(--repo-color-bg-tertiary); border-radius: var(--repo-radius-md);">
<button type="button" onclick="insertMd('# ')" class="repo-btn repo-btn--ghost repo-btn--sm">H1</button>
<button type="button" onclick="insertMd('## ')" class="repo-btn repo-btn--ghost repo-btn--sm">H2</button>
<button type="button" onclick="insertMd('**粗体**')" class="repo-btn repo-btn--ghost repo-btn--sm">粗体</button>
<button type="button" onclick="insertMd('*斜体*')" class="repo-btn repo-btn--ghost repo-btn--sm">斜体</button>
<button type="button" onclick="insertMd('[链接](https://)')" class="repo-btn repo-btn--ghost repo-btn--sm">链接</button>
<button type="button" onclick="insertMd('![图片](https://)')" class="repo-btn repo-btn--ghost repo-btn--sm">图片</button>
<button type="button" onclick="insertMd('```\n代码\n```')" class="repo-btn repo-btn--ghost repo-btn--sm">代码</button>
<button type="button" onclick="insertMd('- ')">列表</button>
<button type="button" onclick="insertMd('> ')" class="repo-btn repo-btn--ghost repo-btn--sm">引用</button>
</div>
<button type="submit" class="repo-btn repo-btn--primary repo-w-full" style="padding: 10px;">
<i class="fas fa-paper-plane repo-mr-2"></i>发布文章
</button>
</form>
</div>
</div>
<!-- 文件上传 -->
<div class="repo-card">
<div class="repo-card__header">
<div class="repo-flex repo-items-center repo-gap-2">
<i class="fas fa-cloud-upload-alt" style="color: var(--repo-color-success);"></i>
<h2 class="repo-card__title">文件上传</h2>
</div>
</div>
<div class="repo-card__body">
<div id="uploadArea"
style="border: 2px dashed var(--repo-color-border); border-radius: var(--repo-radius-lg); padding: 32px; text-align: center; cursor: pointer; transition: all 0.2s ease;">
<div class="repo-text-tertiary repo-text-2xl repo-mb-4">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<h3 class="repo-font-medium repo-mb-2">拖放文件到此处</h3>
<p class="repo-text-sm repo-text-secondary repo-mb-3">或点击选择文件</p>
<input type="file" id="fileInput" multiple class="repo-hidden">
<p class="repo-text-xs repo-text-tertiary">支持图片、视频、文档等 (最大100MB)</p>
</div>
<div id="uploadProgress" class="repo-hidden repo-mt-4"></div>
<div id="uploadedFiles" class="repo-mt-4" style="max-height: 240px; overflow-y: auto;"></div>
</div>
</div>
</div>
<!-- 右侧:文章管理 + 系统工具 -->
<div class="repo-col" style="flex: 0 0 50%; max-width: 50%;">
<div class="repo-card repo-mb-4">
<div class="repo-card__header">
<div class="repo-flex repo-items-center repo-justify-between">
<div class="repo-flex repo-items-center repo-gap-2">
<i class="fas fa-list" style="color: var(--repo-color-primary);"></i>
<h2 class="repo-card__title">文章管理</h2>
</div>
<span class="repo-text-sm repo-text-secondary">{{ articles|length }}篇文章</span>
</div>
</div>
<div class="repo-card__body">
{% if articles %}
<div style="max-height: 600px; overflow-y: auto;">
{% for article in articles %}
<div class="repo-card repo-mb-3">
<div class="repo-card__body repo-flex repo-items-start repo-justify-between" style="padding: 12px 16px;">
<div class="repo-flex-1 repo-mr-4" style="min-width: 0;">
<h3 class="repo-font-medium repo-truncate repo-mb-1">{{ article.title }}</h3>
<div class="repo-text-xs repo-text-secondary">
<div class="repo-flex repo-items-center repo-gap-3 repo-mb-1">
<span><i class="far fa-user repo-mr-1"></i>{{ article.author }}</span>
<span><i class="far fa-clock repo-mr-1"></i>{{ article.created_at }}</span>
</div>
<div>
<i class="fas fa-link repo-mr-1"></i>
<code class="repo-text-xs">/docs/{{ article.filename }}</code>
</div>
</div>
</div>
<div class="repo-flex repo-gap-1 repo-flex-none">
<a href="/docs/{{ article.filename }}" target="_blank"
class="repo-btn repo-btn--ghost repo-btn--sm">
<i class="fas fa-eye"></i>
</a>
<button onclick="deleteArticle('{{ article.filename }}')"
class="repo-btn repo-btn--ghost repo-btn--sm" style="color: var(--repo-color-danger);">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="repo-text-center repo-py-8">
<div class="repo-text-tertiary repo-text-2xl repo-mb-4">
<i class="far fa-file-alt"></i>
</div>
<p class="repo-mb-2">暂无文章</p>
<p class="repo-text-sm repo-text-secondary">发布第一篇文章开始你的博客</p>
</div>
{% endif %}
</div>
</div>
<!-- 系统工具 -->
<div class="repo-card">
<div class="repo-card__header">
<div class="repo-flex repo-items-center repo-gap-2">
<i class="fas fa-tools" style="color: var(--repo-color-warning);"></i>
<h2 class="repo-card__title">系统工具</h2>
</div>
</div>
<div class="repo-card__body">
<div class="repo-row">
<div class="repo-col">
<button onclick="showToast('导出功能开发中', 'info')"
class="repo-btn repo-btn--ghost repo-w-full repo-mb-3" style="padding: 16px; height: auto; flex-direction: column; gap: 8px;">
<i class="fas fa-download" style="font-size: 18px; color: var(--repo-color-primary);"></i>
<span class="repo-text-sm">导出文章</span>
</button>
</div>
<div class="repo-col">
<button onclick="clearCache()"
class="repo-btn repo-btn--ghost repo-w-full repo-mb-3" style="padding: 16px; height: auto; flex-direction: column; gap: 8px;">
<i class="fas fa-broom" style="font-size: 18px; color: var(--repo-color-success);"></i>
<span class="repo-text-sm">清理缓存</span>
</button>
</div>
<div class="repo-col">
<button onclick="showToast('备份功能开发中', 'info')"
class="repo-btn repo-btn--ghost repo-w-full repo-mb-3" style="padding: 16px; height: auto; flex-direction: column; gap: 8px;">
<i class="fas fa-save" style="font-size: 18px; color: var(--repo-color-primary);"></i>
<span class="repo-text-sm">备份数据</span>
</button>
</div>
<div class="repo-col">
<button onclick="showToast('日志查看功能开发中', 'info')"
class="repo-btn repo-btn--ghost repo-w-full repo-mb-3" style="padding: 16px; height: auto; flex-direction: column; gap: 8px;">
<i class="fas fa-clipboard-list" style="font-size: 18px; color: var(--repo-color-danger);"></i>
<span class="repo-text-sm">查看日志</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 文章表单处理
document.getElementById('articleForm').addEventListener('submit', async function(e) {
e.preventDefault();
var formData = new FormData();
formData.append('title', document.getElementById('title').value);
formData.append('content', document.getElementById('content').value);
var submitBtn = this.querySelector('button[type="submit"]');
var originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<span class="repo-spinner repo-spinner--sm repo-mr-2"></span>发布中...';
submitBtn.disabled = true;
try {
var response = await fetch('/admin/create_article', { method: 'POST', body: formData });
var data = await response.json();
if (data.success) {
showToast('文章发布成功!', 'success');
setTimeout(function() { window.location.reload(); }, 1000);
} else {
showToast(data.error || '发布失败', 'error');
}
} catch (error) {
showToast('网络错误: ' + error.message, 'error');
} finally {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
});
// 文件上传
var uploadArea = document.getElementById('uploadArea');
var fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', function() { fileInput.click(); });
uploadArea.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.borderColor = 'var(--repo-color-primary)';
this.style.background = 'rgba(59,130,246,0.05)';
});
uploadArea.addEventListener('dragleave', function() {
this.style.borderColor = 'var(--repo-color-border)';
this.style.background = '';
});
uploadArea.addEventListener('drop', async function(e) {
e.preventDefault();
this.style.borderColor = 'var(--repo-color-border)';
this.style.background = '';
await handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', async function(e) {
await handleFiles(e.target.files);
});
async function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
await uploadFile(files[i]);
}
}
async function uploadFile(file) {
var progressId = 'progress-' + Date.now();
var div = document.createElement('div');
div.id = progressId;
div.className = 'repo-card repo-mb-2';
div.innerHTML = [
'<div class="repo-card__body" style="padding: 10px 14px;">',
' <div class="repo-flex repo-items-center repo-justify-between repo-mb-2">',
' <span class="repo-text-sm repo-truncate" style="max-width: 200px;">' + file.name + '</span>',
' <span class="repo-text-xs repo-text-secondary">' + formatSize(file.size) + '</span>',
' </div>',
' <div class="repo-progress" style="height: 6px; background: var(--repo-color-bg-tertiary); border-radius: 999px; overflow: hidden;">',
' <div class="progress-bar" style="height: 100%; width: 0; background: var(--repo-color-primary); border-radius: 999px; transition: width 0.2s;"></div>',
' </div>',
'</div>'
].join('');
document.getElementById('uploadProgress').appendChild(div);
document.getElementById('uploadProgress').classList.remove('repo-hidden');
var formData = new FormData();
formData.append('file', file);
try {
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
var pct = (e.loaded / e.total) * 100;
div.querySelector('.progress-bar').style.width = pct + '%';
}
});
xhr.onload = function() {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
if (data.success) {
div.querySelector('.progress-bar').style.background = 'var(--repo-color-success)';
var item = document.createElement('div');
item.className = 'repo-flex repo-items-center repo-justify-between repo-card repo-mb-2';
item.style.padding = '10px 14px';
item.innerHTML = [
'<div class="repo-flex repo-items-center repo-gap-2">',
' <i class="fas fa-file" style="color: var(--repo-color-primary);"></i>',
' <div>',
' <p class="repo-text-sm repo-truncate" style="max-width: 180px;">' + data.filename + '</p>',
' <p class="repo-text-xs repo-text-secondary">' + formatSize(file.size) + '</p>',
' </div>',
'</div>',
'<button onclick="copyToClipboard(\'' + data.url + '\')" class="repo-btn repo-btn--ghost repo-btn--sm">复制链接</button>'
].join('');
document.getElementById('uploadedFiles').prepend(item);
setTimeout(function() { div.remove(); if (!document.getElementById('uploadProgress').children.length) { document.getElementById('uploadProgress').classList.add('repo-hidden'); } }, 2000);
showToast(file.name + ' 上传成功', 'success');
} else {
showToast('上传失败: ' + data.error, 'error');
}
}
};
xhr.open('POST', '/admin/upload_file');
xhr.send(formData);
} catch (error) {
showToast('上传失败: ' + error.message, 'error');
}
}
// 删除文章
async function deleteArticle(filename) {
if (!confirm('确定要删除这篇文章吗?此操作不可恢复。')) return;
var formData = new FormData();
formData.append('filename', filename);
try {
var response = await fetch('/admin/delete_article', { method: 'POST', body: formData });
var data = await response.json();
if (data.success) {
showToast('文章删除成功', 'success');
setTimeout(function() { window.location.reload(); }, 1000);
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('网络错误: ' + error.message, 'error');
}
}
// 工具函数
function formatSize(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function insertMd(text) {
var ta = document.getElementById('content');
var start = ta.selectionStart;
var end = ta.selectionEnd;
var selected = ta.value.substring(start, end);
ta.value = ta.value.substring(0, start) + text + ta.value.substring(end);
ta.focus();
if (selected) {
ta.setSelectionRange(start, start + text.length);
} else {
ta.setSelectionRange(start + text.length, start + text.length);
}
}
function clearCache() {
if (confirm('确定要清理系统缓存吗?')) {
showToast('缓存清理中...', 'info');
}
}
// 自动保存草稿
var saveTimeout;
document.getElementById('content').addEventListener('input', function() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(function() {
localStorage.setItem('article_draft', JSON.stringify({
title: document.getElementById('title').value,
content: document.getElementById('content').value,
time: new Date().toLocaleTimeString()
}));
}, 2000);
});
document.addEventListener('DOMContentLoaded', function() {
var draft = localStorage.getItem('article_draft');
if (draft) {
var data = JSON.parse(draft);
if (confirm('发现未保存的草稿 (保存于 ' + data.time + '),是否加载?')) {
document.getElementById('title').value = data.title;
document.getElementById('content').value = data.content;
}
}
});
</script>
{% endblock %}