// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/data/models/account_device.dart'; import '../../core/data/repositories/account_repository.dart'; import '../../core/data/services/auth_controller.dart'; import '../../core/formatting/error_text.dart'; import '../../core/formatting/localized_date_time.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/plain_page_scaffold.dart'; import '../../l10n/app_localizations.dart'; import 'account_bindings.dart'; import 'account_devices_logic.dart'; const _maskVisible = 4; /// 登录设备只读列表页。 class AccountDevicesPage extends StatefulWidget { const AccountDevicesPage({super.key}); @override State createState() => _AccountDevicesPageState(); } class _AccountDevicesPageState extends State { late final AccountDevicesLogic _logic; @override void initState() { super.initState(); _logic = AccountDevicesBinding().create(context.read()); unawaited(_logic.load()); } @override void dispose() { _logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final signedIn = context.watch().isSignedIn; return Theme( data: AppTheme.dark(), child: PlainPageScaffold( pageKey: const Key('account-devices-page'), backKey: const Key('account-devices-back'), title: l10n.accountDevices, child: signedIn ? ListenableBuilder( listenable: _logic, builder: (context, _) => _body(l10n), ) : const AuthGatePanel(), ), ); } Widget _body(AppLocalizations l10n) { if (_logic.loading && _logic.devices.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (_logic.error != null && _logic.devices.isEmpty) { return EmptyState( key: const Key('account-devices-error'), icon: Icons.cloud_off_rounded, title: errorText(l10n, _logic.error, l10n.accountDevicesLoadFailed), actionLabel: l10n.retry, actionKey: const Key('account-devices-retry'), onAction: _logic.load, ); } if (_logic.devices.isEmpty) { return EmptyState( key: const Key('account-devices-empty'), icon: Icons.devices_other_rounded, title: l10n.accountDevicesEmpty, ); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 12, 16, 28), itemCount: _logic.devices.length, separatorBuilder: (_, _) => const SizedBox(height: 10), itemBuilder: (_, index) => _DeviceTile(device: _logic.devices[index], l10n: l10n), ); } } class _DeviceTile extends StatelessWidget { const _DeviceTile({required this.device, required this.l10n}); final AccountDevice device; final AppLocalizations l10n; @override Widget build(BuildContext context) { return Container( key: ValueKey('account-device-${device.id}'), padding: const EdgeInsets.all(AppDimens.cardPadding), decoration: BoxDecoration( color: AppColors.card, borderRadius: BorderRadius.circular(AppDimens.cardRadius), border: Border.all(color: AppColors.outlineVariantDark), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon( Icons.phone_android_rounded, color: AppColors.blueSoft, ), const SizedBox(width: 8), Expanded( child: Text( device.deviceModel.isEmpty ? device.platform : device.deviceModel, style: const TextStyle( fontSize: AppDimens.listTitle, fontWeight: FontWeight.w700, ), ), ), _StatusChip(status: device.status, l10n: l10n), ], ), const SizedBox(height: 10), _line(l10n.accountPlatform, device.platform), _line(l10n.accountDeviceId, _maskDeviceId(device.deviceId)), if (device.firstSeenAt != null) _line( l10n.accountFirstSeen, formatLocalizedDateTime(context, device.firstSeenAt!), ), if (device.lastSeenAt != null) _line( l10n.accountLastActive, formatLocalizedDateTime(context, device.lastSeenAt!), ), ], ), ); } Widget _line(String label, String value) => Padding( padding: const EdgeInsets.only(top: 4), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle( color: AppColors.muted, fontSize: AppDimens.caption, ), ), ), Flexible( child: Text( value.isEmpty ? '-' : value, textAlign: TextAlign.right, style: const TextStyle(fontSize: AppDimens.caption), ), ), ], ), ); } class _StatusChip extends StatelessWidget { const _StatusChip({required this.status, required this.l10n}); final String status; final AppLocalizations l10n; @override Widget build(BuildContext context) { final active = status.toLowerCase() == 'active'; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: active ? AppColors.gold.withValues(alpha: 0.16) : AppColors.raised, borderRadius: BorderRadius.circular(AppDimens.pillRadius), ), child: Text( active ? l10n.accountDeviceActive : l10n.accountDeviceInactive, style: TextStyle( color: active ? AppColors.gold : AppColors.muted, fontSize: AppDimens.caption, ), ), ); } } String _maskDeviceId(String value) { if (value.length <= _maskVisible * 2) return value; final head = value.substring(0, _maskVisible); final tail = value.substring(value.length - _maskVisible); return '$head****$tail'; }