| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../../../core/l10n/app_localizations.dart';
- import '../../../core/utils/avatar_urls.dart';
- import '../../../core/theme/app_colors.dart';
- import '../../../core/utils/dialog_utils.dart';
- import '../../../core/utils/top_toast.dart';
- import '../../../data/repositories/copy_trading_repository.dart';
- class FollowSettingScreen extends ConsumerStatefulWidget {
- const FollowSettingScreen({super.key, required this.trader});
- /// 从 TraderDetailScreen 传入的完整 trader info map
- final Map<String, dynamic> trader;
- @override
- ConsumerState<FollowSettingScreen> createState() =>
- _FollowSettingScreenState();
- }
- class _FollowSettingScreenState extends ConsumerState<FollowSettingScreen> {
- List<Map<String, dynamic>> _symbols = [];
- bool _loadingSymbols = true;
- bool _submitting = false;
- static const _avatarColors = [
- Color(0xFFf7931a),
- Color(0xFF627eea),
- Color(0xFF9945ff),
- Color(0xFFf3ba2f),
- Color(0xFF2775ca),
- Color(0xFF00aae4),
- ];
- @override
- void initState() {
- super.initState();
- _loadSymbols();
- }
- Future<void> _loadSymbols() async {
- final traderId = widget.trader['id']?.toString() ?? '';
- if (traderId.isEmpty) {
- setState(() => _loadingSymbols = false);
- return;
- }
- try {
- final list = await ref
- .read(copyTradingRepositoryProvider)
- .getTraderSymbols(traderId);
- if (mounted) {
- setState(() {
- _symbols = list;
- _loadingSymbols = false;
- });
- }
- } catch (_) {
- if (mounted) setState(() => _loadingSymbols = false);
- }
- }
- Future<void> _submit() async {
- if (_submitting) return;
- setState(() => _submitting = true);
- final traderId = widget.trader['id']?.toString() ?? '';
- try {
- final allSymbolNames = _symbols
- .map((s) =>
- s['symbolName']?.toString() ?? s['symbol']?.toString() ?? '')
- .where((n) => n.isNotEmpty)
- .toList();
- await ref.read(copyTradingRepositoryProvider).followTrader({
- 'traderId': traderId,
- 'tradingMode': '30', // 按交易员比例
- 'tradingAmount': '100', // 100% 跟随
- 'symbols': allSymbolNames,
- });
- if (mounted) {
- showTopToast(
- context,
- message: AppLocalizations.of(context)!.copyTradingSuccess,
- backgroundColor: const Color(0xFF2ECC71),
- );
- Navigator.of(context).pop(true); // 返回 true 表示跟单成功
- }
- } catch (e) {
- if (mounted) {
- setState(() => _submitting = false);
- showTopToast(context, message: extractErrorMessage(e));
- }
- }
- }
- Color _avatarBg(String name) => _avatarColors[
- name.isEmpty ? 0 : name.codeUnitAt(0) % _avatarColors.length];
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- final isDark = Theme.of(context).brightness == Brightness.dark;
- final pageBg = isDark ? AppColors.darkBg : AppColors.lightBg;
- final nickname = widget.trader['nickname']?.toString() ?? '';
- final levelName = widget.trader['levelName']?.toString() ?? '';
- final avatarUrl =
- resolvedAvatarUrlFromRecord(Map<String, dynamic>.from(widget.trader));
- final letter = nickname.isNotEmpty ? nickname[0].toUpperCase() : '?';
- final moneyStrength = widget.trader['moneyStrength']?.toString() ?? '--';
- final registerDays = widget.trader['registerDays']?.toString() ?? '--';
- return Scaffold(
- backgroundColor: pageBg,
- appBar: AppBar(
- leading: IconButton(
- icon: const Icon(Icons.arrow_back_ios, size: 18),
- onPressed: () => Navigator.of(context).pop(),
- ),
- title: Text(AppLocalizations.of(context)!.copyTradingSettings,
- style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)),
- ),
- body: Column(
- children: [
- Expanded(
- child: SingleChildScrollView(
- padding: const EdgeInsets.all(16),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ── 交易员信息卡片 ──────────────────────────────
- Container(
- padding: const EdgeInsets.all(14),
- decoration: BoxDecoration(
- color: isDark
- ? AppColors.darkBgSecondary
- : AppColors.lightBg,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isDark
- ? AppColors.darkDivider
- : AppColors.lightDivider,
- width: 0.5,
- ),
- ),
- child: Row(
- children: [
- // 头像
- _Avatar(
- letter: letter,
- avatarUrl: avatarUrl,
- bg: _avatarBg(nickname),
- size: 48,
- ),
- const SizedBox(width: 12),
- // 名称 + 等级
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- nickname,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 15,
- fontWeight: FontWeight.w700,
- ),
- ),
- if (levelName.isNotEmpty) ...[
- const SizedBox(height: 4),
- _LevelBadge(level: levelName),
- ],
- ],
- ),
- ),
- // 右侧:资金实力 / 入驻天数
- Builder(builder: (context) {
- final l10n = AppLocalizations.of(context)!;
- return Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- _RightStat(
- label: l10n.fundStrength,
- value: '≥$moneyStrength'),
- const SizedBox(height: 6),
- _RightStat(
- label: l10n.settledDaysTitle,
- value: registerDays),
- ],
- );
- }),
- ],
- ),
- ),
- const SizedBox(height: 20),
- // ── 跟单合约 ──────────────────────────────────
- Text(
- AppLocalizations.of(context)!.tradingContracts,
- style: TextStyle(
- color: cs.onSurface.withAlpha(153),
- fontSize: 13,
- ),
- ),
- const SizedBox(height: 10),
- if (_loadingSymbols)
- const Center(
- child:
- CircularProgressIndicator(color: AppColors.brand))
- else if (_symbols.isEmpty)
- Text(
- AppLocalizations.of(context)!.noCopyContracts,
- style: TextStyle(
- color: cs.onSurface.withAlpha(120), fontSize: 13),
- )
- else
- Wrap(
- spacing: 8,
- runSpacing: 8,
- children: _symbols.map((s) {
- final name = s['symbolName']?.toString() ??
- s['symbol']?.toString() ??
- '';
- return Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 14, vertical: 7),
- decoration: BoxDecoration(
- color: AppColors.brand.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: AppColors.brand.withValues(alpha: 0.6),
- width: 1.5,
- ),
- ),
- child: Text(
- name,
- style: const TextStyle(
- color: AppColors.brand,
- fontSize: 13,
- fontWeight: FontWeight.w600,
- ),
- ),
- );
- }).toList(),
- ),
- ],
- ),
- ),
- ),
- // ── 底部按钮 ───────────────────────────────────
- Container(
- padding: EdgeInsets.fromLTRB(
- 16, 12, 16, 12 + MediaQuery.of(context).padding.bottom),
- decoration: BoxDecoration(
- color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
- border: Border(
- top: BorderSide(
- color:
- isDark ? AppColors.darkDivider : AppColors.lightDivider,
- ),
- ),
- ),
- child: SizedBox(
- width: double.infinity,
- height: 48,
- child: ElevatedButton(
- onPressed: _submitting ? null : _submit,
- style: ElevatedButton.styleFrom(
- backgroundColor: AppColors.brand,
- foregroundColor: Colors.black,
- shape: const StadiumBorder(),
- elevation: 0,
- disabledBackgroundColor: AppColors.brand.withAlpha(60),
- ),
- child: _submitting
- ? const SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2, color: Colors.black),
- )
- : Text(AppLocalizations.of(context)!.startCopyTrading,
- style: const TextStyle(
- fontSize: 15, fontWeight: FontWeight.w600)),
- ),
- ),
- ),
- ],
- ),
- );
- }
- }
- // ── 头像 ──────────────────────────────────────────────────
- class _Avatar extends StatelessWidget {
- const _Avatar(
- {required this.letter,
- required this.bg,
- required this.size,
- this.avatarUrl});
- final String letter;
- final Color bg;
- final double size;
- final String? avatarUrl;
- @override
- Widget build(BuildContext context) {
- if (avatarUrl != null && avatarUrl!.isNotEmpty) {
- return ClipOval(
- child: Image.network(
- avatarUrl!,
- width: size,
- height: size,
- fit: BoxFit.cover,
- errorBuilder: (_, __, ___) => _fallback(),
- ),
- );
- }
- return _fallback();
- }
- Widget _fallback() => Container(
- width: size,
- height: size,
- decoration: BoxDecoration(color: bg, shape: BoxShape.circle),
- child: Center(
- child: Text(
- letter,
- style: TextStyle(
- color: Colors.white,
- fontSize: size * 0.4,
- fontWeight: FontWeight.w700,
- ),
- ),
- ),
- );
- }
- // ── 等级角标 ───────────────────────────────────────────────
- class _LevelBadge extends StatelessWidget {
- const _LevelBadge({required this.level});
- final String level;
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
- decoration: BoxDecoration(
- color: AppColors.brand.withValues(alpha: 0.12),
- borderRadius: BorderRadius.circular(4),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const Icon(Icons.access_time, size: 10, color: AppColors.brand),
- const SizedBox(width: 3),
- Text(level,
- style: const TextStyle(
- color: AppColors.brand,
- fontSize: 11,
- fontWeight: FontWeight.w600)),
- ],
- ),
- );
- }
- }
- // ── 右侧统计项 ─────────────────────────────────────────────
- class _RightStat extends StatelessWidget {
- const _RightStat({required this.label, required this.value});
- final String label;
- final String value;
- @override
- Widget build(BuildContext context) {
- final cs = Theme.of(context).colorScheme;
- return Column(
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Text(label,
- style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 11)),
- const SizedBox(height: 2),
- Text(value,
- style: TextStyle(
- color: cs.onSurface,
- fontSize: 14,
- fontWeight: FontWeight.w700)),
- ],
- );
- }
- }
|