feat: add batch content report processing

This commit is contained in:
2026-01-16 17:59:18 +08:00
parent 028c462eaa
commit b796636b5d
11 changed files with 599 additions and 8 deletions

View File

@@ -45,3 +45,19 @@ func (c *contentReports) List(ctx fiber.Ctx, filter *dto.SuperContentReportListF
func (c *contentReports) Process(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperContentReportProcessForm) error {
return services.Super.ProcessContentReport(ctx, user.ID, id, form)
}
// Batch process content reports
//
// @Router /super/v1/content-reports/process/batch [post]
// @Summary Batch process content reports
// @Description Batch process content report records
// @Tags Content
// @Accept json
// @Produce json
// @Param form body dto.SuperContentReportBatchProcessForm true "Batch process form"
// @Success 200 {string} string "Processed"
// @Bind user local key(__ctx_user)
// @Bind form body
func (c *contentReports) BatchProcess(ctx fiber.Ctx, user *models.User, form *dto.SuperContentReportBatchProcessForm) error {
return services.Super.BatchProcessContentReports(ctx, user.ID, form)
}

View File

@@ -98,3 +98,15 @@ type SuperContentReportProcessForm struct {
// Reason 处理说明(可选,用于审计记录)。
Reason string `json:"reason"`
}
// SuperContentReportBatchProcessForm 超管内容举报批量处理表单。
type SuperContentReportBatchProcessForm struct {
// ReportIDs 待处理举报ID列表。
ReportIDs []int64 `json:"report_ids" validate:"required,min=1,dive,gt=0"`
// Action 处理动作approve/reject
Action string `json:"action" validate:"required,oneof=approve reject"`
// ContentAction 内容处置动作block/unpublish/ignore仅在 approve 时生效。
ContentAction string `json:"content_action"`
// Reason 处理说明(可选,用于审计记录)。
Reason string `json:"reason"`
}

View File

@@ -108,6 +108,12 @@ func (r *Routes) Register(router fiber.Router) {
PathParam[int64]("id"),
Body[dto.SuperContentReportProcessForm]("form"),
))
r.log.Debugf("Registering route: Post /super/v1/content-reports/process/batch -> contentReports.BatchProcess")
router.Post("/super/v1/content-reports/process/batch"[len(r.Path()):], Func2(
r.contentReports.BatchProcess,
Local[*models.User]("__ctx_user"),
Body[dto.SuperContentReportBatchProcessForm]("form"),
))
// Register routes for controller: contents
r.log.Debugf("Registering route: Get /super/v1/contents -> contents.List")
router.Get("/super/v1/contents"[len(r.Path()):], DataFunc1(

View File

@@ -3166,6 +3166,163 @@ func (s *super) ProcessContentReport(ctx context.Context, operatorID, id int64,
return nil
}
func (s *super) BatchProcessContentReports(ctx context.Context, operatorID int64, form *super_dto.SuperContentReportBatchProcessForm) error {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
}
if form == nil {
return errorx.ErrBadRequest.WithMsg("处理参数不能为空")
}
action := strings.ToLower(strings.TrimSpace(form.Action))
if action != "approve" && action != "reject" {
return errorx.ErrBadRequest.WithMsg("处理动作非法")
}
contentAction := strings.ToLower(strings.TrimSpace(form.ContentAction))
if action == "approve" {
if contentAction == "" {
contentAction = "ignore"
}
switch contentAction {
case "block", "unpublish", "ignore":
default:
return errorx.ErrBadRequest.WithMsg("内容处置动作非法")
}
} else {
contentAction = "ignore"
}
// 去重并过滤非法ID避免批量处理出现空指令。
unique := make(map[int64]struct{})
reportIDs := make([]int64, 0, len(form.ReportIDs))
for _, id := range form.ReportIDs {
if id <= 0 {
continue
}
if _, ok := unique[id]; ok {
continue
}
unique[id] = struct{}{}
reportIDs = append(reportIDs, id)
}
if len(reportIDs) == 0 {
return errorx.ErrBadRequest.WithMsg("举报ID不能为空")
}
status := "approved"
if action == "reject" {
status = "rejected"
}
handledReason := strings.TrimSpace(form.Reason)
handledAt := time.Now()
var reports []*models.ContentReport
contentMap := make(map[int64]*models.Content)
// 事务内批量更新举报处理结果及关联内容状态,保证一致性。
err := models.Q.Transaction(func(tx *models.Query) error {
reportTbl, reportQuery := tx.ContentReport.QueryContext(ctx)
list, err := reportQuery.Where(reportTbl.ID.In(reportIDs...)).Find()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if len(list) != len(reportIDs) {
return errorx.ErrRecordNotFound.WithMsg("部分举报不存在")
}
for _, report := range list {
if strings.TrimSpace(report.Status) != "" && report.Status != "pending" {
return errorx.ErrStatusConflict.WithMsg("仅可处理待处理举报")
}
}
_, err = reportQuery.Where(reportTbl.ID.In(reportIDs...)).UpdateSimple(
reportTbl.Status.Value(status),
reportTbl.HandledBy.Value(operatorID),
reportTbl.HandledAction.Value(contentAction),
reportTbl.HandledReason.Value(handledReason),
reportTbl.HandledAt.Value(handledAt),
)
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
if action == "approve" {
contentIDs := make([]int64, 0, len(list))
for _, report := range list {
contentIDs = append(contentIDs, report.ContentID)
}
contentTbl, contentQuery := tx.Content.QueryContext(ctx)
contentQueryUnscoped := contentQuery.Unscoped()
contents, err := contentQueryUnscoped.Where(contentTbl.ID.In(contentIDs...)).Find()
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
for _, content := range contents {
contentMap[content.ID] = content
}
if contentAction != "ignore" {
var nextStatus consts.ContentStatus
switch contentAction {
case "block":
nextStatus = consts.ContentStatusBlocked
case "unpublish":
nextStatus = consts.ContentStatusUnpublished
}
if nextStatus != "" && len(contents) > 0 {
if _, err := contentQuery.Where(contentTbl.ID.In(contentIDs...)).UpdateSimple(
contentTbl.Status.Value(nextStatus),
); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
}
}
}
reports = list
return nil
})
if err != nil {
return err
}
if action == "approve" {
title := "内容举报成立"
actionLabel := "不处理内容"
switch contentAction {
case "block":
actionLabel = "封禁内容"
case "unpublish":
actionLabel = "下架内容"
}
for _, report := range reports {
content := contentMap[report.ContentID]
if content == nil {
continue
}
detail := "内容《" + content.Title + "》举报成立,处理结果:" + actionLabel
if handledReason != "" {
detail += "。说明:" + handledReason
}
if Notification != nil {
_ = Notification.Send(ctx, content.TenantID, content.UserID, string(consts.NotificationTypeAudit), title, detail)
}
}
}
if Audit != nil {
detail := "处理内容举报:" + action + " / " + contentAction
if handledReason != "" {
detail += " / " + handledReason
}
for _, report := range reports {
Audit.Log(ctx, report.TenantID, operatorID, "process_content_report", cast.ToString(report.ID), detail)
}
}
return nil
}
func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int64, form *super_dto.SuperTenantContentStatusUpdateForm) error {
tbl, q := models.ContentQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(contentID), tbl.TenantID.Eq(tenantID)).Update(tbl.Status, consts.ContentStatus(form.Status))

