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

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

View File

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

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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
View File

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