开始完善便签新建、编辑逻辑

This commit is contained in:
2025-10-12 18:32:25 +08:00
parent 3957a7d3b2
commit 1bb9b4a79e
13 changed files with 696 additions and 1039 deletions

View File

@@ -4,7 +4,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- 透明 -->
<meta name="apple-mobile-web-app-orientation" content="portrait" />
<!-- 纵向 -->
<title>锤子便签</title> <title>锤子便签</title>
<style> <style>
/* Smartisan Notes Color Scheme - Based on Original Design */ /* Smartisan Notes Color Scheme - Based on Original Design */
@@ -15,8 +18,8 @@
--primary-light: #f5f0e6; /* Light background tone */ --primary-light: #f5f0e6; /* Light background tone */
/* Editor typography - Consistent font size and line height */ /* Editor typography - Consistent font size and line height */
--editor-font-size: 19px; /* Base font size for editor */ --editor-font-size: 23px; /* Base font size for editor */
--editor-line-height: 1.5; /* Line height for editor */ --editor-line-height: 1.1; /* Line height for editor */
/* Background colors - Warm paper-like tones */ /* Background colors - Warm paper-like tones */
--background: #fbf7ed; /* Main app background - warm off-white */ --background: #fbf7ed; /* Main app background - warm off-white */
@@ -92,7 +95,6 @@
padding: 0; padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--background); background-color: var(--background);
background-image: repeating-linear-gradient(0deg, transparent, transparent 10px, var(--background-secondary) 10px, var(--background-secondary) 20px);
color: var(--text-primary); color: var(--text-primary);
/* 适配iPhone X及更新机型的刘海屏 */ /* 适配iPhone X及更新机型的刘海屏 */
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);

View File

