Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
laki_icu_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
laki_icu_app
Commits
b368f64a
Commit
b368f64a
authored
Jun 22, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
1111
parent
ba76b6f5
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
25 additions
and
1471 deletions
+25
-1471
app_router.dart
lib/routes/app_router.dart
+0
-3
app_router.gr.dart
lib/routes/app_router.gr.dart
+25
-46
home_index_cubit.dart
lib/views/home/index/cubit/home_index_cubit.dart
+0
-220
home_index_state.dart
lib/views/home/index/cubit/home_index_state.dart
+0
-91
home_index_view.dart
lib/views/home/index/home_index_view.dart
+0
-330
alert_info_card.dart
lib/views/home/index/widgets/alert_info_card.dart
+0
-144
metric_card.dart
lib/views/home/index/widgets/metric_card.dart
+0
-57
patient_info_card.dart
lib/views/home/index/widgets/patient_info_card.dart
+0
-90
sidebar_menu_item.dart
lib/views/home/index/widgets/sidebar_menu_item.dart
+0
-52
video_fullscreen_page.dart
lib/views/home/index/widgets/video_fullscreen_page.dart
+0
-90
video_player_widget.dart
lib/views/home/index/widgets/video_player_widget.dart
+0
-291
video_stream_switch.dart
lib/views/home/index/widgets/video_stream_switch.dart
+0
-57
No files found.
lib/routes/app_router.dart
View file @
b368f64a
// 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
),
],
],
),
),
];
];
...
...
lib/routes/app_router.gr.dart
View file @
b368f64a
...
@@ -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
_i
3
;
import
'package:laki_icu_app/views/login/login_view.dart'
as
_i
2
;
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
_i
4
;
as
_i
3
;
abstract
class
$
AppRouter
extends
_i
5
.
RootStackRouter
{
abstract
class
$
AppRouter
extends
_i
4
.
RootStackRouter
{
$AppRouter
({
super
.
navigatorKey
});
$AppRouter
({
super
.
navigatorKey
});
@override
@override
final
Map
<
String
,
_i
5
.
PageFactory
>
pagesMap
=
{
final
Map
<
String
,
_i
4
.
PageFactory
>
pagesMap
=
{
DefaultLayoutRoute
.
name
:
(
routeData
)
{
DefaultLayoutRoute
.
name
:
(
routeData
)
{
return
_i
5
.
AutoRoutePage
<
dynamic
>(
return
_i
4
.
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
_i
5
.
AutoRoutePage
<
dynamic
>(
return
_i
4
.
AutoRoutePage
<
dynamic
>(
routeData:
routeData
,
routeData:
routeData
,
child:
_i
3
.
LoginView
(
key:
args
.
key
),
child:
_i
2
.
LoginView
(
key:
args
.
key
),
);
);
},
},
MonitoringIndexRoute
.
name
:
(
routeData
)
{
MonitoringIndexRoute
.
name
:
(
routeData
)
{
return
_i
5
.
AutoRoutePage
<
dynamic
>(
return
_i
4
.
AutoRoutePage
<
dynamic
>(
routeData:
routeData
,
routeData:
routeData
,
child:
const
_i
4
.
MonitoringIndexView
(),
child:
const
_i
3
.
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
_i
5
.
PageRouteInfo
<
void
>
{
class
DefaultLayoutRoute
extends
_i
4
.
PageRouteInfo
<
void
>
{
const
DefaultLayoutRoute
({
List
<
_i
5
.
PageRouteInfo
>?
children
})
const
DefaultLayoutRoute
({
List
<
_i
4
.
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
/// [_i
3
.LoginView]
/// [_i
2
.LoginView]
class
LoginRoute
extends
_i
5
.
PageRouteInfo
<
LoginRouteArgs
>
{
class
LoginRoute
extends
_i
4
.
PageRouteInfo
<
LoginRouteArgs
>
{
LoginRoute
({
LoginRoute
({
_i
6
.
Key
?
key
,
_i
5
.
Key
?
key
,
List
<
_i
5
.
PageRouteInfo
>?
children
,
List
<
_i
4
.
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
_i
5
.
PageInfo
<
LoginRouteArgs
>
page
=
static
const
_i
4
.
PageInfo
<
LoginRouteArgs
>
page
=
_i
5
.
PageInfo
<
LoginRouteArgs
>(
name
);
_i
4
.
PageInfo
<
LoginRouteArgs
>(
name
);
}
}
class
LoginRouteArgs
{
class
LoginRouteArgs
{
const
LoginRouteArgs
({
this
.
key
});
const
LoginRouteArgs
({
this
.
key
});
final
_i
6
.
Key
?
key
;
final
_i
5
.
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
/// [_i
4
.MonitoringIndexView]
/// [_i
3
.MonitoringIndexView]
class
MonitoringIndexRoute
extends
_i
5
.
PageRouteInfo
<
void
>
{
class
MonitoringIndexRoute
extends
_i
4
.
PageRouteInfo
<
void
>
{
const
MonitoringIndexRoute
({
List
<
_i
5
.
PageRouteInfo
>?
children
})
const
MonitoringIndexRoute
({
List
<
_i
4
.
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
_i
5
.
PageInfo
<
void
>
page
=
_i5
.
PageInfo
<
void
>(
name
);
static
const
_i
4
.
PageInfo
<
void
>
page
=
_i4
.
PageInfo
<
void
>(
name
);
}
}
lib/views/home/index/cubit/home_index_cubit.dart
deleted
100644 → 0
View file @
ba76b6f5
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
();
}
}
lib/views/home/index/cubit/home_index_state.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
];
}
lib/views/home/index/home_index_view.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
),
],
),
);
}
}
lib/views/home/index/widgets/alert_info_card.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
),
),
],
),
);
}
}
lib/views/home/index/widgets/metric_card.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
],
),
);
}
}
lib/views/home/index/widgets/patient_info_card.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
);
}
}
lib/views/home/index/widgets/sidebar_menu_item.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
),
),
],
),
),
);
}
}
lib/views/home/index/widgets/video_fullscreen_page.dart
deleted
100644 → 0
View file @
ba76b6f5
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
),
),
],
),
),
);
}
}
lib/views/home/index/widgets/video_player_widget.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
),
],
),
);
}
}
lib/views/home/index/widgets/video_stream_switch.dart
deleted
100644 → 0
View file @
ba76b6f5
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
,
),
),
),
),
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment