Initial commit: DVS Blog v1.0.0

This commit is contained in:
dvs
2026-08-28 11:11:25 +08:00
commit c93e2768bc
9 changed files with 3580 additions and 0 deletions
+444
View File
@@ -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('![图片](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 %}
+91
View File
@@ -0,0 +1,91 @@
<!-- admin_login.html -->
{% extends "base.html" %}
{% block content %}
<div class="repo-min-h-screen repo-flex repo-items-center repo-justify-center" style="padding: 16px;">
<div style="width: 100%; max-width: 420px;">
<div class="repo-card">
<div class="repo-card__body repo-py-6">
<!-- 徽标 -->
<div class="repo-text-center repo-mb-6">
<div class="repo-flex repo-items-center repo-justify-center repo-mb-4">
<div class="repo-avatar repo-avatar--lg" style="background: var(--repo-color-primary); color: #fff;">
<i class="fas fa-lock"></i>
</div>
</div>
<h1 class="repo-text-lg repo-font-bold">管理员登录</h1>
<p class="repo-text-sm repo-text-secondary">DVS博客管理系统</p>
</div>
<!-- 错误信息 -->
{% if error %}
<div class="repo-message" style="position: static; transform: none; animation: none; margin-bottom: 16px; border-left: 3px solid var(--repo-color-danger);">
<i class="fas fa-exclamation-circle" style="color: var(--repo-color-danger);"></i>
<span>{{ error }}</span>
</div>
{% endif %}
<!-- 登录表单 -->
<form method="POST" action="{{ url_for('admin_login') }}">
<div class="repo-mb-4">
<label for="password" class="repo-text-sm repo-font-medium repo-mb-2 repo-block">管理员密码</label>
<div class="repo-relative">
<input type="password" id="password" name="password" required
class="repo-input" placeholder="请输入管理员密码"
style="padding-right: 40px;">
<button type="button" onclick="togglePassword()"
class="repo-absolute" style="right: 10px; top: 8px; color: var(--repo-color-text-tertiary); background: none; border: none;">
<i class="far fa-eye" id="pwdIcon"></i>
</button>
</div>
</div>
<!-- 安全提示 -->
<div class="repo-card repo-mb-4" style="border-color: rgba(59,130,246,0.3); background: rgba(59,130,246,0.05);">
<div class="repo-card__body" style="padding: 12px 16px;">
<div class="repo-flex repo-gap-2">
<i class="fas fa-shield-alt" style="color: var(--repo-color-primary);"></i>
<div>
<p class="repo-text-sm repo-font-medium" style="color: var(--repo-color-primary);">安全提示</p>
<p class="repo-text-xs repo-text-secondary">密码使用SHA256加密存储,请确保在安全环境下登录</p>
</div>
</div>
</div>
</div>
<button type="submit" class="repo-btn repo-btn--primary repo-w-full" style="padding: 10px;">
<i class="fas fa-sign-in-alt repo-mr-2"></i>登录系统
</button>
</form>
<!-- 返回链接 -->
<div class="repo-text-center repo-mt-6 repo-pt-4 repo-border-t">
<a href="/" class="repo-text-sm repo-text-secondary">
<i class="fas fa-arrow-left repo-mr-2"></i>返回博客首页
</a>
</div>
</div>
</div>
<div class="repo-text-center repo-mt-4">
<p class="repo-text-xs repo-text-tertiary">
<i class="fas fa-server repo-mr-1"></i>系统版本:v2.0.1
</p>
</div>
</div>
</div>
<script>
function togglePassword() {
var input = document.getElementById('password');
var icon = document.getElementById('pwdIcon');
if (input.type === 'password') {
input.type = 'text';
icon.className = 'far fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'far fa-eye';
}
}
</script>
{% endblock %}
+173
View File
@@ -0,0 +1,173 @@
<!-- article.html -->
{% extends "base.html" %}
{% block content %}
<div class="repo-container repo-py-6">
<!-- 返回按钮 -->
<div class="repo-mb-4">
<a href="/" class="repo-btn repo-btn--ghost repo-btn--sm">
<i class="fas fa-arrow-left repo-mr-2"></i>返回首页
</a>
</div>
<!-- 文章容器 -->
<div class="repo-card">
<!-- 文章头部 -->
<div class="repo-card__header" style="background: var(--repo-color-primary); color: #fff;">
<h1 class="repo-text-xl repo-font-bold repo-mb-3">{{ title }}</h1>
<div class="repo-flex repo-items-center repo-justify-between repo-flex-wrap repo-gap-3">
<div class="repo-flex repo-items-center repo-gap-4">
<div class="repo-flex repo-items-center repo-gap-1">
<i class="far fa-user"></i>
<span>{{ author }}</span>
</div>
<div class="repo-flex repo-items-center repo-gap-1">
<i class="far fa-clock"></i>
<span>{{ created_at }}</span>
</div>
</div>
<div class="repo-flex repo-gap-2">
<button onclick="window.print()" class="repo-btn repo-btn--ghost repo-btn--sm" style="color: #fff; border-color: rgba(255,255,255,0.3);">
<i class="fas fa-print"></i>
</button>
<button onclick="shareArticle()" class="repo-btn repo-btn--ghost repo-btn--sm" style="color: #fff; border-color: rgba(255,255,255,0.3);">
<i class="fas fa-share-alt"></i>
</button>
</div>
</div>
</div>
<!-- 文章内容 -->
<div class="repo-card__body article-content">
{{ content|safe }}
</div>
<!-- 底部操作 -->
<div class="repo-card__footer">
<div class="repo-flex repo-items-center repo-justify-between">
<div class="repo-text-sm repo-text-secondary">
<i class="far fa-clock repo-mr-1"></i>阅读时间约需 5分钟
</div>
<div class="repo-flex repo-gap-2">
<button onclick="scrollToTop()" class="repo-btn repo-btn--ghost repo-btn--icon">
<i class="fas fa-arrow-up"></i>
</button>
<a href="/" class="repo-btn repo-btn--primary">
返回首页
</a>
</div>
</div>
</div>
</div>
<!-- 导航提示 -->
<div class="repo-row repo-mt-4">
<div class="repo-col">
<div class="repo-card" style="border-color: var(--repo-color-primary);">
<div class="repo-card__body" style="padding: 12px 16px;">
<p class="repo-text-sm" style="color: var(--repo-color-primary);">
<i class="fas fa-info-circle repo-mr-2"></i>使用键盘左右箭头可快速导航文章
</p>
</div>
</div>
</div>
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body" style="padding: 12px 16px;">
<p class="repo-text-sm repo-text-secondary">
<i class="fas fa-bookmark repo-mr-2"></i>按 Ctrl+D 收藏本页
</p>
</div>
</div>
</div>
</div>
</div>
<style>
.article-content {
color: var(--repo-color-text);
line-height: 1.75;
font-size: 15px;
}
.article-content h1, .article-content h2, .article-content h3, .article-content h4 {
color: var(--repo-color-text);
margin-top: 2em;
margin-bottom: 1em;
font-weight: 700;
line-height: 1.4;
}
.article-content h1 { font-size: 1.9em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.4em; }
.article-content h2 { font-size: 1.5em; border-bottom: 1px solid var(--repo-color-border); padding-bottom: 0.3em; }
.article-content h3 { font-size: 1.25em; }
.article-content h4 { font-size: 1.1em; }
.article-content p { margin: 1em 0; }
.article-content a { color: var(--repo-color-link); text-decoration: underline; }
.article-content ul, .article-content ol { margin: 1em 0; padding-left: 1.8em; }
.article-content ul { list-style: disc; }
.article-content ol { list-style: decimal; }
.article-content li { margin: 0.4em 0; }
.article-content blockquote {
border-left: 4px solid var(--repo-color-primary);
padding: 0.6em 1.2em;
margin: 1.5em 0;
color: var(--repo-color-text-secondary);
background: var(--repo-color-bg-tertiary);
border-radius: 0 6px 6px 0;
}
.article-content img { max-width: 100%; border-radius: 12px; margin: 2rem auto; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.article-content table { width: 100%; border-collapse: collapse; margin: 1.5em 0; }
.article-content th, .article-content td { padding: 10px 14px; border: 1px solid var(--repo-color-border); text-align: left; }
.article-content th { background: var(--repo-color-bg-secondary); font-weight: 600; }
.article-content pre {
background: #1f2937; color: #f3f4f6; border-radius: 8px; padding: 1.25rem 1.5rem;
overflow-x: auto; margin: 1.5rem 0; font-family: var(--repo-font-family-mono);
font-size: 0.875em; line-height: 1.6; border: 1px solid #374151;
}
.article-content pre code { background: transparent; padding: 0; border-radius: 0; font-size: inherit; color: inherit; }
.article-content code:not(pre code) {
background: var(--repo-color-bg-tertiary); padding: 0.2rem 0.4rem;
border-radius: 4px; font-size: 0.875em; color: var(--repo-color-text);
}
.article-content pre::-webkit-scrollbar { height: 8px; }
.article-content pre::-webkit-scrollbar-track { background: #1a1a1a; border-radius: 4px; }
.article-content pre::-webkit-scrollbar-thumb { background: var(--repo-color-primary); border-radius: 4px; }
</style>
<script>
function shareArticle() {
if (navigator.share) {
navigator.share({ title: document.title, url: window.location.href });
} else {
copyToClipboard(window.location.href);
}
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.addEventListener('keydown', function(e) {
if (e.key === 'ArrowLeft') { /* 上一篇逻辑 */ }
else if (e.key === 'ArrowRight') { /* 下一篇逻辑 */ }
});
// 代码块复制按钮
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.article-content pre').forEach(function(pre) {
var container = document.createElement('div');
container.className = 'repo-relative';
pre.parentNode.insertBefore(container, pre);
container.appendChild(pre);
var btn = document.createElement('button');
btn.className = 'copy-button';
btn.innerHTML = '<i class="far fa-copy repo-mr-1"></i>复制';
btn.onclick = function() {
var code = pre.querySelector('code') ? pre.querySelector('code').innerText : pre.innerText;
copyToClipboard(code);
};
container.appendChild(btn);
});
});
</script>
{% endblock %}
+115
View File
@@ -0,0 +1,115 @@
<!-- base.html -->
<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}DVS的博客{% endblock %}</title>
<link rel="stylesheet" href="/css.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body data-theme="light" class="repo-bg-gray">
<!-- 主题切换 -->
<div class="repo-fixed" style="top: 16px; right: 16px; z-index: 1060;">
<div class="repo-dropdown" id="themeDropdown">
<button class="repo-btn repo-btn--secondary repo-btn--icon" id="themeToggle" title="切换主题">
<i class="fas fa-sun" id="themeIcon"></i>
</button>
<div class="repo-dropdown__menu" id="themeMenu" style="right: 0; left: auto;">
<button class="repo-dropdown__item" onclick="setTheme('light')">
<i class="fas fa-sun repo-mr-2" style="width:16px;"></i> 浅色模式
</button>
<button class="repo-dropdown__item" onclick="setTheme('dark')">
<i class="fas fa-moon repo-mr-2" style="width:16px;"></i> 深色模式
</button>
</div>
</div>
</div>
{% block content %}{% endblock %}
<script>
// 主题切换
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
document.body.setAttribute('data-theme', theme);
localStorage.setItem('blog-theme', theme);
const icon = document.getElementById('themeIcon');
icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun';
}
// 页面加载时恢复保存的主题
document.addEventListener('DOMContentLoaded', function() {
const savedTheme = localStorage.getItem('blog-theme') || 'light';
setTheme(savedTheme);
});
// 下拉菜单控制
document.getElementById('themeToggle').addEventListener('click', function(e) {
e.stopPropagation();
document.getElementById('themeDropdown').classList.toggle('repo-dropdown--open');
});
document.addEventListener('click', function() {
document.getElementById('themeDropdown').classList.remove('repo-dropdown--open');
});
document.getElementById('themeMenu').addEventListener('click', function(e) {
e.stopPropagation();
});
// 键盘快捷键:Ctrl+T 切换主题
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
const current = document.documentElement.getAttribute('data-theme') || 'light';
setTheme(current === 'dark' ? 'light' : 'dark');
}
});
// Toast消息
function showToast(message, type) {
var existing = document.querySelector('.custom-toast');
if (existing) existing.remove();
var toast = document.createElement('div');
toast.className = 'custom-toast';
toast.style.cssText = 'position:fixed;top:24px;left:50%;transform:translateX(-50%);z-index:1050;padding:12px 24px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);color:#fff;transition:all 0.3s ease;';
var colors = {
success: '#10b981',
error: '#ef4444',
info: '#3b82f6',
warning: '#f59e0b'
};
toast.style.background = colors[type] || colors.info;
var icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
info: 'fa-info-circle',
warning: 'fa-exclamation-triangle'
};
toast.innerHTML = '<div class="repo-flex repo-items-center repo-gap-2"><i class="fas ' + (icons[type] || icons.info) + '"></i><span>' + message + '</span></div>';
document.body.appendChild(toast);
setTimeout(function() {
toast.style.opacity = '0';
setTimeout(function() { if (toast.parentNode) toast.remove(); }, 300);
}, 2000);
}
// 复制到剪贴板
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
showToast('已复制到剪贴板', 'success');
}).catch(function() {
showToast('复制失败', 'error');
});
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+2312
View File
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
<!-- index.html -->
{% extends "base.html" %}
{% block content %}
<div class="repo-container repo-py-6">
<!-- 头部 -->
<div class="repo-card repo-mb-6">
<div class="repo-card__body">
<div class="repo-text-center repo-mb-6">
<h1 class="repo-text-xl repo-font-bold repo-mb-2">DVS——博客</h1>
<p class="repo-text-secondary">记录技术与生活,分享知识与感悟</p>
</div>
<!-- 作者信息 -->
<div class="repo-card repo-mb-4">
<div class="repo-card__body">
<h2 class="repo-card__title repo-mb-4">关于作者</h2>
<div class="repo-row">
<div class="repo-col">
<p class="repo-mb-2"><strong>开发者:</strong>Dvs</p>
<p class="repo-mb-2">
<strong>主页:</strong>
<a href="https://dvsyun.top/me/dvs">https://dvsyun.top/me/dvs</a>
</p>
<p>
<strong>EMAIL:</strong>
me@dvsyun.top dvs6666@163.com
</p>
</div>
<div class="repo-col repo-flex repo-items-center repo-justify-end">
<a href="/admin" class="repo-btn repo-btn--primary">
<i class="fas fa-lock"></i> 管理员登录
</a>
</div>
</div>
</div>
</div>
<!-- 项目展示 -->
<div class="repo-row">
<div class="repo-col">
<div class="repo-card">
<div class="repo-card__body">
<h3 class="repo-font-semibold repo-mb-3">ap_ds音频库</h3>
<span class="repo-tag repo-tag--primary repo-mb-2">v2.4.1</span>
<div class="repo-flex repo-flex-col repo-gap-2 repo-mt-2">
<a href="https://www.dvsyun.top/ap_ds" target="_blank" class="repo-text-link">
<i class="fas fa-globe repo-mr-2"></i>官方网站1
</a>
<a href="https://www.apds.top" target="_blank" class="repo-text-link">
<i class="fas fa-globe repo-mr-2"></i>官方网站2
</a>
<a href="https://pypi.org/project/ap_ds/" target="_blank" class="repo-text-link">
<i class="fab fa-python repo-mr-2"></i>PyPI页面
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 文章列表 -->
<div class="repo-card">
<div class="repo-card__header">
<div class="repo-flex repo-items-center repo-justify-between">
<h2 class="repo-card__title">最新文章</h2>
<div class="repo-flex repo-gap-2">
<button onclick="changeView('grid')" class="repo-btn repo-btn--ghost repo-btn--sm" id="gridBtn">
<i class="fas fa-th-large"></i>
</button>
<button onclick="changeView('list')" class="repo-btn repo-btn--ghost repo-btn--sm" id="listBtn">
<i class="fas fa-list"></i>
</button>
</div>
</div>
</div>
<div class="repo-card__body">
{% if articles %}
<div id="articlesGrid" class="repo-row" style="margin: -8px;">
{% for article in articles %}
<div class="repo-col" style="padding: 8px; flex: 0 0 33.333%; max-width: 33.333%;">
<div class="repo-card" style="height: 100%;">
<div class="repo-card__body">
<h3 class="repo-font-semibold repo-mb-3">{{ article.title }}</h3>
<div class="repo-text-sm repo-text-secondary repo-mb-4">
<div class="repo-flex repo-items-center repo-gap-3">
<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>
<div class="repo-flex repo-items-center repo-justify-between">
<a href="/docs/{{ article.filename }}" class="repo-btn repo-btn--primary repo-btn--sm">
阅读文章
</a>
<span class="repo-text-xs repo-text-tertiary">{{ loop.index }} / {{ articles|length }}</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div id="articlesList" class="repo-hidden" style="display: none;">
{% for article in articles %}
<div class="repo-card repo-mb-3">
<div class="repo-card__body">
<div class="repo-flex repo-items-center repo-justify-between">
<div>
<h3 class="repo-font-semibold repo-mb-2">{{ article.title }}</h3>
<div class="repo-text-sm repo-text-secondary">
<span class="repo-mr-3"><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>
<a href="/docs/{{ article.filename }}" class="repo-btn repo-btn--primary repo-btn--sm">
阅读全文
</a>
</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>
<h3 class="repo-font-semibold repo-mb-2">暂无文章</h3>
<p class="repo-text-secondary">管理员登录后可发布新文章</p>
</div>
{% endif %}
</div>
</div>
<!-- 页脚 -->
<div class="repo-text-center repo-mt-6">
<p class="repo-text-sm repo-text-tertiary">© 2026 DVS的博客 | Powered by Flask</p>
</div>
</div>
<script>
function changeView(view) {
var grid = document.getElementById('articlesGrid');
var list = document.getElementById('articlesList');
var gridBtn = document.getElementById('gridBtn');
var listBtn = document.getElementById('listBtn');
if (view === 'grid') {
grid.style.display = '';
list.style.display = 'none';
gridBtn.classList.add('repo-btn--primary');
gridBtn.classList.remove('repo-btn--ghost');
listBtn.classList.remove('repo-btn--primary');
listBtn.classList.add('repo-btn--ghost');
} else {
grid.style.display = 'none';
list.style.display = '';
listBtn.classList.add('repo-btn--primary');
listBtn.classList.remove('repo-btn--ghost');
gridBtn.classList.remove('repo-btn--primary');
gridBtn.classList.add('repo-btn--ghost');
}
}
document.addEventListener('DOMContentLoaded', function() {
changeView('grid');
});
</script>
{% endblock %}