feat(frontend): 添加积分股划转功能
- 交易页面添加划转入口链接 - 实现双向划转弹窗(划入交易账户/划出到挖矿账户) - 新增划转历史记录页面 - 添加划转相关 API 调用 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1760f9b82c
commit
4e181354f4
|
|
@ -21,6 +21,7 @@ import '../../presentation/pages/c2c/c2c_publish_page.dart';
|
|||
import '../../presentation/pages/c2c/c2c_order_detail_page.dart';
|
||||
import '../../presentation/pages/profile/team_page.dart';
|
||||
import '../../presentation/pages/profile/trading_records_page.dart';
|
||||
import '../../presentation/pages/trading/transfer_records_page.dart';
|
||||
import '../../presentation/pages/profile/help_center_page.dart';
|
||||
import '../../presentation/pages/profile/about_page.dart';
|
||||
import '../../presentation/widgets/main_shell.dart';
|
||||
|
|
@ -157,6 +158,10 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
path: Routes.tradingRecords,
|
||||
builder: (context, state) => const TradingRecordsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.transferRecords,
|
||||
builder: (context, state) => const TransferRecordsPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: Routes.helpCenter,
|
||||
builder: (context, state) => const HelpCenterPage(),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ class Routes {
|
|||
static const String myTeam = '/my-team';
|
||||
// 交易记录
|
||||
static const String tradingRecords = '/trading-records';
|
||||
// 划转记录
|
||||
static const String transferRecords = '/transfer-records';
|
||||
// 其他设置
|
||||
static const String helpCenter = '/help-center';
|
||||
static const String about = '/about';
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ abstract class TradingRemoteDataSource {
|
|||
/// 划出积分股 (从交易账户到挖矿账户)
|
||||
Future<Map<String, dynamic>> transferOut(String amount);
|
||||
|
||||
/// 获取划转历史记录
|
||||
Future<List<dynamic>> getTransferHistory({int page = 1, int pageSize = 50});
|
||||
|
||||
/// 获取我的资产显示信息
|
||||
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation});
|
||||
|
||||
|
|
@ -243,6 +246,19 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<dynamic>> getTransferHistory({int page = 1, int pageSize = 50}) async {
|
||||
try {
|
||||
final response = await client.get(
|
||||
ApiEndpoints.transferHistory,
|
||||
queryParameters: {'page': page, 'pageSize': pageSize},
|
||||
);
|
||||
return response.data['data'] ?? [];
|
||||
} catch (e) {
|
||||
throw ServerException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AssetDisplayModel> getMyAsset({String? dailyAllocation}) async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,18 @@ class TradingRepositoryImpl implements TradingRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<dynamic>>> getTransferHistory({int page = 1, int pageSize = 50}) async {
|
||||
try {
|
||||
final result = await remoteDataSource.getTransferHistory(page: page, pageSize: pageSize);
|
||||
return Right(result);
|
||||
} on ServerException catch (e) {
|
||||
return Left(ServerFailure(e.message));
|
||||
} on NetworkException {
|
||||
return Left(const NetworkFailure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation}) async {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ abstract class TradingRepository {
|
|||
/// 划出积分股 (从交易账户到挖矿账户)
|
||||
Future<Either<Failure, Map<String, dynamic>>> transferOut(String amount);
|
||||
|
||||
/// 获取划转历史记录
|
||||
Future<Either<Failure, List<dynamic>>> getTransferHistory({int page = 1, int pageSize = 50});
|
||||
|
||||
/// 获取我的资产显示信息
|
||||
Future<Either<Failure, AssetDisplay>> getMyAsset({String? dailyAllocation});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/router/routes.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../../data/models/trade_order_model.dart';
|
||||
import '../../../domain/entities/price_info.dart';
|
||||
|
|
@ -352,7 +354,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
final buyEnabledAsync = ref.watch(buyEnabledProvider);
|
||||
final buyEnabled = buyEnabledAsync.valueOrNull ?? false;
|
||||
|
||||
// 可用积分股(交易账户)
|
||||
// 挖矿账户积分股(可划转卖出)
|
||||
final miningShareBalance = asset?.miningShareBalance ?? '0';
|
||||
// 交易账户积分股(可直接卖出)
|
||||
final tradingShareBalance = asset?.tradingShareBalance ?? '0';
|
||||
// 可用积分股(总计:挖矿 + 交易)
|
||||
final availableShares = asset?.availableShares ?? '0';
|
||||
// 可用积分值(现金)
|
||||
final availableCash = asset?.availableCash ?? '0';
|
||||
|
|
@ -497,32 +503,75 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
] else ...[
|
||||
// 可用余额提示
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
if (_selectedTab == 0) ...[
|
||||
// 买入时显示可用积分值
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'可用积分值',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
formatAmount(availableCash),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_selectedTab == 0 ? '可用积分值' : '可用积分股',
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
_selectedTab == 0
|
||||
? formatAmount(availableCash)
|
||||
: formatAmount(availableShares),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
] else ...[
|
||||
// 卖出时显示可用积分股
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'可用积分股',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
formatAmount(tradingShareBalance),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 划转入口
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showTransferDialog(miningShareBalance, tradingShareBalance),
|
||||
child: const Text(
|
||||
'划转',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
// 价格输入
|
||||
_buildInputField('价格', _priceController, '请输入价格', '积分值'),
|
||||
|
|
@ -1094,4 +1143,411 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
ref.invalidate(marketOverviewProvider);
|
||||
ref.invalidate(accountAssetProvider(accountSeq));
|
||||
}
|
||||
|
||||
/// 显示划转弹窗
|
||||
void _showTransferDialog(String miningBalance, String tradingBalance) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => _TransferBottomSheet(
|
||||
miningBalance: miningBalance,
|
||||
tradingBalance: tradingBalance,
|
||||
onTransferComplete: () {
|
||||
final user = ref.read(userNotifierProvider);
|
||||
final accountSeq = user.accountSequence ?? '';
|
||||
_doRefresh(accountSeq);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 划转底部弹窗
|
||||
class _TransferBottomSheet extends ConsumerStatefulWidget {
|
||||
final String miningBalance;
|
||||
final String tradingBalance;
|
||||
final VoidCallback onTransferComplete;
|
||||
|
||||
const _TransferBottomSheet({
|
||||
required this.miningBalance,
|
||||
required this.tradingBalance,
|
||||
required this.onTransferComplete,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_TransferBottomSheet> createState() => _TransferBottomSheetState();
|
||||
}
|
||||
|
||||
class _TransferBottomSheetState extends ConsumerState<_TransferBottomSheet> {
|
||||
static const Color _orange = Color(0xFFFF6B00);
|
||||
static const Color _grayText = Color(0xFF6B7280);
|
||||
static const Color _darkText = Color(0xFF1F2937);
|
||||
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
|
||||
// 0: 从挖矿划入交易, 1: 从交易划出到挖矿
|
||||
int _direction = 0;
|
||||
final _amountController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String get _availableBalance {
|
||||
return _direction == 0 ? widget.miningBalance : widget.tradingBalance;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题栏
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'积分股划转',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Icon(Icons.close, color: _grayText),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 划转方向切换
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: _bgGray,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_direction = 0;
|
||||
_amountController.clear();
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _direction == 0 ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: _direction == 0
|
||||
? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
'划入交易账户',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: _direction == 0 ? FontWeight.bold : FontWeight.normal,
|
||||
color: _direction == 0 ? _orange : _grayText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_direction = 1;
|
||||
_amountController.clear();
|
||||
}),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: _direction == 1 ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
boxShadow: _direction == 1
|
||||
? [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4)]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
'划出到挖矿账户',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: _direction == 1 ? FontWeight.bold : FontWeight.normal,
|
||||
color: _direction == 1 ? _orange : _grayText,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 划转说明
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: _bgGray,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_direction == 0 ? '挖矿账户' : '交易账户',
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formatAmount(_direction == 0 ? widget.miningBalance : widget.tradingBalance),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward, color: _orange),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_direction == 0 ? '交易账户' : '挖矿账户',
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
formatAmount(_direction == 0 ? widget.tradingBalance : widget.miningBalance),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 数量输入
|
||||
const Text(
|
||||
'划转数量',
|
||||
style: TextStyle(fontSize: 14, color: _darkText),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: _bgGray),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _amountController,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
hintText: '请输入划转数量',
|
||||
hintStyle: TextStyle(color: _grayText, fontSize: 14),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_amountController.text = _availableBalance;
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'全部',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'可用: ${formatAmount(_availableBalance)} 积分股',
|
||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'提示: 最低划转数量为 5 积分股',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
context.push(Routes.transferRecords);
|
||||
},
|
||||
child: const Text(
|
||||
'划转记录',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 提交按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleTransfer,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _orange,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_direction == 0 ? '划入交易账户' : '划出到挖矿账户',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleTransfer() async {
|
||||
final amount = _amountController.text.trim();
|
||||
if (amount.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请输入划转数量')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final amountValue = double.tryParse(amount) ?? 0;
|
||||
if (amountValue < 5) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('最低划转数量为 5 积分股')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final available = double.tryParse(_availableBalance) ?? 0;
|
||||
if (amountValue > available) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('划转数量超过可用余额')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
bool success;
|
||||
if (_direction == 0) {
|
||||
// 从挖矿划入交易
|
||||
success = await ref.read(tradingNotifierProvider.notifier).transferIn(amount);
|
||||
} else {
|
||||
// 从交易划出到挖矿
|
||||
success = await ref.read(tradingNotifierProvider.notifier).transferOut(amount);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (success) {
|
||||
Navigator.pop(context);
|
||||
widget.onTransferComplete();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_direction == 0 ? '划入成功' : '划出成功'),
|
||||
backgroundColor: _green,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('划转失败,请稍后重试'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('划转失败: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,318 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/constants/app_colors.dart';
|
||||
import '../../../core/utils/format_utils.dart';
|
||||
import '../../../domain/repositories/trading_repository.dart';
|
||||
import '../../providers/trading_providers.dart';
|
||||
|
||||
/// 划转记录数据模型
|
||||
class TransferRecord {
|
||||
final String transferNo;
|
||||
final String direction; // IN: 划入交易账户, OUT: 划出到挖矿账户
|
||||
final String amount;
|
||||
final String status; // PENDING, COMPLETED, FAILED
|
||||
final DateTime createdAt;
|
||||
final DateTime? completedAt;
|
||||
final String? errorMessage;
|
||||
|
||||
TransferRecord({
|
||||
required this.transferNo,
|
||||
required this.direction,
|
||||
required this.amount,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
this.completedAt,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
factory TransferRecord.fromJson(Map<String, dynamic> json) {
|
||||
return TransferRecord(
|
||||
transferNo: json['transferNo'] ?? '',
|
||||
direction: json['direction'] ?? '',
|
||||
amount: json['amount']?.toString() ?? '0',
|
||||
status: json['status'] ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] ?? '') ?? DateTime.now(),
|
||||
completedAt: json['completedAt'] != null
|
||||
? DateTime.tryParse(json['completedAt'])
|
||||
: null,
|
||||
errorMessage: json['errorMessage'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 划转记录页面 Provider
|
||||
final transferRecordsProvider = FutureProvider<List<TransferRecord>>((ref) async {
|
||||
final repository = ref.watch(tradingRepositoryProvider);
|
||||
final result = await repository.getTransferHistory();
|
||||
|
||||
return result.fold(
|
||||
(failure) => throw Exception(failure.message),
|
||||
(records) => records.map((json) => TransferRecord.fromJson(json as Map<String, dynamic>)).toList(),
|
||||
);
|
||||
});
|
||||
|
||||
class TransferRecordsPage extends ConsumerWidget {
|
||||
const TransferRecordsPage({super.key});
|
||||
|
||||
static const Color _orange = Color(0xFFFF6B00);
|
||||
static const Color _green = Color(0xFF10B981);
|
||||
static const Color _red = Color(0xFFEF4444);
|
||||
static const Color _grayText = Color(0xFF6B7280);
|
||||
static const Color _darkText = Color(0xFF1F2937);
|
||||
static const Color _bgGray = Color(0xFFF3F4F6);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final recordsAsync = ref.watch(transferRecordsProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: _bgGray,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'划转记录',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
iconTheme: const IconThemeData(color: _darkText),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(transferRecordsProvider);
|
||||
},
|
||||
child: recordsAsync.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: _grayText),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败',
|
||||
style: TextStyle(color: _grayText),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: () => ref.invalidate(transferRecordsProvider),
|
||||
child: const Text('点击重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (records) {
|
||||
if (records.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
size: 64,
|
||||
color: _grayText.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'暂无划转记录',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: records.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildRecordCard(records[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRecordCard(TransferRecord record) {
|
||||
final isIn = record.direction == 'IN';
|
||||
final statusColor = _getStatusColor(record.status);
|
||||
final statusText = _getStatusText(record.status);
|
||||
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题行
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: (isIn ? _green : _orange).withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
isIn ? Icons.arrow_downward : Icons.arrow_upward,
|
||||
size: 18,
|
||||
color: isIn ? _green : _orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isIn ? '划入交易账户' : '划出到挖矿账户',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _darkText,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
dateFormat.format(record.createdAt),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 12),
|
||||
// 数量
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'划转数量',
|
||||
style: TextStyle(fontSize: 13, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
'${isIn ? '+' : '-'}${formatAmount(record.amount)} 积分股',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isIn ? _green : _orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 单号
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'划转单号',
|
||||
style: TextStyle(fontSize: 12, color: _grayText),
|
||||
),
|
||||
Text(
|
||||
record.transferNo,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _grayText,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 错误信息
|
||||
if (record.status == 'FAILED' && record.errorMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _red.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 14, color: _red),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
record.errorMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: _red,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getStatusColor(String status) {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return _green;
|
||||
case 'PENDING':
|
||||
return _orange;
|
||||
case 'FAILED':
|
||||
return _red;
|
||||
default:
|
||||
return _grayText;
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText(String status) {
|
||||
switch (status) {
|
||||
case 'COMPLETED':
|
||||
return '已完成';
|
||||
case 'PENDING':
|
||||
return '处理中';
|
||||
case 'FAILED':
|
||||
return '失败';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue