feat(app): 编辑器增强 — 查看模式 + 图层排序 + 标签/贴纸动态化
- EditorPage 新增查看模式: 打开已保存日记默认只读,编辑按钮切换 - EditorBloc 新增 ElementLayerChanged 事件,支持置顶/置底图层排序 - DraggableElement 添加图层控制按钮 (置顶/置底/删除) - TagPanel 标签建议改为从日记历史动态生成 (Top 10 频率) - StickerPickerSheet 重构,预留 API 扩展点
This commit is contained in:
@@ -334,15 +334,26 @@ class _EditorView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EditorViewState extends State<_EditorView> {
|
||||
/// 查看模式:打开已有日记时默认只读,点击"编辑"后进入编辑模式
|
||||
bool _isViewMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 当 journalId 非空时,从 Isar 加载已有日记数据
|
||||
// 当 journalId 非空时,进入查看模式
|
||||
_isViewMode = widget.journalId != null;
|
||||
if (widget.journalId != null) {
|
||||
_loadExistingJournal(widget.journalId!);
|
||||
}
|
||||
}
|
||||
|
||||
/// 从查看模式切换到编辑模式
|
||||
void _enterEditMode() {
|
||||
setState(() => _isViewMode = false);
|
||||
// 切换到画笔工具,进入编辑状态
|
||||
context.read<EditorBloc>().add(ToolChanged(EditorTool.brush));
|
||||
}
|
||||
|
||||
/// 从 Isar 加载已有日记 — 使用 LoadJournal 原子事件一次性还原
|
||||
Future<void> _loadExistingJournal(String id) async {
|
||||
try {
|
||||
@@ -381,6 +392,11 @@ class _EditorViewState extends State<_EditorView> {
|
||||
elements: otherElements,
|
||||
lastSavedAt: entry.updatedAt,
|
||||
));
|
||||
|
||||
// 查看模式下使用 select 工具,避免自动弹出画笔面板
|
||||
if (_isViewMode) {
|
||||
context.read<EditorBloc>().add(ToolChanged(EditorTool.select));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('加载日记数据失败: $e');
|
||||
}
|
||||
@@ -405,26 +421,31 @@ class _EditorViewState extends State<_EditorView> {
|
||||
Expanded(
|
||||
child: BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return _EditorStack(state: state, journalId: widget.journalId);
|
||||
return _EditorStack(
|
||||
state: state,
|
||||
journalId: widget.journalId,
|
||||
isViewMode: _isViewMode,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 底部工具栏(自带底部安全区)
|
||||
BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return EditorToolbar(
|
||||
state: state,
|
||||
onEvent: (event) => context.read<EditorBloc>().add(event),
|
||||
);
|
||||
},
|
||||
),
|
||||
// 底部工具栏 — 仅编辑模式显示
|
||||
if (!_isViewMode)
|
||||
BlocBuilder<EditorBloc, EditorState>(
|
||||
builder: (context, state) {
|
||||
return EditorToolbar(
|
||||
state: state,
|
||||
onEvent: (event) => context.read<EditorBloc>().add(event),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 顶部操作栏 — 日期/撤销重做/标签/心情/完成
|
||||
/// 顶部操作栏 — 查看模式: 返回/日期/评语/编辑按钮;编辑模式: 返回/日期/撤销重做/标签/完成
|
||||
Widget _buildTopBar(BuildContext context, EditorState state) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
@@ -467,51 +488,67 @@ class _EditorViewState extends State<_EditorView> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// 撤销
|
||||
IconButton(
|
||||
icon: const Icon(Icons.undo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Undo()),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
// 重做
|
||||
IconButton(
|
||||
icon: const Icon(Icons.redo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Redo()),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
// 自动保存状态
|
||||
_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)
|
||||
if (_isViewMode) ...[
|
||||
// 查看模式:评语按钮 + 编辑按钮
|
||||
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: _enterEditMode,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
child: const Text('编辑', style: TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// 编辑模式:撤销/重做/标签/评语/完成
|
||||
IconButton(
|
||||
icon: const Icon(Icons.chat_bubble_outline_rounded, size: 18),
|
||||
onPressed: () => _showComments(context),
|
||||
icon: const Icon(Icons.undo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Undo()),
|
||||
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)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.redo_rounded, size: 18),
|
||||
onPressed: () => context.read<EditorBloc>().add(Redo()),
|
||||
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
||||
),
|
||||
),
|
||||
_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)
|
||||
_buildDateMoodStrip(context, state),
|
||||
// 日期 + 心情条 (40px) — 仅编辑模式显示
|
||||
if (!_isViewMode) _buildDateMoodStrip(context, state),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -674,8 +711,13 @@ class _EditorViewState extends State<_EditorView> {
|
||||
class _EditorStack extends StatefulWidget {
|
||||
final EditorState state;
|
||||
final String? journalId;
|
||||
final bool isViewMode;
|
||||
|
||||
const _EditorStack({required this.state, this.journalId});
|
||||
const _EditorStack({
|
||||
required this.state,
|
||||
this.journalId,
|
||||
this.isViewMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_EditorStack> createState() => _EditorStackState();
|
||||
@@ -900,6 +942,7 @@ class _EditorStackState extends State<_EditorStack> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
child: TextField(
|
||||
controller: _titleController,
|
||||
enabled: !widget.isViewMode,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Quicksand',
|
||||
fontSize: 18,
|
||||
@@ -943,8 +986,8 @@ class _EditorStackState extends State<_EditorStack> {
|
||||
if (state.elements.isNotEmpty)
|
||||
_buildElementLayer(context, state),
|
||||
|
||||
// 文字输入覆盖层(文字工具激活时显示)
|
||||
if (state.activeTool == EditorTool.text)
|
||||
// 文字输入覆盖层(文字工具激活时显示)— 仅编辑模式
|
||||
if (!widget.isViewMode && state.activeTool == EditorTool.text)
|
||||
TextInputOverlay(
|
||||
onConfirmed: (text, fontSize, fontColor) {
|
||||
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(
|
||||
child: Row(
|
||||
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),
|
||||
],
|
||||
);
|
||||
@@ -1057,6 +1103,11 @@ class _EditorStackState extends State<_EditorStack> {
|
||||
onDeleted: (id) {
|
||||
context.read<EditorBloc>().add(ElementRemoved(id));
|
||||
},
|
||||
onLayerChanged: (id, change) {
|
||||
context.read<EditorBloc>().add(
|
||||
ElementLayerChanged(elementId: id, change: change),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user