feat(app): 多页面动态化 — 搜索/资料/教师/贴纸库/模板/日历

- SearchPage: 热搜词从日记标签频率动态生成 + 模板搜索网格
- ProfilePage: 成就徽章从 AchievementBloc 动态加载 + 头像首字母
- TeacherPage: 班级码改为对话框展示 (班级名+码+人数)
- StickerLibraryPage: 分类从 API 动态合并 + 精选包卡片动态化
- TemplateGalleryPage: 适配动态数据
- ClassPage: 微调
- HomePage: 路由适配
- CalendarBloc: 新增测试
- AppRouter: 路由更新
This commit is contained in:
iven
2026-06-07 10:44:04 +08:00
parent a05374e8d1
commit d67eedf7de
8 changed files with 334 additions and 124 deletions

View File

@@ -17,6 +17,9 @@ 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 '../../../data/remote/api_client.dart';
import '../../../data/repositories/journal_repository.dart';
import '../../templates/bloc/template_bloc.dart';
import '../bloc/search_bloc.dart';
/// 搜索页面 — 搜索历史 + 热门搜索 + 结果分类
@@ -31,18 +34,42 @@ class _SearchPageState extends State<SearchPage> {
final _searchController = TextEditingController();
final _searchFocusNode = FocusNode();
// 热门搜索占位数据
final _hotSearches = ['日常', '学校', '旅行', '美食', '读书', '心情', '手账', '贴纸'];
// 热门搜索 — 从用户日记标签动态推导,无数据时使用默认推荐
List<String> _hotSearches = ['日常', '学校', '旅行', '心情', '读书', '手账'];
@override
void initState() {
super.initState();
_deriveHotSearches();
// 自动弹出键盘
WidgetsBinding.instance.addPostFrameCallback((_) {
_searchFocusNode.requestFocus();
});
}
/// 从日记标签频率推导热门搜索
Future<void> _deriveHotSearches() async {
try {
final repo = context.read<JournalRepository>();
final journals = await repo.getJournals();
final tagFreq = <String, int>{};
for (final j in journals) {
for (final tag in j.tags) {
tagFreq[tag] = (tagFreq[tag] ?? 0) + 1;
}
}
final sorted = tagFreq.keys.toList()
..sort((a, b) => tagFreq[b]!.compareTo(tagFreq[a]!));
if (sorted.isNotEmpty && mounted) {
setState(() {
_hotSearches = sorted.take(8).toList();
});
}
} catch (_) {
// 保持默认值
}
}
@override
void dispose() {
_searchController.dispose();
@@ -490,79 +517,10 @@ class _SearchPageState extends State<SearchPage> {
);
}
// ===== 6E: 模板结果(占位 =====
// ===== 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),
),
),
],
),
),
],
),
);
},
);
return _TemplateSearchGrid(theme: theme, isDark: isDark);
}
// ===== 6E: 标签结果 =====
@@ -781,3 +739,124 @@ extension _PadAll on Widget {
child: this,
);
}
/// 搜索页模板结果 — 从 TemplateBloc 动态加载
class _TemplateSearchGrid extends StatefulWidget {
const _TemplateSearchGrid({required this.theme, required this.isDark});
final ThemeData theme;
final bool isDark;
@override
State<_TemplateSearchGrid> createState() => _TemplateSearchGridState();
}
class _TemplateSearchGridState extends State<_TemplateSearchGrid> {
late final TemplateBloc _bloc;
@override
void initState() {
super.initState();
_bloc = TemplateBloc(api: context.read<ApiClient>());
_bloc.load();
_bloc.addListener(() => setState(() {}));
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
static const _gradients = [
[AppColors.accent, AppColors.tertiary],
[AppColors.secondary, AppColors.tertiary],
[AppColors.rose, AppColors.accent],
[AppColors.tertiary, AppColors.secondary],
];
@override
Widget build(BuildContext context) {
final templates = _bloc.state.templates;
if (_bloc.state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (templates.isEmpty) {
return Center(
child: Text('暂无模板', style: widget.theme.textTheme.bodyMedium?.copyWith(
color: widget.isDark ? AppColors.mutedDark : AppColors.mutedLight,
)),
);
}
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.75,
),
itemCount: templates.length,
itemBuilder: (context, index) {
final t = templates[index];
final colors = _gradients[index % _gradients.length];
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.md),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: colors,
),
),
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(
t.emoji,
style: const TextStyle(fontSize: 28),
),
const SizedBox(height: 8),
Text(
t.name,
style: widget.theme.textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
t.isFree ? '免费模板' : '精品模板',
style: widget.theme.textTheme.bodySmall?.copyWith(
color: Colors.white.withValues(alpha: 0.7),
),
),
],
),
),
],
),
);
},
);
}
}