Files
nj/app/lib/features/stickers/bloc/sticker_bloc.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

182 lines
4.6 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.
// 贴纸 BLoC — 通过 API 加载贴纸包和贴纸数据
import 'package:flutter/material.dart';
import 'package:nuanji_app/data/remote/api_client.dart';
// ===== 模型 =====
/// 贴纸包
class StickerPack {
final String id;
final String name;
final String? description;
final String? coverImageUrl;
final int stickerCount;
final bool isFree;
final String? category;
const StickerPack({
required this.id,
required this.name,
this.description,
this.coverImageUrl,
this.stickerCount = 0,
this.isFree = true,
this.category,
});
/// 兼容旧 UI 的 emoji 封面(优先用 coverImageUrl否则用默认
String get displayCover => coverImageUrl ?? '🎨';
}
/// 单张贴纸
class Sticker {
final String id;
final String packId;
final String name;
final String imageUrl;
final String? category;
const Sticker({
required this.id,
required this.packId,
required this.name,
required this.imageUrl,
this.category,
});
}
// ===== State =====
/// 贴纸页面状态
class StickerState {
final List<StickerPack> packs;
final String selectedCategory;
final bool isLoading;
final String? errorMessage;
const StickerState({
this.packs = const [],
this.selectedCategory = '全部',
this.isLoading = false,
this.errorMessage,
});
/// 按分类过滤贴纸包
List<StickerPack> get filteredPacks => selectedCategory == '全部'
? packs
: packs.where((p) => p.category == selectedCategory).toList();
/// 所有分类(去重 + 加"全部"
List<String> get categories {
final cats = packs
.map((p) => p.category)
.whereType<String>()
.toSet()
.toList();
return ['全部', ...cats];
}
StickerState copyWith({
List<StickerPack>? packs,
String? selectedCategory,
bool? isLoading,
String? errorMessage,
}) =>
StickerState(
packs: packs ?? this.packs,
selectedCategory: selectedCategory ?? this.selectedCategory,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
// ===== BLoC =====
/// 贴纸 BLoC — ChangeNotifier 模式
class StickerBloc extends ChangeNotifier {
final ApiClient _api;
StickerState _state = const StickerState();
StickerState get state => _state;
StickerBloc({required ApiClient api}) : _api = api;
/// 加载贴纸包列表
void load() {
_state = _state.copyWith(isLoading: true);
notifyListeners();
_fetchPacks();
}
/// 选择分类
void selectCategory(String category) {
_state = _state.copyWith(selectedCategory: category);
notifyListeners();
}
/// 按分类加载贴纸包
void loadByCategory(String? category) {
_state = _state.copyWith(isLoading: true);
notifyListeners();
_fetchPacks(category: category);
}
Future<void> _fetchPacks({String? category}) async {
try {
final queryParams = category != null && category != '全部'
? {'category': category}
: null;
final response = await _api.get(
'/diary/sticker-packs',
queryParams: queryParams,
);
final body = response.data as Map<String, dynamic>;
final list = body['data'] as List? ?? [];
final packs = list.map((item) {
final m = item as Map<String, dynamic>;
return StickerPack(
id: m['id'] as String,
name: m['name'] as String,
description: m['description'] as String?,
coverImageUrl: m['cover_image_url'] as String?,
stickerCount: (m['sticker_count'] as num?)?.toInt() ?? 0,
isFree: m['is_free'] as bool? ?? true,
category: m['category'] as String?,
);
}).toList();
_state = _state.copyWith(isLoading: false, packs: packs);
} catch (e) {
_state = _state.copyWith(
isLoading: false,
errorMessage: '加载贴纸包失败',
);
}
notifyListeners();
}
/// 获取贴纸包内的贴纸列表
Future<List<Sticker>> fetchStickersInPack(String packId) async {
try {
final response = await _api.get('/diary/sticker-packs/$packId/stickers');
final body = response.data as Map<String, dynamic>;
final list = body['data'] as List? ?? [];
return list.map((item) {
final m = item as Map<String, dynamic>;
return Sticker(
id: m['id'] as String,
packId: m['pack_id'] as String,
name: m['name'] as String,
imageUrl: m['image_url'] as String,
category: m['category'] as String?,
);
}).toList();
} catch (e) {
return [];
}
}
}