Files
xianyan/lib/features/correction/correction_provider.dart
Developer 544f77c0ce chore: 完成v2.4.7版本迭代更新
本次更新包含多项功能优化与兼容性修复:
1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷
2. 新增学习计划设置页路由与国际化支持
3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器
4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈
5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案
6. 优化搜索快捷方式跳转逻辑,避免白屏问题
7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译
8. 修复节气日期表排序与跨年边界问题
9. 优化设备信息页面显示,新增系统版本号展示
10. 重构文件传输二维码逻辑,使用纯URL提升兼容性
11. 优化设置项布局,避免文本溢出问题
12. 修复登录页记住账户功能,新增隐私协议守卫
13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
2026-06-17 08:45:34 +08:00

375 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 纠错状态管理
/// 创建时间: 2026-04-28
/// 更新时间: 2026-06-17
/// 作用: 纠错提交功能状态管理 + 纠错历史本地缓存drift
/// 上次更新: 接入 drift 本地缓存,支持离线查看纠错历史
/// ============================================================
import 'package:drift/drift.dart' show Value;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/api_client.dart';
import '../../../core/storage/database/app_database.dart';
import '../../../core/utils/logger.dart';
/// 纠错记录视图模型(统一本地与服务端字段)
///
/// 用于在 UI 层展示,避免直接暴露 drift 数据类与服务端 Map。
class CorrectionItem {
const CorrectionItem({
required this.type,
required this.sourceType,
required this.sourceId,
required this.switchVal,
required this.isLocal,
required this.createtime,
this.content = '',
this.username = '',
this.email = '',
this.sourceUrl = '',
this.isAnonymous = false,
this.localId,
});
/// 纠错类型: error/typo/missing/suggestion
final String type;
/// 内容类型: article/hanzi/cy/poetry/zc/riddle/other
final String sourceType;
/// 内容 ID
final int sourceId;
/// 状态码: 0=待处理, 1=已处理, 2=已拒绝
final int switchVal;
/// 是否本地来源
final bool isLocal;
/// 服务端创建时间戳(秒级)
final int createtime;
/// 纠错描述内容
final String content;
/// 提交者用户名
final String username;
/// 提交者邮箱
final String email;
/// 来源 URL
final String sourceUrl;
/// 是否匿名
final bool isAnonymous;
/// 本地数据库 ID仅本地缓存记录有值
final int? localId;
/// 从服务端 Map 构造
factory CorrectionItem.fromServerMap(Map<String, dynamic> map) {
final rawSourceId = map['source_id'];
int sourceId = 0;
if (rawSourceId is int) {
sourceId = rawSourceId;
} else if (rawSourceId is String) {
sourceId = int.tryParse(rawSourceId) ?? 0;
}
final rawSwitch = map['switch'];
int switchVal = 0;
if (rawSwitch is int) {
switchVal = rawSwitch;
} else if (rawSwitch is String) {
switchVal = int.tryParse(rawSwitch) ?? 0;
}
final rawIsLocal = map['is_local'];
bool isLocal = false;
if (rawIsLocal is bool) {
isLocal = rawIsLocal;
} else if (rawIsLocal is int) {
isLocal = rawIsLocal == 1;
}
return CorrectionItem(
type: map['type'] as String? ?? 'error',
sourceType: map['source_type'] as String? ?? 'other',
sourceId: sourceId,
switchVal: switchVal,
isLocal: isLocal,
createtime: map['createtime'] as int? ?? 0,
content: map['content'] as String? ?? '',
username: map['username'] as String? ?? '',
email: map['mail'] as String? ?? '',
sourceUrl: map['source_url'] as String? ?? '',
);
}
/// 从 drift 数据行构造
factory CorrectionItem.fromDb(CorrectionRecord row) {
return CorrectionItem(
type: row.type,
sourceType: row.sourceType,
sourceId: row.sourceId,
switchVal: row.switchVal,
isLocal: row.isLocal,
createtime: row.createtime,
content: row.content,
username: row.username,
email: row.email,
sourceUrl: row.sourceUrl,
isAnonymous: row.isAnonymous,
localId: row.id,
);
}
/// 转换为 drift Companion用于本地写入
CorrectionRecordsCompanion toCompanion({bool sync = true}) {
final now = DateTime.now();
return CorrectionRecordsCompanion(
type: Value(type),
sourceType: Value(sourceType),
sourceId: Value(sourceId),
content: Value(content),
username: Value(username),
email: Value(email),
sourceUrl: Value(sourceUrl),
switchVal: Value(switchVal),
isLocal: Value(isLocal),
isAnonymous: Value(isAnonymous),
isSynced: Value(sync),
createtime: Value(createtime),
localCreatedAt: Value(now),
updatedAt: Value(now),
);
}
}
class CorrectionState {
const CorrectionState({
this.isSubmitting = false,
this.isSuccess = false,
this.error,
this.corrections = const [],
this.total = 0,
this.isLoadingFromCache = false,
this.isSyncing = false,
});
final bool isSubmitting;
final bool isSuccess;
final String? error;
final List<CorrectionItem> corrections;
final int total;
/// 是否正在从本地缓存加载
final bool isLoadingFromCache;
/// 是否正在与服务器同步
final bool isSyncing;
CorrectionState copyWith({
bool? isSubmitting,
bool? isSuccess,
String? error,
bool clearError = false,
List<CorrectionItem>? corrections,
int? total,
bool? isLoadingFromCache,
bool? isSyncing,
}) {
return CorrectionState(
isSubmitting: isSubmitting ?? this.isSubmitting,
isSuccess: isSuccess ?? this.isSuccess,
error: clearError ? null : (error ?? this.error),
corrections: corrections ?? this.corrections,
total: total ?? this.total,
isLoadingFromCache: isLoadingFromCache ?? this.isLoadingFromCache,
isSyncing: isSyncing ?? this.isSyncing,
);
}
}
class CorrectionNotifier extends Notifier<CorrectionState> {
@override
CorrectionState build() {
// 启动时异步加载本地缓存
Future.microtask(_loadFromCache);
return const CorrectionState();
}
CorrectionNotifier();
final ApiClient _api = ApiClient.instance;
AppDatabase get _db => AppDatabase.instance;
/// 从本地缓存加载纠错历史(离线可用)
Future<void> _loadFromCache() async {
state = state.copyWith(isLoadingFromCache: true);
try {
final rows = await _db.getCorrectionRecords();
final items = rows.map(CorrectionItem.fromDb).toList();
final count = await _db.getCorrectionRecordCount();
state = state.copyWith(
corrections: items,
total: count,
isLoadingFromCache: false,
);
Log.i('纠错历史本地缓存加载: ${items.length}');
} catch (e) {
Log.e('纠错历史本地缓存加载失败', e);
state = state.copyWith(isLoadingFromCache: false);
}
}
/// 提交纠错
///
/// 成功后将记录写入本地缓存;失败时也写入本地(标记 isSynced=false
/// 便于后续同步。返回 true 表示服务器接收成功。
Future<bool> submitCorrection({
required String targetType,
required int targetId,
required String content,
String type = 'error',
String? username,
String? email,
String? sourceUrl,
bool isAnonymous = false,
}) async {
state = state.copyWith(
isSubmitting: true,
clearError: true,
isSuccess: false,
);
try {
final submitData = <String, dynamic>{
'content': content,
'source_type': targetType,
'source_id': targetId,
'type': type,
'switch': isAnonymous ? 1 : 0,
};
if (username != null && username.isNotEmpty) {
submitData['username'] = username;
}
if (email != null && email.isNotEmpty) {
submitData['mail'] = email;
}
if (sourceUrl != null && sourceUrl.isNotEmpty) {
submitData['source_url'] = sourceUrl;
}
final response = await _api.post<Map<String, dynamic>>(
'/api/webapi/correction_submit',
data: submitData,
);
final respData = response.data as Map<String, dynamic>;
final code = respData['code'] as int? ?? 0;
if (code == 1) {
// 提交成功,写入本地缓存
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final item = CorrectionItem(
type: type,
sourceType: targetType,
sourceId: targetId,
switchVal: 0,
isLocal: true,
createtime: nowSec,
content: content,
username: username ?? '',
email: email ?? '',
sourceUrl: sourceUrl ?? '',
isAnonymous: isAnonymous,
);
await _db.insertCorrectionRecord(item.toCompanion());
// 刷新本地缓存视图
await _loadFromCache();
state = state.copyWith(isSubmitting: false, isSuccess: true);
Log.i('纠错提交成功,已写入本地缓存');
return true;
} else {
state = state.copyWith(
isSubmitting: false,
error: respData['msg'] as String? ?? '提交失败',
);
return false;
}
} catch (e) {
Log.e('纠错提交异常', e);
// 网络异常时也写入本地(标记未同步),便于后续重试
final nowSec = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final item = CorrectionItem(
type: type,
sourceType: targetType,
sourceId: targetId,
switchVal: 0,
isLocal: true,
createtime: nowSec,
content: content,
username: username ?? '',
email: email ?? '',
sourceUrl: sourceUrl ?? '',
isAnonymous: isAnonymous,
);
await _db.insertCorrectionRecord(item.toCompanion(sync: false));
await _loadFromCache();
state = state.copyWith(isSubmitting: false, error: '提交失败: $e');
return false;
}
}
/// 加载纠错历史(先读本地 → 立即渲染 → 再请求服务器 → 更新本地)
Future<void> loadCorrections({int page = 1, int limit = 20}) async {
// 1. 先读本地缓存,立即渲染(离线可用)
if (state.corrections.isEmpty) {
await _loadFromCache();
}
// 2. 请求服务器同步
state = state.copyWith(isSyncing: true);
try {
final response = await _api.get<Map<String, dynamic>>(
'/api/webapi/correction_list',
queryParameters: {'page': page, 'limit': limit},
);
final data = response.data as Map<String, dynamic>;
final code = data['code'] as int? ?? 0;
if (code == 1) {
final result = data['data'] as Map<String, dynamic>? ?? {};
final list = (result['list'] as List<dynamic>? ?? [])
.map((e) => CorrectionItem.fromServerMap(e as Map<String, dynamic>))
.toList();
final total = result['total'] as int? ?? 0;
// 3. 全量替换本地缓存
await _db.replaceCorrectionRecords(
list.map((e) => e.toCompanion()).toList(),
);
state = state.copyWith(
corrections: list,
total: total,
isSyncing: false,
);
Log.i('纠错历史服务器同步成功: ${list.length}');
} else {
state = state.copyWith(isSyncing: false);
}
} catch (e) {
Log.e('加载纠错列表失败(保留本地缓存)', e);
state = state.copyWith(isSyncing: false);
}
}
/// 清空本地纠错缓存(用于设置页"清除缓存"等场景)
Future<void> clearLocalCache() async {
await _db.clearCorrectionRecords();
state = state.copyWith(corrections: [], total: 0);
Log.i('纠错历史本地缓存已清空');
}
}
final correctionProvider =
NotifierProvider<CorrectionNotifier, CorrectionState>(
CorrectionNotifier.new,
);