feat: deliver admin redesign pilot
This commit is contained in:
@@ -1,230 +1,239 @@
|
||||
<template>
|
||||
<el-container class="review-page">
|
||||
<el-header class="header">
|
||||
<h2>资料审核</h2>
|
||||
<el-tag type="warning" v-if="stats.pending > 0">待审核 {{ stats.pending }}</el-tag>
|
||||
<el-tag type="info" v-if="stats.processing > 0">审核中 {{ stats.processing }}</el-tag>
|
||||
</el-header>
|
||||
<div class="page-container page-shell review-page">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<span class="page-kicker">用户与资料审核</span>
|
||||
<h1>资料审核</h1>
|
||||
<p>按提交顺序核验必要资料,审核操作保留清晰意见和审计记录。</p>
|
||||
</div>
|
||||
<div class="stat-tags">
|
||||
<el-tag type="warning">待审核 {{ stats.pending }}</el-tag>
|
||||
<el-tag type="info">审核中 {{ stats.processing }}</el-tag>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-main>
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon>
|
||||
<template #default><el-button link type="primary" @click="loadPage">重新加载</el-button></template>
|
||||
</el-alert>
|
||||
|
||||
<section v-else class="surface-card review-panel">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="待审核队列" name="pending">
|
||||
<el-table :data="pendingTasks" v-loading="loading">
|
||||
<el-table-column prop="userId" label="用户ID" width="120" />
|
||||
<el-table-column prop="reviewType" label="审核类型" width="120" />
|
||||
<el-table-column prop="submittedAt" label="提交时间" width="180">
|
||||
<el-tab-pane :label="`待审核队列(${pendingTasks.length})`" name="pending">
|
||||
<el-table :data="pendingTasks" v-loading="loading" row-key="taskId">
|
||||
<el-table-column prop="userId" label="用户编号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="reviewType" label="审核类型" min-width="130">
|
||||
<template #default="{ row }">{{ reviewTypeText(row.reviewType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="submittedAt" label="提交时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatTime(row.submittedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.submittedAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleClaim(row)">领取</el-button>
|
||||
<el-button v-if="canProcess" type="primary" link @click="handleClaim(row)">领取审核</el-button>
|
||||
<span v-else class="muted">只读</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无待审核任务" /></template>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && pendingTasks.length === 0" description="暂无待审核任务" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="我的审核" name="my">
|
||||
<el-table :data="myTasks" v-loading="loading">
|
||||
<el-table-column prop="userId" label="用户ID" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
<el-tab-pane :label="`我的审核(${myTasks.length})`" name="my">
|
||||
<el-table :data="myTasks" v-loading="loading" row-key="taskId">
|
||||
<el-table-column prop="userId" label="用户编号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="reviewType" label="审核类型" min-width="130">
|
||||
<template #default="{ row }">{{ reviewTypeText(row.reviewType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="completedAt" label="完成时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.completedAt ? formatTime(row.completedAt) : '-' }}
|
||||
</template>
|
||||
<el-table-column prop="status" label="状态" width="110">
|
||||
<template #default="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleView(row)">查看</el-button>
|
||||
</template>
|
||||
<el-table-column prop="submittedAt" label="提交时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatTime(row.submittedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<template #default="{ row }"><el-button link type="primary" @click="handleView(row)">查看</el-button></template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无已领取的审核任务" /></template>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-main>
|
||||
</section>
|
||||
|
||||
<!-- 审核详情抽屉 -->
|
||||
<el-drawer v-model="detailVisible" title="审核详情" size="500px">
|
||||
<template v-if="currentTask">
|
||||
<el-drawer v-model="detailVisible" title="审核详情" size="min(560px, 92vw)">
|
||||
<div v-if="detailLoading" v-loading="true" class="drawer-loading"></div>
|
||||
<template v-else-if="currentTask">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="用户ID">{{ currentTask.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核类型">{{ currentTask.reviewType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户编号">{{ currentTask.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核类型">{{ reviewTypeText(currentTask.reviewType) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间">{{ formatTime(currentTask.submittedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前状态">
|
||||
<el-tag :type="statusType(currentTask.status)">{{ statusText(currentTask.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider>用户资料</el-divider>
|
||||
<el-descriptions :column="2" border v-if="currentTask.userProfile">
|
||||
<el-descriptions-item label="姓名">{{ currentTask.userProfile.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ currentTask.userProfile.gender }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年龄">{{ currentTask.userProfile.age }}岁</el-descriptions-item>
|
||||
<el-descriptions-item label="身高">{{ currentTask.userProfile.heightCm }}cm</el-descriptions-item>
|
||||
<el-descriptions-item label="婚姻状况">{{ currentTask.userProfile.maritalStatus }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ currentTask.userProfile.educationCode }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自我介绍" :span="2">
|
||||
{{ currentTask.userProfile.selfIntro || '暂未填写' }}
|
||||
</el-descriptions-item>
|
||||
<h3 class="drawer-heading">用户资料</h3>
|
||||
<el-descriptions v-if="currentTask.userProfile" :column="2" border>
|
||||
<el-descriptions-item label="显示名称">{{ currentTask.userProfile.displayName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText(currentTask.userProfile.gender) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年龄">{{ currentTask.userProfile.age || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="身高">{{ currentTask.userProfile.heightCm ? `${currentTask.userProfile.heightCm} cm` : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="区域">{{ currentTask.userProfile.regionName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ currentTask.userProfile.educationCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自我介绍" :span="2">{{ currentTask.userProfile.selfIntro || '未填写' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="暂无资料快照" />
|
||||
|
||||
<el-divider>审核操作</el-divider>
|
||||
<el-form :model="reviewForm" label-width="80px">
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="reviewForm.comment" type="textarea" :rows="3" placeholder="请输入审核意见" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审核原因">
|
||||
<el-select v-model="reviewForm.reasonCodes" multiple placeholder="可选" style="width:100%">
|
||||
<el-option
|
||||
v-for="reason in reasonOptions"
|
||||
:key="reason.code"
|
||||
:label="reason.name"
|
||||
:value="reason.code"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" @click="handleApprove" :loading="submitting">通过</el-button>
|
||||
<el-button type="warning" @click="handleNeedEdit" :loading="submitting">需修改</el-button>
|
||||
<el-button type="danger" @click="handleReject" :loading="submitting">拒绝</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template v-if="canProcess">
|
||||
<h3 class="drawer-heading">审核操作</h3>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model.trim="reviewForm.comment" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请写明判断依据或需要补充的内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原因标签">
|
||||
<el-select v-model="reviewForm.reasonCodes" multiple class="full-width" placeholder="按需选择">
|
||||
<el-option v-for="reason in reasonOptions" :key="reason.code" :label="reason.name" :value="reason.code" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="review-actions">
|
||||
<el-button type="success" :loading="submitting" @click="runDecision('approve', '审核通过')">通过</el-button>
|
||||
<el-button type="warning" :loading="submitting" @click="runDecision('need-edit', '退回修改')">需修改</el-button>
|
||||
<el-button type="danger" :loading="submitting" @click="runDecision('reject', '拒绝资料')">拒绝</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { get, post } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const activeTab = ref('pending')
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const stats = ref({ pending: 0, processing: 0 })
|
||||
const pendingTasks = ref<any[]>([])
|
||||
const myTasks = ref<any[]>([])
|
||||
const tasks = ref<any[]>([])
|
||||
const currentTask = ref<any>(null)
|
||||
const reasonOptions = ref<any[]>([])
|
||||
const reviewForm = ref({ comment: '', reasonCodes: [] as string[] })
|
||||
const canProcess = computed(() => authStore.permissions.includes('review.process'))
|
||||
const pendingTasks = computed(() => tasks.value.filter((task) => !task.assignedOperatorId))
|
||||
const myTasks = computed(() => tasks.value.filter((task) => task.assignedOperatorId === authStore.operator?.operatorId))
|
||||
|
||||
const reviewForm = ref({
|
||||
comment: '',
|
||||
reasonCodes: [] as string[]
|
||||
})
|
||||
|
||||
async function loadStats() {
|
||||
// TODO: Phase 05 实现真实API调用
|
||||
stats.value = { pending: 12, processing: 3 }
|
||||
async function loadPage() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const [statsResponse, tasksResponse, reasonsResponse]: any[] = await Promise.all([
|
||||
get('/operator/reviews/stats'),
|
||||
get('/operator/reviews/pending', { page: 1, pageSize: 20 }),
|
||||
get('/operator/reviews/reasons'),
|
||||
])
|
||||
stats.value = statsResponse.data || { pending: 0, processing: 0 }
|
||||
tasks.value = tasksResponse.data || []
|
||||
reasonOptions.value = reasonsResponse.data || []
|
||||
} catch (error: any) {
|
||||
tasks.value = []
|
||||
errorMessage.value = error.response?.status === 403
|
||||
? '您没有查看资料审核的权限'
|
||||
: '审核任务加载失败,请检查服务状态后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(ts?: string) {
|
||||
if (!ts) return '-'
|
||||
return new Date(ts).toLocaleString('zh-CN')
|
||||
async function handleClaim(row: any) {
|
||||
try {
|
||||
const response: any = await post(`/operator/reviews/${row.taskId || row.id}/claim`)
|
||||
if (!response.success) throw new Error(response.message || '领取失败')
|
||||
ElMessage.success('审核任务领取成功')
|
||||
await loadPage()
|
||||
await openDetail(row.taskId || row.id)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '领取失败,任务可能已被其他审核员领取')
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(taskId: string) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
currentTask.value = null
|
||||
reviewForm.value = { comment: '', reasonCodes: [] }
|
||||
try {
|
||||
const response: any = await get(`/operator/reviews/${taskId}`)
|
||||
currentTask.value = response.data
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleView(row: any) {
|
||||
await openDetail(row.taskId || row.id)
|
||||
}
|
||||
|
||||
async function runDecision(action: 'approve' | 'need-edit' | 'reject', label: string) {
|
||||
if (!currentTask.value || submitting.value) return
|
||||
if (action !== 'approve' && !reviewForm.value.comment) {
|
||||
ElMessage.warning('请填写审核意见,说明需要修改或拒绝的原因')
|
||||
return
|
||||
}
|
||||
if (action === 'reject') {
|
||||
await ElMessageBox.confirm('拒绝后用户需要重新处理资料,确认继续吗?', '确认拒绝资料', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认拒绝',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const response: any = await post(`/operator/reviews/${currentTask.value.taskId}/${action}`, {
|
||||
comment: reviewForm.value.comment,
|
||||
reasonCodes: reviewForm.value.reasonCodes,
|
||||
})
|
||||
if (!response.success) throw new Error(response.message || `${label}失败`)
|
||||
ElMessage.success(label)
|
||||
detailVisible.value = false
|
||||
await loadPage()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || `${label}失败,请重试`)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function reviewTypeText(value?: string) {
|
||||
return value ? ({ identity: '实名认证', profile: '婚恋资料', local_eligibility: '本地资格' }[value] || value) : '-'
|
||||
}
|
||||
|
||||
function genderText(value?: string) {
|
||||
return value === 'male' ? '男' : value === 'female' ? '女' : '-'
|
||||
}
|
||||
|
||||
function statusType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
processing: 'info',
|
||||
approved: 'success',
|
||||
need_edit: 'warning',
|
||||
rejected: 'danger'
|
||||
}
|
||||
return map[status] || ''
|
||||
return ({ pending: 'warning', processing: 'info', approved: 'success', need_edit: 'warning', rejected: 'danger' } as Record<string, string>)[status] || 'info'
|
||||
}
|
||||
|
||||
function statusText(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
processing: '审核中',
|
||||
approved: '已通过',
|
||||
need_edit: '需修改',
|
||||
rejected: '已拒绝'
|
||||
}
|
||||
return map[status] || status
|
||||
return ({ pending: '待审核', processing: '审核中', approved: '已通过', need_edit: '需修改', rejected: '已拒绝' } as Record<string, string>)[status] || status || '-'
|
||||
}
|
||||
|
||||
function handleClaim(_row: any) {
|
||||
// TODO: 调用 API 领取
|
||||
ElMessage.success('领取成功')
|
||||
function formatTime(time?: string) {
|
||||
return time ? new Date(time).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
function handleView(row: any) {
|
||||
currentTask.value = row
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
submitting.value = true
|
||||
try {
|
||||
// TODO: 调用 API
|
||||
ElMessage.success('审核通过')
|
||||
detailVisible.value = false
|
||||
await loadStats()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNeedEdit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
ElMessage.success('已标记需修改')
|
||||
detailVisible.value = false
|
||||
await loadStats()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
submitting.value = true
|
||||
try {
|
||||
ElMessage.success('已拒绝')
|
||||
detailVisible.value = false
|
||||
await loadStats()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
onMounted(loadPage)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.review-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.page-container{padding:28px}.page-kicker{color:var(--admin-primary);font-size:12px;font-weight:650;letter-spacing:1px}.stat-tags{display:flex;gap:8px}.review-panel{padding:20px 24px}.muted{color:var(--color-text-tertiary);font-size:12px}.drawer-loading{min-height:240px}.drawer-heading{margin:24px 0 12px;font-size:16px}.full-width{width:100%}.review-actions{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:760px){.page-container{padding:18px}}
|
||||
</style>
|
||||
|
||||
@@ -1,176 +1,133 @@
|
||||
<template>
|
||||
<div class="page-container" v-loading="loading">
|
||||
<el-card shadow="never" v-if="user">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户详情</span>
|
||||
<el-tag :type="accountStatusType(user.accountStatus)">{{ accountStatusText(user.accountStatus) }}</el-tag>
|
||||
<el-tag :type="riskLevelType(user.riskLevel)">{{ riskLevelText(user.riskLevel) }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 基础信息 -->
|
||||
<div class="page-container page-shell" v-loading="loading">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<el-button class="back-button" link @click="router.push('/users')">
|
||||
<el-icon><ArrowLeft /></el-icon>返回用户列表
|
||||
</el-button>
|
||||
<h1>用户详情</h1>
|
||||
<p>敏感信息遵循最小权限原则,查看与处置操作均需审计。</p>
|
||||
</div>
|
||||
<div v-if="user" class="heading-tags">
|
||||
<el-tag :type="accountStatusType(user.accountStatus)">{{ accountStatusText(user.accountStatus) }}</el-tag>
|
||||
<el-tag :type="riskLevelType(user.riskLevel)" effect="plain">{{ riskLevelText(user.riskLevel) }}</el-tag>
|
||||
</div>
|
||||
</header>
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" :closable="false" show-icon>
|
||||
<template #default><el-button v-if="accountId" link type="primary" @click="loadDetail">重新加载</el-button></template>
|
||||
</el-alert>
|
||||
<section v-else-if="user" class="surface-card detail-card">
|
||||
<div class="identity-summary">
|
||||
<el-avatar :size="58">{{ (user.displayName || '用')[0] }}</el-avatar>
|
||||
<div><h2>{{ user.displayName || '未设置显示名称' }}</h2><span>用户编号:{{ user.id }}</span></div>
|
||||
<el-progress class="completion" type="dashboard" :percentage="user.profileCompletion || 0" :width="72" :stroke-width="7" color="#EE666B" />
|
||||
</div>
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="基础信息" name="basic">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="用户ID">{{ user.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="显示名称">{{ user.displayName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="显示名称">{{ user.displayName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="实名认证">{{ user.idVerified ? '已认证' : '未认证' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText(user.gender) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="年龄">{{ user.age || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">
|
||||
<template v-if="user.sensitiveMasked">
|
||||
<span class="masked">{{ user.phoneMasked }}</span>
|
||||
<el-button size="small" link @click="requestSensitive">需要查看</el-button>
|
||||
</template>
|
||||
<template v-else>{{ user.phone }}</template>
|
||||
<el-descriptions-item label="手机号">
|
||||
<template v-if="user.sensitiveMasked"><span class="masked">{{ user.phoneMasked || '已脱敏' }}</span><el-button link type="primary" @click="requestSensitive">权限说明</el-button></template>
|
||||
<template v-else>{{ user.phone || '-' }}</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="区域">{{ user.regionName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ formatTime(user.registeredAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近登录">{{ formatTime(user.lastLoginAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 婚恋资料 -->
|
||||
<el-tab-pane label="婚恋资料" name="profile">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="婚姻状态">{{ maritalText(user.maritalStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="子女情况">{{ childrenText(user.childrenStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ eduText(user.educationCode) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职业">{{ occText(user.occupationCode) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自我介绍" :span="2">{{ user.selfIntro || '未填写' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="资料完整度">{{ user.profileCompletion }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ user.educationCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职业">{{ user.occupationCode || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="资料状态">{{ profileStatusText(user.profileStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关系状态">{{ relationshipText(user.relationshipStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自我介绍" :span="2">{{ user.selfIntro || '未填写' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 风险记录 -->
|
||||
<el-tab-pane label="风险记录" name="risk">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="风险分数">{{ user.totalRiskScore ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="风险等级">
|
||||
<el-tag :type="riskLevelType(user.riskLevel)">{{ riskLevelText(user.riskLevel) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="历史举报数">{{ user.totalReportsCount ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处置数">{{ user.activeActionsCount ?? 0 }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div style="margin-top:16px">
|
||||
<el-button type="primary" size="small" @click="showStatusDialog = true">修改账号状态</el-button>
|
||||
<el-tab-pane label="安全与账号" name="risk">
|
||||
<div class="risk-summary">
|
||||
<article><span>风险分数</span><strong>{{ user.totalRiskScore ?? 0 }}</strong></article>
|
||||
<article><span>历史举报</span><strong>{{ user.totalReportsCount ?? 0 }}</strong></article>
|
||||
<article><span>当前处置</span><strong>{{ user.activeActionsCount ?? 0 }}</strong></article>
|
||||
</div>
|
||||
<div class="danger-zone">
|
||||
<div><h3><el-icon><WarningFilled /></el-icon>账号状态处置</h3><p>状态变更会影响用户登录和平台功能,提交前请确认处置依据。</p></div>
|
||||
<el-button v-if="canChangeStatus" type="danger" plain @click="openStatusDialog">修改账号状态</el-button>
|
||||
<el-tag v-else type="info">无状态修改权限</el-tag>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 状态记录 -->
|
||||
<el-tab-pane label="状态记录" name="status">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="账号状态">
|
||||
<el-tag :type="accountStatusType(user.accountStatus)">{{ accountStatusText(user.accountStatus) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="婚恋状态">{{ relationshipText(user.relationshipStatus) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<!-- 修改状态对话框 -->
|
||||
<el-dialog v-model="showStatusDialog" title="修改账号状态" width="400px">
|
||||
<el-form label-width="100px">
|
||||
</section>
|
||||
<el-empty v-else-if="!loading" description="未找到用户信息" />
|
||||
<el-dialog v-model="showStatusDialog" title="修改账号状态" width="420px">
|
||||
<el-alert title="该操作将被写入审计日志,请谨慎处理。" type="warning" :closable="false" show-icon />
|
||||
<el-form label-position="top" class="status-form">
|
||||
<el-form-item label="新状态">
|
||||
<el-select v-model="newStatus" style="width:100%">
|
||||
<el-option label="正常" value="normal" />
|
||||
<el-option label="风险审核中" value="reviewing_risk" />
|
||||
<el-option label="功能受限" value="limited" />
|
||||
<el-option label="临时暂停" value="suspended" />
|
||||
<el-option label="封禁" value="banned" />
|
||||
<el-option label="已注销" value="closed" />
|
||||
<el-select v-model="newStatus" class="full-width">
|
||||
<el-option label="正常" value="normal" /><el-option label="风险审核中" value="reviewing_risk" />
|
||||
<el-option label="功能受限" value="limited" /><el-option label="临时暂停" value="suspended" />
|
||||
<el-option label="封禁" value="banned" /><el-option label="已注销" value="closed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="原因">
|
||||
<el-input v-model="statusNote" type="textarea" :rows="3" placeholder="请输入修改原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showStatusDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="updating" @click="updateStatus">确认</el-button>
|
||||
</template>
|
||||
<template #footer><el-button @click="showStatusDialog = false">取消</el-button><el-button type="danger" :loading="updating" @click="updateStatus">确认变更</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router' // eslint-disable-next-line
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { get, put } from '@/api/client'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const user = ref<any>(null)
|
||||
const activeTab = ref('basic')
|
||||
const showStatusDialog = ref(false)
|
||||
const newStatus = ref('normal')
|
||||
const statusNote = ref('')
|
||||
const updating = ref(false)
|
||||
|
||||
const accountId = route.query.id as string
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
const route = useRoute(); const router = useRouter(); const authStore = useAuthStore()
|
||||
const loading = ref(true); const errorMessage = ref(''); const user = ref<any>(null); const activeTab = ref('basic')
|
||||
const showStatusDialog = ref(false); const newStatus = ref('normal'); const updating = ref(false)
|
||||
const accountId = String(route.query.id || '')
|
||||
const canChangeStatus = computed(() => authStore.permissions.includes('user.status.change'))
|
||||
const ACCOUNT_MAP: Record<string, { text: string; type: string }> = {
|
||||
normal: { text: '正常', type: 'success' },
|
||||
banned: { text: '封禁', type: 'danger' },
|
||||
suspended: { text: '暂停', type: 'warning' },
|
||||
reviewing_risk: { text: '风险审核', type: 'danger' },
|
||||
limited: { text: '受限', type: 'info' }
|
||||
normal: { text: '正常', type: 'success' }, banned: { text: '封禁', type: 'danger' },
|
||||
suspended: { text: '暂停', type: 'warning' }, reviewing_risk: { text: '风险审核', type: 'danger' },
|
||||
limited: { text: '受限', type: 'info' }, closed: { text: '已注销', type: 'info' },
|
||||
}
|
||||
|
||||
const RISK_MAP: Record<string, { text: string; type: string }> = {
|
||||
normal: { text: '正常', type: 'info' },
|
||||
watch: { text: '关注', type: '' },
|
||||
high: { text: '高风险', type: 'warning' },
|
||||
critical: { text: '极高风险', type: 'danger' }
|
||||
normal: { text: '正常', type: 'info' }, watch: { text: '关注', type: 'warning' },
|
||||
high: { text: '高风险', type: 'warning' }, critical: { text: '极高风险', type: 'danger' },
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
if (!accountId) return
|
||||
loading.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res: any = await get(`/operator/users/${accountId}`, {}, { Authorization: `Bearer ${token}` })
|
||||
user.value = res
|
||||
} catch (e) { console.error(e) }
|
||||
if (!accountId) { loading.value = false; errorMessage.value = '缺少用户编号,请从用户列表重新进入'; return }
|
||||
loading.value = true; errorMessage.value = ''
|
||||
try { const response: any = await get(`/operator/users/${accountId}`); user.value = response.data }
|
||||
catch (error: any) { user.value = null; errorMessage.value = error.response?.status === 403 ? '您没有查看该用户详情的权限' : '用户详情加载失败,请检查服务状态后重试' }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
function openStatusDialog() { newStatus.value = user.value?.accountStatus || 'normal'; showStatusDialog.value = true }
|
||||
async function updateStatus() {
|
||||
if (!newStatus.value) return
|
||||
if (!newStatus.value || updating.value) return
|
||||
await ElMessageBox.confirm(`确认将该用户账号状态修改为“${accountStatusText(newStatus.value)}”吗?`, '确认账号状态变更', { type: 'warning', confirmButtonText: '确认变更', cancelButtonText: '取消' })
|
||||
updating.value = true
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
await put(`/operator/users/${accountId}/status`, { accountStatus: newStatus.value }, {
|
||||
Authorization: `Bearer ${token}`
|
||||
})
|
||||
showStatusDialog.value = false
|
||||
loadDetail()
|
||||
} catch (e) { console.error(e) }
|
||||
try { await put(`/operator/users/${accountId}/status`, { accountStatus: newStatus.value }); ElMessage.success('账号状态已更新'); showStatusDialog.value = false; await loadDetail() }
|
||||
finally { updating.value = false }
|
||||
}
|
||||
|
||||
function requestSensitive() { /* TODO: 需要敏感权限的申请流程 */ }
|
||||
|
||||
function genderText(g?: string) { return g === 'male' ? '男' : g === 'female' ? '女' : '-' }
|
||||
function maritalText(m?: string) { const m2 = m as string | undefined; return m2 && { single: '未婚', divorced: '离异', widowed: '丧偶' }[m2] || '-' }
|
||||
function childrenText(c?: string) { const c2 = c as string | undefined; return c2 && { none: '无子女', with_children: '有子女' }[c2] || '-' }
|
||||
function eduText(e?: string) { return e || '-' }
|
||||
function occText(o?: string) { return o || '-' }
|
||||
function profileStatusText(s?: string) { const s2 = s as string | undefined; return s2 && { approved: '已审核', pending: '待审核', need_edit: '需修改' }[s2] || s }
|
||||
function relationshipText(r?: string) { const r2 = r as string | undefined; return r2 && { searching: '寻找中', getting_to_know: '正在了解', matched_successfully: '已牵手', paused: '暂停' }[r2] || r || '-' }
|
||||
function accountStatusType(s: string) { return ACCOUNT_MAP[s]?.type || 'info' }
|
||||
function accountStatusText(s: string) { return ACCOUNT_MAP[s]?.text || s }
|
||||
function riskLevelType(l: string) { return RISK_MAP[l]?.type || 'info' }
|
||||
function riskLevelText(l: string) { return RISK_MAP[l]?.text || l }
|
||||
function formatTime(t?: string) { return t ? new Date(t).toLocaleString('zh-CN') : '-' }
|
||||
|
||||
function requestSensitive() { ElMessage.info('当前内容已脱敏;只有具备敏感数据权限的角色可以查看明文。') }
|
||||
function genderText(value?: string) { return value === 'male' ? '男' : value === 'female' ? '女' : '-' }
|
||||
function maritalText(value?: string) { return value ? ({ single: '未婚', divorced: '离异', widowed: '丧偶' }[value] || value) : '-' }
|
||||
function childrenText(value?: string) { return value ? ({ none: '无子女', with_children: '有子女' }[value] || value) : '-' }
|
||||
function profileStatusText(value?: string) { return value ? ({ approved: '已审核', pending: '待审核', need_edit: '需修改' }[value] || value) : '-' }
|
||||
function relationshipText(value?: string) { return value ? ({ searching: '寻找中', getting_to_know: '正在了解', matched_successfully: '已牵手', paused: '暂停' }[value] || value) : '-' }
|
||||
function accountStatusType(status: string) { return ACCOUNT_MAP[status]?.type || 'info' }
|
||||
function accountStatusText(status: string) { return ACCOUNT_MAP[status]?.text || status || '-' }
|
||||
function riskLevelType(level: string) { return RISK_MAP[level]?.type || 'info' }
|
||||
function riskLevelText(level: string) { return RISK_MAP[level]?.text || level || '-' }
|
||||
function formatTime(time?: string) { return time ? new Date(time).toLocaleString('zh-CN') : '-' }
|
||||
onMounted(loadDetail)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.card-header { display: flex; align-items: center; gap: 12px; }
|
||||
.masked { color: #999; font-style: italic; }
|
||||
.page-container{padding:28px}.back-button{margin-bottom:8px}.heading-tags{display:flex;gap:8px}.detail-card{padding:24px}.identity-summary{display:flex;align-items:center;gap:14px;padding-bottom:20px;border-bottom:1px solid var(--color-border-light)}.identity-summary .el-avatar{color:#fff;background:var(--admin-primary)}.identity-summary h2{font-size:20px}.identity-summary span{display:block;margin-top:6px;color:var(--color-text-tertiary);font-size:12px}.completion{margin-left:auto}.detail-tabs{margin-top:18px}.masked{margin-right:8px;color:var(--color-text-tertiary)}.risk-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.risk-summary article{padding:18px;background:var(--admin-soft-pink);border-radius:12px}.risk-summary span{color:var(--color-text-secondary)}.risk-summary strong{display:block;margin-top:8px;font-size:26px}.danger-zone{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:20px;padding:18px;border:1px solid #F3CCCC;border-radius:12px}.danger-zone h3{display:flex;align-items:center;gap:7px;color:var(--color-danger)}.danger-zone p{margin-top:6px;color:var(--color-text-secondary)}.status-form{margin-top:18px}.full-width{width:100%}@media(max-width:760px){.page-container{padding:18px}.risk-summary{grid-template-columns:1fr}.danger-zone{align-items:flex-start;flex-direction:column}}
|
||||
</style>
|
||||
|
||||
@@ -1,103 +1,170 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>用户管理</span>
|
||||
<el-input v-model="keyword" placeholder="搜索用户ID" clearable style="width:200px" />
|
||||
<el-select v-model="accountStatus" placeholder="账号状态" clearable style="width:140px">
|
||||
<el-option label="正常" value="normal" />
|
||||
<el-option label="封禁" value="banned" />
|
||||
<el-option label="暂停" value="suspended" />
|
||||
<el-option label="风险审核中" value="reviewing_risk" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="loadData" :loading="loading">查询</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="page-container page-shell">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<span class="page-kicker">用户与资料审核</span>
|
||||
<h1>用户管理</h1>
|
||||
<p>仅按当前角色权限查询用户,手机号等敏感信息默认脱敏。</p>
|
||||
</div>
|
||||
<el-tag type="info" effect="plain">共 {{ total }} 人</el-tag>
|
||||
</header>
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="用户ID" width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="displayName" label="姓名" width="120" />
|
||||
<el-table-column prop="gender" label="性别" width="60">
|
||||
<template #default="{row}">{{ row.gender === 'male' ? '男' : row.gender === 'female' ? '女' : '-' }}</template>
|
||||
<section class="surface-card filter-panel" aria-label="用户筛选">
|
||||
<el-input
|
||||
v-model.trim="keyword"
|
||||
class="keyword-input"
|
||||
clearable
|
||||
aria-label="用户编号"
|
||||
placeholder="输入用户编号"
|
||||
@keyup.enter="search"
|
||||
>
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-select v-model="accountStatus" clearable placeholder="全部账号状态" aria-label="账号状态">
|
||||
<el-option label="正常" value="normal" />
|
||||
<el-option label="封禁" value="banned" />
|
||||
<el-option label="暂停" value="suspended" />
|
||||
<el-option label="风险审核中" value="reviewing_risk" />
|
||||
</el-select>
|
||||
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</section>
|
||||
|
||||
<section class="surface-card table-panel">
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
:closable="false"
|
||||
show-icon
|
||||
>
|
||||
<template #default><el-button link type="primary" @click="loadData">重新加载</el-button></template>
|
||||
</el-alert>
|
||||
|
||||
<el-table v-else :data="tableData" v-loading="loading" row-key="id">
|
||||
<el-table-column prop="id" label="用户编号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="displayName" label="显示名称" min-width="110">
|
||||
<template #default="{ row }">{{ row.displayName || '未设置' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="60" />
|
||||
<el-table-column prop="phoneMasked" label="手机尾号" width="120" />
|
||||
<el-table-column prop="regionName" label="区域" width="100" />
|
||||
<el-table-column prop="accountStatus" label="账号状态" width="100">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="accountStatusType(row.accountStatus)">{{ accountStatusText(row.accountStatus) }}</el-tag>
|
||||
<el-table-column prop="gender" label="性别" width="70">
|
||||
<template #default="{ row }">{{ genderText(row.gender) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age" label="年龄" width="70">
|
||||
<template #default="{ row }">{{ row.age || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phoneMasked" label="手机号" min-width="130">
|
||||
<template #default="{ row }">{{ row.phoneMasked || '未绑定' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="regionName" label="区域" min-width="100">
|
||||
<template #default="{ row }">{{ row.regionName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accountStatus" label="账号状态" min-width="105">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="accountStatusType(row.accountStatus)" effect="light">
|
||||
{{ accountStatusText(row.accountStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="riskLevel" label="风险等级" width="100">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="riskLevelType(row.riskLevel)">{{ riskLevelText(row.riskLevel) }}</el-tag>
|
||||
<el-table-column prop="riskLevel" label="风险等级" min-width="105">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="riskLevelType(row.riskLevel)" effect="plain">
|
||||
{{ riskLevelText(row.riskLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="registeredAt" label="注册时间" width="180">
|
||||
<template #default="{row}">{{ formatTime(row.registeredAt) }}</template>
|
||||
<el-table-column prop="registeredAt" label="注册时间" min-width="170">
|
||||
<template #default="{ row }">{{ formatTime(row.registeredAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="goDetail(row.id)">查看</el-button>
|
||||
<el-table-column label="操作" width="86" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="goDetail(row.id)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="没有找到符合条件的用户" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<el-pagination v-if="total > 0" style="margin-top:16px;text-align:right"
|
||||
layout="total, prev, pager, next" :total="total" :page-size="pageSize"
|
||||
:current-page="page" @current-change="handlePageChange" />
|
||||
</el-card>
|
||||
<el-pagination
|
||||
v-if="!errorMessage && total > 0"
|
||||
class="pagination"
|
||||
layout="total, prev, pager, next"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
:current-page="page + 1"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { get } from '@/api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const keyword = ref('')
|
||||
const accountStatus = ref('')
|
||||
const tableData = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(0)
|
||||
const pageSize = ref(20)
|
||||
const pageSize = 20
|
||||
|
||||
const ACCOUNT_MAP: Record<string, { text: string; type: string }> = {
|
||||
normal: { text: '正常', type: 'success' },
|
||||
banned: { text: '封禁', type: 'danger' },
|
||||
suspended: { text: '暂停', type: 'warning' },
|
||||
reviewing_risk: { text: '风险审核', type: 'danger' },
|
||||
limited: { text: '受限', type: 'info' }
|
||||
limited: { text: '受限', type: 'info' },
|
||||
}
|
||||
|
||||
const RISK_MAP: Record<string, { text: string; type: string }> = {
|
||||
normal: { text: '正常', type: 'info' },
|
||||
watch: { text: '关注', type: '' },
|
||||
watch: { text: '关注', type: 'warning' },
|
||||
high: { text: '高风险', type: 'warning' },
|
||||
critical: { text: '极高风险', type: 'danger' }
|
||||
critical: { text: '极高风险', type: 'danger' },
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const token = localStorage.getItem('accessToken')
|
||||
const res: any = await get('/operator/users', {
|
||||
const response: any = await get('/operator/users', {
|
||||
keyword: keyword.value || undefined,
|
||||
accountStatus: accountStatus.value || undefined,
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
}, { Authorization: `Bearer ${token}` })
|
||||
tableData.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loading.value = false }
|
||||
pageSize,
|
||||
})
|
||||
tableData.value = response.data?.list || []
|
||||
total.value = response.data?.total || 0
|
||||
} catch (error: any) {
|
||||
tableData.value = []
|
||||
total.value = 0
|
||||
errorMessage.value = error.response?.status === 403
|
||||
? '您没有查看用户列表的权限'
|
||||
: '用户数据加载失败,请检查服务状态后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handlePageChange(p: number) {
|
||||
page.value = p - 1
|
||||
function search() {
|
||||
page.value = 0
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
keyword.value = ''
|
||||
accountStatus.value = ''
|
||||
search()
|
||||
}
|
||||
|
||||
function handlePageChange(current: number) {
|
||||
page.value = current - 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -105,16 +172,82 @@ function goDetail(id: string) {
|
||||
router.push({ path: '/users/detail', query: { id } })
|
||||
}
|
||||
|
||||
function accountStatusType(s: string) { return ACCOUNT_MAP[s]?.type || 'info' }
|
||||
function accountStatusText(s: string) { return ACCOUNT_MAP[s]?.text || s }
|
||||
function riskLevelType(l: string) { return RISK_MAP[l]?.type || 'info' }
|
||||
function riskLevelText(l: string) { return RISK_MAP[l]?.text || l }
|
||||
function formatTime(t?: string) { return t ? new Date(t).toLocaleString('zh-CN') : '-' }
|
||||
function genderText(gender?: string) {
|
||||
return gender === 'male' ? '男' : gender === 'female' ? '女' : '-'
|
||||
}
|
||||
|
||||
function accountStatusType(status: string) {
|
||||
return ACCOUNT_MAP[status]?.type || 'info'
|
||||
}
|
||||
|
||||
function accountStatusText(status: string) {
|
||||
return ACCOUNT_MAP[status]?.text || status || '-'
|
||||
}
|
||||
|
||||
function riskLevelType(level: string) {
|
||||
return RISK_MAP[level]?.type || 'info'
|
||||
}
|
||||
|
||||
function riskLevelText(level: string) {
|
||||
return RISK_MAP[level]?.text || level || '-'
|
||||
}
|
||||
|
||||
function formatTime(time?: string) {
|
||||
return time ? new Date(time).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.card-header { display: flex; align-items: center; gap: 12px; }
|
||||
.page-container {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.page-kicker {
|
||||
color: var(--admin-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.keyword-input {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.filter-panel .el-select {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table-panel .el-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
justify-content: flex-end;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-container {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.keyword-input,
|
||||
.filter-panel .el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user