358 lines
11 KiB
Vue
358 lines
11 KiB
Vue
<template>
|
||
<div class="aksrt-page-container">
|
||
<PageHeader
|
||
title="举报治理"
|
||
desc="统一维护公开举报入口与后台工单台,支持证据查看、状态流转和处罚语义联动。"
|
||
/>
|
||
|
||
<n-grid cols="1 s:1 m:2" :x-gap="16" :y-gap="16" responsive="screen">
|
||
<n-grid-item>
|
||
<n-card size="small" title="治理开关">
|
||
<n-space wrap>
|
||
<n-tag round :type="form.user_report_enabled ? 'success' : 'default'">
|
||
举报入口:{{ form.user_report_enabled ? '已启用' : '已关闭' }}
|
||
</n-tag>
|
||
<n-tag round :type="form.user_report_public_entry ? 'info' : 'default'">
|
||
公开入口:{{ form.user_report_public_entry ? '显示' : '隐藏' }}
|
||
</n-tag>
|
||
<n-tag round :type="moderationSummary.enabled ? 'success' : 'default'">
|
||
审核系统:{{ moderationSummary.enabled ? '已启用' : '未启用' }}
|
||
</n-tag>
|
||
<n-tag round type="warning">
|
||
待处理:{{ counts.pending }}
|
||
</n-tag>
|
||
<n-tag round type="success">
|
||
已解决:{{ counts.resolved }}
|
||
</n-tag>
|
||
</n-space>
|
||
</n-card>
|
||
</n-grid-item>
|
||
|
||
<n-grid-item>
|
||
<n-card size="small" title="前台说明">
|
||
<n-form :model="form" label-placement="top">
|
||
<n-form-item label="限制提示文案">
|
||
<n-input v-model:value="form.user_report_ban_notice_text" maxlength="80" />
|
||
</n-form-item>
|
||
<n-form-item label="举报分类字典">
|
||
<n-input
|
||
v-model:value="form.user_report_categories"
|
||
type="textarea"
|
||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||
placeholder="spam|垃圾信息 abuse|人身攻击"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
</n-card>
|
||
</n-grid-item>
|
||
</n-grid>
|
||
|
||
<n-card size="small" title="入口配置" class="aksrt-report-page__card">
|
||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||
<n-form-item label="启用举报/禁封结构">
|
||
<n-switch v-model:value="form.user_report_enabled" />
|
||
</n-form-item>
|
||
<n-form-item label="公开主页展示举报入口">
|
||
<n-switch v-model:value="form.user_report_public_entry" :disabled="!form.user_report_enabled" />
|
||
</n-form-item>
|
||
<n-form-item label="公开显示限制提示">
|
||
<n-switch v-model:value="form.user_report_public_notice" :disabled="!form.user_report_enabled" />
|
||
</n-form-item>
|
||
<div class="aksrt-form-actions">
|
||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
||
</div>
|
||
</n-form>
|
||
</n-card>
|
||
|
||
<n-card size="small" title="举报工单台" class="aksrt-report-page__card">
|
||
<n-space vertical :size="12">
|
||
<n-grid cols="1 s:2 m:5" :x-gap="12" :y-gap="12" responsive="screen">
|
||
<n-grid-item>
|
||
<n-input v-model:value="filters.keyword" clearable placeholder="搜索举报人、目标、原因" />
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-select v-model:value="filters.target_type" :options="targetTypeOptions" placeholder="目标类型" />
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-date-picker v-model:value="filters.dateRange" type="daterange" clearable style="width: 100%;" />
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-button type="primary" block @click="handleSearch">筛选工单</n-button>
|
||
</n-grid-item>
|
||
</n-grid>
|
||
|
||
<n-data-table
|
||
:columns="columns"
|
||
:data="reports"
|
||
:loading="reportLoading"
|
||
:bordered="false"
|
||
:pagination="pagination"
|
||
:row-key="(row) => row.id"
|
||
/>
|
||
</n-space>
|
||
</n-card>
|
||
|
||
<ReportDetailDrawer
|
||
v-model:show="drawerVisible"
|
||
:report-id="currentReportId"
|
||
@updated="loadReports"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
|
||
import api from '@/api'
|
||
import PageHeader from '@/components/PageHeader.vue'
|
||
import ReportDetailDrawer from './components/ReportDetailDrawer.vue'
|
||
|
||
const message = useMessage()
|
||
|
||
const loading = ref(true)
|
||
const saving = ref(false)
|
||
const reportLoading = ref(false)
|
||
const reports = ref([])
|
||
const drawerVisible = ref(false)
|
||
const currentReportId = ref(null)
|
||
|
||
const form = reactive({
|
||
user_report_enabled: false,
|
||
user_report_public_entry: true,
|
||
user_report_public_notice: true,
|
||
user_report_ban_notice_text: '当前账号处于限制状态,部分互动能力已关闭。',
|
||
user_report_categories: 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉',
|
||
})
|
||
|
||
const moderationSummary = reactive({
|
||
enabled: false,
|
||
})
|
||
|
||
const filters = reactive({
|
||
keyword: '',
|
||
status: '',
|
||
target_type: '',
|
||
dateRange: null,
|
||
})
|
||
|
||
const counts = reactive({
|
||
pending: 0,
|
||
reviewing: 0,
|
||
resolved: 0,
|
||
dismissed: 0,
|
||
total: 0,
|
||
})
|
||
|
||
const pager = reactive({
|
||
page: 1,
|
||
pageSize: 20,
|
||
itemCount: 0,
|
||
})
|
||
|
||
const optionsState = reactive({
|
||
statuses: [],
|
||
targets: [],
|
||
})
|
||
|
||
const statusOptions = computed(() => optionsState.statuses.length
|
||
? [{ label: '全部状态', value: '' }, ...optionsState.statuses]
|
||
: [
|
||
{ label: '全部状态', value: '' },
|
||
{ label: '待处理', value: 'pending' },
|
||
{ label: '处理中', value: 'reviewing' },
|
||
{ label: '已解决', value: 'resolved' },
|
||
{ label: '已驳回', value: 'dismissed' },
|
||
])
|
||
|
||
const targetTypeOptions = computed(() => optionsState.targets.length
|
||
? [{ label: '全部类型', value: '' }, ...optionsState.targets]
|
||
: [
|
||
{ label: '全部类型', value: '' },
|
||
{ label: '文章', value: 'post' },
|
||
{ label: '评论', value: 'comment' },
|
||
{ label: '用户', value: 'user' },
|
||
])
|
||
|
||
const pagination = computed(() => ({
|
||
page: pager.page,
|
||
pageSize: pager.pageSize,
|
||
itemCount: pager.itemCount,
|
||
showSizePicker: true,
|
||
pageSizes: [10, 20, 50],
|
||
onUpdatePage: (page) => {
|
||
pager.page = page
|
||
loadReports()
|
||
},
|
||
onUpdatePageSize: (pageSize) => {
|
||
pager.pageSize = pageSize
|
||
pager.page = 1
|
||
loadReports()
|
||
},
|
||
}))
|
||
|
||
const columns = [
|
||
{ title: 'ID', key: 'id', width: 70 },
|
||
{
|
||
title: '举报人',
|
||
key: 'reporter',
|
||
width: 160,
|
||
render: (row) => row.reporter?.display_name || '-',
|
||
},
|
||
{
|
||
title: '目标',
|
||
key: 'target',
|
||
minWidth: 220,
|
||
ellipsis: { tooltip: true },
|
||
render: (row) => row.target?.title || row.target?.display_name || row.target?.excerpt || `目标 #${row.target_id}`,
|
||
},
|
||
{
|
||
title: '状态',
|
||
key: 'status',
|
||
width: 100,
|
||
render: (row) => {
|
||
const typeMap = { pending: 'warning', reviewing: 'info', resolved: 'success', dismissed: 'default' }
|
||
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status)
|
||
},
|
||
},
|
||
{
|
||
title: '分类',
|
||
key: 'category',
|
||
width: 120,
|
||
render: (row) => row.category || '-',
|
||
},
|
||
{
|
||
title: '处理记录',
|
||
key: 'note_count',
|
||
width: 90,
|
||
},
|
||
{
|
||
title: '提交时间',
|
||
key: 'created_at',
|
||
width: 170,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 120,
|
||
render: (row) => h(NSpace, { size: 'small' }, () => [
|
||
h(NButton, {
|
||
size: 'tiny',
|
||
type: 'primary',
|
||
quaternary: true,
|
||
onClick: () => openDrawer(row.id),
|
||
}, () => '查看'),
|
||
]),
|
||
},
|
||
]
|
||
|
||
onMounted(async () => {
|
||
await load()
|
||
})
|
||
|
||
function extractData(response) {
|
||
return response?.data ?? response ?? {}
|
||
}
|
||
|
||
function formatDateValue(value) {
|
||
const date = new Date(value)
|
||
const year = date.getFullYear()
|
||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||
const day = `${date.getDate()}`.padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
}
|
||
|
||
function buildReportParams() {
|
||
const params = {
|
||
page: pager.page,
|
||
per_page: pager.pageSize,
|
||
}
|
||
if (filters.keyword) params.keyword = filters.keyword
|
||
if (filters.status) params.status = filters.status
|
||
if (filters.target_type) params.target_type = filters.target_type
|
||
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
|
||
params.date_from = formatDateValue(filters.dateRange[0])
|
||
params.date_to = formatDateValue(filters.dateRange[1])
|
||
}
|
||
return params
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const [schemaRes, moderationRes] = await Promise.all([
|
||
api.getSchemaPage('users', 'report'),
|
||
api.getPermissionsModerationSettings(),
|
||
])
|
||
const schemaData = extractData(schemaRes)
|
||
const moderationData = extractData(moderationRes)
|
||
|
||
form.user_report_enabled = !!schemaData.user_report_enabled
|
||
form.user_report_public_entry = schemaData.user_report_public_entry !== false
|
||
form.user_report_public_notice = schemaData.user_report_public_notice !== false
|
||
form.user_report_ban_notice_text = schemaData.user_report_ban_notice_text || '当前账号处于限制状态,部分互动能力已关闭。'
|
||
form.user_report_categories = schemaData.user_report_categories || 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉'
|
||
moderationSummary.enabled = !!moderationData.enabled
|
||
|
||
await loadReports()
|
||
} catch (error) {
|
||
message.error(error?.message || '加载举报治理页失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadReports() {
|
||
reportLoading.value = true
|
||
try {
|
||
const res = await api.request('/users/reports', { params: buildReportParams() })
|
||
const data = res?.data || {}
|
||
reports.value = data.items || []
|
||
pager.itemCount = data.total || 0
|
||
Object.assign(counts, data.counts || {})
|
||
optionsState.statuses = data.options?.statuses || []
|
||
optionsState.targets = data.options?.targets || []
|
||
} catch (error) {
|
||
message.error(error?.message || '加载举报工单失败')
|
||
} finally {
|
||
reportLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
saving.value = true
|
||
try {
|
||
await api.saveSchemaPage('users', 'report', {
|
||
user_report_enabled: form.user_report_enabled,
|
||
user_report_public_entry: form.user_report_public_entry,
|
||
user_report_public_notice: form.user_report_public_notice,
|
||
user_report_ban_notice_text: form.user_report_ban_notice_text,
|
||
user_report_categories: form.user_report_categories,
|
||
})
|
||
message.success('举报治理配置已保存')
|
||
} catch (error) {
|
||
message.error(error?.message || '保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pager.page = 1
|
||
loadReports()
|
||
}
|
||
|
||
function openDrawer(reportId) {
|
||
currentReportId.value = reportId
|
||
drawerVisible.value = true
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.aksrt-report-page__card {
|
||
margin-top: 16px;
|
||
}
|
||
</style>
|