This commit is contained in:
sion
2026-04-05 23:28:38 +08:00
parent 0c66b3725f
commit da0f9d6f5e
21 changed files with 640 additions and 394 deletions

View File

@@ -16,151 +16,191 @@ const isLoading = computed(() => overviewLoading.value || cashFlowLoading.value)
const overview = computed(() => overviewData.value?.data)
const cashFlow = computed(() => cashFlowData.value?.data || [])
// ========== 模块1: 资金概览 ==========
const fundMetrics = computed(() => [
{
label: '在管资金',
value: overview.value?.fundBalance || 0,
icon: 'lucide:wallet',
color: 'text-blue-600',
bgColor: 'bg-blue-50 dark:bg-blue-950',
},
{
label: '交易账户',
value: overview.value?.tradeValue || 0,
icon: 'lucide:bar-chart-3',
color: 'text-purple-600',
bgColor: 'bg-purple-50 dark:bg-purple-950',
},
{
label: '总资产',
value: (overview.value?.fundBalance || 0) + (overview.value?.tradeValue || 0),
icon: 'lucide:landmark',
color: 'text-orange-600',
bgColor: 'bg-orange-50 dark:bg-orange-950',
},
])
// ========== 工具函数 ==========
// ========== 模块2: 资金流动 ==========
function calcGrowthRate(current: number, previous: number): string {
if (previous === 0)
return current > 0 ? '+100%' : '0%'
const rate = new Decimal(current).minus(previous).div(previous).mul(100).toDecimalPlaces(1)
const sign = rate.gte(0) ? '+' : ''
return `{sign}{rate}%`
function formatCurrency(value: number | undefined): string {
const v = value || 0
if (v >= 100_000_000)
return `${(v / 100_000_000).toFixed(2)}亿`
if (v >= 10_000)
return `${(v / 10_000).toFixed(1)}`
return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const flowMetrics = computed(() => {
const flow = cashFlow.value
const len = flow.length
const thisMonth = len >= 1 ? flow[len - 1] : null
const lastMonth = len >= 2 ? flow[len - 2] : null
function calcGrowthRate(current: number, previous: number): { text: string, up: boolean } {
if (previous === 0) {
return current > 0 ? { text: '+100%', up: true } : { text: '0%', up: true }
}
const rate = new Decimal(current).minus(previous).div(previous).mul(100).toDecimalPlaces(1)
const sign = rate.gte(0) ? '+' : ''
return { text: `${sign}${rate}%`, up: rate.gte(0) }
}
const depositTrend = thisMonth && lastMonth
? calcGrowthRate(thisMonth.deposit as number, lastMonth.deposit as number)
: '+0%'
const withdrawTrend = thisMonth && lastMonth
? calcGrowthRate(thisMonth.withdraw as number, lastMonth.withdraw as number)
: '+0%'
const netInflowTrend = thisMonth && lastMonth
? calcGrowthRate(thisMonth.netInflow as number, lastMonth.netInflow as number)
: '+0%'
// ========== 第一层:核心 KPI ==========
const kpiItems = computed(() => {
const o = overview.value
if (!o) return []
const netInflow = (o.totalDeposit || 0) - (o.totalWithdraw || 0)
const depositGrowth = calcGrowthRate(o.monthlyDeposit, o.lastMonthDeposit)
const withdrawGrowth = calcGrowthRate(o.monthlyWithdraw, o.lastMonthWithdraw)
// 净流入环比用充值环比近似(实际出款与提现趋势一致)
const netMonthGrowth = calcGrowthRate(
o.monthlyDeposit - o.monthlyWithdraw,
o.lastMonthDeposit - o.lastMonthWithdraw,
)
return [
{
label: '累计充值',
value: overview.value?.totalDeposit || 0,
icon: 'lucide:arrow-down-circle',
color: 'text-green-600',
bgColor: 'bg-green-50 dark:bg-green-950',
trend: depositTrend,
value: o.totalDeposit,
icon: 'lucide:arrow-down-to-line',
color: 'text-emerald-600',
bgColor: 'bg-emerald-50 dark:bg-emerald-950/40',
growth: depositGrowth,
},
{
label: '累计提现',
value: overview.value?.totalWithdraw || 0,
icon: 'lucide:arrow-up-circle',
color: 'text-red-600',
bgColor: 'bg-red-50 dark:bg-red-950',
trend: withdrawTrend,
value: o.totalWithdraw,
icon: 'lucide:arrow-up-from-line',
color: 'text-red-500',
bgColor: 'bg-red-50 dark:bg-red-950/40',
growth: withdrawGrowth,
},
{
label: '实际出款',
value: o.totalActualPayout,
icon: 'lucide:banknote',
color: 'text-amber-600',
bgColor: 'bg-amber-50 dark:bg-amber-950/40',
growth: withdrawGrowth,
},
{
label: '净流入',
value: (overview.value?.totalDeposit || 0) - (overview.value?.totalWithdraw || 0),
value: netInflow,
icon: 'lucide:trending-up',
color: 'text-emerald-600',
bgColor: 'bg-emerald-50 dark:bg-emerald-950',
trend: netInflowTrend,
color: 'text-sky-600',
bgColor: 'bg-sky-50 dark:bg-sky-950/40',
growth: netMonthGrowth,
},
]
})
// ========== 模块3: 资金趋势图 ==========
// ========== 第二层:资金趋势图 ==========
const trendChartOption = computed(() => ({
tooltip: { trigger: 'axis' },
legend: { data: ['充值', '提现'], bottom: 0, top: 'auto' },
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
tooltip: {
trigger: 'axis',
formatter(params: any) {
const items = Array.isArray(params) ? params : [params]
const lines = items.map((p: any) =>
`${p.marker} ${p.seriesName}: USDT ${Number(p.value).toLocaleString()}`,
)
return `<div style="font-size:12px"><b>${items[0].axisValue}</b><br/>${lines.join('<br/>')}</div>`
},
},
legend: { data: ['充值', '提现'], bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '14%', top: '8%', containLabel: true },
xAxis: {
type: 'category',
data: cashFlow.value.map((t: any) => t.month),
axisTick: { show: false },
axisLine: { lineStyle: { color: '#e5e7eb' } },
},
yAxis: {
type: 'value',
axisLabel: { formatter: 'USDT{value}K' },
axisLabel: {
formatter(value: number) {
if (value >= 10_000) return `${(value / 10_000).toFixed(0)}`
return `${value}`
},
},
splitLine: { lineStyle: { color: '#f3f4f6', type: 'dashed' } },
},
series: [
{
name: '充值',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: cashFlow.value.map((t: any) => t.deposit),
itemStyle: { color: '#10b981' },
areaStyle: { color: 'rgba(16, 185, 129, 0.1)' },
areaStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(16, 185, 129, 0.25)' },
{ offset: 1, color: 'rgba(16, 185, 129, 0.02)' },
],
},
},
},
{
name: '提现',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
data: cashFlow.value.map((t: any) => t.withdraw),
itemStyle: { color: '#ef4444' },
areaStyle: { color: 'rgba(239, 68, 68, 0.1)' },
areaStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(239, 68, 68, 0.25)' },
{ offset: 1, color: 'rgba(239, 68, 68, 0.02)' },
],
},
},
},
],
}))
// ========== 模块4: 资金分布 ==========
const distributionOption = computed(() => {
const fundBalance = overview.value?.fundBalance || 0
const tradeValue = overview.value?.tradeValue || 0
// ========== 第二层:资产状态 ==========
return {
tooltip: { trigger: 'item', formatter: '{b}: {d}%' },
legend: { orient: 'vertical', right: '5%', top: 'center' },
series: [{
type: 'pie',
radius: ['50%', '75%'],
center: ['35%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
label: { show: true, position: 'inside', formatter: '{d}%', fontSize: 12 },
data: [
{ value: fundBalance, name: '在管资金', itemStyle: { color: '#3b82f6' } },
{ value: tradeValue, name: '交易账户', itemStyle: { color: '#8b5cf6' } },
],
}],
}
const assetMetrics = computed(() => {
const o = overview.value
if (!o) return []
const totalAssets = (o.fundBalance || 0) + (o.tradeValue || 0)
return [
{
label: '平台总资产',
value: totalAssets,
icon: 'lucide:landmark',
color: 'text-violet-600',
},
{
label: '在管资金',
value: o.fundBalance,
icon: 'lucide:wallet',
color: 'text-blue-600',
},
{
label: '冻结中',
value: o.totalFrozen,
icon: 'lucide:lock',
color: 'text-slate-500',
},
]
})
// ========== 模块5: 运营指标 ==========
const operationMetrics = computed(() => [
{ label: '用户总数', value: overview.value?.userCount || 0, icon: 'lucide:users' },
{ label: '待审批', value: overview.value?.pendingCount || 0, icon: 'lucide:clock' },
])
// ========== 第三层:运营快报 ==========
function formatCurrency(value: number): string {
if (value >= 10000)
return `USDT{(value / 10000).toFixed(1)}万`
return `USDT{value.toLocaleString()}`
}
const operationMetrics = computed(() => {
const o = overview.value
if (!o) return []
return [
{ label: '用户总数', value: o.userCount, icon: 'lucide:users', color: 'text-blue-600', bgColor: 'bg-blue-50 dark:bg-blue-950/40' },
{ label: '今日活跃', value: o.todayActiveUsers, icon: 'lucide:activity', color: 'text-green-600', bgColor: 'bg-green-50 dark:bg-green-950/40' },
{ label: '本月新增', value: o.monthNewUsers, icon: 'lucide:user-plus', color: 'text-purple-600', bgColor: 'bg-purple-50 dark:bg-purple-950/40' },
{ label: '待审批', value: o.pendingCount, icon: 'lucide:clock', color: o.pendingCount > 0 ? 'text-amber-600' : 'text-slate-500', bgColor: o.pendingCount > 0 ? 'bg-amber-50 dark:bg-amber-950/40' : 'bg-slate-50 dark:bg-slate-950/40' },
]
})
function navigateTo(path: string) {
router.push(path)
@@ -168,126 +208,114 @@ function navigateTo(path: string) {
</script>
<template>
<BasicPage title="数据看板" description="核心业务数据一目了然">
<BasicPage title="数据看板" description="核心业务数据一">
<div v-if="isLoading" class="flex items-center justify-center py-20">
<UiSpinner class="w-8 h-8" />
</div>
<div v-else class="grid gap-6">
<!-- 模块1: 资金概览 -->
<section class="space-y-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Icon icon="lucide:wallet" class="size-4" />
资金概览
</h2>
<div class="grid gap-3 grid-cols-1 sm:grid-cols-3">
<UiCard v-for="item in fundMetrics" :key="item.label" class="hover:shadow-sm transition-shadow">
<UiCardContent class="p-4">
<div class="flex items-center justify-between">
<!-- 第一层核心 KPI 横幅 -->
<section>
<div class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<UiCard v-for="item in kpiItems" :key="item.label" class="hover:shadow-sm transition-shadow">
<UiCardContent class="p-5">
<div class="flex items-center justify-between mb-3">
<div class="p-2 rounded-lg" :class="[item.bgColor]">
<Icon :icon="item.icon" class="size-4" :class="item.color" />
</div>
<UiBadge
:variant="item.growth.up ? 'default' : 'destructive'"
class="text-xs font-mono"
>
{{ item.growth.text }}
</UiBadge>
</div>
<div class="mt-3 space-y-1">
<p class="text-xs text-muted-foreground">
{{ item.label }}
</p>
<p class="text-lg sm:text-xl font-bold font-mono truncate" :class="item.color">
{{ formatCurrency(item.value) }}
</p>
</div>
<p class="text-xs text-muted-foreground mb-1">
{{ item.label }}
</p>
<p class="text-xl font-bold font-mono truncate" :class="item.color">
{{ formatCurrency(item.value) }}
</p>
</UiCardContent>
</UiCard>
</div>
</section>
<!-- 模块2: 金流动 -->
<section class="space-y-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Icon icon="lucide:git-compare" class="size-4" />
资金流动
</h2>
<div class="grid gap-3 grid-cols-1 sm:grid-cols-3">
<UiCard v-for="item in flowMetrics" :key="item.label" class="hover:shadow-sm transition-shadow">
<UiCardContent class="p-4">
<div class="flex items-center justify-between">
<div class="p-2 rounded-lg" :class="[item.bgColor]">
<Icon :icon="item.icon" class="size-4" :class="item.color" />
</div>
<span class="text-xs font-medium text-green-600">{{ item.trend }}</span>
</div>
<div class="mt-3 space-y-1">
<p class="text-xs text-muted-foreground">
{{ item.label }}
</p>
<p class="text-lg sm:text-xl font-bold font-mono truncate" :class="item.color">
{{ formatCurrency(item.value) }}
</p>
</div>
</UiCardContent>
</UiCard>
</div>
</section>
<!-- 模块3+4: 图表区域 -->
<div class="grid gap-6 lg:grid-cols-2">
<!-- 资金趋势 -->
<section class="space-y-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<!-- 第二层资金趋势 + 产状态 -->
<div class="grid gap-6 lg:grid-cols-5">
<!-- 资金趋势图 -->
<section class="lg:col-span-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-3">
<Icon icon="lucide:trending-up" class="size-4" />
资金趋势
资金流动趋势近6月
</h2>
<UiCard>
<UiCardContent class="p-4">
<VChart :option="trendChartOption" autoresize style="height: 240px" />
<VChart :option="trendChartOption" autoresize style="height: 280px" />
</UiCardContent>
</UiCard>
</section>
<!-- 金分布 -->
<section class="space-y-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Icon icon="lucide:pie-chart" class="size-4" />
金分布
<!-- 产状态面板 -->
<section class="lg:col-span-2">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-3">
<Icon icon="lucide:wallet" class="size-4" />
产状态
</h2>
<UiCard>
<UiCardContent class="p-4">
<VChart :option="distributionOption" autoresize style="height: 240px" />
</UiCardContent>
</UiCard>
<div class="grid gap-3">
<UiCard v-for="item in assetMetrics" :key="item.label" class="hover:shadow-sm transition-shadow">
<UiCardContent class="p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<Icon :icon="item.icon" class="size-4 text-muted-foreground" />
<span class="text-sm text-muted-foreground">{{ item.label }}</span>
</div>
<span class="text-lg font-bold font-mono" :class="item.color">
{{ formatCurrency(item.value) }}
</span>
</div>
</UiCardContent>
</UiCard>
</div>
</section>
</div>
<!-- 模块5: 运营指标 + 快捷入口 -->
<!-- 第三层运营快报 + 快捷入口 -->
<div class="grid gap-6 lg:grid-cols-5">
<!-- 运营指标 -->
<section class="space-y-3 lg:col-span-2">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<!-- 运营快报 -->
<section class="lg:col-span-2">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-3">
<Icon icon="lucide:activity" class="size-4" />
运营指标
运营快报
</h2>
<div class="grid gap-3 grid-cols-2">
<UiCard v-for="item in operationMetrics" :key="item.label" class="hover:shadow-sm transition-shadow">
<UiCard
v-for="item in operationMetrics"
:key="item.label"
class="hover:shadow-sm transition-shadow"
:class="{ 'cursor-pointer': item.label === '待审批' }"
@click="item.label === '待审批' && navigateTo('/monisuo/orders')"
>
<UiCardContent class="p-4">
<div class="flex items-center justify-between">
<div class="min-w-0">
<p class="text-xs text-muted-foreground truncate">
{{ item.label }}
</p>
<p class="text-lg sm:text-xl font-bold mt-1 truncate">
{{ item.value }}
</p>
<div class="flex items-center justify-center mb-2">
<div class="p-2 rounded-lg" :class="[item.bgColor]">
<Icon :icon="item.icon" class="size-4" :class="item.color" />
</div>
<Icon :icon="item.icon" class="size-6 text-muted-foreground/30 shrink-0" />
</div>
<p class="text-xs text-muted-foreground text-center mb-1">
{{ item.label }}
</p>
<p class="text-xl font-bold text-center">
{{ item.value }}
</p>
</UiCardContent>
</UiCard>
</div>
</section>
<!-- 快捷入口 -->
<section class="space-y-3 lg:col-span-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2">
<section class="lg:col-span-3">
<h2 class="text-sm font-medium text-muted-foreground flex items-center gap-2 mb-3">
<Icon icon="lucide:zap" class="size-4" />
快捷入口
</h2>

View File

@@ -104,7 +104,7 @@ function copyToClipboard(text: string) {
手续费
</UiTableHead>
<UiTableHead class="text-right">
应付款
到账金额
</UiTableHead>
<UiTableHead>审批人</UiTableHead>
<UiTableHead class="hidden xl:table-cell">
@@ -282,7 +282,7 @@ function copyToClipboard(text: string) {
<div class="text-muted-foreground">手续费(10%)</div>
<div class="col-span-2 font-mono">-{{ formatAmount(currentOrder.fee || 0) }}</div>
<div class="text-muted-foreground">应付款</div>
<div class="text-muted-foreground">到账金额</div>
<div class="col-span-2 font-mono font-bold text-green-600">{{ formatAmount(currentOrder.receivableAmount || 0) }}</div>
<div class="text-muted-foreground">提现地址</div>
@@ -294,13 +294,21 @@ function copyToClipboard(text: string) {
<span v-else class="text-muted-foreground">-</span>
</div>
<template v-if="currentOrder.network">
<div class="text-muted-foreground">提现网络</div>
<div class="col-span-2">{{ currentOrder.network }}</div>
</template>
<div class="text-muted-foreground">发起时间</div>
<div class="col-span-2">{{ currentOrder.createTime }}</div>
<div v-if="currentOrder.financeApproveTime" class="text-muted-foreground">到账时间</div>
<div v-if="currentOrder.financeApproveTime" class="col-span-2">{{ currentOrder.financeApproveTime }}</div>
<div v-if="currentOrder.approveAdminName" class="text-muted-foreground">审批人</div>
<div v-if="currentOrder.approveAdminName" class="col-span-2">
{{ currentOrder.approveAdminName }}
</div>
<div class="text-muted-foreground">创建时间</div>
<div class="col-span-2">{{ currentOrder.createTime }}</div>
</div>
</div>
<UiDialogFooter>
@@ -340,7 +348,7 @@ function copyToClipboard(text: string) {
<div class="font-mono font-bold text-lg">{{ formatAmount(currentOrder.amount) }}</div>
</div>
<div>
<div class="text-muted-foreground">应付款</div>
<div class="text-muted-foreground">到账金额</div>
<div class="font-mono text-green-600">{{ formatAmount(currentOrder.receivableAmount || 0) }}</div>
</div>
</div>

View File

@@ -133,7 +133,7 @@ function formatAmount(amount: number): string {
function getStatusVariant(order: OrderFund): 'default' | 'secondary' | 'destructive' | 'outline' {
const { type, status } = order
// 充值状态: 1=待付款, 2=待确认, 3=已完成, 4=已驳回, 5=已取消
// 提现状态: 1=待审批, 2=已完成, 3=已驳回, 4=已取消
// 提现状态: 1=待审批, 2=已出款, 3=已驳回, 4=已取消
if (type === 1) {
// 充值
if (status === 1) return 'secondary' // 待付款
@@ -168,7 +168,7 @@ function getStatusText(order: OrderFund): string {
// 提现状态
switch (status) {
case 1: return '待审批'
case 2: return '已完成'
case 2: return '已出款'
case 3: return '已驳回'
case 4: return '已取消'
default: return '未知'
@@ -711,7 +711,7 @@ function copyToClipboard(text: string) {
</template>
<template v-if="currentOrder.type === 2 && currentOrder.receivableAmount">
<div class="text-muted-foreground">
应收款项
到账金额
</div>
<div class="col-span-2 font-mono font-bold text-green-600">
{{ currentOrder.receivableAmount }}
@@ -740,6 +740,16 @@ function copyToClipboard(text: string) {
</div>
</template>
<!-- 提现网络 -->
<template v-if="currentOrder.type === 2 && currentOrder.network">
<div class="text-muted-foreground">
提现网络
</div>
<div class="col-span-2">
{{ currentOrder.network }}
</div>
</template>
<div class="text-muted-foreground">
创建时间
</div>

View File

@@ -60,9 +60,10 @@ export interface OrderFund {
amount: number
fee?: number // 手续费
receivableAmount?: number // 应收款项
status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已完成 3已驳回 4已取消 5待财务审核
status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已出款 3已驳回 4已取消 5待财务审核
walletId?: number
walletAddress?: string
network?: string // 提现网络类型
withdrawContact?: string
payTime?: string
confirmTime?: string
@@ -88,12 +89,24 @@ export interface ColdWallet {
}
export interface FinanceOverview {
// 核心 KPI
totalDeposit: number
totalWithdraw: number
totalActualPayout: number // 实际出款金额
// 资金状态
fundBalance: number
totalFrozen: number // 冻结中金额
tradeValue: number
// 运营数据
pendingCount: number
userCount: number
monthNewUsers: number // 本月新增用户
todayActiveUsers: number // 今日活跃用户
// 环比数据
monthlyDeposit: number
monthlyWithdraw: number
lastMonthDeposit: number
lastMonthWithdraw: number
}
// Auth API