Преглед изворни кода

feat: 新增Markdown编辑器,支持图片上传并插入到光标位置

Co-authored-by: aider (deepseek/deepseek-v4-pro) <aider@aider.chat>
Your Name пре 3 дана
родитељ
комит
fdbc136214
5 измењених фајлова са 207 додато и 4 уклоњено
  1. 112 1
      app.py
  2. 56 0
      static/js/editor.js
  3. 1 1
      templates/base.html
  4. 36 0
      templates/editor.html
  5. 2 2
      templates/index.html

+ 112 - 1
app.py

@@ -41,6 +41,7 @@ for dir_path in [
     config.GENERATED_FOLDER,
     config.POSTS_DATA_FOLDER,
     config.INDEX_FILE.parent,
+    config.UPLOAD_FOLDER / "editor_uploads",
 ]:
     dir_path.mkdir(parents=True, exist_ok=True)
 
@@ -82,6 +83,116 @@ def logout():
     return redirect(url_for("index"))
 
 
+# ==================== 编辑器相关路由 ====================
+
+
+@app.route("/editor", methods=["GET", "POST"])
+def editor():
+    """Markdown 编辑器页面:创建新文章(需要登录)"""
+    if "user" not in session:
+        flash("请先登录", "warning")
+        return redirect(url_for("login"))
+
+    if request.method == "GET":
+        return render_template("editor.html")
+
+    # POST:保存文章
+    title = request.form.get("title", "").strip()
+    if not title:
+        flash("标题不能为空", "danger")
+        return redirect(url_for("editor"))
+
+    md_content = request.form.get("content", "")
+    if not md_content.strip():
+        flash("内容不能为空", "danger")
+        return redirect(url_for("editor"))
+
+    # 生成唯一文章 ID
+    post_id = str(int(time.time() * 1000))
+
+    # 确保文章数据目录存在
+    posts_data_dir = config.POSTS_DATA_FOLDER / post_id
+    posts_data_dir.mkdir(parents=True, exist_ok=True)
+
+    # 保存 Markdown 文件
+    md_path = posts_data_dir / "content.md"
+    with open(md_path, "w", encoding="utf-8") as f:
+        f.write(md_content)
+
+    # 修正图片路径(允许 http、/ 开头路径保持原样)
+    fixed_md = fix_image_paths(md_content, post_id)
+
+    # 渲染 HTML
+    html_body = render_markdown(fixed_md)
+
+    # 提取摘要与缩略图
+    summary = extract_summary(html_body)
+    thumbnail = extract_thumbnail(html_body)
+
+    # 生成日期(精确到分钟)
+    date_iso = datetime.now(timezone.utc).replace(second=0, microsecond=0).isoformat()
+
+    # 生成静态页面
+    generate_static_page(post_id, title, html_body, date_iso, thumbnail)
+
+    # 更新索引
+    index = load_index()
+    index.append(
+        {
+            "id": post_id,
+            "title": title,
+            "date": date_iso,
+            "summary": summary,
+            "thumbnail": thumbnail,
+        }
+    )
+    index.sort(key=lambda p: p.get("date", ""), reverse=True)
+    save_index(index)
+
+    flash("文章发布成功", "success")
+    return redirect(url_for("index"))
+
+
+@app.route("/editor/upload-image", methods=["POST"])
+def editor_upload_image():
+    """处理编辑器中的图片上传,返回 Markdown 图片代码"""
+    if "user" not in session:
+        return jsonify({"success": False, "error": "需要登录"}), 403
+
+    if "image" not in request.files:
+        return jsonify({"success": False, "error": "没有选择图片"}), 400
+
+    file = request.files["image"]
+    if file.filename == "":
+        return jsonify({"success": False, "error": "文件名为空"}), 400
+
+    # 只允许图片类型
+    if not allowed_file(file.filename):
+        return jsonify({"success": False, "error": "不支持的文件类型"}), 400
+
+    # 保存到 editor_uploads 目录
+    editor_uploads_dir = config.UPLOAD_FOLDER / "editor_uploads"
+    editor_uploads_dir.mkdir(parents=True, exist_ok=True)
+
+    safe_name = secure_filename(file.filename)
+    timestamp = str(int(time.time() * 1000))
+    new_name = f"{timestamp}_{safe_name}"
+    file_path = editor_uploads_dir / new_name
+    file.save(str(file_path))
+
+    # 生成可访问 URL 和 Markdown 片段
+    image_url = f"/static/uploads/posts/editor_uploads/{new_name}"
+    markdown_code = f"![{safe_name}]({image_url})"
+
+    return jsonify(
+        {
+            "success": True,
+            "url": image_url,
+            "markdown": markdown_code,
+        }
+    )
+
+
 # ==================== 路由 ====================
 
 
@@ -129,7 +240,7 @@ def index():
 
 @app.route("/upload", methods=["GET", "POST"])
 def upload():
-    """上传文章(需要登录)"""
+    """上传文章(原始文件上传,需要登录)"""
     # 登录检查
     if "user" not in session:
         flash("请先登录", "warning")

+ 56 - 0
static/js/editor.js

