776 lines
22 KiB
Dart
776 lines
22 KiB
Dart
import 'dart:async';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:flutter_svg/flutter_svg.dart';
|
||
import '../core/services/ble_provisioning_service.dart';
|
||
import '../features/device/presentation/controllers/device_controller.dart';
|
||
import '../widgets/animated_gradient_background.dart';
|
||
import '../widgets/gradient_button.dart';
|
||
|
||
class WifiConfigPage extends ConsumerStatefulWidget {
|
||
final Map<String, dynamic>? extra;
|
||
|
||
const WifiConfigPage({super.key, this.extra});
|
||
|
||
@override
|
||
ConsumerState<WifiConfigPage> createState() => _WifiConfigPageState();
|
||
}
|
||
|
||
class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
|
||
with TickerProviderStateMixin {
|
||
int _currentStep = 1;
|
||
String _selectedWifiSsid = '';
|
||
final TextEditingController _passwordController = TextEditingController();
|
||
bool _obscurePassword = true;
|
||
|
||
// Progress State
|
||
double _progress = 0.0;
|
||
String _progressText = '正在连接WiFi...';
|
||
bool _connectFailed = false;
|
||
|
||
// Device Info
|
||
Map<String, dynamic> _deviceInfo = {};
|
||
|
||
// BLE Provisioning
|
||
BleProvisioningService? _provService;
|
||
List<ScannedWifi> _wifiList = [];
|
||
bool _isScanning = false;
|
||
|
||
// Subscriptions
|
||
StreamSubscription? _wifiListSub;
|
||
StreamSubscription? _wifiStatusSub;
|
||
StreamSubscription? _disconnectSub;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_deviceInfo = widget.extra ?? {};
|
||
_provService = _deviceInfo['provService'] as BleProvisioningService?;
|
||
|
||
if (_provService != null) {
|
||
_setupBleListeners();
|
||
// 自动开始 WiFi 扫描
|
||
_requestWifiScan();
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_wifiListSub?.cancel();
|
||
_wifiStatusSub?.cancel();
|
||
_disconnectSub?.cancel();
|
||
_provService?.dispose();
|
||
_passwordController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _setupBleListeners() {
|
||
// 监听 WiFi 列表
|
||
_wifiListSub = _provService!.onWifiList.listen((list) {
|
||
if (!mounted) return;
|
||
debugPrint('[WiFi Config] 收到 WiFi 列表: ${list.length} 个');
|
||
setState(() {
|
||
_wifiList = list;
|
||
_isScanning = false;
|
||
});
|
||
});
|
||
|
||
// 监听 WiFi 连接状态
|
||
_wifiStatusSub = _provService!.onWifiStatus.listen((result) {
|
||
if (!mounted) return;
|
||
debugPrint('[WiFi Config] WiFi 状态: success=${result.success}, reason=${result.reasonCode}');
|
||
if (result.success) {
|
||
setState(() {
|
||
_progress = 1.0;
|
||
_progressText = '配网成功!';
|
||
_currentStep = 4;
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_connectFailed = true;
|
||
_progressText = '连接失败 (错误码: ${result.reasonCode})';
|
||
});
|
||
}
|
||
});
|
||
|
||
// 监听 BLE 断开
|
||
_disconnectSub = _provService!.onDisconnect.listen((_) {
|
||
if (!mounted) return;
|
||
debugPrint('[WiFi Config] BLE 连接已断开');
|
||
// 如果在配网中断开,可能是成功后设备重启
|
||
if (_currentStep == 3 && !_connectFailed) {
|
||
setState(() {
|
||
_progress = 1.0;
|
||
_progressText = '设备正在重启...';
|
||
_currentStep = 4;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _requestWifiScan() async {
|
||
if (_provService == null) return;
|
||
setState(() => _isScanning = true);
|
||
await _provService!.requestWifiScan();
|
||
// WiFi 列表会通过 onWifiList stream 回调
|
||
// 设置超时:10 秒后如果还没收到列表,停止加载
|
||
Future.delayed(const Duration(seconds: 10), () {
|
||
if (mounted && _isScanning) {
|
||
setState(() => _isScanning = false);
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _handleNext() async {
|
||
if (_currentStep == 1 && _selectedWifiSsid.isEmpty) return;
|
||
if (_currentStep == 2 && _passwordController.text.isEmpty) return;
|
||
|
||
if (_currentStep == 4) {
|
||
final sn = _deviceInfo['sn'] as String? ?? '';
|
||
if (sn.isNotEmpty) {
|
||
debugPrint('[WiFi Config] Binding device sn=$sn');
|
||
await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
|
||
}
|
||
if (!mounted) return;
|
||
context.go('/device-control');
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_currentStep++;
|
||
});
|
||
|
||
if (_currentStep == 3) {
|
||
_startConnecting();
|
||
}
|
||
}
|
||
|
||
void _handleBack() {
|
||
if (_currentStep > 1) {
|
||
setState(() {
|
||
_currentStep--;
|
||
if (_currentStep == 1) {
|
||
_connectFailed = false;
|
||
_progress = 0.0;
|
||
}
|
||
});
|
||
} else {
|
||
_provService?.disconnect();
|
||
context.go('/bluetooth');
|
||
}
|
||
}
|
||
|
||
Future<void> _startConnecting() async {
|
||
setState(() {
|
||
_progress = 0.1;
|
||
_progressText = '正在发送WiFi信息...';
|
||
_connectFailed = false;
|
||
});
|
||
|
||
if (_provService != null && _provService!.isConnected) {
|
||
// 通过 BLE 发送 WiFi 凭证
|
||
setState(() {
|
||
_progress = 0.3;
|
||
_progressText = '正在发送WiFi凭证...';
|
||
});
|
||
|
||
await _provService!.sendWifiCredentials(
|
||
_selectedWifiSsid,
|
||
_passwordController.text,
|
||
);
|
||
|
||
setState(() {
|
||
_progress = 0.5;
|
||
_progressText = '等待设备连接WiFi...';
|
||
});
|
||
|
||
// WiFi 状态会通过 onWifiStatus stream 回调
|
||
// 设置超时:60 秒后如果还没收到结果
|
||
Future.delayed(const Duration(seconds: 60), () {
|
||
if (mounted && _currentStep == 3 && !_connectFailed) {
|
||
setState(() {
|
||
_connectFailed = true;
|
||
_progressText = '连接超时,请重试';
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
// 无 BLE 连接(模拟模式),使用 mock 流程
|
||
_startMockConnecting();
|
||
}
|
||
}
|
||
|
||
void _startMockConnecting() {
|
||
const steps = [
|
||
{'progress': 0.3, 'text': '正在连接WiFi...'},
|
||
{'progress': 0.6, 'text': '正在验证密码...'},
|
||
{'progress': 0.9, 'text': '正在同步设备...'},
|
||
{'progress': 1.0, 'text': '完成!'},
|
||
];
|
||
|
||
int stepIndex = 0;
|
||
Timer.periodic(const Duration(milliseconds: 800), (timer) {
|
||
if (stepIndex < steps.length) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_progress = steps[stepIndex]['progress'] as double;
|
||
_progressText = steps[stepIndex]['text'] as String;
|
||
});
|
||
}
|
||
stepIndex++;
|
||
} else {
|
||
timer.cancel();
|
||
if (mounted) {
|
||
setState(() => _currentStep = 4);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.white,
|
||
resizeToAvoidBottomInset: true,
|
||
body: Stack(
|
||
children: [
|
||
const AnimatedGradientBackground(),
|
||
Positioned.fill(
|
||
child: SafeArea(
|
||
child: Column(
|
||
children: [
|
||
_buildHeader(),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||
child: Column(
|
||
children: [
|
||
_buildStepIndicator(),
|
||
const SizedBox(height: 32),
|
||
_buildCurrentStepContent(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
_buildFooter(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildHeader() {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||
child: Row(
|
||
children: [
|
||
GestureDetector(
|
||
onTap: _handleBack,
|
||
child: Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(12),
|
||
color: Colors.white.withOpacity(0.6),
|
||
),
|
||
child: const Icon(
|
||
Icons.arrow_back_ios_new,
|
||
size: 18,
|
||
color: Color(0xFF4B5563),
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Text(
|
||
'WiFi配网',
|
||
textAlign: TextAlign.center,
|
||
style: GoogleFonts.outfit(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w600,
|
||
color: const Color(0xFF1F2937),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 48),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildStepIndicator() {
|
||
return Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: List.generate(4, (index) {
|
||
int step = index + 1;
|
||
bool isActive = step == _currentStep;
|
||
bool isCompleted = step < _currentStep;
|
||
|
||
return AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
width: isActive ? 24 : 8,
|
||
height: 8,
|
||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||
decoration: BoxDecoration(
|
||
color: isCompleted
|
||
? const Color(0xFF22C55E)
|
||
: isActive
|
||
? const Color(0xFF8B5CF6)
|
||
: const Color(0xFF8B5CF6).withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
);
|
||
}),
|
||
);
|
||
}
|
||
|
||
Widget _buildCurrentStepContent() {
|
||
switch (_currentStep) {
|
||
case 1:
|
||
return _buildStep1();
|
||
case 2:
|
||
return _buildStep2();
|
||
case 3:
|
||
return _buildStep3();
|
||
case 4:
|
||
return _buildStep4();
|
||
default:
|
||
return const SizedBox.shrink();
|
||
}
|
||
}
|
||
|
||
// Step 1: 选择 WiFi 网络
|
||
Widget _buildStep1() {
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
margin: const EdgeInsets.only(bottom: 24),
|
||
child: const Icon(Icons.wifi, size: 80, color: Color(0xFF8B5CF6)),
|
||
),
|
||
Text(
|
||
'选择WiFi网络',
|
||
style: GoogleFonts.outfit(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: const Color(0xFF1F2937),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'设备需要连接WiFi以使用AI功能',
|
||
style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)),
|
||
),
|
||
const SizedBox(height: 24),
|
||
|
||
if (_isScanning)
|
||
const Padding(
|
||
padding: EdgeInsets.symmetric(vertical: 40),
|
||
child: Column(
|
||
children: [
|
||
CircularProgressIndicator(color: Color(0xFF8B5CF6)),
|
||
SizedBox(height: 16),
|
||
Text(
|
||
'正在通过设备扫描WiFi...',
|
||
style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
else if (_wifiList.isEmpty)
|
||
Column(
|
||
children: [
|
||
const Padding(
|
||
padding: EdgeInsets.symmetric(vertical: 40),
|
||
child: Text(
|
||
'未扫描到WiFi网络',
|
||
style: TextStyle(fontSize: 14, color: Color(0xFF9CA3AF)),
|
||
),
|
||
),
|
||
GestureDetector(
|
||
onTap: _requestWifiScan,
|
||
child: const Text(
|
||
'重新扫描',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: Color(0xFF8B5CF6),
|
||
decoration: TextDecoration.underline,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
)
|
||
else
|
||
Column(
|
||
children: [
|
||
..._wifiList.map((wifi) => _buildWifiItem(wifi)),
|
||
const SizedBox(height: 8),
|
||
GestureDetector(
|
||
onTap: _requestWifiScan,
|
||
child: const Text(
|
||
'重新扫描',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: Color(0xFF8B5CF6),
|
||
decoration: TextDecoration.underline,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildWifiItem(ScannedWifi wifi) {
|
||
bool isSelected = _selectedWifiSsid == wifi.ssid;
|
||
return GestureDetector(
|
||
onTap: () {
|
||
setState(() => _selectedWifiSsid = wifi.ssid);
|
||
},
|
||
child: Container(
|
||
padding: const EdgeInsets.all(16),
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.8),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: isSelected
|
||
? const Color(0xFF8B5CF6)
|
||
: Colors.white.withOpacity(0.5),
|
||
width: isSelected ? 2 : 1,
|
||
),
|
||
boxShadow: isSelected
|
||
? [
|
||
BoxShadow(
|
||
color: const Color(0xFF8B5CF6).withOpacity(0.2),
|
||
blurRadius: 0,
|
||
spreadRadius: 2,
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
wifi.ssid,
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w500,
|
||
color: Color(0xFF1F2937),
|
||
),
|
||
),
|
||
),
|
||
// WiFi 信号图标
|
||
Icon(
|
||
wifi.level >= 3
|
||
? Icons.wifi
|
||
: wifi.level == 2
|
||
? Icons.wifi_2_bar
|
||
: Icons.wifi_1_bar,
|
||
size: 24,
|
||
color: const Color(0xFF6B7280),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// Step 2: 输入密码
|
||
Widget _buildStep2() {
|
||
return Column(
|
||
children: [
|
||
Container(
|
||
margin: const EdgeInsets.only(bottom: 24),
|
||
child: const Icon(
|
||
Icons.lock_outline,
|
||
size: 80,
|
||
color: Color(0xFF8B5CF6),
|
||
),
|
||
),
|
||
Text(
|
||
_selectedWifiSsid,
|
||
style: GoogleFonts.outfit(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: const Color(0xFF1F2937),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'请输入WiFi密码',
|
||
style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)),
|
||
),
|
||
const SizedBox(height: 24),
|
||
TextField(
|
||
controller: _passwordController,
|
||
obscureText: _obscurePassword,
|
||
onChanged: (_) => setState(() {}),
|
||
decoration: InputDecoration(
|
||
hintText: '输入密码',
|
||
filled: true,
|
||
fillColor: Colors.white.withOpacity(0.8),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
borderSide: BorderSide.none,
|
||
),
|
||
contentPadding: const EdgeInsets.all(20),
|
||
suffixIcon: Padding(
|
||
padding: const EdgeInsets.only(right: 8),
|
||
child: IconButton(
|
||
icon: Icon(
|
||
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
||
color: const Color(0xFF9CA3AF),
|
||
size: 22,
|
||
),
|
||
onPressed: () {
|
||
setState(() => _obscurePassword = !_obscurePassword);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
style: const TextStyle(fontSize: 16),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
// Step 3: 正在连接
|
||
Widget _buildStep3() {
|
||
return Column(
|
||
children: [
|
||
SizedBox(
|
||
height: 120,
|
||
child: Center(
|
||
child: TweenAnimationBuilder<double>(
|
||
tween: Tween(begin: 0, end: 1),
|
||
duration: const Duration(seconds: 1),
|
||
builder: (context, value, child) {
|
||
return Icon(
|
||
Icons.wifi_tethering,
|
||
size: 80 + (value * 10),
|
||
color: const Color(0xFF8B5CF6).withOpacity(1 - value * 0.5),
|
||
);
|
||
},
|
||
onEnd: () {},
|
||
),
|
||
),
|
||
),
|
||
Text(
|
||
'正在配网...',
|
||
style: GoogleFonts.outfit(
|
||
fontSize: 20,
|
||
fontWeight: FontWeight.bold,
|
||
color: const Color(0xFF1F2937),
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(3),
|
||
child: SizedBox(
|
||
height: 6,
|
||
child: LinearProgressIndicator(
|
||
value: _progress,
|
||
backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.2),
|
||
valueColor: AlwaysStoppedAnimation(
|
||
_connectFailed ? const Color(0xFFEF4444) : const Color(0xFF8B5CF6),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
_progressText,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: _connectFailed ? const Color(0xFFEF4444) : const Color(0xFF6B7280),
|
||
),
|
||
),
|
||
if (_connectFailed) ...[
|
||
const SizedBox(height: 24),
|
||
GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_currentStep = 1;
|
||
_connectFailed = false;
|
||
_progress = 0.0;
|
||
});
|
||
},
|
||
child: const Text(
|
||
'返回重新选择',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
color: Color(0xFF8B5CF6),
|
||
decoration: TextDecoration.underline,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
}
|
||
|
||
String _getDeviceIconPath() {
|
||
final type = _deviceInfo['type'] as String? ?? 'plush';
|
||
switch (type) {
|
||
case 'plush_core':
|
||
case 'plush':
|
||
return 'assets/www/icons/pixel-capybara.svg';
|
||
case 'badgeAi':
|
||
return 'assets/www/icons/pixel-badge-ai.svg';
|
||
case 'badge':
|
||
return 'assets/www/icons/pixel-badge-basic.svg';
|
||
default:
|
||
return 'assets/www/icons/pixel-capybara.svg';
|
||
}
|
||
}
|
||
|
||
// Step 4: 配网成功
|
||
Widget _buildStep4() {
|
||
return Column(
|
||
children: [
|
||
const SizedBox(height: 80),
|
||
Stack(
|
||
clipBehavior: Clip.none,
|
||
alignment: Alignment.center,
|
||
children: [
|
||
SizedBox(
|
||
width: 120,
|
||
height: 120,
|
||
child: SvgPicture.asset(
|
||
_getDeviceIconPath(),
|
||
width: 120,
|
||
height: 120,
|
||
placeholderBuilder: (_) => const Icon(
|
||
Icons.smart_toy,
|
||
size: 80,
|
||
color: Color(0xFF8B5CF6),
|
||
),
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: -5,
|
||
right: -5,
|
||
child: Container(
|
||
width: 32,
|
||
height: 32,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF22C55E),
|
||
shape: BoxShape.circle,
|
||
border: Border.all(color: Colors.white, width: 3),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: const Color(0xFF22C55E).withOpacity(0.3),
|
||
blurRadius: 10,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: const Icon(Icons.check, color: Colors.white, size: 18),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 24),
|
||
Text(
|
||
'配网成功!',
|
||
style: GoogleFonts.outfit(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
color: const Color(0xFF1F2937),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'设备已成功连接到网络',
|
||
style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildFooter() {
|
||
bool showNext = false;
|
||
String nextText = '下一步';
|
||
|
||
if (_currentStep == 1 && _selectedWifiSsid.isNotEmpty) showNext = true;
|
||
if (_currentStep == 2 && _passwordController.text.isNotEmpty) {
|
||
showNext = true;
|
||
nextText = '连接';
|
||
}
|
||
if (_currentStep == 4) {
|
||
showNext = true;
|
||
nextText = '进入设备';
|
||
}
|
||
|
||
if (!showNext && _currentStep != 3) {
|
||
return Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: TextButton(
|
||
onPressed: () {
|
||
_provService?.disconnect();
|
||
context.go('/bluetooth');
|
||
},
|
||
child: const Text(
|
||
'取消',
|
||
style: TextStyle(color: Color(0xFF6B7280)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
if (_currentStep == 3) {
|
||
return const SizedBox(height: 100);
|
||
}
|
||
|
||
return Container(
|
||
padding: EdgeInsets.fromLTRB(
|
||
32,
|
||
20,
|
||
32,
|
||
MediaQuery.of(context).padding.bottom + 40,
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
if (_currentStep < 4)
|
||
GestureDetector(
|
||
onTap: _handleBack,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 32,
|
||
vertical: 14,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.8),
|
||
borderRadius: BorderRadius.circular(25),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
),
|
||
child: const Text(
|
||
'取消',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w500,
|
||
color: Color(0xFF6B7280),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
if (_currentStep < 4) const SizedBox(width: 16),
|
||
GradientButton(
|
||
text: nextText,
|
||
onPressed: _handleNext,
|
||
height: 56,
|
||
width: _currentStep == 4 ? 200 : 160,
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|