<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>🔒 密码管理器</title> <style> :root { --bg: #f9f9f9; --text: #000; --border: #ccc; --header: #eee; --btn: #007bff; --btn-hover: #0056b3; --danger: #d9534f; --success: #28a745; --warning: #ffc107; } .dark-mode { --bg: #1e1e1e; --text: #e0e0e0; --border: #444; --header: #2a2a2a; --btn: #0d6efd; --btn-hover: #0b5ed7; --danger: #ff6b6b; --success: #5cb85c; --warning: #ffcc00; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 10px; background: var(--bg); color: var(--text); transition: background-color 0.3s, color 0.3s; } .container { max-width: 900px; margin: 0 auto; } .top-bar h2 { margin-bottom: 12px; } .top-buttons { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; } .theme-toggle { margin-left: auto; } .stats { margin-top: 8px; font-size: 0.9em; color: var(--text); } table { width: 100%; border-collapse: collapse; } th, td { border: 1px solid var(--border); padding: 8px; text-align: center; vertical-align: middle; } th { background: var(--header); } tr:hover { background-color: rgba(0, 123, 255, 0.05); cursor: pointer; } .countdown { color: var(--danger); font-size: 0.85em; margin-left: 6px; } .modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; z-index: 1000; display: none; } .modal-content { background: var(--bg); color: var(--text); padding: 24px; border-radius: 8px; width: 90%; max-width: 400px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); } .modal input { width: 100%; padding: 8px; margin: 8px 0; border: 1px solid var(--border); background: var(--bg); color: var(--text); } .password-strength { height: 6px; background: var(--border); border-radius: 3px; margin: 8px 0; position: relative; } .password-strength-bar { height: 100%; border-radius: 3px; width: 0; transition: width 0.3s, background-color 0.3s; } .password-strength-text { font-size: 0.85em; text-align: right; margin-top: 4px; } .modal-buttons { text-align: right; margin-top: 16px; } .modal-buttons button { padding: 6px 12px; margin-left: 8px; border: none; border-radius: 4px; cursor: pointer; } .btn-primary { background: var(--btn); color: white; } .btn-cancel { background: #6c757d; color: white; } .btn-success { background: var(--success); color: white; } .btn-danger { background: var(--danger); color: white; } .password-wrapper { position: relative; } .toggle-password { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text); cursor: pointer; font-size: 1.2em; } .batch-actions { margin-top: 10px; display: none; } .batch-actions.visible { display: flex; gap: 8px; align-items: center; } .input-with-clear { position: relative; display: inline-flex; align-items: center; } #inputText { padding-right: 28px; } .clear-btn { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 1.2em; color: var(--text); opacity: 0.6; cursor: pointer; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } .clear-btn:hover { opacity: 1; color: var(--danger); } #hintDisplay { font-style: italic; color: var(--text); padding: 8px; background: var(--header); border-radius: 4px; text-align: center; margin-bottom: 12px; } .error-message { color: var(--danger); min-height: 1.2em; margin-top: 4px; } </style> </head> <body> <!-- 主界面(初始隐藏) --> <div class="container" id="mainApp" style="display:none;"> <div class="top-bar"> <h2>密码🙈管理器👁️</h2> <div class="top-buttons"> <button id="addAccountBtn">➕ 添加账号</button> <button id="importBtn">📥 导入 CSV</button> <button id="exportBtn">📤 导出备份</button> <button id="importBackupBtn">📥 从备份恢复</button> <button id="searchBtn">🔍 搜索</button> <button id="changeMasterBtn">🔑 修改主密码</button> <button class="theme-toggle" id="themeToggle">🌓 切换主题</button> </div> <div class="searchInputContainer" id="searchContainer" style="position:relative; margin-top:8px; display:none;"> <input type="text" id="searchInput" placeholder="关键词(空格分隔)..." autocomplete="off" /> </div> <div class="stats"> 总账号数:<span id="totalAccounts">0</span> | 网站数:<span id="totalSites">0</span> <div class="input-with-clear"> <input type="text" id="inputText" placeholder="字符计数器" /> <button type="button" class="clear-btn" id="clearInputBtn">×</button> </div> 字符数:<span id="charCount">0</span> <span>|💡 双击任意行可编辑账号信息,点击密码时临时显示明文</span> </div> <div class="batch-actions" id="batchActions"> <button id="batchDeleteBtn" class="btn-danger">🗑️ 批量删除</button> <button id="batchExportBtn" class="btn-primary">📤 导出选中</button> <span id="selectedCount">0 项已选</span> </div> </div> <table id="accountTable"> <thead> <tr> <th><input type="checkbox" id="selectAll"></th> <th>网站</th> <th>账号</th> <th>密码</th> <th>更新时间</th> <th>备注</th> </tr> </thead> <tbody id="tableBody"></tbody> </table> </div> <!-- 锁屏 / 设置主密码模态框 --> <div id="lockModal" class="modal"> <div class="modal-content"> <h3 id="lockTitle">🔐 设置主密码</h3> <div id="hintDisplay" style="display:none;"></div> <input type="text" id="setupHint" placeholder="设置提示问题(如:你的宠物名字?)" autocomplete="off" /> <input type="password" id="setupAnswer" placeholder="设置答案(即主密码)" autocomplete="off" /> <input type="password" id="confirmAnswer" placeholder="再次输入答案" autocomplete="off" /> <div class="error-message" id="lockError"></div> <div class="modal-buttons"> <button class="btn-success" id="lockConfirm">确定</button> </div> </div> </div> <!-- 修改主密码模态框 --> <div id="changeMasterModal" class="modal"> <div class="modal-content"> <h3>🔑 修改主密码</h3> <input type="password" id="currentAnswer" placeholder="当前主密码(答案)" autocomplete="off" /> <input type="text" id="newHint" placeholder="新的提示问题" autocomplete="off" /> <input type="password" id="newAnswer" placeholder="新主密码(答案)" autocomplete="off" /> <input type="password" id="newAnswerConfirm" placeholder="再次输入新主密码" autocomplete="off" /> <div class="error-message" id="changeMasterError"></div> <div class="modal-buttons"> <button class="btn-cancel" id="changeMasterCancel">取消</button> <button class="btn-success" id="changeMasterConfirm">确认修改</button> </div> </div> </div> <!-- 添加/编辑模态框 --> <div id="addModal" class="modal"> <div class="modal-content"> <h3>➕ 添加新账号</h3> <input type="text" id="modalWebsite" placeholder="网站名(必填)" autocomplete="off" /> <input type="text" id="modalAccount" placeholder="账号(必填)" autocomplete="off" /> <div class="password-wrapper"> <input type="password" id="modalPassword" placeholder="密码(必填)" autocomplete="off" /> <button type="button" class="toggle-password" id="togglePassword">👁️</button> </div> <div class="password-strength"> <div class="password-strength-bar" id="strengthBar"></div> </div> <div class="password-strength-text" id="strengthText">强度:—</div> <input type="text" id="modalNote" placeholder="备注(可选)" autocomplete="off" /> <div class="error-message" id="addError"></div> <div class="modal-buttons"> <button class="btn-cancel" id="addCancel">取消</button> <button class="btn-success" id="addConfirm">添加</button> </div> </div> </div> <!-- 主密码输入模态框(用于保存/导出等操作) --> <div id="masterPasswordModal" class="modal"> <div class="modal-content"> <h3>🔐 请输入主密码</h3> <input type="password" id="masterPasswordInput" placeholder="主密码(答案)" autocomplete="off" /> <div class="error-message" id="masterPasswordError"></div> <div class="modal-buttons"> <button class="btn-cancel" id="masterPasswordCancel">取消</button> <button class="btn-primary" id="masterPasswordConfirm">确认</button> </div> </div> </div> <script> (async () => { // ========== 常量 ========== const DATA_KEY = "encryptedAccountData"; const HINT_KEY = "masterPasswordHint"; const DRAFT_KEY = "passwordManagerDraft"; // ========== 工具函数 ========== function $(id) { return document.getElementById(id); } async function deriveKey(password, salt) { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey"]); return await crypto.subtle.deriveKey( { name: "PBKDF2", salt, iterations: 100000, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); } async function encryptData(data, password) { const salt = crypto.getRandomValues(new Uint8Array(16)); const key = await deriveKey(password, salt); const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = new TextEncoder(); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(JSON.stringify(data))); return { data: Array.from(new Uint8Array(encrypted)), iv: Array.from(iv), salt: Array.from(salt) }; } async function decryptData(encryptedObj, password) { const { data, iv, salt } = encryptedObj; const key = await deriveKey(password, new Uint8Array(salt)); const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: new Uint8Array(iv) }, key, new Uint8Array(data)); const dec = new TextDecoder(); return JSON.parse(dec.decode(decrypted)); } function formatDate(isoString) { if (!isoString) return "—"; return new Date(isoString).toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).replace(/\//g, "-"); } // ========== 全局状态 ========== let fullData = []; let currentEdit = null; // ========== DOM 缓存 ========== const els = { mainApp: $("mainApp"), lockModal: $("lockModal"), lockTitle: $("lockTitle"), hintDisplay: $("hintDisplay"), setupHint: $("setupHint"), setupAnswer: $("setupAnswer"), confirmAnswer: $("confirmAnswer"), lockError: $("lockError"), lockConfirm: $("lockConfirm"), changeMasterModal: $("changeMasterModal"), currentAnswer: $("currentAnswer"), newHint: $("newHint"), newAnswer: $("newAnswer"), newAnswerConfirm: $("newAnswerConfirm"), changeMasterError: $("changeMasterError"), changeMasterConfirm: $("changeMasterConfirm"), changeMasterCancel: $("changeMasterCancel"), masterPasswordModal: $("masterPasswordModal"), masterPasswordInput: $("masterPasswordInput"), masterPasswordError: $("masterPasswordError"), masterPasswordConfirm: $("masterPasswordConfirm"), masterPasswordCancel: $("masterPasswordCancel"), themeToggle: $("themeToggle"), searchBtn: $("searchBtn"), searchContainer: $("searchContainer"), searchInput: $("searchInput"), importBtn: $("importBtn"), exportBtn: $("exportBtn"), importBackupBtn: $("importBackupBtn"), addAccountBtn: $("addAccountBtn"), changeMasterBtn: $("changeMasterBtn"), addModal: $("addModal"), modalWebsite: $("modalWebsite"), modalAccount: $("modalAccount"), modalPassword: $("modalPassword"), togglePassword: $("togglePassword"), modalNote: $("modalNote"), addError: $("addError"), addConfirm: $("addConfirm"), addCancel: $("addCancel"), tableBody: $("tableBody"), selectAll: $("selectAll"), batchActions: $("batchActions"), batchDeleteBtn: $("batchDeleteBtn"), batchExportBtn: $("batchExportBtn"), selectedCount: $("selectedCount"), strengthBar: $("strengthBar"), strengthText: $("strengthText"), totalAccounts: $("totalAccounts"), totalSites: $("totalSites"), clearInputBtn: $("clearInputBtn"), inputText: $("inputText"), charCount: $("charCount") }; // ========== 主密码流程 ========== async function unlockWithAnswer(answer) { try { const raw = localStorage.getItem(DATA_KEY); if (!raw) { els.lockError.textContent = "无加密数据"; return false; } const obj = JSON.parse(raw); fullData = await decryptData(obj, answer); showMainApp(); return true; } catch (e) { els.lockError.textContent = "❌ 答案错误或数据损坏"; return false; } } async function setupMasterPassword() { const hint = els.setupHint.value.trim(); const ans1 = els.setupAnswer.value; const ans2 = els.confirmAnswer.value; if (!hint || !ans1) { els.lockError.textContent = "请填写提示问题和答案"; return; } if (ans1 !== ans2) { els.lockError.textContent = "两次答案不一致"; return; } fullData = []; const encrypted = await encryptData(fullData, ans1); localStorage.setItem(DATA_KEY, JSON.stringify(encrypted)); localStorage.setItem(HINT_KEY, hint); showMainApp(); } function showMainApp() { els.mainApp.style.display = "block"; els.lockModal.style.display = "none"; updateStats(); renderTable(fullData); } els.lockConfirm.addEventListener("click", async () => { const hasData = !!localStorage.getItem(DATA_KEY); const hasHint = !!localStorage.getItem(HINT_KEY); if (hasData && hasHint) { await unlockWithAnswer(els.setupAnswer.value); } else { await setupMasterPassword(); } }); function showLockScreen() { const hasData = !!localStorage.getItem(DATA_KEY); const hint = localStorage.getItem(HINT_KEY); if (hasData && hint) { els.lockTitle.textContent = "🔓 输入主密码"; els.hintDisplay.textContent = `提示:${hint}`; els.hintDisplay.style.display = "block"; els.setupHint.style.display = "none"; els.confirmAnswer.style.display = "none"; els.setupAnswer.placeholder = "请输入答案(主密码)"; } else { els.lockTitle.textContent = "🔐 设置主密码"; els.hintDisplay.style.display = "none"; els.setupHint.style.display = "block"; els.confirmAnswer.style.display = "block"; els.setupAnswer.placeholder = "设置答案(即主密码)"; } els.lockError.textContent = ""; els.setupHint.value = ""; els.setupAnswer.value = ""; els.confirmAnswer.value = ""; els.lockModal.style.display = "flex"; (hasData ? els.setupAnswer : els.setupHint).focus(); } // ========== 修改主密码 ========== async function changeMasterPassword() { const currentAns = els.currentAnswer.value; const newHint = els.newHint.value.trim(); const newAns = els.newAnswer.value; const newAnsConfirm = els.newAnswerConfirm.value; if (!currentAns || !newHint || !newAns) { els.changeMasterError.textContent = "所有字段均为必填"; return; } if (newAns !== newAnsConfirm) { els.changeMasterError.textContent = "新密码两次输入不一致"; return; } try { const encrypted = localStorage.getItem(DATA_KEY); if (!encrypted) throw new Error("无数据"); const obj = JSON.parse(encrypted); const decryptedData = await decryptData(obj, currentAns); const newEncrypted = await encryptData(decryptedData, newAns); localStorage.setItem(DATA_KEY, JSON.stringify(newEncrypted)); localStorage.setItem(HINT_KEY, newHint); els.changeMasterModal.style.display = "none"; alert("✅ 主密码已成功修改!"); location.reload(); } catch (e) { els.changeMasterError.textContent = "❌ 当前主密码错误或操作失败"; } } // ========== 主密码输入通用模态框 ========== function showMasterPasswordModal(callback) { els.masterPasswordError.textContent = ""; els.masterPasswordInput.value = ""; els.masterPasswordModal.style.display = "flex"; els.masterPasswordInput.focus(); const handleConfirm = async () => { const pwd = els.masterPasswordInput.value; if (!pwd) { els.masterPasswordError.textContent = "请输入主密码"; return; } try { callback(pwd); els.masterPasswordModal.style.display = "none"; els.masterPasswordConfirm.removeEventListener("click", handleConfirm); els.masterPasswordCancel.removeEventListener("click", handleCancel); } catch (err) { els.masterPasswordError.textContent = "❌ 主密码错误"; } }; const handleCancel = () => { els.masterPasswordModal.style.display = "none"; els.masterPasswordConfirm.removeEventListener("click", handleConfirm); els.masterPasswordCancel.removeEventListener("click", handleCancel); }; els.masterPasswordConfirm.addEventListener("click", handleConfirm); els.masterPasswordCancel.addEventListener("click", handleCancel); } // ========== 数据操作 ========== function updateStats() { const totalAccounts = fullData.reduce((sum, site) => sum + site.accounts.length, 0); const totalSites = fullData.length; els.totalAccounts.textContent = totalAccounts; els.totalSites.textContent = totalSites; } function renderTable(data) { els.tableBody.innerHTML = ""; const rows = []; data.forEach(site => site.accounts.forEach(acc => rows.push({ website: site.website, account: acc.account, password: acc.password, note: acc.note, updatedAt: acc.updatedAt }) ) ); const siteRowCount = {}; rows.forEach(row => siteRowCount[row.website] = (siteRowCount[row.website] || 0) + 1); const renderedSites = new Set(); rows.forEach(row => { const tr = document.createElement("tr"); tr.dataset.website = row.website; tr.dataset.account = row.account; tr.addEventListener("dblclick", () => showEditModal(row.website, row.account, row.password, row.note)); const checkCell = document.createElement("td"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "row-checkbox"; checkbox.dataset.website = row.website; checkbox.dataset.account = row.account; checkCell.appendChild(checkbox); tr.appendChild(checkCell); const websiteCell = document.createElement("td"); if (!renderedSites.has(row.website)) { websiteCell.textContent = row.website; websiteCell.rowSpan = siteRowCount[row.website]; renderedSites.add(row.website); } else { websiteCell.style.display = "none"; } tr.appendChild(websiteCell); tr.appendChild(createCell(row.account)); const pwdCell = createCell(""); const pwdSpan = document.createElement("span"); pwdSpan.textContent = "••••••"; pwdSpan.style.cursor = "pointer"; pwdSpan.title = "点击显示密码(8秒)"; pwdSpan.onclick = (e) => { e.stopPropagation(); revealPassword(pwdSpan, pwdCell, row.password); }; pwdCell.appendChild(pwdSpan); tr.appendChild(pwdCell); tr.appendChild(createCell(formatDate(row.updatedAt))); tr.appendChild(createCell(row.note || "", "left")); els.tableBody.appendChild(tr); }); bindRowCheckboxes(); updateBatchUI(); } function createCell(text, align = "center") { const td = document.createElement("td"); td.style.textAlign = align; td.textContent = text ?? ""; return td; } function revealPassword(span, parent, password) { span.textContent = password; let sec = 8; const countdown = document.createElement("span"); countdown.className = "countdown"; countdown.textContent = `(${sec}秒后隐藏)`; parent.appendChild(countdown); const iv = setInterval(() => { sec--; if (sec <= 0) { clearInterval(iv); span.textContent = "••••••"; parent.removeChild(countdown); } else { countdown.textContent = `(${sec}秒后隐藏)`; } }, 1000); } function bindRowCheckboxes() { document.querySelectorAll(".row-checkbox").forEach(cb => { cb.removeEventListener("change", handleRowCheckboxChange); cb.addEventListener("change", handleRowCheckboxChange); }); els.selectAll.removeEventListener("change", handleSelectAllChange); els.selectAll.addEventListener("change", handleSelectAllChange); } function handleRowCheckboxChange() { updateBatchUI(); } function handleSelectAllChange() { const checkboxes = document.querySelectorAll(".row-checkbox"); checkboxes.forEach(cb => cb.checked = els.selectAll.checked); updateBatchUI(); } function getSelectedRows() { const selected = []; document.querySelectorAll(".row-checkbox:checked").forEach(cb => { selected.push({ website: cb.dataset.website, account: cb.dataset.account }); }); return selected; } function updateBatchUI() { const checkboxes = document.querySelectorAll(".row-checkbox"); const checkedBoxes = document.querySelectorAll(".row-checkbox:checked"); els.selectAll.checked = checkboxes.length > 0 && checkedBoxes.length === checkboxes.length; const count = checkedBoxes.length; els.selectedCount.textContent = `${count} 项已选`; els.batchActions.classList.toggle("visible", count > 0); } function deleteAccount(website, account) { const site = fullData.find(s => s.website === website); if (!site) return; site.accounts = site.accounts.filter(a => a.account !== account); if (site.accounts.length === 0) { fullData = fullData.filter(s => s.website !== website); } } function batchDelete() { const selected = getSelectedRows(); if (selected.length === 0) return; if (!confirm(`确定要删除选中的 ${selected.length} 个账号吗?此操作不可恢复!`)) return; selected.forEach(({ website, account }) => deleteAccount(website, account)); saveData(); } function batchExport() { const selected = getSelectedRows(); if (selected.length === 0) return; const exportData = []; selected.forEach(({ website, account }) => { const site = fullData.find(s => s.website === website); if (site) { const acc = site.accounts.find(a => a.account === account); if (acc) exportData.push({ website, ...acc }); } }); downloadJSON(exportData, `selected-${new Date().toISOString().slice(0, 10)}.json`); } function handleSearch() { const query = els.searchInput.value.trim().toLowerCase(); if (!query) return renderTable(fullData); const keywords = query.split(/\s+/).filter(k => k); const filtered = fullData.map(site => ({ website: site.website, accounts: site.accounts.filter(acc => keywords.every(k => [site.website, acc.account, acc.note || ""].some(f => f.toLowerCase().includes(k)) ) ) })).filter(s => s.accounts.length > 0); renderTable(filtered); } function toggleSearch() { if (els.searchContainer.style.display === "block") { els.searchContainer.style.display = "none"; els.searchInput.value = ""; handleSearch(); els.searchBtn.textContent = "🔍 搜索"; } else { els.searchBtn.textContent = "❌ 取消"; els.searchContainer.style.display = "block"; els.searchInput.focus(); } } function parseCSVLine(line) { const values = []; let inQuotes = false; let current = ""; for (let i = 0; i < line.length; i++) { const c = line[i]; if (c === '"') { if (inQuotes && line[i + 1] === '"') { current += '"'; i++; } else { inQuotes = !inQuotes; } } else if (c === "," && !inQuotes) { values.push(current); current = ""; } else { current += c; } } values.push(current); return values.map(v => v.replace(/^"(.*)"$/, "$1")); } function importCSV() { const input = document.createElement("input"); input.type = "file"; input.accept = ".csv"; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const text = await file.text(); const lines = text.split(/\r?\n/).filter(l => l.trim()); if (lines.length < 2) return alert("CSV 至少包含标题行和一行数据"); const newData = []; for (let i = 1; i < lines.length; i++) { const vals = parseCSVLine(lines[i]); if (vals.length >= 3) { const [w, a, p, n = ""] = vals; if (w && a && p) newData.push({ website: w, account: a, password: p, note: n }); } } if (newData.length === 0) return alert("未找到有效记录"); const now = new Date().toISOString(); newData.forEach(item => { let site = fullData.find(s => s.website === item.website); if (!site) { site = { website: item.website, accounts: [] }; fullData.push(site); } if (!site.accounts.some(x => x.account === item.account)) { site.accounts.push({ ...item, updatedAt: now }); } }); saveData(); alert(`成功导入 ${newData.length} 条记录`); }; input.click(); } function exportPlainTextBackup() { showMasterPasswordModal(async (pwd) => { try { const encrypted = localStorage.getItem(DATA_KEY); if (!encrypted) throw new Error("无数据"); const obj = JSON.parse(encrypted); const data = await decryptData(obj, pwd); downloadJSON(data, `passwords-backup-${new Date().toISOString().slice(0, 10)}.json`); alert("✅ 明文备份已下载\n⚠️ 此文件包含明文密码,请妥善保管!"); } catch (e) { throw new Error("主密码错误"); } }); } function importPlainTextBackup() { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text); showMasterPasswordModal(async (pwd) => { const encrypted = await encryptData(data, pwd); localStorage.setItem(DATA_KEY, JSON.stringify(encrypted)); alert("✅ 备份已成功导入!\n页面将重新加载。"); location.reload(); }); } catch (err) { alert("❌ 文件无效或主密码错误"); } }; input.click(); } function downloadJSON(data, filename) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // ========== 账号编辑 ========== function evaluatePasswordStrength(pwd) { let score = 0; if (pwd.length >= 8) score++; if (/[a-z]/.test(pwd)) score++; if (/[A-Z]/.test(pwd)) score++; if (/[0-9]/.test(pwd)) score++; if (/[^a-zA-Z0-9]/.test(pwd)) score++; let width = "0%", color = "#ccc", text = "—"; if (score <= 1) { width = "20%"; color = "#ff4444"; text = "弱"; } else if (score <= 2) { width = "40%"; color = "#ffaa00"; text = "中"; } else if (score <= 3) { width = "60%"; color = "#ffcc00"; text = "中强"; } else if (score <= 4) { width = "80%"; color = "#5cb85c"; text = "强"; } else { width = "100%"; color = "#28a745"; text = "很强"; } els.strengthBar.style.width = width; els.strengthBar.style.backgroundColor = color; els.strengthText.textContent = `强度:${text}`; } function saveDraft() { const draft = { website: els.modalWebsite.value, account: els.modalAccount.value, password: els.modalPassword.value, note: els.modalNote.value, isEditing: !!currentEdit, timestamp: Date.now() }; localStorage.setItem(DRAFT_KEY, JSON.stringify(draft)); } function loadDraft() { const raw = localStorage.getItem(DRAFT_KEY); if (!raw) return null; try { const draft = JSON.parse(raw); if (Date.now() - draft.timestamp > 24 * 60 * 60 * 1000) { localStorage.removeItem(DRAFT_KEY); return null; } return draft; } catch { localStorage.removeItem(DRAFT_KEY); return null; } } function clearDraft() { localStorage.removeItem(DRAFT_KEY); } function resetModalToDefault() { document.querySelector("#addModal h3").textContent = "➕ 添加新账号"; els.addConfirm.textContent = "添加"; } function showAddModal() { currentEdit = null; els.addError.textContent = ""; const draft = loadDraft(); if (draft && !draft.isEditing) { els.modalWebsite.value = draft.website; els.modalAccount.value = draft.account; els.modalPassword.value = draft.password; els.modalNote.value = draft.note; evaluatePasswordStrength(draft.password); } else { els.modalWebsite.value = ""; els.modalAccount.value = ""; els.modalPassword.value = ""; els.modalNote.value = ""; els.strengthBar.style.width = "0%"; els.strengthText.textContent = "强度:—"; } els.modalPassword.type = "password"; els.togglePassword.textContent = "👁️"; resetModalToDefault(); els.addModal.style.display = "flex"; els.modalWebsite.focus(); startAutoSave(); } function showEditModal(website, account, password, note) { currentEdit = { website, oldAccount: account }; els.addError.textContent = ""; els.modalWebsite.value = website; els.modalAccount.value = account; els.modalPassword.value = password; els.modalNote.value = note || ""; evaluatePasswordStrength(password); els.modalPassword.type = "password"; els.togglePassword.textContent = "👁️"; document.querySelector("#addModal h3").textContent = "✏️ 编辑账号"; els.addConfirm.textContent = "保存"; els.addModal.style.display = "flex"; els.modalAccount.focus(); startAutoSave(); } function hideAddModal() { els.addModal.style.display = "none"; stopAutoSave(); clearDraft(); } let autoSaveInterval = null; function startAutoSave() { stopAutoSave(); autoSaveInterval = setInterval(saveDraft, 2000); } function stopAutoSave() { if (autoSaveInterval) { clearInterval(autoSaveInterval); autoSaveInterval = null; } } function addAccountFromModal() { const w = els.modalWebsite.value.trim(); const a = els.modalAccount.value.trim(); const p = els.modalPassword.value.trim(); const n = els.modalNote.value.trim(); const now = new Date().toISOString(); if (!w || !a || !p) { els.addError.textContent = "请填写网站、账号和密码"; return; } if (currentEdit) { const { website: oldW, oldAccount } = currentEdit; // 删除旧记录 deleteAccount(oldW, oldAccount); // 添加新记录 let targetSite = fullData.find(s => s.website === w); if (!targetSite) { targetSite = { website: w, accounts: [] }; fullData.push(targetSite); } targetSite.accounts.push({ account: a, password: p, note: n, updatedAt: now }); } else { let site = fullData.find(s => s.website === w); if (!site) { site = { website: w, accounts: [] }; fullData.push(site); } if (site.accounts.some(x => x.account === a)) { els.addError.textContent = "该账号已存在"; return; } site.accounts.push({ account: a, password: p, note: n, updatedAt: now }); } saveData(); hideAddModal(); currentEdit = null; resetModalToDefault(); } function saveData() { showMasterPasswordModal(async (pwd) => { try { const encrypted = await encryptData(fullData, pwd); localStorage.setItem(DATA_KEY, JSON.stringify(encrypted)); renderTable(fullData); updateStats(); } catch (e) { alert("加密保存失败:" + e.message); } }); } function togglePasswordVisibility() { const input = els.modalPassword; const btn = els.togglePassword; if (input.type === "password") { input.type = "text"; btn.textContent = "🙈"; } else { input.type = "password"; btn.textContent = "👁️"; } } // ========== 初始化 ========== function initTheme() { if (localStorage.getItem("theme") === "dark") { document.body.classList.add("dark-mode"); } els.themeToggle.addEventListener("click", () => { document.body.classList.toggle("dark-mode"); localStorage.setItem("theme", document.body.classList.contains("dark-mode") ? "dark" : "light"); }); } function bindEvents() { els.changeMasterBtn.addEventListener("click", () => { els.changeMasterError.textContent = ""; els.changeMasterModal.style.display = "flex"; }); els.changeMasterConfirm.addEventListener("click", changeMasterPassword); els.changeMasterCancel.addEventListener("click", () => els.changeMasterModal.style.display = "none"); els.searchBtn.addEventListener("click", toggleSearch); els.searchInput.addEventListener("input", handleSearch); els.importBtn.addEventListener("click", importCSV); els.exportBtn.addEventListener("click", exportPlainTextBackup); els.importBackupBtn.addEventListener("click", importPlainTextBackup); els.addAccountBtn.addEventListener("click", showAddModal); els.addConfirm.addEventListener("click", addAccountFromModal); els.addCancel.addEventListener("click", hideAddModal); els.togglePassword.addEventListener("click", togglePasswordVisibility); els.modalPassword.addEventListener("input", () => evaluatePasswordStrength(els.modalPassword.value)); els.modalPassword.addEventListener("keypress", (e) => { if (e.key === "Enter") addAccountFromModal(); }); els.batchDeleteBtn.addEventListener("click", batchDelete); els.batchExportBtn.addEventListener("click", batchExport); els.clearInputBtn.addEventListener('click', () => { els.inputText.value = ''; els.charCount.textContent = '0'; els.inputText.focus(); }); els.inputText.addEventListener('input', () => { els.charCount.textContent = els.inputText.value.length; }); } // ========== 启动 ========== initTheme(); bindEvents(); showLockScreen(); })(); </script> </body> </html>密码管理器(我打包成tauri应用把主密码忘了怎么办)
张小明
前端开发工程师
漏洞扫描AWVS安装使用教程,三分钟手把手教会!
一、AWS简介 Acunetix Web Vulnerability Scanner(简称AWVS)是一个自动化的Web漏洞扫描工具,它可以扫描任何通过Web浏览器访问和遵循HITP/HTTPS规则的Web站点。 AWVS原理是基于漏洞匹配方法,通过网络爬虫测试你的网站安全,检测流行安全 AWVS…
绝区零自动化工具全功能实战指南
绝区零自动化工具全功能实战指南 【免费下载链接】ZenlessZoneZero-OneDragon 绝区零 一条龙 | 全自动 | 自动闪避 | 自动每日 | 自动空洞 | 支持手柄 项目地址: https://gitcode.com/gh_mirrors/ze/ZenlessZoneZero-OneDragon 绝区零自动化工具是一款专为《绝区零》玩家…
5分钟掌握Android投屏实用技巧:QtScrcpy全新体验全解析
还在为手机屏幕太小而烦恼?还在寻找高效的跨设备控制方案?QtScrcpy的出现彻底改变了传统投屏方式,这款基于Qt框架开发的Android投屏工具,通过创新的技术架构和丰富的功能特性,为用户带来了前所未有的投屏体验。 【免费…
Lumafly模组管理器:空洞骑士玩家的完整解决方案
Lumafly模组管理器:空洞骑士玩家的完整解决方案 【免费下载链接】Lumafly A cross platform mod manager for Hollow Knight written in Avalonia. 项目地址: https://gitcode.com/gh_mirrors/lu/Lumafly Lumafly是一款专为《空洞骑士》设计的跨平台模组管理…
LobeChat缓存策略优化:减少重复推理开销
LobeChat缓存策略优化:减少重复推理开销 在如今大模型应用遍地开花的时代,一个看似简单的“聊天框”背后,往往隐藏着高昂的算力成本和复杂的工程权衡。以 LobeChat 这类现代化开源对话框架为例,它支持接入 GPT、Claude、通义千问等…
OneNote目录置顶终极方案:一键搞定页面导航难题
OneNote目录置顶终极方案:一键搞定页面导航难题 【免费下载链接】OneMore A OneNote add-in with simple, yet powerful and useful features 项目地址: https://gitcode.com/gh_mirrors/on/OneMore 你有没有遇到过这种情况:在OneNote里写了大量笔…