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
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
25 additions
and
1141 deletions
+25
-1141
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
-0
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
This diff is collapsed.
Click to expand it.
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