// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/account_profile.dart'; import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/account_display.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../../core/widgets/remote_image.dart'; import '../../l10n/app_localizations.dart'; import 'account_bindings.dart'; import 'account_profile_logic.dart'; const _avatarSize = 64.0; const _fieldGap = 14.0; /// 个人资料编辑页。 class AccountProfilePage extends StatefulWidget { const AccountProfilePage({super.key}); @override State createState() => _AccountProfilePageState(); } class _AccountProfilePageState extends State { late final AccountProfileLogic _logic; final _nickname = TextEditingController(); var _gender = 'unknown'; bool _formReady = false; @override void initState() { super.initState(); _logic = AccountProfileBinding().create( context.read(), context.read(), )..addListener(_syncProfile); unawaited(_logic.load()); } void _syncProfile() { final profile = _logic.profile; if (profile == null || _formReady) return; _nickname.text = profile.nickname; _gender = _knownGender(profile.gender); _formReady = true; } @override void dispose() { _logic ..removeListener(_syncProfile) ..dispose(); _nickname.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( data: AppTheme.dark(), child: PlainPageScaffold( pageKey: const Key('account-profile-page'), backKey: const Key('account-profile-back'), title: l10n.accountProfile, child: signedIn ? ListenableBuilder( listenable: _logic, builder: (context, _) => _body(l10n), ) : const AuthGatePanel(), ), ); } Widget _body(AppLocalizations l10n) { if (_logic.loading && _logic.profile == null) { return const Center(child: CircularProgressIndicator()); } final profile = _logic.profile; if (profile == null) { return EmptyState( key: const Key('account-profile-error'), icon: Icons.cloud_off_rounded, title: errorText(l10n, _logic.error, l10n.accountProfileLoadFailed), actionLabel: l10n.retry, actionKey: const Key('account-profile-retry'), onAction: _logic.load, ); } return ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), children: [ _identity(profile, l10n), const SizedBox(height: 16), _editableForm(l10n), const SizedBox(height: 18), _readOnlyCard(profile, l10n), ], ); } Widget _identity(AccountProfile profile, AppLocalizations l10n) { return Row( children: [ ClipOval( child: SizedBox( width: _avatarSize, height: _avatarSize, child: profile.avatarUrl.isEmpty ? const ColoredBox( color: AppColors.raised, child: Icon(Icons.person_rounded, size: 34), ) : RemoteImage(profile.avatarUrl), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( profile.nickname, style: const TextStyle( fontSize: AppDimens.pageTitle, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( '${l10n.accountUid}: ${profile.uid}', style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.caption, ), ), ], ), ), ], ); } Widget _editableForm(AppLocalizations l10n) { return _AccountCard( child: Column( children: [ _nicknameField(l10n), const SizedBox(height: _fieldGap), _genderField(l10n), if (_logic.error != null) _saveError(l10n), const SizedBox(height: 16), _saveButton(l10n), ], ), ); } Widget _nicknameField(AppLocalizations l10n) => TextField( key: const Key('account-nickname'), controller: _nickname, maxLength: AccountProfileLogic.maxNicknameLength, decoration: InputDecoration( labelText: l10n.accountNickname, errorText: _logic.fieldError == null ? null : l10n.accountNicknameInvalid, ), ); Widget _genderField(AppLocalizations l10n) => DropdownButtonFormField( key: const Key('account-gender'), isExpanded: true, initialValue: _gender, decoration: InputDecoration(labelText: l10n.accountGender), items: [ DropdownMenuItem(value: 'male', child: Text(l10n.accountMale)), DropdownMenuItem(value: 'female', child: Text(l10n.accountFemale)), DropdownMenuItem( value: 'unknown', child: Text(l10n.accountUnknownGender), ), ], onChanged: (value) { if (value != null) setState(() => _gender = value); }, ); Widget _saveError(AppLocalizations l10n) => Padding( padding: const EdgeInsets.only(top: 8), child: Text( errorText(l10n, _logic.error, l10n.accountProfileSaveFailed), style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: AppDimens.caption, ), ), ); Widget _saveButton(AppLocalizations l10n) => SizedBox( width: double.infinity, height: 52, child: FilledButton( key: const Key('account-profile-save'), onPressed: _logic.saving ? null : _save, child: _logic.saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(l10n.save), ), ); Widget _readOnlyCard(AccountProfile profile, AppLocalizations l10n) { final localeName = Localizations.localeOf(context).languageCode; return _AccountCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.accountReadOnlyInfo, style: const TextStyle( fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 10), _info(l10n.accountMaskedAccount, profile.maskedTarget), _info( l10n.accountCountry, countryName(profile.countryCode, localeName), ), _info(l10n.accountCurrency, profile.currency), _info( l10n.accountStatus, accountStatusText(profile.status, localeName), ), _info( l10n.accountLanguage, languageName(profile.language, localeName), ), _info( l10n.accountTimezone, timezoneName(profile.timezone, localeName), ), // region 与 country 后端写的是同一个值,重复时不再单列一行 if (profile.regionCode.isNotEmpty && profile.regionCode != profile.countryCode) _info( l10n.accountRegion, countryName(profile.regionCode, localeName), ), _info( l10n.accountVipLevel, vipLevelText(profile.vipLevel, localeName), ), if (profile.createdAt != null) _info( l10n.accountCreatedAt, formatLocalizedDateTime(context, profile.createdAt!), ), if (profile.lastLoginAt != null) _info( l10n.accountLastLogin, formatLocalizedDateTime(context, profile.lastLoginAt!), ), ], ), ); } Widget _info(String label, String value) => Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.caption, ), ), ), Flexible( child: Text( value.isEmpty ? '—' : value, textAlign: TextAlign.right, style: const TextStyle(fontSize: AppDimens.body), ), ), ], ), ); Future _save() async { final ok = await _logic.save(nickname: _nickname.text, gender: _gender); if (!ok || !mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context)!.accountSaved)), ); } String _knownGender(String value) => const {'male', 'female', 'unknown'}.contains(value) ? value : 'unknown'; } class _AccountCard extends StatelessWidget { const _AccountCard({required this.child}); final Widget child; @override Widget build(BuildContext context) => Container( padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.outlineVariantDark), ), child: child, ); }