diff --git a/apps/admin/src/lib/api.ts b/apps/admin/src/lib/api.ts index 94e6d91d..4050d48d 100644 --- a/apps/admin/src/lib/api.ts +++ b/apps/admin/src/lib/api.ts @@ -28,6 +28,7 @@ interface ApiSuccessResponse> { const adminMessageTranslations = new Map([ ['one or more fields failed validation', '有字段校验未通过,请检查标红项。'], + ['only archived articles can be deleted', '只有已归档的文章才允许删除。'], ['response payload is missing the data field', '响应数据缺少 data 字段。'], ['article not found', '未找到文章。'], ['asset not found', '未找到媒体文件。'], diff --git a/apps/admin/src/lib/format.ts b/apps/admin/src/lib/format.ts index 8adfd3a9..543a97d3 100644 --- a/apps/admin/src/lib/format.ts +++ b/apps/admin/src/lib/format.ts @@ -261,6 +261,7 @@ export const formatAuditSummary = (summary: string) => { [/^Updated article (.+)$/i, (m) => `已更新文章 ${m[1]}`], [/^Published article (.+)$/i, (m) => `已发布文章 ${m[1]}`], [/^Archived article (.+)$/i, (m) => `已归档文章 ${m[1]}`], + [/^Deleted article (.+)$/i, (m) => `已删除文章 ${m[1]}`], [/^Created GeekDaily episode (.+)$/i, (m) => `已创建极客日报第 ${m[1]} 期`], [/^Updated GeekDaily episode (.+)$/i, (m) => `已更新极客日报第 ${m[1]} 期`], [/^Created GeekDaily WeChat draft (.+)$/i, (m) => `已创建极客日报第 ${m[1]} 期微信公众号草稿`], diff --git a/apps/admin/src/views/ArticleEditorPage.vue b/apps/admin/src/views/ArticleEditorPage.vue index 0b4b0f17..7c099dcb 100644 --- a/apps/admin/src/views/ArticleEditorPage.vue +++ b/apps/admin/src/views/ArticleEditorPage.vue @@ -52,6 +52,7 @@ const article = ref(null); const loading = ref(true); const saving = ref(false); const actioning = ref(false); +const deleting = ref(false); const errorMessage = ref(''); const successMessage = ref(''); const fieldIssues = ref>({}); @@ -66,6 +67,7 @@ const saveButtonLabel = computed(() => ((isNew.value || form.status === 'draft') const saveButtonClass = computed(() => ['button-link', !canPublish.value && 'button-primary'].filter(Boolean).join(' ')); const canPublish = computed(() => form.status !== 'published'); const canArchive = computed(() => Boolean(article.value) && form.status !== 'archived'); +const canDelete = computed(() => Boolean(article.value) && form.status === 'archived'); const hasIssue = (...paths: string[]) => paths.some((path) => { if (fieldIssues.value[path]) { @@ -212,6 +214,30 @@ const runAction = async (action: 'archive') => { } }; +const deleteArticle = async () => { + if (!article.value) { + return; + } + + const confirmed = window.confirm(`确定删除文章“${article.value.title}”吗?此操作不可恢复。`); + if (!confirmed) { + return; + } + + resetFeedback(); + deleting.value = true; + try { + await adminRequest<{ id: string; title: string }>(`/api/admin/v1/articles/${article.value.id}`, { + method: 'DELETE', + }); + await router.push('/articles'); + } catch (error) { + errorMessage.value = error instanceof Error ? error.message : '无法删除文章。'; + } finally { + deleting.value = false; + } +}; + watch(() => route.fullPath, () => void loadArticle()); onMounted(() => void loadArticle()); @@ -232,13 +258,16 @@ onMounted(() => void loadArticle());
返回列表 - - - + +
diff --git a/apps/admin/src/views/ArticlesPage.vue b/apps/admin/src/views/ArticlesPage.vue index da760eef..313ec169 100644 --- a/apps/admin/src/views/ArticlesPage.vue +++ b/apps/admin/src/views/ArticlesPage.vue @@ -11,7 +11,7 @@ import { } from '@rebase/shared'; import PaginationBar from '../components/PaginationBar.vue'; -import { adminFetchWithMeta } from '../lib/api'; +import { adminFetchWithMeta, adminRequest } from '../lib/api'; import { formatContentStatus, formatDateTime } from '../lib/format'; import { getPublicSiteUrl } from '../lib/runtime-config'; @@ -22,7 +22,9 @@ const rows = ref([]); const pagination = ref(null); const loading = ref(true); const errorMessage = ref(''); +const successMessage = ref(''); const page = ref(1); +const deletingArticleId = ref(''); const statusFilterOpen = ref(false); const statusFilterRef = ref(null); @@ -78,6 +80,35 @@ const loadRows = async () => { } }; +const deleteArticle = async (row: AdminArticleListItem) => { + const confirmed = window.confirm(`确定删除已归档文章“${row.title}”吗?此操作不可恢复。`); + if (!confirmed) { + return; + } + + deletingArticleId.value = row.id; + errorMessage.value = ''; + successMessage.value = ''; + + try { + await adminRequest<{ id: string; title: string }>(`/api/admin/v1/articles/${row.id}`, { + method: 'DELETE', + }); + successMessage.value = `已删除文章:${row.title}`; + + if (rows.value.length === 1 && page.value > 1) { + page.value -= 1; + return; + } + + await loadRows(); + } catch (error) { + errorMessage.value = error instanceof Error ? error.message : '无法删除文章。'; + } finally { + deletingArticleId.value = ''; + } +}; + const goToPage = (nextPage: number) => { page.value = nextPage; }; @@ -157,6 +188,7 @@ onBeforeUnmount(() => {

正在加载文章列表…