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:
parent
d9c953149b
commit
003b571f94
|
|
@ -5,8 +5,9 @@ import '../../../../app/theme/app_spacing.dart';
|
|||
|
||||
/// 交易 - 币安风格交易所
|
||||
///
|
||||
/// 一级市场(打新申购)/ 二级市场(交易所)
|
||||
/// 支持交易对:券/法币、券/数字货币、券/稳定币
|
||||
/// 一级市场(打新申购)/ 二级市场(交易所行情)
|
||||
/// 行业分类过滤 + 排序 + 券列表
|
||||
/// 计价货币由用户在"我的→设置"中配置,默认跟随语言(中文=¥,英文=$)
|
||||
class MarketPage extends StatefulWidget {
|
||||
const MarketPage({super.key});
|
||||
|
||||
|
|
@ -17,7 +18,8 @@ class MarketPage extends StatefulWidget {
|
|||
class _MarketPageState extends State<MarketPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _marketTabController;
|
||||
int _pairTypeIndex = 0; // 0=券/法币, 1=券/数字货币, 2=券/稳定币, 3=收藏
|
||||
String? _selectedCategory; // null = 全部
|
||||
int _sortIndex = 0; // 0=折扣率, 1=价格↑, 2=价格↓, 3=到期时间
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -69,22 +71,113 @@ class _MarketPageState extends State<MarketPage>
|
|||
),
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _marketTabController,
|
||||
body: Column(
|
||||
children: [
|
||||
_buildPrimaryMarket(),
|
||||
_buildSecondaryMarket(),
|
||||
const SizedBox(height: 12),
|
||||
// 行业分类过滤
|
||||
_buildCategoryFilter(),
|
||||
const SizedBox(height: 8),
|
||||
// 排序栏
|
||||
_buildSortBar(),
|
||||
const Divider(height: 1),
|
||||
// 内容
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _marketTabController,
|
||||
children: [
|
||||
_buildPrimaryMarket(),
|
||||
_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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 一级市场 - 打新申购 (Launchpad style)
|
||||
// ============================================================
|
||||
Widget _buildPrimaryMarket() {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 100),
|
||||
itemCount: _mockLaunches.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
|
|
@ -108,7 +201,7 @@ class _MarketPageState extends State<MarketPage>
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header: brand + status badge
|
||||
// Header: brand + category + status
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
|
|
@ -126,14 +219,32 @@ class _MarketPageState extends State<MarketPage>
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(launch.brandName, style: AppTypography.labelMedium),
|
||||
Text(launch.couponName, style: AppTypography.bodySmall),
|
||||
Row(
|
||||
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(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: launch.statusColor.withValues(alpha: 0.1),
|
||||
borderRadius: AppSpacing.borderRadiusFull,
|
||||
|
|
@ -151,19 +262,24 @@ class _MarketPageState extends State<MarketPage>
|
|||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Price + Supply info
|
||||
// Price info
|
||||
Row(
|
||||
children: [
|
||||
_buildLaunchInfo('发行价', '\$${launch.issuePrice.toStringAsFixed(2)}'),
|
||||
_buildLaunchInfo('面值', '\$${launch.faceValue.toStringAsFixed(0)}'),
|
||||
_buildLaunchInfo('折扣', '${(launch.issuePrice / launch.faceValue * 10).toStringAsFixed(1)}折'),
|
||||
_buildLaunchInfo('发行量', '${launch.totalSupply}'),
|
||||
_buildLaunchInfo('发行价',
|
||||
'\$${launch.issuePrice.toStringAsFixed(2)}'),
|
||||
_buildLaunchInfo(
|
||||
'面值', '\$${launch.faceValue.toStringAsFixed(0)}'),
|
||||
_buildLaunchInfo(
|
||||
'折扣',
|
||||
'${(launch.issuePrice / launch.faceValue * 10).toStringAsFixed(1)}折'),
|
||||
_buildLaunchInfo(
|
||||
'发行量', '${launch.totalSupply}'),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Progress bar
|
||||
// Progress
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -187,8 +303,8 @@ class _MarketPageState extends State<MarketPage>
|
|||
value: launch.soldPercent,
|
||||
minHeight: 6,
|
||||
backgroundColor: AppColors.gray100,
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(AppColors.primary),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
AppColors.primary),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -196,7 +312,6 @@ class _MarketPageState extends State<MarketPage>
|
|||
|
||||
if (launch.status == 0) ...[
|
||||
const SizedBox(height: 12),
|
||||
// Countdown
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time_rounded,
|
||||
|
|
@ -236,26 +351,19 @@ class _MarketPageState extends State<MarketPage>
|
|||
}
|
||||
|
||||
// ============================================================
|
||||
// 二级市场 - 交易所行情 (Binance style)
|
||||
// 二级市场 - 交易所行情(按行业分类浏览)
|
||||
// ============================================================
|
||||
Widget _buildSecondaryMarket() {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
// Pair type tabs
|
||||
_buildPairTypeTabs(),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Column headers
|
||||
// 表头
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child:
|
||||
Text('交易对', style: AppTypography.caption)),
|
||||
child: Text('券名/品牌', style: AppTypography.caption)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text('最新价',
|
||||
|
|
@ -269,18 +377,17 @@ class _MarketPageState extends State<MarketPage>
|
|||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Divider(height: 1),
|
||||
|
||||
// Trading pairs list
|
||||
// 券行情列表
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 100),
|
||||
itemCount: _currentPairs.length,
|
||||
itemCount: _mockTradingItems.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final pair = _currentPairs[index];
|
||||
return _buildTradingPairRow(context, pair);
|
||||
final item = _mockTradingItems[index];
|
||||
return _buildTradingRow(context, item);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -288,55 +395,18 @@ class _MarketPageState extends State<MarketPage>
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildPairTypeTabs() {
|
||||
final tabs = ['券/法币', '券/数字货币', '券/稳定币', '收藏'];
|
||||
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;
|
||||
Widget _buildTradingRow(BuildContext context, _TradingItem item) {
|
||||
final isPositive = item.change24h >= 0;
|
||||
final changeColor = isPositive ? AppColors.success : AppColors.error;
|
||||
final changePrefix = isPositive ? '+' : '';
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(context, '/trading/detail', arguments: pair);
|
||||
},
|
||||
onTap: () => Navigator.pushNamed(context, '/trading/detail'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
// Trading pair name
|
||||
// 券名 + 品牌 + 行业标签
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
|
|
@ -345,63 +415,71 @@ class _MarketPageState extends State<MarketPage>
|
|||
Row(
|
||||
children: [
|
||||
Text(
|
||||
pair.baseName,
|
||||
item.couponName,
|
||||
style: AppTypography.labelMedium.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' / ${pair.quoteName}',
|
||||
style: AppTypography.bodySmall.copyWith(
|
||||
color: AppColors.textTertiary,
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
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),
|
||||
Text(
|
||||
'Vol ${pair.volume24h}',
|
||||
'${item.brandName} · Vol ${item.volume24h}',
|
||||
style: AppTypography.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Price
|
||||
// 最新价
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
pair.priceDisplay,
|
||||
'\$${item.currentPrice.toStringAsFixed(2)}',
|
||||
style: AppTypography.labelMedium.copyWith(
|
||||
color: changeColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'≈ \$${pair.priceUsd.toStringAsFixed(2)}',
|
||||
'面值 \$${item.faceValue.toStringAsFixed(0)}',
|
||||
style: AppTypography.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 24h Change
|
||||
// 24h涨跌
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: changeColor,
|
||||
borderRadius: AppSpacing.borderRadiusSm,
|
||||
),
|
||||
child: Text(
|
||||
'$changePrefix${pair.change24h.toStringAsFixed(2)}%',
|
||||
'$changePrefix${item.change24h.toStringAsFixed(2)}%',
|
||||
style: AppTypography.labelSmall.copyWith(
|
||||
color: Colors.white,
|
||||
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 {
|
||||
final String brandName;
|
||||
final String couponName;
|
||||
final String categoryLabel;
|
||||
final double issuePrice;
|
||||
final double faceValue;
|
||||
final int totalSupply;
|
||||
|
|
@ -448,6 +512,7 @@ class _LaunchItem {
|
|||
const _LaunchItem({
|
||||
required this.brandName,
|
||||
required this.couponName,
|
||||
required this.categoryLabel,
|
||||
required this.issuePrice,
|
||||
required this.faceValue,
|
||||
required this.totalSupply,
|
||||
|
|
@ -483,52 +548,44 @@ class _LaunchItem {
|
|||
}
|
||||
}
|
||||
|
||||
class _TradingPair {
|
||||
final String baseName;
|
||||
final String quoteName;
|
||||
final double price;
|
||||
final double priceUsd;
|
||||
class _TradingItem {
|
||||
final String couponName;
|
||||
final String brandName;
|
||||
final String categoryLabel;
|
||||
final double faceValue;
|
||||
final double currentPrice;
|
||||
final double change24h;
|
||||
final String volume24h;
|
||||
final double high24h;
|
||||
final double low24h;
|
||||
final double open24h;
|
||||
|
||||
const _TradingPair({
|
||||
required this.baseName,
|
||||
required this.quoteName,
|
||||
required this.price,
|
||||
required this.priceUsd,
|
||||
const _TradingItem({
|
||||
required this.couponName,
|
||||
required this.brandName,
|
||||
required this.categoryLabel,
|
||||
required this.faceValue,
|
||||
required this.currentPrice,
|
||||
required this.change24h,
|
||||
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
|
||||
// ============================================================
|
||||
final _mockLaunches = [
|
||||
const _LaunchItem(
|
||||
const _mockLaunches = [
|
||||
_LaunchItem(
|
||||
brandName: 'Starbucks',
|
||||
couponName: '星巴克 \$25 礼品卡 2026春季限定',
|
||||
categoryLabel: '餐饮',
|
||||
issuePrice: 21.25,
|
||||
faceValue: 25.0,
|
||||
totalSupply: 10000,
|
||||
soldPercent: 0.73,
|
||||
status: 1,
|
||||
),
|
||||
const _LaunchItem(
|
||||
_LaunchItem(
|
||||
brandName: 'Nike',
|
||||
couponName: 'Nike \$100 运动券 Air Max 联名',
|
||||
categoryLabel: '运动',
|
||||
issuePrice: 82.0,
|
||||
faceValue: 100.0,
|
||||
totalSupply: 5000,
|
||||
|
|
@ -536,110 +593,109 @@ final _mockLaunches = [
|
|||
status: 0,
|
||||
countdown: '2天 14:30:00',
|
||||
),
|
||||
const _LaunchItem(
|
||||
_LaunchItem(
|
||||
brandName: 'Amazon',
|
||||
couponName: 'Amazon \$50 购物券 Prime专属',
|
||||
categoryLabel: '购物',
|
||||
issuePrice: 42.5,
|
||||
faceValue: 50.0,
|
||||
totalSupply: 20000,
|
||||
soldPercent: 1.0,
|
||||
status: 2,
|
||||
),
|
||||
const _LaunchItem(
|
||||
_LaunchItem(
|
||||
brandName: 'Walmart',
|
||||
couponName: 'Walmart \$30 生活券',
|
||||
categoryLabel: '生活',
|
||||
issuePrice: 24.0,
|
||||
faceValue: 30.0,
|
||||
totalSupply: 15000,
|
||||
soldPercent: 0.45,
|
||||
status: 1,
|
||||
),
|
||||
];
|
||||
|
||||
// 券/法币 trading pairs
|
||||
final _fiatPairs = [
|
||||
const _TradingPair(
|
||||
baseName: 'SBUX', quoteName: 'USD',
|
||||
price: 21.35, priceUsd: 21.35, change24h: 2.15,
|
||||
volume24h: '125.3K', high24h: 21.80, low24h: 20.90, open24h: 20.90,
|
||||
),
|
||||
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,
|
||||
_LaunchItem(
|
||||
brandName: 'AMC',
|
||||
couponName: 'AMC \$20 电影券 IMAX场',
|
||||
categoryLabel: '娱乐',
|
||||
issuePrice: 16.0,
|
||||
faceValue: 20.0,
|
||||
totalSupply: 8000,
|
||||
soldPercent: 0.88,
|
||||
status: 1,
|
||||
),
|
||||
];
|
||||
|
||||
// 券/数字货币 trading pairs
|
||||
final _cryptoPairs = [
|
||||
const _TradingPair(
|
||||
baseName: 'SBUX', quoteName: 'BTC',
|
||||
price: 0.000215, priceUsd: 21.35, change24h: 1.85,
|
||||
volume24h: '12.5K', high24h: 0.000220, low24h: 0.000210, open24h: 0.000211,
|
||||
const _mockTradingItems = [
|
||||
_TradingItem(
|
||||
couponName: '星巴克\$25',
|
||||
brandName: 'Starbucks',
|
||||
categoryLabel: '餐饮',
|
||||
faceValue: 25.0,
|
||||
currentPrice: 21.35,
|
||||
change24h: 2.15,
|
||||
volume24h: '125.3K',
|
||||
),
|
||||
const _TradingPair(
|
||||
baseName: 'AMZN', quoteName: 'ETH',
|
||||
price: 0.0234, priceUsd: 85.20, change24h: -2.10,
|
||||
volume24h: '8.3K', high24h: 0.0240, low24h: 0.0230, open24h: 0.0239,
|
||||
_TradingItem(
|
||||
couponName: 'Amazon\$100',
|
||||
brandName: 'Amazon',
|
||||
categoryLabel: '购物',
|
||||
faceValue: 100.0,
|
||||
currentPrice: 85.20,
|
||||
change24h: -1.23,
|
||||
volume24h: '89.7K',
|
||||
),
|
||||
const _TradingPair(
|
||||
baseName: 'NIKE', quoteName: 'BTC',
|
||||
price: 0.000690, priceUsd: 68.50, change24h: 4.56,
|
||||
volume24h: '15.7K', high24h: 0.000700, low24h: 0.000660, open24h: 0.000660,
|
||||
_TradingItem(
|
||||
couponName: 'Nike\$80',
|
||||
brandName: 'Nike',
|
||||
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
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import 'package:flutter/material.dart';
|
|||
import '../../../../app/theme/app_colors.dart';
|
||||
import '../../../../app/theme/app_typography.dart';
|
||||
import '../../../../app/theme/app_spacing.dart';
|
||||
import '../../../../shared/widgets/genex_button.dart';
|
||||
|
||||
/// 交易对详情页 - 币安风格
|
||||
/// 券交易详情页 - 币安风格
|
||||
///
|
||||
/// K线图 + OHLC + 交易深度 + 买卖盘口 + 下单
|
||||
/// 券信息卡片 + K线图 + OHLC + 交易深度 + 买卖盘口 + 下单
|
||||
/// 计价货币由用户在"我的→设置"中配置,默认跟随语言
|
||||
class TradingDetailPage extends StatefulWidget {
|
||||
const TradingDetailPage({super.key});
|
||||
|
||||
|
|
@ -22,6 +22,21 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
String _orderType = 'limit'; // limit, market
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -38,14 +53,14 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('SBUX / USDT'),
|
||||
title: Text(_coupon.couponName),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.star_border_rounded, size: 22),
|
||||
onPressed: () {},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_horiz_rounded, size: 22),
|
||||
icon: const Icon(Icons.share_rounded, size: 22),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
|
|
@ -57,6 +72,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Coupon info card
|
||||
_buildCouponInfoCard(),
|
||||
|
||||
// Price header
|
||||
_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
|
||||
// ============================================================
|
||||
Widget _buildPriceHeader() {
|
||||
final isPositive = _coupon.change24h >= 0;
|
||||
final changeColor = isPositive ? AppColors.success : AppColors.error;
|
||||
final changePrefix = isPositive ? '+' : '';
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
@ -108,15 +207,15 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'21.30',
|
||||
'$_currencySymbol${_coupon.currentPrice.toStringAsFixed(2)}',
|
||||
style: AppTypography.priceLarge.copyWith(
|
||||
color: AppColors.success,
|
||||
color: changeColor,
|
||||
fontSize: 32,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'≈ \$21.30',
|
||||
'折扣 ${(_coupon.currentPrice / _coupon.faceValue * 10).toStringAsFixed(1)}折',
|
||||
style: AppTypography.bodySmall.copyWith(
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
|
|
@ -126,11 +225,11 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.success,
|
||||
color: changeColor,
|
||||
borderRadius: AppSpacing.borderRadiusSm,
|
||||
),
|
||||
child: Text(
|
||||
'+2.05%',
|
||||
'$changePrefix${_coupon.change24h.toStringAsFixed(2)}%',
|
||||
style: AppTypography.labelSmall.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
|
@ -145,9 +244,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
// 24h OHLC stats
|
||||
Row(
|
||||
children: [
|
||||
_buildOhlcStat('24h高', '21.75', AppColors.error),
|
||||
_buildOhlcStat('24h低', '20.85', AppColors.success),
|
||||
_buildOhlcStat('开盘', '20.87', AppColors.textPrimary),
|
||||
_buildOhlcStat('24h高', '${_currencySymbol}21.75', AppColors.error),
|
||||
_buildOhlcStat('24h低', '${_currencySymbol}20.85', AppColors.success),
|
||||
_buildOhlcStat('开盘', '${_currencySymbol}20.87', AppColors.textPrimary),
|
||||
_buildOhlcStat('24h量', '342.5K', AppColors.textPrimary),
|
||||
],
|
||||
),
|
||||
|
|
@ -199,7 +298,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
top: 8,
|
||||
left: 12,
|
||||
child: Text(
|
||||
'SBUX/USDT · $_selectedPeriod',
|
||||
'${_coupon.couponName} · $_selectedPeriod',
|
||||
style: AppTypography.caption.copyWith(
|
||||
color: AppColors.textTertiary,
|
||||
),
|
||||
|
|
@ -291,7 +390,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label, style: AppTypography.caption.copyWith(color: color)),
|
||||
Text('数量', style: AppTypography.caption),
|
||||
Text('数量(张)', style: AppTypography.caption),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
|
@ -322,7 +421,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
prices[i].toStringAsFixed(2),
|
||||
'$_currencySymbol${prices[i].toStringAsFixed(2)}',
|
||||
style: AppTypography.caption.copyWith(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -427,7 +526,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
|
||||
// Price input (hidden for market orders)
|
||||
if (_orderType == 'limit') ...[
|
||||
_buildInputField('价格', '21.30', 'USDT'),
|
||||
_buildInputField('价格', '21.30', _currencySymbol),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
|
|
@ -457,7 +556,9 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
children: [
|
||||
Text('可用', style: AppTypography.caption),
|
||||
Text(
|
||||
_isBuy ? '1,234.56 USDT' : '3 张 SBUX',
|
||||
_isBuy
|
||||
? '${_currencySymbol}1,234.56'
|
||||
: '3 张 ${_coupon.couponName}',
|
||||
style: AppTypography.caption.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
|
@ -483,7 +584,7 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
),
|
||||
),
|
||||
child: Text(
|
||||
_isBuy ? '买入 SBUX' : '卖出 SBUX',
|
||||
_isBuy ? '买入 ${_coupon.couponName}' : '卖出 ${_coupon.couponName}',
|
||||
style: AppTypography.labelLarge.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
|
|
@ -605,17 +706,17 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
const SizedBox(height: 12),
|
||||
|
||||
// Mock orders
|
||||
_buildOrderItem('买入', 'SBUX/USDT', '21.20', '5', '限价',
|
||||
AppColors.success),
|
||||
_buildOrderItem(
|
||||
'买入', _coupon.couponName, '21.20', '5', '限价', AppColors.success),
|
||||
const Divider(height: 1),
|
||||
_buildOrderItem('卖出', 'SBUX/USDT', '21.50', '2', '限价',
|
||||
AppColors.error),
|
||||
_buildOrderItem(
|
||||
'卖出', _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) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
|
|
@ -640,8 +741,8 @@ class _TradingDetailPageState extends State<TradingDetailPage>
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(pair, style: AppTypography.labelSmall),
|
||||
Text('$type · $amount张 @ $price',
|
||||
Text(couponName, style: AppTypography.labelSmall),
|
||||
Text('$type · ${amount}张 @ $_currencySymbol$price',
|
||||
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
|
||||
// ============================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue