import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../../core/theme/app_palette.dart'; import '../../../data/services/id_verify_service.dart'; /// 实名认证输入弹窗:姓名 + 身份证号。 /// /// 已实名时进来就是「修改认证」——姓名预填上次的值,身份证号必须重新输入 /// (服务端只存掩码,拿不回原文,也不该拿回)。 /// /// 返回 true 表示这次认证通过。 Future showIdVerifyDialog({String? reasonText}) async { final ok = await Get.dialog( IdVerifyDialog(reasonText: reasonText), barrierDismissible: false, ); return ok == true; } /// ⚠️ 必须是 StatefulWidget:TextEditingController 要由 State 自己持有、 /// 在 State.dispose() 里释放。 /// /// 早先的写法是在外面 `await Get.dialog(...)` 之后立刻 dispose——那是错的: /// Get.dialog 在路由 pop 的瞬间就返回,而弹窗此时还在播退场动画、里面的 /// TextField 仍挂在树上,controller 被提前释放会破坏元素树, /// 表现为红屏 `'_dependents.isEmpty': is not true`。 class IdVerifyDialog extends StatefulWidget { const IdVerifyDialog({Key? key, this.reasonText}) : super(key: key); final String? reasonText; @override State createState() => _IdVerifyDialogState(); } class _IdVerifyDialogState extends State { late final bool _isModify; late final TextEditingController _nameCtrl; late final TextEditingController _idCtrl; String _error = ''; bool _busy = false; @override void initState() { super.initState(); final svc = IdVerifyService.to; _isModify = svc.verified.value; _nameCtrl = TextEditingController(text: _isModify ? svc.realname.value : ''); _idCtrl = TextEditingController(); } @override void dispose() { _nameCtrl.dispose(); _idCtrl.dispose(); super.dispose(); } Future _submit() async { if (_busy) return; setState(() { _busy = true; _error = ''; }); final msg = await IdVerifyService.to.submit(_nameCtrl.text, _idCtrl.text); // 请求回来时弹窗可能已经被关掉了,不能再 setState。 if (!mounted) return; if (msg == null) { Navigator.of(context).pop(true); return; } setState(() { _busy = false; _error = msg; }); } @override Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; return AlertDialog( backgroundColor: AppPalette.surface(isDark), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), title: Text( _isModify ? 'idVerifyModifyTitle'.tr : 'idVerifyTitle'.tr, style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, color: AppPalette.textPrimary(isDark), ), ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.reasonText ?? 'idVerifyDesc'.tr, style: TextStyle( fontSize: 13.sp, height: 1.6, color: AppPalette.textSecondary(isDark), ), ), SizedBox(height: 16.h), _field( isDark: isDark, controller: _nameCtrl, label: 'idVerifyNameLabel'.tr, hint: 'idVerifyNameHint'.tr, maxLength: 32, ), SizedBox(height: 12.h), _field( isDark: isDark, controller: _idCtrl, label: 'idVerifyIdLabel'.tr, hint: _isModify ? 'idVerifyIdHintModify'.tr : 'idVerifyIdHint'.tr, maxLength: 18, // 身份证号只有数字和末位 X formatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9xX]')), _UpperCaseFormatter(), ], ), if (_error.isNotEmpty) Padding( padding: EdgeInsets.only(top: 10.h), child: Text( _error, style: TextStyle(fontSize: 12.sp, color: Colors.redAccent), ), ), SizedBox(height: 10.h), Text( 'idVerifyPrivacyTip'.tr, style: TextStyle( fontSize: 11.sp, height: 1.5, color: AppPalette.textHint(isDark), ), ), ], ), ), actions: [ TextButton( onPressed: _busy ? null : () => Navigator.of(context).pop(false), child: Text('cancel'.tr, style: TextStyle( fontSize: 14.sp, color: AppPalette.textHint(isDark))), ), TextButton( onPressed: _busy ? null : _submit, child: _busy ? SizedBox( width: 16.w, height: 16.w, child: const CircularProgressIndicator(strokeWidth: 2), ) : Text('idVerifySubmit'.tr, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: AppPalette.accent)), ), ], ); } Widget _field({ required bool isDark, required TextEditingController controller, required String label, required String hint, required int maxLength, List? formatters, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( fontSize: 12.sp, color: AppPalette.textSecondary(isDark))), SizedBox(height: 6.h), TextField( controller: controller, maxLength: maxLength, inputFormatters: formatters, style: TextStyle(fontSize: 14.sp, color: AppPalette.textPrimary(isDark)), decoration: InputDecoration( counterText: '', hintText: hint, hintStyle: TextStyle(fontSize: 13.sp, color: AppPalette.textHint(isDark)), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.r)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10.r), borderSide: BorderSide(color: AppPalette.textHint(isDark)), ), ), ), ], ); } } /// 身份证末位的 x 统一成大写 X,省得因为大小写把合法号码判成非法。 class _UpperCaseFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue) { return newValue.copyWith(text: newValue.text.toUpperCase()); } }