所有一级页面和不分二级页面

This commit is contained in:
2026-05-25 12:58:29 +08:00
parent b9f747de05
commit afef5c4ef1
158 changed files with 13082 additions and 1602 deletions

View 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)

View File

@@ -8,6 +8,10 @@
<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" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 关键! -->

BIN
assets/images/chucao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
assets/images/dianzhan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

@@ -3,3 +3,4 @@ documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-exte
extensions:
- get_it: true
- provider: true
- shared_preferences: true

View 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
**维护者:** 开发团队

View File

@@ -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>

View 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, // 低
}

View File

@@ -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 依赖它,必须先注册

View File

@@ -6,3 +6,7 @@ class Failure {
class NetworkFailure extends Failure {
NetworkFailure(super.message);
}
class ServerFailure extends Failure {
ServerFailure(super.message);
}

View 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);
}

View File

@@ -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; // 重置标志,以免影响下次

View File

@@ -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,
],
);
}

View File

@@ -45,12 +45,20 @@ class _LoginPageState extends State<LoginPage> {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}')));
}
},
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 30.0),
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: 15),
// 顶部区域:图片 + 认证信息 + 标题(压缩空间)
const SizedBox(height: 10),
// 顶部居中图片和认证信息
Center(
@@ -58,20 +66,20 @@ class _LoginPageState extends State<LoginPage> {
children: [
Image.asset(
'assets/images/huawei.png',
width: 80,
height: 80,
width: 60,
height: 60,
fit: BoxFit.contain,
),
const SizedBox(height: 8),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
"通过华为云生态技术认证,请放心使用,证书编号C202605301",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
fontSize: 10,
color: Colors.grey.shade600,
height: 1.4,
height: 1.3,
),
),
),
@@ -79,15 +87,16 @@ class _LoginPageState extends State<LoginPage> {
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
const Text(
"账号登录",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)),
const SizedBox(height: 40),
const SizedBox(height: 24),
// 表单区域
// 账号输入框
_buildInputLabel("账号"),
TextField(
@@ -96,7 +105,7 @@ class _LoginPageState extends State<LoginPage> {
decoration: _inputDecoration(hint: "请输入用户名"),
),
const SizedBox(height: 24),
const SizedBox(height: 20),
// 密码输入框
_buildInputLabel("密码"),
@@ -113,23 +122,21 @@ class _LoginPageState extends State<LoginPage> {
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
// 协议勾选区域
_buildProtocolSection(),
const SizedBox(height: 40),
const SizedBox(height: 30),
// 登录按钮 (使用你的 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
: () {
@@ -137,7 +144,7 @@ class _LoginPageState extends State<LoginPage> {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请先阅读并同意用户协议'),
duration: Duration(seconds: 1), // 这里设置显示时长:2秒
duration: Duration(seconds: 1),
),
);
return;
@@ -149,7 +156,7 @@ class _LoginPageState extends State<LoginPage> {
},
),
),
const SizedBox(height: 24),
const SizedBox(height: 16),
Center(
child: Row(
@@ -159,13 +166,16 @@ class _LoginPageState extends State<LoginPage> {
"没有账号吗?",
style: TextStyle(fontSize: 14, color: Colors.grey),
),
GestureDetector(
InkWell(
onTap: () {
print('点击了立即注册');
print('跳转路径: ${RoutePaths.register}');
debugPrint(' 点击了立即注册');
debugPrint('👉 跳转路径: ${RoutePaths.register}');
context.go(RoutePaths.register);
},
child: const Text(
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
"立即注册",
style: TextStyle(
fontSize: 14,
@@ -174,19 +184,21 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
),
],
),
),
const SizedBox(height: 15),
const SizedBox(height: 20),
],
),
),
),
);
},
),
),
);
}
// 执行登录逻辑

View File

@@ -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
}
}
}

View File

@@ -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,
),
],
);

View File

@@ -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()));
}
}

View File

@@ -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 {
debugPrint('🔍 [TabConfigCubit] _loadConfig 被调用');
// 直接使用代码中的默认配置,不持久化
final defaultConfig = TabConfig.defaultConfig();
_saveConfig(defaultConfig);
debugPrint('🔍 [TabConfigCubit] 默认配置 Tab 数量: ${defaultConfig.items.length}');
debugPrint('🔍 [TabConfigCubit] 启用的 Tab: ${defaultConfig.enabledItems.map((e) => e.id).toList()}');
emit(TabConfigLoaded(defaultConfig));
}
} catch (e) {
final defaultConfig = TabConfig.defaultConfig();
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));
}

View File

