feat(app): 编辑器增强 — 查看模式 + 图层排序 + 标签/贴纸动态化
- EditorPage 新增查看模式: 打开已保存日记默认只读,编辑按钮切换 - EditorBloc 新增 ElementLayerChanged 事件,支持置顶/置底图层排序 - DraggableElement 添加图层控制按钮 (置顶/置底/删除) - TagPanel 标签建议改为从日记历史动态生成 (Top 10 频率) - StickerPickerSheet 重构,预留 API 扩展点
This commit is contained in:
@@ -99,6 +99,16 @@ class ElementSelected extends EditorEvent {
|
|||||||
ElementSelected(this.elementId);
|
ElementSelected(this.elementId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 图层顺序调整方向
|
||||||
|
enum LayerChange { bringToFront, sendToBack }
|
||||||
|
|
||||||
|
/// 调整元素图层顺序
|
||||||
|
class ElementLayerChanged extends EditorEvent {
|
||||||
|
final String elementId;
|
||||||
|
final LayerChange change;
|
||||||
|
ElementLayerChanged({required this.elementId, required this.change});
|
||||||
|
}
|
||||||
|
|
||||||
// --- 工具栏事件 ---
|
// --- 工具栏事件 ---
|
||||||
|
|
||||||
/// 切换活动工具
|
/// 切换活动工具
|
||||||
@@ -335,6 +345,7 @@ class EditorBloc extends Bloc<EditorEvent, EditorState> {
|
|||||||
on<ElementResized>(_onElementResized);
|
on<ElementResized>(_onElementResized);
|
||||||
on<ElementRotated>(_onElementRotated);
|
on<ElementRotated>(_onElementRotated);
|
||||||
on<ElementSelected>(_onElementSelected);
|
on<ElementSelected>(_onElementSelected);
|
||||||
|
on<ElementLayerChanged>(_onElementLayerChanged);
|
||||||
on<ElementsLoaded>(_onElementsLoaded);
|
on<ElementsLoaded>(_onElementsLoaded);
|
||||||
|
|
||||||
// 日记加载事件
|
// 日记加载事件
|
||||||
@@ -492,6 +503,36 @@ class EditorBloc extends Bloc<EditorEvent, EditorState> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 调整元素图层顺序 — 置顶或置底
|
||||||
|
void _onElementLayerChanged(
|
||||||
|
ElementLayerChanged event,
|
||||||
|
Emitter<EditorState> emit,
|
||||||
|
) {
|
||||||
|
final elements = List<JournalElement>.from(state.elements);
|
||||||
|
final index = elements.indexWhere((e) => e.id == event.elementId);
|
||||||
|
if (index == -1) return;
|
||||||
|
|
||||||
|
switch (event.change) {
|
||||||
|
case LayerChange.bringToFront:
|
||||||
|
// 设为最大 zIndex + 1
|
||||||
|
final maxZ = elements.fold<int>(
|
||||||
|
0,
|
||||||
|
(max, e) => e.zIndex > max ? e.zIndex : max,
|
||||||
|
);
|
||||||
|
elements[index] = elements[index].copyWith(zIndex: maxZ + 1);
|
||||||
|
case LayerChange.sendToBack:
|
||||||
|
// 设为最小 zIndex - 1
|
||||||
|
final minZ = elements.fold<int>(
|
||||||
|
0,
|
||||||
|
(min, e) => e.zIndex < min ? e.zIndex : min,
|
||||||
|
);
|
||||||
|
elements[index] = elements[index].copyWith(zIndex: minZ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(state.copyWith(elements: elements, isDirty: true));
|
||||||
|
_scheduleAutoSave();
|
||||||
|
}
|
||||||
|
|
||||||
void _onElementsLoaded(ElementsLoaded event, Emitter<EditorState> emit) {
|
void _onElementsLoaded(ElementsLoaded event, Emitter<EditorState> emit) {
|
||||||
emit(state.copyWith(elements: event.elements));
|
emit(state.copyWith(elements: event.elements));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,15 +334,26 @@ class _EditorView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EditorViewState extends State<_EditorView> {
|
class _EditorViewState extends State<_EditorView> {
|
||||||
|
/// 查看模式:打开已有日记时默认只读,点击"编辑"后进入编辑模式
|
||||||
|
bool _isViewMode = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// 当 journalId 非空时,从 Isar 加载已有日记数据
|
// 当 journalId 非空时,进入查看模式
|
||||||
|
_isViewMode = widget.journalId != null;
|
||||||
if (widget.journalId != null) {
|
if (widget.journalId != null) {
|
||||||
_loadExistingJournal(widget.journalId!);
|
_loadExistingJournal(widget.journalId!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从查看模式切换到编辑模式
|
||||||
|
void _enterEditMode() {
|
||||||
|
setState(() => _isViewMode = false);
|
||||||
|
// 切换到画笔工具,进入编辑状态
|
||||||
|
context.read<EditorBloc>().add(ToolChanged(EditorTool.brush));
|
||||||
|
}
|
||||||
|
|
||||||
/// 从 Isar 加载已有日记 — 使用 LoadJournal 原子事件一次性还原
|
/// 从 Isar 加载已有日记 — 使用 LoadJournal 原子事件一次性还原
|
||||||
Future<void> _loadExistingJournal(String id) async {
|
Future<void> _loadExistingJournal(String id) async {
|
||||||
try {
|
try {
|
||||||
@@ -381,6 +392,11 @@ class _EditorViewState extends State<_EditorView> {
|
|||||||
elements: otherElements,
|
elements: otherElements,
|
||||||
lastSavedAt: entry.updatedAt,
|
lastSavedAt: entry.updatedAt,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// 查看模式下使用 select 工具,避免自动弹出画笔面板
|
||||||
|
if (_isViewMode) {
|
||||||
|
context.read<EditorBloc>().add(ToolChanged(EditorTool.select));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('加载日记数据失败: $e');
|
debugPrint('加载日记数据失败: $e');
|
||||||
}
|
}
|
||||||
@@ -405,26 +421,31 @@ class _EditorViewState extends State<_EditorView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<EditorBloc, EditorState>(
|
child: BlocBuilder<EditorBloc, EditorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return _EditorStack(state: state, journalId: widget.journalId);
|
return _EditorStack(
|
||||||
|
state: state,
|
||||||
|
journalId: widget.journalId,
|
||||||
|
isViewMode: _isViewMode,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 底部工具栏(自带底部安全区)
|
// 底部工具栏 — 仅编辑模式显示
|
||||||
BlocBuilder<EditorBloc, EditorState>(
|
if (!_isViewMode)
|
||||||
builder: (context, state) {
|
BlocBuilder<EditorBloc, EditorState>(
|
||||||
return EditorToolbar(
|
builder: (context, state) {
|
||||||
state: state,
|
return EditorToolbar(
|
||||||
onEvent: (event) => context.read<EditorBloc>().add(event),
|
state: state,
|
||||||
);
|
onEvent: (event) => context.read<EditorBloc>().add(event),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 顶部操作栏 — 日期/撤销重做/标签/心情/完成
|
/// 顶部操作栏 — 查看模式: 返回/日期/评语/编辑按钮;编辑模式: 返回/日期/撤销重做/标签/完成
|
||||||
Widget _buildTopBar(BuildContext context, EditorState state) {
|
Widget _buildTopBar(BuildContext context, EditorState state) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Container(
|
||||||
@@ -467,51 +488,67 @@ class _EditorViewState extends State<_EditorView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 撤销
|
if (_isViewMode) ...[
|
||||||
IconButton(
|
// 查看模式:评语按钮 + 编辑按钮
|
||||||
icon: const Icon(Icons.undo_rounded, size: 18),
|
if (widget.journalId != null)
|
||||||
onPressed: () => context.read<EditorBloc>().add(Undo()),
|
IconButton(
|
||||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
icon: const Icon(Icons.chat_bubble_outline_rounded, size: 18),
|
||||||
),
|
onPressed: () => _showComments(context),
|
||||||
// 重做
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.redo_rounded, size: 18),
|
Padding(
|
||||||
onPressed: () => context.read<EditorBloc>().add(Redo()),
|
padding: const EdgeInsets.only(left: 4),
|
||||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
child: FilledButton.tonal(
|
||||||
),
|
onPressed: _enterEditMode,
|
||||||
// 自动保存状态
|
style: FilledButton.styleFrom(
|
||||||
_buildAutosaveIndicator(state),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
// 标签按钮
|
minimumSize: const Size(0, 32),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.sell_rounded, size: 18),
|
child: const Text('编辑', style: TextStyle(fontSize: 14)),
|
||||||
onPressed: () => _showTagPanel(context, state),
|
),
|
||||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
),
|
||||||
),
|
] else ...[
|
||||||
// 评语按钮(仅已有日记显示)
|
// 编辑模式:撤销/重做/标签/评语/完成
|
||||||
if (widget.journalId != null)
|
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.chat_bubble_outline_rounded, size: 18),
|
icon: const Icon(Icons.undo_rounded, size: 18),
|
||||||
onPressed: () => _showComments(context),
|
onPressed: () => context.read<EditorBloc>().add(Undo()),
|
||||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||||
),
|
),
|
||||||
// 完成/保存按钮
|
IconButton(
|
||||||
Padding(
|
icon: const Icon(Icons.redo_rounded, size: 18),
|
||||||
padding: const EdgeInsets.only(left: 4),
|
onPressed: () => context.read<EditorBloc>().add(Redo()),
|
||||||
child: FilledButton.tonal(
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||||
onPressed: () => _handleSave(context, state),
|
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
),
|
|
||||||
child: const Text('完成', style: TextStyle(fontSize: 14)),
|
|
||||||
),
|
),
|
||||||
),
|
_buildAutosaveIndicator(state),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.sell_rounded, size: 18),
|
||||||
|
onPressed: () => _showTagPanel(context, state),
|
||||||
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||||
|
),
|
||||||
|
if (widget.journalId != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.chat_bubble_outline_rounded, size: 18),
|
||||||
|
onPressed: () => _showComments(context),
|
||||||
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: FilledButton.tonal(
|
||||||
|
onPressed: () => _handleSave(context, state),
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
),
|
||||||
|
child: const Text('完成', style: TextStyle(fontSize: 14)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 日期 + 心情条 (40px)
|
// 日期 + 心情条 (40px) — 仅编辑模式显示
|
||||||
_buildDateMoodStrip(context, state),
|
if (!_isViewMode) _buildDateMoodStrip(context, state),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -674,8 +711,13 @@ class _EditorViewState extends State<_EditorView> {
|
|||||||
class _EditorStack extends StatefulWidget {
|
class _EditorStack extends StatefulWidget {
|
||||||
final EditorState state;
|
final EditorState state;
|
||||||
final String? journalId;
|
final String? journalId;
|
||||||
|
final bool isViewMode;
|
||||||
|
|
||||||
const _EditorStack({required this.state, this.journalId});
|
const _EditorStack({
|
||||||
|
required this.state,
|
||||||
|
this.journalId,
|
||||||
|
this.isViewMode = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_EditorStack> createState() => _EditorStackState();
|
State<_EditorStack> createState() => _EditorStackState();
|
||||||
@@ -900,6 +942,7 @@ class _EditorStackState extends State<_EditorStack> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _titleController,
|
controller: _titleController,
|
||||||
|
enabled: !widget.isViewMode,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'Quicksand',
|
fontFamily: 'Quicksand',
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -943,8 +986,8 @@ class _EditorStackState extends State<_EditorStack> {
|
|||||||
if (state.elements.isNotEmpty)
|
if (state.elements.isNotEmpty)
|
||||||
_buildElementLayer(context, state),
|
_buildElementLayer(context, state),
|
||||||
|
|
||||||
// 文字输入覆盖层(文字工具激活时显示)
|
// 文字输入覆盖层(文字工具激活时显示)— 仅编辑模式
|
||||||
if (state.activeTool == EditorTool.text)
|
if (!widget.isViewMode && state.activeTool == EditorTool.text)
|
||||||
TextInputOverlay(
|
TextInputOverlay(
|
||||||
onConfirmed: (text, fontSize, fontColor) {
|
onConfirmed: (text, fontSize, fontColor) {
|
||||||
final center = Offset(
|
final center = Offset(
|
||||||
@@ -967,8 +1010,8 @@ class _EditorStackState extends State<_EditorStack> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// 图片选择覆盖层(图片工具激活时显示)
|
// 图片选择覆盖层(图片工具激活时显示)— 仅编辑模式
|
||||||
if (state.activeTool == EditorTool.photo)
|
if (!widget.isViewMode && state.activeTool == EditorTool.photo)
|
||||||
Center(
|
Center(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -988,8 +1031,11 @@ class _EditorStackState extends State<_EditorStack> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 空状态提示
|
// 空状态提示 — 仅编辑模式显示
|
||||||
if (state.strokes.isEmpty && state.elements.isEmpty && state.activeTool == EditorTool.select)
|
if (!widget.isViewMode &&
|
||||||
|
state.strokes.isEmpty &&
|
||||||
|
state.elements.isEmpty &&
|
||||||
|
state.activeTool == EditorTool.select)
|
||||||
_buildEmptyHint(context),
|
_buildEmptyHint(context),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1057,6 +1103,11 @@ class _EditorStackState extends State<_EditorStack> {
|
|||||||
onDeleted: (id) {
|
onDeleted: (id) {
|
||||||
context.read<EditorBloc>().add(ElementRemoved(id));
|
context.read<EditorBloc>().add(ElementRemoved(id));
|
||||||
},
|
},
|
||||||
|
onLayerChanged: (id, change) {
|
||||||
|
context.read<EditorBloc>().add(
|
||||||
|
ElementLayerChanged(elementId: id, change: change),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../../data/models/journal_element.dart';
|
import '../../../data/models/journal_element.dart';
|
||||||
|
import '../bloc/editor_bloc.dart' show LayerChange;
|
||||||
|
|
||||||
/// 可拖拽日记元素组件
|
/// 可拖拽日记元素组件
|
||||||
class DraggableElement extends StatefulWidget {
|
class DraggableElement extends StatefulWidget {
|
||||||
@@ -22,6 +23,7 @@ class DraggableElement extends StatefulWidget {
|
|||||||
final void Function(String id, double w, double h)? onResized;
|
final void Function(String id, double w, double h)? onResized;
|
||||||
final void Function(String id, double rotation)? onRotated;
|
final void Function(String id, double rotation)? onRotated;
|
||||||
final ValueChanged<String> onDeleted;
|
final ValueChanged<String> onDeleted;
|
||||||
|
final void Function(String id, LayerChange change)? onLayerChanged;
|
||||||
|
|
||||||
const DraggableElement({
|
const DraggableElement({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -32,6 +34,7 @@ class DraggableElement extends StatefulWidget {
|
|||||||
this.onResized,
|
this.onResized,
|
||||||
this.onRotated,
|
this.onRotated,
|
||||||
required this.onDeleted,
|
required this.onDeleted,
|
||||||
|
this.onLayerChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -142,26 +145,41 @@ class _DraggableElementState extends State<DraggableElement> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 选中时显示删除按钮
|
// 选中时显示操作按钮:图层 + 删除
|
||||||
if (widget.isSelected)
|
if (widget.isSelected)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -12,
|
top: -12,
|
||||||
right: -12,
|
right: -12,
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
onTap: () => widget.onDeleted(widget.element.id),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Container(
|
children: [
|
||||||
width: 24,
|
// 置顶
|
||||||
height: 24,
|
_ActionButton(
|
||||||
decoration: BoxDecoration(
|
icon: Icons.flip_to_front_rounded,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onTap: () => widget.onLayerChanged?.call(
|
||||||
|
widget.element.id,
|
||||||
|
LayerChange.bringToFront,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 置底
|
||||||
|
_ActionButton(
|
||||||
|
icon: Icons.flip_to_back_rounded,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
onTap: () => widget.onLayerChanged?.call(
|
||||||
|
widget.element.id,
|
||||||
|
LayerChange.sendToBack,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// 删除
|
||||||
|
_ActionButton(
|
||||||
|
icon: Icons.close_rounded,
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
shape: BoxShape.circle,
|
onTap: () => widget.onDeleted(widget.element.id),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
],
|
||||||
Icons.close_rounded,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -279,3 +297,32 @@ class _DraggableElementState extends State<DraggableElement> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 选中元素的操作按钮(图层/删除)
|
||||||
|
class _ActionButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _ActionButton({
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 16, color: Colors.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// 贴纸选择底部面板
|
// 贴纸选择底部面板
|
||||||
//
|
//
|
||||||
// Phase 1 使用内置 emoji 贴纸(6 类 60 个),后续替换为贴纸包资源。
|
// Phase 1 使用内置 emoji 贴纸(6 类 60 个)。
|
||||||
// 分类:心情/动物/自然/食物/学校/装饰
|
// 当贴纸包 API 有数据时自动追加到"更多贴纸"分类。
|
||||||
|
// 后续 Phase 2 将完全迁移到贴纸包资源。
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
@@ -14,8 +15,8 @@ class StickerPickerSheet extends StatelessWidget {
|
|||||||
required this.onStickerSelected,
|
required this.onStickerSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Phase 1 内置贴纸集
|
// 内置基础贴纸集(Phase 1 保底,保证离线可用)
|
||||||
static const _stickerCategories = <String, List<String>>{
|
static const _builtinStickers = <String, List<String>>{
|
||||||
'心情': ['😊', '😢', '😡', '🤔', '😐', '🥰', '😋', '🤗', '😴', '🎉'],
|
'心情': ['😊', '😢', '😡', '🤔', '😐', '🥰', '😋', '🤗', '😴', '🎉'],
|
||||||
'动物': ['🐱', '🐶', '🐰', '🐻', '🦊', '🐼', '🐨', '🦄', '🐸', '🦋'],
|
'动物': ['🐱', '🐶', '🐰', '🐻', '🦊', '🐼', '🐨', '🦄', '🐸', '🦋'],
|
||||||
'自然': ['🌸', '🌺', '🌻', '🍀', '🌈', '⭐', '🌙', '☀️', '❄️', '🍃'],
|
'自然': ['🌸', '🌺', '🌻', '🍀', '🌈', '⭐', '🌙', '☀️', '❄️', '🍃'],
|
||||||
@@ -24,6 +25,9 @@ class StickerPickerSheet extends StatelessWidget {
|
|||||||
'装饰': ['💕', '✨', '🎀', '🎵', '🎶', '💫', '🦋', '🌸', '🍀', '💎'],
|
'装饰': ['💕', '✨', '🎀', '🎵', '🎶', '💫', '🦋', '🌸', '🍀', '💎'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 合并后的贴纸分类(预留 API 扩展入口)
|
||||||
|
Map<String, List<String>> get _stickerCategories => _builtinStickers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
// 标签面板 -- 底部抽屉
|
// 标签面板 -- 底部抽屉
|
||||||
// 支持添加/移除自定义标签 + 推荐标签快捷选择
|
// 支持添加/移除自定义标签 + 推荐标签快捷选择
|
||||||
|
// 推荐标签从用户历史标签动态推导,无数据时使用默认推荐
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../../core/theme/app_colors.dart';
|
import '../../../core/theme/app_colors.dart';
|
||||||
|
import '../../../data/repositories/journal_repository.dart';
|
||||||
|
|
||||||
/// 标签面板 -- 底部抽屉
|
/// 标签面板 -- 底部抽屉
|
||||||
class TagPanel extends StatefulWidget {
|
class TagPanel extends StatefulWidget {
|
||||||
@@ -26,15 +29,37 @@ class _TagPanelState extends State<TagPanel> {
|
|||||||
final _controller = TextEditingController();
|
final _controller = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
final _focusNode = FocusNode();
|
||||||
|
|
||||||
static const _suggestedTags = [
|
/// 推荐标签 — 动态推导
|
||||||
'日常', '学习', '读书', '心情', '学校', '旅行',
|
List<String> _suggestedTags = ['日常', '学习', '读书', '心情', '学校', '旅行', '美食', '运动'];
|
||||||
'美食', '运动', '音乐', '梦想',
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
|
_deriveSuggestedTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从用户历史日记标签推导推荐标签
|
||||||
|
Future<void> _deriveSuggestedTags() 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(() {
|
||||||
|
_suggestedTags = sorted.take(10).toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 保持默认值
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user