feat(ui): 应用新设计系统到 Flutter 项目

- 更新颜色系统为 Material Design 3
  * Primary: #72dcff (青色)
  * Secondary: #dd8bfb (紫色)
  * Tertiary: #afffd1 (绿色)

- 创建新的 UI 组件
  * GlassPanel: 毛玻璃效果面板
  * NeonGlow: 霓虹光效组件
  * GradientButton: 渐变按钮组件

- 更新所有页面样式
  * 交易页面 (trade_page.dart)
  * 行情页面 (market_page.dart)
  * 资产页面 (asset_page.dart)
  * 我的页面 (mine_page.dart)
  * 订单页面 (orders_page.dart)

- 支持深色和浅色主题
- 所有 UI 文字使用中文
- 保持现有 API 接口不变

变更统计:
- 9 个文件修改
- 1,893 行新增
- 691 行删除
- 3 个新组件
This commit is contained in:
2026-03-24 02:16:19 +08:00
parent dc61d845a5
commit df0e8beba9
11 changed files with 2625 additions and 705 deletions

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart';
import 'package:provider/provider.dart';
import '../../../core/constants/app_colors.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart';
import '../../components/glass_panel.dart';
/// 行情页面 - 使用 shadcn_ui 现代化设计
/// 行情页面 - Material Design 3 风格
class MarketPage extends StatefulWidget {
const MarketPage({super.key});
@@ -16,11 +17,11 @@ class MarketPage extends StatefulWidget {
}
class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMixin {
final _searchController = TextEditingController();
@override
bool get wantKeepAlive => true;
final _searchController = TextEditingController();
@override
void initState() {
super.initState();
@@ -38,10 +39,9 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
@override
Widget build(BuildContext context) {
super.build(context);
final theme = ShadTheme.of(context);
return Scaffold(
backgroundColor: theme.colorScheme.background,
backgroundColor: AppColorScheme.darkBackground,
body: Consumer<MarketProvider>(
builder: (context, provider, _) {
return Column(
@@ -59,207 +59,300 @@ class _MarketPageState extends State<MarketPage> with AutomaticKeepAliveClientMi
}
Widget _buildSearchBar(MarketProvider provider) {
final theme = ShadTheme.of(context);
return Padding(
padding: AppSpacing.pagePadding,
child: ShadInput(
controller: _searchController,
placeholder: const Text('搜索币种...'),
leading: Icon(
LucideIcons.search,
size: 18,
color: theme.colorScheme.mutedForeground,
padding: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: Container(
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.15),
),
),
child: TextField(
controller: _searchController,
onChanged: provider.search,
style: TextStyle(color: AppColorScheme.darkOnSurface),
decoration: InputDecoration(
hintText: 'Search markets...',
hintStyle: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
prefixIcon: Icon(
LucideIcons.search,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
suffixIcon: _searchController.text.isNotEmpty
? GestureDetector(
onTap: () {
_searchController.clear();
provider.clearSearch();
},
child: Icon(
LucideIcons.x,
size: 18,
color: AppColorScheme.darkOnSurfaceVariant,
),
)
: null,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md + AppSpacing.xs,
),
),
),
trailing: _searchController.text.isNotEmpty
? GestureDetector(
onTap: () {
_searchController.clear();
provider.clearSearch();
},
child: Icon(
LucideIcons.x,
size: 18,
color: theme.colorScheme.mutedForeground,
),
)
: null,
onChanged: provider.search,
),
);
}
Widget _buildTabs(MarketProvider provider) {
final theme = ShadTheme.of(context);
final tabs = [
{'key': 'all', 'label': '全部'},
{'key': 'realtime', 'label': '实时'},
{'key': 'hot', 'label': '热门'},
{'key': 'all', 'label': 'All'},
{'key': 'realtime', 'label': 'Real-time'},
{'key': 'hot', 'label': 'Hot'},
];
return Container(
height: 44,
margin: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md),
child: Row(
children: tabs.asMap().entries.map((entry) {
final index = entry.key;
final tab = entry.value;
final isActive = provider.activeTab == tab['key'];
height: 48,
margin: EdgeInsets.fromLTRB(AppSpacing.md, AppSpacing.md, AppSpacing.md, 0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: tabs.map((tab) {
final isActive = provider.activeTab == tab['key'];
return GestureDetector(
onTap: () => provider.setTab(tab['key']!),
child: Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.lg + AppSpacing.xs, vertical: AppSpacing.sm + AppSpacing.xs),
margin: EdgeInsets.only(right: AppSpacing.sm),
decoration: BoxDecoration(
color: isActive
? theme.colorScheme.primary
: theme.colorScheme.card,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Text(
tab['label']!,
style: TextStyle(
return GestureDetector(
onTap: () => provider.setTab(tab['key']!),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: EdgeInsets.only(right: AppSpacing.sm),
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm + AppSpacing.xs,
),
decoration: BoxDecoration(
color: isActive
? Colors.white
: theme.colorScheme.mutedForeground,
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
? AppColorScheme.darkPrimary.withValues(alpha: 0.1)
: AppColorScheme.darkSurfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.full),
border: isActive
? Border.all(
color: AppColorScheme.darkPrimary.withValues(alpha: 0.2),
)
: null,
boxShadow: isActive
? [
BoxShadow(
color: AppColorScheme.neonGlowPrimary,
blurRadius: 15,
),
]
: null,
),
child: Text(
tab['label']!,
style: TextStyle(
color: isActive
? AppColorScheme.darkPrimary
: AppColorScheme.darkOnSurfaceVariant,
fontWeight: isActive ? FontWeight.w700 : FontWeight.normal,
fontSize: 12,
),
),
),
),
);
}).toList(),
);
}).toList(),
),
),
);
}
Widget _buildCoinList(MarketProvider provider) {
final theme = ShadTheme.of(context);
if (provider.isLoading) {
return Center(
child: CircularProgressIndicator(
color: theme.colorScheme.primary,
color: AppColorScheme.darkPrimary,
),
);
}
if (provider.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.circleAlert,
size: 48,
color: theme.colorScheme.destructive,
),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text(
provider.error!,
style: TextStyle(color: theme.colorScheme.destructive),
),
SizedBox(height: AppSpacing.md),
ShadButton(
onPressed: provider.loadCoins,
child: const Text('重试'),
),
],
),
);
return _buildErrorState(provider);
}
final coins = provider.coins;
if (coins.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.coins,
size: 48,
color: theme.colorScheme.mutedForeground,
),
SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text(
'暂无数据',
style: theme.textTheme.muted,
),
],
),
);
return _buildEmptyState();
}
return RefreshIndicator(
onRefresh: provider.refresh,
color: theme.colorScheme.primary,
color: AppColorScheme.darkPrimary,
backgroundColor: AppColorScheme.darkSurfaceContainer,
child: ListView.builder(
padding: EdgeInsets.fromLTRB(AppSpacing.md, 0, AppSpacing.md, AppSpacing.md),
padding: EdgeInsets.all(AppSpacing.md),
itemCount: coins.length,
itemBuilder: (context, index) => _buildCoinItem(coins[index]),
),
);
}
Widget _buildCoinItem(Coin coin) {
final theme = ShadTheme.of(context);
return Padding(
padding: EdgeInsets.only(bottom: AppSpacing.sm),
child: ShadCard(
padding: AppSpacing.cardPadding,
child: Row(
Widget _buildErrorState(MarketProvider provider) {
return Center(
child: Padding(
padding: AppSpacing.pagePadding,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标
CircleAvatar(
radius: 22,
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.primary,
),
),
Icon(
LucideIcons.circleAlert,
size: 48,
color: AppColorScheme.darkError,
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 名称
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${coin.code}/USDT',
style: theme.textTheme.large.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
coin.name,
style: theme.textTheme.muted,
),
],
),
SizedBox(height: AppSpacing.md),
Text(
provider.error!,
style: TextStyle(color: AppColorScheme.darkError),
textAlign: TextAlign.center,
),
// 涨跌幅
Container(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm + AppSpacing.xs, vertical: AppSpacing.xs + AppSpacing.xs),
decoration: BoxDecoration(
color: getChangeBackgroundColor(coin.isUp),
borderRadius: BorderRadius.circular(AppRadius.sm + AppSpacing.xs),
),
child: Text(
coin.formattedChange,
style: TextStyle(
color: getChangeColor(coin.isUp),
fontWeight: FontWeight.w600,
),
),
SizedBox(height: AppSpacing.lg),
ShadButton(
onPressed: provider.loadCoins,
child: const Text('重试'),
),
],
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
LucideIcons.coins,
size: 48,
color: AppColorScheme.darkOnSurfaceVariant,
),
SizedBox(height: AppSpacing.md),
Text(
'暂无数据',
style: TextStyle(color: AppColorScheme.darkOnSurfaceVariant),
),
],
),
);
}
Widget _buildCoinItem(Coin coin) {
final changeColor = coin.isUp ? AppColorScheme.up : AppColorScheme.down;
final changeBgColor = coin.isUp
? AppColorScheme.darkTertiary.withValues(alpha: 0.1)
: AppColorScheme.darkError.withValues(alpha: 0.1);
return GlassCard(
margin: EdgeInsets.only(bottom: AppSpacing.sm),
child: Row(
children: [
// 图标容器
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColorScheme.darkSurfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(
color: AppColorScheme.darkOutlineVariant.withValues(alpha: 0.2),
),
),
child: Center(
child: Text(
coin.displayIcon,
style: TextStyle(
fontSize: 20,
color: coin.isUp ? AppColorScheme.darkPrimary : AppColorScheme.darkSecondary,
fontWeight: FontWeight.bold,
),
),
),
),
SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 币种信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
coin.code,
style: GoogleFonts.spaceGrotesk(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
SizedBox(width: AppSpacing.xs),
Text(
'/USDT',
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
),
SizedBox(height: AppSpacing.xs / 2),
Text(
coin.name,
style: TextStyle(
fontSize: 12,
color: AppColorScheme.darkOnSurfaceVariant,
),
),
],
),
),
// 价格和涨跌幅
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'\$${coin.formattedPrice}',
style: GoogleFonts.spaceGrotesk(
fontSize: 14,
fontWeight: FontWeight.bold,
color: AppColorScheme.darkOnSurface,
),
),
SizedBox(height: AppSpacing.xs),
Container(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: changeBgColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
border: Border.all(
color: changeColor.withValues(alpha: 0.2),
),
),
child: Text(
coin.formattedChange,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: changeColor,
),
),
),
],
),
],
),
);
}
}