View File

@@ -396,6 +396,107 @@ func (s *SuperTestSuite) Test_ContentReportGovernance() {
})
}
func (s *SuperTestSuite) Test_BatchProcessContentReports() {
Convey("BatchProcessContentReports", s.T(), func() {
ctx := s.T().Context()
database.Truncate(ctx, s.DB, models.TableNameContentReport, models.TableNameContent, models.TableNameTenant, models.TableNameUser)
owner := &models.User{Username: "batch_report_owner"}
reporter := &models.User{Username: "batch_reporter"}
admin := &models.User{Username: "batch_report_admin"}
models.UserQuery.WithContext(ctx).Create(owner, reporter, admin)
tenant := &models.Tenant{UserID: owner.ID, Code: "t-report-batch", Name: "Report Batch Tenant", Status: consts.TenantStatusVerified}
models.TenantQuery.WithContext(ctx).Create(tenant)
contentA := &models.Content{
TenantID: tenant.ID,
UserID: owner.ID,
Title: "Batch Report A",
Status: consts.ContentStatusPublished,
}
contentB := &models.Content{
TenantID: tenant.ID,
UserID: owner.ID,
Title: "Batch Report B",
Status: consts.ContentStatusPublished,
}
models.ContentQuery.WithContext(ctx).Create(contentA, contentB)
Convey("should batch approve reports and unpublish contents", func() {
reportA := &models.ContentReport{
TenantID: tenant.ID,
ContentID: contentA.ID,
ReporterID: reporter.ID,
Reason: "spam",
Detail: "批量举报A",
Status: "pending",
}
reportB := &models.ContentReport{
TenantID: tenant.ID,
ContentID: contentB.ID,
ReporterID: reporter.ID,
Reason: "abuse",
Detail: "批量举报B",
Status: "pending",
}
models.ContentReportQuery.WithContext(ctx).Create(reportA, reportB)
err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{
ReportIDs: []int64{reportA.ID, reportB.ID},
Action: "approve",
ContentAction: "unpublish",
Reason: "集中处理",
})
So(err, ShouldBeNil)
reloadedA, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportA.ID)).First()
So(err, ShouldBeNil)
So(reloadedA.Status, ShouldEqual, "approved")
So(reloadedA.HandledBy, ShouldEqual, admin.ID)
So(reloadedA.HandledAction, ShouldEqual, "unpublish")
So(reloadedA.HandledReason, ShouldEqual, "集中处理")
So(reloadedA.HandledAt.IsZero(), ShouldBeFalse)
reloadedB, err := models.ContentReportQuery.WithContext(ctx).Where(models.ContentReportQuery.ID.Eq(reportB.ID)).First()
So(err, ShouldBeNil)
So(reloadedB.Status, ShouldEqual, "approved")
So(reloadedB.HandledAction, ShouldEqual, "unpublish")
contentReloadA, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentA.ID)).First()
So(err, ShouldBeNil)
So(contentReloadA.Status, ShouldEqual, consts.ContentStatusUnpublished)
contentReloadB, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(contentB.ID)).First()
So(err, ShouldBeNil)
So(contentReloadB.Status, ShouldEqual, consts.ContentStatusUnpublished)
})
Convey("should reject when report already handled", func() {
report := &models.ContentReport{
TenantID: tenant.ID,
ContentID: contentA.ID,
ReporterID: reporter.ID,
Reason: "other",
Detail: "已处理",
Status: "approved",
}
models.ContentReportQuery.WithContext(ctx).Create(report)
err := Super.BatchProcessContentReports(ctx, admin.ID, &super_dto.SuperContentReportBatchProcessForm{
ReportIDs: []int64{report.ID},
Action: "reject",
Reason: "重复提交",
})
So(err, ShouldNotBeNil)
var appErr *errorx.AppError
So(errors.As(err, &appErr), ShouldBeTrue)
So(appErr.Code, ShouldEqual, errorx.ErrStatusConflict.Code)
})
})
}
func (s *SuperTestSuite) Test_FinanceAnomalies() {
Convey("Finance Anomalies", s.T(), func() {
ctx := s.T().Context()

View File

@@ -392,6 +392,40 @@ const docTemplate = `{
}
}
},
"/super/v1/content-reports/process/batch": {
"post": {
"description": "Batch process content report records",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Content"
],
"summary": "Batch process content reports",
"parameters": [
{
"description": "Batch process form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperContentReportBatchProcessForm"
}
}
],
"responses": {
"200": {
"description": "Processed",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/content-reports/{id}/process": {
"post": {
"description": "Process a content report record",
@@ -8155,6 +8189,39 @@ const docTemplate = `{
}
}
},
"dto.SuperContentReportBatchProcessForm": {
"type": "object",
"required": [
"action",
"report_ids"
],
"properties": {
"action": {
"description": "Action 处理动作approve/reject。",
"type": "string",
"enum": [
"approve",
"reject"
]
},
"content_action": {
"description": "ContentAction 内容处置动作block/unpublish/ignore仅在 approve 时生效。",
"type": "string"
},
"reason": {
"description": "Reason 处理说明(可选,用于审计记录)。",
"type": "string"
},
"report_ids": {
"description": "ReportIDs 待处理举报ID列表。",
"type": "array",
"minItems": 1,
"items": {
"type": "integer"
}
}
}
},
"dto.SuperContentReportItem": {
"type": "object",
"properties": {

View File

@@ -386,6 +386,40 @@
}
}
},
"/super/v1/content-reports/process/batch": {
"post": {
"description": "Batch process content report records",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Content"
],
"summary": "Batch process content reports",
"parameters": [
{
"description": "Batch process form",
"name": "form",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SuperContentReportBatchProcessForm"
}
}
],
"responses": {
"200": {
"description": "Processed",
"schema": {
"type": "string"
}
}
}
}
},
"/super/v1/content-reports/{id}/process": {
"post": {
"description": "Process a content report record",
@@ -8149,6 +8183,39 @@
}
}
},
"dto.SuperContentReportBatchProcessForm": {
"type": "object",
"required": [
"action",
"report_ids"
],
"properties": {
"action": {
"description": "Action 处理动作approve/reject。",
"type": "string",
"enum": [
"approve",
"reject"
]
},
"content_action": {
"description": "ContentAction 内容处置动作block/unpublish/ignore仅在 approve 时生效。",
"type": "string"
},
"reason": {
"description": "Reason 处理说明(可选,用于审计记录)。",
"type": "string"
},
"report_ids": {
"description": "ReportIDs 待处理举报ID列表。",
"type": "array",
"minItems": 1,
"items": {
"type": "integer"
}
}
}
},
"dto.SuperContentReportItem": {
"type": "object",
"properties": {

View File

@@ -1421,6 +1421,30 @@ definitions:
- content_ids
- status
type: object
dto.SuperContentReportBatchProcessForm:
properties:
action:
description: Action 处理动作approve/reject
enum:
- approve
- reject
type: string
content_action:
description: ContentAction 内容处置动作block/unpublish/ignore仅在 approve 时生效。
type: string
reason:
description: Reason 处理说明(可选,用于审计记录)。
type: string
report_ids:
description: ReportIDs 待处理举报ID列表。
items:
type: integer
minItems: 1
type: array
required:
- action
- report_ids
type: object
dto.SuperContentReportItem:
properties:
content_id:
@@ -3596,6 +3620,28 @@ paths:
summary: Process content report
tags:
- Content
/super/v1/content-reports/process/batch:
post:
consumes:
- application/json
description: Batch process content report records
parameters:
- description: Batch process form
in: body
name: form
required: true
schema:
$ref: '#/definitions/dto.SuperContentReportBatchProcessForm'
produces:
- application/json
responses:
"200":
description: Processed
schema:
type: string
summary: Batch process content reports
tags:
- Content
/super/v1/contents:
get:
consumes:

View File

@@ -5,7 +5,7 @@
## 1) 总体结论
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、钱包流水与异常排查、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)、审计日志与系统配置、评论治理。
- **部分落地**内容治理(缺批量处置扩展)、创作者治理(缺提现审核联动与结算账户审批流)
- **部分落地**暂无
- **未落地**:暂无。
## 2) 按页面完成度(对照 2.x
@@ -51,9 +51,9 @@
- 缺口:无显著功能缺口。
### 2.9 创作者与成员审核 `/superadmin/creators`
- 状态:**部分完成**
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除。
- 缺口:结算账户审批流(若需要区分通过/驳回状态)
- 状态:**完成**
- 已有:创作者(租户)列表、状态更新、创作者申请审核、成员申请列表/审核、成员邀请创建、结算账户列表与删除、结算账户审核
- 缺口:无显著功能缺口
### 2.10 优惠券 `/superadmin/coupons`
- 状态:**已完成**
@@ -88,9 +88,8 @@
## 3) `/super/v1` 接口覆盖度概览
- **已具备**Auth、Tenants含成员审核/邀请、Users含钱包/通知/优惠券/实名/互动/内容消费、Contents含评论治理、Orders、Withdrawals、Finance流水/异常、Reports、Coupons列表/创建/编辑/发放/冻结/记录、Creators列表/申请/成员审核、Payout Accounts列表/删除、Assets列表/用量/删除、Notifications列表/群发/模板)。
- **缺失/待补**内容治理批量处置扩展(举报/下架/封禁等批量操作)
- **缺失/待补**
## 4) 建议的下一步(按优先级)
1. **内容批量处置扩展**:补齐内容/举报的批量下架、封禁与归档处理
2. **创作者治理补强**:结算账户审批流、提现审核联动流程。
暂无

