You've already forked SmartisanNote.Remake
"新增:便签编辑页面预览模式功能并完善删除动画"
This commit is contained in:
@@ -13,15 +13,28 @@
|
|||||||
|
|
||||||
<!-- 右侧操作按钮 -->
|
<!-- 右侧操作按钮 -->
|
||||||
<!-- 新建便签 -->
|
<!-- 新建便签 -->
|
||||||
<!-- 新建便签 -->
|
|
||||||
<img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction('create')" />
|
<img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction('create')" />
|
||||||
|
|
||||||
<div v-else-if="actionIcon === 'save'" class="code-fun-flex-row code-fun-items-center right-group">
|
<!-- 编辑模式 -->
|
||||||
|
<div v-else-if="actionIcon === 'edit'" class="code-fun-flex-row code-fun-items-center right-group">
|
||||||
<!-- 插入图片 -->
|
<!-- 插入图片 -->
|
||||||
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_pic.png" @click="handleAction('insertImage')" />
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_pic.png" @click="handleAction('insertImage')" />
|
||||||
<!-- 保存便签 -->
|
<!-- 保存便签 -->
|
||||||
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_save_notes.png" @click="handleAction('save')" />
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_save_notes.png" @click="handleAction('save')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览模式 -->
|
||||||
|
<div v-else-if="actionIcon === 'preview'" class="code-fun-flex-row code-fun-items-center right-group">
|
||||||
|
<!-- 删除便签 -->
|
||||||
|
<img
|
||||||
|
ref="deleteButtonRef"
|
||||||
|
class="image_4"
|
||||||
|
:src="deleteButtonFrame"
|
||||||
|
@click="handleAction('delete')"
|
||||||
|
/>
|
||||||
|
<!-- 分享便签 -->
|
||||||
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_share_notes.png" @click="handleAction('share')" />
|
||||||
|
</div>
|
||||||
<!-- 占位符 -->
|
<!-- 占位符 -->
|
||||||
<div v-else class="image_4-placeholder"></div>
|
<div v-else class="image_4-placeholder"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, defineExpose } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -87,6 +100,9 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const localFolderExpanded = ref(false)
|
const localFolderExpanded = ref(false)
|
||||||
|
const deleteButtonRef = ref(null)
|
||||||
|
const deleteButtonFrame = ref('/assets/icons/drawable-xxhdpi/btn_delete_notes.png')
|
||||||
|
const isDeleteAnimating = ref(false)
|
||||||
|
|
||||||
const folderExpanded = computed(() => {
|
const folderExpanded = computed(() => {
|
||||||
// 优先使用父组件传递的isFolderExpanded状态,否则使用本地状态
|
// 优先使用父组件传递的isFolderExpanded状态,否则使用本地状态
|
||||||
@@ -124,6 +140,42 @@ const handleFolderToggle = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 播放删除按钮动画(只使用存在的帧)
|
||||||
|
const playDeleteAnimation = () => {
|
||||||
|
if (isDeleteAnimating.value) return
|
||||||
|
|
||||||
|
isDeleteAnimating.value = true
|
||||||
|
// 只使用存在的帧编号
|
||||||
|
const frames = [1, 6, 9, 10, 13, 16, 19, 21, 23, 25, 26, 27, 28, 29, 30]
|
||||||
|
let currentFrameIndex = 0
|
||||||
|
|
||||||
|
const playFrame = () => {
|
||||||
|
if (currentFrameIndex < frames.length) {
|
||||||
|
// 格式化帧编号(补零)
|
||||||
|
const frameNumber = frames[currentFrameIndex].toString().padStart(4, '0')
|
||||||
|
deleteButtonFrame.value = `/assets/icons/drawable-xxhdpi/title_bar_del_btn_mov_${frameNumber}.png`
|
||||||
|
currentFrameIndex++
|
||||||
|
|
||||||
|
// 使用setTimeout控制动画播放速度
|
||||||
|
setTimeout(playFrame, 50) // 约20fps,调整速度以适应帧数
|
||||||
|
} else {
|
||||||
|
// 动画播放完成,重置为默认图标
|
||||||
|
setTimeout(() => {
|
||||||
|
deleteButtonFrame.value = '/assets/icons/drawable-xxhdpi/btn_delete_notes.png'
|
||||||
|
isDeleteAnimating.value = false
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始播放动画
|
||||||
|
playFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露播放删除动画的方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
playDeleteAnimation
|
||||||
|
})
|
||||||
|
|
||||||
const handleLeftAction = () => {
|
const handleLeftAction = () => {
|
||||||
// 处理左侧图标点击事件
|
// 处理左侧图标点击事件
|
||||||
if (props.onLeftAction) {
|
if (props.onLeftAction) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
|
||||||
|
|
||||||
// 全局常量定义
|
// 全局常量定义
|
||||||
const DELETE_BUTTON_DELAY = 1000 // 删除按钮延时时间(毫秒),用于防止误触
|
const DELETE_BUTTON_DELAY = 1000 // 删除按钮延时时间(毫秒),用于防止误触
|
||||||
@@ -1325,6 +1325,8 @@ const showToolbar = () => {
|
|||||||
if (isKeyboardVisible.value) {
|
if (isKeyboardVisible.value) {
|
||||||
isToolbarVisible.value = true
|
isToolbarVisible.value = true
|
||||||
}
|
}
|
||||||
|
// 通知父组件编辑器获得焦点
|
||||||
|
emit('focus')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理编辑器失焦
|
// 处理编辑器失焦
|
||||||
@@ -1333,6 +1335,8 @@ const handleBlur = () => {
|
|||||||
// 添加延迟以确保点击工具栏按钮时不会立即隐藏
|
// 添加延迟以确保点击工具栏按钮时不会立即隐藏
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleToolbarFocusOut()
|
handleToolbarFocusOut()
|
||||||
|
// 通知父组件编辑器失去焦点
|
||||||
|
emit('blur')
|
||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1663,7 +1667,7 @@ defineExpose({
|
|||||||
font-size: var(--editor-font-size, 1rem);
|
font-size: var(--editor-font-size, 1rem);
|
||||||
line-height: var(--editor-line-height, 1.6);
|
line-height: var(--editor-line-height, 1.6);
|
||||||
color: var(--note-content);
|
color: var(--note-content);
|
||||||
min-height: 12.5rem;
|
min-height: 100vh;
|
||||||
background-color: var(--background-card);
|
background-color: var(--background-card);
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<ion-page>
|
<ion-page>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Header :onBack="handleCancel" :onAction="handleAction" actionIcon="save" />
|
<!-- 头部:编辑模式 -->
|
||||||
|
<Header v-if="isEditorFocus" :onBack="handleCancel" :onAction="handleAction" actionIcon="edit" />
|
||||||
|
<!-- 头部:预览模式 -->
|
||||||
|
<Header v-else ref="headerRef" :onBack="handleCancel" :onAction="handleAction" actionIcon="preview" />
|
||||||
|
<section>
|
||||||
<!-- 顶部信息栏 -->
|
<!-- 顶部信息栏 -->
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span class="edit-time">{{ formattedTime }}</span>
|
<span class="edit-time">{{ formattedTime }}</span>
|
||||||
@@ -12,14 +15,15 @@
|
|||||||
|
|
||||||
<!-- 富文本编辑器 -->
|
<!-- 富文本编辑器 -->
|
||||||
<div class="editor-container">
|
<div class="editor-container">
|
||||||
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" class="rich-text-editor" />
|
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" @focus="handleEditorFocus" @blur="handleEditorBlur" class="rich-text-editor" />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</ion-page>
|
</ion-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/useAppStore'
|
import { useAppStore } from '../stores/useAppStore'
|
||||||
import Header from '../components/Header.vue'
|
import Header from '../components/Header.vue'
|
||||||
@@ -40,6 +44,9 @@ const noteId = computed(() => props.id || props.noteId)
|
|||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const editorRef = ref(null)
|
const editorRef = ref(null)
|
||||||
|
const headerRef = ref(null)
|
||||||
|
// 是否聚焦编辑器
|
||||||
|
const isEditorFocus = ref(false)
|
||||||
|
|
||||||
// 设置便签内容的函数
|
// 设置便签内容的函数
|
||||||
// 用于在编辑器中加载指定便签的内容
|
// 用于在编辑器中加载指定便签的内容
|
||||||
@@ -170,37 +177,100 @@ const handleSave = async () => {
|
|||||||
// 获取编辑器中的实际内容
|
// 获取编辑器中的实际内容
|
||||||
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
|
// 检查内容是否为空
|
||||||
|
if (isContentEmpty(editorContent)) {
|
||||||
|
// 如果是编辑模式且内容为空,则删除便签
|
||||||
if (isEditing && existingNote) {
|
if (isEditing && existingNote) {
|
||||||
// Update existing note
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('空便签已删除')
|
||||||
|
// 删除后返回便签列表页面
|
||||||
|
router.push('/notes')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (isEditing && existingNote) {
|
||||||
|
// 更新现有便签
|
||||||
await store.updateNote(noteId.value, {
|
await store.updateNote(noteId.value, {
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
})
|
})
|
||||||
|
console.log('便签已保存')
|
||||||
} else {
|
} else {
|
||||||
// Create new note
|
// 创建新便签
|
||||||
await store.addNote({
|
await store.addNote({
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
})
|
})
|
||||||
|
console.log('新便签已创建')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
// 保存后切换到预览模式(失去编辑器焦点)
|
||||||
router.push('/notes')
|
isEditorFocus.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In a full implementation, show an alert or toast
|
// In a full implementation, show an alert or toast
|
||||||
console.log('Save error: Failed to save note. Please try again.')
|
console.log('Save error: Failed to save note. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理取消
|
// 检查内容是否为空(无实质性内容)
|
||||||
const handleCancel = () => {
|
const isContentEmpty = content => {
|
||||||
// Check if there are unsaved changes
|
if (!content) return true
|
||||||
const hasUnsavedChanges = content.value !== (existingNote?.content || '')
|
|
||||||
|
|
||||||
if (hasUnsavedChanges) {
|
// 移除HTML标签和空白字符后检查是否为空
|
||||||
showAlert.value = true
|
const plainText = content.replace(/<[^>]*>/g, '').trim()
|
||||||
} else {
|
if (plainText === '') return true
|
||||||
router.push('/notes')
|
|
||||||
|
// 检查是否只有空的HTML元素
|
||||||
|
const strippedContent = content
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.replace(/<br>/g, '')
|
||||||
|
.replace(/<br\/>/g, '')
|
||||||
|
if (strippedContent === '<p></p>' || strippedContent === '<div></div>' || strippedContent === '') return true
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动保存便签(仅在非主动保存操作时调用)
|
||||||
|
const autoSaveNote = async () => {
|
||||||
|
try {
|
||||||
|
// 获取编辑器中的实际内容
|
||||||
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
// 检查内容是否为空,如果为空则删除便签
|
||||||
|
if (isContentEmpty(editorContent)) {
|
||||||
|
// 删除便签
|
||||||
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('空便签已自动删除')
|
||||||
|
} else if (editorContent !== (existingNote?.content || '')) {
|
||||||
|
// 更新现有便签(仅当内容有变化时)
|
||||||
|
await store.updateNote(noteId.value, {
|
||||||
|
content: editorContent,
|
||||||
|
})
|
||||||
|
console.log('便签已自动保存')
|
||||||
|
}
|
||||||
|
} else if (!isContentEmpty(editorContent)) {
|
||||||
|
// 创建新便签(仅当有内容时)
|
||||||
|
// 检查是否已经存在相同内容的便签以避免重复创建
|
||||||
|
const existingNotes = store.notes.filter(n => n.content === editorContent && !n.isDeleted)
|
||||||
|
if (existingNotes.length === 0) {
|
||||||
|
await store.addNote({
|
||||||
|
content: editorContent,
|
||||||
|
isStarred: false,
|
||||||
|
})
|
||||||
|
console.log('新便签已自动保存')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自动保存失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = async () => {
|
||||||
|
// 自动保存便签
|
||||||
|
await autoSaveNote()
|
||||||
|
|
||||||
|
// 直接导航回便签列表页面,因为已经处理了保存或删除逻辑
|
||||||
|
router.push('/notes')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理创建(用于新建便签)
|
// 处理创建(用于新建便签)
|
||||||
@@ -209,14 +279,17 @@ const handleCreate = async () => {
|
|||||||
// 获取编辑器中的实际内容
|
// 获取编辑器中的实际内容
|
||||||
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
// Create new note
|
// 只有当有内容时才创建新便签
|
||||||
|
if (!isContentEmpty(editorContent)) {
|
||||||
await store.addNote({
|
await store.addNote({
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
})
|
})
|
||||||
|
console.log('新便签已创建')
|
||||||
|
}
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
// 创建后切换到预览模式(失去编辑器焦点)
|
||||||
router.push('/notes')
|
isEditorFocus.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In a full implementation, show an alert or toast
|
// In a full implementation, show an alert or toast
|
||||||
console.log('Create error: Failed to create note. Please try again.')
|
console.log('Create error: Failed to create note. Please try again.')
|
||||||
@@ -235,12 +308,92 @@ const handleAction = actionType => {
|
|||||||
// 通过editorRef调用RichTextEditor组件的方法来插入图片
|
// 通过editorRef调用RichTextEditor组件的方法来插入图片
|
||||||
editorRef.value.insertImage()
|
editorRef.value.insertImage()
|
||||||
}
|
}
|
||||||
|
} else if (actionType === 'delete') {
|
||||||
|
// 删除便签
|
||||||
|
handleDelete()
|
||||||
|
} else if (actionType === 'share') {
|
||||||
|
// 分享便签
|
||||||
|
handleShare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setShowAlert = value => {
|
const setShowAlert = value => {
|
||||||
showAlert.value = value
|
showAlert.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理编辑器获得焦点
|
||||||
|
const handleEditorFocus = () => {
|
||||||
|
isEditorFocus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑器失去焦点
|
||||||
|
const handleEditorBlur = () => {
|
||||||
|
isEditorFocus.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除便签
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
// 播放删除按钮动画
|
||||||
|
if (headerRef.value && headerRef.value.playDeleteAnimation) {
|
||||||
|
headerRef.value.playDeleteAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待动画播放完成后再执行删除操作
|
||||||
|
// 15帧 * 50ms = 750ms,再加上一些缓冲时间
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// 删除便签
|
||||||
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('便签已删除')
|
||||||
|
// 返回便签列表页面
|
||||||
|
router.push('/notes')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除便签失败:', error)
|
||||||
|
}
|
||||||
|
}, 800) // 等待约800ms让动画播放完成
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分享便签
|
||||||
|
const handleShare = () => {
|
||||||
|
// 获取编辑器中的实际内容
|
||||||
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
|
// 移除HTML标签,获取纯文本内容用于分享
|
||||||
|
const plainText = editorContent.replace(/<[^>]*>/g, '').trim()
|
||||||
|
|
||||||
|
if (plainText) {
|
||||||
|
// 在实际应用中,这里会调用设备的分享功能
|
||||||
|
// 为了演示,我们使用Web Share API(如果支持)
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator
|
||||||
|
.share({
|
||||||
|
title: '分享便签',
|
||||||
|
text: plainText,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('分享取消或失败:', error)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果不支持Web Share API,可以复制到剪贴板
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(plainText)
|
||||||
|
.then(() => {
|
||||||
|
console.log('内容已复制到剪贴板')
|
||||||
|
// 在实际应用中,这里会显示一个提示消息
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('复制失败:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件卸载前自动保存或删除
|
||||||
|
onBeforeUnmount(async () => {
|
||||||
|
await autoSaveNote()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -249,12 +402,20 @@ const setShowAlert = value => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
section {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-container {
|
.editor-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--background-card);
|
background-color: var(--background-card);
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-editor {
|
.rich-text-editor {
|
||||||
|
|||||||
Reference in New Issue
Block a user