| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import '../../../core/constants/market_list_layout.dart';
- import '../../../core/l10n/app_localizations.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/utils/number_format.dart';
- import '../../../core/utils/symbol_display.dart';
- import '../../../providers/market_provider.dart';
- import '../../widgets/common/app_refresh_indicator.dart';
- import '../../widgets/common/app_shimmer.dart';
- import '../../widgets/common/coin_icon.dart';
- /// 行情页列表点击:按当前选中的「永续 / 现货」Tab 跳转对应 K 线详情。
- void _pushMarketQuoteDetail(
- BuildContext context,
- WidgetRef ref,
- String rawSymbol,
- ) {
- final mode = ref.read(marketProvider).mode;
- final sym = rawSymbol.replaceAll('/', '').replaceAll('-', '').toUpperCase();
- if (sym.isEmpty) return;
- final path =
- mode == MarketMode.futures ? '/market/futures/$sym' : '/market/spot/$sym';
- context.push(path);
- }
- class MarketScreen extends ConsumerWidget {
- const MarketScreen({super.key});
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final mode = ref.watch(marketProvider.select((s) => s.mode));
- final isLoading = ref.watch(marketProvider.select(
- (s) => mode == MarketMode.futures ? s.isLoading : s.spotLoading));
- return Scaffold(
- body: SafeArea(
- child: Column(
- children: [
- _SearchBar(
- onChanged: ref.read(marketProvider.notifier).setSearch,
- ),
- // ── 现货 / 合约 Tab 切换 ──────────────────────
- _MarketTabBar(
- mode: mode,
- onChanged: ref.read(marketProvider.notifier).setMode,
- ),
- Expanded(
- child: isLoading
- ? const _MarketShimmer()
- : mode == MarketMode.futures
- ? const _MarketList()
- : const _SpotMarketList(),
- ),
- ],
- ),
- ),
- );
- }
- }
- // ── 现货/合约 Tab 栏 ──────────────────────────────────────
- class _MarketTabBar extends StatelessWidget {
- const _MarketTabBar({required this.mode, required this.onChanged});
- final MarketMode mode;
- final ValueChanged<MarketMode> onChanged;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final l10n = AppLocalizations.of(context)!;
- Widget tab(String label, MarketMode value) {
- final active = mode == value;
- return Expanded(
- child: GestureDetector(
- onTap: () => onChanged(value),
- behavior: HitTestBehavior.opaque,
- child: Container(
- padding: const EdgeInsets.symmetric(vertical: 10),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: active ? AppColors.brand : Colors.transparent,
- width: 2,
- ),
- ),
- ),
- alignment: Alignment.center,
- child: Text(
- label,
- style: TextStyle(
- color: active ? cs.onSurface : cs.onSurface.withAlpha(140),
- fontSize: 14,
- fontWeight: active ? FontWeight.w600 : FontWeight.w400,
- ),
- ),
- ),
- ),
- );
- }
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Row(
- children: [
- tab(l10n.perpetualFutures, MarketMode.futures),
- tab(l10n.spotTab, MarketMode.spot),
- ],
- ),
- );
- }
- }
- // ── 搜索框 ────────────────────────────────────────────────
- class _SearchBar extends StatelessWidget {
- const _SearchBar({required this.onChanged});
- final ValueChanged<String> onChanged;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return Padding(
- padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
- child: SizedBox(
- height: 38,
- child: Semantics(
- label: 'market_search_input',
- textField: true,
- child: TextField(
- onChanged: onChanged,
- style: TextStyle(color: cs.onSurface, fontSize: 13),
- textAlignVertical: TextAlignVertical.center,
- decoration: InputDecoration(
- hintText: AppLocalizations.of(context)!.searchMarket,
- hintStyle:
- TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 13),
- prefixIcon: Icon(Icons.search,
- color: cs.onSurface.withAlpha(100), size: 18),
- prefixIconConstraints: const BoxConstraints(minWidth: 40),
- isDense: true,
- filled: true,
- fillColor: cs.surface,
- contentPadding:
- const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(24),
- borderSide: BorderSide.none,
- ),
- enabledBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(24),
- borderSide: BorderSide.none,
- ),
- focusedBorder: OutlineInputBorder(
- borderRadius: BorderRadius.circular(24),
- borderSide: BorderSide.none,
- ),
- ),
- ),
- ),
- ),
- );
- }
- }
- // ── 行情列表主体 ──────────────────────────────────────────
- // 只 select displaySymbols(symbol 列表),价格变化不触发列表重建。
- // 各行通过 tickerProvider(symbol) 独立订阅自己的 ticker 数据。
- class _MarketList extends ConsumerWidget {
- const _MarketList();
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final symbols = ref.watch(marketProvider.select((s) => s.displaySymbols));
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // "永续合约" 标题
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
- child: Text(
- AppLocalizations.of(context)!.perpetualFutures,
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w700,
- color: Theme.of(context).colorScheme.onSurface,
- ),
- ),
- ),
- // 列表表头
- const _ListHeader(),
- // 行情行列表
- Expanded(
- child: AppRefreshIndicator(
- onRefresh: () => ref.read(marketProvider.notifier).refresh(),
- child: ListView.builder(
- itemCount: symbols.length,
- itemBuilder: (context, index) {
- // 只传 symbol,不传 ticker 对象
- return _TickerRow(symbol: symbols[index]);
- },
- ),
- ),
- ),
- ],
- );
- }
- }
- // ── 现货行情列表 ──────────────────────────────────────────
- class _SpotMarketList extends ConsumerWidget {
- const _SpotMarketList();
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final symbols =
- ref.watch(marketProvider.select((s) => s.spotDisplaySymbols));
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
- child: Text(
- AppLocalizations.of(context)!.spotTab,
- style: TextStyle(
- fontSize: 16,
- fontWeight: FontWeight.w700,
- color: Theme.of(context).colorScheme.onSurface,
- ),
- ),
- ),
- const _SpotListHeader(),
- Expanded(
- child: AppRefreshIndicator(
- onRefresh: () => ref.read(marketProvider.notifier).refresh(),
- child: symbols.isEmpty
- ? Center(
- child: Text(
- AppLocalizations.of(context)!.noOrders,
- style: TextStyle(
- color: Theme.of(context)
- .colorScheme
- .onSurface
- .withAlpha(140),
- fontSize: 14,
- ),
- ),
- )
- : ListView.builder(
- itemCount: symbols.length,
- itemBuilder: (context, index) =>
- _SpotTickerRow(symbol: symbols[index]),
- ),
- ),
- ),
- ],
- );
- }
- }
- // ── 现货列表表头 ──────────────────────────────────────────
- class _SpotListHeader extends ConsumerWidget {
- const _SpotListHeader();
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final sortField = ref.watch(marketProvider.select((s) => s.spotSortField));
- final sortAsc = ref.watch(marketProvider.select((s) => s.spotSortAsc));
- Widget sortCell({
- required String label,
- required MarketSortField field,
- MainAxisAlignment align = MainAxisAlignment.start,
- }) {
- final active = sortField == field;
- final color = active ? cs.onSurface : cs.onSurface.withAlpha(120);
- return GestureDetector(
- onTap: () => ref.read(marketProvider.notifier).toggleSpotSort(field),
- behavior: HitTestBehavior.opaque,
- child: Row(
- mainAxisAlignment: align,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(label, style: TextStyle(color: color, fontSize: 12)),
- const SizedBox(width: 2),
- _SortIcon(active: active, asc: sortAsc, color: color),
- ],
- ),
- );
- }
- Widget trailingSortCol({
- required String label,
- required MarketSortField field,
- }) {
- 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 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,
- ),
- ],
- ),
- );
- }
- }
- // ── 现货行情行 ────────────────────────────────────────────
- class _SpotTickerRow extends ConsumerWidget {
- const _SpotTickerRow({required this.symbol});
- final String symbol;
- static String _formatVolume(double v) {
- if (v >= 1e9) return '${(v / 1e9).toStringAsFixed(2)}B';
- if (v >= 1e6) return '${(v / 1e6).toStringAsFixed(2)}M';
- if (v >= 1e3) return '${(v / 1e3).toStringAsFixed(2)}K';
- return v.toStringAsFixed(2);
- }
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final ticker = ref.watch(spotTickerProvider(symbol));
- if (ticker == null) return const SizedBox.shrink();
- final volumeStr = _formatVolume(ticker.volume24h);
- final changeColor = AppColors.changeColor(ticker.change24h);
- final changeStr = formatChange(ticker.change24h);
- return InkWell(
- 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,
- ),
- 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,
- ),
- ],
- ),
- Spacer(flex: kMarketListPriceTailSpacerFlex),
- SizedBox(width: kMarketListPriceToBadgeGap),
- SizedBox(
- width: kMarketListChangeBadgeWidth,
- child: 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,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
- // ── 列表表头(合约) ──────────────────────────────────────────
- class _ListHeader extends ConsumerWidget {
- const _ListHeader();
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final sortField = ref.watch(marketProvider.select((s) => s.sortField));
- final sortAsc = ref.watch(marketProvider.select((s) => s.sortAsc));
- Widget buildSortCell({
- required String label,
- required MarketSortField field,
- MainAxisAlignment align = MainAxisAlignment.start,
- required String semanticsLabel,
- }) {
- final active = sortField == field;
- final color = active ? cs.onSurface : cs.onSurface.withAlpha(120);
- return Semantics(
- label: semanticsLabel,
- button: true,
- onTap: () => ref.read(marketProvider.notifier).toggleSort(field),
- child: GestureDetector(
- onTap: () => ref.read(marketProvider.notifier).toggleSort(field),
- behavior: HitTestBehavior.opaque,
- child: Row(
- mainAxisAlignment: align,
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(label, style: TextStyle(color: color, fontSize: 12)),
- const SizedBox(width: 2),
- _SortIcon(active: active, asc: sortAsc, color: color),
- ],
- ),
- ),
- );
- }
- Widget buildTrailingSortCol({
- required String label,
- required MarketSortField field,
- required String semanticsLabel,
- }) {
- final active = sortField == field;
- final color = active ? cs.onSurface : cs.onSurface.withAlpha(120);
- return SizedBox(
- width: kMarketListChangeBadgeWidth,
- child: Semantics(
- label: semanticsLabel,
- button: true,
- 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),
- ],
- ),
- ),
- ),
- ),
- );
- }
- 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'),
- ],
- ),
- );
- }
- }
- /// 排序箭头图标:上下双三角,激活时高亮当前方向
- class _SortIcon extends StatelessWidget {
- const _SortIcon(
- {required this.active, required this.asc, required this.color});
- final bool active;
- final bool asc;
- final Color color;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final dim = cs.onSurface.withAlpha(50);
- return SizedBox(
- width: 12,
- height: 16,
- child: Stack(
- children: [
- Positioned(
- top: 0,
- left: 0,
- right: 0,
- child: Icon(Icons.arrow_drop_up,
- size: 14, color: active && asc ? color : dim),
- ),
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: Icon(Icons.arrow_drop_down,
- size: 14, color: active && !asc ? color : dim),
- ),
- ],
- ),
- );
- }
- }
- // ── 行情行 ────────────────────────────────────────────────
- // 每行通过 tickerProvider(symbol) 独立订阅,
- // BTC 价格变化只重建 BTC 行,不影响其他行。
- class _TickerRow extends ConsumerWidget {
- const _TickerRow({required this.symbol});
- final String symbol;
- static String _formatVolume(double v) {
- if (v >= 1e9) return '${(v / 1e9).toStringAsFixed(2)}B';
- if (v >= 1e6) return '${(v / 1e6).toStringAsFixed(2)}M';
- if (v >= 1e3) return '${(v / 1e3).toStringAsFixed(2)}K';
- return v.toStringAsFixed(2);
- }
- @override
- Widget build(BuildContext context, WidgetRef ref) {
- final cs = Theme.of(context).colorScheme;
- final ticker = ref.watch(tickerProvider(symbol));
- if (ticker == null) return const SizedBox.shrink();
- final volumeStr = _formatVolume(ticker.volume24h);
- final changeColor = AppColors.changeColor(ticker.change24h);
- final changeStr = formatChange(ticker.change24h);
- return Semantics(
- label: 'market_item_${ticker.symbol}',
- button: true,
- onTap: () => _pushMarketQuoteDetail(context, ref, ticker.symbol),
- child: InkWell(
- 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,
- ),
- 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,
- ),
- ],
- ),
- Spacer(flex: kMarketListPriceTailSpacerFlex),
- const SizedBox(width: kMarketListPriceToBadgeGap),
- // 涨跌幅 Badge(固定宽度,避免内容宽度抖动)
- SizedBox(
- width: kMarketListChangeBadgeWidth,
- child: 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,
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
- }
- // ── 行情骨架屏 ──────────────────────────────────────────────
- class _MarketShimmer extends StatelessWidget {
- const _MarketShimmer();
- @override
- Widget build(BuildContext context) {
- return AppShimmer(
- child: ListView.builder(
- physics: const NeverScrollableScrollPhysics(),
- 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),
- ],
- ),
- ),
- ),
- );
- }
- }
|