| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277 |
- import 'dart:io';
- import 'dart:typed_data';
- import 'dart:ui' as ui;
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/services.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:gal/gal.dart';
- import 'package:go_router/go_router.dart';
- import 'package:path_provider/path_provider.dart';
- import 'package:qr_flutter/qr_flutter.dart';
- import 'package:share_plus/share_plus.dart';
- import '../../../core/utils/avatar_urls.dart';
- import '../../../core/l10n/app_localizations.dart';
- import '../../../core/network/dio_client.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/utils/dialog_utils.dart';
- import '../../../core/utils/top_toast.dart';
- import '../../../data/repositories/copy_trading_repository.dart';
- import '../../../data/models/copy_trading/trader.dart';
- import '../../../data/services/auth_service.dart';
- import '../../../providers/app_provider.dart';
- import '../../../providers/auth_provider.dart';
- import '../../../providers/copy_trading_provider.dart';
- import '../../../providers/my_copy_trading_provider.dart';
- import '../../widgets/common/app_shimmer.dart';
- // ── Provider ──────────────────────────────────────────────────────────────────
- final _traderDetailProvider =
- FutureProvider.autoDispose.family<Map<String, dynamic>?, String>(
- (ref, traderId) =>
- ref.read(copyTradingRepositoryProvider).getTraderInfo(traderId),
- );
- // ── Screen ────────────────────────────────────────────────────────────────────
- class TraderDetailScreen extends ConsumerStatefulWidget {
- const TraderDetailScreen({super.key, required this.traderId});
- final String traderId;
- @override
- ConsumerState<TraderDetailScreen> createState() => _TraderDetailScreenState();
- }
- class _TraderDetailScreenState extends ConsumerState<TraderDetailScreen>
- with SingleTickerProviderStateMixin {
- // ── Tab: 历史带单=0 / 当前带单=1
- late TabController _tabController;
- final _scrollCtrl = ScrollController();
- // ── Favorite / Follow state
- bool _isFavorite = false;
- bool _isFollowing = false;
- bool _favoriteLoading = false;
- bool _followLoading = false;
- bool _initialized = false;
- // ── 带单合约
- bool _symbolExpanded = true;
- List<String> _traderSymbols = [];
- // ── 当前带单
- List<Map<String, dynamic>> _currentOrders = [];
- bool _loadingCurrent = false;
- bool _currentLoaded = false;
- int _currentPage = 1;
- bool _currentHasMore = true;
- bool _currentLoadingMore = false;
- // ── 历史带单
- List<Map<String, dynamic>> _historyOrders = [];
- bool _loadingHistory = false;
- bool _historyLoaded = false;
- int _historyPage = 1;
- bool _historyHasMore = true;
- bool _historyLoadingMore = false;
- static const _pageSize = 10;
- static const _avatarColors = [
- Color(0xFFf7931a),
- Color(0xFF627eea),
- Color(0xFF9945ff),
- Color(0xFFf3ba2f),
- Color(0xFF2775ca),
- Color(0xFF00aae4),
- ];
- @override
- void initState() {
- super.initState();
- // 默认显示「当前带单」(index 0)
- _tabController = TabController(length: 2, vsync: this, initialIndex: 0);
- _tabController.addListener(() {
- if (!mounted) return;
- if (!_tabController.indexIsChanging) {
- // 切换到历史带单 tab 时重新拉取
- if (_tabController.index == 1) _loadHistory();
- setState(() {}); // 仅更新 tab 标题计数
- }
- });
- // 进入页面同时预加载两个 tab 的数据
- _loadCurrent();
- _loadHistory();
- _loadTraderSymbols();
- }
- @override
- void dispose() {
- _tabController.dispose();
- _scrollCtrl.dispose();
- super.dispose();
- }
- Future<void> _loadTraderSymbols() async {
- try {
- final list = await ref
- .read(copyTradingRepositoryProvider)
- .getTraderSymbols(widget.traderId);
- if (mounted) {
- setState(() {
- _traderSymbols = list
- .map((s) =>
- s['symbolName']?.toString() ?? s['symbol']?.toString() ?? '')
- .where((n) => n.isNotEmpty)
- .toList()
- ..sort();
- });
- }
- } catch (_) {}
- }
- Color _avatarBg(String name) => _avatarColors[
- name.isEmpty ? 0 : name.codeUnitAt(0) % _avatarColors.length];
- String _fmt(dynamic raw, {int decimals = 2}) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return d.toStringAsFixed(decimals);
- }
- String _fmtPercent(dynamic raw) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return '${d >= 0 ? '+' : ''}${d.toStringAsFixed(2)}%';
- }
- Color _pnlColor(dynamic raw) {
- final d = double.tryParse(raw?.toString() ?? '');
- if (d == null) return AppColors.darkTextSecondary;
- return d >= 0 ? AppColors.rise : AppColors.fall;
- }
- // ── Data loading ──────────────────────────────────────────────────────────
- Future<void> _loadCurrent() async {
- if (_loadingCurrent) return;
- if (!mounted) return;
- setState(() {
- _loadingCurrent = true;
- _currentPage = 1;
- _currentHasMore = true;
- });
- try {
- final orders =
- await ref.read(copyTradingRepositoryProvider).getTraderOrders(
- traderId: widget.traderId,
- type: 'current',
- page: 1,
- pageSize: _pageSize,
- );
- if (mounted)
- setState(() {
- _currentOrders = orders;
- _currentLoaded = true;
- _currentHasMore = orders.length >= _pageSize;
- });
- } catch (_) {
- if (mounted) setState(() => _currentLoaded = true);
- } finally {
- if (mounted) setState(() => _loadingCurrent = false);
- }
- }
- Future<void> _loadMoreCurrent() async {
- if (!_currentHasMore || _currentLoadingMore || _loadingCurrent) return;
- final nextPage = _currentPage + 1;
- setState(() => _currentLoadingMore = true);
- try {
- final orders =
- await ref.read(copyTradingRepositoryProvider).getTraderOrders(
- traderId: widget.traderId,
- type: 'current',
- page: nextPage,
- pageSize: _pageSize,
- );
- if (mounted)
- setState(() {
- _currentOrders = [..._currentOrders, ...orders];
- _currentPage = nextPage;
- _currentHasMore = orders.length >= _pageSize;
- _currentLoadingMore = false;
- });
- } catch (_) {
- if (mounted) setState(() => _currentLoadingMore = false);
- }
- }
- Future<void> _loadHistory() async {
- if (_loadingHistory) return;
- setState(() {
- _loadingHistory = true;
- _historyPage = 1;
- _historyHasMore = true;
- });
- try {
- final orders =
- await ref.read(copyTradingRepositoryProvider).getTraderOrders(
- traderId: widget.traderId,
- type: 'history',
- page: 1,
- pageSize: _pageSize,
- );
- if (mounted)
- setState(() {
- _historyOrders = orders;
- _historyLoaded = true;
- _historyHasMore = orders.length >= _pageSize;
- });
- } catch (_) {
- if (mounted) setState(() => _historyLoaded = true);
- } finally {
- if (mounted) setState(() => _loadingHistory = false);
- }
- }
- Future<void> _loadMoreHistory() async {
- if (!_historyHasMore || _historyLoadingMore || _loadingHistory) return;
- final nextPage = _historyPage + 1;
- setState(() => _historyLoadingMore = true);
- try {
- final orders =
- await ref.read(copyTradingRepositoryProvider).getTraderOrders(
- traderId: widget.traderId,
- type: 'history',
- page: nextPage,
- pageSize: _pageSize,
- );
- if (mounted)
- setState(() {
- _historyOrders = [..._historyOrders, ...orders];
- _historyPage = nextPage;
- _historyHasMore = orders.length >= _pageSize;
- _historyLoadingMore = false;
- });
- } catch (_) {
- if (mounted) setState(() => _historyLoadingMore = false);
- }
- }
- // ── Build ─────────────────────────────────────────────────────────────────
- @override
- Widget build(BuildContext context) {
- ref.watch(localeProvider);
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final async = ref.watch(_traderDetailProvider(widget.traderId));
- ref.listen(_traderDetailProvider(widget.traderId), (_, next) {
- next.whenData((trader) {
- if (trader == null || _initialized) return;
- setState(() {
- _isFavorite = Trader.parseFavoriteFlag(
- trader['isFavorite'] ?? trader['isFavorited'] ?? trader['favorite'],
- );
- _isFollowing = trader['isFollow']?.toString() == '1' ||
- trader['follow']?.toString() == '1';
- _initialized = true;
- });
- });
- });
- final isTrader = ref.watch(copyTradingProvider.select((s) => s.isTrader));
- final trader = async.valueOrNull;
- return Scaffold(
- backgroundColor: isDark ? AppColors.darkBg : AppColors.lightBgSecondary,
- appBar: AppBar(
- backgroundColor: isDark ? AppColors.darkBg : AppColors.lightBg,
- title: Text(AppLocalizations.of(context)!.traderDetail,
- style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
- actions: [
- // 带单员不显示关注按钮
- if (!isTrader)
- async.whenOrNull(
- data: (td) => td == null
- ? const SizedBox()
- : GestureDetector(
- onTap: () => _toggleFavorite(td),
- child: Padding(
- padding: const EdgeInsets.all(14),
- child: Icon(
- _isFavorite
- ? Icons.favorite
- : Icons.favorite_border,
- color: _isFavorite
- ? Colors.red
- : cs.onSurface.withAlpha(153),
- size: 24,
- ),
- ),
- ),
- ) ??
- const SizedBox(),
- ],
- ),
- body: Column(
- children: [
- Expanded(
- child: _buildContent(context, cs, isDark, async, trader, isTrader),
- ),
- // 带单员不显示跟单/取消按钮
- if (!isTrader && trader != null)
- _BottomButton(
- isFollowing: _isFollowing,
- isFull: _isFull(trader),
- loading: _followLoading,
- onTap: () => _onFollowTap(context, trader),
- ),
- ],
- ),
- );
- }
- bool _isFull(Map<String, dynamic> trader) {
- final following = int.tryParse(trader['following']?.toString() ?? '') ?? 0;
- final maxFollow = int.tryParse(trader['maxFollow']?.toString() ?? '') ?? 0;
- return !_isFollowing && maxFollow > 0 && following >= maxFollow;
- }
- Widget _buildContent(
- BuildContext context,
- ColorScheme cs,
- bool isDark,
- AsyncValue<Map<String, dynamic>?> async,
- Map<String, dynamic>? trader,
- bool isTrader,
- ) {
- if (async.hasError && trader == null) {
- return Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(AppLocalizations.of(context)!.loadFailed,
- style: TextStyle(color: cs.onSurface.withAlpha(153))),
- const SizedBox(height: 12),
- ElevatedButton(
- onPressed: () =>
- ref.invalidate(_traderDetailProvider(widget.traderId)),
- style: ElevatedButton.styleFrom(
- backgroundColor: AppColors.brand,
- foregroundColor: Colors.black),
- child: Text(AppLocalizations.of(context)!.retry),
- ),
- ],
- ),
- );
- }
- if (async.isLoading && trader == null) {
- return const _TraderDetailSkeleton();
- }
- final l10n = AppLocalizations.of(context)!;
- final tabBar = TabBar(
- controller: _tabController,
- labelColor: cs.onSurface,
- unselectedLabelColor: cs.onSurface.withAlpha(153),
- indicatorColor: AppColors.brand,
- indicatorWeight: 2.5,
- dividerColor: Colors.transparent,
- labelStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
- unselectedLabelStyle: const TextStyle(fontSize: 14),
- tabs: [
- Tab(
- text:
- '${l10n.currentCopyOrders}(${_currentOrders.length})'),
- Tab(text: l10n.historyCopyOrders),
- ],
- );
- // NestedScrollView: 外层滚动(header)与内层滚动(各 tab 列表)完全独立,
- // 切换 tab 不会影响外层滚动位置,彻底解决回顶问题。
- return NestedScrollView(
- controller: _scrollCtrl,
- headerSliverBuilder: (ctx, innerBoxIsScrolled) => [
- SliverToBoxAdapter(child: _buildProfile(trader, cs, isDark)),
- SliverToBoxAdapter(child: _buildAccountInfo(trader, cs, isDark)),
- SliverToBoxAdapter(child: _buildCoreData(trader, cs, isDark)),
- SliverToBoxAdapter(child: _buildSymbolSection(cs, isDark, l10n)),
- SliverOverlapAbsorber(
- handle: NestedScrollView.sliverOverlapAbsorberHandleFor(ctx),
- sliver: SliverPersistentHeader(
- pinned: true,
- delegate: _StickyTabBarDelegate(
- tabBar: tabBar,
- bgColor: isDark ? AppColors.darkBg : AppColors.lightBg,
- dividerColor: cs.outlineVariant.withAlpha(80),
- ),
- ),
- ),
- ],
- body: TabBarView(
- controller: _tabController,
- physics: const NeverScrollableScrollPhysics(),
- children: [
- _OrderPage(
- orders: _currentOrders,
- loading: _loadingCurrent,
- loaded: _currentLoaded,
- hasMore: _currentHasMore,
- loadingMore: _currentLoadingMore,
- isHistory: false,
- cs: cs,
- onRefresh: () async {
- ref.invalidate(_traderDetailProvider(widget.traderId));
- await _loadCurrent();
- },
- onLoadMore: _loadMoreCurrent,
- ),
- _OrderPage(
- orders: _historyOrders,
- loading: _loadingHistory,
- loaded: _historyLoaded,
- hasMore: _historyHasMore,
- loadingMore: _historyLoadingMore,
- isHistory: true,
- cs: cs,
- onRefresh: () async {
- ref.invalidate(_traderDetailProvider(widget.traderId));
- await _loadHistory();
- },
- onLoadMore: _loadMoreHistory,
- ),
- ],
- ),
- );
- }
- // ── Profile ───────────────────────────────────────────────────────────────
- Widget _buildProfile(
- Map<String, dynamic>? trader, ColorScheme cs, bool isDark) {
- final nickname = trader?['nickname']?.toString() ?? '';
- final description = trader?['description']?.toString() ?? '';
- final tags = (trader?['tags'] as List?)
- ?.map((e) => e.toString())
- .where((t) => t.isNotEmpty)
- .toList() ??
- [];
- final following = trader?['following']?.toString() ?? '--';
- final maxFollow = trader?['maxFollow']?.toString() ?? '--';
- final registerDays = trader?['registerDays']?.toString() ?? '--';
- final levelName = trader?['levelName']?.toString() ?? '';
- final avatarUrl = trader != null
- ? resolvedAvatarUrlFromRecord(Map<String, dynamic>.from(trader))
- : null;
- final letter = nickname.isNotEmpty ? nickname[0].toUpperCase() : '?';
- return Container(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- _Avatar(
- letter: letter,
- avatarUrl: avatarUrl,
- bg: _avatarBg(nickname)),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 昵称 + 等级 badge
- Row(
- children: [
- Flexible(
- child: Text(nickname,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 18,
- fontWeight: FontWeight.w700)),
- ),
- if (levelName.isNotEmpty) ...[
- const SizedBox(width: 8),
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8, vertical: 2),
- decoration: BoxDecoration(
- color: AppColors.brand,
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(levelName,
- style: const TextStyle(
- color: Colors.black,
- fontSize: 11,
- fontWeight: FontWeight.w700)),
- ),
- ],
- ],
- ),
- const SizedBox(height: 8),
- // 入驻天数 + 当前跟随
- Row(
- children: [
- Icon(Icons.calendar_today_outlined,
- size: 13, color: cs.onSurface.withAlpha(120)),
- const SizedBox(width: 3),
- Text(
- AppLocalizations.of(context)!
- .settledDaysLabelFmt(registerDays),
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 12)),
- const SizedBox(width: 16),
- Icon(Icons.group_outlined,
- size: 13, color: cs.onSurface.withAlpha(120)),
- const SizedBox(width: 3),
- Text(
- AppLocalizations.of(context)!
- .currentFollowingLabelFmt(following, maxFollow),
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 12)),
- ],
- ),
- ],
- ),
- ),
- ],
- ),
- if (description.isNotEmpty) ...[
- const SizedBox(height: 12),
- Text(description,
- style: TextStyle(
- color: cs.onSurface.withAlpha(153), fontSize: 13)),
- ],
- if (tags.isNotEmpty) ...[
- const SizedBox(height: 10),
- Wrap(
- spacing: 6,
- runSpacing: 6,
- children: tags.map((t) => _TagChip(tag: t)).toList(),
- ),
- ],
- ],
- ),
- );
- }
- // ── 账户信息 ──────────────────────────────────────────────────────────────
- Widget _buildAccountInfo(
- Map<String, dynamic>? trader, ColorScheme cs, bool isDark) {
- final l10n = AppLocalizations.of(context)!;
- return _StatCard(
- isDark: isDark,
- title: l10n.accountInfoTitle,
- rows: [
- [
- _DetailStat(
- label: l10n.cumFollowProfitAmtUsdt,
- value: _fmt(trader?['profitAmount']),
- valueColor: AppColors.rise,
- ),
- _DetailStat(
- label: l10n.fundStrengthUsdt,
- value: trader?['moneyStrength']?.toString() ?? '--',
- ),
- ],
- [
- _DetailStat(
- label: l10n.cumFollowerCount,
- value: trader?['followCustomer']?.toString() ?? '--',
- ),
- _DetailStat(
- label: l10n.cumTradingDays,
- value: trader?['tradingDays']?.toString() ?? '--',
- ),
- ],
- ],
- );
- }
- // ── 核心数据 ──────────────────────────────────────────────────────────────
- Widget _buildCoreData(
- Map<String, dynamic>? trader, ColorScheme cs, bool isDark) {
- final dayYield30 = trader?['dayYield14'];
- final profit30d = trader?['teamProfit14'];
- final winRate = trader?['winRate14'] ?? trader?['winRate30'];
- final dividendPercent = trader?['dividendPercent'];
- String profit30dStr;
- Color? profit30dColor;
- if (profit30d != null) {
- final d = double.tryParse(profit30d.toString());
- profit30dStr =
- d == null ? '--' : '${d >= 0 ? '+' : ''}${d.toStringAsFixed(2)}';
- profit30dColor = _pnlColor(profit30d);
- } else {
- profit30dStr = '--';
- profit30dColor = null;
- }
- final l10n = AppLocalizations.of(context)!;
- return _StatCard(
- isDark: isDark,
- title: l10n.coreDataTitle,
- rows: [
- [
- _DetailStat(
- label: l10n.yield14d,
- value: _fmtPercent(dayYield30),
- valueColor: _pnlColor(dayYield30),
- ),
- _DetailStat(
- label: l10n.profit14dUsdt,
- value: profit30dStr,
- valueColor: profit30dColor,
- ),
- ],
- [
- _DetailStat(
- label: l10n.winRate14d,
- value: winRate == null ? '--' : '${_fmt(winRate, decimals: 1)}%',
- ),
- _DetailStat(
- label: l10n.profitShareRatio,
- value: dividendPercent == null
- ? '--'
- : '${_fmt(dividendPercent, decimals: 0)}%',
- ),
- ],
- ],
- );
- }
- // ── 带单合约 ──────────────────────────────────────────────────────────────
- Widget _buildSymbolSection(ColorScheme cs, bool isDark, AppLocalizations l10n) {
- final symbols = _traderSymbols;
- if (symbols.isEmpty) return const SizedBox.shrink();
- return Container(
- margin: const EdgeInsets.fromLTRB(12, 8, 12, 0),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- GestureDetector(
- onTap: () => setState(() => _symbolExpanded = !_symbolExpanded),
- behavior: HitTestBehavior.opaque,
- child: Row(
- children: [
- Text(l10n.tradingContracts,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- const Spacer(),
- Icon(
- _symbolExpanded
- ? Icons.keyboard_arrow_up
- : Icons.keyboard_arrow_down,
- color: cs.onSurface.withAlpha(153),
- size: 20,
- ),
- ],
- ),
- ),
- if (_symbolExpanded) ...[
- const SizedBox(height: 10),
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: symbols.map((sym) {
- return Container(
- padding:
- const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
- decoration: BoxDecoration(
- color: AppColors.brand.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: AppColors.brand.withValues(alpha: 0.6),
- width: 1.5,
- ),
- ),
- child: Text(
- sym,
- style: const TextStyle(
- color: AppColors.brand,
- fontSize: 13,
- fontWeight: FontWeight.w600,
- ),
- ),
- );
- }).toList(),
- ),
- ],
- ],
- ),
- );
- }
- // ── Actions ───────────────────────────────────────────────────────────────
- void _onFollowTap(BuildContext context, Map<String, dynamic> trader) {
- if (_isFollowing) {
- _toggleFollow(trader);
- } else {
- context.push<bool>('/follow-setting', extra: trader).then((result) {
- if (result == true && mounted) {
- setState(() => _isFollowing = true);
- ref.read(myCopyTradingProvider.notifier).silentRefresh();
- ref.read(copyTradingProvider.notifier).silentRefresh();
- }
- });
- }
- }
- Future<void> _toggleFavorite(Map<String, dynamic> trader) async {
- if (_favoriteLoading) return;
- if (!ref.read(isLoggedInProvider)) {
- context.push('/login');
- return;
- }
- setState(() => _favoriteLoading = true);
- final repo = ref.read(copyTradingRepositoryProvider);
- final id = trader['id']?.toString() ?? widget.traderId;
- try {
- final ok = _isFavorite
- ? await repo.unfavoriteTrader(id)
- : await repo.favoriteTrader(id);
- if (mounted) {
- setState(() {
- if (ok) _isFavorite = !_isFavorite;
- _favoriteLoading = false;
- });
- if (!ok)
- showTipDialog(context,
- content: AppLocalizations.of(context)!.operationFailedRetry);
- }
- } catch (e) {
- if (mounted) {
- setState(() => _favoriteLoading = false);
- showTipDialog(context, content: extractErrorMessage(e));
- }
- }
- }
- Future<void> _toggleFollow(Map<String, dynamic> trader) async {
- if (_followLoading) return;
- if (!ref.read(isLoggedInProvider)) {
- context.push('/login');
- return;
- }
- final id = trader['id']?.toString() ?? widget.traderId;
- if (_isFollowing) {
- final confirmed = await showConfirmDialog(context,
- content: AppLocalizations.of(context)!.unfollowTraderConfirm);
- if (!confirmed || !mounted) return;
- }
- setState(() => _followLoading = true);
- final repo = ref.read(copyTradingRepositoryProvider);
- try {
- bool ok;
- if (_isFollowing) {
- ok = await repo.unfollowTrader(id);
- if (ok && mounted) {
- setState(() => _isFollowing = false);
- ref.read(myCopyTradingProvider.notifier).silentRefresh();
- ref.read(copyTradingProvider.notifier).silentRefresh();
- }
- } else {
- ok = await repo.followTrader({'traderId': id});
- if (ok && mounted) setState(() => _isFollowing = true);
- }
- if (mounted) {
- setState(() => _followLoading = false);
- if (!ok)
- showTipDialog(context,
- content: AppLocalizations.of(context)!.operationFailedRetry);
- }
- } catch (e) {
- if (mounted) {
- setState(() => _followLoading = false);
- showTipDialog(context, content: extractErrorMessage(e));
- }
- }
- }
- }
- // ── 订单列表页(每个 tab 独立滚动,切换不影响外层位置)─────────────────────────────
- class _OrderPage extends StatelessWidget {
- const _OrderPage({
- required this.orders,
- required this.loading,
- required this.loaded,
- required this.hasMore,
- required this.loadingMore,
- required this.isHistory,
- required this.cs,
- required this.onRefresh,
- required this.onLoadMore,
- });
- final List<Map<String, dynamic>> orders;
- final bool loading;
- final bool loaded;
- final bool hasMore;
- final bool loadingMore;
- final bool isHistory;
- final ColorScheme cs;
- final Future<void> Function() onRefresh;
- final VoidCallback onLoadMore;
- @override
- Widget build(BuildContext context) {
- return Builder(
- builder: (ctx) {
- return NotificationListener<ScrollNotification>(
- onNotification: (n) {
- if (n is ScrollEndNotification &&
- n.metrics.pixels >= n.metrics.maxScrollExtent - 300) {
- onLoadMore();
- }
- return false;
- },
- child: RefreshIndicator(
- color: AppColors.brand,
- onRefresh: onRefresh,
- child: CustomScrollView(
- slivers: [
- SliverOverlapInjector(
- handle: NestedScrollView.sliverOverlapAbsorberHandleFor(ctx),
- ),
- if (loading && !loaded)
- const SliverFillRemaining(
- child: Center(
- child: CircularProgressIndicator(color: AppColors.brand),
- ),
- )
- else if (loaded && orders.isEmpty)
- SliverFillRemaining(
- child: Center(
- child: Text(AppLocalizations.of(context)!.noTradeRecords,
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 14)),
- ),
- )
- else
- SliverPadding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
- sliver: SliverList(
- delegate: SliverChildBuilderDelegate(
- (_, i) {
- if (i < orders.length) {
- return isHistory
- ? _HistoryOrderCard(order: orders[i])
- : _CurrentOrderCard(order: orders[i]);
- }
- if (loadingMore) {
- return const Padding(
- padding: EdgeInsets.symmetric(vertical: 16),
- child: Center(
- child: CircularProgressIndicator(
- color: AppColors.brand, strokeWidth: 2),
- ),
- );
- }
- if (!hasMore && orders.isNotEmpty) {
- return Padding(
- padding: const EdgeInsets.symmetric(vertical: 16),
- child: Center(
- child: Text(
- AppLocalizations.of(context)!.noMore,
- style: TextStyle(
- color: cs.onSurface.withAlpha(100),
- fontSize: 12)),
- ),
- );
- }
- return const SizedBox(height: 20);
- },
- childCount: orders.length + 1,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- );
- }
- }
- // ── Sticky TabBar delegate ────────────────────────────────────────────────────
- class _StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
- const _StickyTabBarDelegate({
- required this.tabBar,
- required this.bgColor,
- required this.dividerColor,
- });
- final TabBar tabBar;
- final Color bgColor;
- final Color dividerColor;
- @override
- double get minExtent => tabBar.preferredSize.height;
- @override
- double get maxExtent => tabBar.preferredSize.height;
- @override
- Widget build(
- BuildContext context, double shrinkOffset, bool overlapsContent) {
- return DecoratedBox(
- decoration: BoxDecoration(
- color: bgColor,
- border: Border(bottom: BorderSide(color: dividerColor, width: 0.5)),
- ),
- child: tabBar,
- );
- }
- @override
- bool shouldRebuild(covariant _StickyTabBarDelegate old) =>
- old.tabBar != tabBar || old.bgColor != bgColor;
- }
- // ── 统计卡片 ──────────────────────────────────────────────────────────────────
- class _StatCard extends StatelessWidget {
- const _StatCard({
- required this.isDark,
- required this.title,
- required this.rows,
- });
- final bool isDark;
- final String title;
- final List<List<Widget>> rows;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return Container(
- margin: const EdgeInsets.fromLTRB(12, 8, 12, 0),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(title,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- ...rows.expand((row) => [
- const SizedBox(height: 14),
- Row(children: row),
- ]),
- ],
- ),
- );
- }
- }
- // ── Avatar ────────────────────────────────────────────────────────────────────
- class _Avatar extends StatelessWidget {
- const _Avatar({required this.letter, required this.bg, this.avatarUrl});
- final String letter;
- final Color bg;
- final String? avatarUrl;
- @override
- Widget build(BuildContext context) {
- if (avatarUrl != null && avatarUrl!.isNotEmpty) {
- return ClipOval(
- child: Image.network(avatarUrl!,
- width: 64,
- height: 64,
- fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => _fallback()),
- );
- }
- return _fallback();
- }
- Widget _fallback() => Container(
- width: 64,
- height: 64,
- decoration: BoxDecoration(color: bg, shape: BoxShape.circle),
- child: Center(
- child: Text(letter,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 24,
- fontWeight: FontWeight.w700)),
- ),
- );
- }
- // ── 标签 chip ─────────────────────────────────────────────────────────────────
- class _TagChip extends StatelessWidget {
- const _TagChip({required this.tag});
- final String tag;
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
- decoration: BoxDecoration(
- color: AppColors.tagBlueBg,
- borderRadius: BorderRadius.circular(20),
- ),
- child: Text(tag,
- style: const TextStyle(color: AppColors.tagBlue, fontSize: 12)),
- );
- }
- }
- // ── 统计项 ────────────────────────────────────────────────────────────────────
- class _DetailStat extends StatelessWidget {
- const _DetailStat(
- {required this.label, required this.value, this.valueColor});
- final String label;
- final String value;
- final Color? valueColor;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(label,
- style:
- TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 12)),
- const SizedBox(height: 4),
- Text(
- value,
- style: TextStyle(
- color: valueColor ?? cs.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w600,
- fontFeatures: const [FontFeature.tabularFigures()],
- ),
- ),
- ],
- ),
- );
- }
- }
- // ── 数量格式化 ─────────────────────────────────────────────────────────────────
- String _fmtQty(dynamic raw) {
- if (raw == null) return '--';
- final str = raw.toString().trim();
- if (str.isEmpty || double.tryParse(str) == null) return '--';
- final isNeg = str.startsWith('-');
- final absStr = isNeg ? str.substring(1) : str;
- final dotIdx = absStr.indexOf('.');
- String s;
- if (dotIdx < 0) {
- s = absStr;
- } else {
- final frac = absStr.substring(dotIdx + 1);
- s = '${absStr.substring(0, dotIdx)}.${frac.length >= 4 ? frac.substring(0, 4) : frac.padRight(4, '0')}';
- }
- if (s.contains('.')) {
- s = s.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '');
- }
- final result = s.isEmpty ? '0' : s;
- return isNeg ? '-$result' : result;
- }
- // ── 当前带单卡片 ───────────────────────────────────────────────────────────────
- class _CurrentOrderCard extends StatelessWidget {
- const _CurrentOrderCard({required this.order});
- final Map<String, dynamic> order;
- String _fmt(dynamic raw, {int decimals = 2}) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return d.toStringAsFixed(decimals);
- }
- String _fmtPnl(dynamic raw, {int decimals = 2}) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return '${d >= 0 ? '+' : ''}${d.toStringAsFixed(decimals)}';
- }
- Color _pnlColor(dynamic raw, ColorScheme cs) {
- final d = double.tryParse(raw?.toString() ?? '');
- if (d == null) return cs.onSurface;
- return d >= 0 ? AppColors.rise : AppColors.fall;
- }
- String _fmtTimestamp(dynamic ts) {
- if (ts == null) return '--';
- final ms = int.tryParse(ts.toString());
- if (ms == null) return ts.toString();
- final dt = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)
- .add(const Duration(hours: 8));
- return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
- '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
- }
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final l10n = AppLocalizations.of(context)!;
- final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg;
- final symbol = order['symbol']?.toString() ?? '--';
- final direction = order['direction']?.toString() ?? '';
- final leverage = order['leverage']?.toString() ?? '--';
- final marginType = order['positionType']?.toString() ??
- order['marginType']?.toString() ??
- l10n.crossMargin;
- final openPrice = order['openPrice'] ?? order['avgOpenPrice'];
- final currentPrice = order['currentPrice'] ?? order['markPrice'];
- final margin = order['principalAmount'];
- final quantity = order['totalPosition'];
- final roi = order['profitRate'];
- final pnl = order['profit'];
- final openTimeStr = _fmtTimestamp(order['openTime']);
- final positionId =
- order['positionId']?.toString() ?? order['id']?.toString() ?? '';
- final isLong = direction == '0';
- final directionLabel =
- isLong ? l10n.openLongBullish : l10n.openShortBearish;
- final directionColor = isLong ? AppColors.rise : AppColors.fall;
- final baseCoin = symbol.contains('/')
- ? symbol.split('/')[0]
- : symbol.replaceAll('USDT', '');
- return Container(
- margin: const EdgeInsets.only(bottom: 10),
- padding: const EdgeInsets.all(14),
- decoration:
- BoxDecoration(color: cardBg, borderRadius: BorderRadius.circular(10)),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 标题行
- Row(
- children: [
- Expanded(
- child: Text('$symbol ${l10n.perpetual}',
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- ),
- _Badge(
- label: directionLabel,
- bgColor: directionColor.withValues(alpha: 0.15),
- textColor: directionColor),
- const SizedBox(width: 6),
- _Badge(
- label: '${leverage}x',
- bgColor: cs.onSurface.withAlpha(20),
- textColor: cs.onSurface.withAlpha(180),
- borderColor: cs.onSurface.withAlpha(60)),
- const SizedBox(width: 6),
- _Badge(
- label: marginType,
- bgColor: cs.onSurface.withAlpha(20),
- textColor: cs.onSurface.withAlpha(153),
- borderColor: cs.onSurface.withAlpha(60)),
- ],
- ),
- const SizedBox(height: 12),
- Row(
- children: [
- _OrderStat(label: l10n.openAvgPriceUsdt, value: _fmt(openPrice)),
- _OrderStat(
- label: l10n.currentPriceUsdt, value: _fmt(currentPrice)),
- _OrderStat(label: l10n.currentMarginUsdt, value: _fmt(margin)),
- ],
- ),
- const SizedBox(height: 10),
- Row(
- children: [
- _OrderStat(
- label: l10n.qtyWithCoin(baseCoin), value: _fmtQty(quantity)),
- _OrderStat(
- label: l10n.returnRate,
- value: roi == null ? '--' : '${_fmtPnl(roi)}%',
- valueColor: _pnlColor(roi, cs)),
- _OrderStat(
- label: l10n.profitUsdt,
- value: _fmtPnl(pnl),
- valueColor: _pnlColor(pnl, cs)),
- ],
- ),
- const SizedBox(height: 10),
- Divider(height: 1, thickness: 0.5, color: cs.outlineVariant),
- const SizedBox(height: 8),
- Row(
- children: [
- Text(l10n.openTimeWithValue(openTimeStr),
- style: TextStyle(
- color: cs.onSurface.withAlpha(120), fontSize: 11)),
- const Spacer(),
- if (positionId.isNotEmpty)
- Row(
- children: [
- Text('${l10n.positionIdPrefix}$positionId',
- style: TextStyle(
- color: cs.onSurface.withAlpha(120), fontSize: 11)),
- const SizedBox(width: 4),
- GestureDetector(
- onTap: () {
- Clipboard.setData(ClipboardData(text: positionId));
- showTopToast(context,
- message: l10n.copyPositionIdSuccess,
- backgroundColor: AppColors.rise);
- },
- child: Icon(Icons.copy_outlined,
- size: 13, color: cs.onSurface.withAlpha(100)),
- ),
- ],
- ),
- ],
- ),
- ],
- ),
- );
- }
- }
- // ── 历史带单卡片 ───────────────────────────────────────────────────────────────
- class _HistoryOrderCard extends StatelessWidget {
- const _HistoryOrderCard({required this.order});
- final Map<String, dynamic> order;
- String _fmt(dynamic raw, {int decimals = 2}) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return d.toStringAsFixed(decimals);
- }
- String _fmtPnl(dynamic raw, {int decimals = 2}) {
- if (raw == null) return '--';
- final d = double.tryParse(raw.toString());
- if (d == null) return '--';
- return '${d >= 0 ? '+' : ''}${d.toStringAsFixed(decimals)}';
- }
- Color _pnlColor(dynamic raw, ColorScheme cs) {
- final d = double.tryParse(raw?.toString() ?? '');
- if (d == null) return cs.onSurface;
- return d >= 0 ? AppColors.rise : AppColors.fall;
- }
- String _fmtTimestamp(dynamic ts) {
- if (ts == null) return '--';
- final ms = int.tryParse(ts.toString());
- if (ms == null) return ts.toString();
- final dt = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)
- .add(const Duration(hours: 8));
- return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
- '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
- }
- void _showShareSheet(BuildContext context) {
- showModalBottomSheet(
- context: context,
- useRootNavigator: true,
- isScrollControlled: true,
- backgroundColor: Colors.transparent,
- builder: (_) => _ShareOrderSheet(order: order, fmt: _fmt),
- );
- }
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg;
- final l10n = AppLocalizations.of(context)!;
- final symbol = order['symbol']?.toString() ?? '--';
- final direction = order['direction']?.toString() ?? '';
- final leverage = order['leverage']?.toString() ?? '--';
- final marginType = order['positionType']?.toString() ??
- order['marginType']?.toString() ??
- l10n.crossMargin;
- final openPrice = order['openPrice'] ?? order['avgOpenPrice'];
- final closePrice = order['closePrice'] ?? order['avgClosePrice'];
- final quantity = order['totalPosition'];
- final pnl = order['profit'];
- final roi = order['profitRate'];
- final openTime = _fmtTimestamp(order['openTime']);
- final closeTime = _fmtTimestamp(order['closeTime']);
- final isLong = direction == '0';
- final directionLabel =
- isLong ? l10n.openLongBullish : l10n.openShortBearish;
- final directionColor = isLong ? AppColors.rise : AppColors.fall;
- final baseCoin = symbol.contains('/')
- ? symbol.split('/')[0]
- : symbol.replaceAll('USDT', '');
- return Container(
- margin: const EdgeInsets.only(bottom: 10),
- padding: const EdgeInsets.all(14),
- decoration:
- BoxDecoration(color: cardBg, borderRadius: BorderRadius.circular(10)),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // 标题行
- Row(
- children: [
- Expanded(
- child: Text('$symbol ${l10n.perpetual}',
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- ),
- GestureDetector(
- onTap: () => _showShareSheet(context),
- child: Icon(Icons.open_in_new,
- size: 14, color: cs.onSurface.withAlpha(100)),
- ),
- ],
- ),
- const SizedBox(height: 8),
- // 徽章行
- Row(
- children: [
- _Badge(
- label: directionLabel,
- bgColor: directionColor.withValues(alpha: 0.15),
- textColor: directionColor),
- const SizedBox(width: 6),
- _Badge(
- label: '${leverage}x',
- bgColor: cs.onSurface.withAlpha(20),
- textColor: cs.onSurface.withAlpha(180),
- borderColor: cs.onSurface.withAlpha(60)),
- const SizedBox(width: 6),
- _Badge(
- label: marginType,
- bgColor: cs.onSurface.withAlpha(20),
- textColor: cs.onSurface.withAlpha(153),
- borderColor: cs.onSurface.withAlpha(60)),
- ],
- ),
- const SizedBox(height: 12),
- // 数据行 1: 数量 + 收益 + 收益率
- Row(
- children: [
- _OrderStat(
- label: l10n.qtyWithCoin(baseCoin), value: _fmtQty(quantity)),
- _OrderStat(
- label: l10n.profitUsdt,
- value: _fmtPnl(pnl),
- valueColor: _pnlColor(pnl, cs)),
- _OrderStat(
- label: l10n.returnRate,
- value: roi == null ? '--' : '${_fmtPnl(roi)}%',
- valueColor: _pnlColor(roi, cs)),
- ],
- ),
- const SizedBox(height: 10),
- // 数据行 2: 开仓均价 + 平仓均价
- Row(
- children: [
- _OrderStat(label: l10n.openAvgPriceUsdt, value: _fmt(openPrice)),
- _OrderStat(
- label: l10n.closeAvgPriceUsdt, value: _fmt(closePrice)),
- const Expanded(child: SizedBox()),
- ],
- ),
- const SizedBox(height: 10),
- Divider(height: 1, thickness: 0.5, color: cs.outlineVariant),
- const SizedBox(height: 8),
- Row(
- children: [
- Text(openTime,
- style: TextStyle(
- color: cs.onSurface.withAlpha(120), fontSize: 11)),
- const Spacer(),
- Text(closeTime,
- style: TextStyle(
- color: cs.onSurface.withAlpha(120), fontSize: 11)),
- ],
- ),
- ],
- ),
- );
- }
- }
- // ── Badge ─────────────────────────────────────────────────────────────────────
- class _Badge extends StatelessWidget {
- const _Badge({
- required this.label,
- required this.bgColor,
- required this.textColor,
- this.borderColor,
- });
- final String label;
- final Color bgColor;
- final Color textColor;
- final Color? borderColor;
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: bgColor,
- borderRadius: BorderRadius.circular(4),
- border: borderColor != null
- ? Border.all(color: borderColor!, width: 0.8)
- : null,
- ),
- child: Text(label,
- style: TextStyle(
- color: textColor, fontSize: 11, fontWeight: FontWeight.w600)),
- );
- }
- }
- // ── OrderStat ─────────────────────────────────────────────────────────────────
- class _OrderStat extends StatelessWidget {
- const _OrderStat({required this.label, required this.value, this.valueColor});
- final String label;
- final String value;
- final Color? valueColor;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(label,
- style:
- TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)),
- const SizedBox(height: 2),
- Text(value,
- style: TextStyle(
- color: valueColor ?? cs.onSurface,
- fontSize: 13,
- fontWeight: FontWeight.w600,
- fontFeatures: const [FontFeature.tabularFigures()],
- )),
- ],
- ),
- );
- }
- }
- // ── 底部按钮 ──────────────────────────────────────────────────────────────────
- class _BottomButton extends StatelessWidget {
- const _BottomButton({
- required this.isFollowing,
- required this.loading,
- required this.onTap,
- this.isFull = false,
- });
- final bool isFollowing;
- final bool isFull;
- final bool loading;
- final VoidCallback onTap;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- return Container(
- padding: EdgeInsets.fromLTRB(
- 16, 12, 16, 12 + MediaQuery.of(context).padding.bottom),
- decoration: BoxDecoration(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- border: Border(
- top: BorderSide(
- color:
- isDark ? AppColors.darkDivider : AppColors.lightDivider)),
- ),
- child: SizedBox(
- width: double.infinity,
- height: 48,
- child: loading
- ? OutlinedButton(
- onPressed: null,
- style: OutlinedButton.styleFrom(
- side: BorderSide(color: cs.outline.withAlpha(50)),
- shape: const StadiumBorder()),
- child: const SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(strokeWidth: 2)),
- )
- : isFull
- ? ElevatedButton(
- onPressed: null,
- style: ElevatedButton.styleFrom(
- backgroundColor: cs.outline.withAlpha(40),
- foregroundColor: cs.onSurface.withAlpha(100),
- elevation: 0,
- shape: const StadiumBorder(),
- disabledBackgroundColor: cs.outline.withAlpha(40),
- disabledForegroundColor: cs.onSurface.withAlpha(100),
- ),
- child: Text(AppLocalizations.of(context)!.fullCapacity,
- style: const TextStyle(
- fontSize: 15, fontWeight: FontWeight.w600)),
- )
- : isFollowing
- ? ElevatedButton(
- onPressed: onTap,
- style: ElevatedButton.styleFrom(
- backgroundColor: isDark
- ? AppColors.darkBgTertiary
- : AppColors.lightBgTertiary,
- foregroundColor: cs.onSurface,
- elevation: 0,
- shape: const StadiumBorder(),
- ),
- child: Text(AppLocalizations.of(context)!.unfollow,
- style: const TextStyle(
- fontSize: 15, fontWeight: FontWeight.w600)),
- )
- : ElevatedButton(
- onPressed: onTap,
- style: ElevatedButton.styleFrom(
- backgroundColor: AppColors.brand,
- foregroundColor: Colors.black,
- shape: const StadiumBorder(),
- elevation: 0,
- ),
- child: Text(AppLocalizations.of(context)!.followTrader,
- style: const TextStyle(
- fontSize: 15, fontWeight: FontWeight.w600)),
- ),
- ),
- );
- }
- }
- // ── 骨架屏 ────────────────────────────────────────────────────────────────────
- class _TraderDetailSkeleton extends StatelessWidget {
- const _TraderDetailSkeleton();
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final cardBg = isDark ? AppColors.darkBgSecondary : AppColors.lightBg;
- return AppShimmer(
- child: SingleChildScrollView(
- physics: const NeverScrollableScrollPhysics(),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Profile card
- Container(
- color: cardBg,
- padding: const EdgeInsets.all(16),
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- shimmerCircle(64),
- const SizedBox(width: 14),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(140, 18),
- const SizedBox(height: 10),
- shimmerBox(200, 13),
- ],
- ),
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- // 账户信息 card
- Container(
- margin: const EdgeInsets.fromLTRB(12, 0, 12, 0),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: cardBg, borderRadius: BorderRadius.circular(12)),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(60, 14),
- const SizedBox(height: 14),
- Row(children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(120, 12),
- const SizedBox(height: 6),
- shimmerBox(80, 15),
- ])),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(100, 12),
- const SizedBox(height: 6),
- shimmerBox(60, 15),
- ])),
- ]),
- const SizedBox(height: 14),
- Row(children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(80, 12),
- const SizedBox(height: 6),
- shimmerBox(50, 15),
- ])),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(80, 12),
- const SizedBox(height: 6),
- shimmerBox(40, 15),
- ])),
- ]),
- ],
- ),
- ),
- const SizedBox(height: 8),
- // 核心数据 card
- Container(
- margin: const EdgeInsets.fromLTRB(12, 0, 12, 0),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: cardBg, borderRadius: BorderRadius.circular(12)),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(60, 14),
- const SizedBox(height: 14),
- Row(children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(80, 12),
- const SizedBox(height: 6),
- shimmerBox(70, 15),
- ])),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- shimmerBox(100, 12),
- const SizedBox(height: 6),
- shimmerBox(70, 15),
- ])),
- ]),
- ],
- ),
- ),
- const SizedBox(height: 16),
- // Tab bar skeleton
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Row(children: [
- Expanded(child: shimmerFill(32, radius: 4)),
- const SizedBox(width: 16),
- Expanded(child: shimmerFill(32, radius: 4)),
- ]),
- ),
- const SizedBox(height: 16),
- // 订单卡片骨架
- ...List.generate(
- 3,
- (_) => Container(
- margin: const EdgeInsets.fromLTRB(12, 0, 12, 10),
- padding: const EdgeInsets.all(14),
- decoration: BoxDecoration(
- color: cs.onSurface.withAlpha(8),
- borderRadius: BorderRadius.circular(10)),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(children: [
- Expanded(child: shimmerBox(100, 14)),
- shimmerBox(60, 20, radius: 4),
- ]),
- const SizedBox(height: 10),
- Row(
- children: List.generate(
- 3,
- (i) => Expanded(
- child: Padding(
- padding:
- EdgeInsets.only(right: i < 2 ? 8.0 : 0),
- child: shimmerBox(double.infinity, 32,
- radius: 4),
- ),
- ))),
- ],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
- // ── 分享带单 BottomSheet ───────────────────────────────────
- class _ShareOrderSheet extends ConsumerStatefulWidget {
- const _ShareOrderSheet({required this.order, required this.fmt});
- final Map<String, dynamic> order;
- final String Function(dynamic, {int decimals}) fmt;
- @override
- ConsumerState<_ShareOrderSheet> createState() => _ShareOrderSheetState();
- }
- class _ShareOrderSheetState extends ConsumerState<_ShareOrderSheet> {
- final _cardKey = GlobalKey();
- bool _sharing = false;
- bool _saving = false;
- String? _inviteCode;
- String? _inviteUrl;
- @override
- void initState() {
- super.initState();
- _loadInviteInfo();
- }
- Future<void> _loadInviteInfo() async {
- try {
- final dio = ref.read(dioClientProvider);
- final data = await AuthService(dio).getMyInfo();
- final prefix = data['promotionPrefix']?.toString() ?? '';
- final code = data['promotionCode']?.toString() ?? '';
- final url =
- (prefix.isNotEmpty || code.isNotEmpty) ? '$prefix$code' : null;
- if (mounted) {
- setState(() {
- _inviteCode = code.isNotEmpty ? code : null;
- _inviteUrl = url;
- });
- }
- } catch (_) {}
- }
- String _fmtTimestamp(dynamic ts) {
- if (ts == null) return '--';
- final ms = int.tryParse(ts.toString());
- if (ms == null) return ts.toString();
- final dt = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: true)
- .add(const Duration(hours: 8));
- return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
- '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
- }
- Future<Uint8List?> _renderCard() async {
- final boundary =
- _cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
- if (boundary == null) return null;
- final image = await boundary.toImage(pixelRatio: 3.0);
- final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
- return byteData?.buffer.asUint8List();
- }
- Future<void> _doSave(BuildContext context) async {
- setState(() => _saving = true);
- try {
- final bytes = await _renderCard();
- if (bytes == null) return;
- await Gal.requestAccess();
- await Gal.putImageBytes(
- bytes,
- name: 'trade_share_${DateTime.now().millisecondsSinceEpoch}',
- );
- if (!context.mounted) return;
- showTopToast(context,
- message: AppLocalizations.of(context)!.saveSuccess,
- backgroundColor: AppColors.rise);
- } on GalException catch (e) {
- if (!context.mounted) return;
- final l10n = AppLocalizations.of(context)!;
- if (e.type == GalExceptionType.accessDenied) {
- showTopToast(context,
- message: l10n.photoPermissionDenied,
- backgroundColor: AppColors.fall);
- } else {
- showTopToast(context,
- message: l10n.saveFailed, backgroundColor: AppColors.fall);
- }
- } catch (e) {
- if (context.mounted) {
- showTopToast(context,
- message: AppLocalizations.of(context)!.saveFailed,
- backgroundColor: AppColors.fall);
- }
- } finally {
- if (mounted) setState(() => _saving = false);
- }
- }
- Future<void> _doShare(BuildContext context) async {
- setState(() => _sharing = true);
- try {
- final bytes = await _renderCard();
- if (bytes == null) return;
- final tmpDir = await getTemporaryDirectory();
- final file = File(
- '${tmpDir.path}/trade_share_${DateTime.now().millisecondsSinceEpoch}.png');
- await file.writeAsBytes(bytes);
- if (!context.mounted) return;
- Navigator.of(context).pop();
- await Share.shareXFiles(
- [XFile(file.path, mimeType: 'image/png')],
- subject: AppLocalizations.of(context)!.myTradingProfit,
- );
- } catch (e) {
- if (context.mounted) {
- showTopToast(context,
- message: AppLocalizations.of(context)!.shareFailed,
- backgroundColor: AppColors.fall);
- }
- } finally {
- if (mounted) setState(() => _sharing = false);
- }
- }
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final order = widget.order;
- final profit = double.tryParse(order['profit']?.toString() ?? '0') ?? 0.0;
- final pnlPositive = profit >= 0;
- final l10n = AppLocalizations.of(context)!;
- final symbol = order['symbol']?.toString() ?? '--';
- final isLong = (order['direction']?.toString() ?? '0') == '0';
- final leverage = order['leverage']?.toString() ?? '--';
- final profitRateRaw =
- double.tryParse(order['profitRate']?.toString() ?? '0') ?? 0.0;
- final profitRateStr =
- '${profitRateRaw >= 0 ? '+' : ''}${profitRateRaw.toStringAsFixed(2)}%';
- final openPrice = widget.fmt(order['openPrice']);
- final closePrice = widget.fmt(order['closePrice']);
- final openTime = _fmtTimestamp(order['openTime']);
- final closeTime = _fmtTimestamp(order['closeTime']);
- return Container(
- decoration: BoxDecoration(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
- borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
- ),
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- // 拖拽指示条
- Container(
- width: 36,
- height: 4,
- decoration: BoxDecoration(
- color: cs.onSurface.withAlpha(60),
- borderRadius: BorderRadius.circular(2),
- ),
- ),
- const SizedBox(height: 16),
- // 分享卡片预览
- RepaintBoundary(
- key: _cardKey,
- child: _TradeShareCard(
- symbol: symbol,
- isLong: isLong,
- leverage: leverage,
- profitRateStr: profitRateStr,
- pnlPositive: pnlPositive,
- openPrice: openPrice,
- closePrice: closePrice,
- openTime: openTime,
- closeTime: closeTime,
- inviteCode: _inviteCode,
- inviteUrl: _inviteUrl,
- ),
- ),
- const SizedBox(height: 24),
- // 操作按钮行:取消 | 保存海报 | 分享
- Row(
- children: [
- Expanded(
- child: OutlinedButton(
- onPressed: () => Navigator.of(context).pop(),
- style: OutlinedButton.styleFrom(
- padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8)),
- ),
- child: Text(l10n.cancelLabel,
- style: TextStyle(color: cs.onSurface, fontSize: 14)),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: OutlinedButton(
- onPressed: _saving ? null : () => _doSave(context),
- style: OutlinedButton.styleFrom(
- padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8)),
- ),
- child: _saving
- ? SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: cs.onSurface.withAlpha(153)),
- )
- : Text(l10n.savePoster,
- style: TextStyle(color: cs.onSurface, fontSize: 14)),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: ElevatedButton(
- onPressed: _sharing ? null : () => _doShare(context),
- style: ElevatedButton.styleFrom(
- backgroundColor:
- pnlPositive ? AppColors.rise : AppColors.fall,
- padding: const EdgeInsets.symmetric(vertical: 12),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(8)),
- elevation: 0,
- ),
- child: _sharing
- ? const SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2, color: Colors.white),
- )
- : Text(l10n.shareLabel,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 14,
- fontWeight: FontWeight.w600)),
- ),
- ),
- ],
- ),
- ],
- ),
- );
- }
- }
- // ── 带单分享卡片内容 ─────────────────────────────────────────
- class _TradeShareCard extends StatelessWidget {
- const _TradeShareCard({
- required this.symbol,
- required this.isLong,
- required this.leverage,
- required this.profitRateStr,
- required this.pnlPositive,
- required this.openPrice,
- required this.closePrice,
- required this.openTime,
- required this.closeTime,
- this.inviteCode,
- this.inviteUrl,
- });
- final String symbol;
- final bool isLong;
- final String leverage;
- final String profitRateStr;
- final bool pnlPositive;
- final String openPrice;
- final String closePrice;
- final String openTime;
- final String closeTime;
- final String? inviteCode;
- final String? inviteUrl;
- String _baseCoin(String sym) {
- if (sym.contains('/')) return sym.split('/').first;
- return sym.toUpperCase().replaceFirst(RegExp(r'USDT$'), '');
- }
- @override
- Widget build(BuildContext context) {
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final l10n = AppLocalizations.of(context)!;
- final sideColor = isLong ? AppColors.rise : AppColors.fall;
- final pnlColor = pnlPositive ? AppColors.rise : AppColors.fall;
- final coinSymbol = _baseCoin(symbol);
- // 主题色变量
- final bgColors = isDark
- ? const [Color(0xFF1A1F2E), Color(0xFF0D1117)]
- : const [Color(0xFFF8F9FB), Color(0xFFEEF0F3)];
- final textPrimary = isDark ? Colors.white : const Color(0xFF1A1F2E);
- final textSecondary = isDark
- ? Colors.white.withAlpha(120)
- : const Color(0xFF1A1F2E).withAlpha(120);
- final textMuted = isDark
- ? Colors.white.withAlpha(80)
- : const Color(0xFF1A1F2E).withAlpha(80);
- final borderColor = isDark
- ? Colors.white.withAlpha(40)
- : const Color(0xFF1A1F2E).withAlpha(30);
- final qrFgColor = isDark ? Colors.white : Colors.black;
- final qrBgColor = isDark ? const Color(0xFF1A1F2E) : Colors.white;
- return Container(
- width: double.infinity,
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- colors: bgColors,
- ),
- borderRadius: BorderRadius.circular(16),
- ),
- clipBehavior: Clip.antiAlias,
- child: Padding(
- padding: const EdgeInsets.all(20),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // LOGO + 品牌名
- Row(
- children: [
- Image.asset(
- 'assets/images/app_icon.png',
- height: 28,
- width: 28,
- errorBuilder: (_, __, ___) => const SizedBox.shrink(),
- ),
- const SizedBox(width: 8),
- Text(
- 'iBit',
- style: TextStyle(
- color: textPrimary,
- fontSize: 14,
- fontWeight: FontWeight.w700,
- letterSpacing: 0.5),
- ),
- ],
- ),
- const SizedBox(height: 14),
- // 币对 + 永续 tag
- Row(
- children: [
- Text(
- '${coinSymbol}USDT',
- style: TextStyle(
- color: textPrimary,
- fontSize: 22,
- fontWeight: FontWeight.w800),
- ),
- const SizedBox(width: 8),
- Container(
- padding:
- const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
- decoration: BoxDecoration(
- color: const Color(0xFFFFAB00),
- borderRadius: BorderRadius.circular(4),
- ),
- child: Text(l10n.perpetual,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 11,
- fontWeight: FontWeight.w700)),
- ),
- ],
- ),
- const SizedBox(height: 4),
- // 方向 + 杠杆
- Text(
- '${isLong ? l10n.openLong : l10n.openShort} ${leverage}X',
- style: TextStyle(
- color: sideColor, fontSize: 15, fontWeight: FontWeight.w700),
- ),
- const SizedBox(height: 14),
- // 收益率(大字)
- Text(l10n.returnRate,
- style: TextStyle(color: textSecondary, fontSize: 12)),
- const SizedBox(height: 4),
- Text(profitRateStr,
- style: TextStyle(
- color: pnlColor,
- fontSize: 36,
- fontWeight: FontWeight.w800,
- letterSpacing: -0.5)),
- const SizedBox(height: 16),
- // 开仓均价 + 平仓均价
- Row(
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(l10n.openAvgPrice,
- style: TextStyle(color: textSecondary, fontSize: 11)),
- const SizedBox(height: 2),
- Text(openPrice,
- style: TextStyle(
- color: textPrimary,
- fontSize: 13,
- fontWeight: FontWeight.w600)),
- ],
- ),
- ),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(l10n.avgClosePrice,
- style: TextStyle(color: textSecondary, fontSize: 11)),
- const SizedBox(height: 2),
- Text(closePrice,
- style: TextStyle(
- color: textPrimary,
- fontSize: 13,
- fontWeight: FontWeight.w600)),
- ],
- ),
- ),
- ],
- ),
- const SizedBox(height: 10),
- // 时间
- Text(closeTime != '--' ? closeTime : openTime,
- style: TextStyle(color: textMuted, fontSize: 11)),
- const SizedBox(height: 14),
- // 分隔线
- Divider(color: borderColor, height: 1),
- const SizedBox(height: 14),
- // 邀请码 + 二维码
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (inviteCode != null)
- RichText(
- text: TextSpan(
- style: const TextStyle(fontSize: 15),
- children: [
- TextSpan(
- text: l10n.inviteCodeLabel,
- style: TextStyle(color: textSecondary),
- ),
- TextSpan(
- text: inviteCode!,
- style: const TextStyle(
- color: AppColors.brand,
- fontWeight: FontWeight.w700),
- ),
- ],
- ),
- ),
- const SizedBox(height: 4),
- Text(l10n.registerAndEarnRebate,
- style: TextStyle(color: textMuted, fontSize: 12)),
- ],
- ),
- ),
- Container(
- decoration: BoxDecoration(
- border: Border.all(color: borderColor, width: 1),
- borderRadius: BorderRadius.circular(6),
- ),
- padding: const EdgeInsets.all(4),
- child: inviteUrl != null
- ? QrImageView(
- data: inviteUrl!,
- version: QrVersions.auto,
- size: 80,
- eyeStyle: QrEyeStyle(
- eyeShape: QrEyeShape.square,
- color: qrFgColor,
- ),
- dataModuleStyle: QrDataModuleStyle(
- dataModuleShape: QrDataModuleShape.square,
- color: qrFgColor,
- ),
- backgroundColor: qrBgColor,
- errorCorrectionLevel: QrErrorCorrectLevel.M,
- )
- : const SizedBox(width: 80, height: 80),
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- }
|