@@ -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,58 +183,96 @@ 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,
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,
),
),
),
),
// 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: () {
setState(() {
_currentIndex = index;
});
_pageController.jumpToPage(index);
},
onTap: () => _onTabChanged(index),
child: SizedBox(
height: 56,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 8),
Icon(
_getIconData(item.icon),
size: 24,
color: isSelected ? Colors.blue : Colors.grey,
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
),
const SizedBox(height: 4),
Text(
item.name,
style: TextStyle(
fontSize: 11,
color: isSelected ? Colors.blue : Colors.grey,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
),
const SizedBox(height: 8),
],
),
),
),
);
}).toList(),
),
],
),
);
},
),
),
),
);

View File

@@ -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,7 +19,8 @@ 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; // 保持页面状态
@@ -28,16 +32,24 @@ class _CustomMainContainerState extends State<CustomMainContainer> with Automati
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();
}

View File

@@ -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:

View File

@@ -1,24 +1,104 @@
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 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,
),
);
_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),
@@ -28,24 +108,55 @@ class CustomBottomNavBar extends StatelessWidget {
],
),
child: SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
child: Stack(
alignment: Alignment.bottomCenter,
children: [
// 底层:Tab 列表
Row(
children: List.generate(enabledTabs.length, (index) {
final tab = enabledTabs[index];
final isSelected = index == state.selectedIndex;
final isSelected = index == selectedIndex;
return _buildNavItem(
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(
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:

View File

@@ -0,0 +1,4 @@
// 数据层聚合导出
export 'datasources/datasources.dart';
export 'models/models.dart';
export 'repositories/repositories.dart';

View File

@@ -0,0 +1,2 @@
// 数据源层
export 'device_remote_datasource.dart';

View File

@@ -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;
}
}

View File

@@ -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,
);
}
}

View File

@@ -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,
);
}
}

View File

@@ -0,0 +1,3 @@
// 数据模型层
export 'device_data_model.dart';
export 'device_status_data_model.dart';

View File

@@ -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();
}
}

View File

@@ -0,0 +1,2 @@
// 仓储实现
export 'device_repository_impl.dart';

View File

@@ -0,0 +1,6 @@
// 设备状态模块 - v2 版本
// 清洁架构实现
export 'presentation/pages/device_status_page.dart';
export 'presentation/bloc/bloc.dart';
export 'presentation/widgets/widgets.dart';

View File

@@ -0,0 +1,4 @@
// 领域层聚合导出
export 'entities/entities.dart';
export 'repositories/device_repository.dart';
export 'usecases/usecases.dart';

View File

@@ -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,
});
}

View File

@@ -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,
});
}

View File

@@ -0,0 +1,3 @@
// 实体层
export 'device_entity.dart';
export 'device_status_entity.dart';

View File

@@ -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});
}

View File

@@ -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});
}

View File

@@ -0,0 +1,2 @@
// 用例层
export 'get_device_status_data_usecase.dart';

View File

@@ -0,0 +1,4 @@
// BLoC 层
export 'device_status_bloc.dart';
export 'device_status_event.dart';
export 'device_status_state.dart';

View File

@@ -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,
));
}
}
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

@@ -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,
),
),
),
),
],
),
);
}
}

View File

@@ -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),
),
),
],
),
],
),
);
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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),
),
),
],
),
);
}
}

View File

@@ -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),
),
],
),
);
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,4 @@
// 表现层聚合导出
export 'bloc/bloc.dart';
export 'pages/device_status_page.dart';
export 'widgets/widgets.dart';

View File

@@ -0,0 +1,2 @@
// Providers 层(用于依赖注入)
export '../bloc/bloc.dart';

View File

@@ -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,
),
),
],
),
),
);
}
}

View File

@@ -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),
),
),
],
);
}
}

View File

@@ -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),
),
),
],
),
],
);
}
}

View File

@@ -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,
),
),
],
);
}
}

View File

@@ -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,
),
),
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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),
),
),
),
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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';
}
}
}

View File

@@ -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,
),
),
],
),
],
),
);
}
}

View File

@@ -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,
),
),
),
],
),
],
),
);
}
}

View File

@@ -0,0 +1,3 @@
// Widgets 层
export 'device_bottom_nav_bar.dart';
export 'device_item_widget.dart';

View File

@@ -58,6 +58,7 @@ class HomeRemoteDataSourceImpl implements HomeRemoteDataSource {
plantOverview: const PlantOverview(
installedCapacity: 1.26,
commissionDate: '2024-05-18',
backgroundImage: 'assets/images/dianzhan.png',
),
);
}

View File

