import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../models/store_info.dart'; import '../../services/device_service.dart'; import '../../services/org_service.dart'; import '../../services/api_client.dart'; import '../../utils/formatters.dart'; import '../../utils/i10n.dart'; /// 激活码管理页:门店选择、激活码信息、生成/重置。 class DeviceActivationPage extends ConsumerStatefulWidget { const DeviceActivationPage({super.key}); @override ConsumerState createState() => _DeviceActivationPageState(); } class _DeviceActivationPageState extends ConsumerState { final OrgService _orgService = OrgService(); final DeviceService _deviceService = DeviceService(); AppLocalizations get t => AppLocalizations.of(context); List _stores = []; String? _selectedStoreId; bool _loadingStores = true; Map? _codeInfo; bool _loading = false; String? _error; @override void initState() { super.initState(); _loadStores(); } Future _loadStores() async { try { final stores = await _orgService.getMyStores(); if (!mounted) return; setState(() { _stores = stores; _loadingStores = false; if (stores.isNotEmpty) { _selectedStoreId = stores.first.orgId; } }); if (_selectedStoreId != null) { _loadCodeInfo(); } } catch (e) { if (!mounted) return; setState(() => _loadingStores = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context).storeListLoadFailed(e.toString()))), ); } } Future _loadCodeInfo() async { if (_selectedStoreId == null) return; setState(() { _loading = true; _error = null; }); try { final info = await _deviceService.getActivationCodeInfo( _selectedStoreId!, ); if (!mounted) return; setState(() { _codeInfo = info; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = '$e'; _loading = false; }); } } void _showGenerateSheet() { if (_selectedStoreId == null) return; showModalBottomSheet( useSafeArea: true, context: context, isScrollControlled: true, builder: (ctx) => _GenerateCodeSheet( deviceService: _deviceService, orgId: _selectedStoreId!, onSaved: () { Navigator.pop(ctx); _loadCodeInfo(); }, ), ); } Future _resetCode() async { if (_selectedStoreId == null) return; final t = AppLocalizations.of(context); final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(t.resetActivationCode), content: Text(t.resetActivationCodeConfirm), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: Text(t.cancel), ), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: Colors.orange), child: Text(t.reset), ), ], ), ); if (confirmed != true) return; try { await _deviceService.resetActivationCode(_selectedStoreId!); if (!mounted) return; // 结果以页面中央弹窗展示(替代左下方 SnackBar)。 _showResultDialog( isSuccess: true, title: t.activationCodeResetSuccess, ); _loadCodeInfo(); } catch (e) { if (!mounted) return; _showResultDialog( isSuccess: false, title: t.activationCodeResetFailed(formatApiError(e)), ); } } /// 居中弹窗展示激活码重置结果(成功/失败)。 /// /// 替代原左下角 SnackBar,保证用户清晰看到操作反馈。 void _showResultDialog({ required bool isSuccess, required String title, }) { final t = AppLocalizations.of(context); showDialog( context: context, builder: (ctx) => AlertDialog( icon: Icon( isSuccess ? Icons.check_circle_outline : Icons.error_outline, color: isSuccess ? Colors.green : Colors.red, size: 42, ), title: Center(child: Text(title, textAlign: TextAlign.center)), actionsAlignment: MainAxisAlignment.center, actions: [ FilledButton( onPressed: () => Navigator.pop(ctx), child: Text(t.confirm), ), ], ), ); } @override Widget build(BuildContext context) { final t = AppLocalizations.of(context); return Scaffold( appBar: AppBar( automaticallyImplyLeading: false, title: Text(t.activationCodeManagement), ), body: Column( children: [ _buildStoreSelector(), const Divider(height: 1), Expanded(child: _buildBody()), ], ), ); } Widget _buildStoreSelector() { if (_loadingStores) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } if (_stores.isEmpty) { return Padding( padding: const EdgeInsets.all(16), child: Text(t.noStoreAvailable), ); } return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), child: DropdownButtonFormField( value: _selectedStoreId, decoration: InputDecoration( labelText: t.selectStore, border: OutlineInputBorder(), prefixIcon: Icon(Icons.store), ), items: _stores .map((s) => DropdownMenuItem( value: s.orgId, child: Text(s.name), )) .toList(), onChanged: (v) { setState(() => _selectedStoreId = v); _loadCodeInfo(); }, ), ); } Widget _buildBody() { if (_selectedStoreId == null) { return Center(child: Text(t.pleaseSelectStoreFirst)); } if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(t.activationCodeLoadFailed(_error!)), const SizedBox(height: 8), FilledButton.tonal( onPressed: _loadCodeInfo, child: Text(t.retry), ), ], ), ); } if (_codeInfo == null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.vpn_key_outlined, size: 64, color: Theme.of(context).colorScheme.outline), const SizedBox(height: 16), Text(t.noActivationCode), const SizedBox(height: 16), FilledButton.icon( onPressed: _showGenerateSheet, icon: const Icon(Icons.add), label: Text(t.generateActivationCode), ), ], ), ); } return RefreshIndicator( onRefresh: _loadCodeInfo, child: ListView( padding: const EdgeInsets.all(16), physics: const AlwaysScrollableScrollPhysics(), children: [ _buildCodeCard(), const SizedBox(height: 16), _buildInfoCard(), const SizedBox(height: 24), Row( children: [ Expanded( child: FilledButton.icon( onPressed: _showGenerateSheet, icon: const Icon(Icons.refresh), label: Text(t.generateActivationCode), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: _resetCode, style: OutlinedButton.styleFrom( foregroundColor: Colors.orange, ), icon: const Icon(Icons.restart_alt), label: Text(t.resetActivationCode), ), ), ], ), ], ), ); } Widget _buildCodeCard() { final code = _codeInfo!['code']?.toString() ?? ''; final theme = Theme.of(context); return Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.vpn_key, color: theme.colorScheme.primary, size: 20), const SizedBox(width: 8), Text(t.currentActivationCode, style: theme.textTheme.titleMedium ?.copyWith(fontWeight: FontWeight.bold)), ], ), const SizedBox(height: 16), if (code.isEmpty) Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( t.activationCodeNoCodeHint, style: const TextStyle(color: Colors.grey), ), ) else Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.colorScheme.primaryContainer, borderRadius: BorderRadius.circular(8), border: Border.all( color: theme.colorScheme.primary.withOpacity(0.4)), ), child: SelectableText( code, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, fontFamily: 'monospace', color: theme.colorScheme.onPrimaryContainer, ), ), ), if (code.isNotEmpty) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () { Clipboard.setData(ClipboardData(text: code)); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.activationCodeCopied)), ); }, icon: const Icon(Icons.copy, size: 18), label: Text(t.copy), ), ], ), ], ], ), ), ); } Widget _buildInfoCard() { final theme = Theme.of(context); final maxCount = _codeInfo!['maxActivateCount']; final activatedCount = _codeInfo!['activatedCount']; final createTime = _codeInfo!['createTime']; String _formatTime(dynamic t) { if (t == null) return '—'; final text = t.toString().trim(); if (text.isEmpty) return '—'; final dt = DateTime.tryParse(text) ?? DateTime.tryParse(text.replaceFirst(' ', 'T')); return dt != null ? Formatters.formatDateTime(dt) : text; } return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _infoRow( theme, t.maxActivateCount, maxCount?.toString() ?? '—', ), _infoRow( theme, t.activatedCount, activatedCount?.toString() ?? '—', ), _infoRow(theme, t.createTime, _formatTime(createTime)), ], ), ), ); } Widget _infoRow(ThemeData theme, String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ SizedBox( width: 100, child: Text( label, style: theme.textTheme.bodyMedium ?.copyWith(color: theme.colorScheme.onSurfaceVariant), ), ), Expanded( child: Text(value, style: theme.textTheme.bodyMedium ?.copyWith(fontWeight: FontWeight.w600)), ), ], ), ); } } /// 生成激活码表单。 class _GenerateCodeSheet extends StatefulWidget { const _GenerateCodeSheet({ required this.deviceService, required this.orgId, required this.onSaved, }); final DeviceService deviceService; final String orgId; final VoidCallback onSaved; @override State<_GenerateCodeSheet> createState() => _GenerateCodeSheetState(); } class _GenerateCodeSheetState extends State<_GenerateCodeSheet> { final _formKey = GlobalKey(); final _countController = TextEditingController(text: '1'); bool _saving = false; @override void dispose() { _countController.dispose(); super.dispose(); } Future _save() async { final t = AppLocalizations.of(context); if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { final count = int.parse(_countController.text.trim()); await widget.deviceService.generateActivationCode( widget.orgId, count, ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.activationCodeGenerated)), ); widget.onSaved(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.generateFailedWithError(e.toString()))), ); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final t = AppLocalizations.of(context); return SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: Container( padding: const EdgeInsets.all(20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( t.generateActivationCode, style: Theme.of(context) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( t.generateActivationCodeHint, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 13, ), ), const SizedBox(height: 20), TextFormField( controller: _countController, decoration: InputDecoration( labelText: t.maxActivationCountLabel, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.looks), helperText: t.maxActivationCountHint, ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], validator: (v) { final count = int.tryParse(v ?? ''); if (count == null || count <= 0) { return t.positiveIntegerRequired; } return null; }, ), const SizedBox(height: 24), SizedBox( width: double.infinity, child: FilledButton( onPressed: _saving ? null : _save, child: _saving ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(t.generate), ), ), ], ), ), ), ); } }