feat: 重构充值提现功能,添加冷钱包管理

后端改动:
- 新增冷钱包管理模块(ColdWallet实体、Mapper、Service、Controller)
- 充值流程:创建订单→显示钱包地址→用户确认打款→管理员审核
- 提现流程:用户输入地址和联系方式→冻结余额→管理员审核
- OrderFund新增字段:walletId, walletAddress, withdrawContact, payTime, confirmTime

前端改动(monisuo-admin):
- 新增冷钱包管理页面(wallets.vue)
- 优化订单管理页面,支持新的状态流转
- 添加调试日志帮助排查登录问题

前端改动(flutter_monisuo):
- 更新OrderFund模型支持新字段
- 充值成功后显示钱包地址弹窗
- 提现时收集提现地址和联系方式
- 新增资金订单页面

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sion
2026-03-22 23:15:23 +08:00
parent a476d0a23b
commit 91227b7e51
21 changed files with 2225 additions and 229 deletions

View File

@@ -107,20 +107,68 @@ function formatAmount(amount: number): string {
return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function getStatusVariant(status: number): 'default' | 'secondary' | 'destructive' {
if (status === 1)
return 'secondary'
if (status === 2)
return 'default'
return 'destructive'
// 根据订单类型和状态获取状态样式
function getStatusVariant(order: OrderFund): 'default' | 'secondary' | 'destructive' | 'outline' {
const { type, status } = order
// 充值状态: 1=待付款, 2=待确认, 3=已完成, 4=已驳回, 5=已取消
// 提现状态: 1=待审批, 2=已完成, 3=已驳回, 4=已取消
if (type === 1) {
// 充值
if (status === 1) return 'secondary' // 待付款
if (status === 2) return 'default' // 待确认
if (status === 3) return 'default' // 已完成
return 'destructive' // 已驳回/已取消
}
else {
// 提现
if (status === 1) return 'default' // 待审批
if (status === 2) return 'default' // 已完成
return 'destructive' // 已驳回/已取消
}
}
function getStatusText(status: number): string {
if (status === 1)
return '待审批'
if (status === 2)
return '已通过'
return '已驳回'
// 根据订单类型和状态获取状态文本
function getStatusText(order: OrderFund): string {
const { type, status } = order
if (type === 1) {
// 充值状态
switch (status) {
case 1: return '待付款'
case 2: return '待确认'
case 3: return '已完成'
case 4: return '已驳回'
case 5: return '已取消'
default: return '未知'
}
}
else {
// 提现状态
switch (status) {
case 1: return '待审批'
case 2: return '已完成'
case 3: return '已驳回'
case 4: return '已取消'
default: return '未知'
}
}
}
// 判断订单是否可审批
// 充值: 仅待确认(status=2)可审批
// 提现: 仅待审批(status=1)可审批
function canApprove(order: OrderFund): boolean {
if (order.type === 1) {
return order.status === 2 // 充值待确认
}
else {
return order.status === 1 // 提现待审批
}
}
// 复制到剪贴板
function copyToClipboard(text: string) {
navigator.clipboard.writeText(text)
toast.success('已复制到剪贴板')
}
</script>
@@ -153,7 +201,11 @@ function getStatusText(status: number): string {
<UiTableHead class="text-right">
金额
</UiTableHead>
<UiTableHead>状态</UiTableHead>
<UiTableHead class="hidden lg:table-cell">
地址/联系方式
</UiTableHead>
<UiTableHead class="hidden xl:table-cell">
时间
</UiTableHead>
<UiTableHead class="text-right">
@@ -163,12 +215,12 @@ function getStatusText(status: number): string {
</UiTableHeader>
<UiTableBody>
<UiTableRow v-if="pendingLoading">
<UiTableCell :col-span="6" class="text-center py-8">
<UiTableCell :col-span="8" class="text-center py-8">
<UiSpinner class="mx-auto" />
</UiTableCell>
</UiTableRow>
<UiTableRow v-else-if="pendingOrders.length === 0">
<UiTableCell :col-span="6" class="text-center py-8 text-muted-foreground">
<UiTableCell :col-span="8" class="text-center py-8 text-muted-foreground">
<Icon icon="lucide:inbox" class="size-8 mx-auto mb-2 opacity-50" />
<p>暂无待审批订单</p>
</UiTableCell>
@@ -187,8 +239,27 @@ function getStatusText(status: number): string {
<UiTableCell class="text-right font-mono font-medium">
¥{{ formatAmount(order.amount) }}
</UiTableCell>
<UiTableCell class="hidden lg:table-cell text-muted-foreground text-sm">
{{ order.createTime }}
<UiTableCell>
<UiBadge :variant="getStatusVariant(order)">
{{ getStatusText(order) }}
</UiBadge>
</UiTableCell>
<UiTableCell class="hidden lg:table-cell">
<div v-if="order.walletAddress" class="max-w-[150px]">
<div class="font-mono text-xs truncate" :title="order.walletAddress">
{{ order.walletAddress }}
</div>
<div v-if="order.withdrawContact" class="text-xs text-muted-foreground">
{{ order.withdrawContact }}
</div>
</div>
<span v-else class="text-muted-foreground">-</span>
</UiTableCell>
<UiTableCell class="hidden xl:table-cell text-muted-foreground text-sm">
<div>{{ order.createTime }}</div>
<div v-if="order.payTime" class="text-xs">
打款: {{ order.payTime }}
</div>
</UiTableCell>
<UiTableCell class="text-right">
<div class="flex justify-end gap-1">
@@ -196,6 +267,7 @@ function getStatusText(status: number): string {
<Icon icon="lucide:eye" class="size-4" />
</UiButton>
<UiButton
v-if="canApprove(order)"
size="sm"
:disabled="approveMutation.isPending.value"
@click="openApproveDialog(order, 2)"
@@ -203,6 +275,7 @@ function getStatusText(status: number): string {
通过
</UiButton>
<UiButton
v-if="canApprove(order)"
size="sm"
variant="destructive"
:disabled="approveMutation.isPending.value"
@@ -233,19 +306,38 @@ function getStatusText(status: number): string {
{{ order.username }}
</div>
</div>
<UiBadge :variant="order.type === 1 ? 'default' : 'destructive'">
{{ order.type === 1 ? '充值' : '提现' }}
</UiBadge>
<div class="text-right">
<UiBadge :variant="order.type === 1 ? 'default' : 'destructive'" class="mb-1">
{{ order.type === 1 ? '充值' : '提现' }}
</UiBadge>
<UiBadge :variant="getStatusVariant(order)" class="block">
{{ getStatusText(order) }}
</UiBadge>
</div>
</div>
<div class="mt-3 pt-3 border-t">
<div class="text-xl font-mono font-bold" :class="order.type === 1 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
{{ order.type === 1 ? '+' : '-' }}¥{{ formatAmount(order.amount) }}
</div>
<!-- 显示地址信息 -->
<div v-if="order.walletAddress" class="mt-2 text-sm">
<span class="text-muted-foreground">{{ order.type === 1 ? '充值地址' : '提现地址' }}:</span>
<div class="font-mono text-xs break-all mt-1 flex items-center gap-1">
{{ order.walletAddress }}
<Icon icon="lucide:copy" class="size-3 cursor-pointer" @click="copyToClipboard(order.walletAddress!)" />
</div>
</div>
<div v-if="order.withdrawContact" class="text-sm text-muted-foreground mt-1">
联系方式: {{ order.withdrawContact }}
</div>
<div class="text-sm text-muted-foreground mt-1">
{{ order.createTime }}
</div>
<div v-if="order.payTime" class="text-xs text-muted-foreground">
确认打款: {{ order.payTime }}
</div>
</div>
<div class="mt-3 flex gap-2">
<div v-if="canApprove(order)" class="mt-3 flex gap-2">
<UiButton size="sm" class="flex-1" @click="openApproveDialog(order, 2)">
通过
</UiButton>
@@ -253,6 +345,11 @@ function getStatusText(status: number): string {
驳回
</UiButton>
</div>
<div v-else class="mt-3">
<UiButton size="sm" variant="outline" class="w-full" @click="viewOrderDetail(order)">
查看详情
</UiButton>
</div>
</UiCard>
</template>
<div v-else class="text-center py-8 text-muted-foreground">
@@ -297,14 +394,20 @@ function getStatusText(status: number): string {
全部
</UiSelectItem>
<UiSelectItem :value="1">
待审批
付款/审批
</UiSelectItem>
<UiSelectItem :value="2">
已通过
待确认/已完成
</UiSelectItem>
<UiSelectItem :value="3">
已完成
</UiSelectItem>
<UiSelectItem :value="4">
已驳回
</UiSelectItem>
<UiSelectItem :value="5">
已取消
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
@@ -328,6 +431,9 @@ function getStatusText(status: number): string {
金额
</UiTableHead>
<UiTableHead>状态</UiTableHead>
<UiTableHead class="hidden lg:table-cell">
地址
</UiTableHead>
<UiTableHead class="hidden xl:table-cell">
时间
</UiTableHead>
@@ -341,12 +447,12 @@ function getStatusText(status: number): string {
</UiTableHeader>
<UiTableBody>
<UiTableRow v-if="allLoading">
<UiTableCell :col-span="8" class="text-center py-8">
<UiTableCell :col-span="9" class="text-center py-8">
<UiSpinner class="mx-auto" />
</UiTableCell>
</UiTableRow>
<UiTableRow v-else-if="allOrders.length === 0">
<UiTableCell :col-span="8" class="text-center py-8 text-muted-foreground">
<UiTableCell :col-span="9" class="text-center py-8 text-muted-foreground">
暂无数据
</UiTableCell>
</UiTableRow>
@@ -364,14 +470,25 @@ function getStatusText(status: number): string {
¥{{ formatAmount(order.amount) }}
</UiTableCell>
<UiTableCell>
<UiBadge :variant="getStatusVariant(order.status)">
{{ getStatusText(order.status) }}
<UiBadge :variant="getStatusVariant(order)">
{{ getStatusText(order) }}
</UiBadge>
</UiTableCell>
<UiTableCell class="hidden xl:table-cell text-muted-foreground text-sm">
{{ order.createTime }}
<UiTableCell class="hidden lg:table-cell">
<div v-if="order.walletAddress" class="max-w-[120px]">
<div class="font-mono text-xs truncate" :title="order.walletAddress">
{{ order.walletAddress }}
</div>
</div>
<span v-else class="text-muted-foreground">-</span>
</UiTableCell>
<UiTableCell class="hidden lg:table-cell text-sm text-muted-foreground max-w-[150px] truncate">
<UiTableCell class="hidden xl:table-cell text-muted-foreground text-sm">
<div>{{ order.createTime }}</div>
<div v-if="order.confirmTime" class="text-xs">
完成: {{ order.confirmTime }}
</div>
</UiTableCell>
<UiTableCell class="hidden lg:table-cell text-sm text-muted-foreground max-w-[120px] truncate">
{{ order.rejectReason || order.adminRemark || '-' }}
</UiTableCell>
<UiTableCell class="text-right">
@@ -404,8 +521,8 @@ function getStatusText(status: number): string {
<UiBadge :variant="order.type === 1 ? 'default' : 'destructive'" class="mb-1">
{{ order.type === 1 ? '充值' : '提现' }}
</UiBadge>
<UiBadge :variant="getStatusVariant(order.status)" class="block">
{{ getStatusText(order.status) }}
<UiBadge :variant="getStatusVariant(order)" class="block">
{{ getStatusText(order) }}
</UiBadge>
</div>
</div>
@@ -413,9 +530,23 @@ function getStatusText(status: number): string {
<div class="text-xl font-mono font-bold">
¥{{ formatAmount(order.amount) }}
</div>
<!-- 显示地址信息 -->
<div v-if="order.walletAddress" class="mt-2 text-sm">
<span class="text-muted-foreground">{{ order.type === 1 ? '充值地址' : '提现地址' }}:</span>
<div class="font-mono text-xs break-all mt-1 flex items-center gap-1">
{{ order.walletAddress }}
<Icon icon="lucide:copy" class="size-3 cursor-pointer" @click="copyToClipboard(order.walletAddress!)" />
</div>
</div>
<div v-if="order.withdrawContact" class="text-sm text-muted-foreground mt-1">
联系方式: {{ order.withdrawContact }}
</div>
<div class="text-sm text-muted-foreground mt-1">
{{ order.createTime }}
</div>
<div v-if="order.confirmTime" class="text-xs text-muted-foreground">
完成: {{ order.confirmTime }}
</div>
<div v-if="order.rejectReason || order.adminRemark" class="text-sm text-muted-foreground mt-1">
备注: {{ order.rejectReason || order.adminRemark }}
</div>
@@ -489,7 +620,7 @@ function getStatusText(status: number): string {
<!-- 订单详情弹窗 -->
<UiDialog v-model:open="showDetailDialog">
<UiDialogContent class="max-w-md">
<UiDialogContent class="max-w-md max-h-[90vh] overflow-y-auto">
<UiDialogHeader>
<UiDialogTitle>订单详情</UiDialogTitle>
</UiDialogHeader>
@@ -529,11 +660,33 @@ function getStatusText(status: number): string {
状态
</div>
<div class="col-span-2">
<UiBadge :variant="getStatusVariant(currentOrder.status)">
{{ getStatusText(currentOrder.status) }}
<UiBadge :variant="getStatusVariant(currentOrder)">
{{ getStatusText(currentOrder) }}
</UiBadge>
</div>
<!-- 充值/提现地址 -->
<div class="text-muted-foreground">
{{ currentOrder.type === 1 ? '充值地址' : '提现地址' }}
</div>
<div class="col-span-2">
<div v-if="currentOrder.walletAddress" class="flex items-start gap-1">
<span class="font-mono text-xs break-all">{{ currentOrder.walletAddress }}</span>
<Icon icon="lucide:copy" class="size-4 cursor-pointer flex-shrink-0" @click="copyToClipboard(currentOrder.walletAddress!)" />
</div>
<span v-else class="text-muted-foreground">-</span>
</div>
<!-- 提现联系方式 -->
<template v-if="currentOrder.type === 2 && currentOrder.withdrawContact">
<div class="text-muted-foreground">
联系方式
</div>
<div class="col-span-2">
{{ currentOrder.withdrawContact }}
</div>
</template>
<div class="text-muted-foreground">
创建时间
</div>
@@ -541,6 +694,26 @@ function getStatusText(status: number): string {
{{ currentOrder.createTime }}
</div>
<!-- 用户确认打款时间(充值) -->
<template v-if="currentOrder.type === 1 && currentOrder.payTime">
<div class="text-muted-foreground">
确认打款
</div>
<div class="col-span-2">
{{ currentOrder.payTime }}
</div>
</template>
<!-- 管理员确认时间 -->
<template v-if="currentOrder.confirmTime">
<div class="text-muted-foreground">
完成时间
</div>
<div class="col-span-2">
{{ currentOrder.confirmTime }}
</div>
</template>
<template v-if="currentOrder.rejectReason">
<div class="text-muted-foreground text-red-500">
驳回原因
@@ -561,14 +734,14 @@ function getStatusText(status: number): string {
</div>
</div>
<UiDialogFooter>
<template v-if="currentOrder?.status === 1">
<template v-if="currentOrder && canApprove(currentOrder)">
<UiButton variant="outline" @click="showDetailDialog = false">
关闭
</UiButton>
<UiButton @click="openApproveDialog(currentOrder!, 2); showDetailDialog = false">
<UiButton @click="openApproveDialog(currentOrder, 2); showDetailDialog = false">
通过
</UiButton>
<UiButton variant="destructive" @click="openApproveDialog(currentOrder!, 3); showDetailDialog = false">
<UiButton variant="destructive" @click="openApproveDialog(currentOrder, 3); showDetailDialog = false">
驳回
</UiButton>
</template>
@@ -581,23 +754,69 @@ function getStatusText(status: number): string {
<!-- 审批弹窗 -->
<UiDialog v-model:open="showApproveDialog">
<UiDialogContent class="max-w-md">
<UiDialogContent class="max-w-md max-h-[90vh] overflow-y-auto">
<UiDialogHeader>
<UiDialogTitle>{{ approveStatus === 2 ? '通过订单' : '驳回订单' }}</UiDialogTitle>
</UiDialogHeader>
<div v-if="currentOrder" class="grid gap-4 py-4">
<div class="p-3 rounded-lg bg-muted/50 text-sm">
<div class="text-muted-foreground">
订单号
<div class="p-3 rounded-lg bg-muted/50 text-sm space-y-2">
<div>
<div class="text-muted-foreground">
订单号
</div>
<div class="font-mono">
{{ currentOrder.orderNo }}
</div>
</div>
<div class="font-mono">
{{ currentOrder.orderNo }}
<div>
<div class="text-muted-foreground">
用户
</div>
<div class="font-medium">
{{ currentOrder.username }}
</div>
</div>
<div class="text-muted-foreground mt-2">
金额
<div>
<div class="text-muted-foreground">
类型
</div>
<div>
<UiBadge :variant="currentOrder.type === 1 ? 'default' : 'destructive'">
{{ currentOrder.type === 1 ? '充值' : '提现' }}
</UiBadge>
</div>
</div>
<div class="font-mono font-bold text-lg">
¥{{ formatAmount(currentOrder.amount) }}
<div>
<div class="text-muted-foreground">
金额
</div>
<div class="font-mono font-bold text-lg">
¥{{ formatAmount(currentOrder.amount) }}
</div>
</div>
<!-- 显示地址信息 -->
<div v-if="currentOrder.walletAddress">
<div class="text-muted-foreground">
{{ currentOrder.type === 1 ? '充值地址' : '提现地址' }}
</div>
<div class="flex items-center gap-1">
<span class="font-mono text-xs break-all">{{ currentOrder.walletAddress }}</span>
<Icon icon="lucide:copy" class="size-4 cursor-pointer flex-shrink-0" @click="copyToClipboard(currentOrder.walletAddress!)" />
</div>
</div>
<!-- 提现联系方式 -->
<div v-if="currentOrder.type === 2 && currentOrder.withdrawContact">
<div class="text-muted-foreground">
联系方式
</div>
<div>{{ currentOrder.withdrawContact }}</div>
</div>
<!-- 充值确认打款时间 -->
<div v-if="currentOrder.type === 1 && currentOrder.payTime">
<div class="text-muted-foreground">
确认打款时间
</div>
<div>{{ currentOrder.payTime }}</div>
</div>
</div>
<div v-if="approveStatus === 3" class="grid gap-2">
@@ -615,7 +834,7 @@ function getStatusText(status: number): string {
</UiButton>
<UiButton
:variant="approveStatus === 3 ? 'destructive' : 'default'"
:disabled="approveMutation.isPending.value"
:disabled="approveMutation.isPending.value || (approveStatus === 3 && !rejectReason.trim())"
@click="handleApprove"
>
<UiSpinner v-if="approveMutation.isPending.value" class="mr-2" />

View File

@@ -0,0 +1,320 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { toast } from 'vue-sonner'
import type { ColdWallet } from '@/services/api/monisuo-admin.api'
import { BasicPage } from '@/components/global-layout'
import {
useGetWalletListQuery,
useCreateWalletMutation,
useUpdateWalletMutation,
useDeleteWalletMutation,
useSetDefaultWalletMutation,
useToggleWalletStatusMutation,
} from '@/services/api/monisuo-admin.api'
const { data, isLoading, refetch } = useGetWalletListQuery()
const createMutation = useCreateWalletMutation()
const updateMutation = useUpdateWalletMutation()
const deleteMutation = useDeleteWalletMutation()
const setDefaultMutation = useSetDefaultWalletMutation()
const toggleStatusMutation = useToggleWalletStatusMutation()
const wallets = computed(() => data.value?.data || [])
const editingWallet = ref<Partial<ColdWallet>>({})
const showEditDialog = ref(false)
const isEditing = ref(false)
// 表单验证
const formErrors = ref<{ name?: string, address?: string }>({})
function validateForm(): boolean {
formErrors.value = {}
if (!editingWallet.value.name?.trim()) {
formErrors.value.name = '请输入钱包名称'
return false
}
if (!editingWallet.value.address?.trim()) {
formErrors.value.address = '请输入钱包地址'
return false
}
return true
}
function openCreateDialog() {
editingWallet.value = { network: 'TRC20', status: 1, isDefault: false }
isEditing.value = false
formErrors.value = {}
showEditDialog.value = true
}
function openEditDialog(wallet: ColdWallet) {
editingWallet.value = { ...wallet }
isEditing.value = true
formErrors.value = {}
showEditDialog.value = true
}
async function saveWallet() {
if (!validateForm())
return
try {
if (isEditing.value) {
await updateMutation.mutateAsync(editingWallet.value)
toast.success('钱包已更新')
}
else {
await createMutation.mutateAsync(editingWallet.value)
toast.success('钱包已创建')
}
showEditDialog.value = false
refetch()
}
catch (e: any) {
toast.error(e.response?.data?.msg || '操作失败')
}
}
async function setDefault(wallet: ColdWallet) {
if (wallet.isDefault)
return
try {
await setDefaultMutation.mutateAsync({ id: wallet.id })
toast.success(`已将 ${wallet.name} 设为默认`)
}
catch (e: any) {
toast.error(e.response?.data?.msg || '设置失败')
}
}
async function toggleStatus(wallet: ColdWallet) {
try {
await toggleStatusMutation.mutateAsync({ id: wallet.id })
toast.success(wallet.status === 1 ? '已禁用' : '已启用')
}
catch (e: any) {
toast.error(e.response?.data?.msg || '操作失败')
}
}
async function deleteWallet(wallet: ColdWallet) {
if (!confirm(`确定删除钱包 ${wallet.name} 吗?`))
return
try {
await deleteMutation.mutateAsync({ id: wallet.id })
toast.success('钱包已删除')
}
catch (e: any) {
toast.error(e.response?.data?.msg || '删除失败')
}
}
</script>
<template>
<BasicPage title="冷钱包管理" description="配置充值收款地址">
<template #actions>
<UiButton @click="openCreateDialog">
<Icon icon="lucide:plus" class="mr-2 h-4 w-4" />
新增钱包
</UiButton>
</template>
<!-- PC端表格 -->
<UiCard class="hidden md:block overflow-x-auto p-4">
<UiTable v-if="!isLoading">
<UiTableHeader>
<UiTableRow>
<UiTableHead>名称</UiTableHead>
<UiTableHead>地址</UiTableHead>
<UiTableHead>网络</UiTableHead>
<UiTableHead>默认</UiTableHead>
<UiTableHead>状态</UiTableHead>
<UiTableHead>操作</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="wallet in wallets" :key="wallet.id">
<UiTableCell class="font-medium">{{ wallet.name }}</UiTableCell>
<UiTableCell class="font-mono text-xs max-w-[200px] truncate">
{{ wallet.address }}
</UiTableCell>
<UiTableCell>{{ wallet.network }}</UiTableCell>
<UiTableCell>
<UiBadge v-if="wallet.isDefault" variant="default">
默认
</UiBadge>
<span v-else class="text-muted-foreground">-</span>
</UiTableCell>
<UiTableCell>
<UiBadge :variant="wallet.status === 1 ? 'default' : 'destructive'">
{{ wallet.status === 1 ? '启用' : '禁用' }}
</UiBadge>
</UiTableCell>
<UiTableCell>
<div class="flex gap-2">
<UiButton size="sm" variant="ghost" @click="openEditDialog(wallet)">
编辑
</UiButton>
<UiButton
size="sm"
variant="ghost"
:disabled="wallet.isDefault"
@click="setDefault(wallet)"
>
设为默认
</UiButton>
<UiButton
size="sm"
variant="ghost"
@click="toggleStatus(wallet)"
>
{{ wallet.status === 1 ? '禁用' : '启用' }}
</UiButton>
<UiButton
size="sm"
variant="destructive"
@click="deleteWallet(wallet)"
>
删除
</UiButton>
</div>
</UiTableCell>
</UiTableRow>
<UiTableRow v-if="wallets.length === 0">
<UiTableCell colspan="6" class="text-center text-muted-foreground py-8">
暂无钱包数据
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
<div v-else class="flex justify-center py-8">
<Icon icon="lucide:loader-2" class="h-6 w-6 animate-spin" />
</div>
</UiCard>
<!-- 移动端卡片 -->
<div class="md:hidden space-y-4">
<div v-if="isLoading" class="flex justify-center py-8">
<Icon icon="lucide:loader-2" class="h-6 w-6 animate-spin" />
</div>
<template v-else-if="wallets.length > 0">
<UiCard v-for="wallet in wallets" :key="wallet.id" class="p-4">
<div class="flex justify-between items-start mb-2">
<div>
<div class="font-medium">{{ wallet.name }}</div>
<div class="text-xs text-muted-foreground">{{ wallet.network }}</div>
</div>
<div class="flex gap-1">
<UiBadge v-if="wallet.isDefault" variant="default" class="text-xs">
默认
</UiBadge>
<UiBadge :variant="wallet.status === 1 ? 'default' : 'destructive'" class="text-xs">
{{ wallet.status === 1 ? '启用' : '禁用' }}
</UiBadge>
</div>
</div>
<div class="font-mono text-xs break-all mb-3 text-muted-foreground">
{{ wallet.address }}
</div>
<div class="flex flex-wrap gap-2">
<UiButton size="sm" variant="outline" @click="openEditDialog(wallet)">
编辑
</UiButton>
<UiButton
size="sm"
variant="outline"
:disabled="wallet.isDefault"
@click="setDefault(wallet)"
>
设为默认
</UiButton>
<UiButton
size="sm"
variant="outline"
@click="toggleStatus(wallet)"
>
{{ wallet.status === 1 ? '禁用' : '启用' }}
</UiButton>
<UiButton
size="sm"
variant="destructive"
@click="deleteWallet(wallet)"
>
删除
</UiButton>
</div>
</UiCard>
</template>
<div v-else class="text-center text-muted-foreground py-8">
暂无钱包数据
</div>
</div>
<!-- 新增/编辑弹窗 -->
<UiDialog v-model:open="showEditDialog">
<UiDialogContent>
<UiDialogHeader>
<UiDialogTitle>{{ isEditing ? '编辑钱包' : '新增钱包' }}</UiDialogTitle>
</UiDialogHeader>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<UiLabel>钱包名称 <span class="text-red-500">*</span></UiLabel>
<UiInput v-model="editingWallet.name" placeholder="如:主钱包" />
<span v-if="formErrors.name" class="text-xs text-red-500">{{ formErrors.name }}</span>
</div>
<div class="grid gap-2">
<UiLabel>钱包地址 <span class="text-red-500">*</span></UiLabel>
<UiInput v-model="editingWallet.address" placeholder="TRC20/ERC20 地址" />
<span v-if="formErrors.address" class="text-xs text-red-500">{{ formErrors.address }}</span>
</div>
<div class="grid gap-2">
<UiLabel>网络类型</UiLabel>
<UiSelect v-model="editingWallet.network">
<UiSelectTrigger>
<UiSelectValue placeholder="选择网络" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem value="TRC20">
TRC20 (波场)
</UiSelectItem>
<UiSelectItem value="ERC20">
ERC20 (以太坊)
</UiSelectItem>
<UiSelectItem value="BEP20">
BEP20 (币安智能链)
</UiSelectItem>
</UiSelectContent>
</UiSelect>
</div>
<div class="flex items-center gap-2">
<UiCheckbox
:checked="editingWallet.isDefault"
@update:checked="editingWallet.isDefault = $event"
/>
<UiLabel class="cursor-pointer" @click="editingWallet.isDefault = !editingWallet.isDefault">
设为默认钱包
</UiLabel>
</div>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="showEditDialog = false">
取消
</UiButton>
<UiButton
:disabled="createMutation.isPending.value || updateMutation.isPending.value"
@click="saveWallet"
>
{{ isEditing ? '保存' : '创建' }}
</UiButton>
</UiDialogFooter>
</UiDialogContent>
</UiDialog>
</BasicPage>
</template>