@@ -0,0 +1,56 @@
+// editor.js - 编辑器增强
+
+document.addEventListener('DOMContentLoaded', function () {
+    const uploadBtn = document.getElementById('uploadImageBtn');
+    const fileInput = document.getElementById('imageFileInput');
+    const markdownTextarea = document.getElementById('markdownContent');
+    const statusDiv = document.getElementById('uploadStatus');
+
+    if (!uploadBtn || !fileInput || !markdownTextarea) return;
+
+    // 点击按钮触发文件选择
+    uploadBtn.addEventListener('click', function () {
+        fileInput.click();
+    });
+
+    fileInput.addEventListener('change', function () {
+        const file = fileInput.files[0];
+        if (!file) return;
+
+        // 显示上传中
+        statusDiv.textContent = '上传中...';
+        uploadBtn.disabled = true;
+
+        const formData = new FormData();
+        formData.append('image', file);
+
+        fetch('/editor/upload-image', {
+            method: 'POST',
+            body: formData
+        })
+        .then(response => response.json())
+        .then(data => {
+            if (data.success) {
+                // 插入 Markdown 到光标位置
+                const start = markdownTextarea.selectionStart;
+                const end = markdownTextarea.selectionEnd;
+                const before = markdownTextarea.value.substring(0, start);
+                const after = markdownTextarea.value.substring(end);
+                markdownTextarea.value = before + data.markdown + '\n' + after;
+                // 移动光标到插入之后
+                markdownTextarea.selectionStart = markdownTextarea.selectionEnd = start + data.markdown.length + 1;
+                markdownTextarea.focus();
+                statusDiv.textContent = '图片已插入';
+            } else {
+                statusDiv.textContent = '上传失败: ' + (data.error || '未知错误');
+            }
+        })
+        .catch(err => {
+            statusDiv.textContent = '上传错误: ' + err.message;
+        })
+        .finally(() => {
+            uploadBtn.disabled = false;
+            fileInput.value = ''; // 以便再次选择同一文件
+        });
+    });
+});

+ 1 - 1
templates/base.html

@@ -25,7 +25,7 @@
             <a href="{{ url_for('index') }}" class="nav-brand">博客</a>
             <ul class="nav-links">
                 <li><a href="{{ url_for('index') }}">首页</a></li>
-                <li><a href="{{ url_for('upload') }}" class="add-post-btn">+ 增加文章</a></li>
+                <li><a href="{{ url_for('editor') }}" class="add-post-btn">+ 增加文章</a></li>
                 <li><a href="{{ url_for('admin') }}">管理</a></li>
             </ul>
         </div>

+ 36 - 0
templates/editor.html

@@ -0,0 +1,36 @@
+{% extends "base.html" %}
+
+{% block title %}编辑文章 - 个人博客{% endblock %}
+
+{% block extra_head %}
+<style>
+    .editor-container { max-width: 800px; margin: 0 auto; }
+    .editor-title { width: 100%; padding: 0.5rem; font-size: 1.5rem; margin-bottom: 1rem; }
+    .editor-toolbar { margin-bottom: 1rem; display: flex; gap: 1rem; align-items: center; }
+    .editor-textarea { width: 100%; min-height: 400px; padding: 1rem; font-family: monospace; font-size: 1rem; resize: vertical; }
+</style>
+{% endblock %}
+
+{% block content %}
+<div class="editor-container">
+    <h1>创建新文章</h1>
+    <form method="POST" id="editorForm">
+        <div class="form-group">
+            <input type="text" name="title" class="editor-title" placeholder="文章标题" required>
+        </div>
+        <div class="editor-toolbar">
+            <button type="button" id="uploadImageBtn" class="btn">📷 插入图片</button>
+            <input type="file" id="imageFileInput" accept="image/*" style="display:none">
+        </div>
+        <div class="form-group">
+            <textarea name="content" id="markdownContent" class="editor-textarea" placeholder="在此书写 Markdown 内容..." required></textarea>
+        </div>
+        <button type="submit" class="btn btn-primary">💾 发布文章</button>
+    </form>
+    <div id="uploadStatus" style="margin-top:0.5rem;"></div>
+</div>
+{% endblock %}
+
+{% block extra_scripts %}
+<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
+{% endblock %}

+ 2 - 2
templates/index.html

@@ -4,7 +4,7 @@
 
 {% block content %}
 <div class="add-post-wrapper">
-    <a href="{{ url_for('upload') }}" class="add-post-btn">+ 添加文章</a>
+    <a href="{{ url_for('editor') }}" class="add-post-btn">+ 添加文章</a>
     {% if logged_in %}
         <a href="{{ url_for('logout') }}" class="add-post-btn">登出</a>
     {% else %}
@@ -35,7 +35,7 @@
         {% endfor %}
     {% else %}
         <div class="empty-state">
-            <p>还没有文章,快去 <a href="{{ url_for('upload') }}">上传</a> 第一篇吧!</p>
+            <p>还没有文章,快去 <a href="{{ url_for('editor') }}">编辑</a> 第一篇吧!</p>
         </div>
     {% endif %}
 </div>