21 changed files with 1111 additions and 540 deletions
@ -1,3 +1,24 @@ |
|||||||
# problem_check_system |
# problem_check_system |
||||||
|
|
||||||
A new Flutter project. |
这个应用需要实现以下功能: |
||||||
|
|
||||||
|
离线登录系统 |
||||||
|
|
||||||
|
问题数据收集(描述、位置、图片等) |
||||||
|
|
||||||
|
本地数据存储 |
||||||
|
|
||||||
|
有网络时手动上传功能 |
||||||
|
|
||||||
|
技术栈 |
||||||
|
Flutter SDK |
||||||
|
|
||||||
|
GetX (状态管理、路由管理、依赖注入) |
||||||
|
|
||||||
|
SQFlite (本地数据库) |
||||||
|
|
||||||
|
Image Picker (图片选择) |
||||||
|
|
||||||
|
Geolocator (位置信息) |
||||||
|
|
||||||
|
HTTP/Dio (网络请求) |
||||||
|
@ -1,3 +1,8 @@ |
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError |
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError |
||||||
android.useAndroidX=true |
android.useAndroidX=true |
||||||
android.enableJetifier=true |
android.enableJetifier=true |
||||||
|
|
||||||
|
systemProp.http.proxyHost=127.0.0.1 |
||||||
|
systemProp.http.proxyPort=7890 |
||||||
|
systemProp.https.proxyHost=127.0.0.1 |
||||||
|
systemProp.https.proxyPort=7890 |
||||||
|
@ -0,0 +1,130 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:image_picker/image_picker.dart'; |
||||||
|
import '../controllers/problem_controller.dart'; |
||||||
|
import 'package:permission_handler/permission_handler.dart'; |
||||||
|
|
||||||
|
class AddProblemController extends GetxController { |
||||||
|
final ProblemController _problemController = Get.put(ProblemController()); |
||||||
|
|
||||||
|
final RxList<XFile> _selectedImages = <XFile>[].obs; |
||||||
|
final TextEditingController descriptionController = TextEditingController(); |
||||||
|
final TextEditingController locationController = TextEditingController(); |
||||||
|
final RxBool isLoading = false.obs; |
||||||
|
|
||||||
|
List<XFile> get selectedImages => _selectedImages; |
||||||
|
|
||||||
|
// 改进的 pickImage 方法 |
||||||
|
Future<void> pickImage(ImageSource source) async { |
||||||
|
try { |
||||||
|
PermissionStatus status; |
||||||
|
String permissionName; |
||||||
|
String actionName; |
||||||
|
|
||||||
|
if (source == ImageSource.camera) { |
||||||
|
permissionName = "相机"; |
||||||
|
actionName = "拍照"; |
||||||
|
status = await Permission.camera.request(); |
||||||
|
} else { |
||||||
|
permissionName = "相册"; |
||||||
|
actionName = "选择图片"; |
||||||
|
|
||||||
|
// 处理不同 Android 版本的相册权限 |
||||||
|
if (await Permission.photos.isGranted) { |
||||||
|
status = PermissionStatus.granted; |
||||||
|
} else if (await Permission.storage.isGranted) { |
||||||
|
status = PermissionStatus.granted; |
||||||
|
} else { |
||||||
|
// 请求适当的权限 |
||||||
|
status = await Permission.photos.request(); |
||||||
|
|
||||||
|
// 如果 photos 权限被拒绝,尝试请求 storage 权限 |
||||||
|
if (status.isDenied || status.isPermanentlyDenied) { |
||||||
|
status = await Permission.storage.request(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (status.isGranted) { |
||||||
|
final ImagePicker picker = ImagePicker(); |
||||||
|
final XFile? image = await picker.pickImage(source: source); |
||||||
|
|
||||||
|
if (image != null) { |
||||||
|
_selectedImages.add(image); |
||||||
|
} |
||||||
|
} else if (status.isPermanentlyDenied) { |
||||||
|
// 权限被永久拒绝,引导用户到设置 |
||||||
|
_showPermissionDeniedDialog(permissionName, actionName); |
||||||
|
} else { |
||||||
|
Get.snackbar('权限被拒绝', '需要$permissionName权限才能$actionName'); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '选择图片失败: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 显示权限被拒绝的对话框 |
||||||
|
void _showPermissionDeniedDialog(String permissionName, String actionName) { |
||||||
|
Get.dialog( |
||||||
|
AlertDialog( |
||||||
|
title: Text('权限被拒绝'), |
||||||
|
content: Text('需要$permissionName权限才能$actionName。是否要前往设置开启权限?'), |
||||||
|
actions: [ |
||||||
|
TextButton(onPressed: () => Get.back(), child: Text('取消')), |
||||||
|
TextButton( |
||||||
|
onPressed: () async { |
||||||
|
Get.back(); |
||||||
|
await openAppSettings(); |
||||||
|
}, |
||||||
|
child: Text('去设置'), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> removeImage(int index) async { |
||||||
|
_selectedImages.removeAt(index); |
||||||
|
} |
||||||
|
|
||||||
|
bool get isFormValid { |
||||||
|
return descriptionController.text.isNotEmpty && |
||||||
|
locationController.text.isNotEmpty; |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> saveProblem() async { |
||||||
|
if (descriptionController.text.isEmpty) { |
||||||
|
Get.snackbar('错误', '请填写问题描述'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isLoading.value = true; |
||||||
|
|
||||||
|
try { |
||||||
|
await _problemController.addProblem( |
||||||
|
descriptionController.text, |
||||||
|
locationController.text, |
||||||
|
_selectedImages.toList(), |
||||||
|
); |
||||||
|
|
||||||
|
// 清空表单 |
||||||
|
descriptionController.clear(); |
||||||
|
locationController.clear(); |
||||||
|
_selectedImages.clear(); |
||||||
|
|
||||||
|
Get.back(); |
||||||
|
Get.snackbar('成功', '问题已保存'); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '保存问题失败: $e'); |
||||||
|
} finally { |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onClose() { |
||||||
|
descriptionController.dispose(); |
||||||
|
locationController.dispose(); |
||||||
|
super.onClose(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:shared_preferences/shared_preferences.dart'; |
||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import '../services/local_database.dart'; |
||||||
|
|
||||||
|
class AuthController extends GetxController { |
||||||
|
final LocalDatabase _localDatabase = LocalDatabase(); |
||||||
|
final RxBool isLoggedIn = false.obs; |
||||||
|
final RxBool isOnlineMode = false.obs; |
||||||
|
final RxString username = ''.obs; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
checkLoginStatus(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> checkLoginStatus() async { |
||||||
|
final prefs = await SharedPreferences.getInstance(); |
||||||
|
final offlineLogin = prefs.getBool('offlineLogin') ?? false; |
||||||
|
isLoggedIn.value = offlineLogin; |
||||||
|
|
||||||
|
if (offlineLogin) { |
||||||
|
username.value = prefs.getString('username') ?? '离线用户'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<bool> offlineLogin(String username) async { |
||||||
|
final prefs = await SharedPreferences.getInstance(); |
||||||
|
await prefs.setBool('offlineLogin', true); |
||||||
|
await prefs.setString('username', username); |
||||||
|
|
||||||
|
isLoggedIn.value = true; |
||||||
|
this.username.value = username; |
||||||
|
isOnlineMode.value = false; |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
Future<bool> onlineLogin(String username, String password) async { |
||||||
|
try { |
||||||
|
final dio = Dio(); |
||||||
|
final response = await dio.post( |
||||||
|
'https://your-server.com/api/login', |
||||||
|
data: {'username': username, 'password': password}, |
||||||
|
); |
||||||
|
|
||||||
|
if (response.statusCode == 200) { |
||||||
|
final prefs = await SharedPreferences.getInstance(); |
||||||
|
await prefs.setBool('offlineLogin', false); |
||||||
|
await prefs.setString('username', username); |
||||||
|
|
||||||
|
isLoggedIn.value = true; |
||||||
|
this.username.value = username; |
||||||
|
isOnlineMode.value = true; |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} catch (e) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> logout() async { |
||||||
|
final prefs = await SharedPreferences.getInstance(); |
||||||
|
await prefs.remove('offlineLogin'); |
||||||
|
await prefs.remove('username'); |
||||||
|
|
||||||
|
isLoggedIn.value = false; |
||||||
|
username.value = ''; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:get/get.dart' hide MultipartFile, FormData; |
||||||
|
import 'package:image_picker/image_picker.dart'; |
||||||
|
import 'package:path_provider/path_provider.dart'; |
||||||
|
import 'dart:io'; |
||||||
|
import '../models/problem_model.dart'; |
||||||
|
import '../services/local_database.dart'; |
||||||
|
|
||||||
|
class ProblemController extends GetxController { |
||||||
|
final LocalDatabase _localDatabase = LocalDatabase(); |
||||||
|
final RxList<Problem> problems = <Problem>[].obs; |
||||||
|
final RxBool isLoading = false.obs; |
||||||
|
|
||||||
|
// 这个方法返回所有已选中的问题对象 |
||||||
|
List<Problem> get selectedProblems { |
||||||
|
return problems.where((p) => p.isChecked.value).toList(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
loadProblems(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> loadProblems() async { |
||||||
|
isLoading.value = true; |
||||||
|
problems.value = await _localDatabase.getProblems(); |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> addProblem( |
||||||
|
String description, |
||||||
|
String location, |
||||||
|
List<XFile> images, |
||||||
|
) async { |
||||||
|
try { |
||||||
|
// 保存图片到本地 |
||||||
|
final List<String> imagePaths = []; |
||||||
|
final directory = await getApplicationDocumentsDirectory(); |
||||||
|
|
||||||
|
for (var i = 0; i < images.length; i++) { |
||||||
|
final String imagePath = |
||||||
|
'${directory.path}/problem_${DateTime.now().millisecondsSinceEpoch}_$i.jpg'; |
||||||
|
final File imageFile = File(imagePath); |
||||||
|
await imageFile.writeAsBytes(await images[i].readAsBytes()); |
||||||
|
imagePaths.add(imagePath); |
||||||
|
} |
||||||
|
|
||||||
|
final Problem problem = Problem( |
||||||
|
description: description, |
||||||
|
location: location, |
||||||
|
imagePaths: imagePaths, |
||||||
|
createdAt: DateTime.now(), |
||||||
|
isUploaded: false, |
||||||
|
); |
||||||
|
|
||||||
|
await _localDatabase.insertProblem(problem); |
||||||
|
problems.add(problem); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '保存问题失败: $e'); |
||||||
|
rethrow; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> uploadProblem(Problem problem) async { |
||||||
|
try { |
||||||
|
final dio = Dio(); |
||||||
|
|
||||||
|
// 准备表单数据 |
||||||
|
final formData = FormData.fromMap({ |
||||||
|
'description': problem.description, |
||||||
|
'location': problem.location, |
||||||
|
'createdAt': problem.createdAt.toIso8601String(), |
||||||
|
'boundInfo': problem.boundInfo ?? '', |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加图片文件 |
||||||
|
for (var path in problem.imagePaths) { |
||||||
|
formData.files.add( |
||||||
|
MapEntry('images', await MultipartFile.fromFile(path)), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
final response = await dio.post( |
||||||
|
'https://your-server.com/api/problems', |
||||||
|
data: formData, |
||||||
|
); |
||||||
|
|
||||||
|
if (response.statusCode == 200) { |
||||||
|
// 更新上传状态 |
||||||
|
problem.isUploaded = true; |
||||||
|
await _localDatabase.updateProblem(problem); |
||||||
|
problems.refresh(); |
||||||
|
|
||||||
|
Get.snackbar('成功', '问题已上传到服务器'); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '上传问题失败: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> uploadAllUnuploaded() async { |
||||||
|
final unuploaded = await _localDatabase.getUnuploadedProblems(); |
||||||
|
for (var problem in unuploaded) { |
||||||
|
await uploadProblem(problem); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> bindInfoToProblem(int problemId, String info) async { |
||||||
|
final problem = problems.firstWhere((p) => p.id == problemId); |
||||||
|
problem.boundInfo = info; |
||||||
|
await _localDatabase.updateProblem(problem); |
||||||
|
problems.refresh(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
|
||||||
|
class Problem { |
||||||
|
String? id; |
||||||
|
String description; |
||||||
|
String location; |
||||||
|
List<String> imagePaths; |
||||||
|
DateTime createdAt; |
||||||
|
bool isUploaded; |
||||||
|
String? boundInfo; |
||||||
|
// 添加可观察的选中状态 |
||||||
|
final RxBool isChecked = false.obs; |
||||||
|
|
||||||
|
Problem({ |
||||||
|
this.id, |
||||||
|
required this.description, |
||||||
|
required this.location, |
||||||
|
required this.imagePaths, |
||||||
|
required this.createdAt, |
||||||
|
this.isUploaded = false, |
||||||
|
this.boundInfo, |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加 toJson 方法 |
||||||
|
Map<String, dynamic> toJson() { |
||||||
|
return { |
||||||
|
'id': id, |
||||||
|
'description': description, |
||||||
|
'location': location, |
||||||
|
'imagePaths': imagePaths, // List<String> 类型可以直接序列化 |
||||||
|
'createdAt': createdAt.toIso8601String(), |
||||||
|
'isUploaded': isUploaded, // bool 类型可以直接序列化 |
||||||
|
'boundInfo': boundInfo, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
Map<String, dynamic> toMap() { |
||||||
|
return { |
||||||
|
'id': id, |
||||||
|
'description': description, |
||||||
|
'location': location, |
||||||
|
'imagePaths': imagePaths.join(';;'), |
||||||
|
'createdAt': createdAt.toIso8601String(), |
||||||
|
'isUploaded': isUploaded ? 1 : 0, |
||||||
|
'boundInfo': boundInfo, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
factory Problem.fromMap(Map<String, dynamic> map) { |
||||||
|
return Problem( |
||||||
|
id: map['id'], |
||||||
|
description: map['description'], |
||||||
|
location: map['location'], |
||||||
|
imagePaths: (map['imagePaths'] as String).split(';;'), |
||||||
|
createdAt: DateTime.parse(map['createdAt']), |
||||||
|
isUploaded: map['isUploaded'] == 1, |
||||||
|
boundInfo: map['boundInfo'], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,135 +0,0 @@ |
|||||||
import 'package:uuid/uuid.dart'; |
|
||||||
|
|
||||||
class Problem { |
|
||||||
final String id; |
|
||||||
final String description; |
|
||||||
final String location; |
|
||||||
final String imageUrl; |
|
||||||
final DateTime createdAt; |
|
||||||
final DateTime updatedAt; |
|
||||||
|
|
||||||
static final Uuid _uuid = const Uuid(); |
|
||||||
|
|
||||||
Problem._({ |
|
||||||
required this.id, |
|
||||||
required this.description, |
|
||||||
required this.location, |
|
||||||
required this.imageUrl, |
|
||||||
required this.createdAt, |
|
||||||
required this.updatedAt, |
|
||||||
}); |
|
||||||
|
|
||||||
// 主工厂构造函数 |
|
||||||
factory Problem({ |
|
||||||
required String description, |
|
||||||
required String location, |
|
||||||
required String imageUrl, |
|
||||||
String? id, |
|
||||||
DateTime? createdAt, |
|
||||||
DateTime? updatedAt, |
|
||||||
}) { |
|
||||||
// 参数验证 |
|
||||||
_validateParameters(description, location, imageUrl); |
|
||||||
|
|
||||||
final now = DateTime.now(); |
|
||||||
return Problem._( |
|
||||||
id: id ?? _uuid.v4(), |
|
||||||
description: description.trim(), |
|
||||||
location: location.trim(), |
|
||||||
imageUrl: imageUrl, |
|
||||||
createdAt: createdAt ?? now, |
|
||||||
updatedAt: updatedAt ?? now, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 参数验证 |
|
||||||
static void _validateParameters( |
|
||||||
String description, |
|
||||||
String location, |
|
||||||
String imageUrl, |
|
||||||
) { |
|
||||||
if (description.isEmpty) { |
|
||||||
throw ArgumentError('Description cannot be empty'); |
|
||||||
} |
|
||||||
if (location.isEmpty) { |
|
||||||
throw ArgumentError('Location cannot be empty'); |
|
||||||
} |
|
||||||
if (imageUrl.isEmpty) { |
|
||||||
throw ArgumentError('Image URL cannot be empty'); |
|
||||||
} |
|
||||||
|
|
||||||
// 验证 URL 格式(简单验证) |
|
||||||
if (!imageUrl.startsWith('http')) { |
|
||||||
throw ArgumentError('Image URL must be a valid URL'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 从 JSON 反序列化 |
|
||||||
factory Problem.fromJson(Map<String, dynamic> json) { |
|
||||||
return Problem( |
|
||||||
id: json['id'], |
|
||||||
description: json['description'], |
|
||||||
location: json['location'], |
|
||||||
imageUrl: json['imageUrl'], |
|
||||||
createdAt: json['createdAt'] != null |
|
||||||
? DateTime.parse(json['createdAt']) |
|
||||||
: null, |
|
||||||
updatedAt: json['updatedAt'] != null |
|
||||||
? DateTime.parse(json['updatedAt']) |
|
||||||
: null, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 转换为 JSON |
|
||||||
Map<String, dynamic> toJson() { |
|
||||||
return { |
|
||||||
'id': id, |
|
||||||
'description': description, |
|
||||||
'location': location, |
|
||||||
'imageUrl': imageUrl, |
|
||||||
'createdAt': createdAt.toIso8601String(), |
|
||||||
'updatedAt': updatedAt.toIso8601String(), |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
// 复制方法 |
|
||||||
Problem copyWith({ |
|
||||||
String? id, |
|
||||||
String? description, |
|
||||||
String? location, |
|
||||||
String? imageUrl, |
|
||||||
DateTime? createdAt, |
|
||||||
DateTime? updatedAt, |
|
||||||
}) { |
|
||||||
return Problem( |
|
||||||
id: id ?? this.id, |
|
||||||
description: description ?? this.description, |
|
||||||
location: location ?? this.location, |
|
||||||
imageUrl: imageUrl ?? this.imageUrl, |
|
||||||
createdAt: createdAt ?? this.createdAt, |
|
||||||
updatedAt: updatedAt ?? DateTime.now(), // 更新时间为当前时间 |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 检查是否有效 |
|
||||||
bool get isValid => description.isNotEmpty && location.isNotEmpty; |
|
||||||
|
|
||||||
// 获取简短描述 |
|
||||||
String get shortDescription { |
|
||||||
if (description.length <= 50) return description; |
|
||||||
return '${description.substring(0, 47)}...'; |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
String toString() { |
|
||||||
return 'Problem(id: $id, description: $shortDescription, location: $location)'; |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
bool operator ==(Object other) => |
|
||||||
identical(this, other) || |
|
||||||
other is Problem && runtimeType == other.runtimeType && id == other.id; |
|
||||||
|
|
||||||
@override |
|
||||||
int get hashCode => id.hashCode; |
|
||||||
} |
|
@ -1,244 +0,0 @@ |
|||||||
import 'dart:io'; |
|
||||||
|
|
||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:problem_check_system/modules/problem/models/problem.dart'; |
|
||||||
import 'package:problem_check_system/modules/problem/view_model/image_controller.dart'; |
|
||||||
import 'package:problem_check_system/modules/problem/view_model/problem_new_controller.dart'; |
|
||||||
|
|
||||||
class ProblemNewPage extends StatelessWidget { |
|
||||||
ProblemNewPage({super.key}); |
|
||||||
|
|
||||||
final ImageController imageController = Get.put(ImageController()); |
|
||||||
final ProblemNewController problemNewController = Get.put( |
|
||||||
ProblemNewController(), |
|
||||||
); |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
return Scaffold( |
|
||||||
appBar: AppBar( |
|
||||||
flexibleSpace: Container( |
|
||||||
decoration: const BoxDecoration( |
|
||||||
gradient: LinearGradient( |
|
||||||
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], // 渐变颜色 |
|
||||||
begin: Alignment.centerLeft, // 从左开始 |
|
||||||
end: Alignment.centerRight, // 到右结束 |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
leading: IconButton( |
|
||||||
icon: const Icon(Icons.arrow_back_ios_new_rounded), // 返回箭头图标 |
|
||||||
color: Colors.white, |
|
||||||
onPressed: () { |
|
||||||
Navigator.pop(context); // 返回上一页逻辑 |
|
||||||
}, |
|
||||||
), |
|
||||||
title: const Text('新增问题', style: TextStyle(color: Colors.white)), |
|
||||||
centerTitle: true, |
|
||||||
backgroundColor: Colors.transparent, // 设置背景透明以显示渐变 |
|
||||||
), |
|
||||||
body: Column( |
|
||||||
children: [ |
|
||||||
Expanded( |
|
||||||
child: Column( |
|
||||||
children: [ |
|
||||||
Card( |
|
||||||
margin: EdgeInsets.all(17.w), |
|
||||||
child: Column( |
|
||||||
children: [ |
|
||||||
ListTile( |
|
||||||
title: const Text( |
|
||||||
'问题描述', // 标题 |
|
||||||
style: TextStyle( |
|
||||||
fontSize: 16, |
|
||||||
fontWeight: FontWeight.bold, |
|
||||||
), |
|
||||||
), |
|
||||||
subtitle: TextField( |
|
||||||
maxLines: null, |
|
||||||
decoration: const InputDecoration( |
|
||||||
hintText: '请输入问题描述', |
|
||||||
border: InputBorder.none, |
|
||||||
), |
|
||||||
onChanged: (value) { |
|
||||||
// 输入值发生变化时的逻辑 |
|
||||||
}, |
|
||||||
), |
|
||||||
), |
|
||||||
Align( |
|
||||||
alignment: Alignment.centerRight, // 设置为右对齐 |
|
||||||
child: Container( |
|
||||||
margin: const EdgeInsets.all(10), |
|
||||||
child: ElevatedButton( |
|
||||||
onPressed: () { |
|
||||||
print("按钮被点击"); |
|
||||||
}, |
|
||||||
child: const Icon(Icons.keyboard_voice), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
Card( |
|
||||||
margin: EdgeInsets.all(17.w), |
|
||||||
child: Column( |
|
||||||
children: [ |
|
||||||
ListTile( |
|
||||||
title: const Text( |
|
||||||
'所在位置', // 标题 |
|
||||||
style: TextStyle( |
|
||||||
fontSize: 16, |
|
||||||
fontWeight: FontWeight.bold, |
|
||||||
), |
|
||||||
), |
|
||||||
subtitle: TextField( |
|
||||||
maxLines: null, |
|
||||||
decoration: const InputDecoration( |
|
||||||
hintText: '请输入问题所在位置', |
|
||||||
border: InputBorder.none, |
|
||||||
), |
|
||||||
onChanged: (value) { |
|
||||||
// 输入值发生变化时的逻辑 |
|
||||||
}, |
|
||||||
), |
|
||||||
), |
|
||||||
Align( |
|
||||||
alignment: Alignment.centerRight, // 设置为右对齐 |
|
||||||
child: Container( |
|
||||||
margin: const EdgeInsets.all(10), |
|
||||||
child: ElevatedButton( |
|
||||||
onPressed: () { |
|
||||||
print("按钮被点击"); |
|
||||||
}, |
|
||||||
child: const Icon(Icons.keyboard_voice), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
Card( |
|
||||||
margin: EdgeInsets.all(17.w), |
|
||||||
child: Column( |
|
||||||
children: [ |
|
||||||
ListTile( |
|
||||||
title: const Text( |
|
||||||
'问题图片', // 标题 |
|
||||||
style: TextStyle( |
|
||||||
fontSize: 16, |
|
||||||
fontWeight: FontWeight.bold, |
|
||||||
), |
|
||||||
), |
|
||||||
subtitle: Row( |
|
||||||
mainAxisAlignment: MainAxisAlignment.start, |
|
||||||
children: [ |
|
||||||
// 展示图片区域 |
|
||||||
Obx(() { |
|
||||||
final file = imageController.selectedImage.value; |
|
||||||
if (file == null) { |
|
||||||
return const Text('尚未选取图片'); |
|
||||||
} |
|
||||||
return Image.file( |
|
||||||
file, |
|
||||||
width: 100, |
|
||||||
height: 100, |
|
||||||
fit: BoxFit.cover, |
|
||||||
); |
|
||||||
}), |
|
||||||
const SizedBox(width: 20), |
|
||||||
Container( |
|
||||||
color: Color(0xffF7F7F7), |
|
||||||
width: 100, |
|
||||||
height: 100, |
|
||||||
child: IconButton( |
|
||||||
icon: const Icon(Icons.image), |
|
||||||
iconSize: 40, // 图标大小 |
|
||||||
onPressed: imageController.pickAndCopyImage, |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
// 底部按钮 |
|
||||||
Container( |
|
||||||
width: 375.w, |
|
||||||
height: 81.h, |
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), |
|
||||||
decoration: BoxDecoration( |
|
||||||
color: Colors.grey[200], |
|
||||||
borderRadius: BorderRadius.circular(12), |
|
||||||
), |
|
||||||
child: Row( |
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
|
||||||
children: [ |
|
||||||
Expanded( |
|
||||||
child: ElevatedButton( |
|
||||||
style: ElevatedButton.styleFrom( |
|
||||||
backgroundColor: Colors.white, |
|
||||||
shape: RoundedRectangleBorder( |
|
||||||
borderRadius: BorderRadius.circular(8), |
|
||||||
), |
|
||||||
), |
|
||||||
onPressed: () { |
|
||||||
// 取消按钮逻辑 |
|
||||||
Get.back(); |
|
||||||
}, |
|
||||||
child: const Text( |
|
||||||
'取消', |
|
||||||
style: TextStyle(color: Colors.grey), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
const SizedBox(width: 10), |
|
||||||
Expanded( |
|
||||||
child: ElevatedButton( |
|
||||||
style: ElevatedButton.styleFrom( |
|
||||||
backgroundColor: Color(0xFF418CFC), |
|
||||||
shape: RoundedRectangleBorder( |
|
||||||
borderRadius: BorderRadius.circular(8), |
|
||||||
), |
|
||||||
), |
|
||||||
onPressed: () async { |
|
||||||
// 确定按钮逻辑 |
|
||||||
final problem1 = Problem( |
|
||||||
description: '墙面裂缝需要修复', |
|
||||||
location: 'A栋101房间', |
|
||||||
imageUrl: 'https://example.com/images/wall_crack.jpg', |
|
||||||
); |
|
||||||
await problemNewController.saveJson( |
|
||||||
problem1.toJson(), |
|
||||||
problem1.id, |
|
||||||
); |
|
||||||
Get.back(); |
|
||||||
Get.snackbar( |
|
||||||
'保存成功', |
|
||||||
'${problem1.id}.json 已创建', |
|
||||||
snackPosition: SnackPosition.TOP, |
|
||||||
snackStyle: SnackStyle.FLOATING, |
|
||||||
backgroundColor: Colors.black87, |
|
||||||
colorText: Colors.white, |
|
||||||
); |
|
||||||
}, |
|
||||||
child: const Text( |
|
||||||
'确定', |
|
||||||
style: TextStyle(color: Colors.white), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -1,29 +0,0 @@ |
|||||||
import 'dart:io'; |
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:image_picker/image_picker.dart'; |
|
||||||
import 'package:path/path.dart'; |
|
||||||
import 'package:path_provider/path_provider.dart'; |
|
||||||
|
|
||||||
class ImageController extends GetxController { |
|
||||||
// Rxn 表示 nullable 的响应式类型 |
|
||||||
final Rxn<File> selectedImage = Rxn<File>(); |
|
||||||
|
|
||||||
// 从相册选取并复制 |
|
||||||
Future<void> pickAndCopyImage() async { |
|
||||||
// 调用 ImagePicker 选取 |
|
||||||
final picked = await ImagePicker().pickImage(source: ImageSource.gallery); |
|
||||||
if (picked == null) return; |
|
||||||
|
|
||||||
final originalFile = File(picked.path); |
|
||||||
|
|
||||||
// 获取应用文档目录 |
|
||||||
final appDir = await getApplicationDocumentsDirectory(); |
|
||||||
final fileName = basename(picked.path); |
|
||||||
|
|
||||||
// 复制到应用目录 |
|
||||||
final copiedFile = await originalFile.copy('${appDir.path}/$fileName'); |
|
||||||
|
|
||||||
// 更新响应式状态 |
|
||||||
selectedImage.value = copiedFile; |
|
||||||
} |
|
||||||
} |
|
@ -1,39 +0,0 @@ |
|||||||
import 'dart:convert'; |
|
||||||
import 'dart:io'; |
|
||||||
|
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:path_provider/path_provider.dart'; |
|
||||||
|
|
||||||
class ProblemNewController extends GetxController { |
|
||||||
late Directory appDocDir; |
|
||||||
|
|
||||||
@override |
|
||||||
void onInit() { |
|
||||||
super.onInit(); |
|
||||||
_initDirectory(); |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> _initDirectory() async { |
|
||||||
appDocDir = await getApplicationDocumentsDirectory(); |
|
||||||
} |
|
||||||
|
|
||||||
// todo problems文件不存在,需要创建 |
|
||||||
/// 保存 Map 数据为 JSON 文件 |
|
||||||
Future<File> saveJson(Map<String, dynamic> data, String fileName) async { |
|
||||||
final path = '${appDocDir.path}/problems/$fileName.json'; |
|
||||||
final file = File(path); |
|
||||||
final jsonStr = jsonEncode(data); |
|
||||||
return file.writeAsString(jsonStr); |
|
||||||
} |
|
||||||
|
|
||||||
/// 读取本地 JSON 并解析为 Map |
|
||||||
Future<Map<String, dynamic>?> readJson(String fileName) async { |
|
||||||
final path = '${appDocDir.path}/problems/$fileName.json'; |
|
||||||
final file = File(path); |
|
||||||
if (!await file.exists()) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
final content = await file.readAsString(); |
|
||||||
return jsonDecode(content) as Map<String, dynamic>; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,78 @@ |
|||||||
|
import 'package:sqflite/sqflite.dart'; |
||||||
|
import 'package:path/path.dart'; |
||||||
|
import 'package:uuid/uuid.dart'; |
||||||
|
import '../models/problem_model.dart'; |
||||||
|
|
||||||
|
class LocalDatabase { |
||||||
|
static final LocalDatabase _instance = LocalDatabase._internal(); |
||||||
|
factory LocalDatabase() => _instance; |
||||||
|
static Database? _database; |
||||||
|
|
||||||
|
LocalDatabase._internal(); |
||||||
|
|
||||||
|
Future<Database> get database async { |
||||||
|
if (_database != null) return _database!; |
||||||
|
_database = await _initDatabase(); |
||||||
|
return _database!; |
||||||
|
} |
||||||
|
|
||||||
|
Future<Database> _initDatabase() async { |
||||||
|
String path = join(await getDatabasesPath(), 'problems.db'); |
||||||
|
return await openDatabase(path, version: 1, onCreate: _onCreate); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _onCreate(Database db, int version) async { |
||||||
|
await db.execute(''' |
||||||
|
CREATE TABLE problems( |
||||||
|
id TEXT PRIMARY KEY, |
||||||
|
description TEXT NOT NULL, |
||||||
|
location TEXT NOT NULL, |
||||||
|
imagePaths TEXT NOT NULL, |
||||||
|
createdAt TEXT NOT NULL, |
||||||
|
isUploaded INTEGER NOT NULL, |
||||||
|
boundInfo TEXT |
||||||
|
) |
||||||
|
'''); |
||||||
|
} |
||||||
|
|
||||||
|
Future<int> insertProblem(Problem problem) async { |
||||||
|
final db = await database; |
||||||
|
problem.id = Uuid().v4(); |
||||||
|
return await db.insert('problems', problem.toMap()); |
||||||
|
} |
||||||
|
|
||||||
|
Future<List<Problem>> getProblems() async { |
||||||
|
final db = await database; |
||||||
|
final List<Map<String, dynamic>> maps = await db.query('problems'); |
||||||
|
return List.generate(maps.length, (i) { |
||||||
|
return Problem.fromMap(maps[i]); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
Future<int> updateProblem(Problem problem) async { |
||||||
|
final db = await database; |
||||||
|
return await db.update( |
||||||
|
'problems', |
||||||
|
problem.toMap(), |
||||||
|
where: 'id = ?', |
||||||
|
whereArgs: [problem.id], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Future<int> deleteProblem(int id) async { |
||||||
|
final db = await database; |
||||||
|
return await db.delete('problems', where: 'id = ?', whereArgs: [id]); |
||||||
|
} |
||||||
|
|
||||||
|
Future<List<Problem>> getUnuploadedProblems() async { |
||||||
|
final db = await database; |
||||||
|
final List<Map<String, dynamic>> maps = await db.query( |
||||||
|
'problems', |
||||||
|
where: 'isUploaded = ?', |
||||||
|
whereArgs: [0], |
||||||
|
); |
||||||
|
return List.generate(maps.length, (i) { |
||||||
|
return Problem.fromMap(maps[i]); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,348 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:image_picker/image_picker.dart'; |
||||||
|
import 'package:problem_check_system/controllers/add_problem_controller.dart'; |
||||||
|
|
||||||
|
class AddProblemPage extends StatelessWidget { |
||||||
|
AddProblemPage({super.key}); |
||||||
|
|
||||||
|
final AddProblemController controller = Get.put(AddProblemController()); |
||||||
|
|
||||||
|
// 显示底部选择器的方法 |
||||||
|
void _showImageSourceBottomSheet(BuildContext context) { |
||||||
|
showModalBottomSheet( |
||||||
|
context: context, |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), |
||||||
|
), |
||||||
|
builder: (BuildContext context) { |
||||||
|
return SafeArea( |
||||||
|
child: Column( |
||||||
|
mainAxisSize: MainAxisSize.min, |
||||||
|
children: [ |
||||||
|
SizedBox(height: 16.h), |
||||||
|
Text( |
||||||
|
'选择图片来源', |
||||||
|
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), |
||||||
|
), |
||||||
|
SizedBox(height: 16.h), |
||||||
|
Divider(height: 1.h), |
||||||
|
ListTile( |
||||||
|
leading: Icon(Icons.camera_alt, color: Colors.blue), |
||||||
|
title: Text('拍照'), |
||||||
|
onTap: () { |
||||||
|
Navigator.pop(context); |
||||||
|
controller.pickImage(ImageSource.camera); |
||||||
|
}, |
||||||
|
), |
||||||
|
Divider(height: 1.h), |
||||||
|
ListTile( |
||||||
|
leading: Icon(Icons.photo_library, color: Colors.blue), |
||||||
|
title: Text('从相册选择'), |
||||||
|
onTap: () { |
||||||
|
Navigator.pop(context); |
||||||
|
controller.pickImage(ImageSource.gallery); |
||||||
|
}, |
||||||
|
), |
||||||
|
Divider(height: 1.h), |
||||||
|
ListTile( |
||||||
|
leading: Icon(Icons.cancel, color: Colors.grey), |
||||||
|
title: Text('取消', style: TextStyle(color: Colors.grey)), |
||||||
|
onTap: () => Navigator.pop(context), |
||||||
|
), |
||||||
|
SizedBox(height: 8.h), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Scaffold( |
||||||
|
appBar: AppBar( |
||||||
|
flexibleSpace: Container( |
||||||
|
decoration: const BoxDecoration( |
||||||
|
gradient: LinearGradient( |
||||||
|
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], |
||||||
|
begin: Alignment.centerLeft, |
||||||
|
end: Alignment.centerRight, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
leading: IconButton( |
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded), |
||||||
|
color: Colors.white, |
||||||
|
onPressed: () { |
||||||
|
Navigator.pop(context); |
||||||
|
}, |
||||||
|
), |
||||||
|
title: const Text('新增问题', style: TextStyle(color: Colors.white)), |
||||||
|
centerTitle: true, |
||||||
|
backgroundColor: Colors.transparent, |
||||||
|
), |
||||||
|
body: Column( |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: SingleChildScrollView( |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'问题描述', |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: TextField( |
||||||
|
maxLines: null, |
||||||
|
controller: controller.descriptionController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
hintText: '请输入问题描述', |
||||||
|
border: InputBorder.none, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'所在位置', |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: TextField( |
||||||
|
maxLines: null, |
||||||
|
controller: controller.locationController, |
||||||
|
decoration: const InputDecoration( |
||||||
|
hintText: '请输入问题所在位置', |
||||||
|
border: InputBorder.none, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'问题图片', |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
SizedBox(height: 8.h), |
||||||
|
_buildImageGridWithAddButton(context), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
_bottomButton(), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildImageGridWithAddButton(BuildContext context) { |
||||||
|
return Obx(() { |
||||||
|
// 计算总项目数(图片数 + 添加按钮) |
||||||
|
int totalItems = controller.selectedImages.length + 1; |
||||||
|
|
||||||
|
return GridView.builder( |
||||||
|
shrinkWrap: true, |
||||||
|
physics: const NeverScrollableScrollPhysics(), |
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( |
||||||
|
crossAxisCount: 3, |
||||||
|
crossAxisSpacing: 8.w, |
||||||
|
mainAxisSpacing: 8.h, |
||||||
|
childAspectRatio: 1, |
||||||
|
), |
||||||
|
itemCount: totalItems, |
||||||
|
itemBuilder: (context, index) { |
||||||
|
// 如果是最后一个项目,显示添加按钮 |
||||||
|
if (index == controller.selectedImages.length) { |
||||||
|
return _buildAddImageButton(context); |
||||||
|
} |
||||||
|
|
||||||
|
// 否则显示图片 |
||||||
|
return _buildImageItem(index); |
||||||
|
}, |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildAddImageButton(BuildContext context) { |
||||||
|
return InkWell( |
||||||
|
onTap: () => _showImageSourceBottomSheet(context), |
||||||
|
child: Container( |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: Colors.grey.shade100, |
||||||
|
borderRadius: BorderRadius.circular(8), |
||||||
|
border: Border.all(color: Colors.grey.shade300, width: 1), |
||||||
|
), |
||||||
|
child: Column( |
||||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||||
|
children: [ |
||||||
|
Icon( |
||||||
|
Icons.add_photo_alternate, |
||||||
|
size: 24.sp, |
||||||
|
color: Colors.grey.shade600, |
||||||
|
), |
||||||
|
SizedBox(height: 4.h), |
||||||
|
Text( |
||||||
|
'添加图片', |
||||||
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 12.sp), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildImageItem(int index) { |
||||||
|
return Container( |
||||||
|
decoration: BoxDecoration( |
||||||
|
borderRadius: BorderRadius.circular(8), |
||||||
|
border: Border.all(color: Colors.grey.shade300), |
||||||
|
), |
||||||
|
child: Stack( |
||||||
|
children: [ |
||||||
|
ClipRRect( |
||||||
|
borderRadius: BorderRadius.circular(8), |
||||||
|
child: Image.file( |
||||||
|
File(controller.selectedImages[index].path), |
||||||
|
width: double.infinity, |
||||||
|
height: double.infinity, |
||||||
|
fit: BoxFit.cover, |
||||||
|
), |
||||||
|
), |
||||||
|
Positioned( |
||||||
|
top: 0, |
||||||
|
right: 0, |
||||||
|
child: GestureDetector( |
||||||
|
onTap: () => controller.removeImage(index), |
||||||
|
child: Container( |
||||||
|
decoration: const BoxDecoration( |
||||||
|
color: Colors.black54, |
||||||
|
shape: BoxShape.circle, |
||||||
|
), |
||||||
|
padding: EdgeInsets.all(4.w), |
||||||
|
child: Icon(Icons.close, color: Colors.white, size: 16.sp), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _bottomButton() { |
||||||
|
return Container( |
||||||
|
width: 375.w, |
||||||
|
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: Colors.grey[200], |
||||||
|
borderRadius: BorderRadius.only( |
||||||
|
topLeft: Radius.circular(12.r), |
||||||
|
topRight: Radius.circular(12.r), |
||||||
|
), |
||||||
|
), |
||||||
|
child: Row( |
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: ElevatedButton( |
||||||
|
style: ElevatedButton.styleFrom( |
||||||
|
backgroundColor: Colors.white, |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(8.r), |
||||||
|
), |
||||||
|
padding: EdgeInsets.symmetric(vertical: 12.h), |
||||||
|
), |
||||||
|
onPressed: () { |
||||||
|
Get.back(); |
||||||
|
}, |
||||||
|
child: Text( |
||||||
|
'取消', |
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 16.sp), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
SizedBox(width: 10.w), |
||||||
|
Expanded( |
||||||
|
child: Obx( |
||||||
|
() => ElevatedButton( |
||||||
|
style: ElevatedButton.styleFrom( |
||||||
|
backgroundColor: const Color(0xFF418CFC), |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(8.r), |
||||||
|
), |
||||||
|
padding: EdgeInsets.symmetric(vertical: 12.h), |
||||||
|
), |
||||||
|
onPressed: controller.isLoading.value |
||||||
|
? null |
||||||
|
: () { |
||||||
|
if (controller.descriptionController.text.isEmpty) { |
||||||
|
Get.snackbar( |
||||||
|
'提示', |
||||||
|
'问题描述不能为空', |
||||||
|
snackPosition: SnackPosition.TOP, |
||||||
|
backgroundColor: Colors.black87, |
||||||
|
colorText: Colors.white, |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
controller.saveProblem(); |
||||||
|
}, |
||||||
|
child: controller.isLoading.value |
||||||
|
? SizedBox( |
||||||
|
width: 20.w, |
||||||
|
height: 20.h, |
||||||
|
child: CircularProgressIndicator( |
||||||
|
strokeWidth: 2, |
||||||
|
valueColor: const AlwaysStoppedAnimation<Color>( |
||||||
|
Colors.white, |
||||||
|
), |
||||||
|
), |
||||||
|
) |
||||||
|
: Text( |
||||||
|
'确定', |
||||||
|
style: TextStyle(color: Colors.white, fontSize: 16.sp), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue