security_screen.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../core/l10n/app_localizations.dart';
  5. import '../../../core/theme/app_colors.dart';
  6. import '../../../data/models/asset/account_auth.dart';
  7. import '../../../providers/security_provider.dart';
  8. class SecurityScreen extends ConsumerWidget {
  9. const SecurityScreen({super.key});
  10. @override
  11. Widget build(BuildContext context, WidgetRef ref) {
  12. final cs = Theme.of(context).colorScheme;
  13. final authAsync = ref.watch(securityAuthProvider);
  14. return Scaffold(
  15. appBar: AppBar(
  16. elevation: 0,
  17. leading: IconButton(
  18. icon: const Icon(Icons.chevron_left, size: 28),
  19. onPressed: () => context.pop(),
  20. ),
  21. title: Text(
  22. AppLocalizations.of(context)!.securitySettingsTitle,
  23. style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
  24. ),
  25. centerTitle: true,
  26. ),
  27. body: authAsync.when(
  28. loading: () => const Center(child: CircularProgressIndicator()),
  29. error: (e, _) => Center(
  30. child: Column(
  31. mainAxisSize: MainAxisSize.min,
  32. children: [
  33. Text(AppLocalizations.of(context)!.loadFailed, style: TextStyle(color: cs.onSurface)),
  34. const SizedBox(height: 12),
  35. ElevatedButton(
  36. onPressed: () =>
  37. ref.read(securityAuthProvider.notifier).refresh(),
  38. child: Text(AppLocalizations.of(context)!.retry),
  39. ),
  40. ],
  41. ),
  42. ),
  43. data: (auth) => _SecurityBody(auth: auth),
  44. ),
  45. );
  46. }
  47. }
  48. // ── 安全设置主体 ─────────────────────────────────────────────
  49. class _SecurityBody extends ConsumerWidget {
  50. const _SecurityBody({required this.auth});
  51. final AccountAuth auth;
  52. @override
  53. Widget build(BuildContext context, WidgetRef ref) {
  54. final cs = Theme.of(context).colorScheme;
  55. final isDark = Theme.of(context).brightness == Brightness.dark;
  56. return Column(
  57. children: [
  58. // ── 安全提示 ─────────────────────────────────────
  59. const _WarningBanner(),
  60. const SizedBox(height: 16),
  61. // ── 安全选项列表 ──────────────────────────────────
  62. Container(
  63. margin: const EdgeInsets.symmetric(horizontal: 16),
  64. decoration: BoxDecoration(
  65. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  66. borderRadius: BorderRadius.circular(12),
  67. ),
  68. child: Column(
  69. children: [
  70. // 谷歌认证
  71. _SecurityItem(
  72. icon: Icons.lock_outline,
  73. title: AppLocalizations.of(context)!.authenticator,
  74. subtitle: AppLocalizations.of(context)!.authenticatorDesc,
  75. statusLabel: auth.isGoogleVerified ? AppLocalizations.of(context)!.certified : AppLocalizations.of(context)!.notCertified,
  76. statusColor:
  77. auth.isGoogleVerified ? AppColors.rise : AppColors.fall,
  78. onTap: () async {
  79. if (auth.isGoogleVerified) {
  80. // 已绑定 → 提示联系客服
  81. _showContactDialog(context);
  82. } else {
  83. // 未绑定 → 进入绑定流程
  84. await context.push('/user/security/google-auth');
  85. // 返回后刷新状态
  86. ref.read(securityAuthProvider.notifier).refresh();
  87. }
  88. },
  89. ),
  90. _divider(cs),
  91. // 邮箱认证
  92. _SecurityItem(
  93. icon: Icons.mail_outline,
  94. title: AppLocalizations.of(context)!.emailAuth,
  95. subtitle: auth.email.isNotEmpty
  96. ? auth.maskedEmail
  97. : AppLocalizations.of(context)!.emailAuthDesc,
  98. statusLabel: auth.email.isNotEmpty ? AppLocalizations.of(context)!.certified : AppLocalizations.of(context)!.notCertified,
  99. statusColor:
  100. auth.email.isNotEmpty ? AppColors.rise : AppColors.fall,
  101. onTap: () {},
  102. ),
  103. _divider(cs),
  104. // 资金密码
  105. _SecurityItem(
  106. icon: Icons.vpn_key_outlined,
  107. title: AppLocalizations.of(context)!.fundPassword,
  108. subtitle: AppLocalizations.of(context)!.fundPasswordDesc,
  109. statusLabel: auth.isFundsVerified ? AppLocalizations.of(context)!.modifyAction : AppLocalizations.of(context)!.notSet,
  110. statusColor:
  111. auth.isFundsVerified ? AppColors.rise : AppColors.fall,
  112. onTap: () async {
  113. // fundsVerified=="1" → 修改模式,否则 → 首次设置
  114. await context.push(
  115. '/user/security/fund-password',
  116. extra: auth.isFundsVerified,
  117. );
  118. ref.read(securityAuthProvider.notifier).refresh();
  119. },
  120. ),
  121. _divider(cs),
  122. // 登录密码
  123. _SecurityItem(
  124. icon: Icons.lock_outline,
  125. title: AppLocalizations.of(context)!.loginPasswordMenu,
  126. subtitle: AppLocalizations.of(context)!.loginPasswordDesc,
  127. statusLabel: auth.isLoginVerified ? AppLocalizations.of(context)!.alreadySet : AppLocalizations.of(context)!.notSet,
  128. statusColor:
  129. auth.isLoginVerified ? AppColors.rise : AppColors.fall,
  130. onTap: () async {
  131. await context.push('/user/security/change-password');
  132. ref.read(securityAuthProvider.notifier).refresh();
  133. },
  134. isLast: true,
  135. ),
  136. ],
  137. ),
  138. ),
  139. ],
  140. );
  141. }
  142. void _showContactDialog(BuildContext context) {
  143. final cs = Theme.of(context).colorScheme;
  144. final isDark = Theme.of(context).brightness == Brightness.dark;
  145. showDialog<void>(
  146. context: context,
  147. builder: (ctx) => Dialog(
  148. backgroundColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  149. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  150. child: Padding(
  151. padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
  152. child: Column(
  153. mainAxisSize: MainAxisSize.min,
  154. children: [
  155. Container(
  156. width: 48,
  157. height: 48,
  158. decoration: BoxDecoration(
  159. color: AppColors.brand.withAlpha(30),
  160. shape: BoxShape.circle,
  161. ),
  162. child: const Icon(Icons.headset_mic_outlined, color: AppColors.brand, size: 24),
  163. ),
  164. const SizedBox(height: 16),
  165. Text(
  166. AppLocalizations.of(context)!.hintTitle,
  167. style: TextStyle(
  168. color: cs.onSurface,
  169. fontSize: 17,
  170. fontWeight: FontWeight.w700,
  171. ),
  172. ),
  173. const SizedBox(height: 10),
  174. Text(
  175. AppLocalizations.of(context)!.contactServiceHint,
  176. textAlign: TextAlign.center,
  177. style: TextStyle(
  178. color: cs.onSurface.withAlpha(180),
  179. fontSize: 14,
  180. height: 1.5,
  181. ),
  182. ),
  183. const SizedBox(height: 24),
  184. SizedBox(
  185. width: double.infinity,
  186. child: ElevatedButton(
  187. onPressed: () => Navigator.of(ctx).pop(),
  188. style: ElevatedButton.styleFrom(
  189. backgroundColor: AppColors.brand,
  190. foregroundColor: Colors.black,
  191. elevation: 0,
  192. padding: const EdgeInsets.symmetric(vertical: 12),
  193. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  194. ),
  195. child: Text(AppLocalizations.of(context)!.gotIt, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
  196. ),
  197. ),
  198. ],
  199. ),
  200. ),
  201. ),
  202. );
  203. }
  204. Widget _divider(ColorScheme cs) => Divider(
  205. height: 1,
  206. indent: 52,
  207. color: cs.outline,
  208. );
  209. }
  210. // ── 安全提示横幅 ─────────────────────────────────────────────
  211. class _WarningBanner extends StatelessWidget {
  212. const _WarningBanner();
  213. @override
  214. Widget build(BuildContext context) {
  215. return Container(
  216. margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
  217. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  218. decoration: BoxDecoration(
  219. color: const Color(0xFF2D2500),
  220. border: Border.all(color: const Color(0xFF4A3A00), width: 1),
  221. borderRadius: BorderRadius.circular(10),
  222. ),
  223. child: Row(
  224. crossAxisAlignment: CrossAxisAlignment.start,
  225. children: [
  226. const Icon(Icons.info_outline, color: AppColors.brand, size: 16),
  227. const SizedBox(width: 8),
  228. Expanded(
  229. child: Text(
  230. AppLocalizations.of(context)!.securityBannerTip,
  231. style: const TextStyle(color: AppColors.brand, fontSize: 13),
  232. ),
  233. ),
  234. ],
  235. ),
  236. );
  237. }
  238. }
  239. // ── 安全选项行 ───────────────────────────────────────────────
  240. class _SecurityItem extends StatelessWidget {
  241. const _SecurityItem({
  242. required this.icon,
  243. required this.title,
  244. required this.subtitle,
  245. required this.onTap,
  246. this.statusLabel,
  247. this.statusColor,
  248. this.isLast = false,
  249. });
  250. final IconData icon;
  251. final String title;
  252. final String subtitle;
  253. final VoidCallback onTap;
  254. final String? statusLabel;
  255. final Color? statusColor;
  256. final bool isLast;
  257. @override
  258. Widget build(BuildContext context) {
  259. final cs = Theme.of(context).colorScheme;
  260. return InkWell(
  261. onTap: onTap,
  262. borderRadius: isLast
  263. ? const BorderRadius.vertical(bottom: Radius.circular(12))
  264. : BorderRadius.zero,
  265. child: Padding(
  266. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  267. child: Row(
  268. children: [
  269. Container(
  270. width: 36,
  271. height: 36,
  272. decoration: BoxDecoration(
  273. color: cs.outline.withAlpha(30),
  274. borderRadius: BorderRadius.circular(8),
  275. ),
  276. child:
  277. Icon(icon, size: 18, color: cs.onSurface.withAlpha(153)),
  278. ),
  279. const SizedBox(width: 12),
  280. Expanded(
  281. child: Column(
  282. crossAxisAlignment: CrossAxisAlignment.start,
  283. children: [
  284. Text(
  285. title,
  286. style: TextStyle(
  287. color: cs.onSurface,
  288. fontSize: 14,
  289. fontWeight: FontWeight.w500,
  290. ),
  291. ),
  292. const SizedBox(height: 2),
  293. Text(
  294. subtitle,
  295. style: TextStyle(
  296. color: cs.onSurface.withAlpha(153),
  297. fontSize: 12,
  298. ),
  299. ),
  300. ],
  301. ),
  302. ),
  303. if (statusLabel != null) ...[
  304. Text(
  305. statusLabel!,
  306. style: TextStyle(
  307. color: statusColor,
  308. fontSize: 13,
  309. fontWeight: FontWeight.w500,
  310. ),
  311. ),
  312. const SizedBox(width: 4),
  313. ],
  314. Icon(Icons.chevron_right,
  315. size: 18, color: cs.onSurface.withAlpha(153)),
  316. ],
  317. ),
  318. ),
  319. );
  320. }
  321. }