81 changed files with 1333 additions and 470 deletions
@ -0,0 +1,60 @@
|
||||
/// 代表企业类型的枚举 |
||||
enum CompanyType { |
||||
// 枚举成员 (使用英文/拼音) -> 关联的服务端字符串值 |
||||
production('生产'), |
||||
usage('使用'), |
||||
storage('储存'), |
||||
business('经营'); |
||||
|
||||
/// 构造函数,用于关联字符串值 |
||||
const CompanyType(this.serverValue); |
||||
|
||||
/// 存储与服务端对应的字符串值 |
||||
final String serverValue; |
||||
|
||||
/// 用于在UI中显示的文本 (这里直接复用 serverValue) |
||||
String get displayText => serverValue; |
||||
|
||||
/// [静态辅助方法] 从服务端的字符串解析成 CompanyType 枚举。 |
||||
/// |
||||
/// 如果传入的字符串无法匹配任何类型,会返回 `null`,这是一种安全处理未知值的方式。 |
||||
static CompanyType? fromServerValue(String? value) { |
||||
if (value == null) return null; |
||||
for (final type in values) { |
||||
// `values` 是枚举自带的、包含所有成员的列表 |
||||
if (type.serverValue == value) { |
||||
return type; |
||||
} |
||||
} |
||||
return null; // 没有找到匹配项 |
||||
} |
||||
} |
||||
|
||||
/// 代表企业规模的枚举 |
||||
enum CompanyScope { |
||||
// 枚举成员 -> 关联的服务端字符串值 |
||||
micro('微型'), |
||||
small('小型'), |
||||
medium('中型'), |
||||
large('大型'); |
||||
|
||||
/// 构造函数,用于关联字符串值 |
||||
const CompanyScope(this.serverValue); |
||||
|
||||
/// 存储与服务端对应的字符串值 |
||||
final String serverValue; |
||||
|
||||
/// 用于在UI中显示的文本 |
||||
String get displayText => serverValue; |
||||
|
||||
/// [静态辅助方法] 从服务端的字符串解析成 CompanyScope 枚举。 |
||||
static CompanyScope? fromServerValue(String? value) { |
||||
if (value == null) return null; |
||||
for (final scope in values) { |
||||
if (scope.serverValue == value) { |
||||
return scope; |
||||
} |
||||
} |
||||
return null; // 没有找到匹配项 |
||||
} |
||||
} |
||||
@ -1,5 +1,5 @@
|
||||
// image_metadata_model.dart |
||||
import 'package:problem_check_system/app/core/data/models/image_status.dart'; |
||||
import 'package:problem_check_system/app/core/models/image_status.dart'; |
||||
|
||||
class ImageMetadata { |
||||
final String localPath; |
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:convert'; |
||||
|
||||
import 'package:problem_check_system/app/core/data/models/image_metadata_model.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
|
||||
/// 问题的数据模型。 |
||||
/// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 |
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/image_metadata_model.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
import 'package:uuid/uuid.dart'; |
||||
|
||||
enum ProblemSyncStatus { |
||||
@ -0,0 +1,24 @@
|
||||
enum SyncStatus { |
||||
/// 未跟踪 - 需要被移除的记录(如本地删除但从未同步过) |
||||
untracked, |
||||
|
||||
/// 已同步 - 与服务器完全一致(类似git的unmodified) |
||||
synced, |
||||
|
||||
/// 待创建 - 新问题,需要上传到服务器(类似git的untracked → staged) |
||||
pendingCreate, |
||||
|
||||
/// 待更新 - 已修改的问题,需要更新到服务器(类似git的modified → staged) |
||||
pendingUpdate, |
||||
|
||||
/// 待删除 - 已标记删除,需要从服务器删除(类似git的deleted → staged) |
||||
pendingDelete, |
||||
} |
||||
|
||||
/// 一个抽象接口,定义了所有“可同步”实体的共同特征。 |
||||
/// 任何需要离线同步功能的实体都应该实现这个接口。 |
||||
abstract class SyncableEntity { |
||||
String get id; |
||||
SyncStatus get syncStatus; |
||||
DateTime get lastModifiedTime; // 同步时通常需要最后修改时间 |
||||
} |
||||
@ -1,10 +1,10 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:get_storage/get_storage.dart'; |
||||
import 'package:problem_check_system/app/core/utils/constants/api_endpoints.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/auth_model.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/user/user.dart'; |
||||
import 'package:problem_check_system/app/core/data/services/network_status_service.dart'; |
||||
import 'package:problem_check_system/app/core/data/services/http_provider.dart'; |
||||
import 'package:problem_check_system/app/core/models/auth_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/user/user.dart'; |
||||
import 'package:problem_check_system/app/core/services/network_status_service.dart'; |
||||
import 'package:problem_check_system/app/core/services/http_provider.dart'; |
||||
|
||||
class AuthRepository extends GetxService { |
||||
final HttpProvider httpProvider; |
||||
@ -0,0 +1,15 @@
|
||||
import 'package:problem_check_system/app/core/models/sync_status.dart'; |
||||
|
||||
abstract class SyncableRepository<T extends SyncableEntity> { |
||||
// --- 上传部分 (已有) --- |
||||
Future<T> syncCreate(T item); |
||||
Future<T> syncUpdate(T item); |
||||
Future<void> syncDelete(String id); |
||||
|
||||
// --- [新] 下载部分 --- |
||||
/// 从服务器拉取自上次同步以来的更新。 |
||||
/// [lastSyncTimestamp]: 客户端记录的最后一次成功同步的时间戳。 |
||||
/// 服务器应返回此时间戳之后所有新建或更新的记录。 |
||||
/// 返回成功在本地保存或更新的实体列表。 |
||||
Future<List<T>> pullFromServer(DateTime? lastSyncTimestamp); |
||||
} |
||||
@ -1,5 +1,6 @@
|
||||
abstract class AppRoutes { |
||||
// 命名路由,使用 const 常量 |
||||
static const navigation = '/navigation'; |
||||
static const home = '/home'; |
||||
static const login = '/login'; |
||||
|
||||
@ -0,0 +1,105 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
import 'package:path/path.dart'; |
||||
|
||||
/// 创建问题表的脚本 |
||||
const String _createProblemsTable = ''' |
||||
CREATE TABLE problems( |
||||
id TEXT PRIMARY KEY, |
||||
description TEXT NOT NULL, |
||||
location TEXT NOT NULL, |
||||
imageUrls TEXT NOT NULL, |
||||
creationTime INTEGER NOT NULL, |
||||
creatorId TEXT NOT NULL, |
||||
lastModifiedTime INTEGER NOT NULL, |
||||
syncStatus INTEGER NOT NULL, |
||||
censorTaskId TEXT, |
||||
bindData TEXT, |
||||
isChecked INTEGER NOT NULL |
||||
) |
||||
'''; |
||||
|
||||
/// 创建企业表的脚本 |
||||
const String _createEnterprisesTable = ''' |
||||
CREATE TABLE enterprises( |
||||
id TEXT PRIMARY KEY, |
||||
syncStatus INTEGER NOT NULL, |
||||
lastModifiedTime INTEGER NOT NULL, |
||||
creationTime INTEGER NOT NULL, |
||||
name TEXT NOT NULL, |
||||
type TEXT NOT NULL, |
||||
address TEXT, |
||||
scale TEXT, |
||||
contactPerson TEXT, |
||||
contactPhone TEXT, |
||||
majorHazardsDescription TEXT |
||||
) |
||||
'''; |
||||
|
||||
/// 数据库服务,负责管理数据库的生命周期。 |
||||
/// 这是一个核心服务,只处理连接、创建和升级,不关心具体的数据操作。 |
||||
class DatabaseService extends GetxService { |
||||
static const String _dbName = 'problems.db'; |
||||
static const int _dbVersion = 1; |
||||
|
||||
Database? _database; |
||||
|
||||
/// 提供一个公共的 getter 来安全地访问数据库实例。 |
||||
Future<Database> get database async { |
||||
if (_database != null) { |
||||
return _database!; |
||||
} |
||||
await _initDatabase(); |
||||
return _database!; |
||||
} |
||||
|
||||
/// 异步初始化数据库连接 |
||||
Future<void> _initDatabase() async { |
||||
try { |
||||
final databasePath = await getDatabasesPath(); |
||||
final path = join(databasePath, _dbName); |
||||
|
||||
_database = await openDatabase( |
||||
path, |
||||
version: _dbVersion, |
||||
onCreate: _onCreate, |
||||
onUpgrade: _onUpgrade, |
||||
); |
||||
|
||||
Get.log('数据库初始化成功'); |
||||
} catch (e) { |
||||
Get.log('数据库初始化失败:$e', isError: true); |
||||
rethrow; |
||||
} |
||||
} |
||||
|
||||
/// 数据库创建时的回调函数 |
||||
Future<void> _onCreate(Database db, int version) async { |
||||
// 创建问题表 |
||||
await db.execute(_createProblemsTable); |
||||
Get.log('`problems` 表创建成功'); |
||||
|
||||
// 创建企业表 |
||||
await db.execute(_createEnterprisesTable); |
||||
Get.log('`enterprises` 表创建成功'); |
||||
} |
||||
|
||||
/// 数据库版本升级处理 |
||||
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { |
||||
Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...'); |
||||
// 实际项目中,这里应包含每个版本的迁移逻辑 |
||||
} |
||||
|
||||
@override |
||||
void onInit() { |
||||
super.onInit(); |
||||
_initDatabase(); |
||||
} |
||||
|
||||
@override |
||||
void onClose() { |
||||
_database?.close(); |
||||
Get.log('数据库连接已关闭'); |
||||
super.onClose(); |
||||
} |
||||
} |
||||
@ -1,8 +1,8 @@
|
||||
// sqlite_provider.dart |
||||
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
import 'package:path/path.dart'; |
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import 'package:get_storage/get_storage.dart'; |
||||
|
||||
/// SyncMetadataService 负责持久化和管理数据同步过程中的元数据。 |
||||
/// |
||||
/// 它的核心职责是记录每个可同步实体类型 (如 Problem, Enterprise) |
||||
/// 最后一次成功从服务器拉取更新的时间戳。 |
||||
/// |
||||
/// 这个服务使用 `get_storage` 实现,因为它提供了高性能的同步读写API。 |
||||
class SyncMetadataService { |
||||
// 使用一个命名的 "box" (容器) 来隔离同步相关的元数据 |
||||
final GetStorage _box = GetStorage('sync_metadata'); |
||||
|
||||
/// 为给定的实体类型生成一个唯一的、可预测的存储键。 |
||||
/// 例如,对于 `Problem` 类型,它会返回 'last_sync_timestamp_Problem'。 |
||||
String _getKeyForEntityType(Type type) { |
||||
return 'last_sync_timestamp_$type'; |
||||
} |
||||
|
||||
/// 获取特定实体类型的最后同步时间戳。 |
||||
/// |
||||
/// [entityType]: 业务实体的类型,例如 `Problem` 或 `Enterprise`。 |
||||
/// |
||||
/// 如果从未同步过该类型,则返回 `null`。 |
||||
DateTime? getLastSyncTimestamp(Type entityType) { |
||||
final key = _getKeyForEntityType(entityType); |
||||
|
||||
// 使用 get_storage 的同步 `read` 方法 |
||||
final int? timestampMillis = _box.read<int?>(key); |
||||
|
||||
if (timestampMillis != null) { |
||||
// 从存储的 UTC 毫秒时间戳重建 DateTime 对象 |
||||
return DateTime.fromMillisecondsSinceEpoch(timestampMillis, isUtc: true); |
||||
} |
||||
|
||||
// 如果没有找到对应的键,说明是第一次同步 |
||||
return null; |
||||
} |
||||
|
||||
/// 设置特定实体类型的最后同步时间戳。 |
||||
/// |
||||
/// [entityType]: 业务实体的类型,例如 `Problem` 或 `Enterprise`。 |
||||
/// [timestamp]: 本次同步成功的时间点。 |
||||
/// |
||||
/// 这个方法会将时间转换为 UTC 毫秒时间戳进行存储,以确保时区安全。 |
||||
Future<void> setLastSyncTimestamp(Type entityType, DateTime timestamp) async { |
||||
final key = _getKeyForEntityType(entityType); |
||||
|
||||
// 关键:始终存储 UTC 格式的毫秒时间戳,这是最可靠的方式 |
||||
final int timestampMillis = timestamp.toUtc().millisecondsSinceEpoch; |
||||
|
||||
// 使用 get_storage 的 `write` 方法。 |
||||
// 它可以是同步的,但 `await` 它可以确保数据被刷新到磁盘,对于关键元数据更安全。 |
||||
await _box.write(key, timestampMillis); |
||||
} |
||||
} |
||||
@ -0,0 +1,186 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/models/sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/syncable_repository.dart'; |
||||
import 'package:problem_check_system/app/core/services/sync_metadata_service.dart'; |
||||
|
||||
// 这是一个假设的依赖,您需要一个方法来从本地数据库获取所有未同步的项。 |
||||
// 这通常是在您的某个 LocalDataSource 或一个聚合的 DAO 中实现的。 |
||||
// import 'package/your_app/data/datasources/combined_local_data_source.dart'; |
||||
|
||||
/// SyncService 是应用数据同步的核心编排器。 |
||||
/// |
||||
/// 它的职责是: |
||||
/// 1. 管理所有可同步的仓库 (Repositories)。 |
||||
/// 2. 编排上传(Push)和下载(Pull)的完整同步流程。 |
||||
/// 3. 与 `SyncMetadataService` 协作,管理每个实体类型的最后同步时间戳。 |
||||
/// |
||||
/// 它本身不包含任何针对特定实体(如Problem或Enterprise)的业务逻辑, |
||||
/// 而是将具体的数据操作委托给已注册的 `SyncableRepository`。 |
||||
class SyncService extends GetxService { |
||||
// 依赖于元数据服务来管理时间戳 |
||||
final SyncMetadataService _metadataService = Get.find(); |
||||
|
||||
// 依赖于一个能获取所有待同步项的数据源 |
||||
// final CombinedLocalDataSource _combinedLocalDataSource = Get.find(); // 示例 |
||||
|
||||
// 使用一个 Map 来存储已注册的、特定类型的同步策略(仓库) |
||||
final Map<Type, SyncableRepository> _repositories = {}; |
||||
|
||||
/// 注册一个 `SyncableRepository`。 |
||||
/// |
||||
/// 在应用启动时,每个可同步的模块都应将其 Repository 注册到 `SyncService`。 |
||||
/// 这使得 `SyncService` 能够知道如何处理对应类型的实体。 |
||||
/// |
||||
/// 泛型 `<T extends SyncableEntity>` 确保了类型安全。 |
||||
void registerRepository<T extends SyncableEntity>( |
||||
SyncableRepository<T> repository, |
||||
) { |
||||
print('[SyncService] Registering repository for type: $T'); |
||||
_repositories[T] = repository; |
||||
} |
||||
|
||||
/// 执行完整的双向同步周期。 |
||||
/// |
||||
/// 这是UI或后台任务应该调用的主要方法。 |
||||
/// 流程: |
||||
/// 1. **上传阶段**: 推送所有本地的待创建、待更新、待删除的记录。 |
||||
/// 2. **下载阶段**: 为每个已注册的实体类型,拉取自上次同步以来的服务器更新。 |
||||
Future<void> performFullSync() async { |
||||
print('[SyncService] === Starting Full Sync Cycle ==='); |
||||
|
||||
// 阶段一: 上传本地所有待处理的更改 |
||||
await _pushAllPendingChanges(); |
||||
|
||||
// 阶段二: 下载所有已注册的实体的服务器更新 |
||||
await _pullAllServerUpdates(); |
||||
|
||||
print('[SyncService] === Full Sync Cycle Finished ==='); |
||||
} |
||||
|
||||
/// --- 私有辅助方法 --- /// |
||||
|
||||
/// **上传阶段** |
||||
/// 获取所有本地待同步的项,并逐个进行同步。 |
||||
Future<void> _pushAllPendingChanges() async { |
||||
print('[SyncService] >> Phase 1: Pushing local changes...'); |
||||
|
||||
// 注意: 您需要实现一个方法来获取所有 feature 的待同步项。 |
||||
// 这里我们假设有一个 `_getAllPendingItems()` 方法。 |
||||
final List<SyncableEntity> allPendingItems = await _getAllPendingItems(); |
||||
|
||||
if (allPendingItems.isEmpty) { |
||||
print('[SyncService] No local changes to push.'); |
||||
return; |
||||
} |
||||
|
||||
print( |
||||
'[SyncService] Found ${allPendingItems.length} pending items to push.', |
||||
); |
||||
|
||||
for (final item in allPendingItems) { |
||||
try { |
||||
await _syncSingleItem(item); |
||||
} catch (e) { |
||||
print( |
||||
'[SyncService] ERROR: Failed to push item ${item.id} of type ${item.runtimeType}. Error: $e', |
||||
); |
||||
// 关键: 单个项目失败不应中断整个上传流程,继续尝试下一个。 |
||||
} |
||||
} |
||||
print('[SyncService] << Push phase completed.'); |
||||
} |
||||
|
||||
/// **下载阶段** |
||||
/// 遍历所有已注册的仓库,并为每个仓库执行拉取操作。 |
||||
Future<void> _pullAllServerUpdates() async { |
||||
print('[SyncService] >> Phase 2: Pulling server updates...'); |
||||
|
||||
// 记录本次同步周期开始的时间。如果某个类型的拉取成功,这将是它的新时间戳。 |
||||
final DateTime syncCycleStartTime = DateTime.now().toUtc(); |
||||
|
||||
for (final entry in _repositories.entries) { |
||||
final entityType = entry.key; |
||||
final repository = entry.value; |
||||
|
||||
try { |
||||
// 1. 获取该实体上一次成功同步的时间戳 |
||||
final lastSyncTimestamp = _metadataService.getLastSyncTimestamp( |
||||
entityType, |
||||
); |
||||
|
||||
print( |
||||
'[SyncService] Pulling updates for $entityType since: ${lastSyncTimestamp ?? 'the beginning'}', |
||||
); |
||||
|
||||
// 2. 委托给对应的仓库去执行拉取和本地保存 |
||||
await repository.pullFromServer(lastSyncTimestamp); |
||||
|
||||
// 3. 如果拉取成功,更新该实体类型的最后同步时间戳 |
||||
await _metadataService.setLastSyncTimestamp( |
||||
entityType, |
||||
syncCycleStartTime, |
||||
); |
||||
print( |
||||
'[SyncService] Successfully pulled and updated timestamp for $entityType.', |
||||
); |
||||
} catch (e) { |
||||
print( |
||||
'[SyncService] ERROR: Failed to pull updates for $entityType. Error: $e', |
||||
); |
||||
// 关键: 一个类型的拉取失败不应中断其他类型的同步。 |
||||
} |
||||
} |
||||
print('[SyncService] << Pull phase completed.'); |
||||
} |
||||
|
||||
/// 根据单个项目的同步状态,委托给对应的仓库执行操作。 |
||||
Future<void> _syncSingleItem(SyncableEntity item) async { |
||||
final repository = _repositories[item.runtimeType]; |
||||
|
||||
if (repository == null) { |
||||
print( |
||||
'[SyncService] WARNING: No repository registered for type ${item.runtimeType}. Skipping item ${item.id}.', |
||||
); |
||||
return; |
||||
} |
||||
|
||||
print( |
||||
'[SyncService] Pushing item ${item.id} (${item.runtimeType}) with status ${item.syncStatus.name}...', |
||||
); |
||||
|
||||
switch (item.syncStatus) { |
||||
case SyncStatus.pendingCreate: |
||||
await repository.syncCreate(item); |
||||
break; |
||||
case SyncStatus.pendingUpdate: |
||||
await repository.syncUpdate(item); |
||||
break; |
||||
case SyncStatus.pendingDelete: |
||||
await repository.syncDelete(item.id); |
||||
break; |
||||
case SyncStatus.synced: |
||||
// 这不应该发生在待处理列表中,但为了安全起见,我们忽略它。 |
||||
break; |
||||
case SyncStatus.untracked: |
||||
// TODO: Handle this case. |
||||
throw UnimplementedError(); |
||||
} |
||||
} |
||||
|
||||
/// [需要您来实现] 获取所有待同步的实体。 |
||||
/// |
||||
/// 这是一个关键的辅助方法,它需要查询本地数据库中所有 |
||||
/// `syncStatus` 不为 `synced` 的记录。 |
||||
/// 这通常需要一个可以跨表查询的聚合数据源。 |
||||
Future<List<SyncableEntity>> _getAllPendingItems() async { |
||||
// 这是一个伪代码实现,您需要根据您的数据层来替换它。 |
||||
// final problems = await problemLocalDataSource.getPendingProblems(); |
||||
// final enterprises = await enterpriseLocalDataSource.getPendingEnterprises(); |
||||
// return [...problems, ...enterprises]; |
||||
|
||||
print( |
||||
'[SyncService] WARNING: _getAllPendingItems() is not implemented. Returning empty list.', |
||||
); |
||||
return []; // 返回空列表以防止错误 |
||||
} |
||||
} |
||||
@ -1,6 +1,6 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/modules/auth/controllers/login_controller.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/features/auth/controllers/login_controller.dart'; |
||||
|
||||
class LoginBinding implements Bindings { |
||||
@override |
||||
@ -0,0 +1,20 @@
|
||||
import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
|
||||
abstract class EnterpriseLocalDataSource { |
||||
Future<void> addEnterprise(EnterpriseModel enterprise); |
||||
} |
||||
|
||||
class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { |
||||
@override |
||||
Future<void> addEnterprise(EnterpriseModel enterprise) async { |
||||
// // 在这里实现将 Enterprise 保存到本地存储的逻辑 |
||||
// // 例如,使用 SQLite、SharedPreferences 或其他本地数据库 |
||||
// final db = await databaseService.database; |
||||
// await db.insert( |
||||
// 'enterprises', // 表名 |
||||
// enterprise.toMap(), |
||||
// conflictAlgorithm: ConflictAlgorithm.replace, |
||||
// ); |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@
|
||||
class EnterpriseRemoteDataSource {} |
||||
|
||||
class EnterpriseRemoteDataSourceImpl implements EnterpriseRemoteDataSource { |
||||
// 在这里实现与远程服务器交互的具体方法 |
||||
} |
||||
@ -1,21 +1,92 @@
|
||||
// lib/app/modules/enterprise_list/models/enterprise_model.dart |
||||
import 'package:problem_check_system/app/core/models/sync_status.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; |
||||
|
||||
class Enterprise { |
||||
final int? id; // 可选的 ID 字段,用于区分新增和修改 |
||||
final String companyName; |
||||
final String companyType; |
||||
final int totalIssues; |
||||
final String creationTime; |
||||
final int uploaded; |
||||
final int notUploaded; |
||||
|
||||
Enterprise({ |
||||
this.id, |
||||
required this.companyName, |
||||
required this.companyType, |
||||
required this.totalIssues, |
||||
required this.creationTime, |
||||
required this.uploaded, |
||||
required this.notUploaded, |
||||
/// `EnterpriseModel` 是 `Enterprise` 实体在数据层的具体实现。 |
||||
/// |
||||
/// 它继承自 `Enterprise`,并增加了与数据源(如SQLite)进行相互转换的能力。 |
||||
/// 这个模型只应在 Data 层内部使用,不应泄露到 Domain 层或 Presentation 层。 |
||||
class EnterpriseModel extends Enterprise { |
||||
const EnterpriseModel({ |
||||
required super.id, |
||||
required super.syncStatus, |
||||
required super.lastModifiedTime, |
||||
required super.creationTime, |
||||
required super.name, |
||||
required super.type, |
||||
super.address, |
||||
super.scale, |
||||
super.contactPerson, |
||||
super.contactPhone, |
||||
super.majorHazardsDescription, |
||||
}); |
||||
|
||||
/// [工厂构造函数] 从 Domain 层的 `Enterprise` 实体创建 `EnterpriseModel`。 |
||||
/// |
||||
/// 这个方法在 `Repository` 中非常有用,当它从 UseCase 接收到一个纯粹的 |
||||
/// `Enterprise` 实体,并需要将其转换为可以存入数据库的 `Model` 时调用。 |
||||
factory EnterpriseModel.fromEntity(Enterprise entity) { |
||||
return EnterpriseModel( |
||||
id: entity.id, |
||||
syncStatus: entity.syncStatus, |
||||
lastModifiedTime: entity.lastModifiedTime, |
||||
creationTime: entity.creationTime, |
||||
name: entity.name, |
||||
type: entity.type, |
||||
address: entity.address, |
||||
scale: entity.scale, |
||||
contactPerson: entity.contactPerson, |
||||
contactPhone: entity.contactPhone, |
||||
majorHazardsDescription: entity.majorHazardsDescription, |
||||
); |
||||
} |
||||
|
||||
/// [工厂构造函数] 从数据库查询返回的 `Map` 创建 `EnterpriseModel`。 |
||||
/// |
||||
/// 这个方法在 `DataSource` 中非常有用,当它从 SQLite 读取数据后, |
||||
/// 需要将原始的 `Map` 转换为一个强类型的 `Model` 对象时调用。 |
||||
factory EnterpriseModel.fromMap(Map<String, dynamic> map) { |
||||
return EnterpriseModel( |
||||
id: map['id'] as String, |
||||
// 从整数索引转换回枚举 |
||||
syncStatus: SyncStatus.values[map['syncStatus'] as int], |
||||
// 从毫秒时间戳转换回 DateTime |
||||
lastModifiedTime: DateTime.fromMillisecondsSinceEpoch( |
||||
map['lastModifiedTime'] as int, |
||||
isUtc: true, |
||||
), |
||||
creationTime: DateTime.fromMillisecondsSinceEpoch( |
||||
map['creationTime'] as int, |
||||
isUtc: true, |
||||
), |
||||
name: map['name'] as String, |
||||
type: map['type'] as String, |
||||
address: map['address'] as String?, |
||||
scale: map['scale'] as String?, |
||||
contactPerson: map['contactPerson'] as String?, |
||||
contactPhone: map['contactPhone'] as String?, |
||||
majorHazardsDescription: map['majorHazardsDescription'] as String?, |
||||
); |
||||
} |
||||
|
||||
/// 将 `EnterpriseModel` 对象转换为一个可以被 `sqflite` 存储的 `Map`。 |
||||
/// |
||||
/// 这个方法在 `DataSource` 中非常有用,当需要将一个 `Model` 对象 |
||||
/// 插入或更新到数据库时调用。 |
||||
Map<String, dynamic> toMap() { |
||||
return { |
||||
'id': id, |
||||
// 将枚举转换为整数索引进行存储 |
||||
'syncStatus': syncStatus.index, |
||||
// 将 DateTime 转换为 UTC 毫秒时间戳进行存储 |
||||
'lastModifiedTime': lastModifiedTime.toUtc().millisecondsSinceEpoch, |
||||
'creationTime': creationTime.toUtc().millisecondsSinceEpoch, |
||||
'name': name, |
||||
'type': type, |
||||
'address': address, |
||||
'scale': scale, |
||||
'contactPerson': contactPerson, |
||||
'contactPhone': contactPhone, |
||||
'majorHazardsDescription': majorHazardsDescription, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,66 @@
|
||||
// ... |
||||
|
||||
import 'package:problem_check_system/app/core/models/sync_status.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; |
||||
import 'package:uuid/uuid.dart'; |
||||
|
||||
class EnterpriseRepositoryImpl implements EnterpriseRepository { |
||||
final EnterpriseLocalDataSource localDataSource; |
||||
final EnterpriseRemoteDataSource remoteDataSource; // 注入远程数据源 |
||||
final Uuid uuid = Uuid(); |
||||
|
||||
EnterpriseRepositoryImpl({ |
||||
required this.localDataSource, |
||||
required this.remoteDataSource, |
||||
}); |
||||
|
||||
@override |
||||
Future<void> addEnterprise(Enterprise enterprise) async { |
||||
final now = DateTime.now(); |
||||
|
||||
// 1. 丰富实体:补充所有系统生成的字段 |
||||
final fullEnterprise = Enterprise( |
||||
id: uuid.v4(), // 生成一个新的唯一ID |
||||
name: enterprise.name, |
||||
type: enterprise.type, |
||||
// ... 复制用户输入的其他字段 ... |
||||
syncStatus: SyncStatus.pendingCreate, // 关键:设置为待创建状态 |
||||
lastModifiedTime: now, |
||||
creationTime: now, |
||||
); |
||||
|
||||
// 2. 将丰富的、完整的实体转换为数据模型 |
||||
final enterpriseModel = EnterpriseModel.fromEntity(fullEnterprise); |
||||
|
||||
// 3. 调用数据源进行持久化 |
||||
await localDataSource.addEnterprise(enterpriseModel); |
||||
} |
||||
|
||||
@override |
||||
Future<List<Enterprise>> pullFromServer(DateTime? lastSyncTimestamp) { |
||||
// TODO: implement pullFromServer |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
Future<Enterprise> syncCreate(Enterprise item) { |
||||
// TODO: implement syncCreate |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
Future<void> syncDelete(String id) { |
||||
// TODO: implement syncDelete |
||||
throw UnimplementedError(); |
||||
} |
||||
|
||||
@override |
||||
Future<Enterprise> syncUpdate(Enterprise item) { |
||||
// TODO: implement syncUpdate |
||||
throw UnimplementedError(); |
||||
} |
||||
} |
||||
@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart'; |
||||
import 'package:problem_check_system/app/core/models/sync_status.dart'; |
||||
|
||||
/// `Enterprise` 实体,代表一个企业的核心业务对象。 |
||||
/// 这是一个纯粹的、不可变的类,不包含任何与数据持久化相关的代码。 |
||||
class Enterprise extends Equatable implements SyncableEntity { |
||||
@override |
||||
final String id; |
||||
@override |
||||
final SyncStatus syncStatus; |
||||
@override |
||||
final DateTime lastModifiedTime; |
||||
final String name; |
||||
final String type; |
||||
final String? address; |
||||
final String? scale; |
||||
final String? contactPerson; |
||||
final String? contactPhone; |
||||
final String? majorHazardsDescription; |
||||
final DateTime creationTime; |
||||
|
||||
const Enterprise({ |
||||
required this.id, |
||||
required this.name, |
||||
required this.type, |
||||
this.address, |
||||
this.scale, |
||||
this.contactPerson, |
||||
this.contactPhone, |
||||
this.majorHazardsDescription, |
||||
required this.lastModifiedTime, |
||||
required this.creationTime, |
||||
required this.syncStatus, |
||||
}); |
||||
|
||||
@override |
||||
List<Object?> get props => [id, syncStatus, name, type]; |
||||
} |
||||
@ -0,0 +1,12 @@
|
||||
import 'package:problem_check_system/app/core/repositories/syncable_repository.dart'; |
||||
|
||||
import '../entities/enterprise.dart'; |
||||
|
||||
abstract class EnterpriseRepository implements SyncableRepository<Enterprise> { |
||||
/// 将一个新的企业实体保存到数据层。 |
||||
/// 实现者应负责处理ID生成、时间戳和初始同步状态。 |
||||
Future<void> addEnterprise(Enterprise enterprise); |
||||
|
||||
// --- 已有方法 --- |
||||
// Future<List<EnterpriseListItem>> getEnterpriseListItems(); |
||||
} |
||||
@ -0,0 +1,12 @@
|
||||
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; |
||||
|
||||
class AddEnterprise { |
||||
final EnterpriseRepository repository; |
||||
|
||||
AddEnterprise({required this.repository}); |
||||
|
||||
Future<void> call(Enterprise enterprise) async { |
||||
await repository.addEnterprise(enterprise); |
||||
} |
||||
} |
||||
@ -1,11 +1,11 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/problem_repository.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; |
||||
import 'package:problem_check_system/modules/home/controllers/home_controller.dart'; |
||||
import 'package:problem_check_system/modules/my/controllers/my_controller.dart'; |
||||
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; |
||||
import 'package:problem_check_system/app/features/home/controllers/home_controller.dart'; |
||||
import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||
|
||||
class HomeBinding implements Bindings { |
||||
@override |
||||
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; |
||||
import 'package:problem_check_system/modules/my/views/my_page.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/problem_page.dart'; |
||||
import 'package:problem_check_system/app/features/my/views/my_page.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; |
||||
|
||||
class HomeController extends GetxController { |
||||
var selectedIndex = 0.obs; |
||||
@ -0,0 +1,14 @@
|
||||
// lib/modules/home/views/home_page.dart |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/features/home/controllers/home_controller.dart'; |
||||
|
||||
class HomePage extends GetView<HomeController> { |
||||
const HomePage({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold(body: Center(child: Text("首页"))); |
||||
} |
||||
} |
||||
@ -1,6 +1,6 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/modules/my/controllers/change_password_controller.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/features/my/controllers/change_password_controller.dart'; |
||||
|
||||
class ChangePasswordBinding implements Bindings { |
||||
@override |
||||
@ -1,5 +1,5 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
|
||||
class ChangePasswordController extends GetxController { |
||||
// 响应式变量,用于存储新密码和确认密码 |
||||
@ -1,7 +1,7 @@
|
||||
import 'package:dio/dio.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/routes/app_routes.dart'; |
||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/core/routes/app_routes.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
|
||||
class MyController extends GetxController { |
||||
final AuthRepository authRepository; |
||||
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:problem_check_system/modules/my/controllers/change_password_controller.dart'; |
||||
import 'package:problem_check_system/app/features/my/controllers/change_password_controller.dart'; |
||||
|
||||
class ChangePasswordPage extends StatelessWidget { |
||||
const ChangePasswordPage({super.key}); |
||||
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/modules/my/controllers/my_controller.dart'; |
||||
import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; |
||||
|
||||
import 'package:problem_check_system/app/routes/app_routes.dart'; |
||||
import 'package:problem_check_system/app/core/routes/app_routes.dart'; |
||||
|
||||
class MyPage extends GetView<MyController> { |
||||
const MyPage({super.key}); |
||||
@ -0,0 +1,32 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; |
||||
import 'package:problem_check_system/app/features/navigation/presentation/controllers/navigation_controller.dart'; |
||||
import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||
|
||||
class NavigationBinding extends Bindings { |
||||
@override |
||||
void dependencies() { |
||||
/// 注册主页控制器 |
||||
Get.lazyPut<NavigationController>(() => NavigationController()); |
||||
Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); |
||||
Get.lazyPut<EnterpriseListController>(() => EnterpriseListController()); |
||||
|
||||
/// 注册问题控制器 |
||||
Get.lazyPut<ProblemController>( |
||||
() => ProblemController( |
||||
problemRepository: Get.find<ProblemRepository>(), |
||||
problemStateManager: Get.find<ProblemStateManager>(), |
||||
), |
||||
fenix: true, |
||||
); |
||||
|
||||
/// 注册我的控制器 |
||||
Get.lazyPut<MyController>( |
||||
() => MyController(authRepository: Get.find<AuthRepository>()), |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; |
||||
import 'package:problem_check_system/app/features/home/views/home_page.dart'; |
||||
import 'package:problem_check_system/app/features/my/views/my_page.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; |
||||
|
||||
class NavigationController extends GetxController { |
||||
var selectedIndex = 0.obs; |
||||
// 页面列表 |
||||
final List<Widget> pages = [ |
||||
const HomePage(), |
||||
const EnterpriseListPage(), |
||||
const ProblemPage(), |
||||
const MyPage(), |
||||
]; |
||||
|
||||
get currentPage => pages[selectedIndex.value]; |
||||
|
||||
void changePageIndex(int index) { |
||||
selectedIndex.value = index; |
||||
} |
||||
} |
||||
@ -0,0 +1,29 @@
|
||||
import 'package:curved_navigation_bar/curved_navigation_bar.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/features/navigation/presentation/controllers/navigation_controller.dart'; |
||||
|
||||
class NavigationPage extends GetView<NavigationController> { |
||||
const NavigationPage({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold( |
||||
bottomNavigationBar: CurvedNavigationBar( |
||||
backgroundColor: Colors.transparent, |
||||
color: Colors.blueAccent, |
||||
items: <Widget>[ |
||||
Icon(Icons.home, color: Colors.white, size: 30), |
||||
Icon(Icons.business, color: Colors.white, size: 30), |
||||
Icon(Icons.list, color: Colors.white, size: 30), |
||||
Icon(Icons.person, color: Colors.white, size: 30), |
||||
], |
||||
onTap: (index) { |
||||
//Handle button tap |
||||
controller.changePageIndex(index); |
||||
}, |
||||
), |
||||
body: Obx(() => controller.currentPage), |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,110 @@
|
||||
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/core/services/database_service.dart'; |
||||
import 'package:sqflite/sqflite.dart'; |
||||
|
||||
const String _tableName = 'problems'; |
||||
|
||||
/// 数据源抽象接口 |
||||
abstract class ProblemLocalDataSource { |
||||
Future<int> insertProblem(Problem problem); |
||||
Future<int> updateProblem(Problem problem); |
||||
Future<int> deleteProblem(String problemId); |
||||
Future<Problem?> getProblemById(String id); |
||||
Future<List<Problem>> getProblems({ |
||||
DateTime? startDate, |
||||
DateTime? endDate, |
||||
List<ProblemSyncStatus>? syncStatuses, |
||||
bool? hasBindData, |
||||
}); |
||||
} |
||||
|
||||
/// 数据源的具体实现 |
||||
class ProblemLocalDataSourceImpl implements ProblemLocalDataSource { |
||||
final DatabaseService _databaseService; |
||||
|
||||
ProblemLocalDataSourceImpl({required DatabaseService databaseService}) |
||||
: _databaseService = databaseService; |
||||
|
||||
@override |
||||
Future<int> insertProblem(Problem problem) async { |
||||
final db = await _databaseService.database; |
||||
return await db.insert( |
||||
_tableName, |
||||
problem.toMap(), |
||||
conflictAlgorithm: ConflictAlgorithm.replace, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Future<int> updateProblem(Problem problem) async { |
||||
final db = await _databaseService.database; |
||||
return await db.update( |
||||
_tableName, |
||||
problem.toMap(), |
||||
where: 'id = ?', |
||||
whereArgs: [problem.id], |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Future<int> deleteProblem(String problemId) async { |
||||
final db = await _databaseService.database; |
||||
return await db.delete(_tableName, where: 'id = ?', whereArgs: [problemId]); |
||||
} |
||||
|
||||
@override |
||||
Future<Problem?> getProblemById(String id) async { |
||||
final db = await _databaseService.database; |
||||
final results = await db.query( |
||||
_tableName, |
||||
where: 'id = ?', |
||||
whereArgs: [id], |
||||
limit: 1, |
||||
); |
||||
return results.isNotEmpty ? Problem.fromMap(results.first) : null; |
||||
} |
||||
|
||||
@override |
||||
Future<List<Problem>> getProblems({ |
||||
DateTime? startDate, |
||||
DateTime? endDate, |
||||
List<ProblemSyncStatus>? syncStatuses, |
||||
bool? hasBindData, |
||||
}) async { |
||||
final db = await _databaseService.database; |
||||
final whereClauses = <String>[]; |
||||
final whereArgs = <dynamic>[]; |
||||
|
||||
if (startDate != null) { |
||||
whereClauses.add('creationTime >= ?'); |
||||
whereArgs.add(startDate.millisecondsSinceEpoch); |
||||
} |
||||
if (endDate != null) { |
||||
whereClauses.add('creationTime <= ?'); |
||||
whereArgs.add(endDate.millisecondsSinceEpoch); |
||||
} |
||||
if (syncStatuses != null && syncStatuses.isNotEmpty) { |
||||
final statuses = syncStatuses.map((s) => s.index).toList(); |
||||
whereClauses.add( |
||||
'syncStatus IN (${List.filled(statuses.length, '?').join(',')})', |
||||
); |
||||
whereArgs.addAll(statuses); |
||||
} |
||||
if (hasBindData != null) { |
||||
if (hasBindData) { |
||||
whereClauses.add('bindData IS NOT NULL AND bindData != ""'); |
||||
} else { |
||||
whereClauses.add('(bindData IS NULL OR bindData = "")'); |
||||
} |
||||
} |
||||
|
||||
final results = await db.query( |
||||
_tableName, |
||||
where: whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null, |
||||
whereArgs: whereArgs.isEmpty ? null : whereArgs, |
||||
orderBy: 'creationTime DESC', |
||||
); |
||||
return results.map((json) => Problem.fromMap(json)).toList(); |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||
|
||||
/// Problem 仓库的抽象接口 |
||||
/// 定义了业务逻辑层需要的数据操作。 |
||||
abstract class ProblemRepository { |
||||
Future<void> addProblem(Problem problem); |
||||
Future<void> updateProblem(Problem problem); |
||||
Future<void> deleteProblem(String problemId); |
||||
Future<Problem?> getProblemById(String id); |
||||
|
||||
Future<void> markAsSynced(String id); |
||||
|
||||
Future<List<Problem>> getProblems({ |
||||
DateTime? startDate, |
||||
DateTime? endDate, |
||||
String? syncStatus, // 业务逻辑层使用字符串,更直观 |
||||
String? bindStatus, |
||||
}); |
||||
} |
||||
@ -1,7 +1,7 @@
|
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_form_controller.dart'; |
||||
|
||||
class ProblemFormBinding extends Bindings { |
||||
@override |
||||
@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/problem_list_page.dart'; // 导入修正后的 ProblemListPage |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/current_filter_bar.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/history_filter_bar.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart'; // 导入自定义下拉菜单 |
||||
|
||||
class ProblemPage extends GetView<ProblemController> { |
||||
const ProblemPage({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Scaffold( |
||||
appBar: AppBar( |
||||
title: Text( |
||||
'问题列表', |
||||
style: TextStyle( |
||||
fontWeight: FontWeight.bold, |
||||
fontFamily: 'MyFont', |
||||
fontSize: 18.sp, |
||||
color: Colors.white, |
||||
), |
||||
), |
||||
backgroundColor: Colors.transparent, |
||||
flexibleSpace: Container( |
||||
decoration: const BoxDecoration( |
||||
gradient: LinearGradient( |
||||
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], |
||||
begin: Alignment.centerLeft, |
||||
end: Alignment.centerRight, |
||||
), |
||||
), |
||||
), |
||||
elevation: 0, |
||||
centerTitle: true, |
||||
actions: [ |
||||
IconButton( |
||||
icon: Icon(Icons.add, color: Colors.white), // 使用 .sp |
||||
onPressed: () {}, |
||||
), |
||||
IconButton( |
||||
icon: Icon(Icons.upload, color: Colors.pink[300]), // 使用 .sp |
||||
onPressed: () {}, |
||||
), |
||||
], |
||||
), |
||||
body: Column( |
||||
children: [ |
||||
CurrentFilterBar(), |
||||
Expanded( |
||||
child: // 使用通用列表组件 |
||||
ProblemListPage( |
||||
problemsToShow: controller.problems, |
||||
viewType: ProblemCardViewType.buttons, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, |
||||
// 使用 Stack 统一管理所有浮动按钮 |
||||
floatingActionButton: Stack( |
||||
children: [ |
||||
// 固定位置的 "添加" 按钮 |
||||
// 使用 Align 和 Positioned |
||||
Align( |
||||
alignment: Alignment.bottomCenter, |
||||
child: Padding( |
||||
padding: EdgeInsets.only(bottom: 24.h), // 底部间距 |
||||
child: FloatingActionButton( |
||||
heroTag: "btn_add", |
||||
onPressed: () { |
||||
controller.toProblemFormPageAndRefresh(); |
||||
}, |
||||
shape: const CircleBorder(), |
||||
backgroundColor: Colors.blue[300], |
||||
foregroundColor: Colors.white, |
||||
child: const Icon(Icons.add), |
||||
), |
||||
), |
||||
), |
||||
|
||||
// 可拖动的 "上传" 按钮 |
||||
Obx(() { |
||||
final isOnline = controller.isOnline.value; |
||||
return Positioned( |
||||
// 使用正确的坐标,left/right 对应 dx,top/bottom 对应 dy |
||||
left: controller.fabUploadPosition.value.dx, |
||||
top: controller.fabUploadPosition.value.dy, |
||||
child: GestureDetector( |
||||
onPanUpdate: (details) { |
||||
// 调用控制器中的方法来更新位置 |
||||
controller.updateFabUploadPosition(details.delta); |
||||
}, |
||||
onPanEnd: (details) { |
||||
// 拖动结束后调用吸附方法 |
||||
controller.snapToEdge(); |
||||
}, |
||||
child: FloatingActionButton( |
||||
heroTag: "btn_upload", |
||||
onPressed: isOnline |
||||
? () => controller.showUploadPage() |
||||
: null, |
||||
foregroundColor: Colors.white, |
||||
backgroundColor: isOnline |
||||
? Colors.red[300] |
||||
: Colors.grey[400], |
||||
child: Icon( |
||||
isOnline |
||||
? Icons.file_upload_outlined |
||||
: Icons.cloud_off_outlined, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
}), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
||||
@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/problem_list_page.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart'; |
||||
|
||||
class ProblemUploadPage extends GetView<ProblemController> { |
||||
const ProblemUploadPage({super.key}); |
||||
@ -1,7 +1,7 @@
|
||||
// widgets/custom_filter_dropdown.dart |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/dropdown_option.dart'; |
||||
|
||||
class CustomFilterDropdown extends StatelessWidget { |
||||
final String title; |
||||
@ -1,6 +1,6 @@
|
||||
// models/date_range_enum.dart |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/dropdown_option.dart'; |
||||
|
||||
enum DateRange { threeDays, oneWeek, oneMonth } |
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// sync_progress_dialog.dart |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/modules/problem/controllers/sync_progress_state.dart'; |
||||
import 'package:problem_check_system/app/features/problem/presentation/controllers/sync_progress_state.dart'; |
||||
|
||||
class SyncProgressDialog extends StatelessWidget { |
||||
final SyncProgressState progressState; |
||||
@ -1,50 +0,0 @@
|
||||
// lib/modules/home/views/home_page.dart |
||||
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/modules/home/controllers/home_controller.dart'; |
||||
|
||||
class HomePage extends GetView<HomeController> { |
||||
const HomePage({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Obx( |
||||
() => Scaffold( |
||||
body: controller.pages[controller.selectedIndex.value], |
||||
|
||||
// 1. 将 BottomNavigationBar 替换为 NavigationBar |
||||
bottomNavigationBar: NavigationBar( |
||||
// 2. 将 'currentIndex' 属性重命名为 'selectedIndex' |
||||
selectedIndex: controller.selectedIndex.value, |
||||
|
||||
// 3. 将 'onTap' 属性重命名为 'onDestinationSelected' |
||||
onDestinationSelected: controller.changeIndex, |
||||
// M3 风格调整 (可选, 但推荐) |
||||
// 动画时长 |
||||
animationDuration: const Duration(milliseconds: 800), |
||||
|
||||
// 4. 将 'items' 替换为 'destinations',并使用 NavigationDestination |
||||
destinations: const [ |
||||
NavigationDestination( |
||||
// M3 的一个重要特性是区分选中和未选中的图标 |
||||
icon: Icon(Icons.home_outlined, color: Colors.blue), |
||||
selectedIcon: Icon(Icons.home, color: Colors.blue), |
||||
label: '企业', |
||||
), |
||||
NavigationDestination( |
||||
icon: Icon(Icons.now_widgets_outlined, color: Colors.blue), |
||||
selectedIcon: Icon(Icons.now_widgets, color: Colors.blue), |
||||
label: '全部问题', |
||||
), |
||||
NavigationDestination( |
||||
icon: Icon(Icons.person_outline, color: Colors.blue), |
||||
selectedIcon: Icon(Icons.person, color: Colors.blue), |
||||
label: '我的', |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
@ -1,165 +0,0 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
import 'package:get/get.dart'; |
||||
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; // 导入修正后的 ProblemListPage |
||||
import 'package:problem_check_system/modules/problem/views/widgets/current_filter_bar.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/widgets/history_filter_bar.dart'; |
||||
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; // 导入自定义下拉菜单 |
||||
|
||||
class ProblemPage extends GetView<ProblemController> { |
||||
const ProblemPage({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return DefaultTabController( |
||||
initialIndex: 0, |
||||
length: 2, |
||||
child: Scaffold( |
||||
body: Column( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
Container( |
||||
width: 375.w, |
||||
height: 83.5.h, |
||||
alignment: Alignment.bottomLeft, |
||||
decoration: const BoxDecoration( |
||||
gradient: LinearGradient( |
||||
begin: Alignment.centerLeft, |
||||
end: Alignment.centerRight, |
||||
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], |
||||
), |
||||
), |
||||
child: TabBar( |
||||
controller: controller.tabController, |
||||
indicatorSize: TabBarIndicatorSize.tab, |
||||
indicator: const BoxDecoration( |
||||
color: Color(0xfff7f7f7), |
||||
borderRadius: BorderRadius.only( |
||||
topLeft: Radius.circular(8), |
||||
topRight: Radius.circular(60), |
||||
), |
||||
), |
||||
tabs: const [ |
||||
Tab(text: '问题列表'), |
||||
Tab(text: '历史问题列表'), |
||||
], |
||||
labelStyle: TextStyle( |
||||
fontFamily: 'MyFont', |
||||
fontWeight: FontWeight.w800, |
||||
fontSize: 14.sp, |
||||
), |
||||
unselectedLabelStyle: TextStyle( |
||||
fontFamily: 'MyFont', |
||||
fontWeight: FontWeight.w800, |
||||
fontSize: 14.sp, |
||||
), |
||||
labelColor: Colors.black, |
||||
unselectedLabelColor: Color(0xfff7f7f7), |
||||
), |
||||
), |
||||
Expanded( |
||||
child: TabBarView( |
||||
controller: controller.tabController, |
||||
children: [ |
||||
// 问题列表 Tab |
||||
DecoratedBox( |
||||
decoration: BoxDecoration(color: Color(0xfff7f7f7)), |
||||
child: Column( |
||||
children: [ |
||||
CurrentFilterBar(), |
||||
Expanded( |
||||
child: // 使用通用列表组件 |
||||
ProblemListPage( |
||||
problemsToShow: controller.problems, |
||||
viewType: ProblemCardViewType.buttons, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
// 历史问题列表 Tab |
||||
DecoratedBox( |
||||
decoration: BoxDecoration(color: Color(0xfff7f7f7)), |
||||
child: Column( |
||||
children: [ |
||||
HistoryFilterBar(), |
||||
Expanded( |
||||
child: // 使用通用列表组件 |
||||
ProblemListPage( |
||||
problemsToShow: controller.historyProblems, |
||||
viewType: ProblemCardViewType.buttons, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
], |
||||
), |
||||
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, |
||||
// 使用 Stack 统一管理所有浮动按钮 |
||||
floatingActionButton: Stack( |
||||
children: [ |
||||
// 固定位置的 "添加" 按钮 |
||||
// 使用 Align 和 Positioned |
||||
Align( |
||||
alignment: Alignment.bottomCenter, |
||||
child: Padding( |
||||
padding: EdgeInsets.only(bottom: 24.h), // 底部间距 |
||||
child: FloatingActionButton( |
||||
heroTag: "btn_add", |
||||
onPressed: () { |
||||
controller.toProblemFormPageAndRefresh(); |
||||
}, |
||||
shape: const CircleBorder(), |
||||
backgroundColor: Colors.blue[300], |
||||
foregroundColor: Colors.white, |
||||
child: const Icon(Icons.add), |
||||
), |
||||
), |
||||
), |
||||
|
||||
// 可拖动的 "上传" 按钮 |
||||
Obx(() { |
||||
final isOnline = controller.isOnline.value; |
||||
return Positioned( |
||||
// 使用正确的坐标,left/right 对应 dx,top/bottom 对应 dy |
||||
left: controller.fabUploadPosition.value.dx, |
||||
top: controller.fabUploadPosition.value.dy, |
||||
child: GestureDetector( |
||||
onPanUpdate: (details) { |
||||
// 调用控制器中的方法来更新位置 |
||||
controller.updateFabUploadPosition(details.delta); |
||||
}, |
||||
onPanEnd: (details) { |
||||
// 拖动结束后调用吸附方法 |
||||
controller.snapToEdge(); |
||||
}, |
||||
child: FloatingActionButton( |
||||
heroTag: "btn_upload", |
||||
onPressed: isOnline |
||||
? () => controller.showUploadPage() |
||||
: null, |
||||
foregroundColor: Colors.white, |
||||
backgroundColor: isOnline |
||||
? Colors.red[300] |
||||
: Colors.grey[400], |
||||
child: Icon( |
||||
isOnline |
||||
? Icons.file_upload_outlined |
||||
: Icons.cloud_off_outlined, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
}), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue