2026-05-17 22:11:46 +08:00
|
|
|
|
/**
|
2026-05-17 22:44:11 +08:00
|
|
|
|
* my-editor.js — VvvebJS 儲存與新增頁面橋接腳本
|
|
|
|
|
|
* 覆蓋 Vvveb.Builder.saveAjax 行為,使儲存與新增頁面功能無縫串接至 Flask 後端
|
2026-05-17 22:11:46 +08:00
|
|
|
|
*/
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
const SLUG = window.VVVEB_PROJECT_SLUG || "";
|
|
|
|
|
|
|
|
|
|
|
|
// ── Toast 通知 ──────────────────────────────────────────────────
|
|
|
|
|
|
function showToast(msg, type) {
|
|
|
|
|
|
const existing = document.getElementById("vvveb-save-toast");
|
|
|
|
|
|
if (existing) existing.remove();
|
|
|
|
|
|
|
|
|
|
|
|
const toast = document.createElement("div");
|
|
|
|
|
|
toast.id = "vvveb-save-toast";
|
|
|
|
|
|
const bg = type === "error" ? "#7f1d1d" : "#14532d";
|
|
|
|
|
|
const border = type === "error" ? "#ef444460" : "#22c55e60";
|
|
|
|
|
|
Object.assign(toast.style, {
|
|
|
|
|
|
position: "fixed",
|
|
|
|
|
|
top: "1rem",
|
|
|
|
|
|
right: "1rem",
|
|
|
|
|
|
zIndex: "99999",
|
|
|
|
|
|
background: bg,
|
|
|
|
|
|
border: "1px solid " + border,
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
|
padding: "0.65rem 1.1rem",
|
|
|
|
|
|
fontSize: "0.85rem",
|
|
|
|
|
|
fontFamily: "Inter, system-ui, sans-serif",
|
|
|
|
|
|
fontWeight: "500",
|
|
|
|
|
|
boxShadow: "0 4px 20px rgba(0,0,0,0.5)",
|
|
|
|
|
|
opacity: "0",
|
|
|
|
|
|
transform: "translateY(-8px)",
|
|
|
|
|
|
transition: "opacity 0.3s ease, transform 0.3s ease",
|
|
|
|
|
|
pointerEvents: "none",
|
|
|
|
|
|
});
|
|
|
|
|
|
toast.textContent = msg;
|
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
toast.style.opacity = "1";
|
|
|
|
|
|
toast.style.transform = "translateY(0)";
|
|
|
|
|
|
});
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
toast.style.opacity = "0";
|
|
|
|
|
|
toast.style.transform = "translateY(-8px)";
|
|
|
|
|
|
setTimeout(() => toast.remove(), 350);
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// ── 覆蓋 Vvveb.Builder.saveAjax ──────────────────────────────────
|
2026-05-17 22:11:46 +08:00
|
|
|
|
function patchSaveAjax() {
|
|
|
|
|
|
if (typeof Vvveb === "undefined" || !Vvveb.Builder) {
|
|
|
|
|
|
// 等待 Vvveb 載入
|
|
|
|
|
|
setTimeout(patchSaveAjax, 200);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* VvvebJS 原始簽名: saveAjax(data, saveUrl, callback, error)
|
|
|
|
|
|
*/
|
|
|
|
|
|
Vvveb.Builder.saveAjax = function (data, saveUrl, callback, error) {
|
2026-05-17 22:11:46 +08:00
|
|
|
|
if (!SLUG) {
|
|
|
|
|
|
showToast("錯誤:找不到專案識別碼 (slug)", "error");
|
2026-05-17 22:44:11 +08:00
|
|
|
|
if (error) error(new Error("Missing slug"));
|
2026-05-17 22:11:46 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 確保 data 為物件並填入專案識別碼
|
|
|
|
|
|
if (typeof data !== "object" || data === null) {
|
|
|
|
|
|
data = {};
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
2026-05-17 22:44:11 +08:00
|
|
|
|
data.slug = SLUG;
|
2026-05-17 22:11:46 +08:00
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 扁平化處理新檔案檔名(確保所有頁面皆存於專案根目錄下)
|
|
|
|
|
|
if (data.file) {
|
|
|
|
|
|
const parts = data.file.split("/");
|
|
|
|
|
|
data.file = parts[parts.length - 1];
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 如果不是新增頁面(即一般的儲存頁面),且未帶有 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);
|
|
|
|
|
|
}
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 顯示儲存中狀態(一般儲存時觸發)
|
2026-05-17 22:11:46 +08:00
|
|
|
|
const saveBtn = document.querySelector(".save-btn");
|
2026-05-17 22:44:11 +08:00
|
|
|
|
if (saveBtn && !isNewPage) {
|
2026-05-17 22:11:46 +08:00
|
|
|
|
saveBtn.querySelector(".loading")?.classList.remove("d-none");
|
|
|
|
|
|
saveBtn.querySelector(".button-text")?.classList.add("d-none");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 發送 POST 請求至 Flask API
|
2026-05-17 22:11:46 +08:00
|
|
|
|
fetch("/api/save", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
2026-05-17 22:44:11 +08:00
|
|
|
|
body: new URLSearchParams(data).toString(),
|
2026-05-17 22:11:46 +08:00
|
|
|
|
})
|
2026-05-17 22:44:11 +08:00
|
|
|
|
.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
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-17 22:11:46 +08:00
|
|
|
|
} else {
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 一般儲存成功
|
|
|
|
|
|
showToast("✓ 已儲存:" + (resData.saved || data.file));
|
|
|
|
|
|
if (callback) callback(resData);
|
|
|
|
|
|
// 停用儲存按鈕(直至下一次變更)
|
|
|
|
|
|
document.querySelectorAll("#top-panel .save-btn").forEach((e) =>
|
|
|
|
|
|
e.setAttribute("disabled", "true")
|
|
|
|
|
|
);
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch((err) => {
|
2026-05-17 22:44:11 +08:00
|
|
|
|
showToast("儲存失敗:" + err.message, "error");
|
|
|
|
|
|
if (error) error(err);
|
2026-05-17 22:11:46 +08:00
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
2026-05-17 22:44:11 +08:00
|
|
|
|
if (saveBtn && !isNewPage) {
|
2026-05-17 22:11:46 +08:00
|
|
|
|
saveBtn.querySelector(".loading")?.classList.add("d-none");
|
|
|
|
|
|
saveBtn.querySelector(".button-text")?.classList.remove("d-none");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-17 22:44:11 +08:00
|
|
|
|
console.log("[my-editor] Robust Vvveb.Builder.saveAjax patched for slug:", SLUG);
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 啟用 Save 按鈕(vvvebjs 預設 disabled)──────────────────────
|
|
|
|
|
|
function enableSaveBtn() {
|
|
|
|
|
|
const btn = document.querySelector(".save-btn");
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
|
btn.removeAttribute("disabled");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setTimeout(enableSaveBtn, 300);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Ctrl+S 快捷鍵 ────────────────────────────────────────────────
|
|
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (typeof Vvveb !== "undefined" && Vvveb.Builder && Vvveb.Builder.saveAjax) {
|
2026-05-17 22:44:11 +08:00
|
|
|
|
// 觸發原生儲存按鈕的 click
|
|
|
|
|
|
const btn = document.querySelector(".save-btn");
|
|
|
|
|
|
if (btn) {
|
|
|
|
|
|
btn.click();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Vvveb.Builder.saveAjax({});
|
|
|
|
|
|
}
|
2026-05-17 22:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 初始化 ────────────────────────────────────────────────────────
|
|
|
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
|
|
patchSaveAjax();
|
|
|
|
|
|
enableSaveBtn();
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
patchSaveAjax();
|
|
|
|
|
|
enableSaveBtn();
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|