Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
/* =====================================================================
|
||||
* 博主管理面板(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 = `
|
||||
<div class="empty-block">
|
||||
<p>!</p>
|
||||
<p class="muted">请先登录</p>
|
||||
<button class="btn btn-primary" id="manage-login">去登录</button>
|
||||
</div>`;
|
||||
$("#manage-login").addEventListener("click", openLoginModal);
|
||||
return;
|
||||
}
|
||||
if (state.user.role !== "blogger") {
|
||||
app.innerHTML = `
|
||||
<div class="empty-block">
|
||||
<p>!</p>
|
||||
<p class="muted">只有博主可以访问发文管理</p>
|
||||
<a class="btn btn-primary" data-link href="/">返回首页</a>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
app.innerHTML = `
|
||||
<h1 class="page-title">博主管理</h1>
|
||||
<div class="manage-grid">
|
||||
<section class="manage-card">
|
||||
<h2 class="section-title" id="form-title">发布新文章</h2>
|
||||
<form class="form" id="article-form">
|
||||
<label for="art-title">标题</label>
|
||||
<input id="art-title" type="text" maxlength="200" required placeholder="文章标题">
|
||||
|
||||
<label>封面</label>
|
||||
<div class="cover-field">
|
||||
<div class="cover-preview hidden" id="cover-preview">
|
||||
<img id="cover-img" alt="封面预览">
|
||||
<button type="button" class="btn btn-outline btn-sm" id="cover-remove">移除封面</button>
|
||||
</div>
|
||||
<div class="cover-actions">
|
||||
<button type="button" class="btn btn-outline" id="btn-cover-upload">📷 上传封面</button>
|
||||
<input type="text" id="cover-url" placeholder="或直接填写图片路径,如 /uploads/article/xxx.jpg">
|
||||
</div>
|
||||
<input type="file" id="cover-file" accept="image/jpeg,image/png,image/webp" hidden>
|
||||
</div>
|
||||
|
||||
<label for="art-content">正文(Markdown)</label>
|
||||
<div class="editor-toolbar">
|
||||
<button type="button" class="btn btn-outline btn-sm" id="btn-insert-image">🖼 插入图片</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="btn-insert-video">🎬 插入视频</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="btn-insert-doc">📄 插入文档</button>
|
||||
<button type="button" class="btn btn-outline btn-sm" id="btn-preview-toggle">预览</button>
|
||||
</div>
|
||||
<textarea id="art-content" rows="14" placeholder="支持 Markdown:标题、列表、代码块、图片等"></textarea>
|
||||
<input type="file" id="insert-image-file" accept="image/jpeg,image/png,image/webp" hidden>
|
||||
<input type="file" id="insert-video-file" accept="video/mp4,video/webm" hidden>
|
||||
<input type="file" id="insert-doc-file" accept=".doc,.docx" hidden>
|
||||
<div class="preview-box hidden" id="preview-box">
|
||||
<div class="article-content" id="preview-content"></div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary btn-block" id="btn-publish">发布文章</button>
|
||||
<button class="btn btn-outline btn-block hidden" id="btn-cancel-edit" type="button">取消编辑</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="manage-card">
|
||||
<h2 class="section-title">我的文章</h2>
|
||||
<div id="my-articles"><p class="muted">加载中…</p></div>
|
||||
</section>
|
||||
|
||||
<section class="manage-card manage-card-wide">
|
||||
<h2 class="section-title" id="proj-form-title">项目管理(在线演示 / GitHub 链接)</h2>
|
||||
<form class="form" id="project-form">
|
||||
<label for="proj-name">项目名称</label>
|
||||
<input id="proj-name" type="text" maxlength="100" required placeholder="项目名称">
|
||||
<label for="proj-desc">项目简介</label>
|
||||
<textarea id="proj-desc" rows="3" maxlength="2000" placeholder="介绍一下这个项目…"></textarea>
|
||||
<label for="proj-tech">技术标签(逗号分隔)</label>
|
||||
<input id="proj-tech" type="text" placeholder="如 HTML, CSS, JavaScript">
|
||||
<label for="proj-github">GitHub 链接(非纯静态项目必填)</label>
|
||||
<input id="proj-github" type="url" placeholder="https://github.com/你的用户名/仓库名">
|
||||
<label>上传代码(zip)</label>
|
||||
<div class="cover-field">
|
||||
<button type="button" class="btn btn-outline" id="btn-proj-upload">📦 选择 zip 文件</button>
|
||||
<span class="muted" id="proj-file-name"></span>
|
||||
</div>
|
||||
<input type="file" id="proj-file" accept=".zip" hidden>
|
||||
<p class="muted form-hint">纯 HTML/CSS/JS(含 index.html)→ 自动开通在线演示;含后端代码 → 请填写 GitHub 链接</p>
|
||||
<button type="button" class="btn btn-primary btn-block" id="btn-proj-save">添加项目</button>
|
||||
<button type="button" class="btn btn-outline btn-block hidden" id="btn-proj-cancel">取消编辑</button>
|
||||
</form>
|
||||
<h3 class="section-title proj-list-title">项目列表</h3>
|
||||
<div id="proj-list"><p class="muted">加载中…</p></div>
|
||||
</section>
|
||||
</div>`;
|
||||
|
||||
// ---------- 封面 / 正文图片 / 预览 ----------
|
||||
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 = `
|
||||
<p class="muted">选择文章发布到的分区,以及谁可以查看。</p>
|
||||
<div class="form">
|
||||
<label>发布分区</label>
|
||||
<div class="radio-row">
|
||||
<label class="radio"><input type="radio" name="pub-category" value="life" ${editingCategory === "study" ? "" : "checked"}> 我的生活</label>
|
||||
<label class="radio"><input type="radio" name="pub-category" value="study" ${editingCategory === "study" ? "checked" : ""}> 我的学习</label>
|
||||
</div>
|
||||
<label>可见范围</label>
|
||||
<div class="radio-row">
|
||||
<label class="radio"><input type="radio" name="pub-visibility" value="public" ${editingVisibility === "friend" ? "" : "checked"}> 公开(所有人可见)</label>
|
||||
<label class="radio"><input type="radio" name="pub-visibility" value="friend" ${editingVisibility === "friend" ? "checked" : ""}> 好友专属(仅好友可见)</label>
|
||||
</div>
|
||||
</div>`;
|
||||
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 = '<p class="empty">还没有发布过文章</p>'; return; }
|
||||
myArticlesEl.innerHTML = "";
|
||||
mine.forEach((a) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "my-article-row";
|
||||
row.innerHTML = `
|
||||
${a.cover ? `<img class="my-article-cover" src="${escapeHtml(a.cover)}" alt="">` : '<span class="my-article-cover-empty"></span>'}
|
||||
<div class="my-article-info">
|
||||
<a class="my-article-title" data-link href="/${a.category || "life"}/${a.id}">${escapeHtml(a.title)}</a>
|
||||
<span class="muted">${formatDate(a.created_time)}</span>
|
||||
</div>
|
||||
${a.visibility === "friend" ? '<span class="badge badge-friend">好友专属</span>' : '<span class="badge badge-public">公开</span>'}
|
||||
<div class="my-article-actions">
|
||||
<button class="btn btn-outline btn-sm my-article-edit">编辑</button>
|
||||
<button class="btn btn-outline btn-sm btn-danger-text my-article-delete">删除</button>
|
||||
</div>`;
|
||||
$(".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 = '<p class="muted">加载失败</p>';
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteArticle(id, title) {
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = `<p>确定删除文章「${escapeHtml(title)}」吗?删除后评论与点赞将一并删除,且不可恢复。</p>`;
|
||||
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 = `
|
||||
<p class="muted">选择谁可以查看这个项目。</p>
|
||||
<div class="form">
|
||||
<label>可见范围</label>
|
||||
<div class="radio-row">
|
||||
<label class="radio"><input type="radio" name="proj-visibility" value="public" ${editingProjVisibility === "friend" ? "" : "checked"}> 公开(所有人可见)</label>
|
||||
<label class="radio"><input type="radio" name="proj-visibility" value="friend" ${editingProjVisibility === "friend" ? "checked" : ""}> 好友专属(仅好友可见)</label>
|
||||
</div>
|
||||
</div>`;
|
||||
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 = '<p class="muted">加载失败</p>'; showToast(err.message, "error"); return; }
|
||||
if (!items.length) { listEl.innerHTML = '<p class="muted">还没有项目,添加一个试试</p>'; return; }
|
||||
listEl.innerHTML = "";
|
||||
items.forEach((p) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "my-article-row";
|
||||
const typeBadge = p.project_type === "static"
|
||||
? '<span class="badge badge-public">在线演示</span>'
|
||||
: '<span class="badge badge-friend">GitHub 链接</span>';
|
||||
const visBadge = p.visibility === "friend"
|
||||
? '<span class="badge badge-friend">好友专属</span>'
|
||||
: '<span class="badge badge-public">公开</span>';
|
||||
row.innerHTML = `
|
||||
<div class="my-article-info">
|
||||
<a class="my-article-title" data-link href="/projects/${p.id}">${escapeHtml(p.name)}</a>
|
||||
<span class="muted">${escapeHtml(p.demo_url || "")}</span>
|
||||
</div>
|
||||
${visBadge}
|
||||
${typeBadge}
|
||||
<div class="my-article-actions">
|
||||
<button class="btn btn-outline btn-sm proj-edit">编辑</button>
|
||||
<button class="btn btn-outline btn-sm btn-danger-text proj-delete">删除</button>
|
||||
</div>`;
|
||||
$(".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 = `<p>确定删除项目「${escapeHtml(p.name)}」吗?在线演示与下载文件将一并删除。</p>`;
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user