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",
|
"@lucide/vue": "^1.17.0",
|
||||||
"@primeuix/themes": "^2.0.3",
|
"@primeuix/themes": "^2.0.3",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
"electron": "^42.3.0",
|
"electron": "^42.3.0",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"nanoid": "^5.1.11",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.5.5",
|
"primevue": "^4.5.5",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
|
"sql.js": "^1.14.1",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"vue": "^3.5.35"
|
"vue": "^3.5.35"
|
||||||
@@ -2828,6 +2833,21 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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": {
|
"node_modules/chownr": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
@@ -4162,6 +4182,19 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@@ -4181,6 +4214,18 @@
|
|||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"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==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -4843,6 +4931,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"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"
|
"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": {
|
"node_modules/lazy-val": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
@@ -6241,9 +6347,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "5.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6252,10 +6358,10 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^18 || >=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
@@ -6553,6 +6659,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/postject": {
|
||||||
"version": "1.0.0-alpha.6",
|
"version": "1.0.0-alpha.6",
|
||||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||||
@@ -6583,6 +6707,12 @@
|
|||||||
"node": "^12.20.0 || >=14"
|
"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": {
|
"node_modules/primevue": {
|
||||||
"version": "4.5.5",
|
"version": "4.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
|
||||||
@@ -6700,6 +6830,19 @@
|
|||||||
"read-binary-file-arch": "cli.js"
|
"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": {
|
"node_modules/rehype-stringify": {
|
||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz",
|
||||||
@@ -6944,6 +7087,19 @@
|
|||||||
"node": ">=11.0.0"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
|
||||||
@@ -7100,6 +7256,12 @@
|
|||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"optional": true
|
"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": {
|
"node_modules/stat-mode": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz",
|
||||||
@@ -7152,6 +7314,15 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
|||||||
@@ -21,13 +21,18 @@
|
|||||||
"@lucide/vue": "^1.17.0",
|
"@lucide/vue": "^1.17.0",
|
||||||
"@primeuix/themes": "^2.0.3",
|
"@primeuix/themes": "^2.0.3",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
"electron": "^42.3.0",
|
"electron": "^42.3.0",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"nanoid": "^5.1.11",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.5.5",
|
"primevue": "^4.5.5",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.2",
|
"remark-rehype": "^11.1.2",
|
||||||
|
"sql.js": "^1.14.1",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"vue": "^3.5.35"
|
"vue": "^3.5.35"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron'
|
import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { is } from '@electron-toolkit/utils'
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { StorageService } from './services/StorageService'
|
||||||
|
|
||||||
let mainWindow = null
|
let mainWindow = null
|
||||||
|
let storageService = null
|
||||||
|
|
||||||
// 标题栏颜色配置
|
// 标题栏颜色配置
|
||||||
const titleBarColors = {
|
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', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
// Save all pending changes before quitting
|
||||||
|
if (storageService) {
|
||||||
|
storageService.flush()
|
||||||
|
storageService.close()
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,38 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
// 安全地暴露 API 给渲染进程
|
// 安全地暴露 API 给渲染进程
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
platform: process.platform,
|
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>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import SideBar from './components/SideBar.vue'
|
import SideBar from './components/SideBar.vue'
|
||||||
import NotesOverview from './components/NotesOverview.vue'
|
import NotesOverview from './components/NotesOverview.vue'
|
||||||
import EditorPanel from './components/EditorPanel.vue'
|
import EditorPanel from './components/EditorPanel.vue'
|
||||||
import { useTheme } from './composables/useTheme'
|
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 { 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 currentNav = ref('all')
|
||||||
const currentNote = ref(null)
|
|
||||||
const filterType = ref('all')
|
const filterType = ref('all')
|
||||||
const filterValue = ref(null)
|
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([])
|
const overviewNotes = ref([])
|
||||||
|
|
||||||
function computeOverviewNotes() {
|
function computeOverviewNotes() {
|
||||||
let result = allNotes.value
|
let result = allNotes.value
|
||||||
if (filterType.value === 'notebook' && filterValue.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) {
|
} 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
|
overviewNotes.value = result
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNavigate(id) {
|
async function onNavigate(id) {
|
||||||
currentNav.value = id
|
currentNav.value = id
|
||||||
filterType.value = 'all'
|
filterType.value = 'all'
|
||||||
filterValue.value = null
|
filterValue.value = null
|
||||||
currentNote.value = null
|
clearCurrentNote()
|
||||||
|
|
||||||
|
if (id === 'trash') {
|
||||||
|
await loadNotes({ isTrashed: 1 })
|
||||||
|
} else {
|
||||||
|
await loadNotes({ isTrashed: 0 })
|
||||||
|
}
|
||||||
computeOverviewNotes()
|
computeOverviewNotes()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectNote(note) {
|
function onSelectNote(note) {
|
||||||
currentNote.value = note
|
getNote(note.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBackToOverview() {
|
function onBackToOverview() {
|
||||||
currentNote.value = null
|
clearCurrentNote()
|
||||||
|
// Reload notes to reflect any changes
|
||||||
|
loadNotes({ isTrashed: filterType.value === 'trash' ? 1 : 0 }).then(() => {
|
||||||
|
computeOverviewNotes()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCreateNote() {
|
async function onCreateNote() {
|
||||||
// TODO: Implement note creation
|
const notebookId = filterType.value === 'notebook' && filterValue.value
|
||||||
console.log('Creating new note...')
|
? filterValue.value.id
|
||||||
|
: 'default'
|
||||||
|
|
||||||
|
const note = await createNote({
|
||||||
|
title: '无标题',
|
||||||
|
content: '',
|
||||||
|
notebookId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
// Auto select the new note
|
||||||
|
getNote(note.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectNotebook(notebook) {
|
function onSelectNotebook(notebook) {
|
||||||
filterType.value = 'notebook'
|
filterType.value = 'notebook'
|
||||||
filterValue.value = notebook
|
filterValue.value = notebook
|
||||||
currentNote.value = null
|
clearCurrentNote()
|
||||||
computeOverviewNotes()
|
loadNotes({ notebookId: notebook.id, isTrashed: 0 }).then(() => {
|
||||||
|
computeOverviewNotes()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectTag(tag) {
|
function onSelectTag(tag) {
|
||||||
filterType.value = 'tag'
|
filterType.value = 'tag'
|
||||||
filterValue.value = tag
|
filterValue.value = tag
|
||||||
currentNote.value = null
|
clearCurrentNote()
|
||||||
computeOverviewNotes()
|
loadNotes({ tagId: tag.id, isTrashed: 0 }).then(() => {
|
||||||
|
computeOverviewNotes()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
|
||||||
computeOverviewNotes()
|
|
||||||
|
|
||||||
function getOverviewTitle() {
|
function getOverviewTitle() {
|
||||||
if (filterType.value === 'notebook' && filterValue.value) {
|
if (filterType.value === 'notebook' && filterValue.value) {
|
||||||
return filterValue.value.name
|
return filterValue.value.name
|
||||||
@@ -82,6 +129,9 @@ function getOverviewTitle() {
|
|||||||
if (filterType.value === 'tag' && filterValue.value) {
|
if (filterType.value === 'tag' && filterValue.value) {
|
||||||
return `标签: ${filterValue.value.name}`
|
return `标签: ${filterValue.value.name}`
|
||||||
}
|
}
|
||||||
|
if (filterType.value === 'search' && filterValue.value) {
|
||||||
|
return `搜索: ${filterValue.value}`
|
||||||
|
}
|
||||||
const titleMap = {
|
const titleMap = {
|
||||||
all: '所有笔记',
|
all: '所有笔记',
|
||||||
recent: '最近编辑',
|
recent: '最近编辑',
|
||||||
@@ -90,6 +140,16 @@ function getOverviewTitle() {
|
|||||||
}
|
}
|
||||||
return titleMap[currentNav.value] || '所有笔记'
|
return titleMap[currentNav.value] || '所有笔记'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize: load data on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadNotes({ isTrashed: 0 }),
|
||||||
|
loadNotebooks(),
|
||||||
|
loadTags()
|
||||||
|
])
|
||||||
|
computeOverviewNotes()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -98,6 +158,8 @@ function getOverviewTitle() {
|
|||||||
<div class="flex flex-1 overflow-hidden">
|
<div class="flex flex-1 overflow-hidden">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<SideBar
|
<SideBar
|
||||||
|
:notebooks="notebooks"
|
||||||
|
:tags="tags"
|
||||||
@navigate="onNavigate"
|
@navigate="onNavigate"
|
||||||
@create-note="onCreateNote"
|
@create-note="onCreateNote"
|
||||||
@select-notebook="onSelectNotebook"
|
@select-notebook="onSelectNotebook"
|
||||||
@@ -120,9 +182,9 @@ function getOverviewTitle() {
|
|||||||
v-else
|
v-else
|
||||||
:note="currentNote"
|
:note="currentNote"
|
||||||
@back="onBackToOverview"
|
@back="onBackToOverview"
|
||||||
|
@save="({ title, content }) => updateNote(currentNote.id, { title, content })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -140,42 +140,6 @@
|
|||||||
--color-app-ring: var(--theme-ring);
|
--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);
|
background-color: rgba(129, 140, 248, 0.3);
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tooltip 样式 */
|
||||||
|
.p-tooltip-text {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ const props = defineProps({
|
|||||||
note: { type: Object, default: null }
|
note: { type: Object, default: null }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['back'])
|
const emit = defineEmits(['back', 'save'])
|
||||||
|
|
||||||
const noteTitle = ref('')
|
const noteTitle = ref('')
|
||||||
const noteContent = ref('')
|
const noteContent = ref('')
|
||||||
|
const isSaving = ref(false)
|
||||||
|
const lastSavedAt = ref(null)
|
||||||
|
|
||||||
// View mode: edit | preview | split
|
// View mode: edit | preview | split
|
||||||
const viewMode = ref('edit')
|
const viewMode = ref('edit')
|
||||||
@@ -42,14 +44,49 @@ const headings = ref([])
|
|||||||
// Rendered HTML for preview
|
// Rendered HTML for preview
|
||||||
const renderedHtml = ref('')
|
const renderedHtml = ref('')
|
||||||
|
|
||||||
|
// Auto-save timer
|
||||||
|
let saveTimer = null
|
||||||
|
const SAVE_DELAY = 1000 // 1 second debounce
|
||||||
|
|
||||||
// Update local state when note prop changes
|
// Update local state when note prop changes
|
||||||
watch(() => props.note, (newNote) => {
|
watch(() => props.note, (newNote) => {
|
||||||
if (newNote) {
|
if (newNote) {
|
||||||
noteTitle.value = newNote.title || ''
|
noteTitle.value = newNote.title || ''
|
||||||
noteContent.value = newNote.content || getDefaultContent(newNote.title)
|
noteContent.value = newNote.content || ''
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { 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
|
// Parse headings from markdown content
|
||||||
watch(noteContent, (content) => {
|
watch(noteContent, (content) => {
|
||||||
const result = []
|
const result = []
|
||||||
@@ -83,14 +120,6 @@ watch(noteContent, async (content) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
function getDefaultContent(title) {
|
|
||||||
return `# ${title}
|
|
||||||
|
|
||||||
在这里开始写作...
|
|
||||||
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return ''
|
if (!dateStr) return ''
|
||||||
const d = new Date(dateStr)
|
const d = new Date(dateStr)
|
||||||
@@ -98,6 +127,9 @@ function formatDate(dateStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
|
// Save before going back
|
||||||
|
if (saveTimer) clearTimeout(saveTimer)
|
||||||
|
doSave()
|
||||||
emit('back')
|
emit('back')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +157,17 @@ const toolbarItems3 = [
|
|||||||
{ icon: Link, title: '链接' },
|
{ icon: Link, title: '链接' },
|
||||||
{ icon: Image, 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -143,9 +186,11 @@ const toolbarItems3 = [
|
|||||||
placeholder="输入标题..."
|
placeholder="输入标题..."
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-3 mt-1 text-xs text-surface-400">
|
<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?.updated_at">最后编辑: {{ formatDate(note.updated_at) }}</span>
|
||||||
<span v-if="note?.date && note?.notebook">|</span>
|
<span v-if="note?.updated_at && notebookName">|</span>
|
||||||
<span v-if="note?.notebook">{{ note.notebook }}</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import Button from 'primevue/button'
|
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 Tag from 'primevue/tag'
|
||||||
import ScrollPanel from 'primevue/scrollpanel'
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
import { Search, LayoutGrid, List, FileText, Clock, Calendar, Folder } from '@lucide/vue'
|
import { Search, LayoutGrid, List, FileText, Clock, Calendar, Folder } from '@lucide/vue'
|
||||||
@@ -15,29 +12,21 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['select-note'])
|
const emit = defineEmits(['select-note'])
|
||||||
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const viewMode = ref('card') // 'card' | 'list'
|
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) {
|
function selectNote(note) {
|
||||||
emit('select-note', note)
|
emit('select-note', note)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return ''
|
||||||
const d = new Date(dateStr)
|
const d = new Date(dateStr)
|
||||||
const month = d.getMonth() + 1
|
const month = d.getMonth() + 1
|
||||||
const day = d.getDate()
|
const day = d.getDate()
|
||||||
return `${month}月${day}日`
|
return `${month}月${day}日`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTagSeverity(tag) {
|
function getTagSeverity(tagName) {
|
||||||
const severityMap = {
|
const severityMap = {
|
||||||
'重要': 'danger',
|
'重要': 'danger',
|
||||||
'待办': 'warning',
|
'待办': 'warning',
|
||||||
@@ -45,7 +34,38 @@ function getTagSeverity(tag) {
|
|||||||
'灵感': 'success',
|
'灵感': 'success',
|
||||||
'会议': 'secondary'
|
'会议': '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>
|
</script>
|
||||||
|
|
||||||
@@ -56,34 +76,18 @@ function getTagSeverity(tag) {
|
|||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h1 class="text-xl font-bold text-surface-900">{{ title }}</h1>
|
<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">
|
<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>
|
<template #icon>
|
||||||
<LayoutGrid :size="16" />
|
<LayoutGrid :size="18" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</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>
|
<template #icon>
|
||||||
<List :size="16" />
|
<List :size="18" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
@@ -92,31 +96,31 @@ function getTagSeverity(tag) {
|
|||||||
<!-- Card View -->
|
<!-- 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-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
|
<div
|
||||||
v-for="note in filteredNotes"
|
v-for="note in notes"
|
||||||
:key="note.id"
|
: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)"
|
@click="selectNote(note)"
|
||||||
>
|
>
|
||||||
<!-- Card Header -->
|
<!-- Card Header -->
|
||||||
<div class="flex items-start gap-2 mb-3">
|
<div class="flex items-start gap-2 mb-3">
|
||||||
<FileText :size="18" class="text-primary-400 shrink-0 mt-0.5" />
|
<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">
|
<h3 class="text-sm font-semibold text-surface-900 line-clamp-2 group-hover:text-primary-600 transition-colors">
|
||||||
{{ note.title }}
|
{{ note.title || '无标题' }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Preview -->
|
<!-- Preview -->
|
||||||
<p class="text-xs text-surface-500 line-clamp-3 mb-3 leading-relaxed">
|
<p class="text-xs text-surface-500 line-clamp-3 mb-3 leading-relaxed">
|
||||||
{{ note.preview }}
|
{{ getPreview(note) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<div class="flex flex-wrap gap-1 mb-3">
|
<div class="flex flex-wrap gap-1 mb-3">
|
||||||
<Tag
|
<Tag
|
||||||
v-for="tag in note.tags.slice(0, 3)"
|
v-for="tagName in getTagNames(note).slice(0, 3)"
|
||||||
:key="tag"
|
:key="tagName"
|
||||||
:value="tag"
|
:value="tagName"
|
||||||
:severity="getTagSeverity(tag)"
|
:severity="getTagSeverity(tagName)"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 justify-between pt-2 border-t border-surface-100">
|
||||||
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
||||||
<Clock :size="12" />
|
<Clock :size="12" />
|
||||||
<span>{{ formatDate(note.date) }}</span>
|
<span>{{ formatDate(note.updated_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
<div class="flex items-center gap-1.5 text-xs text-surface-400">
|
||||||
<Folder :size="12" />
|
<Folder :size="12" />
|
||||||
<span>{{ note.notebook }}</span>
|
<span>{{ getNotebookName(note) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,26 +142,26 @@ function getTagSeverity(tag) {
|
|||||||
<!-- List View -->
|
<!-- List View -->
|
||||||
<div v-else class="flex flex-col gap-1">
|
<div v-else class="flex flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
v-for="note in filteredNotes"
|
v-for="note in notes"
|
||||||
:key="note.id"
|
: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)"
|
@click="selectNote(note)"
|
||||||
>
|
>
|
||||||
<FileText :size="18" class="text-primary-400 shrink-0" />
|
<FileText :size="18" class="text-primary-400 shrink-0" />
|
||||||
|
|
||||||
<div class="flex-1 min-w-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">
|
<h3 class="text-sm font-semibold text-surface-900 truncate group-hover:text-primary-600 transition-colors">
|
||||||
{{ note.title }}
|
{{ note.title || '无标题' }}
|
||||||
</h3>
|
</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>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<Tag
|
<Tag
|
||||||
v-for="tag in note.tags.slice(0, 2)"
|
v-for="tagName in getTagNames(note).slice(0, 2)"
|
||||||
:key="tag"
|
:key="tagName"
|
||||||
:value="tag"
|
:value="tagName"
|
||||||
:severity="getTagSeverity(tag)"
|
:severity="getTagSeverity(tagName)"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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-4 shrink-0 text-xs text-surface-400">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Folder :size="12" />
|
<Folder :size="12" />
|
||||||
<span>{{ note.notebook }}</span>
|
<span>{{ getNotebookName(note) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Calendar :size="12" />
|
<Calendar :size="12" />
|
||||||
<span>{{ formatDate(note.date) }}</span>
|
<span>{{ formatDate(note.updated_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<!-- 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" />
|
<FileText :size="48" class="mb-4 opacity-30" />
|
||||||
<p class="text-sm">没有找到笔记</p>
|
<p class="text-sm">没有找到笔记</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,15 +6,23 @@ import IconField from 'primevue/iconfield'
|
|||||||
import InputIcon from 'primevue/inputicon'
|
import InputIcon from 'primevue/inputicon'
|
||||||
import ScrollPanel from 'primevue/scrollpanel'
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
import {
|
import {
|
||||||
Search, Plus, File, Star,
|
Search, Plus, File, Star, Trash2,
|
||||||
Trash2, Settings, Sun, Moon, Monitor
|
Settings, Sun, Moon, Monitor,
|
||||||
|
BookOpen, Tag, ChevronDown, ChevronRight, Hash
|
||||||
} from '@lucide/vue'
|
} from '@lucide/vue'
|
||||||
import { useTheme } from '../composables/useTheme'
|
import { useTheme } from '../composables/useTheme'
|
||||||
|
import { useNotes } from '../composables/useNotes'
|
||||||
import ButtonIcon from "./ButtonIcon.vue";
|
import ButtonIcon from "./ButtonIcon.vue";
|
||||||
|
|
||||||
const { theme, toggleTheme } = useTheme()
|
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
|
// Collapse state
|
||||||
const collapsed = ref(false)
|
const collapsed = ref(false)
|
||||||
@@ -26,27 +34,33 @@ function toggleCollapsed() {
|
|||||||
// Navigation state
|
// Navigation state
|
||||||
const activeNav = ref('all')
|
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
|
// Search
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
const searchResults = ref([])
|
||||||
|
let searchTimer = null
|
||||||
|
|
||||||
// Mock data - notes for search
|
async function onSearchInput() {
|
||||||
const allNotes = ref([
|
if (searchTimer) clearTimeout(searchTimer)
|
||||||
{ id: 1, title: '项目需求文档', notebook: '工作' },
|
const q = searchQuery.value
|
||||||
{ id: 2, title: '读书笔记 - 深入理解计算机系统', notebook: '学习' },
|
if (!q || q.length < 1) {
|
||||||
{ id: 3, title: '周报 - 第22周', notebook: '工作' },
|
searchResults.value = []
|
||||||
{ id: 4, title: 'Vue 3 学习笔记', notebook: '学习' },
|
return
|
||||||
{ id: 5, title: '旅行计划 - 日本', notebook: '生活' },
|
}
|
||||||
{ id: 6, title: 'Markdown 语法速查表', notebook: '技术' },
|
searchTimer = setTimeout(async () => {
|
||||||
{ id: 7, title: '会议纪要 - 产品评审', notebook: '工作' },
|
searchResults.value = await searchNotes(q)
|
||||||
{ id: 8, title: '健身计划', notebook: '生活' },
|
}, 200)
|
||||||
])
|
}
|
||||||
|
|
||||||
// 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))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Navigation items
|
// Navigation items
|
||||||
const navSections = [
|
const navSections = [
|
||||||
@@ -55,6 +69,7 @@ const navSections = [
|
|||||||
items: [
|
items: [
|
||||||
{ id: 'all', icon: File, label: '所有笔记' },
|
{ id: 'all', icon: File, label: '所有笔记' },
|
||||||
{ id: 'favorites', icon: Star, label: '收藏夹' },
|
{ id: 'favorites', icon: Star, label: '收藏夹' },
|
||||||
|
{ id: 'trash', icon: Trash2, label: '回收站' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -65,6 +80,16 @@ function handleNav(id) {
|
|||||||
emit('navigate', 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() {
|
function createNote() {
|
||||||
emit('create-note')
|
emit('create-note')
|
||||||
}
|
}
|
||||||
@@ -72,6 +97,14 @@ function createNote() {
|
|||||||
function selectSearchNote(note) {
|
function selectSearchNote(note) {
|
||||||
emit('select-note', note)
|
emit('select-note', note)
|
||||||
searchQuery.value = ''
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -84,23 +117,23 @@ function selectSearchNote(note) {
|
|||||||
<template v-if="!collapsed">
|
<template v-if="!collapsed">
|
||||||
<div class="flex-1 relative">
|
<div class="flex-1 relative">
|
||||||
<IconField>
|
<IconField>
|
||||||
<InputIcon>
|
<InputIcon class="pi pi-search" />
|
||||||
<Search :size="16" />
|
|
||||||
</InputIcon>
|
|
||||||
<InputText
|
<InputText
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索笔记..."
|
placeholder="搜索笔记..."
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="small"
|
size="small"
|
||||||
|
@input="onSearchInput"
|
||||||
/>
|
/>
|
||||||
</IconField>
|
</IconField>
|
||||||
|
|
||||||
<!-- Search Results Dropdown -->
|
<!-- Search Results Dropdown -->
|
||||||
<div
|
<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"
|
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
|
<div
|
||||||
v-for="note in filteredNotes"
|
v-for="note in searchResults"
|
||||||
:key="note.id"
|
:key="note.id"
|
||||||
class="px-3 py-2 hover:bg-surface-50 cursor-pointer flex items-center gap-2"
|
class="px-3 py-2 hover:bg-surface-50 cursor-pointer flex items-center gap-2"
|
||||||
@click="selectSearchNote(note)"
|
@click="selectSearchNote(note)"
|
||||||
@@ -108,13 +141,12 @@ function selectSearchNote(note) {
|
|||||||
<File :size="14" class="text-surface-400" />
|
<File :size="14" class="text-surface-400" />
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-sm text-surface-800 truncate">{{ note.title }}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Button icon="pi pi-plus" size="small" severity="secondary" aria-label="新建笔记" @click="createNote" />
|
||||||
<Button icon="pi pi-search" size="small" severity="success" aria-label="Search" />
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
@@ -142,77 +174,206 @@ function selectSearchNote(note) {
|
|||||||
>
|
>
|
||||||
<component :is="item.icon" :size="18" />
|
<component :is="item.icon" :size="18" />
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<!-- Expanded: Full nav -->
|
<!-- Expanded: Full nav -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="py-3">
|
<div class="py-3">
|
||||||
<div class="px-4 py-1 text-xs font-semibold text-surface-400 uppercase tracking-wider">
|
|
||||||
笔记
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-for="item in navSections[0].items"
|
class="px-4 py-1.5 flex items-center justify-between cursor-pointer hover:bg-surface-100 transition-colors"
|
||||||
:key="item.id"
|
@click="toggleSection('notes')"
|
||||||
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)"
|
|
||||||
>
|
>
|
||||||
<component :is="item.icon" :size="18" />
|
<span class="text-xs font-semibold text-surface-400 uppercase tracking-wider">笔记</span>
|
||||||
<span class="text-sm">{{ item.label }}</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>
|
</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>
|
</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>
|
</template>
|
||||||
</ScrollPanel>
|
</ScrollPanel>
|
||||||
|
|
||||||
<!-- Bottom Actions -->
|
<!-- Bottom Actions -->
|
||||||
<div class="p-3 border-t border-app-border">
|
<div class="p-3 border-t border-app-border">
|
||||||
<template v-if="collapsed">
|
<template v-if="collapsed">
|
||||||
<div class="flex flex-col items-center gap-1">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<Button text severity="secondary" size="small" @click="toggleTheme" v-tooltip.right="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'" class="btn-icon w-8 h-8">
|
<Button text severity="secondary" @click="toggleTheme" v-tooltip.right="theme === 'light' ? '切换暗色模式' : theme === 'dark' ? '跟随系统' : '切换亮色模式'" class="btn-icon w-10 h-10">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Sun v-if="theme === 'light'" :size="16" />
|
<Sun v-if="theme === 'light'" :size="20" />
|
||||||
<Moon v-else-if="theme === 'dark'" :size="16" />
|
<Moon v-else-if="theme === 'dark'" :size="20" />
|
||||||
<Monitor v-else :size="16" />
|
<Monitor v-else :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</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>
|
<template #icon>
|
||||||
<Trash2 :size="16" />
|
<Settings :size="20" />
|
||||||
</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" />
|
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<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>
|
</template>
|
||||||
<span class="text-sm">回收站</span>
|
<span class="text-sm">{{ theme === 'light' ? '浅色' : theme === 'dark' ? '深色' : '系统' }}</span>
|
||||||
</Button>
|
</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>
|
<template #icon>
|
||||||
<Sun v-if="theme === 'light'" :size="16" />
|
<Settings :size="18" />
|
||||||
<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" />
|
|
||||||
</template>
|
</template>
|
||||||
|
<span class="text-sm">设置</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 Tooltip from 'primevue/tooltip'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/styles/main.css'
|
import './assets/styles/main.css'
|
||||||
|
import 'primeicons/primeicons.css'
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(PrimeVue, {
|
app.use(PrimeVue, {
|
||||||
|
|||||||
Reference in New Issue
Block a user