feat(device): 完善远程操作指令并新增更多功能入口

This commit is contained in:
TongTongStudio
2026-08-24 08:41:30 +08:00
parent 98de1e1132
commit 26190c1005
9 changed files with 380 additions and 25 deletions

View File

@@ -100,18 +100,21 @@ class DeviceApi {
}
}
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位/拍照等)。
///
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`,
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate` / `take_photo`,
/// 对应接口路径 `/api/v1/client/sn/ops/{op}?sn=xxx`。
/// [extra] 为可选的附加 query 参数(如远程拍照时的 `camera`:`0` 后置 / `1` 前置)。
/// 成功返回 null;失败返回后台错误提示。
Future<String?> sendDeviceOp(String sn, String op) async {
Future<String?> sendDeviceOp(String sn, String op, {Map<String, dynamic>? extra}) async {
try {
final queryParameters = <String, dynamic>{'sn': sn};
if (extra != null) queryParameters.addAll(extra);
final data = await DioClient.post(
'v1/client/sn/ops/$op',
queryParameters: {'sn': sn},
queryParameters: queryParameters,
);
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn data=$data');
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn extra=$extra data=$data');
return null;
} on ApiException catch (e) {
debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn failed: ${e.message}');

View File

@@ -33,8 +33,8 @@ class DeviceRepositoryImpl implements DeviceRepository {
_api.getRecentScreenshots(sn: sn);
@override
Future<String?> sendDeviceOp(String sn, String op) =>
_api.sendDeviceOp(sn, op);
Future<String?> sendDeviceOp(String sn, String op, {Map<String, dynamic>? extra}) =>
_api.sendDeviceOp(sn, op, extra: extra);
@override
Future<String?> sendAppOp(String sn, String op, String packageName) =>

View File

@@ -27,11 +27,12 @@ abstract class DeviceRepository {
/// 已绑定设备最近截图列表。
Future<List<ScreenshotVO>> getRecentScreenshots(String sn);
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。
/// 向设备下发操作指令(重启/关机/截屏/刷新/定位/拍照等)。
///
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`。
/// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate` / `take_photo`。
/// [extra] 为可选的附加 query 参数(如远程拍照时的 `camera`:`0` 后置 / `1` 前置)。
/// 成功返回 null;失败返回错误提示。
Future<String?> sendDeviceOp(String sn, String op);
Future<String?> sendDeviceOp(String sn, String op, {Map<String, dynamic>? extra});
/// 对指定应用执行操作(打开/停止/卸载/清除数据)。
///

View File

@@ -1,6 +1,7 @@
import 'package:cupertino_ui/cupertino_ui.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:material_ui/material_ui.dart';
import '../../../core/utils/time_format.dart';
import '../../../core/widgets/feature_ui.dart';
@@ -576,6 +577,54 @@ class HomeTab extends ConsumerWidget {
await _runDeviceOp(context, ref, l10n, sn, op);
}
/// 远程拍照:弹窗选择摄像头(后置/前置),确认后下发拍照指令。
///
/// 后端 [DeviceService.takePhoto] 通过 `camera` 参数区分摄像头
/// (`0` 后置,`1` 前置),随指令一并下发给设备端。
Future<void> _pickCameraAndTakePhoto(
BuildContext context,
WidgetRef ref,
AppLocalizations l10n,
String? sn,
) async {
if (sn == null || sn.isEmpty) {
_showOpResult(context, l10n, l10n.opCmdFailed);
return;
}
final int? camera = await showCupertinoDialog<int>(
context: context,
builder: (dialogContext) => CupertinoAlertDialog(
title: Text(l10n.cfTakePhotoTitle),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.of(dialogContext).pop(0),
child: Text(l10n.cfCamRear),
),
CupertinoDialogAction(
onPressed: () => Navigator.of(dialogContext).pop(1),
child: Text(l10n.cfCamFront),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(l10n.cancel),
),
],
),
);
if (camera == null) return;
if (!context.mounted) return;
await _runDeviceOp(
context,
ref,
l10n,
sn,
'take_photo',
refreshScreenshots: true,
extra: {'camera': camera},
);
}
/// 下发设备操作指令并弹出结果提示。
Future<void> _runDeviceOp(
BuildContext context,
@@ -585,12 +634,13 @@ class HomeTab extends ConsumerWidget {
String op, {
bool refreshScreenshots = false,
bool refreshLocation = false,
Map<String, dynamic>? extra,
}) async {
if (sn == null || sn.isEmpty) {
_showOpResult(context, l10n, l10n.opCmdFailed);
return;
}
final error = await ref.read(deviceRepositoryProvider).sendDeviceOp(sn, op);
final error = await ref.read(deviceRepositoryProvider).sendDeviceOp(sn, op, extra: extra);
if (!context.mounted) return;
if (error == null) {
if (refreshScreenshots) ref.invalidate(recentScreenshotsProvider(sn));
@@ -647,12 +697,89 @@ class HomeTab extends ConsumerWidget {
);
}
/// 「更多」弹窗:以图标网格列出未在常用区直接展示的远程推送操作,
/// 确保后端 ClientDeviceOpsController 暴露的其余推送接口(如恢复出厂、开发者模式)
/// 在家属端 UI 也有入口,实现「每个推送接口都对接」。
Future<void> _showMoreOpsSheet(
BuildContext context,
WidgetRef ref,
AppLocalizations l10n,
String? sn,
) async {
final items = <_MoreOpItem>[
_MoreOpItem(emoji: '♻️', color: AppColors.orange, label: l10n.cfRestore, op: 'restore'),
_MoreOpItem(emoji: '🛠️', color: AppColors.blue, label: l10n.cfDeveloper, op: 'developer'),
];
if (sn == null || sn.isEmpty) {
_showOpResult(context, l10n, l10n.opCmdFailed);
return;
}
final String? op = await showCupertinoModalPopup<String>(
context: context,
builder: (ctx) => SafeArea(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
decoration: BoxDecoration(
color: Theme.of(ctx).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.cfMoreOpsTitle,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
GridView.count(
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
childAspectRatio: 0.95,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: [
for (final item in items)
IconTile(
emoji: item.emoji,
color: item.color,
label: item.label,
size: 54,
iconSize: 24,
onTap: () => Navigator.of(ctx).pop(item.op),
),
],
),
const SizedBox(height: 12),
CupertinoButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(l10n.cancel),
),
],
),
),
),
);
if (op == null || !context.mounted) return;
await _runDeviceOp(context, ref, l10n, sn, op);
}
Widget _commonCard(BuildContext context, WidgetRef ref, AppLocalizations l10n, String? sn) => CardBox(
child: Column(
children: [
Row(
children: [
Expanded(child: IconTile(emoji: '📁', color: AppColors.blue, label: l10n.cfFileTransfer)),
Expanded(
child: IconTile(
emoji: '📁',
color: AppColors.blue,
label: l10n.cfFileTransfer,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'file_transfer'),
),
),
Expanded(
child: IconTile(
emoji: '📷',
@@ -660,24 +787,129 @@ class HomeTab extends ConsumerWidget {
label: l10n.cfRemoteCam,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'take_photo',
refreshScreenshots: true),
onTap: () => _pickCameraAndTakePhoto(context, ref, l10n, sn),
),
),
Expanded(
child: IconTile(
emoji: '💬',
color: AppColors.orange,
label: l10n.cfMsgSync,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'msg_sync'),
),
),
Expanded(
child: IconTile(
emoji: '🧹',
color: AppColors.purple,
label: l10n.cfClean,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'clean'),
),
),
Expanded(child: IconTile(emoji: '💬', color: AppColors.orange, label: l10n.cfMsgSync)),
Expanded(child: IconTile(emoji: '🧹', color: AppColors.purple, label: l10n.cfClean)),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(child: IconTile(emoji: '📺', color: AppColors.red, label: l10n.cfMirror)),
Expanded(child: IconTile(emoji: '🔔', color: AppColors.teal, label: l10n.cfLost)),
Expanded(child: IconTile(emoji: '🔒', color: const Color(0xFF22C55E), label: l10n.cfAppLock)),
Expanded(child: IconTile(emoji: '➕', color: AppColors.gray, label: l10n.cfMore)),
Expanded(
child: IconTile(
emoji: '📺',
color: AppColors.red,
label: l10n.cfMirror,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'mirror'),
),
),
Expanded(
child: IconTile(
emoji: '🔔',
color: AppColors.teal,
label: l10n.cfLost,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'locate',
refreshLocation: true),
),
),
Expanded(
child: IconTile(
emoji: '🔐',
color: AppColors.blue,
label: l10n.cfScreenLock,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'screen_lock'),
),
),
Expanded(
child: IconTile(
emoji: '🔒',
color: const Color(0xFF22C55E),
label: l10n.cfAppLock,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'refresh'),
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: IconTile(
emoji: '♻️',
color: AppColors.orange,
label: l10n.cfRestore,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'restore'),
),
),
Expanded(
child: IconTile(
emoji: '🛠️',
color: AppColors.blue,
label: l10n.cfDeveloper,
size: 50,
iconSize: 20,
onTap: () => _runDeviceOp(context, ref, l10n, sn, 'developer'),
),
),
Expanded(
child: IconTile(
emoji: '⋯',
color: AppColors.gray,
label: l10n.cfMore,
size: 50,
iconSize: 20,
onTap: () => _showMoreOpsSheet(context, ref, l10n, sn),
),
),
// 占位,保持每行 4 个
const Expanded(child: SizedBox.shrink()),
],
),
],
),
);
}
class _MoreOpItem {
const _MoreOpItem({
required this.emoji,
required this.color,
required this.label,
required this.op,
});
final String emoji;
final Color color;
final String label;
final String op;
}

