978 lines
32 KiB
Vue
978 lines
32 KiB
Vue
<script setup lang="ts">
|
||
import { readError } from '@/shared/utils/error'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Search } from '@element-plus/icons-vue'
|
||
import { computed, reactive, ref } from 'vue'
|
||
|
||
import {
|
||
adjustAdminUserGrowthLevel,
|
||
adjustAdminUserGrowthPoints,
|
||
adjustAdminUserWallet,
|
||
fetchAdminUsers,
|
||
freezeAdminUser,
|
||
manualAdminUserRealname,
|
||
revokeAdminUserRealname,
|
||
setAdminUserDepositFreeQuota,
|
||
setAdminUserQualityOwner,
|
||
unfreezeAdminUser,
|
||
type AdminUserItem,
|
||
type AdminUserQuery,
|
||
} from '@/features/admin/api/adminUsers'
|
||
import { defaultRenterGrowthConfig } from '@/features/admin/api/renterGrowth'
|
||
import { centToYuan, formatCent, yuanToCent } from '@/shared/utils/money'
|
||
import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
|
||
import { realnameStatusLabel, userStatusLabel } from '@/shared/utils/statusLabels'
|
||
import { formatDateTime } from '@/shared/utils/time'
|
||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||
import AdminUserDetailDrawer from '../components/AdminUserDetailDrawer.vue'
|
||
|
||
const adminSession = useAdminSessionStore()
|
||
const canAdjustWallet = computed(() => adminSession.hasPermission('user:wallet_adjust'))
|
||
const canAdjustGrowthPoints = computed(() => adminSession.hasPermission('user:growth_points'))
|
||
const canAdjustGrowthLevel = computed(() => adminSession.hasPermission('user:growth_level'))
|
||
const canSetQualityOwner = computed(() => adminSession.hasPermission('user:quality_owner'))
|
||
const canViewUserDetail = computed(
|
||
() =>
|
||
adminSession.hasPermission('wallet:view') ||
|
||
adminSession.hasPermission('order:list') ||
|
||
adminSession.hasPermission('withdrawal:list')
|
||
)
|
||
|
||
const submitting = ref(false)
|
||
const activeUser = ref<AdminUserItem | null>(null)
|
||
const detailUser = ref<AdminUserItem | null>(null)
|
||
const quotaUser = ref<AdminUserItem | null>(null)
|
||
const realnameUser = ref<AdminUserItem | null>(null)
|
||
const manualRealnameUser = ref<AdminUserItem | null>(null)
|
||
const walletUser = ref<AdminUserItem | null>(null)
|
||
const growthUser = ref<AdminUserItem | null>(null)
|
||
const growthLevelUser = ref<AdminUserItem | null>(null)
|
||
const freezeReason = ref('')
|
||
const revokeRealnameReason = ref('')
|
||
const quotaAmount = ref(0)
|
||
const growthForm = reactive({
|
||
targetPoints: 0,
|
||
reason: '',
|
||
})
|
||
const growthLevelForm = reactive({
|
||
targetLevel: '',
|
||
reason: '',
|
||
})
|
||
const growthLevelOptions = defaultRenterGrowthConfig.levels
|
||
const manualRealnameForm = reactive({
|
||
name: '',
|
||
idNo: '',
|
||
reason: '',
|
||
})
|
||
const walletForm = reactive({
|
||
direction: 'out' as 'in' | 'out',
|
||
amountYuan: 0,
|
||
reason: '',
|
||
referenceNo: '',
|
||
})
|
||
|
||
const filters = reactive<AdminUserQuery>({
|
||
keyword: '',
|
||
status: '',
|
||
has_deposit_free_quota: false,
|
||
quality_owner_only: false,
|
||
})
|
||
|
||
const {
|
||
loading,
|
||
data: users,
|
||
total,
|
||
currentPage,
|
||
currentPageSize,
|
||
load: loadUsers,
|
||
} = useAdminPaginatedTable<AdminUserItem>({
|
||
fetchFn: (page, pageSize) => fetchAdminUsers(page, pageSize, filters),
|
||
})
|
||
|
||
function queryUsers() {
|
||
currentPage.value = 1
|
||
void loadUsers()
|
||
}
|
||
|
||
function resetFilters() {
|
||
filters.keyword = ''
|
||
filters.status = ''
|
||
filters.has_deposit_free_quota = false
|
||
filters.quality_owner_only = false
|
||
queryUsers()
|
||
}
|
||
|
||
function openUserDetail(row: AdminUserItem) {
|
||
detailUser.value = row
|
||
}
|
||
|
||
function openFreeze(row: AdminUserItem) {
|
||
activeUser.value = row
|
||
freezeReason.value = ''
|
||
}
|
||
|
||
function openDepositQuota(row: AdminUserItem) {
|
||
quotaUser.value = row
|
||
quotaAmount.value = centToYuan(row.deposit_free_quota_cent)
|
||
}
|
||
|
||
function openGrowthPoints(row: AdminUserItem) {
|
||
growthUser.value = row
|
||
growthForm.targetPoints = Number(row.renter_growth_points || 0)
|
||
growthForm.reason = ''
|
||
}
|
||
|
||
function openGrowthLevel(row: AdminUserItem) {
|
||
growthLevelUser.value = row
|
||
growthLevelForm.targetLevel = row.renter_growth_level_override || ''
|
||
growthLevelForm.reason = ''
|
||
}
|
||
|
||
function openWalletAdjust(row: AdminUserItem) {
|
||
walletUser.value = row
|
||
walletForm.direction = 'out'
|
||
walletForm.amountYuan = 0
|
||
walletForm.reason = ''
|
||
walletForm.referenceNo = ''
|
||
}
|
||
|
||
async function handleSetDepositQuota() {
|
||
if (!quotaUser.value) return
|
||
submitting.value = true
|
||
try {
|
||
await setAdminUserDepositFreeQuota(quotaUser.value.id, yuanToCent(quotaAmount.value))
|
||
ElMessage.success('人工免押额度已更新')
|
||
quotaUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '设置免押额度失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleAdjustGrowthPoints() {
|
||
if (!growthUser.value) return
|
||
const targetPoints = Number(growthForm.targetPoints)
|
||
if (!Number.isInteger(targetPoints) || targetPoints < 0 || targetPoints > 1000000000) {
|
||
ElMessage.warning('目标积分必须是 0 到 10 亿之间的整数')
|
||
return
|
||
}
|
||
if (targetPoints === Number(growthUser.value.renter_growth_points || 0)) {
|
||
ElMessage.warning('目标积分与当前积分相同')
|
||
return
|
||
}
|
||
if (growthForm.reason.trim().length < 2) {
|
||
ElMessage.warning('请填写至少 2 个字的调整原因')
|
||
return
|
||
}
|
||
const direction = targetPoints > growthUser.value.renter_growth_points ? '提升' : '降低'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确认将用户 ${growthUser.value.phone || growthUser.value.id} 的成长积分从 ${growthUser.value.renter_growth_points} ${direction}至 ${targetPoints}?`,
|
||
'确认调整积分',
|
||
{
|
||
type: 'warning',
|
||
confirmButtonText: '确认调整',
|
||
cancelButtonText: '取消',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await adjustAdminUserGrowthPoints(growthUser.value.id, {
|
||
target_points: targetPoints,
|
||
reason: growthForm.reason.trim(),
|
||
})
|
||
ElMessage.success('成长积分已更新,等级已自动重新计算')
|
||
growthUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '调整成长积分失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleAdjustGrowthLevel() {
|
||
if (!growthLevelUser.value) return
|
||
const targetLevel = growthLevelForm.targetLevel.trim()
|
||
if (targetLevel === (growthLevelUser.value.renter_growth_level_override || '')) {
|
||
ElMessage.warning('目标等级与当前手动等级相同')
|
||
return
|
||
}
|
||
if (growthLevelForm.reason.trim().length < 2) {
|
||
ElMessage.warning('请填写至少 2 个字的调整原因')
|
||
return
|
||
}
|
||
const targetName = targetLevel
|
||
? growthLevelOptions.find(level => level.code === targetLevel)?.name || targetLevel
|
||
: '按积分自动计算'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确认将用户 ${growthLevelUser.value.phone || growthLevelUser.value.id} 的成长等级调整为「${targetName}」?\n手动设置后会锁定等级,后续积分变动不会自动覆盖。`,
|
||
'确认调整等级',
|
||
{
|
||
type: 'warning',
|
||
confirmButtonText: '确认调整',
|
||
cancelButtonText: '取消',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await adjustAdminUserGrowthLevel(growthLevelUser.value.id, {
|
||
target_level: targetLevel,
|
||
reason: growthLevelForm.reason.trim(),
|
||
})
|
||
ElMessage.success('成长等级已更新')
|
||
growthLevelUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '调整成长等级失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleFreeze() {
|
||
if (!activeUser.value) return
|
||
submitting.value = true
|
||
try {
|
||
await freezeAdminUser(activeUser.value.id, freezeReason.value)
|
||
ElMessage.success('用户已冻结')
|
||
activeUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '冻结失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleUnfreeze(row: AdminUserItem) {
|
||
submitting.value = true
|
||
try {
|
||
await unfreezeAdminUser(row.id)
|
||
ElMessage.success('用户已解冻')
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '解冻失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function openRevokeRealname(row: AdminUserItem) {
|
||
realnameUser.value = row
|
||
revokeRealnameReason.value = ''
|
||
}
|
||
|
||
function openManualRealname(row: AdminUserItem) {
|
||
manualRealnameUser.value = row
|
||
manualRealnameForm.name = ''
|
||
manualRealnameForm.idNo = ''
|
||
manualRealnameForm.reason = ''
|
||
}
|
||
|
||
async function handleManualRealname() {
|
||
if (!manualRealnameUser.value) return
|
||
if (!manualRealnameForm.name.trim()) {
|
||
ElMessage.warning('请输入真实姓名')
|
||
return
|
||
}
|
||
if (!manualRealnameForm.idNo.trim() || manualRealnameForm.idNo.trim().length !== 18) {
|
||
ElMessage.warning('请输入18位身份证号')
|
||
return
|
||
}
|
||
submitting.value = true
|
||
try {
|
||
await manualAdminUserRealname(manualRealnameUser.value.id, {
|
||
name: manualRealnameForm.name,
|
||
idNo: manualRealnameForm.idNo,
|
||
reason: manualRealnameForm.reason,
|
||
})
|
||
ElMessage.success('已完成人工实名')
|
||
manualRealnameUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '人工实名失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleRevokeRealname() {
|
||
if (!realnameUser.value) return
|
||
submitting.value = true
|
||
try {
|
||
await revokeAdminUserRealname(realnameUser.value.id, revokeRealnameReason.value)
|
||
ElMessage.success('已撤销实名,用户可重新认证')
|
||
realnameUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, '撤销实名失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleWalletAdjust() {
|
||
if (!walletUser.value) return
|
||
const amountYuan = Number(walletForm.amountYuan || 0)
|
||
if (!(amountYuan > 0)) {
|
||
ElMessage.warning('请输入大于 0 的金额')
|
||
return
|
||
}
|
||
if (amountYuan > 100000) {
|
||
ElMessage.warning('单笔调账不能超过 10 万元')
|
||
return
|
||
}
|
||
if (walletForm.reason.trim().length < 2) {
|
||
ElMessage.warning('请填写至少 2 个字的调账原因')
|
||
return
|
||
}
|
||
if (
|
||
walletForm.direction === 'out' &&
|
||
yuanToCent(amountYuan) > Number(walletUser.value.available_balance_cent || 0)
|
||
) {
|
||
ElMessage.warning('扣款金额不能超过可用余额')
|
||
return
|
||
}
|
||
|
||
const actionLabel = walletForm.direction === 'in' ? '加款' : '扣款'
|
||
const amountText = `¥${amountYuan.toFixed(1)}`
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
`确认为用户 ${walletUser.value.phone || walletUser.value.id} ${actionLabel} ${amountText}?\n操作将写入资金流水与审计日志。`,
|
||
`确认${actionLabel}`,
|
||
{
|
||
type: 'warning',
|
||
confirmButtonText: `确认${actionLabel}`,
|
||
cancelButtonText: '取消',
|
||
}
|
||
)
|
||
} catch {
|
||
return
|
||
}
|
||
|
||
submitting.value = true
|
||
try {
|
||
await adjustAdminUserWallet(walletUser.value.id, {
|
||
direction: walletForm.direction,
|
||
amount_cent: yuanToCent(amountYuan),
|
||
reason: walletForm.reason.trim(),
|
||
reference_no: walletForm.referenceNo.trim() || undefined,
|
||
})
|
||
ElMessage.success(`${actionLabel}成功`)
|
||
walletUser.value = null
|
||
await loadUsers()
|
||
} catch (error) {
|
||
ElMessage.error(readError(error, `${actionLabel}失败`))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
async function handleToggleQualityOwner(row: AdminUserItem) {
|
||
const enabling = row.quality_owner_status !== 'active'
|
||
try {
|
||
const { value } = await ElMessageBox.prompt(
|
||
enabling
|
||
? '后台手动开通将直接发放优质号主资格并跳过保证金缴纳,请填写原因。'
|
||
: '取消前需确保极品专区没有进行中的账号,否则操作会被拒绝;取消后用户自缴保证金将退回可用余额。',
|
||
enabling ? '设置优质号主' : '取消优质号主',
|
||
{
|
||
inputPlaceholder: '请填写 2-200 字操作原因',
|
||
inputValidator: value => {
|
||
const length = value.trim().length
|
||
return (length >= 2 && length <= 200) || '请填写 2-200 字操作原因'
|
||
},
|
||
confirmButtonText: enabling ? '确认开通' : '确认取消',
|
||
cancelButtonText: '取消',
|
||
type: 'warning',
|
||
}
|
||
)
|
||
submitting.value = true
|
||
await setAdminUserQualityOwner(row.id, {
|
||
enabled: enabling,
|
||
reason: value.trim(),
|
||
})
|
||
ElMessage.success(enabling ? '已设置为优质号主' : '已取消优质号主')
|
||
await loadUsers()
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
ElMessage.error(readError(error, '设置优质号主失败'))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
function moneyCent(value: number | string | undefined) {
|
||
return formatCent(Number(value || 0))
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="page">
|
||
<div class="page-header-row">
|
||
<div class="page-header">
|
||
<p class="eyebrow">Users</p>
|
||
<h1>用户管理</h1>
|
||
<p>查看用户信息,处理成长积分、人工免押、实名与余额调账。</p>
|
||
</div>
|
||
<el-button @click="loadUsers">刷新</el-button>
|
||
</div>
|
||
|
||
<div class="user-filter-bar">
|
||
<el-form inline @submit.prevent>
|
||
<el-form-item label="关键词">
|
||
<el-input
|
||
v-model="filters.keyword"
|
||
clearable
|
||
placeholder="手机号 / 昵称 / 用户ID"
|
||
style="width: 220px"
|
||
@keyup.enter="queryUsers"
|
||
@clear="queryUsers"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select
|
||
v-model="filters.status"
|
||
clearable
|
||
placeholder="全部状态"
|
||
style="width: 140px"
|
||
@change="queryUsers"
|
||
>
|
||
<el-option label="正常" value="active" />
|
||
<el-option label="已冻结" value="frozen" />
|
||
<el-option label="已禁用" value="disabled" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="免押额度">
|
||
<el-checkbox v-model="filters.has_deposit_free_quota" @change="queryUsers">
|
||
仅看有额度
|
||
</el-checkbox>
|
||
</el-form-item>
|
||
<el-form-item label="号主类型">
|
||
<el-checkbox v-model="filters.quality_owner_only" @change="queryUsers">
|
||
仅看优质号主
|
||
</el-checkbox>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" :icon="Search" :loading="loading" @click="queryUsers"
|
||
>查询</el-button
|
||
>
|
||
<el-button @click="resetFilters">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<el-table v-loading="loading" class="table-panel" :data="users" empty-text="未找到匹配的用户">
|
||
<el-table-column prop="id" label="用户ID" width="80" />
|
||
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||
<el-table-column prop="phone" label="手机号" min-width="120" />
|
||
<el-table-column label="状态" width="90">
|
||
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="实名状态" width="100">
|
||
<template #default="{ row }">{{ realnameStatusLabel(row.realname_status) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="号主类型" width="110">
|
||
<template #default="{ row }">
|
||
<el-tag v-if="row.quality_owner_status === 'active'" type="warning" effect="light">
|
||
优质号主
|
||
</el-tag>
|
||
<span v-else class="muted-text">普通</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="成长等级" width="100">
|
||
<template #default="{ row }">
|
||
<div class="growth-level-cell">
|
||
<el-tag type="success" effect="light">{{
|
||
row.renter_growth_level_name || '普通'
|
||
}}</el-tag>
|
||
<el-tag
|
||
v-if="row.renter_growth_level_manual"
|
||
type="warning"
|
||
effect="plain"
|
||
size="small"
|
||
>
|
||
手动
|
||
</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="成长积分" width="100" align="right">
|
||
<template #default="{ row }">{{ Number(row.renter_growth_points || 0) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="可用余额" width="108" align="right">
|
||
<template #default="{ row }">
|
||
<span class="balance-available">¥{{ moneyCent(row.available_balance_cent) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="冻结余额" width="108" align="right">
|
||
<template #default="{ row }">
|
||
<span
|
||
class="balance-frozen"
|
||
:class="{ 'is-positive': Number(row.frozen_balance_cent || 0) > 0 }"
|
||
>
|
||
¥{{ moneyCent(row.frozen_balance_cent) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="免押总额" width="150" align="right">
|
||
<template #default="{ row }">
|
||
<div class="quota-cell">
|
||
<strong>¥{{ moneyCent(row.deposit_free_effective_quota_cent) }}</strong>
|
||
<span>
|
||
等级 {{ moneyCent(row.deposit_free_level_quota_cent) }} + 人工
|
||
{{ moneyCent(row.deposit_free_manual_quota_cent) }}
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="剩余免押" width="100" align="right">
|
||
<template #default="{ row }">¥{{ moneyCent(row.deposit_free_remaining_cent) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="注册时间" min-width="160">
|
||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="520" fixed="right" align="right">
|
||
<template #default="{ row }">
|
||
<div class="table-actions">
|
||
<el-button
|
||
v-if="canViewUserDetail"
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
@click="openUserDetail(row)"
|
||
>
|
||
明细
|
||
</el-button>
|
||
<el-button
|
||
v-if="canAdjustWallet"
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
@click="openWalletAdjust(row)"
|
||
>
|
||
余额
|
||
</el-button>
|
||
<el-button
|
||
v-if="canAdjustGrowthPoints"
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
@click="openGrowthPoints(row)"
|
||
>
|
||
积分
|
||
</el-button>
|
||
<el-button
|
||
v-if="canAdjustGrowthLevel"
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
@click="openGrowthLevel(row)"
|
||
>
|
||
等级
|
||
</el-button>
|
||
<el-button text type="primary" size="small" @click="openDepositQuota(row)">
|
||
人工免押
|
||
</el-button>
|
||
<el-button
|
||
v-if="canSetQualityOwner"
|
||
text
|
||
type="warning"
|
||
size="small"
|
||
:loading="submitting"
|
||
@click="handleToggleQualityOwner(row)"
|
||
>
|
||
{{ row.quality_owner_status === 'active' ? '取消优质' : '设为优质' }}
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.realname_status !== 'verified'"
|
||
text
|
||
type="success"
|
||
size="small"
|
||
:loading="submitting"
|
||
@click="openManualRealname(row)"
|
||
>
|
||
实名
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.realname_status !== 'unverified'"
|
||
text
|
||
type="warning"
|
||
size="small"
|
||
:loading="submitting"
|
||
@click="openRevokeRealname(row)"
|
||
>
|
||
撤销实名
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.status === 'active'"
|
||
text
|
||
type="danger"
|
||
size="small"
|
||
:loading="submitting"
|
||
@click="openFreeze(row)"
|
||
>
|
||
冻结
|
||
</el-button>
|
||
<el-button
|
||
v-else
|
||
text
|
||
type="primary"
|
||
size="small"
|
||
:loading="submitting"
|
||
@click="handleUnfreeze(row)"
|
||
>
|
||
解冻
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<AdminTablePagination
|
||
v-if="total > 0"
|
||
v-model:current-page="currentPage"
|
||
v-model:page-size="currentPageSize"
|
||
:total="total"
|
||
:loading="loading"
|
||
@page-change="loadUsers"
|
||
/>
|
||
|
||
<el-dialog
|
||
:model-value="!!activeUser"
|
||
title="冻结用户"
|
||
width="560px"
|
||
@update:model-value="activeUser = null"
|
||
>
|
||
<div v-if="activeUser" class="dialog-body">
|
||
<p>
|
||
<strong>{{ activeUser.phone }}</strong> · {{ activeUser.nickname }}
|
||
</p>
|
||
<el-input
|
||
v-model="freezeReason"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写冻结原因,便于审计追踪"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="activeUser = null">取消</el-button>
|
||
<el-button type="danger" :loading="submitting" @click="handleFreeze">确认冻结</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!manualRealnameUser"
|
||
title="人工实名认证"
|
||
width="560px"
|
||
@update:model-value="manualRealnameUser = null"
|
||
>
|
||
<div v-if="manualRealnameUser" class="dialog-body">
|
||
<p>
|
||
<strong>{{ manualRealnameUser.phone }}</strong> · {{ manualRealnameUser.nickname }}
|
||
</p>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
title="请确认已通过线下或客服材料核验姓名与身份证号,提交后该用户会直接变为已实名。"
|
||
/>
|
||
<el-form label-position="top" @submit.prevent>
|
||
<el-form-item label="真实姓名">
|
||
<el-input v-model="manualRealnameForm.name" clearable placeholder="请输入真实姓名" />
|
||
</el-form-item>
|
||
<el-form-item label="身份证号">
|
||
<el-input
|
||
v-model="manualRealnameForm.idNo"
|
||
clearable
|
||
maxlength="18"
|
||
placeholder="请输入18位身份证号"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="处理原因">
|
||
<el-input
|
||
v-model="manualRealnameForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="填写处理原因,便于审计追踪"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="manualRealnameUser = null">取消</el-button>
|
||
<el-button type="success" :loading="submitting" @click="handleManualRealname"
|
||
>确认实名</el-button
|
||
>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!realnameUser"
|
||
title="撤销实名认证"
|
||
width="560px"
|
||
@update:model-value="realnameUser = null"
|
||
>
|
||
<div v-if="realnameUser" class="dialog-body">
|
||
<p>
|
||
<strong>{{ realnameUser.phone }}</strong> · {{ realnameUser.nickname }}
|
||
</p>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
title="撤销后该用户实名信息将被清除,需重新完成实名认证后才能下单与登记收款码。"
|
||
/>
|
||
<el-input
|
||
v-model="revokeRealnameReason"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="填写撤销原因(如实名姓名与收款账户不一致),便于审计追踪"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="realnameUser = null">取消</el-button>
|
||
<el-button type="warning" :loading="submitting" @click="handleRevokeRealname"
|
||
>确认撤销</el-button
|
||
>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!quotaUser"
|
||
title="设置人工免押额度"
|
||
width="520px"
|
||
@update:model-value="quotaUser = null"
|
||
>
|
||
<div v-if="quotaUser" class="dialog-body">
|
||
<p>
|
||
<strong>{{ quotaUser.phone }}</strong> · {{ quotaUser.nickname }}
|
||
</p>
|
||
<p>
|
||
等级额度 ¥{{ moneyCent(quotaUser.deposit_free_level_quota_cent) }},人工额度 ¥{{
|
||
moneyCent(quotaUser.deposit_free_manual_quota_cent)
|
||
}}
|
||
</p>
|
||
<div class="quota-summary">
|
||
<span>有效总额 ¥{{ moneyCent(quotaUser.deposit_free_effective_quota_cent) }}</span>
|
||
<span>已占用 ¥{{ moneyCent(quotaUser.deposit_free_used_cent) }}</span>
|
||
<strong>剩余 ¥{{ moneyCent(quotaUser.deposit_free_remaining_cent) }}</strong>
|
||
</div>
|
||
<el-input-number
|
||
v-model="quotaAmount"
|
||
class="full-control"
|
||
:min="0"
|
||
:precision="0"
|
||
:step="100"
|
||
/>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :disabled="submitting" @click="quotaUser = null">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="handleSetDepositQuota"
|
||
>保存额度</el-button
|
||
>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!growthUser"
|
||
title="调整成长积分"
|
||
width="540px"
|
||
@update:model-value="growthUser = null"
|
||
>
|
||
<div v-if="growthUser" class="dialog-body growth-dialog">
|
||
<p>
|
||
<strong>{{ growthUser.phone }}</strong> · {{ growthUser.nickname || '-' }} · 当前
|
||
{{ growthUser.renter_growth_level_name }}
|
||
</p>
|
||
<div class="growth-current">
|
||
<span>当前积分</span>
|
||
<strong>{{ growthUser.renter_growth_points }}</strong>
|
||
</div>
|
||
<el-form label-position="top" @submit.prevent>
|
||
<el-form-item label="目标积分">
|
||
<el-input-number
|
||
v-model="growthForm.targetPoints"
|
||
class="full-control"
|
||
:min="0"
|
||
:max="1000000000"
|
||
:step="100"
|
||
step-strictly
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="调整原因(必填)">
|
||
<el-input
|
||
v-model="growthForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="例如:运营活动赠送或修正误发积分"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :disabled="submitting" @click="growthUser = null">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="handleAdjustGrowthPoints">
|
||
确认调整
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!growthLevelUser"
|
||
title="调整成长等级"
|
||
width="540px"
|
||
@update:model-value="growthLevelUser = null"
|
||
>
|
||
<div v-if="growthLevelUser" class="dialog-body growth-dialog">
|
||
<p>
|
||
<strong>{{ growthLevelUser.phone }}</strong> · {{ growthLevelUser.nickname || '-' }} ·
|
||
当前
|
||
{{ growthLevelUser.renter_growth_level_name }}
|
||
{{ growthLevelUser.renter_growth_level_manual ? '(手动锁定)' : '(自动计算)' }}
|
||
</p>
|
||
<div class="growth-current">
|
||
<span>当前成长积分</span>
|
||
<strong>{{ growthLevelUser.renter_growth_points }}</strong>
|
||
</div>
|
||
<el-form label-position="top" @submit.prevent>
|
||
<el-form-item label="目标等级">
|
||
<el-select v-model="growthLevelForm.targetLevel" class="full-control">
|
||
<el-option label="恢复按积分自动计算" value="" />
|
||
<el-option
|
||
v-for="level in growthLevelOptions"
|
||
:key="level.code"
|
||
:label="level.name"
|
||
:value="level.code"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-alert
|
||
type="warning"
|
||
:closable="false"
|
||
title="手动等级会锁定,后续积分变动不会自动覆盖;选择“恢复按积分自动计算”可解除锁定。操作会写入审计日志。"
|
||
/>
|
||
<el-form-item label="调整原因(必填)" class="level-reason-item">
|
||
<el-input
|
||
v-model="growthLevelForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="例如:特殊用户授权、客服补偿或修正异常等级"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :disabled="submitting" @click="growthLevelUser = null">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="handleAdjustGrowthLevel">
|
||
确认调整
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
:model-value="!!walletUser"
|
||
title="余额管理"
|
||
width="560px"
|
||
@update:model-value="walletUser = null"
|
||
>
|
||
<div v-if="walletUser" class="dialog-body wallet-dialog">
|
||
<p>
|
||
<strong>{{ walletUser.phone }}</strong> · {{ walletUser.nickname || '-' }} · ID
|
||
{{ walletUser.id }}
|
||
</p>
|
||
<div class="wallet-balance-cards">
|
||
<div>
|
||
<span>可用余额</span>
|
||
<strong>¥{{ moneyCent(walletUser.available_balance_cent) }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>冻结余额</span>
|
||
<strong>¥{{ moneyCent(walletUser.frozen_balance_cent) }}</strong>
|
||
</div>
|
||
</div>
|
||
<el-alert
|
||
v-if="Number(walletUser.frozen_balance_cent || 0) > 0"
|
||
type="warning"
|
||
:closable="false"
|
||
show-icon
|
||
title="该用户有冻结余额,通常对应进行中的提现单。冻结金额请到「提现审核」处理;此处仅调整可用余额。"
|
||
/>
|
||
<el-alert
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
title="扣款用于线下已打款核销;加款用于客服补偿。操作会写入资金流水与审计日志。"
|
||
/>
|
||
<el-form label-position="top" @submit.prevent>
|
||
<el-form-item label="调账类型">
|
||
<el-radio-group v-model="walletForm.direction">
|
||
<el-radio-button value="out">扣款(核销)</el-radio-button>
|
||
<el-radio-button value="in">加款(补偿)</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="金额(元)">
|
||
<el-input-number
|
||
v-model="walletForm.amountYuan"
|
||
class="full-control"
|
||
:min="0.1"
|
||
:max="100000"
|
||
:precision="1"
|
||
:step="10"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="原因(必填,计入审计)">
|
||
<el-input
|
||
v-model="walletForm.reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
maxlength="200"
|
||
show-word-limit
|
||
placeholder="例如:用户无法提现,已线下转账核销可用余额"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="外部单号(可选)">
|
||
<el-input
|
||
v-model="walletForm.referenceNo"
|
||
clearable
|
||
maxlength="64"
|
||
placeholder="线下转账单号 / 客服工单号"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<el-button :disabled="submitting" @click="walletUser = null">取消</el-button>
|
||
<el-button
|
||
:type="walletForm.direction === 'out' ? 'danger' : 'success'"
|
||
:loading="submitting"
|
||
@click="handleWalletAdjust"
|
||
>
|
||
确认{{ walletForm.direction === 'out' ? '扣款' : '加款' }}
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<AdminUserDetailDrawer
|
||
:model-value="!!detailUser"
|
||
:user="detailUser"
|
||
@update:model-value="(val: boolean) => !val && (detailUser = null)"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped src="./AdminUsersView.css"></style>
|