# 智慧酒店 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

> **注意**: 当前阶段 View 层已完成，但 Cubit 中数据为 mock 硬编码。对接真实 API 时需要补充 Service 和 Repository 层，详见 [对接方案.md](./对接方案.md)。

---

## 3. 如何新增一个页面（完整分层流程）

以新增「运维日志」页面为例，演示 **View → Cubit → Service → Repository → API** 的完整开发流程。

### 3.1 第一步：定义 BO 模型

创建 `lib/models/bo/operation_log_bo.dart`:

```dart
import 'package:equatable/equatable.dart';

class OperationLogBO extends Equatable {
  final String id;
  final String title;
  final String time;

  const OperationLogBO({
    required this.id,
    required this.title,
    required this.time,
  });

  factory OperationLogBO.fromJson(Map<String, dynamic> json) {
    return OperationLogBO(
      id: json['id'] as String,
      title: json['title'] as String,
      time: json['time'] as String,
    );
  }

  @override
  List<Object?> get props => [id, title, time];
}
```

### 3.2 第二步：新建 Repository

创建 `lib/repositories/operation_log_repository.dart`:

```dart
import '../http/response_model.dart';
import '../http/dio_request.dart';
import '../models/bo/operation_log_bo.dart';

class OperationLogRepository {
  Future<ResponseModel<List<OperationLogBO>>> getLogs() {
    return DioRequest.instance.get<List<OperationLogBO>>(
      '/api/operation-logs',
      fromJsonT: (data) {
        final list = data as List<dynamic>;
        return list
            .map((e) => OperationLogBO.fromJson(e as Map<String, dynamic>))
            .toList();
      },
    );
  }
}
```

### 3.3 第三步：新建 Service

创建 `lib/services/operation_log_service.dart`:

```dart
import '../repositories/operation_log_repository.dart';
import '../models/bo/operation_log_bo.dart';

class OperationLogService {
  final OperationLogRepository _repository;

  OperationLogService({required OperationLogRepository repository})
      : _repository = repository;

  Future<List<OperationLogBO>> getLogs() async {
    final result = await _repository.getLogs();
    if (result.success && result.data != null) {
      return result.data!;
    }
    throw Exception(result.msg);
  }
}
```

### 3.4 第四步：定义 State

创建 `lib/views/home/operation_log/cubit/operation_log_state.dart`:

```dart
import 'package:equatable/equatable.dart';
import 'package:smart_hotel_app/models/bo/operation_log_bo.dart';

class OperationLogState extends Equatable {
  final List<OperationLogBO> logs;
  final bool isLoading;
  final String? error;

  const OperationLogState({
    this.logs = const [],
    this.isLoading = false,
    this.error,
  });

  OperationLogState copyWith({
    List<OperationLogBO>? logs,
    bool? isLoading,
    String? error,
  }) {
    return OperationLogState(
      logs: logs ?? this.logs,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }

  @override
  List<Object?> get props => [logs, isLoading, error];
}
```

### 3.5 第五步：定义 Cubit（调用 Service）

创建 `lib/views/home/operation_log/cubit/operation_log_cubit.dart`:

```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:smart_hotel_app/services/operation_log_service.dart';
import 'operation_log_state.dart';

class OperationLogCubit extends Cubit<OperationLogState> {
  final OperationLogService _service;

  OperationLogCubit({required OperationLogService service})
      : _service = service,
        super(const OperationLogState()) {
    loadData();
  }

  Future<void> loadData() async {
    emit(state.copyWith(isLoading: true));
    try {
      final logs = await _service.getLogs();
      emit(state.copyWith(isLoading: false, logs: logs));
    } catch (e) {
      emit(state.copyWith(isLoading: false, error: e.toString()));
    }
  }
}
```

### 3.6 第六步：创建 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';
import 'package:smart_hotel_app/services/operation_log_service.dart';
import 'package:smart_hotel_app/repositories/operation_log_repository.dart';

