From 6fce24d9ec0327d0b83d015d01e44c3eaafc885b Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Mon, 19 Oct 2026 09:51:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(account):=20=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E9=A1=B5=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../account/account_deactivate_page.dart | 157 ++++++++++-------- lib/pages/account/account_entry_card.dart | 93 +++++++++++ lib/pages/account/account_export_page.dart | 95 ++++++----- lib/pages/account/account_profile_page.dart | 143 ++++++++-------- lib/pages/mine/mine_view.dart | 67 +------- 5 files changed, 308 insertions(+), 247 deletions(-) create mode 100644 lib/pages/account/account_entry_card.dart diff --git a/lib/pages/account/account_deactivate_page.dart b/lib/pages/account/account_deactivate_page.dart index 1f54928..61c01d4 100644 --- a/lib/pages/account/account_deactivate_page.dart +++ b/lib/pages/account/account_deactivate_page.dart @@ -69,84 +69,101 @@ class _AccountDeactivatePageState extends State { } Widget _body(AppLocalizations l10n) { - final errorColor = Theme.of(context).colorScheme.error; return ListView( padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: errorColor.withValues(alpha: 0.10), - borderRadius: BorderRadius.circular(14), - border: Border.all(color: errorColor.withValues(alpha: 0.35)), - ), - child: Text( - l10n.accountDeactivateWarning, - style: TextStyle(color: errorColor, height: 1.5), - ), - ), - if (_logic.scheduledAt != null) ...[ - const SizedBox(height: 14), - Container( - key: const Key('account-deactivate-scheduled'), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppColors.card, - borderRadius: BorderRadius.circular(14), - ), - child: Text( - l10n.accountScheduledFor( - formatLocalizedDateTime(context, _logic.scheduledAt!), - ), - ), - ), - ] else ...[ - const SizedBox(height: 16), - TextField( - key: const Key('account-deactivate-reason'), - controller: _reason, - maxLength: AccountDeactivateLogic.maxReasonLength, - maxLines: 4, - decoration: InputDecoration( - labelText: l10n.accountDeactivateReason, - hintText: l10n.accountDeactivateReasonHint, - ), - ), - if (_logic.error != null) - Text( - _message(l10n, _logic.error, l10n.accountDeactivateFailed), - style: TextStyle(color: errorColor, fontSize: 12), - ), - const SizedBox(height: 12), - FilledButton( - key: const Key('account-deactivate-submit'), - onPressed: _logic.requesting ? null : _confirm, - child: _logic.requesting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text(l10n.accountRequestDeactivate), - ), - ], + _warning(l10n), const SizedBox(height: 14), - OutlinedButton( - key: const Key('account-deactivate-cancel'), - onPressed: _logic.cancelling ? null : _cancel, - child: Text(l10n.accountCancelDeactivation), - ), - if (_logic.actionMessage != null) ...[ - const SizedBox(height: 8), - Text( - _message(l10n, _logic.actionMessage, l10n.accountCancelFailed), - style: TextStyle(color: errorColor, fontSize: 12), - ), - ], + if (_logic.scheduledAt != null) + _scheduledPanel(l10n) + else + _requestPanel(l10n), + const SizedBox(height: 14), + _cancelPanel(l10n), ], ); } + Widget _warning(AppLocalizations l10n) { + final errorColor = Theme.of(context).colorScheme.error; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: errorColor.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: errorColor.withValues(alpha: 0.35)), + ), + child: Text( + l10n.accountDeactivateWarning, + style: TextStyle(color: errorColor, height: 1.5), + ), + ); + } + + Widget _scheduledPanel(AppLocalizations l10n) => Container( + key: const Key('account-deactivate-scheduled'), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.card, + borderRadius: BorderRadius.circular(14), + ), + child: Text( + l10n.accountScheduledFor( + formatLocalizedDateTime(context, _logic.scheduledAt!), + ), + ), + ); + + Widget _requestPanel(AppLocalizations l10n) => Column( + children: [ + TextField( + key: const Key('account-deactivate-reason'), + controller: _reason, + maxLength: AccountDeactivateLogic.maxReasonLength, + maxLines: 4, + decoration: InputDecoration( + labelText: l10n.accountDeactivateReason, + hintText: l10n.accountDeactivateReasonHint, + ), + ), + if (_logic.error != null) + _errorText(_message(l10n, _logic.error, l10n.accountDeactivateFailed)), + const SizedBox(height: 12), + FilledButton( + key: const Key('account-deactivate-submit'), + onPressed: _logic.requesting ? null : _confirm, + child: _logic.requesting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text(l10n.accountRequestDeactivate), + ), + ], + ); + + Widget _cancelPanel(AppLocalizations l10n) => Column( + children: [ + OutlinedButton( + key: const Key('account-deactivate-cancel'), + onPressed: _logic.cancelling ? null : _cancel, + child: Text(l10n.accountCancelDeactivation), + ), + if (_logic.actionMessage != null) ...[ + const SizedBox(height: 8), + _errorText( + _message(l10n, _logic.actionMessage, l10n.accountCancelFailed), + ), + ], + ], + ); + + Widget _errorText(String value) => Text( + value, + style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 12), + ); + Future _confirm() async { final l10n = AppLocalizations.of(context)!; final confirmed = await showDialog( diff --git a/lib/pages/account/account_entry_card.dart b/lib/pages/account/account_entry_card.dart new file mode 100644 index 0000000..48f9b38 --- /dev/null +++ b/lib/pages/account/account_entry_card.dart @@ -0,0 +1,93 @@ +// Author: fruit.com.cn + +import 'package:flutter/material.dart'; + +import '../../core/theme/app_colors.dart'; +import '../../l10n/app_localizations.dart'; +import 'account_deactivate_page.dart'; +import 'account_devices_page.dart'; +import 'account_export_page.dart'; +import 'account_profile_page.dart'; + +/// 我的页账号入口卡;只在登录态由父页面展示。 +class AccountEntryCard extends StatelessWidget { + const AccountEntryCard({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Material( + key: const Key('mine-account-card'), + color: AppColors.card, + borderRadius: BorderRadius.circular(16), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + l10n.accountCenter, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + ), + ), + ), + ), + const Divider(height: 1, color: AppColors.outlineVariantDark), + _entry( + context, + key: const Key('mine-account-profile'), + icon: Icons.person_outline_rounded, + title: l10n.accountProfile, + page: const AccountProfilePage(), + ), + const Divider(height: 1, color: AppColors.outlineVariantDark), + _entry( + context, + key: const Key('mine-account-devices'), + icon: Icons.devices_other_rounded, + title: l10n.accountDevices, + page: const AccountDevicesPage(), + ), + const Divider(height: 1, color: AppColors.outlineVariantDark), + _entry( + context, + key: const Key('mine-account-export'), + icon: Icons.file_download_outlined, + title: l10n.accountDataExport, + page: const AccountExportPage(), + ), + const Divider(height: 1, color: AppColors.outlineVariantDark), + _entry( + context, + key: const Key('mine-account-deactivate'), + icon: Icons.person_remove_outlined, + title: l10n.accountDeactivate, + page: const AccountDeactivatePage(), + ), + ], + ), + ); + } + + Widget _entry( + BuildContext context, { + required Key key, + required IconData icon, + required String title, + required Widget page, + }) { + return ListTile( + key: key, + leading: Icon(icon, color: AppColors.blueSoft), + title: Text(title), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => page)), + ); + } +} diff --git a/lib/pages/account/account_export_page.dart b/lib/pages/account/account_export_page.dart index 4d4568a..56c0396 100644 --- a/lib/pages/account/account_export_page.dart +++ b/lib/pages/account/account_export_page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import '../../core/data/models/account_export.dart'; import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/error_text.dart'; @@ -87,57 +88,61 @@ class _AccountExportPageState extends State { key: const Key('account-export-content'), padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), children: [ - _card( - children: [ - Text( - data.profile.nickname, - style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700), - ), - const SizedBox(height: 4), - Text( - '${l10n.accountUid}: ${data.profile.uid}', - style: const TextStyle(color: AppColors.muted, fontSize: 12), - ), - ], - ), + _profileCard(data, l10n), const SizedBox(height: 12), - _card( - children: [ - Text( - l10n.accountCredentials, - style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), - ), - for (final credential in data.credentials) - ListTile( - contentPadding: EdgeInsets.zero, - leading: const Icon( - Icons.verified_user_outlined, - color: AppColors.blueSoft, - ), - title: Text(credential.masked), - subtitle: Text(credential.type), - trailing: Text( - credential.verified - ? l10n.accountVerified - : l10n.accountUnverified, - style: const TextStyle(color: AppColors.muted, fontSize: 11), - ), - ), - _summary(l10n.accountConsentRecords, '${data.consentCount}'), - _summary(l10n.accountDeviceCount, '${data.deviceCount}'), - ], - ), + _credentialsCard(data, l10n), const SizedBox(height: 16), - FilledButton.icon( - key: const Key('account-export-copy'), - onPressed: _copy, - icon: const Icon(Icons.copy_rounded), - label: Text(l10n.accountCopyJson), - ), + _copyButton(l10n), ], ); } + Widget _profileCard(AccountExport data, AppLocalizations l10n) => _card( + children: [ + Text( + data.profile.nickname, + style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w700), + ), + const SizedBox(height: 4), + Text( + '${l10n.accountUid}: ${data.profile.uid}', + style: const TextStyle(color: AppColors.muted, fontSize: 12), + ), + ], + ); + + Widget _credentialsCard(AccountExport data, AppLocalizations l10n) => _card( + children: [ + Text( + l10n.accountCredentials, + style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700), + ), + for (final credential in data.credentials) + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.verified_user_outlined, + color: AppColors.blueSoft, + ), + title: Text(credential.masked), + subtitle: Text(credential.type), + trailing: Text( + credential.verified ? l10n.accountVerified : l10n.accountUnverified, + style: const TextStyle(color: AppColors.muted, fontSize: 11), + ), + ), + _summary(l10n.accountConsentRecords, '${data.consentCount}'), + _summary(l10n.accountDeviceCount, '${data.deviceCount}'), + ], + ); + + Widget _copyButton(AppLocalizations l10n) => FilledButton.icon( + key: const Key('account-export-copy'), + onPressed: _copy, + icon: const Icon(Icons.copy_rounded), + label: Text(l10n.accountCopyJson), + ); + Widget _card({required List children}) => Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( diff --git a/lib/pages/account/account_profile_page.dart b/lib/pages/account/account_profile_page.dart index 674b89e..df3be83 100644 --- a/lib/pages/account/account_profile_page.dart +++ b/lib/pages/account/account_profile_page.dart @@ -156,84 +156,93 @@ class _AccountProfilePageState extends State { } Widget _editableForm(AppLocalizations l10n) { - final scheme = Theme.of(context).colorScheme; return _AccountCard( child: Column( children: [ - TextField( - key: const Key('account-nickname'), - controller: _nickname, - maxLength: AccountProfileLogic.maxNicknameLength, - decoration: InputDecoration( - labelText: l10n.accountNickname, - errorText: _logic.fieldError == null - ? null - : l10n.accountNicknameInvalid, - ), - ), + _nicknameField(l10n), const SizedBox(height: _fieldGap), - DropdownButtonFormField( - key: const Key('account-gender'), - 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); - }, - ), + _genderField(l10n), const SizedBox(height: _fieldGap), - OutlinedButton.icon( - key: const Key('account-birthday'), - onPressed: _pickBirthday, - icon: const Icon(Icons.cake_outlined), - label: Text(_birthday ?? l10n.accountSelectBirthday), - ), - const SizedBox(height: 6), - Align( - alignment: Alignment.centerLeft, - child: Text( - l10n.accountBirthdayHint, - style: const TextStyle(color: AppColors.muted, fontSize: 12), - ), - ), - if (_logic.error != null) ...[ - const SizedBox(height: 8), - Text( - errorText(l10n, _logic.error, l10n.accountProfileSaveFailed), - style: TextStyle(color: scheme.error, fontSize: 12), - ), - ], + _birthdayField(l10n), + if (_logic.error != null) _saveError(l10n), const SizedBox(height: 16), - 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), - ), - ), + _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'), + 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 _birthdayField(AppLocalizations l10n) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlinedButton.icon( + key: const Key('account-birthday'), + onPressed: _pickBirthday, + icon: const Icon(Icons.cake_outlined), + label: Text(_birthday ?? l10n.accountSelectBirthday), + ), + const SizedBox(height: 6), + Text( + l10n.accountBirthdayHint, + style: const TextStyle(color: AppColors.muted, fontSize: 12), + ), + ], + ); + + 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: 12, + ), + ), + ); + + 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) { return _AccountCard( child: Column( diff --git a/lib/pages/mine/mine_view.dart b/lib/pages/mine/mine_view.dart index 3b84bc0..9056524 100644 --- a/lib/pages/mine/mine_view.dart +++ b/lib/pages/mine/mine_view.dart @@ -24,10 +24,7 @@ import '../watch_records/watch_records_page.dart'; import '../favorites/favorites_page.dart'; import '../notices/notices_page.dart'; import '../downloads/downloads_page.dart'; -import '../account/account_profile_page.dart'; -import '../account/account_devices_page.dart'; -import '../account/account_export_page.dart'; -import '../account/account_deactivate_page.dart'; +import '../account/account_entry_card.dart'; class MyPage extends StatefulWidget { const MyPage({ @@ -206,7 +203,7 @@ class _MyPageState extends State { const SizedBox(height: 18), _wallet(l10n), const SizedBox(height: 14), - _accountCard(l10n), + const AccountEntryCard(), ], const SizedBox(height: 14), _menuCard(l10n), @@ -496,66 +493,6 @@ class _MyPageState extends State { ); } - Widget _accountCard(AppLocalizations l10n) { - return Material( - key: const Key('mine-account-card'), - color: AppColors.card, - borderRadius: BorderRadius.circular(16), - clipBehavior: Clip.antiAlias, - child: Column( - children: [ - ListTile( - key: const Key('mine-account-profile'), - leading: const Icon( - Icons.person_outline_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.accountProfile), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => _openAccount(const AccountProfilePage()), - ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - key: const Key('mine-account-devices'), - leading: const Icon( - Icons.devices_other_rounded, - color: AppColors.blueSoft, - ), - title: Text(l10n.accountDevices), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => _openAccount(const AccountDevicesPage()), - ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - key: const Key('mine-account-export'), - leading: const Icon( - Icons.file_download_outlined, - color: AppColors.blueSoft, - ), - title: Text(l10n.accountDataExport), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => _openAccount(const AccountExportPage()), - ), - const Divider(height: 1, color: AppColors.outlineVariantDark), - ListTile( - key: const Key('mine-account-deactivate'), - leading: const Icon( - Icons.person_remove_outlined, - color: AppColors.blueSoft, - ), - title: Text(l10n.accountDeactivate), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () => _openAccount(const AccountDeactivatePage()), - ), - ], - ), - ); - } - - void _openAccount(Widget page) { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); - } - Widget _library( AppLocalizations l10n, Duration position,