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
cf3b74d6
Commit
cf3b74d6
authored
Jun 17, 2026
by
张宏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
清除ndk版本错误
parent
8983fa7f
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
501 additions
and
192 deletions
+501
-192
settings.json
.vscode/settings.json
+2
-1
build.gradle
android/app/build.gradle
+4
-1
AndroidManifest.xml
android/app/src/main/AndroidManifest.xml
+1
-0
MainActivity.kt
...pp/src/main/kotlin/com/qialg/laki_icu_app/MainActivity.kt
+0
-39
strings.xml
android/app/src/main/res/values/strings.xml
+1
-1
build.gradle
android/build.gradle
+7
-0
settings.gradle.kts
android/settings.gradle.kts
+32
-0
Info.plist
ios/Runner/Info.plist
+1
-1
usb_camera_service.dart
lib/services/usb_camera_service.dart
+277
-90
camera_controls.dart
lib/views/home/index/widgets/camera_controls.dart
+48
-32
monitoring_screen.dart
lib/views/home/index/widgets/monitoring_screen.dart
+126
-26
login_view.dart
lib/views/login/login_view.dart
+1
-1
pubspec.yaml
pubspec.yaml
+1
-0
No files found.
.vscode/settings.json
View file @
cf3b74d6
{
{
"dart.flutterSdkPath"
:
".fvm/versions/3.41.9"
,
"dart.flutterSdkPath"
:
".fvm/versions/3.41.9"
,
"java.configuration.updateBuildConfiguration"
:
"
interactive
"
"java.configuration.updateBuildConfiguration"
:
"
disabled
"
}
}
\ No newline at end of file
android/app/build.gradle
View file @
cf3b74d6
...
@@ -24,7 +24,7 @@ if (flutterVersionName == null) {
...
@@ -24,7 +24,7 @@ if (flutterVersionName == null) {
android
{
android
{
namespace
"com.qialg.laki_icu_app"
namespace
"com.qialg.laki_icu_app"
compileSdk
Version
flutter
.
compileSdkVersion
compileSdk
=
36
ndkVersion
flutter
.
ndkVersion
ndkVersion
flutter
.
ndkVersion
compileOptions
{
compileOptions
{
...
@@ -49,6 +49,9 @@ android {
...
@@ -49,6 +49,9 @@ android {
targetSdkVersion
flutter
.
targetSdkVersion
targetSdkVersion
flutter
.
targetSdkVersion
versionCode
flutterVersionCode
.
toInteger
()
versionCode
flutterVersionCode
.
toInteger
()
versionName
flutterVersionName
versionName
flutterVersionName
ndk
{
abiFilters
'armeabi-v7a'
,
'arm64-v8a'
}
}
}
flavorDimensions
"environment"
flavorDimensions
"environment"
...
...
android/app/src/main/AndroidManifest.xml
View file @
cf3b74d6
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
<!-- USB 摄像头权限 -->
<!-- USB 摄像头权限 -->
<uses-feature
android:name=
"android.hardware.usb.host"
android:required=
"true"
/>
<uses-feature
android:name=
"android.hardware.usb.host"
android:required=
"true"
/>
<uses-permission
android:name=
"android.permission.CAMERA"
/>
<uses-permission
android:name=
"android.permission.CAMERA"
/>
<uses-permission
android:name=
"android.permission.RECORD_AUDIO"
/>
<application
<application
android:label=
"@string/app_name"
android:label=
"@string/app_name"
...
...
android/app/src/main/kotlin/com/qialg/laki_icu_app/MainActivity.kt
View file @
cf3b74d6
...
@@ -4,42 +4,3 @@ import io.flutter.embedding.android.FlutterActivity
...
@@ -4,42 +4,3 @@ import io.flutter.embedding.android.FlutterActivity
class
MainActivity
:
FlutterActivity
()
{
class
MainActivity
:
FlutterActivity
()
{
}
}
// package com.example.laki_icu_app
// import com.example.laki_icu_app.camera.UsbCameraPlugin
// import com.example.laki_icu_app.camera.UsbCameraViewFactory
// import io.flutter.embedding.android.FlutterActivity
// import io.flutter.embedding.engine.FlutterEngine
// import io.flutter.plugin.common.MethodChannel
// class MainActivity : FlutterActivity() {
// companion object {
// const val USB_CAMERA_CHANNEL = "usb_camera"
// const val USB_CAMERA_VIEW_TYPE = "usb_camera_view"
// }
// private var usbCameraPlugin: UsbCameraPlugin? = null
// override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
// super.configureFlutterEngine(flutterEngine)
// // 注册 PlatformViewFactory —— 用于在 Flutter 中渲染摄像头预览画面
// flutterEngine
// .platformViewsController
// .registry
// .registerViewFactory(USB_CAMERA_VIEW_TYPE, UsbCameraViewFactory())
// // 注册 MethodChannel —— 用于 Flutter 与原生层通信(拍照、录像等控制指令)
// val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, USB_CAMERA_CHANNEL)
// usbCameraPlugin = UsbCameraPlugin(this, channel)
// }
// override fun onDestroy() {
// usbCameraPlugin?.dispose()
// super.onDestroy()
// }
// }
android/app/src/main/res/values/strings.xml
View file @
cf3b74d6
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources>
<string
name=
"app_name"
>
智能酒店
</string>
<string
name=
"app_name"
>
Laki_icu
</string>
</resources>
</resources>
android/build.gradle
View file @
cf3b74d6
...
@@ -23,6 +23,13 @@ rootProject.buildDir = '../build'
...
@@ -23,6 +23,13 @@ rootProject.buildDir = '../build'
subprojects
{
subprojects
{
project
.
buildDir
=
"${rootProject.buildDir}/${project.name}"
project
.
buildDir
=
"${rootProject.buildDir}/${project.name}"
project
.
evaluationDependsOn
(
':app'
)
project
.
evaluationDependsOn
(
':app'
)
// :libnative 等第三方子模块未显式设置 ndkVersion,AGP 8.11 默认给 27,
// 与 ndk.dir (28.2) 和 :app/:jni 的 flutter.ndkVersion (28.2) 冲突。
// 在插件 apply 后立即注入 28.2,因为 :libnative 自己的 android{} 里没写 ndkVersion,不会被覆盖。
plugins
.
withId
(
'com.android.library'
)
{
android
.
ndkVersion
=
'28.2.13676358'
}
}
}
tasks
.
register
(
"clean"
,
Delete
)
{
tasks
.
register
(
"clean"
,
Delete
)
{
...
...
android/settings.gradle.kts
View file @
cf3b74d6
...
@@ -24,3 +24,35 @@ plugins {
...
@@ -24,3 +24,35 @@ plugins {
}
}
include(":app")
include(":app")
// USB Camera 插件子模块引用
// flutter_usbcamera 的 build.gradle 写死了 implementation project(':libausbc')
// 必须在根项目 settings 中注册 :libausbc, :libuvc, :libnative
// 使用字符串 indexOf 从 .flutter-plugins-dependencies 中解析路径(避免正则转义问题)
val pluginsFile = java.io.File(rootProject.projectDir.parentFile, ".flutter-plugins-dependencies")
val text = pluginsFile.readText()
// JSON 格式: {"name":"flutter_usbcamera","path":"/some/path/",...}
// 找到 "flutter_usbcamera" 然后找它后面的第一个 "path" 值
val cameraKey = "\"flutter_usbcamera\""
val cameraIdx = text.indexOf(cameraKey)
if (cameraIdx >= 0) {
// 从 cameraKey 之后找 "\"path\":\""
val afterKey = text.substring(cameraIdx + cameraKey.length)
val pathKey = "\"path\":\""
val pathIdx = afterKey.indexOf(pathKey)
if (pathIdx >= 0) {
val pathStart = pathIdx + pathKey.length
val pathEnd = afterKey.indexOf("\"", pathStart)
if (pathEnd >= 0) {
val pluginPath = afterKey.substring(pathStart, pathEnd) + "/android"
include(":libuvc")
project(":libuvc").projectDir = java.io.File(pluginPath, "libuvc")
include(":libnative")
project(":libnative").projectDir = java.io.File(pluginPath, "libnative")
include(":libausbc")
project(":libausbc").projectDir = java.io.File(pluginPath, "libausbc")
}
}
}
ios/Runner/Info.plist
View file @
cf3b74d6
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
<
k
e
y
>
CFBundleDevelopmentRegion
<
/k
e
y
>
<
k
e
y
>
CFBundleDevelopmentRegion
<
/k
e
y
>
<
string
>
$
(
DEVELOPMENT_LANGUAGE
)<
/string
>
<
string
>
$
(
DEVELOPMENT_LANGUAGE
)<
/string
>
<
k
e
y
>
CFBundleDisplayName
<
/k
e
y
>
<
k
e
y
>
CFBundleDisplayName
<
/k
e
y
>
<
string
>
智能酒店
<
/string
>
<
string
>
Laki_icu
<
/string
>
<
k
e
y
>
CFBundleExecutable
<
/k
e
y
>
<
k
e
y
>
CFBundleExecutable
<
/k
e
y
>
<
string
>
$
(
EXECUTABLE_NAME
)<
/string
>
<
string
>
$
(
EXECUTABLE_NAME
)<
/string
>
<
k
e
y
>
CFBundleIdentifier
<
/k
e
y
>
<
k
e
y
>
CFBundleIdentifier
<
/k
e
y
>
...
...
lib/services/usb_camera_service.dart
View file @
cf3b74d6
import
'dart:async'
;
import
'dart:io'
;
import
'dart:io'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter
/services
.dart'
;
import
'package:flutter
_usbcamera/flutter_usbcamera
.dart'
;
import
'package:p
ermission_handler/permission_handl
er.dart'
;
import
'package:p
ath_provider/path_provid
er.dart'
;
/// USB 摄像头服务层
/// USB 摄像头服务层
///
///
/// 职责:
/// 职责:
/// - 封装
Flutter 与 Android 原生层之间的 MethodChannel 通信
/// - 封装
[UsbCameraController],对上层提供统一的业务接口
/// -
提供拍照、录像、设备查询等操作接口
/// -
监听设备热插拔事件,自动管理摄像头生命周期
/// -
处理摄像头权限申请
/// -
将事件流中的拍摄完成回调转换为 Future 返回值
///
///
///
MethodChannel 名称: "usb_camera"
///
涉及页面:首页监护舱 - 监控画面
class
UsbCameraService
{
class
UsbCameraService
{
static
const
_channel
=
MethodChannel
(
'usb_camera'
);
static
UsbCameraService
?
_instance
;
final
UsbCameraController
_controller
;
// ==================== 权限管理 ====================
StreamSubscription
<
CameraEvent
>?
_eventSub
;
/// 检查并请求摄像头权限
// 事件广播流(供 UI 层订阅设备状态变化)
/// 返回 true 表示权限已授予
final
StreamController
<
CameraEvent
>
_eventController
=
static
Future
<
bool
>
requestCameraPermission
()
async
{
StreamController
<
CameraEvent
>.
broadcast
();
// iOS 需要 CAMERA 权限,Android 需要 CAMERA 权限
if
(
Platform
.
isAndroid
)
{
// 拍照/录像完成时的 Completer
final
status
=
await
Permission
.
camera
.
request
();
Completer
<
String
?>?
_captureCompleter
;
return
status
.
isGranted
;
}
else
if
(
Platform
.
isIOS
)
{
UsbCameraService
.
_
()
:
_controller
=
UsbCameraController
();
final
status
=
await
Permission
.
camera
.
request
();
return
status
.
isGranted
;
/// 单例实例
static
UsbCameraService
get
instance
{
_instance
??=
UsbCameraService
.
_
();
return
_instance
!;
}
/// USB 设备事件流(设备插拔、摄像头状态、拍摄回调等)
Stream
<
CameraEvent
>
get
eventStream
=>
_eventController
.
stream
;
/// 当前控制器引用(供需要直接调用的场景)
UsbCameraController
get
controller
=>
_controller
;
/// 摄像头是否已打开
bool
_isOpened
=
false
;
bool
get
isOpened
=>
_isOpened
;
/// 是否正在录像
bool
_isRecording
=
false
;
bool
get
isRecording
=>
_isRecording
;
// ==================== 初始化与销毁 ====================
/// 初始化:注册 USB 监听,开始检测设备插拔
Future
<
void
>
initialize
()
async
{
if
(
_eventSub
!=
null
)
return
;
// 已初始化
// 监听插件事件流
_eventSub
=
UsbCameraController
.
events
.
listen
(
_handleEvent
);
// 注册 USB 监听
await
_controller
.
registerUsb
();
debugPrint
(
'[UsbCameraService] 初始化完成,USB 监听已注册'
);
}
/// 处理来自插件的所有事件
void
_handleEvent
(
CameraEvent
event
)
{
debugPrint
(
'[UsbCameraService] 收到事件:
${event.type.name}
, '
'deviceId=
${event.deviceId}
, state=
${event.state}
, path=
${event.path}
'
);
switch
(
event
.
type
)
{
case
CameraEventType
.
onAttachDev
:
// USB 摄像头插入 → 自动请求权限
if
(
event
.
deviceId
!=
null
)
{
_controller
.
requestPermission
(
event
.
deviceId
!);
}
break
;
case
CameraEventType
.
onDetachDev
:
case
CameraEventType
.
onDisConnectDev
:
// 设备拔出或断开
_isOpened
=
false
;
_isRecording
=
false
;
break
;
case
CameraEventType
.
onConnectDev
:
// 权限通过,自动打开摄像头
if
(
event
.
deviceId
!=
null
)
{
_openCameraInternal
(
event
.
deviceId
!);
}
break
;
case
CameraEventType
.
onCancelDev
:
// 权限被拒绝
_isOpened
=
false
;
break
;
case
CameraEventType
.
onCameraState
:
if
(
event
.
state
==
'opened'
)
{
_isOpened
=
true
;
}
else
if
(
event
.
state
==
'closed'
||
event
.
state
==
'error'
)
{
_isOpened
=
false
;
_isRecording
=
false
;
}
break
;
case
CameraEventType
.
onCaptureBegin
:
// 拍摄开始
break
;
case
CameraEventType
.
onCaptureComplete
:
// 拍摄完成 → 解析 Completer
_isRecording
=
false
;
if
(
_captureCompleter
!=
null
&&
!
_captureCompleter
!.
isCompleted
)
{
_captureCompleter
!.
complete
(
event
.
path
);
_captureCompleter
=
null
;
}
break
;
case
CameraEventType
.
onCaptureError
:
// 拍摄出错
_isRecording
=
false
;
if
(
_captureCompleter
!=
null
&&
!
_captureCompleter
!.
isCompleted
)
{
_captureCompleter
!.
completeError
(
Exception
(
event
.
error
??
'拍摄失败'
),
);
_captureCompleter
=
null
;
}
break
;
default
:
break
;
}
}
return
false
;
// 转发事件给所有订阅者
_eventController
.
add
(
event
);
}
}
/// 检查摄像头权限是否已授予
/// 内部打开摄像头方法
static
Future
<
bool
>
hasCameraPermission
()
async
{
Future
<
void
>
_openCameraInternal
(
int
deviceId
)
async
{
return
await
Permission
.
camera
.
isGranted
;
try
{
await
_controller
.
openCamera
(
deviceId
,
request:
const
CameraRequest
(
previewWidth:
640
,
previewHeight:
480
,
renderMode:
RenderMode
.
opengl
,
previewFormat:
PreviewFormat
.
mjpeg
,
),
);
debugPrint
(
'[UsbCameraService] 摄像头已打开: deviceId=
$deviceId
'
);
}
catch
(
e
)
{
debugPrint
(
'[UsbCameraService] 打开摄像头失败:
$e
'
);
}
}
}
// ==================== 设备管理 ====================
// ==================== 设备管理 ====================
/// 获取已连接的 USB 摄像头设备列表
/// 获取已连接的 USB 设备列表
/// 返回 List<Map>,每个 Map 包含 deviceId、deviceName 等字段
Future
<
List
<
UsbDevice
>>
getDeviceList
()
async
{
static
Future
<
List
<
Map
<
String
,
dynamic
>>>
getDeviceList
()
async
{
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'getDeviceList'
);
return
await
_controller
.
getDeviceList
();
if
(
result
is
List
)
{
return
result
.
cast
<
Map
<
dynamic
,
dynamic
>>().
map
((
e
)
{
return
Map
<
String
,
dynamic
>.
from
(
e
);
}).
toList
();
}
return
[];
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'
UsbCameraService.
getDeviceList error:
$e
'
);
debugPrint
(
'
[UsbCameraService]
getDeviceList error:
$e
'
);
return
[];
return
[];
}
}
}
}
// ==================== 摄像头控制 ====================
/// 手动打开指定摄像头
Future
<
bool
>
openCamera
(
int
deviceId
,
{
CameraRequest
?
request
})
async
{
/// 打开摄像头(由 PlatformView 创建时自动触发)
/// 通常不需要手动调用
static
Future
<
bool
>
openCamera
({
int
?
deviceId
})
async
{
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'openCamera'
,
{
await
_controller
.
openCamera
(
if
(
deviceId
!=
null
)
'deviceId'
:
deviceId
,
deviceId
,
});
request:
request
??
const
CameraRequest
(
return
result
==
true
;
previewWidth:
640
,
previewHeight:
480
,
renderMode:
RenderMode
.
opengl
,
previewFormat:
PreviewFormat
.
mjpeg
,
),
);
return
true
;
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'
UsbCameraService.
openCamera error:
$e
'
);
debugPrint
(
'
[UsbCameraService]
openCamera error:
$e
'
);
return
false
;
return
false
;
}
}
}
}
// ==================== 摄像头控制 ====================
/// 关闭摄像头
/// 关闭摄像头
static
Future
<
bool
>
closeCamera
()
async
{
Future
<
void
>
closeCamera
()
async
{
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'closeCamera'
);
await
_controller
.
closeCamera
();
return
result
==
true
;
_isOpened
=
false
;
_isRecording
=
false
;
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'UsbCameraService.closeCamera error:
$e
'
);
debugPrint
(
'[UsbCameraService] closeCamera error:
$e
'
);
return
false
;
}
}
}
}
/// 拍照
/// 拍照
/// [path] 可选,指定照片保存路径;不传则使用默认路径
/// [customPath] 可选,指定照片保存路径;不传则自动生成
/// 返回照片文件的路径
/// 返回照片文件路径
static
Future
<
String
?>
takePhoto
({
String
?
path
})
async
{
Future
<
String
?>
takePhoto
({
String
?
customPath
})
async
{
if
(!
_isOpened
)
{
debugPrint
(
'[UsbCameraService] 摄像头未打开,无法拍照'
);
return
null
;
}
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'takePhoto'
,
{
_captureCompleter
=
Completer
<
String
?>();
if
(
path
!=
null
)
'path'
:
path
,
});
final
path
=
customPath
??
await
_generatePhotoPath
();
return
result
as
String
?;
final
success
=
await
_controller
.
captureImage
(
path:
path
);
if
(!
success
)
{
_captureCompleter
=
null
;
return
null
;
}
// 等待 onCaptureComplete 事件返回路径
final
result
=
await
_captureCompleter
!.
future
;
return
result
;
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'UsbCameraService.takePhoto error:
$e
'
);
_captureCompleter
=
null
;
debugPrint
(
'[UsbCameraService] takePhoto error:
$e
'
);
return
null
;
return
null
;
}
}
}
}
/// 开始录像
/// 开始录像
/// [path] 可选,指定视频保存路径;不传则使用默认路径
/// [customPath] 可选,指定视频保存路径
static
Future
<
bool
>
startRecord
({
String
?
path
})
async
{
Future
<
bool
>
startRecord
({
String
?
customPath
})
async
{
if
(!
_isOpened
)
{
debugPrint
(
'[UsbCameraService] 摄像头未打开,无法录像'
);
return
false
;
}
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'startRecord'
,
{
final
path
=
customPath
??
await
_generateVideoPath
();
if
(
path
!=
null
)
'path'
:
path
,
final
success
=
await
_controller
.
captureVideoStart
(
path:
path
);
});
if
(
success
)
{
return
result
==
true
;
_isRecording
=
true
;
}
return
success
;
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'
UsbCameraService.
startRecord error:
$e
'
);
debugPrint
(
'
[UsbCameraService]
startRecord error:
$e
'
);
return
false
;
return
false
;
}
}
}
}
/// 停止录像
/// 停止录像
/// 返回视频文件的路径
/// 返回视频文件路径
static
Future
<
String
?>
stopRecord
()
async
{
Future
<
String
?>
stopRecord
()
async
{
try
{
if
(!
_isRecording
)
{
final
result
=
await
_channel
.
invokeMethod
(
'stopRecord'
);
debugPrint
(
'[UsbCameraService] 当前未在录像'
);
return
result
as
String
?;
}
catch
(
e
)
{
debugPrint
(
'UsbCameraService.stopRecord error:
$e
'
);
return
null
;
return
null
;
}
}
}
/// 查询摄像头是否已打开
static
Future
<
bool
>
isCameraOpened
()
async
{
try
{
try
{
final
result
=
await
_channel
.
invokeMethod
(
'isCameraOpened'
);
_captureCompleter
=
Completer
<
String
?>();
return
result
==
true
;
final
success
=
await
_controller
.
captureVideoStop
();
if
(!
success
)
{
_captureCompleter
=
null
;
_isRecording
=
false
;
return
null
;
}
// 等待 onCaptureComplete 事件返回路径
final
result
=
await
_captureCompleter
!.
future
;
_isRecording
=
false
;
return
result
;
}
catch
(
e
)
{
}
catch
(
e
)
{
debugPrint
(
'UsbCameraService.isCameraOpened error:
$e
'
);
_captureCompleter
=
null
;
return
false
;
_isRecording
=
false
;
debugPrint
(
'[UsbCameraService] stopRecord error:
$e
'
);
return
null
;
}
}
}
}
/// 设置预览分辨率
// ==================== 路径生成 ====================
static
Future
<
bool
>
setPreviewSize
(
int
width
,
int
height
)
async
{
try
{
/// 生成照片保存路径
final
result
=
await
_channel
.
invokeMethod
(
'setPreviewSize'
,
{
Future
<
String
>
_generatePhotoPath
()
async
{
'width'
:
width
,
final
dir
=
await
_getCameraDirectory
();
'height'
:
height
,
final
timestamp
=
_timestamp
();
});
return
'
${dir.path}
/IMG_
$timestamp
.jpg'
;
return
result
==
true
;
}
}
catch
(
e
)
{
debugPrint
(
'UsbCameraService.setPreviewSize error:
$e
'
);
/// 生成视频保存路径
return
false
;
Future
<
String
>
_generateVideoPath
()
async
{
final
dir
=
await
_getCameraDirectory
();
final
timestamp
=
_timestamp
();
return
'
${dir.path}
/VID_
$timestamp
.mp4'
;
}
/// 获取 USB 摄像头存储目录
Future
<
Directory
>
_getCameraDirectory
()
async
{
final
baseDir
=
Platform
.
isAndroid
?
await
getExternalStorageDirectory
()
:
await
getApplicationDocumentsDirectory
();
final
cameraDir
=
Directory
(
'
${baseDir!.path}
/usb_camera'
);
if
(!
await
cameraDir
.
exists
())
{
await
cameraDir
.
create
(
recursive:
true
);
}
}
return
cameraDir
;
}
/// 生成时间戳字符串
String
_timestamp
()
{
final
now
=
DateTime
.
now
();
return
'
${now.year}${_pad(now.month)}${_pad(now.day)}
_'
'
${_pad(now.hour)}${_pad(now.minute)}${_pad(now.second)}
'
;
}
String
_pad
(
int
value
)
=>
value
.
toString
().
padLeft
(
2
,
'0'
);
// ==================== 销毁 ====================
/// 释放所有资源
Future
<
void
>
dispose
()
async
{
_captureCompleter
=
null
;
_isOpened
=
false
;
_isRecording
=
false
;
await
_eventSub
?.
cancel
();
_eventSub
=
null
;
await
_eventController
.
close
();
await
_controller
.
dispose
();
debugPrint
(
'[UsbCameraService] 资源已释放'
);
}
}
}
}
lib/views/home/index/widgets/camera_controls.dart
View file @
cf3b74d6
...
@@ -7,12 +7,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
...
@@ -7,12 +7,16 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
/// 职责:
/// 职责:
/// - 提供拍照、录像、刷新三个操作按钮
/// - 提供拍照、录像、刷新三个操作按钮
/// - 录像按钮支持录制中/停止录制两种状态切换
/// - 录像按钮支持录制中/停止录制两种状态切换
/// - 摄像头未就绪时按钮置灰不可操作
///
///
/// 涉及页面:首页监护舱页面 - 监控画面底部控制栏
/// 涉及页面:首页监护舱页面 - 监控画面底部控制栏
class
CameraControls
extends
StatelessWidget
{
class
CameraControls
extends
StatelessWidget
{
/// 是否正在录像
/// 是否正在录像
final
bool
isRecording
;
final
bool
isRecording
;
/// 摄像头是否已就绪(已打开)
final
bool
cameraReady
;
/// 拍照回调
/// 拍照回调
final
VoidCallback
?
onTakePhoto
;
final
VoidCallback
?
onTakePhoto
;
...
@@ -25,6 +29,7 @@ class CameraControls extends StatelessWidget {
...
@@ -25,6 +29,7 @@ class CameraControls extends StatelessWidget {
const
CameraControls
({
const
CameraControls
({
super
.
key
,
super
.
key
,
this
.
isRecording
=
false
,
this
.
isRecording
=
false
,
this
.
cameraReady
=
false
,
this
.
onTakePhoto
,
this
.
onTakePhoto
,
this
.
onToggleRecord
,
this
.
onToggleRecord
,
this
.
onRefresh
,
this
.
onRefresh
,
...
@@ -39,19 +44,22 @@ class CameraControls extends StatelessWidget {
...
@@ -39,19 +44,22 @@ class CameraControls extends StatelessWidget {
icon:
Icons
.
refresh
,
icon:
Icons
.
refresh
,
onPressed:
onRefresh
,
onPressed:
onRefresh
,
tooltip:
'刷新画面'
,
tooltip:
'刷新画面'
,
enabled:
true
,
// 刷新按钮始终可用
),
),
SizedBox
(
width:
16
.
w
),
SizedBox
(
width:
16
.
w
),
_buildControlButton
(
_buildControlButton
(
icon:
Icons
.
camera_alt
,
icon:
Icons
.
camera_alt
,
onPressed:
onTakePhoto
,
onPressed:
cameraReady
?
onTakePhoto
:
null
,
tooltip:
'拍照'
,
tooltip:
'拍照'
,
enabled:
cameraReady
,
),
),
SizedBox
(
width:
16
.
w
),
SizedBox
(
width:
16
.
w
),
_buildControlButton
(
_buildControlButton
(
icon:
isRecording
?
Icons
.
stop
:
Icons
.
videocam
,
icon:
isRecording
?
Icons
.
stop
:
Icons
.
videocam
,
onPressed:
onToggleRecord
,
onPressed:
cameraReady
?
onToggleRecord
:
null
,
tooltip:
isRecording
?
'停止录像'
:
'开始录像'
,
tooltip:
isRecording
?
'停止录像'
:
'开始录像'
,
isActive:
isRecording
,
isActive:
isRecording
,
enabled:
cameraReady
,
),
),
],
],
);
);
...
@@ -63,41 +71,49 @@ class CameraControls extends StatelessWidget {
...
@@ -63,41 +71,49 @@ class CameraControls extends StatelessWidget {
VoidCallback
?
onPressed
,
VoidCallback
?
onPressed
,
String
?
tooltip
,
String
?
tooltip
,
bool
isActive
=
false
,
bool
isActive
=
false
,
bool
enabled
=
true
,
})
{
})
{
final
effectiveOpacity
=
enabled
?
1.0
:
0.35
;
return
Tooltip
(
return
Tooltip
(
message:
tooltip
??
''
,
message:
tooltip
??
''
,
child:
GestureDetector
(
child:
GestureDetector
(
onTap:
()
{
onTap:
enabled
HapticFeedback
.
lightImpact
();
?
()
{
onPressed
?.
call
();
HapticFeedback
.
lightImpact
();
},
onPressed
?.
call
();
child:
AnimatedContainer
(
}
duration:
const
Duration
(
milliseconds:
200
),
:
null
,
width:
60
.
w
,
child:
Opacity
(
height:
60
.
w
,
opacity:
effectiveOpacity
,
decoration:
BoxDecoration
(
child:
AnimatedContainer
(
color:
isActive
duration:
const
Duration
(
milliseconds:
200
),
?
const
Color
(
0xCCFF4444
)
width:
60
.
w
,
:
const
Color
(
0x331E10B6
),
height:
60
.
w
,
borderRadius:
BorderRadius
.
circular
(
30
.
r
),
decoration:
BoxDecoration
(
border:
Border
.
all
(
color:
isActive
color:
isActive
?
Colors
.
redAccent
:
Colors
.
white24
,
?
const
Color
(
0xCCFF4444
)
width:
1
,
:
const
Color
(
0x331E10B6
),
borderRadius:
BorderRadius
.
circular
(
30
.
r
),
border:
Border
.
all
(
color:
isActive
?
Colors
.
redAccent
:
Colors
.
white24
,
width:
1
,
),
boxShadow:
isActive
?
[
BoxShadow
(
color:
Colors
.
redAccent
.
withValues
(
alpha:
0.4
),
blurRadius:
12
.
r
,
spreadRadius:
2
,
),
]
:
null
,
),
child:
Icon
(
icon
,
color:
Colors
.
white
,
size:
32
.
w
,
),
),
boxShadow:
isActive
?
[
BoxShadow
(
color:
Colors
.
redAccent
.
withValues
(
alpha:
0.4
),
blurRadius:
12
.
r
,
spreadRadius:
2
,
),
]
:
null
,
),
child:
Icon
(
icon
,
color:
Colors
.
white
,
size:
32
.
w
,
),
),
),
),
),
),
...
...
lib/views/home/index/widgets/monitoring_screen.dart
View file @
cf3b74d6
import
'dart:async'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:flutter_usbcamera/flutter_usbcamera.dart'
;
import
'package:laki_icu_app/services/usb_camera_service.dart'
;
import
'package:laki_icu_app/services/usb_camera_service.dart'
;
import
'camera_controls.dart'
;
import
'camera_controls.dart'
;
...
@@ -8,9 +10,9 @@ import 'camera_controls.dart';
...
@@ -8,9 +10,9 @@ import 'camera_controls.dart';
/// 监控画面组件 —— 集成 USB 摄像头预览画面
/// 监控画面组件 —— 集成 USB 摄像头预览画面
///
///
/// 职责:
/// 职责:
/// - 通过 AndroidView 嵌入 Android 原生 USBCameraTextureView 预览画面
/// - 通过 [UsbCameraPreview] 嵌入 USB 摄像头实时预览画面
/// - 监听 [UsbCameraService.eventStream] 处理设备热插拔和状态变化
/// - 底部控制栏:拍照、录像、刷新按钮
/// - 底部控制栏:拍照、录像、刷新按钮
/// - 通过 [UsbCameraService] 与原生层通信
///
///
/// 涉及页面:首页监护舱页面
/// 涉及页面:首页监护舱页面
class
MonitoringScreen
extends
StatefulWidget
{
class
MonitoringScreen
extends
StatefulWidget
{
...
@@ -30,13 +32,103 @@ class MonitoringScreen extends StatefulWidget {
...
@@ -30,13 +32,103 @@ class MonitoringScreen extends StatefulWidget {
}
}
class
_MonitoringScreenState
extends
State
<
MonitoringScreen
>
{
class
_MonitoringScreenState
extends
State
<
MonitoringScreen
>
{
bool
_isRecording
=
false
;
final
UsbCameraService
_cameraService
=
UsbCameraService
.
instance
;
StreamSubscription
<
CameraEvent
>?
_eventSub
;
bool
_cameraReady
=
false
;
bool
_cameraReady
=
false
;
bool
_isRecording
=
false
;
@override
void
initState
()
{
super
.
initState
();
_initCamera
();
}
Future
<
void
>
_initCamera
()
async
{
// 监听摄像头事件流
_eventSub
=
_cameraService
.
eventStream
.
listen
(
_onCameraEvent
);
// 初始化 USB 监听
await
_cameraService
.
initialize
();
// 检查是否已有设备连接
_refreshState
();
}
void
_onCameraEvent
(
CameraEvent
event
)
{
if
(!
mounted
)
return
;
switch
(
event
.
type
)
{
case
CameraEventType
.
onAttachDev
:
// 设备插入
setState
(()
{});
break
;
case
CameraEventType
.
onDetachDev
:
case
CameraEventType
.
onDisConnectDev
:
// 设备拔出
setState
(()
{
_cameraReady
=
false
;
_isRecording
=
false
;
});
break
;
case
CameraEventType
.
onConnectDev
:
// 权限通过,摄像头即将打开
setState
(()
{});
break
;
case
CameraEventType
.
onCancelDev
:
// 权限被拒绝
setState
(()
{
_cameraReady
=
false
;
});
break
;
case
CameraEventType
.
onCameraState
:
if
(
event
.
state
==
'opened'
)
{
setState
(()
=>
_cameraReady
=
true
);
}
else
if
(
event
.
state
==
'closed'
||
event
.
state
==
'error'
)
{
setState
(()
{
_cameraReady
=
false
;
_isRecording
=
false
;
});
}
break
;
case
CameraEventType
.
onCaptureComplete
:
setState
(()
=>
_isRecording
=
false
);
if
(
event
.
path
!=
null
&&
mounted
)
{
final
isVideo
=
event
.
captureType
==
'video'
||
(
event
.
path
?.
endsWith
(
'.mp4'
)
??
false
);
_showSnackBar
(
isVideo
?
'录像已保存'
:
'拍照成功'
);
}
break
;
case
CameraEventType
.
onCaptureError
:
setState
(()
=>
_isRecording
=
false
);
if
(
mounted
)
{
_showSnackBar
(
'拍摄失败:
${event.error ?? '未知错误'}
'
);
}
break
;
default
:
break
;
}
}
void
_refreshState
()
{
setState
(()
{
_cameraReady
=
_cameraService
.
isOpened
;
_isRecording
=
_cameraService
.
isRecording
;
});
}
@override
@override
void
dispose
()
{
void
dispose
()
{
// 退出页面时关闭摄像头
_eventSub
?.
cancel
();
UsbCameraService
.
closeCamera
();
_cameraService
.
dispose
();
super
.
dispose
();
super
.
dispose
();
}
}
...
@@ -56,7 +148,7 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
...
@@ -56,7 +148,7 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
children:
[
children:
[
// 摄像头预览画面
// 摄像头预览画面
_buildCameraPreview
(),
_buildCameraPreview
(),
// 摄像头未就绪时的提示
// 摄像头未就绪时的
占位
提示
if
(!
_cameraReady
)
_buildPlaceholder
(),
if
(!
_cameraReady
)
_buildPlaceholder
(),
// 底部控制按钮
// 底部控制按钮
Positioned
(
Positioned
(
...
@@ -64,9 +156,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
...
@@ -64,9 +156,10 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
right:
24
.
w
,
right:
24
.
w
,
child:
CameraControls
(
child:
CameraControls
(
isRecording:
_isRecording
,
isRecording:
_isRecording
,
cameraReady:
_cameraReady
,
onTakePhoto:
_onTakePhoto
,
onTakePhoto:
_onTakePhoto
,
onToggleRecord:
_onToggleRecord
,
onToggleRecord:
_onToggleRecord
,
onRefresh:
widget
.
onRefresh
,
onRefresh:
_
onRefresh
,
),
),
),
),
],
],
...
@@ -74,22 +167,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
...
@@ -74,22 +167,11 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
);
);
}
}
/// 构建 USB 摄像头预览画面(通过
PlatformView
)
/// 构建 USB 摄像头预览画面(通过
flutter_usbcamera 插件
)
Widget
_buildCameraPreview
()
{
Widget
_buildCameraPreview
()
{
return
ClipRRect
(
return
ClipRRect
(
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
borderRadius:
BorderRadius
.
circular
(
20
.
r
),
child:
SizedBox
.
expand
(
child:
const
UsbCameraPreview
(),
child:
AndroidView
(
viewType:
'usb_camera_view'
,
onPlatformViewCreated:
(
_
)
{
setState
(()
{
_cameraReady
=
true
;
});
},
creationParams:
const
<
String
,
dynamic
>{},
creationParamsCodec:
const
StandardMessageCodec
(),
),
),
);
);
}
}
...
@@ -128,31 +210,49 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
...
@@ -128,31 +210,49 @@ class _MonitoringScreenState extends State<MonitoringScreen> {
/// 拍照
/// 拍照
Future
<
void
>
_onTakePhoto
()
async
{
Future
<
void
>
_onTakePhoto
()
async
{
final
path
=
await
UsbCameraService
.
takePhoto
();
if
(!
_cameraReady
)
{
_showSnackBar
(
'摄像头未就绪'
);
return
;
}
final
path
=
await
_cameraService
.
takePhoto
();
if
(
path
!=
null
&&
mounted
)
{
if
(
path
!=
null
&&
mounted
)
{
_showSnackBar
(
'拍照成功
:
$path
'
);
_showSnackBar
(
'拍照成功'
);
}
}
widget
.
onSnapshot
?.
call
();
widget
.
onSnapshot
?.
call
();
}
}
/// 切换录像状态
/// 切换录像状态
Future
<
void
>
_onToggleRecord
()
async
{
Future
<
void
>
_onToggleRecord
()
async
{
if
(!
_cameraReady
)
{
_showSnackBar
(
'摄像头未就绪'
);
return
;
}
if
(
_isRecording
)
{
if
(
_isRecording
)
{
final
path
=
await
UsbC
ameraService
.
stopRecord
();
final
path
=
await
_c
ameraService
.
stopRecord
();
if
(
path
!=
null
&&
mounted
)
{
if
(
path
!=
null
&&
mounted
)
{
_showSnackBar
(
'录像已保存
:
$path
'
);
_showSnackBar
(
'录像已保存'
);
}
}
setState
(()
=>
_isRecording
=
false
);
setState
(()
=>
_isRecording
=
false
);
}
else
{
}
else
{
final
success
=
await
UsbC
ameraService
.
startRecord
();
final
success
=
await
_c
ameraService
.
startRecord
();
if
(
success
)
{
if
(
success
)
{
setState
(()
=>
_isRecording
=
true
);
setState
(()
=>
_isRecording
=
true
);
if
(
mounted
)
_showSnackBar
(
'开始录像'
);
if
(
mounted
)
_showSnackBar
(
'开始录像'
);
}
else
{
if
(
mounted
)
_showSnackBar
(
'开始录像失败'
);
}
}
}
}
widget
.
onRecord
?.
call
();
widget
.
onRecord
?.
call
();
}
}
/// 刷新画面
void
_onRefresh
()
{
_refreshState
();
widget
.
onRefresh
?.
call
();
}
void
_showSnackBar
(
String
message
)
{
void
_showSnackBar
(
String
message
)
{
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
ScaffoldMessenger
.
of
(
context
).
showSnackBar
(
SnackBar
(
SnackBar
(
...
...
lib/views/login/login_view.dart
View file @
cf3b74d6
...
@@ -102,7 +102,7 @@ class LoginView extends StatelessWidget {
...
@@ -102,7 +102,7 @@ class LoginView extends StatelessWidget {
),
),
SizedBox
(
height:
40
.
h
),
SizedBox
(
height:
40
.
h
),
const
Text
(
const
Text
(
'
智能酒店管理系统
'
,
'
Laki icu 智能仓
'
,
style:
TextStyle
(
style:
TextStyle
(
color:
Color
(
0xFF333333
),
color:
Color
(
0xFF333333
),
fontSize:
24
,
fontSize:
24
,
...
...
pubspec.yaml
View file @
cf3b74d6
...
@@ -51,6 +51,7 @@ dependencies:
...
@@ -51,6 +51,7 @@ dependencies:
fluttertoast
:
^9.0.0
fluttertoast
:
^9.0.0
permission_handler
:
^11.3.1
permission_handler
:
^11.3.1
flutter_usbcamera
:
^0.0.1
flutter_usbcamera
:
^0.0.1
path_provider
:
^2.1.2
# flutter_usbcamera: ^0.0.1
# flutter_usbcamera: ^0.0.1
# fl_chart: ^0.71.0
# fl_chart: ^0.71.0
# fl_chart: ^1.1.0
# fl_chart: ^1.1.0
...
...
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