fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// 搜索 BLoC — 标签+心情筛选日记
|
||||
// 搜索 BLoC — 关键词+标签+心情筛选日记
|
||||
//
|
||||
// 状态机: SearchInitial → SearchLoading → SearchLoaded/SearchError
|
||||
// Phase 1 使用简单的标签+心情筛选,后续可扩展全文搜索。
|
||||
// 支持关键词搜索、标签筛选、心情筛选、结果分类 tab。
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -11,16 +11,21 @@ import '../../../data/repositories/journal_repository.dart';
|
||||
part 'search_event.dart';
|
||||
part 'search_state.dart';
|
||||
|
||||
/// 搜索 BLoC — 处理标签和心情筛选日记的状态转换
|
||||
/// 搜索 BLoC
|
||||
class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
final JournalRepository _journalRepo;
|
||||
|
||||
/// 内存搜索历史(最多 10 条)
|
||||
final List<String> _searchHistory = [];
|
||||
|
||||
SearchBloc({required JournalRepository journalRepository})
|
||||
: _journalRepo = journalRepository,
|
||||
super(const SearchInitial()) {
|
||||
on<SearchByMood>(_onSearchByMood);
|
||||
on<SearchByTag>(_onSearchByTag);
|
||||
on<SearchByKeyword>(_onSearchByKeyword);
|
||||
on<SearchClear>(_onSearchClear);
|
||||
on<SearchTabChanged>(_onSearchTabChanged);
|
||||
}
|
||||
|
||||
/// 按心情筛选日记
|
||||
@@ -31,7 +36,7 @@ class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
emit(const SearchLoading());
|
||||
try {
|
||||
if (event.mood == null) {
|
||||
emit(const SearchLoaded());
|
||||
emit(SearchLoaded(searchHistory: List.unmodifiable(_searchHistory)));
|
||||
return;
|
||||
}
|
||||
final results = await _journalRepo.getJournals(
|
||||
@@ -42,6 +47,7 @@ class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
emit(SearchLoaded(
|
||||
results: results,
|
||||
activeMood: event.mood!.value,
|
||||
searchHistory: List.unmodifiable(_searchHistory),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(const SearchError('搜索失败,请重试'));
|
||||
@@ -55,6 +61,7 @@ class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
) async {
|
||||
emit(const SearchLoading());
|
||||
try {
|
||||
_addToHistory(event.tag);
|
||||
final results = await _journalRepo.getJournals(
|
||||
tag: event.tag,
|
||||
page: 1,
|
||||
@@ -63,6 +70,47 @@ class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
emit(SearchLoaded(
|
||||
results: results,
|
||||
activeTag: event.tag,
|
||||
searchHistory: List.unmodifiable(_searchHistory),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(const SearchError('搜索失败,请重试'));
|
||||
}
|
||||
}
|
||||
|
||||
/// 关键词搜索 — 在标题中匹配关键词
|
||||
Future<void> _onSearchByKeyword(
|
||||
SearchByKeyword event,
|
||||
Emitter<SearchState> emit,
|
||||
) async {
|
||||
final keyword = event.keyword.trim();
|
||||
if (keyword.isEmpty) {
|
||||
emit(SearchLoaded(searchHistory: List.unmodifiable(_searchHistory)));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const SearchLoading());
|
||||
try {
|
||||
_addToHistory(keyword);
|
||||
// 获取所有日记并在客户端按关键词过滤
|
||||
final allJournals = await _journalRepo.getJournals(
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
);
|
||||
final lowerKeyword = keyword.toLowerCase();
|
||||
final results = allJournals.where((j) {
|
||||
final titleMatch = j.title.toLowerCase().contains(lowerKeyword);
|
||||
final excerptMatch = (j.contentExcerpt ?? '')
|
||||
.toLowerCase()
|
||||
.contains(lowerKeyword);
|
||||
final tagMatch =
|
||||
j.tags.any((t) => t.toLowerCase().contains(lowerKeyword));
|
||||
return titleMatch || excerptMatch || tagMatch;
|
||||
}).toList();
|
||||
|
||||
emit(SearchLoaded(
|
||||
results: results,
|
||||
activeKeyword: keyword,
|
||||
searchHistory: List.unmodifiable(_searchHistory),
|
||||
));
|
||||
} catch (e) {
|
||||
emit(const SearchError('搜索失败,请重试'));
|
||||
@@ -74,6 +122,26 @@ class SearchBloc extends Bloc<SearchEvent, SearchState> {
|
||||
SearchClear event,
|
||||
Emitter<SearchState> emit,
|
||||
) {
|
||||
emit(const SearchLoaded());
|
||||
emit(SearchLoaded(searchHistory: List.unmodifiable(_searchHistory)));
|
||||
}
|
||||
|
||||
/// 切换结果分类 tab
|
||||
void _onSearchTabChanged(
|
||||
SearchTabChanged event,
|
||||
Emitter<SearchState> emit,
|
||||
) {
|
||||
final current = state;
|
||||
if (current is SearchLoaded) {
|
||||
emit(current.copyWith(activeTab: event.tab));
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加到搜索历史(去重,最多 10 条)
|
||||
void _addToHistory(String keyword) {
|
||||
_searchHistory.remove(keyword);
|
||||
_searchHistory.insert(0, keyword);
|
||||
if (_searchHistory.length > 10) {
|
||||
_searchHistory.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,19 @@ final class SearchByTag extends SearchEvent {
|
||||
const SearchByTag(this.tag);
|
||||
}
|
||||
|
||||
/// 关键词搜索
|
||||
final class SearchByKeyword extends SearchEvent {
|
||||
final String keyword;
|
||||
const SearchByKeyword(this.keyword);
|
||||
}
|
||||
|
||||
/// 清除搜索结果
|
||||
final class SearchClear extends SearchEvent {
|
||||
const SearchClear();
|
||||
}
|
||||
|
||||
/// 切换搜索结果分类 tab
|
||||
final class SearchTabChanged extends SearchEvent {
|
||||
final SearchResultTab tab;
|
||||
const SearchTabChanged(this.tab);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
part of 'search_bloc.dart';
|
||||
|
||||
/// 搜索结果分类 tab
|
||||
enum SearchResultTab {
|
||||
all('全部'),
|
||||
journal('日记'),
|
||||
template('模板'),
|
||||
tag('标签');
|
||||
|
||||
const SearchResultTab(this.label);
|
||||
final String label;
|
||||
}
|
||||
|
||||
/// 搜索状态基类
|
||||
sealed class SearchState {
|
||||
const SearchState();
|
||||
@@ -19,7 +30,7 @@ final class SearchLoading extends SearchState {
|
||||
|
||||
/// 搜索结果已加载
|
||||
final class SearchLoaded extends SearchState {
|
||||
/// 搜索结果列表(空列表表示无匹配)
|
||||
/// 日记搜索结果列表
|
||||
final List<JournalEntry> results;
|
||||
|
||||
/// 当前活跃的心情筛选条件
|
||||
@@ -28,24 +39,47 @@ final class SearchLoaded extends SearchState {
|
||||
/// 当前活跃的标签筛选条件
|
||||
final String? activeTag;
|
||||
|
||||
/// 当前活跃的关键词
|
||||
final String? activeKeyword;
|
||||
|
||||
/// 当前选中的结果分类 tab
|
||||
final SearchResultTab activeTab;
|
||||
|
||||
/// 搜索历史(内存中保存,最多 10 条)
|
||||
final List<String> searchHistory;
|
||||
|
||||
const SearchLoaded({
|
||||
this.results = const [],
|
||||
this.activeMood,
|
||||
this.activeTag,
|
||||
this.activeKeyword,
|
||||
this.activeTab = SearchResultTab.all,
|
||||
this.searchHistory = const [],
|
||||
});
|
||||
|
||||
/// 是否有活跃的筛选条件
|
||||
bool get hasActiveFilter => activeMood != null || activeTag != null;
|
||||
bool get hasActiveFilter =>
|
||||
activeMood != null || activeTag != null || activeKeyword != null;
|
||||
|
||||
SearchLoaded copyWith({
|
||||
List<JournalEntry>? results,
|
||||
String? activeMood,
|
||||
bool clearActiveMood = false,
|
||||
String? activeTag,
|
||||
bool clearActiveTag = false,
|
||||
String? activeKeyword,
|
||||
bool clearActiveKeyword = false,
|
||||
SearchResultTab? activeTab,
|
||||
List<String>? searchHistory,
|
||||
}) =>
|
||||
SearchLoaded(
|
||||
results: results ?? this.results,
|
||||
activeMood: activeMood ?? this.activeMood,
|
||||
activeTag: activeTag ?? this.activeTag,
|
||||
activeMood: clearActiveMood ? null : (activeMood ?? this.activeMood),
|
||||
activeTag: clearActiveTag ? null : (activeTag ?? this.activeTag),
|
||||
activeKeyword:
|
||||
clearActiveKeyword ? null : (activeKeyword ?? this.activeKeyword),
|
||||
activeTab: activeTab ?? this.activeTab,
|
||||
searchHistory: searchHistory ?? this.searchHistory,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user