feat(admin): host the ban/report settings page in the SPA schema
The users-interaction sidebar entry 禁封/举报 was a redirect stub; it is
now a schema-driven settings page replicating the reference theme
field-for-field:
- user_ban_s / user_report_s switches, ban_preset_reason and
ban_appeal_keys as repeatable group rows ({t}), ban_appeal_desc and
user_report_desc HTML textareas with the reference default copy, and
user_report_img_s — keys, labels, help texts, defaults and ordering
all mirrored in SchemaUsers.php and the SPA report.js schema
- UserRuntime::ban_settings()/report_settings() and
BanAppeal::settings() read the new group-array values with fallback
to the previous line/comma formats
- delete the self-built SPA queue pages (UsersBan, UsersReport,
ReportDetailDrawer, UsersMessages) that the reference theme does not
have; legacy /users/report bookmarks still redirect to the native page
- contract tests: lock the schema keys/labels, the schemaPage mount and
the deleted-file state; drop reads of removed components
This commit is contained in:
@@ -3,7 +3,8 @@ import { renderFaIcon, schemaPage, customPage } from './helpers'
|
|||||||
/**
|
/**
|
||||||
* Users SPA keeps config-shaped pages.
|
* Users SPA keeps config-shaped pages.
|
||||||
* Message / referral-code queues live under users.php native menus;
|
* Message / referral-code queues live under users.php native menus;
|
||||||
* 封禁/举报 gets a sidebar entry that lands on the native ban page.
|
* 禁封/举报 is a config page (schema section "report"); ticket queues
|
||||||
|
* (举报&禁封申诉) stay on the native WP users.php page.
|
||||||
*/
|
*/
|
||||||
export const usersSection = {
|
export const usersSection = {
|
||||||
key: 'users',
|
key: 'users',
|
||||||
@@ -129,19 +130,15 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'wechat-template',
|
section: 'wechat-template',
|
||||||
}),
|
}),
|
||||||
customPage({
|
schemaPage({
|
||||||
key: 'users-ban',
|
key: 'users-ban',
|
||||||
name: 'UsersBan',
|
name: 'UsersBan',
|
||||||
label: '封禁/举报',
|
label: '禁封/举报',
|
||||||
route: '/users/ban',
|
route: '/users/ban',
|
||||||
component: 'NativeOpsRedirect',
|
title: '禁封/举报',
|
||||||
status: 'available',
|
desc: '禁封功能、举报功能、预置原因、申诉资料与举报说明配置;举报与申诉工单在 WordPress 用户菜单处理。',
|
||||||
title: '封禁/举报',
|
|
||||||
desc: '举报工单、禁封限制与申诉审核,统一在 WordPress 用户菜单处理。',
|
|
||||||
iconClass: 'fa fas fa-gavel',
|
|
||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'ban',
|
section: 'report',
|
||||||
nativeAdminPath: 'users.php?page=aksrt_user_ban',
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader
|
|
||||||
title="禁封管理"
|
|
||||||
desc="查看和管理用户封禁、限制状态,支持按用户 ID 查询和解除限制。"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-grid cols="1 s:2 m:3" :x-gap="16" :y-gap="16" responsive="screen">
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="总限制记录" :value="stats.total" />
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="当前生效" :value="stats.active">
|
|
||||||
<template #suffix>
|
|
||||||
<n-tag v-if="stats.active > 0" type="error" size="small">{{ stats.active }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="封禁账号" :value="stats.banned">
|
|
||||||
<template #suffix>
|
|
||||||
<n-tag v-if="stats.banned > 0" type="error" size="small">严重</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
|
|
||||||
<!-- Search by user ID -->
|
|
||||||
<n-card size="small" title="查询用户限制" style="margin-top:16px;">
|
|
||||||
<n-space>
|
|
||||||
<n-input-number v-model:value="searchUserId" placeholder="用户 ID" :min="1" style="width:160px;" />
|
|
||||||
<n-button type="primary" @click="searchRestrictions" :loading="searching">查询</n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- User restrictions list -->
|
|
||||||
<n-card v-if="userRestrictions.length" size="small" :title="`用户 #${searchUserId} 的限制记录`" style="margin-top:16px;">
|
|
||||||
<n-data-table
|
|
||||||
:columns="columns"
|
|
||||||
:data="userRestrictions"
|
|
||||||
:loading="searching"
|
|
||||||
:bordered="false"
|
|
||||||
/>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Add restriction form -->
|
|
||||||
<n-card size="small" title="添加限制" style="margin-top:16px;">
|
|
||||||
<n-form :model="addForm" label-placement="left" label-width="100" class="aksrt-form">
|
|
||||||
<n-form-item label="用户 ID">
|
|
||||||
<n-input-number v-model:value="addForm.user_id" :min="1" style="width:200px;" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="限制类型">
|
|
||||||
<n-select v-model:value="addForm.type" :options="restrictionTypes" style="width:200px;" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="限制范围">
|
|
||||||
<n-select v-model:value="addForm.scope" :options="scopeOptions" style="width:200px;" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="原因">
|
|
||||||
<n-input v-model:value="addForm.reason" type="textarea" :rows="2" placeholder="限制原因(必填)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="持续时间">
|
|
||||||
<n-select v-model:value="addForm.duration" :options="durationOptions" style="width:200px;" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item>
|
|
||||||
<n-button type="error" @click="addRestriction" :loading="adding" :disabled="!addForm.user_id || !addForm.reason">
|
|
||||||
添加限制
|
|
||||||
</n-button>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { h, reactive, ref, onMounted } from 'vue'
|
|
||||||
import { NButton, NTag, NSpace, useMessage } from 'naive-ui'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const searching = ref(false)
|
|
||||||
const adding = ref(false)
|
|
||||||
const searchUserId = ref(null)
|
|
||||||
const userRestrictions = ref([])
|
|
||||||
const stats = reactive({ total: 0, active: 0, banned: 0 })
|
|
||||||
|
|
||||||
const addForm = reactive({
|
|
||||||
user_id: null,
|
|
||||||
type: 'mute',
|
|
||||||
scope: 'global',
|
|
||||||
reason: '',
|
|
||||||
duration: '7d',
|
|
||||||
})
|
|
||||||
|
|
||||||
const restrictionTypes = [
|
|
||||||
{ label: '警告', value: 'warn' },
|
|
||||||
{ label: '禁言', value: 'mute' },
|
|
||||||
{ label: '停权', value: 'suspend' },
|
|
||||||
{ label: '封禁', value: 'ban' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const scopeOptions = [
|
|
||||||
{ label: '全局', value: 'global' },
|
|
||||||
{ label: '仅评论', value: 'comment' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const durationOptions = [
|
|
||||||
{ label: '1 天', value: '1d' },
|
|
||||||
{ label: '3 天', value: '3d' },
|
|
||||||
{ label: '7 天', value: '7d' },
|
|
||||||
{ label: '30 天', value: '30d' },
|
|
||||||
{ label: '永久', value: 'permanent' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ title: 'ID', key: 'id', width: 60 },
|
|
||||||
{
|
|
||||||
title: '类型', key: 'type', width: 80,
|
|
||||||
render: (row) => h(NTag, { type: getTypeTag(row.type), size: 'small' }, () => getTypeLabel(row.type)),
|
|
||||||
},
|
|
||||||
{ title: '原因', key: 'reason', ellipsis: { tooltip: true } },
|
|
||||||
{ title: '生效时间', key: 'created_at', width: 160 },
|
|
||||||
{ title: '到期时间', key: 'expires_at', width: 160 },
|
|
||||||
{
|
|
||||||
title: '状态', key: 'status', width: 80,
|
|
||||||
render: (row) => {
|
|
||||||
const active = !row.expires_at || new Date(row.expires_at) > new Date()
|
|
||||||
return h(NTag, { type: active ? 'error' : 'default', size: 'small' }, () => active ? '生效中' : '已过期')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作', key: 'actions', width: 100,
|
|
||||||
render: (row) => h(NButton, { size: 'tiny', type: 'error', onClick: () => removeRestriction(row) }, () => '解除'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function getTypeTag(type) {
|
|
||||||
const map = { warn: 'info', mute: 'warning', suspend: 'warning', ban: 'error' }
|
|
||||||
return map[type] || 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeLabel(type) {
|
|
||||||
const map = { warn: '警告', mute: '禁言', suspend: '停权', ban: '封禁' }
|
|
||||||
return map[type] || type
|
|
||||||
}
|
|
||||||
|
|
||||||
function durationToExpiresAt(duration) {
|
|
||||||
if (!duration || duration === 'permanent') return ''
|
|
||||||
const days = parseInt(duration, 10)
|
|
||||||
if (!Number.isFinite(days) || days <= 0) return ''
|
|
||||||
return new Date(Date.now() + days * 86400000).toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStats() {
|
|
||||||
try {
|
|
||||||
const res = await api.request('/users/restrictions/stats')
|
|
||||||
const data = res.data || res
|
|
||||||
stats.total = data.total || 0
|
|
||||||
stats.active = data.active || 0
|
|
||||||
stats.banned = data.banned || 0
|
|
||||||
} catch (e) {
|
|
||||||
// Stats may not be available yet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchRestrictions() {
|
|
||||||
if (!searchUserId.value) {
|
|
||||||
message.warning('请输入用户 ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
searching.value = true
|
|
||||||
try {
|
|
||||||
const res = await api.request(`/users/${searchUserId.value}/restrictions`)
|
|
||||||
const data = res.data || res
|
|
||||||
userRestrictions.value = data.restrictions || []
|
|
||||||
if (userRestrictions.value.length === 0) {
|
|
||||||
message.info('该用户无限制记录')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
message.error('查询失败: ' + (e.message || '未知错误'))
|
|
||||||
} finally {
|
|
||||||
searching.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addRestriction() {
|
|
||||||
if (!addForm.user_id || !addForm.reason) {
|
|
||||||
message.warning('请填写用户 ID 和原因')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
adding.value = true
|
|
||||||
try {
|
|
||||||
await api.request(`/users/${addForm.user_id}/restrict`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
type: addForm.type,
|
|
||||||
scope: addForm.scope,
|
|
||||||
reason: addForm.reason,
|
|
||||||
expires_at: durationToExpiresAt(addForm.duration),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
message.success('限制已添加')
|
|
||||||
addForm.reason = ''
|
|
||||||
if (searchUserId.value === addForm.user_id) {
|
|
||||||
await searchRestrictions()
|
|
||||||
}
|
|
||||||
await loadStats()
|
|
||||||
} catch (e) {
|
|
||||||
message.error('添加失败: ' + (e.message || '未知错误'))
|
|
||||||
} finally {
|
|
||||||
adding.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeRestriction(row) {
|
|
||||||
try {
|
|
||||||
await api.request(`/users/restrictions/${row.id}`, { method: 'DELETE' })
|
|
||||||
message.success('限制已解除')
|
|
||||||
userRestrictions.value = userRestrictions.value.filter((r) => r.id !== row.id)
|
|
||||||
await loadStats()
|
|
||||||
} catch (e) {
|
|
||||||
message.error('解除失败: ' + (e.message || '未知错误'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadStats)
|
|
||||||
</script>
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader
|
|
||||||
title="消息中心"
|
|
||||||
desc="管理站内通知、系统消息和用户消息数据。"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-grid cols="1 s:2 m:4" :x-gap="16" :y-gap="16" responsive="screen">
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="全部消息" :value="stats.total" />
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="未读消息" :value="stats.unread">
|
|
||||||
<template #suffix>
|
|
||||||
<n-tag v-if="stats.unread > 0" type="warning" size="small">{{ stats.unread }}</n-tag>
|
|
||||||
</template>
|
|
||||||
</n-statistic>
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="今日新增" :value="stats.today" />
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-statistic label="系统通知" :value="stats.system" />
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
|
|
||||||
<n-card size="small" title="消息列表" style="margin-top:16px;">
|
|
||||||
<template #header-extra>
|
|
||||||
<n-space>
|
|
||||||
<n-select
|
|
||||||
v-model:value="filters.type"
|
|
||||||
placeholder="消息类型"
|
|
||||||
:options="typeOptions"
|
|
||||||
clearable
|
|
||||||
style="width:160px;"
|
|
||||||
@update:value="handleFilterChange"
|
|
||||||
/>
|
|
||||||
<n-select
|
|
||||||
v-model:value="filters.read"
|
|
||||||
placeholder="已读状态"
|
|
||||||
:options="readOptions"
|
|
||||||
clearable
|
|
||||||
style="width:120px;"
|
|
||||||
@update:value="handleFilterChange"
|
|
||||||
/>
|
|
||||||
<n-button @click="loadMessages" :loading="loading">刷新</n-button>
|
|
||||||
<n-button type="primary" @click="markAllRead" :loading="markingAll">全部已读</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n-data-table
|
|
||||||
:columns="columns"
|
|
||||||
:data="messages"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
:row-class-name="rowClassName"
|
|
||||||
remote
|
|
||||||
@update:page="handlePageChange"
|
|
||||||
@update:page-size="handlePageSizeChange"
|
|
||||||
/>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-drawer v-model:show="showDetail" :width="480">
|
|
||||||
<n-drawer-content :title="detailMessage?.title || '消息详情'">
|
|
||||||
<template v-if="detailMessage">
|
|
||||||
<n-descriptions bordered :column="1" label-placement="left">
|
|
||||||
<n-descriptions-item label="类型">
|
|
||||||
<n-tag :type="getTypeTag(detailMessage.type)" size="small">{{ getTypeLabel(detailMessage.type) }}</n-tag>
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="状态">
|
|
||||||
<n-tag :type="detailMessage.is_read ? 'success' : 'warning'" size="small">
|
|
||||||
{{ detailMessage.is_read ? '已读' : '未读' }}
|
|
||||||
</n-tag>
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="用户">{{ detailMessage.user_name || '-' }}</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="时间">{{ detailMessage.created_at || '-' }}</n-descriptions-item>
|
|
||||||
</n-descriptions>
|
|
||||||
|
|
||||||
<div class="aksrt-message-detail">
|
|
||||||
<h4>{{ detailMessage.title }}</h4>
|
|
||||||
<p>{{ detailMessage.description || detailMessage.content || '暂无详细内容' }}</p>
|
|
||||||
<a
|
|
||||||
v-if="detailMessage.target_url"
|
|
||||||
:href="detailMessage.target_url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
查看目标
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<n-space>
|
|
||||||
<n-button @click="showDetail = false">关闭</n-button>
|
|
||||||
<n-button v-if="detailMessage && !detailMessage.is_read" type="primary" @click="markCurrentRead">
|
|
||||||
标记已读
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
|
||||||
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const markingAll = ref(false)
|
|
||||||
const showDetail = ref(false)
|
|
||||||
const detailMessage = ref(null)
|
|
||||||
const messages = ref([])
|
|
||||||
const typeStats = ref([])
|
|
||||||
const stats = reactive({ total: 0, unread: 0, today: 0, system: 0 })
|
|
||||||
const filters = reactive({ type: null, read: null })
|
|
||||||
const pagination = reactive({ page: 1, pageSize: 20, itemCount: 0, pageCount: 1, showSizePicker: true, pageSizes: [20, 50, 100] })
|
|
||||||
|
|
||||||
const readOptions = [
|
|
||||||
{ label: '未读', value: '0' },
|
|
||||||
{ label: '已读', value: '1' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const typeOptions = computed(() => typeStats.value.map((item) => ({
|
|
||||||
label: getTypeLabel(item.type),
|
|
||||||
value: item.type,
|
|
||||||
})))
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '类型',
|
|
||||||
key: 'type',
|
|
||||||
width: 120,
|
|
||||||
render: (row) => h(NTag, { type: getTypeTag(row.type), size: 'small' }, () => getTypeLabel(row.type)),
|
|
||||||
},
|
|
||||||
{ title: '标题', key: 'title', ellipsis: { tooltip: true } },
|
|
||||||
{ title: '用户', key: 'user_name', width: 140, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '时间', key: 'created_at', width: 180 },
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
key: 'is_read',
|
|
||||||
width: 90,
|
|
||||||
render: (row) => h(NTag, { type: row.is_read ? 'success' : 'warning', size: 'small' }, () => (row.is_read ? '已读' : '未读')),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'actions',
|
|
||||||
width: 140,
|
|
||||||
render: (row) => h(NSpace, { size: 'small' }, () => [
|
|
||||||
h(NButton, { size: 'tiny', onClick: () => openDetail(row) }, () => '查看'),
|
|
||||||
!row.is_read ? h(NButton, { size: 'tiny', type: 'primary', onClick: () => markOneRead(row) }, () => '已读') : null,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function getTypeTag(type) {
|
|
||||||
const map = {
|
|
||||||
notice: 'info',
|
|
||||||
system: 'info',
|
|
||||||
comment_reply: 'success',
|
|
||||||
comment_approved: 'success',
|
|
||||||
download_ready: 'primary',
|
|
||||||
verify_email: 'info',
|
|
||||||
pending_comments: 'warning',
|
|
||||||
}
|
|
||||||
return map[type] || 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeLabel(type) {
|
|
||||||
const map = {
|
|
||||||
notice: '系统通知',
|
|
||||||
system: '系统通知',
|
|
||||||
comment_reply: '评论回复',
|
|
||||||
comment_approved: '评论审核',
|
|
||||||
download_ready: '下载就绪',
|
|
||||||
verify_email: '邮箱验证',
|
|
||||||
pending_comments: '待审评论',
|
|
||||||
}
|
|
||||||
return map[type] || type || '通知'
|
|
||||||
}
|
|
||||||
|
|
||||||
function rowClassName(row) {
|
|
||||||
return row.is_read ? '' : 'aksrt-row--unread'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMessages() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const response = await api.getNotificationOverview({
|
|
||||||
page: pagination.page,
|
|
||||||
per_page: pagination.pageSize,
|
|
||||||
...(filters.type ? { type: filters.type } : {}),
|
|
||||||
...(filters.read !== null ? { is_read: filters.read } : {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const payload = response.data || {}
|
|
||||||
const summary = payload.summary || {}
|
|
||||||
const pager = payload.pagination || {}
|
|
||||||
|
|
||||||
messages.value = Array.isArray(payload.recent) ? payload.recent : []
|
|
||||||
typeStats.value = Array.isArray(payload.types) ? payload.types : []
|
|
||||||
|
|
||||||
stats.total = Number(summary.total || payload.total || 0)
|
|
||||||
stats.unread = Number(summary.unread || payload.unread || 0)
|
|
||||||
stats.today = Number(summary.today || payload.today || 0)
|
|
||||||
stats.system = Number(summary.system || payload.system || 0)
|
|
||||||
|
|
||||||
pagination.itemCount = Number(pager.total_items || messages.value.length)
|
|
||||||
pagination.pageCount = Number(pager.total_pages || 1)
|
|
||||||
pagination.page = Number(pager.page || pagination.page)
|
|
||||||
pagination.pageSize = Number(pager.per_page || pagination.pageSize)
|
|
||||||
} catch (error) {
|
|
||||||
message.error('加载消息失败: ' + (error.message || '未知错误'))
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDetail(row) {
|
|
||||||
detailMessage.value = row
|
|
||||||
showDetail.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markOneRead(row) {
|
|
||||||
try {
|
|
||||||
await api.markNotificationRead(row.id)
|
|
||||||
row.is_read = true
|
|
||||||
stats.unread = Math.max(0, stats.unread - 1)
|
|
||||||
if (detailMessage.value && detailMessage.value.id === row.id) {
|
|
||||||
detailMessage.value.is_read = true
|
|
||||||
}
|
|
||||||
message.success('已标记为已读')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markCurrentRead() {
|
|
||||||
if (detailMessage.value) {
|
|
||||||
await markOneRead(detailMessage.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markAllRead() {
|
|
||||||
markingAll.value = true
|
|
||||||
try {
|
|
||||||
await api.markAllNotificationsRead()
|
|
||||||
messages.value = messages.value.map((item) => ({ ...item, is_read: true }))
|
|
||||||
if (detailMessage.value) {
|
|
||||||
detailMessage.value = { ...detailMessage.value, is_read: true }
|
|
||||||
}
|
|
||||||
stats.unread = 0
|
|
||||||
message.success('全部已读')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '操作失败')
|
|
||||||
} finally {
|
|
||||||
markingAll.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFilterChange() {
|
|
||||||
pagination.page = 1
|
|
||||||
loadMessages()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageChange(page) {
|
|
||||||
pagination.page = page
|
|
||||||
loadMessages()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePageSizeChange(pageSize) {
|
|
||||||
pagination.pageSize = pageSize
|
|
||||||
pagination.page = 1
|
|
||||||
loadMessages()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadMessages)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-row--unread {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-message-detail {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-message-detail h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-message-detail p {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.7;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
<template>
|
|
||||||
<n-drawer :show="show" :width="720" @update:show="$emit('update:show', $event)">
|
|
||||||
<n-drawer-content title="举报详情" closable>
|
|
||||||
<n-spin :show="loading">
|
|
||||||
<template v-if="detail.report">
|
|
||||||
<n-descriptions bordered :column="1" size="small" label-placement="left">
|
|
||||||
<n-descriptions-item label="举报人">
|
|
||||||
{{ detail.report.reporter?.display_name || `用户 #${detail.report.reporter?.id || 0}` }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="举报目标">
|
|
||||||
{{ targetTitle }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="目标类型">
|
|
||||||
{{ detail.report.target_type }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="分类">
|
|
||||||
{{ detail.report.category || '-' }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="当前状态">
|
|
||||||
{{ detail.report.status }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
<n-descriptions-item label="提交时间">
|
|
||||||
{{ detail.report.created_at || '-' }}
|
|
||||||
</n-descriptions-item>
|
|
||||||
</n-descriptions>
|
|
||||||
|
|
||||||
<div class="aksrt-report-drawer__section">
|
|
||||||
<div class="aksrt-report-drawer__title">证据链</div>
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
{{ detail.report.reason || '暂无举报原因' }}
|
|
||||||
</n-alert>
|
|
||||||
<n-space class="aksrt-report-drawer__meta" wrap>
|
|
||||||
<n-tag size="small" round :bordered="false">
|
|
||||||
目标 ID:{{ detail.report.target_id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag
|
|
||||||
v-if="detail.report.target?.status"
|
|
||||||
size="small"
|
|
||||||
type="warning"
|
|
||||||
round
|
|
||||||
:bordered="false"
|
|
||||||
>
|
|
||||||
目标状态:{{ detail.report.target.status }}
|
|
||||||
</n-tag>
|
|
||||||
<a
|
|
||||||
v-if="detail.report.target?.url"
|
|
||||||
:href="detail.report.target.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
class="aksrt-report-drawer__link"
|
|
||||||
>
|
|
||||||
打开目标内容
|
|
||||||
</a>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-report-drawer__section">
|
|
||||||
<div class="aksrt-report-drawer__title">处理记录</div>
|
|
||||||
<div v-if="detail.report.notes?.length" class="aksrt-report-drawer__timeline">
|
|
||||||
<div v-for="(entry, index) in detail.report.notes" :key="index" class="aksrt-report-drawer__timeline-item">
|
|
||||||
<div class="aksrt-report-drawer__timeline-head">
|
|
||||||
<strong>{{ entry.status || '-' }}</strong>
|
|
||||||
<span>{{ entry.timestamp || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
<p>{{ entry.note || '无备注' }}</p>
|
|
||||||
<small>{{ entry.operator_name || '系统' }} {{ entry.effect || '' }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<n-empty v-else description="暂无处理记录" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-report-drawer__section">
|
|
||||||
<div class="aksrt-report-drawer__title">处理工单</div>
|
|
||||||
<n-space vertical :size="12">
|
|
||||||
<n-select v-model:value="form.status" :options="detail.options?.statuses || []" />
|
|
||||||
<n-select v-model:value="form.action_taken" :options="detail.options?.actions || []" />
|
|
||||||
<n-input
|
|
||||||
v-model:value="form.note"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="记录处理依据、证据补充或处罚说明"
|
|
||||||
/>
|
|
||||||
<n-space>
|
|
||||||
<n-button type="primary" :loading="submitting" @click="handleSubmit">提交处理</n-button>
|
|
||||||
<n-button @click="loadDetail">刷新详情</n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<n-empty v-else description="暂无举报详情" />
|
|
||||||
</n-spin>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import api from '@/api'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: { type: Boolean, default: false },
|
|
||||||
reportId: { type: [Number, String], default: null },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'updated'])
|
|
||||||
const message = useMessage()
|
|
||||||
const loading = ref(false)
|
|
||||||
const submitting = ref(false)
|
|
||||||
const detail = ref({ report: null, options: { statuses: [], actions: [] } })
|
|
||||||
const form = reactive({
|
|
||||||
status: 'reviewing',
|
|
||||||
action_taken: 'none',
|
|
||||||
note: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const targetTitle = computed(() => {
|
|
||||||
const target = detail.value?.report?.target
|
|
||||||
if (!target) {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
return target.title || target.display_name || target.excerpt || `目标 #${target.id || 0}`
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => [props.show, props.reportId],
|
|
||||||
async ([visible, id]) => {
|
|
||||||
if (visible && id) {
|
|
||||||
await loadDetail()
|
|
||||||
}
|
|
||||||
if (!visible) {
|
|
||||||
detail.value = { report: null, options: { statuses: [], actions: [] } }
|
|
||||||
resetForm()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
form.status = 'reviewing'
|
|
||||||
form.action_taken = 'none'
|
|
||||||
form.note = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadDetail() {
|
|
||||||
if (!props.reportId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await api.request(`/users/reports/${props.reportId}`)
|
|
||||||
detail.value = res?.data || { report: null, options: { statuses: [], actions: [] } }
|
|
||||||
form.status = detail.value?.report?.status || 'reviewing'
|
|
||||||
form.action_taken = 'none'
|
|
||||||
form.note = ''
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error?.message || '加载举报详情失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!props.reportId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
await api.request(`/users/reports/${props.reportId}/process`, {
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
status: form.status,
|
|
||||||
action_taken: form.action_taken,
|
|
||||||
note: form.note,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
message.success('举报工单已处理')
|
|
||||||
await loadDetail()
|
|
||||||
emit('updated')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error?.message || '处理失败')
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-report-drawer__section {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__title {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__meta {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__link {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__timeline {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__timeline-item {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__timeline-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__timeline-item p {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
color: #111827;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-report-drawer__timeline-item small {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -40,10 +40,6 @@ function createRouteMeta(page) {
|
|||||||
meta.section = page.section
|
meta.section = page.section
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page.nativeAdminPath) {
|
|
||||||
meta.nativeAdminPath = page.nativeAdminPath
|
|
||||||
}
|
|
||||||
|
|
||||||
if (page.component === 'DeferredPage') {
|
if (page.component === 'DeferredPage') {
|
||||||
meta.deferred = true
|
meta.deferred = true
|
||||||
meta.deferredProps = {
|
meta.deferredProps = {
|
||||||
|
|||||||
@@ -1,57 +1,71 @@
|
|||||||
|
// 字段集合、顺序、文案、默认值 1:1 对齐参考主题"用户&互动/禁封/举报"
|
||||||
export const reportSchema = {
|
export const reportSchema = {
|
||||||
key: 'users.report',
|
key: 'users.report',
|
||||||
title: '禁封/举报',
|
title: '禁封/举报',
|
||||||
icon: 'fa fas fa-flag',
|
icon: 'fa fas fa-ban',
|
||||||
description: '配置举报入口、公开限制提示和后续治理分类,为封禁/举报流程预留稳定结构。',
|
description: '禁封功能、举报入口、预置原因、申诉资料与举报说明配置。',
|
||||||
defaults: {
|
defaults: {
|
||||||
user_report_enabled: false,
|
user_ban_s: true,
|
||||||
user_report_public_entry: true,
|
user_report_s: true,
|
||||||
user_report_public_notice: true,
|
ban_preset_reason: [
|
||||||
user_report_ban_notice_text: '当前账号处于限制状态,部分互动能力已关闭。',
|
{ t: '发布色情、违法内容' },
|
||||||
user_report_categories: 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉',
|
{ t: '存在欺诈骗钱行为' },
|
||||||
ban_preset_reason: 'spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作',
|
{ t: '骚扰他人' },
|
||||||
ban_appeal_desc: '',
|
{ t: '涉嫌侵权' },
|
||||||
ban_appeal_keys: '',
|
{ t: '发布垃圾广告信息' },
|
||||||
|
],
|
||||||
|
ban_appeal_desc: '<div>为了良好的交流环境,申诉请阅读以下事项</div>\n<li>首次违规将拉入小黑屋7天</li>\n<li>3次违规后将直接禁封账号</li>\n<li>违规情节严重将无法提交申诉</li>\n<li>申诉提交后约2-3个工作日进行审核</li>\n<li>如有其它疑问请与客服联系</li>',
|
||||||
|
ban_appeal_keys: [
|
||||||
|
{ t: '真实姓名' },
|
||||||
|
{ t: '邮箱' },
|
||||||
|
{ t: '手机号' },
|
||||||
|
],
|
||||||
user_report_img_s: true,
|
user_report_img_s: true,
|
||||||
user_report_desc: '',
|
user_report_desc: '<div>举报说明:</div>\n<li>请确保举报的内容属实</li>\n<li>我们将会在收到举报后2个工作日内进行处理</li>\n<li>举报属实我们将根据社区管理条例对该用户进行封号处理</li>\n<li>如有其它疑问请与客服联系</li>',
|
||||||
},
|
},
|
||||||
groups: [
|
groups: [
|
||||||
{
|
{
|
||||||
key: 'entry',
|
key: 'main',
|
||||||
title: '前台入口',
|
title: '',
|
||||||
icon: 'fa fas fa-sign-in-alt',
|
|
||||||
fields: [
|
|
||||||
{ key: 'user_report_enabled', label: '启用举报/禁封结构', type: 'switch', default: false },
|
|
||||||
{ key: 'user_report_public_entry', label: '公开主页展示举报入口', type: 'switch', default: true, dependsOn: { key: 'user_report_enabled', value: true } },
|
|
||||||
{ key: 'user_report_public_notice', label: '公开显示限制提示', type: 'switch', default: true, dependsOn: { key: 'user_report_enabled', value: true } },
|
|
||||||
{ key: 'user_report_ban_notice_text', label: '限制提示文案', type: 'input', default: '当前账号处于限制状态,部分互动能力已关闭。', dependsOn: { key: 'user_report_public_notice', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'taxonomy',
|
|
||||||
title: '举报分类',
|
|
||||||
icon: 'fa fas fa-list',
|
|
||||||
fields: [
|
|
||||||
{ key: 'user_report_categories', label: '举报分类', type: 'textarea', default: 'spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉', help: '每行一个分类:slug|显示名称', dependsOn: { key: 'user_report_enabled', value: true } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'ban',
|
|
||||||
title: '禁封设置',
|
|
||||||
icon: 'fa fas fa-ban',
|
icon: 'fa fas fa-ban',
|
||||||
fields: [
|
fields: [
|
||||||
{ key: 'ban_preset_reason', label: '预设封禁原因', type: 'textarea', default: 'spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作', help: '每行一条:slug|显示名称', dependsOn: { key: 'user_report_enabled', value: true } },
|
{ key: 'user_ban_s', label: '禁封功能', type: 'switch', default: true, help: '启用后可以设置用户禁封,即小黑屋和封号的功能' },
|
||||||
{ key: 'ban_appeal_desc', label: '申诉说明', type: 'textarea', default: '', dependsOn: { key: 'user_report_enabled', value: true } },
|
{ key: 'user_report_s', label: '举报功能', type: 'switch', default: true, help: '前台显示举报按钮,用户可以举报其它不良信息', dependsOn: { key: 'user_ban_s', value: true } },
|
||||||
{ key: 'ban_appeal_keys', label: '申诉关键词', type: 'input', default: '', help: '逗号分隔', dependsOn: { key: 'user_report_enabled', value: true } },
|
{
|
||||||
],
|
key: 'ban_preset_reason',
|
||||||
},
|
label: '预置原因',
|
||||||
{
|
type: 'group',
|
||||||
key: 'image',
|
button_title: '添加原因',
|
||||||
title: '图片举报',
|
help: '举报/禁封的时候可选择的原因',
|
||||||
icon: 'fa fas fa-image',
|
default: [
|
||||||
fields: [
|
{ t: '发布色情、违法内容' },
|
||||||
{ key: 'user_report_img_s', label: '启用图片举报', type: 'switch', default: true, dependsOn: { key: 'user_report_enabled', value: true } },
|
{ t: '存在欺诈骗钱行为' },
|
||||||
{ key: 'user_report_desc', label: '举报说明', type: 'textarea', default: '', dependsOn: { key: 'user_report_img_s', value: true } },
|
{ t: '骚扰他人' },
|
||||||
|
{ t: '涉嫌侵权' },
|
||||||
|
{ t: '发布垃圾广告信息' },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 't', label: '原因', type: 'input', default: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'ban_appeal_desc', label: '申诉说明', type: 'textarea', rows: 5, help: '用户申诉时候显示的内容(支持HTML代码,请注意代码规范)', default: '<div>为了良好的交流环境,申诉请阅读以下事项</div>\n<li>首次违规将拉入小黑屋7天</li>\n<li>3次违规后将直接禁封账号</li>\n<li>违规情节严重将无法提交申诉</li>\n<li>申诉提交后约2-3个工作日进行审核</li>\n<li>如有其它疑问请与客服联系</li>' },
|
||||||
|
{
|
||||||
|
key: 'ban_appeal_keys',
|
||||||
|
label: '申诉所需资料',
|
||||||
|
type: 'group',
|
||||||
|
button_title: '添加申诉资料',
|
||||||
|
help: '用户提交申诉时候需要提供的资料;此处的信息收集,仅用于记录和申诉审核',
|
||||||
|
default: [
|
||||||
|
{ t: '真实姓名' },
|
||||||
|
{ t: '邮箱' },
|
||||||
|
{ t: '手机号' },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 't', label: '资料项', type: 'input', default: '' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ key: 'user_report_img_s', label: '举报可上传图片', type: 'switch', default: true, help: '举报时允许上传图片进行举证(最多上传3张)', dependsOn: { key: 'user_report_s', value: true } },
|
||||||
|
{ key: 'user_report_desc', label: '举报说明', type: 'textarea', rows: 5, help: '举报时候显示的内容(支持HTML代码,请注意代码规范)', dependsOn: { key: 'user_report_s', value: true }, default: '<div>举报说明:</div>\n<li>请确保举报的内容属实</li>\n<li>我们将会在收到举报后2个工作日内进行处理</li>\n<li>举报属实我们将根据社区管理条例对该用户进行封号处理</li>\n<li>如有其它疑问请与客服联系</li>' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -612,26 +612,48 @@ class SchemaUsers {
|
|||||||
'report' => [
|
'report' => [
|
||||||
'key' => 'report',
|
'key' => 'report',
|
||||||
'title' => '禁封/举报',
|
'title' => '禁封/举报',
|
||||||
'version' => '1.0.0',
|
'version' => '1.1.0',
|
||||||
'option_key' => 'aksrt_settings',
|
'option_key' => 'aksrt_settings',
|
||||||
'groups' => [
|
'groups' => [
|
||||||
['key' => 'entry', 'title' => '前台入口', 'fields' => [
|
// 字段集合、顺序、文案、默认值 1:1 对齐参考主题"用户&互动/禁封举报"
|
||||||
['key' => 'user_report_enabled', 'label' => '启用举报/禁封结构', 'type' => 'switch', 'default' => false],
|
['key' => 'main', 'title' => '', 'fields' => [
|
||||||
['key' => 'user_report_public_entry', 'label' => '公开主页展示举报入口', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
['key' => 'user_ban_s', 'label' => '禁封功能', 'type' => 'switch', 'default' => true, 'help' => '启用后可以设置用户禁封,即小黑屋和封号的功能'],
|
||||||
['key' => 'user_report_public_notice', 'label' => '公开显示限制提示', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
['key' => 'user_report_s', 'label' => '举报功能', 'type' => 'switch', 'default' => true, 'help' => '前台显示举报按钮,用户可以举报其它不良信息', 'dependsOn' => ['key' => 'user_ban_s', 'value' => true]],
|
||||||
['key' => 'user_report_ban_notice_text', 'label' => '限制提示文案', 'type' => 'input', 'default' => '当前账号处于限制状态,部分互动能力已关闭。', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
[
|
||||||
]],
|
'key' => 'ban_preset_reason',
|
||||||
['key' => 'taxonomy', 'title' => '举报分类', 'fields' => [
|
'label' => '预置原因',
|
||||||
['key' => 'user_report_categories', 'label' => '举报分类', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\ncopyright|版权投诉"],
|
'type' => 'group',
|
||||||
]],
|
'button_title' => '添加原因',
|
||||||
['key' => 'ban', 'title' => '禁封设置', 'fields' => [
|
'help' => '举报/禁封的时候可选择的原因',
|
||||||
['key' => 'ban_preset_reason', 'label' => '预设封禁原因', 'type' => 'textarea', 'default' => "spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作", 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
'default' => [
|
||||||
['key' => 'ban_appeal_desc', 'label' => '申诉说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
['t' => '发布色情、违法内容'],
|
||||||
['key' => 'ban_appeal_keys', 'label' => '申诉关键词', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
['t' => '存在欺诈骗钱行为'],
|
||||||
]],
|
['t' => '骚扰他人'],
|
||||||
['key' => 'image', 'title' => '图片举报', 'fields' => [
|
['t' => '涉嫌侵权'],
|
||||||
['key' => 'user_report_img_s', 'label' => '启用图片举报', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
['t' => '发布垃圾广告信息'],
|
||||||
['key' => 'user_report_desc', 'label' => '举报说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'user_report_enabled', 'value' => true]],
|
],
|
||||||
|
'fields' => [
|
||||||
|
['key' => 't', 'label' => '原因', 'type' => 'input', 'default' => ''],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['key' => 'ban_appeal_desc', 'label' => '申诉说明', 'type' => 'textarea', 'rows' => 5, 'help' => '用户申诉时候显示的内容(支持HTML代码,请注意代码规范)', 'default' => "<div>为了良好的交流环境,申诉请阅读以下事项</div>\n<li>首次违规将拉入小黑屋7天</li>\n<li>3次违规后将直接禁封账号</li>\n<li>违规情节严重将无法提交申诉</li>\n<li>申诉提交后约2-3个工作日进行审核</li>\n<li>如有其它疑问请与客服联系</li>"],
|
||||||
|
[
|
||||||
|
'key' => 'ban_appeal_keys',
|
||||||
|
'label' => '申诉所需资料',
|
||||||
|
'type' => 'group',
|
||||||
|
'button_title' => '添加申诉资料',
|
||||||
|
'help' => '用户提交申诉时候需要提供的资料;此处的信息收集,仅用于记录和申诉审核',
|
||||||
|
'default' => [
|
||||||
|
['t' => '真实姓名'],
|
||||||
|
['t' => '邮箱'],
|
||||||
|
['t' => '手机号'],
|
||||||
|
],
|
||||||
|
'fields' => [
|
||||||
|
['key' => 't', 'label' => '资料项', 'type' => 'input', 'default' => ''],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
['key' => 'user_report_img_s', 'label' => '举报可上传图片', 'type' => 'switch', 'default' => true, 'help' => '举报时允许上传图片进行举证(最多上传3张)', 'dependsOn' => ['key' => 'user_report_s', 'value' => true]],
|
||||||
|
['key' => 'user_report_desc', 'label' => '举报说明', 'type' => 'textarea', 'rows' => 5, 'help' => '举报时候显示的内容(支持HTML代码,请注意代码规范)', 'dependsOn' => ['key' => 'user_report_s', 'value' => true], 'default' => "<div>举报说明:</div>\n<li>请确保举报的内容属实</li>\n<li>我们将会在收到举报后2个工作日内进行处理</li>\n<li>举报属实我们将根据社区管理条例对该用户进行封号处理</li>\n<li>如有其它疑问请与客服联系</li>"],
|
||||||
]],
|
]],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class UserRuntime {
|
|||||||
}, $medal['items_by_slug'])),
|
}, $medal['items_by_slug'])),
|
||||||
],
|
],
|
||||||
'report' => [
|
'report' => [
|
||||||
'enabled' => !empty($settings['user_report_enabled']),
|
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? false),
|
||||||
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
|
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
|
||||||
'public_notice' => !isset($settings['user_report_public_notice']) || !empty($settings['user_report_public_notice']),
|
'public_notice' => !isset($settings['user_report_public_notice']) || !empty($settings['user_report_public_notice']),
|
||||||
'ban_notice_text' => $ban_notice_text !== '' ? $ban_notice_text : __('当前账号处于限制状态,部分互动能力已关闭。', 'aksrt'),
|
'ban_notice_text' => $ban_notice_text !== '' ? $ban_notice_text : __('当前账号处于限制状态,部分互动能力已关闭。', 'aksrt'),
|
||||||
@@ -200,21 +200,44 @@ class UserRuntime {
|
|||||||
*/
|
*/
|
||||||
public static function ban_settings(): array {
|
public static function ban_settings(): array {
|
||||||
$settings = Settings::get_all();
|
$settings = Settings::get_all();
|
||||||
$reasons_raw = (string) ($settings['ban_preset_reason'] ?? "spam|垃圾信息\nabuse|人身攻击\nviolation|违规操作");
|
|
||||||
|
// 预置原因:group 数组 [{t:...}](对齐参考主题)或旧版每行一条文本
|
||||||
|
$reasons_raw = $settings['ban_preset_reason'] ?? null;
|
||||||
$reasons = [];
|
$reasons = [];
|
||||||
foreach (preg_split('/\r\n|\r|\n/', $reasons_raw) as $line) {
|
if (is_array($reasons_raw)) {
|
||||||
$line = trim($line);
|
foreach ($reasons_raw as $row) {
|
||||||
if ($line === '') {
|
$text = trim((string) ($row['t'] ?? ''));
|
||||||
continue;
|
if ($text !== '') {
|
||||||
|
$reasons[] = ['slug' => sanitize_key($text), 'name' => sanitize_text_field($text)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach (preg_split('/\r\n|\r|\n/', (string) $reasons_raw) as $line) {
|
||||||
|
$line = trim((string) $line);
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts = explode('|', $line, 2);
|
||||||
|
$reasons[] = [
|
||||||
|
'slug' => sanitize_key($parts[0]),
|
||||||
|
'name' => sanitize_text_field($parts[1] ?? $parts[0]),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
$parts = explode('|', $line, 2);
|
|
||||||
$reasons[] = [
|
|
||||||
'slug' => sanitize_key($parts[0]),
|
|
||||||
'name' => sanitize_text_field($parts[1] ?? $parts[0]),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
$appeal_keys_raw = (string) ($settings['ban_appeal_keys'] ?? '');
|
|
||||||
$appeal_keys = array_filter(array_map('trim', explode(',', $appeal_keys_raw)));
|
// 申诉资料:group 数组 [{t:...}] 或旧版逗号分隔文本
|
||||||
|
$keys_raw = $settings['ban_appeal_keys'] ?? null;
|
||||||
|
$appeal_keys = [];
|
||||||
|
if (is_array($keys_raw)) {
|
||||||
|
foreach ($keys_raw as $row) {
|
||||||
|
$text = trim((string) ($row['t'] ?? ''));
|
||||||
|
if ($text !== '') {
|
||||||
|
$appeal_keys[] = sanitize_text_field($text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$appeal_keys = array_filter(array_map('trim', explode(',', (string) $keys_raw)));
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'reasons' => $reasons,
|
'reasons' => $reasons,
|
||||||
@@ -229,9 +252,9 @@ class UserRuntime {
|
|||||||
public static function report_settings(): array {
|
public static function report_settings(): array {
|
||||||
$settings = Settings::get_all();
|
$settings = Settings::get_all();
|
||||||
return [
|
return [
|
||||||
'enabled' => !empty($settings['user_report_enabled']),
|
'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? false),
|
||||||
'public_entry' => !empty($settings['user_report_public_entry']),
|
'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']),
|
||||||
'public_notice' => !empty($settings['user_report_public_notice']),
|
'public_notice' => !isset($settings['user_report_public_notice']) || !empty($settings['user_report_public_notice']),
|
||||||
'ban_notice_text' => sanitize_text_field((string) ($settings['user_report_ban_notice_text'] ?? '')),
|
'ban_notice_text' => sanitize_text_field((string) ($settings['user_report_ban_notice_text'] ?? '')),
|
||||||
'img_enabled' => !empty($settings['user_report_img_s']),
|
'img_enabled' => !empty($settings['user_report_img_s']),
|
||||||
'desc' => wp_kses_post((string) ($settings['user_report_desc'] ?? '')),
|
'desc' => wp_kses_post((string) ($settings['user_report_desc'] ?? '')),
|
||||||
|
|||||||
+10
-3
@@ -34,10 +34,17 @@ class BanAppeal {
|
|||||||
*/
|
*/
|
||||||
public static function settings(): array {
|
public static function settings(): array {
|
||||||
$all = class_exists('\AKSRT\Settings\Settings') ? (array) \AKSRT\Settings\Settings::get_all() : [];
|
$all = class_exists('\AKSRT\Settings\Settings') ? (array) \AKSRT\Settings\Settings::get_all() : [];
|
||||||
$keys_raw = (string) ($all['ban_appeal_keys'] ?? '');
|
// 申诉资料:group 数组 [{t:...}](对齐参考主题)或旧版逗号分隔文本
|
||||||
$keys = array_values(array_filter(array_map('trim', preg_split('/[,,\n]/', $keys_raw) ?: [])));
|
$keys_raw = $all['ban_appeal_keys'] ?? null;
|
||||||
|
if (is_array($keys_raw)) {
|
||||||
|
$keys = array_values(array_filter(array_map(static function ($row): string {
|
||||||
|
return trim((string) ($row['t'] ?? ''));
|
||||||
|
}, $keys_raw)));
|
||||||
|
} else {
|
||||||
|
$keys = array_values(array_filter(array_map('trim', preg_split('/[,,\n]/', (string) $keys_raw) ?: [])));
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'enabled' => !isset($all['user_report_enabled']) || !empty($all['user_report_enabled']),
|
'enabled' => (bool) ($all['user_report_s'] ?? (isset($all['user_report_enabled']) ? $all['user_report_enabled'] : true)),
|
||||||
'desc' => (string) ($all['ban_appeal_desc'] ?? ''),
|
'desc' => (string) ($all['ban_appeal_desc'] ?? ''),
|
||||||
'keys' => $keys,
|
'keys' => $keys,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -103,8 +103,7 @@ test('asset notifications are absent and interaction notification hooks are pres
|
|||||||
const notifications = read('inc/modules/user-notifications.php');
|
const notifications = read('inc/modules/user-notifications.php');
|
||||||
const settings = read('src/Settings/Integration.php');
|
const settings = read('src/Settings/Integration.php');
|
||||||
const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue');
|
const adminSettings = read('assets/admin/src/pages/content/NotificationCenterSettings.vue');
|
||||||
const messageAdmin = read('assets/admin/src/pages/users/UsersMessages.vue');
|
const combined = [notifications, settings, adminSettings].join('\n');
|
||||||
const combined = [notifications, settings, adminSettings, messageAdmin].join('\n');
|
|
||||||
|
|
||||||
for (const retired of [
|
for (const retired of [
|
||||||
'asset_points_increase',
|
'asset_points_increase',
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const usersMenus = read('src/Admin/Native/UsersMenus.php')
|
|||||||
const mailNotifications = read('src/Settings/MailNotifications.php')
|
const mailNotifications = read('src/Settings/MailNotifications.php')
|
||||||
const home = read('src/User/Home.php')
|
const home = read('src/User/Home.php')
|
||||||
const commentItem = read('inc/helpers/comments/item.php')
|
const commentItem = read('inc/helpers/comments/item.php')
|
||||||
const usersBan = read('assets/admin/src/pages/users/UsersBan.vue')
|
|
||||||
const functions = read('functions.php')
|
const functions = read('functions.php')
|
||||||
const uninstall = read('uninstall.php')
|
const uninstall = read('uninstall.php')
|
||||||
|
|
||||||
@@ -71,15 +70,44 @@ test('restriction class supports lifting and kicks sessions on ban', () => {
|
|||||||
test('ban admin REST endpoints stay in sync with the SPA client', () => {
|
test('ban admin REST endpoints stay in sync with the SPA client', () => {
|
||||||
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrictions'/)
|
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrictions'/)
|
||||||
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrict'/)
|
assert.match(userController, /'\/users\/\(\?P<user_id>\\d\+\)\/restrict'/)
|
||||||
assert.match(usersBan, /\/users\/\$\{addForm\.user_id\}\/restrict`/)
|
// The self-built SPA ban-manager page is removed: ban enforcement lives on
|
||||||
assert.ok(!usersBan.includes('/restrictions`, {'), 'SPA must not POST to the unregistered /restrictions endpoint')
|
// the native reference-theme page, and the SPA hosts the settings schema.
|
||||||
assert.match(usersBan, /expires_at: durationToExpiresAt\(addForm\.duration\)/)
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersBan.vue')), 'self-built SPA ban page must stay deleted')
|
||||||
for (const t of ["value: 'warn'", "value: 'mute'", "value: 'suspend'", "value: 'ban'"]) {
|
assert.ok(!fs.existsSync(path.join(root, 'assets/admin/src/pages/users/UsersReport.vue')), 'self-built SPA report queue must stay deleted')
|
||||||
assert.ok(usersBan.includes(t), `SPA restriction type missing: ${t}`)
|
})
|
||||||
|
|
||||||
|
test('ban/report settings page mirrors the reference theme schema', () => {
|
||||||
|
const schemaPhp = read('src/Admin/Schema/SchemaUsers.php')
|
||||||
|
const schemaJs = read('assets/admin/src/schema/sections/users/report.js')
|
||||||
|
const sections = read('assets/admin/src/constants/sections/users.js')
|
||||||
|
|
||||||
|
// SPA 侧边栏“用户互动 → 禁封/举报”挂载为 schema 设置页(非跳转页)
|
||||||
|
assert.match(sections, /schemaPage\(\{\s*\n\s*key: 'users-ban',/)
|
||||||
|
assert.match(sections, /label: '禁封\/举报'/)
|
||||||
|
assert.match(sections, /section: 'report'/)
|
||||||
|
assert.ok(!sections.includes("component: 'NativeOpsRedirect'"), 'ban page must not be a redirect stub')
|
||||||
|
|
||||||
|
// 字段键 1:1 对齐参考主题:禁封/举报开关、预置原因、申诉说明、申诉资料、图片举报、举报说明
|
||||||
|
for (const key of ['user_ban_s', 'user_report_s', 'ban_preset_reason', 'ban_appeal_desc', 'ban_appeal_keys', 'user_report_img_s', 'user_report_desc']) {
|
||||||
|
assert.ok(schemaPhp.includes(`'${key}'`), `PHP schema missing reference key: ${key}`)
|
||||||
|
assert.ok(schemaJs.includes(key), `SPA schema missing reference key: ${key}`)
|
||||||
}
|
}
|
||||||
for (const dead of ["value: 'muted'", "value: 'restricted'", "value: 'banned'"]) {
|
// 参考主题文案与 group 结构
|
||||||
assert.ok(!usersBan.includes(dead), `invalid SPA restriction type: ${dead}`)
|
for (const label of ['禁封功能', '举报功能', '预置原因', '申诉说明', '申诉所需资料', '举报可上传图片', '举报说明']) {
|
||||||
|
assert.ok(schemaJs.includes(`label: '${label}'`), `SPA schema missing reference label: ${label}`)
|
||||||
}
|
}
|
||||||
|
assert.match(schemaJs, /type: 'group'/)
|
||||||
|
assert.match(schemaJs, /添加原因/)
|
||||||
|
assert.match(schemaJs, /添加申诉资料/)
|
||||||
|
|
||||||
|
// 运行时读取兼容 group 数组 [{t}] 并保留旧格式回退
|
||||||
|
const runtime = read('src/Settings/UserRuntime.php')
|
||||||
|
assert.match(runtime, /ban_settings\(\): array/)
|
||||||
|
assert.match(runtime, /report_settings\(\): array/)
|
||||||
|
assert.match(runtime, /user_report_s/)
|
||||||
|
const appeal = read('src/User/BanAppeal.php')
|
||||||
|
assert.match(appeal, /ban_appeal_keys/)
|
||||||
|
assert.match(appeal, /ban_appeal_desc/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('ban appeal approval actually lifts restrictions', () => {
|
test('ban appeal approval actually lifts restrictions', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user