feat: 功能迭代
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
181
package-lock.json
generated
181
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
|
||||
@@ -1,80 +1,127 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import SideBar from './components/SideBar.vue'
|
||||
import NotesOverview from './components/NotesOverview.vue'
|
||||
import EditorPanel from './components/EditorPanel.vue'
|
||||
import { useTheme } from './composables/useTheme'
|
||||
import { useNotes } from './composables/useNotes'
|
||||
import { useNotebooks } from './composables/useNotebooks'
|
||||
import { useTags } from './composables/useTags'
|
||||
|
||||
// 初始化主题系统
|
||||
const { theme, resolvedTheme, isDark, toggleTheme } = useTheme()
|
||||
|
||||
// 数据层
|
||||
const {
|
||||
notes: allNotes,
|
||||
currentNote,
|
||||
loading: notesLoading,
|
||||
loadNotes,
|
||||
createNote,
|
||||
getNote,
|
||||
updateNote,
|
||||
deleteNote,
|
||||
searchNotes,
|
||||
setCurrentNote,
|
||||
clearCurrentNote
|
||||
} = useNotes()
|
||||
|
||||
const {
|
||||
notebooks,
|
||||
loadNotebooks,
|
||||
createNotebook
|
||||
} = useNotebooks()
|
||||
|
||||
const {
|
||||
tags,
|
||||
loadTags
|
||||
} = useTags()
|
||||
|
||||
const currentNav = ref('all')
|
||||
const currentNote = ref(null)
|
||||
const filterType = ref('all')
|
||||
const filterValue = ref(null)
|
||||
|
||||
// Mock data shared between overview and sidebar
|
||||
const allNotes = ref([
|
||||
{ id: 1, title: '项目需求文档', preview: '本文档描述了Tunji编辑器的核心功能需求和设计方案...', date: '2026-05-31', notebook: '工作', tags: ['重要', '参考'] },
|
||||
{ id: 2, title: '读书笔记 - 深入理解计算机系统', preview: '第一章:计算机系统是由硬件和系统软件组成的...', date: '2026-05-30', notebook: '学习', tags: ['参考'] },
|
||||
{ id: 3, title: '周报 - 第22周', preview: '本周完成了编辑器基础布局搭建,实现了三栏式布局...', date: '2026-05-29', notebook: '工作', tags: ['待办'] },
|
||||
{ id: 4, title: 'Vue 3 学习笔记', preview: 'Composition API 提供了更灵活的代码组织方式,setup函数...', date: '2026-05-28', notebook: '学习', tags: ['参考'] },
|
||||
{ id: 5, title: '旅行计划 - 日本', preview: '东京 -> 京都 -> 大阪,预计行程7天,预算约15000元...', date: '2026-05-27', notebook: '生活', tags: ['灵感'] },
|
||||
{ id: 6, title: 'Markdown 语法速查表', preview: '标题用#号,加粗用**,斜体用*,代码用反引号...', date: '2026-05-26', notebook: '技术', tags: ['参考'] },
|
||||
{ id: 7, title: '会议纪要 - 产品评审', preview: '参会人员:产品、设计、开发。讨论了v1.0的优先级...', date: '2026-05-25', notebook: '工作', tags: ['会议', '重要'] },
|
||||
{ id: 8, title: '健身计划', preview: '周一:胸+三头,周二:背+二头,周三:休息,周四:腿...', date: '2026-05-24', notebook: '生活', tags: ['待办'] },
|
||||
])
|
||||
|
||||
// 过滤后的笔记列表
|
||||
const overviewNotes = ref([])
|
||||
|
||||
function computeOverviewNotes() {
|
||||
let result = allNotes.value
|
||||
if (filterType.value === 'notebook' && filterValue.value) {
|
||||
result = result.filter(n => n.notebook === filterValue.value.name)
|
||||
result = result.filter(n => n.notebook_id === filterValue.value.id)
|
||||
} else if (filterType.value === 'tag' && filterValue.value) {
|
||||
result = result.filter(n => n.tags.includes(filterValue.value.name))
|
||||
result = result.filter(n => {
|
||||
const noteTags = n.tags || []
|
||||
return noteTags.some(t => t.name === filterValue.value.name)
|
||||
})
|
||||
} else if (filterType.value === 'search' && filterValue.value) {
|
||||
result = result.filter(n =>
|
||||
n.title.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
)
|
||||
}
|
||||
overviewNotes.value = result
|
||||
}
|
||||
|
||||
function onNavigate(id) {
|
||||
async function onNavigate(id) {
|
||||
currentNav.value = id
|
||||
filterType.value = 'all'
|
||||
filterValue.value = null
|
||||
currentNote.value = null
|
||||
clearCurrentNote()
|
||||
|
||||
if (id === 'trash') {
|
||||
await loadNotes({ isTrashed: 1 })
|
||||
} else {
|
||||
await loadNotes({ isTrashed: 0 })
|
||||
}
|
||||
computeOverviewNotes()
|
||||
}
|
||||
|
||||
function onSelectNote(note) {
|
||||
currentNote.value = note
|
||||
getNote(note.id)
|
||||
}
|
||||
|
||||
function onBackToOverview() {
|
||||
currentNote.value = null
|
||||
clearCurrentNote()
|
||||
// Reload notes to reflect any changes
|
||||
loadNotes({ isTrashed: filterType.value === 'trash' ? 1 : 0 }).then(() => {
|
||||
computeOverviewNotes()
|
||||
})
|
||||
}
|
||||
|
||||
function onCreateNote() {
|
||||
// TODO: Implement note creation
|
||||
console.log('Creating new note...')
|
||||
async function onCreateNote() {
|
||||
const notebookId = filterType.value === 'notebook' && filterValue.value
|
||||
? filterValue.value.id
|
||||
: 'default'
|
||||
|
||||
const note = await createNote({
|
||||
title: '无标题',
|
||||
content: '',
|
||||
notebookId
|
||||
})
|
||||
|
||||
if (note) {
|
||||
// Auto select the new note
|
||||
getNote(note.id)
|
||||
}
|
||||
}
|
||||
|
||||
function onSelectNotebook(notebook) {
|
||||
filterType.value = 'notebook'
|
||||
filterValue.value = notebook
|
||||
currentNote.value = null
|
||||
computeOverviewNotes()
|
||||
clearCurrentNote()
|
||||
loadNotes({ notebookId: notebook.id, isTrashed: 0 }).then(() => {
|
||||
computeOverviewNotes()
|
||||
})
|
||||
}
|
||||
|
||||
function onSelectTag(tag) {
|
||||
filterType.value = 'tag'
|
||||
filterValue.value = tag
|
||||
currentNote.value = null
|
||||
computeOverviewNotes()
|
||||
clearCurrentNote()
|
||||
loadNotes({ tagId: tag.id, isTrashed: 0 }).then(() => {
|
||||
computeOverviewNotes()
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize
|
||||
computeOverviewNotes()
|
||||
|
||||
function getOverviewTitle() {
|
||||
if (filterType.value === 'notebook' && filterValue.value) {
|
||||
return filterValue.value.name
|
||||
@@ -82,6 +129,9 @@ function getOverviewTitle() {
|
||||
if (filterType.value === 'tag' && filterValue.value) {
|
||||
return `标签: ${filterValue.value.name}`
|
||||
}
|
||||
if (filterType.value === 'search' && filterValue.value) {
|
||||
return `搜索: ${filterValue.value}`
|
||||
}
|
||||
const titleMap = {
|
||||
all: '所有笔记',
|
||||
recent: '最近编辑',
|
||||
@@ -90,6 +140,16 @@ function getOverviewTitle() {
|
||||
}
|
||||
return titleMap[currentNav.value] || '所有笔记'
|
||||
}
|
||||
|
||||
// Initialize: load data on mount
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadNotes({ isTrashed: 0 }),
|
||||
loadNotebooks(),
|
||||
loadTags()
|
||||
])
|
||||
computeOverviewNotes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -98,6 +158,8 @@ function getOverviewTitle() {
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Sidebar -->
|
||||
<SideBar
|
||||
:notebooks="notebooks"
|
||||
:tags="tags"
|
||||
@navigate="onNavigate"
|
||||
@create-note="onCreateNote"
|
||||
@select-notebook="onSelectNotebook"
|
||||
@@ -120,9 +182,9 @@ function getOverviewTitle() {
|
||||
v-else
|
||||
:note="currentNote"
|
||||
@back="onBackToOverview"
|
||||
@save="({ title, content }) => updateNote(currentNote.id, { title, content })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -143,9 +186,11 @@ const toolbarItems3 = [
|
||||
placeholder="输入标题..."
|
||||
/>
|
||||
<div class="flex items-center gap-3 mt-1 text-xs text-surface-400">
|
||||
<span v-if="note?.date">最后编辑: {{ formatDate(note.date) }}</span>
|
||||
<span v-if="note?.date && note?.notebook">|</span>
|
||||
<span v-if="note?.notebook">{{ note.notebook }}</span>
|
||||
<span v-if="note?.updated_at">最后编辑: {{ formatDate(note.updated_at) }}</span>
|
||||
<span v-if="note?.updated_at && notebookName">|</span>
|
||||
<span v-if="notebookName">{{ notebookName }}</span>
|
||||
<span v-if="isSaving" class="text-primary-500">保存中...</span>
|
||||
<span v-else-if="lastSavedAt" class="text-surface-300">已保存</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import Tag from 'primevue/tag'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { Search, LayoutGrid, List, FileText, Clock, Calendar, Folder } from '@lucide/vue'
|
||||
@@ -15,29 +12,21 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['select-note'])
|
||||
|
||||
const searchQuery = ref('')
|
||||
const viewMode = ref('card') // 'card' | 'list'
|
||||
|
||||
const filteredNotes = computed(() => {
|
||||
if (!searchQuery.value) return props.notes
|
||||
const q = searchQuery.value.toLowerCase()
|
||||
return props.notes.filter(
|
||||
n => n.title.toLowerCase().includes(q) || n.preview.toLowerCase().includes(q)
|
||||
)
|
||||
})
|
||||
|
||||
function selectNote(note) {
|
||||
emit('select-note', note)
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
const month = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
return `${month}月${day}日`
|
||||
}
|
||||
|
||||
function getTagSeverity(tag) {
|
||||
function getTagSeverity(tagName) {
|
||||
const severityMap = {
|
||||
'重要': 'danger',
|
||||
'待办': 'warning',
|
||||
@@ -45,7 +34,38 @@ function getTagSeverity(tag) {
|
||||
'灵感': 'success',
|
||||
'会议': 'secondary'
|
||||
}
|
||||
return severityMap[tag] || 'secondary'
|
||||
return severityMap[tagName] || 'secondary'
|
||||
}
|
||||
|
||||
// Get tag names from note's tags array
|
||||
function getTagNames(note) {
|
||||
if (!note.tags) return []
|
||||
return note.tags.map(t => typeof t === 'string' ? t : t.name)
|
||||
}
|
||||
|
||||
// Get notebook display name
|
||||
function getNotebookName(note) {
|
||||
return note.notebook_name || note.notebook || ''
|
||||
}
|
||||
|
||||
// Get preview text from note (truncated content or word count)
|
||||
function getPreview(note) {
|
||||
if (note.content) {
|
||||
// Strip markdown syntax for preview
|
||||
const text = note.content
|
||||
.replace(/#{1,6}\s+/g, '')
|
||||
.replace(/\*\*(.+?)\*\*/g, '$1')
|
||||
.replace(/\*(.+?)\*/g, '$1')
|
||||
.replace(/`(.+?)`/g, '$1')
|
||||
.replace(/\[(.+?)\]\(.+?\)/g, '$1')
|
||||
.replace(/\n+/g, ' ')
|
||||
.trim()
|
||||
return text.substring(0, 150) + (text.length > 150 ? '...' : '')
|
||||
}
|
||||
if (note.word_count) {
|
||||
return `${note.word_count} 字`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -56,34 +76,18 @@ function getTagSeverity(tag) {
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-bold text-surface-900">{{ title }}</h1>
|
||||
<div class="flex items-center gap-1 bg-surface-100 rounded-lg p-0.5">
|
||||
<Button text :severity="viewMode === 'card' ? 'primary' : 'secondary'" size="small" @click="viewMode = 'card'" v-tooltip.top="'卡片视图'" :class="['btn-icon w-8 h-8', viewMode === 'card' && 'btn-active']">
|
||||
<Button :severity="viewMode === 'card' ? 'primary' : 'secondary'" size="small" @click="viewMode = 'card'" v-tooltip.top="'卡片视图'">
|
||||
<template #icon>
|
||||
<LayoutGrid :size="16" />
|
||||
<LayoutGrid :size="18" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button text :severity="viewMode === 'list' ? 'primary' : 'secondary'" size="small" @click="viewMode = 'list'" v-tooltip.top="'列表视图'" :class="['btn-icon w-8 h-8', viewMode === 'list' && 'btn-active']">
|
||||
<Button :severity="viewMode === 'list' ? 'primary' : 'secondary'" size="small" @click="viewMode = 'list'" v-tooltip.top="'列表视图'">
|
||||
<template #icon>
|
||||
<List :size="16" />
|
||||
<List :size="18" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="flex items-center gap-3">
|
||||
<IconField class="flex-1">
|
||||
<InputIcon>
|
||||
<Search :size="16" />
|
||||
</InputIcon>
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索笔记..."
|
||||
class="w-full"
|
||||
size="small"
|
||||
/>
|
||||
</IconField>
|
||||
<span class="text-sm text-surface-400 shrink-0">{{ filteredNotes.length }} 篇笔记</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
@@ -92,31 +96,31 @@ function getTagSeverity(tag) {
|
||||
<!-- Card View -->
|
||||
<div v-if="viewMode === 'card'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pt-1">
|
||||
<div
|
||||
v-for="note in filteredNotes"
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
class="group bg-surface-0 rounded-xl border border-surface-200 p-4 cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary-200 hover:-translate-y-0.5"
|
||||
class="group bg-surface-0 rounded-xl border border-app-border p-4 cursor-pointer transition-all duration-200 hover:shadow-md hover:border-primary-300 hover:-translate-y-0.5"
|
||||
@click="selectNote(note)"
|
||||
>
|
||||
<!-- Card Header -->
|
||||
<div class="flex items-start gap-2 mb-3">
|
||||
<FileText :size="18" class="text-primary-400 shrink-0 mt-0.5" />
|
||||
<h3 class="text-sm font-semibold text-surface-900 line-clamp-2 group-hover:text-primary-600 transition-colors">
|
||||
{{ note.title }}
|
||||
{{ note.title || '无标题' }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- Preview -->
|
||||
<p class="text-xs text-surface-500 line-clamp-3 mb-3 leading-relaxed">
|
||||
{{ note.preview }}
|
||||
{{ getPreview(note) }}
|
||||
</p>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<Tag
|
||||
v-for="tag in note.tags.slice(0, 3)"
|
||||
:key="tag"
|
||||
:value="tag"
|
||||
:severity="getTagSeverity(tag)"
|
||||
v-for="tagName in getTagNames(note).slice(0, 3)"
|
||||
:key="tagName"
|
||||
:value="tagName"
|
||||
:severity="getTagSeverity(tagName)"
|
||||
class="text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -125,11 +129,11 @@ function getTagSeverity(tag) {
|
||||
<div class="flex items-center justify-between pt-2 border-t border-surface-100">
|
||||
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
||||
<Clock :size="12" />
|
||||
<span>{{ formatDate(note.date) }}</span>
|
||||
<span>{{ formatDate(note.updated_at) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
||||
<Folder :size="12" />
|
||||
<span>{{ note.notebook }}</span>
|
||||
<span>{{ getNotebookName(note) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,26 +142,26 @@ function getTagSeverity(tag) {
|
||||
<!-- List View -->
|
||||
<div v-else class="flex flex-col gap-1">
|
||||
<div
|
||||
v-for="note in filteredNotes"
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
class="group flex items-center gap-4 bg-surface-0 rounded-lg border border-surface-200 px-4 py-3 cursor-pointer transition-all duration-200 hover:shadow-sm hover:border-primary-200"
|
||||
class="group flex items-center gap-4 bg-surface-0 rounded-lg border border-app-border px-4 py-3 cursor-pointer transition-all duration-200 hover:shadow-sm hover:border-primary-300"
|
||||
@click="selectNote(note)"
|
||||
>
|
||||
<FileText :size="18" class="text-primary-400 shrink-0" />
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-semibold text-surface-900 truncate group-hover:text-primary-600 transition-colors">
|
||||
{{ note.title }}
|
||||
{{ note.title || '无标题' }}
|
||||
</h3>
|
||||
<p class="text-xs text-surface-500 truncate mt-0.5">{{ note.preview }}</p>
|
||||
<p class="text-xs text-surface-500 truncate mt-0.5">{{ getPreview(note) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Tag
|
||||
v-for="tag in note.tags.slice(0, 2)"
|
||||
:key="tag"
|
||||
:value="tag"
|
||||
:severity="getTagSeverity(tag)"
|
||||
v-for="tagName in getTagNames(note).slice(0, 2)"
|
||||
:key="tagName"
|
||||
:value="tagName"
|
||||
:severity="getTagSeverity(tagName)"
|
||||
class="text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -165,18 +169,18 @@ function getTagSeverity(tag) {
|
||||
<div class="flex items-center gap-4 shrink-0 text-xs text-surface-400">
|
||||
<div class="flex items-center gap-1">
|
||||
<Folder :size="12" />
|
||||
<span>{{ note.notebook }}</span>
|
||||
<span>{{ getNotebookName(note) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Calendar :size="12" />
|
||||
<span>{{ formatDate(note.date) }}</span>
|
||||
<span>{{ formatDate(note.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="filteredNotes.length === 0" class="flex flex-col items-center justify-center py-20 text-surface-400">
|
||||
<div v-if="notes.length === 0" class="flex flex-col items-center justify-center py-20 text-surface-400">
|
||||
<FileText :size="48" class="mb-4 opacity-30" />
|
||||
<p class="text-sm">没有找到笔记</p>
|
||||
</div>
|
||||
|
||||
@@ -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 : ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -84,23 +117,23 @@ function selectSearchNote(note) {
|
||||
<template v-if="!collapsed">
|
||||
<div class="flex-1 relative">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<Search :size="16" />
|
||||
</InputIcon>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索笔记..."
|
||||
class="w-full"
|
||||
size="small"
|
||||
@input="onSearchInput"
|
||||
/>
|
||||
</IconField>
|
||||
|
||||
<!-- Search Results Dropdown -->
|
||||
<div
|
||||
v-if="filteredNotes.length > 0"
|
||||
v-if="searchResults.length > 0"
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-surface-0 border border-app-border rounded-lg shadow-lg z-50 max-h-48 overflow-auto"
|
||||
>
|
||||
<div
|
||||
v-for="note in filteredNotes"
|
||||
v-for="note in searchResults"
|
||||
:key="note.id"
|
||||
class="px-3 py-2 hover:bg-surface-50 cursor-pointer flex items-center gap-2"
|
||||
@click="selectSearchNote(note)"
|
||||
@@ -108,13 +141,12 @@ function selectSearchNote(note) {
|
||||
<File :size="14" class="text-surface-400" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm text-surface-800 truncate">{{ note.title }}</div>
|
||||
<div class="text-xs text-surface-400">{{ note.notebook }}</div>
|
||||
<div class="text-xs text-surface-400">{{ getNotebookName(note) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button icon="pi pi-search" size="small" severity="success" aria-label="Search" />
|
||||
<Button icon="pi pi-plus" size="small" severity="secondary" aria-label="新建笔记" @click="createNote" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="w-full flex justify-center">
|
||||
@@ -142,77 +174,206 @@ function selectSearchNote(note) {
|
||||
>
|
||||
<component :is="item.icon" :size="18" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-px bg-surface-200 my-2" />
|
||||
|
||||
<!-- Notebooks (collapsed) -->
|
||||
<div
|
||||
v-for="nb in notebooks"
|
||||
:key="nb.id"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === `notebook-${nb.id}` ? 'bg-emerald-50 text-emerald-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="selectNotebook(nb)"
|
||||
v-tooltip.right="nb.name"
|
||||
>
|
||||
<BookOpen :size="18" />
|
||||
</div>
|
||||
|
||||
<div class="w-8 h-px bg-surface-200 my-2" />
|
||||
|
||||
<!-- Tags (collapsed) -->
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-lg cursor-pointer transition-colors"
|
||||
:class="activeNav === `tag-${tag.id}` ? 'bg-amber-50 text-amber-600' : 'text-surface-500 hover:bg-surface-100'"
|
||||
@click="selectTag(tag)"
|
||||
v-tooltip.right="tag.name"
|
||||
>
|
||||
<Hash :size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Expanded: Full nav -->
|
||||
<template v-else>
|
||||
<!-- Notes Section -->
|
||||
<div class="py-3">
|
||||
<div class="px-4 py-1 text-xs font-semibold text-surface-400 uppercase tracking-wider">
|
||||
笔记
|
||||
</div>
|
||||
<div
|
||||
v-for="item in navSections[0].items"
|
||||
:key="item.id"
|
||||
class="flex items-center gap-3 px-4 py-2 cursor-pointer transition-colors"
|
||||
:class="activeNav === item.id ? 'bg-primary-50 text-primary-600' : 'text-surface-700 hover:bg-surface-100'"
|
||||
@click="handleNav(item.id)"
|
||||
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
|
||||
@click="toggleSection('notes')"
|
||||
>
|
||||
<component :is="item.icon" :size="18" />
|
||||
<span class="text-sm">{{ item.label }}</span>
|
||||
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">笔记</span>
|
||||
<ChevronDown
|
||||
v-if="expandedSections.notes"
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<div v-show="expandedSections.notes">
|
||||
<div
|
||||
v-for="item in navSections[0].items"
|
||||
:key="item.id"
|
||||
class="flex items-center gap-3 px-4 py-2 mx-2 cursor-pointer transition-colors rounded-lg"
|
||||
:class="activeNav === item.id
|
||||
? 'bg-surface-200/70 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'"
|
||||
@click="handleNav(item.id)"
|
||||
>
|
||||
<component :is="item.icon" :size="18" />
|
||||
<span class="text-sm">{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Notebooks Section -->
|
||||
<div class="py-1">
|
||||
<div
|
||||
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
|
||||
@click="toggleSection('notebooks')"
|
||||
>
|
||||
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">笔记本</span>
|
||||
<ChevronDown
|
||||
v-if="expandedSections.notebooks"
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<div v-show="expandedSections.notebooks">
|
||||
<div
|
||||
v-for="nb in notebooks"
|
||||
:key="nb.id"
|
||||
class="flex items-center gap-3 px-4 py-2 mx-2 cursor-pointer transition-colors rounded-lg group"
|
||||
:class="activeNav === `notebook-${nb.id}`
|
||||
? 'bg-emerald-50 text-emerald-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'"
|
||||
@click="selectNotebook(nb)"
|
||||
>
|
||||
<BookOpen :size="18" />
|
||||
<span class="text-sm flex-1">{{ nb.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Tags Section -->
|
||||
<div class="py-1">
|
||||
<div
|
||||
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
|
||||
@click="toggleSection('tags')"
|
||||
>
|
||||
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">标签</span>
|
||||
<ChevronDown
|
||||
v-if="expandedSections.tags"
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
<ChevronRight
|
||||
v-else
|
||||
:size="14"
|
||||
class="text-surface-400 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="collapse">
|
||||
<div v-show="expandedSections.tags">
|
||||
<div
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
class="flex items-center gap-3 px-4 py-2 mx-2 cursor-pointer transition-colors rounded-lg group"
|
||||
:class="activeNav === `tag-${tag.id}`
|
||||
? 'bg-amber-50 text-amber-700 font-medium shadow-sm'
|
||||
: 'text-surface-600 hover:bg-surface-100'"
|
||||
@click="selectTag(tag)"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-amber-400" />
|
||||
<span class="text-sm flex-1">{{ tag.name }}</span>
|
||||
<span class="text-xs text-surface-400 opacity-0 group-hover:opacity-100 transition-opacity">{{ tag.note_count || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
</ScrollPanel>
|
||||
|
||||
<!-- Bottom Actions -->
|
||||
<div class="p-3 border-t border-app-border">
|
||||
<template v-if="collapsed">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<Button text severity="secondary" size="small" @click="toggleTheme" v-tooltip.right="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'" class="btn-icon w-8 h-8">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<Button text severity="secondary" @click="toggleTheme" v-tooltip.right="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'" class="btn-icon w-10 h-10">
|
||||
<template #icon>
|
||||
<Sun v-if="theme === 'light'" :size="16" />
|
||||
<Moon v-else-if="theme === 'dark'" :size="16" />
|
||||
<Monitor v-else :size="16" />
|
||||
<Sun v-if="theme === 'light'" :size="20" />
|
||||
<Moon v-else-if="theme === 'dark'" :size="20" />
|
||||
<Monitor v-else :size="20" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button text severity="secondary" size="small" @click="handleNav('trash')" v-tooltip.right="'回收站'" :class="['btn-icon w-8 h-8', activeNav === 'trash' && 'btn-active']">
|
||||
<Button text severity="secondary" @click="handleNav('settings')" v-tooltip.right="'设置'" class="btn-icon w-10 h-10">
|
||||
<template #icon>
|
||||
<Trash2 :size="16" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button text severity="secondary" size="small" @click="handleNav('settings')" v-tooltip.right="'设置'" class="btn-icon w-8 h-8">
|
||||
<template #icon>
|
||||
<Settings :size="16" />
|
||||
<Settings :size="20" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button text severity="secondary" size="small" @click="handleNav('trash')" :class="['btn-icon flex-1', activeNav === 'trash' && 'btn-active']">
|
||||
<Button text severity="secondary" @click="toggleTheme" class="btn-icon h-10 px-3 flex items-center gap-2">
|
||||
<template #icon>
|
||||
<Trash2 :size="16" class="mr-2" />
|
||||
<Sun v-if="theme === 'light'" :size="18" />
|
||||
<Moon v-else-if="theme === 'dark'" :size="18" />
|
||||
<Monitor v-else :size="18" />
|
||||
</template>
|
||||
<span class="text-sm">回收站</span>
|
||||
<span class="text-sm">{{ theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '系统' }}</span>
|
||||
</Button>
|
||||
<Button text severity="secondary" size="small" @click="toggleTheme" v-tooltip.top="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'" class="btn-icon w-8 h-8">
|
||||
<Button text severity="secondary" @click="handleNav('settings')" class="btn-icon h-10 px-3 flex items-center gap-2 flex-1">
|
||||
<template #icon>
|
||||
<Sun v-if="theme === 'light'" :size="16" />
|
||||
<Moon v-else-if="theme === 'dark'" :size="16" />
|
||||
<Monitor v-else :size="16" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button text severity="secondary" size="small" @click="handleNav('settings')" class="btn-icon w-8 h-8">
|
||||
<template #icon>
|
||||
<Settings :size="16" />
|
||||
<Settings :size="18" />
|
||||
</template>
|
||||
<span class="text-sm">设置</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.collapse-enter-active,
|
||||
.collapse-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapse-enter-from,
|
||||
.collapse-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
.collapse-enter-to,
|
||||
.collapse-leave-from {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user