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. 177
      lib/modules/problem/controllers/problem_controller.dart
  15. 56
      lib/modules/problem/controllers/problem_form_controller.dart
  16. 11
      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:get_storage/get_storage.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.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/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/auth_repository.dart';
import 'package:problem_check_system/data/repositories/file_repository.dart';
import 'package:problem_check_system/data/repositories/problem_repository.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart';
class InitialBinding implements Bindings { class InitialBinding implements Bindings {
@ -17,11 +18,13 @@ class InitialBinding implements Bindings {
/// ///
Get.put<GetStorage>(GetStorage(), permanent: true); Get.put<GetStorage>(GetStorage(), permanent: true);
Get.put<HttpProvider>(HttpProvider()); Get.put<HttpProvider>(HttpProvider());
Get.put<LocalDatabase>(LocalDatabase()); Get.put<SQLiteProvider>(SQLiteProvider());
Get.put<ConnectivityProvider>(ConnectivityProvider()); Get.put<ConnectivityProvider>(ConnectivityProvider());
} }
void _registerRepositories() { void _registerRepositories() {
Get.lazyPut<FileRepository>(() => FileRepository());
/// ///
Get.lazyPut<AuthRepository>( Get.lazyPut<AuthRepository>(
() => AuthRepository( () => AuthRepository(
@ -32,7 +35,7 @@ class InitialBinding implements Bindings {
); );
Get.lazyPut<ProblemRepository>( Get.lazyPut<ProblemRepository>(
() => ProblemRepository( () => ProblemRepository(
localDatabase: Get.find<LocalDatabase>(), sqliteProvider: Get.find<SQLiteProvider>(),
httpProvider: Get.find<HttpProvider>(), httpProvider: Get.find<HttpProvider>(),
connectivityProvider: Get.find<ConnectivityProvider>(), 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/home/views/home_page.dart';
import 'package:problem_check_system/modules/auth/bindings/login_binding.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/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/my/views/change_password.dart';
import 'package:problem_check_system/modules/problem/views/problem_upload_page.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 { class Problem {
String? id; String? id;
String description; String description;
String location; String location;
List<String> imageUrls; List<ImageMetadata> imageUrls;
DateTime createdAt; DateTime creationTime;
bool isUploaded; SyncStatus syncStatus;
String? censorTaskId; String? censorTaskId;
String? bindData; String? bindData;
// bool isChecked;
final RxBool isChecked = false.obs;
Problem({ Problem({
this.id, this.id,
required this.description, required this.description,
required this.location, required this.location,
required this.imageUrls, required this.imageUrls,
required this.createdAt, required this.creationTime,
this.syncStatus = SyncStatus.notSynced,
this.censorTaskId, this.censorTaskId,
this.bindData, this.bindData,
this.isUploaded = false, this.isChecked = false,
}); });
// copyWith // copyWith method to create a new instance with updated values
Problem copyWith({ Problem copyWith({
String? id, String? id,
String? description, String? description,
String? location, String? location,
List<String>? imageUrls, List<ImageMetadata>? imageUrls,
DateTime? createdAt, DateTime? creationTime,
bool? isUploaded, SyncStatus? syncStatus,
String? censorTaskId, String? censorTaskId,
String? bindData, String? bindData,
}) { }) {
@ -39,40 +42,42 @@ class Problem {
description: description ?? this.description, description: description ?? this.description,
location: location ?? this.location, location: location ?? this.location,
imageUrls: imageUrls ?? this.imageUrls, imageUrls: imageUrls ?? this.imageUrls,
createdAt: createdAt ?? this.createdAt, creationTime: creationTime ?? this.creationTime,
isUploaded: isUploaded ?? this.isUploaded, syncStatus: syncStatus ?? this.syncStatus,
censorTaskId: censorTaskId ?? this.censorTaskId, censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData, bindData: bindData ?? this.bindData,
); );
} }
// toMap fromMap // toMap method for serializing to a database-friendly Map
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'description': description, 'description': description,
'location': location, 'location': location,
'imageUrls': imageUrls.join(';;'), // 使 'imageUrls' 'imageUrls': jsonEncode(imageUrls.map((meta) => meta.toMap()).toList()),
'createdAt': createdAt.millisecondsSinceEpoch, 'creationTime': creationTime.millisecondsSinceEpoch,
'isUploaded': isUploaded ? 1 : 0, 'syncStatus': syncStatus.index,
'censorTaskId': censorTaskId, 'censorTaskId': censorTaskId,
'bindData': bindData, // 使 'bindData' 'bindData': bindData,
}; };
} }
// fromMap toMap // fromMap factory constructor for deserializing from a Map
factory Problem.fromMap(Map<String, dynamic> map) { factory Problem.fromMap(Map<String, dynamic> map) {
return Problem( return Problem(
id: map['id'], id: map['id'],
description: map['description'], description: map['description'],
location: map['location'], location: map['location'],
imageUrls: (map['imageUrls'] as String).split( imageUrls: (jsonDecode(map['imageUrls']) as List)
';;', .map((item) => ImageMetadata.fromMap(item as Map<String, dynamic>))
), // 使 'imageUrls' .toList(),
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), creationTime: DateTime.fromMillisecondsSinceEpoch(
isUploaded: map['isUploaded'] == 1, map['creationTime'] as int,
),
syncStatus: SyncStatus.values[map['syncStatus'] as int],
censorTaskId: map['censorTaskId'], 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 _tokenKey = 'token';
static const String _refreshTokenKey = 'refresh_token'; static const String _refreshTokenKey = 'refresh_token';
static const String _loginKey = 'user'; static const String _loginKey = 'user';
static const String _rememberPassword = 'remember_password'; static const String _rememberMe = 'remember_me';
void saveToken(String token) { void saveToken(String token) {
storage.write(_tokenKey, token); storage.write(_tokenKey, token);
@ -60,12 +60,12 @@ class AuthRepository extends GetxService {
storage.remove(_loginKey); storage.remove(_loginKey);
} }
void addRememberPassword(bool remembered) { void addRememberMe(bool remembered) {
storage.write(_rememberPassword, remembered); storage.write(_rememberMe, remembered);
} }
bool getRememberPassword() { bool getRememberMe() {
return storage.read(_rememberPassword); return storage.read(_rememberMe) ?? false;
} }
void clearAuthData() { 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/models/problem_model.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.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/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 { class ProblemRepository extends GetxService {
final LocalDatabase localDatabase; final SQLiteProvider sqliteProvider;
final HttpProvider httpProvider; final HttpProvider httpProvider;
final ConnectivityProvider connectivityProvider; final ConnectivityProvider connectivityProvider;
final FileRepository fileRepository = Get.find<FileRepository>();
RxBool get isOnline => connectivityProvider.isOnline; RxBool get isOnline => connectivityProvider.isOnline;
ProblemRepository({ ProblemRepository({
required this.localDatabase, required this.sqliteProvider,
required this.httpProvider, required this.httpProvider,
required this.connectivityProvider, required this.connectivityProvider,
}); });
@ -23,14 +30,14 @@ class ProblemRepository extends GetxService {
/// ///
Future<void> updateProblem(Problem problem) async { Future<void> updateProblem(Problem problem) async {
// ID判断 // ID判断
final existingProblem = await localDatabase.getProblemById(problem.id!); final existingProblem = await sqliteProvider.getProblemById(problem.id!);
if (existingProblem != null) { if (existingProblem != null) {
// //
await localDatabase.updateProblem(problem); await sqliteProvider.updateProblem(problem);
} else { } else {
// //
await localDatabase.insertProblem(problem); await sqliteProvider.insertProblem(problem);
} }
} }
@ -44,19 +51,155 @@ class ProblemRepository extends GetxService {
String uploadStatus = '全部', String uploadStatus = '全部',
String bindStatus = '全部', String bindStatus = '全部',
}) async { }) async {
return await localDatabase.getProblems( return await sqliteProvider.getProblems(
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
uploadStatus: uploadStatus, syncStatus: SyncStatus.notSynced,
bindStatus: bindStatus,
); );
} }
Future<void> insertProblem(Problem problem) async { Future<void> insertProblem(Problem problem) async {
await localDatabase.insertProblem(problem); await sqliteProvider.insertProblem(problem);
} }
Future<void> deleteProblem(String id) async { 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 TextEditingController passwordController = TextEditingController();
final isLoading = false.obs; final isLoading = false.obs;
final rememberPassword = false.obs; final rememberMe = false.obs;
LoginController({required AuthRepository authRepository}) LoginController({required AuthRepository authRepository})
: _authRepository = authRepository; : _authRepository = authRepository;
@ -18,13 +18,12 @@ class LoginController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadRememberedUser(); _loadRememberedMe();
} }
void _loadRememberedUser() { void _loadRememberedMe() {
final remember = _authRepository.getRememberPassword(); rememberMe.value = _authRepository.getRememberMe();
rememberPassword.value = remember; if (rememberMe.value) {
if (remember) {
final loginData = _authRepository.getLoginKey(); final loginData = _authRepository.getLoginKey();
usernameController.text = loginData.username; usernameController.text = loginData.username;
passwordController.text = loginData.password; passwordController.text = loginData.password;
@ -64,9 +63,9 @@ class LoginController extends GetxController {
// //
_authRepository.saveToken(loginResponse.token); _authRepository.saveToken(loginResponse.token);
_authRepository.saveRefreshToken(loginResponse.refreshToken); _authRepository.saveRefreshToken(loginResponse.refreshToken);
_authRepository.addRememberPassword(rememberPassword.value); _authRepository.addRememberMe(rememberMe.value);
if (rememberPassword.value) { if (rememberMe.value) {
_authRepository.addLoginKey(loginRequest); _authRepository.addLoginKey(loginRequest);
} else { } else {
_authRepository.removeLoginKey(); _authRepository.removeLoginKey();

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

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

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

177
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:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart' hide MultipartFile, FormData; import 'package:get/get.dart' hide MultipartFile, FormData;
import 'package:flutter/material.dart'; 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/data/repositories/problem_repository.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.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'; 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 RxList<Problem> unUploadedProblems = <Problem>[].obs;
final Rx<bool> allSelected = false.obs; final Rx<bool> allSelected = false.obs;
final RxDouble uploadProgress = 0.0.obs; final RxDouble uploadProgress = 0.0.obs;
// Dio
late CancelToken _cancelToken;
int get selectedCount { int get selectedCount {
return unUploadedProblems return unUploadedProblems.where((problem) => problem.isChecked).length;
.where((problem) => problem.isChecked.value)
.length;
} }
List<Problem> get selectedUnUploadedProblems { List<Problem> get selectedProblems {
return unUploadedProblems return unUploadedProblems.where((problem) => problem.isChecked).toList();
.where((problem) => problem.isChecked.value)
.toList();
} }
/// ///
@ -83,75 +80,111 @@ class ProblemController extends GetxController
void selectAll() { void selectAll() {
final bool newState = !allSelected.value; final bool newState = !allSelected.value;
for (var problem in unUploadedProblems) { for (var problem in unUploadedProblems) {
problem.isChecked.value = newState; problem.isChecked = newState;
} }
allSelected.value = newState; allSelected.value = newState;
// _updateSelectedList(); // _updateSelectedList();
} }
void uploadProblems() async { //
// if (selectedUnUploadedProblems.isEmpty) return; Future<void> handleUpload() async {
// // API if (selectedProblems.isEmpty) {
// // Get.snackbar('提示', '请选择要上传的问题');
// selectedUnUploadedProblems.clear(); return;
for (var problem in selectedUnUploadedProblems) {
await uploadProblem(problem);
}
} }
Future<bool> uploadProblem(Problem problem) async { uploadProgress.value = 0.0;
_cancelToken = CancelToken();
//
showUploadProgressDialog();
try { try {
final formData = FormData.fromMap({ // Repository
'description': problem.description, await problemRepository.uploadProblems(
'location': problem.location, selectedProblems,
'createdAt': problem.createdAt.toIso8601String(), cancelToken: _cancelToken,
'boundInfo': problem.bindData ?? '', onProgress: (progress) {
}); uploadProgress.value = progress;
},
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),
),
),
); );
}
}
// final response = await httpProvider.post( Get.back();
// 'https://your-server.com/api/problems', Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM);
// 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}');
// }
} on DioException catch (e) { } on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout || Get.back();
e.type == DioExceptionType.receiveTimeout) { if (CancelToken.isCancel(e)) {
Get.snackbar('网络超时', '请检查网络连接后重试'); Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.BOTTOM);
} else { } else {
Get.snackbar('网络错误', '上传问题失败: ${e.message}'); Get.snackbar(
'上传失败',
'错误: ${e.message}',
snackPosition: SnackPosition.BOTTOM,
);
} }
return false;
} catch (e) { } catch (e) {
Get.snackbar('错误', '上传问题失败: $e'); Get.back();
return false; 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 // #endregion
// #region // #region
@ -323,16 +356,16 @@ class ProblemController extends GetxController
} }
Future<void> _deleteProblemImages(Problem problem) async { Future<void> _deleteProblemImages(Problem problem) async {
for (var imagePath in problem.imageUrls) { // for (var imagePath in problem.imageUrls) {
try { // try {
final file = File(imagePath); // final file = File(imagePath);
if (await file.exists()) { // if (await file.exists()) {
await file.delete(); // await file.delete();
} // }
} catch (e) { // } catch (e) {
throw Exception(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:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.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 'dart:io';
import '../../../data/models/problem_model.dart'; import 'package:problem_check_system/data/models/problem_model.dart';
import 'problem_controller.dart'; import 'problem_controller.dart';
class ProblemFormController extends GetxController { class ProblemFormController extends GetxController {
final ProblemController _problemController; final ProblemController problemController;
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
final TextEditingController locationController = TextEditingController(); final TextEditingController locationController = TextEditingController();
final RxList<XFile> selectedImages = <XFile>[].obs; final RxList<XFile> selectedImages = <XFile>[].obs;
@ -19,27 +21,23 @@ class ProblemFormController extends GetxController {
Problem? _currentProblem; Problem? _currentProblem;
// 使便 // 使便
ProblemFormController({ProblemController? problemController}) ProblemFormController({required this.problemController});
: _problemController = problemController ?? Get.find<ProblemController>();
// ///
bool get isEditing => _currentProblem != null;
//
void init(Problem? problem) { void init(Problem? problem) {
if (problem != null) {
_currentProblem = problem; _currentProblem = problem;
if (problem != null) {
descriptionController.text = problem.description; descriptionController.text = problem.description;
locationController.text = problem.location; locationController.text = problem.location;
// // ImageMetadata
selectedImages.clear(); // List<String>
for (var path in problem.imageUrls) { final imagePaths = problem.imageUrls
selectedImages.add(XFile(path)); .map((meta) => XFile(meta.localPath))
} .toList();
// selectedImages是一个RxList<String>,UI重建
selectedImages.assignAll(imagePaths);
} else { } else {
//
_currentProblem = null;
descriptionController.clear(); descriptionController.clear();
locationController.clear(); locationController.clear();
selectedImages.clear(); selectedImages.clear();
@ -134,7 +132,7 @@ class ProblemFormController extends GetxController {
return true; return true;
} }
// ///
Future<void> saveProblem() async { Future<void> saveProblem() async {
if (!_validateForm()) { if (!_validateForm()) {
return; return;
@ -144,18 +142,18 @@ class ProblemFormController extends GetxController {
try { try {
// //
final List<String> imagePaths = await _saveImagesToLocal(); final List<ImageMetadata> imagePaths = await _saveImagesToLocal();
if (isEditing && _currentProblem != null) { if (_currentProblem != null) {
// //
final updatedProblem = _currentProblem!.copyWith( final updatedProblem = _currentProblem!.copyWith(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
createdAt: DateTime.now(), // creationTime: DateTime.now(), //
); );
await _problemController.updateProblem(updatedProblem); await problemController.updateProblem(updatedProblem);
Get.back(result: true); // Get.back(result: true); //
Get.snackbar('成功', '问题已更新'); Get.snackbar('成功', '问题已更新');
} else { } else {
@ -164,11 +162,11 @@ class ProblemFormController extends GetxController {
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
createdAt: DateTime.now(), creationTime: DateTime.now(),
isUploaded: false, syncStatus: SyncStatus.synced,
); );
await _problemController.addProblem(problem); await problemController.addProblem(problem);
Get.back(result: true); // Get.back(result: true); //
Get.snackbar('成功', '问题已保存'); Get.snackbar('成功', '问题已保存');
} }
@ -180,8 +178,8 @@ class ProblemFormController extends GetxController {
} }
// //
Future<List<String>> _saveImagesToLocal() async { Future<List<ImageMetadata>> _saveImagesToLocal() async {
final List<String> imagePaths = []; final List<ImageMetadata> imagePaths = [];
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images'); final imagesDir = Directory('${directory.path}/problem_images');
@ -195,13 +193,17 @@ class ProblemFormController extends GetxController {
final String fileName = final String fileName =
'problem_${DateTime.now().millisecondsSinceEpoch}_${path.basename(image.name)}'; 'problem_${DateTime.now().millisecondsSinceEpoch}_${path.basename(image.name)}';
final String imagePath = '${imagesDir.path}/$fileName'; final String imagePath = '${imagesDir.path}/$fileName';
final ImageMetadata imageData = ImageMetadata(
localPath: imagePath,
status: ImageStatus.local,
);
final File imageFile = File(imagePath); final File imageFile = File(imagePath);
// //
final imageBytes = await image.readAsBytes(); final imageBytes = await image.readAsBytes();
await imageFile.writeAsBytes(imageBytes); await imageFile.writeAsBytes(imageBytes);
imagePaths.add(imagePath); imagePaths.add(imageData);
} catch (e) { } catch (e) {
throw Exception(e); throw Exception(e);
} }

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

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

@ -56,10 +56,10 @@ class ProblemListPage extends GetView<ProblemController> {
controller.deleteProblem(problem); controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除'); Get.snackbar('成功', '问题已删除');
}, },
child: ProblemCard(problem, viewType: viewType), child: ProblemCard(problem: problem.obs, viewType: viewType),
); );
} else { } 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( child: Obx(
() => ElevatedButton( () => ElevatedButton(
onPressed: controller.selectedCount > 0 onPressed: controller.selectedCount > 0
? controller.uploadProblems ? controller.handleUpload
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, 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:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.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/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_button.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 } enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends GetView<ProblemController> { class ProblemCard extends GetView<ProblemController> {
final Problem problem; final Rx<Problem> problem;
final ProblemCardViewType viewType; final ProblemCardViewType viewType;
const ProblemCard( const ProblemCard({
this.problem, {
super.key, super.key,
required this.problem,
this.viewType = ProblemCardViewType.buttons, this.viewType = ProblemCardViewType.buttons,
}); });
@ -37,7 +38,7 @@ class ProblemCard extends GetView<ProblemController> {
subtitle: LayoutBuilder( subtitle: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Text( return Text(
problem.description, problem.value.description,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp), style: TextStyle(fontSize: 14.sp),
@ -53,7 +54,10 @@ class ProblemCard extends GetView<ProblemController> {
children: [ children: [
Icon(Icons.location_on, color: Colors.grey, size: 16.h), Icon(Icons.location_on, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w), 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), SizedBox(width: 16.w),
@ -62,7 +66,9 @@ class ProblemCard extends GetView<ProblemController> {
Icon(Icons.access_time, color: Colors.grey, size: 16.h), Icon(Icons.access_time, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( 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), style: TextStyle(fontSize: 12.sp),
), ),
], ],
@ -77,10 +83,11 @@ class ProblemCard extends GetView<ProblemController> {
Wrap( Wrap(
spacing: 8, spacing: 8,
children: [ children: [
problem.isUploaded problem.value.syncStatus == SyncStatus.synced
? TDTag('已上传', isLight: true, theme: TDTagTheme.success) ? TDTag('已上传', isLight: true, theme: TDTagTheme.success)
: TDTag('未上传', isLight: true, theme: TDTagTheme.danger), : 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.primary)
: TDTag('未绑定', isLight: true, theme: TDTagTheme.warning), : TDTag('未绑定', isLight: true, theme: TDTagTheme.warning),
], ],
@ -104,14 +111,16 @@ class ProblemCard extends GetView<ProblemController> {
CustomButton( CustomButton(
text: '修改', text: '修改',
onTap: () { onTap: () {
Get.to(ProblemFormPage(problem: problem)); Get.to(ProblemFormPage(problem: problem.value));
}, },
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
CustomButton( CustomButton(
text: '查看', text: '查看',
onTap: () { onTap: () {
Get.to(ProblemFormPage(problem: problem, isReadOnly: true)); Get.to(
ProblemFormPage(problem: problem.value, isReadOnly: true),
);
}, },
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
@ -123,10 +132,10 @@ class ProblemCard extends GetView<ProblemController> {
child: Obx( child: Obx(
() => Checkbox( () => Checkbox(
// Checkbox value controller.isChecked.value // Checkbox value controller.isChecked.value
value: problem.isChecked.value, value: problem.value.isChecked,
// Checkbox controller // Checkbox controller
onChanged: (bool? value) { onChanged: (bool? value) {
problem.isChecked.value = value ?? false; problem.value.isChecked = value ?? false;
controller.onProblemCheckedChange(); controller.onProblemCheckedChange();
}, },
), ),

Loading…
Cancel
Save