前端修复: - calendar_page: 移除不存在的 JournalEntry.content getter - responsive_scaffold: 移除不存在的 notchThickness 参数 - splash_page: SingleTickerProvider → TickerProvider (多 AnimationController) - profile_page: UserRoleType.name → .code (修复运行时崩溃) - 导入缺失的 user.dart 后端修复: - class_service: generate_class_code 取 UUID 后6位(随机部分)避免碰撞 - diary_role_seed: 移除不存在的 id 列,使用复合主键 ON CONFLICT 基础设施: - config/default.toml: CORS 改为通配符(开发模式) - scripts/dev.sh: 统一启动脚本(自动清理端口) - docs/opendesign/: Open Design 设计规格 HTML 原型稿 验证结果: flutter analyze 0 error, cargo test 77/77 通过, 17个页面全部渲染正常
215 lines
6.9 KiB
Dart
215 lines
6.9 KiB
Dart
// 贴纸库页面 — 贴纸包浏览 + 分类 Tab
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:nuanji_app/core/theme/app_colors.dart';
|
||
import 'package:nuanji_app/core/theme/app_radius.dart';
|
||
import 'package:nuanji_app/data/remote/api_client.dart';
|
||
import '../bloc/sticker_bloc.dart';
|
||
|
||
/// 贴纸库页面 — 分类浏览贴纸包
|
||
class StickerLibraryPage extends StatefulWidget {
|
||
const StickerLibraryPage({super.key});
|
||
|
||
@override
|
||
State<StickerLibraryPage> createState() => _StickerLibraryPageState();
|
||
}
|
||
|
||
class _StickerLibraryPageState extends State<StickerLibraryPage> {
|
||
late final StickerBloc _bloc;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_bloc = StickerBloc(api: context.read<ApiClient>());
|
||
_bloc.load();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_bloc.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('贴纸库')),
|
||
body: ListenableBuilder(
|
||
listenable: _bloc,
|
||
builder: (context, _) {
|
||
final state = _bloc.state;
|
||
|
||
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),
|
||
const SizedBox(height: 16),
|
||
FilledButton.tonal(
|
||
onPressed: _bloc.load,
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
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,
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 贴纸包卡片
|
||
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: AppRadius.mdBorder,
|
||
side: BorderSide(color: colorScheme.outlineVariant),
|
||
),
|
||
child: InkWell(
|
||
onTap: () {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text('打开贴纸包: ${pack.name}')),
|
||
);
|
||
},
|
||
borderRadius: AppRadius.mdBorder,
|
||
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: AppRadius.mdBorder,
|
||
),
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
pack.coverImageUrl != null ? '🎨' : pack.displayCover,
|
||
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,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|