Commit 59de0036 authored by akari's avatar akari

feat: 软件设置子页面

parent 72d5f64e
......@@ -45,8 +45,7 @@ class BluetoothReadBloc extends Bloc<BluetoothReadEvent, BluetoothReadState> {
for (int i = 0; i < maxRetries; i++) {
try {
info = await _storageService.getBluetoothReadInfo();
Log.warn(
'**************sn 第${i + 1}次获取************: ${info.sn}');
Log.warn('**************sn 第${i + 1}次获取************: ${info.sn}');
if (info.hasSn) {
Log.warn('**************sn 获取成功,停止重试************');
break;
......@@ -75,7 +74,28 @@ class BluetoothReadBloc extends Bloc<BluetoothReadEvent, BluetoothReadState> {
BluetoothReadUpdated event,
Emitter<BluetoothReadState> emit,
) {
emit(state.copyWith(info: event.info, clearError: true));
emit(state.copyWith(
info: _resolveDisplayInfo(event.info), clearError: true));
}
BluetoothReadModel _resolveDisplayInfo(BluetoothReadModel next) {
final currentSn = state.info.sn;
final nextSn = next.sn;
if (currentSn == null ||
currentSn.isEmpty ||
nextSn == null ||
nextSn.isEmpty ||
currentSn == nextSn) {
return next;
}
final currentSide = currentSn[currentSn.length - 1].toUpperCase();
if ((currentSide == 'L' || currentSide == 'R') &&
currentSn.substring(0, currentSn.length - 1) == nextSn) {
return next.copyWith(sn: currentSn);
}
return next;
}
void _onCleared(
......
......@@ -588,10 +588,10 @@ class DeviceControlBloc
// ==================== 护理等级控制事件处理 ====================
/// [用户操作] 设置护理等级(发送给MCU)—— 持久化
void _onCareLevelChanged(
Future<void> _onCareLevelChanged(
DeviceControlCareLevelChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
final newData = state.data.copyWith(
careLevel: state.data.careLevel.copyWith(
currentLevel: event.level,
......@@ -602,7 +602,12 @@ class DeviceControlBloc
userSettings: state.userSettings.copyWith(careLevelValue: event.level),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0xA8,
setValue: '${_careLevelProtocolValue(event.level)}',
errorPrefix: '护理等级',
emit: emit,
);
}
// ==================== 新风进化控制事件处理 ====================
......@@ -653,10 +658,10 @@ class DeviceControlBloc
// ==================== 雾化时间设置事件处理 ====================
/// [用户操作] 设置雾化时间(发送给MCU)—— 持久化
void _onNebulizationTimeChanged(
Future<void> _onNebulizationTimeChanged(
DeviceControlNebulizationTimeChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
final newData = state.data.copyWith(
nebulizationTime: state.data.nebulizationTime.copyWith(
setMinutes: event.minutes,
......@@ -668,16 +673,21 @@ class DeviceControlBloc
state.userSettings.copyWith(nebulizationTimeMinutes: event.minutes),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x0C,
setValue: event.minutes,
errorPrefix: '雾化时间',
emit: emit,
);
}
// ==================== 消毒时间设置事件处理 ====================
/// [用户操作] 设置消毒时间(发送给MCU)—— 持久化
void _onDisinfectionTimeChanged(
Future<void> _onDisinfectionTimeChanged(
DeviceControlDisinfectionTimeChanged event,
Emitter<DeviceControlState> emit,
) {
) async {
final newData = state.data.copyWith(
disinfectionTime: state.data.disinfectionTime.copyWith(
setMinutes: event.minutes,
......@@ -689,7 +699,12 @@ class DeviceControlBloc
state.userSettings.copyWith(disinfectionTimeMinutes: event.minutes),
clearError: true,
));
// TODO: 发送设置到MCU
await _writeControlValueToBluetooth(
identifier: 0x0D,
setValue: event.minutes,
errorPrefix: '消毒时间',
emit: emit,
);
}
// ==================== 开放供氧控制事件处理 ====================
......@@ -735,10 +750,10 @@ class DeviceControlBloc
}
/// [用户操作] 清零供氧计时
void _onOxygenTimerCleared(
Future<void> _onOxygenTimerCleared(
DeviceControlOxygenTimerCleared event,
Emitter<DeviceControlState> emit,
) {
) async {
final newData = state.data.copyWith(
oxygenTimer: const OxygenTimerBO(
hours: '00',
......@@ -747,7 +762,12 @@ class DeviceControlBloc
),
);
emit(state.copyWith(data: newData, clearError: true));
// TODO: 发送清零指令到MCU
await _writeControlValueToBluetooth(
identifier: 0x29,
setValue: '0',
errorPrefix: '供氧累计时间清零',
emit: emit,
);
}
// ==================== 循环模式控制事件处理 ====================
......@@ -984,6 +1004,21 @@ class DeviceControlBloc
}
}
int _careLevelProtocolValue(CareLevel level) {
switch (level) {
case CareLevel.off:
return 0;
case CareLevel.level3:
return 1;
case CareLevel.level2:
return 2;
case CareLevel.level1:
return 3;
case CareLevel.special:
return 4;
}
}
List<int>? _buildControlValueCommand({
required int identifier,
required String setValue,
......
......@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:laki_icu_app/utils/logger.dart';
import 'package:permission_handler/permission_handler.dart';
typedef BluetoothPayloadParser<T> = T? Function(List<int> bytes, String rawHex);
......@@ -131,6 +132,11 @@ class BleBluetoothManager<T> {
);
Future<bool> ensureBluetoothReady() async {
if (!await _ensureBluetoothPermissions()) {
_notifyMessage('蓝牙权限未授权,无法扫描或连接设备');
return false;
}
await _ble.initialize();
final status = _ble.status;
......@@ -156,6 +162,30 @@ class BleBluetoothManager<T> {
return true;
}
Future<bool> _ensureBluetoothPermissions() async {
final permissions = <Permission>[
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.locationWhenInUse,
];
final statuses = await permissions.request();
final deniedPermissions = statuses.entries.where((entry) {
final status = entry.value;
return !status.isGranted && !status.isLimited;
}).toList();
if (deniedPermissions.isNotEmpty) {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG permission_denied '
'${deniedPermissions.map((entry) => entry.key).join(',')}',
);
return false;
}
return true;
}
Future<void> scanForDevices({
required List<String> namePrefixes,
Duration timeout = const Duration(seconds: 10),
......@@ -298,16 +328,6 @@ class BleBluetoothManager<T> {
await disconnect();
await Future<void>.delayed(const Duration(milliseconds: 600));
_connectedDeviceId = deviceId;
_notifyCharacteristic = QualifiedCharacteristic(
deviceId: deviceId,
serviceId: serviceUuid,
characteristicId: notifyCharacteristicUuid,
);
_writeCharacteristic = QualifiedCharacteristic(
deviceId: deviceId,
serviceId: serviceUuid,
characteristicId: writeCharacteristicUuid,
);
_setStatus(BluetoothConnectionStatus.connecting);
_notifyMessage('正在连接蓝牙设备: $deviceId');
......@@ -319,20 +339,13 @@ class BleBluetoothManager<T> {
id: deviceId,
withServices: const [],
prescanDuration: const Duration(seconds: 5),
servicesWithCharacteristicsToDiscover: {
serviceUuid: [notifyCharacteristicUuid, writeCharacteristicUuid],
},
connectionTimeout: connectionTimeout,
)
: _ble.connectToDevice(
id: deviceId,
servicesWithCharacteristicsToDiscover: {
serviceUuid: [notifyCharacteristicUuid, writeCharacteristicUuid],
},
connectionTimeout: connectionTimeout,
);
_connectionSubscription = connectionStream
.listen(
_connectionSubscription = connectionStream.listen(
(update) async {
switch (update.connectionState) {
case DeviceConnectionState.connecting:
......@@ -346,9 +359,7 @@ class BleBluetoothManager<T> {
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG state_connected deviceId=$deviceId');
try {
if (enableCharacteristicDiscovery) {
await _discoverServicesAndResolveWriteCharacteristic(deviceId);
}
await _discoverServicesAndResolveWriteCharacteristic(deviceId);
await Future<void>.delayed(const Duration(milliseconds: 500));
final notifyReady = await _enableNotifications(deviceId);
if (notifyReady) {
......@@ -577,24 +588,27 @@ class BleBluetoothManager<T> {
) async {
try {
Log.warn('🔥🔥🔥 BLE_DISCOVER_DEBUG start deviceId=$deviceId');
await _ble.discoverAllServices(deviceId);
final services = await _ble.getDiscoveredServices(deviceId);
QualifiedCharacteristic? fallbackWriteCharacteristic;
QualifiedCharacteristic? pairedNotifyCharacteristic;
QualifiedCharacteristic? pairedIndicateCharacteristic;
QualifiedCharacteristic? pairedWriteWithResponseCharacteristic;
QualifiedCharacteristic? pairedWriteWithoutResponseCharacteristic;
QualifiedCharacteristic? fallbackNotifyCharacteristic;
var fallbackOnlyWithoutResponse = false;
var preferredWriteFound = false;
var preferredNotifyFound = false;
QualifiedCharacteristic? fallbackIndicateCharacteristic;
QualifiedCharacteristic? fallbackWriteWithResponseCharacteristic;
QualifiedCharacteristic? fallbackWriteWithoutResponseCharacteristic;
for (final service in services) {
Log.warn('🔥🔥🔥 BLE_DISCOVER_DEBUG service=${service.id}');
QualifiedCharacteristic? serviceNotifyCharacteristic;
QualifiedCharacteristic? serviceIndicateCharacteristic;
QualifiedCharacteristic? serviceWriteWithResponseCharacteristic;
QualifiedCharacteristic? serviceWriteWithoutResponseCharacteristic;
for (final characteristic in service.characteristics) {
final isPreferredWrite =
_isSameUuid(characteristic.id, writeCharacteristicUuid);
final isPreferredNotify =
_isSameUuid(characteristic.id, notifyCharacteristicUuid);
final canWrite = characteristic.isWritableWithResponse ||
characteristic.isWritableWithoutResponse;
final canNotify =
characteristic.isNotifiable || characteristic.isIndicatable;
Log.warn(
'🔥🔥🔥 BLE_DISCOVER_DEBUG characteristic=${characteristic.id} '
'service=${service.id} '
......@@ -602,9 +616,7 @@ class BleBluetoothManager<T> {
'write=${characteristic.isWritableWithResponse} '
'writeNoResp=${characteristic.isWritableWithoutResponse} '
'notify=${characteristic.isNotifiable} '
'indicate=${characteristic.isIndicatable}'
'${isPreferredWrite ? ' preferredWrite=true' : ''}'
'${isPreferredNotify ? ' preferredNotify=true' : ''}',
'indicate=${characteristic.isIndicatable}',
);
final resolved = QualifiedCharacteristic(
......@@ -613,50 +625,64 @@ class BleBluetoothManager<T> {
characteristicId: characteristic.id,
);
if (canNotify) {
if (characteristic.isNotifiable) {
serviceNotifyCharacteristic ??= resolved;
fallbackNotifyCharacteristic ??= resolved;
if (isPreferredNotify) {
_notifyCharacteristic = resolved;
preferredNotifyFound = true;
}
}
if (characteristic.isIndicatable) {
serviceIndicateCharacteristic ??= resolved;
fallbackIndicateCharacteristic ??= resolved;
}
if (!canWrite) continue;
final onlyWithoutResponse =
!characteristic.isWritableWithResponse &&
characteristic.isWritableWithoutResponse;
fallbackWriteCharacteristic ??= resolved;
fallbackOnlyWithoutResponse = onlyWithoutResponse;
if (isPreferredWrite) {
_writeCharacteristic = resolved;
_writeCharacteristicOnlyWithoutResponse = onlyWithoutResponse;
preferredWriteFound = true;
if (characteristic.isWritableWithResponse &&
serviceWriteWithResponseCharacteristic == null) {
serviceWriteWithResponseCharacteristic = resolved;
fallbackWriteWithResponseCharacteristic ??= resolved;
}
if (characteristic.isWritableWithoutResponse &&
serviceWriteWithoutResponseCharacteristic == null) {
serviceWriteWithoutResponseCharacteristic = resolved;
fallbackWriteWithoutResponseCharacteristic ??= resolved;
}
}
}
if (!preferredNotifyFound && fallbackNotifyCharacteristic != null) {
_notifyCharacteristic = fallbackNotifyCharacteristic;
Log.warn(
'🔥🔥🔥 BLE_DISCOVER_DEBUG preferred_notify_not_available '
'fallback=${_notifyCharacteristic!.characteristicId} '
'service=${_notifyCharacteristic!.serviceId}',
);
final serviceHasNotify = serviceNotifyCharacteristic != null ||
serviceIndicateCharacteristic != null;
final serviceHasWrite =
serviceWriteWithResponseCharacteristic != null ||
serviceWriteWithoutResponseCharacteristic != null;
if (pairedNotifyCharacteristic == null &&
pairedIndicateCharacteristic == null &&
serviceHasNotify &&
serviceHasWrite) {
pairedNotifyCharacteristic = serviceNotifyCharacteristic;
pairedIndicateCharacteristic = serviceIndicateCharacteristic;
pairedWriteWithResponseCharacteristic =
serviceWriteWithResponseCharacteristic;
pairedWriteWithoutResponseCharacteristic =
serviceWriteWithoutResponseCharacteristic;
}
}
if (!preferredWriteFound && fallbackWriteCharacteristic != null) {
_writeCharacteristic = fallbackWriteCharacteristic;
_writeCharacteristicOnlyWithoutResponse = fallbackOnlyWithoutResponse;
Log.warn(
'🔥🔥🔥 BLE_DISCOVER_DEBUG preferred_write_not_writable '
'fallback=${_writeCharacteristic!.characteristicId} '
'service=${_writeCharacteristic!.serviceId} '
'onlyWithoutResponse=$_writeCharacteristicOnlyWithoutResponse',
);
}
final resolvedWriteWithResponse = pairedWriteWithResponseCharacteristic ??
fallbackWriteWithResponseCharacteristic;
final resolvedWriteWithoutResponse =
pairedWriteWithoutResponseCharacteristic ??
fallbackWriteWithoutResponseCharacteristic;
_notifyCharacteristic = pairedNotifyCharacteristic ??
pairedIndicateCharacteristic ??
fallbackNotifyCharacteristic ??
fallbackIndicateCharacteristic;
_writeCharacteristic = pairedWriteWithResponseCharacteristic ??
pairedWriteWithoutResponseCharacteristic ??
fallbackWriteWithResponseCharacteristic ??
fallbackWriteWithoutResponseCharacteristic;
_writeCharacteristicOnlyWithoutResponse = _writeCharacteristic != null &&
_writeCharacteristic != resolvedWriteWithResponse &&
_writeCharacteristic == resolvedWriteWithoutResponse;
final resolvedNotify = _notifyCharacteristic;
final resolvedWrite = _writeCharacteristic;
......@@ -724,10 +750,6 @@ class BleBluetoothManager<T> {
return prefixes.any(value.startsWith);
}
static bool _isSameUuid(Uuid left, Uuid right) {
return left.toString().toLowerCase() == right.toString().toLowerCase();
}
static String _normalizeRemoteIdForSort(String remoteId) {
return remoteId.replaceAll(RegExp(r'[^0-9a-zA-Z]'), '').toUpperCase();
}
......
......@@ -150,8 +150,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
).copyWith(sn: reportSn.isEmpty ? null : reportSn);
if (bluetoothReadInfo.hasSn) {
await _storageService.saveBluetoothReadSn(bluetoothReadInfo.sn!);
await _connectMqttForDeviceSn(bluetoothReadInfo.sn!);
await _fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!);
unawaited(_fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!));
}
eventBus.emit(
BluetoothReadInfoChangedEvent(bluetoothReadInfo),
......@@ -232,18 +231,24 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final boundDevice = await _storageService.getBoundBluetoothDevice();
if (isClosed) return;
final deviceId = boundDevice['deviceId'];
final bluetoothReadInfo = await _storageService.getBluetoothReadInfo();
if (isClosed) return;
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG load_bound_device '
'deviceId=$deviceId deviceName=${boundDevice['deviceName']} '
'hasSn=${bluetoothReadInfo.hasSn}',
'hasDeviceId=${deviceId != null && deviceId.isNotEmpty}',
);
emit(state.copyWith(
boundBluetoothDeviceId:
deviceId == null || deviceId.isEmpty ? null : deviceId,
boundBluetoothDeviceName: boundDevice['deviceName'],
));
if (deviceId != null && deviceId.isNotEmpty) {
_isAutoReconnectEnabled = true;
await _connectBoundBluetoothDevice();
if (isClosed) return;
}
final bluetoothReadInfo = await _storageService.getBluetoothReadInfo();
if (isClosed) return;
if (bluetoothReadInfo.hasSn) {
final sn = _appendBluetoothSideToSn(bluetoothReadInfo.sn!);
if (sn.isNotEmpty) {
......@@ -253,14 +258,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
BluetoothReadInfoChangedEvent(bluetoothReadInfo.copyWith(sn: sn)),
);
}
_connectMqttForDeviceSn(sn);
_fetchCabinDetailAndConnectVideo(sn);
}
}
if (deviceId != null && deviceId.isNotEmpty) {
_isAutoReconnectEnabled = true;
_connectBoundBluetoothDevice();
}
}
/// 页面初始化入口。
......@@ -288,8 +288,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final cameraSn = state.cabinCameraSn;
final wifiPwd = state.cabinWifiPwd;
if (cameraSn == null || cameraSn.isEmpty ||
wifiPwd == null || wifiPwd.isEmpty) {
if (cameraSn == null ||
cameraSn.isEmpty ||
wifiPwd == null ||
wifiPwd.isEmpty) {
debugPrint('[MonitoringIndexCubit] WebRTC 凭证未就绪,跳过连接');
return;
}
......@@ -313,8 +315,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
final cameraSn = state.cabinCameraSn;
final wifiPwd = state.cabinWifiPwd;
if (cameraSn == null || cameraSn.isEmpty ||
wifiPwd == null || wifiPwd.isEmpty) {
if (cameraSn == null ||
cameraSn.isEmpty ||
wifiPwd == null ||
wifiPwd.isEmpty) {
debugPrint('[MonitoringIndexCubit] P2P 凭证未就绪,跳过连接');
return;
}
......@@ -461,6 +465,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
}
Future<void> stopBluetoothScan() async {
await _bluetoothManager.stopScan();
if (!isClosed) {
emit(state.copyWith(isBluetoothScanning: false));
}
}
Future<void> connectBoundBluetoothDevice() async {
_isAutoReconnectEnabled = true;
await _connectBoundBluetoothDevice();
}
Future<void> bindBluetoothDevice(BluetoothScanDevice device) async {
if (state.isBluetoothBinding) return;
......@@ -472,6 +488,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
));
try {
await stopBluetoothScan();
await _bluetoothManager.connectToDevice(device);
if (!_bluetoothManager.isConnected) {
throw StateError('蓝牙连接未就绪');
......@@ -573,16 +590,15 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
);
try {
await _bluetoothManager.connectToDeviceId(deviceId, prescan: true);
await _bluetoothManager.connectToDeviceId(deviceId);
debugPrint(
'🔥🔥🔥 BLE_CONNECT_DEBUG auto_connect_success '
'deviceId=$deviceId isConnected=${_bluetoothManager.isConnected}',
);
if (!isClosed) {
emit(state.copyWith(
bluetoothMessage: _bluetoothManager.isConnected
? '蓝牙设备已自动连接'
: '蓝牙自动连接未就绪,等待重试',
bluetoothMessage:
_bluetoothManager.isConnected ? '蓝牙设备已自动连接' : '蓝牙自动连接未就绪,等待重试',
));
}
if (_bluetoothManager.isConnected) {
......
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
......@@ -91,7 +93,11 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
final cubit = context.read<MonitoringIndexCubit>();
Navigator.of(context).pop();
unawaited(cubit.stopBluetoothScan());
},
icon: Icon(Icons.close, color: Colors.white, size: 48.w),
tooltip: '关闭',
padding: EdgeInsets.zero,
......
import 'package:equatable/equatable.dart';
class CustomerServiceIndexState extends Equatable {
const CustomerServiceIndexState();
const CustomerServiceIndexState({
this.title = '联系我们',
this.hotlineLabel = '服务热线',
this.hotline = '400-0000-0000',
this.serviceTimeLabel = '服务时间',
this.serviceTime = '工作日 早上9:00 - 下午19:00',
});
final String title;
final String hotlineLabel;
final String hotline;
final String serviceTimeLabel;
final String serviceTime;
@override
List<Object?> get props => [];
List<Object?> get props => [
title,
hotlineLabel,
hotline,
serviceTimeLabel,
serviceTime,
];
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../widgets/settings_panel_frame.dart';
import '../cubit/customer_service_index_cubit.dart';
import '../cubit/customer_service_index_state.dart';
class CustomerServicePanel extends StatelessWidget {
const CustomerServicePanel({super.key});
......@@ -10,18 +13,77 @@ class CustomerServicePanel extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsPanelFrame(
backgroundAsset: settingsAssetPanelMain,
padding: EdgeInsets.all(42.w),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'客户服务',
style: TextStyle(
color: Colors.white,
fontSize: 34.sp,
fontWeight: FontWeight.w800,
padding: EdgeInsets.fromLTRB(42.w, 64.h, 42.w, 48.h),
child: BlocBuilder<CustomerServiceIndexCubit, CustomerServiceIndexState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.title,
style: TextStyle(
color: Colors.white,
fontSize: 38.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 46.h),
_ContactLine(
label: state.hotlineLabel,
value: state.hotline,
),
SizedBox(height: 30.h),
_ContactLine(
label: state.serviceTimeLabel,
value: state.serviceTime,
),
SizedBox(height: 42.h),
Divider(color: const Color(0xFF4CA7D2).withValues(alpha: 0.36)),
],
);
},
),
);
}
}
class _ContactLine extends StatelessWidget {
const _ContactLine({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Row(
children: [
SizedBox(
width: 220.w,
child: Text(
label,
style: TextStyle(
color: Colors.white,
fontSize: 29.sp,
fontWeight: FontWeight.w800,
),
),
),
),
Expanded(
child: Text(
value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 29.sp,
fontWeight: FontWeight.w800,
),
),
),
],
);
}
}
import 'package:equatable/equatable.dart';
class RemoteControlAccount extends Equatable {
const RemoteControlAccount({
required this.phone,
});
final String phone;
@override
List<Object?> get props => [phone];
}
class RemoteControlIndexState extends Equatable {
const RemoteControlIndexState();
const RemoteControlIndexState({
this.productName = 'TSAAS ICU 动物医疗监护舱',
this.modelName = '730SE-CICU-A',
this.appName = 'TSAAS 移动APP',
this.adminAccounts = const [
RemoteControlAccount(phone: '130****1233'),
],
this.monitorAccounts = const [
RemoteControlAccount(phone: '130****1233'),
],
});
final String productName;
final String modelName;
final String appName;
final List<RemoteControlAccount> adminAccounts;
final List<RemoteControlAccount> monitorAccounts;
@override
List<Object?> get props => [];
List<Object?> get props => [
productName,
modelName,
appName,
adminAccounts,
monitorAccounts,
];
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../widgets/settings_panel_frame.dart';
import '../cubit/remote_control_index_cubit.dart';
import '../cubit/remote_control_index_state.dart';
class RemoteControlPanel extends StatelessWidget {
const RemoteControlPanel({super.key});
......@@ -10,17 +13,233 @@ class RemoteControlPanel extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsPanelFrame(
backgroundAsset: settingsAssetPanelMain,
padding: EdgeInsets.all(42.w),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'远程控制',
style: TextStyle(
color: Colors.white,
fontSize: 34.sp,
fontWeight: FontWeight.w800,
padding: EdgeInsets.fromLTRB(42.w, 54.h, 42.w, 48.h),
child: BlocBuilder<RemoteControlIndexCubit, RemoteControlIndexState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_RemoteHeader(state: state),
SizedBox(height: 34.h),
Divider(color: const Color(0xFF4CA7D2).withValues(alpha: 0.36)),
SizedBox(height: 32.h),
Expanded(
child: SingleChildScrollView(
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_AccountSection(
title: '管理员',
description: '可以设置更改设备各项参数,启动TSAAS等功能。',
accounts: state.adminAccounts,
),
SizedBox(height: 32.h),
_AccountSection(
title: '监测员',
description: '只能查看设备各项参数,记录诊疗状态等权限。',
accounts: state.monitorAccounts,
),
],
),
),
),
],
);
},
),
);
}
}
class _RemoteHeader extends StatelessWidget {
const _RemoteHeader({required this.state});
final RemoteControlIndexState state;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 20.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 42.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 22.h),
Text(
state.modelName,
style: TextStyle(
color: Colors.white,
fontSize: 36.sp,
fontWeight: FontWeight.w800,
),
),
],
),
),
),
SizedBox(width: 40.w),
_AppQrCard(appName: state.appName),
],
);
}
}
class _AppQrCard extends StatelessWidget {
const _AppQrCard({required this.appName});
final String appName;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 226.w,
child: Column(
children: [
Container(
width: 190.w,
height: 190.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18.r),
),
),
SizedBox(height: 20.h),
Text(
appName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.w800,
),
),
],
),
);
}
}
class _AccountSection extends StatelessWidget {
const _AccountSection({
required this.title,
required this.description,
required this.accounts,
});
final String title;
final String description;
final List<RemoteControlAccount> accounts;
@override
Widget build(BuildContext context) {
final items = [
for (final account in accounts)
_AccountActionCard(
text: account.phone,
icon: Icons.manage_accounts_outlined,
),
const _AccountActionCard(
text: '新增账户',
icon: Icons.person_add_alt_1_outlined,
),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 38.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(width: 24.w),
Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: 6.h),
child: Text(
description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFFAAEFFF),
fontSize: 17.sp,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
SizedBox(height: 28.h),
Wrap(
spacing: 20.w,
runSpacing: 18.h,
children: items,
),
],
);
}
}
class _AccountActionCard extends StatelessWidget {
const _AccountActionCard({
required this.text,
required this.icon,
});
final String text;
final IconData icon;
@override
Widget build(BuildContext context) {
return Container(
width: 235.w,
height: 92.h,
padding: EdgeInsets.symmetric(horizontal: 22.w),
decoration: settingsButtonDecoration(),
child: Row(
children: [
Icon(
icon,
color: const Color(0xFF78C8F5).withValues(alpha: 0.78),
size: 42.w,
),
SizedBox(width: 16.w),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 22.sp,
fontWeight: FontWeight.w800,
),
),
),
],
),
);
}
......
import 'package:equatable/equatable.dart';
class SoftwareUpdateInfo extends Equatable {
const SoftwareUpdateInfo({
required this.title,
required this.currentVersion,
required this.latestVersion,
required this.updateNotes,
required this.actionText,
required this.canUpdate,
this.footerText,
});
final String title;
final String currentVersion;
final String latestVersion;
final List<String> updateNotes;
final String actionText;
final bool canUpdate;
final String? footerText;
@override
List<Object?> get props => [
title,
currentVersion,
latestVersion,
updateNotes,
actionText,
canUpdate,
footerText,
];
}
class SoftwareUpdateIndexState extends Equatable {
const SoftwareUpdateIndexState();
const SoftwareUpdateIndexState({
this.productName = 'TSAAS ICU 动物医疗监护舱',
this.modelName = '730SE-CICU-A',
this.tabletSystem = const SoftwareUpdateInfo(
title: '平板系统',
currentVersion: 'V2.32.4',
latestVersion: 'V2.32.5',
updateNotes: [
'1.重置UI界面',
'2.优化已知bug',
],
actionText: '立即更新',
canUpdate: true,
),
this.deviceFirmware = const SoftwareUpdateInfo(
title: '设备固件OTA(实体按键)',
currentVersion: 'V2.32.4',
latestVersion: 'V2.32.4(当前版本)',
updateNotes: [
'修复bug',
],
actionText: '已是最新版本',
canUpdate: false,
footerText: '*固件升级时间较长,请勿断电',
),
});
final String productName;
final String modelName;
final SoftwareUpdateInfo tabletSystem;
final SoftwareUpdateInfo deviceFirmware;
@override
List<Object?> get props => [];
List<Object?> get props => [
productName,
modelName,
tabletSystem,
deviceFirmware,
];
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../widgets/settings_panel_frame.dart';
import '../cubit/software_update_index_cubit.dart';
import '../cubit/software_update_index_state.dart';
class SoftwareUpdatePanel extends StatelessWidget {
const SoftwareUpdatePanel({super.key});
......@@ -10,17 +13,275 @@ class SoftwareUpdatePanel extends StatelessWidget {
Widget build(BuildContext context) {
return SettingsPanelFrame(
backgroundAsset: settingsAssetPanelMain,
padding: EdgeInsets.all(42.w),
child: Align(
alignment: Alignment.topLeft,
child: Text(
'软件更新',
padding: EdgeInsets.fromLTRB(42.w, 54.h, 42.w, 48.h),
child: BlocBuilder<SoftwareUpdateIndexCubit, SoftwareUpdateIndexState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_UpdateHeader(state: state),
SizedBox(height: 30.h),
Divider(color: const Color(0xFF4CA7D2).withValues(alpha: 0.36)),
SizedBox(height: 36.h),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _UpdateColumn(info: state.tabletSystem)),
Container(
width: 1.w,
margin: EdgeInsets.symmetric(horizontal: 38.w),
color: const Color(0xFF3A9BD2).withValues(alpha: 0.42),
),
Expanded(child: _UpdateColumn(info: state.deviceFirmware)),
],
),
),
],
);
},
),
);
}
}
class _UpdateHeader extends StatelessWidget {
const _UpdateHeader({required this.state});
final SoftwareUpdateIndexState state;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 22.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
state.productName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 42.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 22.h),
Text(
state.modelName,
style: TextStyle(
color: Colors.white,
fontSize: 36.sp,
fontWeight: FontWeight.w800,
),
),
],
),
),
),
SizedBox(width: 34.w),
const _CheckUpdateButton(),
],
);
}
}
class _CheckUpdateButton extends StatelessWidget {
const _CheckUpdateButton();
@override
Widget build(BuildContext context) {
return Container(
width: 245.w,
height: 104.h,
padding: EdgeInsets.symmetric(horizontal: 24.w),
decoration: settingsButtonDecoration(active: true),
child: Row(
children: [
Container(
width: 52.w,
height: 52.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF78C8F5).withValues(alpha: 0.86),
width: 3.w,
),
),
child: Icon(
Icons.arrow_upward,
color: const Color(0xFF78C8F5).withValues(alpha: 0.9),
size: 32.w,
),
),
SizedBox(width: 22.w),
Expanded(
child: Text(
'检查更新',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 23.sp,
fontWeight: FontWeight.w800,
),
),
),
],
),
);
}
}
class _UpdateColumn extends StatelessWidget {
const _UpdateColumn({required this.info});
final SoftwareUpdateInfo info;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
info.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 34.sp,
fontSize: 38.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 42.h),
_VersionLine(label: '当前版本:', value: info.currentVersion),
SizedBox(height: 20.h),
_VersionLine(label: '最新版本:', value: info.latestVersion),
SizedBox(height: 22.h),
Text(
'更新内容:',
style: TextStyle(
color: Colors.white,
fontSize: 27.sp,
fontWeight: FontWeight.w800,
),
),
SizedBox(height: 18.h),
for (final note in info.updateNotes)
Padding(
padding: EdgeInsets.only(bottom: 6.h),
child: Text(
note,
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.w500,
),
),
),
const Spacer(),
Center(child: _UpdateActionButton(info: info)),
if (info.footerText != null) ...[
SizedBox(height: 18.h),
Center(
child: Text(
info.footerText!,
style: TextStyle(
color: Colors.white,
fontSize: 17.sp,
fontWeight: FontWeight.w700,
),
),
),
] else
SizedBox(height: 38.h),
],
);
}
}
class _VersionLine extends StatelessWidget {
const _VersionLine({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
return Text.rich(
TextSpan(
children: [
TextSpan(text: label),
TextSpan(text: value),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 26.sp,
fontWeight: FontWeight.w800,
),
);
}
}
class _UpdateActionButton extends StatelessWidget {
const _UpdateActionButton({required this.info});
final SoftwareUpdateInfo info;
@override
Widget build(BuildContext context) {
final foreground =
info.canUpdate ? Colors.white : Colors.white.withValues(alpha: 0.42);
return Container(
width: 345.w,
height: 70.h,
padding: EdgeInsets.symmetric(horizontal: 28.w),
decoration: settingsButtonDecoration(active: info.canUpdate),
child: Row(
children: [
Text(
'《《',
style: TextStyle(
color: const Color(0xFF67C9F5)
.withValues(alpha: info.canUpdate ? 0.72 : 0.34),
fontSize: 26.sp,
fontWeight: FontWeight.w700,
),
),
Expanded(
child: Text(
info.actionText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: foreground,
fontSize: 26.sp,
fontWeight: FontWeight.w800,
),
),
),
Text(
'》》',
style: TextStyle(
color: const Color(0xFF67C9F5)
.withValues(alpha: info.canUpdate ? 0.72 : 0.34),
fontSize: 26.sp,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment