1. 新增工作台三栏布局模式,适配宽屏设备 2. 添加跨平台系统托盘支持,新增托盘图标资源 3. 修复工作台模式下导航返回异常问题 4. 统一JSON类型安全解析,替换硬类型转换 5. 增加macOS深度链接支持,统一渠道分发信息 6. 优化部分页面生命周期和状态加载逻辑 7. 移除废弃的nearby_connections依赖
376 lines
11 KiB
Dart
376 lines
11 KiB
Dart
/// ============================================================
|
||
/// 闲言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,
|
||
);
|