From 2267d6fc9191b4691c80c70b617dcb3cd019e397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Thu, 21 Aug 2025 17:45:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=8C=E6=96=B0=E5=A2=9E=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=9C=AC=E5=9C=B0=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +- android/app/src/main/AndroidManifest.xml | 15 +- android/gradle.properties | 5 + lib/controllers/add_problem_controller.dart | 130 +++++++ lib/controllers/auth_controller.dart | 73 ++++ lib/controllers/problem_controller.dart | 115 ++++++ lib/models/problem_model.dart | 60 +++ lib/modules/home/home_controller.dart | 2 +- lib/modules/login/views/problem_page.dart | 0 .../{ => components}/custom_button.dart | 0 lib/modules/problem/models/problem.dart | 135 ------- lib/modules/problem/problem_card.dart | 137 ++++--- lib/modules/problem/problem_new_page.dart | 244 ------------ .../problem/view_model/image_controller.dart | 29 -- .../view_model/problem_new_controller.dart | 39 -- lib/services/local_database.dart | 78 ++++ lib/views/add_problem_page.dart | 348 ++++++++++++++++++ .../problem => views}/problem_page.dart | 74 +++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 136 +++++++ pubspec.yaml | 4 + 21 files changed, 1111 insertions(+), 540 deletions(-) create mode 100644 lib/controllers/add_problem_controller.dart create mode 100644 lib/controllers/auth_controller.dart create mode 100644 lib/controllers/problem_controller.dart create mode 100644 lib/models/problem_model.dart delete mode 100644 lib/modules/login/views/problem_page.dart rename lib/modules/problem/{ => components}/custom_button.dart (100%) delete mode 100644 lib/modules/problem/models/problem.dart delete mode 100644 lib/modules/problem/problem_new_page.dart delete mode 100644 lib/modules/problem/view_model/image_controller.dart delete mode 100644 lib/modules/problem/view_model/problem_new_controller.dart create mode 100644 lib/services/local_database.dart create mode 100644 lib/views/add_problem_page.dart rename lib/{modules/problem => views}/problem_page.dart (64%) diff --git a/README.md b/README.md index e5fa47e..d458a7b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # problem_check_system -A new Flutter project. +这个应用需要实现以下功能: + +离线登录系统 + +问题数据收集(描述、位置、图片等) + +本地数据存储 + +有网络时手动上传功能 + +技术栈 +Flutter SDK + +GetX (状态管理、路由管理、依赖注入) + +SQFlite (本地数据库) + +Image Picker (图片选择) + +Geolocator (位置信息) + +HTTP/Dio (网络请求) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1331019..427e780 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,17 @@ - - + + + + + + + + + + + + _selectedImages = [].obs; + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController locationController = TextEditingController(); + final RxBool isLoading = false.obs; + + List get selectedImages => _selectedImages; + + // 改进的 pickImage 方法 + Future 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 removeImage(int index) async { + _selectedImages.removeAt(index); + } + + bool get isFormValid { + return descriptionController.text.isNotEmpty && + locationController.text.isNotEmpty; + } + + Future 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(); + } +} diff --git a/lib/controllers/auth_controller.dart b/lib/controllers/auth_controller.dart new file mode 100644 index 0000000..a3c1cba --- /dev/null +++ b/lib/controllers/auth_controller.dart @@ -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 checkLoginStatus() async { + final prefs = await SharedPreferences.getInstance(); + final offlineLogin = prefs.getBool('offlineLogin') ?? false; + isLoggedIn.value = offlineLogin; + + if (offlineLogin) { + username.value = prefs.getString('username') ?? '离线用户'; + } + } + + Future 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 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 logout() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('offlineLogin'); + await prefs.remove('username'); + + isLoggedIn.value = false; + username.value = ''; + } +} diff --git a/lib/controllers/problem_controller.dart b/lib/controllers/problem_controller.dart new file mode 100644 index 0000000..e2ed97e --- /dev/null +++ b/lib/controllers/problem_controller.dart @@ -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 problems = [].obs; + final RxBool isLoading = false.obs; + + // 这个方法返回所有已选中的问题对象 + List get selectedProblems { + return problems.where((p) => p.isChecked.value).toList(); + } + + @override + void onInit() { + super.onInit(); + loadProblems(); + } + + Future loadProblems() async { + isLoading.value = true; + problems.value = await _localDatabase.getProblems(); + isLoading.value = false; + } + + Future addProblem( + String description, + String location, + List images, + ) async { + try { + // 保存图片到本地 + final List 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 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 uploadAllUnuploaded() async { + final unuploaded = await _localDatabase.getUnuploadedProblems(); + for (var problem in unuploaded) { + await uploadProblem(problem); + } + } + + Future bindInfoToProblem(int problemId, String info) async { + final problem = problems.firstWhere((p) => p.id == problemId); + problem.boundInfo = info; + await _localDatabase.updateProblem(problem); + problems.refresh(); + } +} diff --git a/lib/models/problem_model.dart b/lib/models/problem_model.dart new file mode 100644 index 0000000..bdaa7ea --- /dev/null +++ b/lib/models/problem_model.dart @@ -0,0 +1,60 @@ +import 'package:get/get.dart'; + +class Problem { + String? id; + String description; + String location; + List 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 toJson() { + return { + 'id': id, + 'description': description, + 'location': location, + 'imagePaths': imagePaths, // List 类型可以直接序列化 + 'createdAt': createdAt.toIso8601String(), + 'isUploaded': isUploaded, // bool 类型可以直接序列化 + 'boundInfo': boundInfo, + }; + } + + Map toMap() { + return { + 'id': id, + 'description': description, + 'location': location, + 'imagePaths': imagePaths.join(';;'), + 'createdAt': createdAt.toIso8601String(), + 'isUploaded': isUploaded ? 1 : 0, + 'boundInfo': boundInfo, + }; + } + + factory Problem.fromMap(Map 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'], + ); + } +} diff --git a/lib/modules/home/home_controller.dart b/lib/modules/home/home_controller.dart index 0e677c6..0f81ba6 100644 --- a/lib/modules/home/home_controller.dart +++ b/lib/modules/home/home_controller.dart @@ -1,7 +1,7 @@ // 控制器(Logic 层) import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/modules/problem/problem_page.dart'; +import 'package:problem_check_system/views/problem_page.dart'; class HomeController extends GetxController { // 当前选中索引(状态) diff --git a/lib/modules/login/views/problem_page.dart b/lib/modules/login/views/problem_page.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/modules/problem/custom_button.dart b/lib/modules/problem/components/custom_button.dart similarity index 100% rename from lib/modules/problem/custom_button.dart rename to lib/modules/problem/components/custom_button.dart diff --git a/lib/modules/problem/models/problem.dart b/lib/modules/problem/models/problem.dart deleted file mode 100644 index c5c152c..0000000 --- a/lib/modules/problem/models/problem.dart +++ /dev/null @@ -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 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 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; -} diff --git a/lib/modules/problem/problem_card.dart b/lib/modules/problem/problem_card.dart index 6432e2d..bee3cdd 100644 --- a/lib/modules/problem/problem_card.dart +++ b/lib/modules/problem/problem_card.dart @@ -1,22 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/modules/problem/custom_button.dart'; +import 'package:intl/intl.dart'; +import 'package:problem_check_system/models/problem_model.dart'; +import 'package:problem_check_system/modules/problem/components/custom_button.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; +// 定义枚举类型 +enum ProblemCardViewType { buttons, checkbox } + class ProblemCard extends StatelessWidget { - final bool initialBound; - final bool initialUploaded; - final ProblemCardController controller; + final Problem problem; + final ProblemCardViewType viewType; - ProblemCard({ + const ProblemCard( + this.problem, { super.key, - required this.initialBound, - required this.initialUploaded, - }) : controller = ProblemCardController( - initialBound: initialBound, - initialUploaded: initialUploaded, - ); + this.viewType = ProblemCardViewType.buttons, + }); @override Widget build(BuildContext context) { @@ -28,19 +29,16 @@ class ProblemCard extends StatelessWidget { ListTile( leading: Image.asset( 'assets/images/problem_preview.png', - fit: BoxFit.contain, // 防止图片拉伸 - ), - title: Text( - '问题描述', - style: TextStyle(fontSize: 16.sp), // 动态文字大小 + fit: BoxFit.contain, ), + title: Text('问题描述', style: TextStyle(fontSize: 16.sp)), subtitle: LayoutBuilder( builder: (context, constraints) { return Text( - '硫磺库内南侧地面上存放了阀门、消防水带等物品;12#库存放了脱模剂、愈合成催化剂省略字省略字省略字省略字...', + problem.description, maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 14.sp), // 适配设备 + style: TextStyle(fontSize: 14.sp), ); }, ), @@ -53,10 +51,7 @@ class ProblemCard extends StatelessWidget { children: [ Icon(Icons.location_on, color: Colors.grey, size: 16.h), SizedBox(width: 8.w), - Text( - '汽车机厂房作业区1-C', // 替换为实际文本 - style: TextStyle(fontSize: 12.sp), - ), + Text(problem.location, style: TextStyle(fontSize: 12.sp)), ], ), SizedBox(width: 16.w), @@ -65,7 +60,7 @@ class ProblemCard extends StatelessWidget { Icon(Icons.access_time, color: Colors.grey, size: 16.h), SizedBox(width: 8.w), Text( - '2025-07-31 15:30:29', // 替换为实际时间文本 + DateFormat('yyyy-MM-dd HH:mm').format(problem.createdAt), style: TextStyle(fontSize: 12.sp), ), ], @@ -77,63 +72,63 @@ class ProblemCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ SizedBox(width: 16.w), - Obx( - () => Wrap( - spacing: 8, - children: [ - controller.isUploaded.value - ? TDTag('已上传', isLight: true, theme: TDTagTheme.success) - : TDTag('未上传', isLight: true, theme: TDTagTheme.danger), - controller.isBound.value - ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) - : TDTag( - '未绑定', - isLight: true, - theme: TDTagTheme.warning, - ), - ], - ), - ), - SizedBox(width: 100.w), - Row( + Wrap( + spacing: 8, children: [ - CustomButton( - text: '修改', // 按钮上的文字 - onTap: () { - // 点击事件逻辑 - print('点击修改按钮'); - }, - ), - SizedBox(width: 8.w), - CustomButton( - text: '查看', // 按钮上的文字 - onTap: () { - // 点击事件逻辑 - print('点击查看按钮'); - }, - ), + problem.isUploaded + ? TDTag('已上传', isLight: true, theme: TDTagTheme.success) + : TDTag('未上传', isLight: true, theme: TDTagTheme.danger), + problem.boundInfo != null && problem.boundInfo!.isNotEmpty + ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) + : TDTag('未绑定', isLight: true, theme: TDTagTheme.warning), ], ), + const Spacer(), // 使用 Spacer 替代固定的 SizedBox + _buildBottomActions(), ], ), + SizedBox(height: 8.h), ], ), ); } -} - -class ProblemCardController extends GetxController { - // 是否绑定 - var isBound = false.obs; - // 是否上传 - var isUploaded = false.obs; - // 构造函数传入参数 - ProblemCardController({ - bool initialBound = false, - bool initialUploaded = false, - }) { - isBound.value = initialBound; - isUploaded.value = initialUploaded; + // 渲染底部 UI 的私有方法 + Widget _buildBottomActions() { + switch (viewType) { + case ProblemCardViewType.buttons: + return Row( + children: [ + CustomButton( + text: '修改', + onTap: () { + print('点击修改按钮'); + }, + ), + SizedBox(width: 8.w), + CustomButton( + text: '查看', + onTap: () { + print('点击查看按钮'); + }, + ), + SizedBox(width: 16.w), + ], + ); + case ProblemCardViewType.checkbox: + return Padding( + padding: EdgeInsets.only(right: 16.w), + child: Obx( + () => Checkbox( + // 将 Checkbox 的 value 绑定到 controller.isChecked.value + value: problem.isChecked.value, + // 当 Checkbox 状态改变时,调用 controller 中的方法来更新状态 + onChanged: (bool? value) { + problem.isChecked.value = value ?? false; + }, + ), + ), + ); + } } } diff --git a/lib/modules/problem/problem_new_page.dart b/lib/modules/problem/problem_new_page.dart deleted file mode 100644 index 603b5fd..0000000 --- a/lib/modules/problem/problem_new_page.dart +++ /dev/null @@ -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), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/modules/problem/view_model/image_controller.dart b/lib/modules/problem/view_model/image_controller.dart deleted file mode 100644 index ef95616..0000000 --- a/lib/modules/problem/view_model/image_controller.dart +++ /dev/null @@ -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 selectedImage = Rxn(); - - // 从相册选取并复制 - Future 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; - } -} diff --git a/lib/modules/problem/view_model/problem_new_controller.dart b/lib/modules/problem/view_model/problem_new_controller.dart deleted file mode 100644 index e6edec5..0000000 --- a/lib/modules/problem/view_model/problem_new_controller.dart +++ /dev/null @@ -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 _initDirectory() async { - appDocDir = await getApplicationDocumentsDirectory(); - } - - // todo problems文件不存在,需要创建 - /// 保存 Map 数据为 JSON 文件 - Future saveJson(Map 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?> 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; - } -} diff --git a/lib/services/local_database.dart b/lib/services/local_database.dart new file mode 100644 index 0000000..8c17107 --- /dev/null +++ b/lib/services/local_database.dart @@ -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 get database async { + if (_database != null) return _database!; + _database = await _initDatabase(); + return _database!; + } + + Future _initDatabase() async { + String path = join(await getDatabasesPath(), 'problems.db'); + return await openDatabase(path, version: 1, onCreate: _onCreate); + } + + Future _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 insertProblem(Problem problem) async { + final db = await database; + problem.id = Uuid().v4(); + return await db.insert('problems', problem.toMap()); + } + + Future> getProblems() async { + final db = await database; + final List> maps = await db.query('problems'); + return List.generate(maps.length, (i) { + return Problem.fromMap(maps[i]); + }); + } + + Future updateProblem(Problem problem) async { + final db = await database; + return await db.update( + 'problems', + problem.toMap(), + where: 'id = ?', + whereArgs: [problem.id], + ); + } + + Future deleteProblem(int id) async { + final db = await database; + return await db.delete('problems', where: 'id = ?', whereArgs: [id]); + } + + Future> getUnuploadedProblems() async { + final db = await database; + final List> maps = await db.query( + 'problems', + where: 'isUploaded = ?', + whereArgs: [0], + ); + return List.generate(maps.length, (i) { + return Problem.fromMap(maps[i]); + }); + } +} diff --git a/lib/views/add_problem_page.dart b/lib/views/add_problem_page.dart new file mode 100644 index 0000000..d0fddbc --- /dev/null +++ b/lib/views/add_problem_page.dart @@ -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( + Colors.white, + ), + ), + ) + : Text( + '确定', + style: TextStyle(color: Colors.white, fontSize: 16.sp), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/problem/problem_page.dart b/lib/views/problem_page.dart similarity index 64% rename from lib/modules/problem/problem_page.dart rename to lib/views/problem_page.dart index e802f54..1fe83c6 100644 --- a/lib/modules/problem/problem_page.dart +++ b/lib/views/problem_page.dart @@ -1,13 +1,20 @@ +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/controllers/auth_controller.dart'; +import 'package:problem_check_system/controllers/problem_controller.dart'; import 'package:problem_check_system/modules/problem/components/date_picker_button.dart'; import 'package:problem_check_system/modules/problem/problem_card.dart'; -import 'package:problem_check_system/modules/problem/problem_new_page.dart'; +import 'package:problem_check_system/views/add_problem_page.dart'; class ProblemPage extends StatelessWidget { ProblemPage({super.key}); + final AuthController authController = Get.put(AuthController()); + final ProblemController problemController = Get.put(ProblemController()); + final List> problemData = [ {"initialBound": false, "initialUploaded": false}, {"initialBound": true, "initialUploaded": true}, @@ -93,28 +100,44 @@ class ProblemPage extends StatelessWidget { Expanded( child: Stack( children: [ - SingleChildScrollView( - child: Column( - children: [ - ...problemData.map((data) { - return ProblemCard( - initialBound: - data["initialBound"] ?? false, - initialUploaded: - data["initialUploaded"] ?? false, - ); - }), - SizedBox(height: 64.h), - ], - ), - ), + // SingleChildScrollView( + // child: Column( + // children: [ + // ...problemData.map((data) { + // return ProblemCard( + // initialBound: + // data["initialBound"] ?? false, + // initialUploaded: + // data["initialUploaded"] ?? false, + // ); + // }), + // SizedBox(height: 64.h), + // ], + // ), + // ), + Obx(() { + if (problemController.isLoading.value) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return ListView.builder( + itemCount: problemController.problems.length, + itemBuilder: (context, index) { + final problem = + problemController.problems[index]; + return ProblemCard(problem); + }, + ); + }), Positioned( bottom: 5.h, right: 160.5.w, child: FloatingActionButton( heroTag: "123", onPressed: () { - Get.to(() => ProblemNewPage()); + Get.to(() => AddProblemPage()); }, shape: CircleBorder(), backgroundColor: Colors.blue[300], @@ -127,7 +150,22 @@ class ProblemPage extends StatelessWidget { ), ], ), - ProblemCard(initialBound: false, initialUploaded: false), + Obx(() { + if (problemController.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + + return ListView.builder( + itemCount: problemController.problems.length, + itemBuilder: (context, index) { + final problem = problemController.problems[index]; + return ProblemCard( + problem, + viewType: ProblemCardViewType.checkbox, + ); + }, + ); + }), ], ), ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b878e03..f6d969f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,12 @@ import Foundation import file_selector_macos import path_provider_foundation +import shared_preferences_foundation +import sqflite_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 05e4120..9225354 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" easy_refresh: dependency: transitive description: @@ -81,6 +97,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" file_selector_linux: dependency: transitive description: @@ -264,6 +288,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: @@ -464,6 +496,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.11" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -485,6 +573,46 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -509,6 +637,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" tdesign_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b65dd8c..51fcb44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,14 +7,18 @@ environment: sdk: ^3.8.1 dependencies: + dio: ^5.9.0 flutter: sdk: flutter flutter_screenutil: ^5.9.3 get: ^4.7.2 image_picker: ^1.1.2 + intl: ^0.20.2 path: ^1.9.1 path_provider: ^2.1.5 permission_handler: ^12.0.1 + shared_preferences: ^2.5.3 + sqflite: ^2.4.2 tdesign_flutter: ^0.2.4 uuid: ^4.5.1