chore: configure eslint and update gitignore
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -24,3 +24,10 @@ yarn-error.log*
|
|||||||
# Production build (if you pack the app later)
|
# Production build (if you pack the app later)
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# Linting
|
||||||
|
lint_output.txt
|
||||||
|
final_lint.txt
|
||||||
|
*.log
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
|||||||
22
eslint.config.mjs
Normal file
22
eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
...globals.es2021
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-undef": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
15
export.bat
Normal file
15
export.bat
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
@echo off
|
||||||
|
echo ==========================================
|
||||||
|
echo Exporting Account Data...
|
||||||
|
echo ==========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
node export.js
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ==========================================
|
||||||
|
echo EXPORT COMPLETE!
|
||||||
|
echo Check file: accounts_export.txt
|
||||||
|
echo ==========================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
107
main.js
107
main.js
@@ -1,16 +1,74 @@
|
|||||||
const { app, BrowserWindow, ipcMain, clipboard, Menu } = require('electron');
|
const { app, BrowserWindow, ipcMain, clipboard, Menu, desktopCapturer, screen } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const Database = require('./src/db/database');
|
const Database = require('./src/db/database');
|
||||||
|
|
||||||
// 强制设置用户数据存储位置为项目根目录下的 data 文件夹
|
// Force userData path to project root/data folder
|
||||||
// 这样 Electron 的缓存、Localstorage、日志等都不会写到 C 盘
|
// Prevents Electron cache/localStorage/logs from writing to C drive
|
||||||
app.setPath('userData', path.join(process.cwd(), 'data'));
|
app.setPath('userData', path.join(process.cwd(), 'data'));
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
let db;
|
let db;
|
||||||
|
let backupInterval;
|
||||||
|
|
||||||
|
// Auto backup feature
|
||||||
|
function startAutoBackup() {
|
||||||
|
const backupDir = path.join(process.cwd(), 'DB');
|
||||||
|
const dbPath = path.join(process.cwd(), 'accounts.db');
|
||||||
|
|
||||||
|
// Ensure backup directory exists
|
||||||
|
if (!fs.existsSync(backupDir)) {
|
||||||
|
fs.mkdirSync(backupDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup every 30 minutes
|
||||||
|
backupInterval = setInterval(() => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.getFullYear().toString() +
|
||||||
|
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||||||
|
now.getDate().toString().padStart(2, '0') + '_' +
|
||||||
|
now.getHours().toString().padStart(2, '0') +
|
||||||
|
now.getMinutes().toString().padStart(2, '0') +
|
||||||
|
now.getSeconds().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
const backupPath = path.join(backupDir, `accounts_${timestamp}.db`);
|
||||||
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
console.log(`[Backup] Auto backup success: ${backupPath}`);
|
||||||
|
|
||||||
|
// Clean old backups, keep last 10
|
||||||
|
cleanOldBackups(backupDir, 10);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Backup] Backup failed:', e);
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000); // 30 minutes
|
||||||
|
|
||||||
|
console.log('[Backup] Auto backup started, interval 30 minutes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean old backups
|
||||||
|
function cleanOldBackups(backupDir, keepCount) {
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(backupDir)
|
||||||
|
.filter(f => f.startsWith('accounts_') && f.endsWith('.db'))
|
||||||
|
.map(f => ({ name: f, path: path.join(backupDir, f), time: fs.statSync(path.join(backupDir, f)).mtime }))
|
||||||
|
.sort((a, b) => b.time - a.time);
|
||||||
|
|
||||||
|
if (files.length > keepCount) {
|
||||||
|
files.slice(keepCount).forEach(f => {
|
||||||
|
fs.unlinkSync(f.path);
|
||||||
|
console.log(`[Backup] Deleted old backup: ${f.name}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Backup] Clean old backups failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// 移除应用菜单栏
|
// Remove application menu bar
|
||||||
Menu.setApplicationMenu(null);
|
Menu.setApplicationMenu(null);
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@@ -21,7 +79,8 @@ function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false
|
nodeIntegration: false,
|
||||||
|
sandbox: false
|
||||||
},
|
},
|
||||||
backgroundColor: '#0a0a0b',
|
backgroundColor: '#0a0a0b',
|
||||||
titleBarStyle: 'default',
|
titleBarStyle: 'default',
|
||||||
@@ -34,13 +93,16 @@ function createWindow() {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 开发时打开DevTools
|
// DevTools: opt-in only (prevents auto-opening on startup)
|
||||||
// mainWindow.webContents.openDevTools();
|
if (process.env.ELECTRON_DEVTOOLS === '1') {
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
db = new Database();
|
db = new Database();
|
||||||
createWindow();
|
createWindow();
|
||||||
|
startAutoBackup();
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
@@ -50,6 +112,9 @@ app.whenReady().then(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
|
if (backupInterval) {
|
||||||
|
clearInterval(backupInterval);
|
||||||
|
}
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
@@ -105,3 +170,31 @@ ipcMain.handle('clipboard:write', async (event, text) => {
|
|||||||
clipboard.writeText(text);
|
clipboard.writeText(text);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Screen capture feature
|
||||||
|
ipcMain.handle('screen:capture', async () => {
|
||||||
|
try {
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
|
const { width, height } = primaryDisplay.size;
|
||||||
|
const scaleFactor = primaryDisplay.scaleFactor;
|
||||||
|
|
||||||
|
const sources = await desktopCapturer.getSources({
|
||||||
|
types: ['screen'],
|
||||||
|
thumbnailSize: { width: width * scaleFactor, height: height * scaleFactor }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sources.length > 0) {
|
||||||
|
const thumbnail = sources[0].thumbnail;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
image: thumbnail.toDataURL(),
|
||||||
|
width: thumbnail.getSize().width,
|
||||||
|
height: thumbnail.getSize().height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: 'Failed to capture screen' };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Screen capture failed:', e);
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
1015
package-lock.json
generated
1015
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
|||||||
"description": "本地2FA账号密码管理工具",
|
"description": "本地2FA账号密码管理工具",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron ."
|
"start": "electron .",
|
||||||
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"2fa",
|
"2fa",
|
||||||
@@ -14,10 +15,12 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "^28.0.0"
|
"electron": "^28.0.0",
|
||||||
|
"eslint": "^9.39.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"sql.js": "^1.10.0"
|
"sql.js": "^1.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
48
preload.js
48
preload.js
@@ -1,5 +1,17 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron');
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 加载jsQR库
|
||||||
|
let jsQRLib = null;
|
||||||
|
try {
|
||||||
|
// 直接require jsqr模块
|
||||||
|
const jsqrModule = require('jsqr');
|
||||||
|
// 处理ES module的default export
|
||||||
|
jsQRLib = jsqrModule.default || jsqrModule;
|
||||||
|
console.log('jsQR loaded successfully:', typeof jsQRLib);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load jsQR:', e);
|
||||||
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('api', {
|
contextBridge.exposeInMainWorld('api', {
|
||||||
// Vault operations
|
// Vault operations
|
||||||
getVaults: () => ipcRenderer.invoke('db:getVaults'),
|
getVaults: () => ipcRenderer.invoke('db:getVaults'),
|
||||||
@@ -17,5 +29,39 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
searchAccounts: (query, vaultId) => ipcRenderer.invoke('db:searchAccounts', query, vaultId),
|
searchAccounts: (query, vaultId) => ipcRenderer.invoke('db:searchAccounts', query, vaultId),
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
copyToClipboard: (text) => ipcRenderer.invoke('clipboard:write', text)
|
copyToClipboard: (text) => ipcRenderer.invoke('clipboard:write', text),
|
||||||
|
|
||||||
|
|
||||||
|
// Screenshot
|
||||||
|
captureScreen: () => ipcRenderer.invoke('screen:capture'),
|
||||||
|
|
||||||
|
// QR Code parsing
|
||||||
|
decodeQR: (imageData, width, height) => {
|
||||||
|
if (!jsQRLib) {
|
||||||
|
console.error('jsQR not loaded');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 确保是Uint8ClampedArray格式
|
||||||
|
const data = imageData instanceof Uint8ClampedArray
|
||||||
|
? imageData
|
||||||
|
: new Uint8ClampedArray(imageData);
|
||||||
|
|
||||||
|
console.log('Decoding QR, image size:', width, 'x', height, 'data length:', data.length);
|
||||||
|
const result = jsQRLib(data, width, height);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
console.log('QR code found:', result.data);
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
console.log('No QR code found in image');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('QR decode error:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasJsQR: () => !!jsQRLib,
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
const initSqlJs = require('sql.js');
|
const initSqlJs = require('sql.js');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { app } = require('electron');
|
// const { app } = require('electron');
|
||||||
const CryptoJS = require('crypto-js');
|
const CryptoJS = require('crypto-js');
|
||||||
|
|
||||||
const ENCRYPTION_KEY = 'your-secret-key-2fa-manager-v1';
|
const ENCRYPTION_KEY = 'your-secret-key-2fa-manager-v1';
|
||||||
|
|
||||||
class AccountDatabase {
|
class AccountDatabase {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.db = null;
|
this.db = null;
|
||||||
this.dbPath = null;
|
this.dbPath = null;
|
||||||
this.ready = this.init();
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
// 创建资料库表
|
||||||
const SQL = await initSqlJs();
|
this.db.run(`
|
||||||
|
|
||||||
// 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 (
|
CREATE TABLE IF NOT EXISTS vaults (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
@@ -41,8 +41,8 @@ class AccountDatabase {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 创建账号表
|
// 创建账号表
|
||||||
this.db.run(`
|
this.db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
vault_id INTEGER DEFAULT NULL,
|
vault_id INTEGER DEFAULT NULL,
|
||||||
@@ -60,54 +60,54 @@ class AccountDatabase {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 迁移: 添加 tags 和 vault_id 和 proxy 和 browser_id 列 (如果不存在)
|
// 迁移: 添加 tags 和 vault_id 和 proxy 和 browser_id 列 (如果不存在)
|
||||||
try { this.db.run(`ALTER TABLE accounts ADD COLUMN tags TEXT`); } catch (e) { }
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN tags TEXT`); } catch { /* ignore */ }
|
||||||
try { this.db.run(`ALTER TABLE accounts ADD COLUMN vault_id INTEGER`); } catch (e) { }
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN vault_id INTEGER`); } catch { /* ignore */ }
|
||||||
try { this.db.run(`ALTER TABLE accounts ADD COLUMN proxy TEXT`); } catch (e) { }
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN proxy TEXT`); } catch { /* ignore */ }
|
||||||
try { this.db.run(`ALTER TABLE accounts ADD COLUMN browser_id TEXT`); } catch (e) { }
|
try { this.db.run(`ALTER TABLE accounts ADD COLUMN browser_id TEXT`); } catch { /* ignore */ }
|
||||||
|
|
||||||
// 创建默认资料库 (如果不存在)
|
// 创建默认资料库 (如果不存在)
|
||||||
const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'");
|
const defaultVault = this.db.exec("SELECT id FROM vaults WHERE name = '默认'");
|
||||||
if (!defaultVault.length || !defaultVault[0].values.length) {
|
if (!defaultVault.length || !defaultVault[0].values.length) {
|
||||||
this.db.run("INSERT INTO vaults (name, icon) VALUES ('默认', '🏠')");
|
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;
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_name ON accounts(name)`);
|
||||||
const data = this.db.export();
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)`);
|
||||||
const buffer = Buffer.from(data);
|
this.db.run(`CREATE INDEX IF NOT EXISTS idx_accounts_vault ON accounts(vault_id)`);
|
||||||
fs.writeFileSync(this.dbPath, buffer);
|
|
||||||
|
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 {
|
||||||
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
encrypt(text) {
|
// ==================== 资料库操作 ====================
|
||||||
if (!text) return '';
|
|
||||||
return CryptoJS.AES.encrypt(text, ENCRYPTION_KEY).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
decrypt(ciphertext) {
|
async getVaults() {
|
||||||
if (!ciphertext) return '';
|
await this.ready;
|
||||||
try {
|
const result = this.db.exec(`
|
||||||
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
|
SELECT v.*, COUNT(a.id) as account_count
|
||||||
FROM vaults v
|
FROM vaults v
|
||||||
LEFT JOIN accounts a ON a.vault_id = v.id
|
LEFT JOIN accounts a ON a.vault_id = v.id
|
||||||
@@ -115,211 +115,211 @@ class AccountDatabase {
|
|||||||
ORDER BY v.created_at ASC
|
ORDER BY v.created_at ASC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!result.length) return [];
|
if (!result.length) return [];
|
||||||
|
|
||||||
const columns = result[0].columns;
|
const columns = result[0].columns;
|
||||||
return result[0].values.map(row => {
|
return result[0].values.map(row => {
|
||||||
const obj = {};
|
const obj = {};
|
||||||
columns.forEach((col, i) => obj[col] = row[i]);
|
columns.forEach((col, i) => obj[col] = row[i]);
|
||||||
return obj;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addVault(vault) {
|
sql += ` ORDER BY updated_at DESC LIMIT ? OFFSET ?`;
|
||||||
await this.ready;
|
params.push(limit, offset);
|
||||||
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');
|
const stmt = this.db.prepare(sql);
|
||||||
return { id: result[0]?.values[0][0], ...vault };
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateVault(id, vault) {
|
const stmt = this.db.prepare(sql);
|
||||||
await this.ready;
|
stmt.bind(params);
|
||||||
this.db.run(`UPDATE vaults SET name = ?, icon = ?, color = ? WHERE id = ?`, [
|
stmt.step();
|
||||||
vault.name,
|
const result = stmt.getAsObject();
|
||||||
vault.icon || '📁',
|
stmt.free();
|
||||||
vault.color || '#3b82f6',
|
|
||||||
id
|
|
||||||
]);
|
|
||||||
this.save();
|
|
||||||
return { id, ...vault };
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteVault(id) {
|
return result.count || 0;
|
||||||
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]);
|
async addAccount(account) {
|
||||||
this.db.run('DELETE FROM vaults WHERE id = ? AND name != ?', [id, '默认']);
|
await this.ready;
|
||||||
this.save();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 账号操作 ====================
|
this.db.run(`
|
||||||
|
|
||||||
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, browser_id)
|
INSERT INTO accounts (vault_id, name, username, password, totp_secret, tags, email, proxy, notes, browser_id)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
account.vault_id || null,
|
account.vault_id || null,
|
||||||
account.name,
|
account.name,
|
||||||
account.username,
|
account.username,
|
||||||
this.encrypt(account.password),
|
this.encrypt(account.password),
|
||||||
this.encrypt(account.totp_secret),
|
this.encrypt(account.totp_secret),
|
||||||
account.tags || '',
|
account.tags || '',
|
||||||
account.email,
|
account.email,
|
||||||
account.proxy || '',
|
account.proxy || '',
|
||||||
account.notes,
|
account.notes,
|
||||||
account.browser_id || ''
|
account.browser_id || ''
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
const result = this.db.exec('SELECT last_insert_rowid() as id');
|
const result = this.db.exec('SELECT last_insert_rowid() as id');
|
||||||
const id = result[0]?.values[0][0];
|
const id = result[0]?.values[0][0];
|
||||||
|
|
||||||
return { id, ...account };
|
return { id, ...account };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAccount(id, account) {
|
async updateAccount(id, account) {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
|
|
||||||
this.db.run(`
|
this.db.run(`
|
||||||
UPDATE accounts
|
UPDATE accounts
|
||||||
SET vault_id = ?, name = ?, username = ?, password = ?, totp_secret = ?,
|
SET vault_id = ?, name = ?, username = ?, password = ?, totp_secret = ?,
|
||||||
tags = ?, email = ?, proxy = ?, notes = ?, browser_id = ?, updated_at = datetime('now')
|
tags = ?, email = ?, proxy = ?, notes = ?, browser_id = ?, updated_at = datetime('now')
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`, [
|
`, [
|
||||||
account.vault_id || null,
|
account.vault_id || null,
|
||||||
account.name,
|
account.name,
|
||||||
account.username,
|
account.username,
|
||||||
this.encrypt(account.password),
|
this.encrypt(account.password),
|
||||||
this.encrypt(account.totp_secret),
|
this.encrypt(account.totp_secret),
|
||||||
account.tags || '',
|
account.tags || '',
|
||||||
account.email,
|
account.email,
|
||||||
account.proxy || '',
|
account.proxy || '',
|
||||||
account.notes,
|
account.notes,
|
||||||
account.browser_id || '',
|
account.browser_id || '',
|
||||||
id
|
id
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
return { id, ...account };
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveAccountToVault(accountId, vaultId) {
|
sql += ` ORDER BY updated_at DESC LIMIT 50`;
|
||||||
await this.ready;
|
|
||||||
this.db.run('UPDATE accounts SET vault_id = ?, updated_at = datetime("now") WHERE id = ?',
|
const stmt = this.db.prepare(sql);
|
||||||
[vaultId, accountId]);
|
stmt.bind(params);
|
||||||
this.save();
|
|
||||||
return true;
|
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();
|
||||||
|
|
||||||
async deleteAccount(id) {
|
return accounts;
|
||||||
await this.ready;
|
}
|
||||||
this.db.run('DELETE FROM accounts WHERE id = ?', [id]);
|
|
||||||
this.save();
|
close() {
|
||||||
return true;
|
if (this.db) {
|
||||||
}
|
this.save();
|
||||||
|
this.db.close();
|
||||||
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;
|
module.exports = AccountDatabase;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="Content-Security-Policy"
|
<meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; connect-src 'self' http://localhost:12138 http://127.0.0.1:12138">
|
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' data: https://cdnjs.cloudflare.com; img-src 'self' data:; connect-src 'self' http://localhost:12138 http://127.0.0.1:12138">
|
||||||
<title>2FA 账号管理器</title>
|
<title>2FA 账号管理器</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
@@ -16,10 +16,30 @@
|
|||||||
<!-- 左侧区域 -->
|
<!-- 左侧区域 -->
|
||||||
<div class="list-panel">
|
<div class="list-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h1><i class="fa-solid fa-shield-halved"></i> 账号列表</h1>
|
<div class="header-main">
|
||||||
<div class="search-box">
|
<h1><i class="fa-solid fa-shield-halved"></i> 账号列表</h1>
|
||||||
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
<!-- 4路独立备忘录 -->
|
||||||
<input type="text" id="searchInput" placeholder="搜索账号..." />
|
<div class="memo-bar">
|
||||||
|
<div class="memo-group">
|
||||||
|
<input type="text" id="memo1" placeholder="备忘1" class="memo-field" />
|
||||||
|
<i class="fa-solid fa-copy memo-copy-btn" onclick="copyMemo(1)" title="复制备忘1"></i>
|
||||||
|
</div>
|
||||||
|
<div class="memo-divider"></div>
|
||||||
|
<div class="memo-group">
|
||||||
|
<input type="text" id="memo2" placeholder="备忘2" class="memo-field" />
|
||||||
|
<i class="fa-solid fa-copy memo-copy-btn" onclick="copyMemo(2)" title="复制备忘2"></i>
|
||||||
|
</div>
|
||||||
|
<div class="memo-divider"></div>
|
||||||
|
<div class="memo-group">
|
||||||
|
<input type="text" id="memo3" placeholder="备忘3" class="memo-field" />
|
||||||
|
<i class="fa-solid fa-copy memo-copy-btn" onclick="copyMemo(3)" title="复制备忘3"></i>
|
||||||
|
</div>
|
||||||
|
<div class="memo-divider"></div>
|
||||||
|
<div class="memo-group">
|
||||||
|
<input type="text" id="memo4" placeholder="备忘4" class="memo-field" />
|
||||||
|
<i class="fa-solid fa-copy memo-copy-btn" onclick="copyMemo(4)" title="复制备忘4"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,6 +51,12 @@
|
|||||||
<span>资料库</span>
|
<span>资料库</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索框移到资料库上方 -->
|
||||||
|
<div class="vault-search-box">
|
||||||
|
<i class="fa-solid fa-magnifying-glass search-icon"></i>
|
||||||
|
<input type="text" id="searchInput" placeholder="搜索..." />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="vault-items">
|
<div class="vault-items">
|
||||||
<div class="vault-row all-vault active" onclick="selectVault(null)">
|
<div class="vault-row all-vault active" onclick="selectVault(null)">
|
||||||
<span class="vault-name">全部</span>
|
<span class="vault-name">全部</span>
|
||||||
@@ -95,7 +121,12 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="totpSecret">2FA 密钥</label>
|
<label for="totpSecret">2FA 密钥</label>
|
||||||
<input type="text" id="totpSecret" placeholder="TOTP Secret Key">
|
<div class="totp-input-row">
|
||||||
|
<input type="text" id="totpSecret" placeholder="TOTP Secret Key">
|
||||||
|
<button type="button" id="scanQrBtn" class="btn-icon-qr" title="截图识别二维码">
|
||||||
|
<i class="fa-solid fa-qrcode"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -164,7 +195,7 @@
|
|||||||
<input type="checkbox" id="pwdNumbers" checked> 0-9
|
<input type="checkbox" id="pwdNumbers" checked> 0-9
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" id="pwdSymbols" checked> !@#
|
<input type="checkbox" id="pwdSymbols"> !@#
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
长度: <input type="number" id="pwdLength" value="16" min="8" max="64" style="width: 50px;">
|
长度: <input type="number" id="pwdLength" value="16" min="8" max="64" style="width: 50px;">
|
||||||
|
|||||||
264
src/renderer.js
264
src/renderer.js
@@ -52,22 +52,49 @@ class TOTP {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机名称库
|
// 随机名称库 - 多国家名字
|
||||||
const firstNames = [
|
const firstNames = [
|
||||||
|
// 英美名字
|
||||||
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph',
|
'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph',
|
||||||
'Thomas', 'Christopher', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth',
|
'Thomas', 'Christopher', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth',
|
||||||
'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Alex', 'Jordan', 'Taylor',
|
'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Alex', 'Jordan', 'Taylor',
|
||||||
'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Peyton', 'Cameron', 'Emma',
|
'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Peyton', 'Cameron', 'Emma',
|
||||||
'Oliver', 'Liam', 'Noah', 'Sophia', 'Ava', 'Isabella', 'Mia', 'Charlotte',
|
'Oliver', 'Liam', 'Noah', 'Sophia', 'Ava', 'Isabella', 'Mia', 'Charlotte',
|
||||||
'Benjamin', 'Alexander', 'Sebastian', 'Theodore', 'Victoria', 'Penelope'
|
'Benjamin', 'Alexander', 'Sebastian', 'Theodore', 'Victoria', 'Penelope',
|
||||||
|
// 泰国名字
|
||||||
|
'Somchai', 'Somsak', 'Sompong', 'Narong', 'Prasert', 'Wichai', 'Suchart',
|
||||||
|
'Pornthip', 'Siriporn', 'Siriwan', 'Napaporn', 'Kulap', 'Malai', 'Araya',
|
||||||
|
// 印度名字
|
||||||
|
'Rahul', 'Vikram', 'Arjun', 'Ravi', 'Amit', 'Suresh', 'Rajesh', 'Vijay',
|
||||||
|
'Priya', 'Ananya', 'Deepa', 'Kavita', 'Neha', 'Pooja', 'Sunita', 'Lakshmi',
|
||||||
|
// 希腊名字
|
||||||
|
'Alexandros', 'Dimitrios', 'Georgios', 'Konstantinos', 'Nikolaos', 'Panagiotis',
|
||||||
|
'Eleni', 'Maria', 'Aikaterini', 'Sofia', 'Anastasia', 'Theodora', 'Helena',
|
||||||
|
// 西班牙/拉丁名字
|
||||||
|
'Carlos', 'Miguel', 'Jose', 'Antonio', 'Luis', 'Fernando', 'Pablo',
|
||||||
|
'Maria', 'Carmen', 'Rosa', 'Ana', 'Lucia', 'Isabella', 'Elena',
|
||||||
|
// 意大利名字
|
||||||
|
'Marco', 'Luca', 'Matteo', 'Francesco', 'Giovanni', 'Alessandro', 'Andrea',
|
||||||
|
'Giulia', 'Francesca', 'Chiara', 'Sara', 'Valentina', 'Federica', 'Alessia'
|
||||||
];
|
];
|
||||||
|
|
||||||
const lastNames = [
|
const lastNames = [
|
||||||
|
// 英美姓氏
|
||||||
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
|
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis',
|
||||||
'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson',
|
'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson',
|
||||||
'Martin', 'Lee', 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson',
|
'Martin', 'Lee', 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson',
|
||||||
'Walker', 'Young', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams',
|
'Walker', 'Young', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams',
|
||||||
'Nelson', 'Mitchell', 'Campbell', 'Roberts', 'Carter', 'Phillips', 'Evans'
|
'Nelson', 'Mitchell', 'Campbell', 'Roberts', 'Carter', 'Phillips', 'Evans',
|
||||||
|
// 泰国姓氏
|
||||||
|
'Srisawat', 'Suksawat', 'Charoenrat', 'Wongsuwan', 'Thongchai', 'Boonmee',
|
||||||
|
// 印度姓氏
|
||||||
|
'Sharma', 'Patel', 'Singh', 'Kumar', 'Reddy', 'Verma', 'Gupta', 'Nair', 'Rao',
|
||||||
|
// 希腊姓氏
|
||||||
|
'Papadopoulos', 'Konstantinidis', 'Nikolaidis', 'Georgiou', 'Christodoulou',
|
||||||
|
// 西班牙姓氏
|
||||||
|
'Gonzalez', 'Rodriguez', 'Martinez', 'Lopez', 'Hernandez', 'Sanchez', 'Ramirez',
|
||||||
|
// 意大利姓氏
|
||||||
|
'Rossi', 'Russo', 'Ferrari', 'Esposito', 'Bianchi', 'Romano', 'Colombo'
|
||||||
];
|
];
|
||||||
|
|
||||||
// 应用状态
|
// 应用状态
|
||||||
@@ -102,6 +129,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
generateRandomName();
|
generateRandomName();
|
||||||
generateRandomPassword();
|
generateRandomPassword();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
|
setupMemo();
|
||||||
|
initQRScanner();
|
||||||
|
|
||||||
|
// 检查jsQR是否可用
|
||||||
|
if (window.api.hasJsQR()) {
|
||||||
|
console.log('jsQR library loaded successfully');
|
||||||
|
} else {
|
||||||
|
console.warn('jsQR not available, QR scanning may not work');
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(updateAllTOTP, 1000);
|
setInterval(updateAllTOTP, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -582,14 +619,7 @@ async function showTOTP(id, secret) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyValue(element, value) {
|
async function copyValue(element, value) {
|
||||||
try {
|
await copyToClipboard(value, { element });
|
||||||
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) {
|
async function updateTOTP(id, secret) {
|
||||||
@@ -630,10 +660,7 @@ async function copyTOTP(id) {
|
|||||||
const code = await TOTP.generate(acc.totp_secret);
|
const code = await TOTP.generate(acc.totp_secret);
|
||||||
if (code) {
|
if (code) {
|
||||||
const codeEl = document.getElementById(`totp-${id}`);
|
const codeEl = document.getElementById(`totp-${id}`);
|
||||||
codeEl.classList.add('copied');
|
await copyToClipboard(code, { element: codeEl });
|
||||||
await window.api.copyToClipboard(code);
|
|
||||||
showToast('已复制', 'success');
|
|
||||||
setTimeout(() => { codeEl.classList.remove('copied'); }, 300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,12 +769,19 @@ function resetForm() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text) {
|
async function copyToClipboard(text, { element = null, successMessage = '已复制' } = {}) {
|
||||||
|
if (!text) return false;
|
||||||
try {
|
try {
|
||||||
await window.api.copyToClipboard(text);
|
await window.api.copyToClipboard(text);
|
||||||
showToast('已复制', 'success');
|
if (element) {
|
||||||
|
element.classList.add('copied');
|
||||||
|
setTimeout(() => { element.classList.remove('copied'); }, 300);
|
||||||
|
}
|
||||||
|
showToast(successMessage, 'success');
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Copy failed:', e);
|
console.error('Copy failed:', e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,24 +801,50 @@ async function openBrowser(idOrName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成随机名称 (不含符号的长用户名)
|
// 生成随机字母片段
|
||||||
|
function getRandomLetters(count) {
|
||||||
|
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result += letters[Math.floor(Math.random() * letters.length)];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机数字片段
|
||||||
|
function getRandomNumbers(count) {
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result += Math.floor(Math.random() * 10);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机名称 (基于姓名的用户名)
|
||||||
function generateRandomName() {
|
function generateRandomName() {
|
||||||
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
||||||
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
||||||
const suffix = Math.floor(Math.random() * 10000);
|
const suffix = Math.floor(Math.random() * 10000);
|
||||||
const year = 1990 + Math.floor(Math.random() * 30);
|
const year = 1990 + Math.floor(Math.random() * 30);
|
||||||
|
const randomLetters = getRandomLetters(Math.floor(Math.random() * 3) + 2); // 2-4个随机字母
|
||||||
|
const randomNumbers = getRandomNumbers(Math.floor(Math.random() * 3) + 2); // 2-4个随机数字
|
||||||
|
|
||||||
document.getElementById('randomFullName').value = `${firstName} ${lastName}`;
|
document.getElementById('randomFullName').value = `${firstName} ${lastName}`;
|
||||||
|
|
||||||
// 不含下划线和符号的用户名格式
|
// 改进的用户名格式 - 去掉了real, official等词,增加随机元素
|
||||||
const usernameFormats = [
|
const usernameFormats = [
|
||||||
`${firstName.toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
||||||
`${firstName.toLowerCase()}${lastName.toLowerCase()}${year}`,
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}${year}`,
|
||||||
`${firstName.toLowerCase()}${lastName.toLowerCase()}`,
|
`${firstName.toLowerCase()}${lastName.toLowerCase()}`,
|
||||||
`${firstName[0].toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
`${firstName[0].toLowerCase()}${lastName.toLowerCase()}${suffix}`,
|
||||||
`${lastName.toLowerCase()}${firstName.toLowerCase()}${suffix}`,
|
`${lastName.toLowerCase()}${firstName.toLowerCase()}${suffix}`,
|
||||||
`${firstName.toLowerCase()}${lastName.toLowerCase()}official`,
|
`${firstName.toLowerCase()}${randomNumbers}`,
|
||||||
`real${firstName.toLowerCase()}${lastName.toLowerCase()}`
|
`${lastName.toLowerCase()}${randomNumbers}`,
|
||||||
|
`${firstName.toLowerCase()}${randomLetters}${randomNumbers}`,
|
||||||
|
`${firstName[0].toLowerCase()}${lastName.toLowerCase()}${randomNumbers}`,
|
||||||
|
`${firstName.toLowerCase()}${lastName[0].toLowerCase()}${suffix}`,
|
||||||
|
`${randomLetters}${firstName.toLowerCase()}${randomNumbers}`,
|
||||||
|
`${firstName.toLowerCase()}${year}${randomLetters}`
|
||||||
];
|
];
|
||||||
const username = usernameFormats[Math.floor(Math.random() * usernameFormats.length)];
|
const username = usernameFormats[Math.floor(Math.random() * usernameFormats.length)];
|
||||||
document.getElementById('randomUsername').value = username;
|
document.getElementById('randomUsername').value = username;
|
||||||
@@ -834,3 +894,165 @@ function escapeJs(text) {
|
|||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 备忘录功能 ====================
|
||||||
|
function setupMemo() {
|
||||||
|
const memoIds = ['memo1', 'memo2', 'memo3', 'memo4'];
|
||||||
|
|
||||||
|
// 加载
|
||||||
|
const saved = localStorage.getItem('2fa-memos-v2');
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(saved);
|
||||||
|
memoIds.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.value = data[id] || '';
|
||||||
|
});
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存逻辑
|
||||||
|
const saveAll = () => {
|
||||||
|
const data = {};
|
||||||
|
memoIds.forEach(id => {
|
||||||
|
data[id] = document.getElementById(id).value;
|
||||||
|
});
|
||||||
|
localStorage.setItem('2fa-memos-v2', JSON.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
memoIds.forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener('input', saveAll);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 全局复制函数
|
||||||
|
window.copyMemo = (index) => {
|
||||||
|
const val = document.getElementById(`memo${index}`).value.trim();
|
||||||
|
if (val) {
|
||||||
|
copyToClipboard(val, { successMessage: `已复制备忘录 ${index}` });
|
||||||
|
} else {
|
||||||
|
showToast('该备忘录为空', 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 截图识别二维码功能 ====================
|
||||||
|
|
||||||
|
// Initialize QR scan button
|
||||||
|
function initQRScanner() {
|
||||||
|
const scanBtn = document.getElementById('scanQrBtn');
|
||||||
|
if (scanBtn) {
|
||||||
|
scanBtn.addEventListener('click', startQRScan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start QR scan (full screen)
|
||||||
|
async function startQRScan() {
|
||||||
|
showToast('Scanning screen for QR code...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get screen capture
|
||||||
|
const result = await window.api.captureScreen();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
showToast('Screenshot failed: ' + result.error, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show scanning indicator
|
||||||
|
showScanningIndicator();
|
||||||
|
|
||||||
|
// Parse QR from screenshot
|
||||||
|
const secret = await parseQRFromImage(result.image, result.width, result.height);
|
||||||
|
|
||||||
|
// Hide indicator
|
||||||
|
hideScanningIndicator();
|
||||||
|
|
||||||
|
if (secret) {
|
||||||
|
document.getElementById('totpSecret').value = secret;
|
||||||
|
showToast('2FA key found: ' + secret.substring(0, 8) + '...', 'success');
|
||||||
|
} else {
|
||||||
|
showToast('No valid 2FA QR code found on screen', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('QR scan error:', e);
|
||||||
|
hideScanningIndicator();
|
||||||
|
showToast('Scan failed: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse QR from image
|
||||||
|
async function parseQRFromImage(dataUrl, width, height) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
const dataArray = Array.from(imageData.data);
|
||||||
|
|
||||||
|
console.log('QR: Decoding image', width, 'x', height);
|
||||||
|
|
||||||
|
const code = window.api.decodeQR(dataArray, width, height);
|
||||||
|
console.log('QR: Decode result:', code);
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
const secret = parseOtpAuthUrl(code);
|
||||||
|
console.log('QR: Parsed secret:', secret);
|
||||||
|
resolve(secret);
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.onerror = () => resolve(null);
|
||||||
|
img.src = dataUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析otpauth URL提取密钥
|
||||||
|
function parseOtpAuthUrl(url) {
|
||||||
|
if (!url) return null;
|
||||||
|
|
||||||
|
// 格式: otpauth://totp/Label?secret=XXXX&issuer=XXX
|
||||||
|
const match = url.match(/otpauth:\/\/totp\/[^?]*\?.*secret=([A-Z2-7]+)/i);
|
||||||
|
if (match) {
|
||||||
|
return match[1].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 也尝试匹配hotp
|
||||||
|
const hotpMatch = url.match(/otpauth:\/\/hotp\/[^?]*\?.*secret=([A-Z2-7]+)/i);
|
||||||
|
if (hotpMatch) {
|
||||||
|
return hotpMatch[1].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果URL本身就是密钥格式
|
||||||
|
if (/^[A-Z2-7]{16,}$/i.test(url.trim())) {
|
||||||
|
return url.trim().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示扫描中指示器
|
||||||
|
function showScanningIndicator() {
|
||||||
|
const indicator = document.createElement('div');
|
||||||
|
indicator.className = 'scanning-indicator';
|
||||||
|
indicator.id = 'scanningIndicator';
|
||||||
|
indicator.innerHTML = `
|
||||||
|
<i class="fa-solid fa-qrcode"></i>
|
||||||
|
<span>正在识别二维码...</span>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏扫描中指示器
|
||||||
|
function hideScanningIndicator() {
|
||||||
|
const indicator = document.getElementById('scanningIndicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
543
src/styles.css
543
src/styles.css
@@ -72,40 +72,6 @@ body {
|
|||||||
color: var(--accent);
|
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 {
|
.content-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -333,151 +299,6 @@ body {
|
|||||||
font-size: 10px;
|
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 {
|
.accounts-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -1047,176 +868,6 @@ body {
|
|||||||
border-radius: 3px;
|
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 {
|
.card-action-btn.move {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1302,9 +953,203 @@ body {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-header h1,
|
||||||
.form-section h2,
|
.form-section h2,
|
||||||
.generator-section h3 {
|
.generator-section h3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4路紧凑型备忘录 */
|
||||||
|
.memo-bar {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-group {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-group:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-field {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 5px 4px;
|
||||||
|
min-width: 0;
|
||||||
|
outline: none;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-copy-btn {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
opacity: 0.6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-copy-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memo-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 10px;
|
||||||
|
background: var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 资料库内搜索框 ==================== */
|
||||||
|
.vault-search-box {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-search-box .search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-search-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px 8px 28px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-search-box input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-search-box input::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 2FA输入行 & 截图按钮 ==================== */
|
||||||
|
.totp-input-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totp-input-row input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-qr {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%);
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-qr:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.25) 0%, rgba(139, 92, 246, 0.25) 100%);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-qr:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-qr i {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫描动画 */
|
||||||
|
.scanning-indicator {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10001;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px 32px;
|
||||||
|
background: rgba(17, 17, 19, 0.95);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanning-indicator i {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--accent);
|
||||||
|
animation: pulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scanning-indicator span {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
16
stop.bat
16
stop.bat
@@ -1,24 +1,24 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo 正在关闭 2FA Account Tool...
|
echo Closing 2FA Account Tool...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: 终止所有 electron 相关进程
|
:: Kill all electron processes
|
||||||
taskkill /f /im electron.exe >nul 2>&1
|
taskkill /f /im electron.exe >nul 2>&1
|
||||||
if %errorlevel% equ 0 (
|
if %errorlevel% equ 0 (
|
||||||
echo [已关闭] electron.exe
|
echo [CLOSED] electron.exe
|
||||||
) else (
|
) else (
|
||||||
echo [未运行] electron.exe
|
echo [NOT RUNNING] electron.exe
|
||||||
)
|
)
|
||||||
|
|
||||||
:: 可能有残留的 node 进程
|
:: Kill any remaining node processes
|
||||||
taskkill /f /im node.exe >nul 2>&1
|
taskkill /f /im node.exe >nul 2>&1
|
||||||
if %errorlevel% equ 0 (
|
if %errorlevel% equ 0 (
|
||||||
echo [已关闭] node.exe
|
echo [CLOSED] node.exe
|
||||||
) else (
|
) else (
|
||||||
echo [未运行] node.exe
|
echo [NOT RUNNING] node.exe
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo 程序已彻底关闭,现在可以安全删除 node_modules 文件夹了。
|
echo Program has been closed. You can now safely delete node_modules folder.
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
|
|||||||
Reference in New Issue
Block a user