future #10

Merged
袁涛 merged 37 commits from future into main 2025-10-17 14:46:33 +08:00
18 changed files with 1977 additions and 1323 deletions

1
.nvmdrc Normal file
View File

@@ -0,0 +1 @@
20.0.0

294
IFLOW.md
View File

@@ -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)
* **构建工具**: Vite
* **路由管理**: vue-router
* **状态管理**: Pinia (Vue 3 状态管理库)
* **UI 库**: 原生 CSS使用了锤子便签的经典配色方案定义在 `index.html` 的 CSS 变量中)
* **移动端支持**: Capacitor (用于构建 Android/iOS 应用)
* **样式预处理器**: Less
* **代码语言**: JavaScript (ES6+)
* **UI 组件库**: Ionic Framework
* **日期处理**: moment.js
* **框架**: Vue 3 (Composition API)
* **构建工具**: Vite
* **状态管理**: Pinia
* **路由**: Vue Router
* **UI 组件库**: Ionic Vue (部分使用)
* **PWA 支持**: vite-plugin-pwa
* **本地存储**: IndexedDB (通过 `src/utils/indexedDBStorage.js` 封装)
* **CSS 预处理器**: Less
## 项目结构
```
.
├── android/ # Capacitor Android 项目文件
├── dist/ # 构建后的生产文件
├── node_modules/ # 项目依赖
├── public/ # 静态资源目录
├── src/
│ ├── common/ # 全局样式和通用工具
│ ├── components/ # 可复用的 Vue 组件 (Header, NoteItem, FolderItem)
│ ├── pages/ # 页面级别的 Vue 组件 (NoteList, NoteEditor, Folder, Settings)
│ ├── stores/ # Pinia 状态管理 stores
│ └── useAppStore.js # 全局状态管理 store
│ ├── utils/ # 工具函数
├── dateUtils.js # 日期处理工具,基于 moment.js
│ │ └── storage.js # localStorage 封装,负责数据的读写
│ ├── App.vue # 根组件
│ └── main.js # 应用入口初始化路由、Pinia 和挂载
── index.html # HTML 模板,包含 CSS 变量定义
├── package.json # 项目元数据和脚本命令
├── vite.config.js # Vite 构建配置
└── capacitor.config.json # Capacitor 配置文件
├── android/ # Capacitor Android 项目文件
├── public/ # 静态资源目录 (图标等)
├── src/ # 源代码目录
│ ├── App.vue # 根组件
│ ├── main.js # 应用入口文件
│ ├── common/ # 通用样式
│ ├── components/ # 可复用的 UI 组件
│ ├── pages/ # 页面组件
│ ├── stores/ # Pinia 状态管理
│ └── utils/ # 工具函数
├── index.html # 应用入口 HTML 文件
├── .nvmdrc # node.js 版本
├── update.txt # 更新日志
├── 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`
* 使用 Codefun 原子类样式,用于快速布局。
* 样式规范应遵循项目中已有的风格。
* 使用 Less 作为 CSS 预处理器。
```bash
npm run dev
```
## JavaScript
这将在 `http://localhost:3000` 启动应用。
* 严格遵循ES6规范。
* 遵循JavaScript函数式编程范式。
* 方法类函数应该使用 `function` 进行定义。
* 避免出现超过4个以上的 `ref`超过4个则使用 `reactive`
* 全局变量都集中放置于代码顶部。
* 变量名使用小驼峰命名法。
* 常量名使用全大写。
* 状态类变量命名参考 `isLogin``isOpen`
* 事件类方法命名参考 `onClick``onSelect`
* 变量都应该写有注释说明、类型说明。
* `Promise` 方法使用 `async` `await` 写法,并进行容错处理。
* 字符串拼接使用ES6的模板语法。
* JavaScript规范应遵循项目中已有的风格。
### 构建
## 组件
构建标准 Web 应用:
* 全局组件放在 `components/` 目录下。
* 页面独立组件放在页面根目录下的 `components/`
* 每个组件应该附带 `README.MD` 文档。
* 组件编写应遵循项目中已有的风格。
```bash
npm run build
```
### Header 组件
* **动态按钮**: 根据页面状态显示不同的操作按钮(新建、保存、插入图片)
* **文件夹管理**: 支持文件夹展开/收起功能
构建 PWA 应用:
### NoteItem 组件
* **滑动交互**: 支持右滑显示删除按钮,带有阻尼效果
* **状态切换**: 支持星标和置顶状态的切换
* **视觉反馈**: 滑动时便签夹会切换状态,提供直观的交互反馈
* **日期显示**: 显示格式化后的便签更新时间
```bash
npm run build:pwa
```
### RichTextEditor 组件
* **富文本编辑**: 支持多种文本格式(加粗、居中、待办事项、列表、标题、引用)
* **图片插入**: 支持插入图片功能
* **工具栏**: 提供浮动工具栏,支持格式化操作
构建所有版本 (标准 + PWA):
## 页面
```bash
npm run build:all
```
* 页面使用 Composition API (setup语法糖) 编写。
* 注释、结构规范应遵循项目中已有的风格。
### 部署 PWA
### NoteListPage
* **便签列表**: 显示所有便签,支持置顶便签优先显示
* **文件夹管理**: 支持文件夹的展开和切换
* **搜索功能**: 提供便签搜索功能
* **交互反馈**: 显示便签总数和置顶便签数量
* **智能日期显示**: 根据时间范围显示不同的日期格式
* 今天:显示为 "今天 下午 4:00"
* 昨天:显示为 "昨天 下午 4:00"
* 超过两天但小于一周:显示为 "星期一 10/8 上午 3:00"
* 超过一周但小于一年:显示为 "10天前 9/20 下午 2:00"
* 超过一年:显示为 "635天前 2024/8/10 上午 9:00"
构建 PWA 并上传到服务器:
### NoteEditorPage
* **编辑模式**: 支持新建和编辑便签
* **富文本编辑**: 集成RichTextEditor组件支持丰富的文本格式
* **图片插入**: 支持通过工具栏插入图片
* **状态管理**: 根据路由参数判断是新建还是编辑模式
* **智能日期显示**: 根据时间范围显示不同的日期格式
* 今天:显示为 "今天 下午 4:00"
* 昨天:显示为 "昨天 下午 4:00"
* 超过两天但小于一个月:显示为 "10/8 上午 3:00"
* 超过一个月:显示为 "2024/8/10 上午 9:00"
```bash
npm run deploy:pwa
```
## 状态管理 (Pinia)
这将执行 `vite build --mode pwa` 并运行 `upload-pwa.js` 脚本。
项目现在使用 Pinia 作为状态管理解决方案,主要特点包括:
### Android 应用
* **Store 定义**: 在 `src/stores/useAppStore.js` 中定义了全局状态 store
* **状态结构**: 包含 notes、folders 和 settings 三个主要状态
* **Getters**: 提供了计算属性如 starredNotesCount 和 allNotesCount
* **Actions**: 包含所有状态变更操作,如 addNote、updateNote、deleteNote 等
* **数据持久化**: 通过 storage.js 工具函数与 localStorage 进行数据交互
* **Mock 数据**: 支持加载预设的 mock 数据用于开发和演示
* **使用方式**: 在组件中通过 `const store = useAppStore()` 来访问状态和方法
运行 Android 应用:
## 新增功能特性
```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
View File

@@ -1,103 +0,0 @@
# 锤子便签(重制版)
![项目截图](public/icons/icon-192.png)
这是一个基于 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
View File

View File

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

@@ -1,11 +1,11 @@
{
"name": "smartisannote.vue",
"name": "smartisannote.re",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "smartisannote.vue",
"name": "smartisannote.re",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
@@ -21,6 +21,7 @@
"moment": "^2.30.1",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1"
},
"devDependencies": {
@@ -3303,6 +3304,12 @@
"@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": {
"version": "2.0.7",
"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": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",

View File

@@ -1,5 +1,5 @@
{
"name": "smartisannote.vue",
"name": "smartisannote.re",
"version": "1.0.0",
"description": "锤子便签(重制版)",
"main": "index.js",
@@ -26,6 +26,7 @@
"moment": "^2.30.1",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-draggable-plus": "^0.6.0",
"vue-router": "^4.5.1"
},
"devDependencies": {

View File

@@ -18,11 +18,19 @@ body {
-moz-osx-font-smoothing: grayscale;
}
view,
image,
text {
box-sizing: border-box;
flex-shrink: 0;
img {
user-select: none;
-webkit-tap-highlight-color: transparent;
outline-color: transparent;
lighting-color: transparent;
}
::selection {
background-color: #d3b9a7; /* 选中时的背景颜色 */
color: #ffffff; /* 选中时的文字颜色 */
}
img::selection {
background-color: transparent; /* 选中时的背景颜色 */
color: #ffffff; /* 选中时的文字颜色 */
}
button {
border: none;

View File

@@ -13,15 +13,28 @@
<!-- 右侧操作按钮 -->
<!-- 新建便签 -->
<!-- 新建便签 -->
<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_save_notes.png" @click="handleAction('save')" />
</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>
@@ -29,7 +42,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, defineExpose } from 'vue'
const props = defineProps({
title: {
@@ -87,6 +100,9 @@ const props = defineProps({
})
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(() => {
// 优先使用父组件传递的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 = () => {
// 处理左侧图标点击事件
if (props.onLeftAction) {

View File

@@ -31,6 +31,8 @@
<script setup>
import { computed, ref } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
content: {
type: String,
@@ -139,6 +141,9 @@ const handlePress = () => {
// 只有在未滑动状态下才触发点击事件
if (slideOffset.value === 0 && props.onPress) {
props.onPress()
} else if (slideOffset.value !== 0) {
// 如果当前处于滑动状态,重置滑动状态(收回便签条)
resetSlideState()
}
}
@@ -169,6 +174,21 @@ const handleDelete = () => {
isSlided.value = false
}
// 重置滑动状态
const resetSlideState = () => {
slideOffset.value = 0
isSliding.value = false
isSlided.value = false
}
// 获取滑动状态
const getSlideState = () => {
return isSlided.value
}
// 暴露方法给父组件
defineExpose({ resetSlideState, getSlideState })
// 触摸开始事件处理函数
// 记录触摸开始时的X坐标用于计算滑动距离
const handleTouchStart = e => {

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ import FolderPage from './pages/FolderPage.vue'
// 设置页面
import SettingsPage from './pages/SettingsPage.vue'
// 导入数据库初始化函数
import { initDB } from './utils/indexedDBStorage'
// 配置路由规则
// 定义应用的所有路由路径和对应的组件
const routes = [
@@ -42,6 +45,16 @@ const router = createRouter({
// 创建并挂载Vue应用实例
// 配置Pinia状态管理和Vue Router路由
const app = createApp(App)
// 初始化数据库
initDB().then(() => {
console.log('数据库初始化成功')
}).catch(error => {
console.error('数据库初始化失败:', error)
// 即使数据库初始化失败,也继续启动应用
// 这样可以确保应用在没有IndexedDB支持的环境中仍然可以运行
})
// 使用Pinia进行状态管理
app.use(createPinia())
// 使用Vue Router进行路由管理

View File

@@ -1,25 +1,29 @@
<template>
<ion-page>
<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">
<span class="edit-time">{{ formattedTime }}</span>
<span>|</span>
<span class="word-count">{{ wordCount }}</span>
</div>
<!-- 富文本编辑器 -->
<div class="editor-container">
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" class="rich-text-editor" />
</div>
<!-- 富文本编辑器 -->
<div class="editor-container">
<RichTextEditor ref="editorRef" :modelValue="content" @update:modelValue="handleContentChange" @focus="handleEditorFocus" @blur="handleEditorBlur" class="rich-text-editor" />
</div>
</section>
</div>
</ion-page>
</template>
<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 { useAppStore } from '../stores/useAppStore'
import Header from '../components/Header.vue'
@@ -40,6 +44,9 @@ const noteId = computed(() => props.id || props.noteId)
const store = useAppStore()
const router = useRouter()
const editorRef = ref(null)
const headerRef = ref(null)
// 是否聚焦编辑器
const isEditorFocus = ref(false)
// 设置便签内容的函数
// 用于在编辑器中加载指定便签的内容
@@ -47,35 +54,27 @@ const setNoteContent = async noteId => {
// 确保store数据已加载如果便签列表为空则先加载数据
if (store.notes.length === 0) {
await store.loadData()
console.log('Store loaded, notes count:', store.notes.length)
}
// 从store中查找指定ID的便签
const note = store.notes.find(n => n.id === noteId)
console.log('Found note:', note)
// 确保编辑器已经初始化完成
await nextTick()
console.log('Editor ref:', editorRef.value)
if (note) {
console.log('Setting content:', note.content)
// 无论editorRef是否可用都先设置content的值作为备份
content.value = note.content || ''
// 如果editorRef可用直接设置编辑器内容
if (editorRef.value) {
editorRef.value.setContent(note.content || '')
}
} else {
console.log('Note not available')
}
}
// 加载初始数据
onMounted(async () => {
console.log('NoteEditorPage mounted')
await store.loadData()
console.log('Store loaded, notes count:', store.notes.length)
// 如果是编辑现有便签,在组件挂载后设置内容
if (noteId.value) {
@@ -87,7 +86,6 @@ onMounted(async () => {
watch(
noteId,
async newNoteId => {
console.log('Note ID changed:', newNoteId)
if (newNoteId) {
await setNoteContent(newNoteId)
}
@@ -148,13 +146,11 @@ const debounce = (func, delay) => {
// 延迟300ms更新内容避免用户输入时频繁触发更新
const debouncedHandleContentChange = debounce(newContent => {
content.value = newContent
console.log('Content updated:', newContent)
}, 300)
// 监听编辑器内容变化
// 当编辑器内容发生变化时调用此函数
const handleContentChange = newContent => {
console.log('Editor content changed:', newContent)
debouncedHandleContentChange(newContent)
}
@@ -181,53 +177,123 @@ const handleSave = async () => {
// 获取编辑器中的实际内容
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, {
content: editorContent,
})
console.log('便签已保存')
} else {
// Create new note
// 创建新便签
await store.addNote({
content: editorContent,
isStarred: false,
})
console.log('新便签已创建')
}
// Navigate back to the previous screen
router.push('/notes')
// 保存后切换到预览模式(失去编辑器焦点)
isEditorFocus.value = false
} catch (error) {
// In a full implementation, show an alert or toast
console.log('Save error: Failed to save note. Please try again.')
}
}
// 处理取消
const handleCancel = () => {
// Check if there are unsaved changes
const hasUnsavedChanges = content.value !== (existingNote?.content || '')
// 检查内容是否为空(无实质性内容)
const isContentEmpty = content => {
if (!content) return true
if (hasUnsavedChanges) {
showAlert.value = true
} else {
router.push('/notes')
// 检查是否包含图片元素
const hasImages = /<img[^>]*>|<div[^>]*class="[^"]*editor-image[^"]*"[^>]*>/.test(content)
if (hasImages) return false
// 移除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 () => {
try {
// 获取编辑器中的实际内容
const editorContent = editorRef.value ? editorRef.value.getContent() : content.value
// Create new note
await store.addNote({
content: editorContent,
isStarred: false,
})
// 只有当有内容时才创建新便签
if (!isContentEmpty(editorContent)) {
await store.addNote({
content: editorContent,
isStarred: false,
})
console.log('新便签已创建')
}
// Navigate back to the previous screen
router.push('/notes')
// 创建后切换到预览模式(失去编辑器焦点)
isEditorFocus.value = false
} catch (error) {
// In a full implementation, show an alert or toast
console.log('Create error: Failed to create note. Please try again.')
@@ -246,12 +312,92 @@ const handleAction = actionType => {
// 通过editorRef调用RichTextEditor组件的方法来插入图片
editorRef.value.insertImage()
}
} else if (actionType === 'delete') {
// 删除便签
handleDelete()
} else if (actionType === 'share') {
// 分享便签
handleShare()
}
}
const setShowAlert = 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>
<style lang="less" scoped>
@@ -260,12 +406,20 @@ const setShowAlert = value => {
flex-direction: column;
height: 100vh;
background-color: var(--background);
section {
width: 100%;
height: 100%;
}
}
.editor-container {
flex: 1;
min-height: 100%;
overflow-y: auto;
background-color: var(--background-card);
&.disabled {
pointer-events: none;
}
}
.rich-text-editor {

View File

@@ -32,19 +32,25 @@
</div>
<div class="notes-container">
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
<NoteItem
:title="note.title"
:content="note.content"
:date="formatDate(note.updatedAt)"
:isStarred="note.isStarred"
:isTop="note.isTop || false"
:hasImage="note.hasImage || false"
:onPress="() => handleNotePress(note.id)"
:onStarToggle="() => handleStarToggle(note.id)"
:onTopToggle="() => handleTopToggle(note.id)"
:onDelete="() => confirmDeleteNote(note.id)" />
</div>
<transition-group name="note-list" tag="div" class="notes-list">
<div v-for="note in filteredAndSortedNotes" :key="note.id" class="note-item">
<NoteItem
:ref="
el => {
if (el) noteItemRefs[note.id] = el
}
"
:content="note.content"
:date="formatDate(note.updatedAt)"
:isStarred="note.isStarred"
:isTop="note.isTop || false"
:hasImage="note.hasImage || false"
:onPress="() => handleNotePress(note.id)"
:onStarToggle="() => handleStarToggle(note.id)"
:onTopToggle="() => handleTopToggle(note.id)"
:onDelete="() => confirmDeleteNote(note.id)" />
</div>
</transition-group>
</div>
</ion-content>
</div>
@@ -64,6 +70,7 @@ import { IonContent, IonPage } from '@ionic/vue'
const store = useAppStore()
const router = useRouter()
const noteItemRefs = ref({})
// 页面挂载时加载初始数据
onMounted(() => {
@@ -73,7 +80,7 @@ onMounted(() => {
// 加载预设的模拟数据
store.loadMockData()
} else {
// 从localStorage加载用户数据
// 从Storage加载用户数据
store.loadData()
}
})
@@ -97,11 +104,12 @@ const trashNotesCount = computed(() => {
// 根据当前文件夹过滤便签
const filteredNotes = computed(() => {
// 预处理搜索查询,提高性能
const lowerCaseQuery = searchQuery.value.toLowerCase().trim()
const lowerCaseQuery = searchQuery.value?.toLowerCase().trim() || ''
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
@@ -168,6 +176,21 @@ const allNotesCount = computed(() => {
})
const handleNotePress = noteId => {
// 检查是否有便签条处于展开状态
let hasSlidedNote = false
Object.values(noteItemRefs.value).forEach(noteItem => {
// 注意isSlided是ref值需要通过.value访问
if (noteItem && noteItem.getSlideState()) {
hasSlidedNote = true
noteItem.resetSlideState()
}
})
// 如果有便签条处于展开状态,不跳转到编辑页面
if (hasSlidedNote) {
return
}
// 使用vue-router导航到编辑页面
router.push(`/editor/${noteId}`)
}
@@ -373,9 +396,35 @@ const notes = computed(() => store.notes)
.notes-container {
flex: 1;
position: relative;
}
.notes-list {
position: relative;
}
.note-item {
margin: 0.6rem 0;
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* 便签列表动画 */
.note-list-enter-active,
.note-list-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.note-list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.note-list-move {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.note-list-leave-active {
position: absolute;
width: calc(100% - 1rem);
}
</style>

View File

@@ -45,7 +45,7 @@ const store = useAppStore()
const router = useRouter()
// 页面挂载时加载初始数据
// 从localStorage加载用户设置和便签数据
// 从Storage加载用户设置和便签数据
onMounted(() => {
store.loadData()
})

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import * as storage from '../utils/storage'
import * as storage from '../utils/indexedDBStorage'
import { getCurrentDateTime, getPastDate } from '../utils/dateUtils'
/**
@@ -48,13 +48,13 @@ export const useAppStore = defineStore('app', {
actions: {
/**
* 初始化数据
* 从localStorage加载便签、文件夹和设置数据
* 从Storage加载便签、文件夹和设置数据
* 如果没有数据则加载预设的mock数据
* @returns {Promise<void>}
*/
async loadData() {
try {
// 从localStorage加载数据
// 从Storage加载数据
const loadedNotes = await storage.getNotes()
const loadedFolders = await storage.getFolders()
const loadedSettings = await storage.getSettings()
@@ -81,92 +81,22 @@ export const useAppStore = defineStore('app', {
async loadMockData() {
// Mock notes - 使用固定的日期值以避免每次运行时变化
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 = [
{
id: '1',
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,
updatedAt: fixedCurrentDate,
folderId: null,
isStarred: 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, // 包含图片
isDeleted: false, // 未删除
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 - 使用固定的日期值
@@ -201,14 +131,14 @@ export const useAppStore = defineStore('app', {
this.folders = mockFolders
this.settings = mockSettings
// 保存到localStorage
// 保存到Storage
await storage.saveNotes(mockNotes)
await storage.saveFolders(mockFolders)
await storage.saveSettings(mockSettings)
},
/**
* 保存便签数据到localStorage
* 保存便签数据到Storage
* @returns {Promise<void>}
*/
async saveNotes() {
@@ -220,7 +150,7 @@ export const useAppStore = defineStore('app', {
},
/**
* 保存文件夹数据到localStorage
* 保存文件夹数据到Storage
* @returns {Promise<void>}
*/
async saveFolders() {
@@ -232,7 +162,7 @@ export const useAppStore = defineStore('app', {
},
/**
* 保存设置数据到localStorage
* 保存设置数据到Storage
* @returns {Promise<void>}
*/
async saveSettings() {

View 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)
}
}

View File

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