diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 37012f6..b30aa13 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,13 +13,18 @@
"@lucide/vue": "^1.17.0",
"@primeuix/themes": "^2.0.3",
"@tailwindcss/vite": "^4.3.0",
+ "chokidar": "^5.0.0",
"electron": "^42.3.0",
"electron-vite": "^5.0.0",
+ "gray-matter": "^4.0.3",
+ "nanoid": "^5.1.11",
+ "primeicons": "^7.0.0",
"primevue": "^4.5.5",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
+ "sql.js": "^1.14.1",
"tailwindcss": "^4.3.0",
"unified": "^11.0.5",
"vue": "^3.5.35"
@@ -2828,6 +2833,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -4162,6 +4182,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@@ -4181,6 +4214,18 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/extract-zip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -4577,6 +4622,49 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/gray-matter/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/gray-matter/node_modules/js-yaml": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/gray-matter/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4843,6 +4931,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -5003,6 +5100,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/lazy-val": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
@@ -6241,9 +6347,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
+ "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"funding": [
{
"type": "github",
@@ -6252,10 +6358,10 @@
],
"license": "MIT",
"bin": {
- "nanoid": "bin/nanoid.cjs"
+ "nanoid": "bin/nanoid.js"
},
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": "^18 || >=20"
}
},
"node_modules/node-abi": {
@@ -6553,6 +6659,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/postcss/node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/postject": {
"version": "1.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
@@ -6583,6 +6707,12 @@
"node": "^12.20.0 || >=14"
}
},
+ "node_modules/primeicons": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
+ "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==",
+ "license": "MIT"
+ },
"node_modules/primevue": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
@@ -6700,6 +6830,19 @@
"read-binary-file-arch": "cli.js"
}
},
+ "node_modules/readdirp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/rehype-stringify": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
@@ -6944,6 +7087,19 @@
"node": ">=11.0.0"
}
},
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/semver": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
@@ -7100,6 +7256,12 @@
"license": "BSD-3-Clause",
"optional": true
},
+ "node_modules/sql.js": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.1.tgz",
+ "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==",
+ "license": "MIT"
+ },
"node_modules/stat-mode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
@@ -7152,6 +7314,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/sumchecker": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
diff --git a/package.json b/package.json
index f478b03..cbcf7c0 100644
--- a/package.json
+++ b/package.json
@@ -21,13 +21,18 @@
"@lucide/vue": "^1.17.0",
"@primeuix/themes": "^2.0.3",
"@tailwindcss/vite": "^4.3.0",
+ "chokidar": "^5.0.0",
"electron": "^42.3.0",
"electron-vite": "^5.0.0",
+ "gray-matter": "^4.0.3",
+ "nanoid": "^5.1.11",
+ "primeicons": "^7.0.0",
"primevue": "^4.5.5",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
+ "sql.js": "^1.14.1",
"tailwindcss": "^4.3.0",
"unified": "^11.0.5",
"vue": "^3.5.35"
diff --git a/src/main/index.js b/src/main/index.js
index 5a7d611..6e6c908 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -1,8 +1,10 @@
import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron'
import { join } from 'path'
import { is } from '@electron-toolkit/utils'
+import { StorageService } from './services/StorageService'
let mainWindow = null
+let storageService = null
// 标题栏颜色配置
const titleBarColors = {
@@ -59,9 +61,189 @@ ipcMain.handle('theme:set', (_, theme) => {
})
})
-app.whenReady().then(createWindow)
+// ==================== Register IPC Handlers ====================
+
+function registerIpcHandlers() {
+ // --- Notes ---
+ ipcMain.handle('notes:create', async (_, data) => {
+ try {
+ return await storageService.createNote(data)
+ } catch (err) {
+ console.error('Failed to create note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:get', async (_, id) => {
+ try {
+ return await storageService.getNote(id)
+ } catch (err) {
+ console.error('Failed to get note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:update', async (_, id, data) => {
+ try {
+ return await storageService.updateNote(id, data)
+ } catch (err) {
+ console.error('Failed to update note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:delete', async (_, id, permanent) => {
+ try {
+ return await storageService.deleteNote(id, permanent)
+ } catch (err) {
+ console.error('Failed to delete note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:restore', async (_, id) => {
+ try {
+ return await storageService.restoreNote(id)
+ } catch (err) {
+ console.error('Failed to restore note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:list', async (_, filter) => {
+ try {
+ return await storageService.listNotes(filter)
+ } catch (err) {
+ console.error('Failed to list notes:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notes:search', async (_, query) => {
+ try {
+ return await storageService.searchNotes(query)
+ } catch (err) {
+ console.error('Failed to search notes:', err)
+ throw err
+ }
+ })
+
+ // --- Notebooks ---
+ ipcMain.handle('notebooks:create', async (_, data) => {
+ try {
+ return await storageService.createNotebook(data)
+ } catch (err) {
+ console.error('Failed to create notebook:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notebooks:list', async () => {
+ try {
+ return await storageService.listNotebooks()
+ } catch (err) {
+ console.error('Failed to list notebooks:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notebooks:rename', async (_, id, name) => {
+ try {
+ return await storageService.renameNotebook(id, name)
+ } catch (err) {
+ console.error('Failed to rename notebook:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('notebooks:delete', async (_, id) => {
+ try {
+ return await storageService.deleteNotebook(id)
+ } catch (err) {
+ console.error('Failed to delete notebook:', err)
+ throw err
+ }
+ })
+
+ // --- Tags ---
+ ipcMain.handle('tags:create', async (_, name) => {
+ try {
+ return await storageService.createTag(name)
+ } catch (err) {
+ console.error('Failed to create tag:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('tags:list', async () => {
+ try {
+ return await storageService.listTags()
+ } catch (err) {
+ console.error('Failed to list tags:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('tags:delete', async (_, id) => {
+ try {
+ return await storageService.deleteTag(id)
+ } catch (err) {
+ console.error('Failed to delete tag:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('tags:addToNote', async (_, noteId, tagName) => {
+ try {
+ return await storageService.addTagToNote(noteId, tagName)
+ } catch (err) {
+ console.error('Failed to add tag to note:', err)
+ throw err
+ }
+ })
+
+ ipcMain.handle('tags:removeFromNote', async (_, noteId, tagName) => {
+ try {
+ return await storageService.removeTagFromNote(noteId, tagName)
+ } catch (err) {
+ console.error('Failed to remove tag from note:', err)
+ throw err
+ }
+ })
+
+ // --- Stats ---
+ ipcMain.handle('stats:get', async () => {
+ try {
+ return await storageService.getStats()
+ } catch (err) {
+ console.error('Failed to get stats:', err)
+ throw err
+ }
+ })
+}
+
+// ==================== App Lifecycle ====================
+
+app.whenReady().then(async () => {
+ // Initialize storage with user data directory
+ const workspacePath = join(app.getPath('documents'), 'Tunji')
+ storageService = new StorageService()
+ await storageService.init(workspacePath)
+
+ // Register IPC handlers
+ registerIpcHandlers()
+
+ // Create window
+ createWindow()
+})
app.on('window-all-closed', () => {
+ // Save all pending changes before quitting
+ if (storageService) {
+ storageService.flush()
+ storageService.close()
+ }
+
if (process.platform !== 'darwin') {
app.quit()
}
diff --git a/src/main/indexpreload.js b/src/main/indexpreload.js
index dfbcaa3..18403ba 100644
--- a/src/main/indexpreload.js
+++ b/src/main/indexpreload.js
@@ -3,5 +3,38 @@ import { contextBridge, ipcRenderer } from 'electron'
// 安全地暴露 API 给渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
platform: process.platform,
- setTheme: (theme) => ipcRenderer.invoke('theme:set', theme)
+
+ // Theme
+ setTheme: (theme) => ipcRenderer.invoke('theme:set', theme),
+
+ // Notes
+ notes: {
+ create: (data) => ipcRenderer.invoke('notes:create', data),
+ get: (id) => ipcRenderer.invoke('notes:get', id),
+ update: (id, data) => ipcRenderer.invoke('notes:update', id, data),
+ delete: (id, permanent) => ipcRenderer.invoke('notes:delete', id, permanent),
+ restore: (id) => ipcRenderer.invoke('notes:restore', id),
+ list: (filter) => ipcRenderer.invoke('notes:list', filter),
+ search: (query) => ipcRenderer.invoke('notes:search', query),
+ },
+
+ // Notebooks
+ notebooks: {
+ create: (data) => ipcRenderer.invoke('notebooks:create', data),
+ list: () => ipcRenderer.invoke('notebooks:list'),
+ rename: (id, name) => ipcRenderer.invoke('notebooks:rename', id, name),
+ delete: (id) => ipcRenderer.invoke('notebooks:delete', id),
+ },
+
+ // Tags
+ tags: {
+ create: (name) => ipcRenderer.invoke('tags:create', name),
+ list: () => ipcRenderer.invoke('tags:list'),
+ delete: (id) => ipcRenderer.invoke('tags:delete', id),
+ addToNote: (noteId, tagName) => ipcRenderer.invoke('tags:addToNote', noteId, tagName),
+ removeFromNote: (noteId, tagName) => ipcRenderer.invoke('tags:removeFromNote', noteId, tagName),
+ },
+
+ // Stats
+ getStats: () => ipcRenderer.invoke('stats:get'),
})
diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue
index 7307b4d..67bae42 100644
--- a/src/renderer/src/App.vue
+++ b/src/renderer/src/App.vue
@@ -1,80 +1,127 @@
@@ -98,6 +158,8 @@ function getOverviewTitle() {
updateNote(currentNote.id, { title, content })"
/>
-
diff --git a/src/renderer/src/assets/styles/main.css b/src/renderer/src/assets/styles/main.css
index 418c1d7..18621ea 100644
--- a/src/renderer/src/assets/styles/main.css
+++ b/src/renderer/src/assets/styles/main.css
@@ -140,42 +140,6 @@
--color-app-ring: var(--theme-ring);
}
-/* ----- 统一按钮样式 ----- */
-
-/* PrimeVue Button 统一 12px 圆角 */
-.p-button {
- border-radius: 12px !important;
-}
-
-/* 按钮统一交互过渡 */
-.btn-icon {
- transition: background-color 0.15s ease, color 0.15s ease;
-}
-
-/* 激活态按钮 */
-.btn-active {
- background: var(--theme-bg-elevated) !important;
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !important;
- color: var(--theme-primary-600) !important;
-}
-
-/* 实心主色按钮 */
-.btn-filled:not(.p-button-text) {
- background: #00bf66 !important;
- border-color: #00bf66 !important;
- color: #ffffff !important;
-}
-
-.btn-filled:not(.p-button-text):hover {
- background: #00a555 !important;
- border-color: #00a555 !important;
-}
-
-.btn-filled:not(.p-button-text):active {
- background: #00914b !important;
- border-color: #00914b !important;
-}
-
/* ----- 全局基础样式 ----- */
/* 主题切换平滑过渡 */
@@ -207,3 +171,8 @@ html {
background-color: rgba(129, 140, 248, 0.3);
color: #e2e8f0;
}
+
+/* Tooltip 样式 */
+.p-tooltip-text {
+ font-size: 0.75rem !important;
+}
diff --git a/src/renderer/src/components/EditorPanel.vue b/src/renderer/src/components/EditorPanel.vue
index 95b3cf4..fe448d8 100644
--- a/src/renderer/src/components/EditorPanel.vue
+++ b/src/renderer/src/components/EditorPanel.vue
@@ -20,10 +20,12 @@ const props = defineProps({
note: { type: Object, default: null }
})
-const emit = defineEmits(['back'])
+const emit = defineEmits(['back', 'save'])
const noteTitle = ref('')
const noteContent = ref('')
+const isSaving = ref(false)
+const lastSavedAt = ref(null)
// View mode: edit | preview | split
const viewMode = ref('edit')
@@ -42,14 +44,49 @@ const headings = ref([])
// Rendered HTML for preview
const renderedHtml = ref('')
+// Auto-save timer
+let saveTimer = null
+const SAVE_DELAY = 1000 // 1 second debounce
+
// Update local state when note prop changes
watch(() => props.note, (newNote) => {
if (newNote) {
noteTitle.value = newNote.title || ''
- noteContent.value = newNote.content || getDefaultContent(newNote.title)
+ noteContent.value = newNote.content || ''
}
}, { immediate: true })
+// Auto-save when content changes
+function scheduleSave() {
+ if (saveTimer) clearTimeout(saveTimer)
+ saveTimer = setTimeout(async () => {
+ await doSave()
+ }, SAVE_DELAY)
+}
+
+async function doSave() {
+ if (!props.note) return
+ isSaving.value = true
+ try {
+ emit('save', {
+ title: noteTitle.value,
+ content: noteContent.value
+ })
+ lastSavedAt.value = new Date()
+ } finally {
+ isSaving.value = false
+ }
+}
+
+// Watch for content changes and trigger auto-save
+watch(noteContent, () => {
+ scheduleSave()
+})
+
+watch(noteTitle, () => {
+ scheduleSave()
+})
+
// Parse headings from markdown content
watch(noteContent, (content) => {
const result = []
@@ -83,14 +120,6 @@ watch(noteContent, async (content) => {
}
}, { immediate: true })
-function getDefaultContent(title) {
- return `# ${title}
-
-在这里开始写作...
-
-`
-}
-
function formatDate(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
@@ -98,6 +127,9 @@ function formatDate(dateStr) {
}
function goBack() {
+ // Save before going back
+ if (saveTimer) clearTimeout(saveTimer)
+ doSave()
emit('back')
}
@@ -125,6 +157,17 @@ const toolbarItems3 = [
{ icon: Link, title: '链接' },
{ icon: Image, title: '图片' },
]
+
+// Tag names for display
+const tagNames = computed(() => {
+ if (!props.note?.tags) return []
+ return props.note.tags.map(t => typeof t === 'string' ? t : t.name)
+})
+
+// Notebook name
+const notebookName = computed(() => {
+ return props.note?.notebook_name || props.note?.notebook || ''
+})
@@ -143,9 +186,11 @@ const toolbarItems3 = [
placeholder="输入标题..."
/>
- 最后编辑: {{ formatDate(note.date) }}
- |
- {{ note.notebook }}
+ 最后编辑: {{ formatDate(note.updated_at) }}
+ |
+ {{ notebookName }}
+ 保存中...
+ 已保存
diff --git a/src/renderer/src/components/NotesOverview.vue b/src/renderer/src/components/NotesOverview.vue
index 916e9af..815e80c 100644
--- a/src/renderer/src/components/NotesOverview.vue
+++ b/src/renderer/src/components/NotesOverview.vue
@@ -1,9 +1,6 @@
@@ -56,34 +76,18 @@ function getTagSeverity(tag) {
{{ title }}
-
-
-
-
-
-
-
-
-
-
- {{ filteredNotes.length }} 篇笔记
-
@@ -92,31 +96,31 @@ function getTagSeverity(tag) {
- {{ note.title }}
+ {{ note.title || '无标题' }}
- {{ note.preview }}
+ {{ getPreview(note) }}
@@ -125,11 +129,11 @@ function getTagSeverity(tag) {
- {{ formatDate(note.date) }}
+ {{ formatDate(note.updated_at) }}
- {{ note.notebook }}
+ {{ getNotebookName(note) }}
@@ -138,26 +142,26 @@ function getTagSeverity(tag) {
- {{ note.title }}
+ {{ note.title || '无标题' }}
-
{{ note.preview }}
+
{{ getPreview(note) }}
@@ -165,18 +169,18 @@ function getTagSeverity(tag) {
- {{ note.notebook }}
+ {{ getNotebookName(note) }}
- {{ formatDate(note.date) }}
+ {{ formatDate(note.updated_at) }}
-
+
diff --git a/src/renderer/src/components/SideBar.vue b/src/renderer/src/components/SideBar.vue
index ad13fef..10f317c 100644
--- a/src/renderer/src/components/SideBar.vue
+++ b/src/renderer/src/components/SideBar.vue
@@ -6,15 +6,23 @@ import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'
import ScrollPanel from 'primevue/scrollpanel'
import {
- Search, Plus, File, Star,
- Trash2, Settings, Sun, Moon, Monitor
+ Search, Plus, File, Star, Trash2,
+ Settings, Sun, Moon, Monitor,
+ BookOpen, Tag, ChevronDown, ChevronRight, Hash
} from '@lucide/vue'
import { useTheme } from '../composables/useTheme'
+import { useNotes } from '../composables/useNotes'
import ButtonIcon from "./ButtonIcon.vue";
const { theme, toggleTheme } = useTheme()
+const { searchNotes } = useNotes()
-const emit = defineEmits(['navigate', 'create-note', 'select-notebook', 'select-tag'])
+const props = defineProps({
+ notebooks: { type: Array, default: () => [] },
+ tags: { type: Array, default: () => [] }
+})
+
+const emit = defineEmits(['navigate', 'create-note', 'select-notebook', 'select-tag', 'select-note'])
// Collapse state
const collapsed = ref(false)
@@ -26,27 +34,33 @@ function toggleCollapsed() {
// Navigation state
const activeNav = ref('all')
+// Section expand state
+const expandedSections = ref({
+ notes: true,
+ notebooks: true,
+ tags: true
+})
+
+function toggleSection(section) {
+ expandedSections.value[section] = !expandedSections.value[section]
+}
+
// Search
const searchQuery = ref('')
+const searchResults = ref([])
+let searchTimer = null
-// Mock data - notes for search
-const allNotes = ref([
- { id: 1, title: '项目需求文档', notebook: '工作' },
- { id: 2, title: '读书笔记 - 深入理解计算机系统', notebook: '学习' },
- { id: 3, title: '周报 - 第22周', notebook: '工作' },
- { id: 4, title: 'Vue 3 学习笔记', notebook: '学习' },
- { id: 5, title: '旅行计划 - 日本', notebook: '生活' },
- { id: 6, title: 'Markdown 语法速查表', notebook: '技术' },
- { id: 7, title: '会议纪要 - 产品评审', notebook: '工作' },
- { id: 8, title: '健身计划', notebook: '生活' },
-])
-
-// Filtered notes for search dropdown
-const filteredNotes = computed(() => {
- if (!searchQuery.value) return []
- const q = searchQuery.value.toLowerCase()
- return allNotes.value.filter(n => n.title.toLowerCase().includes(q))
-})
+async function onSearchInput() {
+ if (searchTimer) clearTimeout(searchTimer)
+ const q = searchQuery.value
+ if (!q || q.length < 1) {
+ searchResults.value = []
+ return
+ }
+ searchTimer = setTimeout(async () => {
+ searchResults.value = await searchNotes(q)
+ }, 200)
+}
// Navigation items
const navSections = [
@@ -55,6 +69,7 @@ const navSections = [
items: [
{ id: 'all', icon: File, label: '所有笔记' },
{ id: 'favorites', icon: Star, label: '收藏夹' },
+ { id: 'trash', icon: Trash2, label: '回收站' },
]
}
]
@@ -65,6 +80,16 @@ function handleNav(id) {
emit('navigate', id)
}
+function selectNotebook(notebook) {
+ activeNav.value = `notebook-${notebook.id}`
+ emit('select-notebook', notebook)
+}
+
+function selectTag(tag) {
+ activeNav.value = `tag-${tag.id}`
+ emit('select-tag', tag)
+}
+
function createNote() {
emit('create-note')
}
@@ -72,6 +97,14 @@ function createNote() {
function selectSearchNote(note) {
emit('select-note', note)
searchQuery.value = ''
+ searchResults.value = []
+}
+
+// Get notebook display name for a note
+function getNotebookName(note) {
+ if (note.notebook_name) return note.notebook_name
+ const nb = props.notebooks.find(n => n.id === note.notebook_id)
+ return nb ? nb.name : ''
}
@@ -84,23 +117,23 @@ function selectSearchNote(note) {
-
-
-
+
+
{{ note.title }}
-
{{ note.notebook }}
+
{{ getNotebookName(note) }}
-
-
+
@@ -142,77 +174,206 @@ function selectSearchNote(note) {
>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- 笔记
-
-
- {{ item.label }}
+ 笔记
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+ 标签
+
+
+
+
+
+
+
+
{{ tag.name }}
+
{{ tag.note_count || 0 }}
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
+
-
-
-
-
-
-
+
-
+
-
+
+
+
- 回收站
+ {{ theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '系统' }}
-
+
-
-
-
-
-
-
-
-
+
+ 设置
+
+
diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js
index 6ae7b4e..c99f59e 100644
--- a/src/renderer/src/main.js
+++ b/src/renderer/src/main.js
@@ -4,7 +4,7 @@ import Aura from '@primeuix/themes/aura'
import Tooltip from 'primevue/tooltip'
import App from './App.vue'
import './assets/styles/main.css'
-
+import 'primeicons/primeicons.css'
const app = createApp(App)
app.use(PrimeVue, {