feat(trading): add buy function control switch with admin management

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-17 08:56:35 -08:00
parent e8f3c34723
commit 481a355d72
14 changed files with 454 additions and 119 deletions

View File

@ -0,0 +1,6 @@
-- ============================================================================
-- trading-service 添加买入功能开关
-- ============================================================================
-- AlterTable: 添加买入功能开关字段到 trading_configs 表
ALTER TABLE "trading_configs" ADD COLUMN "buy_enabled" BOOLEAN NOT NULL DEFAULT false;

View File

@ -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")

View File

@ -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)

View File

@ -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<void> {
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,

View File

@ -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() {
</CardContent>
</Card>
{/* 买入功能开关 */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<ShoppingCart className="h-5 w-5" />
</CardTitle>
<CardDescription>App中使用买入功能</CardDescription>
</div>
{buyEnabledLoading ? (
<Skeleton className="h-6 w-16" />
) : buyEnabledData?.enabled ? (
<Badge variant="default" className="flex items-center gap-1 bg-green-500">
<CheckCircle2 className="h-3 w-3" />
</Badge>
) : (
<Badge variant="secondary" className="flex items-center gap-1">
<Pause className="h-3 w-3" />
</Badge>
)}
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
<div className="space-y-1">
<p className="font-medium"></p>
<p className="text-sm text-muted-foreground">
{buyEnabledData?.enabled
? '用户可以在兑换页面使用买入功能购买积分股'
: '买入功能已关闭,用户在兑换页面将看到"待开启"提示'}
</p>
</div>
<Switch
checked={buyEnabledData?.enabled ?? false}
onCheckedChange={(checked) => setBuyEnabled.mutate(checked)}
disabled={setBuyEnabled.isPending || buyEnabledLoading}
/>
</div>
</CardContent>
</Card>
{/* 市场概览和销毁进度 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 市场概览 */}

View File

@ -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<BurnStatus> => {
const response = await tradingClient.get('/burn/status');

View File

@ -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'],

View File

@ -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';

View File

@ -11,6 +11,9 @@ import '../../../core/network/api_endpoints.dart';
import '../../../core/error/exceptions.dart';
abstract class TradingRemoteDataSource {
///
Future<bool> getBuyEnabled();
///
Future<PriceInfoModel> getCurrentPrice();
@ -114,6 +117,17 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
TradingRemoteDataSourceImpl({required this.client});
@override
Future<bool> getBuyEnabled() async {
try {
final response = await client.get(ApiEndpoints.buyEnabled);
return response.data['enabled'] ?? false;
} catch (e) {
// false
return false;
}
}
@override
Future<PriceInfoModel> getCurrentPrice() async {
try {

View File

@ -15,6 +15,20 @@ class TradingRepositoryImpl implements TradingRepository {
TradingRepositoryImpl({required this.remoteDataSource});
@override
Future<Either<Failure, bool>> 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<Either<Failure, PriceInfo>> getCurrentPrice() async {
try {

View File

@ -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<Either<Failure, bool>> getBuyEnabled();
///
Future<Either<Failure, PriceInfo>> getCurrentPrice();

View File

@ -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<AssetPage> createState() => _AssetPageState();
}
class _AssetPageState extends ConsumerState<AssetPage> {
//
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,

View File

@ -348,6 +348,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
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<TradingPage> {
_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<TradingPage> {
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<TradingPage> {
),
),
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,
),
),
),
),
),
],
],
),
);

View File

@ -14,6 +14,23 @@ final tradingRepositoryProvider = Provider<TradingRepository>((ref) {
return getIt<TradingRepository>();
});
// Provider (2)
final buyEnabledProvider = FutureProvider<bool>((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<String>((ref) => '1h');