import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../core/auth/auth_state.dart'; import '../../core/theme/app_theme.dart'; import '../../models/session.dart'; import '../../providers/session_provider.dart'; import '../../widgets/data_table_card.dart'; import '../../widgets/mobile_list_card.dart'; /// 设备 / 状态管理:本店在线设备/会话列表。 /// 所有登录用户只读;管理员/超级管理员可「强制下线」其他登录者。 class DeviceManagementScreen extends ConsumerWidget { const DeviceManagementScreen({super.key}); static final _fmt = DateFormat('MM-dd HH:mm'); @override Widget build(BuildContext context, WidgetRef ref) { final asyncSessions = ref.watch(sessionListProvider); final role = ref.watch(authStateProvider.select((s) => s.user?.role)); final canKick = role == 'admin' || role == 'superadmin'; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(24, 20, 24, 8), child: Row( children: [ const Text('设备 / 状态管理', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(width: 12), Text( canKick ? '管理员可强制下线其他登录设备' : '仅查看,无操作权限', style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary), ), const Spacer(), IconButton( tooltip: '刷新', icon: const Icon(Icons.refresh, size: 20), onPressed: () => ref.read(sessionListProvider.notifier).reload(), ), ], ), ), Expanded( child: asyncSessions.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 40, color: AppTheme.textSecondary), const SizedBox(height: 12), Text('加载失败:$e', style: const TextStyle(color: AppTheme.textSecondary)), const SizedBox(height: 12), ElevatedButton( onPressed: () => ref.read(sessionListProvider.notifier).reload(), child: const Text('重试'), ), ], ), ), data: (sessions) => _buildTable(context, ref, sessions, canKick), ), ), ], ); } Widget _buildTable(BuildContext context, WidgetRef ref, List sessions, bool canKick) { Widget onlineBadge(bool online) => Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.circle, size: 9, color: online ? AppTheme.success : AppTheme.textSecondary), const SizedBox(width: 4), Text(online ? '在线' : '离线', style: TextStyle( fontSize: 13, color: online ? AppTheme.success : AppTheme.textSecondary)), ], ); String fmt(DateTime? t) => t == null ? '-' : _fmt.format(t); Widget? kickButton(DeviceSession s, {bool dense = false}) { if (!canKick) return null; if (s.isCurrent) { return const Text('当前设备', style: TextStyle(fontSize: 12, color: AppTheme.textSecondary)); } return TextButton( key: Key('btn_kick_${s.id}'), onPressed: () => _confirmKick(context, ref, s), child: Text('强制下线', style: TextStyle( fontSize: dense ? 13 : 12, color: AppTheme.danger)), ); } Widget sessionCard(DeviceSession s) { final action = kickButton(s, dense: true); return MobileListCard( title: Text(s.username.isEmpty ? '用户#${s.userId}' : s.username), subtitle: Text('${s.platformLabel} · ${s.platformClassLabel}'), trailing: onlineBadge(s.online), fields: [ if (s.deviceName.isNotEmpty) MobileCardField('设备', s.deviceName), if (s.ip.isNotEmpty) MobileCardField('IP', s.ip), MobileCardField('登录时间', fmt(s.createdAt)), MobileCardField('最近活跃', fmt(s.lastSeenAt)), if (s.isCurrent) const MobileCardField('备注', '当前设备'), ], actions: action == null ? null : [action], ); } return DataTableCard( mobileCards: sessions.map(sessionCard).toList(), columns: const [ DataColumn(label: Text('用户')), DataColumn(label: Text('平台')), DataColumn(label: Text('设备')), DataColumn(label: Text('IP')), DataColumn(label: Text('登录时间')), DataColumn(label: Text('最近活跃')), DataColumn(label: Text('状态')), DataColumn(label: Text('操作')), ], rows: sessions.isEmpty ? [ const DataRow(cells: [ DataCell(Text('暂无在线设备', style: TextStyle(color: AppTheme.textSecondary))), DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), DataCell(SizedBox()), ]) ] : sessions.map((s) { return DataRow(cells: [ DataCell(Row( mainAxisSize: MainAxisSize.min, children: [ Text(s.username.isEmpty ? '用户#${s.userId}' : s.username, style: const TextStyle(fontWeight: FontWeight.w500)), if (s.isCurrent) ...[ const SizedBox(width: 6), Container( padding: const EdgeInsets.symmetric( horizontal: 6, vertical: 1), decoration: BoxDecoration( color: AppTheme.primary.withAlpha(26), borderRadius: BorderRadius.circular(4), ), child: const Text('本机', style: TextStyle( fontSize: 11, color: AppTheme.primary)), ), ], ], )), DataCell(Text('${s.platformLabel} · ${s.platformClassLabel}')), DataCell(Text(s.deviceName.isEmpty ? '-' : s.deviceName)), DataCell(Text(s.ip.isEmpty ? '-' : s.ip, style: const TextStyle( fontFamily: 'monospace', fontSize: 12, color: AppTheme.textSecondary))), DataCell(Text(fmt(s.createdAt))), DataCell(Text(fmt(s.lastSeenAt))), DataCell(onlineBadge(s.online)), DataCell(kickButton(s) ?? const SizedBox()), ]); }).toList(), ); } Future _confirmKick( BuildContext context, WidgetRef ref, DeviceSession s) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('强制下线'), content: Text( '确认将「${s.username.isEmpty ? '用户#${s.userId}' : s.username}」的' '${s.platformLabel}设备下线?该设备约 30 秒内退出登录。'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('取消'), ), ElevatedButton( onPressed: () => Navigator.of(ctx).pop(true), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.danger, foregroundColor: Colors.white), child: const Text('强制下线'), ), ], ), ); if (confirmed != true) return; try { await ref.read(sessionListProvider.notifier).forceLogout(s.id); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('已下线'), backgroundColor: AppTheme.success)); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('操作失败:$e'), backgroundColor: AppTheme.danger)); } } } }