feat(mobile): UI-03 theme calibration and account pages (01-04)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
/// 语言选择 BottomSheet(UI-03 页面 04,登录页与设置页复用)。
|
||||
///
|
||||
/// 带拖柄;「Follow device / English / 中文」各以自身语言书写。临时
|
||||
/// 选中(点击仅切换视觉标记)+ **Done 提交**——滑动关闭/点外部取消
|
||||
/// 不改动语言偏好;提交后经 [LocaleSettings.setChoice] 统一刷新
|
||||
/// (MaterialApp 重建,IndexedStack 页签状态不受影响)。
|
||||
///
|
||||
/// 选项行为自绘单选圆点(触控行高 ≥48,选中态主色填充),Semantics
|
||||
/// 携带选中状态(读屏不依赖颜色)。
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../app/locale_controller.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
import '../tokens.dart';
|
||||
|
||||
/// 打开语言选择面板(唯一入口,两处调用共用同一行为)。
|
||||
Future<void> showLanguageSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) => const LanguageSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 语言选择面板内容:临时选中 + Done 提交。
|
||||
class LanguageSheet extends ConsumerStatefulWidget {
|
||||
const LanguageSheet({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<LanguageSheet> createState() => _LanguageSheetState();
|
||||
}
|
||||
|
||||
class _LanguageSheetState extends ConsumerState<LanguageSheet> {
|
||||
/// 临时选中(打开时刻快照;未点 Done 离开即丢弃)。
|
||||
late AppLocaleChoice _selected = ref.read(localeSettingsProvider);
|
||||
|
||||
/// 档位 → 展示名(语言名固定以自身语言书写,不随界面语言变化)。
|
||||
String _labelOf(AppLocalizations l10n, AppLocaleChoice choice) =>
|
||||
switch (choice) {
|
||||
AppLocaleChoice.system => l10n.localeFollowSystem,
|
||||
AppLocaleChoice.zh => l10n.localeChinese,
|
||||
AppLocaleChoice.en => l10n.localeEnglish,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
kSpace16, 0, kSpace16, kSpace16,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final choice in AppLocaleChoice.values)
|
||||
_LanguageOptionRow(
|
||||
label: _labelOf(l10n, choice),
|
||||
selected: choice == _selected,
|
||||
onTap: () => setState(() => _selected = choice),
|
||||
),
|
||||
const SizedBox(height: kSpace16),
|
||||
FilledButton(
|
||||
onPressed: _commit,
|
||||
child: Text(l10n.languageDone),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Done 提交:写入档位后关闭(滑动关闭/取消路径不经过这里)。
|
||||
Future<void> _commit() async {
|
||||
final navigator = Navigator.of(context);
|
||||
await ref
|
||||
.read(localeSettingsProvider.notifier)
|
||||
.setChoice(_selected);
|
||||
navigator.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个语言选项行:整行可点(≥48 触控高)+ 自绘单选圆点。
|
||||
class _LanguageOptionRow extends StatelessWidget {
|
||||
const _LanguageOptionRow({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Semantics(
|
||||
selected: selected,
|
||||
button: true,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(kRadiusInput),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 48),
|
||||
padding: const EdgeInsets.symmetric(vertical: kSpace12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: selected ? FontWeight.w600 : null,
|
||||
color: selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: kSpace12),
|
||||
_RadioDot(selected: selected),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 自绘单选圆点(20dp):未选 = 分隔色描边空心,选中 = 主色描边 +
|
||||
/// 主色内点;配 [Semantics.selected] 提供非视觉状态。
|
||||
class _RadioDot extends StatelessWidget {
|
||||
const _RadioDot({required this.selected});
|
||||
|
||||
final bool selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final color = selected ? scheme.primary : scheme.outline;
|
||||
return Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: color, width: 2),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: selected
|
||||
? Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: scheme.primary,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user