Initial commit: DVS Blog v1.0.0
This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
<!-- 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('')" 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 %}
|
||||
Reference in New Issue
Block a user