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 = "";
}
};