feat(trading): 添加涨跌幅显示及修复成交明细数据

1. 后端:
   - 添加 getFirstSnapshot() 获取上线首日价格
   - PriceInfo 接口增加 priceChangePercent 和 initialPrice 字段
   - 计算涨跌幅 = (当前价格 - 首日价格) / 首日价格 × 100%
   - 修复 originalQuantity 为0时的数据计算逻辑

2. 前端:
   - 交易页面涨跌幅移到价格下方单独显示
   - 添加"较上线首日"说明文字
   - 根据涨跌正负显示不同颜色和图标

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 05:24:23 -08:00
parent 7ff7157115
commit 995dfa898e
9 changed files with 108 additions and 23 deletions

View File

@ -17,6 +17,8 @@ export interface PriceInfo {
burnMultiplier: string; burnMultiplier: string;
minuteBurnRate: string; minuteBurnRate: string;
snapshotTime: Date; snapshotTime: Date;
priceChangePercent: string; // 较上线首日涨跌幅
initialPrice: string; // 上线首日价格
} }
@Injectable() @Injectable()
@ -36,11 +38,12 @@ export class PriceService {
* *
*/ */
async getCurrentPrice(): Promise<PriceInfo> { async getCurrentPrice(): Promise<PriceInfo> {
const [sharePool, blackHole, circulationPool, config] = await Promise.all([ const [sharePool, blackHole, circulationPool, config, firstSnapshot] = await Promise.all([
this.sharePoolRepository.getPool(), this.sharePoolRepository.getPool(),
this.blackHoleRepository.getBlackHole(), this.blackHoleRepository.getBlackHole(),
this.circulationPoolRepository.getPool(), this.circulationPoolRepository.getPool(),
this.tradingConfigRepository.getConfig(), this.tradingConfigRepository.getConfig(),
this.priceSnapshotRepository.getFirstSnapshot(),
]); ]);
const greenPoints = sharePool?.greenPoints || Money.zero(); const greenPoints = sharePool?.greenPoints || Money.zero();
@ -65,6 +68,13 @@ export class PriceService {
// 获取当前每分钟销毁率 // 获取当前每分钟销毁率
const minuteBurnRate = config?.minuteBurnRate || Money.zero(); const minuteBurnRate = config?.minuteBurnRate || Money.zero();
// 计算较上线首日的涨跌幅
const initialPrice = firstSnapshot?.price || price;
let priceChangePercent = new Decimal(0);
if (!initialPrice.isZero()) {
priceChangePercent = price.value.minus(initialPrice.value).dividedBy(initialPrice.value).times(100);
}
return { return {
price: price.toFixed(18), price: price.toFixed(18),
greenPoints: greenPoints.toFixed(8), greenPoints: greenPoints.toFixed(8),
@ -74,6 +84,8 @@ export class PriceService {
burnMultiplier: burnMultiplier.toFixed(18), burnMultiplier: burnMultiplier.toFixed(18),
minuteBurnRate: minuteBurnRate.toFixed(18), minuteBurnRate: minuteBurnRate.toFixed(18),
snapshotTime: new Date(), snapshotTime: new Date(),
priceChangePercent: priceChangePercent.toFixed(2),
initialPrice: initialPrice.toFixed(18),
}; };
} }
@ -208,13 +220,23 @@ export class PriceService {
* *
*/ */
async getLatestSnapshot(): Promise<PriceInfo | null> { async getLatestSnapshot(): Promise<PriceInfo | null> {
const snapshot = await this.priceSnapshotRepository.getLatestSnapshot(); const [snapshot, firstSnapshot] = await Promise.all([
this.priceSnapshotRepository.getLatestSnapshot(),
this.priceSnapshotRepository.getFirstSnapshot(),
]);
if (!snapshot) { if (!snapshot) {
return null; return null;
} }
const burnMultiplier = await this.getCurrentBurnMultiplier(); const burnMultiplier = await this.getCurrentBurnMultiplier();
// 计算较上线首日的涨跌幅
const initialPrice = firstSnapshot?.price || snapshot.price;
let priceChangePercent = new Decimal(0);
if (!initialPrice.isZero()) {
priceChangePercent = snapshot.price.value.minus(initialPrice.value).dividedBy(initialPrice.value).times(100);
}
return { return {
price: snapshot.price.toFixed(18), price: snapshot.price.toFixed(18),
greenPoints: snapshot.greenPoints.toFixed(8), greenPoints: snapshot.greenPoints.toFixed(8),
@ -224,6 +246,8 @@ export class PriceService {
burnMultiplier: burnMultiplier.toFixed(18), burnMultiplier: burnMultiplier.toFixed(18),
minuteBurnRate: snapshot.minuteBurnRate.toFixed(18), minuteBurnRate: snapshot.minuteBurnRate.toFixed(18),
snapshotTime: snapshot.snapshotTime, snapshotTime: snapshot.snapshotTime,
priceChangePercent: priceChangePercent.toFixed(2),
initialPrice: initialPrice.toFixed(18),
}; };
} }

View File

@ -279,9 +279,17 @@ export class OrderRepository {
}; };
}), }),
...sellerTrades.map((t) => { ...sellerTrades.map((t) => {
// 卖方:计算销毁倍数 = 有效积分股 / 原始数量 // 卖方:计算销毁倍数和原始数量
const effectiveQty = Number(t.quantity); const effectiveQty = Number(t.quantity); // 有效积分股(含销毁倍数)
const originalQty = Number(t.originalQuantity || t.quantity); const burnQty = Number(t.burnQuantity);
// 原始数量优先使用数据库值如果为0则通过 有效量 - 销毁量 计算
// 注意:使用 != null 而不是 || 来正确处理0值
let originalQty = t.originalQuantity != null && Number(t.originalQuantity) > 0
? Number(t.originalQuantity)
: effectiveQty - burnQty;
// 确保原始数量不为负数
if (originalQty <= 0) originalQty = effectiveQty;
// 销毁倍数 = 有效积分股 / 原始数量
const burnMultiplier = originalQty > 0 ? effectiveQty / originalQty : 1; const burnMultiplier = originalQty > 0 ? effectiveQty / originalQty : 1;
// 交易总额 = 有效积分股 × 价格 // 交易总额 = 有效积分股 × 价格
const grossAmount = effectiveQty * Number(t.price); const grossAmount = effectiveQty * Number(t.price);
@ -292,7 +300,7 @@ export class OrderRepository {
type: 'SELL' as const, type: 'SELL' as const,
price: t.price.toString(), price: t.price.toString(),
quantity: t.quantity.toString(), // 有效积分股 quantity: t.quantity.toString(), // 有效积分股
originalQuantity: (t.originalQuantity || t.quantity).toString(), // 原始卖出数量 originalQuantity: originalQty.toString(), // 原始卖出数量
burnQuantity: t.burnQuantity.toString(), // 销毁数量 burnQuantity: t.burnQuantity.toString(), // 销毁数量
burnMultiplier: burnMultiplier.toString(), // 销毁倍数 burnMultiplier: burnMultiplier.toString(), // 销毁倍数
grossAmount: grossAmount.toString(), // 交易总额 grossAmount: grossAmount.toString(), // 交易总额

View File

@ -28,6 +28,19 @@ export class PriceSnapshotRepository {
return this.toDomain(record); return this.toDomain(record);
} }
/**
* 线
*/
async getFirstSnapshot(): Promise<PriceSnapshotEntity | null> {
const record = await this.prisma.priceSnapshot.findFirst({
orderBy: { snapshotTime: 'asc' },
});
if (!record) {
return null;
}
return this.toDomain(record);
}
async getSnapshotAt(time: Date): Promise<PriceSnapshotEntity | null> { async getSnapshotAt(time: Date): Promise<PriceSnapshotEntity | null> {
// 获取指定时间之前最近的快照 // 获取指定时间之前最近的快照
const record = await this.prisma.priceSnapshot.findFirst({ const record = await this.prisma.priceSnapshot.findFirst({

View File

@ -83,9 +83,9 @@ final appRouterProvider = Provider<GoRouter>((ref) {
return Routes.login; return Routes.login;
} }
// 访 // 访
if (isLoggedIn && currentPath == Routes.login) { if (isLoggedIn && currentPath == Routes.login) {
return Routes.contribution; return Routes.trading;
} }
return null; return null;

View File

@ -10,6 +10,8 @@ class PriceInfoModel extends PriceInfo {
required super.burnMultiplier, required super.burnMultiplier,
required super.minuteBurnRate, required super.minuteBurnRate,
required super.snapshotTime, required super.snapshotTime,
required super.priceChangePercent,
required super.initialPrice,
}); });
factory PriceInfoModel.fromJson(Map<String, dynamic> json) { factory PriceInfoModel.fromJson(Map<String, dynamic> json) {
@ -24,6 +26,8 @@ class PriceInfoModel extends PriceInfo {
snapshotTime: json['snapshotTime'] != null snapshotTime: json['snapshotTime'] != null
? DateTime.parse(json['snapshotTime'].toString()) ? DateTime.parse(json['snapshotTime'].toString())
: DateTime.now(), : DateTime.now(),
priceChangePercent: json['priceChangePercent']?.toString() ?? '0',
initialPrice: json['initialPrice']?.toString() ?? '0',
); );
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
class MarketOverview extends Equatable { class MarketOverview extends Equatable {
/// ///
final String price; final String price;
/// ///
final String greenPoints; final String greenPoints;
/// ///
final String blackHoleAmount; final String blackHoleAmount;

View File

@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
class PriceInfo extends Equatable { class PriceInfo extends Equatable {
/// ///
final String price; final String price;
/// ///
final String greenPoints; final String greenPoints;
/// ///
final String blackHoleAmount; final String blackHoleAmount;
@ -18,6 +18,10 @@ class PriceInfo extends Equatable {
final String minuteBurnRate; final String minuteBurnRate;
/// ///
final DateTime snapshotTime; final DateTime snapshotTime;
/// 线
final String priceChangePercent;
/// 线
final String initialPrice;
const PriceInfo({ const PriceInfo({
required this.price, required this.price,
@ -28,6 +32,8 @@ class PriceInfo extends Equatable {
required this.burnMultiplier, required this.burnMultiplier,
required this.minuteBurnRate, required this.minuteBurnRate,
required this.snapshotTime, required this.snapshotTime,
required this.priceChangePercent,
required this.initialPrice,
}); });
@override @override
@ -40,5 +46,7 @@ class PriceInfo extends Equatable {
burnMultiplier, burnMultiplier,
minuteBurnRate, minuteBurnRate,
snapshotTime, snapshotTime,
priceChangePercent,
initialPrice,
]; ];
} }

View File

@ -36,7 +36,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
// token // token
// token API 401 // token API 401
if (mounted) { if (mounted) {
context.go(Routes.contribution); context.go(Routes.trading);
} }
} else { } else {
context.go(Routes.login); context.go(Routes.login);

View File

@ -167,7 +167,8 @@ class _TradingPageState extends ConsumerState<TradingPage> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [ children: [
AmountText( AmountText(
amount: priceInfo != null ? formatPrice(price) : null, amount: priceInfo != null ? formatPrice(price) : null,
@ -179,31 +180,58 @@ class _TradingPageState extends ConsumerState<TradingPage> {
letterSpacing: -0.75, letterSpacing: -0.75,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 4),
Container( Text(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), '积分值',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondaryOf(context),
),
),
],
),
const SizedBox(height: 8),
Builder(
builder: (context) {
final changePercent = double.tryParse(priceInfo?.priceChangePercent ?? '0') ?? 0;
final isPositive = changePercent >= 0;
final color = isPositive ? _green : _red;
final icon = isPositive ? Icons.trending_up : Icons.trending_down;
final sign = isPositive ? '+' : '';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _green.withValues(alpha: 0.1), color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.trending_up, size: 16, color: _green), Icon(icon, size: 16, color: color),
const SizedBox(width: 4),
Text(
'较上线首日',
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondaryOf(context),
),
),
const SizedBox(width: 4),
DataText( DataText(
data: isLoading ? null : '+0.00%', data: isLoading ? null : '$sign${changePercent.toStringAsFixed(2)}%',
isLoading: isLoading, isLoading: isLoading,
placeholder: '+--.--%', placeholder: '+--.--%',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _green, color: color,
), ),
), ),
], ],
), ),
), );
], },
), ),
], ],
), ),
@ -313,7 +341,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Row( Row(
children: [ children: [
_buildMarketDataItem( _buildMarketDataItem(
'积分', '积分',
market != null ? formatCompact(market.greenPoints) : null, market != null ? formatCompact(market.greenPoints) : null,
_orange, _orange,
isLoading, isLoading,