feat: add batch content governance actions
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user