Merge pull request 'future' (#13) from future into main

Reviewed-on: yuantao/SmartisanNote.Remake#13
This commit is contained in:
2025-11-03 09:50:41 +08:00
10 changed files with 2343 additions and 1668 deletions

View File

@@ -88,6 +88,10 @@
--white-60: #ffffff99; /* 60% white */ --white-60: #ffffff99; /* 60% white */
--white-80: #ffffffcc; /* 80% white */ --white-80: #ffffffcc; /* 80% white */
--white-90: #ffffffe6; /* 90% white */ --white-90: #ffffffe6; /* 90% white */
--confirmFontSize: 0.8rem;
--confirmBg: rgba(0, 0, 0, 0.15);
--confirmBtnColor: #000000cc;
} }
body { body {

View File

@@ -27,7 +27,6 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -13,19 +13,17 @@
</template> </template>
<!-- 设置页面 --> <!-- 设置页面 -->
<transition <transition name="settings-slide" v-show="isSettingsRoute" appear>
name="settings-slide"
v-show="isSettingsRoute"
appear>
<SettingsPage class="setting-page" /> <SettingsPage class="setting-page" />
</transition> </transition>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, watch, computed } from 'vue' import { ref, watch, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import '@/common/base.css' import '@/common/base.css'
import { initModalService } from '@/utils/modalService'
// 导入页面组件 // 导入页面组件
import NoteListPage from './pages/NoteListPage.vue' import NoteListPage from './pages/NoteListPage.vue'
@@ -33,6 +31,7 @@ import SettingsPage from './pages/SettingsPage.vue'
const route = useRoute() const route = useRoute()
const transitionName = ref('slide-left') const transitionName = ref('slide-left')
const modalRef = ref()
// 计算是否为设置页面路由 // 计算是否为设置页面路由
const isSettingsRoute = computed(() => { const isSettingsRoute = computed(() => {
@@ -53,15 +52,15 @@ watch(
transitionName.value = 'slide-right' transitionName.value = 'slide-right'
return return
} }
// 判断导航方向 // 判断导航方向
const toDepth = toPath.split('/').length const toDepth = toPath.split('/').length
const fromDepth = fromPath.split('/').length const fromDepth = fromPath.split('/').length
// 如果是进入更深的页面(如从列表页进入编辑页),使用左滑动画 // 如果是进入更深的页面(如从列表页进入编辑页),使用左滑动画
if (toDepth > fromDepth) { if (toDepth > fromDepth) {
transitionName.value = 'slide-left' transitionName.value = 'slide-left'
} }
// 如果是返回上层页面(如从编辑页返回列表页),使用右滑动画 // 如果是返回上层页面(如从编辑页返回列表页),使用右滑动画
else if (toDepth < fromDepth) { else if (toDepth < fromDepth) {
transitionName.value = 'slide-right' transitionName.value = 'slide-right'
@@ -73,7 +72,10 @@ watch(
} }
) )
// 无额外处理函数 // 初始化弹框服务
onMounted(() => {
initModalService()
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

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,335 @@
<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, </div>
default: '10/10上午9:28', </template>
},
onAllClick: { <script setup>
type: Function, import { ref, computed, nextTick } from 'vue'
default: null, import { useAppStore } from '../stores/useAppStore'
}, import FolderItem from './FolderItem.vue'
onStarredClick: { import { showConfirm, showPrompt } from '../utils/modalService'
type: Function,
default: null, const store = useAppStore()
}, const props = defineProps({
onTrashClick: { allCount: {
type: Function, type: Number,
default: null, default: 0,
}, },
onArchiveClick: { starredCount: {
type: Function, type: Number,
default: null, default: 0,
}, },
onAddFolder: { trashCount: {
type: Function, type: Number,
default: null, default: 0,
}, },
}) archiveCount: {
type: Number,
const handleAllClick = () => { default: 0,
if (props.onAllClick) { },
props.onAllClick() selectedFolder: {
} type: String,
} default: '',
},
const handleStarredClick = () => { lastSyncTime: {
if (props.onStarredClick) { type: String,
props.onStarredClick() default: '10/10上午9:28',
} },
} onAllClick: {
type: Function,
const handleTrashClick = () => { default: null,
if (props.onTrashClick) { },
props.onTrashClick() onStarredClick: {
} type: Function,
} default: null,
},
const handleArchiveClick = () => { onTrashClick: {
if (props.onArchiveClick) { type: Function,
props.onArchiveClick() default: null,
} },
} onArchiveClick: {
type: Function,
const handleAddFolder = event => { default: null,
// 阻止事件冒泡到父元素 },
event.stopPropagation() onAddFolder: {
if (props.onAddFolder) { type: Function,
props.onAddFolder() default: null,
} },
} onFolderClick: {
</script> type: Function,
default: null,
<style lang="less" scoped> },
.page { })
.section_7 {
margin-top: 2rem; // 重命名文件夹相关状态
background-color: #00000000;
.section_8 { const showRenameFolderModal = ref(false)
padding: 0.29rem 0.92rem;
background-color: #f4f4f4; const renameFolderId = ref(null)
border: solid 0.063rem #f0ece7;
.image_7, const renameFolderName = ref('')
.image_8 { // 选择模式相关状态
border-radius: 0.63rem;
width: 2rem; const isSelectionMode = ref(false)
height: 2rem;
object-fit: contain; const selectedFolders = ref([])
} // 计算自定义文件夹(排除系统文件夹)
.text_11 { const customFolders = computed(() => {
color: #cacaca; return store.folders.filter(folder => !['all', 'starred', 'trash', 'archive'].includes(folder.id))
font-size: 0.7rem; })
line-height: 1.16rem;
} // 获取文件夹中的便签数量
} const getFolderNoteCount = folderId => {
} return store.notes.filter(note => note.folderId === folderId && !note.isDeleted).length
} }
</style>
// 切换选择模式
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 = async () => {
if (selectedFolders.value.length === 0) return
try {
const confirmed = await showConfirm(`确定要删除选中的 ${selectedFolders.value.length} 个文件夹吗?文件夹中的便签将移至"全部便签"。`, '删除文件夹')
if (confirmed) {
// 删除选中的文件夹
for (const folderId of selectedFolders.value) {
// 跳过系统文件夹
if (['all', 'starred', 'trash', 'archive'].includes(folderId)) continue
await store.deleteFolder(folderId)
}
// 清空选中项并退出选择模式
selectedFolders.value = []
isSelectionMode.value = false
}
} catch (error) {
console.error('删除文件夹失败:', error)
}
}
// 处理删除文件夹
const handleDeleteFolder = async folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
// 确认删除
try {
const confirmed = await showConfirm(`确定要删除文件夹 "${getFolderName(folderId)}" 吗?文件夹中的便签将移至"全部便签"。`, '删除文件夹')
if (confirmed) {
await store.deleteFolder(folderId)
}
} catch (error) {
console.error('删除文件夹失败:', error)
}
}
// 处理编辑文件夹
const handleEditFolder = async folderId => {
// 阻止事件冒泡到父元素
event.stopPropagation()
const folder = store.folders.find(f => f.id === folderId)
if (folder) {
try {
const newName = await showPrompt('请输入文件夹名称', '重命名文件夹', '请输入文件夹名称', folder.name)
if (newName && newName.trim()) {
await store.updateFolder(folderId, { name: newName.trim() })
}
} catch (error) {
// 用户取消操作或出现错误
console.log('重命名文件夹操作已取消或出现错误:', 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 handleAddFolder = async () => {
try {
const folderName = await showPrompt('请输入文件夹名称', '添加文件夹', '请输入文件夹名称')
if (folderName && folderName.trim()) {
const newFolder = {
name: folderName.trim(),
id: `folder_${Date.now()}`, // 生成唯一ID
createdAt: new Date().toISOString(),
}
await store.addFolder(newFolder)
}
} catch (error) {
// 用户取消操作或出现错误
console.log('添加文件夹操作已取消或出现错误:', 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;
}
}
}
}
</style>

View File

@@ -1,206 +1,347 @@
<template> <template>
<div class="code-fun-flex-col code-fun-justify-start"> <div v-if="visible" class="pd-mask" @click="handleMaskClick">
<div class="code-fun-flex-col code-fun-justify-start group_1"> <div class="pd-confirm" @click.stop>
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-end group"> <h2 class="pd-title">{{ dynamicTitle || title }}</h2>
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start code-fun-relative group_2">
<div class="code-fun-flex-col section_7"> <div class="pd-input-container" v-if="dynamicShowInput || showInput">
<div class="code-fun-flex-col code-fun-justify-start section_1"> <input ref="inputRef" v-model="inputModel" class="pd-input" :placeholder="dynamicInputPlaceholder || inputPlaceholder" @keyup.enter="handleConfirm" />
<div class="code-fun-flex-row section_11"> </div>
<div class="code-fun-flex-col code-fun-justify-start section_12" @click="handleCancel">
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper_2"> <p class="pd-message" v-else>{{ dynamicMessage || message }}</p>
<span class="text_4">{{ cancelText }}</span>
</div> <div class="pd-buttons">
</div> <button v-if="dynamicShowConfirm || showConfirm" class="pd-button pd-confirm-btn" @click="handleConfirm">
<div class="code-fun-flex-col code-fun-justify-start section_13 code-fun-ml-8" @click="handleConfirm"> {{ dynamicConfirmText || confirmText }}
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper_3"> </button>
<span class="text_5">{{ confirmText }}</span>
</div> <button v-if="dynamicShowCancel || showCancel" class="pd-button pd-cancel" @click="handleCancel">
</div> {{ dynamicCancelText || cancelText }}
</div> </button>
</div> </div>
<div class="code-fun-flex-col code-fun-justify-start code-fun-relative section_9"> </div>
<div class="code-fun-flex-col code-fun-justify-start section_10"> </div>
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start text-wrapper"> </template>
<input
v-model="inputValue" <script setup>
class="text_3" import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
:placeholder="placeholder"
@keyup.enter="handleConfirm" // 响应式数据用于动态更新 Modal 内容
/>
</div> const dynamicTitle = ref('')
</div>
</div> const dynamicMessage = ref('')
</div>
<div class="code-fun-flex-col section_8 pos"> const dynamicConfirmText = ref('确认')
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start code-fun-self-stretch group_3">
<span class="text_2">{{ title }}</span> const dynamicCancelText = ref('取消')
</div>
<div class="code-fun-self-start divider"></div> const dynamicShowConfirm = ref(true)
</div>
</div> const dynamicShowCancel = ref(true)
</div>
</div> const dynamicMaskClosable = ref(false)
</div>
</template> const dynamicShowInput = ref(false)
<script setup> const dynamicInputPlaceholder = ref('请输入文字')
import { ref } from 'vue';
const props = defineProps({
const props = defineProps({ title: {
title: { type: String,
type: String, default: '',
default: '新建文件夹' },
}, message: {
placeholder: { type: String,
type: String, default: '',
default: '新建文件夹' },
}, confirmText: {
confirmText: { type: String,
type: String, default: '确认',
default: '确定' },
}, cancelText: {
cancelText: { type: String,
type: String, default: '取消',
default: '取消' },
} showConfirm: {
}); type: Boolean,
default: true,
const emit = defineEmits(['confirm', 'cancel']); },
showCancel: {
const inputValue = ref(''); type: Boolean,
default: true,
const handleConfirm = () => { },
emit('confirm', inputValue.value); maskClosable: {
}; type: Boolean,
default: false,
const handleCancel = () => { },
emit('cancel'); showInput: {
}; type: Boolean,
</script> default: false,
},
<style scoped lang="less"> inputPlaceholder: {
.group_1 { type: String,
padding-bottom: 1rem; default: '请输入文字',
.group { },
padding: 0.5rem 0 0.25rem; inputValue: {
.group_2 { type: String,
margin-right: 0.75rem; default: '',
width: 32.31rem; },
.section_7 { })
padding-top: 5.75rem;
background-color: #00000000; const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
width: 29.06rem; const visible = defineModel('visible', { type: Boolean, default: false })
height: 18.16rem; const inputModel = defineModel('inputValue', { type: String, default: '' })
background-image: url(https://codefun-proj-user-res-1256085488.cos.ap-guangzhou.myqcloud.com/686f20ecd54496f19f54e801/68e862ab9520a30011f388ff/17600601480475170758.png);
background-repeat: no-repeat; const inputRef = ref()
background-size: 100% auto;
background-position: 0% 0%; // Promise 控制变量
.section_1 {
margin-top: 7.88rem; let resolvePromise, rejectPromise
background-color: #00000000;
.section_11 { // 返回 Promise 的方法,模拟原生 confirm 行为
padding: 0.5rem 0.5rem 0.75rem;
background-color: #f4f4f7; const show = (options = {}) => {
border-radius: 0rem 0rem 0.75rem 0.75rem; // 更新动态数据
border: solid 0.032rem #edeee8;
.section_12 { if (options.title !== undefined) dynamicTitle.value = options.title
padding: 0.25rem 0;
background-color: #00000000; if (options.message !== undefined) dynamicMessage.value = options.message
width: 13.63rem;
height: 3.13rem; if (options.confirmText !== undefined) dynamicConfirmText.value = options.confirmText
.text-wrapper_2 {
margin: 0 0.25rem; if (options.cancelText !== undefined) dynamicCancelText.value = options.cancelText
padding: 0.75rem 0;
background-color: #f2f2f2; if (options.showConfirm !== undefined) dynamicShowConfirm.value = options.showConfirm
border-radius: 0.25rem;
width: 13.34rem; if (options.showCancel !== undefined) dynamicShowCancel.value = options.showCancel
border: solid 0.032rem #d7d7d7;
.text_4 { if (options.maskClosable !== undefined) dynamicMaskClosable.value = options.maskClosable
color: #757575;
font-size: 1.23rem; if (options.showInput !== undefined) dynamicShowInput.value = options.showInput
font-weight: 700;
line-height: 1.23rem; if (options.inputPlaceholder !== undefined) dynamicInputPlaceholder.value = options.inputPlaceholder
}
} if (options.inputValue !== undefined) inputModel.value = options.inputValue
}
.section_13 { // 显示对话框
margin-right: 0.25rem;
padding-top: 0.25rem; visible.value = true
background-color: #00000000;
width: 13.59rem; emit('update:visible', true)
height: 3.06rem;
.text-wrapper_3 { // 返回 Promise
margin-left: 0.25rem;
padding: 0.75rem 0; return new Promise((resolve, reject) => {
border-radius: 0.25rem; resolvePromise = resolve
background-image: url('assets/53062683132af1946e1a4953530af228.png');
background-size: 100% 100%; rejectPromise = reject
background-repeat: no-repeat; })
width: 13.34rem; }
.text_5 {
color: #cddff2; const handleConfirm = () => {
font-size: 1.19rem; emit('confirm')
line-height: 1.19rem; visible.value = false
} emit('update:visible', false)
}
} // 解决 Promise传递输入框的值
} if (resolvePromise) {
} resolvePromise(inputModel.value)
.section_9 { resolvePromise = null
margin-top: -12.5rem; rejectPromise = null
padding: 1.75rem 0 0.25rem; }
background-color: #00000000; }
.section_10 {
margin: 0 1.5rem; const handleCancel = () => {
padding-top: 0.25rem; emit('cancel')
background-color: #00000000; visible.value = false
width: 25.88rem; emit('update:visible', false)
.text-wrapper {
margin: 0 0.25rem; // 拒绝 Promise
padding: 0.75rem 0; if (rejectPromise) {
background-color: #fefefe; rejectPromise()
border-radius: 0.25rem; resolvePromise = null
width: 25.53rem; rejectPromise = null
border: solid 0.063rem #e1e1e1; }
.text_3 { }
margin-left: 1rem;
color: #d0d0d0; const handleMaskClick = () => {
font-size: 1.38rem; if (props.maskClosable) {
font-weight: 700; handleCancel() // 点击遮罩相当于取消
line-height: 1.38rem; }
} }
}
} // 添加/移除 body 滚动锁定
} const lockBodyScroll = () => {
} document.body.style.overflow = 'hidden'
.section_8 { }
padding: 0 0.13rem;
background-color: #00000000; const unlockBodyScroll = () => {
width: 32.31rem; document.body.style.overflow = ''
.group_3 { }
padding: 1.5rem 0;
width: 32.06rem; onMounted(() => {
.text_2 { if (visible.value) {
margin-left: 10.5rem; lockBodyScroll()
color: #7a7a7a; }
font-size: 1.54rem; })
font-weight: 700;
line-height: 1.54rem; onUnmounted(() => {
} unlockBodyScroll()
} })
.divider {
background-color: #f0f0f0; // 监听 visible 变化
width: 29rem; watch(visible, (newVal, oldVal) => {
height: 0.094rem; if (newVal) {
} lockBodyScroll()
} // 如果模态框包含输入框,则自动获取焦点
.pos { if (dynamicShowInput.value || props.showInput) {
position: absolute; // 使用 nextTick 确保在 DOM 更新后聚焦
left: 0; nextTick(() => {
right: 0; // 添加更长的延迟确保动画完成
top: 0; setTimeout(() => {
} // 确保 inputRef 存在并且可见
} if (inputRef.value && inputRef.value.offsetParent !== null) {
} inputRef.value.focus()
} inputRef.value.select() // 选中所有文本,提升用户体验
</style> }
}, 100)
})
}
} else {
unlockBodyScroll()
// 重置输入框引用,确保下次打开时能正确获取焦点
if (inputRef.value) {
inputRef.value.blur()
}
}
})
// 暴露 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-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-input-container {
margin-block: 1.5em;
padding: 0 10% 1em;
position: relative;
z-index: 2;
}
.pd-input {
width: 100%;
padding: 0.8em;
font-size: 1em;
border: 1px solid var(--confirmBtnBorder, #f1f1f1);
border-radius: 4px;
box-sizing: border-box;
outline: none;
background: var(--confirmTheme, #fff);
color: var(--confirmColor, #636363);
box-shadow: 0px 0px 4px inset rgba(0, 0, 0, 0.12);
}
.pd-input::placeholder {
color: var(--black-20);
}
.pd-input:focus {
border-color: #4d90fe;
}
.pd-input::selection {
background-color: #4d90fe; /* 选中时的背景颜色 */
color: #ffffff; /* 选中时的文字颜色 */
}
.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,430 +1,473 @@
<template> <template>
<ion-page> <ion-page>
<div class="container"> <div class="container">
<ion-content class="content"> <ion-content class="content">
<Header <Header
:title="headerTitle" :title="headerTitle"
:onAction="handleHeaderAction" :onAction="handleHeaderAction"
actionIcon="create" actionIcon="create"
leftType="settings" leftType="settings"
:onLeftAction="handleSettingsPress" :onLeftAction="handleSettingsPress"
:onFolderToggle="handleFolderToggle" :onFolderToggle="handleFolderToggle"
:isFolderExpanded="isFolderExpanded" :isFolderExpanded="isFolderExpanded"
:onTitlePress="handleFolderToggle" :onTitlePress="handleFolderToggle"
slot="fixed" /> slot="fixed" />
<!-- 悬浮文件夹列表 - 使用绝对定位实现 --> <!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
<div v-if="isFolderExpanded" class="folder-list" slot="fixed"> <transition name="folder-slide">
<FolderManage <div v-if="isFolderExpanded" class="folder-list" slot="fixed">
:allCount="allNotesCount" <FolderManage
:starredCount="starredNotesCount" :allCount="allNotesCount"
:trashCount="trashNotesCount" :starredCount="starredNotesCount"
:archiveCount="0" :trashCount="trashNotesCount"
:selectedFolder="currentFolder" :archiveCount="0"
:onAllClick="handleAllNotesClick" :selectedFolder="currentFolder"
:onStarredClick="handleStarredNotesClick" :onAllClick="handleAllNotesClick"
:onTrashClick="handleTrashNotesClick" /> :onStarredClick="handleStarredNotesClick"
</div> :onTrashClick="handleTrashNotesClick"
<!-- 点击外部区域收起文件夹列表的覆盖层 --> :onFolderClick="handleFolderClick"
<div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" class="folder-overlay" slot="fixed"></div> :onAddFolder="handleAddFolder" />
<div class="search-container"> </div>
<SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" /> </transition>
</div> <!-- 点击外部区域收起文件夹列表的覆盖层 -->
<div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" class="folder-overlay" slot="fixed"></div>
<div class="notes-container"> <div class="search-container">
<transition-group name="note-list" tag="div" class="notes-list"> <SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" />
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item"> </div>
<NoteItem
:ref=" <div class="notes-container">
el => { <transition-group name="note-list" tag="div" class="notes-list">
if (el) noteItemRefs[note.id] = el <div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
} <NoteItem
" :ref="
:content="note.content" el => {
:date="formatDate(note.updatedAt)" if (el) noteItemRefs[note.id] = el
:isStarred="note.isStarred" }
:isTop="note.isTop || false" "
:hasImage="note.hasImage || false" :content="note.content"
:onPress="() => handleNotePress(note.id)" :date="formatDate(note.updatedAt)"
:onStarToggle="() => handleStarToggle(note.id)" :isStarred="note.isStarred"
:onTopToggle="() => handleTopToggle(note.id)" :isTop="note.isTop || false"
:onDelete="() => confirmDeleteNote(note.id)" /> :hasImage="note.hasImage || false"
</div> :onPress="() => handleNotePress(note.id)"
</transition-group> :onStarToggle="() => handleStarToggle(note.id)"
</div> :onTopToggle="() => handleTopToggle(note.id)"
</ion-content> :onDelete="() => confirmDeleteNote(note.id)" />
</div> </div>
</ion-page> </transition-group>
</template> </div>
</ion-content>
<script setup> </div>
import { ref, computed, onMounted } from 'vue' </ion-page>
import { useRouter } from 'vue-router' </template>
import { useAppStore } from '../stores/useAppStore'
import NoteItem from '../components/NoteItem.vue' <script setup>
import Header from '../components/Header.vue' import { ref, computed, onMounted } from 'vue'
import FolderManage from '../components/FolderManage.vue' import { useRouter } from 'vue-router'
import SearchBar from '../components/Search.vue' import { useAppStore } from '../stores/useAppStore'
import { formatNoteListDate } from '../utils/dateUtils' import NoteItem from '../components/NoteItem.vue'
import { IonContent, IonPage } from '@ionic/vue' import Header from '../components/Header.vue'
import FolderManage from '../components/FolderManage.vue'
const store = useAppStore() import SearchBar from '../components/Search.vue'
const router = useRouter() import { formatNoteListDate } from '../utils/dateUtils'
const noteItemRefs = ref({}) import { IonContent, IonPage } from '@ionic/vue'
// 页面挂载时加载初始数据 const store = useAppStore()
onMounted(() => { const router = useRouter()
// 检查URL参数是否包含mock数据加载指令用于开发和演示 const noteItemRefs = ref({})
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.get('mock') === 'true') { // 页面挂载时加载初始数据
// 加载预设的模拟数据 onMounted(() => {
store.loadMockData() // 检查URL参数是否包含mock数据加载指令用于开发和演示
} else { const urlParams = new URLSearchParams(window.location.search)
// 从Storage加载用户数据 if (urlParams.get('mock') === 'true') {
store.loadData() // 加载预设的模拟数据
} store.loadMockData()
}) } else {
// 从Storage加载用户数据
const searchQuery = ref('') store.loadData()
const sortBy = ref('date') // 排序方式:'date'(按日期)、'title'(按标题)、'starred'(按星标) }
const isFolderExpanded = ref(false) // 文件夹列表是否展开 })
const currentFolder = ref('all') // 当前选中的文件夹,默认是"全部便签"
const noteToDelete = ref(null) const searchQuery = ref('')
const sortBy = ref('date') // 排序方式:'date'(按日期)、'title'(按标题)、'starred'(按星标)
// 计算加星便签数量(未删除的) const isFolderExpanded = ref(false) // 文件夹列表是否展开
const starredNotesCount = computed(() => { const currentFolder = ref('all') // 当前选中的文件夹,默认是"全部便签"
return store.notes.filter(note => note.isStarred && !note.isDeleted).length const noteToDelete = ref(null)
})
// 计算加星便签数量(未删除的)
// 计算回收站便签数量 const starredNotesCount = computed(() => {
const trashNotesCount = computed(() => { return store.notes.filter(note => note.isStarred && !note.isDeleted).length
return store.notes.filter(note => note.isDeleted).length })
})
// 计算回收站便签数量
// 根据当前文件夹过滤便签 const trashNotesCount = computed(() => {
const filteredNotes = computed(() => { return store.notes.filter(note => note.isDeleted).length
// 预处理搜索查询,提高性能 })
const lowerCaseQuery = searchQuery.value?.toLowerCase().trim() || ''
// 根据当前文件夹过滤便签
return store.notes.filter(note => { const filteredNotes = computed(() => {
// 先检查搜索条件 // 预处理搜索查询,提高性能
const matchesSearch = const lowerCaseQuery = searchQuery.value?.toLowerCase().trim() || ''
!lowerCaseQuery || (note.title && typeof note.title === 'string' && note.title.toLowerCase().includes(lowerCaseQuery)) || (note.content && typeof note.content === 'string' && note.content.toLowerCase().includes(lowerCaseQuery))
return store.notes.filter(note => {
if (!matchesSearch) return false // 先检查搜索条件
const matchesSearch =
// 再检查文件夹条件 !lowerCaseQuery || (note.title && typeof note.title === 'string' && note.title.toLowerCase().includes(lowerCaseQuery)) || (note.content && typeof note.content === 'string' && note.content.toLowerCase().includes(lowerCaseQuery))
switch (currentFolder.value) {
case 'all': if (!matchesSearch) return false
// 全部便签中不显示已删除的便签
return !note.isDeleted // 再检查文件夹条件
case 'starred': switch (currentFolder.value) {
// 加星便签中只显示未删除的加星便签 case 'all':
return note.isStarred && !note.isDeleted // 全部便签中不显示已删除的便签
case 'trash': return !note.isDeleted
// 回收站中只显示已删除的便签 case 'starred':
return note.isDeleted // 加星便签中只显示未删除的加星便签
default: return note.isStarred && !note.isDeleted
// 自定义文件夹中不显示已删除的便签 case 'trash':
return note.folderId === currentFolder.value && !note.isDeleted // 回收站中只显示已删除的便签
} return note.isDeleted
}) default:
}) // 自定义文件夹中不显示已删除的便签
return note.folderId === currentFolder.value && !note.isDeleted
// 过滤并排序便签列表 }
// 首先按置顶状态排序,置顶的便签排在前面 })
// 然后根据sortBy的值进行二次排序 })
const filteredAndSortedNotes = computed(() => {
return [...filteredNotes.value].sort((a, b) => { // 过滤并排序便签列表
// 置顶的便签排在前面 // 首先按置顶状态排序,置顶的便签排在前面
if (a.isTop && !b.isTop) return -1 // 然后根据sortBy的值进行二次排序
if (!a.isTop && b.isTop) return 1 const filteredAndSortedNotes = computed(() => {
return [...filteredNotes.value].sort((a, b) => {
// 根据排序方式排序 // 置顶的便签排在前面
switch (sortBy.value) { if (a.isTop && !b.isTop) return -1
case 'title': if (!a.isTop && b.isTop) return 1
// 按标题字母顺序排序
return a.title.localeCompare(b.title) // 根据排序方式排序
case 'starred': switch (sortBy.value) {
// 按星标状态排序,加星的便签排在前面 case 'title':
return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0) // 按标题字母顺序排序
case 'date': return a.title.localeCompare(b.title)
default: case 'starred':
// 按更新时间倒序排列(最新的在前 // 按星标状态排序,加星的便签排在前
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0)
} case 'date':
}) default:
}) // 按更新时间倒序排列(最新的在前)
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
// 计算头部标题 }
const headerTitle = computed(() => { })
switch (currentFolder.value) { })
case 'all':
return '全部便签' // 计算头部标题
case 'starred': const headerTitle = computed(() => {
return '加星便签' switch (currentFolder.value) {
case 'trash': case 'all':
return '回收站' return '全部便签'
default: case 'starred':
return '全部便签' return '加星便签'
} case 'trash':
}) return '回收站'
default:
// 计算全部便签数量(未删除的) // 查找自定义文件夹的名称
const allNotesCount = computed(() => { const folder = store.folders.find(f => f.id === currentFolder.value)
return store.notes.filter(note => !note.isDeleted).length return folder ? folder.name : '全部便签'
}) }
})
const handleNotePress = noteId => {
// 检查是否有便签条处于展开状态 // 计算全部便签数量(未删除的)
let hasSlidedNote = false const allNotesCount = computed(() => {
Object.values(noteItemRefs.value).forEach(noteItem => { return store.notes.filter(note => !note.isDeleted).length
// 注意isSlided是ref值需要通过.value访问 })
if (noteItem && noteItem.getSlideState()) {
hasSlidedNote = true const handleNotePress = noteId => {
noteItem.resetSlideState() // 检查是否有便签条处于展开状态
} let hasSlidedNote = false
}) Object.values(noteItemRefs.value).forEach(noteItem => {
// 注意isSlided是ref值需要通过.value访问
// 如果有便签条处于展开状态,不跳转到编辑页面 if (noteItem && noteItem.getSlideState()) {
if (hasSlidedNote) { hasSlidedNote = true
return noteItem.resetSlideState()
} }
})
// 使用vue-router导航到编辑页面
router.push(`/editor/${noteId}`) // 如果有便签条处于展开状态,不跳转到编辑页面
} if (hasSlidedNote) {
return
const handleAddNote = () => { }
// 使用vue-router导航到新建便签页面
router.push('/editor') // 使用vue-router导航到编辑页面
} router.push(`/editor/${noteId}`)
}
// 处理Header组件的操作按钮点击事件
const handleHeaderAction = actionType => { const handleAddNote = () => {
if (actionType === 'create') { // 使用vue-router导航到新建便签页面
handleAddNote() router.push('/editor')
} }
}
// 处理Header组件的操作按钮点击事件
const handleStarToggle = async noteId => { const handleHeaderAction = actionType => {
const note = store.notes.find(n => n.id === noteId) if (actionType === 'create') {
if (note) { handleAddNote()
try { }
await store.updateNote(noteId, { isStarred: !note.isStarred }) }
console.log(`便签 ${noteId} 星标状态已更新`)
} catch (error) { const handleStarToggle = async noteId => {
console.error('更新便签星标状态失败:', error) const note = store.notes.find(n => n.id === noteId)
} if (note) {
} try {
} await store.updateNote(noteId, { isStarred: !note.isStarred })
console.log(`便签 ${noteId} 星标状态已更新`)
const handleTopToggle = async noteId => { } catch (error) {
const note = store.notes.find(n => n.id === noteId) console.error('更新便签星标状态失败:', error)
if (note) { }
try { }
await store.updateNote(noteId, { isTop: !note.isTop }) }
console.log(`便签 ${noteId} 置顶状态已更新`)
} catch (error) { const handleTopToggle = async noteId => {
console.error('更新便签置顶状态失败:', error) const note = store.notes.find(n => n.id === noteId)
} if (note) {
} try {
} await store.updateNote(noteId, { isTop: !note.isTop })
console.log(`便签 ${noteId} 置顶状态已更新`)
const confirmDeleteNote = async noteId => { } catch (error) {
noteToDelete.value = noteId console.error('更新便签置顶状态失败:', error)
if (noteToDelete.value) { }
try { }
// 检查当前是否在回收站中 }
if (currentFolder.value === 'trash') {
// 在回收站中删除便签,彻底删除 const confirmDeleteNote = async noteId => {
await store.permanentlyDeleteNote(noteToDelete.value) noteToDelete.value = noteId
console.log(`便签 ${noteToDelete.value} 已彻底删除`) if (noteToDelete.value) {
} else { try {
// 不在回收站中,将便签移至回收站 // 检查当前是否在回收站中
await store.moveToTrash(noteToDelete.value) if (currentFolder.value === 'trash') {
console.log(`便签 ${noteToDelete.value} 已移至回收站`) // 在回收站中删除便签,彻底删除
} await store.permanentlyDeleteNote(noteToDelete.value)
noteToDelete.value = null console.log(`便签 ${noteToDelete.value} 已彻底删除`)
} catch (error) { } else {
console.error('删除便签失败:', error) // 不在回收站中,将便签移至回收站
} await store.moveToTrash(noteToDelete.value)
} console.log(`便签 ${noteToDelete.value} 已移至回收站`)
} }
noteToDelete.value = null
// 处理排序方式切换 } catch (error) {
// 循环切换排序选项:按日期 -> 按标题 -> 按星标 -> 按日期... console.error('删除便签失败:', error)
const handleSort = () => { }
const sortOptions = ['date', 'title', 'starred'] }
const currentIndex = sortOptions.indexOf(sortBy.value) }
const nextIndex = (currentIndex + 1) % sortOptions.length
sortBy.value = sortOptions[nextIndex] // 处理排序方式切换
console.log('当前排序方式:', sortOptions[nextIndex]) // 循环切换排序选项:按日期 -> 按标题 -> 按星标 -> 按日期...
} const handleSort = () => {
const sortOptions = ['date', 'title', 'starred']
const handleAllNotesClick = () => { const currentIndex = sortOptions.indexOf(sortBy.value)
setCurrentFolder('all') const nextIndex = (currentIndex + 1) % sortOptions.length
setIsFolderExpanded(false) sortBy.value = sortOptions[nextIndex]
} console.log('当前排序方式:', sortOptions[nextIndex])
}
const handleStarredNotesClick = () => {
setCurrentFolder('starred') const handleAllNotesClick = () => {
setIsFolderExpanded(false) setCurrentFolder('all')
} setIsFolderExpanded(false)
}
const handleTrashNotesClick = () => {
setCurrentFolder('trash') const handleStarredNotesClick = () => {
setIsFolderExpanded(false) setCurrentFolder('starred')
} setIsFolderExpanded(false)
}
const handleFolderPress = () => {
// 使用vue-router导航到文件夹页面 const handleTrashNotesClick = () => {
router.push('/folders') setCurrentFolder('trash')
} setIsFolderExpanded(false)
}
const handleSettingsPress = () => {
// 使用vue-router导航到设置页面 const handleFolderClick = folderId => {
router.push('/settings') setCurrentFolder(folderId)
} setIsFolderExpanded(false)
}
const handleFolderToggle = () => {
// 在实际应用中,这里会触发文件夹列表的展开/收起 const handleAddFolder = () => {
isFolderExpanded.value = !isFolderExpanded.value // 文件夹添加功能已在FolderManage组件中实现
} // 这里只需关闭文件夹列表
setIsFolderExpanded(false)
const handleSearch = query => { }
// 搜索功能已在computed属性filteredAndSortedNotes中实现
console.log('搜索:', query) const handleFolderPress = () => {
// 使用vue-router导航到文件夹页面
// 可以在这里添加搜索统计或其它功能 router.push('/folders')
if (query && query.length > 0) { }
console.log(`找到 ${filteredAndSortedNotes.value.length} 个匹配的便签`)
} const handleSettingsPress = () => {
} // 使用vue-router导航到设置页面
router.push('/settings')
const handleClearSearch = () => { }
// 清除搜索已在v-model中处理
console.log('搜索已清除') const handleFolderToggle = () => {
// 在实际应用中,这里会触发文件夹列表的展开/收起
// 清除搜索后可以重置一些状态 isFolderExpanded.value = !isFolderExpanded.value
setSearchQuery('') }
}
const handleSearch = query => {
const handleSearchFocus = () => { // 搜索功能已在computed属性filteredAndSortedNotes中实现
console.log('搜索栏获得焦点') console.log('搜索:', query)
// 可以在这里添加获得焦点时的特殊处理
} // 可以在这里添加搜索统计或其它功能
if (query && query.length > 0) {
const handleSearchBlur = () => { console.log(`找到 ${filteredAndSortedNotes.value.length} 个匹配的便签`)
console.log('搜索栏失去焦点') }
// 可以在这里添加失去焦点时的特殊处理 }
}
const handleClearSearch = () => {
// 防抖函数,用于避免频繁触发搜索 // 清除搜索已在v-model中处理
// 通过延迟执行函数,只在最后一次调用后执行 console.log('搜索已清除')
const debounceSearch = (func, delay) => {
let timeoutId // 清除搜索后可以重置一些状态
return function (...args) { setSearchQuery('')
clearTimeout(timeoutId) }
timeoutId = setTimeout(() => func.apply(this, args), delay)
} const handleSearchFocus = () => {
} console.log('搜索栏获得焦点')
// 可以在这里添加获得焦点时的特殊处理
// 防抖搜索处理函数延迟300ms执行搜索 }
const debouncedHandleSearch = debounceSearch(query => {
handleSearch(query) const handleSearchBlur = () => {
}, 300) console.log('搜索栏失去焦点')
// 可以在这里添加失去焦点时的特殊处理
// 改进的日期格式化函数 }
const formatDate = dateString => {
return formatNoteListDate(dateString) // 防抖函数,用于避免频繁触发搜索
} // 通过延迟执行函数,只在最后一次调用后执行
const debounceSearch = (func, delay) => {
const setCurrentFolder = folder => { let timeoutId
currentFolder.value = folder return function (...args) {
} clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
const setIsFolderExpanded = expanded => { }
isFolderExpanded.value = expanded }
}
// 防抖搜索处理函数延迟300ms执行搜索
const setSearchQuery = query => { const debouncedHandleSearch = debounceSearch(query => {
searchQuery.value = query handleSearch(query)
} }, 300)
const notes = computed(() => store.notes) // 改进的日期格式化函数
</script> const formatDate = dateString => {
<style lang="less" scoped> return formatNoteListDate(dateString)
.container { }
width: 100vw;
height: 100vh; const setCurrentFolder = folder => {
background: url(/assets/icons/drawable-xxhdpi/note_background.png); currentFolder.value = folder
background-size: cover; }
}
const setIsFolderExpanded = expanded => {
.folder-list { isFolderExpanded.value = expanded
position: absolute; }
top: 3.125rem;
left: 10%; const setSearchQuery = query => {
right: 10%; searchQuery.value = query
z-index: 1000; }
background-color: var(--background-card);
border-radius: 0.5rem; const notes = computed(() => store.notes)
box-shadow: 0 0.125rem 0.25rem var(--shadow); </script>
border: 1px solid #f0ece7; <style lang="less" scoped>
overflow: hidden; .container {
} width: 100vw;
height: 100vh;
.folder-overlay { background: url(/assets/icons/drawable-xxhdpi/note_background.png);
position: absolute; background-size: cover;
top: 0; }
left: 0;
right: 0; .folder-list {
bottom: 0; position: absolute;
background-color: transparent; top: 3.125rem;
z-index: 99; left: 10%;
} right: 10%;
.content { z-index: 1000;
--background: transparent; background-color: var(--background-card);
--padding-top: 4.5rem; border-radius: 0.5rem;
--padding-bottom: 2rem; box-shadow: 0 0.125rem 0.25rem var(--shadow);
} border: 1px solid #f0ece7;
overflow: hidden;
.search-container { }
padding: 0.8rem 0.5rem;
} .folder-overlay {
position: absolute;
.notes-container { top: 0;
flex: 1; left: 0;
position: relative; right: 0;
} bottom: 0;
background-color: transparent;
.notes-list { z-index: 99;
position: relative; }
} .content {
--background: transparent;
.note-item { --padding-top: 4.5rem;
margin: 0.6rem 0; --padding-bottom: 2rem;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); }
}
.search-container {
/* 便签列表动画 */ padding: 0.8rem 0.5rem;
.note-list-enter-active, }
.note-list-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); .notes-container {
} flex: 1;
position: relative;
.note-list-leave-to { }
opacity: 0;
transform: translateX(-30px); .notes-list {
} position: relative;
}
.note-list-move {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); .note-item {
} margin: 0.6rem 0;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
.note-list-leave-active { }
position: absolute;
width: calc(100% - 1rem); /* 便签列表动画 */
} .note-list-enter-active,
</style> .note-list-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.note-list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.note-list-move {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.note-list-leave-active {
position: absolute;
width: calc(100% - 1rem);
}
/* 文件夹列表动画 */
.folder-slide-enter-active,
.folder-slide-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.folder-slide-enter-from {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
.folder-slide-enter-to {
opacity: 1;
transform: scale(1) translateY(0);
}
.folder-slide-leave-from {
opacity: 1;
transform: scale(1) translateY(0);
}
.folder-slide-leave-to {
opacity: 0;
transform: scale(0.8) translateY(-20px);
}
</style>

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)
}
} }

113
src/utils/modalService.js Normal file
View File

@@ -0,0 +1,113 @@
import { createApp } from 'vue'
import Modal from '@/components/Modal.vue'
/**
* 全局弹框服务
* 提供统一的弹框调用接口,使用项目中已有的 Modal 组件
*/
// 创建一个全局的 Modal 实例容器
let modalInstance = null
let modalContainer = null
/**
* 初始化 Modal 实例
* 在应用启动时调用一次
*/
export const initModalService = () => {
if (!modalContainer) {
// 创建一个隐藏的 div 作为 Modal 的挂载点
modalContainer = document.createElement('div')
document.body.appendChild(modalContainer)
// 创建 Modal 实例
const app = createApp(Modal)
modalInstance = app.mount(modalContainer)
}
return modalInstance
}
/**
* 显示确认弹框
* @param {string} message - 弹框消息
* @param {string} title - 弹框标题
* @param {Object} options - 弹框选项
* @returns {Promise<boolean>} 用户确认返回 true取消返回 false
*/
export const showConfirm = (message, title = '确认', options = {}) => {
// 确保 Modal 实例已初始化
if (!modalInstance) {
initModalService()
}
// 显示弹框
return modalInstance.show({
title,
message,
showInput: false,
showConfirm: true,
showCancel: true,
...options
})
}
/**
* 显示提示弹框
* @param {string} message - 弹框消息
* @param {string} title - 弹框标题
* @param {Object} options - 弹框选项
* @returns {Promise<void>}
*/
export const showAlert = (message, title = '提示', options = {}) => {
// 确保 Modal 实例已初始化
if (!modalInstance) {
initModalService()
}
// 显示弹框
return modalInstance.show({
title,
message,
showInput: false,
showConfirm: true,
showCancel: false,
...options
})
}
/**
* 显示输入弹框
* @param {string} message - 弹框消息
* @param {string} title - 弹框标题
* @param {string} placeholder - 输入框占位符
* @param {string} defaultValue - 输入框默认值
* @param {Object} options - 弹框选项
* @returns {Promise<string|null>} 用户输入的值,取消返回 null
*/
export const showPrompt = (message, title = '输入', placeholder = '请输入文字', defaultValue = '', options = {}) => {
// 确保 Modal 实例已初始化
if (!modalInstance) {
initModalService()
}
// 显示弹框
return modalInstance.show({
title,
message,
showInput: true,
showConfirm: true,
showCancel: true,
inputPlaceholder: placeholder,
inputValue: defaultValue,
...options
})
}
// 默认导出所有方法
export default {
initModalService,
showConfirm,
showAlert,
showPrompt
}