feat(mining-app): fix login bugs and connect contribution page to real API
Login fixes:
- Add AuthEventBus for global 401 error handling with auto-logout
- Add route guards with GoRouter redirect to protect authenticated routes
- Remove setMockUser() security vulnerability and legacy login() dead code
- Remove unused AuthInterceptor class
Contribution page:
- Add ContributionRecord entity and model for records API
- Connect contribution details card to GET /accounts/{id}/records endpoint
- Display real team stats (direct referrals, unlocked levels/tiers)
- Calculate expiration countdown from actual record data
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a89f4c829d
commit
add405aa65
|
|
@ -755,7 +755,8 @@
|
||||||
"Bash(ssh -o StrictHostKeyChecking=no ceshi@103.39.231.231 \"ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ceshi@192.168.1.111 ''cd /home/ceshi/rwadurian/backend/services && git pull''\")",
|
"Bash(ssh -o StrictHostKeyChecking=no ceshi@103.39.231.231 \"ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ceshi@192.168.1.111 ''cd /home/ceshi/rwadurian/backend/services && git pull''\")",
|
||||||
"Bash(set DATABASE_URL=postgresql://user:pass@localhost:5432/db)",
|
"Bash(set DATABASE_URL=postgresql://user:pass@localhost:5432/db)",
|
||||||
"Bash(cmd /c \"set DATABASE_URL=postgresql://user:pass@localhost:5432/db && npx prisma migrate dev --name add_nickname_to_synced_legacy_users --create-only\")",
|
"Bash(cmd /c \"set DATABASE_URL=postgresql://user:pass@localhost:5432/db && npx prisma migrate dev --name add_nickname_to_synced_legacy_users --create-only\")",
|
||||||
"Bash(dir \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\frontend\")"
|
"Bash(dir \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\frontend\")",
|
||||||
|
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mining-app\\): fix login bugs and connect contribution page to real API\n\nLogin fixes:\n- Add AuthEventBus for global 401 error handling with auto-logout\n- Add route guards with GoRouter redirect to protect authenticated routes\n- Remove setMockUser\\(\\) security vulnerability and legacy login\\(\\) dead code\n- Remove unused AuthInterceptor class\n\nContribution page:\n- Add ContributionRecord entity and model for records API\n- Connect contribution details card to GET /accounts/{id}/records endpoint\n- Display real team stats \\(direct referrals, unlocked levels/tiers\\)\n- Calculate expiration countdown from actual record data\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ final getIt = GetIt.instance;
|
||||||
Future<void> configureDependencies() async {
|
Future<void> configureDependencies() async {
|
||||||
// Dio
|
// Dio
|
||||||
final dio = Dio();
|
final dio = Dio();
|
||||||
dio.interceptors.add(AuthInterceptor());
|
|
||||||
dio.interceptors.add(LoggingInterceptor());
|
dio.interceptors.add(LoggingInterceptor());
|
||||||
getIt.registerSingleton<Dio>(dio);
|
getIt.registerSingleton<Dio>(dio);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,30 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../constants/app_constants.dart';
|
import '../constants/app_constants.dart';
|
||||||
import '../error/exceptions.dart';
|
import '../error/exceptions.dart';
|
||||||
|
|
||||||
|
/// 全局未授权事件控制器,用于通知 401 错误
|
||||||
|
class AuthEventBus {
|
||||||
|
static final AuthEventBus _instance = AuthEventBus._internal();
|
||||||
|
factory AuthEventBus() => _instance;
|
||||||
|
AuthEventBus._internal();
|
||||||
|
|
||||||
|
final _unauthorizedController = StreamController<void>.broadcast();
|
||||||
|
|
||||||
|
/// 监听未授权事件
|
||||||
|
Stream<void> get onUnauthorized => _unauthorizedController.stream;
|
||||||
|
|
||||||
|
/// 触发未授权事件
|
||||||
|
void emitUnauthorized() {
|
||||||
|
_unauthorizedController.add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_unauthorizedController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
|
|
||||||
|
|
@ -107,6 +129,8 @@ class ApiClient {
|
||||||
case DioExceptionType.badResponse:
|
case DioExceptionType.badResponse:
|
||||||
final statusCode = e.response?.statusCode;
|
final statusCode = e.response?.statusCode;
|
||||||
if (statusCode == 401) {
|
if (statusCode == 401) {
|
||||||
|
// 触发全局未授权事件,通知应用进行登出处理
|
||||||
|
AuthEventBus().emitUnauthorized();
|
||||||
return UnauthorizedException();
|
return UnauthorizedException();
|
||||||
}
|
}
|
||||||
final message = e.response?.data?['error']?['message']?[0] ?? '服务器错误';
|
final message = e.response?.data?['error']?['message']?[0] ?? '服务器错误';
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,6 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
class AuthInterceptor extends Interceptor {
|
|
||||||
String? _token;
|
|
||||||
|
|
||||||
void setToken(String token) {
|
|
||||||
_token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearToken() {
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
||||||
if (_token != null) {
|
|
||||||
options.headers['Authorization'] = 'Bearer $_token';
|
|
||||||
}
|
|
||||||
handler.next(options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoggingInterceptor extends Interceptor {
|
class LoggingInterceptor extends Interceptor {
|
||||||
final _logger = Logger(
|
final _logger = Logger(
|
||||||
printer: PrettyPrinter(methodCount: 0),
|
printer: PrettyPrinter(methodCount: 0),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../presentation/pages/splash/splash_page.dart';
|
import '../../presentation/pages/splash/splash_page.dart';
|
||||||
|
|
@ -11,11 +13,85 @@ import '../../presentation/pages/trading/trading_page.dart';
|
||||||
import '../../presentation/pages/asset/asset_page.dart';
|
import '../../presentation/pages/asset/asset_page.dart';
|
||||||
import '../../presentation/pages/profile/profile_page.dart';
|
import '../../presentation/pages/profile/profile_page.dart';
|
||||||
import '../../presentation/widgets/main_shell.dart';
|
import '../../presentation/widgets/main_shell.dart';
|
||||||
|
import '../../presentation/providers/user_providers.dart';
|
||||||
|
import '../network/api_client.dart';
|
||||||
import 'routes.dart';
|
import 'routes.dart';
|
||||||
|
|
||||||
|
/// 不需要登录就能访问的公开路由
|
||||||
|
const _publicRoutes = {
|
||||||
|
Routes.splash,
|
||||||
|
Routes.login,
|
||||||
|
Routes.register,
|
||||||
|
Routes.forgotPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 路由刷新通知器,用于在登录状态变化时刷新路由
|
||||||
|
class AuthNotifier extends ChangeNotifier {
|
||||||
|
bool _isLoggedIn = false;
|
||||||
|
StreamSubscription<void>? _unauthorizedSubscription;
|
||||||
|
|
||||||
|
AuthNotifier() {
|
||||||
|
// 监听 401 未授权事件
|
||||||
|
_unauthorizedSubscription = AuthEventBus().onUnauthorized.listen((_) {
|
||||||
|
_isLoggedIn = false;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isLoggedIn => _isLoggedIn;
|
||||||
|
|
||||||
|
void updateLoginState(bool isLoggedIn) {
|
||||||
|
if (_isLoggedIn != isLoggedIn) {
|
||||||
|
_isLoggedIn = isLoggedIn;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unauthorizedSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final authNotifierProvider = ChangeNotifierProvider<AuthNotifier>((ref) {
|
||||||
|
final authNotifier = AuthNotifier();
|
||||||
|
|
||||||
|
// 监听用户登录状态变化
|
||||||
|
ref.listen<bool>(isLoggedInProvider, (previous, next) {
|
||||||
|
authNotifier.updateLoginState(next);
|
||||||
|
});
|
||||||
|
|
||||||
|
return authNotifier;
|
||||||
|
});
|
||||||
|
|
||||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
|
final authNotifier = ref.watch(authNotifierProvider);
|
||||||
|
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: Routes.splash,
|
initialLocation: Routes.splash,
|
||||||
|
refreshListenable: authNotifier,
|
||||||
|
redirect: (context, state) {
|
||||||
|
final isLoggedIn = ref.read(isLoggedInProvider);
|
||||||
|
final currentPath = state.uri.path;
|
||||||
|
|
||||||
|
// splash 页面不做重定向,让它自己处理初始化逻辑
|
||||||
|
if (currentPath == Routes.splash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未登录且访问受保护路由,重定向到登录页
|
||||||
|
if (!isLoggedIn && !_publicRoutes.contains(currentPath)) {
|
||||||
|
return Routes.login;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已登录且访问登录页,重定向到首页
|
||||||
|
if (isLoggedIn && currentPath == Routes.login) {
|
||||||
|
return Routes.contribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.splash,
|
path: Routes.splash,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
import '../../models/contribution_model.dart';
|
import '../../models/contribution_model.dart';
|
||||||
|
import '../../models/contribution_record_model.dart';
|
||||||
import '../../../core/network/api_client.dart';
|
import '../../../core/network/api_client.dart';
|
||||||
import '../../../core/network/api_endpoints.dart';
|
import '../../../core/network/api_endpoints.dart';
|
||||||
import '../../../core/error/exceptions.dart';
|
import '../../../core/error/exceptions.dart';
|
||||||
|
import '../../../domain/entities/contribution_record.dart';
|
||||||
|
|
||||||
abstract class ContributionRemoteDataSource {
|
abstract class ContributionRemoteDataSource {
|
||||||
Future<ContributionModel> getUserContribution(String accountSequence);
|
Future<ContributionModel> getUserContribution(String accountSequence);
|
||||||
|
Future<ContributionRecordsPageModel> getContributionRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
ContributionSourceType? sourceType,
|
||||||
|
bool includeExpired = false,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
||||||
|
|
@ -21,4 +30,44 @@ class ContributionRemoteDataSourceImpl implements ContributionRemoteDataSource {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ContributionRecordsPageModel> getContributionRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
ContributionSourceType? sourceType,
|
||||||
|
bool includeExpired = false,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final queryParams = <String, dynamic>{
|
||||||
|
'page': page,
|
||||||
|
'pageSize': pageSize,
|
||||||
|
'includeExpired': includeExpired,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sourceType != null) {
|
||||||
|
queryParams['sourceType'] = _sourceTypeToString(sourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await client.get(
|
||||||
|
ApiEndpoints.contributionRecords(accountSequence),
|
||||||
|
queryParameters: queryParams,
|
||||||
|
);
|
||||||
|
return ContributionRecordsPageModel.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sourceTypeToString(ContributionSourceType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ContributionSourceType.personal:
|
||||||
|
return 'PERSONAL';
|
||||||
|
case ContributionSourceType.teamLevel:
|
||||||
|
return 'TEAM_LEVEL';
|
||||||
|
case ContributionSourceType.teamBonus:
|
||||||
|
return 'TEAM_BONUS';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import '../../domain/entities/contribution_record.dart';
|
||||||
|
|
||||||
|
class ContributionRecordModel extends ContributionRecord {
|
||||||
|
const ContributionRecordModel({
|
||||||
|
required super.id,
|
||||||
|
required super.sourceType,
|
||||||
|
required super.sourceAdoptionId,
|
||||||
|
super.sourceAccountSequence,
|
||||||
|
required super.treeCount,
|
||||||
|
required super.baseContribution,
|
||||||
|
required super.distributionRate,
|
||||||
|
super.levelDepth,
|
||||||
|
super.bonusTier,
|
||||||
|
required super.finalContribution,
|
||||||
|
required super.effectiveDate,
|
||||||
|
required super.expireDate,
|
||||||
|
required super.isExpired,
|
||||||
|
required super.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ContributionRecordModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ContributionRecordModel(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
sourceType: _parseSourceType(json['sourceType']),
|
||||||
|
sourceAdoptionId: json['sourceAdoptionId']?.toString() ?? '',
|
||||||
|
sourceAccountSequence: json['sourceAccountSequence']?.toString(),
|
||||||
|
treeCount: json['treeCount'] ?? 0,
|
||||||
|
baseContribution: json['baseContribution']?.toString() ?? '0',
|
||||||
|
distributionRate: json['distributionRate']?.toString() ?? '0',
|
||||||
|
levelDepth: json['levelDepth'],
|
||||||
|
bonusTier: json['bonusTier'],
|
||||||
|
finalContribution: json['finalContribution']?.toString() ?? '0',
|
||||||
|
effectiveDate: DateTime.parse(json['effectiveDate']),
|
||||||
|
expireDate: DateTime.parse(json['expireDate']),
|
||||||
|
isExpired: json['isExpired'] == true,
|
||||||
|
createdAt: DateTime.parse(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ContributionSourceType _parseSourceType(String? type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'PERSONAL':
|
||||||
|
return ContributionSourceType.personal;
|
||||||
|
case 'TEAM_LEVEL':
|
||||||
|
return ContributionSourceType.teamLevel;
|
||||||
|
case 'TEAM_BONUS':
|
||||||
|
return ContributionSourceType.teamBonus;
|
||||||
|
default:
|
||||||
|
return ContributionSourceType.personal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContributionRecordsPageModel extends ContributionRecordsPage {
|
||||||
|
const ContributionRecordsPageModel({
|
||||||
|
required super.data,
|
||||||
|
required super.total,
|
||||||
|
required super.page,
|
||||||
|
required super.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ContributionRecordsPageModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final dataList = (json['data'] as List<dynamic>?) ?? [];
|
||||||
|
return ContributionRecordsPageModel(
|
||||||
|
data: dataList.map((e) => ContributionRecordModel.fromJson(e)).toList(),
|
||||||
|
total: json['total'] ?? 0,
|
||||||
|
page: json['page'] ?? 1,
|
||||||
|
pageSize: json['pageSize'] ?? 50,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import '../../domain/entities/contribution.dart';
|
import '../../domain/entities/contribution.dart';
|
||||||
|
import '../../domain/entities/contribution_record.dart';
|
||||||
import '../../domain/repositories/contribution_repository.dart';
|
import '../../domain/repositories/contribution_repository.dart';
|
||||||
import '../../core/error/exceptions.dart';
|
import '../../core/error/exceptions.dart';
|
||||||
import '../../core/error/failures.dart';
|
import '../../core/error/failures.dart';
|
||||||
|
|
@ -21,4 +22,28 @@ class ContributionRepositoryImpl implements ContributionRepository {
|
||||||
return Left(const NetworkFailure());
|
return Left(const NetworkFailure());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, ContributionRecordsPage>> getContributionRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
ContributionSourceType? sourceType,
|
||||||
|
bool includeExpired = false,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await remoteDataSource.getContributionRecords(
|
||||||
|
accountSequence,
|
||||||
|
sourceType: sourceType,
|
||||||
|
includeExpired: includeExpired,
|
||||||
|
page: page,
|
||||||
|
pageSize: pageSize,
|
||||||
|
);
|
||||||
|
return Right(result);
|
||||||
|
} on ServerException catch (e) {
|
||||||
|
return Left(ServerFailure(e.message));
|
||||||
|
} on NetworkException {
|
||||||
|
return Left(const NetworkFailure());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// 贡献值来源类型
|
||||||
|
enum ContributionSourceType {
|
||||||
|
personal, // 个人 - 认种榴莲树
|
||||||
|
teamLevel, // 团队层级 - 直推/间推奖励
|
||||||
|
teamBonus, // 团队奖励 - 额外奖励
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 贡献值记录
|
||||||
|
class ContributionRecord extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final ContributionSourceType sourceType;
|
||||||
|
final String sourceAdoptionId;
|
||||||
|
final String? sourceAccountSequence;
|
||||||
|
final int treeCount;
|
||||||
|
final String baseContribution;
|
||||||
|
final String distributionRate;
|
||||||
|
final int? levelDepth;
|
||||||
|
final int? bonusTier;
|
||||||
|
final String finalContribution;
|
||||||
|
final DateTime effectiveDate;
|
||||||
|
final DateTime expireDate;
|
||||||
|
final bool isExpired;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
const ContributionRecord({
|
||||||
|
required this.id,
|
||||||
|
required this.sourceType,
|
||||||
|
required this.sourceAdoptionId,
|
||||||
|
this.sourceAccountSequence,
|
||||||
|
required this.treeCount,
|
||||||
|
required this.baseContribution,
|
||||||
|
required this.distributionRate,
|
||||||
|
this.levelDepth,
|
||||||
|
this.bonusTier,
|
||||||
|
required this.finalContribution,
|
||||||
|
required this.effectiveDate,
|
||||||
|
required this.expireDate,
|
||||||
|
required this.isExpired,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 获取记录的显示标题
|
||||||
|
String get displayTitle {
|
||||||
|
switch (sourceType) {
|
||||||
|
case ContributionSourceType.personal:
|
||||||
|
return '认种榴莲树';
|
||||||
|
case ContributionSourceType.teamLevel:
|
||||||
|
if (levelDepth == 1) {
|
||||||
|
return '直推奖励';
|
||||||
|
}
|
||||||
|
return '团队奖励($levelDepth级)';
|
||||||
|
case ContributionSourceType.teamBonus:
|
||||||
|
return '团队额外奖励';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
sourceType,
|
||||||
|
sourceAdoptionId,
|
||||||
|
sourceAccountSequence,
|
||||||
|
treeCount,
|
||||||
|
baseContribution,
|
||||||
|
distributionRate,
|
||||||
|
levelDepth,
|
||||||
|
bonusTier,
|
||||||
|
finalContribution,
|
||||||
|
effectiveDate,
|
||||||
|
expireDate,
|
||||||
|
isExpired,
|
||||||
|
createdAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 贡献值记录分页响应
|
||||||
|
class ContributionRecordsPage extends Equatable {
|
||||||
|
final List<ContributionRecord> data;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const ContributionRecordsPage({
|
||||||
|
required this.data,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get hasMore => page * pageSize < total;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [data, total, page, pageSize];
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
import '../../core/error/failures.dart';
|
import '../../core/error/failures.dart';
|
||||||
import '../entities/contribution.dart';
|
import '../entities/contribution.dart';
|
||||||
|
import '../entities/contribution_record.dart';
|
||||||
|
|
||||||
abstract class ContributionRepository {
|
abstract class ContributionRepository {
|
||||||
Future<Either<Failure, Contribution>> getUserContribution(String accountSequence);
|
Future<Either<Failure, Contribution>> getUserContribution(String accountSequence);
|
||||||
|
Future<Either<Failure, ContributionRecordsPage>> getContributionRecords(
|
||||||
|
String accountSequence, {
|
||||||
|
ContributionSourceType? sourceType,
|
||||||
|
bool includeExpired = false,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import '../../../core/constants/app_colors.dart';
|
import '../../../core/constants/app_colors.dart';
|
||||||
import '../../../core/utils/format_utils.dart';
|
import '../../../core/utils/format_utils.dart';
|
||||||
|
import '../../../domain/entities/contribution.dart';
|
||||||
|
import '../../../domain/entities/contribution_record.dart';
|
||||||
import '../../providers/user_providers.dart';
|
import '../../providers/user_providers.dart';
|
||||||
import '../../providers/contribution_providers.dart';
|
import '../../providers/contribution_providers.dart';
|
||||||
|
|
||||||
|
|
@ -21,6 +24,12 @@ class ContributionPage extends ConsumerWidget {
|
||||||
final user = ref.watch(userNotifierProvider);
|
final user = ref.watch(userNotifierProvider);
|
||||||
final accountSequence = user.accountSequence ?? '';
|
final accountSequence = user.accountSequence ?? '';
|
||||||
final contributionAsync = ref.watch(contributionProvider(accountSequence));
|
final contributionAsync = ref.watch(contributionProvider(accountSequence));
|
||||||
|
final recordsParams = ContributionRecordsParams(
|
||||||
|
accountSequence: accountSequence,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 3,
|
||||||
|
);
|
||||||
|
final recordsAsync = ref.watch(contributionRecordsProvider(recordsParams));
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
|
|
@ -28,6 +37,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
ref.invalidate(contributionProvider(accountSequence));
|
ref.invalidate(contributionProvider(accountSequence));
|
||||||
|
ref.invalidate(contributionRecordsProvider(recordsParams));
|
||||||
},
|
},
|
||||||
child: contributionAsync.when(
|
child: contributionAsync.when(
|
||||||
data: (contribution) {
|
data: (contribution) {
|
||||||
|
|
@ -50,13 +60,13 @@ class ContributionPage extends ConsumerWidget {
|
||||||
_buildTodayEstimateCard(contribution),
|
_buildTodayEstimateCard(contribution),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 贡献值明细
|
// 贡献值明细
|
||||||
_buildContributionDetailCard(contribution),
|
_buildContributionDetailCard(context, ref, recordsAsync),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 团队层级统计
|
// 团队层级统计
|
||||||
_buildTeamStatsCard(contribution),
|
_buildTeamStatsCard(contribution),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 贡献值失效倒计时
|
// 贡献值失效倒计时
|
||||||
_buildExpirationCard(contribution),
|
_buildExpirationCard(contribution, recordsAsync),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|
@ -144,7 +154,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTotalContributionCard(contribution) {
|
Widget _buildTotalContributionCard(Contribution? contribution) {
|
||||||
final total = contribution?.effectiveContribution ?? '0';
|
final total = contribution?.effectiveContribution ?? '0';
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -189,7 +199,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
Icon(Icons.info_outline, size: 14, color: _grayText.withOpacity(0.7)),
|
Icon(Icons.info_outline, size: 14, color: _grayText.withOpacity(0.7)),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'贡献值有效期: 剩余 730 天',
|
'贡献值有效期: 730 天',
|
||||||
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.9)),
|
style: TextStyle(fontSize: 12, color: _grayText.withOpacity(0.9)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -200,7 +210,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThreeColumnStats(contribution) {
|
Widget _buildThreeColumnStats(Contribution? contribution) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -240,7 +250,13 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTodayEstimateCard(contribution) {
|
Widget _buildTodayEstimateCard(Contribution? contribution) {
|
||||||
|
// 基于贡献值计算预估收益(暂时显示占位符,后续可接入实际计算API)
|
||||||
|
final effectiveContribution = double.tryParse(contribution?.effectiveContribution ?? '0') ?? 0;
|
||||||
|
// 简单估算:假设每日发放总量为 10000 积分股,按贡献值占比分配
|
||||||
|
// 这里先显示"--"表示暂无数据,后续可接入实际计算
|
||||||
|
final hasContribution = effectiveContribution > 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -279,22 +295,25 @@ class ContributionPage extends ConsumerWidget {
|
||||||
// 收益数值
|
// 收益数值
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: const [
|
children: [
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '+156.78 ',
|
text: hasContribution ? '计算中' : '--',
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _green),
|
style: TextStyle(
|
||||||
|
fontSize: hasContribution ? 14 : 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _green,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
const TextSpan(
|
||||||
text: '积分',
|
text: ' 积分股',
|
||||||
style: TextStyle(fontSize: 12, color: _green),
|
style: TextStyle(fontSize: 12, color: _green),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('股', style: TextStyle(fontSize: 12, color: _green)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -302,7 +321,11 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContributionDetailCard(contribution) {
|
Widget _buildContributionDetailCard(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
AsyncValue<ContributionRecordsPage?> recordsAsync,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -320,9 +343,11 @@ class ContributionPage extends ConsumerWidget {
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: _darkText),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {
|
||||||
child: Row(
|
// TODO: 跳转到完整记录页面
|
||||||
children: const [
|
},
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
Text('查看全部', style: TextStyle(fontSize: 12, color: _orange)),
|
Text('查看全部', style: TextStyle(fontSize: 12, color: _orange)),
|
||||||
Icon(Icons.chevron_right, size: 14, color: _orange),
|
Icon(Icons.chevron_right, size: 14, color: _orange),
|
||||||
],
|
],
|
||||||
|
|
@ -332,26 +357,68 @@ class ContributionPage extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// 明细列表
|
// 明细列表
|
||||||
_buildDetailRow('认种榴莲树', '2024-01-15 14:30', '+22,617.00'),
|
recordsAsync.when(
|
||||||
const Divider(height: 24),
|
data: (recordsPage) {
|
||||||
_buildDetailRow('团队奖励(5级)', '2024-01-15 09:12', '+1,130.85'),
|
if (recordsPage == null || recordsPage.data.isEmpty) {
|
||||||
const Divider(height: 24),
|
return const Padding(
|
||||||
_buildDetailRow('直推奖励', '2024-01-14 18:45', '+565.43'),
|
padding: EdgeInsets.symmetric(vertical: 20),
|
||||||
|
child: Text(
|
||||||
|
'暂无贡献值记录',
|
||||||
|
style: TextStyle(fontSize: 14, color: _grayText),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Column(
|
||||||
|
children: recordsPage.data.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final record = entry.value;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildDetailRow(record),
|
||||||
|
if (index < recordsPage.data.length - 1) const Divider(height: 24),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 20),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
error: (error, _) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
child: Text(
|
||||||
|
'加载失败',
|
||||||
|
style: TextStyle(fontSize: 14, color: _grayText.withOpacity(0.7)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailRow(String title, String time, String amount) {
|
Widget _buildDetailRow(ContributionRecord record) {
|
||||||
|
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||||
|
final formattedDate = dateFormat.format(record.createdAt);
|
||||||
|
final amount = '+${formatAmount(record.finalContribution)}';
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText)),
|
Text(
|
||||||
|
record.displayTitle,
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: _darkText),
|
||||||
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(time, style: const TextStyle(fontSize: 12, color: _grayText)),
|
Text(formattedDate, style: const TextStyle(fontSize: 12, color: _grayText)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -362,7 +429,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTeamStatsCard(contribution) {
|
Widget _buildTeamStatsCard(Contribution? contribution) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -382,7 +449,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
_buildTeamStatItem('直推人数', '${contribution?.directReferralAdoptedCount ?? 0}', '人'),
|
_buildTeamStatItem('直推人数', '${contribution?.directReferralAdoptedCount ?? 0}', '人'),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_buildTeamStatItem('团队总人数', '128', '人'),
|
_buildTeamStatItem('已解锁奖励', '${contribution?.unlockedBonusTiers ?? 0}', '档'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -391,7 +458,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
_buildTeamStatItem('已解锁层级', '${contribution?.unlockedLevelDepth ?? 0}', '级'),
|
_buildTeamStatItem('已解锁层级', '${contribution?.unlockedLevelDepth ?? 0}', '级'),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
_buildTeamStatItem('团队认种总数', '456', '棵'),
|
_buildTeamStatItem('是否认种', contribution?.hasAdopted == true ? '是' : '否', ''),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -419,10 +486,11 @@ class ContributionPage extends ConsumerWidget {
|
||||||
text: '$value ',
|
text: '$value ',
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange),
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: _orange),
|
||||||
),
|
),
|
||||||
TextSpan(
|
if (unit.isNotEmpty)
|
||||||
text: unit,
|
TextSpan(
|
||||||
style: const TextStyle(fontSize: 12, color: _grayText),
|
text: unit,
|
||||||
),
|
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -432,7 +500,39 @@ class ContributionPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExpirationCard(contribution) {
|
Widget _buildExpirationCard(
|
||||||
|
Contribution? contribution,
|
||||||
|
AsyncValue<ContributionRecordsPage?> recordsAsync,
|
||||||
|
) {
|
||||||
|
// 从记录中获取最近的过期日期
|
||||||
|
DateTime? nearestExpireDate;
|
||||||
|
recordsAsync.whenData((recordsPage) {
|
||||||
|
if (recordsPage != null && recordsPage.data.isNotEmpty) {
|
||||||
|
// 找到未过期记录中最近的过期日期
|
||||||
|
final activeRecords = recordsPage.data.where((r) => !r.isExpired).toList();
|
||||||
|
if (activeRecords.isNotEmpty) {
|
||||||
|
activeRecords.sort((a, b) => a.expireDate.compareTo(b.expireDate));
|
||||||
|
nearestExpireDate = activeRecords.first.expireDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算剩余天数和进度
|
||||||
|
final now = DateTime.now();
|
||||||
|
int daysRemaining = 730; // 默认值
|
||||||
|
double progress = 1.0;
|
||||||
|
String expireDateText = '暂无过期信息';
|
||||||
|
|
||||||
|
if (nearestExpireDate != null) {
|
||||||
|
daysRemaining = nearestExpireDate!.difference(now).inDays;
|
||||||
|
if (daysRemaining < 0) daysRemaining = 0;
|
||||||
|
// 假设总有效期为730天
|
||||||
|
progress = daysRemaining / 730;
|
||||||
|
if (progress > 1) progress = 1;
|
||||||
|
if (progress < 0) progress = 0;
|
||||||
|
expireDateText = '您的贡献值将于 ${DateFormat('yyyy-MM-dd').format(nearestExpireDate!)} 失效';
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -443,8 +543,8 @@ class ContributionPage extends ConsumerWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 标题
|
// 标题
|
||||||
Row(
|
const Row(
|
||||||
children: const [
|
children: [
|
||||||
Icon(Icons.timer_outlined, color: _orange, size: 24),
|
Icon(Icons.timer_outlined, color: _orange, size: 24),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -458,7 +558,7 @@ class ContributionPage extends ConsumerWidget {
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.8,
|
value: progress,
|
||||||
minHeight: 10,
|
minHeight: 10,
|
||||||
backgroundColor: _bgGray,
|
backgroundColor: _bgGray,
|
||||||
valueColor: const AlwaysStoppedAnimation<Color>(_orange),
|
valueColor: const AlwaysStoppedAnimation<Color>(_orange),
|
||||||
|
|
@ -466,9 +566,14 @@ class ContributionPage extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 说明文字
|
// 说明文字
|
||||||
const Text(
|
Text(
|
||||||
'您的贡献值将于 2026-01-15 失效',
|
expireDateText,
|
||||||
style: TextStyle(fontSize: 12, color: _grayText),
|
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'剩余 $daysRemaining 天',
|
||||||
|
style: const TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 提示
|
// 提示
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../domain/entities/contribution.dart';
|
import '../../domain/entities/contribution.dart';
|
||||||
|
import '../../domain/entities/contribution_record.dart';
|
||||||
import '../../domain/usecases/contribution/get_user_contribution.dart';
|
import '../../domain/usecases/contribution/get_user_contribution.dart';
|
||||||
|
import '../../domain/repositories/contribution_repository.dart';
|
||||||
import '../../core/di/injection.dart';
|
import '../../core/di/injection.dart';
|
||||||
|
|
||||||
final getUserContributionUseCaseProvider = Provider<GetUserContribution>((ref) {
|
final getUserContributionUseCaseProvider = Provider<GetUserContribution>((ref) {
|
||||||
return getIt<GetUserContribution>();
|
return getIt<GetUserContribution>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final contributionRepositoryProvider = Provider<ContributionRepository>((ref) {
|
||||||
|
return getIt<ContributionRepository>();
|
||||||
|
});
|
||||||
|
|
||||||
final contributionProvider = FutureProvider.family<Contribution?, String>(
|
final contributionProvider = FutureProvider.family<Contribution?, String>(
|
||||||
(ref, accountSequence) async {
|
(ref, accountSequence) async {
|
||||||
final useCase = ref.watch(getUserContributionUseCaseProvider);
|
final useCase = ref.watch(getUserContributionUseCaseProvider);
|
||||||
|
|
@ -17,3 +23,44 @@ final contributionProvider = FutureProvider.family<Contribution?, String>(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// 贡献值记录请求参数
|
||||||
|
class ContributionRecordsParams {
|
||||||
|
final String accountSequence;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
const ContributionRecordsParams({
|
||||||
|
required this.accountSequence,
|
||||||
|
this.page = 1,
|
||||||
|
this.pageSize = 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is ContributionRecordsParams &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
accountSequence == other.accountSequence &&
|
||||||
|
page == other.page &&
|
||||||
|
pageSize == other.pageSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => accountSequence.hashCode ^ page.hashCode ^ pageSize.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 贡献值记录 Provider
|
||||||
|
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
|
||||||
|
(ref, params) async {
|
||||||
|
final repository = ref.watch(contributionRepositoryProvider);
|
||||||
|
final result = await repository.getContributionRecords(
|
||||||
|
params.accountSequence,
|
||||||
|
page: params.page,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
);
|
||||||
|
return result.fold(
|
||||||
|
(failure) => throw Exception(failure.message),
|
||||||
|
(records) => records,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -214,29 +214,6 @@ class UserNotifier extends StateNotifier<UserState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy method for compatibility
|
|
||||||
void login({
|
|
||||||
required String accountSequence,
|
|
||||||
String? nickname,
|
|
||||||
String? phone,
|
|
||||||
}) {
|
|
||||||
state = state.copyWith(
|
|
||||||
accountSequence: accountSequence,
|
|
||||||
nickname: nickname,
|
|
||||||
phone: phone,
|
|
||||||
isLoggedIn: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy mock method for development
|
|
||||||
void setMockUser() {
|
|
||||||
state = state.copyWith(
|
|
||||||
accountSequence: '1001',
|
|
||||||
nickname: '测试用户',
|
|
||||||
phone: '138****8888',
|
|
||||||
isLoggedIn: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>(
|
final userNotifierProvider = StateNotifierProvider<UserNotifier, UserState>(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue