This commit is contained in:
sion
2026-04-08 01:09:57 +08:00
parent 007915b6f2
commit 26169accff
11 changed files with 776 additions and 81 deletions

View File

@@ -2,10 +2,10 @@
import { Icon } from '@iconify/vue'
import { toast } from 'vue-sonner'
import type { User } from '@/services/api/monisuo-admin.api'
import type { User, UserStats } from '@/services/api/monisuo-admin.api'
import { BasicPage } from '@/components/global-layout'
import { useGetUserListQuery, useUpdateUserStatusMutation } from '@/services/api/monisuo-admin.api'
import { useGetUserListQuery, useGetUserStatsQuery, useUpdateUserStatusMutation } from '@/services/api/monisuo-admin.api'
const pageNum = ref(1)
const pageSize = ref(10)
@@ -29,12 +29,47 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
// 用户详情弹窗
const showDetailDialog = ref(false)
const selectedUser = ref<User | null>(null)
const activeTab = ref('overview')
const { data: statsData, isLoading: statsLoading } = useGetUserStatsQuery(
computed(() => selectedUser.value?.id ?? 0),
)
const stats = computed(() => statsData.value?.data as UserStats | undefined)
function viewUserDetail(user: User) {
selectedUser.value = user
activeTab.value = 'overview'
showDetailDialog.value = true
}
function formatAmount(val: number | string | undefined | null): string {
if (val == null)
return '0.00'
const n = typeof val === 'string' ? Number.parseFloat(val) : val
if (Number.isNaN(n))
return '0.00'
return n.toFixed(2)
}
function formatTime(val: string | undefined | null): string {
if (!val)
return '-'
return val.replace('T', ' ').substring(0, 19)
}
function getFundOrderStatusText(type: number, status: number): string {
if (type === 1) {
const map: Record<number, string> = { 1: '待付款', 2: '待确认', 3: '已完成', 4: '已驳回', 5: '已取消' }
return map[status] || '未知'
}
const map: Record<number, string> = { 1: '待审批', 2: '已出款', 3: '已驳回', 4: '已取消', 5: '待财务审核' }
return map[status] || '未知'
}
function getTradeDirectionText(dir: number): string {
return dir === 1 ? '买入' : '卖出'
}
async function toggleStatus(user: User) {
const newStatus = user.status === 1 ? 0 : 1
const action = newStatus === 0 ? '禁用' : '启用'
@@ -292,73 +327,469 @@ function handlePageSizeChange(size: unknown) {
</div>
</div>
<!-- 用户详情弹窗 -->
<!-- 用户详情弹窗 - 多标签页数据面板 -->
<UiDialog v-model:open="showDetailDialog">
<UiDialogContent class="max-w-md">
<UiDialogContent class="max-w-3xl max-h-[90vh] overflow-y-auto">
<UiDialogHeader>
<UiDialogTitle>用户详情</UiDialogTitle>
</UiDialogHeader>
<div v-if="selectedUser" class="space-y-4">
<div class="grid grid-cols-3 gap-2 text-sm">
<div class="text-muted-foreground">
用户ID
</div>
<div class="col-span-2 font-medium">
{{ selectedUser.id }}
</div>
<div class="text-muted-foreground">
用户名
</div>
<div class="col-span-2 font-medium">
<UiDialogTitle>
用户详情
<span v-if="selectedUser" class="text-muted-foreground font-normal ml-2">
{{ selectedUser.username }}
</div>
</span>
</UiDialogTitle>
</UiDialogHeader>
<div class="text-muted-foreground">
昵称
</div>
<div class="col-span-2">
{{ selectedUser.nickname || '-' }}
</div>
<div class="text-muted-foreground">
手机
</div>
<div class="col-span-2">
{{ selectedUser.phone || '-' }}
</div>
<div class="text-muted-foreground">
邮箱
</div>
<div class="col-span-2">
{{ selectedUser.email || '-' }}
</div>
<div class="text-muted-foreground">
状态
</div>
<div class="col-span-2">
<UiBadge :variant="selectedUser.status === 1 ? 'default' : 'destructive'">
{{ selectedUser.status === 1 ? '正常' : '禁用' }}
</UiBadge>
</div>
<div class="text-muted-foreground">
注册时间
</div>
<div class="col-span-2">
{{ selectedUser.createTime }}
</div>
<div class="text-muted-foreground">
更新时间
</div>
<div class="col-span-2">
{{ selectedUser.updateTime }}
</div>
</div>
<!-- Loading -->
<div v-if="statsLoading" class="py-12 text-center">
<UiSpinner class="mx-auto" />
</div>
<div v-else-if="stats" class="space-y-4">
<!-- 标签页 -->
<UiTabs v-model="activeTab">
<UiTabsList class="w-full">
<UiTabsTrigger value="overview" class="flex-1">
概览
</UiTabsTrigger>
<UiTabsTrigger value="fund" class="flex-1">
充提记录
</UiTabsTrigger>
<UiTabsTrigger value="referral" class="flex-1">
推广信息
</UiTabsTrigger>
<UiTabsTrigger value="bonus" class="flex-1">
福利记录
</UiTabsTrigger>
<UiTabsTrigger value="trade" class="flex-1">
交易记录
</UiTabsTrigger>
</UiTabsList>
<!-- ====== 概览 ====== -->
<UiTabsContent value="overview" class="space-y-4 mt-4">
<!-- 基本信息 -->
<div class="grid grid-cols-3 gap-x-6 gap-y-2 text-sm">
<div class="text-muted-foreground">
用户ID
</div>
<div class="col-span-2 font-medium">
{{ stats.user.id }}
</div>
<div class="text-muted-foreground">
用户名
</div>
<div class="col-span-2 font-medium">
{{ stats.user.username }}
</div>
<div class="text-muted-foreground">
昵称
</div>
<div class="col-span-2">
{{ stats.user.nickname || '-' }}
</div>
<div class="text-muted-foreground">
手机
</div>
<div class="col-span-2">
{{ stats.user.phone || '-' }}
</div>
<div class="text-muted-foreground">
推广码
</div>
<div class="col-span-2 font-mono font-medium">
{{ stats.user.referralCode || '-' }}
</div>
<div class="text-muted-foreground">
状态
</div>
<div class="col-span-2">
<UiBadge :variant="stats.user.status === 1 ? 'default' : 'destructive'">
{{ stats.user.status === 1 ? '正常' : '禁用' }}
</UiBadge>
</div>
<div class="text-muted-foreground">
注册时间
</div>
<div class="col-span-2">
{{ formatTime(stats.user.createTime) }}
</div>
</div>
<!-- KPI 卡片 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
资金账户余额
</div>
<div class="text-lg font-bold font-mono">
{{ formatAmount(stats.fundAccount?.balance) }}
</div>
<div class="text-xs text-muted-foreground mt-1">
冻结: {{ formatAmount(stats.fundAccount?.frozen) }}
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
累计充值
</div>
<div class="text-lg font-bold font-mono text-green-600 dark:text-green-400">
{{ formatAmount(stats.fundAccount?.totalDeposit) }}
</div>
<div class="text-xs text-muted-foreground mt-1">
{{ stats.depositStats?.successCount || 0 }} 笔成功
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
累计提现
</div>
<div class="text-lg font-bold font-mono text-red-600 dark:text-red-400">
{{ formatAmount(stats.fundAccount?.totalWithdraw) }}
</div>
<div class="text-xs text-muted-foreground mt-1">
手续费: {{ formatAmount(stats.withdrawStats?.totalFee) }}
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
累计领取福利
</div>
<div class="text-lg font-bold font-mono text-amber-600 dark:text-amber-400">
{{ formatAmount(stats.bonusStats?.totalBonusClaimed) }}
</div>
<div class="text-xs text-muted-foreground mt-1">
{{ stats.bonusStats?.totalBonusCount || 0 }} 次领取
</div>
</UiCard>
</div>
<!-- 持仓列表 -->
<div v-if="stats.tradeAccounts?.length">
<div class="text-sm font-medium mb-2">
交易持仓
</div>
<div class="rounded-md border">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>币种</UiTableHead>
<UiTableHead class="text-right">
数量
</UiTableHead>
<UiTableHead class="text-right">
现价
</UiTableHead>
<UiTableHead class="text-right">
市值
</UiTableHead>
<UiTableHead class="text-right">
成本价
</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="t in stats.tradeAccounts" :key="t.coinCode">
<UiTableCell class="font-medium">
{{ t.coinCode }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ Number(t.quantity).toFixed(4) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(t.price) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(t.value) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(t.avgPrice) }}
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</div>
</div>
<div v-else class="text-sm text-muted-foreground text-center py-4">
暂无持仓
</div>
</UiTabsContent>
<!-- ====== 充提记录 ====== -->
<UiTabsContent value="fund" class="space-y-4 mt-4">
<!-- 充提统计 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
充值笔数
</div>
<div class="text-lg font-bold font-mono">
{{ stats.depositStats?.totalCount || 0 }}
<span class="text-xs text-muted-foreground font-normal">({{ stats.depositStats?.successCount || 0 }} 笔成功)</span>
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
充值总额
</div>
<div class="text-lg font-bold font-mono text-green-600 dark:text-green-400">
{{ formatAmount(stats.depositStats?.totalAmount) }}
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
提现笔数
</div>
<div class="text-lg font-bold font-mono">
{{ stats.withdrawStats?.totalCount || 0 }}
<span class="text-xs text-muted-foreground font-normal">({{ stats.withdrawStats?.successCount || 0 }} 笔成功)</span>
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
提现总额
</div>
<div class="text-lg font-bold font-mono text-red-600 dark:text-red-400">
{{ formatAmount(stats.withdrawStats?.totalAmount) }}
</div>
</UiCard>
</div>
<!-- 订单列表 -->
<div v-if="stats.recentFundOrders?.length">
<div class="rounded-md border">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>时间</UiTableHead>
<UiTableHead>类型</UiTableHead>
<UiTableHead class="text-right">
金额
</UiTableHead>
<UiTableHead class="text-right">
手续费
</UiTableHead>
<UiTableHead class="text-right">
到账
</UiTableHead>
<UiTableHead>状态</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="o in stats.recentFundOrders" :key="o.orderNo">
<UiTableCell class="text-xs">
{{ formatTime(o.createTime) }}
</UiTableCell>
<UiTableCell>
<UiBadge :variant="o.type === 1 ? 'default' : 'secondary'">
{{ o.type === 1 ? '充值' : '提现' }}
</UiBadge>
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(o.amount) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ o.fee ? formatAmount(o.fee) : '-' }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ o.receivableAmount ? formatAmount(o.receivableAmount) : '-' }}
</UiTableCell>
<UiTableCell>
<UiBadge variant="outline">
{{ getFundOrderStatusText(o.type, o.status) }}
</UiBadge>
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</div>
</div>
<div v-else class="text-sm text-muted-foreground text-center py-4">
暂无充提记录
</div>
</UiTabsContent>
<!-- ====== 推广信息 ====== -->
<UiTabsContent value="referral" class="space-y-4 mt-4">
<!-- 推广概览 -->
<div class="grid grid-cols-3 gap-3">
<UiCard class="p-4 text-center">
<div class="text-xs text-muted-foreground mb-1">
推广码
</div>
<div class="text-lg font-bold font-mono">
{{ stats.user.referralCode || '-' }}
</div>
</UiCard>
<UiCard class="p-4 text-center">
<div class="text-xs text-muted-foreground mb-1">
直接推广
</div>
<div class="text-lg font-bold font-mono">
{{ stats.referralStats?.directCount || 0 }}
</div>
</UiCard>
<UiCard class="p-4 text-center">
<div class="text-xs text-muted-foreground mb-1">
间接推广
</div>
<div class="text-lg font-bold font-mono">
{{ stats.referralStats?.indirectCount || 0 }}
</div>
</UiCard>
</div>
<!-- 推广人列表 -->
<div v-if="stats.referralStats?.referrals?.length">
<div class="text-sm font-medium mb-2">
直接推广人列表
</div>
<div class="rounded-md border">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>用户名</UiTableHead>
<UiTableHead>昵称</UiTableHead>
<UiTableHead>注册时间</UiTableHead>
<UiTableHead>已充值</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="r in stats.referralStats.referrals" :key="r.userId">
<UiTableCell class="font-medium">
{{ r.username }}
</UiTableCell>
<UiTableCell>{{ r.nickname || '-' }}</UiTableCell>
<UiTableCell class="text-xs">
{{ formatTime(r.createTime) }}
</UiTableCell>
<UiTableCell>
<UiBadge :variant="r.deposited ? 'default' : 'secondary'">
{{ r.deposited ? '是' : '否' }}
</UiBadge>
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</div>
</div>
<div v-else class="text-sm text-muted-foreground text-center py-4">
暂无推广记录
</div>
</UiTabsContent>
<!-- ====== 福利记录 ====== -->
<UiTabsContent value="bonus" class="space-y-4 mt-4">
<!-- 福利统计 -->
<div class="grid grid-cols-2 gap-3">
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
累计领取金额
</div>
<div class="text-xl font-bold font-mono text-amber-600 dark:text-amber-400">
{{ formatAmount(stats.bonusStats?.totalBonusClaimed) }} USDT
</div>
</UiCard>
<UiCard class="p-4">
<div class="text-xs text-muted-foreground mb-1">
累计领取次数
</div>
<div class="text-xl font-bold font-mono">
{{ stats.bonusStats?.totalBonusCount || 0 }}
</div>
</UiCard>
</div>
<!-- 福利记录列表 -->
<div v-if="stats.bonusStats?.records?.length">
<div class="rounded-md border">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>类型</UiTableHead>
<UiTableHead class="text-right">
金额
</UiTableHead>
<UiTableHead>时间</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="(b, i) in stats.bonusStats.records" :key="i">
<UiTableCell>
<UiBadge variant="outline">
{{ b.type }}
</UiBadge>
</UiTableCell>
<UiTableCell class="text-right font-mono text-amber-600 dark:text-amber-400">
+{{ formatAmount(b.amount) }}
</UiTableCell>
<UiTableCell class="text-xs">
{{ formatTime(b.time) }}
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</div>
</div>
<div v-else class="text-sm text-muted-foreground text-center py-4">
暂无福利领取记录
</div>
</UiTabsContent>
<!-- ====== 交易记录 ====== -->
<UiTabsContent value="trade" class="space-y-4 mt-4">
<div v-if="stats.recentTradeOrders?.length">
<div class="rounded-md border">
<UiTable>
<UiTableHeader>
<UiTableRow>
<UiTableHead>时间</UiTableHead>
<UiTableHead>币种</UiTableHead>
<UiTableHead>方向</UiTableHead>
<UiTableHead class="text-right">
价格
</UiTableHead>
<UiTableHead class="text-right">
数量
</UiTableHead>
<UiTableHead class="text-right">
金额
</UiTableHead>
</UiTableRow>
</UiTableHeader>
<UiTableBody>
<UiTableRow v-for="t in stats.recentTradeOrders" :key="t.orderNo">
<UiTableCell class="text-xs">
{{ formatTime(t.createTime) }}
</UiTableCell>
<UiTableCell class="font-medium">
{{ t.coinCode }}
</UiTableCell>
<UiTableCell>
<UiBadge :variant="t.direction === 1 ? 'default' : 'destructive'">
{{ getTradeDirectionText(t.direction) }}
</UiBadge>
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(t.price) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ Number(t.quantity).toFixed(4) }}
</UiTableCell>
<UiTableCell class="text-right font-mono">
{{ formatAmount(t.amount) }}
</UiTableCell>
</UiTableRow>
</UiTableBody>
</UiTable>
</div>
</div>
<div v-else class="text-sm text-muted-foreground text-center py-4">
暂无交易记录
</div>
</UiTabsContent>
</UiTabs>
</div>
<UiDialogFooter>
<UiButton variant="outline" @click="showDetailDialog = false">
关闭

