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 {
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
];

View File

@ -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
// ============================================================