/* ===================================================================== * 博主管理面板(manage.js) * 发文 / 编辑 / 删除文章(简介编辑已移至主页简介处)。 * ===================================================================== */ import { state } from "./state.js"; import { $, escapeHtml, formatDate, openModal, showToast } from "./utils.js"; import { api, uploadFile, uploadForm } from "./api.js"; import { renderMarkdown } from "./markdown.js"; import { navigate } from "./router.js"; import { openLoginModal } from "./auth.js"; function insertAtCursor(input, text) { const start = input.selectionStart ?? input.value.length; const end = input.selectionEnd ?? input.value.length; input.value = input.value.slice(0, start) + text + input.value.slice(end); input.focus(); const pos = start + text.length; input.setSelectionRange(pos, pos); } export async function renderManage(app) { document.title = "博主管理 - 孤竹居士的博客"; if (!state.user) { app.innerHTML = `

!

请先登录

`; $("#manage-login").addEventListener("click", openLoginModal); return; } if (state.user.role !== "blogger") { app.innerHTML = `

!

只有博主可以访问发文管理

返回首页
`; return; } app.innerHTML = `

博主管理

发布新文章

我的文章

加载中…

项目管理(在线演示 / GitHub 链接)

纯 HTML/CSS/JS(含 index.html)→ 自动开通在线演示;含后端代码 → 请填写 GitHub 链接

项目列表

加载中…

`; // ---------- 封面 / 正文图片 / 预览 ---------- const coverState = { url: "" }; const coverFile = $("#cover-file", app); const coverImg = $("#cover-img", app); const coverPreview = $("#cover-preview", app); const coverUrlInput = $("#cover-url", app); $("#btn-cover-upload", app).addEventListener("click", () => coverFile.click()); coverFile.addEventListener("change", async () => { const file = coverFile.files && coverFile.files[0]; if (!file) return; try { const data = await uploadFile("/api/upload/article", file); coverState.url = data.url; coverPreview.classList.remove("hidden"); coverImg.src = data.url; coverUrlInput.value = data.url; showToast("封面上传成功", "success"); } catch (err) { showToast(err.message, "error"); } coverFile.value = ""; }); $("#cover-remove", app).addEventListener("click", () => { coverState.url = ""; coverUrlInput.value = ""; coverPreview.classList.add("hidden"); coverImg.src = ""; }); coverUrlInput.addEventListener("input", () => { coverState.url = coverUrlInput.value.trim(); }); const contentInput = $("#art-content", app); const insertFile = $("#insert-image-file", app); $("#btn-insert-image", app).addEventListener("click", () => insertFile.click()); insertFile.addEventListener("change", async () => { const file = insertFile.files && insertFile.files[0]; if (!file) return; try { const data = await uploadFile("/api/upload/article", file); insertAtCursor(contentInput, `![](${data.url})`); showToast("图片已插入正文", "success"); } catch (err) { showToast(err.message, "error"); } insertFile.value = ""; }); // ---------- 正文视频插入(仅博主,mp4/webm) ---------- const insertVideoFile = $("#insert-video-file", app); $("#btn-insert-video", app).addEventListener("click", () => insertVideoFile.click()); insertVideoFile.addEventListener("change", async () => { const file = insertVideoFile.files && insertVideoFile.files[0]; if (!file) return; try { const data = await uploadFile("/api/upload/video", file); insertAtCursor(contentInput, `@[视频](${data.url})`); showToast("视频已插入正文", "success"); } catch (err) { showToast(err.message, "error"); } insertVideoFile.value = ""; }); // ---------- 正文文档插入(Word doc/docx)---------- // 只能用于“我的生活 / 我的学习”分区的文章:上传时携带分区给后端校验(项目区不使用此功能) const insertDocFile = $("#insert-doc-file", app); $("#btn-insert-doc", app).addEventListener("click", () => insertDocFile.click()); insertDocFile.addEventListener("change", async () => { const file = insertDocFile.files && insertDocFile.files[0]; if (!file) return; try { // 上传文档时携带文章分区:编辑已有文章用其分区,新文章默认 life(发布时仍可调整) const category = editingCategory || "life"; const data = await uploadFile("/api/upload/doc", file, { category }); insertAtCursor(contentInput, `[📄 ${escapeHtml(data.filename)}](${data.url})`); showToast("文档已插入正文", "success"); } catch (err) { showToast(err.message, "error"); } insertDocFile.value = ""; }); const previewBox = $("#preview-box", app); $("#btn-preview-toggle", app).addEventListener("click", () => { previewBox.classList.toggle("hidden"); if (!previewBox.classList.contains("hidden")) { $("#preview-content", app).innerHTML = renderMarkdown(contentInput.value); } }); // ---------- 发布 / 编辑 ---------- let editingId = null; let editingCategory = "life"; let editingVisibility = "public"; const setEditing = (article) => { editingId = article ? article.id : null; editingCategory = article ? (article.category || "life") : "life"; editingVisibility = article ? (article.visibility || "public") : "public"; $("#form-title", app).textContent = article ? "编辑文章" : "发布新文章"; $("#btn-publish", app).textContent = article ? "保存修改" : "发布文章"; $("#btn-cancel-edit", app).classList.toggle("hidden", !article); if (article) { $("#art-title", app).value = article.title; contentInput.value = article.content; coverState.url = article.cover || ""; coverUrlInput.value = article.cover || ""; if (article.cover) { coverPreview.classList.remove("hidden"); coverImg.src = article.cover; } else { coverPreview.classList.add("hidden"); coverImg.src = ""; } } }; $("#btn-cancel-edit", app).addEventListener("click", () => { $("#article-form", app).reset(); setEditing(null); coverState.url = ""; coverUrlInput.value = ""; coverPreview.classList.add("hidden"); coverImg.src = ""; }); $("#btn-publish", app).addEventListener("click", () => { const title = $("#art-title", app).value.trim(); const content = contentInput.value.trim(); if (!title) { showToast("请填写文章标题", "error"); return; } if (!content) { showToast("请填写文章正文", "error"); return; } // 发布管理弹窗:选择发布分区与可见范围后再确认 const modalContent = document.createElement("div"); modalContent.innerHTML = `

选择文章发布到的分区,以及谁可以查看。

`; const modal = openModal({ title: editingId ? "保存修改" : "发布管理", content: modalContent }); const actions = document.createElement("div"); actions.className = "modal-actions"; const cancelBtn = document.createElement("button"); cancelBtn.type = "button"; cancelBtn.className = "btn btn-outline"; cancelBtn.textContent = "取消"; cancelBtn.addEventListener("click", () => modal.close()); const confirmBtn = document.createElement("button"); confirmBtn.type = "button"; confirmBtn.id = "pub-confirm"; confirmBtn.className = "btn btn-primary"; confirmBtn.textContent = editingId ? "保存修改" : "确认发布"; confirmBtn.addEventListener("click", async () => { const category = modalContent.querySelector('input[name="pub-category"]:checked').value; const visibility = modalContent.querySelector('input[name="pub-visibility"]:checked').value; confirmBtn.disabled = true; confirmBtn.textContent = "发布中…"; try { const body = { title, content, cover: coverState.url || null, visibility, category }; let newId = editingId; if (editingId) { await api(`/api/article/${editingId}`, { method: "PUT", body }); showToast("修改已保存", "success"); } else { const created = await api("/api/article/add", { method: "POST", body }); newId = created.id; showToast("发布成功", "success"); } modal.close(); $("#article-form", app).reset(); setEditing(null); coverState.url = ""; coverUrlInput.value = ""; coverPreview.classList.add("hidden"); coverImg.src = ""; previewBox.classList.add("hidden"); if (newId) navigate(`/${category}/${newId}`); else await loadMyArticles(); } catch (err) { showToast(err.message, "error"); } confirmBtn.disabled = false; confirmBtn.textContent = editingId ? "保存修改" : "确认发布"; }); actions.appendChild(cancelBtn); actions.appendChild(confirmBtn); modalContent.appendChild(actions); }); // ---------- 我的文章:列表 + 编辑 / 删除 ---------- const myArticlesEl = $("#my-articles", app); async function loadMyArticles() { try { const data = await api("/api/article/list?page=1&page_size=100"); const mine = data.items.filter((a) => a.author_id === state.user.id); if (!mine.length) { myArticlesEl.innerHTML = '

还没有发布过文章

'; return; } myArticlesEl.innerHTML = ""; mine.forEach((a) => { const row = document.createElement("div"); row.className = "my-article-row"; row.innerHTML = ` ${a.cover ? `` : ''}
${escapeHtml(a.title)} ${formatDate(a.created_time)}
${a.visibility === "friend" ? '好友专属' : '公开'}
`; $(".my-article-edit", row).addEventListener("click", async () => { try { const detail = await api(`/api/article/${a.id}`); setEditing(detail); window.scrollTo({ top: 0, behavior: "smooth" }); } catch (err) { showToast(err.message, "error"); } }); $(".my-article-delete", row).addEventListener("click", () => { confirmDeleteArticle(a.id, a.title); }); myArticlesEl.appendChild(row); }); } catch (err) { myArticlesEl.innerHTML = '

加载失败

'; showToast(err.message, "error"); } } function confirmDeleteArticle(id, title) { const content = document.createElement("div"); content.innerHTML = `

确定删除文章「${escapeHtml(title)}」吗?删除后评论与点赞将一并删除,且不可恢复。

`; const modal = openModal({ title: "删除确认", content }); const actions = document.createElement("div"); actions.className = "modal-actions"; const cancelBtn = document.createElement("button"); cancelBtn.className = "btn btn-outline"; cancelBtn.textContent = "取消"; cancelBtn.addEventListener("click", () => modal.close()); const okBtn = document.createElement("button"); okBtn.className = "btn btn-danger"; okBtn.textContent = "确认删除"; okBtn.addEventListener("click", async () => { okBtn.disabled = true; okBtn.textContent = "删除中…"; try { await api(`/api/article/${id}`, { method: "DELETE" }); modal.close(); showToast("删除成功", "success"); await loadMyArticles(); } catch (err) { showToast(err.message, "error"); okBtn.disabled = false; okBtn.textContent = "确认删除"; } }); actions.appendChild(cancelBtn); actions.appendChild(okBtn); content.appendChild(actions); } // ---------- 项目管理(L0 静态托管) ---------- let editingProjectId = null; let editingProjVisibility = "public"; let selectedZip = null; const projName = $("#proj-name", app); const projDesc = $("#proj-desc", app); const projTech = $("#proj-tech", app); const projGithub = $("#proj-github", app); const projFileInput = $("#proj-file", app); const projFileNameEl = $("#proj-file-name", app); $("#btn-proj-upload", app).addEventListener("click", () => projFileInput.click()); projFileInput.addEventListener("change", () => { selectedZip = projFileInput.files && projFileInput.files[0]; projFileNameEl.textContent = selectedZip ? selectedZip.name : ""; }); function resetProjectForm() { editingProjectId = null; editingProjVisibility = "public"; selectedZip = null; $("#project-form", app).reset(); projFileNameEl.textContent = ""; $("#btn-proj-save", app).textContent = "添加项目"; $("#btn-proj-cancel", app).classList.add("hidden"); $("#proj-form-title", app).textContent = "项目管理(在线演示 / GitHub 链接)"; } $("#btn-proj-cancel", app).addEventListener("click", resetProjectForm); $("#btn-proj-save", app).addEventListener("click", async () => { const name = projName.value.trim(); if (!name) { showToast("请填写项目名称", "error"); return; } // 保存前弹窗选择可见范围(对齐文章发布流程) const modalContent = document.createElement("div"); modalContent.innerHTML = `

选择谁可以查看这个项目。

`; const modal = openModal({ title: editingProjectId ? "保存修改" : "发布管理", content: modalContent }); const actions = document.createElement("div"); actions.className = "modal-actions"; const cancelBtn = document.createElement("button"); cancelBtn.type = "button"; cancelBtn.className = "btn btn-outline"; cancelBtn.textContent = "取消"; cancelBtn.addEventListener("click", () => modal.close()); const confirmBtn = document.createElement("button"); confirmBtn.type = "button"; confirmBtn.className = "btn btn-primary"; confirmBtn.textContent = editingProjectId ? "保存修改" : "确认添加"; confirmBtn.addEventListener("click", async () => { const visibility = modalContent.querySelector('input[name="proj-visibility"]:checked').value; confirmBtn.disabled = true; confirmBtn.textContent = "保存中…"; const fields = { name, description: projDesc.value.trim(), tech: projTech.value.trim(), github_url: projGithub.value.trim(), visibility, }; try { if (editingProjectId) { await uploadForm(`/api/project/${editingProjectId}`, fields, selectedZip, "PUT"); showToast("项目已更新", "success"); } else { await uploadForm("/api/project/add", fields, selectedZip); showToast("项目已添加", "success"); } modal.close(); resetProjectForm(); await loadProjects(); } catch (err) { showToast(err.message, "error"); } confirmBtn.disabled = false; confirmBtn.textContent = editingProjectId ? "保存修改" : "确认添加"; }); actions.appendChild(cancelBtn); actions.appendChild(confirmBtn); modalContent.appendChild(actions); }); async function loadProjects() { const listEl = $("#proj-list", app); let items = []; try { items = await api("/api/project/list"); } catch (err) { listEl.innerHTML = '

加载失败

'; showToast(err.message, "error"); return; } if (!items.length) { listEl.innerHTML = '

还没有项目,添加一个试试

'; return; } listEl.innerHTML = ""; items.forEach((p) => { const row = document.createElement("div"); row.className = "my-article-row"; const typeBadge = p.project_type === "static" ? '在线演示' : 'GitHub 链接'; const visBadge = p.visibility === "friend" ? '好友专属' : '公开'; row.innerHTML = `
${escapeHtml(p.name)} ${escapeHtml(p.demo_url || "")}
${visBadge} ${typeBadge}
`; $(".proj-edit", row).addEventListener("click", () => { editingProjectId = p.id; editingProjVisibility = p.visibility || "public"; projName.value = p.name; projDesc.value = p.description || ""; projTech.value = p.tech || ""; projGithub.value = p.github_url || ""; selectedZip = null; projFileNameEl.textContent = ""; $("#btn-proj-save", app).textContent = "保存修改"; $("#btn-proj-cancel", app).classList.remove("hidden"); $("#proj-form-title", app).textContent = `编辑项目:${p.name}`; window.scrollTo({ top: 0, behavior: "smooth" }); }); $(".proj-delete", row).addEventListener("click", () => confirmDeleteProject(p)); listEl.appendChild(row); }); } function confirmDeleteProject(p) { const content = document.createElement("div"); content.innerHTML = `

确定删除项目「${escapeHtml(p.name)}」吗?在线演示与下载文件将一并删除。

`; const modal = openModal({ title: "删除确认", content }); const actions = document.createElement("div"); actions.className = "modal-actions"; const cancelBtn = document.createElement("button"); cancelBtn.className = "btn btn-outline"; cancelBtn.textContent = "取消"; cancelBtn.addEventListener("click", () => modal.close()); const okBtn = document.createElement("button"); okBtn.className = "btn btn-danger"; okBtn.textContent = "确认删除"; okBtn.addEventListener("click", async () => { okBtn.disabled = true; okBtn.textContent = "删除中…"; try { await api(`/api/project/${p.id}`, { method: "DELETE" }); modal.close(); showToast("项目已删除", "success"); await loadProjects(); } catch (err) { showToast(err.message, "error"); okBtn.disabled = false; okBtn.textContent = "确认删除"; } }); actions.appendChild(cancelBtn); actions.appendChild(okBtn); content.appendChild(actions); } await loadMyArticles(); await loadProjects(); }