Переглянути джерело

update 首页和资产也样式修改

maotouying_cc 1 місяць тому
батько
коміт
650bc42e32

+ 1 - 1
lib/core/config/app_config.dart

@@ -102,7 +102,7 @@ abstract class AppConfig {
   );
 
   /// 应用版本号(与 pubspec.yaml version 保持一致)
-  static const String appVersion = '4.1.4';
+  static const String appVersion = '4.1.5';
 
   /// 是否启用 Mock 模式
   static const bool enableMock = bool.fromEnvironment(

+ 13 - 12
lib/core/constants/market_list_layout.dart

@@ -1,16 +1,17 @@
 /// 首页与「行情」Tab 列表共用布局常数。
 ///
-/// 行结构:`Expanded(名称区)` → [kMarketListNameToPriceGap] → `最新价(自然宽度)`
-/// → `Spacer` → 间距 → 涨跌幅块。弹性空白在「最新价」与「涨跌幅」之间,
-/// 避免「名称 | 最新价」各占 Expanded 时中间出现大块死区。
-const double kMarketListChangeBadgeWidth = 80;
-const double kMarketListPriceToBadgeGap = 8;
+/// `Expanded(leading+名称)` | 固定宽最新价列 | 间距 | 固定宽涨跌幅列
+/// 表头与数据行结构一致,保证「最新价 / 24h涨跌幅」列对齐。
+const double kMarketListChangeBadgeWidth = 72;
 
-/// 名称列右缘与最新价左缘的固定小间距(非弹性)。
-const double kMarketListNameToPriceGap = 10;
+/// 最新价列固定宽度(表头与数据共用)。
+const double kMarketListPriceColumnWidth = 108;
 
-/// 左侧「图标 + 名称」与「最新价↔涨跌幅」之间的 Spacer 的 flex 比例。
-/// 名称区 flex 必须显著大于 Spacer,否则名称 [Expanded] 过窄会出现大面积「…」截断;
-/// 弹性留白仍主要落在「最新价」与涨跌幅之间,不会回到「名称 | 最新价」双 Expanded 的死区。
-const int kMarketListNameClusterFlex = 5;
-const int kMarketListPriceTailSpacerFlex = 1;
+/// 最新价列右缘与涨跌幅块左缘的间距。
+const double kMarketListPriceToBadgeGap = 24;
+
+/// 列表行:图标/占位与名称文字之间的间距。
+const double kMarketListLeadingToNameGap = 10;
+
+/// 首页表头占位:与 36 图标 + 10 间距对齐。
+const double kMarketListHeaderLeadingWidth = 46;

+ 8 - 0
lib/presentation/screens/asset/asset_screen.dart

@@ -232,6 +232,8 @@ class _AssetShimmer extends StatelessWidget {
                         shimmerBox(60, 12),
                       ],
                     ),
+                    const SizedBox(width: 8),
+                    shimmerBox(18, 18, radius: 4),
                   ],
                 ),
               ),
@@ -512,6 +514,12 @@ class AssetAccountRow extends StatelessWidget {
                 Text(usdAmount, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12)),
             ],
           ),
+          const SizedBox(width: 8),
+          Icon(
+            Icons.chevron_right,
+            size: 18,
+            color: cs.onSurface.withAlpha(102),
+          ),
         ],
       ),
     );

+ 202 - 269
lib/presentation/screens/home/home_screen.dart

@@ -9,7 +9,6 @@ import 'package:shimmer/shimmer.dart';
 import 'package:url_launcher/url_launcher.dart';
 
 import '../../../core/config/app_config.dart';
-import '../../../core/constants/market_list_layout.dart';
 import '../../../core/l10n/app_localizations.dart';
 import '../../../core/theme/app_colors.dart';
 import '../../../core/navigation/broker_navigation.dart';
@@ -31,6 +30,7 @@ import '../../../providers/market_provider.dart'
 import '../../widgets/common/app_refresh_indicator.dart';
 import '../../widgets/common/coin_icon.dart';
 import '../../widgets/common/update_dialog.dart';
+import '../../widgets/market_list_row_layout.dart';
 import 'activity_carousel.dart';
 import 'top_traders_section.dart';
 
