Files
nj/app/lib/features/stickers/views/sticker_library_page.dart
iven 8331db63ba feat(app): 设置页 UI + Mood/成就/贴纸 BLoC 接入 API + B7 测试扩展
前端改动:
- 新建设置页面 (主题切换/关于/隐私政策/用户协议/儿童隐私保护)
- SettingsBloc 注册到 MultiRepositoryProvider 全局可访问
- MoodBloc 修复编译错误 + 接入 /diary/stats/mood API
- MoodPage 添加错误状态展示和重试按钮
- AchievementBloc + 页面改造接入 /diary/achievements API
- StickerBloc + 页面改造接入 /diary/sticker-packs API
- TemplateBloc + 页面改造接入 /diary/templates API
- ProfilePage 设置入口改为跳转 /settings
- 添加 /settings 路由

后端改动:
- 扩展 mood_stats_service 测试 (连续天数算法/心情计数/边界场景)
- 新增 class_service 测试 (班级码生成/唯一性/错误映射)
- 新增 achievement_service 测试 (DTO 结构/序列化/map 构建)
- 新增 sticker_service 测试 (DTO 序列化/错误处理)
- 扩展 dto.rs 测试 (achievement/mood_stats/sticker/template/notification)
- 清理 2 个 unused import warning

验证:
- cargo check 0 error 0 warning
- flutter analyze 0 error
2026-06-01 11:19:43 +08:00

214 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 贴纸库页面 — 贴纸包浏览 + 分类 Tab
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nuanji_app/core/theme/app_colors.dart';
import 'package:nuanji_app/data/remote/api_client.dart';
import '../bloc/sticker_bloc.dart';
/// 贴纸库页面 — 分类浏览贴纸包
class StickerLibraryPage extends StatefulWidget {
const StickerLibraryPage({super.key});
@override
State<StickerLibraryPage> createState() => _StickerLibraryPageState();
}
class _StickerLibraryPageState extends State<StickerLibraryPage> {
late final StickerBloc _bloc;
@override
void initState() {
super.initState();
_bloc = StickerBloc(api: context.read<ApiClient>());
_bloc.load();
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(title: const Text('贴纸库')),
body: ListenableBuilder(
listenable: _bloc,
builder: (context, _) {
final state = _bloc.state;
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: colorScheme.error),
const SizedBox(height: 16),
FilledButton.tonal(
onPressed: _bloc.load,
child: const Text('重试'),
),
],
),
);
}
final categories = state.categories;
return Column(
children: [
// 分类选择器(横向滚动 Chips
SizedBox(
height: 48,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: categories.map((cat) {
final isSelected = cat == state.selectedCategory;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: isSelected,
label: Text(cat),
onSelected: (_) => _bloc.selectCategory(cat),
selectedColor: colorScheme.primaryContainer,
checkmarkColor: colorScheme.primary,
),
);
}).toList(),
),
),
const SizedBox(height: 8),
// 贴纸包网格
Expanded(
child: state.filteredPacks.isEmpty
? const Center(child: Text('暂无贴纸包'))
: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.85,
),
itemCount: state.filteredPacks.length,
itemBuilder: (context, index) {
return _StickerPackCard(
pack: state.filteredPacks[index],
colorScheme: colorScheme,
);
},
),
),
],
);
},
),
);
}
}
/// 贴纸包卡片
class _StickerPackCard extends StatelessWidget {
const _StickerPackCard({
required this.pack,
required this.colorScheme,
});
final StickerPack pack;
final ColorScheme colorScheme;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: colorScheme.outlineVariant),
),
child: InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('打开贴纸包: ${pack.name}')),
);
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 贴纸包封面图标
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
),
alignment: Alignment.center,
child: Text(
pack.coverImageUrl != null ? '🎨' : pack.displayCover,
style: const TextStyle(fontSize: 32),
),
),
const SizedBox(height: 12),
// 名称
Text(
pack.name,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 数量和价格标签
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${pack.stickerCount}',
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurface.withValues(alpha: 0.5),
),
),
if (!pack.isFree) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppColors.accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'积分',
style: theme.textTheme.labelSmall?.copyWith(
color: AppColors.accent,
),
),
),
],
],
),
],
),
),
),
);
}
}