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:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user