feat: add batch content governance actions

This commit is contained in:
2026-01-16 14:19:43 +08:00
parent e5f40287c3
commit daaacc3fa4
11 changed files with 431 additions and 3 deletions

View File

@@ -131,6 +131,17 @@ export const ContentService = {
}
});
},
async batchUpdateContentStatus({ content_ids, status, reason } = {}) {
if (!Array.isArray(content_ids) || content_ids.length === 0) throw new Error('content_ids is required');
return requestJson('/super/v1/contents/status/batch', {
method: 'POST',
body: {
content_ids,
status,
reason
}
});
},
async getContentStatistics({ tenant_id, start_at, end_at, granularity } = {}) {
const iso = (d) => {
if (!d) return undefined;

View File

@@ -102,6 +102,17 @@ const reviewActionOptions = [
{ label: '驳回', value: 'reject' }
];
const batchStatusDialogVisible = ref(false);
const batchStatusSubmitting = ref(false);
const batchStatusValue = ref('unpublished');
const batchStatusReason = ref('');
const batchStatusTargetIDs = ref([]);
const batchStatusOptions = [
{ label: '下架内容', value: 'unpublished' },
{ label: '封禁内容', value: 'blocked' }
];
const statusOptions = [
{ label: '全部', value: '' },
{ label: 'draft', value: 'draft' },
@@ -359,6 +370,7 @@ function getReportActionLabel(value) {
const selectedCount = computed(() => selectedContents.value.length);
const reviewTargetCount = computed(() => reviewTargetIDs.value.length);
const batchStatusTargetCount = computed(() => batchStatusTargetIDs.value.length);
const reportProcessNeedsContentAction = computed(() => reportProcessAction.value === 'approve');
async function loadContents() {
@@ -449,6 +461,52 @@ async function confirmReview() {
}
}
function openBatchStatusDialog(statusValue) {
if (!selectedContents.value.length) {
toast.add({ severity: 'warn', summary: '请先选择内容', detail: '至少选择 1 条内容进行处置', life: 3000 });
return;
}
const ids = selectedContents.value.map((row) => getContentID(row)).filter((id) => id > 0);
if (!ids.length) {
toast.add({ severity: 'warn', summary: '选择无效', detail: '未识别到可处置的内容', life: 3000 });
return;
}
batchStatusTargetIDs.value = ids;
batchStatusValue.value = statusValue || 'unpublished';
batchStatusReason.value = '';
batchStatusDialogVisible.value = true;
}
async function confirmBatchStatus() {
const ids = batchStatusTargetIDs.value.filter((id) => id > 0);
if (!ids.length) return;
const statusValue = batchStatusValue.value;
const reason = batchStatusReason.value.trim();
if (statusValue === 'blocked' && !reason) {
toast.add({ severity: 'warn', summary: '请输入原因', detail: '封禁内容时需填写原因', life: 3000 });
return;
}
batchStatusSubmitting.value = true;
try {
await ContentService.batchUpdateContentStatus({
content_ids: ids,
status: statusValue,
reason: reason || undefined
});
toast.add({ severity: 'success', summary: '处置完成', detail: `已处理 ${ids.length} 条内容`, life: 3000 });
batchStatusDialogVisible.value = false;
batchStatusTargetIDs.value = [];
selectedContents.value = [];
await loadContents();
} catch (error) {
toast.add({ severity: 'error', summary: '处置失败', detail: error?.message || '无法完成内容处置', life: 4000 });
} finally {
batchStatusSubmitting.value = false;
}
}
function onSearch() {
page.value = 1;
loadContents();
@@ -721,6 +779,8 @@ watch(
<div class="flex items-center gap-2">
<Button label="批量通过" icon="pi pi-check" severity="success" :disabled="selectedCount === 0" @click="openBatchReviewDialog('approve')" />
<Button label="批量驳回" icon="pi pi-times" severity="danger" :disabled="selectedCount === 0" @click="openBatchReviewDialog('reject')" />
<Button label="批量下架" icon="pi pi-ban" severity="warning" :disabled="selectedCount === 0" @click="openBatchStatusDialog('unpublished')" />
<Button label="批量封禁" icon="pi pi-shield" severity="danger" :disabled="selectedCount === 0" @click="openBatchStatusDialog('blocked')" />
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadContents" :disabled="loading" />
</div>
</div>
@@ -1213,6 +1273,30 @@ watch(
</template>
</Dialog>
<Dialog v-model:visible="batchStatusDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">批量处置内容</span>
<span class="text-muted-color"> {{ batchStatusTargetCount }} </span>
</div>
</template>
<div class="flex flex-col gap-4">
<div>
<label class="block font-medium mb-2">处置动作</label>
<Select v-model="batchStatusValue" :options="batchStatusOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div>
<label class="block font-medium mb-2">处置说明</label>
<InputText v-model="batchStatusReason" placeholder="封禁内容建议填写原因" class="w-full" />
</div>
<div class="text-sm text-muted-color">处置后会记录审计并通知作者</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="batchStatusDialogVisible = false" :disabled="batchStatusSubmitting" />
<Button label="确认处置" icon="pi pi-check" severity="danger" @click="confirmBatchStatus" :loading="batchStatusSubmitting" :disabled="batchStatusSubmitting || (batchStatusValue === 'blocked' && !batchStatusReason.trim())" />
</template>
</Dialog>
<Dialog v-model:visible="reportProcessDialogVisible" :modal="true" :style="{ width: '560px' }">
<template #header>
<div class="flex items-center gap-2">