refactor(account): 收敛账号页组件结构

This commit is contained in:
Zhou Weidong committed 2026-10-19 09:51:23 +08:00
1 parent da095115d4
commit 6fce24d9ec
5 files changed
+308 -247

No files matched your search

+87 -70
View File
@@ -69,84 +69,101 @@ class _AccountDeactivatePageState extends State<AccountDeactivatePage> {
}
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<void> _confirm() async {
final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>(
+93
View File
@@ -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<void>(MaterialPageRoute(builder: (_) => page)),
);
}
}
+50 -45
View File
@@ -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<AccountExportPage> {
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<Widget> children}) => Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
+76 -67
View File
@@ -156,84 +156,93 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
}
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<String>(
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<String>(
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(
+2 -65
View File
@@ -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<MyPage> {
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<MyPage> {
);
}
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<void>(MaterialPageRoute(builder: (_) => page));
}
Widget _library(
AppLocalizations l10n,
Duration position,