You've already forked SmartisanNote.Remake
future #10
294
IFLOW.md
294
IFLOW.md
@@ -1,208 +1,168 @@
|
|||||||
# SmartisanNote.vue 项目概览
|
# SmartisanNote.Remake - IFLOW 上下文
|
||||||
|
|
||||||
## 项目简介
|
## 项目概述
|
||||||
|
|
||||||
这是一个基于 Vue 3 和 Vite 构建的单页 Web 应用(SPA),旨在模仿锤子科技(Smartisan)的便签应用。该项目使用 localStorage 进行本地数据持久化,支持便签的增删改查、文件夹管理、便签置顶、星标标记、基础设置(如云同步、深色模式)以及丰富的交互功能。
|
这是一个基于 Vue 3、Vite 和 Pinia 的移动端现代化 Web 应用,旨在重现并改进经典的锤子便签应用体验。该项目采用 PWA(渐进式 Web 应用)技术,支持离线使用和安装到主屏幕。
|
||||||
|
|
||||||
## 技术栈
|
### 主要技术栈
|
||||||
|
|
||||||
* **核心框架**: Vue 3 (Composition API)
|
* **框架**: Vue 3 (Composition API)
|
||||||
* **构建工具**: Vite
|
* **构建工具**: Vite
|
||||||
* **路由管理**: vue-router
|
* **状态管理**: Pinia
|
||||||
* **状态管理**: Pinia (Vue 3 状态管理库)
|
* **路由**: Vue Router
|
||||||
* **UI 库**: 原生 CSS,使用了锤子便签的经典配色方案(定义在 `index.html` 的 CSS 变量中)
|
* **UI 组件库**: Ionic Vue (部分使用)
|
||||||
* **移动端支持**: Capacitor (用于构建 Android/iOS 应用)
|
* **PWA 支持**: vite-plugin-pwa
|
||||||
* **样式预处理器**: Less
|
* **本地存储**: IndexedDB (通过 `src/utils/indexedDBStorage.js` 封装)
|
||||||
* **代码语言**: JavaScript (ES6+)
|
* **CSS 预处理器**: Less
|
||||||
* **UI 组件库**: Ionic Framework
|
|
||||||
* **日期处理**: moment.js
|
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── android/ # Capacitor Android 项目文件
|
├── android/ # Capacitor Android 项目文件
|
||||||
├── dist/ # 构建后的生产文件
|
├── public/ # 静态资源目录 (图标等)
|
||||||
├── node_modules/ # 项目依赖
|
├── src/ # 源代码目录
|
||||||
├── public/ # 静态资源目录
|
│ ├── App.vue # 根组件
|
||||||
├── src/
|
│ ├── main.js # 应用入口文件
|
||||||
│ ├── common/ # 全局样式和通用工具
|
│ ├── common/ # 通用样式
|
||||||
│ ├── components/ # 可复用的 Vue 组件 (Header, NoteItem, FolderItem)
|
│ ├── components/ # 可复用的 UI 组件
|
||||||
│ ├── pages/ # 页面级别的 Vue 组件 (NoteList, NoteEditor, Folder, Settings)
|
│ ├── pages/ # 页面组件
|
||||||
│ ├── stores/ # Pinia 状态管理 stores
|
│ ├── stores/ # Pinia 状态管理
|
||||||
│ │ └── useAppStore.js # 全局状态管理 store
|
│ └── utils/ # 工具函数
|
||||||
│ ├── utils/ # 工具函数
|
├── index.html # 应用入口 HTML 文件
|
||||||
│ │ ├── dateUtils.js # 日期处理工具,基于 moment.js
|
├── .nvmdrc # node.js 版本
|
||||||
│ │ └── storage.js # localStorage 封装,负责数据的读写
|
├── update.txt # 更新日志
|
||||||
│ ├── App.vue # 根组件
|
├── package.json # 项目依赖和脚本
|
||||||
│ └── main.js # 应用入口,初始化路由、Pinia 和挂载
|
├── vite.config.js # Vite 配置文件
|
||||||
├── index.html # HTML 模板,包含 CSS 变量定义
|
└── capacitor.config.json # Capacitor 配置文件
|
||||||
├── package.json # 项目元数据和脚本命令
|
|
||||||
├── vite.config.js # Vite 构建配置
|
|
||||||
└── capacitor.config.json # Capacitor 配置文件
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发与构建命令
|
|
||||||
|
|
||||||
* **安装依赖**: `npm install`
|
|
||||||
* **启动开发服务器**: `npm run dev`
|
|
||||||
* 默认端口: 3000
|
|
||||||
* 基于 Vite,支持热更新。
|
|
||||||
* **构建生产版本**: `npm run build`
|
|
||||||
* 使用 Vite 构建,并同步到 Capacitor 项目 (`npx cap sync`)。
|
|
||||||
* **构建所有版本**: `npm run build:all`
|
|
||||||
* 构建标准版本和 PWA 版本
|
|
||||||
* **部署PWA版本**: `npm run deploy:pwa`
|
|
||||||
* 构建并部署PWA版本到FTP服务器
|
|
||||||
* **在 Android 设备上运行**: `npm run android`
|
|
||||||
* 需要预先配置好 Android 开发环境。
|
|
||||||
|
|
||||||
## 核心功能
|
## 核心功能
|
||||||
|
|
||||||
### 便签管理
|
* **便签管理**: 创建、编辑、删除、置顶、加星便签。
|
||||||
* **增删改查**: 支持创建、查看、编辑和删除便签
|
* **文件夹管理**: 将便签分类到不同的文件夹中。
|
||||||
* **星标标记**: 可以将重要便签标记为星标便签
|
* **搜索功能**: 按标题或内容搜索便签。
|
||||||
* **置顶功能**: 支持将便签置顶显示在列表顶部
|
* **回收站**: 临时存储已删除的便签,支持彻底删除。
|
||||||
* **图片标记**: 可以标记便签中是否包含图片
|
* **多种排序方式**: 按更新时间、标题、星标状态排序。
|
||||||
* **滑动删除**: 支持右滑显示删除按钮,带有阻尼效果的交互体验
|
* **PWA 支持**: 可安装为独立应用,支持离线使用。
|
||||||
* **富文本编辑**: 支持加粗、居中、待办事项、列表、标题、引用等格式
|
* **本地存储**: 所有数据存储在浏览器的 `IndexedDB` 中。
|
||||||
|
* **深色模式**: (计划中) 支持切换深色/浅色主题。
|
||||||
|
* **云同步**: (计划中) 支持多设备间数据同步。
|
||||||
|
|
||||||
### 文件夹管理
|
## 开发与构建
|
||||||
* **分类组织**: 支持创建文件夹对便签进行分类管理
|
|
||||||
* **默认文件夹**: 提供"全部便签"、"加星便签"、"回收站"等默认文件夹
|
|
||||||
|
|
||||||
### 搜索功能
|
### 前置条件
|
||||||
* **全文搜索**: 支持按标题和内容搜索便签
|
|
||||||
* **实时过滤**: 搜索结果实时更新
|
|
||||||
|
|
||||||
### 设置功能
|
确保已安装 Node.js (>=14) 和 npm。
|
||||||
* **云同步**: 支持云同步设置(待实现)
|
|
||||||
* **深色模式**: 支持深色模式切换(待完善)
|
|
||||||
|
|
||||||
### 日期时间处理
|
### 安装依赖
|
||||||
* **智能格式化**: 根据时间范围自动格式化日期显示
|
|
||||||
* **多场景适配**: 不同页面使用不同的日期格式化规则
|
|
||||||
* **本地化支持**: 支持中文日期格式显示
|
|
||||||
|
|
||||||
## 代码规范与开发约定
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
* **状态管理**: 使用 Pinia 进行全局状态管理,通过 `useAppStore` composable 函数访问状态。
|
### 开发
|
||||||
* **数据持久化**: 所有数据(便签、文件夹、设置)均通过 `src/utils/storage.js` 与 `localStorage` 进行交互。
|
|
||||||
* **路由**: 使用 `vue-router` 和 `createWebHashHistory` 进行前端路由管理。
|
|
||||||
* **UI 风格**: 颜色方案严格遵循 `index.html` 中定义的 CSS 变量,以保持锤子便签的视觉风格。
|
|
||||||
* **组件组织**: 页面组件 (`pages/`) 和可复用组件 (`components/`) 分离,结构清晰。
|
|
||||||
* **代码风格**: 采用标准的 Vue 3 Composition API 写法,使用 ES6 模块系统 (`import`/`export`)。
|
|
||||||
* **日期处理**: 使用 moment.js 进行日期处理,通过 `src/utils/dateUtils.js` 统一管理日期格式化逻辑。
|
|
||||||
|
|
||||||
## 样式
|
启动开发服务器:
|
||||||
|
|
||||||
* 全局样式文件是位于 `common/` 目录下的 `base.css`。
|
```bash
|
||||||
* 使用 Codefun 原子类样式,用于快速布局。
|
npm run dev
|
||||||
* 样式规范应遵循项目中已有的风格。
|
```
|
||||||
* 使用 Less 作为 CSS 预处理器。
|
|
||||||
|
|
||||||
## JavaScript
|
这将在 `http://localhost:3000` 启动应用。
|
||||||
|
|
||||||
* 严格遵循ES6规范。
|
### 构建
|
||||||
* 遵循JavaScript函数式编程范式。
|
|
||||||
* 方法类函数应该使用 `function` 进行定义。
|
|
||||||
* 避免出现超过4个以上的 `ref`,超过4个则使用 `reactive`。
|
|
||||||
* 全局变量都集中放置于代码顶部。
|
|
||||||
* 变量名使用小驼峰命名法。
|
|
||||||
* 常量名使用全大写。
|
|
||||||
* 状态类变量命名参考 `isLogin`、`isOpen`。
|
|
||||||
* 事件类方法命名参考 `onClick`、`onSelect`。
|
|
||||||
* 变量都应该写有注释说明、类型说明。
|
|
||||||
* `Promise` 方法使用 `async` `await` 写法,并进行容错处理。
|
|
||||||
* 字符串拼接使用ES6的模板语法。
|
|
||||||
* JavaScript规范应遵循项目中已有的风格。
|
|
||||||
|
|
||||||
## 组件
|
构建标准 Web 应用:
|
||||||
|
|
||||||
* 全局组件放在 `components/` 目录下。
|
```bash
|
||||||
* 页面独立组件放在页面根目录下的 `components/`。
|
npm run build
|
||||||
* 每个组件应该附带 `README.MD` 文档。
|
```
|
||||||
* 组件编写应遵循项目中已有的风格。
|
|
||||||
|
|
||||||
### Header 组件
|
构建 PWA 应用:
|
||||||
* **动态按钮**: 根据页面状态显示不同的操作按钮(新建、保存、插入图片)
|
|
||||||
* **文件夹管理**: 支持文件夹展开/收起功能
|
|
||||||
|
|
||||||
### NoteItem 组件
|
```bash
|
||||||
* **滑动交互**: 支持右滑显示删除按钮,带有阻尼效果
|
npm run build:pwa
|
||||||
* **状态切换**: 支持星标和置顶状态的切换
|
```
|
||||||
* **视觉反馈**: 滑动时便签夹会切换状态,提供直观的交互反馈
|
|
||||||
* **日期显示**: 显示格式化后的便签更新时间
|
|
||||||
|
|
||||||
### RichTextEditor 组件
|
构建所有版本 (标准 + PWA):
|
||||||
* **富文本编辑**: 支持多种文本格式(加粗、居中、待办事项、列表、标题、引用)
|
|
||||||
* **图片插入**: 支持插入图片功能
|
|
||||||
* **工具栏**: 提供浮动工具栏,支持格式化操作
|
|
||||||
|
|
||||||
## 页面
|
```bash
|
||||||
|
npm run build:all
|
||||||
|
```
|
||||||
|
|
||||||
* 页面使用 Composition API (setup语法糖) 编写。
|
### 部署 PWA
|
||||||
* 注释、结构规范应遵循项目中已有的风格。
|
|
||||||
|
|
||||||
### NoteListPage
|
构建 PWA 并上传到服务器:
|
||||||
* **便签列表**: 显示所有便签,支持置顶便签优先显示
|
|
||||||
* **文件夹管理**: 支持文件夹的展开和切换
|
|
||||||
* **搜索功能**: 提供便签搜索功能
|
|
||||||
* **交互反馈**: 显示便签总数和置顶便签数量
|
|
||||||
* **智能日期显示**: 根据时间范围显示不同的日期格式
|
|
||||||
* 今天:显示为 "今天 下午 4:00"
|
|
||||||
* 昨天:显示为 "昨天 下午 4:00"
|
|
||||||
* 超过两天但小于一周:显示为 "星期一 10/8 上午 3:00"
|
|
||||||
* 超过一周但小于一年:显示为 "10天前 9/20 下午 2:00"
|
|
||||||
* 超过一年:显示为 "635天前 2024/8/10 上午 9:00"
|
|
||||||
|
|
||||||
### NoteEditorPage
|
```bash
|
||||||
* **编辑模式**: 支持新建和编辑便签
|
npm run deploy:pwa
|
||||||
* **富文本编辑**: 集成RichTextEditor组件,支持丰富的文本格式
|
```
|
||||||
* **图片插入**: 支持通过工具栏插入图片
|
|
||||||
* **状态管理**: 根据路由参数判断是新建还是编辑模式
|
|
||||||
* **智能日期显示**: 根据时间范围显示不同的日期格式
|
|
||||||
* 今天:显示为 "今天 下午 4:00"
|
|
||||||
* 昨天:显示为 "昨天 下午 4:00"
|
|
||||||
* 超过两天但小于一个月:显示为 "10/8 上午 3:00"
|
|
||||||
* 超过一个月:显示为 "2024/8/10 上午 9:00"
|
|
||||||
|
|
||||||
## 状态管理 (Pinia)
|
这将执行 `vite build --mode pwa` 并运行 `upload-pwa.js` 脚本。
|
||||||
|
|
||||||
项目现在使用 Pinia 作为状态管理解决方案,主要特点包括:
|
### Android 应用
|
||||||
|
|
||||||
* **Store 定义**: 在 `src/stores/useAppStore.js` 中定义了全局状态 store
|
运行 Android 应用:
|
||||||
* **状态结构**: 包含 notes、folders 和 settings 三个主要状态
|
|
||||||
* **Getters**: 提供了计算属性如 starredNotesCount 和 allNotesCount
|
|
||||||
* **Actions**: 包含所有状态变更操作,如 addNote、updateNote、deleteNote 等
|
|
||||||
* **数据持久化**: 通过 storage.js 工具函数与 localStorage 进行数据交互
|
|
||||||
* **Mock 数据**: 支持加载预设的 mock 数据用于开发和演示
|
|
||||||
* **使用方式**: 在组件中通过 `const store = useAppStore()` 来访问状态和方法
|
|
||||||
|
|
||||||
## 新增功能特性
|
```bash
|
||||||
|
npm run android
|
||||||
|
```
|
||||||
|
|
||||||
### 滑动交互
|
## 代码规范与约定
|
||||||
* **阻尼效果**: 右滑超过阈值后提供阻尼效果,增强交互体验
|
|
||||||
* **状态切换**: 滑动时便签夹会切换到展开状态,松开后根据位置决定是否保持展开
|
|
||||||
* **视觉反馈**: 滑动过程中提供实时视觉反馈
|
|
||||||
|
|
||||||
### Mock 数据
|
### 代码风格与结构
|
||||||
* **自动加载**: 当检测到无数据时自动加载预设的 mock 数据
|
|
||||||
* **手动加载**: 可通过 URL 参数 `?mock=true` 强制加载 mock 数据
|
|
||||||
* **丰富内容**: 包含多种类型的便签、文件夹和设置示例
|
|
||||||
* **固定日期**: 使用固定的日期值确保数据一致性
|
|
||||||
|
|
||||||
### 增强的便签功能
|
* **框架**: Vue 3 (Composition API) 与 Pinia 状态管理
|
||||||
* **置顶支持**: 便签可以置顶显示在列表顶部
|
* **构建工具**: Vite
|
||||||
* **图片标记**: 可以标记便签是否包含图片
|
* **路由**: Vue Router
|
||||||
* **排序优化**: 置顶便签优先显示,按更新时间排序
|
* **UI 组件库**: Ionic Vue (部分使用)
|
||||||
|
* **PWA 支持**: vite-plugin-pwa
|
||||||
|
* **本地存储**: IndexedDB (通过 `src/utils/indexedDBStorage.js` 封装)
|
||||||
|
* **CSS 预处理器**: Less
|
||||||
|
* **CSS 命名**: 使用 BEM 命名规范,部分使用原子化 CSS 类名(以 `code-fun-` 开头)
|
||||||
|
* **响应式设计**: 使用 viewport 单位 (vw/vh) 和 CSS 变量实现响应式布局
|
||||||
|
* **图标**: 使用 PNG 图片作为图标,存储在 `public/assets/icons/` 目录下
|
||||||
|
|
||||||
### PWA 支持
|
### 命名规范
|
||||||
* **离线使用**: 支持构建PWA版本,可离线使用
|
|
||||||
* **自动部署**: 支持一键构建并部署到FTP服务器
|
|
||||||
|
|
||||||
### 智能日期处理
|
* **文件命名**:
|
||||||
* **统一管理**: 通过 `dateUtils.js` 统一管理所有日期处理逻辑
|
* 组件文件使用 PascalCase 命名法,如 `NoteItem.vue`
|
||||||
* **多格式支持**: 支持多种日期格式化方式以适应不同场景
|
* 页面文件使用 PascalCase 命名法,如 `NoteListPage.vue`
|
||||||
* **本地化显示**: 支持中文友好的日期时间显示
|
* 工具文件使用 camelCase 命名法,如 `dateUtils.js`
|
||||||
* **场景适配**: 不同页面使用最适合的日期格式化规则
|
* Store 文件使用 `use` 前缀 + PascalCase 命名法,如 `useAppStore.js`
|
||||||
|
* **组件命名**: 使用 PascalCase 命名法,如 `NoteItem`
|
||||||
|
* **变量命名**: 使用 camelCase 命名法,如 `noteToDelete`
|
||||||
|
* **常量命名**: 使用 UPPER_SNAKE_CASE 命名法,如 `NOTES_KEY`
|
||||||
|
* **函数命名**: 使用 camelCase 命名法,如 `handleNotePress`
|
||||||
|
|
||||||
|
### 代码组织
|
||||||
|
|
||||||
|
* **组件结构**:
|
||||||
|
* 使用 `<template>`, `<script setup>`, `<style>` 三段式结构
|
||||||
|
* 计算属性 (`computed`) 置于响应式数据之后
|
||||||
|
* 方法 (`methods`) 置于计算属性之后
|
||||||
|
* 生命周期钩子 (`onMounted`, `onUnmounted` 等) 置于方法之前
|
||||||
|
* 样式使用 Less 预处理器,并使用 `scoped` 属性避免样式污染
|
||||||
|
* **状态管理**:
|
||||||
|
* 使用 Pinia 进行全局状态管理
|
||||||
|
* Store 文件中按照 state, getters, actions 的顺序组织代码
|
||||||
|
* 异步操作使用 `async/await` 语法
|
||||||
|
* **工具函数**:
|
||||||
|
* 工具函数按功能模块划分文件,如 `indexedDBStorage.js`, `dateUtils.js`
|
||||||
|
* 工具函数使用 `export const` 导出
|
||||||
|
|
||||||
|
### 注释规范
|
||||||
|
|
||||||
|
* **单行注释**: 使用 `//` 进行单行注释,注释应位于代码上方或行尾
|
||||||
|
* **多行注释**: 使用 `/* */` 进行多行注释,用于解释复杂逻辑
|
||||||
|
* **函数注释**: 使用 JSDoc 格式为函数添加注释,包含函数描述、参数类型和返回值类型
|
||||||
|
|
||||||
|
## 代码提交规范
|
||||||
|
|
||||||
|
* 提交信息应清晰描述变更内容,如 `修复 搜索功能空值检查` 或 `新增 删除按钮功能`。
|
||||||
|
* 对于功能性的新增或修改,使用 `新增` 前缀。
|
||||||
|
* 对于错误修复,使用 `修复` 前缀。
|
||||||
|
* 对于性能优化、代码重构(既不修复错误也不添加功能),使用 `优化` 前缀。
|
||||||
|
* 对于文档更新,使用 `文档` 前缀。
|
||||||
|
* 提交信息应使用中文。
|
||||||
103
README.md
103
README.md
@@ -1,103 +0,0 @@
|
|||||||
# 锤子便签(重制版)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这是一个基于 Vue 3 和 Vite 构建的单页 Web 应用(SPA),旨在模仿锤子科技(Smartisan)的便签应用。该项目使用 localStorage 进行本地数据持久化,支持便签的增删改查、文件夹管理、便签置顶、星标标记等丰富的交互功能。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
### 核心功能
|
|
||||||
- ✅ 便签的增删改查操作
|
|
||||||
- ✅ 便签置顶和星标标记
|
|
||||||
- ✅ 便签滑动删除交互(带阻尼效果)
|
|
||||||
- ✅ 文件夹分类管理
|
|
||||||
- ✅ 便签全文搜索
|
|
||||||
- ✅ 富文本编辑(支持加粗、居中、待办事项、列表、标题、引用等格式)
|
|
||||||
- ✅ 图片插入功能
|
|
||||||
- ✅ 响应式设计,适配移动端设备
|
|
||||||
- ✅ 刘海屏兼容
|
|
||||||
|
|
||||||
### 技术亮点
|
|
||||||
- ⚡ 基于 Vite 构建,开发体验流畅
|
|
||||||
- 🖼️ 使用原生 CSS 和锤子便签经典配色方案
|
|
||||||
- 📦 状态管理采用 Pinia
|
|
||||||
- 🔄 路由管理使用 vue-router
|
|
||||||
- 📱 支持构建为 PWA 应用
|
|
||||||
- 🚀 支持一键部署到 FTP 服务器
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
- Node.js >= 16.0.0
|
|
||||||
- npm >= 7.0.0
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 启动开发服务器
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
默认访问地址:http://localhost:3000
|
|
||||||
|
|
||||||
### 构建生产版本
|
|
||||||
```bash
|
|
||||||
# 构建标准版本
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 构建 PWA 版本
|
|
||||||
npm run build:pwa
|
|
||||||
|
|
||||||
# 构建并部署 PWA 版本到 FTP 服务器
|
|
||||||
npm run deploy:pwa
|
|
||||||
```
|
|
||||||
|
|
||||||
### 在 Android 设备上运行
|
|
||||||
```bash
|
|
||||||
npm run android
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
```
|
|
||||||
.
|
|
||||||
├── android/ # Capacitor Android 项目文件
|
|
||||||
├── dist/ # 构建后的生产文件
|
|
||||||
├── node_modules/ # 项目依赖
|
|
||||||
├── public/ # 静态资源目录
|
|
||||||
├── src/
|
|
||||||
│ ├── common/ # 全局样式和通用工具
|
|
||||||
│ ├── components/ # 可复用的 Vue 组件
|
|
||||||
│ ├── pages/ # 页面级别的 Vue 组件
|
|
||||||
│ ├── stores/ # Pinia 状态管理 stores
|
|
||||||
│ ├── utils/ # 工具函数
|
|
||||||
│ ├── App.vue # 根组件
|
|
||||||
│ └── main.js # 应用入口
|
|
||||||
├── index.html # HTML 模板
|
|
||||||
├── package.json # 项目元数据和脚本命令
|
|
||||||
├── vite.config.js # Vite 构建配置
|
|
||||||
└── capacitor.config.json # Capacitor 配置文件
|
|
||||||
```
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
- **核心框架**: Vue 3 (Composition API)
|
|
||||||
- **构建工具**: Vite
|
|
||||||
- **路由管理**: vue-router
|
|
||||||
- **状态管理**: Pinia
|
|
||||||
- **UI 库**: 原生 CSS
|
|
||||||
- **移动端支持**: Capacitor
|
|
||||||
- **样式预处理器**: Less
|
|
||||||
|
|
||||||
## 开发约定
|
|
||||||
- 使用 Vue 3 Composition API
|
|
||||||
- 遵循 ES6 规范
|
|
||||||
- 使用 Pinia 进行状态管理
|
|
||||||
- 通过 localStorage 实现数据持久化
|
|
||||||
- 组件和页面分离的目录结构
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
ISC
|
|
||||||
|
|
||||||
## 贡献
|
|
||||||
欢迎提交 Issue 和 Pull Request 来帮助改进这个项目。
|
|
||||||
0
console.txt
Normal file
0
console.txt
Normal file
74
history.txt
74
history.txt
@@ -1,74 +0,0 @@
|
|||||||
│ 1. Primary Request and Intent: │
|
|
||||||
│ - Fix issue where page scrolling in NoteListPage.vue triggers sliding events in NoteItem component │
|
|
||||||
│ - Add drag-and-drop sorting functionality for editor-image pictures in the rich text editor, showing drag handle │
|
|
||||||
│ (detail_note_item_image_move.png) when image is focused and hiding when blurred │
|
|
||||||
│ │
|
|
||||||
│ 2. Key Technical Concepts: │
|
|
||||||
│ - Vue 3 Composition API with script setup │
|
|
||||||
│ - Touch event handling and event propagation prevention │
|
|
||||||
│ - Direction detection for distinguishing scroll vs. slide gestures │
|
|
||||||
│ - CSS touch-action property for controlling touch behaviors │
|
|
||||||
│ - Drag and drop API for reordering elements │
|
|
||||||
│ - DOM manipulation with createElement and event listeners │
|
|
||||||
│ - Image resizing and positioning in rich text editor │
|
|
||||||
│ │
|
|
||||||
│ 3. Files and Code Sections: │
|
|
||||||
│ - E:\yuantao\SmartisanNote.VUE\src\components\NoteItem.vue │
|
|
||||||
│ - Enhanced event propagation prevention in touch handlers with stopImmediatePropagation() │
|
|
||||||
│ - Improved direction detection to better distinguish scrolling from sliding │
|
|
||||||
│ - Added CSS touch-action: pan-y to restrict touch behaviors │
|
|
||||||
│ - Code snippet: Added stopImmediatePropagation() calls in handleContainerTouchStart, handleContainerTouchMove, │
|
|
||||||
│ handleContainerTouchEnd, handleTouchStart, and handleTouchEnd functions │
|
|
||||||
│ - E:\yuantao\SmartisanNote.VUE\src\components\RichTextEditor.vue │
|
|
||||||
│ - Fixed drag handle creation and positioning for images │
|
|
||||||
│ - Improved drag and drop sorting functionality │
|
|
||||||
│ - Enhanced focus/mouse event handlers for showing/hiding drag handles │
|
|
||||||
│ - Fixed variable naming conflict in adjustExistingImages function │
|
|
||||||
│ - Code snippet: Modified insertImage function to create drag handles after image loading and improved event listener attachment │
|
|
||||||
│ - Code snippet: Updated adjustExistingImages function to properly handle existing images and their drag handles │
|
|
||||||
│ - Code snippet: Updated CSS styles for .image-drag-handle to ensure proper z-index and positioning │
|
|
||||||
│ │
|
|
||||||
│ 4. Errors and fixes: │
|
|
||||||
│ - Variable naming conflict error "[vue/compiler-sfc] Identifier 'images' has already been declared": │
|
|
||||||
│ - Identified duplicate variable declarations in adjustExistingImages function │
|
|
||||||
│ - Fixed by removing redundant 'existingImages' variable and reusing existing 'images' variable │
|
|
||||||
│ - User feedback confirmed the error was resolved │
|
|
||||||
│ - Drag handle visibility issue: │
|
|
||||||
│ - Identified that drag handles were not properly created or displayed │
|
|
||||||
│ - Fixed by ensuring drag handles are created after image loading and improving CSS styles │
|
|
||||||
│ - Enhanced event listener attachment to ensure proper show/hide functionality │
|
|
||||||
│ │
|
|
||||||
│ 5. Problem Solving: │
|
|
||||||
│ - Solved NoteItem sliding event propagation issue with event.stopPropagation() and direction detection │
|
|
||||||
│ - Implemented image drag handle functionality with proper creation, positioning, and event handling │
|
|
||||||
│ - Resolved variable naming conflict in RichTextEditor component │
|
|
||||||
│ - Fixed drag handle visibility issues by improving creation logic and CSS styles │
|
|
||||||
│ │
|
|
||||||
│ 6. All user messages: │
|
|
||||||
│ - "@src\pages\NoteListPage.vue 在页面上下滑动时不应该触发NoteItem组件内的滑动事件" │
|
|
||||||
│ - "现在依然存在这个问题" │
|
|
||||||
│ - "把所有输出、注释的语言改为中文" │
|
|
||||||
│ - "我并没有看到拖拽手柄" │
|
|
||||||
│ - "我并没有看到任何相关的调试信息" │
|
|
||||||
│ - "出现了错误:[vue/compiler-sfc] Identifier 'images' has already been declared. (1082:12)" │
|
|
||||||
│ │
|
|
||||||
│ 7. Pending Tasks: │
|
|
||||||
│ - Verify that the fixes have resolved both issues: │
|
|
||||||
│ - Page scrolling in NoteListPage.vue should no longer trigger NoteItem sliding events │
|
|
||||||
│ - Image drag handles should now properly display when images are focused or hovered │
|
|
||||||
│ │
|
|
||||||
│ 8. Current Work: │
|
|
||||||
│ Working on finalizing the fixes for both issues. Recently updated: │
|
|
||||||
│ 1. RichTextEditor.vue to properly create and display image drag handles with improved event handling │
|
|
||||||
│ 2. NoteItem.vue to better prevent event propagation during page scrolling │
|
|
||||||
│ The changes include: │
|
|
||||||
│ - Modified insertImage function to create drag handles after image loading │
|
|
||||||
│ - Updated adjustExistingImages function to properly handle existing images │
|
|
||||||
│ - Enhanced CSS styles for drag handles │
|
|
||||||
│ - Added stopImmediatePropagation() calls in NoteItem touch handlers │
|
|
||||||
│ │
|
|
||||||
│ 9. Optional Next Step: │
|
|
||||||
│ Test the implemented solutions to verify that: │
|
|
||||||
│ 1. Page scrolling in NoteListPage.vue no longer triggers NoteItem sliding events │
|
|
||||||
│ 2. Image drag handles properly display when images are focused or hovered │
|
|
||||||
│ The most recent work focused on "检查RichTextEditor.vue中图片拖拽手柄不显示的问题" and "确保拖拽手柄的显示逻辑正确处理焦点和鼠标事件". │
|
|
||||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "smartisannote.vue",
|
"name": "smartisannote.re",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "smartisannote.vue",
|
"name": "smartisannote.re",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3303,6 +3304,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/sortablejs": {
|
||||||
|
"version": "1.15.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||||
|
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -12807,6 +12814,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-draggable-plus": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/sortablejs": "^1.15.8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/sortablejs": "^1.15.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-hot-reload-api": {
|
"node_modules/vue-hot-reload-api": {
|
||||||
"version": "2.3.4",
|
"version": "2.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "smartisannote.vue",
|
"name": "smartisannote.re",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "锤子便签(重制版)",
|
"description": "锤子便签(重制版)",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
|
"vue-draggable-plus": "^0.6.0",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -18,11 +18,19 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
view,
|
img {
|
||||||
image,
|
user-select: none;
|
||||||
text {
|
-webkit-tap-highlight-color: transparent;
|
||||||
box-sizing: border-box;
|
outline-color: transparent;
|
||||||
flex-shrink: 0;
|
lighting-color: transparent;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background-color: #d3b9a7; /* 选中时的背景颜色 */
|
||||||
|
color: #ffffff; /* 选中时的文字颜色 */
|
||||||
|
}
|
||||||
|
img::selection {
|
||||||
|
background-color: transparent; /* 选中时的背景颜色 */
|
||||||
|
color: #ffffff; /* 选中时的文字颜色 */
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -13,15 +13,28 @@
|
|||||||
|
|
||||||
<!-- 右侧操作按钮 -->
|
<!-- 右侧操作按钮 -->
|
||||||
<!-- 新建便签 -->
|
<!-- 新建便签 -->
|
||||||
<!-- 新建便签 -->
|
|
||||||
<img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction('create')" />
|
<img v-if="actionIcon === 'create'" class="image_4" src="/assets/icons/drawable-xxhdpi/btn_create.png" @click="handleAction('create')" />
|
||||||
|
|
||||||
<div v-else-if="actionIcon === 'save'" class="code-fun-flex-row code-fun-items-center right-group">
|
<!-- 编辑模式 -->
|
||||||
|
<div v-else-if="actionIcon === 'edit'" class="code-fun-flex-row code-fun-items-center right-group">
|
||||||
<!-- 插入图片 -->
|
<!-- 插入图片 -->
|
||||||
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_pic.png" @click="handleAction('insertImage')" />
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_pic.png" @click="handleAction('insertImage')" />
|
||||||
<!-- 保存便签 -->
|
<!-- 保存便签 -->
|
||||||
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_save_notes.png" @click="handleAction('save')" />
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_save_notes.png" @click="handleAction('save')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览模式 -->
|
||||||
|
<div v-else-if="actionIcon === 'preview'" class="code-fun-flex-row code-fun-items-center right-group">
|
||||||
|
<!-- 删除便签 -->
|
||||||
|
<img
|
||||||
|
ref="deleteButtonRef"
|
||||||
|
class="image_4"
|
||||||
|
:src="deleteButtonFrame"
|
||||||
|
@click="handleAction('delete')"
|
||||||
|
/>
|
||||||
|
<!-- 分享便签 -->
|
||||||
|
<img class="image_4" src="/assets/icons/drawable-xxhdpi/btn_share_notes.png" @click="handleAction('share')" />
|
||||||
|
</div>
|
||||||
<!-- 占位符 -->
|
<!-- 占位符 -->
|
||||||
<div v-else class="image_4-placeholder"></div>
|
<div v-else class="image_4-placeholder"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +42,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, defineExpose } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -87,6 +100,9 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const localFolderExpanded = ref(false)
|
const localFolderExpanded = ref(false)
|
||||||
|
const deleteButtonRef = ref(null)
|
||||||
|
const deleteButtonFrame = ref('/assets/icons/drawable-xxhdpi/btn_delete_notes.png')
|
||||||
|
const isDeleteAnimating = ref(false)
|
||||||
|
|
||||||
const folderExpanded = computed(() => {
|
const folderExpanded = computed(() => {
|
||||||
// 优先使用父组件传递的isFolderExpanded状态,否则使用本地状态
|
// 优先使用父组件传递的isFolderExpanded状态,否则使用本地状态
|
||||||
@@ -124,6 +140,42 @@ const handleFolderToggle = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 播放删除按钮动画(只使用存在的帧)
|
||||||
|
const playDeleteAnimation = () => {
|
||||||
|
if (isDeleteAnimating.value) return
|
||||||
|
|
||||||
|
isDeleteAnimating.value = true
|
||||||
|
// 只使用存在的帧编号
|
||||||
|
const frames = [1, 6, 9, 10, 13, 16, 19, 21, 23, 25, 26, 27, 28, 29, 30]
|
||||||
|
let currentFrameIndex = 0
|
||||||
|
|
||||||
|
const playFrame = () => {
|
||||||
|
if (currentFrameIndex < frames.length) {
|
||||||
|
// 格式化帧编号(补零)
|
||||||
|
const frameNumber = frames[currentFrameIndex].toString().padStart(4, '0')
|
||||||
|
deleteButtonFrame.value = `/assets/icons/drawable-xxhdpi/title_bar_del_btn_mov_${frameNumber}.png`
|
||||||
|
currentFrameIndex++
|
||||||
|
|
||||||
|
// 使用setTimeout控制动画播放速度
|
||||||
|
setTimeout(playFrame, 50) // 约20fps,调整速度以适应帧数
|
||||||
|
} else {
|
||||||
|
// 动画播放完成,重置为默认图标
|
||||||
|
setTimeout(() => {
|
||||||
|
deleteButtonFrame.value = '/assets/icons/drawable-xxhdpi/btn_delete_notes.png'
|
||||||
|
isDeleteAnimating.value = false
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始播放动画
|
||||||
|
playFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露播放删除动画的方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
playDeleteAnimation
|
||||||
|
})
|
||||||
|
|
||||||
const handleLeftAction = () => {
|
const handleLeftAction = () => {
|
||||||
// 处理左侧图标点击事件
|
// 处理左侧图标点击事件
|
||||||
if (props.onLeftAction) {
|
if (props.onLeftAction) {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -139,6 +141,9 @@ const handlePress = () => {
|
|||||||
// 只有在未滑动状态下才触发点击事件
|
// 只有在未滑动状态下才触发点击事件
|
||||||
if (slideOffset.value === 0 && props.onPress) {
|
if (slideOffset.value === 0 && props.onPress) {
|
||||||
props.onPress()
|
props.onPress()
|
||||||
|
} else if (slideOffset.value !== 0) {
|
||||||
|
// 如果当前处于滑动状态,重置滑动状态(收回便签条)
|
||||||
|
resetSlideState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +174,21 @@ const handleDelete = () => {
|
|||||||
isSlided.value = false
|
isSlided.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置滑动状态
|
||||||
|
const resetSlideState = () => {
|
||||||
|
slideOffset.value = 0
|
||||||
|
isSliding.value = false
|
||||||
|
isSlided.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取滑动状态
|
||||||
|
const getSlideState = () => {
|
||||||
|
return isSlided.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({ resetSlideState, getSlideState })
|
||||||
|
|
||||||
// 触摸开始事件处理函数
|
// 触摸开始事件处理函数
|
||||||
// 记录触摸开始时的X坐标,用于计算滑动距离
|
// 记录触摸开始时的X坐标,用于计算滑动距离
|
||||||
const handleTouchStart = e => {
|
const handleTouchStart = e => {
|
||||||
@@ -216,7 +236,7 @@ const handleTouchMove = e => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
// 阻止父级滚动容器的滚动行为
|
// 阻止父级滚动容器的滚动行为
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
|
|
||||||
// 只有当滑动达到一定距离时才阻止页面滚动
|
// 只有当滑动达到一定距离时才阻止页面滚动
|
||||||
if (diffX > 5) {
|
if (diffX > 5) {
|
||||||
e.preventDefault() // 防止页面滚动
|
e.preventDefault() // 防止页面滚动
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
13
src/main.js
13
src/main.js
@@ -13,6 +13,9 @@ import FolderPage from './pages/FolderPage.vue'
|
|||||||
// 设置页面
|
// 设置页面
|
||||||
import SettingsPage from './pages/SettingsPage.vue'
|
import SettingsPage from './pages/SettingsPage.vue'
|
||||||
|
|
||||||
|
// 导入数据库初始化函数
|
||||||
|
import { initDB } from './utils/indexedDBStorage'
|
||||||
|
|
||||||
// 配置路由规则
|
// 配置路由规则
|
||||||
// 定义应用的所有路由路径和对应的组件
|
// 定义应用的所有路由路径和对应的组件
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -42,6 +45,16 @@ const router = createRouter({
|
|||||||
// 创建并挂载Vue应用实例
|
// 创建并挂载Vue应用实例
|
||||||
// 配置Pinia状态管理和Vue Router路由
|
// 配置Pinia状态管理和Vue Router路由
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 初始化数据库
|
||||||
|
initDB().then(() => {
|
||||||
|
console.log('数据库初始化成功')
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('数据库初始化失败:', error)
|
||||||
|
// 即使数据库初始化失败,也继续启动应用
|
||||||
|
// 这样可以确保应用在没有IndexedDB支持的环境中仍然可以运行
|
||||||
|
})
|
||||||
|
|
||||||
// 使用Pinia进行状态管理
|
// 使用Pinia进行状态管理
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
// 使用Vue Router进行路由管理
|
// 使用Vue Router进行路由管理
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<ion-page>
|
<ion-page>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Header :onBack="handleCancel" :onAction="handleAction" actionIcon="save" />
|
<!-- 头部:编辑模式 -->
|
||||||
|
<Header v-if="isEditorFocus" :onBack="handleCancel" :onAction="handleAction" actionIcon="edit" />
|
||||||
|
<!-- 头部:预览模式 -->
|
||||||
|
<Header v-else ref="headerRef" :onBack="handleCancel" :onAction="handleAction" actionIcon="preview" />
|
||||||
|
<section>
|
||||||
|
<!-- 顶部信息栏 -->
|
||||||
|
<div class="header-info">
|
||||||
|
<span class="edit-time">{{ formattedTime }}</span>
|
||||||
|
<span>|</span>
|
||||||
|
<span class="word-count">{{ wordCount }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 顶部信息栏 -->
|
<!-- 富文本编辑器 -->
|
||||||
<div class="header-info">
|
<div class="editor-container">
|
||||||
<span class="edit-time">{{ formattedTime }}</span>
|
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" @focus="handleEditorFocus" @blur="handleEditorBlur" class="rich-text-editor" />
|
||||||
<span>|</span>
|
</div>
|
||||||
<span class="word-count">{{ wordCount }}</span>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 富文本编辑器 -->
|
|
||||||
<div class="editor-container">
|
|
||||||
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" class="rich-text-editor" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ion-page>
|
</ion-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
import { ref, computed, onMounted, nextTick, watch, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '../stores/useAppStore'
|
import { useAppStore } from '../stores/useAppStore'
|
||||||
import Header from '../components/Header.vue'
|
import Header from '../components/Header.vue'
|
||||||
@@ -40,6 +44,9 @@ 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)
|
||||||
|
const headerRef = ref(null)
|
||||||
|
// 是否聚焦编辑器
|
||||||
|
const isEditorFocus = ref(false)
|
||||||
|
|
||||||
// 设置便签内容的函数
|
// 设置便签内容的函数
|
||||||
// 用于在编辑器中加载指定便签的内容
|
// 用于在编辑器中加载指定便签的内容
|
||||||
@@ -47,35 +54,27 @@ const setNoteContent = async noteId => {
|
|||||||
// 确保store数据已加载,如果便签列表为空则先加载数据
|
// 确保store数据已加载,如果便签列表为空则先加载数据
|
||||||
if (store.notes.length === 0) {
|
if (store.notes.length === 0) {
|
||||||
await store.loadData()
|
await store.loadData()
|
||||||
console.log('Store loaded, notes count:', store.notes.length)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从store中查找指定ID的便签
|
// 从store中查找指定ID的便签
|
||||||
const note = store.notes.find(n => n.id === noteId)
|
const note = store.notes.find(n => n.id === noteId)
|
||||||
console.log('Found note:', note)
|
|
||||||
|
|
||||||
// 确保编辑器已经初始化完成
|
// 确保编辑器已经初始化完成
|
||||||
await nextTick()
|
await nextTick()
|
||||||
console.log('Editor ref:', editorRef.value)
|
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
console.log('Setting content:', note.content)
|
|
||||||
// 无论editorRef是否可用,都先设置content的值作为备份
|
// 无论editorRef是否可用,都先设置content的值作为备份
|
||||||
content.value = note.content || ''
|
content.value = note.content || ''
|
||||||
// 如果editorRef可用,直接设置编辑器内容
|
// 如果editorRef可用,直接设置编辑器内容
|
||||||
if (editorRef.value) {
|
if (editorRef.value) {
|
||||||
editorRef.value.setContent(note.content || '')
|
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) {
|
if (noteId.value) {
|
||||||
@@ -87,7 +86,6 @@ onMounted(async () => {
|
|||||||
watch(
|
watch(
|
||||||
noteId,
|
noteId,
|
||||||
async newNoteId => {
|
async newNoteId => {
|
||||||
console.log('Note ID changed:', newNoteId)
|
|
||||||
if (newNoteId) {
|
if (newNoteId) {
|
||||||
await setNoteContent(newNoteId)
|
await setNoteContent(newNoteId)
|
||||||
}
|
}
|
||||||
@@ -148,13 +146,11 @@ const debounce = (func, delay) => {
|
|||||||
// 延迟300ms更新内容,避免用户输入时频繁触发更新
|
// 延迟300ms更新内容,避免用户输入时频繁触发更新
|
||||||
const debouncedHandleContentChange = debounce(newContent => {
|
const debouncedHandleContentChange = debounce(newContent => {
|
||||||
content.value = newContent
|
content.value = newContent
|
||||||
console.log('Content updated:', newContent)
|
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
// 监听编辑器内容变化
|
// 监听编辑器内容变化
|
||||||
// 当编辑器内容发生变化时调用此函数
|
// 当编辑器内容发生变化时调用此函数
|
||||||
const handleContentChange = newContent => {
|
const handleContentChange = newContent => {
|
||||||
console.log('Editor content changed:', newContent)
|
|
||||||
debouncedHandleContentChange(newContent)
|
debouncedHandleContentChange(newContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,53 +177,123 @@ const handleSave = async () => {
|
|||||||
// 获取编辑器中的实际内容
|
// 获取编辑器中的实际内容
|
||||||
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
if (isEditing && existingNote) {
|
// 检查内容是否为空
|
||||||
// Update existing note
|
if (isContentEmpty(editorContent)) {
|
||||||
|
// 如果是编辑模式且内容为空,则删除便签
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('空便签已删除')
|
||||||
|
// 删除后返回便签列表页面
|
||||||
|
router.push('/notes')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (isEditing && existingNote) {
|
||||||
|
// 更新现有便签
|
||||||
await store.updateNote(noteId.value, {
|
await store.updateNote(noteId.value, {
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
})
|
})
|
||||||
|
console.log('便签已保存')
|
||||||
} else {
|
} else {
|
||||||
// Create new note
|
// 创建新便签
|
||||||
await store.addNote({
|
await store.addNote({
|
||||||
content: editorContent,
|
content: editorContent,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
})
|
})
|
||||||
|
console.log('新便签已创建')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
// 保存后切换到预览模式(失去编辑器焦点)
|
||||||
router.push('/notes')
|
isEditorFocus.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In a full implementation, show an alert or toast
|
// In a full implementation, show an alert or toast
|
||||||
console.log('Save error: Failed to save note. Please try again.')
|
console.log('Save error: Failed to save note. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理取消
|
// 检查内容是否为空(无实质性内容)
|
||||||
const handleCancel = () => {
|
const isContentEmpty = content => {
|
||||||
// Check if there are unsaved changes
|
if (!content) return true
|
||||||
const hasUnsavedChanges = content.value !== (existingNote?.content || '')
|
|
||||||
|
|
||||||
if (hasUnsavedChanges) {
|
// 检查是否包含图片元素
|
||||||
showAlert.value = true
|
const hasImages = /<img[^>]*>|<div[^>]*class="[^"]*editor-image[^"]*"[^>]*>/.test(content)
|
||||||
} else {
|
if (hasImages) return false
|
||||||
router.push('/notes')
|
|
||||||
|
// 移除HTML标签和空白字符后检查是否为空
|
||||||
|
const plainText = content.replace(/<[^>]*>/g, '').trim()
|
||||||
|
if (plainText === '') return true
|
||||||
|
|
||||||
|
// 检查是否只有空的HTML元素
|
||||||
|
const strippedContent = content
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.replace(/<br>/g, '')
|
||||||
|
.replace(/<br\/>/g, '')
|
||||||
|
if (strippedContent === '<p></p>' || strippedContent === '<div></div>' || strippedContent === '') return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动保存便签(仅在非主动保存操作时调用)
|
||||||
|
const autoSaveNote = async () => {
|
||||||
|
try {
|
||||||
|
// 获取编辑器中的实际内容
|
||||||
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
// 检查内容是否为空,如果为空则删除便签
|
||||||
|
if (isContentEmpty(editorContent)) {
|
||||||
|
// 删除便签
|
||||||
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('空便签已自动删除')
|
||||||
|
} else if (editorContent !== (existingNote?.content || '')) {
|
||||||
|
// 更新现有便签(仅当内容有变化时)
|
||||||
|
await store.updateNote(noteId.value, {
|
||||||
|
content: editorContent,
|
||||||
|
})
|
||||||
|
console.log('便签已自动保存')
|
||||||
|
}
|
||||||
|
} else if (!isContentEmpty(editorContent)) {
|
||||||
|
// 创建新便签(仅当有内容时)
|
||||||
|
// 检查是否已经存在相同内容的便签以避免重复创建
|
||||||
|
const existingNotes = store.notes.filter(n => n.content === editorContent && !n.isDeleted)
|
||||||
|
if (existingNotes.length === 0) {
|
||||||
|
await store.addNote({
|
||||||
|
content: editorContent,
|
||||||
|
isStarred: false,
|
||||||
|
})
|
||||||
|
console.log('新便签已自动保存')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('自动保存失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理取消
|
||||||
|
const handleCancel = async () => {
|
||||||
|
// 自动保存便签
|
||||||
|
await autoSaveNote()
|
||||||
|
|
||||||
|
// 直接导航回便签列表页面,因为已经处理了保存或删除逻辑
|
||||||
|
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
|
||||||
|
|
||||||
// Create new note
|
// 只有当有内容时才创建新便签
|
||||||
await store.addNote({
|
if (!isContentEmpty(editorContent)) {
|
||||||
content: editorContent,
|
await store.addNote({
|
||||||
isStarred: false,
|
content: editorContent,
|
||||||
})
|
isStarred: false,
|
||||||
|
})
|
||||||
|
console.log('新便签已创建')
|
||||||
|
}
|
||||||
|
|
||||||
// Navigate back to the previous screen
|
// 创建后切换到预览模式(失去编辑器焦点)
|
||||||
router.push('/notes')
|
isEditorFocus.value = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// In a full implementation, show an alert or toast
|
// In a full implementation, show an alert or toast
|
||||||
console.log('Create error: Failed to create note. Please try again.')
|
console.log('Create error: Failed to create note. Please try again.')
|
||||||
@@ -246,12 +312,92 @@ const handleAction = actionType => {
|
|||||||
// 通过editorRef调用RichTextEditor组件的方法来插入图片
|
// 通过editorRef调用RichTextEditor组件的方法来插入图片
|
||||||
editorRef.value.insertImage()
|
editorRef.value.insertImage()
|
||||||
}
|
}
|
||||||
|
} else if (actionType === 'delete') {
|
||||||
|
// 删除便签
|
||||||
|
handleDelete()
|
||||||
|
} else if (actionType === 'share') {
|
||||||
|
// 分享便签
|
||||||
|
handleShare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setShowAlert = value => {
|
const setShowAlert = value => {
|
||||||
showAlert.value = value
|
showAlert.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理编辑器获得焦点
|
||||||
|
const handleEditorFocus = () => {
|
||||||
|
isEditorFocus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑器失去焦点
|
||||||
|
const handleEditorBlur = () => {
|
||||||
|
isEditorFocus.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除便签
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (isEditing && existingNote) {
|
||||||
|
// 播放删除按钮动画
|
||||||
|
if (headerRef.value && headerRef.value.playDeleteAnimation) {
|
||||||
|
headerRef.value.playDeleteAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待动画播放完成后再执行删除操作
|
||||||
|
// 15帧 * 50ms = 750ms,再加上一些缓冲时间
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// 删除便签
|
||||||
|
await store.deleteNote(noteId.value)
|
||||||
|
console.log('便签已删除')
|
||||||
|
// 返回便签列表页面
|
||||||
|
router.push('/notes')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除便签失败:', error)
|
||||||
|
}
|
||||||
|
}, 800) // 等待约800ms让动画播放完成
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分享便签
|
||||||
|
const handleShare = () => {
|
||||||
|
// 获取编辑器中的实际内容
|
||||||
|
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
|
||||||
|
|
||||||
|
// 移除HTML标签,获取纯文本内容用于分享
|
||||||
|
const plainText = editorContent.replace(/<[^>]*>/g, '').trim()
|
||||||
|
|
||||||
|
if (plainText) {
|
||||||
|
// 在实际应用中,这里会调用设备的分享功能
|
||||||
|
// 为了演示,我们使用Web Share API(如果支持)
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator
|
||||||
|
.share({
|
||||||
|
title: '分享便签',
|
||||||
|
text: plainText,
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('分享取消或失败:', error)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 如果不支持Web Share API,可以复制到剪贴板
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(plainText)
|
||||||
|
.then(() => {
|
||||||
|
console.log('内容已复制到剪贴板')
|
||||||
|
// 在实际应用中,这里会显示一个提示消息
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('复制失败:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件卸载前自动保存或删除
|
||||||
|
onBeforeUnmount(async () => {
|
||||||
|
await autoSaveNote()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -260,12 +406,20 @@ const setShowAlert = value => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
|
section {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-container {
|
.editor-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: var(--background-card);
|
background-color: var(--background-card);
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-editor {
|
.rich-text-editor {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
:isFolderExpanded="isFolderExpanded"
|
:isFolderExpanded="isFolderExpanded"
|
||||||
:onTitlePress="handleFolderToggle"
|
:onTitlePress="handleFolderToggle"
|
||||||
slot="fixed" />
|
slot="fixed" />
|
||||||
|
|
||||||
<!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
|
<!-- 悬浮文件夹列表 - 使用绝对定位实现 -->
|
||||||
<div v-if="isFolderExpanded" class="folder-list" slot="fixed">
|
<div v-if="isFolderExpanded" class="folder-list" slot="fixed">
|
||||||
<FolderManage
|
<FolderManage
|
||||||
@@ -32,19 +32,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notes-container">
|
<div class="notes-container">
|
||||||
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
|
<transition-group name="note-list" tag="div" class="notes-list">
|
||||||
<NoteItem
|
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
|
||||||
:title="note.title"
|
<NoteItem
|
||||||
:content="note.content"
|
:ref="
|
||||||
:date="formatDate(note.updatedAt)"
|
el => {
|
||||||
:isStarred="note.isStarred"
|
if (el) noteItemRefs[note.id] = el
|
||||||
:isTop="note.isTop || false"
|
}
|
||||||
:hasImage="note.hasImage || false"
|
"
|
||||||
:onPress="() => handleNotePress(note.id)"
|
:content="note.content"
|
||||||
:onStarToggle="() => handleStarToggle(note.id)"
|
:date="formatDate(note.updatedAt)"
|
||||||
:onTopToggle="() => handleTopToggle(note.id)"
|
:isStarred="note.isStarred"
|
||||||
:onDelete="() => confirmDeleteNote(note.id)" />
|
:isTop="note.isTop || false"
|
||||||
</div>
|
:hasImage="note.hasImage || false"
|
||||||
|
:onPress="() => handleNotePress(note.id)"
|
||||||
|
:onStarToggle="() => handleStarToggle(note.id)"
|
||||||
|
:onTopToggle="() => handleTopToggle(note.id)"
|
||||||
|
:onDelete="() => confirmDeleteNote(note.id)" />
|
||||||
|
</div>
|
||||||
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +70,7 @@ import { IonContent, IonPage } from '@ionic/vue'
|
|||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const noteItemRefs = ref({})
|
||||||
|
|
||||||
// 页面挂载时加载初始数据
|
// 页面挂载时加载初始数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -73,7 +80,7 @@ onMounted(() => {
|
|||||||
// 加载预设的模拟数据
|
// 加载预设的模拟数据
|
||||||
store.loadMockData()
|
store.loadMockData()
|
||||||
} else {
|
} else {
|
||||||
// 从localStorage加载用户数据
|
// 从Storage加载用户数据
|
||||||
store.loadData()
|
store.loadData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -97,11 +104,12 @@ const trashNotesCount = computed(() => {
|
|||||||
// 根据当前文件夹过滤便签
|
// 根据当前文件夹过滤便签
|
||||||
const filteredNotes = computed(() => {
|
const filteredNotes = computed(() => {
|
||||||
// 预处理搜索查询,提高性能
|
// 预处理搜索查询,提高性能
|
||||||
const lowerCaseQuery = searchQuery.value.toLowerCase().trim()
|
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)
|
const matchesSearch =
|
||||||
|
!lowerCaseQuery || (note.title && typeof note.title === 'string' && note.title.toLowerCase().includes(lowerCaseQuery)) || (note.content && typeof note.content === 'string' && note.content.toLowerCase().includes(lowerCaseQuery))
|
||||||
|
|
||||||
if (!matchesSearch) return false
|
if (!matchesSearch) return false
|
||||||
|
|
||||||
@@ -168,6 +176,21 @@ const allNotesCount = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleNotePress = noteId => {
|
const handleNotePress = noteId => {
|
||||||
|
// 检查是否有便签条处于展开状态
|
||||||
|
let hasSlidedNote = false
|
||||||
|
Object.values(noteItemRefs.value).forEach(noteItem => {
|
||||||
|
// 注意:isSlided是ref值,需要通过.value访问
|
||||||
|
if (noteItem && noteItem.getSlideState()) {
|
||||||
|
hasSlidedNote = true
|
||||||
|
noteItem.resetSlideState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果有便签条处于展开状态,不跳转到编辑页面
|
||||||
|
if (hasSlidedNote) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 使用vue-router导航到编辑页面
|
// 使用vue-router导航到编辑页面
|
||||||
router.push(`/editor/${noteId}`)
|
router.push(`/editor/${noteId}`)
|
||||||
}
|
}
|
||||||
@@ -373,9 +396,35 @@ const notes = computed(() => store.notes)
|
|||||||
|
|
||||||
.notes-container {
|
.notes-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-list {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-item {
|
.note-item {
|
||||||
margin: 0.6rem 0;
|
margin: 0.6rem 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 便签列表动画 */
|
||||||
|
.note-list-enter-active,
|
||||||
|
.note-list-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-list-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-list-move {
|
||||||
|
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-list-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 1rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const store = useAppStore()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 页面挂载时加载初始数据
|
// 页面挂载时加载初始数据
|
||||||
// 从localStorage加载用户设置和便签数据
|
// 从Storage加载用户设置和便签数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.loadData()
|
store.loadData()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import * as storage from '../utils/storage'
|
import * as storage from '../utils/indexedDBStorage'
|
||||||
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
|
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,13 +48,13 @@ export const useAppStore = defineStore('app', {
|
|||||||
actions: {
|
actions: {
|
||||||
/**
|
/**
|
||||||
* 初始化数据
|
* 初始化数据
|
||||||
* 从localStorage加载便签、文件夹和设置数据
|
* 从Storage加载便签、文件夹和设置数据
|
||||||
* 如果没有数据则加载预设的mock数据
|
* 如果没有数据则加载预设的mock数据
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadData() {
|
async loadData() {
|
||||||
try {
|
try {
|
||||||
// 从localStorage加载数据
|
// 从Storage加载数据
|
||||||
const loadedNotes = await storage.getNotes()
|
const loadedNotes = await storage.getNotes()
|
||||||
const loadedFolders = await storage.getFolders()
|
const loadedFolders = await storage.getFolders()
|
||||||
const loadedSettings = await storage.getSettings()
|
const loadedSettings = await storage.getSettings()
|
||||||
@@ -81,92 +81,22 @@ 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 fixedYesterday = '2025-10-11T10:00:00.000Z';
|
|
||||||
const fixedTwoDaysAgo = '2025-10-10T10:00:00.000Z';
|
|
||||||
const fixedThreeDaysAgo = '2025-10-09T10:00:00.000Z';
|
|
||||||
const fixedFourDaysAgo = '2025-10-08T10:00:00.000Z';
|
|
||||||
const fixedFiveDaysAgo = '2025-10-07T10:00:00.000Z';
|
|
||||||
|
|
||||||
// 预设的便签示例数据
|
// 预设的便签示例数据 - 仅保留一条关于应用功能介绍和示例的便签
|
||||||
const mockNotes = [
|
const mockNotes = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
title: '欢迎使用锤子便签',
|
title: '欢迎使用锤子便签',
|
||||||
content: '这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。',
|
content: '<p>这是一个功能强大的便签应用,您可以在这里记录您的想法、待办事项等。</p><br><h2>功能介绍</h2><p>1. 创建和编辑便签<br>2. 为便签加星和置顶<br>3. 将便签分类到文件夹<br>4. 搜索便签内容<br>5. 回收站功能</p><br><h2>编辑器功能演示</h2><br><h2>标题格式</h2><p>点击标题按钮可创建居中的标题</p><br><h2>待办事项</h2><div class="todo-container"><div class="todo-icon"></div><div contenteditable="true" class="todo-content">这是一个待办事项</div></div><div class="todo-container"><div class="todo-icon completed"></div><div contenteditable="true" class="todo-content" style="color: var(--text-tertiary); text-decoration: line-through;">这是一个已完成的待办事项</div></div><br><h2>列表格式</h2><ul><li>无序列表项1</li><li>无序列表项2</li></ul><br><h2>文本格式</h2><p><strong>加粗文本</strong></p><br><h2>引用格式</h2><div class="quote-container"><div class="quote-icon"></div><div class="quote-content">这是一段引用文本<br>可以用来引用他人的话语</div></div><br><h2>图片</h2><p>点击图片按钮可以插入图片,长按图片可以拖拽排序</p>',
|
||||||
createdAt: fixedCurrentDate,
|
createdAt: fixedCurrentDate,
|
||||||
updatedAt: fixedCurrentDate,
|
updatedAt: fixedCurrentDate,
|
||||||
folderId: null,
|
folderId: null,
|
||||||
isStarred: true, // 加星便签
|
isStarred: true, // 加星便签
|
||||||
isTop: true, // 置顶便签
|
isTop: true, // 置顶便签
|
||||||
hasImage: false, // 不包含图片
|
|
||||||
isDeleted: false, // 未删除
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
title: '待办事项',
|
|
||||||
content: '1. 完成项目报告\n2. 购买 groceries\n3. 预约医生\n4. 给朋友打电话',
|
|
||||||
createdAt: fixedYesterday,
|
|
||||||
updatedAt: fixedYesterday,
|
|
||||||
folderId: null,
|
|
||||||
isStarred: true, // 加星便签
|
|
||||||
isTop: false, // 非置顶
|
|
||||||
hasImage: true, // 包含图片
|
hasImage: true, // 包含图片
|
||||||
isDeleted: false, // 未删除
|
isDeleted: false, // 未删除
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
}
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
title: '购物清单',
|
|
||||||
content: '苹果\n牛奶\n面包\n鸡蛋\n西红柿\n咖啡',
|
|
||||||
createdAt: fixedTwoDaysAgo,
|
|
||||||
updatedAt: fixedTwoDaysAgo,
|
|
||||||
folderId: null,
|
|
||||||
isStarred: false, // 非加星
|
|
||||||
isTop: false, // 非置顶
|
|
||||||
hasImage: false, // 不包含图片
|
|
||||||
isDeleted: false, // 未删除
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
title: '项目想法',
|
|
||||||
content: '1. 实现云同步功能\n2. 添加深色模式\n3. 支持Markdown语法\n4. 添加标签功能',
|
|
||||||
createdAt: fixedThreeDaysAgo,
|
|
||||||
updatedAt: fixedThreeDaysAgo,
|
|
||||||
folderId: null,
|
|
||||||
isStarred: false, // 非加星
|
|
||||||
isTop: false, // 非置顶
|
|
||||||
hasImage: false, // 不包含图片
|
|
||||||
isDeleted: false, // 未删除
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
title: '读书笔记',
|
|
||||||
content: '《Vue.js实战》\n- 组件化思想是Vue的核心\n- 理解响应式原理很重要\n- Pinia是Vue 3的推荐状态管理库',
|
|
||||||
createdAt: fixedFourDaysAgo,
|
|
||||||
updatedAt: fixedFourDaysAgo,
|
|
||||||
folderId: null,
|
|
||||||
isStarred: false, // 非加星
|
|
||||||
isTop: false, // 非置顶
|
|
||||||
hasImage: false, // 不包含图片
|
|
||||||
isDeleted: false, // 未删除
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
title: '已删除的便签',
|
|
||||||
content: '这是一条已删除的便签示例,应该只在回收站中显示。',
|
|
||||||
createdAt: fixedFiveDaysAgo,
|
|
||||||
updatedAt: fixedFiveDaysAgo,
|
|
||||||
folderId: null,
|
|
||||||
isStarred: false, // 非加星
|
|
||||||
isTop: false, // 非置顶
|
|
||||||
hasImage: false, // 不包含图片
|
|
||||||
isDeleted: true, // 已删除
|
|
||||||
deletedAt: fixedYesterday,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
// Mock folders - 使用固定的日期值
|
// Mock folders - 使用固定的日期值
|
||||||
@@ -201,14 +131,14 @@ export const useAppStore = defineStore('app', {
|
|||||||
this.folders = mockFolders
|
this.folders = mockFolders
|
||||||
this.settings = mockSettings
|
this.settings = mockSettings
|
||||||
|
|
||||||
// 保存到localStorage
|
// 保存到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)
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存便签数据到localStorage
|
* 保存便签数据到Storage
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async saveNotes() {
|
async saveNotes() {
|
||||||
@@ -220,7 +150,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存文件夹数据到localStorage
|
* 保存文件夹数据到Storage
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async saveFolders() {
|
async saveFolders() {
|
||||||
@@ -232,7 +162,7 @@ export const useAppStore = defineStore('app', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存设置数据到localStorage
|
* 保存设置数据到Storage
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
|||||||
475
src/utils/indexedDBStorage.js
Normal file
475
src/utils/indexedDBStorage.js
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
import { getCurrentDateTime, getTimestamp } from './dateUtils'
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
const DB_NAME = 'SmartisanNoteDB'
|
||||||
|
const DB_VERSION = 2 // 更新版本号以确保数据库重新创建
|
||||||
|
const NOTES_STORE = 'notes'
|
||||||
|
const FOLDERS_STORE = 'folders'
|
||||||
|
const SETTINGS_STORE = 'settings'
|
||||||
|
|
||||||
|
let db = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开数据库连接
|
||||||
|
* @returns {Promise<IDBDatabase>} 数据库实例
|
||||||
|
*/
|
||||||
|
const openDB = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (db) {
|
||||||
|
return resolve(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('无法打开数据库'))
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onsuccess = () => {
|
||||||
|
db = request.result
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const database = event.target.result
|
||||||
|
|
||||||
|
// 删除现有的对象存储(如果版本已更改)
|
||||||
|
if (event.oldVersion > 0) {
|
||||||
|
if (database.objectStoreNames.contains(NOTES_STORE)) {
|
||||||
|
database.deleteObjectStore(NOTES_STORE)
|
||||||
|
}
|
||||||
|
if (database.objectStoreNames.contains(FOLDERS_STORE)) {
|
||||||
|
database.deleteObjectStore(FOLDERS_STORE)
|
||||||
|
}
|
||||||
|
if (database.objectStoreNames.contains(SETTINGS_STORE)) {
|
||||||
|
database.deleteObjectStore(SETTINGS_STORE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建便签存储对象
|
||||||
|
const notesStore = database.createObjectStore(NOTES_STORE, { keyPath: 'id' })
|
||||||
|
notesStore.createIndex('folderId', 'folderId', { unique: false })
|
||||||
|
notesStore.createIndex('isStarred', 'isStarred', { unique: false })
|
||||||
|
notesStore.createIndex('isDeleted', 'isDeleted', { unique: false })
|
||||||
|
notesStore.createIndex('createdAt', 'createdAt', { unique: false })
|
||||||
|
notesStore.createIndex('updatedAt', 'updatedAt', { unique: false })
|
||||||
|
|
||||||
|
// 创建文件夹存储对象
|
||||||
|
database.createObjectStore(FOLDERS_STORE, { keyPath: 'id' })
|
||||||
|
|
||||||
|
// 创建设置存储对象
|
||||||
|
database.createObjectStore(SETTINGS_STORE)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储中获取数据
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @returns {Promise<Array>} 数据数组
|
||||||
|
*/
|
||||||
|
const getAllFromStore = async (storeName) => {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.getAll()
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(request.result || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error(`获取 ${storeName} 数据失败`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据到存储
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @param {Array} data - 要保存的数据数组
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const saveToStore = async (storeName, data) => {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
|
||||||
|
// 清除现有数据
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const clearRequest = store.clear()
|
||||||
|
clearRequest.onsuccess = () => resolve()
|
||||||
|
clearRequest.onerror = () => reject(new Error(`清除 ${storeName} 数据失败`))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加新数据
|
||||||
|
for (const item of data) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const addRequest = store.add(item)
|
||||||
|
addRequest.onsuccess = () => resolve()
|
||||||
|
addRequest.onerror = () => reject(new Error(`保存 ${storeName} 数据失败`))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储中获取单个项
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @param {string} id - 项的ID
|
||||||
|
* @returns {Promise<Object|null>} 项对象或null
|
||||||
|
*/
|
||||||
|
const getFromStore = async (storeName, id) => {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readonly')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.get(id)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(request.result || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error(`获取 ${storeName} 项失败`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向存储中添加项
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @param {Object} item - 要添加的项
|
||||||
|
* @returns {Promise<Object>} 添加的项
|
||||||
|
*/
|
||||||
|
const addToStore = async (storeName, item) => {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.add(item)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error(`添加 ${storeName} 项失败`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新存储中的项
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @param {string} id - 项的ID
|
||||||
|
* @param {Object} updates - 要更新的属性对象
|
||||||
|
* @returns {Promise<Object|null>} 更新后的项或null
|
||||||
|
*/
|
||||||
|
const updateInStore = async (storeName, id, updates) => {
|
||||||
|
const item = await getFromStore(storeName, id)
|
||||||
|
if (!item) return null
|
||||||
|
|
||||||
|
const updatedItem = { ...item, ...updates }
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.put(updatedItem)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(updatedItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error(`更新 ${storeName} 项失败`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储中删除项
|
||||||
|
* @param {string} storeName - 存储名称
|
||||||
|
* @param {string} id - 要删除的项的ID
|
||||||
|
* @returns {Promise<boolean>} 删除成功返回true,失败返回false
|
||||||
|
*/
|
||||||
|
const deleteFromStore = async (storeName, id) => {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([storeName], 'readwrite')
|
||||||
|
const store = transaction.objectStore(storeName)
|
||||||
|
const request = store.delete(id)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error(`删除 ${storeName} 项失败`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 便签操作函数
|
||||||
|
// 提供便签的增删改查功能
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有便签数据
|
||||||
|
* 从IndexedDB中读取便签数据
|
||||||
|
* @returns {Promise<Array>} 便签数组
|
||||||
|
*/
|
||||||
|
export const getNotes = async () => {
|
||||||
|
try {
|
||||||
|
const notes = await getAllFromStore(NOTES_STORE)
|
||||||
|
return ensureNotesDefaults(notes)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting notes:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存便签数据
|
||||||
|
* 将便签数组保存到IndexedDB
|
||||||
|
* @param {Array} notes - 便签数组
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const saveNotes = async (notes) => {
|
||||||
|
try {
|
||||||
|
await saveToStore(NOTES_STORE, notes)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving notes:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加新便签
|
||||||
|
* 创建一个新的便签对象并添加到便签列表中
|
||||||
|
* @param {Object} note - 便签对象,包含便签内容和其他属性
|
||||||
|
* @returns {Promise<Object>} 新创建的便签对象
|
||||||
|
*/
|
||||||
|
export const addNote = async (note) => {
|
||||||
|
try {
|
||||||
|
// 创建新的便签对象,添加必要的属性
|
||||||
|
const newNote = {
|
||||||
|
title: note.title || '',
|
||||||
|
content: note.content || '',
|
||||||
|
id: note.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||||
|
createdAt: note.createdAt || getCurrentDateTime(), // 创建时间
|
||||||
|
updatedAt: note.updatedAt || getCurrentDateTime(), // 更新时间
|
||||||
|
isStarred: note.isStarred || false, // 是否加星
|
||||||
|
isTop: note.isTop || false, // 是否置顶
|
||||||
|
hasImage: note.hasImage || false, // 是否包含图片
|
||||||
|
isDeleted: note.isDeleted || false, // 是否已删除
|
||||||
|
deletedAt: note.deletedAt || null, // 删除时间
|
||||||
|
folderId: note.folderId || null, // 文件夹ID
|
||||||
|
...note
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到存储
|
||||||
|
await addToStore(NOTES_STORE, newNote)
|
||||||
|
return newNote
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding note:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新便签
|
||||||
|
* 根据ID查找并更新便签信息
|
||||||
|
* @param {string} id - 便签ID
|
||||||
|
* @param {Object} updates - 要更新的属性对象
|
||||||
|
* @returns {Promise<Object|null>} 更新后的便签对象,如果未找到则返回null
|
||||||
|
*/
|
||||||
|
export const updateNote = async (id, updates) => {
|
||||||
|
try {
|
||||||
|
// 更新便签并保存
|
||||||
|
const updatedNote = await updateInStore(NOTES_STORE, id, {
|
||||||
|
...updates,
|
||||||
|
updatedAt: getCurrentDateTime() // 更新最后修改时间
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedNote
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating note:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除便签
|
||||||
|
* 根据ID从便签列表中移除便签
|
||||||
|
* @param {string} id - 要删除的便签ID
|
||||||
|
* @returns {Promise<boolean>} 删除成功返回true,未找到便签返回false
|
||||||
|
*/
|
||||||
|
export const deleteNote = async (id) => {
|
||||||
|
try {
|
||||||
|
// 从存储中删除
|
||||||
|
const result = await deleteFromStore(NOTES_STORE, id)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting note:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件夹操作函数
|
||||||
|
// 提供文件夹的增删改查功能
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有文件夹数据
|
||||||
|
* 从IndexedDB中读取文件夹数据
|
||||||
|
* @returns {Promise<Array>} 文件夹数组
|
||||||
|
*/
|
||||||
|
export const getFolders = async () => {
|
||||||
|
try {
|
||||||
|
const folders = await getAllFromStore(FOLDERS_STORE)
|
||||||
|
return ensureFoldersDefaults(folders)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting folders:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文件夹数据
|
||||||
|
* 将文件夹数组保存到IndexedDB
|
||||||
|
* @param {Array} folders - 文件夹数组
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const saveFolders = async (folders) => {
|
||||||
|
try {
|
||||||
|
await saveToStore(FOLDERS_STORE, folders)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving folders:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加新文件夹
|
||||||
|
* 创建一个新的文件夹对象并添加到文件夹列表中
|
||||||
|
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性
|
||||||
|
* @returns {Promise<Object>} 新创建的文件夹对象
|
||||||
|
*/
|
||||||
|
export const addFolder = async (folder) => {
|
||||||
|
try {
|
||||||
|
// 创建新的文件夹对象,添加必要的属性
|
||||||
|
const newFolder = {
|
||||||
|
name: folder.name || '',
|
||||||
|
id: folder.id || getTimestamp().toString(), // 使用时间戳生成唯一ID
|
||||||
|
createdAt: folder.createdAt || getCurrentDateTime(), // 创建时间
|
||||||
|
...folder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到存储
|
||||||
|
await addToStore(FOLDERS_STORE, newFolder)
|
||||||
|
return newFolder
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding folder:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置操作函数
|
||||||
|
// 提供应用设置的读取和保存功能
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用设置
|
||||||
|
* 从IndexedDB中读取设置数据
|
||||||
|
* @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置
|
||||||
|
*/
|
||||||
|
export const getSettings = async () => {
|
||||||
|
try {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([SETTINGS_STORE], 'readonly')
|
||||||
|
const store = transaction.objectStore(SETTINGS_STORE)
|
||||||
|
const request = store.get('settings')
|
||||||
|
|
||||||
|
const settings = await new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => {
|
||||||
|
resolve(request.result || { cloudSync: false, darkMode: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
reject(new Error('获取设置失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return settings
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting settings:', error)
|
||||||
|
// 出错时返回默认设置
|
||||||
|
return { cloudSync: false, darkMode: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存应用设置
|
||||||
|
* 将设置对象保存到IndexedDB
|
||||||
|
* @param {Object} settings - 设置对象
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const saveSettings = async (settings) => {
|
||||||
|
try {
|
||||||
|
const database = await openDB()
|
||||||
|
const transaction = database.transaction([SETTINGS_STORE], 'readwrite')
|
||||||
|
const store = transaction.objectStore(SETTINGS_STORE)
|
||||||
|
const request = store.put(settings, 'settings')
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
request.onerror = () => reject(new Error('保存设置失败'))
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving settings:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保数据有默认值
|
||||||
|
* @param {Array} notes - 便签数组
|
||||||
|
* @returns {Array} 处理后的便签数组
|
||||||
|
*/
|
||||||
|
const ensureNotesDefaults = (notes) => {
|
||||||
|
return notes.map(note => ({
|
||||||
|
title: note.title || '',
|
||||||
|
content: note.content || '',
|
||||||
|
id: note.id,
|
||||||
|
createdAt: note.createdAt,
|
||||||
|
updatedAt: note.updatedAt,
|
||||||
|
isStarred: note.isStarred || false,
|
||||||
|
isTop: note.isTop || false,
|
||||||
|
hasImage: note.hasImage || false,
|
||||||
|
isDeleted: note.isDeleted || false,
|
||||||
|
deletedAt: note.deletedAt || null,
|
||||||
|
folderId: note.folderId || null,
|
||||||
|
...note
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保文件夹数据有默认值
|
||||||
|
* @param {Array} folders - 文件夹数组
|
||||||
|
* @returns {Array} 处理后的文件夹数组
|
||||||
|
*/
|
||||||
|
const ensureFoldersDefaults = (folders) => {
|
||||||
|
return folders.map(folder => ({
|
||||||
|
name: folder.name || '',
|
||||||
|
id: folder.id,
|
||||||
|
createdAt: folder.createdAt,
|
||||||
|
...folder
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据库
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const initDB = async () => {
|
||||||
|
try {
|
||||||
|
await openDB()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error initializing database:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { getCurrentDateTime, getTimestamp } from './dateUtils'
|
|
||||||
|
|
||||||
// 本地存储键名常量
|
|
||||||
// 用于在localStorage中标识不同类型的数据
|
|
||||||
const NOTES_KEY = 'notes'; // 便签数据键名
|
|
||||||
const FOLDERS_KEY = 'folders'; // 文件夹数据键名
|
|
||||||
const SETTINGS_KEY = 'settings'; // 设置数据键名
|
|
||||||
|
|
||||||
// 便签操作函数
|
|
||||||
// 提供便签的增删改查功能
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有便签数据
|
|
||||||
* 从localStorage中读取便签数据并解析为JavaScript对象
|
|
||||||
* @returns {Promise<Array>} 便签数组,如果读取失败则返回空数组
|
|
||||||
*/
|
|
||||||
export const getNotes = async () => {
|
|
||||||
try {
|
|
||||||
const notesJson = localStorage.getItem(NOTES_KEY);
|
|
||||||
return notesJson ? JSON.parse(notesJson) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting notes:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存便签数据
|
|
||||||
* 将便签数组转换为JSON字符串并保存到localStorage
|
|
||||||
* @param {Array} notes - 便签数组
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export const saveNotes = async (notes) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(NOTES_KEY, JSON.stringify(notes));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving notes:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加新便签
|
|
||||||
* 创建一个新的便签对象并添加到便签列表中
|
|
||||||
* @param {Object} note - 便签对象,包含便签内容和其他属性
|
|
||||||
* @returns {Promise<Object>} 新创建的便签对象
|
|
||||||
*/
|
|
||||||
export const addNote = async (note) => {
|
|
||||||
// 创建新的便签对象,添加必要的属性
|
|
||||||
const newNote = {
|
|
||||||
...note,
|
|
||||||
id: getTimestamp().toString(), // 使用时间戳生成唯一ID
|
|
||||||
createdAt: getCurrentDateTime(), // 创建时间
|
|
||||||
updatedAt: getCurrentDateTime(), // 更新时间
|
|
||||||
isStarred: note.isStarred || false, // 是否加星
|
|
||||||
isTop: note.isTop || false, // 是否置顶
|
|
||||||
hasImage: note.hasImage || false, // 是否包含图片
|
|
||||||
isDeleted: note.isDeleted || false, // 是否已删除
|
|
||||||
deletedAt: note.deletedAt || null // 删除时间
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取现有便签列表,添加新便签并保存
|
|
||||||
const notes = await getNotes();
|
|
||||||
notes.push(newNote);
|
|
||||||
await saveNotes(notes);
|
|
||||||
|
|
||||||
return newNote;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新便签
|
|
||||||
* 根据ID查找并更新便签信息
|
|
||||||
* @param {string} id - 便签ID
|
|
||||||
* @param {Object} updates - 要更新的属性对象
|
|
||||||
* @returns {Promise<Object|null>} 更新后的便签对象,如果未找到则返回null
|
|
||||||
*/
|
|
||||||
export const updateNote = async (id, updates) => {
|
|
||||||
// 获取所有便签并查找要更新的便签
|
|
||||||
const notes = await getNotes();
|
|
||||||
const index = notes.findIndex(note => note.id === id);
|
|
||||||
|
|
||||||
// 如果未找到指定ID的便签,返回null
|
|
||||||
if (index === -1) return null;
|
|
||||||
|
|
||||||
// 创建更新后的便签对象
|
|
||||||
const updatedNote = {
|
|
||||||
...notes[index],
|
|
||||||
...updates,
|
|
||||||
updatedAt: getCurrentDateTime(), // 更新最后修改时间
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新便签列表并保存
|
|
||||||
notes[index] = updatedNote;
|
|
||||||
await saveNotes(notes);
|
|
||||||
|
|
||||||
return updatedNote;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除便签
|
|
||||||
* 根据ID从便签列表中移除便签
|
|
||||||
* @param {string} id - 要删除的便签ID
|
|
||||||
* @returns {Promise<boolean>} 删除成功返回true,未找到便签返回false
|
|
||||||
*/
|
|
||||||
export const deleteNote = async (id) => {
|
|
||||||
// 获取所有便签并过滤掉要删除的便签
|
|
||||||
const notes = await getNotes();
|
|
||||||
const filteredNotes = notes.filter(note => note.id !== id);
|
|
||||||
|
|
||||||
// 如果便签数量没有变化,说明未找到要删除的便签
|
|
||||||
if (notes.length === filteredNotes.length) return false;
|
|
||||||
|
|
||||||
// 保存更新后的便签列表
|
|
||||||
await saveNotes(filteredNotes);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文件夹操作函数
|
|
||||||
// 提供文件夹的增删改查功能
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有文件夹数据
|
|
||||||
* 从localStorage中读取文件夹数据并解析为JavaScript对象
|
|
||||||
* @returns {Promise<Array>} 文件夹数组,如果读取失败则返回空数组
|
|
||||||
*/
|
|
||||||
export const getFolders = async () => {
|
|
||||||
try {
|
|
||||||
const foldersJson = localStorage.getItem(FOLDERS_KEY);
|
|
||||||
return foldersJson ? JSON.parse(foldersJson) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting folders:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存文件夹数据
|
|
||||||
* 将文件夹数组转换为JSON字符串并保存到localStorage
|
|
||||||
* @param {Array} folders - 文件夹数组
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export const saveFolders = async (folders) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(FOLDERS_KEY, JSON.stringify(folders));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving folders:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加新文件夹
|
|
||||||
* 创建一个新的文件夹对象并添加到文件夹列表中
|
|
||||||
* @param {Object} folder - 文件夹对象,包含文件夹名称等属性
|
|
||||||
* @returns {Promise<Object>} 新创建的文件夹对象
|
|
||||||
*/
|
|
||||||
export const addFolder = async (folder) => {
|
|
||||||
// 创建新的文件夹对象,添加必要的属性
|
|
||||||
const newFolder = {
|
|
||||||
...folder,
|
|
||||||
id: getTimestamp().toString(), // 使用时间戳生成唯一ID
|
|
||||||
createdAt: getCurrentDateTime(), // 创建时间
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取现有文件夹列表,添加新文件夹并保存
|
|
||||||
const folders = await getFolders();
|
|
||||||
folders.push(newFolder);
|
|
||||||
await saveFolders(folders);
|
|
||||||
|
|
||||||
return newFolder;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置操作函数
|
|
||||||
// 提供应用设置的读取和保存功能
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取应用设置
|
|
||||||
* 从localStorage中读取设置数据并解析为JavaScript对象
|
|
||||||
* @returns {Promise<Object>} 设置对象,如果读取失败则返回默认设置
|
|
||||||
*/
|
|
||||||
export const getSettings = async () => {
|
|
||||||
try {
|
|
||||||
const settingsJson = localStorage.getItem(SETTINGS_KEY);
|
|
||||||
// 如果没有保存的设置,返回默认设置
|
|
||||||
return settingsJson ? JSON.parse(settingsJson) : { cloudSync: false, darkMode: false };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting settings:', error);
|
|
||||||
// 出错时返回默认设置
|
|
||||||
return { cloudSync: false, darkMode: false };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存应用设置
|
|
||||||
* 将设置对象转换为JSON字符串并保存到localStorage
|
|
||||||
* @param {Object} settings - 设置对象
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
export const saveSettings = async (settings) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving settings:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user