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 |
// 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 { |
class ImageMetadata { |
||||||
final String localPath; |
final String localPath; |
||||||
@ -1,7 +1,7 @@ |
|||||||
import 'dart:convert'; |
import 'dart:convert'; |
||||||
|
|
||||||
import 'package:problem_check_system/app/core/data/models/image_metadata_model.dart'; |
import 'package:problem_check_system/app/core/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/problem_sync_status.dart'; |
||||||
|
|
||||||
/// 问题的数据模型。 |
/// 问题的数据模型。 |
||||||
/// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 |
/// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 |
||||||
@ -1,7 +1,7 @@ |
|||||||
import 'package:get/get.dart'; |
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/models/image_metadata_model.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/problem_model.dart'; |
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||||
import 'package:uuid/uuid.dart'; |
import 'package:uuid/uuid.dart'; |
||||||
|
|
||||||
enum ProblemSyncStatus { |
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/get.dart'; |
||||||
import 'package:get_storage/get_storage.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/utils/constants/api_endpoints.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/auth_model.dart'; |
import 'package:problem_check_system/app/core/models/auth_model.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/user/user.dart'; |
import 'package:problem_check_system/app/core/models/user/user.dart'; |
||||||
import 'package:problem_check_system/app/core/data/services/network_status_service.dart'; |
import 'package:problem_check_system/app/core/services/network_status_service.dart'; |
||||||
import 'package:problem_check_system/app/core/data/services/http_provider.dart'; |
import 'package:problem_check_system/app/core/services/http_provider.dart'; |
||||||
|
|
||||||
class AuthRepository extends GetxService { |
class AuthRepository extends GetxService { |
||||||
final HttpProvider httpProvider; |
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 { |
abstract class AppRoutes { |
||||||
// 命名路由,使用 const 常量 |
// 命名路由,使用 const 常量 |
||||||
|
static const navigation = '/navigation'; |
||||||
static const home = '/home'; |
static const home = '/home'; |
||||||
static const login = '/login'; |
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 |
// sqlite_provider.dart |
||||||
|
|
||||||
import 'package:get/get.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/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_model.dart'; |
||||||
import 'package:sqflite/sqflite.dart'; |
import 'package:sqflite/sqflite.dart'; |
||||||
import 'package:path/path.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: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'; |
||||||
import 'package:problem_check_system/modules/auth/controllers/login_controller.dart'; |
import 'package:problem_check_system/app/features/auth/controllers/login_controller.dart'; |
||||||
|
|
||||||
class LoginBinding implements Bindings { |
class LoginBinding implements Bindings { |
||||||
@override |
@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 { |
/// `EnterpriseModel` 是 `Enterprise` 实体在数据层的具体实现。 |
||||||
final int? id; // 可选的 ID 字段,用于区分新增和修改 |
/// |
||||||
final String companyName; |
/// 它继承自 `Enterprise`,并增加了与数据源(如SQLite)进行相互转换的能力。 |
||||||
final String companyType; |
/// 这个模型只应在 Data 层内部使用,不应泄露到 Domain 层或 Presentation 层。 |
||||||
final int totalIssues; |
class EnterpriseModel extends Enterprise { |
||||||
final String creationTime; |
const EnterpriseModel({ |
||||||
final int uploaded; |
required super.id, |
||||||
final int notUploaded; |
required super.syncStatus, |
||||||
|
required super.lastModifiedTime, |
||||||
Enterprise({ |
required super.creationTime, |
||||||
this.id, |
required super.name, |
||||||
required this.companyName, |
required super.type, |
||||||
required this.companyType, |
super.address, |
||||||
required this.totalIssues, |
super.scale, |
||||||
required this.creationTime, |
super.contactPerson, |
||||||
required this.uploaded, |
super.contactPhone, |
||||||
required this.notUploaded, |
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:get/get.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||||
import 'package:problem_check_system/app/core/data/repositories/auth_repository.dart'; |
import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; |
||||||
import 'package:problem_check_system/app/core/data/repositories/problem_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/enterprise/presentation/controllers/enterprise_list_controller.dart'; |
||||||
import 'package:problem_check_system/modules/home/controllers/home_controller.dart'; |
import 'package:problem_check_system/app/features/home/controllers/home_controller.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/modules/problem/controllers/problem_controller.dart'; |
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||||
|
|
||||||
class HomeBinding implements Bindings { |
class HomeBinding implements Bindings { |
||||||
@override |
@override |
||||||
@ -1,8 +1,8 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:get/get.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/enterprise/presentation/pages/enterprise_list_page.dart'; |
||||||
import 'package:problem_check_system/modules/my/views/my_page.dart'; |
import 'package:problem_check_system/app/features/my/views/my_page.dart'; |
||||||
import 'package:problem_check_system/modules/problem/views/problem_page.dart'; |
import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; |
||||||
|
|
||||||
class HomeController extends GetxController { |
class HomeController extends GetxController { |
||||||
var selectedIndex = 0.obs; |
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: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'; |
||||||
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 ChangePasswordBinding implements Bindings { |
class ChangePasswordBinding implements Bindings { |
||||||
@override |
@override |
||||||
@ -1,5 +1,5 @@ |
|||||||
import 'package:get/get.dart'; |
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 { |
class ChangePasswordController extends GetxController { |
||||||
// 响应式变量,用于存储新密码和确认密码 |
// 响应式变量,用于存储新密码和确认密码 |
||||||
@ -1,7 +1,7 @@ |
|||||||
import 'package:dio/dio.dart'; |
import 'package:dio/dio.dart'; |
||||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||||
import 'package:problem_check_system/app/routes/app_routes.dart'; |
import 'package:problem_check_system/app/core/routes/app_routes.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 MyController extends GetxController { |
class MyController extends GetxController { |
||||||
final AuthRepository authRepository; |
final AuthRepository authRepository; |
||||||
@ -1,7 +1,7 @@ |
|||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||||
import 'package:flutter_screenutil/flutter_screenutil.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 { |
class ChangePasswordPage extends StatelessWidget { |
||||||
const ChangePasswordPage({super.key}); |
const ChangePasswordPage({super.key}); |
||||||
@ -1,9 +1,9 @@ |
|||||||
import 'package:flutter/material.dart'; |
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: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> { |
class MyPage extends GetView<MyController> { |
||||||
const MyPage({super.key}); |
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:get/get.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/problem_model.dart'; |
import 'package:problem_check_system/app/core/models/problem_model.dart'; |
||||||
import 'package:problem_check_system/app/core/data/models/problem_sync_status.dart'; |
import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
||||||
import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart'; |
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_form_controller.dart'; |
||||||
|
|
||||||
class ProblemFormBinding extends Bindings { |
class ProblemFormBinding extends Bindings { |
||||||
@override |
@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/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:problem_check_system/modules/problem/controllers/problem_controller.dart'; |
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; |
||||||
import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; |
import 'package:problem_check_system/app/features/problem/presentation/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/views/widgets/problem_card.dart'; |
||||||
|
|
||||||
class ProblemUploadPage extends GetView<ProblemController> { |
class ProblemUploadPage extends GetView<ProblemController> { |
||||||
const ProblemUploadPage({super.key}); |
const ProblemUploadPage({super.key}); |
||||||
@ -1,7 +1,7 @@ |
|||||||
// widgets/custom_filter_dropdown.dart |
// widgets/custom_filter_dropdown.dart |
||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:flutter_screenutil/flutter_screenutil.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 { |
class CustomFilterDropdown extends StatelessWidget { |
||||||
final String title; |
final String title; |
||||||
@ -1,6 +1,6 @@ |
|||||||
// models/date_range_enum.dart |
// models/date_range_enum.dart |
||||||
import 'package:flutter/material.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 } |
enum DateRange { threeDays, oneWeek, oneMonth } |
||||||
|
|
||||||
@ -1,7 +1,7 @@ |
|||||||
// sync_progress_dialog.dart |
// sync_progress_dialog.dart |
||||||
import 'package:flutter/material.dart'; |
import 'package:flutter/material.dart'; |
||||||
import 'package:get/get.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 { |
class SyncProgressDialog extends StatelessWidget { |
||||||
final SyncProgressState progressState; |
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