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:
2026-04-05 22:24:04 +08:00
parent e2624b845a
commit d8cd38c4de
17 changed files with 3980 additions and 2922 deletions

View File

@@ -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('重试'),