profile_screen.dart 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:cached_network_image/cached_network_image.dart';
  4. import 'package:flutter_riverpod/flutter_riverpod.dart';
  5. import 'package:go_router/go_router.dart';
  6. import '../../../core/config/app_config.dart';
  7. import '../../../core/l10n/app_localizations.dart';
  8. import '../../../core/navigation/broker_navigation.dart';
  9. import '../../../core/theme/app_colors.dart';
  10. import '../../../providers/app_provider.dart';
  11. import '../../../providers/app_version_provider.dart';
  12. import '../../../providers/currency_provider.dart';
  13. import '../../../providers/customer_service_provider.dart';
  14. import '../../../providers/profile_provider.dart';
  15. import '../../widgets/common/update_dialog.dart';
  16. import '../../../core/utils/top_toast.dart';
  17. class ProfileScreen extends ConsumerWidget {
  18. const ProfileScreen({super.key});
  19. @override
  20. Widget build(BuildContext context, WidgetRef ref) {
  21. final l10n = AppLocalizations.of(context)!;
  22. final state = ref.watch(profileProvider);
  23. final isLoggedIn = state.user.isLoggedIn;
  24. return Scaffold(
  25. appBar: AppBar(
  26. title: Text(
  27. l10n.profile,
  28. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  29. ),
  30. centerTitle: true,
  31. ),
  32. body: ListView(
  33. children: [
  34. if (isLoggedIn) _UserCard(state: state) else const _GuestCard(),
  35. const SizedBox(height: 12),
  36. _SectionCard(
  37. title: l10n.quickFunctions,
  38. child: _QuickFunctions(l10n: l10n),
  39. ),
  40. const SizedBox(height: 12),
  41. _AppSettings(version: state.appVersion, isLoggedIn: isLoggedIn),
  42. if (isLoggedIn) ...[
  43. const SizedBox(height: 24),
  44. _LogoutButton(
  45. onTap: () => _showLogoutDialog(context, ref),
  46. ),
  47. ],
  48. const SizedBox(height: 40),
  49. ],
  50. ),
  51. );
  52. }
  53. void _showLogoutDialog(BuildContext context, WidgetRef ref) {
  54. final l10n = AppLocalizations.of(context)!;
  55. final cs = Theme.of(context).colorScheme;
  56. showDialog(
  57. context: context,
  58. builder: (ctx) => AlertDialog(
  59. backgroundColor: cs.surface,
  60. title: Text(
  61. l10n.logoutTitle,
  62. style: TextStyle(color: cs.onSurface),
  63. ),
  64. content: Text(
  65. l10n.logoutConfirm,
  66. style: TextStyle(color: cs.onSurface.withAlpha(153)),
  67. ),
  68. actions: [
  69. TextButton(
  70. onPressed: () => Navigator.of(ctx).pop(),
  71. child: Text(l10n.cancel,
  72. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  73. ),
  74. TextButton(
  75. onPressed: () {
  76. Navigator.of(ctx).pop();
  77. ref.read(profileProvider.notifier).logout();
  78. },
  79. child: Text(l10n.confirm,
  80. style: const TextStyle(color: AppColors.brand)),
  81. ),
  82. ],
  83. ),
  84. );
  85. }
  86. }
  87. // ── 未登录:访客卡片 ──────────────────────────────────────────
  88. class _GuestCard extends StatelessWidget {
  89. const _GuestCard();
  90. @override
  91. Widget build(BuildContext context) {
  92. final l10n = AppLocalizations.of(context)!;
  93. final cs = Theme.of(context).colorScheme;
  94. final isDark = Theme.of(context).brightness == Brightness.dark;
  95. return Container(
  96. margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  97. padding: const EdgeInsets.all(16),
  98. decoration: BoxDecoration(
  99. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  100. borderRadius: BorderRadius.circular(12),
  101. ),
  102. child: Row(
  103. children: [
  104. Container(
  105. width: 56,
  106. height: 56,
  107. decoration: BoxDecoration(
  108. color: cs.outline.withAlpha(40),
  109. shape: BoxShape.circle,
  110. ),
  111. child: Icon(
  112. Icons.person_outline,
  113. color: cs.onSurface.withAlpha(153),
  114. size: 32,
  115. ),
  116. ),
  117. const SizedBox(width: 14),
  118. Expanded(
  119. child: Column(
  120. crossAxisAlignment: CrossAxisAlignment.start,
  121. children: [
  122. Text(
  123. l10n.guestGreeting,
  124. style: TextStyle(
  125. color: cs.onSurface,
  126. fontSize: 16,
  127. fontWeight: FontWeight.w600,
  128. ),
  129. ),
  130. const SizedBox(height: 10),
  131. SizedBox(
  132. height: 34,
  133. child: ElevatedButton(
  134. onPressed: () => context.push('/login'),
  135. style: ElevatedButton.styleFrom(
  136. backgroundColor: AppColors.brand,
  137. foregroundColor: Colors.black,
  138. shape: RoundedRectangleBorder(
  139. borderRadius: BorderRadius.circular(17),
  140. ),
  141. elevation: 0,
  142. padding: const EdgeInsets.symmetric(horizontal: 20),
  143. minimumSize: Size.zero,
  144. ),
  145. child: Text(
  146. l10n.loginRegister,
  147. style: const TextStyle(
  148. fontSize: 13,
  149. fontWeight: FontWeight.w600,
  150. ),
  151. ),
  152. ),
  153. ),
  154. ],
  155. ),
  156. ),
  157. ],
  158. ),
  159. );
  160. }
  161. }
  162. // ── 已登录:用户信息卡片 ─────────────────────────────────────────
  163. class _UserCard extends StatelessWidget {
  164. const _UserCard({required this.state});
  165. final ProfileState state;
  166. @override
  167. Widget build(BuildContext context) {
  168. final l10n = AppLocalizations.of(context)!;
  169. final user = state.user;
  170. final cs = Theme.of(context).colorScheme;
  171. final isDark = Theme.of(context).brightness == Brightness.dark;
  172. return Container(
  173. margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  174. padding: const EdgeInsets.all(16),
  175. decoration: BoxDecoration(
  176. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  177. borderRadius: BorderRadius.circular(12),
  178. ),
  179. child: Row(
  180. children: [
  181. Container(
  182. width: 56,
  183. height: 56,
  184. clipBehavior: Clip.antiAlias,
  185. decoration: const BoxDecoration(
  186. color: AppColors.brand,
  187. shape: BoxShape.circle,
  188. ),
  189. child: (user.avatarUrl != null && user.avatarUrl!.isNotEmpty)
  190. ? CachedNetworkImage(
  191. imageUrl: user.avatarUrl!,
  192. fit: BoxFit.cover,
  193. width: 56,
  194. height: 56,
  195. fadeInDuration: Duration.zero,
  196. errorWidget: (_, __, ___) => Center(
  197. child: Text(
  198. user.avatarLetter,
  199. style: const TextStyle(
  200. color: Colors.white,
  201. fontSize: 24,
  202. fontWeight: FontWeight.w700,
  203. ),
  204. ),
  205. ),
  206. )
  207. : Center(
  208. child: Text(
  209. user.avatarLetter,
  210. style: const TextStyle(
  211. color: Colors.white,
  212. fontSize: 24,
  213. fontWeight: FontWeight.w700,
  214. ),
  215. ),
  216. ),
  217. ),
  218. const SizedBox(width: 14),
  219. Expanded(
  220. child: Column(
  221. crossAxisAlignment: CrossAxisAlignment.start,
  222. children: [
  223. Text(
  224. user.email,
  225. style: TextStyle(
  226. color: cs.onSurface,
  227. fontSize: 16,
  228. fontWeight: FontWeight.w600,
  229. ),
  230. ),
  231. const SizedBox(height: 4),
  232. Row(
  233. children: [
  234. Text(
  235. 'UID: ${user.uid}',
  236. style: TextStyle(
  237. color: cs.onSurface.withAlpha(153),
  238. fontSize: 13,
  239. ),
  240. ),
  241. const SizedBox(width: 6),
  242. GestureDetector(
  243. onTap: () {
  244. Clipboard.setData(ClipboardData(text: user.uid));
  245. showTopToast(context,
  246. message: l10n.uidCopied,
  247. backgroundColor: AppColors.rise);
  248. },
  249. child: Icon(
  250. Icons.copy,
  251. size: 14,
  252. color: cs.onSurface.withAlpha(153),
  253. ),
  254. ),
  255. ],
  256. ),
  257. ],
  258. ),
  259. ),
  260. ],
  261. ),
  262. );
  263. }
  264. }
  265. // ── 卡片容器 ─────────────────────────────────────────────────
  266. class _SectionCard extends StatelessWidget {
  267. const _SectionCard({required this.title, required this.child});
  268. final String title;
  269. final Widget child;
  270. @override
  271. Widget build(BuildContext context) {
  272. final cs = Theme.of(context).colorScheme;
  273. final isDark = Theme.of(context).brightness == Brightness.dark;
  274. return Container(
  275. margin: const EdgeInsets.symmetric(horizontal: 16),
  276. decoration: BoxDecoration(
  277. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  278. borderRadius: BorderRadius.circular(12),
  279. ),
  280. child: Column(
  281. crossAxisAlignment: CrossAxisAlignment.start,
  282. children: [
  283. Padding(
  284. padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
  285. child: Text(
  286. title,
  287. style: TextStyle(
  288. color: cs.onSurface.withAlpha(153),
  289. fontSize: 13,
  290. ),
  291. ),
  292. ),
  293. child,
  294. ],
  295. ),
  296. );
  297. }
  298. }
  299. // ── 常用功能 网格 ─────────────────────────────────────────────
  300. class _QuickFunctions extends StatelessWidget {
  301. final AppLocalizations l10n;
  302. const _QuickFunctions({required this.l10n});
  303. @override
  304. Widget build(BuildContext context) {
  305. final items = [
  306. (
  307. icon: Icons.language,
  308. label: l10n.languageSwitch,
  309. route: '/user/language'
  310. ),
  311. (
  312. icon: Icons.shield_outlined,
  313. label: l10n.security,
  314. route: '/user/security'
  315. ),
  316. (
  317. icon: Icons.notifications_outlined,
  318. label: l10n.announcements,
  319. route: '/user/messages'
  320. ),
  321. (icon: Icons.help_outline, label: l10n.helpCenter, route: '/user/help'),
  322. ];
  323. return Padding(
  324. padding: const EdgeInsets.fromLTRB(8, 0, 8, 16),
  325. child: Row(
  326. children: items
  327. .map((item) => Expanded(
  328. child: _QuickItemWidget(
  329. icon: item.icon, label: item.label, route: item.route)))
  330. .toList(),
  331. ),
  332. );
  333. }
  334. }
  335. class _QuickItemWidget extends StatelessWidget {
  336. const _QuickItemWidget(
  337. {required this.icon, required this.label, required this.route});
  338. final IconData icon;
  339. final String label;
  340. final String route;
  341. @override
  342. Widget build(BuildContext context) {
  343. final cs = Theme.of(context).colorScheme;
  344. return GestureDetector(
  345. onTap: () => context.push(route),
  346. child: Column(
  347. children: [
  348. Container(
  349. width: 52,
  350. height: 52,
  351. decoration: BoxDecoration(
  352. color: cs.outline.withAlpha(30),
  353. borderRadius: BorderRadius.circular(14),
  354. ),
  355. child: Icon(icon, color: cs.onSurface, size: 24),
  356. ),
  357. const SizedBox(height: 6),
  358. Text(
  359. label,
  360. maxLines: 1,
  361. overflow: TextOverflow.ellipsis,
  362. textAlign: TextAlign.center,
  363. style: TextStyle(
  364. color: cs.onSurface.withAlpha(153),
  365. fontSize: 12,
  366. ),
  367. ),
  368. ],
  369. ),
  370. );
  371. }
  372. }
  373. // ── APP 设置列表 ──────────────────────────────────────────────
  374. class _AppSettings extends ConsumerWidget {
  375. const _AppSettings({required this.version, required this.isLoggedIn});
  376. final String version;
  377. final bool isLoggedIn;
  378. @override
  379. Widget build(BuildContext context, WidgetRef ref) {
  380. final l10n = AppLocalizations.of(context)!;
  381. final cs = Theme.of(context).colorScheme;
  382. final isDark = Theme.of(context).brightness == Brightness.dark;
  383. final themeMode = ref.watch(themeProvider);
  384. final currencyState = ref.watch(currencyProvider);
  385. final selectedCurrency = currencyState.selected;
  386. return Container(
  387. margin: const EdgeInsets.symmetric(horizontal: 16),
  388. decoration: BoxDecoration(
  389. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  390. borderRadius: BorderRadius.circular(12),
  391. ),
  392. child: Column(
  393. children: [
  394. if (isLoggedIn) ...[
  395. _SettingRow(
  396. label: l10n.broker,
  397. onTap: () => openBrokerEntry(context, ref),
  398. showTopRadius: true,
  399. ),
  400. Divider(
  401. height: 1,
  402. indent: 16,
  403. endIndent: 0,
  404. color: cs.outline.withAlpha(40)),
  405. ],
  406. _ThemeModeRow(themeMode: themeMode, ref: ref, l10n: l10n),
  407. Divider(
  408. height: 1,
  409. indent: 16,
  410. endIndent: 0,
  411. color: cs.outline.withAlpha(40)),
  412. _SettingRow(
  413. label: l10n.currency,
  414. trailing: Text(
  415. selectedCurrency != null
  416. ? '${selectedCurrency.currency} ${selectedCurrency.symbol}'
  417. : 'USD \$',
  418. style:
  419. TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
  420. ),
  421. onTap: () => _showCurrencyPicker(context, ref, currencyState, l10n),
  422. ),
  423. Divider(
  424. height: 1,
  425. indent: 16,
  426. endIndent: 0,
  427. color: cs.outline.withAlpha(40)),
  428. _SettingRow(
  429. label: l10n.stakingTitle,
  430. onTap: () => context.push('/finance/ido'),
  431. ),
  432. Divider(
  433. height: 1,
  434. indent: 16,
  435. endIndent: 0,
  436. color: cs.outline.withAlpha(40)),
  437. _SettingRow(
  438. label: l10n.serviceRoute,
  439. onTap: () => context.push('/user/service-route'),
  440. showTopRadius: false,
  441. ),
  442. Divider(
  443. height: 1,
  444. indent: 16,
  445. endIndent: 0,
  446. color: cs.outline.withAlpha(40)),
  447. _SettingRow(
  448. label: l10n.currentVersion,
  449. trailing: Text(
  450. version,
  451. style:
  452. TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13),
  453. ),
  454. onTap: () => _checkUpdate(context, ref, l10n),
  455. ),
  456. Divider(
  457. height: 1,
  458. indent: 16,
  459. endIndent: 0,
  460. color: cs.outline.withAlpha(40)),
  461. _SettingRow(
  462. label: l10n.clearCache,
  463. onTap: () => _confirmClearCache(context, ref, l10n),
  464. showBottomRadius: !AppConfig.customerServiceEnabled,
  465. ),
  466. if (AppConfig.customerServiceEnabled) ...[
  467. Column(children: [
  468. Divider(
  469. height: 1,
  470. indent: 16,
  471. endIndent: 0,
  472. color: cs.outline.withAlpha(40)),
  473. _SettingRow(
  474. label: l10n.customerService,
  475. onTap: () => openCustomerService(context, ref),
  476. showBottomRadius: true,
  477. ),
  478. ]),
  479. ],
  480. ],
  481. ),
  482. );
  483. }
  484. Future<void> _checkUpdate(
  485. BuildContext context, WidgetRef ref, AppLocalizations l10n) async {
  486. ref.invalidate(appVersionProvider);
  487. final result = await ref.read(appVersionProvider.future);
  488. if (!context.mounted) return;
  489. if (result != null && result.hasUpdate) {
  490. UpdateDialog.show(context, result);
  491. } else {
  492. showTopToast(context,
  493. message: l10n.alreadyLatestVersion, backgroundColor: AppColors.rise);
  494. }
  495. }
  496. void _showCurrencyPicker(BuildContext context, WidgetRef ref,
  497. CurrencyState currencyState, AppLocalizations l10n) {
  498. final cs = Theme.of(context).colorScheme;
  499. showModalBottomSheet<void>(
  500. context: context,
  501. useRootNavigator: true,
  502. isScrollControlled: true,
  503. backgroundColor: cs.surface,
  504. shape: const RoundedRectangleBorder(
  505. borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
  506. ),
  507. builder: (ctx) => Consumer(
  508. builder: (ctx, innerRef, _) {
  509. final live = innerRef.watch(currencyProvider);
  510. return SafeArea(
  511. child: ConstrainedBox(
  512. constraints: BoxConstraints(
  513. maxHeight: MediaQuery.of(ctx).size.height * 0.75,
  514. ),
  515. child: Column(
  516. mainAxisSize: MainAxisSize.min,
  517. children: [
  518. const SizedBox(height: 12),
  519. Container(
  520. width: 36,
  521. height: 4,
  522. decoration: BoxDecoration(
  523. color: cs.outline.withAlpha(80),
  524. borderRadius: BorderRadius.circular(2),
  525. ),
  526. ),
  527. const SizedBox(height: 16),
  528. Padding(
  529. padding: const EdgeInsets.symmetric(horizontal: 16),
  530. child: Text(
  531. l10n.selectCurrency,
  532. style: TextStyle(
  533. color: cs.onSurface,
  534. fontSize: 16,
  535. fontWeight: FontWeight.w600),
  536. ),
  537. ),
  538. const SizedBox(height: 12),
  539. if (live.isLoading)
  540. Padding(
  541. padding: const EdgeInsets.symmetric(vertical: 24),
  542. child: CircularProgressIndicator(
  543. strokeWidth: 2, color: cs.onSurface),
  544. )
  545. else if (live.rates.isEmpty)
  546. Padding(
  547. padding: const EdgeInsets.symmetric(vertical: 24),
  548. child: Text(l10n.noCurrencyAvailable,
  549. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  550. )
  551. else
  552. Flexible(
  553. child: SingleChildScrollView(
  554. child: Column(
  555. children: live.rates.map((rate) {
  556. final isSelected =
  557. rate.currency == live.selectedCode;
  558. return GestureDetector(
  559. behavior: HitTestBehavior.opaque,
  560. onTap: () {
  561. innerRef
  562. .read(currencyProvider.notifier)
  563. .selectCurrency(rate);
  564. Navigator.of(ctx).pop();
  565. },
  566. child: Padding(
  567. padding: const EdgeInsets.symmetric(
  568. horizontal: 16, vertical: 14),
  569. child: Row(
  570. children: [
  571. Text(
  572. rate.displayName,
  573. style: TextStyle(
  574. color: cs.onSurface,
  575. fontSize: 15,
  576. fontWeight: isSelected
  577. ? FontWeight.w600
  578. : FontWeight.w400,
  579. ),
  580. ),
  581. const Spacer(),
  582. if (isSelected)
  583. const Icon(Icons.check,
  584. color: AppColors.brand, size: 20),
  585. ],
  586. ),
  587. ),
  588. );
  589. }).toList(),
  590. ),
  591. ),
  592. ),
  593. const SizedBox(height: 8),
  594. ],
  595. ),
  596. ),
  597. );
  598. },
  599. ),
  600. );
  601. }
  602. Future<void> _confirmClearCache(
  603. BuildContext context, WidgetRef ref, AppLocalizations l10n) async {
  604. final cs = Theme.of(context).colorScheme;
  605. final confirmed = await showDialog<bool>(
  606. context: context,
  607. builder: (ctx) => AlertDialog(
  608. backgroundColor: cs.surface,
  609. title: Text(l10n.tips, style: TextStyle(color: cs.onSurface)),
  610. content: Text(
  611. l10n.confirmClearCache,
  612. style: TextStyle(color: cs.onSurface.withAlpha(153)),
  613. ),
  614. actions: [
  615. TextButton(
  616. onPressed: () => Navigator.of(ctx).pop(false),
  617. child: Text(l10n.cancel,
  618. style: TextStyle(color: cs.onSurface.withAlpha(153))),
  619. ),
  620. TextButton(
  621. onPressed: () => Navigator.of(ctx).pop(true),
  622. child: Text(l10n.confirm,
  623. style: const TextStyle(color: AppColors.brand)),
  624. ),
  625. ],
  626. ),
  627. );
  628. if (confirmed != true || !context.mounted) return;
  629. await ref.read(profileProvider.notifier).clearCache();
  630. if (!context.mounted) return;
  631. showTopToast(context,
  632. message: l10n.cacheCleared, duration: const Duration(seconds: 1));
  633. }
  634. }
  635. class _ThemeModeRow extends StatelessWidget {
  636. const _ThemeModeRow(
  637. {required this.themeMode, required this.ref, required this.l10n});
  638. final ThemeMode themeMode;
  639. final WidgetRef ref;
  640. final AppLocalizations l10n;
  641. @override
  642. Widget build(BuildContext context) {
  643. final cs = Theme.of(context).colorScheme;
  644. return Padding(
  645. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
  646. child: Row(
  647. children: [
  648. Text(
  649. l10n.themeColor,
  650. style: TextStyle(color: cs.onSurface, fontSize: 14),
  651. ),
  652. const SizedBox(width: 16),
  653. Expanded(
  654. child: Row(
  655. mainAxisAlignment: MainAxisAlignment.end,
  656. children: [
  657. Flexible(
  658. child: _ThemeChip(
  659. label: l10n.lightMode,
  660. selected: themeMode == ThemeMode.light,
  661. onTap: () => ref
  662. .read(themeProvider.notifier)
  663. .setTheme(ThemeMode.light),
  664. ),
  665. ),
  666. const SizedBox(width: 6),
  667. Flexible(
  668. child: _ThemeChip(
  669. label: l10n.darkMode,
  670. selected: themeMode == ThemeMode.dark,
  671. onTap: () => ref
  672. .read(themeProvider.notifier)
  673. .setTheme(ThemeMode.dark),
  674. ),
  675. ),
  676. ],
  677. ),
  678. ),
  679. ],
  680. ),
  681. );
  682. }
  683. }
  684. class _ThemeChip extends StatelessWidget {
  685. const _ThemeChip(
  686. {required this.label, required this.selected, required this.onTap});
  687. final String label;
  688. final bool selected;
  689. final VoidCallback onTap;
  690. @override
  691. Widget build(BuildContext context) {
  692. final cs = Theme.of(context).colorScheme;
  693. return GestureDetector(
  694. onTap: onTap,
  695. child: Container(
  696. padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
  697. decoration: BoxDecoration(
  698. color: selected ? AppColors.brand : cs.outline.withAlpha(30),
  699. borderRadius: BorderRadius.circular(16),
  700. ),
  701. child: Text(
  702. label,
  703. maxLines: 1,
  704. overflow: TextOverflow.ellipsis,
  705. style: TextStyle(
  706. color: selected ? Colors.black : cs.onSurface.withAlpha(153),
  707. fontSize: 12,
  708. fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
  709. ),
  710. ),
  711. ),
  712. );
  713. }
  714. }
  715. class _SettingRow extends StatelessWidget {
  716. const _SettingRow({
  717. required this.label,
  718. required this.onTap,
  719. this.trailing,
  720. this.showTopRadius = false,
  721. this.showBottomRadius = false,
  722. });
  723. final String label;
  724. final VoidCallback onTap;
  725. final Widget? trailing;
  726. final bool showTopRadius;
  727. final bool showBottomRadius;
  728. @override
  729. Widget build(BuildContext context) {
  730. final cs = Theme.of(context).colorScheme;
  731. return InkWell(
  732. onTap: onTap,
  733. borderRadius: BorderRadius.vertical(
  734. top: showTopRadius ? const Radius.circular(12) : Radius.zero,
  735. bottom: showBottomRadius ? const Radius.circular(12) : Radius.zero,
  736. ),
  737. child: Padding(
  738. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  739. child: Row(
  740. children: [
  741. Expanded(
  742. child: Text(
  743. label,
  744. style: TextStyle(
  745. color: cs.onSurface,
  746. fontSize: 14,
  747. ),
  748. ),
  749. ),
  750. if (trailing != null) trailing!,
  751. const SizedBox(width: 4),
  752. Icon(Icons.chevron_right,
  753. size: 18, color: cs.onSurface.withAlpha(102)),
  754. ],
  755. ),
  756. ),
  757. );
  758. }
  759. }
  760. // ── 退出登录按钮 ──────────────────────────────────────────────
  761. class _LogoutButton extends StatelessWidget {
  762. const _LogoutButton({required this.onTap});
  763. final VoidCallback onTap;
  764. @override
  765. Widget build(BuildContext context) {
  766. final l10n = AppLocalizations.of(context)!;
  767. final cs = Theme.of(context).colorScheme;
  768. final isDark = Theme.of(context).brightness == Brightness.dark;
  769. return Padding(
  770. padding: const EdgeInsets.symmetric(horizontal: 16),
  771. child: GestureDetector(
  772. onTap: onTap,
  773. child: Container(
  774. height: 48,
  775. decoration: BoxDecoration(
  776. color:
  777. isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  778. borderRadius: BorderRadius.circular(12),
  779. ),
  780. child: Center(
  781. child: Text(
  782. l10n.logoutButton,
  783. style: TextStyle(
  784. color: cs.onSurface,
  785. fontSize: 15,
  786. fontWeight: FontWeight.w500,
  787. ),
  788. ),
  789. ),
  790. ),
  791. ),
  792. );
  793. }
  794. }