feat(app): 完善播放协议搜索与空状态

This commit is contained in:
Zhou Weidong committed 2026-10-23 12:38:47 +08:00
1 parent 671e0ce143
commit ef29fc5f15
85 files changed
+2976 -768

No files matched your search

@@ -0,0 +1,106 @@
// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../core/theme/app_colors.dart';
import '../../core/theme/app_dimens.dart';
import '../../l10n/app_localizations.dart';
import 'account_deactivate_logic.dart';
/// 注销原因:固定选项在标题下换行排列,补充说明留在输入框内。
class DeactivateReasonField extends StatelessWidget {
const DeactivateReasonField({
super.key,
required this.controller,
required this.onChanged,
});
final TextEditingController controller;
final VoidCallback onChanged;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final options = _options(l10n);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.accountDeactivateReason,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.listTitle,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimens.spaceSm),
Wrap(
spacing: AppDimens.spaceSm,
runSpacing: AppDimens.spaceSm,
children: [
for (var index = 0; index < options.length; index++)
_chip(options[index], index, options),
],
),
const SizedBox(height: AppDimens.spaceMd),
TextField(
key: const Key('account-deactivate-reason'),
controller: controller,
maxLength: AccountDeactivateLogic.maxReasonLength,
minLines: 3,
maxLines: 4,
textAlignVertical: TextAlignVertical.top,
onChanged: (_) => onChanged(),
decoration: InputDecoration(
hintText: l10n.accountDeactivateReasonHint,
floatingLabelBehavior: FloatingLabelBehavior.never,
alignLabelWithHint: true,
contentPadding: const EdgeInsets.all(AppDimens.cardPadding),
),
),
],
);
}
Widget _chip(_ReasonOption option, int index, List<_ReasonOption> options) {
final selected = option.custom
? _customSelected(options)
: controller.text == option.label;
return ChoiceChip(
key: Key('account-deactivate-reason-$index'),
label: Text(option.label),
selected: selected,
onSelected: (_) => _select(option),
);
}
bool _customSelected(List<_ReasonOption> options) {
final text = controller.text.trim();
if (text.isEmpty) return false;
return options
.where((item) => !item.custom)
.every((item) => item.label != text);
}
void _select(_ReasonOption option) {
controller.text = option.custom ? '' : option.label;
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
onChanged();
}
List<_ReasonOption> _options(AppLocalizations l10n) => [
_ReasonOption(l10n.accountDeactivateReasonUnused),
_ReasonOption(l10n.accountDeactivateReasonContent),
_ReasonOption(l10n.accountDeactivateReasonPrivacy),
_ReasonOption(l10n.accountDeactivateReasonOther, custom: true),
];
}
class _ReasonOption {
const _ReasonOption(this.label, {this.custom = false});
final String label;
final bool custom;
}
+99 -112
View File
@@ -10,132 +10,119 @@ import 'account_devices_page.dart';
import 'account_export_page.dart';
import 'account_profile_page.dart';
const _accountTitleWidth = 64.0;
/// 我的页账号入口卡;只在登录态由父页面展示。
/// 设置页账号入口,登录后按列表展示。
class AccountEntryCard extends StatelessWidget {
const AccountEntryCard({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final rows = <_AccountRow>[
_AccountRow(
key: const Key('mine-account-profile'),
icon: Icons.person_outline_rounded,
title: l10n.accountProfile,
page: const AccountProfilePage(),
),
_AccountRow(
key: const Key('mine-account-devices'),
icon: Icons.devices_other_rounded,
title: l10n.accountDevices,
page: const AccountDevicesPage(),
),
_AccountRow(
key: const Key('mine-account-export'),
icon: Icons.file_download_outlined,
title: l10n.accountDataExport,
page: const AccountExportPage(),
),
_AccountRow(
key: const Key('mine-account-deactivate'),
icon: Icons.person_remove_outlined,
title: l10n.accountDeactivate,
page: const AccountDeactivatePage(),
),
];
return Material(
key: const Key('mine-account-card'),
color: AppColors.card,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(AppDimens.cardRadius),
clipBehavior: Clip.antiAlias,
child: SizedBox(
height: AppDimens.accountCardHeight,
child: Row(
children: [
SizedBox(
width: _accountTitleWidth,
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
l10n.accountCenter,
style: const TextStyle(
fontSize: AppDimens.caption,
fontWeight: FontWeight.w800,
),
),
),
),
const VerticalDivider(
width: 1,
color: AppColors.outlineVariantDark,
),
Expanded(
child: Row(
children: [
_entry(
context,
key: const Key('mine-account-profile'),
icon: Icons.person_outline_rounded,
title: l10n.accountProfile,
page: const AccountProfilePage(),
divider: true,
),
_entry(
context,
key: const Key('mine-account-devices'),
icon: Icons.devices_other_rounded,
title: l10n.accountDevices,
page: const AccountDevicesPage(),
divider: true,
),
_entry(
context,
key: const Key('mine-account-export'),
icon: Icons.file_download_outlined,
title: l10n.accountDataExport,
page: const AccountExportPage(),
divider: true,
),
_entry(
context,
key: const Key('mine-account-deactivate'),
icon: Icons.person_remove_outlined,
title: l10n.accountDeactivate,
page: const AccountDeactivatePage(),
divider: false,
),
],
),
),
],
),
),
);
}
Widget _entry(
BuildContext context, {
required Key key,
required IconData icon,
required String title,
required Widget page,
required bool divider,
}) {
return Expanded(
child: Row(
child: Column(
children: [
Expanded(
child: InkWell(
key: key,
onTap: () => Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => page)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: AppColors.blueSoft,
size: AppDimens.accountIcon,
),
const SizedBox(height: 2),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: AppDimens.accountLabel,
height: 1.15,
),
),
],
for (var index = 0; index < rows.length; index++) ...[
if (index > 0)
const Divider(
height: AppDimens.mineGridDividerWidth,
thickness: AppDimens.mineGridDividerWidth,
color: AppColors.outlineVariantDark,
),
),
),
if (divider)
const VerticalDivider(
width: 1,
color: AppColors.outlineVariantDark,
),
_AccountListTile(row: rows[index]),
],
],
),
);
}
}
class _AccountRow {
const _AccountRow({
required this.key,
required this.icon,
required this.title,
required this.page,
});
final Key key;
final IconData icon;
final String title;
final Widget page;
}
class _AccountListTile extends StatelessWidget {
const _AccountListTile({required this.row});
final _AccountRow row;
@override
Widget build(BuildContext context) {
return InkWell(
key: row.key,
onTap: () => Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => row.page)),
child: SizedBox(
height: AppDimens.mineMoreRowHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: AppDimens.spaceLg),
child: Row(
children: [
Icon(
row.icon,
color: AppColors.blueSoft,
size: AppDimens.accountIcon,
),
const SizedBox(width: AppDimens.spaceMd),
Expanded(
child: Text(
row.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: AppDimens.listTitle,
),
),
),
const Icon(
Icons.chevron_right_rounded,
color: AppColors.textSecondary,
size: AppDimens.accountIcon,
),
],
),
),
),
);
}
}