From 481a355d72ec493a1964b6a7e4c1d2aa46748834 Mon Sep 17 00:00:00 2001 From: hailin Date: Sat, 17 Jan 2026 08:56:35 -0800 Subject: [PATCH] feat(trading): add buy function control switch with admin management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add buyEnabled field to TradingConfig in trading-service with migration - Add API endpoints for get/set buy enabled status in admin controller - Add buy function switch card in mining-admin-web trading page - Implement buyEnabledProvider in mining-app with 2-minute cache - Show "待开启" when buy function is disabled in trading page - Add real-time asset value refresh in asset page (1-second updates) Co-Authored-By: Claude Opus 4.5 --- .../0004_add_buy_enabled/migration.sql | 6 + .../trading-service/prisma/schema.prisma | 6 +- .../src/api/controllers/admin.controller.ts | 33 ++- .../repositories/trading-config.repository.ts | 14 + .../src/app/(dashboard)/trading/page.tsx | 51 ++++ .../src/features/trading/api/trading.api.ts | 13 + .../src/features/trading/hooks/use-trading.ts | 28 ++ .../lib/core/network/api_endpoints.dart | 3 + .../remote/trading_remote_datasource.dart | 14 + .../repositories/trading_repository_impl.dart | 14 + .../repositories/trading_repository.dart | 4 +- .../presentation/pages/asset/asset_page.dart | 112 ++++++-- .../pages/trading/trading_page.dart | 258 +++++++++++------- .../providers/trading_providers.dart | 17 ++ 14 files changed, 454 insertions(+), 119 deletions(-) create mode 100644 backend/services/trading-service/prisma/migrations/0004_add_buy_enabled/migration.sql diff --git a/backend/services/trading-service/prisma/migrations/0004_add_buy_enabled/migration.sql b/backend/services/trading-service/prisma/migrations/0004_add_buy_enabled/migration.sql new file mode 100644 index 00000000..28d68068 --- /dev/null +++ b/backend/services/trading-service/prisma/migrations/0004_add_buy_enabled/migration.sql @@ -0,0 +1,6 @@ +-- ============================================================================ +-- trading-service 添加买入功能开关 +-- ============================================================================ + +-- AlterTable: 添加买入功能开关字段到 trading_configs 表 +ALTER TABLE "trading_configs" ADD COLUMN "buy_enabled" BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/services/trading-service/prisma/schema.prisma b/backend/services/trading-service/prisma/schema.prisma index d8c6ec48..e67345be 100644 --- a/backend/services/trading-service/prisma/schema.prisma +++ b/backend/services/trading-service/prisma/schema.prisma @@ -12,8 +12,8 @@ datasource db { // 交易全局配置 model TradingConfig { id String @id @default(uuid()) - // 总积分股数量: 100.02亿 - totalShares Decimal @default(10002000000) @map("total_shares") @db.Decimal(30, 8) + // 总积分股数量: 100.02亿 (1000.2亿 = 100020000000) + totalShares Decimal @default(100020000000) @map("total_shares") @db.Decimal(30, 8) // 目标销毁量: 100亿 (4年销毁完) burnTarget Decimal @default(10000000000) @map("burn_target") @db.Decimal(30, 8) // 销毁周期: 4年 (分钟数) 365*4*1440 = 2102400 @@ -22,6 +22,8 @@ model TradingConfig { minuteBurnRate Decimal @default(4756.468797564687) @map("minute_burn_rate") @db.Decimal(30, 18) // 是否启用交易 isActive Boolean @default(false) @map("is_active") + // 是否启用买入功能(默认关闭) + buyEnabled Boolean @default(false) @map("buy_enabled") // 启动时间 activatedAt DateTime? @map("activated_at") createdAt DateTime @default(now()) @map("created_at") diff --git a/backend/services/trading-service/src/api/controllers/admin.controller.ts b/backend/services/trading-service/src/api/controllers/admin.controller.ts index 49108192..e97624f9 100644 --- a/backend/services/trading-service/src/api/controllers/admin.controller.ts +++ b/backend/services/trading-service/src/api/controllers/admin.controller.ts @@ -1,9 +1,13 @@ -import { Controller, Get, Post, HttpCode, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository'; import { Public } from '../../shared/guards/jwt-auth.guard'; +class SetBuyEnabledDto { + enabled: boolean; +} + @ApiTags('Admin') @Controller('admin') export class AdminController { @@ -56,6 +60,7 @@ export class AdminController { return { initialized: false, isActive: false, + buyEnabled: false, activatedAt: null, message: '交易系统未初始化', }; @@ -64,6 +69,7 @@ export class AdminController { return { initialized: true, isActive: config.isActive, + buyEnabled: config.buyEnabled, activatedAt: config.activatedAt, totalShares: config.totalShares.toFixed(8), burnTarget: config.burnTarget.toFixed(8), @@ -73,6 +79,31 @@ export class AdminController { }; } + @Get('trading/buy-enabled') + @Public() + @ApiOperation({ summary: '获取买入功能开关状态' }) + @ApiResponse({ status: 200, description: '返回买入功能是否启用' }) + async getBuyEnabled() { + const config = await this.tradingConfigRepository.getConfig(); + return { + enabled: config?.buyEnabled ?? false, + }; + } + + @Post('trading/buy-enabled') + @Public() // TODO: 生产环境应添加管理员权限验证 + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '设置买入功能开关' }) + @ApiResponse({ status: 200, description: '买入功能开关设置成功' }) + async setBuyEnabled(@Body() dto: SetBuyEnabledDto) { + await this.tradingConfigRepository.setBuyEnabled(dto.enabled); + return { + success: true, + enabled: dto.enabled, + message: dto.enabled ? '买入功能已开启' : '买入功能已关闭', + }; + } + @Post('trading/activate') @Public() // TODO: 生产环境应添加管理员权限验证 @HttpCode(HttpStatus.OK) diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts index 6791f8ba..952dcba5 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/trading-config.repository.ts @@ -10,6 +10,7 @@ export interface TradingConfigEntity { burnPeriodMinutes: number; minuteBurnRate: Money; isActive: boolean; + buyEnabled: boolean; activatedAt: Date | null; createdAt: Date; updatedAt: Date; @@ -85,6 +86,18 @@ export class TradingConfigRepository { }); } + async setBuyEnabled(enabled: boolean): Promise { + const config = await this.prisma.tradingConfig.findFirst(); + if (!config) { + throw new Error('Trading config not initialized'); + } + + await this.prisma.tradingConfig.update({ + where: { id: config.id }, + data: { buyEnabled: enabled }, + }); + } + private toDomain(record: any): TradingConfigEntity { return { id: record.id, @@ -93,6 +106,7 @@ export class TradingConfigRepository { burnPeriodMinutes: record.burnPeriodMinutes, minuteBurnRate: new Money(record.minuteBurnRate), isActive: record.isActive, + buyEnabled: record.buyEnabled ?? false, activatedAt: record.activatedAt, createdAt: record.createdAt, updatedAt: record.updatedAt, diff --git a/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx index 2d693d91..f2cc3395 100644 --- a/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx +++ b/frontend/mining-admin-web/src/app/(dashboard)/trading/page.tsx @@ -6,6 +6,8 @@ import { useTradingStatus, useActivateTrading, useDeactivateTrading, + useBuyEnabled, + useSetBuyEnabled, useBurnStatus, useBurnRecords, useMarketOverview, @@ -27,7 +29,9 @@ import { DollarSign, Activity, RefreshCw, + ShoppingCart, } from 'lucide-react'; +import { Switch } from '@/components/ui/switch'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; @@ -35,8 +39,10 @@ export default function TradingPage() { const { data: tradingStatus, isLoading: tradingLoading, refetch: refetchTrading } = useTradingStatus(); const { data: burnStatus, isLoading: burnLoading, refetch: refetchBurn } = useBurnStatus(); const { data: marketOverview, isLoading: marketLoading, refetch: refetchMarket } = useMarketOverview(); + const { data: buyEnabledData, isLoading: buyEnabledLoading } = useBuyEnabled(); const activateTrading = useActivateTrading(); const deactivateTrading = useDeactivateTrading(); + const setBuyEnabled = useSetBuyEnabled(); const [recordsPage, setRecordsPage] = useState(1); const [recordsFilter, setRecordsFilter] = useState<'ALL' | 'MINUTE_BURN' | 'SELL_BURN'>('ALL'); @@ -168,6 +174,51 @@ export default function TradingPage() { + {/* 买入功能开关 */} + + +
+
+ + + 买入功能开关 + + 控制用户是否可以在App中使用买入功能 +
+ {buyEnabledLoading ? ( + + ) : buyEnabledData?.enabled ? ( + + + 已开启 + + ) : ( + + + 已关闭 + + )} +
+
+ +
+
+

买入功能

+

+ {buyEnabledData?.enabled + ? '用户可以在兑换页面使用买入功能购买积分股' + : '买入功能已关闭,用户在兑换页面将看到"待开启"提示'} +

+
+ setBuyEnabled.mutate(checked)} + disabled={setBuyEnabled.isPending || buyEnabledLoading} + /> +
+
+
+ {/* 市场概览和销毁进度 */}
{/* 市场概览 */} diff --git a/frontend/mining-admin-web/src/features/trading/api/trading.api.ts b/frontend/mining-admin-web/src/features/trading/api/trading.api.ts index d0051255..022a4486 100644 --- a/frontend/mining-admin-web/src/features/trading/api/trading.api.ts +++ b/frontend/mining-admin-web/src/features/trading/api/trading.api.ts @@ -40,6 +40,7 @@ tradingClient.interceptors.response.use( export interface TradingStatus { initialized: boolean; isActive: boolean; + buyEnabled: boolean; activatedAt: string | null; totalShares?: string; burnTarget?: string; @@ -95,6 +96,18 @@ export const tradingApi = { return response.data; }, + // 获取买入功能开关状态 + getBuyEnabled: async (): Promise<{ enabled: boolean }> => { + const response = await tradingClient.get('/admin/trading/buy-enabled'); + return response.data; + }, + + // 设置买入功能开关 + setBuyEnabled: async (enabled: boolean): Promise<{ success: boolean; enabled: boolean; message: string }> => { + const response = await tradingClient.post('/admin/trading/buy-enabled', { enabled }); + return response.data; + }, + // 获取销毁状态 getBurnStatus: async (): Promise => { const response = await tradingClient.get('/burn/status'); diff --git a/frontend/mining-admin-web/src/features/trading/hooks/use-trading.ts b/frontend/mining-admin-web/src/features/trading/hooks/use-trading.ts index 3de9f6f0..c98bf878 100644 --- a/frontend/mining-admin-web/src/features/trading/hooks/use-trading.ts +++ b/frontend/mining-admin-web/src/features/trading/hooks/use-trading.ts @@ -50,6 +50,34 @@ export function useDeactivateTrading() { }); } +export function useBuyEnabled() { + return useQuery({ + queryKey: ['trading', 'buy-enabled'], + queryFn: () => tradingApi.getBuyEnabled(), + refetchInterval: 30000, + }); +} + +export function useSetBuyEnabled() { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: (enabled: boolean) => tradingApi.setBuyEnabled(enabled), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['trading'] }); + toast({ title: data.message || (data.enabled ? '买入功能已开启' : '买入功能已关闭'), variant: 'success' as any }); + }, + onError: (error: any) => { + toast({ + title: '设置失败', + description: error?.response?.data?.message || '请稍后重试', + variant: 'destructive', + }); + }, + }); +} + export function useBurnStatus() { return useQuery({ queryKey: ['trading', 'burn-status'], diff --git a/frontend/mining-app/lib/core/network/api_endpoints.dart b/frontend/mining-app/lib/core/network/api_endpoints.dart index aec0acac..c14d2175 100644 --- a/frontend/mining-app/lib/core/network/api_endpoints.dart +++ b/frontend/mining-app/lib/core/network/api_endpoints.dart @@ -29,6 +29,9 @@ class ApiEndpoints { static const String priceHistory = '/api/v2/trading/price/history'; static const String priceKlines = '/api/v2/trading/price/klines'; + // Trading admin endpoints + static const String buyEnabled = '/api/v2/trading/admin/trading/buy-enabled'; + // Trading account endpoints static String tradingAccount(String accountSequence) => '/api/v2/trading/trading/accounts/$accountSequence'; diff --git a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart index 095a3d84..cad763c5 100644 --- a/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart +++ b/frontend/mining-app/lib/data/datasources/remote/trading_remote_datasource.dart @@ -11,6 +11,9 @@ import '../../../core/network/api_endpoints.dart'; import '../../../core/error/exceptions.dart'; abstract class TradingRemoteDataSource { + /// 获取买入功能开关状态 + Future getBuyEnabled(); + /// 获取当前价格信息 Future getCurrentPrice(); @@ -114,6 +117,17 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource { TradingRemoteDataSourceImpl({required this.client}); + @override + Future getBuyEnabled() async { + try { + final response = await client.get(ApiEndpoints.buyEnabled); + return response.data['enabled'] ?? false; + } catch (e) { + // 如果获取失败,默认返回false(关闭状态) + return false; + } + } + @override Future getCurrentPrice() async { try { diff --git a/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart b/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart index 99f6227f..8ac9acb1 100644 --- a/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart +++ b/frontend/mining-app/lib/data/repositories/trading_repository_impl.dart @@ -15,6 +15,20 @@ class TradingRepositoryImpl implements TradingRepository { TradingRepositoryImpl({required this.remoteDataSource}); + @override + Future> getBuyEnabled() async { + try { + final result = await remoteDataSource.getBuyEnabled(); + return Right(result); + } on ServerException catch (e) { + return Left(ServerFailure(e.message)); + } on NetworkException { + return Left(const NetworkFailure()); + } catch (e) { + return const Right(false); + } + } + @override Future> getCurrentPrice() async { try { diff --git a/frontend/mining-app/lib/domain/repositories/trading_repository.dart b/frontend/mining-app/lib/domain/repositories/trading_repository.dart index 9cd53e03..817b160e 100644 --- a/frontend/mining-app/lib/domain/repositories/trading_repository.dart +++ b/frontend/mining-app/lib/domain/repositories/trading_repository.dart @@ -3,12 +3,14 @@ import '../../core/error/failures.dart'; import '../entities/price_info.dart'; import '../entities/market_overview.dart'; import '../entities/trading_account.dart'; -import '../entities/trade_order.dart'; import '../entities/asset_display.dart'; import '../entities/kline.dart'; import '../../data/models/trade_order_model.dart'; abstract class TradingRepository { + /// 获取买入功能开关状态 + Future> getBuyEnabled(); + /// 获取当前价格信息 Future> getCurrentPrice(); diff --git a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart index 4c0d31f3..3c32bd95 100644 --- a/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart +++ b/frontend/mining-app/lib/presentation/pages/asset/asset_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -8,9 +9,14 @@ import '../../providers/user_providers.dart'; import '../../providers/asset_providers.dart'; import '../../widgets/shimmer_loading.dart'; -class AssetPage extends ConsumerWidget { +class AssetPage extends ConsumerStatefulWidget { const AssetPage({super.key}); + @override + ConsumerState createState() => _AssetPageState(); +} + +class _AssetPageState extends ConsumerState { // 设计色彩 static const Color _orange = Color(0xFFFF6B00); static const Color _green = Color(0xFF10B981); @@ -23,8 +29,57 @@ class AssetPage extends ConsumerWidget { static const Color _scandal = Color(0xFFDCFCE7); static const Color _jewel = Color(0xFF15803D); + // 实时刷新相关状态 + Timer? _refreshTimer; + int _elapsedSeconds = 0; + double _initialDisplayValue = 0; + double _initialShareBalance = 0; + double _growthPerSecond = 0; + String? _lastAccountSequence; + @override - Widget build(BuildContext context, WidgetRef ref) { + void dispose() { + _refreshTimer?.cancel(); + super.dispose(); + } + + /// 启动定时器 + void _startTimer(AssetDisplay asset) { + _refreshTimer?.cancel(); + _elapsedSeconds = 0; + _initialDisplayValue = double.tryParse(asset.displayAssetValue) ?? 0; + _initialShareBalance = double.tryParse(asset.shareBalance) ?? 0; + _growthPerSecond = AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond); + + _refreshTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (mounted) { + setState(() { + _elapsedSeconds++; + }); + } + }); + } + + /// 重置定时器(刷新时调用) + void _resetTimer() { + _refreshTimer?.cancel(); + _elapsedSeconds = 0; + } + + /// 计算当前实时资产显示值 + double get _currentDisplayValue { + return _initialDisplayValue + (_elapsedSeconds * _growthPerSecond * (double.tryParse(_lastAsset?.currentPrice ?? '0') ?? 0)); + } + + /// 计算当前实时积分股余额 + double get _currentShareBalance { + return _initialShareBalance + (_elapsedSeconds * _growthPerSecond); + } + + AssetDisplay? _lastAsset; + + @override + Widget build(BuildContext context) { final user = ref.watch(userNotifierProvider); final accountSequence = user.accountSequence ?? ''; // 使用 public API,不依赖 JWT token @@ -34,6 +89,17 @@ class AssetPage extends ConsumerWidget { final isLoading = assetAsync.isLoading || accountSequence.isEmpty; final asset = assetAsync.valueOrNull; + // 当数据加载完成时启动定时器 + if (asset != null && (_lastAsset == null || _lastAccountSequence != accountSequence)) { + _lastAccountSequence = accountSequence; + _lastAsset = asset; + WidgetsBinding.instance.addPostFrameCallback((_) { + _startTimer(asset); + }); + } else if (asset != null) { + _lastAsset = asset; + } + return Scaffold( backgroundColor: const Color(0xFFF5F5F5), body: SafeArea( @@ -42,6 +108,8 @@ class AssetPage extends ConsumerWidget { builder: (context, constraints) { return RefreshIndicator( onRefresh: () async { + _resetTimer(); + _lastAsset = null; ref.invalidate(accountAssetProvider(accountSequence)); }, child: SingleChildScrollView( @@ -58,14 +126,14 @@ class AssetPage extends ConsumerWidget { child: Column( children: [ const SizedBox(height: 8), - // 总资产卡片 - 始终显示,数字部分闪烁 - _buildTotalAssetCard(asset, isLoading), + // 总资产卡片 - 始终显示,数字部分闪烁,实时刷新 + _buildTotalAssetCard(asset, isLoading, _currentDisplayValue), const SizedBox(height: 24), // 快捷操作按钮 _buildQuickActions(context), const SizedBox(height: 24), - // 资产列表 - 始终显示,数字部分闪烁 - _buildAssetList(asset, isLoading), + // 资产列表 - 始终显示,数字部分闪烁,实时刷新 + _buildAssetList(asset, isLoading, _currentShareBalance), const SizedBox(height: 24), // 交易统计 _buildEarningsCard(asset, isLoading), @@ -104,12 +172,17 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading) { + Widget _buildTotalAssetCard(AssetDisplay? asset, bool isLoading, double currentDisplayValue) { // 计算每秒增长 final growthPerSecond = asset != null ? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond) : 0.0; + // 使用实时计算的资产值(如果有) + final displayValue = asset != null && currentDisplayValue > 0 + ? currentDisplayValue.toString() + : asset?.displayAssetValue; + return Container( decoration: BoxDecoration( color: Colors.white, @@ -180,9 +253,9 @@ class AssetPage extends ConsumerWidget { ], ), const SizedBox(height: 8), - // 金额 - 闪烁占位符 + // 金额 - 实时刷新显示 AmountText( - amount: asset != null ? formatAmount(asset.displayAssetValue) : null, + amount: displayValue != null ? formatAmount(displayValue) : null, isLoading: isLoading, prefix: '¥ ', style: const TextStyle( @@ -299,24 +372,27 @@ class AssetPage extends ConsumerWidget { ); } - Widget _buildAssetList(AssetDisplay? asset, bool isLoading) { - // 计算倍数资产 - final shareBalance = double.tryParse(asset?.shareBalance ?? '0') ?? 0; + Widget _buildAssetList(AssetDisplay? asset, bool isLoading, double currentShareBalance) { + // 使用实时积分股余额 + final shareBalance = asset != null && currentShareBalance > 0 + ? currentShareBalance + : double.tryParse(asset?.shareBalance ?? '0') ?? 0; final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0; final multipliedAsset = shareBalance * multiplier; + final currentPrice = double.tryParse(asset?.currentPrice ?? '0') ?? 0; return Column( children: [ - // 积分股 + // 积分股 - 实时刷新 _buildAssetItem( icon: Icons.trending_up, iconColor: _orange, iconBgColor: _serenade, title: '积分股', - amount: asset?.shareBalance, + amount: asset != null ? shareBalance.toString() : null, isLoading: isLoading, valueInCny: asset != null - ? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}' + ? '¥${formatAmount((shareBalance * currentPrice).toString())}' : null, tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null, growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : null, @@ -347,12 +423,6 @@ class AssetPage extends ConsumerWidget { ); } - String _calculateValue(String balance, String price) { - final b = double.tryParse(balance) ?? 0; - final p = double.tryParse(price) ?? 0; - return (b * p).toString(); - } - Widget _buildAssetItem({ required IconData icon, required Color iconColor, diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index f61fa5b0..e1187a38 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -348,6 +348,10 @@ class _TradingPageState extends ConsumerState { final assetAsync = ref.watch(accountAssetProvider(accountSequence)); final asset = assetAsync.valueOrNull; + // 获取买入功能开关状态 + final buyEnabledAsync = ref.watch(buyEnabledProvider); + final buyEnabled = buyEnabledAsync.valueOrNull ?? false; + // 可用积分股(交易账户) final availableShares = asset?.availableShares ?? '0'; // 可用积分值(现金) @@ -358,6 +362,15 @@ class _TradingPageState extends ConsumerState { _priceController.text = currentPrice; } + // 如果选中买入但买入功能未开启,强制切换到卖出 + if (_selectedTab == 0 && !buyEnabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _selectedTab == 0) { + setState(() => _selectedTab = 1); + } + }); + } + return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.all(20), @@ -375,25 +388,49 @@ class _TradingPageState extends ConsumerState { children: [ Expanded( child: GestureDetector( - onTap: () => setState(() => _selectedTab = 0), + onTap: buyEnabled ? () => setState(() => _selectedTab = 0) : null, child: Container( padding: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( border: Border( bottom: BorderSide( - color: _selectedTab == 0 ? _orange : Colors.transparent, + color: _selectedTab == 0 && buyEnabled ? _orange : Colors.transparent, width: 2, ), ), ), - child: Text( - '买入', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: _selectedTab == 0 ? _orange : _grayText, - ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '买入', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: buyEnabled + ? (_selectedTab == 0 ? _orange : _grayText) + : _grayText.withValues(alpha: 0.5), + ), + ), + if (!buyEnabled) ...[ + const SizedBox(width: 4), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: _grayText.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '待开启', + style: TextStyle( + fontSize: 10, + color: _grayText.withValues(alpha: 0.7), + ), + ), + ), + ], + ], ), ), ), @@ -427,96 +464,129 @@ class _TradingPageState extends ConsumerState { ), ), const SizedBox(height: 24), - // 可用余额提示 - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: _orange.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _selectedTab == 0 ? '可用积分值' : '可用积分股', - style: const TextStyle(fontSize: 12, color: _grayText), - ), - Text( - _selectedTab == 0 - ? formatAmount(availableCash) - : formatAmount(availableShares), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: _orange, + // 买入功能未开启时显示提示 + if (_selectedTab == 0 && !buyEnabled) ...[ + Container( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Icon( + Icons.lock_outline, + size: 48, + color: _grayText.withValues(alpha: 0.5), ), - ), - ], - ), - ), - const SizedBox(height: 16), - // 价格输入 - _buildInputField('价格', _priceController, '请输入价格', '积分值'), - const SizedBox(height: 16), - // 数量输入 - 带"全部"按钮 - _buildQuantityInputField( - '数量', - _quantityController, - '请输入数量', - '积分股', - _selectedTab == 1 ? availableShares : null, - _selectedTab == 0 ? availableCash : null, - currentPrice, - ), - const SizedBox(height: 16), - // 预计获得/支出 - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: _bgGray, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - _selectedTab == 0 ? '预计支出' : '预计获得', - style: const TextStyle(fontSize: 12, color: _grayText), - ), - Text( - _calculateEstimate(), - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: _orange, + const SizedBox(height: 16), + const Text( + '买入功能待开启', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: _grayText, + ), ), - ), - ], - ), - ), - const SizedBox(height: 24), - // 提交按钮 - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - onPressed: _handleTrade, - style: ElevatedButton.styleFrom( - backgroundColor: _orange, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + const SizedBox(height: 8), + Text( + '买入功能暂未开放,请耐心等待', + style: TextStyle( + fontSize: 14, + color: _grayText.withValues(alpha: 0.7), + ), + ), + ], ), - child: Text( - _selectedTab == 0 ? '买入积分股' : '卖出积分股', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, + ), + ] else ...[ + // 可用余额提示 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _orange.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _selectedTab == 0 ? '可用积分值' : '可用积分股', + style: const TextStyle(fontSize: 12, color: _grayText), + ), + Text( + _selectedTab == 0 + ? formatAmount(availableCash) + : formatAmount(availableShares), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _orange, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // 价格输入 + _buildInputField('价格', _priceController, '请输入价格', '积分值'), + const SizedBox(height: 16), + // 数量输入 - 带"全部"按钮 + _buildQuantityInputField( + '数量', + _quantityController, + '请输入数量', + '积分股', + _selectedTab == 1 ? availableShares : null, + _selectedTab == 0 ? availableCash : null, + currentPrice, + ), + const SizedBox(height: 16), + // 预计获得/支出 + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _bgGray, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _selectedTab == 0 ? '预计支出' : '预计获得', + style: const TextStyle(fontSize: 12, color: _grayText), + ), + Text( + _calculateEstimate(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: _orange, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + // 提交按钮 + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: _handleTrade, + style: ElevatedButton.styleFrom( + backgroundColor: _orange, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + _selectedTab == 0 ? '买入积分股' : '卖出积分股', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), ), ), ), - ), + ], ], ), ); diff --git a/frontend/mining-app/lib/presentation/providers/trading_providers.dart b/frontend/mining-app/lib/presentation/providers/trading_providers.dart index 339beef3..2ae67cfb 100644 --- a/frontend/mining-app/lib/presentation/providers/trading_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/trading_providers.dart @@ -14,6 +14,23 @@ final tradingRepositoryProvider = Provider((ref) { return getIt(); }); +// 买入功能开关 Provider (2分钟缓存) +final buyEnabledProvider = FutureProvider((ref) async { + final repository = ref.watch(tradingRepositoryProvider); + final result = await repository.getBuyEnabled(); + + ref.keepAlive(); + final timer = Timer(const Duration(minutes: 2), () { + ref.invalidateSelf(); + }); + ref.onDispose(() => timer.cancel()); + + return result.fold( + (failure) => false, // 获取失败时默认关闭 + (enabled) => enabled, + ); +}); + // K线周期选择 final selectedKlinePeriodProvider = StateProvider((ref) => '1h');