@@ -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,32 +68,52 @@ 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(
Expanded(
child: InkWell(
onTap: _showPlantSelector,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'示例光伏电站',
style: TextStyle(
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 Spacer(),
],
),
),
),
IconButton(
icon: const Icon(Icons.sentiment_satisfied_alt_outlined, size: 24),
onPressed: () => print('点击表情'),
icon: const Icon(Icons.qr_code_scanner, size: 24, color: Color(0xFF1D2129)),
onPressed: () => print('点击扫码'),
),
],
),
const SizedBox(height: 8),
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 40),
sliver: SliverList(
delegate: SliverChildListDelegate([
const SizedBox(height: 16),
// 天气和告警
Row(
children: [
Expanded(
@@ -149,31 +169,23 @@ class _HomeV2PageState extends State<HomeV2Page> {
),
],
),
],
),
),
),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
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);
},
)),
],
),
);
},
);
}
}

View File

@@ -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,
),

View File

@@ -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,

View File

@@ -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,55 +94,30 @@ class QuickEntryCard extends StatelessWidget {
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildQuickEntryItem(
icon: Icons.upload_file,
label: '设备上报',
),
_buildQuickEntryItem(
icon: Icons.qr_code_scanner,
label: '扫一扫',
),
_buildQuickEntryItem(
icon: Icons.assignment,
label: '工单任务',
),
_buildQuickEntryItem(
icon: Icons.notifications_active,
label: '告警中心',
),
],
),
),
],
);
}
Widget _buildQuickEntryItem({
required IconData icon,
required String label,
}) {
return InkWell(
onTap: () => print('点击:$label'),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
color: const Color(0xFF165DFF),
size: 32,
size: 26,
),
const SizedBox(height: 8),
const SizedBox(height: 6),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 14,
fontSize: 12,
height: 1.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
);
}
}

View File

@@ -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,6 +60,7 @@ class StatsGrid extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showArrow)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -72,6 +77,14 @@ class StatsGrid extends StatelessWidget {
color: Color(0xFF86909C),
),
],
)
else
Text(
title,
style: const TextStyle(
color: Color(0xFF4E5969),
fontSize: 14,
),
),
const Spacer(),
Row(
@@ -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();
}
}

View File

@@ -30,41 +30,28 @@ 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),
),
],
),
child: Row(
Row(
children: [
Expanded(
child: _buildWorkOrderItem(
child: _buildWorkOrderCard(
title: '待处理',
count: stats.pending,
color: const Color(0xFFF53F3F),
bgColor: Colors.white,
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
Expanded(
child: _buildWorkOrderItem(
child: _buildWorkOrderCard(
title: '处理中',
count: stats.processing,
color: const Color(0xFFFF7D00),
bgColor: const Color(0xFFFFF7E6),
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
Expanded(
child: _buildWorkOrderItem(
child: _buildWorkOrderCard(
title: '待验收',
count: stats.pendingAcceptance,
color: const Color(0xFF1D2129),
@@ -73,22 +60,33 @@ class WorkOrderCard extends StatelessWidget {
),
],
),
),
],
);
}
Widget _buildWorkOrderItem({
Widget _buildWorkOrderCard({
required String title,
required int count,
required Color color,
required Color bgColor,
}) {
return Container(
padding: const EdgeInsets.all(8),
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(8),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -100,10 +98,10 @@ class WorkOrderCard extends StatelessWidget {
title,
style: TextStyle(
color: color,
fontSize: 14,
fontSize: 13,
),
),
const SizedBox(height: 4),
const SizedBox(height: 8),
Text(
count.toString(),
style: TextStyle(
@@ -116,11 +114,12 @@ class WorkOrderCard extends StatelessWidget {
),
Icon(
Icons.arrow_forward_ios,
size: 16,
size: 14,
color: color.withOpacity(0.5),
),
],
),
),
);
}
}

View 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
```

View File

@@ -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;
}
}

View File

@@ -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();
}

View 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];
}

View 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'));
}
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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();
}

View 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();
}
}

View File

@@ -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();
}
}

View 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 ToggleNightModeUseCase {
ToggleNightModeUseCase(this._repository);
final MyRepository _repository;
Future<Either<Failure, bool>> call(bool enabled) async {
return await _repository.toggleNightMode(enabled);
}
}

View 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';

View 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;
}

View 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();
}
}

View 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('确定'),
),
],
),
);
}
}

View 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('确定'),
),
],
),
);
}
}

View 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(),
),
];
}

View 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];
}

View 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;
}
}
}

View 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,
),
),
],
),
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View 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. 添加草稿保存功能

View 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` 文件即可。

View File

@@ -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);
}

View File

@@ -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('提交失败,请重试'));
}
}

View 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