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 {
|
model TradingConfig {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
// 总积分股数量: 100.02亿
|
// 总积分股数量: 100.02亿 (1000.2亿 = 100020000000)
|
||||||
totalShares Decimal @default(10002000000) @map("total_shares") @db.Decimal(30, 8)
|
totalShares Decimal @default(100020000000) @map("total_shares") @db.Decimal(30, 8)
|
||||||
// 目标销毁量: 100亿 (4年销毁完)
|
// 目标销毁量: 100亿 (4年销毁完)
|
||||||
burnTarget Decimal @default(10000000000) @map("burn_target") @db.Decimal(30, 8)
|
burnTarget Decimal @default(10000000000) @map("burn_target") @db.Decimal(30, 8)
|
||||||
// 销毁周期: 4年 (分钟数) 365*4*1440 = 2102400
|
// 销毁周期: 4年 (分钟数) 365*4*1440 = 2102400
|
||||||
|
|
@ -22,6 +22,8 @@ model TradingConfig {
|
||||||
minuteBurnRate Decimal @default(4756.468797564687) @map("minute_burn_rate") @db.Decimal(30, 18)
|
minuteBurnRate Decimal @default(4756.468797564687) @map("minute_burn_rate") @db.Decimal(30, 18)
|
||||||
// 是否启用交易
|
// 是否启用交易
|
||||||
isActive Boolean @default(false) @map("is_active")
|
isActive Boolean @default(false) @map("is_active")
|
||||||
|
// 是否启用买入功能(默认关闭)
|
||||||
|
buyEnabled Boolean @default(false) @map("buy_enabled")
|
||||||
// 启动时间
|
// 启动时间
|
||||||
activatedAt DateTime? @map("activated_at")
|
activatedAt DateTime? @map("activated_at")
|
||||||
createdAt DateTime @default(now()) @map("created_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 { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
|
||||||
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
|
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
|
||||||
import { Public } from '../../shared/guards/jwt-auth.guard';
|
import { Public } from '../../shared/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
class SetBuyEnabledDto {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('Admin')
|
@ApiTags('Admin')
|
||||||
@Controller('admin')
|
@Controller('admin')
|
||||||
export class AdminController {
|
export class AdminController {
|
||||||
|
|
@ -56,6 +60,7 @@ export class AdminController {
|
||||||
return {
|
return {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
|
buyEnabled: false,
|
||||||
activatedAt: null,
|
activatedAt: null,
|
||||||
message: '交易系统未初始化',
|
message: '交易系统未初始化',
|
||||||
};
|
};
|
||||||
|
|
@ -64,6 +69,7 @@ export class AdminController {
|
||||||
return {
|
return {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
isActive: config.isActive,
|
isActive: config.isActive,
|
||||||
|
buyEnabled: config.buyEnabled,
|
||||||
activatedAt: config.activatedAt,
|
activatedAt: config.activatedAt,
|
||||||
totalShares: config.totalShares.toFixed(8),
|
totalShares: config.totalShares.toFixed(8),
|
||||||
burnTarget: config.burnTarget.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')
|
@Post('trading/activate')
|
||||||
@Public() // TODO: 生产环境应添加管理员权限验证
|
@Public() // TODO: 生产环境应添加管理员权限验证
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface TradingConfigEntity {
|
||||||
burnPeriodMinutes: number;
|
burnPeriodMinutes: number;
|
||||||
minuteBurnRate: Money;
|
minuteBurnRate: Money;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
buyEnabled: boolean;
|
||||||
activatedAt: Date | null;
|
activatedAt: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: 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 {
|
private toDomain(record: any): TradingConfigEntity {
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
|
|
@ -93,6 +106,7 @@ export class TradingConfigRepository {
|
||||||
burnPeriodMinutes: record.burnPeriodMinutes,
|
burnPeriodMinutes: record.burnPeriodMinutes,
|
||||||
minuteBurnRate: new Money(record.minuteBurnRate),
|
minuteBurnRate: new Money(record.minuteBurnRate),
|
||||||
isActive: record.isActive,
|
isActive: record.isActive,
|
||||||
|
buyEnabled: record.buyEnabled ?? false,
|
||||||
activatedAt: record.activatedAt,
|
activatedAt: record.activatedAt,
|
||||||
createdAt: record.createdAt,
|
createdAt: record.createdAt,
|
||||||
updatedAt: record.updatedAt,
|
updatedAt: record.updatedAt,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import {
|
||||||
useTradingStatus,
|
useTradingStatus,
|
||||||
useActivateTrading,
|
useActivateTrading,
|
||||||
useDeactivateTrading,
|
useDeactivateTrading,
|
||||||
|
useBuyEnabled,
|
||||||
|
useSetBuyEnabled,
|
||||||
useBurnStatus,
|
useBurnStatus,
|
||||||
useBurnRecords,
|
useBurnRecords,
|
||||||
useMarketOverview,
|
useMarketOverview,
|
||||||
|
|
@ -27,7 +29,9 @@ import {
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Activity,
|
Activity,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
ShoppingCart,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { zhCN } from 'date-fns/locale';
|
import { zhCN } from 'date-fns/locale';
|
||||||
|
|
||||||
|
|
@ -35,8 +39,10 @@ export default function TradingPage() {
|
||||||
const { data: tradingStatus, isLoading: tradingLoading, refetch: refetchTrading } = useTradingStatus();
|
const { data: tradingStatus, isLoading: tradingLoading, refetch: refetchTrading } = useTradingStatus();
|
||||||
const { data: burnStatus, isLoading: burnLoading, refetch: refetchBurn } = useBurnStatus();
|
const { data: burnStatus, isLoading: burnLoading, refetch: refetchBurn } = useBurnStatus();
|
||||||
const { data: marketOverview, isLoading: marketLoading, refetch: refetchMarket } = useMarketOverview();
|
const { data: marketOverview, isLoading: marketLoading, refetch: refetchMarket } = useMarketOverview();
|
||||||
|
const { data: buyEnabledData, isLoading: buyEnabledLoading } = useBuyEnabled();
|
||||||
const activateTrading = useActivateTrading();
|
const activateTrading = useActivateTrading();
|
||||||
const deactivateTrading = useDeactivateTrading();
|
const deactivateTrading = useDeactivateTrading();
|
||||||
|
const setBuyEnabled = useSetBuyEnabled();
|
||||||
|
|
||||||
const [recordsPage, setRecordsPage] = useState(1);
|
const [recordsPage, setRecordsPage] = useState(1);
|
||||||
const [recordsFilter, setRecordsFilter] = useState<'ALL' | 'MINUTE_BURN' | 'SELL_BURN'>('ALL');
|
const [recordsFilter, setRecordsFilter] = useState<'ALL' | 'MINUTE_BURN' | 'SELL_BURN'>('ALL');
|
||||||
|
|
@ -168,6 +174,51 @@ export default function TradingPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* 市场概览 */}
|
{/* 市场概览 */}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ tradingClient.interceptors.response.use(
|
||||||
export interface TradingStatus {
|
export interface TradingStatus {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
buyEnabled: boolean;
|
||||||
activatedAt: string | null;
|
activatedAt: string | null;
|
||||||
totalShares?: string;
|
totalShares?: string;
|
||||||
burnTarget?: string;
|
burnTarget?: string;
|
||||||
|
|
@ -95,6 +96,18 @@ export const tradingApi = {
|
||||||
return response.data;
|
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> => {
|
getBurnStatus: async (): Promise<BurnStatus> => {
|
||||||
const response = await tradingClient.get('/burn/status');
|
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() {
|
export function useBurnStatus() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['trading', 'burn-status'],
|
queryKey: ['trading', 'burn-status'],
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ class ApiEndpoints {
|
||||||
static const String priceHistory = '/api/v2/trading/price/history';
|
static const String priceHistory = '/api/v2/trading/price/history';
|
||||||
static const String priceKlines = '/api/v2/trading/price/klines';
|
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
|
// Trading account endpoints
|
||||||
static String tradingAccount(String accountSequence) =>
|
static String tradingAccount(String accountSequence) =>
|
||||||
'/api/v2/trading/trading/accounts/$accountSequence';
|
'/api/v2/trading/trading/accounts/$accountSequence';
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import '../../../core/network/api_endpoints.dart';
|
||||||
import '../../../core/error/exceptions.dart';
|
import '../../../core/error/exceptions.dart';
|
||||||
|
|
||||||
abstract class TradingRemoteDataSource {
|
abstract class TradingRemoteDataSource {
|
||||||
|
/// 获取买入功能开关状态
|
||||||
|
Future<bool> getBuyEnabled();
|
||||||
|
|
||||||
/// 获取当前价格信息
|
/// 获取当前价格信息
|
||||||
Future<PriceInfoModel> getCurrentPrice();
|
Future<PriceInfoModel> getCurrentPrice();
|
||||||
|
|
||||||
|
|
@ -114,6 +117,17 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
|
|
||||||
TradingRemoteDataSourceImpl({required this.client});
|
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
|
@override
|
||||||
Future<PriceInfoModel> getCurrentPrice() async {
|
Future<PriceInfoModel> getCurrentPrice() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,20 @@ class TradingRepositoryImpl implements TradingRepository {
|
||||||
|
|
||||||
TradingRepositoryImpl({required this.remoteDataSource});
|
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
|
@override
|
||||||
Future<Either<Failure, PriceInfo>> getCurrentPrice() async {
|
Future<Either<Failure, PriceInfo>> getCurrentPrice() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import '../../core/error/failures.dart';
|
||||||
import '../entities/price_info.dart';
|
import '../entities/price_info.dart';
|
||||||
import '../entities/market_overview.dart';
|
import '../entities/market_overview.dart';
|
||||||
import '../entities/trading_account.dart';
|
import '../entities/trading_account.dart';
|
||||||
import '../entities/trade_order.dart';
|
|
||||||
import '../entities/asset_display.dart';
|
import '../entities/asset_display.dart';
|
||||||
import '../entities/kline.dart';
|
import '../entities/kline.dart';
|
||||||
import '../../data/models/trade_order_model.dart';
|
import '../../data/models/trade_order_model.dart';
|
||||||
|
|
||||||
abstract class TradingRepository {
|
abstract class TradingRepository {
|
||||||
|
/// 获取买入功能开关状态
|
||||||
|
Future<Either<Failure, bool>> getBuyEnabled();
|
||||||
|
|
||||||
/// 获取当前价格信息
|
/// 获取当前价格信息
|
||||||
Future<Either<Failure, PriceInfo>> getCurrentPrice();
|
Future<Either<Failure, PriceInfo>> getCurrentPrice();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
@ -8,9 +9,14 @@ import '../../providers/user_providers.dart';
|
||||||
import '../../providers/asset_providers.dart';
|
import '../../providers/asset_providers.dart';
|
||||||
import '../../widgets/shimmer_loading.dart';
|
import '../../widgets/shimmer_loading.dart';
|
||||||
|
|
||||||
class AssetPage extends ConsumerWidget {
|
class AssetPage extends ConsumerStatefulWidget {
|
||||||
const AssetPage({super.key});
|
const AssetPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<AssetPage> createState() => _AssetPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetPageState extends ConsumerState<AssetPage> {
|
||||||
// 设计色彩
|
// 设计色彩
|
||||||
static const Color _orange = Color(0xFFFF6B00);
|
static const Color _orange = Color(0xFFFF6B00);
|
||||||
static const Color _green = Color(0xFF10B981);
|
static const Color _green = Color(0xFF10B981);
|
||||||
|
|
@ -23,8 +29,57 @@ class AssetPage extends ConsumerWidget {
|
||||||
static const Color _scandal = Color(0xFFDCFCE7);
|
static const Color _scandal = Color(0xFFDCFCE7);
|
||||||
static const Color _jewel = Color(0xFF15803D);
|
static const Color _jewel = Color(0xFF15803D);
|
||||||
|
|
||||||
|
// 实时刷新相关状态
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
int _elapsedSeconds = 0;
|
||||||
|
double _initialDisplayValue = 0;
|
||||||
|
double _initialShareBalance = 0;
|
||||||
|
double _growthPerSecond = 0;
|
||||||
|
String? _lastAccountSequence;
|
||||||
|
|
||||||
@override
|
@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 user = ref.watch(userNotifierProvider);
|
||||||
final accountSequence = user.accountSequence ?? '';
|
final accountSequence = user.accountSequence ?? '';
|
||||||
// 使用 public API,不依赖 JWT token
|
// 使用 public API,不依赖 JWT token
|
||||||
|
|
@ -34,6 +89,17 @@ class AssetPage extends ConsumerWidget {
|
||||||
final isLoading = assetAsync.isLoading || accountSequence.isEmpty;
|
final isLoading = assetAsync.isLoading || accountSequence.isEmpty;
|
||||||
final asset = assetAsync.valueOrNull;
|
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(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
backgroundColor: const Color(0xFFF5F5F5),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
|
@ -42,6 +108,8 @@ class AssetPage extends ConsumerWidget {
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
|
_resetTimer();
|
||||||
|
_lastAsset = null;
|
||||||
ref.invalidate(accountAssetProvider(accountSequence));
|
ref.invalidate(accountAssetProvider(accountSequence));
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
|
@ -58,14 +126,14 @@ class AssetPage extends ConsumerWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 总资产卡片 - 始终显示,数字部分闪烁
|
// 总资产卡片 - 始终显示,数字部分闪烁,实时刷新
|
||||||
_buildTotalAssetCard(asset, isLoading),
|
_buildTotalAssetCard(asset, isLoading, _currentDisplayValue),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 快捷操作按钮
|
// 快捷操作按钮
|
||||||
_buildQuickActions(context),
|
_buildQuickActions(context),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 资产列表 - 始终显示,数字部分闪烁
|
// 资产列表 - 始终显示,数字部分闪烁,实时刷新
|
||||||
_buildAssetList(asset, isLoading),
|
_buildAssetList(asset, isLoading, _currentShareBalance),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 交易统计
|
// 交易统计
|
||||||
_buildEarningsCard(asset, isLoading),
|
_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
|
final growthPerSecond = asset != null
|
||||||
? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond)
|
? AssetValueCalculator.calculateGrowthPerSecond(asset.assetGrowthPerSecond)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
||||||
|
// 使用实时计算的资产值(如果有)
|
||||||
|
final displayValue = asset != null && currentDisplayValue > 0
|
||||||
|
? currentDisplayValue.toString()
|
||||||
|
: asset?.displayAssetValue;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -180,9 +253,9 @@ class AssetPage extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// 金额 - 闪烁占位符
|
// 金额 - 实时刷新显示
|
||||||
AmountText(
|
AmountText(
|
||||||
amount: asset != null ? formatAmount(asset.displayAssetValue) : null,
|
amount: displayValue != null ? formatAmount(displayValue) : null,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
prefix: '¥ ',
|
prefix: '¥ ',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
@ -299,24 +372,27 @@ class AssetPage extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAssetList(AssetDisplay? asset, bool isLoading) {
|
Widget _buildAssetList(AssetDisplay? asset, bool isLoading, double currentShareBalance) {
|
||||||
// 计算倍数资产
|
// 使用实时积分股余额
|
||||||
final shareBalance = double.tryParse(asset?.shareBalance ?? '0') ?? 0;
|
final shareBalance = asset != null && currentShareBalance > 0
|
||||||
|
? currentShareBalance
|
||||||
|
: double.tryParse(asset?.shareBalance ?? '0') ?? 0;
|
||||||
final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0;
|
final multiplier = double.tryParse(asset?.burnMultiplier ?? '0') ?? 0;
|
||||||
final multipliedAsset = shareBalance * multiplier;
|
final multipliedAsset = shareBalance * multiplier;
|
||||||
|
final currentPrice = double.tryParse(asset?.currentPrice ?? '0') ?? 0;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// 积分股
|
// 积分股 - 实时刷新
|
||||||
_buildAssetItem(
|
_buildAssetItem(
|
||||||
icon: Icons.trending_up,
|
icon: Icons.trending_up,
|
||||||
iconColor: _orange,
|
iconColor: _orange,
|
||||||
iconBgColor: _serenade,
|
iconBgColor: _serenade,
|
||||||
title: '积分股',
|
title: '积分股',
|
||||||
amount: asset?.shareBalance,
|
amount: asset != null ? shareBalance.toString() : null,
|
||||||
isLoading: isLoading,
|
isLoading: isLoading,
|
||||||
valueInCny: asset != null
|
valueInCny: asset != null
|
||||||
? '¥${formatAmount(_calculateValue(asset.shareBalance, asset.currentPrice))}'
|
? '¥${formatAmount((shareBalance * currentPrice).toString())}'
|
||||||
: null,
|
: null,
|
||||||
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
|
tag: asset != null ? '含倍数资产: ${formatCompact(multipliedAsset.toString())}' : null,
|
||||||
growthText: asset != null ? '每秒 +${formatDecimal(asset.assetGrowthPerSecond, 8)}' : 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({
|
Widget _buildAssetItem({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required Color iconColor,
|
required Color iconColor,
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
|
final assetAsync = ref.watch(accountAssetProvider(accountSequence));
|
||||||
final asset = assetAsync.valueOrNull;
|
final asset = assetAsync.valueOrNull;
|
||||||
|
|
||||||
|
// 获取买入功能开关状态
|
||||||
|
final buyEnabledAsync = ref.watch(buyEnabledProvider);
|
||||||
|
final buyEnabled = buyEnabledAsync.valueOrNull ?? false;
|
||||||
|
|
||||||
// 可用积分股(交易账户)
|
// 可用积分股(交易账户)
|
||||||
final availableShares = asset?.availableShares ?? '0';
|
final availableShares = asset?.availableShares ?? '0';
|
||||||
// 可用积分值(现金)
|
// 可用积分值(现金)
|
||||||
|
|
@ -358,6 +362,15 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
_priceController.text = currentPrice;
|
_priceController.text = currentPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果选中买入但买入功能未开启,强制切换到卖出
|
||||||
|
if (_selectedTab == 0 && !buyEnabled) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted && _selectedTab == 0) {
|
||||||
|
setState(() => _selectedTab = 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
@ -375,25 +388,49 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => setState(() => _selectedTab = 0),
|
onTap: buyEnabled ? () => setState(() => _selectedTab = 0) : null,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: _selectedTab == 0 ? _orange : Colors.transparent,
|
color: _selectedTab == 0 && buyEnabled ? _orange : Colors.transparent,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
'买入',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
textAlign: TextAlign.center,
|
children: [
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 14,
|
'买入',
|
||||||
fontWeight: FontWeight.bold,
|
textAlign: TextAlign.center,
|
||||||
color: _selectedTab == 0 ? _orange : _grayText,
|
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),
|
const SizedBox(height: 24),
|
||||||
// 可用余额提示
|
// 买入功能未开启时显示提示
|
||||||
Container(
|
if (_selectedTab == 0 && !buyEnabled) ...[
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(24),
|
||||||
color: _orange.withValues(alpha: 0.05),
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(8),
|
children: [
|
||||||
),
|
Icon(
|
||||||
child: Row(
|
Icons.lock_outline,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
size: 48,
|
||||||
children: [
|
color: _grayText.withValues(alpha: 0.5),
|
||||||
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),
|
||||||
],
|
const Text(
|
||||||
),
|
'买入功能待开启',
|
||||||
),
|
style: TextStyle(
|
||||||
const SizedBox(height: 16),
|
fontSize: 16,
|
||||||
// 价格输入
|
fontWeight: FontWeight.bold,
|
||||||
_buildInputField('价格', _priceController, '请输入价格', '积分值'),
|
color: _grayText,
|
||||||
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: 8),
|
||||||
],
|
Text(
|
||||||
),
|
'买入功能暂未开放,请耐心等待',
|
||||||
),
|
style: TextStyle(
|
||||||
const SizedBox(height: 24),
|
fontSize: 14,
|
||||||
// 提交按钮
|
color: _grayText.withValues(alpha: 0.7),
|
||||||
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 ? '买入积分股' : '卖出积分股',
|
] else ...[
|
||||||
style: const TextStyle(
|
// 可用余额提示
|
||||||
fontSize: 16,
|
Container(
|
||||||
fontWeight: FontWeight.bold,
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
color: Colors.white,
|
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>();
|
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线周期选择
|
// K线周期选择
|
||||||
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
|
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue