feat: 初始化 kenaito-notebook 桌面记事本项目
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
build/bin
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
||||||
|
server/config.properties
|
||||||
|
|
||||||
|
# 编译产物
|
||||||
|
kenaito-notebook
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
# Kenaito Notebook
|
||||||
|
|
||||||
|
一个基于 Go + Wails + Vue 2 + Gin Server 的多层级嵌套 Markdown 笔记应用。
|
||||||
|
|
||||||
|
## 📖 项目简介
|
||||||
|
|
||||||
|
Kenaito Notebook 是一款轻量级、高性能的 Markdown 笔记应用,支持无限层级嵌套的文件夹结构,帮助你高效组织和管理笔记内容。采用前后端分离架构,Wails 桌面客户端通过 REST API 与独立的 Gin 服务端通信。支持用户注册登录,笔记数据按用户隔离。
|
||||||
|
|
||||||
|
## 🏗️ 架构概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Wails 桌面客户端 │
|
||||||
|
│ app.go (HTTP 客户端 + JWT) │
|
||||||
|
│ + Wails 原生功能 (文件对话框等) │
|
||||||
|
│ Vue 2 + Element UI + mavon │
|
||||||
|
└──────────┬──────────────────────┘
|
||||||
|
│ HTTP REST API (Bearer Token)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Server (Gin) :8080 │
|
||||||
|
│ /api/auth/* (公开) │
|
||||||
|
│ /api/notes/* (需登录) │
|
||||||
|
│ /api/files/* (需登录) │
|
||||||
|
│ /api/export/* (需登录) │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ AuthService (bcrypt + JWT) │
|
||||||
|
│ Database (SQLite) │
|
||||||
|
│ FileService │
|
||||||
|
│ ExportService │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
| 层级 | 技术 | 版本 |
|
||||||
|
|--------------|-----------------------------|---------|
|
||||||
|
| 桌面框架 | Wails | v2.12.0 |
|
||||||
|
| 客户端后端 | Go | 1.25+ |
|
||||||
|
| 服务端框架 | Gin | v1.10.0 |
|
||||||
|
| 数据库 | SQLite (modernc.org/sqlite) | v1.53.0 |
|
||||||
|
| 密码哈希 | bcrypt (golang.org/x/crypto)| v0.x |
|
||||||
|
| 前端 | Vue | 2.7.x |
|
||||||
|
| UI 框架 | Element UI | 2.15.x |
|
||||||
|
| Markdown 编辑 | mavon-editor | 2.10.x |
|
||||||
|
| Markdown 解析 | goldmark | v1.8.x |
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
### 用户系统
|
||||||
|
|
||||||
|
- 🔐 **用户注册/登录** - 用户名 + 密码 + 邮箱注册,JWT Token 认证
|
||||||
|
- 🔑 **安全存储** - 密码 bcrypt 加密,Token 7 天有效期
|
||||||
|
- 🚪 **登录/注销** - 优雅的登录界面,一键注销
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
- 📁 **多层级嵌套** - 支持无限层级的文件夹和笔记组织
|
||||||
|
- ✏️ **Markdown 编辑** - 实时预览、语法高亮、工具栏快捷操作
|
||||||
|
- 🔍 **全文搜索** - 支持标题和内容的模糊搜索
|
||||||
|
- 🏷️ **标签管理** - 为笔记添加自定义标签分类
|
||||||
|
- 💾 **自动保存** - 编辑内容 500ms 防抖自动保存
|
||||||
|
- 🗑️ **回收站** - 软删除 + 恢复 + 永久删除
|
||||||
|
|
||||||
|
### 导出功能
|
||||||
|
|
||||||
|
- 📤 **HTML 导出** - 生成格式完整的 HTML 页面
|
||||||
|
- 📝 **Markdown 导出** - 导出原始 Markdown 文件
|
||||||
|
- 📦 **ZIP 导出** - 文件夹/全部笔记打包导出
|
||||||
|
|
||||||
|
### Markdown 增强
|
||||||
|
|
||||||
|
- 🎨 **代码高亮** - 支持多种编程语言的语法高亮
|
||||||
|
- 📊 **表格支持** - Markdown 表格渲染
|
||||||
|
- 🖼️ **图片上传** - 支持粘贴和上传图片,本地文件服务存储
|
||||||
|
- 🔗 **链接处理** - 自动识别和处理链接
|
||||||
|
- 📐 **数学公式** - KaTeX 数学公式支持
|
||||||
|
|
||||||
|
## 📦 安装与使用
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- **Go** >= 1.25
|
||||||
|
- **Node.js** >= 18
|
||||||
|
- **Wails CLI** v2.x
|
||||||
|
|
||||||
|
### 安装 Wails CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd kenaito-notebook
|
||||||
|
```
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装客户端前端依赖
|
||||||
|
cd frontend && npm install && cd ..
|
||||||
|
|
||||||
|
# 安装 Server 依赖(会自动下载)
|
||||||
|
cd server && go mod tidy && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动
|
||||||
|
|
||||||
|
> ⚠️ 必须先启动 Server,再启动 Wails 客户端。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 启动 Server(默认监听 :8080)
|
||||||
|
cd server
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# 2. 新开终端,启动 Wails 客户端
|
||||||
|
cd kenaito-notebook
|
||||||
|
wails dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建 Server
|
||||||
|
cd server && go build -o kenaito-notebook-server . && cd ..
|
||||||
|
|
||||||
|
# 构建 Wails 客户端
|
||||||
|
wails build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物:
|
||||||
|
|
||||||
|
```
|
||||||
|
server/kenaito-notebook-server # Server 可执行文件
|
||||||
|
build/bin/kenaito-notebook.app # macOS 客户端
|
||||||
|
build/bin/kenaito-notebook.exe # Windows 客户端
|
||||||
|
build/bin/kenaito-notebook # Linux 客户端
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
kenaito-notebook/
|
||||||
|
├── app.go # Wails 客户端 (HTTP 客户端 + 原生功能)
|
||||||
|
├── main.go # Wails 入口
|
||||||
|
├── go.mod # 客户端模块 (仅依赖 wails/v2)
|
||||||
|
├── wails.json # Wails 项目配置
|
||||||
|
│
|
||||||
|
├── models/ # 共享数据模型
|
||||||
|
│ └── note.go # Note / NoteTree 定义
|
||||||
|
│
|
||||||
|
├── server/ # 独立服务端 (Gin)
|
||||||
|
│ ├── main.go # Gin 启动入口 (路由注册 + 认证中间件)
|
||||||
|
│ ├── config.go # 配置管理
|
||||||
|
│ ├── go.mod # 服务端模块
|
||||||
|
│ ├── models/ # 数据模型
|
||||||
|
│ │ ├── note.go # Note / NoteTree
|
||||||
|
│ │ └── user.go # User 用户模型
|
||||||
|
│ ├── services/ # 业务服务层
|
||||||
|
│ │ ├── database.go # SQLite 数据库服务 (笔记 + 用户表)
|
||||||
|
│ │ ├── fileservice.go # 文件/图片上传服务
|
||||||
|
│ │ ├── export.go # 导出服务 (HTML/Markdown/ZIP)
|
||||||
|
│ │ └── auth.go # 认证服务 (bcrypt + JWT)
|
||||||
|
│ └── handlers/ # REST API 处理器
|
||||||
|
│ ├── note.go # 笔记 CRUD API
|
||||||
|
│ ├── file.go # 文件上传 API
|
||||||
|
│ ├── export.go # 导出 API
|
||||||
|
│ └── auth.go # 认证 API (注册/登录/中间件)
|
||||||
|
│
|
||||||
|
├── frontend/ # 前端项目
|
||||||
|
│ ├── index.html
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── vite.config.js
|
||||||
|
│ └── src/
|
||||||
|
│ ├── main.js
|
||||||
|
│ ├── App.vue # 主组件 (登录态判断 + 布局)
|
||||||
|
│ ├── style.css
|
||||||
|
│ └── components/
|
||||||
|
│ ├── LoginView.vue # 登录/注册页面
|
||||||
|
│ ├── NoteTree.vue # 树形导航组件
|
||||||
|
│ └── TrashView.vue # 回收站视图
|
||||||
|
│
|
||||||
|
├── build/ # 构建输出
|
||||||
|
│ └── bin/
|
||||||
|
│
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 数据存储
|
||||||
|
|
||||||
|
### 数据库位置
|
||||||
|
|
||||||
|
SQLite 数据库和资源文件存储在用户主目录下:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.kenaito-notebook/
|
||||||
|
├── notes.db # SQLite 数据库
|
||||||
|
└── assets/ # 图片资源
|
||||||
|
└── 2026/06/ # 按日期分组
|
||||||
|
├── a1b2c3d4.png
|
||||||
|
└── e5f6g7h8.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库表结构
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 用户表
|
||||||
|
CREATE TABLE users
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
hashed_password TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX idx_users_username ON users (username);
|
||||||
|
|
||||||
|
-- 笔记表
|
||||||
|
CREATE TABLE notes
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
parent_id INTEGER DEFAULT 0, -- 父级 ID,0 为根目录
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
content TEXT DEFAULT '',
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
is_folder BOOLEAN DEFAULT FALSE,
|
||||||
|
tag TEXT DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME DEFAULT NULL -- 软删除标记
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_notes_parent_id ON notes (parent_id);
|
||||||
|
CREATE INDEX idx_notes_sort_order ON notes (sort_order);
|
||||||
|
CREATE INDEX idx_notes_deleted_at ON notes (deleted_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 API 文档
|
||||||
|
|
||||||
|
### Server REST API
|
||||||
|
|
||||||
|
Server 默认运行在 `http://localhost:8080`,所有端点前缀为 `/api`。
|
||||||
|
|
||||||
|
#### 认证
|
||||||
|
|
||||||
|
| 方法 | 路径 | 请求体 | 返回 | 说明 |
|
||||||
|
|------|---------------------|------------------------------------------|----------------|----------|
|
||||||
|
| POST | `/api/auth/register`| `{username, password, email}` | `{token}` | 用户注册 |
|
||||||
|
| POST | `/api/auth/login` | `{username, password}` | `{token}` | 用户登录 |
|
||||||
|
| GET | `/api/auth/me` | - (需 Bearer Token) | `{id, username, email, created_at}` | 获取当前用户 |
|
||||||
|
|
||||||
|
> 🔒 除 `/api/auth/register` 和 `/api/auth/login` 外,所有 API 需在请求头中携带 `Authorization: Bearer <token>`。
|
||||||
|
|
||||||
|
#### 笔记管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 请求体 | 返回 | 说明 |
|
||||||
|
|--------|--------------------------|-----------------------------------------------|---------------|----------|
|
||||||
|
| GET | `/api/notes/tree` | - | `[]NoteTree` | 获取笔记树 |
|
||||||
|
| GET | `/api/notes/:id` | - | `Note` | 获取单个笔记 |
|
||||||
|
| POST | `/api/notes` | `{parentId, title, isFolder}` | `Note` | 创建笔记/文件夹 |
|
||||||
|
| PUT | `/api/notes/:id` | `{title, content, tag}` | `{message}` | 更新笔记 |
|
||||||
|
| DELETE | `/api/notes/:id` | - | `{message}` | 软删除(移入回收站)|
|
||||||
|
| GET | `/api/notes/search?q=xxx`| - | `[]Note` | 搜索笔记 |
|
||||||
|
|
||||||
|
#### 回收站
|
||||||
|
|
||||||
|
| 方法 | 路径 | 返回 | 说明 |
|
||||||
|
|--------|-----------------------|-------------|----------|
|
||||||
|
| GET | `/api/trash` | `[]Note` | 获取回收站 |
|
||||||
|
| POST | `/api/trash/:id/restore`| `{message}` | 恢复笔记 |
|
||||||
|
| DELETE | `/api/trash/:id` | `{message}` | 永久删除 |
|
||||||
|
| DELETE | `/api/trash` | `{message}` | 清空回收站 |
|
||||||
|
|
||||||
|
#### 文件管理
|
||||||
|
|
||||||
|
| 方法 | 路径 | 请求体 | 返回 | 说明 |
|
||||||
|
|------|----------------------|--------------------------|-----------|---------|
|
||||||
|
| POST | `/api/files/image` | `{data: base64}` | `{url}` | 上传图片 |
|
||||||
|
| POST | `/api/files/upload` | `{data: base64, filename}`| `{url}` | 上传文件 |
|
||||||
|
| GET | `/api/files/server-url`| - | `{url}` | 文件服务器地址 |
|
||||||
|
| GET | `/api/files/assets-path`| - | `{path}` | 资源目录路径 |
|
||||||
|
|
||||||
|
#### 导出
|
||||||
|
|
||||||
|
| 方法 | 路径 | 请求体 | 返回 | 说明 |
|
||||||
|
|------|---------------------------------|--------------------------|-------------|-----------|
|
||||||
|
| POST | `/api/export/html` | `{title, content}` | `{filePath}`| 导出 HTML |
|
||||||
|
| POST | `/api/export/markdown` | `{title, content}` | `{filePath}`| 导出 Markdown|
|
||||||
|
| POST | `/api/export/folder-zip/:folderId`| - | `{filePath}`| 文件夹导出 ZIP |
|
||||||
|
| POST | `/api/export/all-zip` | - | `{filePath}`| 全部导出 ZIP |
|
||||||
|
|
||||||
|
### Wails 客户端 API
|
||||||
|
|
||||||
|
Wails 客户端通过 `window.go.main.App.*` 调用,方法名与 Server API 对应:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 用户认证
|
||||||
|
const token = await window.go.main.App.Register(username, password, email)
|
||||||
|
const token = await window.go.main.App.Login(username, password)
|
||||||
|
window.go.main.App.Logout()
|
||||||
|
const user = await window.go.main.App.GetUser()
|
||||||
|
const loggedIn = await window.go.main.App.IsLoggedIn()
|
||||||
|
|
||||||
|
// 获取笔记树
|
||||||
|
const tree = await window.go.main.App.GetNoteTree()
|
||||||
|
|
||||||
|
// 获取单个笔记
|
||||||
|
const note = await window.go.main.App.GetNote(noteId)
|
||||||
|
|
||||||
|
// 创建笔记
|
||||||
|
await window.go.main.App.CreateNote(parentId, title, isFolder)
|
||||||
|
|
||||||
|
// 更新笔记
|
||||||
|
await window.go.main.App.UpdateNote(id, title, content, tag)
|
||||||
|
|
||||||
|
// 删除笔记
|
||||||
|
await window.go.main.App.DeleteNote(id)
|
||||||
|
|
||||||
|
// 搜索笔记
|
||||||
|
const results = await window.go.main.App.SearchNotes(keyword)
|
||||||
|
|
||||||
|
// 保存图片
|
||||||
|
const url = await window.go.main.App.SaveImage(base64Data)
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
await window.go.main.App.ExportHTML(title, content)
|
||||||
|
await window.go.main.App.ExportMarkdown(title, content)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 界面说明
|
||||||
|
|
||||||
|
### 登录/注册页面
|
||||||
|
|
||||||
|
首次打开应用会显示登录/注册页面,支持:
|
||||||
|
- 用户名 + 密码登录
|
||||||
|
- 用户名 + 密码 + 邮箱注册
|
||||||
|
- 登录/注册模式一键切换
|
||||||
|
- 表单验证(用户名长度、密码长度、邮箱格式、确认密码)
|
||||||
|
|
||||||
|
### 主界面布局
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📝 Kenaito Notebook 👤 username [退出] │
|
||||||
|
├─────────────┬───────────────────────────────────────────────┤
|
||||||
|
│ 🔍 搜索... │ 笔记标题 │
|
||||||
|
├─────────────┤ [导出 ▼] │
|
||||||
|
│ [新建笔记] │ [标签] │
|
||||||
|
│ [新建文件夹] │ ┌─────────────────────────────────────────────┐
|
||||||
|
├─────────────┤ │ │
|
||||||
|
│ 📁 文件夹 │ │ Markdown 编辑器 │
|
||||||
|
│ 📄 笔记1 │ │ (实时预览) │
|
||||||
|
│ 📁 子文件夹│ │ │
|
||||||
|
│ 📄 笔记2│ │ │
|
||||||
|
│ 📄 笔记3 │ │ │
|
||||||
|
│ │ └─────────────────────────────────────────────┘
|
||||||
|
└─────────────┴───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 操作说明
|
||||||
|
|
||||||
|
| 操作 | 方式 |
|
||||||
|
|----------|--------------|
|
||||||
|
| 登录/注册 | 首页自动显示 |
|
||||||
|
| 选择笔记 | 单击笔记项 |
|
||||||
|
| 展开/收起文件夹 | 单击文件夹左侧箭头 |
|
||||||
|
| 新建笔记 | 点击「新建笔记」按钮 |
|
||||||
|
| 新建文件夹 | 点击「新建文件夹」按钮 |
|
||||||
|
| 在文件夹内新建 | 右键文件夹 → 选择操作 |
|
||||||
|
| 删除笔记 | 右键笔记 → 删除 |
|
||||||
|
| 搜索 | 在搜索框输入关键词 |
|
||||||
|
| 导出 | 点击右上角「导出」按钮 |
|
||||||
|
| 退出登录 | 点击右上角「退出」按钮 |
|
||||||
|
|
||||||
|
## 🚀 开发指南
|
||||||
|
|
||||||
|
### 开发环境配置
|
||||||
|
|
||||||
|
1. **安装 Go**: https://golang.org/dl/
|
||||||
|
2. **安装 Node.js**: https://nodejs.org/
|
||||||
|
3. **安装 Wails**:
|
||||||
|
```bash
|
||||||
|
go install github.com/wailsapp/wails/v2/cmd/wails@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
|
||||||
|
#### 添加 Server API
|
||||||
|
|
||||||
|
1. 在 `server/handlers/` 中添加处理器
|
||||||
|
2. 在 `server/main.go` 的路由组中注册路由(公开接口放 `auth` 组,需登录接口放 `protected` 组)
|
||||||
|
3. 在 `app.go` 中添加对应的 HTTP 调用方法
|
||||||
|
4. 在 `frontend/wailsjs/go/main/App.js` 和 `App.d.ts` 中添加绑定声明
|
||||||
|
5. 前端通过 `window.go.main.App.MethodName()` 调用
|
||||||
|
|
||||||
|
#### 认证机制
|
||||||
|
|
||||||
|
- 注册:POST `/api/auth/register`,返回 JWT Token,自动保存到客户端
|
||||||
|
- 登录:POST `/api/auth/login`,返回 JWT Token,自动保存到客户端
|
||||||
|
- 客户端 `doRequest()` 自动在请求头中添加 `Authorization: Bearer <token>`
|
||||||
|
- Server 中间件校验 Token 有效性,无效/过期返回 401
|
||||||
|
- Token 使用 HMAC-SHA256 签名,有效期 7 天
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- Go 代码遵循 `gofmt` 标准
|
||||||
|
- Vue 代码使用 ESLint 格式化
|
||||||
|
- 提交信息使用中文描述
|
||||||
|
|
||||||
|
## 🐛 常见问题
|
||||||
|
|
||||||
|
### Q: 数据库文件在哪里?
|
||||||
|
|
||||||
|
A: 数据库存储在 `~/.kenaito-notebook/notes.db`
|
||||||
|
|
||||||
|
### Q: 如何备份笔记?
|
||||||
|
|
||||||
|
A: 备份 `~/.kenaito-notebook/` 目录即可
|
||||||
|
|
||||||
|
### Q: 图片上传后存储在哪里?
|
||||||
|
|
||||||
|
A: 图片存储在 `~/.kenaito-notebook/assets/` 目录,按日期分组
|
||||||
|
|
||||||
|
### Q: Server 启动失败怎么办?
|
||||||
|
|
||||||
|
A: 确认 8080 端口未被占用。可通过环境变量 `SERVER_PORT=9090` 修改端口。
|
||||||
|
|
||||||
|
### Q: Wails 客户端连接不上 Server?
|
||||||
|
|
||||||
|
A: 确保 Server 已启动。默认连接 `http://localhost:8080`。
|
||||||
|
|
||||||
|
### Q: 忘记密码了怎么办?
|
||||||
|
|
||||||
|
A: 目前暂不支持密码重置功能,请重新注册账号。
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
欢迎提交 Issue 和 Pull Request!
|
||||||
|
|
||||||
|
## 📮 联系方式
|
||||||
|
|
||||||
|
如有问题或建议,请提交 Issue。
|
||||||
@@ -0,0 +1,530 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
stdruntime "runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
|
|
||||||
|
"kenaito-notebook/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 配置后端地址
|
||||||
|
const defaultServerURL = "http://localhost:8080"
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
serverURL string
|
||||||
|
http *http.Client
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{
|
||||||
|
serverURL: defaultServerURL,
|
||||||
|
http: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
log.Println("[App] Starting up, server:", a.serverURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) shutdown(ctx context.Context) {
|
||||||
|
log.Println("[App] Shutting down")
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest 发起 HTTP 请求的通用方法
|
||||||
|
func (a *App) doRequest(method, path string, body interface{}) ([]byte, error) {
|
||||||
|
var bodyReader io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyReader = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, a.serverURL+path, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
if a.token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+a.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("server connection failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
var errResp struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(data, &errResp) == nil && errResp.Error != "" {
|
||||||
|
return nil, fmt.Errorf(errResp.Error)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("server returned %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 用户注册
|
||||||
|
func (a *App) Register(email, password, code string) (string, error) {
|
||||||
|
body := map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"code": code,
|
||||||
|
}
|
||||||
|
data, err := a.doRequest("POST", "/api/auth/register", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
a.token = resp.Token
|
||||||
|
return resp.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendVerifyCode 发送验证码
|
||||||
|
func (a *App) SendVerifyCode(email, purpose string) error {
|
||||||
|
body := map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"purpose": purpose,
|
||||||
|
}
|
||||||
|
_, err := a.doRequest("POST", "/api/auth/send-code", body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCode 验证验证码
|
||||||
|
func (a *App) VerifyCode(email, code, purpose string) error {
|
||||||
|
body := map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"code": code,
|
||||||
|
"purpose": purpose,
|
||||||
|
}
|
||||||
|
_, err := a.doRequest("POST", "/api/auth/verify-code", body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword 重置密码
|
||||||
|
func (a *App) ResetPassword(email, code, newPassword string) error {
|
||||||
|
body := map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"code": code,
|
||||||
|
"newPassword": newPassword,
|
||||||
|
}
|
||||||
|
_, err := a.doRequest("POST", "/api/auth/reset-password", body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
func (a *App) Login(email, password string) (string, error) {
|
||||||
|
body := map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
data, err := a.doRequest("POST", "/api/auth/login", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
a.token = resp.Token
|
||||||
|
return resp.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout 退出登录
|
||||||
|
func (a *App) Logout() {
|
||||||
|
a.token = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser 获取当前用户信息
|
||||||
|
func (a *App) GetUser() (map[string]interface{}, error) {
|
||||||
|
data, err := a.doRequest("GET", "/api/auth/me", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var user map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLoggedIn 是否已登录
|
||||||
|
func (a *App) IsLoggedIn() bool {
|
||||||
|
return a.token != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNoteTree 获取笔记树
|
||||||
|
func (a *App) GetNoteTree() ([]*models.NoteTree, error) {
|
||||||
|
data, err := a.doRequest("GET", "/api/notes/tree", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result []*models.NoteTree
|
||||||
|
if err := json.Unmarshal(data, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNote 获取单个笔记
|
||||||
|
func (a *App) GetNote(id int64) (*models.Note, error) {
|
||||||
|
data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/%d", id), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var note models.Note
|
||||||
|
if err := json.Unmarshal(data, ¬e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ¬e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNote 创建笔记或文件夹
|
||||||
|
func (a *App) CreateNote(parentID int64, title string, isFolder bool) (*models.Note, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"parentId": parentID,
|
||||||
|
"title": title,
|
||||||
|
"isFolder": isFolder,
|
||||||
|
}
|
||||||
|
data, err := a.doRequest("POST", "/api/notes", body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var note models.Note
|
||||||
|
if err := json.Unmarshal(data, ¬e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ¬e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateNote 更新笔记
|
||||||
|
func (a *App) UpdateNote(id int64, title, content, tag string) error {
|
||||||
|
body := map[string]string{
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
"tag": tag,
|
||||||
|
}
|
||||||
|
_, err := a.doRequest("PUT", fmt.Sprintf("/api/notes/%d", id), body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNote 删除笔记
|
||||||
|
func (a *App) DeleteNote(id int64) error {
|
||||||
|
_, err := a.doRequest("DELETE", fmt.Sprintf("/api/notes/%d", id), nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchNotes 搜索笔记
|
||||||
|
func (a *App) SearchNotes(keyword string) ([]*models.Note, error) {
|
||||||
|
data, err := a.doRequest("GET", fmt.Sprintf("/api/notes/search?q=%s", keyword), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var notes []*models.Note
|
||||||
|
if err := json.Unmarshal(data, ¬es); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTrashNotes 获取回收站中的笔记
|
||||||
|
func (a *App) GetTrashNotes() ([]*models.Note, error) {
|
||||||
|
data, err := a.doRequest("GET", "/api/trash", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var notes []*models.Note
|
||||||
|
if err := json.Unmarshal(data, ¬es); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreNote 恢复笔记
|
||||||
|
func (a *App) RestoreNote(id int64) error {
|
||||||
|
_, err := a.doRequest("POST", fmt.Sprintf("/api/trash/%d/restore", id), nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PermanentDeleteNote 永久删除笔记
|
||||||
|
func (a *App) PermanentDeleteNote(id int64) error {
|
||||||
|
_, err := a.doRequest("DELETE", fmt.Sprintf("/api/trash/%d", id), nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmptyTrash 清空回收站
|
||||||
|
func (a *App) EmptyTrash() error {
|
||||||
|
_, err := a.doRequest("DELETE", "/api/trash", nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveImage 保存图片
|
||||||
|
func (a *App) SaveImage(base64Data string) (string, error) {
|
||||||
|
body := map[string]string{"data": base64Data}
|
||||||
|
data, err := a.doRequest("POST", "/api/files/image", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFile 保存任意文件
|
||||||
|
func (a *App) SaveFile(base64Data, filename string) (string, error) {
|
||||||
|
body := map[string]string{
|
||||||
|
"data": base64Data,
|
||||||
|
"filename": filename,
|
||||||
|
}
|
||||||
|
data, err := a.doRequest("POST", "/api/files/upload", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return resp.URL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileServerURL 获取文件服务器地址
|
||||||
|
func (a *App) GetFileServerURL() string {
|
||||||
|
data, err := a.doRequest("GET", "/api/files/server-url", nil)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resp.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAssetsPath 获取资源路径
|
||||||
|
func (a *App) GetAssetsPath() string {
|
||||||
|
data, err := a.doRequest("GET", "/api/files/assets-path", nil)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return resp.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectDirectory 弹出目录选择对话框
|
||||||
|
func (a *App) SelectDirectory(title string) (string, error) {
|
||||||
|
result, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
|
||||||
|
Title: title,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result == "" {
|
||||||
|
return "", fmt.Errorf("用户取消选择")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectSaveFile 弹出文件保存对话框
|
||||||
|
func (a *App) SelectSaveFile(title, defaultFilename string) (string, error) {
|
||||||
|
result, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
|
||||||
|
Title: title,
|
||||||
|
DefaultFilename: defaultFilename,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if result == "" {
|
||||||
|
return "", fmt.Errorf("用户取消保存")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevealInFinder 在 Finder 中显示文件
|
||||||
|
func (a *App) RevealInFinder(path string) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch stdruntime.GOOS {
|
||||||
|
case "darwin":
|
||||||
|
cmd = exec.Command("open", "-R", path)
|
||||||
|
case "windows":
|
||||||
|
cmd = exec.Command("explorer", "/select,", path)
|
||||||
|
default:
|
||||||
|
cmd = exec.Command("xdg-open", filepath.Dir(path))
|
||||||
|
}
|
||||||
|
cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportHTML 导出 HTML
|
||||||
|
func (a *App) ExportHTML(title, content string) (string, error) {
|
||||||
|
body := map[string]string{"title": title, "content": content}
|
||||||
|
data, err := a.doRequest("POST", "/api/export/html", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFilename := title + ".html"
|
||||||
|
savePath, err := a.SelectSaveFile("导出 HTML 文件", defaultFilename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.Ext(savePath) != ".html" {
|
||||||
|
savePath += ".html"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(resp.FilePath, savePath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportMarkdown 导出 Markdown
|
||||||
|
func (a *App) ExportMarkdown(title, content string) (string, error) {
|
||||||
|
body := map[string]string{"title": title, "content": content}
|
||||||
|
data, err := a.doRequest("POST", "/api/export/markdown", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultFilename := title + ".md"
|
||||||
|
savePath, err := a.SelectSaveFile("导出 Markdown 文件", defaultFilename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.Ext(savePath) != ".md" {
|
||||||
|
savePath += ".md"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(resp.FilePath, savePath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFolderMarkdown 导出整个文件夹为 ZIP
|
||||||
|
func (a *App) ExportFolderMarkdown(folderID int64) (string, error) {
|
||||||
|
data, err := a.doRequest("POST", fmt.Sprintf("/api/export/folder-zip/%d", folderID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
savePath, err := a.SelectSaveFile("导出文件夹为 ZIP", "export.zip")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.Ext(savePath) != ".zip" {
|
||||||
|
savePath += ".zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(resp.FilePath, savePath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportAllMarkdown 导出所有笔记为 ZIP
|
||||||
|
func (a *App) ExportAllMarkdown() (string, error) {
|
||||||
|
data, err := a.doRequest("POST", "/api/export/all-zip", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
savePath, err := a.SelectSaveFile("导出所有笔记为 ZIP", "所有笔记.zip")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.Ext(savePath) != ".zip" {
|
||||||
|
savePath += ".zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(resp.FilePath, savePath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return savePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Build Directory
|
||||||
|
|
||||||
|
The build directory is used to house all the build files and assets for your application.
|
||||||
|
|
||||||
|
The structure is:
|
||||||
|
|
||||||
|
* bin - Output directory
|
||||||
|
* darwin - macOS specific files
|
||||||
|
* windows - Windows specific files
|
||||||
|
|
||||||
|
## Mac
|
||||||
|
|
||||||
|
The `darwin` directory holds files specific to Mac builds.
|
||||||
|
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||||
|
and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
The directory contains the following files:
|
||||||
|
|
||||||
|
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||||
|
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||||
|
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||||
|
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||||
|
will be created using the `appicon.png` file in the build directory.
|
||||||
|
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||||
|
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||||
|
as well as the application itself (right click the exe -> properties -> details)
|
||||||
|
- `wails.exe.manifest` - The main application manifest file.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{.Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "{{.Info.ProductVersion}}"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||||
|
"CompanyName": "{{.Info.CompanyName}}",
|
||||||
|
"FileDescription": "{{.Info.ProductName}}",
|
||||||
|
"LegalCopyright": "{{.Info.Copyright}}",
|
||||||
|
"ProductName": "{{.Info.ProductName}}",
|
||||||
|
"Comments": "{{.Info.Comments}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||||
|
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||||
|
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||||
|
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
!insertmacro wails.associateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
!insertmacro wails.unassociateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "{{.Name}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
File "..\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||||
|
|
||||||
|
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateCustomProtocols
|
||||||
|
; Create custom protocols associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs,
|
||||||
|
check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>kenaito-notebook</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script src="./src/main.js" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Generated
+2399
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^2.7.14",
|
||||||
|
"element-ui": "^2.15.14",
|
||||||
|
"mavon-editor": "^2.10.4",
|
||||||
|
"highlight.js": "^11.9.0",
|
||||||
|
"marked": "^9.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue2": "^2.3.1",
|
||||||
|
"vite": "^3.0.7",
|
||||||
|
"vite-plugin-vue2": "^2.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
2bd2793b3ea7c36dd202ef834837c1bb
|
||||||
@@ -0,0 +1,938 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" class="app-container">
|
||||||
|
<!-- 未登录:显示登录页面 -->
|
||||||
|
<login-view v-if="!isLoggedIn" @login-success="onLoginSuccess" />
|
||||||
|
|
||||||
|
<!-- 已登录:显示主界面 -->
|
||||||
|
<el-container v-else style="height: 100vh">
|
||||||
|
<el-aside width="280px" class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<div class="title-row">
|
||||||
|
<el-dropdown trigger="click" @command="handleUserCommand">
|
||||||
|
<span class="user-info">
|
||||||
|
<i class="el-icon-user-solid"></i>
|
||||||
|
{{ email }}
|
||||||
|
<i class="el-icon-arrow-down"></i>
|
||||||
|
</span>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item command="logout">
|
||||||
|
<i class="el-icon-switch-button"></i> 退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="search-box">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索笔记..."
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<note-tree
|
||||||
|
:notes="searchKeyword ? searchResults : noteTree"
|
||||||
|
:selected-id="selectedNoteId"
|
||||||
|
@select="openNote"
|
||||||
|
@delete="deleteNote"
|
||||||
|
@context-menu="handleContextMenu"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<el-button
|
||||||
|
:type="showTrash ? 'primary' : 'text'"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="openTrash"
|
||||||
|
>
|
||||||
|
回收站
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-aside>
|
||||||
|
|
||||||
|
<el-main class="main-content">
|
||||||
|
<!-- 回收站视图 -->
|
||||||
|
<trash-view
|
||||||
|
v-if="showTrash"
|
||||||
|
:trash-notes="trashNotes"
|
||||||
|
@restore="restoreNote"
|
||||||
|
@permanent-delete="permanentDeleteNote"
|
||||||
|
@empty-trash="emptyTrash"
|
||||||
|
@close="showTrash = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 多标签页 -->
|
||||||
|
<div v-else-if="tabs.length > 0" class="tabs-container">
|
||||||
|
<div class="tabs-header">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'is-active': activeTabId === tab.id }"
|
||||||
|
@click="switchTab(tab.id)"
|
||||||
|
>
|
||||||
|
<span class="tab-title">{{ tab.title || '未命名' }}</span>
|
||||||
|
<span class="tab-close" @click.stop="closeTab(tab.id)">
|
||||||
|
<i class="el-icon-close"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 编辑器内容 -->
|
||||||
|
<div v-if="activeTab" class="editor-container">
|
||||||
|
<div class="editor-header">
|
||||||
|
<el-input
|
||||||
|
v-model="activeTab.title"
|
||||||
|
class="note-title-input"
|
||||||
|
@blur="saveTab"
|
||||||
|
placeholder="笔记标题"
|
||||||
|
/>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<el-dropdown trigger="click" @command="handleExport">
|
||||||
|
<el-button size="small" icon="el-icon-download">
|
||||||
|
导出 <i class="el-icon-arrow-down el-icon--right"></i>
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item command="html">导出 HTML</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="markdown">导出 Markdown</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<mavon-editor
|
||||||
|
ref="md"
|
||||||
|
v-model="activeTab.content"
|
||||||
|
:toolbars="toolbars"
|
||||||
|
:imageUpload="imageUpload"
|
||||||
|
@change="onContentChange"
|
||||||
|
@imgAdd="handleImgAdd"
|
||||||
|
@imgDel="handleImgDel"
|
||||||
|
style="height: calc(100vh - 180px)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="!showTrash" class="empty-state">
|
||||||
|
<i class="el-icon-document"></i>
|
||||||
|
<p>选择或创建一个笔记开始编写</p>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NoteTree from './components/NoteTree.vue'
|
||||||
|
import TrashView from './components/TrashView.vue'
|
||||||
|
import LoginView from './components/LoginView.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
NoteTree,
|
||||||
|
TrashView,
|
||||||
|
LoginView
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoggedIn: false,
|
||||||
|
email: '',
|
||||||
|
noteTree: [],
|
||||||
|
tabs: [], // 打开的标签页列表
|
||||||
|
activeTabId: null, // 当前激活的标签页 ID
|
||||||
|
searchKeyword: '',
|
||||||
|
searchResults: [],
|
||||||
|
saveTimers: {}, // 每个标签页的保存定时器
|
||||||
|
showTrash: false, // 是否显示回收站
|
||||||
|
trashNotes: [], // 回收站中的笔记
|
||||||
|
toolbars: {
|
||||||
|
bold: true,
|
||||||
|
italic: true,
|
||||||
|
header: true,
|
||||||
|
underline: true,
|
||||||
|
strikethrough: true,
|
||||||
|
mark: true,
|
||||||
|
quote: true,
|
||||||
|
ol: true,
|
||||||
|
ul: true,
|
||||||
|
link: true,
|
||||||
|
imagelink: true,
|
||||||
|
code: true,
|
||||||
|
table: true,
|
||||||
|
fullscreen: true,
|
||||||
|
htmlcode: true,
|
||||||
|
undo: true,
|
||||||
|
redo: true,
|
||||||
|
preview: true,
|
||||||
|
subfield: true,
|
||||||
|
help: true
|
||||||
|
},
|
||||||
|
imageUpload: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedNoteId() {
|
||||||
|
return this.activeTabId
|
||||||
|
},
|
||||||
|
activeTab() {
|
||||||
|
return this.tabs.find(tab => tab.id === this.activeTabId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
// 检查登录状态
|
||||||
|
try {
|
||||||
|
const loggedIn = await window.go.main.App.IsLoggedIn()
|
||||||
|
if (loggedIn) {
|
||||||
|
this.isLoggedIn = true
|
||||||
|
await this.loadUserInfo()
|
||||||
|
await this.loadNoteTree()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to check login status:', err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听全局粘贴和拖放事件
|
||||||
|
document.addEventListener('paste', this.handlePaste)
|
||||||
|
document.addEventListener('dragover', this.handleDragOver, true)
|
||||||
|
document.addEventListener('drop', this.handleDrop, true)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.removeEventListener('paste', this.handlePaste)
|
||||||
|
document.removeEventListener('dragover', this.handleDragOver, true)
|
||||||
|
document.removeEventListener('drop', this.handleDrop, true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onLoginSuccess() {
|
||||||
|
this.isLoggedIn = true
|
||||||
|
await this.loadUserInfo()
|
||||||
|
await this.loadNoteTree()
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadUserInfo() {
|
||||||
|
try {
|
||||||
|
const user = await window.go.main.App.GetUser()
|
||||||
|
this.email = user.email || ''
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load user info:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUserCommand(command) {
|
||||||
|
if (command === 'logout') {
|
||||||
|
this.$confirm('确定要退出登录吗?', '提示', {
|
||||||
|
type: 'info',
|
||||||
|
confirmButtonText: '退出',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}).then(() => {
|
||||||
|
window.go.main.App.Logout()
|
||||||
|
this.isLoggedIn = false
|
||||||
|
this.email = ''
|
||||||
|
this.tabs = []
|
||||||
|
this.activeTabId = null
|
||||||
|
this.noteTree = []
|
||||||
|
this.$message.success('已退出登录')
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadNoteTree() {
|
||||||
|
try {
|
||||||
|
this.noteTree = await window.go.main.App.GetNoteTree()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load note tree:', err)
|
||||||
|
this.$message.error('加载笔记失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 打开笔记到标签页
|
||||||
|
async openNote(note) {
|
||||||
|
if (note.isFolder) return
|
||||||
|
|
||||||
|
// 检查是否已经在标签页中
|
||||||
|
const existingTab = this.tabs.find(tab => tab.id === note.id)
|
||||||
|
if (existingTab) {
|
||||||
|
this.activeTabId = note.id
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fullNote = await window.go.main.App.GetNote(note.id)
|
||||||
|
this.tabs.push({
|
||||||
|
id: fullNote.id,
|
||||||
|
title: fullNote.title,
|
||||||
|
content: fullNote.content,
|
||||||
|
parentId: fullNote.parentId,
|
||||||
|
isDirty: false
|
||||||
|
})
|
||||||
|
this.activeTabId = fullNote.id
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load note:', err)
|
||||||
|
this.$message.error('加载笔记内容失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换标签页
|
||||||
|
switchTab(tabId) {
|
||||||
|
this.activeTabId = tabId
|
||||||
|
},
|
||||||
|
|
||||||
|
// 关闭标签页
|
||||||
|
closeTab(tabId) {
|
||||||
|
const tab = this.tabs.find(t => t.id === tabId)
|
||||||
|
if (tab && tab.isDirty) {
|
||||||
|
this.$confirm('有未保存的更改,确定要关闭吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
this.removeTab(tabId)
|
||||||
|
}).catch(() => {})
|
||||||
|
} else {
|
||||||
|
this.removeTab(tabId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeTab(tabId) {
|
||||||
|
const index = this.tabs.findIndex(t => t.id === tabId)
|
||||||
|
this.tabs.splice(index, 1)
|
||||||
|
|
||||||
|
if (this.activeTabId === tabId) {
|
||||||
|
// 切换到相邻的标签页
|
||||||
|
if (this.tabs.length > 0) {
|
||||||
|
const newIndex = Math.min(index, this.tabs.length - 1)
|
||||||
|
this.activeTabId = this.tabs[newIndex].id
|
||||||
|
} else {
|
||||||
|
this.activeTabId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
if (this.saveTimers[tabId]) {
|
||||||
|
clearTimeout(this.saveTimers[tabId])
|
||||||
|
delete this.saveTimers[tabId]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createNoteDirectly(parentId, isFolder) {
|
||||||
|
try {
|
||||||
|
const title = isFolder ? '未命名文件夹' : '未命名笔记'
|
||||||
|
const newNote = await window.go.main.App.CreateNote(parentId, title, isFolder)
|
||||||
|
await this.loadNoteTree()
|
||||||
|
|
||||||
|
// 如果是新建笔记,自动打开
|
||||||
|
if (!isFolder) {
|
||||||
|
await this.openNote(newNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$message.success('创建成功')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create note:', err)
|
||||||
|
this.$message.error('创建失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteNote(note) {
|
||||||
|
// 如果标签页中有该笔记,先关闭
|
||||||
|
const tabIndex = this.tabs.findIndex(t => t.id === note.id)
|
||||||
|
if (tabIndex > -1) {
|
||||||
|
this.removeTab(note.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.go.main.App.DeleteNote(note.id)
|
||||||
|
await this.loadNoteTree()
|
||||||
|
if (this.showTrash) await this.loadTrashNotes()
|
||||||
|
this.$message.success('已移到回收站')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete note:', err)
|
||||||
|
this.$message.error('删除失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveTab() {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
|
||||||
|
if (this.saveTimers[this.activeTabId]) {
|
||||||
|
clearTimeout(this.saveTimers[this.activeTabId])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveTimers[this.activeTabId] = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await window.go.main.App.UpdateNote(
|
||||||
|
this.activeTab.id,
|
||||||
|
this.activeTab.title,
|
||||||
|
this.activeTab.content,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
this.activeTab.isDirty = false
|
||||||
|
await this.loadNoteTree()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save note:', err)
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
|
||||||
|
onContentChange() {
|
||||||
|
if (this.activeTab) {
|
||||||
|
this.activeTab.isDirty = true
|
||||||
|
}
|
||||||
|
this.saveTab()
|
||||||
|
},
|
||||||
|
|
||||||
|
// 粘贴事件处理
|
||||||
|
async handlePaste(e) {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
|
||||||
|
const items = e.clipboardData?.items
|
||||||
|
if (!items) return
|
||||||
|
|
||||||
|
// 检查是否有文件数据
|
||||||
|
const files = []
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].kind === 'file') {
|
||||||
|
const file = items[i].getAsFile()
|
||||||
|
if (file) files.push(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 0) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const maxSize = 6 * 1024 * 1024 // 6MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
this.$message.warning(`文件 ${file.name} 大小超过 6MB,已跳过`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let base64Data
|
||||||
|
const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)
|
||||||
|
if (isImage) {
|
||||||
|
base64Data = await this.compressImage(file)
|
||||||
|
} else {
|
||||||
|
base64Data = await this.fileToBase64(file)
|
||||||
|
}
|
||||||
|
const fileUrl = await window.go.main.App.SaveFile(base64Data, file.name)
|
||||||
|
this.insertFileLink(file.name, fileUrl)
|
||||||
|
this.$message.success(`${file.name} 嵌入成功`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to upload file:', err)
|
||||||
|
this.$message.error(`${file.name} 上传失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 拖放事件处理(全局 capture 阶段)
|
||||||
|
handleDragOver(e) {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleDrop(e) {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
|
||||||
|
const files = e.dataTransfer?.files
|
||||||
|
if (!files || files.length === 0) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const maxSize = 6 * 1024 * 1024 // 6MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
this.$message.warning(`文件 ${file.name} 大小超过 6MB,已跳过`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let base64Data
|
||||||
|
const isImage = /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)
|
||||||
|
if (isImage) {
|
||||||
|
base64Data = await this.compressImage(file)
|
||||||
|
} else {
|
||||||
|
base64Data = await this.fileToBase64(file)
|
||||||
|
}
|
||||||
|
const fileUrl = await window.go.main.App.SaveFile(base64Data, file.name)
|
||||||
|
this.insertFileLink(file.name, fileUrl)
|
||||||
|
this.$message.success(`${file.name} 嵌入成功`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to upload file:', err)
|
||||||
|
this.$message.error(`${file.name} 上传失败`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fileToBase64(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result)
|
||||||
|
reader.onerror = error => reject(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
insertFileLink(filename, filePath) {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
|
||||||
|
// 根据文件类型决定插入格式
|
||||||
|
const isImage = /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(filename)
|
||||||
|
let markdown = ''
|
||||||
|
if (isImage) {
|
||||||
|
markdown = ``
|
||||||
|
} else {
|
||||||
|
markdown = `[${filename}](${filePath})`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在内容末尾插入
|
||||||
|
this.activeTab.content += `\n\n${markdown}\n`
|
||||||
|
this.saveTab()
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleSearch() {
|
||||||
|
if (!this.searchKeyword.trim()) {
|
||||||
|
this.searchResults = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.searchResults = await window.go.main.App.SearchNotes(this.searchKeyword)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to search notes:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleExport(command) {
|
||||||
|
if (!this.activeTab) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
let filePath
|
||||||
|
if (command === 'html') {
|
||||||
|
filePath = await window.go.main.App.ExportHTML(
|
||||||
|
this.activeTab.title,
|
||||||
|
this.activeTab.content
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filePath = await window.go.main.App.ExportMarkdown(
|
||||||
|
this.activeTab.title,
|
||||||
|
this.activeTab.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
this.$confirm('导出成功!是否打开文件所在目录?', '导出完成', {
|
||||||
|
confirmButtonText: '打开目录',
|
||||||
|
cancelButtonText: '确定',
|
||||||
|
type: 'success'
|
||||||
|
}).then(() => {
|
||||||
|
window.go.main.App.RevealInFinder(filePath)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 用户取消选择不显示错误
|
||||||
|
if (err && err.includes && err.includes('用户取消')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error('Failed to export:', err)
|
||||||
|
this.$message.error('导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async exportFolder(folder) {
|
||||||
|
try {
|
||||||
|
const filePath = await window.go.main.App.ExportFolderMarkdown(folder.id)
|
||||||
|
if (filePath) {
|
||||||
|
this.$confirm('导出成功!是否打开文件所在目录?', '导出完成', {
|
||||||
|
confirmButtonText: '打开目录',
|
||||||
|
cancelButtonText: '确定',
|
||||||
|
type: 'success'
|
||||||
|
}).then(() => {
|
||||||
|
window.go.main.App.RevealInFinder(filePath)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.includes && err.includes('用户取消')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error('Failed to export folder:', err)
|
||||||
|
this.$message.error('导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async exportAll() {
|
||||||
|
try {
|
||||||
|
const filePath = await window.go.main.App.ExportAllMarkdown()
|
||||||
|
if (filePath) {
|
||||||
|
this.$confirm('导出成功!是否打开文件所在目录?', '导出完成', {
|
||||||
|
confirmButtonText: '打开目录',
|
||||||
|
cancelButtonText: '确定',
|
||||||
|
type: 'success'
|
||||||
|
}).then(() => {
|
||||||
|
window.go.main.App.RevealInFinder(filePath)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err && err.includes && err.includes('用户取消')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error('Failed to export all:', err)
|
||||||
|
this.$message.error('导出失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleContextMenu(event) {
|
||||||
|
const { command, note } = event
|
||||||
|
if (command === 'newChild') {
|
||||||
|
this.createNoteDirectly(note.id, false)
|
||||||
|
} else if (command === 'newFolder') {
|
||||||
|
this.createNoteDirectly(note ? note.id : 0, true)
|
||||||
|
} else if (command === 'newNote') {
|
||||||
|
this.createNoteDirectly(0, false)
|
||||||
|
} else if (command === 'delete') {
|
||||||
|
this.deleteNote(note)
|
||||||
|
} else if (command === 'trash') {
|
||||||
|
this.showTrash = true
|
||||||
|
} else if (command === 'renamed') {
|
||||||
|
this.loadNoteTree()
|
||||||
|
} else if (command === 'exportFolder') {
|
||||||
|
this.exportFolder(note)
|
||||||
|
} else if (command === 'exportAll') {
|
||||||
|
this.exportAll()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 回收站相关方法
|
||||||
|
async openTrash() {
|
||||||
|
this.showTrash = !this.showTrash
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTrashNotes() {
|
||||||
|
try {
|
||||||
|
const notes = await window.go.main.App.GetTrashNotes()
|
||||||
|
this.trashNotes = notes || []
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load trash notes:', err)
|
||||||
|
this.$message.error('加载回收站失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async restoreNote(note) {
|
||||||
|
try {
|
||||||
|
await this.$confirm(`确定要恢复 "${note.title || '未命名'}" 吗?`, '提示', {
|
||||||
|
type: 'info',
|
||||||
|
confirmButtonText: '恢复',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
|
||||||
|
await window.go.main.App.RestoreNote(note.id)
|
||||||
|
await this.loadTrashNotes()
|
||||||
|
await this.loadNoteTree()
|
||||||
|
this.$message.success('已恢复')
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error('Failed to restore note:', err)
|
||||||
|
this.$message.error('恢复失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async permanentDeleteNote(note) {
|
||||||
|
try {
|
||||||
|
await this.$confirm(`确定要永久删除 "${note.title || '未命名'}" 吗?此操作不可恢复!`, '警告', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '永久删除',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
|
||||||
|
await window.go.main.App.PermanentDeleteNote(note.id)
|
||||||
|
await this.loadTrashNotes()
|
||||||
|
this.$message.success('已永久删除')
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error('Failed to permanently delete note:', err)
|
||||||
|
this.$message.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async emptyTrash() {
|
||||||
|
try {
|
||||||
|
await this.$confirm('确定要清空回收站吗?所有笔记将被永久删除!', '警告', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '清空',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
})
|
||||||
|
|
||||||
|
await window.go.main.App.EmptyTrash()
|
||||||
|
await this.loadTrashNotes()
|
||||||
|
this.$message.success('回收站已清空')
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error('Failed to empty trash:', err)
|
||||||
|
this.$message.error('清空失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 图片压缩为 JPEG 格式,质量 50
|
||||||
|
compressImage(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => {
|
||||||
|
const img = new Image()
|
||||||
|
img.src = reader.result
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
// 压缩为 JPEG 格式,质量 50
|
||||||
|
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.5)
|
||||||
|
resolve(compressedDataUrl)
|
||||||
|
}
|
||||||
|
img.onerror = reject
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// mavon-editor 图片上传处理
|
||||||
|
async handleImgAdd(pos, $file) {
|
||||||
|
try {
|
||||||
|
// 压缩图片
|
||||||
|
const compressedDataUrl = await this.compressImage($file)
|
||||||
|
// 保存到后端文件服务器
|
||||||
|
const fileUrl = await window.go.main.App.SaveFile(compressedDataUrl, $file.name)
|
||||||
|
// 替换编辑器中的图片链接
|
||||||
|
this.$refs.md.$img2Url(pos, fileUrl)
|
||||||
|
this.$message.success('图片上传成功')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to upload image:', err)
|
||||||
|
this.$message.error('图片上传失败')
|
||||||
|
this.$refs.md.$img2Url(pos, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImgDel(pos) {
|
||||||
|
// 图片删除时可以做清理工作(可选)
|
||||||
|
console.log('Image deleted at position:', pos)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
showTrash(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.loadTrashNotes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info:hover {
|
||||||
|
background-color: #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state i {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state p {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签页样式 */
|
||||||
|
.tabs-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-header {
|
||||||
|
display: flex;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
overflow-x: auto;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 40px;
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 200px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background-color: #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.is-active {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 2px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 8px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover .tab-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
background-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title-input .el-input__inner {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title-input .el-input__inner:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 覆盖 mavon-editor 样式 */
|
||||||
|
.v-note-wrapper {
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
z-index: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-note-op {
|
||||||
|
border-bottom: 1px solid #e4e7ed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-show-content {
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1 class="login-title">Kenaito Notebook</h1>
|
||||||
|
<p class="login-subtitle">{{ isLogin ? '欢迎回来,请登录' : '创建新账号' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="form"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
@submit.native.prevent="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="form.email"
|
||||||
|
prefix-icon="el-icon-message"
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
prefix-icon="el-icon-lock"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
size="medium"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!isLogin" prop="confirmPassword">
|
||||||
|
<el-input
|
||||||
|
v-model="form.confirmPassword"
|
||||||
|
prefix-icon="el-icon-lock"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
size="medium"
|
||||||
|
show-password
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item v-if="!isLogin" prop="code">
|
||||||
|
<div style="display: flex; gap: 10px;">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
prefix-icon="el-icon-postcard"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
size="medium"
|
||||||
|
style="flex: 1;"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
size="medium"
|
||||||
|
:disabled="codeCooldown > 0 || !form.email"
|
||||||
|
:loading="sendingCode"
|
||||||
|
@click="handleSendCode"
|
||||||
|
>
|
||||||
|
{{ codeCooldown > 0 ? codeCooldown + 's' : '发送验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
native-type="submit"
|
||||||
|
:loading="loading"
|
||||||
|
style="width: 100%"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
{{ loading ? '处理中...' : (isLogin ? '登 录' : '注 册') }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<span>{{ isLogin ? '还没有账号?' : '已有账号?' }}</span>
|
||||||
|
<el-button type="text" @click="toggleMode">
|
||||||
|
{{ isLogin ? '立即注册' : '去登录' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { SendVerifyCode } from '../../wailsjs/go/main/App'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoginView',
|
||||||
|
data() {
|
||||||
|
const validateConfirmPassword = (rule, value, callback) => {
|
||||||
|
if (value !== this.form.password) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isLogin: true,
|
||||||
|
loading: false,
|
||||||
|
sendingCode: false,
|
||||||
|
codeCooldown: 0,
|
||||||
|
cooldownTimer: null,
|
||||||
|
form: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
code: ''
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, message: '密码至少 6 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||||
|
{ validator: validateConfirmPassword, trigger: 'blur' }
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||||
|
{ len: 6, message: '验证码为 6 位数字', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.cooldownTimer) {
|
||||||
|
clearInterval(this.cooldownTimer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMode() {
|
||||||
|
this.isLogin = !this.isLogin
|
||||||
|
if (this.cooldownTimer) {
|
||||||
|
clearInterval(this.cooldownTimer)
|
||||||
|
this.codeCooldown = 0
|
||||||
|
}
|
||||||
|
this.form.code = ''
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.form) {
|
||||||
|
this.$refs.form.clearValidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async handleSendCode() {
|
||||||
|
try {
|
||||||
|
await this.$refs.form.validateField('email')
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendingCode = true
|
||||||
|
try {
|
||||||
|
const purpose = 'register'
|
||||||
|
await SendVerifyCode(this.form.email, purpose)
|
||||||
|
this.$message.success('验证码已发送到您的邮箱')
|
||||||
|
this.startCooldown()
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error(err.toString() || '发送验证码失败')
|
||||||
|
} finally {
|
||||||
|
this.sendingCode = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startCooldown() {
|
||||||
|
this.codeCooldown = 60
|
||||||
|
this.cooldownTimer = setInterval(() => {
|
||||||
|
this.codeCooldown--
|
||||||
|
if (this.codeCooldown <= 0) {
|
||||||
|
clearInterval(this.cooldownTimer)
|
||||||
|
this.cooldownTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
if (!this.$refs.form) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$refs.form.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
if (this.isLogin) {
|
||||||
|
await window.go.main.App.Login(this.form.email, this.form.password)
|
||||||
|
this.$message.success('登录成功')
|
||||||
|
} else {
|
||||||
|
await window.go.main.App.Register(this.form.email, this.form.password, this.form.code)
|
||||||
|
this.$message.success('注册成功')
|
||||||
|
}
|
||||||
|
this.$emit('login-success')
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error(err.toString() || '操作失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item.is-required >>> .el-form-item__label::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
<template>
|
||||||
|
<div class="note-tree" @contextmenu.prevent="showContextMenu($event, null)">
|
||||||
|
<div v-if="!notes || notes.length === 0" class="empty-tree">
|
||||||
|
<p>暂无笔记</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="tree-content">
|
||||||
|
<div
|
||||||
|
v-for="note in notes"
|
||||||
|
:key="note.id"
|
||||||
|
class="tree-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="tree-node"
|
||||||
|
:class="{
|
||||||
|
'is-selected': selectedId === note.id,
|
||||||
|
'is-folder': note.isFolder
|
||||||
|
}"
|
||||||
|
@click="$emit('select', note)"
|
||||||
|
@contextmenu.prevent="showContextMenu($event, note)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="note.isFolder"
|
||||||
|
class="expand-icon"
|
||||||
|
:class="{ 'is-expanded': expandedIds.includes(note.id) }"
|
||||||
|
@click.stop="toggleExpand(note.id)"
|
||||||
|
>
|
||||||
|
<i class="el-icon-arrow-right"></i>
|
||||||
|
</span>
|
||||||
|
<span v-else class="expand-icon-placeholder"></span>
|
||||||
|
|
||||||
|
<span class="node-icon">
|
||||||
|
<i :class="getNodeIcon(note)"></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-if="renamingId === note.id"
|
||||||
|
ref="renameInput"
|
||||||
|
v-model="renameTitle"
|
||||||
|
class="rename-input"
|
||||||
|
@blur="confirmRename(note)"
|
||||||
|
@keyup.enter="confirmRename(note)"
|
||||||
|
@keyup.escape="cancelRename"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<span v-else class="node-label-wrapper">
|
||||||
|
<span class="node-label" :title="note.title">
|
||||||
|
{{ note.title || '未命名' }}
|
||||||
|
</span>
|
||||||
|
<span v-if="note.createdAt" class="node-date">
|
||||||
|
{{ formatDate(note.createdAt) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="note.isFolder && note.children && expandedIds.includes(note.id)"
|
||||||
|
class="tree-children"
|
||||||
|
>
|
||||||
|
<note-tree
|
||||||
|
:notes="note.children"
|
||||||
|
:selected-id="selectedId"
|
||||||
|
@select="$emit('select', $event)"
|
||||||
|
@delete="$emit('delete', $event)"
|
||||||
|
@context-menu="$emit('context-menu', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右键菜单 -->
|
||||||
|
<div
|
||||||
|
v-if="contextMenu.visible"
|
||||||
|
class="context-menu"
|
||||||
|
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<template v-if="contextMenu.note && contextMenu.note.isFolder">
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('newChild')">
|
||||||
|
<i class="el-icon-document-add"></i> 新建笔记
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('newFolder')">
|
||||||
|
<i class="el-icon-folder-add"></i> 新建子文件夹
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-divider"></div>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('rename')">
|
||||||
|
<i class="el-icon-edit"></i> 重命名
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('exportFolder')">
|
||||||
|
<i class="el-icon-download"></i> 导出为 Markdown
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item delete" @click.stop="handleContextAction('delete')">
|
||||||
|
<i class="el-icon-delete"></i> 删除
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="contextMenu.note">
|
||||||
|
<div class="context-menu-item delete" @click.stop="handleContextAction('delete')">
|
||||||
|
<i class="el-icon-delete"></i> 删除
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('newNote')">
|
||||||
|
<i class="el-icon-document-add"></i> 新建笔记
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('newFolder')">
|
||||||
|
<i class="el-icon-folder-add"></i> 新建文件夹
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-divider"></div>
|
||||||
|
<div class="context-menu-item" @click.stop="handleContextAction('exportAll')">
|
||||||
|
<i class="el-icon-download"></i> 导出所有笔记
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NoteTree',
|
||||||
|
props: {
|
||||||
|
notes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selectedId: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expandedIds: [],
|
||||||
|
renamingId: null,
|
||||||
|
renameTitle: '',
|
||||||
|
contextMenu: {
|
||||||
|
visible: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
note: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('click', this.hideContextMenu)
|
||||||
|
document.addEventListener('contextmenu', this.hideContextMenu)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
document.removeEventListener('click', this.hideContextMenu)
|
||||||
|
document.removeEventListener('contextmenu', this.hideContextMenu)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleExpand(id) {
|
||||||
|
const index = this.expandedIds.indexOf(id)
|
||||||
|
if (index > -1) {
|
||||||
|
this.expandedIds.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
this.expandedIds.push(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getNodeIcon(note) {
|
||||||
|
if (note.isFolder) {
|
||||||
|
return this.expandedIds.includes(note.id)
|
||||||
|
? 'el-icon-folder-opened'
|
||||||
|
: 'el-icon-folder'
|
||||||
|
}
|
||||||
|
return 'el-icon-document'
|
||||||
|
},
|
||||||
|
|
||||||
|
showContextMenu(event, note) {
|
||||||
|
event.stopPropagation()
|
||||||
|
this.contextMenu = {
|
||||||
|
visible: true,
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
note: note
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hideContextMenu() {
|
||||||
|
this.contextMenu.visible = false
|
||||||
|
},
|
||||||
|
|
||||||
|
handleContextAction(command) {
|
||||||
|
if (command === 'rename') {
|
||||||
|
this.startRename(this.contextMenu.note)
|
||||||
|
} else {
|
||||||
|
this.$emit('context-menu', {
|
||||||
|
command: command,
|
||||||
|
note: this.contextMenu.note
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.hideContextMenu()
|
||||||
|
},
|
||||||
|
|
||||||
|
startRename(note) {
|
||||||
|
this.renamingId = note.id
|
||||||
|
this.renameTitle = note.title
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.renameInput) {
|
||||||
|
this.$refs.renameInput.focus()
|
||||||
|
this.$refs.renameInput.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async confirmRename(note) {
|
||||||
|
if (!this.renamingId) return
|
||||||
|
const newTitle = this.renameTitle.trim()
|
||||||
|
if (newTitle && newTitle !== note.title) {
|
||||||
|
await window.go.main.App.UpdateNote(note.id, newTitle, note.content, note.tag)
|
||||||
|
this.$emit('context-menu', { command: 'renamed', note })
|
||||||
|
}
|
||||||
|
this.renamingId = null
|
||||||
|
this.renameTitle = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelRename() {
|
||||||
|
this.renamingId = null
|
||||||
|
this.renameTitle = ''
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
notes: {
|
||||||
|
handler(newNotes) {
|
||||||
|
// 自动展开第一级文件夹
|
||||||
|
if (newNotes) {
|
||||||
|
newNotes.forEach(note => {
|
||||||
|
if (note.isFolder && note.children && note.children.length > 0) {
|
||||||
|
if (!this.expandedIds.includes(note.id)) {
|
||||||
|
this.expandedIds.push(note.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.note-tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tree {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-content {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-item {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover {
|
||||||
|
background-color: #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.is-selected {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.is-folder {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon.is-expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon-placeholder {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rename-input {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #409eff;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右键菜单 */
|
||||||
|
.context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.delete {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e4e7ed;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<div class="trash-view">
|
||||||
|
<div class="trash-header">
|
||||||
|
<h3>
|
||||||
|
<i class="el-icon-delete"></i> 回收站
|
||||||
|
<span v-if="trashNotes.length > 0" class="trash-count">({{ trashNotes.length }})</span>
|
||||||
|
</h3>
|
||||||
|
<div class="trash-header-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="trashNotes.length > 0"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-delete-solid"
|
||||||
|
@click="emptyTrash"
|
||||||
|
>
|
||||||
|
清空回收站
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="trashNotes.length === 0" class="empty-trash">
|
||||||
|
<i class="el-icon-folder-delete"></i>
|
||||||
|
<p>回收站为空</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="trash-list">
|
||||||
|
<div
|
||||||
|
v-for="note in trashNotes"
|
||||||
|
:key="note.id"
|
||||||
|
class="trash-item"
|
||||||
|
>
|
||||||
|
<div class="trash-item-icon">
|
||||||
|
<i :class="note.isFolder ? 'el-icon-folder' : 'el-icon-document'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="trash-item-info">
|
||||||
|
<div class="trash-item-title">{{ note.title || '未命名' }}</div>
|
||||||
|
<div class="trash-item-meta">
|
||||||
|
<span v-if="note.deletedAt" class="delete-time">
|
||||||
|
删除于: {{ formatDate(note.deletedAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="trash-item-actions">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="restoreNote(note)"
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="permanentDelete(note)"
|
||||||
|
>
|
||||||
|
永久删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TrashView',
|
||||||
|
props: {
|
||||||
|
trashNotes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||||
|
},
|
||||||
|
|
||||||
|
restoreNote(note) {
|
||||||
|
this.$emit('restore', note)
|
||||||
|
},
|
||||||
|
|
||||||
|
permanentDelete(note) {
|
||||||
|
this.$emit('permanent-delete', note)
|
||||||
|
},
|
||||||
|
|
||||||
|
emptyTrash() {
|
||||||
|
this.$emit('empty-trash')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trash-view {
|
||||||
|
padding: 16px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #303133;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-count {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-trash {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-trash i {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item:hover {
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trash-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import ElementUI from 'element-ui'
|
||||||
|
import 'element-ui/lib/theme-chalk/index.css'
|
||||||
|
import mavonEditor from 'mavon-editor'
|
||||||
|
import 'mavon-editor/dist/css/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
Vue.use(ElementUI, { size: 'small' })
|
||||||
|
Vue.use(mavonEditor)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
render: h => h(App)
|
||||||
|
}).$mount('#app')
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局滚动条样式 */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #c0c4cc;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element UI 样式覆盖 */
|
||||||
|
.el-menu {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-tree {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown 预览样式 */
|
||||||
|
.markdown-body {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1,
|
||||||
|
.markdown-body h2,
|
||||||
|
.markdown-body h3,
|
||||||
|
.markdown-body h4,
|
||||||
|
.markdown-body h5,
|
||||||
|
.markdown-body h6 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h1 {
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
font-size: 2em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body h2 {
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
border-bottom: 1px solid #eaecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body code {
|
||||||
|
background-color: rgba(27, 31, 35, 0.05);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 85%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
display: inline;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
max-width: auto;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body blockquote {
|
||||||
|
border-left: 0.25em solid #dfe2e5;
|
||||||
|
color: #6a737d;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
display: block;
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th,
|
||||||
|
.markdown-body table td {
|
||||||
|
border: 1px solid #dfe2e5;
|
||||||
|
padding: 6px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table th {
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 1px solid #c6cbd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body table tr:nth-child(2n) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img {
|
||||||
|
border-style: none;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a {
|
||||||
|
color: #0366d6;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body hr {
|
||||||
|
height: 0.25em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 24px 0;
|
||||||
|
background-color: #e1e4e8;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {defineConfig} from 'vite'
|
||||||
|
import { createVuePlugin } from 'vite-plugin-vue2'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [createVuePlugin()],
|
||||||
|
server: {
|
||||||
|
// Wails dev 模式下的配置
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
strictPort: true,
|
||||||
|
// 不启用 HMR 的 WebSocket 代理,让 Wails 处理
|
||||||
|
},
|
||||||
|
// 确保资源路径正确
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
// 确保输出目录正确
|
||||||
|
outDir: 'dist',
|
||||||
|
assetsDir: 'assets'
|
||||||
|
}
|
||||||
|
})
|
||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {models} from '../models';
|
||||||
|
|
||||||
|
export function CreateNote(arg1:number,arg2:string,arg3:boolean):Promise<models.Note>;
|
||||||
|
|
||||||
|
export function DeleteNote(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function EmptyTrash():Promise<void>;
|
||||||
|
|
||||||
|
export function ExportAllMarkdown():Promise<string>;
|
||||||
|
|
||||||
|
export function ExportFolderMarkdown(arg1:number):Promise<string>;
|
||||||
|
|
||||||
|
export function ExportHTML(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function ExportMarkdown(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function GetAssetsPath():Promise<string>;
|
||||||
|
|
||||||
|
export function GetFileServerURL():Promise<string>;
|
||||||
|
|
||||||
|
export function GetNote(arg1:number):Promise<models.Note>;
|
||||||
|
|
||||||
|
export function GetNoteTree():Promise<Array<models.NoteTree>>;
|
||||||
|
|
||||||
|
export function GetTrashNotes():Promise<Array<models.Note>>;
|
||||||
|
|
||||||
|
export function GetUser():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function IsLoggedIn():Promise<boolean>;
|
||||||
|
|
||||||
|
export function Login(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function Logout():Promise<void>;
|
||||||
|
|
||||||
|
export function PermanentDeleteNote(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function Register(arg1:string,arg2:string,arg3:string):Promise<string>;
|
||||||
|
|
||||||
|
export function ResetPassword(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||||
|
|
||||||
|
export function RestoreNote(arg1:number):Promise<void>;
|
||||||
|
|
||||||
|
export function RevealInFinder(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function SaveFile(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function SaveImage(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function SearchNotes(arg1:string):Promise<Array<models.Note>>;
|
||||||
|
|
||||||
|
export function SelectDirectory(arg1:string):Promise<string>;
|
||||||
|
|
||||||
|
export function SelectSaveFile(arg1:string,arg2:string):Promise<string>;
|
||||||
|
|
||||||
|
export function SendVerifyCode(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
|
export function UpdateNote(arg1:number,arg2:string,arg3:string,arg4:string):Promise<void>;
|
||||||
|
|
||||||
|
export function VerifyCode(arg1:string,arg2:string,arg3:string):Promise<void>;
|
||||||
Executable
+119
@@ -0,0 +1,119 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function CreateNote(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['CreateNote'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteNote(arg1) {
|
||||||
|
return window['go']['main']['App']['DeleteNote'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyTrash() {
|
||||||
|
return window['go']['main']['App']['EmptyTrash']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportAllMarkdown() {
|
||||||
|
return window['go']['main']['App']['ExportAllMarkdown']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportFolderMarkdown(arg1) {
|
||||||
|
return window['go']['main']['App']['ExportFolderMarkdown'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportHTML(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['ExportHTML'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExportMarkdown(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['ExportMarkdown'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetAssetsPath() {
|
||||||
|
return window['go']['main']['App']['GetAssetsPath']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetFileServerURL() {
|
||||||
|
return window['go']['main']['App']['GetFileServerURL']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetNote(arg1) {
|
||||||
|
return window['go']['main']['App']['GetNote'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetNoteTree() {
|
||||||
|
return window['go']['main']['App']['GetNoteTree']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetTrashNotes() {
|
||||||
|
return window['go']['main']['App']['GetTrashNotes']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetUser() {
|
||||||
|
return window['go']['main']['App']['GetUser']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsLoggedIn() {
|
||||||
|
return window['go']['main']['App']['IsLoggedIn']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Login(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['Login'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Logout() {
|
||||||
|
return window['go']['main']['App']['Logout']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermanentDeleteNote(arg1) {
|
||||||
|
return window['go']['main']['App']['PermanentDeleteNote'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Register(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['Register'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResetPassword(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['ResetPassword'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RestoreNote(arg1) {
|
||||||
|
return window['go']['main']['App']['RestoreNote'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RevealInFinder(arg1) {
|
||||||
|
return window['go']['main']['App']['RevealInFinder'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveFile(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['SaveFile'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveImage(arg1) {
|
||||||
|
return window['go']['main']['App']['SaveImage'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchNotes(arg1) {
|
||||||
|
return window['go']['main']['App']['SearchNotes'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectDirectory(arg1) {
|
||||||
|
return window['go']['main']['App']['SelectDirectory'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectSaveFile(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['SelectSaveFile'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendVerifyCode(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['SendVerifyCode'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateNote(arg1, arg2, arg3, arg4) {
|
||||||
|
return window['go']['main']['App']['UpdateNote'](arg1, arg2, arg3, arg4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VerifyCode(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['VerifyCode'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
Executable
+113
@@ -0,0 +1,113 @@
|
|||||||
|
export namespace models {
|
||||||
|
|
||||||
|
export class Note {
|
||||||
|
id: number;
|
||||||
|
parentId: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
sortOrder: number;
|
||||||
|
isFolder: boolean;
|
||||||
|
tag: string;
|
||||||
|
// Go type: time
|
||||||
|
createdAt: any;
|
||||||
|
// Go type: time
|
||||||
|
updatedAt: any;
|
||||||
|
// Go type: time
|
||||||
|
deletedAt?: any;
|
||||||
|
children?: Note[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new Note(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.parentId = source["parentId"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.content = source["content"];
|
||||||
|
this.sortOrder = source["sortOrder"];
|
||||||
|
this.isFolder = source["isFolder"];
|
||||||
|
this.tag = source["tag"];
|
||||||
|
this.createdAt = this.convertValues(source["createdAt"], null);
|
||||||
|
this.updatedAt = this.convertValues(source["updatedAt"], null);
|
||||||
|
this.deletedAt = this.convertValues(source["deletedAt"], null);
|
||||||
|
this.children = this.convertValues(source["children"], Note);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class NoteTree {
|
||||||
|
id: number;
|
||||||
|
parentId: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
sortOrder: number;
|
||||||
|
isFolder: boolean;
|
||||||
|
tag: string;
|
||||||
|
// Go type: time
|
||||||
|
createdAt: any;
|
||||||
|
// Go type: time
|
||||||
|
updatedAt: any;
|
||||||
|
// Go type: time
|
||||||
|
deletedAt?: any;
|
||||||
|
children?: Note[];
|
||||||
|
children: NoteTree[];
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new NoteTree(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.parentId = source["parentId"];
|
||||||
|
this.title = source["title"];
|
||||||
|
this.content = source["content"];
|
||||||
|
this.sortOrder = source["sortOrder"];
|
||||||
|
this.isFolder = source["isFolder"];
|
||||||
|
this.tag = source["tag"];
|
||||||
|
this.createdAt = this.convertValues(source["createdAt"], null);
|
||||||
|
this.updatedAt = this.convertValues(source["updatedAt"], null);
|
||||||
|
this.deletedAt = this.convertValues(source["deletedAt"], null);
|
||||||
|
this.children = this.convertValues(source["children"], Note);
|
||||||
|
this.children = this.convertValues(source["children"], NoteTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||||
|
if (!a) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.slice && a.map) {
|
||||||
|
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||||
|
} else if ("object" === typeof a) {
|
||||||
|
if (asMap) {
|
||||||
|
for (const key of Object.keys(a)) {
|
||||||
|
a[key] = new classs(a[key]);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
return new classs(a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
+330
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width : number
|
||||||
|
height : number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||||
|
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||||
|
export function WindowIsFullscreen(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||||
|
export function WindowIsMaximised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||||
|
export function WindowIsMinimised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||||
|
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||||
|
export function WindowIsNormal(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
|
|
||||||
|
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||||
|
// Returns the current text stored on clipboard
|
||||||
|
export function ClipboardGetText(): Promise<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||||
|
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||||
|
|
||||||
|
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||||
|
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
export function OnFileDropOff() :void
|
||||||
|
|
||||||
|
// Check if the file path resolver is available
|
||||||
|
export function CanResolveFilePaths(): boolean;
|
||||||
|
|
||||||
|
// Resolves file paths for an array of files
|
||||||
|
export function ResolveFilePaths(files: File[]): void
|
||||||
|
|
||||||
|
// Notification types
|
||||||
|
export interface NotificationOptions {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string; // macOS and Linux only
|
||||||
|
body?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
data?: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationAction {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
destructive?: boolean; // macOS-specific
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationCategory {
|
||||||
|
id?: string;
|
||||||
|
actions?: NotificationAction[];
|
||||||
|
hasReplyField?: boolean;
|
||||||
|
replyPlaceholder?: string;
|
||||||
|
replyButtonTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||||
|
// Initializes the notification service for the application.
|
||||||
|
// This must be called before sending any notifications.
|
||||||
|
export function InitializeNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||||
|
// Cleans up notification resources and releases any held connections.
|
||||||
|
export function CleanupNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||||
|
// Checks if notifications are available on the current platform.
|
||||||
|
export function IsNotificationAvailable(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||||
|
// Requests notification authorization from the user (macOS only).
|
||||||
|
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||||
|
// Checks the current notification authorization status (macOS only).
|
||||||
|
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||||
|
// Sends a basic notification with the given options.
|
||||||
|
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||||
|
|
||||||
|
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||||
|
// Sends a notification with action buttons. Requires a registered category.
|
||||||
|
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||||
|
|
||||||
|
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||||
|
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||||
|
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||||
|
// Removes a previously registered notification category.
|
||||||
|
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||||
|
// Removes all pending notifications from the notification center.
|
||||||
|
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||||
|
// Removes a specific pending notification by its identifier.
|
||||||
|
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||||
|
// Removes all delivered notifications from the notification center.
|
||||||
|
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||||
|
// Removes a specific delivered notification by its identifier.
|
||||||
|
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||||
|
// Removes a notification by its identifier (cross-platform convenience function).
|
||||||
|
export function RemoveNotification(identifier: string): Promise<void>;
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName, ...additionalEventNames) {
|
||||||
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsFullscreen() {
|
||||||
|
return window.runtime.WindowIsFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximised() {
|
||||||
|
return window.runtime.WindowIsMaximised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMinimised() {
|
||||||
|
return window.runtime.WindowIsMinimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsNormal() {
|
||||||
|
return window.runtime.WindowIsNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardGetText() {
|
||||||
|
return window.runtime.ClipboardGetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardSetText(text) {
|
||||||
|
return window.runtime.ClipboardSetText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @callback OnFileDropCallback
|
||||||
|
* @param {number} x - x coordinate of the drop
|
||||||
|
* @param {number} y - y coordinate of the drop
|
||||||
|
* @param {string[]} paths - A list of file paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||||
|
*/
|
||||||
|
export function OnFileDrop(callback, useDropTarget) {
|
||||||
|
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
*/
|
||||||
|
export function OnFileDropOff() {
|
||||||
|
return window.runtime.OnFileDropOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanResolveFilePaths() {
|
||||||
|
return window.runtime.CanResolveFilePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveFilePaths(files) {
|
||||||
|
return window.runtime.ResolveFilePaths(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitializeNotifications() {
|
||||||
|
return window.runtime.InitializeNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CleanupNotifications() {
|
||||||
|
return window.runtime.CleanupNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsNotificationAvailable() {
|
||||||
|
return window.runtime.IsNotificationAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestNotificationAuthorization() {
|
||||||
|
return window.runtime.RequestNotificationAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckNotificationAuthorization() {
|
||||||
|
return window.runtime.CheckNotificationAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotification(options) {
|
||||||
|
return window.runtime.SendNotification(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotificationWithActions(options) {
|
||||||
|
return window.runtime.SendNotificationWithActions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterNotificationCategory(category) {
|
||||||
|
return window.runtime.RegisterNotificationCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotificationCategory(categoryId) {
|
||||||
|
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllPendingNotifications() {
|
||||||
|
return window.runtime.RemoveAllPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemovePendingNotification(identifier) {
|
||||||
|
return window.runtime.RemovePendingNotification(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllDeliveredNotifications() {
|
||||||
|
return window.runtime.RemoveAllDeliveredNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveDeliveredNotification(identifier) {
|
||||||
|
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotification(identifier) {
|
||||||
|
return window.runtime.RemoveNotification(identifier);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
module kenaito-notebook
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v2 v2.12.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// replace github.com/wailsapp/wails/v2 v2.12.0 => /Users/mac/go/pkg/mod
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
|
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||||
|
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||||
|
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||||
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
|
||||||
|
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||||
|
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||||
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create an instance of the app structure
|
||||||
|
app := NewApp()
|
||||||
|
|
||||||
|
// Create application with options
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "kenaito-notebook",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
MinWidth: 800,
|
||||||
|
MinHeight: 600,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
OnStartup: app.startup,
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
Mac: &mac.Options{
|
||||||
|
DisableZoom: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
println("Error:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ParentID int64 `json:"parentId"` // 0 = root level
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
SortOrder int `json:"sortOrder"`
|
||||||
|
IsFolder bool `json:"isFolder"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DeletedAt *time.Time `json:"deletedAt"` // nil = not deleted
|
||||||
|
Children []*Note `json:"children,omitempty"` // 子项(用于树形结构)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoteTree struct {
|
||||||
|
Note
|
||||||
|
Children []*NoteTree `json:"children"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port int
|
||||||
|
BaseURL string
|
||||||
|
SMTP services.EmailConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadProperties(filename string) map[string]string {
|
||||||
|
props := make(map[string]string)
|
||||||
|
|
||||||
|
// Try executable directory first
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err == nil {
|
||||||
|
exeDir := filepath.Dir(exe)
|
||||||
|
props = loadFromFile(filepath.Join(exeDir, filename))
|
||||||
|
if len(props) > 0 {
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try working directory
|
||||||
|
props = loadFromFile(filename)
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromFile(path string) map[string]string {
|
||||||
|
props := make(map[string]string)
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx := strings.Index(line, "="); idx > 0 {
|
||||||
|
key := strings.TrimSpace(line[:idx])
|
||||||
|
value := strings.TrimSpace(line[idx+1:])
|
||||||
|
props[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
props := loadProperties("config.properties")
|
||||||
|
|
||||||
|
port := 8080
|
||||||
|
if p := props["server.port"]; p != "" {
|
||||||
|
if v, err := strconv.Atoi(p); err == nil {
|
||||||
|
port = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := props["server.base_url"]
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = fmt.Sprintf("http://localhost:%d", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
Port: port,
|
||||||
|
BaseURL: baseURL,
|
||||||
|
SMTP: services.EmailConfig{
|
||||||
|
Host: props["smtp.host"],
|
||||||
|
Port: props["smtp.port"],
|
||||||
|
Username: props["smtp.username"],
|
||||||
|
Password: props["smtp.password"],
|
||||||
|
From: props["smtp.from"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
module kenaito-notebook-server
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/yuin/goldmark v1.8.2
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
|
modernc.org/sqlite v1.53.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
modernc.org/libc v1.73.4 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
)
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
|
||||||
|
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
|
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
|
||||||
|
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
|
||||||
|
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
|
||||||
|
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
|
||||||
|
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isValidEmail(email string) bool {
|
||||||
|
_, err := mail.ParseAddress(email)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
db *services.Database
|
||||||
|
auth *services.AuthService
|
||||||
|
email *services.EmailService
|
||||||
|
verify *services.VerificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(db *services.Database, auth *services.AuthService, email *services.EmailService, verify *services.VerificationService) *AuthHandler {
|
||||||
|
return &AuthHandler{db: db, auth: auth, email: email, verify: verify}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
Code string `json:"code" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendCodeRequest struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Purpose string `json:"purpose" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyCodeRequest struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Code string `json:"code" binding:"required"`
|
||||||
|
Purpose string `json:"purpose" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetPasswordRequest struct {
|
||||||
|
Email string `json:"email" binding:"required"`
|
||||||
|
Code string `json:"code" binding:"required"`
|
||||||
|
NewPassword string `json:"newPassword" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendCode 发送验证码
|
||||||
|
func (h *AuthHandler) SendCode(c *gin.Context) {
|
||||||
|
var req SendCodeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("[Auth] SendCode bind error: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidEmail(req.Email) {
|
||||||
|
log.Printf("[Auth] SendCode invalid email: %s", req.Email)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Purpose != "register" && req.Purpose != "reset" {
|
||||||
|
log.Printf("[Auth] SendCode invalid purpose: %s", req.Purpose)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求类型"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册时检查邮箱是否已注册
|
||||||
|
if req.Purpose == "register" {
|
||||||
|
existing, _ := h.db.GetUserByEmail(req.Email)
|
||||||
|
if existing != nil {
|
||||||
|
log.Printf("[Auth] SendCode email already registered: %s", req.Email)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置密码时检查邮箱是否存在
|
||||||
|
if req.Purpose == "reset" {
|
||||||
|
existing, _ := h.db.GetUserByEmail(req.Email)
|
||||||
|
if existing == nil {
|
||||||
|
log.Printf("[Auth] SendCode email not found for reset: %s", req.Email)
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "邮箱未注册"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限频检查
|
||||||
|
if !h.verify.CheckRateLimit(req.Email, req.Purpose) {
|
||||||
|
log.Printf("[Auth] SendCode rate limit exceeded: %s, purpose: %s", req.Email, req.Purpose)
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{"error": "发送过于频繁,请 60 秒后重试"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
code := h.verify.GenerateCode(req.Email, req.Purpose)
|
||||||
|
if err := h.email.SendVerificationCode(req.Email, code, req.Purpose); err != nil {
|
||||||
|
log.Printf("[Auth] SendCode send email failed: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "发送验证码失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "验证码已发送"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCode 验证验证码
|
||||||
|
func (h *AuthHandler) VerifyCode(c *gin.Context) {
|
||||||
|
var req VerifyCodeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("[Auth] VerifyCode bind error: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.verify.VerifyCode(req.Email, req.Code, req.Purpose); err != nil {
|
||||||
|
log.Printf("[Auth] VerifyCode failed: email=%s, purpose=%s, err=%v", req.Email, req.Purpose, err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "验证码正确"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 用户注册
|
||||||
|
func (h *AuthHandler) Register(c *gin.Context) {
|
||||||
|
var req RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("[Auth] Register bind error: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidEmail(req.Email) {
|
||||||
|
log.Printf("[Auth] Register invalid email: %s", req.Email)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已注册
|
||||||
|
existing, _ := h.db.GetUserByEmail(req.Email)
|
||||||
|
if existing != nil {
|
||||||
|
log.Printf("[Auth] Register email already registered: %s", req.Email)
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已被注册"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证并消耗验证码
|
||||||
|
if err := h.verify.UseCode(req.Email, req.Code, "register"); err != nil {
|
||||||
|
log.Printf("[Auth] Register use code failed: email=%s, err=%v", req.Email, err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 哈希密码
|
||||||
|
hashedPassword, err := h.auth.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Register hash password failed: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
user, err := h.db.CreateUser(req.Email, hashedPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Register create user failed: email=%s, err=%v", req.Email, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 token
|
||||||
|
token, err := h.auth.GenerateToken(user.ID, user.Email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Register generate token failed: userID=%d, err=%v", user.ID, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "注册失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"token": token,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
func (h *AuthHandler) Login(c *gin.Context) {
|
||||||
|
var req LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("[Auth] Login bind error: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写邮箱和密码"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidEmail(req.Email) {
|
||||||
|
log.Printf("[Auth] Login invalid email: %s", req.Email)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
user, err := h.db.GetUserByEmail(req.Email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Login user not found: %s, err=%v", req.Email, err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if !h.auth.CheckPassword(req.Password, user.HashedPassword) {
|
||||||
|
log.Printf("[Auth] Login password mismatch: email=%s", req.Email)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 token
|
||||||
|
token, err := h.auth.GenerateToken(user.ID, user.Email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Login generate token failed: userID=%d, err=%v", user.ID, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "登录失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"token": token,
|
||||||
|
"user": user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword 重置密码
|
||||||
|
func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||||
|
var req ResetPasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
log.Printf("[Auth] ResetPassword bind error: %v", err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写完整信息"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidEmail(req.Email) {
|
||||||
|
log.Printf("[Auth] ResetPassword invalid email: %s", req.Email)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证并消耗验证码
|
||||||
|
if err := h.verify.UseCode(req.Email, req.Code, "reset"); err != nil {
|
||||||
|
log.Printf("[Auth] ResetPassword use code failed: email=%s, err=%v", req.Email, err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
user, err := h.db.GetUserByEmail(req.Email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] ResetPassword user not found: %s, err=%v", req.Email, err)
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 哈希新密码
|
||||||
|
hashedPassword, err := h.auth.HashPassword(req.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] ResetPassword hash password failed: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
if err := h.db.UpdatePassword(user.ID, hashedPassword); err != nil {
|
||||||
|
log.Printf("[Auth] ResetPassword update password failed: userID=%d, err=%v", user.ID, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "密码重置成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMe 获取当前用户信息
|
||||||
|
func (h *AuthHandler) GetMe(c *gin.Context) {
|
||||||
|
userID, exists := c.Get("userID")
|
||||||
|
if !exists {
|
||||||
|
log.Printf("[Auth] GetMe userID not found in context")
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.db.GetUserByID(userID.(int64))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] GetMe user not found: userID=%v, err=%v", userID, err)
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthMiddleware JWT 认证中间件
|
||||||
|
func AuthMiddleware(auth *services.AuthService) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
log.Printf("[Auth] Middleware missing Authorization header: ip=%s", c.ClientIP())
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 Bearer token
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
|
log.Printf("[Auth] Middleware invalid auth format: ip=%s", c.ClientIP())
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := auth.ValidateToken(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Auth] Middleware token validation failed: ip=%s, err=%v", c.ClientIP(), err)
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息存入上下文
|
||||||
|
c.Set("userID", payload.UserID)
|
||||||
|
c.Set("email", payload.Email)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/models"
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExportHandler struct {
|
||||||
|
db *services.Database
|
||||||
|
exportSvc *services.ExportService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExportHandler(db *services.Database, exportSvc *services.ExportService) *ExportHandler {
|
||||||
|
return &ExportHandler{db: db, exportSvc: exportSvc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExportHandler) ExportHTML(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath, err := h.getTempExportPath(req.Title, ".html")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.exportSvc.ExportHTML(req.Title, req.Content, outputPath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExportHandler) ExportMarkdown(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath, err := h.getTempExportPath(req.Title, ".md")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.exportSvc.ExportMarkdown(req.Title, req.Content, outputPath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExportHandler) ExportFolderZip(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
folderID, err := strconv.ParseInt(c.Param("folderId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid folder id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
folderTree, err := h.db.GetFolderTree(userID, folderID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
noteInfoTree := convertNoteToNoteInfo(folderTree)
|
||||||
|
|
||||||
|
outputPath, err := h.getTempExportPath(folderTree.Title, ".zip")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.exportSvc.ExportFolderZip(folderTree.Title, noteInfoTree.Children, outputPath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExportHandler) ExportAllZip(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
allNotesTree, err := h.db.GetAllNotesTree(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
noteInfos := make([]*services.NoteInfo, len(allNotesTree))
|
||||||
|
for i, noteTree := range allNotesTree {
|
||||||
|
noteInfos[i] = convertNoteTreeToNoteInfo(noteTree)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath, err := h.getTempExportPath("所有笔记", ".zip")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.exportSvc.ExportAllZip(noteInfos, outputPath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"filePath": outputPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ExportHandler) getTempExportPath(title, ext string) (string, error) {
|
||||||
|
tmpDir := filepath.Join(os.TempDir(), "kenaito-notebook-export")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create temp export dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
safeName := h.exportSvc.GetSafeFilename(title)
|
||||||
|
return filepath.Join(tmpDir, fmt.Sprintf("%s%s", safeName, ext)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNoteToNoteInfo(note *models.Note) *services.NoteInfo {
|
||||||
|
if note == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &services.NoteInfo{
|
||||||
|
Title: note.Title,
|
||||||
|
Content: note.Content,
|
||||||
|
IsFolder: note.IsFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
if note.Children != nil && len(note.Children) > 0 {
|
||||||
|
info.Children = make([]*services.NoteInfo, len(note.Children))
|
||||||
|
for i, child := range note.Children {
|
||||||
|
info.Children[i] = convertNoteToNoteInfo(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertNoteTreeToNoteInfo(noteTree *models.NoteTree) *services.NoteInfo {
|
||||||
|
if noteTree == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
info := &services.NoteInfo{
|
||||||
|
Title: noteTree.Title,
|
||||||
|
Content: noteTree.Content,
|
||||||
|
IsFolder: noteTree.IsFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
if noteTree.Children != nil && len(noteTree.Children) > 0 {
|
||||||
|
info.Children = make([]*services.NoteInfo, len(noteTree.Children))
|
||||||
|
for i, child := range noteTree.Children {
|
||||||
|
info.Children[i] = convertNoteTreeToNoteInfo(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileHandler struct {
|
||||||
|
fileSvc *services.FileService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileHandler(fileSvc *services.FileService) *FileHandler {
|
||||||
|
return &FileHandler{fileSvc: fileSvc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FileHandler) SaveImage(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Data string `json:"data" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := h.fileSvc.SaveImage(req.Data)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"url": url})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FileHandler) SaveFile(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
Data string `json:"data" binding:"required"`
|
||||||
|
Filename string `json:"filename" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := h.fileSvc.SaveFile(req.Data, req.Filename)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"url": url})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FileHandler) GetFileServerURL(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"url": h.fileSvc.GetBaseURL()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FileHandler) GetAssetsPath(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"path": h.fileSvc.GetAssetsPath()})
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoteHandler struct {
|
||||||
|
db *services.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNoteHandler(db *services.Database) *NoteHandler {
|
||||||
|
return &NoteHandler{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) GetNoteTree(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
result, err := h.db.GetNoteTree(userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[API] GetNoteTree error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) GetNote(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
note, err := h.db.GetNote(id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) CreateNote(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
ParentID int64 `json:"parentId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
IsFolder bool `json:"isFolder"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
note, err := h.db.CreateNote(userID, req.ParentID, req.Title, req.IsFolder)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) UpdateNote(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.UpdateNote(id, req.Title, req.Content, req.Tag); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) DeleteNote(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.DeleteNote(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) SearchNotes(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
keyword := c.Query("q")
|
||||||
|
if keyword == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "keyword is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notes, err := h.db.SearchNotes(userID, keyword)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) GetTrashNotes(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
notes, err := h.db.GetTrashNotes(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) RestoreNote(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.RestoreNote(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "restored"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) PermanentDeleteNote(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.PermanentDeleteNote(id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "permanently deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NoteHandler) EmptyTrash(c *gin.Context) {
|
||||||
|
userID := c.GetInt64("userID")
|
||||||
|
|
||||||
|
if err := h.db.EmptyTrash(userID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "trash emptied"})
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/handlers"
|
||||||
|
"kenaito-notebook-server/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
db, err := services.NewDatabase()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
log.Println("[Server] Database initialized")
|
||||||
|
|
||||||
|
fileSvc, err := services.NewFileService(cfg.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize file service: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("[Server] File service initialized")
|
||||||
|
|
||||||
|
exportSvc := services.NewExportService()
|
||||||
|
authSvc := services.NewAuthService()
|
||||||
|
emailSvc := services.NewEmailService(cfg.SMTP)
|
||||||
|
verifySvc := services.NewVerificationService()
|
||||||
|
log.Println("[Server] Services initialized")
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
|
noteHandler := handlers.NewNoteHandler(db)
|
||||||
|
fileHandler := handlers.NewFileHandler(fileSvc)
|
||||||
|
exportHandler := handlers.NewExportHandler(db, exportSvc)
|
||||||
|
authHandler := handlers.NewAuthHandler(db, authSvc, emailSvc, verifySvc)
|
||||||
|
|
||||||
|
// 静态文件服务(图片、附件等)
|
||||||
|
r.Static("/api/files/assets", fileSvc.GetAssetsPath())
|
||||||
|
|
||||||
|
api := r.Group("/api")
|
||||||
|
{
|
||||||
|
// 认证路由(无需登录)
|
||||||
|
auth := api.Group("/auth")
|
||||||
|
{
|
||||||
|
auth.POST("/register", authHandler.Register)
|
||||||
|
auth.POST("/login", authHandler.Login)
|
||||||
|
auth.POST("/send-code", authHandler.SendCode)
|
||||||
|
auth.POST("/verify-code", authHandler.VerifyCode)
|
||||||
|
auth.POST("/reset-password", authHandler.ResetPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要登录的路由
|
||||||
|
protected := api.Group("")
|
||||||
|
protected.Use(handlers.AuthMiddleware(authSvc))
|
||||||
|
{
|
||||||
|
// 用户信息
|
||||||
|
protected.GET("/auth/me", authHandler.GetMe)
|
||||||
|
|
||||||
|
// 笔记管理
|
||||||
|
notes := protected.Group("/notes")
|
||||||
|
{
|
||||||
|
notes.GET("/tree", noteHandler.GetNoteTree)
|
||||||
|
notes.GET("/search", noteHandler.SearchNotes)
|
||||||
|
notes.GET("/:id", noteHandler.GetNote)
|
||||||
|
notes.POST("", noteHandler.CreateNote)
|
||||||
|
notes.PUT("/:id", noteHandler.UpdateNote)
|
||||||
|
notes.DELETE("/:id", noteHandler.DeleteNote)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回收站
|
||||||
|
trash := protected.Group("/trash")
|
||||||
|
{
|
||||||
|
trash.GET("", noteHandler.GetTrashNotes)
|
||||||
|
trash.POST("/:id/restore", noteHandler.RestoreNote)
|
||||||
|
trash.DELETE("/:id", noteHandler.PermanentDeleteNote)
|
||||||
|
trash.DELETE("", noteHandler.EmptyTrash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件管理
|
||||||
|
files := protected.Group("/files")
|
||||||
|
{
|
||||||
|
files.POST("/image", fileHandler.SaveImage)
|
||||||
|
files.POST("/upload", fileHandler.SaveFile)
|
||||||
|
files.GET("/server-url", fileHandler.GetFileServerURL)
|
||||||
|
files.GET("/assets-path", fileHandler.GetAssetsPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
export := protected.Group("/export")
|
||||||
|
{
|
||||||
|
export.POST("/html", exportHandler.ExportHTML)
|
||||||
|
export.POST("/markdown", exportHandler.ExportMarkdown)
|
||||||
|
export.POST("/folder-zip/:folderId", exportHandler.ExportFolderZip)
|
||||||
|
export.POST("/all-zip", exportHandler.ExportAllZip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf(":%d", cfg.Port)
|
||||||
|
log.Printf("[Server] Starting on %s", addr)
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
log.Fatalf("Failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Note struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ParentID int64 `json:"parentId"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
SortOrder int `json:"sortOrder"`
|
||||||
|
IsFolder bool `json:"isFolder"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DeletedAt *time.Time `json:"deletedAt"`
|
||||||
|
Children []*Note `json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoteTree struct {
|
||||||
|
Note
|
||||||
|
Children []*NoteTree `json:"children"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidCredentials = errors.New("邮箱或密码错误")
|
||||||
|
ErrUserExists = errors.New("邮箱已被注册")
|
||||||
|
ErrInvalidToken = errors.New("无效的 token")
|
||||||
|
ErrTokenExpired = errors.New("token 已过期")
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWT 密钥,生产环境应从环境变量或配置文件读取
|
||||||
|
var jwtSecret = []byte("kenaito-notebook-jwt-secret-2026")
|
||||||
|
|
||||||
|
type jwtHeader struct {
|
||||||
|
Alg string `json:"alg"`
|
||||||
|
Typ string `json:"typ"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwtPayload struct {
|
||||||
|
UserID int64 `json:"userId"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
Iat int64 `json:"iat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthService struct{}
|
||||||
|
|
||||||
|
func NewAuthService() *AuthService {
|
||||||
|
return &AuthService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword 使用 bcrypt 哈希密码
|
||||||
|
func (s *AuthService) HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPassword 验证密码
|
||||||
|
func (s *AuthService) CheckPassword(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateToken 生成 JWT token
|
||||||
|
func (s *AuthService) GenerateToken(userID int64, email string) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
payload := jwtPayload{
|
||||||
|
UserID: userID,
|
||||||
|
Email: email,
|
||||||
|
Exp: now.Add(7 * 24 * time.Hour).Unix(), // 7 天过期
|
||||||
|
Iat: now.Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
header := jwtHeader{Alg: "HS256", Typ: "JWT"}
|
||||||
|
headerJSON, err := json.Marshal(header)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||||
|
|
||||||
|
// Payload
|
||||||
|
payloadJSON, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
|
||||||
|
|
||||||
|
// 签名
|
||||||
|
signingInput := headerB64 + "." + payloadB64
|
||||||
|
mac := hmac.New(sha256.New, jwtSecret)
|
||||||
|
mac.Write([]byte(signingInput))
|
||||||
|
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
return signingInput + "." + signature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateToken 验证 JWT token,返回 payload
|
||||||
|
func (s *AuthService) ValidateToken(tokenStr string) (*jwtPayload, error) {
|
||||||
|
parts := strings.Split(tokenStr, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
signingInput := parts[0] + "." + parts[1]
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
mac := hmac.New(sha256.New, jwtSecret)
|
||||||
|
mac.Write([]byte(signingInput))
|
||||||
|
expectedSig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||||
|
|
||||||
|
if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 payload
|
||||||
|
payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload jwtPayload
|
||||||
|
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查过期
|
||||||
|
if time.Now().Unix() > payload.Exp {
|
||||||
|
return nil, ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return &payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractTokenID 从 token 字符串中提取用户 ID(不验证签名,仅用于快速检查)
|
||||||
|
func ExtractTokenID(token string) (int64, error) {
|
||||||
|
parts := strings.Split(token, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return 0, fmt.Errorf("invalid token format")
|
||||||
|
}
|
||||||
|
payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var payload jwtPayload
|
||||||
|
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return payload.UserID, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
|
"kenaito-notebook-server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase() (*Database, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbDir := filepath.Join(homeDir, ".kenaito-notebook")
|
||||||
|
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := filepath.Join(dbDir, "notes.db")
|
||||||
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, fmt.Errorf("failed to enable WAL mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createTables(db); err != nil {
|
||||||
|
db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Database{db: db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTables(db *sql.DB) error {
|
||||||
|
query := `
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL DEFAULT 0,
|
||||||
|
parent_id INTEGER DEFAULT 0,
|
||||||
|
title TEXT NOT NULL DEFAULT '',
|
||||||
|
content TEXT DEFAULT '',
|
||||||
|
sort_order INTEGER DEFAULT 0,
|
||||||
|
is_folder BOOLEAN DEFAULT FALSE,
|
||||||
|
tag TEXT DEFAULT '',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_parent_id ON notes(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_sort_order ON notes(sort_order);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notes_user_id ON notes(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
hashed_password TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
// 迁移:给已有表加 deleted_at 列
|
||||||
|
var count int
|
||||||
|
err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='deleted_at'").Scan(&count)
|
||||||
|
if err == nil && count == 0 {
|
||||||
|
_, err = db.Exec("ALTER TABLE notes ADD COLUMN deleted_at DATETIME DEFAULT NULL")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移:给已有表加 user_id 列
|
||||||
|
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('notes') WHERE name='user_id'").Scan(&count)
|
||||||
|
if err == nil && count == 0 {
|
||||||
|
_, err = db.Exec("ALTER TABLE notes ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec(query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) Close() error {
|
||||||
|
return d.db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 笔记相关方法 ---
|
||||||
|
|
||||||
|
func (d *Database) CreateNote(userID int64, parentID int64, title string, isFolder bool) (*models.Note, error) {
|
||||||
|
now := time.Now()
|
||||||
|
result, err := d.db.Exec(`
|
||||||
|
INSERT INTO notes (user_id, parent_id, title, is_folder, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`, userID, parentID, title, isFolder, now, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.Note{
|
||||||
|
ID: id,
|
||||||
|
ParentID: parentID,
|
||||||
|
Title: title,
|
||||||
|
IsFolder: isFolder,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetNote(id int64) (*models.Note, error) {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := d.db.QueryRow(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes WHERE id = ?
|
||||||
|
`, id).Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return note, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetNotesByParentID(userID, parentID int64) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes WHERE user_id = ? AND parent_id = ? AND deleted_at IS NULL
|
||||||
|
ORDER BY sort_order, created_at
|
||||||
|
`, userID, parentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetAllNotes(userID int64) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes WHERE user_id = ? AND deleted_at IS NULL AND is_folder = FALSE
|
||||||
|
ORDER BY sort_order, created_at
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetAllNotesWithFolders(userID int64) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes WHERE user_id = ? AND deleted_at IS NULL
|
||||||
|
ORDER BY sort_order, created_at
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetFolderTree(userID, folderID int64) (*models.Note, error) {
|
||||||
|
folder, err := d.GetNote(folderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
children, err := d.GetNotesByParentID(userID, folderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.Children = make([]*models.Note, len(children))
|
||||||
|
for i, child := range children {
|
||||||
|
if child.IsFolder {
|
||||||
|
childFolder, err := d.GetFolderTree(userID, child.ID)
|
||||||
|
if err == nil {
|
||||||
|
folder.Children[i] = childFolder
|
||||||
|
} else {
|
||||||
|
folder.Children[i] = child
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
folder.Children[i] = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return folder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetAllNotesTree(userID int64) ([]*models.NoteTree, error) {
|
||||||
|
notes, err := d.GetAllNotesWithFolders(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTree(notes, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateNote(id int64, title, content, tag string) error {
|
||||||
|
now := time.Now()
|
||||||
|
_, err := d.db.Exec(`
|
||||||
|
UPDATE notes SET title = ?, content = ?, tag = ?, updated_at = ?
|
||||||
|
WHERE id = ?
|
||||||
|
`, title, content, tag, now, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) DeleteNote(id int64) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
err := d.softDeleteChildren(id, now)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE id = ?", now, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) softDeleteChildren(parentID int64, deletedAt time.Time) error {
|
||||||
|
rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ? AND deleted_at IS NULL", parentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.softDeleteChildren(id, deletedAt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.db.Exec("UPDATE notes SET deleted_at = ? WHERE parent_id = ? AND deleted_at IS NULL", deletedAt, parentID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetNoteTree(userID int64) ([]*models.NoteTree, error) {
|
||||||
|
notes, err := d.getAllNotes(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTree(notes, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) getAllNotes(userID int64) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes
|
||||||
|
WHERE user_id = ? AND deleted_at IS NULL
|
||||||
|
ORDER BY sort_order, created_at
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTree(notes []*models.Note, parentID int64) []*models.NoteTree {
|
||||||
|
var result []*models.NoteTree
|
||||||
|
for _, note := range notes {
|
||||||
|
if note.ParentID == parentID {
|
||||||
|
node := &models.NoteTree{
|
||||||
|
Note: *note,
|
||||||
|
}
|
||||||
|
node.Children = buildTree(notes, note.ID)
|
||||||
|
result = append(result, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) SearchNotes(userID int64, keyword string) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes
|
||||||
|
WHERE user_id = ? AND (title LIKE ? OR content LIKE ?) AND deleted_at IS NULL
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
`, userID, "%"+keyword+"%", "%"+keyword+"%")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateSortOrder(id int64, sortOrder int) error {
|
||||||
|
_, err := d.db.Exec("UPDATE notes SET sort_order = ? WHERE id = ?", sortOrder, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetTrashNotes(userID int64) ([]*models.Note, error) {
|
||||||
|
rows, err := d.db.Query(`
|
||||||
|
SELECT id, parent_id, title, content, sort_order, is_folder, tag, created_at, updated_at, deleted_at
|
||||||
|
FROM notes
|
||||||
|
WHERE user_id = ? AND deleted_at IS NOT NULL
|
||||||
|
ORDER BY deleted_at DESC
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var notes []*models.Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := &models.Note{}
|
||||||
|
err := rows.Scan(
|
||||||
|
¬e.ID, ¬e.ParentID, ¬e.Title, ¬e.Content,
|
||||||
|
¬e.SortOrder, ¬e.IsFolder, ¬e.Tag,
|
||||||
|
¬e.CreatedAt, ¬e.UpdatedAt, ¬e.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
return notes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) RestoreNote(id int64) error {
|
||||||
|
note, err := d.GetNote(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if note.IsFolder {
|
||||||
|
err = d.restoreChildrenRecursive(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE id = ?", id)
|
||||||
|
} else {
|
||||||
|
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL, parent_id = 0 WHERE id = ?", id)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) restoreChildrenRecursive(parentID int64) error {
|
||||||
|
var parentDeletedAt *time.Time
|
||||||
|
err := d.db.QueryRow("SELECT deleted_at FROM notes WHERE id = ?", parentID).Scan(&parentDeletedAt)
|
||||||
|
if err != nil || parentDeletedAt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := d.db.Query(
|
||||||
|
"SELECT id, is_folder FROM notes WHERE parent_id = ? AND deleted_at = ?",
|
||||||
|
parentID, *parentDeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
type childInfo struct {
|
||||||
|
id int64
|
||||||
|
isFolder bool
|
||||||
|
}
|
||||||
|
var children []childInfo
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var child childInfo
|
||||||
|
if err := rows.Scan(&child.id, &child.isFolder); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
children = append(children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range children {
|
||||||
|
if child.isFolder {
|
||||||
|
if err := d.restoreChildrenRecursive(child.id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(children) > 0 {
|
||||||
|
_, err = d.db.Exec("UPDATE notes SET deleted_at = NULL WHERE parent_id = ? AND deleted_at = ?", parentID, *parentDeletedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) PermanentDeleteNote(id int64) error {
|
||||||
|
err := d.permanentDeleteChildren(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.db.Exec("DELETE FROM notes WHERE id = ?", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) permanentDeleteChildren(parentID int64) error {
|
||||||
|
rows, err := d.db.Query("SELECT id FROM notes WHERE parent_id = ?", parentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.permanentDeleteChildren(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = d.db.Exec("DELETE FROM notes WHERE parent_id = ?", parentID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) EmptyTrash(userID int64) error {
|
||||||
|
_, err := d.db.Exec("DELETE FROM notes WHERE user_id = ? AND deleted_at IS NOT NULL", userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 用户相关方法 ---
|
||||||
|
|
||||||
|
type UserRecord struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
HashedPassword string `json:"-"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) CreateUser(email, hashedPassword string) (*UserRecord, error) {
|
||||||
|
now := time.Now()
|
||||||
|
result, err := d.db.Exec(`
|
||||||
|
INSERT INTO users (email, hashed_password, created_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`, email, hashedPassword, now)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserRecord{
|
||||||
|
ID: id,
|
||||||
|
HashedPassword: hashedPassword,
|
||||||
|
Email: email,
|
||||||
|
CreatedAt: now,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetUserByEmail(email string) (*UserRecord, error) {
|
||||||
|
user := &UserRecord{}
|
||||||
|
err := d.db.QueryRow(`
|
||||||
|
SELECT id, hashed_password, email, created_at
|
||||||
|
FROM users WHERE email = ?
|
||||||
|
`, email).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetUserByID(id int64) (*UserRecord, error) {
|
||||||
|
user := &UserRecord{}
|
||||||
|
err := d.db.QueryRow(`
|
||||||
|
SELECT id, hashed_password, email, created_at
|
||||||
|
FROM users WHERE id = ?
|
||||||
|
`, id).Scan(&user.ID, &user.HashedPassword, &user.Email, &user.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdatePassword(id int64, hashedPassword string) error {
|
||||||
|
_, err := d.db.Exec("UPDATE users SET hashed_password = ? WHERE id = ?", hashedPassword, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailConfig struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
From string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailService struct {
|
||||||
|
cfg EmailConfig
|
||||||
|
configured bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEmailService(cfg EmailConfig) *EmailService {
|
||||||
|
configured := cfg.Host != "" && cfg.Username != "" && cfg.Password != ""
|
||||||
|
if !configured {
|
||||||
|
log.Println("[Email] SMTP not configured, will print codes to console")
|
||||||
|
}
|
||||||
|
return &EmailService{cfg: cfg, configured: configured}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendVerificationCode 发送验证码邮件
|
||||||
|
func (s *EmailService) SendVerificationCode(to, code, purpose string) error {
|
||||||
|
var subject, body string
|
||||||
|
switch purpose {
|
||||||
|
case "register":
|
||||||
|
subject = "Kenaito Notebook - 注册验证码"
|
||||||
|
body = fmt.Sprintf(`
|
||||||
|
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
||||||
|
<h2 style="color: #333;">Kenaito Notebook</h2>
|
||||||
|
<p>你正在注册账号,验证码如下:</p>
|
||||||
|
<div style="background: #f5f7fa; padding: 16px; border-radius: 8px; text-align: center; margin: 20px 0;">
|
||||||
|
<span style="font-size: 32px; font-weight: bold; color: #409eff; letter-spacing: 8px;">%s</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #909399; font-size: 14px;">验证码 5 分钟内有效,请勿泄露给他人。</p>
|
||||||
|
</div>`, code)
|
||||||
|
case "reset":
|
||||||
|
subject = "Kenaito Notebook - 重置密码验证码"
|
||||||
|
body = fmt.Sprintf(`
|
||||||
|
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
||||||
|
<h2 style="color: #333;">Kenaito Notebook</h2>
|
||||||
|
<p>你正在重置密码,验证码如下:</p>
|
||||||
|
<div style="background: #f5f7fa; padding: 16px; border-radius: 8px; text-align: center; margin: 20px 0;">
|
||||||
|
<span style="font-size: 32px; font-weight: bold; color: #e6a23c; letter-spacing: 8px;">%s</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #909399; font-size: 14px;">验证码 5 分钟内有效,请勿泄露给他人。</p>
|
||||||
|
</div>`, code)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown purpose: %s", purpose)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.configured {
|
||||||
|
log.Printf("[Email] SMTP not configured. Verification code for %s (%s): %s", to, purpose, code)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.send(to, subject, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmailService) send(to, subject, htmlBody string) error {
|
||||||
|
addr := net.JoinHostPort(s.cfg.Host, s.cfg.Port)
|
||||||
|
|
||||||
|
headers := make(map[string]string)
|
||||||
|
headers["From"] = s.cfg.From
|
||||||
|
headers["To"] = to
|
||||||
|
headers["Subject"] = subject
|
||||||
|
headers["MIME-Version"] = "1.0"
|
||||||
|
headers["Content-Type"] = "text/html; charset=UTF-8"
|
||||||
|
|
||||||
|
var msg strings.Builder
|
||||||
|
for k, v := range headers {
|
||||||
|
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
|
||||||
|
}
|
||||||
|
msg.WriteString("\r\n")
|
||||||
|
msg.WriteString(htmlBody)
|
||||||
|
|
||||||
|
auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.Host)
|
||||||
|
|
||||||
|
// SSL connection (port 465)
|
||||||
|
if s.cfg.Port == "465" {
|
||||||
|
return s.sendTLS(addr, auth, to, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// STARTTLS connection (port 587)
|
||||||
|
return smtp.SendMail(addr, auth, s.cfg.From, []string{to}, []byte(msg.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EmailService) sendTLS(addr string, auth smtp.Auth, to, msg string) error {
|
||||||
|
conn, err := tls.Dial("tcp", addr, &tls.Config{
|
||||||
|
ServerName: s.cfg.Host,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to SMTP server: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client, err := smtp.NewClient(conn, s.cfg.Host)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create SMTP client: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err = client.Auth(auth); err != nil {
|
||||||
|
return fmt.Errorf("SMTP auth failed: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Mail(s.cfg.From); err != nil {
|
||||||
|
return fmt.Errorf("SMTP MAIL FROM failed: %w", err)
|
||||||
|
}
|
||||||
|
if err = client.Rcpt(to); err != nil {
|
||||||
|
return fmt.Errorf("SMTP RCPT TO failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := client.Data()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SMTP DATA failed: %w", err)
|
||||||
|
}
|
||||||
|
if _, err = w.Write([]byte(msg)); err != nil {
|
||||||
|
return fmt.Errorf("SMTP write failed: %w", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
return fmt.Errorf("SMTP close data failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Quit()
|
||||||
|
}
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExportService struct{}
|
||||||
|
|
||||||
|
func NewExportService() *ExportService {
|
||||||
|
return &ExportService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportHTML(title, content, outputPath string) error {
|
||||||
|
var htmlContent strings.Builder
|
||||||
|
if err := goldmark.Convert([]byte(content), &htmlContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlTemplate := `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||||
|
code {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "Consolas", "Monaco", monospace;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #ddd;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{.Title}}</h1>
|
||||||
|
{{.Content}}
|
||||||
|
<hr>
|
||||||
|
<p style="color: #999; font-size: 12px;">
|
||||||
|
导出于 {{.ExportTime}} | Kenaito Notebook
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
tmpl, err := template.New("html").Parse(htmlTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return tmpl.Execute(file, map[string]string{
|
||||||
|
"Title": title,
|
||||||
|
"Content": htmlContent.String(),
|
||||||
|
"ExportTime": fmt.Sprintf("%s", time.Now().Format("2006-01-02 15:04:05")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportMarkdown(title, content, outputPath string) error {
|
||||||
|
fullContent := fmt.Sprintf("# %s\n\n%s", title, content)
|
||||||
|
return os.WriteFile(outputPath, []byte(fullContent), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportFolderMarkdown(folderTitle string, notes []NoteInfo, outputPath string) error {
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(fmt.Sprintf("# %s\n\n", folderTitle))
|
||||||
|
|
||||||
|
for _, note := range notes {
|
||||||
|
content.WriteString(fmt.Sprintf("## %s\n\n", note.Title))
|
||||||
|
content.WriteString(note.Content)
|
||||||
|
content.WriteString("\n\n---\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(outputPath, []byte(content.String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportAllMarkdown(notes []NoteInfo, outputPath string) error {
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString("# 所有笔记\n\n")
|
||||||
|
|
||||||
|
for _, note := range notes {
|
||||||
|
content.WriteString(fmt.Sprintf("## %s\n\n", note.Title))
|
||||||
|
content.WriteString(note.Content)
|
||||||
|
content.WriteString("\n\n---\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(outputPath, []byte(content.String()), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportFolderZip(folderTitle string, notes []*NoteInfo, outputPath string) error {
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
w := zip.NewWriter(file)
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
rootFolder := es.GetSafeFilename(folderTitle) + "/"
|
||||||
|
_, err = w.Create(rootFolder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
es.addNotesToZip(w, notes, rootFolder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) ExportAllZip(notes []*NoteInfo, outputPath string) error {
|
||||||
|
file, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
w := zip.NewWriter(file)
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
rootFolder := "所有笔记/"
|
||||||
|
_, err = w.Create(rootFolder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
es.addNotesToZip(w, notes, rootFolder)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) addNotesToZip(w *zip.Writer, notes []*NoteInfo, parentPath string) {
|
||||||
|
for _, note := range notes {
|
||||||
|
safeName := es.GetSafeFilename(note.Title)
|
||||||
|
|
||||||
|
if note.IsFolder {
|
||||||
|
folderPath := parentPath + safeName + "/"
|
||||||
|
_, _ = w.Create(folderPath)
|
||||||
|
|
||||||
|
if note.Children != nil {
|
||||||
|
es.addNotesToZip(w, note.Children, folderPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filePath := parentPath + safeName + ".md"
|
||||||
|
fullContent := fmt.Sprintf("# %s\n\n%s", note.Title, note.Content)
|
||||||
|
|
||||||
|
f, err := w.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, _ = f.Write([]byte(fullContent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoteInfo struct {
|
||||||
|
Title string
|
||||||
|
Content string
|
||||||
|
IsFolder bool
|
||||||
|
Children []*NoteInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) GetDefaultExportPath(title, ext string) string {
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
exportDir := filepath.Join(homeDir, "Desktop")
|
||||||
|
safeName := es.GetSafeFilename(title)
|
||||||
|
return filepath.Join(exportDir, fmt.Sprintf("%s%s", safeName, ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es *ExportService) GetSafeFilename(title string) string {
|
||||||
|
safeName := strings.ReplaceAll(title, "/", "-")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "\\", "-")
|
||||||
|
safeName = strings.ReplaceAll(safeName, ":", "-")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "*", "")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "?", "")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "\"", "")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "<", "")
|
||||||
|
safeName = strings.ReplaceAll(safeName, ">", "")
|
||||||
|
safeName = strings.ReplaceAll(safeName, "|", "")
|
||||||
|
|
||||||
|
if strings.TrimSpace(safeName) == "" {
|
||||||
|
safeName = "未命名笔记"
|
||||||
|
}
|
||||||
|
|
||||||
|
return safeName
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MaxFileSize = 6 * 1024 * 1024 // 6MB
|
||||||
|
|
||||||
|
type FileService struct {
|
||||||
|
dataDir string
|
||||||
|
baseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileService(baseURL string) (*FileService, error) {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataDir := filepath.Join(homeDir, ".kenaito-notebook", "assets")
|
||||||
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[FileService] Assets directory: %s", dataDir)
|
||||||
|
log.Printf("[FileService] Base URL: %s", baseURL)
|
||||||
|
|
||||||
|
return &FileService{
|
||||||
|
dataDir: dataDir,
|
||||||
|
baseURL: strings.TrimRight(baseURL, "/"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) GetBaseURL() string {
|
||||||
|
return fs.baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) SaveImage(base64Data string) (string, error) {
|
||||||
|
parts := strings.Split(base64Data, ",")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid base64 format")
|
||||||
|
}
|
||||||
|
|
||||||
|
header := parts[0]
|
||||||
|
ext := ".png"
|
||||||
|
if strings.Contains(header, "jpeg") || strings.Contains(header, "jpg") {
|
||||||
|
ext = ".jpg"
|
||||||
|
} else if strings.Contains(header, "gif") {
|
||||||
|
ext = ".gif"
|
||||||
|
} else if strings.Contains(header, "webp") {
|
||||||
|
ext = ".webp"
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(len(data)) > MaxFileSize {
|
||||||
|
return "", fmt.Errorf("文件大小超过 6MB 限制")
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fmt.Sprintf("%s%s", uuid.New().String()[:8], ext)
|
||||||
|
|
||||||
|
dateDir := time.Now().Format("2006/01")
|
||||||
|
dirPath := filepath.Join(fs.dataDir, dateDir)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(dirPath, filename)
|
||||||
|
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/api/files/assets/%s/%s", fs.baseURL, dateDir, filename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) SaveFile(base64Data, filename string) (string, error) {
|
||||||
|
parts := strings.Split(base64Data, ",")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid base64 format")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := base64.StdEncoding.DecodeString(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(len(data)) > MaxFileSize {
|
||||||
|
return "", fmt.Errorf("文件大小超过 6MB 限制")
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
nameWithoutExt := strings.TrimSuffix(filename, ext)
|
||||||
|
uniqueFilename := fmt.Sprintf("%s_%s%s", nameWithoutExt, uuid.New().String()[:8], ext)
|
||||||
|
|
||||||
|
dateDir := time.Now().Format("2006/01")
|
||||||
|
dirPath := filepath.Join(fs.dataDir, dateDir)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(dirPath, uniqueFilename)
|
||||||
|
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/api/files/assets/%s/%s", fs.baseURL, dateDir, uniqueFilename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FileService) GetAssetsPath() string {
|
||||||
|
return fs.dataDir
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type verificationEntry struct {
|
||||||
|
code string
|
||||||
|
expiresAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerificationService struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
codes map[string]*verificationEntry // key: "email:purpose"
|
||||||
|
times map[string]time.Time // key: "email:purpose" — 上次发送时间
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVerificationService() *VerificationService {
|
||||||
|
v := &VerificationService{
|
||||||
|
codes: make(map[string]*verificationEntry),
|
||||||
|
times: make(map[string]time.Time),
|
||||||
|
}
|
||||||
|
// 定期清理过期验证码
|
||||||
|
go v.cleanup()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCode 生成 6 位随机验证码
|
||||||
|
func (v *VerificationService) GenerateCode(email, purpose string) string {
|
||||||
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||||
|
key := email + ":" + purpose
|
||||||
|
|
||||||
|
v.mu.Lock()
|
||||||
|
v.codes[key] = &verificationEntry{
|
||||||
|
code: code,
|
||||||
|
expiresAt: time.Now().Add(5 * time.Minute),
|
||||||
|
}
|
||||||
|
v.times[key] = time.Now()
|
||||||
|
v.mu.Unlock()
|
||||||
|
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckRateLimit 检查是否在 60 秒限频内
|
||||||
|
func (v *VerificationService) CheckRateLimit(email, purpose string) bool {
|
||||||
|
key := email + ":" + purpose
|
||||||
|
v.mu.RLock()
|
||||||
|
last, exists := v.times[key]
|
||||||
|
v.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists && time.Since(last) < 60*time.Second {
|
||||||
|
return false // 被限频
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCode 验证验证码(不消耗,仅校验)
|
||||||
|
func (v *VerificationService) VerifyCode(email, code, purpose string) error {
|
||||||
|
key := email + ":" + purpose
|
||||||
|
v.mu.RLock()
|
||||||
|
entry, exists := v.codes[key]
|
||||||
|
v.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("验证码不存在或已过期")
|
||||||
|
}
|
||||||
|
if time.Now().After(entry.expiresAt) {
|
||||||
|
return fmt.Errorf("验证码已过期")
|
||||||
|
}
|
||||||
|
if entry.code != code {
|
||||||
|
return fmt.Errorf("验证码错误")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseCode 验证并消耗验证码(验证通过后删除)
|
||||||
|
func (v *VerificationService) UseCode(email, code, purpose string) error {
|
||||||
|
key := email + ":" + purpose
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
|
entry, exists := v.codes[key]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("验证码不存在或已过期")
|
||||||
|
}
|
||||||
|
if time.Now().After(entry.expiresAt) {
|
||||||
|
delete(v.codes, key)
|
||||||
|
return fmt.Errorf("验证码已过期")
|
||||||
|
}
|
||||||
|
if entry.code != code {
|
||||||
|
return fmt.Errorf("验证码错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(v.codes, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup 定期清理过期验证码
|
||||||
|
func (v *VerificationService) cleanup() {
|
||||||
|
ticker := time.NewTicker(10 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
v.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for key, entry := range v.codes {
|
||||||
|
if now.After(entry.expiresAt) {
|
||||||
|
delete(v.codes, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "kenaito-notebook",
|
||||||
|
"outputfilename": "kenaito-notebook",
|
||||||
|
"frontend:install": "npm install",
|
||||||
|
"frontend:build": "npm run build",
|
||||||
|
"frontend:dev:watcher": "npm run dev",
|
||||||
|
"frontend:dev:serverUrl": "http://localhost:5173",
|
||||||
|
"author": {
|
||||||
|
"name": "Odboy",
|
||||||
|
"email": "tianjun@odboy.cn"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user