Files
xianyan/lib/features/correction/correction_provider.dart
Developer 83720002e6 feat: 新增工作台模式、系统托盘,修复多平台兼容性问题
1. 新增工作台三栏布局模式,适配宽屏设备
2. 添加跨平台系统托盘支持,新增托盘图标资源
3. 修复工作台模式下导航返回异常问题
4. 统一JSON类型安全解析,替换硬类型转换
5. 增加macOS深度链接支持,统一渠道分发信息
6. 优化部分页面生命周期和状态加载逻辑
7. 移除废弃的nearby_connections依赖
2026-06-19 06:43:55 +08:00

376 lines
11 KiB
Dart
Raw Permalink 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-19
/// 作用: 纠错提交功能状态管理 + 纠错历史本地缓存drift
/// 上次更新: 修复JSON类型安全问题使用SafeJson.parseInt替代as int? ?? 0
/// ============================================================
import 'package:drift/drift.dart' show Value;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/utils/safe_json.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: SafeJson.parseInt(map['createtime']),
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 = SafeJson.parseInt(respData['code']);
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 = SafeJson.parseInt(data['code']);
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 = SafeJson.parseInt(result['total']);
// 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,
);