| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../../../core/l10n/app_localizations.dart';
- import '../../../core/network/dio_client.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/utils/number_format.dart';
- import '../../../data/services/spot_service.dart';
- import '../../widgets/common/app_tab_bar.dart';
- import '../../../providers/spot_provider.dart';
- import '../../../providers/spot_symbol_cache_provider.dart';
- /// 现货历史记录:历史委托 / 历史成交
- class SpotHistoryScreen extends ConsumerStatefulWidget {
- const SpotHistoryScreen({super.key, required this.symbol});
- final String symbol;
- @override
- ConsumerState<SpotHistoryScreen> createState() => _SpotHistoryScreenState();
- }
- class _SpotHistoryScreenState extends ConsumerState<SpotHistoryScreen>
- with SingleTickerProviderStateMixin {
- late TabController _tabCtrl;
- late PageController _pageCtrl;
- static const int _pageSize = 20;
- static const int _maxSkipEmptyPages = 12;
- _KindFilter _kind = _KindFilter.all;
- _StatusFilter _status = _StatusFilter.all;
- _TimeFilter _time = _TimeFilter.all;
- /// true:仅当前路由交易对;false:全部交易对
- bool _useCurrentSymbolOnly = false;
- /// 是否已在交易对筛选里点选过;未点选时 pill 展示原型「交易对」
- bool _pairFilterTouched = false;
- final _ordersScroll = ScrollController();
- final List<SpotOrder> _orders = [];
- int _nextOrdersPage = 1;
- bool _ordersHasMore = true;
- bool _ordersLoading = false;
- int _ordersRawLoaded = 0;
- final _tradesScroll = ScrollController();
- final List<SpotOrder> _trades = [];
- int _nextTradesPage = 1;
- bool _tradesHasMore = true;
- bool _tradesLoading = false;
- int _tradesRawLoaded = 0;
- @override
- void initState() {
- super.initState();
- _tabCtrl = TabController(length: 2, vsync: this);
- _pageCtrl = PageController();
- // Tab 点击 → 驱动 PageView 动画(与合约历史页一致)
- _tabCtrl.addListener(() {
- if (!mounted) return;
- if (!_tabCtrl.indexIsChanging) return;
- _pageCtrl.animateToPage(
- _tabCtrl.index,
- duration: const Duration(milliseconds: 280),
- curve: Curves.easeOut,
- );
- });
- // 横向滑动 PageView → 指示器 offset 平滑插值
- _pageCtrl.addListener(() {
- if (!mounted) return;
- if (!_pageCtrl.hasClients) return;
- if (_tabCtrl.indexIsChanging) return;
- final page = _pageCtrl.page!;
- final offset = page - _tabCtrl.index;
- if (offset.abs() <= 1.0) {
- _tabCtrl.offset = offset.clamp(-1.0, 1.0);
- }
- });
- _ordersScroll.addListener(_onOrdersScroll);
- _tradesScroll.addListener(_onTradesScroll);
- _refreshOrders();
- }
- void _resetFiltersToDefault() {
- _kind = _KindFilter.all;
- _status = _StatusFilter.all;
- _time = _TimeFilter.all;
- _useCurrentSymbolOnly = false;
- _pairFilterTouched = false;
- }
- /// PageView 切换完成:重置筛选并刷新当前页数据
- void _onPageCommitted(int index) {
- if (!mounted) return;
- _resetFiltersToDefault();
- if (index == 0) {
- if (_ordersScroll.hasClients) {
- _ordersScroll.jumpTo(0);
- }
- _refreshOrders();
- } else {
- if (_tradesScroll.hasClients) {
- _tradesScroll.jumpTo(0);
- }
- _refreshTrades();
- }
- }
- void _onOrdersScroll() {
- if (_ordersScroll.position.pixels >=
- _ordersScroll.position.maxScrollExtent - 200) {
- _loadMoreOrders();
- }
- }
- void _onTradesScroll() {
- if (_tradesScroll.position.pixels >=
- _tradesScroll.position.maxScrollExtent - 200) {
- _loadMoreTrades();
- }
- }
- @override
- void dispose() {
- _tabCtrl.dispose();
- _pageCtrl.dispose();
- _ordersScroll.dispose();
- _tradesScroll.dispose();
- super.dispose();
- }
- String get _apiSymbol => widget.symbol
- .replaceAll('/', '')
- .replaceAll('-', '')
- .toUpperCase();
- /// 交易对 pill:未选过展示「交易对」;选过后展示「全部交易对」或具体币对
- String _pairChipDisplay(AppLocalizations l10n) {
- if (!_pairFilterTouched) return l10n.spotHistoryFilterSymbol;
- return _useCurrentSymbolOnly ? _apiSymbol : l10n.spotFilterSymbolAllPairs;
- }
- /// 委托类型:全部时展示「全部委托」,否则展示市价/限价(与原型一致)
- String _kindChipDisplay(AppLocalizations l10n) {
- switch (_kind) {
- case _KindFilter.all:
- return l10n.spotFilterEntrustAll;
- case _KindFilter.market:
- return l10n.spotFilterKindMarket;
- case _KindFilter.limit:
- return l10n.spotFilterKindLimit;
- }
- }
- String _statusChipDisplay(AppLocalizations l10n) {
- switch (_status) {
- case _StatusFilter.all:
- return l10n.spotHistoryFilterStatus;
- case _StatusFilter.completed:
- return l10n.spotOrderStatusCompleted;
- case _StatusFilter.partial:
- return l10n.spotOrderStatusPartialFilled;
- case _StatusFilter.cancelled:
- return l10n.spotOrderStatusCancelled;
- }
- }
- String _timeChipDisplay(AppLocalizations l10n) {
- switch (_time) {
- case _TimeFilter.all:
- return l10n.spotHistoryFilterTime;
- case _TimeFilter.d7:
- return l10n.spotFilterTime7d;
- case _TimeFilter.d30:
- return l10n.spotFilterTime30d;
- }
- }
- int? get _ctimeBeginMs {
- final now = DateTime.now();
- switch (_time) {
- case _TimeFilter.all:
- return null;
- case _TimeFilter.d7:
- return now.subtract(const Duration(days: 7)).millisecondsSinceEpoch;
- case _TimeFilter.d30:
- return now.subtract(const Duration(days: 30)).millisecondsSinceEpoch;
- }
- }
- /// 历史委托 Tab:传 type 给服务端(并做客户端二次过滤)
- int? _apiTypeOrders() {
- switch (_kind) {
- case _KindFilter.all:
- return null;
- case _KindFilter.limit:
- return 1;
- case _KindFilter.market:
- return 2;
- }
- }
- /// 历史成交 Tab:不按委托类型筛(与原型一致),仅客户端可按状态/时间筛
- int? _apiTypeTrades() => null;
- int? _apiStatus() {
- switch (_status) {
- case _StatusFilter.all:
- case _StatusFilter.partial:
- return null;
- case _StatusFilter.completed:
- return 2;
- case _StatusFilter.cancelled:
- return 4;
- }
- }
- bool _matchesFilters(SpotOrder o, {required bool tradesTab}) {
- if (_useCurrentSymbolOnly) {
- final os = o.symbol.replaceAll('/', '').toUpperCase();
- if (os != _apiSymbol) return false;
- }
- if (!tradesTab) {
- switch (_kind) {
- case _KindFilter.all:
- break;
- case _KindFilter.limit:
- if (o.typeCode != 1) return false;
- case _KindFilter.market:
- if (o.typeCode != 2) return false;
- }
- }
- switch (_status) {
- case _StatusFilter.all:
- break;
- case _StatusFilter.completed:
- if (o.statusCode != 2) return false;
- case _StatusFilter.partial:
- final isPartial = o.statusCode == 3 ||
- ((o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6) &&
- o.tradedAmount > 0 &&
- o.tradedAmount < o.amount);
- if (!isPartial) return false;
- case _StatusFilter.cancelled:
- final partial = o.tradedAmount > 0 && o.tradedAmount < o.amount;
- final isCancelState =
- o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6;
- if (!isCancelState || partial) return false;
- }
- final begin = _ctimeBeginMs;
- if (begin != null) {
- final t = o.createTime;
- if (t != null && t.millisecondsSinceEpoch < begin) return false;
- }
- return true;
- }
- Future<void> _refreshOrders() async {
- setState(() {
- _ordersLoading = true;
- _nextOrdersPage = 1;
- _ordersHasMore = true;
- _orders.clear();
- _ordersRawLoaded = 0;
- });
- await _fetchOrdersPage();
- }
- Future<void> _loadMoreOrders() async {
- if (_ordersLoading || !_ordersHasMore) return;
- setState(() => _ordersLoading = true);
- await _fetchOrdersPage();
- }
- Future<void> _fetchOrdersPage() async {
- try {
- final svc = SpotService(ref.read(dioClientProvider));
- int skipped = 0;
- while (skipped <= _maxSkipEmptyPages && _ordersHasMore) {
- final data = await svc.getHistoryOrders(
- page: _nextOrdersPage,
- size: _pageSize,
- symbol: _useCurrentSymbolOnly ? _apiSymbol : null,
- type: _apiTypeOrders(),
- status: _apiStatus(),
- ctimeBegin: _ctimeBeginMs,
- );
- final list = _extractRecordMaps(data);
- final total = _parsePageTotal(data);
- if (list.isEmpty) {
- _ordersHasMore = false;
- break;
- }
- _ordersRawLoaded += list.length;
- final filtered = list
- .map(SpotOrder.fromJson)
- .where((o) => _matchesFilters(o, tradesTab: false))
- .toList();
- _orders.addAll(filtered);
- _nextOrdersPage++;
- _ordersHasMore = total > _ordersRawLoaded ||
- (total == 0 && list.length >= _pageSize);
- if (filtered.isNotEmpty || !_ordersHasMore) break;
- skipped++;
- }
- } catch (_) {
- _ordersHasMore = false;
- } finally {
- if (mounted) setState(() => _ordersLoading = false);
- }
- }
- Future<void> _refreshTrades() async {
- setState(() {
- _tradesLoading = true;
- _nextTradesPage = 1;
- _tradesHasMore = true;
- _trades.clear();
- _tradesRawLoaded = 0;
- });
- await _fetchTradesPage();
- }
- Future<void> _loadMoreTrades() async {
- if (_tradesLoading || !_tradesHasMore) return;
- setState(() => _tradesLoading = true);
- await _fetchTradesPage();
- }
- Future<void> _fetchTradesPage() async {
- try {
- final svc = SpotService(ref.read(dioClientProvider));
- int skipped = 0;
- while (skipped <= _maxSkipEmptyPages && _tradesHasMore) {
- final data = await svc.getTrades(
- page: _nextTradesPage,
- size: _pageSize,
- symbol: _useCurrentSymbolOnly ? _apiSymbol : null,
- type: _apiTypeTrades(),
- status: _apiStatus(),
- ctimeBegin: _ctimeBeginMs,
- );
- final list = _extractRecordMaps(data);
- final total = _parsePageTotal(data);
- if (list.isEmpty) {
- _tradesHasMore = false;
- break;
- }
- _tradesRawLoaded += list.length;
- final filtered = list
- .map(SpotOrder.fromJson)
- .where((o) => _matchesFilters(o, tradesTab: true))
- .toList();
- _trades.addAll(filtered);
- _nextTradesPage++;
- _tradesHasMore = total > _tradesRawLoaded ||
- (total == 0 && list.length >= _pageSize);
- if (filtered.isNotEmpty || !_tradesHasMore) break;
- skipped++;
- }
- } catch (_) {
- _tradesHasMore = false;
- } finally {
- if (mounted) setState(() => _tradesLoading = false);
- }
- }
- Future<void> _applyFilters() async {
- await Future.wait([_refreshOrders(), _refreshTrades()]);
- }
- @override
- Widget build(BuildContext context) {
- final l10n = AppLocalizations.of(context)!;
- return Scaffold(
- appBar: AppBar(
- title: Text(l10n.spotHistoryRecordsTitle),
- bottom: AppTabBar(
- controller: _tabCtrl,
- tabs: [
- Tab(text: l10n.spotHistoryTitle),
- Tab(text: l10n.spotHistoryTradesTab),
- ],
- ),
- ),
- body: PageView(
- controller: _pageCtrl,
- physics: const BouncingScrollPhysics(
- parent: AlwaysScrollableScrollPhysics(),
- ),
- onPageChanged: (index) {
- if (!mounted) return;
- if (!_tabCtrl.indexIsChanging) {
- _tabCtrl.index = index;
- }
- _onPageCommitted(index);
- },
- children: [
- _OrdersTabBody(
- scrollController: _ordersScroll,
- orders: _orders,
- loading: _ordersLoading,
- hasMore: _ordersHasMore,
- onRefresh: _refreshOrders,
- tradesMode: false,
- pairText: _pairChipDisplay(l10n),
- kindText: _kindChipDisplay(l10n),
- statusText: _statusChipDisplay(l10n),
- timeText: _timeChipDisplay(l10n),
- onPickSymbol: () => _pickSymbol(l10n),
- onPickKind: () => _pickKind(l10n),
- onPickStatus: () => _pickStatus(l10n),
- onPickTime: () => _pickTime(l10n),
- ),
- _OrdersTabBody(
- scrollController: _tradesScroll,
- orders: _trades,
- loading: _tradesLoading,
- hasMore: _tradesHasMore,
- onRefresh: _refreshTrades,
- tradesMode: true,
- pairText: _pairChipDisplay(l10n),
- kindText: null,
- statusText: _statusChipDisplay(l10n),
- timeText: _timeChipDisplay(l10n),
- onPickSymbol: () => _pickSymbol(l10n),
- onPickKind: null,
- onPickStatus: () => _pickStatus(l10n),
- onPickTime: () => _pickTime(l10n),
- ),
- ],
- ),
- );
- }
- Future<void> _pickSymbol(AppLocalizations l10n) async {
- final cs = Theme.of(context).colorScheme;
- final v = await showModalBottomSheet<bool>(
- context: context,
- useRootNavigator: true,
- backgroundColor: cs.surface,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
- ),
- builder: (ctx) => _SimpleSheet<bool>(
- selected: _useCurrentSymbolOnly,
- options: [
- (false, l10n.spotFilterSymbolAllPairs),
- (true, _apiSymbol),
- ],
- ),
- );
- if (v != null && mounted) {
- setState(() {
- _pairFilterTouched = true;
- _useCurrentSymbolOnly = v;
- });
- await _applyFilters();
- }
- }
- Future<void> _pickKind(AppLocalizations l10n) async {
- final cs = Theme.of(context).colorScheme;
- final v = await showModalBottomSheet<_KindFilter>(
- context: context,
- useRootNavigator: true,
- backgroundColor: cs.surface,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
- ),
- builder: (ctx) => _SimpleSheet<_KindFilter>(
- selected: _kind,
- options: [
- (_KindFilter.all, l10n.spotFilterKindAll),
- (_KindFilter.market, l10n.spotFilterKindMarket),
- (_KindFilter.limit, l10n.spotFilterKindLimit),
- ],
- ),
- );
- if (v != null && mounted) {
- setState(() => _kind = v);
- await _applyFilters();
- }
- }
- Future<void> _pickStatus(AppLocalizations l10n) async {
- final cs = Theme.of(context).colorScheme;
- final v = await showModalBottomSheet<_StatusFilter>(
- context: context,
- useRootNavigator: true,
- backgroundColor: cs.surface,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
- ),
- builder: (ctx) => _SimpleSheet<_StatusFilter>(
- selected: _status,
- options: [
- (_StatusFilter.all, l10n.spotFilterStatusAll),
- (_StatusFilter.completed, l10n.spotOrderStatusCompleted),
- (_StatusFilter.partial, l10n.spotOrderStatusPartialFilled),
- (_StatusFilter.cancelled, l10n.spotOrderStatusCancelled),
- ],
- ),
- );
- if (v != null && mounted) {
- setState(() => _status = v);
- await _applyFilters();
- }
- }
- Future<void> _pickTime(AppLocalizations l10n) async {
- final cs = Theme.of(context).colorScheme;
- final v = await showModalBottomSheet<_TimeFilter>(
- context: context,
- useRootNavigator: true,
- backgroundColor: cs.surface,
- shape: const RoundedRectangleBorder(
- borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
- ),
- builder: (ctx) => _SimpleSheet<_TimeFilter>(
- selected: _time,
- options: [
- (_TimeFilter.all, l10n.spotFilterTimeAll),
- (_TimeFilter.d7, l10n.spotFilterTime7d),
- (_TimeFilter.d30, l10n.spotFilterTime30d),
- ],
- ),
- );
- if (v != null && mounted) {
- setState(() => _time = v);
- await _applyFilters();
- }
- }
- }
- List<Map<String, dynamic>> _extractRecordMaps(Map<String, dynamic> data) {
- final raw = data['records'] ?? data['list'];
- if (raw is! List) return [];
- return raw.whereType<Map<String, dynamic>>().toList();
- }
- int _parsePageTotal(Map<String, dynamic> data) {
- final t = data['total'] ?? data['totalRow'] ?? data['totalCount'];
- if (t is int) return t;
- if (t is num) return t.toInt();
- return 0;
- }
- enum _KindFilter { all, market, limit }
- enum _StatusFilter { all, completed, partial, cancelled }
- enum _TimeFilter { all, d7, d30 }
- /// 选项样式与合约页「仓位模式」底部弹窗一致:圆角描边、选中加粗 + 对勾
- class _SimpleSheet<T> extends StatelessWidget {
- const _SimpleSheet({
- super.key,
- required this.selected,
- required this.options,
- });
- final T selected;
- final List<(T, String)> options;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return SafeArea(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- for (var i = 0; i < options.length; i++) ...[
- if (i > 0) const SizedBox(height: 10),
- GestureDetector(
- onTap: () => Navigator.pop(context, options[i].$1),
- child: Container(
- width: double.infinity,
- padding:
- const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
- decoration: BoxDecoration(
- border: Border.all(
- color: options[i].$1 == selected
- ? cs.onSurface
- : cs.outline,
- width: options[i].$1 == selected ? 1.5 : 1,
- ),
- borderRadius: BorderRadius.circular(10),
- color: Colors.transparent,
- ),
- child: Row(
- children: [
- Expanded(
- child: Text(
- options[i].$2,
- style: TextStyle(
- color: cs.onSurface,
- fontWeight: FontWeight.w600,
- fontSize: 14,
- ),
- ),
- ),
- if (options[i].$1 == selected)
- Icon(Icons.check_circle,
- size: 16, color: cs.onSurface),
- ],
- ),
- ),
- ),
- ],
- ],
- ),
- ),
- ),
- );
- }
- }
- class _OrdersTabBody extends StatelessWidget {
- const _OrdersTabBody({
- required this.scrollController,
- required this.orders,
- required this.loading,
- required this.hasMore,
- required this.onRefresh,
- required this.tradesMode,
- required this.pairText,
- this.kindText,
- required this.statusText,
- required this.timeText,
- required this.onPickSymbol,
- this.onPickKind,
- required this.onPickStatus,
- required this.onPickTime,
- });
- final ScrollController scrollController;
- final List<SpotOrder> orders;
- final bool loading;
- final bool hasMore;
- final Future<void> Function() onRefresh;
- final bool tradesMode;
- final String pairText;
- final String? kindText;
- final String statusText;
- final String timeText;
- final VoidCallback onPickSymbol;
- final VoidCallback? onPickKind;
- final VoidCallback onPickStatus;
- final VoidCallback onPickTime;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- final showKind = !tradesMode && kindText != null && onPickKind != null;
- return Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
- child: Row(
- children: [
- Expanded(
- child: _DropdownFilterPill(
- text: pairText,
- onTap: onPickSymbol,
- ),
- ),
- const SizedBox(width: 8),
- if (showKind) ...[
- Expanded(
- child: _DropdownFilterPill(
- text: kindText!,
- onTap: onPickKind!,
- ),
- ),
- const SizedBox(width: 8),
- ],
- Expanded(
- child: _DropdownFilterPill(
- text: statusText,
- onTap: onPickStatus,
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: _DropdownFilterPill(
- text: timeText,
- onTap: onPickTime,
- ),
- ),
- ],
- ),
- ),
- Expanded(
- child: RefreshIndicator(
- onRefresh: onRefresh,
- child: orders.isEmpty && !loading
- ? ListView(
- physics: const AlwaysScrollableScrollPhysics(),
- children: [
- const SizedBox(height: 120),
- Center(
- child: Text(
- tradesMode ? l10n.noTradeData : l10n.noHistoryOrders,
- style: TextStyle(
- color: cs.onSurface.withAlpha(140),
- fontSize: 14,
- ),
- ),
- ),
- ],
- )
- : ListView.builder(
- controller: scrollController,
- physics: const AlwaysScrollableScrollPhysics(),
- itemCount: orders.length + 1,
- itemBuilder: (_, i) {
- if (i == orders.length) {
- if (loading) {
- return const Padding(
- padding: EdgeInsets.all(16),
- child: Center(child: CircularProgressIndicator()),
- );
- }
- if (!hasMore) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 16),
- child: Center(
- child: Text(
- l10n.noMoreData,
- style: TextStyle(
- color: cs.onSurface.withAlpha(120),
- fontSize: 12,
- ),
- ),
- ),
- );
- }
- return const SizedBox.shrink();
- }
- return _HistoryCard(
- order: orders[i],
- tradesMode: tradesMode,
- );
- },
- ),
- ),
- ),
- ],
- );
- }
- }
- /// 原型:浅灰圆角条,单行文案 + 右侧下拉箭头;默认展示维度名,选中后展示选项文案
- class _DropdownFilterPill extends StatelessWidget {
- const _DropdownFilterPill({
- required this.text,
- required this.onTap,
- });
- final String text;
- final VoidCallback onTap;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final bg = isDark
- ? cs.surfaceContainerHighest.withAlpha(120)
- : const Color(0xFFF2F2F4);
- return Material(
- color: bg,
- borderRadius: BorderRadius.circular(8),
- child: InkWell(
- onTap: onTap,
- borderRadius: BorderRadius.circular(8),
- child: Padding(
- padding: const EdgeInsets.fromLTRB(8, 10, 4, 10),
- child: Row(
- children: [
- Expanded(
- child: Text(
- text,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: 13,
- color: cs.onSurface,
- fontWeight: FontWeight.w500,
- ),
- ),
- ),
- Icon(
- Icons.keyboard_arrow_down,
- size: 20,
- color: cs.onSurface.withAlpha(130),
- ),
- ],
- ),
- ),
- ),
- );
- }
- }
- class _HistoryCard extends ConsumerWidget {
- const _HistoryCard({
- required this.order,
- required this.tradesMode,
- });
- final SpotOrder order;
- final bool tradesMode;
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- final base = _spotBaseAsset(order.symbol);
- final quote = 'USDT';
- final sym = order.symbol.replaceAll('/', '').toUpperCase();
- final precision = ref.watch(
- spotSymbolCacheProvider.select((map) => map[sym]));
- final pricePre = precision?.pricePre ?? 2;
- final volPre = precision?.volumePre ?? 4;
- final statusLabel = _historyStatusLabel(order, l10n);
- final statusColor = _historyStatusColor(cs, order);
- final sideColor =
- order.side == SpotSide.buy ? AppColors.rise : AppColors.fall;
- final typeSide = _orderTypeSideLine(order, l10n);
- return Container(
- padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(color: cs.outline.withAlpha(40)),
- ),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Expanded(
- child: Text(
- order.symbol.replaceAll('/', ''),
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w700,
- ),
- ),
- ),
- Text(
- statusLabel,
- style: TextStyle(
- color: statusColor,
- fontSize: 13,
- fontWeight: FontWeight.w600,
- ),
- ),
- ],
- ),
- const SizedBox(height: 6),
- Text(
- typeSide,
- style: TextStyle(
- color: sideColor,
- fontSize: 13,
- fontWeight: FontWeight.w600,
- ),
- ),
- const SizedBox(height: 10),
- if (tradesMode) ...[
- _kvRow(
- context,
- '${l10n.tradedDealAmount}($base)',
- '${formatAmount(order.tradedAmount, decimals: volPre)} / ${formatAmount(order.amount, decimals: volPre)}',
- ),
- const SizedBox(height: 6),
- _kvRow(
- context,
- '${l10n.spotAvgPrice}($quote)',
- order.avgPrice > 0
- ? formatAmount(order.avgPrice, decimals: pricePre)
- : '--',
- ),
- const SizedBox(height: 6),
- _kvRow(
- context,
- l10n.spotDealTime,
- order.createTime != null
- ? _fmtTime(order.createTime!)
- : '--',
- ),
- ] else ...[
- _kvRow(
- context,
- '${l10n.spotEntrustQuantity}($base)',
- formatAmount(order.amount, decimals: volPre),
- ),
- const SizedBox(height: 6),
- _kvRow(
- context,
- '${l10n.orderPriceLabel}($quote)',
- order.type == SpotOrderType.market && order.price <= 0
- ? l10n.marketPrice
- : formatAmount(order.price, decimals: pricePre),
- ),
- const SizedBox(height: 6),
- _kvRow(
- context,
- l10n.orderTime,
- order.createTime != null
- ? _fmtTime(order.createTime!)
- : '--',
- ),
- ],
- ],
- ),
- );
- }
- }
- Widget _kvRow(BuildContext context, String k, String v) {
- final cs = Theme.of(context).colorScheme;
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- flex: 12,
- child: Text(
- k,
- style: TextStyle(
- color: cs.onSurface.withAlpha(140),
- fontSize: 12,
- ),
- ),
- ),
- Expanded(
- flex: 13,
- child: Text(
- v,
- textAlign: TextAlign.right,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 13,
- fontWeight: FontWeight.w500,
- fontFeatures: const [FontFeature.tabularFigures()],
- ),
- ),
- ),
- ],
- );
- }
- String _fmtTime(DateTime t) {
- final x = t.toLocal();
- String two(int n) => n.toString().padLeft(2, '0');
- return '${x.year}-${two(x.month)}-${two(x.day)} ${two(x.hour)}:${two(x.minute)}:${two(x.second)}';
- }
- String _spotBaseAsset(String symbolUpper) {
- final s = symbolUpper.replaceAll('/', '').toUpperCase();
- const quotes = ['USDT', 'USDC', 'BUSD', 'TUSD'];
- for (final q in quotes) {
- if (s.endsWith(q) && s.length > q.length) {
- return s.substring(0, s.length - q.length);
- }
- }
- return s;
- }
- Color _historyStatusColor(ColorScheme cs, SpotOrder o) {
- if (o.statusCode == 2) return AppColors.rise;
- return cs.onSurface;
- }
- String _historyStatusLabel(SpotOrder o, AppLocalizations l10n) {
- if (o.statusCode == 2) return l10n.spotOrderStatusCompleted;
- if (o.statusCode == 3) return l10n.spotOrderStatusPartialFilled;
- if (o.statusCode == 4 || o.statusCode == 5 || o.statusCode == 6) {
- if (o.tradedAmount > 0 && o.tradedAmount < o.amount) {
- return l10n.spotOrderStatusPartialFilled;
- }
- return l10n.spotOrderStatusCancelled;
- }
- return o.status;
- }
- String _orderTypeSideLine(SpotOrder o, AppLocalizations l10n) {
- final type = switch (o.type) {
- SpotOrderType.market => l10n.marketOrder,
- SpotOrderType.conditionalMarket => l10n.spotOrderTypeMarketConditional,
- _ => l10n.limitOrder,
- };
- final side = o.side == SpotSide.buy ? l10n.buyAction : l10n.sellAction;
- return '$type / $side';
- }
|