2.优化修改了tcp重连机制,具体操作:断开重连时候不再建立新链接 而是将旧链接重置,并重新发送数据。 3.大面积的给项目中的各个页面 手动接入了多语言的支持。同时也调整了相应带来的问题英文长度较大影响原有的样式的问题,已解决了对应的样式不和谐问题。保证了外观的统一性。 4.给项目中的主要操作加了几个核心的操作比如远程遥控和路径规划添加了双层的保护,设置里安全模式,即在此期间对服务器推送的重复登录操作后的自动退出登录做一个拦截 保证操作的安全性。
394 lines
16 KiB
Dart
394 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:get_it/get_it.dart';
|
||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_card.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/stats_grid.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/work_order_card.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/quick_entry_card.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_trend_chart.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/plant_overview_card.dart';
|
||
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_status_indicator.dart';
|
||
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
|
||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||
|
||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||
|
||
class HomeV2Page extends StatefulWidget {
|
||
const HomeV2Page({super.key});
|
||
|
||
@override
|
||
State<HomeV2Page> createState() => _HomeV2PageState();
|
||
}
|
||
|
||
class _HomeV2PageState extends State<HomeV2Page> {
|
||
late HomeV2Bloc _bloc;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_bloc = context.read<HomeV2Bloc>();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFFF5F7FA),
|
||
body: BlocConsumer<HomeV2Bloc, HomeV2State>(
|
||
listener: (context, state) {
|
||
// 🔥 监听错误状态,显示友好提示
|
||
if (state is HomeV2Error && state.shouldShowError) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(state.message),
|
||
duration: const Duration(seconds: 2),
|
||
behavior: SnackBarBehavior.floating,
|
||
backgroundColor: Colors.orange,
|
||
),
|
||
);
|
||
}
|
||
},
|
||
builder: (context, state) {
|
||
// 🔥 错误状态显示重试按钮
|
||
if (state is HomeV2Error) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(Icons.error_outline, size: 48, color: Colors.orange),
|
||
const SizedBox(height: 12),
|
||
Text(state.message, style: const TextStyle(fontSize: 14, color: Colors.grey)),
|
||
const SizedBox(height: 16),
|
||
ElevatedButton(
|
||
onPressed: () => _bloc.add(const HomeV2LoadData()),
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
if (state is HomeV2Loaded) {
|
||
return RefreshIndicator(
|
||
onRefresh: () async {
|
||
_bloc.add(const HomeV2Refresh());
|
||
},
|
||
child: CustomScrollView(
|
||
slivers: [
|
||
SliverAppBar(
|
||
pinned: true,
|
||
backgroundColor: Colors.white,
|
||
elevation: 0,
|
||
automaticallyImplyLeading: false,
|
||
flexibleSpace: FlexibleSpaceBar(
|
||
background: Container(
|
||
padding: const EdgeInsets.fromLTRB(16, 40, 16, 16),
|
||
color: Colors.white,
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: InkWell(
|
||
onTap: _showPlantSelector,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Flexible(
|
||
child: StreamBuilder<SiteState>(
|
||
stream: sl<SiteCubit>().stream,
|
||
builder: (context, snapshot) {
|
||
final siteState =
|
||
snapshot.data ??
|
||
sl<SiteCubit>().state;
|
||
return Text(
|
||
siteState.selectedSite?.siteName ??
|
||
AppLocalizations.of(
|
||
context,
|
||
).translate(
|
||
'home_v2.select_site',
|
||
),
|
||
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),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
TcpStatusIndicator(
|
||
onTap: () => _showDeviceStatusModal(context),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
SliverPadding(
|
||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 40),
|
||
sliver: SliverList(
|
||
delegate: SliverChildListDelegate([
|
||
const SizedBox(height: 16),
|
||
// 天气和告警
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF2F3F5),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Icon(
|
||
Icons.cloud_queue,
|
||
size: 16,
|
||
color: Color(0xFF165DFF),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
AppLocalizations.of(
|
||
context,
|
||
).translate('home_v2.weather'),
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: Color(0xFF4E5969),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 8,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFFFF0F0),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
const Icon(
|
||
Icons.warning_amber_rounded,
|
||
size: 16,
|
||
color: Color(0xFFF53F3F),
|
||
),
|
||
const SizedBox(width: 6),
|
||
Expanded(
|
||
child: Text(
|
||
AppLocalizations.of(
|
||
context,
|
||
).translate('home_v2.emergency_alarm'),
|
||
style: const TextStyle(
|
||
fontSize: 13,
|
||
color: Color(0xFFF53F3F),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
const Icon(
|
||
Icons.arrow_forward_ios,
|
||
size: 12,
|
||
color: Color(0xFFF53F3F),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 16),
|
||
PowerCard(data: state.homeData),
|
||
const SizedBox(height: 10),
|
||
StatsGrid(data: state.homeData),
|
||
const SizedBox(height: 10),
|
||
PlantOverviewCard(
|
||
overview: state.homeData.plantOverview,
|
||
),
|
||
const SizedBox(height: 10),
|
||
WorkOrderCard(stats: state.homeData.workOrderStats),
|
||
const SizedBox(height: 10),
|
||
QuickEntryCard(),
|
||
const SizedBox(height: 10),
|
||
PowerTrendChart(
|
||
data: state.homeData.powerTrendData,
|
||
trendType: state.trendType,
|
||
onToggleType: (type) =>
|
||
_bloc.add(HomeV2ToggleTrendType(type)),
|
||
),
|
||
const SizedBox(height: 40),
|
||
]),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// 🔥 Initial/Loading 状态显示加载指示器
|
||
return const Center(child: CircularProgressIndicator(color: Color(0xFF165DFF)));
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void didChangeDependencies() {
|
||
super.didChangeDependencies();
|
||
// 首次进入时自动加载数据,但不显示转圈
|
||
if (_bloc.state is HomeV2Initial) {
|
||
// 延迟 100ms,确保 AppUserCubit 状态已更新
|
||
Future.delayed(const Duration(milliseconds: 100), () {
|
||
if (mounted) {
|
||
_bloc.add(const HomeV2LoadData());
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
void _showPlantSelector() {
|
||
if (_bloc.state is! HomeV2Loaded) return;
|
||
|
||
final currentState = _bloc.state as HomeV2Loaded;
|
||
final sites = currentState.sites;
|
||
|
||
if (sites.isEmpty) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
AppLocalizations.of(context).translate('home_v2.no_sites'),
|
||
),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
showModalBottomSheet(
|
||
context: context,
|
||
backgroundColor: Colors.white,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||
),
|
||
builder: (context) {
|
||
return BlocBuilder<HomeV2Bloc, HomeV2State>(
|
||
bloc: _bloc,
|
||
builder: (context, state) {
|
||
if (state is! HomeV2Loaded) return Container();
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.all(14),
|
||
constraints: BoxConstraints(
|
||
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
AppLocalizations.of(
|
||
context,
|
||
).translate('home_v2.select_site'),
|
||
style: const TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
color: Color(0xFF1D2129),
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
Expanded(
|
||
child: ListView.builder(
|
||
shrinkWrap: true,
|
||
itemCount: state.sites.length,
|
||
itemBuilder: (context, index) {
|
||
final site = state.sites[index];
|
||
final isSelected = state.selectedSite?.id == site.id;
|
||
|
||
return ListTile(
|
||
title: Text(site.siteName),
|
||
subtitle:
|
||
site.siteCode != null && site.siteCode!.isNotEmpty
|
||
? Text(
|
||
AppLocalizations.of(context)
|
||
.translate('home_v2.site_code')
|
||
.replaceAll('%s', site.siteCode!),
|
||
)
|
||
: null,
|
||
trailing: isSelected
|
||
? const Icon(
|
||
Icons.check,
|
||
color: Color(0xFF165DFF),
|
||
)
|
||
: null,
|
||
onTap: () {
|
||
// 更新全局选中的场站
|
||
sl<SiteCubit>().selectSite(site);
|
||
Navigator.pop(context);
|
||
|
||
// 切换场站后,重新加载首页数据
|
||
_bloc.add(const HomeV2LoadData());
|
||
},
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// 显示设备状态模态框 - 从底部滑出
|
||
void _showDeviceStatusModal(BuildContext context) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
isScrollControlled: true,
|
||
backgroundColor: Colors.transparent,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular(16),
|
||
topRight: Radius.circular(16),
|
||
),
|
||
),
|
||
builder: (BuildContext context) {
|
||
return MultiBlocProvider(
|
||
providers: [
|
||
BlocProvider.value(value: context.read<DeviceStatusBloc>()),
|
||
BlocProvider.value(
|
||
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
|
||
),
|
||
],
|
||
child: const DeviceStatusModal(),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|