| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- 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: '<i class="ri-delete-bin-line"></i> 确认删除',
- 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 ? `<br>${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 = "";
- }
- };
|