Base URL: http://localhost:5000/api
{
"code": 0,
"msg": "success",
"data": {}
}| Field | Type | Description |
|---|---|---|
| code | int | 0 = success, non-zero = error |
| msg | string | Status message |
| data | object/array | Response payload |
For paginated list endpoints, the response includes additional pagination fields.
All note/notebook endpoints require Authorization: Bearer <access_token> header.
Access tokens expire after 1 hour. Refresh tokens expire after 7 days.
Create a new account.
POST /api/auth/register
Content-Type: application/json
{ "username": "allen", "password": "123456" }
Response:
{
"code": 0,
"msg": "success",
"data": { "username": "allen" }
}Login and receive tokens.
POST /api/auth/login
{ "username": "allen", "password": "123456" }
Response:
{
"code": 0,
"msg": "success",
"data": {
"token": "a1b2c3...",
"refreshToken": "d4e5f6...",
"expiresIn": 3600,
"user": { "username": "allen", "createdAt": "2026-07-28T10:00:00Z" }
}
}| Field | Description |
|---|---|
| token | Access token (Bearer), expires in 1 hour |
| refreshToken | Used to obtain a new access token, expires in 7 days |
| expiresIn | Access token TTL in seconds |
Get a new access token using the refresh token. This invalidates the old token pair.
POST /api/auth/refresh
{ "refreshToken": "d4e5f6..." }
Response (same structure as login):
{
"code": 0,
"data": {
"token": "new-access...",
"refreshToken": "new-refresh...",
"expiresIn": 3600
}
}Get current user info. Requires auth.
Response:
{
"code": 0,
"data": { "username": "allen", "createdAt": "2026-07-28T10:00:00Z" }
}Invalidate current token pair. Requires auth.
{
"id": "n1",
"title": "Weekly Meeting Notes",
"content": "# Meeting\n\n- item 1\n- item 2",
"notebookId": "nb1",
"color": "blue",
"isPinned": true,
"version": 3,
"createdAt": "2026-07-20T09:00:00Z",
"updatedAt": "2026-07-27T15:30:00Z",
"deletedAt": null
}| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| title | string | Note title (max 100 chars) |
| content | string | Note content in Markdown format |
| notebookId | string/null | Associated notebook ID |
| color | string | blue / green / yellow / orange / red / gray |
| isPinned | boolean | Pinned to top |
| version | int | Monotonic version, incremented on every write. Used for offline merge detection |
| createdAt | string | ISO 8601 creation timestamp |
| updatedAt | string | ISO 8601 last update timestamp. Also used as optimistic lock key |
| deletedAt | string/null | ISO 8601 deletion timestamp |
{
"id": "nb1",
"name": "Work",
"color": "#4A90D9",
"createdAt": "2026-07-01T10:00:00Z"
}List all notebooks for current user.
Create a notebook. Body: { "name": "Travel", "color": "#F5A623" }
Update a notebook.
Delete a notebook. Notes in this notebook get notebookId set to null.
Paginated list of non-deleted notes.
| Query Parameter | Type | Default | Description |
|---|---|---|---|
| page | int | 1 | Page number |
| size | int | 20 | Items per page (max 100) |
| sortBy | string | updatedAt | updatedAt or createdAt |
| keyword | string | — | Search in title and content |
| notebookId | string | — | Filter by notebook |
| color | string | — | Filter by color |
Response:
{
"code": 0,
"msg": "success",
"data": [ /* Note[] */ ],
"pagination": {
"page": 1,
"size": 20,
"total": 45,
"totalPages": 3
}
}Notes are sorted: pinned first, then by sortBy descending.
Incremental sync — returns only notes changed since the given timestamp. Designed for mobile/desktop sync after reconnect.
| Query Parameter | Type | Description |
|---|---|---|
| since | string | ISO 8601 timestamp. Return items with updatedAt > since |
Response:
{
"code": 0,
"data": {
"updated": [ /* Note[] — notes modified since `since` */ ],
"deletedIds": [ "n1", "n2" ],
"serverTime": "2026-07-28T12:00:00Z"
}
}Sync flow:
- Client stores
lastSyncTimelocally - Calls
GET /api/notes/sync?since=<lastSyncTime> - Merges
updatedinto local cache - Removes notes in
deletedIdsfrom local cache - Stores new
serverTimeaslastSyncTime
Get a single note.
Create a new note.
{
"title": "New Note",
"content": "# Hello\n\nThis is **markdown**.",
"notebookId": "nb1",
"color": "yellow",
"isPinned": false
}Update a note. Supports optimistic locking via If-Match header.
PUT /api/notes/n1
Authorization: Bearer xxx
If-Match: 2026-07-27T15:30:00Z
Content-Type: application/json
{ "title": "Updated Title", "content": "New content" }
Conflict detection: If If-Match does not equal the server's current updatedAt, the server returns:
HTTP 409 Conflict
{
"code": 409,
"msg": "Conflict: note was modified by another device. Server version: 2026-07-28T10:00:00Z"
}
The client should:
- Re-fetch the latest note
- Let the user merge changes
- Retry with the new
updatedAt
If If-Match is not provided, the update is always accepted (last-write-wins).
Move a note to trash.
Toggle pin status.
List all deleted notes.
Recover a note from trash.
Permanently delete a note from trash.
图片上传和附件管理,用于富文本编辑器中内嵌图片的存储与跨设备同步。
{
"attachId": "a1b2c3...",
"noteId": "n1",
"type": 0,
"fileName": "photo.jpg",
"fileId": "f_abc123",
"width": 1920,
"height": 1080,
"md5": "d41d8cd9...",
"url": "",
"state": 0,
"createdAt": "2026-08-01T10:00:00Z"
}| 字段 | 类型 | 说明 |
|---|---|---|
| attachId | string | 附件唯一标识 (UUID) |
| noteId | string | 所属笔记 ID |
| type | int | 0 = 图片, 1 = 手写, 2 = 语音 |
| fileName | string | 原始文件名 |
| fileId | string | 服务端文件标识 |
| width | int | 图片宽度 (px) |
| height | int | 图片高度 (px) |
| md5 | string | 文件 MD5,空字符串 = 待同步 |
| url | string | 空 = 仅本地待上传, 非空 = 已同步的服务端 fileId |
| state | int | 0 = NEW, 1 = SYNCED, 2 = MODIFIED |
| createdAt | string | ISO 8601 创建时间 |
上传图片文件到服务器。需要认证。
POST /api/files/upload
Authorization: Bearer xxx
Content-Type: multipart/form-data
file: (binary image data)
noteId: n1 (可选)
Response:
{
"code": 0,
"data": {
"fileId": "f_abc123",
"url": "https://cdn.example.com/f_abc123.webp",
"width": 1920,
"height": 1080,
"md5": "d41d8cd98f00b204e9800998ecf8427e"
}
}| 约束 | 值 |
|---|---|
| 最大文件大小 | 10 MB |
| 支持格式 | JPEG, PNG, GIF, WebP |
| 超时 | 60s |
下载附件图片。返回二进制流,Content-Type 为实际图片类型。
前端可通过
getAttachmentDownloadUrl(attachId)获取完整 URL 直接用于<img src>。
获取笔记的所有附件列表。需要认证。
GET /api/notes/n1/attachments
Response:
{
"code": 0,
"data": [
{
"attachId": "a1b2c3...",
"noteId": "n1",
"type": 0,
"fileName": "photo.jpg",
"fileId": "f_abc123",
"width": 1920,
"height": 1080,
"md5": "d41d8cd9...",
"state": 1,
"createdAt": "2026-08-01T10:00:00Z"
}
]
}更新附件元数据(同步后写回 md5/fileId)。需要认证。
PUT /api/attachments/a1b2c3...
Content-Type: application/json
{ "md5": "d41d8cd9...", "state": 1 }
删除笔记中的附件(同时删除服务器文件)。需要认证。
DELETE /api/attachments/a1b2c3...
for each Attachment where md5 == "":
if url == "" → POST /api/files/upload (上传)
if url != "" → GET /attachments/:id/download (下载)
→ PUT /attachments/:id (写回 md5)
文字颜色不通过独立 API 传输,而是内联在笔记 content 中。使用 CSS 变量名存储,支持浅/深色模式自动切换。
| CSS 变量 | 浅色模式 | 深色模式 |
|---|---|---|
--blueColor |
#1A73E8 |
#8AB4F8 |
--redColor |
#EA4335 |
#F28B82 |
--greenColor |
#34A853 |
#81C995 |
--orangeColor |
#FB9600 |
#FDD663 |
--yellowColor |
#F9AB00 |
#FDE293 |
--grayColor |
#5F6368 |
#BDC1C6 |
这是<span style="color: var(--blueColor)">蓝色文字</span>,后面正常。// api/index.js
uploadFile(file, noteId) → POST /files/upload
getAttachmentDownloadUrl(attachId) → 返回下载 URL 字符串
getNoteAttachments(noteId) → GET /notes/:id/attachments
updateAttachment(attachId, data) → PUT /attachments/:id
deleteAttachment(attachId) → DELETE /attachments/:id文字颜色不需要额外 API — 内容本身已携带颜色信息,随
PUT /api/notes/:id的 content 字段一同持久化。
{ "status": "ok" }跨设备同步的用户分享设置。
获取当前用户的分享页脚自定义文本。需要认证。
Response:
{
"code": 0,
"data": {
"logoText": "分享来自 Open Note",
"watermark": "备忘录"
}
}| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| logoText | string | "分享来自 Open Note" |
分享图片页脚第一行署名 |
| watermark | string | "备忘录" |
分享图片页脚第二行水印 |
更新分享设置。仅需发送要变更的字段,未提供的保留原值。
PUT /api/settings/share
Authorization: Bearer xxx
Content-Type: application/json
{ "logoText": "来自 allen 的分享" }
Response:
{
"code": 0,
"data": {
"logoText": "来自 allen 的分享",
"watermark": "备忘录"
}
}src/api/index.js
getShareSettingsAPI() → GET /settings/share
updateShareSettingsAPI(data) → PUT /settings/share
src/config/shareSettings.js
getShareSettings() → 调用 API,失败回退 localStorage
getShareSettingsCached() → 同步读取 localStorage 缓存(即时展示用)
saveShareSettings(partial) → 先写 localStorage,再调 API 持久化
读取: API 成功 → 写入 localStorage 缓存 → 返回
API 失败 → 从 localStorage 读取 → 返回缓存值
写入: localStorage 即时更新(UI 即时响应)
API 调用后台静默同步
API 失败 → 本地缓存生效,下次成功时自动覆盖
修改设置后需重新点击分享按钮以重新生成图片,页脚文字才会更新。
| Feature | Implementation |
|---|---|
| Pagination | ?page=&size= — mobile uses small page sizes, desktop uses larger |
| Incremental Sync | GET /api/notes/sync?since= — mobile fetches only changes after reconnect |
| Token Refresh | POST /api/auth/refresh — clients auto-refresh before token expiry |
| Optimistic Lock | If-Match: <updatedAt> header — 409 Conflict on concurrent edits |
| Versioning | version field on every note — clients track local version for offline merge |
| Auto-refresh | Axios interceptor detects 401 → calls /auth/refresh → retries |
# Mock server
cd mock-server
pip install flask flask-cors
python server.py # → http://localhost:5000
# Frontend
cd note-frontend
npm install
npm run dev # → http://localhost:3000