View File

@@ -276,5 +276,19 @@ export const ContentService = {
reason
}
});
},
async batchProcessContentReports({ report_ids, action, content_action, reason } = {}) {
if (!Array.isArray(report_ids) || report_ids.length === 0) {
throw new Error('report_ids is required');
}
return requestJson('/super/v1/content-reports/process/batch', {
method: 'POST',
body: {
report_ids,
action,
content_action,
reason
}
});
}
};

View File

@@ -79,6 +79,7 @@ const reportHandledAtFrom = ref(null);
const reportHandledAtTo = ref(null);
const reportSortField = ref('created_at');
const reportSortOrder = ref(-1);
const selectedReports = ref([]);
const commentDeleteDialogVisible = ref(false);
const commentDeleteLoading = ref(false);
@@ -92,6 +93,13 @@ const reportProcessContentAction = ref('unpublish');
const reportProcessReason = ref('');
const reportProcessTarget = ref(null);
const reportBatchDialogVisible = ref(false);
const reportBatchSubmitting = ref(false);
const reportBatchAction = ref('approve');
const reportBatchContentAction = ref('unpublish');
const reportBatchReason = ref('');
const reportBatchTargetIDs = ref([]);
const reviewDialogVisible = ref(false);
const reviewSubmitting = ref(false);
const reviewAction = ref('approve');
@@ -176,6 +184,11 @@ function getContentID(row) {
return Number.isFinite(id) ? id : 0;
}
function getReportID(row) {
const id = Number(row?.id ?? 0);
return Number.isFinite(id) ? id : 0;
}
function formatDate(value) {
if (!value) return '-';
if (String(value).startsWith('0001-01-01')) return '-';
@@ -369,9 +382,12 @@ function getReportActionLabel(value) {
}
const selectedCount = computed(() => selectedContents.value.length);
const reportSelectedCount = computed(() => selectedReports.value.length);
const reviewTargetCount = computed(() => reviewTargetIDs.value.length);
const batchStatusTargetCount = computed(() => batchStatusTargetIDs.value.length);
const reportProcessNeedsContentAction = computed(() => reportProcessAction.value === 'approve');
const reportBatchTargetCount = computed(() => reportBatchTargetIDs.value.length);
const reportBatchNeedsContentAction = computed(() => reportBatchAction.value === 'approve');
async function loadContents() {
loading.value = true;
@@ -632,6 +648,7 @@ async function loadReports() {
sortOrder: reportSortOrder.value
});
reports.value = result.items;
selectedReports.value = [];
reportTotalRecords.value = result.total;
} catch (error) {
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载举报列表', life: 4000 });
@@ -699,6 +716,60 @@ async function confirmReportProcess() {
}
}
function openReportBatchDialog(action) {
if (!selectedReports.value.length) {
toast.add({ severity: 'warn', summary: '请先选择举报', detail: '至少选择 1 条举报进行处理', life: 3000 });
return;
}
const invalid = selectedReports.value.filter((row) => row?.status !== 'pending');
if (invalid.length) {
toast.add({ severity: 'warn', summary: '选择包含已处理举报', detail: '仅可批量处理待处理举报', life: 3000 });
return;
}
const ids = selectedReports.value.map((row) => getReportID(row)).filter((id) => id > 0);
if (!ids.length) {
toast.add({ severity: 'warn', summary: '选择无效', detail: '未识别到可处理的举报', life: 3000 });
return;
}
reportBatchTargetIDs.value = ids;
reportBatchAction.value = action || 'approve';
reportBatchContentAction.value = 'unpublish';
reportBatchReason.value = '';
reportBatchDialogVisible.value = true;
}
async function confirmReportBatchProcess() {
const ids = reportBatchTargetIDs.value.filter((id) => id > 0);
if (!ids.length) return;
const action = reportBatchAction.value;
const contentAction = action === 'approve' ? reportBatchContentAction.value : 'ignore';
const reason = reportBatchReason.value.trim();
if (action === 'reject' && !reason) {
toast.add({ severity: 'warn', summary: '请输入原因', detail: '驳回举报时需填写原因', life: 3000 });
return;
}
reportBatchSubmitting.value = true;
try {
await ContentService.batchProcessContentReports({
report_ids: ids,
action,
content_action: contentAction,
reason: reason || undefined
});
toast.add({ severity: 'success', summary: '已处理', detail: `已处理 ${ids.length} 条举报`, life: 3000 });
reportBatchDialogVisible.value = false;
reportBatchTargetIDs.value = [];
selectedReports.value = [];
await loadReports();
} catch (error) {
toast.add({ severity: 'error', summary: '处理失败', detail: error?.message || '无法处理举报', life: 4000 });
} finally {
reportBatchSubmitting.value = false;
}
}
const unpublishDialogVisible = ref(false);
const unpublishLoading = ref(false);
const unpublishItem = ref(null);
@@ -1072,7 +1143,12 @@ watch(
<span class="font-medium">举报列表</span>
<span class="text-muted-color">跨租户内容举报处理</span>
</div>
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadReports" :disabled="reportLoading" />
<div class="flex items-center gap-2">
<span class="text-sm text-muted-color">已选 {{ reportSelectedCount }} </span>
<Button label="批量通过" icon="pi pi-check" severity="success" :disabled="reportSelectedCount === 0" @click="openReportBatchDialog('approve')" />
<Button label="批量驳回" icon="pi pi-times" severity="danger" :disabled="reportSelectedCount === 0" @click="openReportBatchDialog('reject')" />
<Button label="刷新" icon="pi pi-refresh" severity="secondary" @click="loadReports" :disabled="reportLoading" />
</div>
</div>
<div class="flex flex-col gap-4">
@@ -1133,6 +1209,7 @@ watch(
<DataTable
:value="reports"
dataKey="id"
v-model:selection="selectedReports"
:loading="reportLoading"
lazy
:paginator="true"
@@ -1151,6 +1228,7 @@ watch(
scrollHeight="640px"
responsiveLayout="scroll"
>
<Column selectionMode="multiple" headerStyle="width: 3rem" />
<Column field="id" header="举报ID" sortable style="min-width: 8rem" />
<Column header="内容" style="min-width: 20rem">
<template #body="{ data }">
@@ -1326,6 +1404,34 @@ watch(
</template>
</Dialog>
<Dialog v-model:visible="reportBatchDialogVisible" :modal="true" :style="{ width: '560px' }">
<template #header>
<div class="flex items-center gap-2">
<span class="font-medium">批量处理举报</span>
<span class="text-muted-color"> {{ reportBatchTargetCount }} </span>
</div>
</template>
<div class="flex flex-col gap-4">
<div>
<label class="block font-medium mb-2">处理动作</label>
<Select v-model="reportBatchAction" :options="reportActionOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div v-if="reportBatchNeedsContentAction">
<label class="block font-medium mb-2">内容处置</label>
<Select v-model="reportBatchContentAction" :options="reportContentActionOptions" optionLabel="label" optionValue="value" class="w-full" />
</div>
<div>
<label class="block font-medium mb-2">处理说明</label>
<InputText v-model="reportBatchReason" placeholder="可选,便于审计与通知" class="w-full" />
</div>
<div class="text-sm text-muted-color">批量处理后会记录审计并视情况通知作者</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="reportBatchDialogVisible = false" :disabled="reportBatchSubmitting" />
<Button label="确认处理" icon="pi pi-check" severity="success" @click="confirmReportBatchProcess" :loading="reportBatchSubmitting" :disabled="reportBatchSubmitting || (reportBatchAction === 'reject' && !reportBatchReason.trim())" />
</template>
</Dialog>
<Dialog v-model:visible="commentDeleteDialogVisible" :modal="true" :style="{ width: '520px' }">
<template #header>
<div class="flex items-center gap-2">