Commit 4baa296e authored by xuchentao's avatar xuchentao

feat: add article list publishing actions

parent c5cd0ab3
Pipeline #406 canceled with stages
......@@ -71,16 +71,97 @@ async function loadArticles() {
for (const article of articles) {
const card = document.createElement("article");
card.className = "article-row";
card.innerHTML = '<div class="article-summary"><h2></h2><p><span class="category"></span><span class="updated"></span><span class="status"></span></p></div><button class="ghost small edit-button">编辑</button>';
card.innerHTML = '<div class="article-summary"><h2></h2><p><span class="category"></span><span class="updated"></span><span class="status"></span></p></div><div class="row-actions"></div>';
card.querySelector("h2").textContent = article.title;
card.querySelector(".category").textContent = article.category;
card.querySelector(".updated").textContent = `更新于 ${article.updated}`;
applyStatus(card.querySelector(".status"), article.publishStatus);
card.querySelector(".edit-button").onclick = () => openEditor(article.slug);
const actions = card.querySelector(".row-actions");
const edit = listAction("编辑", "ghost small", () => openEditor(article.slug));
const publish = listAction("发布", "primary small", () => publishFromList(article));
publish.disabled = article.publishStatus === "live";
publish.title = publish.disabled ? "文章当前已上线" : "发布到官网";
actions.append(edit, publish, listMoreActions(article));
list.append(card);
}
}
function listAction(label, className, action) {
const button = document.createElement("button");
button.type = "button";
button.className = className;
button.textContent = label;
button.onclick = action;
return button;
}
function listMenuAction(label, className, action) {
return listAction(label, `menu-action ${className}`.trim(), async (event) => {
event.currentTarget.closest("details").open = false;
await action();
});
}
function listMoreActions(article) {
const details = document.createElement("details");
details.className = "more-actions row-more-actions";
const summary = document.createElement("summary");
summary.textContent = "更多";
summary.setAttribute("aria-label", `${article.title}的更多操作`);
const menu = document.createElement("div");
menu.className = "action-menu";
if (article.publishStatus === "edited-draft") {
menu.append(listMenuAction("放弃未发布修改", "", () => discardFromList(article)));
}
if (["live", "edited-draft"].includes(article.publishStatus)) {
menu.append(listMenuAction("下架文章", "danger", () => unpublishFromList(article)));
}
menu.append(listMenuAction("删除文章", "danger", () => deleteFromList(article)));
details.append(summary, menu);
details.addEventListener("toggle", () => {
if (!details.open) return;
document.querySelectorAll(".row-more-actions[open]").forEach((item) => {
if (item !== details) item.open = false;
});
});
return details;
}
async function publishFromList(article) {
try {
await triggerBuild(`/articles/${article.slug}/publish`);
await loadArticles();
} catch {}
}
async function discardFromList(article) {
if (!confirm(`确定放弃《${article.title}》未发布的修改,恢复为当前线上版本吗?`)) return;
try {
await api(`/articles/${article.slug}/draft`, { method: "DELETE" });
await loadArticles();
} catch (error) { alert(error.message); }
}
async function unpublishFromList(article) {
const detail = article.publishStatus === "edited-draft" ? "未发布的修改会继续保留为草稿。" : "文章内容会继续保留为草稿。";
if (!confirm(`确定将《${article.title}》从官网下架吗?${detail}`)) return;
try {
await triggerBuild(`/articles/${article.slug}/unpublish`, { action: "下架" });
await loadArticles();
} catch {}
}
async function deleteFromList(article) {
const prompt = article.publishStatus === "new-draft"
? `确定彻底删除草稿《${article.title}》吗?删除后无法恢复。`
: `确定彻底删除《${article.title}》吗?线上版本和草稿都会删除,且无法恢复。`;
if (!confirm(prompt)) return;
try {
await triggerBuild(`/articles/${article.slug}`, { action: "删除", method: "DELETE" });
await loadArticles();
} catch {}
}
function fillCategories(selected) {
const current = selected || $("#category").value || categories[0] || "";
const options = $("#category-options");
......@@ -130,6 +211,11 @@ function toggleCategoryMenu() {
$("#category-trigger").addEventListener("click", (event) => { event.stopPropagation(); toggleCategoryMenu(); });
$("#category-menu").addEventListener("click", (event) => event.stopPropagation());
document.addEventListener("click", closeCategoryMenu);
document.addEventListener("click", (event) => {
document.querySelectorAll(".row-more-actions[open]").forEach((details) => {
if (!details.contains(event.target)) details.open = false;
});
});
document.addEventListener("keydown", (event) => { if (event.key === "Escape") closeCategoryMenu(); });
async function createCategory() {
......
......@@ -44,6 +44,7 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.view-actions { display: flex; gap: 9px; }
.article-list { display: grid; gap: 10px; }
.article-row { padding: 18px 20px; display: flex; align-items: center; justify-content: space-between; gap: 20px; background: white; border: 1px solid var(--border); border-radius: 14px; }
.article-summary { min-width: 0; flex: 1; }
.article-row h2 { margin: 0 0 8px; font-size: 17px; }
.article-row p { margin: 0; display: flex; flex-wrap: wrap; gap: 8px 13px; align-items: center; color: var(--muted); font-size: 12px; }
.article-row code { color: #8d9995; }
......@@ -51,7 +52,10 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.status.live { color: #188367; background: #e2f6ef; }
.status.new-draft { color: #916300; background: #fff2c7; }
.status.edited-draft { color: #9c4c22; background: #ffeadc; }
.row-actions { display: flex; gap: 8px; }
.row-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.row-actions button:disabled { cursor: default; opacity: .48; transform: none; }
.row-more-actions summary { min-height: 34px; padding-inline: 13px; font-size: 12px; }
.row-more-actions .action-menu { top: calc(100% + 8px); bottom: auto; z-index: 12; }
.empty { padding: 70px; color: var(--muted); text-align: center; background: white; border: 1px dashed var(--border); border-radius: 14px; }
#edit-form { display: grid; gap: 18px; }
.field-panel, .editor-panel { padding: 24px; background: white; border: 1px solid var(--border); border-radius: 16px; }
......@@ -148,6 +152,8 @@ input:focus, textarea:focus, select:focus { outline: 2px solid rgba(22,174,156,.
.field-row, .editor-grid { grid-template-columns: 1fr; }
.view { width: min(100% - 28px, 1120px); padding-top: 28px; }
.row-actions { width: 100%; }
.row-actions > button, .row-more-actions { flex: 1; }
.row-more-actions summary { width: 100%; justify-content: center; }
.edit-actions { flex-wrap: wrap; }
#message { width: 100%; margin: 4px 0 0; }
.login-card { padding: 30px 24px; }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment