feat(app): 多页面动态化 — 搜索/资料/教师/贴纸库/模板/日历
- SearchPage: 热搜词从日记标签频率动态生成 + 模板搜索网格 - ProfilePage: 成就徽章从 AchievementBloc 动态加载 + 头像首字母 - TeacherPage: 班级码改为对话框展示 (班级名+码+人数) - StickerLibraryPage: 分类从 API 动态合并 + 精选包卡片动态化 - TemplateGalleryPage: 适配动态数据 - ClassPage: 微调 - HomePage: 路由适配 - CalendarBloc: 新增测试 - AppRouter: 路由更新
This commit is contained in:
@@ -42,6 +42,7 @@ import '../../features/templates/views/template_gallery_page.dart';
|
|||||||
import '../../features/settings/views/settings_page.dart';
|
import '../../features/settings/views/settings_page.dart';
|
||||||
import '../../features/auth/bloc/auth_bloc.dart';
|
import '../../features/auth/bloc/auth_bloc.dart';
|
||||||
import '../../features/search/bloc/search_bloc.dart';
|
import '../../features/search/bloc/search_bloc.dart';
|
||||||
|
import '../../features/discover/bloc/discover_bloc.dart';
|
||||||
import '../../data/repositories/journal_repository.dart';
|
import '../../data/repositories/journal_repository.dart';
|
||||||
import '../../data/remote/api_client.dart';
|
import '../../data/remote/api_client.dart';
|
||||||
|
|
||||||
@@ -168,7 +169,13 @@ GoRouter createAppRouter(AuthBloc authBloc) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/discover',
|
path: '/discover',
|
||||||
name: 'discover',
|
name: 'discover',
|
||||||
builder: (context, state) => const DiscoverPage(),
|
builder: (context, state) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => DiscoverBloc(api: context.read<ApiClient>())
|
||||||
|
..add(const DiscoverLoadData()),
|
||||||
|
child: const DiscoverPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
// 个人中心
|
// 个人中心
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ class _DiaryWallCard extends StatelessWidget {
|
|||||||
radius: 16,
|
radius: 16,
|
||||||
backgroundColor: AppColors.rose.withValues(alpha: 0.2),
|
backgroundColor: AppColors.rose.withValues(alpha: 0.2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'同',
|
journal.title.isNotEmpty ? journal.title[0] : '日',
|
||||||
style: theme.textTheme.labelMedium?.copyWith(color: AppColors.rose),
|
style: theme.textTheme.labelMedium?.copyWith(color: AppColors.rose),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ class _HomeView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_GreetingHeader(
|
_GreetingHeader(
|
||||||
greeting: greeting,
|
greeting: greeting,
|
||||||
username: '小暖',
|
username: context.select<AuthBloc, String>((bloc) {
|
||||||
|
final s = bloc.state;
|
||||||
|
return s is Authenticated ? s.user.displayLabel : '同学';
|
||||||
|
}),
|
||||||
dateText: dateText,
|
dateText: dateText,
|
||||||
onSearchTap: () => context.push('/search'),
|
onSearchTap: () => context.push('/search'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:nuanji_app/features/auth/bloc/auth_bloc.dart';
|
|||||||
import 'package:nuanji_app/features/profile/bloc/settings_bloc.dart';
|
import 'package:nuanji_app/features/profile/bloc/settings_bloc.dart';
|
||||||
import 'package:nuanji_app/data/models/user.dart';
|
import 'package:nuanji_app/data/models/user.dart';
|
||||||
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
import 'package:nuanji_app/data/repositories/journal_repository.dart';
|
||||||
|
import 'package:nuanji_app/features/achievement/bloc/achievement_bloc.dart';
|
||||||
|
import 'package:nuanji_app/data/remote/api_client.dart';
|
||||||
|
|
||||||
/// 个人中心页面
|
/// 个人中心页面
|
||||||
class ProfilePage extends StatelessWidget {
|
class ProfilePage extends StatelessWidget {
|
||||||
@@ -60,7 +62,10 @@ class ProfilePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text('😊', style: TextStyle(fontSize: 36)),
|
child: Text(
|
||||||
|
displayName.isNotEmpty ? displayName[0] : '😊',
|
||||||
|
style: const TextStyle(fontSize: 36, color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// 用户名
|
// 用户名
|
||||||
@@ -91,7 +96,7 @@ class ProfilePage extends StatelessWidget {
|
|||||||
_LiveStatsBar(borderSoft: borderSoft, colorScheme: colorScheme),
|
_LiveStatsBar(borderSoft: borderSoft, colorScheme: colorScheme),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// ---- 成就徽章 ----
|
// ---- 成就徽章(动态加载) ----
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Text('成就徽章', style: theme.textTheme.titleMedium?.copyWith(
|
child: Text('成就徽章', style: theme.textTheme.titleMedium?.copyWith(
|
||||||
@@ -101,21 +106,11 @@ class ProfilePage extends StatelessWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 100,
|
height: 100,
|
||||||
child: ListView(
|
child: _AchievementBadges(
|
||||||
scrollDirection: Axis.horizontal,
|
accentSoft: accentSoft,
|
||||||
children: [
|
tertiarySoft: tertiarySoft,
|
||||||
_BadgeItem(emoji: '📝', name: '初出茅庐', bgColor: accentSoft, locked: false),
|
roseSoft: roseSoft,
|
||||||
const SizedBox(width: 12),
|
secondarySoft: secondarySoft,
|
||||||
_BadgeItem(emoji: '🔥', name: '七日连续', bgColor: tertiarySoft, locked: false),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_BadgeItem(emoji: '🎨', name: '装饰达人', bgColor: roseSoft, locked: false),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_BadgeItem(emoji: '🌟', name: '人气之星', bgColor: secondarySoft, locked: true),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_BadgeItem(emoji: '🏆', name: '写作高手', bgColor: accentSoft, locked: true),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
_BadgeItem(emoji: '💎', name: '全能王', bgColor: tertiarySoft, locked: true),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
@@ -430,9 +425,79 @@ class _LiveStatsBarState extends State<_LiveStatsBar> {
|
|||||||
VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft),
|
VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft),
|
||||||
_StatItem(label: '本月日记', value: '$_monthCount', valueColor: widget.colorScheme.onSurface),
|
_StatItem(label: '本月日记', value: '$_monthCount', valueColor: widget.colorScheme.onSurface),
|
||||||
VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft),
|
VerticalDivider(width: 1, indent: 4, endIndent: 4, color: widget.borderSoft),
|
||||||
_StatItem(label: '贴纸数', value: '--', valueColor: widget.colorScheme.onSurface),
|
_StatItem(label: '贴纸数', value: _totalCount > 0 ? '$_totalCount' : '0', valueColor: widget.colorScheme.onSurface),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 成就徽章动态组件 — 从 AchievementBloc 加载真实数据
|
||||||
|
class _AchievementBadges extends StatefulWidget {
|
||||||
|
const _AchievementBadges({
|
||||||
|
required this.accentSoft,
|
||||||
|
required this.tertiarySoft,
|
||||||
|
required this.roseSoft,
|
||||||
|
required this.secondarySoft,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color accentSoft;
|
||||||
|
final Color tertiarySoft;
|
||||||
|
final Color roseSoft;
|
||||||
|
final Color secondarySoft;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AchievementBadges> createState() => _AchievementBadgesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AchievementBadgesState extends State<_AchievementBadges> {
|
||||||
|
late final AchievementBloc _bloc;
|
||||||
|
List<Achievement> _achievements = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_bloc = AchievementBloc(api: context.read<ApiClient>());
|
||||||
|
_bloc.load();
|
||||||
|
_bloc.addListener(_onUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdate() {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_achievements = _bloc.state.achievements;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bloc.removeListener(_onUpdate);
|
||||||
|
_bloc.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_achievements.isEmpty) {
|
||||||
|
return const Center(child: Text('暂无成就', style: TextStyle(fontSize: 13)));
|
||||||
|
}
|
||||||
|
|
||||||
|
final bgColors = [widget.accentSoft, widget.tertiarySoft, widget.roseSoft, widget.secondarySoft];
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _achievements.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final a = _achievements[index];
|
||||||
|
return _BadgeItem(
|
||||||
|
emoji: a.icon ?? '🏆',
|
||||||
|
name: a.name,
|
||||||
|
bgColor: bgColors[index % bgColors.length],
|
||||||
|
locked: !a.isUnlocked,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import '../../../core/theme/app_colors.dart';
|
|||||||
import '../../../core/theme/app_radius.dart';
|
import '../../../core/theme/app_radius.dart';
|
||||||
import '../../../core/utils/mood_utils.dart';
|
import '../../../core/utils/mood_utils.dart';
|
||||||
import '../../../data/models/journal_entry.dart';
|
import '../../../data/models/journal_entry.dart';
|
||||||
|
import '../../../data/remote/api_client.dart';
|
||||||
|
import '../../../data/repositories/journal_repository.dart';
|
||||||
|
import '../../templates/bloc/template_bloc.dart';
|
||||||
import '../bloc/search_bloc.dart';
|
import '../bloc/search_bloc.dart';
|
||||||
|
|
||||||
/// 搜索页面 — 搜索历史 + 热门搜索 + 结果分类
|
/// 搜索页面 — 搜索历史 + 热门搜索 + 结果分类
|
||||||
@@ -31,18 +34,42 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
final _searchFocusNode = FocusNode();
|
final _searchFocusNode = FocusNode();
|
||||||
|
|
||||||
// 热门搜索占位数据
|
// 热门搜索 — 从用户日记标签动态推导,无数据时使用默认推荐
|
||||||
final _hotSearches = ['日常', '学校', '旅行', '美食', '读书', '心情', '手账', '贴纸'];
|
List<String> _hotSearches = ['日常', '学校', '旅行', '心情', '读书', '手账'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_deriveHotSearches();
|
||||||
// 自动弹出键盘
|
// 自动弹出键盘
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_searchFocusNode.requestFocus();
|
_searchFocusNode.requestFocus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从日记标签频率推导热门搜索
|
||||||
|
Future<void> _deriveHotSearches() async {
|
||||||
|
try {
|
||||||
|
final repo = context.read<JournalRepository>();
|
||||||
|
final journals = await repo.getJournals();
|
||||||
|
final tagFreq = <String, int>{};
|
||||||
|
for (final j in journals) {
|
||||||
|
for (final tag in j.tags) {
|
||||||
|
tagFreq[tag] = (tagFreq[tag] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final sorted = tagFreq.keys.toList()
|
||||||
|
..sort((a, b) => tagFreq[b]!.compareTo(tagFreq[a]!));
|
||||||
|
if (sorted.isNotEmpty && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_hotSearches = sorted.take(8).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 保持默认值
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
@@ -490,79 +517,10 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 6E: 模板结果(占位) =====
|
// ===== 6E: 模板结果(动态加载) =====
|
||||||
|
|
||||||
Widget _buildTemplateResults(ThemeData theme, bool isDark) {
|
Widget _buildTemplateResults(ThemeData theme, bool isDark) {
|
||||||
// Phase 1 占位 — 模板功能未实现
|
return _TemplateSearchGrid(theme: theme, isDark: isDark);
|
||||||
return GridView.builder(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: 2,
|
|
||||||
mainAxisSpacing: 12,
|
|
||||||
crossAxisSpacing: 12,
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
),
|
|
||||||
itemCount: 4,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final gradients = [
|
|
||||||
const [AppColors.accent, AppColors.tertiary],
|
|
||||||
const [AppColors.secondary, AppColors.tertiary],
|
|
||||||
const [AppColors.rose, AppColors.accent],
|
|
||||||
const [AppColors.tertiary, AppColors.secondary],
|
|
||||||
];
|
|
||||||
final labels = ['每日心情', '旅行手账', '读书笔记', '日常记录'];
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
colors: gradients[index],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// 装饰圆
|
|
||||||
Positioned(
|
|
||||||
right: -10,
|
|
||||||
bottom: -10,
|
|
||||||
child: Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.white.withValues(alpha: 0.12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
labels[index],
|
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'即将上线',
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: Colors.white.withValues(alpha: 0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 6E: 标签结果 =====
|
// ===== 6E: 标签结果 =====
|
||||||
@@ -781,3 +739,124 @@ extension _PadAll on Widget {
|
|||||||
child: this,
|
child: this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 搜索页模板结果 — 从 TemplateBloc 动态加载
|
||||||
|
class _TemplateSearchGrid extends StatefulWidget {
|
||||||
|
const _TemplateSearchGrid({required this.theme, required this.isDark});
|
||||||
|
final ThemeData theme;
|
||||||
|
final bool isDark;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TemplateSearchGrid> createState() => _TemplateSearchGridState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TemplateSearchGridState extends State<_TemplateSearchGrid> {
|
||||||
|
late final TemplateBloc _bloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_bloc = TemplateBloc(api: context.read<ApiClient>());
|
||||||
|
_bloc.load();
|
||||||
|
_bloc.addListener(() => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bloc.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const _gradients = [
|
||||||
|
[AppColors.accent, AppColors.tertiary],
|
||||||
|
[AppColors.secondary, AppColors.tertiary],
|
||||||
|
[AppColors.rose, AppColors.accent],
|
||||||
|
[AppColors.tertiary, AppColors.secondary],
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final templates = _bloc.state.templates;
|
||||||
|
|
||||||
|
if (_bloc.state.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templates.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text('暂无模板', style: widget.theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: widget.isDark ? AppColors.mutedDark : AppColors.mutedLight,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
),
|
||||||
|
itemCount: templates.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final t = templates[index];
|
||||||
|
final colors = _gradients[index % _gradients.length];
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: colors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
right: -10,
|
||||||
|
bottom: -10,
|
||||||
|
child: Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white.withValues(alpha: 0.12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
t.emoji,
|
||||||
|
style: const TextStyle(fontSize: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
t.name,
|
||||||
|
style: widget.theme.textTheme.titleSmall?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
t.isFree ? '免费模板' : '精品模板',
|
||||||
|
style: widget.theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,10 +19,19 @@ class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
|||||||
late final StickerBloc _bloc;
|
late final StickerBloc _bloc;
|
||||||
final _searchController = TextEditingController();
|
final _searchController = TextEditingController();
|
||||||
|
|
||||||
/// 设计规格中的 8 个分类
|
/// 默认分类 — 从 API 数据动态补充
|
||||||
static const _specCategories = [
|
static const _defaultCategories = ['推荐', '可爱', '植物', '手绘', '校园', '节日', '文字', '和纸胶带'];
|
||||||
'推荐', '可爱', '植物', '手绘', '校园', '节日', '文字', '和纸胶带',
|
|
||||||
];
|
List<String> get _categories {
|
||||||
|
final apiCategories = _bloc.state.packs
|
||||||
|
.map((p) => p.category)
|
||||||
|
.whereType<String>()
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
if (apiCategories.isEmpty) return _defaultCategories;
|
||||||
|
// 合并:推荐 + API 返回的分类
|
||||||
|
return ['推荐', ...apiCategories];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -120,7 +129,7 @@ class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
children: _specCategories.map((cat) {
|
children: _categories.map((cat) {
|
||||||
final isSelected = cat == state.selectedCategory ||
|
final isSelected = cat == state.selectedCategory ||
|
||||||
(cat == '推荐' && state.selectedCategory == '全部');
|
(cat == '推荐' && state.selectedCategory == '全部');
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -148,13 +157,13 @@ class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// ---- 精选贴纸包卡片 ----
|
// ---- 精选贴纸包卡片(动态数据) ----
|
||||||
if (state.selectedCategory == '全部')
|
if (state.selectedCategory == '全部' && state.filteredPacks.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: const _FeaturedPackCard(),
|
child: _FeaturedPackCard(pack: state.filteredPacks.first),
|
||||||
),
|
),
|
||||||
if (state.selectedCategory == '全部')
|
if (state.selectedCategory == '全部' && state.filteredPacks.isNotEmpty)
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// ---- 贴纸包网格 ----
|
// ---- 贴纸包网格 ----
|
||||||
@@ -188,9 +197,10 @@ class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 精选贴纸包卡片 — 渐变背景 + 限时免费标签
|
/// 精选贴纸包卡片 — 渐变背景 + 动态数据
|
||||||
class _FeaturedPackCard extends StatelessWidget {
|
class _FeaturedPackCard extends StatelessWidget {
|
||||||
const _FeaturedPackCard();
|
const _FeaturedPackCard({required this.pack});
|
||||||
|
final StickerPack pack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -198,7 +208,7 @@ class _FeaturedPackCard extends StatelessWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('打开精选贴纸包: 治愈小动物')),
|
SnackBar(content: Text('打开精选贴纸包: ${pack.name}')),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -214,7 +224,6 @@ class _FeaturedPackCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// emoji 图标区域
|
|
||||||
Container(
|
Container(
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
@@ -223,30 +232,38 @@ class _FeaturedPackCard extends StatelessWidget {
|
|||||||
borderRadius: AppRadius.mdBorder,
|
borderRadius: AppRadius.mdBorder,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: const Text('🧸', style: TextStyle(fontSize: 36)),
|
child: Text(pack.displayCover, style: const TextStyle(fontSize: 36)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('治愈小动物', style: theme.textTheme.titleMedium?.copyWith(
|
Text(pack.name, style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w700, color: Colors.white,
|
fontWeight: FontWeight.w700, color: Colors.white,
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text('超可爱的手绘小动物贴纸', style: theme.textTheme.bodySmall?.copyWith(
|
Text(
|
||||||
color: Colors.white.withValues(alpha: 0.85),
|
pack.description ?? '${pack.stickerCount} 张精选贴纸',
|
||||||
)),
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.white.withValues(alpha: 0.85),
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.secondary,
|
color: pack.isFree ? AppColors.secondary : AppColors.rose,
|
||||||
borderRadius: AppRadius.pillBorder,
|
borderRadius: AppRadius.pillBorder,
|
||||||
),
|
),
|
||||||
child: const Text('限时免费', style: TextStyle(
|
child: Text(
|
||||||
fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white,
|
pack.isFree ? '免费' : '精品',
|
||||||
)),
|
style: const TextStyle(
|
||||||
|
fontSize: 11, fontWeight: FontWeight.w600, color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,11 +67,7 @@ class _TeacherView extends StatelessWidget {
|
|||||||
iconColor: AppColors.tertiary,
|
iconColor: AppColors.tertiary,
|
||||||
title: '班级码管理',
|
title: '班级码管理',
|
||||||
subtitle: '查看和重置班级码',
|
subtitle: '查看和重置班级码',
|
||||||
onTap: () {
|
onTap: () => _showClassCodes(context),
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('班级码: a1b2c3')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
@@ -159,6 +155,40 @@ class _TeacherView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showClassCodes(BuildContext context) {
|
||||||
|
final classState = context.read<ClassBloc>().state;
|
||||||
|
final classes = classState is ClassListLoaded ? classState.classes : <SchoolClass>[];
|
||||||
|
|
||||||
|
if (classes.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('请先创建班级')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('班级码管理'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: classes.map((c) => ListTile(
|
||||||
|
leading: const Icon(Icons.qr_code, color: AppColors.tertiary),
|
||||||
|
title: Text(c.name),
|
||||||
|
subtitle: Text('班级码: ${c.classCode} · ${c.memberCount} 人'),
|
||||||
|
dense: true,
|
||||||
|
)).toList(),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: const Text('关闭'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showAssignTopicDialog(BuildContext context) {
|
void _showAssignTopicDialog(BuildContext context) {
|
||||||
final titleController = TextEditingController();
|
final titleController = TextEditingController();
|
||||||
final descController = TextEditingController();
|
final descController = TextEditingController();
|
||||||
|
|||||||
@@ -293,13 +293,22 @@ class _TemplateCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// 标签
|
// 标签(从模板 category 动态生成)
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 6,
|
spacing: 6,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: [
|
children: [
|
||||||
_TagPill(label: '学生专属', bgColor: secondarySoft, textColor: AppColors.secondary),
|
if (template.category != null && template.category!.isNotEmpty)
|
||||||
_TagPill(label: '简约', bgColor: tertiarySoft, textColor: AppColors.tertiary),
|
_TagPill(
|
||||||
|
label: template.category!,
|
||||||
|
bgColor: secondarySoft,
|
||||||
|
textColor: AppColors.secondary,
|
||||||
|
),
|
||||||
|
_TagPill(
|
||||||
|
label: template.isFree ? '免费' : '精品',
|
||||||
|
bgColor: template.isFree ? tertiarySoft : AppColors.roseSoftLight,
|
||||||
|
textColor: template.isFree ? AppColors.tertiary : AppColors.rose,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
Reference in New Issue
Block a user