feat: 增加弹出气泡、搜索设备页面

This commit is contained in:
liyi 2025-09-03 17:05:28 +08:00
parent 64f1a28997
commit 8501b5ea48
15 changed files with 545 additions and 151 deletions

View File

@ -11,6 +11,21 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 系统通知 --> <!-- 系统通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 蓝牙相关权限 -->
<!-- Android 12以下需要的蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android 12及以上需要的蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 位置权限Android 12以下蓝牙扫描需要 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -11,7 +11,7 @@ import 'package:starwork_flutter/routes/app_pages.dart';
import 'package:starwork_flutter/routes/app_routes.dart'; import 'package:starwork_flutter/routes/app_routes.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
App({super.key, required this.initialRoute}); App({super.key, required this.initialRoute});
String initialRoute; String initialRoute;
@ -29,15 +29,17 @@ class _AppState extends State<App> {
builder: (_, child) { builder: (_, child) {
return GetMaterialApp( return GetMaterialApp(
theme: ThemeData( theme: ThemeData(
scaffoldBackgroundColor: Colors.white, scaffoldBackgroundColor: Colors.white,
brightness: Brightness.light, brightness: Brightness.light,
textSelectionTheme: TextSelectionThemeData( textSelectionTheme: TextSelectionThemeData(
cursorColor: Colors.blue.shade200, cursorColor: Colors.blue.shade200,
selectionColor: Colors.blue.shade100, selectionColor: Colors.blue.shade100,
selectionHandleColor: Colors.blue.shade300, selectionHandleColor: Colors.blue.shade300,
), ),
dialogBackgroundColor: Colors.white, dialogBackgroundColor: Colors.white,
), appBarTheme: AppBarTheme(
backgroundColor: Colors.white,
)),
// //
localizationsDelegates: const [ localizationsDelegates: const [
GlobalMaterialLocalizations.delegate, // Material组件本地化字符串 GlobalMaterialLocalizations.delegate, // Material组件本地化字符串

View File

@ -46,4 +46,132 @@ class AppPermission {
return false; return false;
} }
// Android 12
static Future<bool> requestBluetoothPermissions() async {
try {
// Android 12
List<Permission> bluetoothPermissions = [
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.bluetoothAdvertise,
];
// Android 12
List<Permission> locationPermissions = [
Permission.location,
Permission.locationWhenInUse,
];
bool bluetoothGranted = true;
bool locationGranted = true;
// Android 12+
Map<Permission, PermissionStatus> bluetoothStatuses = await bluetoothPermissions.request();
for (var status in bluetoothStatuses.values) {
if (status != PermissionStatus.granted) {
bluetoothGranted = false;
break;
}
}
// Android 12
Map<Permission, PermissionStatus> locationStatuses = await locationPermissions.request();
for (var status in locationStatuses.values) {
if (status != PermissionStatus.granted) {
locationGranted = false;
break;
}
}
bool hasPermission = bluetoothGranted && locationGranted;
if (hasPermission) {
print("蓝牙权限已授予");
} else {
print("蓝牙权限被拒绝");
}
return hasPermission;
} catch (e) {
print("请求蓝牙权限失败: $e");
return false;
}
}
//
static Future<bool> checkBluetoothScanPermission() async {
try {
var status = await Permission.bluetoothScan.status;
return status == PermissionStatus.granted;
} catch (e) {
// Android 12
return await checkLocationPermission();
}
}
//
static Future<bool> checkBluetoothConnectPermission() async {
try {
var status = await Permission.bluetoothConnect.status;
return status == PermissionStatus.granted;
} catch (e) {
// Android 12true
print("蓝牙连接权限检查失败可能是Android 12以下版本: $e");
return true;
}
}
// 广
static Future<bool> checkBluetoothAdvertisePermission() async {
try {
var status = await Permission.bluetoothAdvertise.status;
return status == PermissionStatus.granted;
} catch (e) {
// Android 12true
print("蓝牙广播权限检查失败可能是Android 12以下版本: $e");
return true;
}
}
// Android 12
static Future<bool> checkLocationPermission() async {
var status = await Permission.location.status;
return status == PermissionStatus.granted;
}
//
static Future<bool> requestLocationPermission() async {
try {
final PermissionStatus status = await Permission.location.request();
if (status == PermissionStatus.granted) {
print("位置权限已授予");
return true;
} else if (status == PermissionStatus.denied) {
print("位置权限被拒绝");
return false;
} else if (status.isPermanentlyDenied) {
print("位置权限被永久拒绝,跳转到设置页面");
openAppSettings();
return false;
}
return false;
} catch (e) {
print("请求位置权限失败: $e");
return false;
}
}
//
static Future<bool> checkAllBluetoothPermissions() async {
bool scanPermission = await checkBluetoothScanPermission();
bool connectPermission = await checkBluetoothConnectPermission();
bool advertisePermission = await checkBluetoothAdvertisePermission();
bool locationPermission = await checkLocationPermission();
return scanPermission && connectPermission && advertisePermission && locationPermission;
}
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/common/widgets/custom_dialog_widget.dart';
class BaseController extends GetxController { class BaseController extends GetxController {
void showToast(String message) { void showToast(String message) {
@ -24,6 +25,24 @@ class BaseController extends GetxController {
EasyLoading.showError(message.tr); EasyLoading.showError(message.tr);
} }
void showCustomDialog({
required String title,
required Widget content,
required VoidCallback onConfirm,
String? confirmText
}) {
Get.dialog(
CustomDialogWidget(
title: title,
content: content,
onConfirm: onConfirm,
confirmText: confirmText,
),
barrierDismissible: false, //
useSafeArea: true, //
);
}
@override @override
void onClose() { void onClose() {
if (EasyLoading.isShow) { if (EasyLoading.isShow) {

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class CustomDialogWidget extends StatefulWidget {
CustomDialogWidget({
super.key,
required this.title,
required this.content,
required this.onConfirm,
String? confirmText,
}) : confirmText = confirmText ?? '确认'.tr;
final String title;
final Widget content;
final VoidCallback onConfirm;
final String confirmText;
@override
State<CustomDialogWidget> createState() => _CustomDialogWidgetState();
}
class _CustomDialogWidgetState extends State<CustomDialogWidget> {
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14.r), //
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 18.h),
Text(
widget.title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 10.h),
widget.content,
SizedBox(height: 10.h),
Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(
color: Colors.grey, //
width: 0.5, //
),
),
),
child: Row(
children: [
Expanded(
//
child: GestureDetector(
onTap: () {
Get.back();
},
child: Container(
alignment: Alignment.center,
height: 42.h,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: Colors.grey, //
width: 0.5, //
),
),
),
child: Text(
'取消'.tr,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
),
),
),
Expanded(
//
child: GestureDetector(
onTap: () {
Get.back();
widget.onConfirm();
},
child: Container(
alignment: Alignment.center,
height: 42.h,
child: Text(
widget.confirmText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: Colors.blue,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
),
)
],
),
);
}
}

View File

@ -32,7 +32,7 @@ class F {
static String get apiHost { static String get apiHost {
switch (appFlavor) { switch (appFlavor) {
case Flavor.skyDev: case Flavor.skyDev:
return 'http://10.0.2.2/api'; return 'http://192.168.1.136/api';
case Flavor.skyPre: case Flavor.skyPre:
return 'https://loacl.work.star-lock.cn/api'; return 'https://loacl.work.star-lock.cn/api';
case Flavor.skyRelease: case Flavor.skyRelease:

View File

@ -1,5 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/routes/app_routes.dart'; import 'package:starwork_flutter/routes/app_routes.dart';
import 'package:starwork_flutter/views/device/searchDevice/search_device_binding.dart';
import 'package:starwork_flutter/views/device/searchDevice/search_device_view.dart';
import 'package:starwork_flutter/views/home/home_binding.dart'; import 'package:starwork_flutter/views/home/home_binding.dart';
import 'package:starwork_flutter/views/home/home_view.dart'; import 'package:starwork_flutter/views/home/home_view.dart';
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_binding.dart'; import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_binding.dart';
@ -60,5 +62,10 @@ class AppPages {
page: () => const SetNewPasswordView(), page: () => const SetNewPasswordView(),
binding: SetNewPasswordBinding(), binding: SetNewPasswordBinding(),
), ),
GetPage(
name: AppRoutes.searchDevice,
page: () => const SearchDeviceView(),
binding: SearchDeviceBinding(),
),
]; ];
} }

View File

@ -8,4 +8,5 @@ class AppRoutes{
static const String inputVerificationCode = '/inputVerificationCode'; static const String inputVerificationCode = '/inputVerificationCode';
static const String forgotPassword = '/forgotPassword'; static const String forgotPassword = '/forgotPassword';
static const String setNewPassword = '/setNewPassword'; static const String setNewPassword = '/setNewPassword';
static const String searchDevice = '/searchDevice';
} }

View File

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import 'package:starwork_flutter/views/device/searchDevice/search_device_controller.dart';
class SearchDeviceBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SearchDeviceController>(() => SearchDeviceController());
}
}

View File

@ -0,0 +1,22 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:starwork_flutter/base/app_permission.dart';
import 'package:starwork_flutter/base/base_controller.dart';
class SearchDeviceController extends BaseController {
//
final RxBool _isSearching = false.obs;
// Getter
bool get isSearching => _isSearching.value;
@override
void onInit() async {
super.onInit();
}
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:starwork_flutter/views/device/searchDevice/search_device_controller.dart';
import 'package:starwork_flutter/views/device/searchDevice/widget/search_device_rotating_icon_widget.dart';
class SearchDeviceView extends GetView<SearchDeviceController> {
const SearchDeviceView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Obx(() => Text(
controller.isSearching ? '搜索设备中'.tr : '搜索设备'.tr,
)),
SizedBox(
width: 8.w,
),
Obx(
() => SearchDeviceRotatingIconWidget(
isRotating: controller.isSearching,
radius: 10.w,
rotationDuration: 1500,
),
),
],
),
),
body: Container(
// TODO:
child: Center(
child: Text('设备搜索页面'),
),
),
);
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class SearchDeviceRotatingIconWidget extends StatefulWidget {
///
final bool isRotating;
///
final int rotationDuration;
///
final Color? color;
///
final double? radius;
const SearchDeviceRotatingIconWidget({
super.key,
required this.isRotating,
this.rotationDuration = 1000,
this.color = Colors.grey,
this.radius,
});
@override
_SearchDeviceRotatingIconWidgetState createState() => _SearchDeviceRotatingIconWidgetState();
}
class _SearchDeviceRotatingIconWidgetState extends State<SearchDeviceRotatingIconWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: Duration(milliseconds: widget.rotationDuration),
vsync: this,
);
//
if (widget.isRotating) {
_animationController.repeat();
}
}
@override
void didUpdateWidget(SearchDeviceRotatingIconWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// isRotating状态发生变化时
if (widget.isRotating != oldWidget.isRotating) {
if (widget.isRotating) {
_startRotation();
} else {
_stopRotation();
}
}
//
if (widget.rotationDuration != oldWidget.rotationDuration) {
_animationController.duration = Duration(milliseconds: widget.rotationDuration);
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
///
void _startRotation() {
if (!_animationController.isAnimating) {
_animationController.repeat();
}
}
///
void _stopRotation() {
if (_animationController.isAnimating) {
_animationController.stop();
}
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.rotate(
angle: _animationController.value * 2 * 3.14159,
child: CupertinoActivityIndicator(
radius: widget.radius ?? 10.w,
color: widget.color,
),
);
},
);
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/base/app_permission.dart'; import 'package:starwork_flutter/base/app_permission.dart';
import 'package:starwork_flutter/routes/app_routes.dart';
import 'package:super_tooltip/super_tooltip.dart'; import 'package:super_tooltip/super_tooltip.dart';
import 'package:starwork_flutter/views/home/widget/home_attendance_chart_area_widget.dart'; import 'package:starwork_flutter/views/home/widget/home_attendance_chart_area_widget.dart';
import 'package:starwork_flutter/views/home/widget/home_carousel_area_widget.dart'; import 'package:starwork_flutter/views/home/widget/home_carousel_area_widget.dart';
@ -175,10 +176,15 @@ class HomeView extends GetView<HomeController> {
SizedBox( SizedBox(
width: 14.w, width: 14.w,
), ),
Icon( GestureDetector(
Icons.cancel, onTap: () {
color: const Color(0xFFEE9846), controller.isOpenNotificationPermission.value = true;
size: 18.sp, },
child: Icon(
Icons.cancel,
color: const Color(0xFFEE9846),
size: 18.sp,
),
) )
], ],
), ),
@ -263,12 +269,12 @@ class HomeView extends GetView<HomeController> {
{ {
'title': '搜索设备', 'title': '搜索设备',
'icon': Icons.sensors_rounded, 'icon': Icons.sensors_rounded,
'onTap': () => _handleMenuTap('添加设备'), 'onTap': () => _handleMenuTap(0),
}, },
{ {
'title': '加入团队', 'title': '加入团队',
'icon': Icons.group_add, 'icon': Icons.group_add,
'onTap': () => _handleMenuTap('创建群组'), 'onTap': () => _handleMenuTap(1),
}, },
]; ];
@ -329,7 +335,9 @@ class HomeView extends GetView<HomeController> {
size: 18.sp, size: 18.sp,
color: Colors.black87, color: Colors.black87,
), ),
SizedBox(width: 8.w,), SizedBox(
width: 8.w,
),
Flexible( Flexible(
child: Text( child: Text(
title, title,
@ -348,12 +356,13 @@ class HomeView extends GetView<HomeController> {
} }
/// ///
void _handleMenuTap(String menuTitle) { void _handleMenuTap(int menuIndex) {
Get.snackbar( switch (menuIndex) {
'提示', case 0:
'点击了:$menuTitle', Get.toNamed(AppRoutes.searchDevice);
snackPosition: SnackPosition.TOP, break;
duration: const Duration(seconds: 2), case 1:
); break;
}
} }
} }

View File

@ -72,127 +72,44 @@ class LoginController extends BaseController {
required String content, required String content,
required VoidCallback onConfirm, required VoidCallback onConfirm,
}) { }) {
Get.dialog( showCustomDialog(
Dialog( title: title,
backgroundColor: Colors.white, content: RichText(
shape: RoundedRectangleBorder( text: TextSpan(
borderRadius: BorderRadius.circular(14.r), //
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( TextSpan(
padding: EdgeInsets.only(top: 22.h), text: '请你阅读并同意'.tr,
child: Text( style: TextStyle(
title, fontSize: 12.sp,
style: TextStyle( color: Colors.grey,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
), ),
), ),
Padding( TextSpan(
padding: EdgeInsets.all(22.w), text: '《用户协议》'.tr,
child: RichText( style: TextStyle(
text: TextSpan( fontSize: 12.sp,
children: [ color: Colors.blue,
TextSpan(
text: '请你阅读并同意'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
TextSpan(
text: '《用户协议》'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
),
),
TextSpan(
text: ''.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
TextSpan(
text: '《隐私政策》'.tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue,
),
),
],
),
), ),
), ),
SizedBox(height: 20.h), TextSpan(
Container( text: ''.tr,
decoration: const BoxDecoration( style: TextStyle(
border: Border( fontSize: 12.sp,
top: BorderSide( color: Colors.grey,
color: Colors.grey, //
width: 0.5, //
),
),
), ),
child: Row( ),
children: [ TextSpan(
Expanded( text: '《隐私政策》'.tr,
// style: TextStyle(
child: GestureDetector( fontSize: 12.sp,
onTap: () { color: Colors.blue,
Get.back();
},
child: Container(
alignment: Alignment.center,
height: 42.h,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: Colors.grey, //
width: 0.5, //
),
),
),
child: Text(
'取消'.tr,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
),
),
),
Expanded(
//
child: GestureDetector(
onTap: () {
Get.back();
onConfirm();
},
child: Container(
alignment: Alignment.center,
height: 42.h,
child: Text(
'同意并继续'.tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: Colors.blue,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
), ),
) ),
], ],
), ),
), ),
onConfirm: onConfirm,
confirmText: '同意并继续'.tr,
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:starwork_flutter/base/app_permission.dart'; import 'package:starwork_flutter/base/app_permission.dart';
@ -201,10 +202,16 @@ class MessagesView extends GetView<MessagesController> {
SizedBox( SizedBox(
width: 14.w, width: 14.w,
), ),
Icon( GestureDetector(
Icons.cancel, onTap: () {
color: const Color(0xFFEE9846), controller.homeController.isOpenNotificationPermission.value =
size: 18.sp, true;
},
child: Icon(
Icons.cancel,
color: const Color(0xFFEE9846),
size: 18.sp,
),
) )
], ],
), ),
@ -217,25 +224,31 @@ class MessagesView extends GetView<MessagesController> {
return RefreshIndicator( return RefreshIndicator(
onRefresh: _onRefresh, onRefresh: _onRefresh,
// //
color: const Color(0xFF4A90E2), // color: const Color(0xFF4A90E2),
backgroundColor: Colors.white, // //
backgroundColor: Colors.white,
//
// //
displacement: 60.0, // 40.0 displacement: 60.0,
edgeOffset: 0.0, // 0.0 // 40.0
edgeOffset: 0.0,
// 0.0
// //
triggerMode: RefreshIndicatorTriggerMode.onEdge, // triggerMode: RefreshIndicatorTriggerMode.onEdge,
//
// RefreshIndicatorTriggerMode.onEdge: // RefreshIndicatorTriggerMode.onEdge:
// RefreshIndicatorTriggerMode.anywhere: // RefreshIndicatorTriggerMode.anywhere:
// //
strokeWidth: 2.5, // 2.0 strokeWidth: 2.5,
// 2.0
// //
semanticsLabel: '下拉刷新消息列表', semanticsLabel: '下拉刷新消息列表',
semanticsValue: '刷新中...', semanticsValue: '刷新中...',
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics( physics: const AlwaysScrollableScrollPhysics(
// //