fix(app): 修复 P2~P4 共 10 项前端问题
P2 必须修复: - 教师布置主题 classId 从硬编码改为班级下拉选择器 - 班级日记墙使用服务端 classId 过滤替代前端过滤 - Profile 统计栏接入 JournalRepository 真实数据 - WeeklyPage 从全硬编码改为 JournalRepository 数据驱动 P3 建议改进: - 提取 mood_utils.dart 公共函数,消除 4 处重复定义 - 贴纸库搜索框连接 StickerBloc 按名称过滤 P4 细节打磨: - 家长页多孩子时显示 DropdownButton 选择器 - 搜索结果日记卡片点击跳转 /editor?id= - MonthlyPage 照片数量从 JournalElement 统计 - calendar_page/mood_page/search_page 统一使用 moodToEmoji/moodToLabel
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
// 搜索页面 — 标签+心情筛选日记
|
||||
// 搜索页面 — 搜索历史 + 热门搜索 + 关键词高亮 + 分类 Tab
|
||||
//
|
||||
// 通过 SearchBloc 驱动搜索状态:
|
||||
// - 关键词输入 → SearchByKeyword event
|
||||
// - 标签点击 → SearchByTag event
|
||||
// - 心情选择 → SearchByMood event
|
||||
// - Tab 切换 → SearchTabChanged event
|
||||
// - 清除按钮 → SearchClear event
|
||||
// 搜索结果由 BlocBuilder<SearchBloc, SearchState> 响应式渲染。
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_radius.dart';
|
||||
import '../../../core/utils/mood_utils.dart';
|
||||
import '../../../data/models/journal_entry.dart';
|
||||
import '../bloc/search_bloc.dart';
|
||||
|
||||
/// 搜索页面 — 标签+心情筛选日记
|
||||
/// 搜索页面 — 搜索历史 + 热门搜索 + 结果分类
|
||||
class SearchPage extends StatefulWidget {
|
||||
const SearchPage({super.key});
|
||||
|
||||
@@ -24,14 +29,24 @@ class SearchPage extends StatefulWidget {
|
||||
|
||||
class _SearchPageState extends State<SearchPage> {
|
||||
final _searchController = TextEditingController();
|
||||
final _searchFocusNode = FocusNode();
|
||||
|
||||
// Phase 1 占位标签数据
|
||||
final _recentTags = ['日常', '学校', '旅行', '美食', '读书', '心情'];
|
||||
final _moodFilters = Mood.values;
|
||||
// 热门搜索占位数据
|
||||
final _hotSearches = ['日常', '学校', '旅行', '美食', '读书', '心情', '手账', '贴纸'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 自动弹出键盘
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_searchFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -39,120 +54,341 @@ class _SearchPageState extends State<SearchPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return BlocBuilder<SearchBloc, SearchState>(
|
||||
builder: (context, state) {
|
||||
final hasFilter = state is SearchLoaded && state.hasActiveFilter;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索日记...',
|
||||
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 6A: 搜索头部 — 返回 + 输入框 + 取消
|
||||
_buildSearchHeader(
|
||||
context, theme, colorScheme, isDark),
|
||||
// 6C: 结果分类 Tab(有结果时显示)
|
||||
if (state case SearchLoaded(:final hasActiveFilter) when hasActiveFilter)
|
||||
_buildResultTabs(context, state),
|
||||
// 主体内容
|
||||
Expanded(
|
||||
child: _buildBody(
|
||||
context, theme, colorScheme, isDark, state),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: hasFilter
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.filter_alt_off),
|
||||
tooltip: '清除筛选',
|
||||
onPressed: _clearSearch,
|
||||
)
|
||||
: (_searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_clearSearch();
|
||||
},
|
||||
)
|
||||
: null),
|
||||
),
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context.read<SearchBloc>().add(SearchByTag(value.trim()));
|
||||
}
|
||||
},
|
||||
],
|
||||
),
|
||||
),
|
||||
body: _buildBody(context, theme, colorScheme, state),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据搜索状态构建 body
|
||||
// ===== 6A: 搜索头部 =====
|
||||
|
||||
Widget _buildSearchHeader(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
) {
|
||||
final surfaceWarmColor = isDark
|
||||
? AppColors.surfaceWarmDark
|
||||
: AppColors.surfaceWarmLight;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
SizedBox(
|
||||
width: 44,
|
||||
height: 44,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new,
|
||||
size: 18,
|
||||
color: isDark
|
||||
? AppColors.fgDark
|
||||
: AppColors.fgLight,
|
||||
),
|
||||
style: IconButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppRadius.pill),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 搜索输入框
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 44,
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索日记...',
|
||||
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: isDark
|
||||
? AppColors.mutedDark
|
||||
: AppColors.mutedLight,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: isDark
|
||||
? AppColors.mutedDark
|
||||
: AppColors.mutedLight,
|
||||
),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 18,
|
||||
color: isDark
|
||||
? AppColors.mutedDark
|
||||
: AppColors.mutedLight,
|
||||
),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_clearSearch();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
fillColor: surfaceWarmColor,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppRadius.pill),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
isDense: true,
|
||||
),
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context
|
||||
.read<SearchBloc>()
|
||||
.add(SearchByKeyword(value.trim()));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 取消按钮
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: AppColors.accent,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 6C: 结果分类 Tab =====
|
||||
|
||||
Widget _buildResultTabs(
|
||||
BuildContext context, SearchLoaded state) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Row(
|
||||
children: SearchResultTab.values.map((tab) {
|
||||
final isActive = state.activeTab == tab;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context
|
||||
.read<SearchBloc>()
|
||||
.add(SearchTabChanged(tab));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: isActive
|
||||
? AppColors.accent
|
||||
: Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
tab.label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: isActive
|
||||
? AppColors.accent
|
||||
: (isDark
|
||||
? AppColors.mutedDark
|
||||
: AppColors.mutedLight),
|
||||
fontWeight: isActive
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 主体内容 =====
|
||||
|
||||
Widget _buildBody(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
SearchState state,
|
||||
) {
|
||||
return switch (state) {
|
||||
SearchInitial() => _buildSuggestions(context, theme, colorScheme),
|
||||
SearchLoading() => const Center(child: CircularProgressIndicator()),
|
||||
SearchLoaded(:final results, :final activeMood, :final activeTag) =>
|
||||
_hasActiveFilter(activeMood, activeTag)
|
||||
? _buildResults(context, theme, colorScheme, results)
|
||||
: _buildSuggestions(context, theme, colorScheme),
|
||||
SearchError(:final message) => _buildError(colorScheme, message),
|
||||
SearchInitial() => _buildSuggestions(
|
||||
context, theme, colorScheme, isDark, []),
|
||||
SearchLoading() =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
SearchLoaded(
|
||||
:final results,
|
||||
:final activeMood,
|
||||
:final activeTag,
|
||||
:final activeKeyword,
|
||||
:final activeTab,
|
||||
:final searchHistory,
|
||||
) =>
|
||||
hasActiveFilter(activeMood, activeTag, activeKeyword)
|
||||
? _buildFilteredResults(
|
||||
context,
|
||||
theme,
|
||||
colorScheme,
|
||||
isDark,
|
||||
results,
|
||||
activeKeyword,
|
||||
activeTab,
|
||||
)
|
||||
: _buildSuggestions(
|
||||
context, theme, colorScheme, isDark, searchHistory),
|
||||
SearchError(:final message) =>
|
||||
_buildError(colorScheme, message),
|
||||
};
|
||||
}
|
||||
|
||||
bool _hasActiveFilter(String? mood, String? tag) =>
|
||||
mood != null || tag != null;
|
||||
bool hasActiveFilter(String? mood, String? tag, String? keyword) =>
|
||||
mood != null || tag != null || keyword != null;
|
||||
|
||||
// ===== 6B: 搜索历史 + 热门搜索 =====
|
||||
|
||||
/// 建议区域 — 标签云 + 心情选择
|
||||
Widget _buildSuggestions(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
List<String> searchHistory,
|
||||
) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 搜索历史
|
||||
if (searchHistory.isNotEmpty) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'最近搜索',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _clearSearch,
|
||||
child: Text(
|
||||
'清除',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: AppColors.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: searchHistory.map((keyword) {
|
||||
return ActionChip(
|
||||
label: Text(keyword),
|
||||
onPressed: () {
|
||||
_searchController.text = keyword;
|
||||
context
|
||||
.read<SearchBloc>()
|
||||
.add(SearchByKeyword(keyword));
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// 热门搜索
|
||||
Text(
|
||||
'常用标签',
|
||||
style:
|
||||
theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
'热门搜索',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _recentTags.map((tag) {
|
||||
children: _hotSearches.map((keyword) {
|
||||
return ActionChip(
|
||||
label: Text(tag),
|
||||
label: Text(keyword),
|
||||
onPressed: () {
|
||||
_searchController.text = tag;
|
||||
context.read<SearchBloc>().add(SearchByTag(tag));
|
||||
_searchController.text = keyword;
|
||||
context
|
||||
.read<SearchBloc>()
|
||||
.add(SearchByKeyword(keyword));
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 心情筛选
|
||||
Text(
|
||||
'按心情筛选',
|
||||
style:
|
||||
theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: _moodFilters.map((mood) {
|
||||
final color =
|
||||
AppColors.moodColors[mood.value] ?? colorScheme.primary;
|
||||
children: Mood.values.map((mood) {
|
||||
final color = AppColors.moodColors[mood.value] ??
|
||||
colorScheme.primary;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_searchController.text = _moodLabel(mood);
|
||||
context.read<SearchBloc>().add(SearchByMood(mood));
|
||||
_searchController.text = moodToLabel(mood);
|
||||
context
|
||||
.read<SearchBloc>()
|
||||
.add(SearchByMood(mood));
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -164,11 +400,12 @@ class _SearchPageState extends State<SearchPage> {
|
||||
color: color.withValues(alpha: 0.15),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(_moodEmoji(mood),
|
||||
child: Text(moodToEmoji(mood),
|
||||
style: const TextStyle(fontSize: 24)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(_moodLabel(mood), style: theme.textTheme.labelSmall),
|
||||
Text(moodToLabel(mood),
|
||||
style: theme.textTheme.labelSmall),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -179,12 +416,16 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 搜索结果列表
|
||||
Widget _buildResults(
|
||||
// ===== 按分类 Tab 过滤结果 =====
|
||||
|
||||
Widget _buildFilteredResults(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
List<JournalEntry> results,
|
||||
String? keyword,
|
||||
SearchResultTab activeTab,
|
||||
) {
|
||||
if (results.isEmpty) {
|
||||
return Center(
|
||||
@@ -198,7 +439,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
Text(
|
||||
'没有找到匹配的日记',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
color:
|
||||
colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -211,17 +453,173 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// 根据活跃 Tab 选择展示内容
|
||||
switch (activeTab) {
|
||||
case SearchResultTab.all:
|
||||
case SearchResultTab.journal:
|
||||
return _buildJournalResults(
|
||||
context, theme, colorScheme, isDark, results, keyword);
|
||||
case SearchResultTab.template:
|
||||
return _buildTemplateResults(theme, isDark);
|
||||
case SearchResultTab.tag:
|
||||
return _buildTagResults(theme, isDark, results);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 日记结果列表 =====
|
||||
|
||||
Widget _buildJournalResults(
|
||||
BuildContext context,
|
||||
ThemeData theme,
|
||||
ColorScheme colorScheme,
|
||||
bool isDark,
|
||||
List<JournalEntry> results,
|
||||
String? keyword,
|
||||
) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: results.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = results[index];
|
||||
return _JournalCard(entry: entry);
|
||||
return _JournalCard(
|
||||
entry: entry,
|
||||
keyword: keyword,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 6E: 模板结果(占位) =====
|
||||
|
||||
Widget _buildTemplateResults(ThemeData theme, bool isDark) {
|
||||
// Phase 1 占位 — 模板功能未实现
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.75,
|
||||
),
|
||||
itemCount: 4,
|
||||
itemBuilder: (context, index) {
|
||||
final gradients = [
|
||||
const [AppColors.accent, AppColors.tertiary],
|
||||
const [AppColors.secondary, AppColors.tertiary],
|
||||
const [AppColors.rose, AppColors.accent],
|
||||
const [AppColors.tertiary, AppColors.secondary],
|
||||
];
|
||||
final labels = ['每日心情', '旅行手账', '读书笔记', '日常记录'];
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: gradients[index],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 装饰圆
|
||||
Positioned(
|
||||
right: -10,
|
||||
bottom: -10,
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.12),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
labels[index],
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'即将上线',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 6E: 标签结果 =====
|
||||
|
||||
Widget _buildTagResults(
|
||||
ThemeData theme, bool isDark, List<JournalEntry> results) {
|
||||
// 从搜索结果中提取所有标签及其频次
|
||||
final tagFreq = <String, int>{};
|
||||
for (final entry in results) {
|
||||
for (final tag in entry.tags) {
|
||||
tagFreq[tag] = (tagFreq[tag] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final sortedTags = tagFreq.keys.toList()
|
||||
..sort((a, b) => tagFreq[b]!.compareTo(tagFreq[a]!));
|
||||
|
||||
if (sortedTags.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'没有找到相关标签',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: isDark ? AppColors.mutedDark : AppColors.mutedLight,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: sortedTags.map((tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? AppColors.surfaceDark
|
||||
: AppColors.surfaceLight,
|
||||
borderRadius: BorderRadius.circular(AppRadius.pill),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? AppColors.borderDark
|
||||
: AppColors.borderLight,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'$tag (${tagFreq[tag]})',
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
).padAll(16);
|
||||
}
|
||||
|
||||
/// 错误提示
|
||||
Widget _buildError(ColorScheme colorScheme, String message) {
|
||||
return Center(
|
||||
@@ -253,29 +651,50 @@ class _SearchPageState extends State<SearchPage> {
|
||||
_searchController.clear();
|
||||
context.read<SearchBloc>().add(const SearchClear());
|
||||
}
|
||||
|
||||
String _moodEmoji(Mood mood) => switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
|
||||
String _moodLabel(Mood mood) => switch (mood) {
|
||||
Mood.happy => '开心',
|
||||
Mood.calm => '平静',
|
||||
Mood.sad => '难过',
|
||||
Mood.angry => '生气',
|
||||
Mood.thinking => '思考',
|
||||
};
|
||||
}
|
||||
|
||||
/// 日记卡片 — 在搜索结果中展示单条日记摘要
|
||||
// ===== 6D: 关键词高亮辅助函数 =====
|
||||
|
||||
/// 将文本中的关键词部分用高亮样式包裹
|
||||
Widget _highlightText(String text, String? keyword) {
|
||||
if (keyword == null || keyword.isEmpty || !text.toLowerCase().contains(keyword.toLowerCase())) {
|
||||
return Text(text);
|
||||
}
|
||||
|
||||
final lowerText = text.toLowerCase();
|
||||
final lowerKeyword = keyword.toLowerCase();
|
||||
final spans = <TextSpan>[];
|
||||
int start = 0;
|
||||
|
||||
while (start < text.length) {
|
||||
final index = lowerText.indexOf(lowerKeyword, start);
|
||||
if (index == -1) {
|
||||
spans.add(TextSpan(text: text.substring(start)));
|
||||
break;
|
||||
}
|
||||
if (index > start) {
|
||||
spans.add(TextSpan(text: text.substring(start, index)));
|
||||
}
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(index, index + keyword.length),
|
||||
style: const TextStyle(
|
||||
color: AppColors.accent,
|
||||
backgroundColor: AppColors.tertiarySoftLight,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
));
|
||||
start = index + keyword.length;
|
||||
}
|
||||
|
||||
return RichText(text: TextSpan(style: const TextStyle(), children: spans));
|
||||
}
|
||||
|
||||
/// 日记卡片 — 在搜索结果中展示单条日记摘要(支持关键词高亮)
|
||||
class _JournalCard extends StatelessWidget {
|
||||
final JournalEntry entry;
|
||||
final String? keyword;
|
||||
|
||||
const _JournalCard({required this.entry});
|
||||
const _JournalCard({required this.entry, this.keyword});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -293,30 +712,23 @@ class _JournalCard extends StatelessWidget {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
// TODO: 导航到日记详情页
|
||||
context.push('/editor?id=${entry.id}');
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 第一行:心情 emoji + 标题 + 日期
|
||||
// 第一行:心情 emoji + 标题(高亮)+ 日期
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
_moodEmoji(entry.mood),
|
||||
moodToEmoji(entry.mood),
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: _highlightText(entry.title, keyword),
|
||||
),
|
||||
Text(
|
||||
DateFormat('MM/dd').format(entry.date),
|
||||
@@ -360,11 +772,12 @@ class _JournalCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _moodEmoji(Mood mood) => switch (mood) {
|
||||
Mood.happy => '😊',
|
||||
Mood.calm => '😌',
|
||||
Mood.sad => '😢',
|
||||
Mood.angry => '😠',
|
||||
Mood.thinking => '🤔',
|
||||
};
|
||||
}
|
||||
|
||||
/// Padding 扩展 — 简化 Wrap 的 padding
|
||||
extension _PadAll on Widget {
|
||||
Widget padAll(double value) => Padding(
|
||||
padding: EdgeInsets.all(value),
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user