| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import '../../../core/l10n/app_localizations.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/utils/number_format.dart';
- import '../../../providers/asset_provider.dart';
- import '../../../providers/futures_provider.dart' show FuturesPosition, OrderSide;
- import '../../../providers/market_provider.dart' show marketProvider;
- import '../../widgets/common/app_refresh_indicator.dart';
- import '../futures/futures_screen.dart' show SharePositionSheet;
- /// marginMode → l10n 标签
- String _marginModeLabel(String mode, AppLocalizations l10n) {
- switch (mode) {
- case '分仓': return l10n.splitMargin;
- default: return l10n.crossMargin;
- }
- }
- /// marginMode → 标签颜色
- Color _marginModeColor(String mode) {
- switch (mode) {
- case '分仓':
- case '逐仓': return AppColors.rankPurple;
- default: return AppColors.tagBlue;
- }
- }
- /// 合约 Tab
- class AssetFuturesTab extends ConsumerWidget {
- const AssetFuturesTab({super.key, required this.state, required this.notifier});
- final AssetState state;
- final AssetNotifier notifier;
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- final obscure = state.obscureBalance;
- final swapBalance = state.walletBalanceExcludeEG('SWAP').toDouble();
- final display = obscure ? '******' : formatPrice(swapBalance, decimalPlaces: 2);
- return AppRefreshIndicator(
- onRefresh: notifier.silentRefresh,
- child: ListView(
- children: [
- // 合约账户余额
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Text(l10n.futuresAccount, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
- const SizedBox(width: 6),
- GestureDetector(
- onTap: notifier.toggleObscure,
- child: Icon(
- obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined,
- size: 16, color: cs.onSurface.withAlpha(153),
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(display, style: TextStyle(color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5)),
- const SizedBox(width: 6),
- Padding(
- padding: const EdgeInsets.only(bottom: 5),
- child: Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
- ),
- ],
- ),
- const SizedBox(height: 12),
- // 钱包余额
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(l10n.walletBalance, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
- Text(
- obscure ? '******' : formatPrice(state.accountBalance('SWAP').toDouble(), decimalPlaces: 2),
- style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500),
- ),
- ],
- ),
- const SizedBox(height: 8),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(l10n.unrealizedPnl, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
- Text(
- obscure ? '******' : formatPrice(state.unrealizedPnl('SWAP').toDouble(), decimalPlaces: 2),
- style: TextStyle(
- color: _getPnlColor(context, state.unrealizedPnl('SWAP').toDouble()),
- fontSize: 13,
- fontWeight: FontWeight.w500,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- // 交易 + 划转 按钮
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
- child: Row(
- children: [
- Expanded(
- child: GestureDetector(
- onTap: () {
- final symbols = ref.read(marketProvider).displaySymbols;
- final symbol = symbols.isNotEmpty ? symbols.first : 'BTCUSDT';
- context.go('/futures/$symbol');
- },
- child: Container(
- height: 44,
- decoration: BoxDecoration(
- color: AppColors.brand,
- borderRadius: BorderRadius.circular(22),
- ),
- child: Center(
- child: Text(l10n.futures, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600)),
- ),
- ),
- ),
- ),
- const SizedBox(width: 12),
- Expanded(
- child: GestureDetector(
- onTap: () => context.push('/asset/transfer?from=SWAP&to=SPOT'),
- child: Container(
- height: 44,
- decoration: BoxDecoration(
- color: AppColors.brand,
- borderRadius: BorderRadius.circular(22),
- ),
- child: Center(
- child: Text(l10n.transfer, style: const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.w600)),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 24),
- // 持仓标题
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
- child: Text(l10n.positions, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)),
- ),
- if (state.positions.isEmpty)
- Padding(
- padding: const EdgeInsets.symmetric(vertical: 40),
- child: Center(
- child: Text(l10n.noPositions,
- style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
- ),
- )
- else
- for (final pos in state.positions)
- _AssetPositionCard(position: pos),
- const SizedBox(height: 32),
- ],
- ),
- );
- }
- Color _getPnlColor(BuildContext context, double pnl) {
- if (pnl > 0) return AppColors.rise;
- if (pnl < 0) return AppColors.fall;
- return Theme.of(context).colorScheme.onSurface;
- }
- }
- /// 资产合约 tab 的持仓卡片,样式与合约页面 _PositionCard 一致
- class _AssetPositionCard extends StatelessWidget {
- const _AssetPositionCard({required this.position});
- final FuturesPosition position;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- final coinName = position.symbol.replaceAll('/', '').replaceAll('USDT', '');
- final isLong = position.side == OrderSide.long;
- final pnlColor = position.unrealizedPnl >= 0 ? AppColors.rise : AppColors.fall;
- final sideColor = isLong ? AppColors.rise : AppColors.fall;
- final symbol = '${coinName}USDT';
- return GestureDetector(
- onTap: () => context.go('/futures/$symbol'),
- child: Container(
- decoration: BoxDecoration(
- border: Border(bottom: BorderSide(color: cs.outline.withAlpha(50))),
- ),
- padding: const EdgeInsets.fromLTRB(12, 10, 12, 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── 标题行 ──────────────────────────────────────────
- Row(
- children: [
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
- decoration: BoxDecoration(
- color: sideColor,
- borderRadius: BorderRadius.circular(3),
- ),
- child: Text(
- isLong ? l10n.openLong : l10n.openShort,
- style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
- ),
- ),
- const SizedBox(width: 4),
- Expanded(
- child: Row(
- children: [
- Flexible(
- child: Text(
- '${coinName}USDT',
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w700),
- ),
- ),
- const SizedBox(width: 4),
- _SmallTag(text: l10n.perpetual),
- const SizedBox(width: 3),
- _SmallTag(
- text: _marginModeLabel(position.marginMode, l10n),
- color: _marginModeColor(position.marginMode),
- ),
- const SizedBox(width: 3),
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
- decoration: BoxDecoration(
- color: AppColors.leverageGoldBg,
- borderRadius: BorderRadius.circular(3),
- ),
- child: Text(
- '${position.leverage.toInt()}X',
- style: const TextStyle(color: AppColors.leverageGold, fontSize: 10, fontWeight: FontWeight.w700),
- ),
- ),
- ],
- ),
- ),
- const SizedBox(width: 8),
- GestureDetector(
- onTap: () => _sharePosition(context),
- child: Icon(Icons.share_outlined, size: 16, color: cs.onSurface.withAlpha(153)),
- ),
- ],
- ),
- const SizedBox(height: 8),
- // ── 未实现盈亏 + 收益率 ──────────────────────────────
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text('${l10n.unrealizedPnl} (USDT)',
- style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)),
- Text(
- formatAmount(position.unrealizedPnl),
- style: TextStyle(color: pnlColor, fontSize: 16, fontWeight: FontWeight.w700),
- ),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(l10n.returnRate,
- style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)),
- Text(
- '${formatAmount(position.roe)}%',
- style: TextStyle(color: pnlColor, fontSize: 13, fontWeight: FontWeight.w600),
- ),
- ],
- ),
- ],
- ),
- const SizedBox(height: 6),
- // ── 数据行 1 ────────────────────────────────────────
- Row(
- children: [
- _DataCol(
- label: '${l10n.positionSize}($coinName)',
- value: formatQuantity(position.size),
- align: CrossAxisAlignment.start,
- ),
- _DataCol(
- label: '${l10n.marginLabel}(USDT)',
- value: formatAmount(position.margin),
- align: CrossAxisAlignment.center,
- ),
- _DataCol(
- label: l10n.marginRatioLabel,
- value: '${formatAmount(position.marginRatio)}%',
- align: CrossAxisAlignment.end,
- ),
- ],
- ),
- const SizedBox(height: 4),
- // ── 数据行 2 ────────────────────────────────────────
- Row(
- children: [
- _DataCol(
- label: '${l10n.openAvgPrice}(USDT)',
- value: formatPrice(position.entryPrice),
- align: CrossAxisAlignment.start,
- ),
- _DataCol(
- label: '${l10n.latestLabel}(USDT)',
- value: formatPrice(position.markPrice),
- align: CrossAxisAlignment.center,
- ),
- _DataCol(
- label: '${l10n.liqPrice}(USDT)',
- value: position.liquidationPrice > 0 ? formatPrice(position.liquidationPrice) : '--',
- valueColor: position.liquidationPrice > 0 ? AppColors.fall : cs.onSurface.withAlpha(153),
- align: CrossAxisAlignment.end,
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- void _sharePosition(BuildContext context) {
- showModalBottomSheet<void>(
- context: context,
- useRootNavigator: true,
- backgroundColor: Colors.transparent,
- isScrollControlled: true,
- builder: (_) => SharePositionSheet(position: position),
- );
- }
- }
- class _SmallTag extends StatelessWidget {
- const _SmallTag({required this.text, this.color});
- final String text;
- final Color? color;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final c = color;
- if (c != null) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
- decoration: BoxDecoration(
- color: c.withAlpha(isDark ? 45 : 25),
- borderRadius: BorderRadius.circular(3),
- ),
- child: Text(text, style: TextStyle(color: c, fontSize: 9, fontWeight: FontWeight.w500)),
- );
- }
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(3),
- border: Border.all(color: cs.outline.withAlpha(100)),
- ),
- child: Text(text, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)),
- );
- }
- }
- class _DataCol extends StatelessWidget {
- const _DataCol({
- required this.label,
- required this.value,
- this.valueColor,
- this.align = CrossAxisAlignment.start,
- });
- final String label;
- final String value;
- final Color? valueColor;
- final CrossAxisAlignment align;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final textAlign = align == CrossAxisAlignment.end
- ? TextAlign.right
- : align == CrossAxisAlignment.center
- ? TextAlign.center
- : TextAlign.left;
- return Expanded(
- child: Column(
- crossAxisAlignment: align,
- children: [
- Text(label,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- textAlign: textAlign,
- style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 9)),
- Text(value,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- textAlign: textAlign,
- style: TextStyle(
- color: valueColor ?? cs.onSurface,
- fontSize: 12,
- fontWeight: FontWeight.w500)),
- ],
- ),
- );
- }
- }
|