253 lines
8.9 KiB
Dart
253 lines
8.9 KiB
Dart
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,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|