fix(app): Phase 1.1 紧急修复 — SyncEngine 接入 + authorId + catch 异常处理
- feat(sync): SyncEngine 接入 EditorPage, 保存时 enqueue + 网络恢复自动 trySync - fix(editor): authorId 从 AuthBloc 获取, 替代硬编码 'local' - fix(bloc): class_bloc/calendar/profile/parent catch(_).全部改为 debugPrint - feat(editor): 编辑器工具栏拆分 (brush_panel/tag_panel/text_format_bar/dot_grid_painter) - feat(editor): EditorBloc 扩展 + EditorPage 增强 - feat(search): SearchBloc 扩展搜索功能 - feat(home): HomeBloc/HomePage 增强 - feat(auth): LoginPage 增强 - feat(templates): TemplateGalleryPage 重构 - fix(web): 管理端班级/日记页面修复 - fix(server): comment_service + theme_handler 修复 - docs: 添加全链路审计报告和验证截图
This commit is contained in:
@@ -8,6 +8,9 @@ import 'package:nuanji_app/core/theme/app_radius.dart';
|
||||
import 'package:nuanji_app/data/remote/api_client.dart';
|
||||
import '../bloc/template_bloc.dart';
|
||||
|
||||
/// 视图模式
|
||||
enum _ViewMode { daily, weekly, monthly }
|
||||
|
||||
/// 模板画廊页面 — 浏览和选择日记模板
|
||||
class TemplateGalleryPage extends StatefulWidget {
|
||||
const TemplateGalleryPage({super.key});
|
||||
@@ -18,6 +21,7 @@ class TemplateGalleryPage extends StatefulWidget {
|
||||
|
||||
class _TemplateGalleryPageState extends State<TemplateGalleryPage> {
|
||||
late final TemplateBloc _bloc;
|
||||
_ViewMode _viewMode = _ViewMode.daily;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -36,91 +40,189 @@ class _TemplateGalleryPageState extends State<TemplateGalleryPage> {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final surfaceWarm = isDark ? AppColors.surfaceWarmDark : AppColors.surfaceWarmLight;
|
||||
|
||||
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),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonal(
|
||||
onPressed: _bloc.load,
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final categories = state.categories;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 分类选择器
|
||||
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(),
|
||||
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('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
);
|
||||
}
|
||||
|
||||
// 模板网格
|
||||
Expanded(
|
||||
child: state.filteredTemplates.isEmpty
|
||||
? const Center(child: Text('暂无模板'))
|
||||
: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.78,
|
||||
),
|
||||
itemCount: state.filteredTemplates.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _TemplateCard(
|
||||
template: state.filteredTemplates[index],
|
||||
);
|
||||
},
|
||||
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: 12),
|
||||
|
||||
// ---- 视图选择器 (日/周/月) ----
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
_ViewModeButton(
|
||||
emoji: '📅', label: '日视图',
|
||||
selected: _viewMode == _ViewMode.daily,
|
||||
surfaceWarm: surfaceWarm,
|
||||
onTap: () => setState(() => _viewMode = _ViewMode.daily),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ViewModeButton(
|
||||
emoji: '📊', label: '周视图',
|
||||
selected: _viewMode == _ViewMode.weekly,
|
||||
surfaceWarm: surfaceWarm,
|
||||
onTap: () => setState(() => _viewMode = _ViewMode.weekly),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ViewModeButton(
|
||||
emoji: '📈', label: '月视图',
|
||||
selected: _viewMode == _ViewMode.monthly,
|
||||
surfaceWarm: surfaceWarm,
|
||||
onTap: () => setState(() => _viewMode = _ViewMode.monthly),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ---- 分类选择器 ----
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: state.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: 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: 8),
|
||||
|
||||
// ---- 模板网格 (200px 高预览) ----
|
||||
Expanded(
|
||||
child: state.filteredTemplates.isEmpty
|
||||
? const Center(child: Text('暂无模板'))
|
||||
: GridView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 0.52,
|
||||
),
|
||||
itemCount: state.filteredTemplates.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _TemplateCard(
|
||||
template: state.filteredTemplates[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 模板卡片
|
||||
/// 视图模式按钮
|
||||
class _ViewModeButton extends StatelessWidget {
|
||||
const _ViewModeButton({
|
||||
required this.emoji,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.surfaceWarm,
|
||||
required this.onTap,
|
||||
});
|
||||
final String emoji;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final Color surfaceWarm;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? surfaceWarm : Colors.transparent,
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.accent : Theme.of(context).colorScheme.outlineVariant,
|
||||
width: selected ? 1.5 : 1,
|
||||
),
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(emoji, style: const TextStyle(fontSize: 14)),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
|
||||
color: selected ? AppColors.accent : Theme.of(context).colorScheme.onSurface,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 模板卡片 — 200px 预览 + 使用按钮 + 标签
|
||||
class _TemplateCard extends StatelessWidget {
|
||||
const _TemplateCard({required this.template});
|
||||
|
||||
@@ -130,6 +232,9 @@ class _TemplateCard extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final secondarySoft = isDark ? AppColors.secondarySoftDark : AppColors.secondarySoftLight;
|
||||
final tertiarySoft = isDark ? AppColors.tertiarySoftDark : AppColors.tertiarySoftLight;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
@@ -137,21 +242,14 @@ class _TemplateCard extends StatelessWidget {
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
// 使用模板创建日记
|
||||
context.push('/editor?template=${template.id}');
|
||||
},
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 模板预览区
|
||||
Container(
|
||||
width: 72,
|
||||
height: 72,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 模板预览区 — 200px 高
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
@@ -166,33 +264,88 @@ class _TemplateCard extends StatelessWidget {
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
template.emoji,
|
||||
style: const TextStyle(fontSize: 36),
|
||||
style: const TextStyle(fontSize: 48),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 模板名称
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// 模板名称
|
||||
Text(
|
||||
template.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
|
||||
// 描述
|
||||
if (template.description != null)
|
||||
Text(
|
||||
template.name,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
template.description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
// 描述
|
||||
if (template.description != null)
|
||||
Text(
|
||||
template.description!,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 标签
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_TagPill(label: '学生专属', bgColor: secondarySoft, textColor: AppColors.secondary),
|
||||
_TagPill(label: '简约', bgColor: tertiarySoft, textColor: AppColors.tertiary),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 使用按钮
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
context.push('/editor?template=${template.id}');
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppColors.accent,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: AppRadius.pillBorder),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text('使用', style: TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white,
|
||||
)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 标签胶囊
|
||||
class _TagPill extends StatelessWidget {
|
||||
const _TagPill({required this.label, required this.bgColor, required this.textColor});
|
||||
final String label;
|
||||
final Color bgColor;
|
||||
final Color textColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: AppRadius.pillBorder,
|
||||
),
|
||||
child: Text(label, style: TextStyle(
|
||||
fontSize: 10, fontWeight: FontWeight.w500, color: textColor,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user