From 03d7eca1397c3024bfde9dd19a4e5d805282329c Mon Sep 17 00:00:00 2001 From: nudoragon Date: Tue, 26 May 2026 11:53:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/main.cpython-313.pyc | Bin 0 -> 28936 bytes main.py | 129 +++++++++++++++++ pyproject.toml | 6 + templates/dashboard.html | 133 ++++++++++++++++++ templates/editor.html | 18 +++ .../__pycache__/media_manager.cpython-313.pyc | Bin 0 -> 4719 bytes utils/media_manager.py | 105 ++++++++++++++ 7 files changed, 391 insertions(+) create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 utils/__pycache__/media_manager.cpython-313.pyc create mode 100644 utils/media_manager.py diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1585d1426ef45be47d9ffa6304c179db16c366c5 GIT binary patch literal 28936 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 literal 0 HcmV?d00001 diff --git a/main.py b/main.py index 4a4f092..f4d1383 100644 --- a/main.py +++ b/main.py @@ -8,8 +8,13 @@ import shutil from datetime import datetime, timezone from pathlib import Path from typing import Any +import os +import uuid from flask import Flask, Response, abort, jsonify, render_template, request, send_from_directory +from werkzeug.utils import secure_filename + +from utils.media_manager import save_upload, list_media, delete_media app = Flask(__name__) @@ -132,6 +137,43 @@ def _copy_blank_template(dest: Path) -> None: ) +def build_page_tree(slug: str, max_depth: int = 3) -> dict[str, Any]: + """遞歸建構專案的頁面樹狀結構(根據資料夾結構)。""" + proj_dir = _project_dir(slug) + + def scan_folder(folder: Path, current_depth: int = 0): + items: list[dict[str, Any]] = [] + if current_depth > max_depth: + return items + try: + names = sorted([p.name for p in folder.iterdir()]) + except Exception: + return items + + for name in names: + if name.startswith("."): + continue + full = folder / name + if full.is_dir(): + children = scan_folder(full, current_depth + 1) + items.append({ + "type": "folder", + "name": name, + "title": name.replace("-", " ").title(), + "children": children, + }) + elif full.is_file() and full.suffix.lower() == ".html": + rel = str(full.relative_to(proj_dir)).replace("\\", "/") + items.append({ + "type": "file", + "name": rel, + "title": Path(name).stem.replace("-", " ").title(), + }) + return items + + return {"root": scan_folder(proj_dir, 0)} + + # ── 路由:一般頁面 ────────────────────────────────────────────────────────── @app.route("/") # type: ignore[untyped-decorator] @@ -195,6 +237,40 @@ def api_list_projects() -> Response: return jsonify(projects) +@app.route("/api/projects//settings", methods=["GET"]) # type: ignore[untyped-decorator] +def api_get_settings(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + data = _load_project(slug) + settings = data.get("settings", {}) + # provide sensible defaults + defaults = { + "title": data.get("name", slug), + "description": data.get("description", ""), + "logo_url": None, + "favicon_url": None, + "primary_color": "#007bff", + "secondary_color": "#6c757d", + } + merged = {**defaults, **settings} + return jsonify(merged) + + +@app.route("/api/projects//settings", methods=["PUT"]) # type: ignore[untyped-decorator] +def api_put_settings(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + body: dict[str, Any] = request.get_json(force=True) or {} + data = _load_project(slug) + settings = data.get("settings", {}) + settings.update(body) + data["settings"] = settings + data["updated_at"] = _now_iso() + _save_project(slug, data) + return jsonify({"ok": True, "settings": settings}) + + + @app.route("/api/projects", methods=["POST"]) # type: ignore[untyped-decorator] def api_create_project() -> tuple[Response, int]: body: dict[str, Any] = request.get_json(force=True) or {} @@ -261,6 +337,59 @@ def api_list_pages(slug: str) -> tuple[Response, int] | Response: return jsonify(_list_pages(slug)) +@app.route("/api/projects//pages-tree", methods=["GET"]) # type: ignore[untyped-decorator] +def api_pages_tree(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + tree = build_page_tree(slug, max_depth=5) + return jsonify(tree) + + +@app.route("/api/projects//media/upload", methods=["POST"]) # type: ignore[untyped-decorator] +def api_media_upload(slug: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + if "file" not in request.files: + return jsonify({"error": "缺少檔案 (file)"}), 400 + f = request.files["file"] + if f.filename == "": + return jsonify({"error": "檔名為空"}), 400 + # basic validation + filename = secure_filename(f.filename) + meta = save_upload(proj_dir, f) + # substitute slug in returned urls + if isinstance(meta.get("url"), str): + meta["url"] = meta["url"].replace("{slug}", slug) + if isinstance(meta.get("thumb"), str): + meta["thumb"] = meta["thumb"].replace("{slug}", slug) + return jsonify({"ok": True, "file": meta}) + + +@app.route("/api/projects//media/list", methods=["GET"]) # type: ignore[untyped-decorator] +def api_media_list(slug: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + items = list_media(proj_dir) + # patch urls + for it in items: + if "filename" in it and "date" in it: + it["url"] = f"/sites/{slug}/media/images/{it['date']}/{it['filename']}" + return jsonify(items) + + +@app.route("/api/projects//media/", methods=["DELETE"]) # type: ignore[untyped-decorator] +def api_media_delete(slug: str, rel_path: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + ok = delete_media(proj_dir, rel_path) + if not ok: + return jsonify({"error": "刪除失敗或檔案不存在"}), 400 + return jsonify({"ok": True}) + + @app.route("/api/projects//pages", methods=["POST"]) # type: ignore[untyped-decorator] def api_create_page(slug: str) -> tuple[Response, int]: if not _project_dir(slug).exists(): diff --git a/pyproject.toml b/pyproject.toml index aad68eb..857afac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,10 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "flask>=3.1", +] + +[tool.pip] +# Image processing for media manager +dependencies = [ + "Pillow>=10.0" ] \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index acc3d10..8bdc7df 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,3 +1,136 @@ + + + + + + 管理器 - Dashboard + + + + +
+

網站管理器

+
+ +
+
概覽
+
網站設定
+
頁面管理
+
媒體庫
+
+ +
+
選擇一個專案在右側編輯。
+ + + + + + +
+ + + + diff --git a/templates/editor.html b/templates/editor.html index f4a09b3..742f65a 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -108,6 +108,9 @@ + + +