所有一级页面和不分二级页面
This commit is contained in:
45
android/.kotlin/errors/errors-1779355041254.log
Normal file
45
android/.kotlin/errors/errors-1779355041254.log
Normal file
@@ -0,0 +1,45 @@
|
||||
kotlin version: 2.2.20
|
||||
error message: Failed connecting to the daemon in 4 retries
|
||||
|
||||
error message: Daemon compilation failed: Could not connect to Kotlin compile daemon
|
||||
java.lang.RuntimeException: Could not connect to Kotlin compile daemon
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111)
|
||||
at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74)
|
||||
at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
|
||||
at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60)
|
||||
at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54)
|
||||
at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
|
||||
at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59)
|
||||
at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169)
|
||||
at org.gradle.internal.Factories$1.create(Factories.java:31)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)
|
||||
at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164)
|
||||
at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133)
|
||||
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source)
|
||||
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
|
||||
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
|
||||
at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- 存储权限 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
BIN
assets/images/chucao.png
Normal file
BIN
assets/images/chucao.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
BIN
assets/images/dianzhan.png
Normal file
BIN
assets/images/dianzhan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 521 KiB |
BIN
assets/images/qinxi.png
Normal file
BIN
assets/images/qinxi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/images/xunjian.png
Normal file
BIN
assets/images/xunjian.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
assets/images/yiwancheng.png
Normal file
BIN
assets/images/yiwancheng.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -2,4 +2,5 @@ description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- get_it: true
|
||||
- provider: true
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
197
docs/TAB_CONFIG_DEVELOPMENT_GUIDE.md
Normal file
197
docs/TAB_CONFIG_DEVELOPMENT_GUIDE.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Tab 配置系统开发规范
|
||||
|
||||
## 📋 功能说明
|
||||
|
||||
Tab 配置系统支持动态开关控制底部导航栏的 Tab 项显示/隐藏,配置自动保存到 SharedPreferences。
|
||||
|
||||
## 🔧 添加新 Tab 项的步骤
|
||||
|
||||
当需要添加新的 Tab 项时,必须按以下顺序修改 **3 个文件**:
|
||||
|
||||
### 1️⃣ 修改 `tab_config.dart` - 添加默认配置
|
||||
|
||||
**文件路径:** `lib/features/main_container/domain/tab_config.dart`
|
||||
|
||||
在 `TabConfig.defaultConfig()` 方法的 `items` 列表中添加新的 `TabConfigItem`:
|
||||
|
||||
```dart
|
||||
TabConfigItem(
|
||||
id: 'your_tab_id', // 唯一标识符(小写+下划线)
|
||||
name: '显示名称', // 中文名称(简洁,建议2-4个字)
|
||||
nameEn: 'Display Name', // 英文名称
|
||||
icon: 'icon_name_rounded', // 图标名称(Material Icons)
|
||||
isEnabled: true, // 默认是否启用(true/false)
|
||||
order: 7, // 排序序号(从1开始递增)
|
||||
),
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- `id` 必须唯一,建议使用有意义的英文标识
|
||||
- `name` 要简洁,符合 TAB 命名规范
|
||||
- `order` 决定 Tab 在导航栏中的显示顺序
|
||||
- `isEnabled` 控制默认是否显示该 Tab
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ 修改 `custom_main_container.dart` - 添加页面映射
|
||||
|
||||
**文件路径:** `lib/features/main_container/presentation/pages/custom_main_container.dart`
|
||||
|
||||
#### 2.1 导入新页面
|
||||
|
||||
在文件顶部添加新页面的 import:
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/xxx/presentation/pages/your_page.dart';
|
||||
```
|
||||
|
||||
#### 2.2 在 `_buildPages` 方法中添加 case
|
||||
|
||||
```dart
|
||||
List<Widget> _buildPages(List<dynamic> enabledTabs) {
|
||||
return enabledTabs.map((tab) {
|
||||
switch (tab.id) {
|
||||
case 'home_v2':
|
||||
return const HomeV2Page();
|
||||
case 'home':
|
||||
return const HomePage();
|
||||
case 'device':
|
||||
return const DeviceStatusPage();
|
||||
case 'ai':
|
||||
return const AiPage();
|
||||
case 'warning':
|
||||
return const WarningCenterPage();
|
||||
case 'my':
|
||||
return const MyPage();
|
||||
case 'your_tab_id': // 👈 新增这个 case
|
||||
return const YourPage();
|
||||
default:
|
||||
return const HomePage();
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ 修改 `tab_settings_page.dart` - 添加图标映射
|
||||
|
||||
**文件路径:** `lib/features/main_container/presentation/pages/tab_settings_page.dart`
|
||||
|
||||
在 `_getIconData` 方法中添加新图标的映射:
|
||||
|
||||
```dart
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'home_rounded':
|
||||
return Icons.home_rounded;
|
||||
case 'grid_view_rounded':
|
||||
return Icons.grid_view_rounded;
|
||||
case 'devices_rounded':
|
||||
return Icons.devices_rounded;
|
||||
case 'auto_awesome_rounded':
|
||||
return Icons.auto_awesome_rounded;
|
||||
case 'warning_amber_rounded':
|
||||
return Icons.warning_amber_rounded;
|
||||
case 'person_rounded':
|
||||
return Icons.person_rounded;
|
||||
case 'your_icon_name': // 👈 新增这个 case
|
||||
return Icons.your_icon_name;
|
||||
default:
|
||||
return Icons.home_rounded;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意:** 如果使用的是 Material Icons 标准图标,通常不需要修改此文件,除非使用了特殊图标。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 检查清单
|
||||
|
||||
添加新 Tab 后,请确认:
|
||||
|
||||
- [ ] 已在 `tab_config.dart` 中添加 `TabConfigItem` 配置
|
||||
- [ ] 已在 `custom_main_container.dart` 中导入新页面
|
||||
- [ ] 已在 `custom_main_container.dart` 的 `switch` 中添加 case 分支
|
||||
- [ ] 已在 `tab_settings_page.dart` 中添加图标映射(如需要)
|
||||
- [ ] `id`、`order` 没有与其他 Tab 冲突
|
||||
- [ ] 测试开关功能是否正常
|
||||
- [ ] 测试切换 Tab 是否正常显示对应页面
|
||||
|
||||
---
|
||||
|
||||
## 🚀 完整示例:添加"消息"Tab
|
||||
|
||||
假设要添加一个"消息"Tab,显示消息中心页面:
|
||||
|
||||
### 步骤 1:修改 `tab_config.dart`
|
||||
|
||||
```dart
|
||||
TabConfigItem(
|
||||
id: 'message',
|
||||
name: '消息',
|
||||
nameEn: 'Messages',
|
||||
icon: 'notifications_rounded',
|
||||
isEnabled: true,
|
||||
order: 7,
|
||||
),
|
||||
```
|
||||
|
||||
### 步骤 2:修改 `custom_main_container.dart`
|
||||
|
||||
```dart
|
||||
// 顶部添加导入
|
||||
import 'package:maibu_satabot_v2/features/message/presentation/pages/message_page.dart';
|
||||
|
||||
// 在 _buildPages 方法中添加
|
||||
case 'message':
|
||||
return const MessagePage();
|
||||
```
|
||||
|
||||
### 步骤 3:修改 `tab_settings_page.dart`
|
||||
|
||||
```dart
|
||||
case 'notifications_rounded':
|
||||
return Icons.notifications_rounded;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见问题
|
||||
|
||||
### Q1: 添加新 Tab 后看不到?
|
||||
**A:** 点击 Tab 配置页面的"重置为默认"按钮,或清除应用数据重新运行。
|
||||
|
||||
### Q2: 点击 Tab 后页面不显示?
|
||||
**A:** 检查 `custom_main_container.dart` 中的 `switch-case` 是否正确添加了新 Tab 的映射。
|
||||
|
||||
### Q3: 图标显示不正确?
|
||||
**A:** 检查 `tab_settings_page.dart` 中的 `_getIconData` 方法是否添加了对应的图标映射。
|
||||
|
||||
### Q4: Tab 顺序不对?
|
||||
**A:** 检查 `tab_config.dart` 中各 Tab 的 `order` 值,确保按期望顺序排列。
|
||||
|
||||
---
|
||||
|
||||
## 📝 核心原理
|
||||
|
||||
1. **配置管理:** `TabConfigCubit` 管理所有 Tab 的配置和选中状态
|
||||
2. **持久化:** 配置自动保存到 SharedPreferences,重启应用后保持
|
||||
3. **动态渲染:** 底部导航栏根据 `enabledItems` 动态渲染启用的 Tab
|
||||
4. **页面切换:** 使用 `IndexedStack` 保持各页面状态,通过 `selectedIndex` 切换
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
1. **Tab 命名:** 保持简洁,中文 2-4 个字,避免过长
|
||||
2. **默认状态:** 新功能建议设置 `isEnabled: false`,通过灰度发布逐步开放
|
||||
3. **排序规划:** 预留 order 间隔(如 10, 20, 30),方便后续插入新 Tab
|
||||
4. **图标选择:** 优先使用 Material Icons 的 `_rounded` 版本,风格统一
|
||||
5. **测试验证:** 每次修改后测试开关、切换、持久化等功能
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2026-05-20
|
||||
**维护者:** 开发团队
|
||||
@@ -66,5 +66,16 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<!-- 相机权限 -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>需要使用相机拍照和录像</string>
|
||||
<!-- 相册权限 -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>需要访问相册以选择图片和视频</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>需要保存拍摄的照片和视频到相册</string>
|
||||
<!-- 麦克风权限(录像需要) -->
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>录像时需要使用麦克风录制声音</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
104
lib/core/consts/workorder_consts.dart
Normal file
104
lib/core/consts/workorder_consts.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 工单模块颜色常量
|
||||
class WorkOrderColors {
|
||||
WorkOrderColors._();
|
||||
|
||||
/// 主色调
|
||||
static const Color primary = Color(0xFF165DFF);
|
||||
|
||||
/// 待处理色(橙色)
|
||||
static const Color pending = Color(0xFFFF7D00);
|
||||
|
||||
/// 执行中色(蓝色)
|
||||
static const Color executing = Color(0xFF165DFF);
|
||||
|
||||
/// 已完成色(绿色)
|
||||
static const Color completed = Color(0xFF00B42A);
|
||||
|
||||
/// 背景色
|
||||
static const Color background = Color(0xFFFFFFFF);
|
||||
|
||||
/// 页面背景色
|
||||
static const Color pageBackground = Color(0xFFF7F8FA);
|
||||
|
||||
/// 主要文字色
|
||||
static const Color primaryText = Color(0xFF1D2129);
|
||||
|
||||
/// 次要文字色
|
||||
static const Color secondaryText = Color(0xFF4E5969);
|
||||
|
||||
/// 辅助文字色
|
||||
static const Color auxiliaryText = Color(0xFF86909C);
|
||||
|
||||
/// 分割线颜色
|
||||
static const Color divider = Color(0xFFF0F0F0);
|
||||
|
||||
/// 进度条背景色
|
||||
static const Color progressBackground = Color(0xFFF0F0F0);
|
||||
|
||||
/// 卡片阴影
|
||||
static List<BoxShadow> get cardShadow => [
|
||||
const BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// 工单模块尺寸常量
|
||||
class WorkOrderDimensions {
|
||||
WorkOrderDimensions._();
|
||||
|
||||
/// 通用圆角
|
||||
static const double borderRadius = 16.0;
|
||||
|
||||
/// 水平边距
|
||||
static const double horizontalPadding = 16.0;
|
||||
|
||||
/// 模块间距
|
||||
static const double moduleSpacing = 20.0;
|
||||
|
||||
/// 导航栏高度
|
||||
static const double navigationBarHeight = 44.0;
|
||||
|
||||
/// 标签栏高度
|
||||
static const double tabBarHeight = 44.0;
|
||||
}
|
||||
|
||||
/// 全局应用常量 (供 UI 组件使用)
|
||||
class AppConstants {
|
||||
AppConstants._();
|
||||
|
||||
static const Color primaryColor = WorkOrderColors.primary;
|
||||
static const Color backgroundColor = WorkOrderColors.background;
|
||||
static const Color primaryTextColor = WorkOrderColors.primaryText;
|
||||
static const Color secondaryTextColor = WorkOrderColors.secondaryText;
|
||||
static const Color auxiliaryTextColor = WorkOrderColors.auxiliaryText;
|
||||
static const Color pendingColor = WorkOrderColors.pending;
|
||||
static const Color executingColor = WorkOrderColors.executing;
|
||||
static const Color completedColor = WorkOrderColors.completed;
|
||||
|
||||
static const double borderRadius = WorkOrderDimensions.borderRadius;
|
||||
static const double horizontalPadding = WorkOrderDimensions.horizontalPadding;
|
||||
static const double moduleSpacing = WorkOrderDimensions.moduleSpacing;
|
||||
static const double navigationBarHeight = WorkOrderDimensions.navigationBarHeight;
|
||||
|
||||
static List<BoxShadow> get cardShadow => WorkOrderColors.cardShadow;
|
||||
}
|
||||
|
||||
/// 工单状态枚举
|
||||
enum WorkOrderStatus {
|
||||
pending, // 待处理
|
||||
executing, // 执行中
|
||||
completed, // 已完成
|
||||
all, // 全部
|
||||
}
|
||||
|
||||
/// 工单优先级枚举
|
||||
enum WorkOrderPriority {
|
||||
high, // 高
|
||||
medium, // 中
|
||||
low, // 低
|
||||
}
|
||||
@@ -61,6 +61,18 @@ import '../../features/v2/home/data/repositories/home_repository_impl.dart';
|
||||
import '../../features/v2/home/domain/repositories/home_repository.dart';
|
||||
import '../../features/v2/home/domain/usecases/get_home_data_usecase.dart';
|
||||
import '../../features/v2/home/presentation/bloc/home_v2_bloc.dart';
|
||||
import '../../features/v2/device_list/data/datasources/device_remote_datasource.dart' as device_v2;
|
||||
import '../../features/v2/device_list/data/repositories/device_repository_impl.dart' as device_v2_repo;
|
||||
import '../../features/v2/device_list/domain/repositories/device_repository.dart' as device_v2_domain;
|
||||
import '../../features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart' as device_v2_usecase;
|
||||
import '../../features/v2/device_list/presentation/bloc/device_status_bloc.dart' as device_v2_bloc;
|
||||
import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart';
|
||||
import '../../features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart';
|
||||
import '../../features/v2/waring_center/data/repositories/alarm_repository_impl.dart';
|
||||
import '../../features/v2/waring_center/domain/repositories/alarm_repository.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart';
|
||||
import '../../features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart';
|
||||
import '../../features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
||||
import '../app/app_user_cubit.dart';
|
||||
import '../localization/locale_cubit.dart';
|
||||
import '../../features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
@@ -194,6 +206,22 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<GetHomeDataUseCase>(() => GetHomeDataUseCase(sl()));
|
||||
sl.registerFactory<HomeV2Bloc>(() => HomeV2Bloc(sl()));
|
||||
|
||||
/// Device Status V2
|
||||
sl.registerLazySingleton<device_v2.DeviceRemoteDataSource>(() => device_v2.DeviceRemoteDataSourceImpl());
|
||||
sl.registerLazySingleton<device_v2_domain.DeviceRepository>(() => device_v2_repo.DeviceRepositoryImpl(remoteDataSource: sl()));
|
||||
sl.registerLazySingleton<device_v2_usecase.GetDeviceStatusDataUseCase>(() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl()));
|
||||
sl.registerFactory<device_v2_bloc.DeviceStatusBloc>(() => device_v2_bloc.DeviceStatusBloc(sl()));
|
||||
|
||||
/// Alarm Center V2
|
||||
sl.registerLazySingleton<AlarmRemoteDataSource>(() => AlarmRemoteDataSourceImpl());
|
||||
sl.registerLazySingleton<AlarmRepository>(() => AlarmRepositoryImpl(sl()));
|
||||
sl.registerLazySingleton<GetAlarmListUseCase>(() => GetAlarmListUseCase(sl()));
|
||||
sl.registerLazySingleton<GetAlarmCountUseCase>(() => GetAlarmCountUseCase(sl()));
|
||||
sl.registerFactory<AlarmCubit>(() => AlarmCubit(
|
||||
getAlarmListUseCase: sl(),
|
||||
getAlarmCountUseCase: sl(),
|
||||
));
|
||||
|
||||
|
||||
/// 5. 状态管理 (Cubit/Bloc)
|
||||
sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册
|
||||
|
||||
@@ -6,3 +6,7 @@ class Failure {
|
||||
class NetworkFailure extends Failure {
|
||||
NetworkFailure(super.message);
|
||||
}
|
||||
|
||||
class ServerFailure extends Failure {
|
||||
ServerFailure(super.message);
|
||||
}
|
||||
|
||||
10
lib/core/error/workorder_failure.dart
Normal file
10
lib/core/error/workorder_failure.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
|
||||
/// 工单模块特定的失败类型
|
||||
class WorkOrderFailure extends Failure {
|
||||
WorkOrderFailure(super.message);
|
||||
}
|
||||
|
||||
class UnknownFailure extends WorkOrderFailure {
|
||||
UnknownFailure({required String message}) : super(message);
|
||||
}
|
||||
@@ -97,7 +97,7 @@ class TcpClient {
|
||||
try {
|
||||
var packets = _decoder.decode(data);
|
||||
//debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}');
|
||||
_logger.logWithLevel('✅ [TCP] 解码成功,包数量:${packets.length}',shouldLog: true);
|
||||
_logger.logWithLevel('✅ [TCP] 解码成功,包数量:${packets.length}',shouldLog: false);
|
||||
for (var packet in packets) {
|
||||
// 🔥 最根部日志:收到任何推送都打印
|
||||
// debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}');
|
||||
@@ -106,23 +106,23 @@ class TcpClient {
|
||||
if (!_controller.isClosed) {
|
||||
_controller.add(packet);
|
||||
//debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}');
|
||||
_logger.logWithLevel('✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}' ,shouldLog: true);
|
||||
_logger.logWithLevel('✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}' ,shouldLog: false);
|
||||
}
|
||||
if (packet.command == 0xFF) {
|
||||
//debugPrint('收到服务端心跳,自动回复...');
|
||||
_logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...',shouldLog: true);
|
||||
_logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...',shouldLog: false);
|
||||
sendHeartbeat(); // 回复 AB AA FF AA AB
|
||||
}
|
||||
if (packet.command == 0x03) {
|
||||
// debugPrint('⚠️ 收到认证响应:${packet.payload}');
|
||||
_logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}',shouldLog: true);
|
||||
_logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}',shouldLog: false);
|
||||
// 解析 payload 看是否有错误信息
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// debugPrint('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常
|
||||
_logger.logWithLevel('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace', shouldLog: true);
|
||||
_logger.logWithLevel('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace', shouldLog: false);
|
||||
}
|
||||
},
|
||||
onDone: (){
|
||||
@@ -130,7 +130,7 @@ class TcpClient {
|
||||
// debugPrint('来到断线重连!');
|
||||
if (!_isSwitching) {
|
||||
//debugPrint('onDone❌ [TCP] 连接已断开!');
|
||||
_logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: true);
|
||||
_logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: false);
|
||||
_handleDisconnect();
|
||||
}
|
||||
_isSwitching = false; // 重置标志,以免影响下次
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/routes/home_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/pages/tab_settings_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/routes/my_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/waring_center/presentation/routes/warning_center_routes.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/routes/alarm_center_routes.dart';
|
||||
|
||||
import '../../features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_state.dart';
|
||||
@@ -22,11 +22,14 @@ GoRouter createRouter(AuthCubit authCubit) {
|
||||
redirect: (context, state) {
|
||||
final loggedIn = authCubit.state is AuthAuthenticated;
|
||||
final loggingIn = state.matchedLocation == RoutePaths.login;
|
||||
final registering = state.matchedLocation == RoutePaths.register;
|
||||
|
||||
if (!loggedIn && !loggingIn) {
|
||||
// 如果未登录,只允许访问 login 和 register 页面
|
||||
if (!loggedIn && !loggingIn && !registering) {
|
||||
return RoutePaths.login;
|
||||
}
|
||||
if (loggedIn && loggingIn) {
|
||||
// 如果已登录,尝试访问 login 或 register,则跳转到首页
|
||||
if (loggedIn && (loggingIn || registering)) {
|
||||
return RoutePaths.home;
|
||||
}
|
||||
return null;
|
||||
@@ -60,7 +63,7 @@ GoRouter createRouter(AuthCubit authCubit) {
|
||||
...HomeRoutes.routes,
|
||||
...AiRoutes.routes,
|
||||
...MyRoutes.routes,
|
||||
...WarningCenterRoutes.routes,
|
||||
...AlarmCenterRoutes.routes,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,145 +45,157 @@ class _LoginPageState extends State<LoginPage> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}')));
|
||||
}
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// 顶部居中图片和认证信息
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/huawei.png',
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
"通过华为云生态技术认证,请放心使用,证书编号C202605301",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.4,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 顶部区域:图片 + 认证信息 + 标题(压缩空间)
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 顶部居中图片和认证信息
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/huawei.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Text(
|
||||
"通过华为云生态技术认证,请放心使用,证书编号C202605301",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"账号登录",
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"账号登录",
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 账号输入框
|
||||
_buildInputLabel("账号"),
|
||||
TextField(
|
||||
controller: _userCtrl,
|
||||
cursorColor: Colors.black,
|
||||
decoration: _inputDecoration(hint: "请输入用户名"),
|
||||
),
|
||||
// 表单区域
|
||||
// 账号输入框
|
||||
_buildInputLabel("账号"),
|
||||
TextField(
|
||||
controller: _userCtrl,
|
||||
cursorColor: Colors.black,
|
||||
decoration: _inputDecoration(hint: "请输入用户名"),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 密码输入框
|
||||
_buildInputLabel("密码"),
|
||||
TextField(
|
||||
controller: _pwdCtrl,
|
||||
obscureText: _obscurePwd,
|
||||
cursorColor: Colors.black,
|
||||
decoration: _inputDecoration(
|
||||
hint: "请输入密码",
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: Colors.grey, size: 20),
|
||||
onPressed: () => setState(() => _obscurePwd = !_obscurePwd),
|
||||
// 密码输入框
|
||||
_buildInputLabel("密码"),
|
||||
TextField(
|
||||
controller: _pwdCtrl,
|
||||
obscureText: _obscurePwd,
|
||||
cursorColor: Colors.black,
|
||||
decoration: _inputDecoration(
|
||||
hint: "请输入密码",
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: Colors.grey, size: 20),
|
||||
onPressed: () => setState(() => _obscurePwd = !_obscurePwd),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 协议勾选区域
|
||||
_buildProtocolSection(),
|
||||
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 底部区域:登录按钮 + 注册链接
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: BlocBuilder<LoginCubit, LoginState>(
|
||||
builder: (context, state) {
|
||||
bool isLoading = state is LoginLoading;
|
||||
|
||||
return CCPrimaryButton(
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
if (!_isAgreed) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请先阅读并同意用户协议'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_handleLogin();
|
||||
},
|
||||
text: isLoading ? '登录中...' : '登 录',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"没有账号吗?",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
debugPrint(' 点击了立即注册');
|
||||
debugPrint('👉 跳转路径: ${RoutePaths.register}');
|
||||
context.go(RoutePaths.register);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Text(
|
||||
"立即注册",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 协议勾选区域
|
||||
_buildProtocolSection(),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 登录按钮 (使用你的 CCPrimaryButton 并增加状态控制)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: BlocBuilder<LoginCubit, LoginState>(
|
||||
builder: (context, state) {
|
||||
// 关键:判断是否正在 Loading
|
||||
bool isLoading = state is LoginLoading;
|
||||
|
||||
return CCPrimaryButton(
|
||||
// 核心修复:正在加载时 → onPressed 为 null,按钮禁用
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
if (!_isAgreed) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('请先阅读并同意用户协议'),
|
||||
duration: Duration(seconds: 1), // 这里设置显示时长:2秒
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
_handleLogin();
|
||||
},
|
||||
text: isLoading ? '登录中...' : '登 录',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"没有账号吗?",
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
print('点击了立即注册');
|
||||
print('跳转路径: ${RoutePaths.register}');
|
||||
context.go(RoutePaths.register);
|
||||
},
|
||||
child: const Text(
|
||||
"立即注册",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../../../../core/error/failure.dart' as core_error;
|
||||
import '../../../auth/domain/errors/auth_failure.dart';
|
||||
import '../../data/models/device_add_path_point_model.dart';
|
||||
import '../../data/models/device_work_area_param_model.dart';
|
||||
@@ -33,7 +34,7 @@ class GeneratePathUseCaseImpl implements GeneratePathUseCase {
|
||||
final result = await _repository.generatePath(reference: reference, heading: heading, outer: outer, holes: holes, workType: workType);
|
||||
return Right(result); // 成功时返回 Right
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(e.toString(), 1)); // 失败时返回 Left
|
||||
return Left(core_error.ServerFailure(e.toString())); // 失败时返回 Left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,17 +80,17 @@ class TabConfig extends Equatable {
|
||||
items: [
|
||||
TabConfigItem(
|
||||
id: 'home_v2',
|
||||
name: '首页V2',
|
||||
nameEn: 'Home V2',
|
||||
name: '首页',
|
||||
nameEn: 'Home',
|
||||
icon: 'home_rounded',
|
||||
isEnabled: false,
|
||||
isEnabled: true,
|
||||
order: 1,
|
||||
),
|
||||
TabConfigItem(
|
||||
id: 'home',
|
||||
name: '状态',
|
||||
nameEn: 'Status',
|
||||
icon: 'grid_view_rounded',
|
||||
id: 'device',
|
||||
name: '设备',
|
||||
nameEn: 'Device',
|
||||
icon: 'devices_rounded',
|
||||
isEnabled: true,
|
||||
order: 2,
|
||||
),
|
||||
@@ -99,7 +99,7 @@ class TabConfig extends Equatable {
|
||||
name: 'AI',
|
||||
nameEn: 'AI',
|
||||
icon: 'auto_awesome_rounded',
|
||||
isEnabled: true,
|
||||
isEnabled: false,
|
||||
order: 3,
|
||||
),
|
||||
TabConfigItem(
|
||||
@@ -110,13 +110,37 @@ class TabConfig extends Equatable {
|
||||
isEnabled: true,
|
||||
order: 4,
|
||||
),
|
||||
TabConfigItem(
|
||||
id: 'workorder',
|
||||
name: '工单',
|
||||
nameEn: 'WorkOrder',
|
||||
icon: 'assignment_rounded',
|
||||
isEnabled: true,
|
||||
order: 5,
|
||||
),
|
||||
TabConfigItem(
|
||||
id: 'report',
|
||||
name: '上报',
|
||||
nameEn: 'Report',
|
||||
icon: 'post_add_rounded',
|
||||
isEnabled: true,
|
||||
order: 6,
|
||||
),
|
||||
TabConfigItem(
|
||||
id: 'my',
|
||||
name: '旧我的',
|
||||
nameEn: 'Old Profile',
|
||||
icon: 'person_rounded',
|
||||
isEnabled: false,
|
||||
order: 7,
|
||||
),
|
||||
TabConfigItem(
|
||||
id: 'me',
|
||||
name: '我的',
|
||||
nameEn: 'Profile',
|
||||
icon: 'person_rounded',
|
||||
isEnabled: true,
|
||||
order: 5,
|
||||
order: 8,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart';
|
||||
|
||||
class TabConfigState {
|
||||
final TabConfig config;
|
||||
final int selectedIndex;
|
||||
|
||||
const TabConfigState({
|
||||
required this.config,
|
||||
this.selectedIndex = 0,
|
||||
});
|
||||
|
||||
TabConfigState copyWith({
|
||||
TabConfig? config,
|
||||
int? selectedIndex,
|
||||
}) {
|
||||
return TabConfigState(
|
||||
config: config ?? this.config,
|
||||
selectedIndex: selectedIndex ?? this.selectedIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabConfigCubit extends Cubit<TabConfigState> {
|
||||
TabConfigCubit() : super(TabConfigState(config: TabConfig.defaultConfig()));
|
||||
|
||||
/// 切换 Tab 的启用状态
|
||||
void toggleTab(String tabId) {
|
||||
final newConfig = state.config.toggleItem(tabId);
|
||||
|
||||
// 如果当前选中的 Tab 被禁用,自动切换到第一个启用的 Tab
|
||||
int newIndex = state.selectedIndex;
|
||||
if (newIndex >= newConfig.enabledItems.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
|
||||
emit(state.copyWith(config: newConfig, selectedIndex: newIndex));
|
||||
}
|
||||
|
||||
/// 切换选中的 Tab
|
||||
void selectTab(int index) {
|
||||
if (index >= 0 && index < state.config.enabledItems.length) {
|
||||
emit(state.copyWith(selectedIndex: index));
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置为默认配置
|
||||
void resetToDefault() {
|
||||
emit(TabConfigState(config: TabConfig.defaultConfig()));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'dart:convert';
|
||||
@@ -9,8 +10,9 @@ class TabConfigInitial extends TabConfigState {}
|
||||
|
||||
class TabConfigLoaded extends TabConfigState {
|
||||
final TabConfig config;
|
||||
final int selectedIndex;
|
||||
|
||||
TabConfigLoaded(this.config);
|
||||
TabConfigLoaded(this.config, {this.selectedIndex = 0});
|
||||
}
|
||||
|
||||
class TabConfigCubit extends Cubit<TabConfigState> {
|
||||
@@ -22,31 +24,13 @@ class TabConfigCubit extends Cubit<TabConfigState> {
|
||||
}
|
||||
|
||||
void _loadConfig() {
|
||||
try {
|
||||
final configJson = _prefs.getString(_configKey);
|
||||
if (configJson != null) {
|
||||
final List<dynamic> decoded = jsonDecode(configJson);
|
||||
final items = decoded.map((e) {
|
||||
final map = Map<String, dynamic>.from(e);
|
||||
return TabConfigItem(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String,
|
||||
nameEn: map['nameEn'] as String,
|
||||
icon: map['icon'] as String,
|
||||
isEnabled: map['isEnabled'] as bool,
|
||||
order: map['order'] as int,
|
||||
);
|
||||
}).toList();
|
||||
emit(TabConfigLoaded(TabConfig(items: items)));
|
||||
} else {
|
||||
final defaultConfig = TabConfig.defaultConfig();
|
||||
_saveConfig(defaultConfig);
|
||||
emit(TabConfigLoaded(defaultConfig));
|
||||
}
|
||||
} catch (e) {
|
||||
final defaultConfig = TabConfig.defaultConfig();
|
||||
emit(TabConfigLoaded(defaultConfig));
|
||||
}
|
||||
debugPrint('🔍 [TabConfigCubit] _loadConfig 被调用');
|
||||
// 直接使用代码中的默认配置,不持久化
|
||||
final defaultConfig = TabConfig.defaultConfig();
|
||||
debugPrint('🔍 [TabConfigCubit] 默认配置 Tab 数量: ${defaultConfig.items.length}');
|
||||
debugPrint('🔍 [TabConfigCubit] 启用的 Tab: ${defaultConfig.enabledItems.map((e) => e.id).toList()}');
|
||||
emit(TabConfigLoaded(defaultConfig));
|
||||
debugPrint('✅ [TabConfigCubit] 状态已 emit');
|
||||
}
|
||||
|
||||
void _saveConfig(TabConfig config) {
|
||||
@@ -70,17 +54,25 @@ class TabConfigCubit extends Cubit<TabConfigState> {
|
||||
final currentState = state as TabConfigLoaded;
|
||||
final newConfig = currentState.config.toggleItem(itemId);
|
||||
|
||||
// 只有配置真正改变时才保存和 emit
|
||||
if (newConfig != currentState.config) {
|
||||
_saveConfig(newConfig);
|
||||
emit(TabConfigLoaded(newConfig));
|
||||
// 不保存,直接更新状态
|
||||
emit(TabConfigLoaded(newConfig, selectedIndex: currentState.selectedIndex));
|
||||
}
|
||||
}
|
||||
|
||||
void selectTab(int index) {
|
||||
if (state is TabConfigLoaded) {
|
||||
final currentState = state as TabConfigLoaded;
|
||||
final enabledTabs = currentState.config.enabledItems;
|
||||
|
||||
// 确保索引在有效范围内
|
||||
if (index >= 0 && index < enabledTabs.length) {
|
||||
emit(TabConfigLoaded(currentState.config, selectedIndex: index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void resetToDefault() {
|
||||
final defaultConfig = TabConfig.defaultConfig();
|
||||
_saveConfig(defaultConfig);
|
||||
emit(TabConfigLoaded(defaultConfig));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,14 @@ import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart'
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/waring_center/presentation/pages/warning_center_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart' as my_v2;
|
||||
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
|
||||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||||
|
||||
class MainWrapper extends StatefulWidget {
|
||||
@@ -20,6 +25,28 @@ class MainWrapper extends StatefulWidget {
|
||||
class _MainWrapperState extends State<MainWrapper> {
|
||||
int _currentIndex = 0;
|
||||
final PageController _pageController = PageController();
|
||||
final List<GlobalKey> _tabKeys = [];
|
||||
double? _circleLeft;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
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
|
||||
void dispose() {
|
||||
@@ -27,16 +54,32 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTabChanged(int index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
_pageController.jumpToPage(index);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_updateCirclePosition();
|
||||
});
|
||||
}
|
||||
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'home_rounded':
|
||||
return Icons.home_rounded;
|
||||
case 'grid_view_rounded':
|
||||
return Icons.grid_view_rounded;
|
||||
case 'devices_rounded':
|
||||
return Icons.devices_rounded;
|
||||
case 'auto_awesome_rounded':
|
||||
return Icons.auto_awesome_rounded;
|
||||
case 'warning_amber_rounded':
|
||||
return Icons.warning_amber_rounded;
|
||||
case 'assignment_rounded':
|
||||
return Icons.assignment_rounded;
|
||||
case 'post_add_rounded':
|
||||
return Icons.post_add_rounded;
|
||||
case 'person_rounded':
|
||||
return Icons.person_rounded;
|
||||
default:
|
||||
@@ -44,6 +87,24 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (tab.id) {
|
||||
case 'home_v2':
|
||||
@@ -53,12 +114,23 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
);
|
||||
case 'home':
|
||||
return const HomePage();
|
||||
case 'device':
|
||||
return const DeviceStatusPage();
|
||||
case 'ai':
|
||||
return const AiPage();
|
||||
case 'warning':
|
||||
return const WarningCenterPage();
|
||||
return BlocProvider(
|
||||
create: (context) => sl<AlarmCubit>(),
|
||||
child: const AlarmCenterPage(),
|
||||
);
|
||||
case 'workorder':
|
||||
return const WorkOrderPage();
|
||||
case 'report':
|
||||
return const ReportPage();
|
||||
case 'my':
|
||||
return const MyPage();
|
||||
case 'me':
|
||||
return const my_v2.MyPage();
|
||||
default:
|
||||
return const HomePage();
|
||||
}
|
||||
@@ -87,8 +159,19 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
_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(
|
||||
extendBody: true,
|
||||
extendBody: false,
|
||||
body: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -100,57 +183,95 @@ class _MainWrapperState extends State<MainWrapper> {
|
||||
children: enabledItems.map((tab) => _buildCurrentPage(tab)).toList(),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
decoration: BoxDecoration(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: enabledItems.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isSelected = _currentIndex == index;
|
||||
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
_pageController.jumpToPage(index);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Icon(
|
||||
_getIconData(item.icon),
|
||||
size: 24,
|
||||
color: isSelected ? Colors.blue : Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected ? Colors.blue : Colors.grey,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
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: const Color(0xFFF2F3F5),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Tab 项
|
||||
Row(
|
||||
children: enabledItems.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isSelected = _currentIndex == index;
|
||||
|
||||
return Expanded(
|
||||
key: _tabKeys[index],
|
||||
child: InkWell(
|
||||
onTap: () => _onTabChanged(index),
|
||||
child: SizedBox(
|
||||
height: 56,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
_getIconData(item.icon),
|
||||
size: 24,
|
||||
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
item.name,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,11 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/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/presentation/widgets/custom_bottom_nav_bar.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart';
|
||||
|
||||
import '../../../waring_center/presentation/pages/warning_center_page.dart';
|
||||
import '../../../v2/waring_center/presentation/pages/alarm_center_page.dart';
|
||||
|
||||
class CustomMainContainer extends StatefulWidget {
|
||||
const CustomMainContainer({super.key});
|
||||
@@ -16,28 +19,37 @@ class CustomMainContainer extends StatefulWidget {
|
||||
State<CustomMainContainer> createState() => _CustomMainContainerState();
|
||||
}
|
||||
|
||||
class _CustomMainContainerState extends State<CustomMainContainer> with AutomaticKeepAliveClientMixin {
|
||||
class _CustomMainContainerState extends State<CustomMainContainer>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
bool get wantKeepAlive => true; // 保持页面状态
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
|
||||
debugPrint('🔍 [CustomMainContainer] build 被调用');
|
||||
|
||||
|
||||
return BlocBuilder<TabConfigCubit, TabConfigState>(
|
||||
builder: (context, state) {
|
||||
if (state is! TabConfigLoaded) {
|
||||
return const Scaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final enabledTabs = state.config.enabledItems;
|
||||
final currentIndex = state.selectedIndex;
|
||||
|
||||
debugPrint('🔍 [CustomMainContainer] enabledTabs=${enabledTabs.length}, currentIndex=$currentIndex');
|
||||
debugPrint('🔍 [CustomMainContainer] enabledTabs IDs=${enabledTabs.map((e) => e.id).toList()}');
|
||||
|
||||
|
||||
debugPrint(
|
||||
'🔍 [CustomMainContainer] enabledTabs=${enabledTabs.length}, currentIndex=$currentIndex',
|
||||
);
|
||||
debugPrint(
|
||||
'🔍 [CustomMainContainer] enabledTabs IDs=${enabledTabs.map((e) => e.id).toList()}',
|
||||
);
|
||||
|
||||
if (enabledTabs.isEmpty) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('请至少启用一个Tab')),
|
||||
);
|
||||
return const Scaffold(body: Center(child: Text('请至少启用一个Tab')));
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
@@ -45,7 +57,21 @@ class _CustomMainContainerState extends State<CustomMainContainer> with Automati
|
||||
index: currentIndex,
|
||||
children: _buildPages(enabledTabs),
|
||||
),
|
||||
bottomNavigationBar: const CustomBottomNavBar(),
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 调试信息:显示当前 Tab ID
|
||||
Container(
|
||||
color: Colors.yellow.withOpacity(0.8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(
|
||||
'当前选中Tab索引: $currentIndex, ID: ${enabledTabs[currentIndex].id}',
|
||||
style: const TextStyle(fontSize: 10, color: Colors.black),
|
||||
),
|
||||
),
|
||||
const CustomBottomNavBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -53,19 +79,32 @@ class _CustomMainContainerState extends State<CustomMainContainer> with Automati
|
||||
|
||||
List<Widget> _buildPages(List<dynamic> enabledTabs) {
|
||||
return enabledTabs.map((tab) {
|
||||
debugPrint('🔍 [buildPages] Tab ID: ${tab.id}, Name: ${tab.name}');
|
||||
switch (tab.id) {
|
||||
case 'home_v2':
|
||||
return const HomeV2Page();
|
||||
case 'home':
|
||||
return const HomePage();
|
||||
debugPrint('✅ 匹配到 home_v2');
|
||||
return const HomeV2Page(key: ValueKey('home_v2'));
|
||||
case 'device':
|
||||
debugPrint('✅ 匹配到 device');
|
||||
return const DeviceStatusPage(key: ValueKey('device'));
|
||||
case 'ai':
|
||||
return const AiPage();
|
||||
debugPrint('✅ 匹配到 ai');
|
||||
return const AiPage(key: ValueKey('ai'));
|
||||
case 'warning':
|
||||
return const WarningCenterPage();
|
||||
debugPrint('✅ 匹配到 warning');
|
||||
return const AlarmCenterPage(key: ValueKey('warning'));
|
||||
case 'workorder':
|
||||
debugPrint('✅ 匹配到 workorder');
|
||||
return const WorkOrderPage(key: ValueKey('workorder'));
|
||||
case 'report':
|
||||
debugPrint('✅ 匹配到 report');
|
||||
return const ReportPage(key: ValueKey('report'));
|
||||
case 'my':
|
||||
return const MyPage();
|
||||
debugPrint('✅ 匹配到 my');
|
||||
return const MyPage(key: ValueKey('my'));
|
||||
default:
|
||||
return const HomePage();
|
||||
debugPrint('❌ 未匹配到 Tab ID: ${tab.id}');
|
||||
return const SizedBox();
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -68,10 +68,16 @@ class TabSettingsPage extends StatelessWidget {
|
||||
return Icons.home_rounded;
|
||||
case 'grid_view_rounded':
|
||||
return Icons.grid_view_rounded;
|
||||
case 'devices_rounded':
|
||||
return Icons.devices_rounded;
|
||||
case 'auto_awesome_rounded':
|
||||
return Icons.auto_awesome_rounded;
|
||||
case 'warning_amber_rounded':
|
||||
return Icons.warning_amber_rounded;
|
||||
case 'assignment_rounded':
|
||||
return Icons.assignment_rounded;
|
||||
case 'post_add_rounded':
|
||||
return Icons.post_add_rounded;
|
||||
case 'person_rounded':
|
||||
return Icons.person_rounded;
|
||||
default:
|
||||
|
||||
@@ -1,51 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart';
|
||||
import 'package:cc_ui_kit/cc_ui_kit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
|
||||
class CustomBottomNavBar extends StatelessWidget {
|
||||
class CustomBottomNavBar extends StatefulWidget {
|
||||
const CustomBottomNavBar({super.key});
|
||||
|
||||
@override
|
||||
State<CustomBottomNavBar> createState() => _CustomBottomNavBarState();
|
||||
}
|
||||
|
||||
class _CustomBottomNavBarState extends State<CustomBottomNavBar>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
int _previousIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
_animation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.0,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TabConfigCubit, TabConfigState>(
|
||||
builder: (context, state) {
|
||||
if (state is! TabConfigLoaded) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final enabledTabs = state.config.enabledItems;
|
||||
|
||||
|
||||
if (enabledTabs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return _buildNavigationBar(
|
||||
constraints: constraints,
|
||||
enabledTabs: enabledTabs,
|
||||
selectedIndex: state.selectedIndex,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationBar({
|
||||
required BoxConstraints constraints,
|
||||
required List<dynamic> enabledTabs,
|
||||
required int selectedIndex,
|
||||
}) {
|
||||
final tabWidth = constraints.maxWidth / enabledTabs.length;
|
||||
final circleDiameter = 48.0;
|
||||
|
||||
// 当索引改变时,更新动画
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_previousIndex != selectedIndex) {
|
||||
final beginOffset = _previousIndex * tabWidth + tabWidth / 2;
|
||||
final endOffset = selectedIndex * tabWidth + tabWidth / 2;
|
||||
|
||||
_animation = Tween<double>(
|
||||
begin: beginOffset - circleDiameter / 2,
|
||||
end: endOffset - circleDiameter / 2,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
);
|
||||
|
||||
_previousIndex = selectedIndex;
|
||||
_animationController.forward(from: 0.0);
|
||||
}
|
||||
});
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
// 底层:Tab 列表
|
||||
Row(
|
||||
children: List.generate(enabledTabs.length, (index) {
|
||||
final tab = enabledTabs[index];
|
||||
final isSelected = index == state.selectedIndex;
|
||||
|
||||
return _buildNavItem(
|
||||
context,
|
||||
icon: _getIconData(tab.icon),
|
||||
label: tab.name,
|
||||
isSelected: isSelected,
|
||||
onTap: () => context.read<TabConfigCubit>().selectTab(index),
|
||||
final isSelected = index == selectedIndex;
|
||||
|
||||
return Expanded(
|
||||
child: _buildNavItem(
|
||||
context,
|
||||
icon: _getIconData(tab.icon),
|
||||
label: tab.name,
|
||||
isSelected: isSelected,
|
||||
onTap: () => context.read<TabConfigCubit>().selectTab(index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
// 上层:动画圆形背景
|
||||
AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
bottom: 8,
|
||||
left: _animation.value,
|
||||
child: Container(
|
||||
width: circleDiameter,
|
||||
height: circleDiameter,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 8,
|
||||
spreadRadius: 1,
|
||||
offset: Offset.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,15 +167,18 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected ? const Color(0xFF2196F3) : Colors.grey,
|
||||
color: isSelected ? Colors.black : const Color(0xFF666666),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -72,8 +186,8 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected ? const Color(0xFF2196F3) : Colors.grey,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: isSelected ? Colors.black : const Color(0xFF666666),
|
||||
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -84,12 +198,18 @@ class CustomBottomNavBar extends StatelessWidget {
|
||||
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'home_rounded':
|
||||
return Icons.home_rounded;
|
||||
case 'grid_view_rounded':
|
||||
return Icons.grid_view_rounded;
|
||||
case 'devices_rounded':
|
||||
return Icons.devices_rounded;
|
||||
case 'auto_awesome_rounded':
|
||||
return Icons.auto_awesome_rounded;
|
||||
case 'warning_amber_rounded':
|
||||
return Icons.warning_amber_rounded;
|
||||
case 'assignment_rounded':
|
||||
return Icons.assignment_rounded;
|
||||
case 'person_rounded':
|
||||
return Icons.person_rounded;
|
||||
default:
|
||||
|
||||
4
lib/features/v2/device_list/data/data.dart
Normal file
4
lib/features/v2/device_list/data/data.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
// 数据层聚合导出
|
||||
export 'datasources/datasources.dart';
|
||||
export 'models/models.dart';
|
||||
export 'repositories/repositories.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
// 数据源层
|
||||
export 'device_remote_datasource.dart';
|
||||
@@ -0,0 +1,95 @@
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
import '../models/device_data_model.dart';
|
||||
import '../models/device_status_data_model.dart';
|
||||
|
||||
/// 设备远程数据源抽象接口
|
||||
abstract class DeviceRemoteDataSource {
|
||||
Future<DeviceStatusDataModel> getDeviceStatus();
|
||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter});
|
||||
}
|
||||
|
||||
/// 设备远程数据源实现类 - 模拟从 API 获取数据
|
||||
class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource {
|
||||
@override
|
||||
Future<DeviceStatusDataModel> getDeviceStatus() async {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 模拟数据
|
||||
return const DeviceStatusDataModel(
|
||||
total: 136,
|
||||
online: 122,
|
||||
offline: 6,
|
||||
exception: 8,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceDataModel>> getDeviceList({String? typeFilter}) async {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// 模拟数据 - 根据设计图
|
||||
final allDevices = [
|
||||
const DeviceDataModel(
|
||||
deviceId: '10001',
|
||||
name: '逆变器 INV-001',
|
||||
type: '逆变器',
|
||||
status: '在线',
|
||||
power: 125.8,
|
||||
todayEnergy: 752.6,
|
||||
),
|
||||
const DeviceDataModel(
|
||||
deviceId: '10002',
|
||||
name: '逆变器 INV-002',
|
||||
type: '逆变器',
|
||||
status: '在线',
|
||||
power: 118.6,
|
||||
todayEnergy: 698.2,
|
||||
),
|
||||
const DeviceDataModel(
|
||||
deviceId: '10003',
|
||||
name: '汇流箱 SCB-001',
|
||||
type: '汇流箱',
|
||||
status: '在线',
|
||||
power: 0,
|
||||
todayEnergy: 0,
|
||||
current: 12.6,
|
||||
temperature: 36.2,
|
||||
),
|
||||
const DeviceDataModel(
|
||||
deviceId: '10004',
|
||||
name: '组件组串 STR-001',
|
||||
type: '组件',
|
||||
status: '在线',
|
||||
power: 85.3,
|
||||
todayEnergy: 0,
|
||||
radiation: 823,
|
||||
),
|
||||
const DeviceDataModel(
|
||||
deviceId: '10005',
|
||||
name: '监控 MON-001',
|
||||
type: '监控',
|
||||
status: '离线',
|
||||
power: 0,
|
||||
todayEnergy: 0,
|
||||
),
|
||||
const DeviceDataModel(
|
||||
deviceId: '10006',
|
||||
name: '逆变器 INV-003',
|
||||
type: '逆变器',
|
||||
status: '异常',
|
||||
power: 45.2,
|
||||
todayEnergy: 256.8,
|
||||
),
|
||||
];
|
||||
|
||||
// 根据类型过滤
|
||||
if (typeFilter != null && typeFilter != '全部') {
|
||||
return allDevices.where((device) => device.type == typeFilter).toList();
|
||||
}
|
||||
|
||||
return allDevices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
|
||||
/// 设备数据模型
|
||||
class DeviceDataModel {
|
||||
final String deviceId;
|
||||
final String name;
|
||||
final String type;
|
||||
final String status;
|
||||
final double power;
|
||||
final double todayEnergy;
|
||||
final double? temperature;
|
||||
final double? current;
|
||||
final double? voltage;
|
||||
final double? radiation;
|
||||
|
||||
const DeviceDataModel({
|
||||
required this.deviceId,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.power,
|
||||
required this.todayEnergy,
|
||||
this.temperature,
|
||||
this.current,
|
||||
this.voltage,
|
||||
this.radiation,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建数据模型
|
||||
factory DeviceDataModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceDataModel(
|
||||
deviceId: json['deviceId'] as String,
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String,
|
||||
status: json['status'] as String,
|
||||
power: (json['power'] as num).toDouble(),
|
||||
todayEnergy: (json['todayEnergy'] as num).toDouble(),
|
||||
temperature: json['temperature']?.toDouble(),
|
||||
current: json['current']?.toDouble(),
|
||||
voltage: json['voltage']?.toDouble(),
|
||||
radiation: json['radiation']?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'deviceId': deviceId,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'status': status,
|
||||
'power': power,
|
||||
'todayEnergy': todayEnergy,
|
||||
'temperature': temperature,
|
||||
'current': current,
|
||||
'voltage': voltage,
|
||||
'radiation': radiation,
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为实体
|
||||
DeviceEntity toEntity() {
|
||||
return DeviceEntity(
|
||||
deviceId: deviceId,
|
||||
name: name,
|
||||
type: type,
|
||||
status: status,
|
||||
power: power,
|
||||
todayEnergy: todayEnergy,
|
||||
temperature: temperature,
|
||||
current: current,
|
||||
voltage: voltage,
|
||||
radiation: radiation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
|
||||
/// 设备状态统计数据模型
|
||||
class DeviceStatusDataModel {
|
||||
final int total;
|
||||
final int online;
|
||||
final int offline;
|
||||
final int exception;
|
||||
|
||||
const DeviceStatusDataModel({
|
||||
required this.total,
|
||||
required this.online,
|
||||
required this.offline,
|
||||
required this.exception,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建数据模型
|
||||
factory DeviceStatusDataModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceStatusDataModel(
|
||||
total: json['total'] as int,
|
||||
online: json['online'] as int,
|
||||
offline: json['offline'] as int,
|
||||
exception: json['exception'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'total': total,
|
||||
'online': online,
|
||||
'offline': offline,
|
||||
'exception': exception,
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为实体
|
||||
DeviceStatusEntity toEntity() {
|
||||
return DeviceStatusEntity(
|
||||
total: total,
|
||||
online: online,
|
||||
offline: offline,
|
||||
exception: exception,
|
||||
);
|
||||
}
|
||||
}
|
||||
3
lib/features/v2/device_list/data/models/models.dart
Normal file
3
lib/features/v2/device_list/data/models/models.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
// 数据模型层
|
||||
export 'device_data_model.dart';
|
||||
export 'device_status_data_model.dart';
|
||||
@@ -0,0 +1,23 @@
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
import '../../domain/repositories/device_repository.dart';
|
||||
import '../datasources/device_remote_datasource.dart';
|
||||
|
||||
/// 设备仓储实现类
|
||||
class DeviceRepositoryImpl implements DeviceRepository {
|
||||
final DeviceRemoteDataSource remoteDataSource;
|
||||
|
||||
const DeviceRepositoryImpl({required this.remoteDataSource});
|
||||
|
||||
@override
|
||||
Future<DeviceStatusEntity> getDeviceStatus() async {
|
||||
final dataModel = await remoteDataSource.getDeviceStatus();
|
||||
return dataModel.toEntity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<DeviceEntity>> getDeviceList({String? typeFilter}) async {
|
||||
final dataModels = await remoteDataSource.getDeviceList(typeFilter: typeFilter);
|
||||
return dataModels.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// 仓储实现
|
||||
export 'device_repository_impl.dart';
|
||||
6
lib/features/v2/device_list/device.dart
Normal file
6
lib/features/v2/device_list/device.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
// 设备状态模块 - v2 版本
|
||||
// 清洁架构实现
|
||||
|
||||
export 'presentation/pages/device_status_page.dart';
|
||||
export 'presentation/bloc/bloc.dart';
|
||||
export 'presentation/widgets/widgets.dart';
|
||||
4
lib/features/v2/device_list/domain/domain.dart
Normal file
4
lib/features/v2/device_list/domain/domain.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
// 领域层聚合导出
|
||||
export 'entities/entities.dart';
|
||||
export 'repositories/device_repository.dart';
|
||||
export 'usecases/usecases.dart';
|
||||
@@ -0,0 +1,26 @@
|
||||
/// 设备实体类
|
||||
class DeviceEntity {
|
||||
final String deviceId;
|
||||
final String name;
|
||||
final String type;
|
||||
final String status;
|
||||
final double power;
|
||||
final double todayEnergy;
|
||||
final double? temperature;
|
||||
final double? current;
|
||||
final double? voltage;
|
||||
final double? radiation;
|
||||
|
||||
const DeviceEntity({
|
||||
required this.deviceId,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.power,
|
||||
required this.todayEnergy,
|
||||
this.temperature,
|
||||
this.current,
|
||||
this.voltage,
|
||||
this.radiation,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/// 设备状态统计实体类
|
||||
class DeviceStatusEntity {
|
||||
final int total;
|
||||
final int online;
|
||||
final int offline;
|
||||
final int exception;
|
||||
|
||||
const DeviceStatusEntity({
|
||||
required this.total,
|
||||
required this.online,
|
||||
required this.offline,
|
||||
required this.exception,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// 实体层
|
||||
export 'device_entity.dart';
|
||||
export 'device_status_entity.dart';
|
||||
@@ -0,0 +1,8 @@
|
||||
import '../entities/device_entity.dart';
|
||||
import '../entities/device_status_entity.dart';
|
||||
|
||||
/// 设备仓储抽象接口
|
||||
abstract class DeviceRepository {
|
||||
Future<DeviceStatusEntity> getDeviceStatus();
|
||||
Future<List<DeviceEntity>> getDeviceList({String? typeFilter});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import '../entities/device_entity.dart';
|
||||
import '../entities/device_status_entity.dart';
|
||||
import '../repositories/device_repository.dart';
|
||||
|
||||
/// 获取设备状态数据用例
|
||||
class GetDeviceStatusDataUseCase {
|
||||
final DeviceRepository repository;
|
||||
|
||||
const GetDeviceStatusDataUseCase({required this.repository});
|
||||
|
||||
Future<DeviceStatusResponse> execute({String? typeFilter}) async {
|
||||
try {
|
||||
final status = await repository.getDeviceStatus();
|
||||
final devices = await repository.getDeviceList(typeFilter: typeFilter);
|
||||
return DeviceStatusResponse(status: status, devices: devices);
|
||||
} catch (e) {
|
||||
throw Exception('获取设备数据失败: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备状态响应类
|
||||
class DeviceStatusResponse {
|
||||
final DeviceStatusEntity status;
|
||||
final List<DeviceEntity> devices;
|
||||
|
||||
const DeviceStatusResponse({required this.status, required this.devices});
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// 用例层
|
||||
export 'get_device_status_data_usecase.dart';
|
||||
4
lib/features/v2/device_list/presentation/bloc/bloc.dart
Normal file
4
lib/features/v2/device_list/presentation/bloc/bloc.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
// BLoC 层
|
||||
export 'device_status_bloc.dart';
|
||||
export 'device_status_event.dart';
|
||||
export 'device_status_state.dart';
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/get_device_status_data_usecase.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'device_status_state.dart';
|
||||
|
||||
class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
final GetDeviceStatusDataUseCase getDeviceStatusDataUseCase;
|
||||
|
||||
DeviceStatusBloc(this.getDeviceStatusDataUseCase)
|
||||
: super(const DeviceStatusInitial()) {
|
||||
on<DeviceStatusLoadData>(_onLoadData);
|
||||
on<DeviceStatusRefresh>(_onRefresh);
|
||||
on<DeviceStatusChangeType>(_onChangeType);
|
||||
on<DeviceStatusSearch>(_onSearch);
|
||||
}
|
||||
|
||||
Future<void> _onLoadData(
|
||||
DeviceStatusLoadData event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
) async {
|
||||
emit(const DeviceStatusLoading());
|
||||
|
||||
try {
|
||||
final response = await getDeviceStatusDataUseCase.execute();
|
||||
|
||||
emit(DeviceStatusLoaded(
|
||||
deviceStatus: response.status,
|
||||
devices: response.devices,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRefresh(
|
||||
DeviceStatusRefresh event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
) async {
|
||||
if (state is DeviceStatusLoaded) {
|
||||
final currentState = state as DeviceStatusLoaded;
|
||||
|
||||
try {
|
||||
final response = await getDeviceStatusDataUseCase.execute(
|
||||
typeFilter: currentState.selectedType == '全部'
|
||||
? null
|
||||
: currentState.selectedType,
|
||||
);
|
||||
|
||||
emit(currentState.copyWith(
|
||||
deviceStatus: response.status,
|
||||
devices: response.devices,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onChangeType(
|
||||
DeviceStatusChangeType event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
) {
|
||||
if (state is DeviceStatusLoaded) {
|
||||
final currentState = state as DeviceStatusLoaded;
|
||||
|
||||
// 🔥 直接更新选中类型,不重新请求接口(前端过滤)
|
||||
emit(currentState.copyWith(
|
||||
selectedType: event.type,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearch(
|
||||
DeviceStatusSearch event,
|
||||
Emitter<DeviceStatusState> emit,
|
||||
) {
|
||||
if (state is DeviceStatusLoaded) {
|
||||
final currentState = state as DeviceStatusLoaded;
|
||||
|
||||
emit(currentState.copyWith(
|
||||
searchKeyword: event.keyword,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class DeviceStatusEvent extends Equatable {
|
||||
const DeviceStatusEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class DeviceStatusLoadData extends DeviceStatusEvent {
|
||||
const DeviceStatusLoadData();
|
||||
}
|
||||
|
||||
class DeviceStatusRefresh extends DeviceStatusEvent {
|
||||
const DeviceStatusRefresh();
|
||||
}
|
||||
|
||||
class DeviceStatusChangeType extends DeviceStatusEvent {
|
||||
final String type;
|
||||
|
||||
const DeviceStatusChangeType(this.type);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type];
|
||||
}
|
||||
|
||||
class DeviceStatusSearch extends DeviceStatusEvent {
|
||||
final String keyword;
|
||||
|
||||
const DeviceStatusSearch(this.keyword);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [keyword];
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
import '../../domain/entities/device_status_entity.dart';
|
||||
|
||||
abstract class DeviceStatusState extends Equatable {
|
||||
const DeviceStatusState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class DeviceStatusInitial extends DeviceStatusState {
|
||||
const DeviceStatusInitial();
|
||||
}
|
||||
|
||||
class DeviceStatusLoading extends DeviceStatusState {
|
||||
const DeviceStatusLoading();
|
||||
}
|
||||
|
||||
class DeviceStatusLoaded extends DeviceStatusState {
|
||||
final DeviceStatusEntity deviceStatus;
|
||||
final List<DeviceEntity> devices;
|
||||
final String selectedType;
|
||||
final String searchKeyword;
|
||||
|
||||
const DeviceStatusLoaded({
|
||||
required this.deviceStatus,
|
||||
required this.devices,
|
||||
this.selectedType = '全部',
|
||||
this.searchKeyword = '',
|
||||
});
|
||||
|
||||
DeviceStatusLoaded copyWith({
|
||||
DeviceStatusEntity? deviceStatus,
|
||||
List<DeviceEntity>? devices,
|
||||
String? selectedType,
|
||||
String? searchKeyword,
|
||||
}) {
|
||||
return DeviceStatusLoaded(
|
||||
deviceStatus: deviceStatus ?? this.deviceStatus,
|
||||
devices: devices ?? this.devices,
|
||||
selectedType: selectedType ?? this.selectedType,
|
||||
searchKeyword: searchKeyword ?? this.searchKeyword,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [deviceStatus, devices, selectedType, searchKeyword];
|
||||
}
|
||||
|
||||
class DeviceStatusError extends DeviceStatusState {
|
||||
final String message;
|
||||
|
||||
const DeviceStatusError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class DeviceStatusEmpty extends DeviceStatusState {
|
||||
final DeviceStatusEntity deviceStatus;
|
||||
|
||||
const DeviceStatusEmpty({required this.deviceStatus});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [deviceStatus];
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 清洗/除草机器人任务详情页
|
||||
class CleaningWeedingRobotTaskPage extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const CleaningWeedingRobotTaskPage({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCleaning = robot['type'] == '清洗机器人';
|
||||
final title = isCleaning ? '清洗机器人任务' : '除草机器人任务';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, color: Color(0xFF1D2129)),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildTaskInfoCard(isCleaning),
|
||||
const SizedBox(height: 12),
|
||||
_buildProgressCard(isCleaning),
|
||||
const SizedBox(height: 12),
|
||||
_buildDeviceStatusCard(isCleaning),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildBottomButtons(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskInfoCard(bool isCleaning) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
isCleaning ? '清洗任务-组件清洗A区' : '除草任务-光伏板除草B区',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'进行中',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('任务编号', isCleaning ? 'CL-2025052001' : 'WD-2025052001'),
|
||||
_buildInfoRow('作业区域', isCleaning ? '清洗区A区-01' : '除草区B区-02'),
|
||||
_buildInfoRow('开始时间', '05-20 09:20'),
|
||||
_buildInfoRow('预计完成', '05-20 14:30'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
isCleaning ? 'assets/images/qinxi.png' : 'assets/images/chucao.png',
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'$label:',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressCard(bool isCleaning) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'作业进度',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'覆盖面积',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isCleaning ? '8,230 m² / 15,000 m²' : '5,600 m² / 12,000 m²',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isCleaning ? '55%' : '47%',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: isCleaning ? 0.55 : 0.47,
|
||||
backgroundColor: const Color(0xFFF2F3F5),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF165DFF)),
|
||||
minHeight: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'预计完成时间',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'14:30(剩余 2h10m)',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceStatusCard(bool isCleaning) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'设备状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusRow('电量', '68%', 0.68),
|
||||
const SizedBox(height: 12),
|
||||
if (isCleaning) ...[
|
||||
_buildStatusRow('水箱水位', '62%', 0.62),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithBadge('喷淋压力', '0.45 MPa', '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithBadge('刷头状态', '正常', '正常'),
|
||||
] else ...[
|
||||
_buildStatusRow('燃油量', '75%', 0.75),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithBadge('刀片状态', '正常', '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithBadge('导航状态', '正常', '正常'),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusRow(String label, String value, double progress) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: progress,
|
||||
backgroundColor: const Color(0xFFF2F3F5),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
progress > 0.5 ? const Color(0xFF00B42A) : const Color(0xFFFF7D00),
|
||||
),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusRowWithBadge(String label, String value, String badgeText) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
badgeText,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomButtons() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'开始作业',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF7D00),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'暂停',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF4E5969),
|
||||
side: const BorderSide(color: Color(0xFFC9CDD4)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'返航/回充',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/di/injection.dart';
|
||||
import '../bloc/device_status_bloc.dart';
|
||||
import '../bloc/device_status_event.dart';
|
||||
import '../bloc/device_status_state.dart';
|
||||
import '../widgets/device_item_widget.dart';
|
||||
import '../widgets/drone_station_item_card.dart';
|
||||
import 'robot_list_page.dart';
|
||||
import 'drone_station_detail_page.dart';
|
||||
/// 设备状态页面 - 使用 BLoC 模式
|
||||
class DeviceStatusPage extends StatelessWidget {
|
||||
const DeviceStatusPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => sl<DeviceStatusBloc>()..add(const DeviceStatusLoadData()),
|
||||
child: const DeviceStatusView(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 设备状态视图
|
||||
class DeviceStatusView extends StatelessWidget {
|
||||
const DeviceStatusView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
body: SafeArea(
|
||||
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAppBar(),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(
|
||||
child: _buildContent(context, state),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar() {
|
||||
return Container(
|
||||
height: 44.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: const Row(
|
||||
children: [
|
||||
Text(
|
||||
'设备状态',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Icon(
|
||||
Icons.more_vert,
|
||||
size: 24,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16.0, 8, 16.0, 8),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索设备名称/编号',
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF86909C),
|
||||
fontSize: 14,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.search,
|
||||
color: Color(0xFF86909C),
|
||||
size: 24,
|
||||
),
|
||||
suffixIcon: TextButton(
|
||||
onPressed: () {
|
||||
// TODO: 触发搜索
|
||||
},
|
||||
child: const Text(
|
||||
'搜索',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF165DFF),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF2F3F5),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF165DFF),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
// 🔥 点击键盘确定键时触发搜索
|
||||
context.read<DeviceStatusBloc>().add(DeviceStatusSearch(value));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeFilterBar(BuildContext context) {
|
||||
final types = ['全部', '机器人', '无人机机场', '逆变器', '汇流箱', '组件', '监控'];
|
||||
|
||||
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
String selectedType = '全部';
|
||||
if (state is DeviceStatusLoaded) {
|
||||
selectedType = state.selectedType;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
itemCount: types.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(width: 24.0),
|
||||
itemBuilder: (context, index) {
|
||||
final type = types[index];
|
||||
final isSelected = selectedType == type;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<DeviceStatusBloc>().add(DeviceStatusChangeType(type));
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
type,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: isSelected ? const Color(0xFF135BFF) : const Color(0xFF666666),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF0D57FF) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context, DeviceStatusState state) {
|
||||
// 如果选择的是"机器人",显示机器人专属页面
|
||||
if (state is DeviceStatusLoaded && state.selectedType == '机器人') {
|
||||
return const RobotListPage();
|
||||
}
|
||||
|
||||
// 如果选择的是"无人机机场",显示机场列表
|
||||
if (state is DeviceStatusLoaded && state.selectedType == '无人机机场') {
|
||||
return _buildDroneStationList(context);
|
||||
}
|
||||
|
||||
// 其他类型显示原有设备列表
|
||||
return _buildDeviceList(context, state);
|
||||
}
|
||||
|
||||
Widget _buildDeviceList(BuildContext context, DeviceStatusState state) {
|
||||
if (state is DeviceStatusLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DeviceStatusError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DeviceStatusBloc>().add(const DeviceStatusLoadData());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is DeviceStatusLoaded) {
|
||||
// 🔥 先根据类型过滤
|
||||
List filteredByType = state.devices;
|
||||
if (state.selectedType != '全部') {
|
||||
filteredByType = state.devices.where((device) {
|
||||
return device.type == state.selectedType;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// 🔥 再根据搜索关键词过滤
|
||||
final filteredDevices = filteredByType.where((device) {
|
||||
if (state.searchKeyword.isEmpty) return true;
|
||||
return device.name.toLowerCase().contains(state.searchKeyword.toLowerCase()) ||
|
||||
device.deviceId.toLowerCase().contains(state.searchKeyword.toLowerCase());
|
||||
}).toList();
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
context.read<DeviceStatusBloc>().add(const DeviceStatusRefresh());
|
||||
},
|
||||
color: const Color(0xFF165DFF),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: [
|
||||
_buildStatusCard(state),
|
||||
const SizedBox(height: 20.0),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: filteredDevices.length,
|
||||
itemBuilder: (context, index) {
|
||||
final device = filteredDevices[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: DeviceItemWidget(
|
||||
device: device,
|
||||
onTap: () {
|
||||
debugPrint('点击设备: ${device.name}');
|
||||
// TODO: 导航到设备详情页
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: Text('暂无数据'));
|
||||
}
|
||||
|
||||
Widget _buildDroneStationList(BuildContext context) {
|
||||
// 模拟机场数据
|
||||
final stations = [
|
||||
{'name': '1号无人机机场', 'id': 'AIRPORT-01'},
|
||||
{'name': '2号无人机机场', 'id': 'AIRPORT-02'},
|
||||
{'name': '3号无人机机场', 'id': 'AIRPORT-03'},
|
||||
];
|
||||
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
itemCount: stations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final station = stations[index];
|
||||
return DroneStationItemCard(
|
||||
name: station['name'] as String,
|
||||
id: station['id'] as String,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneStationDetailPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusCard(DeviceStatusLoaded state) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.total.toString(),
|
||||
label: '全部设备',
|
||||
dotColor: const Color(0xFF86909C),
|
||||
textColor: const Color(0xFF86909C), // 新增字体颜色
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.online.toString(),
|
||||
label: '在线',
|
||||
dotColor: const Color(0xFF00B42A),
|
||||
textColor: const Color(0xFF00B42A),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.exception.toString(),
|
||||
label: '告警',
|
||||
dotColor: const Color(0xFFFF7D00),
|
||||
textColor: const Color(0xFFFF7D00),
|
||||
),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFE5E6EB),
|
||||
),
|
||||
_buildStatItem(
|
||||
value: state.deviceStatus.offline.toString(),
|
||||
label: '离线',
|
||||
dotColor: const Color(0xFFF53F3F),
|
||||
textColor: const Color(0xFFF53F3F),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatItem({
|
||||
required String value,
|
||||
required String label,
|
||||
required Color dotColor,
|
||||
required Color? textColor, // 新增字体颜色
|
||||
}) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor ?? const Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: dotColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor ?? const Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 无人机任务与航线控制页面
|
||||
class DroneMissionControlPage extends StatelessWidget {
|
||||
const DroneMissionControlPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'无人机任务与航线控制',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings, color: Color(0xFF1D2129)),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildMissionInfoCard(),
|
||||
const SizedBox(height: 12),
|
||||
_buildRouteMap(),
|
||||
const SizedBox(height: 12),
|
||||
_buildWaypointList(),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildActionButtons(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMissionInfoCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'任务信息',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow('任务名称', '逆变器区巡检任务'),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildInfoColumn('任务编号', 'UAV-2025052001'),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'进行中',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('巡检区域', '逆变器区A区'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行高度', '80 m'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('飞行速度', '8.0 m/s'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('预计时长', '26 min'),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('电量预估', '68% (可飞行 22 min)'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRouteMap() {
|
||||
return Container(
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaypointList() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'航点列表',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.map, size: 20, color: Color(0xFF86909C)),
|
||||
onPressed: () {},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20, color: Color(0xFF86909C)),
|
||||
onPressed: () {},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildWaypointItem('1', '起飞点', '50m', '拍照'),
|
||||
const SizedBox(height: 8),
|
||||
_buildWaypointItem('2', '航点1', '80m', '拍照'),
|
||||
const SizedBox(height: 8),
|
||||
_buildWaypointItem('3', '航点2', '80m', '拍照'),
|
||||
const SizedBox(height: 8),
|
||||
_buildWaypointItem('4', '航点3', '80m', '拍照'),
|
||||
const SizedBox(height: 8),
|
||||
_buildWaypointItem('5', '航点4', '80m', '拍照'),
|
||||
const SizedBox(height: 8),
|
||||
_buildWaypointItem('6', '航点5', '80m', '拍照'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWaypointItem(String number, String name, String height, String action) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF165DFF),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
number,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'H: $height',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
action,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.download_outlined,
|
||||
size: 18,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF5F6F8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'开始巡检',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF7D00),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: const Text(
|
||||
'暂停任务',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF4E5969),
|
||||
side: const BorderSide(color: Color(0xFFC9CDD4)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'返航降落',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoColumn(String label, String value) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'drone_mission_control_page.dart';
|
||||
import 'drone_video_control_page.dart';
|
||||
|
||||
/// 无人机机场详情页面
|
||||
class DroneStationDetailPage extends StatelessWidget {
|
||||
const DroneStationDetailPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'无人机机场详情',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_buildAirportStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
_buildDroneStatusCard(context),
|
||||
const SizedBox(height: 12),
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 机场状态卡片
|
||||
Widget _buildAirportStatusCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'机场状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('位置', '升压站旁 - 无人机机场'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('舱门状态', '关闭', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithProgress('充电状态', '充电中', '85%'),
|
||||
const SizedBox(height: 12),
|
||||
_buildWeatherRow(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('网络状态', '4G/5G 强'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机状态卡片
|
||||
Widget _buildDroneStatusCard(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneVideoControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'无人机状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'无人机 - 01',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDroneBatteryRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('飞行模式', '待命'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('机身状态', '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.flight_takeoff,
|
||||
label: '开舱',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.flight,
|
||||
label: '起飞准备',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.home,
|
||||
label: '返航',
|
||||
color: const Color(0xFFFF7D00),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.task,
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneMissionControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActionButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态行(带状态标签)
|
||||
Widget _buildStatusRow(String label, String value, {String? status}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (status != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 带进度条的状态行
|
||||
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
progress,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 气象条件行
|
||||
Widget _buildWeatherRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'气象条件',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'晴',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'25°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'东南风 2级',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机电量行
|
||||
Widget _buildDroneBatteryRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'电量',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: 0.78,
|
||||
backgroundColor: const Color(0xFFF2F3F5),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B42A)),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'78%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 无人机机场与设备状态页面
|
||||
class DroneStationStatusPage extends StatelessWidget {
|
||||
const DroneStationStatusPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// 顶部导航栏
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
pinned: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'无人机机场与设备状态',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.notifications_none, color: Color(0xFF1D2129)),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
// 内容区域
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 机场状态卡片
|
||||
_buildAirportStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
// 无人机状态卡片
|
||||
_buildDroneStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
// 快捷操作
|
||||
_buildQuickActions(),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 80), // 为底部 Tab 栏留出空间
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 机场状态卡片
|
||||
Widget _buildAirportStatusCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'机场状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('位置', '升压站旁 - 无人机机场'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('舱门状态', '关闭', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithProgress('充电状态', '充电中', '85%'),
|
||||
const SizedBox(height: 12),
|
||||
_buildWeatherRow(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('网络状态', '4G/5G 强'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机状态卡片
|
||||
Widget _buildDroneStatusCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'无人机状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'无人机 - 01',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDroneBatteryRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('飞行模式', '待命'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('机身状态', '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActions() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.flight_takeoff,
|
||||
label: '开舱',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.flight,
|
||||
label: '起飞准备',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.home,
|
||||
label: '返航',
|
||||
color: const Color(0xFFFF7D00),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
icon: Icons.task,
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActionButton({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态行(带状态标签)
|
||||
Widget _buildStatusRow(String label, String value, {String? status}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (status != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 带进度条的状态行
|
||||
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
progress,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 气象条件行
|
||||
Widget _buildWeatherRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'气象条件',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'晴',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'25°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'东南风 2级',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机电量行
|
||||
Widget _buildDroneBatteryRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'电量',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: 0.78,
|
||||
backgroundColor: const Color(0xFFF2F3F5),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B42A)),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'78%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 无人机视频回传/远程控制页面
|
||||
class DroneVideoControlPage extends StatefulWidget {
|
||||
const DroneVideoControlPage({super.key});
|
||||
|
||||
@override
|
||||
State<DroneVideoControlPage> createState() => _DroneVideoControlPageState();
|
||||
}
|
||||
|
||||
class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||
int _selectedTab = 0; // 0: 视频画面, 1: 热成像, 2: 抓拍记录
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: const Text(
|
||||
'无人机视频回传 / 远程控制',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings, color: Color(0xFF1D2129)),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Tab切换
|
||||
_buildTabBar(),
|
||||
// 内容区域
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// 视频画面
|
||||
_buildVideoPlayer(),
|
||||
const SizedBox(height: 12),
|
||||
// 飞行数据
|
||||
_buildFlightData(),
|
||||
const SizedBox(height: 12),
|
||||
// AI识别结果
|
||||
_buildAIResults(),
|
||||
const SizedBox(height: 12),
|
||||
// 地图和摇杆
|
||||
_buildMapAndJoystick(),
|
||||
const SizedBox(height: 16),
|
||||
// 底部工具栏
|
||||
_buildBottomToolbar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tab切换栏
|
||||
Widget _buildTabBar() {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildTabItem('视频画面', 0),
|
||||
const SizedBox(width: 8),
|
||||
_buildTabItem('热成像', 1),
|
||||
const SizedBox(width: 8),
|
||||
_buildTabItem('抓拍记录', 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabItem(String label, int index) {
|
||||
final isSelected = _selectedTab == index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedTab = index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.white : const Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 视频播放器
|
||||
Widget _buildVideoPlayer() {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 240,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1D2129),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 录制标识
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: 12,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF53F3F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'REC 00:12:36',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 缩放标识
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
right: 12,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'1.0x',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 飞行数据
|
||||
Widget _buildFlightData() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildDataItem('高度', '78 m'),
|
||||
const SizedBox(width: 16),
|
||||
_buildDataItem('速度', '7.5 m/s'),
|
||||
const SizedBox(width: 16),
|
||||
_buildDataItem('距离', '1.26 km'),
|
||||
const SizedBox(width: 16),
|
||||
_buildDataItem('云台角度', '-15° / 0°'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataItem(String label, String value) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// AI识别结果
|
||||
Widget _buildAIResults() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'AI 识别结果',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildAIResultItem('热斑疑似', '3 处'),
|
||||
const SizedBox(height: 8),
|
||||
_buildAIResultItem('组件异常', '2 处'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAIResultItem(String label, String count) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Color(0xFFFF7D00),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
count,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.chevron_right,
|
||||
color: Color(0xFF86909C),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 地图和摇杆
|
||||
Widget _buildMapAndJoystick() {
|
||||
return Row(
|
||||
children: [
|
||||
// 地图
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 摇杆控制器
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 中心圆点
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFC9CDD4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
// 上
|
||||
Positioned(
|
||||
top: 16,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_up, size: 32, color: Color(0xFF4E5969)),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
// 下
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_drop_down, size: 32, color: Color(0xFF4E5969)),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
// 左
|
||||
Positioned(
|
||||
left: 16,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_left, size: 32, color: Color(0xFF4E5969)),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
// 右
|
||||
Positioned(
|
||||
right: 16,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_right, size: 32, color: Color(0xFF4E5969)),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 底部工具栏
|
||||
Widget _buildBottomToolbar() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildToolItem(Icons.camera_alt_outlined, '抓拍'),
|
||||
_buildToolItem(Icons.videocam_outlined, '录像'),
|
||||
_buildToolItem(Icons.zoom_in_outlined, '变焦'),
|
||||
_buildToolItem(Icons.lightbulb_outline, '补光灯'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolItem(IconData icon, String label) {
|
||||
return GestureDetector(
|
||||
onTap: () {},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, size: 28, color: const Color(0xFF4E5969)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/robot_header_card.dart';
|
||||
import '../widgets/robot_status_bar.dart';
|
||||
import '../widgets/robot_control_panel.dart';
|
||||
import '../widgets/robot_checklist.dart';
|
||||
import '../widgets/robot_task_info.dart';
|
||||
import '../widgets/robot_action_buttons.dart';
|
||||
|
||||
/// 机器人控制详情页
|
||||
class RobotControlPage extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotControlPage({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
'${robot['type']}控制',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, color: Color(0xFF1D2129)),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
children: [
|
||||
RobotHeaderCard(robot: robot),
|
||||
const SizedBox(height: 8),
|
||||
RobotStatusBar(robot: robot),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
RobotControlPanel(robot: robot),
|
||||
const SizedBox(height: 8),
|
||||
RobotTaskInfo(robot: robot),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: RobotChecklist(robot: robot)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 20),
|
||||
child: RobotActionButtons(robot: robot),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../widgets/robot_item_card.dart';
|
||||
import 'robot_control_page.dart';
|
||||
import 'cleaning_weeding_robot_task_page.dart';
|
||||
|
||||
/// 机器人列表页面
|
||||
class RobotListPage extends StatefulWidget {
|
||||
const RobotListPage({super.key});
|
||||
|
||||
@override
|
||||
State<RobotListPage> createState() => _RobotListPageState();
|
||||
}
|
||||
|
||||
class _RobotListPageState extends State<RobotListPage> {
|
||||
String? _selectedType; // 选中的类型
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: [
|
||||
_buildStatsCard(),
|
||||
_buildQuickActions(),
|
||||
_buildCurrentTask(),
|
||||
..._buildRobotList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatsCard() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人';
|
||||
}),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
gradient: _selectedType == '巡检机器人'
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFE8F4FF),
|
||||
Color(0xFFFFFFFF),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
color: _selectedType != '巡检机器人' ? Colors.transparent : null,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'巡检机器人',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '巡检机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/images/xunjian.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
const Text('12', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线9台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '巡检机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF00B42A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人';
|
||||
}),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
gradient: _selectedType == '清洗机器人'
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFE8F4FF),
|
||||
Color(0xFFFFFFFF),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
color: _selectedType != '清洗机器人' ? Colors.transparent : null,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'清洗机器人',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '清洗机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/images/qinxi.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
const Text('8', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线6台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '清洗机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF00B42A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() {
|
||||
_selectedType = _selectedType == '除草机器人' ? null : '除草机器人';
|
||||
}),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
gradient: _selectedType == '除草机器人'
|
||||
? const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFFE8F4FF),
|
||||
Color(0xFFFFFFFF),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
color: _selectedType != '除草机器人' ? Colors.transparent : null,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'除草机器人',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '除草机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset('assets/images/chucao.png', width: 48, height: 48),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'6',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _selectedType == '除草机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF00B42A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'在线4台',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: _selectedType == '除草机器人'
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF00B42A),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActions() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(value: '19', label: '在线设备', valueColor: const Color(0xFF165DFF)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '8', label: '作业中', valueColor: const Color(0xFF00B42A)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '12', label: '待命', valueColor: const Color(0xFFFF7D00)),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(value: '2', label: '故障数', valueColor: const Color(0xFFF53F3F)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusItem({required String value, required String label, required Color valueColor}) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: valueColor)),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF4E5969))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentTask() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('当前任务', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset('assets/images/xunjian.png', width: 80, height: 80, fit: BoxFit.cover),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(child: Text('巡检任务-逆变器区巡检', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1D2129)))),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(color: const Color(0xFF165DFF).withOpacity(0.1), borderRadius: BorderRadius.circular(4)),
|
||||
child: const Text('进行中', style: TextStyle(fontSize: 11, color: Color(0xFF165DFF), fontWeight: FontWeight.w500)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildTaskInfo('机器人:', '巡检机器人-01'),
|
||||
_buildTaskInfo('区域:', '逆变器区A区'),
|
||||
_buildTaskInfo('进度:', '62%'),
|
||||
_buildTaskInfo('预计完成:', '12:30'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTaskInfo(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(text: label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))),
|
||||
TextSpan(text: value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129))),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRobotList() {
|
||||
final allRobots = [
|
||||
{'name': '巡检机器人-01', 'id': 'RB001', 'type': '巡检机器人', 'status': '在线', 'battery': 85.0, 'task': '逆变器区域巡检'},
|
||||
{'name': '巡检机器人-02', 'id': 'RB002', 'type': '巡检机器人', 'status': '在线', 'battery': 62.0, 'task': '组件区域巡检'},
|
||||
{'name': '巡检机器人-03', 'id': 'RB003', 'type': '巡检机器人', 'status': '离线', 'battery': 15.0, 'task': '充电中'},
|
||||
{'name': '清洗机器人-01', 'id': 'RB004', 'type': '清洗机器人', 'status': '在线', 'battery': 78.0, 'task': '组件清洗作业'},
|
||||
{'name': '清洗机器人-02', 'id': 'RB005', 'type': '清洗机器人', 'status': '在线', 'battery': 91.0, 'task': '待机中'},
|
||||
{'name': '除草机器人-01', 'id': 'RB006', 'type': '除草机器人', 'status': '在线', 'battery': 55.0, 'task': '光伏板除草'},
|
||||
];
|
||||
|
||||
final filteredRobots = _selectedType == null ? allRobots : allRobots.where((robot) => robot['type'] == _selectedType).toList();
|
||||
|
||||
if (filteredRobots.isEmpty) {
|
||||
return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))];
|
||||
}
|
||||
|
||||
return filteredRobots.map((robot) => RobotItemCard(
|
||||
name: robot['name'] as String,
|
||||
id: robot['id'] as String,
|
||||
type: robot['type'] as String,
|
||||
status: robot['status'] as String,
|
||||
battery: robot['battery'] as double,
|
||||
task: robot['task'] as String,
|
||||
onTap: () {
|
||||
// 巡检机器人使用原来的控制页面
|
||||
// 清洗/除草机器人使用新的任务详情页面
|
||||
if (robot['type'] == '巡检机器人') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => RobotControlPage(robot: robot),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CleaningWeedingRobotTaskPage(robot: robot),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)).toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// 表现层聚合导出
|
||||
export 'bloc/bloc.dart';
|
||||
export 'pages/device_status_page.dart';
|
||||
export 'widgets/widgets.dart';
|
||||
@@ -0,0 +1,2 @@
|
||||
// Providers 层(用于依赖注入)
|
||||
export '../bloc/bloc.dart';
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 底部导航栏组件
|
||||
class DeviceBottomNavBar extends StatelessWidget {
|
||||
final int currentIndex;
|
||||
final Function(int) onTap;
|
||||
|
||||
const DeviceBottomNavBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 49.0,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildNavItem(
|
||||
icon: Icons.home_outlined,
|
||||
label: '首页',
|
||||
isSelected: currentIndex == 0,
|
||||
onTap: () => onTap(0),
|
||||
),
|
||||
_buildNavItem(
|
||||
icon: Icons.devices,
|
||||
label: '设备',
|
||||
isSelected: currentIndex == 1,
|
||||
onTap: () => onTap(1),
|
||||
),
|
||||
_buildNavItem(
|
||||
icon: Icons.notifications_outlined,
|
||||
label: '告警',
|
||||
isSelected: currentIndex == 2,
|
||||
onTap: () => onTap(2),
|
||||
),
|
||||
_buildNavItem(
|
||||
icon: Icons.assignment_outlined,
|
||||
label: '工单',
|
||||
isSelected: currentIndex == 3,
|
||||
onTap: () => onTap(3),
|
||||
),
|
||||
_buildNavItem(
|
||||
icon: Icons.person_outlined,
|
||||
label: '我的',
|
||||
isSelected: currentIndex == 4,
|
||||
onTap: () => onTap(4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected ? const Color(0xFF004CFD) : const Color(0xFF86909C),
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected ? const Color(0xFF004FFF) : const Color(0xFF86909C),
|
||||
fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../domain/entities/device_entity.dart';
|
||||
|
||||
/// 设备列表项 Widget
|
||||
class DeviceItemWidget extends StatelessWidget {
|
||||
final DeviceEntity device;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const DeviceItemWidget({
|
||||
super.key,
|
||||
required this.device,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:设备信息
|
||||
Row(
|
||||
children: [
|
||||
// 设备图标
|
||||
_buildDeviceIcon(device.type),
|
||||
const SizedBox(width: 12),
|
||||
// 设备名称和 ID
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
device.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'ID: ${device.deviceId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 状态标签
|
||||
_buildStatusTag(device.status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 第二行:设备数据
|
||||
_buildDeviceData(device),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceIcon(String type) {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
switch (type) {
|
||||
case '机器人':
|
||||
icon = Icons.smart_toy;
|
||||
color = const Color(0xFF165DFF);
|
||||
break;
|
||||
case '逆变器':
|
||||
icon = Icons.power;
|
||||
color = const Color(0xFF165DFF);
|
||||
break;
|
||||
case '汇流箱':
|
||||
icon = Icons.electrical_services;
|
||||
color = const Color(0xFF722ED1);
|
||||
break;
|
||||
case '组件':
|
||||
icon = Icons.solar_power;
|
||||
color = const Color(0xFF13C2C2);
|
||||
break;
|
||||
case '监控':
|
||||
icon = Icons.videocam;
|
||||
color = const Color(0xFFEB2F96);
|
||||
break;
|
||||
default:
|
||||
icon = Icons.devices;
|
||||
color = const Color(0xFF165DFF);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 28,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusTag(String status) {
|
||||
Color bgColor;
|
||||
Color textColor;
|
||||
|
||||
switch (status) {
|
||||
case '在线':
|
||||
bgColor = const Color(0xFFF0F9FF);
|
||||
textColor = const Color(0xFF00B42A);
|
||||
break;
|
||||
case '离线':
|
||||
bgColor = const Color(0xFFF5F0FF);
|
||||
textColor = const Color(0xFFF53F3F);
|
||||
break;
|
||||
case '异常':
|
||||
bgColor = const Color(0xFFFFF7E6);
|
||||
textColor = const Color(0xFFFF7D00);
|
||||
break;
|
||||
default:
|
||||
bgColor = const Color(0xFFF5F5F5);
|
||||
textColor = const Color(0xFF86909C);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceData(DeviceEntity device) {
|
||||
final dataItems = <Widget>[];
|
||||
|
||||
// 根据设备类型显示不同的数据
|
||||
if (device.type == '逆变器') {
|
||||
dataItems.add(_buildDataItem('${device.power}', 'kW | 功率'));
|
||||
dataItems.add(const SizedBox(width: 16));
|
||||
dataItems.add(_buildDataItem('${device.todayEnergy}', 'kWh | 今日发电'));
|
||||
} else if (device.type == '汇流箱') {
|
||||
if (device.current != null) {
|
||||
dataItems.add(_buildDataItem('${device.current}', 'A | 电流'));
|
||||
dataItems.add(const SizedBox(width: 16));
|
||||
}
|
||||
if (device.temperature != null) {
|
||||
dataItems.add(_buildDataItem('${device.temperature}', '℃ | 温度'));
|
||||
}
|
||||
} else if (device.type == '组件') {
|
||||
dataItems.add(_buildDataItem('${device.power}', 'kW | 功率'));
|
||||
dataItems.add(const SizedBox(width: 16));
|
||||
if (device.radiation != null) {
|
||||
dataItems.add(_buildDataItem('${device.radiation}', 'W/m² | 辐照度'));
|
||||
}
|
||||
} else {
|
||||
// 监控等其他类型
|
||||
dataItems.add(_buildDataItem('--', '数据'));
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: dataItems,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDataItem(String value, String unit) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
unit,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 无人机机场列表项卡片
|
||||
class DroneStationItemCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String id;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const DroneStationItemCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.id,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'机场状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('位置', '升压站旁 - 无人机机场'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('舱门状态', '关闭', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithProgress('充电状态', '充电中', '85%'),
|
||||
const SizedBox(height: 12),
|
||||
_buildWeatherRow(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('网络状态', '4G/5G 强'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态行(带状态标签)
|
||||
Widget _buildStatusRow(String label, String value, {String? status}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (status != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 带进度条的状态行
|
||||
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
progress,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 气象条件行
|
||||
Widget _buildWeatherRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'气象条件',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'晴',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'25°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'东南风 2级',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../pages/drone_mission_control_page.dart';
|
||||
|
||||
/// 无人机机场与设备状态组件
|
||||
class DroneStationStatusWidget extends StatelessWidget {
|
||||
const DroneStationStatusWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// 机场状态卡片
|
||||
_buildAirportStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
// 无人机状态卡片
|
||||
_buildDroneStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
// 快捷操作
|
||||
_buildQuickActions(context),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 机场状态卡片
|
||||
Widget _buildAirportStatusCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'机场状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('位置', '升压站旁 - 无人机机场'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('舱门状态', '关闭', status: '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRowWithProgress('充电状态', '充电中', '85%'),
|
||||
const SizedBox(height: 12),
|
||||
_buildWeatherRow(),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('网络状态', '4G/5G 强'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机状态卡片
|
||||
Widget _buildDroneStatusCard() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'无人机状态',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'无人机 - 01',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDroneBatteryRow(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('飞行模式', '待命'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('机身状态', '正常'),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActions(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'快捷操作',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.flight_takeoff,
|
||||
label: '开舱',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.flight,
|
||||
label: '起飞准备',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.home,
|
||||
label: '返航',
|
||||
color: const Color(0xFFFF7D00),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildQuickActionButton(
|
||||
context,
|
||||
icon: Icons.task,
|
||||
label: '任务下发',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DroneMissionControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 快捷操作按钮
|
||||
Widget _buildQuickActionButton(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态行(带状态标签)
|
||||
Widget _buildStatusRow(String label, String value, {String? status}) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (status != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 带进度条的状态行
|
||||
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
progress,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 气象条件行
|
||||
Widget _buildWeatherRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'气象条件',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'晴',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'25°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'东南风 2级',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 无人机电量行
|
||||
Widget _buildDroneBatteryRow() {
|
||||
return Row(
|
||||
children: [
|
||||
const Text(
|
||||
'电量',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: 0.78,
|
||||
backgroundColor: const Color(0xFFF2F3F5),
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B42A)),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'78%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 底部操作按钮
|
||||
class RobotActionButtons extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotActionButtons({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: '开始',
|
||||
color: const Color(0xFF165DFF),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: '暂停',
|
||||
color: const Color(0xFFFF7D00),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: '返航',
|
||||
color: Colors.white,
|
||||
borderColor: const Color(0xFFC9CDD4),
|
||||
textColor: const Color(0xFF4E5969),
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required String label,
|
||||
required Color color,
|
||||
required VoidCallback onTap,
|
||||
Color? borderColor,
|
||||
Color? textColor,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: borderColor != null
|
||||
? Border.all(color: borderColor, width: 1)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 检查清单
|
||||
class RobotChecklist extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotChecklist({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'检查清单',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildCheckItem('逆变器', true),
|
||||
_buildCheckItem('汇流箱', true),
|
||||
_buildCheckItem('支架结构', true),
|
||||
_buildCheckItem('线缆通道', false, isWarning: true),
|
||||
_buildCheckItem('环境与杂物', false),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCheckItem(String label, bool isDone, {bool isWarning = false}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isDone)
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFF00B42A),
|
||||
size: 20,
|
||||
)
|
||||
else if (isWarning)
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: Color(0xFFFF7D00),
|
||||
size: 20,
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.radio_button_unchecked,
|
||||
color: Color(0xFFC9CDD4),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 机器人控制面板
|
||||
class RobotControlPanel extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotControlPanel({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'控制面板',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 方向控制盘
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 120,
|
||||
height: 120,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// 背景圆
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F6F8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
// 上
|
||||
Positioned(
|
||||
top: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_upward, () {}),
|
||||
),
|
||||
// 下
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_downward, () {}),
|
||||
),
|
||||
// 左
|
||||
Positioned(
|
||||
left: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_back, () {}),
|
||||
),
|
||||
// 右
|
||||
Positioned(
|
||||
right: 8,
|
||||
child: _buildDirectionButton(Icons.arrow_forward, () {}),
|
||||
),
|
||||
// 中心按钮
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF4080FF),
|
||||
Color(0xFF165DFF),
|
||||
],
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 速度档位
|
||||
const Text(
|
||||
'速度档位',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSpeedButton('低速', false, () {}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpeedButton('中速', true, () {}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildSpeedButton('高速', false, () {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDirectionButton(IconData icon, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: const Color(0xFF1D2129), size: 20),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedButton(String label, bool isSelected, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFF5F6F8),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.white : const Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 机器人顶部信息卡片
|
||||
class RobotHeaderCard extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotHeaderCard({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
robot['name'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00B42A).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'在线',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 信号强度
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.signal_cellular_alt,
|
||||
size: 16,
|
||||
color: const Color(0xFF00B42A),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'强',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF00B42A),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 设置图标
|
||||
const Icon(Icons.settings, size: 20, color: Color(0xFF86909C)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 机器人图片
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
'assets/images/xunjian.png',
|
||||
height: 180,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 机器人卡片组件
|
||||
class RobotItemCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String id;
|
||||
final String type; // 机器人类型:巡检机器人、清洗机器人、除草机器人
|
||||
final String status;
|
||||
final double battery;
|
||||
final String task;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const RobotItemCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.battery,
|
||||
required this.task,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:图标、名称、状态
|
||||
Row(
|
||||
children: [
|
||||
// 机器人图标
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
_getRobotImage(),
|
||||
width: 48,
|
||||
height: 48,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 名称和ID
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'ID: $id',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 状态标签
|
||||
_buildStatusTag(status),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 第二行:电量和任务
|
||||
Row(
|
||||
children: [
|
||||
// 电量
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
battery > 50
|
||||
? Icons.battery_full
|
||||
: battery > 20
|
||||
? Icons.battery_std
|
||||
: Icons.battery_alert,
|
||||
size: 16,
|
||||
color: battery > 50
|
||||
? const Color(0xFF00B42A)
|
||||
: battery > 20
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFFF53F3F),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${battery.toInt()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: battery > 50
|
||||
? const Color(0xFF00B42A)
|
||||
: battery > 20
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFFF53F3F),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'电量',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 任务状态
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
task,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'当前任务',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusTag(String status) {
|
||||
Color bgColor;
|
||||
Color textColor;
|
||||
|
||||
switch (status) {
|
||||
case '在线':
|
||||
bgColor = const Color(0xFFF0F9FF);
|
||||
textColor = const Color(0xFF00B42A);
|
||||
break;
|
||||
case '离线':
|
||||
bgColor = const Color(0xFFF5F0FF);
|
||||
textColor = const Color(0xFFF53F3F);
|
||||
break;
|
||||
case '异常':
|
||||
bgColor = const Color(0xFFFFF7E6);
|
||||
textColor = const Color(0xFFFF7D00);
|
||||
break;
|
||||
default:
|
||||
bgColor = const Color(0xFFF5F5F5);
|
||||
textColor = const Color(0xFF86909C);
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getRobotImage() {
|
||||
switch (type) {
|
||||
case '巡检机器人':
|
||||
return 'assets/images/xunjian.png';
|
||||
case '清洗机器人':
|
||||
return 'assets/images/qinxi.png';
|
||||
case '除草机器人':
|
||||
return 'assets/images/chucao.png';
|
||||
default:
|
||||
return 'assets/images/xunjian.png';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 机器人状态栏
|
||||
class RobotStatusBar extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotStatusBar({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildStatusItem(
|
||||
label: '速度',
|
||||
value: '1.2',
|
||||
unit: 'm/s',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '里程',
|
||||
value: '2.36',
|
||||
unit: 'km',
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '电量',
|
||||
value: '82',
|
||||
unit: '%',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)),
|
||||
_buildStatusItem(
|
||||
label: '信号',
|
||||
value: '强',
|
||||
unit: '',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusItem({
|
||||
required String label,
|
||||
required String value,
|
||||
required String unit,
|
||||
Color? valueColor,
|
||||
}) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: valueColor ?? const Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
if (unit.isNotEmpty)
|
||||
Text(
|
||||
unit,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: valueColor ?? const Color(0xFF1D2129),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 当前任务信息
|
||||
class RobotTaskInfo extends StatelessWidget {
|
||||
final Map<String, dynamic> robot;
|
||||
|
||||
const RobotTaskInfo({super.key, required this.robot});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'当前任务',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
robot['task'] ?? '逆变器区巡检',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'进行中',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Color(0xFF165DFF),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Widgets 层
|
||||
export 'device_bottom_nav_bar.dart';
|
||||
export 'device_item_widget.dart';
|
||||
@@ -58,6 +58,7 @@ class HomeRemoteDataSourceImpl implements HomeRemoteDataSource {
|
||||
plantOverview: const PlantOverview(
|
||||
installedCapacity: 1.26,
|
||||
commissionDate: '2024-05-18',
|
||||
backgroundImage: 'assets/images/dianzhan.png',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,18 @@ class HomeV2Page extends StatefulWidget {
|
||||
|
||||
class _HomeV2PageState extends State<HomeV2Page> {
|
||||
late HomeV2Bloc _bloc;
|
||||
String _selectedPlant = '示例光伏电站';
|
||||
final List<String> _plantList = [
|
||||
'示例光伏电站',
|
||||
'一号光伏电站',
|
||||
'二号光伏电站',
|
||||
'三号光伏电站',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = context.read<HomeV2Bloc>();
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -33,12 +39,6 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
body: BlocBuilder<HomeV2Bloc, HomeV2State>(
|
||||
builder: (context, state) {
|
||||
if (state is HomeV2Loading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is HomeV2Error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -68,86 +68,40 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
pinned: true,
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
|
||||
color: Colors.white,
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text(
|
||||
'示例光伏电站',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: _showPlantSelector,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_selectedPlant,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.sentiment_satisfied_alt_outlined, size: 24),
|
||||
onPressed: () => print('点击表情'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'多云 28°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF0F0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)),
|
||||
const SizedBox(width: 6),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'紧急告警',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
icon: const Icon(Icons.qr_code_scanner, size: 24, color: Color(0xFF1D2129)),
|
||||
onPressed: () => print('点击扫码'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -155,25 +109,83 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 40),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
const SizedBox(height: 16),
|
||||
// 天气和告警
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF2F3F5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'多云 28°C',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF0F0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)),
|
||||
const SizedBox(width: 6),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'紧急告警',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
PowerCard(data: state.homeData),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
StatsGrid(data: state.homeData),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
PlantOverviewCard(overview: state.homeData.plantOverview),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
WorkOrderCard(stats: state.homeData.workOrderStats),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
QuickEntryCard(),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 10),
|
||||
PowerTrendChart(
|
||||
data: state.homeData.powerTrendData,
|
||||
trendType: state.trendType,
|
||||
onToggleType: (type) => _bloc.add(HomeV2ToggleTrendType(type)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 40),
|
||||
]),
|
||||
),
|
||||
),
|
||||
@@ -187,4 +199,54 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// 首次进入时自动加载数据,但不显示转圈
|
||||
if (_bloc.state is HomeV2Initial) {
|
||||
_bloc.add(const HomeV2LoadData());
|
||||
}
|
||||
}
|
||||
|
||||
void _showPlantSelector() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
builder: (context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'选择电站',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
..._plantList.map((plant) => ListTile(
|
||||
title: Text(plant),
|
||||
trailing: _selectedPlant == plant
|
||||
? const Icon(Icons.check, color: Color(0xFF165DFF))
|
||||
: null,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPlant = plant;
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ class PowerCard extends StatelessWidget {
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF165DFF), Color(0xFF4080FF)],
|
||||
colors: [
|
||||
Color(0xFF165DFF),
|
||||
Color(0xFF4080FF),
|
||||
Color(0xFF7CB3FF),
|
||||
Color(0xFFB8D4FF),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
|
||||
@@ -87,10 +87,16 @@ class PowerTrendChart extends StatelessWidget {
|
||||
reservedSize: 30,
|
||||
interval: 6,
|
||||
getTitlesWidget: (value, meta) {
|
||||
const titles = ['00:00', '06:00', '12:00', '18:00', '24:00'];
|
||||
if (value.toInt() % 6 == 0 && value.toInt() < titles.length) {
|
||||
final index = value.toInt();
|
||||
if (index == 0 || index == 6 || index == 12 || index == 18 || index == 23) {
|
||||
String time;
|
||||
if (index == 23) {
|
||||
time = '23:00';
|
||||
} else {
|
||||
time = '${index.toString().padLeft(2, '0')}:00';
|
||||
}
|
||||
return Text(
|
||||
titles[value.toInt() ~/ 6],
|
||||
time,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF86909C),
|
||||
fontSize: 12,
|
||||
|
||||
@@ -27,11 +27,65 @@ class QuickEntryCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
SizedBox(
|
||||
height: 85,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
children: [
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.upload_file,
|
||||
label: '设备上报',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.qr_code_scanner,
|
||||
label: '扫一扫',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.assignment,
|
||||
label: '工单任务',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.notifications_active,
|
||||
label: '告警中心',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.map,
|
||||
label: '电厂地图',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildQuickEntryCard(
|
||||
icon: Icons.devices,
|
||||
label: '设备列表',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickEntryCard({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () => print('点击:$label'),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SizedBox(
|
||||
width: 85,
|
||||
height: 85,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
@@ -40,54 +94,29 @@ class QuickEntryCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildQuickEntryItem(
|
||||
icon: Icons.upload_file,
|
||||
label: '设备上报',
|
||||
Icon(
|
||||
icon,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 26,
|
||||
),
|
||||
_buildQuickEntryItem(
|
||||
icon: Icons.qr_code_scanner,
|
||||
label: '扫一扫',
|
||||
),
|
||||
_buildQuickEntryItem(
|
||||
icon: Icons.assignment,
|
||||
label: '工单任务',
|
||||
),
|
||||
_buildQuickEntryItem(
|
||||
icon: Icons.notifications_active,
|
||||
label: '告警中心',
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickEntryItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: () => print('点击:$label'),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,23 +10,26 @@ class StatsGrid extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1.5,
|
||||
childAspectRatio: 1.8,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_buildStatCard(
|
||||
title: '今日发电量',
|
||||
value: data.todayGeneration.toStringAsFixed(0),
|
||||
value: _formatNumber(data.todayGeneration),
|
||||
unit: 'kWh',
|
||||
valueColor: const Color(0xFF00B42A),
|
||||
showArrow: false,
|
||||
),
|
||||
_buildStatCard(
|
||||
title: '等效小时数',
|
||||
value: data.equivalentHours.toStringAsFixed(1),
|
||||
unit: 'h',
|
||||
valueColor: const Color(0xFF165DFF),
|
||||
showArrow: false,
|
||||
),
|
||||
_buildAlarmCard(data.alarmCount),
|
||||
_buildWorkOrderCard(data.pendingWorkOrders),
|
||||
@@ -39,9 +42,10 @@ class StatsGrid extends StatelessWidget {
|
||||
required String value,
|
||||
required String unit,
|
||||
required Color valueColor,
|
||||
bool showArrow = false,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -56,23 +60,32 @@ class StatsGrid extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 14,
|
||||
if (showArrow)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 14,
|
||||
),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
@@ -102,7 +115,7 @@ class StatsGrid extends StatelessWidget {
|
||||
|
||||
Widget _buildAlarmCard(int count) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF0F0),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -125,15 +138,15 @@ class StatsGrid extends StatelessWidget {
|
||||
'告警数',
|
||||
style: TextStyle(
|
||||
color: Color(0xFFF53F3F),
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFF53F3F),
|
||||
fontSize: 32,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -141,15 +154,15 @@ class StatsGrid extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFFE8E8),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.notifications_active,
|
||||
size: 28,
|
||||
Icons.notifications_none,
|
||||
size: 24,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
),
|
||||
@@ -160,7 +173,7 @@ class StatsGrid extends StatelessWidget {
|
||||
|
||||
Widget _buildWorkOrderCard(int count) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -183,15 +196,15 @@ class StatsGrid extends StatelessWidget {
|
||||
'待办工单',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF4E5969),
|
||||
fontSize: 14,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF1D2129),
|
||||
fontSize: 32,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -199,15 +212,15 @@ class StatsGrid extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFE8F3FF),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.assignment,
|
||||
size: 28,
|
||||
Icons.content_paste,
|
||||
size: 24,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
@@ -215,4 +228,18 @@ class StatsGrid extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatNumber(double number) {
|
||||
final formatted = number.toStringAsFixed(0);
|
||||
final parts = formatted.split('.');
|
||||
final intPart = parts[0];
|
||||
final buffer = StringBuffer();
|
||||
for (int i = 0; i < intPart.length; i++) {
|
||||
if (i > 0 && (intPart.length - i) % 3 == 0) {
|
||||
buffer.write(',');
|
||||
}
|
||||
buffer.write(intPart[i]);
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,96 +30,95 @@ class WorkOrderCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildWorkOrderCard(
|
||||
title: '待处理',
|
||||
count: stats.pending,
|
||||
color: const Color(0xFFF53F3F),
|
||||
bgColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildWorkOrderItem(
|
||||
title: '待处理',
|
||||
count: stats.pending,
|
||||
color: const Color(0xFFF53F3F),
|
||||
bgColor: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildWorkOrderCard(
|
||||
title: '处理中',
|
||||
count: stats.processing,
|
||||
color: const Color(0xFFFF7D00),
|
||||
bgColor: const Color(0xFFFFF7E6),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildWorkOrderItem(
|
||||
title: '处理中',
|
||||
count: stats.processing,
|
||||
color: const Color(0xFFFF7D00),
|
||||
bgColor: const Color(0xFFFFF7E6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _buildWorkOrderCard(
|
||||
title: '待验收',
|
||||
count: stats.pendingAcceptance,
|
||||
color: const Color(0xFF1D2129),
|
||||
bgColor: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildWorkOrderItem(
|
||||
title: '待验收',
|
||||
count: stats.pendingAcceptance,
|
||||
color: const Color(0xFF1D2129),
|
||||
bgColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWorkOrderItem({
|
||||
Widget _buildWorkOrderCard({
|
||||
required String title,
|
||||
required int count,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
// TODO: 点击跳转逻辑
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color: color.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: color.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
64
lib/features/v2/my/README.md
Normal file
64
lib/features/v2/my/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 我的页面模块
|
||||
|
||||
## 架构说明
|
||||
|
||||
采用清洁架构(Clean Architecture)四层划分:
|
||||
|
||||
### 1. Domain 层(领域层)
|
||||
- `entities/` - 实体类(UserProfileEntity, MenuItemEntity)
|
||||
- `repositories/` - 仓储接口(MyRepository)
|
||||
- `usecases/` - 用例(GetMenuItemsUseCase, ToggleNightModeUseCase, ClearCacheUseCase)
|
||||
|
||||
### 2. Data 层(数据层)
|
||||
- `models/` - 数据模型(UserProfileModel, MenuItemModel)
|
||||
- `datasources/` - 数据源接口和实现
|
||||
- `repositories/` - 仓储实现(MyRepositoryImpl)
|
||||
|
||||
### 3. Presentation 层(表现层)
|
||||
- `states/` - 状态管理(MyState)
|
||||
- `cubit/` - 业务逻辑(MyCubit)
|
||||
- `pages/` - 页面(MyPage)
|
||||
- `widgets/` - UI 组件(UserProfileCard, MenuItemCard)
|
||||
- `constants/` - 常量定义(MyColors, MyDimensions)
|
||||
- `routes/` - 路由配置(MyRoutes)
|
||||
|
||||
### 4. DI 层(依赖注入)
|
||||
- `di/` - 依赖注入配置(MyDependencyInjector)
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 用户信息展示(头像、姓名、角色)
|
||||
- 菜单列表(个人信息、消息设置、离线缓存、夜间模式等)
|
||||
- 夜间模式切换
|
||||
- 缓存清除功能
|
||||
- 响应式状态管理(Cubit)
|
||||
|
||||
## 使用示例
|
||||
|
||||
```dart
|
||||
import 'package:maibu_satabot_v2/features/v2/my/my.dart';
|
||||
|
||||
// 在路由配置中使用
|
||||
GoRoute(
|
||||
path: 'my',
|
||||
name: 'my',
|
||||
builder: (context, state) => const MyPage(),
|
||||
),
|
||||
```
|
||||
|
||||
## 数据流
|
||||
|
||||
1. **初始化**: MyPage 通过 MyDependencyInjector 创建 MyCubit
|
||||
2. **加载数据**: MyCubit 调用 UseCase 获取数据
|
||||
3. **状态更新**: 数据通过 Entity → State 传递给 UI
|
||||
4. **用户交互**: UI 触发 Cubit 方法,更新状态
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Presentation → Domain ← Data
|
||||
↓ ↓
|
||||
Cubit UseCase
|
||||
↓ ↓
|
||||
State Repository
|
||||
```
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/models/my_models.dart';
|
||||
|
||||
/// 我的页面远程数据源实现(模拟数据)
|
||||
class MyRemoteDataSourceImpl implements MyRemoteDataSource {
|
||||
@override
|
||||
Future<UserProfileModel> getUserProfile() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
return UserProfileModel(
|
||||
name: '张工程师',
|
||||
role: '运维工程师',
|
||||
avatar: 'assets/images/avatar_engineer.png',
|
||||
userId: 'ENG2024001',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MenuItemModel>> getMenuItems() async {
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
return [
|
||||
MenuItemModel(
|
||||
id: 'personal_info',
|
||||
title: '个人信息',
|
||||
icon: 'person',
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'message_settings',
|
||||
title: '消息设置',
|
||||
icon: 'notifications',
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'offline_cache',
|
||||
title: '离线缓存',
|
||||
icon: 'cloud_download',
|
||||
subTitle: '1.26 GB',
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'night_mode',
|
||||
title: '夜间模式',
|
||||
icon: 'dark_mode',
|
||||
hasSwitch: true,
|
||||
switchValue: false,
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'system_settings',
|
||||
title: '系统设置',
|
||||
icon: 'settings',
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'help_feedback',
|
||||
title: '帮助与反馈',
|
||||
icon: 'help',
|
||||
),
|
||||
MenuItemModel(
|
||||
id: 'about_us',
|
||||
title: '关于我们',
|
||||
icon: 'info',
|
||||
subTitle: 'V2.1.0',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> toggleNightMode(bool enabled) async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
// 模拟保存设置
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> clearCache() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
// 模拟清除缓存
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/models/my_models.dart';
|
||||
|
||||
/// 我的页面远程数据源抽象
|
||||
abstract class MyRemoteDataSource {
|
||||
/// 获取用户信息
|
||||
Future<UserProfileModel> getUserProfile();
|
||||
|
||||
/// 获取菜单列表
|
||||
Future<List<MenuItemModel>> getMenuItems();
|
||||
|
||||
/// 切换夜间模式
|
||||
Future<bool> toggleNightMode(bool enabled);
|
||||
|
||||
/// 清除缓存
|
||||
Future<bool> clearCache();
|
||||
}
|
||||
92
lib/features/v2/my/data/models/my_models.dart
Normal file
92
lib/features/v2/my/data/models/my_models.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
|
||||
/// 用户信息数据模型
|
||||
class UserProfileModel extends Equatable {
|
||||
const UserProfileModel({
|
||||
required this.name,
|
||||
required this.role,
|
||||
required this.avatar,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String role;
|
||||
final String avatar;
|
||||
final String userId;
|
||||
|
||||
/// 转换为实体
|
||||
UserProfileEntity toEntity() {
|
||||
return UserProfileEntity(
|
||||
name: name,
|
||||
role: role,
|
||||
avatar: avatar,
|
||||
userId: userId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, role, avatar, userId];
|
||||
}
|
||||
|
||||
/// 菜单项数据模型
|
||||
class MenuItemModel extends Equatable {
|
||||
const MenuItemModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.subTitle,
|
||||
this.hasSwitch = false,
|
||||
this.switchValue = false,
|
||||
this.hasBadge = false,
|
||||
this.badgeCount = 0,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String icon;
|
||||
final String? subTitle;
|
||||
final bool hasSwitch;
|
||||
final bool switchValue;
|
||||
final bool hasBadge;
|
||||
final int badgeCount;
|
||||
|
||||
/// 转换为实体
|
||||
MenuItemEntity toEntity() {
|
||||
return MenuItemEntity(
|
||||
id: id,
|
||||
title: title,
|
||||
icon: icon,
|
||||
subTitle: subTitle,
|
||||
hasSwitch: hasSwitch,
|
||||
switchValue: switchValue,
|
||||
hasBadge: hasBadge,
|
||||
badgeCount: badgeCount,
|
||||
);
|
||||
}
|
||||
|
||||
MenuItemModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? icon,
|
||||
String? subTitle,
|
||||
bool? hasSwitch,
|
||||
bool? switchValue,
|
||||
bool? hasBadge,
|
||||
int? badgeCount,
|
||||
}) {
|
||||
return MenuItemModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
icon: icon ?? this.icon,
|
||||
subTitle: subTitle ?? this.subTitle,
|
||||
hasSwitch: hasSwitch ?? this.hasSwitch,
|
||||
switchValue: switchValue ?? this.switchValue,
|
||||
hasBadge: hasBadge ?? this.hasBadge,
|
||||
badgeCount: badgeCount ?? this.badgeCount,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, icon, subTitle, hasSwitch, switchValue, hasBadge, badgeCount];
|
||||
}
|
||||
53
lib/features/v2/my/data/repositories/my_repository_impl.dart
Normal file
53
lib/features/v2/my/data/repositories/my_repository_impl.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart';
|
||||
|
||||
/// 我的页面仓储实现
|
||||
class MyRepositoryImpl implements MyRepository {
|
||||
MyRepositoryImpl(this._remoteDataSource);
|
||||
|
||||
final MyRemoteDataSource _remoteDataSource;
|
||||
|
||||
@override
|
||||
Future<Either<Failure, UserProfileEntity>> getUserProfile() async {
|
||||
try {
|
||||
final model = await _remoteDataSource.getUserProfile();
|
||||
return right(model.toEntity());
|
||||
} catch (e) {
|
||||
return left(ServerFailure('获取用户信息失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<MenuItemEntity>>> getMenuItems() async {
|
||||
try {
|
||||
final models = await _remoteDataSource.getMenuItems();
|
||||
final entities = models.map((model) => model.toEntity()).toList();
|
||||
return right(entities);
|
||||
} catch (e) {
|
||||
return left(ServerFailure('获取菜单列表失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> toggleNightMode(bool enabled) async {
|
||||
try {
|
||||
final result = await _remoteDataSource.toggleNightMode(enabled);
|
||||
return right(result);
|
||||
} catch (e) {
|
||||
return left(ServerFailure('切换夜间模式失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> clearCache() async {
|
||||
try {
|
||||
final result = await _remoteDataSource.clearCache();
|
||||
return right(result);
|
||||
} catch (e) {
|
||||
return left(ServerFailure('清除缓存失败: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/features/v2/my/di/my_di.dart
Normal file
32
lib/features/v2/my/di/my_di.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/datasources/impl/my_remote_datasource_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/data/repositories/my_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/clear_cache_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/get_menu_items_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart';
|
||||
|
||||
/// 我的页面依赖注入
|
||||
class MyDependencyInjector {
|
||||
/// 创建 MyCubit 实例
|
||||
static MyCubit createMyCubit() {
|
||||
// 创建数据源
|
||||
final MyRemoteDataSource remoteDataSource = MyRemoteDataSourceImpl();
|
||||
|
||||
// 创建仓储
|
||||
final MyRepository repository = MyRepositoryImpl(remoteDataSource);
|
||||
|
||||
// 创建用例
|
||||
final GetMenuItemsUseCase getMenuItemsUseCase = GetMenuItemsUseCase(repository);
|
||||
final ToggleNightModeUseCase toggleNightModeUseCase = ToggleNightModeUseCase(repository);
|
||||
final ClearCacheUseCase clearCacheUseCase = ClearCacheUseCase(repository);
|
||||
|
||||
// 创建 Cubit
|
||||
return MyCubit(
|
||||
getMenuItemsUseCase: getMenuItemsUseCase,
|
||||
toggleNightModeUseCase: toggleNightModeUseCase,
|
||||
clearCacheUseCase: clearCacheUseCase,
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/features/v2/my/domain/entities/my_entities.dart
Normal file
81
lib/features/v2/my/domain/entities/my_entities.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 用户信息实体
|
||||
class UserProfileEntity extends Equatable {
|
||||
const UserProfileEntity({
|
||||
required this.name,
|
||||
required this.role,
|
||||
required this.avatar,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String role;
|
||||
final String avatar;
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, role, avatar, userId];
|
||||
|
||||
UserProfileEntity copyWith({
|
||||
String? name,
|
||||
String? role,
|
||||
String? avatar,
|
||||
String? userId,
|
||||
}) {
|
||||
return UserProfileEntity(
|
||||
name: name ?? this.name,
|
||||
role: role ?? this.role,
|
||||
avatar: avatar ?? this.avatar,
|
||||
userId: userId ?? this.userId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 我的页面菜单项实体
|
||||
class MenuItemEntity extends Equatable {
|
||||
const MenuItemEntity({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
this.subTitle,
|
||||
this.hasSwitch = false,
|
||||
this.switchValue = false,
|
||||
this.hasBadge = false,
|
||||
this.badgeCount = 0,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String icon;
|
||||
final String? subTitle;
|
||||
final bool hasSwitch;
|
||||
final bool switchValue;
|
||||
final bool hasBadge;
|
||||
final int badgeCount;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, icon, subTitle, hasSwitch, switchValue, hasBadge, badgeCount];
|
||||
|
||||
MenuItemEntity copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? icon,
|
||||
String? subTitle,
|
||||
bool? hasSwitch,
|
||||
bool? switchValue,
|
||||
bool? hasBadge,
|
||||
int? badgeCount,
|
||||
}) {
|
||||
return MenuItemEntity(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
icon: icon ?? this.icon,
|
||||
subTitle: subTitle ?? this.subTitle,
|
||||
hasSwitch: hasSwitch ?? this.hasSwitch,
|
||||
switchValue: switchValue ?? this.switchValue,
|
||||
hasBadge: hasBadge ?? this.hasBadge,
|
||||
badgeCount: badgeCount ?? this.badgeCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
18
lib/features/v2/my/domain/repositories/my_repository.dart
Normal file
18
lib/features/v2/my/domain/repositories/my_repository.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
|
||||
/// 我的页面仓储接口
|
||||
abstract class MyRepository {
|
||||
/// 获取用户信息
|
||||
Future<Either<Failure, UserProfileEntity>> getUserProfile();
|
||||
|
||||
/// 获取菜单列表
|
||||
Future<Either<Failure, List<MenuItemEntity>>> getMenuItems();
|
||||
|
||||
/// 切换夜间模式
|
||||
Future<Either<Failure, bool>> toggleNightMode(bool enabled);
|
||||
|
||||
/// 清除缓存
|
||||
Future<Either<Failure, bool>> clearCache();
|
||||
}
|
||||
14
lib/features/v2/my/domain/usecases/clear_cache_usecase.dart
Normal file
14
lib/features/v2/my/domain/usecases/clear_cache_usecase.dart
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart';
|
||||
|
||||
/// 清除缓存用例
|
||||
class ClearCacheUseCase {
|
||||
ClearCacheUseCase(this._repository);
|
||||
|
||||
final MyRepository _repository;
|
||||
|
||||
Future<Either<Failure, bool>> call() async {
|
||||
return await _repository.clearCache();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart';
|
||||
|
||||
/// 获取菜单列表用例
|
||||
class GetMenuItemsUseCase {
|
||||
GetMenuItemsUseCase(this._repository);
|
||||
|
||||
final MyRepository _repository;
|
||||
|
||||
Future<Either<Failure, List<MenuItemEntity>>> call() async {
|
||||
return await _repository.getMenuItems();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:maibu_satabot_v2/core/error/failure.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart';
|
||||
|
||||
/// 切换夜间模式用例
|
||||
class ToggleNightModeUseCase {
|
||||
ToggleNightModeUseCase(this._repository);
|
||||
|
||||
final MyRepository _repository;
|
||||
|
||||
Future<Either<Failure, bool>> call(bool enabled) async {
|
||||
return await _repository.toggleNightMode(enabled);
|
||||
}
|
||||
}
|
||||
23
lib/features/v2/my/my.dart
Normal file
23
lib/features/v2/my/my.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
library;
|
||||
|
||||
export 'domain/entities/my_entities.dart';
|
||||
export 'domain/repositories/my_repository.dart';
|
||||
export 'domain/usecases/get_menu_items_usecase.dart';
|
||||
export 'domain/usecases/toggle_night_mode_usecase.dart';
|
||||
export 'domain/usecases/clear_cache_usecase.dart';
|
||||
|
||||
export 'data/models/my_models.dart';
|
||||
export 'data/datasources/my_remote_datasource.dart';
|
||||
export 'data/datasources/impl/my_remote_datasource_impl.dart';
|
||||
export 'data/repositories/my_repository_impl.dart';
|
||||
|
||||
export 'presentation/states/my_state.dart';
|
||||
export 'presentation/cubit/my_cubit.dart';
|
||||
export 'presentation/pages/my_page.dart';
|
||||
export 'presentation/constants/my_constants.dart';
|
||||
export 'presentation/routes/my_routes.dart';
|
||||
export 'presentation/widgets/user_profile_card.dart';
|
||||
export 'presentation/widgets/quick_action_card.dart';
|
||||
export 'presentation/widgets/menu_item_card.dart';
|
||||
|
||||
export 'di/my_di.dart';
|
||||
41
lib/features/v2/my/presentation/constants/my_constants.dart
Normal file
41
lib/features/v2/my/presentation/constants/my_constants.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
/// 我的页面全局常量
|
||||
/// 包含颜色、尺寸等全局配置
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 颜色常量
|
||||
class MyColors {
|
||||
/// 主色调
|
||||
static const Color primary = Color(0xFF165DFF);
|
||||
|
||||
/// 背景色
|
||||
static const Color background = Color(0xFFF5F7FA);
|
||||
|
||||
/// 卡片背景色
|
||||
static const Color cardBackground = Color(0xFFFFFFFF);
|
||||
|
||||
/// 主要文字色
|
||||
static const Color textPrimary = Color(0xFF1D2129);
|
||||
|
||||
/// 次要文字色
|
||||
static const Color textSecondary = Color(0xFF4E5969);
|
||||
|
||||
/// 辅助文字色
|
||||
static const Color textTertiary = Color(0xFF86909C);
|
||||
}
|
||||
|
||||
/// 尺寸常量
|
||||
class MyDimensions {
|
||||
/// 通用圆角
|
||||
static const double borderRadius = 12.0;
|
||||
|
||||
/// 水平边距
|
||||
static const double horizontalPadding = 16.0;
|
||||
|
||||
/// 模块间距
|
||||
static const double moduleSpacing = 16.0;
|
||||
|
||||
/// 卡片阴影模糊半径
|
||||
static const double cardShadowBlur = 8.0;
|
||||
}
|
||||
102
lib/features/v2/my/presentation/cubit/my_cubit.dart
Normal file
102
lib/features/v2/my/presentation/cubit/my_cubit.dart
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/clear_cache_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/get_menu_items_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart';
|
||||
|
||||
/// 我的页面 Cubit
|
||||
class MyCubit extends Cubit<MyState> {
|
||||
MyCubit({
|
||||
required GetMenuItemsUseCase getMenuItemsUseCase,
|
||||
required ToggleNightModeUseCase toggleNightModeUseCase,
|
||||
required ClearCacheUseCase clearCacheUseCase,
|
||||
}) : _getMenuItemsUseCase = getMenuItemsUseCase,
|
||||
_toggleNightModeUseCase = toggleNightModeUseCase,
|
||||
_clearCacheUseCase = clearCacheUseCase,
|
||||
super(MyInitial());
|
||||
|
||||
final GetMenuItemsUseCase _getMenuItemsUseCase;
|
||||
final ToggleNightModeUseCase _toggleNightModeUseCase;
|
||||
final ClearCacheUseCase _clearCacheUseCase;
|
||||
|
||||
/// 加载数据
|
||||
Future<void> loadData() async {
|
||||
emit(MyLoading());
|
||||
|
||||
try {
|
||||
final menuResult = await _getMenuItemsUseCase();
|
||||
|
||||
final menuItems = menuResult.fold(
|
||||
(failure) => <MenuItemEntity>[],
|
||||
(items) => items,
|
||||
);
|
||||
|
||||
if (menuItems.isEmpty) {
|
||||
emit(const MyError('加载菜单失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 模拟用户信息(实际应从 Auth 模块获取)
|
||||
final userProfile = UserProfileEntity(
|
||||
name: '张工程师',
|
||||
role: '运维工程师',
|
||||
avatar: '',
|
||||
userId: 'ENG2024001',
|
||||
);
|
||||
|
||||
emit(MyLoaded(
|
||||
userProfile: userProfile,
|
||||
menuItems: menuItems,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(MyError('加载数据失败: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换夜间模式
|
||||
Future<void> toggleNightMode(bool enabled) async {
|
||||
final currentState = state;
|
||||
if (currentState is MyLoaded) {
|
||||
// 乐观更新 UI
|
||||
emit(currentState.copyWith(isNightMode: enabled));
|
||||
|
||||
final result = await _toggleNightModeUseCase(enabled);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
// 如果失败,回滚状态
|
||||
emit(currentState.copyWith(isNightMode: !enabled));
|
||||
emit(MyError('切换夜间模式失败'));
|
||||
},
|
||||
(_) {
|
||||
// 成功,保持当前状态
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除缓存
|
||||
Future<void> clearCache() async {
|
||||
final currentState = state;
|
||||
if (currentState is! MyLoaded) return;
|
||||
|
||||
emit(MyLoading());
|
||||
|
||||
final result = await _clearCacheUseCase();
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
emit(MyError('清除缓存失败: ${failure.message}'));
|
||||
},
|
||||
(success) {
|
||||
emit(currentState.copyWith(cacheSize: '0 MB'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 刷新数据
|
||||
Future<void> refresh() async {
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
173
lib/features/v2/my/presentation/pages/my_page.dart
Normal file
173
lib/features/v2/my/presentation/pages/my_page.dart
Normal file
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/di/my_di.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/user_profile_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/quick_action_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/menu_item_card.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/system_settings_page.dart';
|
||||
|
||||
/// 我的页面(100% 还原设计稿)
|
||||
class MyPage extends StatelessWidget {
|
||||
const MyPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => MyDependencyInjector.createMyCubit()..loadData(),
|
||||
child: _MyPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MyPageContent extends StatelessWidget {
|
||||
const _MyPageContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
),
|
||||
child: Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
backgroundColor: MyColors.background,
|
||||
body: BlocConsumer<MyCubit, MyState>(
|
||||
listener: (context, state) {
|
||||
if (state is MyError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is MyLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: MyColors.primary),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is MyLoaded) {
|
||||
final cubit = context.read<MyCubit>();
|
||||
return Column(
|
||||
children: [
|
||||
// 用户信息卡片(延伸到状态栏)
|
||||
UserProfileCard(
|
||||
name: state.userProfile.name,
|
||||
role: state.userProfile.role,
|
||||
onTap: () {
|
||||
// TODO: 跳转到个人信息页面
|
||||
debugPrint('点击个人信息');
|
||||
},
|
||||
),
|
||||
// 快捷入口卡片(负margin向上重叠)
|
||||
Transform.translate(
|
||||
offset: const Offset(0, -15),
|
||||
child: QuickActionCard(
|
||||
onMessageTap: () {
|
||||
debugPrint('我的消息');
|
||||
},
|
||||
onFavoriteTap: () {
|
||||
debugPrint('我的收藏');
|
||||
},
|
||||
onWorkOrderTap: () {
|
||||
debugPrint('我的工单');
|
||||
},
|
||||
),
|
||||
),
|
||||
// 菜单列表(白色卡片)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 15, left: 16, right: 16, bottom: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x0D000000),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.5,
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: state.menuItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = state.menuItems[index];
|
||||
return MenuItemCard(
|
||||
item: item,
|
||||
onTap: () => _handleMenuItemTap(context, cubit, item),
|
||||
onSwitchChanged: item.hasSwitch
|
||||
? (value) => cubit.toggleNightMode(value)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 处理菜单项点击
|
||||
void _handleMenuItemTap(BuildContext context, MyCubit cubit, dynamic item) {
|
||||
switch (item.id) {
|
||||
case 'offline_cache':
|
||||
_showClearCacheDialog(context, cubit);
|
||||
break;
|
||||
case 'night_mode':
|
||||
// 由 Switch 处理
|
||||
break;
|
||||
case 'system_settings':
|
||||
// 跳转到系统设置页面
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SystemSettingsPage(),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
debugPrint('点击菜单: ${item.title}');
|
||||
// TODO: 根据 ID 跳转到对应页面
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示清除缓存对话框
|
||||
void _showClearCacheDialog(BuildContext context, MyCubit cubit) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('清除缓存'),
|
||||
content: const Text('确定要清除所有缓存数据吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
cubit.clearCache();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
284
lib/features/v2/my/presentation/pages/system_settings_page.dart
Normal file
284
lib/features/v2/my/presentation/pages/system_settings_page.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/router/route_paths.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/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
|
||||
/// 系统设置综合页面
|
||||
class SystemSettingsPage extends StatefulWidget {
|
||||
const SystemSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<SystemSettingsPage> createState() => _SystemSettingsPageState();
|
||||
}
|
||||
|
||||
class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).translate('my.settings'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
// Tab 设置
|
||||
_buildTabSettingsSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 语言设置
|
||||
_buildLanguageSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 退出登录
|
||||
_buildLogoutButton(context),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Tab 设置
|
||||
Widget _buildTabSettingsSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Tab 设置',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<TabConfigCubit, dynamic>(
|
||||
builder: (context, state) {
|
||||
if (state.runtimeType.toString() != 'TabConfigLoaded') {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final tabs = state.config.items;
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tab = tabs[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tab.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: tab.isEnabled,
|
||||
onChanged: (value) {
|
||||
context.read<TabConfigCubit>().toggleTab(tab.id);
|
||||
},
|
||||
activeColor: const Color(0xFF165DFF),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 语言设置
|
||||
Widget _buildLanguageSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).translate('my.language'),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BlocBuilder<LocaleCubit, Locale>(
|
||||
builder: (context, locale) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildLanguageOption(
|
||||
AppLocalizations.of(context).translate('my.chinese'),
|
||||
'zh',
|
||||
locale.languageCode == 'zh',
|
||||
() => context.read<LocaleCubit>().setLocale(const Locale('zh', 'CN')),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildLanguageOption(
|
||||
AppLocalizations.of(context).translate('my.english'),
|
||||
'en',
|
||||
locale.languageCode == 'en',
|
||||
() => context.read<LocaleCubit>().setLocale(const Locale('en', 'US')),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFF165DFF),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 退出登录按钮
|
||||
Widget _buildLogoutButton(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showLogoutDialog(context);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).translate('login.logout'),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示退出登录确认对话框
|
||||
void _showLogoutDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).translate('login.logout')),
|
||||
content: const Text('确定要退出登录吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
// 执行退出登录逻辑
|
||||
await GetIt.I<UserStorage>().deleteUser();
|
||||
GetIt.I<AuthCubit>().logout();
|
||||
context.go(RoutePaths.login);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFF53F3F),
|
||||
),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
lib/features/v2/my/presentation/routes/my_routes.dart
Normal file
13
lib/features/v2/my/presentation/routes/my_routes.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart';
|
||||
|
||||
/// 我的页面路由配置
|
||||
class MyRoutes {
|
||||
static List<RouteBase> get routes => [
|
||||
GoRoute(
|
||||
path: 'my',
|
||||
name: 'my',
|
||||
builder: (context, state) => const MyPage(),
|
||||
),
|
||||
];
|
||||
}
|
||||
58
lib/features/v2/my/presentation/states/my_state.dart
Normal file
58
lib/features/v2/my/presentation/states/my_state.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
|
||||
/// 我的页面状态基类
|
||||
abstract class MyState extends Equatable {
|
||||
const MyState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
/// 初始状态
|
||||
class MyInitial extends MyState {}
|
||||
|
||||
/// 加载中状态
|
||||
class MyLoading extends MyState {}
|
||||
|
||||
/// 加载成功状态
|
||||
class MyLoaded extends MyState {
|
||||
const MyLoaded({
|
||||
required this.userProfile,
|
||||
required this.menuItems,
|
||||
this.isNightMode = false,
|
||||
this.cacheSize = '1.26 GB',
|
||||
});
|
||||
|
||||
final UserProfileEntity userProfile;
|
||||
final List<MenuItemEntity> menuItems;
|
||||
final bool isNightMode;
|
||||
final String cacheSize;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userProfile, menuItems, isNightMode, cacheSize];
|
||||
|
||||
MyLoaded copyWith({
|
||||
UserProfileEntity? userProfile,
|
||||
List<MenuItemEntity>? menuItems,
|
||||
bool? isNightMode,
|
||||
String? cacheSize,
|
||||
}) {
|
||||
return MyLoaded(
|
||||
userProfile: userProfile ?? this.userProfile,
|
||||
menuItems: menuItems ?? this.menuItems,
|
||||
isNightMode: isNightMode ?? this.isNightMode,
|
||||
cacheSize: cacheSize ?? this.cacheSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载失败状态
|
||||
class MyError extends MyState {
|
||||
const MyError(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
118
lib/features/v2/my/presentation/widgets/menu_item_card.dart
Normal file
118
lib/features/v2/my/presentation/widgets/menu_item_card.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart';
|
||||
|
||||
/// 菜单项卡片组件(100% 还原设计稿)
|
||||
class MenuItemCard extends StatelessWidget {
|
||||
const MenuItemCard({
|
||||
super.key,
|
||||
required this.item,
|
||||
this.onTap,
|
||||
this.onSwitchChanged,
|
||||
});
|
||||
|
||||
final MenuItemEntity item;
|
||||
final VoidCallback? onTap;
|
||||
final ValueChanged<bool>? onSwitchChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 图标
|
||||
Icon(
|
||||
_getIconData(item.icon),
|
||||
size: 20,
|
||||
color: const Color(0xFF4E5969),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 标题
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 副标题或开关
|
||||
if (item.subTitle != null) ...[
|
||||
Text(
|
||||
item.subTitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
if (item.hasSwitch) ...[
|
||||
Transform.scale(
|
||||
scale: 0.85,
|
||||
child: Switch(
|
||||
value: item.switchValue,
|
||||
onChanged: onSwitchChanged,
|
||||
activeColor: const Color(0xFF165DFF),
|
||||
activeTrackColor: const Color(0xFF165DFF).withOpacity(0.3),
|
||||
inactiveThumbColor: const Color(0xFFC9CDD4),
|
||||
inactiveTrackColor: const Color(0xFFE5E6EB),
|
||||
),
|
||||
),
|
||||
],
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: const Color(0xFFC9CDD4),
|
||||
size: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// 分割线
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 48),
|
||||
child: Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: const Color(0xFFF0F0F0),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据字符串获取 IconData
|
||||
IconData _getIconData(String iconName) {
|
||||
switch (iconName) {
|
||||
case 'person':
|
||||
return Icons.person_outline;
|
||||
case 'notifications':
|
||||
return Icons.notifications_outlined;
|
||||
case 'cloud_download':
|
||||
return Icons.cloud_download_outlined;
|
||||
case 'dark_mode':
|
||||
return Icons.dark_mode_outlined;
|
||||
case 'settings':
|
||||
return Icons.settings_outlined;
|
||||
case 'help':
|
||||
return Icons.help_outline;
|
||||
case 'info':
|
||||
return Icons.info_outline;
|
||||
default:
|
||||
return Icons.label_outline;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
lib/features/v2/my/presentation/widgets/quick_action_card.dart
Normal file
122
lib/features/v2/my/presentation/widgets/quick_action_card.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart';
|
||||
|
||||
/// 快捷入口卡片组件(我的消息、我的收藏、我的工单)
|
||||
class QuickActionCard extends StatelessWidget {
|
||||
const QuickActionCard({
|
||||
super.key,
|
||||
required this.onMessageTap,
|
||||
required this.onFavoriteTap,
|
||||
required this.onWorkOrderTap,
|
||||
});
|
||||
|
||||
final VoidCallback onMessageTap;
|
||||
final VoidCallback onFavoriteTap;
|
||||
final VoidCallback onWorkOrderTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0x0D000000),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// 我的消息
|
||||
_buildQuickActionItem(
|
||||
icon: Icons.notifications_outlined,
|
||||
label: '我的消息',
|
||||
onTap: onMessageTap,
|
||||
showBadge: true,
|
||||
),
|
||||
// 分割线
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFF0F0F0),
|
||||
),
|
||||
// 我的收藏
|
||||
_buildQuickActionItem(
|
||||
icon: Icons.favorite_border,
|
||||
label: '我的收藏',
|
||||
onTap: onFavoriteTap,
|
||||
),
|
||||
// 分割线
|
||||
Container(
|
||||
width: 1,
|
||||
height: 40,
|
||||
color: const Color(0xFFF0F0F0),
|
||||
),
|
||||
// 我的工单
|
||||
_buildQuickActionItem(
|
||||
icon: Icons.assignment_outlined,
|
||||
label: '我的工单',
|
||||
onTap: onWorkOrderTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickActionItem({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
bool showBadge = false,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 24,
|
||||
color: MyColors.primary,
|
||||
),
|
||||
if (showBadge)
|
||||
Positioned(
|
||||
right: -4,
|
||||
top: -4,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFF4D4F),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: MyColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart';
|
||||
|
||||
/// 用户信息卡片组件(100% 还原设计稿)
|
||||
class UserProfileCard extends StatelessWidget {
|
||||
const UserProfileCard({
|
||||
super.key,
|
||||
required this.name,
|
||||
required this.role,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String role;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 70, 20, 50),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFF165DFF),
|
||||
Color(0xFF0E42CC),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 头像
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/app_logo.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 32,
|
||||
color: const Color(0xFF165DFF),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
// 用户信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
role,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 箭头
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
212
lib/features/v2/report/README.md
Normal file
212
lib/features/v2/report/README.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 现场上报模块使用说明
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 在路由中注册页面
|
||||
|
||||
```dart
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/report/report.dart';
|
||||
|
||||
// 在路由配置中添加
|
||||
GoRoute(
|
||||
path: '/report',
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (context) => ReportDependencyInjector.createReportCubit(),
|
||||
child: const ReportPage(),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
### 2. 跳转到上报页面
|
||||
|
||||
```dart
|
||||
context.push('/report');
|
||||
```
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 清洁架构分层
|
||||
|
||||
```
|
||||
lib/features/v2/report/
|
||||
├── domain/ # 领域层(核心业务逻辑)
|
||||
│ ├── entities/ # 实体类
|
||||
│ │ └── report_entity.dart
|
||||
│ ├── repositories/ # 仓储接口
|
||||
│ │ └── report_repository.dart
|
||||
│ └── usecases/ # 用例
|
||||
│ └── submit_report_usecase.dart
|
||||
├── data/ # 数据层(数据获取与存储)
|
||||
│ ├── datasources/ # 数据源
|
||||
│ │ ├── report_remote_datasource.dart
|
||||
│ │ └── report_remote_datasource_impl.dart
|
||||
│ ├── models/ # 数据模型
|
||||
│ │ └── report_model.dart
|
||||
│ └── repositories/ # 仓储实现
|
||||
│ └── report_repository_impl.dart
|
||||
├── presentation/ # 表现层(UI + 状态管理)
|
||||
│ ├── constants/ # 常量
|
||||
│ │ └── report_constants.dart
|
||||
│ ├── cubit/ # 状态管理
|
||||
│ │ └── report_cubit.dart
|
||||
│ ├── states/ # 状态定义
|
||||
│ │ └── report_state.dart
|
||||
│ ├── pages/ # 页面
|
||||
│ │ └── report_page.dart
|
||||
│ ── widgets/ # 组件
|
||||
│ ├── report_type_selector.dart
|
||||
│ ├── device_selector.dart
|
||||
│ ├── description_input.dart
|
||||
│ ├── media_uploader.dart
|
||||
│ └── level_selector.dart
|
||||
├── di/ # 依赖注入
|
||||
│ └── report_di.dart
|
||||
└── report.dart # 统一导出
|
||||
```
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 上报类型选择
|
||||
- 设备故障
|
||||
- 巡检记录
|
||||
- 隐患上报
|
||||
- 缺陷上报
|
||||
|
||||
### 2. 设备选择
|
||||
- 点击选择设备(需实现设备选择页面)
|
||||
|
||||
### 3. 问题描述
|
||||
- 多行文本输入
|
||||
- 最大 200 字符限制
|
||||
- 实时字符计数
|
||||
|
||||
### 4. 媒体上传
|
||||
- 拍照上传
|
||||
- 录像上传
|
||||
- 图片预览
|
||||
- 删除已上传图片
|
||||
|
||||
### 5. 问题等级
|
||||
- 一般(灰色)
|
||||
- 重要(橙色)
|
||||
- 紧急(红色)
|
||||
|
||||
## 表单验证
|
||||
|
||||
提交时自动验证以下必填项:
|
||||
1. 设备选择
|
||||
2. 问题描述
|
||||
3. 问题等级
|
||||
|
||||
## 状态管理
|
||||
|
||||
### 状态类型
|
||||
- `ReportInitial`: 初始状态
|
||||
- `ReportFormState`: 表单数据状态
|
||||
- `ReportSubmitting`: 提交中状态
|
||||
- `ReportSuccess`: 提交成功状态
|
||||
- `ReportFailure`: 提交失败状态
|
||||
|
||||
### Cubit 方法
|
||||
- `selectReportType(ReportType)`: 选择上报类型
|
||||
- `selectDevice(String)`: 选择设备
|
||||
- `updateDescription(String)`: 更新问题描述
|
||||
- `selectProblemLevel(ProblemLevel)`: 选择问题等级
|
||||
- `addMediaFile(String)`: 添加媒体文件
|
||||
- `removeMediaFile(int)`: 删除媒体文件
|
||||
- `submitReport()`: 提交上报
|
||||
- `resetForm()`: 重置表单
|
||||
|
||||
## 自定义扩展
|
||||
|
||||
### 1. 对接真实 API
|
||||
|
||||
修改 `ReportRemoteDataSourceImpl`:
|
||||
|
||||
```dart
|
||||
class ReportRemoteDataSourceImpl implements ReportRemoteDataSource {
|
||||
final Dio dio; // 注入 Dio 实例
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> submitReport(ReportModel report) async {
|
||||
try {
|
||||
final response = await dio.post(
|
||||
'/api/report/submit',
|
||||
data: report.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return right(true);
|
||||
} else {
|
||||
return left(ServerFailure('提交失败'));
|
||||
}
|
||||
} catch (e) {
|
||||
return left(NetworkFailure('网络错误: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现设备选择
|
||||
|
||||
在 `ReportPage` 中修改 `_showDevicePicker` 方法:
|
||||
|
||||
```dart
|
||||
void _showDevicePicker(BuildContext context) {
|
||||
// 跳转到设备选择页面
|
||||
context.push('/device-select').then((device) {
|
||||
if (device != null) {
|
||||
context.read<ReportCubit>().selectDevice(device);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 实现真实的相机/相册功能
|
||||
|
||||
添加依赖:
|
||||
```yaml
|
||||
dependencies:
|
||||
image_picker: ^1.1.2
|
||||
```
|
||||
|
||||
修改 `MediaUploader`:
|
||||
|
||||
```dart
|
||||
final ImagePicker picker = ImagePicker();
|
||||
|
||||
Future<void> _pickImage() async {
|
||||
final XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
cubit.addMediaFile(image.path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖说明
|
||||
|
||||
### 项目已有依赖
|
||||
- flutter_bloc: ^9.1.1
|
||||
- equatable: ^2.0.8
|
||||
- fpdart: ^1.2.0
|
||||
|
||||
### 可选依赖(按需添加)
|
||||
- image_picker: ^1.1.2 (图片/视频选择)
|
||||
- dio: ^5.9.0 (HTTP 请求,项目已有)
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有颜色、尺寸常量已抽取到 `report_constants.dart`
|
||||
2. 遵循清洁架构原则,内层不依赖外层
|
||||
3. 使用 fpdart 进行错误处理(Either 类型)
|
||||
4. 所有 UI 组件已适配 iOS 安全区
|
||||
5. 模拟数据已实现,可直接运行测试
|
||||
|
||||
## 后续开发建议
|
||||
|
||||
1. 实现真实的设备选择页面
|
||||
2. 对接后端 API
|
||||
3. 添加图片压缩功能
|
||||
4. 实现离线缓存
|
||||
5. 添加草稿保存功能
|
||||
84
lib/features/v2/report/USAGE_EXAMPLE.md
Normal file
84
lib/features/v2/report/USAGE_EXAMPLE.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# 现场上报模块 - 使用示例
|
||||
|
||||
## 1. 在路由中注册
|
||||
|
||||
在你的路由配置文件(如 `lib/core/router/app_router.dart`)中添加:
|
||||
|
||||
```dart
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/report/report.dart';
|
||||
|
||||
// 在 routes 列表中添加
|
||||
GoRoute(
|
||||
path: '/report',
|
||||
builder: (context, state) => BlocProvider(
|
||||
create: (context) => ReportDependencyInjector.createReportCubit(),
|
||||
child: const ReportPage(),
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
## 2. 跳转到上报页面
|
||||
|
||||
在任何页面中,使用以下方式跳转:
|
||||
|
||||
```dart
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// 跳转到上报页面
|
||||
context.push('/report');
|
||||
```
|
||||
|
||||
## 3. 完整示例
|
||||
|
||||
```dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class SomePage extends StatelessWidget {
|
||||
const SomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('示例页面')),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 跳转到现场上报页面
|
||||
context.push('/report');
|
||||
},
|
||||
child: const Text('现场上报'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 依赖说明
|
||||
|
||||
### 必需依赖(项目已包含)
|
||||
- flutter_bloc: ^9.1.1
|
||||
- equatable: ^2.0.8
|
||||
- fpdart: ^1.2.0
|
||||
- go_router: ^17.0.1
|
||||
|
||||
### 可选依赖(按需添加)
|
||||
如果需要实现真实的图片/视频上传功能,需要添加:
|
||||
```yaml
|
||||
dependencies:
|
||||
image_picker: ^1.1.2
|
||||
```
|
||||
|
||||
## 5. 注意事项
|
||||
|
||||
1. 确保在跳转前已经配置好路由
|
||||
2. BlocProvider 必须在 ReportPage 的父级
|
||||
3. 当前使用模拟数据,可直接运行测试
|
||||
4. 所有常量已抽取,便于主题定制
|
||||
|
||||
## 6. 后续开发
|
||||
|
||||
对接真实 API 时,修改 `lib/features/v2/report/data/datasources/report_remote_datasource_impl.dart` 文件即可。
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../../../core/error/failure.dart';
|
||||
import '../models/report_model.dart';
|
||||
|
||||
/// 上报远程数据源接口
|
||||
abstract class ReportRemoteDataSource {
|
||||
/// 提交上报数据
|
||||
Future<Either<Failure, bool>> submitReport(ReportModel report);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../../../core/error/failure.dart';
|
||||
import '../models/report_model.dart';
|
||||
import 'report_remote_datasource.dart';
|
||||
|
||||
/// 上报远程数据源实现(模拟接口请求)
|
||||
class ReportRemoteDataSourceImpl implements ReportRemoteDataSource {
|
||||
@override
|
||||
Future<Either<Failure, bool>> submitReport(ReportModel report) async {
|
||||
// 模拟网络请求延迟
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
// 模拟成功返回
|
||||
return right(true);
|
||||
|
||||
// 模拟失败情况(取消注释以测试错误处理)
|
||||
// return left(ServerFailure('提交失败,请重试'));
|
||||
}
|
||||
}
|
||||
68
lib/features/v2/report/data/models/report_model.dart
Normal file
68
lib/features/v2/report/data/models/report_model.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import '../../domain/entities/report_entity.dart';
|
||||
|
||||
/// 上报数据模型
|
||||
class ReportModel {
|
||||
final String? location;
|
||||
final String? reportType;
|
||||
final String? deviceId;
|
||||
final String? description;
|
||||
final List<String>? mediaUrls;
|
||||
final String? problemLevel;
|
||||
|
||||
ReportModel({
|
||||
this.location,
|
||||
this.reportType,
|
||||
this.deviceId,
|
||||
this.description,
|
||||
this.mediaUrls,
|
||||
this.problemLevel,
|
||||
});
|
||||
|
||||
/// 从 JSON 创建
|
||||
factory ReportModel.fromJson(Map<String, dynamic> json) {
|
||||
return ReportModel(
|
||||
location: json['location'] as String?,
|
||||
reportType: json['reportType'] as String?,
|
||||
deviceId: json['deviceId'] as String?,
|
||||
description: json['description'] as String?,
|
||||
mediaUrls: (json['mediaUrls'] as List<dynamic>?)?.cast<String>(),
|
||||
problemLevel: json['problemLevel'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为 JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'location': location,
|
||||
'reportType': reportType,
|
||||
'deviceId': deviceId,
|
||||
'description': description,
|
||||
'mediaUrls': mediaUrls,
|
||||
'problemLevel': problemLevel,
|
||||
};
|
||||
}
|
||||
|
||||
/// 转换为实体
|
||||
ReportEntity toEntity() {
|
||||
return ReportEntity(
|
||||
location: location,
|
||||
reportType: reportType,
|
||||
deviceId: deviceId,
|
||||
description: description,
|
||||
mediaUrls: mediaUrls,
|
||||
problemLevel: problemLevel,
|
||||
);
|
||||
}
|
||||
|
||||
/// 从实体创建
|
||||
factory ReportModel.fromEntity(ReportEntity entity) {
|
||||
return ReportModel(
|
||||
location: entity.location,
|
||||
reportType: entity.reportType,
|
||||
deviceId: entity.deviceId,
|
||||
description: entity.description,
|
||||
mediaUrls: entity.mediaUrls,
|
||||
problemLevel: entity.problemLevel,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user