From 9269b8af772782f327217f3efe9a6f42a2791cbd Mon Sep 17 00:00:00 2001 From: TYt50 <106930118+TYt50@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:56:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BE=BF=E6=90=BA?= =?UTF-8?q?=E7=89=88=E5=B9=B6=E5=A2=9E=E5=8A=A0=E4=B8=80=E9=94=AE=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 16 + .gitignore | 24 + main.js | 107 ++++ package-lock.json | 888 ++++++++++++++++++++++++++++++ package.json | 23 + preload.js | 21 + src/db/database.js | 322 +++++++++++ src/index.html | 198 +++++++ src/renderer.js | 813 +++++++++++++++++++++++++++ src/styles.css | 1305 ++++++++++++++++++++++++++++++++++++++++++++ start.vbs | 3 + stop.bat | 24 + 一键导出账号.bat | 6 + 13 files changed, 3750 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 main.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 preload.js create mode 100644 src/db/database.js create mode 100644 src/index.html create mode 100644 src/renderer.js create mode 100644 src/styles.css create mode 100644 start.vbs create mode 100644 stop.bat create mode 100644 一键导出账号.bat diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c77fe31 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig helps maintain consistent coding styles +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +charset = utf-8 +trim_trailing_whitespace = false + +[*.{js,jsx,ts,tsx,json}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6afc3bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ + +# Database and user data (Portable data) +accounts.db +data/ +*.db-journal + +# Exports (Contains sensitive information) +accounts_export.txt +export.js + +# OS files +.DS_Store +Thumbs.db + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Production build (if you pack the app later) +dist/ +build/ diff --git a/main.js b/main.js new file mode 100644 index 0000000..43dfa19 --- /dev/null +++ b/main.js @@ -0,0 +1,107 @@ +const { app, BrowserWindow, ipcMain, clipboard, Menu } = require('electron'); +const path = require('path'); +const Database = require('./src/db/database'); + +// 强制设置用户数据存储位置为项目根目录下的 data 文件夹 +// 这样 Electron 的缓存、Localstorage、日志等都不会写到 C 盘 +app.setPath('userData', path.join(process.cwd(), 'data')); + +let mainWindow; +let db; + +function createWindow() { + // 移除应用菜单栏 + Menu.setApplicationMenu(null); + + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 900, + minHeight: 600, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false + }, + backgroundColor: '#0a0a0b', + titleBarStyle: 'default', + show: false + }); + + mainWindow.loadFile('src/index.html'); + + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + }); + + // 开发时打开DevTools + // mainWindow.webContents.openDevTools(); +} + +app.whenReady().then(() => { + db = new Database(); + createWindow(); + + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); +}); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// ==================== Vault IPC Handlers ==================== +ipcMain.handle('db:getVaults', async () => { + return db.getVaults(); +}); + +ipcMain.handle('db:addVault', async (event, vault) => { + return db.addVault(vault); +}); + +ipcMain.handle('db:updateVault', async (event, id, vault) => { + return db.updateVault(id, vault); +}); + +ipcMain.handle('db:deleteVault', async (event, id) => { + return db.deleteVault(id); +}); + +// ==================== Account IPC Handlers ==================== +ipcMain.handle('db:getAccounts', async (event, page, limit, vaultId) => { + return db.getAccounts(page, limit, vaultId); +}); + +ipcMain.handle('db:getAccountCount', async (event, vaultId) => { + return db.getAccountCount(vaultId); +}); + +ipcMain.handle('db:addAccount', async (event, account) => { + return db.addAccount(account); +}); + +ipcMain.handle('db:updateAccount', async (event, id, account) => { + return db.updateAccount(id, account); +}); + +ipcMain.handle('db:deleteAccount', async (event, id) => { + return db.deleteAccount(id); +}); + +ipcMain.handle('db:moveAccountToVault', async (event, accountId, vaultId) => { + return db.moveAccountToVault(accountId, vaultId); +}); + +ipcMain.handle('db:searchAccounts', async (event, query, vaultId) => { + return db.searchAccounts(query, vaultId); +}); + +ipcMain.handle('clipboard:write', async (event, text) => { + clipboard.writeText(text); + return true; +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5926005 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,888 @@ +{ + "name": "2fa-account-tool", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "2fa-account-tool", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "sql.js": "^1.10.0" + }, + "devDependencies": { + "electron": "^28.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron": { + "version": "28.3.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz", + "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..63f6a83 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "2fa-account-tool", + "version": "1.0.0", + "description": "本地2FA账号密码管理工具", + "main": "main.js", + "scripts": { + "start": "electron ." + }, + "keywords": [ + "2fa", + "password-manager", + "totp" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "electron": "^28.0.0" + }, + "dependencies": { + "crypto-js": "^4.2.0", + "sql.js": "^1.10.0" + } +} \ No newline at end of file diff --git a/preload.js b/preload.js new file mode 100644 index 0000000..a5cd302 --- /dev/null +++ b/preload.js @@ -0,0 +1,21 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +contextBridge.exposeInMainWorld('api', { + // Vault operations + getVaults: () => ipcRenderer.invoke('db:getVaults'), + addVault: (vault) => ipcRenderer.invoke('db:addVault', vault), + updateVault: (id, vault) => ipcRenderer.invoke('db:updateVault', id, vault), + deleteVault: (id) => ipcRenderer.invoke('db:deleteVault', id), + + // Account operations + getAccounts: (page, limit, vaultId) => ipcRenderer.invoke('db:getAccounts', page, limit, vaultId), + getAccountCount: (vaultId) => ipcRenderer.invoke('db:getAccountCount', vaultId), + addAccount: (account) => ipcRenderer.invoke('db:addAccount', account), + updateAccount: (id, account) => ipcRenderer.invoke('db:updateAccount', id, account), + deleteAccount: (id) => ipcRenderer.invoke('db:deleteAccount', id), + moveAccountToVault: (accountId, vaultId) => ipcRenderer.invoke('db:moveAccountToVault', accountId, vaultId), + searchAccounts: (query, vaultId) => ipcRenderer.invoke('db:searchAccounts', query, vaultId), + + // Clipboard + copyToClipboard: (text) => ipcRenderer.invoke('clipboard:write', text) +}); diff --git a/src/db/database.js b/src/db/database.js new file mode 100644 index 0000000..ceb1913 --- /dev/null +++ b/src/db/database.js @@ -0,0 +1,322 @@ +const initSqlJs = require('sql.js'); +const path = require('path'); +const fs = require('fs'); +const { app } = require('electron'); +const CryptoJS = require('crypto-js'); + +const ENCRYPTION_KEY = 'your-secret-key-2fa-manager-v1'; + +class AccountDatabase { + constructor() { + this.db = null; + this.dbPath = null; + this.ready = this.init(); + } + + async init() { + const SQL = await initSqlJs(); + + // const userDataPath = app.getPath('userData'); + // this.dbPath = path.join(userDataPath, 'accounts.db'); + + // 便携版:将数据库放在项目根目录下 + this.dbPath = path.join(process.cwd(), 'accounts.db'); + + // 尝试加载现有数据库 + if (fs.existsSync(this.dbPath)) { + const buffer = fs.readFileSync(this.dbPath); + this.db = new SQL.Database(buffer); + } else { + this.db = new SQL.Database(); + } + + // 创建资料库表 + this.db.run(` + CREATE TABLE IF NOT EXISTS vaults ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + icon TEXT DEFAULT '📁', + color TEXT DEFAULT '#3b82f6', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // 创建账号表 + this.db.run(` + CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + vault_id INTEGER DEFAULT NULL, + name TEXT NOT NULL, + username TEXT, + password TEXT, + totp_secret TEXT, + tags TEXT, + email TEXT, + proxy TEXT, + notes TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (vault_id) REFERENCES vaults(id) + ) + `); + + // 迁移: 添加 tags 和 vault_id 和 proxy 列 (如果不存在) + try { this.db.run(`ALTER TABLE accounts ADD COLUMN tags TEXT`); } catch (e) { } + try { this.db.run(`ALTER TABLE accounts ADD COLUMN vault_id INTEGER`); } catch (e) { } + try { this.db.run(`ALTER TABLE accounts ADD COLUMN proxy TEXT`); } catch (e) { } + + // 创建默认资料库 (如果不存在) + const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'"); + if (!defaultVault.length || !defaultVault[0].values.length) { + this.db.run("INSERT INTO vaults (name, icon) VALUES ('默认', '🏠')"); + } + + // 创建索引 + this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_name ON accounts(name)`); + this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)`); + this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_vault ON accounts(vault_id)`); + + this.save(); + return true; + } + + save() { + if (!this.db || !this.dbPath) return; + const data = this.db.export(); + const buffer = Buffer.from(data); + fs.writeFileSync(this.dbPath, buffer); + } + + encrypt(text) { + if (!text) return ''; + return CryptoJS.AES.encrypt(text, ENCRYPTION_KEY).toString(); + } + + decrypt(ciphertext) { + if (!ciphertext) return ''; + try { + const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY); + return bytes.toString(CryptoJS.enc.Utf8); + } catch (e) { + return ciphertext; + } + } + + // ==================== 资料库操作 ==================== + + async getVaults() { + await this.ready; + const result = this.db.exec(` + SELECT v.*, COUNT(a.id) as account_count + FROM vaults v + LEFT JOIN accounts a ON a.vault_id = v.id + GROUP BY v.id + ORDER BY v.created_at ASC + `); + + if (!result.length) return []; + + const columns = result[0].columns; + return result[0].values.map(row => { + const obj = {}; + columns.forEach((col, i) => obj[col] = row[i]); + return obj; + }); + } + + async addVault(vault) { + await this.ready; + this.db.run(`INSERT INTO vaults (name, icon, color) VALUES (?, ?, ?)`, [ + vault.name, + vault.icon || '📁', + vault.color || '#3b82f6' + ]); + this.save(); + + const result = this.db.exec('SELECT last_insert_rowid() as id'); + return { id: result[0]?.values[0][0], ...vault }; + } + + async updateVault(id, vault) { + await this.ready; + this.db.run(`UPDATE vaults SET name = ?, icon = ?, color = ? WHERE id = ?`, [ + vault.name, + vault.icon || '📁', + vault.color || '#3b82f6', + id + ]); + this.save(); + return { id, ...vault }; + } + + async deleteVault(id) { + await this.ready; + // 将该资料库中的账号移到默认资料库 + const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'"); + const defaultId = defaultVault[0]?.values[0]?.[0] || null; + + this.db.run('UPDATE accounts SET vault_id = ? WHERE vault_id = ?', [defaultId, id]); + this.db.run('DELETE FROM vaults WHERE id = ? AND name != ?', [id, '默认']); + this.save(); + return true; + } + + // ==================== 账号操作 ==================== + + async getAccounts(page = 1, limit = 10, vaultId = null) { + await this.ready; + const offset = (page - 1) * limit; + + let sql = `SELECT * FROM accounts`; + const params = []; + + if (vaultId !== null) { + sql += ` WHERE vault_id = ?`; + params.push(vaultId); + } + + sql += ` ORDER BY updated_at DESC LIMIT ? OFFSET ?`; + params.push(limit, offset); + + const stmt = this.db.prepare(sql); + stmt.bind(params); + + const accounts = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + accounts.push({ + ...row, + password: this.decrypt(row.password), + totp_secret: this.decrypt(row.totp_secret) + }); + } + stmt.free(); + + return accounts; + } + + async getAccountCount(vaultId = null) { + await this.ready; + let sql = 'SELECT COUNT(*) as count FROM accounts'; + const params = []; + + if (vaultId !== null) { + sql += ' WHERE vault_id = ?'; + params.push(vaultId); + } + + const stmt = this.db.prepare(sql); + stmt.bind(params); + stmt.step(); + const result = stmt.getAsObject(); + stmt.free(); + + return result.count || 0; + } + + async addAccount(account) { + await this.ready; + + this.db.run(` + INSERT INTO accounts (vault_id, name, username, password, totp_secret, tags, email, proxy, notes) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + account.vault_id || null, + account.name, + account.username, + this.encrypt(account.password), + this.encrypt(account.totp_secret), + account.tags || '', + account.email, + account.proxy || '', + account.notes + ]); + + this.save(); + + const result = this.db.exec('SELECT last_insert_rowid() as id'); + const id = result[0]?.values[0][0]; + + return { id, ...account }; + } + + async updateAccount(id, account) { + await this.ready; + + this.db.run(` + UPDATE accounts + SET vault_id = ?, name = ?, username = ?, password = ?, totp_secret = ?, + tags = ?, email = ?, proxy = ?, notes = ?, updated_at = datetime('now') + WHERE id = ? + `, [ + account.vault_id || null, + account.name, + account.username, + this.encrypt(account.password), + this.encrypt(account.totp_secret), + account.tags || '', + account.email, + account.proxy || '', + account.notes, + id + ]); + + this.save(); + return { id, ...account }; + } + + async moveAccountToVault(accountId, vaultId) { + await this.ready; + this.db.run('UPDATE accounts SET vault_id = ?, updated_at = datetime("now") WHERE id = ?', + [vaultId, accountId]); + this.save(); + return true; + } + + async deleteAccount(id) { + await this.ready; + this.db.run('DELETE FROM accounts WHERE id = ?', [id]); + this.save(); + return true; + } + + async searchAccounts(query, vaultId = null) { + await this.ready; + const searchTerm = `%${query}%`; + + let sql = `SELECT * FROM accounts WHERE (name LIKE ? OR username LIKE ? OR email LIKE ? OR tags LIKE ? OR proxy LIKE ?)`; + const params = [searchTerm, searchTerm, searchTerm, searchTerm, searchTerm]; + + if (vaultId !== null) { + sql += ` AND vault_id = ?`; + params.push(vaultId); + } + + sql += ` ORDER BY updated_at DESC LIMIT 50`; + + const stmt = this.db.prepare(sql); + stmt.bind(params); + + const accounts = []; + while (stmt.step()) { + const row = stmt.getAsObject(); + accounts.push({ + ...row, + password: this.decrypt(row.password), + totp_secret: this.decrypt(row.totp_secret) + }); + } + stmt.free(); + + return accounts; + } + + close() { + if (this.db) { + this.save(); + this.db.close(); + } + } +} + +module.exports = AccountDatabase; diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..5ff9a8f --- /dev/null +++ b/src/index.html @@ -0,0 +1,198 @@ + + + + + + + + 2FA 账号管理器 + + + + + +
+ +
+
+

账号列表

+ +
+ + +
+ +
+
+ 资料库 +
+ +
+
+ 全部 + 0 +
+
+ +
+
+ + +
+ + +
+ +
+
+ + +
+ + +
+ + + +
+

添加账号

+
+ + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

随机生成器

+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + + + + +
+
+ + + +
+
+
+
+
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/src/renderer.js b/src/renderer.js new file mode 100644 index 0000000..ed340e7 --- /dev/null +++ b/src/renderer.js @@ -0,0 +1,813 @@ +// TOTP 生成器 +class TOTP { + static async generate(secret) { + if (!secret) return null; + + try { + const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + let bits = ''; + const cleanSecret = secret.replace(/\s/g, '').toUpperCase(); + + for (let i = 0; i < cleanSecret.length; i++) { + const val = base32chars.indexOf(cleanSecret[i]); + if (val === -1) continue; + bits += val.toString(2).padStart(5, '0'); + } + + const bytes = new Uint8Array(Math.floor(bits.length / 8)); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(bits.substr(i * 8, 8), 2); + } + + const epoch = Math.floor(Date.now() / 1000); + const timeStep = Math.floor(epoch / 30); + + const timeBuffer = new ArrayBuffer(8); + const timeView = new DataView(timeBuffer); + timeView.setUint32(4, timeStep, false); + + const key = await crypto.subtle.importKey( + 'raw', bytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign'] + ); + + const signature = await crypto.subtle.sign('HMAC', key, timeBuffer); + const hmac = new Uint8Array(signature); + + const offset = hmac[hmac.length - 1] & 0xf; + const binary = ((hmac[offset] & 0x7f) << 24) | + ((hmac[offset + 1] & 0xff) << 16) | + ((hmac[offset + 2] & 0xff) << 8) | + (hmac[offset + 3] & 0xff); + + const otp = binary % 1000000; + return otp.toString().padStart(6, '0'); + } catch (e) { + console.error('TOTP generation error:', e); + return null; + } + } + + static getTimeRemaining() { + return 30 - (Math.floor(Date.now() / 1000) % 30); + } +} + +// 随机名称库 +const firstNames = [ + 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', + 'Thomas', 'Christopher', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', + 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Alex', 'Jordan', 'Taylor', + 'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Peyton', 'Cameron', 'Emma', + 'Oliver', 'Liam', 'Noah', 'Sophia', 'Ava', 'Isabella', 'Mia', 'Charlotte', + 'Benjamin', 'Alexander', 'Sebastian', 'Theodore', 'Victoria', 'Penelope' +]; + +const lastNames = [ + 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', + 'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', + 'Martin', 'Lee', 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson', + 'Walker', 'Young', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams', + 'Nelson', 'Mitchell', 'Campbell', 'Roberts', 'Carter', 'Phillips', 'Evans' +]; + +// 应用状态 +let accounts = []; +let vaults = []; +let currentPage = 1; +let totalPages = 1; +const itemsPerPage = 8; +let editingId = null; +let selectedVaultId = null; +let movingAccountId = null; +let totalAccountCount = 0; + +// DOM 元素 +const accountsList = document.getElementById('accountsList'); +const accountForm = document.getElementById('accountForm'); +const formTitle = document.getElementById('formTitle'); +const searchInput = document.getElementById('searchInput'); +const prevBtn = document.getElementById('prevBtn'); +const nextBtn = document.getElementById('nextBtn'); +const pageInfo = document.getElementById('pageInfo'); +const cancelBtn = document.getElementById('cancelBtn'); +const toast = document.getElementById('toast'); +const vaultList = document.getElementById('vaultList'); +const vaultFormSection = document.getElementById('vaultFormSection'); +const moveModal = document.getElementById('moveModal'); + +// 初始化 +document.addEventListener('DOMContentLoaded', () => { + loadVaults(); + loadAccounts(); + generateRandomName(); + generateRandomPassword(); + setupEventListeners(); + setInterval(updateAllTOTP, 1000); +}); + +function setupEventListeners() { + accountForm.addEventListener('submit', handleFormSubmit); + cancelBtn.addEventListener('click', resetForm); + + // 搜索 + let searchTimeout; + searchInput.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + if (e.target.value.trim()) { + searchAccounts(e.target.value.trim()); + } else { + loadAccounts(); + } + }, 300); + }); + + // 分页 + prevBtn.addEventListener('click', () => { + if (currentPage > 1) { currentPage--; loadAccounts(); } + }); + nextBtn.addEventListener('click', () => { + if (currentPage < totalPages) { currentPage++; loadAccounts(); } + }); + + // 随机生成器 + document.getElementById('rollName').addEventListener('click', generateRandomName); + document.getElementById('rollPassword').addEventListener('click', generateRandomPassword); + + document.querySelectorAll('.generator-section .copy-btn').forEach(btn => { + btn.addEventListener('click', () => { + const targetId = btn.dataset.target; + const value = document.getElementById(targetId).value; + copyToClipboard(value); + }); + }); + + document.getElementById('randomFullName').addEventListener('click', function () { + copyToClipboard(this.value); + }); + document.getElementById('randomUsername').addEventListener('click', function () { + copyToClipboard(this.value); + }); + + ['pwdUppercase', 'pwdLowercase', 'pwdNumbers', 'pwdSymbols', 'pwdLength'].forEach(id => { + document.getElementById(id).addEventListener('change', generateRandomPassword); + }); + + // 资料库表单 (保留用于兼容) + document.getElementById('cancelVaultBtn').addEventListener('click', () => { + vaultFormSection.style.display = 'none'; + }); + document.getElementById('saveVaultBtn').addEventListener('click', saveVault); + + // 迁移模态框 + document.getElementById('cancelMoveBtn').addEventListener('click', () => { + moveModal.style.display = 'none'; + movingAccountId = null; + }); + document.getElementById('confirmMoveBtn').addEventListener('click', confirmMoveAccount); +} + +// ==================== 资料库操作 ==================== + +async function loadVaults() { + try { + vaults = await window.api.getVaults(); + totalAccountCount = await window.api.getAccountCount(null); + renderVaultList(); + } catch (e) { + console.error('Failed to load vaults:', e); + } +} + +function renderVaultList() { + // 更新全部账号数量 + document.getElementById('allCount').textContent = totalAccountCount; + + // 更新全部账号选中状态 + const allVaultRow = document.querySelector('.vault-row.all-vault'); + if (allVaultRow) { + allVaultRow.classList.toggle('active', selectedVaultId === null); + } + + vaultList.innerHTML = vaults.map(v => ` +
+ ${escapeHtml(v.name)} + ${v.account_count || 0} + ${v.name !== '默认' ? ` + + ` : ''} +
+ `).join(''); + + // 绑定事件 + vaultList.querySelectorAll('.vault-row').forEach(row => { + const id = parseInt(row.dataset.id); + const vault = vaults.find(v => v.id === id); + if (!vault) return; + + let clickTimeout = null; + + row.addEventListener('click', (e) => { + if (row.classList.contains('editing')) return; + if (e.target.closest('.vault-delete')) { + e.stopPropagation(); + deleteVault(id); + return; + } + + // 延迟执行单击,以便检测是否是双击 + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + return; + } + clickTimeout = setTimeout(() => { + clickTimeout = null; + selectVault(id); + }, 200); + }); + + // 只有非默认资料库可以双击改名 + if (vault.name !== '默认') { + row.addEventListener('dblclick', (e) => { + e.stopPropagation(); + e.preventDefault(); + // 取消单击 + if (clickTimeout) { + clearTimeout(clickTimeout); + clickTimeout = null; + } + startEditVault(row, id); + }); + } + }); +} + +function selectVault(id) { + selectedVaultId = id; + currentPage = 1; + loadAccounts(); + renderVaultList(); +} + +// 双击资料库开始编辑 +function startEditVault(row, id) { + const vault = vaults.find(v => v.id === id); + if (!vault || vault.name === '默认') return; + + row.classList.add('editing'); + const nameSpan = row.querySelector('.vault-name'); + const originalName = vault.name; + + const input = document.createElement('input'); + input.type = 'text'; + input.className = 'vault-edit'; + input.value = originalName; + nameSpan.replaceWith(input); + + // 延迟聚焦以确保元素已渲染 + setTimeout(() => { + input.focus(); + input.select(); + }, 10); + + let isFinished = false; + async function finishEdit() { + if (isFinished) return; + isFinished = true; + + const newName = input.value.trim(); + if (newName && newName !== originalName) { + try { + await window.api.updateVault(id, { name: newName }); + showToast('资料库已更新', 'success'); + } catch (e) { + console.error('Update vault failed:', e); + showToast('更新失败', 'error'); + } + } + loadVaults(); + } + + input.addEventListener('blur', finishEdit); + input.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'Enter') { + input.blur(); + } else if (e.key === 'Escape') { + input.value = originalName; + input.blur(); + } + }); + + // 阻止输入框的点击事件冒泡 + input.addEventListener('click', (e) => { + e.stopPropagation(); + }); +} + +// 点击 + 新增资料库 (行内创建) +async function showAddVaultForm() { + // 检查是否已有输入框 + if (vaultList.querySelector('.vault-new-row')) return; + + // 创建新增行 + const editRow = document.createElement('div'); + editRow.className = 'vault-new-row'; + editRow.innerHTML = ` + + `; + + // 插入到 vaultList 末尾 + vaultList.appendChild(editRow); + + const input = editRow.querySelector('input'); + + // 延迟聚焦 + setTimeout(() => { + input.focus(); + }, 10); + + let isFinished = false; + async function finishCreate() { + if (isFinished) return; + isFinished = true; + + const name = input.value.trim(); + if (name) { + try { + await window.api.addVault({ name }); + showToast('资料库已创建', 'success'); + } catch (e) { + console.error('Add vault failed:', e); + showToast('创建失败', 'error'); + } + } + loadVaults(); + } + + input.addEventListener('blur', finishCreate); + input.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'Enter') { + input.blur(); + } else if (e.key === 'Escape') { + loadVaults(); + } + }); + + // 阻止点击事件冒泡 + input.addEventListener('click', (e) => { + e.stopPropagation(); + }); +} + +async function saveVault() { + const id = document.getElementById('vaultId').value; + const name = document.getElementById('vaultName').value.trim(); + + if (!name) { + showToast('请输入资料库名称', 'error'); + return; + } + + try { + if (id) { + await window.api.updateVault(parseInt(id), { name }); + showToast('资料库已更新', 'success'); + } else { + await window.api.addVault({ name }); + showToast('资料库已创建', 'success'); + } + vaultFormSection.style.display = 'none'; + loadVaults(); + } catch (e) { + console.error('Save vault failed:', e); + showToast('保存失败', 'error'); + } +} + +async function deleteVault(id) { + const vault = vaults.find(v => v.id === id); + if (!vault || vault.name === '默认') return; + + if (!confirm(`确定要删除资料库「${vault.name}」吗?\n其中的账号将被移到默认资料库。`)) return; + + try { + await window.api.deleteVault(id); + showToast('资料库已删除', 'success'); + if (selectedVaultId === id) { + selectedVaultId = null; + } + loadVaults(); + loadAccounts(); + } catch (e) { + console.error('Delete vault failed:', e); + showToast('删除失败', 'error'); + } +} + +function openMoveDialog(accountId) { + movingAccountId = accountId; + const select = document.getElementById('moveToVault'); + select.innerHTML = vaults.map(v => + `` + ).join(''); + moveModal.style.display = 'flex'; +} + +async function confirmMoveAccount() { + if (!movingAccountId) return; + + const newVaultId = parseInt(document.getElementById('moveToVault').value); + + try { + await window.api.moveAccountToVault(movingAccountId, newVaultId); + showToast('账号已迁移', 'success'); + moveModal.style.display = 'none'; + movingAccountId = null; + loadAccounts(); + loadVaults(); + } catch (e) { + console.error('Move failed:', e); + showToast('迁移失败', 'error'); + } +} + +// ==================== 账号操作 ==================== + +async function loadAccounts() { + try { + const count = await window.api.getAccountCount(selectedVaultId); + totalPages = Math.max(1, Math.ceil(count / itemsPerPage)); + + if (currentPage > totalPages) { + currentPage = totalPages; + } + + accounts = await window.api.getAccounts(currentPage, itemsPerPage, selectedVaultId); + renderAccounts(); + updatePagination(); + } catch (e) { + console.error('Failed to load accounts:', e); + showToast('加载账号失败', 'error'); + } +} + +async function searchAccounts(query) { + try { + accounts = await window.api.searchAccounts(query, selectedVaultId); + currentPage = 1; + totalPages = 1; + renderAccounts(); + updatePagination(); + } catch (e) { + console.error('Search failed:', e); + } +} + +function renderAccounts() { + if (accounts.length === 0) { + accountsList.innerHTML = ` +
+
+
暂无账号
在右侧添加你的第一个账号
+
+ `; + return; + } + + accountsList.innerHTML = accounts.map(acc => { + const tags = acc.tags ? acc.tags.split(',').map(t => t.trim()).filter(t => t) : []; + const tagsHtml = tags.length > 0 + ? `
${tags.map(t => `${escapeHtml(t)}`).join('')}
` + : ''; + + return ` +
+
+ ${tagsHtml || '无标签'} +
+ + + +
+
+ + ${acc.username ? ` +
+ 账号 + ${escapeHtml(acc.username)} +
+ ` : ''} + + ${acc.password ? ` +
+ 密码 + •••••••• +
+ ` : ''} + + ${acc.email ? ` +
+ 邮箱 + ${escapeHtml(acc.email)} +
+ ` : ''} + + ${acc.proxy ? ` +
+ 代理 + ${escapeHtml(acc.proxy)} +
+ ` : ''} + + ${acc.totp_secret ? ` +
+
+ 点击显示 2FA +
+ +
+ ` : ''} +
+ `; + }).join(''); + + // 不再自动更新TOTP,改为点击时才显示 +} + +// 保存已激活的TOTP账号列表 +const activeTOTPs = new Map(); + +// 显示TOTP +async function showTOTP(id, secret) { + const container = document.getElementById(`totp-container-${id}`); + const visible = document.getElementById(`totp-visible-${id}`); + + if (!container || !visible) return; + + container.style.display = 'none'; + visible.style.display = 'flex'; + + // 保存到激活列表 + activeTOTPs.set(id, secret); + + // 立即更新一次 + await updateTOTP(id, secret); +} + +async function copyValue(element, value) { + try { + await window.api.copyToClipboard(value); + element.classList.add('copied'); + showToast('已复制', 'success'); + setTimeout(() => { element.classList.remove('copied'); }, 300); + } catch (e) { + console.error('Copy failed:', e); + } +} + +async function updateTOTP(id, secret) { + const codeEl = document.getElementById(`totp-${id}`); + const timerEl = document.getElementById(`timer-${id}`); + + if (!codeEl || !timerEl) return; + + const code = await TOTP.generate(secret); + if (code) { + codeEl.textContent = code.substring(0, 3) + ' ' + code.substring(3); + } + + const remaining = TOTP.getTimeRemaining(); + const progress = (remaining / 30) * 88; + const progressCircle = timerEl.querySelector('.progress'); + progressCircle.style.strokeDashoffset = 88 - progress; + + timerEl.classList.remove('warning', 'danger'); + if (remaining <= 5) { + timerEl.classList.add('danger'); + } else if (remaining <= 10) { + timerEl.classList.add('warning'); + } +} + +function updateAllTOTP() { + // 只更新已激活显示的TOTP + activeTOTPs.forEach((secret, id) => { + updateTOTP(id, secret); + }); +} + +async function copyTOTP(id) { + const acc = accounts.find(a => a.id === id); + if (!acc || !acc.totp_secret) return; + + const code = await TOTP.generate(acc.totp_secret); + if (code) { + const codeEl = document.getElementById(`totp-${id}`); + codeEl.classList.add('copied'); + await window.api.copyToClipboard(code); + showToast('已复制', 'success'); + setTimeout(() => { codeEl.classList.remove('copied'); }, 300); + } +} + +function updatePagination() { + pageInfo.textContent = `${currentPage} / ${totalPages}`; + prevBtn.disabled = currentPage <= 1; + nextBtn.disabled = currentPage >= totalPages; +} + +async function handleFormSubmit(e) { + e.preventDefault(); + + const username = document.getElementById('username').value.trim(); + const tags = document.getElementById('tags').value.trim(); + + // 使用标签的第一个作为name,如果没有标签则使用用户名 + const firstTag = tags ? tags.split(',')[0].trim() : ''; + const autoName = firstTag || username || '未命名'; + + const account = { + vault_id: selectedVaultId, + name: autoName, + username: username, + password: document.getElementById('password').value, + totp_secret: document.getElementById('totpSecret').value.trim(), + tags: tags, + email: document.getElementById('email').value.trim(), + proxy: document.getElementById('proxy').value.trim(), + notes: document.getElementById('notes').value.trim() + }; + + try { + if (editingId) { + const existingAcc = accounts.find(a => a.id === editingId); + if (existingAcc) { + account.vault_id = existingAcc.vault_id; + } + await window.api.updateAccount(editingId, account); + showToast('账号已更新', 'success'); + } else { + await window.api.addAccount(account); + showToast('账号已添加', 'success'); + } + + resetForm(); + loadAccounts(); + loadVaults(); + } catch (e) { + console.error('Save failed:', e); + showToast('保存失败', 'error'); + } +} + +function editAccount(id) { + const acc = accounts.find(a => a.id === id); + if (!acc) return; + + editingId = id; + formTitle.innerHTML = ' 编辑账号'; + + document.getElementById('accountId').value = id; + document.getElementById('username').value = acc.username || ''; + document.getElementById('password').value = acc.password || ''; + document.getElementById('totpSecret').value = acc.totp_secret || ''; + document.getElementById('email').value = acc.email || ''; + document.getElementById('proxy').value = acc.proxy || ''; + document.getElementById('tags').value = acc.tags || ''; + document.getElementById('notes').value = acc.notes || ''; + + document.querySelectorAll('.account-card').forEach(card => { + card.classList.toggle('active', card.dataset.id == id); + }); + + document.getElementById('username').focus(); +} + +async function deleteAccount(id) { + if (!confirm('确定要删除这个账号吗?')) return; + + try { + await window.api.deleteAccount(id); + showToast('账号已删除', 'success'); + + if (editingId === id) { + resetForm(); + } + + loadAccounts(); + loadVaults(); + } catch (e) { + console.error('Delete failed:', e); + showToast('删除失败', 'error'); + } +} + +function resetForm() { + editingId = null; + formTitle.innerHTML = ' 添加账号'; + accountForm.reset(); + document.getElementById('accountId').value = ''; + + document.querySelectorAll('.account-card').forEach(card => { + card.classList.remove('active'); + }); +} + +async function copyToClipboard(text) { + try { + await window.api.copyToClipboard(text); + showToast('已复制', 'success'); + } catch (e) { + console.error('Copy failed:', e); + } +} + +// 生成随机名称 (不含符号的长用户名) +function generateRandomName() { + const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; + const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; + const suffix = Math.floor(Math.random() * 10000); + const year = 1990 + Math.floor(Math.random() * 30); + + document.getElementById('randomFullName').value = `${firstName} ${lastName}`; + + // 不含下划线和符号的用户名格式 + const usernameFormats = [ + `${firstName.toLowerCase()}${lastName.toLowerCase()}${suffix}`, + `${firstName.toLowerCase()}${lastName.toLowerCase()}${year}`, + `${firstName.toLowerCase()}${lastName.toLowerCase()}`, + `${firstName[0].toLowerCase()}${lastName.toLowerCase()}${suffix}`, + `${lastName.toLowerCase()}${firstName.toLowerCase()}${suffix}`, + `${firstName.toLowerCase()}${lastName.toLowerCase()}official`, + `real${firstName.toLowerCase()}${lastName.toLowerCase()}` + ]; + const username = usernameFormats[Math.floor(Math.random() * usernameFormats.length)]; + document.getElementById('randomUsername').value = username; +} + +function generateRandomPassword() { + const length = parseInt(document.getElementById('pwdLength').value) || 16; + const useUppercase = document.getElementById('pwdUppercase').checked; + const useLowercase = document.getElementById('pwdLowercase').checked; + const useNumbers = document.getElementById('pwdNumbers').checked; + const useSymbols = document.getElementById('pwdSymbols').checked; + + let chars = ''; + if (useUppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + if (useLowercase) chars += 'abcdefghijklmnopqrstuvwxyz'; + if (useNumbers) chars += '0123456789'; + if (useSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; + + if (!chars) chars = 'abcdefghijklmnopqrstuvwxyz'; + + let password = ''; + const array = new Uint32Array(length); + crypto.getRandomValues(array); + + for (let i = 0; i < length; i++) { + password += chars[array[i] % chars.length]; + } + + document.getElementById('randomPassword').value = password; +} + +function showToast(message, type = 'info') { + toast.textContent = message; + toast.className = 'toast show'; + if (type === 'success') toast.classList.add('success'); + setTimeout(() => { toast.classList.remove('show'); }, 2000); +} + +function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function escapeJs(text) { + if (!text) return ''; + return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..bb5cce4 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,1305 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg-primary: #0a0a0b; + --bg-secondary: #111113; + --bg-tertiary: #18181b; + --bg-hover: #1f1f23; + --border-color: #27272a; + --border-subtle: #1f1f23; + --text-primary: #fafafa; + --text-secondary: #a1a1aa; + --text-muted: #71717a; + --accent: #3b82f6; + --accent-hover: #2563eb; + --success: #22c55e; + --danger: #ef4444; + --danger-hover: #dc2626; + --totp-progress: #3b82f6; + --card-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +/* 全局图标样式提升可见性 */ +i[class*="fa-"] { + color: var(--text-secondary); + transition: color 0.2s, transform 0.2s; + display: inline-block; + /* 允许 transform */ +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + line-height: 1.5; + overflow: hidden; +} + +.app-container { + display: flex; + height: 100vh; +} + +/* 左侧面板 */ +.list-panel { + flex: 1; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border-color); + min-width: 0; +} + +.panel-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); +} + +.panel-header h1 { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.02em; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.panel-header h1 i { + color: var(--accent); +} + +.search-box { + position: relative; +} + +.search-box .search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + font-size: 12px; +} + +.search-box input { + width: 100%; + padding: 10px 14px 10px 36px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-box input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.search-box input::placeholder { + color: var(--text-muted); +} + +/* 内容区域:资料库 + 账号卡片 */ +.content-area { + flex: 1; + display: flex; + overflow: hidden; +} + +/* 资料库侧边栏 */ +.vault-sidebar { + width: 140px; + min-width: 140px; + background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(15, 23, 42, 0.95) 100%); + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; +} + +.vault-sidebar-header { + padding: 12px 14px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.vault-sidebar-header i { + font-size: 10px; + color: var(--accent); +} + +.vault-items { + flex: 1; + padding: 8px; + display: flex; + flex-direction: column; + gap: 4px; + overflow-y: auto; +} + +.vault-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + position: relative; +} + +.vault-row:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary); +} + +.vault-row.active { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(59, 130, 246, 0.1) 100%); + color: var(--accent); + box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.3); +} + +.vault-row.active i { + color: var(--accent); +} + +.vault-row i { + width: 16px; + text-align: center; + font-size: 12px; + opacity: 0.8; +} + +.vault-row .vault-name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vault-count { + font-size: 10px; + padding: 2px 7px; + background: rgba(255, 255, 255, 0.08); + border-radius: 10px; + color: var(--text-muted); + font-weight: 600; +} + +.vault-row.active .vault-count { + background: rgba(59, 130, 246, 0.25); + color: var(--accent); +} + +/* 删除按钮 */ +.vault-row .vault-delete { + position: absolute; + right: 8px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-muted); + font-size: 9px; + cursor: pointer; + opacity: 0; + transition: all 0.15s; +} + +.vault-row:hover .vault-delete { + opacity: 1; +} + +.vault-row .vault-delete:hover { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +/* 新增资料库按钮 */ +.vault-add-btn { + margin: 8px; + margin-top: auto; + padding: 10px; + border-radius: 8px; + border: 1px dashed var(--border-color); + background: transparent; + color: var(--text-muted); + font-size: 11px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + cursor: pointer; + transition: all 0.2s; +} + +.vault-add-btn:hover { + border-color: var(--accent); + color: var(--accent); + background: rgba(59, 130, 246, 0.05); +} + +.vault-add-btn i { + font-size: 10px; +} + +/* 行内编辑输入框 */ +.vault-row input.vault-edit { + flex: 1; + min-width: 0; + max-width: 70px; + padding: 4px 6px; + background: var(--bg-tertiary); + border: 1px solid var(--accent); + border-radius: 5px; + color: var(--text-primary); + font-size: 11px; + font-weight: 500; + outline: none; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); + transition: all 0.2s; +} + +.vault-row input.vault-edit::placeholder { + color: var(--text-muted); + font-weight: 400; +} + +/* 新增资料库专用行 */ +.vault-new-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 8px; + background: rgba(59, 130, 246, 0.08); + border: 1px solid rgba(59, 130, 246, 0.2); + margin-top: 4px; + width: 100%; + box-sizing: border-box; +} + +.vault-new-row i { + color: var(--accent); + font-size: 11px; + width: 14px; + text-align: center; + flex-shrink: 0; +} + +.vault-new-row input { + flex: 1; + min-width: 0; + padding: 5px 8px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 5px; + color: var(--text-primary); + font-size: 11px; + font-weight: 500; + outline: none; + transition: all 0.2s; +} + +.vault-new-row input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); +} + +.vault-new-row input::placeholder { + color: var(--text-muted); + font-weight: 400; + font-size: 10px; +} + +.panel-header h1 { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.02em; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 8px; +} + +.panel-header h1 i { + color: var(--accent); +} + +.search-box { + position: relative; + margin-bottom: 12px; +} + +.search-box .search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 12px; +} + +.search-box input { + width: 100%; + padding: 10px 14px 10px 36px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.search-box input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.search-box input::placeholder { + color: var(--text-muted); +} + +/* 资料库侧边栏 */ +.vault-sidebar { + display: flex; + flex-direction: column; + gap: 2px; +} + +.vault-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s; + color: var(--text-secondary); + font-size: 13px; +} + +.vault-row:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.vault-row.active { + background: rgba(59, 130, 246, 0.15); + color: var(--accent); +} + +.vault-row.active i { + color: var(--accent); +} + +.vault-row i { + width: 16px; + text-align: center; + font-size: 12px; +} + +.vault-row span:first-of-type { + flex: 1; +} + +.vault-count { + font-size: 11px; + padding: 2px 8px; + background: var(--bg-tertiary); + border-radius: 10px; + color: var(--text-muted); +} + +.vault-row.add-vault { + border: 1px dashed var(--border-color); + margin-top: 4px; +} + +.vault-row.add-vault:hover { + border-color: var(--accent); + color: var(--accent); +} + +.vault-row .vault-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.15s; +} + +.vault-row:hover .vault-actions { + opacity: 1; +} + +.vault-action-btn { + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-secondary); + font-size: 10px; + cursor: pointer; + transition: all 0.15s; +} + +.vault-action-btn:hover { + background: var(--bg-primary); + color: var(--text-primary); +} + +.vault-action-btn.delete:hover { + color: var(--danger); +} + +/* 账号列表 */ +.accounts-list { + flex: 1; + overflow-y: auto; + padding: 12px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + align-content: start; +} + +.accounts-list::-webkit-scrollbar { + width: 6px; +} + +.accounts-list::-webkit-scrollbar-track { + background: transparent; +} + +.accounts-list::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +.accounts-list::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} + +/* 账号卡片 */ +.account-card { + background: var(--bg-secondary); + border: 1px solid var(--border-subtle); + border-radius: 10px; + padding: 12px; + transition: border-color 0.2s; + min-height: 180px; + max-height: 280px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.account-card:hover { + border-color: var(--border-color); +} + +.account-card.active { + border-color: var(--accent); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; +} + +.card-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 140px; +} + +.card-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.card-header .card-tags { + margin-bottom: 0; + flex: 1; +} + +.card-name-placeholder { + font-size: 12px; + color: var(--text-muted); + font-style: italic; +} + +.tag { + font-size: 10px; + padding: 2px 6px; + background: var(--bg-tertiary); + border-radius: 4px; + color: var(--text-muted); +} + +.card-actions { + display: flex; + gap: 4px; +} + +.card-action-btn { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 6px; + color: var(--text-secondary); + cursor: pointer; + transition: background 0.15s, color 0.15s; + font-size: 14px; +} + +.card-action-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.card-action-btn.delete:hover { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +/* 信息行 - 点击复制 */ +.info-row { + display: flex; + align-items: center; + padding: 4px 0; + gap: 6px; + overflow: hidden; + max-width: 100%; +} + +.info-label { + font-size: 11px; + color: var(--text-muted); + width: 32px; + flex-shrink: 0; +} + +.info-value { + flex: 1; + font-size: 12px; + color: var(--text-secondary); + font-family: 'SF Mono', 'Fira Code', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 3px 6px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; + max-width: calc(100% - 40px); + min-width: 0; +} + +.info-value:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.info-value:active { + background: var(--accent); + color: white; +} + +.info-value.password { + letter-spacing: 1px; +} + +.info-value.copied { + background: var(--success) !important; + color: white !important; +} + +/* TOTP 显示 */ +.totp-section { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-subtle); +} + +.totp-display { + display: flex; + align-items: center; + gap: 8px; +} + +.totp-code { + font-size: 18px; + font-weight: 700; + font-family: 'SF Mono', 'Fira Code', monospace; + letter-spacing: 2px; + color: var(--text-primary); + padding: 3px 6px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; +} + +.totp-code:hover { + background: var(--bg-tertiary); +} + +.totp-code:active, +.totp-code.copied { + background: var(--success); + color: white; +} + +.totp-timer { + width: 24px; + height: 24px; + position: relative; + flex-shrink: 0; +} + +.totp-timer svg { + transform: rotate(-90deg); +} + +.totp-timer circle { + fill: none; + stroke-width: 3; +} + +.totp-timer .bg { + stroke: var(--bg-tertiary); +} + +.totp-timer .progress { + stroke: var(--totp-progress); + stroke-linecap: round; + transition: stroke-dashoffset 1s linear; +} + +.totp-timer.warning .progress { + stroke: #f59e0b; +} + +.totp-timer.danger .progress { + stroke: var(--danger); +} + +/* TOTP 点击显示样式 */ +.totp-display.totp-hidden { + cursor: pointer; + padding: 8px 12px; + background: var(--bg-tertiary); + border-radius: 6px; + transition: all 0.2s; +} + +.totp-display.totp-hidden:hover { + background: var(--bg-hover); +} + +.totp-placeholder { + font-size: 12px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 6px; +} + +.totp-placeholder i { + font-size: 14px; + color: var(--accent); +} + +/* 分页 */ +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 16px; + border-top: 1px solid var(--border-color); +} + +.page-btn { + padding: 8px 16px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.page-btn:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); +} + +.page-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.page-info { + font-size: 13px; + color: var(--text-muted); +} + +/* 右侧表单面板 */ +.form-panel { + width: 320px; + background: var(--bg-secondary); + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.form-section { + padding: 14px 16px; + border-bottom: 1px solid var(--border-color); +} + +.form-section h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + letter-spacing: -0.02em; +} + +.form-group { + margin-bottom: 10px; +} + +.form-group label { + display: block; + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.form-group input, +.form-group textarea { + width: 100%; + padding: 8px 10px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 13px; + transition: border-color 0.2s, box-shadow 0.2s; + resize: none; +} + +.form-group input:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +.form-group input::placeholder, +.form-group textarea::placeholder { + color: var(--text-muted); +} + +.form-actions { + display: flex; + gap: 10px; + margin-top: 14px; +} + +.btn-primary { + flex: 1; + padding: 8px 16px; + background: var(--accent); + border: none; + border-radius: 6px; + color: white; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-secondary { + flex: 1; + padding: 8px 16px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.btn-secondary:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +/* 生成器区域 */ +.generator-section { + padding: 14px 16px; +} + +.generator-section h3 { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 10px; + letter-spacing: -0.02em; +} + +.generator-item { + margin-bottom: 10px; +} + +.generator-item>label { + display: block; + font-size: 11px; + color: var(--text-muted); + margin-bottom: 4px; +} + +.generator-row { + display: flex; + gap: 6px; +} + +.generator-row input { + flex: 1; + padding: 8px 10px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 12px; + font-family: 'SF Mono', 'Fira Code', monospace; +} + +.btn-icon { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text-primary); + font-size: 14px; + cursor: pointer; + transition: all 0.15s; +} + +.btn-icon i.fa-dice { + color: var(--accent); +} + +.btn-icon:hover i.fa-dice { + transform: rotate(15deg); +} + +.btn-icon:hover { + background: var(--bg-hover); + border-color: var(--text-muted); +} + +.password-options { + display: flex; + flex-wrap: wrap; + gap: 6px 10px; + margin-bottom: 6px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + width: 14px; + height: 14px; + accent-color: var(--accent); +} + +.checkbox-label input[type="number"] { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + padding: 2px 6px; + font-size: 12px; +} + +/* Toast 提示 */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(100px); + padding: 12px 24px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + opacity: 0; + transition: transform 0.3s, opacity 0.3s; + z-index: 1000; +} + +.toast.show { + transform: translateX(-50%) translateY(0); + opacity: 1; +} + +.toast.success { + border-color: var(--success); + background: rgba(34, 197, 94, 0.1); +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--text-muted); + text-align: center; + padding: 40px; + grid-column: 1 / -1; +} + +.empty-state-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.empty-state-text { + font-size: 14px; +} + +/* 响应式滚动条 */ +.form-panel::-webkit-scrollbar { + width: 6px; +} + +.form-panel::-webkit-scrollbar-track { + background: transparent; +} + +.form-panel::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +/* ==================== 资料库区域 ==================== */ +.vault-section { + padding: 16px; + border-bottom: 1px solid var(--border-color); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.section-header h3 { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); +} + +.btn-small { + padding: 4px 10px; + background: var(--accent); + border: none; + border-radius: 4px; + color: white; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.btn-small:hover { + background: var(--accent-hover); +} + +.btn-small-secondary { + padding: 4px 10px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-secondary); + font-size: 12px; + cursor: pointer; + transition: all 0.15s; +} + +.btn-small-secondary:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.vault-list { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 150px; + overflow-y: auto; +} + +.vault-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--bg-tertiary); + border: 1px solid var(--border-subtle); + border-radius: 6px; + cursor: pointer; + transition: all 0.15s; +} + +.vault-item:hover { + border-color: var(--border-color); +} + +.vault-item.active { + border-color: var(--accent); + background: rgba(59, 130, 246, 0.1); +} + +.vault-item-icon { + font-size: 14px; +} + +.vault-item-name { + flex: 1; + font-size: 12px; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vault-item-count { + font-size: 10px; + color: var(--text-muted); + padding: 2px 6px; + background: var(--bg-primary); + border-radius: 10px; +} + +.vault-item-actions { + display: flex; + gap: 2px; + opacity: 0; + transition: opacity 0.15s; +} + +.vault-item:hover .vault-item-actions { + opacity: 1; +} + +.vault-action-btn { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 4px; + color: var(--text-secondary); + font-size: 10px; + cursor: pointer; + transition: all 0.15s; +} + +.vault-action-btn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.vault-action-btn.delete:hover { + color: var(--danger); +} + +.vault-form { + margin-top: 10px; + padding: 10px; + background: var(--bg-tertiary); + border-radius: 6px; +} + +.vault-form-row { + display: flex; + gap: 8px; + margin-bottom: 8px; +} + +.vault-form-row input { + flex: 1; + padding: 6px 10px; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + color: var(--text-primary); + font-size: 12px; +} + +.vault-form-row input:focus { + outline: none; + border-color: var(--accent); +} + +.vault-form-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +/* ==================== 迁移按钮 ==================== */ +.card-action-btn.move { + font-size: 12px; +} + +.card-action-btn.move:hover { + background: rgba(59, 130, 246, 0.15); + color: var(--accent); +} + +/* ==================== 模态框 ==================== */ +.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 24px; + min-width: 300px; +} + +.modal-content h3 { + font-size: 16px; + margin-bottom: 16px; +} + +.modal-content select { + width: 100%; + padding: 10px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 14px; + margin-bottom: 16px; +} + +.modal-content select:focus { + outline: none; + border-color: var(--accent); +} + +.modal-actions { + display: flex; + gap: 12px; + justify-content: flex-end; +} + +/* ==================== 资料库表单区域 ==================== */ +.vault-form-section { + padding: 20px; + border-bottom: 1px solid var(--border-color); + background: var(--bg-secondary); +} + +.vault-form-section h3 { + font-size: 15px; + font-weight: 600; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.vault-form-section h3 i { + color: var(--accent); +} + +.modal-content h3 i, +.form-section h2 i, +.generator-section h3 i { + color: var(--accent); +} + +.form-section h2, +.generator-section h3 { + display: flex; + align-items: center; + gap: 8px; +} \ No newline at end of file diff --git a/start.vbs b/start.vbs new file mode 100644 index 0000000..4967034 --- /dev/null +++ b/start.vbs @@ -0,0 +1,3 @@ +Set WshShell = CreateObject("WScript.Shell") +WshShell.CurrentDirectory = CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName) +WshShell.Run "cmd /c npm start", 0, False diff --git a/stop.bat b/stop.bat new file mode 100644 index 0000000..35a6100 --- /dev/null +++ b/stop.bat @@ -0,0 +1,24 @@ +@echo off +echo 正在关闭 2FA Account Tool... +echo. + +:: 终止所有 electron 相关进程 +taskkill /f /im electron.exe >nul 2>&1 +if %errorlevel% equ 0 ( + echo [已关闭] electron.exe +) else ( + echo [未运行] electron.exe +) + +:: 可能有残留的 node 进程 +taskkill /f /im node.exe >nul 2>&1 +if %errorlevel% equ 0 ( + echo [已关闭] node.exe +) else ( + echo [未运行] node.exe +) + +echo. +echo 程序已彻底关闭,现在可以安全删除 node_modules 文件夹了。 +echo. +pause diff --git a/一键导出账号.bat b/一键导出账号.bat new file mode 100644 index 0000000..4890b01 --- /dev/null +++ b/一键导出账号.bat @@ -0,0 +1,6 @@ +@echo off +echo 正在准备导出账号数据... +node export.js +echo. +echo 导出完成!按任意键退出。 +pause > nul