xiangyixiangqin/admin/src/views/user/detail.vue
2026-02-08 13:58:03 +08:00

830 lines
26 KiB
Vue

<script setup lang="ts">
/**
* 用户详情页面
* Requirements: 3.2
*/
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import StatusTag from '@/components/StatusTag/index.vue'
import { getUserDetail, updateUserStatus, updateContactCount, updateMemberLevel, cancelRealName } from '@/api/user'
import { getFullImageUrl } from '@/utils/image'
import type { UserDetail } from '@/types/user.d'
const route = useRoute()
const router = useRouter()
// 加载状态
const loading = ref(false)
// 用户详情数据
const userDetail = ref<UserDetail | null>(null)
// 当前激活的Tab
const activeTab = ref('basic')
// 用户ID
const userId = computed(() => Number(route.params.id))
// 获取用户详情
const fetchUserDetail = async () => {
if (!userId.value) {
ElMessage.error('用户ID无效')
return
}
loading.value = true
try {
userDetail.value = await getUserDetail(userId.value)
} catch (error) {
console.error('获取用户详情失败:', error)
ElMessage.error('获取用户详情失败')
} finally {
loading.value = false
}
}
// 返回列表
const handleBack = () => {
router.push('/user/list')
}
// 切换用户状态
const handleToggleStatus = async () => {
if (!userDetail.value) return
const newStatus = userDetail.value.status === 1 ? 2 : 1
const statusText = newStatus === 1 ? '启用' : '禁用'
try {
await ElMessageBox.confirm(
`确定要${statusText}该用户吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await updateUserStatus(userId.value, newStatus)
ElMessage.success(`${statusText}成功`)
fetchUserDetail()
} catch (error) {
if (error !== 'cancel') {
console.error('更新用户状态失败:', error)
}
}
}
// 修改联系次数
const handleEditContactCount = async () => {
if (!userDetail.value) return
try {
const { value } = await ElMessageBox.prompt(
'请输入新的联系次数',
'修改联系次数',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: String(userDetail.value.contactCount),
inputPattern: /^\d+$/,
inputErrorMessage: '请输入有效的数字'
}
)
const newCount = parseInt(value, 10)
await updateContactCount(userId.value, newCount)
ElMessage.success('修改成功')
fetchUserDetail()
} catch (error) {
if (error !== 'cancel') {
console.error('修改联系次数失败:', error)
}
}
}
// 会员等级选项 - 固定等级名称
const memberLevelOptions = [
{ value: 0, label: '非会员' },
{ value: 1, label: '等级1 - 不限时会员' },
{ value: 2, label: '等级2 - 诚意会员' },
{ value: 3, label: '等级3 - 家庭版会员' },
{ value: 4, label: '等级4 - 限时会员' }
]
// 修改会员等级对话框
const memberLevelDialogVisible = ref(false)
const memberLevelForm = ref({
memberLevel: 0,
memberExpireTime: ''
})
// 打开修改会员等级对话框
const handleEditMemberLevel = () => {
if (!userDetail.value) return
memberLevelForm.value = {
memberLevel: userDetail.value.memberLevel,
memberExpireTime: userDetail.value.memberExpireTime ? userDetail.value.memberExpireTime.substring(0, 10) : ''
}
memberLevelDialogVisible.value = true
}
// 确认修改会员等级
const handleConfirmMemberLevel = async () => {
try {
const { memberLevel } = memberLevelForm.value
await updateMemberLevel(userId.value, memberLevel, undefined)
ElMessage.success('修改成功')
memberLevelDialogVisible.value = false
fetchUserDetail()
} catch (error) {
console.error('修改会员等级失败:', error)
ElMessage.error('修改失败')
}
}
// 取消实名状态
const handleCancelRealName = async () => {
if (!userDetail.value || !userDetail.value.isRealName) return
try {
await ElMessageBox.confirm(
'确定要取消该用户的实名状态吗?取消后用户需要重新进行实名认证。',
'取消实名',
{
confirmButtonText: '确定取消',
cancelButtonText: '取消',
type: 'warning'
}
)
await cancelRealName(userId.value)
ElMessage.success('取消实名成功')
fetchUserDetail()
} catch (error) {
if (error !== 'cancel') {
console.error('取消实名失败:', error)
ElMessage.error('取消实名失败')
}
}
}
// 格式化时间
const formatTime = (time: string) => {
if (!time) return '-'
return time.replace('T', ' ').substring(0, 19)
}
// 格式化金额
const formatMoney = (amount: number) => {
return `¥${amount.toFixed(2)}`
}
// 学历映射
const educationMap: Record<number, string> = {
1: '高中',
2: '中专',
3: '大专',
4: '本科',
5: '研究生',
6: '博士及以上'
}
// 婚姻状态映射
const marriageStatusMap: Record<number, string> = {
1: '未婚',
2: '离异未育',
3: '离异已育'
}
// 格式化学历数组
const formatEducation = (education: string | number[] | undefined) => {
if (!education) return '-'
// 如果是字符串,尝试解析为数组
let eduArray: number[] = []
if (typeof education === 'string') {
try {
eduArray = JSON.parse(education)
} catch {
return education || '-'
}
} else if (Array.isArray(education)) {
eduArray = education
}
if (!eduArray || eduArray.length === 0) return '-'
return eduArray.map(e => educationMap[e] || e).join('、')
}
// 格式化婚姻状态数组
const formatMarriageStatus = (status: string | number[] | undefined) => {
if (!status) return '-'
// 如果是字符串,尝试解析为数组
let statusArray: number[] = []
if (typeof status === 'string') {
try {
statusArray = JSON.parse(status)
} catch {
return status || '-'
}
} else if (Array.isArray(status)) {
statusArray = status
}
if (!statusArray || statusArray.length === 0) return '-'
return statusArray.map(s => marriageStatusMap[s] || s).join('、')
}
// 格式化月收入
const formatIncome = (min: number | undefined, max: number | undefined) => {
const incomeMap: Record<number, string> = {
1: '5000以下',
2: '5000-10000',
3: '10000-20000',
4: '20000-50000',
5: '50000以上'
}
// 优先显示最小值(期望最低收入)
if (min) return incomeMap[min] || '-'
if (max) return incomeMap[max] || '-'
return '不限'
}
onMounted(() => {
fetchUserDetail()
})
</script>
<template>
<div
v-loading="loading"
class="user-detail-page"
>
<!-- 页面头部 -->
<div class="page-header">
<el-button
:icon="ArrowLeft"
@click="handleBack"
>
返回列表
</el-button>
<div
v-if="userDetail"
class="header-actions"
>
<el-button
:type="userDetail.status === 1 ? 'danger' : 'success'"
@click="handleToggleStatus"
>
{{ userDetail.status === 1 ? '禁用用户' : '启用用户' }}
</el-button>
</div>
</div>
<template v-if="userDetail">
<!-- 用户基本信息卡片 -->
<el-card
shadow="never"
class="user-card"
>
<div class="user-header">
<el-avatar
:src="getFullImageUrl(userDetail.avatar)"
:size="80"
/>
<div class="user-header__info">
<div class="user-name">
<span class="nickname">{{ userDetail.nickname || '未设置昵称' }}</span>
<StatusTag
:status="userDetail.status"
:options="[
{ value: 1, label: '正常', type: 'success' },
{ value: 2, label: '禁用', type: 'danger' }
]"
/>
</div>
<div class="user-tags">
<el-tag size="small">
{{ userDetail.xiangQinNo }}
</el-tag>
<el-tag
v-if="userDetail.isMember"
type="warning"
size="small"
>
{{ userDetail.memberLevelText }}
</el-tag>
<el-tag
v-if="userDetail.isRealName"
type="success"
size="small"
>
已实名
</el-tag>
</div>
</div>
</div>
</el-card>
<!-- Tab内容区 -->
<el-card shadow="never">
<el-tabs v-model="activeTab">
<!-- 基本信息 -->
<el-tab-pane
label="基本信息"
name="basic"
>
<el-descriptions
:column="3"
border
>
<el-descriptions-item label="用户ID">
{{ userDetail.userId }}
</el-descriptions-item>
<el-descriptions-item label="相亲编号">
{{ userDetail.xiangQinNo }}
</el-descriptions-item>
<el-descriptions-item label="昵称">
{{ userDetail.nickname || '-' }}
</el-descriptions-item>
<el-descriptions-item label="手机号">
{{ userDetail.phone || '-' }}
</el-descriptions-item>
<el-descriptions-item label="性别">
{{ userDetail.genderText || '-' }}
</el-descriptions-item>
<el-descriptions-item label="城市">
{{ userDetail.profile?.workCity || userDetail.city || '-' }}
</el-descriptions-item>
<el-descriptions-item label="实名状态">
<el-tag
v-if="userDetail.isRealName"
type="success"
size="small"
>
已实名
</el-tag>
<el-tag
v-else
type="info"
size="small"
>
未实名
</el-tag>
<el-button
v-if="userDetail.isRealName"
type="danger"
link
size="small"
style="margin-left: 8px;"
@click="handleCancelRealName"
>
取消实名
</el-button>
</el-descriptions-item>
<el-descriptions-item label="会员等级">
<el-tag
v-if="userDetail.isMember"
type="warning"
>
{{ userDetail.memberLevelText }}
</el-tag>
<span v-else>非会员</span>
<el-button
type="primary"
link
size="small"
style="margin-left: 8px;"
@click="handleEditMemberLevel"
>
修改
</el-button>
</el-descriptions-item>
<el-descriptions-item label="会员到期时间">
{{ formatTime(userDetail.memberExpireTime) }}
</el-descriptions-item>
<el-descriptions-item label="剩余联系次数">
<span>{{ userDetail.contactCount }}</span>
<el-button
type="primary"
link
size="small"
style="margin-left: 8px;"
@click="handleEditContactCount"
>
修改
</el-button>
</el-descriptions-item>
<el-descriptions-item label="注册时间">
{{ formatTime(userDetail.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="最后登录">
{{ formatTime(userDetail.lastLoginTime) }}
</el-descriptions-item>
<el-descriptions-item label="OpenID">
{{ userDetail.openId || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<!-- 相亲资料 -->
<el-tab-pane
label="相亲资料"
name="profile"
>
<template v-if="userDetail.profile">
<el-descriptions
:column="3"
border
title="基本资料"
>
<el-descriptions-item label="与孩子关系">
{{ userDetail.profile.relationshipText }}
</el-descriptions-item>
<el-descriptions-item label="姓氏">
{{ userDetail.profile.surname }}
</el-descriptions-item>
<el-descriptions-item label="孩子性别">
{{ userDetail.profile.childGenderText }}
</el-descriptions-item>
<el-descriptions-item label="出生年份">
{{ userDetail.profile.birthYear }}年
</el-descriptions-item>
<el-descriptions-item label="年龄">
{{ userDetail.profile.age }}岁
</el-descriptions-item>
<el-descriptions-item label="学历">
{{ userDetail.profile.educationText }}
</el-descriptions-item>
<el-descriptions-item label="身高">
{{ userDetail.profile.height }}cm
</el-descriptions-item>
<el-descriptions-item label="体重">
{{ userDetail.profile.weight }}kg
</el-descriptions-item>
<el-descriptions-item label="月收入">
{{ userDetail.profile.monthlyIncomeText }}
</el-descriptions-item>
<el-descriptions-item label="工作地点">
{{ userDetail.profile.workProvince }}{{ userDetail.profile.workCity }}{{ userDetail.profile.workDistrict || '' }}
</el-descriptions-item>
<el-descriptions-item label="职业">
{{ userDetail.profile.occupation }}
</el-descriptions-item>
<el-descriptions-item label="家乡">
{{ userDetail.profile.homeProvince || '' }}{{ userDetail.profile.homeCity || '-' }}
</el-descriptions-item>
<el-descriptions-item label="房产情况">
{{ userDetail.profile.houseStatusText }}
</el-descriptions-item>
<el-descriptions-item label="车辆情况">
{{ userDetail.profile.carStatusText }}
</el-descriptions-item>
<el-descriptions-item label="婚姻状态">
{{ userDetail.profile.marriageStatusText }}
</el-descriptions-item>
<el-descriptions-item label="期望结婚时间">
{{ userDetail.profile.expectMarryTimeText }}
</el-descriptions-item>
<el-descriptions-item label="微信号">
{{ userDetail.profile.weChatNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="照片公开">
{{ userDetail.profile.isPhotoPublic ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item
label="相亲介绍"
:span="3"
>
{{ userDetail.profile.introduction || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions
:column="3"
border
title="父母情况"
>
<el-descriptions-item label="父母基本情况">
{{ userDetail.profile.parentStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="父母退休情况">
{{ userDetail.profile.parentRetireStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="父母居住城市">
{{ userDetail.profile.parentProvince || '' }}{{ userDetail.profile.parentCity || '-' }}
</el-descriptions-item>
<el-descriptions-item label="父母住房情况">
{{ userDetail.profile.parentHousingStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="父母养老保险">
{{ userDetail.profile.parentPensionStatus || '-' }}
</el-descriptions-item>
<el-descriptions-item label="父母医疗保险">
{{ userDetail.profile.parentMedicalStatus || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-descriptions
:column="3"
border
title="审核信息"
>
<el-descriptions-item label="审核状态">
<StatusTag
:status="userDetail.profile.auditStatus"
preset="audit"
/>
</el-descriptions-item>
<el-descriptions-item label="审核时间">
{{ formatTime(userDetail.profile.auditTime) }}
</el-descriptions-item>
<el-descriptions-item label="拒绝原因">
{{ userDetail.profile.rejectReason || '-' }}
</el-descriptions-item>
<el-descriptions-item label="资料创建时间">
{{ formatTime(userDetail.profile.createTime) }}
</el-descriptions-item>
<el-descriptions-item label="资料更新时间">
{{ formatTime(userDetail.profile.updateTime) }}
</el-descriptions-item>
</el-descriptions>
<!-- 用户照片 -->
<template v-if="userDetail.photos && userDetail.photos.length > 0">
<el-divider />
<h4>用户照片</h4>
<div class="photo-list">
<div
v-for="photo in userDetail.photos"
:key="photo.photoId"
class="photo-item"
>
<el-image
:src="getFullImageUrl(photo.photoUrl)"
:preview-src-list="userDetail.photos.map(p => getFullImageUrl(p.photoUrl))"
fit="cover"
/>
<el-tag
v-if="photo.isMain"
type="primary"
size="small"
class="main-tag"
>
主照片
</el-tag>
</div>
</div>
</template>
<!-- 择偶要求 -->
<template v-if="userDetail.requirement">
<el-divider />
<el-descriptions
:column="3"
border
title="择偶要求"
>
<el-descriptions-item label="期望年龄">
{{ userDetail.requirement.ageMin || '-' }} ~ {{ userDetail.requirement.ageMax || '-' }}岁
</el-descriptions-item>
<el-descriptions-item label="期望身高">
{{ userDetail.requirement.heightMin || '-' }} ~ {{ userDetail.requirement.heightMax || '-' }}cm
</el-descriptions-item>
<el-descriptions-item label="期望学历">
{{ formatEducation(userDetail.requirement.education) }}
</el-descriptions-item>
<el-descriptions-item label="期望工作城市">
{{ userDetail.requirement.workCity || '-' }}
</el-descriptions-item>
<el-descriptions-item label="期望月收入">
{{ formatIncome(userDetail.requirement.incomeMin, userDetail.requirement.incomeMax) }}
</el-descriptions-item>
<el-descriptions-item label="期望婚姻状态">
{{ formatMarriageStatus(userDetail.requirement.marriageStatus) }}
</el-descriptions-item>
<el-descriptions-item label="是否要求有房">
{{ userDetail.requirement.requireHouse ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item label="是否要求有车">
{{ userDetail.requirement.requireCar ? '是' : '否' }}
</el-descriptions-item>
</el-descriptions>
</template>
</template>
<el-empty
v-else
description="暂无相亲资料"
/>
</el-tab-pane>
<!-- 订单记录 -->
<el-tab-pane
label="订单记录"
name="orders"
>
<template v-if="userDetail.orders && userDetail.orders.length > 0">
<el-table
:data="userDetail.orders"
stripe
border
>
<el-table-column
prop="orderNo"
label="订单号"
width="200"
/>
<el-table-column
prop="orderTypeText"
label="订单类型"
width="120"
/>
<el-table-column
prop="productName"
label="商品名称"
min-width="150"
/>
<el-table-column
label="订单金额"
width="120"
align="right"
>
<template #default="{ row }">
{{ formatMoney(row.amount) }}
</template>
</el-table-column>
<el-table-column
label="实付金额"
width="120"
align="right"
>
<template #default="{ row }">
{{ formatMoney(row.payAmount) }}
</template>
</el-table-column>
<el-table-column
label="状态"
width="100"
align="center"
>
<template #default="{ row }">
<StatusTag
:status="row.status"
:options="[
{ value: 1, label: '待支付', type: 'warning' },
{ value: 2, label: '已支付', type: 'success' },
{ value: 3, label: '已取消', type: 'info' },
{ value: 4, label: '已退款', type: 'danger' }
]"
/>
</template>
</el-table-column>
<el-table-column
label="支付时间"
width="170"
>
<template #default="{ row }">
{{ formatTime(row.payTime) }}
</template>
</el-table-column>
<el-table-column
label="创建时间"
width="170"
>
<template #default="{ row }">
{{ formatTime(row.createTime) }}
</template>
</el-table-column>
</el-table>
</template>
<el-empty
v-else
description="暂无订单记录"
/>
</el-tab-pane>
</el-tabs>
</el-card>
</template>
<el-empty
v-else-if="!loading"
description="用户不存在"
/>
<!-- 修改会员等级对话框 -->
<el-dialog
v-model="memberLevelDialogVisible"
title="修改会员等级"
width="400px"
>
<el-form label-width="100px">
<el-form-item label="会员等级">
<el-select
v-model="memberLevelForm.memberLevel"
placeholder="请选择会员等级"
style="width: 100%;"
>
<el-option
v-for="item in memberLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="memberLevelDialogVisible = false">
取消
</el-button>
<el-button
type="primary"
@click="handleConfirmMemberLevel"
>
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.user-detail-page {
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.user-card {
margin-bottom: 16px;
.user-header {
display: flex;
align-items: center;
gap: 20px;
&__info {
.user-name {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
.nickname {
font-size: 20px;
font-weight: 600;
color: #303133;
}
}
.user-tags {
display: flex;
gap: 8px;
}
}
}
}
.photo-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 16px;
.photo-item {
position: relative;
width: 120px;
height: 120px;
.el-image {
width: 100%;
height: 100%;
border-radius: 8px;
}
.main-tag {
position: absolute;
top: 4px;
left: 4px;
}
}
}
h4 {
margin: 0 0 8px 0;
color: #303133;
}
}
</style>