This commit is contained in:
2026-04-08 02:42:59 +08:00
parent c259fb4504
commit 5d753d3fa4
4 changed files with 128 additions and 173 deletions

View File

@@ -152,27 +152,25 @@ class AppTheme {
), ),
), ),
// 輸入框 - 底部線條風格 // 輸入框 - Ghost Border 風格
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
filled: true, filled: true,
fillColor: AppColorScheme.lightSurfaceLow, fillColor: AppColorScheme.lightSurfaceLow,
border: UnderlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide( borderSide: BorderSide(
color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5),
width: 2,
), ),
), ),
enabledBorder: UnderlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: BorderSide( borderSide: BorderSide(
color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5), color: AppColorScheme.lightOutlineVariant.withValues(alpha: 0.5),
width: 2,
), ),
), ),
focusedBorder: UnderlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
borderSide: const BorderSide(color: AppColorScheme.lightPrimary, width: 2), borderSide: const BorderSide(color: AppColorScheme.lightPrimary, width: 1),
), ),
hintStyle: TextStyle(color: AppColorScheme.lightOnSurfaceMuted), hintStyle: TextStyle(color: AppColorScheme.lightOnSurfaceMuted),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(

View File

@@ -298,27 +298,30 @@ class _TransferPageState extends State<TransferPage> {
), ),
], ],
), ),
// 交换按钮 - 右侧贴分割线 // 交换按钮 - 右側居中分割
Positioned( Positioned(
right: 12, right: 12,
top: 20, top: 20,
child: GestureDetector( bottom: 20,
onTap: _toggleDirection, child: Center(
child: Container( child: GestureDetector(
width: 28, onTap: _toggleDirection,
height: 28, child: Container(
decoration: BoxDecoration( width: 28,
color: colorScheme.surface, height: 28,
shape: BoxShape.circle, decoration: BoxDecoration(
border: Border.all( color: colorScheme.surface,
color: colorScheme.outlineVariant, shape: BoxShape.circle,
width: 1, border: Border.all(
color: colorScheme.outlineVariant,
width: 1,
),
),
child: Icon(
LucideIcons.repeat2,
size: 13,
color: colorScheme.onSurfaceVariant,
), ),
),
child: Icon(
LucideIcons.arrowUpDown,
size: 14,
color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),

View File

@@ -8,6 +8,7 @@ import '../../../core/theme/app_spacing.dart';
import '../../../data/models/account_models.dart'; import '../../../data/models/account_models.dart';
import '../../../data/services/bonus_service.dart'; import '../../../data/services/bonus_service.dart';
import '../../../providers/asset_provider.dart'; import '../../../providers/asset_provider.dart';
import '../../components/coin_icon.dart';
/// 賬單頁面 — 代幣盈虧賬單 + 新人福利賬單 + 推廣福利賬單 /// 賬單頁面 — 代幣盈虧賬單 + 新人福利賬單 + 推廣福利賬單
class BillsPage extends StatefulWidget { class BillsPage extends StatefulWidget {
@@ -302,17 +303,7 @@ class _BillsPageState extends State<BillsPage> with SingleTickerProviderStateMix
children: [ children: [
Row( Row(
children: [ children: [
CircleAvatar( CoinIcon(symbol: h.coinCode, size: 32),
radius: 16,
backgroundColor: colorScheme.primary.withValues(alpha: 0.1),
child: Text(
h.coinCode.substring(0, 1),
style: AppTextStyles.labelLarge(context).copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
Text(h.coinCode, style: AppTextStyles.headlineMedium(context).copyWith( Text(h.coinCode, style: AppTextStyles.headlineMedium(context).copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -1,11 +1,8 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../../core/theme/app_color_scheme.dart';
import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_theme.dart'; import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_theme_extension.dart';
import '../../../data/models/coin.dart'; import '../../../data/models/coin.dart';
import '../../../providers/market_provider.dart'; import '../../../providers/market_provider.dart';
import '../../components/coin_icon.dart'; import '../../components/coin_icon.dart';
@@ -35,9 +32,10 @@ class _MarketPageState extends State<MarketPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: context.colors.surface, backgroundColor: colorScheme.surface,
body: Consumer<MarketProvider>( body: Consumer<MarketProvider>(
builder: (context, provider, _) { builder: (context, provider, _) {
if (provider.isLoading) { if (provider.isLoading) {
@@ -50,8 +48,8 @@ class _MarketPageState extends State<MarketPage>
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => provider.refresh(), onRefresh: () => provider.refresh(),
color: context.colors.primary, color: colorScheme.primary,
backgroundColor: context.colors.surfaceContainerHighest, backgroundColor: colorScheme.surfaceContainerHighest,
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@@ -63,7 +61,6 @@ class _MarketPageState extends State<MarketPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 頁面標題 "行情"
Text( Text(
'行情', '行情',
style: AppTextStyles.displaySmall(context).copyWith( style: AppTextStyles.displaySmall(context).copyWith(
@@ -72,13 +69,10 @@ class _MarketPageState extends State<MarketPage>
), ),
), ),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 搜索框 _buildSearchBar(colorScheme),
_buildSearchBar(context),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 精選區域BTC + ETH 卡片
_buildFeaturedSection(provider), _buildFeaturedSection(provider),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// 全部幣種列表(含表頭)
_buildCoinList(provider), _buildCoinList(provider),
], ],
), ),
@@ -89,8 +83,11 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 搜索框 // ============================================
Widget _buildSearchBar(BuildContext context) { // 搜索框
// ============================================
Widget _buildSearchBar(ColorScheme colorScheme) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
// TODO: 彈出搜索界面 // TODO: 彈出搜索界面
@@ -98,22 +95,19 @@ class _MarketPageState extends State<MarketPage>
child: Container( child: Container(
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colors.surfaceContainerHigh, color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(AppRadius.md), borderRadius: BorderRadius.circular(AppRadius.md),
), ),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row( child: Row(
children: [ children: [
Icon( Icon(LucideIcons.search,
LucideIcons.search, size: 16, color: colorScheme.onSurfaceVariant),
size: 16,
color: context.colors.onSurfaceVariant,
),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
Text( Text(
'搜索幣種名稱或代碼', '搜索幣種名稱或代碼',
style: AppTextStyles.bodyMedium(context).copyWith( style: AppTextStyles.bodyMedium(context).copyWith(
color: context.appColors.onSurfaceMuted, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -122,7 +116,10 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 精選區域BTC + ETH 大卡片 // ============================================
// 精選區域
// ============================================
Widget _buildFeaturedSection(MarketProvider provider) { Widget _buildFeaturedSection(MarketProvider provider) {
final featured = provider.featuredCoins; final featured = provider.featuredCoins;
if (featured.isEmpty) return const SizedBox.shrink(); if (featured.isEmpty) return const SizedBox.shrink();
@@ -145,8 +142,12 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 幣種列表(含表頭) // ============================================
// 幣種列表
// ============================================
Widget _buildCoinList(MarketProvider provider) { Widget _buildCoinList(MarketProvider provider) {
final colorScheme = Theme.of(context).colorScheme;
final coins = provider.otherCoins; final coins = provider.otherCoins;
if (coins.isEmpty) { if (coins.isEmpty) {
@@ -159,19 +160,17 @@ class _MarketPageState extends State<MarketPage>
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.appColors.surfaceCard, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.lg), borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all( border: Border.all(
color: context.appColors.ghostBorder, color: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1, width: 1,
), ),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// 表頭行 _buildListHeader(colorScheme),
_buildListHeader(context),
// 列表項
ListView.separated( ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@@ -179,7 +178,7 @@ class _MarketPageState extends State<MarketPage>
separatorBuilder: (_, __) => Divider( separatorBuilder: (_, __) => Divider(
height: 1, height: 1,
thickness: 1, thickness: 1,
color: context.colors.outlineVariant.withValues(alpha: 0.5 * 0.15), color: colorScheme.outlineVariant.withValues(alpha: 0.15),
indent: AppSpacing.md, indent: AppSpacing.md,
endIndent: AppSpacing.md, endIndent: AppSpacing.md,
), ),
@@ -190,10 +189,9 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 列表表頭行:幣種 | 最新價 | 漲跌幅 Widget _buildListHeader(ColorScheme colorScheme) {
Widget _buildListHeader(BuildContext context) { return Padding(
return const Padding( padding: const EdgeInsets.fromLTRB(
padding: EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.md,
AppSpacing.sm + AppSpacing.xs, AppSpacing.sm + AppSpacing.xs,
AppSpacing.md, AppSpacing.md,
@@ -207,7 +205,7 @@ class _MarketPageState extends State<MarketPage>
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColorScheme.darkOnSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -219,11 +217,11 @@ class _MarketPageState extends State<MarketPage>
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColorScheme.darkOnSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
SizedBox( SizedBox(
width: 72, width: 72,
child: Text( child: Text(
@@ -232,7 +230,7 @@ class _MarketPageState extends State<MarketPage>
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColorScheme.darkOnSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -241,23 +239,29 @@ class _MarketPageState extends State<MarketPage>
); );
} }
/// 錯誤狀態 // ============================================
// 錯誤狀態
// ============================================
Widget _buildErrorState(MarketProvider provider) { Widget _buildErrorState(MarketProvider provider) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Padding( child: Padding(
padding: AppSpacing.pagePadding, padding: AppSpacing.pagePadding,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(LucideIcons.circleAlert, size: 48, color: context.colors.error), Icon(LucideIcons.circleAlert,
size: 48, color: colorScheme.error),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
Text( Text(
provider.error ?? '加載失敗', provider.error ?? '加載失敗',
style: TextStyle(color: context.colors.error), style: TextStyle(color: colorScheme.error),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
ShadButton( ElevatedButton(
onPressed: () => provider.refresh(), onPressed: () => provider.refresh(),
child: const Text('重試'), child: const Text('重試'),
), ),
@@ -268,7 +272,10 @@ class _MarketPageState extends State<MarketPage>
} }
} }
/// 精選卡片BTC / ETH (148px 高度,漸變背景,含迷你柱狀圖) // ============================================
// 精選卡片 — 簡約風格,用貨幣圖標代替柱狀圖
// ============================================
class _FeaturedCard extends StatelessWidget { class _FeaturedCard extends StatelessWidget {
final Coin coin; final Coin coin;
@@ -276,43 +283,55 @@ class _FeaturedCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isUp = coin.isUp; final isUp = coin.isUp;
final isBtc = coin.code == 'BTC';
final gradient = final changeColor =
isBtc ? AppColorScheme.btcCardGradient : AppColorScheme.ethCardGradient; isUp ? colorScheme.tertiary : colorScheme.error;
final barColor = final changeBgColor =
isBtc ? AppColorScheme.btcBarColor : AppColorScheme.ethBarColor; changeColor.withValues(alpha: 0.12);
final changeBgColor = isUp
? AppColorScheme.darkTertiary.withValues(alpha: 0.2)
: AppColorScheme.darkError.withValues(alpha: 0.2);
final changeColor = isUp
? AppColorScheme.darkTertiary
: AppColorScheme.darkError;
return Container( return Container(
height: 148, height: 120,
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: gradient, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.lg), borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
width: 1,
),
), ),
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// 第一行:交易對名 + 漲跌幅標籤 // 左側:圖標 + 交易對
Row( Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
CoinIcon(symbol: coin.code, size: 32),
const SizedBox(height: 8),
Text( Text(
'${coin.code}/USDT', '${coin.code}/U',
style: const TextStyle( style: AppTextStyles.bodyLarge(context).copyWith(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
const Spacer(),
// 右側:價格 + 漲跌
Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_formatFeaturedPrice(coin),
style: AppTextStyles.numberLarge(context).copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xs + 2, horizontal: AppSpacing.xs + 2,
@@ -333,29 +352,11 @@ class _FeaturedCard extends StatelessWidget {
), ),
], ],
), ),
// 第二行:大號價格
Text(
'\$${_formatFeaturedPrice(coin)}',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w800,
height: 1.2,
),
),
// 第三行:迷你柱狀圖
Expanded(
child: _MiniBarChart(
barColor: barColor,
seed: coin.code.hashCode,
),
),
], ],
), ),
); );
} }
/// 精選卡片使用簡短價格格式(帶逗號)
String _formatFeaturedPrice(Coin coin) { String _formatFeaturedPrice(Coin coin) {
if (coin.price >= 1000) { if (coin.price >= 1000) {
return _addCommas(coin.price.toStringAsFixed(2)); return _addCommas(coin.price.toStringAsFixed(2));
@@ -380,47 +381,10 @@ class _FeaturedCard extends StatelessWidget {
} }
} }
/// 迷你柱狀圖(模擬價格走勢) // ============================================
class _MiniBarChart extends StatelessWidget { // 幣種列表行
final Color barColor; // ============================================
final int seed;
const _MiniBarChart({required this.barColor, required this.seed});
@override
Widget build(BuildContext context) {
// 生成隨機但確定的高度序列
final heights = _generateHeights();
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: heights.map((h) {
return Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Container(
height: h,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(AppRadius.sm),
),
),
),
);
}).toList(),
);
}
List<double> _generateHeights() {
final random = Random(seed);
const base = 6.0;
const range = 12.0;
return List.generate(6, (_) => base + random.nextDouble() * range);
}
}
/// 幣種列表行
class _CoinRow extends StatelessWidget { class _CoinRow extends StatelessWidget {
final Coin coin; final Coin coin;
@@ -428,12 +392,10 @@ class _CoinRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final isUp = coin.isUp; final isUp = coin.isUp;
final changeColor = final changeColor = isUp ? colorScheme.tertiary : colorScheme.error;
isUp ? context.appColors.up : context.appColors.down; final changeBgColor = changeColor.withValues(alpha: 0.12);
final changeBgColor = isUp
? context.appColors.upBackground
: context.appColors.downBackground;
return GestureDetector( return GestureDetector(
onTap: () => _navigateToTrade(context), onTap: () => _navigateToTrade(context),
@@ -445,21 +407,19 @@ class _CoinRow extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
// 頭像36px 圓形
CoinIcon( CoinIcon(
symbol: coin.code, symbol: coin.code,
size: 36, size: 36,
isCircle: true, isCircle: true,
), ),
const SizedBox(width: AppSpacing.sm + AppSpacing.xs), const SizedBox(width: AppSpacing.sm + AppSpacing.xs),
// 幣種信息:交易對 + 全名
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'${coin.code}/USDT', '${coin.code}/U',
style: AppTextStyles.numberMedium(context).copyWith( style: AppTextStyles.numberMedium(context).copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -472,17 +432,15 @@ class _CoinRow extends StatelessWidget {
], ],
), ),
), ),
// 價格固定寬度90右對齊
SizedBox( SizedBox(
width: 90, width: 90,
child: Text( child: Text(
'\$${coin.formattedPrice}', coin.formattedPrice,
textAlign: TextAlign.right, textAlign: TextAlign.right,
style: AppTextStyles.numberMedium(context), style: AppTextStyles.numberMedium(context),
), ),
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
// 漲跌幅標籤固定寬度72
SizedBox( SizedBox(
width: 72, width: 72,
child: Container( child: Container(
@@ -515,7 +473,10 @@ class _CoinRow extends StatelessWidget {
} }
} }
/// 空狀態 // ============================================
// 空狀態
// ============================================
class _EmptyState extends StatelessWidget { class _EmptyState extends StatelessWidget {
final IconData icon; final IconData icon;
final String message; final String message;
@@ -529,20 +490,22 @@ class _EmptyState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl), padding: const EdgeInsets.all(AppSpacing.xl),
child: Column( child: Column(
children: [ children: [
Icon(icon, size: 48, color: context.colors.onSurfaceVariant), Icon(icon, size: 48, color: colorScheme.onSurfaceVariant),
const SizedBox(height: AppSpacing.sm + AppSpacing.xs), const SizedBox(height: AppSpacing.sm + AppSpacing.xs),
Text( Text(
message, message,
style: TextStyle(color: context.colors.onSurfaceVariant), style: TextStyle(color: colorScheme.onSurfaceVariant),
), ),
if (onRetry != null) ...[ if (onRetry != null) ...[
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
ShadButton( ElevatedButton(
onPressed: onRetry, onPressed: onRetry,
child: const Text('重試'), child: const Text('重試'),
), ),