Files
nj/app/lib/features/stickers/views/sticker_library_page.dart
iven 7e3597dc77 feat(diary): B4+B5+B6 后端服务 + F5/F6/F7 前端模块
后端 (erp-diary):
- B4: CommentService 班级成员验证 + 删除评语 + SSE 通知推送
- B4: NotificationService 评语/主题/成就三类通知事件
- B5: StickerService 贴纸包列表 + 贴纸查询 + 模板管理
- B5: AchievementService 成就列表 + 解锁 + SSE 通知
- B6: MoodStatsService 心情统计 + 连续天数
- B6: ContentSafetyService 敏感词过滤框架
- SSE handler 增加 diary.notification.* 事件处理
- 新增 14 个 API 端点 + diary.comment.delete 权限

前端 (Flutter):
- F5: CalendarBloc + 月视图日历 + 日记列表
- F6: MoodBloc + fl_chart 心情饼图 + 统计卡片 + 连续天数
- F7: 贴纸库分类浏览 + 模板画廊
- 首页改为日记流 + 心情快速选择
- 成就页改为徽章收集展示

验证: cargo check ✓ cargo test 17/17 ✓ flutter analyze 0 error
2026-06-01 09:32:09 +08:00

217 lines
6.8 KiB
Dart

// 贴纸库页面 — 贴纸包浏览 + 贴纸网格
import 'package:flutter/material.dart';
import 'package:nuanji_app/core/theme/app_colors.dart';
/// 贴纸包数据模型
class StickerPack {
final String id;
final String name;
final String? coverEmoji;
final int stickerCount;
final bool isFree;
final String? category;
const StickerPack({
required this.id,
required this.name,
this.coverEmoji,
this.stickerCount = 0,
this.isFree = true,
this.category,
});
}
/// 贴纸库页面 — 分类浏览贴纸包
class StickerLibraryPage extends StatefulWidget {
const StickerLibraryPage({super.key});
@override
State<StickerLibraryPage> createState() => _StickerLibraryPageState();
}
class _StickerLibraryPageState extends State<StickerLibraryPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
// Phase 1 占位数据
final _categories = ['全部', '动物', '食物', '自然', '节日', '表情'];
final _packs = const [
StickerPack(id: '1', name: '可爱猫咪', coverEmoji: '🐱', stickerCount: 24, isFree: true, category: '动物'),
StickerPack(id: '2', name: '小兔子系列', coverEmoji: '🐰', stickerCount: 20, isFree: true, category: '动物'),
StickerPack(id: '3', name: '甜品派对', coverEmoji: '🍰', stickerCount: 18, isFree: true, category: '食物'),
StickerPack(id: '4', name: '花朵合集', coverEmoji: '🌸', stickerCount: 22, isFree: true, category: '自然'),
StickerPack(id: '5', name: '夏日清凉', coverEmoji: '🍉', stickerCount: 16, isFree: true, category: '食物'),
StickerPack(id: '6', name: '星空物语', coverEmoji: '', stickerCount: 20, isFree: false, category: '自然'),
StickerPack(id: '7', name: '开心表情', coverEmoji: '😄', stickerCount: 30, isFree: true, category: '表情'),
StickerPack(id: '8', name: '新年快乐', coverEmoji: '🎉', stickerCount: 15, isFree: false, category: '节日'),
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _categories.length, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('贴纸库'),
bottom: TabBar(
controller: _tabController,
isScrollable: true,
tabAlignment: TabAlignment.start,
tabs: _categories.map((c) => Tab(text: c)).toList(),
),
),
body: TabBarView(
controller: _tabController,
children: _categories.map((category) {
final filtered = category == '全部'
? _packs
: _packs.where((p) => p.category == category).toList();
return _StickerPackGrid(packs: filtered, colorScheme: colorScheme);
}).toList(),
),
);
}
}
/// 贴纸包网格
class _StickerPackGrid extends StatelessWidget {
const _StickerPackGrid({
required this.packs,
required this.colorScheme,
});
final List<StickerPack> packs;
final ColorScheme colorScheme;
@override
Widget build(BuildContext context) {
if (packs.isEmpty) {
return const Center(child: Text('暂无贴纸包'));
}
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.85,
),
itemCount: packs.length,
itemBuilder: (context, index) {
final pack = packs[index];
return _StickerPackCard(pack: pack, 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: () {
// Phase 1: 展示贴纸包详情页(待实现)
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.coverEmoji ?? '🎨',
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,
),
),
),
],
],
),
],
),
),
),
);
}
}