// TOTP 生成器 class TOTP { static async generate(secret) { if (!secret) return null; try { const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; const cleanSecret = secret.replace(/\s/g, '').toUpperCase(); for (let i = 0; i < cleanSecret.length; i++) { const val = base32chars.indexOf(cleanSecret[i]); if (val === -1) continue; bits += val.toString(2).padStart(5, '0'); } const bytes = new Uint8Array(Math.floor(bits.length / 8)); for (let i = 0; i < bytes.length; i++) { bytes[i] = parseInt(bits.substr(i * 8, 8), 2); } const epoch = Math.floor(Date.now() / 1000); const timeStep = Math.floor(epoch / 30); const timeBuffer = new ArrayBuffer(8); const timeView = new DataView(timeBuffer); timeView.setUint32(4, timeStep, false); const key = await crypto.subtle.importKey( 'raw', bytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign'] ); const signature = await crypto.subtle.sign('HMAC', key, timeBuffer); const hmac = new Uint8Array(signature); const offset = hmac[hmac.length - 1] & 0xf; const binary = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); const otp = binary % 1000000; return otp.toString().padStart(6, '0'); } catch (e) { console.error('TOTP generation error:', e); return null; } } static getTimeRemaining() { return 30 - (Math.floor(Date.now() / 1000) % 30); } } // 随机名称库 const firstNames = [ 'James', 'John', 'Robert', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Christopher', 'Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Alex', 'Jordan', 'Taylor', 'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Peyton', 'Cameron', 'Emma', 'Oliver', 'Liam', 'Noah', 'Sophia', 'Ava', 'Isabella', 'Mia', 'Charlotte', 'Benjamin', 'Alexander', 'Sebastian', 'Theodore', 'Victoria', 'Penelope' ]; const lastNames = [ 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', 'Anderson', 'Taylor', 'Thomas', 'Moore', 'Jackson', 'Martin', 'Lee', 'Thompson', 'White', 'Harris', 'Clark', 'Lewis', 'Robinson', 'Walker', 'Young', 'King', 'Wright', 'Scott', 'Green', 'Baker', 'Adams', 'Nelson', 'Mitchell', 'Campbell', 'Roberts', 'Carter', 'Phillips', 'Evans' ]; // 应用状态 let accounts = []; let vaults = []; let currentPage = 1; let totalPages = 1; const itemsPerPage = 8; let editingId = null; let selectedVaultId = null; let movingAccountId = null; let totalAccountCount = 0; // DOM 元素 const accountsList = document.getElementById('accountsList'); const accountForm = document.getElementById('accountForm'); const formTitle = document.getElementById('formTitle'); const searchInput = document.getElementById('searchInput'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); const pageInfo = document.getElementById('pageInfo'); const cancelBtn = document.getElementById('cancelBtn'); const toast = document.getElementById('toast'); const vaultList = document.getElementById('vaultList'); const vaultFormSection = document.getElementById('vaultFormSection'); const moveModal = document.getElementById('moveModal'); // 初始化 document.addEventListener('DOMContentLoaded', () => { loadVaults(); loadAccounts(); generateRandomName(); generateRandomPassword(); setupEventListeners(); setInterval(updateAllTOTP, 1000); }); function setupEventListeners() { accountForm.addEventListener('submit', handleFormSubmit); cancelBtn.addEventListener('click', resetForm); // 搜索 let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { if (e.target.value.trim()) { searchAccounts(e.target.value.trim()); } else { loadAccounts(); } }, 300); }); // 分页 prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; loadAccounts(); } }); nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; loadAccounts(); } }); // 随机生成器 document.getElementById('rollName').addEventListener('click', generateRandomName); document.getElementById('rollPassword').addEventListener('click', generateRandomPassword); document.querySelectorAll('.generator-section .copy-btn').forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.dataset.target; const value = document.getElementById(targetId).value; copyToClipboard(value); }); }); document.getElementById('randomFullName').addEventListener('click', function () { copyToClipboard(this.value); }); document.getElementById('randomUsername').addEventListener('click', function () { copyToClipboard(this.value); }); ['pwdUppercase', 'pwdLowercase', 'pwdNumbers', 'pwdSymbols', 'pwdLength'].forEach(id => { document.getElementById(id).addEventListener('change', generateRandomPassword); }); // 资料库表单 (保留用于兼容) document.getElementById('cancelVaultBtn').addEventListener('click', () => { vaultFormSection.style.display = 'none'; }); document.getElementById('saveVaultBtn').addEventListener('click', saveVault); // 迁移模态框 document.getElementById('cancelMoveBtn').addEventListener('click', () => { moveModal.style.display = 'none'; movingAccountId = null; }); document.getElementById('confirmMoveBtn').addEventListener('click', confirmMoveAccount); } // ==================== 资料库操作 ==================== async function loadVaults() { try { vaults = await window.api.getVaults(); totalAccountCount = await window.api.getAccountCount(null); renderVaultList(); } catch (e) { console.error('Failed to load vaults:', e); } } function renderVaultList() { // 更新全部账号数量 document.getElementById('allCount').textContent = totalAccountCount; // 更新全部账号选中状态 const allVaultRow = document.querySelector('.vault-row.all-vault'); if (allVaultRow) { allVaultRow.classList.toggle('active', selectedVaultId === null); } vaultList.innerHTML = vaults.map(v => `
${escapeHtml(v.name)} ${v.account_count || 0} ${v.name !== '默认' ? ` ` : ''}
`).join(''); // 绑定事件 vaultList.querySelectorAll('.vault-row').forEach(row => { const id = parseInt(row.dataset.id); const vault = vaults.find(v => v.id === id); if (!vault) return; let clickTimeout = null; row.addEventListener('click', (e) => { if (row.classList.contains('editing')) return; if (e.target.closest('.vault-delete')) { e.stopPropagation(); deleteVault(id); return; } // 延迟执行单击,以便检测是否是双击 if (clickTimeout) { clearTimeout(clickTimeout); clickTimeout = null; return; } clickTimeout = setTimeout(() => { clickTimeout = null; selectVault(id); }, 200); }); // 只有非默认资料库可以双击改名 if (vault.name !== '默认') { row.addEventListener('dblclick', (e) => { e.stopPropagation(); e.preventDefault(); // 取消单击 if (clickTimeout) { clearTimeout(clickTimeout); clickTimeout = null; } startEditVault(row, id); }); } }); } function selectVault(id) { selectedVaultId = id; currentPage = 1; loadAccounts(); renderVaultList(); } // 双击资料库开始编辑 function startEditVault(row, id) { const vault = vaults.find(v => v.id === id); if (!vault || vault.name === '默认') return; row.classList.add('editing'); const nameSpan = row.querySelector('.vault-name'); const originalName = vault.name; const input = document.createElement('input'); input.type = 'text'; input.className = 'vault-edit'; input.value = originalName; nameSpan.replaceWith(input); // 延迟聚焦以确保元素已渲染 setTimeout(() => { input.focus(); input.select(); }, 10); let isFinished = false; async function finishEdit() { if (isFinished) return; isFinished = true; const newName = input.value.trim(); if (newName && newName !== originalName) { try { await window.api.updateVault(id, { name: newName }); showToast('资料库已更新', 'success'); } catch (e) { console.error('Update vault failed:', e); showToast('更新失败', 'error'); } } loadVaults(); } input.addEventListener('blur', finishEdit); input.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter') { input.blur(); } else if (e.key === 'Escape') { input.value = originalName; input.blur(); } }); // 阻止输入框的点击事件冒泡 input.addEventListener('click', (e) => { e.stopPropagation(); }); } // 点击 + 新增资料库 (行内创建) async function showAddVaultForm() { // 检查是否已有输入框 if (vaultList.querySelector('.vault-new-row')) return; // 创建新增行 const editRow = document.createElement('div'); editRow.className = 'vault-new-row'; editRow.innerHTML = ` `; // 插入到 vaultList 末尾 vaultList.appendChild(editRow); const input = editRow.querySelector('input'); // 延迟聚焦 setTimeout(() => { input.focus(); }, 10); let isFinished = false; async function finishCreate() { if (isFinished) return; isFinished = true; const name = input.value.trim(); if (name) { try { await window.api.addVault({ name }); showToast('资料库已创建', 'success'); } catch (e) { console.error('Add vault failed:', e); showToast('创建失败', 'error'); } } loadVaults(); } input.addEventListener('blur', finishCreate); input.addEventListener('keydown', (e) => { e.stopPropagation(); if (e.key === 'Enter') { input.blur(); } else if (e.key === 'Escape') { loadVaults(); } }); // 阻止点击事件冒泡 input.addEventListener('click', (e) => { e.stopPropagation(); }); } async function saveVault() { const id = document.getElementById('vaultId').value; const name = document.getElementById('vaultName').value.trim(); if (!name) { showToast('请输入资料库名称', 'error'); return; } try { if (id) { await window.api.updateVault(parseInt(id), { name }); showToast('资料库已更新', 'success'); } else { await window.api.addVault({ name }); showToast('资料库已创建', 'success'); } vaultFormSection.style.display = 'none'; loadVaults(); } catch (e) { console.error('Save vault failed:', e); showToast('保存失败', 'error'); } } async function deleteVault(id) { const vault = vaults.find(v => v.id === id); if (!vault || vault.name === '默认') return; if (!confirm(`确定要删除资料库「${vault.name}」吗?\n其中的账号将被移到默认资料库。`)) return; try { await window.api.deleteVault(id); showToast('资料库已删除', 'success'); if (selectedVaultId === id) { selectedVaultId = null; } loadVaults(); loadAccounts(); } catch (e) { console.error('Delete vault failed:', e); showToast('删除失败', 'error'); } } function openMoveDialog(accountId) { movingAccountId = accountId; const select = document.getElementById('moveToVault'); select.innerHTML = vaults.map(v => `` ).join(''); moveModal.style.display = 'flex'; } async function confirmMoveAccount() { if (!movingAccountId) return; const newVaultId = parseInt(document.getElementById('moveToVault').value); try { await window.api.moveAccountToVault(movingAccountId, newVaultId); showToast('账号已迁移', 'success'); moveModal.style.display = 'none'; movingAccountId = null; loadAccounts(); loadVaults(); } catch (e) { console.error('Move failed:', e); showToast('迁移失败', 'error'); } } // ==================== 账号操作 ==================== async function loadAccounts() { try { const count = await window.api.getAccountCount(selectedVaultId); totalPages = Math.max(1, Math.ceil(count / itemsPerPage)); if (currentPage > totalPages) { currentPage = totalPages; } accounts = await window.api.getAccounts(currentPage, itemsPerPage, selectedVaultId); renderAccounts(); updatePagination(); } catch (e) { console.error('Failed to load accounts:', e); showToast('加载账号失败', 'error'); } } async function searchAccounts(query) { try { accounts = await window.api.searchAccounts(query, selectedVaultId); currentPage = 1; totalPages = 1; renderAccounts(); updatePagination(); } catch (e) { console.error('Search failed:', e); } } function renderAccounts() { if (accounts.length === 0) { accountsList.innerHTML = `
暂无账号
在右侧添加你的第一个账号
`; return; } accountsList.innerHTML = accounts.map(acc => { const tags = acc.tags ? acc.tags.split(',').map(t => t.trim()).filter(t => t) : []; const tagsHtml = tags.length > 0 ? `
${tags.map(t => `${escapeHtml(t)}`).join('')}
` : ''; return `
${tagsHtml || '无标签'}
${acc.username ? `
账号 ${escapeHtml(acc.username)}
` : ''} ${acc.password ? `
密码 ••••••••
` : ''} ${acc.email ? `
邮箱 ${escapeHtml(acc.email)}
` : ''} ${acc.proxy ? `
代理 ${escapeHtml(acc.proxy)}
` : ''} ${acc.totp_secret ? `
点击显示 2FA
` : ''}
`; }).join(''); // 不再自动更新TOTP,改为点击时才显示 } // 保存已激活的TOTP账号列表 const activeTOTPs = new Map(); // 显示TOTP async function showTOTP(id, secret) { const container = document.getElementById(`totp-container-${id}`); const visible = document.getElementById(`totp-visible-${id}`); if (!container || !visible) return; container.style.display = 'none'; visible.style.display = 'flex'; // 保存到激活列表 activeTOTPs.set(id, secret); // 立即更新一次 await updateTOTP(id, secret); } async function copyValue(element, value) { try { await window.api.copyToClipboard(value); element.classList.add('copied'); showToast('已复制', 'success'); setTimeout(() => { element.classList.remove('copied'); }, 300); } catch (e) { console.error('Copy failed:', e); } } async function updateTOTP(id, secret) { const codeEl = document.getElementById(`totp-${id}`); const timerEl = document.getElementById(`timer-${id}`); if (!codeEl || !timerEl) return; const code = await TOTP.generate(secret); if (code) { codeEl.textContent = code.substring(0, 3) + ' ' + code.substring(3); } const remaining = TOTP.getTimeRemaining(); const progress = (remaining / 30) * 88; const progressCircle = timerEl.querySelector('.progress'); progressCircle.style.strokeDashoffset = 88 - progress; timerEl.classList.remove('warning', 'danger'); if (remaining <= 5) { timerEl.classList.add('danger'); } else if (remaining <= 10) { timerEl.classList.add('warning'); } } function updateAllTOTP() { // 只更新已激活显示的TOTP activeTOTPs.forEach((secret, id) => { updateTOTP(id, secret); }); } async function copyTOTP(id) { const acc = accounts.find(a => a.id === id); if (!acc || !acc.totp_secret) return; const code = await TOTP.generate(acc.totp_secret); if (code) { const codeEl = document.getElementById(`totp-${id}`); codeEl.classList.add('copied'); await window.api.copyToClipboard(code); showToast('已复制', 'success'); setTimeout(() => { codeEl.classList.remove('copied'); }, 300); } } function updatePagination() { pageInfo.textContent = `${currentPage} / ${totalPages}`; prevBtn.disabled = currentPage <= 1; nextBtn.disabled = currentPage >= totalPages; } async function handleFormSubmit(e) { e.preventDefault(); const username = document.getElementById('username').value.trim(); const tags = document.getElementById('tags').value.trim(); // 使用标签的第一个作为name,如果没有标签则使用用户名 const firstTag = tags ? tags.split(',')[0].trim() : ''; const autoName = firstTag || username || '未命名'; const account = { vault_id: selectedVaultId, name: autoName, username: username, password: document.getElementById('password').value, totp_secret: document.getElementById('totpSecret').value.trim(), tags: tags, email: document.getElementById('email').value.trim(), proxy: document.getElementById('proxy').value.trim(), notes: document.getElementById('notes').value.trim() }; try { if (editingId) { const existingAcc = accounts.find(a => a.id === editingId); if (existingAcc) { account.vault_id = existingAcc.vault_id; } await window.api.updateAccount(editingId, account); showToast('账号已更新', 'success'); } else { await window.api.addAccount(account); showToast('账号已添加', 'success'); } resetForm(); loadAccounts(); loadVaults(); } catch (e) { console.error('Save failed:', e); showToast('保存失败', 'error'); } } function editAccount(id) { const acc = accounts.find(a => a.id === id); if (!acc) return; editingId = id; formTitle.innerHTML = ' 编辑账号'; document.getElementById('accountId').value = id; document.getElementById('username').value = acc.username || ''; document.getElementById('password').value = acc.password || ''; document.getElementById('totpSecret').value = acc.totp_secret || ''; document.getElementById('email').value = acc.email || ''; document.getElementById('proxy').value = acc.proxy || ''; document.getElementById('tags').value = acc.tags || ''; document.getElementById('notes').value = acc.notes || ''; document.querySelectorAll('.account-card').forEach(card => { card.classList.toggle('active', card.dataset.id == id); }); document.getElementById('username').focus(); } async function deleteAccount(id) { if (!confirm('确定要删除这个账号吗?')) return; try { await window.api.deleteAccount(id); showToast('账号已删除', 'success'); if (editingId === id) { resetForm(); } loadAccounts(); loadVaults(); } catch (e) { console.error('Delete failed:', e); showToast('删除失败', 'error'); } } function resetForm() { editingId = null; formTitle.innerHTML = ' 添加账号'; accountForm.reset(); document.getElementById('accountId').value = ''; document.querySelectorAll('.account-card').forEach(card => { card.classList.remove('active'); }); } async function copyToClipboard(text) { try { await window.api.copyToClipboard(text); showToast('已复制', 'success'); } catch (e) { console.error('Copy failed:', e); } } // 生成随机名称 (不含符号的长用户名) function generateRandomName() { const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; const suffix = Math.floor(Math.random() * 10000); const year = 1990 + Math.floor(Math.random() * 30); document.getElementById('randomFullName').value = `${firstName} ${lastName}`; // 不含下划线和符号的用户名格式 const usernameFormats = [ `${firstName.toLowerCase()}${lastName.toLowerCase()}${suffix}`, `${firstName.toLowerCase()}${lastName.toLowerCase()}${year}`, `${firstName.toLowerCase()}${lastName.toLowerCase()}`, `${firstName[0].toLowerCase()}${lastName.toLowerCase()}${suffix}`, `${lastName.toLowerCase()}${firstName.toLowerCase()}${suffix}`, `${firstName.toLowerCase()}${lastName.toLowerCase()}official`, `real${firstName.toLowerCase()}${lastName.toLowerCase()}` ]; const username = usernameFormats[Math.floor(Math.random() * usernameFormats.length)]; document.getElementById('randomUsername').value = username; } function generateRandomPassword() { const length = parseInt(document.getElementById('pwdLength').value) || 16; const useUppercase = document.getElementById('pwdUppercase').checked; const useLowercase = document.getElementById('pwdLowercase').checked; const useNumbers = document.getElementById('pwdNumbers').checked; const useSymbols = document.getElementById('pwdSymbols').checked; let chars = ''; if (useUppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (useLowercase) chars += 'abcdefghijklmnopqrstuvwxyz'; if (useNumbers) chars += '0123456789'; if (useSymbols) chars += '!@#$%^&*()_+-=[]{}|;:,.<>?'; if (!chars) chars = 'abcdefghijklmnopqrstuvwxyz'; let password = ''; const array = new Uint32Array(length); crypto.getRandomValues(array); for (let i = 0; i < length; i++) { password += chars[array[i] % chars.length]; } document.getElementById('randomPassword').value = password; } function showToast(message, type = 'info') { toast.textContent = message; toast.className = 'toast show'; if (type === 'success') toast.classList.add('success'); setTimeout(() => { toast.classList.remove('show'); }, 2000); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function escapeJs(text) { if (!text) return ''; return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); }