feat(theme): update color scheme with new Slate theme and improved surface hierarchy
Updated the app's color scheme to implement a new "Slate" theme with refined dark and light variants. Changed background colors from #0A0E14 to #0B1120 for dark mode and updated surface layer colors to follow Material Design 3 specifications. Modified text colors and outline variants for better contrast and accessibility. Updated font sizes in transaction details screen from 11px to 12px for improved readability.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import '../../../core/theme/app_color_scheme.dart';
|
||||
import '../../../core/theme/app_spacing.dart';
|
||||
import '../../../core/theme/app_spacing.dart' show AppRadius;
|
||||
import '../../../data/models/coin.dart';
|
||||
import '../../../providers/market_provider.dart';
|
||||
import '../../components/glass_panel.dart';
|
||||
@@ -53,24 +55,30 @@ class _MarketPageState extends State<MarketPage>
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: AppSpacing.pagePadding,
|
||||
padding: const EdgeInsets.only(top: 16, left: 16, right: 16, bottom: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 上半区:BTC + ETH 突出展示
|
||||
_buildFeaturedSection(provider),
|
||||
SizedBox(height: AppSpacing.lg),
|
||||
// 下半区标题
|
||||
Text(
|
||||
'代币列表',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
// 页面标题 "行情"
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 0, bottom: 8),
|
||||
child: Text(
|
||||
'行情',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
// 下半区:代币列表
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// 精选区域:BTC + ETH 卡片
|
||||
_buildFeaturedSection(provider),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// 分区标题:全部币种 + 更多
|
||||
_buildSectionHeader(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
// 币种列表卡片
|
||||
_buildCoinList(provider),
|
||||
],
|
||||
),
|
||||
@@ -81,7 +89,7 @@ class _MarketPageState extends State<MarketPage>
|
||||
);
|
||||
}
|
||||
|
||||
/// 上半区:BTC + ETH 大卡片
|
||||
/// 精选区域:BTC + ETH 大卡片
|
||||
Widget _buildFeaturedSection(MarketProvider provider) {
|
||||
final featured = provider.featuredCoins;
|
||||
if (featured.isEmpty) return const SizedBox.shrink();
|
||||
@@ -95,7 +103,7 @@ class _MarketPageState extends State<MarketPage>
|
||||
Expanded(child: _FeaturedCard(coin: btc))
|
||||
else
|
||||
const Expanded(child: SizedBox.shrink()),
|
||||
SizedBox(width: AppSpacing.md),
|
||||
const SizedBox(width: 12),
|
||||
if (eth != null)
|
||||
Expanded(child: _FeaturedCard(coin: eth))
|
||||
else
|
||||
@@ -104,9 +112,37 @@ class _MarketPageState extends State<MarketPage>
|
||||
);
|
||||
}
|
||||
|
||||
/// 下半区:代币列表
|
||||
/// 分区标题:全部币种 + 更多
|
||||
Widget _buildSectionHeader() {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'全部币种',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'更多 >',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 币种列表
|
||||
Widget _buildCoinList(MarketProvider provider) {
|
||||
final coins = provider.otherCoins;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (coins.isEmpty) {
|
||||
return _EmptyState(
|
||||
@@ -116,12 +152,28 @@ class _MarketPageState extends State<MarketPage>
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: coins.length,
|
||||
separatorBuilder: (_, __) => SizedBox(height: AppSpacing.sm),
|
||||
itemBuilder: (context, index) => _CoinListItem(coin: coins[index]),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withOpacity(0.15),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: coins.length,
|
||||
separatorBuilder: (_, __) => Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: colorScheme.outlineVariant.withOpacity(0.5 * 0.15),
|
||||
indent: 16,
|
||||
endIndent: 16,
|
||||
),
|
||||
itemBuilder: (context, index) => _CoinRow(coin: coins[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,13 +187,13 @@ class _MarketPageState extends State<MarketPage>
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.circleAlert, size: 48, color: colorScheme.error),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
provider.error ?? '加载失败',
|
||||
style: TextStyle(color: colorScheme.error),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
ShadButton(
|
||||
onPressed: () => provider.refresh(),
|
||||
child: const Text('重试'),
|
||||
@@ -153,7 +205,7 @@ class _MarketPageState extends State<MarketPage>
|
||||
}
|
||||
}
|
||||
|
||||
/// 上半区大卡片:BTC / ETH
|
||||
/// 精选卡片:BTC / ETH (130px 高度,含迷你柱状图)
|
||||
class _FeaturedCard extends StatelessWidget {
|
||||
final Coin coin;
|
||||
|
||||
@@ -168,89 +220,146 @@ class _FeaturedCard extends StatelessWidget {
|
||||
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
|
||||
final changeBgColor = isUp
|
||||
? AppColorScheme.getUpBackgroundColor(isDark)
|
||||
: colorScheme.error.withOpacity(0.1);
|
||||
: AppColorScheme.getDownBackgroundColor(isDark);
|
||||
|
||||
return GlassPanel(
|
||||
padding: EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 图标 + 币种代码
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
coin.displayIcon,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
coin.code,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: AppSpacing.md),
|
||||
// 当前价格
|
||||
Text(
|
||||
'\$${coin.formattedPrice}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
// 24h 涨跌幅
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: changeBgColor,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
border: Border.all(color: changeColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: changeColor,
|
||||
padding: const EdgeInsets.all(16),
|
||||
height: 130,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// 第一行:币种名称 + 涨跌徽章
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${coin.code}/USDT',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: changeBgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: changeColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 第二行:价格
|
||||
Text(
|
||||
'\$${_formatFeaturedPrice(coin)}',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 第三行:币种全名
|
||||
Text(
|
||||
coin.name,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// 第四行:迷你柱状图
|
||||
Expanded(
|
||||
child: _MiniBarChart(isUp: isUp, isDark: isDark, seed: coin.code.hashCode),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 精选卡片使用简短价格格式(带逗号)
|
||||
String _formatFeaturedPrice(Coin coin) {
|
||||
if (coin.price >= 1000) {
|
||||
return _addCommas(coin.price.toStringAsFixed(2));
|
||||
}
|
||||
return coin.price.toStringAsFixed(2);
|
||||
}
|
||||
|
||||
String _addCommas(String text) {
|
||||
final parts = text.split('.');
|
||||
final intPart = parts[0];
|
||||
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
|
||||
final buffer = StringBuffer();
|
||||
int count = 0;
|
||||
for (int i = intPart.length - 1; i >= 0; i--) {
|
||||
if (count > 0 && count % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(intPart[i]);
|
||||
count++;
|
||||
}
|
||||
return '${buffer.toString().split('').reversed.join()}$decPart';
|
||||
}
|
||||
}
|
||||
|
||||
/// 下半区列表项
|
||||
class _CoinListItem extends StatelessWidget {
|
||||
/// 迷你柱状图(模拟价格走势)
|
||||
class _MiniBarChart extends StatelessWidget {
|
||||
final bool isUp;
|
||||
final bool isDark;
|
||||
final int seed;
|
||||
|
||||
const _MiniBarChart({required this.isUp, required this.isDark, required this.seed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final barColor = isUp
|
||||
? AppColorScheme.getUpColor(isDark)
|
||||
: AppColorScheme.getDownColor(isDark);
|
||||
|
||||
// 生成随机但确定的高度序列
|
||||
final heights = _generateHeights();
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: heights.map((h) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 1.5),
|
||||
child: Container(
|
||||
height: h,
|
||||
decoration: BoxDecoration(
|
||||
color: barColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<double> _generateHeights() {
|
||||
final random = Random(seed);
|
||||
final base = 8.0;
|
||||
final range = 16.0;
|
||||
return List.generate(6, (_) => base + random.nextDouble() * range);
|
||||
}
|
||||
}
|
||||
|
||||
/// 币种列表行
|
||||
class _CoinRow extends StatelessWidget {
|
||||
final Coin coin;
|
||||
|
||||
const _CoinListItem({required this.coin});
|
||||
const _CoinRow({required this.coin});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -258,102 +367,72 @@ class _CoinListItem extends StatelessWidget {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final isUp = coin.isUp;
|
||||
final changeColor =
|
||||
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.down;
|
||||
isUp ? AppColorScheme.getUpColor(isDark) : AppColorScheme.getDownColor(isDark);
|
||||
final changeBgColor = isUp
|
||||
? AppColorScheme.getUpBackgroundColor(isDark)
|
||||
: colorScheme.error.withOpacity(0.1);
|
||||
: AppColorScheme.getDownBackgroundColor(isDark);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _navigateToTrade(context),
|
||||
child: GlassPanel(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm + AppSpacing.xs,
|
||||
),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
// 币种图标
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
coin.displayIcon,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
|
||||
// 币种信息
|
||||
// 头像:圆形字母头像
|
||||
_CoinAvatar(letter: coin.displayIcon, code: coin.code),
|
||||
const SizedBox(width: 10),
|
||||
// 币种信息:交易对 + 全名
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
coin.code,
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(width: AppSpacing.xs),
|
||||
Text(
|
||||
'/USDT',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
'${coin.code}/USDT',
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
coin.name,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.normal,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 价格和涨跌幅
|
||||
// 右侧:价格 + 涨跌标签
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'\$${coin.formattedPrice}',
|
||||
style: GoogleFonts.spaceGrotesk(
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: 2,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: changeBgColor,
|
||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||
border: Border.all(color: changeColor.withOpacity(0.2)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
coin.formattedChange,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: changeColor,
|
||||
),
|
||||
),
|
||||
@@ -372,6 +451,55 @@ class _CoinListItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 币种头像组件
|
||||
class _CoinAvatar extends StatelessWidget {
|
||||
final String letter;
|
||||
final String code;
|
||||
|
||||
const _CoinAvatar({required this.letter, required this.code});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// 从 .pen 设计中的 accent-light 和 accent-primary
|
||||
final bgColor = colorScheme.primary.withOpacity(isDark ? 0.15 : 0.1);
|
||||
final textColor = colorScheme.primary;
|
||||
|
||||
return Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_getLetter(),
|
||||
style: GoogleFonts.inter(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getLetter() {
|
||||
const letterMap = {
|
||||
'SOL': 'S',
|
||||
'BNB': 'B',
|
||||
'XRP': 'X',
|
||||
'DOGE': 'D',
|
||||
'ADA': 'A',
|
||||
'DOT': 'D',
|
||||
};
|
||||
return letterMap[code] ?? code.substring(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// 空状态
|
||||
class _EmptyState extends StatelessWidget {
|
||||
final IconData icon;
|
||||
@@ -386,17 +514,17 @@ class _EmptyState extends StatelessWidget {
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(AppSpacing.xl),
|
||||
padding: const EdgeInsets.all(AppSpacing.xl),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
|
||||
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
SizedBox(height: AppSpacing.md),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
ShadButton(
|
||||
onPressed: onRetry,
|
||||
child: const Text('重试'),
|
||||
|
||||
Reference in New Issue
Block a user