后端 (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
217 lines
6.8 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|