| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439 |
- import 'package:flutter/material.dart';
- import 'package:flutter/services.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 '../../../core/utils/top_toast.dart';
- import '../../../providers/futures_provider.dart';
- /// 委托详情页
- class OrderDetailScreen extends ConsumerStatefulWidget {
- const OrderDetailScreen({super.key, required this.order});
- final FuturesOrder order;
- @override
- ConsumerState<OrderDetailScreen> createState() => _OrderDetailScreenState();
- }
- class _OrderDetailScreenState extends ConsumerState<OrderDetailScreen> {
- bool _cancelling = false;
- FuturesOrder get order => widget.order;
- String _baseCoin(String sym) {
- if (sym.contains('/')) return sym.split('/').first;
- return sym.toUpperCase().replaceFirst(RegExp(r'USDT$'), '');
- }
- String _formatDateTime(DateTime? dt) {
- if (dt == null) return '--';
- final y = dt.year;
- final mo = dt.month.toString().padLeft(2, '0');
- final d = dt.day.toString().padLeft(2, '0');
- final h = dt.hour.toString().padLeft(2, '0');
- final mi = dt.minute.toString().padLeft(2, '0');
- final s = dt.second.toString().padLeft(2, '0');
- return '$y-$mo-$d $h:$mi:$s';
- }
- Future<void> _cancelOrder(BuildContext context) async {
- setState(() => _cancelling = true);
- final activeSymbol = ref.read(futuresActiveSymbolProvider);
- final notifier = ref.read(futuresProvider(activeSymbol).notifier);
- final err = await notifier.cancelOrder(order);
- if (!context.mounted) return;
- setState(() => _cancelling = false);
- final l10n = AppLocalizations.of(context)!;
- if (err == null) {
- showTopToast(context, message: l10n.cancelOrderSuccess, backgroundColor: AppColors.rise);
- context.pop();
- } else {
- showTopToast(context, message: resolveProviderError(err, l10n) ?? err, backgroundColor: AppColors.fall);
- }
- }
- /// 去除多余的尾零,整数不带小数点;为空/未传才显示 '--',由调用方处理
- String _rawNum(double v) {
- if (v == v.truncateToDouble()) return v.toInt().toString();
- return v.toString();
- }
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- final isOpen = order.isOpenOrder;
- final isLong = order.side == OrderSide.long;
- final actionColor = isOpen
- ? (isLong ? AppColors.rise : AppColors.fall)
- : (isLong ? AppColors.fall : AppColors.rise);
- final coinSymbol = _baseCoin(order.symbol);
- final hasTrigger = order.triggerPrice > 0;
- final hasProfit = order.profitPrice != null && order.profitPrice! > 0;
- final hasLoss = order.lossPrice != null && order.lossPrice! > 0;
- final hasTpsl = hasProfit || hasLoss;
- final canCancel = order.isPending;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final scaffoldBg = isDark ? AppColors.darkBg : AppColors.lightBgSecondary;
- final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg;
- return Scaffold(
- backgroundColor: scaffoldBg,
- appBar: AppBar(
- elevation: 0,
- centerTitle: true,
- backgroundColor: cardBg,
- title: Text(
- l10n.orderDetail,
- style: TextStyle(
- color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700),
- ),
- ),
- body: Stack(
- children: [
- SingleChildScrollView(
- padding: EdgeInsets.only(bottom: canCancel ? 90 : 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── 标的行 ──────────────────────────────────────
- Container(
- color: cardBg,
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
- child: Row(
- children: [
- // 开多/开空/平多/平空 chip
- Container(
- padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
- decoration: BoxDecoration(
- color: actionColor,
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(
- isOpen
- ? (isLong ? l10n.openLong : l10n.openShort)
- : (isLong ? l10n.closeLong : l10n.closeShort),
- style: const TextStyle(
- color: Colors.white,
- fontSize: 11,
- fontWeight: FontWeight.w700),
- ),
- ),
- const SizedBox(width: 6),
- Expanded(
- child: Row(
- children: [
- Flexible(
- child: Text(
- order.symbol,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w700),
- ),
- ),
- const SizedBox(width: 6),
- _SmTag(text: order.type == OrderType.market
- ? l10n.marketHint
- : order.type == OrderType.limit
- ? l10n.limitLabel
- : l10n.planOrderLabel),
- const SizedBox(width: 4),
- _SmTag(
- text: switch (order.marginMode) {
- '分仓' || '逐仓' => l10n.splitMargin,
- _ => l10n.crossMargin,
- },
- color: switch (order.marginMode) {
- '分仓' || '逐仓' => AppColors.rankPurple,
- _ => AppColors.tagBlue,
- },
- ),
- const SizedBox(width: 4),
- _LevTag(leverage: order.leverage.toInt()),
- ],
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- // ── 委托基本信息 ─────────────────────────────────
- _InfoCard(rows: [
- _InfoRowData('${l10n.orderPriceLabel}(USDT)', order.priceDisplay),
- _InfoRowData('${l10n.entrustAmount}($coinSymbol)', _rawNum(order.size)),
- _InfoRowData('${l10n.filledVolume}($coinSymbol)', _rawNum(order.filledSize)),
- _InfoRowData(
- l10n.avgTradePrice,
- order.tradedPrice > 0 ? _rawNum(order.tradedPrice) : '--',
- ),
- if (hasTrigger)
- _InfoRowData(
- l10n.triggerCondition,
- '${l10n.markLabel}≥${_rawNum(order.triggerPrice)}',
- valueColor: cs.onSurface.withAlpha(100),
- isLast: true,
- ),
- ]),
- const SizedBox(height: 8),
- // ── 止盈止损 ─────────────────────────────────────
- if (hasTpsl) ...[
- _InfoCard(
- sectionTitle: l10n.stopProfitLoss,
- rows: [
- if (hasProfit)
- _InfoRowData(
- '${l10n.takeProfitPrice}(USDT)',
- _rawNum(order.profitPrice!),
- valueColor: AppColors.rise,
- isLast: !hasLoss,
- ),
- if (hasLoss)
- _InfoRowData(
- '${l10n.stopLossPrice}(USDT)',
- _rawNum(order.lossPrice!),
- valueColor: AppColors.fall,
- isLast: true,
- ),
- ],
- ),
- const SizedBox(height: 8),
- ],
- // ── 时间 + 订单 ID ───────────────────────────────
- _InfoCard(rows: [
- _InfoRowData(l10n.createTime, _formatDateTime(order.createTime)),
- _InfoRowData(
- 'ID',
- order.id,
- isLast: true,
- copyable: true,
- ),
- ]),
- const SizedBox(height: 8),
- // ── 交易明细(已成交时显示)──────────────────────────
- if (order.filledSize > 0) ...[
- _InfoCard(
- sectionTitle: l10n.tradeDetailLabel,
- rows: [
- _InfoRowData(l10n.tradePrice, order.tradedPrice > 0 ? _rawNum(order.tradedPrice) : '--'),
- _InfoRowData('${l10n.filledVolume}($coinSymbol)', _rawNum(order.filledSize)),
- if (order.fee > 0)
- _InfoRowData('${l10n.fee}(USDT)', formatAmount(order.fee)),
- _InfoRowData(l10n.tradeTime, _formatDateTime(order.dealTime ?? order.createTime), isLast: true),
- ],
- ),
- const SizedBox(height: 8),
- ],
- ],
- ),
- ),
- // ── 撤单按钮(仅委托中状态)─────────────────────────────
- if (canCancel)
- Positioned(
- left: 0,
- right: 0,
- bottom: 0,
- child: Container(
- color: Theme.of(context).brightness == Brightness.dark
- ? AppColors.darkBgSecondary
- : AppColors.lightBg,
- padding: const EdgeInsets.fromLTRB(16, 12, 16, 28),
- child: SizedBox(
- height: 50,
- child: ElevatedButton(
- onPressed: _cancelling ? null : () => _cancelOrder(context),
- style: ElevatedButton.styleFrom(
- backgroundColor: AppColors.brand,
- elevation: 0,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(25)),
- ),
- child: _cancelling
- ? const SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2, color: Colors.white),
- )
- : Text(
- l10n.cancelOrder,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 16,
- fontWeight: FontWeight.w700,
- letterSpacing: 2),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
- // ── 共用 widgets ───────────────────────────────────────────────
- class _SmTag extends StatelessWidget {
- const _SmTag({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: 6, vertical: 2),
- decoration: BoxDecoration(
- color: c.withAlpha(isDark ? 45 : 25),
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(text,
- style: TextStyle(
- color: c, fontSize: 11, fontWeight: FontWeight.w600)),
- );
- }
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: cs.onSurface.withAlpha(12),
- border: Border.all(color: cs.outline.withAlpha(80)),
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(text,
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 11,
- fontWeight: FontWeight.w600)),
- );
- }
- }
- class _LevTag extends StatelessWidget {
- const _LevTag({required this.leverage});
- final int leverage;
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: AppColors.leverageGoldBg,
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text('${leverage}X',
- style: const TextStyle(
- color: AppColors.leverageGold,
- fontSize: 11,
- fontWeight: FontWeight.w700)),
- );
- }
- }
- class _InfoRowData {
- const _InfoRowData(this.label, this.value,
- {this.valueColor, this.isLast = false, this.copyable = false});
- final String label;
- final String value;
- final Color? valueColor;
- final bool isLast;
- final bool copyable;
- }
- class _InfoCard extends StatelessWidget {
- const _InfoCard({required this.rows, this.sectionTitle});
- final List<_InfoRowData> rows;
- final String? sectionTitle;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- return Container(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (sectionTitle != null)
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
- child: Text(sectionTitle!,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- ),
- ...rows.map((r) {
- return Column(
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16, vertical: 13),
- child: Row(
- children: [
- Expanded(
- child: Text(r.label,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 13)),
- ),
- const SizedBox(width: 8),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(r.value,
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.end,
- style: TextStyle(
- color: r.valueColor ?? cs.onSurface,
- fontSize: 13,
- fontWeight: FontWeight.w500)),
- if (r.copyable) ...[
- const SizedBox(width: 6),
- GestureDetector(
- onTap: () {
- Clipboard.setData(
- ClipboardData(text: r.value));
- showTopToast(context, message: AppLocalizations.of(context)!.copied, backgroundColor: AppColors.rise);
- },
- child: Icon(Icons.copy_outlined,
- size: 15,
- color: cs.onSurface.withAlpha(100)),
- ),
- ],
- ],
- ),
- ],
- ),
- ),
- if (!r.isLast)
- Divider(
- height: 1,
- thickness: 0.5,
- indent: 16,
- endIndent: 16,
- color: cs.outline.withAlpha(60)),
- ],
- );
- }).toList(),
- ],
- ),
- );
- }
- }
|