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 账号管理器
+
+
+
+
+
+
+
+
+
+
+
迁移账号到资料库
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 `
+
+
+
+ ${acc.username ? `
+
+ 账号
+ ${escapeHtml(acc.username)}
+
+ ` : ''}
+
+ ${acc.password ? `
+
+ 密码
+ ••••••••
+
+ ` : ''}
+
+ ${acc.email ? `
+
+ 邮箱
+ ${escapeHtml(acc.email)}
+
+ ` : ''}
+
+ ${acc.proxy ? `
+
+ 代理
+ ${escapeHtml(acc.proxy)}
+
+ ` : ''}
+
+ ${acc.totp_secret ? `
+
+ ` : ''}
+
+ `;
+ }).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