提交 748edfd8 authored 作者: MrQi's avatar MrQi

refactor: clx verification 2.0.0

上级 59255731
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/async-2.11.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "characters",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/characters-1.3.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "clock",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "collection",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/collection-1.18.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "fake_async",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "flutter",
"rootUri": "file:///Users/mrqi/flutter/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-3.0.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/mrqi/flutter/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-2.0.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-2.0.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "lints",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/lints-3.0.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "matcher",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.16+1",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.8.0",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/meta-1.11.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "path",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/mrqi/flutter/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "source_span",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.11.1",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.2",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.2.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.1",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "test_api",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.6.1",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "vector_math",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4",
"packageUri": "lib/",
"languageVersion": "2.14"
},
{
"name": "vm_service",
"rootUri": "file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vm_service-13.0.0",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "clx_verification_code",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.3"
}
],
"generated": "2026-03-16T06:42:06.704656Z",
"generator": "pub",
"generatorVersion": "3.3.4"
}
async
2.18
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/async-2.11.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/async-2.11.0/lib/
boolean_selector
2.17
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.1/lib/
characters
2.12
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/characters-1.3.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/characters-1.3.0/lib/
clock
2.12
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.1/lib/
collection
2.18
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/collection-1.18.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/collection-1.18.0/lib/
fake_async
2.12
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.1/lib/
flutter_lints
3.1
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-3.0.2/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-3.0.2/lib/
leak_tracker
3.1
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.0/lib/
leak_tracker_flutter_testing
3.1
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-2.0.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-2.0.1/lib/
leak_tracker_testing
3.1
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-2.0.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-2.0.1/lib/
lints
3.0
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/lints-3.0.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/lints-3.0.0/lib/
matcher
3.0
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.16+1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.16+1/lib/
material_color_utilities
2.17
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.8.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.8.0/lib/
meta
2.12
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/meta-1.11.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/meta-1.11.0/lib/
path
3.0
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.0/lib/
source_span
2.18
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.0/lib/
stack_trace
2.18
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.11.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.11.1/lib/
stream_channel
2.19
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.2/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.2/lib/
string_scanner
2.18
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.2.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.2.0/lib/
term_glyph
2.12
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.1/lib/
test_api
3.0
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.6.1/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.6.1/lib/
vector_math
2.14
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4/lib/
vm_service
3.0
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vm_service-13.0.0/
file:///Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/vm_service-13.0.0/lib/
clx_verification_code
3.3
file:///Users/mrqi/Desktop/company_plugs/clx_verification_code/
file:///Users/mrqi/Desktop/company_plugs/clx_verification_code/lib/
sky_engine
3.2
file:///Users/mrqi/flutter/bin/cache/pkg/sky_engine/
file:///Users/mrqi/flutter/bin/cache/pkg/sky_engine/lib/
flutter
3.2
file:///Users/mrqi/flutter/packages/flutter/
file:///Users/mrqi/flutter/packages/flutter/lib/
flutter_test
3.2
file:///Users/mrqi/flutter/packages/flutter_test/
file:///Users/mrqi/flutter/packages/flutter_test/lib/
2
3.19.6
\ No newline at end of file
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"fluttertoast","path":"/Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.2.8/","native_build":true,"dependencies":[]}],"android":[{"name":"fluttertoast","path":"/Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.2.8/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"fluttertoast","path":"/Users/mrqi/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.2.8/","dependencies":[]}]},"dependencyGraph":[{"name":"fluttertoast","dependencies":[]}],"date_created":"2025-04-24 14:46:53.353732","version":"3.19.6"}
\ No newline at end of file
差异被折叠。
library clx_verification_code;
/// 公共控制器
export 'code_manage/clx_code_manage_logic.dart';
/// -------公共View---------
export 'views/clx_line_code_manage_view.dart';
export 'views/clx_page_code_manage_view.dart';
/// -------公共Package---------
export 'package:get/get.dart';
export 'package:fluttertoast/fluttertoast.dart';
/// -------公共Widget---------
export 'widgets/input_widget/code_input_paint.dart';
export 'widgets/input_widget/code_input_view.dart';
export 'widgets/tips_widget/no_receive_resend_sms_view.dart';
export 'widgets/tips_widget/no_receive_sms_view.dart';
export 'widgets/tips_widget/no_receive_voice_view.dart';
/// -------公共Utils---------
export 'utils/line_code_manage_config.dart';
export 'utils/page_code_manage_config.dart';
/// 新项目建议只依赖以下导出:
///
/// - [ClxVerificationController]:验证码控制器(纯 Flutter/ChangeNotifier)
/// - [ClxVerificationLineField] / [ClxVerificationBoxField]:单行/分格组件
/// - [BaseVerificationStyleConfig]:统一样式配置
export 'core/clx_verification_controller.dart';
export 'widgets/clx_verification_line_field.dart';
export 'widgets/clx_verification_box_field.dart';
export 'utils/verification_style_config.dart';
export 'utils/code_send_enum.dart';
\ No newline at end of file
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:fluttertoast/fluttertoast.dart';
import '../utils/code_send_enum.dart';
class CLXCodeManageLogic extends GetxController {
CLXCodeManageLogic(
{this.maxLength = 6,
this.isFirstResponsed = false,
this.isAccessOutbound = true});
/// 验证码长度
final int maxLength;
/// 是否是成为第一响应者
final bool isFirstResponsed;
/// 是否接入外呼功能
final bool isAccessOutbound;
/// 验证码输入框焦点
final FocusNode codeInoutFocus = FocusNode();
/// 验证码输入框控制器
final TextEditingController codeInoutCtrl = TextEditingController();
/// 倒计时
RxInt countdown = 60.obs;
/// 定时器
Timer? timer;
/// 获取短信验证码
Future<bool?> Function()? getSmsVerificationCode;
/// 获取语音验证码
Future<bool?> Function()? obtainVoiceVerificationCode;
/// 校验验证码输入是否有误
Future<String?> Function(String code)? verifyCode;
/// 联系客服点击事件
Future Function()? contactCustomerService;
/// 验证码校验错误信息
RxString verifyCodeError = ''.obs;
/// 获取验证码按钮文案
RxString codeSendBtn = '获取验证码'.obs;
/// 验证码发送状态
Rx<CodeSendType> codeSendType = CodeSendType.none.obs;
/// 是否展示提示信息模块
RxBool isShowTip = false.obs;
/// 刷新页面需要
RxString refreshPage = ''.obs;
/// 是否允许发送验证码
bool canSend = true;
@override
void onInit() {
super.onInit();
codeInoutCtrl.addListener(_verifyCodeInput);
if (isFirstResponsed) {
codeInoutFocus.requestFocus();
}
}
/// 校验验证码
_verifyCodeInput() async {
/// 处理多次发送校验间隔问题
if (refreshPage.value == codeInoutCtrl.text) {
return;
}
handleTipInfo();
update();
refreshPage.value = codeInoutCtrl.text;
if (codeInoutCtrl.text.length != maxLength) {
return;
}
final result = await verifyCode?.call(codeInoutCtrl.text) ?? '';
verifyCodeError.value = result;
handleTipInfo();
}
/// 外部调用开启定时器
startTimerAndChangeType() {
if (!canSend) {
return;
}
canSend = false;
_startCountdown();
handleCodeSendType();
}
/// 发送短信验证码
sendSmsCode() async {
if (!canSend) {
return;
}
canSend = false;
if (countdown.value != 60) {
return;
}
final result = await getSmsVerificationCode?.call();
if (result == true) {
handleCodeSendType();
_startCountdown();
} else {
canSend = true;
}
}
/// 发送语音验证码
sendVoiceCode() async {
if (!canSend) {
return;
}
canSend = false;
if (countdown.value != 60) {
return;
}
final result = await obtainVoiceVerificationCode?.call();
if (result == true) {
handleCodeSendType(isSmsSend: false);
_startCountdown(isSmsSend: false);
Fluttertoast.showToast(
msg: '即将向您电话播报',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 2,
);
} else {
canSend = true;
}
}
/// 联系客服
contactService() async {
await contactCustomerService?.call();
}
/// 开启倒计时
_startCountdown({bool isSmsSend = true}) {
if (countdown.value != 60) {
return;
}
timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (countdown.value == 0) {
timer.cancel();
codeSendBtn.value = '';
countdown.value = 60;
canSend = true;
handleCodeSendType(isSmsSend: isSmsSend);
handleTipInfo();
} else {
countdown.value--;
codeSendBtn.value = '已发送${countdown.value}s';
}
});
}
/// 处理验证码发送状态
handleCodeSendType({bool isSmsSend = true}) {
switch (codeSendType.value) {
case CodeSendType.none:
codeSendType.value = CodeSendType.firstSendSmsCode;
break;
case CodeSendType.firstSendSmsCode:
codeSendType.value = CodeSendType.notReceiveSmsCode;
break;
case CodeSendType.notReceiveSmsCode:
if (isSmsSend) {
codeSendType.value = CodeSendType.resendSmsCode;
} else {
codeSendType.value = CodeSendType.firstSendVoiceCode;
}
break;
case CodeSendType.resendSmsCode:
if (isAccessOutbound) {
codeSendType.value = CodeSendType.notReceiveResendSmsCode;
}
break;
case CodeSendType.notReceiveResendSmsCode:
codeSendType.value = CodeSendType.firstSendVoiceCode;
break;
case CodeSendType.firstSendVoiceCode:
codeSendType.value = CodeSendType.notReceiveVoiceCode;
break;
case CodeSendType.notReceiveVoiceCode:
codeSendType.value = CodeSendType.resendVoiceCode;
break;
case CodeSendType.resendVoiceCode:
codeSendType.value = CodeSendType.notReceiveResendVoiceCode;
break;
case CodeSendType.notReceiveResendVoiceCode:
codeSendType.value = CodeSendType.resendVoiceCode;
break;
default:
codeSendType.value = CodeSendType.notReceiveResendVoiceCode;
break;
}
}
/// 处理提示信息
handleTipInfo() {
/// 如果输入框内容不为空,则不展示提示信息
isShowTip.value = verifyCodeError.value.isNotEmpty ||
codeInoutCtrl.text.isEmpty && countdown.value == 60;
}
}
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../utils/code_send_enum.dart';
/// 短信验证码发送策略
typedef SmsCodeSender = Future<bool> Function();
/// 语音验证码发送策略
typedef VoiceCodeSender = Future<bool> Function();
/// 验证码校验结果
class VerificationResult {
const VerificationResult({
this.errorMessage,
});
/// 错误文案,null 或空字符串表示校验通过
final String? errorMessage;
bool get isValid => errorMessage == null || errorMessage!.isEmpty;
}
/// 验证码校验策略
typedef CodeVerifier = Future<VerificationResult> Function(String code);
/// 客服兜底策略
typedef CustomerServiceHandler = Future<void> Function();
/// 倒计时抽象,便于后续替换为可测试实现
abstract class CountdownTimer {
/// 启动倒计时
void start({
required int seconds,
required ValueChanged<int> onTick,
required VoidCallback onCompleted,
});
/// 取消倒计时
void cancel();
}
/// 默认基于 [Timer.periodic] 的倒计时实现
class DefaultCountdownTimer implements CountdownTimer {
Timer? _timer;
@override
void start({
required int seconds,
required ValueChanged<int> onTick,
required VoidCallback onCompleted,
}) {
_timer?.cancel();
var current = seconds;
onTick(current);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
current -= 1;
if (current <= 0) {
timer.cancel();
onCompleted();
} else {
onTick(current);
}
});
}
@override
void cancel() {
_timer?.cancel();
_timer = null;
}
}
/// 统一的验证码控制器(2.0.0 版本推荐使用)
///
/// - 不依赖 GetX,仅使用 [ChangeNotifier] 对外通知状态变化;
/// - 发码、校验、联系客服通过策略接口注入,方便在不同业务中复用;
/// - 内部仍复用 [CodeSendType] 表达发送状态,后续可按需演进为完整状态对象。
class ClxVerificationController extends ChangeNotifier {
ClxVerificationController({
this.maxLength = 6,
this.isFirstResponder = false,
this.isAccessOutbound = true,
this.initialCountdown = 60,
CountdownTimer? countdownTimer,
TextEditingController? textController,
FocusNode? focusNode,
}) : countdownTimer = countdownTimer ?? DefaultCountdownTimer(),
textController = textController ?? TextEditingController(),
focusNode = focusNode ?? FocusNode() {
_secondsLeft = initialCountdown;
_codeSendButtonText = _defaultSendText;
_attachTextListener();
if (isFirstResponder) {
this.focusNode.requestFocus();
}
}
/// 验证码长度
final int maxLength;
/// 是否在创建后自动聚焦
final bool isFirstResponder;
/// 是否接入外呼(语音验证码、人工客服)
final bool isAccessOutbound;
/// 倒计时初始秒数
final int initialCountdown;
/// 文本输入控制器
final TextEditingController textController;
/// 焦点
final FocusNode focusNode;
/// 倒计时实现
final CountdownTimer countdownTimer;
/// 发送短信策略
SmsCodeSender? smsCodeSender;
/// 发送语音策略
VoiceCodeSender? voiceCodeSender;
/// 校验策略
CodeVerifier? codeVerifier;
/// 联系客服策略
CustomerServiceHandler? customerServiceHandler;
/// 剩余倒计时时间(秒)
int _secondsLeft = 60;
/// 是否正在发送/冷却中
bool _isSending = false;
/// 当前发送状态
CodeSendType _codeSendType = CodeSendType.none;
/// 错误提示
String _errorText = '';
/// 提示模块是否展示
bool _showTip = false;
/// 获取验证码按钮文案
String _codeSendButtonText = '';
/// 最近一次已经触发校验的内容(用于简单防抖)
String _lastVerifiedText = '';
static const String _defaultSendText = '获取验证码';
// 对外只读访问
int get secondsLeft => _secondsLeft;
bool get isSending => _isSending;
CodeSendType get codeSendType => _codeSendType;
String get errorText => _errorText;
bool get showTip => _showTip;
String get codeSendButtonText => _codeSendButtonText;
bool get canSendCode => !_isSending && _secondsLeft == initialCountdown;
/// 文本变化监听
void _attachTextListener() {
textController.addListener(_handleTextChanged);
}
void _handleTextChanged() {
final text = textController.text;
// 简单防抖:与上次完全相同则不再触发
if (text == _lastVerifiedText) {
return;
}
_updateTipVisibility();
if (text.length != maxLength) {
notifyListeners();
return;
}
_verify(text);
}
Future<void> _verify(String code) async {
final verifier = codeVerifier;
if (verifier == null) {
return;
}
_lastVerifiedText = code;
final result = await verifier(code);
_errorText = result.errorMessage ?? '';
_updateTipVisibility();
notifyListeners();
}
/// 主动触发展示倒计时与状态变化(例如由外部流程先发送了短信)
void startTimerAndChangeType() {
if (!canSendCode) {
return;
}
_startCountdown(isSmsSend: true);
_advanceCodeSendType(isSmsSend: true);
notifyListeners();
}
Future<void> sendSmsCode() async {
if (!canSendCode) {
return;
}
final sender = smsCodeSender;
if (sender == null) {
return;
}
_isSending = true;
notifyListeners();
final ok = await sender();
if (ok) {
_advanceCodeSendType(isSmsSend: true);
_startCountdown(isSmsSend: true);
} else {
_isSending = false;
_updateTipVisibility();
notifyListeners();
}
}
Future<void> sendVoiceCode() async {
if (!canSendCode) {
return;
}
if (!isAccessOutbound) {
return;
}
final sender = voiceCodeSender;
if (sender == null) {
return;
}
_isSending = true;
notifyListeners();
final ok = await sender();
if (ok) {
_advanceCodeSendType(isSmsSend: false);
_startCountdown(isSmsSend: false);
} else {
_isSending = false;
_updateTipVisibility();
notifyListeners();
}
}
Future<void> contactService() async {
final handler = customerServiceHandler;
if (handler == null) {
return;
}
await handler();
}
void _startCountdown({required bool isSmsSend}) {
_isSending = true;
countdownTimer.start(
seconds: initialCountdown,
onTick: (value) {
_secondsLeft = value;
_codeSendButtonText = '已发送${_secondsLeft}s';
notifyListeners();
},
onCompleted: () {
_secondsLeft = initialCountdown;
_codeSendButtonText = _defaultSendText;
_isSending = false;
_advanceCodeSendType(isSmsSend: isSmsSend);
_updateTipVisibility();
notifyListeners();
},
);
}
void _advanceCodeSendType({required bool isSmsSend}) {
switch (_codeSendType) {
case CodeSendType.none:
_codeSendType = CodeSendType.firstSendSmsCode;
break;
case CodeSendType.firstSendSmsCode:
_codeSendType = CodeSendType.notReceiveSmsCode;
break;
case CodeSendType.notReceiveSmsCode:
_codeSendType =
isSmsSend ? CodeSendType.resendSmsCode : CodeSendType.firstSendVoiceCode;
break;
case CodeSendType.resendSmsCode:
if (isAccessOutbound) {
_codeSendType = CodeSendType.notReceiveResendSmsCode;
}
break;
case CodeSendType.notReceiveResendSmsCode:
_codeSendType = CodeSendType.firstSendVoiceCode;
break;
case CodeSendType.firstSendVoiceCode:
_codeSendType = CodeSendType.notReceiveVoiceCode;
break;
case CodeSendType.notReceiveVoiceCode:
_codeSendType = CodeSendType.resendVoiceCode;
break;
case CodeSendType.resendVoiceCode:
_codeSendType = CodeSendType.notReceiveResendVoiceCode;
break;
case CodeSendType.notReceiveResendVoiceCode:
_codeSendType = CodeSendType.resendVoiceCode;
break;
}
}
void _updateTipVisibility() {
_showTip = _errorText.isNotEmpty ||
(textController.text.isEmpty && _secondsLeft == initialCountdown);
}
@override
void dispose() {
countdownTimer.cancel();
textController.removeListener(_handleTextChanged);
super.dispose();
}
}
import 'package:flutter/widgets.dart';
class PageCodeManageConfig {
/// 外层容器
Decoration? decoration;
/// 外边距
EdgeInsetsGeometry? margin;
/// 内边距
EdgeInsetsGeometry? padding;
/// 输入框是否紧贴
bool isCollapsed;
/// 提示文字
String? hintText;
/// 提示文字样式
TextStyle? hintStyle;
/// 输入框文字样式
TextStyle? style;
/// 倒计时文字样式
TextStyle? countdownStyle;
/// 倒计时文字样式
TextStyle? codeSendStyle;
/// 重新获取文字样式
TextStyle? reSendStyle;
/// 提示文字样式
TextStyle? tipsStyle;
/// 错误提示文字样式
TextStyle? errorStyle;
/// 语音发送文字样式
TextStyle? voiceSendStyle;
/// 联系客服文字样式
TextStyle? contactStyle;
/// 是否隐藏输入内容
final bool obscureText;
/// 输入框背景色
final Color inputRectBg;
/// 输入框间距
final double runSpance;
/// 输入框圆角
final double radius;
PageCodeManageConfig({
this.decoration = const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
color: Color(0xffF7F8FA),
),
this.margin = const EdgeInsets.symmetric(horizontal: 30),
this.padding = const EdgeInsets.symmetric(horizontal: 30),
this.isCollapsed = false,
this.hintText,
this.hintStyle = const TextStyle(
color: Color(0xffC9CDD4), fontSize: 14, fontWeight: FontWeight.w400),
this.style = const TextStyle(
color: Color(0xff333C4C), fontSize: 15, fontWeight: FontWeight.w500),
this.countdownStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 14,
fontWeight: FontWeight.w500,
),
this.codeSendStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 14,
fontWeight: FontWeight.w500,
),
this.reSendStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 12,
fontWeight: FontWeight.w600,
),
this.tipsStyle = const TextStyle(
color: Color(0xff86909C),
fontSize: 12,
fontWeight: FontWeight.w400,
),
this.errorStyle = const TextStyle(
color: Color(0xffFF4D4F),
fontSize: 12,
fontWeight: FontWeight.w400,
),
this.voiceSendStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 12,
fontWeight: FontWeight.w600,
),
this.contactStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 12,
fontWeight: FontWeight.w600,
),
this.obscureText = false,
this.inputRectBg = const Color(0xffF2F3F5),
this.runSpance = 8,
this.radius = 8,
});
}
import 'package:flutter/widgets.dart';
class LineCodeManageConfig {
/// 2.0.0 版本统一的验证码样式基础配置
///
/// 旧版的 [LineCodeManageConfig] / [PageCodeManageConfig] 仍可继续使用,
/// 新版推荐使用该配置搭配 2.0.0 的控制器与组件。
class BaseVerificationStyleConfig {
/// 外层容器
Decoration? decoration;
final Decoration? decoration;
/// 外边距
EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry? margin;
/// 内边距
EdgeInsetsGeometry? padding;
/// 输入框是否紧贴
bool isCollapsed;
/// 是否小窗口模式
bool isSmallWindow;
/// 提示文字
String? hintText;
final EdgeInsetsGeometry? padding;
/// 提示文字样式
TextStyle? hintStyle;
/// 输入框文字样式
TextStyle? style;
final TextStyle? hintStyle;
/// 倒计时文字样式
TextStyle? countdownStyle;
/// 输入文字样式
final TextStyle? textStyle;
/// 倒计时文字样式
TextStyle? codeSendStyle;
/// 重新获取文字样式
TextStyle? reSendStyle;
/// 提示文字样式
TextStyle? tipsStyle;
/// 错误文字样式
final TextStyle? errorStyle;
/// 错误提示文字样式
TextStyle? errorStyle;
/// 普通提示文字样式
final TextStyle? tipsStyle;
/// 语音发送文字样式
TextStyle? voiceSendStyle;
/// 主操作(按钮)文字样式
final TextStyle? primaryActionStyle;
/// 联系客服文字样式
TextStyle? contactStyle;
/// 次操作(重发/语音/联系客服)文字样式
final TextStyle? secondaryActionStyle;
LineCodeManageConfig({
const BaseVerificationStyleConfig({
this.decoration = const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
color: Color(0xffF7F8FA),
),
this.margin = const EdgeInsets.symmetric(horizontal: 30),
this.padding = const EdgeInsets.symmetric(horizontal: 30),
this.isCollapsed = false,
this.isSmallWindow = false,
this.hintText,
this.hintStyle = const TextStyle(
color: Color(0xffC9CDD4), fontSize: 14, fontWeight: FontWeight.w400),
this.style = const TextStyle(
color: Color(0xff333C4C), fontSize: 15, fontWeight: FontWeight.w500),
this.countdownStyle = const TextStyle(
color: Color(0xff4F4535),
color: Color(0xffC9CDD4),
fontSize: 14,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w400,
),
this.codeSendStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 14,
this.textStyle = const TextStyle(
color: Color(0xff333C4C),
fontSize: 15,
fontWeight: FontWeight.w500,
),
this.reSendStyle = const TextStyle(
color: Color(0xff4F4535),
this.errorStyle = const TextStyle(
color: Color(0xffFF4D4F),
fontSize: 12,
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w400,
),
this.tipsStyle = const TextStyle(
color: Color(0xff86909C),
fontSize: 12,
fontWeight: FontWeight.w400,
),
this.errorStyle = const TextStyle(
color: Color(0xffFF4D4F),
fontSize: 12,
fontWeight: FontWeight.w400,
),
this.voiceSendStyle = const TextStyle(
this.primaryActionStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 12,
fontWeight: FontWeight.w600,
fontSize: 14,
fontWeight: FontWeight.w500,
),
this.contactStyle = const TextStyle(
this.secondaryActionStyle = const TextStyle(
color: Color(0xff4F4535),
fontSize: 12,
fontWeight: FontWeight.w600,
),
});
}
import 'package:flutter/material.dart';
import '../clx_verification_code.dart';
class CLXLineCodeManageView extends StatelessWidget {
const CLXLineCodeManageView({
super.key,
required this.loigc,
required this.config,
this.disabled = false,
});
/// 页面状态变化处理控制器
final CLXCodeManageLogic loigc;
/// 是否禁用
final bool disabled;
/// 样式配置
final LineCodeManageConfig config;
@override
Widget build(BuildContext context) {
return Container(
margin: config.margin,
padding: config.padding,
decoration: config.decoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: loigc.codeInoutCtrl,
keyboardType: TextInputType.number,
maxLength: loigc.maxLength,
style: config.style,
enabled: !disabled,
enableInteractiveSelection: false,
decoration: InputDecoration(
isCollapsed: config.isCollapsed,
counterText: '',
hintText: config.hintText,
hintStyle: config.hintStyle,
border: InputBorder.none,
),
),
),
const SizedBox(width: 10),
Obx(
() => Visibility(
visible: loigc.codeSendBtn.value.isNotEmpty,
child: GestureDetector(
onTap: loigc.sendSmsCode,
child: Text(
loigc.codeSendBtn.value,
style: loigc.codeSendType.value.isUnSend
? config.countdownStyle
: config.codeSendStyle,
),
),
),
),
],
),
Obx(
() => Visibility(
visible: loigc.verifyCodeError.value.isNotEmpty,
child: Column(
children: [
const SizedBox(height: 5),
Text(
loigc.verifyCodeError.value,
style: config.errorStyle,
),
],
),
),
),
Obx(
() => Visibility(
visible: loigc.isShowTip.value,
child: Column(
children: [
/// 展示重新发送验证码按钮以及获取语音验证码按钮
if (loigc.codeSendType.value ==
CodeSendType.notReceiveSmsCode)
NoReceiveSmsView(
isAccessOutCall: loigc.isAccessOutbound,
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
voiceSendStyle: config.voiceSendStyle,
isSmallWindow: config.isSmallWindow,
sendSmsCode: loigc.sendSmsCode,
sendVoiceCode: loigc.sendVoiceCode,
),
/// 展示没有收到二次发送的验证码的提示
if (loigc.codeSendType.value ==
CodeSendType.notReceiveResendSmsCode)
NoReceiveResendSmsView(
isAccessOutCall: loigc.isAccessOutbound,
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
sendVoiceCode: loigc.sendVoiceCode,
),
/// 展示没有收到首次发送的语音验证码的提示
if (loigc.codeSendType.value ==
CodeSendType.notReceiveVoiceCode ||
loigc.codeSendType.value ==
CodeSendType.notReceiveResendVoiceCode)
NoReceiveVoiceView(
isSmallWindow: config.isSmallWindow,
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
contactStyle: config.contactStyle,
sendVoiceCode: loigc.sendVoiceCode,
contactService: loigc.contactService,
isShowContact: loigc.codeSendType.value ==
CodeSendType.notReceiveResendVoiceCode,
),
],
),
),
),
],
),
);
}
}
import 'package:flutter/widgets.dart';
import '../clx_verification_code.dart';
class CLXPageCodeManageView extends StatelessWidget {
const CLXPageCodeManageView({
super.key,
required this.loigc,
required this.config,
});
/// 页面状态变化处理控制器
final CLXCodeManageLogic loigc;
/// 是否隐藏输入内容
final PageCodeManageConfig config;
@override
Widget build(BuildContext context) {
return Container(
margin: config.margin,
padding: config.padding,
decoration: config.decoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GetBuilder(
init: loigc,
builder: (controller) {
return CodeInputView(
controller: loigc.codeInoutCtrl,
focusNode: loigc.codeInoutFocus,
obscureText: config.obscureText,
maxLength: loigc.maxLength,
inputRectBg: config.inputRectBg,
runSpance: config.runSpance,
radius: config.radius,
);
},
),
Obx(
() => Visibility(
visible: loigc.verifyCodeError.value.isNotEmpty,
child: Column(
children: [
const SizedBox(height: 5),
Text(
loigc.verifyCodeError.value,
style: config.errorStyle,
),
],
),
),
),
Obx(
() => Visibility(
visible: loigc.isShowTip.value,
child: Column(
children: [
/// 展示重新发送验证码按钮以及获取语音验证码按钮
if (loigc.codeSendType.value ==
CodeSendType.notReceiveSmsCode)
NoReceiveSmsView(
isAccessOutCall: loigc.isAccessOutbound,
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
voiceSendStyle: config.voiceSendStyle,
isSmallWindow: false,
sendSmsCode: loigc.sendSmsCode,
sendVoiceCode: loigc.sendVoiceCode,
),
/// 展示没有收到二次发送的验证码的提示
if (loigc.codeSendType.value ==
CodeSendType.notReceiveResendSmsCode)
NoReceiveResendSmsView(
isAccessOutCall: loigc.isAccessOutbound,
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
sendVoiceCode: loigc.sendVoiceCode,
),
/// 展示没有收到首次发送的语音验证码的提示
if (loigc.codeSendType.value ==
CodeSendType.notReceiveVoiceCode ||
loigc.codeSendType.value ==
CodeSendType.notReceiveResendVoiceCode)
NoReceiveVoiceView(
tipsStyle: config.tipsStyle,
reSendStyle: config.reSendStyle,
contactStyle: config.contactStyle,
isSmallWindow: false,
sendVoiceCode: loigc.sendVoiceCode,
contactService: loigc.contactService,
isShowContact: loigc.codeSendType.value ==
CodeSendType.notReceiveResendVoiceCode,
),
],
),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import '../core/clx_verification_controller.dart';
import '../utils/verification_style_config.dart';
import '../utils/code_send_enum.dart';
import 'input_widget/code_input_view.dart';
import 'tips_widget/no_receive_sms_view.dart';
import 'tips_widget/no_receive_resend_sms_view.dart';
import 'tips_widget/no_receive_voice_view.dart';
/// 2.0.0 推荐使用的「分格输入 + 获取验证码」组件
class ClxVerificationBoxField extends StatelessWidget {
const ClxVerificationBoxField({
super.key,
required this.controller,
this.styleConfig = const BaseVerificationStyleConfig(),
this.obscureText = false,
this.inputRectBg = const Color(0xffF2F3F5),
this.runSpance = 8,
this.radius = 8,
});
final ClxVerificationController controller;
final BaseVerificationStyleConfig styleConfig;
final bool obscureText;
final Color inputRectBg;
final double runSpance;
final double radius;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Container(
margin: styleConfig.margin,
padding: styleConfig.padding,
decoration: styleConfig.decoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CodeInputView(
controller: controller.textController,
focusNode: controller.focusNode,
obscureText: obscureText,
inputRectBg: inputRectBg,
maxLength: controller.maxLength,
runSpance: runSpance,
radius: radius,
),
if (controller.errorText.isNotEmpty) ...[
const SizedBox(height: 5),
Text(
controller.errorText,
style: styleConfig.errorStyle,
),
],
if (controller.showTip) _buildTips(),
],
),
);
},
);
}
Widget _buildTips() {
final type = controller.codeSendType;
if (type == CodeSendType.notReceiveSmsCode) {
return NoReceiveSmsView(
isAccessOutCall: controller.isAccessOutbound,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
voiceSendStyle: styleConfig.secondaryActionStyle,
isSmallWindow: false,
sendSmsCode: controller.sendSmsCode,
sendVoiceCode: controller.sendVoiceCode,
);
}
if (type == CodeSendType.notReceiveResendSmsCode) {
return NoReceiveResendSmsView(
isAccessOutCall: controller.isAccessOutbound,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
sendVoiceCode: controller.sendVoiceCode,
);
}
if (type == CodeSendType.notReceiveVoiceCode ||
type == CodeSendType.notReceiveResendVoiceCode) {
return NoReceiveVoiceView(
isSmallWindow: false,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
contactStyle: styleConfig.secondaryActionStyle,
sendVoiceCode: controller.sendVoiceCode,
contactService: controller.contactService,
isShowContact: type == CodeSendType.notReceiveResendVoiceCode,
);
}
return const SizedBox.shrink();
}
}
import 'package:flutter/material.dart';
import '../core/clx_verification_controller.dart';
import '../utils/verification_style_config.dart';
import '../utils/code_send_enum.dart';
import 'tips_widget/no_receive_sms_view.dart';
import 'tips_widget/no_receive_resend_sms_view.dart';
import 'tips_widget/no_receive_voice_view.dart';
/// 2.0.0 推荐使用的「单行输入 + 获取验证码」组件
class ClxVerificationLineField extends StatelessWidget {
const ClxVerificationLineField({
super.key,
required this.controller,
this.styleConfig = const BaseVerificationStyleConfig(),
this.hintText,
this.enabled = true,
this.isSmallWindow = false,
});
final ClxVerificationController controller;
final BaseVerificationStyleConfig styleConfig;
final String? hintText;
final bool enabled;
final bool isSmallWindow;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Container(
margin: styleConfig.margin,
padding: styleConfig.padding,
decoration: styleConfig.decoration,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: controller.textController,
focusNode: controller.focusNode,
keyboardType: TextInputType.number,
maxLength: controller.maxLength,
style: styleConfig.textStyle,
enabled: enabled,
enableInteractiveSelection: false,
decoration: InputDecoration(
isCollapsed: true,
counterText: '',
hintText: hintText,
hintStyle: styleConfig.hintStyle,
border: InputBorder.none,
),
),
),
const SizedBox(width: 10),
if (controller.codeSendButtonText.isNotEmpty)
GestureDetector(
onTap: controller.sendSmsCode,
child: Text(
controller.codeSendButtonText,
style: controller.codeSendType.isUnSend
? styleConfig.primaryActionStyle
: styleConfig.primaryActionStyle,
),
),
],
),
if (controller.errorText.isNotEmpty) ...[
const SizedBox(height: 5),
Text(
controller.errorText,
style: styleConfig.errorStyle,
),
],
if (controller.showTip) _buildTips(),
],
),
);
},
);
}
Widget _buildTips() {
final type = controller.codeSendType;
if (type == CodeSendType.notReceiveSmsCode) {
return NoReceiveSmsView(
isAccessOutCall: controller.isAccessOutbound,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
voiceSendStyle: styleConfig.secondaryActionStyle,
isSmallWindow: isSmallWindow,
sendSmsCode: controller.sendSmsCode,
sendVoiceCode: controller.sendVoiceCode,
);
}
if (type == CodeSendType.notReceiveResendSmsCode) {
return NoReceiveResendSmsView(
isAccessOutCall: controller.isAccessOutbound,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
sendVoiceCode: controller.sendVoiceCode,
);
}
if (type == CodeSendType.notReceiveVoiceCode ||
type == CodeSendType.notReceiveResendVoiceCode) {
return NoReceiveVoiceView(
isSmallWindow: isSmallWindow,
tipsStyle: styleConfig.tipsStyle,
reSendStyle: styleConfig.secondaryActionStyle,
contactStyle: styleConfig.secondaryActionStyle,
sendVoiceCode: controller.sendVoiceCode,
contactService: controller.contactService,
isShowContact: type == CodeSendType.notReceiveResendVoiceCode,
);
}
return const SizedBox.shrink();
}
}
......@@ -11,11 +11,6 @@ dependencies:
flutter:
sdk: flutter
# 状态管理
get: ^4.3.7
# 吐司管理
fluttertoast: ^8.2.4
dev_dependencies:
flutter_test:
sdk: flutter
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论