@RoutePage()
class OperationLogView extends StatelessWidget {
  const OperationLogView({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => OperationLogCubit(
        service: OperationLogService(
          repository: OperationLogRepository(),
        ),
      ),
      child: Scaffold(
        appBar: AppBar(title: const Text('运维日志')),
        body: BlocBuilder<OperationLogCubit, OperationLogState>(
          builder: (context, state) {
            if (state.isLoading) {
              return const Center(child: CircularProgressIndicator());
            }
            if (state.error != null) {
              return Center(child: Text('错误: ${state.error}'));
            }
            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.7 第七步：注册路由

在 [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.8 第八步：页面跳转

```dart
// 在任意页面中导航
context.pushRoute(const OperationLogRoute());
```

---

## 4. 如何发起网络请求（完整流程）

### 4.1 数据流向总览

```
View (UI)  →  Cubit (状态管理)  →  Service (业务逻辑)  →  Repository (API调用)  →  DioRequest (网络层)
```

### 4.2 新增 Repository

在 `lib/repositories/` 下新建文件，使用 `fromJsonT` 参数传入反序列化函数：

```dart
import '../http/response_model.dart';
import '../http/dio_request.dart';
import '../models/bo/device_bo.dart';

class DeviceRepository {
  /// 获取设备列表
  Future<ResponseModel<List<DeviceBO>>> getDeviceList() {
    return DioRequest.instance.get<List<DeviceBO>>(
      '/api/devices',
      fromJsonT: (data) {
        final list = data as List<dynamic>;
        return list
            .map((e) => DeviceBO.fromJson(e as Map<String, dynamic>))
            .toList();
      },
    );
  }

  /// 获取设备详情
  Future<ResponseModel<DeviceBO>> getDeviceDetail(String id) {
    return DioRequest.instance.get<DeviceBO>(
      '/api/devices/$id',
      fromJsonT: (data) => DeviceBO.fromJson(data as Map<String, dynamic>),
    );
  }

  /// 发送设备控制指令
  Future<ResponseModel> controlDevice(String id, Map<String, dynamic> params) {
    return DioRequest.instance.post('/api/devices/$id/control', data: params);
  }
}
```

#### 4.2.1 POST 请求示例（简单到复杂）

**示例 1：简单 POST，无请求体，无响应解析**

```dart
/// 退出登录 - 最简单的 POST
Future<ResponseModel> logout() {
  return DioRequest.instance.post('/api/logout');
}
```

**示例 2：POST 带简单请求体，不解析响应**

```dart
/// 登录 - 表单提交
Future<ResponseModel> login(String username, String password) {
  return DioRequest.instance.post('/api/login', data: {
    'username': username,
    'password': password,
  });
}
```

**示例 3：POST 带路径参数，不解析响应**

```dart
/// 切换规则开关
Future<ResponseModel> toggleRule(String ruleId, bool enabled) {
  return DioRequest.instance.post('/api/rules/$ruleId/toggle', data: {
    'enabled': enabled,
  });
}
```

**示例 4：POST 带路径参数 + 查询参数，不解析响应**

```dart
/// 提交巡检结果（带查询参数如 ?inspectionId=xxx）
Future<ResponseModel> submitInspectionResult(
  String deviceId, {
  required String inspectionId,
  required Map<String, dynamic> result,
}) {
  return DioRequest.instance.post(
    '/api/inspections/devices/$deviceId/submit',
    data: result,
    queryParameters: {'inspectionId': inspectionId},
  );
}
```

**示例 5：POST 带复杂请求体，解析响应数据**

```dart
/// 批量设备控制 - 返回操作结果列表
Future<ResponseModel<List<ControlResultBO>>> batchControlDevices(
  List<Map<String, dynamic>> commands,
) {
  return DioRequest.instance.post<List<ControlResultBO>>(
    '/api/devices/batch-control',
    data: {
      'commands': commands,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
    },
    fromJsonT: (data) {
      final list = data as List<dynamic>;
      return list
          .map((e) => ControlResultBO.fromJson(e as Map<String, dynamic>))
          .toList();
    },
  );
}
```

**示例 6：POST 带可选参数，解析单个响应对象**

```dart
/// 创建巡检任务 - 部分字段可选
Future<ResponseModel<InspectionTaskBO>> createInspectionTask({
  required String name,
  required List<String> deviceIds,
  String? assignee,
  DateTime? scheduledTime,
  String? remark,
}) {
  final body = <String, dynamic>{
    'name': name,
    'deviceIds': deviceIds,
  };
  if (assignee != null) body['assignee'] = assignee;
  if (scheduledTime != null) body['scheduledTime'] = scheduledTime.toIso8601String();
  if (remark != null) body['remark'] = remark;

  return DioRequest.instance.post<InspectionTaskBO>(
    '/api/inspections',
    data: body,
    fromJsonT: (data) => InspectionTaskBO.fromJson(data as Map<String, dynamic>),
  );
}
```

**示例 7：PUT 请求（更新资源）**

```dart
/// 更新房间设备状态
Future<ResponseModel<RoomBO>> updateRoomDevice(
  String roomNumber,
  String deviceName,
  Map<String, dynamic> settings,
) {
  return DioRequest.instance.put<RoomBO>(
    '/api/rooms/$roomNumber/devices/$deviceName',
    data: settings,
    fromJsonT: (data) => RoomBO.fromJson(data as Map<String, dynamic>),
  );
}
```

**示例 8：DELETE 请求**

```dart
/// 删除巡检记录
Future<ResponseModel> deleteInspectionRecord(String recordId) {
  return DioRequest.instance.delete('/api/inspections/records/$recordId');
}
```

### 4.3 新增 Service（业务逻辑封装）

在 `lib/services/` 下新建文件：

```dart
import '../repositories/device_repository.dart';
import '../models/bo/device_bo.dart';

class DeviceService {
  final DeviceRepository _repository;

  DeviceService({required DeviceRepository repository})
      : _repository = repository;

  Future<List<DeviceBO>> getDevices() async {
    final result = await _repository.getDeviceList();
    if (result.success && result.data != null) {
      return result.data!;
    }
    throw Exception(result.msg);
  }

  Future<DeviceBO> getDeviceDetail(String id) async {
    final result = await _repository.getDeviceDetail(id);
    if (result.success && result.data != null) {
      return result.data!;
    }
    throw Exception(result.msg);
  }
}
```

### 4.4 在 Cubit 中调用 Service

```dart
class DeviceCubit extends Cubit<DeviceState> {
  final DeviceService _service;

  DeviceCubit({required DeviceService service})
      : _service = service,
        super(const DeviceState()) {
    loadDevices();
  }

  Future<void> loadDevices() async {
    emit(state.copyWith(isLoading: true));
    try {
      final devices = await _service.getDevices();
      emit(state.copyWith(isLoading: false, devices: devices));
    } catch (e) {
      emit(state.copyWith(isLoading: false, error: e.toString()));
    }
  }
}
```

### 4.5 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/` 下的独立组件，通过参数接收数据