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
+20
View File
@@ -0,0 +1,20 @@
# Python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
venv/
env/
# Blog data
docs/
uploads/
articles.json
*.md
# System files
Thumbs.db
.DS_Store
desktop.ini
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 DVS (dvs-dvsxt)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+233
View File
@@ -0,0 +1,233 @@
import os
import hashlib
import secrets
from datetime import datetime
from flask import Flask, render_template, request, redirect, url_for, send_from_directory, session, abort, jsonify
from werkzeug.utils import secure_filename
import markdown
import json
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB
app.config['ARTICLES_FOLDER'] = 'docs'
app.config['ALLOWED_EXTENSIONS'] = {
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp',
'mp4', 'webm', 'ogg', 'mov',
'mp3', 'wav', 'ogg', 'm4a',
'pdf', 'doc', 'docx', 'txt', 'md',
'zip', 'rar', '7z'
}
# 确保文件夹存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['ARTICLES_FOLDER'], exist_ok=True)
# 管理员密码的SHA256哈希
ADMIN_PASSWORD_HASH = '7ea8aa746eaaa6dfbe7f1cde97c0ecca9afa204d071c487fb4f7debb5fcd301e'
def check_password(password):
"""检查密码是否正确"""
return hashlib.sha256(password.encode()).hexdigest() == ADMIN_PASSWORD_HASH
def allowed_file(filename):
"""检查文件扩展名是否允许"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
def get_articles():
"""获取所有文章信息"""
articles = []
if os.path.exists('articles.json'):
with open('articles.json', 'r', encoding='utf-8-sig') as f:
articles = json.load(f)
return articles
def save_articles(articles):
"""保存文章信息"""
with open('articles.json', 'w', encoding='utf-8') as f:
json.dump(articles, f, ensure_ascii=False, indent=2)
def generate_random_filename():
"""生成随机文件名"""
return secrets.token_urlsafe(16)
# 提供CSS文件
@app.route('/css.css')
def serve_css():
"""提供css.css样式文件"""
return send_from_directory('templates', 'css.css')
# 首页
@app.route('/')
def index():
"""首页显示所有文章"""
articles = get_articles()
articles.sort(key=lambda x: x.get('created_at', ''), reverse=True)
return render_template('index.html', articles=articles)
@app.route('/docs/<path:filename>')
def show_article(filename):
"""显示文章内容"""
article_path = os.path.join(app.config['ARTICLES_FOLDER'], filename)
if not os.path.exists(article_path):
abort(404)
articles = get_articles()
article_info = None
for article in articles:
if article.get('filename') == filename:
article_info = article
break
if not article_info:
abort(404)
with open(article_path, 'r', encoding='utf-8') as f:
content = f.read()
html_content = markdown.markdown(content, extensions=[
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.tables',
'markdown.extensions.toc'
])
return render_template('article.html',
content=html_content,
title=article_info.get('title', '文章'),
created_at=article_info.get('created_at', ''),
author=article_info.get('author', '管理员'))
@app.route('/admin', methods=['GET', 'POST'])
def admin_login():
"""管理员登录"""
if request.method == 'POST':
password = request.form.get('password', '')
if check_password(password):
session['admin_logged_in'] = True
return redirect(url_for('admin_dashboard'))
else:
return render_template('admin_login.html', error='密码错误')
return render_template('admin_login.html')
@app.route('/admin/dashboard')
def admin_dashboard():
"""管理员仪表板"""
if not session.get('admin_logged_in'):
return redirect(url_for('admin_login'))
articles = get_articles()
return render_template('admin.html', articles=articles)
@app.route('/uploads/<path:filename>')
def uploaded_file(filename):
"""提供上传的文件"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
@app.route('/admin/logout')
def admin_logout():
"""退出登录"""
session.pop('admin_logged_in', None)
return redirect(url_for('index'))
@app.route('/admin/create_article', methods=['POST'])
def create_article():
"""创建新文章"""
if not session.get('admin_logged_in'):
return jsonify({'error': '未登录'}), 401
title = request.form.get('title', '').strip()
content = request.form.get('content', '').strip()
if not title or not content:
return jsonify({'error': '标题和内容不能为空'}), 400
# 生成随机文件名
filename = generate_random_filename() + '.md'
article_path = os.path.join(app.config['ARTICLES_FOLDER'], filename)
# 保存文章内容
with open(article_path, 'w', encoding='utf-8') as f:
f.write(content)
# 保存文章信息
articles = get_articles()
articles.append({
'id': len(articles) + 1,
'title': title,
'filename': filename,
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'author': 'Dvs (DvsXT)'
})
save_articles(articles)
return jsonify({
'success': True,
'filename': filename,
'message': '文章创建成功'
})
@app.route('/admin/upload_file', methods=['POST'])
def upload_file():
"""上传文件"""
if not session.get('admin_logged_in'):
return jsonify({'error': '未登录'}), 401
if 'file' not in request.files:
return jsonify({'error': '没有文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# 添加时间戳避免重名
name, ext = os.path.splitext(filename)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"{name}_{timestamp}{ext}"
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# 返回文件的URL
file_url = url_for('uploaded_file', filename=filename, _external=True)
return jsonify({
'success': True,
'url': file_url,
'filename': filename
})
return jsonify({'error': '不支持的文件类型'}), 400
@app.route('/admin/delete_article', methods=['POST'])
def delete_article():
"""删除文章"""
if not session.get('admin_logged_in'):
return jsonify({'error': '未登录'}), 401
filename = request.form.get('filename', '').strip()
if not filename:
return jsonify({'error': '文件名不能为空'}), 400
# 删除文章文件
article_path = os.path.join(app.config['ARTICLES_FOLDER'], filename)
if os.path.exists(article_path):
os.remove(article_path)
# 从文章中移除
articles = get_articles()
articles = [article for article in articles if article.get('filename') != filename]
save_articles(articles)
return jsonify({'success': True, 'message': '文章删除成功'})
if __name__ == '__main__':
# 初始化articles.json文件
if not os.path.exists('articles.json'):
save_articles([])
app.run(debug=False, port=50, host='0.0.0.0')
+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 %}