@@ -343,54 +343,39 @@ class _HomeShimmer extends StatelessWidget {
             itemCount: 6,
             itemBuilder: (_, __) => Padding(
               padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-              child: Row(
-                children: [
-                  Expanded(
-                    flex: kMarketListNameClusterFlex,
-                    child: Row(
-                      children: [
-                        Container(
-                          width: 36,
-                          height: 36,
-                          decoration: BoxDecoration(
-                            color: Colors.white,
-                            borderRadius: BorderRadius.circular(10),
-                          ),
-                        ),
-                        const SizedBox(width: 10),
-                        Expanded(
-                          child: Column(
-                            crossAxisAlignment: CrossAxisAlignment.start,
-                            children: [
-                              _shimmerBox(70, 14),
-                              const SizedBox(height: 6),
-                              _shimmerBox(50, 11),
-                            ],
-                          ),
-                        ),
-                      ],
-                    ),
-                  ),
-                  SizedBox(width: kMarketListNameToPriceGap),
-                  Column(
-                    crossAxisAlignment: CrossAxisAlignment.end,
-                    children: [
-                      _shimmerBox(80, 14),
-                      const SizedBox(height: 6),
-                      _shimmerBox(50, 11),
-                    ],
+              child: MarketListDataRow(
+                leading: Container(
+                  width: 36,
+                  height: 36,
+                  decoration: BoxDecoration(
+                    color: Colors.white,
+                    borderRadius: BorderRadius.circular(10),
                   ),
-                  Spacer(flex: kMarketListPriceTailSpacerFlex),
-                  SizedBox(width: kMarketListPriceToBadgeGap),
-                  Container(
-                    width: kMarketListChangeBadgeWidth,
-                    height: 30,
-                    decoration: BoxDecoration(
-                      color: Colors.white,
-                      borderRadius: BorderRadius.circular(6),
-                    ),
+                ),
+                name: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    _shimmerBox(70, 14),
+                    const SizedBox(height: 6),
+                    _shimmerBox(50, 11),
+                  ],
+                ),
+                price: Column(
+                  crossAxisAlignment: CrossAxisAlignment.end,
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    _shimmerBox(80, 14),
+                    const SizedBox(height: 6),
+                    _shimmerBox(50, 11),
+                  ],
+                ),
+                change: Container(
+                  height: 30,
+                  decoration: BoxDecoration(
+                    color: Colors.white,
+                    borderRadius: BorderRadius.circular(6),
                   ),
-                ],
+                ),
               ),
             ),
           ),
@@ -1288,106 +1273,84 @@ class _HomeSpotTickerRow extends ConsumerWidget {
       onTap: () => context.push('/market/spot/${ticker.symbol}'),
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        child: Row(
-          children: [
-            Expanded(
-              flex: kMarketListNameClusterFlex,
-              child: Row(
-                children: [
-                  CoinIcon(
-                    symbol: ticker.baseAsset,
-                    iconUrl: ticker.icon,
-                    size: 36,
-                    borderRadius: 10,
-                  ),
-                  const SizedBox(width: 10),
-                  Expanded(
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        Text(
-                          formatUsdtPairDisplay(ticker.symbol),
-                          style: TextStyle(
-                            color: cs.onSurface,
-                            fontSize: 14,
-                            fontWeight: FontWeight.w500,
-                          ),
-                          maxLines: 1,
-                          overflow: TextOverflow.ellipsis,
-                        ),
-                        Text(
-                          AppLocalizations.of(context)!.spot,
-                          style: TextStyle(
-                            color: cs.onSurface.withAlpha(153),
-                            fontSize: 11,
-                          ),
-                        ),
-                      ],
-                    ),
-                  ),
-                ],
-              ),
-            ),
-            SizedBox(width: kMarketListNameToPriceGap),
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.end,
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(
-                  ticker.lastPrice > 0
-                      ? (ticker.lastPriceStr != null
-                          ? formatRawPrice(ticker.lastPriceStr!)
-                          : formatPrice(ticker.lastPrice))
-                      : '--',
-                  style: TextStyle(
-                    color: cs.onSurface,
-                    fontSize: 14,
-                    fontWeight: FontWeight.w500,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+        child: MarketListDataRow(
+          leading: CoinIcon(
+            symbol: ticker.baseAsset,
+            iconUrl: ticker.icon,
+            size: 36,
+            borderRadius: 10,
+          ),
+          name: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                formatUsdtPairDisplay(ticker.symbol),
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 14,
+                  fontWeight: FontWeight.w500,
                 ),
-                Text(
-                  ticker.lastPrice > 0
-                      ? formatFiatPrice(ticker.lastPrice)
-                      : '--',
-                  style: TextStyle(
-                    color: cs.onSurface.withAlpha(153),
-                    fontSize: 11,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                AppLocalizations.of(context)!.spot,
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(153),
+                  fontSize: 11,
                 ),
-              ],
-            ),
-            Spacer(flex: kMarketListPriceTailSpacerFlex),
-            SizedBox(width: kMarketListPriceToBadgeGap),
-            SizedBox(
-              width: kMarketListChangeBadgeWidth,
-              child: Container(
-                height: 34,
-                alignment: Alignment.center,
-                decoration: BoxDecoration(
-                  color: changeColor,
-                  borderRadius: BorderRadius.circular(6),
+              ),
+            ],
+          ),
+          price: Column(
+            crossAxisAlignment: CrossAxisAlignment.end,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text(
+                ticker.lastPrice > 0
+                    ? (ticker.lastPriceStr != null
+                        ? formatRawPrice(ticker.lastPriceStr!)
+                        : formatPrice(ticker.lastPrice))
+                    : '--',
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 14,
+                  fontWeight: FontWeight.w500,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
-                child: Text(
-                  changeStr,
-                  style: const TextStyle(
-                    color: Colors.white,
-                    fontSize: 13,
-                    fontWeight: FontWeight.w600,
-                    fontFeatures: [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.center,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                ticker.lastPrice > 0 ? formatFiatPrice(ticker.lastPrice) : '--',
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(153),
+                  fontSize: 11,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+            ],
+          ),
+          change: Container(
+            height: 34,
+            alignment: Alignment.center,
+            decoration: BoxDecoration(
+              color: changeColor,
+              borderRadius: BorderRadius.circular(6),
+            ),
+            child: Text(
+              changeStr,
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 13,
+                fontWeight: FontWeight.w600,
+                fontFeatures: [FontFeature.tabularFigures()],
               ),
+              textAlign: TextAlign.center,
             ),
-          ],
+          ),
         ),
       ),
     );
