From 18fc9849a334580660b9ea3726a3f6241e72320e Mon Sep 17 00:00:00 2001 From: nudoragon Date: Sun, 17 May 2026 22:44:11 +0800 Subject: [PATCH] Add Page --- main.py | 50 +++++++++++-- static/js/my-editor.js | 116 +++++++++++++++++------------- websites/my-website/about-us.html | 33 +++++++++ websites/my-website/index.html | 28 +++++++- websites/my-website/my-page.html | 33 +++++++++ 5 files changed, 203 insertions(+), 57 deletions(-) create mode 100644 websites/my-website/about-us.html create mode 100644 websites/my-website/my-page.html diff --git a/main.py b/main.py index 13394e5..6c95d14 100644 --- a/main.py +++ b/main.py @@ -254,7 +254,7 @@ def api_create_page(slug: str) -> tuple[Response, int]: @app.route("/api/save", methods=["POST"]) # type: ignore[untyped-decorator] def api_save() -> tuple[Response, int] | Response: - """接受 vvvebjs 的儲存請求,寫入對應專案目錄.""" + """接受 vvvebjs 的儲存或新增頁面請求,寫入對應專案目錄.""" if request.is_json: raw: dict[str, Any] = request.get_json(force=True) or {} body: dict[str, Any] = raw @@ -263,15 +263,52 @@ def api_save() -> tuple[Response, int] | Response: body = {k: (v[0] if isinstance(v, list) else v) for k, v in form.items()} slug: str = str(body.get("slug", "")).strip() - filename: str = str(body.get("file", "")).strip() - html: str = str(body.get("html", "")).strip() - - if not slug or not filename or not html: - return jsonify({"error": "缺少必要參數 slug / file / html"}), 400 + if not slug: + return jsonify({"error": "缺少專案識別碼 (slug)"}), 400 if not _project_dir(slug).exists(): return jsonify({"error": "專案不存在"}), 404 + # 1. 判斷是否為新增頁面請求 (含有 startTemplateUrl) + start_template_url = str(body.get("startTemplateUrl", "")).strip() + if start_template_url: + title = str(body.get("title", "")).strip() or "New Page" + # 取得安全的檔名 (只取檔名部分,例如 'about.html') + filename = Path(str(body.get("file", "untitled.html")).strip()).name + if not filename.endswith(".html"): + filename += ".html" + + safe_path = _sanitize_file_path(slug, filename) + if safe_path is None: + return jsonify({"error": "不合法的頁面名稱"}), 400 + + if safe_path.exists(): + return jsonify({"error": "頁面已存在"}), 409 + + # 解析並複製樣板 (定位在 static/Vvvebjs 目錄下) + template_source = BASE_DIR / "static" / "Vvvebjs" / start_template_url + if template_source.exists() and template_source.is_file(): + shutil.copy(template_source, safe_path) + else: + _copy_blank_template(safe_path) + + # 回傳 VvvebJS FileManager 所期待的 JSON 格式 + page_name = safe_path.stem + return jsonify({ + "ok": True, + "name": page_name, + "title": title, + "file": safe_path.name, + "url": f"/sites/{slug}/{safe_path.name}" + }) + + # 2. 一般儲存頁面請求 + filename = str(body.get("file", "")).strip() + html: str = str(body.get("html", "")).strip() + + if not filename or not html: + return jsonify({"error": "缺少必要參數 file / html"}), 400 + safe_path = _sanitize_file_path(slug, filename) if safe_path is None: return jsonify({"error": "不合法的檔案路徑"}), 400 @@ -280,6 +317,7 @@ def api_save() -> tuple[Response, int] | Response: return jsonify({"ok": True, "saved": safe_path.name}) + @app.route("/sites//") # type: ignore[untyped-decorator] def serve_site_file(slug: str, filename: str) -> Response: proj_dir = _project_dir(slug) diff --git a/static/js/my-editor.js b/static/js/my-editor.js index c2fcf3f..49fc709 100644 --- a/static/js/my-editor.js +++ b/static/js/my-editor.js @@ -1,6 +1,6 @@ /** - * my-editor.js — VvvebJS 儲存橋接腳本 - * 覆蓋 vvvebjs 的 saveAjax 行為,附加 project slug 後送至 Flask /api/save + * my-editor.js — VvvebJS 儲存與新增頁面橋接腳本 + * 覆蓋 Vvveb.Builder.saveAjax 行為,使儲存與新增頁面功能無縫串接至 Flask 後端 */ (function () { "use strict"; @@ -49,7 +49,7 @@ }, 3000); } - // ── 覆蓋 saveAjax ─────────────────────────────────────────────── + // ── 覆蓋 Vvveb.Builder.saveAjax ────────────────────────────────── function patchSaveAjax() { if (typeof Vvveb === "undefined" || !Vvveb.Builder) { // 等待 Vvveb 載入 @@ -57,82 +57,94 @@ return; } - const originalSaveAjax = Vvveb.Builder.saveAjax - ? Vvveb.Builder.saveAjax.bind(Vvveb.Builder) - : null; - - Vvveb.Builder.saveAjax = function (saveUrl) { + /** + * VvvebJS 原始簽名: saveAjax(data, saveUrl, callback, error) + */ + Vvveb.Builder.saveAjax = function (data, saveUrl, callback, error) { if (!SLUG) { showToast("錯誤:找不到專案識別碼 (slug)", "error"); + if (error) error(new Error("Missing slug")); return; } - // 取得目前編輯的頁面檔名 - const currentPage = Vvveb.FileManager - ? Vvveb.FileManager.getCurrentPage - ? Vvveb.FileManager.getCurrentPage() - : null - : null; + // 確保 data 為物件並填入專案識別碼 + if (typeof data !== "object" || data === null) { + data = {}; + } + data.slug = SLUG; - let filename = "index.html"; - if (currentPage && currentPage.filename) { - filename = currentPage.filename; - } else if (currentPage && currentPage.file) { - // 只取檔名部分 - filename = currentPage.file.split("/").pop(); + // 扁平化處理新檔案檔名(確保所有頁面皆存於專案根目錄下) + if (data.file) { + const parts = data.file.split("/"); + data.file = parts[parts.length - 1]; } - // 取得 HTML 內容 - let html = ""; - try { - html = Vvveb.Builder.getHtml ? Vvveb.Builder.getHtml() : ""; - } catch (e) { - console.error("[my-editor] getHtml failed:", e); + // 如果不是新增頁面(即一般的儲存頁面),且未帶有 HTML 內容,則動態獲取目前 HTML + const isNewPage = !!data.startTemplateUrl; + if (!isNewPage && !data.html) { + try { + data.html = Vvveb.Builder.getHtml ? Vvveb.Builder.getHtml() : ""; + } catch (e) { + console.error("[my-editor] getHtml failed:", e); + } } - if (!html) { - showToast("無法取得頁面內容", "error"); - return; - } - - // 顯示儲存中狀態 + // 顯示儲存中狀態(一般儲存時觸發) const saveBtn = document.querySelector(".save-btn"); - if (saveBtn) { + if (saveBtn && !isNewPage) { saveBtn.querySelector(".loading")?.classList.remove("d-none"); saveBtn.querySelector(".button-text")?.classList.add("d-none"); } - const body = new URLSearchParams({ - slug: SLUG, - file: filename, - html: html, - }); - + // 發送 POST 請求至 Flask API fetch("/api/save", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: body.toString(), + body: new URLSearchParams(data).toString(), }) - .then((res) => res.json()) - .then((data) => { - if (data.ok) { - showToast("✓ 已儲存:" + (data.saved || filename)); + .then((res) => { + if (!res.ok) { + return res.json().then((errData) => { + throw new Error(errData.error || "伺服器儲存錯誤"); + }); + } + return res.json(); + }) + .then((resData) => { + if (isNewPage) { + // 新增頁面成功:秀出通知並執行 Vvveb 原生 callback 以載入新頁面 + showToast("✓ 成功建立新頁面:" + resData.file); + if (callback) { + callback({ + name: resData.name, + title: resData.title, + file: resData.file, + url: resData.url + }); + } } else { - showToast("儲存失敗:" + (data.error || "未知錯誤"), "error"); + // 一般儲存成功 + showToast("✓ 已儲存:" + (resData.saved || data.file)); + if (callback) callback(resData); + // 停用儲存按鈕(直至下一次變更) + document.querySelectorAll("#top-panel .save-btn").forEach((e) => + e.setAttribute("disabled", "true") + ); } }) .catch((err) => { - showToast("網路錯誤:" + err.message, "error"); + showToast("儲存失敗:" + err.message, "error"); + if (error) error(err); }) .finally(() => { - if (saveBtn) { + if (saveBtn && !isNewPage) { saveBtn.querySelector(".loading")?.classList.add("d-none"); saveBtn.querySelector(".button-text")?.classList.remove("d-none"); } }); }; - console.log("[my-editor] saveAjax patched for slug:", SLUG); + console.log("[my-editor] Robust Vvveb.Builder.saveAjax patched for slug:", SLUG); } // ── 啟用 Save 按鈕(vvvebjs 預設 disabled)────────────────────── @@ -150,7 +162,13 @@ if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); if (typeof Vvveb !== "undefined" && Vvveb.Builder && Vvveb.Builder.saveAjax) { - Vvveb.Builder.saveAjax("/api/save"); + // 觸發原生儲存按鈕的 click + const btn = document.querySelector(".save-btn"); + if (btn) { + btn.click(); + } else { + Vvveb.Builder.saveAjax({}); + } } } }); diff --git a/websites/my-website/about-us.html b/websites/my-website/about-us.html new file mode 100644 index 0000000..968063a --- /dev/null +++ b/websites/my-website/about-us.html @@ -0,0 +1,33 @@ + + + + + + + My page + + + + + + + + + +
+
+
+

Bootstrap 5 start page

+

Start by dragging components to page or double click to edit text

+
+
+
+ + + \ No newline at end of file diff --git a/websites/my-website/index.html b/websites/my-website/index.html index 968063a..09ddd9c 100644 --- a/websites/my-website/index.html +++ b/websites/my-website/index.html @@ -1,5 +1,5 @@ - + @@ -22,7 +22,31 @@
-
+

Bootstrap 5 start page

Start by dragging components to page or double click to edit text

diff --git a/websites/my-website/my-page.html b/websites/my-website/my-page.html new file mode 100644 index 0000000..8821b1c --- /dev/null +++ b/websites/my-website/my-page.html @@ -0,0 +1,33 @@ + + + + + + + + My page + + + + + + + + + +
+
+
+

Bootstrap 5 start page

+

Start by dragging components to page or double click to edit text

+
+
+
+ +