Files
hfb_sys/frontend/src/features/admin/views/AdminUsersView.vue
T

978 lines
32 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.
<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>