window.initUserRepoBrowser = async function initUserRepoBrowser({ resourceId }) { const refSelect = document.getElementById("refSelect"); const reloadRepo = document.getElementById("reloadRepo"); const treeEl = document.getElementById("tree"); const fileContent = document.getElementById("fileContent"); const filePlaceholder = document.getElementById("filePlaceholder"); const breadcrumb = document.getElementById("breadcrumb"); const downloadBtn = document.getElementById("downloadBtn"); const repoModalBackdrop = document.getElementById("repoModalBackdrop"); const repoModalTitle = document.getElementById("repoModalTitle"); const repoModalClose = document.getElementById("repoModalClose"); const repoModalBody = document.getElementById("repoModalBody"); const repoModalFooter = document.getElementById("repoModalFooter"); if (!refSelect || !reloadRepo || !treeEl || !fileContent || !breadcrumb || !downloadBtn) return; let currentRef = ""; let currentPath = ""; let canEditRepo = false; let refKinds = { branches: new Set(), tags: new Set() }; let selectedFilePath = ""; let selectedFileContent = ""; const repoWriteActionsEnabled = false; function closeRepoModal() { if (!repoModalBackdrop) return; repoModalBackdrop.style.display = "none"; if (repoModalTitle) repoModalTitle.textContent = ""; if (repoModalBody) repoModalBody.innerHTML = ""; if (repoModalFooter) repoModalFooter.innerHTML = ""; } function openRepoModal(title, bodyNodes, footerNodes, icon = "ri-code-line") { if (!repoModalBackdrop || !repoModalTitle || !repoModalBody || !repoModalFooter) return; repoModalTitle.innerHTML = ""; repoModalTitle.appendChild(el("i", { class: icon })); repoModalTitle.appendChild(document.createTextNode(title)); repoModalBody.innerHTML = ""; repoModalFooter.innerHTML = ""; bodyNodes.forEach((n) => repoModalBody.appendChild(n)); footerNodes.forEach((n) => repoModalFooter.appendChild(n)); repoModalBackdrop.style.display = ""; } if (repoModalBackdrop && repoModalBackdrop.dataset.repoBound !== "1") { if (repoModalClose) repoModalClose.addEventListener("click", closeRepoModal); repoModalBackdrop.addEventListener("click", (evt) => { if (evt.target === repoModalBackdrop) closeRepoModal(); }); repoModalBackdrop.dataset.repoBound = "1"; } function isBranchRef(ref) { return refKinds.branches.has(ref); } function setBreadcrumb(path) { breadcrumb.innerHTML = ""; const parts = path ? path.split("/") : []; const items = [{ name: "根目录", path: "" }]; let acc = ""; parts.forEach((p) => { acc = acc ? `${acc}/${p}` : p; items.push({ name: p, path: acc }); }); items.forEach((it, idx) => { const a = el("a", { href: "#" }, it.name); a.addEventListener("click", async (e) => { e.preventDefault(); currentPath = it.path; await loadTree(); }); breadcrumb.appendChild(a); if (idx < items.length - 1) breadcrumb.appendChild(el("span", { class: "muted" }, "/")); }); } async function loadRefs() { const refs = await apiFetch(`/resources/${resourceId}/repo/refs`); refSelect.innerHTML = ""; refKinds = { branches: new Set(), tags: new Set() }; const branchGroup = document.createElement("optgroup"); branchGroup.label = "分支"; (refs.branches || []).forEach((b) => { const name = (b.name || "").trim(); if (!name) return; refKinds.branches.add(name); branchGroup.appendChild(el("option", { value: name }, name)); }); const tagGroup = document.createElement("optgroup"); tagGroup.label = "标签"; (refs.tags || []).forEach((t) => { const name = (t.name || "").trim(); if (!name) return; refKinds.tags.add(name); tagGroup.appendChild(el("option", { value: name }, name)); }); if (branchGroup.children.length) refSelect.appendChild(branchGroup); if (tagGroup.children.length) refSelect.appendChild(tagGroup); currentRef = refSelect.value; } async function loadTree() { fileContent.textContent = ""; if (filePlaceholder) filePlaceholder.style.display = ""; selectedFilePath = ""; selectedFileContent = ""; setBreadcrumb(currentPath); treeEl.innerHTML = ""; const params = new URLSearchParams(); params.set("ref", currentRef); params.set("path", currentPath); const data = await apiFetch(`/resources/${resourceId}/repo/tree?${params.toString()}`); data.items.forEach((it) => { const rightText = String(it.path || ""); const isLocked = it.type !== "dir" && it.guestAllowed === false; const rightNode = isLocked ? el( "div", { class: "muted tree-locked", style: "font-size: 0.85rem; display: flex; align-items: center; gap: 6px;" }, el("i", { class: "ri-lock-2-line" }), "需登录" ) : rightText && rightText !== it.name ? el("div", { class: "muted", style: "font-size: 0.85rem;" }, rightText) : null; const row = el( "div", { class: `card${isLocked ? " is-locked" : ""}` }, el( "div", { style: "display: flex; align-items: center; gap: 8px; font-weight: 500;" }, el("i", { class: it.type === "dir" ? "ri-folder-3-fill" : "ri-file-text-line", style: `font-size: 1.2rem; color: ${it.type === "dir" ? "#fbbf24" : "var(--muted)"};` }), it.name ), rightNode ); row.addEventListener("click", async () => { if (it.type === "dir") { currentPath = it.path; await loadTree(); return; } if (it.guestAllowed === false) { Swal.fire({ title: '需要登录', text: '未登录仅可预览文档/配置等普通文本文件', icon: 'info', showCancelButton: true, confirmButtonText: '去登录', cancelButtonText: '取消', confirmButtonColor: 'var(--brand)' }).then((result) => { if (result.isConfirmed) { window.location.href = `/ui/login?next=${currentNextParam()}`; } }); return; } const p = new URLSearchParams(); p.set("ref", currentRef); p.set("path", it.path); try { const f = await apiFetch(`/resources/${resourceId}/repo/file?${p.toString()}`); fileContent.textContent = f.content; if (filePlaceholder) filePlaceholder.style.display = "none"; selectedFilePath = it.path; selectedFileContent = f.content; } catch (e) { if (e.status === 401 && e.detail?.error === "login_required") { Swal.fire({ title: '需要登录', text: '未登录仅可预览文档/配置等普通文本文件', icon: 'info', showCancelButton: true, confirmButtonText: '去登录', cancelButtonText: '取消', confirmButtonColor: 'var(--brand)' }).then((result) => { if (result.isConfirmed) { window.location.href = `/ui/login?next=${currentNextParam()}`; } }); return; } fileContent.textContent = `无法预览:${e.detail?.error || e.status || "unknown"}`; if (filePlaceholder) filePlaceholder.style.display = "none"; } }); treeEl.appendChild(row); }); } async function showCommits() { const p = new URLSearchParams(); p.set("ref", currentRef); const focusPath = selectedFilePath || currentPath || ""; if (focusPath) p.set("path", focusPath); p.set("limit", "20"); const msg = el("div", { class: "muted" }, "加载中…"); openRepoModal("提交历史", [msg], [el("button", { class: "btn", onclick: closeRepoModal }, "关闭")], "ri-history-line"); try { const data = await apiFetch(`/resources/${resourceId}/repo/commits?${p.toString()}`); const items = data.items || []; if (!items.length) { msg.textContent = "没有找到提交记录"; return; } msg.remove(); const list = el("div", {}); items.forEach((it) => { const sha = String(it.sha || ""); list.appendChild( el( "div", { class: "card", style: "margin-bottom:12px; padding: 16px; border-left: 3px solid var(--brand); border-radius: 8px;" }, el( "div", { style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;" }, el("div", { style: "font-weight: 500; font-size: 1.05rem;" }, String(it.subject || "")), el("span", { class: "badge", style: "font-family: monospace; font-size: 0.85rem;" }, sha.slice(0, 7)) ), el( "div", { class: "muted", style: "display: flex; align-items: center; gap: 8px; font-size: 0.9rem;" }, el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-user-line" }), it.authorName || ""), el("span", { style: "display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-time-line" }), formatDateTime(it.authorDate)) ) ) ); }); if (repoModalBody) repoModalBody.appendChild(list); } catch (e) { msg.textContent = `加载失败:${e.detail?.error || e.status || "unknown"}${e.detail?.message ? `\n${e.detail.message}` : ""}`; } } if (downloadBtn) { const toolbar = downloadBtn.closest(".toolbar"); if (toolbar && !document.getElementById("commitsBtn")) { const commitsBtn = el("button", { id: "commitsBtn", class: "btn", style: "border-radius: 8px; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-history-line" }), "提交历史"); commitsBtn.addEventListener("click", showCommits); toolbar.insertBefore(commitsBtn, downloadBtn); } } if (refSelect.dataset.repoBound !== "1") { refSelect.addEventListener("change", async () => { currentRef = refSelect.value; currentPath = ""; await loadTree(); }); reloadRepo.addEventListener("click", async () => { await loadRefs(); currentPath = ""; await loadTree(); }); refSelect.dataset.repoBound = "1"; } try { await apiFetch("/admin/settings"); canEditRepo = true; } catch (e) { canEditRepo = false; } if (canEditRepo && repoWriteActionsEnabled) { const toolbar = downloadBtn.closest(".toolbar"); if (toolbar && !document.getElementById("repoWriteActions")) { const createBtn = el("button", { class: "btn", style: "border-radius: 8px; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-file-add-line" }), "新建文件"); const editBtn = el("button", { class: "btn", style: "border-radius: 8px; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-edit-line" }), "在线编辑"); const delBtn = el("button", { class: "btn btn-danger", style: "border-radius: 8px; display: flex; align-items: center; gap: 4px;" }, el("i", { class: "ri-delete-bin-line" }), "删除"); function requireBranchOrToast() { if (isBranchRef(currentRef)) return true; Swal.fire({ icon: 'warning', title: '操作受限', text: '仅支持在分支上进行编辑或提交操作' }); return false; } function requireSelectedFileOrToast() { if (selectedFilePath) return true; Swal.fire({ icon: 'info', title: '未选择文件', text: '请先在左侧目录结构中选择一个文件' }); return false; } async function createFile() { if (!requireBranchOrToast()) return; const pathInput = el("input", { class: "input", placeholder: "例如:README.md 或 docs/intro.md" }); const defaultPath = currentPath ? `${currentPath.replace(/\\/g, "/").replace(/\/+$/, "")}/new-file.txt` : "new-file.txt"; pathInput.value = defaultPath; const msgInput = el("input", { class: "input", placeholder: "提交信息,例如:Add new file" }); const ta = el("textarea", { class: "input", style: "min-height:260px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;" }); const msg = el("div", { class: "muted" }); const saveBtn = el("button", { class: "btn btn-primary" }, "提交"); saveBtn.addEventListener("click", async () => { msg.textContent = ""; saveBtn.disabled = true; try { const res = await apiFetch(`/resources/${resourceId}/repo/file`, { method: "POST", body: { ref: currentRef, path: pathInput.value.trim(), content: ta.value, message: msgInput.value.trim() }, }); msg.textContent = `提交成功:${String(res.commit || "").slice(0, 10)}`; await loadTree(); setTimeout(closeRepoModal, 600); } catch (e) { msg.textContent = `提交失败:${e.detail?.error || e.status || "unknown"}${e.detail?.message ? `\n${e.detail.message}` : ""}`; } finally { saveBtn.disabled = false; } }); openRepoModal( "新建文件", [ el( "div", { style: "display: flex; flex-direction: column; gap: 16px;" }, el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "文件路径"), pathInput), el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "提交信息"), msgInput), el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "文件内容"), ta), msg ) ], [el("button", { class: "btn", onclick: closeRepoModal }, "取消"), saveBtn], "ri-file-add-line" ); } async function editFile() { if (!requireBranchOrToast()) return; if (!requireSelectedFileOrToast()) return; const pathText = el("input", { class: "input", value: selectedFilePath, disabled: true, style: "background: #f1f5f9; color: var(--muted); cursor: not-allowed;" }); const msgInput = el("input", { class: "input", placeholder: "提交信息,例如:Update README" }); const ta = el("textarea", { class: "input", style: "min-height:320px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;" }, ""); ta.value = selectedFileContent || ""; const msg = el("div", { class: "muted" }); const saveBtn = el("button", { class: "btn btn-primary" }, "提交"); saveBtn.addEventListener("click", async () => { msg.textContent = ""; saveBtn.disabled = true; try { const res = await apiFetch(`/resources/${resourceId}/repo/file`, { method: "PUT", body: { ref: currentRef, path: selectedFilePath, content: ta.value, message: msgInput.value.trim() }, }); selectedFileContent = ta.value; msg.textContent = `提交成功:${String(res.commit || "").slice(0, 10)}`; await loadTree(); setTimeout(closeRepoModal, 600); } catch (e) { msg.textContent = `提交失败:${e.detail?.error || e.status || "unknown"}${e.detail?.message ? `\n${e.detail.message}` : ""}`; } finally { saveBtn.disabled = false; } }); openRepoModal( "在线编辑", [ el( "div", { style: "display: flex; flex-direction: column; gap: 16px;" }, el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "文件路径"), pathText), el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "提交信息"), msgInput), el("div", { style: "display: flex; flex-direction: column; gap: 8px;" }, el("div", { class: "label", style: "font-weight: 600;" }, "文件内容"), ta), msg ) ], [el("button", { class: "btn", onclick: closeRepoModal }, "取消"), saveBtn], "ri-edit-line" ); } async function deleteFile() { if (!requireBranchOrToast()) return; if (!requireSelectedFileOrToast()) return; Swal.fire({ title: '确认删除?', text: `您即将删除文件:${selectedFilePath}`, icon: 'warning', input: 'text', inputPlaceholder: '提交信息,例如:Delete file', showCancelButton: true, confirmButtonColor: '#d33', cancelButtonColor: 'var(--border)', confirmButtonText: ' 确认删除', cancelButtonText: '取消', showLoaderOnConfirm: true, customClass: { cancelButton: 'btn', confirmButton: 'btn btn-danger' }, preConfirm: async (message) => { try { const res = await apiFetch(`/resources/${resourceId}/repo/file`, { method: "DELETE", body: { ref: currentRef, path: selectedFilePath, message: (message || "").trim() }, }); return res; } catch (e) { Swal.showValidationMessage(`删除失败:${e.detail?.error || e.status || "unknown"}${e.detail?.message ? `
${e.detail.message}` : ""}`); } }, allowOutsideClick: () => !Swal.isLoading() }).then(async (result) => { if (result.isConfirmed) { selectedFilePath = ""; selectedFileContent = ""; Swal.fire({ title: '删除成功!', text: `提交 ID:${String(result.value.commit || "").slice(0, 10)}`, icon: 'success', timer: 1500, showConfirmButton: false }); await loadTree(); } }); } createBtn.addEventListener("click", createFile); editBtn.addEventListener("click", editFile); delBtn.addEventListener("click", deleteFile); const group = btnGroup(createBtn, editBtn, delBtn); group.id = "repoWriteActions"; const commitsBtn = document.getElementById("commitsBtn"); toolbar.insertBefore(group, commitsBtn || downloadBtn); } } try { breadcrumb.textContent = "加载中…"; await loadRefs(); await loadTree(); } catch (e) { breadcrumb.textContent = "仓库加载失败"; treeEl.innerHTML = ""; treeEl.appendChild(el("div", { class: "card", style: "margin: 8px; padding: 16px; border-radius: 12px;" }, `加载失败:${e.detail?.error || e.status || "unknown"}`)); fileContent.textContent = ""; } };