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:
parent
7ff7157115
commit
995dfa898e
|
|
@ -17,6 +17,8 @@ export interface PriceInfo {
|
|||
burnMultiplier: string;
|
||||
minuteBurnRate: string;
|
||||
snapshotTime: Date;
|
||||
priceChangePercent: string; // 较上线首日涨跌幅
|
||||
initialPrice: string; // 上线首日价格
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -36,11 +38,12 @@ export class PriceService {
|
|||
* 获取当前价格信息
|
||||
*/
|
||||
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.blackHoleRepository.getBlackHole(),
|
||||
this.circulationPoolRepository.getPool(),
|
||||
this.tradingConfigRepository.getConfig(),
|
||||
this.priceSnapshotRepository.getFirstSnapshot(),
|
||||
]);
|
||||
|
||||
const greenPoints = sharePool?.greenPoints || Money.zero();
|
||||
|
|
@ -65,6 +68,13 @@ export class PriceService {
|
|||
// 获取当前每分钟销毁率
|
||||
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 {
|
||||
price: price.toFixed(18),
|
||||
greenPoints: greenPoints.toFixed(8),
|
||||
|
|
@ -74,6 +84,8 @@ export class PriceService {
|
|||
burnMultiplier: burnMultiplier.toFixed(18),
|
||||
minuteBurnRate: minuteBurnRate.toFixed(18),
|
||||
snapshotTime: new Date(),
|
||||
priceChangePercent: priceChangePercent.toFixed(2),
|
||||
initialPrice: initialPrice.toFixed(18),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -208,13 +220,23 @@ export class PriceService {
|
|||
* 获取最新价格快照
|
||||
*/
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 {
|
||||
price: snapshot.price.toFixed(18),
|
||||
greenPoints: snapshot.greenPoints.toFixed(8),
|
||||
|
|
@ -224,6 +246,8 @@ export class PriceService {
|
|||
burnMultiplier: burnMultiplier.toFixed(18),
|
||||
minuteBurnRate: snapshot.minuteBurnRate.toFixed(18),
|
||||
snapshotTime: snapshot.snapshotTime,
|
||||
priceChangePercent: priceChangePercent.toFixed(2),
|
||||
initialPrice: initialPrice.toFixed(18),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -279,9 +279,17 @@ export class OrderRepository {
|
|||
};
|
||||
}),
|
||||
...sellerTrades.map((t) => {
|
||||
// 卖方:计算销毁倍数 = 有效积分股 / 原始数量
|
||||
const effectiveQty = Number(t.quantity);
|
||||
const originalQty = Number(t.originalQuantity || t.quantity);
|
||||
// 卖方:计算销毁倍数和原始数量
|
||||
const effectiveQty = Number(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 grossAmount = effectiveQty * Number(t.price);
|
||||
|
|
@ -292,7 +300,7 @@ export class OrderRepository {
|
|||
type: 'SELL' as const,
|
||||
price: t.price.toString(),
|
||||
quantity: t.quantity.toString(), // 有效积分股
|
||||
originalQuantity: (t.originalQuantity || t.quantity).toString(), // 原始卖出数量
|
||||
originalQuantity: originalQty.toString(), // 原始卖出数量
|
||||
burnQuantity: t.burnQuantity.toString(), // 销毁数量
|
||||
burnMultiplier: burnMultiplier.toString(), // 销毁倍数
|
||||
grossAmount: grossAmount.toString(), // 交易总额
|
||||
|
|
|
|||
|
|
@ -28,6 +28,19 @@ export class PriceSnapshotRepository {
|
|||
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> {
|
||||
// 获取指定时间之前最近的快照
|
||||
const record = await this.prisma.priceSnapshot.findFirst({
|
||||
|
|
|
|||
|
|
@ -83,9 +83,9 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
|||
return Routes.login;
|
||||
}
|
||||
|
||||
// 已登录且访问登录页,重定向到首页
|
||||
// 已登录且访问登录页,重定向到兑换页
|
||||
if (isLoggedIn && currentPath == Routes.login) {
|
||||
return Routes.contribution;
|
||||
return Routes.trading;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ class PriceInfoModel extends PriceInfo {
|
|||
required super.burnMultiplier,
|
||||
required super.minuteBurnRate,
|
||||
required super.snapshotTime,
|
||||
required super.priceChangePercent,
|
||||
required super.initialPrice,
|
||||
});
|
||||
|
||||
factory PriceInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -24,6 +26,8 @@ class PriceInfoModel extends PriceInfo {
|
|||
snapshotTime: json['snapshotTime'] != null
|
||||
? DateTime.parse(json['snapshotTime'].toString())
|
||||
: DateTime.now(),
|
||||
priceChangePercent: json['priceChangePercent']?.toString() ?? '0',
|
||||
initialPrice: json['initialPrice']?.toString() ?? '0',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
|
|||
class MarketOverview extends Equatable {
|
||||
/// 当前价格
|
||||
final String price;
|
||||
/// 积分值池
|
||||
/// 积分股池
|
||||
final String greenPoints;
|
||||
/// 黑洞销毁量
|
||||
final String blackHoleAmount;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
|
|||
class PriceInfo extends Equatable {
|
||||
/// 当前价格
|
||||
final String price;
|
||||
/// 积分值池
|
||||
/// 积分股池
|
||||
final String greenPoints;
|
||||
/// 黑洞销毁量
|
||||
final String blackHoleAmount;
|
||||
|
|
@ -18,6 +18,10 @@ class PriceInfo extends Equatable {
|
|||
final String minuteBurnRate;
|
||||
/// 快照时间
|
||||
final DateTime snapshotTime;
|
||||
/// 较上线首日涨跌幅(百分比)
|
||||
final String priceChangePercent;
|
||||
/// 上线首日价格
|
||||
final String initialPrice;
|
||||
|
||||
const PriceInfo({
|
||||
required this.price,
|
||||
|
|
@ -28,6 +32,8 @@ class PriceInfo extends Equatable {
|
|||
required this.burnMultiplier,
|
||||
required this.minuteBurnRate,
|
||||
required this.snapshotTime,
|
||||
required this.priceChangePercent,
|
||||
required this.initialPrice,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -40,5 +46,7 @@ class PriceInfo extends Equatable {
|
|||
burnMultiplier,
|
||||
minuteBurnRate,
|
||||
snapshotTime,
|
||||
priceChangePercent,
|
||||
initialPrice,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
|||
// 已登录,直接跳转,不需要主动刷新 token
|
||||
// token 刷新只在 API 返回 401 时才触发
|
||||
if (mounted) {
|
||||
context.go(Routes.contribution);
|
||||
context.go(Routes.trading);
|
||||
}
|
||||
} else {
|
||||
context.go(Routes.login);
|
||||
|
|
|
|||
|
|
@ -167,7 +167,8 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
AmountText(
|
||||
amount: priceInfo != null ? formatPrice(price) : null,
|
||||
|
|
@ -179,31 +180,58 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
letterSpacing: -0.75,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'积分值',
|
||||
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(
|
||||
color: _green.withValues(alpha: 0.1),
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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(
|
||||
data: isLoading ? null : '+0.00%',
|
||||
data: isLoading ? null : '$sign${changePercent.toStringAsFixed(2)}%',
|
||||
isLoading: isLoading,
|
||||
placeholder: '+--.--%',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _green,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -313,7 +341,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
Row(
|
||||
children: [
|
||||
_buildMarketDataItem(
|
||||
'积分值池',
|
||||
'积分股池',
|
||||
market != null ? formatCompact(market.greenPoints) : null,
|
||||
_orange,
|
||||
isLoading,
|
||||
|
|
|
|||
Loading…
Reference in New Issue