Files
ttstd_family_care/lib/features/device/presentation/device_manage_page.dart
2026-08-21 17:12:17 +08:00

253 lines
8.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/widgets/feature_ui.dart';
import '../../../core/widgets/settings_ui.dart';
import '../../../l10n/app_localizations.dart';
import '../data/device_providers.dart';
import '../domain/device_models.dart';
import 'device_manage_controller.dart';
/// 设备管理页(我的 → 绑定设备 → 管理)。
///
/// 列出当前账号已绑定的全部设备,每项尾部提供「解绑」按钮;
/// 右上角提供「添加设备」入口跳转到绑定页。
class DeviceManagePage extends ConsumerWidget {
const DeviceManagePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final devicesAsync = ref.watch(myDevicesProvider);
final action = ref.watch(deviceManageControllerProvider);
// 一次性提示:展示后立即消费,避免重建时重复弹窗。
ref.listen<DeviceManageState>(deviceManageControllerProvider, (prev, next) {
final alert = next.alert;
if (alert == null) return;
ref.read(deviceManageControllerProvider.notifier).consumeAlert();
AppDialogs.alert(
context,
message: alert,
confirmText: l10n.commonConfirm,
);
});
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
if (context.canPop()) {
context.pop();
} else {
context.go('/profile');
}
},
child: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(l10n.deviceManageTitle),
leading: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.back),
onPressed: () => context.pop(),
),
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.add, size: 26),
onPressed: () => context.push('/device/bind'),
),
),
child: SafeArea(
child: Stack(
children: [
ListView(
padding: const EdgeInsets.only(bottom: 24),
children: [
SectionTitle(l10n.deviceBound),
devicesAsync.when(
loading: () => const CardBox(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(child: CupertinoActivityIndicator()),
),
),
error: (e, _) => CardBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.deviceLoadFailed,
style: const TextStyle(fontSize: 13, color: AppColors.sub),
),
const SizedBox(height: 10),
// 局部错误:内联展示 + retry见 AGENTS.md §12
GestureDetector(
onTap: () => ref.invalidate(myDevicesProvider),
child: Text(
l10n.commonRetry,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppColors.green,
),
),
),
],
),
),
data: (devices) => devices.isEmpty
? CardBox(
child: Text(
l10n.deviceNone,
style: const TextStyle(fontSize: 13, color: AppColors.sub),
),
)
: CardBox(
padding: EdgeInsets.zero,
child: Column(
children: [
for (var i = 0; i < devices.length; i++) ...[
if (i > 0) const AppDivider(),
_DeviceRow(
device: devices[i],
onUnbind: () =>
_confirmUnbind(context, ref, devices[i], l10n),
),
],
],
),
),
),
const SizedBox(height: 8),
SecondaryButton(
text: l10n.deviceManageAdd,
icon: CupertinoIcons.add_circled,
onPressed: () => context.push('/device/bind'),
),
],
),
// 解绑请求进行中:全屏蒙层拦截重复点击。
if (action.submitting)
const Positioned.fill(
child: ColoredBox(
color: Color(0x33000000),
child: Center(child: CupertinoActivityIndicator(radius: 14)),
),
),
],
),
),
),
);
}
/// 解绑二次确认,确认后调用控制器并提示结果。
Future<void> _confirmUnbind(
BuildContext context,
WidgetRef ref,
DeviceBrief device,
AppLocalizations l10n,
) async {
final name = device.snName?.isNotEmpty == true ? device.snName! : device.serialno;
final confirmed = await AppDialogs.confirm(
context,
title: l10n.deviceUnbindConfirmTitle,
message: l10n.deviceUnbindConfirmMessage(name),
confirmText: l10n.deviceUnbind,
cancelText: l10n.commonCancel,
);
if (!confirmed) return;
final ok = await ref
.read(deviceManageControllerProvider.notifier)
.unbind(device.serialno);
if (!ok || !context.mounted) return;
await AppDialogs.alert(
context,
message: l10n.deviceUnbindSuccess,
confirmText: l10n.commonConfirm,
);
}
}
/// 设备列表项:左侧图标 + 名称/SN右侧解绑按钮。
class _DeviceRow extends StatelessWidget {
const _DeviceRow({required this.device, required this.onUnbind});
final DeviceBrief device;
final VoidCallback onUnbind;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final title =
device.snName?.isNotEmpty == true ? device.snName! : device.serialno;
final subtitle = device.snModel?.isNotEmpty == true
? '${device.serialno} · ${device.snModel}'
: device.serialno;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: AppColors.green,
borderRadius: BorderRadius.circular(11),
),
child: const Center(child: Text('📱', style: TextStyle(fontSize: 17))),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14.5,
fontWeight: FontWeight.w600,
color: AppColors.ink,
),
),
const SizedBox(height: 2),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: AppColors.sub),
),
],
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: onUnbind,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
decoration: BoxDecoration(
color: const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(18),
border: Border.all(color: AppColors.red.withValues(alpha: 0.4)),
),
child: Text(
l10n.deviceUnbind,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w700,
color: AppColors.red,
),
),
),
),
],
),
);
}
}