@@ -8,6 +8,7 @@ html {
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
touch-action: none;
} }
body { body {

View File

@@ -14,7 +14,7 @@
</div> </div>
<div class="code-fun-flex-row code-fun-justify-between mt-17-5"> <div class="code-fun-flex-row code-fun-justify-between mt-17-5">
<!-- 便签正文第一行 --> <!-- 便签正文第一行 -->
<span class="font_3 text_19">{{ title }}</span> <span class="font_3 text_19">{{ content }}</span>
<!-- 便签中是否存在图片 --> <!-- 便签中是否存在图片 -->
<img v-if="hasImage" class="image_28" src="/assets/icons/drawable-xxhdpi/list_item_image_icon.png" /> <img v-if="hasImage" class="image_28" src="/assets/icons/drawable-xxhdpi/list_item_image_icon.png" />
</div> </div>
@@ -32,10 +32,6 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
const props = defineProps({ const props = defineProps({
title: {
type: String,
required: true,
},
content: { content: {
type: String, type: String,
required: true, required: true,
@@ -251,6 +247,12 @@ const handleTouchEnd = () => {
color: #816d61; color: #816d61;
font-size: 0.9rem; font-size: 0.9rem;
line-height: 0.9rem; line-height: 0.9rem;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 1;
} }
.image_28 { .image_28 {
width: 1.06rem; width: 1.06rem;

View File

@@ -28,6 +28,26 @@ const editorRef = ref(null)
const content = ref(props.modelValue || '') const content = ref(props.modelValue || '')
const isToolbarVisible = ref(false) const isToolbarVisible = ref(false)
// 初始化编辑器内容
onMounted(() => {
console.log('RichTextEditor mounted, initial content:', props.modelValue)
if (editorRef.value) {
if (props.modelValue) {
try {
editorRef.value.innerHTML = props.modelValue
content.value = props.modelValue
console.log('Initial content set successfully')
} catch (error) {
console.error('Failed to set initial content:', error)
}
} else {
// 即使没有初始内容,也要确保编辑器是可编辑的
editorRef.value.contentEditable = true
console.log('Editor initialized without initial content')
}
}
})
// 工具配置 // 工具配置
const tools = ref([ const tools = ref([
{ {
@@ -68,6 +88,15 @@ const tools = ref([
}, },
]) ])
// 处理输入事件
const handleInput = () => {
if (editorRef.value) {
content.value = editorRef.value.innerHTML
console.log('Input event handled, content:', content.value)
emit('update:modelValue', content.value)
}
}
// 检查当前选区是否已经在某种格式中 // 检查当前选区是否已经在某种格式中
const isAlreadyInFormat = formatType => { const isAlreadyInFormat = formatType => {
const selection = window.getSelection() const selection = window.getSelection()
@@ -550,14 +579,6 @@ const insertImage = () => {
fileInput.click() fileInput.click()
} }
// 处理输入事件
const handleInput = () => {
if (editorRef.value) {
content.value = editorRef.value.innerHTML
emit('update:modelValue', content.value)
}
}
// 处理键盘事件 // 处理键盘事件
const handleKeydown = e => { const handleKeydown = e => {
// 处理Shift+Enter键换行 // 处理Shift+Enter键换行
@@ -710,15 +731,42 @@ const handleToolbarFocusOut = () => {
}, 200) // 增加延迟时间,确保有足够时间处理点击事件 }, 200) // 增加延迟时间,确保有足够时间处理点击事件
} }
// 监听外部值变化 // 暴露方法给父组件
defineExpose({ defineExpose({
getContent: () => content.value, getContent: () => content.value,
setContent: newContent => { setContent: newContent => {
content.value = newContent console.log('Setting content in editor:', newContent)
content.value = newContent || ''
if (editorRef.value) { if (editorRef.value) {
editorRef.value.innerHTML = newContent try {
editorRef.value.innerHTML = content.value
console.log('Content set successfully in editorRef')
} catch (error) {
console.error('Failed to set innerHTML:', error)
// 备选方案使用textContent
try {
editorRef.value.textContent = content.value
console.log('Content set using textContent')
} catch (textContentError) {
console.error('Failed to set textContent:', textContentError)
}
}
} else {
// 如果editorRef还不可用延迟设置
console.log('Editor ref is not available, will retry when mounted')
setTimeout(() => {
if (editorRef.value) {
try {
editorRef.value.innerHTML = content.value
console.log('Content set successfully after delay')
} catch (error) {
console.error('Failed to set innerHTML after delay:', error)
}
}
}, 100)
} }
}, },
insertImage,
}) })
</script> </script>
@@ -914,31 +962,7 @@ defineExpose({
text-align: center; text-align: center;
} }
/* 引用格式样式 */
:deep(.quote-container) {
position: relative;
margin: 0 0 12px 0;
line-height: var(--editor-line-height, 1.6);
}
:deep(.quote-icon) {
position: absolute;
left: 0;
top: 0;
width: var(--editor-font-size, 16px);
height: var(--editor-font-size, 16px);
margin-top: 3px;
}
:deep(.quote-content) {
border-left: 3px solid var(--primary);
padding: 0 var(--editor-font-size, 16px) 0 32px;
margin-left: var(--editor-font-size, 16px);
color: var(--text-secondary);
background-color: var(--background-secondary);
font-style: italic;
line-height: var(--editor-line-height, 1.6);
}
.editor-content img { .editor-content img {
max-width: 100%; max-width: 100%;
@@ -972,71 +996,3 @@ defineExpose({
margin: 0; margin: 0;
} }
</style> </style>
<script>
// 插入图片
const insertImage = () => {
// 创建文件输入元素
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = 'image/*'
fileInput.style.display = 'none'
// 添加到文档中
document.body.appendChild(fileInput)
// 监听文件选择事件
fileInput.addEventListener('change', function (event) {
const file = event.target.files[0]
if (file && file.type.startsWith('image/')) {
// 创建FileReader读取文件
const reader = new FileReader()
reader.onload = function (e) {
// 获取图片数据URL
const imageDataUrl = e.target.result
// 获取当前选区
const selection = window.getSelection()
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
// 创建图片元素
const img = document.createElement('img')
img.src = imageDataUrl
img.className = 'editor-image'
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.display = 'block'
img.style.margin = '0 auto'
// 插入图片到当前光标位置
range.insertNode(img)
// 添加换行
const br = document.createElement('br')
img.parentNode.insertBefore(br, img.nextSibling)
// 触发输入事件更新内容
handleInput()
// 重新聚焦到编辑器
if (editorRef.value) {
editorRef.value.focus()
}
}
}
reader.readAsDataURL(file)
}
// 清理文件输入元素
document.body.removeChild(fileInput)
})
// 触发文件选择对话框
fileInput.click()
}
export default {
insertImage
}
</script>

View File

@@ -1,29 +1,17 @@
<template> <template>
<ion-page> <ion-page>
<Header <Header title="文件夹" :onBack="() => window.history.back()" />
title="文件夹" <div class="folder-page-container">
:onBack="() => window.history.back()" <div class="search-container">
/> <ion-icon :icon="search" class="search-icon"></ion-icon>
<div style="padding: 10px; background-color: var(--background)"> <ion-input placeholder="搜索文件夹..." :value="searchQuery" @ionChange="e => setSearchQuery(e.detail.value)" class="search-input"></ion-input>
<div style="display: flex; align-items: center; background-color: var(--search-bar-background); border-radius: 8px; padding: 0 10px"> <ion-button v-if="searchQuery.length > 0" fill="clear" @click="() => setSearchQuery('')">
<ion-icon :icon="search" style="font-size: 20px; color: var(--text-tertiary)"></ion-icon> <ion-icon :icon="closeCircle" class="clear-icon"></ion-icon>
<ion-input
placeholder="搜索文件夹..."
:value="searchQuery"
@ionChange="e => setSearchQuery(e.detail.value)"
style="--padding-start: 10px; --padding-end: 10px; flex: 1; font-size: 16px; color: var(--text-primary)"
></ion-input>
<ion-button
v-if="searchQuery.length > 0"
fill="clear"
@click="() => setSearchQuery('')"
>
<ion-icon :icon="closeCircle" style="font-size: 20px; color: var(--text-tertiary)"></ion-icon>
</ion-button> </ion-button>
</div> </div>
</div> </div>
<ion-content> <ion-content>
<ion-list style="background-color: var(--background); padding: 0 16px; --ion-item-background: var(--background)"> <ion-list class="folder-list">
<FolderManage <FolderManage
:allCount="allNotesCount" :allCount="allNotesCount"
:starredCount="starredNotesCount" :starredCount="starredNotesCount"
@@ -33,47 +21,46 @@
:onAllClick="() => handleFolderPress('all')" :onAllClick="() => handleFolderPress('all')"
:onStarredClick="() => handleFolderPress('starred')" :onStarredClick="() => handleFolderPress('starred')"
:onTrashClick="() => handleFolderPress('trash')" :onTrashClick="() => handleFolderPress('trash')"
:onArchiveClick="() => handleFolderPress('archive')" :onArchiveClick="() => handleFolderPress('archive')" />
/>
</ion-list> </ion-list>
</ion-content> </ion-content>
</ion-page> </ion-page>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../stores/useAppStore'; import { useAppStore } from '../stores/useAppStore'
import { search, closeCircle } from 'ionicons/icons'; import { search, closeCircle } from 'ionicons/icons'
import FolderManage from '../components/FolderManage.vue'; import FolderManage from '../components/FolderManage.vue'
import Header from '../components/Header.vue'; import Header from '../components/Header.vue'
const store = useAppStore(); const store = useAppStore()
// 加载初始数据 // 加载初始数据
onMounted(() => { onMounted(() => {
store.loadData(); store.loadData()
}); })
const searchQuery = ref(''); const searchQuery = ref('')
const selectedFolder = ref('all'); const selectedFolder = ref('all')
// Calculate note count for each folder // Calculate note count for each folder
const foldersWithCount = computed(() => { const foldersWithCount = computed(() => {
return store.folders.map(folder => { return store.folders.map(folder => {
const noteCount = store.notes.filter(note => note.folderId === folder.id).length; const noteCount = store.notes.filter(note => note.folderId === folder.id).length
return { return {
...folder, ...folder,
noteCount, noteCount,
}; }
}); })
}); })
// Add default folders at the beginning // Add default folders at the beginning
const allNotesCount = computed(() => store.notes.length); const allNotesCount = computed(() => store.notes.length)
const starredNotesCount = computed(() => store.notes.filter(note => note.isStarred).length); const starredNotesCount = computed(() => store.notes.filter(note => note.isStarred).length)
// Assuming we have a way to track deleted notes in the future // Assuming we have a way to track deleted notes in the future
const trashNotesCount = 0; const trashNotesCount = 0
const archiveCount = 0; const archiveCount = 0
const foldersWithAllNotes = computed(() => { const foldersWithAllNotes = computed(() => {
return [ return [
@@ -81,39 +68,76 @@ const foldersWithAllNotes = computed(() => {
{ id: 'starred', name: '加星便签', noteCount: starredNotesCount.value, createdAt: new Date() }, { id: 'starred', name: '加星便签', noteCount: starredNotesCount.value, createdAt: new Date() },
{ id: 'trash', name: '回收站', noteCount: trashNotesCount, createdAt: new Date() }, { id: 'trash', name: '回收站', noteCount: trashNotesCount, createdAt: new Date() },
...foldersWithCount.value, ...foldersWithCount.value,
]; ]
}); })
const handleFolderPress = (folderId) => { const handleFolderPress = folderId => {
// 更新选中的文件夹状态 // 更新选中的文件夹状态
selectedFolder.value = folderId; selectedFolder.value = folderId
// 在实际应用中这里会将选中的文件夹传递回NoteListScreen // 在实际应用中这里会将选中的文件夹传递回NoteListScreen
// 通过导航参数传递选中的文件夹ID // 通过导航参数传递选中的文件夹ID
window.location.hash = `#/notes?folder=${folderId}`; window.location.hash = `#/notes?folder=${folderId}`
}; }
const handleAddFolder = () => { const handleAddFolder = () => {
// In a full implementation, this would open a folder creation dialog // In a full implementation, this would open a folder creation dialog
console.log('Add folder pressed'); console.log('Add folder pressed')
}; }
const handleSearch = () => { const handleSearch = () => {
// In a full implementation, this would filter folders based on searchQuery // In a full implementation, this would filter folders based on searchQuery
console.log('Search for:', searchQuery.value); console.log('Search for:', searchQuery.value)
}; }
const handleBackPress = () => { const handleBackPress = () => {
window.history.back(); window.history.back()
}; }
// Filter folders based on search query // Filter folders based on search query
const filteredFolders = computed(() => { const filteredFolders = computed(() => {
return foldersWithAllNotes.value.filter(folder => return foldersWithAllNotes.value.filter(folder => folder.name.toLowerCase().includes(searchQuery.value.toLowerCase()))
folder.name.toLowerCase().includes(searchQuery.value.toLowerCase()) })
);
});
const setSearchQuery = (value) => { const setSearchQuery = value => {
searchQuery.value = value; searchQuery.value = value
}; }
</script> </script>
<style scoped>
.folder-page-container {
padding: 10px;
background-color: var(--background);
}
.search-container {
display: flex;
align-items: center;
background-color: var(--search-bar-background);
border-radius: 8px;
padding: 0 10px;
}
.search-icon {
font-size: 20px;
color: var(--text-tertiary);
}
.search-input {
--padding-start: 10px;
--padding-end: 10px;
flex: 1;
font-size: 16px;
color: var(--text-primary);
}
.clear-icon {
font-size: 20px;
color: var(--text-tertiary);
}
.folder-list {
background-color: var(--background);
padding: 0 16px;
--ion-item-background: var(--background);
}
</style>

View File

@@ -4,7 +4,7 @@
<Header :onBack="handleCancel" :onAction="handleAction" actionIcon="save" /> <Header :onBack="handleCancel" :onAction="handleAction" actionIcon="save" />
<!-- 顶部信息栏 --> <!-- 顶部信息栏 -->
<div class="header-info" v-if="isEditing"> <div class="header-info">
<span class="edit-time">{{ formattedTime }}</span> <span class="edit-time">{{ formattedTime }}</span>
<span>|</span> <span>|</span>
<span class="word-count">{{ wordCount }}</span> <span class="word-count">{{ wordCount }}</span>
@@ -12,7 +12,11 @@
<!-- 富文本编辑器 --> <!-- 富文本编辑器 -->
<div class="editor-container"> <div class="editor-container">
<RichTextEditor ref="editorRef" v-model="content" class="rich-text-editor" /> <RichTextEditor
ref="editorRef"
:modelValue="content"
@update:modelValue="handleContentChange"
class="rich-text-editor" />
</div> </div>
<ion-alert <ion-alert
@@ -35,34 +39,123 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, nextTick, watch } from 'vue'
import { useAppStore } from '../stores/useAppStore' import { useAppStore } from '../stores/useAppStore'
import Header from '../components/Header.vue' import Header from '../components/Header.vue'
import RichTextEditor from '../components/RichTextEditor.vue' import RichTextEditor from '../components/RichTextEditor.vue'
const props = defineProps({ const props = defineProps({
noteId: { id: {
type: String, type: String,
default: null, default: null,
}, },
}) })
// 为了保持向后兼容性我们也支持noteId属性
const noteId = computed(() => props.id || props.noteId)
const store = useAppStore() const store = useAppStore()
const editorRef = ref(null) const editorRef = ref(null)
// 设置便签内容的函数
const setNoteContent = async (noteId) => {
// 确保store数据已加载
if (store.notes.length === 0) {
await store.loadData()
console.log('Store loaded, notes count:', store.notes.length)
}
const note = store.notes.find(n => n.id === noteId)
console.log('Found note:', note)
// 确保编辑器已经初始化
await nextTick()
console.log('Editor ref:', editorRef.value)
if (note) {
console.log('Setting content:', note.content)
// 无论editorRef是否可用都先设置content的值
content.value = note.content || ''
// 如果editorRef可用直接设置内容
if (editorRef.value) {
editorRef.value.setContent(note.content || '')
}
} else {
console.log('Note not available')
}
}
// 加载初始数据 // 加载初始数据
onMounted(async () => { onMounted(async () => {
console.log('NoteEditorPage mounted')
await store.loadData() await store.loadData()
console.log('Store loaded, notes count:', store.notes.length)
// 如果是编辑现有便签,在组件挂载后设置内容
if (noteId.value) {
await setNoteContent(noteId.value)
}
}) })
// 监听noteId变化确保在编辑器准备好后设置内容
watch(noteId, async (newNoteId) => {
console.log('Note ID changed:', newNoteId)
if (newNoteId) {
await setNoteContent(newNoteId)
}
}, { immediate: true })
// 监听store变化确保在store加载后设置内容
watch(() => store.notes, async (newNotes) => {
if (noteId.value && newNotes.length > 0) {
await setNoteContent(noteId.value)
}
}, { immediate: true })
// Check if we're editing an existing note // Check if we're editing an existing note
const isEditing = !!props.noteId const isEditing = !!noteId.value
const existingNote = isEditing ? store.notes.find(n => n.id === props.noteId) : null const existingNote = isEditing ? store.notes.find(n => n.id === noteId.value) : null
// Initialize state with existing note data or empty strings // Initialize state with existing note data or empty strings
const content = ref(existingNote?.content || '') const content = ref(existingNote?.content || '')
// 当组件挂载时,确保编辑器初始化为空内容(针对新建便签)
onMounted(() => {
if (!isEditing && editorRef.value) {
console.log('Initializing editor for new note')
editorRef.value.setContent('')
}
})
// 监听store变化确保在store加载后设置内容
watch(() => store.notes, async (newNotes) => {
if (noteId.value && newNotes.length > 0) {
await setNoteContent(noteId.value)
}
}, { immediate: true })
const showAlert = ref(false) const showAlert = ref(false)
// 防抖函数
const debounce = (func, delay) => {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
}
}
// 防抖处理内容变化
const debouncedHandleContentChange = debounce((newContent) => {
content.value = newContent
console.log('Content updated:', newContent)
}, 300)
// 监听编辑器内容变化
const handleContentChange = (newContent) => {
console.log('Editor content changed:', newContent)
debouncedHandleContentChange(newContent)
}
// 计算属性 // 计算属性
const formattedTime = computed(() => { const formattedTime = computed(() => {
const now = new Date() const now = new Date()
@@ -93,15 +186,18 @@ const wordCount = computed(() => {
// 处理保存 // 处理保存
const handleSave = async () => { const handleSave = async () => {
try { try {
// 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
if (isEditing && existingNote) { if (isEditing && existingNote) {
// Update existing note // Update existing note
await store.updateNote(props.noteId, { await store.updateNote(noteId.value, {
content: content.value, content: editorContent,
}) })
} else { } else {
// Create new note // Create new note
await store.addNote({ await store.addNote({
content: content.value, content: editorContent,
isStarred: false, isStarred: false,
}) })
} }
@@ -129,9 +225,12 @@ const handleCancel = () => {
// 处理创建(用于新建便签) // 处理创建(用于新建便签)
const handleCreate = async () => { const handleCreate = async () => {
try { try {
// 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
// Create new note // Create new note
await store.addNote({ await store.addNote({
content: content.value, content: editorContent,
isStarred: false, isStarred: false,
}) })
@@ -185,7 +284,7 @@ const setShowAlert = value => {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 10px;
padding: 8px 16px; padding: 1.5rem 16px 0.7rem 16px;
background-color: var(--background-card); background-color: var(--background-card);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
font-size: 0.7rem; font-size: 0.7rem;

View File

@@ -6,41 +6,26 @@
<!-- 悬浮文件夹列表 - 使用绝对定位实现 --> <!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
<div <div
v-if="isFolderExpanded" v-if="isFolderExpanded"
style="position: absolute; top: 50px; left: 10%; right: 10%; z-index: 1000; background-color: var(--background-card); border-radius: 8px; box-shadow: 0 2px 4px var(--shadow); border: 1px solid #f0ece7; overflow: hidden"> class="folder-list">
<FolderManage <FolderManage
:allCount="notes.length" :allCount="allNotesCount"
:starredCount="starredNotesCount" :starredCount="starredNotesCount"
:trashCount="0" :trashCount="trashNotesCount"
:archiveCount="0" :archiveCount="0"
:selectedFolder="currentFolder" :selectedFolder="currentFolder"
:onAllClick=" :onAllClick="handleAllNotesClick"
() => { :onStarredClick="handleStarredNotesClick"
setCurrentFolder('all') :onTrashClick="handleTrashNotesClick" />
setIsFolderExpanded(false)
}
"
:onStarredClick="
() => {
setCurrentFolder('starred')
setIsFolderExpanded(false)
}
"
:onTrashClick="
() => {
setCurrentFolder('trash')
setIsFolderExpanded(false)
}
" />
</div> </div>
<!-- 点击外部区域收起文件夹列表的覆盖层 --> <!-- 点击外部区域收起文件夹列表的覆盖层 -->
<div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: transparent; z-index: 99"></div> <div v-if="isFolderExpanded" @click="() => setIsFolderExpanded(false)" class="folder-overlay"></div>
<div style="padding: 0.8rem 0.5rem"> <div class="search-container">
<SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" /> <SearchBar v-model="searchQuery" @search="handleSearch" @clear="handleClearSearch" @focus="handleSearchFocus" @blur="handleSearchBlur" />
</div> </div>
<div style="flex: 1"> <div class="notes-container">
<div v-for="note in filteredAndSortedNotes" :key="note.id" style="margin: 0.4rem 0"> <div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
<NoteItem <NoteItem
:title="note.title" :title="note.title"
:content="note.content" :content="note.content"
@@ -76,7 +61,6 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../stores/useAppStore' import { useAppStore } from '../stores/useAppStore'
import { create, settings } from 'ionicons/icons'
import NoteItem from '../components/NoteItem.vue' import NoteItem from '../components/NoteItem.vue'
import Header from '../components/Header.vue' import Header from '../components/Header.vue'
import FolderManage from '../components/FolderManage.vue' import FolderManage from '../components/FolderManage.vue'
@@ -102,50 +86,67 @@ const currentFolder = ref('all') // 默认文件夹是"全部便签"
const showAlert = ref(false) const showAlert = ref(false)
const noteToDelete = ref(null) const noteToDelete = ref(null)
// 计算加星便签数量 // 计算加星便签数量(未删除的)
const starredNotesCount = computed(() => { const starredNotesCount = computed(() => {
return store.notes.filter(note => note.isStarred).length return store.notes.filter(note => note.isStarred && !note.isDeleted).length
}) })
// 计算置顶便签数量 // 计算回收站便签数量
const topNotesCount = computed(() => { const trashNotesCount = computed(() => {
return filteredAndSortedNotes.value.filter(note => note.isTop).length return store.notes.filter(note => note.isDeleted).length
}) })
// 根据当前文件夹过滤便签 // 根据当前文件夹过滤便签
const filteredNotes = computed(() => { const filteredNotes = computed(() => {
// 预处理搜索查询,提高性能
const lowerCaseQuery = searchQuery.value.toLowerCase().trim()
return store.notes.filter(note => { return store.notes.filter(note => {
// 先检查搜索条件
const matchesSearch = !lowerCaseQuery ||
note.title.toLowerCase().includes(lowerCaseQuery) ||
note.content.toLowerCase().includes(lowerCaseQuery)
if (!matchesSearch) return false
// 再检查文件夹条件
switch (currentFolder.value) { switch (currentFolder.value) {
case 'all': case 'all':
return true // 全部便签中不显示已删除的便签
return !note.isDeleted
case 'starred': case 'starred':
return note.isStarred // 加星便签中只显示未删除的加星便签
return note.isStarred && !note.isDeleted
case 'trash': case 'trash':
// 假设我们有一个isDeleted属性来标识已删除的便签 // 回收站中只显示已删除的便签
return note.isDeleted || false return note.isDeleted
default: default:
return note.folderId === currentFolder.value // 自定义文件夹中不显示已删除的便签
return note.folderId === currentFolder.value && !note.isDeleted
} }
}) })
}) })
// Filter and sort notes // Filter and sort notes
const filteredAndSortedNotes = computed(() => { const filteredAndSortedNotes = computed(() => {
return filteredNotes.value return [...filteredNotes.value].sort((a, b) => {
.filter(note => note.title.toLowerCase().includes(searchQuery.value.toLowerCase()) || note.content.toLowerCase().includes(searchQuery.value.toLowerCase())) // 置顶的便签排在前面
.sort((a, b) => { if (a.isTop && !b.isTop) return -1
// 置顶的便签排在前面 if (!a.isTop && b.isTop) return 1
if (a.isTop && !b.isTop) return -1
if (!a.isTop && b.isTop) return 1
if (sortBy.value === 'title') { // 根据排序方式排序
switch (sortBy.value) {
case 'title':
return a.title.localeCompare(b.title) return a.title.localeCompare(b.title)
} else if (sortBy.value === 'starred') { case 'starred':
// 加星的便签排在前面
return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0) return (b.isStarred ? 1 : 0) - (a.isStarred ? 1 : 0)
} else { case 'date':
default:
// 按更新时间倒序排列(最新的在前)
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
} }
}) })
}) })
// 计算头部标题 // 计算头部标题
@@ -158,10 +159,15 @@ const headerTitle = computed(() => {
case 'trash': case 'trash':
return '回收站' return '回收站'
default: default:
return '文件夹' return '全部便签'
} }
}) })
// 计算全部便签数量(未删除的)
const allNotesCount = computed(() => {
return store.notes.filter(note => !note.isDeleted).length
})
const handleNotePress = noteId => { const handleNotePress = noteId => {
// 导航到编辑页面的逻辑将在路由中处理 // 导航到编辑页面的逻辑将在路由中处理
window.location.hash = `#/editor/${noteId}` window.location.hash = `#/editor/${noteId}`
@@ -187,21 +193,44 @@ const handleDeleteNote = noteId => {
const handleStarToggle = async noteId => { const handleStarToggle = async noteId => {
const note = store.notes.find(n => n.id === noteId) const note = store.notes.find(n => n.id === noteId)
if (note) { if (note) {
await store.updateNote(noteId, { isStarred: !note.isStarred }) try {
await store.updateNote(noteId, { isStarred: !note.isStarred })
console.log(`Note ${noteId} starred status updated`)
} catch (error) {
console.error('Failed to update note star status:', error)
}
} }
} }
const handleTopToggle = async noteId => { const handleTopToggle = async noteId => {
const note = store.notes.find(n => n.id === noteId) const note = store.notes.find(n => n.id === noteId)
if (note) { if (note) {
await store.updateNote(noteId, { isTop: !note.isTop }) try {
await store.updateNote(noteId, { isTop: !note.isTop })
console.log(`Note ${noteId} top status updated`)
} catch (error) {
console.error('Failed to update note top status:', error)
}
} }
} }
const confirmDeleteNote = () => { const confirmDeleteNote = async () => {
if (noteToDelete.value) { if (noteToDelete.value) {
store.deleteNote(noteToDelete.value) try {
noteToDelete.value = null // 检查当前是否在回收站中
if (currentFolder.value === 'trash') {
// 在回收站中删除便签,彻底删除
await store.permanentlyDeleteNote(noteToDelete.value)
console.log(`Note ${noteToDelete.value} permanently deleted`)
} else {
// 不在回收站中,将便签移至回收站
await store.moveToTrash(noteToDelete.value)
console.log(`Note ${noteToDelete.value} moved to trash`)
}
noteToDelete.value = null
} catch (error) {
console.error('Failed to delete note:', error)
}
} }
showAlert.value = false showAlert.value = false
} }
@@ -215,6 +244,21 @@ const handleSort = () => {
console.log('Sort by:', sortOptions[nextIndex]) console.log('Sort by:', sortOptions[nextIndex])
} }
const handleAllNotesClick = () => {
setCurrentFolder('all')
setIsFolderExpanded(false)
}
const handleStarredNotesClick = () => {
setCurrentFolder('starred')
setIsFolderExpanded(false)
}
const handleTrashNotesClick = () => {
setCurrentFolder('trash')
setIsFolderExpanded(false)
}
const handleFolderPress = () => { const handleFolderPress = () => {
// 导航到文件夹页面的逻辑将在路由中处理 // 导航到文件夹页面的逻辑将在路由中处理
window.location.hash = '#/folders' window.location.hash = '#/folders'
@@ -228,29 +272,77 @@ const handleSettingsPress = () => {
const handleFolderToggle = () => { const handleFolderToggle = () => {
// 在实际应用中,这里会触发文件夹列表的展开/收起 // 在实际应用中,这里会触发文件夹列表的展开/收起
isFolderExpanded.value = !isFolderExpanded.value isFolderExpanded.value = !isFolderExpanded.value
console.log('Folder expanded:', !isFolderExpanded.value)
} }
const handleSearch = query => { const handleSearch = query => {
// 搜索功能已在computed属性filteredAndSortedNotes中实现 // 搜索功能已在computed属性filteredAndSortedNotes中实现
console.log('Search for:', query) console.log('Search for:', query)
// 可以在这里添加搜索统计或其它功能
if (query && query.length > 0) {
console.log(`Found ${filteredAndSortedNotes.value.length} matching notes`)
}
} }
const handleClearSearch = () => { const handleClearSearch = () => {
// 清除搜索已在v-model中处理 // 清除搜索已在v-model中处理
console.log('Search cleared') console.log('Search cleared')
// 清除搜索后可以重置一些状态
setSearchQuery('')
} }
const handleSearchFocus = () => { const handleSearchFocus = () => {
console.log('Search bar focused') console.log('Search bar focused')
// 可以在这里添加获得焦点时的特殊处理
} }
const handleSearchBlur = () => { const handleSearchBlur = () => {
console.log('Search bar blurred') console.log('Search bar blurred')
// 可以在这里添加失去焦点时的特殊处理
} }
// 防抖搜索函数,避免频繁触发搜索
const debounceSearch = (func, delay) => {
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func.apply(this, args), delay)
}
}
// 防抖搜索处理
const debouncedHandleSearch = debounceSearch((query) => {
handleSearch(query)
}, 300)
// 改进的日期格式化函数
const formatDate = dateString => { const formatDate = dateString => {
return new Date(dateString).toLocaleDateString() const date = new Date(dateString)
const now = new Date()
// 计算日期差
const diffTime = now - date
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
// 今天的便签显示时间
if (diffDays === 0) {
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 昨天的便签显示"昨天"
if (diffDays === 1) {
return '昨天'
}
// 一周内的便签显示星期几
if (diffDays < 7) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdays[date.getDay()]
}
// 超过一周的便签显示月日
return `${date.getMonth() + 1}/${date.getDate()}`
} }
const setCurrentFolder = folder => { const setCurrentFolder = folder => {
@@ -278,4 +370,39 @@ const notes = computed(() => store.notes)
background: url(/assets/icons/drawable-xxhdpi/note_background.png); background: url(/assets/icons/drawable-xxhdpi/note_background.png);
background-size: cover; background-size: cover;
} }
</style>
.folder-list {
position: absolute;
top: 50px;
left: 10%;
right: 10%;
z-index: 1000;
background-color: var(--background-card);
border-radius: 8px;
box-shadow: 0 2px 4px 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;
}
.search-container {
padding: 0.8rem 0.5rem;
}
.notes-container {
flex: 1;
}
.note-item {
margin: 0.4rem 0;
}
</style>

View File

@@ -5,31 +5,31 @@
:onBack="handleBackPress" :onBack="handleBackPress"
/> />
<ion-content style="background-color: var(--background)"> <ion-content class="settings-content">
<div style="margin-bottom: 12px; background-color: var(--background-card)"> <div class="settings-section">
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px"> <div class="section-header">
账户 账户
</div> </div>
<div button @click="handleLogin" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer"> <div button @click="handleLogin" class="settings-item settings-item-clickable">
<div style="font-size: 16px; color: var(--text-primary)">登录云同步</div> <div class="item-text-primary">登录云同步</div>
<div style="font-size: 15px; color: var(--text-tertiary)">未登录</div> <div class="item-text-tertiary">未登录</div>
</div> </div>
</div> </div>
<div style="margin-bottom: 12px; background-color: var(--background-card)"> <div class="settings-section">
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px"> <div class="section-header">
偏好设置 偏好设置
</div> </div>
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border)"> <div class="settings-item settings-item-border">
<div style="font-size: 16px; color: var(--text-primary)">云同步</div> <div class="item-text-primary">云同步</div>
<ion-toggle <ion-toggle
slot="end" slot="end"
:checked="settings.cloudSync" :checked="settings.cloudSync"
@ion-change="toggleCloudSync" @ion-change="toggleCloudSync"
></ion-toggle> ></ion-toggle>
</div> </div>
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px"> <div class="settings-item">
<div style="font-size: 16px; color: var(--text-primary)">深色模式</div> <div class="item-text-primary">深色模式</div>
<ion-toggle <ion-toggle
slot="end" slot="end"
:checked="settings.darkMode" :checked="settings.darkMode"
@@ -38,41 +38,41 @@
</div> </div>
</div> </div>
<div style="margin-bottom: 12px; background-color: var(--background-card)"> <div class="settings-section">
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px"> <div class="section-header">
数据管理 数据管理
</div> </div>
<div button @click="handleBackup" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer"> <div button @click="handleBackup" class="settings-item settings-item-clickable settings-item-border">
<img :src="'/assets/icons/drawable-xxhdpi/btn_save_pic.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" /> <img :src="'/assets/icons/drawable-xxhdpi/btn_save_pic.png'" class="item-icon" />
<div style="font-size: 16px; color: var(--text-primary)">备份便签</div> <div class="item-text-primary">备份便签</div>
</div> </div>
<div button @click="handleRestore" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer"> <div button @click="handleRestore" class="settings-item settings-item-clickable settings-item-border">
<img :src="'/assets/icons/drawable-xxhdpi/btn_restore.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" /> <img :src="'/assets/icons/drawable-xxhdpi/btn_restore.png'" class="item-icon" />
<div style="font-size: 16px; color: var(--text-primary)">恢复便签</div> <div class="item-text-primary">恢复便签</div>
</div> </div>
<div button @click="handleExport" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer"> <div button @click="handleExport" class="settings-item settings-item-clickable settings-item-border">
<img :src="'/assets/icons/drawable-xxhdpi/btn_share.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" /> <img :src="'/assets/icons/drawable-xxhdpi/btn_share.png'" class="item-icon" />
<div style="font-size: 16px; color: var(--text-primary)">导出便签</div> <div class="item-text-primary">导出便签</div>
</div> </div>
<div button @click="handleImport" style="display: flex; align-items: center; background-color: var(--background-card); padding: 14px 16px; cursor: pointer"> <div button @click="handleImport" class="settings-item settings-item-clickable">
<img :src="'/assets/icons/drawable-xxhdpi/btn_load_error.png'" style="width: 20px; height: 20px; color: var(--text-primary); margin-right: 12px" /> <img :src="'/assets/icons/drawable-xxhdpi/btn_load_error.png'" class="item-icon" />
<div style="font-size: 16px; color: var(--text-primary)">导入便签</div> <div class="item-text-primary">导入便签</div>
</div> </div>
</div> </div>
<div style="margin-bottom: 12px; background-color: var(--background-card)"> <div class="settings-section">
<div style="background-color: var(--background-secondary); font-size: 13px; font-weight: 600; color: var(--text-tertiary); padding: 10px 16px"> <div class="section-header">
关于 关于
</div> </div>
<div style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border)"> <div class="settings-item settings-item-border">
<div style="font-size: 16px; color: var(--text-primary)">版本</div> <div class="item-text-primary">版本</div>
<div style="font-size: 15px; color: var(--text-tertiary)">1.0.0</div> <div class="item-text-tertiary">1.0.0</div>
</div> </div>
<div button @click="handlePrivacyPolicy" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer"> <div button @click="handlePrivacyPolicy" class="settings-item settings-item-clickable settings-item-border">
<div style="font-size: 16px; color: var(--text-primary)">隐私政策</div> <div class="item-text-primary">隐私政策</div>
</div> </div>
<div button @click="handleTermsOfService" style="display: flex; justify-content: space-between; align-items: center; background-color: var(--background-card); padding: 14px 16px; cursor: pointer"> <div button @click="handleTermsOfService" class="settings-item settings-item-clickable">
<div style="font-size: 16px; color: var(--text-primary)">服务条款</div> <div class="item-text-primary">服务条款</div>
</div> </div>
</div> </div>
</ion-content> </ion-content>
@@ -139,4 +139,65 @@ const handleBackPress = () => {
}; };
const settings = computed(() => store.settings); const settings = computed(() => store.settings);
</script> </script>
<style scoped>
.settings-content {
background-color: var(--background);
}
.settings-section {
margin-bottom: 12px;
background-color: var(--background-card);
}
.section-header {
background-color: var(--background-secondary);
font-size: 13px;
font-weight: 600;
color: var(--text-tertiary);
padding: 10px 16px;
}
.settings-item {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--background-card);
padding: 14px 16px;
}
.settings-item-border {
border-bottom: 1px solid var(--border);
}
.settings-item-clickable {
cursor: pointer;
}
.item-text-primary {
font-size: 16px;
color: var(--text-primary);
}
.item-text-tertiary {
font-size: 15px;
color: var(--text-tertiary);
}
.item-icon {
width: 20px;
height: 20px;
color: var(--text-primary);
margin-right: 12px;
}
.settings-item-clickable .item-text-primary {
flex: 1;
}
.settings-item-clickable {
display: flex;
align-items: center;
}
</style>

View File

@@ -1,44 +1,44 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia'
import * as storage from '../utils/storage'; import * as storage from '../utils/storage'
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({
notes: [], notes: [],
folders: [], folders: [],
settings: { cloudSync: false, darkMode: false } settings: { cloudSync: false, darkMode: false },
}), }),
getters: { getters: {
starredNotesCount: (state) => { starredNotesCount: state => {
return state.notes.filter(note => note.isStarred).length; return state.notes.filter(note => note.isStarred).length
},
allNotesCount: state => {
return state.notes.length
}, },
allNotesCount: (state) => {
return state.notes.length;
}
}, },
actions: { actions: {
// 初始化数据 // 初始化数据
async loadData() { async loadData() {
try { try {
const loadedNotes = await storage.getNotes(); const loadedNotes = await storage.getNotes()
const loadedFolders = await storage.getFolders(); const loadedFolders = await storage.getFolders()
const loadedSettings = await storage.getSettings(); const loadedSettings = await storage.getSettings()
// 如果没有数据则加载mock数据 // 如果没有数据则加载mock数据
if (loadedNotes.length === 0 && loadedFolders.length === 0) { if (loadedNotes.length === 0 && loadedFolders.length === 0) {
this.loadMockData(); this.loadMockData()
} else { } else {
this.notes = loadedNotes; this.notes = loadedNotes
this.folders = loadedFolders; this.folders = loadedFolders
this.settings = loadedSettings; this.settings = loadedSettings
} }
} catch (error) { } catch (error) {
console.error('Error loading data:', error); console.error('Error loading data:', error)
} }
}, },
// 加载mock数据 // 加载mock数据
async loadMockData() { async loadMockData() {
// Mock notes // Mock notes
@@ -52,7 +52,9 @@ export const useAppStore = defineStore('app', {
folderId: null, folderId: null,
isStarred: true, isStarred: true,
isTop: true, isTop: true,
hasImage: false hasImage: false,
isDeleted: false,
deletedAt: null,
}, },
{ {
id: '2', id: '2',
@@ -63,7 +65,9 @@ export const useAppStore = defineStore('app', {
folderId: null, folderId: null,
isStarred: true, isStarred: true,
isTop: false, isTop: false,
hasImage: true hasImage: true,
isDeleted: false,
deletedAt: null,
}, },
{ {
id: '3', id: '3',
@@ -74,7 +78,9 @@ export const useAppStore = defineStore('app', {
folderId: null, folderId: null,
isStarred: false, isStarred: false,
isTop: false, isTop: false,
hasImage: false hasImage: false,
isDeleted: false,
deletedAt: null,
}, },
{ {
id: '4', id: '4',
@@ -85,7 +91,9 @@ export const useAppStore = defineStore('app', {
folderId: null, folderId: null,
isStarred: false, isStarred: false,
isTop: false, isTop: false,
hasImage: false hasImage: false,
isDeleted: false,
deletedAt: null,
}, },
{ {
id: '5', id: '5',
@@ -96,145 +104,191 @@ export const useAppStore = defineStore('app', {
folderId: null, folderId: null,
isStarred: false, isStarred: false,
isTop: false, isTop: false,
hasImage: false hasImage: false,
} isDeleted: false,
]; deletedAt: null,
},
{
id: '6',
title: '已删除的便签',
content: '这是一条已删除的便签示例,应该只在回收站中显示。',
createdAt: new Date(Date.now() - 432000000).toISOString(), // 5天前
updatedAt: new Date(Date.now() - 432000000).toISOString(),
folderId: null,
isStarred: false,
isTop: false,
hasImage: false,
isDeleted: true,
deletedAt: new Date(Date.now() - 86400000).toISOString(), // 1天前删除
},
]
// Mock folders // Mock folders
const mockFolders = [ const mockFolders = [
{ {
id: 'folder1', id: 'folder1',
name: '工作', name: '工作',
createdAt: new Date().toISOString() createdAt: new Date().toISOString(),
}, },
{ {
id: 'folder2', id: 'folder2',
name: '个人', name: '个人',
createdAt: new Date().toISOString() createdAt: new Date().toISOString(),
}, },
{ {
id: 'folder3', id: 'folder3',
name: '学习', name: '学习',
createdAt: new Date().toISOString() createdAt: new Date().toISOString(),
} },
]; ]
// Mock settings // Mock settings
const mockSettings = { const mockSettings = {
cloudSync: false, cloudSync: false,
darkMode: false darkMode: false,
}; }
this.notes = mockNotes; this.notes = mockNotes
this.folders = mockFolders; this.folders = mockFolders
this.settings = mockSettings; this.settings = mockSettings
// 保存到localStorage // 保存到localStorage
await storage.saveNotes(mockNotes); await storage.saveNotes(mockNotes)
await storage.saveFolders(mockFolders); await storage.saveFolders(mockFolders)
await storage.saveSettings(mockSettings); await storage.saveSettings(mockSettings)
}, },
// 保存notes到localStorage // 保存notes到localStorage
async saveNotes() { async saveNotes() {
try { try {
await storage.saveNotes(this.notes); await storage.saveNotes(this.notes)
} catch (error) { } catch (error) {
console.error('Error saving notes:', error); console.error('Error saving notes:', error)
} }
}, },
// 保存folders到localStorage // 保存folders到localStorage
async saveFolders() { async saveFolders() {
try { try {
await storage.saveFolders(this.folders); await storage.saveFolders(this.folders)
} catch (error) { } catch (error) {
console.error('Error saving folders:', error); console.error('Error saving folders:', error)
} }
}, },
// 保存settings到localStorage // 保存settings到localStorage
async saveSettings() { async saveSettings() {
try { try {
await storage.saveSettings(this.settings); await storage.saveSettings(this.settings)
} catch (error) { } catch (error) {
console.error('Error saving settings:', error); console.error('Error saving settings:', error)
} }
}, },
// Note functions // Note functions
async addNote(note) { async addNote(note) {
try { try {
const newNote = await storage.addNote(note); const newNote = await storage.addNote(note)
this.notes.push(newNote); this.notes.push(newNote)
return newNote; return newNote
} catch (error) { } catch (error) {
console.error('Error adding note:', error); console.error('Error adding note:', error)
throw error; throw error
} }
}, },
async updateNote(id, updates) { async updateNote(id, updates) {
try { try {
const updatedNote = await storage.updateNote(id, updates); const updatedNote = await storage.updateNote(id, updates)
if (updatedNote) { if (updatedNote) {
const index = this.notes.findIndex(note => note.id === id); const index = this.notes.findIndex(note => note.id === id)
if (index !== -1) { if (index !== -1) {
this.notes[index] = updatedNote; this.notes[index] = updatedNote
} }
} }
return updatedNote; return updatedNote
} catch (error) { } catch (error) {
console.error('Error updating note:', error); console.error('Error updating note:', error)
throw error; throw error
} }
}, },
async deleteNote(id) { async deleteNote(id) {
try { try {
const result = await storage.deleteNote(id); const result = await storage.deleteNote(id)
if (result) { if (result) {
this.notes = this.notes.filter(note => note.id !== id); this.notes = this.notes.filter(note => note.id !== id)
} }
return result; return result
} catch (error) { } catch (error) {
console.error('Error deleting note:', error); console.error('Error deleting note:', error)
throw error; throw error
} }
}, },
// 将便签移至回收站
async moveToTrash(id) {
try {
const updatedNote = await storage.updateNote(id, { isDeleted: true, deletedAt: new Date().toISOString() })
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
}
},
// 永久删除便签
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
}
},
// Folder functions // Folder functions
async addFolder(folder) { async addFolder(folder) {
try { try {
const newFolder = await storage.addFolder(folder); const newFolder = await storage.addFolder(folder)
this.folders.push(newFolder); this.folders.push(newFolder)
return newFolder; return newFolder
} catch (error) { } catch (error) {
console.error('Error adding folder:', error); console.error('Error adding folder:', error)
throw error; throw error
} }
}, },
// Settings functions // Settings functions
async updateSettings(newSettings) { async updateSettings(newSettings) {
try { try {
const updatedSettings = { ...this.settings, ...newSettings }; const updatedSettings = { ...this.settings, ...newSettings }
this.settings = updatedSettings; this.settings = updatedSettings
await storage.saveSettings(updatedSettings); await storage.saveSettings(updatedSettings)
} catch (error) { } catch (error) {
console.error('Error updating settings:', error); console.error('Error updating settings:', error)
throw error; throw error
} }
}, },
// 切换云同步设置 // 切换云同步设置
async toggleCloudSync() { async toggleCloudSync() {
await this.updateSettings({ cloudSync: !this.settings.cloudSync }); await this.updateSettings({ cloudSync: !this.settings.cloudSync })
}, },
// 切换深色模式设置 // 切换深色模式设置
async toggleDarkMode() { async toggleDarkMode() {
await this.updateSettings({ darkMode: !this.settings.darkMode }); await this.updateSettings({ darkMode: !this.settings.darkMode })
} },
} },
}); })

View File

@@ -1,74 +0,0 @@
// Smartisan Notes Color Scheme - Based on Original Design
export default {
// Primary colors - Original Smartisan Notes brown/gold palette
primary: '#5c3c2a', // Main brown color for UI elements
primaryDark: '#4a3224', // Darker shade of primary
primaryLight: '#f5f0e6', // Light background tone
// Background colors - Warm paper-like tones
background: '#fbf7ed', // Main app background - warm off-white
backgroundSecondary: '#f7f2e9', // Slightly darker background
backgroundCard: '#ffffff', // Pure white for cards/notes
searchBarBackground: '#f0f0f0', // Search bar background - light gray
// Text colors - Brown/black tones for readability
textPrimary: '#5c3c2a', // Main text color - dark brown
textSecondary: '#6e482f', // Secondary text - medium brown
textTertiary: '#9e836c', // Tertiary text - light brown/gray
textInverted: '#ffffff', // White text for dark backgrounds
// Accent colors - Smartisan's signature colors
accentBlue: '#5c89f2', // Blue for links/actions
accentGreen: '#97cc4e', // Green for success/positive actions
accentRed: '#e65c53', // Red for errors/dangerous actions
accentOrange: '#f0880d', // Orange for warnings/highlights
accentYellow: '#ffd633', // Yellow for starred items/highlights (updated to match original)
// Note specific colors
noteTitle: '#5c3c2a', // Note title color
noteContent: '#6e482f', // Note content color
noteDate: '#b9a691', // Date/time color
noteStar: '#ffd633', // Star/favorite color (updated to match original)
// Folder colors
folderName: '#5c3c2a', // Folder name color
folderCount: '#99000000', // Folder item count color (60% black)
folderItemSelected: '#f0f0f0', // Folder item selected background color
// Button colors - Based on Smartisan's button styles
buttonPrimary: '#5c3c2a', // Primary button - brown
buttonSecondary: '#97cc4e', // Secondary button - green
buttonDanger: '#e65c53', // Danger button - red
buttonDisabled: '#d4d4d5', // Disabled button - light gray
// Status colors
success: '#79ad31', // Success - green
warning: '#f0880d', // Warning - orange
error: '#e64746', // Error - red
info: '#5c89f2', // Info - blue
// UI elements - Borders, dividers, shadows
border: '#e5ddca', // Light brown border
divider: '#e5e5e5', // Light gray divider
shadow: '#00000014', // Subtle shadow
// Transparency variants
black05: '#0000000d', // 5% black
black10: '#0000001a', // 10% black
black20: '#00000033', // 20% black
black30: '#0000004d', // 30% black
black40: '#00000066', // 40% black
black50: '#00000080', // 50% black
black60: '#00000099', // 60% black
black80: '#000000cc', // 80% black
black90: '#000000e6', // 90% black
white10: '#ffffff1a', // 10% white
white20: '#ffffff33', // 20% white
white30: '#ffffff4d', // 30% white
white40: '#ffffff66', // 40% white
white50: '#ffffff80', // 50% white
white60: '#ffffff99', // 60% white
white80: '#ffffffcc', // 80% white
white90: '#ffffffe6', // 90% white
};

View File

@@ -30,7 +30,9 @@ export const addNote = async (note) => {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
isStarred: note.isStarred || false, isStarred: note.isStarred || false,
isTop: note.isTop || false, isTop: note.isTop || false,
hasImage: note.hasImage || false hasImage: note.hasImage || false,
isDeleted: note.isDeleted || false,
deletedAt: note.deletedAt || null
}; };
const notes = await getNotes(); const notes = await getNotes();

View File

@@ -1,574 +0,0 @@
// Styles for Smartisan Notes - Based on React Native version
export default {
// Common styles - Based on Smartisan Notes design principles
container: {
flex: 1,
backgroundColor: 'var(--background)',
},
// Header styles - Warm, minimal design
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: 'var(--background)',
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
},
headerTitleContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
headerTitleTouchable: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: 'var(--text-primary)',
textAlign: 'center',
},
headerFolderArrow: {
width: 20,
height: 20,
tintColor: 'var(--text-primary)',
marginLeft: 8,
},
headerButton: {
padding: 8,
},
headerButtonText: {
fontSize: 16,
color: 'var(--primary)',
fontWeight: '500',
},
headerActionIcon: {
width: 24,
height: 24,
tintColor: 'var(--primary)',
},
// Folder list styles
folderListContainer: {
position: 'absolute',
top: 50,
left: '10%',
right: '10%',
backgroundColor: 'var(--background-card)',
borderRadius: 8,
shadowColor: 'var(--shadow)',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
zIndex: 100,
},
folderListItem: {
paddingVertical: 12,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
folderListItemActive: {
backgroundColor: 'var(--folder-item-selected)',
},
folderListItemText: {
fontSize: 16,
color: 'var(--text-primary)',
},
folderListItemTextActive: {
color: 'var(--primary)',
fontWeight: '500',
},
folderListItemCount: {
fontSize: 13,
color: 'var(--text-tertiary)',
},
// Note list styles - Clean, paper-like appearance
noteListContainer: {
flex: 1,
backgroundColor: 'var(--background)',
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: 'var(--background-card)',
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
},
searchInputContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'var(--background-card)',
height: 36,
paddingHorizontal: 8,
paddingVertical: 0,
},
searchInputBackground: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f0f0f0',
borderRadius: 4,
height: 36,
paddingHorizontal: 8,
paddingVertical: 0,
},
searchInput: {
flex: 1,
fontSize: 16,
color: 'var(--text-primary)',
marginLeft: 8,
marginRight: 8,
padding: 0,
includeFontPadding: false,
},
searchLeftIcon: {
width: 20,
height: 20,
tintColor: 'var(--text-tertiary)',
},
searchClearIcon: {
width: 20,
height: 20,
tintColor: 'var(--text-tertiary)',
},
noteListEmptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 16,
backgroundColor: 'var(--background)',
},
noteListEmptyText: {
fontSize: 18,
fontWeight: '600',
color: 'var(--text-tertiary)',
marginBottom: 8,
},
noteListEmptySubtext: {
fontSize: 14,
color: 'var(--text-tertiary)',
textAlign: 'center',
lineHeight: 20,
},
noteCount: {
fontSize: 13,
color: 'var(--text-tertiary)',
paddingHorizontal: 16,
paddingVertical: 8,
},
// Note item styles - Paper note appearance with subtle shadows
noteItem: {
padding: 0,
borderRadius: 6,
borderLeftWidth: 1,
borderLeftColor: 'transparent',
shadowColor: 'var(--shadow)',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 1,
overflow: 'hidden',
backgroundColor: 'var(--background-card)',
},
noteItemDeleteButton: {
backgroundColor: 'var(--accent-red)',
justifyContent: 'center',
alignItems: 'center',
width: 80,
height: '100%',
borderRadius: 6,
marginBottom: 10,
},
noteItemDeleteButtonImage: {
width: 24,
height: 24,
tintColor: 'var(--text-inverted)',
},
noteItemHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
},
noteItemTitle: {
fontSize: 16,
fontWeight: '600',
color: 'var(--note-title)',
flex: 1,
marginRight: 8,
},
noteItemStar: {
width: 20,
height: 20,
tintColor: 'var(--note-star)',
},
noteItemContent: {
fontSize: 14,
color: 'var(--note-content)',
marginBottom: 8,
lineHeight: 20,
includeFontPadding: false,
},
noteItemDate: {
fontSize: 12,
color: 'var(--note-date)',
includeFontPadding: false,
},
// Floating action button - Circular button with warm color
fab: {
position: 'absolute',
bottom: 24,
right: 24,
backgroundColor: 'var(--primary)',
width: 50,
height: 50,
borderRadius: 25,
justifyContent: 'center',
alignItems: 'center',
shadowColor: 'var(--shadow)',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 3,
elevation: 3,
},
fabIcon: {
width: 24,
height: 24,
tintColor: 'var(--text-inverted)',
},
// Folder item styles - Clean list items with folder icon
folderItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 16,
marginBottom: 1,
borderLeftWidth: 3,
borderLeftColor: 'var(--accent-orange)',
overflow: 'hidden',
backgroundColor: 'var(--background-card)',
},
folderItemIcon: {
width: 24,
height: 24,
tintColor: 'var(--folder-name)',
},
folderItemInfo: {
flex: 1,
marginLeft: 12,
},
folderItemName: {
fontSize: 16,
fontWeight: '500',
color: 'var(--folder-name)',
marginBottom: 2,
},
folderItemCount: {
fontSize: 13,
color: 'var(--folder-count)',
},
folderItemArrow: {
fontSize: 18,
color: 'var(--text-tertiary)',
},
// Note editor styles - Clean writing surface
noteEditorContainer: {
flex: 1,
backgroundColor: 'var(--background-card)',
},
editorToolbar: {
flexDirection: 'row',
paddingVertical: 8,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
backgroundColor: 'var(--background-card)',
},
editorToolbarButton: {
padding: 8,
marginRight: 8,
},
editorToolbarIcon: {
width: 24,
height: 24,
tintColor: 'var(--text-primary)',
},
noteEditorContent: {
flex: 1,
padding: 16,
},
noteEditorTitle: {
fontSize: 22,
fontWeight: '600',
color: 'var(--note-title)',
marginBottom: 16,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
},
noteEditorContentInput: {
fontSize: 16,
color: 'var(--note-content)',
lineHeight: 24,
flex: 1,
textAlignVertical: 'top',
},
// Note detail styles - Clean reading experience
noteDetailContainer: {
flex: 1,
backgroundColor: 'var(--background-card)',
},
noteDetailContent: {
flex: 1,
padding: 16,
},
noteDetailHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
paddingVertical: 4,
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
},
noteDetailDate: {
fontSize: 13,
color: 'var(--note-date)',
},
noteDetailStarIcon: {
width: 24,
height: 24,
tintColor: 'var(--note-star)',
},
noteDetailContentText: {
fontSize: 16,
color: 'var(--note-content)',
lineHeight: 24,
includeFontPadding: false,
},
noteDetailFooter: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 12,
backgroundColor: 'var(--background)',
borderTopWidth: 1,
borderTopColor: 'var(--border)',
},
noteDetailActionButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: 'var(--primary)',
borderRadius: 4,
},
noteDetailActionButtonText: {
color: 'var(--text-inverted)',
fontWeight: '500',
fontSize: 15,
marginLeft: 8,
},
noteDetailActionIcon: {
width: 20,
height: 20,
tintColor: 'var(--text-inverted)',
},
// Settings styles - Clean, organized sections
settingsSection: {
backgroundColor: 'var(--background-card)',
marginBottom: 12,
},
settingsSectionTitle: {
fontSize: 13,
fontWeight: '600',
color: 'var(--text-tertiary)',
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: 'var(--background-secondary)',
},
settingsItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: 'var(--border)',
},
settingsItemWithIcon: {
flexDirection: 'row',
alignItems: 'center',
},
settingsItemIcon: {
width: 20,
height: 20,
tintColor: 'var(--text-primary)',
marginRight: 12,
},
settingsItemText: {
fontSize: 16,
color: 'var(--text-primary)',
},
settingsItemValue: {
fontSize: 15,
color: 'var(--text-tertiary)',
},
// Modal styles - Clean dialogs
modalContainer: {
flex: 1,
backgroundColor: 'var(--black-50)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: 'var(--background-card)',
borderRadius: 8,
padding: 20,
width: '80%',
maxWidth: 300,
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
color: 'var(--text-primary)',
marginBottom: 16,
textAlign: 'center',
},
modalInput: {
borderWidth: 1,
borderColor: 'var(--border)',
borderRadius: 4,
padding: 12,
fontSize: 16,
color: 'var(--text-primary)',
marginBottom: 16,
},
modalButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
},
modalButton: {
flex: 1,
paddingVertical: 12,
alignItems: 'center',
borderRadius: 4,
},
modalButtonCancel: {
backgroundColor: 'var(--background-secondary)',
marginRight: 8,
},
modalButtonConfirm: {
backgroundColor: 'var(--primary)',
marginLeft: 8,
},
modalButtonText: {
fontSize: 16,
fontWeight: '500',
},
modalButtonTextCancel: {
color: 'var(--text-primary)',
},
modalButtonTextConfirm: {
color: 'var(--text-inverted)',
},
// Overlay style for dismissing folder list
overlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'transparent',
zIndex: 99,
},
};

View File

@@ -1,23 +0,0 @@
// Types
export const Note = {
id: String,
title: String,
content: String,
createdAt: Date,
updatedAt: Date,
folderId: String,
isStarred: Boolean,
isTop: Boolean,
hasImage: Boolean
};
export const Folder = {
id: String,
name: String,
createdAt: Date
};
export const Settings = {
cloudSync: Boolean,
darkMode: Boolean
};