fix(app): 修复 P2~P4 共 10 项前端问题
P2 必须修复: - 教师布置主题 classId 从硬编码改为班级下拉选择器 - 班级日记墙使用服务端 classId 过滤替代前端过滤 - Profile 统计栏接入 JournalRepository 真实数据 - WeeklyPage 从全硬编码改为 JournalRepository 数据驱动 P3 建议改进: - 提取 mood_utils.dart 公共函数,消除 4 处重复定义 - 贴纸库搜索框连接 StickerBloc 按名称过滤 P4 细节打磨: - 家长页多孩子时显示 DropdownButton 选择器 - 搜索结果日记卡片点击跳转 /editor?id= - MonthlyPage 照片数量从 JournalElement 统计 - calendar_page/mood_page/search_page 统一使用 moodToEmoji/moodToLabel
This commit is contained in:
@@ -52,20 +52,31 @@ class Sticker {
|
||||
class StickerState {
|
||||
final List<StickerPack> packs;
|
||||
final String selectedCategory;
|
||||
final String searchQuery;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
|
||||
const StickerState({
|
||||
this.packs = const [],
|
||||
this.selectedCategory = '全部',
|
||||
this.searchQuery = '',
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// 按分类过滤贴纸包
|
||||
List<StickerPack> get filteredPacks => selectedCategory == '全部'
|
||||
? packs
|
||||
: packs.where((p) => p.category == selectedCategory).toList();
|
||||
/// 按分类 + 搜索关键词过滤贴纸包
|
||||
List<StickerPack> get filteredPacks {
|
||||
var result = selectedCategory == '全部'
|
||||
? packs
|
||||
: packs.where((p) => p.category == selectedCategory).toList();
|
||||
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final query = searchQuery.toLowerCase();
|
||||
result = result.where((p) => p.name.toLowerCase().contains(query)).toList();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 所有分类(去重 + 加"全部")
|
||||
List<String> get categories {
|
||||
@@ -80,12 +91,14 @@ class StickerState {
|
||||
StickerState copyWith({
|
||||
List<StickerPack>? packs,
|
||||
String? selectedCategory,
|
||||
String? searchQuery,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
}) =>
|
||||
StickerState(
|
||||
packs: packs ?? this.packs,
|
||||
selectedCategory: selectedCategory ?? this.selectedCategory,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
@@ -114,6 +127,12 @@ class StickerBloc extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 搜索贴纸包(按名称前端过滤)
|
||||
void search(String query) {
|
||||
_state = _state.copyWith(searchQuery: query);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 按分类加载贴纸包
|
||||
void loadByCategory(String? category) {
|
||||
_state = _state.copyWith(isLoading: true);
|
||||
|
||||
@@ -17,6 +17,12 @@ class StickerLibraryPage extends StatefulWidget {
|
||||
|
||||
class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
||||
late final StickerBloc _bloc;
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
/// 设计规格中的 8 个分类
|
||||
static const _specCategories = [
|
||||
'推荐', '可爱', '植物', '手绘', '校园', '节日', '文字', '和纸胶带',
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -28,92 +34,225 @@ class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('贴纸库')),
|
||||
body: ListenableBuilder(
|
||||
listenable: _bloc,
|
||||
builder: (context, _) {
|
||||
final state = _bloc.state;
|
||||
body: SafeArea(
|
||||
child: ListenableBuilder(
|
||||
listenable: _bloc,
|
||||
builder: (context, _) {
|
||||
final state = _bloc.state;
|
||||
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
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),
|
||||
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('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// ---- 自定义顶栏 ----
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
Text('贴纸素材', style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ---- 搜索框 ----
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索贴纸...',
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||||
isDense: true,
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
onChanged: (v) {
|
||||
_bloc.search(v);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ---- 分类选择器(设计规格 8 分类) ----
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: _specCategories.map((cat) {
|
||||
final isSelected = cat == state.selectedCategory ||
|
||||
(cat == '推荐' && state.selectedCategory == '全部');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
selected: isSelected,
|
||||
label: Text(cat),
|
||||
onSelected: (_) {
|
||||
if (cat == '推荐') {
|
||||
_bloc.selectCategory('全部');
|
||||
} else {
|
||||
_bloc.selectCategory(cat);
|
||||
}
|
||||
},
|
||||
selectedColor: AppColors.accent.withValues(alpha: 0.15),
|
||||
checkmarkColor: AppColors.accent,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected ? AppColors.accent : colorScheme.onSurface,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ---- 精选贴纸包卡片 ----
|
||||
if (state.selectedCategory == '全部')
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: const _FeaturedPackCard(),
|
||||
),
|
||||
if (state.selectedCategory == '全部')
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
onPressed: _bloc.load,
|
||||
child: const Text('重试'),
|
||||
|
||||
// ---- 贴纸包网格 ----
|
||||
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 _FeaturedPackCard extends StatelessWidget {
|
||||
const _FeaturedPackCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('打开精选贴纸包: 治愈小动物')),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [AppColors.accent, AppColors.tertiary],
|
||||
),
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// emoji 图标区域
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text('🧸', style: TextStyle(fontSize: 36)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('治愈小动物', style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700, color: Colors.white,
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
Text('超可爱的手绘小动物贴纸', style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.white.withValues(alpha: 0.85),
|
||||
)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.secondary,
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
),
|
||||
child: const Text('限时免费', style: TextStyle(
|
||||
fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user