Files
danyang-match/admin-web/src/modules/user/UserDetailView.vue
T
WorkBuddy 3c2b967274
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Canceled after 0s
CI/CD Pipeline / E2E (live stack) (push) Canceled after 0s
CI/CD Pipeline / Backend Build & Test (push) Canceled after 0s
CI/CD Pipeline / Admin Web Build (push) Canceled after 0s
CI/CD Pipeline / Mini Program Compile Check (push) Canceled after 0s
CI/CD Pipeline / Migration Validation (push) Canceled after 0s
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Canceled after 0s
release: freeze 2026-10-15 validated release baseline
2026-10-15 09:47:31 +08:00

134 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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.displayNumber || '实名后生成' }}</span></div>
<el-progress class="completion" type="dashboard" :percentage="user.profileCompletion || 0" :width="72" :stroke-width="7" color="var(--admin-coral-solid)" />
</div>
<el-tabs v-model="activeTab" class="detail-tabs">
<el-tab-pane label="基础信息" name="basic">
<el-descriptions :column="2" border>
<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 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="学历">{{ 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">
<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-tabs>
</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" 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>
<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 { 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'
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' }, closed: { text: '已注销', type: 'info' },
}
const RISK_MAP: Record<string, { text: string; type: string }> = {
normal: { text: '正常', type: 'info' }, watch: { text: '关注', type: 'warning' },
high: { text: '高风险', type: 'warning' }, critical: { text: '极高风险', type: 'danger' },
}
async function loadDetail() {
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 || updating.value) return
await ElMessageBox.confirm(`确认将该用户账号状态修改为“${accountStatusText(newStatus.value)}”吗?`, '确认账号状态变更', { type: 'warning', confirmButtonText: '确认变更', cancelButtonText: '取消' })
updating.value = true
try { await put(`/operator/users/${accountId}/status`, { accountStatus: newStatus.value }); ElMessage.success('账号状态已更新'); showStatusDialog.value = false; await loadDetail() }
finally { updating.value = false }
}
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: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:var(--color-text-inverse);background:var(--admin-coral-solid)}.identity-summary h2{font-size:var(--font-size-2xl)}.identity-summary span{display:block;margin-top:6px;color:var(--color-text-tertiary);font-size:var(--font-size-xs)}.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:var(--font-size-4xl)}.danger-zone{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-top:20px;padding:18px;border:1px solid var(--color-border-danger);border-radius:12px}.danger-zone h3{display:flex;align-items:center;gap:7px;color:var(--color-danger-strong)}.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>