feat(app): 编辑器增强 — 查看模式 + 图层排序 + 标签/贴纸动态化
- EditorPage 新增查看模式: 打开已保存日记默认只读,编辑按钮切换 - EditorBloc 新增 ElementLayerChanged 事件,支持置顶/置底图层排序 - DraggableElement 添加图层控制按钮 (置顶/置底/删除) - TagPanel 标签建议改为从日记历史动态生成 (Top 10 频率) - StickerPickerSheet 重构,预留 API 扩展点
This commit is contained in:
@@ -12,6 +12,7 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../data/models/journal_element.dart';
|
||||
import '../bloc/editor_bloc.dart' show LayerChange;
|
||||
|
||||
/// 可拖拽日记元素组件
|
||||
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 rotation)? onRotated;
|
||||
final ValueChanged<String> onDeleted;
|
||||
final void Function(String id, LayerChange change)? onLayerChanged;
|
||||
|
||||
const DraggableElement({
|
||||
super.key,
|
||||
@@ -32,6 +34,7 @@ class DraggableElement extends StatefulWidget {
|
||||
this.onResized,
|
||||
this.onRotated,
|
||||
required this.onDeleted,
|
||||
this.onLayerChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -142,26 +145,41 @@ class _DraggableElementState extends State<DraggableElement> {
|
||||
),
|
||||
),
|
||||
|
||||
// 选中时显示删除按钮
|
||||
// 选中时显示操作按钮:图层 + 删除
|
||||
if (widget.isSelected)
|
||||
Positioned(
|
||||
top: -12,
|
||||
right: -12,
|
||||
child: GestureDetector(
|
||||
onTap: () => widget.onDeleted(widget.element.id),
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 置顶
|
||||
_ActionButton(
|
||||
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,
|
||||
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';
|
||||
|
||||
@@ -14,8 +15,8 @@ class StickerPickerSheet extends StatelessWidget {
|
||||
required this.onStickerSelected,
|
||||
});
|
||||
|
||||
// Phase 1 内置贴纸集
|
||||
static const _stickerCategories = <String, List<String>>{
|
||||
// 内置基础贴纸集(Phase 1 保底,保证离线可用)
|
||||
static const _builtinStickers = <String, List<String>>{
|
||||
'心情': ['😊', '😢', '😡', '🤔', '😐', '🥰', '😋', '🤗', '😴', '🎉'],
|
||||
'动物': ['🐱', '🐶', '🐰', '🐻', '🦊', '🐼', '🐨', '🦄', '🐸', '🦋'],
|
||||
'自然': ['🌸', '🌺', '🌻', '🍀', '🌈', '⭐', '🌙', '☀️', '❄️', '🍃'],
|
||||
@@ -24,6 +25,9 @@ class StickerPickerSheet extends StatelessWidget {
|
||||
'装饰': ['💕', '✨', '🎀', '🎵', '🎶', '💫', '🦋', '🌸', '🍀', '💎'],
|
||||
};
|
||||
|
||||
/// 合并后的贴纸分类(预留 API 扩展入口)
|
||||
Map<String, List<String>> get _stickerCategories => _builtinStickers;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// 标签面板 -- 底部抽屉
|
||||
// 支持添加/移除自定义标签 + 推荐标签快捷选择
|
||||
// 推荐标签从用户历史标签动态推导,无数据时使用默认推荐
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../data/repositories/journal_repository.dart';
|
||||
|
||||
/// 标签面板 -- 底部抽屉
|
||||
class TagPanel extends StatefulWidget {
|
||||
@@ -26,15 +29,37 @@ class _TagPanelState extends State<TagPanel> {
|
||||
final _controller = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
static const _suggestedTags = [
|
||||
'日常', '学习', '读书', '心情', '学校', '旅行',
|
||||
'美食', '运动', '音乐', '梦想',
|
||||
];
|
||||
/// 推荐标签 — 动态推导
|
||||
List<String> _suggestedTags = ['日常', '学习', '读书', '心情', '学校', '旅行', '美食', '运动'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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
|
||||
|
||||
Reference in New Issue
Block a user