103 lines
2.6 KiB
Dart
103 lines
2.6 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import '../../app/theme/app_colors.dart';
|
|
import '../../app/theme/app_typography.dart';
|
|
import '../../app/theme/app_spacing.dart';
|
|
|
|
/// 短信验证码倒计时按钮
|
|
///
|
|
/// - 初始: 显示 [label] (如 "获取验证码")
|
|
/// - 点击后: 显示 "XXs" 倒计时,禁用
|
|
/// - 倒计时结束: 恢复可点击
|
|
class CountdownButton extends StatefulWidget {
|
|
final String label;
|
|
final int seconds;
|
|
final Future<void> Function() onPressed;
|
|
final bool enabled;
|
|
|
|
const CountdownButton({
|
|
super.key,
|
|
required this.label,
|
|
this.seconds = 60,
|
|
required this.onPressed,
|
|
this.enabled = true,
|
|
});
|
|
|
|
@override
|
|
State<CountdownButton> createState() => _CountdownButtonState();
|
|
}
|
|
|
|
class _CountdownButtonState extends State<CountdownButton> {
|
|
Timer? _timer;
|
|
int _remaining = 0;
|
|
bool _loading = false;
|
|
|
|
bool get _isCounting => _remaining > 0;
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _handlePress() async {
|
|
if (_isCounting || _loading || !widget.enabled) return;
|
|
|
|
setState(() => _loading = true);
|
|
try {
|
|
await widget.onPressed();
|
|
_startCountdown();
|
|
} catch (_) {
|
|
// 发送失败不启动倒计时,让用户重试
|
|
} finally {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
void _startCountdown() {
|
|
setState(() => _remaining = widget.seconds);
|
|
_timer?.cancel();
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (!mounted) {
|
|
timer.cancel();
|
|
return;
|
|
}
|
|
setState(() {
|
|
_remaining--;
|
|
if (_remaining <= 0) {
|
|
timer.cancel();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final disabled = _isCounting || _loading || !widget.enabled;
|
|
final text = _isCounting ? '${_remaining}s' : widget.label;
|
|
|
|
return SizedBox(
|
|
height: AppSpacing.buttonHeightSm,
|
|
child: TextButton(
|
|
onPressed: disabled ? null : _handlePress,
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: AppColors.primary,
|
|
disabledForegroundColor: AppColors.textTertiary,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: AppSpacing.borderRadiusSm,
|
|
),
|
|
textStyle: AppTypography.labelMedium,
|
|
),
|
|
child: _loading
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Text(text),
|
|
),
|
|
);
|
|
}
|
|
}
|