You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
7.1 KiB
224 lines
7.1 KiB
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<bool> showIdVerifyDialog({String? reasonText}) async {
|
|
final ok = await Get.dialog<bool>(
|
|
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<IdVerifyDialog> createState() => _IdVerifyDialogState();
|
|
}
|
|
|
|
class _IdVerifyDialogState extends State<IdVerifyDialog> {
|
|
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<void> _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<TextInputFormatter>? 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());
|
|
}
|
|
}
|
|
|