import 'package:flutter/material.dart'; import '../models/ble_models.dart'; import '../providers/ble_provider.dart'; class BleProvisionScreen extends StatefulWidget { const BleProvisionScreen({super.key, this.provider}); final BleProvider? provider; @override State createState() => _BleProvisionScreenState(); } class _BleProvisionScreenState extends State { late final BleProvider _provider; late final bool _ownsProvider; final TextEditingController _ssidController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final GlobalKey _formKey = GlobalKey(); @override void initState() { super.initState(); _ownsProvider = widget.provider == null; _provider = widget.provider ?? BleProvider(); WidgetsBinding.instance.addPostFrameCallback((_) { _provider.startScan(); }); } @override void dispose() { _ssidController.dispose(); _passwordController.dispose(); if (_ownsProvider) { _provider.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Showen BLE 配网')), body: AnimatedBuilder( animation: _provider, builder: (BuildContext context, _) { return Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF0F172A), Color(0xFF111827)], ), ), child: SafeArea( child: ListView( padding: const EdgeInsets.all(16), children: [ _StatusBanner(provider: _provider), const SizedBox(height: 16), _ProgressCard(state: _provider.provisioningState), const SizedBox(height: 16), _buildDevicesCard(), const SizedBox(height: 16), _buildWifiCard(), const SizedBox(height: 16), _buildResultCard(), ], ), ), ); }, ), ); } Widget _buildDevicesCard() { return _GlassCard( title: '1. 扫描 Showen 设备', trailing: TextButton.icon( onPressed: _provider.isProvisioning ? null : _provider.retryScan, icon: _provider.isScanning ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), label: Text(_provider.isScanning ? '扫描中' : '重新扫描'), ), child: Column( children: [ if (_provider.devices.isEmpty) const _EmptyState( icon: Icons.bluetooth_searching, title: '未发现 Showen 设备', subtitle: '请确认设备已开机,且蓝牙广播名称包含 Showen。', ) else ..._provider.devices.map((BleDevice device) { final bool selected = _provider.selectedDevice?.id == device.id; return Padding( padding: const EdgeInsets.only(bottom: 12), child: InkWell( borderRadius: BorderRadius.circular(14), onTap: _provider.isConnecting || _provider.isProvisioning ? null : () => _handleDeviceSelected(device), child: Ink( decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? const Color(0xFF22C55E) : Colors.white24, ), color: selected ? const Color(0x1A22C55E) : const Color(0x141E293B), ), padding: const EdgeInsets.all(14), child: Row( children: [ Icon( selected ? Icons.bluetooth_connected : Icons.bluetooth, color: Colors.white, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( device.name, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( device.id, style: const TextStyle( color: Color(0xFF94A3B8), fontSize: 12, ), ), ], ), ), _SignalBadge(rssi: device.rssi), ], ), ), ), ); }), ], ), ); } Widget _buildWifiCard() { return _GlassCard( title: '2. 输入 WiFi 凭据', child: Form( key: _formKey, child: Column( children: [ TextFormField( controller: _ssidController, enabled: !_provider.isProvisioning, style: const TextStyle(color: Colors.white), decoration: _inputDecoration('WiFi SSID', Icons.wifi), validator: (String? value) { if ((value ?? '').trim().isEmpty) { return '请输入 WiFi 名称'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _passwordController, enabled: !_provider.isProvisioning, obscureText: true, style: const TextStyle(color: Colors.white), decoration: _inputDecoration('WiFi 密码', Icons.lock_outline), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: (_provider.selectedDevice == null || !_provider.isConnected || _provider.isProvisioning) ? null : _handleProvisioning, icon: _provider.isProvisioning ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.router_outlined), label: Text(_provider.isProvisioning ? '配网中...' : '开始配网'), ), ), ], ), ), ); } Widget _buildResultCard() { final BleStatus? status = _provider.latestStatus; final bool success = _provider.provisioningState == ProvisioningState.success; final bool failed = _provider.provisioningState == ProvisioningState.failed; return _GlassCard( title: '3. 配网结果', child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( success ? Icons.check_circle_outline : failed ? Icons.error_outline : Icons.info_outline, color: success ? const Color(0xFF22C55E) : failed ? const Color(0xFFF87171) : const Color(0xFF38BDF8), ), const SizedBox(width: 8), Expanded( child: Text( success ? 'WiFi 已连接成功' : failed ? (_provider.errorMessage ?? '配网失败') : '等待设备返回状态', style: const TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ], ), if (status != null) ...[ const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.white12), ), child: Text( 'action: ${status.action}\nstate: ${status.state ?? '-'}\nerror: ${status.error ?? '-'}', style: const TextStyle( color: Color(0xFFCBD5E1), height: 1.5, ), ), ), ], if (failed) ...[ const SizedBox(height: 16), OutlinedButton.icon( onPressed: _provider.retryScan, icon: const Icon(Icons.replay), label: const Text('重试'), ), ], ], ), ); } Future _handleDeviceSelected(BleDevice device) async { try { await _provider.connectToDevice(device); if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已连接 ${device.name}')), ); } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('连接失败: $error')), ); } } Future _handleProvisioning() async { if (!_formKey.currentState!.validate()) { return; } try { await _provider.provisionWifi( _ssidController.text.trim(), _passwordController.text, ); if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('设备已完成 WiFi 配网')), ); } catch (error) { if (!mounted) { return; } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('配网失败: $error')), ); } } InputDecoration _inputDecoration(String label, IconData icon) { return InputDecoration( labelText: label, labelStyle: const TextStyle(color: Color(0xFFCBD5E1)), prefixIcon: Icon(icon, color: const Color(0xFF38BDF8)), filled: true, fillColor: const Color(0x141E293B), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.white24), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFF38BDF8), width: 1.2), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFF87171)), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Color(0xFFF87171), width: 1.2), ), ); } } class _StatusBanner extends StatelessWidget { const _StatusBanner({required this.provider}); final BleProvider provider; @override Widget build(BuildContext context) { final bool connected = provider.isConnected; final String headline = connected ? '已连接 ${provider.selectedDevice?.name ?? 'Showen 设备'}' : provider.isScanning ? '正在扫描附近 Showen 设备' : '选择设备后即可开始 BLE 配网'; return Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: const LinearGradient( colors: [Color(0xFF0EA5E9), Color(0xFF22C55E)], ), boxShadow: const [ BoxShadow( color: Color(0x330EA5E9), blurRadius: 24, offset: Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'ShowenV2 BLE Provisioning', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( headline, style: const TextStyle( color: Colors.white, height: 1.5, ), ), ], ), ); } } class _ProgressCard extends StatelessWidget { const _ProgressCard({required this.state}); final ProvisioningState state; @override Widget build(BuildContext context) { final List<_StepMeta> steps = <_StepMeta>[ _StepMeta('扫描设备', state.index >= ProvisioningState.scanning.index), _StepMeta('连接设备', state.index >= ProvisioningState.connecting.index), _StepMeta('写入凭据', state.index >= ProvisioningState.writingCredentials.index), _StepMeta('连接 WiFi', state.index >= ProvisioningState.connectingWifi.index), _StepMeta('完成', state == ProvisioningState.success), ]; return _GlassCard( title: '当前进度', child: Wrap( runSpacing: 12, spacing: 12, children: steps.map(( _StepMeta step) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: step.active ? const Color(0x1A22C55E) : Colors.black.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(999), border: Border.all( color: step.active ? const Color(0xFF22C55E) : Colors.white24, ), ), child: Text( step.label, style: TextStyle( color: step.active ? const Color(0xFFF8FAFC) : const Color(0xFF94A3B8), fontWeight: FontWeight.w600, ), ), ); }).toList(), ), ); } } class _StepMeta { const _StepMeta(this.label, this.active); final String label; final bool active; } class _GlassCard extends StatelessWidget { const _GlassCard({ required this.title, required this.child, this.trailing, }); final String title; final Widget child; final Widget? trailing; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: const Color(0xB31E293B), borderRadius: BorderRadius.circular(20), border: Border.all(color: Colors.white10), ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( title, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w700, ), ), ), if (trailing != null) trailing!, ], ), const SizedBox(height: 16), child, ], ), ); } } class _SignalBadge extends StatelessWidget { const _SignalBadge({required this.rssi}); final int rssi; @override Widget build(BuildContext context) { final IconData icon = switch (rssi) { >= -60 => Icons.network_wifi, >= -75 => Icons.network_wifi_3_bar, >= -90 => Icons.network_wifi_2_bar, _ => Icons.network_wifi_1_bar, }; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: const Color(0xFF38BDF8), size: 18), const SizedBox(width: 6), Text( '$rssi dBm', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w600, ), ), ], ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState({ required this.icon, required this.title, required this.subtitle, }); final IconData icon; final String title; final String subtitle; @override Widget build(BuildContext context) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.white10), ), child: Column( children: [ Icon(icon, color: const Color(0xFF38BDF8), size: 32), const SizedBox(height: 10), Text( title, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 6), Text( subtitle, textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF94A3B8), height: 1.5), ), ], ), ); } }