flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
@@ -0,0 +1,31 @@
/// ============================================================
/// 【AppDatabase】本地业务数据库(存储分层第三层)—— 平台分发入口
///
/// 用法不变:main.dart 里 `await AppDatabase.instance.init();`
///
/// 一、为什么拆成条件导出(三个文件):
/// isar 3.x 生成的 Schema 代码(*.g.dart)里含 64 位哈希常量
/// (CollectionSchema.id,如 -6819722535046815095),远超 JS 可精确
/// 表示的最大整数 2^53,dart2js 编译 Web 端时会直接报
/// "The integer literal ... can't be represented exactly in JavaScript"。
/// 即使 kIsWeb 在【运行时】跳过数据库,【编译期】依然失败——
/// 因为 app_database.dart 静态 import 了模型 → 静态 import 了 .g.dart。
///
/// 二、条件导出的分发规则(编译期决定,运行期零开销):
/// - native(Android/iOS/桌面,dart.library.io 为真):
/// → app_database_io.dart,真 Isar 实现
/// - web(dart2js / dart2wasm,dart.library.io 为假):
/// → app_database_web.dart,空实现(web 仅调试 UI/网络)
///
/// 三、高频坑:
/// 1. 三件套必须都定义 `class AppDatabase`,且成员签名保持一致,
/// 否则共享代码(如 main.dart)在某个平台会编译不过;
/// 2. web 端别用 Isar 集合类型(`IsarCollection<T>`),
/// 那只存在于 io 实现里——web 端没有这些成员;
/// 3. 改模型字段后仍要重跑:
/// dart run build_runner build --delete-conflicting-outputs
/// ============================================================
library;
export 'app_database_web.dart'
if (dart.library.io) 'app_database_io.dart';
@@ -0,0 +1,117 @@
import 'package:isar_community/isar.dart';
import '../models/play_record.dart';
import '../models/question.dart';
import '../models/wrong_question.dart';
import '../models/answer_record.dart';
import 'package:new_learn/core/version/app_version_entity.dart';
import 'package:path_provider/path_provider.dart';
/// ============================================================
/// 【AppDatabase】Isar 本地业务数据库 —— native(Android/iOS/桌面)真实现
///
/// 仅当 dart.library.io 为真时编译(见 app_database.dart 条件导出)。
/// web 端不会编译本文件,因此可以放心 import isar 与 *.g.dart。
///
/// 一、为什么需要第三层(对照前两层):
/// 1. flutter_secure_storage → Token 等敏感凭据(最高安全)
/// 2. shared_preferences → 用户信息/开关等小配置(明文快读)
/// 3. AppDatabase(Isar) → 题库/错题/播放记录等大批量结构化数据
/// 几十上百条 JSON 塞进 shared_preferences 会越写越卡,
/// Isar 是本地 NoSQL 数据库:读写毫秒级、强类型、ACID 事务、支持索引。
///
/// 二、为什么用 isar_community 而不是 hive / isar:
/// - hive:存 Map 无类型保障,官方已停更;
/// - isar 3.1.8:只发在已宕机的 pub.isar-community.dev,装不上;
/// - isar_community:社区在 pub.dev 的维护版,API 与 isar 完全一致。
///
/// 三、高频坑(Isar 专属):
/// 1. init() 必须在 runApp 前 await(见 main.dart),
/// 否则页面访问集合会抛 "Missing XxxSchema in Isar.open";
/// 2. 模型改字段后必须重跑代码生成:
/// dart run build_runner build --delete-conflicting-outputs
/// (生成 *.g.dart,XxxSchema 就在里面);
/// 3. 所有写操作包在 isar.writeTxn() 里;查询用 where()/filter();
/// 4. 集合访问用 `isar.collection<T>()`(与生成的 isar.xxx 等价);
/// 5. Web 端调试用空实现(app_database_web.dart),不要在这加 kIsWeb:
/// 本文件只会在 native 编译,kIsWeb 恒为 false。
/// ============================================================
class AppDatabase {
AppDatabase._();
static final AppDatabase instance = AppDatabase._();
late Isar isar;
/// 初始化:打开 Isar 实例并注册全部集合 Schema(main() 里 await)
Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
isar = await Isar.open(
[
QuestionSchema,
WrongQuestionSchema,
PlayRecordSchema,
RemoteVersionEntitySchema,
AnswerRecordSchema,
],
directory: dir.path,
name: 'app_db',
inspector: true, // ✅加上这一行,debug模式启用Isar Inspector调试页面
);
debugPrintIsarInit();
}
/// 集合访问守卫:未初始化时给出明确报错(而不是空指针)
void _ensureReady() {
if (!isar.isOpen) {
throw StateError('本地数据库(Isar)未初始化:请在 main() 里 await init()');
}
}
/// 题库集合:新增/查询/删除题目都走它
IsarCollection<Question> get questions {
_ensureReady();
return isar.collection<Question>();
}
/// 错题本集合
IsarCollection<WrongQuestion> get wrongQuestions {
_ensureReady();
return isar.collection<WrongQuestion>();
}
/// 答题进度集合(续答功能:进卷读回 / 作答写入 / 交卷清理)
IsarCollection<AnswerRecord> get answerRecords {
_ensureReady();
return isar.collection<AnswerRecord>();
}
/// 播放记录集合
IsarCollection<PlayRecord> get playRecords {
_ensureReady();
return isar.collection<PlayRecord>();
}
/// 远端版本策略缓存集合(版本更新机制用)
IsarCollection<RemoteVersionEntity> get remoteVersions {
_ensureReady();
return isar.collection<RemoteVersionEntity>();
}
/// 清空全部业务数据(换账号/恢复出厂设置时调用)
Future<void> clearAll() async {
_ensureReady();
await isar.writeTxn(() async {
await questions.clear();
await wrongQuestions.clear();
await playRecords.clear();
await remoteVersions.clear();
await answerRecords.clear();
});
}
}
/// 打印初始化完成日志(独立函数便于 web 空实现复用相同日志风格)
void debugPrintIsarInit() {
// ignore: avoid_print
print('▶ 【Isar】业务数据库初始化完成(题库/错题/播放记录)');
}
@@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
/// ============================================================
/// 【AppDatabase】本地业务数据库 —— web 空实现(Chrome/Edge 调试用)
///
/// 仅当 dart.library.io 为假时编译(见 app_database.dart 条件导出)。
/// isar 3.x 全系不支持 web(官方版解析不了 Dart 3;社区版 web 实现是
/// 占位 stub,调用即抛 "Please use Isar 2.5.0 if you need web support"),
/// 且生成代码里的 64 位哈希常量 dart2js 编译不过。
/// 所以 web 端用空实现:init() 直接跳过,UI/登录/网络调试完全不受影响。
/// 生产走 Android/iOS,app_database_io.dart 真 Isar 照常。
///
/// ⚠ 注意:本文件不要 import isar 或任何 *.g.dart ——
/// web 端没有 IsarCollection 等类型,集合读写成员也不存在(编译报错)。
/// 这正是"web 只调试、不上数据库"的强制约束。
/// ============================================================
class AppDatabase {
AppDatabase._();
static final AppDatabase instance = AppDatabase._();
/// web 调试模式:跳过本地数据库(UI/网络正常调试)
Future<void> init() async {
debugPrint('▶ 【Isar】Web 调试模式:跳过本地数据库(UI/网络正常调试)');
}
}
@@ -0,0 +1,35 @@
import 'package:isar_community/isar.dart';
part 'answer_record.g.dart';
/// ============================================================
/// 【AnswerRecord】答题进度持久化模型(续答功能)
///
/// 一、解决什么问题:
/// 答题中途退出 → 答案只存在 Riverpod 内存里,进程一杀就丢。
/// 本模型按"试卷"维度把每题答案落 Isar,下次进卷按 paperId
/// 读回注入 answerProvider,实现续答。
///
/// 二、设计要点:
/// 1. paperId 加 @Index() 索引:进卷恢复、交卷清理都按它查
/// (where(paperId).equalTo() 走索引,100 题毫秒级);
/// 2. answer 统一 String:选择 "A,B" / 判断 "对" / 解答文本,
/// 与 Question 模型的存储约定一致,交卷序列化也简单;
/// 3. 交卷成功后按 paperId 批量删除,磁盘上永远只留
/// "进行中"的试卷答案,不会膨胀。
///
/// 三、高频坑:
/// 1. 和 Question 一样必须有 Id 主键 + 重跑 build_runner;
/// 2. 模型 import 了 isar,web 端不能 import 本文件——
/// store 层用条件导出(见 answer_record_store.dart)。
/// ============================================================
@collection
class AnswerRecord {
Id id = Isar.autoIncrement; // Isar 内部主键(自动递增)
@Index()
int? paperId; // 试卷 id(续答恢复/交卷清理的维度)
int? questionId; // 题目 id(对应 Question.questionId)
String? answer; // 用户答案(多选 "A,B" 逗号拼接)
DateTime? updatedAt; // 作答时间
}
@@ -0,0 +1,981 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'answer_record.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetAnswerRecordCollection on Isar {
IsarCollection<AnswerRecord> get answerRecords => this.collection();
}
const AnswerRecordSchema = CollectionSchema(
name: r'AnswerRecord',
id: -783120389370632988,
properties: {
r'answer': PropertySchema(id: 0, name: r'answer', type: IsarType.string),
r'paperId': PropertySchema(id: 1, name: r'paperId', type: IsarType.long),
r'questionId': PropertySchema(
id: 2,
name: r'questionId',
type: IsarType.long,
),
r'updatedAt': PropertySchema(
id: 3,
name: r'updatedAt',
type: IsarType.dateTime,
),
},
estimateSize: _answerRecordEstimateSize,
serialize: _answerRecordSerialize,
deserialize: _answerRecordDeserialize,
deserializeProp: _answerRecordDeserializeProp,
idName: r'id',
indexes: {
r'paperId': IndexSchema(
id: -7152058561277781812,
name: r'paperId',
unique: false,
replace: false,
properties: [
IndexPropertySchema(
name: r'paperId',
type: IndexType.value,
caseSensitive: false,
),
],
),
},
links: {},
embeddedSchemas: {},
getId: _answerRecordGetId,
getLinks: _answerRecordGetLinks,
attach: _answerRecordAttach,
version: '3.3.2',
);
int _answerRecordEstimateSize(
AnswerRecord object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.answer;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _answerRecordSerialize(
AnswerRecord object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.answer);
writer.writeLong(offsets[1], object.paperId);
writer.writeLong(offsets[2], object.questionId);
writer.writeDateTime(offsets[3], object.updatedAt);
}
AnswerRecord _answerRecordDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = AnswerRecord();
object.answer = reader.readStringOrNull(offsets[0]);
object.id = id;
object.paperId = reader.readLongOrNull(offsets[1]);
object.questionId = reader.readLongOrNull(offsets[2]);
object.updatedAt = reader.readDateTimeOrNull(offsets[3]);
return object;
}
P _answerRecordDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readStringOrNull(offset)) as P;
case 1:
return (reader.readLongOrNull(offset)) as P;
case 2:
return (reader.readLongOrNull(offset)) as P;
case 3:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _answerRecordGetId(AnswerRecord object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _answerRecordGetLinks(AnswerRecord object) {
return [];
}
void _answerRecordAttach(
IsarCollection<dynamic> col,
Id id,
AnswerRecord object,
) {
object.id = id;
}
extension AnswerRecordQueryWhereSort
on QueryBuilder<AnswerRecord, AnswerRecord, QWhere> {
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhere> anyPaperId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
const IndexWhereClause.any(indexName: r'paperId'),
);
});
}
}
extension AnswerRecordQueryWhere
on QueryBuilder<AnswerRecord, AnswerRecord, QWhereClause> {
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> idNotEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> paperIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'paperId', value: [null]),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause>
paperIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [null],
includeLower: false,
upper: [],
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> paperIdEqualTo(
int? paperId,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'paperId', value: [paperId]),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> paperIdNotEqualTo(
int? paperId,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [],
upper: [paperId],
includeUpper: false,
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [paperId],
includeLower: false,
upper: [],
),
);
} else {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [paperId],
includeLower: false,
upper: [],
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [],
upper: [paperId],
includeUpper: false,
),
);
}
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause>
paperIdGreaterThan(int? paperId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [paperId],
includeLower: include,
upper: [],
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> paperIdLessThan(
int? paperId, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [],
upper: [paperId],
includeUpper: include,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterWhereClause> paperIdBetween(
int? lowerPaperId,
int? upperPaperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.between(
indexName: r'paperId',
lower: [lowerPaperId],
includeLower: includeLower,
upper: [upperPaperId],
includeUpper: includeUpper,
),
);
});
}
}
extension AnswerRecordQueryFilter
on QueryBuilder<AnswerRecord, AnswerRecord, QFilterCondition> {
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'answer'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'answer'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> answerEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> answerBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'answer',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'answer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> answerMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'answer',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'answer', value: ''),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
answerIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'answer', value: ''),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'paperId'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'paperId'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdEqualTo(int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'paperId', value: value),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdGreaterThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'paperId',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdLessThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'paperId',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
paperIdBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'paperId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'questionId'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'questionId'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdEqualTo(int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'questionId', value: value),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdGreaterThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'questionId',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdLessThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'questionId',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
questionIdBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'questionId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'updatedAt'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'updatedAt'),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'updatedAt', value: value),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtGreaterThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'updatedAt',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtLessThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'updatedAt',
value: value,
),
);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterFilterCondition>
updatedAtBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'updatedAt',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension AnswerRecordQueryObject
on QueryBuilder<AnswerRecord, AnswerRecord, QFilterCondition> {}
extension AnswerRecordQueryLinks
on QueryBuilder<AnswerRecord, AnswerRecord, QFilterCondition> {}
extension AnswerRecordQuerySortBy
on QueryBuilder<AnswerRecord, AnswerRecord, QSortBy> {
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByAnswer() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'answer', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByAnswerDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'answer', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByPaperId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'paperId', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByPaperIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'paperId', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy>
sortByQuestionIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> sortByUpdatedAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.desc);
});
}
}
extension AnswerRecordQuerySortThenBy
on QueryBuilder<AnswerRecord, AnswerRecord, QSortThenBy> {
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByAnswer() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'answer', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByAnswerDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'answer', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByPaperId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'paperId', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByPaperIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'paperId', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy>
thenByQuestionIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.desc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QAfterSortBy> thenByUpdatedAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.desc);
});
}
}
extension AnswerRecordQueryWhereDistinct
on QueryBuilder<AnswerRecord, AnswerRecord, QDistinct> {
QueryBuilder<AnswerRecord, AnswerRecord, QDistinct> distinctByAnswer({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'answer', caseSensitive: caseSensitive);
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QDistinct> distinctByPaperId() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'paperId');
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QDistinct> distinctByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'questionId');
});
}
QueryBuilder<AnswerRecord, AnswerRecord, QDistinct> distinctByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'updatedAt');
});
}
}
extension AnswerRecordQueryProperty
on QueryBuilder<AnswerRecord, AnswerRecord, QQueryProperty> {
QueryBuilder<AnswerRecord, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<AnswerRecord, String?, QQueryOperations> answerProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'answer');
});
}
QueryBuilder<AnswerRecord, int?, QQueryOperations> paperIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'paperId');
});
}
QueryBuilder<AnswerRecord, int?, QQueryOperations> questionIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'questionId');
});
}
QueryBuilder<AnswerRecord, DateTime?, QQueryOperations> updatedAtProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'updatedAt');
});
}
}
+24
View File
@@ -0,0 +1,24 @@
import 'package:isar_community/isar.dart';
part 'play_record.g.dart';
/// ============================================================
/// 【PlayRecord】播放记录模型(对应 AppDatabase 的播放记录集合)
///
/// 一、字段设计:课程 id + 进度(0~1)+ 最近播放时间,
/// 下次进课程直接按 progress 续播。
///
/// 二、高频坑:
/// 1. progress 用 double 存小数进度,展示成百分比自己换算;
/// 2. updatedAt 每次播放暂停都更新,用于"最近播放"排序;
/// 3. 改字段后记得重跑 build_runner。
/// ============================================================
@collection
class PlayRecord {
Id id = Isar.autoIncrement; // Isar 内部主键(自动递增)
int? courseId; // 课程/课时 id
String? courseName; // 课程名
double? progress; // 播放进度 0~1
DateTime? updatedAt; // 最近播放时间
}
+852
View File
@@ -0,0 +1,852 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'play_record.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetPlayRecordCollection on Isar {
IsarCollection<PlayRecord> get playRecords => this.collection();
}
const PlayRecordSchema = CollectionSchema(
name: r'PlayRecord',
id: -7338646677296495696,
properties: {
r'courseId': PropertySchema(id: 0, name: r'courseId', type: IsarType.long),
r'courseName': PropertySchema(
id: 1,
name: r'courseName',
type: IsarType.string,
),
r'progress': PropertySchema(
id: 2,
name: r'progress',
type: IsarType.double,
),
r'updatedAt': PropertySchema(
id: 3,
name: r'updatedAt',
type: IsarType.dateTime,
),
},
estimateSize: _playRecordEstimateSize,
serialize: _playRecordSerialize,
deserialize: _playRecordDeserialize,
deserializeProp: _playRecordDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _playRecordGetId,
getLinks: _playRecordGetLinks,
attach: _playRecordAttach,
version: '3.3.2',
);
int _playRecordEstimateSize(
PlayRecord object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.courseName;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _playRecordSerialize(
PlayRecord object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeLong(offsets[0], object.courseId);
writer.writeString(offsets[1], object.courseName);
writer.writeDouble(offsets[2], object.progress);
writer.writeDateTime(offsets[3], object.updatedAt);
}
PlayRecord _playRecordDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = PlayRecord();
object.courseId = reader.readLongOrNull(offsets[0]);
object.courseName = reader.readStringOrNull(offsets[1]);
object.id = id;
object.progress = reader.readDoubleOrNull(offsets[2]);
object.updatedAt = reader.readDateTimeOrNull(offsets[3]);
return object;
}
P _playRecordDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readLongOrNull(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
case 2:
return (reader.readDoubleOrNull(offset)) as P;
case 3:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _playRecordGetId(PlayRecord object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _playRecordGetLinks(PlayRecord object) {
return [];
}
void _playRecordAttach(IsarCollection<dynamic> col, Id id, PlayRecord object) {
object.id = id;
}
extension PlayRecordQueryWhereSort
on QueryBuilder<PlayRecord, PlayRecord, QWhere> {
QueryBuilder<PlayRecord, PlayRecord, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension PlayRecordQueryWhere
on QueryBuilder<PlayRecord, PlayRecord, QWhereClause> {
QueryBuilder<PlayRecord, PlayRecord, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension PlayRecordQueryFilter
on QueryBuilder<PlayRecord, PlayRecord, QFilterCondition> {
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'courseId'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'courseId'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseIdEqualTo(
int? value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'courseId', value: value),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseIdGreaterThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'courseId',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseIdLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'courseId',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseIdBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'courseId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'courseName'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'courseName'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseNameEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseNameBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'courseName',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'courseName',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> courseNameMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'courseName',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'courseName', value: ''),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
courseNameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'courseName', value: ''),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> progressIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'progress'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
progressIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'progress'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> progressEqualTo(
double? value, {
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'progress',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
progressGreaterThan(
double? value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'progress',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> progressLessThan(
double? value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'progress',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> progressBetween(
double? lower,
double? upper, {
bool includeLower = true,
bool includeUpper = true,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'progress',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
epsilon: epsilon,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
updatedAtIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'updatedAt'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
updatedAtIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'updatedAt'),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> updatedAtEqualTo(
DateTime? value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'updatedAt', value: value),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition>
updatedAtGreaterThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'updatedAt',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> updatedAtLessThan(
DateTime? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'updatedAt',
value: value,
),
);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterFilterCondition> updatedAtBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'updatedAt',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension PlayRecordQueryObject
on QueryBuilder<PlayRecord, PlayRecord, QFilterCondition> {}
extension PlayRecordQueryLinks
on QueryBuilder<PlayRecord, PlayRecord, QFilterCondition> {}
extension PlayRecordQuerySortBy
on QueryBuilder<PlayRecord, PlayRecord, QSortBy> {
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByCourseId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseId', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByCourseIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseId', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByCourseName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseName', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByCourseNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseName', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByProgress() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'progress', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByProgressDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'progress', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> sortByUpdatedAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.desc);
});
}
}
extension PlayRecordQuerySortThenBy
on QueryBuilder<PlayRecord, PlayRecord, QSortThenBy> {
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByCourseId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseId', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByCourseIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseId', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByCourseName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseName', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByCourseNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'courseName', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByProgress() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'progress', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByProgressDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'progress', Sort.desc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.asc);
});
}
QueryBuilder<PlayRecord, PlayRecord, QAfterSortBy> thenByUpdatedAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'updatedAt', Sort.desc);
});
}
}
extension PlayRecordQueryWhereDistinct
on QueryBuilder<PlayRecord, PlayRecord, QDistinct> {
QueryBuilder<PlayRecord, PlayRecord, QDistinct> distinctByCourseId() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'courseId');
});
}
QueryBuilder<PlayRecord, PlayRecord, QDistinct> distinctByCourseName({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'courseName', caseSensitive: caseSensitive);
});
}
QueryBuilder<PlayRecord, PlayRecord, QDistinct> distinctByProgress() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'progress');
});
}
QueryBuilder<PlayRecord, PlayRecord, QDistinct> distinctByUpdatedAt() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'updatedAt');
});
}
}
extension PlayRecordQueryProperty
on QueryBuilder<PlayRecord, PlayRecord, QQueryProperty> {
QueryBuilder<PlayRecord, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<PlayRecord, int?, QQueryOperations> courseIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'courseId');
});
}
QueryBuilder<PlayRecord, String?, QQueryOperations> courseNameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'courseName');
});
}
QueryBuilder<PlayRecord, double?, QQueryOperations> progressProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'progress');
});
}
QueryBuilder<PlayRecord, DateTime?, QQueryOperations> updatedAtProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'updatedAt');
});
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:isar_community/isar.dart';
part 'question.g.dart';
/// ============================================================
/// 【Question】题库题目模型(Isar 业务集合,对应 AppDatabase 的题库)
///
/// 一、为什么用强类型类而不是 Hive 的 Map:
/// Isar 是强类型 NoSQL,每张"表"(集合)必须对应一个 Dart 类,
/// 类字段即数据库列。字段名/类型写错会在编译期直接报错,
/// 比 Hive 存 Map(运行时才崩)安全得多。
///
/// 二、题型字段 type(与试卷接口模型 ExamQuestion 共用约定):
/// 1 单选 / 2 多选 / 3 判断 / 4 解答;
/// ⚠ 本 Isar 模型是"题库下载/收藏"等本地资产用途,
/// 试卷做题页必须用纯 Dart 的 ExamQuestion(features/exam/),
/// 业务代码 import 本模型会把 schema 哈希拉进 web 编译图导致
/// dart2js 编译失败(详见 ExamQuestion 头注释)。
/// options 统一存 JSON 字符串(选择题存选项数组,判断/解答可空),
/// answer 统一存字符串(多选 "A,B" 逗号拼接),渲染层按题型解析。
///
/// 三、高频坑:
/// 1. 每个集合必须有 Id 主键,Id id = Isar.autoIncrement 表示自增,
/// 写入时 Isar 会自动分配 id 回填到对象上;
/// 2. 字段尽量用可空类型(String?),未赋值存 null;
/// 3. 改字段结构后必须重跑 build_runner,否则 Schema 还是旧的。
/// ============================================================
@collection
class Question {
Id id = Isar.autoIncrement; // Isar 内部主键(自动递增,别手动赋值)
int? questionId; // 服务器题号(业务去重用)
String? title; // 题干(HTML 富文本,RichTextContent 渲染)
String? options; // 选项(JSON 字符串,如 ["A项","B项","C项","D项"])
String? answer; // 正确答案(多选 "A,B" 逗号拼接)
int? type; // 题型:1 单选 / 2 多选 / 3 判断 / 4 解答
String? analysis; // 解析(交卷后查看答案用)
int? categoryId; // 分类 id
DateTime? createdAt; // 收藏/下载时间
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
import 'package:isar_community/isar.dart';
part 'wrong_question.g.dart';
/// ============================================================
/// 【WrongQuestion】错题本模型(对应 AppDatabase 的错题集合)
///
/// 一、字段设计:存题目快照 + 我的错误答案 + 时间,
/// 离线也能看错题,不用每次进错题本都拉接口。
///
/// 二、高频坑:
/// 1. 和 Question 一样必须有 Id 主键;
/// 2. 服务器题号存 questionId 用于去重(同一题错多次只留一条);
/// 3. 改字段后记得重跑 build_runner。
/// ============================================================
@collection
class WrongQuestion {
Id id = Isar.autoIncrement; // Isar 内部主键(自动递增)
int? questionId; // 题目 id(去重)
String? title; // 题干快照
String? wrongAnswer; // 我提交的错误答案
DateTime? wrongAt; // 加入错题本时间
}
@@ -0,0 +1,931 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wrong_question.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetWrongQuestionCollection on Isar {
IsarCollection<WrongQuestion> get wrongQuestions => this.collection();
}
const WrongQuestionSchema = CollectionSchema(
name: r'WrongQuestion',
id: -8005968927277545936,
properties: {
r'questionId': PropertySchema(
id: 0,
name: r'questionId',
type: IsarType.long,
),
r'title': PropertySchema(id: 1, name: r'title', type: IsarType.string),
r'wrongAnswer': PropertySchema(
id: 2,
name: r'wrongAnswer',
type: IsarType.string,
),
r'wrongAt': PropertySchema(
id: 3,
name: r'wrongAt',
type: IsarType.dateTime,
),
},
estimateSize: _wrongQuestionEstimateSize,
serialize: _wrongQuestionSerialize,
deserialize: _wrongQuestionDeserialize,
deserializeProp: _wrongQuestionDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _wrongQuestionGetId,
getLinks: _wrongQuestionGetLinks,
attach: _wrongQuestionAttach,
version: '3.3.2',
);
int _wrongQuestionEstimateSize(
WrongQuestion object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
{
final value = object.title;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
{
final value = object.wrongAnswer;
if (value != null) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _wrongQuestionSerialize(
WrongQuestion object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeLong(offsets[0], object.questionId);
writer.writeString(offsets[1], object.title);
writer.writeString(offsets[2], object.wrongAnswer);
writer.writeDateTime(offsets[3], object.wrongAt);
}
WrongQuestion _wrongQuestionDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = WrongQuestion();
object.id = id;
object.questionId = reader.readLongOrNull(offsets[0]);
object.title = reader.readStringOrNull(offsets[1]);
object.wrongAnswer = reader.readStringOrNull(offsets[2]);
object.wrongAt = reader.readDateTimeOrNull(offsets[3]);
return object;
}
P _wrongQuestionDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readLongOrNull(offset)) as P;
case 1:
return (reader.readStringOrNull(offset)) as P;
case 2:
return (reader.readStringOrNull(offset)) as P;
case 3:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _wrongQuestionGetId(WrongQuestion object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _wrongQuestionGetLinks(WrongQuestion object) {
return [];
}
void _wrongQuestionAttach(
IsarCollection<dynamic> col,
Id id,
WrongQuestion object,
) {
object.id = id;
}
extension WrongQuestionQueryWhereSort
on QueryBuilder<WrongQuestion, WrongQuestion, QWhere> {
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension WrongQuestionQueryWhere
on QueryBuilder<WrongQuestion, WrongQuestion, QWhereClause> {
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhereClause> idEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhereClause> idNotEqualTo(
Id id,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension WrongQuestionQueryFilter
on QueryBuilder<WrongQuestion, WrongQuestion, QFilterCondition> {
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
idGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'questionId'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'questionId'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdEqualTo(int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'questionId', value: value),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdGreaterThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'questionId',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdLessThan(int? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'questionId',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
questionIdBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'questionId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'title'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'title'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleEqualTo(String? value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'title',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'title',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'title',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'title', value: ''),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
titleIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'title', value: ''),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'wrongAnswer'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'wrongAnswer'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerEqualTo(String? value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerBetween(
String? lower,
String? upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'wrongAnswer',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'wrongAnswer',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'wrongAnswer',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'wrongAnswer', value: ''),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAnswerIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'wrongAnswer', value: ''),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'wrongAt'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'wrongAt'),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'wrongAt', value: value),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtGreaterThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'wrongAt',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtLessThan(DateTime? value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'wrongAt',
value: value,
),
);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterFilterCondition>
wrongAtBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'wrongAt',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension WrongQuestionQueryObject
on QueryBuilder<WrongQuestion, WrongQuestion, QFilterCondition> {}
extension WrongQuestionQueryLinks
on QueryBuilder<WrongQuestion, WrongQuestion, QFilterCondition> {}
extension WrongQuestionQuerySortBy
on QueryBuilder<WrongQuestion, WrongQuestion, QSortBy> {
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy>
sortByQuestionIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByTitle() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'title', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByTitleDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'title', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByWrongAnswer() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAnswer', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy>
sortByWrongAnswerDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAnswer', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByWrongAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAt', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> sortByWrongAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAt', Sort.desc);
});
}
}
extension WrongQuestionQuerySortThenBy
on QueryBuilder<WrongQuestion, WrongQuestion, QSortThenBy> {
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy>
thenByQuestionIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'questionId', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByTitle() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'title', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByTitleDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'title', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByWrongAnswer() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAnswer', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy>
thenByWrongAnswerDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAnswer', Sort.desc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByWrongAt() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAt', Sort.asc);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QAfterSortBy> thenByWrongAtDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'wrongAt', Sort.desc);
});
}
}
extension WrongQuestionQueryWhereDistinct
on QueryBuilder<WrongQuestion, WrongQuestion, QDistinct> {
QueryBuilder<WrongQuestion, WrongQuestion, QDistinct> distinctByQuestionId() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'questionId');
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QDistinct> distinctByTitle({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'title', caseSensitive: caseSensitive);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QDistinct> distinctByWrongAnswer({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'wrongAnswer', caseSensitive: caseSensitive);
});
}
QueryBuilder<WrongQuestion, WrongQuestion, QDistinct> distinctByWrongAt() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'wrongAt');
});
}
}
extension WrongQuestionQueryProperty
on QueryBuilder<WrongQuestion, WrongQuestion, QQueryProperty> {
QueryBuilder<WrongQuestion, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<WrongQuestion, int?, QQueryOperations> questionIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'questionId');
});
}
QueryBuilder<WrongQuestion, String?, QQueryOperations> titleProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'title');
});
}
QueryBuilder<WrongQuestion, String?, QQueryOperations> wrongAnswerProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'wrongAnswer');
});
}
QueryBuilder<WrongQuestion, DateTime?, QQueryOperations> wrongAtProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'wrongAt');
});
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:shared_preferences/shared_preferences.dart';
/// ============================================================
/// 【PrivacyStorage】隐私协议同意状态本地存储
///
/// 作用:
/// 1. 记录用户是否已经同意启动隐私协议;
/// 2. App 冷启动时先读这个值,决定是否要先拦住进入主流程;
/// 3. 一旦同意过,就不再重复弹首次启动隐私弹层。
///
/// 为什么单独拆一个存储类:
/// 这类"启动门禁"和用户资料不是一回事,放独立文件更清晰,
/// 后续如果要补"撤回授权 / 重新查看协议"也更好扩展。
/// ============================================================
class PrivacyStorage {
PrivacyStorage._();
static final PrivacyStorage instance = PrivacyStorage._();
static const String _privacyAcceptedKey = 'privacy_accepted';
/// 读取是否已同意隐私协议。
/// 首次安装/首次打开没有记录时,默认返回 false。
Future<bool> readAccepted() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_privacyAcceptedKey) ?? false;
}
/// 保存"已同意隐私协议"状态。
Future<void> saveAccepted(bool accepted) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_privacyAcceptedKey, accepted);
}
}
@@ -0,0 +1,23 @@
/// ============================================================
/// 【AnswerRecordStore】答题进度数据源 —— 平台分发入口(条件导出)
///
/// 一、为什么也要条件导出(和 WrongQuestionStore 同理):
/// AnswerRecord 模型 import 了 isar,web 端 import 直接编译失败。
/// 所以"Isar 访问"全部收口到本 store,页面永远只面向本文件:
/// - native → answer_record_store_io.dart(真 Isar,续答落盘)
/// - web → answer_record_store_web.dart(空实现,跳过持久化)
///
/// 二、续答三动作(对应三个方法):
/// findByPaper() 进卷时读回 → 注入 answerProvider
/// put() 每次作答增量写单条(毫秒级)
/// deleteByPaper() 交卷成功后按 paperId 批量清理
///
/// 三、接口统一用 dynamic:
/// 模型 import 了 isar,web 端无法 import 模型,接口层返回
/// `List<dynamic>`,页面用 dynamic 访问字段(见 wrong_question_store)。
/// ============================================================
library;
/// 如果是原生平台就用 Isar 真实现,否则用空实现兜底
export 'answer_record_store_web.dart'
if (dart.library.io) 'answer_record_store_io.dart';
@@ -0,0 +1,68 @@
import 'package:isar_community/isar.dart';
import 'package:new_learn/core/storage/database/app_database_io.dart';
import '../models/answer_record.dart';
/// ============================================================
/// 【AnswerRecordStore】答题进度数据源 —— native 真实现(Isar)
///
/// 一、续答数据流(进卷 → 答题 → 交卷):
/// findByPaper() 进卷时按 paperId 查全部答案 → 注入 answerProvider
/// put() 每题作答写一条(增量 put,幂等:同题覆盖旧答案)
/// deleteByPaper() 交卷成功后批量删该卷记录(防止下次恢复旧答案)
///
/// 二、为什么用 @Index() 索引:
/// where(paperId).equalTo() 走索引查询,100 题毫秒级;
/// 交卷 deleteByPaper 同样按 paperId 过滤,一次事务删干净。
///
/// 三、坑:
/// 1. 写操作必须包在 isar.writeTxn()(Isar 事务约束);
/// 2. 改模型字段后重跑 build_runner;
/// 3. ⚠ 这里必须 import app_database_io.dart(具体实现),
/// 不能 import app_database.dart(条件导出入口)!
/// 分析器会检查所有条件分支,web 分支下入口解析成空实现,
/// db.isar / db.answerRecords 会报 undefined getter。
/// ============================================================
class AnswerRecordStore {
AnswerRecordStore._();
static final AnswerRecordStore instance = AnswerRecordStore._();
/// 【进卷恢复】按试卷查全部已答记录
Future<List<dynamic>> findByPaper(int paperId) async {
final db = AppDatabase.instance;
final list = await db.answerRecords
.where()
.paperIdEqualTo(paperId)
.findAll();
return list.cast<dynamic>();
}
/// 【作答写入】每题答案增量落盘(同题重复作答自动覆盖旧值)
Future<void> put({
required int paperId,
required int questionId,
required String answer,
}) async {
final db = AppDatabase.instance;
await db.isar.writeTxn(() async {
await db.answerRecords.put(
AnswerRecord()
..paperId = paperId
..questionId = questionId
..answer = answer
..updatedAt = DateTime.now(),
);
});
}
/// 【交卷清理】按试卷批量删除进度(交卷成功后调用)
Future<void> deleteByPaper(int paperId) async {
final db = AppDatabase.instance;
await db.isar.writeTxn(() async {
await db.answerRecords
.where()
.paperIdEqualTo(paperId)
.deleteAll();
});
}
}
@@ -0,0 +1,27 @@
import 'package:flutter/foundation.dart';
/// ============================================================
/// 【AnswerRecordStore】答题进度数据源 —— web 空实现
///
/// web 端不 import isar / 模型(编译约束),所有操作变为空实现:
/// 写入 → 打印提示;读取 → 空列表。
/// 效果:web 调试时"续答持久化"自动降级为纯内存(不落盘),
/// UI/滑动/答题逻辑全部正常,只有刷新页面后进度丢失。
/// ============================================================
class AnswerRecordStore {
AnswerRecordStore._();
static final AnswerRecordStore instance = AnswerRecordStore._();
Future<List<dynamic>> findByPaper(int paperId) async => const [];
Future<void> put({
required int paperId,
required int questionId,
required String answer,
}) async {
debugPrint('▶ 【Isar】Web 调试模式:答题进度写入已跳过(仅真机可持久化续答)');
}
Future<void> deleteByPaper(int paperId) async {}
}
@@ -0,0 +1,25 @@
/// ============================================================
/// 【WrongQuestionStore】错题本数据源 —— 平台分发入口(条件导出)
///
/// 一、为什么也要条件导出(和 AppDatabase 同理):
/// Isar 的集合类型(IsarCollection<T>)只存在于 native 实现,
/// web 端 import isar / *.g.dart 直接编译失败。页面如果直接
/// 访问 AppDatabase.instance.wrongQuestions,web 编译就报错。
/// 所以把"Isar 访问"全部收口到本 store,页面永远只面向本文件:
/// - native → wrong_question_store_io.dart(真 Isar)
/// - web → wrong_question_store_web.dart(空实现,跳过写库)
///
/// 二、为什么接口用 dynamic 而非 WrongQuestion:
/// WrongQuestion 模型 import 了 isar,web 端无法 import 模型。
/// 接口统一返回 List<dynamic>,页面用 dynamic 访问字段,
/// web 编译通过、运行时只是拿不到数据(演示降级提示)。
///
/// 三、这是【Isar 深水区:watch 响应式】的入口:
/// watchAll() 返回 Stream:数据库一变(任何地方 put/delete),
/// 流自动发事件 → 页面 StreamBuilder 自动重建,不用手动 reload。
/// ============================================================
library;
/// 如果是原生平台就用 Isar 真实现,否则用空实现兜底
export 'wrong_question_store_web.dart'
if (dart.library.io) 'wrong_question_store_io.dart';
@@ -0,0 +1,79 @@
import 'package:isar_community/isar.dart';
import 'package:new_learn/core/storage/database/app_database_io.dart';
import '../models/wrong_question.dart';
/// ============================================================
/// 【WrongQuestionStore】错题本数据源 —— native 真实现(Isar)
///
/// 一、watch 响应式(本文件核心):
/// watchAll() → AppDatabase.wrongQuestions.where().watch(fireImmediately: true)
/// - fireImmediately: true = 订阅后立刻吐一次当前全量数据,
/// 页面首帧就能显示,不用再单独查一次;
/// - 之后任何地方 put/delete 错题(即使不是本页面),
/// 流自动发新事件 → 订阅它的 StreamBuilder 自动重建。
/// 这就是"数据库推数据"而非"页面拉数据"。
///
/// 二、对比非响应式:
/// findAll() → where().findAll() 是主动查询,要自己触发/刷新。
///
/// 三、坑:
/// 1. 写操作必须包在 isar.writeTxn()(Isar 事务约束);
/// 2. 改模型字段后重跑 build_runner;
/// 3. watch 返回 `Stream<List<Object>>`,接口层 cast 成
/// `List<dynamic>` 供页面消费(web 端也要能编译);
/// 4. ⚠ 这里必须 import app_database_io.dart(具体实现),
/// 不能 import app_database.dart(条件导出入口)!
/// 分析器会检查所有条件分支,web 分支下入口解析成空实现,
/// db.isar / db.wrongQuestions 会报 undefined getter。
/// 本文件只在 native 编译,直接 import io 实现最稳。
/// 5. ⚠ isar_community 3.3.2 的 API 和 isar 3.1.x 不一样(大坑):
/// - IsarCollection 上【没有】watch(),全量监听要写成
/// where().watch()(返回 `Stream<List<T>>`);
/// - QueryBuilder 上【没有】findAll(),终端方法(findAll/
/// watch/count/findFirst...)都在 extension 里,而 Dart 的
/// extension 成员要求定义它的库被 import 才可见(import 不传递),
/// 所以本文件必须加 import 'package:isar_community/isar.dart',
/// 否则 where().findAll() / where().watch() 报 undefined method。
/// ============================================================
class WrongQuestionStore {
WrongQuestionStore._();
static final WrongQuestionStore instance = WrongQuestionStore._();
/// 添加一条错题(演示:任何写入都会触发 watch 事件)
Future<void> add({
int? questionId,
String? title,
String? wrongAnswer,
}) async {
final db = AppDatabase.instance;
await db.isar.writeTxn(() async {
await db.wrongQuestions.put(
WrongQuestion()
..questionId = questionId
..title = title
..wrongAnswer = wrongAnswer
..wrongAt = DateTime.now(),
);
});
}
/// 删除一条(按 Isar 主键 id)
Future<void> remove(int id) async {
final db = AppDatabase.instance;
await db.isar.writeTxn(() => db.wrongQuestions.delete(id));
}
/// 【响应式】错题集合 watch 流:数据一变自动发事件
Stream<List<dynamic>> watchAll() {
return AppDatabase.instance.wrongQuestions
.where()
.watch(fireImmediately: true)
.cast<List<dynamic>>();
}
/// 【非响应式】手动查询全部(对比 watch 用,需自己触发刷新)
Future<List<dynamic>> findAll() async {
return AppDatabase.instance.wrongQuestions.where().findAll();
}
}
@@ -0,0 +1,28 @@
import 'package:flutter/foundation.dart';
/// ============================================================
/// 【WrongQuestionStore】错题本数据源 —— web 空实现
///
/// web 端不 import isar / 模型(编译约束),所有操作变为空实现:
/// 写入 → 打印提示;读取 → 空流/空列表。
/// demo 页面在 web 上用 kIsWeb 显示"仅真机可用"提示即可。
/// ============================================================
class WrongQuestionStore {
WrongQuestionStore._();
static final WrongQuestionStore instance = WrongQuestionStore._();
Future<void> add({
int? questionId,
String? title,
String? wrongAnswer,
}) async {
debugPrint('▶ 【Isar】Web 调试模式:错题写入已跳过(仅真机可用)');
}
Future<void> remove(int id) async {}
Stream<List<dynamic>> watchAll() => const Stream.empty();
Future<List<dynamic>> findAll() async => const [];
}
+127
View File
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:new_learn/core/models/user_model.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// ============================================================
/// 【UserStorage】用户本地存储(token 安全存储 + 用户信息缓存)
///
/// 一、为什么用两个插件(对应 Vue 的 uni.setStorageSync):
/// 1. flutter_secure_storage → 存 Token(敏感凭据)
/// Android 走 Keystore 加密(EncryptedSharedPreferences),
/// iOS 走 Keychain,越狱/root 也难读出,适合放登录凭据;
/// 2. shared_preferences → 存用户信息缓存(非敏感)
/// 明文 XML/plist,读写快,适合放 userName/avatar 这类
/// 不敏感数据 + 各种开关。
/// 混合方案 = 业界标准做法:凭据安全、配置轻量。
///
/// 二、高频坑:
/// 1. 两个插件都要 await 初始化,别在 build 里同步调用;
/// 2. shared_preferences 存对象必须先 jsonEncode 成字符串;
/// 3. 密码永远不要明文存 shared_preferences!(记住密码≠存密码)
/// ============================================================
class UserStorage {
UserStorage._();
static final UserStorage instance = UserStorage._();
static const String _tokenKey = 'auth_token';
static const String _userKey = 'current_user';
final FlutterSecureStorage _secure = const FlutterSecureStorage();
// ---------- Token(安全存储) ----------
/// 保存登录 Token(登录成功后调用)
Future<void> saveToken(String token) =>
_secure.write(key: _tokenKey, value: token);
/// 读取 Token(冷启动 restoreToken 用)
Future<String?> readToken() => _secure.read(key: _tokenKey);
/// 清除 Token(退出登录时调用)
Future<void> clearToken() => _secure.delete(key: _tokenKey);
// ---------- 用户信息(普通缓存) ----------
/// 保存当前用户信息(登录后调 GetCurrentUserExtension 拿到再存)
///
/// 作用:
/// 1. 把"内存里的用户对象"落到本地;
/// 2. App 下次冷启动时,currentUserProvider.build() 可以直接读缓存回填;
/// 3. 这样头像/用户名/班级等信息不用等接口返回,页面也能先展示。
///
/// 为什么要 jsonEncode:
/// SharedPreferences 不能直接存 Dart 对象,只能存 String/int/bool 等基础类型,
/// 所以要先把 CurrentUserExtension → Map → JSON 字符串。
Future<void> saveUserInfo(CurrentUserExtension user) async {
// 拿到 SharedPreferences 单例,后续通过 key-value 方式写本地缓存。
final prefs = await SharedPreferences.getInstance();
// user.toJson():对象转 Map
// jsonEncode(...):Map 转字符串
// _userKey:当前用户缓存的固定 key
await prefs.setString(_userKey, jsonEncode(user.toJson()));
}
/// 读取当前用户信息(没有登录过则返回 null)
Future<CurrentUserExtension?> readUserInfo() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_userKey);
if (raw == null) return null;
try {
return CurrentUserExtension.fromJson(
jsonDecode(raw) as Map<String, dynamic>,
);
} catch (_) {
return null; // 缓存损坏时当没登录过处理
}
}
/// 清除用户信息(退出登录时调用)
Future<void> clearUserInfo() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_userKey);
}
// ---------- 记住密码(账号普通缓存 + 密码安全存储) ----------
static const String _rememberedAccountKey = 'remembered_account';
static const String _rememberedPasswordKey = 'remembered_password';
/// 保存记住的账号(非敏感 → shared_preferences 明文即可)
Future<void> saveRememberedAccount(String account) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_rememberedAccountKey, account);
}
/// 读取记住的账号(没存过返回 null)
Future<String?> readRememberedAccount() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_rememberedAccountKey);
}
Future<void> clearRememberedAccount() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_rememberedAccountKey);
}
/// 保存记住的密码(敏感 → flutter_secure_storage 加密存储)
/// 注意:只有用户勾选"记住密码"才调用,不勾选绝不落盘
Future<void> saveRememberedPassword(String password) =>
_secure.write(key: _rememberedPasswordKey, value: password);
Future<String?> readRememberedPassword() =>
_secure.read(key: _rememberedPasswordKey);
Future<void> clearRememberedPassword() =>
_secure.delete(key: _rememberedPasswordKey);
/// 一键退出登录:清 Token + 用户信息
/// (记住的账号密码保留,方便用户下次登录)
Future<void> clearAll() async {
await clearToken();
await clearUserInfo();
}
}