You've already forked SmartisanNote.Remake
Merge pull request 'future' (#13) from future into main
Reviewed-on: yuantao/SmartisanNote.Remake#13
This commit is contained in:
@@ -88,6 +88,10 @@
|
||||
--white-60: #ffffff99; /* 60% white */
|
||||
--white-80: #ffffffcc; /* 80% white */
|
||||
--white-90: #ffffffe6; /* 90% white */
|
||||
|
||||
--confirmFontSize: 0.8rem;
|
||||
--confirmBg: rgba(0, 0, 0, 0.15);
|
||||
--confirmBtnColor: #000000cc;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"moment": "^2.30.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-draggable-plus": "^0.6.0",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
20
src/App.vue
20
src/App.vue
@@ -13,19 +13,17 @@
|
||||
</template>
|
||||
|
||||
<!-- 设置页面 -->
|
||||
<transition
|
||||
name="settings-slide"
|
||||
v-show="isSettingsRoute"
|
||||
appear>
|
||||
<transition name="settings-slide" v-show="isSettingsRoute" appear>
|
||||
<SettingsPage class="setting-page" />
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import '@/common/base.css'
|
||||
import { initModalService } from '@/utils/modalService'
|
||||
|
||||
// 导入页面组件
|
||||
import NoteListPage from './pages/NoteListPage.vue'
|
||||
@@ -33,6 +31,7 @@ import SettingsPage from './pages/SettingsPage.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const transitionName = ref('slide-left')
|
||||
const modalRef = ref()
|
||||
|
||||
// 计算是否为设置页面路由
|
||||
const isSettingsRoute = computed(() => {
|
||||
@@ -53,15 +52,15 @@ watch(
|
||||
transitionName.value = 'slide-right'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 判断导航方向
|
||||
const toDepth = toPath.split('/').length
|
||||
const fromDepth = fromPath.split('/').length
|
||||
|
||||
|
||||
// 如果是进入更深的页面(如从列表页进入编辑页),使用左滑动画
|
||||
if (toDepth > fromDepth) {
|
||||
transitionName.value = 'slide-left'
|
||||
}
|
||||
}
|
||||
// 如果是返回上层页面(如从编辑页返回列表页),使用右滑动画
|
||||
else if (toDepth < fromDepth) {
|
||||
transitionName.value = 'slide-right'
|
||||
@@ -73,7 +72,10 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 无额外处理函数
|
||||
// 初始化弹框服务
|
||||
onMounted(() => {
|
||||
initModalService()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,77 +1,168 @@
|
||||
<template>
|
||||
<div @click="onPress" class="code-fun-flex-row code-fun-items-center code-fun-relative folder-item">
|
||||
<img class="folder-icon" :src="iconSrc" />
|
||||
<span class="folder-name">{{ name }}</span>
|
||||
<span class="folder-count">{{ noteCount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noteCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
onPress: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
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'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style 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;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div @click="onPress" class="code-fun-flex-row code-fun-items-center code-fun-relative code-fun-justify-between folder-item">
|
||||
<div class="code-fun-flex-row code-fun-items-center">
|
||||
<!-- 文件夹图标或复选框 -->
|
||||
<div v-if="isSelectionMode && showDeleteButton" class="folder-checkbox" @click.stop="onCheckboxClick">
|
||||
<img :src="isChecked ? '/assets/icons/drawable-xxhdpi/check_box_on.png' : '/assets/icons/drawable-xxhdpi/check_box_off.png'" class="checkbox-icon" />
|
||||
</div>
|
||||
|
||||
<img v-else class="folder-icon" :src="iconSrc" />
|
||||
<span class="folder-name">{{ name }}</span>
|
||||
<span class="folder-count">{{ noteCount }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 编辑按钮,仅在选择模式下对自定义文件夹显示 -->
|
||||
<div v-if="showDeleteButton && isSelectionMode" class="folder-actions">
|
||||
<img class="edit-icon" src="/assets/icons/drawable-xxhdpi/icon_folder_rename.png" @click.stop="onEdit" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
noteCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
|
||||
onPress: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
onEdit: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
|
||||
onDelete: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
isSelectionMode: {
|
||||
type: Boolean,
|
||||
|
||||
default: false,
|
||||
},
|
||||
|
||||
isChecked: {
|
||||
type: Boolean,
|
||||
|
||||
default: false,
|
||||
},
|
||||
|
||||
onCheckboxClick: {
|
||||
type: Function,
|
||||
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,135 +1,335 @@
|
||||
<template>
|
||||
<div class="code-fun-flex-col page">
|
||||
<!-- 全部便签文件夹项 -->
|
||||
<FolderItem id="all" name="全部便签" :noteCount="allCount" :isSelected="selectedFolder === 'all'" :onPress="handleAllClick" />
|
||||
|
||||
<!-- 加星便签文件夹项 -->
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div class="code-fun-flex-row code-fun-items-center">
|
||||
<img class="image_8 code-fun-ml-12" :src="`assets/icons/drawable-xxhdpi/btn_add_folder.png`" @click="handleAddFolder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import FolderItem from './FolderItem.vue'
|
||||
|
||||
const props = defineProps({
|
||||
allCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
starredCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
trashCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
archiveCount: {
|
||||
type: Number,
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
const handleAllClick = () => {
|
||||
if (props.onAllClick) {
|
||||
props.onAllClick()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStarredClick = () => {
|
||||
if (props.onStarredClick) {
|
||||
props.onStarredClick()
|
||||
}
|
||||
}
|
||||
|
||||
const handleTrashClick = () => {
|
||||
if (props.onTrashClick) {
|
||||
props.onTrashClick()
|
||||
}
|
||||
}
|
||||
|
||||
const handleArchiveClick = () => {
|
||||
if (props.onArchiveClick) {
|
||||
props.onArchiveClick()
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddFolder = event => {
|
||||
// 阻止事件冒泡到父元素
|
||||
event.stopPropagation()
|
||||
if (props.onAddFolder) {
|
||||
props.onAddFolder()
|
||||
}
|
||||
}
|
||||
</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: 2rem;
|
||||
height: 2rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.text_11 {
|
||||
color: #cacaca;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.16rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="code-fun-flex-col page">
|
||||
<!-- 全部便签文件夹项 -->
|
||||
|
||||
<FolderItem id="all" name="全部便签" :noteCount="allCount" :isSelected="selectedFolder === 'all'" :onPress="handleAllClick" />
|
||||
|
||||
<!-- 加星便签文件夹项 -->
|
||||
|
||||
<FolderItem id="starred" name="加星便签" :noteCount="starredCount" :isSelected="selectedFolder === 'starred'" :onPress="handleStarredClick" />
|
||||
|
||||
<!-- 回收站文件夹项 -->
|
||||
|
||||
<FolderItem id="trash" name="回收站" :noteCount="trashCount" :isSelected="selectedFolder === 'trash'" :onPress="handleTrashClick" />
|
||||
|
||||
<!-- 自定义文件夹项 -->
|
||||
|
||||
<FolderItem
|
||||
v-for="folder in customFolders"
|
||||
:key="folder.id"
|
||||
:id="folder.id"
|
||||
:name="folder.name"
|
||||
:noteCount="getFolderNoteCount(folder.id)"
|
||||
:isSelected="selectedFolder === folder.id"
|
||||
:onPress="() => handleFolderClick(folder.id)"
|
||||
:onEdit="() => handleEditFolder(folder.id)"
|
||||
:onDelete="() => handleDeleteFolder(folder.id)"
|
||||
:isSelectionMode="isSelectionMode"
|
||||
:isChecked="selectedFolders.includes(folder.id)"
|
||||
:onCheckboxClick="() => toggleFolderSelection(folder.id)" />
|
||||
|
||||
<div class="code-fun-flex-col code-fun-justify-start section_7">
|
||||
<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="isSelectionMode ? 'assets/icons/drawable-xxhdpi/btn_back_black.png' : 'assets/icons/drawable-xxhdpi/btn_edit_folder.png'" @click="toggleSelectionMode" />
|
||||
|
||||
<!-- 同步信息区域 -->
|
||||
|
||||
<span class="text_11 code-fun-ml-10">上次同步:{{ lastSyncTime }}</span>
|
||||
</div>
|
||||
|
||||
<div class="code-fun-flex-row code-fun-items-center">
|
||||
<!-- 新建文件夹按钮 / 删除按钮 -->
|
||||
|
||||
<img
|
||||
class="image_8 code-fun-ml-12"
|
||||
:src="isSelectionMode ? 'assets/icons/drawable-xxhdpi/icon_folder_trash.png' : 'assets/icons/drawable-xxhdpi/btn_add_folder.png'"
|
||||
@click="isSelectionMode ? handleDeleteSelectedFolders() : handleAddFolder()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { useAppStore } from '../stores/useAppStore'
|
||||
import FolderItem from './FolderItem.vue'
|
||||
import { showConfirm, showPrompt } from '../utils/modalService'
|
||||
|
||||
const store = useAppStore()
|
||||
const props = defineProps({
|
||||
allCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
starredCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
trashCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
archiveCount: {
|
||||
type: Number,
|
||||
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 showRenameFolderModal = ref(false)
|
||||
|
||||
const renameFolderId = ref(null)
|
||||
|
||||
const renameFolderName = ref('')
|
||||
// 选择模式相关状态
|
||||
|
||||
const isSelectionMode = ref(false)
|
||||
|
||||
const selectedFolders = ref([])
|
||||
// 计算自定义文件夹(排除系统文件夹)
|
||||
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 = 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>
|
||||
|
||||
@@ -1,206 +1,347 @@
|
||||
<template>
|
||||
<div class="code-fun-flex-col code-fun-justify-start">
|
||||
<div class="code-fun-flex-col code-fun-justify-start group_1">
|
||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-end group">
|
||||
<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="code-fun-flex-col code-fun-justify-start section_1">
|
||||
<div class="code-fun-flex-row section_11">
|
||||
<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">
|
||||
<span class="text_4">{{ cancelText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-fun-flex-col code-fun-justify-start section_13 code-fun-ml-8" @click="handleConfirm">
|
||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-center text-wrapper_3">
|
||||
<span class="text_5">{{ confirmText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-relative section_9">
|
||||
<div class="code-fun-flex-col code-fun-justify-start section_10">
|
||||
<div class="code-fun-flex-col code-fun-justify-start code-fun-items-start text-wrapper">
|
||||
<input
|
||||
v-model="inputValue"
|
||||
class="text_3"
|
||||
:placeholder="placeholder"
|
||||
@keyup.enter="handleConfirm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-fun-flex-col section_8 pos">
|
||||
<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>
|
||||
</div>
|
||||
<div class="code-fun-self-start divider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '新建文件夹'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '新建文件夹'
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel']);
|
||||
|
||||
const inputValue = ref('');
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', inputValue.value);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.group_1 {
|
||||
padding-bottom: 1rem;
|
||||
.group {
|
||||
padding: 0.5rem 0 0.25rem;
|
||||
.group_2 {
|
||||
margin-right: 0.75rem;
|
||||
width: 32.31rem;
|
||||
.section_7 {
|
||||
padding-top: 5.75rem;
|
||||
background-color: #00000000;
|
||||
width: 29.06rem;
|
||||
height: 18.16rem;
|
||||
background-image: url(https://codefun-proj-user-res-1256085488.cos.ap-guangzhou.myqcloud.com/686f20ecd54496f19f54e801/68e862ab9520a30011f388ff/17600601480475170758.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% auto;
|
||||
background-position: 0% 0%;
|
||||
.section_1 {
|
||||
margin-top: 7.88rem;
|
||||
background-color: #00000000;
|
||||
.section_11 {
|
||||
padding: 0.5rem 0.5rem 0.75rem;
|
||||
background-color: #f4f4f7;
|
||||
border-radius: 0rem 0rem 0.75rem 0.75rem;
|
||||
border: solid 0.032rem #edeee8;
|
||||
.section_12 {
|
||||
padding: 0.25rem 0;
|
||||
background-color: #00000000;
|
||||
width: 13.63rem;
|
||||
height: 3.13rem;
|
||||
.text-wrapper_2 {
|
||||
margin: 0 0.25rem;
|
||||
padding: 0.75rem 0;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 0.25rem;
|
||||
width: 13.34rem;
|
||||
border: solid 0.032rem #d7d7d7;
|
||||
.text_4 {
|
||||
color: #757575;
|
||||
font-size: 1.23rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.23rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.section_13 {
|
||||
margin-right: 0.25rem;
|
||||
padding-top: 0.25rem;
|
||||
background-color: #00000000;
|
||||
width: 13.59rem;
|
||||
height: 3.06rem;
|
||||
.text-wrapper_3 {
|
||||
margin-left: 0.25rem;
|
||||
padding: 0.75rem 0;
|
||||
border-radius: 0.25rem;
|
||||
background-image: url('assets/53062683132af1946e1a4953530af228.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
width: 13.34rem;
|
||||
.text_5 {
|
||||
color: #cddff2;
|
||||
font-size: 1.19rem;
|
||||
line-height: 1.19rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.section_9 {
|
||||
margin-top: -12.5rem;
|
||||
padding: 1.75rem 0 0.25rem;
|
||||
background-color: #00000000;
|
||||
.section_10 {
|
||||
margin: 0 1.5rem;
|
||||
padding-top: 0.25rem;
|
||||
background-color: #00000000;
|
||||
width: 25.88rem;
|
||||
.text-wrapper {
|
||||
margin: 0 0.25rem;
|
||||
padding: 0.75rem 0;
|
||||
background-color: #fefefe;
|
||||
border-radius: 0.25rem;
|
||||
width: 25.53rem;
|
||||
border: solid 0.063rem #e1e1e1;
|
||||
.text_3 {
|
||||
margin-left: 1rem;
|
||||
color: #d0d0d0;
|
||||
font-size: 1.38rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.38rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.section_8 {
|
||||
padding: 0 0.13rem;
|
||||
background-color: #00000000;
|
||||
width: 32.31rem;
|
||||
.group_3 {
|
||||
padding: 1.5rem 0;
|
||||
width: 32.06rem;
|
||||
.text_2 {
|
||||
margin-left: 10.5rem;
|
||||
color: #7a7a7a;
|
||||
font-size: 1.54rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.54rem;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
background-color: #f0f0f0;
|
||||
width: 29rem;
|
||||
height: 0.094rem;
|
||||
}
|
||||
}
|
||||
.pos {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div v-if="visible" class="pd-mask" @click="handleMaskClick">
|
||||
<div class="pd-confirm" @click.stop>
|
||||
<h2 class="pd-title">{{ dynamicTitle || title }}</h2>
|
||||
|
||||
<div class="pd-input-container" v-if="dynamicShowInput || showInput">
|
||||
<input ref="inputRef" v-model="inputModel" class="pd-input" :placeholder="dynamicInputPlaceholder || inputPlaceholder" @keyup.enter="handleConfirm" />
|
||||
</div>
|
||||
|
||||
<p class="pd-message" v-else>{{ dynamicMessage || message }}</p>
|
||||
|
||||
<div class="pd-buttons">
|
||||
<button v-if="dynamicShowConfirm || showConfirm" class="pd-button pd-confirm-btn" @click="handleConfirm">
|
||||
{{ dynamicConfirmText || confirmText }}
|
||||
</button>
|
||||
|
||||
<button v-if="dynamicShowCancel || showCancel" class="pd-button pd-cancel" @click="handleCancel">
|
||||
{{ dynamicCancelText || cancelText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 响应式数据用于动态更新 Modal 内容
|
||||
|
||||
const dynamicTitle = ref('')
|
||||
|
||||
const dynamicMessage = ref('')
|
||||
|
||||
const dynamicConfirmText = ref('确认')
|
||||
|
||||
const dynamicCancelText = ref('取消')
|
||||
|
||||
const dynamicShowConfirm = ref(true)
|
||||
|
||||
const dynamicShowCancel = ref(true)
|
||||
|
||||
const dynamicMaskClosable = ref(false)
|
||||
|
||||
const dynamicShowInput = ref(false)
|
||||
|
||||
const dynamicInputPlaceholder = ref('请输入文字')
|
||||
|
||||
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: false,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
default: '请输入文字',
|
||||
},
|
||||
inputValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel', 'update:visible'])
|
||||
const visible = defineModel('visible', { type: Boolean, default: false })
|
||||
const inputModel = defineModel('inputValue', { type: String, default: '' })
|
||||
|
||||
const inputRef = ref()
|
||||
|
||||
// Promise 控制变量
|
||||
|
||||
let resolvePromise, rejectPromise
|
||||
|
||||
// 返回 Promise 的方法,模拟原生 confirm 行为
|
||||
|
||||
const show = (options = {}) => {
|
||||
// 更新动态数据
|
||||
|
||||
if (options.title !== undefined) dynamicTitle.value = options.title
|
||||
|
||||
if (options.message !== undefined) dynamicMessage.value = options.message
|
||||
|
||||
if (options.confirmText !== undefined) dynamicConfirmText.value = options.confirmText
|
||||
|
||||
if (options.cancelText !== undefined) dynamicCancelText.value = options.cancelText
|
||||
|
||||
if (options.showConfirm !== undefined) dynamicShowConfirm.value = options.showConfirm
|
||||
|
||||
if (options.showCancel !== undefined) dynamicShowCancel.value = options.showCancel
|
||||
|
||||
if (options.maskClosable !== undefined) dynamicMaskClosable.value = options.maskClosable
|
||||
|
||||
if (options.showInput !== undefined) dynamicShowInput.value = options.showInput
|
||||
|
||||
if (options.inputPlaceholder !== undefined) dynamicInputPlaceholder.value = options.inputPlaceholder
|
||||
|
||||
if (options.inputValue !== undefined) inputModel.value = options.inputValue
|
||||
|
||||
// 显示对话框
|
||||
|
||||
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(inputModel.value)
|
||||
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, oldVal) => {
|
||||
if (newVal) {
|
||||
lockBodyScroll()
|
||||
// 如果模态框包含输入框,则自动获取焦点
|
||||
if (dynamicShowInput.value || props.showInput) {
|
||||
// 使用 nextTick 确保在 DOM 更新后聚焦
|
||||
nextTick(() => {
|
||||
// 添加更长的延迟确保动画完成
|
||||
setTimeout(() => {
|
||||
// 确保 inputRef 存在并且可见
|
||||
if (inputRef.value && inputRef.value.offsetParent !== null) {
|
||||
inputRef.value.focus()
|
||||
inputRef.value.select() // 选中所有文本,提升用户体验
|
||||
}
|
||||
}, 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>
|
||||
|
||||
@@ -1,430 +1,473 @@
|
||||
<template>
|
||||
<ion-page>
|
||||
<div class="container">
|
||||
<ion-content class="content">
|
||||
<Header
|
||||
:title="headerTitle"
|
||||
:onAction="handleHeaderAction"
|
||||
actionIcon="create"
|
||||
leftType="settings"
|
||||
:onLeftAction="handleSettingsPress"
|
||||
:onFolderToggle="handleFolderToggle"
|
||||
:isFolderExpanded="isFolderExpanded"
|
||||
:onTitlePress="handleFolderToggle"
|
||||
slot="fixed" />
|
||||
|
||||
<!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
|
||||
<div v-if="isFolderExpanded" class="folder-list" slot="fixed">
|
||||
<FolderManage
|
||||
:allCount="allNotesCount"
|
||||
:starredCount="starredNotesCount"
|
||||
:trashCount="trashNotesCount"
|
||||
:archiveCount="0"
|
||||
:selectedFolder="currentFolder"
|
||||
:onAllClick="handleAllNotesClick"
|
||||
:onStarredClick="handleStarredNotesClick"
|
||||
:onTrashClick="handleTrashNotesClick" />
|
||||
</div>
|
||||
<!-- 点击外部区域收起文件夹列表的覆盖层 -->
|
||||
<div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" class="folder-overlay" slot="fixed"></div>
|
||||
<div class="search-container">
|
||||
<SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" />
|
||||
</div>
|
||||
|
||||
<div class="notes-container">
|
||||
<transition-group name="note-list" tag="div" class="notes-list">
|
||||
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
|
||||
<NoteItem
|
||||
:ref="
|
||||
el => {
|
||||
if (el) noteItemRefs[note.id] = el
|
||||
}
|
||||
"
|
||||
:content="note.content"
|
||||
:date="formatDate(note.updatedAt)"
|
||||
:isStarred="note.isStarred"
|
||||
:isTop="note.isTop || false"
|
||||
:hasImage="note.hasImage || false"
|
||||
:onPress="() => handleNotePress(note.id)"
|
||||
:onStarToggle="() => handleStarToggle(note.id)"
|
||||
:onTopToggle="() => handleTopToggle(note.id)"
|
||||
:onDelete="() => confirmDeleteNote(note.id)" />
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/useAppStore'
|
||||
import NoteItem from '../components/NoteItem.vue'
|
||||
import Header from '../components/Header.vue'
|
||||
import FolderManage from '../components/FolderManage.vue'
|
||||
import SearchBar from '../components/Search.vue'
|
||||
import { formatNoteListDate } from '../utils/dateUtils'
|
||||
import { IonContent, IonPage } from '@ionic/vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
const noteItemRefs = ref({})
|
||||
|
||||
// 页面挂载时加载初始数据
|
||||
onMounted(() => {
|
||||
// 检查URL参数是否包含mock数据加载指令,用于开发和演示
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.get('mock') === 'true') {
|
||||
// 加载预设的模拟数据
|
||||
store.loadMockData()
|
||||
} else {
|
||||
// 从Storage加载用户数据
|
||||
store.loadData()
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref('')
|
||||
const sortBy = ref('date') // 排序方式:'date'(按日期)、'title'(按标题)、'starred'(按星标)
|
||||
const isFolderExpanded = ref(false) // 文件夹列表是否展开
|
||||
const currentFolder = ref('all') // 当前选中的文件夹,默认是"全部便签"
|
||||
const noteToDelete = ref(null)
|
||||
|
||||
// 计算加星便签数量(未删除的)
|
||||
const starredNotesCount = computed(() => {
|
||||
return store.notes.filter(note => note.isStarred && !note.isDeleted).length
|
||||
})
|
||||
|
||||
// 计算回收站便签数量
|
||||
const trashNotesCount = computed(() => {
|
||||
return store.notes.filter(note => note.isDeleted).length
|
||||
})
|
||||
|
||||
// 根据当前文件夹过滤便签
|
||||
const filteredNotes = computed(() => {
|
||||
// 预处理搜索查询,提高性能
|
||||
const lowerCaseQuery = searchQuery.value?.toLowerCase().trim() || ''
|
||||
|
||||
return store.notes.filter(note => {
|
||||
// 先检查搜索条件
|
||||
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))
|
||||
|
||||
if (!matchesSearch) return false
|
||||
|
||||
// 再检查文件夹条件
|
||||
switch (currentFolder.value) {
|
||||
case 'all':
|
||||
// 全部便签中不显示已删除的便签
|
||||
return !note.isDeleted
|
||||
case 'starred':
|
||||
// 加星便签中只显示未删除的加星便签
|
||||
return note.isStarred && !note.isDeleted
|
||||
case 'trash':
|
||||
// 回收站中只显示已删除的便签
|
||||
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
|
||||
if (!a.isTop && b.isTop) return 1
|
||||
|
||||
// 根据排序方式排序
|
||||
switch (sortBy.value) {
|
||||
case 'title':
|
||||
// 按标题字母顺序排序
|
||||
return a.title.localeCompare(b.title)
|
||||
case 'starred':
|
||||
// 按星标状态排序,加星的便签排在前面
|
||||
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':
|
||||
return '加星便签'
|
||||
case 'trash':
|
||||
return '回收站'
|
||||
default:
|
||||
return '全部便签'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算全部便签数量(未删除的)
|
||||
const allNotesCount = computed(() => {
|
||||
return store.notes.filter(note => !note.isDeleted).length
|
||||
})
|
||||
|
||||
const handleNotePress = noteId => {
|
||||
// 检查是否有便签条处于展开状态
|
||||
let hasSlidedNote = false
|
||||
Object.values(noteItemRefs.value).forEach(noteItem => {
|
||||
// 注意:isSlided是ref值,需要通过.value访问
|
||||
if (noteItem && noteItem.getSlideState()) {
|
||||
hasSlidedNote = true
|
||||
noteItem.resetSlideState()
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有便签条处于展开状态,不跳转到编辑页面
|
||||
if (hasSlidedNote) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用vue-router导航到编辑页面
|
||||
router.push(`/editor/${noteId}`)
|
||||
}
|
||||
|
||||
const handleAddNote = () => {
|
||||
// 使用vue-router导航到新建便签页面
|
||||
router.push('/editor')
|
||||
}
|
||||
|
||||
// 处理Header组件的操作按钮点击事件
|
||||
const handleHeaderAction = actionType => {
|
||||
if (actionType === 'create') {
|
||||
handleAddNote()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStarToggle = async noteId => {
|
||||
const note = store.notes.find(n => n.id === noteId)
|
||||
if (note) {
|
||||
try {
|
||||
await store.updateNote(noteId, { isStarred: !note.isStarred })
|
||||
console.log(`便签 ${noteId} 星标状态已更新`)
|
||||
} catch (error) {
|
||||
console.error('更新便签星标状态失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTopToggle = async noteId => {
|
||||
const note = store.notes.find(n => n.id === noteId)
|
||||
if (note) {
|
||||
try {
|
||||
await store.updateNote(noteId, { isTop: !note.isTop })
|
||||
console.log(`便签 ${noteId} 置顶状态已更新`)
|
||||
} catch (error) {
|
||||
console.error('更新便签置顶状态失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteNote = async noteId => {
|
||||
noteToDelete.value = noteId
|
||||
if (noteToDelete.value) {
|
||||
try {
|
||||
// 检查当前是否在回收站中
|
||||
if (currentFolder.value === 'trash') {
|
||||
// 在回收站中删除便签,彻底删除
|
||||
await store.permanentlyDeleteNote(noteToDelete.value)
|
||||
console.log(`便签 ${noteToDelete.value} 已彻底删除`)
|
||||
} else {
|
||||
// 不在回收站中,将便签移至回收站
|
||||
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 handleAllNotesClick = () => {
|
||||
setCurrentFolder('all')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleStarredNotesClick = () => {
|
||||
setCurrentFolder('starred')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleTrashNotesClick = () => {
|
||||
setCurrentFolder('trash')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleFolderPress = () => {
|
||||
// 使用vue-router导航到文件夹页面
|
||||
router.push('/folders')
|
||||
}
|
||||
|
||||
const handleSettingsPress = () => {
|
||||
// 使用vue-router导航到设置页面
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
const handleFolderToggle = () => {
|
||||
// 在实际应用中,这里会触发文件夹列表的展开/收起
|
||||
isFolderExpanded.value = !isFolderExpanded.value
|
||||
}
|
||||
|
||||
const handleSearch = query => {
|
||||
// 搜索功能已在computed属性filteredAndSortedNotes中实现
|
||||
console.log('搜索:', query)
|
||||
|
||||
// 可以在这里添加搜索统计或其它功能
|
||||
if (query && query.length > 0) {
|
||||
console.log(`找到 ${filteredAndSortedNotes.value.length} 个匹配的便签`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
// 清除搜索已在v-model中处理
|
||||
console.log('搜索已清除')
|
||||
|
||||
// 清除搜索后可以重置一些状态
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const handleSearchFocus = () => {
|
||||
console.log('搜索栏获得焦点')
|
||||
// 可以在这里添加获得焦点时的特殊处理
|
||||
}
|
||||
|
||||
const handleSearchBlur = () => {
|
||||
console.log('搜索栏失去焦点')
|
||||
// 可以在这里添加失去焦点时的特殊处理
|
||||
}
|
||||
|
||||
// 防抖函数,用于避免频繁触发搜索
|
||||
// 通过延迟执行函数,只在最后一次调用后执行
|
||||
const debounceSearch = (func, delay) => {
|
||||
let timeoutId
|
||||
return function (...args) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => func.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖搜索处理函数,延迟300ms执行搜索
|
||||
const debouncedHandleSearch = debounceSearch(query => {
|
||||
handleSearch(query)
|
||||
}, 300)
|
||||
|
||||
// 改进的日期格式化函数
|
||||
const formatDate = dateString => {
|
||||
return formatNoteListDate(dateString)
|
||||
}
|
||||
|
||||
const setCurrentFolder = folder => {
|
||||
currentFolder.value = folder
|
||||
}
|
||||
|
||||
const setIsFolderExpanded = expanded => {
|
||||
isFolderExpanded.value = expanded
|
||||
}
|
||||
|
||||
const setSearchQuery = query => {
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
const notes = computed(() => store.notes)
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url(/assets/icons/drawable-xxhdpi/note_background.png);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
position: absolute;
|
||||
top: 3.125rem;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
z-index: 1000;
|
||||
background-color: var(--background-card);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem var(--shadow);
|
||||
border: 1px solid #f0ece7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.folder-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
z-index: 99;
|
||||
}
|
||||
.content {
|
||||
--background: transparent;
|
||||
--padding-top: 4.5rem;
|
||||
--padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 0.8rem 0.5rem;
|
||||
}
|
||||
|
||||
.notes-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notes-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
margin: 0.6rem 0;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* 便签列表动画 */
|
||||
.note-list-enter-active,
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<ion-page>
|
||||
<div class="container">
|
||||
<ion-content class="content">
|
||||
<Header
|
||||
:title="headerTitle"
|
||||
:onAction="handleHeaderAction"
|
||||
actionIcon="create"
|
||||
leftType="settings"
|
||||
:onLeftAction="handleSettingsPress"
|
||||
:onFolderToggle="handleFolderToggle"
|
||||
:isFolderExpanded="isFolderExpanded"
|
||||
:onTitlePress="handleFolderToggle"
|
||||
slot="fixed" />
|
||||
|
||||
<!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
|
||||
<transition name="folder-slide">
|
||||
<div v-if="isFolderExpanded" class="folder-list" slot="fixed">
|
||||
<FolderManage
|
||||
:allCount="allNotesCount"
|
||||
:starredCount="starredNotesCount"
|
||||
:trashCount="trashNotesCount"
|
||||
:archiveCount="0"
|
||||
:selectedFolder="currentFolder"
|
||||
:onAllClick="handleAllNotesClick"
|
||||
:onStarredClick="handleStarredNotesClick"
|
||||
:onTrashClick="handleTrashNotesClick"
|
||||
:onFolderClick="handleFolderClick"
|
||||
:onAddFolder="handleAddFolder" />
|
||||
</div>
|
||||
</transition>
|
||||
<!-- 点击外部区域收起文件夹列表的覆盖层 -->
|
||||
<div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" class="folder-overlay" slot="fixed"></div>
|
||||
<div class="search-container">
|
||||
<SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" />
|
||||
</div>
|
||||
|
||||
<div class="notes-container">
|
||||
<transition-group name="note-list" tag="div" class="notes-list">
|
||||
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
|
||||
<NoteItem
|
||||
:ref="
|
||||
el => {
|
||||
if (el) noteItemRefs[note.id] = el
|
||||
}
|
||||
"
|
||||
:content="note.content"
|
||||
:date="formatDate(note.updatedAt)"
|
||||
:isStarred="note.isStarred"
|
||||
:isTop="note.isTop || false"
|
||||
:hasImage="note.hasImage || false"
|
||||
:onPress="() => handleNotePress(note.id)"
|
||||
:onStarToggle="() => handleStarToggle(note.id)"
|
||||
:onTopToggle="() => handleTopToggle(note.id)"
|
||||
:onDelete="() => confirmDeleteNote(note.id)" />
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/useAppStore'
|
||||
import NoteItem from '../components/NoteItem.vue'
|
||||
import Header from '../components/Header.vue'
|
||||
import FolderManage from '../components/FolderManage.vue'
|
||||
import SearchBar from '../components/Search.vue'
|
||||
import { formatNoteListDate } from '../utils/dateUtils'
|
||||
import { IonContent, IonPage } from '@ionic/vue'
|
||||
|
||||
const store = useAppStore()
|
||||
const router = useRouter()
|
||||
const noteItemRefs = ref({})
|
||||
|
||||
// 页面挂载时加载初始数据
|
||||
onMounted(() => {
|
||||
// 检查URL参数是否包含mock数据加载指令,用于开发和演示
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.get('mock') === 'true') {
|
||||
// 加载预设的模拟数据
|
||||
store.loadMockData()
|
||||
} else {
|
||||
// 从Storage加载用户数据
|
||||
store.loadData()
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref('')
|
||||
const sortBy = ref('date') // 排序方式:'date'(按日期)、'title'(按标题)、'starred'(按星标)
|
||||
const isFolderExpanded = ref(false) // 文件夹列表是否展开
|
||||
const currentFolder = ref('all') // 当前选中的文件夹,默认是"全部便签"
|
||||
const noteToDelete = ref(null)
|
||||
|
||||
// 计算加星便签数量(未删除的)
|
||||
const starredNotesCount = computed(() => {
|
||||
return store.notes.filter(note => note.isStarred && !note.isDeleted).length
|
||||
})
|
||||
|
||||
// 计算回收站便签数量
|
||||
const trashNotesCount = computed(() => {
|
||||
return store.notes.filter(note => note.isDeleted).length
|
||||
})
|
||||
|
||||
// 根据当前文件夹过滤便签
|
||||
const filteredNotes = computed(() => {
|
||||
// 预处理搜索查询,提高性能
|
||||
const lowerCaseQuery = searchQuery.value?.toLowerCase().trim() || ''
|
||||
|
||||
return store.notes.filter(note => {
|
||||
// 先检查搜索条件
|
||||
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))
|
||||
|
||||
if (!matchesSearch) return false
|
||||
|
||||
// 再检查文件夹条件
|
||||
switch (currentFolder.value) {
|
||||
case 'all':
|
||||
// 全部便签中不显示已删除的便签
|
||||
return !note.isDeleted
|
||||
case 'starred':
|
||||
// 加星便签中只显示未删除的加星便签
|
||||
return note.isStarred && !note.isDeleted
|
||||
case 'trash':
|
||||
// 回收站中只显示已删除的便签
|
||||
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
|
||||
if (!a.isTop && b.isTop) return 1
|
||||
|
||||
// 根据排序方式排序
|
||||
switch (sortBy.value) {
|
||||
case 'title':
|
||||
// 按标题字母顺序排序
|
||||
return a.title.localeCompare(b.title)
|
||||
case 'starred':
|
||||
// 按星标状态排序,加星的便签排在前面
|
||||
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':
|
||||
return '加星便签'
|
||||
case 'trash':
|
||||
return '回收站'
|
||||
default:
|
||||
// 查找自定义文件夹的名称
|
||||
const folder = store.folders.find(f => f.id === currentFolder.value)
|
||||
return folder ? folder.name : '全部便签'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算全部便签数量(未删除的)
|
||||
const allNotesCount = computed(() => {
|
||||
return store.notes.filter(note => !note.isDeleted).length
|
||||
})
|
||||
|
||||
const handleNotePress = noteId => {
|
||||
// 检查是否有便签条处于展开状态
|
||||
let hasSlidedNote = false
|
||||
Object.values(noteItemRefs.value).forEach(noteItem => {
|
||||
// 注意:isSlided是ref值,需要通过.value访问
|
||||
if (noteItem && noteItem.getSlideState()) {
|
||||
hasSlidedNote = true
|
||||
noteItem.resetSlideState()
|
||||
}
|
||||
})
|
||||
|
||||
// 如果有便签条处于展开状态,不跳转到编辑页面
|
||||
if (hasSlidedNote) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用vue-router导航到编辑页面
|
||||
router.push(`/editor/${noteId}`)
|
||||
}
|
||||
|
||||
const handleAddNote = () => {
|
||||
// 使用vue-router导航到新建便签页面
|
||||
router.push('/editor')
|
||||
}
|
||||
|
||||
// 处理Header组件的操作按钮点击事件
|
||||
const handleHeaderAction = actionType => {
|
||||
if (actionType === 'create') {
|
||||
handleAddNote()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStarToggle = async noteId => {
|
||||
const note = store.notes.find(n => n.id === noteId)
|
||||
if (note) {
|
||||
try {
|
||||
await store.updateNote(noteId, { isStarred: !note.isStarred })
|
||||
console.log(`便签 ${noteId} 星标状态已更新`)
|
||||
} catch (error) {
|
||||
console.error('更新便签星标状态失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleTopToggle = async noteId => {
|
||||
const note = store.notes.find(n => n.id === noteId)
|
||||
if (note) {
|
||||
try {
|
||||
await store.updateNote(noteId, { isTop: !note.isTop })
|
||||
console.log(`便签 ${noteId} 置顶状态已更新`)
|
||||
} catch (error) {
|
||||
console.error('更新便签置顶状态失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteNote = async noteId => {
|
||||
noteToDelete.value = noteId
|
||||
if (noteToDelete.value) {
|
||||
try {
|
||||
// 检查当前是否在回收站中
|
||||
if (currentFolder.value === 'trash') {
|
||||
// 在回收站中删除便签,彻底删除
|
||||
await store.permanentlyDeleteNote(noteToDelete.value)
|
||||
console.log(`便签 ${noteToDelete.value} 已彻底删除`)
|
||||
} else {
|
||||
// 不在回收站中,将便签移至回收站
|
||||
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 handleAllNotesClick = () => {
|
||||
setCurrentFolder('all')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleStarredNotesClick = () => {
|
||||
setCurrentFolder('starred')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleTrashNotesClick = () => {
|
||||
setCurrentFolder('trash')
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleFolderClick = folderId => {
|
||||
setCurrentFolder(folderId)
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleAddFolder = () => {
|
||||
// 文件夹添加功能已在FolderManage组件中实现
|
||||
// 这里只需关闭文件夹列表
|
||||
setIsFolderExpanded(false)
|
||||
}
|
||||
|
||||
const handleFolderPress = () => {
|
||||
// 使用vue-router导航到文件夹页面
|
||||
router.push('/folders')
|
||||
}
|
||||
|
||||
const handleSettingsPress = () => {
|
||||
// 使用vue-router导航到设置页面
|
||||
router.push('/settings')
|
||||
}
|
||||
|
||||
const handleFolderToggle = () => {
|
||||
// 在实际应用中,这里会触发文件夹列表的展开/收起
|
||||
isFolderExpanded.value = !isFolderExpanded.value
|
||||
}
|
||||
|
||||
const handleSearch = query => {
|
||||
// 搜索功能已在computed属性filteredAndSortedNotes中实现
|
||||
console.log('搜索:', query)
|
||||
|
||||
// 可以在这里添加搜索统计或其它功能
|
||||
if (query && query.length > 0) {
|
||||
console.log(`找到 ${filteredAndSortedNotes.value.length} 个匹配的便签`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClearSearch = () => {
|
||||
// 清除搜索已在v-model中处理
|
||||
console.log('搜索已清除')
|
||||
|
||||
// 清除搜索后可以重置一些状态
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const handleSearchFocus = () => {
|
||||
console.log('搜索栏获得焦点')
|
||||
// 可以在这里添加获得焦点时的特殊处理
|
||||
}
|
||||
|
||||
const handleSearchBlur = () => {
|
||||
console.log('搜索栏失去焦点')
|
||||
// 可以在这里添加失去焦点时的特殊处理
|
||||
}
|
||||
|
||||
// 防抖函数,用于避免频繁触发搜索
|
||||
// 通过延迟执行函数,只在最后一次调用后执行
|
||||
const debounceSearch = (func, delay) => {
|
||||
let timeoutId
|
||||
return function (...args) {
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => func.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖搜索处理函数,延迟300ms执行搜索
|
||||
const debouncedHandleSearch = debounceSearch(query => {
|
||||
handleSearch(query)
|
||||
}, 300)
|
||||
|
||||
// 改进的日期格式化函数
|
||||
const formatDate = dateString => {
|
||||
return formatNoteListDate(dateString)
|
||||
}
|
||||
|
||||
const setCurrentFolder = folder => {
|
||||
currentFolder.value = folder
|
||||
}
|
||||
|
||||
const setIsFolderExpanded = expanded => {
|
||||
isFolderExpanded.value = expanded
|
||||
}
|
||||
|
||||
const setSearchQuery = query => {
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
const notes = computed(() => store.notes)
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: url(/assets/icons/drawable-xxhdpi/note_background.png);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.folder-list {
|
||||
position: absolute;
|
||||
top: 3.125rem;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
z-index: 1000;
|
||||
background-color: var(--background-card);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.125rem 0.25rem var(--shadow);
|
||||
border: 1px solid #f0ece7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.folder-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: transparent;
|
||||
z-index: 99;
|
||||
}
|
||||
.content {
|
||||
--background: transparent;
|
||||
--padding-top: 4.5rem;
|
||||
--padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 0.8rem 0.5rem;
|
||||
}
|
||||
|
||||
.notes-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.notes-list {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
margin: 0.6rem 0;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* 便签列表动画 */
|
||||
.note-list-enter-active,
|
||||
.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>
|
||||
|
||||
@@ -1,336 +1,383 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import * as storage from '../utils/indexedDBStorage'
|
||||
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
|
||||
|
||||
/**
|
||||
* 应用状态管理Store
|
||||
* 使用Pinia进行状态管理,包含便签、文件夹和设置数据
|
||||
*/
|
||||
export const useAppStore = defineStore('app', {
|
||||
/**
|
||||
* 状态定义
|
||||
* 包含应用的核心数据:便签列表、文件夹列表和设置
|
||||
*/
|
||||
state: () => ({
|
||||
notes: [], // 便签列表
|
||||
folders: [], // 文件夹列表
|
||||
settings: { cloudSync: false, darkMode: false }, // 应用设置
|
||||
}),
|
||||
|
||||
/**
|
||||
* 计算属性
|
||||
* 基于状态派生的计算值
|
||||
*/
|
||||
getters: {
|
||||
/**
|
||||
* 计算加星便签数量
|
||||
* @param {Object} state - 当前状态对象
|
||||
* @returns {number} 加星便签的数量
|
||||
*/
|
||||
starredNotesCount: state => {
|
||||
return state.notes.filter(note => note.isStarred).length
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算所有便签数量
|
||||
* @param {Object} state - 当前状态对象
|
||||
* @returns {number} 所有便签的数量
|
||||
*/
|
||||
allNotesCount: state => {
|
||||
return state.notes.length
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 状态变更操作
|
||||
* 包含所有修改状态的方法
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* 初始化数据
|
||||
* 从Storage加载便签、文件夹和设置数据
|
||||
* 如果没有数据则加载预设的mock数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadData() {
|
||||
try {
|
||||
// 从Storage加载数据
|
||||
const loadedNotes = await storage.getNotes()
|
||||
const loadedFolders = await storage.getFolders()
|
||||
const loadedSettings = await storage.getSettings()
|
||||
|
||||
// 如果没有数据,则加载mock数据
|
||||
if (loadedNotes.length === 0 && loadedFolders.length === 0) {
|
||||
this.loadMockData()
|
||||
} else {
|
||||
// 否则使用加载的数据
|
||||
this.notes = loadedNotes
|
||||
this.folders = loadedFolders
|
||||
this.settings = loadedSettings
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预设的mock数据
|
||||
* 用于开发和演示目的,提供示例便签、文件夹和设置
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadMockData() {
|
||||
// Mock notes - 使用固定的日期值以避免每次运行时变化
|
||||
const fixedCurrentDate = '2025-10-12T10:00:00.000Z';
|
||||
|
||||
// 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签
|
||||
const mockNotes = [
|
||||
{
|
||||
id: '1',
|
||||
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>',
|
||||
createdAt: fixedCurrentDate,
|
||||
updatedAt: fixedCurrentDate,
|
||||
folderId: null,
|
||||
isStarred: true, // 加星便签
|
||||
isTop: true, // 置顶便签
|
||||
hasImage: true, // 包含图片
|
||||
isDeleted: false, // 未删除
|
||||
deletedAt: null,
|
||||
}
|
||||
]
|
||||
|
||||
// Mock folders - 使用固定的日期值
|
||||
// 预设的文件夹示例数据
|
||||
const mockFolders = [
|
||||
{
|
||||
id: 'folder1',
|
||||
name: '工作',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'folder2',
|
||||
name: '个人',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'folder3',
|
||||
name: '学习',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock settings
|
||||
// 预设的设置示例数据
|
||||
const mockSettings = {
|
||||
cloudSync: false, // 云同步关闭
|
||||
darkMode: false, // 深色模式关闭
|
||||
}
|
||||
|
||||
// 更新store状态
|
||||
this.notes = mockNotes
|
||||
this.folders = mockFolders
|
||||
this.settings = mockSettings
|
||||
|
||||
// 保存到Storage
|
||||
await storage.saveNotes(mockNotes)
|
||||
await storage.saveFolders(mockFolders)
|
||||
await storage.saveSettings(mockSettings)
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存便签数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveNotes() {
|
||||
try {
|
||||
await storage.saveNotes(this.notes)
|
||||
} catch (error) {
|
||||
console.error('Error saving notes:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存文件夹数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveFolders() {
|
||||
try {
|
||||
await storage.saveFolders(this.folders)
|
||||
} catch (error) {
|
||||
console.error('Error saving folders:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存设置数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveSettings() {
|
||||
try {
|
||||
await storage.saveSettings(this.settings)
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 便签操作函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 添加新便签
|
||||
* @param {Object} note - 便签对象
|
||||
* @returns {Promise<Object>} 新创建的便签对象
|
||||
*/
|
||||
async addNote(note) {
|
||||
try {
|
||||
const newNote = await storage.addNote(note)
|
||||
this.notes.push(newNote)
|
||||
|
||||
return newNote
|
||||
} catch (error) {
|
||||
console.error('Error adding note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新便签
|
||||
* @param {string} id - 便签ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object>} 更新后的便签对象
|
||||
*/
|
||||
async updateNote(id, updates) {
|
||||
try {
|
||||
const updatedNote = await storage.updateNote(id, updates)
|
||||
if (updatedNote) {
|
||||
const index = this.notes.findIndex(note => note.id === id)
|
||||
if (index !== -1) {
|
||||
this.notes[index] = updatedNote
|
||||
}
|
||||
}
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除便签
|
||||
* @param {string} id - 要删除的便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
async deleteNote(id) {
|
||||
try {
|
||||
const result = await storage.deleteNote(id)
|
||||
if (result) {
|
||||
this.notes = this.notes.filter(note => note.id !== id)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 将便签移至回收站
|
||||
* 将便签标记为已删除状态,并记录删除时间
|
||||
* @param {string} id - 便签ID
|
||||
* @returns {Promise<Object>} 更新后的便签对象
|
||||
*/
|
||||
async moveToTrash(id) {
|
||||
try {
|
||||
const updatedNote = await storage.updateNote(id, { isDeleted: true, deletedAt: getCurrentDateTime() })
|
||||
if (updatedNote) {
|
||||
const index = this.notes.findIndex(note => note.id === id)
|
||||
if (index !== -1) {
|
||||
this.notes[index] = updatedNote
|
||||
}
|
||||
}
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error moving note to trash:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 永久删除便签
|
||||
* 从便签列表中彻底移除便签
|
||||
* @param {string} id - 便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
async permanentlyDeleteNote(id) {
|
||||
try {
|
||||
const result = await storage.deleteNote(id)
|
||||
if (result) {
|
||||
this.notes = this.notes.filter(note => note.id !== id)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error permanently deleting note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 文件夹操作函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 添加新文件夹
|
||||
* @param {Object} folder - 文件夹对象
|
||||
* @returns {Promise<Object>} 新创建的文件夹对象
|
||||
*/
|
||||
async addFolder(folder) {
|
||||
try {
|
||||
const newFolder = await storage.addFolder(folder)
|
||||
this.folders.push(newFolder)
|
||||
return newFolder
|
||||
} catch (error) {
|
||||
console.error('Error adding 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 })
|
||||
},
|
||||
},
|
||||
})
|
||||
import { defineStore } from 'pinia'
|
||||
import * as storage from '../utils/indexedDBStorage'
|
||||
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
|
||||
|
||||
/**
|
||||
* 应用状态管理Store
|
||||
* 使用Pinia进行状态管理,包含便签、文件夹和设置数据
|
||||
*/
|
||||
export const useAppStore = defineStore('app', {
|
||||
/**
|
||||
* 状态定义
|
||||
* 包含应用的核心数据:便签列表、文件夹列表和设置
|
||||
*/
|
||||
state: () => ({
|
||||
notes: [], // 便签列表
|
||||
folders: [], // 文件夹列表
|
||||
settings: { cloudSync: false, darkMode: false }, // 应用设置
|
||||
}),
|
||||
|
||||
/**
|
||||
* 计算属性
|
||||
* 基于状态派生的计算值
|
||||
*/
|
||||
getters: {
|
||||
/**
|
||||
* 计算加星便签数量
|
||||
* @param {Object} state - 当前状态对象
|
||||
* @returns {number} 加星便签的数量
|
||||
*/
|
||||
starredNotesCount: state => {
|
||||
return state.notes.filter(note => note.isStarred).length
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算所有便签数量
|
||||
* @param {Object} state - 当前状态对象
|
||||
* @returns {number} 所有便签的数量
|
||||
*/
|
||||
allNotesCount: state => {
|
||||
return state.notes.length
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 状态变更操作
|
||||
* 包含所有修改状态的方法
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* 初始化数据
|
||||
* 从Storage加载便签、文件夹和设置数据
|
||||
* 如果没有数据则加载预设的mock数据
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadData() {
|
||||
try {
|
||||
// 从Storage加载数据
|
||||
const loadedNotes = await storage.getNotes()
|
||||
const loadedFolders = await storage.getFolders()
|
||||
const loadedSettings = await storage.getSettings()
|
||||
|
||||
// 如果没有数据,则加载mock数据
|
||||
if (loadedNotes.length === 0 && loadedFolders.length === 0) {
|
||||
this.loadMockData()
|
||||
} else {
|
||||
// 否则使用加载的数据
|
||||
this.notes = loadedNotes
|
||||
this.folders = loadedFolders
|
||||
this.settings = loadedSettings
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载预设的mock数据
|
||||
* 用于开发和演示目的,提供示例便签、文件夹和设置
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async loadMockData() {
|
||||
// Mock notes - 使用固定的日期值以避免每次运行时变化
|
||||
const fixedCurrentDate = '2025-10-12T10:00:00.000Z';
|
||||
|
||||
// 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签
|
||||
const mockNotes = [
|
||||
{
|
||||
id: '1',
|
||||
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>',
|
||||
createdAt: fixedCurrentDate,
|
||||
updatedAt: fixedCurrentDate,
|
||||
folderId: null,
|
||||
isStarred: true, // 加星便签
|
||||
isTop: true, // 置顶便签
|
||||
hasImage: true, // 包含图片
|
||||
isDeleted: false, // 未删除
|
||||
deletedAt: null,
|
||||
}
|
||||
]
|
||||
|
||||
// Mock folders - 使用固定的日期值
|
||||
// 预设的文件夹示例数据
|
||||
const mockFolders = [
|
||||
{
|
||||
id: 'folder1',
|
||||
name: '工作',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'folder2',
|
||||
name: '个人',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'folder3',
|
||||
name: '学习',
|
||||
createdAt: '2025-10-12T10:00:00.000Z',
|
||||
},
|
||||
]
|
||||
|
||||
// Mock settings
|
||||
// 预设的设置示例数据
|
||||
const mockSettings = {
|
||||
cloudSync: false, // 云同步关闭
|
||||
darkMode: false, // 深色模式关闭
|
||||
}
|
||||
|
||||
// 更新store状态
|
||||
this.notes = mockNotes
|
||||
this.folders = mockFolders
|
||||
this.settings = mockSettings
|
||||
|
||||
// 保存到Storage
|
||||
await storage.saveNotes(mockNotes)
|
||||
await storage.saveFolders(mockFolders)
|
||||
await storage.saveSettings(mockSettings)
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存便签数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveNotes() {
|
||||
try {
|
||||
await storage.saveNotes(this.notes)
|
||||
} catch (error) {
|
||||
console.error('Error saving notes:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存文件夹数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveFolders() {
|
||||
try {
|
||||
await storage.saveFolders(this.folders)
|
||||
} catch (error) {
|
||||
console.error('Error saving folders:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存设置数据到Storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async saveSettings() {
|
||||
try {
|
||||
await storage.saveSettings(this.settings)
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 便签操作函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 添加新便签
|
||||
* @param {Object} note - 便签对象
|
||||
* @returns {Promise<Object>} 新创建的便签对象
|
||||
*/
|
||||
async addNote(note) {
|
||||
try {
|
||||
const newNote = await storage.addNote(note)
|
||||
this.notes.push(newNote)
|
||||
|
||||
return newNote
|
||||
} catch (error) {
|
||||
console.error('Error adding note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新便签
|
||||
* @param {string} id - 便签ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object>} 更新后的便签对象
|
||||
*/
|
||||
async updateNote(id, updates) {
|
||||
try {
|
||||
const updatedNote = await storage.updateNote(id, updates)
|
||||
if (updatedNote) {
|
||||
const index = this.notes.findIndex(note => note.id === id)
|
||||
if (index !== -1) {
|
||||
this.notes[index] = updatedNote
|
||||
}
|
||||
}
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除便签
|
||||
* @param {string} id - 要删除的便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
async deleteNote(id) {
|
||||
try {
|
||||
const result = await storage.deleteNote(id)
|
||||
if (result) {
|
||||
this.notes = this.notes.filter(note => note.id !== id)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 将便签移至回收站
|
||||
* 将便签标记为已删除状态,并记录删除时间
|
||||
* @param {string} id - 便签ID
|
||||
* @returns {Promise<Object>} 更新后的便签对象
|
||||
*/
|
||||
async moveToTrash(id) {
|
||||
try {
|
||||
const updatedNote = await storage.updateNote(id, { isDeleted: true, deletedAt: getCurrentDateTime() })
|
||||
if (updatedNote) {
|
||||
const index = this.notes.findIndex(note => note.id === id)
|
||||
if (index !== -1) {
|
||||
this.notes[index] = updatedNote
|
||||
}
|
||||
}
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error moving note to trash:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 永久删除便签
|
||||
* 从便签列表中彻底移除便签
|
||||
* @param {string} id - 便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
async permanentlyDeleteNote(id) {
|
||||
try {
|
||||
const result = await storage.deleteNote(id)
|
||||
if (result) {
|
||||
this.notes = this.notes.filter(note => note.id !== id)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error permanently deleting note:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 文件夹操作函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 添加新文件夹
|
||||
* @param {Object} folder - 文件夹对象
|
||||
* @returns {Promise<Object>} 新创建的文件夹对象
|
||||
*/
|
||||
async addFolder(folder) {
|
||||
try {
|
||||
const newFolder = await storage.addFolder(folder)
|
||||
this.folders.push(newFolder)
|
||||
return newFolder
|
||||
} catch (error) {
|
||||
console.error('Error adding folder:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新文件夹
|
||||
* @param {string} id - 文件夹ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object>} 更新后的文件夹对象
|
||||
*/
|
||||
async updateFolder(id, updates) {
|
||||
try {
|
||||
const updatedFolder = await storage.updateFolder(id, updates)
|
||||
if (updatedFolder) {
|
||||
const index = this.folders.findIndex(folder => folder.id === id)
|
||||
if (index !== -1) {
|
||||
this.folders[index] = updatedFolder
|
||||
}
|
||||
}
|
||||
return updatedFolder
|
||||
} catch (error) {
|
||||
console.error('Error updating folder:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* @param {string} id - 要删除的文件夹ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
async deleteFolder(id) {
|
||||
try {
|
||||
const result = await storage.deleteFolder(id)
|
||||
if (result) {
|
||||
// 将文件夹中的便签移回"全部便签"
|
||||
const notesInFolder = this.notes.filter(note => note.folderId === id)
|
||||
for (const note of notesInFolder) {
|
||||
await this.updateNote(note.id, { folderId: null })
|
||||
}
|
||||
|
||||
// 从文件夹列表中移除文件夹
|
||||
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 })
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,475 +1,510 @@
|
||||
import { getCurrentDateTime, getTimestamp } from './dateUtils'
|
||||
|
||||
// 数据库配置
|
||||
const DB_NAME = 'SmartisanNoteDB'
|
||||
const DB_VERSION = 2 // 更新版本号以确保数据库重新创建
|
||||
const NOTES_STORE = 'notes'
|
||||
const FOLDERS_STORE = 'folders'
|
||||
const SETTINGS_STORE = 'settings'
|
||||
|
||||
let db = null
|
||||
|
||||
/**
|
||||
* 打开数据库连接
|
||||
* @returns {Promise<IDBDatabase>} 数据库实例
|
||||
*/
|
||||
const openDB = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (db) {
|
||||
return resolve(db)
|
||||
}
|
||||
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error('无法打开数据库'))
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
db = request.result
|
||||
resolve(db)
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const database = event.target.result
|
||||
|
||||
// 删除现有的对象存储(如果版本已更改)
|
||||
if (event.oldVersion > 0) {
|
||||
if (database.objectStoreNames.contains(NOTES_STORE)) {
|
||||
database.deleteObjectStore(NOTES_STORE)
|
||||
}
|
||||
if (database.objectStoreNames.contains(FOLDERS_STORE)) {
|
||||
database.deleteObjectStore(FOLDERS_STORE)
|
||||
}
|
||||
if (database.objectStoreNames.contains(SETTINGS_STORE)) {
|
||||
database.deleteObjectStore(SETTINGS_STORE)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建便签存储对象
|
||||
const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' })
|
||||
notesStore.createIndex('folderId', 'folderId', { unique: false })
|
||||
notesStore.createIndex('isStarred', 'isStarred', { unique: false })
|
||||
notesStore.createIndex('isDeleted', 'isDeleted', { unique: false })
|
||||
notesStore.createIndex('createdAt', 'createdAt', { unique: false })
|
||||
notesStore.createIndex('updatedAt', 'updatedAt', { unique: false })
|
||||
|
||||
// 创建文件夹存储对象
|
||||
database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' })
|
||||
|
||||
// 创建设置存储对象
|
||||
database.createObjectStore(SETTINGS_STORE)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中获取数据
|
||||
* @param {string} storeName - 存储名称
|
||||
* @returns {Promise<Array>} 数据数组
|
||||
*/
|
||||
const getAllFromStore = async (storeName) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.getAll()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || [])
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`获取 ${storeName} 数据失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据到存储
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {Array} data - 要保存的数据数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const saveToStore = async (storeName, data) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
|
||||
// 清除现有数据
|
||||
await new Promise((resolve, reject) => {
|
||||
const clearRequest = store.clear()
|
||||
clearRequest.onsuccess = () => resolve()
|
||||
clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`))
|
||||
})
|
||||
|
||||
// 添加新数据
|
||||
for (const item of data) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const addRequest = store.add(item)
|
||||
addRequest.onsuccess = () => resolve()
|
||||
addRequest.onerror = () => reject(new Error(`保存 ${storeName} 数据失败`))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中获取单个项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 项的ID
|
||||
* @returns {Promise<Object|null>} 项对象或null
|
||||
*/
|
||||
const getFromStore = async (storeName, id) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.get(id)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || null)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`获取 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 向存储中添加项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {Object} item - 要添加的项
|
||||
* @returns {Promise<Object>} 添加的项
|
||||
*/
|
||||
const addToStore = async (storeName, item) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.add(item)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(item)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`添加 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新存储中的项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 项的ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object|null>} 更新后的项或null
|
||||
*/
|
||||
const updateInStore = async (storeName, id, updates) => {
|
||||
const item = await getFromStore(storeName, id)
|
||||
if (!item) return null
|
||||
|
||||
const updatedItem = { ...item, ...updates }
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.put(updatedItem)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(updatedItem)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`更新 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中删除项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 要删除的项的ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
const deleteFromStore = async (storeName, id) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.delete(id)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`删除 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 便签操作函数
|
||||
// 提供便签的增删改查功能
|
||||
|
||||
/**
|
||||
* 获取所有便签数据
|
||||
* 从IndexedDB中读取便签数据
|
||||
* @returns {Promise<Array>} 便签数组
|
||||
*/
|
||||
export const getNotes = async () => {
|
||||
try {
|
||||
const notes = await getAllFromStore(NOTES_STORE)
|
||||
return ensureNotesDefaults(notes)
|
||||
} catch (error) {
|
||||
console.error('Error getting notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存便签数据
|
||||
* 将便签数组保存到IndexedDB
|
||||
* @param {Array} notes - 便签数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveNotes = async (notes) => {
|
||||
try {
|
||||
await saveToStore(NOTES_STORE, notes)
|
||||
} catch (error) {
|
||||
console.error('Error saving notes:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新便签
|
||||
* 创建一个新的便签对象并添加到便签列表中
|
||||
* @param {Object} note - 便签对象,包含便签内容和其他属性
|
||||
* @returns {Promise<Object>} 新创建的便签对象
|
||||
*/
|
||||
export const addNote = async (note) => {
|
||||
try {
|
||||
// 创建新的便签对象,添加必要的属性
|
||||
const newNote = {
|
||||
title: note.title || '',
|
||||
content: note.content || '',
|
||||
id: note.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||
createdAt: note.createdAt || getCurrentDateTime(), // 创建时间
|
||||
updatedAt: note.updatedAt || getCurrentDateTime(), // 更新时间
|
||||
isStarred: note.isStarred || false, // 是否加星
|
||||
isTop: note.isTop || false, // 是否置顶
|
||||
hasImage: note.hasImage || false, // 是否包含图片
|
||||
isDeleted: note.isDeleted || false, // 是否已删除
|
||||
deletedAt: note.deletedAt || null, // 删除时间
|
||||
folderId: note.folderId || null, // 文件夹ID
|
||||
...note
|
||||
}
|
||||
|
||||
// 添加到存储
|
||||
await addToStore(NOTES_STORE, newNote)
|
||||
return newNote
|
||||
} catch (error) {
|
||||
console.error('Error adding note:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新便签
|
||||
* 根据ID查找并更新便签信息
|
||||
* @param {string} id - 便签ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object|null>} 更新后的便签对象,如果未找到则返回null
|
||||
*/
|
||||
export const updateNote = async (id, updates) => {
|
||||
try {
|
||||
// 更新便签并保存
|
||||
const updatedNote = await updateInStore(NOTES_STORE, id, {
|
||||
...updates,
|
||||
updatedAt: getCurrentDateTime() // 更新最后修改时间
|
||||
})
|
||||
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除便签
|
||||
* 根据ID从便签列表中移除便签
|
||||
* @param {string} id - 要删除的便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,未找到便签返回false
|
||||
*/
|
||||
export const deleteNote = async (id) => {
|
||||
try {
|
||||
// 从存储中删除
|
||||
const result = await deleteFromStore(NOTES_STORE, id)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 文件夹操作函数
|
||||
// 提供文件夹的增删改查功能
|
||||
|
||||
/**
|
||||
* 获取所有文件夹数据
|
||||
* 从IndexedDB中读取文件夹数据
|
||||
* @returns {Promise<Array>} 文件夹数组
|
||||
*/
|
||||
export const getFolders = async () => {
|
||||
try {
|
||||
const folders = await getAllFromStore(FOLDERS_STORE)
|
||||
return ensureFoldersDefaults(folders)
|
||||
} catch (error) {
|
||||
console.error('Error getting folders:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件夹数据
|
||||
* 将文件夹数组保存到IndexedDB
|
||||
* @param {Array} folders - 文件夹数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveFolders = async (folders) => {
|
||||
try {
|
||||
await saveToStore(FOLDERS_STORE, folders)
|
||||
} catch (error) {
|
||||
console.error('Error saving folders:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新文件夹
|
||||
* 创建一个新的文件夹对象并添加到文件夹列表中
|
||||
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性
|
||||
* @returns {Promise<Object>} 新创建的文件夹对象
|
||||
*/
|
||||
export const addFolder = async (folder) => {
|
||||
try {
|
||||
// 创建新的文件夹对象,添加必要的属性
|
||||
const newFolder = {
|
||||
name: folder.name || '',
|
||||
id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||
createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间
|
||||
...folder
|
||||
}
|
||||
|
||||
// 添加到存储
|
||||
await addToStore(FOLDERS_STORE, newFolder)
|
||||
return newFolder
|
||||
} catch (error) {
|
||||
console.error('Error adding folder:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 设置操作函数
|
||||
// 提供应用设置的读取和保存功能
|
||||
|
||||
/**
|
||||
* 获取应用设置
|
||||
* 从IndexedDB中读取设置数据
|
||||
* @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置
|
||||
*/
|
||||
export const getSettings = async () => {
|
||||
try {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([SETTINGS_STORE], 'readonly')
|
||||
const store = transaction.objectStore(SETTINGS_STORE)
|
||||
const request = store.get('settings')
|
||||
|
||||
const settings = await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || { cloudSync: false, darkMode: false })
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error('获取设置失败'))
|
||||
}
|
||||
})
|
||||
|
||||
return settings
|
||||
} catch (error) {
|
||||
console.error('Error getting settings:', error)
|
||||
// 出错时返回默认设置
|
||||
return { cloudSync: false, darkMode: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存应用设置
|
||||
* 将设置对象保存到IndexedDB
|
||||
* @param {Object} settings - 设置对象
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveSettings = async (settings) => {
|
||||
try {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([SETTINGS_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(SETTINGS_STORE)
|
||||
const request = store.put(settings, 'settings')
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(new Error('保存设置失败'))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保数据有默认值
|
||||
* @param {Array} notes - 便签数组
|
||||
* @returns {Array} 处理后的便签数组
|
||||
*/
|
||||
const ensureNotesDefaults = (notes) => {
|
||||
return notes.map(note => ({
|
||||
title: note.title || '',
|
||||
content: note.content || '',
|
||||
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)
|
||||
}
|
||||
import { getCurrentDateTime, getTimestamp } from './dateUtils'
|
||||
|
||||
// 数据库配置
|
||||
const DB_NAME = 'SmartisanNoteDB'
|
||||
const DB_VERSION = 2 // 更新版本号以确保数据库重新创建
|
||||
const NOTES_STORE = 'notes'
|
||||
const FOLDERS_STORE = 'folders'
|
||||
const SETTINGS_STORE = 'settings'
|
||||
|
||||
let db = null
|
||||
|
||||
/**
|
||||
* 打开数据库连接
|
||||
* @returns {Promise<IDBDatabase>} 数据库实例
|
||||
*/
|
||||
const openDB = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (db) {
|
||||
return resolve(db)
|
||||
}
|
||||
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error('无法打开数据库'))
|
||||
}
|
||||
|
||||
request.onsuccess = () => {
|
||||
db = request.result
|
||||
resolve(db)
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const database = event.target.result
|
||||
|
||||
// 删除现有的对象存储(如果版本已更改)
|
||||
if (event.oldVersion > 0) {
|
||||
if (database.objectStoreNames.contains(NOTES_STORE)) {
|
||||
database.deleteObjectStore(NOTES_STORE)
|
||||
}
|
||||
if (database.objectStoreNames.contains(FOLDERS_STORE)) {
|
||||
database.deleteObjectStore(FOLDERS_STORE)
|
||||
}
|
||||
if (database.objectStoreNames.contains(SETTINGS_STORE)) {
|
||||
database.deleteObjectStore(SETTINGS_STORE)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建便签存储对象
|
||||
const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' })
|
||||
notesStore.createIndex('folderId', 'folderId', { unique: false })
|
||||
notesStore.createIndex('isStarred', 'isStarred', { unique: false })
|
||||
notesStore.createIndex('isDeleted', 'isDeleted', { unique: false })
|
||||
notesStore.createIndex('createdAt', 'createdAt', { unique: false })
|
||||
notesStore.createIndex('updatedAt', 'updatedAt', { unique: false })
|
||||
|
||||
// 创建文件夹存储对象
|
||||
database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' })
|
||||
|
||||
// 创建设置存储对象
|
||||
database.createObjectStore(SETTINGS_STORE)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中获取数据
|
||||
* @param {string} storeName - 存储名称
|
||||
* @returns {Promise<Array>} 数据数组
|
||||
*/
|
||||
const getAllFromStore = async (storeName) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.getAll()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || [])
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`获取 ${storeName} 数据失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据到存储
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {Array} data - 要保存的数据数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const saveToStore = async (storeName, data) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
|
||||
// 清除现有数据
|
||||
await new Promise((resolve, reject) => {
|
||||
const clearRequest = store.clear()
|
||||
clearRequest.onsuccess = () => resolve()
|
||||
clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`))
|
||||
})
|
||||
|
||||
// 添加新数据
|
||||
for (const item of data) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const addRequest = store.add(item)
|
||||
addRequest.onsuccess = () => resolve()
|
||||
addRequest.onerror = () => reject(new Error(`保存 ${storeName} 数据失败`))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中获取单个项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 项的ID
|
||||
* @returns {Promise<Object|null>} 项对象或null
|
||||
*/
|
||||
const getFromStore = async (storeName, id) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readonly')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.get(id)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || null)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`获取 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 向存储中添加项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {Object} item - 要添加的项
|
||||
* @returns {Promise<Object>} 添加的项
|
||||
*/
|
||||
const addToStore = async (storeName, item) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.add(item)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(item)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`添加 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新存储中的项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 项的ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object|null>} 更新后的项或null
|
||||
*/
|
||||
const updateInStore = async (storeName, id, updates) => {
|
||||
const item = await getFromStore(storeName, id)
|
||||
if (!item) return null
|
||||
|
||||
const updatedItem = { ...item, ...updates }
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.put(updatedItem)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(updatedItem)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`更新 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储中删除项
|
||||
* @param {string} storeName - 存储名称
|
||||
* @param {string} id - 要删除的项的ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||
*/
|
||||
const deleteFromStore = async (storeName, id) => {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([storeName], 'readwrite')
|
||||
const store = transaction.objectStore(storeName)
|
||||
const request = store.delete(id)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(true)
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error(`删除 ${storeName} 项失败`))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 便签操作函数
|
||||
// 提供便签的增删改查功能
|
||||
|
||||
/**
|
||||
* 获取所有便签数据
|
||||
* 从IndexedDB中读取便签数据
|
||||
* @returns {Promise<Array>} 便签数组
|
||||
*/
|
||||
export const getNotes = async () => {
|
||||
try {
|
||||
const notes = await getAllFromStore(NOTES_STORE)
|
||||
return ensureNotesDefaults(notes)
|
||||
} catch (error) {
|
||||
console.error('Error getting notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存便签数据
|
||||
* 将便签数组保存到IndexedDB
|
||||
* @param {Array} notes - 便签数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveNotes = async (notes) => {
|
||||
try {
|
||||
await saveToStore(NOTES_STORE, notes)
|
||||
} catch (error) {
|
||||
console.error('Error saving notes:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新便签
|
||||
* 创建一个新的便签对象并添加到便签列表中
|
||||
* @param {Object} note - 便签对象,包含便签内容和其他属性
|
||||
* @returns {Promise<Object>} 新创建的便签对象
|
||||
*/
|
||||
export const addNote = async (note) => {
|
||||
try {
|
||||
// 创建新的便签对象,添加必要的属性
|
||||
const newNote = {
|
||||
title: note.title || '',
|
||||
content: note.content || '',
|
||||
id: note.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||
createdAt: note.createdAt || getCurrentDateTime(), // 创建时间
|
||||
updatedAt: note.updatedAt || getCurrentDateTime(), // 更新时间
|
||||
isStarred: note.isStarred || false, // 是否加星
|
||||
isTop: note.isTop || false, // 是否置顶
|
||||
hasImage: note.hasImage || false, // 是否包含图片
|
||||
isDeleted: note.isDeleted || false, // 是否已删除
|
||||
deletedAt: note.deletedAt || null, // 删除时间
|
||||
folderId: note.folderId || null, // 文件夹ID
|
||||
...note
|
||||
}
|
||||
|
||||
// 添加到存储
|
||||
await addToStore(NOTES_STORE, newNote)
|
||||
return newNote
|
||||
} catch (error) {
|
||||
console.error('Error adding note:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新便签
|
||||
* 根据ID查找并更新便签信息
|
||||
* @param {string} id - 便签ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object|null>} 更新后的便签对象,如果未找到则返回null
|
||||
*/
|
||||
export const updateNote = async (id, updates) => {
|
||||
try {
|
||||
// 更新便签并保存
|
||||
const updatedNote = await updateInStore(NOTES_STORE, id, {
|
||||
...updates,
|
||||
updatedAt: getCurrentDateTime() // 更新最后修改时间
|
||||
})
|
||||
|
||||
return updatedNote
|
||||
} catch (error) {
|
||||
console.error('Error updating note:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除便签
|
||||
* 根据ID从便签列表中移除便签
|
||||
* @param {string} id - 要删除的便签ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,未找到便签返回false
|
||||
*/
|
||||
export const deleteNote = async (id) => {
|
||||
try {
|
||||
// 从存储中删除
|
||||
const result = await deleteFromStore(NOTES_STORE, id)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 文件夹操作函数
|
||||
// 提供文件夹的增删改查功能
|
||||
|
||||
/**
|
||||
* 获取所有文件夹数据
|
||||
* 从IndexedDB中读取文件夹数据
|
||||
* @returns {Promise<Array>} 文件夹数组
|
||||
*/
|
||||
export const getFolders = async () => {
|
||||
try {
|
||||
const folders = await getAllFromStore(FOLDERS_STORE)
|
||||
return ensureFoldersDefaults(folders)
|
||||
} catch (error) {
|
||||
console.error('Error getting folders:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件夹数据
|
||||
* 将文件夹数组保存到IndexedDB
|
||||
* @param {Array} folders - 文件夹数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveFolders = async (folders) => {
|
||||
try {
|
||||
await saveToStore(FOLDERS_STORE, folders)
|
||||
} catch (error) {
|
||||
console.error('Error saving folders:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新文件夹
|
||||
* 创建一个新的文件夹对象并添加到文件夹列表中
|
||||
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性
|
||||
* @returns {Promise<Object>} 新创建的文件夹对象
|
||||
*/
|
||||
export const addFolder = async (folder) => {
|
||||
try {
|
||||
// 创建新的文件夹对象,添加必要的属性
|
||||
const newFolder = {
|
||||
name: folder.name || '',
|
||||
id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||
createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间
|
||||
...folder
|
||||
}
|
||||
|
||||
// 添加到存储
|
||||
await addToStore(FOLDERS_STORE, newFolder)
|
||||
return newFolder
|
||||
} catch (error) {
|
||||
console.error('Error adding folder:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件夹
|
||||
* 根据ID查找并更新文件夹信息
|
||||
* @param {string} id - 文件夹ID
|
||||
* @param {Object} updates - 要更新的属性对象
|
||||
* @returns {Promise<Object|null>} 更新后的文件夹对象,如果未找到则返回null
|
||||
*/
|
||||
export const updateFolder = async (id, updates) => {
|
||||
try {
|
||||
// 更新文件夹并保存
|
||||
const updatedFolder = await updateInStore(FOLDERS_STORE, id, updates)
|
||||
return updatedFolder
|
||||
} catch (error) {
|
||||
console.error('Error updating folder:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* 根据ID从文件夹列表中移除文件夹
|
||||
* @param {string} id - 要删除的文件夹ID
|
||||
* @returns {Promise<boolean>} 删除成功返回true,未找到文件夹返回false
|
||||
*/
|
||||
export const deleteFolder = async (id) => {
|
||||
try {
|
||||
// 从存储中删除
|
||||
const result = await deleteFromStore(FOLDERS_STORE, id)
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Error deleting folder:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置操作函数
|
||||
// 提供应用设置的读取和保存功能
|
||||
|
||||
/**
|
||||
* 获取应用设置
|
||||
* 从IndexedDB中读取设置数据
|
||||
* @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置
|
||||
*/
|
||||
export const getSettings = async () => {
|
||||
try {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([SETTINGS_STORE], 'readonly')
|
||||
const store = transaction.objectStore(SETTINGS_STORE)
|
||||
const request = store.get('settings')
|
||||
|
||||
const settings = await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result || { cloudSync: false, darkMode: false })
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error('获取设置失败'))
|
||||
}
|
||||
})
|
||||
|
||||
return settings
|
||||
} catch (error) {
|
||||
console.error('Error getting settings:', error)
|
||||
// 出错时返回默认设置
|
||||
return { cloudSync: false, darkMode: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存应用设置
|
||||
* 将设置对象保存到IndexedDB
|
||||
* @param {Object} settings - 设置对象
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const saveSettings = async (settings) => {
|
||||
try {
|
||||
const database = await openDB()
|
||||
const transaction = database.transaction([SETTINGS_STORE], 'readwrite')
|
||||
const store = transaction.objectStore(SETTINGS_STORE)
|
||||
const request = store.put(settings, 'settings')
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(new Error('保存设置失败'))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保数据有默认值
|
||||
* @param {Array} notes - 便签数组
|
||||
* @returns {Array} 处理后的便签数组
|
||||
*/
|
||||
const ensureNotesDefaults = (notes) => {
|
||||
return notes.map(note => ({
|
||||
title: note.title || '',
|
||||
content: note.content || '',
|
||||
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
113
src/utils/modalService.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user