|
|
|
|
@@ -0,0 +1,299 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<a-breadcrumb class="breadcrumb">
|
|
|
|
|
<a-breadcrumb-item>履约安全与风控</a-breadcrumb-item>
|
|
|
|
|
<a-breadcrumb-item>违规举报工单处理</a-breadcrumb-item>
|
|
|
|
|
</a-breadcrumb>
|
|
|
|
|
|
|
|
|
|
<a-card class="general-card" title="不良违规行为举报与风控处置中心">
|
|
|
|
|
<template #extra>
|
|
|
|
|
<a-space>
|
|
|
|
|
<a-button @click="fetchData">
|
|
|
|
|
<template #icon><icon-refresh /></template>
|
|
|
|
|
刷新工单
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<a-alert type="warning" style="margin-bottom: 16px">
|
|
|
|
|
违规处理原则:认真核验用户提交的违规描述与聊天凭证截图。如属实存在涉黄、涉赌、诱导线下脱离平台私下交易、辱骂挑衅或虚假核销等不良行为,应从严予以警告、扣减信用分或下架处罚;如凭证不足或属误报,请予以驳回。
|
|
|
|
|
</a-alert>
|
|
|
|
|
|
|
|
|
|
<!-- 筛选栏 -->
|
|
|
|
|
<a-row :gutter="16" style="margin-bottom: 16px">
|
|
|
|
|
<a-col :span="6">
|
|
|
|
|
<a-select v-model="filterStatus" placeholder="按工单处理状态筛选" allow-clear>
|
|
|
|
|
<a-option value="">全部状态</a-option>
|
|
|
|
|
<a-option value="待处理">待处理</a-option>
|
|
|
|
|
<a-option value="已核实处罚">已核实处罚</a-option>
|
|
|
|
|
<a-option value="已驳回">已驳回</a-option>
|
|
|
|
|
</a-select>
|
|
|
|
|
</a-col>
|
|
|
|
|
<a-col :span="8">
|
|
|
|
|
<a-input
|
|
|
|
|
v-model="searchKeyword"
|
|
|
|
|
placeholder="搜索举报人ID/被举报人/违规原因"
|
|
|
|
|
allow-clear
|
|
|
|
|
>
|
|
|
|
|
<template #prefix><icon-search /></template>
|
|
|
|
|
</a-input>
|
|
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
|
|
|
|
|
<a-table
|
|
|
|
|
:data="filteredList"
|
|
|
|
|
:loading="loading"
|
|
|
|
|
:pagination="{ pageSize: 10, showTotal: true }"
|
|
|
|
|
row-key="id"
|
|
|
|
|
>
|
|
|
|
|
<template #columns>
|
|
|
|
|
<a-table-column title="工单号" data-index="id" :width="150">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<span class="code-font">{{ record.id }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="举报人" :width="120">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<span class="code-font">UID: {{ record.reporterId }}</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="被举报对象" :width="170">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-weight: 600">{{ record.targetName || '未知对象' }}</div>
|
|
|
|
|
<div style="font-size: 12px; color: var(--color-text-3)">
|
|
|
|
|
{{ record.targetType }} (ID: {{ record.targetId }})
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="违规类型" :width="150">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-tag color="red">{{ record.reason }}</a-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="详情描述与举报陈述" :width="260">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div class="desc-text">{{ record.description || '举报人未提供文字陈述' }}</div>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="线索凭证截图" :width="150">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div v-if="parseEvidence(record.evidence).length > 0">
|
|
|
|
|
<a-image-preview-group>
|
|
|
|
|
<a-space wrap :size="4">
|
|
|
|
|
<a-image
|
|
|
|
|
v-for="(img, idx) in parseEvidence(record.evidence)"
|
|
|
|
|
:key="idx"
|
|
|
|
|
:src="img"
|
|
|
|
|
width="38"
|
|
|
|
|
height="38"
|
|
|
|
|
fit="cover"
|
|
|
|
|
style="border-radius: 4px; border: 1px solid var(--color-border-2)"
|
|
|
|
|
/>
|
|
|
|
|
</a-space>
|
|
|
|
|
</a-image-preview-group>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else style="color: var(--color-text-4); font-size: 12px">无上传截图</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="提交时间" data-index="createdAt" :width="160" />
|
|
|
|
|
<a-table-column title="工单状态" :width="120">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-tag :color="getStatusColor(record.status)">
|
|
|
|
|
{{ record.status }}
|
|
|
|
|
</a-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="处理结果与备注" :width="200">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<div v-if="record.handleResult" style="font-size: 12px">
|
|
|
|
|
<div style="font-weight: 600">{{ record.handleResult }}</div>
|
|
|
|
|
<div style="color: var(--color-text-3)">{{ record.handleNote }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else style="color: var(--color-text-4); font-size: 12px">待人工介入处置</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
<a-table-column title="操作" :width="150" fixed="right" align="center">
|
|
|
|
|
<template #cell="{ record }">
|
|
|
|
|
<a-space v-if="record.status === '待处理'">
|
|
|
|
|
<a-button
|
|
|
|
|
type="primary"
|
|
|
|
|
status="danger"
|
|
|
|
|
size="small"
|
|
|
|
|
@click="openHandleModal(record, 'punish')"
|
|
|
|
|
>
|
|
|
|
|
违规处分
|
|
|
|
|
</a-button>
|
|
|
|
|
<a-button
|
|
|
|
|
type="outline"
|
|
|
|
|
size="small"
|
|
|
|
|
@click="openHandleModal(record, 'reject')"
|
|
|
|
|
>
|
|
|
|
|
驳回
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-space>
|
|
|
|
|
<span v-else style="color: var(--color-text-3); font-size: 12px">已结案</span>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table-column>
|
|
|
|
|
</template>
|
|
|
|
|
</a-table>
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<!-- 举报处置对话框 -->
|
|
|
|
|
<a-modal
|
|
|
|
|
v-model:visible="handleModalVisible"
|
|
|
|
|
:title="handleAction === 'punish' ? '举报属实 - 违规处分判定' : '驳回违规举报工单'"
|
|
|
|
|
@ok="submitHandle"
|
|
|
|
|
@cancel="handleModalVisible = false"
|
|
|
|
|
>
|
|
|
|
|
<a-form :model="handleForm" layout="vertical">
|
|
|
|
|
<a-form-item label="被举报对象">
|
|
|
|
|
<a-input :model-value="`${currentRecord?.targetName || ''} (ID: ${currentRecord?.targetId || ''})`" readonly />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item label="违规原因">
|
|
|
|
|
<a-input :model-value="currentRecord?.reason || ''" readonly />
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item v-if="handleAction === 'punish'" label="处分措施" required>
|
|
|
|
|
<a-radio-group v-model="handleForm.penalty">
|
|
|
|
|
<a-radio value="warning">严重警告并扣除信誉分 (扣 0.2)</a-radio>
|
|
|
|
|
<a-radio value="punish_only">仅下发违规整改通知</a-radio>
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
<a-form-item
|
|
|
|
|
:label="handleAction === 'punish' ? '处置备注与依据' : '驳回原因说明'"
|
|
|
|
|
required
|
|
|
|
|
>
|
|
|
|
|
<a-textarea
|
|
|
|
|
v-model="handleForm.note"
|
|
|
|
|
:placeholder="handleAction === 'punish' ? '例如:经核验聊天记录属实存在诱导私下交易行为,从严扣分警告' : '例如:线索证据不足,聊天记录未发现明显违规事实'"
|
|
|
|
|
:max-length="200"
|
|
|
|
|
show-word-limit
|
|
|
|
|
/>
|
|
|
|
|
</a-form-item>
|
|
|
|
|
</a-form>
|
|
|
|
|
</a-modal>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
import { ref, computed, onMounted } from 'vue';
|
|
|
|
|
import axios from 'axios';
|
|
|
|
|
import { Message } from '@arco-design/web-vue';
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
const rawList = ref<any[]>([]);
|
|
|
|
|
const filterStatus = ref('');
|
|
|
|
|
const searchKeyword = ref('');
|
|
|
|
|
|
|
|
|
|
const handleModalVisible = ref(false);
|
|
|
|
|
const currentRecord = ref<any>(null);
|
|
|
|
|
const handleAction = ref<'punish' | 'reject'>('punish');
|
|
|
|
|
const handleForm = ref({
|
|
|
|
|
penalty: 'warning',
|
|
|
|
|
note: '',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.get('/api/reports');
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
rawList.value = Array.isArray(res.data) ? res.data : (res.data.data || []);
|
|
|
|
|
}
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error('获取举报工单列表失败');
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filteredList = computed(() => {
|
|
|
|
|
return rawList.value.filter((item) => {
|
|
|
|
|
const matchStatus = !filterStatus.value || item.status === filterStatus.value;
|
|
|
|
|
const kw = searchKeyword.value.trim().toLowerCase();
|
|
|
|
|
const matchKeyword =
|
|
|
|
|
!kw ||
|
|
|
|
|
String(item.id).toLowerCase().includes(kw) ||
|
|
|
|
|
String(item.reporterId).toLowerCase().includes(kw) ||
|
|
|
|
|
String(item.targetName || '').toLowerCase().includes(kw) ||
|
|
|
|
|
String(item.reason || '').toLowerCase().includes(kw);
|
|
|
|
|
return matchStatus && matchKeyword;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const parseEvidence = (evidence: any): string[] => {
|
|
|
|
|
if (!evidence) return [];
|
|
|
|
|
if (Array.isArray(evidence)) return evidence;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(evidence);
|
|
|
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getStatusColor = (status: string) => {
|
|
|
|
|
if (status === '待处理') return 'orange';
|
|
|
|
|
if (status === '已核实处罚') return 'red';
|
|
|
|
|
if (status === '已驳回') return 'gray';
|
|
|
|
|
return 'blue';
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openHandleModal = (record: any, action: 'punish' | 'reject') => {
|
|
|
|
|
currentRecord.value = record;
|
|
|
|
|
handleAction.value = action;
|
|
|
|
|
handleForm.value.note = action === 'punish'
|
|
|
|
|
? '经平台安全团队核验,违规事实清楚,给予处分并记录违规档案。'
|
|
|
|
|
: '经核验未发现明显违规事实,证据不足,暂予驳回。';
|
|
|
|
|
handleModalVisible.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const submitHandle = async () => {
|
|
|
|
|
if (!handleForm.value.note.trim()) {
|
|
|
|
|
Message.warning('请填写处理备注说明');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res: any = await axios.post(`/api/reports/${currentRecord.value.id}/handle`, {
|
|
|
|
|
action: handleAction.value,
|
|
|
|
|
handleNote: handleForm.value.note.trim(),
|
|
|
|
|
penaltyOptions: {
|
|
|
|
|
deductCredit: handleForm.value.penalty === 'warning' ? 5 : 0
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
Message.success(res.data?.message || '举报工单处置成功');
|
|
|
|
|
handleModalVisible.value = false;
|
|
|
|
|
fetchData();
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
Message.error(err.response?.data?.message || err.message || '处置失败');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
.container {
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
}
|
|
|
|
|
.breadcrumb {
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
}
|
|
|
|
|
.code-font {
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
.desc-text {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
color: var(--color-text-2);
|
|
|
|
|
background: var(--color-fill-2);
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
</style>
|