asset_screen.dart 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import '../../../core/l10n/app_localizations.dart';
  4. import '../../../core/theme/app_colors.dart';
  5. import '../../../core/utils/number_format.dart';
  6. import '../../../providers/asset_provider.dart';
  7. import '../../../providers/my_copy_trading_provider.dart';
  8. import '../../widgets/common/app_shimmer.dart';
  9. import '../../widgets/common/app_tab_bar.dart';
  10. import '../../widgets/common/network_error_body.dart';
  11. import 'asset_overview_tab.dart';
  12. import 'asset_futures_tab.dart';
  13. import 'asset_copy_trading_tab.dart';
  14. import 'asset_spot_tab.dart';
  15. import 'asset_spot_trading_tab.dart';
  16. import 'asset_staking_tab.dart';
  17. class AssetScreen extends ConsumerStatefulWidget {
  18. const AssetScreen({super.key});
  19. @override
  20. ConsumerState<AssetScreen> createState() => _AssetScreenState();
  21. }
  22. class _AssetScreenState extends ConsumerState<AssetScreen>
  23. with SingleTickerProviderStateMixin {
  24. late TabController _tabController;
  25. late PageController _pageController;
  26. List<String> _getLocalizedTabs(BuildContext context) {
  27. final l10n = AppLocalizations.of(context)!;
  28. return [
  29. l10n.assetOverview,
  30. l10n.fund,
  31. l10n.spotTab,
  32. l10n.futures,
  33. l10n.copyTrading,
  34. l10n.assetTabLocked,
  35. ];
  36. }
  37. @override
  38. void initState() {
  39. super.initState();
  40. _tabController = TabController(length: 6, vsync: this);
  41. _pageController = PageController();
  42. // 拖动 PageView → 实时更新指示器 offset(平滑插值)
  43. _pageController.addListener(() {
  44. if (!_pageController.hasClients) return;
  45. final page = _pageController.page!;
  46. final offset = page - _tabController.index;
  47. if (offset.abs() <= 1.0 && !_tabController.indexIsChanging) {
  48. _tabController.offset = offset.clamp(-1.0, 1.0);
  49. }
  50. });
  51. _tabController.addListener(_onTabChanged);
  52. }
  53. void _onTabChanged() {
  54. if (_tabController.indexIsChanging) {
  55. // Tab 点击 → 驱动 PageView 动画
  56. _pageController.animateToPage(
  57. _tabController.index,
  58. duration: const Duration(milliseconds: 280),
  59. curve: Curves.easeOut,
  60. );
  61. } else {
  62. // Tab 切换完成:更新当前子 tab 索引并按需刷新
  63. final idx = _tabController.index;
  64. final prevIdx = ref.read(currentAssetSubTabProvider);
  65. ref.read(currentAssetSubTabProvider.notifier).state = idx;
  66. if (prevIdx == 2 && idx != 2) {
  67. ref.read(assetProvider.notifier).onSpotTradingTabHidden();
  68. }
  69. WidgetsBinding.instance.addPostFrameCallback((_) {
  70. if (mounted) _refreshForTab(idx);
  71. });
  72. // 仅合约 tab(索引 3)需要定时拉持仓;现货 tab 首次进入 HTTP,后续走 WS
  73. final notifier = ref.read(assetProvider.notifier);
  74. if (idx == 3) {
  75. notifier.startPositionPolling();
  76. } else {
  77. notifier.stopPositionPolling();
  78. }
  79. }
  80. setState(() {});
  81. }
  82. void _refreshForTab(int tabIndex) {
  83. final n = ref.read(assetProvider.notifier);
  84. if (tabIndex == 2) {
  85. n.onSpotTradingTabVisible();
  86. } else {
  87. n.silentRefresh();
  88. }
  89. if (tabIndex == 4) {
  90. // 跟单 tab 额外刷新跟单仓位
  91. ref.read(myCopyTradingProvider.notifier).silentRefresh();
  92. } else if (tabIndex == 5) {
  93. n.loadStakingOrders();
  94. }
  95. }
  96. void _navigateToTab(int index) {
  97. _tabController.animateTo(index);
  98. _pageController.animateToPage(
  99. index,
  100. duration: const Duration(milliseconds: 280),
  101. curve: Curves.easeOut,
  102. );
  103. }
  104. @override
  105. void dispose() {
  106. ref.read(assetProvider.notifier).onSpotTradingTabHidden();
  107. ref.read(assetProvider.notifier).stopPositionPolling();
  108. _tabController.dispose();
  109. _pageController.dispose();
  110. super.dispose();
  111. }
  112. String _getTabLabel(int index) {
  113. const labels = [
  114. 'asset_tab_overview',
  115. 'asset_tab_funds',
  116. 'asset_tab_spot_trading',
  117. 'asset_tab_futures',
  118. 'asset_tab_copy_trading',
  119. 'asset_tab_staking',
  120. ];
  121. return labels[index];
  122. }
  123. @override
  124. Widget build(BuildContext context) {
  125. final state = ref.watch(assetProvider);
  126. final notifier = ref.read(assetProvider.notifier);
  127. final isChineseLocale =
  128. Localizations.localeOf(context).languageCode == 'zh';
  129. final tabs = _getLocalizedTabs(context);
  130. return Scaffold(
  131. body: SafeArea(
  132. child: Column(
  133. children: [
  134. // ── Tab 栏 ─────────────────────────────────────
  135. TabBar(
  136. controller: _tabController,
  137. isScrollable: !isChineseLocale,
  138. indicator: StretchTabIndicator(
  139. controller: _tabController,
  140. color: AppColors.brand,
  141. height: 3,
  142. borderRadius: 1.5,
  143. ),
  144. indicatorSize: TabBarIndicatorSize.label,
  145. labelStyle:
  146. const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
  147. unselectedLabelStyle:
  148. const TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
  149. tabAlignment:
  150. isChineseLocale ? TabAlignment.fill : TabAlignment.start,
  151. dividerColor: Colors.transparent,
  152. labelPadding: isChineseLocale
  153. ? EdgeInsets.zero
  154. : const EdgeInsets.symmetric(horizontal: 14),
  155. padding: isChineseLocale
  156. ? const EdgeInsets.only(top: 8)
  157. : const EdgeInsets.fromLTRB(4, 8, 16, 0),
  158. tabs: <Widget>[
  159. for (int i = 0; i < 6; i++)
  160. Semantics(
  161. label: _getTabLabel(i),
  162. button: true,
  163. child: Tab(text: tabs[i]),
  164. ),
  165. ],
  166. ),
  167. // ── Tab 内容 ───────────────────────────────────
  168. Expanded(
  169. child: _buildContent(state, notifier),
  170. ),
  171. ],
  172. ),
  173. ),
  174. );
  175. }
  176. Widget _buildContent(AssetState state, AssetNotifier notifier) {
  177. if (state.isLoading) return const _AssetShimmer();
  178. if (state.errorMessage != null) {
  179. return NetworkErrorBody(onRetry: notifier.loadAssets);
  180. }
  181. return PageView(
  182. controller: _pageController,
  183. physics: const BouncingScrollPhysics(
  184. parent: AlwaysScrollableScrollPhysics(),
  185. ),
  186. onPageChanged: (index) {
  187. if (_tabController.indexIsChanging) return;
  188. _tabController.index = index;
  189. },
  190. children: [
  191. AssetOverviewTab(
  192. state: state,
  193. notifier: notifier,
  194. onNavigateToStaking: () => _navigateToTab(5),
  195. ),
  196. AssetSpotTab(state: state, notifier: notifier),
  197. AssetSpotTradingTab(state: state, notifier: notifier),
  198. AssetFuturesTab(state: state, notifier: notifier),
  199. AssetCopyTradingTab(state: state, notifier: notifier),
  200. AssetStakingTab(state: state, notifier: notifier),
  201. ],
  202. );
  203. }
  204. }
  205. // ── 资产骨架屏 ──────────────────────────────────────────────
  206. class _AssetShimmer extends StatelessWidget {
  207. const _AssetShimmer();
  208. @override
  209. Widget build(BuildContext context) {
  210. return AppShimmer(
  211. child: SingleChildScrollView(
  212. physics: const NeverScrollableScrollPhysics(),
  213. padding: const EdgeInsets.all(16),
  214. child: Column(
  215. crossAxisAlignment: CrossAxisAlignment.start,
  216. children: [
  217. // 资产估值头部
  218. shimmerBox(80, 13),
  219. const SizedBox(height: 10),
  220. shimmerBox(180, 34),
  221. const SizedBox(height: 6),
  222. shimmerBox(120, 13),
  223. const SizedBox(height: 24),
  224. // 账户卡片列表
  225. ...List.generate(
  226. 3,
  227. (_) => Padding(
  228. padding: const EdgeInsets.only(bottom: 12),
  229. child: Container(
  230. padding: const EdgeInsets.symmetric(
  231. horizontal: 16, vertical: 16),
  232. decoration: BoxDecoration(
  233. color: Colors.white,
  234. borderRadius: BorderRadius.circular(12),
  235. ),
  236. child: Row(
  237. children: [
  238. shimmerCircle(22),
  239. const SizedBox(width: 12),
  240. shimmerBox(80, 14),
  241. const Spacer(),
  242. Column(
  243. crossAxisAlignment: CrossAxisAlignment.end,
  244. children: [
  245. shimmerBox(90, 15),
  246. const SizedBox(height: 4),
  247. shimmerBox(60, 12),
  248. ],
  249. ),
  250. const SizedBox(width: 8),
  251. shimmerBox(18, 18, radius: 4),
  252. ],
  253. ),
  254. ),
  255. )),
  256. const SizedBox(height: 8),
  257. // 持仓卡片
  258. Container(
  259. padding: const EdgeInsets.all(16),
  260. decoration: BoxDecoration(
  261. color: Colors.white,
  262. borderRadius: BorderRadius.circular(12),
  263. ),
  264. child: Column(
  265. children: [
  266. Row(
  267. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  268. children: [shimmerBox(100, 14), shimmerBox(60, 14)],
  269. ),
  270. const SizedBox(height: 12),
  271. Row(
  272. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  273. children:
  274. List.generate(3, (_) => shimmerBox(70, 40, radius: 6)),
  275. ),
  276. ],
  277. ),
  278. ),
  279. ],
  280. ),
  281. ),
  282. );
  283. }
  284. }
  285. // ══════════════════════════════════════════════════════════════
  286. // 共享组件(供各 Tab 文件使用)
  287. // ══════════════════════════════════════════════════════════════
  288. /// 各 Tab 通用的资产估值头部
  289. class AssetHeader extends StatelessWidget {
  290. const AssetHeader({super.key, required this.state, required this.notifier});
  291. final AssetState state;
  292. final AssetNotifier notifier;
  293. @override
  294. Widget build(BuildContext context) {
  295. final cs = Theme.of(context).colorScheme;
  296. final total = state.totalUsdtValue;
  297. final display =
  298. state.obscureBalance ? '******' : formatPrice(total, decimalPlaces: 2);
  299. return Padding(
  300. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  301. child: Column(
  302. crossAxisAlignment: CrossAxisAlignment.start,
  303. children: [
  304. Row(
  305. children: [
  306. Text(AppLocalizations.of(context)!.assetValuation,
  307. style: TextStyle(
  308. color: cs.onSurface.withAlpha(153), fontSize: 13)),
  309. const SizedBox(width: 6),
  310. Semantics(
  311. label: 'asset_btn_toggle_balance',
  312. button: true,
  313. enabled: true,
  314. onTap: notifier.toggleObscure,
  315. child: GestureDetector(
  316. onTap: notifier.toggleObscure,
  317. child: Icon(
  318. state.obscureBalance
  319. ? Icons.visibility_off_outlined
  320. : Icons.visibility_outlined,
  321. size: 16,
  322. color: cs.onSurface.withAlpha(153),
  323. ),
  324. ),
  325. ),
  326. ],
  327. ),
  328. const SizedBox(height: 8),
  329. Row(
  330. crossAxisAlignment: CrossAxisAlignment.end,
  331. children: [
  332. Semantics(
  333. label: 'asset_text_total_value',
  334. enabled: true,
  335. child: Text(display,
  336. style: TextStyle(
  337. color: cs.onSurface,
  338. fontSize: 32,
  339. fontWeight: FontWeight.w700,
  340. letterSpacing: -0.5)),
  341. ),
  342. const SizedBox(width: 6),
  343. Semantics(
  344. label: 'asset_text_currency',
  345. enabled: true,
  346. child: Padding(
  347. padding: const EdgeInsets.only(bottom: 5),
  348. child: Text('USDT',
  349. style: TextStyle(
  350. color: cs.onSurface.withAlpha(153), fontSize: 14)),
  351. ),
  352. ),
  353. ],
  354. ),
  355. ],
  356. ),
  357. );
  358. }
  359. }
  360. /// 合约/跟单持仓卡片
  361. class AssetPositionCard extends StatelessWidget {
  362. const AssetPositionCard({
  363. super.key,
  364. required this.obscure,
  365. this.symbol = 'BTCUSDT',
  366. this.isLong = true,
  367. this.positionType = '',
  368. this.leverage = '20X',
  369. this.pnl = '+24.35',
  370. this.pnlColor,
  371. this.roi = '+5.36%',
  372. this.rows = const [],
  373. this.onShare,
  374. });
  375. final bool obscure;
  376. final String symbol;
  377. final bool isLong;
  378. final String positionType;
  379. final String leverage;
  380. final String pnl;
  381. final Color? pnlColor;
  382. final String roi;
  383. final List<List<String>> rows;
  384. final VoidCallback? onShare;
  385. @override
  386. Widget build(BuildContext context) {
  387. final cs = Theme.of(context).colorScheme;
  388. final isDark = Theme.of(context).brightness == Brightness.dark;
  389. final sideColor = isLong ? AppColors.rise : AppColors.fall;
  390. final effectivePnlColor = pnlColor ?? AppColors.rise;
  391. return Container(
  392. margin: const EdgeInsets.symmetric(horizontal: 16),
  393. padding: const EdgeInsets.all(16),
  394. decoration: BoxDecoration(
  395. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  396. borderRadius: BorderRadius.circular(12),
  397. border: isDark
  398. ? null
  399. : Border.all(color: AppColors.lightBorder, width: 0.5),
  400. ),
  401. child: Column(
  402. crossAxisAlignment: CrossAxisAlignment.start,
  403. children: [
  404. // 头部:币种 + 标签
  405. Row(
  406. children: [
  407. Container(
  408. width: 32,
  409. height: 32,
  410. decoration: BoxDecoration(
  411. color: sideColor, borderRadius: BorderRadius.circular(8)),
  412. child: Center(
  413. child: Text(
  414. isLong
  415. ? AppLocalizations.of(context)!.longLabel
  416. : AppLocalizations.of(context)!.shortLabel,
  417. style: const TextStyle(
  418. color: Colors.white,
  419. fontSize: 13,
  420. fontWeight: FontWeight.w700)),
  421. ),
  422. ),
  423. const SizedBox(width: 10),
  424. Text(symbol,
  425. style: TextStyle(
  426. color: cs.onSurface,
  427. fontSize: 15,
  428. fontWeight: FontWeight.w600)),
  429. const SizedBox(width: 8),
  430. _Tag(label: AppLocalizations.of(context)!.perpetual),
  431. const SizedBox(width: 4),
  432. _Tag(label: positionType),
  433. const SizedBox(width: 4),
  434. Container(
  435. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  436. decoration: BoxDecoration(
  437. color: cs.onSurface.withAlpha(20),
  438. borderRadius: BorderRadius.circular(4),
  439. ),
  440. child: Text(leverage,
  441. style: TextStyle(
  442. color: cs.onSurface,
  443. fontSize: 11,
  444. fontWeight: FontWeight.w600)),
  445. ),
  446. const Spacer(),
  447. GestureDetector(
  448. onTap: onShare,
  449. child: Icon(Icons.share_outlined,
  450. size: 18, color: cs.onSurface.withAlpha(153)),
  451. ),
  452. ],
  453. ),
  454. const SizedBox(height: 14),
  455. // 未实现盈亏 + 收益率
  456. Container(
  457. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  458. decoration: const BoxDecoration(),
  459. child: Row(
  460. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  461. children: [
  462. Column(
  463. crossAxisAlignment: CrossAxisAlignment.start,
  464. children: [
  465. Text(
  466. '${AppLocalizations.of(context)!.unrealizedPnl} (USDT)',
  467. style: TextStyle(
  468. color: cs.onSurface.withAlpha(153), fontSize: 11)),
  469. const SizedBox(height: 2),
  470. Text(
  471. obscure ? '******' : pnl,
  472. style: TextStyle(
  473. color: effectivePnlColor,
  474. fontSize: 20,
  475. fontWeight: FontWeight.w700),
  476. ),
  477. ],
  478. ),
  479. Column(
  480. crossAxisAlignment: CrossAxisAlignment.end,
  481. children: [
  482. Text(AppLocalizations.of(context)!.returnRate,
  483. style: TextStyle(
  484. color: cs.onSurface.withAlpha(153), fontSize: 11)),
  485. const SizedBox(height: 2),
  486. Text(roi,
  487. style: TextStyle(
  488. color: effectivePnlColor,
  489. fontSize: 16,
  490. fontWeight: FontWeight.w600)),
  491. ],
  492. ),
  493. ],
  494. ),
  495. ),
  496. const SizedBox(height: 14),
  497. // 数据网格
  498. _DataGrid(obscure: obscure, rows: rows),
  499. ],
  500. ),
  501. );
  502. }
  503. }
  504. /// 账户行
  505. class AssetAccountRow extends StatelessWidget {
  506. const AssetAccountRow({
  507. super.key,
  508. required this.icon,
  509. required this.label,
  510. required this.amount,
  511. required this.usdAmount,
  512. this.onTransfer,
  513. this.onTap,
  514. });
  515. final IconData icon;
  516. final String label;
  517. final String amount;
  518. final String usdAmount;
  519. final VoidCallback? onTransfer;
  520. final VoidCallback? onTap;
  521. @override
  522. Widget build(BuildContext context) {
  523. final cs = Theme.of(context).colorScheme;
  524. final isDark = Theme.of(context).brightness == Brightness.dark;
  525. return GestureDetector(
  526. onTap: onTap,
  527. behavior: HitTestBehavior.opaque,
  528. child: Container(
  529. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  530. decoration: BoxDecoration(
  531. color:
  532. isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  533. borderRadius: BorderRadius.circular(12),
  534. border: isDark
  535. ? null
  536. : Border.all(color: AppColors.lightBorder, width: 0.5),
  537. ),
  538. child: Row(
  539. children: [
  540. Icon(icon, color: cs.onSurface.withAlpha(153), size: 22),
  541. const SizedBox(width: 12),
  542. Expanded(
  543. child: Text(label,
  544. style: TextStyle(
  545. color: cs.onSurface,
  546. fontSize: 14,
  547. fontWeight: FontWeight.w500)),
  548. ),
  549. if (onTransfer != null) ...[
  550. OutlinedButton(
  551. onPressed: onTransfer,
  552. style: OutlinedButton.styleFrom(
  553. side: const BorderSide(color: AppColors.brand),
  554. foregroundColor: AppColors.brand,
  555. minimumSize: const Size(0, 32),
  556. padding: const EdgeInsets.symmetric(horizontal: 12),
  557. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  558. ),
  559. child: Text(
  560. AppLocalizations.of(context)!.transfer,
  561. style: const TextStyle(fontSize: 12),
  562. ),
  563. ),
  564. const SizedBox(width: 12),
  565. ],
  566. Column(
  567. crossAxisAlignment: CrossAxisAlignment.end,
  568. children: [
  569. Text(amount,
  570. style: TextStyle(
  571. color: cs.onSurface,
  572. fontSize: 15,
  573. fontWeight: FontWeight.w600)),
  574. if (usdAmount.isNotEmpty)
  575. Text(usdAmount,
  576. style: TextStyle(
  577. color: cs.onSurface.withAlpha(120), fontSize: 12)),
  578. ],
  579. ),
  580. const SizedBox(width: 8),
  581. Icon(
  582. Icons.chevron_right,
  583. size: 18,
  584. color: cs.onSurface.withAlpha(102),
  585. ),
  586. ],
  587. ),
  588. ),
  589. );
  590. }
  591. }
  592. // ── 内部私有组件 ──────────────────────────────────────────
  593. class _Tag extends StatelessWidget {
  594. const _Tag({required this.label});
  595. final String label;
  596. @override
  597. Widget build(BuildContext context) {
  598. final cs = Theme.of(context).colorScheme;
  599. return Container(
  600. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  601. decoration: BoxDecoration(
  602. border: Border.all(color: cs.onSurface.withAlpha(60)),
  603. borderRadius: BorderRadius.circular(4),
  604. ),
  605. child: Text(label,
  606. style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 10)),
  607. );
  608. }
  609. }
  610. class _DataGrid extends StatelessWidget {
  611. const _DataGrid({required this.obscure, required this.rows});
  612. final bool obscure;
  613. final List<List<String>> rows;
  614. @override
  615. Widget build(BuildContext context) {
  616. final cs = Theme.of(context).colorScheme;
  617. return Column(
  618. children: rows.map((row) {
  619. return Padding(
  620. padding: const EdgeInsets.only(bottom: 12),
  621. child: Row(
  622. children: [
  623. for (int i = 0; i < row.length; i += 2) ...[
  624. Expanded(
  625. child: Column(
  626. crossAxisAlignment: i == 0
  627. ? CrossAxisAlignment.start
  628. : i + 2 >= row.length
  629. ? CrossAxisAlignment.end
  630. : CrossAxisAlignment.center,
  631. children: [
  632. Text(row[i],
  633. style: TextStyle(
  634. color: cs.onSurface.withAlpha(120), fontSize: 11),
  635. textAlign: i == 0
  636. ? TextAlign.left
  637. : i + 2 >= row.length
  638. ? TextAlign.right
  639. : TextAlign.center),
  640. const SizedBox(height: 2),
  641. Text(
  642. obscure ? '***' : row[i + 1],
  643. style: TextStyle(
  644. color: cs.onSurface,
  645. fontSize: 13,
  646. fontWeight: FontWeight.w500),
  647. textAlign: i == 0
  648. ? TextAlign.left
  649. : i + 2 >= row.length
  650. ? TextAlign.right
  651. : TextAlign.center,
  652. ),
  653. ],
  654. ),
  655. ),
  656. ],
  657. ],
  658. ),
  659. );
  660. }).toList(),
  661. );
  662. }
  663. }