資料夾功能修正

This commit is contained in:
2026-05-18 14:26:46 +08:00
parent 7acef614cc
commit f41e39846d
10 changed files with 1982 additions and 57 deletions

View File

@@ -332,6 +332,116 @@
window.addEventListener("keydown", keyHandler);
}
// ── 三欄位頁面表單對話框 (頁面名稱 / 檔案名稱 / 資料夾) ────────────
function showModalPageForm(actionTitle, defaultTitle, defaultFile, defaultFolder, callback) {
const existing = document.getElementById("vvveb-custom-modal");
if (existing) existing.remove();
const overlay = document.createElement("div");
overlay.id = "vvveb-custom-modal";
Object.assign(overlay.style, {
position: "fixed", top: "0", left: "0", width: "100%", height: "100%",
zIndex: "100000", background: "rgba(10,11,18,0.65)",
backdropFilter: "blur(12px)", display: "flex",
alignItems: "center", justifyContent: "center",
opacity: "0", transition: "opacity 0.25s ease-out"
});
const card = document.createElement("div");
Object.assign(card.style, {
background: "rgba(22,24,38,0.97)",
border: "1px solid rgba(255,255,255,0.08)",
boxShadow: "0 20px 50px rgba(0,0,0,0.6), 0 0 40px rgba(99,102,241,0.1)",
borderRadius: "16px", width: "460px", padding: "2rem",
color: "#f8fafc", fontFamily: "Inter, system-ui, sans-serif",
transform: "scale(0.95)",
transition: "transform 0.25s cubic-bezier(0.34,1.56,0.64,1)",
display: "flex", flexDirection: "column", gap: "1.1rem"
});
const fieldStyle = `
width:100%; background:rgba(13,14,24,0.8);
border:1px solid rgba(255,255,255,0.1); border-radius:8px;
padding:0.6rem 0.85rem; color:#fff; font-size:0.88rem;
outline:none; box-sizing:border-box; transition:border-color 0.2s, box-shadow 0.2s;`;
const labelStyle = `display:block; font-size:0.78rem; color:#94a3b8; margin-bottom:0.3rem; font-weight:500;`;
card.innerHTML = `
<div style="font-size:1.1rem; font-weight:600; color:#fff; display:flex; align-items:center; gap:0.55rem;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#818cf8" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
${actionTitle}
</div>
<div>
<label style="${labelStyle}">頁面名稱(顯示用)</label>
<input id="pf-title" type="text" style="${fieldStyle}" value="${defaultTitle}" placeholder="我的頁面" />
</div>
<div>
<label style="${labelStyle}">檔案名稱(.html 副檔名可省略)</label>
<input id="pf-file" type="text" style="${fieldStyle}" value="${defaultFile}" placeholder="my-page" />
</div>
<div>
<label style="${labelStyle}">儲存至資料夾(留空則存放於根目錄)</label>
<input id="pf-folder" type="text" style="${fieldStyle}" value="${defaultFolder}" placeholder="留空 = 根目錄" />
</div>
<div style="display:flex; justify-content:flex-end; gap:0.75rem; margin-top:0.4rem;">
<button id="pf-cancel" style="background:transparent; border:1px solid rgba(255,255,255,0.1); border-radius:8px; padding:0.5rem 1rem; color:#94a3b8; font-size:0.85rem; cursor:pointer; transition:background 0.2s;">取消</button>
<button id="pf-confirm" style="background:#4f46e5; border:none; border-radius:8px; padding:0.5rem 1.2rem; color:#fff; font-size:0.85rem; font-weight:600; cursor:pointer; box-shadow:0 4px 12px rgba(79,70,229,0.3); transition:background 0.2s;">確定</button>
</div>`;
overlay.appendChild(card);
document.body.appendChild(overlay);
const titleInput = card.querySelector("#pf-title");
const fileInput = card.querySelector("#pf-file");
const folderInput = card.querySelector("#pf-folder");
const cancelBtn = card.querySelector("#pf-cancel");
const confirmBtn = card.querySelector("#pf-confirm");
// Focus style
[titleInput, fileInput, folderInput].forEach(inp => {
inp.addEventListener("focus", () => { inp.style.borderColor = "#818cf8"; inp.style.boxShadow = "0 0 10px rgba(129,140,248,0.2)"; });
inp.addEventListener("blur", () => { inp.style.borderColor = "rgba(255,255,255,0.1)"; inp.style.boxShadow = "none"; });
});
cancelBtn.addEventListener("mouseover", () => { cancelBtn.style.background = "rgba(255,255,255,0.06)"; });
cancelBtn.addEventListener("mouseout", () => { cancelBtn.style.background = "transparent"; });
confirmBtn.addEventListener("mouseover", () => { confirmBtn.style.background = "#4338ca"; });
confirmBtn.addEventListener("mouseout", () => { confirmBtn.style.background = "#4f46e5"; });
// Auto-fill filename from title
titleInput.addEventListener("input", () => {
const slug = titleInput.value.trim().toLowerCase()
.replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
if (slug) fileInput.value = slug;
});
setTimeout(() => { overlay.style.opacity = "1"; card.style.transform = "scale(1)"; }, 10);
titleInput.focus();
titleInput.select();
function close(confirmed) {
overlay.style.opacity = "0";
card.style.transform = "scale(0.95)";
setTimeout(() => {
overlay.remove();
if (confirmed) {
callback({ title: titleInput.value.trim(), filename: fileInput.value.trim(), folder: folderInput.value.trim() });
} else {
callback(null);
}
}, 220);
}
cancelBtn.onclick = () => close(false);
confirmBtn.onclick = () => close(true);
fileInput.onkeydown = titleInput.onkeydown = folderInput.onkeydown = (e) => {
if (e.key === "Enter") close(true);
if (e.key === "Escape") close(false);
};
}
// ── 覆蓋 Vvveb.Builder.saveAjax ──────────────────────────────────
function patchSaveAjax() {
if (typeof Vvveb === "undefined" || !Vvveb.Builder) {
@@ -501,21 +611,25 @@
});
};
// 覆蓋重新命名 / 複製頁面
// 覆蓋重新命名 / 複製頁面(三欄位對話框:頁面名稱、檔案名稱、儲存至資料夾)
Vvveb.FileManager.renamePage = function (element, e, duplicate = false) {
let page = element.dataset;
showModalPrompt(`請輸入 "${page.file}" 的新檔名:`, page.file, function (newfile) {
if (!newfile) return;
const currentTitle = element.querySelector("label > span")?.textContent?.trim() || page.file.replace(".html","");
const currentFile = page.file.replace(".html","");
const action = duplicate ? "複製頁面" : "重新命名頁面";
// 確保副檔名為 .html
if (!newfile.endsWith(".html")) {
newfile += ".html";
}
showModalPageForm(action, currentTitle, currentFile, "", function (formData) {
if (!formData) return;
let { title, filename, folder } = formData;
if (!filename) return;
if (!filename.endsWith(".html")) filename += ".html";
const bodyData = {
slug: SLUG,
file: page.file,
newfile: newfile,
newfile: filename,
title: title,
folder: folder,
duplicate: duplicate ? "true" : "false"
};
@@ -534,40 +648,32 @@
})
.then((data) => {
showToast(`${data.message}`);
let baseName = data.newfile.replace('.html', '');
let newName = friendlyName(data.newfile.replace(/.*[\/\\]+/, '')).replace('.html', '');
const baseName = data.name || data.newfile.replace('.html', '');
const newTitle = data.title || title || baseName;
if (duplicate) {
// 複製頁面:在 FileManager 中加入新頁面
let pageData = Object.assign({}, Vvveb.FileManager.pages[page.page]);
pageData["file"] = data.newfile;
pageData["title"] = newName;
pageData["title"] = newTitle;
pageData["url"] = data.url;
pageData["name"] = baseName;
Vvveb.FileManager.addPage(baseName, pageData);
} else {
// 重新命名:更新現有節點資訊
const oldPageKey = page.page;
Vvveb.FileManager.pages[oldPageKey]["file"] = data.newfile;
Vvveb.FileManager.pages[oldPageKey]["title"] = newName;
Vvveb.FileManager.pages[oldPageKey]["title"] = newTitle;
Vvveb.FileManager.pages[oldPageKey]["url"] = data.url;
Vvveb.FileManager.pages[oldPageKey]["name"] = baseName;
let link = element.querySelector("a.view");
if (link) {
link.setAttribute("href", data.url);
}
let span = element.querySelector("label > span");
if (!span) {
span = element.querySelector("span");
}
if (span) {
span.textContent = newName;
}
if (link) link.setAttribute("href", data.url);
let span = element.querySelector("label > span") || element.querySelector("span");
if (span) span.textContent = newTitle;
element.dataset.file = data.newfile;
element.dataset.page = baseName;
// 將 key 重新綁定
Vvveb.FileManager.pages[baseName] = Vvveb.FileManager.pages[oldPageKey];
if (baseName !== oldPageKey) {
delete Vvveb.FileManager.pages[oldPageKey];
@@ -666,6 +772,129 @@
console.log("[my-editor] Vvveb.NewSection.insert patched to prevent layout crashes.");
}
// ── 頁面清單樹狀結構 ────────────────────────────────────────────────
function patchPageTree() {
if (typeof Vvveb === "undefined" || !Vvveb.FileManager) {
setTimeout(patchPageTree, 300);
return;
}
// 攔截 addPages — 批次新增後重組樹狀結構
const origAddPages = Vvveb.FileManager.addPages;
if (origAddPages) {
Vvveb.FileManager.addPages = function (pages) {
origAddPages.call(this, pages);
setTimeout(buildPageTree, 80);
};
}
// 攔截 addPage — 單筆新增Rename/Duplicate後重組
const origAddPage = Vvveb.FileManager.addPage;
if (origAddPage) {
Vvveb.FileManager.addPage = function (name, page, ...rest) {
const result = origAddPage.call(this, name, page, ...rest);
setTimeout(buildPageTree, 80);
return result;
};
}
console.log("[my-editor] Page tree patch ready.");
}
function buildPageTree() {
// 找到頁面清單容器(支援多種可能的 selector
const list = (
document.querySelector("#file-manager .files") ||
document.querySelector(".file-manager .files") ||
document.querySelector("#file-manager ul") ||
(() => {
const li = document.querySelector("li[data-file]");
return li ? li.closest("ul") : null;
})()
);
if (!list) return;
// 取得所有頁面 li不包含我們建立的 folder-node
const allPageItems = [...list.querySelectorAll("li[data-file]:not(.folder-node-item)")];
if (allPageItems.length === 0) return;
// 移除舊的資料夾節點(避免重複)
list.querySelectorAll(".folder-node").forEach(n => n.remove());
// 分組:根頁面 vs 子資料夾頁面
const rootItems = [];
const folderMap = {}; // folderName -> [li]
allPageItems.forEach(li => {
const pageKey = li.dataset.page || "";
if (pageKey.includes("/")) {
const folderName = pageKey.split("/").slice(0, -1).join("/");
if (!folderMap[folderName]) folderMap[folderName] = [];
folderMap[folderName].push(li);
li.remove(); // 先從扁平清單移除,稍後放進資料夾節點
} else {
rootItems.push(li);
}
});
// 每個資料夾建一個折疊節點並加到清單尾端
for (const folderName in folderMap) {
const folderNode = createFolderNode(folderName, folderMap[folderName]);
list.appendChild(folderNode);
}
}
function createFolderNode(folderPath, childItems) {
const displayName = folderPath.split("/").pop()
.replace(/-/g, " ")
.replace(/\b\w/g, c => c.toUpperCase());
const li = document.createElement("li");
li.className = "folder-node";
li.dataset.folder = folderPath;
li.style.cssText = "list-style:none;";
li.innerHTML = `
<div class="folder-header" style="
display:flex; align-items:center; gap:0.45rem;
padding:0.35rem 0.6rem; cursor:pointer; border-radius:6px;
user-select:none; color:inherit;
transition:background 0.15s;">
<svg class="chevron" width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
style="transition:transform 0.2s; flex-shrink:0; opacity:0.6;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
<svg width="15" height="15" viewBox="0 0 24 24" fill="#6366f1" style="flex-shrink:0;">
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
</svg>
<span style="font-weight:500;">${displayName}</span>
</div>
<ul class="folder-children" style="padding-left:1.1rem; margin:0; list-style:none;"></ul>`;
const childrenUl = li.querySelector(".folder-children");
childItems.forEach(child => {
child.classList.add("folder-node-item");
childrenUl.appendChild(child);
});
// 折疊切換
const header = li.querySelector(".folder-header");
const chevron = li.querySelector(".chevron");
let collapsed = false;
header.addEventListener("mouseenter", () => { header.style.background = "rgba(99,102,241,0.08)"; });
header.addEventListener("mouseleave", () => { header.style.background = ""; });
header.addEventListener("click", (e) => {
if (e.target.closest("li[data-file]")) return;
collapsed = !collapsed;
childrenUl.style.display = collapsed ? "none" : "";
chevron.style.transform = collapsed ? "rotate(-90deg)" : "";
});
return li;
}
// ── 動態中文化 VvvebJS 元件與區塊 ─────────────────────────────────
function patchI18n() {
if (typeof Vvveb === "undefined" || !Vvveb.ComponentsGroup || Object.keys(Vvveb.ComponentsGroup).length === 0) {
@@ -979,6 +1208,7 @@
patchSaveAjax();
patchFileManager();
patchNewSection();
patchPageTree();
patchI18n();
enableSaveBtn();
});
@@ -986,6 +1216,7 @@
patchSaveAjax();
patchFileManager();
patchNewSection();
patchPageTree();
patchI18n();
enableSaveBtn();
}