@@ -1402,42 +1365,31 @@ class _MarketListHeader extends StatelessWidget {
     final cs = Theme.of(context).colorScheme;
     return Padding(
       padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
-      child: Row(
-        children: [
-          const SizedBox(width: 46),
-          Expanded(
-            flex: kMarketListNameClusterFlex,
-            child: Text(
-              AppLocalizations.of(context)!.coinNameLabel,
-              style: TextStyle(
-                color: cs.onSurface.withAlpha(153),
-                fontSize: 12,
-              ),
-            ),
+      child: MarketListHeaderRow(
+        leading: const SizedBox(width: 36),
+        name: Text(
+          AppLocalizations.of(context)!.coinNameLabel,
+          style: TextStyle(
+            color: cs.onSurface.withAlpha(153),
+            fontSize: 12,
           ),
-          SizedBox(width: kMarketListNameToPriceGap),
-          Text(
-            AppLocalizations.of(context)!.latestPrice,
-            textAlign: TextAlign.end,
-            style: TextStyle(
-              color: cs.onSurface.withAlpha(153),
-              fontSize: 12,
-            ),
+        ),
+        price: Text(
+          AppLocalizations.of(context)!.latestPrice,
+          textAlign: TextAlign.end,
+          style: TextStyle(
+            color: cs.onSurface.withAlpha(153),
+            fontSize: 12,
           ),
-          Spacer(flex: kMarketListPriceTailSpacerFlex),
-          SizedBox(width: kMarketListPriceToBadgeGap),
-          SizedBox(
-            width: kMarketListChangeBadgeWidth,
-            child: Text(
-              AppLocalizations.of(context)!.change24h,
-              textAlign: TextAlign.center,
-              style: TextStyle(
-                color: cs.onSurface.withAlpha(153),
-                fontSize: 12,
-              ),
-            ),
+        ),
+        change: Text(
+          AppLocalizations.of(context)!.change24h,
+          textAlign: TextAlign.center,
+          style: TextStyle(
+            color: cs.onSurface.withAlpha(153),
+            fontSize: 12,
           ),
-        ],
+        ),
       ),
     );
   }
@@ -1473,107 +1425,88 @@ class _MarketTickerRow extends StatelessWidget {
       onTap: onTap,
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        child: Row(
-          children: [
-            Expanded(
-              flex: kMarketListNameClusterFlex,
-              child: Row(
-                children: [
-                  CoinIcon(
-                      symbol: ticker.baseAsset,
-                      iconUrl: ticker.icon,
-                      size: 36,
-                      borderRadius: 10),
-                  const SizedBox(width: 10),
-                  Expanded(
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        Text(
-                          formatUsdtPairDisplay(ticker.symbol),
-                          style: TextStyle(
-                            color: cs.onSurface,
-                            fontSize: 14,
-                            fontWeight: FontWeight.w500,
-                          ),
-                          maxLines: 1,
-                          overflow: TextOverflow.ellipsis,
-                        ),
-                        Text(
-                          ticker.isFutures
-                              ? '${AppLocalizations.of(context)!.turnover} $volStr'
-                              : AppLocalizations.of(context)!.spot,
-                          style: TextStyle(
-                            color: cs.onSurface.withAlpha(153),
-                            fontSize: 11,
-                          ),
-                          maxLines: 1,
-                          overflow: TextOverflow.ellipsis,
-                        ),
-                      ],
-                    ),
-                  ),
-                ],
-              ),
-            ),
-            SizedBox(width: kMarketListNameToPriceGap),
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.end,
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(
-                  ticker.lastPriceStr != null
-                      ? formatRawPrice(ticker.lastPriceStr!)
-                      : formatPrice(ticker.lastPrice,
-                          decimalPlaces: ticker.pricePrecision),
-                  style: TextStyle(
-                    color: cs.onSurface,
-                    fontSize: 14,
-                    fontWeight: FontWeight.w500,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+        child: MarketListDataRow(
+          leading: CoinIcon(
+            symbol: ticker.baseAsset,
+            iconUrl: ticker.icon,
+            size: 36,
+            borderRadius: 10,
+          ),
+          name: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                formatUsdtPairDisplay(ticker.symbol),
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 14,
+                  fontWeight: FontWeight.w500,
                 ),
-                Text(
-                  formatFiatPrice(ticker.lastPrice,
-                      pricePrecision: ticker.pricePrecision),
-                  style: TextStyle(
-                    color: cs.onSurface.withAlpha(153),
-                    fontSize: 11,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                ticker.isFutures
+                    ? '${AppLocalizations.of(context)!.turnover} $volStr'
+                    : AppLocalizations.of(context)!.spot,
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(153),
+                  fontSize: 11,
                 ),
-              ],
-            ),
-            Spacer(flex: kMarketListPriceTailSpacerFlex),
-            SizedBox(width: kMarketListPriceToBadgeGap),
-            SizedBox(
-              width: kMarketListChangeBadgeWidth,
-              child: Container(
-                height: 34,
-                alignment: Alignment.center,
-                decoration: BoxDecoration(
-                  color: color,
-                  borderRadius: BorderRadius.circular(6),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+            ],
+          ),
+          price: Column(
+            crossAxisAlignment: CrossAxisAlignment.end,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text(
+                ticker.lastPriceStr != null
+                    ? formatRawPrice(ticker.lastPriceStr!)
+                    : formatPrice(ticker.lastPrice,
+                        decimalPlaces: ticker.pricePrecision),
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 14,
+                  fontWeight: FontWeight.w500,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
-                child: Text(
-                  changeStr,
-                  style: const TextStyle(
-                    color: Colors.white,
-                    fontSize: 13,
-                    fontWeight: FontWeight.w600,
-                    fontFeatures: [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.center,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                formatFiatPrice(ticker.lastPrice,
+                    pricePrecision: ticker.pricePrecision),
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(153),
+                  fontSize: 11,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
               ),
+            ],
+          ),
+          change: Container(
+            height: 34,
+            alignment: Alignment.center,
+            decoration: BoxDecoration(
+              color: color,
+              borderRadius: BorderRadius.circular(6),
             ),
-          ],
+            child: Text(
+              changeStr,
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 13,
+                fontWeight: FontWeight.w600,
+                fontFeatures: [FontFeature.tabularFigures()],
+              ),
+              textAlign: TextAlign.center,
+            ),
+          ),
         ),
       ),
     );

+ 232 - 307
lib/presentation/screens/market/market_screen.dart

@@ -11,6 +11,7 @@ import '../../../providers/market_provider.dart';
 import '../../widgets/common/app_refresh_indicator.dart';
 import '../../widgets/common/app_shimmer.dart';
 import '../../widgets/common/coin_icon.dart';
+import '../../widgets/market_list_row_layout.dart';
 
 /// 行情页列表点击:按当前选中的「永续 / 现货」Tab 跳转对应 K 线详情。
 void _pushMarketQuoteDetail(
@@ -304,23 +305,20 @@ class _SpotListHeader extends ConsumerWidget {
     }) {
       final active = sortField == field;
       final color = active ? cs.onSurface : cs.onSurface.withAlpha(120);
-      return SizedBox(
-        width: kMarketListChangeBadgeWidth,
-        child: GestureDetector(
-          onTap: () => ref.read(marketProvider.notifier).toggleSpotSort(field),
-          behavior: HitTestBehavior.opaque,
-          child: FittedBox(
-            fit: BoxFit.scaleDown,
-            alignment: Alignment.centerRight,
-            child: Row(
-              mainAxisAlignment: MainAxisAlignment.end,
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(label, style: TextStyle(color: color, fontSize: 12)),
-                const SizedBox(width: 2),
-                _SortIcon(active: active, asc: sortAsc, color: color),
-              ],
-            ),
+      return GestureDetector(
+        onTap: () => ref.read(marketProvider.notifier).toggleSpotSort(field),
+        behavior: HitTestBehavior.opaque,
+        child: FittedBox(
+          fit: BoxFit.scaleDown,
+          alignment: Alignment.center,
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.center,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text(label, style: TextStyle(color: color, fontSize: 12)),
+              const SizedBox(width: 2),
+              _SortIcon(active: active, asc: sortAsc, color: color),
+            ],
           ),
         ),
       );
@@ -328,28 +326,22 @@ class _SpotListHeader extends ConsumerWidget {
 
     return Padding(
       padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
-      child: Row(
-        children: [
-          Expanded(
-            flex: kMarketListNameClusterFlex,
-            child: sortCell(
-              label: AppLocalizations.of(context)!.nameVolume,
-              field: MarketSortField.volume,
-            ),
-          ),
-          SizedBox(width: kMarketListNameToPriceGap),
-          sortCell(
-            label: AppLocalizations.of(context)!.latestPriceFull,
-            field: MarketSortField.price,
-            align: MainAxisAlignment.end,
-          ),
-          Spacer(flex: kMarketListPriceTailSpacerFlex),
-          SizedBox(width: kMarketListPriceToBadgeGap),
-          trailingSortCol(
-            label: AppLocalizations.of(context)!.change24hFull,
-            field: MarketSortField.change,
-          ),
-        ],
+      child: MarketListHeaderRow(
+        leading: const SizedBox(width: 30),
+        leadingGap: 8,
+        name: sortCell(
+          label: AppLocalizations.of(context)!.nameVolume,
+          field: MarketSortField.volume,
+        ),
+        price: sortCell(
+          label: AppLocalizations.of(context)!.latestPriceFull,
+          field: MarketSortField.price,
+          align: MainAxisAlignment.end,
+        ),
+        change: trailingSortCol(
+          label: AppLocalizations.of(context)!.change24hFull,
+          field: MarketSortField.change,
+        ),
       ),
     );
   }
@@ -382,106 +374,89 @@ class _SpotTickerRow extends ConsumerWidget {
       onTap: () => _pushMarketQuoteDetail(context, ref, ticker.symbol),
       child: Padding(
         padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-        child: Row(
-          children: [
-            Expanded(
-              flex: kMarketListNameClusterFlex,
-              child: Row(
-                children: [
-                  CoinIcon(
-                      symbol: ticker.baseAsset, iconUrl: ticker.icon, size: 30),
-                  const SizedBox(width: 8),
-                  Expanded(
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      mainAxisSize: MainAxisSize.min,
-                      children: [
-                        Text(
-                          formatUsdtPairDisplay(ticker.symbol),
-                          style: TextStyle(
-                            color: cs.onSurface,
-                            fontSize: 14,
-                            fontWeight: FontWeight.w600,
-                          ),
-                          maxLines: 1,
-                          overflow: TextOverflow.ellipsis,
-                        ),
-                        Text(
-                          '${AppLocalizations.of(context)!.spot} · $volumeStr',
-                          style: TextStyle(
-                            color: cs.onSurface.withAlpha(120),
-                            fontSize: 11,
-                          ),
-                          maxLines: 1,
-                          overflow: TextOverflow.ellipsis,
-                        ),
-                      ],
-                    ),
-                  ),
-                ],
-              ),
-            ),
-            SizedBox(width: kMarketListNameToPriceGap),
-            Column(
-              crossAxisAlignment: CrossAxisAlignment.end,
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                Text(
-                  ticker.lastPrice > 0
-                      ? (ticker.lastPriceStr != null
-                          ? formatRawPrice(ticker.lastPriceStr!)
-                          : formatPrice(ticker.lastPrice))
-                      : '--',
-                  style: TextStyle(
-                    color: cs.onSurface,
-                    fontSize: 13,
-                    fontWeight: FontWeight.w500,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+        child: MarketListDataRow(
+          leading: CoinIcon(
+            symbol: ticker.baseAsset,
+            iconUrl: ticker.icon,
+            size: 30,
+          ),
+          leadingGap: 8,
+          name: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Text(
+                formatUsdtPairDisplay(ticker.symbol),
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 14,
+                  fontWeight: FontWeight.w600,
                 ),
-                Text(
-                  ticker.lastPrice > 0
-                      ? formatFiatPrice(ticker.lastPrice,
-                          pricePrecision: ticker.pricePrecision)
-                      : '--',
-                  style: TextStyle(
-                    color: cs.onSurface.withAlpha(120),
-                    fontSize: 11,
-                    fontFeatures: const [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.end,
-                  maxLines: 1,
-                  overflow: TextOverflow.ellipsis,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                '${AppLocalizations.of(context)!.spot} · $volumeStr',
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(120),
+                  fontSize: 11,
                 ),
-              ],
-            ),
-            Spacer(flex: kMarketListPriceTailSpacerFlex),
-            SizedBox(width: kMarketListPriceToBadgeGap),
-            SizedBox(
-              width: kMarketListChangeBadgeWidth,
-              child: Container(
-                height: 34,
-                decoration: BoxDecoration(
-                  color: changeColor,
-                  borderRadius: BorderRadius.circular(6),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+            ],
+          ),
+          price: Column(
+            crossAxisAlignment: CrossAxisAlignment.end,
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Text(
+                ticker.lastPrice > 0
+                    ? (ticker.lastPriceStr != null
+                        ? formatRawPrice(ticker.lastPriceStr!)
+                        : formatPrice(ticker.lastPrice))
+                    : '--',
+                style: TextStyle(
+                  color: cs.onSurface,
+                  fontSize: 13,
+                  fontWeight: FontWeight.w500,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
-                alignment: Alignment.center,
-                child: Text(
-                  ticker.lastPrice > 0 ? changeStr : '--',
-                  style: const TextStyle(
-                    color: Colors.white,
-                    fontSize: 13,
-                    fontWeight: FontWeight.w600,
-                    fontFeatures: [FontFeature.tabularFigures()],
-                  ),
-                  textAlign: TextAlign.center,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              Text(
+                ticker.lastPrice > 0
+                    ? formatFiatPrice(ticker.lastPrice,
+                        pricePrecision: ticker.pricePrecision)
+                    : '--',
+                style: TextStyle(
+                  color: cs.onSurface.withAlpha(120),
+                  fontSize: 11,
+                  fontFeatures: const [FontFeature.tabularFigures()],
                 ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
               ),
+            ],
+          ),
+          change: Container(
+            height: 34,
+            decoration: BoxDecoration(
+              color: changeColor,
+              borderRadius: BorderRadius.circular(6),
             ),
-          ],
+            alignment: Alignment.center,
+            child: Text(
+              ticker.lastPrice > 0 ? changeStr : '--',
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 13,
+                fontWeight: FontWeight.w600,
+                fontFeatures: [FontFeature.tabularFigures()],
+              ),
+              textAlign: TextAlign.center,
+            ),
+          ),
         ),
       ),
     );
@@ -534,27 +509,24 @@ class _ListHeader extends ConsumerWidget {
     }) {
       final active = sortField == field;
       final color = active ? cs.onSurface : cs.onSurface.withAlpha(120);
-      return SizedBox(
-        width: kMarketListChangeBadgeWidth,
-        child: Semantics(
-          label: semanticsLabel,
-          button: true,
+      return Semantics(
+        label: semanticsLabel,
+        button: true,
+        onTap: () => ref.read(marketProvider.notifier).toggleSort(field),
+        child: GestureDetector(
           onTap: () => ref.read(marketProvider.notifier).toggleSort(field),
-          child: GestureDetector(
-            onTap: () => ref.read(marketProvider.notifier).toggleSort(field),
-            behavior: HitTestBehavior.opaque,
-            child: FittedBox(
-              fit: BoxFit.scaleDown,
-              alignment: Alignment.centerRight,
-              child: Row(
-                mainAxisAlignment: MainAxisAlignment.end,
-                mainAxisSize: MainAxisSize.min,
-                children: [
-                  Text(label, style: TextStyle(color: color, fontSize: 12)),
-                  const SizedBox(width: 2),
-                  _SortIcon(active: active, asc: sortAsc, color: color),
-                ],
-              ),
+          behavior: HitTestBehavior.opaque,
+          child: FittedBox(
+            fit: BoxFit.scaleDown,
+            alignment: Alignment.center,
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                Text(label, style: TextStyle(color: color, fontSize: 12)),
+                const SizedBox(width: 2),
+                _SortIcon(active: active, asc: sortAsc, color: color),
+              ],
             ),
           ),
         ),
@@ -563,28 +535,21 @@ class _ListHeader extends ConsumerWidget {
 
     return Padding(
       padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
-      child: Row(
-        children: [
-          Expanded(
-            flex: kMarketListNameClusterFlex,
-            child: buildSortCell(
-                label: AppLocalizations.of(context)!.nameVolume,
-                field: MarketSortField.volume,
-                semanticsLabel: 'market_sort_volume'),
-          ),
-          SizedBox(width: kMarketListNameToPriceGap),
-          buildSortCell(
-              label: AppLocalizations.of(context)!.latestPriceFull,
-              field: MarketSortField.price,
-              align: MainAxisAlignment.end,
-              semanticsLabel: 'market_sort_price'),
-          Spacer(flex: kMarketListPriceTailSpacerFlex),
-          SizedBox(width: kMarketListPriceToBadgeGap),
-          buildTrailingSortCol(
-              label: AppLocalizations.of(context)!.change24hFull,
-              field: MarketSortField.change,
-              semanticsLabel: 'market_sort_change'),
-        ],
+      child: MarketListHeaderRow(
+        leading: const SizedBox(width: 40),
+        name: buildSortCell(
+            label: AppLocalizations.of(context)!.nameVolume,
+            field: MarketSortField.volume,
+            semanticsLabel: 'market_sort_volume'),
+        price: buildSortCell(
+            label: AppLocalizations.of(context)!.latestPriceFull,
+            field: MarketSortField.price,
+            align: MainAxisAlignment.end,
+            semanticsLabel: 'market_sort_price'),
+        change: buildTrailingSortCol(
+            label: AppLocalizations.of(context)!.change24hFull,
+            field: MarketSortField.change,
+            semanticsLabel: 'market_sort_change'),
       ),
     );
   }
@@ -661,110 +626,85 @@ class _TickerRow extends ConsumerWidget {
         onTap: () => _pushMarketQuoteDetail(context, ref, ticker.symbol),
         child: Padding(
           padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 11),
-          child: Row(
-            children: [
-              // 头像
-              Expanded(
-                flex: kMarketListNameClusterFlex,
-                child: Row(
-                  children: [
-                    CoinIcon(
-                      symbol: ticker.baseAsset,
-                      iconUrl: ticker.icon,
-                      size: 40,
-                      borderRadius: 12,
-                    ),
-                    const SizedBox(width: 10),
-                    Expanded(
-                      child: Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        children: [
-                          Text(
-                            formatUsdtPairDisplay(ticker.symbol),
-                            maxLines: 1,
-                            overflow: TextOverflow.ellipsis,
-                            style: TextStyle(
-                              color: cs.onSurface,
-                              fontSize: 13,
-                              fontWeight: FontWeight.w500,
-                            ),
-                          ),
-                          Text(
-                            '${AppLocalizations.of(context)!.perpetual} · $volumeStr',
-                            maxLines: 1,
-                            overflow: TextOverflow.ellipsis,
-                            style: TextStyle(
-                                color: cs.onSurface.withAlpha(153),
-                                fontSize: 12),
-                          ),
-                        ],
-                      ),
-                    ),
-                  ],
-                ),
-              ),
-
-              SizedBox(width: kMarketListNameToPriceGap),
-              Column(
-                crossAxisAlignment: CrossAxisAlignment.end,
-                mainAxisSize: MainAxisSize.min,
-                children: [
-                  Text(
-                    ticker.lastPriceStr != null
-                        ? formatRawPrice(ticker.lastPriceStr!)
-                        : formatPrice(ticker.lastPrice),
-                    style: TextStyle(
-                      color: cs.onSurface,
-                      fontSize: 14,
-                      fontWeight: FontWeight.w500,
-                      fontFeatures: const [FontFeature.tabularFigures()],
-                    ),
-                    textAlign: TextAlign.end,
-                    maxLines: 1,
-                    overflow: TextOverflow.ellipsis,
+          child: MarketListDataRow(
+            leading: CoinIcon(
+              symbol: ticker.baseAsset,
+              iconUrl: ticker.icon,
+              size: 40,
+              borderRadius: 12,
+            ),
+            name: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  formatUsdtPairDisplay(ticker.symbol),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                  style: TextStyle(
+                    color: cs.onSurface,
+                    fontSize: 13,
+                    fontWeight: FontWeight.w500,
                   ),
-                  Text(
-                    formatFiatPrice(ticker.lastPrice,
-                        pricePrecision: ticker.pricePrecision),
-                    style: TextStyle(
-                      color: cs.onSurface.withAlpha(153),
-                      fontSize: 11,
-                      fontFeatures: const [FontFeature.tabularFigures()],
-                    ),
-                    textAlign: TextAlign.end,
-                    maxLines: 1,
-                    overflow: TextOverflow.ellipsis,
+                ),
+                Text(
+                  '${AppLocalizations.of(context)!.perpetual} · $volumeStr',
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                  style: TextStyle(
+                    color: cs.onSurface.withAlpha(153),
+                    fontSize: 12,
                   ),
-                ],
-              ),
-
-              Spacer(flex: kMarketListPriceTailSpacerFlex),
-
-              const SizedBox(width: kMarketListPriceToBadgeGap),
-
-              // 涨跌幅 Badge(固定宽度,避免内容宽度抖动)
-              SizedBox(
-                width: kMarketListChangeBadgeWidth,
-                child: Container(
-                  height: 34,
-                  decoration: BoxDecoration(
-                    color: changeColor,
-                    borderRadius: BorderRadius.circular(6),
+                ),
+              ],
+            ),
+            price: Column(
+              crossAxisAlignment: CrossAxisAlignment.end,
+              mainAxisSize: MainAxisSize.min,
+              children: [
+                Text(
+                  ticker.lastPriceStr != null
+                      ? formatRawPrice(ticker.lastPriceStr!)
+                      : formatPrice(ticker.lastPrice),
+                  style: TextStyle(
+                    color: cs.onSurface,
+                    fontSize: 14,
+                    fontWeight: FontWeight.w500,
+                    fontFeatures: const [FontFeature.tabularFigures()],
                   ),
-                  alignment: Alignment.center,
-                  child: Text(
-                    changeStr,
-                    style: const TextStyle(
-                      color: Colors.white,
-                      fontSize: 13,
-                      fontWeight: FontWeight.w600,
-                      fontFeatures: [FontFeature.tabularFigures()],
-                    ),
-                    textAlign: TextAlign.center,
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                ),
+                Text(
+                  formatFiatPrice(ticker.lastPrice,
+                      pricePrecision: ticker.pricePrecision),
+                  style: TextStyle(
+                    color: cs.onSurface.withAlpha(153),
+                    fontSize: 11,
+                    fontFeatures: const [FontFeature.tabularFigures()],
                   ),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
                 ),
+              ],
+            ),
+            change: Container(
+              height: 34,
+              decoration: BoxDecoration(
+                color: changeColor,
+                borderRadius: BorderRadius.circular(6),
               ),
-            ],
+              alignment: Alignment.center,
+              child: Text(
+                changeStr,
+                style: const TextStyle(
+                  color: Colors.white,
+                  fontSize: 13,
+                  fontWeight: FontWeight.w600,
+                  fontFeatures: [FontFeature.tabularFigures()],
+                ),
+                textAlign: TextAlign.center,
+              ),
+            ),
           ),
         ),
       ),
@@ -784,40 +724,25 @@ class _MarketShimmer extends StatelessWidget {
         itemCount: 10,
         itemBuilder: (_, __) => Padding(
           padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
-          child: Row(
-            children: [
-              Expanded(
-                flex: kMarketListNameClusterFlex,
-                child: Row(
-                  children: [
-                    shimmerCircle(36),
-                    const SizedBox(width: 10),
-                    Expanded(
-                      child: Column(
-                        crossAxisAlignment: CrossAxisAlignment.start,
-                        children: [
-                          shimmerBox(70, 14),
-                          const SizedBox(height: 6),
-                          shimmerBox(50, 11),
-                        ],
-                      ),
-                    ),
-                  ],
-                ),
-              ),
-              SizedBox(width: kMarketListNameToPriceGap),
-              Column(
-                crossAxisAlignment: CrossAxisAlignment.end,
-                children: [
-                  shimmerBox(80, 14),
-                  const SizedBox(height: 6),
-                  shimmerBox(50, 11),
-                ],
-              ),
-              Spacer(flex: kMarketListPriceTailSpacerFlex),
-              const SizedBox(width: kMarketListPriceToBadgeGap),
-              shimmerBox(kMarketListChangeBadgeWidth, 30, radius: 6),
-            ],
+          child: MarketListDataRow(
+            leading: shimmerCircle(40),
+            name: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                shimmerBox(70, 14),
+                const SizedBox(height: 6),
+                shimmerBox(50, 11),
+              ],
+            ),
+            price: Column(
+              crossAxisAlignment: CrossAxisAlignment.end,
+              children: [
+                shimmerBox(80, 14),
+                const SizedBox(height: 6),
+                shimmerBox(50, 11),
+              ],
+            ),
+            change: shimmerBox(kMarketListChangeBadgeWidth, 30, radius: 6),
           ),
         ),
       ),

+ 83 - 0
lib/presentation/widgets/market_list_row_layout.dart

@@ -0,0 +1,83 @@
+import 'package:flutter/material.dart';
+
+import '../../core/constants/market_list_layout.dart';
+
+/// 行情列表数据行:左侧名称区 | 固定宽最新价 | 间距 | 固定宽涨跌幅。
+class MarketListDataRow extends StatelessWidget {
+  const MarketListDataRow({
+    super.key,
+    this.leading,
+    this.leadingGap = kMarketListLeadingToNameGap,
+    required this.name,
+    required this.price,
+    required this.change,
+  });
+
+  /// 图标等;为 null 时不占 leading 间距(表头无图标场景)。
+  final Widget? leading;
+
+  /// [leading] 与 [name] 之间的间距;leading 已自带间距时可传 0。
+  final double leadingGap;
+
+  final Widget name;
+  final Widget price;
+  final Widget change;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: [
+        Expanded(
+          child: Row(
+            children: [
+              if (leading != null) ...[
+                leading!,
+                if (leadingGap > 0) SizedBox(width: leadingGap),
+              ],
+              Expanded(child: name),
+            ],
+          ),
+        ),
+        SizedBox(
+          width: kMarketListPriceColumnWidth,
+          child: Align(
+            alignment: Alignment.centerRight,
+            child: price,
+          ),
+        ),
+        const SizedBox(width: kMarketListPriceToBadgeGap),
+        SizedBox(width: kMarketListChangeBadgeWidth, child: change),
+      ],
+    );
+  }
+}
+
+/// 行情列表表头,列宽与 [MarketListDataRow] 一致。
+class MarketListHeaderRow extends StatelessWidget {
+  const MarketListHeaderRow({
+    super.key,
+    this.leading,
+    this.leadingGap = kMarketListLeadingToNameGap,
+    required this.name,
+    required this.price,
+    required this.change,
+  });
+
+  final Widget? leading;
+  final double leadingGap;
+  final Widget name;
+  final Widget price;
+  final Widget change;
+
+  @override
+  Widget build(BuildContext context) {
+    return MarketListDataRow(
+      leading: leading,
+      leadingGap: leadingGap,
+      name: name,
+      price: price,
+      change: change,
+    );
+  }
+}

+ 1 - 1
pubspec.yaml

@@ -1,7 +1,7 @@
 name: cex_skeleton
 description: iBit — 加密货币交易 App
 publish_to: 'none'
-version: 4.1.4+40104
+version: 4.1.5+40105
 
 
 environment: