From ab26ea7ef9dd10cab9daff484d125f8b7f1b607c Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Sun, 31 May 2026 17:13:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD=E8=BF=AD=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 5 +- package-lock.json | 136 ++++++ package.json | 1 + src/main/index.js | 145 +++++- src/main/indexpreload.js | 11 + src/renderer/src/App.vue | 172 +++++-- src/renderer/src/components/NoteList.vue | 4 +- src/renderer/src/components/NotesOverview.vue | 4 +- src/renderer/src/components/SideBar.vue | 462 +++++++++++++----- .../src/components/WorkspaceSetup.vue | 115 +++++ src/renderer/src/main.js | 4 + 11 files changed, 868 insertions(+), 191 deletions(-) create mode 100644 src/renderer/src/components/WorkspaceSetup.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fa409cf..6747d8d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,10 @@ "Bash(ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ node *)", "Bash(pkill -f \"electron-vite\")", "Bash(npm list *)", - "Bash(npx electron-vite *)" + "Bash(npx electron-vite *)", + "WebSearch", + "WebFetch(domain:primevue.org)", + "Bash(curl -s http://localhost:5173)" ] } } diff --git a/package-lock.json b/package-lock.json index b30aa13..6cb86b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "electron-vite": "^5.0.0", "gray-matter": "^4.0.3", "nanoid": "^5.1.11", + "pinia": "^3.0.4", "primeicons": "^7.0.0", "primevue": "^4.5.5", "rehype-stringify": "^10.0.1", @@ -2110,6 +2111,39 @@ "@vue/shared": "3.5.35" } }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, "node_modules/@vue/reactivity": { "version": "3.5.35", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz", @@ -2568,6 +2602,15 @@ "node": ">=6.0.0" } }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/boolean": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", @@ -3003,6 +3046,21 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4767,6 +4825,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -4962,6 +5026,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", @@ -6326,6 +6402,12 @@ "node": ">= 18" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -6591,6 +6673,12 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6609,6 +6697,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -6982,6 +7091,12 @@ "node": ">= 4" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -7248,6 +7363,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -7335,6 +7459,18 @@ "node": ">= 8.0" } }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index cbcf7c0..95eaa15 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "electron-vite": "^5.0.0", "gray-matter": "^4.0.3", "nanoid": "^5.1.11", + "pinia": "^3.0.4", "primeicons": "^7.0.0", "primevue": "^4.5.5", "rehype-stringify": "^10.0.1", diff --git a/src/main/index.js b/src/main/index.js index 6e6c908..937500e 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,10 +1,12 @@ -import { app, BrowserWindow, ipcMain, nativeTheme } from 'electron' +import { app, BrowserWindow, ipcMain, nativeTheme, dialog } from 'electron' import { join } from 'path' +import { existsSync, readFileSync, writeFileSync } from 'fs' import { is } from '@electron-toolkit/utils' import { StorageService } from './services/StorageService' let mainWindow = null let storageService = null +let workspaceReady = false // 标题栏颜色配置 const titleBarColors = { @@ -18,6 +20,39 @@ const titleBarColors = { } } +// ==================== Config Management ==================== + +const configPath = join(app.getPath('userData'), 'config.json') + +function loadConfig() { + try { + if (existsSync(configPath)) { + const data = readFileSync(configPath, 'utf-8') + return JSON.parse(data) + } + } catch (err) { + console.error('Failed to load config:', err) + } + return {} +} + +function saveConfig(config) { + try { + writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8') + } catch (err) { + console.error('Failed to save config:', err) + } +} + +function sendWorkspaceStatus() { + if (!mainWindow) return + const config = loadConfig() + mainWindow.webContents.send('workspace:status', { + ready: workspaceReady, + path: config.workspacePath || null + }) +} + function createWindow() { // 根据系统主题设置初始标题栏颜色 const isDark = nativeTheme.shouldUseDarkColors @@ -47,6 +82,11 @@ function createWindow() { } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } + + // Send workspace status once renderer is ready + mainWindow.webContents.on('did-finish-load', () => { + sendWorkspaceStatus() + }) } // 处理渲染进程的主题切换请求 @@ -54,11 +94,84 @@ ipcMain.handle('theme:set', (_, theme) => { if (!mainWindow) return const colors = theme === 'dark' ? titleBarColors.dark : titleBarColors.light - mainWindow.setTitleBarOverlay({ - color: colors.color, - symbolColor: colors.symbolColor, - height: 36 + try { + mainWindow.setTitleBarOverlay({ + color: colors.color, + symbolColor: colors.symbolColor, + height: 36 + }) + } catch (err) { + // Titlebar overlay may not be enabled on all platforms + console.warn('Failed to set titlebar overlay:', err.message) + } +}) + +// ==================== Workspace IPC Handlers ==================== + +ipcMain.handle('workspace:selectFolder', async () => { + if (!mainWindow) return { canceled: true } + + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openDirectory', 'createDirectory'], + title: '选择笔记存储文件夹', + buttonLabel: '选择此文件夹' }) + + if (result.canceled || result.filePaths.length === 0) { + return { canceled: true } + } + + return { canceled: false, path: result.filePaths[0] } +}) + +ipcMain.handle('workspace:setPath', async (_, workspacePath) => { + console.log('[workspace:setPath] Setting workspace to:', workspacePath) + try { + // Check if this folder already has a Tunji workspace + const tunjiDir = join(workspacePath, '.tunji') + const isExistingWorkspace = existsSync(tunjiDir) + + // Close existing storage service if any + if (storageService) { + storageService.flush() + storageService.close() + storageService = null + workspaceReady = false + } + + // Save config + saveConfig({ workspacePath }) + console.log('[workspace:setPath] Config saved') + + // Initialize storage service with new path + storageService = new StorageService() + await storageService.init(workspacePath) + workspaceReady = true + console.log('[workspace:setPath] StorageService initialized, existing:', isExistingWorkspace) + + // Notify renderer + sendWorkspaceStatus() + + return { success: true, path: workspacePath, isExisting: isExistingWorkspace } + } catch (err) { + console.error('[workspace:setPath] Failed:', err) + workspaceReady = false + return { success: false, error: err.message } + } +}) + +ipcMain.handle('workspace:getStatus', () => { + const config = loadConfig() + return { + ready: workspaceReady, + path: config.workspacePath || null + } +}) + +ipcMain.handle('workspace:checkFolder', (_, folderPath) => { + const tunjiDir = join(folderPath, '.tunji') + const hasWorkspace = existsSync(tunjiDir) + return { hasWorkspace } }) // ==================== Register IPC Handlers ==================== @@ -225,15 +338,23 @@ function registerIpcHandlers() { // ==================== 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 + // Register IPC handlers first (workspace handlers are already registered above) registerIpcHandlers() - // Create window + // Check if workspace is already configured + const config = loadConfig() + if (config.workspacePath && existsSync(config.workspacePath)) { + try { + storageService = new StorageService() + await storageService.init(config.workspacePath) + workspaceReady = true + } catch (err) { + console.error('Failed to initialize storage:', err) + workspaceReady = false + } + } + + // Create window (will send workspace status on load) createWindow() }) diff --git a/src/main/indexpreload.js b/src/main/indexpreload.js index 18403ba..5de29ad 100644 --- a/src/main/indexpreload.js +++ b/src/main/indexpreload.js @@ -37,4 +37,15 @@ contextBridge.exposeInMainWorld('electronAPI', { // Stats getStats: () => ipcRenderer.invoke('stats:get'), + + // Workspace + workspace: { + selectFolder: () => ipcRenderer.invoke('workspace:selectFolder'), + setPath: (path) => ipcRenderer.invoke('workspace:setPath', path), + getStatus: () => ipcRenderer.invoke('workspace:getStatus'), + checkFolder: (path) => ipcRenderer.invoke('workspace:checkFolder', path), + onStatusChange: (callback) => { + ipcRenderer.on('workspace:status', (_, data) => callback(data)) + }, + }, }) diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 67bae42..fffe277 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -1,41 +1,33 @@