feat(kline): add dynamic history loading on pan
Add support for loading more K-line history data when user pans to the left edge. Backend API now accepts 'before' parameter for pagination. Frontend uses KlinesNotifier to manage accumulated data with proper deduplication. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
99c1ff1fb7
commit
13f1b687ee
|
|
@ -61,11 +61,19 @@ export class PriceController {
|
||||||
example: '1h',
|
example: '1h',
|
||||||
})
|
})
|
||||||
@ApiQuery({ name: 'limit', required: false, type: Number, description: '返回数量,默认100' })
|
@ApiQuery({ name: 'limit', required: false, type: Number, description: '返回数量,默认100' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'before',
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
description: '获取此时间之前的K线数据(ISO datetime),用于加载更多历史数据',
|
||||||
|
})
|
||||||
async getKlines(
|
async getKlines(
|
||||||
@Query('period') period: string = '1h',
|
@Query('period') period: string = '1h',
|
||||||
@Query('limit') limit: number = 100,
|
@Query('limit') limit: number = 100,
|
||||||
|
@Query('before') before?: string,
|
||||||
) {
|
) {
|
||||||
return this.priceService.getKlines(period, Math.min(limit, 500));
|
const beforeTime = before ? new Date(before) : undefined;
|
||||||
|
return this.priceService.getKlines(period, Math.min(limit, 500), beforeTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('depth')
|
@Get('depth')
|
||||||
|
|
|
||||||
|
|
@ -233,10 +233,12 @@ export class PriceService {
|
||||||
*
|
*
|
||||||
* @param period K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d
|
* @param period K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d
|
||||||
* @param limit 返回数量
|
* @param limit 返回数量
|
||||||
|
* @param before 获取此时间之前的K线数据(用于加载更多历史)
|
||||||
*/
|
*/
|
||||||
async getKlines(
|
async getKlines(
|
||||||
period: string = '1h',
|
period: string = '1h',
|
||||||
limit: number = 100,
|
limit: number = 100,
|
||||||
|
before?: Date,
|
||||||
): Promise<
|
): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
time: string;
|
time: string;
|
||||||
|
|
@ -251,7 +253,8 @@ export class PriceService {
|
||||||
const periodMinutes = this.parsePeriodToMinutes(period);
|
const periodMinutes = this.parsePeriodToMinutes(period);
|
||||||
|
|
||||||
// 计算时间范围
|
// 计算时间范围
|
||||||
const endTime = new Date();
|
// 如果指定了before,则获取before之前的数据;否则获取到当前时间的数据
|
||||||
|
const endTime = before ? before : new Date();
|
||||||
const startTime = new Date(endTime.getTime() - periodMinutes * limit * 60 * 1000);
|
const startTime = new Date(endTime.getTime() - periodMinutes * limit * 60 * 1000);
|
||||||
|
|
||||||
// 获取原始快照数据
|
// 获取原始快照数据
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,8 @@ abstract class TradingRemoteDataSource {
|
||||||
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
Future<AssetDisplayModel> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
||||||
|
|
||||||
/// 获取K线数据
|
/// 获取K线数据
|
||||||
Future<List<KlineModel>> getKlines({String period = '1h', int limit = 100});
|
/// [before] 获取此时间之前的K线数据(用于加载更多历史)
|
||||||
|
Future<List<KlineModel>> getKlines({String period = '1h', int limit = 100, DateTime? before});
|
||||||
|
|
||||||
/// P2P转账 - 发送积分股给其他用户
|
/// P2P转账 - 发送积分股给其他用户
|
||||||
Future<P2pTransferModel> p2pTransfer({
|
Future<P2pTransferModel> p2pTransfer({
|
||||||
|
|
@ -307,11 +308,22 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<KlineModel>> getKlines({String period = '1h', int limit = 100}) async {
|
Future<List<KlineModel>> getKlines({
|
||||||
|
String period = '1h',
|
||||||
|
int limit = 100,
|
||||||
|
DateTime? before,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
final queryParams = <String, dynamic>{
|
||||||
|
'period': period,
|
||||||
|
'limit': limit,
|
||||||
|
};
|
||||||
|
if (before != null) {
|
||||||
|
queryParams['before'] = before.toUtc().toIso8601String();
|
||||||
|
}
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
ApiEndpoints.priceKlines,
|
ApiEndpoints.priceKlines,
|
||||||
queryParameters: {'period': period, 'limit': limit},
|
queryParameters: queryParams,
|
||||||
);
|
);
|
||||||
final List<dynamic> data = response.data;
|
final List<dynamic> data = response.data;
|
||||||
return data.map((json) => KlineModel.fromJson(json)).toList();
|
return data.map((json) => KlineModel.fromJson(json)).toList();
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,9 @@ class TradingRepositoryImpl implements TradingRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Either<Failure, List<Kline>>> getKlines({String period = '1h', int limit = 100}) async {
|
Future<Either<Failure, List<Kline>>> getKlines({String period = '1h', int limit = 100, DateTime? before}) async {
|
||||||
try {
|
try {
|
||||||
final result = await remoteDataSource.getKlines(period: period, limit: limit);
|
final result = await remoteDataSource.getKlines(period: period, limit: limit, before: before);
|
||||||
return Right(result);
|
return Right(result);
|
||||||
} on ServerException catch (e) {
|
} on ServerException catch (e) {
|
||||||
return Left(ServerFailure(e.message));
|
return Left(ServerFailure(e.message));
|
||||||
|
|
|
||||||
|
|
@ -55,5 +55,6 @@ abstract class TradingRepository {
|
||||||
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
Future<Either<Failure, AssetDisplay>> getAccountAsset(String accountSequence, {String? dailyAllocation});
|
||||||
|
|
||||||
/// 获取K线数据
|
/// 获取K线数据
|
||||||
Future<Either<Failure, List<Kline>>> getKlines({String period = '1h', int limit = 100});
|
/// [before] 获取此时间之前的K线数据(用于加载更多历史)
|
||||||
|
Future<Either<Failure, List<Kline>>> getKlines({String period = '1h', int limit = 100, DateTime? before});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,19 +45,28 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// 初始化时加载K线数据
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[_selectedTimeRange]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final priceAsync = ref.watch(currentPriceProvider);
|
final priceAsync = ref.watch(currentPriceProvider);
|
||||||
final marketAsync = ref.watch(marketOverviewProvider);
|
final marketAsync = ref.watch(marketOverviewProvider);
|
||||||
final ordersAsync = ref.watch(ordersProvider);
|
final ordersAsync = ref.watch(ordersProvider);
|
||||||
final klinesAsync = ref.watch(klinesProvider);
|
final klinesState = ref.watch(klinesNotifierProvider);
|
||||||
final user = ref.watch(userNotifierProvider);
|
final user = ref.watch(userNotifierProvider);
|
||||||
final accountSequence = user.accountSequence ?? '';
|
final accountSequence = user.accountSequence ?? '';
|
||||||
|
|
||||||
// 全屏K线图模式
|
// 全屏K线图模式
|
||||||
if (_isFullScreen) {
|
if (_isFullScreen) {
|
||||||
return KlineChartWidget(
|
return KlineChartWidget(
|
||||||
klines: klinesAsync.valueOrNull ?? [],
|
klines: klinesState.klines,
|
||||||
currentPrice: priceAsync.valueOrNull?.price ?? '0',
|
currentPrice: priceAsync.valueOrNull?.price ?? '0',
|
||||||
isFullScreen: true,
|
isFullScreen: true,
|
||||||
onFullScreenToggle: () => setState(() => _isFullScreen = false),
|
onFullScreenToggle: () => setState(() => _isFullScreen = false),
|
||||||
|
|
@ -66,7 +75,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
onTimeRangeChanged: (index) {
|
onTimeRangeChanged: (index) {
|
||||||
setState(() => _selectedTimeRange = index);
|
setState(() => _selectedTimeRange = index);
|
||||||
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
||||||
|
ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[index]);
|
||||||
},
|
},
|
||||||
|
isLoadingMore: klinesState.isLoadingMore,
|
||||||
|
hasMoreHistory: klinesState.hasMoreHistory,
|
||||||
|
onLoadMoreHistory: () => ref.read(klinesNotifierProvider.notifier).loadMoreHistory(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +92,8 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
ref.invalidate(currentPriceProvider);
|
ref.invalidate(currentPriceProvider);
|
||||||
ref.invalidate(marketOverviewProvider);
|
ref.invalidate(marketOverviewProvider);
|
||||||
ref.invalidate(ordersProvider);
|
ref.invalidate(ordersProvider);
|
||||||
ref.invalidate(klinesProvider);
|
// 重新加载K线数据
|
||||||
|
await ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[_selectedTimeRange]);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -89,7 +103,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildPriceCard(priceAsync),
|
_buildPriceCard(priceAsync),
|
||||||
_buildChartSection(priceAsync, klinesAsync),
|
_buildChartSection(priceAsync, klinesState),
|
||||||
_buildMarketDataCard(marketAsync),
|
_buildMarketDataCard(marketAsync),
|
||||||
_buildTradingPanel(priceAsync),
|
_buildTradingPanel(priceAsync),
|
||||||
_buildMyOrdersCard(ordersAsync),
|
_buildMyOrdersCard(ordersAsync),
|
||||||
|
|
@ -197,10 +211,9 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChartSection(AsyncValue<PriceInfo?> priceAsync, AsyncValue<List<Kline>> klinesAsync) {
|
Widget _buildChartSection(AsyncValue<PriceInfo?> priceAsync, KlinesState klinesState) {
|
||||||
final priceInfo = priceAsync.valueOrNull;
|
final priceInfo = priceAsync.valueOrNull;
|
||||||
final currentPrice = priceInfo?.price ?? '0.000000';
|
final currentPrice = priceInfo?.price ?? '0.000000';
|
||||||
final klines = klinesAsync.valueOrNull ?? [];
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
|
@ -208,13 +221,13 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
color: AppColors.cardOf(context),
|
color: AppColors.cardOf(context),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
child: klinesAsync.isLoading
|
child: klinesState.isLoading && klinesState.klines.isEmpty
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 280,
|
height: 280,
|
||||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
)
|
)
|
||||||
: KlineChartWidget(
|
: KlineChartWidget(
|
||||||
klines: klines,
|
klines: klinesState.klines,
|
||||||
currentPrice: currentPrice,
|
currentPrice: currentPrice,
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
onFullScreenToggle: () => setState(() => _isFullScreen = true),
|
onFullScreenToggle: () => setState(() => _isFullScreen = true),
|
||||||
|
|
@ -223,7 +236,11 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
onTimeRangeChanged: (index) {
|
onTimeRangeChanged: (index) {
|
||||||
setState(() => _selectedTimeRange = index);
|
setState(() => _selectedTimeRange = index);
|
||||||
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
ref.read(selectedKlinePeriodProvider.notifier).state = _timeRanges[index];
|
||||||
|
ref.read(klinesNotifierProvider.notifier).loadKlines(_timeRanges[index]);
|
||||||
},
|
},
|
||||||
|
isLoadingMore: klinesState.isLoadingMore,
|
||||||
|
hasMoreHistory: klinesState.hasMoreHistory,
|
||||||
|
onLoadMoreHistory: () => ref.read(klinesNotifierProvider.notifier).loadMoreHistory(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,189 @@ String _periodToApiParam(String period) {
|
||||||
return periodMap[period] ?? '1h';
|
return periodMap[period] ?? '1h';
|
||||||
}
|
}
|
||||||
|
|
||||||
// K线数据 Provider (根据选中周期获取)
|
/// K线数据状态
|
||||||
|
class KlinesState {
|
||||||
|
final List<Kline> klines;
|
||||||
|
final bool isLoading;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
final bool hasMoreHistory;
|
||||||
|
final String? error;
|
||||||
|
final String period;
|
||||||
|
|
||||||
|
const KlinesState({
|
||||||
|
this.klines = const [],
|
||||||
|
this.isLoading = false,
|
||||||
|
this.isLoadingMore = false,
|
||||||
|
this.hasMoreHistory = true,
|
||||||
|
this.error,
|
||||||
|
this.period = '1h',
|
||||||
|
});
|
||||||
|
|
||||||
|
KlinesState copyWith({
|
||||||
|
List<Kline>? klines,
|
||||||
|
bool? isLoading,
|
||||||
|
bool? isLoadingMore,
|
||||||
|
bool? hasMoreHistory,
|
||||||
|
String? error,
|
||||||
|
String? period,
|
||||||
|
}) {
|
||||||
|
return KlinesState(
|
||||||
|
klines: klines ?? this.klines,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||||
|
hasMoreHistory: hasMoreHistory ?? this.hasMoreHistory,
|
||||||
|
error: error,
|
||||||
|
period: period ?? this.period,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// K线数据管理器 - 支持累积数据和加载更多历史
|
||||||
|
class KlinesNotifier extends StateNotifier<KlinesState> {
|
||||||
|
final TradingRepository repository;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
|
||||||
|
KlinesNotifier({required this.repository}) : super(const KlinesState());
|
||||||
|
|
||||||
|
/// 初始加载K线数据
|
||||||
|
Future<void> loadKlines(String period) async {
|
||||||
|
final apiPeriod = _periodToApiParam(period);
|
||||||
|
|
||||||
|
// 如果周期变化,重置数据
|
||||||
|
if (state.period != apiPeriod) {
|
||||||
|
state = KlinesState(isLoading: true, period: apiPeriod);
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(isLoading: true, error: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await repository.getKlines(period: apiPeriod, limit: 100);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
state = state.copyWith(isLoading: false, error: failure.message);
|
||||||
|
},
|
||||||
|
(klines) {
|
||||||
|
state = state.copyWith(
|
||||||
|
klines: klines,
|
||||||
|
isLoading: false,
|
||||||
|
hasMoreHistory: klines.length >= 100, // 如果返回100条,可能还有更多
|
||||||
|
);
|
||||||
|
_startAutoRefresh(apiPeriod);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载更多历史数据(向左滑动到边界时触发)
|
||||||
|
Future<void> loadMoreHistory() async {
|
||||||
|
if (state.isLoadingMore || !state.hasMoreHistory || state.klines.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(isLoadingMore: true);
|
||||||
|
|
||||||
|
// 获取当前最早K线的时间作为before参数
|
||||||
|
final oldestKline = state.klines.first;
|
||||||
|
final beforeTime = oldestKline.time;
|
||||||
|
|
||||||
|
final result = await repository.getKlines(
|
||||||
|
period: state.period,
|
||||||
|
limit: 100,
|
||||||
|
before: beforeTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
state = state.copyWith(isLoadingMore: false, error: failure.message);
|
||||||
|
},
|
||||||
|
(newKlines) {
|
||||||
|
if (newKlines.isEmpty) {
|
||||||
|
// 没有更多历史数据了
|
||||||
|
state = state.copyWith(isLoadingMore: false, hasMoreHistory: false);
|
||||||
|
} else {
|
||||||
|
// 将新数据插入到现有数据前面(按时间顺序)
|
||||||
|
// 需要去重,因为可能有重叠
|
||||||
|
final existingTimes = state.klines.map((k) => k.time.millisecondsSinceEpoch).toSet();
|
||||||
|
final uniqueNewKlines = newKlines.where(
|
||||||
|
(k) => !existingTimes.contains(k.time.millisecondsSinceEpoch)
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
final combinedKlines = [...uniqueNewKlines, ...state.klines];
|
||||||
|
// 按时间排序
|
||||||
|
combinedKlines.sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
|
||||||
|
state = state.copyWith(
|
||||||
|
klines: combinedKlines,
|
||||||
|
isLoadingMore: false,
|
||||||
|
hasMoreHistory: newKlines.length >= 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 刷新最新数据(不清除历史)
|
||||||
|
Future<void> refreshLatest() async {
|
||||||
|
if (state.klines.isEmpty) {
|
||||||
|
return loadKlines(state.period);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await repository.getKlines(period: state.period, limit: 100);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) {
|
||||||
|
// 刷新失败不影响现有数据
|
||||||
|
},
|
||||||
|
(newKlines) {
|
||||||
|
if (newKlines.isEmpty) return;
|
||||||
|
|
||||||
|
// 合并新数据到现有数据末尾
|
||||||
|
final existingTimes = state.klines.map((k) => k.time.millisecondsSinceEpoch).toSet();
|
||||||
|
final uniqueNewKlines = newKlines.where(
|
||||||
|
(k) => !existingTimes.contains(k.time.millisecondsSinceEpoch)
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
if (uniqueNewKlines.isNotEmpty) {
|
||||||
|
final combinedKlines = [...state.klines, ...uniqueNewKlines];
|
||||||
|
combinedKlines.sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
state = state.copyWith(klines: combinedKlines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最新K线的收盘价(实时更新)
|
||||||
|
if (newKlines.isNotEmpty && state.klines.isNotEmpty) {
|
||||||
|
final latestNew = newKlines.last;
|
||||||
|
final latestExisting = state.klines.last;
|
||||||
|
if (latestNew.time.millisecondsSinceEpoch == latestExisting.time.millisecondsSinceEpoch) {
|
||||||
|
// 更新最新K线
|
||||||
|
final updatedKlines = [...state.klines];
|
||||||
|
updatedKlines[updatedKlines.length - 1] = latestNew;
|
||||||
|
state = state.copyWith(klines: updatedKlines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startAutoRefresh(String period) {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||||
|
refreshLatest();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// K线数据 Provider(支持累积和加载更多)
|
||||||
|
final klinesNotifierProvider = StateNotifierProvider<KlinesNotifier, KlinesState>((ref) {
|
||||||
|
final repository = ref.watch(tradingRepositoryProvider);
|
||||||
|
return KlinesNotifier(repository: repository);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保留原有的简单 Provider 以保持向后兼容
|
||||||
final klinesProvider = FutureProvider<List<Kline>>((ref) async {
|
final klinesProvider = FutureProvider<List<Kline>>((ref) async {
|
||||||
final repository = ref.watch(tradingRepositoryProvider);
|
final repository = ref.watch(tradingRepositoryProvider);
|
||||||
final selectedPeriod = ref.watch(selectedKlinePeriodProvider);
|
final selectedPeriod = ref.watch(selectedKlinePeriodProvider);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ class KlineChartWidget extends StatefulWidget {
|
||||||
final List<String> timeRanges;
|
final List<String> timeRanges;
|
||||||
final int selectedTimeIndex;
|
final int selectedTimeIndex;
|
||||||
final Function(int)? onTimeRangeChanged;
|
final Function(int)? onTimeRangeChanged;
|
||||||
|
/// 是否正在加载更多历史数据
|
||||||
|
final bool isLoadingMore;
|
||||||
|
/// 是否还有更多历史数据可加载
|
||||||
|
final bool hasMoreHistory;
|
||||||
|
/// 加载更多历史数据的回调
|
||||||
|
final VoidCallback? onLoadMoreHistory;
|
||||||
|
|
||||||
const KlineChartWidget({
|
const KlineChartWidget({
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -29,6 +35,9 @@ class KlineChartWidget extends StatefulWidget {
|
||||||
this.timeRanges = const ['1分', '5分', '15分', '30分', '1时', '4时', '日'],
|
this.timeRanges = const ['1分', '5分', '15分', '30分', '1时', '4时', '日'],
|
||||||
this.selectedTimeIndex = 4,
|
this.selectedTimeIndex = 4,
|
||||||
this.onTimeRangeChanged,
|
this.onTimeRangeChanged,
|
||||||
|
this.isLoadingMore = false,
|
||||||
|
this.hasMoreHistory = true,
|
||||||
|
this.onLoadMoreHistory,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -399,6 +408,40 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
scrollOffset: _scrollX,
|
scrollOffset: _scrollX,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// 加载更多历史数据指示器
|
||||||
|
if (widget.isLoadingMore)
|
||||||
|
Positioned(
|
||||||
|
left: 8,
|
||||||
|
top: mainChartHeight / 2 - 12,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.cardOf(context).withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: _orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'加载中...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: AppColors.textSecondaryOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
// 十字线信息
|
// 十字线信息
|
||||||
if (_showCrossLine && _crossLineIndex >= 0 && _crossLineIndex < widget.klines.length)
|
if (_showCrossLine && _crossLineIndex >= 0 && _crossLineIndex < widget.klines.length)
|
||||||
_buildCrossLineInfo(),
|
_buildCrossLineInfo(),
|
||||||
|
|
@ -545,6 +588,12 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
|
||||||
void _onScaleEnd(ScaleEndDetails details) {
|
void _onScaleEnd(ScaleEndDetails details) {
|
||||||
_startFocalPoint = null;
|
_startFocalPoint = null;
|
||||||
_isScaling = false;
|
_isScaling = false;
|
||||||
|
|
||||||
|
// 检查是否滚动到了左边界(历史数据边界)
|
||||||
|
// 如果滚动接近0且还有更多历史数据,触发加载
|
||||||
|
if (_scrollX <= 50 && widget.hasMoreHistory && !widget.isLoadingMore) {
|
||||||
|
widget.onLoadMoreHistory?.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLongPressStart(LongPressStartDetails details) {
|
void _onLongPressStart(LongPressStartDetails details) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue