Browse Source

refactor : imageUrls 修改我 imageMetadata

dev
徐振升 2 weeks ago
parent
commit
72c871ca9b
  1. 9
      lib/app/bindings/initial_binding.dart
  2. 2
      lib/app/routes/app_pages.dart
  3. 22
      lib/data/models/enum_model.dart
  4. 32
      lib/data/models/image_metadata_model.dart
  5. 57
      lib/data/models/problem_model.dart
  6. 148
      lib/data/providers/local_database.dart
  7. 147
      lib/data/providers/sqlite_provider.dart
  8. 10
      lib/data/repositories/auth_repository.dart
  9. 71
      lib/data/repositories/file_repository.dart
  10. 167
      lib/data/repositories/problem_repository.dart
  11. 15
      lib/modules/auth/controllers/login_controller.dart
  12. 4
      lib/modules/auth/views/login_page.dart
  13. 0
      lib/modules/my/bindings/change_password_binding.dart
  14. 179
      lib/modules/problem/controllers/problem_controller.dart
  15. 56
      lib/modules/problem/controllers/problem_form_controller.dart
  16. 13
      lib/modules/problem/views/problem_form_page.dart
  17. 4
      lib/modules/problem/views/problem_list_page.dart
  18. 26
      lib/modules/problem/views/problem_upload_page.dart
  19. 33
      lib/modules/problem/views/widgets/problem_card.dart

9
lib/app/bindings/initial_binding.dart

