Compare commits
2 Commits
3547f80383
...
c8b597f683
| Author | SHA1 | Date | |
|---|---|---|---|
| c8b597f683 | |||
| a1e01da241 |
@@ -16,6 +16,9 @@ class BleManager {
|
|||||||
BluetoothCharacteristic? _writeCharacteristic;
|
BluetoothCharacteristic? _writeCharacteristic;
|
||||||
BluetoothCharacteristic? _readCharacteristic;
|
BluetoothCharacteristic? _readCharacteristic;
|
||||||
|
|
||||||
|
/// 🔥 已连接设备的名称(连接时从扫描结果或设备属性保存,断开时清空)
|
||||||
|
String? _connectedDeviceName;
|
||||||
|
|
||||||
/// 有状态的协议解析器(支持 BLE 分片)
|
/// 有状态的协议解析器(支持 BLE 分片)
|
||||||
final ProtocolParser _parser = ProtocolParser();
|
final ProtocolParser _parser = ProtocolParser();
|
||||||
|
|
||||||
@@ -52,11 +55,19 @@ class BleManager {
|
|||||||
bool get isConnected => _connectedDevice != null;
|
bool get isConnected => _connectedDevice != null;
|
||||||
BluetoothDevice? get connectedDevice => _connectedDevice;
|
BluetoothDevice? get connectedDevice => _connectedDevice;
|
||||||
BluetoothDevice? get connectingDevice => _connectingDevice;
|
BluetoothDevice? get connectingDevice => _connectingDevice;
|
||||||
|
|
||||||
|
/// 🔥 获取已连接设备的名称(连接时缓存,断开后为 null)
|
||||||
|
String? get connectedDeviceName => _connectedDeviceName;
|
||||||
Stream<List<ScanResult>> get scanResults => _scanController.stream;
|
Stream<List<ScanResult>> get scanResults => _scanController.stream;
|
||||||
Stream<BlePacket> get packetStream => _packetController.stream;
|
Stream<BlePacket> get packetStream => _packetController.stream;
|
||||||
Stream<BluetoothAdapterState> get adapterState =>
|
Stream<BluetoothAdapterState> get adapterState =>
|
||||||
FlutterBluePlus.adapterState;
|
FlutterBluePlus.adapterState;
|
||||||
|
|
||||||
|
/// 🔥 获取当前蓝牙扫描结果列表(供外部查询使用)
|
||||||
|
List<ScanResult> getScanResults() {
|
||||||
|
return _scanResults.values.toList();
|
||||||
|
}
|
||||||
|
|
||||||
/// 连接状态变化流:连接成功时发出 device,断开时发出 null
|
/// 连接状态变化流:连接成功时发出 device,断开时发出 null
|
||||||
Stream<BluetoothDevice?> get connectionStream => _connectionController.stream;
|
Stream<BluetoothDevice?> get connectionStream => _connectionController.stream;
|
||||||
|
|
||||||
@@ -241,6 +252,18 @@ class BleManager {
|
|||||||
|
|
||||||
/// 连接设备,返回 null 表示成功,返回错误信息字符串表示失败
|
/// 连接设备,返回 null 表示成功,返回错误信息字符串表示失败
|
||||||
Future<String?> connect(BluetoothDevice device) async {
|
Future<String?> connect(BluetoothDevice device) async {
|
||||||
|
// 🔥 连接前先保存设备名(从扫描结果或设备属性中获取)
|
||||||
|
final scanResult = _scanResults[device.remoteId];
|
||||||
|
final advName = scanResult?.device.advName ?? '';
|
||||||
|
final platformName = device.platformName;
|
||||||
|
_connectedDeviceName = platformName.isNotEmpty
|
||||||
|
? platformName
|
||||||
|
: (advName.isNotEmpty ? advName : device.remoteId.toString());
|
||||||
|
developer.log(
|
||||||
|
'[BLE] 🔥 保存已连接设备名: $_connectedDeviceName',
|
||||||
|
name: 'BleManager',
|
||||||
|
);
|
||||||
|
|
||||||
// 标记正在连接,通知所有监听者
|
// 标记正在连接,通知所有监听者
|
||||||
_connectingDevice = device;
|
_connectingDevice = device;
|
||||||
_connectingController.add(device);
|
_connectingController.add(device);
|
||||||
@@ -328,6 +351,7 @@ class BleManager {
|
|||||||
void _onDeviceDisconnected() {
|
void _onDeviceDisconnected() {
|
||||||
_connectedDevice = null;
|
_connectedDevice = null;
|
||||||
_connectingDevice = null;
|
_connectingDevice = null;
|
||||||
|
_connectedDeviceName = null; // 🔥 清空已连接设备名
|
||||||
_writeCharacteristic = null;
|
_writeCharacteristic = null;
|
||||||
_readCharacteristic = null;
|
_readCharacteristic = null;
|
||||||
_negotiatedMtu = 23;
|
_negotiatedMtu = 23;
|
||||||
|
|||||||
4
lib/core/env/env_config.dart
vendored
4
lib/core/env/env_config.dart
vendored
@@ -24,4 +24,8 @@ class EnvConfig {
|
|||||||
// ─── HTTP 接口 ───
|
// ─── HTTP 接口 ───
|
||||||
static int get httpPort => isProduction ? 8081 : 59003;
|
static int get httpPort => isProduction ? 8081 : 59003;
|
||||||
static String get baseUrl => 'http://$serverHost:$httpPort';
|
static String get baseUrl => 'http://$serverHost:$httpPort';
|
||||||
|
|
||||||
|
// ─── MQTT(一方后端:任务消息 / 割草机实时状态,prod=1883 / test=59007)───
|
||||||
|
// 注:第三方无人机 OSD(droneOsd,WebSocket 8083)不纳入环境管理,保持硬编码
|
||||||
|
static int get mqttPort => isProduction ? 1883 : 59007;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
import '../../../../env/env_config.dart';
|
||||||
|
|
||||||
/// MQTT 传输协议类型
|
/// MQTT 传输协议类型
|
||||||
enum MqttProtocol {
|
enum MqttProtocol {
|
||||||
/// 纯 TCP 连接(原生 MQTT)
|
/// 纯 TCP 连接(原生 MQTT)
|
||||||
@@ -67,11 +69,11 @@ class MqttConfig extends Equatable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 任务状态消息(TCP MQTT)
|
/// 任务状态消息(TCP MQTT)——端口随环境切换(prod=1883 / test=59007)
|
||||||
factory MqttConfig.taskMessage() {
|
factory MqttConfig.taskMessage() {
|
||||||
return const MqttConfig(
|
return MqttConfig(
|
||||||
host: '1.95.137.212',
|
host: EnvConfig.serverHost,
|
||||||
port: 1883,
|
port: EnvConfig.mqttPort,
|
||||||
username: 'maibu',
|
username: 'maibu',
|
||||||
password: 'jsmbzn520',
|
password: 'jsmbzn520',
|
||||||
protocol: MqttProtocol.tcp,
|
protocol: MqttProtocol.tcp,
|
||||||
@@ -81,12 +83,12 @@ class MqttConfig extends Equatable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔥 割草机机器状态(TCP MQTT)——独立连接,地址 1.95.137.212:59007
|
/// 🔥 割草机机器状态(TCP MQTT)——独立连接,端口随环境切换(prod=1883 / test=59007)
|
||||||
/// 仅用于 mower/{sn}/property/realtime/post 和 mower/{sn}/property/location/post
|
/// 仅用于 mower/{sn}/property/realtime/post 和 mower/{sn}/property/location/post
|
||||||
factory MqttConfig.mowerRealtime() {
|
factory MqttConfig.mowerRealtime() {
|
||||||
return const MqttConfig(
|
return MqttConfig(
|
||||||
host: '1.95.137.212',
|
host: EnvConfig.serverHost,
|
||||||
port: 59007,
|
port: EnvConfig.mqttPort,
|
||||||
username: 'maibu',
|
username: 'maibu',
|
||||||
password: 'jsmbzn520',
|
password: 'jsmbzn520',
|
||||||
protocol: MqttProtocol.tcp,
|
protocol: MqttProtocol.tcp,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:convert';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:xml/xml.dart';
|
import 'package:xml/xml.dart';
|
||||||
|
|
||||||
|
import '../../../../core/consts/http_api_consts.dart';
|
||||||
import '../../../../core/di/injection.dart';
|
import '../../../../core/di/injection.dart';
|
||||||
import '../../../../core/storage/user_storage.dart';
|
import '../../../../core/storage/user_storage.dart';
|
||||||
import '../../domain/repositories/path_repository.dart';
|
import '../../domain/repositories/path_repository.dart';
|
||||||
@@ -47,7 +48,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
required String userId,
|
required String userId,
|
||||||
required String jsonData,
|
required String jsonData,
|
||||||
}) async {
|
}) async {
|
||||||
final url = Uri.parse('http://1.95.137.212:59003/iot/workRecord/add');
|
final url = Uri.parse('${HttpApiConsts.baseUrl}/iot/workRecord/add');
|
||||||
final headers = {'Content-Type': 'application/json'};
|
final headers = {'Content-Type': 'application/json'};
|
||||||
final body = jsonEncode({
|
final body = jsonEncode({
|
||||||
'workName': workName,
|
'workName': workName,
|
||||||
@@ -74,7 +75,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final url = Uri.parse(
|
final url = Uri.parse(
|
||||||
'http://1.95.137.212:59003/iot/workRecord/selectByUserId',
|
'${HttpApiConsts.baseUrl}/iot/workRecord/selectByUserId',
|
||||||
).replace(queryParameters: {'userId': userId, '_t': timestamp.toString()});
|
).replace(queryParameters: {'userId': userId, '_t': timestamp.toString()});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -103,7 +104,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final url =
|
final url =
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'http://1.95.137.212:59003/iot/workRecord/deleteByWorkName',
|
'${HttpApiConsts.baseUrl}/iot/workRecord/deleteByWorkName',
|
||||||
).replace(
|
).replace(
|
||||||
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
||||||
);
|
);
|
||||||
@@ -132,7 +133,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final url =
|
final url =
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'http://1.95.137.212:59003/iot/workRecord/selectByWorkName',
|
'${HttpApiConsts.baseUrl}/iot/workRecord/selectByWorkName',
|
||||||
).replace(
|
).replace(
|
||||||
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
queryParameters: {'workName': workName, '_t': timestamp.toString()},
|
||||||
);
|
);
|
||||||
@@ -268,7 +269,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
final url =
|
final url =
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
'http://1.95.137.212:59003/iot/workRecord/selectBySiteId',
|
'${HttpApiConsts.baseUrl}/iot/workRecord/selectBySiteId',
|
||||||
).replace(
|
).replace(
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'siteId': siteId.toString(),
|
'siteId': siteId.toString(),
|
||||||
@@ -508,7 +509,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 鍒涘缓璁惧<E79281>浠诲姟锛堥€氳繃鎺ュ彛鎵ц<E98EB5>浣滀笟锛?
|
/// 鍒涘缓璁惧<E79281>浠诲姟锛堥€氳繃鎺ュ彛鎵ц<E98EB5>浣滀笟锛?
|
||||||
/// 鎺ュ彛鍦板潃: http://1.95.137.212:59003/iot/deviceTask/createDeviceTask
|
/// 鎺ュ彛鍦板潃: ${HttpApiConsts.baseUrl}/iot/deviceTask/createDeviceTask
|
||||||
/// 鍏ュ弬: {"deviceId":"...","routeId":76,"siteId":22,"orgId":5}
|
/// 鍏ュ弬: {"deviceId":"...","routeId":76,"siteId":22,"orgId":5}
|
||||||
/// 杩斿洖: 鍒涘缓鎴愬姛鐨勪换鍔<E68DA2>D
|
/// 杩斿洖: 鍒涘缓鎴愬姛鐨勪换鍔<E68DA2>D
|
||||||
@override
|
@override
|
||||||
@@ -531,7 +532,7 @@ class PathRepositoryImpl implements PathRepository {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await dio.post(
|
final response = await dio.post(
|
||||||
'http://1.95.137.212:59003/iot/deviceTask/createDeviceTask',
|
'${HttpApiConsts.baseUrl}/iot/deviceTask/createDeviceTask',
|
||||||
data: body,
|
data: body,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
|||||||
taskPool: taskList,
|
taskPool: taskList,
|
||||||
currentTask: currentTask,
|
currentTask: currentTask,
|
||||||
currentTaskId: currentTask?.id,
|
currentTaskId: currentTask?.id,
|
||||||
|
// 🔥 活跃任务为空时必须显式清空,否则 copyWith 会保留旧任务导致卡片一直显示
|
||||||
|
clearCurrentTask: currentTask == null,
|
||||||
|
clearCurrentTaskId: currentTask == null,
|
||||||
activeTasks: activeTasks,
|
activeTasks: activeTasks,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -398,7 +401,7 @@ class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
|||||||
|
|
||||||
/// 清除当前任务
|
/// 清除当前任务
|
||||||
void clearCurrentTask() {
|
void clearCurrentTask() {
|
||||||
emit(state.copyWith(currentTask: null, currentTaskId: null));
|
emit(state.copyWith(clearCurrentTask: true, clearCurrentTaskId: true));
|
||||||
_logger.logWithLevel('🧹 清除当前任务');
|
_logger.logWithLevel('🧹 清除当前任务');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ class DeviceTaskState extends Equatable {
|
|||||||
DeviceTaskState copyWith({
|
DeviceTaskState copyWith({
|
||||||
List<DeviceTaskEntity>? taskPool,
|
List<DeviceTaskEntity>? taskPool,
|
||||||
DeviceTaskEntity? currentTask,
|
DeviceTaskEntity? currentTask,
|
||||||
|
bool clearCurrentTask = false, // 🔥 显式清空 currentTask(copyWith 传 null 无法置空)
|
||||||
int? currentTaskId,
|
int? currentTaskId,
|
||||||
|
bool clearCurrentTaskId = false, // 🔥 显式清空 currentTaskId
|
||||||
List<DeviceTaskEntity>? activeTasks, // 🔥 新增
|
List<DeviceTaskEntity>? activeTasks, // 🔥 新增
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
@@ -41,8 +43,9 @@ class DeviceTaskState extends Equatable {
|
|||||||
}) {
|
}) {
|
||||||
return DeviceTaskState(
|
return DeviceTaskState(
|
||||||
taskPool: taskPool ?? this.taskPool,
|
taskPool: taskPool ?? this.taskPool,
|
||||||
currentTask: currentTask ?? this.currentTask,
|
currentTask: clearCurrentTask ? null : (currentTask ?? this.currentTask),
|
||||||
currentTaskId: currentTaskId ?? this.currentTaskId,
|
currentTaskId:
|
||||||
|
clearCurrentTaskId ? null : (currentTaskId ?? this.currentTaskId),
|
||||||
activeTasks: activeTasks ?? this.activeTasks, // 🔥 新增
|
activeTasks: activeTasks ?? this.activeTasks, // 🔥 新增
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
|
|
||||||
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
|
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
|
||||||
double _headingAngle = 0.0; // 当前机器航向角(单位:度)
|
double _headingAngle = 0.0; // 当前机器航向角(单位:度)
|
||||||
|
|
||||||
|
// 🔥 地图 zoom 日志节流:上次已打印过的 zoom 值(变化 ≥ 0.1 才再打)
|
||||||
|
double? _lastLoggedZoom;
|
||||||
List typedPathList =
|
List typedPathList =
|
||||||
<work_area_model.DeviceAddPathPointModel>[]; // 存储生成路径的坐标列表(已转换为LatLng)
|
<work_area_model.DeviceAddPathPointModel>[]; // 存储生成路径的坐标列表(已转换为LatLng)
|
||||||
|
|
||||||
@@ -274,6 +277,34 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 新增:zoom 变化日志(节流)
|
||||||
|
// 用途:方便开发者手动 pinch/双击缩放到“理想级别”,从日志里读出 zoom 数值,
|
||||||
|
// 反向调整 fitBounds 的 maxZoom(或其他缩放相关参数)。
|
||||||
|
// 节流策略:只有 zoom 变化 ≥ 0.1 才打一次,避免拖拽/缩放过程中刷屏。
|
||||||
|
// 事件来源(event.source)可区分“用户手势”还是“代码调用”:
|
||||||
|
// - dragUpdate / pinchZoomUpdate / doubleTapZoomAnimation* → 用户交互
|
||||||
|
// - custom → controller.move / fitBounds 等代码调用
|
||||||
|
if (mounted && _mapController.camera != null) {
|
||||||
|
final camera = _mapController.camera!;
|
||||||
|
final currentZoom = camera.zoom;
|
||||||
|
final shouldLog =
|
||||||
|
_lastLoggedZoom == null ||
|
||||||
|
(currentZoom - _lastLoggedZoom!).abs() >= 0.1;
|
||||||
|
if (shouldLog) {
|
||||||
|
final delta =
|
||||||
|
_lastLoggedZoom == null
|
||||||
|
? 'init'
|
||||||
|
: '${currentZoom > _lastLoggedZoom! ? "↑放大" : "↓缩小"} '
|
||||||
|
'${(currentZoom - _lastLoggedZoom!).toStringAsFixed(2)}';
|
||||||
|
debugPrint(
|
||||||
|
'🔍 [地图缩放] zoom=${currentZoom.toStringAsFixed(3)} ($delta), '
|
||||||
|
'center=(${camera.center.latitude.toStringAsFixed(6)}, ${camera.center.longitude.toStringAsFixed(6)}), '
|
||||||
|
'source=${event.source}',
|
||||||
|
);
|
||||||
|
_lastLoggedZoom = currentZoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🌐 场站高清图层:监听动态范围更新 + 触发一次 GetCapabilities 刷新
|
// 🌐 场站高清图层:监听动态范围更新 + 触发一次 GetCapabilities 刷新
|
||||||
@@ -687,6 +718,47 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 选中路线任务时,自动校验"上次执行的任务"是否仍然存在
|
||||||
|
/// 若任务已被取消/完成/不存在,则清除当前任务,避免面板显示过期任务卡片
|
||||||
|
Future<void> _validateCurrentTaskOnSelect() async {
|
||||||
|
try {
|
||||||
|
final targetDevice =
|
||||||
|
context.read<RemoteControlCubit>().state.targetDevice;
|
||||||
|
final deviceId = targetDevice?.deviceName;
|
||||||
|
if (deviceId == null || deviceId.isEmpty) return;
|
||||||
|
|
||||||
|
final taskCubit = sl<DeviceTaskCubit>();
|
||||||
|
|
||||||
|
// 优先取内存中的 taskId,其次从本地恢复(仅用于存在性校验,不提前构造卡片,避免闪现假任务)
|
||||||
|
var previousTaskId = taskCubit.state.currentTaskId;
|
||||||
|
if (previousTaskId == null) {
|
||||||
|
previousTaskId = await _restoreTaskIdFromLocal(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有历史任务,无需校验,也不展示卡片
|
||||||
|
if (previousTaskId == null) return;
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'🔄 [选中校验] 校验上次任务是否存在, deviceId=$deviceId, taskId=$previousTaskId',
|
||||||
|
);
|
||||||
|
await taskCubit.fetchAndFilterTask(deviceId);
|
||||||
|
|
||||||
|
// 🔥 查询后如果上次的任务已不在活跃任务中,清除当前任务,卡片不再展示
|
||||||
|
final stillExists = taskCubit.state.activeTasks.any(
|
||||||
|
(t) => t.id == previousTaskId,
|
||||||
|
);
|
||||||
|
if (!stillExists) {
|
||||||
|
debugPrint('🔄 [选中校验] 任务已不存在,清除当前任务与本地缓存');
|
||||||
|
taskCubit.clearCurrentTask();
|
||||||
|
await _clearTaskIdFromLocal(deviceId);
|
||||||
|
} else {
|
||||||
|
debugPrint('🔄 [选中校验] 任务仍存在,保留显示');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('🔄 [选中校验] 校验失败: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 新增:计算坐标列表的边界范围 ==========
|
// ========== 新增:计算坐标列表的边界范围 ==========
|
||||||
LatLngBounds? calculateBounds(List<LatLng> points) {
|
LatLngBounds? calculateBounds(List<LatLng> points) {
|
||||||
if (points.isEmpty) return null;
|
if (points.isEmpty) return null;
|
||||||
@@ -1833,6 +1905,9 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
_selectedPlot = plot;
|
_selectedPlot = plot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔥 选中任务时自动校验上次的任务是否还存在:不存在则不展示(等同手动刷新)
|
||||||
|
_validateCurrentTaskOnSelect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_traceManager.reset();
|
_traceManager.reset();
|
||||||
|
|
||||||
@@ -2081,7 +2156,25 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
'🗺️ [选中地块] 准备移动地图: allPoints=${allPoints.length}, gcjPathPoints=${gcjPathPoints.length}, gcjOuterPoints=${gcjOuterPoints.length}',
|
'🗺️ [选中地块] 准备移动地图: allPoints=${allPoints.length}, gcjPathPoints=${gcjPathPoints.length}, gcjOuterPoints=${gcjOuterPoints.length}',
|
||||||
);
|
);
|
||||||
if (allPoints.isNotEmpty) {
|
if (allPoints.isNotEmpty) {
|
||||||
moveMapToPointsCenter(allPoints);
|
// 🔥 选中路线时自动定位到路径区域:
|
||||||
|
// - center = bounds.center = 金黄线(gcjPathPoints)+ 灰色边框(gcjOuterPoints)所有点的
|
||||||
|
// bounding box 几何中心,保证路径永远居中,不会因屏幕当前位置而偏移
|
||||||
|
// - zoom = 21.0 固定值(开发者实测的理想级别):不管路径大小都缩到同一级别,视觉一致
|
||||||
|
// - ⚠️ zoom=21 超过 ESRI 底图 19 级上限,底图可能白或靠场站高清叠加层显示(开发者已确认可接受)
|
||||||
|
// - 一次性操作,不锁交互:用户之后仍可自由 pinch/drag 缩放平移
|
||||||
|
final bounds = calculateBounds(allPoints);
|
||||||
|
if (bounds != null) {
|
||||||
|
const double targetZoom = 21.0;
|
||||||
|
final zoomBefore = _mapController.camera?.zoom;
|
||||||
|
_mapController.move(bounds.center, targetZoom);
|
||||||
|
debugPrint(
|
||||||
|
'🗺️ [选中地块] move 完成: zoom=$zoomBefore → $targetZoom, '
|
||||||
|
'center=${bounds.center} (路径几何中心)',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ [选中地块] bounds 计算失败,回退到仅居中(保持原缩放)');
|
||||||
|
moveMapToPointsCenter(allPoints);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('❌ [选中地块] allPoints 为空,无法移动地图!');
|
debugPrint('❌ [选中地块] allPoints 为空,无法移动地图!');
|
||||||
}
|
}
|
||||||
@@ -3262,8 +3355,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
|||||||
gctracePoint?.clear();
|
gctracePoint?.clear();
|
||||||
tracePoint = [];
|
tracePoint = [];
|
||||||
gctracePoint = [];
|
gctracePoint = [];
|
||||||
|
// 🔥 取消任务后消除规划路径显示,只保留原先的边界框(gcjOuterPoints)
|
||||||
|
gcjPathPoints = [];
|
||||||
});
|
});
|
||||||
debugPrint('[停止作业] 最终 setState 完成,轨迹已清空');
|
debugPrint('[停止作业] 最终 setState 完成,轨迹与路径已清空,保留边界框');
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('══════════ [停止作业] 结束 ══════════');
|
debugPrint('══════════ [停止作业] 结束 ══════════');
|
||||||
|
|||||||
@@ -50,6 +50,40 @@ class TabConfig extends Equatable {
|
|||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// roleKey == 'personal'(个人角色):底部导航与「系统设置 → Tab 设置」强制只保留
|
||||||
|
/// 「设备(device) + 我的(me)」,且常显、开关锁定不可关闭;其他角色不受影响。
|
||||||
|
static const String personalRoleKey = 'personal';
|
||||||
|
static const Set<String> personalVisibleTabIds = {'device', 'me'};
|
||||||
|
|
||||||
|
/// 底部导航实际渲染的 Tab:
|
||||||
|
/// - personal:强制返回 设备 + 我的(忽略 isEnabled,恒为启用),按 order 排序
|
||||||
|
/// - 其他角色:返回用户启用的 Tab(enabledItems)
|
||||||
|
List<TabConfigItem> navItemsForRole(String? roleKey) {
|
||||||
|
if (roleKey == personalRoleKey) {
|
||||||
|
final forced = items
|
||||||
|
.where((item) => personalVisibleTabIds.contains(item.id))
|
||||||
|
.map((item) => item.copyWith(isEnabled: true))
|
||||||
|
.toList();
|
||||||
|
forced.sort((a, b) => a.order.compareTo(b.order));
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
return enabledItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「Tab 设置」列表应展示的 Tab:
|
||||||
|
/// - personal:只列出 设备 + 我的,按 order 排序
|
||||||
|
/// - 其他角色:列出全部
|
||||||
|
List<TabConfigItem> settingItemsForRole(String? roleKey) {
|
||||||
|
if (roleKey == personalRoleKey) {
|
||||||
|
final visible = items
|
||||||
|
.where((item) => personalVisibleTabIds.contains(item.id))
|
||||||
|
.toList();
|
||||||
|
visible.sort((a, b) => a.order.compareTo(b.order));
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
TabConfig copyWith({List<TabConfigItem>? items}) {
|
TabConfig copyWith({List<TabConfigItem>? items}) {
|
||||||
return TabConfig(items: items ?? this.items);
|
return TabConfig(items: items ?? this.items);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/
|
|||||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||||
|
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||||
|
|
||||||
class MainWrapper extends StatefulWidget {
|
class MainWrapper extends StatefulWidget {
|
||||||
const MainWrapper({super.key});
|
const MainWrapper({super.key});
|
||||||
@@ -30,8 +31,6 @@ class MainWrapper extends StatefulWidget {
|
|||||||
class _MainWrapperState extends State<MainWrapper> {
|
class _MainWrapperState extends State<MainWrapper> {
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
final PageController _pageController = PageController();
|
final PageController _pageController = PageController();
|
||||||
final List<GlobalKey> _tabKeys = [];
|
|
||||||
double? _circleLeft;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -39,22 +38,6 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
|
|
||||||
// 🔥 确保 FloatBarSettingService 已初始化
|
// 🔥 确保 FloatBarSettingService 已初始化
|
||||||
GetIt.I<FloatBarSettingService>();
|
GetIt.I<FloatBarSettingService>();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_updateCirclePosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateCirclePosition() {
|
|
||||||
if (_tabKeys.isEmpty || _currentIndex >= _tabKeys.length) return;
|
|
||||||
|
|
||||||
final key = _tabKeys[_currentIndex];
|
|
||||||
final renderBox = key.currentContext?.findRenderObject() as RenderBox?;
|
|
||||||
if (renderBox != null) {
|
|
||||||
setState(() {
|
|
||||||
_circleLeft = renderBox.localToGlobal(Offset.zero).dx;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -68,52 +51,36 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
_currentIndex = index;
|
_currentIndex = index;
|
||||||
});
|
});
|
||||||
_pageController.jumpToPage(index);
|
_pageController.jumpToPage(index);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_updateCirclePosition();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IconData _getIconData(String iconName) {
|
/// Tab 图标:选中用 filled(实心),未选用 outlined(描边),对比更清晰
|
||||||
|
IconData _navIcon(String iconName, bool selected) {
|
||||||
switch (iconName) {
|
switch (iconName) {
|
||||||
case 'home_rounded':
|
case 'home_rounded':
|
||||||
return Icons.home_rounded;
|
return selected ? Icons.home_rounded : Icons.home_outlined;
|
||||||
case 'grid_view_rounded':
|
case 'grid_view_rounded':
|
||||||
return Icons.grid_view_rounded;
|
return selected ? Icons.grid_view_rounded : Icons.grid_view_outlined;
|
||||||
case 'devices_rounded':
|
case 'devices_rounded':
|
||||||
return Icons.devices_rounded;
|
return selected ? Icons.devices_rounded : Icons.devices_outlined;
|
||||||
case 'auto_awesome_rounded':
|
case 'auto_awesome_rounded':
|
||||||
return Icons.auto_awesome_rounded;
|
return selected
|
||||||
|
? Icons.auto_awesome_rounded
|
||||||
|
: Icons.auto_awesome_outlined;
|
||||||
case 'warning_amber_rounded':
|
case 'warning_amber_rounded':
|
||||||
return Icons.warning_amber_rounded;
|
return selected
|
||||||
|
? Icons.warning_amber_rounded
|
||||||
|
: Icons.warning_amber_outlined;
|
||||||
case 'assignment_rounded':
|
case 'assignment_rounded':
|
||||||
return Icons.assignment_rounded;
|
return selected ? Icons.assignment_rounded : Icons.assignment_outlined;
|
||||||
case 'post_add_rounded':
|
case 'post_add_rounded':
|
||||||
return Icons.post_add_rounded;
|
return selected ? Icons.post_add_rounded : Icons.post_add_outlined;
|
||||||
case 'person_rounded':
|
case 'person_rounded':
|
||||||
return Icons.person_rounded;
|
return selected ? Icons.person_rounded : Icons.person_outlined;
|
||||||
default:
|
default:
|
||||||
return Icons.home_rounded;
|
return selected ? Icons.home_rounded : Icons.home_outlined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getIndicatorPosition(int itemCount, int currentIndex) {
|
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
final itemWidth = screenWidth / itemCount;
|
|
||||||
return currentIndex * itemWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _getCircularLeftPosition(int itemCount, int currentIndex) {
|
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
final itemWidth = screenWidth / itemCount;
|
|
||||||
// 圆形应该在每个Tab项的中心
|
|
||||||
final tabCenterX = 12 + (currentIndex * itemWidth) + (itemWidth / 2);
|
|
||||||
final circularLeft = tabCenterX - 22; // 22 = 44/2,让圆形中心对齐Tab中心
|
|
||||||
|
|
||||||
// 边界限制
|
|
||||||
final maxLeft = screenWidth - 12 - 44;
|
|
||||||
return circularLeft.clamp(12.0, maxLeft);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCurrentPage(TabConfigItem tab) {
|
Widget _buildCurrentPage(TabConfigItem tab) {
|
||||||
switch (tab.id) {
|
switch (tab.id) {
|
||||||
case 'home_v2':
|
case 'home_v2':
|
||||||
@@ -147,6 +114,8 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// 🔥 个人角色(roleKey == 'personal')底部导航强制只显示「设备 + 我的」
|
||||||
|
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||||
return BlocBuilder<TabConfigCubit, TabConfigState>(
|
return BlocBuilder<TabConfigCubit, TabConfigState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state is! TabConfigLoaded) {
|
if (state is! TabConfigLoaded) {
|
||||||
@@ -156,7 +125,7 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final config = state.config;
|
final config = state.config;
|
||||||
final enabledItems = config.enabledItems;
|
final enabledItems = config.navItemsForRole(roleKey);
|
||||||
|
|
||||||
if (enabledItems.isEmpty) {
|
if (enabledItems.isEmpty) {
|
||||||
return const Scaffold(body: Center(child: Text('请至少启用一个 Tab')));
|
return const Scaffold(body: Center(child: Text('请至少启用一个 Tab')));
|
||||||
@@ -166,24 +135,16 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
_currentIndex = 0;
|
_currentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化 GlobalKey
|
|
||||||
if (_tabKeys.length != enabledItems.length) {
|
|
||||||
_tabKeys.clear();
|
|
||||||
for (int i = 0; i < enabledItems.length; i++) {
|
|
||||||
_tabKeys.add(GlobalKey());
|
|
||||||
}
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_updateCirclePosition();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
extendBody: false,
|
extendBody: false,
|
||||||
|
// 底部导航预留区(SafeArea 那条)与页面同色,消除突兀的底色带
|
||||||
|
backgroundColor: context.appColors.pageBackground,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
PageView(
|
PageView(
|
||||||
controller: _pageController,
|
controller: _pageController,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
// 🔥 启用左右滑动切换 Tab
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
onPageChanged: (index) {
|
onPageChanged: (index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIndex = index;
|
_currentIndex = index;
|
||||||
@@ -205,102 +166,138 @@ class _MainWrapperState extends State<MainWrapper> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
bottomNavigationBar: Container(
|
bottomNavigationBar: SafeArea(
|
||||||
decoration: BoxDecoration(
|
top: false,
|
||||||
color: context.appColors.cardBackground,
|
child: Padding(
|
||||||
),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
child: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return Container(
|
final itemCount = enabledItems.length;
|
||||||
margin: const EdgeInsets.symmetric(
|
// Tab 较少时收缩整体宽度并居中,Tab 多时铺满可用宽度
|
||||||
horizontal: 12,
|
const double preferredTabWidth = 84;
|
||||||
vertical: 16,
|
const double barPaddingH = 6;
|
||||||
),
|
final double maxBarWidth = constraints.maxWidth;
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
final double barWidth =
|
||||||
decoration: BoxDecoration(
|
itemCount * preferredTabWidth < maxBarWidth
|
||||||
color: context.appColors.cardBackground,
|
? itemCount * preferredTabWidth
|
||||||
borderRadius: BorderRadius.circular(25),
|
: maxBarWidth;
|
||||||
border: Border.all(
|
final double tabWidth =
|
||||||
color: context.appColors.divider,
|
(barWidth - barPaddingH * 2) / itemCount;
|
||||||
width: 1,
|
// 选中高亮:包裹「图标 + 文字」的紧凑胶囊 chip
|
||||||
),
|
const double indicatorH = 46;
|
||||||
boxShadow: [
|
final double indicatorW =
|
||||||
BoxShadow(
|
tabWidth - 8 < 54 ? tabWidth - 8 : 54.0;
|
||||||
color: context.appColors.cardShadow,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// 滑动圆形背景
|
|
||||||
if (_circleLeft != null)
|
|
||||||
AnimatedPositioned(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
left: _circleLeft! - 8, // 减去外层 margin + 向右偏移
|
|
||||||
top: 6,
|
|
||||||
child: Container(
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.appColors.divider,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(
|
|
||||||
color: context.appColors.divider,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Tab 项
|
|
||||||
Row(
|
|
||||||
children: enabledItems.asMap().entries.map((entry) {
|
|
||||||
final index = entry.key;
|
|
||||||
final item = entry.value;
|
|
||||||
final isSelected = _currentIndex == index;
|
|
||||||
|
|
||||||
return Expanded(
|
// 用 Align + heightFactor 只水平居中,高度贴合内容;
|
||||||
key: _tabKeys[index],
|
// 切勿用 Center:它在底部栏有界高度约束下会撑满全屏,把 body 挤没
|
||||||
child: InkWell(
|
return Align(
|
||||||
onTap: () => _onTabChanged(index),
|
alignment: Alignment.center,
|
||||||
child: SizedBox(
|
heightFactor: 1.0,
|
||||||
height: 56,
|
child: Container(
|
||||||
child: Column(
|
width: barWidth,
|
||||||
mainAxisSize: MainAxisSize.min,
|
padding: const EdgeInsets.symmetric(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
horizontal: barPaddingH,
|
||||||
children: [
|
vertical: 2,
|
||||||
Icon(
|
),
|
||||||
_getIconData(item.icon),
|
decoration: BoxDecoration(
|
||||||
size: 24,
|
color: context.appColors.cardBackground,
|
||||||
color: isSelected
|
borderRadius: BorderRadius.circular(30),
|
||||||
? context.appColors.textPrimary
|
border: Border.all(
|
||||||
: context.appColors.textTertiary,
|
color: context.appColors.divider,
|
||||||
),
|
width: 1,
|
||||||
const SizedBox(height: 4),
|
),
|
||||||
Text(
|
boxShadow: [
|
||||||
item.name,
|
BoxShadow(
|
||||||
style: TextStyle(
|
color: context.appColors.cardShadow,
|
||||||
fontSize: 11,
|
blurRadius: 12,
|
||||||
color: isSelected
|
offset: const Offset(0, 4),
|
||||||
? context.appColors.textPrimary
|
),
|
||||||
: context.appColors.textTertiary,
|
],
|
||||||
fontWeight: isSelected
|
),
|
||||||
? FontWeight.w600
|
child: SizedBox(
|
||||||
: FontWeight.w400,
|
height: 56,
|
||||||
),
|
child: Stack(
|
||||||
),
|
children: [
|
||||||
],
|
// 蓝色胶囊 chip:纯数学定位,居中包裹图标 + 文字
|
||||||
|
AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 280),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
left: _currentIndex * tabWidth +
|
||||||
|
(tabWidth - indicatorW) / 2,
|
||||||
|
top: (56 - indicatorH) / 2,
|
||||||
|
width: indicatorW,
|
||||||
|
height: indicatorH,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.appColors.primary.withOpacity(
|
||||||
|
0.12,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
indicatorH / 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}).toList(),
|
// Tab 项
|
||||||
|
Row(
|
||||||
|
children:
|
||||||
|
enabledItems.asMap().entries.map((entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final item = entry.value;
|
||||||
|
final isSelected = _currentIndex == index;
|
||||||
|
|
||||||
|
return Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
indicatorH / 2,
|
||||||
|
),
|
||||||
|
onTap: () => _onTabChanged(index),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedScale(
|
||||||
|
scale: isSelected ? 1.1 : 1.0,
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 200,
|
||||||
|
),
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
child: Icon(
|
||||||
|
_navIcon(item.icon, isSelected),
|
||||||
|
size: 22,
|
||||||
|
color: isSelected
|
||||||
|
? context.appColors.primary
|
||||||
|
: context
|
||||||
|
.appColors.textTertiary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
AnimatedDefaultTextStyle(
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 200,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
height: 1.0,
|
||||||
|
color: isSelected
|
||||||
|
? context.appColors.primary
|
||||||
|
: context
|
||||||
|
.appColors.textTertiary,
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w400,
|
||||||
|
),
|
||||||
|
child: Text(item.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class RobotDataModel {
|
|||||||
final int statusCode; // 接口原始 status 状态码
|
final int statusCode; // 接口原始 status 状态码
|
||||||
final double battery;
|
final double battery;
|
||||||
final String task;
|
final String task;
|
||||||
|
final bool hasHost;
|
||||||
|
|
||||||
const RobotDataModel({
|
const RobotDataModel({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -20,6 +21,7 @@ class RobotDataModel {
|
|||||||
this.statusCode = 0,
|
this.statusCode = 0,
|
||||||
required this.battery,
|
required this.battery,
|
||||||
required this.task,
|
required this.task,
|
||||||
|
this.hasHost = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 从 JSON 创建数据模型
|
/// 从 JSON 创建数据模型
|
||||||
@@ -46,6 +48,7 @@ class RobotDataModel {
|
|||||||
statusCode: statusCode is int ? statusCode : 0,
|
statusCode: statusCode is int ? statusCode : 0,
|
||||||
battery: batteryValue,
|
battery: batteryValue,
|
||||||
task: json['task'] ?? json['currentTask'] ?? '待机中',
|
task: json['task'] ?? json['currentTask'] ?? '待机中',
|
||||||
|
hasHost: json['hasHost'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_initConnectionListener();
|
_initConnectionListener();
|
||||||
_syncInitialState();
|
_syncInitialState();
|
||||||
|
|
||||||
|
// 🔥 页面加载完成后自动连接设备(如果尚未连接且未在连接中)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (!_isConnected && !_isConnecting) {
|
||||||
|
_connect();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 监听 BleManager 的全局连接状态流
|
/// 监听 BleManager 的全局连接状态流
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||||
import '../../../../../core/di/injection.dart';
|
import '../../../../../core/di/injection.dart';
|
||||||
@@ -11,6 +12,7 @@ import '../../../../../components/tcp_status_indicator.dart';
|
|||||||
import '../../../../../components/device_status_modal.dart';
|
import '../../../../../components/device_status_modal.dart';
|
||||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||||
import '../../../site/presentation/widgets/site_selector_widget.dart';
|
import '../../../site/presentation/widgets/site_selector_widget.dart';
|
||||||
|
import '../../../../main_container/domain/tab_config.dart';
|
||||||
import '../bloc/device_status_bloc.dart' as DeviceListBloc;
|
import '../bloc/device_status_bloc.dart' as DeviceListBloc;
|
||||||
import '../bloc/device_status_event.dart' as DeviceListEvent;
|
import '../bloc/device_status_event.dart' as DeviceListEvent;
|
||||||
import '../bloc/device_status_state.dart' as DeviceListState;
|
import '../bloc/device_status_state.dart' as DeviceListState;
|
||||||
@@ -18,6 +20,7 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_statu
|
|||||||
import '../bloc/drone_station_bloc.dart';
|
import '../bloc/drone_station_bloc.dart';
|
||||||
import '../bloc/drone_station_event.dart';
|
import '../bloc/drone_station_event.dart';
|
||||||
import '../bloc/drone_station_state.dart';
|
import '../bloc/drone_station_state.dart';
|
||||||
|
import 'ble_device_detail_page.dart';
|
||||||
import '../bloc/robot_list_bloc.dart'; // 🔥 添加 RobotListBloc 导入
|
import '../bloc/robot_list_bloc.dart'; // 🔥 添加 RobotListBloc 导入
|
||||||
import '../bloc/robot_list_event.dart'; // 🔥 添加 RobotListEvent 导入
|
import '../bloc/robot_list_event.dart'; // 🔥 添加 RobotListEvent 导入
|
||||||
import '../bloc/robot_list_state.dart'; // 🔥 添加 RobotListState 导入
|
import '../bloc/robot_list_state.dart'; // 🔥 添加 RobotListState 导入
|
||||||
@@ -74,6 +77,22 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// 🔥 个人角色(roleKey == 'personal')只有一个场站,自动默认选中第一个
|
||||||
|
final roleKey = sl<AppUserCubit>().state.user?.roleKey;
|
||||||
|
if (roleKey == TabConfig.personalRoleKey) {
|
||||||
|
final siteCubit = sl<SiteCubit>();
|
||||||
|
if (siteCubit.state.sites.isEmpty) {
|
||||||
|
// 场站列表为空则先加载
|
||||||
|
siteCubit.loadSites();
|
||||||
|
} else if (siteCubit.state.selectedSite == null) {
|
||||||
|
// 还没有选中场站则自动选中第一个
|
||||||
|
final firstSite = siteCubit.state.sites.first;
|
||||||
|
debugPrint(
|
||||||
|
'🔥 [DeviceStatus] 个人角色自动选中首个场站: ${firstSite.siteName} (id=${firstSite.id})',
|
||||||
|
);
|
||||||
|
siteCubit.selectSite(firstSite);
|
||||||
|
}
|
||||||
|
}
|
||||||
_currentSiteId = sl<SiteCubit>().state.selectedSite?.id;
|
_currentSiteId = sl<SiteCubit>().state.selectedSite?.id;
|
||||||
_siteSub = sl<SiteCubit>().stream.listen((siteState) {
|
_siteSub = sl<SiteCubit>().stream.listen((siteState) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -210,6 +229,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAppBar(BuildContext context) {
|
Widget _buildAppBar(BuildContext context) {
|
||||||
|
// 🔥 个人角色(roleKey == 'personal')隐藏场站选择器
|
||||||
|
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||||
|
final showSiteSelector = roleKey != TabConfig.personalRoleKey;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 44.0,
|
height: 44.0,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
@@ -218,8 +241,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Flexible(child: SiteSelectorWidget(compact: true)),
|
if (showSiteSelector) ...[
|
||||||
const SizedBox(width: 8),
|
const Flexible(child: SiteSelectorWidget(compact: true)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
],
|
||||||
Container(
|
Container(
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -397,24 +422,40 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTypeFilterBar(BuildContext context) {
|
Widget _buildTypeFilterBar(BuildContext context) {
|
||||||
final typeCodes = [
|
// 🔥 个人角色(roleKey == 'personal')只显示「全部、机器人、无人机」3 个 tab
|
||||||
'all',
|
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||||
'robot',
|
final isPersonal = roleKey == TabConfig.personalRoleKey;
|
||||||
'drone_station',
|
|
||||||
'inverter',
|
List<String> typeCodes;
|
||||||
'combiner_box',
|
List<String> typeLabels;
|
||||||
'module',
|
|
||||||
'monitor',
|
if (isPersonal) {
|
||||||
];
|
typeCodes = ['all', 'robot', 'drone_station'];
|
||||||
final typeLabels = [
|
typeLabels = [
|
||||||
AppLocalizations.of(context).translate('device_list_v2.all'),
|
AppLocalizations.of(context).translate('device_list_v2.all'),
|
||||||
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
||||||
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
||||||
AppLocalizations.of(context).translate('device_list_v2.inverter'),
|
];
|
||||||
AppLocalizations.of(context).translate('device_list_v2.combiner_box'),
|
} else {
|
||||||
AppLocalizations.of(context).translate('device_list_v2.module'),
|
typeCodes = [
|
||||||
AppLocalizations.of(context).translate('device_list_v2.monitor'),
|
'all',
|
||||||
];
|
'robot',
|
||||||
|
'drone_station',
|
||||||
|
'inverter',
|
||||||
|
'combiner_box',
|
||||||
|
'module',
|
||||||
|
'monitor',
|
||||||
|
];
|
||||||
|
typeLabels = [
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.all'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.robot'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.drone_station'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.inverter'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.combiner_box'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.module'),
|
||||||
|
AppLocalizations.of(context).translate('device_list_v2.monitor'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return BlocBuilder<
|
return BlocBuilder<
|
||||||
DeviceListBloc.DeviceStatusBloc,
|
DeviceListBloc.DeviceStatusBloc,
|
||||||
@@ -426,56 +467,81 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
selectedType = state.selectedType;
|
selectedType = state.selectedType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取当前选中的索引
|
||||||
|
int selectedIndex = typeCodes.indexOf(selectedType);
|
||||||
|
if (selectedIndex == -1) selectedIndex = 0; // 默认为 'all'
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 40,
|
height: 40,
|
||||||
child: ListView.separated(
|
child: NotificationListener<ScrollNotification>(
|
||||||
scrollDirection: Axis.horizontal,
|
onNotification: (ScrollNotification notification) {
|
||||||
physics: const ClampingScrollPhysics(),
|
// 🔥 监听滑动结束事件,自动切换选中状态
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
if (notification is ScrollEndNotification &&
|
||||||
itemCount: typeCodes.length,
|
notification.metrics.axis == Axis.horizontal) {
|
||||||
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
|
final position = notification.metrics.pixels;
|
||||||
itemBuilder: (context, index) {
|
final itemWidth = 56.0; // 预估每个 tab 的宽度(包括间距)
|
||||||
final typeCode = typeCodes[index];
|
final newIndex = (position / itemWidth).round();
|
||||||
final typeLabel = typeLabels[index];
|
|
||||||
final isSelected = selectedType == typeCode;
|
|
||||||
|
|
||||||
return GestureDetector(
|
if (newIndex >= 0 &&
|
||||||
onTap: () {
|
newIndex < typeCodes.length &&
|
||||||
|
newIndex != selectedIndex) {
|
||||||
|
final newTypeCode = typeCodes[newIndex];
|
||||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||||
DeviceListEvent.DeviceStatusChangeType(typeCode),
|
DeviceListEvent.DeviceStatusChangeType(newTypeCode),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Column(
|
}
|
||||||
mainAxisSize: MainAxisSize.min,
|
return false;
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
typeLabel,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
color: isSelected
|
|
||||||
? context.appColors.primary
|
|
||||||
: context.appColors.textSecondary,
|
|
||||||
fontWeight: isSelected
|
|
||||||
? FontWeight.w600
|
|
||||||
: FontWeight.normal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Container(
|
|
||||||
width: 20,
|
|
||||||
height: 2,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected
|
|
||||||
? context.appColors.primary
|
|
||||||
: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(1),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
itemCount: typeCodes.length,
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final typeCode = typeCodes[index];
|
||||||
|
final typeLabel = typeLabels[index];
|
||||||
|
final isSelected = selectedType == typeCode;
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||||
|
DeviceListEvent.DeviceStatusChangeType(typeCode),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
typeLabel,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: isSelected
|
||||||
|
? context.appColors.primary
|
||||||
|
: context.appColors.textSecondary,
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? context.appColors.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -677,6 +743,36 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'🔴🔴🔴 [全部-选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}',
|
'🔴🔴🔴 [全部-选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}',
|
||||||
);
|
);
|
||||||
|
debugPrint(
|
||||||
|
'🔴🔴🔴 [全部-选中机器人] hasHost: ${robot.hasHost}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔥 hasHost 字段判定:false → 蓝牙连接流程
|
||||||
|
if (!robot.hasHost) {
|
||||||
|
debugPrint('⚠️ [全部-EmbeddedRobotList] hasHost=false,进入蓝牙连接流程');
|
||||||
|
|
||||||
|
// 从设备名中提取时间戳(如 MC700PRO-CN-JS-1751525588120-0000000A-9527 → 1751525588120)
|
||||||
|
final timestamp = _extractTimestamp(robot.name);
|
||||||
|
if (timestamp == null || timestamp.isEmpty) {
|
||||||
|
debugPrint('❌ [全部-EmbeddedRobotList] 未提取到时间戳');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('设备名称格式错误,无法提取时间戳'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('✅ [全部-EmbeddedRobotList] 提取到时间戳: $timestamp');
|
||||||
|
|
||||||
|
// 调用蓝牙连接流程
|
||||||
|
_connectToDeviceViaBluetooth(context, robot.name, timestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasHost == true → 走原来的逻辑
|
||||||
|
debugPrint('✅ [全部-EmbeddedRobotList] hasHost=true,走正常控制流程');
|
||||||
|
|
||||||
// 1. 将当前机器人设置为全局待控制设备
|
// 1. 将当前机器人设置为全局待控制设备
|
||||||
final device = DeviceEntity(
|
final device = DeviceEntity(
|
||||||
@@ -1234,4 +1330,177 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 从设备名中提取时间戳
|
||||||
|
String? _extractTimestamp(String deviceName) {
|
||||||
|
final regex = RegExp(r'(\d{13})');
|
||||||
|
final match = regex.firstMatch(deviceName);
|
||||||
|
return match?.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 蓝牙连接流程
|
||||||
|
Future<void> _connectToDeviceViaBluetooth(
|
||||||
|
BuildContext context,
|
||||||
|
String deviceName,
|
||||||
|
String timestamp,
|
||||||
|
) async {
|
||||||
|
final bleManager = BleManager.instance;
|
||||||
|
|
||||||
|
// 🔥 0. 优先检查:如果该设备已经连接,直接跳转详情页,不再扫描
|
||||||
|
final connectedDevice = bleManager.connectedDevice;
|
||||||
|
if (connectedDevice != null) {
|
||||||
|
final connectedName = bleManager.connectedDeviceName ?? '';
|
||||||
|
debugPrint('🔍 [蓝牙连接] 已连接设备名: $connectedName,匹配时间戳: $timestamp');
|
||||||
|
if (connectedName.contains(timestamp)) {
|
||||||
|
debugPrint('✅ [蓝牙连接] 设备已连接,直接跳转详情页: $connectedName');
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: connectedDevice),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查蓝牙状态
|
||||||
|
final isBluetoothOn = await bleManager.checkBluetooth();
|
||||||
|
if (!isBluetoothOn) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('请先打开手机蓝牙'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 显示加载圈
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('正在搜索蓝牙设备...', style: TextStyle(color: Colors.white)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 启动蓝牙扫描
|
||||||
|
await bleManager.startScan(continuous: false);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
|
// 4. 关闭加载圈
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// 5. 获取扫描结果并查找匹配设备
|
||||||
|
final scanResults = bleManager.getScanResults();
|
||||||
|
BluetoothDevice? matchedDevice;
|
||||||
|
for (final result in scanResults) {
|
||||||
|
final devName = result.device.platformName.isNotEmpty
|
||||||
|
? result.device.platformName
|
||||||
|
: result.device.advName;
|
||||||
|
if (devName.contains(timestamp)) {
|
||||||
|
matchedDevice = result.device;
|
||||||
|
debugPrint('✅ [蓝牙连接] 找到匹配设备: $devName');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedDevice != null) {
|
||||||
|
debugPrint('🚀 [蓝牙连接] 跳转到 BLE 详情页并连接');
|
||||||
|
final device = matchedDevice;
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: device),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ [蓝牙连接] 未在扫描列表中找到设备,弹窗提示并后台监测');
|
||||||
|
_showNotFoundDialogAndMonitor(context, deviceName, timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 弹窗提示并后台持续监测设备(单例模式,防止重复弹窗)
|
||||||
|
void _showNotFoundDialogAndMonitor(
|
||||||
|
BuildContext context,
|
||||||
|
String deviceName,
|
||||||
|
String timestamp,
|
||||||
|
) {
|
||||||
|
// 🔥 检查是否已有弹窗存在,如果有则先关闭
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
debugPrint('⚠️ [后台监测] 检测到已有弹窗,先关闭上一个');
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dialogShown = true;
|
||||||
|
StreamSubscription? subscription;
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
dialogShown = true;
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('未找到设备'),
|
||||||
|
content: const Text('请确认机器已上线并打开蓝牙了'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
dialogShown = false;
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
},
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
dialogShown = false;
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
},
|
||||||
|
child: const Text('确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 启动后台监测
|
||||||
|
final bleManager = BleManager.instance;
|
||||||
|
subscription = bleManager.scanResults.listen((results) {
|
||||||
|
if (!dialogShown) {
|
||||||
|
subscription?.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final result in results) {
|
||||||
|
final devName = result.device.platformName.isEmpty
|
||||||
|
? result.device.advName
|
||||||
|
: result.device.platformName;
|
||||||
|
|
||||||
|
if (devName.contains(timestamp)) {
|
||||||
|
debugPrint('✅ [后台监测] 匹配成功!设备名: $devName');
|
||||||
|
subscription?.cancel();
|
||||||
|
|
||||||
|
// 🔥 弹窗可能已关闭,需要先 pop 当前弹窗再跳转
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: result.device),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:maibu_satabot_v2/core/bluetooth/ble_manager.dart';
|
||||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||||
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
||||||
import '../../../../../core/di/injection.dart';
|
import '../../../../../core/di/injection.dart';
|
||||||
import '../../../../../core/logging/i_logger_service.dart';
|
import '../../../../../core/logging/i_logger_service.dart';
|
||||||
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
import '../../../../devices/presentation/bloc/devices_cubit.dart';
|
||||||
import '../../../../devices/domain/entities/device_entity.dart';
|
import '../../../../devices/domain/entities/device_entity.dart';
|
||||||
|
import '../../../../devices/presentation/bloc/device_status_bloc.dart';
|
||||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||||
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||||
import '../../data/models/robot_data_model.dart';
|
import '../../data/models/robot_data_model.dart';
|
||||||
@@ -14,6 +19,7 @@ import '../bloc/robot_list_bloc.dart';
|
|||||||
import '../bloc/robot_list_event.dart';
|
import '../bloc/robot_list_event.dart';
|
||||||
import '../bloc/robot_list_state.dart';
|
import '../bloc/robot_list_state.dart';
|
||||||
import '../widgets/robot_item_card.dart';
|
import '../widgets/robot_item_card.dart';
|
||||||
|
import 'ble_device_detail_page.dart';
|
||||||
import 'robot_control_page.dart';
|
import 'robot_control_page.dart';
|
||||||
import 'cleaning_weeding_robot_task_page.dart';
|
import 'cleaning_weeding_robot_task_page.dart';
|
||||||
|
|
||||||
@@ -87,7 +93,8 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
children: [
|
children: [
|
||||||
_buildStatsCard(state),
|
_buildStatsCard(state),
|
||||||
_buildQuickActions(state),
|
_buildQuickActions(state),
|
||||||
_buildCurrentTask(state),
|
// 🔥 个人角色暂时注释掉当前任务卡片
|
||||||
|
// _buildCurrentTask(state),
|
||||||
..._buildRobotList(state),
|
..._buildRobotList(state),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -646,6 +653,38 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
debugPrint(
|
debugPrint(
|
||||||
'🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}',
|
'🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}',
|
||||||
);
|
);
|
||||||
|
debugPrint(
|
||||||
|
'🔴🔴🔴 [选中机器人] hasHost: ${robot.hasHost}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔥 hasHost 字段判定:false → 蓝牙连接流程,true 走原逻辑
|
||||||
|
if (!robot.hasHost) {
|
||||||
|
// hasHost == false 或接口未返回该字段 → 蓝牙连接流程
|
||||||
|
debugPrint('⚠️ [RobotListPage] hasHost=false,进入蓝牙连接流程');
|
||||||
|
debugPrint('📱 [RobotListPage] 机器人名称: ${robot.name}');
|
||||||
|
|
||||||
|
// 从设备名中提取时间戳(例如:MC700PRO-CN-JS-1751525588120-0000000A-9527)
|
||||||
|
final timestamp = _extractTimestamp(robot.name);
|
||||||
|
if (timestamp == null || timestamp.isEmpty) {
|
||||||
|
debugPrint('❌ [RobotListPage] 无法从设备名中提取时间戳: ${robot.name}');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('设备名格式异常,无法进行蓝牙连接'),
|
||||||
|
backgroundColor: context.appColors.warning,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('✅ [RobotListPage] 提取到时间戳: $timestamp');
|
||||||
|
|
||||||
|
// 调用蓝牙连接流程
|
||||||
|
_connectToDeviceViaBluetooth(context, robot.name, timestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasHost == true → 走原来的逻辑
|
||||||
|
debugPrint('✅ [RobotListPage] hasHost=true,走正常控制流程');
|
||||||
|
|
||||||
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
|
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
|
||||||
final device = DeviceEntity(
|
final device = DeviceEntity(
|
||||||
@@ -687,4 +726,219 @@ class _RobotListViewState extends State<RobotListView> {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🔥 从设备名中提取时间戳
|
||||||
|
/// 例如:MC700PRO-CN-JS-1751525588120-0000000A-9527 → 1751525588120
|
||||||
|
String? _extractTimestamp(String deviceName) {
|
||||||
|
try {
|
||||||
|
// 使用正则表达式匹配中间的一串数字(13位时间戳)
|
||||||
|
final regex = RegExp(r'(\d{13})');
|
||||||
|
final match = regex.firstMatch(deviceName);
|
||||||
|
if (match != null) {
|
||||||
|
return match.group(1);
|
||||||
|
}
|
||||||
|
debugPrint('⚠️ [_extractTimestamp] 未找到时间戳格式,返回 null');
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ [_extractTimestamp] 解析失败: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 蓝牙连接流程
|
||||||
|
/// 1. 检查蓝牙状态
|
||||||
|
/// 2. 在 BLE 扫描列表中查找包含时间戳的设备
|
||||||
|
/// 3. 如果找到则连接并跳转详情页
|
||||||
|
/// 4. 如果没找到则弹窗提示并后台持续监测
|
||||||
|
Future<void> _connectToDeviceViaBluetooth(
|
||||||
|
BuildContext context,
|
||||||
|
String deviceName,
|
||||||
|
String timestamp,
|
||||||
|
) async {
|
||||||
|
final bleManager = BleManager.instance;
|
||||||
|
|
||||||
|
// 🔥 0. 优先检查:如果该设备已经连接,直接跳转详情页,不再扫描
|
||||||
|
final connectedDevice = bleManager.connectedDevice;
|
||||||
|
if (connectedDevice != null) {
|
||||||
|
final connectedName = bleManager.connectedDeviceName ?? '';
|
||||||
|
debugPrint('🔍 [蓝牙连接] 已连接设备名: $connectedName,匹配时间戳: $timestamp');
|
||||||
|
if (connectedName.contains(timestamp)) {
|
||||||
|
debugPrint('✅ [蓝牙连接] 设备已连接,直接跳转详情页: $connectedName');
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: connectedDevice),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 检查蓝牙是否打开
|
||||||
|
final isBluetoothOn = await bleManager.checkBluetooth();
|
||||||
|
if (!isBluetoothOn) {
|
||||||
|
debugPrint('❌ [蓝牙连接] 蓝牙未打开,提示用户');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text('请先打开蓝牙'),
|
||||||
|
backgroundColor: context.appColors.warning,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('✅ [蓝牙连接] 蓝牙已打开,开始搜索设备: $deviceName');
|
||||||
|
|
||||||
|
// 2. 显示加载圈
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('正在搜索蓝牙设备...', style: TextStyle(color: Colors.white)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. 启动蓝牙扫描(只扫一次,不要连续扫)
|
||||||
|
try {
|
||||||
|
await bleManager.startScan(continuous: false);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ [蓝牙连接] 启动扫描失败: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 等待一下让扫描结果出现
|
||||||
|
await Future.delayed(const Duration(milliseconds: 1500));
|
||||||
|
|
||||||
|
// 5. 关闭加载圈
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
|
// 6. 获取当前扫描结果列表
|
||||||
|
final scanResults = bleManager.getScanResults();
|
||||||
|
debugPrint('🔍 [蓝牙连接] 扫描到 ${scanResults.length} 个设备');
|
||||||
|
|
||||||
|
// 5. 查找包含时间戳的设备
|
||||||
|
BluetoothDevice? matchedDevice;
|
||||||
|
for (final result in scanResults) {
|
||||||
|
final dev = result.device;
|
||||||
|
final devName = dev.platformName.isEmpty
|
||||||
|
? (dev.advName.isEmpty ? dev.remoteId.toString() : dev.advName)
|
||||||
|
: dev.platformName;
|
||||||
|
|
||||||
|
debugPrint(' 📱 发现设备: $devName (包含时间戳?: ${devName.contains(timestamp)})');
|
||||||
|
|
||||||
|
// 检查设备名是否包含时间戳
|
||||||
|
if (devName.contains(timestamp)) {
|
||||||
|
debugPrint('✅ [蓝牙连接] 匹配成功!设备名: $devName');
|
||||||
|
matchedDevice = dev;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedDevice != null) {
|
||||||
|
debugPrint('🚀 [蓝牙连接] 跳转到 BLE 详情页并连接');
|
||||||
|
// 跳转到 BLE 设备详情页(详情页会自动连接)
|
||||||
|
final device = matchedDevice;
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: device),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ [蓝牙连接] 未在扫描列表中找到设备,弹窗提示并后台监测');
|
||||||
|
// 6. 没找到设备,弹窗提示并后台持续监测
|
||||||
|
_showNotFoundDialogAndMonitor(context, deviceName, timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🔥 弹窗提示并后台持续监测设备(单例模式,防止重复弹窗)
|
||||||
|
void _showNotFoundDialogAndMonitor(
|
||||||
|
BuildContext context,
|
||||||
|
String deviceName,
|
||||||
|
String timestamp,
|
||||||
|
) {
|
||||||
|
// 🔥 检查是否已有弹窗存在,如果有则先关闭
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
debugPrint('⚠️ [后台监测] 检测到已有弹窗,先关闭上一个');
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool dialogShown = true;
|
||||||
|
StreamSubscription? subscription;
|
||||||
|
|
||||||
|
// 先显示对话框
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
dialogShown = true;
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('未找到设备'),
|
||||||
|
content: const Text('请确认机器已上线并打开蓝牙了'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
dialogShown = false;
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
},
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
dialogShown = false;
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
},
|
||||||
|
child: const Text('确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).then((_) {
|
||||||
|
// 对话框关闭后不停止监测,继续运行
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动后台监测 BLE 扫描列表
|
||||||
|
final bleManager = BleManager.instance;
|
||||||
|
subscription = bleManager.scanResults.listen((results) {
|
||||||
|
if (!dialogShown) {
|
||||||
|
// 对话框已关闭,停止监测
|
||||||
|
subscription?.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('🔍 [后台监测] 扫描到 ${results.length} 个设备,搜索时间戳: $timestamp');
|
||||||
|
|
||||||
|
for (final result in results) {
|
||||||
|
final dev = result.device;
|
||||||
|
final devName = dev.platformName.isEmpty
|
||||||
|
? (dev.advName.isEmpty ? dev.remoteId.toString() : dev.advName)
|
||||||
|
: dev.platformName;
|
||||||
|
|
||||||
|
if (devName.contains(timestamp)) {
|
||||||
|
debugPrint('✅ [后台监测] 匹配成功!设备名: $devName');
|
||||||
|
subscription?.cancel();
|
||||||
|
|
||||||
|
// 🔥 弹窗可能已关闭,需要先 pop 当前弹窗再跳转
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => BleDeviceDetailPage(device: dev!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ class _MyPageContent extends StatelessWidget {
|
|||||||
final userName = user?.nickname ?? user?.username ?? '未知用户';
|
final userName = user?.nickname ?? user?.username ?? '未知用户';
|
||||||
final userRole = user?.email ?? user?.phone ?? '未设置';
|
final userRole = user?.email ?? user?.phone ?? '未设置';
|
||||||
|
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 用户信息卡片(延伸到状态栏)
|
// 用户信息卡片(延伸到状态栏)
|
||||||
UserProfileCard(
|
UserProfileCard(
|
||||||
@@ -124,11 +125,9 @@ class _MyPageContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.5,
|
|
||||||
),
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
// 末尾追加退出登录,占据“关于我们”原来的位置
|
// 末尾追加退出登录,占据“关于我们”原来的位置
|
||||||
itemCount: state.menuItems.length + 1,
|
itemCount: state.menuItems.length + 1,
|
||||||
@@ -160,6 +159,7 @@ class _MyPageContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:maibu_satabot_v2/core/theme/AppTheme.dart';
|
|||||||
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
||||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||||
|
import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart';
|
||||||
|
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/core/update/update_cubit.dart';
|
import 'package:maibu_satabot_v2/core/update/update_cubit.dart';
|
||||||
import 'package:maibu_satabot_v2/core/update/update_state.dart';
|
import 'package:maibu_satabot_v2/core/update/update_state.dart';
|
||||||
@@ -146,6 +148,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
|||||||
|
|
||||||
/// Tab 设置
|
/// Tab 设置
|
||||||
Widget _buildTabSettingsSection() {
|
Widget _buildTabSettingsSection() {
|
||||||
|
// 🔥 个人角色(roleKey == 'personal'):Tab 设置只列出「设备 + 我的」,且开关常开锁定
|
||||||
|
final roleKey = context.watch<AppUserCubit>().state.user?.roleKey;
|
||||||
|
final isPersonal = roleKey == TabConfig.personalRoleKey;
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -177,7 +182,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
|||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
final tabs = state.config.items;
|
final tabs = state.config.settingItemsForRole(roleKey);
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -199,10 +204,13 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
value: tab.isEnabled,
|
// 个人角色:设备/我的 强制常开且不可关闭
|
||||||
onChanged: (value) {
|
value: isPersonal ? true : tab.isEnabled,
|
||||||
context.read<TabConfigCubit>().toggleTab(tab.id);
|
onChanged: isPersonal
|
||||||
},
|
? null
|
||||||
|
: (value) {
|
||||||
|
context.read<TabConfigCubit>().toggleTab(tab.id);
|
||||||
|
},
|
||||||
activeColor: const Color(0xFF165DFF),
|
activeColor: const Color(0xFF165DFF),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -132,10 +132,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -944,18 +944,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1493,10 +1493,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.7"
|
||||||
time:
|
time:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user