Commit b95e2acc authored by 张宏's avatar 张宏

ui调整

parent 5bd72d8e
...@@ -4,21 +4,21 @@ import 'package:smart_hotel_app/views/home/inspection/cubit/inspection_state.dar ...@@ -4,21 +4,21 @@ import 'package:smart_hotel_app/views/home/inspection/cubit/inspection_state.dar
class InspectionCubit extends Cubit<InspectionState> { class InspectionCubit extends Cubit<InspectionState> {
static const List<String> allRooms = [ static const List<String> allRooms = [
'客厅', '101',
'主卧', '102',
'玄关', '103',
'厨房', '104',
'书房', '105',
'阳台', '106',
]; ];
static final List<InspectionDevice> allDevices = [ static final List<InspectionDevice> allDevices = [
InspectionDevice( InspectionDevice(
id: '1', id: '1',
icon: Icons.wifi, icon: Icons.wifi,
name: '客厅主控网关', name: '101主控网关',
type: '智能网关', type: '智能网关',
location: '客厅电视柜', location: '101电视柜',
status: '正常', status: '正常',
time: '2024-01-15 14:30:00', time: '2024-01-15 14:30:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -26,9 +26,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -26,9 +26,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '2', id: '2',
icon: Icons.thermostat, icon: Icons.thermostat,
name: '温湿度传感器-客厅', name: '温湿度传感器-101',
type: '传感器', type: '传感器',
location: '客厅角落', location: '101角落',
status: '正常', status: '正常',
time: '2024-01-15 14:25:00', time: '2024-01-15 14:25:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -36,9 +36,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -36,9 +36,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '3', id: '3',
icon: Icons.videocam, icon: Icons.videocam,
name: '智能摄像头-门口', name: '智能摄像头-102',
type: '摄像头', type: '摄像头',
location: '入户门口', location: '102门口',
status: '告警', status: '告警',
time: '2024-01-15 10:00:00', time: '2024-01-15 10:00:00',
statusColor: const Color.fromRGBO(255, 77, 79, 1.0), statusColor: const Color.fromRGBO(255, 77, 79, 1.0),
...@@ -46,9 +46,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -46,9 +46,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '4', id: '4',
icon: Icons.light, icon: Icons.light,
name: '智能吸顶灯-主卧', name: '智能吸顶灯-102',
type: '传感器', type: '传感器',
location: '主卧天花板', location: '102天花板',
status: '正常', status: '正常',
time: '2024-01-15 14:25:00', time: '2024-01-15 14:25:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -56,9 +56,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -56,9 +56,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '5', id: '5',
icon: Icons.thermostat, icon: Icons.thermostat,
name: '温湿度传感器-主卧', name: '温湿度传感器-103',
type: '传感器', type: '传感器',
location: '主卧角落', location: '103角落',
status: '正常', status: '正常',
time: '2024-01-15 14:25:00', time: '2024-01-15 14:25:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -66,9 +66,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -66,9 +66,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '6', id: '6',
icon: Icons.light, icon: Icons.light,
name: '玄关灯-智能', name: '103灯-智能',
type: '智能灯', type: '智能灯',
location: '玄关', location: '103',
status: '开启', status: '开启',
time: '2024-01-15 14:20:00', time: '2024-01-15 14:20:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -76,9 +76,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -76,9 +76,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '7', id: '7',
icon: Icons.sensor_door, icon: Icons.sensor_door,
name: '门窗传感器-阳台', name: '门窗传感器-104',
type: '传感器', type: '传感器',
location: '阳台门', location: '104门',
status: '离线', status: '离线',
time: '2024-01-15 12:00:00', time: '2024-01-15 12:00:00',
statusColor: const Color.fromRGBO(102, 102, 102, 1.0), statusColor: const Color.fromRGBO(102, 102, 102, 1.0),
...@@ -86,9 +86,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -86,9 +86,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '8', id: '8',
icon: Icons.kitchen, icon: Icons.kitchen,
name: '智能插座-厨房', name: '智能插座-105',
type: '插座', type: '插座',
location: '厨房台面', location: '105台面',
status: '关闭', status: '关闭',
time: '2024-01-15 14:15:00', time: '2024-01-15 14:15:00',
statusColor: const Color.fromRGBO(100, 116, 139, 1.0), statusColor: const Color.fromRGBO(100, 116, 139, 1.0),
...@@ -96,9 +96,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -96,9 +96,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '9', id: '9',
icon: Icons.smoke_free, icon: Icons.smoke_free,
name: '烟雾报警器-厨房', name: '烟雾报警器-105',
type: '报警器', type: '报警器',
location: '厨房天花板', location: '105天花板',
status: '正常', status: '正常',
time: '2024-01-15 14:30:00', time: '2024-01-15 14:30:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
...@@ -106,9 +106,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -106,9 +106,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '10', id: '10',
icon: Icons.desk, icon: Icons.desk,
name: '智能台灯-书房', name: '智能台灯-106',
type: '智能灯', type: '智能灯',
location: '书房书桌', location: '106书桌',
status: '关闭', status: '关闭',
time: '2024-01-15 14:10:00', time: '2024-01-15 14:10:00',
statusColor: const Color.fromRGBO(100, 116, 139, 1.0), statusColor: const Color.fromRGBO(100, 116, 139, 1.0),
...@@ -116,9 +116,9 @@ class InspectionCubit extends Cubit<InspectionState> { ...@@ -116,9 +116,9 @@ class InspectionCubit extends Cubit<InspectionState> {
InspectionDevice( InspectionDevice(
id: '11', id: '11',
icon: Icons.ac_unit, icon: Icons.ac_unit,
name: '智能空调-客厅', name: '智能空调-101',
type: '空调', type: '空调',
location: '客厅', location: '101',
status: '正常', status: '正常',
time: '2024-01-15 14:30:00', time: '2024-01-15 14:30:00',
statusColor: const Color.fromRGBO(26, 188, 156, 1.0), statusColor: const Color.fromRGBO(26, 188, 156, 1.0),
......
...@@ -33,7 +33,7 @@ class InspectionState extends Equatable { ...@@ -33,7 +33,7 @@ class InspectionState extends Equatable {
const InspectionState({ const InspectionState({
this.selectedTab = DeviceStatusTab.total, this.selectedTab = DeviceStatusTab.total,
this.selectedRooms = const ['客厅'], this.selectedRooms = const ['101'],
this.deviceList = const [], this.deviceList = const [],
this.tabCounts = const { this.tabCounts = const {
DeviceStatusTab.total: 11, DeviceStatusTab.total: 11,
......
...@@ -19,12 +19,12 @@ class FilterPanel extends StatelessWidget { ...@@ -19,12 +19,12 @@ class FilterPanel extends StatelessWidget {
}); });
static const List<String> allRooms = [ static const List<String> allRooms = [
'客厅', '101',
'主卧', '102',
'玄关', '103',
'厨房', '104',
'书房', '105',
'阳台', '106',
]; ];
@override @override
......
...@@ -83,6 +83,36 @@ class InspectionDeviceView extends StatelessWidget { ...@@ -83,6 +83,36 @@ class InspectionDeviceView extends StatelessWidget {
), ),
SizedBox(height: 30.h), SizedBox(height: 30.h),
StartInspectionButton(deviceId: state.deviceId), StartInspectionButton(deviceId: state.deviceId),
SizedBox(height: 20.h),
GestureDetector(
onTap: () {
context.pushRoute(
InspectionHistoryRoute(deviceId: state.deviceId),
);
},
child: Container(
width: double.infinity,
height: 88.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.r),
border: Border.all(
color: const Color.fromRGBO(66, 165, 245, 1.0),
width: 2.w,
),
),
child: Center(
child: Text(
'巡检记录',
style: TextStyle(
color: const Color.fromRGBO(66, 165, 245, 1.0),
fontSize: 32.sp,
fontWeight: FontWeight.bold,
),
),
),
),
),
SizedBox(height: 30.h), SizedBox(height: 30.h),
], ],
); );
......
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:smart_hotel_app/routes/app_router.gr.dart'; import 'package:smart_hotel_app/routes/app_router.gr.dart';
class StartInspectionButton extends StatelessWidget { class StartInspectionButton extends StatelessWidget {
...@@ -11,10 +12,45 @@ class StartInspectionButton extends StatelessWidget { ...@@ -11,10 +12,45 @@ class StartInspectionButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
context.pushRoute( showDialog(
InspectionHistoryRoute(deviceId: deviceId), context: context,
barrierDismissible: false,
builder: (context) => Center(
child: Container(
width: 140.w,
height: 140.h,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
LoadingAnimationWidget.waveDots(
color: const Color.fromRGBO(66, 165, 245, 1.0),
size: 50,
),
SizedBox(height: 12.h),
Text(
'巡检中...',
style: TextStyle(
color: const Color.fromRGBO(100, 116, 139, 1.0),
fontSize: 24.sp,
),
),
],
),
),
),
); );
await Future.delayed(const Duration(seconds: 3));
if (context.mounted) {
context.popRoute();
context.pushRoute(
InspectionHistoryRoute(deviceId: deviceId),
);
}
}, },
child: Container( child: Container(
width: double.infinity, width: double.infinity,
......
...@@ -29,8 +29,8 @@ class ProfileMenuBlock extends StatelessWidget { ...@@ -29,8 +29,8 @@ class ProfileMenuBlock extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 96.w, width: 68.w,
height: 96.h, height: 68.h,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color.fromRGBO(229, 241, 255, 1), color: const Color.fromRGBO(229, 241, 255, 1),
borderRadius: BorderRadius.circular(28.r), borderRadius: BorderRadius.circular(28.r),
...@@ -52,11 +52,12 @@ class ProfileMenuBlock extends StatelessWidget { ...@@ -52,11 +52,12 @@ class ProfileMenuBlock extends StatelessWidget {
), ),
), ),
), ),
Icon( if (item.title == '告警信息' || item.title == '设备列表')
Icons.chevron_right, Icon(Icons.chevron_right,
color: const Color.fromRGBO(100, 116, 139, 1.0), color: const Color.fromRGBO(100, 116, 139, 1.0),
size: 32.sp, size: 32.sp,
), ),
], ],
), ),
), ),
...@@ -65,6 +66,9 @@ class ProfileMenuBlock extends StatelessWidget { ...@@ -65,6 +66,9 @@ class ProfileMenuBlock extends StatelessWidget {
void _handleMenuTap(BuildContext context, String title) { void _handleMenuTap(BuildContext context, String title) {
switch (title) { switch (title) {
case '设备列表':
context.pushRoute(const InspectionDetailRoute());
break;
case '告警信息': case '告警信息':
context.pushRoute(const AbnormalListRoute()); context.pushRoute(const AbnormalListRoute());
break; break;
......
# 智慧酒店 App 上手帮助
# 智慧酒店 App 上手帮助
## 1. 环境搭建
### 1.1 前置条件
```bash
# 确认 Flutter 版本 (需要 3.2.5+)
flutter --version
# 确认 Android SDK / Xcode 已安装
flutter doctor
```
### 1.2 项目初始化
```bash
cd smart_hotel_app
# 安装依赖
flutter pub get
# 生成路由代码(修改路由配置后需要重新执行)
flutter pub run build_runner build --delete-conflicting-outputs
```
### 1.3 运行项目
```bash
# 连接设备后运行
flutter run
# 指定设备
flutter run -d <device_id>
```
---
## 2. 项目架构速览
```
lib/
├── main.dart ← 入口,全局依赖初始化
├── blocs/auth/ ← 全局认证状态(BLoC 模式)
├── http/ ← 网络层:Dio封装 + 拦截器
├── models/bo/ ← 数据模型
├── repositories/ ← API 调用层
├── routes/ ← 路由配置(auto_route)
├── services/ ← 业务逻辑层
├── utils/ ← 工具:常量、事件总线、存储、日志
└── views/ ← 页面,每个页面 = cubit/ + widget/ + view.dart
├── layout/ ← 底部Tab框架
├── login/ ← 登录
├── home/ ← 告警Tab
├── service/ ← 管理Tab
├── report/ ← 能耗Tab
├── profile/ ← 我的Tab
└── search/ ← 搜索
```
**分层关系**: View → Cubit/BLoC → Service → Repository → DioRequest → API
---
## 3. 如何新增一个页面
以新增「运维日志」页面为例,演示完整开发流程。
### 3.1 第一步:新建文件夹结构
```bash
mkdir -p lib/views/home/operation_log/cubit
mkdir -p lib/views/home/operation_log/widget
```
### 3.2 第二步:定义 State
创建 `lib/views/home/operation_log/cubit/operation_log_state.dart`:
```dart
import 'package:equatable/equatable.dart';
class OperationLogState extends Equatable {
final List<LogItem> logs;
final bool isLoading;
final String? error;
const OperationLogState({
this.logs = const [],
this.isLoading = false,
this.error,
});
OperationLogState copyWith({
List<LogItem>? logs,
bool? isLoading,
String? error,
}) {
return OperationLogState(
logs: logs ?? this.logs,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
@override
List<Object?> get props => [logs, isLoading, error];
}
class LogItem extends Equatable {
final String id;
final String title;
final String time;
const LogItem({required this.id, required this.title, required this.time});
@override
List<Object?> get props => [id, title, time];
}
```
### 3.3 第三步:定义 Cubit
创建 `lib/views/home/operation_log/cubit/operation_log_cubit.dart`:
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'operation_log_state.dart';
class OperationLogCubit extends Cubit<OperationLogState> {
OperationLogCubit() : super(const OperationLogState()) {
loadData();
}
Future<void> loadData() async {
emit(state.copyWith(isLoading: true));
try {
// TODO: 调用 API 获取数据
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(
isLoading: false,
logs: [
const LogItem(id: '1', title: '设备巡检完成', time: '2026-06-05 10:00'),
],
));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
}
```
### 3.4 第四步:创建 View
创建 `lib/views/home/operation_log/operation_log_view.dart`:
```dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'cubit/operation_log_cubit.dart';
import 'cubit/operation_log_state.dart';
@RoutePage()
class OperationLogView extends StatelessWidget {
const OperationLogView({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => OperationLogCubit(),
child: Scaffold(
appBar: AppBar(title: const Text('运维日志')),
body: BlocBuilder<OperationLogCubit, OperationLogState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: state.logs.length,
itemBuilder: (context, index) {
final log = state.logs[index];
return ListTile(
title: Text(log.title),
subtitle: Text(log.time),
);
},
);
},
),
),
);
}
}
```
### 3.5 第五步:注册路由
[app_router.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/routes/app_router.dart) 中添加:
```dart
AutoRoute(page: OperationLogRoute.page),
```
然后运行代码生成:
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```
### 3.6 第六步:页面跳转
```dart
// 在任意页面中导航
context.pushRoute(const OperationLogRoute());
```
---
## 4. 如何发起网络请求
### 4.1 新增 Repository
`lib/repositories/` 下新建文件:
```dart
import '../http/response_model.dart';
import '../http/dio_request.dart';
class DeviceRepository {
Future<ResponseModel> getDeviceList() {
return DioRequest.instance.get('/api/devices');
}
Future<ResponseModel> getDeviceDetail(String id) {
return DioRequest.instance.get('/api/devices/$id');
}
}
```
### 4.2 在 Cubit 中调用
```dart
class DeviceCubit extends Cubit<DeviceState> {
final DeviceRepository _repository = DeviceRepository();
Future<void> loadDevices() async {
emit(state.copyWith(isLoading: true));
try {
final result = await _repository.getDeviceList();
if (result.success) {
// 处理 result.data
}
} catch (e) {
emit(state.copyWith(error: e.toString()));
}
}
}
```
### 4.3 API 响应格式约定
后端接口返回标准 JSON 格式:
```json
{
"code": 200,
"msg": "success",
"data": { ... }
}
```
`ResponseModel.fromJson` 会自动解析,`fromJsonT` 参数可传入自定义反序列化函数。
---
## 5. 全局状态访问
### 5.1 读取 AuthBloc 状态
```dart
final authState = context.read<AuthBloc>().state;
if (authState is AuthSuccess) {
final username = authState.userInfo.username;
}
```
### 5.2 监听 Token 过期
```dart
// Token 过期时自动弹出登录页,无需手动处理
// 网络拦截器检测到 401 时发出 TokenExpiredEvent
// main.dart 中已订阅该事件,自动跳转登录页
```
---
## 6. 本地存储使用
```dart
final storage = StorageService();
// Token 操作
await storage.saveToken('your_token');
final token = await storage.getToken();
final isExpired = await storage.isTokenExpired();
// 记住密码
await storage.saveRememberCredentials(username: 'admin', password: '123456');
final (username, password, enabled) = await storage.getRememberCredentials();
await storage.clearRememberCredentials();
```
---
## 7. 屏幕适配
项目使用 `flutter_screenutil`,设计稿基准为常规移动端尺寸。
```dart
// 使用 .w / .h / .sp / .r 进行适配
Container(
width: 100.w, // 根据屏幕宽度等比缩放
height: 50.h, // 根据屏幕高度等比缩放
padding: EdgeInsets.all(28.w),
child: Text('标题', style: TextStyle(fontSize: 32.sp)),
)
```
---
## 8. 图表使用
使用 `fl_chart` 绘制图表,项目中已有示例:
| 文件 | 图表类型 | 示例组件 |
|------|---------|---------|
| `home/abnormal/widget/temperature_char_block.dart` | 折线图 | 温度曲线 |
| `home/device/widget/power_char_block.dart` | 柱状图 | 功率图表 |
| `report/index/widget/weekly_power_chart.dart` | 柱状图 | 周用电量 |
| `report/energy/widget/hourly_chart_block.dart` | 折线图 | 分时能耗 |
| `report/energy/widget/zone_chart_block.dart` | 饼图 | 区域能耗占比 |
新增图表可参考这些文件。
---
## 9. 常见问题
### Q: `build_runner` 报错怎么办?
```bash
# 先清理再重新生成
flutter pub run build_runner clean
flutter pub run build_runner build --delete-conflicting-outputs
```
### Q: 如何切换 API 环境地址?
修改 [constants.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/utils/constants.dart) 中的 `baseUrl`
### Q: 路由跳转后白屏?
检查是否执行了 `build_runner` 生成 `app_router.gr.dart`,以及 View 文件是否添加 `@RoutePage()`
### Q: 如何添加新的底部 Tab?
1.[app_router.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/routes/app_router.dart)`DefaultLayoutRoute``children` 中添加新路由
2.[default_view.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/views/layout/default_view.dart)`routes``BottomNavigationBar` 中添加对应项
3. 运行 `build_runner`
---
## 10. 代码规范提醒
- **命名约定**:
- 文件名: `snake_case.dart`
- 类名: `PascalCase`
- 变量/方法: `camelCase`
- 常量: `camelCase``SCREAMING_SNAKE_CASE`
- **每个页面模块结构**: 必须包含 `cubit/` + `widget/` + `view.dart`
- **State 不可变**: 始终使用 `copyWith` 更新状态,不要直接修改 state 字段
- **UI 与逻辑分离**: View 只负责 UI 渲染,业务逻辑放在 Cubit/Service 中
- **组件化**: 可复用的 UI 块抽成 `widget/` 下的独立组件,通过参数接收数据
\ No newline at end of file
# 智慧酒店 App 技术方案
# 智慧酒店 App 技术方案
## 1. 项目概述
**项目名称**: smart_hotel_app(智慧酒店管理 App)
**技术栈**: Flutter 3.x + Dart 3.2+
**项目定位**: 为酒店运维管理人员提供设备监控、巡检管理、能耗报告、服务管理等一站式移动端解决方案。
**核心功能模块**:
| 模块 | TAB 名称 | 功能描述 |
|------|----------|---------|
| 告警模块 | 告警 | 异常告警列表、告警详情(温度/电压异常) |
| 管理模块 | 管理 | 巡检任务管理、巡检拓扑图、设备详情、房间设备控制 |
| 能耗模块 | 能耗 | 能耗总览、设备能耗列表、房间能耗报告、区域能耗报告、规则管理 |
| 个人中心 | 我的 | 用户信息、菜单入口 |
---
## 2. 架构设计
### 2.1 整体架构
项目采用 **分层架构 + BLoC 状态管理** 模式,自上而下分为:
```
┌─────────────────────────────────────────────┐
│ Views(UI 层) │
│ 页面/视图组件(View) + 可复用组件(Widget) │
├─────────────────────────────────────────────┤
│ Cubit/BLoC(状态管理层) │
│ 业务状态管理 + 事件处理 │
├─────────────────────────────────────────────┤
│ Service(业务逻辑层) │
│ 业务逻辑封装、数据转换 │
├─────────────────────────────────────────────┤
│ Repository(数据仓库层) │
│ 数据源抽象,连接 HTTP 请求 │
├─────────────────────────────────────────────┤
│ HTTP(网络层) │
│ Dio 封装 + 拦截器链(请求/响应/错误) │
├─────────────────────────────────────────────┤
│ Models(数据模型层) │
│ BO/DTO 对象定义 │
└─────────────────────────────────────────────┘
```
### 2.2 依赖注入
项目未使用 DI 框架(如 get_it),而是采用 **手动注入** 方式。在 `main.dart``_MyAppState.initState()` 中集中创建核心单例,通过构造函数向下传递。
```
main.dart
└─ initState()
├─ StorageService() → 全局存储服务
├─ DioRequest.instance.init() → 网络请求初始化
├─ AuthRepository() → 认证仓库
├─ AuthService() → 认证业务服务
├─ AuthBloc() → 全局认证状态
└─ AppRouter() → 路由配置
```
---
## 3. 目录结构详解
```
lib/
├── main.dart # 应用入口,全局初始化
├── assets/ # 静态资源
│ ├── icon/ # 图标资源
│ ├── login/ # 登录页资源
│ └── tab/ # 底部导航栏图标
├── blocs/ # 全局 BLoC 状态管理
│ └── auth/ # 认证模块
│ ├── auth_bloc.dart # AuthBloc 业务逻辑
│ ├── auth_event.dart # AuthEvent 事件定义
│ └── auth_state.dart # AuthState 状态定义
├── http/ # 网络请求层
│ ├── dio_request.dart # Dio 单例封装(GET/POST/PUT/DELETE)
│ ├── response_model.dart # 统一响应模型 ResponseModel<T>
│ ├── exceptions/ # 异常定义
│ │ ├── app_exception.dart # 基类异常
│ │ ├── business_exception.dart
│ │ ├── cancel_exception.dart
│ │ ├── network_exception.dart
│ │ └── server_exception.dart
│ └── interceptors/ # Dio 拦截器
│ ├── request_interceptor.dart # 请求拦截(Token 注入、UUID)
│ ├── response_interceptor.dart # 响应拦截(统一处理)
│ └── error_interceptor.dart # 错误拦截(异常转换)
├── models/ # 数据模型
│ └── bo/ # 业务对象 Business Object
│ └── user_info_bo.dart # 用户信息
├── repositories/ # 数据仓库层
│ └── auth_repository.dart # 认证 API 调用
├── routes/ # 路由配置(auto_route)
│ ├── app_router.dart # 路由表定义
│ └── app_router.gr.dart # 自动生成的代码
├── services/ # 业务服务层
│ └── auth_service.dart # 认证业务逻辑
├── utils/ # 工具类
│ ├── constants.dart # 全局常量(BaseURL、超时、错误码)
│ ├── event_bus.dart # 全局事件总线
│ ├── logger.dart # 日志工具
│ └── storage/ # 本地存储
│ └── storage_service.dart # FlutterSecureStorage 封装
└── views/ # 视图层
├── layout/ # 布局框架
│ └── default_view.dart # 底部 Tab 框架(AutoTabsScaffold)
├── login/ # 登录模块
│ ├── login_view.dart
│ ├── cubit/
│ └── widget/
├── home/ # 告警 Tab(首页)
│ ├── index/ # 告警首页
│ ├── abnormal/ # 告警详情
│ ├── abnormal_list/ # 告警列表
│ ├── device/ # 设备详情
│ ├── inspection/ # 巡检详情
│ ├── inspection_device/ # 巡检设备
│ ├── inspection_history/ # 巡检历史
│ └── inspection_topology/ # 巡检拓扑
├── service/ # 管理 Tab
│ ├── index/ # 管理首页
│ ├── room/ # 房间详情
│ └── device/ # 设备控制
├── report/ # 能耗 Tab
│ ├── index/ # 能耗首页
│ ├── device/ # 设备能耗列表/详情
│ ├── energy/ # 区域能耗
│ ├── room/ # 房间能耗
│ └── rule/ # 规则管理
├── profile/ # 我的 Tab
│ └── index/ # 个人中心
└── search/ # 全局搜索
└── index_view.dart
```
---
## 4. 核心技术说明
### 4.1 状态管理:BLoC + Cubit
项目混合使用 **BLoC**(全局认证)和 **Cubit**(页面级状态)两种模式:
#### BLoC 使用场景:全局认证流程
| 文件 | 职责 |
|------|------|
| `auth_event.dart` | 定义事件:`AuthLoginRequested``AuthTokenExpiredEvent``AuthLogoutRequestedEvent` |
| `auth_state.dart` | 定义状态:`AuthInitial``AuthLoading``AuthSuccess``AuthFailure``AuthTokenExpired``AuthLoggedOut` |
| `auth_bloc.dart` | 事件处理:登录、Token过期、退出登录 |
`main.dart` 中通过 `BlocProvider` 注入全局 AuthBloc,监听 `AuthTokenExpired` 状态跳转登录页,监听 `AuthLoggedOut` 执行退出。
#### Cubit 使用场景:页面级状态
每个功能页面都有独立的 **Cubit + State** 组合:
```
views/{模块}/{页面}/
├── cubit/
│ ├── xxx_cubit.dart # 业务逻辑
│ └── xxx_state.dart # 数据状态 + copyWith
└── widget/ # 可复用组件
```
State 类使用 `Equatable` 便于比较,提供 `copyWith` 方法实现不可变更新。
**数据流向**:
```
Cubit.emit(state.copyWith(xxx: newValue))
→ BlocBuilder 监听变化
→ Widget rebuild
```
### 4.2 路由系统:auto_route
使用 `auto_route` 实现声明式路由。
**路由配置**[app_router.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/routes/app_router.dart)):
```
LoginRoute (initial)
→ DefaultLayoutRoute (AutoTabsScaffold)
├── HomeRoute (告警首页, initial tab)
├── ServiceIndexRoute (管理首页)
├── ReportIndexRoute (能耗首页)
└── ProfileRoute (我的)
→ SearchRoute (全局搜索)
→ AbnormalDetailRoute (告警详情)
→ DeviceDetailRoute (设备详情)
→ ... 其他二级页面
```
- 所有页面使用 `@RoutePage()` 注解标记
- 根路由为 `LoginRoute`,登录成功跳转到 `DefaultLayoutRoute`
- `DefaultLayoutRoute` 内嵌 4 个 Tab,通过 `AutoTabsScaffold` + `BottomNavigationBar` 实现
- 二级页面(详情)在 Tab 外层定义,支持全屏 push
### 4.3 网络请求层
#### DioRequest 单例
封装 Dio 实例,提供 `get/post/put/delete` 方法,返回 `ResponseModel<T>`
**配置项**[constants.dart](file:///Users/zh/Documents/work/zkrq/coding/smart_hotel_app/lib/utils/constants.dart)):
| 配置 | 值 | 说明 |
|------|-----|------|
| baseUrl | `http://localhost:8080` | API 基础地址 |
| connectTimeout | 30000ms | 连接超时 |
| receiveTimeout | 30000ms | 接收超时 |
| sendTimeout | 30000ms | 发送超时 |
#### 拦截器链
```
RequestInterceptor
→ 注入 Authorization Bearer Token
→ 注入 X-Request-UUID 追踪ID
→ 注入 X-Timestamp 时间戳
→ 输出请求日志
ResponseInterceptor
→ 响应日志输出
→ 响应数据预处理
ErrorInterceptor
→ DioException 转 AppException 子类
→ 业务异常 / 网络异常 / 服务器异常分离处理
```
#### ResponseModel
```dart
class ResponseModel<T> {
final int code; // 业务状态码
final String msg; // 提示信息
final T? data; // 泛型数据
final String uuid; // 请求唯一标识
final bool success; // code == 200
final int timestamp; // 时间戳
}
```
### 4.4 数据持久化:FlutterSecureStorage
StorageService 封装 `flutter_secure_storage`,提供:
| 功能 | 方法 |
|------|------|
| Token 存取 | `saveToken()` / `getToken()` / `deleteToken()` / `hasToken()` |
| Token 过期检查 | `isTokenExpired()` / `getTokenExpiry()` |
| 记住密码 | `saveRememberCredentials()` / `getRememberCredentials()` / `clearRememberCredentials()` |
Android 端启用 `encryptedSharedPreferences: true` 增强安全性。
### 4.5 事件总线
全局 `eventBus``StreamController.broadcast`)用于跨模块通信:
| 事件 | 用途 |
|------|------|
| `TokenExpiredEvent` | Token 过期通知,触发全局重新登录 |
| `LogoutEvent` | 退出登录通知 |
`main.dart` 中订阅 `TokenExpiredEvent`,触发 `AuthBloc``AuthTokenExpiredEvent`
---
## 5. 业务模块设计
### 5.1 认证流程
```
LoginView → 输入用户名密码 → AuthBloc(AuthLoginRequested)
→ AuthService.login()
→ AuthRepository.login() → POST /login
→ 成功: StorageService.saveToken() → AuthSuccess → 跳转首页
→ 失败: AuthFailure → 显示错误
Token 过期:
网络拦截器 (401) → ErrorInterceptor → eventBus(TokenExpiredEvent)
→ AuthBloc(AuthTokenExpiredEvent) → 清除 Token → 跳转登录页
```
### 5.2 页面状态管理模式
所有业务页面遵循统一模式:
```
1. 定义 State: extends Equatable, 提供 copyWith
2. 定义 Cubit: extends Cubit<State>, 构造函数初始化数据
3. View 使用 BlocProvider 创建 Cubit
4. UI 通过 BlocBuilder 监听 State 变化
5. 子组件通过接口参数接收数据(props down, events up)
```
### 5.3 子模块页面与路由对应
#### 告警模块(Home)
| 页面 | 路由 | 功能 |
|------|------|------|
| 告警首页 | `HomeRoute` | 统计概览、任务列表、温度告警、电压波动 |
| 告警详情 | `AbnormalDetailRoute` | 温度曲线、告警信息、处理操作 |
| 告警列表 | `AbnormalListRoute` | 告警筛选、状态过滤、列表展示 |
| 设备详情 | `DeviceDetailRoute` | 设备参数、功率曲线、状态信息 |
| 巡检详情 | `InspectionDetailRoute` | 巡检任务、设备列表、筛选面板 |
| 巡检设备 | `InspectionDeviceRoute` | 设备概览、巡检项目、巡检历史 |
| 巡检历史 | `InspectionHistoryRoute` | 巡检记录列表、统计卡片 |
| 巡检拓扑 | `InspectionTopologyRoute` | 网络拓扑图、状态图例 |
#### 管理模块(Service)
| 页面 | 路由 | 功能 |
|------|------|------|
| 管理首页 | `ServiceIndexRoute` | 服务统计、房间管理卡片 |
| 房间详情 | `ServiceRoomDetailRoute` | 房间信息、设备状态、设备控制 |
| 设备控制 | `ServiceDeviceDetailRoute` | 模式选择、温度控制、风速调节 |
#### 能耗模块(Report)
| 页面 | 路由 | 功能 |
|------|------|------|
| 能耗首页 | `ReportIndexRoute` | 设备在线率、能耗指标、周用电图、房间分布 |
| 设备能耗列表 | `ReportDeviceListRoute` | 设备卡片、筛选Tab |
| 设备能耗详情 | `ReportDeviceDetailRoute` | 单设备能耗详情 |
| 区域能耗 | `ReportEnergyDetailRoute` | 能耗统计、分时/分区图表 |
| 房间能耗 | `ReportRoomDetailRoute` | 楼层选择、房间网格、详情卡片 |
| 规则管理 | `ReportRuleManagementRoute` | 规则卡片管理 |
---
## 6. 依赖库清单
| 库 | 版本 | 用途 |
|-----|------|------|
| flutter_bloc | ^8.1.5 | BLoC/Cubit 状态管理 |
| equatable | ^2.0.5 | 值对象相等比较 |
| auto_route | ^7.9.2 | 声明式路由管理 |
| dio | ^5.4.0 | HTTP 网络请求 |
| flutter_secure_storage | ^9.0.0 | 安全本地存储 |
| flutter_screenutil | ^5.9.3 | 屏幕适配(设计稿基准) |
| fl_chart | ^0.71.0 | 图表绘制(折线/柱状/饼图) |
| flutter_switch | ^0.3.2 | 开关组件 |
| fluttertoast | ^8.2.5 | Toast 提示 |
| loading_animation_widget | ^1.3.0 | 加载动画 |
---
## 7. 开发环境要求
| 工具 | 版本要求 |
|------|---------|
| Flutter SDK | >= 3.2.5 |
| Dart SDK | >= 3.2.5 < 4.0.0 |
| Android Studio / VS Code | 最新稳定版 |
| Android minSdk | 21+ |
| iOS Deployment Target | 12.0+ |
---
## 8. 状态码约定
| 状态码 | 含义 |
|--------|------|
| 200 | 请求成功 |
| 401 | Token 过期/未授权 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
---
## 9. 关键设计决策
1. **Cubit 而非 BLoC**: 大多数页面业务逻辑简单,使用 Cubit 减少样板代码(无需定义 Event 类),仅在认证等复杂流程使用 BLoC。
2. **手动依赖注入**: 项目规模适中,未引入 get_it 等 DI 框架,依赖关系清晰可控。
3. **auto_route 代码生成**: 路由通过 `@RoutePage()` 注解 + `build_runner` 自动生成 `app_router.gr.dart`,减少手写路由模板代码。
4. **State copyWith 模式**: 所有 State 不可变,通过 `copyWith` 更新部分字段,保证状态变更可追踪。
5. **Token 自动注入**: `RequestInterceptor` 在每次请求时自动从 SecureStorage 读取 Token 并注入 Header。
6. **统一错误处理**: `ErrorInterceptor` 将 Dio 异常转换为项目内部异常类型,方便上层统一处理。
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment