新增 文件夹管理功能:实现文件夹选择模式、添加、编辑和删除功能

This commit is contained in:
yuantao
2025-10-23 18:35:23 +08:00
parent ff4adcd949
commit 2eb7eb8618
6 changed files with 1880 additions and 1024 deletions

View File

@@ -0,0 +1,249 @@
<template>
<div v-if="visible" class="pd-mask" @click="handleMaskClick">
<div class="pd-confirm" @click.stop>
<div class="pd-blur"></div>
<div class="pd-plan"></div>
<h2 class="pd-title" v-if="title">{{ title }}</h2>
<p class="pd-message">{{ message }}</p>
<div class="pd-buttons">
<button v-if="showConfirm" class="pd-button pd-confirm-btn" @click="handleConfirm">
{{ confirmText }}
</button>
<button v-if="showCancel" class="pd-button pd-cancel" @click="handleCancel">
{{ cancelText }}
</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
const props = defineProps({
title: {
type: String,
default: '',
},
message: {
type: String,
default: '',
},
confirmText: {
type: String,
default: '确认',
},
cancelText: {
type: String,
default: '取消',
},
showConfirm: {
type: Boolean,
default: true,
},
showCancel: {
type: Boolean,
default: true,
},
maskClosable: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
const visible = defineModel('visible', { type: Boolean, default: false })
// Promise 控制变量
let resolvePromise, rejectPromise
// 返回 Promise 的方法,模拟原生 confirm 行为
const show = (options = {}) => {
// 更新 props 值
Object.assign(props, options)
// 显示对话框
visible.value = true
emit('update:visible', true)
// 返回 Promise
return new Promise((resolve, reject) => {
resolvePromise = resolve
rejectPromise = reject
})
}
const handleConfirm = () => {
emit('confirm')
visible.value = false
emit('update:visible', false)
// 解决 Promise
if (resolvePromise) {
resolvePromise()
resolvePromise = null
rejectPromise = null
}
}
const handleCancel = () => {
emit('cancel')
visible.value = false
emit('update:visible', false)
// 拒绝 Promise
if (rejectPromise) {
rejectPromise()
resolvePromise = null
rejectPromise = null
}
}
const handleMaskClick = () => {
if (props.maskClosable) {
handleCancel() // 点击遮罩相当于取消
}
}
// 添加/移除 body 滚动锁定
const lockBodyScroll = () => {
document.body.style.overflow = 'hidden'
}
const unlockBodyScroll = () => {
document.body.style.overflow = ''
}
onMounted(() => {
if (visible.value) {
lockBodyScroll()
}
})
onUnmounted(() => {
unlockBodyScroll()
})
// 监听 visible 变化
watch(visible, newVal => {
if (newVal) {
lockBodyScroll()
} else {
unlockBodyScroll()
}
})
// 暴露 show 方法给父组件使用
defineExpose({
show,
})
</script>
<style scoped>
.pd-mask {
position: fixed;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999999999;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: inherit;
background: var(--confirmBg, inherit);
}
.pd-confirm {
background: inherit;
background: var(--confirmTheme, #fff);
text-align: center;
color: var(--confirmColor, #636363);
font-size: var(--confirmFontSize, 1rem);
max-width: 75vw;
min-width: 20em;
box-shadow: 0px 35px 35px -10px rgba(0, 0, 0, 0.33);
border-radius: 10px;
position: relative;
white-space: break-spaces;
word-break: break-all;
overflow: hidden;
}
.pd-blur,
.pd-plan {
position: absolute;
inset: 0;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.pd-plan {
background: inherit;
background: rgba(255, 255, 255, 0.66);
filter: blur(10px) saturate(2);
}
.pd-title {
position: relative;
font-size: 1.3em;
min-height: 1em;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
margin: 0;
padding: 0.5em 0;
}
.pd-message {
position: relative;
border-top: 1px solid var(--confirmBtnBorder, #f1f1f1);
width: 100%;
max-height: 70vh;
border-bottom: 1px solid var(--confirmBtnBorder, #f1f1f1);
margin: 0 auto;
box-sizing: border-box;
padding: 2em 10%;
line-height: 1.4;
overflow: auto;
}
.pd-buttons {
display: flex;
}
.pd-button {
position: relative;
width: 100%;
font-size: 1em;
appearance: none;
background: var(--confirmBtnBg, #fafafa);
color: var(--confirmBtnColor, #636363);
border: none;
border-right: 1px solid var(--confirmBtnBorder, #f1f1f1);
padding: 1em 0;
cursor: pointer;
outline: none;
}
/* 当同时显示确认和取消按钮时每个按钮占50%宽度 */
.pd-button:first-child:nth-last-child(2),
.pd-button:first-child:nth-last-child(2) ~ .pd-button {
width: 50%;
}
/* 当只显示一个按钮时该按钮占100%宽度 */
.pd-button:first-child:nth-last-child(1) {
width: 100%;
border-right: none;
}
/* 只有最后一个按钮没有右边框 */
.pd-button:last-child {
border-right: none;
}
</style>

View File

@@ -1,77 +1,168 @@
<template> <template>
<div @click="onPress" class="code-fun-flex-row code-fun-items-center code-fun-relative folder-item"> <div @click="onPress" class="code-fun-flex-row code-fun-items-center code-fun-relative code-fun-justify-between folder-item">
<img class="folder-icon" :src="iconSrc" /> <div class="code-fun-flex-row code-fun-items-center">
<span class="folder-name">{{ name }}</span> <!-- 文件夹图标或复选框 -->
<span class="folder-count">{{ noteCount }}</span> <div v-if="isSelectionMode && showDeleteButton" class="folder-checkbox" @click.stop="onCheckboxClick">
</div> <img :src="isChecked ? '/assets/icons/drawable-xxhdpi/check_box_on.png' : '/assets/icons/drawable-xxhdpi/check_box_off.png'" class="checkbox-icon" />
</template> </div>
<script setup> <img v-else class="folder-icon" :src="iconSrc" />
import { computed } from 'vue' <span class="folder-name">{{ name }}</span>
<span class="folder-count">{{ noteCount }}</span>
const props = defineProps({ </div>
id: {
type: String, <!-- 编辑按钮仅在选择模式下对自定义文件夹显示 -->
required: true, <div v-if="showDeleteButton && isSelectionMode" class="folder-actions">
}, <img class="edit-icon" src="/assets/icons/drawable-xxhdpi/icon_folder_rename.png" @click.stop="onEdit" />
name: { </div>
type: String, </div>
required: true, </template>
},
noteCount: { <script setup>
type: Number, import { computed } from 'vue'
required: true,
}, const props = defineProps({
onPress: { id: {
type: Function, type: String,
required: true, required: true,
}, },
isSelected: {
type: Boolean, name: {
default: false, type: String,
}, required: true,
}) },
const iconSrc = computed(() => { noteCount: {
switch (props.id) { type: Number,
case 'all': required: true,
return 'assets/icons/drawable-xxhdpi/icon_folder_all.png' },
case 'starred':
return 'assets/icons/drawable-xxhdpi/icon_folder_favorite.png' onPress: {
case 'trash': type: Function,
return 'assets/icons/drawable-xxhdpi/icon_folder_trash.png' required: true,
case 'archive': },
return 'assets/icons/drawable-xxhdpi/icon_folder_document.png'
default: onEdit: {
return 'assets/icons/drawable-xxhdpi/icon_folder_document.png' type: Function,
} default: null,
}) },
</script>
onDelete: {
<style scoped> type: Function,
.folder-item { default: null,
padding: 0.3rem 0; },
background-color: #00000000;
} isSelected: {
type: Boolean,
.folder-icon { default: false,
width: 1.8rem; },
height: 1.8rem;
flex-shrink: 0; isSelectionMode: {
margin-inline: 0.3rem; type: Boolean,
}
default: false,
.folder-name { },
font-size: 0.9rem;
line-height: 1.52rem; isChecked: {
color: #9b9b9b; type: Boolean,
}
default: false,
.folder-count { },
font-size: 0.8rem;
line-height: 1.16rem; onCheckboxClick: {
margin-left: 0.7rem; type: Function,
margin-top: 0.2rem;
color: #b8b8b8; default: null,
} },
</style> })
const iconSrc = computed(() => {
switch (props.id) {
case 'all':
return 'assets/icons/drawable-xxhdpi/icon_folder_all.png'
case 'starred':
return 'assets/icons/drawable-xxhdpi/icon_folder_favorite.png'
case 'trash':
return 'assets/icons/drawable-xxhdpi/icon_folder_trash.png'
case 'archive':
return 'assets/icons/drawable-xxhdpi/icon_folder_document.png'
default:
return 'assets/icons/drawable-xxhdpi/icon_folder_document.png'
}
})
// 仅对自定义文件夹显示删除按钮
const showDeleteButton = computed(() => {
return !['all', 'starred', 'trash', 'archive'].includes(props.id)
})
</script>
<style lang="less" scoped>
.folder-item {
padding: 0.3rem 0;
background-color: #00000000;
}
.folder-icon {
width: 1.8rem;
height: 1.8rem;
flex-shrink: 0;
margin-inline: 0.3rem;
}
.folder-name {
font-size: 0.9rem;
line-height: 1.52rem;
color: #9b9b9b;
}
.folder-count {
font-size: 0.8rem;
line-height: 1.16rem;
margin-left: 0.7rem;
margin-top: 0.2rem;
color: #b8b8b8;
}
.folder-actions {
display: flex;
margin-left: auto;
margin-right: 0.5rem;
}
.edit-icon,
.delete-icon {
width: 1.5rem;
height: 1.5rem;
margin: 0 0.2rem;
opacity: 0.5;
}
.edit-icon:hover,
.delete-icon:hover {
opacity: 1;
}
.folder-checkbox {
display: flex;
align-items: center;
justify-content: center;
width: 1.8rem;
height: 1.8rem;
flex-shrink: 0;
margin-inline: 0.3rem;
}
.checkbox-icon {
width: 70%;
height: 70%;
}
</style>

View File

@@ -1,135 +1,554 @@
<template> <template>
<div class="code-fun-flex-col page"> <div class="code-fun-flex-col page">
<!-- 全部便签文件夹项 --> <!-- 全部便签文件夹项 -->
<FolderItem id="all" name="全部便签" :noteCount="allCount" :isSelected="selectedFolder === 'all'" :onPress="handleAllClick" />
<FolderItem id="all" name="全部便签" :noteCount="allCount" :isSelected="selectedFolder === 'all'" :onPress="handleAllClick" />
<!-- 加星便签文件夹项 -->
<FolderItem id="starred" name="加星便签" :noteCount="starredCount" :isSelected="selectedFolder === 'starred'" :onPress="handleStarredClick" /> <!-- 加星便签文件夹项 -->
<!-- 回收站文件夹项 --> <FolderItem id="starred" name="加星便签" :noteCount="starredCount" :isSelected="selectedFolder === 'starred'" :onPress="handleStarredClick" />
<FolderItem id="trash" name="回收站" :noteCount="trashCount" :isSelected="selectedFolder === 'trash'" :onPress="handleTrashClick" />
<!-- 回收站文件夹项 -->
<!-- 同步信息区域 -->
<div class="code-fun-flex-col code-fun-justify-start section_7"> <FolderItem id="trash" name="回收站" :noteCount="trashCount" :isSelected="selectedFolder === 'trash'" :onPress="handleTrashClick" />
<div class="code-fun-flex-row code-fun-justify-between code-fun-items-center section_8">
<div class="code-fun-flex-row code-fun-items-center"> <!-- 自定义文件夹项 -->
<img class="code-fun-shrink-0 image_7" :src="`assets/icons/drawable-xxhdpi/btn_edit_folder.png`" />
<span class="text_11 code-fun-ml-10">上次同步:{{ lastSyncTime }}</span> <FolderItem
</div> v-for="folder in customFolders"
<div class="code-fun-flex-row code-fun-items-center"> :key="folder.id"
<img class="image_8 code-fun-ml-12" :src="`assets/icons/drawable-xxhdpi/btn_add_folder.png`" @click="handleAddFolder" /> :id="folder.id"
</div> :name="folder.name"
</div> :noteCount="getFolderNoteCount(folder.id)"
</div> :isSelected="selectedFolder === folder.id"
</div> :onPress="() => handleFolderClick(folder.id)"
</template> :onEdit="() => handleEditFolder(folder.id)"
:onDelete="() => handleDeleteFolder(folder.id)"
<script setup> :isSelectionMode="isSelectionMode"
import { computed } from 'vue' :isChecked="selectedFolders.includes(folder.id)"
import FolderItem from './FolderItem.vue' :onCheckboxClick="() => toggleFolderSelection(folder.id)" />
const props = defineProps({ <div class="code-fun-flex-col code-fun-justify-start section_7">
allCount: { <div class="code-fun-flex-row code-fun-justify-between code-fun-items-center section_8">
type: Number, <div class="code-fun-flex-row code-fun-items-center">
default: 0, <!-- 切换文件夹选择模式按钮 -->
},
starredCount: { <img class="code-fun-shrink-0 image_7" :src="isSelectionMode ? 'assets/icons/drawable-xxhdpi/btn_back_black.png' : 'assets/icons/drawable-xxhdpi/btn_edit_folder.png'" @click="toggleSelectionMode" />
type: Number,
default: 0, <!-- 同步信息区域 -->
},
trashCount: { <span class="text_11 code-fun-ml-10">上次同步:{{ lastSyncTime }}</span>
type: Number, </div>
default: 0,
}, <div class="code-fun-flex-row code-fun-items-center">
archiveCount: { <!-- 新建文件夹按钮 / 删除按钮 -->
type: Number,
default: 0, <img
}, class="image_8 code-fun-ml-12"
selectedFolder: { :src="isSelectionMode ? 'assets/icons/drawable-xxhdpi/icon_folder_trash.png' : 'assets/icons/drawable-xxhdpi/btn_add_folder.png'"
type: String, @click="isSelectionMode ? handleDeleteSelectedFolders() : handleAddFolder" />
default: '', </div>
}, </div>
lastSyncTime: { </div>
type: String,
default: '10/10上午9:28', <!-- 添加文件夹的模态框 -->
},
onAllClick: { <div v-if="showAddFolderModal" class="modal-overlay" @click="showAddFolderModal = false">
type: Function, <div class="modal-content" @click.stop>
default: null, <h3>添加文件夹</h3>
},
onStarredClick: { <input v-model="newFolderName" placeholder="请输入文件夹名称" class="folder-input" @keyup.enter="confirmAddFolder" ref="folderInput" />
type: Function,
default: null, <div class="modal-actions">
}, <button @click="showAddFolderModal = false" class="cancel-btn">取消</button>
onTrashClick: {
type: Function, <button @click="confirmAddFolder" class="confirm-btn">确定</button>
default: null, </div>
}, </div>
onArchiveClick: { </div>
type: Function,
default: null, <!-- 重命名文件夹的模态框 -->
},
onAddFolder: { <div v-if="showRenameFolderModal" class="modal-overlay" @click="showRenameFolderModal = false">
type: Function, <div class="modal-content" @click.stop>
default: null, <h3>重命名文件夹</h3>
},
}) <input v-model="renameFolderName" placeholder="请输入文件夹名称" class="folder-input" @keyup.enter="confirmRenameFolder" ref="folderInput" />
const handleAllClick = () => { <div class="modal-actions">
if (props.onAllClick) { <button @click="showRenameFolderModal = false" class="cancel-btn">取消</button>
props.onAllClick()
} <button @click="confirmRenameFolder" class="confirm-btn">确定</button>
} </div>
</div>
const handleStarredClick = () => { </div>
if (props.onStarredClick) {
props.onStarredClick() <!-- 确认删除文件夹的模态框 -->
}
} <div v-if="showDeleteConfirmModal" class="modal-overlay" @click="showDeleteConfirmModal = false">
<div class="modal-content" @click.stop>
const handleTrashClick = () => { <h3>删除文件夹</h3>
if (props.onTrashClick) {
props.onTrashClick() <p>确定要删除选中的 {{ selectedFolders.length }} 个文件夹吗文件夹中的便签将移至"全部便签"</p>
}
} <div class="modal-actions">
<button @click="showDeleteConfirmModal = false" class="cancel-btn">取消</button>
const handleArchiveClick = () => {
if (props.onArchiveClick) { <button @click="confirmDeleteSelectedFolders" class="confirm-btn">确定</button>
props.onArchiveClick() </div>
} </div>
} </div>
</div>
const handleAddFolder = event => { </template>
// 阻止事件冒泡到父元素
event.stopPropagation() <script setup>
if (props.onAddFolder) { import { ref, computed, nextTick } from 'vue'
props.onAddFolder()
} import { useAppStore } from '../stores/useAppStore'
}
</script> import FolderItem from './FolderItem.vue'
<style lang="less" scoped> const store = useAppStore()
.page {
.section_7 { const props = defineProps({
margin-top: 2rem; allCount: {
background-color: #00000000; type: Number,
.section_8 {
padding: 0.29rem 0.92rem; default: 0,
background-color: #f4f4f4; },
border: solid 0.063rem #f0ece7;
.image_7, starredCount: {
.image_8 { type: Number,
border-radius: 0.63rem;
width: 2rem; default: 0,
height: 2rem; },
object-fit: contain;
} trashCount: {
.text_11 { type: Number,
color: #cacaca;
font-size: 0.7rem; default: 0,
line-height: 1.16rem; },
}
} archiveCount: {
} type: Number,
}
</style> default: 0,
},
selectedFolder: {
type: String,
default: '',
},
lastSyncTime: {
type: String,
default: '10/10上午9:28',
},
onAllClick: {
type: Function,
default: null,
},
onStarredClick: {
type: Function,
default: null,
},
onTrashClick: {
type: Function,
default: null,
},
onArchiveClick: {
type: Function,
default: null,
},
onAddFolder: {
type: Function,
default: null,
},
onFolderClick: {
type: Function,
default: null,
},
})
// 添加文件夹相关状态
const showAddFolderModal = ref(false)
const newFolderName = ref('')
const folderInput = ref(null)
// 重命名文件夹相关状态
const showRenameFolderModal = ref(false)
const renameFolderId = ref(null)
const renameFolderName = ref('')
// 选择模式相关状态
const isSelectionMode = ref(false)
const selectedFolders = ref([])
const showDeleteConfirmModal = ref(false)
// 计算自定义文件夹(排除系统文件夹)
const customFolders = computed(() => {
return store.folders.filter(folder => !['all', 'starred', 'trash', 'archive'].includes(folder.id))
})
// 获取文件夹中的便签数量
const getFolderNoteCount = folderId => {
return store.notes.filter(note => note.folderId === folderId && !note.isDeleted).length
}
// 切换选择模式
const toggleSelectionMode = () => {
isSelectionMode.value = !isSelectionMode.value
// 退出选择模式时清空选中项
if (!isSelectionMode.value) {
selectedFolders.value = []
}
}
// 切换文件夹选中状态
const toggleFolderSelection = folderId => {
if (!isSelectionMode.value) return
const index = selectedFolders.value.indexOf(folderId)
if (index > -1) {
// 已选中,取消选中
selectedFolders.value.splice(index, 1)
} else {
// 未选中,添加选中
selectedFolders.value.push(folderId)
}
}
// 处理文件夹点击
const handleFolderClick = folderId => {
if (isSelectionMode.value) {
toggleFolderSelection(folderId)
} else {
if (props.onFolderClick) {
props.onFolderClick(folderId)
}
}
}
// 处理删除选中的文件夹
const handleDeleteSelectedFolders = () => {
if (selectedFolders.value.length === 0) return
showDeleteConfirmModal.value = true
}
// 确认删除选中的文件夹
const confirmDeleteSelectedFolders = async () => {
try {
// 删除选中的文件夹
for (const folderId of selectedFolders.value) {
// 跳过系统文件夹
if (['all', 'starred', 'trash', 'archive'].includes(folderId)) continue
await store.deleteFolder(folderId)
}
// 清空选中项并退出选择模式
selectedFolders.value = []
isSelectionMode.value = false
showDeleteConfirmModal.value = false
} catch (error) {
console.error('删除文件夹失败:', error)
}
}
// 处理删除文件夹
const handleDeleteFolder = folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
// 确认删除
if (confirm(`确定要删除文件夹 "${getFolderName(folderId)}" 吗?文件夹中的便签将移至"全部便签"。`)) {
try {
store.deleteFolder(folderId)
} catch (error) {
console.error('删除文件夹失败:', error)
}
}
}
// 处理编辑文件夹
const handleEditFolder = folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
const folder = store.folders.find(f => f.id === folderId)
if (folder) {
renameFolderId.value = folderId
renameFolderName.value = folder.name
showRenameFolderModal.value = true
// 在下次DOM更新后聚焦输入框
nextTick(() => {
if (folderInput.value) {
folderInput.value.focus()
}
})
}
}
// 确认重命名文件夹
const confirmRenameFolder = async () => {
if (renameFolderName.value.trim() && renameFolderId.value) {
try {
await store.updateFolder(renameFolderId.value, { name: renameFolderName.value.trim() })
showRenameFolderModal.value = false
renameFolderId.value = null
renameFolderName.value = ''
} catch (error) {
console.error('重命名文件夹失败:', error)
}
}
}
// 获取文件夹名称
const getFolderName = folderId => {
const folder = store.folders.find(f => f.id === folderId)
return folder ? folder.name : ''
}
const handleAllClick = () => {
if (isSelectionMode.value) {
toggleFolderSelection('all')
} else {
if (props.onAllClick) {
props.onAllClick()
}
}
}
const handleStarredClick = () => {
if (isSelectionMode.value) {
toggleFolderSelection('starred')
} else {
if (props.onStarredClick) {
props.onStarredClick()
}
}
}
const handleTrashClick = () => {
if (isSelectionMode.value) {
toggleFolderSelection('trash')
} else {
if (props.onTrashClick) {
props.onTrashClick()
}
}
}
const handleArchiveClick = () => {
if (isSelectionMode.value) {
toggleFolderSelection('archive')
} else {
if (props.onArchiveClick) {
props.onArchiveClick()
}
}
}
const handleAddFolder = event => {
// 阻止事件冒泡到父元素
event.stopPropagation()
showAddFolderModal.value = true
newFolderName.value = ''
// 在下次DOM更新后聚焦输入框
nextTick(() => {
if (folderInput.value) {
folderInput.value.focus()
}
})
}
const confirmAddFolder = async () => {
if (newFolderName.value.trim()) {
try {
const newFolder = {
name: newFolderName.value.trim(),
id: `folder_${Date.now()}`, // 生成唯一ID
createdAt: new Date().toISOString(),
}
await store.addFolder(newFolder)
showAddFolderModal.value = false
newFolderName.value = ''
} catch (error) {
console.error('添加文件夹失败:', error)
}
}
}
</script>
<style lang="less" scoped>
.page {
.section_7 {
margin-top: 2rem;
background-color: #00000000;
.section_8 {
padding: 0.29rem 0.92rem;
background-color: #f4f4f4;
border: solid 0.063rem #f0ece7;
.image_7,
.image_8 {
border-radius: 0.63rem;
width: 2.2rem;
height: 1.75rem;
object-fit: contain;
background: url(/assets/icons/drawable-xxhdpi/folder_bottom_button_normal.9.png), #fdfbfb;
}
.text_11 {
color: #cacaca;
font-size: 0.7rem;
line-height: 1.16rem;
}
}
}
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1001;
}
.modal-content {
background: white;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
width: 80%;
max-width: 300px;
h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
}
.folder-input {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 0.3rem;
font-size: 1rem;
margin-bottom: 1.5rem;
box-sizing: border-box;
}
.modal-actions {
display: flex;
justify-content: space-between;
button {
flex: 1;
padding: 0.8rem;
border: none;
border-radius: 0.3rem;
font-size: 1rem;
cursor: pointer;
margin: 0 0.5rem;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.cancel-btn {
background-color: #f0f0f0;
color: #333;
&:hover {
background-color: #e0e0e0;
}
}
.confirm-btn {
background-color: #4a90e2;
color: white;
&:hover {
background-color: #357ae8;
}
}
}
}
</style>

View File

@@ -24,7 +24,9 @@
:selectedFolder="currentFolder" :selectedFolder="currentFolder"
:onAllClick="handleAllNotesClick" :onAllClick="handleAllNotesClick"
:onStarredClick="handleStarredNotesClick" :onStarredClick="handleStarredNotesClick"
:onTrashClick="handleTrashNotesClick" /> :onTrashClick="handleTrashNotesClick"
:onFolderClick="handleFolderClick"
:onAddFolder="handleAddFolder" />
</div> </div>
</transition> </transition>
<!-- 点击外部区域收起文件夹列表的覆盖层 --> <!-- 点击外部区域收起文件夹列表的覆盖层 -->
@@ -168,7 +170,9 @@ const headerTitle = computed(() => {
case 'trash': case 'trash':
return '回收站' return '回收站'
default: default:
return '全部便签' // 查找自定义文件夹的名称
const folder = store.folders.find(f => f.id === currentFolder.value)
return folder ? folder.name : '全部便签'
} }
}) })
@@ -279,6 +283,17 @@ const handleTrashNotesClick = () => {
setIsFolderExpanded(false) setIsFolderExpanded(false)
} }
const handleFolderClick = (folderId) => {
setCurrentFolder(folderId)
setIsFolderExpanded(false)
}
const handleAddFolder = () => {
// 文件夹添加功能已在FolderManage组件中实现
// 这里只需关闭文件夹列表
setIsFolderExpanded(false)
}
const handleFolderPress = () => { const handleFolderPress = () => {
// 使用vue-router导航到文件夹页面 // 使用vue-router导航到文件夹页面
router.push('/folders') router.push('/folders')

View File

@@ -1,336 +1,383 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as storage from '../utils/indexedDBStorage' import * as storage from '../utils/indexedDBStorage'
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils' import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
/** /**
* 应用状态管理Store * 应用状态管理Store
* 使用Pinia进行状态管理包含便签、文件夹和设置数据 * 使用Pinia进行状态管理包含便签、文件夹和设置数据
*/ */
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
/** /**
* 状态定义 * 状态定义
* 包含应用的核心数据:便签列表、文件夹列表和设置 * 包含应用的核心数据:便签列表、文件夹列表和设置
*/ */
state: () => ({ state: () => ({
notes: [], // 便签列表 notes: [], // 便签列表
folders: [], // 文件夹列表 folders: [], // 文件夹列表
settings: { cloudSync: false, darkMode: false }, // 应用设置 settings: { cloudSync: false, darkMode: false }, // 应用设置
}), }),
/** /**
* 计算属性 * 计算属性
* 基于状态派生的计算值 * 基于状态派生的计算值
*/ */
getters: { getters: {
/** /**
* 计算加星便签数量 * 计算加星便签数量
* @param {Object} state - 当前状态对象 * @param {Object} state - 当前状态对象
* @returns {number} 加星便签的数量 * @returns {number} 加星便签的数量
*/ */
starredNotesCount: state => { starredNotesCount: state => {
return state.notes.filter(note => note.isStarred).length return state.notes.filter(note => note.isStarred).length
}, },
/** /**
* 计算所有便签数量 * 计算所有便签数量
* @param {Object} state - 当前状态对象 * @param {Object} state - 当前状态对象
* @returns {number} 所有便签的数量 * @returns {number} 所有便签的数量
*/ */
allNotesCount: state => { allNotesCount: state => {
return state.notes.length return state.notes.length
}, },
}, },
/** /**
* 状态变更操作 * 状态变更操作
* 包含所有修改状态的方法 * 包含所有修改状态的方法
*/ */
actions: { actions: {
/** /**
* 初始化数据 * 初始化数据
* 从Storage加载便签、文件夹和设置数据 * 从Storage加载便签、文件夹和设置数据
* 如果没有数据则加载预设的mock数据 * 如果没有数据则加载预设的mock数据
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadData() { async loadData() {
try { try {
// 从Storage加载数据 // 从Storage加载数据
const loadedNotes = await storage.getNotes() const loadedNotes = await storage.getNotes()
const loadedFolders = await storage.getFolders() const loadedFolders = await storage.getFolders()
const loadedSettings = await storage.getSettings() const loadedSettings = await storage.getSettings()
// 如果没有数据则加载mock数据 // 如果没有数据则加载mock数据
if (loadedNotes.length === 0 && loadedFolders.length === 0) { if (loadedNotes.length === 0 && loadedFolders.length === 0) {
this.loadMockData() this.loadMockData()
} else { } else {
// 否则使用加载的数据 // 否则使用加载的数据
this.notes = loadedNotes this.notes = loadedNotes
this.folders = loadedFolders this.folders = loadedFolders
this.settings = loadedSettings this.settings = loadedSettings
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error) console.error('Error loading data:', error)
} }
}, },
/** /**
* 加载预设的mock数据 * 加载预设的mock数据
* 用于开发和演示目的,提供示例便签、文件夹和设置 * 用于开发和演示目的,提供示例便签、文件夹和设置
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadMockData() { async loadMockData() {
// Mock notes - 使用固定的日期值以避免每次运行时变化 // Mock notes - 使用固定的日期值以避免每次运行时变化
const fixedCurrentDate = '2025-10-12T10:00:00.000Z'; const fixedCurrentDate = '2025-10-12T10:00:00.000Z';
// 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签 // 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签
const mockNotes = [ const mockNotes = [
{ {
id: '1', id: '1',
title: '欢迎使用锤子便签', title: '欢迎使用锤子便签',
content: '<p>这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。</p><br><h2>功能介绍</h2><p>1. 创建和编辑便签<br>2. 为便签加星和置顶<br>3. 将便签分类到文件夹<br>4. 搜索便签内容<br>5. 回收站功能</p><br><h2>编辑器功能演示</h2><br><h2>标题格式</h2><p>点击标题按钮可创建居中的标题</p><br><h2>待办事项</h2><div class="todo-container"><div class="todo-icon"></div><div contenteditable="true" class="todo-content">这是一个待办事项</div></div><div class="todo-container"><div class="todo-icon completed"></div><div contenteditable="true" class="todo-content" style="color: var(--text-tertiary); text-decoration: line-through;">这是一个已完成的待办事项</div></div><br><h2>列表格式</h2><ul><li>无序列表项1</li><li>无序列表项2</li></ul><br><h2>文本格式</h2><p><strong>加粗文本</strong></p><br><h2>引用格式</h2><div class="quote-container"><div class="quote-icon"></div><div class="quote-content">这是一段引用文本<br>可以用来引用他人的话语</div></div><br><h2>图片</h2><p>点击图片按钮可以插入图片,长按图片可以拖拽排序</p>', content: '<p>这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。</p><br><h2>功能介绍</h2><p>1. 创建和编辑便签<br>2. 为便签加星和置顶<br>3. 将便签分类到文件夹<br>4. 搜索便签内容<br>5. 回收站功能</p><br><h2>编辑器功能演示</h2><br><h2>标题格式</h2><p>点击标题按钮可创建居中的标题</p><br><h2>待办事项</h2><div class="todo-container"><div class="todo-icon"></div><div contenteditable="true" class="todo-content">这是一个待办事项</div></div><div class="todo-container"><div class="todo-icon completed"></div><div contenteditable="true" class="todo-content" style="color: var(--text-tertiary); text-decoration: line-through;">这是一个已完成的待办事项</div></div><br><h2>列表格式</h2><ul><li>无序列表项1</li><li>无序列表项2</li></ul><br><h2>文本格式</h2><p><strong>加粗文本</strong></p><br><h2>引用格式</h2><div class="quote-container"><div class="quote-icon"></div><div class="quote-content">这是一段引用文本<br>可以用来引用他人的话语</div></div><br><h2>图片</h2><p>点击图片按钮可以插入图片,长按图片可以拖拽排序</p>',
createdAt: fixedCurrentDate, createdAt: fixedCurrentDate,
updatedAt: fixedCurrentDate, updatedAt: fixedCurrentDate,
folderId: null, folderId: null,
isStarred: true, // 加星便签 isStarred: true, // 加星便签
isTop: true, // 置顶便签 isTop: true, // 置顶便签
hasImage: true, // 包含图片 hasImage: true, // 包含图片
isDeleted: false, // 未删除 isDeleted: false, // 未删除
deletedAt: null, deletedAt: null,
} }
] ]
// Mock folders - 使用固定的日期值 // Mock folders - 使用固定的日期值
// 预设的文件夹示例数据 // 预设的文件夹示例数据
const mockFolders = [ const mockFolders = [
{ {
id: 'folder1', id: 'folder1',
name: '工作', name: '工作',
createdAt: '2025-10-12T10:00:00.000Z', createdAt: '2025-10-12T10:00:00.000Z',
}, },
{ {
id: 'folder2', id: 'folder2',
name: '个人', name: '个人',
createdAt: '2025-10-12T10:00:00.000Z', createdAt: '2025-10-12T10:00:00.000Z',
}, },
{ {
id: 'folder3', id: 'folder3',
name: '学习', name: '学习',
createdAt: '2025-10-12T10:00:00.000Z', createdAt: '2025-10-12T10:00:00.000Z',
}, },
] ]
// Mock settings // Mock settings
// 预设的设置示例数据 // 预设的设置示例数据
const mockSettings = { const mockSettings = {
cloudSync: false, // 云同步关闭 cloudSync: false, // 云同步关闭
darkMode: false, // 深色模式关闭 darkMode: false, // 深色模式关闭
} }
// 更新store状态 // 更新store状态
this.notes = mockNotes this.notes = mockNotes
this.folders = mockFolders this.folders = mockFolders
this.settings = mockSettings this.settings = mockSettings
// 保存到Storage // 保存到Storage
await storage.saveNotes(mockNotes) await storage.saveNotes(mockNotes)
await storage.saveFolders(mockFolders) await storage.saveFolders(mockFolders)
await storage.saveSettings(mockSettings) await storage.saveSettings(mockSettings)
}, },
/** /**
* 保存便签数据到Storage * 保存便签数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async saveNotes() { async saveNotes() {
try { try {
await storage.saveNotes(this.notes) await storage.saveNotes(this.notes)
} catch (error) { } catch (error) {
console.error('Error saving notes:', error) console.error('Error saving notes:', error)
} }
}, },
/** /**
* 保存文件夹数据到Storage * 保存文件夹数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async saveFolders() { async saveFolders() {
try { try {
await storage.saveFolders(this.folders) await storage.saveFolders(this.folders)
} catch (error) { } catch (error) {
console.error('Error saving folders:', error) console.error('Error saving folders:', error)
} }
}, },
/** /**
* 保存设置数据到Storage * 保存设置数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async saveSettings() { async saveSettings() {
try { try {
await storage.saveSettings(this.settings) await storage.saveSettings(this.settings)
} catch (error) { } catch (error) {
console.error('Error saving settings:', error) console.error('Error saving settings:', error)
} }
}, },
/** /**
* 便签操作函数 * 便签操作函数
*/ */
/** /**
* 添加新便签 * 添加新便签
* @param {Object} note - 便签对象 * @param {Object} note - 便签对象
* @returns {Promise<Object>} 新创建的便签对象 * @returns {Promise<Object>} 新创建的便签对象
*/ */
async addNote(note) { async addNote(note) {
try { try {
const newNote = await storage.addNote(note) const newNote = await storage.addNote(note)
this.notes.push(newNote) this.notes.push(newNote)
return newNote return newNote
} catch (error) { } catch (error) {
console.error('Error adding note:', error) console.error('Error adding note:', error)
throw error throw error
} }
}, },
/** /**
* 更新便签 * 更新便签
* @param {string} id - 便签ID * @param {string} id - 便签ID
* @param {Object} updates - 要更新的属性对象 * @param {Object} updates - 要更新的属性对象
* @returns {Promise<Object>} 更新后的便签对象 * @returns {Promise<Object>} 更新后的便签对象
*/ */
async updateNote(id, updates) { async updateNote(id, updates) {
try { try {
const updatedNote = await storage.updateNote(id, updates) const updatedNote = await storage.updateNote(id, updates)
if (updatedNote) { if (updatedNote) {
const index = this.notes.findIndex(note => note.id === id) const index = this.notes.findIndex(note => note.id === id)
if (index !== -1) { if (index !== -1) {
this.notes[index] = updatedNote this.notes[index] = updatedNote
} }
} }
return updatedNote return updatedNote
} catch (error) { } catch (error) {
console.error('Error updating note:', error) console.error('Error updating note:', error)
throw error throw error
} }
}, },
/** /**
* 删除便签 * 删除便签
* @param {string} id - 要删除的便签ID * @param {string} id - 要删除的便签ID
* @returns {Promise<boolean>} 删除成功返回true失败返回false * @returns {Promise<boolean>} 删除成功返回true失败返回false
*/ */
async deleteNote(id) { async deleteNote(id) {
try { try {
const result = await storage.deleteNote(id) const result = await storage.deleteNote(id)
if (result) { if (result) {
this.notes = this.notes.filter(note => note.id !== id) this.notes = this.notes.filter(note => note.id !== id)
} }
return result return result
} catch (error) { } catch (error) {
console.error('Error deleting note:', error) console.error('Error deleting note:', error)
throw error throw error
} }
}, },
/** /**
* 将便签移至回收站 * 将便签移至回收站
* 将便签标记为已删除状态,并记录删除时间 * 将便签标记为已删除状态,并记录删除时间
* @param {string} id - 便签ID * @param {string} id - 便签ID
* @returns {Promise<Object>} 更新后的便签对象 * @returns {Promise<Object>} 更新后的便签对象
*/ */
async moveToTrash(id) { async moveToTrash(id) {
try { try {
const updatedNote = await storage.updateNote(id, { isDeleted: true, deletedAt: getCurrentDateTime() }) const updatedNote = await storage.updateNote(id, { isDeleted: true, deletedAt: getCurrentDateTime() })
if (updatedNote) { if (updatedNote) {
const index = this.notes.findIndex(note => note.id === id) const index = this.notes.findIndex(note => note.id === id)
if (index !== -1) { if (index !== -1) {
this.notes[index] = updatedNote this.notes[index] = updatedNote
} }
} }
return updatedNote return updatedNote
} catch (error) { } catch (error) {
console.error('Error moving note to trash:', error) console.error('Error moving note to trash:', error)
throw error throw error
} }
}, },
/** /**
* 永久删除便签 * 永久删除便签
* 从便签列表中彻底移除便签 * 从便签列表中彻底移除便签
* @param {string} id - 便签ID * @param {string} id - 便签ID
* @returns {Promise<boolean>} 删除成功返回true失败返回false * @returns {Promise<boolean>} 删除成功返回true失败返回false
*/ */
async permanentlyDeleteNote(id) { async permanentlyDeleteNote(id) {
try { try {
const result = await storage.deleteNote(id) const result = await storage.deleteNote(id)
if (result) { if (result) {
this.notes = this.notes.filter(note => note.id !== id) this.notes = this.notes.filter(note => note.id !== id)
} }
return result return result
} catch (error) { } catch (error) {
console.error('Error permanently deleting note:', error) console.error('Error permanently deleting note:', error)
throw error throw error
} }
}, },
/** /**
* 文件夹操作函数 * 文件夹操作函数
*/ */
/** /**
* 添加新文件夹 * 添加新文件夹
* @param {Object} folder - 文件夹对象 * @param {Object} folder - 文件夹对象
* @returns {Promise<Object>} 新创建的文件夹对象 * @returns {Promise<Object>} 新创建的文件夹对象
*/ */
async addFolder(folder) { async addFolder(folder) {
try { try {
const newFolder = await storage.addFolder(folder) const newFolder = await storage.addFolder(folder)
this.folders.push(newFolder) this.folders.push(newFolder)
return newFolder return newFolder
} catch (error) { } catch (error) {
console.error('Error adding folder:', error) console.error('Error adding folder:', error)
throw error throw error
} }
}, },
/** /**
* 设置操作函数 * 更新文件夹
*/ * @param {string} id - 文件夹ID
* @param {Object} updates - 要更新的属性对象
/** * @returns {Promise<Object>} 更新后的文件夹对象
* 更新设置 */
* @param {Object} newSettings - 新的设置对象 async updateFolder(id, updates) {
* @returns {Promise<void>} try {
*/ const updatedFolder = await storage.updateFolder(id, updates)
async updateSettings(newSettings) { if (updatedFolder) {
try { const index = this.folders.findIndex(folder => folder.id === id)
const updatedSettings = { ...this.settings, ...newSettings } if (index !== -1) {
this.settings = updatedSettings this.folders[index] = updatedFolder
await storage.saveSettings(updatedSettings) }
} catch (error) { }
console.error('Error updating settings:', error) return updatedFolder
throw error } catch (error) {
} console.error('Error updating folder:', error)
}, throw error
}
/** },
* 切换云同步设置
* 开启或关闭云同步功能 /**
* @returns {Promise<void>} * 删除文件夹
*/ * @param {string} id - 要删除的文件夹ID
async toggleCloudSync() { * @returns {Promise<boolean>} 删除成功返回true失败返回false
await this.updateSettings({ cloudSync: !this.settings.cloudSync }) */
}, async deleteFolder(id) {
try {
/** const result = await storage.deleteFolder(id)
* 切换深色模式设置 if (result) {
* 开启或关闭深色模式 // 将文件夹中的便签移回"全部便签"
* @returns {Promise<void>} const notesInFolder = this.notes.filter(note => note.folderId === id)
*/ for (const note of notesInFolder) {
async toggleDarkMode() { await this.updateNote(note.id, { folderId: null })
await this.updateSettings({ darkMode: !this.settings.darkMode }) }
},
}, // 从文件夹列表中移除文件夹
}) this.folders = this.folders.filter(folder => folder.id !== id)
}
return result
} catch (error) {
console.error('Error deleting folder:', error)
throw error
}
},
/**
* 设置操作函数
*/
/**
* 更新设置
* @param {Object} newSettings - 新的设置对象
* @returns {Promise<void>}
*/
async updateSettings(newSettings) {
try {
const updatedSettings = { ...this.settings, ...newSettings }
this.settings = updatedSettings
await storage.saveSettings(updatedSettings)
} catch (error) {
console.error('Error updating settings:', error)
throw error
}
},
/**
* 切换云同步设置
* 开启或关闭云同步功能
* @returns {Promise<void>}
*/
async toggleCloudSync() {
await this.updateSettings({ cloudSync: !this.settings.cloudSync })
},
/**
* 切换深色模式设置
* 开启或关闭深色模式
* @returns {Promise<void>}
*/
async toggleDarkMode() {
await this.updateSettings({ darkMode: !this.settings.darkMode })
},
},
})

View File

@@ -1,475 +1,510 @@
import { getCurrentDateTime, getTimestamp } from './dateUtils' import { getCurrentDateTime, getTimestamp } from './dateUtils'
// 数据库配置 // 数据库配置
const DB_NAME = 'SmartisanNoteDB' const DB_NAME = 'SmartisanNoteDB'
const DB_VERSION = 2 // 更新版本号以确保数据库重新创建 const DB_VERSION = 2 // 更新版本号以确保数据库重新创建
const NOTES_STORE = 'notes' const NOTES_STORE = 'notes'
const FOLDERS_STORE = 'folders' const FOLDERS_STORE = 'folders'
const SETTINGS_STORE = 'settings' const SETTINGS_STORE = 'settings'
let db = null let db = null
/** /**
* 打开数据库连接 * 打开数据库连接
* @returns {Promise<IDBDatabase>} 数据库实例 * @returns {Promise<IDBDatabase>} 数据库实例
*/ */
const openDB = () => { const openDB = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (db) { if (db) {
return resolve(db) return resolve(db)
} }
const request = indexedDB.open(DB_NAME, DB_VERSION) const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onerror = () => { request.onerror = () => {
reject(new Error('无法打开数据库')) reject(new Error('无法打开数据库'))
} }
request.onsuccess = () => { request.onsuccess = () => {
db = request.result db = request.result
resolve(db) resolve(db)
} }
request.onupgradeneeded = (event) => { request.onupgradeneeded = (event) => {
const database = event.target.result const database = event.target.result
// 删除现有的对象存储(如果版本已更改) // 删除现有的对象存储(如果版本已更改)
if (event.oldVersion > 0) { if (event.oldVersion > 0) {
if (database.objectStoreNames.contains(NOTES_STORE)) { if (database.objectStoreNames.contains(NOTES_STORE)) {
database.deleteObjectStore(NOTES_STORE) database.deleteObjectStore(NOTES_STORE)
} }
if (database.objectStoreNames.contains(FOLDERS_STORE)) { if (database.objectStoreNames.contains(FOLDERS_STORE)) {
database.deleteObjectStore(FOLDERS_STORE) database.deleteObjectStore(FOLDERS_STORE)
} }
if (database.objectStoreNames.contains(SETTINGS_STORE)) { if (database.objectStoreNames.contains(SETTINGS_STORE)) {
database.deleteObjectStore(SETTINGS_STORE) database.deleteObjectStore(SETTINGS_STORE)
} }
} }
// 创建便签存储对象 // 创建便签存储对象
const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' }) const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' })
notesStore.createIndex('folderId', 'folderId', { unique: false }) notesStore.createIndex('folderId', 'folderId', { unique: false })
notesStore.createIndex('isStarred', 'isStarred', { unique: false }) notesStore.createIndex('isStarred', 'isStarred', { unique: false })
notesStore.createIndex('isDeleted', 'isDeleted', { unique: false }) notesStore.createIndex('isDeleted', 'isDeleted', { unique: false })
notesStore.createIndex('createdAt', 'createdAt', { unique: false }) notesStore.createIndex('createdAt', 'createdAt', { unique: false })
notesStore.createIndex('updatedAt', 'updatedAt', { unique: false }) notesStore.createIndex('updatedAt', 'updatedAt', { unique: false })
// 创建文件夹存储对象 // 创建文件夹存储对象
database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' }) database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' })
// 创建设置存储对象 // 创建设置存储对象
database.createObjectStore(SETTINGS_STORE) database.createObjectStore(SETTINGS_STORE)
} }
}) })
} }
/** /**
* 从存储中获取数据 * 从存储中获取数据
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @returns {Promise<Array>} 数据数组 * @returns {Promise<Array>} 数据数组
*/ */
const getAllFromStore = async (storeName) => { const getAllFromStore = async (storeName) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readonly') const transaction = database.transaction([storeName], 'readonly')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.getAll() const request = store.getAll()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(request.result || []) resolve(request.result || [])
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`获取 ${storeName} 数据失败`)) reject(new Error(`获取 ${storeName} 数据失败`))
} }
}) })
} }
/** /**
* 保存数据到存储 * 保存数据到存储
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @param {Array} data - 要保存的数据数组 * @param {Array} data - 要保存的数据数组
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const saveToStore = async (storeName, data) => { const saveToStore = async (storeName, data) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
// 清除现有数据 // 清除现有数据
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const clearRequest = store.clear() const clearRequest = store.clear()
clearRequest.onsuccess = () => resolve() clearRequest.onsuccess = () => resolve()
clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`)) clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`))
}) })
// 添加新数据 // 添加新数据
for (const item of data) { for (const item of data) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const addRequest = store.add(item) const addRequest = store.add(item)
addRequest.onsuccess = () => resolve() addRequest.onsuccess = () => resolve()
addRequest.onerror = () => reject(new Error(`保存 ${storeName} 数据失败`)) addRequest.onerror = () => reject(new Error(`保存 ${storeName} 数据失败`))
}) })
} }
} }
/** /**
* 从存储中获取单个项 * 从存储中获取单个项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @param {string} id - 项的ID * @param {string} id - 项的ID
* @returns {Promise<Object|null>} 项对象或null * @returns {Promise<Object|null>} 项对象或null
*/ */
const getFromStore = async (storeName, id) => { const getFromStore = async (storeName, id) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readonly') const transaction = database.transaction([storeName], 'readonly')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.get(id) const request = store.get(id)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(request.result || null) resolve(request.result || null)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`获取 ${storeName} 项失败`)) reject(new Error(`获取 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 向存储中添加项 * 向存储中添加项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @param {Object} item - 要添加的项 * @param {Object} item - 要添加的项
* @returns {Promise<Object>} 添加的项 * @returns {Promise<Object>} 添加的项
*/ */
const addToStore = async (storeName, item) => { const addToStore = async (storeName, item) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.add(item) const request = store.add(item)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(item) resolve(item)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`添加 ${storeName} 项失败`)) reject(new Error(`添加 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 更新存储中的项 * 更新存储中的项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @param {string} id - 项的ID * @param {string} id - 项的ID
* @param {Object} updates - 要更新的属性对象 * @param {Object} updates - 要更新的属性对象
* @returns {Promise<Object|null>} 更新后的项或null * @returns {Promise<Object|null>} 更新后的项或null
*/ */
const updateInStore = async (storeName, id, updates) => { const updateInStore = async (storeName, id, updates) => {
const item = await getFromStore(storeName, id) const item = await getFromStore(storeName, id)
if (!item) return null if (!item) return null
const updatedItem = { ...item, ...updates } const updatedItem = { ...item, ...updates }
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.put(updatedItem) const request = store.put(updatedItem)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(updatedItem) resolve(updatedItem)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`更新 ${storeName} 项失败`)) reject(new Error(`更新 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 从存储中删除项 * 从存储中删除项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @param {string} id - 要删除的项的ID * @param {string} id - 要删除的项的ID
* @returns {Promise<boolean>} 删除成功返回true失败返回false * @returns {Promise<boolean>} 删除成功返回true失败返回false
*/ */
const deleteFromStore = async (storeName, id) => { const deleteFromStore = async (storeName, id) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.delete(id) const request = store.delete(id)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(true) resolve(true)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`删除 ${storeName} 项失败`)) reject(new Error(`删除 ${storeName} 项失败`))
} }
}) })
} }
// 便签操作函数 // 便签操作函数
// 提供便签的增删改查功能 // 提供便签的增删改查功能
/** /**
* 获取所有便签数据 * 获取所有便签数据
* 从IndexedDB中读取便签数据 * 从IndexedDB中读取便签数据
* @returns {Promise<Array>} 便签数组 * @returns {Promise<Array>} 便签数组
*/ */
export const getNotes = async () => { export const getNotes = async () => {
try { try {
const notes = await getAllFromStore(NOTES_STORE) const notes = await getAllFromStore(NOTES_STORE)
return ensureNotesDefaults(notes) return ensureNotesDefaults(notes)
} catch (error) { } catch (error) {
console.error('Error getting notes:', error) console.error('Error getting notes:', error)
return [] return []
} }
} }
/** /**
* 保存便签数据 * 保存便签数据
* 将便签数组保存到IndexedDB * 将便签数组保存到IndexedDB
* @param {Array} notes - 便签数组 * @param {Array} notes - 便签数组
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export const saveNotes = async (notes) => { export const saveNotes = async (notes) => {
try { try {
await saveToStore(NOTES_STORE, notes) await saveToStore(NOTES_STORE, notes)
} catch (error) { } catch (error) {
console.error('Error saving notes:', error) console.error('Error saving notes:', error)
} }
} }
/** /**
* 添加新便签 * 添加新便签
* 创建一个新的便签对象并添加到便签列表中 * 创建一个新的便签对象并添加到便签列表中
* @param {Object} note - 便签对象,包含便签内容和其他属性 * @param {Object} note - 便签对象,包含便签内容和其他属性
* @returns {Promise<Object>} 新创建的便签对象 * @returns {Promise<Object>} 新创建的便签对象
*/ */
export const addNote = async (note) => { export const addNote = async (note) => {
try { try {
// 创建新的便签对象,添加必要的属性 // 创建新的便签对象,添加必要的属性
const newNote = { const newNote = {
title: note.title || '', title: note.title || '',
content: note.content || '', content: note.content || '',
id: note.id || getTimestamp().toString(), // 使用时间戳生成唯一ID id: note.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
createdAt: note.createdAt || getCurrentDateTime(), // 创建时间 createdAt: note.createdAt || getCurrentDateTime(), // 创建时间
updatedAt: note.updatedAt || getCurrentDateTime(), // 更新时间 updatedAt: note.updatedAt || getCurrentDateTime(), // 更新时间
isStarred: note.isStarred || false, // 是否加星 isStarred: note.isStarred || false, // 是否加星
isTop: note.isTop || false, // 是否置顶 isTop: note.isTop || false, // 是否置顶
hasImage: note.hasImage || false, // 是否包含图片 hasImage: note.hasImage || false, // 是否包含图片
isDeleted: note.isDeleted || false, // 是否已删除 isDeleted: note.isDeleted || false, // 是否已删除
deletedAt: note.deletedAt || null, // 删除时间 deletedAt: note.deletedAt || null, // 删除时间
folderId: note.folderId || null, // 文件夹ID folderId: note.folderId || null, // 文件夹ID
...note ...note
} }
// 添加到存储 // 添加到存储
await addToStore(NOTES_STORE, newNote) await addToStore(NOTES_STORE, newNote)
return newNote return newNote
} catch (error) { } catch (error) {
console.error('Error adding note:', error) console.error('Error adding note:', error)
throw error throw error
} }
} }
/** /**
* 更新便签 * 更新便签
* 根据ID查找并更新便签信息 * 根据ID查找并更新便签信息
* @param {string} id - 便签ID * @param {string} id - 便签ID
* @param {Object} updates - 要更新的属性对象 * @param {Object} updates - 要更新的属性对象
* @returns {Promise<Object|null>} 更新后的便签对象如果未找到则返回null * @returns {Promise<Object|null>} 更新后的便签对象如果未找到则返回null
*/ */
export const updateNote = async (id, updates) => { export const updateNote = async (id, updates) => {
try { try {
// 更新便签并保存 // 更新便签并保存
const updatedNote = await updateInStore(NOTES_STORE, id, { const updatedNote = await updateInStore(NOTES_STORE, id, {
...updates, ...updates,
updatedAt: getCurrentDateTime() // 更新最后修改时间 updatedAt: getCurrentDateTime() // 更新最后修改时间
}) })
return updatedNote return updatedNote
} catch (error) { } catch (error) {
console.error('Error updating note:', error) console.error('Error updating note:', error)
throw error throw error
} }
} }
/** /**
* 删除便签 * 删除便签
* 根据ID从便签列表中移除便签 * 根据ID从便签列表中移除便签
* @param {string} id - 要删除的便签ID * @param {string} id - 要删除的便签ID
* @returns {Promise<boolean>} 删除成功返回true未找到便签返回false * @returns {Promise<boolean>} 删除成功返回true未找到便签返回false
*/ */
export const deleteNote = async (id) => { export const deleteNote = async (id) => {
try { try {
// 从存储中删除 // 从存储中删除
const result = await deleteFromStore(NOTES_STORE, id) const result = await deleteFromStore(NOTES_STORE, id)
return result return result
} catch (error) { } catch (error) {
console.error('Error deleting note:', error) console.error('Error deleting note:', error)
return false return false
} }
} }
// 文件夹操作函数 // 文件夹操作函数
// 提供文件夹的增删改查功能 // 提供文件夹的增删改查功能
/** /**
* 获取所有文件夹数据 * 获取所有文件夹数据
* 从IndexedDB中读取文件夹数据 * 从IndexedDB中读取文件夹数据
* @returns {Promise<Array>} 文件夹数组 * @returns {Promise<Array>} 文件夹数组
*/ */
export const getFolders = async () => { export const getFolders = async () => {
try { try {
const folders = await getAllFromStore(FOLDERS_STORE) const folders = await getAllFromStore(FOLDERS_STORE)
return ensureFoldersDefaults(folders) return ensureFoldersDefaults(folders)
} catch (error) { } catch (error) {
console.error('Error getting folders:', error) console.error('Error getting folders:', error)
return [] return []
} }
} }
/** /**
* 保存文件夹数据 * 保存文件夹数据
* 将文件夹数组保存到IndexedDB * 将文件夹数组保存到IndexedDB
* @param {Array} folders - 文件夹数组 * @param {Array} folders - 文件夹数组
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export const saveFolders = async (folders) => { export const saveFolders = async (folders) => {
try { try {
await saveToStore(FOLDERS_STORE, folders) await saveToStore(FOLDERS_STORE, folders)
} catch (error) { } catch (error) {
console.error('Error saving folders:', error) console.error('Error saving folders:', error)
} }
} }
/** /**
* 添加新文件夹 * 添加新文件夹
* 创建一个新的文件夹对象并添加到文件夹列表中 * 创建一个新的文件夹对象并添加到文件夹列表中
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性 * @param {Object} folder - 文件夹对象,包含文件夹名称等属性
* @returns {Promise<Object>} 新创建的文件夹对象 * @returns {Promise<Object>} 新创建的文件夹对象
*/ */
export const addFolder = async (folder) => { export const addFolder = async (folder) => {
try { try {
// 创建新的文件夹对象,添加必要的属性 // 创建新的文件夹对象,添加必要的属性
const newFolder = { const newFolder = {
name: folder.name || '', name: folder.name || '',
id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间 createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间
...folder ...folder
} }
// 添加到存储 // 添加到存储
await addToStore(FOLDERS_STORE, newFolder) await addToStore(FOLDERS_STORE, newFolder)
return newFolder return newFolder
} catch (error) { } catch (error) {
console.error('Error adding folder:', error) console.error('Error adding folder:', error)
throw error throw error
} }
} }
// 设置操作函数 /**
// 提供应用设置的读取和保存功能 * 更新文件夹
* 根据ID查找并更新文件夹信息
/** * @param {string} id - 文件夹ID
* 获取应用设置 * @param {Object} updates - 要更新的属性对象
* 从IndexedDB中读取设置数据 * @returns {Promise<Object|null>} 更新后的文件夹对象如果未找到则返回null
* @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置 */
*/ export const updateFolder = async (id, updates) => {
export const getSettings = async () => { try {
try { // 更新文件夹并保存
const database = await openDB() const updatedFolder = await updateInStore(FOLDERS_STORE, id, updates)
const transaction = database.transaction([SETTINGS_STORE], 'readonly') return updatedFolder
const store = transaction.objectStore(SETTINGS_STORE) } catch (error) {
const request = store.get('settings') console.error('Error updating folder:', error)
throw error
const settings = await new Promise((resolve, reject) => { }
request.onsuccess = () => { }
resolve(request.result || { cloudSync: false, darkMode: false })
} /**
* 删除文件夹
request.onerror = () => { * 根据ID从文件夹列表中移除文件夹
reject(new Error('获取设置失败')) * @param {string} id - 要删除的文件夹ID
} * @returns {Promise<boolean>} 删除成功返回true未找到文件夹返回false
}) */
export const deleteFolder = async (id) => {
return settings try {
} catch (error) { // 从存储中删除
console.error('Error getting settings:', error) const result = await deleteFromStore(FOLDERS_STORE, id)
// 出错时返回默认设置 return result
return { cloudSync: false, darkMode: false } } catch (error) {
} console.error('Error deleting folder:', error)
} return false
}
/** }
* 保存应用设置
* 将设置对象保存到IndexedDB // 设置操作函数
* @param {Object} settings - 设置对象 // 提供应用设置的读取和保存功能
* @returns {Promise<void>}
*/ /**
export const saveSettings = async (settings) => { * 获取应用设置
try { * 从IndexedDB中读取设置数据
const database = await openDB() * @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置
const transaction = database.transaction([SETTINGS_STORE], 'readwrite') */
const store = transaction.objectStore(SETTINGS_STORE) export const getSettings = async () => {
const request = store.put(settings, 'settings') try {
const database = await openDB()
await new Promise((resolve, reject) => { const transaction = database.transaction([SETTINGS_STORE], 'readonly')
request.onsuccess = () => resolve() const store = transaction.objectStore(SETTINGS_STORE)
request.onerror = () => reject(new Error('保存设置失败')) const request = store.get('settings')
})
} catch (error) { const settings = await new Promise((resolve, reject) => {
console.error('Error saving settings:', error) request.onsuccess = () => {
} resolve(request.result || { cloudSync: false, darkMode: false })
} }
/** request.onerror = () => {
* 确保数据有默认值 reject(new Error('获取设置失败'))
* @param {Array} notes - 便签数组 }
* @returns {Array} 处理后的便签数组 })
*/
const ensureNotesDefaults = (notes) => { return settings
return notes.map(note => ({ } catch (error) {
title: note.title || '', console.error('Error getting settings:', error)
content: note.content || '', // 出错时返回默认设置
id: note.id, return { cloudSync: false, darkMode: false }
createdAt: note.createdAt, }
updatedAt: note.updatedAt, }
isStarred: note.isStarred || false,
isTop: note.isTop || false, /**
hasImage: note.hasImage || false, * 保存应用设置
isDeleted: note.isDeleted || false, * 将设置对象保存到IndexedDB
deletedAt: note.deletedAt || null, * @param {Object} settings - 设置对象
folderId: note.folderId || null, * @returns {Promise<void>}
...note */
})) export const saveSettings = async (settings) => {
} try {
const database = await openDB()
/** const transaction = database.transaction([SETTINGS_STORE], 'readwrite')
* 确保文件夹数据有默认值 const store = transaction.objectStore(SETTINGS_STORE)
* @param {Array} folders - 文件夹数组 const request = store.put(settings, 'settings')
* @returns {Array} 处理后的文件夹数组
*/ await new Promise((resolve, reject) => {
const ensureFoldersDefaults = (folders) => { request.onsuccess = () => resolve()
return folders.map(folder => ({ request.onerror = () => reject(new Error('保存设置失败'))
name: folder.name || '', })
id: folder.id, } catch (error) {
createdAt: folder.createdAt, console.error('Error saving settings:', error)
...folder }
})) }
}
/**
/** * 确保数据有默认值
* 初始化数据库 * @param {Array} notes - 便签数组
* @returns {Promise<void>} * @returns {Array} 处理后的便签数组
*/ */
export const initDB = async () => { const ensureNotesDefaults = (notes) => {
try { return notes.map(note => ({
await openDB() title: note.title || '',
} catch (error) { content: note.content || '',
console.error('Error initializing database:', error) id: note.id,
} createdAt: note.createdAt,
updatedAt: note.updatedAt,
isStarred: note.isStarred || false,
isTop: note.isTop || false,
hasImage: note.hasImage || false,
isDeleted: note.isDeleted || false,
deletedAt: note.deletedAt || null,
folderId: note.folderId || null,
...note
}))
}
/**
* 确保文件夹数据有默认值
* @param {Array} folders - 文件夹数组
* @returns {Array} 处理后的文件夹数组
*/
const ensureFoldersDefaults = (folders) => {
return folders.map(folder => ({
name: folder.name || '',
id: folder.id,
createdAt: folder.createdAt,
...folder
}))
}
/**
* 初始化数据库
* @returns {Promise<void>}
*/
export const initDB = async () => {
try {
await openDB()
} catch (error) {
console.error('Error initializing database:', error)
}
} }