import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/utils/avatar_urls.dart'; import '../../../core/utils/dialog_utils.dart'; import '../../../core/utils/top_toast.dart'; import '../../../data/repositories/copy_trading_repository.dart'; import '../../../providers/profile_provider.dart'; class TraderSettingsScreen extends ConsumerStatefulWidget { const TraderSettingsScreen({super.key}); @override ConsumerState createState() => _TraderSettingsScreenState(); } class _TraderSettingsScreenState extends ConsumerState { bool _loading = true; bool _saving = false; String _traderId = ''; String? _avatarUrl; late TextEditingController _nicknameCtrl; late TextEditingController _descCtrl; // 所有可选标签 List> _allTags = []; // 已选标签 id 集合 Set _selectedTagIds = {}; static const int _maxTags = 4; // 所有可选合约(固定列表,与安卓一致) static const List _allSymbols = ['BTC/USDT', 'ETH/USDT']; // 已选合约(symbol 名称集合) Set _selectedSymbols = {}; // 初始合约(用于判断是否变更) Set _initialSymbols = {}; @override void initState() { super.initState(); _nicknameCtrl = TextEditingController(); _descCtrl = TextEditingController(); _loadAll(); } @override void dispose() { _nicknameCtrl.dispose(); _descCtrl.dispose(); super.dispose(); } Future _loadAll() async { setState(() => _loading = true); try { final repo = ref.read(copyTradingRepositoryProvider); // 并行: 个人信息 + 所有标签 + 已选标签 final results = await Future.wait([ repo.getFollowerInfo(), // [0] repo.getAllTags(), // [1] repo.getMyTags(), // [2] ]); final info = results[0] as Map?; final allTags = results[1] as List>; final myTags = results[2] as List>; _traderId = info?['id']?.toString() ?? ''; _avatarUrl = info != null ? resolvedAvatarUrlFromRecord(info) : null; _nicknameCtrl.text = info?['nickname']?.toString() ?? ''; _descCtrl.text = info?['description']?.toString() ?? ''; _allTags = allTags; _selectedTagIds = myTags.map((t) => t['id']?.toString() ?? '').toSet(); // 加载已选合约(API 只返回已选的,全量列表用固定 _allSymbols) if (_traderId.isNotEmpty) { final symbols = await repo.getTraderSymbols(_traderId); _selectedSymbols = symbols .map((s) => s['symbol']?.toString() ?? '') .where((s) => s.isNotEmpty) .toSet(); _initialSymbols = Set.from(_selectedSymbols); } if (context.mounted) setState(() => _loading = false); } catch (e) { if (context.mounted) setState(() => _loading = false); } } Future _save() async { if (!context.mounted) return; setState(() => _saving = true); try { final repo = ref.read(copyTradingRepositoryProvider); final nickname = _nicknameCtrl.text.trim(); final desc = _descCtrl.text.trim(); // 顺序执行,避免并行时部分成功部分失败导致 UI 混乱 await repo.updateTraderProfile(nickname: nickname, description: desc); await repo.updateTraderTags(_selectedTagIds.toList()); // 仅当合约有变更时才调用(有跟随者时后台会拒绝) final symbolsChanged = !_selectedSymbols.containsAll(_initialSymbols) || !_initialSymbols.containsAll(_selectedSymbols); if (symbolsChanged) { await repo.updateTraderSymbols(_selectedSymbols.toList()); } if (context.mounted) { showTopToast(context, message: AppLocalizations.of(context)!.savedSuccess, backgroundColor: const Color(0xFF2ECC71)); context.pop(); } } catch (e) { if (context.mounted) { showTopToast(context, message: extractErrorMessage(e)); } } finally { if (context.mounted) setState(() => _saving = false); } } Future _pickAvatar() async { final l10nSheet = AppLocalizations.of(context)!; final source = await showModalBottomSheet( context: context, useRootNavigator: true, builder: (sheetCtx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () => Navigator.pop(sheetCtx, ImageSource.camera), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row(children: [ const Icon(Icons.camera_alt_outlined), const SizedBox(width: 16), Text(l10nSheet.takePhoto) ]), ), ), GestureDetector( onTap: () => Navigator.pop(sheetCtx, ImageSource.gallery), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row(children: [ const Icon(Icons.photo_library_outlined), const SizedBox(width: 16), Text(l10nSheet.chooseFromAlbum) ]), ), ), GestureDetector( onTap: () => Navigator.pop(sheetCtx), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row(children: [ const Icon(Icons.close), const SizedBox(width: 16), Text(l10nSheet.cancelLabel) ]), ), ), ], ), ), ); if (source == null || !context.mounted) return; final picker = ImagePicker(); final XFile? file = await picker.pickImage( source: source, maxWidth: 800, maxHeight: 800, imageQuality: 85, ); if (file == null || !context.mounted) return; setState(() => _saving = true); try { final url = await ref.read(copyTradingRepositoryProvider).updateAvatar(file.path); if (context.mounted && url != null) { setState(() { _avatarUrl = normalizeAvatarHttpUrl(url) ?? url; }); ref.invalidate(profileProvider); showTopToast(context, message: AppLocalizations.of(context)!.avatarUpdated, backgroundColor: const Color(0xFF2ECC71)); } } catch (e) { if (context.mounted) showTopToast(context, message: extractErrorMessage(e)); } finally { if (context.mounted) setState(() => _saving = false); } } Future _cancelTrader() async { final l10n = AppLocalizations.of(context)!; final confirm = await showDialog( context: context, builder: (ctx) { final cs = Theme.of(ctx).colorScheme; final isDark = Theme.of(ctx).brightness == Brightness.dark; return AlertDialog( backgroundColor: isDark ? const Color(0xFF1E1E1E) : Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), title: Text( l10n.confirmCancelTitle, style: TextStyle( color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600), ), content: Text( l10n.confirmCancelTraderMsg, style: TextStyle( color: cs.onSurface.withAlpha(180), fontSize: 14, height: 1.5), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(l10n.cancelLabel, style: TextStyle(color: cs.onSurface.withAlpha(153))), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: Text(l10n.confirm, style: const TextStyle(color: Colors.red)), ), ], ); }, ); if (confirm != true) return; try { await ref.read(copyTradingRepositoryProvider).cancelTraderQualification(); if (context.mounted) { showTopToast(context, message: AppLocalizations.of(context)!.applicationSubmitted, backgroundColor: const Color(0xFF2ECC71)); context.pop(); } } catch (e) { if (context.mounted) { showTopToast(context, message: extractErrorMessage(e)); } } } void _toggleTag(String id) { setState(() { if (_selectedTagIds.contains(id)) { _selectedTagIds.remove(id); } else { if (_selectedTagIds.length >= _maxTags) return; _selectedTagIds.add(id); } }); } void _toggleSymbol(String symbol) { setState(() { if (_selectedSymbols.contains(symbol)) { _selectedSymbols.remove(symbol); } else { _selectedSymbols.add(symbol); } }); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final nickname = _nicknameCtrl.text.isEmpty ? 'T' : _nicknameCtrl.text[0].toUpperCase(); return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios, size: 18), onPressed: () => context.pop(), ), title: Text(AppLocalizations.of(context)!.tradingSettings, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), ), body: _loading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), // ── 头像 ────────────────────────────────── Center( child: GestureDetector( onTap: _pickAvatar, child: Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(20), child: (_avatarUrl != null && _avatarUrl!.isNotEmpty) ? Image.network( _avatarUrl!, width: 80, height: 80, fit: BoxFit.cover, errorBuilder: (_, __, ___) => _DefaultAvatar(letter: nickname), ) : _DefaultAvatar(letter: nickname), ), Positioned( bottom: 2, right: 2, child: Container( width: 22, height: 22, decoration: BoxDecoration( color: cs.surface, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withAlpha(30), blurRadius: 4) ], ), child: Icon(Icons.edit_outlined, size: 13, color: cs.onSurface.withAlpha(180)), ), ), ], ), ), ), const SizedBox(height: 28), // ── 带单昵称 ────────────────────────────── _SectionLabel( label: AppLocalizations.of(context)!.tradingNickname), const SizedBox(height: 8), _InputField( controller: _nicknameCtrl, hint: AppLocalizations.of(context)!.enterNickname, maxLength: 10), const SizedBox(height: 20), // ── 个人签名 ────────────────────────────── _SectionLabel( label: AppLocalizations.of(context)!.personalBio), const SizedBox(height: 8), _SignatureField(controller: _descCtrl), const SizedBox(height: 20), // ── 标签 ───────────────────────────────── Row( children: [ _SectionLabel( label: AppLocalizations.of(context)!.tagsLabel), const Spacer(), Text( AppLocalizations.of(context)!.maxTagsHint( _maxTags.toString(), _selectedTagIds.length.toString()), style: TextStyle( color: cs.onSurface.withAlpha(120), fontSize: 12), ), ], ), const SizedBox(height: 10), _TagsWrap( tags: _allTags, selectedIds: _selectedTagIds, onToggle: _toggleTag, ), const SizedBox(height: 20), // ── 带单合约 ────────────────────────────── _SectionLabel( label: AppLocalizations.of(context)!.tradingContracts), const SizedBox(height: 10), _SymbolsWrap( allSymbols: _allSymbols, selected: _selectedSymbols, onToggle: _toggleSymbol, ), const SizedBox(height: 32), // ── 保存按钮 ────────────────────────────── SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: _saving ? null : _save, style: ElevatedButton.styleFrom( backgroundColor: AppColors.brand, foregroundColor: Colors.black, disabledBackgroundColor: AppColors.brand.withAlpha(80), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(26)), elevation: 0, ), child: _saving ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: Colors.black, strokeWidth: 2), ) : Text(AppLocalizations.of(context)!.saveLabel, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 16), // ── 取消资格 ───────────────────────────── Center( child: GestureDetector( onTap: _cancelTrader, child: Text( AppLocalizations.of(context)!.cancelTraderQualify, style: TextStyle( color: cs.onSurface.withAlpha(153), fontSize: 13, decoration: TextDecoration.underline, decorationColor: cs.onSurface.withAlpha(100), ), ), ), ), const SizedBox(height: 32), ], ), ), ); } } // ── 通用子组件 ──────────────────────────────────────────── class _SectionLabel extends StatelessWidget { const _SectionLabel({required this.label}); final String label; @override Widget build(BuildContext context) { return Text( label, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, fontSize: 14, fontWeight: FontWeight.w600, ), ); } } class _InputField extends StatelessWidget { const _InputField( {required this.controller, required this.hint, this.maxLength}); final TextEditingController controller; final String hint; final int? maxLength; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Container( decoration: BoxDecoration( border: Border.all(color: cs.outline.withAlpha(60)), borderRadius: BorderRadius.circular(8), ), child: TextField( controller: controller, maxLength: maxLength, maxLengthEnforcement: MaxLengthEnforcement.enforced, style: TextStyle(color: cs.onSurface, fontSize: 14), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 14), contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), border: InputBorder.none, counterText: '', ), ), ); } } class _SignatureField extends StatefulWidget { const _SignatureField({required this.controller}); final TextEditingController controller; @override State<_SignatureField> createState() => _SignatureFieldState(); } class _SignatureFieldState extends State<_SignatureField> { static const int _maxLen = 20; void _onControllerChanged() => setState(() {}); @override void initState() { super.initState(); widget.controller.addListener(_onControllerChanged); } @override void dispose() { widget.controller.removeListener(_onControllerChanged); super.dispose(); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final count = widget.controller.text.length; return Container( decoration: BoxDecoration( border: Border.all(color: cs.outline.withAlpha(60)), borderRadius: BorderRadius.circular(8), ), child: Column( children: [ TextField( controller: widget.controller, maxLines: 4, maxLength: _maxLen, style: TextStyle(color: cs.onSurface, fontSize: 14), decoration: InputDecoration( hintText: AppLocalizations.of(context)!.bioHint, hintStyle: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 13), contentPadding: const EdgeInsets.fromLTRB(14, 12, 14, 4), border: InputBorder.none, counterText: '', ), ), Padding( padding: const EdgeInsets.fromLTRB(0, 0, 12, 8), child: Align( alignment: Alignment.centerRight, child: Text( '$count/$_maxLen', style: TextStyle(color: cs.onSurface.withAlpha(100), fontSize: 12), ), ), ), ], ), ); } } class _TagsWrap extends StatelessWidget { const _TagsWrap( {required this.tags, required this.selectedIds, required this.onToggle}); final List> tags; final Set selectedIds; final void Function(String id) onToggle; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; // 如果 API 返回空则用默认标签 final displayTags = tags.isNotEmpty ? tags : [ {'id': '1', 'name': l10n.tagShortTerm}, {'id': '2', 'name': l10n.tagMidLong}, {'id': '3', 'name': l10n.tagConservative}, {'id': '4', 'name': l10n.tagAggressive}, {'id': '5', 'name': l10n.tagHighLeverage}, {'id': '6', 'name': l10n.tagLowLeverage}, ]; return Wrap( spacing: 10, runSpacing: 10, children: displayTags.map((tag) { final id = tag['id']?.toString() ?? ''; final name = tag['name']?.toString() ?? ''; final selected = selectedIds.contains(id); return GestureDetector( onTap: () => onToggle(id), child: Container( padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 8), decoration: BoxDecoration( color: selected ? AppColors.brand : Colors.transparent, border: Border.all( color: selected ? AppColors.brand : cs.outline.withAlpha(80)), borderRadius: BorderRadius.circular(20), ), child: Text( name, style: TextStyle( color: selected ? Colors.black : cs.onSurface, fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.normal, ), ), ), ); }).toList(), ); } } class _SymbolsWrap extends StatelessWidget { const _SymbolsWrap( {required this.allSymbols, required this.selected, required this.onToggle}); final List allSymbols; final Set selected; final void Function(String symbol) onToggle; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Wrap( spacing: 10, runSpacing: 10, children: allSymbols.map((symbol) { final isSelected = selected.contains(symbol); return GestureDetector( onTap: () => onToggle(symbol), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: isSelected ? AppColors.brand : Colors.transparent, border: Border.all( color: isSelected ? AppColors.brand : cs.outline.withAlpha(80), ), borderRadius: BorderRadius.circular(20), ), child: Text( symbol, style: TextStyle( color: isSelected ? Colors.black : cs.onSurface, fontSize: 13, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ), ); }).toList(), ); } } class _DefaultAvatar extends StatelessWidget { const _DefaultAvatar({required this.letter}); final String letter; @override Widget build(BuildContext context) { return Container( width: 80, height: 80, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF5B7BE8), Color(0xFF7B5EA7)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), ), child: Center( child: Text(letter, style: const TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.w700)), ), ); } }