/* =====================================================================
* 博主管理面板(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 = `
博主管理
`;
// ---------- 封面 / 正文图片 / 预览 ----------
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, ``);
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();
}