@ -2,8 +2,9 @@ import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
import 'package:problem_check_system/data/providers/local_database.dart';
import 'package:problem_check_system/data/providers/sqlite_provider.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
import 'package:problem_check_system/data/repositories/file_repository.dart';
import 'package:problem_check_system/data/repositories/problem_repository.dart';
class InitialBinding implements Bindings {
@ -17,11 +18,13 @@ class InitialBinding implements Bindings {
///
Get.put<GetStorage>(GetStorage(), permanent: true);
Get.put<HttpProvider>(HttpProvider());
Get.put<LocalDatabase>(LocalDatabase());
Get.put<SQLiteProvider>(SQLiteProvider());
Get.put<ConnectivityProvider>(ConnectivityProvider());
}
void _registerRepositories() {
Get.lazyPut<FileRepository>(() => FileRepository());
///
Get.lazyPut<AuthRepository>(
() => AuthRepository(
@ -32,7 +35,7 @@ class InitialBinding implements Bindings {
);
Get.lazyPut<ProblemRepository>(
() => ProblemRepository(
localDatabase: Get.find<LocalDatabase>(),
sqliteProvider: Get.find<SQLiteProvider>(),
httpProvider: Get.find<HttpProvider>(),
connectivityProvider: Get.find<ConnectivityProvider>(),
),

2
lib/app/routes/app_pages.dart

@ -3,7 +3,7 @@ import 'package:problem_check_system/modules/home/bindings/home_binding.dart';
import 'package:problem_check_system/modules/home/views/home_page.dart';
import 'package:problem_check_system/modules/auth/bindings/login_binding.dart';
import 'package:problem_check_system/modules/auth/views/login_page.dart';
import 'package:problem_check_system/modules/my/bingdings/change_password_binding.dart';
import 'package:problem_check_system/modules/my/bindings/change_password_binding.dart';
import 'package:problem_check_system/modules/my/views/change_password.dart';
import 'package:problem_check_system/modules/problem/views/problem_upload_page.dart';

22
lib/data/models/enum_model.dart

@ -0,0 +1,22 @@
enum SyncStatus {
///
notSynced,
///
synced,
///
modified,
}
//
enum ImageStatus {
///
local,
///
synced,
///
deleted,
}

32
lib/data/models/image_metadata_model.dart

@ -0,0 +1,32 @@
// image_metadata_model.dart
import 'package:problem_check_system/data/models/enum_model.dart';
class ImageMetadata {
final String localPath;
final String? remoteUrl;
final ImageStatus status;
ImageMetadata({
required this.localPath,
this.remoteUrl,
required this.status,
});
// For saving to SQL
Map<String, dynamic> toMap() {
return {
'localPath': localPath,
'remoteUrl': remoteUrl,
'status': status.index,
};
}
// For reading from SQL
factory ImageMetadata.fromMap(Map<String, dynamic> map) {
return ImageMetadata(
localPath: map['localPath'] as String,
remoteUrl: map['remoteUrl'] as String?,
status: ImageStatus.values[map['status'] as int],
);
}
}

57
lib/data/models/problem_model.dart

@ -1,36 +1,39 @@
import 'package:get/get.dart';
// problem_model.dart
import 'dart:convert';
import 'package:problem_check_system/data/models/enum_model.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
class Problem {
String? id;
String description;
String location;
List<String> imageUrls;
DateTime createdAt;
bool isUploaded;
List<ImageMetadata> imageUrls;
DateTime creationTime;
SyncStatus syncStatus;
String? censorTaskId;
String? bindData;
//
final RxBool isChecked = false.obs;
bool isChecked;
Problem({
this.id,
required this.description,
required this.location,
required this.imageUrls,
required this.createdAt,
required this.creationTime,
this.syncStatus = SyncStatus.notSynced,
this.censorTaskId,
this.bindData,
this.isUploaded = false,
this.isChecked = false,
});
// copyWith
// copyWith method to create a new instance with updated values
Problem copyWith({
String? id,
String? description,
String? location,
List<String>? imageUrls,
DateTime? createdAt,
bool? isUploaded,
List<ImageMetadata>? imageUrls,
DateTime? creationTime,
SyncStatus? syncStatus,
String? censorTaskId,
String? bindData,
}) {
@ -39,40 +42,42 @@ class Problem {
description: description ?? this.description,
location: location ?? this.location,
imageUrls: imageUrls ?? this.imageUrls,
createdAt: createdAt ?? this.createdAt,
isUploaded: isUploaded ?? this.isUploaded,
creationTime: creationTime ?? this.creationTime,
syncStatus: syncStatus ?? this.syncStatus,
censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData,
);
}
// toMap fromMap
// toMap method for serializing to a database-friendly Map
Map<String, dynamic> toMap() {
return {
'id': id,
'description': description,
'location': location,
'imageUrls': imageUrls.join(';;'), // 使 'imageUrls'
'createdAt': createdAt.millisecondsSinceEpoch,
'isUploaded': isUploaded ? 1 : 0,
'imageUrls': jsonEncode(imageUrls.map((meta) => meta.toMap()).toList()),
'creationTime': creationTime.millisecondsSinceEpoch,
'syncStatus': syncStatus.index,
'censorTaskId': censorTaskId,
'bindData': bindData, // 使 'bindData'
'bindData': bindData,
};
}
// fromMap toMap
// fromMap factory constructor for deserializing from a Map
factory Problem.fromMap(Map<String, dynamic> map) {
return Problem(
id: map['id'],
description: map['description'],
location: map['location'],
imageUrls: (map['imageUrls'] as String).split(
';;',
), // 使 'imageUrls'
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
isUploaded: map['isUploaded'] == 1,
imageUrls: (jsonDecode(map['imageUrls']) as List)
.map((item) => ImageMetadata.fromMap(item as Map<String, dynamic>))
.toList(),
creationTime: DateTime.fromMillisecondsSinceEpoch(
map['creationTime'] as int,
),
syncStatus: SyncStatus.values[map['syncStatus'] as int],
censorTaskId: map['censorTaskId'],
bindData: map['bindData'], // 使 'bindData'
bindData: map['bindData'],
);
}
}

148
lib/data/providers/local_database.dart

@ -1,148 +0,0 @@
import 'package:get/get.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
import '../models/problem_model.dart';
/// LocalDatabase GetxService SQLite
/// GetxService
class LocalDatabase extends GetxService {
static const String _dbName = 'problems.db';
static const String _tableName = 'problems';
/// LocalDatabase 访
late Database _database;
/// onInit GetxService
/// 使
@override
void onInit() {
super.onInit();
_initDatabase();
}
///
Future<void> _initDatabase() async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, _dbName);
_database = await openDatabase(path, version: 1, onCreate: _onCreate);
}
///
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $_tableName(
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
location TEXT NOT NULL,
imageUrls TEXT NOT NULL,
createdAt INTEGER NOT NULL,
isUploaded INTEGER NOT NULL,
censorTaskId TEXT,
bindData TEXT
)
''');
}
/// UUID
///
Future<int> insertProblem(Problem problem) async {
problem.id = const Uuid().v4();
return await _database.insert(_tableName, problem.toMap());
}
/// ID
///
Future<int> deleteProblem(String id) async {
return await _database.delete(_tableName, where: 'id = ?', whereArgs: [id]);
}
///
///
Future<int> updateProblem(Problem problem) async {
return await _database.update(
_tableName,
problem.toMap(),
where: 'id = ?',
whereArgs: [problem.id],
);
}
/// ID
/// Problem null
Future<Problem?> getProblemById(String id) async {
final List<Map<String, dynamic>> maps = await _database.query(
_tableName,
where: 'id = ?',
whereArgs: [id],
limit: 1, // 1ID是唯一的
);
if (maps.isNotEmpty) {
// Problem
return Problem.fromMap(maps.first);
} else {
// null
return null;
}
}
///
/// - `startDate`/`endDate`
/// - `uploadStatus`'已上传', '未上传', '全部'
/// - `bindStatus`'已绑定', '未绑定', '全部'
Future<List<Problem>> getProblems({
DateTime? startDate,
DateTime? endDate,
String uploadStatus = '全部',
String bindStatus = '全部',
}) async {
final List<String> whereClauses = [];
final List<dynamic> whereArgs = [];
if (startDate != null) {
whereClauses.add('createdAt >= ?');
whereArgs.add(startDate.millisecondsSinceEpoch);
}
if (endDate != null) {
whereClauses.add('createdAt <= ?');
whereArgs.add(endDate.millisecondsSinceEpoch);
}
if (uploadStatus != '全部') {
whereClauses.add('isUploaded = ?');
whereArgs.add(uploadStatus == '已上传' ? 1 : 0);
}
if (bindStatus != '全部') {
if (bindStatus == '已绑定') {
whereClauses.add('censorTaskId IS NOT NULL');
} else {
whereClauses.add('censorTaskId IS NULL');
}
}
final String whereString = whereClauses.join(' AND ');
final List<Map<String, dynamic>> maps = await _database.query(
_tableName,
where: whereString.isEmpty ? null : whereString,
whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'createdAt DESC',
);
return List.generate(maps.length, (i) {
return Problem.fromMap(maps[i]);
});
}
/// onClose GetxService
/// GetxService
@override
void onClose() {
_database.close();
super.onClose();
}
}

147
lib/data/providers/sqlite_provider.dart

@ -0,0 +1,147 @@
// sqlite_provider.dart
import 'package:get/get.dart';
import 'package:problem_check_system/data/models/enum_model.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
/// `SQLiteProvider` GetxService SQLite
///
class SQLiteProvider extends GetxService {
static const String _dbName = 'problems.db';
static const String _tableName = 'problems';
/// 访
late Database _database;
///
@override
void onInit() {
super.onInit();
_initDatabase();
}
///
Future<void> _initDatabase() async {
final databasePath = await getDatabasesPath();
final path = join(databasePath, _dbName);
_database = await openDatabase(path, version: 1, onCreate: _onCreate);
}
///
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $_tableName(
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
location TEXT NOT NULL,
imageUrls TEXT NOT NULL,
creationTime INTEGER NOT NULL,
syncStatus INTEGER NOT NULL,
censorTaskId TEXT,
bindData TEXT
)
''');
}
/// ---
/// ** (CRUD) **
///
/// `problem` `id` UUID
Future<int> insertProblem(Problem problem) async {
final problemToInsert = problem.copyWith(
id: problem.id ?? const Uuid().v4(),
);
return await _database.insert(
_tableName,
problemToInsert.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
/// ID
///
Future<int> deleteProblem(String id) async {
return await _database.delete(_tableName, where: 'id = ?', whereArgs: [id]);
}
///
///
Future<int> updateProblem(Problem problem) async {
return await _database.update(
_tableName,
problem.toMap(),
where: 'id = ?',
whereArgs: [problem.id],
);
}
/// ID
/// `Problem` `null`
Future<Problem?> getProblemById(String id) async {
final List<Map<String, dynamic>> maps = await _database.query(
_tableName,
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
if (maps.isNotEmpty) {
return Problem.fromMap(maps.first);
}
return null;
}
///
///
Future<List<Problem>> getProblems({
DateTime? startDate,
DateTime? endDate,
SyncStatus? syncStatus,
}) async {
final List<String> whereClauses = [];
final List<dynamic> whereArgs = [];
if (startDate != null) {
whereClauses.add('creationTime >= ?');
whereArgs.add(startDate.millisecondsSinceEpoch);
}
if (endDate != null) {
whereClauses.add('creationTime <= ?');
whereArgs.add(endDate.millisecondsSinceEpoch);
}
if (syncStatus != null) {
whereClauses.add('syncStatus = ?');
whereArgs.add(syncStatus.index);
}
final String? whereString = whereClauses.isNotEmpty
? whereClauses.join(' AND ')
: null;
final List<Map<String, dynamic>> maps = await _database.query(
_tableName,
where: whereString,
whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'creationTime DESC',
);
return maps.map((json) => Problem.fromMap(json)).toList();
}
/// ---
/// `GetxService`
///
@override
void onClose() {
_database.close();
super.onClose();
}
}

10
lib/data/repositories/auth_repository.dart

@ -20,7 +20,7 @@ class AuthRepository extends GetxService {
static const String _tokenKey = 'token';
static const String _refreshTokenKey = 'refresh_token';
static const String _loginKey = 'user';
static const String _rememberPassword = 'remember_password';
static const String _rememberMe = 'remember_me';
void saveToken(String token) {
storage.write(_tokenKey, token);
@ -60,12 +60,12 @@ class AuthRepository extends GetxService {
storage.remove(_loginKey);
}
void addRememberPassword(bool remembered) {
storage.write(_rememberPassword, remembered);
void addRememberMe(bool remembered) {
storage.write(_rememberMe, remembered);
}
bool getRememberPassword() {
return storage.read(_rememberPassword);
bool getRememberMe() {
return storage.read(_rememberMe) ?? false;
}
void clearAuthData() {

71
lib/data/repositories/file_repository.dart

@ -0,0 +1,71 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; // kDebugMode debugPrint
import 'package:get/get.dart' hide FormData, MultipartFile;
import 'package:path/path.dart' as p;
import 'package:problem_check_system/data/providers/http_provider.dart';
class FileRepository {
final HttpProvider _httpProvider = Get.find<HttpProvider>();
///
/// @param imageFile
/// @param cancelToken
/// @param onSendProgress
/// @return URL
Future<String> uploadImage(
File imageFile, {
required CancelToken cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
// 1. FormData multipart/form-data
final formData = FormData.fromMap({
// 'file':
'file': await MultipartFile.fromFile(
imageFile.path,
filename: p.basename(imageFile.path),
),
});
// 2. 使 HttpProvider post
final response = await _httpProvider.post(
'/api/Objects/association/problem',
data: formData,
cancelToken: cancelToken, // post
onSendProgress: onSendProgress, // post
);
// --- () ---
if (kDebugMode) {
debugPrint('服务器返回的状态码: ${response.statusCode}');
debugPrint('服务器返回的原始数据: ${response.data}');
}
// 3. URL
if (response.statusCode == 200) {
final Map<String, dynamic> data = response.data;
// URL 'url'
final imageUrl = data['url'];
if (imageUrl is String && imageUrl.isNotEmpty) {
return imageUrl;
} else {
//
throw Exception('服务器响应中未找到有效的图片URL');
}
} else {
// 200
throw Exception('上传失败,状态码: ${response.statusCode}');
}
} on DioException catch (e) {
// Dio 便
// 使 rethrow
rethrow;
} catch (e) {
//
throw Exception('图片上传发生未知错误: $e');
}
}
}

167
lib/data/repositories/problem_repository.dart

@ -1,20 +1,27 @@
import 'package:get/get.dart';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:get/get.dart' hide MultipartFile, FormData;
import 'package:problem_check_system/data/models/enum_model.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
import 'package:problem_check_system/data/providers/local_database.dart';
import 'package:problem_check_system/data/providers/sqlite_provider.dart';
import 'package:problem_check_system/data/repositories/file_repository.dart';
///
///
class ProblemRepository extends GetxService {
final LocalDatabase localDatabase;
final SQLiteProvider sqliteProvider;
final HttpProvider httpProvider;
final ConnectivityProvider connectivityProvider;
final FileRepository fileRepository = Get.find<FileRepository>();
RxBool get isOnline => connectivityProvider.isOnline;
ProblemRepository({
required this.localDatabase,
required this.sqliteProvider,
required this.httpProvider,
required this.connectivityProvider,
});
@ -23,14 +30,14 @@ class ProblemRepository extends GetxService {
///
Future<void> updateProblem(Problem problem) async {
// ID判断
final existingProblem = await localDatabase.getProblemById(problem.id!);
final existingProblem = await sqliteProvider.getProblemById(problem.id!);
if (existingProblem != null) {
//
await localDatabase.updateProblem(problem);
await sqliteProvider.updateProblem(problem);
} else {
//
await localDatabase.insertProblem(problem);
await sqliteProvider.insertProblem(problem);
}
}
@ -44,19 +51,155 @@ class ProblemRepository extends GetxService {
String uploadStatus = '全部',
String bindStatus = '全部',
}) async {
return await localDatabase.getProblems(
return await sqliteProvider.getProblems(
startDate: startDate,
endDate: endDate,
uploadStatus: uploadStatus,
bindStatus: bindStatus,
syncStatus: SyncStatus.notSynced,
);
}
Future<void> insertProblem(Problem problem) async {
await localDatabase.insertProblem(problem);
await sqliteProvider.insertProblem(problem);
}
Future<void> deleteProblem(String id) async {
await localDatabase.deleteProblem(id);
await sqliteProvider.deleteProblem(id);
}
// * /api/Objects/association/${file.name}
///
Future<Problem> uploadProblem(
Problem problem, {
required CancelToken cancelToken,
required void Function(double progress) onProgress,
}) async {
try {
final newImages = problem.imageUrls
.where((img) => img.status == ImageStatus.local)
.toList();
final totalFilesToUpload = newImages.length;
int filesUploadedCount = 0;
// 1. ImageStatus.local
final List<String> remoteUrls = [];
for (var image in newImages) {
// return
if (cancelToken.isCancelled) {
throw DioException(
requestOptions: RequestOptions(path: ''),
type: DioExceptionType.cancel,
error: '上传已取消',
);
}
final imageFile = File(image.localPath);
final url = await fileRepository.uploadImage(
imageFile,
cancelToken: cancelToken,
onSendProgress: (sent, total) {
double overallProgress =
(filesUploadedCount + (sent / total)) / totalFilesToUpload;
onProgress(overallProgress);
},
);
remoteUrls.add(url);
filesUploadedCount++;
}
onProgress(1.0); // 100%
// 2. API payload
final List<String> finalRemoteUrls = [];
int newImageIndex = 0;
for (var image in problem.imageUrls) {
if (image.status == ImageStatus.synced) {
finalRemoteUrls.add(image.remoteUrl!);
} else if (image.status == ImageStatus.local) {
finalRemoteUrls.add(remoteUrls[newImageIndex]);
newImageIndex++;
}
}
final apiPayload = {
'id': problem.id,
'description': problem.description,
'location': problem.location,
'imageUrls': finalRemoteUrls, // 使URL列表
'createdAt': problem.creationTime.toIso8601String(),
// ...
};
// 3.
final response = await httpProvider.post(
'/api/problem',
data: apiPayload,
cancelToken: cancelToken,
);
// 4.
if (response.statusCode == 200) {
final List<ImageMetadata> updatedImageMetadata = [];
int uploadedUrlIndex = 0;
for (var image in problem.imageUrls) {
if (image.status == ImageStatus.local) {
updatedImageMetadata.add(
ImageMetadata(
localPath: image.localPath,
remoteUrl: remoteUrls[uploadedUrlIndex],
status: ImageStatus.synced,
),
);
uploadedUrlIndex++;
} else {
updatedImageMetadata.add(image);
}
}
//
return problem.copyWith(
syncStatus: SyncStatus.synced,
imageUrls: updatedImageMetadata,
);
} else {
throw Exception('问题上传失败,状态码: ${response.statusCode}');
}
} on DioException {
rethrow;
}
}
///
///
Future<List<Problem>> uploadProblems(
List<Problem> problems, {
required CancelToken cancelToken,
required void Function(double progress) onProgress,
}) async {
final int totalProblems = problems.length;
final List<Problem> updatedProblems = [];
try {
for (int i = 0; i < totalProblems; i++) {
//
if (cancelToken.isCancelled) {
break;
}
final problemToUpload = problems[i];
//
final updatedProblem = await uploadProblem(
problemToUpload,
cancelToken: cancelToken,
onProgress: (progress) {
// ( + ) /
final overallProgress = (i + progress) / totalProblems;
onProgress(overallProgress);
},
);
updatedProblems.add(updatedProblem);
}
return updatedProblems;
} on DioException {
rethrow;
}
}
}

15
lib/modules/auth/controllers/login_controller.dart

@ -10,7 +10,7 @@ class LoginController extends GetxController {
final TextEditingController passwordController = TextEditingController();
final isLoading = false.obs;
final rememberPassword = false.obs;
final rememberMe = false.obs;
LoginController({required AuthRepository authRepository})
: _authRepository = authRepository;
@ -18,13 +18,12 @@ class LoginController extends GetxController {
@override
void onInit() {
super.onInit();
_loadRememberedUser();
_loadRememberedMe();
}
void _loadRememberedUser() {
final remember = _authRepository.getRememberPassword();
rememberPassword.value = remember;
if (remember) {
void _loadRememberedMe() {
rememberMe.value = _authRepository.getRememberMe();
if (rememberMe.value) {
final loginData = _authRepository.getLoginKey();
usernameController.text = loginData.username;
passwordController.text = loginData.password;
@ -64,9 +63,9 @@ class LoginController extends GetxController {
//
_authRepository.saveToken(loginResponse.token);
_authRepository.saveRefreshToken(loginResponse.refreshToken);
_authRepository.addRememberPassword(rememberPassword.value);
_authRepository.addRememberMe(rememberMe.value);
if (rememberPassword.value) {
if (rememberMe.value) {
_authRepository.addLoginKey(loginRequest);
} else {
_authRepository.removeLoginKey();

4
lib/modules/auth/views/login_page.dart

@ -127,8 +127,8 @@ class LoginPage extends GetView<LoginController> {
children: [
Obx(
() => Checkbox(
value: controller.rememberPassword.value,
onChanged: (value) => controller.rememberPassword.value = value!,
value: controller.rememberMe.value,
onChanged: (value) => controller.rememberMe.value = value!,
),
),
Text(

0
lib/modules/my/bingdings/change_password_binding.dart → lib/modules/my/bindings/change_password_binding.dart

179
lib/modules/problem/controllers/problem_controller.dart

@ -5,8 +5,6 @@ import 'package:dio/dio.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart' hide MultipartFile, FormData;
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:problem_check_system/data/repositories/problem_repository.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
@ -26,16 +24,15 @@ class ProblemController extends GetxController
final RxList<Problem> unUploadedProblems = <Problem>[].obs;
final Rx<bool> allSelected = false.obs;
final RxDouble uploadProgress = 0.0.obs;
// Dio
late CancelToken _cancelToken;
int get selectedCount {
return unUploadedProblems
.where((problem) => problem.isChecked.value)
.length;
return unUploadedProblems.where((problem) => problem.isChecked).length;
}
List<Problem> get selectedUnUploadedProblems {
return unUploadedProblems
.where((problem) => problem.isChecked.value)
.toList();
List<Problem> get selectedProblems {
return unUploadedProblems.where((problem) => problem.isChecked).toList();
}
///
@ -83,75 +80,111 @@ class ProblemController extends GetxController
void selectAll() {
final bool newState = !allSelected.value;
for (var problem in unUploadedProblems) {
problem.isChecked.value = newState;
problem.isChecked = newState;
}
allSelected.value = newState;
// _updateSelectedList();
}
void uploadProblems() async {
// if (selectedUnUploadedProblems.isEmpty) return;
// // API
// //
// selectedUnUploadedProblems.clear();
for (var problem in selectedUnUploadedProblems) {
await uploadProblem(problem);
//
Future<void> handleUpload() async {
if (selectedProblems.isEmpty) {
Get.snackbar('提示', '请选择要上传的问题');
return;
}
}
Future<bool> uploadProblem(Problem problem) async {
uploadProgress.value = 0.0;
_cancelToken = CancelToken();
//
showUploadProgressDialog();
try {
final formData = FormData.fromMap({
'description': problem.description,
'location': problem.location,
'createdAt': problem.createdAt.toIso8601String(),
'boundInfo': problem.bindData ?? '',
});
for (var imageUrl in problem.imageUrls) {
final file = File(imageUrl);
if (await file.exists()) {
formData.files.add(
MapEntry(
'images',
await MultipartFile.fromFile(
imageUrl,
filename: path.basename(imageUrl),
),
),
);
}
}
// Repository
await problemRepository.uploadProblems(
selectedProblems,
cancelToken: _cancelToken,
onProgress: (progress) {
uploadProgress.value = progress;
},
);
// final response = await httpProvider.post(
// 'https://your-server.com/api/problems',
// data: formData,
// options: Options(
// sendTimeout: const Duration(seconds: 30),
// receiveTimeout: const Duration(seconds: 30),
// ),
// );
// if (response.statusCode == 200) {
// final updatedProblem = problem.copyWith(isUploaded: true);
// await updateProblem(updatedProblem);
return true;
// } else {
// throw Exception('服务器返回错误状态码: ${response.statusCode}');
// }
Get.back();
Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM);
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
Get.snackbar('网络超时', '请检查网络连接后重试');
Get.back();
if (CancelToken.isCancel(e)) {
Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.BOTTOM);
} else {
Get.snackbar('网络错误', '上传问题失败: ${e.message}');
Get.snackbar(
'上传失败',
'错误: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
);
}
return false;
} catch (e) {
Get.snackbar('错误', '上传问题失败: $e');
return false;
Get.back();
Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.BOTTOM);
}
}
///
void showUploadProgressDialog() {
//
Get.defaultDialog(
title: '上传问题中...',
content: Obx(() {
// final progress = (uploadProgress.value * 100).toInt();
return Column(
// mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
LinearProgressIndicator(
value: uploadProgress.value,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
),
SizedBox(height: 16.h),
// Text('已完成: $progress%'),
Text('已上传: ${unUploadedProblems.length} / $selectedCount'),
],
);
}),
barrierDismissible: false, //
// "取消"
cancel: ElevatedButton(
onPressed: () {
// controller
cancelUpload();
},
child: Text('取消', style: TextStyle(color: Colors.red)),
),
);
//
// ...
}
void cancelUpload() {
//
// 1. HTTP
// 2. 0
uploadProgress.value = 0.0;
// 3.
Get.back();
// 4.
// Get.snackbar('提示', '上传已取消');
}
void uploadProblems() async {
// if (selectedUnUploadedProblems.isEmpty) return;
// // API
// //
// selectedUnUploadedProblems.clear();
// for (var problem in selectedUnUploadedProblems) {
// await uploadProblem(problem);
// }
}
// #endregion
// #region
@ -323,16 +356,16 @@ class ProblemController extends GetxController
}
Future<void> _deleteProblemImages(Problem problem) async {
for (var imagePath in problem.imageUrls) {
try {
final file = File(imagePath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
throw Exception(e);
}
}
// for (var imagePath in problem.imageUrls) {
// try {
// final file = File(imagePath);
// if (await file.exists()) {
// await file.delete();
// }
// } catch (e) {
// throw Exception(e);
// }
// }
}
///

56
lib/modules/problem/controllers/problem_form_controller.dart

@ -4,12 +4,14 @@ import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.dart';
import 'package:problem_check_system/data/models/enum_model.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'dart:io';
import '../../../data/models/problem_model.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'problem_controller.dart';
class ProblemFormController extends GetxController {
final ProblemController _problemController;
final ProblemController problemController;
final TextEditingController descriptionController = TextEditingController();
final TextEditingController locationController = TextEditingController();
final RxList<XFile> selectedImages = <XFile>[].obs;
@ -19,27 +21,23 @@ class ProblemFormController extends GetxController {
Problem? _currentProblem;
// 使便
ProblemFormController({ProblemController? problemController})
: _problemController = problemController ?? Get.find<ProblemController>();
ProblemFormController({required this.problemController});
//
bool get isEditing => _currentProblem != null;
//
///
void init(Problem? problem) {
_currentProblem = problem;
if (problem != null) {
_currentProblem = problem;
descriptionController.text = problem.description;
locationController.text = problem.location;
//
selectedImages.clear();
for (var path in problem.imageUrls) {
selectedImages.add(XFile(path));
}
// ImageMetadata
// List<String>
final imagePaths = problem.imageUrls
.map((meta) => XFile(meta.localPath))
.toList();
// selectedImages是一个RxList<String>,UI重建
selectedImages.assignAll(imagePaths);
} else {
//
_currentProblem = null;
descriptionController.clear();
locationController.clear();
selectedImages.clear();
@ -134,7 +132,7 @@ class ProblemFormController extends GetxController {
return true;
}
//
///
Future<void> saveProblem() async {
if (!_validateForm()) {
return;
@ -144,18 +142,18 @@ class ProblemFormController extends GetxController {
try {
//
final List<String> imagePaths = await _saveImagesToLocal();
final List<ImageMetadata> imagePaths = await _saveImagesToLocal();
if (isEditing && _currentProblem != null) {
if (_currentProblem != null) {
//
final updatedProblem = _currentProblem!.copyWith(
description: descriptionController.text,
location: locationController.text,
imageUrls: imagePaths,
createdAt: DateTime.now(), //
creationTime: DateTime.now(), //
);
await _problemController.updateProblem(updatedProblem);
await problemController.updateProblem(updatedProblem);
Get.back(result: true); //
Get.snackbar('成功', '问题已更新');
} else {
@ -164,11 +162,11 @@ class ProblemFormController extends GetxController {
description: descriptionController.text,
location: locationController.text,
imageUrls: imagePaths,
createdAt: DateTime.now(),
isUploaded: false,
creationTime: DateTime.now(),
syncStatus: SyncStatus.synced,
);
await _problemController.addProblem(problem);
await problemController.addProblem(problem);
Get.back(result: true); //
Get.snackbar('成功', '问题已保存');
}
@ -180,8 +178,8 @@ class ProblemFormController extends GetxController {
}
//
Future<List<String>> _saveImagesToLocal() async {
final List<String> imagePaths = [];
Future<List<ImageMetadata>> _saveImagesToLocal() async {
final List<ImageMetadata> imagePaths = [];
final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images');
@ -195,13 +193,17 @@ class ProblemFormController extends GetxController {
final String fileName =
'problem_${DateTime.now().millisecondsSinceEpoch}_${path.basename(image.name)}';
final String imagePath = '${imagesDir.path}/$fileName';
final ImageMetadata imageData = ImageMetadata(
localPath: imagePath,
status: ImageStatus.local,
);
final File imageFile = File(imagePath);
//
final imageBytes = await image.readAsBytes();
await imageFile.writeAsBytes(imageBytes);
imagePaths.add(imagePath);
imagePaths.add(imageData);
} catch (e) {
throw Exception(e);
}

13
lib/modules/problem/views/problem_form_page.dart

@ -6,21 +6,16 @@ import 'package:image_picker/image_picker.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
class ProblemFormPage extends StatelessWidget {
class ProblemFormPage extends GetView<ProblemFormController> {
final Problem? problem;
final bool isReadOnly; //
final ProblemFormController controller = Get.put(ProblemFormController());
final bool isReadOnly;
//
ProblemFormPage({super.key, this.problem, this.isReadOnly = false});
const ProblemFormPage({super.key, this.problem, this.isReadOnly = false});
@override
Widget build(BuildContext context) {
//
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.init(problem);
});
controller.init(problem);
return Scaffold(
appBar: AppBar(
flexibleSpace: Container(

4
lib/modules/problem/views/problem_list_page.dart

@ -56,10 +56,10 @@ class ProblemListPage extends GetView<ProblemController> {
controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除');
},
child: ProblemCard(problem, viewType: viewType),
child: ProblemCard(problem: problem.obs, viewType: viewType),
);
} else {
return ProblemCard(problem, viewType: viewType);
return ProblemCard(problem: problem.obs, viewType: viewType);
}
}

26
lib/modules/problem/views/problem_upload_page.dart

@ -59,7 +59,7 @@ class ProblemUploadPage extends GetView<ProblemController> {
child: Obx(
() => ElevatedButton(
onPressed: controller.selectedCount > 0
? controller.uploadProblems
? controller.handleUpload
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
@ -73,28 +73,4 @@ class ProblemUploadPage extends GetView<ProblemController> {
),
);
}
Widget uploadProgressWidget() {
return AlertDialog(
title: Text('上传中...'),
content: Obx(() {
final progress = (controller.uploadProgress.value * 100).toInt();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
value: controller.uploadProgress.value,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
SizedBox(height: 16),
Text('已完成: $progress%'),
Text(
'已上传: ${controller.unUploadedProblems.length} / ${controller.selectedCount}',
),
],
);
}),
);
}
}

33
lib/modules/problem/views/widgets/problem_card.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:problem_check_system/data/models/enum_model.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart';
@ -12,12 +13,12 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends GetView<ProblemController> {
final Problem problem;
final Rx<Problem> problem;
final ProblemCardViewType viewType;
const ProblemCard(
this.problem, {
const ProblemCard({
super.key,
required this.problem,
this.viewType = ProblemCardViewType.buttons,
});
@ -37,7 +38,7 @@ class ProblemCard extends GetView<ProblemController> {
subtitle: LayoutBuilder(
builder: (context, constraints) {
return Text(
problem.description,
problem.value.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp),
@ -53,7 +54,10 @@ class ProblemCard extends GetView<ProblemController> {
children: [
Icon(Icons.location_on, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w),
Text(problem.location, style: TextStyle(fontSize: 12.sp)),
Text(
problem.value.location,
style: TextStyle(fontSize: 12.sp),
),
],
),
SizedBox(width: 16.w),
@ -62,7 +66,9 @@ class ProblemCard extends GetView<ProblemController> {
Icon(Icons.access_time, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w),
Text(
DateFormat('yyyy-MM-dd HH:mm').format(problem.createdAt),
DateFormat(
'yyyy-MM-dd HH:mm',
).format(problem.value.creationTime),
style: TextStyle(fontSize: 12.sp),
),
],
@ -77,10 +83,11 @@ class ProblemCard extends GetView<ProblemController> {
Wrap(
spacing: 8,
children: [
problem.isUploaded
problem.value.syncStatus == SyncStatus.synced
? TDTag('已上传', isLight: true, theme: TDTagTheme.success)
: TDTag('未上传', isLight: true, theme: TDTagTheme.danger),
problem.bindData != null && problem.bindData!.isNotEmpty
problem.value.bindData != null &&
problem.value.bindData!.isNotEmpty
? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary)
: TDTag('未绑定', isLight: true, theme: TDTagTheme.warning),
],
@ -104,14 +111,16 @@ class ProblemCard extends GetView<ProblemController> {
CustomButton(
text: '修改',
onTap: () {
Get.to(ProblemFormPage(problem: problem));
Get.to(ProblemFormPage(problem: problem.value));
},
),
SizedBox(width: 8.w),
CustomButton(
text: '查看',
onTap: () {
Get.to(ProblemFormPage(problem: problem, isReadOnly: true));
Get.to(
ProblemFormPage(problem: problem.value, isReadOnly: true),
);
},
),
SizedBox(width: 16.w),
@ -123,10 +132,10 @@ class ProblemCard extends GetView<ProblemController> {
child: Obx(
() => Checkbox(
// Checkbox value controller.isChecked.value
value: problem.isChecked.value,
value: problem.value.isChecked,
// Checkbox controller
onChanged: (bool? value) {
problem.isChecked.value = value ?? false;
problem.value.isChecked = value ?? false;
controller.onProblemCheckedChange();
},
),

Loading…
Cancel
Save