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:
parent
e8f3c34723
commit
481a355d72
|
|
@ -0,0 +1,6 @@
|
|||
-- ============================================================================
|
||||
-- trading-service 添加买入功能开关
|
||||
-- ============================================================================
|
||||
|
||||
-- AlterTable: 添加买入功能开关字段到 trading_configs 表
|
||||
ALTER TABLE "trading_configs" ADD COLUMN "buy_enabled" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
{/* 市场概览 */}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue