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

Reviewed-on: yuantao/SmartisanNote.Remake#15
This commit is contained in:
2025-11-03 15:24:15 +08:00
11 changed files with 2922 additions and 919 deletions

187
README.md
View File

@@ -1,109 +1,130 @@
# 锤子便签(重制版) # Smartisan Note 离线支持说明
这是一个基于 Vue 3、Vite 和 Pinia 的移动端现代化 Web 应用,旨在重现并改进经典的锤子便签应用体验。该项目采用 PWA渐进式 Web 应用)技术,支持离线使用和安装到主屏幕。 ## 离线功能特性
## 主要特性 Smartisan Note 应用支持完整的离线工作模式,确保用户在没有网络连接的情况下也能正常使用所有功能。
- **便签管理**: 创建、编辑、删除、置顶、加星便签 ### 核心离线功能
- **文件夹管理**: 将便签分类到不同的文件夹中
- **搜索功能**: 按标题或内容搜索便签
- **回收站**: 临时存储已删除的便签,支持彻底删除
- **多种排序方式**: 按更新时间、标题、星标状态排序
- **PWA 支持**: 可安装为独立应用,支持离线使用
- **本地存储**: 所有数据存储在浏览器的 IndexedDB 中
- **富文本编辑**: 支持标题、待办事项、列表、加粗、引用、图片等多种格式
- **动画效果**: 使用 Oku Motion 实现流畅的动画效果
- **拖拽排序**: 支持图片拖拽排序
## 技术栈 1. **完整的离线编辑**
- 所有便签的创建、编辑、删除操作都可以在离线状态下完成
- 支持富文本编辑器的所有功能(标题、待办事项、列表、加粗、引用、图片等)
- 便签数据实时保存到本地 IndexedDB 数据库
- **框架**: Vue 3 (Composition API) 2. **智能数据同步**
- **构建工具**: Vite - 网络恢复时自动同步离线期间的操作
- **状态管理**: Pinia - 离线操作队列管理,确保数据一致性
- **路由**: Vue Router - 冲突解决机制(时间戳优先)
- **UI 组件库**: Ionic Vue (部分使用)
- **PWA 支持**: vite-plugin-pwa
- **本地存储**: IndexedDB (通过 `src/utils/indexedDBStorage.js` 封装)
- **CSS 预处理器**: Less
- **动画库**: @oku-ui/motion
- **拖拽库**: vue-draggable-plus
## 项目结构 3. **资源缓存**
- 核心静态资源CSS、JS、图片缓存到浏览器缓存
- 应用界面在离线状态下完全可用
- 字体和图标资源本地缓存
``` 4. **网络状态感知**
. - 实时检测网络连接状态
├── android/ # Capacitor Android 项目文件 - 离线/在线状态切换时的用户提示
├── public/ # 静态资源目录 (图标等) - 网络类型识别2G/3G/4G/WiFi
├── src/ # 源代码目录
│ ├── App.vue # 根组件
│ ├── main.js # 应用入口文件
│ ├── common/ # 通用样式
│ ├── components/ # 可复用的 UI 组件
│ ├── pages/ # 页面组件
│ ├── stores/ # Pinia 状态管理
│ └── utils/ # 工具函数
├── index.html # 应用入口 HTML 文件
├── package.json # 项目依赖和脚本
├── vite.config.js # Vite 配置文件
└── capacitor.config.json # Capacitor 配置文件
```
## 开发与构建 ## 技术实现
### 前置条件 ### Service Worker
确保已安装 Node.js (>=14) 和 npm。 应用使用自定义 Service Worker (`src/sw.js`) 实现以下功能:
### 安装依赖 - **缓存策略**
- 静态资源:缓存优先
```bash - 数据请求:网络优先,失败时回退到缓存
npm install - API 请求:网络优先,失败时返回离线响应
```
- **后台同步**
### 开发 - 支持后台数据同步
- 离线操作队列处理
启动开发服务器:
### IndexedDB 存储
使用 IndexedDB 作为本地数据存储:
- **数据结构**
- 便签存储 (`notes`)
- 文件夹存储 (`folders`)
- 设置存储 (`settings`)
- **离线队列**
- 操作记录到本地存储队列
- 网络恢复时自动处理队列
### 网络状态管理
- 实时网络状态检测
- 网络类型识别
- 在线/离线状态切换处理
## 使用说明
### 离线使用
1. 应用首次加载时会缓存核心资源
2. 断网后应用仍可正常使用所有功能
3. 离线期间的所有操作都会保存到本地
### 数据同步
1. 网络恢复时自动开始同步
2. 可在设置中查看同步状态
3. 支持手动触发同步(未来版本)
## 开发说明
### 构建命令
```bash ```bash
# 开发模式
npm run dev npm run dev
```
这将在 `http://localhost:3000` 启动应用。 # 构建标准版本
### 构建
构建标准 Web 应用:
```bash
npm run build npm run build
```
构建 PWA 应用: # 构建PWA版本(支持离线)
```bash
npm run build:pwa npm run build:pwa
```
构建所有版本 (标准 + PWA): # 构建所有版本
```bash
npm run build:all npm run build:all
```
### 部署 PWA # 部署PWA版本
构建 PWA 并上传到服务器:
```bash
npm run deploy:pwa npm run deploy:pwa
``` ```
这将执行 `vite build --mode pwa` 并运行 `upload-pwa.js` 脚本。 ### 目录结构
### Android 应用
运行 Android 应用:
```bash
npm run android
``` ```
src/
├── sw.js # Service Worker 实现
├── utils/
│ ├── indexedDBStorage.js # IndexedDB 存储管理
│ ├── networkUtils.js # 网络状态工具
│ └── dateUtils.js # 日期工具
├── stores/
│ └── useAppStore.js # 应用状态管理
├── components/
│ └── ... # UI 组件
└── pages/
├── NoteListPage.vue # 便签列表页面
├── NoteEditorPage.vue # 便签编辑页面
└── SettingsPage.vue # 设置页面
```
## 测试离线功能
1. 使用浏览器开发者工具的 Network Throttling 功能模拟离线环境
2. 在离线状态下创建、编辑、删除便签
3. 恢复网络连接,观察数据同步过程
4. 检查离线期间的操作是否正确同步
## 未来改进
1. 添加手动同步按钮
2. 增强冲突解决机制
3. 支持多设备数据同步
4. 添加数据导出/导入功能
5. 增强离线状态下的用户提示

View File

@@ -5,6 +5,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"android": "npx cap run android", "android": "npx cap run android",
"build": "vite build",
"build:pwa": "vite build --mode pwa",
"build:all": "vite build && vite build --mode pwa", "build:all": "vite build && vite build --mode pwa",
"deploy:pwa": "vite build --mode pwa && node upload-pwa.js", "deploy:pwa": "vite build --mode pwa && node upload-pwa.js",
"dev": "vite" "dev": "vite"

View File

@@ -1,5 +1,13 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 网络状态指示器 -->
<div v-if="!isOnline" class="network-status">
<div class="offline-indicator">
<span class="offline-icon"></span>
<span class="offline-text">离线模式</span>
</div>
</div>
<!-- 设置页面背景列表页 --> <!-- 设置页面背景列表页 -->
<NoteListPage v-show="showBackgroundPage" class="background-page" /> <NoteListPage v-show="showBackgroundPage" class="background-page" />
@@ -20,10 +28,11 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, computed, onMounted } from 'vue' import { ref, watch, computed, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import '@/common/base.css' import '@/common/base.css'
import { initModalService } from '@/utils/modalService' import { initModalService } from '@/utils/modalService'
import { addNetworkListener, removeNetworkListener, testOnline } from '@/utils/networkUtils'
// 导入页面组件 // 导入页面组件
import NoteListPage from './pages/NoteListPage.vue' import NoteListPage from './pages/NoteListPage.vue'
@@ -32,6 +41,7 @@ import SettingsPage from './pages/SettingsPage.vue'
const route = useRoute() const route = useRoute()
const transitionName = ref('slide-left') const transitionName = ref('slide-left')
const modalRef = ref() const modalRef = ref()
const isOnline = ref(navigator.onLine)
// 计算是否为设置页面路由 // 计算是否为设置页面路由
const isSettingsRoute = computed(() => { const isSettingsRoute = computed(() => {
@@ -43,6 +53,19 @@ const showBackgroundPage = computed(() => {
return route.path === '/settings' return route.path === '/settings'
}) })
// 网络状态变化回调
const handleOnline = () => {
isOnline.value = true
console.log('网络已连接')
// 可以在这里触发数据同步
}
const handleOffline = () => {
isOnline.value = false
console.log('网络已断开')
// 可以在这里显示离线提示
}
// 监听路由变化,动态设置过渡动画方向 // 监听路由变化,动态设置过渡动画方向
watch( watch(
() => route.path, () => route.path,
@@ -72,9 +95,15 @@ watch(
} }
) )
// 初始化弹框服务 // 初始化弹框服务和网络监听
onMounted(() => { onMounted(() => {
initModalService() initModalService()
addNetworkListener(handleOnline, handleOffline)
})
// 移除网络监听
onUnmounted(() => {
removeNetworkListener(handleOnline, handleOffline)
}) })
</script> </script>
@@ -87,6 +116,38 @@ onMounted(() => {
background-color: #f5f5f5; // 设置默认背景色,防止闪烁 background-color: #f5f5f5; // 设置默认背景色,防止闪烁
} }
// 网络状态指示器
.network-status {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10000;
display: flex;
justify-content: center;
pointer-events: none;
}
.offline-indicator {
background-color: #ff6b6b;
color: white;
padding: 8px 16px;
border-radius: 0 0 4px 4px;
font-size: 14px;
display: flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
pointer-events: auto;
}
.offline-icon {
margin-right: 8px;
}
.offline-text {
font-weight: 500;
}
// 背景页面样式 // 背景页面样式
.background-page { .background-page {
position: absolute; position: absolute;

View File

@@ -1,6 +1,13 @@
<template> <template>
<ion-page> <ion-page>
<div class="container"> <div class="container">
<!-- 离线状态提示 -->
<div v-if="!store.isOnline" class="offline-banner">
<div class="offline-content">
<span class="offline-icon"></span>
<span class="offline-text">离线模式 - 数据将自动同步</span>
</div>
</div>
<!-- 头部编辑模式 --> <!-- 头部编辑模式 -->
<Header v-if="isEditorFocus" :onBack="handleCancel" :onAction="handleAction" actionIcon="edit" /> <Header v-if="isEditorFocus" :onBack="handleCancel" :onAction="handleAction" actionIcon="edit" />
<!-- 头部预览模式 --> <!-- 头部预览模式 -->
@@ -12,7 +19,6 @@
<span>|</span> <span>|</span>
<span class="word-count">{{ wordCount }}</span> <span class="word-count">{{ wordCount }}</span>
</div> </div>
<!-- 富文本编辑器 --> <!-- 富文本编辑器 -->
<div class="note-container" :style="{ height: noteContainerHeight }"> <div class="note-container" :style="{ height: noteContainerHeight }">
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" @focus="handleEditorFocus" @blur="handleEditorBlur" class="rich-text-editor" /> <RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" @focus="handleEditorFocus" @blur="handleEditorBlur" class="rich-text-editor" />
@@ -21,7 +27,6 @@
</div> </div>
</ion-page> </ion-page>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue' import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -30,18 +35,16 @@ import Header from '../components/Header.vue'
import RichTextEditor from '../components/RichTextEditor.vue' import RichTextEditor from '../components/RichTextEditor.vue'
import { formatNoteEditorDate } from '../utils/dateUtils' import { formatNoteEditorDate } from '../utils/dateUtils'
import { IonPage } from '@ionic/vue' import { IonPage } from '@ionic/vue'
import { isOnline } from '../utils/networkUtils'
const props = defineProps({ const props = defineProps({
id: { id: {
type: String, type: String,
default: null, default: null,
}, },
}) })
// 为了保持向后兼容性我们也支持noteId属性 // 为了保持向后兼容性我们也支持noteId属性
// 通过计算属性确保无论使用id还是noteId都能正确获取便签ID // 通过计算属性确保无论使用id还是noteId都能正确获取便签ID
const noteId = computed(() => props.id || props.noteId) const noteId = computed(() => props.id || props.noteId)
const store = useAppStore() const store = useAppStore()
const router = useRouter() const router = useRouter()
const editorRef = ref(null) const editorRef = ref(null)
@@ -49,10 +52,8 @@ const headerRef = ref(null)
const headerInfoRef = ref(null) const headerInfoRef = ref(null)
// 是否聚焦编辑器 // 是否聚焦编辑器
const isEditorFocus = ref(false) const isEditorFocus = ref(false)
// 计算.note-container的高度 // 计算.note-container的高度
const noteContainerHeight = ref('100vh') const noteContainerHeight = ref('100vh')
// 设置便签内容的函数 // 设置便签内容的函数
// 用于在编辑器中加载指定便签的内容 // 用于在编辑器中加载指定便签的内容
const setNoteContent = async noteId => { const setNoteContent = async noteId => {
@@ -60,13 +61,10 @@ const setNoteContent = async noteId => {
if (store.notes.length === 0) { if (store.notes.length === 0) {
await store.loadData() await store.loadData()
} }
// 从store中查找指定ID的便签 // 从store中查找指定ID的便签
const note = store.notes.find(n => n.id === noteId) const note = store.notes.find(n => n.id === noteId)
// 确保编辑器已经初始化完成 // 确保编辑器已经初始化完成
await nextTick() await nextTick()
if (note) { if (note) {
// 无论editorRef是否可用都先设置content的值作为备份 // 无论editorRef是否可用都先设置content的值作为备份
content.value = note.content || '' content.value = note.content || ''
@@ -76,20 +74,16 @@ const setNoteContent = async noteId => {
} }
} }
} }
// 加载初始数据 // 加载初始数据
onMounted(async () => { onMounted(async () => {
await store.loadData() await store.loadData()
// 如果是编辑现有便签,在组件挂载后设置内容 // 如果是编辑现有便签,在组件挂载后设置内容
if (noteId.value) { if (noteId.value) {
await setNoteContent(noteId.value) await setNoteContent(noteId.value)
} }
// 等待DOM更新后计算.note-container的高度 // 等待DOM更新后计算.note-container的高度
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
// 监听noteId变化确保在编辑器准备好后设置内容 // 监听noteId变化确保在编辑器准备好后设置内容
watch( watch(
noteId, noteId,
@@ -102,7 +96,6 @@ watch(
}, },
{ immediate: true } { immediate: true }
) )
// 监听store变化确保在store加载后设置内容 // 监听store变化确保在store加载后设置内容
watch( watch(
() => store.notes, () => store.notes,
@@ -115,15 +108,12 @@ watch(
}, },
{ immediate: true } { immediate: true }
) )
// 检查是否正在编辑现有便签 // 检查是否正在编辑现有便签
// 如果noteId存在则表示是编辑模式否则是新建模式 // 如果noteId存在则表示是编辑模式否则是新建模式
const isEditing = !!noteId.value const isEditing = !!noteId.value
const existingNote = isEditing ? store.notes.find(n => n.id === noteId.value) : null const existingNote = isEditing ? store.notes.find(n => n.id === noteId.value) : null
// 初始化内容状态,如果是编辑现有便签则使用便签内容,否则为空字符串 // 初始化内容状态,如果是编辑现有便签则使用便签内容,否则为空字符串
const content = ref(existingNote?.content || '') const content = ref(existingNote?.content || '')
// 当组件挂载时,确保编辑器初始化为空内容(针对新建便签) // 当组件挂载时,确保编辑器初始化为空内容(针对新建便签)
onMounted(() => { onMounted(() => {
if (!isEditing && editorRef.value) { if (!isEditing && editorRef.value) {
@@ -131,7 +121,6 @@ onMounted(() => {
editorRef.value.setContent('') editorRef.value.setContent('')
} }
}) })
// 监听store变化确保在store加载后设置内容 // 监听store变化确保在store加载后设置内容
watch( watch(
() => store.notes, () => store.notes,
@@ -145,7 +134,6 @@ watch(
{ immediate: true } { immediate: true }
) )
const showAlert = ref(false) const showAlert = ref(false)
// 防抖函数 // 防抖函数
// 用于避免函数在短时间内被频繁调用,提高性能 // 用于避免函数在短时间内被频繁调用,提高性能
const debounce = (func, delay) => { const debounce = (func, delay) => {
@@ -155,19 +143,16 @@ const debounce = (func, delay) => {
timeoutId = setTimeout(() => func.apply(this, args), delay) timeoutId = setTimeout(() => func.apply(this, args), delay)
} }
} }
// 防抖处理内容变化 // 防抖处理内容变化
// 延迟300ms更新内容避免用户输入时频繁触发更新 // 延迟300ms更新内容避免用户输入时频繁触发更新
const debouncedHandleContentChange = debounce(newContent => { const debouncedHandleContentChange = debounce(newContent => {
content.value = newContent content.value = newContent
}, 300) }, 300)
// 监听编辑器内容变化 // 监听编辑器内容变化
// 当编辑器内容发生变化时调用此函数 // 当编辑器内容发生变化时调用此函数
const handleContentChange = newContent => { const handleContentChange = newContent => {
debouncedHandleContentChange(newContent) debouncedHandleContentChange(newContent)
} }
// 计算属性 - 格式化时间显示 // 计算属性 - 格式化时间显示
// 如果是编辑现有便签则显示便签的更新时间,否则显示当前时间 // 如果是编辑现有便签则显示便签的更新时间,否则显示当前时间
const formattedTime = computed(() => { const formattedTime = computed(() => {
@@ -176,7 +161,6 @@ const formattedTime = computed(() => {
} }
return formatNoteEditorDate(new Date()) return formatNoteEditorDate(new Date())
}) })
// 计算属性 - 计算字数 // 计算属性 - 计算字数
// 移除HTML标签后计算纯文本字数 // 移除HTML标签后计算纯文本字数
const wordCount = computed(() => { const wordCount = computed(() => {
@@ -184,13 +168,11 @@ const wordCount = computed(() => {
const textContent = content.value.replace(/<[^>]*>/g, '') const textContent = content.value.replace(/<[^>]*>/g, '')
return textContent.length || 0 return textContent.length || 0
}) })
// 处理保存 // 处理保存
const handleSave = async () => { const handleSave = async () => {
try { try {
// 获取编辑器中的实际内容 // 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
// 检查内容是否为空 // 检查内容是否为空
if (isContentEmpty(editorContent)) { if (isContentEmpty(editorContent)) {
// 如果是编辑模式且内容为空,则删除便签 // 如果是编辑模式且内容为空,则删除便签
@@ -215,7 +197,6 @@ const handleSave = async () => {
}) })
console.log('新便签已创建') console.log('新便签已创建')
} }
// 保存后切换到预览模式(失去编辑器焦点) // 保存后切换到预览模式(失去编辑器焦点)
isEditorFocus.value = false isEditorFocus.value = false
} catch (error) { } catch (error) {
@@ -223,35 +204,28 @@ const handleSave = async () => {
console.log('Save error: Failed to save note. Please try again.') console.log('Save error: Failed to save note. Please try again.')
} }
} }
// 检查内容是否为空(无实质性内容) // 检查内容是否为空(无实质性内容)
const isContentEmpty = content => { const isContentEmpty = content => {
if (!content) return true if (!content) return true
// 检查是否包含图片元素 // 检查是否包含图片元素
const hasImages = /<img[^>]*>|<div[^>]*class="[^"]*editor-image[^"]*"[^>]*>/.test(content) const hasImages = /<img[^>]*>|<div[^>]*class="[^"]*editor-image[^"]*"[^>]*>/.test(content)
if (hasImages) return false if (hasImages) return false
// 移除HTML标签和空白字符后检查是否为空 // 移除HTML标签和空白字符后检查是否为空
const plainText = content.replace(/<[^>]*>/g, '').trim() const plainText = content.replace(/<[^>]*>/g, '').trim()
if (plainText === '') return true if (plainText === '') return true
// 检查是否只有空的HTML元素 // 检查是否只有空的HTML元素
const strippedContent = content const strippedContent = content
.replace(/\s+/g, '') .replace(/\s+/g, '')
.replace(/<br>/g, '') .replace(/<br>/g, '')
.replace(/<br\/>/g, '') .replace(/<br\/>/g, '')
if (strippedContent === '<p></p>' || strippedContent === '<div></div>' || strippedContent === '') return true if (strippedContent === '<p></p>' || strippedContent === '<div></div>' || strippedContent === '') return true
return false return false
} }
// 自动保存便签(仅在非主动保存操作时调用) // 自动保存便签(仅在非主动保存操作时调用)
const autoSaveNote = async () => { const autoSaveNote = async () => {
try { try {
// 获取编辑器中的实际内容 // 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
if (isEditing && existingNote) { if (isEditing && existingNote) {
// 检查内容是否为空,如果为空则删除便签 // 检查内容是否为空,如果为空则删除便签
if (isContentEmpty(editorContent)) { if (isContentEmpty(editorContent)) {
@@ -281,22 +255,18 @@ const autoSaveNote = async () => {
console.error('自动保存失败:', error) console.error('自动保存失败:', error)
} }
} }
// 处理取消 // 处理取消
const handleCancel = async () => { const handleCancel = async () => {
// 自动保存便签 // 自动保存便签
await autoSaveNote() await autoSaveNote()
// 直接导航回便签列表页面,因为已经处理了保存或删除逻辑 // 直接导航回便签列表页面,因为已经处理了保存或删除逻辑
router.push('/notes') router.push('/notes')
} }
// 处理创建(用于新建便签) // 处理创建(用于新建便签)
const handleCreate = async () => { const handleCreate = async () => {
try { try {
// 获取编辑器中的实际内容 // 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
// 只有当有内容时才创建新便签 // 只有当有内容时才创建新便签
if (!isContentEmpty(editorContent)) { if (!isContentEmpty(editorContent)) {
await store.addNote({ await store.addNote({
@@ -305,7 +275,6 @@ const handleCreate = async () => {
}) })
console.log('新便签已创建') console.log('新便签已创建')
} }
// 创建后切换到预览模式(失去编辑器焦点) // 创建后切换到预览模式(失去编辑器焦点)
isEditorFocus.value = false isEditorFocus.value = false
} catch (error) { } catch (error) {
@@ -313,7 +282,6 @@ const handleCreate = async () => {
console.log('Create error: Failed to create note. Please try again.') console.log('Create error: Failed to create note. Please try again.')
} }
} }
// 处理Header组件的操作按钮点击事件 // 处理Header组件的操作按钮点击事件
const handleAction = actionType => { const handleAction = actionType => {
if (actionType === 'save') { if (actionType === 'save') {
@@ -334,11 +302,9 @@ const handleAction = actionType => {
handleShare() handleShare()
} }
} }
const setShowAlert = value => { const setShowAlert = value => {
showAlert.value = value showAlert.value = value
} }
// 处理编辑器获得焦点 // 处理编辑器获得焦点
const handleEditorFocus = () => { const handleEditorFocus = () => {
isEditorFocus.value = true isEditorFocus.value = true
@@ -347,7 +313,6 @@ const handleEditorFocus = () => {
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
} }
// 处理编辑器失去焦点 // 处理编辑器失去焦点
const handleEditorBlur = () => { const handleEditorBlur = () => {
isEditorFocus.value = false isEditorFocus.value = false
@@ -356,7 +321,6 @@ const handleEditorBlur = () => {
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
} }
// 处理删除便签 // 处理删除便签
const handleDelete = async () => { const handleDelete = async () => {
if (isEditing && existingNote) { if (isEditing && existingNote) {
@@ -364,7 +328,6 @@ const handleDelete = async () => {
if (headerRef.value && headerRef.value.playDeleteAnimation) { if (headerRef.value && headerRef.value.playDeleteAnimation) {
headerRef.value.playDeleteAnimation() headerRef.value.playDeleteAnimation()
} }
// 等待动画播放完成后再执行删除操作 // 等待动画播放完成后再执行删除操作
// 15帧 * 50ms = 750ms再加上一些缓冲时间 // 15帧 * 50ms = 750ms再加上一些缓冲时间
setTimeout(async () => { setTimeout(async () => {
@@ -380,15 +343,12 @@ const handleDelete = async () => {
}, 800) // 等待约800ms让动画播放完成 }, 800) // 等待约800ms让动画播放完成
} }
} }
// 处理分享便签 // 处理分享便签
const handleShare = () => { const handleShare = () => {
// 获取编辑器中的实际内容 // 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
// 移除HTML标签获取纯文本内容用于分享 // 移除HTML标签获取纯文本内容用于分享
const plainText = editorContent.replace(/<[^>]*>/g, '').trim() const plainText = editorContent.replace(/<[^>]*>/g, '').trim()
if (plainText) { if (plainText) {
// 在实际应用中,这里会调用设备的分享功能 // 在实际应用中,这里会调用设备的分享功能
// 为了演示我们使用Web Share API如果支持 // 为了演示我们使用Web Share API如果支持
@@ -415,13 +375,11 @@ const handleShare = () => {
} }
} }
} }
// 计算.note-container的高度 // 计算.note-container的高度
const calculateNoteContainerHeight = () => { const calculateNoteContainerHeight = () => {
nextTick(() => { nextTick(() => {
let headerHeight = 0 let headerHeight = 0
let headerInfoHeight = 0 let headerInfoHeight = 0
// 获取Header组件的高度 // 获取Header组件的高度
if (headerRef.value?.$el) { if (headerRef.value?.$el) {
headerHeight = headerRef.value.$el.offsetHeight || 0 headerHeight = headerRef.value.$el.offsetHeight || 0
@@ -430,7 +388,6 @@ const calculateNoteContainerHeight = () => {
const headerElement = document.querySelector('.component') const headerElement = document.querySelector('.component')
headerHeight = headerElement ? headerElement.offsetHeight : 100 // 默认高度 headerHeight = headerElement ? headerElement.offsetHeight : 100 // 默认高度
} }
// 获取.header-info的高度 // 获取.header-info的高度
if (headerInfoRef.value) { if (headerInfoRef.value) {
headerInfoHeight = headerInfoRef.value.offsetHeight || 0 headerInfoHeight = headerInfoRef.value.offsetHeight || 0
@@ -439,42 +396,35 @@ const calculateNoteContainerHeight = () => {
const headerInfoElement = document.querySelector('.header-info') const headerInfoElement = document.querySelector('.header-info')
headerInfoHeight = headerInfoElement ? headerInfoElement.offsetHeight : 40 // 默认高度 headerInfoHeight = headerInfoElement ? headerInfoElement.offsetHeight : 40 // 默认高度
} }
// 计算剩余高度 (屏幕高度 - Header高度 - HeaderInfo高度) // 计算剩余高度 (屏幕高度 - Header高度 - HeaderInfo高度)
const totalHeaderHeight = headerHeight + headerInfoHeight const totalHeaderHeight = headerHeight + headerInfoHeight
const screenHeight = window.innerHeight const screenHeight = window.innerHeight
const newHeight = screenHeight - totalHeaderHeight const newHeight = screenHeight - totalHeaderHeight
// 设置.note-container的高度 // 设置.note-container的高度
noteContainerHeight.value = `${newHeight}px` noteContainerHeight.value = `${newHeight}px`
}) })
} }
// 在组件挂载后和内容变化时重新计算高度 // 在组件挂载后和内容变化时重新计算高度
onMounted(() => { onMounted(() => {
// 等待DOM更新后再计算高度 // 等待DOM更新后再计算高度
nextTick(() => { nextTick(() => {
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
// 监听窗口大小变化事件 // 监听窗口大小变化事件
window.addEventListener('resize', calculateNoteContainerHeight) window.addEventListener('resize', calculateNoteContainerHeight)
}) })
// 监听编辑器焦点变化,重新计算高度 // 监听编辑器焦点变化,重新计算高度
watch(isEditorFocus, () => { watch(isEditorFocus, () => {
nextTick(() => { nextTick(() => {
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
}) })
// 监听内容变化重新计算高度当内容变化可能导致header-info高度变化时 // 监听内容变化重新计算高度当内容变化可能导致header-info高度变化时
watch(content, () => { watch(content, () => {
nextTick(() => { nextTick(() => {
calculateNoteContainerHeight() calculateNoteContainerHeight()
}) })
}) })
// 在组件卸载前自动保存或删除 // 在组件卸载前自动保存或删除
onBeforeUnmount(async () => { onBeforeUnmount(async () => {
await autoSaveNote() await autoSaveNote()
@@ -482,7 +432,6 @@ onBeforeUnmount(async () => {
window.removeEventListener('resize', calculateNoteContainerHeight) window.removeEventListener('resize', calculateNoteContainerHeight)
}) })
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.container { .container {
display: flex; display: flex;
@@ -494,17 +443,36 @@ onBeforeUnmount(async () => {
height: 100%; height: 100%;
} }
} }
.offline-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10000;
background-color: #ff6b6b;
color: white;
padding: 8px 16px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.offline-content {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
}
.offline-icon {
margin-right: 8px;
}
.note-container { .note-container {
flex: 1; flex: 1;
overflow-y: scroll; overflow-y: scroll;
background-color: var(--background); background-color: var(--background);
} }
.rich-text-editor { .rich-text-editor {
min-height: 100%; min-height: 100%;
} }
.header-info { .header-info {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@@ -1,6 +1,14 @@
<template> <template>
<ion-page> <ion-page>
<div class="container"> <div class="container">
<!-- 离线状态提示 -->
<div v-if="!store.isOnline" class="offline-banner">
<div class="offline-content">
<span class="offline-icon"></span>
<span class="offline-text">离线模式 - 数据将自动同步</span>
</div>
</div>
<ion-content class="content"> <ion-content class="content">
<Header <Header
:title="headerTitle" :title="headerTitle"
@@ -62,7 +70,7 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAppStore } from '../stores/useAppStore' import { useAppStore } from '../stores/useAppStore'
import NoteItem from '../components/NoteItem.vue' import NoteItem from '../components/NoteItem.vue'
@@ -379,6 +387,31 @@ const notes = computed(() => store.notes)
background-size: cover; background-size: cover;
} }
.offline-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10000;
background-color: #ff6b6b;
color: white;
padding: 8px 16px;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.offline-content {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 500;
}
.offline-icon {
margin-right: 8px;
}
.folder-list { .folder-list {
position: absolute; position: absolute;
top: 3.125rem; top: 3.125rem;

View File

@@ -15,6 +15,14 @@
<div class="item-text-primary">云同步</div> <div class="item-text-primary">云同步</div>
<SwitchButton :model-value="settings.cloudSync" @update:model-value="toggleCloudSync" /> <SwitchButton :model-value="settings.cloudSync" @update:model-value="toggleCloudSync" />
</div> </div>
<div class="settings-item settings-item-border">
<div class="item-text-primary">离线数据同步</div>
<div class="item-text-tertiary">{{ syncStatusText }}</div>
</div>
<div v-if="store.lastSyncTime" class="settings-item">
<div class="item-text-primary">最后同步时间</div>
<div class="item-text-tertiary">{{ formatLastSyncTime }}</div>
</div>
</SettingGroup> </SettingGroup>
<SettingGroup title="关于"> <SettingGroup title="关于">
@@ -57,6 +65,26 @@ const toggleCloudSync = value => {
store.toggleCloudSync() store.toggleCloudSync()
} }
// 同步状态文本
const syncStatusText = computed(() => {
switch (store.syncStatus) {
case 'syncing':
return '正在同步...'
case 'error':
return '同步失败'
default:
return store.isOnline ? '已连接' : '离线模式'
}
})
// 格式化最后同步时间
const formatLastSyncTime = computed(() => {
if (!store.lastSyncTime) return '从未同步'
const date = new Date(store.lastSyncTime)
return date.toLocaleString()
})
// 处理登录云同步按钮点击事件 // 处理登录云同步按钮点击事件
// 在完整实现中,这里会打开登录界面 // 在完整实现中,这里会打开登录界面
const handleLogin = () => { const handleLogin = () => {

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as storage from '../utils/indexedDBStorage' import * as storage from '../utils/indexedDBStorage'
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils' import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
import { isOnline, testOnline } from '../utils/networkUtils'
/** /**
* 应用状态管理Store * 应用状态管理Store
* 使用Pinia进行状态管理包含便签、文件夹和设置数据 * 使用Pinia进行状态管理包含便签、文件夹和设置数据
@@ -15,8 +15,10 @@ export const useAppStore = defineStore('app', {
notes: [], // 便签列表 notes: [], // 便签列表
folders: [], // 文件夹列表 folders: [], // 文件夹列表
settings: { cloudSync: false, darkMode: false }, // 应用设置 settings: { cloudSync: false, darkMode: false }, // 应用设置
isOnline: navigator.onLine, // 网络状态
syncStatus: 'idle', // 同步状态idle, syncing, error
lastSyncTime: null, // 最后同步时间
}), }),
/** /**
* 计算属性 * 计算属性
* 基于状态派生的计算值 * 基于状态派生的计算值
@@ -30,7 +32,6 @@ export const useAppStore = defineStore('app', {
starredNotesCount: state => { starredNotesCount: state => {
return state.notes.filter(note => note.isStarred).length return state.notes.filter(note => note.isStarred).length
}, },
/** /**
* 计算所有便签数量 * 计算所有便签数量
* @param {Object} state - 当前状态对象 * @param {Object} state - 当前状态对象
@@ -40,7 +41,6 @@ export const useAppStore = defineStore('app', {
return state.notes.length return state.notes.length
}, },
}, },
/** /**
* 状态变更操作 * 状态变更操作
* 包含所有修改状态的方法 * 包含所有修改状态的方法
@@ -58,7 +58,6 @@ export const useAppStore = defineStore('app', {
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()
@@ -68,11 +67,50 @@ export const useAppStore = defineStore('app', {
this.folders = loadedFolders this.folders = loadedFolders
this.settings = loadedSettings this.settings = loadedSettings
} }
// 添加网络状态监听
this.addNetworkListeners()
} catch (error) { } catch (error) {
console.error('Error loading data:', error) console.error('Error loading data:', error)
} }
}, },
/**
* 添加网络状态监听器
*/
addNetworkListeners() {
const handleOnline = () => {
this.isOnline = true
console.log('网络已连接')
// 网络连接时尝试同步数据
this.syncOfflineData()
}
const handleOffline = () => {
this.isOnline = false
console.log('网络已断开')
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
},
/**
* 同步离线数据
* 当网络连接恢复时同步离线期间的操作
*/
async syncOfflineData() {
if (!this.settings.cloudSync) {
return
}
this.syncStatus = 'syncing'
try {
// 这里应该实现与云服务同步的逻辑
// 暂时使用模拟同步
await new Promise(resolve => setTimeout(resolve, 1000))
this.syncStatus = 'idle'
this.lastSyncTime = new Date().toISOString()
console.log('离线数据同步完成')
} catch (error) {
console.error('离线数据同步失败:', error)
this.syncStatus = 'error'
}
},
/** /**
* 加载预设的mock数据 * 加载预设的mock数据
* 用于开发和演示目的,提供示例便签、文件夹和设置 * 用于开发和演示目的,提供示例便签、文件夹和设置
@@ -80,14 +118,14 @@ export const useAppStore = defineStore('app', {
*/ */
async loadMockData() { async loadMockData() {
// Mock notes - 使用固定的日期值以避免每次运行时变化 // Mock notes - 使用固定的日期值以避免每次运行时变化
const fixedCurrentDate = '2025-10-12T10:00:00.000Z'; const fixedCurrentDate = '2025-10-12T10:00:00.000Z'
// 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签 // 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签
const mockNotes = [ const mockNotes = [
{ {
id: '1', id: '1',
title: '欢迎使用锤子便签', title: '欢迎使用锤子便签',
content: '<p>这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。</p><br><h2>功能介绍</h2><p>1. 创建和编辑便签<br>2. 为便签加星和置顶<br>3. 将便签分类到文件夹<br>4. 搜索便签内容<br>5. 回收站功能</p><br><h2>编辑器功能演示</h2><br><h2>标题格式</h2><p>点击标题按钮可创建居中的标题</p><br><h2>待办事项</h2><div class="todo-container"><div class="todo-icon"></div><div contenteditable="true" class="todo-content">这是一个待办事项</div></div><div class="todo-container"><div class="todo-icon completed"></div><div contenteditable="true" class="todo-content" style="color: var(--text-tertiary); text-decoration: line-through;">这是一个已完成的待办事项</div></div><br><h2>列表格式</h2><ul><li>无序列表项1</li><li>无序列表项2</li></ul><br><h2>文本格式</h2><p><strong>加粗文本</strong></p><br><h2>引用格式</h2><div class="quote-container"><div class="quote-icon"></div><div class="quote-content">这是一段引用文本<br>可以用来引用他人的话语</div></div><br><h2>图片</h2><p>点击图片按钮可以插入图片,长按图片可以拖拽排序</p>', content:
'<p>这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。</p><br><h2>功能介绍</h2><p>1. 创建和编辑便签<br>2. 为便签加星和置顶<br>3. 将便签分类到文件夹<br>4. 搜索便签内容<br>5. 回收站功能</p><br><h2>编辑器功能演示</h2><br><h2>标题格式</h2><p>点击标题按钮可创建居中的标题</p><br><h2>待办事项</h2><div class="todo-container"><div class="todo-icon"></div><div contenteditable="true" class="todo-content">这是一个待办事项</div></div><div class="todo-container"><div class="todo-icon completed"></div><div contenteditable="true" class="todo-content" style="color: var(--text-tertiary); text-decoration: line-through;">这是一个已完成的待办事项</div></div><br><h2>列表格式</h2><ul><li>无序列表项1</li><li>无序列表项2</li></ul><br><h2>文本格式</h2><p><strong>加粗文本</strong></p><br><h2>引用格式</h2><div class="quote-container"><div class="quote-icon"></div><div class="quote-content">这是一段引用文本<br>可以用来引用他人的话语</div></div><br><h2>图片</h2><p>点击图片按钮可以插入图片,长按图片可以拖拽排序</p>',
createdAt: fixedCurrentDate, createdAt: fixedCurrentDate,
updatedAt: fixedCurrentDate, updatedAt: fixedCurrentDate,
folderId: null, folderId: null,
@@ -96,31 +134,26 @@ export const useAppStore = defineStore('app', {
hasImage: true, // 包含图片 hasImage: true, // 包含图片
isDeleted: false, // 未删除 isDeleted: false, // 未删除
deletedAt: null, deletedAt: null,
} },
] ]
// Mock folders - 使用固定的日期值 // Mock folders - 使用固定的日期值
// 预设的文件夹示例数据 // 预设的文件夹示例数据
const mockFolders = [] const mockFolders = []
// Mock settings // Mock settings
// 预设的设置示例数据 // 预设的设置示例数据
const mockSettings = { const mockSettings = {
cloudSync: false, // 云同步关闭 cloudSync: false, // 云同步关闭
darkMode: false, // 深色模式关闭 darkMode: false, // 深色模式关闭
} }
// 更新store状态 // 更新store状态
this.notes = mockNotes this.notes = mockNotes
this.folders = mockFolders this.folders = mockFolders
this.settings = mockSettings this.settings = mockSettings
// 保存到Storage // 保存到Storage
await storage.saveNotes(mockNotes) await storage.saveNotes(mockNotes)
await storage.saveFolders(mockFolders) await storage.saveFolders(mockFolders)
await storage.saveSettings(mockSettings) await storage.saveSettings(mockSettings)
}, },
/** /**
* 保存便签数据到Storage * 保存便签数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
@@ -132,7 +165,6 @@ export const useAppStore = defineStore('app', {
console.error('Error saving notes:', error) console.error('Error saving notes:', error)
} }
}, },
/** /**
* 保存文件夹数据到Storage * 保存文件夹数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
@@ -144,7 +176,6 @@ export const useAppStore = defineStore('app', {
console.error('Error saving folders:', error) console.error('Error saving folders:', error)
} }
}, },
/** /**
* 保存设置数据到Storage * 保存设置数据到Storage
* @returns {Promise<void>} * @returns {Promise<void>}
@@ -156,11 +187,9 @@ export const useAppStore = defineStore('app', {
console.error('Error saving settings:', error) console.error('Error saving settings:', error)
} }
}, },
/** /**
* 便签操作函数 * 便签操作函数
*/ */
/** /**
* 添加新便签 * 添加新便签
* @param {Object} note - 便签对象 * @param {Object} note - 便签对象
@@ -170,14 +199,12 @@ export const useAppStore = defineStore('app', {
try { try {
const newNote = await storage.addNote(note) const newNote = await storage.addNote(note)
this.notes.push(newNote) this.notes.push(newNote)
return newNote return newNote
} catch (error) { } catch (error) {
console.error('Error adding note:', error) console.error('Error adding note:', error)
throw error throw error
} }
}, },
/** /**
* 更新便签 * 更新便签
* @param {string} id - 便签ID * @param {string} id - 便签ID
@@ -199,7 +226,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 删除便签 * 删除便签
* @param {string} id - 要删除的便签ID * @param {string} id - 要删除的便签ID
@@ -217,7 +243,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 将便签移至回收站 * 将便签移至回收站
* 将便签标记为已删除状态,并记录删除时间 * 将便签标记为已删除状态,并记录删除时间
@@ -239,7 +264,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 永久删除便签 * 永久删除便签
* 从便签列表中彻底移除便签 * 从便签列表中彻底移除便签
@@ -258,11 +282,9 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 文件夹操作函数 * 文件夹操作函数
*/ */
/** /**
* 添加新文件夹 * 添加新文件夹
* @param {Object} folder - 文件夹对象 * @param {Object} folder - 文件夹对象
@@ -278,7 +300,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 更新文件夹 * 更新文件夹
* @param {string} id - 文件夹ID * @param {string} id - 文件夹ID
@@ -300,7 +321,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 删除文件夹 * 删除文件夹
* @param {string} id - 要删除的文件夹ID * @param {string} id - 要删除的文件夹ID
@@ -315,7 +335,6 @@ export const useAppStore = defineStore('app', {
for (const note of notesInFolder) { for (const note of notesInFolder) {
await this.updateNote(note.id, { folderId: null }) await this.updateNote(note.id, { folderId: null })
} }
// 从文件夹列表中移除文件夹 // 从文件夹列表中移除文件夹
this.folders = this.folders.filter(folder => folder.id !== id) this.folders = this.folders.filter(folder => folder.id !== id)
} }
@@ -325,11 +344,9 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 设置操作函数 * 设置操作函数
*/ */
/** /**
* 更新设置 * 更新设置
* @param {Object} newSettings - 新的设置对象 * @param {Object} newSettings - 新的设置对象
@@ -345,7 +362,6 @@ export const useAppStore = defineStore('app', {
throw error throw error
} }
}, },
/** /**
* 切换云同步设置 * 切换云同步设置
* 开启或关闭云同步功能 * 开启或关闭云同步功能
@@ -354,7 +370,6 @@ export const useAppStore = defineStore('app', {
async toggleCloudSync() { async toggleCloudSync() {
await this.updateSettings({ cloudSync: !this.settings.cloudSync }) await this.updateSettings({ cloudSync: !this.settings.cloudSync })
}, },
/** /**
* 切换深色模式设置 * 切换深色模式设置
* 开启或关闭深色模式 * 开启或关闭深色模式

1690
src/sw.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
import { getCurrentDateTime, getTimestamp } from './dateUtils' import { getCurrentDateTime, getTimestamp } from './dateUtils'
import { OfflineQueue } from './networkUtils'
// 数据库配置 // 数据库配置
const DB_NAME = 'SmartisanNoteDB' const DB_NAME = 'SmartisanNoteDB'
const DB_VERSION = 2 // 更新版本号以确保数据库重新创建 const DB_VERSION = 2 // 更新版本号以确保数据库重新创建
const NOTES_STORE = 'notes' const NOTES_STORE = 'notes'
const FOLDERS_STORE = 'folders' const FOLDERS_STORE = 'folders'
const SETTINGS_STORE = 'settings' const SETTINGS_STORE = 'settings'
let db = null let db = null
// 创建离线队列实例
const offlineQueue = new OfflineQueue()
/** /**
* 打开数据库连接 * 打开数据库连接
* @returns {Promise<IDBDatabase>} 数据库实例 * @returns {Promise<IDBDatabase>} 数据库实例
@@ -18,21 +18,16 @@ const openDB = () => {
if (db) { if (db) {
return resolve(db) return resolve(db)
} }
const request = indexedDB.open(DB_NAME, DB_VERSION) const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onerror = () => { request.onerror = () => {
reject(new Error('无法打开数据库')) reject(new Error('无法打开数据库'))
} }
request.onsuccess = () => { request.onsuccess = () => {
db = request.result db = request.result
resolve(db) resolve(db)
} }
request.onupgradeneeded = event => {
request.onupgradeneeded = (event) => {
const database = event.target.result const database = event.target.result
// 删除现有的对象存储(如果版本已更改) // 删除现有的对象存储(如果版本已更改)
if (event.oldVersion > 0) { if (event.oldVersion > 0) {
if (database.objectStoreNames.contains(NOTES_STORE)) { if (database.objectStoreNames.contains(NOTES_STORE)) {
@@ -45,7 +40,6 @@ const openDB = () => {
database.deleteObjectStore(SETTINGS_STORE) database.deleteObjectStore(SETTINGS_STORE)
} }
} }
// 创建便签存储对象 // 创建便签存储对象
const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' }) const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' })
notesStore.createIndex('folderId', 'folderId', { unique: false }) notesStore.createIndex('folderId', 'folderId', { unique: false })
@@ -53,38 +47,32 @@ const openDB = () => {
notesStore.createIndex('isDeleted', 'isDeleted', { unique: false }) notesStore.createIndex('isDeleted', 'isDeleted', { unique: false })
notesStore.createIndex('createdAt', 'createdAt', { unique: false }) notesStore.createIndex('createdAt', 'createdAt', { unique: false })
notesStore.createIndex('updatedAt', 'updatedAt', { unique: false }) notesStore.createIndex('updatedAt', 'updatedAt', { unique: false })
// 创建文件夹存储对象 // 创建文件夹存储对象
database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' }) database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' })
// 创建设置存储对象 // 创建设置存储对象
database.createObjectStore(SETTINGS_STORE) database.createObjectStore(SETTINGS_STORE)
} }
}) })
} }
/** /**
* 从存储中获取数据 * 从存储中获取数据
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
* @returns {Promise<Array>} 数据数组 * @returns {Promise<Array>} 数据数组
*/ */
const getAllFromStore = async (storeName) => { const getAllFromStore = async storeName => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readonly') const transaction = database.transaction([storeName], 'readonly')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.getAll() const request = store.getAll()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(request.result || []) resolve(request.result || [])
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`获取 ${storeName} 数据失败`)) reject(new Error(`获取 ${storeName} 数据失败`))
} }
}) })
} }
/** /**
* 保存数据到存储 * 保存数据到存储
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
@@ -95,14 +83,12 @@ const saveToStore = async (storeName, data) => {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
// 清除现有数据 // 清除现有数据
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const clearRequest = store.clear() const clearRequest = store.clear()
clearRequest.onsuccess = () => resolve() clearRequest.onsuccess = () => resolve()
clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`)) clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`))
}) })
// 添加新数据 // 添加新数据
for (const item of data) { for (const item of data) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -112,7 +98,6 @@ const saveToStore = async (storeName, data) => {
}) })
} }
} }
/** /**
* 从存储中获取单个项 * 从存储中获取单个项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
@@ -124,18 +109,15 @@ const getFromStore = async (storeName, id) => {
const transaction = database.transaction([storeName], 'readonly') const transaction = database.transaction([storeName], 'readonly')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.get(id) const request = store.get(id)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(request.result || null) resolve(request.result || null)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`获取 ${storeName} 项失败`)) reject(new Error(`获取 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 向存储中添加项 * 向存储中添加项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
@@ -147,18 +129,15 @@ const addToStore = async (storeName, item) => {
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.add(item) const request = store.add(item)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(item) resolve(item)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`添加 ${storeName} 项失败`)) reject(new Error(`添加 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 更新存储中的项 * 更新存储中的项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
@@ -169,24 +148,20 @@ const addToStore = async (storeName, item) => {
const updateInStore = async (storeName, id, updates) => { const updateInStore = async (storeName, id, updates) => {
const item = await getFromStore(storeName, id) const item = await getFromStore(storeName, id)
if (!item) return null if (!item) return null
const updatedItem = { ...item, ...updates } const updatedItem = { ...item, ...updates }
const database = await openDB() const database = await openDB()
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.put(updatedItem) const request = store.put(updatedItem)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(updatedItem) resolve(updatedItem)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`更新 ${storeName} 项失败`)) reject(new Error(`更新 ${storeName} 项失败`))
} }
}) })
} }
/** /**
* 从存储中删除项 * 从存储中删除项
* @param {string} storeName - 存储名称 * @param {string} storeName - 存储名称
@@ -198,21 +173,17 @@ const deleteFromStore = async (storeName, id) => {
const transaction = database.transaction([storeName], 'readwrite') const transaction = database.transaction([storeName], 'readwrite')
const store = transaction.objectStore(storeName) const store = transaction.objectStore(storeName)
const request = store.delete(id) const request = store.delete(id)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(true) resolve(true)
} }
request.onerror = () => { request.onerror = () => {
reject(new Error(`删除 ${storeName} 项失败`)) reject(new Error(`删除 ${storeName} 项失败`))
} }
}) })
} }
// 便签操作函数 // 便签操作函数
// 提供便签的增删改查功能 // 提供便签的增删改查功能
/** /**
* 获取所有便签数据 * 获取所有便签数据
* 从IndexedDB中读取便签数据 * 从IndexedDB中读取便签数据
@@ -227,28 +198,26 @@ export const getNotes = async () => {
return [] return []
} }
} }
/** /**
* 保存便签数据 * 保存便签数据
* 将便签数组保存到IndexedDB * 将便签数组保存到IndexedDB
* @param {Array} notes - 便签数组 * @param {Array} notes - 便签数组
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export const saveNotes = async (notes) => { export const saveNotes = async notes => {
try { try {
await saveToStore(NOTES_STORE, notes) await saveToStore(NOTES_STORE, notes)
} catch (error) { } catch (error) {
console.error('Error saving notes:', error) console.error('Error saving notes:', error)
} }
} }
/** /**
* 添加新便签 * 添加新便签
* 创建一个新的便签对象并添加到便签列表中 * 创建一个新的便签对象并添加到便签列表中
* @param {Object} note - 便签对象,包含便签内容和其他属性 * @param {Object} note - 便签对象,包含便签内容和其他属性
* @returns {Promise<Object>} 新创建的便签对象 * @returns {Promise<Object>} 新创建的便签对象
*/ */
export const addNote = async (note) => { export const addNote = async note => {
try { try {
// 创建新的便签对象,添加必要的属性 // 创建新的便签对象,添加必要的属性
const newNote = { const newNote = {
@@ -263,18 +232,18 @@ export const addNote = async (note) => {
isDeleted: note.isDeleted || false, // 是否已删除 isDeleted: note.isDeleted || false, // 是否已删除
deletedAt: note.deletedAt || null, // 删除时间 deletedAt: note.deletedAt || null, // 删除时间
folderId: note.folderId || null, // 文件夹ID folderId: note.folderId || null, // 文件夹ID
...note ...note,
} }
// 添加到存储 // 添加到存储
await addToStore(NOTES_STORE, newNote) await addToStore(NOTES_STORE, newNote)
// 添加到离线队列(用于同步)
offlineQueue.addOperation('add', NOTES_STORE, newNote)
return newNote return newNote
} catch (error) { } catch (error) {
console.error('Error adding note:', error) console.error('Error adding note:', error)
throw error throw error
} }
} }
/** /**
* 更新便签 * 更新便签
* 根据ID查找并更新便签信息 * 根据ID查找并更新便签信息
@@ -287,36 +256,40 @@ export const updateNote = async (id, updates) => {
// 更新便签并保存 // 更新便签并保存
const updatedNote = await updateInStore(NOTES_STORE, id, { const updatedNote = await updateInStore(NOTES_STORE, id, {
...updates, ...updates,
updatedAt: getCurrentDateTime() // 更新最后修改时间 updatedAt: getCurrentDateTime(), // 更新最后修改时间
}) })
if (updatedNote) {
// 添加到离线队列(用于同步)
offlineQueue.addOperation('update', NOTES_STORE, 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
} }
} }
/** /**
* 删除便签 * 删除便签
* 根据ID从便签列表中移除便签 * 根据ID从便签列表中移除便签
* @param {string} id - 要删除的便签ID * @param {string} id - 要删除的便签ID
* @returns {Promise<boolean>} 删除成功返回true未找到便签返回false * @returns {Promise<boolean>} 删除成功返回true未找到便签返回false
*/ */
export const deleteNote = async (id) => { export const deleteNote = async id => {
try { try {
// 从存储中删除 // 从存储中删除
const result = await deleteFromStore(NOTES_STORE, id) const result = await deleteFromStore(NOTES_STORE, id)
if (result) {
// 添加到离线队列(用于同步)
offlineQueue.addOperation('delete', NOTES_STORE, { id })
}
return result return result
} catch (error) { } catch (error) {
console.error('Error deleting note:', error) console.error('Error deleting note:', error)
return false return false
} }
} }
// 文件夹操作函数 // 文件夹操作函数
// 提供文件夹的增删改查功能 // 提供文件夹的增删改查功能
/** /**
* 获取所有文件夹数据 * 获取所有文件夹数据
* 从IndexedDB中读取文件夹数据 * 从IndexedDB中读取文件夹数据
@@ -331,46 +304,44 @@ export const getFolders = async () => {
return [] return []
} }
} }
/** /**
* 保存文件夹数据 * 保存文件夹数据
* 将文件夹数组保存到IndexedDB * 将文件夹数组保存到IndexedDB
* @param {Array} folders - 文件夹数组 * @param {Array} folders - 文件夹数组
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export const saveFolders = async (folders) => { export const saveFolders = async folders => {
try { try {
await saveToStore(FOLDERS_STORE, folders) await saveToStore(FOLDERS_STORE, folders)
} catch (error) { } catch (error) {
console.error('Error saving folders:', error) console.error('Error saving folders:', error)
} }
} }
/** /**
* 添加新文件夹 * 添加新文件夹
* 创建一个新的文件夹对象并添加到文件夹列表中 * 创建一个新的文件夹对象并添加到文件夹列表中
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性 * @param {Object} folder - 文件夹对象,包含文件夹名称等属性
* @returns {Promise<Object>} 新创建的文件夹对象 * @returns {Promise<Object>} 新创建的文件夹对象
*/ */
export const addFolder = async (folder) => { export const addFolder = async folder => {
try { try {
// 创建新的文件夹对象,添加必要的属性 // 创建新的文件夹对象,添加必要的属性
const newFolder = { const newFolder = {
name: folder.name || '', name: folder.name || '',
id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间 createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间
...folder ...folder,
} }
// 添加到存储 // 添加到存储
await addToStore(FOLDERS_STORE, newFolder) await addToStore(FOLDERS_STORE, newFolder)
// 添加到离线队列(用于同步)
offlineQueue.addOperation('add', FOLDERS_STORE, newFolder)
return newFolder return newFolder
} catch (error) { } catch (error) {
console.error('Error adding folder:', error) console.error('Error adding folder:', error)
throw error throw error
} }
} }
/** /**
* 更新文件夹 * 更新文件夹
* 根据ID查找并更新文件夹信息 * 根据ID查找并更新文件夹信息
@@ -382,33 +353,38 @@ export const updateFolder = async (id, updates) => {
try { try {
// 更新文件夹并保存 // 更新文件夹并保存
const updatedFolder = await updateInStore(FOLDERS_STORE, id, updates) const updatedFolder = await updateInStore(FOLDERS_STORE, id, updates)
if (updatedFolder) {
// 添加到离线队列(用于同步)
offlineQueue.addOperation('update', FOLDERS_STORE, updatedFolder)
}
return updatedFolder return updatedFolder
} catch (error) { } catch (error) {
console.error('Error updating folder:', error) console.error('Error updating folder:', error)
throw error throw error
} }
} }
/** /**
* 删除文件夹 * 删除文件夹
* 根据ID从文件夹列表中移除文件夹 * 根据ID从文件夹列表中移除文件夹
* @param {string} id - 要删除的文件夹ID * @param {string} id - 要删除的文件夹ID
* @returns {Promise<boolean>} 删除成功返回true未找到文件夹返回false * @returns {Promise<boolean>} 删除成功返回true未找到文件夹返回false
*/ */
export const deleteFolder = async (id) => { export const deleteFolder = async id => {
try { try {
// 从存储中删除 // 从存储中删除
const result = await deleteFromStore(FOLDERS_STORE, id) const result = await deleteFromStore(FOLDERS_STORE, id)
if (result) {
// 添加到离线队列(用于同步)
offlineQueue.addOperation('delete', FOLDERS_STORE, { id })
}
return result return result
} catch (error) { } catch (error) {
console.error('Error deleting folder:', error) console.error('Error deleting folder:', error)
return false return false
} }
} }
// 设置操作函数 // 设置操作函数
// 提供应用设置的读取和保存功能 // 提供应用设置的读取和保存功能
/** /**
* 获取应用设置 * 获取应用设置
* 从IndexedDB中读取设置数据 * 从IndexedDB中读取设置数据
@@ -420,17 +396,14 @@ export const getSettings = async () => {
const transaction = database.transaction([SETTINGS_STORE], 'readonly') const transaction = database.transaction([SETTINGS_STORE], 'readonly')
const store = transaction.objectStore(SETTINGS_STORE) const store = transaction.objectStore(SETTINGS_STORE)
const request = store.get('settings') const request = store.get('settings')
const settings = await new Promise((resolve, reject) => { const settings = await new Promise((resolve, reject) => {
request.onsuccess = () => { request.onsuccess = () => {
resolve(request.result || { cloudSync: false, darkMode: false }) resolve(request.result || { cloudSync: false, darkMode: false })
} }
request.onerror = () => { request.onerror = () => {
reject(new Error('获取设置失败')) reject(new Error('获取设置失败'))
} }
}) })
return settings return settings
} catch (error) { } catch (error) {
console.error('Error getting settings:', error) console.error('Error getting settings:', error)
@@ -438,35 +411,34 @@ export const getSettings = async () => {
return { cloudSync: false, darkMode: false } return { cloudSync: false, darkMode: false }
} }
} }
/** /**
* 保存应用设置 * 保存应用设置
* 将设置对象保存到IndexedDB * 将设置对象保存到IndexedDB
* @param {Object} settings - 设置对象 * @param {Object} settings - 设置对象
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export const saveSettings = async (settings) => { export const saveSettings = async settings => {
try { try {
const database = await openDB() const database = await openDB()
const transaction = database.transaction([SETTINGS_STORE], 'readwrite') const transaction = database.transaction([SETTINGS_STORE], 'readwrite')
const store = transaction.objectStore(SETTINGS_STORE) const store = transaction.objectStore(SETTINGS_STORE)
const request = store.put(settings, 'settings') const request = store.put(settings, 'settings')
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
request.onsuccess = () => resolve() request.onsuccess = () => resolve()
request.onerror = () => reject(new Error('保存设置失败')) request.onerror = () => reject(new Error('保存设置失败'))
}) })
// 添加到离线队列(用于同步)
offlineQueue.addOperation('update', SETTINGS_STORE, settings)
} catch (error) { } catch (error) {
console.error('Error saving settings:', error) console.error('Error saving settings:', error)
} }
} }
/** /**
* 确保数据有默认值 * 确保数据有默认值
* @param {Array} notes - 便签数组 * @param {Array} notes - 便签数组
* @returns {Array} 处理后的便签数组 * @returns {Array} 处理后的便签数组
*/ */
const ensureNotesDefaults = (notes) => { const ensureNotesDefaults = notes => {
return notes.map(note => ({ return notes.map(note => ({
title: note.title || '', title: note.title || '',
content: note.content || '', content: note.content || '',
@@ -479,24 +451,22 @@ const ensureNotesDefaults = (notes) => {
isDeleted: note.isDeleted || false, isDeleted: note.isDeleted || false,
deletedAt: note.deletedAt || null, deletedAt: note.deletedAt || null,
folderId: note.folderId || null, folderId: note.folderId || null,
...note ...note,
})) }))
} }
/** /**
* 确保文件夹数据有默认值 * 确保文件夹数据有默认值
* @param {Array} folders - 文件夹数组 * @param {Array} folders - 文件夹数组
* @returns {Array} 处理后的文件夹数组 * @returns {Array} 处理后的文件夹数组
*/ */
const ensureFoldersDefaults = (folders) => { const ensureFoldersDefaults = folders => {
return folders.map(folder => ({ return folders.map(folder => ({
name: folder.name || '', name: folder.name || '',
id: folder.id, id: folder.id,
createdAt: folder.createdAt, createdAt: folder.createdAt,
...folder ...folder,
})) }))
} }
/** /**
* 初始化数据库 * 初始化数据库
* @returns {Promise<void>} * @returns {Promise<void>}

236
src/utils/networkUtils.js Normal file
View File

@@ -0,0 +1,236 @@
/**
* 网络状态检测和离线功能支持工具
*/
/**
* 检查网络连接状态
* @returns {boolean} 网络是否连接
*/
export const isOnline = () => {
return navigator.onLine;
};
/**
* 监听网络状态变化
* @param {Function} onlineCallback - 网络连接时的回调
* @param {Function} offlineCallback - 网络断开时的回调
*/
export const addNetworkListener = (onlineCallback, offlineCallback) => {
window.addEventListener('online', onlineCallback);
window.addEventListener('offline', offlineCallback);
};
/**
* 移除网络状态监听
* @param {Function} onlineCallback - 网络连接时的回调
* @param {Function} offlineCallback - 网络断开时的回调
*/
export const removeNetworkListener = (onlineCallback, offlineCallback) => {
window.removeEventListener('online', onlineCallback);
window.removeEventListener('offline', offlineCallback);
};
/**
* 检测网络类型和速度
* @returns {Object} 网络信息对象
*/
export const getNetworkInfo = () => {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (connection) {
return {
effectiveType: connection.effectiveType, // 'slow-2g', '2g', '3g', '4g'
downlink: connection.downlink, // 下行速度 (Mbps)
rtt: connection.rtt, // 往返时间 (ms)
saveData: connection.saveData, // 是否开启了数据保护模式
};
}
return {
effectiveType: 'unknown',
downlink: null,
rtt: null,
saveData: false,
};
};
/**
* 检查是否为慢速网络
* @returns {boolean} 是否为慢速网络
*/
export const isSlowNetwork = () => {
const networkInfo = getNetworkInfo();
return networkInfo.effectiveType === 'slow-2g' || networkInfo.effectiveType === '2g';
};
/**
* 在线测试 - 发送一个请求来测试网络连接
* @param {string} url - 测试URL默认为当前域名
* @returns {Promise<boolean>} 网络是否可用
*/
export const testOnline = async (url = null) => {
if (!isOnline()) {
return false;
}
try {
const testUrl = url || `${window.location.protocol}//${window.location.host}/`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
const response = await fetch(testUrl, {
method: 'HEAD',
cache: 'no-cache',
signal: controller.signal,
});
clearTimeout(timeoutId);
return response.ok;
} catch (error) {
console.warn('Online test failed:', error.message);
return false;
}
};
/**
* 检查 IndexedDB 可用性
* @returns {Promise<boolean>} IndexedDB 是否可用
*/
export const isIndexedDBAvailable = async () => {
if (!window.indexedDB) {
return false;
}
try {
const name = 'test-db';
const version = 1;
const request = window.indexedDB.open(name, version);
return new Promise((resolve) => {
request.onsuccess = () => {
const db = request.result;
db.close();
window.indexedDB.deleteDatabase(name); // 清理测试数据库
resolve(true);
};
request.onerror = () => {
resolve(false);
};
// 超时处理
setTimeout(() => {
resolve(false);
}, 5000);
});
} catch (error) {
console.error('IndexedDB availability check failed:', error);
return false;
}
};
/**
* 检查存储配额
* @returns {Promise<Object>} 存储配额信息
*/
export const getStorageQuota = async () => {
if (navigator.storage && navigator.storage.estimate) {
try {
const quota = await navigator.storage.estimate();
return {
quota: quota.quota,
usage: quota.usage,
usageDetails: quota.usageDetails,
percentage: quota.quota ? (quota.usage / quota.quota) * 100 : 0,
};
} catch (error) {
console.error('Storage quota estimation failed:', error);
return null;
}
}
return null;
};
/**
* 检查存储空间是否充足
* @param {number} requiredSpace - 需要的存储空间(字节)
* @returns {Promise<boolean>} 是否有足够存储空间
*/
export const hasSufficientStorage = async (requiredSpace = 10 * 1024 * 1024) => { // 默认10MB
const storageInfo = await getStorageQuota();
if (!storageInfo) {
// 无法获取存储信息时假设空间充足
return true;
}
return (storageInfo.quota - storageInfo.usage) > requiredSpace;
};
/**
* 离线数据操作队列
*/
export class OfflineQueue {
constructor() {
this.queue = JSON.parse(localStorage.getItem('offlineQueue') || '[]');
}
/**
* 添加操作到离线队列
* @param {string} type - 操作类型 (add, update, delete)
* @param {string} storeName - 存储名
* @param {Object} data - 数据
*/
addOperation(type, storeName, data) {
const operation = {
id: Date.now() + Math.random(),
type,
storeName,
data,
timestamp: Date.now(),
};
this.queue.push(operation);
this.saveQueue();
}
/**
* 获取所有待处理的操作
* @returns {Array} 操作队列
*/
getOperations() {
return this.queue;
}
/**
* 处理操作队列
* @param {Function} processFn - 处理函数
*/
async processQueue(processFn) {
for (const operation of [...this.queue]) {
try {
await processFn(operation);
// 从队列中移除已处理的操作
this.queue = this.queue.filter(op => op.id !== operation.id);
this.saveQueue();
} catch (error) {
console.error('Failed to process offline operation:', error);
// 如果处理失败,保留操作在队列中以备后续重试
}
}
}
/**
* 保存队列到本地存储
*/
saveQueue() {
localStorage.setItem('offlineQueue', JSON.stringify(this.queue));
}
/**
* 清空队列
*/
clear() {
this.queue = [];
this.saveQueue();
}
}

View File

@@ -1,20 +1,19 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const isPwaMode = mode === 'pwa' const isPwaMode = mode === 'pwa'
const plugins = [vue()] const plugins = [vue()]
// 只在PWA模式下添加PWA插件 // 只在PWA模式下添加PWA插件
if (isPwaMode) { if (isPwaMode) {
plugins.push( plugins.push(
VitePWA({ VitePWA({
strategies: 'generateSW',
registerType: 'autoUpdate', registerType: 'autoUpdate',
devOptions: { devOptions: {
enabled: false, enabled: false,
type: 'module',
}, },
manifest: { manifest: {
name: '锤子便签', name: '锤子便签',
@@ -38,42 +37,22 @@ export default defineConfig(({ mode }) => {
], ],
}, },
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,wasm,png,jpg,jpeg,svg,ico}'], globPatterns: ['**/*.{js,css,html,ico,png,jpg,jpeg,svg,woff,woff2,ttf,eot}'],
runtimeCaching: [ // 预缓存我们指定的静态资源
{ additionalManifestEntries: [
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, { url: '/', revision: null },
handler: 'CacheFirst', { url: '/index.html', revision: null },
options: { { url: '/icons/icon-192.png', revision: null },
cacheName: 'google-fonts-cache', { url: '/icons/icon-512.png', revision: null },
expiration: { // 添加更多需要预缓存的静态资源
maxEntries: 10, { url: '/assets/icons/drawable-xxhdpi/note_background.png', revision: null },
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days { url: '/assets/icons/drawable-xxhdpi/note_setting_bg.png', revision: null },
}, { url: '/assets/icons/drawable-xxhdpi/action_bar_default.png', revision: null },
cacheableResponse: {
statuses: [0, 200],
},
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'gstatic-fonts-cache',
expiration: {
maxEntries: 10,
maxAgeSeconds: 60 * 60 * 24 * 365, // <== 365 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
], ],
}, },
}) })
) )
} }
return { return {
plugins, plugins,
resolve: { resolve: {