Commit b368f64a authored by 张宏's avatar 张宏

1111

parent ba76b6f5
// ignore_for_file: unused_import // ignore_for_file: unused_import
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:laki_icu_app/views/home/index/home_index_view.dart';
import 'package:laki_icu_app/views/login/login_view.dart'; import 'package:laki_icu_app/views/login/login_view.dart';
import 'package:laki_icu_app/views/layout/default_view.dart'; import 'package:laki_icu_app/views/layout/default_view.dart';
import 'package:laki_icu_app/views/monitoring/index/monitoring_index_view.dart'; import 'package:laki_icu_app/views/monitoring/index/monitoring_index_view.dart';
...@@ -24,8 +23,6 @@ class AppRouter extends $AppRouter { ...@@ -24,8 +23,6 @@ class AppRouter extends $AppRouter {
children: [ children: [
// MonitoringIndexRoute / MonitoringIndexView(监控页) // MonitoringIndexRoute / MonitoringIndexView(监控页)
AutoRoute(page: MonitoringIndexRoute.page, initial: true), AutoRoute(page: MonitoringIndexRoute.page, initial: true),
// HomeRoute / HomeView(首页,物理文件保留)
AutoRoute(page: HomeIndexRoute.page),
], ],
), ),
]; ];
......
...@@ -8,43 +8,36 @@ ...@@ -8,43 +8,36 @@
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i5; import 'package:auto_route/auto_route.dart' as _i4;
import 'package:flutter/material.dart' as _i6; import 'package:flutter/material.dart' as _i5;
import 'package:laki_icu_app/views/home/index/home_index_view.dart' as _i2;
import 'package:laki_icu_app/views/layout/default_view.dart' as _i1; import 'package:laki_icu_app/views/layout/default_view.dart' as _i1;
import 'package:laki_icu_app/views/login/login_view.dart' as _i3; import 'package:laki_icu_app/views/login/login_view.dart' as _i2;
import 'package:laki_icu_app/views/monitoring/index/monitoring_index_view.dart' import 'package:laki_icu_app/views/monitoring/index/monitoring_index_view.dart'
as _i4; as _i3;
abstract class $AppRouter extends _i5.RootStackRouter { abstract class $AppRouter extends _i4.RootStackRouter {
$AppRouter({super.navigatorKey}); $AppRouter({super.navigatorKey});
@override @override
final Map<String, _i5.PageFactory> pagesMap = { final Map<String, _i4.PageFactory> pagesMap = {
DefaultLayoutRoute.name: (routeData) { DefaultLayoutRoute.name: (routeData) {
return _i5.AutoRoutePage<dynamic>( return _i4.AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i1.DefaultLayoutView(), child: const _i1.DefaultLayoutView(),
); );
}, },
HomeIndexRoute.name: (routeData) {
return _i5.AutoRoutePage<dynamic>(
routeData: routeData,
child: const _i2.HomeIndexView(),
);
},
LoginRoute.name: (routeData) { LoginRoute.name: (routeData) {
final args = routeData.argsAs<LoginRouteArgs>( final args = routeData.argsAs<LoginRouteArgs>(
orElse: () => const LoginRouteArgs()); orElse: () => const LoginRouteArgs());
return _i5.AutoRoutePage<dynamic>( return _i4.AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: _i3.LoginView(key: args.key), child: _i2.LoginView(key: args.key),
); );
}, },
MonitoringIndexRoute.name: (routeData) { MonitoringIndexRoute.name: (routeData) {
return _i5.AutoRoutePage<dynamic>( return _i4.AutoRoutePage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i4.MonitoringIndexView(), child: const _i3.MonitoringIndexView(),
); );
}, },
}; };
...@@ -52,8 +45,8 @@ abstract class $AppRouter extends _i5.RootStackRouter { ...@@ -52,8 +45,8 @@ abstract class $AppRouter extends _i5.RootStackRouter {
/// generated route for /// generated route for
/// [_i1.DefaultLayoutView] /// [_i1.DefaultLayoutView]
class DefaultLayoutRoute extends _i5.PageRouteInfo<void> { class DefaultLayoutRoute extends _i4.PageRouteInfo<void> {
const DefaultLayoutRoute({List<_i5.PageRouteInfo>? children}) const DefaultLayoutRoute({List<_i4.PageRouteInfo>? children})
: super( : super(
DefaultLayoutRoute.name, DefaultLayoutRoute.name,
initialChildren: children, initialChildren: children,
...@@ -61,29 +54,15 @@ class DefaultLayoutRoute extends _i5.PageRouteInfo<void> { ...@@ -61,29 +54,15 @@ class DefaultLayoutRoute extends _i5.PageRouteInfo<void> {
static const String name = 'DefaultLayoutRoute'; static const String name = 'DefaultLayoutRoute';
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name); static const _i4.PageInfo<void> page = _i4.PageInfo<void>(name);
}
/// generated route for
/// [_i2.HomeIndexView]
class HomeIndexRoute extends _i5.PageRouteInfo<void> {
const HomeIndexRoute({List<_i5.PageRouteInfo>? children})
: super(
HomeIndexRoute.name,
initialChildren: children,
);
static const String name = 'HomeIndexRoute';
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name);
} }
/// generated route for /// generated route for
/// [_i3.LoginView] /// [_i2.LoginView]
class LoginRoute extends _i5.PageRouteInfo<LoginRouteArgs> { class LoginRoute extends _i4.PageRouteInfo<LoginRouteArgs> {
LoginRoute({ LoginRoute({
_i6.Key? key, _i5.Key? key,
List<_i5.PageRouteInfo>? children, List<_i4.PageRouteInfo>? children,
}) : super( }) : super(
LoginRoute.name, LoginRoute.name,
args: LoginRouteArgs(key: key), args: LoginRouteArgs(key: key),
...@@ -92,14 +71,14 @@ class LoginRoute extends _i5.PageRouteInfo<LoginRouteArgs> { ...@@ -92,14 +71,14 @@ class LoginRoute extends _i5.PageRouteInfo<LoginRouteArgs> {
static const String name = 'LoginRoute'; static const String name = 'LoginRoute';
static const _i5.PageInfo<LoginRouteArgs> page = static const _i4.PageInfo<LoginRouteArgs> page =
_i5.PageInfo<LoginRouteArgs>(name); _i4.PageInfo<LoginRouteArgs>(name);
} }
class LoginRouteArgs { class LoginRouteArgs {
const LoginRouteArgs({this.key}); const LoginRouteArgs({this.key});
final _i6.Key? key; final _i5.Key? key;
@override @override
String toString() { String toString() {
...@@ -108,9 +87,9 @@ class LoginRouteArgs { ...@@ -108,9 +87,9 @@ class LoginRouteArgs {
} }
/// generated route for /// generated route for
/// [_i4.MonitoringIndexView] /// [_i3.MonitoringIndexView]
class MonitoringIndexRoute extends _i5.PageRouteInfo<void> { class MonitoringIndexRoute extends _i4.PageRouteInfo<void> {
const MonitoringIndexRoute({List<_i5.PageRouteInfo>? children}) const MonitoringIndexRoute({List<_i4.PageRouteInfo>? children})
: super( : super(
MonitoringIndexRoute.name, MonitoringIndexRoute.name,
initialChildren: children, initialChildren: children,
...@@ -118,5 +97,5 @@ class MonitoringIndexRoute extends _i5.PageRouteInfo<void> { ...@@ -118,5 +97,5 @@ class MonitoringIndexRoute extends _i5.PageRouteInfo<void> {
static const String name = 'MonitoringIndexRoute'; static const String name = 'MonitoringIndexRoute';
static const _i5.PageInfo<void> page = _i5.PageInfo<void>(name); static const _i4.PageInfo<void> page = _i4.PageInfo<void>(name);
} }
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/services/monitoring_service.dart';
import 'package:laki_icu_app/services/p2p_video_service.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
import 'home_index_state.dart';
/// 设备凭据 —— Mock 阶段使用占位值,后续接口接入后替换
/// TODO: 从舱详情接口获取真实的 cameraSn 和 wifiPwd
const _mockDeviceNo = 'MOCK_DEVICE_SN';
const _mockDevicePassword = 'MOCK_WIFI_PWD';
class HomeIndexCubit extends Cubit<HomeIndexState> {
final MonitoringService _monitoringService;
final WebrtcService _webrtcService;
final P2pVideoService _p2pVideoService;
StreamSubscription<WebrtcConnectionState>? _webrtcStateSub;
StreamSubscription<P2pServiceState>? _p2pStateSub;
/// 连接代际计数器 —— 每次切换模式时 +1,
/// 防止旧连接的异步结果污染当前模式的状态。
int _switchGen = 0;
HomeIndexCubit()
: _monitoringService = MonitoringService(),
_webrtcService = WebrtcService(),
_p2pVideoService = P2pVideoService(),
super(const HomeIndexState()) {
_listenWebrtcState();
_listenP2pState();
loadData();
}
/// 监听 WebRTC 连接状态变化并同步到 State
void _listenWebrtcState() {
_webrtcStateSub = _webrtcService.connectionStateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(videoConnectionState: state));
});
}
/// 监听 P2P 视频服务状态变化并同步到 State
void _listenP2pState() {
_p2pStateSub = _p2pVideoService.stateStream.listen((state) {
if (isClosed) return;
emit(this.state.copyWith(
isP2pConnected: state == P2pServiceState.connected,
));
});
}
Future<void> loadData() async {
emit(state.copyWith(isLoading: true, error: null));
try {
final metrics = await _monitoringService.getMetrics();
if (isClosed) return;
final patientInfo = await _monitoringService.getPatientInfo();
if (isClosed) return;
final alerts = await _monitoringService.getAlerts();
if (isClosed) return;
final menuItems = await _monitoringService.getMenuItems();
if (isClosed) return;
emit(state.copyWith(
isLoading: false,
status: HomeIndexStatus.success,
metrics: metrics,
patientInfo: patientInfo,
alerts: alerts,
menuItems: menuItems,
error: null,
));
// 数据加载完成后,按当前模式初始化视频连接
_initVideoByMode();
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
isLoading: false,
status: HomeIndexStatus.failure,
error: e.toString(),
));
}
}
/// 依据当前 [VideoStreamMode] 启动对应的视频连接
void _initVideoByMode() {
if (state.videoStreamMode == VideoStreamMode.p2p) {
_initP2p();
} else {
_initWebrtc();
}
}
/// 初始化 WebRTC 视频连接(已有逻辑保持不变)
Future<void> _initWebrtc() async {
await _webrtcService.connect(
// deviceNo: _mockDeviceNo,
// password: _mockDevicePassword,
deviceNo: 'VE10085871QOXG',
password: '143548',
);
}
/// 手动重试 WebRTC
Future<void> retryWebrtc() async {
await _initWebrtc();
}
/// 初始化 P2P 视频连接
Future<void> _initP2p() async {
await _p2pVideoService.connect(
deviceId: 'VE10085871QOXG',
username: 'admin',
password: '143548',
);
}
// ==================== 模式切换 ====================
/// 切换视频传输模式
///
/// 先停止当前模式 → 更新状态 → 启动新模式。
/// 使用 [_switchGen] 代际计数器防止旧连接的异步结果污染当前状态。
Future<void> switchStreamMode(VideoStreamMode mode) async {
if (state.isSwitchingMode) return;
if (state.videoStreamMode == mode) return;
final currentGen = ++_switchGen;
debugPrint(
'[HomeIndexCubit] 切换模式: ${state.videoStreamMode}$mode (gen=$currentGen)');
// 1. 标记切换中
emit(state.copyWith(isSwitchingMode: true));
// 2. 停止当前模式(用 stop 停连接,保留 StreamController 可复用)
if (state.videoStreamMode == VideoStreamMode.p2p) {
await _p2pVideoService.stop();
} else {
await _webrtcService.stop();
}
// 3. 竞态检查:如果在清理期间又触发了一次切换则放弃本次
if (currentGen != _switchGen || isClosed) return;
// 4. 切换到新模式
emit(state.copyWith(
videoStreamMode: mode,
isSwitchingMode: false,
isP2pConnected: false,
videoConnectionState: WebrtcConnectionState.disconnected,
));
// 5. 启动新模式视频连接
if (mode == VideoStreamMode.p2p) {
await _initP2p();
} else {
await _initWebrtc();
}
}
/// 手动重试当前模式的视频连接
Future<void> retryCurrentMode() async {
if (state.videoStreamMode == VideoStreamMode.p2p) {
await _p2pVideoService.stop();
await _initP2p();
} else {
await _webrtcService.stop();
await _initWebrtc();
}
}
Future<void> refreshData() async {
try {
final metrics = await _monitoringService.getMetrics();
if (isClosed) return;
final patientInfo = await _monitoringService.getPatientInfo();
if (isClosed) return;
final alerts = await _monitoringService.getAlerts();
if (isClosed) return;
final menuItems = await _monitoringService.getMenuItems();
if (isClosed) return;
emit(state.copyWith(
status: HomeIndexStatus.success,
metrics: metrics,
patientInfo: patientInfo,
alerts: alerts,
menuItems: menuItems,
error: null,
));
} catch (e) {
if (isClosed) return;
emit(state.copyWith(
status: HomeIndexStatus.failure,
error: e.toString(),
));
}
}
/// 获取 WebRTC 渲染器供 UI 层使用
WebrtcService get webrtcService => _webrtcService;
/// 获取 P2P 视频播放控制器供 UI 层使用
P2pVideoService get p2pVideoService => _p2pVideoService;
@override
Future<void> close() {
_webrtcStateSub?.cancel();
_p2pStateSub?.cancel();
_webrtcService.dispose();
_p2pVideoService.dispose();
return super.close();
}
}
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/services/webrtc_service.dart';
enum HomeIndexStatus {
initial,
loading,
success,
failure,
}
class HomeIndexState extends Equatable {
final HomeIndexStatus status;
final bool isLoading;
final String? error;
final List<MonitoringMetricBO> metrics;
final PatientInfoBO? patientInfo;
final List<AlertInfoBO> alerts;
final List<MonitoringMenuItemBO> menuItems;
/// WebRTC 视频连接状态
final WebrtcConnectionState videoConnectionState;
/// 当前视频传输模式
final VideoStreamMode videoStreamMode;
/// 是否正在切换传输模式(用于 UI loading 状态)
final bool isSwitchingMode;
/// P2P 模式是否已连接
final bool isP2pConnected;
const HomeIndexState({
this.status = HomeIndexStatus.initial,
this.isLoading = false,
this.error,
this.metrics = const [],
this.patientInfo,
this.alerts = const [],
this.menuItems = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected,
this.videoStreamMode = VideoStreamMode.webrtc,
this.isSwitchingMode = false,
this.isP2pConnected = false,
});
HomeIndexState copyWith({
HomeIndexStatus? status,
bool? isLoading,
String? error,
List<MonitoringMetricBO>? metrics,
PatientInfoBO? patientInfo,
List<AlertInfoBO>? alerts,
List<MonitoringMenuItemBO>? menuItems,
WebrtcConnectionState? videoConnectionState,
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
bool? isP2pConnected,
}) {
return HomeIndexState(
status: status ?? this.status,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
metrics: metrics ?? this.metrics,
patientInfo: patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
menuItems: menuItems ?? this.menuItems,
videoConnectionState:
videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
isP2pConnected: isP2pConnected ?? this.isP2pConnected,
);
}
@override
List<Object?> get props => [
status,
isLoading,
error,
metrics,
patientInfo,
alerts,
menuItems,
videoConnectionState,
videoStreamMode,
isSwitchingMode,
isP2pConnected,
];
}
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'cubit/home_index_cubit.dart';
import 'cubit/home_index_state.dart';
import 'widgets/alert_info_card.dart';
import 'widgets/metric_card.dart';
import 'widgets/patient_info_card.dart';
import 'widgets/sidebar_menu_item.dart';
import 'widgets/video_player_widget.dart';
@RoutePage()
class HomeIndexView extends StatelessWidget {
const HomeIndexView({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => HomeIndexCubit(),
child: const HomeIndexContent(),
);
}
}
class HomeIndexContent extends StatefulWidget {
const HomeIndexContent({super.key});
@override
State<HomeIndexContent> createState() => _HomeIndexContentState();
}
class _HomeIndexContentState extends State<HomeIndexContent> {
late final HomeIndexCubit _homeIndexCubit;
@override
void initState() {
super.initState();
_homeIndexCubit = context.read<HomeIndexCubit>();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F1F47),
body: SafeArea(
child: Column(
children: [
_buildAppBar(),
Expanded(
child: BlocBuilder<HomeIndexCubit, HomeIndexState>(
builder: (context, state) {
if (state.isLoading && state.metrics.isEmpty) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
if (state.status == HomeIndexStatus.failure &&
state.metrics.isEmpty) {
return _buildError(state.error);
}
return _buildMainContent(state);
},
),
),
_buildFooter(),
],
),
),
);
}
Widget _buildError(String? error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'加载失败: ${error ?? '未知错误'}',
style: TextStyle(color: Colors.white, fontSize: 24.sp),
),
SizedBox(height: 24.h),
ElevatedButton(
onPressed: () => _homeIndexCubit.loadData(),
child: const Text('重试'),
),
],
),
);
}
Widget _buildAppBar() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
border: Border(bottom: BorderSide(color: Colors.white24, width: 1)),
),
child: Row(
children: [
const CircleAvatar(
backgroundColor: Colors.white,
radius: 24,
child: Icon(Icons.local_hospital, color: Color(0xFF1E10B6), size: 28),
),
SizedBox(width: 16.w),
Text(
'加邦TSAAS动物医疗监护舱',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Spacer(),
_buildAppBarIcon(Icons.bluetooth, 'TSAAS'),
SizedBox(width: 16.w),
_buildAppBarIcon(Icons.description, null),
SizedBox(width: 16.w),
_buildAppBarIcon(Icons.settings, null),
],
),
);
}
Widget _buildAppBarIcon(IconData icon, String? label) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
border: Border.all(color: Colors.white24, width: 1),
borderRadius: BorderRadius.circular(20.r),
),
child: Row(
children: [
Icon(icon, color: Colors.white, size: 20.w),
if (label != null) ...[
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
),
),
],
],
),
);
}
Widget _buildMainContent(HomeIndexState state) {
return SingleChildScrollView(
padding: EdgeInsets.all(24.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧区域 (3/4宽度)
Expanded(
flex: 3,
child: Column(
children: [
// 指标栏(100%宽度)
_buildMetricsSection(state.metrics),
SizedBox(height: 24.h),
// 下方:监控画面 + 右侧信息卡片
SizedBox(
height: 500.h,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 监控画面模块(2/3宽度、100%高度)
Expanded(
flex: 2,
child: VideoPlayerWidget(
streamMode: state.videoStreamMode,
isSwitchingMode: state.isSwitchingMode,
webrtcRenderer:
_homeIndexCubit.webrtcService.renderer,
webrtcConnectionState: state.videoConnectionState,
p2pController:
_homeIndexCubit.p2pVideoService.playerController,
isP2pConnected: state.isP2pConnected,
onRetry: () => _homeIndexCubit.retryCurrentMode(),
onToggleMode: _onToggleStreamMode,
),
),
SizedBox(width: 24.w),
// 右侧(1/3宽度):患者信息卡片 + 告警信息卡片
Expanded(
flex: 1,
child: Column(
children: [
// 患者信息卡片(50%高度)
Expanded(
flex: 1,
child: state.patientInfo == null
? _buildEmptyCard('暂无患者信息')
: PatientInfoCard(
patientInfo: state.patientInfo!,
),
),
SizedBox(height: 24.h),
// 告警信息卡片(50%高度)
Expanded(
flex: 1,
child: AlertInfoCard(alerts: state.alerts),
),
],
),
),
],
),
),
],
),
),
SizedBox(width: 24.w),
// 右侧区域 (1/4宽度)
Expanded(
flex: 1,
child: SizedBox(
height: 500.h,
child: Column(
children: state.menuItems.map((item) {
return Expanded(
child: Padding(
padding: EdgeInsets.only(bottom: 24.h),
child: SidebarMenuItem(
item: item,
onTap: () {},
),
),
);
}).toList(),
),
),
),
],
),
);
}
/// 模式切换回调
void _onToggleStreamMode() {
final cubit = _homeIndexCubit;
final currentMode = cubit.state.videoStreamMode;
final nextMode = currentMode == VideoStreamMode.p2p
? VideoStreamMode.webrtc
: VideoStreamMode.p2p;
cubit.switchStreamMode(nextMode);
}
Widget _buildEmptyCard(String text) {
return Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Center(
child: Text(
text,
style: TextStyle(color: Colors.white70, fontSize: 24.sp),
),
),
);
}
Widget _buildMetricsSection(List<MonitoringMetricBO> metrics) {
return Container(
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Row(
children: metrics.map((metric) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: MetricCard(metric: metric),
),
);
}).toList(),
),
);
}
Widget _buildFooter() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 12.h),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
border: Border(top: BorderSide(color: Colors.white24, width: 1)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'总运行时长: 35 h',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
),
),
Text(
'环境温度: 25.6 ℃',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
class AlertInfoCard extends StatelessWidget {
final List<AlertInfoBO> alerts;
const AlertInfoCard({
super.key,
required this.alerts,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'告警信息',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
if (alerts.isNotEmpty) _buildStatus(alerts.first.status),
],
),
SizedBox(height: 12.h),
if (alerts.isNotEmpty)
...alerts.map((alert) => _buildAlertItem(alert))
else
Text(
'暂无告警信息',
style: TextStyle(color: Colors.white70, fontSize: 20.sp),
),
SizedBox(height: 4.h),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: Size(40.w, 30.h),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'详情 >',
style: TextStyle(
fontSize: 18.sp,
color: Colors.white70,
),
),
),
),
],
),
);
}
Widget _buildStatus(String status) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h),
decoration: BoxDecoration(
color: const Color(0xFFFFB800),
borderRadius: BorderRadius.circular(20.r),
),
child: Text(
status,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
);
}
Widget _buildAlertItem(AlertInfoBO alert) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber_rounded,
color: const Color(0xFFFF7D00),
size: 20.w,
),
SizedBox(width: 6.w),
Expanded(
child: Text(
alert.title,
style: TextStyle(
fontSize: 22.sp,
fontWeight: FontWeight.w500,
color: const Color(0xFFFF7D00),
),
),
),
],
),
SizedBox(height: 6.h),
Padding(
padding: EdgeInsets.only(left: 26.w),
child: Text(
alert.description,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white70,
),
),
),
SizedBox(height: 4.h),
Padding(
padding: EdgeInsets.only(left: 26.w),
child: Text(
alert.time,
style: TextStyle(
fontSize: 16.sp,
color: Colors.white54,
),
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
class MetricCard extends StatelessWidget {
final MonitoringMetricBO metric;
const MetricCard({
super.key,
required this.metric,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h, horizontal: 8.w),
decoration: BoxDecoration(
color: Color(metric.backgroundColorValue),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Color(metric.borderColorValue),
width: 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
metric.value,
style: TextStyle(
fontSize: 36.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 4.h),
Text(
metric.unit,
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
),
),
SizedBox(height: 8.h),
Text(
metric.label,
style: TextStyle(
fontSize: 18.sp,
color: Colors.white70,
),
textAlign: TextAlign.center,
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
class PatientInfoCard extends StatelessWidget {
final PatientInfoBO patientInfo;
const PatientInfoCard({
super.key,
required this.patientInfo,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF1E10B6), Color(0xFF3A87FF)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
border: Border.all(color: Colors.white24, width: 1),
borderRadius: BorderRadius.circular(20.r),
),
child: patientInfo.avatarUrl.isEmpty
? Icon(Icons.pets, size: 48.w, color: Colors.white54)
: Image.network(
patientInfo.avatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.pets,
size: 48.w,
color: Colors.white54,
);
},
),
),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
patientInfo.name,
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 8.h),
_buildInfoText('类型:${patientInfo.type}'),
SizedBox(height: 4.h),
_buildInfoText('电话:${patientInfo.phone}'),
SizedBox(height: 4.h),
_buildInfoText('品种:${patientInfo.breed}'),
SizedBox(height: 4.h),
_buildInfoText('医生:${patientInfo.doctor}'),
],
),
),
],
),
);
}
Widget _buildInfoText(String text) {
return Text(
text,
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
class SidebarMenuItem extends StatelessWidget {
final MonitoringMenuItemBO item;
final VoidCallback? onTap;
const SidebarMenuItem({
super.key,
required this.item,
this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
borderRadius: BorderRadius.circular(20.r),
border: Border.all(color: Colors.white24, width: 1),
),
child: Row(
children: [
Text(
item.icon,
style: TextStyle(fontSize: 32.w),
),
SizedBox(width: 12.w),
Expanded(
child: Text(
item.title,
style: TextStyle(
fontSize: 20.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:vsdk/app_player.dart';
import '../../../../enums/video_stream_mode_enum.dart';
/// 通用视频全屏播放页面(兼容 WebRTC + P2P)
///
/// 全屏展示摄像头实时画面,点击任意位置退出全屏。
///
/// 涉及页面:首页监护舱 - 监控画面
class VideoFullscreenPage extends StatelessWidget {
/// 当前视频传输模式
final VideoStreamMode mode;
/// WebRTC 模式下的视频渲染器
final RTCVideoRenderer? webrtcRenderer;
/// P2P 模式下的播放控制器
final AppPlayerController? p2pController;
const VideoFullscreenPage({
super.key,
required this.mode,
this.webrtcRenderer,
this.p2pController,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Stack(
fit: StackFit.expand,
children: [
// 根据模式渲染不同的视频画面
_buildVideoContent(),
// 底部提示栏
_buildBottomBar(),
],
),
),
);
}
/// 按模式渲染视频内容
Widget _buildVideoContent() {
if (mode == VideoStreamMode.p2p && p2pController != null) {
return AppPlayerView(controller: p2pController!);
}
if (mode == VideoStreamMode.webrtc && webrtcRenderer != null) {
return RTCVideoView(
webrtcRenderer!,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
);
}
return const SizedBox.shrink();
}
/// 底部提示栏(左下角)
Widget _buildBottomBar() {
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 48),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black54, Colors.transparent],
),
),
child: const Row(
children: [
Icon(Icons.fullscreen_exit, color: Colors.white70, size: 20),
SizedBox(width: 8),
Text(
'点击任意位置退出全屏',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:vsdk/app_player.dart';
import '../../../../enums/video_stream_mode_enum.dart';
import '../../../../services/webrtc_service.dart';
import 'video_fullscreen_page.dart';
import 'video_stream_switch.dart';
/// 通用实时视频播放组件(兼容 WebRTC + P2P 双模式)
///
/// 职责:
/// - WebRTC 模式:通过 [RTCVideoView] 渲染远程摄像头实时画面
/// - P2P 模式:通过 [AppPlayerView] 渲染 Texture 视频画面
/// - 依据当前模式的连接状态显示对应的状态指示器
/// - 右下角提供模式切换按钮(P2P/RTC)和全屏按钮
///
/// 涉及页面:首页监护舱 - 监控画面
class VideoPlayerWidget extends StatelessWidget {
/// 当前视频传输模式
final VideoStreamMode streamMode;
/// 是否正在切换模式
final bool isSwitchingMode;
/// WebRTC 视频渲染器(WebRTC 模式时使用)
final RTCVideoRenderer? webrtcRenderer;
/// WebRTC 连接状态
final WebrtcConnectionState webrtcConnectionState;
/// P2P 视频播放控制器(P2P 模式时使用)
final AppPlayerController? p2pController;
/// P2P 模式是否已连接
final bool isP2pConnected;
/// 连接失败时点击重试回调
final VoidCallback? onRetry;
/// 模式切换回调
final VoidCallback? onToggleMode;
const VideoPlayerWidget({
super.key,
required this.streamMode,
this.isSwitchingMode = false,
this.webrtcRenderer,
required this.webrtcConnectionState,
this.p2pController,
this.isP2pConnected = false,
this.onRetry,
this.onToggleMode,
});
/// 当前模式视频是否正在显示
bool get _isVideoShowing {
if (streamMode == VideoStreamMode.webrtc) {
return webrtcConnectionState == WebrtcConnectionState.connected;
}
return isP2pConnected && p2pController != null;
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20.r),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xCC63A0FF), Color(0xCC0024C4)],
),
border: Border.all(color: Colors.white24, width: 1),
),
child: Stack(
fit: StackFit.expand,
children: [
// 视频画面层
_buildVideoLayer(),
// 状态指示层
_buildStatusOverlay(),
// 模式切换按钮(右下角倒数第二个位置)
if (onToggleMode != null && !isSwitchingMode)
_buildToggleModeButton(),
// 全屏按钮(右下角最后一个位置)
if (_isVideoShowing) _buildFullscreenButton(context),
],
),
),
);
}
// ==================== 视频画面层 ====================
/// 视频画面
Widget _buildVideoLayer() {
if (streamMode == VideoStreamMode.webrtc) {
if (webrtcConnectionState == WebrtcConnectionState.connected &&
webrtcRenderer != null) {
return RTCVideoView(
webrtcRenderer!,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
);
}
return _buildPlaceholder();
}
// P2P 模式
if (isP2pConnected && p2pController != null) {
return AppPlayerView(controller: p2pController!);
}
return _buildPlaceholder();
}
// ==================== 状态遮罩层 ====================
/// 状态遮罩层
Widget _buildStatusOverlay() {
if (isSwitchingMode) {
return _buildSwitchingOverlay();
}
if (streamMode == VideoStreamMode.webrtc) {
switch (webrtcConnectionState) {
case WebrtcConnectionState.connecting:
return _buildLoadingOverlay('正在连接摄像头...');
case WebrtcConnectionState.reconnecting:
return _buildLoadingOverlay('正在重新连接...');
case WebrtcConnectionState.failed:
return _buildErrorOverlay();
default:
return const SizedBox.shrink();
}
}
// P2P 模式:未连接显示加载
if (!isP2pConnected) {
return _buildLoadingOverlay('正在 P2P 连接...');
}
return const SizedBox.shrink();
}
/// 模式切换中遮罩
Widget _buildSwitchingOverlay() {
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
'正在切换传输模式...',
style: TextStyle(fontSize: 18.sp, color: Colors.white70),
),
],
),
),
);
}
/// 加载中遮罩
Widget _buildLoadingOverlay(String message) {
return Container(
color: Colors.black26,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(color: Colors.white70),
SizedBox(height: 16.h),
Text(
message,
style: TextStyle(fontSize: 18.sp, color: Colors.white70),
),
],
),
),
);
}
/// 错误遮罩
Widget _buildErrorOverlay() {
return Container(
color: Colors.black38,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48.w, color: Colors.white54),
SizedBox(height: 16.h),
Text(
'视频连接失败',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white70,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 24.h),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重新连接'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3A87FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20.r),
),
),
),
],
),
),
);
}
// ==================== 叠加按钮 ====================
/// 全屏按钮(右下角)
Widget _buildFullscreenButton(BuildContext context) {
return Positioned(
right: 12.w,
bottom: 12.h,
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VideoFullscreenPage(
mode: streamMode,
webrtcRenderer: webrtcRenderer,
p2pController: p2pController,
),
),
);
},
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.fullscreen,
color: Colors.white,
size: 22.w,
),
),
),
);
}
/// 模式切换按钮(全屏按钮左侧 8px 间距)
Widget _buildToggleModeButton() {
return Positioned(
right: 12.w + 40.w + 8.w,
bottom: 12.h,
child: VideoStreamSwitch(
currentMode: streamMode,
onToggle: onToggleMode!,
),
);
}
/// 未连接时的占位
Widget _buildPlaceholder() {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.videocam_off, size: 48.w, color: Colors.white38),
SizedBox(height: 16.h),
Text(
'暂无视频画面',
style: TextStyle(
fontSize: 20.sp,
color: Colors.white54,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../../enums/video_stream_mode_enum.dart';
/// P2P/WebRTC 模式切换控件
///
/// 显示当前传输模式(P2P / RTC),点击切换至另一种模式。
/// 位置:视频容器右下角,全屏按钮左侧。
///
/// 涉及页面:首页监护舱 - 监控画面
class VideoStreamSwitch extends StatelessWidget {
/// 当前视频传输模式
final VideoStreamMode currentMode;
/// 点击切换回调
final VoidCallback onToggle;
const VideoStreamSwitch({
super.key,
required this.currentMode,
required this.onToggle,
});
@override
Widget build(BuildContext context) {
final isP2p = currentMode == VideoStreamMode.p2p;
return GestureDetector(
onTap: onToggle,
child: Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: isP2p
? const Color(0xFF3A87FF).withValues(alpha: 0.6)
: Colors.black38,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isP2p ? Colors.white54 : Colors.white24,
width: 1,
),
),
child: Center(
child: Text(
isP2p ? 'P2P' : 'RTC',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
);
}
}
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