refactor: 交易页面改为券+行业分类模式,移除交易对tabs

- market_page: 移除券/法币/数字货币/稳定币tabs,改为行业分类过滤(餐饮/购物/娱乐/出行/生活/运动)
- market_page: 新增排序栏(折扣率/价格/到期时间),二级市场改为券名+品牌+行业标签展示
- trading_detail_page: 移除SBUX/USDT交易对概念,改为券信息卡片+配置货币符号
- trading_detail_page: 新增券信息卡片(品牌/行业/信用评级/面值/到期),价格显示折扣率
- 计价货币由用户在"我的→设置"中配置,默认跟随语言

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-11 20:55:24 -08:00
parent d9c953149b
commit 003b571f94
2 changed files with 424 additions and 242 deletions

View File

@ -5,8 +5,9 @@ import '../../../../app/theme/app_spacing.dart';
/// - /// -
/// ///
/// / /// /
/// /// /// + +
/// "我的→设置"(=¥=$)
class MarketPage extends StatefulWidget { class MarketPage extends StatefulWidget {
const MarketPage({super.key}); const MarketPage({super.key});
@ -17,7 +18,8 @@ class MarketPage extends StatefulWidget {
class _MarketPageState extends State<MarketPage> class _MarketPageState extends State<MarketPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late TabController _marketTabController; late TabController _marketTabController;
int _pairTypeIndex = 0; // 0=/, 1=/, 2=/, 3= String? _selectedCategory; // null =
int _sortIndex = 0; // 0=, 1=, 2=, 3=
@override @override
void initState() { void initState() {
@ -69,13 +71,104 @@ class _MarketPageState extends State<MarketPage>
), ),
), ),
), ),
body: TabBarView( body: Column(
children: [
const SizedBox(height: 12),
//
_buildCategoryFilter(),
const SizedBox(height: 8),
//
_buildSortBar(),
const Divider(height: 1),
//
Expanded(
child: TabBarView(
controller: _marketTabController, controller: _marketTabController,
children: [ children: [
_buildPrimaryMarket(), _buildPrimaryMarket(),
_buildSecondaryMarket(), _buildSecondaryMarket(),
], ],
), ),
),
],
),
);
}
// ============================================================
//
// ============================================================
Widget _buildCategoryFilter() {
final categories = [
(null, '全部'),
('dining', '餐饮'),
('shopping', '购物'),
('entertainment', '娱乐'),
('travel', '出行'),
('life', '生活'),
('sports', '运动'),
];
return SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: categories.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final (key, label) = categories[index];
final isSelected = _selectedCategory == key;
return GestureDetector(
onTap: () => setState(() => _selectedCategory = key),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : AppColors.gray50,
borderRadius: AppSpacing.borderRadiusFull,
border: isSelected
? null
: Border.all(color: AppColors.borderLight),
),
alignment: Alignment.center,
child: Text(
label,
style: AppTypography.labelSmall.copyWith(
color: isSelected ? Colors.white : AppColors.textSecondary,
),
),
),
);
},
),
);
}
// ============================================================
//
// ============================================================
Widget _buildSortBar() {
final sorts = ['折扣率', '价格↑', '价格↓', '到期时间'];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row(
children: sorts.asMap().entries.map((entry) {
final isSelected = _sortIndex == entry.key;
return Padding(
padding: const EdgeInsets.only(right: 16),
child: GestureDetector(
onTap: () => setState(() => _sortIndex = entry.key),
child: Text(
entry.value,
style: AppTypography.caption.copyWith(
color: isSelected ? AppColors.primary : AppColors.textTertiary,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
),
);
}).toList(),
),
); );
} }
@ -84,7 +177,7 @@ class _MarketPageState extends State<MarketPage>
// ============================================================ // ============================================================
Widget _buildPrimaryMarket() { Widget _buildPrimaryMarket() {
return ListView.separated( return ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100), padding: const EdgeInsets.fromLTRB(20, 12, 20, 100),
itemCount: _mockLaunches.length, itemCount: _mockLaunches.length,
separatorBuilder: (_, __) => const SizedBox(height: 12), separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) { itemBuilder: (context, index) {
@ -108,7 +201,7 @@ class _MarketPageState extends State<MarketPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Header: brand + status badge // Header: brand + category + status
Row( Row(
children: [ children: [
Container( Container(
@ -126,14 +219,32 @@ class _MarketPageState extends State<MarketPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(launch.brandName, style: AppTypography.labelMedium), Row(
Text(launch.couponName, style: AppTypography.bodySmall), children: [
Text(launch.brandName,
style: AppTypography.labelMedium),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: AppColors.gray100,
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(launch.categoryLabel,
style: AppTypography.caption
.copyWith(fontSize: 10)),
),
],
),
Text(launch.couponName,
style: AppTypography.bodySmall),
], ],
), ),
), ),
Container( Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: launch.statusColor.withValues(alpha: 0.1), color: launch.statusColor.withValues(alpha: 0.1),
borderRadius: AppSpacing.borderRadiusFull, borderRadius: AppSpacing.borderRadiusFull,
@ -151,19 +262,24 @@ class _MarketPageState extends State<MarketPage>
const SizedBox(height: 16), const SizedBox(height: 16),
// Price + Supply info // Price info
Row( Row(
children: [ children: [
_buildLaunchInfo('发行价', '\$${launch.issuePrice.toStringAsFixed(2)}'), _buildLaunchInfo('发行价',
_buildLaunchInfo('面值', '\$${launch.faceValue.toStringAsFixed(0)}'), '\$${launch.issuePrice.toStringAsFixed(2)}'),
_buildLaunchInfo('折扣', '${(launch.issuePrice / launch.faceValue * 10).toStringAsFixed(1)}'), _buildLaunchInfo(
_buildLaunchInfo('发行量', '${launch.totalSupply}'), '面值', '\$${launch.faceValue.toStringAsFixed(0)}'),
_buildLaunchInfo(
'折扣',
'${(launch.issuePrice / launch.faceValue * 10).toStringAsFixed(1)}'),
_buildLaunchInfo(
'发行量', '${launch.totalSupply}'),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// Progress bar // Progress
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -187,8 +303,8 @@ class _MarketPageState extends State<MarketPage>
value: launch.soldPercent, value: launch.soldPercent,
minHeight: 6, minHeight: 6,
backgroundColor: AppColors.gray100, backgroundColor: AppColors.gray100,
valueColor: valueColor: const AlwaysStoppedAnimation<Color>(
const AlwaysStoppedAnimation<Color>(AppColors.primary), AppColors.primary),
), ),
), ),
], ],
@ -196,7 +312,6 @@ class _MarketPageState extends State<MarketPage>
if (launch.status == 0) ...[ if (launch.status == 0) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
// Countdown
Row( Row(
children: [ children: [
const Icon(Icons.access_time_rounded, const Icon(Icons.access_time_rounded,
@ -236,26 +351,19 @@ class _MarketPageState extends State<MarketPage>
} }
// ============================================================ // ============================================================
// - (Binance style) // -
// ============================================================ // ============================================================
Widget _buildSecondaryMarket() { Widget _buildSecondaryMarket() {
return Column( return Column(
children: [ children: [
const SizedBox(height: 12), //
// Pair type tabs
_buildPairTypeTabs(),
const SizedBox(height: 8),
// Column headers
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
flex: 3, flex: 3,
child: child: Text('券名/品牌', style: AppTypography.caption)),
Text('交易对', style: AppTypography.caption)),
Expanded( Expanded(
flex: 2, flex: 2,
child: Text('最新价', child: Text('最新价',
@ -269,18 +377,17 @@ class _MarketPageState extends State<MarketPage>
], ],
), ),
), ),
const Divider(height: 1), const Divider(height: 1),
// Trading pairs list //
Expanded( Expanded(
child: ListView.separated( child: ListView.separated(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 100), padding: const EdgeInsets.fromLTRB(20, 0, 20, 100),
itemCount: _currentPairs.length, itemCount: _mockTradingItems.length,
separatorBuilder: (_, __) => const Divider(height: 1), separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final pair = _currentPairs[index]; final item = _mockTradingItems[index];
return _buildTradingPairRow(context, pair); return _buildTradingRow(context, item);
}, },
), ),
), ),
@ -288,55 +395,18 @@ class _MarketPageState extends State<MarketPage>
); );
} }
Widget _buildPairTypeTabs() { Widget _buildTradingRow(BuildContext context, _TradingItem item) {
final tabs = ['券/法币', '券/数字货币', '券/稳定币', '收藏']; final isPositive = item.change24h >= 0;
return SizedBox(
height: 36,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: tabs.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final isSelected = _pairTypeIndex == index;
return GestureDetector(
onTap: () => setState(() => _pairTypeIndex = index),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? AppColors.primary : AppColors.gray50,
borderRadius: AppSpacing.borderRadiusFull,
border:
isSelected ? null : Border.all(color: AppColors.borderLight),
),
alignment: Alignment.center,
child: Text(
tabs[index],
style: AppTypography.labelSmall.copyWith(
color: isSelected ? Colors.white : AppColors.textSecondary,
),
),
),
);
},
),
);
}
Widget _buildTradingPairRow(BuildContext context, _TradingPair pair) {
final isPositive = pair.change24h >= 0;
final changeColor = isPositive ? AppColors.success : AppColors.error; final changeColor = isPositive ? AppColors.success : AppColors.error;
final changePrefix = isPositive ? '+' : ''; final changePrefix = isPositive ? '+' : '';
return InkWell( return InkWell(
onTap: () { onTap: () => Navigator.pushNamed(context, '/trading/detail'),
Navigator.pushNamed(context, '/trading/detail', arguments: pair);
},
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
child: Row( child: Row(
children: [ children: [
// Trading pair name // + +
Expanded( Expanded(
flex: 3, flex: 3,
child: Column( child: Column(
@ -345,63 +415,71 @@ class _MarketPageState extends State<MarketPage>
Row( Row(
children: [ children: [
Text( Text(
pair.baseName, item.couponName,
style: AppTypography.labelMedium.copyWith( style: AppTypography.labelMedium.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
Text( const SizedBox(width: 4),
' / ${pair.quoteName}', Container(
style: AppTypography.bodySmall.copyWith( padding: const EdgeInsets.symmetric(
color: AppColors.textTertiary, horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: AppColors.gray100,
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(
item.categoryLabel,
style: AppTypography.caption
.copyWith(fontSize: 9),
), ),
), ),
], ],
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'Vol ${pair.volume24h}', '${item.brandName} · Vol ${item.volume24h}',
style: AppTypography.caption, style: AppTypography.caption,
), ),
], ],
), ),
), ),
// Price //
Expanded( Expanded(
flex: 2, flex: 2,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
pair.priceDisplay, '\$${item.currentPrice.toStringAsFixed(2)}',
style: AppTypography.labelMedium.copyWith( style: AppTypography.labelMedium.copyWith(
color: changeColor, color: changeColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
Text( Text(
'\$${pair.priceUsd.toStringAsFixed(2)}', '面值 \$${item.faceValue.toStringAsFixed(0)}',
style: AppTypography.caption, style: AppTypography.caption,
), ),
], ],
), ),
), ),
// 24h Change // 24h涨跌
Expanded( Expanded(
flex: 2, flex: 2,
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 10, vertical: 6), horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
color: changeColor, color: changeColor,
borderRadius: AppSpacing.borderRadiusSm, borderRadius: AppSpacing.borderRadiusSm,
), ),
child: Text( child: Text(
'$changePrefix${pair.change24h.toStringAsFixed(2)}%', '$changePrefix${item.change24h.toStringAsFixed(2)}%',
style: AppTypography.labelSmall.copyWith( style: AppTypography.labelSmall.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -415,21 +493,6 @@ class _MarketPageState extends State<MarketPage>
), ),
); );
} }
List<_TradingPair> get _currentPairs {
switch (_pairTypeIndex) {
case 0:
return _fiatPairs;
case 1:
return _cryptoPairs;
case 2:
return _stablePairs;
case 3:
return _favoritePairs;
default:
return _fiatPairs;
}
}
} }
// ============================================================ // ============================================================
@ -438,6 +501,7 @@ class _MarketPageState extends State<MarketPage>
class _LaunchItem { class _LaunchItem {
final String brandName; final String brandName;
final String couponName; final String couponName;
final String categoryLabel;
final double issuePrice; final double issuePrice;
final double faceValue; final double faceValue;
final int totalSupply; final int totalSupply;
@ -448,6 +512,7 @@ class _LaunchItem {
const _LaunchItem({ const _LaunchItem({
required this.brandName, required this.brandName,
required this.couponName, required this.couponName,
required this.categoryLabel,
required this.issuePrice, required this.issuePrice,
required this.faceValue, required this.faceValue,
required this.totalSupply, required this.totalSupply,
@ -483,52 +548,44 @@ class _LaunchItem {
} }
} }
class _TradingPair { class _TradingItem {
final String baseName; final String couponName;
final String quoteName; final String brandName;
final double price; final String categoryLabel;
final double priceUsd; final double faceValue;
final double currentPrice;
final double change24h; final double change24h;
final String volume24h; final String volume24h;
final double high24h;
final double low24h;
final double open24h;
const _TradingPair({ const _TradingItem({
required this.baseName, required this.couponName,
required this.quoteName, required this.brandName,
required this.price, required this.categoryLabel,
required this.priceUsd, required this.faceValue,
required this.currentPrice,
required this.change24h, required this.change24h,
required this.volume24h, required this.volume24h,
required this.high24h,
required this.low24h,
required this.open24h,
}); });
String get priceDisplay => price >= 1
? price.toStringAsFixed(2)
: price.toStringAsFixed(6);
String get pairSymbol => '$baseName/$quoteName';
} }
// ============================================================ // ============================================================
// Mock Data // Mock Data
// ============================================================ // ============================================================
final _mockLaunches = [ const _mockLaunches = [
const _LaunchItem( _LaunchItem(
brandName: 'Starbucks', brandName: 'Starbucks',
couponName: '星巴克 \$25 礼品卡 2026春季限定', couponName: '星巴克 \$25 礼品卡 2026春季限定',
categoryLabel: '餐饮',
issuePrice: 21.25, issuePrice: 21.25,
faceValue: 25.0, faceValue: 25.0,
totalSupply: 10000, totalSupply: 10000,
soldPercent: 0.73, soldPercent: 0.73,
status: 1, status: 1,
), ),
const _LaunchItem( _LaunchItem(
brandName: 'Nike', brandName: 'Nike',
couponName: 'Nike \$100 运动券 Air Max 联名', couponName: 'Nike \$100 运动券 Air Max 联名',
categoryLabel: '运动',
issuePrice: 82.0, issuePrice: 82.0,
faceValue: 100.0, faceValue: 100.0,
totalSupply: 5000, totalSupply: 5000,
@ -536,110 +593,109 @@ final _mockLaunches = [
status: 0, status: 0,
countdown: '2天 14:30:00', countdown: '2天 14:30:00',
), ),
const _LaunchItem( _LaunchItem(
brandName: 'Amazon', brandName: 'Amazon',
couponName: 'Amazon \$50 购物券 Prime专属', couponName: 'Amazon \$50 购物券 Prime专属',
categoryLabel: '购物',
issuePrice: 42.5, issuePrice: 42.5,
faceValue: 50.0, faceValue: 50.0,
totalSupply: 20000, totalSupply: 20000,
soldPercent: 1.0, soldPercent: 1.0,
status: 2, status: 2,
), ),
const _LaunchItem( _LaunchItem(
brandName: 'Walmart', brandName: 'Walmart',
couponName: 'Walmart \$30 生活券', couponName: 'Walmart \$30 生活券',
categoryLabel: '生活',
issuePrice: 24.0, issuePrice: 24.0,
faceValue: 30.0, faceValue: 30.0,
totalSupply: 15000, totalSupply: 15000,
soldPercent: 0.45, soldPercent: 0.45,
status: 1, status: 1,
), ),
]; _LaunchItem(
brandName: 'AMC',
// / trading pairs couponName: 'AMC \$20 电影券 IMAX场',
final _fiatPairs = [ categoryLabel: '娱乐',
const _TradingPair( issuePrice: 16.0,
baseName: 'SBUX', quoteName: 'USD', faceValue: 20.0,
price: 21.35, priceUsd: 21.35, change24h: 2.15, totalSupply: 8000,
volume24h: '125.3K', high24h: 21.80, low24h: 20.90, open24h: 20.90, soldPercent: 0.88,
), status: 1,
const _TradingPair(
baseName: 'AMZN', quoteName: 'USD',
price: 85.20, priceUsd: 85.20, change24h: -1.23,
volume24h: '89.7K', high24h: 87.50, low24h: 84.10, open24h: 86.26,
),
const _TradingPair(
baseName: 'NIKE', quoteName: 'USD',
price: 68.50, priceUsd: 68.50, change24h: 5.32,
volume24h: '234.1K', high24h: 69.20, low24h: 65.00, open24h: 65.04,
),
const _TradingPair(
baseName: 'TGT', quoteName: 'CNY',
price: 168.80, priceUsd: 24.00, change24h: -0.56,
volume24h: '45.2K', high24h: 170.50, low24h: 167.00, open24h: 169.75,
),
const _TradingPair(
baseName: 'WMT', quoteName: 'USD',
price: 42.30, priceUsd: 42.30, change24h: 1.87,
volume24h: '67.8K', high24h: 43.00, low24h: 41.50, open24h: 41.52,
),
const _TradingPair(
baseName: 'COST', quoteName: 'USD',
price: 38.75, priceUsd: 38.75, change24h: 3.41,
volume24h: '156.2K', high24h: 39.50, low24h: 37.40, open24h: 37.47,
), ),
]; ];
// / trading pairs const _mockTradingItems = [
final _cryptoPairs = [ _TradingItem(
const _TradingPair( couponName: '星巴克\$25',
baseName: 'SBUX', quoteName: 'BTC', brandName: 'Starbucks',
price: 0.000215, priceUsd: 21.35, change24h: 1.85, categoryLabel: '餐饮',
volume24h: '12.5K', high24h: 0.000220, low24h: 0.000210, open24h: 0.000211, faceValue: 25.0,
currentPrice: 21.35,
change24h: 2.15,
volume24h: '125.3K',
), ),
const _TradingPair( _TradingItem(
baseName: 'AMZN', quoteName: 'ETH', couponName: 'Amazon\$100',
price: 0.0234, priceUsd: 85.20, change24h: -2.10, brandName: 'Amazon',
volume24h: '8.3K', high24h: 0.0240, low24h: 0.0230, open24h: 0.0239, categoryLabel: '购物',
faceValue: 100.0,
currentPrice: 85.20,
change24h: -1.23,
volume24h: '89.7K',
), ),
const _TradingPair( _TradingItem(
baseName: 'NIKE', quoteName: 'BTC', couponName: 'Nike\$80',
price: 0.000690, priceUsd: 68.50, change24h: 4.56, brandName: 'Nike',
volume24h: '15.7K', high24h: 0.000700, low24h: 0.000660, open24h: 0.000660, categoryLabel: '运动',
faceValue: 80.0,
currentPrice: 68.50,
change24h: 5.32,
volume24h: '234.1K',
),
_TradingItem(
couponName: 'Target\$30',
brandName: 'Target',
categoryLabel: '购物',
faceValue: 30.0,
currentPrice: 24.00,
change24h: -0.56,
volume24h: '45.2K',
),
_TradingItem(
couponName: 'Walmart\$50',
brandName: 'Walmart',
categoryLabel: '生活',
faceValue: 50.0,
currentPrice: 42.30,
change24h: 1.87,
volume24h: '67.8K',
),
_TradingItem(
couponName: 'Costco\$40',
brandName: 'Costco',
categoryLabel: '购物',
faceValue: 40.0,
currentPrice: 33.60,
change24h: 3.41,
volume24h: '156.2K',
),
_TradingItem(
couponName: 'AMC\$20',
brandName: 'AMC',
categoryLabel: '娱乐',
faceValue: 20.0,
currentPrice: 16.80,
change24h: -2.10,
volume24h: '78.5K',
),
_TradingItem(
couponName: 'Uber\$25',
brandName: 'Uber',
categoryLabel: '出行',
faceValue: 25.0,
currentPrice: 21.00,
change24h: 0.95,
volume24h: '34.1K',
), ),
]; ];
// / trading pairs
final _stablePairs = [
const _TradingPair(
baseName: 'SBUX', quoteName: 'USDT',
price: 21.30, priceUsd: 21.30, change24h: 2.05,
volume24h: '342.5K', high24h: 21.75, low24h: 20.85, open24h: 20.87,
),
const _TradingPair(
baseName: 'AMZN', quoteName: 'USDT',
price: 85.10, priceUsd: 85.10, change24h: -1.35,
volume24h: '189.2K', high24h: 87.40, low24h: 84.00, open24h: 86.26,
),
const _TradingPair(
baseName: 'NIKE', quoteName: 'USDC',
price: 68.45, priceUsd: 68.45, change24h: 5.20,
volume24h: '278.9K', high24h: 69.10, low24h: 64.90, open24h: 65.07,
),
const _TradingPair(
baseName: 'WMT', quoteName: 'USDT',
price: 42.25, priceUsd: 42.25, change24h: 1.92,
volume24h: '98.4K', high24h: 42.90, low24h: 41.45, open24h: 41.45,
),
const _TradingPair(
baseName: 'TGT', quoteName: 'USDC',
price: 24.10, priceUsd: 24.10, change24h: -0.41,
volume24h: '34.1K', high24h: 24.50, low24h: 23.80, open24h: 24.20,
),
];
// Favorites
final _favoritePairs = [
_stablePairs[0], // SBUX/USDT
_fiatPairs[2], // NIKE/USD
];

View File

@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_colors.dart';
import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_typography.dart';
import '../../../../app/theme/app_spacing.dart'; import '../../../../app/theme/app_spacing.dart';
import '../../../../shared/widgets/genex_button.dart';
/// - /// -
/// ///
/// K线图 + OHLC + + + /// + K线图 + OHLC + + +
/// "我的→设置"
class TradingDetailPage extends StatefulWidget { class TradingDetailPage extends StatefulWidget {
const TradingDetailPage({super.key}); const TradingDetailPage({super.key});
@ -22,6 +22,21 @@ class _TradingDetailPageState extends State<TradingDetailPage>
String _orderType = 'limit'; // limit, market String _orderType = 'limit'; // limit, market
late TabController _bottomTabController; late TabController _bottomTabController;
// Mock coupon data ()
final _coupon = const _CouponInfo(
couponName: '星巴克\$25 礼品卡',
brandName: 'Starbucks',
categoryLabel: '餐饮',
faceValue: 25.0,
currentPrice: 21.30,
change24h: 2.05,
creditRating: 'AAA',
expiryDate: '2026/06/30',
);
// ()
final String _currencySymbol = '\$';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -38,14 +53,14 @@ class _TradingDetailPageState extends State<TradingDetailPage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('SBUX / USDT'), title: Text(_coupon.couponName),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.star_border_rounded, size: 22), icon: const Icon(Icons.star_border_rounded, size: 22),
onPressed: () {}, onPressed: () {},
), ),
IconButton( IconButton(
icon: const Icon(Icons.more_horiz_rounded, size: 22), icon: const Icon(Icons.share_rounded, size: 22),
onPressed: () {}, onPressed: () {},
), ),
], ],
@ -57,6 +72,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Coupon info card
_buildCouponInfoCard(),
// Price header // Price header
_buildPriceHeader(), _buildPriceHeader(),
@ -94,12 +112,93 @@ class _TradingDetailPageState extends State<TradingDetailPage>
); );
} }
// ============================================================
// Coupon Info Card -
// ============================================================
Widget _buildCouponInfoCard() {
return Container(
margin: const EdgeInsets.fromLTRB(20, 8, 20, 0),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.primarySurface,
borderRadius: AppSpacing.borderRadiusMd,
border: Border.all(color: AppColors.primary.withValues(alpha: 0.15)),
),
child: Row(
children: [
// Icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: AppSpacing.borderRadiusSm,
),
child: const Icon(Icons.confirmation_number_outlined,
color: AppColors.primary, size: 20),
),
const SizedBox(width: 12),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(_coupon.brandName,
style: AppTypography.labelMedium),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: AppColors.gray100,
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(_coupon.categoryLabel,
style:
AppTypography.caption.copyWith(fontSize: 10)),
),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 5, vertical: 1),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.1),
borderRadius: AppSpacing.borderRadiusFull,
),
child: Text(_coupon.creditRating,
style: AppTypography.caption.copyWith(
fontSize: 10,
color: AppColors.success,
fontWeight: FontWeight.w600,
)),
),
],
),
const SizedBox(height: 2),
Text(
'面值 $_currencySymbol${_coupon.faceValue.toStringAsFixed(0)} · 到期 ${_coupon.expiryDate}',
style: AppTypography.caption,
),
],
),
),
],
),
);
}
// ============================================================ // ============================================================
// Price Header - OHLC + 24h stats // Price Header - OHLC + 24h stats
// ============================================================ // ============================================================
Widget _buildPriceHeader() { Widget _buildPriceHeader() {
final isPositive = _coupon.change24h >= 0;
final changeColor = isPositive ? AppColors.success : AppColors.error;
final changePrefix = isPositive ? '+' : '';
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -108,15 +207,15 @@ class _TradingDetailPageState extends State<TradingDetailPage>
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
'21.30', '$_currencySymbol${_coupon.currentPrice.toStringAsFixed(2)}',
style: AppTypography.priceLarge.copyWith( style: AppTypography.priceLarge.copyWith(
color: AppColors.success, color: changeColor,
fontSize: 32, fontSize: 32,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'\$21.30', '折扣 ${(_coupon.currentPrice / _coupon.faceValue * 10).toStringAsFixed(1)}',
style: AppTypography.bodySmall.copyWith( style: AppTypography.bodySmall.copyWith(
color: AppColors.textTertiary, color: AppColors.textTertiary,
), ),
@ -126,11 +225,11 @@ class _TradingDetailPageState extends State<TradingDetailPage>
padding: padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4), const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.success, color: changeColor,
borderRadius: AppSpacing.borderRadiusSm, borderRadius: AppSpacing.borderRadiusSm,
), ),
child: Text( child: Text(
'+2.05%', '$changePrefix${_coupon.change24h.toStringAsFixed(2)}%',
style: AppTypography.labelSmall.copyWith( style: AppTypography.labelSmall.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -145,9 +244,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
// 24h OHLC stats // 24h OHLC stats
Row( Row(
children: [ children: [
_buildOhlcStat('24h高', '21.75', AppColors.error), _buildOhlcStat('24h高', '${_currencySymbol}21.75', AppColors.error),
_buildOhlcStat('24h低', '20.85', AppColors.success), _buildOhlcStat('24h低', '${_currencySymbol}20.85', AppColors.success),
_buildOhlcStat('开盘', '20.87', AppColors.textPrimary), _buildOhlcStat('开盘', '${_currencySymbol}20.87', AppColors.textPrimary),
_buildOhlcStat('24h量', '342.5K', AppColors.textPrimary), _buildOhlcStat('24h量', '342.5K', AppColors.textPrimary),
], ],
), ),
@ -199,7 +298,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
top: 8, top: 8,
left: 12, left: 12,
child: Text( child: Text(
'SBUX/USDT · $_selectedPeriod', '${_coupon.couponName} · $_selectedPeriod',
style: AppTypography.caption.copyWith( style: AppTypography.caption.copyWith(
color: AppColors.textTertiary, color: AppColors.textTertiary,
), ),
@ -291,7 +390,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(label, style: AppTypography.caption.copyWith(color: color)), Text(label, style: AppTypography.caption.copyWith(color: color)),
Text('数量', style: AppTypography.caption), Text('数量(张)', style: AppTypography.caption),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -322,7 +421,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
prices[i].toStringAsFixed(2), '$_currencySymbol${prices[i].toStringAsFixed(2)}',
style: AppTypography.caption.copyWith( style: AppTypography.caption.copyWith(
color: color, color: color,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -427,7 +526,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
// Price input (hidden for market orders) // Price input (hidden for market orders)
if (_orderType == 'limit') ...[ if (_orderType == 'limit') ...[
_buildInputField('价格', '21.30', 'USDT'), _buildInputField('价格', '21.30', _currencySymbol),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
@ -457,7 +556,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
children: [ children: [
Text('可用', style: AppTypography.caption), Text('可用', style: AppTypography.caption),
Text( Text(
_isBuy ? '1,234.56 USDT' : '3 张 SBUX', _isBuy
? '${_currencySymbol}1,234.56'
: '3 张 ${_coupon.couponName}',
style: AppTypography.caption.copyWith( style: AppTypography.caption.copyWith(
color: AppColors.textPrimary, color: AppColors.textPrimary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -483,7 +584,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
), ),
), ),
child: Text( child: Text(
_isBuy ? '买入 SBUX' : '卖出 SBUX', _isBuy ? '买入 ${_coupon.couponName}' : '卖出 ${_coupon.couponName}',
style: AppTypography.labelLarge.copyWith(color: Colors.white), style: AppTypography.labelLarge.copyWith(color: Colors.white),
), ),
), ),
@ -605,17 +706,17 @@ class _TradingDetailPageState extends State<TradingDetailPage>
const SizedBox(height: 12), const SizedBox(height: 12),
// Mock orders // Mock orders
_buildOrderItem('买入', 'SBUX/USDT', '21.20', '5', '限价', _buildOrderItem(
AppColors.success), '买入', _coupon.couponName, '21.20', '5', '限价', AppColors.success),
const Divider(height: 1), const Divider(height: 1),
_buildOrderItem('卖出', 'SBUX/USDT', '21.50', '2', '限价', _buildOrderItem(
AppColors.error), '卖出', _coupon.couponName, '21.50', '2', '限价', AppColors.error),
], ],
), ),
); );
} }
Widget _buildOrderItem(String side, String pair, String price, Widget _buildOrderItem(String side, String couponName, String price,
String amount, String type, Color sideColor) { String amount, String type, Color sideColor) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
@ -640,8 +741,8 @@ class _TradingDetailPageState extends State<TradingDetailPage>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(pair, style: AppTypography.labelSmall), Text(couponName, style: AppTypography.labelSmall),
Text('$type · $amount张 @ $price', Text('$type · ${amount}张 @ $_currencySymbol$price',
style: AppTypography.caption), style: AppTypography.caption),
], ],
), ),
@ -720,6 +821,31 @@ class _TradingDetailPageState extends State<TradingDetailPage>
} }
} }
// ============================================================
// Data Model
// ============================================================
class _CouponInfo {
final String couponName;
final String brandName;
final String categoryLabel;
final double faceValue;
final double currentPrice;
final double change24h;
final String creditRating;
final String expiryDate;
const _CouponInfo({
required this.couponName,
required this.brandName,
required this.categoryLabel,
required this.faceValue,
required this.currentPrice,
required this.change24h,
required this.creditRating,
required this.expiryDate,
});
}
// ============================================================ // ============================================================
// Mock Candlestick Chart Painter // Mock Candlestick Chart Painter
// ============================================================ // ============================================================