提交 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
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
# clx_verification_code
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
一个封装完善的 Flutter 验证码组件包,支持:
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
- 短信验证码发送与倒计时
- 语音验证码与人工客服兜底
- 单行输入 / 分格输入两种展示形态
- 可配置样式、多语言文案与多主题适配
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
2.0.0 起提供全新的 **纯 Flutter API**,弱化对 GetX/Toast 的依赖,更易测试与扩展。
## Features
---
TODO: List what your package can do. Maybe include images, gifs, or videos.
## 安装
## Getting started
`pubspec.yaml` 中加入:
TODO: List prerequisites and provide or point to information on how to
start using the package.
```yaml
dependencies:
clx_verification_code: ^2.0.0
```
---
## 2.0.0 核心 API 一览
- **控制器**
- `ClxVerificationController`:基于 `ChangeNotifier`,负责验证码输入、校验、倒计时、发送状态机。
- **视图组件**
- `ClxVerificationLineField`:单行输入 + 右侧“获取验证码”按钮。
- `ClxVerificationBoxField`:分格输入(类似支付验证码方框)。
- **配置**
- `BaseVerificationStyleConfig`:统一的样式配置入口。
- `CodeSendType`:验证码发送状态(未发送 / 首次短信 / 重发短信 / 首次语音 / 重发语音等)。
---
## 架构概览
### 模块关系图
```mermaid
flowchart TD
app["App业务代码"]
ctrl["ClxVerificationController"]
lineField["ClxVerificationLineField"]
boxField["ClxVerificationBoxField"]
styleCfg["BaseVerificationStyleConfig"]
codeSend["CodeSendType"]
tipsSms["NoReceiveSmsView"]
tipsResendSms["NoReceiveResendSmsView"]
tipsVoice["NoReceiveVoiceView"]
codeInputView["CodeInputView"]
codeInputPaint["CodeInputPaint"]
countdown["CountdownTimer/DefaultCountdownTimer"]
stratSms["SmsCodeSender"]
stratVoice["VoiceCodeSender"]
stratVerify["CodeVerifier"]
stratCs["CustomerServiceHandler"]
app --> ctrl
app --> lineField
app --> boxField
app --> styleCfg
app --> stratSms
app --> stratVoice
app --> stratVerify
app --> stratCs
ctrl --> codeSend
ctrl --> countdown
lineField --> ctrl
lineField --> styleCfg
lineField --> tipsSms
lineField --> tipsResendSms
lineField --> tipsVoice
boxField --> ctrl
boxField --> styleCfg
boxField --> codeInputView
codeInputView --> codeInputPaint
boxField --> tipsSms
boxField --> tipsResendSms
boxField --> tipsVoice
ctrl -.uses.-> stratSms
ctrl -.uses.-> stratVoice
ctrl -.uses.-> stratVerify
ctrl -.uses.-> stratCs
```
### 验证码发送与校验流程图
```mermaid
flowchart TD
start["用户点击发送按钮"]
checkCanSend{"canSendCode ?"}
callSender["调用 SmsCodeSender/VoiceCodeSender"]
fail["发送失败\n(isSending=false, 更新提示)"]
success["发送成功\n启动倒计时"]
countdown["CountdownTimer 每秒回调\n更新 secondsLeft 和按钮文案"]
done["倒计时结束\n重置按钮/状态并刷新提示"]
inputChange["用户输入/粘贴验证码"]
checkLength{"长度==maxLength ?"}
checkRepeat{"与上次校验内容相同 ?"}
callVerify["调用 CodeVerifier(code)"]
setError["更新 VerificationResult\n(errorText) & 提示显隐"]
start --> checkCanSend
checkCanSend -- 否 --> fail
checkCanSend -- 是 --> callSender
callSender -->|失败| fail
callSender -->|成功| success
success --> countdown
countdown -->|到0| done
inputChange --> checkRepeat
checkRepeat -- 是 --> setError
checkRepeat -- 否 --> checkLength
checkLength -- 否 --> setError
checkLength -- 是 --> callVerify --> setError
```
---
## 快速开始(2.0.0 推荐用法)
### 1. 创建控制器并注入策略
```dart
final controller = ClxVerificationController(
maxLength: 6,
isFirstResponder: true,
isAccessOutbound: true, // 是否接入语音 & 人工客服
)
..smsCodeSender = () async {
// TODO: 调用你的「发送短信验证码」接口,成功返回 true
return true;
}
..voiceCodeSender = () async {
// TODO: 调用你的「发送语音验证码」接口,成功返回 true
return true;
}
..codeVerifier = (code) async {
// TODO: 调用你的「校验验证码」接口
final ok = await verifyFromServer(code);
return VerificationResult(errorMessage: ok ? null : '验证码错误');
}
..customerServiceHandler = () async {
// TODO: 跳转到客服页 / 打开客服对话框
};
```
> 上面的 `SmsCodeSender` / `VoiceCodeSender` / `CodeVerifier` / `CustomerServiceHandler`
> 就是该包的「策略接口」(Strategy),你可以在不同业务中注入不同实现。
### 2. 单行输入模式(Line)
```dart
ClxVerificationLineField(
controller: controller,
styleConfig: const BaseVerificationStyleConfig(),
hintText: '请输入短信验证码',
enabled: true,
isSmallWindow: false, // 小窗/弹层可设为 true,提示区域会改为纵向布局
)
```
### 3. 分格输入模式(Box)
```dart
ClxVerificationBoxField(
controller: controller,
styleConfig: const BaseVerificationStyleConfig(),
obscureText: false, // 是否密文(显示 *)
inputRectBg: const Color(0xffF2F3F5),
runSpance: 8,
radius: 8,
)
```
倒计时和“未收到短信/语音”的提示区域会自动根据内部状态机(`CodeSendType`)切换,无需业务额外处理。
---
## 国际化与多主题支持
2.0.0 版本本身不强行约束文案与主题颜色,你可以通过以下方式适配:
### 1. 文案国际化
- **输入提示文案**:通过 `hintText` 传入当前语言下的占位文字。
- **错误文案**:由你的 `CodeVerifier` 返回,对接业务侧 i18n(如 `S.of(context).codeError`)。
- **“未收到”提示文案**
- 目前默认使用中文文案(如「没有收到短信?」「重新获取」「获取语音验证码」「联系人工服务」)。
- 若你有统一的文案系统,可以:
- 自行 fork `NoReceive*View` 组件,替换其中的字符串;
- 或在后续版本中将这些文案抽象为可配置项(本包已为样式配置抽离了统一入口,文案抽离会遵循同一思路)。
### 2. 多主题样式
通过 `BaseVerificationStyleConfig` 可以统一注入不同主题下的样式,例如:
## Usage
```dart
final lightStyle = BaseVerificationStyleConfig(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: Colors.white,
),
textStyle: const TextStyle(color: Colors.black87, fontSize: 16),
hintStyle: const TextStyle(color: Colors.grey, fontSize: 14),
errorStyle: const TextStyle(color: Colors.redAccent, fontSize: 12),
primaryActionStyle: const TextStyle(color: Colors.blue, fontSize: 14),
secondaryActionStyle: const TextStyle(color: Colors.blueGrey, fontSize: 12),
);
final darkStyle = BaseVerificationStyleConfig(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: const Color(0xFF1E1E1E),
),
textStyle: const TextStyle(color: Colors.white, fontSize: 16),
hintStyle: const TextStyle(color: Colors.white54, fontSize: 14),
errorStyle: const TextStyle(color: Colors.redAccent, fontSize: 12),
primaryActionStyle: const TextStyle(color: Colors.lightBlueAccent, fontSize: 14),
secondaryActionStyle: const TextStyle(color: Colors.blueGrey, fontSize: 12),
);
```
在不同主题下传入不同的 `styleConfig` 即可:
```dart
ClxVerificationLineField(
controller: controller,
styleConfig: isDarkMode ? darkStyle : lightStyle,
hintText: S.of(context).codeHint,
)
```
如需进一步拆分不同场景的配置(如登录/支付/修改手机不同主题),可以在业务层再封一层工厂/配置管理,内部返回不同的 `BaseVerificationStyleConfig` 实例。
---
## 从 1.x 迁移到 2.0.0
### 1. 控制器迁移
**旧版(1.x):**
```dart
final logic = CLXCodeManageLogic(
maxLength: 6,
isFirstResponsed: true,
isAccessOutbound: true,
)
..getSmsVerificationCode = () async { /* ... */ }
..obtainVoiceVerificationCode = () async { /* ... */ }
..verifyCode = (code) async { /* 返回错误文案 */ }
..contactCustomerService = () async { /* ... */ };
```
**新版(2.0.0):**
```dart
final controller = ClxVerificationController(
maxLength: 6,
isFirstResponder: true,
isAccessOutbound: true,
)
..smsCodeSender = () async { /* ... */ }
..voiceCodeSender = () async { /* ... */ }
..codeVerifier = (code) async {
final msg = await verifyFromServer(code);
return VerificationResult(errorMessage: msg);
}
..customerServiceHandler = () async { /* ... */ };
```
迁移要点:
- `isFirstResponsed``isFirstResponder`(命名修正)。
- 回调由多个字段迁移为一组明确的“策略接口”,返回值更语义化(`VerificationResult`)。
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
### 2. 视图组件迁移
**旧版单行视图:**
```dart
CLXLineCodeManageView(
loigc: logic,
config: LineCodeManageConfig(
// 各种样式配置
),
)
```
**新版单行视图:**
```dart
const like = 'sample';
ClxVerificationLineField(
controller: controller,
styleConfig: const BaseVerificationStyleConfig(),
hintText: '请输入验证码',
)
```
## Additional information
**旧版分格视图:**
```dart
CLXPageCodeManageView(
loigc: logic,
config: PageCodeManageConfig(
obscureText: false,
// 其他样式配置
),
)
```
**新版分格视图:**
```dart
ClxVerificationBoxField(
controller: controller,
styleConfig: const BaseVerificationStyleConfig(),
obscureText: false,
)
```
### 3. 状态与提示逻辑
- 旧版:直接使用 `logic.codeSendType``logic.isShowTip``logic.codeSendBtn` 等 Rx 字段,并依赖 GetX 的 `Obx` / `GetBuilder`
- 新版:通过 `ClxVerificationController` 的只读属性访问:
- `controller.codeSendType`
- `controller.showTip`
- `controller.codeSendButtonText`
- 外部若自定义 UI,可使用 `AnimatedBuilder`/`ValueListenableBuilder` 监听 `controller`
如需暂时保持旧逻辑,可继续使用 1.x API;建议在新页面/新业务中直接使用 2.0.0 API,并逐步替换历史调用。
---
## 版本历史
### 2.0.0
- 新增纯 Flutter 的 `ClxVerificationController`,移除对 GetX 的强依赖。
- 新增 `ClxVerificationLineField` / `ClxVerificationBoxField` 统一组件。
- 新增 `BaseVerificationStyleConfig`,支持统一样式与多主题配置。
- 调整包入口导出,区分 2.0.0 推荐 API 与 1.x 兼容 API。
### 1.0.1
- 增加拦截,防止直接粘贴提示文案导致触发多次输入变化
- 增加拦截,防止直接粘贴提示文案导致触发多次输入变化。
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';
export 'utils/code_send_enum.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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论