笔记编辑功能修复

This commit is contained in:
DelLevin-Home
2026-07-16 04:12:04 +08:00
parent 1aab6858a7
commit 10ecf57fa7
9 changed files with 481 additions and 24 deletions

View File

@@ -412,6 +412,7 @@ class _CloudSheetContentState extends State<_CloudSheetContent> {
}
Future<void> _loadRemoteInfo() async {
if (!mounted) return;
setState(() => _loading = true);
final info = await WebDAVService.instance.getRemoteBackupInfo();
if (mounted) {

View File

@@ -2024,9 +2024,9 @@ class _MovieDetailPageState extends State<MovieDetailPage> {
Navigator.of(context).pop(); // close dialog
provider.selectMovie(null);
} else {
final navigator = Navigator.of(context);
navigator.pop();
navigator.pop();
// 先关 dialog再关详情页分两步避免连续 pop 导致的导航栈异常
Navigator.of(context).pop(); // close dialog
if (mounted) Navigator.of(context).pop(); // close detail page
}
if (mounted && context.mounted) {
ToastUtil.show(context, '已删除');

View File

@@ -10,6 +10,7 @@ import '../../models/data_models.dart';
import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart';
import '../../widgets/fade_in_local_image.dart';
import '../../widgets/tag_side_panel.dart';
import '../../widgets/vditor_editor.dart';
class NoteAddPage extends StatefulWidget {
@@ -126,7 +127,6 @@ class _NoteAddPageState extends State<NoteAddPage> {
// 标题输入
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: TextField(
controller: _titleCtrl,
@@ -143,6 +143,13 @@ class _NoteAddPageState extends State<NoteAddPage> {
),
)),
),
// 标签行 — 标题下面,靠左
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Align(alignment: Alignment.centerLeft, child: _buildTagRow(colors)),
)),
),
// 内容编辑
Expanded(
child: VditorEditor(
@@ -169,6 +176,64 @@ class _NoteAddPageState extends State<NoteAddPage> {
]);
}
Widget _buildTagRow(ColorScheme colors) {
return Wrap(
spacing: 6,
runSpacing: 4,
children: [
for (final tag in _tags)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(width: 3),
GestureDetector(
onTap: () => setState(() => _tags.remove(tag)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
),
]),
),
GestureDetector(
onTap: _showTagPanel,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
const SizedBox(width: 2),
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
]),
),
),
],
);
}
Future<void> _showTagPanel() async {
final provider = context.read<AppProvider>();
final tagRows = await provider.getTags('note_tag');
final allTags = tagRows.map((t) => t['name'] as String).toSet();
for (final note in provider.notes) {
allTags.addAll(note.tags);
}
if (!mounted) return;
TagSidePanel.show(
context: context,
selectedTags: List.from(_tags),
allAvailableTags: allTags.toList()..sort(),
onTagsChanged: (newTags) {
setState(() => _tags = newTags);
},
);
}
Widget _buildPreviewArea(ColorScheme colors) {
return ListView(
padding: const EdgeInsets.all(24),
@@ -178,6 +243,21 @@ class _NoteAddPageState extends State<NoteAddPage> {
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600, color: colors.onSurface, height: 1.3)),
const SizedBox(height: 16),
],
// 标签
if (_tags.isNotEmpty) ...[
Wrap(spacing: 6, runSpacing: 4, children: [
for (final tag in _tags)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: colors.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text(tag, style: TextStyle(fontSize: 12, color: colors.onPrimaryContainer, fontWeight: FontWeight.w500)),
),
]),
const SizedBox(height: 20),
],
Markdown(
data: _contentCtrl.text,
styleSheet: _buildMarkdownStyleSheet(colors),

View File

@@ -12,6 +12,7 @@ import '../../utils/toast_util.dart';
import '../../utils/image_path_helper.dart';
import '../../utils/responsive.dart';
import '../../widgets/vditor_editor.dart';
import '../../widgets/tag_side_panel.dart';
import 'note_share_page.dart';
/// 笔记详情页
@@ -444,7 +445,6 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
// 标题输入
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: TextField(controller: _titleCtrl, maxLines: 1,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
@@ -454,6 +454,13 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
onChanged: (_) => setState(() {})),
)),
),
// 标签行 — 标题下面,靠左
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Align(alignment: Alignment.centerLeft, child: _buildEditTagRow(colors)),
)),
),
// 内容编辑
Expanded(
child: VditorEditor(
@@ -480,6 +487,64 @@ class _NoteDetailPageState extends State<NoteDetailPage> {
]);
}
Widget _buildEditTagRow(ColorScheme colors) {
return Wrap(
spacing: 6,
runSpacing: 4,
children: [
for (final tag in _editTags)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(tag, style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.6))),
const SizedBox(width: 3),
GestureDetector(
onTap: () => setState(() => _editTags.remove(tag)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.3)),
),
]),
),
GestureDetector(
onTap: _showEditTagPanel,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
border: Border.all(color: colors.onSurface.withValues(alpha: 0.25), width: 1),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.add, size: 12, color: colors.onSurface.withValues(alpha: 0.35)),
const SizedBox(width: 2),
Text('标签', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.35))),
]),
),
),
],
);
}
Future<void> _showEditTagPanel() async {
final provider = context.read<AppProvider>();
final tagRows = await provider.getTags('note_tag');
final allTags = tagRows.map((t) => t['name'] as String).toSet();
for (final note in provider.notes) {
allTags.addAll(note.tags);
}
if (!mounted) return;
TagSidePanel.show(
context: context,
selectedTags: List.from(_editTags),
allAvailableTags: allTags.toList()..sort(),
onTagsChanged: (newTags) {
setState(() => _editTags = newTags);
},
);
}
Widget _buildPreviewArea(ColorScheme colors, Note note) {
return ListView(
padding: const EdgeInsets.symmetric(vertical: 24),

View File

@@ -143,6 +143,12 @@ class _NoteFormPageState extends State<NoteFormPage> {
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final topPadding = MediaQuery.of(context).padding.top;
// Windows 桌面端:与编辑页一致的布局
if (Platform.isWindows) {
return _buildWindowsDesktopLayout(colors, topPadding);
}
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
@@ -156,7 +162,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
body: LayoutBuilder(
builder: (context, constraints) {
final keyboardH = MediaQuery.of(context).viewInsets.bottom;
final contentH = constraints.maxHeight * 0.4;
return Stack(
children: [
Column(
@@ -164,19 +169,16 @@ class _NoteFormPageState extends State<NoteFormPage> {
// 顶部区域 — 固定不动
_buildHeader(colors, topPadding),
// 可滚动内容
// 移动端CustomScrollView 包裹所有内容
Expanded(
child: CustomScrollView(
controller: _scrollController,
physics: _editorTouched ? const NeverScrollableScrollPhysics() : null,
slivers: [
// 标题行(点击编辑)
SliverToBoxAdapter(child: _buildTitleInput(colors)),
// 编辑区域 — 高度随内容撑开,触摸时禁用外层滚动
SliverToBoxAdapter(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: contentH),
constraints: BoxConstraints(minHeight: constraints.maxHeight * 0.4),
child: GestureDetector(
onTapDown: (_) => setState(() => _editorTouched = true),
onTapUp: (_) => setState(() => _editorTouched = false),
@@ -184,8 +186,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
child: _buildEditor(),
)),
),
// 图片 + 标签 + 字数 — 随内容撑开
SliverToBoxAdapter(child: _buildImageGrid()),
SliverToBoxAdapter(child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
@@ -198,8 +198,6 @@ class _NoteFormPageState extends State<NoteFormPage> {
style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3)),
),
)),
// 与工具栏的间距
const SliverToBoxAdapter(child: SizedBox(height: 80)),
],
),
@@ -222,6 +220,128 @@ class _NoteFormPageState extends State<NoteFormPage> {
);
}
/// Windows 桌面端布局 — 与编辑页一致
Widget _buildWindowsDesktopLayout(ColorScheme colors, double topPadding) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _confirmLeave();
if (shouldPop && context.mounted) Navigator.pop(context);
},
child: Scaffold(
backgroundColor: colors.surface,
body: Column(
children: [
// 顶栏 — 与编辑页一致
Container(
height: 52,
decoration: BoxDecoration(color: colors.surface,
border: Border(bottom: BorderSide(color: colors.outlineVariant, width: 0.5))),
child: Row(children: [
const SizedBox(width: 8),
IconButton(icon: Icon(Icons.close, color: colors.onSurface, size: 18),
onPressed: () async {
final shouldPop = await _confirmLeave();
if (shouldPop && context.mounted) Navigator.pop(context);
}),
Expanded(child: Text(_titleController.text.isNotEmpty ? _titleController.text : '新建笔记',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface.withValues(alpha: 0.6)),
maxLines: 1, overflow: TextOverflow.ellipsis)),
if (_saveStatus == 'saved')
Padding(padding: const EdgeInsets.only(right: 8),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Colors.green, shape: BoxShape.circle)),
const SizedBox(width: 4),
Text('已保存', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.4))),
])),
FilledButton.icon(onPressed: _saveNote,
icon: const Icon(Icons.check, size: 16), label: const Text('保存'),
style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
const SizedBox(width: 16),
]),
),
// 标题输入 — 与编辑页一致
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: TextField(controller: _titleController, maxLines: 1,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface, height: 1.4),
decoration: InputDecoration(hintText: '添加标题',
hintStyle: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: colors.onSurface.withValues(alpha: 0.2), height: 1.4),
border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, isDense: true, contentPadding: EdgeInsets.zero),
onChanged: (_) => setState(() {})),
)),
),
// 标签行 — 与编辑页一致
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Center(child: ConstrainedBox(constraints: const BoxConstraints(maxWidth: 720),
child: Align(alignment: Alignment.centerLeft, child: _buildTagChips()),
)),
),
// 内容编辑
Expanded(
child: VditorEditor(
key: _vditorKey,
initialContent: _contentController.text,
noteId: _isEditing && widget.note != null ? widget.note!.id : (_tempNoteId ?? ''),
isDark: Theme.of(context).brightness == Brightness.dark,
surfaceColor: colors.surface,
onContentChanged: (value) {
_contentController.text = value;
_onTextChanged();
},
),
),
// 图片网格
if (_images.isNotEmpty) _buildEditImageGrid(colors),
// 底部字数
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Text('${_contentController.text.length}', style: TextStyle(fontSize: 11, color: colors.onSurface.withValues(alpha: 0.3))),
]),
),
],
),
),
);
}
/// Windows 桌面端图片网格 — 与编辑页一致
Widget _buildEditImageGrid(ColorScheme colors) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 0),
height: 72,
child: ListView.separated(scrollDirection: Axis.horizontal,
itemCount: _images.length + 1,
separatorBuilder: (_, __) => const SizedBox(width: 6),
itemBuilder: (ctx, i) {
if (i < _images.length) {
return Stack(children: [
InkWell(onTap: () => _showImagePreview(i),
child: Container(width: 56, height: 56,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), border: Border.all(color: colors.outlineVariant, width: 0.5)),
clipBehavior: Clip.antiAlias,
child: FadeInLocalImage(path: _images[i], fit: BoxFit.cover))),
Positioned(top: -4, right: -4,
child: GestureDetector(onTap: () => setState(() => _images.removeAt(i)),
child: Container(width: 16, height: 16,
decoration: BoxDecoration(color: colors.surface, shape: BoxShape.circle, border: Border.all(color: colors.outline)),
child: Icon(Icons.close, size: 10, color: colors.onSurface.withValues(alpha: 0.5))))),
]);
}
return InkWell(onTap: _pickImage,
child: Container(width: 56, height: 56,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(6), color: colors.surfaceContainerHighest,
border: Border.all(color: colors.outlineVariant)),
child: Icon(Icons.add_photo_alternate_outlined, size: 20, color: colors.onSurface.withValues(alpha: 0.3))));
}),
);
}
/// 顶部区域:返回 / 年月日 周几 / 保存按钮
Widget _buildHeader(ColorScheme colors, double topPadding) {
return Container(
@@ -498,11 +618,63 @@ class _NoteFormPageState extends State<NoteFormPage> {
return false;
}
/// 离开确认(自动保存后直接返回)
/// 离开确认:有内容时弹窗让用户选择,无内容时直接离开
Future<bool> _confirmLeave() async {
_autoSaveTimer?.cancel();
if (_hasContent()) await _autoSave();
return true;
if (!_hasContent()) return true;
final result = await showDialog<String>(
context: context,
builder: (ctx) {
final colors = Theme.of(ctx).colorScheme;
return AlertDialog(
backgroundColor: colors.surface,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
title: const Text('确认离开', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
content: const Text('内容尚未保存,是否保存后离开?', style: TextStyle(fontSize: 14, height: 1.5)),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, 'discard'),
style: TextButton.styleFrom(
foregroundColor: colors.error,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('丢弃'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, 'cancel'),
style: TextButton.styleFrom(
foregroundColor: colors.onSurface.withValues(alpha: 0.6),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('继续编辑'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, 'save'),
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
foregroundColor: colors.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
child: const Text('保存'),
),
],
actionsPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
);
},
);
if (result == 'save') {
await _autoSave();
return true;
} else if (result == 'discard') {
return true;
}
// 'cancel' 或关闭对话框 → 留在页面
return false;
}
Future<void> _saveNote() async {
@@ -678,7 +850,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
Widget _buildGridImageItem(int index) {
final colors = Theme.of(context).colorScheme;
final size = (MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3;
final size = ((MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3).clamp(60.0, 120.0);
return InkWell(
onTap: () => _showImagePreview(index),
onLongPress: () => _showDeleteImageDialog(index),
@@ -702,7 +874,7 @@ class _NoteFormPageState extends State<NoteFormPage> {
/// 添加图片按钮
Widget _buildAddImageButton() {
final colors = Theme.of(context).colorScheme;
final size = (MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3;
final size = ((MediaQuery.of(context).size.width - 16 * 2 - 10 * 2) / 3).clamp(60.0, 120.0);
return InkWell(
onTap: _pickImage,
borderRadius: BorderRadius.circular(10),