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

View File

@ -46,4 +46,132 @@ class AppPermission {
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_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:starwork_flutter/common/widgets/custom_dialog_widget.dart';
class BaseController extends GetxController {
void showToast(String message) {
@ -24,6 +25,24 @@ class BaseController extends GetxController {
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
void onClose() {
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 {
switch (appFlavor) {
case Flavor.skyDev:
return 'http://10.0.2.2/api';
return 'http://192.168.1.136/api';
case Flavor.skyPre:
return 'https://loacl.work.star-lock.cn/api';
case Flavor.skyRelease:

View File

@ -1,5 +1,7 @@
import 'package:get/get.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_view.dart';
import 'package:starwork_flutter/views/login/forgotPassword/forgot_password_binding.dart';
@ -60,5 +62,10 @@ class AppPages {
page: () => const SetNewPasswordView(),
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 forgotPassword = '/forgotPassword';
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:get/get.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:starwork_flutter/views/home/widget/home_attendance_chart_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(
width: 14.w,
),
Icon(
Icons.cancel,
color: const Color(0xFFEE9846),
size: 18.sp,
GestureDetector(
onTap: () {
controller.isOpenNotificationPermission.value = true;
},
child: Icon(
Icons.cancel,
color: const Color(0xFFEE9846),
size: 18.sp,
),
)
],
),
@ -263,12 +269,12 @@ class HomeView extends GetView<HomeController> {
{
'title': '搜索设备',
'icon': Icons.sensors_rounded,
'onTap': () => _handleMenuTap('添加设备'),
'onTap': () => _handleMenuTap(0),
},
{
'title': '加入团队',
'icon': Icons.group_add,
'onTap': () => _handleMenuTap('创建群组'),
'onTap': () => _handleMenuTap(1),
},
];
@ -329,7 +335,9 @@ class HomeView extends GetView<HomeController> {
size: 18.sp,
color: Colors.black87,
),
SizedBox(width: 8.w,),
SizedBox(
width: 8.w,
),
Flexible(
child: Text(
title,
@ -348,12 +356,13 @@ class HomeView extends GetView<HomeController> {
}
///
void _handleMenuTap(String menuTitle) {
Get.snackbar(
'提示',
'点击了:$menuTitle',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 2),
);
void _handleMenuTap(int menuIndex) {
switch (menuIndex) {
case 0:
Get.toNamed(AppRoutes.searchDevice);
break;
case 1:
break;
}
}
}

View File

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

View File

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