chore: configure eslint and update gitignore

This commit is contained in:
TYt50
2026-02-01 15:07:02 +08:00
parent 1cbd819ff6
commit 8908d56518
13 changed files with 1938 additions and 643 deletions

7
.gitignore vendored
View File

@@ -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
View 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
View 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
View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }

View File

@@ -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,
}); });

View File

@@ -1,7 +1,7 @@
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';
@@ -61,10 +61,10 @@ 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 = '默认'");
@@ -98,7 +98,7 @@ class AccountDatabase {
try { try {
const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY); const bytes = CryptoJS.AES.decrypt(ciphertext, ENCRYPTION_KEY);
return bytes.toString(CryptoJS.enc.Utf8); return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) { } catch {
return ciphertext; return ciphertext;
} }
} }

View File

@@ -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">
<div class="header-main">
<h1><i class="fa-solid fa-shield-halved"></i> 账号列表</h1> <h1><i class="fa-solid fa-shield-halved"></i> 账号列表</h1>
<div class="search-box"> <!-- 4路独立备忘录 -->
<i class="fa-solid fa-magnifying-glass search-icon"></i> <div class="memo-bar">
<input type="text" id="searchInput" placeholder="搜索账号..." /> <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>
<div class="totp-input-row">
<input type="text" id="totpSecret" placeholder="TOTP Secret Key"> <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;">

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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