Compare commits
3 Commits
d98d3d59f2
...
7c5b588306
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5b588306 | ||
|
|
d3f2424d1a | ||
|
|
da0f9d6f5e |
Binary file not shown.
@@ -89,6 +89,9 @@ class ApiEndpoints {
|
||||
/// 取消订单
|
||||
static const String cancelOrder = '/api/fund/cancel';
|
||||
|
||||
/// 获取可用提现网络列表
|
||||
static const String walletNetworks = '/api/wallet/networks';
|
||||
|
||||
/// 获取充提记录
|
||||
static const String fundOrders = '/api/fund/orders';
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ class OrderFund {
|
||||
// 提现状态: 1=待审批, 2=已完成, 3=已驳回, 4=已取消, 5=待财务审核
|
||||
final int? walletId; // 冷钱包ID(充值)
|
||||
final String? walletAddress; // 钱包地址(充值/提现)
|
||||
final String? network; // 提现网络类型
|
||||
final String? withdrawContact; // 提现联系方式
|
||||
final String remark;
|
||||
final String? rejectReason;
|
||||
@@ -102,6 +103,7 @@ class OrderFund {
|
||||
required this.status,
|
||||
this.walletId,
|
||||
this.walletAddress,
|
||||
this.network,
|
||||
this.withdrawContact,
|
||||
required this.remark,
|
||||
this.rejectReason,
|
||||
@@ -124,6 +126,7 @@ class OrderFund {
|
||||
status: json['status'] as int? ?? 1,
|
||||
walletId: json['walletId'] as int?,
|
||||
walletAddress: json['walletAddress'] as String?,
|
||||
network: json['network'] as String?,
|
||||
withdrawContact: json['withdrawContact'] as String?,
|
||||
remark: json['remark']?.toString() ?? '',
|
||||
rejectReason: json['rejectReason'] as String?,
|
||||
@@ -167,7 +170,7 @@ class OrderFund {
|
||||
case 1:
|
||||
return '待审批';
|
||||
case 2:
|
||||
return '已完成';
|
||||
return '已出款';
|
||||
case 3:
|
||||
return '已驳回';
|
||||
case 4:
|
||||
|
||||
@@ -51,6 +51,7 @@ class FundService {
|
||||
Future<ApiResponse<Map<String, dynamic>>> withdraw({
|
||||
required String amount,
|
||||
required String withdrawAddress,
|
||||
String? network,
|
||||
String? withdrawContact,
|
||||
String? remark,
|
||||
}) async {
|
||||
@@ -59,12 +60,27 @@ class FundService {
|
||||
data: {
|
||||
'amount': amount,
|
||||
'withdrawAddress': withdrawAddress,
|
||||
if (network != null) 'network': network,
|
||||
if (withdrawContact != null) 'withdrawContact': withdrawContact,
|
||||
if (remark != null) 'remark': remark,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取可用的提现网络列表
|
||||
Future<ApiResponse<List<String>>> getWalletNetworks() async {
|
||||
final response = await _client.get<List<dynamic>>(
|
||||
ApiEndpoints.walletNetworks,
|
||||
);
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(
|
||||
response.data!.cast<String>(),
|
||||
response.message,
|
||||
);
|
||||
}
|
||||
return ApiResponse.fail(response.message ?? '获取网络列表失败');
|
||||
}
|
||||
|
||||
/// 取消订单
|
||||
Future<ApiResponse<void>> cancelOrder(String orderNo) async {
|
||||
return _client.post<void>(
|
||||
|
||||
@@ -185,6 +185,7 @@ class AssetProvider extends ChangeNotifier {
|
||||
Future<ApiResponse<Map<String, dynamic>>> withdraw({
|
||||
required String amount,
|
||||
required String withdrawAddress,
|
||||
String? network,
|
||||
String? withdrawContact,
|
||||
String? remark,
|
||||
}) async {
|
||||
@@ -192,6 +193,7 @@ class AssetProvider extends ChangeNotifier {
|
||||
final response = await _fundService.withdraw(
|
||||
amount: amount,
|
||||
withdrawAddress: withdrawAddress,
|
||||
network: network,
|
||||
withdrawContact: withdrawContact,
|
||||
remark: remark,
|
||||
);
|
||||
@@ -244,6 +246,19 @@ class AssetProvider extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取可用提现网络列表
|
||||
Future<List<String>> getWalletNetworks() async {
|
||||
try {
|
||||
final response = await _fundService.getWalletNetworks();
|
||||
if (response.success) {
|
||||
return response.data ?? [];
|
||||
}
|
||||
return [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新所有资产数据
|
||||
Future<void> refreshAll({bool force = false}) async {
|
||||
await Future.wait([
|
||||
|
||||
@@ -120,12 +120,911 @@ class _AssetPageState extends State<AssetPage> with AutomaticKeepAliveClientMixi
|
||||
HoldingsSection(holdings: _activeTab == 1 ? provider.holdings : []),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
displayBalance,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '充值',
|
||||
type: NeonButtonType.tertiary,
|
||||
onPressed: () => _showDepositDialog(context),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '提现',
|
||||
type: NeonButtonType.secondary,
|
||||
onPressed: () => _showWithdrawDialog(context, fund?.balance),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '划转',
|
||||
type: NeonButtonType.outline,
|
||||
onPressed: () => _navigateToTransfer(context),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易账户卡片 - Glass Panel 风格
|
||||
class _TradeAccountCard extends StatelessWidget {
|
||||
final List holdings;
|
||||
final String? tradeBalance;
|
||||
|
||||
const _TradeAccountCard({required this.holdings, this.tradeBalance});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// 计算总市值(所有持仓折算成USDT)
|
||||
double totalValue = 0;
|
||||
for (var h in holdings) {
|
||||
final value = double.tryParse(h.currentValue?.toString() ?? '0') ?? 0;
|
||||
totalValue += value;
|
||||
}
|
||||
|
||||
// 对持仓进行排序:USDT 放在最上面
|
||||
final sortedHoldings = List.from(holdings);
|
||||
sortedHoldings.sort((a, b) {
|
||||
final codeA = (a.coinCode ?? a['coinCode'] ?? '').toString().toUpperCase();
|
||||
final codeB = (b.coinCode ?? b['coinCode'] ?? '').toString().toUpperCase();
|
||||
if (codeA == 'USDT') return -1;
|
||||
if (codeB == 'USDT') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return GlassPanel(
|
||||
padding: AppSpacing.cardPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题行
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.trendingUp,
|
||||
size: 18,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'交易账户',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 14,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
// 余额
|
||||
Text(
|
||||
'余额 (USDT)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
totalValue.toStringAsFixed(2),
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
// 持仓列表标题
|
||||
Text(
|
||||
'持仓列表',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
if (sortedHoldings.isEmpty)
|
||||
const _EmptyState(icon: LucideIcons.wallet, message: '暂无持仓')
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: sortedHoldings.length,
|
||||
separatorBuilder: (_, __) => Container(
|
||||
margin: EdgeInsets.only(left: 56),
|
||||
height: 1,
|
||||
color: AppColorScheme.glassPanelBorder,
|
||||
),
|
||||
itemBuilder: (context, index) => _HoldingItem(holding: sortedHoldings[index]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 空状态
|
||||
class _EmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String message;
|
||||
|
||||
const _EmptyState({required this.icon, required this.message});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(AppSpacing.xl),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 48,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 持仓项
|
||||
class _HoldingItem extends StatelessWidget {
|
||||
final dynamic holding;
|
||||
|
||||
const _HoldingItem({required this.holding});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: AppSpacing.sm),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
holding.coinCode.substring(0, 1),
|
||||
style: TextStyle(
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
holding.coinCode,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'数量: ${holding.quantity}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${holding.currentValue} USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
holding.formattedProfitRate,
|
||||
style: TextStyle(
|
||||
color: holding.isProfit ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Dialogs - Glass Panel 风格
|
||||
// ============================================
|
||||
|
||||
void _showDepositDialog(BuildContext context) {
|
||||
final amountController = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GlassPanel(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Deposit (充值)',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Asset: USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.1,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.wallet,
|
||||
color: colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
ShadForm(
|
||||
key: formKey,
|
||||
child: ShadInputFormField(
|
||||
id: 'amount',
|
||||
controller: amountController,
|
||||
label: const Text('充值金额'),
|
||||
placeholder: const Text('最低 1000 USDT'),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) return '请输入金额';
|
||||
final n = double.tryParse(v);
|
||||
if (n == null || n <= 0) return '请输入有效金额';
|
||||
if (n < 1000) return '单笔最低充值1000 USDT';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '取消',
|
||||
type: NeonButtonType.outline,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
height: 48,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '下一步',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: () async {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
Navigator.of(ctx).pop();
|
||||
final response = await context.read<AssetProvider>().deposit(
|
||||
amount: amountController.text,
|
||||
);
|
||||
if (context.mounted) {
|
||||
if (response.success && response.data != null) {
|
||||
_showDepositResultDialog(context, response.data!);
|
||||
} else {
|
||||
_showResultDialog(context, '申请失败', response.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
height: 48,
|
||||
showGlow: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDepositResultDialog(BuildContext context, Map<String, dynamic> data) {
|
||||
final orderNo = data['orderNo'] as String? ?? '';
|
||||
final amount = data['amount']?.toString() ?? '0.00';
|
||||
final walletAddress = data['walletAddress'] as String? ?? '';
|
||||
final walletNetwork = data['walletNetwork'] as String? ?? 'TRC20';
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GlassPanel(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
NeonIcon(
|
||||
icon: Icons.check_circle,
|
||||
color: AppColorScheme.getUpColor(isDark),
|
||||
size: 24,
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'充值申请成功',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
_InfoRow(label: '订单号', value: orderNo),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_InfoRow(label: '充值金额', value: '$amount USDT', isBold: true),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Text(
|
||||
'请向以下地址转账:',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
_WalletAddressCard(address: walletAddress, network: walletNetwork),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorScheme.warning.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(
|
||||
color: AppColorScheme.warning.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 16, color: AppColorScheme.warning),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'转账完成后请点击"已打款"按钮确认',
|
||||
style: TextStyle(fontSize: 12, color: AppColorScheme.warning),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '稍后确认',
|
||||
type: NeonButtonType.outline,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '已打款',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
final response = await context.read<AssetProvider>().confirmPay(orderNo);
|
||||
if (context.mounted) {
|
||||
_showResultDialog(
|
||||
context,
|
||||
response.success ? '确认成功' : '确认失败',
|
||||
response.success ? '请等待管理员审核' : response.message,
|
||||
);
|
||||
}
|
||||
},
|
||||
height: 44,
|
||||
showGlow: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool isBold;
|
||||
|
||||
const _InfoRow({required this.label, required this.value, this.isBold = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletAddressCard extends StatelessWidget {
|
||||
final String address;
|
||||
final String network;
|
||||
|
||||
const _WalletAddressCard({required this.address, required this.network});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
address,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: address));
|
||||
ToastUtils.show('地址已复制到剪贴板');
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(AppSpacing.xs),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.copy,
|
||||
size: 16,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'网络: $network',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showWithdrawDialog(BuildContext context, String? balance) {
|
||||
final amountController = TextEditingController();
|
||||
final addressController = TextEditingController();
|
||||
final contactController = TextEditingController();
|
||||
final formKey = GlobalKey<ShadFormState>();
|
||||
final feeNotifier = ValueNotifier<String>('提现将扣除10%手续费');
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final networksNotifier = ValueNotifier<List<String>>([]);
|
||||
final selectedNetworkNotifier = ValueNotifier<String?>(null);
|
||||
|
||||
amountController.addListener(() {
|
||||
final amount = double.tryParse(amountController.text) ?? 0;
|
||||
if (amount > 0) {
|
||||
final fee = amount * 0.1;
|
||||
final receivable = amount - fee;
|
||||
feeNotifier.value = '手续费(10%): -${fee.toStringAsFixed(2)} USDT | 应付款: ${receivable.toStringAsFixed(2)} USDT';
|
||||
} else {
|
||||
feeNotifier.value = '提现将扣除10%手续费';
|
||||
}
|
||||
});
|
||||
|
||||
// 获取网络列表
|
||||
context.read<AssetProvider>().getWalletNetworks().then((list) {
|
||||
networksNotifier.value = list;
|
||||
if (list.isNotEmpty) {
|
||||
selectedNetworkNotifier.value = list.first;
|
||||
}
|
||||
});
|
||||
|
||||
showShadDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setState) => Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: GlassPanel(
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
child: Icon(
|
||||
LucideIcons.wallet,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
'提现 (Withdraw)',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Securely transfer your assets to an external wallet address.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (balance != null) ...[
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorScheme.up.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(
|
||||
color: AppColorScheme.up.withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'可用余额: ',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.1,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$balance USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorScheme.up,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
ShadForm(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
ShadInputFormField(
|
||||
id: 'amount',
|
||||
controller: amountController,
|
||||
label: const Text('提现金额'),
|
||||
placeholder: const Text('请输入提现金额(USDT)'),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: Validators.amount,
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 14, color: Colors.orange),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<String>(
|
||||
valueListenable: feeNotifier,
|
||||
builder: (_, text, __) => Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
// 提现网络选择
|
||||
ValueListenableBuilder<List<String>>(
|
||||
valueListenable: networksNotifier,
|
||||
builder: (_, networks, __) {
|
||||
if (networks.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'提现网络',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ValueListenableBuilder<String?>(
|
||||
valueListenable: selectedNetworkNotifier,
|
||||
builder: (_, selected, __) => ShadSelect<String>(
|
||||
initialValue: selected ?? networks.first,
|
||||
placeholder: const Text('选择提现网络'),
|
||||
onChanged: (value) {
|
||||
selectedNetworkNotifier.value = value;
|
||||
},
|
||||
selectedOptionBuilder: (context, value) => Text(value),
|
||||
options: networks.map((network) => ShadOption<String>(
|
||||
value: network,
|
||||
child: Text(network),
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
ShadInputFormField(
|
||||
id: 'address',
|
||||
controller: addressController,
|
||||
label: const Text('目标地址'),
|
||||
placeholder: const Text('请输入提现地址'),
|
||||
validator: (v) => Validators.required(v, '提现地址'),
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
ShadInputFormField(
|
||||
id: 'contact',
|
||||
controller: contactController,
|
||||
label: const Text('联系方式(可选)'),
|
||||
placeholder: const Text('联系方式'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '取消',
|
||||
type: NeonButtonType.outline,
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
height: 44,
|
||||
showGlow: false,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: NeonButton(
|
||||
text: '提交',
|
||||
type: NeonButtonType.primary,
|
||||
onPressed: () async {
|
||||
if (formKey.currentState!.saveAndValidate()) {
|
||||
Navigator.of(ctx).pop();
|
||||
final response = await context.read<AssetProvider>().withdraw(
|
||||
amount: amountController.text,
|
||||
withdrawAddress: addressController.text,
|
||||
network: selectedNetworkNotifier.value,
|
||||
withdrawContact: contactController.text.isNotEmpty
|
||||
? contactController.text
|
||||
: null,
|
||||
);
|
||||
if (context.mounted) {
|
||||
_showResultDialog(
|
||||
context,
|
||||
response.success ? '申请成功' : '申请失败',
|
||||
response.success ? '请等待管理员审批' : response.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
height: 44,
|
||||
showGlow: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.verified_user,
|
||||
size: 12,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'End-to-End Encrypted Transaction',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
letterSpacing: 0.1,
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToTransfer(BuildContext context) async {
|
||||
final result = await Navigator.push<bool>(
|
||||
|
||||
@@ -68,7 +68,7 @@ class _FundOrderCard extends StatelessWidget {
|
||||
case 1:
|
||||
return '待审批';
|
||||
case 2:
|
||||
return '已完成';
|
||||
return '已出款';
|
||||
case 3:
|
||||
return '已驳回';
|
||||
case 4:
|
||||
|
||||
@@ -224,39 +224,65 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
|
||||
const SizedBox(height: 8),
|
||||
_buildRejectionReason(order),
|
||||
],
|
||||
if (order.receivableAmount != null && !order.isDeposit) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildPayableRow(order),
|
||||
if (order.withdrawContact != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'联系方式: ${order.withdrawContact}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
if (order.canCancel || order.canConfirmPay) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildActions(order),
|
||||
// 提现详情
|
||||
if (!isDeposit) ...[
|
||||
if (order.fee != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Text('手续费(10%): ', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
Text('-${order.fee} USDT', style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (order.receivableAmount != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Text('到账金额: ', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
Text('${order.receivableAmount} USDT', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (order.network != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'提现网络: ${order.network}',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'创建: ${_formatTime(order.createTime)}',
|
||||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||||
),
|
||||
if (order.rejectReason != null)
|
||||
Expanded(
|
||||
child: Text(
|
||||
'驳回: ${order.rejectReason}',
|
||||
style: const TextStyle(fontSize: 11, color: downColor),
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Card Header - type badge + status badge
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCardHeader(OrderFund order) {
|
||||
final upColor = AppColorScheme.getUpColor(_isDark);
|
||||
final downColor = AppColorScheme.getDownColor(_isDark);
|
||||
final upBg = AppColorScheme.getUpBackgroundColor(_isDark, opacity: 0.12);
|
||||
final downBg = AppColorScheme.getDownBackgroundColor(_isDark, opacity: 0.12);
|
||||
final typeColor = order.isDeposit ? upColor : downColor;
|
||||
final typeBg = order.isDeposit ? upBg : downBg;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_buildBadge(order.typeText, typeColor, typeBg),
|
||||
_buildStatusBadge(order),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(OrderFund order) {
|
||||
final upColor = AppColorScheme.getUpColor(_isDark);
|
||||
final downColor = AppColorScheme.getDownColor(_isDark);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
4
sql/patch_dashboard_indexes.sql
Normal file
4
sql/patch_dashboard_indexes.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- 看板性能优化:为 order_fund 添加组合索引
|
||||
-- 用于聚合查询(充值/提现统计、月度趋势)
|
||||
ALTER TABLE order_fund ADD INDEX idx_type_status (type, status);
|
||||
ALTER TABLE order_fund ADD INDEX idx_type_status_time (type, status, create_time);
|
||||
@@ -8,11 +8,13 @@ import com.it.rattan.monisuo.context.UserContext;
|
||||
import com.it.rattan.monisuo.entity.*;
|
||||
import com.it.rattan.monisuo.mapper.AccountFundMapper;
|
||||
import com.it.rattan.monisuo.mapper.OrderFundMapper;
|
||||
import com.it.rattan.monisuo.mapper.OrderTradeMapper;
|
||||
import com.it.rattan.monisuo.service.*;
|
||||
import com.it.rattan.monisuo.util.JwtUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -46,6 +48,9 @@ public class AdminController {
|
||||
@Autowired
|
||||
private OrderFundMapper orderFundMapper;
|
||||
|
||||
@Autowired
|
||||
private OrderTradeMapper orderTradeMapper;
|
||||
|
||||
// ==================== 公开接口 ====================
|
||||
|
||||
/**
|
||||
@@ -451,7 +456,7 @@ public class AdminController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 资金总览
|
||||
* 资金总览(看板专用,一次 API 返回所有数据)
|
||||
*/
|
||||
@GetMapping("/finance/overview")
|
||||
public Result<Map<String, Object>> getFinanceOverview() {
|
||||
@@ -461,21 +466,47 @@ public class AdminController {
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
||||
// 合并为一次查询获取充值总额、提现总额、待审批数
|
||||
Map<String, Object> fundStats = orderFundMapper.sumFinanceOverview();
|
||||
// 本月和上月时间范围(用于环比)
|
||||
LocalDate today = LocalDate.now();
|
||||
LocalDateTime monthStart = today.withDayOfMonth(1).atStartOfDay();
|
||||
LocalDateTime monthEnd = today.plusMonths(1).withDayOfMonth(1).atStartOfDay();
|
||||
LocalDateTime lastMonthStart = today.minusMonths(1).withDayOfMonth(1).atStartOfDay();
|
||||
LocalDateTime lastMonthEnd = monthStart;
|
||||
|
||||
// 查询1:order_fund 一次聚合(充值总额、提现总额、实际出款、待审批、环比)
|
||||
Map<String, Object> fundStats = orderFundMapper.sumDashboardStats(
|
||||
monthStart, monthEnd, lastMonthStart, lastMonthEnd);
|
||||
data.put("totalDeposit", fundStats.get("totalDeposit"));
|
||||
data.put("totalWithdraw", fundStats.get("totalWithdraw"));
|
||||
data.put("totalActualPayout", fundStats.get("totalActualPayout"));
|
||||
data.put("pendingCount", ((Number) fundStats.get("pendingCount")).intValue());
|
||||
data.put("monthlyDeposit", fundStats.get("monthlyDeposit"));
|
||||
data.put("monthlyWithdraw", fundStats.get("monthlyWithdraw"));
|
||||
data.put("lastMonthDeposit", fundStats.get("lastMonthDeposit"));
|
||||
data.put("lastMonthWithdraw", fundStats.get("lastMonthWithdraw"));
|
||||
|
||||
BigDecimal fundBalance = accountFundMapper.sumAllBalance();
|
||||
data.put("fundBalance", fundBalance);
|
||||
// 查询2:account_fund 余额和冻结
|
||||
Map<String, Object> balanceStats = accountFundMapper.sumBalanceAndFrozen();
|
||||
data.put("fundBalance", balanceStats.get("totalBalance"));
|
||||
data.put("totalFrozen", balanceStats.get("totalFrozen"));
|
||||
|
||||
// 查询3:交易账户市值
|
||||
BigDecimal tradeValue = accountFundMapper.sumAllTradeValue();
|
||||
data.put("tradeValue", tradeValue != null ? tradeValue : BigDecimal.ZERO);
|
||||
|
||||
// 查询4:用户统计
|
||||
long userCount = userService.count();
|
||||
data.put("userCount", userCount);
|
||||
|
||||
int monthNewUsers = userService.count(new LambdaQueryWrapper<User>()
|
||||
.ge(User::getCreateTime, monthStart));
|
||||
data.put("monthNewUsers", monthNewUsers);
|
||||
|
||||
// 查询5:今日活跃用户(今日有过交易的独立用户数)
|
||||
int todayActiveUsers = orderTradeMapper.countDistinctUserByTime(
|
||||
today.atStartOfDay(), LocalDateTime.now());
|
||||
data.put("todayActiveUsers", todayActiveUsers);
|
||||
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -135,4 +135,13 @@ public class ColdWalletController {
|
||||
result.put("network", wallet.getNetwork());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户端 - 获取可用的提现网络列表
|
||||
*/
|
||||
@GetMapping("/api/wallet/networks")
|
||||
public Result<List<String>> getNetworks() {
|
||||
List<String> networks = coldWalletService.getEnabledNetworks();
|
||||
return Result.success(networks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ public class FundController {
|
||||
|
||||
BigDecimal amount = request.getAmount();
|
||||
String withdrawAddress = request.getWithdrawAddress();
|
||||
String network = request.getNetwork();
|
||||
String withdrawContact = request.getWithdrawContact();
|
||||
String remark = request.getRemark();
|
||||
|
||||
@@ -99,7 +100,7 @@ public class FundController {
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> result = fundService.withdraw(userId, amount, withdrawAddress, withdrawContact, remark);
|
||||
Map<String, Object> result = fundService.withdraw(userId, amount, withdrawAddress, network, withdrawContact, remark);
|
||||
return Result.success("申请成功,等待审批", result);
|
||||
} catch (Exception e) {
|
||||
return Result.fail(e.getMessage());
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.math.BigDecimal;
|
||||
public class WithdrawRequest {
|
||||
private BigDecimal amount;
|
||||
private String withdrawAddress;
|
||||
private String network;
|
||||
private String withdrawContact;
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ public class OrderFund implements Serializable {
|
||||
/** 冷钱包地址(充值)/提现地址 */
|
||||
private String walletAddress;
|
||||
|
||||
/** 提现网络类型(TRC20/ERC20等) */
|
||||
private String network;
|
||||
|
||||
/** 提现联系方式 */
|
||||
private String withdrawContact;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.it.rattan.monisuo.entity.AccountFund;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 资金账户Mapper
|
||||
@@ -15,6 +16,12 @@ public interface AccountFundMapper extends BaseMapper<AccountFund> {
|
||||
@Select("SELECT IFNULL(SUM(balance), 0) FROM account_fund")
|
||||
BigDecimal sumAllBalance();
|
||||
|
||||
/**
|
||||
* 一次查询获取总余额和总冻结金额
|
||||
*/
|
||||
@Select("SELECT IFNULL(SUM(balance), 0) as totalBalance, IFNULL(SUM(frozen), 0) as totalFrozen FROM account_fund")
|
||||
Map<String, Object> sumBalanceAndFrozen();
|
||||
|
||||
@Select("SELECT IFNULL(SUM(total_deposit), 0) FROM account_fund")
|
||||
BigDecimal sumTotalDeposit();
|
||||
|
||||
|
||||
@@ -16,31 +16,34 @@ import java.util.Map;
|
||||
@Mapper
|
||||
public interface OrderFundMapper extends BaseMapper<OrderFund> {
|
||||
|
||||
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 1 AND status = 2")
|
||||
BigDecimal sumCompletedDeposit();
|
||||
|
||||
@Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 2 AND status = 2")
|
||||
BigDecimal sumCompletedWithdraw();
|
||||
|
||||
@Select("SELECT COUNT(*) FROM order_fund WHERE status = 1")
|
||||
int countPending();
|
||||
|
||||
/**
|
||||
* 一次性聚合查询:充值总额、提现总额、待审批数
|
||||
* 一次性聚合查询:充值总额、提现总额、实际出款、待审批数、本月/上月环比数据
|
||||
* 充值已完成 status=3,提现已完成 status=2
|
||||
*/
|
||||
@Select("SELECT " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 2 THEN amount ELSE 0 END), 0) as totalDeposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 3 THEN amount ELSE 0 END), 0) as totalDeposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN amount ELSE 0 END), 0) as totalWithdraw, " +
|
||||
"SUM(CASE WHEN status IN (1, 5) THEN 1 ELSE 0 END) as pendingCount " +
|
||||
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN IFNULL(receivable_amount, amount * 0.9) ELSE 0 END), 0) as totalActualPayout, " +
|
||||
"SUM(CASE WHEN (type = 1 AND status = 2) OR (type = 2 AND status IN (1, 5)) THEN 1 ELSE 0 END) as pendingCount, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 3 AND create_time >= #{monthStart} AND create_time < #{monthEnd} THEN amount ELSE 0 END), 0) as monthlyDeposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 AND create_time >= #{monthStart} AND create_time < #{monthEnd} THEN amount ELSE 0 END), 0) as monthlyWithdraw, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 3 AND create_time >= #{lastMonthStart} AND create_time < #{lastMonthEnd} THEN amount ELSE 0 END), 0) as lastMonthDeposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 AND create_time >= #{lastMonthStart} AND create_time < #{lastMonthEnd} THEN amount ELSE 0 END), 0) as lastMonthWithdraw " +
|
||||
"FROM order_fund")
|
||||
Map<String, Object> sumFinanceOverview();
|
||||
Map<String, Object> sumDashboardStats(
|
||||
@Param("monthStart") LocalDateTime monthStart,
|
||||
@Param("monthEnd") LocalDateTime monthEnd,
|
||||
@Param("lastMonthStart") LocalDateTime lastMonthStart,
|
||||
@Param("lastMonthEnd") LocalDateTime lastMonthEnd);
|
||||
|
||||
// ========== 分析相关查询 ==========
|
||||
|
||||
/**
|
||||
* 指定时间段内的手续费总额(0.5%)
|
||||
* 充值已完成 status=3,提现已完成 status=2
|
||||
*/
|
||||
@Select("SELECT IFNULL(SUM(amount * 0.005), 0) FROM order_fund WHERE status = 2 AND create_time >= #{startTime}")
|
||||
@Select("SELECT IFNULL(SUM(amount * 0.005), 0) FROM order_fund WHERE " +
|
||||
"((type = 1 AND status = 3) OR (type = 2 AND status = 2)) AND create_time >= #{startTime}")
|
||||
BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime);
|
||||
|
||||
/**
|
||||
@@ -58,9 +61,10 @@ public interface OrderFundMapper extends BaseMapper<OrderFund> {
|
||||
|
||||
/**
|
||||
* 按月分组统计充值/提现金额(替代循环 N 次查询)
|
||||
* 充值已完成 status=3,提现已完成 status=2
|
||||
*/
|
||||
@Select("SELECT DATE_FORMAT(create_time, '%Y-%m') as month, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 2 THEN amount ELSE 0 END), 0) as deposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 1 AND status = 3 THEN amount ELSE 0 END), 0) as deposit, " +
|
||||
"IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN amount ELSE 0 END), 0) as withdraw " +
|
||||
"FROM order_fund WHERE create_time >= #{startTime} AND create_time < #{endTime} " +
|
||||
"GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month")
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 冷钱包地址服务
|
||||
@@ -28,6 +29,18 @@ public class ColdWalletService {
|
||||
return coldWalletMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有启用的网络类型(去重)
|
||||
*/
|
||||
public List<String> getEnabledNetworks() {
|
||||
List<ColdWallet> wallets = getEnabledList();
|
||||
return wallets.stream()
|
||||
.map(ColdWallet::getNetwork)
|
||||
.filter(n -> n != null && !n.isEmpty())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的钱包列表
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.*;
|
||||
*
|
||||
* 状态定义:
|
||||
* 充值: 1=待付款, 2=待确认, 3=已完成, 4=已驳回, 5=已取消
|
||||
* 提现: 1=待审批, 2=已完成, 3=已驳回, 4=已取消
|
||||
* 提现: 1=待审批, 2=已出款, 3=已驳回, 4=已取消
|
||||
*
|
||||
* 审批参数说明:
|
||||
* status=2 表示审批通过(充值订单最终状态为3,提现订单最终状态为2)
|
||||
@@ -128,7 +128,7 @@ public class FundService {
|
||||
*/
|
||||
@Transactional
|
||||
public Map<String, Object> withdraw(Long userId, BigDecimal amount, String withdrawAddress,
|
||||
String withdrawContact, String remark) {
|
||||
String network, String withdrawContact, String remark) {
|
||||
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new RuntimeException("提现金额必须大于0");
|
||||
}
|
||||
@@ -200,6 +200,7 @@ public class FundService {
|
||||
order.setReceivableAmount(receivableAmount);
|
||||
order.setStatus(1); // 待审批
|
||||
order.setWalletAddress(withdrawAddress);
|
||||
order.setNetwork(network);
|
||||
order.setWithdrawContact(withdrawContact);
|
||||
order.setRemark(remark);
|
||||
order.setCreateTime(LocalDateTime.now());
|
||||
|
||||
Reference in New Issue
Block a user