View File

@@ -536,12 +536,48 @@ abstract class AppLocalizations {
/// **'远程相机'**
String get cfRemoteCam;
/// 远程拍照弹窗标题
///
/// In zh, this message translates to:
/// **'选择摄像头'**
String get cfTakePhotoTitle;
/// 远程拍照:后置摄像头
///
/// In zh, this message translates to:
/// **'后置摄像头'**
String get cfCamRear;
/// 远程拍照:前置摄像头
///
/// In zh, this message translates to:
/// **'前置摄像头'**
String get cfCamFront;
/// 常用功能:更多
///
/// In zh, this message translates to:
/// **'更多'**
String get cfMore;
/// 更多操作弹窗标题
///
/// In zh, this message translates to:
/// **'更多操作'**
String get cfMoreOpsTitle;
/// 更多操作:恢复出厂设置
///
/// In zh, this message translates to:
/// **'恢复出厂设置'**
String get cfRestore;
/// 更多操作:开发者模式
///
/// In zh, this message translates to:
/// **'开发者模式'**
String get cfDeveloper;
/// 联系人页标题
///
/// In zh, this message translates to:
@@ -989,9 +1025,15 @@ abstract class AppLocalizations {
/// 常用功能:防丢失
///
/// In zh, this message translates to:
/// **'丢失提醒'**
/// **'丢失模式'**
String get cfLost;
/// 常用功能:屏幕锁定
///
/// In zh, this message translates to:
/// **'屏幕锁定'**
String get cfScreenLock;
/// 常用功能:应用锁
///
/// In zh, this message translates to:

View File

@@ -230,9 +230,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get cfRemoteCam => 'Remote Camera';
@override
String get cfTakePhotoTitle => 'Choose Camera';
@override
String get cfCamRear => 'Rear Camera';
@override
String get cfCamFront => 'Front Camera';
@override
String get cfMore => 'More';
@override
String get cfMoreOpsTitle => 'More Operations';
@override
String get cfRestore => 'Factory Reset';
@override
String get cfDeveloper => 'Developer Mode';
@override
String get contactsTitle => 'Contacts';
@@ -461,7 +479,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get cfMirror => 'Screen Mirror';
@override
String get cfLost => 'Loss Alert';
String get cfLost => 'Lost Mode';
@override
String get cfScreenLock => 'Screen Lock';
@override
String get cfAppLock => 'App Lock';

View File

@@ -229,9 +229,27 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get cfRemoteCam => '远程相机';
@override
String get cfTakePhotoTitle => '选择摄像头';
@override
String get cfCamRear => '后置摄像头';
@override
String get cfCamFront => '前置摄像头';
@override
String get cfMore => '更多';
@override
String get cfMoreOpsTitle => '更多操作';
@override
String get cfRestore => '恢复出厂设置';
@override
String get cfDeveloper => '开发者模式';
@override
String get contactsTitle => '联系人';
@@ -457,7 +475,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get cfMirror => '屏幕镜像';
@override
String get cfLost => '丢失提醒';
String get cfLost => '丢失模式';
@override
String get cfScreenLock => '屏幕锁定';
@override
String get cfAppLock => '应用锁';

View File

@@ -73,7 +73,13 @@
"menuApps": "Apps",
"menuMore": "More",
"cfRemoteCam": "Remote Camera",
"cfTakePhotoTitle": "Choose Camera",
"cfCamRear": "Rear Camera",
"cfCamFront": "Front Camera",
"cfMore": "More",
"cfMoreOpsTitle": "More Operations",
"cfRestore": "Factory Reset",
"cfDeveloper": "Developer Mode",
"contactsTitle": "Contacts",
"contactName": "Name",
"contactNick": "Nickname (optional)",
@@ -160,7 +166,8 @@
"cfMsgSync": "Message Sync",
"cfClean": "Clean & Boost",
"cfMirror": "Screen Mirror",
"cfLost": "Loss Alert",
"cfLost": "Lost Mode",
"cfScreenLock": "Screen Lock",
"cfAppLock": "App Lock",
"msgOnline": "Device Online Alert",
"msgScreenshot": "Screenshot Sync",

View File

@@ -292,10 +292,34 @@
"@cfRemoteCam": {
"description": "常用功能:远程相机"
},
"cfTakePhotoTitle": "选择摄像头",
"@cfTakePhotoTitle": {
"description": "远程拍照弹窗标题"
},
"cfCamRear": "后置摄像头",
"@cfCamRear": {
"description": "远程拍照:后置摄像头"
},
"cfCamFront": "前置摄像头",
"@cfCamFront": {
"description": "远程拍照:前置摄像头"
},
"cfMore": "更多",
"@cfMore": {
"description": "常用功能:更多"
},
"cfMoreOpsTitle": "更多操作",
"@cfMoreOpsTitle": {
"description": "更多操作弹窗标题"
},
"cfRestore": "恢复出厂设置",
"@cfRestore": {
"description": "更多操作:恢复出厂设置"
},
"cfDeveloper": "开发者模式",
"@cfDeveloper": {
"description": "更多操作:开发者模式"
},
"contactsTitle": "联系人",
"@contactsTitle": {
"description": "联系人页标题"
@@ -592,10 +616,14 @@
"@cfMirror": {
"description": "常用功能:屏幕镜像"
},
"cfLost": "丢失提醒",
"cfLost": "丢失模式",
"@cfLost": {
"description": "常用功能:防丢失"
},
"cfScreenLock": "屏幕锁定",
"@cfScreenLock": {
"description": "常用功能:屏幕锁定"
},
"cfAppLock": "应用锁",
"@cfAppLock": {
"description": "常用功能:应用锁"