fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

- 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:
iven
2026-06-02 21:21:43 +08:00
parent 7e928ae1e1
commit 49d4aa36a7
55 changed files with 2738 additions and 677 deletions

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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,
);
}