Initial commit: DVS Blog v1.0.0
This commit is contained in:
+20
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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')
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 %}
|
||||||
Reference in New Issue
Block a user