Compare commits

...

3 Commits

Author SHA1 Message Date
sion
7c5b588306 111 2026-04-05 23:37:38 +08:00
sion
d3f2424d1a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	flutter_monisuo/lib/ui/pages/asset/asset_page.dart
#	flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart
2026-04-05 23:34:18 +08:00
sion
da0f9d6f5e 111 2026-04-05 23:28:38 +08:00
22 changed files with 1335 additions and 240 deletions

View File

@@ -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';

View File

@@ -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:

View File

@@ -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>(

View File

@@ -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([

View File

@@ -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>(

View File

@@ -68,7 +68,7 @@ class _FundOrderCard extends StatelessWidget {
case 1:
return '待审批';
case 2:
return '完成';
return '出款';
case 3:
return '已驳回';
case 4:

View File

@@ -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);

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

View 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);

View File

@@ -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;
// 查询1order_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);
// 查询2account_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);
}

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -47,6 +47,9 @@ public class OrderFund implements Serializable {
/** 冷钱包地址(充值)/提现地址 */
private String walletAddress;
/** 提现网络类型(TRC20/ERC20等) */
private String network;
/** 提现联系方式 */
private String withdrawContact;

View File

@@ -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();

View File

@@ -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")

View File

@@ -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());
}
/**
* 获取启用的钱包列表
*/

View File

@@ -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());