View File

@@ -148,6 +148,83 @@ export function useGetUserDetailQuery(userId: number) {
})
}
export interface UserStats {
user: User
fundAccount: {
balance: number
frozen: number
totalDeposit: number
totalWithdraw: number
}
tradeAccounts: Array<{
coinCode: string
coinName: string
quantity: number
price: number
value: number
avgPrice: number
change24h: number
}>
depositStats: {
totalCount: number
totalAmount: number
successCount: number
successAmount: number
}
withdrawStats: {
totalCount: number
totalAmount: number
successCount: number
successAmount: number
totalFee: number
}
referralStats: {
directCount: number
indirectCount: number
referrals: Array<{
userId: number
username: string
nickname: string
createTime: string
deposited: boolean
}>
}
bonusStats: {
totalBonusClaimed: number
totalBonusCount: number
records: Array<{
type: string
amount: number
time: string
}>
}
recentFundOrders: OrderFund[]
recentTradeOrders: Array<{
id: number
orderNo: string
coinCode: string
direction: number
price: number
quantity: number
amount: number
createTime: string
}>
}
export function useGetUserStatsQuery(userId: Ref<number> | ComputedRef<number> | number) {
const { axiosInstance } = useAxios()
return useQuery<ApiResult<UserStats>, AxiosError>({
queryKey: computed(() => ['useGetUserStatsQuery', unref(userId)]),
queryFn: async () => {
const id = unref(userId)
const response = await axiosInstance.get('/admin/user/stats', { params: { userId: id } })
return response.data
},
enabled: computed(() => !!unref(userId)),
})
}
export function useUpdateUserStatusMutation() {
const { axiosInstance } = useAxios()
const queryClient = useQueryClient()